Close

Know how Box0 I2C, SPI is different

A project log for Box0

Free and open source tool for exploring science and electronics anytime anywhere

kuldeep-singh-dhakaKuldeep Singh Dhaka 04/17/2016 at 10:380 Comments

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