当前位置:首页 > > 小麦大叔
[导读]相信很多道友都有对输入IO 口进行滤波的需求,比如按键输入、红外对管输入等。本文以按键为例,简单介绍了如何进行较为高效的滤波。

来源:公众号【鱼鹰谈单片机】
作者:鱼鹰Osprey
ID   : emOsprey

相信很多道友都有对输入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.1 void 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使其空转了。但是如果这个任务本身功能比较复杂,那么这种处理会 严重影响其他功能的执行,所以这种代码应该不会出现在工作多年的工程师手中。
那么是否有更高效的方式呢?
有,就是记录前后两次电平的变化,通过比较两次电平是否相等来确定电平是否稳定(这个方式在《延时功能进化论(合集)》有做简单介绍)。
V 1 . 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 后检测的应该还是低电平,那么程序认为此时电平已经稳定了,那也没有问题,因为它已经跳过了抖动期。
V 2 . 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,毕竟变化的时间虽然短,但还是变化了的嘛(这个问题稍后讨论)。
还有一个问题,看过鱼鹰以前笔记的人都知道,这种计时方式是存在问题的,因为如果你的电平稳定时间很长,长到四字节计时器溢出了,那么就可能出问题。
不过在这里,即使出现溢出,也没关系,结果是一样的,因为如果电平稳定时间很长了,那么肯定已经上报过一次了,后面肯定也不需要再次上报了。
V 2 . 5
V2.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\n",temp); } rt_thread_delay(5); } }
当你修改PB0电平时,可得到如下测试结果:
在这个例子中,要求电平稳定时间20 ms,而线程的执行周期为 5 ms,即电平采样率为5 ms,当你的手速点击足够快时(如果不够快,可以加长 20 ms),那么应该不会有任何打印信息输出。
需要注意的是,采样率比较关键,如果电平变化快,而采样率设置的不合适,那么不能完全反应外界引脚电平的变化,这个和“香农定理”有关,超出鱼鹰的范围,就不多说了。
V 3 . 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 的限制,如何打破这种限制呢?
V 3 . 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; // 不上报}
V 3 . 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; // 不上报}
因为函数没有全局变量,所以可以认为它是一个可重入函数(前提是传入的参数指针地址不同),可放心使用。
以上就是鱼鹰能想到的比较高效的滤波方式,可以参考借鉴一番。当然,如果你有更好的方式,不如留言交流。


—— The End —

推荐好文   点击蓝色字体即可跳转
 感觉身体被掏空!只因为肝了这篇空间矢量控制算法
 当心!别再被大小端的问题坑了
 PID微分器与滤波器的爱恨情仇
 简易PID算法的快速扫盲
 增量式PID到底是什么?
 三面大疆惨败,因为不懂PID的积分抗饱和

原创不易,欢迎转发、留言、点赞、分享给你的朋友,感谢您的支持!


长按识别二维码关注获取更多内容


免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭