4、UART串口通信:硬件串口初始化,printf打印调试信息,与PC串口助手通信

串口,在嵌入式开发里就像人的嘴巴和耳朵。芯片要跟外界说话,最直接、最常用的方式就是通过UART。我个人做项目这么多年,几乎每个板子第一件事就是先把串口调通。为啥?因为串口通了,你才能看到芯片在“想”什么。

这一章,我们就来搞定ESP32的硬件串口初始化,把printf重定向到串口,再跟PC上的串口助手对上话。说白了,就是让芯片开口说话,让你能听懂。

4.1 硬件串口初始化:让芯片准备好“开口”

ESP32一共有3个硬件UART接口:UART0、UART1、UART2。默认情况下,UART0被用来做下载和日志输出,也就是我们接USB转串口那个口。UART1和UART2是空闲的,你可以接到其他串口设备上,比如GPS模块、蓝牙模块。

初始化一个串口,需要做三件事:配置参数、分配引脚、安装驱动。我习惯把这三步写在一个函数里,清晰明了。

核心步骤:

  1. 配置参数:波特率、数据位、停止位、校验位、流控。
  2. 分配引脚:把UART的TX、RX映射到芯片的具体GPIO上。
  3. 安装驱动:告诉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双向通信,每一步都环环相扣。

UART串口通信知识体系 UART串口通信知识体系 硬件串口初始化 配置参数 分配引脚 安装驱动 printf重定向 重写_write函数 调用uart_write_bytes PC串口通信 发送数据 接收数据 回显响应 核心流程总结 1. 初始化UART硬件(参数→引脚→驱动) 2. 重定向printf到指定UART端口 3. 通过uart_read_bytes/uart_write_bytes与PC双向通信

串口通信,是嵌入式开发的“基本功”。你把它练熟了,后面做WiFi配网、无线透传,心里就有底了。因为不管网络怎么传,最终数据都要从串口进、从串口出。串口通了,整个链路就通了一半。