4. 日志与打印调试:UART打印的配置与使用、日志分级与格式化、环形缓冲区实现日志记录
调试嵌入式系统,说白了就是跟看不见摸不着的代码斗智斗勇。你没法像PC程序那样打断点、看变量,很多时候唯一的窗口就是那根串口线。我个人习惯,拿到一块新板子,第一件事就是把UART打印调通。没有打印,就像在黑屋子里修表,全靠摸。
4.1 UART打印的配置与使用
UART打印是嵌入式调试的“Hello World”,但很多人栽在细节上。我见过不少工程师,代码逻辑没问题,就是打印不出来,最后发现是波特率配错了,或者引脚复用没开。
配置UART,核心就三步:
- 使能时钟:别忘了给UART外设和对应的GPIO口开时钟。STM32上用
__HAL_RCC_USARTx_CLK_ENABLE(),别漏了GPIO的。 - 配置GPIO:TX脚要配成复用推挽输出,RX脚配成浮空输入或上拉输入。具体看芯片手册,别想当然。
- 初始化UART:波特率、数据位、停止位、校验位。最常见的是115200 8N1。
核心要点:调试阶段,建议把UART中断优先级设高一点,避免在高负载下丢数据。我曾在电机控制项目里吃过这个亏,一启动电机,打印就断断续续,查了半天才发现是中断被抢了。
下面是一个典型的初始化代码片段,以STM32 HAL库为例:
// UART句柄定义
UART_HandleTypeDef huart1;
// UART初始化函数
void UART_Init(void)
{
// 1. 使能时钟
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 2. 配置GPIO:PA9-TX, PA10-RX
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置UART
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
}
// 重定向printf到UART
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100);
return ch;
}
小技巧:用fputc重定向printf,调试时直接写printf("value = %d\n", val);,方便又直观。但注意,printf很占资源,中断里别用。
4.2 日志分级与格式化
项目大了,打印满天飞,你根本分不清哪个是错误、哪个是警告。我建议从一开始就引入日志分级。说白了,就是给每条打印贴个标签,告诉你看的人:这条信息有多重要。
常用的分级标准:
| 级别 | 宏定义 | 使用场景 |
|---|---|---|
| ERROR | LOG_ERROR |
系统无法继续运行的致命错误 |
| WARN | LOG_WARN |
不影响运行但需要注意的异常 |
| INFO | LOG_INFO |
关键状态变化,如初始化完成 |
| DEBUG | LOG_DEBUG |
开发阶段的详细调试信息 |
实现起来很简单,用宏包装一下:
#define LOG_LEVEL 3 // 0:ERROR, 1:WARN, 2:INFO, 3:DEBUG
#define LOG_ERROR(fmt, ...) \
do { if (LOG_LEVEL >= 0) printf("[ERROR] " fmt "\r\n", ##__VA_ARGS__); } while(0)
#define LOG_WARN(fmt, ...) \
do { if (LOG_LEVEL >= 1) printf("[WARN] " fmt "\r\n", ##__VA_ARGS__); } while(0)
#define LOG_INFO(fmt, ...) \
do { if (LOG_LEVEL >= 2) printf("[INFO] " fmt "\r\n", ##__VA_ARGS__); } while(0)
#define LOG_DEBUG(fmt, ...) \
do { if (LOG_LEVEL >= 3) printf("[DEBUG] " fmt "\r\n", ##__VA_ARGS__); } while(0)
格式化方面,我习惯加上时间戳和文件名。比如:[INFO] [main.c:120] System initialized。这样出问题了,一眼就能定位到代码位置。
注意:发布版本时,记得把LOG_LEVEL设低,或者干脆关掉所有调试打印。我曾经见过一个产品,线上版本还开着DEBUG日志,结果串口被日志撑爆,导致通信卡死。
4.3 环形缓冲区实现日志记录
直接printf有个大问题:它是阻塞的。如果UART发送速度跟不上日志产生速度,系统就会被拖慢。尤其是在中断里,你总不能等它发完再出来吧?
解决方案就是环形缓冲区。说白了,就是一个固定大小的数组,加上读写指针。日志先写到缓冲区里,后台任务或者中断再慢慢往外发。这样,日志产生者不会被阻塞。
下面是一个简单的环形缓冲区实现:
#define RING_BUF_SIZE 1024
typedef struct {
uint8_t buffer[RING_BUF_SIZE];
volatile uint16_t head; // 写指针
volatile uint16_t tail; // 读指针
} ring_buffer_t;
ring_buffer_t log_ring;
// 初始化
void ring_buffer_init(ring_buffer_t *rb)
{
rb->head = 0;
rb->tail = 0;
}
// 写入一个字节
int ring_buffer_put(ring_buffer_t *rb, uint8_t data)
{
uint16_t next = (rb->head + 1) % RING_BUF_SIZE;
if (next == rb->tail) {
return -1; // 缓冲区满,丢数据
}
rb->buffer[rb->head] = data;
rb->head = next;
return 0;
}
// 读取一个字节
int ring_buffer_get(ring_buffer_t *rb, uint8_t *data)
{
if (rb->head == rb->tail) {
return -1; // 缓冲区空
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % RING_BUF_SIZE;
return 0;
}
// 日志写入函数(非阻塞)
void log_write(const char *str)
{
while (*str) {
if (ring_buffer_put(&log_ring, *str) != 0) {
// 缓冲区满了,丢弃后续字符
break;
}
str++;
}
}
// 后台任务:从缓冲区取数据并发送
void log_send_task(void)
{
uint8_t ch;
while (1) {
if (ring_buffer_get(&log_ring, &ch) == 0) {
HAL_UART_Transmit(&huart1, &ch, 1, 10);
} else {
// 缓冲区空,稍等再查
HAL_Delay(1);
}
}
}
关键设计:环形缓冲区里,head == tail表示空,(head + 1) % SIZE == tail表示满。这样会浪费一个字节的空间,但判断逻辑简单可靠。我见过有人用计数方式,结果在多线程环境下计数不同步,数据全乱了。
你想想看,有了环形缓冲区,中断里只管往里面塞数据,主循环里慢慢往外吐。哪怕UART波特率只有9600,也不会拖慢系统响应。
下面这张图展示了整个日志系统的数据流:
经验之谈:缓冲区大小怎么定?我一般按最坏情况算:假设所有任务同时打日志,每秒产生多少字节,再乘以2作为余量。比如每秒最多1KB日志,缓冲区设2KB就够。太大浪费RAM,太小容易丢数据。
嗯,到这里,日志系统的基本框架就搭起来了。从UART配置,到分级格式化,再到环形缓冲区异步输出,每一步都有坑,但也都有解法。你动手试试,把这几块拼起来,调试效率能提升一大截。