cancel
Showing results for 
Search instead for 
Did you mean: 

STM32H573 - How to Determine Actual Number of I2C Bytes Sent to Controller?

paulrpotts
Associate

Hi all,

I am implementing a protocol over I2C that requires the STM32 part to act as peripheral and respond to host data reads. I get two types of read transactions: random access, where the controller sends the peripheral address with the WRITE bit set, then a memory address to begin reading, then another start and the peripheral address with the READ bit set. And sequential reads, where the controller doesn't specify an address, it just starts reading. To handle this I need to maintain a "next read address" internally. Here's a diagram of the sequential read:

Screenshot 2026-03-05 145227.png

I am using HAL APIs successfully and driving this from FreeRTOS. I'm testing it with an Aardvark debugger standing in for the controller and an Analog Discovery 2 watching the bus and decoding it. My task turns on listen mode. I get the expected calls to HAL_I2C_AddrCallback(). I can successfully distinguish the two types of transactions, get the memory address with HAL_I2C_Slave_Seq_Receive_IT() with FIRST_FRAME, and send the requested bytes with HAL_I2C_Slave_Seq_Transmit_IT() with NEXT_FRAME.

The difficulty I'm having is trying to figure out how many bytes my code is actually getting onto the I2C bus. I need to be able to keep track of this in order to keep my internal "next read address" correct, so that if the controller starts another sequential read, where it doesn't supply a memory address, I know exactly which byte to send next.

I'd like to be able to just increment my internal "next read address" in the HAL_I2C_SlaveTxCpltCallback(). But this doesn't quite work. It seems that the peripheral and HAL driver lags behind in that the callbacks indicating that the transmission is complete get ahead of what has gone onto the bus. So when I get the HAL_I2C_ErrorCallback(), I have sent out two bytes more than the controller actually wants. (One is expected, as with this sort of sequential read the only way I have of knowing when to stop is by sending an extra byte and getting a NACK). So one might think I could just always just increment my internal "next read address" when I get a transmit complete callback, and back it up when I get the error callback and verify that the error was a NACK. (This ought to work fine because the 8-bit address is supposed to overflow/underflow).

But this doesn't quite seem to be the case either. It looks like I get either one extra HAL_I2C_SlaveTxCpltCallback(), or (sometimes) two. How much I do in the callback functions seems to affect this (I have code to put logging messages into a queue for another task to send to the UART; this slows things down enough to affect how many callbacks I get. Playing around with interrupt and task priority between the I2C task and the UART task also affect this. In other words, it's not quite deterministic and there seems to be a bit of a race. If I don't do any of that logging, just update some counters, it seems to reliably only get one callback ahead of the bus, but I'm not really happy with just assuming that will always be the case, especially once I have a number of additional tasks to talk to SPI devices, run control loops, etc.

I also get two successive error callbacks, which is an annoyance, but I can update a state variable when I get the first one and ignore the second one, so that seems harmless. Then I get the listen complete callback and signal my task to start the cycle over again.

What I'm looking for is a way that I can determine from inside the transmit complete callback whether the send operation I'm getting the callback for is actually completed at the hardware level, so that I'm able to keep my internal "next read address" value accurate.

I've seen some other messages on this board recommending using buffers and counts, but I'm not sure I can apply that device when I have to send bytes one at a time. I'd love to be able to use DMA calls, but it doesn't seem like I'll be able to make them work in a way that is compatible with the HAL sequential drivers that allow me to specify first, next, etc.

I have experience writing low-level drivers from scratch but I'd really like to stick with the HAL APIs if I can.

Thanks for any suggestions.

2 REPLIES 2
LurkingKiwi
Associate II

AIUI the NACK tells the slave transmitter that the master isn't wanting any MORE data, it's not a way to reject the last byte. The byte which gets the NACK has been accepted by the master receiver, it's not "extra" or lost.

Thanks for responding, @LurkingKiwi 

The problem is not that I thought the NACK indicates that the last byte was not accepted. I've understood that the NACK indicates the controller doesn't want to read any more data. The problem is that I am not able to get an accurate count of how many bytes actually went to the controller by using the slave TX complete and error callbacks.

 

Trying to explain this with more pictures and fewer words: if I follow this state machine to respond to the two types of controller read transaction (one with memory address supplied and one without, where it is supposed to start with the internally maintained next read address):

Screenshot 2026-03-07 164531.png

In the Wait Current Read ACK, where I determine ACK by getting a slave TX complete callback, and NACK by getting an error callback, sometimes the state machine "runs ahead" of what is actually going onto the I2C bus. I can get one or two extra TX complete callbacks. I say "one or two" because it is inconsistent, and that's the problem - I can't assume a precise relationship between the number of slave TX complete callbacks and the number of bytes that actually went to the controller before it sent NACK.


Here's a screen shot of my Analog Discovery 2 capturing the protocol along with a log of the UART messages. The logging code queues up formatted strings for another task to send to the UART, so it slows down the execution time of the interrupt service routinse. This example shows the behavior I expect. The first command is a controller read of 3 bytes at memory address 0. I send 3 bytes, I get 3 slave TX complete callbacks, my internal "next read address" is incremented to 0x03, the I get the error callback with NACK. Why two error callbacks? Who knows, but I can ignore the second one. And please ignore the ListenCpltCallback() log line that says the next read address is now 2 - that's due to my temporary "fix" to compensate for the problem of the transmits and callbacks getting ahead of what is going onto the bus, which I'll show in the case below.

Screenshot 2026-03-07 170817.png

Now, here's the behavior if I turn off logging from the callbacks and only send the counts when the transaction is finished. In other words, the ISRs should be running at full speed:

 

Screenshot 2026-03-07 173327.png

The log doesn't capture all the details, but just counts: 3 bytes went out on the wire, but the state machine "ran ahead" and I transmitted 5 bytes, and got 4 TX complete callbacks, so it my code thinks the "next read address" should be 4. (Ignore the "err" count - that only records error reported by the error callbacks that aren't NACK).

 

Given this, my code can't figure out how to keep track of the "next read address" correctly. Does this make more sense now?

 

Apparently I'm not the only one to have this difficulty. See: https://community.st.com/t5/stm32-mcus-products/hal-i2c-slavetxcpltcallback-is-called-despite-not-all-bytes-are/td-p/100005

 

Thanks,

Paul