I2C Operation as a Slave Device

When considering communication between or among microcontrollers, I’ve discouraged use of the SPI interface because of its lack of a buffered interface in all but the ATmega4809 Ardruino AVR-based boards. However the handshaking protocol built into the I2C standard interface makes I2C communication a safe, reliable choice. (The I2C interface is called TWI in the Microchip/Atmel documentation).

When viewed from the slave device, the master addresses it and then sends it a sequence of bytes, normally bytes representing a command of some sort followed by optional data. Then the master requests data from the slave in return. The slave can NAK (negative acknowledge or reject) bytes it is sent to signal errors.

The underlying I2C driver code provided in the Arduino software library adds a layer of buffering and partially uses callback functions for slave operation. I’m not keen on having software provided data buffers because they might not be necessary and both slow operation and take precious RAM memory. I do want everything to use callback functions so that the interface can be completely interrupt driven. I ended up implementing a pair of callback functions:

For the callback function for data receiving, call it slaveRx, there is a single argument and return value. At the start of the transaction, the callback function is invoked with an argument of -1 which lets the application know that a transaction has started. The callback function returns a 1 to ACK (acknowledge) the command or 0 to NAK. The the callback function is called once for every data byte received, and returns ACK or NAK. After the last data byte the callback function is called one last time with an argument of -2 to indicate the end of the data. If the application is buffering all the data bytes, this last call indicates that all the data is now available and the application can take action. But buffering is optional, and the application can take action on each byte as it arrives if it wishes to.

The second callback function is used for transmitting data back to the master. This function, slaveTx, is called when the master wants the next byte of data. The callback function returns the next byte to transmit. The function is called once for each byte.

Overall, it looks like this:

Implementing a 256 byte I2C EEPROM emulation in the microcontroller takes just this code for data and the callback functions:

uint8_t eeprom[256];
uint8_t index;        // Current eeprom address
uint8_t state;        // EEPROM state machine state


/* This function is called when data has come in, except
 *  the value of -1 is start of transmission and -2 is end of transmission
 *  return value is true for ACK and false for NAK
 */
bool slaveRx(int value) {
  if (value < 0) { // Not data, so just reset
    state = 0;
    return true; 
  }
  if (state == 0) {
    // Most significant address byte -- ignore
    state++;
    return true;
  } else if (state == 1) {
    // least significant address byte
    index = value;
    state++;
    return true;
  } else {
    // data to write to "eeprom"
    eeprom[index++] = value;
    return true;
  }
}

/* This function is called when the I2C wants to transmit a byte. */
uint8_t slaveTx(void) {
    return eeprom[index++];
}

Note that even though these functions are called from an ISR it is not necessary to declare the variables as volatile. The new drivers will be eventually distributed in my future book.