日志采集基础:如何配置调试串口(UART)输出
烧录失败的时候,最怕的就是两眼一抹黑。芯片不跑、灯不亮、调试器连不上——这时候你手里还剩什么?说白了,就剩一根串口线。
我做了十几年固件,踩过最大的坑就是:日志系统没配好,出了问题连个屁都抓不到。所以今天咱们就聊聊,怎么把UART调试串口配得稳稳当当,确保日志一条都不丢。
1. 选对串口引脚,别让硬件坑了你
先别急着写代码。你得确认板子上哪个UART是留给调试的。很多MCU有多个UART外设,但有些引脚可能被复用成其他功能。
我的习惯是:拿到新板子第一件事,翻原理图或者数据手册,确认以下几点:
- UART编号:比如USART1、UART4,别搞混
- 引脚号:TX是哪个GPIO,RX是哪个
- 复用功能:是否需要配置AFIO或GPIO_AF
- 电平标准:3.3V还是1.8V?接USB转串口模块时要注意
| 检查项 | 常见问题 | 我的建议 |
|---|---|---|
| UART编号 | 用了不支持中断的UART | 优先选带FIFO和硬件流控的UART |
| 引脚复用 | 被其他外设占用 | 初始化时强制重映射 |
| 电平匹配 | 3.3V接5V烧坏引脚 | 加电平转换或选兼容模块 |
2. 波特率:不是越快越好
波特率这东西,很多人觉得设得越高越好。115200、921600,甚至2Mbps往上怼。但你想想看,波特率越高,对时钟精度和线路质量的要求也越高。
我个人的经验是:量产调试用115200,实验室调试用460800。为什么?
- 115200:几乎所有串口工具都支持,线长3米以内基本稳定
- 460800:适合短距离、低干扰环境,日志量大的时候能明显减少丢包
- 921600及以上:除非你用屏蔽线+短距离,否则别碰。我见过太多人设了1Mbps,结果跑几步就乱码
核心原则:波特率的选择要满足 系统时钟误差 < 2%。你可以用公式算一下:
实际波特率 = 系统时钟 / (16 * USARTDIV)
误差 = |(目标波特率 - 实际波特率) / 目标波特率| * 100%
如果误差超过2%,建议换一个能整除的波特率值。
3. 流控:什么时候该用硬件流控?
很多人觉得流控可有可无。我一开始也这么想,直到有一次……
那是个跑RTOS的项目,任务多、日志量大。我用的是115200波特率,没开流控。结果系统跑着跑着就卡死了——日志发送缓冲区满了,但串口还在拼命往外吐,CPU被中断占满,任务调度都停了。
从那以后,我给自己定了个规矩:
- 日志量小(每秒几十条以内):无流控,用中断发送就够了
- 日志量大(每秒几百条以上):必须开硬件流控(RTS/CTS)
- 极端情况(比如DMA+环形缓冲区):硬件流控+软件握手机制双保险
| 场景 | 推荐配置 | 原因 |
|---|---|---|
| 简单调试 | 无流控,中断发送 | 实现简单,资源占用少 |
| 中等日志量 | 无流控,DMA发送 | 减少CPU占用,提高吞吐 |
| 高日志量 | 硬件流控 + DMA | 防止缓冲区溢出,保证不丢数据 |
4. 缓冲区设计:日志不丢的核心
配置好串口只是第一步。真正决定日志会不会丢的,是缓冲区怎么设计。
我见过太多人直接用 printf 往串口怼,结果系统一忙,字符就丢了。正确的做法是:
- 用环形缓冲区:生产者(你的代码)往缓冲区写,消费者(UART中断或DMA)从缓冲区读
- 缓冲区大小要合理:太小容易溢出,太大浪费内存。我一般按
最大日志速率 × 最大延迟时间来算 - 加超时丢弃机制:如果缓冲区满了,老日志该丢就丢,别让系统卡死
一个小技巧:在环形缓冲区里加一个 overflow_count 计数器。每次丢日志时自增,这样你事后能知道丢了多少条。我习惯把这个值放在系统心跳里定期打印出来。
5. 配置流程总览
说了这么多,咱们用一张图把整个流程串起来。你照着这个顺序做,基本不会出问题。
6. 代码示例:一个靠谱的UART初始化
光说不练假把式。下面是我个人常用的UART初始化代码,你拿去改改就能用。
// 以STM32 HAL库为例,配置UART调试串口
void Debug_UART_Init(void)
{
UART_HandleTypeDef huart;
// 1. 使能时钟
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 2. 配置GPIO复用
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_9 | GPIO_PIN_10; // TX=PA9, RX=PA10
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &gpio);
// 3. 配置UART参数
huart.Instance = USART1;
huart.Init.BaudRate = 115200; // 波特率
huart.Init.WordLength = UART_WORDLENGTH_8B;
huart.Init.StopBits = UART_STOPBITS_1;
huart.Init.Parity = UART_PARITY_NONE;
huart.Init.Mode = UART_MODE_TX_RX;
huart.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 无流控
huart.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart);
// 4. 使能UART中断(用于接收)
HAL_NVIC_SetPriority(USART1_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
注意:上面的代码里 HwFlowCtl 我设成了 NONE。如果你要用硬件流控,记得改成 UART_HWCONTROL_RTS_CTS,并且把RTS/CTS对应的GPIO也配好。
7. 验证:怎么确认日志没丢?
配置完了,怎么知道它到底稳不稳?我的做法是:
- 压力测试:写一个循环,每秒打印1000条日志,跑10分钟
- 对比计数:每条日志带一个递增序号,收完后检查有没有跳号
- 观察缓冲区:把
overflow_count打印出来,看有没有非零值
如果发现丢日志,别急着改代码。先拿示波器看TX脚的波形——很多时候是硬件问题,不是软件问题。我遇到过好几次,最后发现是杜邦线接触不良。
我曾经花了一整天调一个丢日志的问题,最后发现是USB转串口模块的RX引脚虚焊了。从那以后,我包里常备一个逻辑分析仪,出问题先看波形,不瞎猜。
好了,日志采集的基础就这些。配置好UART,你就有了烧录失败时的第一道防线。下一件事,就是怎么把这些日志高效地存下来——那是后面章节的内容了。