Memory modules over I2C

Memory is one of the resource you will miss on microcontrollers. Also not all microcontrollers allow you to store data during power loss / restarts. When developing embedded devices you may want to take idempotency and statelessness into consideration since relying on some state outside may be causing you system stop working/starting.

Anyway in some cases a state durable between restarts may be necessary. Or even just the need to extend the limited memory on the PIC itself. The are plenty different options and memory technologies (e.g. EEPROM, Flash) which differ with parameters like durability, speed, etc. After a bit of research one finds quickly that big chunk of them (and also the most affordable ones) are implementing the I2C interface.

Since I already implemented couple peripherals using I2C interface and different PICs expose it differently I decided to abstract the I2C implementation and all the modules using I2C should use this abstraction.

In most cases you need to communicate with an I2C device in one of the following ways:

  • Reading 1 byte from a 1 byte register
  • Reading 1 byte from a 2 byte register
  • Writing 1 byte to a 1 byte register
  • Writing 1 byte to a 2 byte register
  • Writing 1 byte
  • Writing x bytes of data

Depending on the PIC you use a different MPLAB Code Configurator (MCC) code will be generated for you. Most PICs I use have whether full I2C peripheral or an Master Synchronous Serial Port (MSSP) peripheral combining an I2C and SPI interface in one peripheral. In case of MSSP I recommend using the Foundation Service Library together with I2CMASTER and I2CSIMPLE Foundation Services. This will generate a similar code to the full I2C peripheral one.

I2C Abstraction

For brevity I am not showing the header file and removed the MSSP without Foundation Services support.

/* 
 * File:   i2c.h
 * Author: Jan Kubovy <jan@kubovy.eu>
 */
#include "i2c.h"

//#if defined I2C || defined I2C_MSSP || defined I2C_MSSP_FOUNDATION

#if defined I2C_MSSP
uint8_t writeBuffer[3];
#endif

inline void I2C_init(void) {
#if defined I2C_MSSP
    // ... removed ...
#elif defined I2C_MSSP_FOUNDATION
    i2c1_driver_open();
#endif
}

inline uint8_t I2C_readRegister(uint8_t address, uint8_t reg) {
#if defined I2C
    return i2c1_read1ByteRegister(address, reg);
#elif defined I2C_MSSP
    // ... removed ...
#elif defined I2C_MSSP_FOUNDATION
    return i2c_read1ByteRegister(address, reg);
#endif
}

inline uint8_t I2C_readRegister2(uint8_t address, uint8_t regHigh, 
uint8_t regLow) { #if defined I2C return i2c1_read1ByteRegister2(address, regHigh, regLow); #elif defined I2C_MSSP // ... removed ... #elif defined I2C_MSSP_FOUNDATION return i2c_read1ByteRegister2(address, regHigh, regLow); #endif } inline void I2C_writeByte(uint8_t address, uint8_t byte) { #if defined I2C i2c1_writeNBytes(address, &byte, 1); #elif defined I2C_MSSP // ... removed ... #elif defined I2C_MSSP_FOUNDATION i2c_writeNBytes(address, &byte, 1); #endif } inline void I2C_writeRegister(uint8_t address, uint8_t reg, uint8_t byte) { #if defined I2C i2c1_write1ByteRegister(address, reg, byte); #elif defined I2C_MSSP // ... removed ... #elif defined I2C_MSSP_FOUNDATION i2c_write1ByteRegister(address, reg, byte); #endif } inline void I2C_writeRegister2(uint8_t address, uint8_t regHigh, uint8_t regLow,
uint8_t byte) { #if defined I2C i2c1_write1ByteRegister2(address, regHigh, regLow, byte); #elif defined I2C_MSSP // ... removed ... #elif defined I2C_MSSP_FOUNDATION i2c_write1ByteRegister2(address, regHigh, regLow, byte); #endif } inline void I2C_writeData(uint8_t address, uint8_t *data, uint8_t len) { #if defined I2C i2c1_writeNBytes(address, data, len); #elif defined I2C_MSSP // ... removed ... #elif defined I2C_MSSP_FOUNDATION i2c_writeNBytes(address, data, len); #endif } //#endif

Since the proprietary MSSP implementation was removed we can see that the I2C abstraction could be simply replaces with function reference setters in the modules implementing I2C saving us some precious program memory.

Memory interface

The memory interface boils down to a very simple code:

/* 
 * File:   memory.c
 * Author: Jan Kubovy <jan@kubovy.eu>
 */
#include <stdarg.h>
#include "memory.h"
#include "i2c.h"

/**
* Read byte from memory.
*
* @param address I2C memory device address.
* @param regHigh Memory registry high byte.
* @param regLow Memory registry low byte.
* @return Byte at register.
*/
uint8_t MEM_read(uint8_t address, uint8_t regHigh, uint8_t regLow) { return I2C_readRegister2(address, regHigh, regLow); } /**
* Write byte to memory.
*
* @param address I2C memory device address.
* @param regHigh Memory registry high byte.
* @param regLow Memory registry low byte.
* @param byte Byte to write.
*/
void MEM_write(uint8_t address, uint8_t regHigh, uint8_t regLow, uint8_t byte) { I2C_writeRegister2(address, regHigh, regLow, byte); }

See the MCLIB repo for full implementation.

Leave a Reply

Your email address will not be published. Required fields are marked *