摘要(From AI):
这篇博客详细讲解了 ESP32 UART 通信的基础知识、配置流程和实践代码,涵盖了 UART 的工作原理、API 使用方法以及实际应用场景,结合完整的代码示例展示了如何与外部设备(如 4G 模块)进行串口通信。内容逻辑清晰,注释详尽,并附有运行调试截图验证效果,实用性强
前言:本文档是本人在依照乐鑫科技编写的ESP32 API文档进行学习时所做的学习笔记,可能存在疏漏和错误,如有发现,敬请指正。
文章目录
- UART
- Set Communication Parameters
- Single Step
- Multiple Steps
- Set Communication Pins
- uart_set_pin()
- Install Drivers
- uart_driver_install()
- Run UART Communication
- Transmit Data
- uart_write_bytes()
- uart_write_bytes_with_break()
- uart_tx_chars()
- uart_wait_tx_done()
- Receive Data
- uart_read_bytes()
- uart_get_buffered_data_len()
- uart_flush()
- Delete a Driver
- uart_driver_delete()
- Check for Errors
- ESP_ERROR_CHECK()
- Example Code:UART Communication Task
参考资料
ESP 官方文档(写的真的很好)
UART
UART(Universal Asynchronous Receiver-Transmitter,通用异步收发器)是一种常见的串行通信接口,用于在设备之间传输数据。它的特点是简单、可靠,广泛应用于嵌入式系统、微控制器、传感器和计算机外围设备中
UART的工作原理
- 异步通信:UART不需要额外的时钟信号,通信双方使用预先约定的波特率(baud rate)来同步数据。
- 数据帧结构:数据以帧的形式传输,每帧包括:
- 起始位(Start Bit):1位,表示数据帧的开始,通常为低电平
- 数据位(Data Bits):5到8位,表示实际传输的数据
- 校验位(Parity Bit,非必须):1位,用于检测数据错误
- 停止位(Stop Bit):1或2位,表示数据帧的结束
- 双向通信:UART包括发送端(TX)和接收端(RX),数据从一个设备的TX引脚传输到另一个设备的RX引脚
优点
- 硬件简单,不需要额外的时钟线
- 支持全双工通信(TX和RX同时工作)
- 波特率灵活,适配多种设备需求
缺点
- 通信速度较慢,不适合大数据量的高速传输
- 距离有限,通常适用于短距离通信
- 对波特率一致性要求较高
ESP32 芯片有 3 个 UART 控制器(端口),每个控制器都有一组相同的寄存器以简化编程并提高灵活性
每个 UART 控制器可以独立配置波特率、数据位长度、位顺序、停止位位数、奇偶校验位等参数。所有具备完整功能的 UART 控制器都能与不同制造商的 UART 设备兼容,并且支持红外数据协会 (IrDA) 定义的标准协议
使用 UART 通信流程
Set Communication Parameters 设置通信参数
设置波特率、数据位、停止位等Set Communication Pins 设置通信管脚
分配连接设备的管脚Install Drivers 安装驱动程序
为 UART 驱动程序分配 ESP32 资源Run UART Communication 运行 UART
通信发送/接收数据Use Interrupts 使用中断
触发特定通信事件的中断Delete a Driver 删除驱动程序
如无需 UART 通信,则释放已分配的资源
Set Communication Parameters
UART 通信参数可以在一个步骤中完成全部配置,也可以在多个步骤中单独配置
Single Step
使用 uart_param_config()
可以在一个步骤中完成通信参数的全部配置
esp_err_t uart_param_config(uart_port_t uart_num,const uart_config_t *uart_config)
参数
uart_num
UART 端口(控制器)号,最大端口号为(UART_NUM_max-1)
uart_config
UART 参数设置,本质为一个 uart_config_t 结构体,应包含所有必要参数
const uart_port_t uart_num = UART_NUM_2;
uart_config_t uart_config = {.baud_rate = 115200,// 设置通信波特率.data_bits = UART_DATA_8_BITS,// 设置每帧数据的传输位数.parity = UART_PARITY_DISABLE,// 设置是否使用校验位,用于检测传输错误.stop_bits = UART_STOP_BITS_1,// 设置停止位的数量,用于标识一帧的结束.flow_ctrl = UART_HW_FLOWCTRL_CTS_RTS,// 设置流控模式.rx_flow_ctrl_thresh = 122,// 设置接收数据流控的触发阈值
};
以上这些数据的定义可以在
uart_types.h
找到,如:
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,
Multiple Steps
可以使用相应的设置函数单独设置每一个参数
配置参数 | 函数 |
---|---|
波特率 | uart_set_baudrate() |
传输位 | 调用 uart_set_word_length() |
奇偶控制 | 调用 uart_set_parity() 设置 uart_parity_t |
停止位 | 调用 uart_set_stop_bits() 设置 uart_stop_bits_t |
硬件流控模式 | 调用 uart_set_hw_flow_ctrl() 设置 uart_hw_flowcontrol_t |
通信模式 | 调用 uart_set_mode() 设置 uart_mode_t |
每个函数都可使用 _get_
对应项来查看当前设置值;例如 uart_set_baudrate()
和 uart_get_baudrate()
uart_set_baudrate()
设置串口通信波特率
esp_err_t uart_set_baudrate(uart_port_t uart_num,uint32_t baudrate)
参数
uart_num
UART 端口号
baudrate
UART 波特率值
返回值
ESP_FAIL
参数错误
ESP_OK
成功
uart_get_baudrate()
获取 UART 波特率配置
esp_err_t uart_get_baudrate(uart_port_t uart_num,uint32_t *baudrate)
参数
uart_num
UART 端口号
baudrate
指向用于存储 UART 波特率值的空间的指针
返回值
ESP_FAIL
参数错误
ESP_OK
成功将结果写入 *baudrate
Set Communication Pins
通信参数设置完成后,可以配置与其他 UART 设备连接的 GPIO 管脚;通过函数 uart_set_pin()
,指定配置 Tx、Rx、RTS 和 CTS 信号的 GPIO 管脚编号
uart_set_pin()
将 UART 外设的信号分配给 GPIO 引脚
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_num
UART 端口号
tx_io_num
Tx 引脚 GPIO 编号
rx_io_num
Rx 引脚 GPIO 编号
rts_io_num
RTS 引脚 GPIO 编号
cts_io_num
CTS 引脚 GPIO 编号
如要为特定信号保留(不使用)当前分配的管脚编号,可传递宏 UART_PIN_NO_CHANGE
RTS(Request to Send) 和 CTS(Clear to Send) 是串行通信中的硬件流控信号,用于管理数据流量
RTS信号由发送方控制,用于通知接收方“我准备好发送数据了”。当发送方的数据准备就绪时,会将RTS信号拉高(或拉低,具体取决于硬件逻辑),以请求对方允许发送
CTS信号由接收方控制,用于通知发送方“我已准备好接收数据”。只有在接收方拉高(或拉低)CTS信号后,发送方才会开始传输数据在以下场景中,RTS、CTS 可以不接(可能存在问题)
1.设备之间的通信不使用硬件流控
2.传输的数据量较小或通信速率较低,不接RTS/CTS通常不会引起问题,因为接收方有足够时间处理数据
3.接收设备有足够大的缓冲区
4.通信是单向的
如果为 UART 信号配置的 GPIO 编号与该 GPIO 的 IOMUX 信号之一匹配,那么信号将通过 IOMUX 直接连接。否则,GPIO 和信号将通过 GPIO 矩阵(GPIO Matrix)连接
例如,在 ESP32 上,如果调用 uart_set_pin(0, 1, 3, -1, -1),由于 GPIO1 是 UART0 的默认 TX 引脚,而 GPIO3 是 UART0 的默认 RX 引脚,这两个引脚将分别通过 IOMUX 直接连接到 U0TXD 和 U0RXD,完全绕过 GPIO 矩阵
这种检查是逐引脚进行的。因此,可以实现如下情况:RX 引脚通过 GPIO 矩阵绑定到一个 GPIO,而 TX 引脚则通过 IOMUX 绑定到其默认的 GPIOIOMUX 是嵌入式芯片中管理 GPIO 功能的重要工具,负责在多个功能之间选择最合适的信号连接方式。它通过直接映射机制,在固定的引脚上提供高效的信号传输,但在灵活性上略逊于 GPIO Matrix
GPIO Matrix 是嵌入式系统中的一种硬件抽象机制,用于将芯片内部的外设信号映射到任意的 GPIO 引脚上。它通过一个矩阵式的硬件逻辑层,允许开发者灵活地配置信号和引脚之间的关系,而不受固定引脚的限制
可以将TX和RX配置为共享同一个IO引脚(单线模式),但需要注意输出冲突的问题,这可能会损坏引脚。为保护引脚,应提前对其应用开漏配置和上拉电阻,或者由上层协议确保不会出现两端同时输出的情况
返回值
ESP_FAIL
参数错误
ESP_OK
成功
Install Drivers
通信管脚设置完成后,调用 uart_driver_install()
安装驱动程序并指定以下参数
- UART 控制器编号
- Tx 环形缓冲区的大小 - 指定用于存储 UART 发送数据的环形缓冲区的大小(以字节为单位)
- Rx 环形缓冲区的大小 - 指定用于存储 UART 接收数据的环形缓冲区的大小(以字节为单位)
- 指向事件队列句柄的指针 - 用于指向事件队列(Event Queue)的句柄指针
- 事件队列大小 - 指定事件队列中可以存储的最大事件数
- 分配中断的标志 - 用于指定是否为 UART 分配硬件中断,以及中断的相关设置
环形缓冲区是一种循环队列,支持高效的数据流管理,因为其结构允许在数据未完全发送完的情况下继续写入新数据
环形缓冲区过小:可能导致数据等待区满,无法及时写入新数据。
环形缓冲区过大:占用内存资源Tx 环形缓冲区:数据被写入缓冲区后,UART 硬件会从中取出数据并逐字节发送
Rx 环形缓冲区:接收的数据会先存储到这个缓冲区中,供程序按需读取
uart_driver_install()
安装 UART 驱动程序
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_num
UART 端口号
rx_buffer_size
Rx 环形缓冲区大小
tx_buffer_size
Tx 环形缓冲区大小
queue_size
UART 事件队列大小
uart_queue
事件队列句柄,成功后,这里会编写一个新的队列句柄,以提供对 UART 事件的访问;若设置为 NULL
,驱动程序将不使用事件队列,此时 queue_size 的值会被忽略
intr_alloc_flags
用于分配中断的标志,用于配置 UART 驱动的中断行为
UART 驱动的中断行为是指 UART 硬件模块通过中断机制向处理器发出信号,以通知某些事件的发生(例如数据接收完成、发送缓冲区空等)。中断行为是 UART 驱动的重要特性之一,用于提高通信效率和实时性,避免 CPU 频繁轮询设备状态
返回值
ESP_FAIL
参数错误
ESP_OK
成功
Run UART Communication
串行通信由每个 UART 控制器的有限状态机 (FSM) 控制
UART 控制器的有限状态机(FSM, Finite State Machine) 是 UART 硬件模块内部用于管理数据收发过程的逻辑机制。FSM 是一种基于状态转换的逻辑模型,它通过状态和状态间的转换规则,实现对复杂操作的高效管理。UART 控制器的 FSM 在发送和接收数据时会根据当前状态和输入条件进行动态切换,以确保通信的可靠性和正确性
发送数据的过程分为以下步骤:
- 将数据写入 Tx FIFO 缓冲区(First In, First Out(先进先出))
- FSM 序列化数据
- FSM 发送数据
接收数据的过程类似,只是步骤相反:
- FSM 处理且并行化传入的串行流
- FSM 将数据写入 Rx FIFO 缓冲区
- 从 Rx FIFO 缓冲区读取数据
因此,应用程序仅会通过 uart_write_bytes()
和 uart_read_bytes()
从特定缓冲区写入或读取数据,其余工作由 FSM 完成
Transmit Data
uart_write_bytes()
从给定的缓冲区和长度向 UART 端口发送数据
如果 UART 驱动程序的参数“tx_buffer_size”设置为零,在所有数据都发送出去或至少推入 Tx FIFO 之前,此函数不会返回
如果“tx_buffer_size”>0,此函数将在将所有数据复制到 Tx 环形缓冲区后返回,UART ISR 将逐渐将数据从环形缓冲区移动到 Tx FIFO
int uart_write_bytes(uart_port_t uart_num,const void *src,size_t size)
参数
uart_num
UART 端口号
src
数据缓冲区地址
size
发送的数据长度
返回值
-1
参数错误
其它值
推送到 TX FIFO 的字节数
uart_write_bytes_with_break()
该函数从给定的缓冲区和长度向 UART 端口发送数据
如果 UART 驱动程序的参数“tx_buffer_size”设置为零,在所有数据和中断信号发出之前,此函数不会返回。所有数据发出后,发送中断信号
如果“tx_buffer_size”>0,此函数将在将所有数据复制到 Tx 环形缓冲区后返回,UART ISR 将逐渐将数据从环形缓冲区移动到 Tx FIFO。所有数据发出后,发送中断信号
int uart_write_bytes_with_break(uart_port_t uart_num,const void *src,size_t size,int brk_len)
参数
uart_num
UART 端口号
src
数据缓冲区地址
size
发送的数据长度
brk_len
中断信号持续时间(单位:以当前波特率发送一个比特所需的时间)
参数
brk_len
的作用是指定在发送完数据后生成的 BREAK 信号的长度。BREAK 信号是串行通信协议中用来指示通信线路的异常状态或特殊条件的一种信号:
BREAK 信号是一种特殊的状态,指通信线路(TX 引脚)被拉低到逻辑 0,并保持比正常帧的停止位更长的时间,可以用来指示通信异常、同步主从设备或标识数据帧结束
返回值
-1
参数错误
其它值
推送到 TX FIFO 的字节数
uart_tx_chars()
将给定缓冲区中的数据发送到指定的 UART 端口
- 本函数不等待 TX FIFO 缓冲区有足够空间,而是将尽可能多的数据填充到可用的 TX FIFO 中,并在缓冲区满时立即返回
int uart_tx_chars(uart_port_t uart_num,const char *buffer,uint32_t len)
参数
uart_num
UART 端口号
buffer
数据缓冲区地址
len
要发送的数据长度(字节数)
返回值
-1
参数错误
其它值
推送到 TX FIFO 的字节数
uart_wait_tx_done()
等待UART TX FIFO为空
esp_err_t uart_wait_tx_done(uart_port_t uart_num,TickType_t ticks_to_wait)
参数
uart_num
UART 端口号
ticks_to_wait
等待时间,以RTOS节拍为单位计数
返回值
ESP_FAIL
参数错误
ESP_OK
成功
ESP_ERR_TIMEOUT
超时
Receive Data
一旦 UART 接收了数据,并将其保存在 Rx FIFO 缓冲区中,就需要使用函数 uart_read_bytes() 检索数据
uart_read_bytes()
从 UART 缓冲区读取字节
int uart_read_bytes(uart_port_t uart_num,void *buf,uint32_t length,TickType_t ticks_to_wait)
参数
uart_num
UART 端口号
buf
指向缓冲区的指针
length
数据长度
ticks_to_wait
等待时间,以 RTOS 节拍为单位计数
返回值
-1
错误
其它值
从缓冲区读取到的字节数
uart_get_buffered_data_len()
获取 RX 环形缓冲区缓存的数据长度
esp_err_t uart_get_buffered_data_len(uart_port_t uart_num,size_t *size)
参数
uart_num
UART 端口号
size
指向用于存储 size 值的空间的指针
返回值
ESP_FAIL
参数错误
ESP_OK
成功将结果写入 size
uart_flush()
UART 环形缓冲区刷新,将丢弃 UART RX 缓冲区中的所有数据
esp_err_t uart_flush(uart_port_t uart_num)
参数
uart_num
UART 端口号
返回值
ESP_FAIL
参数错误
ESP_OK
成功
Use Interrupts
这部分涉及内容较多且复杂,待后续整理
Delete a Driver
uart_driver_delete()
卸载 UART 驱动程序
esp_err_t uart_driver_delete(uart_port_t uart_num)
参数
uart_num
UART 端口号
返回值
ESP_FAIL
参数错误
ESP_OK
成功
Check for Errors
ESP_ERROR_CHECK()
ESP_ERROR_CHECK 是 ESP-IDF(Espressif IoT Development Framework)提供的一个宏,用于检查函数的返回值是否表示成功。如果返回值不是 ESP_OK(表示无错误),ESP_ERROR_CHECK 会打印出错误信息并终止程序的执行
示例:
ESP_ERROR_CHECK(uart_driver_install(ECHO_UART_PORT_NUM, BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags));ESP_ERROR_CHECK(uart_param_config(ECHO_UART_PORT_NUM, &uart_config));ESP_ERROR_CHECK(uart_set_pin(ECHO_UART_PORT_NUM, ECHO_TEST_TXD, ECHO_TEST_RXD, ECHO_TEST_RTS, ECHO_TEST_CTS));
工作原理
- 宏接收一个表达式
x
,通常是一个函数调用 - 执行该表达式,并将其返回值存储在临时变量
__err_rc
中 - 检查返回值是否等于
ESP_OK
- 如果等于
ESP_OK
- 无任何动作,继续执行程序。
- 如果不等于
ESP_OK
- 打印错误日志,包括返回值(错误代码)
- 调用
abort()
函数终止程序(适用于调试阶段)
- 如果等于
Example Code:UART Communication Task
这里是一个使用UART1与外部设备进行通信的例子,GPIO16和17连接到了一个4G通信模块,模块上已经配置好了和华为云IoTDA进行通信的配置。
通过串口透传,4G模块能够将所有通过串口发送给它的数据直接发送到IotDA,并从IoTDA接受数据,再发送至单片机
有关 FreeRTOS 的部分详见 ESP32专栏
/* * Author: Lamonce * Date: 2024-11-28 * Description: Test uart1 */ #include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h" // 串口驱动
#include "driver/gpio.h" // GPIO驱动
#include "esp_log.h" // 日志
#include "sdkconfig.h" // 配置
#include <string.h> // 字符串 #define UART_1_TX_PIN 17
#define UART_1_RX_PIN 16
#define UART_1_BAUDRATE 115200 // 波特率115200 #define BUF_SIZE 1024 * 2 void uartInit1(void)
{ uart_config_t uart_config = { .baud_rate = UART_1_BAUDRATE, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .rx_flow_ctrl_thresh = 0, // 由于关闭了硬件流控,所以阈值设置为0 }; // 配置串口,使用ESP_ERROR_CHECK来检查错误 ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &uart_config)); // 配置串口参数 ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, UART_1_TX_PIN, UART_1_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); // 设置串口引脚,不使用硬件流控,因此cts和rts引脚都设置为UART_PIN_NO_CHANGE ESP_ERROR_CHECK(uart_driver_install(UART_NUM_1, BUF_SIZE, BUF_SIZE, 0, NULL, 0)); // 安装串口驱动,缓冲区大小为BUF_SIZE,事件回调函数为NULL,事件标志为0,表示不使用事件
} void uartTest1(void *pvParameters)
{ uartInit1(); char *receiveData = (char *)pvParameters; char *sendData = "Hello, Lamonce!"; while (1) { uart_write_bytes(UART_NUM_1, sendData, strlen(sendData)); // 发送数据 ESP_LOGI("UART_1", "Sent %d bytes: %s", strlen(sendData), sendData); // 日志打印发送的数据 vTaskDelay(pdMS_TO_TICKS(5000)); // 延时5秒 size_t len; if (uart_get_buffered_data_len(UART_NUM_1, &len) == ESP_OK && len > 0) { len = (len < BUF_SIZE - 1) ? len : BUF_SIZE - 1; // 判断数据长度是否大于缓冲区大小减1,如果是,则根据缓冲区大小减1读取数据 uart_read_bytes(UART_NUM_1, receiveData, len, pdMS_TO_TICKS(5000)); // 读取数据,最大等待时间为5000ms receiveData[len] = '\0';// 添加字符串结束符 ESP_LOGI("UART_1", "Received %d bytes: %s", len, receiveData); // 日志打印接收到的数据 } else { ESP_LOGI("UART_1", "No data received"); // 日志打印没有数据接收 } }
} TaskHandle_t uartTest1TaskHandle; void app_main(void)
{ char *receiveData = (char *)malloc(BUF_SIZE); // 存储数据 xTaskCreate(uartTest1, "uartTest1", 1024 * 5, (void *)receiveData, 5, &uartTest1TaskHandle);
}
由于在测试代码中没有设置上传topic,因此IoTDA在读取上传的数据时会有格式问题,但是我们仍旧能从下图中看出我们想要发送的数据是什么
这里我以“Audio”命令下发了一条数据,从日志中可以看到,数据成功被ESP32收到