cancel
Showing results for 
Search instead for 
Did you mean: 

I2C MCU Slave register structure conditional ACK/NACK

Nicolas_Z
Associate

Hello everyone,

we have a setup where the STM32U375 MCU is the slave in I2C communication.

We want to NACK the host request on certain conditions.

 

Basic description of our firmware

We are internally using a register structure in the MCU and the host can send a read/write request via I2C to access the contents of the registers.
The current register address is handled internally by the firmware and automatically incremented on each byte written / read.

For example, if the requested register address is 0x0001, the host should not be allowed to write to the register but only to read it.
First we receive the one-byte device address, then the 2-byte register address.
After receiving the 2-byte register address, we can check if the host is allowed to read/write on this register and want to either ACK or NACK the communication on I2C1.

 

Solution tried but not successfull

It seems when HAL_I2C_AddrCallback is called then it is already too late to NACK the current byte and the hardware has already ACK’ed it.
To generate the NACK so far, we have been using "__HAL_I2C_GENERATE_NACK()".

For a Host-Write to the MCU slave, this approach works fine except that the NACK is received by the host one byte late (see screenshot). This solution is not very clean and not preferred by us.STM_Forum_SBC_Logic_Analyzer_Write_NACK_description.pngFor a Host-Read, this approach does not work. A NACK is never received by the host and the SCL line stays low afterwards, effectively having killed the I2C connection (see screenshot).STM_Forum_SBC_Logic_Analyzer_Read_Kill_description.png

Slave Byte Control

We then found some documentation about Slave Byte Control (SBC) mode but I could not find any examples of this being used.

From my understanding about SBC mode, we cannot use the high-level HAL commands like HAL_I2C_AddrCallback, HAL_I2C_SlaveRxCpltCallback, etc. and instead implement all the logic manually (I2C1.CR1, I2C1.CR2, I2C1.ISR registers) inside "I2C1_EV_IRQHandler" (and delete "HAL_I2C_EV_IRQHandler(&hi2c1);" inside the Handler).

If my understanding is correct, that would require a lot of extra work to properly implement.

For Host-Read we also thought about sending only 0xFF-Bytes for the requested number of bytes but this is not our preferred solution.
For Host-Write we currently have no real solution, except for writing a fault into our status register (also not preferred).

I have provided the general I2C Callback code we use below.

 

Questions

So basically, I have four questions:

  • Am I missing some obvious solution on how to choose whether to ACK or NACK in I2C communication as a slave?
  • How are (read and write) permissions in I2C communication generally handled when using a register structure?
  • Is my understanding about SBC implementation (not being able to use the HAL Callback functions) correct?
  • Can you provide me with a minimal example on how to properly use SBC mode?

 

Thank you in advance for any information,

Nicolas

 

static uint8_t i2c1_rx_buffer[2];
static uint8_t i2c1_tx_buffer[1];
static uint32_t register_address = 0x00u;
static uint16_t last_match_address = 0x0u;

void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) {
	last_match_address = AddrMatchCode >> 1;
	// master sends a write the first byte will be the register address -> trigger receive to read it
	if (hi2c->Instance == I2C1 && TransferDirection == I2C_DIRECTION_TRANSMIT) {
		i2c1_rx_buffer[0] = 0x00;
		i2c1_rx_buffer[1] = 0x00;
		HAL_I2C_Slave_Seq_Receive_IT(hi2c, &i2c1_rx_buffer[0], 2, I2C_LAST_FRAME_NO_STOP);
	}
	// master sends a read -> trigger transmit to send it
	else if (hi2c->Instance == I2C1 && TransferDirection == I2C_DIRECTION_RECEIVE) {
		i2c1_tx_buffer[0] = 0x00;
        // Example: Do not ACK register address 1 access by host
        // The current register_address is handled internally by the firmware
		if (register_address == 0x01) {
			__HAL_I2C_GENERATE_NACK(hi2c);
			HAL_I2C_EnableListen_IT(hi2c);
			return;
		}
		Host_Read_Register_Value(last_match_address, register_address, i2c1_tx_buffer);
		HAL_I2C_Slave_Seq_Transmit_IT(hi2c, i2c1_tx_buffer, 1, I2C_FIRST_FRAME);
	}
}

void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c) {
	if (hi2c->Instance == I2C1 && hi2c->XferOptions == I2C_FIRST_FRAME) {
		register_address = ((uint16_t) i2c1_rx_buffer[0]) << 8 | i2c1_rx_buffer[1];
        // Example: Do not ACK register address 1 access by host
		if (register_address == 0x01) {
			__HAL_I2C_GENERATE_NACK(hi2c);
			HAL_I2C_EnableListen_IT(hi2c);
			return;
		}
		i2cStatus = HAL_I2C_Slave_Seq_Receive_IT(hi2c, &i2c1_rx_buffer[1], 1, I2C_NEXT_FRAME);
	} else if (hi2c->Instance == I2C1 && hi2c->XferOptions == I2C_NEXT_FRAME) {
		Intermediate_Host_Write_Register_Value(last_match_address, register_address, i2c1_rx_buffer[1]);
		Increment_Register_Address(last_match_address, 1);
		HAL_I2C_Slave_Seq_Receive_IT(hi2c, &i2c1_rx_buffer[1], 1, I2C_NEXT_FRAME);
	}
}

 

1 REPLY 1
Saket_Om
ST Employee

Hello @Nicolas_Z 

With SBC enabled, the I²C peripheral gives you byte‑level control of ACK/NACK, but this is incompatible with the high‑level HAL slave APIs (`HAL_I2C_Slave_Seq_*`, `HAL_I2C_EV_IRQHandler`, etc.), which assume the hardware handles ACK/NACK automatically. In practice, you don’t use the usual HAL callbacks in SBC mode; instead, you implement your own small state machine directly in the I²C event interrupt, reading `ISR` flags (ADDR, RXNE, TXIS, STOP, NACKF) and writing `CR1/CR2`, `TXDR`, and `RXDR` to decide for each byte whether to ACK, NACK, or provide data.

To give better visibility on the answered topics, please click on "Accept as Solution" on the reply which solved your issue or answered your question.
Saket_Om