UART in ESP32 || Control LED from Computer
This is Second tutorial in the ESP32 Series, and today we will continue using the UART to control the LED connected to the ESP32. I will be using the ESP32 WROOM Devkit for this tutorials along with the Espressif IDE (ESP-IDF).
I have already covered how to use the UART for simple transmit and receive in the previous tutorial. This tutorial will specifically focus on how to make use of the received data to control the LED.
Let’s start with the IDE and we will create a new project.
Setting up the Project
We will first create a project using the examples provided in the IDE by default. Later we will modify it according to our need.
- Fist we will create a new Espressif IDF project
- Then give some name to this project and click next
- check “Create project using one of the templates”
- goto Peripherals -> uart and select the “uart_async_rxtxtasks“
- The name of the project will be modified after this, so make sure you change it back
- Click finish to generate the project
The example we chose above is simplest example to start with UART. The ESP32 also supports interrupt and DMA for uart, but in this tutorial we will do the simplest method of sending and receiving data, i.e using the blocking mode.
The code will generate and you can find the main.c file in the main folder. The pre-generated code will also work pretty well, but here I will modify it a little and explain you the entire code.
Connection
ESP32 have 3 UARTs, UART0, 1 and 2. I am going to use the UART2 as the pins for UART2 are defined clearly on the board. This is shown in the picture below
As shown in the image above, the RX2 and TX2 pins, GPIO16 and GPIO17 respectively, represents the UART2 pins. I am going to connect them to the FT232 USB to UART device. The pins must be cross connected, Rx to TX and TX to RX. This is shown in the picture below.
![](https://controllerstech.com/wp-content/uploads/2024/07/esp32_uart2_1.avif)
- I have the TX2 (GPIO_17) connected to the RX pin of the FTDI
- The RX2 (GPIO_16) is connected to the TX pin of the FTDI
- As I mentioned before, for the UART to communicate, the devices must be cross connected.
- Also the LED is connected to the pin 13 (GPIO_13). There is a 100Ω Resistor connected between the ground and the LED in order to reduce the voltage supplied to the LED.
The CODE
Defines
#define UART_NUM UART_NUM_2
#define TXD_PIN (GPIO_NUM_17)
#define RXD_PIN (GPIO_NUM_16)
uint8_t RxData[128];
static const int RX_BUF_SIZE = 128;
#define LED_NUM GPIO_NUM_13
TaskHandle_t txTaskHandle;
- As I mentioned before, I am going to use the UART2 in this tutorial, so I have defined the UART_NUM as UART_NUM_2.
- Now we can just simply use the UART_NUM wherever the uart number is needed to be provided.
- I have modified the Tx and Rx Pins as per my setup.
- The RxData buffer is defined as 128 Bytes. It will be used to store the data received via the UART.
- Global definition for the RX buffer is necessary as we want to use this buffer in TX Task too.
- The Rx buffer size is defined as 128 Bytes.
- The LED is connected to GPIO_13, so the LED_NUM is defined accordingly.
- I have also defined the Task Handle for the TX Task. We want to control the execution (suspend and resume) of the TX task, so handle is needed for that.
Initialize the LED
void ledConfig (void)
{
gpio_reset_pin(LED_NUM);
gpio_set_direction(LED_NUM, GPIO_MODE_OUTPUT);
}
The ledConfig function initialises the LED pin. Inside this function we will
- Reset the LED pin.
- Set the Direction of the LED pin as OUTPUT.
Initialize the UART
void init(void)
{
const uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
// We won't use a buffer for sending data.
uart_driver_install(UART_NUM, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
uart_param_config(UART_NUM, &uart_config);
uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
uart_driver_install(UART_NUM, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
- Here First of all we will install the UART Driver. The
uart_driver_install
function takes the following argument- @
uart_port_t
, The UART Instance you are using, UART2 in this example - @
int rx_buffer_size
, The size of the RX Ring Buffer. It should be greater than minimum length, which is defined in the soc_caps.h file#define SOC_UART_FIFO_LEN (128
). Here the RX_BUF_SIZE is multiplied by 2, so the Ring Buffer Size will be 256 Bytes. - @
int tx_buffer_size
, The size of the TX Ring Buffer. We are not using the TX FIFO, so the size is set to 0 in this example. - @
int queue_size
, The UART Queue size, 0 in this case since we are not using any queue in this tutorial - @
QueueHandle_t *uart_queue
, The UART queue, NULL in this example since we are not using the queue - @
int intr_alloc_flags
, The interrupt FLAG, 0 in this example since we are not using interrupt
- @
uart_param_config(UART_NUM, &uart_config);
- Then we will configure the UART parameters. The configuration used is 8-N-1 with the baud rate of 115200. Hardware flow control is disabled and the UART source clock is set to DEFAULT, which is actually the APB clock.
uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
- Finally we will set the pins for the UART. The
uart_set_pin
function takes the following parameters- @
uart_port_t
, The UART_NUM you are using, UART2 in this example - @
int tx_io_num
, The TX Pin number, GPIO_NUM_17 in this example - @
int rx_io_num
, The RX Pin number, GPIO_NUM_16 in this example - @
int rts_io_num
and @int cts_io_num
, The RTS and CTS Pin numbers. We are not using hardware flow control so these parameters are set toUART_PIN_NO_CHANGE
(-1).
- @
The TX Task
static void tx_task(void *arg)
{
uint8_t *data = (uint8_t*) malloc(150);
while (1)
{
int len = sprintf((char *)data, "Data Received is '%s' \n", RxData);
uart_write_bytes(UART_NUM, data, len);
vTaskSuspend(NULL);
}
free(data);
}
The TX task will send data once and then it will go into the suspension.
- First we will allocate the 150 bytes to the data buffer, where we will store the data to be transmitted.
- We will modify the data received by the RX Task (Stored in the RxData array).
- We will store the modified data into the data buffer. Here we will just add a small string (“Data Received is”) in front of the received data.
uart_write_bytes
will be used to write the data. The function takes the following parameters- @
uart_port_t
, The UART Instance you are using, UART2 in this example. - @
const void* src,
The pointer to the source address, data in this example. - @
size_t size
, The length of the data to be transmitted, len in this example.
- @
- After transmitting the data, we will suspend the tx_task.
- vTaskSuspend is used to suspend any task. The parameter of this function is the task we want to suspend and passing the NULL parameter suspends the calling task itself.
- At last we will free the allocated memory to the data buffer.
The RX Task
static void rx_task(void *arg)
{
int ledState = 0;
static const char *RX_TASK_TAG = "RX_TASK";
esp_log_level_set(RX_TASK_TAG, ESP_LOG_INFO);
while (1) {
const int rxBytes = uart_read_bytes(UART_NUM, RxData, RX_BUF_SIZE, 200 / portTICK_PERIOD_MS);
if (rxBytes > 0) {
RxData[rxBytes] = 0;
if (strcmp((char*)RxData, "ON") == 0) ledState = 1;
else if (strcmp((char*)RxData, "OFF") == 0) ledState = 0;
gpio_set_level(LED_NUM, ledState);
vTaskResume(txTaskHandle);
ESP_LOGI(RX_TASK_TAG, "Read %d bytes: '%s'", rxBytes, RxData);
}
}
}
RX Task will be used to receive the data from the UART and then control the LED based on the received command. It will also resume the TX task, so that it can transmit the data.
- Here we will first define a variable ledState, which will be used to keep track of the LED state.
- Then create the RX_TASK_TAG, which will be used to log the information data to the terminal.
- Receive the data using the function
uart_read_bytes
. It takes the following parameters.- @
uart_port_t
, The UART Instance you are using, UART2 in this example. - @
void* buf
, The pointer to the buffer where the data will be saved, RxData in this example. - @
uint32_t length
, The length of the buffer, 128 bytes in this example. - @
TickType_t ticks_to_wait
, The time to wait for the data to arrive, 200 ms in this example.
- @
If the required amount of data (RX_BUF_SIZE) does not arrive in 200ms, the timeout will occur. Whatever data was received in this 200ms will be stored in the RxData buffer. This data is then logged to the terminal.
The uart_read_bytes
returns the number of bytes received via the UART. This number is stored in the rxBytes variable. So if even a single byte is received, this variable will be greater than 0.
if (rxBytes > 0)
{
RxData[rxBytes] = 0;
if (strcmp((char*)RxData, "ON") == 0) ledState = 1;
else if (strcmp((char*)RxData, "OFF") == 0) ledState = 0;
gpio_set_level(LED_NUM, ledState);
vTaskResume(txTaskHandle);
ESP_LOGI(RX_TASK_TAG, "Read %d bytes: '%s'", rxBytes, RxData);
}
We will write the rest of the code inside this condition, if (rxBytes > 0)
- Here we will compare the RxData array with the string (ON). If the buffer matches with the string, the strcmp function will return 0 and we will set the variable ledState to 1.
- Then compare the RxData array with another string (OFF). If the buffer matches with the string, we will reset the variable ledState to 0.
- Next we will set the GPIO level as per the current LED State (ledState).
- Then we will resume the TX Task, so that it can transmit the modified data.
- The parameter of the function vTaskResume is Task Handle of the task which you want to resume.
- Finally the RX Task will log the received data to the terminal.
The main function
void app_main(void)
{
init();
ledConfig();
xTaskCreate(rx_task, "uart_rx_task", 1024 * 2, NULL, configMAX_PRIORITIES - 1, NULL);
xTaskCreate(tx_task, "uart_tx_task", 1024 * 2, NULL, configMAX_PRIORITIES - 2, &txTaskHandle);
}
- In the main function, we will first initialize the UART and the LED.
- Then we will create 2 tasks, rx_task and tx_task
- The stack size for both the tasks is set to 2 Kilobytes
- The priority of the receiver task is highest, and that of the transmitter task is 1 below the receiver task
- After the Tasks has been created, the rx_task will start looking for the data sent by the computer. When the “ON” or “OFF” command is sent, the LED will toggle accordingly. Also the tx_task will transmit the modified received data via the UART.
Result
Below is the output of the above project.
As you can see in the gif above:
- When the command ON is sent, the LED is turned on. Also the modified data (Data received is ‘ON’) is received back to the serial console.
- Similarly, when the command OFF is sent, the LED turned off. The modified data is again received by the serial console.