I2C is serial communication bus. It is very popular in embedded development and mostly used to communicate to various low speed peripherals, such as eeproms and various sensors.
The control and use of I2C fits nicely into the Device-Driver-Component model described in this book. It is a serial interface and the controlling Device object will have to read/write characters one by one, just like it was with UART. It would be nice if we coud reuse the Character Driver we implemented before. However, the I2C is multi-master / multi-slave bus and there is a need to specify the slave ID (or address) when initiating read and/or write operation.
ID Adaptor
It is quite clear that some kind of ID Device Adaptor is needed. It will be constructed with additional ID parameter and will be responsible to forward all the API calls from the Character Driver to I2C Device while adding one extra parameter of ID.
The implementation of such adaptor is very simple and straightforward:
template <typenameTDevice>classIdAdaptor{public: // Type of the underlaying device.typedef TDevice Device; // Character type defined in the wrapped devicetypedeftypename TDevice::CharType CharType; // Device identification type defined in the wrapped device class.typedeftypename TDevice::DeviceIdType DeviceIdType;IdAdaptor(Device& device,DeviceIdType id):device_(device),id_(id) { }template <typenameTFunc>voidsetCanReadHandler(TFunc&& func) {device_.setCanReadHandler(id_, std::forward<TFunc>(func)); }template <typenameTFunc>voidsetCanWriteHandler(TFunc&& func) {device_.setCanWriteHandler(id_, std::forward<TFunc>(func)); }template <typenameTFunc>voidsetReadCompleteHandler(TFunc&& func) {device_.setReadCompleteHandler(id_, std::forward<TFunc>(func)); }template <typenameTFunc>voidsetWriteCompleteHandler(TFunc&& func) {device_.setWriteCompleteHandler(id_, std::forward<TFunc>(func)); }template <typename... TArgs>voidstartRead(TArgs&&... args) {device_.startRead(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>boolcancelRead(TArgs&&... args) {returndevice_.cancelRead(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>voidstartWrite(TArgs&&... args) {device_.startWrite(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>boolcancelWrite(TArgs&&... args) {returndevice_.cancelWrite(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>boolsuspend(TArgs&&... args) {returndevice_.suspend(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>voidresume(TArgs&&... args) {device_.resume(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>boolcanRead(TArgs&&... args) {returndevice_.canRead(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>boolcanWrite(TArgs&&... args) {returndevice_.canWrite(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>CharTyperead(TArgs&&... args) {returndevice_.read(id_, std::forward<TArgs>(args)...); }template <typename... TArgs>voidwrite(TArgs&&... args) {device_.write(id_, std::forward<TArgs>(args)...); }private: Device& device_; DeviceIdType id_;};
The I2C protocol allows existence of multiple independent slaves on the same bus. It means there may be several independent Components that communicate to different I2C devices (for example EEPROM and temperature sensor), but must share the same Device control object and may issue read/write requests to it in parallel. To resolve this problem, there must be some kind of operation queuing facility that is responsible to queue all the read/write requests to the Device and issue them one by one.
The objects' usage map looks like this:
Such queue is a platform/product independent piece of code and it should be implemented without using dynamic memory allocation and/or exceptions. It means that it should receive number of various Driver objects, that may issue independent read/write requests to it (i.e. size of the internal queue), as a template parameter and probably use Static (Fixed Size) Queue to queue all the requests that are coming in. It should also receive callback storage types to report when a new character can be read/written, as well as when read/write operation is complete.
When the TSize template parameter is set to 1, there is no need for all the queuing facility and the DeviceOpQueue class may become a simple pass-through inline class using template specialisation:
template <typenameTDevice>classDeviceOpQueue<TDevice,1>{public:typedeftypename TDevice::PinIdType PinIdType;template <typename... TArgs>voidstartRead(TArgs&&... args) {device_.startRead(std::forward<TArgs>(args)...) }template <typename... TArgs>boolcancelRead(PinIdType id,TArgs&&... args) {static_cast<void>(id); // No use for id in the Device itselfreturndevice_.cancelRead(std::forward<TArgs>(args)...) }template <typename... TArgs>boolsuspend(PinIdType id,TArgs&&... args) {static_cast<void>(id); // No use for id in the Device itselfreturndevice_.suspend(std::forward<TArgs>(args)...) } ...};
Please note that ID Adaptor and Operations Queue are both Device layer classes. The serve as wrappers to actual peripheral control Device in order to expose the right interface to the upper layer Driver.
I2C Device
The only thing that remains is to properly implement I2C control device, which can be used by the DeviceOpQueue, which in turn is used by the IdAdaptor. The IdAdaptor object can be used with the existing CharacterDriver implemented to be used with the UART peripheral.
Based on the information above, the platform specific I2C control Device object must provide the following public interface:
classI2CDevice{public: // Single character typetypedef std::uint8_t CharType; // ID typetypedef std::uint8_t DeviceIdType; // Context typestypedef embxx::device::context::EventLoop EventLoopContext;typedef embxx::device::context::Interrupt InterruptContext; // Set various interrupt handlerstemplate <typenameTFunc>voidsetCanReadHandler(TFunc&& func);template <typenameTFunc>voidsetCanWriteHandler(TFunc&& func);template <typenameTFunc>voidsetReadCompleteHandler(TFunc&& func);template <typenameTFunc>voidsetWriteCompleteHandler(TFunc&& func); // Start read for both contexts.voidstartRead(DeviceIdType address, std::size_t length,EventLoopContext);voidstartRead(DeviceIdType address, std::size_t length,InterruptContext); // Cancel read for both contexts.boolcancelRead(EventLoopContext);boolcancelRead(InterruptContext); // Start write for both contexts.voidstartWrite(DeviceIdType address, std::size_t length,EventLoopContext);voidstartWrite(DeviceIdType address, std::size_t length,InterruptContext); TContext context); // Cancel write for both contexts.boolcancelWrite(EventLoopContext);boolcancelWrite(InterruptContext); // Suspend/Resumeboolsuspend(EventLoopContext);voidresume(EventLoopContext); // Helper functions to manage read/write during the interruptboolcanRead(InterruptContext);boolcanWrite(InterruptContext);CharTyperead(InterruptContext);voidwrite(CharType value,InterruptContext);};
Such device to control I2C0 interface on RaspberryPi platform is implemented in src/device/I2C0.h file of embxx_on_rpi project.
EEPROM Access Application
The embxx_on_rpi project contains an application called app_i2c0_eeprom. It implements a parallel access to 2 EEPROMs connected to the same I2C0 bus, but having different addresses. The EEPROMs are accessed independently at the same time with read/write operations. These operations are queued and managed by the DeviceOpQueue object that wraps actual I2C control Device and forwards the requests one by one.