嵌入式里经常要根据一个索引或命令码执行不同操作,最直觉的写法是 switch-case:
void handle_cmd(uint8_t cmd) {
switch (cmd) {
case CMD_START: do_start(); break;
case CMD_STOP: do_stop(); break;
case CMD_RESET: do_reset(); break;
case CMD_STATUS: do_status(); break;
// ...越写越长
}
}
命令少的时候没问题。命令一多,这个函数就变成几百行的”垃圾桶”,每次新增都要改同一个地方,容易漏写 break,也难以单独测试某一个处理函数。
函数指针表
把每个命令对应的处理函数收进一个数组,用命令码直接索引:
typedef void (*cmd_handler_t)(void);
static const cmd_handler_t cmd_table[] = {
[CMD_START] = do_start,
[CMD_STOP] = do_stop,
[CMD_RESET] = do_reset,
[CMD_STATUS] = do_status,
};
void handle_cmd(uint8_t cmd) {
if (cmd < ARRAY_SIZE(cmd_table) && cmd_table[cmd]) {
cmd_table[cmd]();
}
}
分发逻辑永远只有两行。新增命令只在表里加一行,不碰分发函数本身。
这里用到了 C99 的指定初始化语法([CMD_START] = do_start),未显式指定的槽位自动为 NULL,所以 cmd_table[cmd] 的非空检查能正确拦截未定义的命令码。
带参数和返回值
实际场景里处理函数往往需要参数,统一函数签名即可:
typedef esp_err_t (*param_handler_t)(uint8_t ch, uint32_t val);
static const param_handler_t ops[] = {
[OP_COUPLING] = set_coupling,
[OP_PROBE] = set_probe,
[OP_VOLT_SCALE] = set_volt_scale,
};
esp_err_t apply_op(uint8_t op, uint8_t ch, uint32_t val) {
if (op >= ARRAY_SIZE(ops) || !ops[op]) {
return ESP_ERR_INVALID_ARG;
}
return ops[op](ch, val);
}
进阶:硬件抽象层
函数指针表更大的用途是解耦。把一组相关操作打包成一张表,上层只依赖这张表的接口,不依赖具体实现:
typedef struct {
esp_err_t (*set_ch_enable)(uint8_t ch, bool en);
esp_err_t (*set_timebase)(int16_t idx);
esp_err_t (*set_trig_level)(int16_t mv);
// ...
} hw_ops_t;
// 注册真实硬件后端
ui_hw_ops_init(&fpga_hw_ops);
// 注册 PC 模拟器后端
ui_hw_ops_init(&sim_hw_ops);
UI 层调用完全相同的代码,换一行初始化就能在真实硬件和 PC 模拟器之间切换——这个工程就是这样做的。
适合 vs 不适合
| 适合 | 不适合 |
|---|---|
| 命令/事件分发 | 条件复杂(多变量组合判断) |
| 协议帧处理 | 分支极少(2-3个),switch 更直观 |
| 状态机转移函数 | 各分支逻辑差异极大,强行统一签名反而别扭 |
| 硬件抽象/后端切换 |
核心思路:把”判断做什么”和”怎么做”分开。判断逻辑固定在表查找,扩展只需往表里加条目。


没有回复内容