串口通讯非常简单,也非常重要,它是广泛用于计算机、嵌入式系统和工业设备之间的数据传输方式,掌握它基本就可以算是嵌入式设备入门了,本章节也比较长,且相关知识将分成3个部分,都是干货:
1、基本的串口通信
2、Modbus
3、使用Modbus进行OTA
下面先介绍基础串口通信:
实物图

原理图


可以看出esp32-s3的串口0(TXD0/RXD0)通过跳线(P4跳线座子的U0 TXD连RXD,U0 RXD连TXD)连接到U12的CH340C这个芯片,再由这个芯片连接到USB座子(CH340 D+连CH340 D+,CH340 D-连CH340 D-);CH340C这个芯片是CH340C 是南京沁恒微电子(WCH)推出的 USB 转串口芯片,内置时钟无需外部晶振,支持 5V/3.3V 电源电压,广泛用于单片机串口下载、设备升级等场景。
ESP32-IDF示例代码解析
1.使用peripherals\uart\uart_echo
![[Pasted image 20260118162311.png]]
2.分析示例代码:
c
// 从配置文件中读取UART引脚配置
#define ECHO_TEST_TXD (CONFIG_EXAMPLE_UART_TXD) // UART发送引脚
#define ECHO_TEST_RXD (CONFIG_EXAMPLE_UART_RXD) // UART接收引脚
#define ECHO_TEST_RTS (UART_PIN_NO_CHANGE) // RTS引脚(流控制),保持不变
#define ECHO_TEST_CTS (UART_PIN_NO_CHANGE) // CTS引脚(流控制),保持不变
// UART配置参数
#define ECHO_UART_PORT_NUM (CONFIG_EXAMPLE_UART_PORT_NUM) // UART端口号
#define ECHO_UART_BAUD_RATE (CONFIG_EXAMPLE_UART_BAUD_RATE) // UART波特率
#define ECHO_TASK_STACK_SIZE (CONFIG_EXAMPLE_TASK_STACK_SIZE) // 任务堆栈大小
// 日志标签,用于标识日志输出来源
static const char *TAG = "UART TEST";
// 数据缓冲区大小
#define BUF_SIZE (1024)
/**
* UART回显任务函数
* @param arg 任务参数(本例中未使用)
*
* 该任务的主要功能:
* 1. 初始化UART驱动
* 2. 配置UART参数
* 3. 循环读取接收到的数据并回传
*/
static void echo_task(void *arg)
{
/* 配置UART驱动参数、通信引脚并安装驱动 */
uart_config_t uart_config = {
.baud_rate = ECHO_UART_BAUD_RATE, // 波特率设置
.data_bits = UART_DATA_8_BITS, // 数据位: 8位
.parity = UART_PARITY_DISABLE, // 校验位: 无校验
.stop_bits = UART_STOP_BITS_1, // 停止位: 1位
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 硬件流控制: 禁用
.source_clk = UART_SCLK_DEFAULT, // 时钟源: 使用默认时钟
};
int intr_alloc_flags = 0; // 中断分配标志
#if CONFIG_UART_ISR_IN_IRAM
// 如果配置了UART中断在IRAM中,设置相应的中断标志
intr_alloc_flags = ESP_INTR_FLAG_IRAM;
#endif
// 安装UART驱动
// 参数: 端口号, Rx缓冲区大小, Tx缓冲区大小, 事件队列大小, 事件队列句柄, 中断标志
ESP_ERROR_CHECK(uart_driver_install(ECHO_UART_PORT_NUM, BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags));
// 配置UART参数
ESP_ERROR_CHECK(uart_param_config(ECHO_UART_PORT_NUM, &uart_config));
// 设置UART引脚
// 参数: 端口号, TX引脚, RX引脚, RTS引脚, CTS引脚
ESP_ERROR_CHECK(uart_set_pin(ECHO_UART_PORT_NUM, ECHO_TEST_TXD, ECHO_TEST_RXD, ECHO_TEST_RTS, ECHO_TEST_CTS));
// 配置临时缓冲区用于接收数据
uint8_t *data = (uint8_t *) malloc(BUF_SIZE);
// 主循环: 持续读取并回显数据
while (1) {
// 从UART读取数据
// 参数: 端口号, 数据缓冲区, 读取最大字节数, 超时时间(20ms)
int len = uart_read_bytes(ECHO_UART_PORT_NUM, data, (BUF_SIZE - 1), 20 / portTICK_PERIOD_MS);
// 将数据写回UART(回显)
uart_write_bytes(ECHO_UART_PORT_NUM, (const char *) data, len);
// 如果接收到数据,打印日志
if (len) {
data[len] = '\0'; // 添加字符串结束符
ESP_LOGI(TAG, "Recv str: %s", (char *) data);
}
}
}
/**
* 应用主函数
* 这是程序的入口点
*/
void app_main(void)
{
// 创建并启动UART回显任务
// 参数: 任务函数, 任务名称, 堆栈大小, 任务参数, 优先级, 任务句柄
xTaskCreate(echo_task, "uart_echo_task", ECHO_TASK_STACK_SIZE, NULL, 10, NULL);
}
3.配置

4.调试

关键知识点
硬件
不同的型号,串口数量可能不一样,需要参考对应的技术参考手册,ESP32-S3有3个串口,UART0~2。先来介绍串口的硬件知识:
数据通信(传输)
我们知道,2个设备之间要交换数据,有2种方式:1种是两个设备直接连接数据线,1种是两个设备收发无线电信号(比如wifi、蓝牙),而最简单,最容易的就是第一种,那第一种,又分为2种情况:
![[Pasted image 20260118203317.png]]
发送方同时刻发多位数据给接收方,这就是并行通信 ,而发送方同时刻发一位给接收方,就是串行通信 ,可以理解为多行道和单行道;另外,根据数据传输方向,我们又分为单工、半双工和全双工 :

如a)一方只发不收,一方只收不发,称为单工通信 ;
如b) 同一时刻,一方发送数据,另一方接收数据,等到下一时刻,转换角色,发送数据的设备改成接收数据,接收数据的设备改成发送数据;它们不能同时接收和发送数据,这种称为半双工通信 ;
如c)同一时刻,双方都能接收和发送数据,称为全双工通信 或者双工通信 。
而上面只有1根数据线的进行传输通信,发送方一个高电平和一个低电平,接收方怎么知道这两个电平信号对应的数据是10,而不是100、110或者1100等等呢?这个时候就有2个办法:
1、加一根时钟信号线,原理的那根线叫数据线,一般时钟信号线上的信号是固定频率的,约定诸如发送方在从低电平变成高电平时(上升沿)的数据有效,那么接收方也只在这个时候采样数据信号线上的数据,是高电平,就是数据1,是低电平,就是数据0。
2、两边约定一个采样率,以固定的频率进行采样。

这样就引出了第三个概念:同步通信和异步通信 ,有时钟信号线的就是同步通信,没有的就是异步通信。
最后在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。
传信率 :每秒钟传输的信息量,即每秒钟传输的二进制位数,单位为 bit/s(即比特每秒), 因而又称为比特率 。
传码率 :每秒钟传输的码元个数,单位为 Baud(即波特每秒),因而又称为波特率 。
比特率和波特率这两个概念又常常被人们混淆。比特率很好理解,我们来看看波特率,波特率被传输的是码元,码元是信号被调制后的概念,每个码元都可以表示一定 bit 的数据信息量。
举个例子,在 TTL 电平标准的通信中,用 0V 表示逻辑 0,5V 表示逻辑 1,这时候这个码元就可以表示两种状态。如果电平信号 0V、2V、4V 和 6V 分别表示二进制数 00、01、10、11,这时候每一个码元就可以表示四种状态。 由上述可以看出,码元携带一定的比特信息,所以比特率和波特率也是有一定的关系的。
比特率和波特率的关系可以用以下式子表示:
比特率 = 波特率 * log2M
其中 M 表示码元承载的信息量。我们也可以理解 M 为码元的进制数。
举个例子:波特率为 100 Baud,即每秒传输 100 个码元,如果码元采用十六进制编码(即 M=16,代入上述式子),那么这时候的比特率就是400 bit/s。如果码元采用二进制编码(即M=2, 代入上述式子),那么这时候的比特率就是 100 bit/s。
可以看出采用二进制的时候,波特率和比特率数值上相等。但是这里要注意,它们的相等只是数值相等,其意义上不同,看波特率和波特率单位就知道。由于我们的所用的数字系统都 是二进制的,所以有部分人久而久之就直接把波特率和比特率混淆了。
以上,并行、串行;单、双工;同步、异步;比特率、波特率就是数据通信中最基本的概念。
串口
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力

硬件示意图

如上图:
•简单双向串口通信有两根通信线(发送端TX和接收端RX)
•TX与RX要交叉连接
•当只需单向的数据传输时,可以只接一根通信线
•当电平标准不一致时,需要加电平转换芯片
根据电平标准不一样,串口又分为3种:
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
•TTL电平:+3.3V或+5V表示1,0V表示0
•RS232电平:-3-15V表示1,+3+15V表示0
•RS485电平:两线压差+2+6V表示1,-2-6V表示0(差分信号),半双工
通信关键参数
| 参数 | 说明 | 常用值 |
|---|---|---|
| 波特率 (Baud Rate) | 每秒传输的符号数(通常等于比特数),决定通信速度。 | 9600、19200、115200 bps 等。速率越高,传输距离越短。 |
| 数据位 (Data Bits) | 每个数据帧中有效数据的位数。 | 5、7、8 位。8 位最常见,可传输标准 ASCII 或扩展 ASCII 字符。 |
| 停止位 (Stop Bits) | 标识一个数据帧结束的位数,为高电平。 | 1、1.5、2 位。1 位最常用,用于接收端时钟同步。 |
| 奇偶校验位 (Parity Bit) | 一种简单的错误检测机制,可选。 | 无校验 (None)、奇校验 (Odd)、偶校验 (Even)。校验位确保数据位中"1"的总数为奇数或偶数。 |
软件
枚举类型
c
/**
* @brief 数据位枚举
*/
typedef enum {
UART_DATA_5_BITS = 0x0, /*!< 5位数据位*/
UART_DATA_6_BITS = 0x1, /*!< 6位数据位*/
UART_DATA_7_BITS = 0x2, /*!< 7位数据位*/
UART_DATA_8_BITS = 0x3, /*!< 8位数据位*/
UART_DATA_BITS_MAX = 0x4,
} uart_word_length_t;
/**
* @brief 停止位枚举
*/
typedef enum {
UART_STOP_BITS_1 = 0x1, /*!< 1位停止位*/
UART_STOP_BITS_1_5 = 0x2, /*!< 1.5位停止位*/
UART_STOP_BITS_2 = 0x3, /*!< 2位停止位*/
UART_STOP_BITS_MAX = 0x4,
} uart_stop_bits_t;
/**
* @brief 校验位枚举
*/
typedef enum {
UART_PARITY_DISABLE = 0x0, /*!< 无校验*/
UART_PARITY_EVEN = 0x2, /*!< 偶校验*/
UART_PARITY_ODD = 0x3 /*!< 奇校验*/
} uart_parity_t;
函数
c
esp_err_t uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, int queue_size, QueueHandle_t* uart_queue, int intr_alloc_flags);
函数功能
安装UART驱动程序,并将UART设置为默认配置。UART中断处理程序将附加到运行此函数的CPU核心上。
输入参数
| 参数 | 类型 | 说明 |
|---|---|---|
| uart_num | uart_port_t | UART端口号,最大端口号为 (UART_NUM_MAX - 1) |
| rx_buffer_size | int | UART接收环形缓冲区大小,应大于 UART_HW_FIFO_LEN(uart_num) |
| tx_buffer_size | int | UART发送环形缓冲区大小,设置为0时驱动不使用TX缓冲区,TX函数会阻塞直到所有数据发送完成 |
| queue_size | int | UART事件队列大小/深度 |
| intr_alloc_flags | int | 用于分配中断的标志,一个或多个(按位或)ESP_INTR_FLAG_*值 |
输出参数
| 参数 | 类型 | 说明 |
|---|---|---|
| uart_queue | QueueHandle_t* | UART事件队列句柄(输出参数)。成功时,新的队列句柄会写入此处以提供访问UART事件。如果设置为NULL,驱动程序将不使用事件队列 |
返回值
| 返回值 | 说明 |
|---|---|
| ESP_OK | 成功 |
| ESP_FAIL | 参数错误 |
注意事项
- Rx_buffer_size 应大于 UART_HW_FIFO_LEN(uart_num)
- Tx_buffer_size 应为0或大于 UART_HW_FIFO_LEN(uart_num)
- 不要设置 ESP_INTR_FLAG_IRAM 标志(驱动的ISR处理程序不在IRAM中)
c
esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config);
函数功能
设置UART配置参数。该函数用于配置UART的各种通信参数,如波特率、数据位、校验位、停止位等。
输入参数
| 参数 | 类型 | 说明 |
|---|---|---|
| uart_num | uart_port_t | UART端口号,最大端口号为 (UART_NUM_MAX - 1) |
| uart_config | const uart_config_t* | UART参数设置 |
| 返回值 |
| 返回值 | 说明 |
|---|---|
| ESP_OK | 成功 |
| ESP_FAIL | 参数错误,例如波特率不可实现 |
uart_config_t 结构体说明
| 字段 | 类型 | 说明 |
|---|---|---|
| baud_rate | int | UART波特率,注意实际设置的波特率可能因舍入误差而与用户配置的值略有偏差 |
| data_bits | uart_word_length_t | UART字节大小(数据位) |
| parity | uart_parity_t | UART校验模式 |
| stop_bits | uart_stop_bits_t | UART停止位 |
| flow_ctrl | uart_hw_flowcontrol_t | UART硬件流控制模式(CTS/RTS) |
| rx_flow_ctrl_thresh | uint8_t | UART硬件RTS阈值 |
| source_clk | uart_sclk_t | UART源时钟选择 |
| lp_source_clk | lp_uart_sclk_t | 低功耗模式下源时钟选择 |
| flags.allow_pd | uint32_t (bit) | 如果设置,允许系统进入睡眠模式时关闭电源域,可节省功耗但会增加RAM消耗 |
| flags.backup_before_sleep | uint32_t (bit) | 已弃用,含义与allow_pd相同 |
c
esp_err_t uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num);
函数功能
将UART外设的信号分配给GPIO引脚。该函数用于配置UART的TX、RX、RTS、CTS等引脚对应的GPIO编号。
输入参数
| 参数 | 类型 | 说明 |
|---|---|---|
| uart_num | uart_port_t | UART端口号,最大端口号为 (UART_NUM_MAX - 1) |
| tx_io_num | int | UART TX引脚的GPIO编号 |
| rx_io_num | int | UART RX引脚的GPIO编号 |
| rts_io_num | int | UART RTS引脚的GPIO编号 |
| cts_io_num | int | UART CTS引脚的GPIO编号 |
| 返回值 |
| 返回值 | 说明 |
|---|---|
| ESP_OK | 成功 |
| ESP_FAIL | 参数错误 |
注意事项
-
IOMUX与GPIO Matrix自动选择:
- 如果为UART信号配置的GPIO编号与该GPIO的IOMUX信号匹配,信号将通过IOMUX直接连接
- 否则,GPIO和信号将通过GPIO矩阵连接
- 例如,在ESP32上调用
uart_set_pin(0, 1, 3, -1, -1)时,由于GPIO1是UART0的默认TX引脚,GPIO3是默认RX引脚,两者会通过IOMUX直接连接到U0TXD和U0RXD,完全绕过GPIO矩阵 - 检查是按引脚逐个进行的,因此可以让RX引脚通过GPIO矩阵绑定到GPIO,而TX通过IOMUX绑定到GPIO
-
单线模式:
- 可以配置TX和RX共享同一个IO(单线模式)
- 需注意输出冲突,可能损坏焊盘
- 请预先对焊盘应用开漏和上拉作为保护,或上层协议必须保证两端不会同时输出
-
引脚设置:
- 使用
-1表示不使用该引脚(如不需要硬件流控制时,RTS/CTS设为-1)
- 使用
c
int uart_write_bytes(uart_port_t uart_num, const void* src, size_t size);
函数功能
向UART端口发送指定缓冲区和长度的数据。
输入参数
| 参数 | 类型 | 说明 |
|---|---|---|
| uart_num | uart_port_t | UART端口号,最大端口号为 (UART_NUM_MAX - 1) |
| src | const void* | 数据缓冲区地址 |
| size | size_t | 要发送的数据长度 |
| 返回值 |
| 返回值 | 说明 |
|---|---|
| -1 | 参数错误 |
| >=0 | 推送到TX FIFO的字节数 |
工作机制
情况1:tx_buffer_size = 0
- 函数不会返回,直到所有数据已发送出去,或至少推入TX FIFO
- 阻塞模式:等待数据完全发送
情况2:tx_buffer_size > 0
- 函数在将所有数据复制到TX环形缓冲区后立即返回
- UART ISR会逐渐将数据从环形缓冲区移动到TX FIFO
- 非阻塞模式:数据在后台发送
c
int uart_read_bytes(uart_port_t uart_num, void* buf, uint32_t length, TickType_t ticks_to_wait);
函数功能
从UART缓冲区读取指定长度的字节数据。
输入参数
| 参数 | 类型 | 说明 |
|---|---|---|
| uart_num | uart_port_t | UART端口号,最大端口号为 (UART_NUM_MAX - 1) |
| buf | void* | 指向缓冲区的指针,用于存储读取的数据 |
| length | uint32_t | 要读取的数据长度 |
| ticks_to_wait | TickType_t | 超时时间,单位为RTOS ticks |
输出参数
| 参数 | 类型 | 说明 |
|---|---|---|
| buf | void* | 读取的数据将存储在此缓冲区中 |
返回值
| 返回值 | 说明 |
|---|---|
| -1 | 错误 |
| >=0 | 从UART缓冲区实际读取的字节数 |
工作机制
- 从UART接收缓冲区读取数据
- 支持超时机制,防止无限等待
- 如果缓冲区中数据不足,等待指定时间
- 超时后返回实际读取的字节数(可能小于请求的长度)
补充知识--IOMUX和GPIO Matrix
这里涉及到esp32 管脚功能设置,它比STM32更灵活,STM32使用GPIO的复用功能连接外设,若有重复,需要重映射,同一时间一个管脚通常只能承载一个复用功能,而ESP32采用IOMUX + GPIO Matrix的机制,同一时间一个管脚可以通过GPIO Matrix承载多个外设信号,若使用IOMUX,则提供专用高速通道,低延迟,高速度,若不支持IOMUX,则使用GPIO Matrix,延迟多些,但可测量,约数十ns。
每款芯片的IOMUX表,需要查看其技术规格书 ,esp32-s3的IOMUX表如下:
![[Pasted image 20260219180124.png]]
![[Pasted image 20260219180144.png]]
注意以下信息:
![[Pasted image 20260219180402.png]]
就是说红色的这几个管脚,如果使用的是模组 ,就不要使用了,而什么是模组,见我第一篇文章[[01-esp32介绍 物联网时代的瑞士军刀,还是开发者的电子鸦片]]
IO MUX、RTC IO MUX 和 GPIO 交换矩阵结构框图如下,具体说明可以参考对应的芯片的技术参考手册 ,若有兴趣,我们后面单独开一章介绍:
![[Pasted image 20260219180632.png]]
使用串口0打印hello world
c
#include <stdio.h>
#include "driver/uart.h" // ESP32 UART驱动头文件
#include "driver/gpio.h" // ESP32 GPIO驱动头文件
#include "freertos/task.h" // FreeRTOS任务相关函数
void app_main(void)
{
/* 配置UART驱动参数、通信引脚并安装驱动 */
uart_config_t uart_config = {
.baud_rate = 115200, // 波特率设置
.data_bits = UART_DATA_8_BITS, // 数据位: 8位
.parity = UART_PARITY_DISABLE, // 校验位: 无校验
.stop_bits = UART_STOP_BITS_1, // 停止位: 1位
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 硬件流控制: 禁用
.source_clk = UART_SCLK_DEFAULT, // 时钟源: 使用默认时钟
};
int intr_alloc_flags = 0; // 中断分配标志
// 安装UART驱动
// 参数: 端口号, Rx缓冲区大小, Tx缓冲区大小, 事件队列大小, 事件队列句柄, 中断标志
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_0, 2048, 0, 0, NULL, intr_alloc_flags));
// 配置UART参数
ESP_ERROR_CHECK(uart_param_config(UART_NUM_0, &uart_config));
// 设置UART引脚
// 参数: 端口号, TX引脚, RX引脚, RTS引脚, CTS引脚
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_0, GPIO_NUM_43, GPIO_NUM_44, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
// 配置临时缓冲区用于接收数据
char data[20] = "hello world";
// 主循环: 持续读取并回显数据
while (1) {
// 将数据写到UART
uart_write_bytes(UART_NUM_0, (const char *) data, sizeof(data));
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}

实操练习:
实现功能 :
通过串口输入字符控制led灯:
- 输入"led on",点亮led灯
- 输入"led off", 关闭led灯
- 其他,返回命令错误
答案在gitee: