cancel
Showing results for 
Search instead for 
Did you mean: 

I2C and interrupts on SCL

sb_st
Associate III

Hi;

I have a tlv493d magnetic encoder, with which I would like to communicate via i2c with an stm32f4 MCU. 

I can successfully initiate communication with this sensor via I2c, to perform an initial configuration of the sensor. I can verify this by way of an oscilloscope and a logic analyzer - my initial read/write transactions all seem fine.

One of the properties of the tlv493d is that - once it's been initialized - it indicates that it has data ready to be read by periodically pulling the SCL line low. To accommodate this, I have - in addition to my I2C lines - a GPIO that uses EXTI to sense this interrupt. 

The relevant parts of my code (I am working cubeIDE, using HAL) are:

 

 

// Sensor interrupt callback
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
	if (GPIO_Pin == SENSOR_INT_Pin) {
		tlv493d_data_ready = 1;
	}
}

// Main code
void sensor_read_task(void *argument)
{
	// Initialize the sensor. This consistently returns
	// HAL_OK, and I can verify the sensor is working 
	// with a logic probe.
	sensor_status = TLV493D_initialize( &sensor, &sensor_config);
	
	// Enable interrupts on the GPIO	
	HAL_NVIC_EnableIRQ(EXTI4_IRQn);

	while (1)
	{
		if (tlv493d_data_ready > 0 ) {
			HAL_NVIC_DisableIRQ(EXTI4_IRQn);
			sensor_status = TLV493D_read_data(&sensor);
        		tlv493d_data_ready = 0;
        		HAL_NVIC_EnableIRQ(EXTI4_IRQn);
		}
	}
}

 

 

 

My problem is that in the main while() superloop, my TLV493D_read_data() function always returns HAL_BUSY, and I cannot get meaningful data from the sensor. 

Trolling around these forums, I see mention of resetting the I2C bus (though there are also accompanying comments that having to do this usually indicates problems elsewhere). When I try to add some reset logic to my read function, like this:

 

 

HAL_StatusTypeDef TLV493D_readRegisters(TLV493D_HandleTypeDef *sensor,
                                    	uint8_t num_registers_to_read, uint8_t *data) {
	HAL_StatusTypeDef status;
	uint32_t i2c_error;

	// Verify device is ready before read
	status = HAL_I2C_IsDeviceReady(sensor->i2c_handle,
					TLV493D_I2C_ADDR,
					3,  // Number of trials
					100);  // Timeout

	if (status != HAL_OK) {
		// Explicitly reset I2C peripheral
		if (HAL_I2C_DeInit(sensor->i2c_handle) != HAL_OK) {
			return HAL_ERROR;
		}

		if (HAL_I2C_Init(sensor->i2c_handle) != HAL_OK) {
			return HAL_ERROR;
		}
	}

	// Verify device is ready before read
	status = HAL_I2C_IsDeviceReady(sensor->i2c_handle,
					TLV493D_I2C_ADDR,
					3,  // Number of trials
					100);  // Timeout
	if (status != HAL_OK) {
		// Log that device is not responding
		return status;
	}

	// Attempt read with reduced timeout for diagnostics
	return HAL_I2C_Master_Receive(sensor->i2c_handle,
					STLV493D_I2C_ADDR,
					data,
					num_registers_to_read,
					HAL_MAX_DELAY);

 

 

 I notice that read transactions DO seem to work, though they are now wholly unrelated to the actual interrupt pulse being sent. 

Here, visually, is what's happening as seen by a logic analyzer. In the below image, we can see the (lower) SCL line being pulled down periodically by the sensor. I can see in a debugger that the interrupt callback is indeed being called properly, and the read function attempts to run, but always returns HAL_BUSY, and we can see no pulses on the SDA line. 

Screenshot 2024-12-16 at 10.05.07 AM.png

Below, in contrast, is what happens when I reset the I2C bus inside my read function (which I think is probably happening every time it fires). I would expect to see very small bursts of read transactions on SDA, with long periods of relative silence between the interrupt pulses. Instead, the read transaction seems to be constantly called, as though it is triggering interrupts itself, despite me disabling them in the code above when trying to do a read. 

Screenshot 2024-12-16 at 10.10.32 AM.png

I've tried this same setup on a Nucleo board as well as on a homemade development board, with similar results. Further, I can hook this sensor to a separate esp32 development board I have and it runs flawlessly, so I think the sensor hardware itself is sound. 

How else can I troubleshoot this? I've tried everything I can think to search for online, and am stumped. 

 

8 REPLIES 8
TDK
Guru

If SCL is low when you try to do an I2C transaction, the chip will return HAL_BUSY because the bus is busy. If you set the interrupt to happen on a rising edge instead of a falling edge, it should fix this.

SCL getting pulled low by a slave seems like a recipe for a confused bus master.

> My problem is that in the main while() superloop, my TLV493D_read_data() function always returns HAL_BUSY, and I cannot get meaningful data from the sensor. 

It would be helpful to see the code of this function. Should be able to instrument and see where in the HAL library it triggers the HAL_BUSY.

If you feel a post has answered your question, please click "Accept as Solution".
sb_st
Associate III

 

If SCL is low when you try to do an I2C transaction, the chip will return HAL_BUSY because the bus is busy. If you set the interrupt to happen on a rising edge instead of a falling edge, it should fix this.

Oh interesting - you're suggesting my I2C transaction might be starting before the sensor has released the SCL line. I have tried inserting an osDelay() after returning from my interrupt callback, but maybe I need to wait longer...or, as you say, trigger on the rising rather than the falling edge. I'll try that, thanks!

 


It would be helpful to see the code of this function. Should be able to instrument and see where in the HAL library it triggers the HAL_BUSY.


Sure, I actually included it in the first code snippet above. Here it is again:

 

// Main code
void sensor_read_task(void *argument)
{
	// Initialize the sensor. This consistently returns
	// HAL_OK, and I can verify the sensor is working 
	// with a logic probe.
	sensor_status = TLV493D_initialize( &sensor, &sensor_config);
	
	// Enable interrupts on the GPIO	
	HAL_NVIC_EnableIRQ(EXTI4_IRQn);

	while (1)
	{
		if (tlv493d_data_ready > 0 ) {
			HAL_NVIC_DisableIRQ(EXTI4_IRQn);
			sensor_status = TLV493D_read_data(&sensor);
        		tlv493d_data_ready = 0;
        		HAL_NVIC_EnableIRQ(EXTI4_IRQn);
		}
	}
}

 

Something else I might be getting wrong here: this sensor doesn't seem to need/use individual register addressing when doing reads. From the sensor's datasheet:

  • The sensor addressing consists of one byte followed by one ACK bit. The purpose of the addressing is to identify (slave number) the sensor with which the communication should take place. These bits are required independently of whether only one or multiple slaves in bus configuration are connected to the master. The master sends 7 address bits starting with the MSB followed by one read/write bit (read = high, write = low). The slave (sensor) responds with one ACK bit. Every bit in the SDA line is pulled down or up while SCL is low, then SCL is pulled up for a pulse and again down before the SDA line is pulled again down.
  • Each data transfer consist of one byte data followed by one ACK bit. If the LSB bit of the sensor addressing byte was a write (low), then the master writes 8 data bits to the slave and the slaves responds with one ACK bit. If the LSB bit of the sensor addressing was a read (high), then the master reads 8 data bits from the slave and the master responds with one ACK bit. Data bytes start always with the register address 00H and as many bytes will be transferred as the SCL line is generating pulses (following a 9 bit pattern), till the stop condition

 

In my initialization code, I do a HAL_I2C_Master_Transmit(), sending a single 0x00 byte, which the datasheet says is meant to reset it. Then I do a HAL_I2C_Master_Receive(), which is successful. 

 

But, as I mentioned, subsequent calls to HAL_I2C_Master_Receive() always report HAL_BUSY. Am I meant to be preceding all HAL_I2C_Master_Receive() calls with HAL_I2C_Master_Transmit() of that 0x00 byte, to tell the sensor I want to start reading from register 0? Or is it enough to just send the HAL_I2C_Master_Receive() on its own? I'm a little confused about whether I should think of a read operation as one single transaction or two. 

Thank you again for the time!

 

sb_st
Associate III

I don't know if this is a further clue, but observing my SDA/SCL lines with a log probe throughout the initialization process, I'm noticing the "Setup to read to [0x7F+NAK]" bit off to the right there. Does that indicate something amiss with the communication just prior?

Screenshot 2024-12-18 at 6.29.44 AM.png

TDK
Guru

The last transaction looks okay to me.

I would instrument the code to understand where HAL_BUSY is being reported. The contents of the I2C handle state machine can also offer clues. Is its state READY or something else?

If you feel a post has answered your question, please click "Accept as Solution".
MHoll.2
Senior

I'm using the tlv493d, but I have never tried to use the INT mode (I have INT = 0, FAST = 1, LOW_POWER = 1, reading the data every 2ms I have garanteed new data).

If  You realy need the low-power mode (100Hz) then probably You really need to deinit the I2C and after the interrupt event init the I2C and read the Data.

From what I see in Your log the init seems OK (including the read).

I'm using the library from Infineon (https://softwaretools.infineon.com/tools/com.ifx.tb.tool.xensivtlx493dgenericlibrarysoftware)

The low level write and read function are:

int32_t ReadHallSensor(uint8_t addr, uint8_t *data, uint8_t count)
{
	HAL_StatusTypeDef ret;

	if(xSemaphoreTake(sI2CMutex, 100) == pdPASS)
	{
		ret = HAL_I2C_Master_Receive_IT(&hi2c1, addr, data, count);
		if(ret == HAL_OK)
		{
			// wait for Complete event
			if(xSemaphoreTake(sI2CRXEndEvent, 5 + (count / 8)) == pdTRUE)
			{
				xSemaphoreGive(sI2CMutex);
				return(0);
			}
			else
			{
				xSemaphoreGive(sI2CMutex);
				return(1);
			}
		}
	}
	else
	{
		return(1);
	}
    return(1);
}
int32_t WriteHallSensor(uint8_t addr, uint8_t* data, uint8_t count)
{
	HAL_StatusTypeDef ret;

	if(xSemaphoreTake(sI2CMutex, 100) == pdPASS)
	{
		ret = HAL_I2C_Master_Transmit_IT(&hi2c1, addr, data, count);
		if(ret == HAL_OK)
		{
			// wait for Complete event
			if(xSemaphoreTake(sI2CTXEndEvent, 5 + (count / 8)) == pdTRUE)
			{
				xSemaphoreGive(sI2CMutex);
				return(0);
			}
			else
			{
				xSemaphoreGive(sI2CMutex);
				return(1);
			}
		}
	}
	else
	{
		return(1);
	}
    return(1);
}

 

 

sb_st
Associate III

Hi @TDK, thank you!

 


I would instrument the code to understand where HAL_BUSY is being reported

I'm sorry, can you elaborate what you mean by "instrument the code"? I am using a logic analyzer while stepping through code with a debugger, to view all the steps of what I am seeing. 

Here, for example, are screen grabs of what the I2C transaction looks like when this sensor is attached to an esp32 using nearly-identical driver code (and which works successfully):

i2c_logic_esp32.png

And here is a screen grab of the I2C I see when running my code on the stm32:

i2c_logic_stm32_using_master_transmit.png

They look alright to me, or at least I see nothing obvious that would suggest the transactions themselves are incorrect. So I'm thinking there's something else going on here. 

Another clue: if I do like @MHoll.2's comment below mentions and disable interrupts on the sensor, and just periodically read it ever 10 ms, everything works as expected. So my trouble here seems related to the use of interrupts. 

 


The contents of the I2C handle state machine can also offer clues. Is its state READY or something else?

Hm, thanks for pointing me to this. I tried modifying my superloop to:

	while (1)
	{
		if (tlv493d_data_ready) {
			LL_EXTI_DisableIT_0_31(LL_EXTI_LINE_4);
			sensor_status = HAL_I2C_IsDeviceReady(sensor_config.i2c_handle,
										   SB_TLV493D_I2C_ADDR,
										   3,  // Number of trials
										   100);  // Timeout
			while (sensor_status != HAL_OK) {

				// Explicitly reset I2C peripheral
				sensor_status = HAL_I2C_DeInit(sensor_config.i2c_handle);
				sensor_status = HAL_I2C_Init(sensor_config.i2c_handle);

				// Verify device is ready before read
				sensor_status = HAL_I2C_IsDeviceReady(sensor_config.i2c_handle,
											   SB_TLV493D_I2C_ADDR,
											   3,  // Number of trials
											   100);  // Timeout
			}

		sensor_status = SB_TLV493D_read_data(&sensor);
        	tlv493d_data_ready = 0;

        	LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_4);

		}

	}

In the above, the HAL_I2C_IsDeviceReady() on Line 5 returns HAL_BUSY. When I expect the state of the handle at the this point, I see:

Screenshot 2024-12-19 at 8.43.59 AM.png

Stepping through the I2C DeInit/Init loop there, the I2C reset seems to work but HAL_I2C_IsDeviceReady() continues to report HAL_BUSY, and the handle state continually looks like:

Screenshot 2024-12-19 at 8.49.35 AM.png

 

 

Hi @MHoll.2 thanks for the info! I'd eventually like to work towards getting I2C interrupts working, as well as semaphore stuff, but am trying to get a simplest-case setup going here. I DO see that if I disable interrupts on the tlv493d and just periodically read it using my code above, it seems to work fine. This, at least, suggests to me that the i2c transactions are structured properly. So I think my issue is related specifically to the interrupt stuff. 

If HAL_I2C_DeviceReady is returning HAL_BUSY, by "instrument the code" I mean set a breakpoint and figure out which line is returning HAL_BUSY and determine why. In this case, it has to be this line:

> I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK

So the I2C_FLAG_BUSY flag is set. What does the manual say about that?

TDK_0-1734662125820.png

Your slave is pulling SCL low periodically, so it makes sense that flag would be set. How does it say it gets cleared? A stop condition.

TDK_1-1734662221763.png

So a stop condition is when SDA is low and SCL goes high. You'll need to generate that in order to clear the BUSY bit and let the peripheral progress. You can do that be reconfiguring the SDA/SCL pins as open-drain output, pulling SDA low, then pulling SCL low temporarily, then releasing SDA. Resetting the peripheral would also do the same thing. Something like:

__HAL_RCC_I2C1_FORCE_RESET();
__HAL_RCC_I2C1_RELEASE_RESET();

followed by re-initializing the peripheral.

 

 

Having a slave pulling SCL low periodically while the bus is idle violates I2C bus protocol, hence it will be annoying to deal with here.

If you feel a post has answered your question, please click "Accept as Solution".