A better approach for transaction based I/O

Most I/O in a microcontroller (like in an Arduino board) consists of transactions. The microcontroller sends a command or a command plus data to a peripheral device and then, perhaps after a short delay, the device sends a response of status and data back to the microcontroller.

The Arduino library tends to have only blocking functions — those that only return when the transaction is complete. This makes it impossible to do more than one thing at a time. The library functions also do the buffering, sometimes double buffering, meaning time is wasted copying data and space is wasted in excess buffer sizes.

So for the new book I’m writing, I am creating new interface functions for the SPI and I2C/TWI interfaces which are the main offenders. There will be others, such as MODBUS over RS485 that are also transaction based. I’ve settled on a common interface format that involves two functions for each device for handling transactions.

The first function is a status check. It returns a value of 0 if the device is busy and a non-zero value if it is not busy. This value may also contain an error code for the previous transaction. Since attempting a transfer will block if the device is busy, this function allows checking prior to the transfer call. This function is also used to see if the previous transfer has completed, although a callback function can be used instead, eliminating the need to poll.

The function that does the transfer has the following arguments:

  • Device address (if needed by protocol — I2C)
  • Address of data to send
  • Length of data to send
  • Address of received data
  • Length of data to receive
  • Address of variable to store actual length of data received (if required by protocol)
  • Callback function — called on completion and gets passed address of received data and (if needed by protocol) error code and/or actual length of data received.

Some things to note:

  • The driver does no data buffering. Any buffering needed by the application must be done by the application program.
  • Send and receive data may be at the same address for all devices and protocols I’m looking at. If either send or received data is not needed, the corresponding pointer can be NULL and length can be 0.
  • The callback function can be used to start the next transaction, allowing a chain of transactions to be set up and run “in the background”.

So far this seems to dramatically simplify code while improving performance in the examples I’ve tried. Slave operation of I2C already uses callback functions and needs little change.