开关检测二三事:端口不足,滤波时间不同

  作者:驴三 时间:2020-03-18来源:电子产品世界

熟悉洒家写作风格的朋友们都知道,洒家行文一向生动活泼,甚而有时浮夸得没个章法。但是,在这个全民战疫的关键时刻,似乎任何的轻佻都是对前线战士的不恭敬。故而,今天严肃紧张一把,跟大家开门见山,讲一讲开关检测的问题。

有的朋友可能会觉得,开关检测对于每一个嵌入式工程师来讲都是入门级别的问题,有什么好讲的呢?好吧,对于你这种想法,我只能像春晚上晓明哥哥对祖儿妹妹讲的那样:

我不要你觉得,我要我觉得!

问你三个问题吧。第一,如果因为这样或那样的原因,您选用的MCU的IO口不够,无法一一对应地处理那么多路开关信号,你该咋个办?

第二,如何区分开关的“动作”和“状态”?按下和弹起的动作一闪即逝,状态却长期保持,怎么区分并处理?

第三,是关于大家耳熟能详的滤波问题,您可不要说只需要进行硬件滤波就够了这种跌份的话哈。假设您的开关信号性质有所不同,它们需要的滤波时间也不一样,你怎么以一种统一的方式去处理他们呢?

今天洒家和大家分享的就是关于这三个问题的想法和解决方案。

要想逃避现实,最好的方式就是深深介入现实之中。

嵌入式工程师的日常工作不是在锦绣河山里做文章,而是在螺丝壳里做道场。在日复一日的工作中,工程师逐渐积累了丰富的实战经验。比如硬件不够可以用软件来凑,内存不足可以牺牲实时性,以时间换空间。

那么,IO口不够呢?

鲁迅先生曾说:“希望是本无所谓有,无所谓无的。这正如地上的路,其实地上本没有路,走的人多了,也便成了路。”

勇敢的嵌入式工程师是遇山开路、遇水搭桥的开拓者,面对无路之境,自会以大无畏的霹雳手段趟出一条路来。IO口不够,MCU又不能换(想一想老板那冷飕飕的目光),自然也会另辟蹊径,柳暗花明,不至于山穷水复,找不到出路的。

聪明的小伙伴们应该已经跃跃欲试,准备抢答了。不过,授人以鱼不如授人以渔,在直接给出答案之前,大家可以先想一个问题:为什么现在大家用的计算机,基本上没有并口了呢?

在计算机刚刚出现的那个年代里,打印和绘图是非常重要的应用。由于计算机速度的限制,串行传输速度相当有限,无法应对打印绘图这种需要高速数据传输的应用,于是,并口大行其道数十载。说句不怕暴露年龄的话,笔者刚上班时,单位的台式机和笔记本都是有并口的。

当然,鱼与熊掌不可兼得,并行传输也有其缺点,那就是需要的端口和线路远高于串行传输,会消耗较高的电路板资源和软件解析能力,所以,随着计算机和串行传输速度的提升,并口也开始慢慢地退出了历史的舞台。

触类旁通,见微知著,大家是不是品出来什么了?

对,在嵌入式设计中,一个MCU端口处理一路开关信号是“并行处理方式”,类比于计算机并口,需要消耗较多的端口。端口不够的解决方案自然是“并行转串行”,以串行的方式进行开关信号的检测。

在具体的实现上,需要选择“多路开关检测接口芯片”,这种芯片可以检测多路开关量输入信号,并将检测到的开关状态通过SPI发送给MCU。这种方式可以极大地节省MCU的IO口资源,比如说检测16路开关,并行方式需要16个MCU IO端口,串行方式只需要一个SPI端口就可以了。

话不多说,再来看第二个问题:怎么区分开关的“动作”和“状态”?

至于为什么要区分“动作”和“状态”。是因为在嵌入式产品中,有一种很常见的应用逻辑:开关A、B、C处于闭合状态且开关D、E、F处于断开状态时,按下或松开开关G,执行某个操作。

在这种逻辑里,“按下”和“松开”是两种动作,“闭合”和“断开”是两种状态。用电路的知识来类比的话,动作是沿跳变,状态是电平。

“动作”是一闪即逝的花火,状态是千年不变的承诺。我们做区分为的是,让动作“阅后即焚”,不至于成为反复触发操作的脉搏。

为了说明这一点,洒家跟大家分享一下自己设计的结构体和代码实现,这部分也可以用在对第三个问题的解答上。

typedef struct{

     unsigned switch_state:1;

     unsigned swon_event:1;

     unsigned swoff_event:1;

     unsigned cursw:1;

     unsigned detect_cnt:4;

     e_SwId   switch_id;

}s_Switch;

在这个结构体的成员变量里面,switch_id标识开关节点,大家可以用“解释性”很强的枚举来表示它。这里的switch_state表示的是开关信号的状态,swon_event和swoff_event分别表示开关从断开到闭合和从闭合到断开的变化,即上述的“动作”。 cursw和detect_cnt用于开关信号采集的软件消抖功能。

为了同时检测开关状态和动作,可以设置一个10ms的周期定时器,周期性地对每个SWITCH_ID对应的开关信号进行检测,具体实现为:

void SwDetect(e_SwId sw_id)  

{

    uint8_t filter_time;

    filter_time = SW_DETECT_TIMES;

    if(SWITCH_OFF == Sw[sw_id].switch_state){

        if(IOVALID == Sw[sw_id].cursw){

            if(Sw[sw_id].detect_cnt < filter_time){

                Sw[sw_id].detect_cnt++;   

            }else{                                          

                Sw[sw_id].switch_state = SWITCH_ON;       

                Sw[sw_id].detect_cnt = 0;                      

                Sw[sw_id].swon_event = 1;          

            }

        }else{

            Sw[sw_id].detect_cnt = 0;

        }

    }else{

        if(IOINVALID == Sw[sw_id].cursw){

            if(Sw[sw_id].detect_cnt < filter_time){

                Sw[sw_id].detect_cnt++;

            }else{                                                        

                Sw[sw_id].switch_state = SWITCH_OFF;

                Sw[sw_id].swoff_event = 1;

                Sw[sw_id].detect_cnt = 0;

            }

        }else{

            Sw[sw_id].detect_cnt = 0;

        }

    }

}

当开关动作发生时,swon_event和swoff_event置一,在执行完相关操作之后,将swon_event和swoff_event清零,就完成了让动作“阅后即焚”。

所以,上面那种根据某些开关的状态和动作执行相关操作的逻辑的具体实现为:

If(Switch[SWITCH_ID_1].swon_event == 1)

{

If(Switch[SWITCH_ID_2].switch_state == “ON”){

    操作1

}

Switch[SWITCH_ID_1].swon_event = 0

}

下面接着讲第三个问题:怎么应对不同的滤波时间?

正如上面讲过的那样,对于一般的开关节点,设计一10ms的定时器周期性地读取开关当前状态cursw,然后根据其维持当前状态的周期次数(根据不同应用场景,可以设置为5次或者10次,分别对应50ms或100ms的滤波时间)以判断switch_state、swon_event、swoff_event。

那么,对于那些特殊的开关信号,也许需要采用较典型值长或者短的消抖时间,我们只需要针对该开关信号对应的那个SWITCH_ID表征的结构体变量,设置它的滤波次数filter_time(见上面那段程序)即可。

讲到这里,有些不爱看代码的同学可能模糊了,这里,帮人帮到底,洒家不惜笔墨,详细开展一番。

首先,设定一个10ms的定时器,在它的中断服务程序里,执行开关信号检测。

对应在我们这里,可以认为它的中断服务程序(ISR)执行的就是下面这个IoInputDetect函数。(需要说明的是,一般情况下我们不会在中断服务程序里执行这种耗时较长的程序,这里只是为了方便大家理解)

void IoInputDetect(void)

{

    e_SwId sw_idx;

    ReadIoSwitch();

    for(sw_idx = MIN_SWITCH;sw_idx < MAX_SWITCH;sw_idx++){

        SwDetect(sw_idx);   

    } 

}

这个函数里面,在ReadIoSwitch函数里面读取每个开关(以SWITCH_ID标识)的当前状态,赋给其cursw,需要注意的是,这里的cursw表示的是当下这一刻的开关状态,不是经过滤波处理后的稳定开关状态。

第二步:根据每个开关的当前状态cursw,判断其稳定的开关状态switch_state、开关动作swon_event和swoff_event。即上面在for循环中执行的SwDetect函数。

SwDetect函数语句在第二节中,它的核心思想就是判断开关当前状态cursw是否持续稳定在SWITCH_ON或者SWITCH_OFF状态。当前的switch_state为ON的状态下,如果持续filter_time个10ms,cursw一直为OFF状态,则将switch_state赋为OFF状态,同时,将swoff_event赋为1。反之亦然。

当滤波时间不同时,显然只需要将该switch_id对应的开关结构体的filter_time置为不同于典型值的特殊值即可。

后记

洒家在这篇文章里面分享的开关检测方法,不止适用于数字IO形式的开关信号,还适用于其它信号。

比如通过RF方式接收的遥控信号,虽然是一种射频性质的信号,但是这种信号对应的是遥控器上的物理按键,它在逻辑上自然也等价于本文讲的开关信号,所以,可以用上述那个结构体和那些代码判断遥控信号,解析出某个遥控按键按下、松开的动作和状态,同时对它进行滤波处理。

再举一反三,无论是RF信号、模拟信号、数字信号、网络信号,只要该输入信号在逻辑上可以等价于物理开关,它就可以使用本文所述的方法处理。

你觉得呢?

关键词:

加入微信
获取电子行业最新资讯
搜索微信公众号:EEPW

或用微信扫描左侧二维码

相关文章


用户评论

请文明上网,做现代文明人
验证码:
查看电脑版