|
回调能解耦,也能把代码绕晕。嵌入式项目里要先说清谁注册、谁调用、回调在哪个上下文执行、能不能阻塞、生命周期由谁负责。把这些边界写清楚,回调才能从“到处跳”变成稳定扩展点。 --- 一、回调混乱:谁调用谁说不清 最危险的写法是只存一个裸函数指针: static void (*rxCallback)(uint8_t data);void uartIrqHandler(void){ // 中断里直接调业务回调,风险很高 rxCallback(uartReadByte());}它的问题不是函数指针本身,而是没有上下文、没有生命周期、没有执行约束。 --- 二、回调接口要带上下文 typedef void (*EventCallback_t)(void *userData, uint32_t event);typedef struct { EventCallback_t callback; void *userData; bool active;} CallbackSlot_t;注册函数要检查参数,并把上下文一起保存: bool callbackRegister(CallbackSlot_t *slot, EventCallback_t callback, void *userData){ if ((slot == NULL) || (callback == NULL)) { return false; } slot->callback = callback; slot->userData = userData; slot->active = true; return true;}--- 三、调用边界:ISR里尽量只投递事件 嵌入式架构核心原则:中断上下文极简执行,禁止耗时、阻塞、复杂业务逻辑。所有回调业务统一剥离到主循环或任务线程执行 void uartIrqHandler(void){ uint8_t data = uartReadByte(); // ISR 中只投递事件,不直接跑复杂业务回调 eventPost(EVENT_UART_RX, data);}在主线程/任务中统一调度、触发回调,保证系统实时性与稳定性: void callbackInvoke(CallbackSlot_t *slot, uint32_t event){ if ((slot != NULL) && slot->active) { slot->callback(slot->userData, event); }}这套隔离机制,彻底避免了中断阻塞、优先级翻转、系统卡死等核心问题。 --- 四、注册和调用边界 问题 | 要写清楚 | 谁注册 | 初始化阶段还是运行时 | 谁调用 | ISR、任务、线程还是主循环 | 能否阻塞 | ISR 回调必须短 | 生命周期 | 注销前对象是否还活着 | 错误处理 | 回调失败如何反馈 |
--- 五、千万不要这样做 回调里再次注册回调: 生命周期会变复杂,容易出现悬空指针。 中断里跑业务回调: 一旦回调阻塞,实时性就没了。 没有注销机制: 模块卸载或重初始化后,旧函数指针还在。 错误被吞掉:回调失败至少要有计数或日志,不要静默丢失。 --- 六、总结 回调架构的关键不是函数指针语法,而是调用边界。谁注册、谁调用、在哪个上下文、能不能阻塞,这几件事写清楚,回调才会变成扩展点,而不是隐藏调用链。 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |