1

表驱动法在STM32中的应用

 1 year ago
source link: https://server.51cto.com/article/720310.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

表驱动法在STM32中的应用

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

所谓表驱动法(Table-Driven Approach)简而言之就是用查表的方法获取数据。此处的“表”通常为数组,但可视为数据库的一种体现。

根据字典中的部首检字表查找读音未知的汉字就是典型的表驱动法,即以每个字的字形为依据,计算出一个索引值,并映射到对应的页数。相比一页一页地顺序翻字典查字,部首检字法效率极高。

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

上面讲概念总是枯燥的,我们简单写一个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']++;
}

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

在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中表驱动法有很多很多用处,本文的例子已经过多了,如果在通勤路上用手机看到这里,已经很难了。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK