• 表驱动法在STM32中的应用


    1、概念

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

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

    2、简单示例

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

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

    复制代码
    void fun(int day)
    {
        if (day == 1)
        {
            printf("Monday\n");
        }
        else if (day == 2)
        {
            printf("Tuesday\n");
        }
        else if (day == 3)
        {
            printf("Wednesday\n");
        }
        else if (day == 4)
        {
            printf("Thursday\n");
        }
        else if (day == 5)
        {
            printf("Friday\n");
        }
        else if (day == 6)
        {
            printf("Saturday\n");
        }
        else if (day == 7)
        {
            printf("Sunday\n");
        }
    }
    复制代码

    使用switch…case的方法写

    复制代码
    void fun(int day)
    {
        switch (day)
        {
        case 1:
            printf("Monday\n");
            break;
        case 2:
            printf("Tuesday\n");
            break;
        case 3:
            printf("Wednesday\n");
            break;
        case 4;
            printf("Thursday\n");
            break;
            case 5:
            printf("Friday\n");
            break;
        case 6:
            printf("Saturday\n");
            break;
        case 7:printf("Sunday\n");
            break;
        default:
            break;
        }
    }
    复制代码

    使用表驱动法实现

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

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

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

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

    常规写法

    复制代码
    int32_t aDigitCharNum[10] = {0}; /* 输入字符串中各数字字符出现的次数 */
    int32_t dwStrLen = strlen(szDigits);
    
    int32_t dwStrIdx = 0;
    for (; dwStrIdx < dwStrLen; dwStrIdx++)
    {
        switch (szDigits[dwStrIdx])
        {
        case '1':
            aDigitCharNum[0]++;
            break;
        case '2':
            aDigitCharNum[1]++;
            break;
        //... ...
        case '9':
            aDigitCharNum[8]++;
            break;
        }
    }
    复制代码

    表驱动法

    for(; dwStrIdx < dwStrLen; dwStrIdx++)
    {
        aDigitCharNum[szDigits[dwStrIdx] - '0']++;
    }

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

    3、在MCU中应用

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

    常规写法

    复制代码
    void LED_Ctrl(void)
    {
        static uint32_t sta = 0;
    
        if (0 == sta)
        {
            LED1_On();
        }
        else
        {
            LED1_Off();
        }
    
        if (1 == sta)
        {
            LED2_On();
        }
        else
        {
            LED2_Off();
        }
    
        /* 两个灯,最大不超过2 */
        sta = (sta + 1) % 2;
    }
    
    /* 主函数运行 */
    int main(void)
    {
        while (1)
        {
            LED_Ctrl();
            os_delay(200);
        }
    }
    复制代码

    表驱动法

    复制代码
    extern void LED1_On(void);
    extern void LED1_Off(void);
    extern void LED2_On(void);
    extern void LED2_Off(void);
    
    /* 把同一个灯的操作封装起来 */
    struct tagLEDFuncCB
    {
        void (*LedOn)(void);
        void (*LedOff)(void);
    };
    
    /* 定义需要操作到的灯的表 */
    const static struct tagLEDFuncCB LedOpTable[] =
    {
            {LED1_On, LED1_Off},
            {LED2_On, LED2_Off},
    };
    
    void LED_Ctrl(void)
    {
        static uint32_t sta = 0;
        uint8_t i;
    
        for (i = 0; i < sizeof(LedOpTable) / sizeof(LedOpTable[0]); i++)
        {
            (sta == i) ? (LedOpTable[i].LED_On()) : (LedOpTable[i].LED_Off());
        }
    
        /* 跑下个灯 */
        sta = (sta + 1) % (sizeof(LedOpTable) / sizeof(LedOpTable[0]));
    }
    
    int main(void)
    {
        while (1)
        {
            LED_Ctrl();
            os_delay(200);
        }
    }
    复制代码

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

    复制代码
    const static struct tagLEDFuncCB LedOpTable[] =
    {
        {LED1_On, LED1_Off},
        {LED2_On, LED2_Off},
        {LED3_On, LED3_Off},
    };
    复制代码

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

    复制代码
    typedef struct
    {
        rt_uint8_t CMD;
        rt_uint8_t (*callback_func)(rt_uint8_t cmd, rt_uint8_t *msg, uint8_t len);
    } _FUNCCALLBACK;
    
    _FUNCCALLBACK callback_list[] =
    {
        {cmd1, func_callback1},
        {cmd2, func_callback2},
        {cmd3, func_callback3},
        {cmd4, func_callback41},
        ...
    };
    
    void poll_task(rt_uint8_t cmd, rt_uint8_t *msg, uint8_t len)
    {
        int cmd_indexmax = sizeof(callback_list) / sizeof(_FUNCCALLBACK);
        int cmd_index = 0;
    
        for (cmd_index = 0; cmd_index < cmd_indexmax; cmd_index++)
        {
            if (callback_list[cmd_index].CMD == cmd)
            {
                if (callback_list[cmd_index])
                {
                    /* 处理逻辑  */
                    callback_list[cmd_index].callback_func(cmd, msg, len);
                }
            }
        }
    }
    复制代码

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

    结构体封装

    复制代码
    typedef enum
    {
        stage1 = 0,
        stage2,
        stage3,
        stage4,
        stage5,
        stage6,
        stage7,
        stage8,
        stage9,
    } SCENE;
    typedef struct
    {
        void (*current_operate)(); //当前场景的处理函数
        SCENE Index;               //当前场景的标签
        SCENE Up;                  //按下Up键跳转的场景
        SCENE Down;                //按下Down键跳转的场景
        SCENE Right;               //按下Left键跳转的场景
        SCENE Left;                //按下Right键跳转的场景
    } STAGE_TAB;
    复制代码

    函数映射表

    复制代码
    STAGE_TAB stage_tab[] = {
        //operate        Index   Up      Down    Left    Right
        {Stage1_Handler, stage1, stage4, stage7, stage3, stage2},
        {Stage2_Handler, stage2, stage5, stage8, stage1, stage3},
        {Stage3_Handler, stage3, stage6, stage9, stage2, stage1},
        {Stage4_Handler, stage4, stage7, stage1, stage6, stage5},
        {Stage5_Handler, stage5, stage8, stage2, stage4, stage6},
        {Stage6_Handler, stage6, stage9, stage3, stage5, stage4},
        {Stage7_Handler, stage7, stage1, stage4, stage9, stage8},
        {Stage8_Handler, stage8, stage2, stage5, stage7, stage9},
        {Stage9_Handler, stage9, stage3, stage6, stage8, stage7},
    };
    复制代码

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

    char current_stage=stage1;
    char prev_stage=current_stage;

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

    current_stage =stage_tab[current_stage].Up;

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

    if(current_stage!=prev_stage)
    {
      stage_tab[current_stage].current_operate();
      prev_stage=current_stage;
    }

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

    4、后记

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

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

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

    祝大家变得更强!

     

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

     

  • 相关阅读:
    上班族为何需要做副业?如何靠副业月入过万?
    (LinkedList与链表) 和 (ArrayList与顺序表)的区别
    基于 Three.js 的 3D 模型加载优化
    基于生物地理学算法优化的BP神经网络(预测应用) - 附代码
    谷粒学院 —— 10、课程管理:整合阿里云视频点播
    `英语` 2022/8/12
    佛山科学技术学院考研介绍
    什么样的触达方式,会员会喜欢?
    精彩回顾|关系网络赋能银行数字化转型的应用与实践
    HackTheBox---Starting Point-- Tier 0---Meow
  • 原文地址:https://www.cnblogs.com/Fireflycjd/p/16861722.html