We are familiar alot of I2C/SPI adapter.
adapter_start(bla);
adapter_read_nbytes(foo);
adapter_write_nbytes(bar);
adapter_stop();
START bla
bar[0] foo[1] foo[2] ..... # read
RESTART bla
bar[0] bar[1] bar[2] .... # write
STOP
Now, here are some problem with the design.
Problem1
Suppose,
Right after adapter_read_nbyte(), the process moved from executing to waiting state.[process scheduling]
And later the process get CPU back after 1ms or 10ms...
For that wait duration (1ms or 10ms...), I2C (and multi-master SPI) bus is in a state that other master cannot access the bus.
=> Atomicity problem
Because partial state is send after every call.
Problem2
Usually, the commands (over USB) are implemented via blocking EP0 Control transfer.
(alternatively, such facility is implemented over usb2uart based communication which have similar issues)
All control command are send via control EP0. [1]
So, if another process want to use the same device for some other purpose. (example standard command need to be send by kernel or communicate with other interface)
=> Concurrency problem
beacuse EP0 is [in worst case] blocked by the adapter_*() code.
[1] though non EP0 can be used, but lets not get into that and is whole another story
Problem3
(not so big problem)
Adapter hardware specific code.
So, another adapter commonizing library is required.
=> Maintanance problem
Now,
Here is how Box0 I2C/SPI does the work.
Instead of calling adapter_*() like API, another paradigm was designed.
A packet [i call it] transaction which contain all the necessary state.
Something like:
adapter_start(bla);
adapter_read_nbytes(foo);
adapter_write_nbytes(bar);
adapter_stop();
is converted into
[address: bla, read: nbyte, data: foo], [address: bla, write: nbyte, data: foo]
And the whole above is called a transaction. (and read, write are sub-transaction)
(note: The sub-transcation "address" field can vary and if the previous sub-transaction had the same "address" a RESTART is infered instead of START)
START bla
foo[0] foo[1] foo[2] ..... # read
RESTART bla
bar[0] bar[1] bar[2] .... # write
STOP
The transaction is send to the executor (in our case box0-v5).
The executor execute the transaction and return back the state information.
[readed: nbyte, data: bar], # if all bytes are written, success [written: nbyte] # if all (nbytes) written, success
So, by sharing full state information (that are complete in itself) there is no way of enter unknown state.
Solution of Problem1
Even if the process is moved into wait state. Bus is not affected at all.
Because,
either the transaction is executed and status is returned back (when the process get the CPU back).
Or the transaction is executed when the process get the CPU back.
Solution of Problem2
Since the control endpoint is only used for querying the status of the transaction (finished executing or not), it does not block the control endpoint.
also, backoff algorithm is used to prevent repeated querying.
Solution of Problem3
Since the state paradigm is independent of hardware.
It store lower level description of what need to be done & is complete in itself.
More portable
Adding sugar
Inside libbox0, there is a API to accept and send transaction to executor,
for I2C: b0_i2c_start()
for SPI: b0_spi_start()
Now, based on the transaction API, high level function are also build.
b0_i2c_read() - read n byte
b0_i2c_write() - write n byte
b0_i2c_write8read - write 8bit and then read n byte (commonly used)
Optionally: the backends (inside libbox0) can implement these too, if it want to.
Tip: The full specification is given in Box0 USB Specification and you can explore libbox0 code too.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.