如果你正在做一个需要统计通过数量的项目,不管是产线上的零件、仓库里的包裹,还是实验室里的样本,“知光”这个名字你可能不陌生——它是我花了大半年时间打磨的一套低成本实时计数方案,而整套系统最核心、也最容易翻车的部分,就是计数模块。计数模块听起来简单:东西过去,数量加一。但真正把它放到现场,你会发现电磁干扰、灰尘污染、物料遮挡方式、传感器响应速度,每一个变量都能让数字变得离谱。这篇文章不打算讲PPT式的架构图,而是老实记录我在知光项目里从硬件选型、信号调理、软件状态机到看板展示踩过的坑和沉淀下来的方法。内容既适合正在选型传感器的硬件工程师,也适合写计数逻辑的嵌入式开发者,甚至对想自己搭一套计数系统的创客同样有参考价值。
1. 项目思路拆解:计数模块到底要解决什么问题
1.1 “知光”的含义与核心设计理念
知光这个名字来源于项目的核心感知方式:通过光线的通断变化识别物体的通过。当物体经过传感器时,光路被遮挡,传感器输出电平翻转,MCU检测到这个翻转就知道“有一件东西过去了”。这听起来像最基础的光电开关应用,但真正深入做下去,你会发现“知道光变了”只是第一步,难点在于知道这次变化是有效的事件而不是噪声。
设计理念可以概括为三点。第一,感知层用成熟的光电传感方案,不追求花哨,保证硬件成本和可靠性可接受。第二,判定层把计数这件事从“数脉冲”上升到“识别事件”,用状态机把抖动脉冲、反复遮挡、连续通过这些现场情况过滤掉。第三,数据层把计数结果以统一格式上报,方便接入产线看板、MES系统或者自建的统计页面。整个模块的定位不是做一个玩具Demo,而是能在车间、仓库、实验室这种实际环境里长期运行的部件。
1.2 计数需求的三层拆解
我在做需求分析的时候,把计数需求拆成了三层,每一层对应不同的实现重点。
第一层是物理事件感知。物体是否出现在检测区域,传感器能不能可靠地给出信号。这一层最容易踩坑的是选型错误,比如漫反射传感器遇到黑色物体检测距离急剧缩短,或者对射传感器被灰尘挡住导致信号没法恢复。第二层是信号去噪与事件判定。传感器输出的原始波形往往带着毛刺,物体边缘可能反射多次,电机启停会引入干扰,如果不做消抖就把信号直接接到中断引脚,计数结果会非常离谱。第三层是数据记录与可视化。计数结果存在哪里,怎么上传,断网时怎么办,看板怎么展示,多通道怎么管理。这一层虽然不直接影响单个计数的准确性,却决定了系统能不能真正投入使用。
这三层是一层层往上叠的。底层硬件不可靠,上层软件做得再好也没用;软件逻辑不严谨,再贵的传感器也会数错;数据链路不通,计数再准也只是设备上的一个数字。所以我在知光项目里始终强调:每一层都要独立验证,不能等全部做完再回来找问题。
1.3 适用场景分析与影响范围
知光计数模块可以覆盖的场景比想象中广。最典型的应用是产线产量统计,传送带上的零件经过光电传感器,系统自动记录每个班次的产出数量。这个场景最看重的是长时间运行不出错,因为一个班次动辄几千上万个零件,漏掉或重复几个可能就会被现场人员质疑。其次是仓库进出库计数,包裹或料箱通过输送线时自动计数,帮助核对发货数量。再次是自动化设备动作循环计数,比如注塑机每完成一次合模动作就计数一次,用于统计设备利用率。还有一些比较轻量的场景,比如展会门口统计人流、实验室记录样本试管通过数量,这些场景不需要采集任何生物信息,只统计通过数量,合规风险很低。
影响范围要从数据源头来看。计数模块是整个系统里离物理世界最近的一环,它给出的数字会直接进入生产报表、库存台账和设备效率计算。如果源头数据不可信,后面所有分析都是空中楼阁。因此这套模块的设计目标很明确:单次计数准确率达到99.9%以上,长时间运行不漂移,断电重启后计数不丢失,多通道数据能统一汇总。
2. 硬件选型与信号调理:计数准确的第一道关卡
2.1 三类光电传感器的选型对比
光电传感器看起来都差不多,实际用起来差别很大。我在知光项目里先后试过三类,这里把它们的特性列出来供参考。
| 类型 | 检测方式 | 检测距离 | 抗干扰能力 | 成本 | 适用场景 |
|---|---|---|---|---|---|
| 对射型 | 发射端与接收端分离,物体遮挡光线 | 可达数十米 | 较强,不受背景物体影响 | 中高 | 传送带零件、包裹,安装空间允许时优先选 |
| 漫反射型 | 发射与接收一体,靠物体反射光线 | 几十厘米到数米 | 较弱,颜色、角度、背景反射都影响 | 低 | 检测体积较大且表面反光较好的物体 |
| 槽型光电 | 物体穿过U形槽遮挡光路 | 几毫米到几厘米 | 强,结构固定 | 低 | 小零件、计数轮、纸张卡片等薄片类 |
从经验来看,计数类应用最优先选对射型或者槽型。它们在原理上就只认“光路有没有被遮断”,不依赖物体表面的反射特性,所以黑色物体、透明物体也能可靠检测。漫反射型虽然安装方便,但遇到深色表面、斜面、高反光背景时容易误判,我通常只在检测对象是规则浅色箱体的场景里用它。
选型还需要注意传感器输出类型。常见的输出有NPN开集输出、PNP输出、继电器输出。MCU读取电平变化最好的是NPN或PNP输出,继电器输出响应太慢,不适合高频计数。我用的较多的是NPN开集输出,配合上拉电阻直接接到MCU引脚,逻辑简单,接线也省事。
2.2 从传感器到MCU:上拉、滤波与整形
传感器信号不能直接一根线接到MCU引脚就算完事,中间有几个细节值得认真处理。以NPN输出为例,传感器不导通时引脚通过上拉电阻被拉到高电平,导通时引脚被拉到低电平,MCU检测的就是这个电平跳变。上拉电阻我一般选4.7kΩ到10kΩ,阻值太小会增加功耗,太大则容易被分布电容拖慢边沿。
现场信号往往带着毛刺,特别是电机、变频器附近,传感器供电线上会出现几十毫伏到几伏的噪声。我的做法是在传感器输出和MCU引脚之间加一级RC低通滤波,电阻选1kΩ,电容选100nF,时间常数约0.1ms,既不会滤掉正常的物体遮挡信号,又能吸收大部分高频毛刺。如果信号还有严重的沿抖动,可以再加施密特触发器整形,常见的74HC14或者带施密特输入的MCU引脚都能解决边沿慢导致的重复触发问题。
更强硬的保护手段是光耦隔离。传感器在现场,MCU在控制箱里,两者地电位可能不一样,长距离走线也容易感应浪涌。用光耦把信号隔离后再进MCU,可以有效避免地环路噪声和静电损伤。知光模块里我给每一路传感器都设计了光耦隔离,实测在电柜附近频繁启停电机的情况下,计数不再出现偶发跳变。
2.3 主控选型与通信接口
主控芯片的选择取决于计数速率和通信需求。如果只是简单计数,一个STM32F103或者国产GD32F103就足够,内部定时器资源多,外部中断数量够用,价格还很低。如果还需要跑网络上报,我会用ESP32,自带的Wi-Fi和蓝牙能直接连MQTT,省掉一颗单独的通信芯片。对纯工业场景,也可以选带CAN或者RS485接口的芯片,走Modbus协议接到PLC或上位机。
通信接口方面,知光模块预留了三种方式。第一种是串口,TTL电平直接输出计数结果,适合调试和短距离对接。第二种是RS485走Modbus RTU,工业现场最常用,抗干扰能力强,一根总线可以挂多台设备。第三种是Wi-Fi走MQTT,适合没有布线的仓库、门店,配合配网功能和看板系统非常方便。我的经验是不要把通信做成只用某一种,至少保留串口,因为现场调试时串口是最可靠的救命通道。
3. 软件逻辑:从脉冲毛刺到可信计数的关键实现
3.1 为什么不能直接数脉冲上升沿
很多第一次做计数模块的人会把传感器输出直接接到中断引脚,上升沿计数加一。这个方案在实验室里通常能跑通,一到现场就露馅。问题在于,一个物体通过时产生的电平变化不只是一次干净的跳变。
物体边缘往往不是完全垂直于光路,传送带振动也会让物体在传感器前晃,光路被部分遮挡后很快又恢复,再遮挡,这就会形成一串窄脉冲。如果直接把边沿送给计数器,同一个物体可能被记成三四个。另外,传感器本身的响应也有延迟和回差,慢速移动的物体可能让输出信号长时间处于中间状态,产生抖动。还有电磁干扰,继电器吸合或变频器启动的瞬间,长线上会感应出几十纳秒的电压毛刺,如果恰好接近阈值,就能产生一个假脉冲。
所以我在软硬件上采用了双重手段:硬件上做RC滤波和施密特整形,把大部分毛刺挡在MCU外面;软件上做时间窗口判定,只有遮挡信号持续超过一定时间才认为是一次有效通过。两个手段配合,误触发率才能压到极低。
3.2 状态机消抖:手写可靠的通过判定
软件消抖的核心是状态机。我这里给出一套经过现场验证的判定逻辑,用C语言伪代码表示,逻辑上有四个状态:空闲、确认、锁定。每隔5ms扫描一次传感器输入。
typedef enum { IDLE, // 无遮挡,等待事件 CONFIRMING, // 检测到遮挡,正在消抖确认 LOCKED // 已计数,等待遮挡结束并冷却 } state_t; static state_t state = IDLE; static uint32_t enter_ms = 0; static uint32_t last_keep_ms = 0; #define HOLD_MS 20 // 遮挡持续超过20ms才算有效 #define COOL_MS 80 // 信号恢复后冷却80ms才能再次计数 void update_count(uint32_t now_ms) { bool blocked = read_sensor_blocked(); switch (state) { case IDLE: if (blocked) { state = CONFIRMING; enter_ms = now_ms; } break; case CONFIRMING: if (!blocked) { // 抖动或异物短暂掠过,回到空闲 state = IDLE; } else if (now_ms - enter_ms >= HOLD_MS) { // 遮挡时间足够长,确认一次有效通过 count++; state = LOCKED; last_keep_ms = now_ms; } break; case LOCKED: if (blocked) { // 物体还在传感器前,持续刷新时间 last_keep_ms = now_ms; } else if (now_ms - last_keep_ms >= COOL_MS) { // 遮挡已结束且冷却完成,准备下一次计数 state = IDLE; } break; } }这个状态机的逻辑并不复杂,关键在于把“有效遮挡”和“干扰脉冲”区分开。HOLD_MS设得太小会过滤不掉窄干扰,设得太大则快速通过的物体会被漏计。我的经验是先量通过遮挡的最短时间,取它的三分之一作为HOLD_MS的上限,再留出余量。比如传送带上最小零件遮挡时间为60ms,HOLD_MS可以设15到20ms。
LOCKED状态解决的是物体还没完全离开时不能再次计数的问题。如果两个物体挨得很近,前一个还没走完,后一个已经进入检测区,传感器信号会一直保持遮挡状态,这时候不应计数。等到信号恢复,再等COOL_MS时间,让传感器输出完全回落到稳定的高电平,才允许识别下一个事件。
3.3 快速连续通过的速度上限与补偿
软件消抖会限制最大计数速度。最直接的计算方式是:一个完整计数周期 = 物体遮挡时间 + 冷却时间。假设物体遮挡时间为80ms,冷却时间为80ms,那么理论最大计数速率是每秒约6个。如果产线速度更高,就需要调整HOLD_MS和COOL_MS,或者改用硬件计数器。
对于非常快的应用,比如每分钟几百次的冲压计数,软件轮询可能应付不来。这时我会用MCU的硬件计数功能,比如STM32的定时器外部时钟模式或者编码器模式,把传感器信号直接接到定时器的输入引脚,由硬件对边沿计数,MCU只负责周期性读取计数值。这种方式可以轻松应付几十kHz的信号,但要注意的是,硬件计数无法做复杂的消抖,所以前端信号调理电路必须做得足够干净。
还有一种情况是双传感器互补判定。在传送带的两个位置各放一个传感器,检测物体的到达顺序,可以判断物体的运动方向,还能在两个传感器的信号同时有效时判定为一次完整的通过。这个方案对方向区分有硬性要求的场景很实用,代价是硬件成本翻倍。我在知光模块里保留了第二路传感器接口,需要时扩展就行。
4. 完整落地流程:从原理验证到可视化看板
4.1 原型搭建与测试方法
从原理到实物,我建议先搭一个最简原型再往后推进。原型阶段的硬件清单很基础:一块STM32或ESP32开发板、一个槽型光电或对射传感器、一个面包板、几根杜邦线、一个万用表。先把传感器接到IO口,用串口打印原始电平状态,手动拿物体通过传感器,观察串口输出的变化。
这一步其实是在验证最底层的东西:传感器接线是否正确,上拉电阻有没有接,信号极性对不对,电平翻转是否干脆。别小看这个环节,我遇到过很多次接反信号、供电电压不足导致传感器输出异常的问题,如果直接跳到写完整代码,往往会被这些低级错误折磨一两天。原型验证通过后再把状态机代码烧进去,用同一物体反复通过,测试连续计数的一致性。
测试物料要覆盖不同情况。至少准备黑色、白色、金属、透明四种样品,分别测试。黑色物体对漫反射传感器不友好,透明物体对红外对射传感器可能检测不到,这些都要在原型阶段暴露出来。我还会用一块薄挡板模拟快速遮蔽和恢复,用来验证HOLD_MS参数设得是否合理。
4.2 固件参数配置与发布
固件里不应该写死太多参数,我把关键参数全部做成可配置项,烧录后用串口命令或者配置文件就能修改。这个做法在进场调试时非常省事,不用为了改一个冷却时间反复重新烧录固件。
| 参数 | 默认值 | 说明 |
|---|---|---|
| HOLD_MS | 20 | 遮挡确认时间,决定最快可识别速度 |
| COOL_MS | 80 | 计数后冷却时间,防止连续重复计数 |
| 通道使能 | 1 | 哪一路传感器参与计数 |
| 计数上限 | 999999 | 超出后自动清零或保持 |
| 上报间隔 | 5s | 定时上报计数值,也可边沿变化即时上报 |
调试模式下,固件还要输出事件日志,每条日志带时间戳和状态机迁移信息。现场出现问题的时候,靠这些日志可以快速还原当时的信号时序,而不用靠猜。我自己习惯把日志写成一行一条,比如“12345ms state=CONFIRMING blocked=1”,方便在串口助手或者终端里直接查看。
发布固件前要做一个连续压力测试。我会用一个小电机带动圆盘,盘上固定一个挡片,让它持续转动来模拟连续通过,跑大约一万次,记录误计次数。早期版本我遇到过一万次里多计了三次的情况,后来发现是HOLD_MS偏小,挡片边缘在临界位置抖动被当成了两次。调整参数后再次测试,误计数降到零,这台测试工装也一直保留下来,每次改代码都重新跑一遍。
4.3 数据上报与可视化看板搭建
计数结果只在本地设备里没有意义,必须能汇总到看板上。知光模块的数据上报方式我做了两种:一种是传统工业现场用RS485 Modbus,PLC或上位机定时轮询读取;另一种是Wi-Fi设备直接走MQTT,上报JSON格式数据,适合轻量级看板。
MQTT上报的数据结构设计得比较简单,核心是设备标识、时间戳和各个通道的计数值,例如:
{ "device_id": "zhiguang-count-01", "ts": 1712312345, "channels": { "ch1": 12345, "ch2": 9876 } }看板我推荐先用Node-RED搭一个最小可用版本,它自带MQTT节点,收到消息后在UI里直接显示数字,十分钟内就能跑通。后续如果要做历史趋势和报表,可以对接Grafana或者InfluxDB,用时间序列数据库存储计数快照,绘制产量曲线。注意数据上报和本地计数要解耦,设备本地始终维护最新计数值,通信中断时先缓存,恢复后再补报,避免断网几分钟导致产量数据丢失。
5. 现场调试的坑与排查技巧
5.1 数字乱跳:先查电源和地
现场技术员反馈最多的问题是“数字自己会跳”。第一次遇到时我以为是传感器坏了,后来用示波器一看,传感器供电线上全是几十毫伏的纹波,而且传感器地和MCU地之间存在0.5V左右的电位差。原因是24V开关电源在大功率负载变化时输出电压波动,传感器在这种供电条件下输出信号自然不干净。
排查方法很直接:拿万用表测传感器供电脚和地脚之间的电压纹波,再把示波器探头夹在传感器输出信号上看波形。如果是电源问题,在传感器供电端加一只100µF电解电容和100nF陶瓷电容,通常能压掉大部分纹波。地电位差的问题则需要把传感器返回地和主控板地单独用粗线相连,避免把地线走成环。
5.2 强光干扰与粉尘环境
车间里最头疼的是自然光和环境光源带来的干扰。红外对射传感器抗强光能力通常不错,但阳光直射接收管时仍然可能让接收电路饱和,导致输出状态异常。我的处理方式有两个方向:机械上给传感器加遮光罩,把检测区域之外的光线挡掉;电气上选择带调制的传感器,也就是发射端调制到几十kHz频率,接收端只放大同频信号,这样能显著抑制环境光的直流分量。
粉尘环境是另一个隐形杀手。对射式传感器使用时间长了,发射端和接收端的透镜会蒙上一层灰,光强衰减到一定程度后,物体还没完全离开光路,接收端已经认为光线恢复,导致冷却时间提前结束,出现重复计数。维护上要定期清洁透镜,更稳妥的办法是在设计安装位时让透镜朝下,减少积灰面积,有条件还能加气吹装置。
5.3 常见问题速查表
| 现象 | 可能原因 | 处理措施 |
|---|---|---|
| 计数偏多 | 信号抖动、接线引入毛刺 | 增大HOLD_MS,检查地线,加RC滤波 |
| 计数偏少 | HOLD_MS太大、物体遮挡过快 | 实测最快通过遮挡时间,下调HOLD_MS |
| 数字完全不动 | 传感器损坏、接线松动 | 串口看原始电平,用万用表量传感器供电 |
| 干扰导致偶发跳变 | 电机启停、继电器吸合噪声 | 光耦隔离、屏蔽线、远离动力线走线 |
| 物体经过不识别 | 黑色物体、反光角度差 | 换成对射式或槽型传感器 |
| 看板数据不对 | 设备上报失败、断网丢数据 | 设备本地增加缓存,恢复后补报 |
5.4 调试工具与参数调优建议
调试计数问题,我的常备工具是万用表、示波器、逻辑分析仪和一根USB转TTL串口线。万用表负责量电压和检查通路,示波器负责看信号波形和噪声,逻辑分析仪在需要同时观察多路信号时序时非常好用。串口线则用来直接读取MCU打印的事件日志。
参数调优不要凭感觉。先摸清楚现场的实际运动参数,用秒表或示波器量一个物体从进入检测区到完全离开的时间,再把这组数据代入状态机逻辑里去推导合理的HOLD_MS和COOL_MS。比如测得遮挡时间是100ms,冷却时间应该留出至少50%的余量,设80ms比较合适。同时要考虑到最极端的情况,比如两个物体紧挨着连续通过,这时遮挡时间会被拉长,状态机应该把这种连续遮挡识别为多次独立事件,关键是COOL_MS之后要立即重新开始判断,而不是死等一个完整的空闲时间。
还有一个小技巧:在固件里加入统计变量,把“检测到的遮挡次数”“有效计数次数”“被过滤掉的抖动次数”分别记录下来。现场调试时直接看这三个数,就能知道问题出在物理层还是软件层。如果抖动次数很高,优先查硬件干扰;如果有效计数次数和实际数量对不上,再看状态机参数是否合理。
最后分享一个我自己的习惯:每次改完计数逻辑,我都会用同一个测试工装跑一万次连续触发,记录误计次数再上线。知光项目走到今天,这个模块前前后后改了六版,大部分时间都不是在写功能,而是在跟“为什么多了一个数字”死磕。如果你也在做类似的东西,不用急着堆功能,先把一次通过只记一次这件事做到位,后面所有数据才有意义。