Unexpected I2C timeout on ESP32
Posted: Wed Mar 22, 2023 8:57 am
I am experiencing a strange I2C timeout in previously functional software, which used to run on an ESP32-S3 chip, after trying to use the same software on an ESP32 (no -S3) chip. The software is rather simple and connects to a u-blox GNSS chip to read UBX navigation messages. The chip's I2C interface exposes registers numbered 0xfd, 0xfe and 0xff. The registers 0xfd and 0xfe contain a 16 bit integer with the number of bytes available to read from the chips internal buffer and multiple reads from register 0xff gives access to the actual UBX message. The chip features a 'register auto-increment' function, meaning that if you read more than one byte after selecting a register, the register pointer will auto-increment and you will get the data in the following registers. If the register pointer has reached 0xff, it stays at 0xff.
Removing all error checking, the code looks more or less like this:
As pointed out, this code works perfectly when running on an ESP-S3 chip. After trying the code on an ESP32 chip, the read and write methods started to sproadically return ESP_ERR_TIMEOUT, based on the timing without honouring the requested timeout provided in I2C_TIMEOUT. Even the first call to i2c_master_write_read_device (to read the number of available bytes) could fail with ESP_ERR_TIMEOUT, but a typical flow would look like this:
Considering that I may have a glitch, perhaps even somewhere else in the software, I reimplemented the core logic for I2C communication in a tiny project in Arduino IDE using the Wire library abstraction. The library simplifies the code and leaves less room for own mistakes and I could actually confirm, that when implemented with the Wire library, the communication worked as expected also on the ESP32.
Digging through the implementation of the Wire library for ESP32, the only obviously relevant difference I could find is that in the function i2cInit in esp32-hal-i2c.c the function i2c_set_timeout is called like this:
For the ESP32, I2C_LL_MAX_TIMEOUT translates through multiple defines to the value 0xFFFFF.
Adding this function call to my software solved the problem there as well. I can now run the I2C bus at 400 kbps (the fastest speed supported by the u-blox chip) without running into the problems with the ESP_ERR_TIMEOUT error.
Just to again clarify: Using i2c_set_timeout is only required when the code runs on the ESP32 chip. When the code runs on the ESP32-S3 chip, it works without using i2c_set_timeout.
Since the documentation is very weak at this point, here are two questions:
What is the difference between the timeout set with i2c_set_timeout and the timeout passed as the last argument to the other read and write functions? In the Wire library, the value passed to i2c_set_timeout (0xFFFFF) seems to translate to about 13ms (it is documented as the number of 80 MHz ticks) and the default value of the timeout passed to the read/write functions is set to 50ms. This does not make obvious sense to me.
Why is the usage of i2c_set_timeout only required on the ESP32 chip and not on the ESP32-S3 chip? Looking through the code examples linked in the Espressif documentation, I find no actual usage of the i2c_set_timeout function and I don't get the impression that it is supposed to be required.
Removing all error checking, the code looks more or less like this:
Code: Select all
// initialize the I2C bus with i2c_param_config and i2c_driver_install,
// then in a loop, use the following code to read individual UBX messages
uint8_t *buffer = ...;
uint8_t reg = 0xfd;
// write 0xfd to the chip (select register 0xfd) and read two bytes
i2c_master_write_read_device(I2C_PORT, ZEDF9P_ADDR, ®, 1, buffer, 2, I2C_TIMEOUT);
// buffer[0] now contains the value in register 0xfd and buffer[1] the value in register 0xfe,
// the number of available bytes is big endian encoded
uint16_t available = (buffer[0] << 8) | buffer[1];
if (available > 6) { // UBX messages are longer than 6 bytes
// the register pointer is now at 0xff, so we can read actual data without requesting a specific register
// first read the 6 byte UBX header:
i2c_master_read_from_device(I2C_PORT, ZEDF9P_ADDR, buffer, 6, I2C_TIMEOUT);
// buffer[0] and buffer[1] will now contain the UBX prefix 0xb562
// buffer[2] and buffer[3] will contain the message type (class and id)
// buffer[4] and buffer[5] the (now little endian encoded) length of the actual message data:
uint16_t length = buffer[4] | (buffer[5] << 8);
// read the remaining part of the UBX message and two additional bytes with checksum data:
i2c_master_read_from_device(I2C_PORT, ZEDF9P_ADDR, buffer + 6, length + 2, I2C_TIMEOUT);
// the buffer should now contain one complete UBX message with header, payload data and checksum bytes
}
- the first call to i2c_master_write_read_device would succeed and indicate that 100 bytes of data are available
- the second call to i2c_master_read_from_device (reading the 6 bytes UBX header) would fail with ESP_ERR_TIMEOUT within milliseconds, although a much higher timeout was specified with I2C_TIMEOUT
- reading the number of available bytes from register 0xfd and 0xfe again would show that at least some number of bytes or even all 6 bytes have actually been read although the read method returned ESP_ERR_TIMEOUT
Considering that I may have a glitch, perhaps even somewhere else in the software, I reimplemented the core logic for I2C communication in a tiny project in Arduino IDE using the Wire library abstraction. The library simplifies the code and leaves less room for own mistakes and I could actually confirm, that when implemented with the Wire library, the communication worked as expected also on the ESP32.
Digging through the implementation of the Wire library for ESP32, the only obviously relevant difference I could find is that in the function i2cInit in esp32-hal-i2c.c the function i2c_set_timeout is called like this:
Code: Select all
i2c_set_timeout((i2c_port_t)i2c_num, I2C_LL_MAX_TIMEOUT);
Adding this function call to my software solved the problem there as well. I can now run the I2C bus at 400 kbps (the fastest speed supported by the u-blox chip) without running into the problems with the ESP_ERR_TIMEOUT error.
Just to again clarify: Using i2c_set_timeout is only required when the code runs on the ESP32 chip. When the code runs on the ESP32-S3 chip, it works without using i2c_set_timeout.
Since the documentation is very weak at this point, here are two questions:
What is the difference between the timeout set with i2c_set_timeout and the timeout passed as the last argument to the other read and write functions? In the Wire library, the value passed to i2c_set_timeout (0xFFFFF) seems to translate to about 13ms (it is documented as the number of 80 MHz ticks) and the default value of the timeout passed to the read/write functions is set to 50ms. This does not make obvious sense to me.
Why is the usage of i2c_set_timeout only required on the ESP32 chip and not on the ESP32-S3 chip? Looking through the code examples linked in the Espressif documentation, I find no actual usage of the i2c_set_timeout function and I don't get the impression that it is supposed to be required.