2026-02-17 1:58 AM - edited 2026-02-17 2:01 AM
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.
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.
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.For 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).
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.
So basically, I have four questions:
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);
}
}
2026-03-24 7:23 AM
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.