I2C Slave Transmitter/Receiver Not Working

Hello!
I'm having trouble with my I2C slave code for both a slave receiver and slave writer. I'm pretty new to Arduino software development, although I do have a background in embedded firmware with C. I'm trying to use the code below to test my master transmitter/receiver code for the ATmega644.

I've already tried testing the program and even tried to see a waveform appear on the oscilloscope, but I don't see anything. Is the implementation below faulty? I've tried looking through other questions relating to slave transmitter/receiver, but I couldn't find anything helpful. Any help would be appreciated.

#include <Wire.h>

volatile byte data[4] = {0, 0, 0, 0}; // dummy register for i2c to access
volatile byte addr_pointer = 0xFF;    // points to address in dummy buffer

void setup() {
  digitalWrite(SDA, LOW);             // turn off internal pull-up resistors (they already exist externally)
  digitalWrite(SCL, LOW);             // turn off internal pull-up resistors (they already exist externally)
  Wire.begin(0x20);                   // begin device as slave @ address 0x20
  Wire.onRequest(requestEvent);       // master receive event (read)
  Wire.onReceive(receiveEvent);       // master transmit event (write)
}

void loop() {
  delay(2000);
  for (int i = 0; i < 4; i++) {
    Serial.print(data[i], HEX);
  }
  Serial.println();
}

void requestEvent() {
  while (Wire.available()) {             // loop until master sends NACK
    Wire.write(data[addr_pointer]);     // send data to I2C bus
    addr_pointer = addr_pointer + 1;    // increment register value
  }
}

void receiveEvent(int howMany) {
  addr_pointer = Wire.read();           // get base address from I2C bus
  while(Wire.available() > 0) {         // loop through all data (should account for repeated restart?)
    data[addr_pointer] = Wire.read();   // receive and store data in dummy reg
    addr_pointer = addr_pointer + 1;    // increment register pointer
  }
}

Here is the Master Transmitter firmware (in C).

/* 
 * File:   i2c.c
 * Author: Shadi Zogheib
 *
 * Created on July 9, 2021, 1:51 PM
 */

#include <avr/io.h>
#include <util/twi.h>
#include "i2c.h"

#define CLEAR_TWINT         (TWCR = (1 << TWINT) | (1 << TWEN))
#define SET_ACK             (TWCR = (1 << TWINT) | (1 << TWEA) | (1 << TWEN))
#define SET_NACK            (TWCR = (1 << TWINT) | (1 << TWEN))
#define I2C_START           (TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN))
#define I2C_STOP            (TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN))


/* Initialize I2C protocol parameters */
void i2c_init() {
    TWBR = 8;               // Set I2C clock to 50kHz for TWPS = 0
    TWCR = (1 << TWEN);     // Enable I2C protocol on MCU pins
}


/* This function is called when data must be written
 * to the I2C bus. The parameters are:
 *      data <-- pointer to data array
 *      ack  <-- ACK value for the specific operation
 *      nack <-- NACK value for the specific operation
 * Returns 0 for NACK, 1 for ACK. */
int8_t load_reg(int8_t* data, uint8_t ack, uint8_t nack) {
    // Clear TWINT flag
    CLEAR_TWINT;
    
    // Load register with data to transmit
    TWDR = *data;
    
    // Wait for transmission completion
    while ((TW_STATUS != ack) && (TW_STATUS != nack));
    
    // Check if NACK is received, return 0 if NACK
    if (TW_STATUS == nack)
        return 0;
    
    return 1;
}

/* This function is called when data must be read
 * from the I2C bus. The parameters are:
 *      data      <-- pointer to data array
 *      last_byte <-- last byte indicator */
void load_data(int8_t* data, uint8_t last_byte) {
    // If last byte, set NACK, otherwise, set ACK for more data (and clear TWINT)
    if (last_byte)
        SET_NACK;
    else
        SET_ACK;
    
    // Wait for data to fully arrive over I2C bus
    while ((TW_STATUS != TW_MR_DATA_ACK) && (TW_STATUS != TW_MR_DATA_ACK));
    
    // Load data field with data from TWDR register
    *data = TWDR;
}


/* Write byte(s) to the specified device's register.
 * This function takes the following parameters:
 *      device      <-- I2C slave ID
 *      addr        <-- register to access
 *      data        <-- 1-2 bytes to transmit
 *      valid       <-- defines valid data bytes:
 *          (0) Transmit data to lower byte of register
 *          (1) Transmit data to upper byte of register
 *          (2) Transmit data to both bytes of register
 * Ack_status flag indicates if function should continue. */
void i2c_write(int8_t device, int8_t addr_base, int16_t data, uint8_t valid) {
    // Safeguard the rest of the function
    if (valid > 2)
        return;
    
    int8_t ack_status;
    int8_t sla = device + TW_WRITE;
    
    // Update address pointer
    int8_t addr = addr_base + (int8_t)(valid & 1);
    
    // Start I2C communication process
    I2C_START;
    while (TW_STATUS != TW_START);
    
    // Load TWDR register with slave ID
    ack_status = load_reg(&sla, TW_MT_SLA_ACK, TW_MT_SLA_NACK);
    
    // Load TWDR register with register base address
    if (ack_status)
        ack_status = load_reg(&addr, TW_MT_DATA_ACK, TW_MT_DATA_NACK);
    
    // Load TWDR register with only the valid data to be written
    int8_t byte;
    if (ack_status && !(valid & 1)) {
        byte = (int8_t)(data & 0x00FF);
        ack_status = load_reg(&byte, TW_MT_DATA_ACK, TW_MT_DATA_NACK);
    }
    if (ack_status && (valid > 0)) {
        byte = (int8_t)(data >> 8);
        ack_status = load_reg(&byte, TW_MT_DATA_ACK, TW_MT_DATA_NACK);
    }
    
    // Stop I2C communication process
    I2C_STOP;
}

/* Incremental read from the specified device's register.
 * This function takes the following parameters:
 *      device      <-- I2C slave ID
 *      addr        <-- register to access
 *      data        <-- byte to return
 *      data_size   <-- size of data
 * Ack_status flag indicates if function should continue. */
void i2c_read(int8_t device, int8_t addr, int8_t* data, uint8_t data_size) {
    // Safeguard the rest of the function
    if (data_size == 0)
        return;
    
    int8_t ack_status;
    int8_t sla = device + TW_WRITE;
    
    // Start I2C communication process
    I2C_START;
    while (TW_STATUS != TW_START);
    
    // Load TWDR register with slave ID (master transmitter mode)
    ack_status = load_reg(&sla, TW_MT_SLA_ACK, TW_MT_SLA_NACK);
    
    // Load TWDR register with register base address
    if (ack_status)
        ack_status = load_reg(&addr, TW_MT_DATA_ACK, TW_MT_DATA_NACK);
    
    // Do not continue if NACK is received
    if (!ack_status) {
        I2C_STOP;
        return;
    }
    
    // Perform repeated start
    I2C_START;
    while (TW_STATUS != TW_REP_START);
    
    // Load TWDR register with the same slave ID (switch to master receiver mode)
    sla = device + TW_READ;
    ack_status = load_reg(&sla, TW_MR_SLA_ACK, TW_MR_SLA_NACK);
    
    // Load data field with data from TWDR register
    uint8_t i = 0;
    uint8_t last_byte;
    while ((i < data_size) && (ack_status)) {
        last_byte = ((i) == (data_size - 1)) ? (1) : (0);
        load_data(&data[i], last_byte);
        i = i + 1;
    }
    
    // Stop I2C communication process
    I2C_STOP;
}

Post your Master Sketch.

You can make it easier by defining a data transfer with a fixed number of bytes, for example with a 'struct'. It is also easier to use a Arduino board, or at least a clone that is 100% compatible.

Suppose you want to emulate a EEPROM, then you can put the register-address in the first byte when sending data and have a variable number of bytes to write.
If the Slave runs the requestEvent() function and the number of data bytes is variable, that is tricky, because the Slave does not know how many bytes the Master wants. The buffer size is 32. So you could fill the buffer with the maximum number of bytes (up to 32) and the Master will stop the I2C session when it has enough.

The requestEvent() can not use the Wire.available(). The Slave does not know how much data the Master has requested.

The receiveEvent() can use the Wire.available(), but it does not have to use it, because the number of bytes is already known by the 'howMany' parameter.

@GolamMostafa I have edited the original post to include the I2C Master protocol in C.

@Koepel I have a test code that does not write or request more than 4 bytes. The testbed is only to test if the communication protocol on the ATmega644 is implemented correctly and that there is some communication between the Arduino UNO and the ATmega644.

Also, I guess I am a little confused about the requestEvent() and receiveEvent() handlers. For example, when an onReceive() event occurs, has the Arduino already taken care of the low-level I2C communication and stored all the transmitted data in a buffer? Is the onRequest() event called as soon as a "read" condition is transmitted by the Master? These are questions I couldn't find answers to and would determine my Arduino setup. The Master implementation can only write up to 2 bytes, but can read as many as it wants.

It is beyond my ability to help you unless your Master is an Arduino Board.

Assuming the Slave is an Arduino, your Slave sketch can be tidied up as follows:

#include <Wire.h>
byte myData[2];

byte data[4] = {0, 0, 0, 0}; // dummy register for i2c to access
volatile byte addr_pointer = 0xFF;    // points to address in dummy buffer

void setup() 
{
   Wire.begin(0x20);                                // begin device as slave @ address 0x20
   Wire.onRequest(requestEvent);       // master receive event (read)
   Wire.onReceive(receiveEvent);       // master transmit event (write)
}

void loop() 
{
     delay(2000);
     for (int i = 0; i < 4; i++) 
     {
         Serial.print(data[i], HEX);
      }
  Serial.println();
}

void requestEvent() 
{
    //Your Master must be excuting codes that correspond to
    //Wire.requestFrom(0x20, 2);    //2-byte data is being requested from Slave
    //when that command comes from Master, the Slave is interrupted and comes here.
   //Slave keeps the data into Buffer
   //Master is waiting from ACK from Slave
    Wire.write(0x12);    //data1 for example
    Wire.write(0x34);
   //Slave MCU sends ACK and then goes to loop() function
   //Master gets ACK signal and reads data from Slave Buffer
}

void receiveEvent(int howMany) 
{
      //number of data bytes that have arrived  from Master is equal to howMany
      for(int i=0; i<howMany; i++)
      {
            myData[i] = Wire.read();  //collect data from Slave's buffer and save in array
      }
  }

Please delete your Master code, don't use any of it.
Use the normal Arduino Wire library.
It took many years to get the bugs out of the Arduino Wire library for the AVR branch.

Use the Slave code by @GolamMostafa
I show the very basic principle here: Arduino in Slave mode.

Once everything works flawlessly, then you can start to think about using (emulated) registers with a register-address. But only if you have a very good reason.

Yes. All the data is in a buffer inside the Wire library. I don't know if the I2C session is completely finished or is about to be finished. The onReceive handler is called from a interrupt routine, so you must be very careful.

Yes, and the Slave pulls the SCL low to pause the Master so the Slave can run the onRequest handler in software. The buffer is filled in the onRequest handler and if that function has finished, then Wire library passes that data on to the Master. It is even possible that the SCL is made low for each byte that is send, because the Wire library handles that in a interrupt. I think it is an interrupt for each byte.