单片机回调函数实战:告别阻塞代码,实现事件驱动编程
1. 项目概述从“又卡又乱”到“回调函数”的救赎如果你写过一段时间的单片机程序尤其是那种带点复杂逻辑、需要处理多个事件比如按键、串口数据、定时任务的大概率会经历过这样的场景主循环main里的while(1)越来越臃肿里面塞满了各种if判断和delay函数。程序跑起来感觉反应迟钝按个键要等半天才有反应串口数据一多就容易丢包。更头疼的是代码结构想加个新功能得在一大坨if-else里小心翼翼地找位置生怕改错了哪一行逻辑。这就是典型的“又卡又乱”的单片机代码。很多人把问题归咎于单片机性能太弱或者自己算法写得不好。但很多时候问题的根源不在于算力而在于程序的结构设计。一个最直接、也最容易被忽视的“解药”就是回调函数。回调函数不是什么高深莫测的黑科技它本质上是一种编程范式一种“你准备好数据/事件后通知我来处理”的约定。在单片机这种单线程、资源受限的环境里恰当地使用回调函数能像疏通拥堵的河道一样让程序流程变得清晰、响应变得及时。这篇文章我就以一个老嵌入式工程师踩过无数坑的视角来拆解为什么你的代码会卡会乱以及如何用回调函数这把手术刀对代码进行一场彻底的重构。2. 代码“又卡又乱”的根源剖析在深入回调函数之前我们必须先搞清楚敌人长什么样。“卡”和“乱”是表象其背后是嵌入式裸机编程无RTOS中几种常见的反模式。2.1 “卡”的罪魁祸首阻塞式编程与轮询地狱“卡”的本质是CPU时间被无效或低效地占用导致关键任务无法得到及时响应。2.1.1 无处不在的delay这是新手最常犯的错误。为了等待一个事件如按键消抖、传感器稳定、等待固定时间直接在代码里写delay_ms(100)。在这100毫秒里CPU除了空转计数什么也做不了。如果主循环中这样的delay有好几个整个系统的响应速度就会呈指数级下降。想象一下你在等一个快递员按键按下但你不时去门口看一眼轮询而是搬个板凳坐在门口死等delay这期间其他快递员串口数据、定时任务来了你也完全不知道。2.1.2 密集轮询消耗CPU另一种“卡”来源于while循环等待。例如等待一个ADC转换完成标志位while(ADC_GetFlagStatus(ADC_FLAG_EOC) RESET); // 死等转换完成或者等待串口发送完成while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 死等发送完成这些代码在标志位未置起前会持续占用CPU进行判断。在高速运行的MCU上这种等待可能只消耗几个微秒影响不大。但在低速MCU上或者等待一个外部设备如I2C从设备响应时这种阻塞可能是毫秒甚至十毫秒级的同样会严重拖慢系统。2.1.3 冗长的顺序处理在主循环中如果有一个处理函数本身执行时间很长比如一个复杂的数学运算、一段冗长的字符串拼接那么在这个函数执行期间系统同样无法响应其他事件。即使你没有用delay程序依然会“卡”住。2.2 “乱”的结构根源高耦合与面条式代码“乱”指的是代码结构混乱模块间纠缠不清维护和扩展困难。2.2.1 上帝视角的主循环所有的事件检测和处理逻辑都堆砌在主循环函数里。主循环成了一个“上帝类”它需要知道按键如何扫描、串口数据如何解析、LED如何闪烁等所有细节。这违反了“单一职责原则”。当你想修改按键处理逻辑时不得不去动主循环的代码风险很高。2.2.2 模块间的直接调用链模块A直接调用模块B的函数模块B又直接调用模块C的函数。这种紧耦合使得任何一个模块的修改都可能产生连锁反应。例如显示模块直接调用了传感器模块的读取函数来获取数据显示。如果后来传感器型号换了接口变了你就必须去修改显示模块的代码这显然是不合理的。2.2.3 状态标志位满天飞为了在模块间通信我们定义了大量的全局变量作为状态标志key_pressed,uart_rx_done,adc_ready… 然后在主循环的不同位置去检查这些标志。这些全局变量缺乏管理谁都可以修改很容易出现竞态条件虽然裸机是顺序执行但逻辑上的先后依赖可能导致错误调试起来也非常痛苦。代码逻辑分散在检查标志位和处理标志位的地方阅读时需要来回跳转这就是“面条式代码”。注意这里的“卡”和“乱”往往是相辅相成的。因为代码乱逻辑不清晰我们更倾向于用简单的delay和轮询来解决问题导致更“卡”因为“卡”我们为了赶时间或绕过问题又会写出更“乱”的临时性代码形成恶性循环。3. 回调函数嵌入式世界的“事件驱动”引擎回调函数是打破上述恶性循环的关键工具。它的核心思想是**“订阅-通知”** 或“好莱坞原则”Don‘t call us, we’ll call you。你上层应用告诉我底层驱动或模块当某个事件发生时该做什么注册一个函数指针然后你就不用管了事件发生时我自然会调用你注册的函数。3.1 回调函数的基本原理与实现在C语言中回调通过函数指针实现。函数指针是一个变量它存储的是函数的入口地址。3.1.1 定义一个回调函数类型为了代码清晰通常先用typedef定义一个函数指针类型。// 定义一个无返回值、无参数的回调函数类型 typedef void (*callback_t)(void); // 定义一个带参数的回调函数类型例如串口接收到数据 typedef void (*uart_rx_callback_t)(uint8_t received_data);callback_t就是一个类型它表示“一个指向无返回值、无参数函数的指针”。uart_rx_callback_t则表示“一个指向接收一个uint8_t参数且无返回值的函数的指针”。3.1.2 在模块中提供注册接口一个提供回调功能的模块需要有一个“注册”函数允许外部将回调函数传进来。// 在按键驱动模块 key.c 中 static callback_t key_pressed_callback NULL; // 静态全局变量存储回调函数指针 void key_register_pressed_callback(callback_t cb) { if (cb ! NULL) { key_pressed_callback cb; } }3.1.3 在事件触发处调用回调在模块内部当事件如按键按下发生时检查回调函数指针是否有效非NULL如果有效则调用它。// 在按键扫描函数中可能在定时器中断或主循环中被调用 void key_scan(void) { if (/* 检测到按键按下且消抖成功 */) { // ... 其他处理如清除标志 if (key_pressed_callback ! NULL) { key_pressed_callback(); // 关键通知外部“按键按下了你注册的处理函数该干活了” } } }3.1.4 上层应用注册回调在应用层如main.c你实现具体的处理函数并将其注册给底层模块。// 应用层实现的按键处理函数 static void my_key_handler(void) { LED_Toggle(); // 例如按键按下则翻转LED } int main(void) { // 初始化 key_init(); led_init(); // 注册回调告诉按键模块按下时请调用 my_key_handler key_register_pressed_callback(my_key_handler); while(1) { key_scan(); // 按键扫描内部会判断并触发回调 // 其他任务... } }通过这四步按键模块 (key.c) 和应用层 (main.c) 就解耦了。按键模块只负责“检测按下”这个事件完全不知道按下后具体要做什么。应用层则专注于“按下后做什么”这个业务逻辑。两者通过一个函数指针契约连接清晰且灵活。3.2 回调函数如何解决“卡”的问题回调函数通常与中断和状态机结合使用这是解决“卡”问题的黄金组合。3.2.1 中断 回调将阻塞变为异步以前用轮询等待串口接收一个字节// 阻塞式接收卡 uint8_t uart_receive_byte_blocking(void) { while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) RESET); // 死等 return USART_ReceiveData(USART1); }现在我们开启串口接收中断并在中断服务程序ISR中调用回调// 串口驱动模块 uart.c static uart_rx_callback_t rx_callback NULL; void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); if (rx_callback ! NULL) { rx_callback(data); // 收到数据立刻通知应用层 } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } void uart_register_rx_callback(uart_rx_callback_t cb) { rx_callback cb; }应用层注册一个数据处理函数。这样CPU完全不用等待串口数据数据一来中断发生回调函数被即时调用处理。主循环得以解放可以去处理其他任务系统响应性极大提升。3.2.2 定时器 回调实现精准的非阻塞延时我们可以利用硬件定时器中断实现一个软件定时器或滴答模块。该模块管理多个定时任务每个任务到期时执行其注册的回调函数。// 软件定时器模块 timer.c typedef struct { uint32_t timeout_ticks; uint32_t reload_ticks; callback_t cb; bool is_active; } soft_timer_t; soft_timer_t timer_list[MAX_TIMERS]; // 在系统滴答中断如SysTick中调用 void sys_tick_isr(void) { for(int i0; iMAX_TIMERS; i) { if(timer_list[i].is_active) { if(--timer_list[i].timeout_ticks 0) { if(timer_list[i].cb ! NULL) { timer_list[i].cb(); // 定时时间到触发回调 } timer_list[i].timeout_ticks timer_list[i].reload_ticks; // 重载如果是周期定时 } } } } // 应用层实现一个LED闪烁的任务 static void led_blink_task(void) { LED_Toggle(); } // 启动一个周期为500ms的定时器 timer_start(timer_list[0], 500, led_blink_task);现在LED闪烁不再需要delay_ms(500)来阻塞而是由定时器模块在后台默默计时到期后自动调用led_blink_task。主循环彻底空闲出来。3.3 回调函数如何解决“乱”的问题回调函数是实现模块化和低耦合设计的基石。3.3.1 清晰的接口契约模块对外只暴露有限的接口初始化函数、注册回调函数。例如一个温湿度传感器驱动sht3x.c可能只提供void sht3x_init(void); void sht3x_trigger_measurement(void); void sht3x_register_measurement_done_callback(sht3x_callback_t cb); // 测量完成回调 typedef void (*sht3x_callback_t)(float temperature, float humidity); // 回调带数据应用层不需要知道sht3x.c内部是如何通过I2C通信、如何校验数据的。它只需要知道触发测量然后等回调通知结果。这种“面向接口而非实现”的编程使得模块职责清晰边界明确。3.3.2 反转的控制流IoC传统模式下主循环高层模块调用各个子模块的函数控制权在高层。使用回调后变成了高层模块向底层模块注册回调底层模块在适当时机“反向”调用高层的函数控制权在底层/框架。这就是控制反转。它解除了高层对底层的直接依赖两者都依赖于抽象的回调接口。这使得底层模块可以独立开发、测试和复用。3.3.3 消灭全局状态标志很多之前需要用全局标志来传递的事件现在可以通过回调函数参数直接传递。例如串口接收完成标志uart_rx_done和对应的数据uart_rx_buffer可以取消。取而代之的是在串口接收中断中直接将收到的字节通过回调函数rx_callback(byte)传递给处理函数。数据流向是直接的、即时的避免了中间状态在不同模块间共享和管理的混乱。4. 从零开始用回调函数重构一个经典案例让我们用一个具体的例子将上述理论付诸实践。假设我们有一个经典需求开发板上有两个按键KEY1, KEY2和一个LED。要求按下KEY1LED亮按下KEY2LED灭。同时需要通过串口打印按键事件。4.1 重构前典型的“又卡又乱”代码// main.c (重构前) #include stm32f1xx.h #include delay.h #include usart.h #define KEY1_PIN GPIO_Pin_0 #define KEY2_PIN GPIO_Pin_1 #define LED_PIN GPIO_Pin_2 uint8_t key1_pressed 0; uint8_t key2_pressed 0; void GPIO_Init(void) { // ... 初始化GPIOKEY1/KEY2为上拉输入LED为推挽输出 } int main(void) { delay_init(); USART1_Init(115200); GPIO_Init(); printf(System Start\r\n); while(1) { // 按键扫描阻塞消抖 if(GPIO_ReadInputDataBit(GPIOA, KEY1_PIN) 0) { // 按下为低电平 delay_ms(20); // 卡住20ms if(GPIO_ReadInputDataBit(GPIOA, KEY1_PIN) 0) { key1_pressed 1; } } // 同样的代码扫描KEY2这里省略... // 处理按键事件 if(key1_pressed) { GPIO_SetBits(GPIOC, LED_PIN); // LED亮 printf(KEY1 Pressed, LED ON\r\n); key1_pressed 0; // 如果此时串口发送慢这里可能又会卡住 } if(key2_pressed) { GPIO_ResetBits(GPIOC, LED_PIN); // LED灭 printf(KEY2 Pressed, LED OFF\r\n); key2_pressed 0; } // 如果还想做其他事情代码会越来越臃肿... } }问题分析卡delay_ms(20)用于按键消抖这20ms内CPU完全停滞。如果按键长按或者多个按键卡顿更严重。printf内部也可能因为等待发送完成而阻塞。乱按键检测、消抖、事件处理、LED控制、串口打印全部揉在main函数里。全局标志keyX_pressed增加了状态管理的复杂度。想加个双击功能代码会变得难以维护。4.2 重构后基于回调的清晰架构我们将系统拆分为几个模块按键驱动 (key.c/.h)、LED驱动 (led.c/.h)、串口打印模块 (debug.c/.h)以及应用层 (app.c/.h)。4.2.1 按键驱动模块 (key.h / key.c)// key.h #ifndef __KEY_H #define __KEY_H #include stdint.h // 定义按键编号 typedef enum { KEY_ID_1 0, KEY_ID_2, KEY_NUM } key_id_t; // 定义按键事件类型 typedef enum { KEY_EVENT_PRESSED 0, // 按下 KEY_EVENT_RELEASED, // 释放 KEY_EVENT_LONG_PRESSED, // 长按可以扩展 } key_event_t; // 定义回调函数类型当某个按键发生某个事件时被调用 typedef void (*key_callback_t)(key_id_t id, key_event_t event); // 模块接口 void key_init(void); void key_register_callback(key_id_t id, key_callback_t cb); void key_scan_task(void); // 需要在主循环或定时器中断中周期性调用 #endif// key.c #include key.h #include stm32f1xx.h #include timer.h // 假设我们有一个提供系统tick的非阻塞定时器模块 static key_callback_t callbacks[KEY_NUM] {NULL, NULL}; static uint32_t key_press_start_ticks[KEY_NUM] {0}; static uint8_t key_last_state[KEY_NUM] {1}; // 默认上拉为高 void key_init(void) { // 初始化对应GPIO为上拉输入... } void key_register_callback(key_id_t id, key_callback_t cb) { if (id KEY_NUM) { callbacks[id] cb; } } void key_scan_task(void) { const uint32_t DEBOUNCE_TICKS 20; // 20ms消抖时间基于系统tick static uint32_t key_scan_timer 0; uint32_t current_tick get_system_tick(); // 获取当前系统tick // 每5ms扫描一次按键非阻塞 if (current_tick - key_scan_timer 5) { return; } key_scan_timer current_tick; for (int i 0; i KEY_NUM; i) { uint8_t current_state GPIO_ReadInputDataBit(KEY_GPIO_PORT, key_pins[i]); // 状态变化检测 if (current_state ! key_last_state[i]) { // 更新状态记录 key_last_state[i] current_state; // 如果是按下从高到低 if (current_state 0) { key_press_start_ticks[i] current_tick; // 注意这里不立即触发回调等待消抖稳定 } else { // 释放从低到高 // 释放时如果按下时间大于消抖时间则认为是有效的“按下-释放”事件 if (current_tick - key_press_start_ticks[i] DEBOUNCE_TICKS) { if (callbacks[i] ! NULL) { callbacks[i](i, KEY_EVENT_PRESSED); // 触发按下事件回调 // 如果需要也可以在这里触发一个释放事件回调 // callbacks[i](i, KEY_EVENT_RELEASED); } } } } // 长按检测示例 if (current_state 0) { if ((current_tick - key_press_start_ticks[i] 1000) (callbacks[i] ! NULL)) { // 长按1秒触发 callbacks[i](i, KEY_EVENT_LONG_PRESSED); key_press_start_ticks[i] current_tick; // 重置避免连续触发 } } } }这个按键驱动模块的特点非阻塞消抖利用系统tick计时key_scan_task函数执行极快不会卡住主循环。事件抽象定义了标准的按键事件按下、释放、长按。回调注册为每个按键预留了回调函数指针应用层可以灵活注册。4.2.2 应用层模块 (app.c / app.h)// app.h void app_init(void); void app_task(void); // 主循环任务// app.c #include app.h #include key.h #include led.h #include debug.h // 串口打印封装 // 按键1的事件处理回调 static void on_key1_action(key_id_t id, key_event_t event) { if (event KEY_EVENT_PRESSED) { led_set_state(LED_STATE_ON); debug_printf(KEY1 Pressed, LED ON\r\n); } } // 按键2的事件处理回调 static void on_key2_action(key_id_t id, key_event_t event) { if (event KEY_EVENT_PRESSED) { led_set_state(LED_STATE_OFF); debug_printf(KEY2 Pressed, LED OFF\r\n); } } void app_init(void) { key_init(); led_init(); debug_init(); // 注册回调将具体的处理逻辑绑定到硬件事件上 key_register_callback(KEY_ID_1, on_key1_action); key_register_callback(KEY_ID_2, on_key2_action); debug_printf(System Start (Callback Version)\r\n); } void app_task(void) { // 主循环里只需要调用各个模块的任务函数 key_scan_task(); // 非阻塞按键扫描 // 其他任务如传感器数据采集任务、显示刷新任务等 // ... }4.2.3 主函数 (main.c)#include app.h int main(void) { // 系统时钟等初始化 system_init(); // 应用初始化注册所有回调 app_init(); while(1) { // 主循环变得极其简洁和清晰 app_task(); // 执行应用任务 // 这里可以加入低功耗睡眠指令当所有任务都是事件驱动时CPU大部分时间可休眠 // __WFI(); } }4.3 重构前后对比与优势总结特性重构前传统轮询重构后回调事件驱动主循环复杂度高塞满各种检测和处理逻辑极低只有模块任务调度响应性差受delay和长任务阻塞好基于中断和定时tick近乎实时代码耦合度高硬件操作与业务逻辑紧耦合低模块通过清晰接口通信可维护性差修改一个功能可能影响全局好功能模块化修改局部化可扩展性差添加新功能需修改主循环好添加新模块或回调即可CPU利用率低大量时间在空转或阻塞高无谓等待少可进入休眠省电通过这个案例你可以清晰地看到回调函数如何将一团乱麻的代码梳理成职责清晰的模块。主循环从“上帝”变成了“调度员”它只负责协调各个模块调用它们的xxx_task函数而具体的“做什么”则分散到了各个模块和应用层的回调函数中。这种结构正是许多小型RTOS实时操作系统内核设计思想的雏形。5. 进阶技巧与避坑指南掌握了基本用法我们再来探讨一些进阶技巧和实践中容易踩的坑。5.1 带参数与上下文如何传递更多信息基本的回调函数可能只通知“事件发生了”。但很多时候我们需要传递事件相关的数据。这可以通过定义带参数的回调类型来实现如前文的uart_rx_callback_t(uint8_t data)。更复杂的情况是回调函数可能需要访问它所属模块的上下文比如一个结构体实例。在C语言中这通常通过一个称为“回调上下文”callback context或“用户参数”user argument的指针来实现。5.1.1 使用void*传递上下文// 定义带上下文参数的回调类型 typedef void (*sensor_data_callback_t)(float data, void *context); // 在模块注册接口中增加上下文参数 void sensor_register_callback(sensor_data_callback_t cb, void *context); // 在模块触发回调时传递上下文 static sensor_data_callback_t user_callback NULL; static void *user_context NULL; void sensor_measurement_done_isr(float result) { if (user_callback ! NULL) { user_callback(result, user_context); // 将数据和上下文一起传给应用层 } } // 应用层使用 typedef struct { uint8_t id; char name[10]; } my_device_t; my_device_t dev1 {1, Sensor1}; static void my_sensor_handler(float data, void *ctx) { my_device_t *my_dev (my_device_t *)ctx; // 将void*转换回具体类型 printf(Device %s(%d) got data: %.2f\r\n, my_dev-name, my_dev-id, data); } int main() { sensor_register_callback(my_sensor_handler, (void*)dev1); // 注册时传入设备上下文 }这种方式允许同一个回调函数处理多个不同实例如多个同类型传感器的数据只需在注册时传入不同的上下文指针即可极大地增加了灵活性。5.2 回调函数的安全性与注意事项回调函数虽好但使用不当也会引入问题。5.2.1 空指针检查这是最基本也是最重要的一条。在调用回调函数指针前必须检查其是否为NULL。if (callback ! NULL) { callback(); }如果忘记检查一旦回调指针未初始化或被错误置为NULL程序将跳转到非法地址执行导致硬件错误HardFault系统崩溃。这种错误在单片机调试中非常隐蔽。5.2.2 中断服务程序ISR中的回调在ISR中调用回调函数是常见做法如串口接收中断但必须注意回调函数必须简短高效。ISR应该快进快出长时间执行会阻塞其他中断影响系统实时性。避免在ISR回调中进行耗时的操作如复杂的浮点运算、printf可能阻塞、或等待其他硬件信号。如果回调函数需要操作共享资源如队列、缓冲区、全局变量而该资源也可能在主循环中被访问则需要考虑临界区保护。对于简单的8位/16位单片机如果操作是原子的一条指令完成可能没问题。但对于非原子操作如32位变量在8位机上操作或者操作数据结构就需要暂时关闭中断或使用信号量如果有RTOS。5.2.3 回调函数的执行时间即使不在中断中也要注意回调函数的执行时间。如果一个回调函数执行时间过长它会阻塞注册该回调的模块任务的执行。例如如果在定时器任务循环中处理多个软件定时器的回调其中一个回调执行了100ms那么其他定时器回调都会被延迟。在设计时应将耗时任务拆解或者将耗时操作放到主循环中回调函数只负责设置标志或向队列投递消息。5.2.4 避免在回调中调用可能阻塞的函数例如在某个按键回调函数中调用了一个需要等待I2C应答的函数里面用了while轮询。这会导致整个系统在等待I2C时失去响应。正确的做法是使用基于中断和状态机的非阻塞I2C驱动并在回调中只触发I2C操作在I2C操作完成的中断回调中再进行后续处理。5.3 从裸机回调到RTOS的桥梁消息队列当系统复杂度继续上升单纯的回调可能不够用。例如多个中断源快速产生事件或者回调函数需要执行较复杂的任务。这时可以引入“消息队列”作为缓冲。思路是中断服务程序ISR或底层驱动不再直接调用应用层回调而是将一个“消息”包含事件类型、数据等放入一个队列。应用层的主循环或一个专用的任务不断地从队列中取出消息并分发给对应的处理函数。// 一个简化的消息队列示例 typedef struct { event_type_t type; union { uint8_t u8_data; float float_data; // ... 其他数据 } value; } event_msg_t; // 在串口中断中 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); event_msg_t msg; msg.type EVENT_UART_RX; msg.value.u8_data data; queue_push(uart_rx_queue, msg); // 将消息放入队列而非直接调用回调 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } // 在主循环中 void app_task(void) { event_msg_t msg; if(queue_pop(uart_rx_queue, msg, 0)) { // 非阻塞取消息 switch(msg.type) { case EVENT_UART_RX: handle_uart_data(msg.value.u8_data); // 在这里处理相当于回调 break; // ... 处理其他事件 } } // ... 处理其他任务 }这种方式解耦了事件产生和事件处理使得ISR更短处理函数可以在主循环的合适时机被执行系统调度更灵活。这其实就是许多RTOS中任务间通信机制如队列、邮箱的雏形。当你觉得裸机回调模式捉襟见肘时就是考虑引入RTOS的好时机了。6. 总结与个人心得回调函数不是万能的但对于改善单片机尤其是裸机程序的结构和响应能力它是一剂立竿见影的良药。它强迫你将代码模块化思考模块间的接口从而写出更清晰、更易维护的代码。从我个人的经验来看从“又卡又乱”到“清晰流畅”的转变关键在于思维模式的转变从“我该怎么做”的过程式思维转变为“当XX发生时需要做什么”的事件驱动思维。回调函数是这种思维在代码上的直接体现。开始实践时可以从一个小模块开始比如先把按键处理改成回调方式。你会立刻感受到主循环变得清爽。然后逐步将串口、定时器、传感器等都进行改造。在这个过程中你可能会遇到一些挑战比如如何管理多个回调、如何安全地传递数据但这都是成长的必经之路。最终你会发现你的代码库变成了一个由许多独立、可测试、可复用的模块组成的生态系统而不是一个庞大的、脆弱的“巨石”应用。最后一个小建议在定义回调接口时多花点时间设计。一个好的回调接口应该是意图明确、参数合理的。例如on_temperature_changed(float new_temp)就比一个泛泛的sensor_callback()要好得多。清晰的接口是良好协作的开始无论是人与人还是模块与模块之间。