电子产业一站式赋能平台

PCB联盟网

搜索
查看: 196|回复: 0
收起左侧

表驱动法在STM32中的应用

[复制链接]

567

主题

567

帖子

4209

积分

四级会员

Rank: 4

积分
4209
发表于 2022-10-10 08:30:00 | 显示全部楼层 |阅读模式
概念
9 r4 |9 N3 c3 o% a) S所谓表驱动法(Table-Driven Approach)简而言之就是用查表的方法获取数据。此处的“表”通常为数组,但可视为数据库的一种体现。根据字典中的部首检字表查找读音未知的汉字就是典型的表驱动法,即以每个字的字形为依据,计算出一个索引值,并映射到对应的页数。相比一页一页地顺序翻字典查字,部首检字法效率极高。具体到编程方面,在数据不多时可用逻辑判断语句(if…else或switch…case)来获取值;但随着数据的增多,逻辑语句会越来越长,此时表驱动法的优势就开始显现。  B2 U. z& \" l3 S& F1 P- q
简单示例
3 g% @6 ?( @2 G: P/ O% W4 O! G上面讲概念总是枯燥的,我们简单写一个C语言的例子。下面例子功能:传入不同的数字打印不同字符串。使用if…else逐级判断的写法如下
# k" g2 o' R5 s9 H# x. b
  • void fun(int day){    if (day == 1)    {        printf("Monday# Q" F  T1 Z* n/ N- h
    ");    }    else if (day == 2)    {        printf("Tuesday
    - }% N* E: y' k1 h");    }    else if (day == 3)    {        printf("Wednesday# `9 Y8 }8 t# N
    ");    }    else if (day == 4)    {        printf("Thursday
    - n& A0 E" \1 A0 C: {");    }    else if (day == 5)    {        printf("Friday
    / k" t9 B3 N3 [/ e5 ?8 ]");    }    else if (day == 6)    {        printf("Saturday
    1 ^  u4 |4 e& U  |/ I8 {) a6 J");    }    else if (day == 7)    {        printf("Sunday4 {7 _1 s1 D9 |- n3 X
    ");    }}使用switch…case的方法写
    ; ^4 p: x& K7 W; K: ^* Z" l
  • void fun(int day){    switch (day)    {    case 1:        printf("Monday
    # j6 A8 R6 {' ]) E! i");        break;    case 2:        printf("Tuesday: s  B7 W3 K, T
    ");        break;    case 3:        printf("Wednesday5 |8 Y+ L5 ^0 {% U5 t9 B* q
    ");        break;    case 4;        printf("Thursday, K3 I0 j5 T( i6 l# y0 d
    ");        break;        case 5:        printf("Friday9 M7 o3 f* X  l* q# y) T
    ");        break;    case 6:        printf("Saturday; T( @% W. m( Q/ }
    ");        break;    case 7:printf("Sunday
    / J& w# |  a/ }! e");        break;    default:        break;    }}使用表驱动法实现) Z* f+ X, \" C8 ]
  • char weekDay[] = {Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday};void fun(int day){  printf("%s
    / x- E& W$ o' Y# a" M( L+ D",weekDay[day]);}看完示例,可能“恍然大悟”,一拍大腿,原来表驱动法就是这么简单啊。是的,它的核心原理就是这个简单,如上面例子一样。5 K4 y/ L) }9 T7 `# p
    如果上面的例子还没get这种用法的好处,那么再举一个栗子。
    8 V  b7 y! L+ Q: I- p统计用户输入的一串数字中每个数字出现的次数。- z. N: _) A  y1 U" ?! k
    常规写法
  • int32_t aDigitCharNum[10] = {0}; /* 输入字符串中各数字字符出现的次数 */int32_t dwStrLen = strlen(szDigits);
    + O( H% w% ]( bint32_t dwStrIdx = 0;for (; dwStrIdx {    switch (szDigits[dwStrIdx])    {    case '1':        aDigitCharNum[0]++;        break;    case '2':        aDigitCharNum[1]++;        break;    //... ...    case '9':        aDigitCharNum[8]++;        break;    }}表驱动法
    ! L/ N: j2 X$ K' h; v9 i8 u
  • for(; dwStrIdx {    aDigitCharNum[szDigits[dwStrIdx] - '0']++;}偶尔在一些开源项目中看到类似的操作,惊呼“骚操作”,其实他们有规范的叫法:表驱动法。. Z2 D( e: U# Q* M0 N( n
    在MCU中应用
    * P3 O  A4 ?# F( [2 o% e在MCU中的应用示例,怎么少的了点灯大师操作呢?首先来点一下流水LED灯吧。
    1 J/ d: n: a: ]: M, B* G常规写法" U; e7 T! @. S1 l7 v
  • void LED_Ctrl(void){    static uint32_t sta = 0;
    / g$ j# B/ O5 B8 ?" G  D    if (0 == sta)    {        LED1_On();    }    else    {        LED1_Off();    }$ X, ^% U+ u0 \& a1 f$ |" N
        if (1 == sta)    {        LED2_On();    }    else    {        LED2_Off();    }
    2 u  o# n7 {* g# X3 j    /* 两个灯,最大不超过2 */    sta = (sta + 1) % 2;}
    * W! Z& o, `# Z# `$ R/* 主函数运行 */int main(void){    while (1)    {        LED_Ctrl();        os_delay(200);    }}表驱动法0 x3 H8 p( {" v/ a; N
  • extern void LED1_On(void);extern void LED1_Off(void);extern void LED2_On(void);extern void LED2_Off(void);: ]: Y( O4 H- _1 G: `% n4 c6 V
    /* 把同一个灯的操作封装起来 */struct tagLEDFuncCB{    void (*LedOn)(void);    void (*LedOff)(void);};) B. _0 c: ], A# _
    /* 定义需要操作到的灯的表 */const static struct tagLEDFuncCB LedOpTable[] ={        {LED1_On, LED1_Off},        {LED2_On, LED2_Off},};
    ! t9 t& Z' q- H2 F, a0 Cvoid LED_Ctrl(void){    static uint32_t sta = 0;    uint8_t i;' c/ G; ~5 N8 C2 C; O
        for (i = 0; i sizeof(LedOpTable) / sizeof(LedOpTable[0]); i++)    {        (sta == i) ? (LedOpTable.LED_On()) : (LedOpTable.LED_Off());    }/ D' B" m1 X# Q# E6 n" T+ k
        /* 跑下个灯 */    sta = (sta + 1) % (sizeof(LedOpTable) / sizeof(LedOpTable[0]));}/ [! F9 W( a3 Z7 H5 z# x3 l
    int main(void){    while (1)    {        LED_Ctrl();        os_delay(200);    }}这样的代码结构紧凑,因为和结构体结合起来了,方便添加下一个LED灯到流水灯序列中,这其中涉及到函数指针,详细请看《回调函数》,只需要修改LedOpTable如下
    $ {! X" ]6 S$ `' v/ ]" _
  • 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;
    8 q! x4 v/ B# |- X_FUNCCALLBACK callback_list[] ={    {cmd1, func_callback1},    {cmd2, func_callback2},    {cmd3, func_callback3},    {cmd4, func_callback41},    ...};
    : m7 {! b8 N; x) ]9 \3 `9 x4 U# n$ Wvoid 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;+ K' R" A; P' U5 D* k  @% z; d
        for (cmd_index = 0; cmd_index     {        if (callback_list[cmd_index].CMD == cmd)        {            if (callback_list[cmd_index])            {                /* 处理逻辑  */                callback_list[cmd_index].callback_func(cmd, msg, len);            }        }    }}除上述例子,表驱动法在UI界面中也有良好的应用,如下
    & a2 ?3 ~, x* c4 ]! m5 b  I1 ?结构体封装
  • 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;函数映射表
    # X: e  W$ x7 h% ~
  • 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},};定义两个变量保存当前场景和上一个场景* r# {0 m2 Y" y5 O0 w$ N
  • char current_stage=stage1;char prev_stage=current_stage;按下Up按键 跳转到指定场景current_stage的值根据映射表改变
    ) q4 }- f( i. L9 I1 q( ]0 M
  • current_stage =stage_tab[current_stage].Up;场景改变后 根据映射表执行相应的函数Handler7 h, ^0 V$ `( i2 a  ~
  • 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
    # C; g0 _  K; t% ^& A" n7 v$ S& n2 l5 ?. l/ p7 M
    后记
    ) Z3 J! g4 l' M. n7 ]5 O这篇文章我也看到网上一遍表驱动法的后总结的笔记,可能也有很多同学和我一样,在自己的项目中熟练应用了这种“技巧”,但今天才知道名字:表驱动法。
    : Z2 W' e1 @2 F' K! _: a这篇文章多数都是代码示例,实在因为表驱动法大家应该都熟练应用了,这篇文章算是总结一下吧。9 d1 K; S2 C5 x- c9 @
    学习知识,可以像在学校从概念一点点学习,也可以在工作中慢慢积累,然后总结记录,回归最初的概念,丰富自己的知识框架。: K3 m# ^+ `% _, i
    祝大家变得更强!END
    " }; f7 _3 t  R- y- P1 G5 y

    2uhazf4qq1t64014187353.gif

    2uhazf4qq1t64014187353.gif
    & s! V8 @1 V6 X, ~, v8 `# `4 Z) z

    t0h4nm5vm1i64014187453.gif

    t0h4nm5vm1i64014187453.gif
    ) o0 L+ M2 |( {# |$ v8 P9 Y
    ?测量代码运行时间  必读
      ]0 r7 X( n& k8 D; i/ j?聊一聊const关键字
    % O% B) o2 ?& G6 v' s& f?联合体在单片机中的应用  
    . F+ f/ {3 `2 `8 l& V6 s2 ?6 l6 D?教你如何在STM32中使用DSP指令
    8 y$ _* o" h4 J$ J3 A2 z?STM32单片机中结构体和枚举的结合
  • 回复

    使用道具 举报

    发表回复

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则


    联系客服 关注微信 下载APP 返回顶部 返回列表