Skip to content

Commit ef965b2

Browse files
committed
feat(uhci): Add uart dma ota example for uhci
1 parent 6b988d8 commit ef965b2

12 files changed

+368
-0
lines changed

examples/peripherals/.build-test-rules.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,14 @@ examples/peripherals/twai/twai_self_test:
506506
temporary: true
507507
reason: lack of runners
508508

509+
examples/peripherals/uart/uart_dma_ota:
510+
disable:
511+
- if: SOC_UHCI_SUPPORTED != 1
512+
disable_test:
513+
- if: IDF_TARGET in ["esp32p4"]
514+
temporary: true
515+
reason: Lack runners
516+
509517
examples/peripherals/uart/uart_echo_rs485:
510518
enable:
511519
- if: INCLUDE_DEFAULT == 1
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# The following lines of boilerplate have to be in your project's CMakeLists
2+
# in this exact order for cmake to work correctly
3+
cmake_minimum_required(VERSION 3.16)
4+
5+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
6+
project(uart_dma_ota)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
| Supported Targets | ESP32-C3 | ESP32-C6 | ESP32-P4 | ESP32-S3 |
2+
| ----------------- | -------- | -------- | -------- | -------- |
3+
4+
# UART OTA Example
5+
6+
This example demonstrates how to use UART to perform an Over-the-Air (OTA) firmware update on an ESP32-based device. It transfers firmware data via UART using DMA and applies the update seamlessly.
7+
8+
## How to use example
9+
10+
### Hardware Required
11+
12+
The example can be run on any development board, that is based on the Espressif SoC. The board shall be connected to a computer with a single USB cable for flashing and monitoring. The external interface should have 3.3V outputs. You may use e.g. 3.3V compatible USB-to-Serial dongle.
13+
14+
### Software Required
15+
16+
1. Partition Table:
17+
18+
The example requires a valid partition table with an OTA partition. Update your partitions.csv or equivalent partition table to include an OTA data partition. Below is an example of a partition table with OTA support:
19+
20+
```
21+
# Name, Type, SubType, Offset, Size, Flags
22+
nvs, data, nvs, 0x9000, 0x6000,
23+
factory, app, factory, 0x10000, 1M,
24+
ota_0, app, ota_0, 0x110000, 1M,
25+
ota_1, app, ota_1, 0x210000, 1M,
26+
```
27+
28+
You can also use `CONFIG_PARTITION_TABLE_TWO_OTA_LARGE` if you don't want to define a partition table by yourself.
29+
30+
2. Firmware Binary:
31+
32+
Prepare the firmware binary (.bin file) you want to flash via OTA. This binary should be compatible with the ESP32 platform and must match the partition table configuration.
33+
Example:
34+
35+
*1.* Build the firmware binary via:
36+
37+
```
38+
idf.py build
39+
```
40+
41+
*2.* Locate the binary file in the build/ directory (e.g., build/your_project.bin)
42+
43+
### Setup the Hardware
44+
45+
Connect the external serial interface to the board as follows.
46+
47+
```
48+
49+
+-------------------+ +-------------------+
50+
| ESP Chip | | External UART Pin |
51+
| Interface | | |
52+
+-------------------+ +-------------------+
53+
| |
54+
| |
55+
Receive Data (RxD): + GPIO9 <------------------------------> TxD +
56+
| |
57+
Ground: + GND <--------------------------------> GND +
58+
| |
59+
60+
```
61+
62+
### Configure the project
63+
64+
Use the command below to configure project using Kconfig menu as showed in the table above.
65+
The default Kconfig values can be changed such as: UART_BAUD_RATE, UART_PORT_NUM, UART_RX_IO (Refer to Kconfig file).
66+
67+
```
68+
idf.py menuconfig
69+
```
70+
71+
### Build and Flash
72+
73+
Build the project and flash it to the board, then run monitor tool to view serial output:
74+
75+
```
76+
idf.py -p PORT flash monitor
77+
```
78+
79+
Then transmit the prepared bin to ESP-chips. Please note that the bin should be in one packet transaction which means there should not have any interval during the transaction in this example because it uses idle to judge whether the packet finishes or not.
80+
81+
(To exit the serial monitor, type ``Ctrl-]``.)
82+
83+
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
84+
85+
## Example Output
86+
87+
```
88+
I (277) main_task: Started on CPU0
89+
I (287) main_task: Calling app_main()
90+
I (287) uhci-example: UHCI initialized
91+
I (447) uhci-example: OTA process started
92+
I (18047) uhci-example: Received size: 150032
93+
I (18047) uhci-example: Total received size: 150032
94+
I (18047) esp_image: segment 0: paddr=001d0020 vaddr=3c020020 size=06748h ( 26440) map
95+
I (18057) esp_image: segment 1: paddr=001d6770 vaddr=3fc8b400 size=0111ch ( 4380)
96+
I (18067) esp_image: segment 2: paddr=001d7894 vaddr=40380000 size=08784h ( 34692)
97+
I (18077) esp_image: segment 3: paddr=001e0020 vaddr=42000020 size=11d98h ( 73112) map
98+
....
99+
I (18147) uhci-example: OTA update successful. Rebooting...
100+
.... (the following part depends on your bin, this output depends on example/get-started/hello_world)
101+
I (268) sleep_gpio: Enable automatic switching of GPIO sleep configuration
102+
I (275) main_task: Started on CPU0
103+
I (275) main_task: Calling app_main()
104+
Hello world!
105+
This is esp32c3 chip with 1 CPU core(s), WiFi/BLE, silicon revision v0.4, 4MB external flash
106+
Minimum free heap size: 333524 bytes
107+
Restarting in 10 seconds...
108+
Restarting in 9 seconds...
109+
Restarting in 8 seconds...
110+
```
111+
112+
113+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
idf_component_register(SRCS "uart_dma_ota_example_main.c"
2+
REQUIRES esp_driver_uart app_update
3+
INCLUDE_DIRS ".")
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
menu "Example Configuration"
2+
3+
config UART_RX_IO
4+
int "UART RX GPIO Num"
5+
default 2
6+
help
7+
GPIO number for UART RX line.
8+
9+
config UART_PORT_NUM
10+
int "UART port number"
11+
default 1
12+
help
13+
The UART device attached to DMA.
14+
15+
config UART_BAUD_RATE
16+
int "uart baud rate"
17+
default 1000000
18+
help
19+
Uart baud rate.
20+
21+
endmenu
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include <stdio.h>
8+
#include <string.h>
9+
#include "freertos/FreeRTOS.h"
10+
#include "freertos/task.h"
11+
#include "freertos/queue.h"
12+
#include "driver/uart.h"
13+
#include "driver/uhci.h"
14+
#include "esp_log.h"
15+
#include "esp_ota_ops.h"
16+
#include "esp_err.h"
17+
#include "esp_check.h"
18+
19+
static const char *TAG = "uhci-example";
20+
21+
#define EXAMPLE_UART_NUM CONFIG_UART_PORT_NUM
22+
#define EXAMPLE_UART_BAUD_RATE CONFIG_UART_BAUD_RATE
23+
#define EXAMPLE_UART_RX_IO CONFIG_UART_RX_IO
24+
#define UART_DMA_OTA_BUFFER_SIZE (10 * 1024)
25+
26+
typedef enum {
27+
UHCI_EVT_PARTIAL_DATA,
28+
UHCI_EVT_EOF,
29+
} uhci_event_t;
30+
31+
typedef struct {
32+
QueueHandle_t uhci_queue;
33+
size_t receive_size;
34+
uint8_t *ota_data1;
35+
uint8_t *ota_data2;
36+
bool use_ota_data1;
37+
} ota_example_context_t;
38+
39+
static bool s_uhci_rx_event_cbs(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx)
40+
{
41+
ota_example_context_t *ctx = (ota_example_context_t *)user_ctx;
42+
BaseType_t xTaskWoken = 0;
43+
uhci_event_t evt = 0;
44+
45+
if (edata->flags.totally_received) {
46+
evt = UHCI_EVT_EOF;
47+
} else {
48+
evt = UHCI_EVT_PARTIAL_DATA;
49+
}
50+
51+
// Choose the buffer to store received data
52+
ctx->receive_size = edata->recv_size;
53+
if (ctx->use_ota_data1) {
54+
ctx->ota_data1 = edata->data;
55+
} else {
56+
ctx->ota_data2 = edata->data;
57+
}
58+
59+
// Toggle the buffer for the next receive
60+
ctx->use_ota_data1 = !ctx->use_ota_data1;
61+
62+
xQueueSendFromISR(ctx->uhci_queue, &evt, &xTaskWoken);
63+
return xTaskWoken;
64+
}
65+
66+
static void perform_ota_update(uhci_controller_handle_t uhci_ctrl, ota_example_context_t *ctx)
67+
{
68+
const esp_partition_t *ota_partition = esp_ota_get_next_update_partition(NULL);
69+
if (!ota_partition) {
70+
ESP_LOGE(TAG, "No OTA partition found");
71+
return;
72+
}
73+
74+
esp_ota_handle_t ota_handle;
75+
ESP_ERROR_CHECK(esp_ota_begin(ota_partition, OTA_SIZE_UNKNOWN, &ota_handle));
76+
ESP_LOGI(TAG, "OTA process started");
77+
78+
uhci_event_t evt;
79+
uint32_t received_size = 0;
80+
uint8_t *pdata = heap_caps_calloc(1, UART_DMA_OTA_BUFFER_SIZE, MALLOC_CAP_DEFAULT);
81+
assert(pdata);
82+
ESP_ERROR_CHECK(uhci_receive(uhci_ctrl, pdata, UART_DMA_OTA_BUFFER_SIZE));
83+
while (1) {
84+
if (xQueueReceive(ctx->uhci_queue, &evt, portMAX_DELAY) == pdTRUE) {
85+
uint8_t *data_to_write = ctx->use_ota_data1 ? ctx->ota_data2 : ctx->ota_data1;
86+
ESP_ERROR_CHECK(esp_ota_write(ota_handle, data_to_write, ctx->receive_size));
87+
received_size += ctx->receive_size;
88+
if (evt == UHCI_EVT_EOF) {
89+
break;
90+
}
91+
92+
}
93+
}
94+
free(pdata);
95+
96+
ESP_LOGI(TAG, "Total received size: %ld", received_size);
97+
ESP_ERROR_CHECK(esp_ota_end(ota_handle));
98+
ESP_ERROR_CHECK(esp_ota_set_boot_partition(ota_partition));
99+
}
100+
101+
void app_main(void)
102+
{
103+
uart_config_t uart_config = {
104+
.baud_rate = EXAMPLE_UART_BAUD_RATE,
105+
.data_bits = UART_DATA_8_BITS,
106+
.parity = UART_PARITY_DISABLE,
107+
.stop_bits = UART_STOP_BITS_1,
108+
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
109+
.source_clk = UART_SCLK_DEFAULT,
110+
};
111+
112+
ESP_ERROR_CHECK(uart_param_config(EXAMPLE_UART_NUM, &uart_config));
113+
ESP_ERROR_CHECK(uart_set_pin(EXAMPLE_UART_NUM, -1, EXAMPLE_UART_RX_IO, -1, -1));
114+
115+
uhci_controller_config_t uhci_cfg = {
116+
.uart_port = EXAMPLE_UART_NUM,
117+
.tx_trans_queue_depth = 1,
118+
.max_receive_internal_mem = UART_DMA_OTA_BUFFER_SIZE,
119+
.max_transmit_size = UART_DMA_OTA_BUFFER_SIZE,
120+
.dma_burst_size = 32,
121+
.rx_eof_flags.idle_eof = 1, // receive finishes when rx line turns idle.
122+
};
123+
124+
uhci_controller_handle_t uhci_ctrl;
125+
ESP_ERROR_CHECK(uhci_new_controller(&uhci_cfg, &uhci_ctrl));
126+
127+
ESP_LOGI(TAG, "UHCI initialized, baud rate is %d, rx pin is %d", uart_config.baud_rate, EXAMPLE_UART_RX_IO);
128+
129+
ota_example_context_t *ctx = calloc(1, sizeof(ota_example_context_t));
130+
assert(ctx);
131+
132+
ctx->uhci_queue = xQueueCreate(2, sizeof(uhci_event_t));
133+
assert(ctx->uhci_queue);
134+
135+
ctx->use_ota_data1 = true; // Start with ota_data1
136+
137+
uhci_event_callbacks_t uhci_cbs = {
138+
.on_rx_trans_event = s_uhci_rx_event_cbs,
139+
};
140+
141+
ESP_ERROR_CHECK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, ctx));
142+
143+
perform_ota_update(uhci_ctrl, ctx);
144+
145+
ESP_ERROR_CHECK(uhci_del_controller(uhci_ctrl));
146+
147+
free(ctx);
148+
ESP_LOGI(TAG, "OTA update successful. Rebooting...");
149+
esp_restart();
150+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: CC0-1.0
3+
import os
4+
5+
import pytest
6+
import serial
7+
from pytest_embedded import Dut
8+
from pytest_embedded_idf.utils import idf_parametrize
9+
10+
PACKET_SIZE = 10 * 1024 # 10 KB for upper computer
11+
FLASH_PORT = '/dev/serial_ports/ttyUSB-esp32'
12+
13+
14+
def send_file_via_uart(port: str, baud_rate: int, file_path: str, packet_size: int) -> None:
15+
try:
16+
with serial.Serial(port, baud_rate, timeout=1) as ser:
17+
print(f'Opened {port} at {baud_rate} baud')
18+
19+
file_size = os.path.getsize(file_path)
20+
print(f'File size: {file_size} bytes')
21+
22+
with open(file_path, 'rb') as file:
23+
bytes_sent = 0
24+
while bytes_sent < file_size:
25+
chunk = file.read(packet_size)
26+
if not chunk:
27+
break
28+
ser.write(chunk)
29+
bytes_sent += len(chunk)
30+
31+
print('File sent successfully!')
32+
33+
except Exception as e:
34+
print(f'Error: {e}')
35+
36+
37+
@pytest.mark.usb_serial_jtag
38+
@pytest.mark.parametrize(
39+
'port, flash_port, config',
40+
[
41+
pytest.param('/dev/serial_ports/ttyACM-esp32', FLASH_PORT, 'defaults'),
42+
],
43+
indirect=True,
44+
)
45+
@idf_parametrize('target', ['esp32c6', 'esp32c3', 'esp32s3'], indirect=['target'])
46+
def test_uart_dma_ota(dut: Dut) -> None:
47+
dut.expect_exact('uhci-example: OTA process started')
48+
# We OTA the same binary to another partition and switch to there.
49+
binary_path = os.path.join(dut.app.binary_path, 'uart_dma_ota.bin')
50+
assert os.path.exists(binary_path), f'OTA binary not found at {binary_path}'
51+
52+
buad_rate = dut.app.sdkconfig.get('UART_BAUD_RATE')
53+
send_file_via_uart(FLASH_PORT, buad_rate, binary_path, PACKET_SIZE)
54+
55+
dut.expect('OTA update successful. Rebooting', timeout=10)
56+
dut.expect('ESP-ROM:', timeout=10)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
2+
CONFIG_UART_PORT_NUM=0
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CONFIG_IDF_TARGET="esp32c3"
2+
CONFIG_UART_RX_IO=20
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CONFIG_IDF_TARGET="esp32c6"
2+
CONFIG_UART_RX_IO=17

0 commit comments

Comments
 (0)