Software driver SPI I/O expander

Started by steven02 3 years ago5 replieslatest reply 3 years ago234 views


I have been facing a task to develop a software driver in C++ for a chip which makes available 8 digital inputs over the SPI bus. In the time being I have been thinking about the interface of the driver. The idea which I have is

class DigitalInputsDriver
  enum class Input {
  enum class State 
  DigitalInputsDriver(Spi *spiDriver);
  void initialize(void);
  State readInput(Input input);

  Spi *spiDriver;

So the fact that the digital inputs are on the SPI is hidden for the client's code. The problem which I have is how to implement the readInput method. 

First idea which I had was to implement the readInput method as a blocking i.e. client's code stucks in this method until the SPI transaction for reading the digital inputs finishes. This idea seems to me not appropriate due to the blocking nature.

Second idea was to use the interrupt and callback function mechanism. This solution seems to me better from the software timing point of view but I don't know how to precisely implement it. Because in that case the readInput can't return state of the digital input due to the fact that its call only starts the SPI transaction for reading the digital inputs.

Another idea which I had was to add some public method (let's say refresh()) into the driver's interface. This method will be called periodically and will invoke SPI transaction for reading the digital inputs and based on them will fill some internal variable of the driver (copy of the remote registers in the chip). The readInput will access to the internal variable.

Does anybody think that some of my ideas is usable or does anybody know better solution? Thanks in advance for any suggestions.

#c++, #spi, #driver, #oop

[ - ]
Reply by s-lightNovember 29, 2020

hello steven,

i think what way to go is mainly a architectural & style decision and should therefore involve the surrounding code as basis...

my favorite is the last version - there is a `update` or `refresh` method that initiate the reading. if you have an interrupt for the read-result this can populate internal variables.
from there on you can go several ways:
1. callback:
 on the next invocation of `update` this finds a changed value on pin x and runs the callback function.

2. readInput returns the current internal state.
2.5. readInputLast returns the last value - if there has a new reading occurred - otherwise nothing..

in my arduino libraries i look to implement a uniform api that is basically
the `update`and callback style. often i do the full sensor request in the update call. so it is a blocking call as the sensor do not have a interrupt pin available... as most of the project i do are in some kind art related i have no *real-time* or really *fast-response* timing requirements..

hope that helps in some kind..

sunny greetings


[ - ]
Reply by hodgecNovember 29, 2020

Hi Steven02,

Usually the SPI interface is running at speeds in the MHz range.  Assuming 8 MHz then the transfer time is 1us per byte.  Depending on the amount of data to be transfer and the application requirements, the block time will likely be mute.  I often do these SPI routines that way and adjust the transfer rate to give me a small transfer time, assuming the hardware can support it.

However, if the block time can not be tolerated, then I typically write these to run continuous in the background using a periodic interrupt.  When written in this manner, you have to ensure that user access to the data is atomic.  This can be done using atomic operations or by disabling the interrupt during access. When using this method there still exist the fact that the user application could call the readInput before it is valid.  This can only occur during the initial start-up, and will correct itself on all subsequent reads. As long as that is understood by the user then I is not an issue.   However, you can also create and isValid() or isReady() function if you prefer the user not have to deal with it.  

I have also used the third method you descried except I call it run() or threadProc().  In these systems, I'm using a design method called "run to completion" where the functions are similar to task that are called from a super loop.  Each function does only what it has to do and never blocks.  So internally there is a state.  The state can be say sending, waiting, reading ect.  In this case if the state is waiting then the function checks to see it data is ready.  If it is ready it changes state to reading otherwise it immediately returns.

Keep in mind, if these digital inputs are not de-bounced, then the user is will have to read the inputs multiple times in order to de-bounce them.

I have not really answered your question because you have state how the hardware works.  In most cases the required data is returned during the write transfer.  However that are cases when one writes a command and the data is return later.  In this case the driver must periodic poll the device for the data.  I'm not clear on how yours system works.

[ - ]
Reply by matthewbarrNovember 29, 2020

Without knowing the real time requirements of your application it is hard to be very emphatic about a recommended approach. You are absolutely correct to worry about blocking and real time issues.

One approach is to simply block while you perform the SPI interface read as you've mentioned. Your application may or may not be sensitive to this, I would guess the time to be in the sub-millisecond ballpark. You should be able to bound the maximum read delay based on your hardware and decide whether or not you can tolerate it.

Another approach is to run an interrupt that periodically samples input state. The call to read data will return immediately with the last sampled state. Depending on your real time requirements you can adjust the interrupt rate, making a trade-off between interrupt processing overhead and maximum delay from last sample. Your initialization method can allow the application to configure the interrupt rate. This is similar to your refresh() idea but cleaner I think, it eliminates the refresh() method from your interface and relieves the application of the burden of calling it periodically.

Yet another approach is the callback model, again as you've mentioned. The call to read data initiates the read and returns immediately with no data. When data is available, an application-supplied callback function is invoked to return the data. This is probably the best performing solution, it is non-blocking with minimal interrupt overhead and returns fresh read data state. It is a more complex implementation from the application perspective, and you have to handle the case where read data is called while a previous read is in progress. You can look a variety of RTOS examples to get an idea how you might implement a callback mechanism.

So I guess what I'm saying is that I think all of your suggestions have merit! Your chosen approach really depends on your application requirements. You also have the option of implementing a combination of these approaches. You could have two read methods, one that returns data, another that makes a callback. The initialization method could have the option of configuring or disabling the periodic interrupt, and the read method that returns data could default to blocking if the periodic interrupt is disabled.

If I had to go with just one I'd use the periodic interrupt approach, assuming your real time read data latency requirements are not excessively tight to the point where you have to crank the interrupt rate way up to meet them. This approach is easy to implement and the application interface is straightforward.

[ - ]
Reply by DilbertoNovember 29, 2020

HI,  steven02!

The last method you quoted is used by industrial PLCs ( Programmable Logic Controllers ).

Seems the best to me.


[ - ]
Reply by CustomSargeNovember 29, 2020

Howdy, I'm not a good C coder, so no help there. But SPI can run 12MBps while I2C is much slower. My point is, from a high level language, a well written transaction should be nearly transparent. I'll suggest 2 functions, both calling chunks of assembler: one is passed a mask to return a single bit as true/false and another returns a byte wide integer. All do the read, then parse for call type. Good Hunting <<<)))