当前位置:首页 > 公众号精选 > 小麦大叔
[导读]相信很多道友都有对输入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获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

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

单片机是一种嵌入式系统,它是一块集成电路芯片,内部包含了处理器、存储器和输入输出接口等功能。

关键字: 单片机 编写程序 嵌入式

在现代电子技术的快速发展中,单片机以其高度的集成性、稳定性和可靠性,在工业自动化、智能家居、医疗设备、航空航天等诸多领域得到了广泛应用。S32单片机,作为其中的佼佼者,其引脚功能丰富多样,是实现与外部设备通信、控制、数据...

关键字: s32单片机引脚 单片机

在微控制器领域,MSP430与STM32无疑是两颗璀璨的明星。它们各自凭借其独特的技术特点和广泛的应用领域,在市场上占据了重要的位置。本文将深入解析MSP430与STM32之间的区别,探讨它们在不同应用场景下的优势和局限...

关键字: MSP430 STM32 单片机

该系列产品有助于嵌入式设计人员在更广泛的系统中轻松实现USB功能

关键字: 单片机 嵌入式设计 USB

单片机编程语言是程序员与微控制器进行交流的桥梁,它们构成了单片机系统的软件开发基石,决定着如何有效、高效地控制和管理单片机的各项资源。随着微控制器技术的不断发展,针对不同应用场景的需求,形成了丰富多样的编程语言体系。本文...

关键字: 单片机 微控制器

单片机,全称为“单片微型计算机”或“微控制器”(Microcontroller Unit,简称MCU),是一种高度集成化的电子器件,它是现代科技领域的关键组件,尤其在自动化控制、物联网、消费电子、汽车电子、工业控制等领域...

关键字: 单片机 MCU

STM32是由意法半导体公司(STMicroelectronics)推出的基于ARM Cortex-M内核的32位微控制器系列,以其高性能、低功耗、丰富的外设接口和强大的生态系统深受广大嵌入式开发者喜爱。本文将详细介绍S...

关键字: STM32 单片机

在当前的科技浪潮中,单片机作为嵌入式系统的重要组成部分,正以其强大的功能和广泛的应用领域受到越来越多行业的青睐。在众多单片机中,W79E2051以其卓越的性能和稳定的工作特性,成为市场上的明星产品。本文将深入探讨W79E...

关键字: 单片机 w79e2051单片机

单片机,又称为微控制器或微处理器,是现代电子设备中的核心部件之一。它集成了中央处理器、存储器、输入输出接口等电路,通过外部信号引脚与外部设备进行通信,实现对设备的控制和管理。本文将详细介绍单片机的外部信号引脚名称及其功能...

关键字: 单片机 微控制器 中央处理器

随着科技的飞速发展,单片机和嵌入式系统在现代电子设备中的应用越来越广泛。它们不仅提高了设备的智能化水平,还推动了各行各业的创新与发展。在单片机和嵌入式系统的开发中,编程语言的选择至关重要。本文将深入探讨单片机和嵌入式系统...

关键字: 单片机 嵌入式系统 电子设备
关闭
关闭