如遇开发技术问题,欢迎前往开发者社区,极海技术团队将在线为您解答~
极海官方开发者社区​https://community.geehy.cn/

《APM32芯得》系列内容为用户使用APM32系列产品的经验总结,均转载自21ic论坛极海半导体专区,全文未作任何修改,未经原文作者授权禁止转载。

目录

前言

1. 什么是表驱动法?

1.1 一个简单的比喻

2. 为什么要有表驱动法?

3. 表驱动法有啥好处?

4. 状态机简单介绍

4.1 什么是状态机?

4.2 嵌入式里为什么用状态机?

5. 表驱动法和状态机怎么结合?

5.1 基本步骤

6. 用APM32F407写个例程:LED闪烁状态机

6.1 硬件准备

6.2 功能目标

6.3 设计思路

7. 例程代码

8. 例程怎么工作的?

实验现象

9. 表驱动法的妙处在这儿

10. 疑问:如果后面要在这份代码上加一个功能,好加吗?

10.1 为什么好加?

10.2 举个例子:加一个“呼吸灯”状态

总结

附件


前言

大家好,今天我们要聊聊嵌入式C语言开发里一个特别有用的技术——表驱动法(Table-Driven Programming)。别看名字有点高大上,其实它就是一个简单又高效的编程思路,尤其在像APM32F407这样的MCU(微控制器)上特别好使。这篇文章我会用大白话把这玩意儿讲清楚,从它是什么、为什么要有它、有什么好处,到最后怎么用它写个简单的例程,所以我会尽量细致,把每个点都掰开了揉碎了讲。

1. 什么是表驱动法?

表驱动法,简单来说,就是一种“查表做事”的编程方法。想象一下,你去饭店吃饭,菜单上列好了菜名和价格,你不用跟服务员一个个问“这个菜多少钱,那个菜怎么做”,直接看菜单就知道答案。表驱动法也是这个道理:它把程序里的一些规则、逻辑或者行为提前写在一个表格里(在C语言里通常是数组或者结构体数组),程序运行的时候,根据输入或者当前情况去这个表格里找对应的答案,然后执行。

传统的编程方式可能是用一堆if-else或者switch-case来判断条件,比如“如果输入是A就干啥,如果是B就干啥”,但条件一多,代码就变得又长又乱。而表驱动法呢,就像是把这些条件和结果整理成一张表格,程序只需要“查表”就行了,不用写那么多判断语句。

1.1 一个简单的比喻

假设你要做一个计算器,能算加减乘除。传统的办法可能是:

if (操作 == '+') {
    结果 = a + b;
} else if (操作 == '-') {
    结果 = a - b;
} else if (操作 == '*') {
    结果 = a * b;
} else if (操作 == '/') {
    结果 = a / b;
}

但用表驱动法,你可以先建个表格:

程序运行时,拿到操作符(比如+),直接去表格里找对应的函数来执行,代码就简洁多了。

2. 为什么要有表驱动法?

在嵌入式开发里,表驱动法可不是随便搞出来的,它是为了解决实际问题而生的。咱们得从嵌入式系统的特点说起:

  1. 资源有限:像APM32F407这样的MCU,内存(RAM)和闪存(Flash)都很小,CPU算力也不强。如果代码写得太复杂,占用的空间和计算时间都会增加,可能直接跑不动。
  2. 逻辑复杂:嵌入式系统经常要处理各种状态和条件,比如控制一个设备,可能有“开”“关”“闪烁”好几种模式,还要根据按键、传感器输入来切换,条件一多,传统的if-else就容易写成一团乱麻。
  3. 维护麻烦:开发嵌入式程序时,需求经常会变。比如客户今天说“加个新功能”,明天说“改下逻辑”,如果代码里全是if-else,每次改都得翻遍整个程序,容易出错。

表驱动法就像是一个“聪明管家”,它把这些乱七八糟的逻辑整理成一张表格,程序只需要照着表格做事,既省力又不容易出错。

3. 表驱动法有啥好处?

说了半天,表驱动法到底好在哪儿?咱们一条条来看:

  1. 代码简洁
    用表格代替一堆条件判断,代码量直接少一大截。你看表格一眼就知道每个情况该干啥,不用在长长的if-else里找来找去。
  2. 易于维护
    如果要改逻辑或者加新功能,只需要在表格里改几行数据就行了,程序的核心代码几乎不用动。比如你要加个新状态,直接在表格里加一行,不需要重新写一堆判断。
  3. 执行效率高
    在MCU上,查表通常比跑一堆if-else快。为什么?因为查表就是从内存里取数据,速度固定,而条件判断得一条条比对,条件越多越慢。
  4. 可扩展性好
    系统升级或者功能增加时,表格可以轻松扩展,不用担心代码结构崩掉。比如你原来有3个状态,后来要加到10个,表格多加几行就搞定。

总结一下,表驱动法就像是给程序装了个“导航仪”,告诉它“别瞎猜了,直接照着地图走”,既快又准。

4. 状态机简单介绍

在讲怎么用表驱动法之前,咱们先聊聊状态机(State Machine),因为后面例程里会用到它。别被名字吓到,状态机其实是个很直白的东西,尤其在嵌入式开发里特别常见。

4.1 什么是状态机?

状态机就是一个描述“事物当前状态和怎么变”的模型。举个生活里的例子:你家的灯有几种状态——“关”“开”“闪烁”,你按一下开关,它就从“关”变成“开”,再按一下变成“闪烁”,再按又变回“关”。这个过程就是状态机在工作。

在程序里,状态机通常有这几个部分:

  • 状态(States):系统当前是什么情况,比如“关”“开”。
  • 事件(Events):触发状态变化的东西,比如“按开关”。
  • 动作(Actions):状态变的时候要干啥,比如“点亮灯”。
4.2 嵌入式里为什么用状态机?

嵌入式系统经常要控制东西,比如LED、电机、显示屏,这些东西都有不同的工作模式(状态),而且会根据输入(比如按键、传感器)来切换模式。用状态机来写代码,能让逻辑清晰,像画流程图一样简单。

5. 表驱动法和状态机怎么结合?

在嵌入式C语言里,表驱动法状态机简直是“天生一对”。咱们可以用表格来记录状态机的所有规则,程序运行时根据当前状态和事件去查表,找到下一步该干啥。

5.1 基本步骤
  1. 列出状态和事件:先搞清楚系统有几种状态,可能发生什么事件。比如LED有“关”“开”“慢闪”“快闪”4种状态,事件有“按键按下”“定时器到时”。
  2. 建个表格:把每个状态和事件的组合写成表格,标明“遇到这个事件会变成啥状态,要干啥事”。这张表就是状态机的“说明书”。
  3. 程序查表:程序跑的时候,看看当前状态是什么,发生了啥事件,然后去表格里找对应的行,执行动作,切换状态。

这种方法把复杂的逻辑变成了“查字典”,简单又高效。

6. 用APM32F407写个例程:LED闪烁状态机

好了,理论讲了不少,咱们来点实际的。用APM32F407这个MCU写一个简单的例程:控制一个LED,通过按键切换它的闪烁模式(常灭、常亮、慢闪、快闪),用表驱动法结合状态机来实现。

6.1 硬件准备
  • MCU: APM32F407。
  • LED: 板载LED。
  • 按键: 板载KEY。
6.2 功能目标
  • 默认状态:LED常灭。
  • 按一下按键:变成慢闪(每秒闪一次)。
  • 再按一下:变成快闪(每0.2秒闪一次)。
  • 再按一下:变成常亮。
  • 再按一下:回到常灭。
  • 循环往复。
6.3 设计思路

1. 定义状态和事件
状态

  • STATE_OFF:LED常灭
  • STATE_ON:LED常亮
  • STATE_SLOW_BLINK:LED慢闪
  • STATE_FAST_BLINK:LED快闪
    事件
  • EVENT_KEY_PRESS:按键按下
  • EVENT_TIMER:定时器到时(控制闪烁)

2. 设计状态转换规则
咱们用文字先把规则写出来:

  • 常灭时
    • 按键按下 → 变成慢闪,LED先关掉。
    • 定时器到时 → 啥也不干,还是常灭。
  • 慢闪时
    • 按键按下 → 变成快闪,不用动LED。
    • 定时器到时 → 翻转LED状态(亮变灭,灭变亮)。
  • 快闪时
    • 按键按下 → 变成常亮,LED点亮。
    • 定时器到时 → 翻转LED状态。
  • 常亮时
    • 按键按下 → 变成常灭,LED关闭。
    • 定时器到时 → 啥也不干,还是常亮。

3. 用表格表示
咱们把这些规则整理成一个表格:

这张表就是咱们的“状态机说明书”,程序会根据它来做事。

4. 怎么写成代码?
在C语言里,咱们用结构体数组来实现这个表格。每个结构体记录一条规则,包括:

  • 当前状态
  • 事件
  • 下个状态
  • 要执行的动作(用函数指针表示)

程序运行时,拿到当前状态和事件后,遍历这个数组,找到匹配的那一行,执行动作,更新状态。

7. 例程代码
/* Includes */
#include "main.h"
#include "Board.h"
#include "stdio.h"
#include "apm32f4xx_gpio.h"
#include "apm32f4xx_adc.h"
#include "apm32f4xx_misc.h"
#include "apm32f4xx_usart.h"
#include "apm32f4xx_tmr.h"

// 定义状态
typedef enum
{
    STATE_OFF,          // 常灭
    STATE_ON,           // 常亮
    STATE_SLOW_BLINK,   // 慢闪
    STATE_FAST_BLINK    // 快闪
} State;

// 定义事件
typedef enum
{
    EVENT_KEY_PRESS,    // 按键按下
    EVENT_TIMER         // 定时器到时
} Event;

// 定义状态转换结构体
typedef struct
{
    State current_state;    // 当前状态
    Event event;            // 事件
    State next_state;       // 下个状态
    void (*action)(void);   // 动作函数指针
} Transition;

// 动作函数
void turn_off_led(void) { APM_TINY_LEDOff(LED2); }
void turn_on_led(void) { APM_TINY_LEDOn(LED2); }
void toggle_led(void) { APM_TINY_LEDToggle(LED2); }

// 状态转换表
Transition state_table[] =
{
    {STATE_OFF,        EVENT_KEY_PRESS, STATE_SLOW_BLINK, turn_off_led},
    {STATE_OFF,        EVENT_TIMER,     STATE_OFF,        NULL},
    {STATE_SLOW_BLINK, EVENT_KEY_PRESS, STATE_FAST_BLINK, NULL},
    {STATE_SLOW_BLINK, EVENT_TIMER,     STATE_SLOW_BLINK, toggle_led},
    {STATE_FAST_BLINK, EVENT_KEY_PRESS, STATE_ON,         turn_on_led},
    {STATE_FAST_BLINK, EVENT_TIMER,     STATE_FAST_BLINK, toggle_led},
    {STATE_ON,         EVENT_KEY_PRESS, STATE_OFF,        turn_off_led},
    {STATE_ON,         EVENT_TIMER,     STATE_ON,         NULL}
};

// 当前状态
State current_state = STATE_OFF;
volatile uint32_t timer_counter = 0;
volatile uint32_t timer_threshold = 1000;

// 处理事件的函数
void process_event(Event event)
{
    int table_size = sizeof(state_table) / sizeof(Transition);
    for (int i = 0; i < table_size; i++)
    {
        if (state_table[i].current_state == current_state && state_table[i].event == event)
        {
            if (state_table[i].action != NULL)
            {
                state_table[i].action();
            }
            current_state = state_table[i].next_state;
            if (current_state == STATE_SLOW_BLINK)
            {
                timer_threshold = 1000;  // 1秒
            }
            else if (current_state == STATE_FAST_BLINK)
            {
                timer_threshold = 200;   // 0.2秒
            }
            return;
        }
    }
}

// 主函数
int main(void)
{
    // ... 初始化代码 ...
    APM_TINY_LEDInit(LED2);
    APM_TINY_PBInit(BUTTON_KEY1, BUTTON_MODE_GPIO);
    // ... 定时器和串口初始化 ...

    while (1)
    {
        // 检测按键
        if (APM_TINY_PBGetState(BUTTON_KEY1) == BIT_RESET)
        {
            process_event(EVENT_KEY_PRESS);
            delay_ms(50);  // 防抖
            while (APM_TINY_PBGetState(BUTTON_KEY1) == BIT_RESET);  // 等待松开
        }
    }
}

// 定时器中断处理函数
void TMR2_IRQHandler(void)
{
    if (TMR_ReadIntFlag(TMR2, TMR_INT_UPDATE) != RESET)
    {
        TMR_ClearIntFlag(TMR2, TMR_INT_UPDATE);
        timer_counter++;
        if (timer_counter >= timer_threshold)
        {
            timer_counter = 0;
            process_event(EVENT_TIMER);
        }
    }
}

// ... 其他辅助函数 ...
8. 例程怎么工作的?

8.1 启动
程序开始时,LED是常灭状态(STATE_OFF)。

8.2 按键

  • 第一次按:查表,从STATE_OFF跳到STATE_SLOW_BLINK,LED开始慢闪。
  • 第二次按:跳到STATE_FAST_BLINK,LED快闪。
  • 第三次按:跳到STATE_ON,LED常亮。
  • 第四次按:回到STATE_OFF,LED常灭。

8.3 定时器
在慢闪和快闪状态下,每次定时器到时就翻转LED,其他状态无动作。

实验现象

https://v.youku.com/v_show/id_XNjQ2MzgxNzc4OA==.html?spm=a1z3jc.11711052.0.0&isextonly=1

9. 表驱动法的妙处在这儿

你看这个例程:

  • 逻辑全在表里:状态转换和动作都写在state_table里,代码里没一堆if-else
  • 改起来方便:想加个“超快闪”状态?在表里加几行就行,不用动主逻辑。
  • 效率高:查表比判断快,MCU跑起来不费劲。
10. 疑问:如果后面要在这份代码上加一个功能,好加吗?

我这份代码是用表驱动法状态机设计的,所以扩展性很强,加新功能是相对容易的。下面我详细跟你说说为什么好加,以及具体怎么加,拿“呼吸灯”状态举个例子。

10.1 为什么好加?

我的代码用的是表驱动法状态机,这俩组合起来就像搭积木,结构清晰,扩展方便。

  • 状态机把程序分成一个个独立的状态。
  • 表驱动法把状态之间的转换和动作都写在一个表格里,想加新功能,只要往表格里加几行就行。

这种设计的好处是,代码的可维护性扩展性特别高。

10.2 举个例子:加一个“呼吸灯”状态

1. 定义新状态
State枚举里加一个STATE_BREATH

typedef enum {
    // ... 其他状态 ...
    STATE_BREATH      // 呼吸灯,新加的状态
} State;

2. 定义新动作
“呼吸灯”效果需要用PWM实现。先写一个动作函数breath_led来调整亮度。

void breath_led(void) {
    // 此处应为PWM调光代码
    // 为演示,这里仅作示意
}

3. 更新状态转换表
state_table里加入新规则。比如从STATE_ON按键跳到STATE_BREATH,再按一下回到STATE_OFF

Transition state_table[] = {
    // ... 原来的规则 ...
    {STATE_ON,         EVENT_KEY_PRESS, STATE_BREATH,     breath_led},   // 新增
    {STATE_BREATH,     EVENT_KEY_PRESS, STATE_OFF,        turn_off_led}, // 新增
    // ... 定时器事件 ...
    {STATE_BREATH,     EVENT_TIMER,     STATE_BREATH,     breath_led}    // 新增
};

你看,加一个新功能主要就是加状态加动作加规则这三步,核心的process_event函数完全不用改。

注意:动作函数的执行时间不宜过长,避免阻塞主循环。复杂的动作(如呼吸灯)最好在定时器中断里分步执行。

总结

表驱动法是个超级实用的技术,尤其在嵌入式C语言开发里。它通过把逻辑变成表格数据,让代码更简洁好维护跑得快。在状态机里用表驱动法,简直是如虎添翼,能把复杂的控制逻辑整理得井井有条。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐