用函数指针表替代大段 switch-case:嵌入式命令分发的常用技巧

嵌入式里经常要根据一个索引或命令码执行不同操作,最直觉的写法是 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 更直观
状态机转移函数 各分支逻辑差异极大,强行统一签名反而别扭
硬件抽象/后端切换  

核心思路:把”判断做什么”和”怎么做”分开。判断逻辑固定在表查找,扩展只需往表里加条目。

请登录后发表评论

    没有回复内容