嵌入式C语言回调函数架构设计:告别硬编码,让外设驱动可灵活复用
裸机或RTOS项目里,最容易出现“驱动调业务”的硬耦合:UART中断里直接ParseModbus、按键扫描里直接ControlRelay、定时器回调里直接BlinkLED。换协议、换板卡就要改驱动源文件,维护成本陡增。用函数指针做回调注册,把“事件发生”和“事件处理”拆开,驱动只负责收数据、置标志、调指针,应用层注册具体函数,复用性会明显提升。
一、定义统一回调类型
按外设事件抽象参数,避免每个驱动写一套怪异签名:
typedef enum {
EVT_UART_RX,
EVT_GPIO_FALL,
EVT_TIMER_PERIOD
} drv_event_t;
typedef void (*drv_callback_t)(drv_event_t evt,
uint8_t inst,
void *data,
uint16_t len,
void *ctx);
inst区分多实例端口/引脚,ctx传应用私有对象,避免用全局变量;data/len用于UART、DMA等带载荷事件。
二、驱动内部用注册表管理多实例
以UART为例,不在中断里写业务,只存回调并转发:
#define UART_MAX 3
static struct {
drv_callback_t cb;
void *ctx;
uint8_t buf[128];
uint16_t len;
} uart_slot[UART_MAX];
void uart_register(uint8_t port, drv_callback_t cb, void *ctx){
if(port < UART_MAX){
uart_slot[port].cb = cb;
uart_slot[port].ctx = ctx;
}
}
static void uart_irq(uint8_t port){
/* 硬件收数据入buf,收完一帧后 */
struct uart_slot *s = &uart_slot[port];
if(s->cb)
s->cb(EVT_UART_RX, port, s->buf, s->len, s->ctx);
}
应用层初始化时注册不同协议解析函数,驱动文件不动;同一份UART驱动可同时接Modbus、AT、私有协议。
三、GPIO与定时器同样解耦
按键/外部中断不直控业务,用边沿回调:
typedef void (*gpio_cb_t)(uint8_t pin, void *ctx);
static gpio_cb_t gpiocb[16];
void gpio_register(uint8_t pin, gpio_cb_t cb){
if(pin < 16) gpiocb[pin] = cb;
}
void EXTI_IRQHandler(void){
uint8_t pin = get_pending_pin();
if(gpiocb[pin]) gpiocb[pin](pin, NULL);
}
定时器周期事件可用函数指针数组做轻量调度:
typedef void (*timer_cb_t)(void);
timer_cb_t timer_tbl[4];
void timer_period_isr(void){
for(int i=0;i<4;i++)
if(timer_tbl[i]) timer_tbl[i]();
}
CMSIS驱动体系也采用类似思路:Initialize时注册SignalEvent回调,中断只发事件、不处理业务。
四、中断上下文:短回调或事件入队
ISR里直接调复杂业务会阻塞其他中断。两点原则:
高速、低延迟场景可在ISR直接调回调,但只做置标志、入环形缓冲、发轻量通知;
解析、打印、写Flash等重活抛到主循环/RTOS任务。配合事件队列:
typedef struct { drv_event_t evt; uint8_t inst; void *ctx; } eq_t;
eq_t eq[16]; volatile uint8_t qh,qt;
void isr_post(drv_event_t e, uint8_t i, void *c){
eq[qh].evt=e; eq[qh].inst=i; eq[qh].ctx=c;
qh=(qh+1)%16;
}
主循环出队后调用应用回调,既解耦又保实时。
五、避坑要点
所有函数指针初始化为NULL,调用前判空,防止未注册跑飞;
多实例别用单全局回调,用数组/句柄结构体;
回调签名全程统一,避免强制转换导致栈破坏;
RTOS中回调若操作队列/信号量,确认所用API可在ISR调用;
注销设备时把槽位清NULL,防止悬空指针。
把“驱动只发事件、应用注册处理”作为规矩,外设驱动就能跨项目原样拷贝:换协议换回调,换板子换底层寄存器,业务层几乎不动。





