4. UART串口通信
串口,在嵌入式开发里就像人的嘴巴和耳朵。芯片要跟外界说话,最直接、最常用的方式就是通过UART。我个人做项目这么多年,几乎每个板子第一件事就是先把串口调通。为啥?因为串口通了,你才能看到芯片在“想”什么。
这一章,我们就来搞定ESP32的硬件串口初始化,把printf重定向到串口,再跟PC上的串口助手对上话。说白了,就是让芯片开口说话,让你能听懂。
4.1 硬件串口初始化:让芯片准备好“开口”
ESP32一共有3个硬件UART接口:UART0、UART1、UART2。默认情况下,UART0被用来做下载和日志输出,也就是我们接USB转串口那个口。UART1和UART2是空闲的,你可以接到其他串口设备上,比如GPS模块、蓝牙模块。
初始化一个串口,需要做三件事:配置参数、分配引脚、安装驱动。我习惯把这三步写在一个函数里,清晰明了。
核心步骤:
- 配置参数:波特率、数据位、停止位、校验位、流控。
- 分配引脚:把UART的TX、RX映射到芯片的具体GPIO上。
- 安装驱动:告诉ESP-IDF,这个串口要开始工作了。
来看代码。这是我最常用的一套初始化模板,几乎每个项目都从它开始改。
#include "driver/uart.h"
#include "driver/gpio.h"
// 定义串口参数
#define UART_PORT_NUM UART_NUM_1 // 使用UART1
#define UART_BAUD_RATE 115200 // 波特率
#define UART_TX_GPIO 17 // TX引脚
#define UART_RX_GPIO 16 // RX引脚
#define UART_BUF_SIZE 1024 // 缓冲区大小
void uart_init(void)
{
// 1. 配置串口参数
uart_config_t uart_config = {
.baud_rate = 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_APB, // 时钟源
};
uart_param_config(UART_PORT_NUM, &uart_config);
// 2. 分配引脚
uart_set_pin(UART_PORT_NUM, UART_TX_GPIO, UART_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 3. 安装驱动
uart_driver_install(UART_PORT_NUM, UART_BUF_SIZE,
UART_BUF_SIZE, 0, NULL, 0);
}
嗯,这里要注意一点:uart_param_config 和 uart_set_pin 的顺序不能乱。先配参数,再绑引脚,最后装驱动。我刚开始学的时候,把引脚分配写在了驱动安装后面,结果串口死活不工作,查了半天才发现是顺序问题。
4.2 printf重定向:让调试信息“飞”出来
串口初始化好了,怎么把信息打印出来?最直接的办法是用 uart_write_bytes() 函数。但每次都写这个,太麻烦了。我们更习惯用 printf,因为它支持格式化输出,调试起来特别方便。
ESP-IDF其实已经帮我们做好了。默认情况下,printf 输出到UART0。但如果你像我一样,想把调试信息从UART1打印出来,就需要手动重定向一下。
怎么做?重写 _write 函数。这个函数是C库底层调用 printf 时的最终出口。我们把它劫持过来,改成往UART1写数据。
// 重定向printf到UART1
int _write(int file, char *data, int len)
{
// 把数据通过UART1发送出去
uart_write_bytes(UART_PORT_NUM, (const char *)data, len);
return len;
}
就这么简单。加上这段代码后,你在任何地方调用 printf("Hello, World!\n"),数据都会从UART1的TX引脚飞出去。
我的小技巧: 调试阶段,我习惯在 printf 前面加个时间戳。比如 printf("[%d] WiFi connected\n", esp_timer_get_time() / 1000);。这样看日志时,能清楚知道每个事件发生的时间点,排查问题快很多。
4.3 与PC串口助手通信:双向对话
光会发不行,还得会收。PC上的串口助手(比如SSCOM、Putty、Arduino IDE的串口监视器)发过来的数据,芯片要能接收并响应。这才是真正的“对话”。
接收数据,我用的是 uart_read_bytes()。这个函数会阻塞等待,直到收到数据或超时。我一般把它放在一个循环里,不断轮询。
void uart_echo_task(void *arg)
{
uint8_t *data = (uint8_t *)malloc(UART_BUF_SIZE);
while (1) {
// 读取串口数据
int len = uart_read_bytes(UART_PORT_NUM, data,
UART_BUF_SIZE, pdMS_TO_TICKS(100));
if (len > 0) {
// 把收到的数据原样发回去(回显)
uart_write_bytes(UART_PORT_NUM, (const char *)data, len);
// 同时在调试口打印出来
printf("Received: %.*s\n", len, data);
}
}
free(data);
}
这段代码实现了一个“回显”功能。你在PC串口助手发“AT”,芯片收到后,会原样返回“AT”,同时在调试口打印一行日志。
我曾经在一个项目中,用这个回显功能做远程调试。设备部署在野外,通过4G模块透传数据。我在服务器端发一条指令,设备收到后原样返回,我就能确认链路是否通畅。这个“回显”测试,是我每次调试无线透传的第一步。
4.4 常见问题与避坑指南
串口通信看着简单,但坑不少。我把自己踩过的坑列出来,你遇到了可以少走弯路。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 串口没输出 | TX/RX接反了 | 交换TX和RX线 |
| 输出乱码 | 波特率不匹配 | 检查PC和芯片的波特率是否一致 |
| printf不工作 | _write函数未重定向 | 确认是否添加了_write函数 |
| 接收数据丢失 | 缓冲区太小或中断优先级低 | 增大UART_BUF_SIZE,或调整中断优先级 |
| 芯片重启后串口卡死 | 驱动未正确卸载 | 在重启前调用uart_driver_delete() |
特别注意: 千万不要在UART0上同时接两个设备。UART0默认被USB转串口占用,如果你再外接一个串口设备到同一个引脚,两个设备会打架,导致通信失败。我见过有人把GPS模块直接焊在UART0上,结果下载程序都下不进去。
4.5 本章知识体系
下面这张图,把本章的核心逻辑串起来了。从硬件初始化,到printf重定向,再到与PC双向通信,每一步都环环相扣。
串口通信,是嵌入式开发的“基本功”。你把它练熟了,后面做WiFi配网、无线透传,心里就有底了。因为不管网络怎么传,最终数据都要从串口进、从串口出。串口通了,整个链路就通了一半。