cancel
Showing results for 
Search instead for 
Did you mean: 

I2C slave receive unknown length message

MHoss.1
Associate III

General idea:

I'm trying to write the I2C as slave on a L476. The master might need to change a bunch of registers at the same time, so instead of sending n separate commands I want to receive one long command and the prepare the response to be sent back at once. The idea is that the master sends a message with arbitrary size that has a 4 byte header, the first two bytes determine the length of the message. The message is stored in a buffer, in the main loop the message is parsed and a response is put together. Then the Master polls a header of 4 bytes that tells master how big is the response, and finally it polls the message of known length.

My Understanding:

This can be done in two ways:

  1. To use use HAL_I2C_Slave_Receive_IT with a size larger than any command I might get and handle the AF error in HAL_I2C_ErrorCallback. I couldn't make this work as the error callback was getting triggered during HAL_I2C_Slave_Transmit_IT. Not sure why? (The master knew the expected size of the transmission).
  2. Use HAL_I2C_AddrCallback to match the address, then get the header using HAL_I2C_Slave_Seq_Receive_IT and findout the total length of the packet. Use HAL_I2C_Slave_Seq_Receive_IT again to get the rest of it.

Confusion:

For the sequential, in the user manual the the xferOptions are explained on page 290.

For my workflow, first the address is matched and HAL_I2C_AddrCallback is called. If the direction is I2C_DIRECTION_TRANSMIT, then I call HAL_I2C_Slave_Seq_Receive_IT to get 4 frames. Here, according to the manual, I have to use I2C_NEXT_FRAME as I don't expect start or address but the sample repo is using I2C_FIRST_FRAME. After the transfer of the 4 bytes HAL_I2C_SlaveRxCpltCallback is called where I check the header to figure how many more bytes I need. Here I should call HAL_I2C_Slave_Seq_Receive_IT with I2C_LAST_FRAME to finish the data transmission. The sample repo has never handled finishing the communication, even with __HAL_I2C_GENERATE_NACK which is confusing for me. Now to send the data back to the master I have used HAL_I2C_Slave_Seq_Transmit_IT with a known length. The sample repo is using I2C_LAST_FRAME and so do it. The transmission goes through, correct number of bytes are sent but only the first byte is correct and the next received packet is lost even though there is no error on either side.

Here's a minimal version of the code:

typedef struct {
    uint8_t header[4];
    uint8_t cargo[256];
    uint16_t len;
} i2c_buffer_t;
 
static i2c_buffer_t aRxBuffer = {.cargo = {0}, .len = 0};
static i2c_buffer_t aTxBuffer = {.cargo = {0}, .len = 0};
 
void parseI2CRxBuffer() {
    // No new packet or line is busy
    if (!aRxBuffer.len || hi2c3.State != HAL_I2C_STATE_READY) {
        return;
    }
 
	// Resend what is received
    uint16_t length = aRxBuffer.header[0] | aRxBuffer.header[1] << 8;
    memcpy(aTxBuffer.cargo, aRxBuffer.cargo, length);
    aTxBuffer.len = length;
	// Print what is received
 
	// Indicate the buffer is processed
    aRxBuffer.len = 0;
}
 
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c) {
	if (hi2c->Instance==hi2c3.Instance) {
	    if (aRxBuffer.len == 4) {
	        // Receive the rest of the bytes
	        uint16_t length = aRxBuffer.header[0] | aRxBuffer.header[1] << 8;
            aRxBuffer.len += length;
	        HAL_I2C_Slave_Seq_Receive_IT(hi2c, aRxBuffer.cargo, length, I2C_NEXT_FRAME);
	    }
	    else if (aRxBuffer.len > 4) {
			// All bytes received, terminate communication
	        __HAL_I2C_GENERATE_NACK(hi2c);
	    }
	}
}
 
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c) {
    parseI2CRxBuffer();
    HAL_I2C_EnableListen_IT(hi2c); // Restart
}
 
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) {
    DEBUG_PRINT("[I2C] line error %d\n", HAL_I2C_GetError(hi2c));
}
 
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t transferDirection, uint16_t AddrMatchCode) {
    if (AddrMatchCode != I2C_OWN_ADDRESS) {
        return;
    }
 
    if (transferDirection == I2C_DIRECTION_TRANSMIT) {
        // In Receive mode, get the header
        aRxBuffer.len = 4;
        aTxBuffer.len = 0;
        aTxBuffer.header[0] = 0;
        aTxBuffer.header[1] = 0;
        i2cSeqReceive(aRxBuffer.header, 4, I2C_FIRST_FRAME);
    }
    else {
        // Sending the response to the master
        static uint8_t sendCargo;
        if (sendCargo) {
            HAL_I2C_Slave_Seq_Transmit_IT(hi2c, aTxBuffer.cargo, aTxBuffer.len, I2C_LAST_FRAME);
            sendCargo = 0;
        }
        else {
            aTxBuffer.header[0] = aTxBuffer.len & 0xFF;
            aTxBuffer.header[1] = aTxBuffer.len >> 8;
            HAL_I2C_Slave_Seq_Transmit_IT(hi2c, aTxBuffer.header, 4, I2C_LAST_FRAME);
            sendCargo = 1;
        }
    }
}

And I test it with this Arduino code

void sendNBytes(uint8_t n) {
  Wire.beginTransmission(i2c);
  Wire.write(n);
  Wire.write(0);
  for (int i=0; i<n+2; i++) {
    Wire.write(i);
  }
  Wire.endTransmission(true);
}
 
void receiveNBytes(int n, uint8_t buff[]) {
  Wire.requestFrom(i2c, n);
  for (int i=0; i<n; i++) { 
    buff[i] = Wire.read(); // receive a byte as character
  }
}
 
void receivePacket() {
  uint8_t buff[256];
  receiveNBytes(4, buff);
  int n = buff[0];
  receiveNBytes(n, &buff[4]);
  for (int i=0; i<4+n; i++) {
    Serial.print(buff[i]);
    Serial.print(" ");
  }
  Serial.println("");
}
 
void setup() {
  Serial.begin(9600);
  delay(1000);
  Wire.begin();
 
  Serial.println("Sending 5 bytes");
  sendNBytes(5);
  delay(500);
  receivePacket();
 
  delay(500);
  Serial.println("Sending 10 bytes");
  sendNBytes(10);
}

5 REPLIES 5
MM..1
Chief II

For your inspiration, with stdlib i use CPAL part stm32f0xx_i2c_cpal_usercallback.c and here exist big amount usefull calls where one is

/**
  * @brief  Manages Rx transfer event
  * @param  pDevInitStruct
  * @retval None
  */
extern __IO uint32_t bytesReceived ;
extern __IO uint8_t evntBuffer[];
void CPAL_I2C_RX_UserCallback(CPAL_InitTypeDef* pDevInitStruct)
{
	bytesReceived += 1;
 
if(pDevInitStruct->pCPAL_TransferRx->wAddr1 == TARGET_ADDR && bytesReceived > 1 && bytesReceived >= evntBuffer[1]+1)	
	{
// end on length in first byte
 I2C_GenerateSTOP( I2C2, ENABLE);
	}
}

then i call CPAL_I2C_Read(&MASTERSTRUCTURE); with numdata for larger as any and this STOP receive on real message end.

KnarfB
Principal III

The other day I wrote some I2C slave code which mimics an EEPROM or similar. The first byte is the initial offset, and the master can read/write (with slave-side autoincrement) as much as it likes:

https://gitlab.com/stm32mcu/i2c_master_and_servant/-/blob/master/Src/main.c

hth

KnarfB

Thanks @MM..1​  but the standard library is not supported on many newer boards.

Thanks, your code helped me figure my code should work fine and it turned out the arduino I was using was faulty. Changed it to a Nano 33 IoT and it runs over 4k communications fine. However, when I connect it to an older Nano or Uno (both are 5v) and want to send a 200-bytes long message it generates and acknowledge fail after 27th byte. The I2C pins are supposed to be 5v tolerant so I'm not sure where the problem is coming from. I should check it with an oscilloscope today but would appreciate any input if you've had similar experience before.

Checking the signals with a scope is a good idea. It's somehow tricky to find the right value for the pull-ups.