搞嵌入式或者工业数据采集的人,十有八九都撞上过这么一件怪事:示波器上清清楚楚地看到每一个脉冲都来了,电平合适、边沿够陡,程序里的触发标志也确实被置起来了,可最后读出的脉冲计数值就是比实际少一截。少个一两个还能甩锅给干扰,少几十个、上百个就彻底坐不住了。更迷惑的是,你单独拿一个低频信号去测,计数完全准确;一旦信号变成突发式的高速脉冲串,漏数就冒出来了。
这个问题的本质往往不是“没触发”,而是“触发了,但系统在死区时间里把紧随其后的第二次、第三次事件吞掉了”。所谓死区,就是系统在处理一个事件期间无法响应下一个事件的时间窗口。只要窗口存在,计数就必然存在丢失的上限。这篇文章我会从原理层面拆开“触发但计数偏少”这条链路,讲清楚死区在高速采集系统里藏在哪里,然后分享一次完整的排查实录,最后给出一套能在方案阶段就规避丢数问题的设计清单。
1. 现象拆解:“触发”和“计数”中间隔着一道看不见的坎
1.1 一个极具迷惑性的现场
先还原一下典型的故障现场。设备端输出一路脉冲信号,可能是编码器的A相,也可能是流量计或光栅尺的反馈脉冲。MCU这边用的是外部中断,每来一个上升沿进一次中断服务程序(ISR),在ISR里对变量自增一次。
调试的时候你在中断里点了个LED或者翻转一个引脚,肉眼观察会发现“每次都触发”——LED闪得跟信号节奏一致,逻辑分析仪也持续抓到引脚上有活动。但软件里最终累计的数字,就是比你在示波器上用光标数出来的总边沿数少。这个矛盾能把人绕晕:既然触发每次都发生了,计数为什么会少?
关键在于,你看到的“每次都触发”是低频视角下的错觉。假设信号平均频率只有1kHz,看起来间隔1ms很从容,但实际信号往往是突发式的:某段时间内连续来了几十个脉冲,间隔只有50微秒,随后又安静几百毫秒。平均频率不高,不代表瞬时频率不高。系统的死区恰恰只在瞬时高密度的段落里发作,所以低频段测试永远正常,一上转速、一给负载、一遇到突发脉冲就露馅。
1.2 从引脚到寄存器:一条会丢数的链路
脉冲计数从来不是“信号进引脚,计数加一”这么简单,它是一条完整的链路:
信号源 -> 电平调理(RC滤波、整形、施密特回差) -> 引脚输入级 -> 内部滤波/边沿检测 -> 中断控制器/触发单元 -> CPU响应 -> 中断服务程序执行 -> 计数值写入。
这一路每一个环节都有两个属性:一是处理单个事件需要的时间,二是在处理过程中对后续事件的“态度”。有些环节是阻塞式的,处理完一个才能看下一个;有些环节是队列式的,能暂存几个但满了也会丢;有些环节是边沿触发加标志位,标志位只有“有/无”两种状态,重复来多少次都只能记成一次。
这就好比只有一个窗口的办事大厅。正常情况下来一个办一个没问题,但高峰期两个人间隔极短地走进来,第一个人还占着窗口,第二个人等了一会儿没人理,只能走掉。走掉的那个人就是“丢失的脉冲”。对系统来说,这个“无人在意的走掉”完全没有痕迹,所以你只看触发标志,永远发现不了问题。
1.3 死区时间:系统的“反应不过来”
给“死区”下一个工程化的定义:系统从开始处理一个事件,到能够重新处理下一个事件之间的最短时间间隔,记作 T_dead。在脉冲计数场景里,系统的最大连续计数能力大约等于 1 / T_dead,也就是每秒最多能处理多少不重叠的脉冲。
假设你的中断服务程序实测需要5微秒,那么理论上连续脉冲间隔小于5微秒就会出现丢失。注意“连续”这两个字很重要:如果脉冲间隔在100微秒左右,5微秒的死区不会造成任何影响,示波器看触发也一切正常;真正出问题的是成对出现、间隔逼近T_dead的突发脉冲。这也是为什么很多工程师调了半天示波器触发、采样率,问题依然岿然不动——他们根本没往“系统处理速度跟不上瞬时事件的密度”这个方向去想。
理解这点之后,排查方向就清晰了:不要问“信号来了没有”,而要问“每次事件从来到被记下来,系统需要多长时间?在这个窗口里又来了多少个事件?”
2. 死区从哪里来:藏在四个环节里的“隐形杀手”
2.1 中断服务程序执行时间:最经典的死区
外部中断+ISR计数是最常见的计数方案,也是死区最集中的地方。一次完整的中断处理包含硬件响应延迟、压栈、ISR代码执行、出栈、恢复现场这几个阶段。硬件延迟本身很短,通常几个指令周期就完事,真正的黑洞在ISR代码里。
很多人写ISR很随意,里面塞了printf、浮点运算、复杂的条件分支,甚至调用了带阻塞的第三方库函数。这些操作的耗时动辄几十微秒,直接决定了系统的T_dead。我曾经见过一个现场,ISR里为了调试加了一行串口打印,打印一次要40多微秒,而现场的脉冲间隔最短只有30微秒,结果计数值大概只有实际值的70%。删掉打印后立竿见影,到95%以上。
还有一个更隐蔽的坑:同一条外部中断线只有一个挂起标志位,它只能表达“有事件待处理”,不能表达“有几次事件待处理”。如果ISR还没执行完,同一条引脚又连续来了两个脉冲,后续的硬件事件会被折叠成一次。你看到的触发标志确实被置位了,ISR也确实执行了,但计数就是少了一段,这类问题用示波器看触发永远看不出来。
2.2 输入滤波与消抖:把窄脉冲“滤”没了
为了防止抖动和噪声,很多系统会在输入级加RC低通滤波器,或者在单片机内部配置数字滤波器。滤波器的本质是一个“只让慢信号通过”的装置,它天然对窄脉冲不友好。RC时间常数越大,能识别的最小脉宽就越宽;如果RC常数设成了10微秒,而实际脉冲宽度只有几百纳秒,信号还没充到阈值就被拉回去了,计数器一个都计不到。
更迷惑的是,示波器探头是高阻输入的,它不参与系统侧的滤波,所以你在示波器上看到的波形是干净的原始脉冲,而MCU引脚上看到的已经被RC电路“压扁”成了三角波,根本触发不了。这就是“示波器看到了,系统没看到”的典型场景。
施密特触发器的回差电压也要留意。回差大抗干扰好,但会把幅度刚过阈值的信号拒之门外;回差太小则可能在振铃沿上反复触发。设计输入电路时,既要保证信号能触发电平阈值,又不至于被消抖网络削掉脉宽,这本身就是一个平衡问题。
2.3 触发重武装与转换过程:数据采集内部的“冷静期”
如果系统里用到了示波器、逻辑分析仪或者带硬件触发的采集卡,那第三个死区藏在“触发-采集-再武装”这个循环里。以常见的示波器触发为例,一次触发发生后,仪器要把一段数据写入采集存储、完成显示处理,然后重新进入等待触发状态。这个重新武装的时间被称为re-arm时间或触发休止期。如果第二个触发事件来得比re-arm时间还早,仪器就漏掉了这次触发,屏幕上看到的只是“好像触发了”,但实际上事件没有被采到。
ADC采样触发器同理。比如硬件定时器触发ADC采样,定时器事件来了,但ADC上一次转换还没结束,新的触发事件就可能被仲裁逻辑丢弃;如果系统加了FIFO缓冲,事件可以先排队,可FIFO填满之后同样会触发溢出丢数。排查这类问题时要特别留意:触发源产生了事件、触发标志被置位,这只是“事件发生”的证据,不等于“事件已经被转换/被采集”的证据。
2.4 软件轮询与标志位锁存:主循环里悄悄漏掉的计数
有些低成本方案不用中断,而是主循环里轮询GPIO电平。轮询周期如果是1毫秒,而脉冲高电平只持续几百纳秒,轮询代码极有可能根本采不到那个高电平,计数自然为零。即便用了边沿中断,ISR里只置一个“有事件发生”的软件标志,主循环每轮只对这个标志处理一次,那么一个标志位在一个周期内不管被置几次,主循环都只当成一次事件处理。这就是“标志位折叠”,典型的软件级死区。
正确的做法是:ISR里不能只置标志,还要对事件次数做累加。也就是说,ISR中执行 count++,主循环读取并清零临时值,再用临时值累加进业务变量。否则只要主循环的响应周期比事件间隔长,计数就会系统性偏少。
3. 一次真实排查实录:用逻辑分析仪“抓住”丢失的脉冲
3.1 故障现场与初步假设
某电机试验台项目,电机每转一圈编码器输出1000个脉冲,需要精确统计转数和转速。控制板上用STM32F407的外部中断EXTI对A相上升沿计数,低频时一切正常,转速拉高或者频繁加减速时,计数值就会比理论值少几十个。现场用示波器看A相波形,每个脉冲都清晰、干净,5V电平,脉宽2微秒,转速最高时脉冲间隔也有20微秒以上。
初步怀疑是信号反射或者地线干扰,但把线缆换成屏蔽双绞线、增加终端电阻之后问题照旧。又怀疑是示波器触发设置问题,但示波器只是旁观,对MCU计数没有影响。于是决定从固件侧量化死区。
3.2 第一步:量化ISR耗时(GPIO翻转法)
在ISR的入口立即把一个测试引脚拉高,在出口拉低,用示波器同时观察输入脉冲和这个测试引脚。测试引脚的高电平宽度,就是ISR从进入核心代码到退出的实际耗时。
实测结果:大多数时候ISR需要2.4微秒,偶尔因为Flash等待周期和分支判断会到3.5微秒。相对20微秒的脉冲间隔来说,这个耗时似乎绰绰有余。但注意,这只是“一次ISR”的耗时;如果ISR执行期间新脉冲再次到达,同一个EXTI线的挂起标志只能记录下来一个待处理事件,而且ISR退出前往往会顺手清标志,这就会把正在等待中的新事件也一起清掉。那一刻,丢数已经发生了。
3.3 第二步:交叉验证硬件计数与软件计数
为了确认脉冲确实进入了MCU、问题只出在中断软件路径,我在同一颗芯片上用定时器TIM2的外部时钟模式,把同一个A相信号接到TIM2的ETR引脚,由硬件直接计数。外部时钟模式不走中断、不占CPU,脉冲边沿直接驱动计数器加一。
然后做对比实验:同一个信号源同时送入外部中断EXTI和TIM2硬件计数,跑一段固定转数。结果TIM2的硬件计数值与示波器人工统计完全一致,EXTI软件计数值稳定偏少,相差的频率与突发脉冲出现的频率呈正相关。到这里基本可以断定:信号链路、引脚电平都没问题,问题锁死在“中断响应+挂起标志”这一层。
3.4 第三步:找到“合并事件”的真凶
下一步是验证“挂起标志折叠”的猜想。我在ISR里加了临时监控:进入ISR后先读EXTI挂起状态寄存器,如果能读到“另一个事件已经在等待”,就额外打一个冗余标记。跑了几分钟后,果然抓到了这种场景:ISR尚未退出,新的脉冲已经到达,挂起标志重新被置位;此时同一个EXTI线只能表示“又来了一次”,如果两次到达间隔极短,第二次硬事件会被折叠在第一次之后,ISR根本无法区分究竟来了一个还是两个。
这里必须强调:不同MCU对同线重复触发的具体行为略有差异,但共性是一致的——挂起标志位是“有/无”逻辑,不是计数器。只要系统依赖中断标志来判断“来了几个事件”,在极端突发密度下就一定会丢。这也是高速脉冲计数场景里,我坚决推荐硬件计数器的根本原因。
3.5 修复方案与复测结果
修复方案分两步走。第一步,把核心计数功能从EXTI中断迁移到TIM2外部时钟模式,CPU不再每个脉冲都进中断,只在计数器即将溢出时进一次中断读取并扩展计数位。第二步,如果仍需要保留EXTI用于边沿时间戳,则在ISR里严格做到“读硬件计数值/读挂起状态、清理标志、更新软件变量”,不做任何多余操作,并把EXTI中断优先级提到最高,避免被其他中断长时间阻塞。
复测结果:从1000转/分到6000转/分全程跑测试,连续运行数小时,TIM2硬件计数与基准示波器统计完全一致,EXTI软件计数也恢复到了百万分之一量级的误差。最有价值的是,我们顺手在代码里加了“EXTI挂起标志重复置位”的统计量,跑完一看,之前丢数的高峰期,这个统计量正好对应上——死区问题终于在代码里有了显式证据。
4. 从根上解决:一套可落地的“防丢数”设计清单
4.1 把计数下放到硬件:能用计数器就不要用中断
市面上绝大多数MCU都有定时器外部时钟模式或输入捕获模式,这类硬件模块的最大优势是:事件到达与计数器累加之间没有软件参与,脉冲由内部逻辑直接驱动寄存器变化,T_dead被压缩到纳秒级,而不是微秒级。
| 计数方案 | 典型死区 | 适用场景 | 注意事项 |
|---|---|---|---|
| 外部中断+ISR | 微秒级到数十微秒 | 低频、简单计数 | ISR必须极短,注意标志折叠 |
| 定时器外部时钟模式 | 纳秒级(硬件计数) | 高中频连续计数 | 注意计数器溢出处理 |
| 输入捕获+DMA | 取决于捕获单元,较中断更稳 | 需要时间戳/高频采集 | DMA缓冲要足够深 |
| 专用计数芯片 | 芯片规格决定 | 工业现场高可靠场景 | 增加成本和布线 |
| FPGA内部计数器 | 时钟周期级 | 超高频率、多通道 | 工程量最大 |
个人经验优先顺序是:第一选硬件计数器,第二选输入捕获+DMA,第三才是外部中断。外部中断只适合频率低、事件稀疏、且你能保证ISR足够短的场景,不适合任何“有一定瞬时密度”的工业信号。
4.2 中断里只留三件事:读、清、置标志
如果项目里实在只能用中断,那就把ISR压缩到极致。我给自己定过一条铁律:ISR里只允许做三件事。
- 读取硬件寄存器或锁存值,把数据“抢”到局部变量里;
- 清理中断标志位,确保硬件不会再被同一个事件占住;
- 更新一个volatile修饰的计数变量或软件标志,然后立刻返回。
禁止在ISR里做的事情包括但不限于:串口打印、浮点运算、函数调用、动态内存分配、延时、等待某个标志、修改外设寄存器配置。需要串口输出调试时,只把事件数量和关键值放入环形缓冲区,由主循环或低优先级任务去慢慢发送。
另外一个容易忽略的点:计数值变量在ISR里累加,主循环里读取,这种跨上下文访问如果不加volatile、不关中断保护,轻则有缓存一致性问题,重则读到中间态。用关中断保护临界区时,关中断时间要短,不要在关中断里塞耗时的逻辑,否则又人为扩大了系统死区。
4.3 输入调理的平衡术:滤掉噪声,也要放过窄脉冲
输入级滤波的目标不是“最大程度滤噪声”,而是“在保证信号完整的前提下抑制噪声”。设计时可以这样算:先确定系统要识别的最小脉宽 T_min,RC滤波时间常数建议控制在 T_min/5 到 T_min/3 之间。比如最小脉宽10微秒的脉冲,RC常数取2~3微秒比较合理;如果RC常数大于10微秒,这个信号基本就废了。
对于频率更高或者边沿更陡的信号,更好的方案是先用高速比较器或光耦做电平整形,把信号变成干净的方波,再做脉冲输入。这个方案同时解决了模拟噪声和共模干扰问题。施密特触发器的回差按信号幅度的20%~30%选取,既能抑制振铃误触发,又不会把矮信号挡在阈值以下。
传输线的末端阻抗匹配也要做起来。脉冲频率高、边沿快时,反射会造成额外振铃,振铃一旦叠加在阈值附近,就会导致误触发或者漏触发。这类问题往往表现为“计数偶尔多,偶尔少”,而且跟线缆长度关联明显。
4.4 FIFO、缓冲与溢出感知:让系统知道“忙不过来”
死区不可能完全消除,但一定要让“丢事件”这件事变成可检测、可统计的,而不是偷偷摸摸发生。
如果外设带硬件FIFO,比如ADC、UART、输入捕获,优先启用FIFO。事件密集时可以暂存在FIFO里,软件批量取走,这比一个一个中断处理要高效得多,也天然降低了丢数概率。FIFO若满,状态寄存器里会有溢出标志;软件应当在检测到溢出时明确记录“丢失了N个事件”,而不是假装无事发生。
对于硬件计数器溢出,同样要建立“扩展计数”机制:计数器溢出时进中断,把溢出次数累加到一个扩展变量里,总计数 = 扩展变量 × 计数器满量程 + 当前计数值。这样即便脉冲频率很高,只要平均频率低于计数器溢出速率,就不会丢。
给系统加“过载检测”也是一个值得投资的习惯。在软件里维护一个丢失计数器,当系统检测到FIFO溢出、中断标志折叠、信号间隔小于理论安全阈值时,将丢失次数记录到诊断日志里。这样到了现场,调试者不用靠猜,直接看日志就知道系统跑到什么工况时开始扛不住了。
4.5 压测验收:用突发脉冲把系统打满
很多团队验收计数值时只用一个固定频率方波:1kHz、10kHz、100kHz各跑一遍,看起来准确就算通过。这种测试最大的问题是掩盖死区——固定频率下,只要每个脉冲间隔都大于T_dead,系统永远表现正常;真正要把死区逼出来,必须用突发脉冲。
推荐的做法是用信号发生器输出“成簇的脉冲串”:每簇包含N个脉冲,簇内间隔从1微秒到100微秒随机变化,簇与簇之间有较长的静默期。跑一段时间后,把系统计数值与信号发生器实际发出的总脉冲数对比。如果计数偏少,大概率就是死区问题。更严格一些,用伪随机序列生成脉冲间隔分布,连续跑24小时,要求十万级脉冲的累计误差为零。
验收标准要写进测试文档:固定频率测试只能证明“稳态计数能力”,突发脉冲压测才能证明“动态计数能力”。两者都过了,才敢说这套计数系统在高速场景下是可靠的。
5. 长期避坑手册与经验速查
5.1 “计数偏少”原因速查表
| 现象 | 可能原因 | 快速验证方法 | 解决方案 |
|---|---|---|---|
| 高频突发时偏少,低频准确 | ISR耗时太长或标志折叠 | GPIO翻转法测ISR耗时 | 压缩ISR,改用硬件计数器 |
| 示波器看到脉冲,系统没反应 | 输入RC滤波把窄脉冲滤掉 | 对比滤波前后波形 | 减小RC时间常数,加整形电路 |
| 触发标志置位,但数据采集少 | 触发re-arm/休整期过长 | 检查采集设备holdoff设置 | 改连续采集,配置最短休整时间 |
| 主循环每周期只处理一次标志 | 软件标志位折叠 | 在ISR里增加次数累加变量 | 计数累加放ISR,主循环只负责取走 |
| 低优先级中断被长时间阻塞 | 高优先级任务关闭中断过久 | 统计各中断阻塞时间 | 调整优先级,减少关中断临界区 |
| ADC触发后样本偏少 | 转换未完成时触发被丢弃 | 检查ADC忙标志与FIFO溢出标志 | 启用FIFO,增加转换时间余量 |
这张表我一直在自己项目里当排查手册用。遇到“计数偏少”先不要急着怀疑信号源和干扰,对照表格把系统每一级“处理时间”都测一遍,往往比拍脑袋调参高效得多。
5.2 我自己在相关项目里的三个习惯
第一个习惯,在方案阶段就算“计数链路预算”。把最小脉宽、最小间隔、ISR预估耗时、FIFO深度、软件轮询周期这些数字全列在一张表里,然后问一句:在最极端的瞬态条件下,这条链路能不能扛住?很多时候,死区问题在写第一行代码之前就暴露了。
第二个习惯,做双通道冗余验证。同一路信号同时接硬件计数器和软件中断计数器,冒烟测试和长时间烤机时两边对比。这不仅能发现固件里的隐性丢数,还能积累现场信号特征的实测数据。硬件计数器那一路,我一直把它当“照妖镜”用,软件怎么改,拿硬件通道一对比,立刻见真章。
第三个习惯,把丢数当作系统过载指标来监控。不要只把最终计数值存下来,还要把溢出次数、挂起折叠次数、ISR最大耗时这些指标也存下来。系统“忙不过来”是正常现象,关键是要能看见它、量化它,并在过载时及时报警。有了这些内部指标,现场排查那种“时好时不坏”的诡异故障,效率能翻好几倍。
5.3 常用工具与测量小技巧
测量高速脉冲最趁手的工具是逻辑分析仪,采样率要高于信号最高频率的5倍以上,这样才能准确还原脉宽和间隔分布。把抓到的数据用解码器统计一下,连续跑个几十秒,就能看出有没有极窄脉冲、最小间隔是多少、突发簇的分布形态是什么样的。这比示波器一帧一帧去数可靠得多。
如果只有示波器,推荐用“余辉显示+波形统计测量”功能,直接统计一段时间内信号的最小脉宽和最小周期。注意探头带宽至少要达到信号频率的5倍,否则边沿变缓、幅度变小,会造成人为的“假死区”。探头接地线也要短,普通长地线探头测量高速脉冲时会引入振铃,直接影响判断。
最后一个小技巧:用GPIO翻转法测ISR耗时的时候,测试引脚的飞线要尽量短,尽量靠近MCU引脚,不要用几十厘米的杜邦线。飞线本身如果有寄生电容,会让波形边沿变缓,读出来的高电平宽度就不准。正确做法是直接在芯片引脚旁边的测试点上测量,必要时用同轴短线引出。
说到底,脉冲计数偏少这个坑,绝大多数情况下不是玄学,而是死区时间的数学题。把每级处理时间和事件间隔摆到桌面上,谁是瓶颈一目了然。我自己后来在方案评审时必定先问三个问题:这条信号最短的脉宽是多少?突发时最小间隔是多少?系统处理单个事件的最坏耗时是多少?三个数字一对冲,很多死区问题还没到编码阶段就提前暴露了。希望这次的经验整理,也能让你以后少走几趟弯路。