从闯红灯检测到超声波测距:STM32 传感器实战
从红外反射原理出发,读懂 GPIO、回波计时与应用状态判断。
一只蜂鸣器响起来,说明程序读到了某个电平;屏幕上出现“20.0 cm”,说明程序完成了一次数值换算。但这两件事还不能证明系统已经可靠地识别了车辆,或者准确地测出了距离。
我觉得,传感器开发最值得琢磨的地方,就是把物理现象、电子电路、软件采样和应用规则连起来。这篇文章,我想用两个 STM32 实验——红外遮挡报警和超声波测距——把这条链路讲清楚:传感器究竟感知了什么,单片机怎样读懂它,程序又该如何把原始信号变成有意义的判断。
如果你已经学过一点 C 语言,正在接触 STM32,可以顺着“原理 → 接线 → 源码 → 改进”的顺序读下去。我会先拆解现有程序,再给出自己的扩展思路,其中几个注释与实现不一致的细节,也很值得拿出来讨论。
先说明一点:这篇文章侧重原理和源码分析,不是一份上板实测报告。文中的数值示例用于推导,扩展代码用于说明设计思路;实际电平、时序和测量精度,还需要结合自己的硬件验证。
关键词:STM32L431 · 红外反射 · ST118 / ST188 · HC-SR04 · GPIO · EXTI · DWT · 状态机
目录
- 先看清两个实验实际完成了什么
- 红外传感器:先分清主动、被动与转换原理
- 反射式红外:从光强变化到 GPIO 电平
- 实验准备与接线核对
- B01 源码解析:遮挡报警怎样运行
- 从遮挡检测扩展到闯红灯演示
- HC-SR04:用声音的往返时间测距离
- B02 源码解析:EXTI 与 DWT 如何配合
- 让测距程序更可靠的六个改进点
- 实验验证、记录方法与排障
- 红外与超声波如何选择、如何组合
- 总结:从“读到数”走向“可信的判断”
一、先看清两个实验实际完成了什么
我读这类实验工程时,会先对照三个地方:实验名称、注释说明和实际代码。它们不一定完全一致。把这个问题弄清楚,后面才不会把尚未实现的功能当成程序已有的能力。
| 项目 | 实验定位 | 源码已经实现的功能 | 还需要补充或确认的部分 |
|---|---|---|---|
| B01 闯红灯实验 | 反射式光电检测 | 读取数字输入;显示“有遮挡/无遮挡”;控制蜂鸣器;状态改变时打印串口信息 | 红绿灯切换、方向识别、越线判定和事件计数 |
| B02 超声波实验 | 测距与车位检测教学 | 触发测量;中断记录回波时间;换算距离;LCD 与串口输出 | 注释所说的“20 cm 有车/无车”判断,在这里分析的完整版本中还没有实现 |
我会沿着 main.c → 传感器驱动 → handle.c 这条路线阅读代码:先看初始化和调用顺序,再看信号如何读取,最后看显示与业务判断。这样能把零散的函数串成一条完整的数据流。
这里有一个我想贯穿全文的区别:红外开关输出的是“是否达到检测条件”;超声波模块输出的是与距离相关的时间信息。 二者都能服务于“有没有车”的问题,但软件所承担的工作不同。
二、红外传感器:先分清主动、被动与转换原理
2.1 红外是电磁波,不等于“温度传感器”
红外线是波长大于可见红光、处于可见光与微波之间的电磁波,波长范围通常近似写作 0.76~1000 μm。不同应用领域对波段的分界略有差异。对嵌入式开发来说,我更关心器件实际响应哪个波段,以及它能否接收到预期的信号。
同样叫“红外传感器”,可能是在接收红外 LED 的反射光,也可能是在接收目标自身的热辐射。前者可以检测纸片、工件或小车经过;后者可以用于人体移动感应、测温或热成像。
不能因为红外辐射与热现象有关,就把所有红外器件都理解成依靠物体温度工作。 例如,B01 的反射检测就不要求被测物体是热源。
2.2 两种分类维度不要混为一谈
| 分类维度 | 类别 | 核心问题 | 典型例子 |
|---|---|---|---|
| 是否主动照射目标 | 主动式 | 系统自己发光,再看光路如何变化 | 对射、漫反射、回归反射 |
| 是否主动照射目标 | 被动式 | 接收目标及背景的辐射 | PIR、红外测温、被动热成像 |
| 能量如何变成电信号 | 光子型 | 入射光引起载流子变化 | 光电二极管、光敏三极管 |
| 能量如何变成电信号 | 热型 | 吸收辐射后,温度变化引起电学量变化 | 热电堆、热释电元件、微测辐射热计 |
“主动/被动”描述光源关系,“光子型/热型”描述转换机制,不是同一层面的互斥选项。我把它们分开列出来,是因为这两种分类很容易在学习时混淆。热释电也只是热型探测的一种,不能代表所有被动红外传感器。
2.3 PIR 为什么不能直接替代反射式检测
PIR 利用热释电效应,对落在敏感元上的红外辐射变化产生响应。配合菲涅尔透镜形成的多个视区,移动人体更容易造成可检测的变化。稳定不动的热源不一定能维持持续触发。
因此,普通 PIR 更适合“有人活动吗”,不适合直接回答“这个静止的小车是否仍停在指定位置”。此外,“被动”只表示不主动发出探测光,并不表示整个模块无需供电。
说到红外检测,还经常会遇到塞贝克、珀尔帖和汤姆逊效应等概念。围绕这两个实验,我建议先抓住最有用的区别:热电堆利用温差产生电信号;热释电元件响应自身温度的变化;反射式光电接收器则直接响应入射光。 把这三条路径分开,后续接线和读程序就不容易混淆。
三、反射式红外:从光强变化到 GPIO 电平
3.1 三种常见光路
| 方式 | 发射与接收的布局 | 目标如何影响光路 | 安装与使用要点 |
|---|---|---|---|
| 对射式 | 发射器与接收器位于两侧 | 目标经过时减少接收光 | 两端需要对准,适合检测通道穿越 |
| 漫反射式 | 发射器和接收器在同侧 | 目标反射的光进入接收器 | 布线方便,但受表面反射率、距离和角度影响 |
| 回归反射式 | 同侧探头,对面安装反光板 | 目标遮断往返光路 | 反光板不需供电;不能随意用普通镜子替代 |
这里描述的是工作结构,不意味着每一类都具有固定的量程。透明物体也不是绝对不可检测,而是需要匹配光学结构和专用型号。OMRON 光电传感器原理与分类
3.2 B01 的信号处理链路
红外发射器 → 被测表面 → 反射光进入接收器
↓
接收电路输出电压变化
↓
整形/比较电路(若模块配有)
↓
STM32 GPIO 读取 0 或 1
↓
显示、蜂鸣器与应用判断
我用这张简图梳理信号的来龙去脉,它并不对应某块实验板的完整原理图。裸光敏三极管输出首先是模拟电学量;成品模块可能加入比较器,把它转换成数字开关量。GPIO 读到的 0/1 已经丢失了大部分光强细节,不能据此直接算出精确距离。
3.3 器件型号和驱动名称要分开看
学习反射式红外检测时,经常会接触到 ST188 这类器件。到了具体工程里,还会遇到另一套软件命名:例如 B01 的驱动使用 ST118 作为名称,对外提供 ST118_Init() 和 ST118_Read_State()。
这里我会把“驱动叫什么”和“板上装了什么”分开。函数名只是软件接口,不能代替实物型号,更不能用来推断封装、检测距离或供电要求。后面的代码分析沿用 ST118 的命名;真正接线和选参数时,还是要看器件丝印、原理图与数据手册。
3.4 “白色高、黑色低”不是通用规则
以一种常见接法为例:光敏三极管的集电极接电源,发射极经电阻接地,并从发射极取信号。在正常工作范围内,反射光增强,输出电压通常随之升高。如果改从集电极取样,或调整比较器接法、加入反相电路,数字逻辑就可能相反。
黑白纸实验也依赖材料在所用红外波段的反射率,不能只看肉眼颜色。透明、光亮、倾斜或纹理复杂的目标,均需要单独验证。
实际开发中,建议先建立明确的转换关系:
原始 GPIO 电平 → 根据实测极性归一化 → object_present
之后的报警和计数只使用 object_present,避免把“高电平”与“有物体”散落在多个函数里。
四、实验准备与接线核对
4.1 平台与工具
工程目标器件为 STM32L431VCTx,使用 STM32 HAL,配有 LCD、LED、串口等板级驱动。B01 额外初始化蜂鸣器;B02 初始化 DWT 周期计数器。工程目录中提供了 Keil 的 .uvprojx 工程文件。
复现时需要实验主板、对应传感器、可靠电源、下载器及适配的开发环境。万用表用于核对供电和输出电平;示波器或逻辑分析仪用于检查 TRIG 脉宽与 ECHO 时序。调试串口按源码配置为 115200 bit/s、8 数据位、无校验、1 停止位、无硬件流控。
4.2 以实际宏定义为准的引脚表
| 功能 | 工程中的配置 | 工作模式 | 依据 |
|---|---|---|---|
| B01 红外信号输入 | PD14 | 普通输入,GPIO_NOPULL |
Hardware/ST118/st118.c |
| B02 TRIG | PD11 | 推挽输出 | Hardware/HCSR04/hcsr04.c |
| B02 ECHO | PD9 | 上升沿和下降沿外部中断 | 同上 |
| B02 ECHO 中断入口 | EXTI9_5_IRQHandler |
EXTI 第 5~9 线共用入口 | 同上 |
| 调试串口 USART2 | PA2 / PA3 | TX / RX,AF7 | Src/usart.c |
B01 驱动的旧注释出现过 PB0,但宏定义实际为 GPIOD + GPIO_PIN_14。这份代码使用的是 PD14,接线时不能照搬旧注释。 MCU 端口名也不等于实验板排针的物理编号,连线前还要核对板上接口标识。
4.3 先确认电气兼容,再通电
常见 HC-SR04 模块采用 5 V 供电。STM32L431xx 的 MCU 供电范围则为 1.71~3.6 V;“多数 GPIO 可耐受 5 V”不等于所有引脚、所有模式、所有上电顺序均可直接连接。需核对具体引脚的 I/O 类型、数据手册限制及实验板上是否已有转换电路。HC-SR04 电气参数、STM32L431xx 数据手册
建议按以下顺序操作:
- 断电后识别 VCC、GND、OUT / TRIG / ECHO,确认连接器方向。
- 确认主板与传感器共地,不能仅连信号线。
- 查明传感器输出的高电平幅度与驱动方式。
- 必要时加入匹配的电平转换;开漏输出还要核对上拉电压。
- 在接入 MCU 前或通过安全测量点检查信号,避免把未知电压直接接到 GPIO。
“5 V 供电、OUT 接 GPIO”看起来只有两句话,实际却涉及输出电平、输入耐压和上电状态。我的建议是先把这些电气条件确认清楚,再讨论程序为什么读不到信号。
五、B01 源码解析:遮挡报警怎样运行
5.1 主循环只负责持续调度
下面摘录主流程,省略打印与时钟配置的展开代码:
HAL_Init();
SystemClock_Config();
LCD_Display_Init();
gpio_module_open(BEEP | LEDS);
uart_open(uart2, 115200);
ST118_Init();
while (1) {
ST118_Handle();
}
真正的采样节奏放在 ST118_Handle() 里:
if ((HAL_GetTick() - tickstart) >= 1000) {
tickstart = HAL_GetTick();
st118_state = ST118_Read_State();
/* 后续执行显示、蜂鸣器控制和串口输出 */
}
这是一种基于系统节拍的周期调度。while(1) 虽然不停循环,但传感器大约每 1000 ms 才读取一次。无符号时间差写法也比直接比较某个绝对截止时间更适合处理节拍计数回绕,前提是使用匹配的无符号类型与合理的比较间隔。
5.2 应用层实际如何解释电平
| 读到的状态 | LCD 显示 | 蜂鸣器 |
|---|---|---|
st118_state == 1 |
有遮挡 | beep_open() |
st118_state == 0 |
无遮挡 | beep_close() |
驱动函数只是返回 HAL_GPIO_ReadPin() 的结果,并未反相。这里有个容易踩坑的地方:驱动注释写的是“1 无遮挡、0 有遮挡”,正好与应用层分支相反。从实现来看,程序采用的是上表中的对应关系;至于它是否符合实物输出,还得上板测量。这也是我建议先打印原始 GPIO、再调报警逻辑的原因。
5.3 为什么串口没有每秒重复输出
程序使用静态变量 st118_state_rev 保存上一次状态,只在状态改变时打印。它初始为 2,而正常 GPIO 结果是 0 或 1,因此第一次有效采样也会输出日志。
这能减少串口刷屏,但要区分:日志没有变化,不等于程序没有采样。 LED 的周期翻转提供了另一个运行提示;LCD 和蜂鸣器仍在采样分支中更新。
5.4 一秒一次适合演示,不适合保证捕获快速通过
假设目标只在检测区停留 100 ms,而采样间隔为 1000 ms,目标可能恰好在两次采样之间经过,程序完全看不到。这是时间尺度上的漏检,不是传感器光学响应慢。
改进时可提高采样频率或使用边沿中断,但应同时考虑输入抖动、目标最短停留时间及 LCD/串口开销。用于消抖的稳定窗口也不能长到把真实的短事件过滤掉。
六、从遮挡检测扩展到闯红灯演示
6.1 业务条件必须明确
只有“有遮挡”这个输入,无法区分红灯与绿灯,也无法确认目标行进方向。若要在模型路口演示“闯红灯”,至少要增加交通灯状态和检测区进入事件,并让探头的安装位置对应模型中的越线位置。
为了把问题讲清楚,我先给模型路口定一条简单的规则:
当有效交通灯状态为红灯,且目标新进入预先定义的越线检测区时,记录一次报警事件。
这只是模型演示规则,不是完整的道路交通判定系统。车辆是否在变灯前已经进入、方向如何、检测区与停止线的关系,都需要额外信息。
6.2 用边沿事件避免重复计数
我的处理思路是只记录“从无到有”的进入事件,而不是每次读到“有目标”就累加。下面是据此写出的扩展示意代码,尚未做上板验证。输入 present 已完成电平极性归一化与滤波;red_phase 表示已确认的红灯状态。
#include <stdbool.h>
#include <stdint.h>
static bool primed = false;
static bool previous_present = false;
static uint32_t violation_count = 0;
bool UpdateCrossing(bool present, bool red_phase)
{
/* 启动时先建立基线,不把已存在的目标当作新进入。 */
if (!primed) {
previous_present = present;
primed = true;
return false;
}
bool entered = present && !previous_present;
previous_present = present;
if (red_phase && entered) {
++violation_count;
return true; /* 交给独立的报警任务处理 */
}
return false;
}
| 已有状态 | 当前状态 | 灯色 | 本次是否新增事件 |
|---|---|---|---|
| 无目标 | 有目标 | 红灯 | 是 |
| 有目标 | 有目标 | 红灯 | 否,目标仍停留 |
| 无目标 | 有目标 | 绿灯 | 否 |
| 有目标 | 无目标 | 任意 | 否,更新状态等待下一次进入 |
这个示例还存在采样分辨率限制:如果目标进入与灯色切换发生在同一采样间隔内,单靠两个布尔值不能判断先后。进一步实现时,应记录事件时间戳,并规定边界情况的处理方式。
6.3 将采样、判断和显示分开
建议把程序拆成三层:采样层快速更新稳定输入;判断层产生一次性的进入/离开事件;显示层以较低频率刷新 LCD、输出日志和控制报警时长。
需要区分方向时,可考虑沿运动路径设置 A、B 两个检测区,通过 A→B 或 B→A 的顺序判断方向,并增加超时窗口。但两个探头也不能自动解决并行目标、遮挡或倒退问题,仍需限定演示场景。
七、HC-SR04:用声音的往返时间测距离
7.1 TRIG 与 ECHO 各自负责什么
HC-SR04 将超声发射、接收与部分信号处理封装在模块内部。MCU 给 TRIG 一个触发脉冲,模块发出声波,再通过 ECHO 高电平宽度表达测距时间。常见模块要求 TRIG 高电平至少保持 10 μs,随后发出 8 个周期的 40 kHz 声波。测量周期建议大于 60 ms,以减少前后测量相互干扰;使用兼容型号时,还要确认其时序要求。HC-SR04 时序说明
MCU: TRIG 触发 等待 ECHO 边沿 处理本次结果
│ │ │
模块: 发出声波 ───→ 目标反射 ───→ 接收到回波
ECHO: ________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾|________
↑ ↑
t_start t_end
└── 脉宽 Δt ───┘
该图只表达事件顺序,不按比例绘制,也不表示 TRIG 与 ECHO 必须同时上升。
7.2 距离为什么要除以二
声波先从传感器到目标,再从目标返回。测到的是往返时间,因此:
d = c × Δt / 2
其中,d 是单程距离,c 是声速,Δt 是往返时间。按原工程采用的近似声速 340 m/s:
c = 340 m/s = 0.034 cm/μs
d(cm) = 0.034 × Δt(μs) / 2
= 0.017 × Δt(μs)
≈ Δt(μs) / 58.8
计算示例,不是实测数据:若 ECHO 高电平宽度为 1176 μs,按源码的 1176 / 58.8 算法,结果为 20.0 cm。
7.3 时间分辨率不等于距离准确度
计时足够细,只意味着软件能分辨更小的时间差。距离准确度还取决于声速估计、时钟误差、回波检测、目标角度和材料、安装方式等因素。
HC-SR04 常见的标称测量范围为 2~400 cm,但这不是任意材质、任意尺寸目标都能达到的保证。低于近距离盲区时不应简单相信读数;没有回波也不能当成 0 cm。HC-SR04 测量参数
八、B02 源码解析:EXTI 与 DWT 如何配合
8.1 用外部中断记录 ECHO 边沿
源码把 PD9 配置为 GPIO_MODE_IT_RISING_FALLING。ECHO 上升时记录起始计数,下降时记录结束计数,再计算差值。其核心思想可概括为:
/* 原工程逻辑的节选与整理 */
if (HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin)) {
HCSR04.trigger_flag = 1;
HCSR04.start_time = DWT_CYCCNT;
} else if (HCSR04.trigger_flag) {
HCSR04.end_time = DWT_CYCCNT;
if (HCSR04.end_time > HCSR04.start_time) {
HCSR04.value = HCSR04.end_time - HCSR04.start_time;
HCSR04.flag = 1;
}
HCSR04.trigger_flag = 0;
}
完整中断函数还检查和清除 EXTI 挂起位。移植时不能只复制上述片段而忘记中断标志处理;也不要另写一个同名 EXTI9_5_IRQHandler(),导致重复定义或分散处理。原文件注释中的 EXTI0_IRQHandler 并不是当前实现使用的入口。
8.2 DWT 记录的是 CPU 周期数
DWT_Init() 使能周期计数功能。源码中定义 SYSCLOCK = 80,单位是 MHz,意味着每微秒按 80 个 CPU 周期换算。
Δt(μs) = 周期数 / 80
d(cm) ≈ 周期数 / 80 / 58.8
B02 配置使用 HSE 和 PLL;结合工程中的 8 MHz HSE 定义,配置目标为 8 / 1 × 20 / 2 = 80 MHz。这与驱动常量吻合,但实物晶振及运行配置仍需核对。若移植到 40 MHz 平台却仍除以 80,计算结果会约为真实距离的一半。
还应注意源码表达式:
HCSR04.distence = HCSR04.value / SYSCLOCK / 58.8;
HCSR04.value 与 SYSCLOCK 都是整数,前一次除法会先截断不足 1 μs 的部分。需要保留这部分分辨率时,可以采用浮点除法;这只是数值精度改善,并不自动提高传感器本体的准确度。
8.3 函数名容易造成的误解:触发不等于本次结果已就绪
HCSR04_Handle() 先发送触发脉冲,再立即检查 HCSR04.flag。但声波传播和回波中断在稍后才完成。因此,它并不是“等待本次测量完成再返回”的同步接口。
在通常的时序下,第一次调用返回全局结构体初值 0;随后中断记录测量结果;下一次调用时,主循环才消费上一轮结果。没有新回波时,它还可能继续返回保留的旧距离。这里的关键问题不是显示能否变化,而是返回值是否属于刚触发的那次测量。
8.4 两次限幅隐藏了什么
原工程在驱动层把大于等于 400 cm 的计算值限制为 400;在 Distance_Handle() 中又把大于 100 cm 的值限制为 100,然后再用于 LCD 和串口输出。
因此,屏幕显示 100.0 cm,不能证明目标真的位于 100 cm;它也可能是更大的结果被截断。 100 cm 是这份程序的呈现上限,不是 HC-SR04 的名义量程。
此外,串口只在 distance > 2 时打印距离;LCD 则仍会刷新。这解释了为什么某些情况下屏幕有数值,串口却没有对应的测距输出。
8.5 注释中的“20 cm 车位判断”仍需补写
完整版本的 Distance_Handle() 没有按 20 cm 分支切换“有车/无车”,而是固定绘制一个车位图标。若要实现这一教学目标,应先取得有效、未过期、未被显示限幅替代的距离,再执行状态判断。
九、让测距程序更可靠的六个改进点
读完测距流程,我最想继续改进的是结果是否及时、异常是否可见,以及时间基准是否可靠。下面六点是我的改进思路,需要在现有程序之外继续实现。
9.1 把触发、完成和超时做成明确的状态
IDLE ── 发出 TRIG ─────→ WAIT_RISE
WAIT_RISE ── 检测到上升沿 ──→ WAIT_FALL
WAIT_FALL ── 检测到下降沿 ──→ READY
WAIT_RISE ── 超时 ──────────→ TIMEOUT
WAIT_FALL ── 超时 ──────────→ TIMEOUT
READY / TIMEOUT ── 主循环消费结果 ──→ IDLE
更准确地说,WAIT_FALL 在正常下降沿时进入 READY,在超时时进入 TIMEOUT;两种结果都由主循环消费,再回到 IDLE。触发前应确认上一轮结束、ECHO 状态合理,并满足模块测量间隔。
超时阈值应结合模块规格和最大测距时间设置。以 4 m、340 m/s 估算,往返时间约为 23.5 ms,但还需给模块响应留余量,不能把这个理论值机械当成所有模块的超时参数。
9.2 距离必须带上有效性与时间戳
可以让上层接收一份结果记录,而不只是一个 float:
/* 建议的数据接口;不是原工程定义 */
typedef enum {
RANGE_OK,
RANGE_TIMEOUT,
RANGE_OUT_OF_RANGE
} RangeStatus;
typedef struct {
float distance_cm;
uint32_t timestamp_ms;
RangeStatus status;
} RangeSample;
有效结果可以显示距离;超时显示“无有效回波”;超出量程显示“超量程”。若暂时保留最后一次有效值,应同时标明它已经过期,避免旧数据看起来仍像实时测量。
9.3 不要依赖空循环精确产生微秒延时
原 Delay_us() 通过 while(i--) 估算延时。循环执行时间受编译优化、指令生成等因素影响,空循环甚至可能被优化掉;虽然调用写着 Delay_us(12),静态源码并不能证明引脚实际高电平恰好保持 12 μs。
可改为基于 DWT 差值等待,或由定时器输出脉冲,并用示波器验证 TRIG。DWT 方案仍要检查计数器是否已启动、系统时钟是否正确,以及调试暂停或低功耗模式是否影响计时。
9.4 正确处理计数回绕与共享数据
80 MHz 下,32 位计数器约每 2³² / 80,000,000 ≈ 53.7 s 回绕一次。原代码要求 end_time > start_time,会拒绝跨回绕的有效脉宽。
对于短于一个计数周期、且经过超时约束的测量,可用无符号模减法:
uint32_t elapsed_cycles = end_cycles - start_cycles;
float elapsed_us = (float)elapsed_cycles / 80.0f;
float distance_cm = elapsed_us / 58.8f;
原程序还在触发之后的结果处理阶段清零 DWT。如果新测量已经开始,这可能扰动本次时间基准;让计数器自由运行通常更容易管理。
中断与主循环共同访问的标志及计数数据,应检查可见性与一致性。volatile 能约束部分编译器优化,但不能让一组字段自动成为完整、原子的快照。可以用短临界区复制结果、序号校验或双缓冲,并规定先写数据、后发布就绪标志的顺序。
9.5 用滤波与滞回稳定车位判断
以车位模型为例,可以设计“距离 ≤18 cm 进入占用,距离 ≥22 cm 退出占用,中间保持原状态”的滞回规则。这是围绕 20 cm 给出的示例参数,不是实验标定结果。
处理顺序建议为:
原始测量 → 有效性检查 → 去除异常值/中值滤波 → 滞回判断 → 显示
不要把超时伪装成 0 加入滤波器;没有有效历史状态时,中间区域可以保持“待确认”。滤波能减少跳变,但也会增加响应延迟,应通过测试确定窗口大小。
9.6 保留原始值,单独处理显示上限
业务判断使用真实有效距离,界面需要限幅时另建显示变量。例如,结果大于 100 cm 时显示“>100 cm”,而不是把原始值覆盖成 100。
若后续对精度要求更高,还应评估声速随环境变化带来的误差,以及中断延迟的抖动。硬件定时器输入捕获可以减少软件记录边沿时间带来的不确定性,但前提是芯片引脚复用、布线和定时器资源允许,不能只替换函数而不检查硬件。
十、实验验证、记录方法与排障
10.1 红外实验:先验证电平,再验证业务
如果要验证这个实验,我会先保持目标位置和环境光不变,记录“无目标”和“目标进入”时的输出电压与 GPIO 值;然后逐项改变材质、距离、角度和光照。每次只改变一个变量,才能判断差异来自哪里。
| 测试条件 | 应记录的内容 | 判断重点 |
|---|---|---|
| 无目标 / 目标持续存在 | OUT 电压、GPIO、LCD、蜂鸣器 | 实物极性是否与应用层一致 |
| 黑纸 / 白纸,同距离同角度 | 是否稳定触发、临界位置 | 反射差异是否足够可靠 |
| 快速经过检测区 | 实际经过次数、识别次数 | 1 s 采样是否漏检 |
| 目标停留数秒 | 检测状态与事件计数 | 持续存在是否被重复计数 |
| 改变环境光 | 空场误报、目标漏报 | 是否需要遮光、调节阈值 |
按 B01 的代码,GPIO 为 1 时应显示“有遮挡”并打开蜂鸣器。上板时可以把这个预期作为第一项检查,但不要反过来用程序里的文字证明传感器极性正确。
10.2 超声波实验:使用独立基准测量
将传感器固定,使其正对平整、尺寸足够的目标板,用尺子测量探头参考面到目标面的距离。记录所选参考面,避免一次从探头表面量、另一次从电路板边缘量。
可选择 5、10、20、50、80 cm 等测试点,每点重复采集至少 10 次有效读数,并单独计数超时或无效样本。若要测试超过 100 cm 的情况,需要先去除或区分应用层的 100 cm 限幅。
我把记录表的格式列在下面,复现时可以直接填入自己的测量结果。表格尚未填写实测数据。
| 基准距离 / cm | 有效读数均值 / cm | 最小~最大 / cm | 无效次数 / 总次数 | 平均误差 / cm |
|---|---|---|---|---|
| 5 | 待实测 | 待实测 | 待实测 | 待计算 |
| 10 | 待实测 | 待实测 | 待实测 | 待计算 |
| 20 | 待实测 | 待实测 | 待实测 | 待计算 |
| 50 | 待实测 | 待实测 | 待实测 | 待计算 |
| 80 | 待实测 | 待实测 | 待实测 | 待计算 |
平均误差 = 有效测量均值 − 基准距离
相对误差 = 平均误差 / 基准距离 × 100%
有效率 = 有效样本数 / 总测量次数 × 100%
相对误差也可取绝对值报告,但应注明口径。重复读数很接近只说明重复性较好,若它们整体偏离基准,仍可能存在系统误差。
10.3 常见问题按链路排查
| 现象 | 优先检查 | 原因或处理方向 |
|---|---|---|
| 红外状态始终不变 | 电源、共地、PD14、OUT 电压 | 引脚接错、输入悬空、阈值或光路不合适 |
| 红外显示与实际相反 | 原始 GPIO 与实测极性 | B01 注释与应用层逻辑存在矛盾 |
| 快速通过没有报警 | 采样周期及实际停留时间 | 目标可能在两次采样之间经过 |
| B02 学生工程没有持续测量 | code start 留空区域 |
主循环、处理逻辑或返回语句尚未补齐 |
| 首次显示 0,随后才更新 | flag 与触发/返回顺序 |
原接口通常消费前一轮回波结果 |
| 移走目标后仍显示旧距离 | 超时处理和结果时间戳 | 没有新回波时旧数据仍被返回 |
| 距离整体成比例偏大或偏小 | 实际主频、SYSCLOCK、单位换算 |
时间基准与换算系数可能不一致 |
| 屏幕总是 100 cm | 上层 distance > 100 分支 |
程序限幅,不等于传感器达到量程上限 |
| TRIG 无脉冲或脉宽不正确 | PD11、初始化、Delay_us() |
空循环延时不可靠或 GPIO 配置有误 |
| ECHO 有边沿但不更新 | PD9、EXTI9_5、NVIC、挂起标志 | 中断配置、入口冲突或结果交接问题 |
| 串口中文乱码 | 字符编码与串口参数 | 原工程中文源码使用 GBK 类编码;不要将乱码误判为测量错误 |
有效的排障顺序是:电源与接地 → 接口电平 → 引脚信号 → 中断与时间戳 → 换算 → 业务判断 → 显示。 没有必要在 TRIG 尚未输出时就调整滤波参数。
十一、红外与超声波如何选择、如何组合
11.1 根据需要的信息选择
| 对比项 | B01 反射式红外检测 | B02 HC-SR04 测距 |
|---|---|---|
| MCU 获得的信息 | 一个数字状态 | 回波脉宽,进一步换算距离 |
| 主要软件工作 | 极性归一化、滤波、事件判断 | 时序控制、计时、超时、距离处理 |
| 主要场景限制 | 表面反射率、距离、角度、环境光 | 声学反射、目标尺寸与角度、近距离盲区 |
| 适合演示的问题 | 目标是否进入指定检测区 | 目标距离多少、是否靠近某阈值 |
| 原程序的明显限制 | 约 1 s 采样,没有灯色条件 | 旧结果返回、没有明确超时状态、100 cm 显示限幅 |
这张表只比较文章中的两种实验方案。选型时仍要看具体型号:并非所有红外产品都只输出开关量,也不是所有超声波传感器都采用相同的接口。
11.2 一个可继续扩展的模型路口方案
如果把这两个实验继续往下做,我会尝试让超声波模块估计目标靠近程度,让红外探头标记目标进入指定位置的事件,再由交通灯状态机判断是否满足报警规则。事件记录中保留时间、灯色、红外状态和最近一次有效距离,方便回头分析。
但“多个传感器组合”不能简单写成两个条件相与:一个测的是位置附近的反射状态,另一个测的是声波方向上的目标距离,它们未必对应同一物体,也未必在同一时刻更新。要先校准安装位置、覆盖范围和时间戳,再定义关联窗口。
遇到传感器超时或数据过旧时,应报告“状态未知/传感器异常”,而不是自动解释为“没有车辆”。这一步能防止软件把缺失信息包装成确定结论。
十二、总结:从“读到数”走向“可信的判断”
B01 展示了最基础的传感器应用链路:把反射变化转换为数字电平,再控制显示和蜂鸣器。B02 往前走了一步:把回波脉宽转换为距离,并把时间测量引入单片机程序。
梳理完这两个实验,我更想留下的不是几个函数名,而是下面五条思路:
- 先确认传感器到底测什么。 反射光强、热辐射变化和声波飞行时间,不能混为一谈。
- 以实现和实物核对注释。 驱动名称、PD14/PB0 引脚差异,以及尚未实现的 20 cm 判断,都值得在动手前澄清。
- 把时间作为数据的一部分。 一秒采样会漏掉短事件;返回的距离也可能属于上一轮测量。
- 把无效状态单独表达。 0、100、400 和旧距离,都不应该承担它们没有被定义过的错误含义。
- 把物理检测与业务规则分开。 “有目标”不自动等于“闯红灯”;“有距离”也不自动等于“可靠识别车位”。
对我来说,一个传感器项目值得继续打磨的地方,就在这些细节里。屏幕能显示、蜂鸣器会响,只是开始;真正把它弄明白,还要能解释这个结果从哪里来、什么时候得到、在哪些条件下有效,以及出错时系统会怎样表现。
写评论
读完想说的,写在这里。
加载评论中…