0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

高电平稳定时该如何处理呢?

lhl545545 来源:玩转单片机 作者:玩转单片机 2020-06-10 14:28 次阅读

相信很多道友都有对输入IO 口进行滤波的需求,比如按键输入、红外对管输入等。这里鱼鹰就以按键为例介绍如何进行较为高效的滤波。我们以为接入单片机引脚的按键按下后(并弹起)电平变化应该是这样的:

高电平稳定时该如何处理呢?

实际上却是这样的:

高电平稳定时该如何处理呢?

首先思考一个问题,如果没有进行滤波,会有什么问题?一次按下过程可能被认为多次按下,因为按下后有抖动过程,这个过程电平并不稳定,导致单片机在很短的时间内多次检测到低电平状态。这样一来,本来只按下了一次,程序却认为按下了多次,这对按键功能会产生影响。如果将按键引脚设置为外部中断触发,那么在极短的时间内CPU将多次进入中断,影响中断的性能(所以对于非数字接口,即没有稳定的高低电平的接口,如果不需要非常高的实时性,那么鱼鹰不建议设置为外部中断触发方式)。那么我们该如何进行处理呢?很自然的,因为按下过程中有抖动期,我们就会想办法跳过抖动时间,然后再检测电平变化,所以,V0.1 版本就应运而生,这也是郭天祥老师告诉我们初学者最简单易懂的方式:V0.1

typedef enum { KEY_LEVEL_DOWN, // 假设低电平为按下 KEY_LEVEL_UP, }KeyLevelTypedef; KeyLevelTypedef get_key_level(){ return (KeyLevelTypedef)HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); } // V0.1void key_scan(){ // 欢迎关注:鱼鹰谈单片机 if(get_key_level() == KEY_LEVEL_DOWN) { HAL_Delay(20); // 假设抖动时间 20 ms if(get_key_level() == KEY_LEVEL_DOWN) { key_flag = 1;// 按键按下标志位 } } }对于初学者而言,这段代码简单易懂,但是对于工作多年的人来说,这种方式效率极其低下。有可能你会说,如果使用操作系统,当延时函数使用系统延时,那么这段时间就可以切换到其他任务进行处理,而不会浪费CPU使其空转了。但是如果这个任务本身功能比较复杂,那么这种处理会严重影响其他功能的执行,所以这种代码应该不会出现在工作多年的工程师手中。那么是否有更高效的方式呢?有,就是记录前后两次电平的变化,通过比较两次电平是否相等来确定电平是否稳定(这个方式在《延时功能进化论(合集)》有做简单介绍)。V1.0

typedef enum { KEY_STATE_IDLE, // 按键空闲 KEY_STATE_DOWN, // 按键按下 KEY_STATE_FINISH, // 按键处理完成(由应用程序设置) }KeyStateTypedef; KeyStateTypedef key_state;KeyLevelTypedef key_last_level; // 上次电平状态 // V1.0// 函数调用周期 20 ms(如何实现应该不需要再说明了吧)void key_scan(){ // 欢迎关注:鱼鹰谈单片机 KeyLevelTypedef temp; // 可不可以不使用这个中间变量? temp = get_key_level(); if(temp != key_last_level){ key_last_level = temp; return; } // 当运行到这里,说明电平已经稳定下来了 if(temp == KEY_LEVEL_DOWN){if(key_state == KEY_STATE_IDLE){ // 确保曾经释放过按键,这样可以保证在按下时不会不停设置该标志位 key_state = KEY_STATE_DOWN;// 按键按下标志位 } } else{ if(key_state == KEY_STATE_FINISH){ // 防止多线程情况下同时修改 key_state = KEY_STATE_IDLE; // 释放按键 } }}在这里,使用了两个全局变量,一个是 key_state,一个是 key_last_level。前者共三种状态,这是为了防止按键扫描和按键处理程序不是顺序执行而设定的。当你按下按键后,保证按键处理程序必然可以得到按下状态,同时只有释放了按键之后才可以更改状态位,然后才能再次触发。这样可以保证按键扫描和按键处理得以顺序执行(这里面的关系需要考虑清楚,否则的就会写出有 BUG 的程序)。而后者只在本函数使用,所以不存在使用风险(前提是没有多个任务同时调用该函数,否则照样有风险)。可以看到该代码没有任何延时函数,简单、高效,当然这里有一个前提,那就是该函数的调用周期必须大于抖动时间,但是也别太长,否则实时性不好。假设抖动期时间为 20 ms,实现 20 ms 的调用周期有很多种方法:1、中断定时器定时调用2、软件定时器调用(需操作系统)3、线程内周期执行该函数(需操作系统)4、使用鱼鹰介绍的方式(《延时功能进化论之V2.5~V2.7(鱼鹰强烈建议)》)我们再次看这个图:

高电平稳定时该如何处理呢?

如果我们使用 V1.0 的方式,我们就会发现,当程序运行在抖动期,因为函数调用的时间大于抖动时间,那么程序总是可以得到稳定后的状态。比如空闲状态下(key_last_level为高电平),突然按下按键,假设在抖动中期程序检测到高电平,那么20 ms 后检测的是低电平,显然这是不相等的(key_last_level更新为低电平),那么程序就会执行下一次,下一次即40 ms 后检测肯定是低电平(如果不是,说明电平不稳定),此时电平相等,即可认为电平稳定了。而如果在抖动中期程序检测到低电平,那么20 ms 后检测的应该还是低电平,那么程序认为此时电平已经稳定了,那也没有问题,因为它已经跳过了抖动期。V2.0如果说,滤波只有按键这种抖动的话,那么上述方式应该算很不错了,但有时对IO滤波的需求比较复杂,那么上述方式只可参考,不可直接拿来对任何 IO 进行滤波。而且很多时候,程序需要定时检测多个 IO 的电平状态,当电平发生变化时,我们能及时通知应用层,而且只在电平发生变化时才进行通知。但与此同时我们需要在电平稳定之后才通知,而不是变化后马上进行通知,否则可能在电平抖动时多次通知。所以针对这种需求,我们需要设计一个更加通用一些的滤波函数,能应对所有数字 IO 的滤波(包括按键)。其实按键滤波已经包含了滤波思想,只是不够通用,需要进一步改进。

typedef enum { LEVEL_LOW, // LEVEL_HIGH, }LevelTypedef;

typedef struct { uint32_t last_time; // 上次时间 LevelTypedef last_level; // 上次电平状态}FilterParaTypedef;

// V2.0// para 滤波变量,level 当前检测电平状态, time 当前时间戳,单位 1 ms, stable_time希望电平稳定的时间uint8_t filter(FilterParaTypedef *para, LevelTypedef level, uint32_t time, uint32_t stable_time){ // 欢迎关注:鱼鹰谈单片机 if(level != para-》last_level){ para-》last_level = level; // 更新当前电平状态 para-》last_time = time; // 更新电平变化的时刻 return 0; // 电平未稳定 } if(time - para-》last_time 》 stable_time){ // 这两个条件可以放在一起进行 && 判断吗? return 1; // 需要上报 } return 0; // 电平稳定时间不够长}

这个代码的思想就是,当电平不稳定时,更新当前时间戳,一旦电平不再变化,并且持续的时间够长(这个时间由用户决定),那么返回 1 表示电平已经稳定了(这个函数没有调用周期限制,调用周期不同,会产生一些影响,这个和滤波时间精度有关)。这个代码看起来挺简单的,也好像没啥问题,但实际上是存在问题的。看到那个稳定时间判断条件了吗?如果下次继续执行这个函数,那么程序依然返回 1,所以它总是会在稳定后不停的返回 1(判断条件总是成立),这样一来,这个函数并不能实现电平变化后才进行通知,也就是说调用者无法通过返回值直接决定下一步动作。可能你会说,如果在返回 1 之前先更新一下时间戳呢?看过鱼鹰之前笔记的应该知道,这种方式会周期性返回 1,即如果希望电平稳定时间为 10 ms,那么在电平稳定后,每隔 10 ms 返回 1,这是我们不希望看到的。那么有没有什么解决办法呢?当然。因为我们只希望在变化之后再稳定时才返回1,即我们既希望短暂电平变化并不返回1,而那些长时间稳定的电平能在稳定时间阈值之后返回1,又希望在稳定之后只返回一次1,之后电平变化后如果再次稳定才返回1。有点绕口,看图好了:

高电平稳定时该如何处理呢?

因为目前判断条件总是返回1,所以我们需要增加限制条件,让它不总是返回1。简单的办法是,增加一个变量,用于记录上次的稳定后的电平,比如这样:

typedef enum { LEVEL_LOW, // LEVEL_HIGH, }LevelTypedef;

typedef struct { uint32_t last_time; // 上次时间 LevelTypedef last_level; // 上次电平状态 LevelTypedef last_stable_level; // 上次稳定的电平状态 }FilterParaTypedef;

// V2.0// para 滤波变量,level 当前检测电平状态, time 当前时间戳,单位 1 ms, stable_time希望电平稳定的时间uint8_t filter(FilterParaTypedef *para, LevelTypedef level, uint32_t time, uint32_t stable_time){ // 欢迎关注:鱼鹰谈单片机 if(level != para-》last_level){ para-》last_level = level; // 更新当前电平状态 para-》last_time = time; // 更新电平变化的时刻 return 0; // 电平未稳定 } if(time - para-》last_time 》 stable_time){ // 这两个条件可以放在一起进行 && 判断吗? if(level != para-》last_stable_level) { // 电平稳定时间够长且电平发生了变化 para-》last_stable_level = level; return 1; // 需要上报 } } return 0; // 电平稳定时间不够长}

这样一来,下一次继续执行时,就不会再次返回1了。但是以上代码其实是有一个隐含问题的,那就是如果两次长时间电平之间有一个短时间的不同电平存在,那么也只会上报一次,即返回一次1,即如下情况:

高电平稳定时该如何处理呢?

如果说这是你想要的效果,那么恭喜你,你不用更改代码;但如果这不是你想要的结果,那这个代码就存在BUG,毕竟变化的时间虽然短,但还是变化了的嘛(这个问题稍后讨论)。还有一个问题,看过鱼鹰以前笔记的人都知道,这种计时方式是存在问题的,因为如果你的电平稳定时间很长,长到四字节计时器溢出了,那么就可能出问题。不过在这里,即使出现溢出,也没关系,结果是一样的,因为如果电平稳定时间很长了,那么肯定已经上报过一次了,后面肯定也不需要再次上报了。V2.5V2.0方式确实很高效,但是为了只在变化时上报一次,就要增加一个变量还是很不爽的,如果说鱼鹰没有找到好的方式,那么鱼鹰会采用的,但凑巧的是,鱼鹰想到了更好的方式,不需要增加这个变量也能达到效果。一个用于计时,一个用于记录上次电平,这两个变量肯定是不可或缺的。但是如果你仔细思考一下,就会发现,所谓的记录上次电平,其实是在变化时就被快速更改了的,它记录的是实时电平变化,而计时是在变化后更新时间戳,稳定时判断稳定时间,如果我们把计时顺序换一下,会如何呢?即,稳定时更新时间戳,变化时判断稳定时间,而记录电平的变量只记录已稳定的电平,会怎么样?

typedef struct { uint32_t last_time; // 上次时间 LevelTypedef last_stable_level; // 上次稳定的电平状态 }FilterParaTypedef;

// V2.5// para 滤波变量,level 当前检测电平状态, time 当前时间戳,单位 1 ms, stable_time希望电平稳定的时间uint8_t filter(FilterParaTypedef *para, LevelTypedef level, uint32_t time, uint32_t stable_time){ // 欢迎关注:鱼鹰谈单片机 if(level != para-》last_stable_level){ if(time - para-》last_time 》 stable_time) { para-》last_stable_level = level; // 如果这次电平稳定时间足够长,那么记录这次稳定的电平 return 1; // 上报 } return 0; // 不上报,同时不更新时间戳(稳定时间不够) }

para-》last_time = time; // 不断更新电平稳定时间,保存电平稳定时的时间戳 return 0; // 不上报}

上面的代码比V2.0简单了许多,但也稍微难理解,但如果你仿真测试一番,其实也容易理解。测试代码(rt_tick_get() 函数用于获取当前时间,单位 ms):

FilterParaTypedef FilterPara;

void task(void *parameter){ while(1) { LevelTypedef temp = (LevelTypedef)HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); if(filter(&FilterPara, temp, rt_tick_get(), 100)) { rt_kprintf(“stable level is %u

”,temp); } rt_thread_delay(5); } }

当你修改PB0电平时,可得到如下测试结果:

高电平稳定时该如何处理呢?

在这个例子中,要求电平稳定时间20 ms,而线程的执行周期为 5 ms,即电平采样率为5 ms,当你的手速点击足够快时(如果不够快,可以加长 20 ms),那么应该不会有任何打印信息输出。需要注意的是,采样率比较关键,如果电平变化快,而采样率设置的不合适,那么不能完全反应外界引脚电平的变化,这个和“香农定理”有关,超出鱼鹰的范围,就不多说了。V3.0有的时候需求可能要求只需要稳定一个高电平或者低电平才上报,其他时候不上报,那么该如何修改V2.5的代码呢?上报时加入限制条件即可,如下所示:

// V3.0// para 滤波变量,level 当前检测电平状态, time 当前时间戳,单位 1 ms, stable_time希望电平稳定的时间uint8_t filter(FilterParaTypedef *para, LevelTypedef level, uint32_t time, uint32_t stable_time){ // 欢迎关注:鱼鹰谈单片机 if(level != para-》last_stable_level){ if(time - para-》last_time 》 stable_time) { para-》last_stable_level = level; // 如果这次电平稳定时间足够长,那么记录这次稳定的电平 if(level == LEVEL_HIGH) // LEVEL_HIGH 可以作为 para 的成员变量参数传入,方便适应其他电平 { return 1; // 上报 } } return 0; // 不上报,同时不更新时间戳(稳定时间不够) }

para-》last_time = time; // 不断更新电平稳定时间,保存电平稳定时的时间戳 return 0; // 不上报}

这样一来,只会在高电平稳定时才会进行上报,而低电平却不会上报。但是这种方式同样有一个隐藏限制,那就是低电平必须能稳定一段时间,否则下次高电平无法上报,照样有 V2.0 的限制,如何打破这种限制呢?V3.1如果我们的需求是,变化后高电平稳定时上报一次,如果之后存在低电平,然后又变为高电平,并且稳定了,那么希望也能上报,那该如何处理呢?

高电平稳定时该如何处理呢?

代码如下:

// V3.1// para 滤波变量,level 当前检测电平状态, time 当前时间戳,单位 1 ms, stable_time希望电平稳定的时间uint8_t filter(FilterParaTypedef *para, LevelTypedef level, uint32_t time, uint32_t stable_time){ // 欢迎关注:鱼鹰谈单片机 if(level != para-》last_stable_level){ if(level != LEVEL_HIGH) // LEVEL_HIGH 可以作为 para 的成员变量参数传入,方便适应其他电平 { para-》last_stable_level = level; // 快速切换状态 // para-》last_time = time; // 是否有必要同时更新时间戳呢? } else if(time - para-》last_time 》 stable_time) { para-》last_stable_level = level; // 如果这次电平稳定时间足够长,那么记录这次稳定的电平 if(level == LEVEL_HIGH) // LEVEL_HIGH 可以作为 para 的成员变量参数传入,方便适应其他电平 { return 1; // 上报 } } return 0; // 不上报,同时不更新时间戳(稳定时间不够) }

para-》last_time = time; // 不断更新电平稳定时间,保存电平稳定时的时间戳 return 0; // 不上报}

V3.2为了让这个滤波代码(事实上已经不仅仅承担滤波功能,同时承担了变化并稳定后上报功能)更加通用,可以这样设计:

typedef enum { LEVEL_LOW, // LEVEL_HIGH, }LevelTypedef;

typedef struct { uint32_t last_time; // 上次时间 LevelTypedef last_stable_level; // 上次稳定的电平状态 LevelTypedef filter_level; // 希望滤波的电平}FilterParaTypedef;

// V3.2// para 滤波变量,level 当前检测电平状态, time 当前时间戳,单位 1 ms, stable_time希望电平稳定的时间uint8_t filter(FilterParaTypedef *para, LevelTypedef level, uint32_t time, uint32_t stable_time){ // 欢迎关注:鱼鹰谈单片机 if(level != para-》last_stable_level){ if(level != para-》filter_level) // LEVEL_HIGH 可以作为 para 的成员变量参数传入,方便适应其他电平 { para-》last_stable_level = level; // 快速切换状态 // para-》last_time = time; // 是否有必要同时更新时间戳呢? } else if(time - para-》last_time 》 stable_time) { para-》last_stable_level = level; // 如果这次电平稳定时间足够长,那么记录这次稳定的电平 if(level == para-》filter_level) // LEVEL_HIGH 可以作为 para 的成员变量参数传入,方便适应其他电平 { return 1; // 上报 } } return 0; // 不上报,同时不更新时间戳(稳定时间不够) }

para-》last_time = time; // 不断更新电平稳定时间,保存电平稳定时的时间戳 return 0; // 不上报}

因为函数没有全局变量,所以可以认为它是一个可重入函数(前提是传入的参数指针地址不同),可放心使用。
责任编辑:pj

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 单片机
    +关注

    关注

    6032

    文章

    44516

    浏览量

    633032
  • 滤波
    +关注

    关注

    10

    文章

    662

    浏览量

    56594
  • 高电平
    +关注

    关注

    6

    文章

    146

    浏览量

    21333
收藏 人收藏

    评论

    相关推荐

    DAC7744用作TMS320F2812的DAC时是否需要将2812输出的高电平转化为5v的高电平

    请问DAC7744用作TMS320F2812的DAC时是否需要将2812输出的高电平转化为5v的高电平?谢谢!
    发表于 11-15 06:57

    高电平输入和低电平输入是什么意思

    在现代电子系统中,数字电路扮演着至关重要的角色。这些电路处理的是二进制信号,即由逻辑“1”和逻辑“0”组成的信号。这些逻辑状态通常通过电压水平来表示,其中高电平代表逻辑“1”,低电平代表逻辑“0
    的头像 发表于 10-17 14:56 1330次阅读

    rca输出是低电平还是高电平

    (黄色插头)。这些连接器通常用于连接家庭影院系统、音响设备、游戏机和其他多媒体设备。 关于RCA输出是低电平还是高电平,这实际上是一个关于信号电平的问题。在电子学中,电平通常指的是信号
    的头像 发表于 10-17 11:01 544次阅读

    芯片引脚悬空是高电平还是低电平

    芯片引脚悬空时的电平状态(高电平或低电平)并不是一个固定答案,它取决于多个因素,包括芯片类型、生产厂家、引脚特性以及周围电路环境等。 首先,从逻辑门电路的角度来看,当引脚悬空时,其电平
    的头像 发表于 08-28 09:55 1776次阅读

    INA-138设计了一款电路,取样后1脚的输出不是一个稳定电平,如何处理

    设计了一款电路,使用到INA-138 ,SOT-23封装,取样后1脚的输出不是一个稳定电平,如何处理
    发表于 08-12 06:51

    与非门的闲置输入端如何处理

    在数字电路设计中,与非门(NAND gate)是一种基本的逻辑门,它具有两个或多个输入端和一个输出端。当所有输入端都为高电平时,输出端为低电平;当至少有一个输入端为低电平时,输出端为高电平
    的头像 发表于 07-30 14:47 1268次阅读

    高电平和低电平输入有什么区别

    Level)通常指电压高于某个阈值的电平状态,而低电平(Low Level)则指电压低于阈值的电平状态。在数字电路中,高电平和低
    的头像 发表于 07-23 11:25 4241次阅读

    请问stm8的空脚不用何处理

    我现在没用的空脚没有设置输入输出模式也没外接上拉电阻或电容,每当我去触碰这个空脚时都会引起单片机复位。加了个104就好了,但是文档上说要接上下拉电阻,我试了管脚也会复位。那究竟stm8不用的管脚何处理?
    发表于 05-08 06:00

    播放avi视频时,音频数据何处理啊 ?

    请教下,播放 avi 视频时,音频数据何处理啊 ???
    发表于 05-06 07:56

    STM8L spi通信的单线双向通信,何处理收和发

    如题,SPI通信中,在手册和库中都有描述,但是在具体使用中,何处理收和发?貌似这就是传说中的三线SPI,有人能指点下么,谢谢
    发表于 04-28 08:01

    STM32F103RCT6定时器采集高电平数据有波动的原因?

    通过STM32F103RCT6需要采集3路方波的高电平,没有使用输入捕获采集(输入捕获是特定的引脚,比较麻烦)采集的方波的周期是7MS频率145HZ,通过定时加外部中断的方式实现了采集,由于对采集
    发表于 04-15 06:51

    STM32的IO口初始化为输入浮空,那管脚是高电平还是低电平

    STM32的IO口初始化为输入浮空,那管脚是高电平还是低电平
    发表于 04-07 07:12

    AD7606 Busy信号不稳定的原因?如何处理

    AD7606的Busy信号,有时候上电后有,有时候上电后没有。如果有,就一直有Busy信号,正常,高电平大约4us。如果没有Busy信号,就一直没有,一直是低电平。 我输入给ConvertA
    发表于 12-14 06:05

    什么是串扰?何处理它?

    什么是串扰?何处理它?
    的头像 发表于 12-05 16:39 797次阅读
    什么是串扰?<b class='flag-5'>该</b>如<b class='flag-5'>何处理</b>它?

    何处理同轴阻抗失配?如何避免阻抗失配这种风险

    何处理同轴阻抗失配?如何避免阻抗失配这种风险? 同轴阻抗失配是电子通信领域中一种常见的问题,当同轴电缆的输出端口的阻抗与接收端口不匹配时,就会发生阻抗失配。这种失配会导致信号反射、传输效率降低
    的头像 发表于 11-28 14:18 1051次阅读