回调与错误处理:在回调中传递错误码,统一错误处理
写嵌入式程序这么多年,我踩过最大的坑之一,就是错误处理。
你想想看,一个系统里几十个模块,每个函数都返回错误码。主调函数拿到错误码后,要么直接忽略,要么打印一句 "Error!" 就完事。等到系统跑飞了,你连问题出在哪都不知道。
回调函数里的错误处理,更是重灾区。回调是异步的,错误发生的时候,主调函数可能早就跑远了。这时候怎么把错误信息传回来?怎么统一处理?
嗯,今天我们就来聊聊这个话题。
回调中的错误码传递:两种主流方式
我个人习惯把回调中的错误传递分成两类:同步回调和异步回调。处理方式完全不同。
方式一:通过返回值传递错误码
这种方式适用于同步回调。回调函数执行完毕后,直接返回一个错误码给调用者。
// 定义回调类型:返回错误码
typedef int (*SensorReadCallback)(uint8_t* data, uint32_t len);
// 传感器驱动函数
int read_sensor(SensorReadCallback cb) {
uint8_t buffer[64];
int ret = cb(buffer, sizeof(buffer));
if (ret != 0) {
// 回调执行失败,统一处理
log_error("Sensor read callback failed: %d", ret);
return ret;
}
// 处理数据...
return 0;
}
// 用户实现的回调
int my_sensor_callback(uint8_t* data, uint32_t len) {
if (data == NULL || len == 0) {
return -EINVAL; // 参数无效
}
// 实际读取操作...
if (/* 硬件错误 */) {
return -EIO; // IO错误
}
return 0;
}
这种方式简单直接。但有个问题:如果回调函数既要返回数据,又要返回错误码,怎么办?
我在项目中遇到过这种情况。当时我用了输出参数+返回值的组合方式:
typedef int (*DataProcessCallback)(uint8_t* input, uint32_t input_len,
uint8_t* output, uint32_t* output_len);
// 返回值:0成功,负值表示错误码
// output_len:输出数据的实际长度
方式二:通过上下文结构体传递错误码
异步回调就没这么简单了。回调执行的时候,主调函数可能已经返回了。这时候错误码不能通过返回值传递,得找个"永久"的地方存着。
我常用的做法是:定义一个上下文结构体,把错误码塞进去。
typedef struct {
void* user_data; // 用户自定义数据
int error_code; // 错误码,初始化为0
char error_msg[64]; // 错误描述
} CallbackContext;
typedef void (*AsyncCallback)(CallbackContext* ctx, uint8_t* data, uint32_t len);
// 异步操作
void start_async_operation(AsyncCallback cb, CallbackContext* ctx) {
// 启动异步任务...
// 任务完成后调用 cb(ctx, data, len)
}
// 用户回调
void my_async_callback(CallbackContext* ctx, uint8_t* data, uint32_t len) {
if (/* 发生错误 */) {
ctx->error_code = -ETIMEDOUT;
snprintf(ctx->error_msg, sizeof(ctx->error_msg), "Operation timed out");
return;
}
// 正常处理...
}
这样做的好处是:错误信息跟着上下文走,不会丢失。主调函数可以在任意时刻检查 ctx->error_code。
统一错误处理:一个中心化的错误分发器
说实话,早期我写代码时,每个回调函数里都自己处理错误。结果就是:有的打印日志,有的直接复位,有的默默忽略。系统出问题时,排查起来特别痛苦。
后来我学乖了。设计了一个统一的错误处理中心,所有回调里的错误都交给它处理。
核心思想:回调函数只负责检测错误,不负责处理错误。错误处理由专门的模块统一完成。
// 错误处理中心
typedef void (*ErrorHandler)(int error_code, const char* module, const char* msg);
static ErrorHandler g_error_handler = NULL;
// 注册全局错误处理器
void register_error_handler(ErrorHandler handler) {
g_error_handler = handler;
}
// 错误上报接口
void report_error(int error_code, const char* module, const char* msg) {
if (g_error_handler) {
g_error_handler(error_code, module, msg);
}
}
// 回调中使用
void my_callback(void* data, uint32_t len) {
if (data == NULL) {
report_error(-EINVAL, "my_module", "data is NULL");
return;
}
// 正常处理...
}
这样设计后,整个系统的错误处理就统一了。你可以根据不同的错误等级做不同的事情:
| 错误等级 | 错误码范围 | 处理方式 |
|---|---|---|
| INFO | 0 ~ -99 | 记录日志,继续运行 |
| WARNING | -100 ~ -499 | 记录日志,尝试恢复 |
| ERROR | -500 ~ -999 | 记录日志,通知主控 |
| FATAL | -1000 以下 | 记录日志,系统复位 |
实战案例:一个带错误处理的定时器回调
我记得有一次做工业控制项目,定时器回调里经常出现超时错误。一开始每个回调自己处理,代码乱成一锅粥。后来我重构了一下:
// 定时器回调上下文
typedef struct {
TimerHandle timer;
int timeout_count;
int last_error;
} TimerContext;
// 统一的定时器错误处理
void timer_error_handler(int error_code, const char* module, const char* msg) {
// 记录到环形缓冲区
log_to_ring_buffer(error_code, module, msg);
// 如果是严重错误,触发看门狗
if (error_code < -500) {
trigger_watchdog();
}
}
// 定时器回调
void on_timer_tick(TimerContext* ctx) {
int ret = check_hardware_status();
if (ret != 0) {
// 只上报错误,不处理
report_error(ret, "timer", "Hardware check failed");
ctx->last_error = ret;
return;
}
// 正常处理...
}
// 主循环中检查错误
void main_loop() {
while (1) {
if (g_timer_ctx.last_error != 0) {
// 统一处理累积的错误
handle_accumulated_errors(&g_timer_ctx);
g_timer_ctx.last_error = 0;
}
// 其他任务...
}
}
小技巧:在回调中只设置错误码,不要直接调用复位或重启函数。把错误处理延迟到主循环或专门的错误处理线程中,可以避免在中断上下文中做危险操作。
避坑指南:我曾经犯过的错
我曾经在一个项目中,回调函数里直接调用了 printf 打印错误信息。结果在中断上下文中,printf 导致系统死锁。从那以后,我定了一条铁律:回调函数里绝对不做 IO 操作。
还有一次,我在回调里用了一个全局变量来传递错误码。结果多个回调同时触发时,错误码被覆盖了。嗯,后来我改用线程局部存储(TLS)或者上下文结构体,才彻底解决这个问题。
警告:不要在回调函数中直接调用 exit()、abort() 等终止函数。回调通常运行在库或框架的上下文中,直接终止会导致资源泄漏。正确的做法是设置一个"致命错误"标志,让主循环安全地退出。
知识体系:回调错误处理的核心逻辑
下面这张图,是我多年经验总结出来的回调错误处理流程。你看一眼就能明白整个体系:
说白了,这个流程的核心就一句话:回调只负责"报错",不负责"处理"。错误处理统一交给中心模块,根据错误等级做不同的事情。
总结
回调中的错误处理,其实就三个要点:
- 传递方式要选对:同步回调用返回值,异步回调用上下文结构体
- 处理要统一:设计一个错误处理中心,所有回调都通过它上报错误
- 回调要轻量:只检测和上报错误,不做 IO、不复位、不退出
做到这三点,你的系统在出问题时,排查起来会轻松很多。嗯,至少不用像我当年那样,对着几百行日志一个一个翻了。