找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 3834|回复: 4
收起左侧

[后续]单片机状态机+事件驱动 按键扫描

  [复制链接]
ID:84652 发表于 2020-4-9 23:38 | 显示全部楼层 |阅读模式
本帖最后由 王朗的诱惑 于 2020-4-14 22:26 编辑

前接[失败]状态机+事件驱动 按键扫描。(如果懒得看前面的内容可以直接看本帖,无妨)
继2018年10月实验过一个按键扫描失败,时隔将近一年半,后续它终于来了!
这次换用了IAP15W4K61S4,把之前的程序搞好了。必须要承认一个错误:之前单片机死机其实不是单片机速度问题,而是程序设计问题[惭愧……],现在已经解决。这次的程序添加了事件队列,进一步释放定时器资源。定时器只负责添加任务,由主程序清空任务队列+执行事件函数。所以芯片慢点也没关系啦。

正文
状态机、事件队列
状态机还是挺常见,不过为什么要用事件队列呢?因为中断函数不能拖得太长,如果定时器中断执行的事件函数还没完,下一次中断又来了可怎么办……所以加入事件队列机制,中断只负责把需要执行的事件入队,由主函数执行并清空队列里面的事件函数,也算是某种“异步”了吧。
假如主函数清空队列的速度还没有定时器入队快,那么队列满了以后,之后的按键事件直接抛弃,不会响应。
参考资料资源占用
  • 定时器0 - 按键扫描
  • 定时器1 - 串口1
  • Program Size: data=59.3 xdata=100 code=4214(MDK5 C51编译,8级优化,还是占了挺多空间的,不过功能强大)

功能说明
  • 支持button和switch
  • 支持button单击、多击、长按、多按键组合
  • 支持任意IO的按键连接

文件说明
  • KeyScanConfig.h
    按键扫描相关的一些常量,根据需要修改。
    1. #define     EN_P4                        //如果按键连接到P4上需要使能此项 有的封装没有P4
    2. #define     EN_P5                        //如果按键连接到P5上需要使能此项 有的封装没有P5
    3. #define     EN_P6                        //如果按键连接到P6上需要使能此项 有的封装没有P6
    4. typedef     unsigned int        keyTriggerType_t;    //数据类型是几位就支持几个按键 定义多了占内存
    5. #define     MAX_KEY_NUMBER      (sizeof(keyTriggerType_t)<<3)//最大支持按键数量为keyTriggerType_t的位数即(sizeof(keyTriggerType_t)*8)

    6. #define     DEBOUNCE_TIME               20          //消抖延时ms
    7. #define     LONG_PRESS_TIME             1500        //长按判定时间ms
    8. #define     N_CLICK_NUMBER              2           //连击判定次数
    9. #define     N_CLICK_TIMELIMIT           300         //连击间隔超时时间ms (超过此时间判定为单击)
    10. #define     EVENT_QUEUE_LEN             8           // 事件队列长度,为了简化计算,需要为2的整数次幂
    复制代码

  • KeyScan.h
    各种枚举、结构体、函数定义。不需要用户修改。
  • KeyScan.c
    按键扫描的实现。不需要用户修改。

使用方法
以下步骤在主函数main.c中操作。
  • 把KeyScan.h, KeyScan.c, KeyScanConfig.h放到工程目录下。
  • 在主函数中引入头文件
    1. #include "main.h"
    2. #include "Uart.h"
    3. #include "KeyScan.h"
    复制代码
    main.h包含基本数据类型的typedef定义,一些C库的头文件引入和系统时钟设置;
    Uart.h是单片机串口模块头文件;
    KeyScan.h是按键扫描模块头文件。
  • 假如现在有4个按键A,B,C,D,给按键编号,放到枚举类型里。
    1. enum EnumUserKey{                 //按键编号 从0开始 不得超过(MAX_KEY_NUMBER-1)
    2.     EnumKey_A = 0,
    3.     EnumKey_B = 1,
    4.     EnumKey_C = 2,
    5.     EnumKey_D = 3
    6. };
    复制代码

  • 定义按键相关的两个结构体,作为全局变量。GPIO_KEY_NUM是第3步中按键的数量,这里是4;FUNC_KEY_NUM是第5步中事件函数的数量,这里是3,功能与按键是独立的,数量可以不相等。
    1. #define GPIO_KEY_NUM 4                                  // 按键总数,即enum EnumUserKey定义的按键数量
    2. xdata KeyIO_t SingleKey[GPIO_KEY_NUM];                  // 按键IO数组
    3. #define FUNC_KEY_NUM 3                                  // 用户自定义的功能总数
    4. xdata KeyFunc_t KeyFuncs[FUNC_KEY_NUM];                 // 按键功能数组
    复制代码

  • 定义按键功能函数。比如,需要按键A,B单击分别触发,按键C,D同时按下触发,功能函数可以定义成下面这样,函数名字随意。
    1. void KeyAPressEvent(void){
    2.     P40 = ~P40;
    3. }
    4. void KeyBPressEvent(void){
    5.     Delay100ms();
    6. }
    7. void KeyCDPressEvent(void){
    8.     P41 = ~P41;
    9.     // printf发送长串被中断打断会死机,使用UartSendString
    10.     // 如果很短可以使用printf
    11.     UartSendString("testtesttesttesttesttesttesttesttesttesttesttesttest\r\n");
    12.     Delay100ms();       // 长延时也不会死机了,哈哈
    13.     UartSendString("testtesttesttesttesttesttesttesttesttesttesttesttest\r\n");
    14.     Delay100ms();
    15. }
    复制代码

  • 好的,现在按键有了,功能也有了,但是还没联系到一起。下面是按键扫描初始化函数,把它们联系起来。
    1. //按键扫描初始化
    2. void KeyInit(void){
    3.     u8 i;
    4.     // 函数指针必须全部初始化为NULL
    5.     for(i=0; i<FUNC_KEY_NUM; i++){
    6.         KeyFuncs.fp_singleClick = NULL;
    7.         KeyFuncs.fp_comboClick = NULL;
    8.         KeyFuncs.fp_longPress = NULL;
    9.         KeyFuncs.fp_multiPress = NULL;
    10.     }
    11.    
    12.     // 注册按键 Port1必须是IO口 Port2是IO口或"GND"
    13.     SingleKey[EnumKey_A].IOPort1 = "P36"; SingleKey[EnumKey_A].IOPort2 = "GND";
    14.     SingleKey[EnumKey_B].IOPort1 = "P52"; SingleKey[EnumKey_B].IOPort2 = "GND";
    15.     SingleKey[EnumKey_C].IOPort1 = "P54"; SingleKey[EnumKey_C].IOPort2 = "GND";
    16.     SingleKey[EnumKey_D].IOPort1 = "P53"; SingleKey[EnumKey_D].IOPort2 = "GND";
    17.    
    18.     // 需要响应的键值 注意是键值! 不是键编号! 组合按键用或
    19.     KeyFuncs[0].triggerValue = TRIGGER_VALUE(EnumKey_A);
    20.     // 注册回调函数为单击功能
    21.     KeyFuncs[0].fp_singleClick = KeyAPressEvent;
    22.    
    23.     // 需要响应的键值 注意是键值! 不是键编号! 组合按键用或
    24.     KeyFuncs[1].triggerValue = TRIGGER_VALUE(EnumKey_B);
    25.     // 注册回调函数为单击功能
    26.     KeyFuncs[1].fp_singleClick = KeyBPressEvent;
    27.    
    28.     // 需要响应的键值 注意是键值! 不是键编号! 组合按键用或
    29.     KeyFuncs[2].triggerValue = TRIGGER_VALUE(EnumKey_C) | TRIGGER_VALUE(EnumKey_D);
    30.     // 注册回调函数为组合键功能
    31.     KeyFuncs[2].fp_multiPress = KeyCDPressEvent;
    32.    
    33.     KeyScanInit((KeyIO_t*)&SingleKey, GPIO_KEY_NUM, (KeyFunc_t*)&KeyFuncs, FUNC_KEY_NUM);
    34. }
    复制代码
    初始化过程可以分为4个步骤:

    • 初始化函数指针为NULL,这里的函数指针变量来自第4步定义的xdata KeyFunc_t KeyFuncs[FUNC_KEY_NUM]
    • 告诉单片机按键的硬件连线位置,假如按键A,B,C,D的一端分别连到单片机的P36,P52,P54,P53上,另一端接地,就按照上面的程序设置。如果按键是矩阵的,没有接地,就把按键两端的IO都对应写成字符串。

    (这么做的好处就是可以把按键随便乱接,毕竟有的封装,比如SOP-16是没有完整的一组8bit IO引出的,假如在这个单片机上用传统的方式应用4×4矩阵键盘,位处理是不是特别难受?)
    • 这一步把按键和一个事件函数联系起来。
      需要用到TRIGGER_VALUE宏,把按键编号转换成触发值,如果用到组合键,把各个按键的触发值用|连接即可。
      还需要注意的就是按键的功能是靠结构体成员的名字来区分的,有fp_singleClick, fp_comboClick, fp_longPress, fp_multiPress共4种,给哪个赋值,对应的事件函数就是什么功能。
    • 把刚才设置好的结构体给到按键扫描程序,开始按键扫描。

  • 编写主函数,把KeyInit()放到初始化里,把KeyEventProcess()放到while(1)里。
    1. void main(){
    2.     EA = 1;
    3.     UartInit();
    4.     KeyInit();        //按键扫描初始化
    5.     // printf发送长串被中断打断会死机,使用UartSendString
    6.     // printf("testtesttesttesttesttesttesttesttesttesttesttesttest\r\n");
    7.     UartSendString("testtesttesttesttesttesttesttesttesttesttesttesttest\r\n");
    8.     while(1){
    9.         KeyEventProcess();
    10.     }
    11. }
    复制代码
    KeyEventProcess()检查事件队列,执行队列里所有函数。如果队列满了,那么再有按键事件的话,就会被忽略。所以,慢点按,哈哈。(其实单片机速度够,快点按也没事,而且队列长度能改,在KeyScanConfig.h里)

注意事项
  • printf发送字符串有bug。
    具体发生在printf发送过程中被中断打断的时候,现象是死机,串口不断发送乱码。调节串口中断优先级为最高没有改善。
    如果没有中断打断,不会有bug。
  • 系统主频率在main.h里对应实际主频率修改。
  • 串口波特率在Uart.h里对应实际波特率修改。
  • 目前仅在IAP15W4K61S4上实验,15系列的单片机可以直接用,如果更换其他系列单片机需要修改Timer0的初始化和中断函数。(在KeyScan.c里定义)
  • 编译报了3个警告,不影响。

其他
害,原谅我偷懒下吧……其实文档已经在github上面写好了,这里不知道怎么用markdown编辑器,搞得排版很乱……所以剩下的设计思路参考Wiki吧。

工程代码已上传。(也可以到github上面下载https://github.com/AdjWang/C51KeyScan)


评分

参与人数 1黑币 +50 收起 理由
admin + 50 共享资料的黑币奖励!

查看全部评分

回复

使用道具 举报

ID:84652 发表于 2020-4-10 11:50 | 显示全部楼层
本帖最后由 王朗的诱惑 于 2020-4-14 22:30 编辑

之前上传失败了…… 状态机按键20200409bak.zip (1.12 MB, 下载次数: 38)
回复

使用道具 举报

ID:626079 发表于 2020-4-14 15:28 | 显示全部楼层
大侠真的太历害得不要不要的。 佩服,啥时候有楼主一半的水平就可以了。
回复

使用道具 举报

ID:84652 发表于 2020-4-14 22:35 | 显示全部楼层
xmfjfhcel 发表于 2020-4-14 15:28
大侠真的太历害得不要不要的。 佩服,啥时候有楼主一半的水平就可以了。

哈哈,多谢夸奖。我也不聪明啊,只是努力并坚持下来,玩单片机快6年了,我能做到的,你也一定能,说不定还能做得更好。
回复

使用道具 举报

ID:137736 发表于 2022-9-1 18:06 | 显示全部楼层
很好,可以参考,不过太消耗资源了
回复

使用道具 举报

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

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表