Micro LOG:单片机开发调试带颜色打印
这篇文档介绍了一个为嵌入式开发设计的定制化LOG系统,它通过宏定义实现,具备彩色分级、自动时间戳、代码定位和零运行时开销等特点。文档还提供了在STM32F103RCT6平台上使用STM32CubeMX和HAL库进行工程配置的步骤,以及如何将LOG系统移植到Keil5工程中的方法。最后,文档提供了一个完整的使用示例和源码获取方式。这个Micro LOG方案能够在资源受限的嵌入式环境中提供专业级的调试
1. 项目概述:为什么需要定制化LOG系统
在嵌入式开发中,调试信息输出是排查问题的生命线。标准的printf功能单一,无法满足复杂场景需求。这个Micro LOG驱动通过宏定义魔法,实现了:
- 带颜色分级:信息/警告/错误一目了然
- 自动时间戳:精确到毫秒的运行时序分析
- 代码定位:自动显示文件名、行号、函数名
- 零运行时开销:所有功能在编译期完成
示例代码:STM32F103RCT6 + STM32CubeMX + HAL库,通过USART1输出(波特率115200)

2. 工程配置
2.1 STM32CubeMX 配置

①:配置时钟,然后设置时钟树,HCLK设置为72MHz,确保HAL_GetTick精度为1ms
②:调试接口:SYS中Debug选项选择Serial Wire,防止烧录后无法连接
③:USART1配置:模式设为Asynchronous,参数默认115200-8-N-1
④:NVIC:如需要中断接收可开启USART1全局中断(本LOG方案仅使用轮询发送)
2.2 Keil5配置
新增文件夹与.c.h文件,将文章末尾的源码复制进去:
Project/
├── Core/
├── Drivers/
├── Micro_LOG/ ← 新增
│ ├── micro_log.c
│ ├── micro_log.h
└── MDK-ARM/
(1)添加文件micro_log.c.h 到工程文件中,然后keil5添加组,加入.c.h文件:

(2) 添加头文件路径:

(3) 必须勾选Use MicroLIB选项(Options for Target -> Target),否则printf会链接标准C库,导致HAL_UART_Transmit无法正常工作,程序卡死。

3. 移植内容,适配工程
3.1 重定向原理
单片机没有终端,需要将printf的输出重定向到串口。C库通过调用底层fputc函数实现每个字符的输出:
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
为何必须勾选MicroLIB?
- 标准C库
printf依赖大量系统调用(文件系统、内存管理等),在裸机环境无法链接 - MicroLIB是Keil为嵌入式裁剪的精简库,允许用户自定义
fputc实现 - 不勾选会导致链接错误或运行时异常(通常表现为程序启动后立即死机)
3.2 头文件内容修改
只需要修改micro_log.h头文件中的 串口打印 修改为自己的函数接口,以及还有时间tick获取函数,其他无需修改

4. 宏的魔法:编译器内置符号与变参宏
4.1 编译器自动提供的定位信息
这些宏由编译器在预处理阶段自动替换:
__FILE__:当前源文件的完整路径字符串(如"C:\\Project\\Src\\main.c")__LINE__:当前代码行号整数__FUNCTION__:当前函数名字符串__VA_ARGS__:可变参数宏的参数列表
4.2 变参宏的两种格式
基础格式:
#define LOG(fmt, ...) log_printf(fmt, ##__VA_ARGS__)
...代表可变参数,调用时fmt后的所有参数会被打包__VA_ARGS__在宏展开时解包为实际参数##__VA_ARGS__的##作用是:当可变参数为空时,自动删除前面的逗号,避免编译错误
带颜色格式:
#define LOG_C(color, fmt, ...) log_printf(color fmt CLR_RESET, ##__VA_ARGS__)
- 这里用到了字符串自动拼接特性:
color fmt CLR_RESET会被连接成一个格式字符串 - 例如:
LOG_C(CLR_RED, "Error: %d", 500)展开为log_printf("\x1b[31m" "Error: %d" "\x1b[0m", 500) - C语言中相邻字符串字面量会自动拼接,最终为
log_printf("\x1b[31mError: %d\x1b[0m", 500)
5. 三大核心功能实现原理
5.1 时间戳机制
基于HAL_GetTick()获取系统启动后的毫秒计数(32位循环计数):
// 转换逻辑:总毫秒 → 时分秒毫秒
uint32_t log_time = HAL_GetTick();
uint16_t msec = log_time % 1000;
uint8_t sec = (log_time / 1000) % 60;
uint8_t min = (log_time / 1000 / 60) % 60;
uint8_t hour = log_time / 1000 / 60 / 60;
精度与范围:
- 精度1ms,最大计时约49天(2^32 ms)
- 超过24小时不自动回零,持续累加
5.2 颜色输出原理
利用ANSI转义码控制终端显示属性:
- 格式:
\x1b[XXm或\033[XXm \x1b是ESC键的ASCII码(十进制27)[31m设置前景色为红色,[0m重置所有属性
单片机串口发送这些字符后,PC端串口助手必须支持ANSI解析才能显示颜色(如MobaXterm、Xshell、VSCode Serial Monitor),教程中使用的软件是微软商店下载的“串口调试助手”。
5.3 代码定位实现
通过__FILE__和__LINE__与__FUNCTION__在调用点捕获信息:
LOG_I("初始化完成");
// 实际展开:
log_printf(CLR_GREEN "[%s][%s:%d] 初始化完成\n" CLR_RESET,
log_get_time(),
log_get_filename("C:\\Project\\Src\\main.c"), // __FILE__的值
__FUNCTION__, //__FUNCTION__的值
98); // __LINE__的值
log_get_filename()函数使用strrchr查找最后一个路径分隔符,返回文件名部分,避免长路径占用串口带宽。
6. 断言机制:运行时检查利器
LOG_ASSERT(expr);
#define LOG_ASSERT(expr) \
do \
{ \
if (!(expr)) \
{ \
log_printf(CLR_RED "\r\n[ASSERT FAILED] %s\r\nFile: %s:%d\r\nFunction: %s\r\n" CLR_RESET, \
#expr, log_get_filename(__FILE__), __LINE__, __FUNCTION__); \
while (1) \
; \
} \
} while (0)
当expr为假时,打印错误信息并死循环:
- 打印失败表达式字符串(
#expr的字符串化操作) - 显示文件名、行号、函数名
- 锁定程序防止继续运行
适用场景:
- 指针空检查:
LOG_ASSERT(p != NULL); - 参数合法性:
LOG_ASSERT(size <= MAX_SIZE); - 状态机完整性:在default分支中触发
7. 完整使用示例
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
LOG_I("系统启动,版本v1.0\r\n"); // 绿色普通信息
while (1)
{
LOG("普通打印:%d\r\n", 123); // 无附加信息
LOG_C(CLR_GREEN, "纯绿色文本\r\n"); // 仅颜色
LOG_T("带时间戳:传感器值=%d\r\n", 1234); // 时间+数据
LOG_TC(CLR_BLUE, "蓝色时间戳\r\n"); // 颜色+时间
LOG_W("电压偏低:%.2fV\r\n", 3.16); // 黄色警告+全信息
LOG_E("通信超时!\r\n"); // 红色错误+全信息
HAL_Delay(1000);
}
}
8. 效果展示与验证

预期输出格式:
[00:00:00.001][main.c,main:98] 系统启动,版本v1.0
普通打印:123
纯绿色文本
[00:00:00.010] 带时间戳:传感器值=1234
[00:00:00.014] 蓝色时间戳
[00:00:00.017][main.c,main:117] 电压偏低:3.14V
[00:00:00.023][main.c,main:119] 通信超时!
9.结语
这个Micro LOG方案在最小资源占用下提供了专业级的调试体验,关键是所有功能通过宏在编译期展开,不增加运行时负担。代码已验证可在STM32F103RCT6上稳定运行,如移植其他芯片,只需要适配自己的printf函数即可。
10.源码
扫描下方二维码加入嵌入式技术交流群,即可获取源码压缩包,群内同步答疑 AT 驱动开发、移植问题及后续版本更新,同时有更多嵌入式问题也欢迎讨论~ +q:181921938

源码遵循开源TIM协议,如果这个项目对你的物联网开发有帮助,请给它一个 ⭐ !:
- 源码仓库(GitHub):ZeroOneLab/MicroLOG
- 源码仓库(Gitee):零壹实验室-ZeroOneLab/MicroLOG
10.1 micro_log.c
#include "micro_log.h"
#include "usart.h"
static char log_time_str[16];
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
/**
* @brief 获取当前时间
* @param 无
* @retval 无
*/
char *log_get_time(void)
{
uint32_t log_time = log_get_tick();
uint16_t msec = log_time % 1000;
log_time /= 1000;
uint8_t sec = log_time % 60;
log_time /= 60;
uint8_t min = log_time % 60;
log_time /= 60;
uint8_t hour = log_time;
snprintf(log_time_str, sizeof(log_time_str), "%02d:%02d:%02d.%03d", hour, min, sec, msec);
return log_time_str;
}
/**
* @brief 从路径中提取文件名
* @param path 文件路径
* @retval 文件名指针
*/
const char *log_get_filename(const char *path)
{
const char *p = strrchr(path, '/');
if (p)
return p + 1;
p = strrchr(path, '\\');
if (p)
return p + 1;
return path;
}
10.2 micro_log.h
#ifndef _MICRO_LOG_H
#define _MICRO_LOG_H
#include <stdio.h>
#include <string.h>
#include "main.h"
char *log_get_time(void); // 获取时间
const char *log_get_filename(const char *path); // 获取文件名
#define log_printf(fmt, ...) printf(fmt, ##__VA_ARGS__) // 核心函数打印接口
#define log_get_tick HAL_GetTick // 获取系统滴答计数
/* ============== 颜色定义 ============== */
#define CLR_RED "\x1b[31m" // 红色
#define CLR_ORANGE "\x1b[33;1m" // 橙色
#define CLR_YELLOW "\x1b[33m" // 黄色
#define CLR_GREEN "\x1b[32m" // 绿色
#define CLR_BLUE "\x1b[34m" // 蓝色
#define CLR_PURPLE "\x1b[35m" // 紫色
#define CLR_RESET "\x1b[0m" // 重置颜色
/* ============== 核心日志接口 ============== */
// 1. LOG(...) - 无时间戳printf,无颜色
#define LOG(fmt, ...) log_printf(fmt, ##__VA_ARGS__)
// 2. LOG_C(color, ...) - 无时间戳printf,带颜色
#define LOG_C(color, fmt, ...) log_printf(color fmt CLR_RESET, ##__VA_ARGS__)
// 3. LOG_T(...) - 带时间戳printf,无颜色
#define LOG_T(fmt, ...) log_printf("[%s] " fmt, log_get_time(), ##__VA_ARGS__)
// 4. LOG_TC(color, ...) - 带时间戳printf,带颜色
#define LOG_TC(color, fmt, ...) log_printf(color "[%s] " fmt CLR_RESET, log_get_time(), ##__VA_ARGS__)
/* ============== 文件位置+时间戳日志 ============== */
// LOG_I - 文件+时间信息(绿色)
#define LOG_I(fmt, ...) log_printf(CLR_GREEN "[%s][%s,%s:%d] " fmt CLR_RESET, \
log_get_time(), log_get_filename(__FILE__), __FUNCTION__, __LINE__, ##__VA_ARGS__)
// LOG_W - 文件+时间警告(黄色)
#define LOG_W(fmt, ...) log_printf(CLR_YELLOW "[%s][%s,%s:%d] " fmt CLR_RESET, \
log_get_time(), log_get_filename(__FILE__), __FUNCTION__, __LINE__, ##__VA_ARGS__)
// LOG_E - 文件+时间错误(红色)
#define LOG_E(fmt, ...) log_printf(CLR_RED "[%s][%s,%s:%d] " fmt CLR_RESET, \
log_get_time(), log_get_filename(__FILE__), __FUNCTION__, __LINE__, ##__VA_ARGS__)
/* ============== 断言宏 ============== */
#define LOG_ASSERT(expr) \
do \
{ \
if (!(expr)) \
{ \
log_printf(CLR_RED "\r\n[ASSERT FAILED] %s\r\nFile: %s:%d\r\nFunction: %s\r\n" CLR_RESET, \
#expr, log_get_filename(__FILE__), __LINE__, __FUNCTION__); \
while (1) \
; \
} \
} while (0)
#endif /* _MICRO_LOG_H */
更多推荐




所有评论(0)