3小时精通嵌入式串口通信!从零玩转ESP32+Modbus+OTA(1)

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

注意事项

  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)
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:

https://gitee.com/zhangdong_road/esp32

相关推荐
钰珠AIOT2 小时前
连接电池的座子2端的电阻只有0.24欧,这个是断路吗,为什么?
单片机·嵌入式硬件·机器人
枫叶丹43 小时前
【Qt开发】Qt界面优化(五)-> Qt样式表(QSS) 子控件选择器
c语言·开发语言·数据库·c++·qt
Hello_Embed3 小时前
Modbus 传感器开发:从寄存器规划到点表设计
笔记·stm32·单片机·学习·modbus
天天爱吃肉82183 小时前
【新能源商用车驱动电机整车运行状态电气性能全维度分析(附6图实战解读)】
嵌入式硬件·汽车
Once_day3 小时前
GCC编译(4)构造和析构函数
c语言·c++·编译和链接
StandbyTime3 小时前
C语言学习-菜鸟教程C经典100例-练习76
c语言
StandbyTime3 小时前
C语言学习-菜鸟教程C经典100例-练习77
c语言
小龙报3 小时前
【51单片机】不止是调光!51 单片机 PWM 实战:呼吸灯 + 直流电机正反转 + 转速控制
数据结构·c++·stm32·单片机·嵌入式硬件·物联网·51单片机
送外卖的工程师4 小时前
STM32 驱动五线四相步进电机(28BYJ-48+ULN2003)教程
stm32·单片机·嵌入式硬件·mcu·物联网·51单片机·proteus