拓冰建站拓冰建站
首页 / 资讯中心 / 正文

嵌入式系统中Delay的妙用:捕捉瞬时边沿、去抖与脉冲展宽

做控制的年头多了你会发现很多问题不是算法多复杂而是系统根本没有在正确的时间点看到信号。我调试过一套自动化设备的IO输入板卡客户反馈说设备偶尔会漏检一个传感器信号现象是故障率大约千分之一特别难复现。示波器挂上去抓了半天终于逮到那个“幽灵事件”传感器输出的有效脉冲宽度只有80微秒而控制器的输入检测任务每2毫秒才扫描一次。80微秒的脉冲落进2毫秒的扫描周期里绝大多数情况下都被采样点完美错过。这个案子最后不是靠复杂的算法解决的而是靠着正确使用Delay——让底层把这根线上的瞬间边沿可靠地捕捉下来再告诉上层ASW“刚才发生了一件事”。这篇我想聊的核心就一句话Delay在嵌入式系统里不只是用来“拖延时间”的它是捕捉瞬时边沿的关键手段。如果你平时写驱动、调接口或者负责把底层信号抽象成语义事件交给应用层软件ASW使用这篇文章应该对你有用。我会从边沿为什么会丢讲起拆解Delay在采样、去抖、展宽中的三个角色最后给出可以直接抄的代码和排查经验。1. 边沿捕捉的底层逻辑为什么瞬间事件容易被系统漏掉1.1 边沿到底是什么从电平跳变说起边沿这个词做硬件或者嵌软的都不会陌生。数字信号不是凭空出现的不管是按键按下、编码器旋转、还是通信总线上的一个起始位它们在电路层面最终都表现为一根电平线的跳变。从低到高叫上升沿从高到低叫下降沿。这句话说起来简单但工程上大家真正关心的其实不是那个跳变本身而是跳变背后的“事件语义”。举个具体例子。我在调试一台伺服驱动器的过程中碰到过编码器Z信号输出的情况。Z信号每转一圈输出一个脉冲用来给位置计数做归零校准。正常来说这个脉冲宽度在1微秒左右非常窄。假如你只用普通的GPIO轮询去看这个信号采样周期哪怕做到了100微秒理论上也有99%的概率什么都看不到。但如果你理解了“Z信号是一个瞬时边沿事件”你就会知道正确做法是用带输入捕获的定时器通道去等这个边沿并且用硬件记录边沿到达时的计数值。这背后依赖的还是“在正确的时间点采样”这一基本逻辑。这里的关键点必须说清楚边沿是一个瞬间量它没有宽度或者说宽度极窄。系统如果想要识别它本质上需要回答两个问题第一电平从什么变成了什么第二这个变化发生在什么时候。第二个问题往往比第一个问题难得多因为它要求系统有精确的时间基准。1.2 轮询、中断、DMA不同“看见”方式的天花板既然要知道电平有没有发生跳变最容易想到的办法就是不停地读这根线的状态。这里就有三种常见手段各有各的天花板。轮询最直白定时去读GPIO电平然后跟前一次读到的值比较。如果两个相邻采样点之间信号已经跳了一个来回程序就什么都没看到。做低功耗产品的时候这个问题尤其突出。设备在休眠模式下会用慢速时钟或低频任务去扫描唤醒引脚扫描周期可能是几十毫秒而外部唤醒信号也许只是一个几十毫秒宽的脉冲。采样太稀疏这个脉冲就会从系统的眼皮底下溜走。所以轮询的瓶颈非常明确采样频率必须远高于信号的变化频率否则就漏事件。中断比轮询跟进得及时边沿一来就触发ISR。但中断也有代价。频繁的边沿会产生频繁的中断如果ISR里处理不当系统大部分时间都在来回切换上下文整体实时性反而下降。而且瞬态很短、后面又跟了一串噪声的边沿ISR很难单独判断“这到底是一次有效触发还是抖动”。中断可以告诉你“有东西发生了”但经常无法告诉你“这件事是否有效”。DMA和专用外设比如输入捕获、正交解码器是更高级的手段它们本质上是在硬件层面完成电平采样、时间戳记录甚至计数。但外设资源总归有限通道数量、引脚重映射、和低功耗模式的兼容性每个都是约束。所以工程上最需要的其实是利用好“时间”这个资源配合已有外设把边沿事件可靠地捡起来——这正是Delay要大展拳脚的地方。2. Delay在边沿捕捉中的三个典型角色2.1 采样节拍器用固定延时做定时采样第一种用法最基础把Delay当作采样节拍器。逻辑很直白每隔固定时间读取一次引脚电平把当前值和上一次的值做比较。发生跳变就说明检测到了边沿。这里有个问题会让很多新手犯难固定时间到底选多少我自己一般按两条约束来推。第一采样周期必须小于最短脉冲宽度。假设信号中最窄的脉冲是200微秒那采样周期最好不大于100微秒至少留一倍余量脉冲更窄那就得再加密。第二要给其他任务留出执行时间。采样太密CPU会一直干这一件活系统功耗和响应都会被拖垮。这两个约束之间是一个没有万能答案的取舍点只能根据实际负载去调。在实现上采样节拍既可以靠阻塞式延时比如常见的delay_ms/delay_us也可以靠非阻塞的定时器回调。阻塞式延时代码最简单但有个很大的坑延时期间CPU被占死如果此时有一个更高优先级的边沿需要处理实时性就会被打折。我在第五部分会专门展开。我的建议是如果工程里已有固定的时基任务比如1毫秒或10毫秒的周期任务直接把采样逻辑挂在这个任务里比单独写一个阻塞延时循环要稳得多。2.2 去抖过滤器延时不是等待而是确认第二种用法是去抖。按键、继电器触点、霍尔传感器输出都会因为机械或电气原因产生抖动。抖动在波形上是一连串密集的毛刺边沿如果不处理一次物理动作会被系统识别成很多次。去抖的思路就是用Delay换时间窗口。常见做法是检测到第一个边沿之后不立刻确认而是延时一段时间比如10毫秒到20毫秒等到这个窗口结束再重新读一次电平如果电平仍然保持在目标状态才认为这是一次有效事件。这种“先等再确认”的思路本质上是把Delay当作了一个质量过滤器。这里想强调一个容易被忽略的点去抖Delay不是拍脑袋定的。它必须要大于抖动持续时间的最大值同时要小于连续两次有效操作之间的最短间隔。如果太小去不干净如果太大用户快速操作时会明显感觉“卡顿”或“丢操作”。比如某款触控面板我把去抖时间从15毫秒改到30毫秒按键明显变钝改到5毫秒偶尔就会出现一次操作被识别成两次。所以这个参数没有标准答案唯一可靠的办法是拿示波器实测抖动波形再根据操作频率去定。2.3 脉冲展宽器让短脉冲活到系统来得及处理第三种用法是脉冲展宽。有些信号源输出的脉冲非常窄窄到CPU轮询根本不可能覆盖也窄到ASW层不可能实时感知。这时候可以借鉴一个经典的电路思想——单稳态触发在软件里用Delay模拟出来检测到边沿后输出一个持续较长时间的高电平或低电平系统随后用普通的轮询或中断去处理这个“展宽后的信号”。这个方法在低速系统里非常实用。我曾经把一个只有30微秒宽的周期性脉冲用软件展宽到2毫秒然后让主循环每500微秒去查一次标志位就能稳定捕捉。核心实现就几句话边沿到来时置位输出并记录当前时间每次检查输出是否超时超时就自动复位。代码我给到第四部分。要注意的是展宽会丢失原始的精确时刻信息。如果应用需要知道脉冲的精确到达时间就必须配合硬件定时器的输入捕获或时间戳。展宽适合“只关心有没有发生”的场景而时间戳适合“还要关心什么时候发生”的场景。把这两个混在一起很容易做出一个“看起来能用、实际精度很差”的系统。这三种角色对应了Delay应用的三个层次定时采样、带确认的延时、主动展宽的延时。工程里不是只选一种而是随着信号特性和系统负载组合使用。3. 从硬件边沿到ASW事件一条完整的“瞬间事件”链路3.1 ASW能看懂什么事件接口与时间戳聊到这里就得把ASW请出来了。ASW是Application Software的缩写也就是应用层软件。在AUTOSAR这类分层架构里ASW不直接操作寄存器也不关心某个引脚的电平。它通过RTE和底层BSW通信拿到的是经过抽象的数据和事件。这就带来一个很现实的问题ASW天生就是“看不懂”裸边沿的。你给它一个引脚电平跳变的信息它不知道这根线是传感器输出、是按键、还是故障指示。它需要的是一个带语义的事件比如“输入1上升沿有效”、“Z相脉冲到达”、“按钮被按下”。从物理边沿到语义事件中间隔着一层处理逻辑而Delay就是处理逻辑里最重要的时间工具。我见过不少项目把物理边沿直接映射成软件标志位让ASW轮询去“猜”事件。比如ASW周期判断“如果引脚电平为高就认为有事件”这种做法在信号宽裕、时序不敏感的时候勉强能跑一旦信号变窄或者出现毛刺ASW就很容易误判而且问题发生在应用层排查起来特别痛苦。正确做法是把“电平”和“事件”分开底层负责把电平边沿翻译成事件ASW只消费事件。3.2 底层捕获与上层消费缓存、队列、回调底层把边沿捕获、去抖、展宽之后生成一个标准事件结构体里面通常包含三个要素事件ID、事件来源、时间戳。然后通过队列把事件交给上层。队列在这里非常关键。它能把瞬时突发的事件缓存下来让ASW在它的运行节奏里慢慢消费而不至于被一次突如其来的边沿打乱节奏。我实际用过的队列有两种形态。一种是简单的环形缓冲适合单生产者单消费者的场景底层在中断里往队列写ASW任务周期性地从队列读另一种是带回调通知的队列底层写入事件后触发一个软中断或设置事件标志ASW只在有事件时被唤醒。前一种实现简单、开销可控后一种响应更快、实时性更好。至于选哪种主要看ASW任务的周期和系统对事件时延的要求。这里有一个容易翻车的点如果底层产生事件的速度长期高于ASW消费的速度队列就会溢出事件会被丢弃。排查的时候你会看到“明明有事件产生ASW却偶尔没反应”。所以在设计阶段就得估算峰值事件率把队列深度留足同时考虑漏报率指标。没有哪个队列是无限深的关键是让溢出的概率小到业务可接受。3.3 时间基准为什么事件必须带时间戳事件结构体里的时间戳看似不起眼其实是整个链路的点睛之笔。ASW拿到“有边沿”还不够很多时候它要做的是多路事件的时序关系判断。比如判断两个传感器的信号到达间隔是否在合理范围内或者计算编码器两个脉冲之间的时间差来推算转速。这些计算需要的不是“事件的先后顺序”而是“事件发生的绝对时刻”。时间戳可以来自硬件定时器比如一个自由运行的16位或32位定时器在边沿到来时触发输入捕获并把计数值锁存下来。这个计数值经过换算就能得到微秒甚至纳秒级的事件时刻。相比之下靠软件Delay去估算时间就粗糙得多因为从边沿到ISR再到事件入队每一步都有不可控的延迟。如果你在设计一个对时序有要求的系统建议从一开始就把时间戳纳入事件结构体。这样ASW在应用层做时序分析的时候就不用回头改底层驱动。我见过太多项目前期省了这个字段后期想加涉及到的改动几乎是牵一发动全身。4. 工程实现用Delay实现边沿捕捉与事件上报4.1 需求场景短脉冲计数与按键去抖这一部分我准备用一个实际场景来串起前面讲的思路。假设现在有一块控制板有两路输入需要处理。第一路是编码器Z信号输出窄脉冲宽度约50微秒要求记录脉冲次数并上报给ASW。第二路是一个机械按键按键按下时会产生约3毫秒到8毫秒的抖动要求识别一次有效的按下事件并上报给ASW。这个场景不算复杂但已经同时覆盖了窄脉冲捕捉、去抖、事件上报三件事。我用一个简化的C代码demo来演示实际工程里你可以在这个基础上扩展成更规范的服务层。4.2 代码级别的边沿检测实现先定义事件结构体和队列接口。事件结构体里包含事件ID、时间戳和一个保留字段typedef struct { uint16_t event_id; uint32_t timestamp_ticks; uint16_t reserved; } asw_event_t; #define ASW_EVENT_Z_PULSE 0x01 #define ASW_EVENT_KEY_PRESS 0x02 #define EVENT_QUEUE_SIZE 16 static asw_event_t event_queue[EVENT_QUEUE_SIZE]; static volatile uint8_t q_head 0; static volatile uint8_t q_tail 0; static void event_queue_push(uint16_t id, uint32_t ts) { uint8_t next (q_tail 1) % EVENT_QUEUE_SIZE; if (next q_head) { return; // 队列满丢弃 } event_queue[q_tail].event_id id; event_queue[q_tail].timestamp_ticks ts; q_tail next; }注意这里我用了环形缓冲长度是16。实际工程里队列长度要根据峰值事件率来定不能拍脑袋。如果担心事件漏报可以在丢弃时置一个溢出标志用于后期统计。然后是对Z信号的边沿检测。因为Z脉冲宽度只有50微秒普通轮询做不到这里用外部中断加输入捕获更合理void EXTI_Z_IRQHandler(void) { uint32_t ts hw_timer_capture(); event_queue_push(ASW_EVENT_Z_PULSE, ts); }这个中断函数里没有复杂逻辑只做两件事读取硬件捕获的时间戳把事件推入队列。这样整个ISR执行时间可以控制在几百纳秒到几微秒级别不会影响系统实时性。时间戳精度取决于定时器时钟频率比如72MHz的定时器单个tick约为13.9纳秒。按键去抖则采用状态机加非阻塞延时。我不用delay_ms阻塞等待因为那样会把MCU死死卡在按键处理上。更好的做法是用一个周期任务来做去抖判断时间基准来自系统节拍#define DEBOUNCE_TICKS_MS 15 typedef enum { KEY_IDLE, KEY_CONFIRM, } key_debounce_state_t; static key_debounce_state_t key_state KEY_IDLE; static uint32_t key_change_time 0; void key_scan_task(void) { uint8_t level gpio_read(KEY_GPIO_PORT, KEY_GPIO_PIN); static uint8_t last_level 0; switch (key_state) { case KEY_IDLE: if (level ! last_level) { key_change_time system_get_ticks_ms(); key_state KEY_CONFIRM; } break; case KEY_CONFIRM: // 一直延时到去抖窗口结束 if (system_get_ticks_ms() - key_change_time DEBOUNCE_TICKS_MS) { if (level ! last_level) { // 去抖窗口结束后电平仍然处于新状态确认是一次有效边沿 if (level 1) { event_queue_push(ASW_EVENT_KEY_PRESS, system_get_ticks_ms()); } last_level level; } key_state KEY_IDLE; } break; } }这段代码里的核心技巧是用system_get_ticks_ms() - key_change_time来判断是否到了延时结束的时机而不是在检测到变化那一刻调用delay函数去傻等。两种写法实现的功能类似但非阻塞版本在等待过程中CPU可以继续执行其他任务系统的实时性和功耗表现会好很多。4.3 定时器与延时的取舍上面的代码里出现了一个关键选择到底用阻塞式延时还是用非阻塞的时基判断。我在实际项目里基本遵循下面的取舍原则方式优点缺点适用场景阻塞式delay_ms/delay_us逻辑简单、可读性好CPU被占死、实时性差、中断嵌套时容易出问题初始化流程、非实时性的低速逻辑系统节拍非阻塞判断CPU空闲、实时性好、可扩展代码稍复杂、需要有时基基准去抖、超时判断、周期性采样硬件定时器输入捕获精度最高、不占CPU外设资源有限、配置复杂窄脉冲、需要精确时间戳的场合我个人的习惯是凡是涉及“边沿、事件、超时”这类和瞬间时序强相关的逻辑一律优先用非阻塞或硬件定时器方案阻塞式延时只保留在系统初始化和不需要关心实时性的地方。这样做的好处是整个系统的“响应底板”被抬高了ASW能够感知的事件粒度更细。5. 复盘与排查Delay相关的典型坑5.1 延时函数卡死的常见姿态“STM32延时函数delay卡死”是搜索热度特别高的话题我猜很多人都遇到过。我自己就踩过一个特别经典的坑在某个外设的中断处理函数里调用了delay_ms而这个中断优先级比较低。当时系统还有另一个高优先级中断频繁触发每次高优先级中断都要占一段时间低优先级中断里的delay又在不断重载SysTick一来二去SysTick的中断优先级反而低于当前正在执行的中断导致系统的时基跳动异常delay永远等不到结束条件程序就“卡死”了。还有一种情况更隐蔽阻塞式延时使用SysTick作为时基但某些低功耗模式下SysTick的时钟源被关闭了或者延时函数在进入临界区关中断期间被调用系统时间不往前走delay自然就成了死循环。排查这类问题我的建议是先回答三个问题这段延时是在中断里还是主循环里系统当前处在什么功耗模式延时期间CPU是否被允许响应更高优先级的中断把这三个问题理清楚80%的延时卡死都能找到根因。不要一上来就怀疑编译器优化或者芯片有问题大多数时候还是用法不对。5.2 边沿丢失、误触发、抖动误判边沿丢失是另一个高频问题我在文章开头说的80微秒脉冲漏检就属于这一类。排查的时候不要只在应用层找原因要顺着信号链路一步步回溯先看物理层波形是否正常再看底层采样/中断是否真的触发了最后看事件是否成功入队并被ASW消费。示波器依然是最好用的工具观测点如果太靠后问题往往就被中间环节吃掉了。误触发通常和去抖参数设置不当有关。我遇到过一例静电放电导致的误触发现象是设备偶尔会无端记录一次非法输入。示波器抓下来发现ESD事件在信号线上耦合出一串极窄的毛刺单个毛刺宽度只有几百纳秒。我的底层虽然做了去抖但去抖窗口只有1毫秒根本拦不住这串毛刺。后来把去抖窗口加大到10毫秒问题才消失。这里要提醒一句去抖时间变大会带来响应延迟必须和应用层的操作体验放在一起权衡。抖动误判则经常出现在“边沿计数”类需求里。比如编码器信号如果带有毛刺软件计数会比实际转数多出不少。此时除了在软件里做去抖还应该在硬件上增加RC滤波或施密特触发器整形软硬结合效果最好。5.3 时序分析中的Delay感知最后把视角拉远一点。在FPGA设计或SoC时序收敛里Delay的含义和嵌入式软件里的延时不完全一样但本质都是“时间差”。做FPGA综合时input delay指的是信号从外部器件输出到FPGA输入引脚之间需要的建立时间也就是数据相对时钟的到达时间。如果这个值配置不对综合工具就无法保证边沿能被正确采样。Vivado里可以导出pin delay用来做板级时序分析本质上也是在描述“边沿到底什么时候到达”。在功耗分析里也有类似概念。PrimeTime PX做功耗分析时可以选择zero delay模型忽略掉门延迟直接用理想时钟来评估开关功耗但更精确的分析会带上真实的cell delay和net delay。这个差异告诉我们一个系统如果完全不考虑Delay得到的评估结果和真实行为之间会有不小的偏差。和嵌入式里“不用延时捕捉边沿ASW就看不懂瞬间事件”其实是同一个道理。6. 我的实操体会与扩展建议说句实在话Delay本身不是什么高深技术单片机入门课程第一节就会讲。但在真实工程里Delay用得精不精直接决定了一个系统能否稳定地感知外部世界。我在好几个项目里都发现出问题的地方不是算法也不是通信协议而恰恰是最不起眼的“信号有没有被正确看见”。这个看不见的环节往往要花掉排障时间的大头。根据我的个人经验建议大家在接手一个新平台时先把几个东西摸清楚系统节拍是多少有没有可用的自由运行定时器各中断的优先级是怎么分配的低功耗模式和时基之间的关系是什么。这四个问题搞明白之后你再面对“瞬间发生的事”就知道该用采样、去抖、展宽还是硬件捕获了。最后再分享一个小技巧给ASW上报事件的时候除了时间戳尽量再加一个“事件计数器”。也就是从本次上电开始这类事件总共发生过多少次。这个计数器在排查“疑似漏事件”的现场特别有用。ASW可以拿它和底层驱动的硬件计数做对比一下子就能判断出是底层漏了事件还是上层消费丢的事件。多这么一条信息很多疑难杂症都能少绕一大圈。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门