Interfacing external EEPROM using I2C drivers on nrf52840 in Zephyr NCS v2.5.0

Hi. I am working on a project and I need to interface a EEPROM to store some data. I tried using I2C drivers from the ncs but it appears that the APIs support only 8 bit addresses as shown below.

Write API:

/**
 * @brief Write multiple bytes to an internal address of an I2C device.
 *
 * This is equivalent to:
 *
 *     i2c_burst_write(spec->bus, spec->addr, start_addr, buf, num_bytes);
 *
 * @param spec I2C specification from devicetree.
 * @param start_addr Internal address to which the data is being written.
 * @param buf Memory pool from which the data is transferred.
 * @param num_bytes Number of bytes being written.
 *
 * @return a value from i2c_burst_write()
 */
static inline int i2c_burst_write_dt(const struct i2c_dt_spec *spec,
				     uint8_t start_addr,
				     const uint8_t *buf,
				     uint32_t num_bytes);

Read API:

/**
 * @brief Read multiple bytes from an internal address of an I2C device.
 *
 * This is equivalent to:
 *
 *     i2c_burst_read(spec->bus, spec->addr, start_addr, buf, num_bytes);
 *
 * @param spec I2C specification from devicetree.
 * @param start_addr Internal address from which the data is being read.
 * @param buf Memory pool that stores the retrieved data.
 * @param num_bytes Number of bytes to read.
 *
 * @return a value from i2c_burst_read()
 */
static inline int i2c_burst_read_dt(const struct i2c_dt_spec *spec,
				    uint8_t start_addr,
				    uint8_t *buf,
				    uint32_t num_bytes);

So, is there any other alternative for this which I can use to access the EEPROM as 8 bits are not enough to access all the addresses. This driver is more suitable for sensors I believe.

  • Hi Sahil

    These functions are primarily intended for sensors or configuration registers, yes, not for accessing EEPROM devices. 

    If you use the more generic i2c_write(..), i2c_write_dt(..), i2c_read(..) or i2c_read_dt(..) functions you can format the data payload exactly as you wish, and you should be able to use an address size matching that of your connected EEPROM. 

    If you need to do a TX -> RX transaction using repeated start you can use the i2c_transfer(..) or i2c_transfer_dt(..) functions. 

    Another alternative is to check for a native driver for your EEPROM device in Zephyr. If one exists you can interface your device directly using one of the higher level flash API's, rather than interfacing it directly through the I2C driver. 

    Best regards
    Torbjørn

  • Hi Torbjorn,

    I saw the functions you mentioned and I am not sure if I can use these functions. Let me explain. Here:

    /**
     * @brief Write a set amount of data to an I2C device.
     *
     * This routine writes a set amount of data synchronously.
     *
     * @param dev Pointer to the device structure for an I2C controller
     * driver configured in controller mode.
     * @param buf Memory pool from which the data is transferred.
     * @param num_bytes Number of bytes to write.
     * @param addr Address to the target I2C device for writing.
     *
     * @retval 0 If successful.
     * @retval -EIO General input / output error.
     */
    static inline int i2c_write(const struct device *dev, const uint8_t *buf,
    			    uint32_t num_bytes, uint16_t addr);

    As you can see, according to my understanding, the address field is used to get the slave address and not the read/write address. 

    /**
     * @brief Complete I2C DT information
     *
     * @param bus is the I2C bus
     * @param addr is the target address
     */
    struct i2c_dt_spec {
    	const struct device *bus;
    	uint16_t addr;
    };
    

    You can see above the i2c_dt_spec used in i2c_read_dt/i2c_write_dt has slave address parameter. And i2c_read_dt/i2c_write_dt functions internally call i2c_read/i2c_write with that address from the i2c_dt_spec structure as you can see below:

    /**
     * @brief Write a set amount of data to an I2C device.
     *
     * This is equivalent to:
     *
     *     i2c_write(spec->bus, buf, num_bytes, spec->addr);
     *
     * @param spec I2C specification from devicetree.
     * @param buf Memory pool from which the data is transferred.
     * @param num_bytes Number of bytes to write.
     *
     * @return a value from i2c_write()
     */
    static inline int i2c_write_dt(const struct i2c_dt_spec *spec,
    			       const uint8_t *buf, uint32_t num_bytes)
    {
    	return i2c_write(spec->bus, buf, num_bytes, spec->addr);
    }

    What are your views on this?

    Regards,

    Sahil

  • Hi Sahil

    The addr field in the function calls should be the static I2C address of your EEPROM, that is correct, not the memory address that you are trying to access. 

    My point is that you can encode the EEPROM address in the I2C buffer itself, something like this:

    int eeprom_write(uint16_t eeprom_addr, uint8_t *data, int length)
    {
        static uint8_t write_buf[EEPROM_MAX_WRITE_LENGTH + 2];
        if (length > EEPROM_MAX_WRITE_LENGTH) return -ENOMEM;
        write_buf[0] = (uint8_t)(eeprom_addr >> 8);
        write_buf[1] = (uint8_t)(eeprom_addr);
        memcpy(&write_buf[2], data, length);
        return i2c_write(dev, write_buf, length + 2, EEPROM_I2C_ADDR);
    }

    Please note this is just an example. Without taking a look at the specification of your EEPROM I don't know exactly what the format of the instructions should be. 

    Best regards
    Torbjørn

  • Hi,

    Thanks for your help. Its working now.

    Regards,

    Sahil

Related