• 表驱动法在STM32中的应用


    1、概念

    所谓表驱动法(Table-Driven Approach)简而言之就是用查表的方法获取数据。此处的“表”通常为数组,但可视为数据库的一种体现。根据字典中的部首检字表查找读音未知的汉字就是典型的表驱动法,即以每个字的字形为依据,计算出一个索引值,并映射到对应的页数。相比一页一页地顺序翻字典查字,部首检字法效率极高。

    具体到编程方面,在数据不多时可用逻辑判断语句(if…else或switch…case)来获取值;但随着数据的增多,逻辑语句会越来越长,此时表驱动法的优势就开始显现。

    2、简单示例

    上面讲概念总是枯燥的,我们简单写一个C语言的例子。下面例子功能:传入不同的数字打印不同字符串。

    使用if…else逐级判断的写法如下

    1. void fun(int day)
    2. {
    3. if (day == 1)
    4. {
    5. printf("Monday\n");
    6. }
    7. else if (day == 2)
    8. {
    9. printf("Tuesday\n");
    10. }
    11. else if (day == 3)
    12. {
    13. printf("Wednesday\n");
    14. }
    15. else if (day == 4)
    16. {
    17. printf("Thursday\n");
    18. }
    19. else if (day == 5)
    20. {
    21. printf("Friday\n");
    22. }
    23. else if (day == 6)
    24. {
    25. printf("Saturday\n");
    26. }
    27. else if (day == 7)
    28. {
    29. printf("Sunday\n");
    30. }
    31. }

    使用switch…case的方法写

    1. void fun(int day)
    2. {
    3. switch (day)
    4. {
    5. case 1:
    6. printf("Monday\n");
    7. break;
    8. case 2:
    9. printf("Tuesday\n");
    10. break;
    11. case 3:
    12. printf("Wednesday\n");
    13. break;
    14. case 4;
    15. printf("Thursday\n");
    16. break;
    17. case 5:
    18. printf("Friday\n");
    19. break;
    20. case 6:
    21. printf("Saturday\n");
    22. break;
    23. case 7:printf("Sunday\n");
    24. break;
    25. default:
    26. break;
    27. }
    28. }

    使用表驱动法实现

    1. char weekDay[] = {Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday};
    2. void fun(int day)
    3. {
    4. printf("%s\n",weekDay[day]);
    5. }

    看完示例,可能“恍然大悟”,一拍大腿,原来表驱动法就是这么简单啊。是的,它的核心原理就是这个简单,如上面例子一样。

    如果上面的例子还没get这种用法的好处,那么再举一个栗子。

    统计用户输入的一串数字中每个数字出现的次数。

    常规写法

    1. int32_t aDigitCharNum[10] = {0}; /* 输入字符串中各数字字符出现的次数 */
    2. int32_t dwStrLen = strlen(szDigits);
    3. int32_t dwStrIdx = 0;
    4. for (; dwStrIdx < dwStrLen; dwStrIdx++)
    5. {
    6. switch (szDigits[dwStrIdx])
    7. {
    8. case '1':
    9. aDigitCharNum[0]++;
    10. break;
    11. case '2':
    12. aDigitCharNum[1]++;
    13. break;
    14. //... ...
    15. case '9':
    16. aDigitCharNum[8]++;
    17. break;
    18. }
    19. }

    表驱动法

    1. for(; dwStrIdx < dwStrLen; dwStrIdx++)
    2. {
    3. aDigitCharNum[szDigits[dwStrIdx] - '0']++;
    4. }

    偶尔在一些开源项目中看到类似的操作,惊呼“骚操作”,其实他们有规范的叫法:表驱动法。

    3、在MCU中应用

    MCU中的应用示例,怎么少的了点灯大师操作呢?首先来点一下流水LED灯吧。

    常规写法

    1. void LED_Ctrl(void)
    2. {
    3. static uint32_t sta = 0;
    4. if (0 == sta)
    5. {
    6. LED1_On();
    7. }
    8. else
    9. {
    10. LED1_Off();
    11. }
    12. if (1 == sta)
    13. {
    14. LED2_On();
    15. }
    16. else
    17. {
    18. LED2_Off();
    19. }
    20. /* 两个灯,最大不超过2 */
    21. sta = (sta + 1) % 2;
    22. }
    23. /* 主函数运行 */
    24. int main(void)
    25. {
    26. while (1)
    27. {
    28. LED_Ctrl();
    29. os_delay(200);
    30. }
    31. }

    表驱动法

    1. extern void LED1_On(void);
    2. extern void LED1_Off(void);
    3. extern void LED2_On(void);
    4. extern void LED2_Off(void);
    5. /* 把同一个灯的操作封装起来 */
    6. struct tagLEDFuncCB
    7. {
    8. void (*LedOn)(void);
    9. void (*LedOff)(void);
    10. };
    11. /* 定义需要操作到的灯的表 */
    12. const static struct tagLEDFuncCB LedOpTable[] =
    13. {
    14. {LED1_On, LED1_Off},
    15. {LED2_On, LED2_Off},
    16. };
    17. void LED_Ctrl(void)
    18. {
    19. static uint32_t sta = 0;
    20. uint8_t i;
    21. for (i = 0; i < sizeof(LedOpTable) / sizeof(LedOpTable[0]); i++)
    22. {
    23. (sta == i) ? (LedOpTable[i].LED_On()) : (LedOpTable[i].LED_Off());
    24. }
    25. /* 跑下个灯 */
    26. sta = (sta + 1) % (sizeof(LedOpTable) / sizeof(LedOpTable[0]));
    27. }
    28. int main(void)
    29. {
    30. while (1)
    31. {
    32. LED_Ctrl();
    33. os_delay(200);
    34. }
    35. }

    这样的代码结构紧凑,因为和结构体结合起来了,方便添加下一个LED灯到流水灯序列中,这其中涉及到函数指针,详细请看《回调函数》,只需要修改LedOpTable如下

    1. const static struct tagLEDFuncCB LedOpTable[] =
    2. {
    3. {LED1_On, LED1_Off},
    4. {LED2_On, LED2_Off},
    5. {LED3_On, LED3_Off},
    6. };

    这年头谁还把流水灯搞的这么花里胡哨的啊,那么就举例在串口解析中的应用,之前的文章推送过《回调函数在命令解析中的应用》,下面只贴一下代码

    1. typedef struct
    2. {
    3. rt_uint8_t CMD;
    4. rt_uint8_t (*callback_func)(rt_uint8_t cmd, rt_uint8_t *msg, uint8_t len);
    5. } _FUNCCALLBACK;
    6. _FUNCCALLBACK callback_list[] =
    7. {
    8. {cmd1, func_callback1},
    9. {cmd2, func_callback2},
    10. {cmd3, func_callback3},
    11. {cmd4, func_callback41},
    12. ...
    13. };
    14. void poll_task(rt_uint8_t cmd, rt_uint8_t *msg, uint8_t len)
    15. {
    16. int cmd_indexmax = sizeof(callback_list) / sizeof(_FUNCCALLBACK);
    17. int cmd_index = 0;
    18. for (cmd_index = 0; cmd_index < cmd_indexmax; cmd_index++)
    19. {
    20. if (callback_list[cmd_index].CMD == cmd)
    21. {
    22. if (callback_list[cmd_index])
    23. {
    24. /* 处理逻辑 */
    25. callback_list[cmd_index].callback_func(cmd, msg, len);
    26. }
    27. }
    28. }
    29. }

    除上述例子,表驱动法在UI界面中也有良好的应用,如下

    结构体封装

    1. typedef enum
    2. {
    3. stage1 = 0,
    4. stage2,
    5. stage3,
    6. stage4,
    7. stage5,
    8. stage6,
    9. stage7,
    10. stage8,
    11. stage9,
    12. } SCENE;
    13. typedef struct
    14. {
    15. void (*current_operate)(); //当前场景的处理函数
    16. SCENE Index; //当前场景的标签
    17. SCENE Up; //按下Up键跳转的场景
    18. SCENE Down; //按下Down键跳转的场景
    19. SCENE Right; //按下Left键跳转的场景
    20. SCENE Left; //按下Right键跳转的场景
    21. } STAGE_TAB;

    函数映射表

    1. STAGE_TAB stage_tab[] = {
    2. //operate Index Up Down Left Right
    3. {Stage1_Handler, stage1, stage4, stage7, stage3, stage2},
    4. {Stage2_Handler, stage2, stage5, stage8, stage1, stage3},
    5. {Stage3_Handler, stage3, stage6, stage9, stage2, stage1},
    6. {Stage4_Handler, stage4, stage7, stage1, stage6, stage5},
    7. {Stage5_Handler, stage5, stage8, stage2, stage4, stage6},
    8. {Stage6_Handler, stage6, stage9, stage3, stage5, stage4},
    9. {Stage7_Handler, stage7, stage1, stage4, stage9, stage8},
    10. {Stage8_Handler, stage8, stage2, stage5, stage7, stage9},
    11. {Stage9_Handler, stage9, stage3, stage6, stage8, stage7},
    12. };

    定义两个变量保存当前场景和上一个场景

    1. char current_stage=stage1;
    2. char prev_stage=current_stage;

    按下Up按键 跳转到指定场景current_stage的值根据映射表改变

    current_stage =stage_tab[current_stage].Up;

    场景改变后 根据映射表执行相应的函数Handler

    1. if(current_stage!=prev_stage)
    2. {
    3. stage_tab[current_stage].current_operate();
    4. prev_stage=current_stage;
    5. }

    这是一个简单的菜单操作,结合了表驱动法。在MCU中表驱动法有很多很多用处,本文的例子已经过多了,如果在通勤路上用手机看到这里,已经很难了。关于UI操作,大神figght在github开源了zBitsView仓库,单片机实现屏幕界面,多层菜单。很牛,很优秀的代码,有兴趣的同学可以学习一下。https://github.com/figght/zBitsView

    4、后记

    这篇文章我也看到网上一遍表驱动法的后总结的笔记,可能也有很多同学和我一样,在自己的项目中熟练应用了这种“技巧”,但今天才知道名字:表驱动法。

    这篇文章多数都是代码示例,实在因为表驱动法大家应该都熟练应用了,这篇文章算是总结一下吧。

    学习知识,可以像在学校从概念一点点学习,也可以在工作中慢慢积累,然后总结记录,回归最初的概念,丰富自己的知识框架。

    祝大家变得更强!

    点击查看:C语言进阶专辑

  • 相关阅读:
    多线程---操作系统
    【408计算机组成原理】—进位计数制(二)
    批量更新 AWS ECS Fargate 服务
    Java 基于微信小程序的农产品自主供销小程序
    【C语言】 01.C语言常见概念
    企业信息查询:洞悉市场,抢占先机
    单调栈题目:接雨水
    信息系统项目管理师必背核心考点(七十三)黑/白/灰盒测试
    GO语言网络编程(并发编程)定时器
    自动泊车系统设计学习笔记
  • 原文地址:https://blog.csdn.net/Firefly_cjd/article/details/127711020