HW independent driver interface design - I2C - Page 1
Forum section: electronics / general

author: pthor
author_rank: Newbie
timestamp: October 05, 2016, 09:24:24 pm
content: 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:int i2c_master_send(struct i2c_client *client, const char *buf, int count);Code:int i2c_master_recv(struct i2c_client *client, char *buf, int count);Code:int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg, int num);https://www.kernel.org/doc/Documentation/i2c/writing-clientsIn addition to direct functions for most of the SMBus transactions. I think the i2c_transfer(..) seems interesting.QuoteDo 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.

author: andyturk
author_rank: Frequent Contributor
timestamp: October 06, 2016, 12:53:41 am
content: 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.

author: free_electron
author_rank: Super Contributor
timestamp: October 06, 2016, 04:19:18 am
content: here is how i do it :layered approachlow level : these are atomic routines that twiggle the pins.byte sda_low : make sda lowbyte sda_release : make sda tristatebyte scl_low : make scl lowbyte scl_release : make scl tristatebyte read_scl : sample SCLbyte read_sda : sample SDAthe above functions either return 0x00 or 0x40 or 0x80. nothing elseany 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_Startsend_stopsend_byterestartget_ackgive_ackgive_nackreset_busstretch_clockrelease_clockany operation here is also atomic with hard timeouts.returning status bytes are Or-ed with local status bytes. if non-zero : abort entire sequencestretch_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 busreset_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= success0x01= ACK failure within time limit0x02= wrong bus_state0x04= SDA_stucklow0x08= SCL_stucklow0x10= arbitration_loss0x80 = 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)

author: obiwanjacobi
author_rank: Super Contributor
timestamp: October 06, 2016, 05:57:37 am
content: 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

author: nctnico
author_rank: Super Contributor
timestamp: October 06, 2016, 07:55:20 am
content: @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.

author: dannyf
author_rank: Super Contributor
timestamp: October 06, 2016, 02:43:54 pm
content: "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.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: October 06, 2016, 02:54:07 pm
content: Quote from: dannyf on October 06, 2016, 02:43:54 pmThat 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...

author: free_electron
author_rank: Super Contributor
timestamp: October 06, 2016, 03:13:32 pm
content: Quote from: nctnico on October 06, 2016, 07:55:20 amhe 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.

author: Kalvin
author_rank: Super Contributor
timestamp: October 06, 2016, 03:18:58 pm
content: Quote from: obiwanjacobi on October 06, 2016, 02:54:07 pmTypical 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.

author: nctnico
author_rank: Super Contributor
timestamp: October 06, 2016, 04:23:45 pm
content: Quote from: dannyf on October 06, 2016, 02:43:54 pmBad 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).

author: dannyf
author_rank: Super Contributor
timestamp: October 06, 2016, 05:15:20 pm
content: "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.

author: Sal Ammoniac
author_rank: Super Contributor
timestamp: October 06, 2016, 05:55:40 pm
content: Quote from: nctnico on October 06, 2016, 04:23:45 pmDepends 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.

author: pthor
author_rank: Newbie
timestamp: October 06, 2016, 06:56:19 pm
content: Thanks for good ideas and thoughts guys,Quote from: andyturk on October 06, 2016, 12:53:41 amYes, 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.hQuote from: andyturk on October 06, 2016, 12:53:41 amYes, 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.Quote from: free_electron on October 06, 2016, 03:13:32 pmThat 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?

author: pthor
author_rank: Newbie
timestamp: October 06, 2016, 06:59:12 pm
content: Quote from: obiwanjacobi on October 06, 2016, 02:54:07 pmI'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.

author: dannyf
author_rank: Super Contributor
timestamp: October 06, 2016, 08:27:54 pm
content: "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.

author: nctnico
author_rank: Super Contributor
timestamp: October 06, 2016, 08:35:48 pm
content: Quote from: pthor on October 06, 2016, 06:56:19 pmIIRC some devices allow (or even need: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).

author: pthor
author_rank: Newbie
timestamp: October 06, 2016, 09:12:51 pm
content: Quote from: nctnico on October 06, 2016, 08:35:48 pmI 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:i2c_transfer(struct i2c_adapter * adap,struct i2c_msg * msgs,int num);Edit: added quote from below link:Quotehttps://www.kernel.org/doc/htmldocs/device-drivers/API-i2c-transfer.htmlAnyone cleaner?

author: nctnico
author_rank: Super Contributor
timestamp: October 06, 2016, 09:35:19 pm
content: 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

author: senso
author_rank: Frequent Contributor
timestamp: October 07, 2016, 12:00:03 pm
content: Quote from: nctnico on October 06, 2016, 08:35:48 pmYup..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.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: October 07, 2016, 01:18:07 pm
content: Quote from: pthor on October 06, 2016, 06:59:12 pmIn 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: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:// for complex class hierarchies typedefs make it readable againtypedef 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 outthe Arduino Examples in my Library.

author: Kalvin
author_rank: Super Contributor
timestamp: October 07, 2016, 02:27:32 pm
content: Agreed, the C++ templates can generate very efficient code as the compiler's optimizer can see the complete class.