feat(uhci): Add uart dma ota example for uhci

This commit is contained in:
C.S.M
2025-04-24 14:16:08 +08:00
parent 6b988d8a07
commit ef965b2fc1
12 changed files with 368 additions and 0 deletions

View File

@@ -506,6 +506,14 @@ examples/peripherals/twai/twai_self_test:
temporary: true
reason: lack of runners
examples/peripherals/uart/uart_dma_ota:
disable:
- if: SOC_UHCI_SUPPORTED != 1
disable_test:
- if: IDF_TARGET in ["esp32p4"]
temporary: true
reason: Lack runners
examples/peripherals/uart/uart_echo_rs485:
enable:
- if: INCLUDE_DEFAULT == 1

View File

@@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(uart_dma_ota)

View File

@@ -0,0 +1,113 @@
| Supported Targets | ESP32-C3 | ESP32-C6 | ESP32-P4 | ESP32-S3 |
| ----------------- | -------- | -------- | -------- | -------- |
# UART OTA Example
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.
## How to use example
### Hardware Required
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.
### Software Required
1. Partition Table:
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:
```
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
factory, app, factory, 0x10000, 1M,
ota_0, app, ota_0, 0x110000, 1M,
ota_1, app, ota_1, 0x210000, 1M,
```
You can also use `CONFIG_PARTITION_TABLE_TWO_OTA_LARGE` if you don't want to define a partition table by yourself.
2. Firmware Binary:
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.
Example:
*1.* Build the firmware binary via:
```
idf.py build
```
*2.* Locate the binary file in the build/ directory (e.g., build/your_project.bin)
### Setup the Hardware
Connect the external serial interface to the board as follows.
```
+-------------------+ +-------------------+
| ESP Chip | | External UART Pin |
| Interface | | |
+-------------------+ +-------------------+
| |
| |
Receive Data (RxD): + GPIO9 <------------------------------> TxD +
| |
Ground: + GND <--------------------------------> GND +
| |
```
### Configure the project
Use the command below to configure project using Kconfig menu as showed in the table above.
The default Kconfig values can be changed such as: UART_BAUD_RATE, UART_PORT_NUM, UART_RX_IO (Refer to Kconfig file).
```
idf.py menuconfig
```
### Build and Flash
Build the project and flash it to the board, then run monitor tool to view serial output:
```
idf.py -p PORT flash monitor
```
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.
(To exit the serial monitor, type ``Ctrl-]``.)
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
## Example Output
```
I (277) main_task: Started on CPU0
I (287) main_task: Calling app_main()
I (287) uhci-example: UHCI initialized
I (447) uhci-example: OTA process started
I (18047) uhci-example: Received size: 150032
I (18047) uhci-example: Total received size: 150032
I (18047) esp_image: segment 0: paddr=001d0020 vaddr=3c020020 size=06748h ( 26440) map
I (18057) esp_image: segment 1: paddr=001d6770 vaddr=3fc8b400 size=0111ch ( 4380)
I (18067) esp_image: segment 2: paddr=001d7894 vaddr=40380000 size=08784h ( 34692)
I (18077) esp_image: segment 3: paddr=001e0020 vaddr=42000020 size=11d98h ( 73112) map
....
I (18147) uhci-example: OTA update successful. Rebooting...
.... (the following part depends on your bin, this output depends on example/get-started/hello_world)
I (268) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (275) main_task: Started on CPU0
I (275) main_task: Calling app_main()
Hello world!
This is esp32c3 chip with 1 CPU core(s), WiFi/BLE, silicon revision v0.4, 4MB external flash
Minimum free heap size: 333524 bytes
Restarting in 10 seconds...
Restarting in 9 seconds...
Restarting in 8 seconds...
```

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "uart_dma_ota_example_main.c"
REQUIRES esp_driver_uart app_update
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,21 @@
menu "Example Configuration"
config UART_RX_IO
int "UART RX GPIO Num"
default 2
help
GPIO number for UART RX line.
config UART_PORT_NUM
int "UART port number"
default 1
help
The UART device attached to DMA.
config UART_BAUD_RATE
int "uart baud rate"
default 1000000
help
Uart baud rate.
endmenu

View File

@@ -0,0 +1,150 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/uart.h"
#include "driver/uhci.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_err.h"
#include "esp_check.h"
static const char *TAG = "uhci-example";
#define EXAMPLE_UART_NUM CONFIG_UART_PORT_NUM
#define EXAMPLE_UART_BAUD_RATE CONFIG_UART_BAUD_RATE
#define EXAMPLE_UART_RX_IO CONFIG_UART_RX_IO
#define UART_DMA_OTA_BUFFER_SIZE (10 * 1024)
typedef enum {
UHCI_EVT_PARTIAL_DATA,
UHCI_EVT_EOF,
} uhci_event_t;
typedef struct {
QueueHandle_t uhci_queue;
size_t receive_size;
uint8_t *ota_data1;
uint8_t *ota_data2;
bool use_ota_data1;
} ota_example_context_t;
static bool s_uhci_rx_event_cbs(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx)
{
ota_example_context_t *ctx = (ota_example_context_t *)user_ctx;
BaseType_t xTaskWoken = 0;
uhci_event_t evt = 0;
if (edata->flags.totally_received) {
evt = UHCI_EVT_EOF;
} else {
evt = UHCI_EVT_PARTIAL_DATA;
}
// Choose the buffer to store received data
ctx->receive_size = edata->recv_size;
if (ctx->use_ota_data1) {
ctx->ota_data1 = edata->data;
} else {
ctx->ota_data2 = edata->data;
}
// Toggle the buffer for the next receive
ctx->use_ota_data1 = !ctx->use_ota_data1;
xQueueSendFromISR(ctx->uhci_queue, &evt, &xTaskWoken);
return xTaskWoken;
}
static void perform_ota_update(uhci_controller_handle_t uhci_ctrl, ota_example_context_t *ctx)
{
const esp_partition_t *ota_partition = esp_ota_get_next_update_partition(NULL);
if (!ota_partition) {
ESP_LOGE(TAG, "No OTA partition found");
return;
}
esp_ota_handle_t ota_handle;
ESP_ERROR_CHECK(esp_ota_begin(ota_partition, OTA_SIZE_UNKNOWN, &ota_handle));
ESP_LOGI(TAG, "OTA process started");
uhci_event_t evt;
uint32_t received_size = 0;
uint8_t *pdata = heap_caps_calloc(1, UART_DMA_OTA_BUFFER_SIZE, MALLOC_CAP_DEFAULT);
assert(pdata);
ESP_ERROR_CHECK(uhci_receive(uhci_ctrl, pdata, UART_DMA_OTA_BUFFER_SIZE));
while (1) {
if (xQueueReceive(ctx->uhci_queue, &evt, portMAX_DELAY) == pdTRUE) {
uint8_t *data_to_write = ctx->use_ota_data1 ? ctx->ota_data2 : ctx->ota_data1;
ESP_ERROR_CHECK(esp_ota_write(ota_handle, data_to_write, ctx->receive_size));
received_size += ctx->receive_size;
if (evt == UHCI_EVT_EOF) {
break;
}
}
}
free(pdata);
ESP_LOGI(TAG, "Total received size: %ld", received_size);
ESP_ERROR_CHECK(esp_ota_end(ota_handle));
ESP_ERROR_CHECK(esp_ota_set_boot_partition(ota_partition));
}
void app_main(void)
{
uart_config_t uart_config = {
.baud_rate = EXAMPLE_UART_BAUD_RATE,
.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,
};
ESP_ERROR_CHECK(uart_param_config(EXAMPLE_UART_NUM, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(EXAMPLE_UART_NUM, -1, EXAMPLE_UART_RX_IO, -1, -1));
uhci_controller_config_t uhci_cfg = {
.uart_port = EXAMPLE_UART_NUM,
.tx_trans_queue_depth = 1,
.max_receive_internal_mem = UART_DMA_OTA_BUFFER_SIZE,
.max_transmit_size = UART_DMA_OTA_BUFFER_SIZE,
.dma_burst_size = 32,
.rx_eof_flags.idle_eof = 1, // receive finishes when rx line turns idle.
};
uhci_controller_handle_t uhci_ctrl;
ESP_ERROR_CHECK(uhci_new_controller(&uhci_cfg, &uhci_ctrl));
ESP_LOGI(TAG, "UHCI initialized, baud rate is %d, rx pin is %d", uart_config.baud_rate, EXAMPLE_UART_RX_IO);
ota_example_context_t *ctx = calloc(1, sizeof(ota_example_context_t));
assert(ctx);
ctx->uhci_queue = xQueueCreate(2, sizeof(uhci_event_t));
assert(ctx->uhci_queue);
ctx->use_ota_data1 = true; // Start with ota_data1
uhci_event_callbacks_t uhci_cbs = {
.on_rx_trans_event = s_uhci_rx_event_cbs,
};
ESP_ERROR_CHECK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, ctx));
perform_ota_update(uhci_ctrl, ctx);
ESP_ERROR_CHECK(uhci_del_controller(uhci_ctrl));
free(ctx);
ESP_LOGI(TAG, "OTA update successful. Rebooting...");
esp_restart();
}

View File

@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import os
import pytest
import serial
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
PACKET_SIZE = 10 * 1024 # 10 KB for upper computer
FLASH_PORT = '/dev/serial_ports/ttyUSB-esp32'
def send_file_via_uart(port: str, baud_rate: int, file_path: str, packet_size: int) -> None:
try:
with serial.Serial(port, baud_rate, timeout=1) as ser:
print(f'Opened {port} at {baud_rate} baud')
file_size = os.path.getsize(file_path)
print(f'File size: {file_size} bytes')
with open(file_path, 'rb') as file:
bytes_sent = 0
while bytes_sent < file_size:
chunk = file.read(packet_size)
if not chunk:
break
ser.write(chunk)
bytes_sent += len(chunk)
print('File sent successfully!')
except Exception as e:
print(f'Error: {e}')
@pytest.mark.usb_serial_jtag
@pytest.mark.parametrize(
'port, flash_port, config',
[
pytest.param('/dev/serial_ports/ttyACM-esp32', FLASH_PORT, 'defaults'),
],
indirect=True,
)
@idf_parametrize('target', ['esp32c6', 'esp32c3', 'esp32s3'], indirect=['target'])
def test_uart_dma_ota(dut: Dut) -> None:
dut.expect_exact('uhci-example: OTA process started')
# We OTA the same binary to another partition and switch to there.
binary_path = os.path.join(dut.app.binary_path, 'uart_dma_ota.bin')
assert os.path.exists(binary_path), f'OTA binary not found at {binary_path}'
buad_rate = dut.app.sdkconfig.get('UART_BAUD_RATE')
send_file_via_uart(FLASH_PORT, buad_rate, binary_path, PACKET_SIZE)
dut.expect('OTA update successful. Rebooting', timeout=10)
dut.expect('ESP-ROM:', timeout=10)

View File

@@ -0,0 +1,2 @@
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_UART_PORT_NUM=0

View File

@@ -0,0 +1,2 @@
CONFIG_IDF_TARGET="esp32c3"
CONFIG_UART_RX_IO=20

View File

@@ -0,0 +1,2 @@
CONFIG_IDF_TARGET="esp32c6"
CONFIG_UART_RX_IO=17

View File

@@ -0,0 +1,2 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_UART_RX_IO=44

View File

@@ -0,0 +1,3 @@
# partition table layout, with a 4MB flash size
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_TWO_OTA_LARGE=y