串口通讯非常简单,也非常重要,它是广泛用于计算机、嵌入式系统和工业设备之间的数据传输方式,掌握它基本就可以算是嵌入式设备入门了,本章节也比较长,且相关知识将分成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.分析示例代码:

// 从配置文件中读取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”的总数为奇数或偶数。

软件

枚举类型

/**
 * @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;

函数

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_numuart_port_tUART端口号,最大端口号为 (UART_NUM_MAX - 1)
rx_buffer_sizeintUART接收环形缓冲区大小,应大于 UART_HW_FIFO_LEN(uart_num)
tx_buffer_sizeintUART发送环形缓冲区大小,设置为0时驱动不使用TX缓冲区,TX函数会阻塞直到所有数据发送完成
queue_sizeintUART事件队列大小/深度
intr_alloc_flagsint用于分配中断的标志,一个或多个(按位或)ESP_INTR_FLAG_*值

输出参数

参数类型说明
uart_queueQueueHandle_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中)
esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config);

函数功能
设置UART配置参数。该函数用于配置UART的各种通信参数,如波特率、数据位、校验位、停止位等。

输入参数

参数类型说明
uart_numuart_port_tUART端口号,最大端口号为 (UART_NUM_MAX - 1)
uart_configconst uart_config_t*UART参数设置
返回值
返回值说明
ESP_OK成功
ESP_FAIL参数错误,例如波特率不可实现

uart_config_t 结构体说明

字段类型说明
baud_rateintUART波特率,注意实际设置的波特率可能因舍入误差而与用户配置的值略有偏差
data_bitsuart_word_length_tUART字节大小(数据位)
parityuart_parity_tUART校验模式
stop_bitsuart_stop_bits_tUART停止位
flow_ctrluart_hw_flowcontrol_tUART硬件流控制模式(CTS/RTS)
rx_flow_ctrl_threshuint8_tUART硬件RTS阈值
source_clkuart_sclk_tUART源时钟选择
lp_source_clklp_uart_sclk_t低功耗模式下源时钟选择
flags.allow_pduint32_t (bit)如果设置,允许系统进入睡眠模式时关闭电源域,可节省功耗但会增加RAM消耗
flags.backup_before_sleepuint32_t (bit)已弃用,含义与allow_pd相同
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_numuart_port_tUART端口号,最大端口号为 (UART_NUM_MAX - 1)
tx_io_numintUART TX引脚的GPIO编号
rx_io_numintUART RX引脚的GPIO编号
rts_io_numintUART RTS引脚的GPIO编号
cts_io_numintUART CTS引脚的GPIO编号
返回值
返回值说明
ESP_OK成功
ESP_FAIL参数错误

注意事项

  1. 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
  2. 单线模式:

    • 可以配置TX和RX共享同一个IO(单线模式)
    • 需注意输出冲突,可能损坏焊盘
    • 请预先对焊盘应用开漏和上拉作为保护,或上层协议必须保证两端不会同时输出
  3. 引脚设置:

    • 使用 -1 表示不使用该引脚(如不需要硬件流控制时,RTS/CTS设为-1)
int uart_write_bytes(uart_port_t uart_num, const void* src, size_t size);

函数功能
向UART端口发送指定缓冲区和长度的数据。
输入参数

参数类型说明
uart_numuart_port_tUART端口号,最大端口号为 (UART_NUM_MAX - 1)
srcconst void*数据缓冲区地址
sizesize_t要发送的数据长度
返回值
返回值说明
-1参数错误
>=0推送到TX FIFO的字节数

工作机制

情况1:tx_buffer_size = 0

  • 函数不会返回,直到所有数据已发送出去,或至少推入TX FIFO
  • 阻塞模式:等待数据完全发送

情况2:tx_buffer_size > 0

  • 函数在将所有数据复制到TX环形缓冲区后立即返回
  • UART ISR会逐渐将数据从环形缓冲区移动到TX FIFO
  • 非阻塞模式:数据在后台发送
int uart_read_bytes(uart_port_t uart_num, void* buf, uint32_t length, TickType_t ticks_to_wait);

函数功能
从UART缓冲区读取指定长度的字节数据。

输入参数

参数类型说明
uart_numuart_port_tUART端口号,最大端口号为 (UART_NUM_MAX - 1)
bufvoid*指向缓冲区的指针,用于存储读取的数据
lengthuint32_t要读取的数据长度
ticks_to_waitTickType_t超时时间,单位为RTOS ticks

输出参数

参数类型说明
bufvoid*读取的数据将存储在此缓冲区中

返回值

返回值说明
-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

#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:
https://gitee.com/zhangdong_road/esp32

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐