admin 管理员组

文章数量: 1184232

还在靠printf瞎调试?8个嵌入式日志技巧,让你从“踩坑王”变“debug大神”!

你有没有过这种崩溃时刻?嵌入式程序一跑就卡,串口里堆着密密麻麻的printf,一会儿是“连接了”,一会儿是“读数据了”,盯着屏幕看半小时,除了“这到底哪错了”的灵魂拷问,啥线索都抓不到——最后拍着桌子想“要是日志能‘说人话’就好了”?

别慌!今天不是教你多写几行printf凑数,而是给你一套“日志调试外挂”。学会这8个技巧,下次遇到bug,日志会主动告诉你“问题在WiFi模块第23行”“数据处理慢了500微秒”,再也不用像没头苍蝇一样乱撞~

1. 给日志“贴标签”:一眼锁定问题模块

先问个扎心的问题:如果日志只写“连接失败”,你能分清是WiFi、传感器还是存储模块出的错吗?大概率只能懵在原地。

解决办法超简单:给每个模块起个“专属外号”,比如WiFi模块的日志都带【WIFI】,传感器的带【SENSOR】,存储的带【STORAGE】。写代码时用模块专属的日志宏,比如WiFi模块用WIFI_LOG_I(I是“信息”的意思),传感器用SENSOR_LOG_E(E是“错误”)。

举个实际例子,在wifi_module.c里写:

// 先给WiFi模块定义标签
#define LOG_TAG_WIFI "[WIFI]"
// 再定义专属日志宏
#define WIFI_LOG_I(fmt, ...) LOG_INFO(LOG_TAG_WIFI fmt, ##__VA_ARGS__)

// 用的时候超直观
void wifi_scan_networks(void){
  WIFI_LOG_I("开始扫描网络啦...");
  int count = scan_available_networks();
  if (count > 0){
    WIFI_LOG_I("找到%d个网络", count);
  } else {
    WIFI_LOG_W("一个网络都没找到哦~", count); // W是“警告”
  }
}

最后日志输出长这样,谁的问题一目了然:

[WIFI]开始扫描网络啦...
[WIFI]找到3个网络
[WIFI]网络[0]:MyHome_WiFi,信号强度:-45

再也不用对着“连接失败”猜半天——看标签就知道“锅”是谁的!

2. 搞懂“什么时候打日志”:别等崩了才补

很多人要么“日志少得可怜”,崩了都不知道哪步错;要么“日志多到刷屏”,关键信息被淹没。其实只要盯紧这8个场景,日志就够用不冗余:

① 错误处理:“翻车现场”必须记

内存申请失败、传感器初始化崩了、I2C读不出数据——这些“致命翻车”必须写日志!别只写“错了”,要把“翻车细节”说清楚:

// 比如内存申请失败,日志要记“要了多少内存,还剩多少”
void *ptr = malloc(size);
if (ptr == NULL) {
  LOG_ERROR("内存申请失败:要%d字节,剩余%d字节", size, get_free_memory());
  return -1;
}

下次看日志就知道:哦,不是代码逻辑错,是内存不够了!

② 关键操作:给流程“拍vlog”

像WiFi连接、数据上传这种核心流程,要从“开始”到“结束”记全程。比如WiFi连热点:

int wifi_connect(const char *ssid, const char *password) {
  LOG_INFO("准备连热点:%s", ssid); // 第一步:开始连
  if (wifi_scan(ssid) != 0){
    LOG_WARN("没找到这个热点:%s", ssid); // 第二步:扫描失败
    return -1;
  }
  if (wifi_auth(password) != 0){
    LOG_ERROR("密码错了,认证失败!"); // 第三步:认证翻车
    return -2;
  }
  LOG_INFO("连成功啦!IP是:%s", get_local_ip()); // 第四步:成功收尾
  return 0;
}

哪步卡壳,日志里清清楚楚,不用反复单步调试。

③ 系统启动/关闭:“开机清单”不能漏

系统启动时,要记版本、编译时间、初始化步骤;关闭时记退出原因——就像电脑开机的“自检清单”:

void system_startup(void) {
  LOG_INFO("=== 系统开始启动 ===");
  LOG_INFO("固件版本:%s", FIRMWARE_VERSION); // 比如V1.2.3
  LOG_INFO("编译时间:%s %s", __DATE__, __TIME__); // 2024-09-02 08:30:00
  LOG_INFO("正在初始化硬件...");
  hardware_init();
  LOG_INFO("=== 系统准备好啦 ===");
}

要是启动卡在“初始化硬件”,直接定位到hardware_init()函数,省半小时排查时间。

④ 性能监控:给系统“装体温计”

CPU占用太高、内存快满了——这些“亚健康信号”要实时记!比如每隔一段时间查一次性能:

void log_system_performance(void){
  performance_info_t perf;
  get_performance_info(&perf); // 拿CPU、内存数据
  LOG_INFO("性能状态:内存用%d/%dKB,CPU占%d%%,活跃任务%d个",
           perf.memory_used, perf.memory_total, perf.cpu_usage, perf.active_tasks);
  
  // 要是CPU超90%,立刻警告
  if (perf.cpu_usage > 90){
    LOG_WARN("CPU要炸了!占用率:%d%%", perf.cpu_usage);
  }
}

不用等系统卡爆才发现问题,日志会提前“喊救命”。

⑤ 通信日志:给数据“拍X光”

串口、I2C、WiFi发的数据,光看“发了”没用,要记“发了啥、发了多少字节”。比如把二进制数据转成十六进制写日志:

void log_protocol_data(const char *direction, uint8_t *data, int len) {
  char hex_str[256]={0};
  bytes_to_hex_string(data, len, hex_str, sizeof(hex_str)); // 转十六进制
  LOG_DEBUG("通信数据:%s方向,%d字节,内容:%s", direction, len, hex_str);
}

要是发出去的数据不对,日志里一看十六进制就知道——比如本该是0x01,结果发了0x02,瞬间定位问题。

⑥ 用户行为:记好“操作台账”

用户按了哪个键、点了哪个按钮,要记下来。比如按键按下时:

void button_press_handler(int button_id) {
  char details[64];
  snprintf(details, sizeof(details), "第%d号按键被按下", button_id);
  log_user_action("按键操作", details); // 记用户行为
  handle_button_press(button_id);
}

要是用户说“按了按钮没反应”,查日志就知道:是没触发handler,还是handler里的逻辑错了。

⑦ 分支执行:给流程“画路线图”

switch-case、if-else这些分支,很容易“走岔路”。比如处理传感器数据时,记清楚走了哪个分支:

void process_sensor_data(sensor_type_t type, int value) {
  switch(type){
    case SENSOR_TYPE_TEMPERATURE:
      LOG_DEBUG("处理温度数据:%d℃", value); // 走温度分支
      process_temperature(value);
      break;
    case SENSOR_TYPE_HUMIDITY:
      LOG_DEBUG("处理湿度数据:%d%%", value); // 走湿度分支
      process_humidity(value);
      break;
    default:
      LOG_WARN("不认识的传感器类型:%d,值:%d", type, value); // 走岔路警告
      break;
  }
}

要是传感器数据没处理,看日志就知道:哦,是传感器类型传错了,走了default分支。

⑧ 崩溃信息:“事故现场”要保护

程序崩了别慌,让日志记下车祸现场——比如调用栈(哪个函数调用了哪个函数)、系统状态:

void crash_handler(int signo) {
  void *array[100];
  size_t size;
  char **strings;
  
  LOG_FATAL("程序崩了!信号:%d(%s)", signo, strsignal(signo)); // 致命错误
  // 拿调用栈
  size = backtrace(array, 100);
  strings = backtrace_symbols(array, size);
  LOG_FATAL("调用栈(%zd层):", size);
  for(size_t i=0;i<size;i++){
    LOG_FATAL("  第%d层:%s", i, strings[i]); // 每层函数都记
  }
  // 记系统状态
  LOG_FATAL("崩溃时状态:CPU=%d%%,内存=%dKB", get_cpu_usage(), get_memory_usage());
}

有了调用栈,比如看到“第3层:wifi_send_data”,直接去这个函数查,比瞎猜快10倍。

3. 日志“开关”:不想看的日志,一键关掉

有些模块平时不用看日志,比如存储模块,要是它的日志一直跳,反而挡着WiFi、传感器的关键信息——这时候就需要“开关”。

在配置文件里加个宏定义,想关哪个模块就设为0:

// config.h 里控制
#define WIFI_LOG_ENABLE 1    // 开WiFi日志
#define SENSOR_LOG_ENABLE 1  // 开传感器日志
#define STORAGE_LOG_ENABLE 0 // 关存储日志(不用看)

// 再写条件编译的日志宏
#if WIFI_LOG_ENABLE
#define WIFI_LOG(fmt, ...) LOG_DEBUG("[WIFI]" fmt, ##__VA_ARGS__)
#else
#define WIFI_LOG(fmt, ...) // 关了就啥都不输出,编译时自动删掉
#endif

要是后来想看看存储模块的日志,改回STORAGE_LOG_ENABLE 1就行,不用改代码逻辑,灵活得很。

4. 加“时间戳”:谁快谁慢,一眼看穿

你有没有遇到过“两个操作谁先谁后”的困惑?比如“传感器读数”和“WiFi发数据”,到底哪个慢了,导致数据没发出去?

给日志加“时间戳”就行,精确到微秒(1微秒=100万分之一秒),比你掐表准多了:

// 先写个拿微秒时间戳的函数
#include <sys/time.h>
uint64_t get_timestamp_us(void){
  struct timeval tv;
  gettimeofday(&tv, NULL);
  return tv.tv_sec * 1000000ULL + tv.tv_usec; // 秒转微秒+微秒部分
}

// 再写个带时间戳的日志宏
#define LOG_WITH_TIMESTAMP(level, fmt, ...)\
do {\
  uint64_t ts = get_timestamp_us();\
  printf("[%llu.%06llu][%s]" fmt "\n",\
         ts / 1000000, ts % 1000000, level, ##__VA_ARGS__);\
} while(0)

用的时候,日志会显示:

[1620000000.123456][INFO][WIFI]开始连接热点
[1620000000.123987][INFO][WIFI]连接成功

一算就知道:WiFi连接用了531微秒,要是超过1秒,就知道是网络慢了,不是代码错了。

还能测函数耗时,比如数据处理:

// 开始计时宏
#define PERF_START(name) \
uint64_t perf_start_##name = get_timestamp_us();\
LOG_DEBUG("开始计时:%s", #name)

// 结束计时宏
#define PERF_END(name)\
uint64_t perf_end_##name = get_timestamp_us();\
LOG_INFO("计时结束:%s用了%llu微秒", #name, perf_end_##name - perf_start_##name)

// 用的时候
void data_processing(void) {
  PERF_START(总数据处理);       // 开始总计时
  PERF_START(数据验证);         // 开始验证计时
  validate_input_data();
  PERF_END(数据验证);           // 结束验证:用了200微秒
  PERF_START(算法执行);         // 开始算法计时
  execute_algorithm();
  PERF_END(算法执行);           // 结束算法:用了300微秒
  PERF_END(总数据处理);         // 结束总计时:用了500微秒
}

哪个步骤慢了,日志直接告诉你,性能瓶颈一下就抓着了。

5. 日志“分级”:重要的日志,别被刷屏淹没

日志也分“轻重缓急”,不能啥都堆一起。比如“系统崩了”和“进入函数了”,重要程度差100倍,要是全显示,前者会被后者淹没。

先定义5个常用级别,从“致命”到“啰嗦”:

级别用途例子
FATAL(致命)系统崩了,没法继续跑内存耗尽、核心模块初始化失败
ERROR(错误)功能用不了,但系统还能跑WiFi认证失败、传感器读不出数据
WARN(警告)有问题但不影响功能,需注意传感器值超范围、内存快满了
INFO(信息)正常状态,比如启动、连接成功系统启动完成、WiFi连好了
DEBUG(调试)调试用的细节,比如进入函数、变量值进入data_processing函数

然后设个“当前日志级别”,比如设成INFO,那DEBUG和TRACE的日志就不显示,只留重要的:

// 定义级别
typedef enum {
  LOG_LEVEL_FATAL = 0,
  LOG_LEVEL_ERROR = 1,
  LOG_LEVEL_WARN = 2,
  LOG_LEVEL_INFO = 3,
  LOG_LEVEL_DEBUG = 4
} log_level_t;

// 当前级别设为INFO(只显示INFO及以上)
static log_level_t g_current_log_level = LOG_LEVEL_INFO;

// 日志输出宏:级别够才显示
#define LOG_OUTPUT(level, level_str, fmt, ...) \
do {\
  if (level <= g_current_log_level){\ // 级别够不够?
    printf("[%s][%s:%d]" fmt "\n", \
           level_str, __FUNCTION__, __LINE__, ##__VA_ARGS__);\
  }\
} while(0)

// 再封装成常用宏
#define LOG_FATAL(fmt, ...) LOG_OUTPUT(LOG_LEVEL_FATAL, "FATAL", fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) LOG_OUTPUT(LOG_LEVEL_ERROR, "ERROR", fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...)  LOG_OUTPUT(LOG_LEVEL_INFO,  "INFO",  fmt, ##__VA_ARGS__)

开发时设成DEBUG,方便查细节;发布时设成WARN,只留警告和错误,不占内存也不刷屏。

6. 格式“统一”:日志不乱,搜起来才快

要是日志一会儿是“[时间]连错了”,一会儿是“WiFi:崩了”,格式乱七八糟,想搜“WiFi错误”都搜不到——所以得搞“统一格式”。

建议固定成这个模板,包含“时间、级别、文件行号、函数名、内容”:

// 统一格式:[时间戳][级别][文件名:行号][函数名] 内容
#define LOG_FORMAT "[%s][%s][%s:%d][%s] %s\n"

void log_output(log_level_t level, const char *tag,
                const char *file, int line,
                const char *func, const char *message) {
  // 拿格式化时间(比如2024-01-15 14:30:25.123456)
  char timestamp[32];
  get_formatted_timestamp(timestamp, sizeof(timestamp));
  // 拿级别字符串(比如INFO、ERROR)
  const char *level_str = get_level_string(level);
  // 按格式输出
  printf(LOG_FORMAT, timestamp, level_str, file, line, func, message);
}

// 再封装成便捷宏
#define LOG_MSG(level, tag, fmt, ...)\
do {\
  char msg_buffer[256];\
  snprintf(msg_buffer, sizeof(msg_buffer), fmt, ##__VA_ARGS__);\
  log_output(level, tag, __FILE__, __LINE__, __FUNCTION__, msg_buffer);\
} while(0)

最后日志长这样,整整齐齐:

[2024-01-15 14:30:25.123456][INFO][main.c:45][wifi_init] WiFi模块初始化好了
[2024-01-15 14:30:25.234567][DEBUG][sensor.c:128][read_temperature] 温度:25.6℃
[2024-01-15 14:30:25.345678][WARN][storage.c:67][save_data] 存储快满了:95%

想找“storage的警告”,搜“[WARN][storage.c”立刻定位;想找哪行代码,行号都标好了,直接跳转,比找迷宫还简单。

7. 动态“过滤”:不用重启,实时调日志

调试的时候,想临时看看WiFi模块的日志,又不想改代码、重新编译?这时候“动态过滤”就派上用场——像遥控器换台一样,想调啥就调啥。

比如用EasyLogger(一个常用的嵌入式日志库),初始化后就能实时控制:

#include "elog.h"

void log_system_init(void){
  elog_init(); // 初始化EasyLogger
  // 设每个级别的显示格式:比如ERROR级显示“级别+标签+时间”
  elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
  // DEBUG级显示所有信息,除了函数名
  elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);
  elog_start(); // 启动日志
}

// 运行时控制:比如串口输入命令
void runtime_log_control(void) {
  elog_set_filter_lvl(ELOG_LVL_DEBUG); // 临时切到DEBUG级别
  elog_set_filter_tag("WIFI");         // 只看WiFi模块的日志
  elog_set_filter_kw("error");         // 只看含“error”的日志
}

还能通过串口发命令控制,不用停程序:

  • 输入“log level 4” → 切换到DEBUG级别
  • 输入“log module wifi 0” → 临时关掉WiFi日志
  • 输入“log status” → 看当前日志配置

调试的时候,想看啥就调啥,不用反复重启程序,省半小时等待时间。

8. Release/Debug“智能切换”:发布时别带“调试垃圾”

开发时(Debug版)需要详细日志,方便查bug;发布时(Release版)要是还输出一堆调试日志,不仅占内存,还可能泄露敏感信息(比如密码、硬件地址)——所以得让日志“智能切换”。

#ifdef DEBUG判断编译版本,自动调整日志配置:

#ifdef DEBUG // 开发版(Debug)
#define DEBUG_LOG_ENABLE 1          // 开调试日志
#define DEFAULT_LOG_LEVEL LOG_LEVEL_DEBUG // 日志级别设为DEBUG
#define LOG_TO_UART 1               // 日志输出到串口(方便调试)
#define LOG_TO_FILE 1               // 日志存到文件(留底)
#else // 发布版(Release)
#define DEBUG_LOG_ENABLE 0          // 关调试日志
#define DEFAULT_LOG_LEVEL LOG_LEVEL_WARN // 只显示WARN及以上(重要信息)
#define LOG_TO_UART 0               // 不输出到串口(省资源)
#define LOG_TO_FILE 1               // 日志存文件(方便后期排查)
#endif

// 智能日志宏:Debug版输出,Release版删掉
#if DEBUG_LOG_ENABLE
#define DBG_LOG(fmt, ...) LOG_DEBUG(fmt, ##__VA_ARGS__)
#else
#define DBG_LOG(fmt, ...) // Release版啥都不输出,编译时自动移除
#endif

这样发布的程序,没有冗余的调试日志,又轻又安全;开发时又有详细日志,不用手动删来删去,省心!

最后:好日志不是“越多越好”,而是“该有就有”

其实嵌入式日志的核心,不是写得越多越好,而是“在关键地方写对信息”。记住这5个核心要点,日志就好用了:

  1. 关键位置必打日志(错误、核心流程、性能)
  2. 给日志贴模块标签(好定位)
  3. 按级别分类(不刷屏)
  4. 加时间戳(算耗时、看时序)
  5. 支持动态控制(省时间)

还要避开这5个误区:

  • 过度日志:像话痨一样,关键信息被淹没
  • 信息不足:就一句“错了”,跟没说一样
  • 格式混乱:不好搜、不好分析
  • 忽视性能:日志太多拖慢程序(比如每秒写1000条日志)
  • 泄露敏感:把密码、硬件地址写进日志

学会这8个技巧,下次调试的时候,你就不用对着屏幕发呆了——日志会告诉你“问题在哪、为什么错、怎么改”,从“printf小白”变成“debug大神”,真的没那么难~

本文标签: 让你 还在 大神 嵌入式 技巧