EEVblog Electronics Community Forum

Electronics => Microcontrollers => Topic started by: pthor on October 05, 2016, 09:24:24 pm

Title: HW independent driver interface design - I2C
Post by: pthor on October 05, 2016, 09:24:24 pm
Hello,

I'm in the middle of my first proper MCU project and I am starting to write my first I2C device drivers. (Drivers for I2C devices, not for the microcontrollers I2C module)

Around the vendor supplied MCU drivers I am creating a thin hardware/mcu independent wrapper interface. This is with the purpose of writing application that is MCU/hardware independent and to be able to develop that code supported by unit tests and mocks on desktop system. This seems to work fine, but since I don't have that much experience in writing I2C driver nor designing such API interfaces, I would like to ask you guys for your advice and opinions.

I believe the design goals would be:
- As narrow API as reasonable. So all accessing drivers would use the limited sets same commands
- A interface that is good enough to support most needs from a variety of devices -> so that there will be little need to change the interface in later on.

What would be the optimal I2C HAL/API interface for you?

I have not tought much about open() / close() and eventual interupt driven callbacks, but for covering write/read variations.

I took at what interface Linux kernel supplies:

Code: [Select]
int i2c_master_send(struct i2c_client *client, const char *buf, int count);
Code: [Select]
int i2c_master_recv(struct i2c_client *client, char *buf, int count);
Code: [Select]
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg, int num);https://www.kernel.org/doc/Documentation/i2c/writing-clients (https://www.kernel.org/doc/Documentation/i2c/writing-clients)

In addition to direct functions for most of the SMBus transactions. I think the i2c_transfer(..) seems interesting.

Quote
This sends a series of messages. Each message can be a read or write,
and they can be mixed in any way. The transactions are combined: no
stop bit is sent between transaction. The i2c_msg structure contains
for each message the client address, the number of bytes of the message
and the message data itself.

Do I read it right, that with these three functions, I could basically cover all my I2C write/read needs? I have for example now a command i2c_writeNoStop(..) to mimic the start of a combined write+read transaction. But the Linux i2c_transfer interface would cover such transaction, and other needs?

Any experiences with using such interface or would you recommend an other layout?

All opinions are appreciated, Thanks.
Title: Re: HW independent driver interface design - I2C
Post by: andyturk on October 06, 2016, 12:53:41 am
If you plan to use I2C devices in a multi-threaded system, don't forget that an I2C peripheral might be shared between multiple threads.

E.g., suppose you have two slaves hooked up to a single I2C bus and one thread per slave device to deal with data. You'll need to make sure that one thread doesn't try to start a transaction if the bus is already in use by the other thread.

This is fairly easy to code up with mutexes, but adding those into system that wasn't designed for them can be tricky. Give the multi-threaded use case some thought now while you're designing your API.
Title: Re: HW independent driver interface design - I2C
Post by: free_electron on October 06, 2016, 04:19:18 am
here is how i do it :

layered approach


low level : these are atomic routines that twiggle the pins.
byte sda_low   : make sda low
byte sda_release : make sda tristate
byte scl_low : make scl low
byte scl_release : make scl tristate
byte read_scl : sample SCL
byte read_sda : sample SDA
the above functions either return 0x00 or 0x40 or 0x80. nothing else
any state change is verified. if the bus does not physically go to the desired state : return 0x40 or 0x80.

----------------------------------------
medium level can only call low level. medium level is a hard boundary. anything higher up can only use this level. the low level is completely blocked off.
medium level :
set_timeout (microseconds)
Send_Start
send_stop
send_byte
restart
get_ack
give_ack
give_nack
reset_bus
stretch_clock
release_clock

any operation here is also atomic with hard timeouts.
returning status bytes are Or-ed with local status bytes. if non-zero : abort entire sequence

stretch_clock sets a flag to lock the SCK hard low after byte transfer for a given duration.
release_clock returns clock to high level and resets that flag. this allow a master to retain control over the clock and lock the bus
reset_bus keeps toggling the clock until SDA and SCL both sample HIGH. At that point a single START , followed by a single STOP is sent after which both SDA and SCL should be high again.
--------------------------------------

high level : these routine ONLY call into medium level. they NEVER call low level. these routines are also coded as atomic ( non-interruptable)


byte Write_byte (slave_address,data)
byte Write_register (slave_address,register_address,data)
byte read_byte (slave_address,*data)
byte read_register(slave_address,register_address,*data)

byte write_array(slave_address,base_address,*datablock,length)
byte read_array(slave_address,base_address,*datablock,length)

return data :

0x00= success
0x01= ACK failure within time limit
0x02= wrong bus_state
0x04= SDA_stucklow
0x08= SCL_stucklow
0x10= arbitration_loss
0x80 = transport in progress  : if another thread tries the call it is rejected you can trap this higher up.

doing this makes a complete thread safe library. you wont have induced bus contention. lockups can be cleared. and multimaster is handled as well as you can trap arbitration loss. your transport routine can retry automatically by calling itself if an arbitration_loss is detected (while (not success) blah)
Title: Re: HW independent driver interface design - I2C
Post by: obiwanjacobi on October 06, 2016, 05:57:37 am
I have not written I2C (yet) but I have written a usart API (for AVR)..... in C++ ... with templates....
As an API design, I (still) think a subset of C++ (templates) will provide a very good basis to create very intuitive APIs.

For inspiration in API design:
http://atl.codeplex.com/SourceControl/latest#Source/Code/ArduinoTemplateLibrary/AVR/Usart.h (http://atl.codeplex.com/SourceControl/latest#Source/Code/ArduinoTemplateLibrary/AVR/Usart.h)

[2c]
Title: Re: HW independent driver interface design - I2C
Post by: nctnico on October 06, 2016, 07:55:20 am
@Free_electron: why go for bit-banging if every modern microcontroller has a hardware I2C port?

In my experience having the following functions is the most flexible:
i2c_start(address, write)
i2c_read(buffer, count)
i2c_write(buffer, count)
i2c_stop()

With these 4 functions you can create any type of I2C transfer at a higher level.
Title: Re: HW independent driver interface design - I2C
Post by: dannyf on October 06, 2016, 02:43:54 pm
"What would be the optimal I2C HAL/API interface for you? "

What's optimal is highly subjective. Being a slow protocol, i2c is best done over interrupts so your MCU doesn't wait for the transmission.

With that said, the read and write routines will cover most of you need - I happen to use them myselves, with the exception that I use a i2c-select function to pick the slave sddress .

You should also write the code so that you can layer in lower level i2c calls. With this approach, you can switch between software approach and hardware approach, without changes to user code. Ie. Try to avoid commingling implementation code into user code.
Title: Re: HW independent driver interface design - I2C
Post by: obiwanjacobi on October 06, 2016, 02:54:07 pm
You should also write the code so that you can layer in lower level i2c calls. With this approach, you can switch between software approach and hardware approach, without changes to user code. Ie. Try to avoid commingling implementation code into user code.

That is why I love the C++ template way of coding: very flexible and extensible - allows the dev to be in control.

But how would you accomplish that in plain C? - what most embedded coders on this forum seem to prefer...
Title: Re: HW independent driver interface design - I2C
Post by: free_electron on October 06, 2016, 03:13:32 pm
@Free_electron: why go for bit-banging if every modern microcontroller has a hardware I2C port?

In my experience having the following functions is the most flexible:
i2c_start(address, write)
i2c_read(buffer, count)
i2c_write(buffer, count)
i2c_stop()

With these 4 functions you can create any type of I2C transfer at a higher level.

he didnt specify what micro and wants a hardware independent library.
if your controller has hardware i2c then you throw out the lower and intermediate layer and provide an intermediate layer that interfaces witht he bus controller. the toplayer remains unchanged.

Title: Re: HW independent driver interface design - I2C
Post by: Kalvin on October 06, 2016, 03:18:58 pm
But how would you accomplish that in plain C? - what most embedded coders on this forum seem to prefer...

Typical method is to use function pointers. It is quite easy to do simple OOP with a C when you stuff the object data and the function pointers into a struct and pass the pointer to the struct as the first argument to the function. This pointer will be similiar to the C++'s this pointer.
Title: Re: HW independent driver interface design - I2C
Post by: nctnico on October 06, 2016, 04:23:45 pm
Being a slow protocol, i2c is best done over interrupts so your MCU doesn't wait for the transmission.
Bad idea because it will make the programming much harder AND you'll have to somehow wait for the transaction to finish. Also reading 16 bits from an I2C device at 100kHz takes about 350us. You have to make the 'doing something else' very efficient not to waste more than 350us!

When it comes to SPI and I2C transactions I just wait until the transaction ends because usually you want to read something back at the point the transaction starts. If there is timing critical stuff I put that in the timer interrupt (higher priority parallel process).
Title: Re: HW independent driver interface design - I2C
Post by: dannyf on October 06, 2016, 05:15:20 pm

"But how would you accomplish that in plain C? - "

Quite a fee ways of doing. Kalvin mentioned func pointers, or call back functions.

I sometimes simply copy the right modules into the project, knowing that the functions have the same names. So i2cwrite() always writes data to the bus, using either hardware or software routines, depending on what's in that project folder.

The key is to build your user code on s logic programming model that minimizes hardware specificities. My user code is often layers away from the hardware.

For example, my software i2c is built on a set of i2chigh and i2clow routines, which are in trurn built on ioin and iolow routines, which are in turn built on plate form specific gpio libraries that I link in at time of build.

What this does for me is that I can freely change out the underlying modules and my code will still run.

Hope it helps.
Title: Re: HW independent driver interface design - I2C
Post by: Sal Ammoniac on October 06, 2016, 05:55:40 pm
Being a slow protocol, i2c is best done over interrupts so your MCU doesn't wait for the transmission.
Bad idea because it will make the programming much harder AND you'll have to somehow wait for the transaction to finish. Also reading 16 bits from an I2C device at 100kHz takes about 350us. You have to make the 'doing something else' very efficient not to waste more than 350us!

Depends on what infrastructure you have available to support the driver. As soon as my I2C driver starts the I/O (usually by writing to the peripheral register that issues a START on the I2C bus) it calls the RTOS "sleep on event" function. The ISR is implemented as a simple state machine that sequences through the I2C bus states and eventually calls the "wake up tasks waiting on event" function. Each interrupt is short and fast and the CPU can execute other tasks while all this is happening.
Title: Re: HW independent driver interface design - I2C
Post by: pthor on October 06, 2016, 06:56:19 pm
Thanks for good ideas and thoughts guys,

If you plan to use I2C devices in a multi-threaded system, don't forget that an I2C peripheral might be shared between multiple threads.
...

Yes, that is a good point, and I know I have to account for that. The interface should also be able to abstract the bus instance itself, so one can easily organize use of several devices on several different buses. I see that the linux i2c interface also handles that nicely with the i2c_adapter struct, which of many things has the mutex for the bus.
http://lxr.free-electrons.com/source/include/linux/i2c.h (http://lxr.free-electrons.com/source/include/linux/i2c.h)

... Give the multi-threaded use case some thought now while you're designing your API.

Yes, I appreciate that you point this out. As I have not really figured out what type of scheduling I will run in this application yet, I have not put that much thought into it. And I should. The interface should be flexible enough to handle multi threading.
@Free_electron: why go for bit-banging if every modern microcontroller has a hardware I2C port?

In my experience having the following functions is the most flexible:
i2c_start(address, write)
i2c_read(buffer, count)
i2c_write(buffer, count)
i2c_stop()

With these 4 functions you can create any type of I2C transfer at a higher level.

he didn't specify what micro and wants a hardware independent library.
if your controller has hardware i2c then you throw out the lower and intermediate layer and provide an intermediate layer that interfaces with he bus controller. the top layer remains unchanged.

That is right free_electron, the focus for this discussion is on the higher level interface. I.e. providing the most narrow but still flexible interface possible. If the underlying implementation is a hardware serial controller or software implementation using GPIO, it should not affect for this interface.

nctnico: I could get rid of these i2c_start() and i2c_stop() commands right, if I embed them in the implementation of read() and write, in addition to one function that can generate all the combined transaction.

Which brings me back to one question about the linux example: Do you agree that with this i2c_transfer() call, I could build up all types of transactions that I would have need for? Similar to what nctnio mention, but with then hiding start() and stop()

Any strange bus transaction types that I have not thought about?

Title: Re: HW independent driver interface design - I2C
Post by: pthor on October 06, 2016, 06:59:12 pm
You should also write the code so that you can layer in lower level i2c calls. With this approach, you can switch between software approach and hardware approach, without changes to user code. Ie. Try to avoid commingling implementation code into user code.

That is why I love the C++ template way of coding: very flexible and extensible - allows the dev to be in control.

But how would you accomplish that in plain C? - what most embedded coders on this forum seem to prefer...

I'm just curious, in this context, what problem here would you like to solve using templates. Any simple examples? I'm currently implementing this in C, but still curious.
Title: Re: HW independent driver interface design - I2C
Post by: dannyf on October 06, 2016, 08:27:54 pm
"simple state machine t"

NXP provided such examples , through call back functions, in their lpc210x data sheet.

I think atmel bought the same i2c IP and has simikiar examples in some data sheet.

That's more complex but is the way to go in a real implementation. Most of vendor libraries are written that way now.
Title: Re: HW independent driver interface design - I2C
Post by: nctnico on October 06, 2016, 08:35:48 pm

In my experience having the following functions is the most flexible:
i2c_start(address, write)
i2c_read(buffer, count)
i2c_write(buffer, count)
i2c_stop()

With these 4 functions you can create any type of I2C transfer at a higher level.
nctnico: I could get rid of these i2c_start() and i2c_stop() commands right, if I embed them in the implementation of read() and write, in addition to one function that can generate all the combined transaction.
IIRC some devices allow (or even need: http://www.i2c-bus.org/repeated-start-condition/ (http://www.i2c-bus.org/repeated-start-condition/)) to ommit the stop between writing the address and reading the data. You can't accomodate that in the device driver if you have no control over starts & stops. Because I2C handling usually is very device specific I use these 4 functions to create a specific device driver (for an ADC, eeprom, sensor, whatever).
Title: Re: HW independent driver interface design - I2C
Post by: pthor on October 06, 2016, 09:12:51 pm
IIRC some devices allow (or even need: http://www.i2c-bus.org/repeated-start-condition/ (http://www.i2c-bus.org/repeated-start-condition/)) to ommit the stop between writing the address and reading the data. You can't accomodate that in the device driver if you have no control over starts & stops. Because I2C handling usually is very device specific I use these 4 functions to create a specific device driver (for an ADC, eeprom, sensor, whatever).

I understand, but having control of the start and stop signaling does not mean I have to expose it on the application code interface? I could implement the interface by using your functions, yes.

In your example, even a simple write transaction need three separate function calls, and the user could mess up by forgetting to call stop().

I think I get it now regard this linux i2c_transfer function, which is also supposed to handle repeated starts, one would just prepare an array of write/read messages and pass it to the:

Code: [Select]
i2c_transfer(struct i2c_adapter * adap, struct i2c_msg * msgs, int num);
Edit: added quote from below link:

Quote
msgs
One or more messages to execute before STOP is issued to terminate the operation; each message begins with a START.
https://www.kernel.org/doc/htmldocs/device-drivers/API-i2c-transfer.html (https://www.kernel.org/doc/htmldocs/device-drivers/API-i2c-transfer.html)

Anyone cleaner?
Title: Re: HW independent driver interface design - I2C
Post by: nctnico on October 06, 2016, 09:35:19 pm
IMHO it would cause a lot of overhead and you'd still be sorting out start/stops at the device driver level so the 'higher level' interface doesn't improve things from a functional perspective.
See how many flags you can have and how complicated it will be to implement an I2C driver and higher level device driver which complies to the 'specs'. Which specs? There is no detailed description for each flag and their interactions/priorities; all you can do is make an educated guess but someone else will interpret the meaning of the flags differently:
http://lxr.free-electrons.com/source/include/uapi/linux/i2c.h#L68 (http://lxr.free-electrons.com/source/include/uapi/linux/i2c.h#L68)
Title: Re: HW independent driver interface design - I2C
Post by: senso on October 07, 2016, 12:00:03 pm

In my experience having the following functions is the most flexible:
i2c_start(address, write)
i2c_read(buffer, count)
i2c_write(buffer, count)
i2c_stop()

With these 4 functions you can create any type of I2C transfer at a higher level.
nctnico: I could get rid of these i2c_start() and i2c_stop() commands right, if I embed them in the implementation of read() and write, in addition to one function that can generate all the combined transaction.
IIRC some devices allow (or even need: http://www.i2c-bus.org/repeated-start-condition/ (http://www.i2c-bus.org/repeated-start-condition/)) to ommit the stop between writing the address and reading the data. You can't accomodate that in the device driver if you have no control over starts & stops. Because I2C handling usually is very device specific I use these 4 functions to create a specific device driver (for an ADC, eeprom, sensor, whatever).

Yup..
Using about 10 i2c sensors in a project right now, each sensor wants a combination of start, repeated start and stop between address and reading/writing, some also want to see ack's or nack's in the mix, so all my "drivers" use just the basic i2c routines and each chip as its own dedicated function to read and write registers to them.
Title: Re: HW independent driver interface design - I2C
Post by: obiwanjacobi on October 07, 2016, 01:18:07 pm
You should also write the code so that you can layer in lower level i2c calls. With this approach, you can switch between software approach and hardware approach, without changes to user code. Ie. Try to avoid commingling implementation code into user code.

That is why I love the C++ template way of coding: very flexible and extensible - allows the dev to be in control.

But how would you accomplish that in plain C? - what most embedded coders on this forum seem to prefer...

I'm just curious, in this context, what problem here would you like to solve using templates. Any simple examples? I'm currently implementing this in C, but still curious.

In my view using C++ template results in cleaner code AND more efficient code with respect to layering and extensibility etc.

In my library (signature) I use an upside-down class hierarchy to eleminate virtual functions. So no overhead and more efficient than the C suggestion of using function pointers / callbacks.

I basically stack the code layers on top of each other and let the compiler optimize it into pretty efficient code (at least with the GCC AVR compiler).

So for example:
Code: [Select]
template <class BaseT>
class WireProtocol : public BaseT
{
  // hi level functions for sending data across I2C
  // implementation calls only a couple of basic methods on BaseT
}

class WireDriverForMCU_x
{
   // implements basic methods specific for MCU-x
}

Usage:
Code: [Select]
// for complex class hierarchies typedefs make it readable again
typedef MyI2C WireProtocol<WireDriverForMCU_x>;

MyI2C myI2C;
myI2C.CallProtocolMethod(someParam);

The advantage is that the protocol does not have to change to use a different driver class (MCU specific). No overhead is introduced even when using classes. Because no member data or virtuals are used. Adding member data does add extra allocation size for each instance, but that is usually what you want and expect.
Very important in this design is how the different layers talk to eachother and that these methods will allow for exchanging parts of the class hierarchy stack. Another core design principle is the single responsibility principle.

In my library I have simple classes for working with arrays, but also more complex classes for working with a LCD or a USART...

Want to know more: check out the Arduino Examples in my Library (http://atl.codeplex.com/SourceControl/latest#Source/Code/Arduino Libraries/ATL_HD44780/examples/LCD/LCD.ino).
Title: Re: HW independent driver interface design - I2C
Post by: Kalvin on October 07, 2016, 02:27:32 pm
Agreed, the C++ templates can generate very efficient code as the compiler's optimizer can see the complete class.  :-+