Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 31

Implementing Timeouts for Blocking Serial Communication

Recommended resources

Hello! Welcome to our next lesson on serial communication.

In the last session, we built robust I2C code by checking API return values, making our application resilient to transient communication failures. We learned to identify errors like ESP_ERR_TIMEOUT and implement retry logic. Today, we take the next logical step: you will learn how to implement a timeout mechanism for blocking serial communication calls.

This lesson will give you direct control over how long your program is willing to wait for a peripheral. This prevents a single, unresponsive device from freezing your entire system. This concept is fundamental to creating reliable, responsive embedded systems.

We will cover:

  • The concept of "blocking" vs. "non-blocking" function calls.
  • How indefinite blocking can halt your application.
  • How to use the timeout parameters in the ESP-IDF's UART and I2C driver APIs.
  • The trade-offs involved in choosing an appropriate timeout value.

1. The Problem: Blocking Calls

Many functions, especially those dealing with I/O, are blocking. This means the function will not return until its operation is complete. For example, a function to read 10 bytes from a serial port might wait indefinitely if the data never arrives. While waiting, your program is completely stuck at that line of code, unable to do anything else.

To get an intuitive feel for this problem, we'll watch a video that demonstrates it using a simple, common example: blinking LEDs with Arduino's delay() function. The principle is exactly the same for an ESP32 waiting on a sensor.

LIVE Shop Talk 35: Understanding Blocking and Non-Blocking Operations and Functions

This video from Paul McWhorter, 'LIVE Shop Talk 35: Understanding Blocking and Non-Blocking Operations and Functions', provides an excellent visual demonstration of what a blocking function is and why it's a problem in embedded systems.

Please watch from 01:16 to 09:48. You'll see: A simple sketch to blink one, then two LEDs using the delay() function. This works fine when they do the same thing. The problem becomes obvious when trying to blink the two LEDs at different rates. The presenter explains exactly why this happens: the delay() function is blocking, and the microcontroller can only execute one instruction at a time. While it's stuck in delay(), no other code can run.

As you saw, the delay() function prevents any other part of the loop from running until it has finished. A serial read function waiting for data from an unpowered or disconnected sensor behaves in the exact same way—it can block your program forever.

From your mechanical engineering background, you can think of this like a critical control loop that gets stuck waiting for a sensor update. If the sensor fails and the code blocks, the loop can no longer send updated commands to actuators, potentially leading to system instability or failure.


2. Timeouts: The Solution to Blocking

The solution is to put a limit on how long a blocking function is allowed to wait. This is a timeout. If the operation doesn't complete within the specified time, the function gives up and returns an error, allowing your program to continue.

Many ESP-IDF functions that perform I/O operations have a built-in timeout parameter for this very reason. Let's look at how to use them for our serial protocols.


3. Implementing UART Timeouts

For UART communication, the primary function for reading data is uart_read_bytes(). As we'll see in the documentation, it includes a parameter specifically for setting a timeout.

Universal Asynchronous Receiver/Transmitter (UART) - ESP32

Let's look at the official ESP-IDF documentation for the uart_read_bytes function.

In the API Reference section, find the documentation for the uart_read_bytes function. Focus on its last parameter: TickType_t ticks_to_wait.

The ticks_to_wait parameter specifies the maximum number of FreeRTOS "ticks" (the smallest unit of time for the scheduler) that the function should wait. Waiting forever can be specified with portMAX_DELAY.

Conveniently, FreeRTOS provides a macro pdMS_TO_TICKS() to convert milliseconds into ticks, which is much more intuitive.

The following video demonstrates the practical effect of this timeout parameter.

ESP32 UART Communication with ESP-IDF #1 | Simple TX & RX Tutorial

The video 'ESP32 UART Communication with ESP-IDF #1' by ControllersTech shows a practical example of using uart_read_bytes and adjusting its timeout.

Watch from 07:47 to 08:38, and then from 11:22 to 11:48 and 12:54 to 13:19. Notice: The explanation of the timeout parameter in uart_read_bytes. The visible latency when a long timeout (1 second) is used. How reducing the timeout to 200ms improves responsiveness. The discussion of the trade-off between latency and ensuring you receive all expected data.

Here is how you would implement a UART read with a 100ms timeout:

#include "driver/uart.h"
#include "esp_log.h"

#define BUF_SIZE 1024
static const char* TAG = "UART_TIMEOUT_EXAMPLE";

void read_from_uart() {
    uint8_t* data = (uint8_t*) malloc(BUF_SIZE);
    const uart_port_t uart_num = UART_NUM_2;
    
    // Set a timeout of 100 milliseconds
    const TickType_t timeout_ticks = pdMS_TO_TICKS(100);

    // Try to read up to BUF_SIZE bytes, waiting a maximum of 100ms.
    int length = uart_read_bytes(uart_num, data, BUF_SIZE, timeout_ticks);

    if (length > 0) {
        ESP_LOGI(TAG, "Read %d bytes", length);
        // Process the received data here...
    } else {
        // If length is 0, it means the timeout expired before any data arrived.
        // If length is -1, it indicates a driver error.
        ESP_LOGW(TAG, "UART read timed out or failed.");
    }

    free(data);
}

In the UART driver, a timeout is not considered an esp_err_t error; the function simply returns with the number of bytes it managed to read before the time ran out (which can be 0).


4. Implementing I2C Timeouts

The I2C driver handles timeouts in a way that aligns more closely with our previous lesson. Most I2C transaction functions return an esp_err_t and accept a timeout value in milliseconds.

Inter-Integrated Circuit (I2C) - ESP32

Now let's examine the I2C driver documentation. You'll see that a timeout parameter is a standard feature across most of its functions.

In the API reference, look at the definitions for i2c_master_transmit() and i2c_master_receive(). Notice the final parameter, int xfer_timeout_ms, and the ESP_ERR_TIMEOUT return value in the 'Returns' section.

As the documentation shows, functions like i2c_master_transmit, i2c_master_receive, and i2c_master_transmit_receive all accept a timeout in milliseconds. If the bus is busy or the slave device doesn't respond within that time, the function will return ESP_ERR_TIMEOUT.

This makes handling the timeout straightforward, building on what you learned in the last lesson.

#include "driver/i2c.h"
#include "esp_log.h"
#include "esp_err.h"

static const char* TAG = "I2C_TIMEOUT_EXAMPLE";

esp_err_t read_i2c_sensor(i2c_master_dev_handle_t dev_handle, uint8_t* data_buf, size_t data_len) {
    // Set a timeout of 50 milliseconds for this transaction.
    int timeout_ms = 50;

    esp_err_t ret = i2c_master_receive(dev_handle, data_buf, data_len, timeout_ms);

    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "I2C read successful.");
    } else if (ret == ESP_ERR_TIMEOUT) {
        ESP_LOGE(TAG, "I2C read timed out after %d ms.", timeout_ms);
        // This is where you might implement retry logic from the previous lesson.
    } else {
        ESP_LOGE(TAG, "I2C read failed: %s", esp_err_to_name(ret));
    }

    return ret;
}

Setting the timeout to -1 tells the function to wait forever, which is generally not recommended unless you have a specific reason to do so.


5. Choosing the Right Timeout Value

Selecting a timeout value is a balancing act:

  • Too Short: You risk ESP_ERR_TIMEOUT errors even when the device is working correctly but is just a bit slow to respond. This can be caused by sensor processing time or a busy communication bus.
  • Too Long: If a device fails, your application will be blocked for the full duration of the timeout, reducing its responsiveness. A 5-second timeout means your task is frozen for 5 seconds every time it tries to communicate with a dead sensor.

The ideal value depends on the specific device (check its datasheet for response times), the I2C bus speed, and your application's real-time requirements. A good starting point is often between 20ms and 100ms.

Test your understanding!

You are communicating with an I2C environmental sensor that, according to its datasheet, can take up to 45ms to complete a measurement and make the data available for reading after a command is sent. You are setting up a call to i2c_master_transmit_receive() to command a measurement and then read the result. Which timeout value is most appropriate?

  1. 10ms
  2. 50ms
  3. 1000ms
  4. -1 (wait forever)
Show answer

The best answer is 2. 50ms. A 10ms timeout is too short and will likely fail every time. 50ms provides a small buffer over the 45ms maximum time specified in the datasheet. A 1000ms timeout would work, but it would make your application unnecessarily sluggish if the sensor fails. Waiting forever is rarely a good idea.


Conclusion

You have now learned how to tame blocking calls and build more predictable, resilient embedded applications. By setting explicit timeouts, you ensure that a single unresponsive peripheral won't bring your entire system to a halt.

Key Takeaways:

  • Blocking calls halt program execution until they are complete, which can be dangerous if they wait indefinitely.
  • Timeouts provide an exit strategy, limiting the maximum time a function will block.
  • In ESP-IDF UART, use the ticks_to_wait parameter in uart_read_bytes(). A timeout here results in reading 0 bytes.
  • In ESP-IDF I2C, use the xfer_timeout_ms parameter in transaction functions. A timeout here returns the ESP_ERR_TIMEOUT error code, which you can handle explicitly.
  • Choosing a timeout value requires balancing device response time against system responsiveness.

This lesson concludes our module on Serial Communication Protocols. We've covered configuration, data parsing, error checking, and now, timeout handling for UART and I2C.

In our next lesson, we will begin a new module on a powerful feature of the ESP32: FreeRTOS. While timeouts prevent a task from blocking forever, they still cause it to block for a short period. With FreeRTOS, we will learn how to run multiple tasks concurrently, so that one task waiting on a sensor doesn't stop other tasks, like updating a display or controlling a motor, from running. This will be a major step forward in building complex applications.