- 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. 用表格表示
咱们把这些规则整理成一个表格:
当前状态
| 事件
| 下个状态
| 动作
|
STATE_OFF
| EVENT_KEY_PRESS
| STATE_SLOW_BLINK
| 关LED
|
STATE_OFF
| EVENT_TIMER
| STATE_OFF
| 无
|
STATE_SLOW_BLINK
| EVENT_KEY_PRESS
| STATE_FAST_BLINK
| 无
|
STATE_SLOW_BLINK
| EVENT_TIMER
| STATE_SLOW_BLINK
| 翻转LED
|
STATE_FAST_BLINK
| EVENT_KEY_PRESS
| STATE_ON
| 点亮LED
|
STATE_FAST_BLINK
| EVENT_TIMER
| STATE_FAST_BLINK
| 翻转LED
|
STATE_ON
| EVENT_KEY_PRESS
| STATE_OFF
| 关LED
|
STATE_ON
| EVENT_TIMER
| STATE_ON
| 无
|
这张表就是咱们的“状态机说明书”,程序会根据它来做事。
4. 怎么写成代码?
在C语言里,咱们用结构体数组来实现这个表格。每个结构体记录一条规则,包括:
- 当前状态
- 事件
- 下个状态
- 要执行的动作(用函数指针表示)
程序运行时,拿到当前状态和事件后,遍历这个数组,找到匹配的那一行,执行动作,更新状态。
5. 动作怎么实现?
动作就是控制LED的函数,比如“点亮”“关闭”“翻转”。这些函数会操作APM32F407的GPIO寄存器。
6. 定时器怎么弄?
慢闪和快闪需要定时器来控制节奏。咱们可以用APM32F4070的TMR2定时器,慢闪设1秒触发一次,快闪设0.2秒触发一次。状态机里会根据当前状态决定定时器的周期。
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"
- /** @addtogroup Examples
- @{
- */
- /** @addtogroup ADC_AnalogWindowWatchdog
- @{
- */
- /** @defgroup ADC_AnalogWindowWatchdog_Macros Macros
- @{
- */
- /* printf using USART1 */
- #define DEBUG_USART USART1
- /**@} end of group ADC_AnalogWindowWatchdog_Macros*/
- /** @defgroup ADC_AnalogWindowWatchdog_Functions Functions
- @{
- */
- void USARTInit(void);
- // 定义状态
- 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)
- {
- // 翻转LED状态
- 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;
- }
- }
- }
- // 延时函数(简单防抖用)
- void delay_ms(uint32_t ms)
- {
- for (uint32_t i = 0; i < ms * 8000; i++); // 粗略延时
- }
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] Main program
- *
- * @param None
- *
- * @retval None
- */
- int main(void)
- {
- APM_TINY_LEDInit(LED2);
- APM_TINY_LEDInit(LED3);
- APM_TINY_PBInit(BUTTON_KEY1, BUTTON_MODE_GPIO);
- USARTInit();
- RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_TMR2);
- TMR_BaseConfig_T tmrBaseConfig;
- tmrBaseConfig.clockDivision = TMR_CLOCK_DIV_1;
- tmrBaseConfig.countMode = TMR_COUNTER_MODE_UP;
- tmrBaseConfig.division = 83;
- tmrBaseConfig.period = 999;
- tmrBaseConfig.repetitionCounter = 0;
- TMR_ConfigTimeBase(TMR2, &tmrBaseConfig);
- TMR_EnableInterrupt(TMR2, TMR_INT_UPDATE);
- NVIC_EnableIRQRequest(TMR2_IRQn, 0, 0);
- TMR_Enable(TMR2);
- 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 USARTInit(void)
- {
- /* USART Initialization */
- USART_Config_T usartConfigStruct;
- /* USART configuration */
- USART_ConfigStructInit(&usartConfigStruct);
- usartConfigStruct.baudRate = 115200;
- usartConfigStruct.mode = USART_MODE_TX_RX;
- usartConfigStruct.parity = USART_PARITY_NONE;
- usartConfigStruct.stopBits = USART_STOP_BIT_1;
- usartConfigStruct.wordLength = USART_WORD_LEN_8B;
- usartConfigStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
- /* COM1 init*/
- APM_TINY_COMInit(COM1, &usartConfigStruct);
- }
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] This function handles TMR2 Handler
- *
- * @param None
- *
- * @retval None
- *
- */
- 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);
- }
- }
- }
- #if defined (__CC_ARM) || defined (__ICCARM__) || (defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050))
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] Redirect C Library function printf to serial port.
- * After Redirection, you can use printf function.
- *
- * @param ch: The characters that need to be send.
- *
- * @param *f: pointer to a FILE that can recording all information
- * needed to control a stream
- *
- * @retval The characters that need to be send.
- *
- * @note
- */
- int fputc(int ch, FILE* f)
- {
- /* send a byte of data to the serial port */
- USART_TxData(DEBUG_USART, (uint8_t)ch);
- /* wait for the data to be send */
- while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
- return (ch);
- }
- #elif defined (__GNUC__)
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] Redirect C Library function printf to serial port.
- * After Redirection, you can use printf function.
- *
- * @param ch: The characters that need to be send.
- *
- * @retval The characters that need to be send.
- *
- * @note
- */
- int __io_putchar(int ch)
- {
- /* send a byte of data to the serial port */
- USART_TxData(DEBUG_USART, ch);
- /* wait for the data to be send */
- while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
- return ch;
- }
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] Redirect C Library function printf to serial port.
- * After Redirection, you can use printf function.
- *
- * @param file: Meaningless in this function.
- *
- * @param *ptr: Buffer pointer for data to be sent.
- *
- * @param len: Length of data to be sent.
- *
- * @retval The characters that need to be send.
- *
- * @note
- */
- int _write(int file, char* ptr, int len)
- {
- int i;
- for (i = 0; i < len; i++)
- {
- __io_putchar(*ptr++);
- }
- return len;
- }
- #else
- #warning Not supported compiler type
- #endif
代码说明
1. 状态和事件:用枚举类型定义,简单明了。
2. 转换表:state_table是个结构体数组,每行对应一条规则。
3. 动作函数:控制LED的点亮、关闭、翻转。
4. 事件处理:process_event函数遍历表格,找到匹配的规则,执行动作并更新状态。
5. 主循环:检测按键和定时器事件,调用process_event。
6. 定时器调整:根据状态动态设置闪烁周期(慢闪1秒,快闪0.2秒)
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/video?vid=XNjQ2MzgxNzc4OA%3D%3D
9. 表驱动法的妙处在这儿
你看这个例程:
- 逻辑全在表里:状态转换和动作都写在state_table里,代码里没一堆if-else。
- 改起来方便:想加个“超快闪”状态?在表里加几行就行,不用动主逻辑。
- 效率高:查表比判断快,MCU跑起来不费劲。
10. 疑问:如果后面要在这份代码上加一个功能,好加吗?
我这份代码是用表驱动法和状态机设计的,所以扩展性很强,加新功能是相对容易的。下面我详细跟你说说为什么好加,以及具体怎么加,拿“呼吸灯”状态(LED慢慢变亮再慢慢变暗,循环往复)举个例子,给你讲清楚。
10.1 为什么好加?
我的代码用的是表驱动法和状态机,这俩组合起来就像搭积木,结构清晰,扩展方便。具体来说:
- 状态机把程序分成一个个独立的状态,每个状态有自己的行为和跳转规则。
- 表驱动法把状态之间的转换和动作都写在一个表格里,想加新功能,只要往表格里加几行就行,不用大改主逻辑。
这种设计的好处是,代码的可维护性和扩展性特别高,加新功能就像在积木堆里加一块新积木,原来的东西不会乱。
10.2 举个例子:加一个“呼吸灯”状态
假设你想加一个“呼吸灯”状态,让LED慢慢变亮再慢慢变暗,循环往复。我一步步告诉你怎么做。
1. 定义新状态
首先,在状态的枚举里加一个新状态,比如叫STATE_BREATH。代码可能是这样的:
- typedef enum {
- STATE_OFF, // 常灭
- STATE_ON, // 常亮
- STATE_SLOW_BLINK, // 慢闪
- STATE_FAST_BLINK, // 快闪
- STATE_BREATH // 呼吸灯,新加的状态
- } State;
这一步很简单,就是告诉系统多了一个状态。
2. 定义新动作
“呼吸灯”效果需要LED亮度渐变,通常得用PWM(脉宽调制)来实现。假设你的硬件是APM32F407,可以用TMR3的PWM通道(比如PA5)控制LED亮度。
先得配置PWM(具体配置得看硬件手册),然后写一个动作函数,比如breath_led,来调整亮度。为了简单,我们先用软件延时模拟一下效果:
- void breath_led(void) {
- // 模拟呼吸效果:慢慢变亮再慢慢变暗
- for (int i = 0; i < 100; i++) {
- turn_on_led(); // LED亮
- delay_ms(i); // 亮的时间逐渐增加
- turn_off_led(); // LED灭
- delay_ms(100 - i); // 灭的时间逐渐减少
- }
- }
这只是个粗糙的模拟,实际中你可以用PWM占空比递增递减来实现平滑效果,后面可以优化。
3. 更新状态转换表
状态转换表是核心,定义了每个状态收到事件后怎么跳转、做什么动作。假设原来有这些规则:
- Transition state_table[] = {
- {STATE_OFF, EVENT_KEY_PRESS, STATE_SLOW_BLINK, turn_off_led},
- {STATE_SLOW_BLINK, EVENT_KEY_PRESS, STATE_FAST_BLINK, NULL},
- {STATE_FAST_BLINK, EVENT_KEY_PRESS, STATE_ON, turn_on_led},
- {STATE_ON, EVENT_KEY_PRESS, STATE_OFF, turn_off_led},
- // 定时器事件
- {STATE_SLOW_BLINK, EVENT_TIMER, STATE_SLOW_BLINK, toggle_led},
- {STATE_FAST_BLINK, EVENT_TIMER, STATE_FAST_BLINK, toggle_led}
- };
现在加“呼吸灯”状态,比如按键从STATE_ON跳到STATE_BREATH,再按一下回到STATE_OFF:
- Transition state_table[] = {
- // 原来的规则...
- {STATE_OFF, EVENT_KEY_PRESS, STATE_SLOW_BLINK, turn_off_led},
- {STATE_SLOW_BLINK, EVENT_KEY_PRESS, STATE_FAST_BLINK, NULL},
- {STATE_FAST_BLINK, EVENT_KEY_PRESS, STATE_ON, turn_on_led},
- {STATE_ON, EVENT_KEY_PRESS, STATE_BREATH, breath_led}, // 新增
- {STATE_BREATH, EVENT_KEY_PRESS, STATE_OFF, turn_off_led}, // 新增
- // 定时器事件
- {STATE_OFF, EVENT_TIMER, STATE_OFF, NULL},
- {STATE_SLOW_BLINK, EVENT_TIMER, STATE_SLOW_BLINK, toggle_led},
- {STATE_FAST_BLINK, EVENT_TIMER, STATE_FAST_BLINK, toggle_led},
- {STATE_ON, EVENT_TIMER, STATE_ON, NULL},
- {STATE_BREATH, EVENT_TIMER, STATE_BREATH, breath_led} // 新增,定时器驱动呼吸效果
- };
这里加了三行:
- 从STATE_ON按键跳到STATE_BREATH,执行breath_led。
- 从STATE_BREATH按键跳回STATE_OFF,关灯。
- STATE_BREATH收到定时器事件时,保持状态并执行breath_led。
4. 调整定时器(可选)
如果想让呼吸效果更平滑,可以调快定时器(比如每10ms触发一次),然后在breath_led里逐步调整PWM占空比,而不是用阻塞的循环。不过为了简单,我们先用上面的方式。
加新功能的步骤总结
从上面看,加一个新功能就三步:
1. 加状态:在枚举里定义新状态。
2. 加动作:写一个函数实现新功能的行为。
3. 加规则:在状态转换表里加几行,定义跳转逻辑。
整个过程不改主循环的process_event函数,代码结构保持不变。这就是表驱动法的优势:改动少,风险低。
注意事项
加功能虽然简单,但有些地方得留心:
- 动作函数时间:如果breath_led用阻塞循环,会卡住主循环,影响按键响应。可以用定时器中断分步调整亮度,避免阻塞。
- 硬件资源:如果LED原来用GPIO,现在改PWM,得确保硬件配置正确,别冲突。
- 逻辑清晰:状态跳转要设计好,别弄成死循环或跳不过去。
总的来说,我这份代码因为用了表驱动法和状态机,加新功能特别容易。像“呼吸灯”这样的功能,按照上面步骤操作,几分钟就能搞定,而且不会把原有代码搞乱。
总结
表驱动法是个超级实用的技术,尤其在嵌入式C语言开发里。它通过把逻辑变成表格数据,让代码更简洁、好维护、跑得快。在状态机里用表驱动法,简直是如虎添翼,能把复杂的控制逻辑整理得井井有条。
附件
例程代码:
Table_driven_method_LED_Example.zip
(806.16 KB, 下载次数: 63)