3、UART串口通信:硬件串口配置、自定义波特率、串口缓冲区管理、与PC通信实战
UART串口,说白了就是嵌入式设备与外界对话的“嘴巴”。你想想看,一个ESP32芯片,没有屏幕没有键盘,怎么知道它跑没跑起来?怎么调试代码?怎么跟PC交换数据?全靠串口。
我个人习惯,拿到一块新的ESP32开发板,第一件事就是点亮LED,第二件事就是打通串口。串口通了,后面所有事情才有底。这一章,咱们就把ESP32的UART串口彻底搞明白。
3.1 ESP32的硬件串口资源
ESP32一共有3个硬件UART控制器:UART0、UART1、UART2。每个都是独立的硬件外设,可以同时工作,互不干扰。
| 串口编号 | 默认引脚(TX / RX) | 常用场景 | 备注 |
|---|---|---|---|
| UART0 | GPIO1 / GPIO3 | 固件下载、调试日志输出 | 默认连接USB转串口芯片 |
| UART1 | GPIO9 / GPIO10 | 外接传感器、GPS模块 | 注意避开Flash占用引脚 |
| UART2 | GPIO16 / GPIO17 | 外接蓝牙模块、RS485 | 引脚自由度高,推荐使用 |
3.2 硬件串口配置实战
ESP-IDF里配置串口,核心就三步:参数设置 → 安装驱动 → 引脚绑定。我直接上代码,边看边讲。
#include "driver/uart.h"
#include "driver/gpio.h"
// 定义串口参数
#define UART_PORT_NUM UART_NUM_2
#define UART_TX_GPIO 17
#define UART_RX_GPIO 16
#define UART_BAUD_RATE 115200
#define UART_BUF_SIZE 1024
void uart_init(void)
{
// 第一步:配置串口参数
uart_config_t uart_config = {
.baud_rate = 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_APB,
};
uart_param_config(UART_PORT_NUM, &uart_config);
// 第二步:设置引脚
uart_set_pin(UART_PORT_NUM, UART_TX_GPIO, UART_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 第三步:安装驱动,分配缓冲区
uart_driver_install(UART_PORT_NUM, UART_BUF_SIZE * 2,
UART_BUF_SIZE * 2, 0, NULL, 0);
}
这里有个细节:uart_driver_install的第二个参数是接收缓冲区大小,第三个是发送缓冲区大小。我建议接收缓冲区至少设成你预期最大数据包的2倍,防止丢包。
3.3 自定义波特率:不按套路出牌
标准波特率大家都熟:9600、115200、921600。但有时候,项目要求就是不走寻常路。比如我做过一个工业项目,对方设备非要跑一个19200的波特率——等等,19200不是标准吗?嗯,但那次对方要的是14400,这就有点偏了。
ESP32的UART时钟源是APB(默认80MHz),通过分频器可以生成任意波特率。公式很简单:
实际波特率 = APB时钟频率 / (分频系数 × 8)
ESP-IDF的uart_param_config会自动计算最接近的分频值。你只管填目标波特率,驱动会帮你搞定。但要注意,波特率越高,对时钟精度要求也越高。我曾经在14400波特率下通信偶尔乱码,排查后发现是APB时钟被其他外设动态调整了。解决办法:固定source_clk为UART_SCLK_APB,别用默认的UART_SCLK_DEFAULT。
baud_rate字段填数字就行。ESP32的UART硬件支持连续可调,不是只能选那几个固定值。
3.4 串口缓冲区管理:别让数据丢了
串口通信最怕什么?数据来了,CPU没空处理,结果被新数据覆盖。这就是缓冲区溢出。ESP32的UART驱动内部维护了一个环形缓冲区(Ring Buffer),接收到的数据先存进去,应用程序再从里面读。
我画了一张图,帮你理解数据流动的过程:
实际开发中,我建议你这样管理缓冲区:
- 接收缓冲区:至少1024字节起步。如果数据量大(比如GPS NMEA语句,一条最长82字节,但每秒10条),建议设到2048以上。
- 发送缓冲区:如果只是发短命令,256字节就够了。如果要发文件或固件,设到4096。
- 应用层缓冲区:自己再开一个环形队列或链表,从驱动层读出来后,放到应用层慢慢处理。
3.5 与PC通信实战:收发数据
打通串口之后,怎么跟PC收发数据?我直接给一个完整的收发示例,你拿去就能用。
// 发送数据到PC
void uart_send_data(const char *data, size_t len)
{
uart_write_bytes(UART_PORT_NUM, data, len);
}
// 从PC接收数据(非阻塞)
int uart_receive_data(uint8_t *buf, size_t max_len)
{
size_t available = 0;
uart_get_buffered_data_len(UART_PORT_NUM, &available);
if (available == 0) {
return 0; // 没有数据
}
size_t len = (available > max_len) ? max_len : available;
int ret = uart_read_bytes(UART_PORT_NUM, buf, len, 0);
return ret;
}
// 主循环中调用
void app_main(void)
{
uart_init();
uint8_t rx_buf[256];
while (1) {
int len = uart_receive_data(rx_buf, sizeof(rx_buf));
if (len > 0) {
// 把收到的数据原样发回去(回环测试)
uart_send_data((const char *)rx_buf, len);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
用串口助手(比如Putty、sscom)连上ESP32,波特率设115200,发什么就回什么。这就是最简单的串口回环测试,用来验证硬件和驱动是否正常。
uart_read_bytes的最后一个参数是超时时间(毫秒)。设为0表示非阻塞,没数据立刻返回。设为portMAX_DELAY会一直等。我建议在任务循环里用非阻塞方式,别把CPU卡死。
3.6 常见问题与排查思路
串口出问题,别慌。我总结了一个排查清单,按顺序查,90%的问题都能解决:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全收不到数据 | TX/RX接反、波特率不匹配 | 用示波器或逻辑分析仪看波形 |
| 数据乱码 | 波特率误差大、电平不匹配 | 检查时钟源、加电平转换芯片 |
| 丢数据 | 缓冲区太小、中断优先级低 | 加大缓冲区、提高中断优先级 |
| 偶尔卡死 | 中断服务函数里做了耗时操作 | ISR里只做标志位,处理放任务里 |
嗯,这一章的内容就到这里。串口是嵌入式开发的基本功,看似简单,但坑不少。把缓冲区管理好、波特率配准、引脚选对,后面用MQTT跟云平台通信时,串口就是你最可靠的调试通道。