
🔥铅笔小新z:个人主页
🎬博客专栏:stm32学习
💫滴水不绝,可穿石;步履不休,能至渊。

STM32 输入捕获实战:用 HC-SR04 做出可按键控制的超声波测距
- 适用平台:STM32F103ZET6、STM32CubeMX、STM32 HAL 库,系统主频 72 MHz
- 外设与技术点:HC-SR04、TIM4 输入捕获、TIM6/TIM7 基本定时器、GPIO、USART1
- 最终效果:三个按键分别控制测距、串口打印和近距告警;串口显示厘米距离,距离进入 2~10 cm 时点亮高亮 LED
- 适合读者:已经会写 C 语言、了解 GPIO 和定时器计数,但还没有独立完成过"传感器 + 中断 + 串口"小项目的人
这篇不讲"把外设都配一遍",只回答两个问题:
- 怎样稳定地产生 HC-SR04 需要的 10 μs 触发脉冲?
- 怎样准确测出 Echo 引脚高电平持续了多久,并把它换成厘米?
整条主线可以写成一句话:
PA1 发出 10 μs 高脉冲,模块拉高 PB8;TIM4 在上升沿记下起始计数,在下降沿记下结束计数;两个计数的差值就是回声往返时间,再除以 2 并乘声速,就得到距离。
#mermaid-svg-LaqJc9P6tcPHqZWl{font-size:20px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LaqJc9P6tcPHqZWl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LaqJc9P6tcPHqZWl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LaqJc9P6tcPHqZWl .error-icon{fill:#552222;}#mermaid-svg-LaqJc9P6tcPHqZWl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LaqJc9P6tcPHqZWl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LaqJc9P6tcPHqZWl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LaqJc9P6tcPHqZWl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LaqJc9P6tcPHqZWl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LaqJc9P6tcPHqZWl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LaqJc9P6tcPHqZWl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LaqJc9P6tcPHqZWl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LaqJc9P6tcPHqZWl .marker.cross{stroke:#333333;}#mermaid-svg-LaqJc9P6tcPHqZWl svg{font-size:20px;}#mermaid-svg-LaqJc9P6tcPHqZWl p{margin:0;}#mermaid-svg-LaqJc9P6tcPHqZWl .label{color:#333;}#mermaid-svg-LaqJc9P6tcPHqZWl .cluster-label text{fill:#333;}#mermaid-svg-LaqJc9P6tcPHqZWl .cluster-label span{color:#333;}#mermaid-svg-LaqJc9P6tcPHqZWl .cluster-label span p{background-color:transparent;}#mermaid-svg-LaqJc9P6tcPHqZWl .label text,#mermaid-svg-LaqJc9P6tcPHqZWl span{fill:#333;color:#333;}#mermaid-svg-LaqJc9P6tcPHqZWl .node rect,#mermaid-svg-LaqJc9P6tcPHqZWl .node circle,#mermaid-svg-LaqJc9P6tcPHqZWl .node ellipse,#mermaid-svg-LaqJc9P6tcPHqZWl .node polygon,#mermaid-svg-LaqJc9P6tcPHqZWl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LaqJc9P6tcPHqZWl .rough-node .label text,#mermaid-svg-LaqJc9P6tcPHqZWl .node .label text,#mermaid-svg-LaqJc9P6tcPHqZWl .image-shape .label,#mermaid-svg-LaqJc9P6tcPHqZWl .icon-shape .label{text-anchor:middle;}#mermaid-svg-LaqJc9P6tcPHqZWl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LaqJc9P6tcPHqZWl .rough-node .label,#mermaid-svg-LaqJc9P6tcPHqZWl .node .label,#mermaid-svg-LaqJc9P6tcPHqZWl .image-shape .label,#mermaid-svg-LaqJc9P6tcPHqZWl .icon-shape .label{text-align:center;}#mermaid-svg-LaqJc9P6tcPHqZWl .node.clickable{cursor:pointer;}#mermaid-svg-LaqJc9P6tcPHqZWl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LaqJc9P6tcPHqZWl .arrowheadPath{fill:#333333;}#mermaid-svg-LaqJc9P6tcPHqZWl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LaqJc9P6tcPHqZWl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LaqJc9P6tcPHqZWl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LaqJc9P6tcPHqZWl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LaqJc9P6tcPHqZWl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LaqJc9P6tcPHqZWl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LaqJc9P6tcPHqZWl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LaqJc9P6tcPHqZWl .cluster text{fill:#333;}#mermaid-svg-LaqJc9P6tcPHqZWl .cluster span{color:#333;}#mermaid-svg-LaqJc9P6tcPHqZWl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LaqJc9P6tcPHqZWl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LaqJc9P6tcPHqZWl rect.text{fill:none;stroke-width:0;}#mermaid-svg-LaqJc9P6tcPHqZWl .icon-shape,#mermaid-svg-LaqJc9P6tcPHqZWl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LaqJc9P6tcPHqZWl .icon-shape p,#mermaid-svg-LaqJc9P6tcPHqZWl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LaqJc9P6tcPHqZWl .icon-shape .label rect,#mermaid-svg-LaqJc9P6tcPHqZWl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LaqJc9P6tcPHqZWl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LaqJc9P6tcPHqZWl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LaqJc9P6tcPHqZWl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 按键 K0 / K1 / K2
三个使能标志
测量调度
PA1 输出 10 μs 触发
HC-SR04 发射与接收
PB8 输出 Echo 脉宽
TIM4 双通道捕获
计数差换算距离
状态灯 / 高亮告警灯
USART1 输出日志
图 1:系统数据流。按键只改"是否工作",真正的测距链路始终是 PA1 → 模块 → PB8。
这篇文章必须讲清 6 个知识点:
- HC-SR04 的 Echo 脉宽为什么代表往返时间。
- 输入捕获与普通 GPIO 轮询的本质区别。
- 为什么 PB8 一个引脚就能同时捕获上升沿和下降沿。
- PSC、ARR、CCR 在这个项目里各自负责什么。
- 为什么触发脉冲和按键扫描要交给两个基本定时器。
- HAL 回调、状态机和超时处理怎样把整条链路串起来。
1. 原理:先把"时间"变成"距离"
1.1 HC-SR04 到底做了什么

HC-SR04 正面有两个像"眼睛"的金属圆筒:一个发射超声波,一个接收回波。模块本身不直接输出厘米数,它只完成三件事:
- 收到触发信号后,发射 8 个连续的 40 kHz 超声脉冲;
- 同时把 Echo 引脚拉高;
- 收到回波后,把 Echo 引脚拉低。

所以 Echo 高电平的宽度,就是超声波从模块出发、碰到障碍物再返回的往返时间。这个过程和对着山谷喊一声再听回声很像,只不过这里把"声音跑了多久"换成了"高电平持续了多久"。
TIM4 HC-SR04 STM32 TIM4 HC-SR04 STM32 #mermaid-svg-hgyiH3q4mfvN6rKm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hgyiH3q4mfvN6rKm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hgyiH3q4mfvN6rKm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hgyiH3q4mfvN6rKm .error-icon{fill:#552222;}#mermaid-svg-hgyiH3q4mfvN6rKm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hgyiH3q4mfvN6rKm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hgyiH3q4mfvN6rKm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hgyiH3q4mfvN6rKm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hgyiH3q4mfvN6rKm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hgyiH3q4mfvN6rKm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hgyiH3q4mfvN6rKm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hgyiH3q4mfvN6rKm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hgyiH3q4mfvN6rKm .marker.cross{stroke:#333333;}#mermaid-svg-hgyiH3q4mfvN6rKm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hgyiH3q4mfvN6rKm p{margin:0;}#mermaid-svg-hgyiH3q4mfvN6rKm .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hgyiH3q4mfvN6rKm text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-hgyiH3q4mfvN6rKm .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hgyiH3q4mfvN6rKm .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-hgyiH3q4mfvN6rKm .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-hgyiH3q4mfvN6rKm .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-hgyiH3q4mfvN6rKm #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-hgyiH3q4mfvN6rKm .sequenceNumber{fill:white;}#mermaid-svg-hgyiH3q4mfvN6rKm #sequencenumber{fill:#333;}#mermaid-svg-hgyiH3q4mfvN6rKm #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-hgyiH3q4mfvN6rKm .messageText{fill:#333;stroke:none;}#mermaid-svg-hgyiH3q4mfvN6rKm .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hgyiH3q4mfvN6rKm .labelText,#mermaid-svg-hgyiH3q4mfvN6rKm .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-hgyiH3q4mfvN6rKm .loopText,#mermaid-svg-hgyiH3q4mfvN6rKm .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-hgyiH3q4mfvN6rKm .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hgyiH3q4mfvN6rKm .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hgyiH3q4mfvN6rKm .noteText,#mermaid-svg-hgyiH3q4mfvN6rKm .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-hgyiH3q4mfvN6rKm .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hgyiH3q4mfvN6rKm .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hgyiH3q4mfvN6rKm .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hgyiH3q4mfvN6rKm .actorPopupMenu{position:absolute;}#mermaid-svg-hgyiH3q4mfvN6rKm .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-hgyiH3q4mfvN6rKm .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hgyiH3q4mfvN6rKm .actor-man circle,#mermaid-svg-hgyiH3q4mfvN6rKm line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-hgyiH3q4mfvN6rKm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 触发前把 TIM4 的 CNT 清零 脉宽 = CNT_fall - CNT_rise PA1 输出 10 μs 高脉冲 发射 8 个 40 kHz 脉冲 Echo 变为高电平 CH3 捕获上升沿,锁存 CNT_rise Echo 变为低电平 CH4 捕获下降沿,锁存 CNT_fall
图 2:一次完整测距的时序。真正需要测量的是 Echo 高电平的持续时间。
距离公式是:
text
距离 = 声速 × 往返时间 ÷ 2
当 TIM4 的一个计数正好代表 1 μs 时,若回波脉宽为 t_us,声速取 343 m/s,也就是 0.0343 cm/μs:
text
距离(cm) = t_us × 0.0343 ÷ 2
= t_us × 0.01715
例如回波高电平持续 1000 μs:
text
1000 × 0.01715 = 17.15 cm
有的代码把声速取成 340 m/s,得到的结果也能用;本文取 343 m/s,对应约 20 ℃ 的常温空气。温度变化会改变声速,但这不是当前项目的首要矛盾。
知识小结
结论: HC-SR04 的 Echo 高电平宽度表示超声波的往返时间,距离等于"声速 × 脉宽 ÷ 2"。
原因: 超声波要先到障碍物再返回,脉宽包含了双程时间,所以最后必须除以 2。
关键参数: 触发脉宽至少 10 μs,模块内部发射 8 个 40 kHz 脉冲,常温声速可按 343 m/s 计算。
常见误区: 直接把"脉宽 × 声速"当成距离,会得到真实距离的两倍;把 Echo 当成模拟电压也不能得到距离,它承载的是时间信息。
1.2 为什么用输入捕获,而不是一直读 GPIO
假设你用普通 GPIO 轮询来测 Echo:
c
while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) == GPIO_PIN_RESET) {
/* 等上升沿 */
}
/* 再不停地读,直到变低 */
这当然能跑,但它有两个问题:
- CPU 被死循环占住,其他任务很难及时执行;
- 读取和判断都有软件延迟,记录的时间点不够准确。
输入捕获像一只由硬件按下的秒表:
- 定时器每 1 μs 让
CNT加 1; - Echo 出现上升沿时,硬件立刻把当时的
CNT复制到CCR3; - Echo 出现下降沿时,硬件再把当时的
CNT复制到CCR4; - 中断只需在事后读取两个值并相减。
这里要区分两个寄存器的作用:
- ARR 决定计数器数到哪里回绕。 在 PWM 输出里它决定周期和频率。
- CCR 存放比较值。 在 PWM 输出里它决定高电平长度;在输入捕获里,它保存的是边沿到来瞬间的
CNT快照,不是占空比。
#mermaid-svg-HTDy2NtUHf6sz9TG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HTDy2NtUHf6sz9TG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HTDy2NtUHf6sz9TG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HTDy2NtUHf6sz9TG .error-icon{fill:#552222;}#mermaid-svg-HTDy2NtUHf6sz9TG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HTDy2NtUHf6sz9TG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HTDy2NtUHf6sz9TG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HTDy2NtUHf6sz9TG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HTDy2NtUHf6sz9TG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HTDy2NtUHf6sz9TG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HTDy2NtUHf6sz9TG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HTDy2NtUHf6sz9TG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HTDy2NtUHf6sz9TG .marker.cross{stroke:#333333;}#mermaid-svg-HTDy2NtUHf6sz9TG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HTDy2NtUHf6sz9TG p{margin:0;}#mermaid-svg-HTDy2NtUHf6sz9TG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HTDy2NtUHf6sz9TG .cluster-label text{fill:#333;}#mermaid-svg-HTDy2NtUHf6sz9TG .cluster-label span{color:#333;}#mermaid-svg-HTDy2NtUHf6sz9TG .cluster-label span p{background-color:transparent;}#mermaid-svg-HTDy2NtUHf6sz9TG .label text,#mermaid-svg-HTDy2NtUHf6sz9TG span{fill:#333;color:#333;}#mermaid-svg-HTDy2NtUHf6sz9TG .node rect,#mermaid-svg-HTDy2NtUHf6sz9TG .node circle,#mermaid-svg-HTDy2NtUHf6sz9TG .node ellipse,#mermaid-svg-HTDy2NtUHf6sz9TG .node polygon,#mermaid-svg-HTDy2NtUHf6sz9TG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HTDy2NtUHf6sz9TG .rough-node .label text,#mermaid-svg-HTDy2NtUHf6sz9TG .node .label text,#mermaid-svg-HTDy2NtUHf6sz9TG .image-shape .label,#mermaid-svg-HTDy2NtUHf6sz9TG .icon-shape .label{text-anchor:middle;}#mermaid-svg-HTDy2NtUHf6sz9TG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HTDy2NtUHf6sz9TG .rough-node .label,#mermaid-svg-HTDy2NtUHf6sz9TG .node .label,#mermaid-svg-HTDy2NtUHf6sz9TG .image-shape .label,#mermaid-svg-HTDy2NtUHf6sz9TG .icon-shape .label{text-align:center;}#mermaid-svg-HTDy2NtUHf6sz9TG .node.clickable{cursor:pointer;}#mermaid-svg-HTDy2NtUHf6sz9TG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HTDy2NtUHf6sz9TG .arrowheadPath{fill:#333333;}#mermaid-svg-HTDy2NtUHf6sz9TG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HTDy2NtUHf6sz9TG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HTDy2NtUHf6sz9TG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HTDy2NtUHf6sz9TG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HTDy2NtUHf6sz9TG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HTDy2NtUHf6sz9TG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HTDy2NtUHf6sz9TG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HTDy2NtUHf6sz9TG .cluster text{fill:#333;}#mermaid-svg-HTDy2NtUHf6sz9TG .cluster span{color:#333;}#mermaid-svg-HTDy2NtUHf6sz9TG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HTDy2NtUHf6sz9TG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HTDy2NtUHf6sz9TG rect.text{fill:none;stroke-width:0;}#mermaid-svg-HTDy2NtUHf6sz9TG .icon-shape,#mermaid-svg-HTDy2NtUHf6sz9TG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HTDy2NtUHf6sz9TG .icon-shape p,#mermaid-svg-HTDy2NtUHf6sz9TG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HTDy2NtUHf6sz9TG .icon-shape .label rect,#mermaid-svg-HTDy2NtUHf6sz9TG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HTDy2NtUHf6sz9TG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HTDy2NtUHf6sz9TG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HTDy2NtUHf6sz9TG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Echo 上升沿
硬件锁存 CNT 到 CCR3
Echo 下降沿
硬件锁存 CNT 到 CCR4
中断读取起始时间
中断读取结束时间
脉宽 = 结束 - 起始
图 3:输入捕获把"计时"交给硬件,把"算差值"留给软件。
知识小结
结论: 输入捕获用硬件在边沿瞬间锁存定时器计数值,比 GPIO 轮询更适合测量精确脉宽。
原因: 定时器一直在独立计数,边沿触发时只需复制
CNT,软件中断延迟不会改变已经锁存的时间戳。关键参数: 捕获通道、边沿极性、定时器计数周期。本项目希望一个计数等于 1 μs。
常见误区: 把输入捕获的 CCR 当成占空比。这里的 CCR 是时间快照;PWM 输出时的 CCR 才常用来设置占空比。
1.3 一个 PB8 引脚,怎样同时捕获两个边沿
TIM4 的通道 3 对应 PB8。这里用到一个很实用的配置:
- 通道 3 选择直接输入捕获,监测上升沿,记录开始时间;
- 通道 4 选择间接输入捕获,也使用通道 3 的输入信号 TI3,但监测下降沿,记录结束时间。
"间接"不是另外接一根线,而是把 CH4 的捕获来源换成 TI3。因此外部只接 PB8,一根 Echo 线就够。
#mermaid-svg-zG3NyNDM2NpeOkh7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zG3NyNDM2NpeOkh7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zG3NyNDM2NpeOkh7 .error-icon{fill:#552222;}#mermaid-svg-zG3NyNDM2NpeOkh7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zG3NyNDM2NpeOkh7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zG3NyNDM2NpeOkh7 .marker.cross{stroke:#333333;}#mermaid-svg-zG3NyNDM2NpeOkh7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zG3NyNDM2NpeOkh7 p{margin:0;}#mermaid-svg-zG3NyNDM2NpeOkh7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zG3NyNDM2NpeOkh7 .cluster-label text{fill:#333;}#mermaid-svg-zG3NyNDM2NpeOkh7 .cluster-label span{color:#333;}#mermaid-svg-zG3NyNDM2NpeOkh7 .cluster-label span p{background-color:transparent;}#mermaid-svg-zG3NyNDM2NpeOkh7 .label text,#mermaid-svg-zG3NyNDM2NpeOkh7 span{fill:#333;color:#333;}#mermaid-svg-zG3NyNDM2NpeOkh7 .node rect,#mermaid-svg-zG3NyNDM2NpeOkh7 .node circle,#mermaid-svg-zG3NyNDM2NpeOkh7 .node ellipse,#mermaid-svg-zG3NyNDM2NpeOkh7 .node polygon,#mermaid-svg-zG3NyNDM2NpeOkh7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zG3NyNDM2NpeOkh7 .rough-node .label text,#mermaid-svg-zG3NyNDM2NpeOkh7 .node .label text,#mermaid-svg-zG3NyNDM2NpeOkh7 .image-shape .label,#mermaid-svg-zG3NyNDM2NpeOkh7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zG3NyNDM2NpeOkh7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zG3NyNDM2NpeOkh7 .rough-node .label,#mermaid-svg-zG3NyNDM2NpeOkh7 .node .label,#mermaid-svg-zG3NyNDM2NpeOkh7 .image-shape .label,#mermaid-svg-zG3NyNDM2NpeOkh7 .icon-shape .label{text-align:center;}#mermaid-svg-zG3NyNDM2NpeOkh7 .node.clickable{cursor:pointer;}#mermaid-svg-zG3NyNDM2NpeOkh7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zG3NyNDM2NpeOkh7 .arrowheadPath{fill:#333333;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zG3NyNDM2NpeOkh7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zG3NyNDM2NpeOkh7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zG3NyNDM2NpeOkh7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zG3NyNDM2NpeOkh7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zG3NyNDM2NpeOkh7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zG3NyNDM2NpeOkh7 .cluster text{fill:#333;}#mermaid-svg-zG3NyNDM2NpeOkh7 .cluster span{color:#333;}#mermaid-svg-zG3NyNDM2NpeOkh7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zG3NyNDM2NpeOkh7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zG3NyNDM2NpeOkh7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zG3NyNDM2NpeOkh7 .icon-shape,#mermaid-svg-zG3NyNDM2NpeOkh7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zG3NyNDM2NpeOkh7 .icon-shape p,#mermaid-svg-zG3NyNDM2NpeOkh7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zG3NyNDM2NpeOkh7 .icon-shape .label rect,#mermaid-svg-zG3NyNDM2NpeOkh7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zG3NyNDM2NpeOkh7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zG3NyNDM2NpeOkh7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zG3NyNDM2NpeOkh7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PB8 / TI3
TIM4 CH3
直接捕获
上升沿
TIM4 CH4
间接捕获 TI3
下降沿
CCR3 = CNT_rise
CCR4 = CNT_fall
图 4:CH3 和 CH4 共用同一个输入源,分别抓起始与结束。
捕获开始前,软件先执行 __HAL_TIM_SET_COUNTER(&htim4, 0)。这样每次测距都从 0 开始数,读数更直观。对 2~400 cm 的有效量程来说,最长往返时间约 23.3 ms,远小于 16 位定时器 65.536 ms 的回绕周期,因此不会在一轮测量中数满。
代码里仍然建议用无符号 16 位减法:
c
uint16_t pulse_us = (uint16_t)(fall_count - rise_count);
即使中途发生一次回绕,无符号减法仍能得到正确的时间差。这比手写一堆 if 更可靠。
知识小结
结论: PB8 通过 TIM4_CH3 直接捕获上升沿、TIM4_CH4 间接捕获下降沿,用一个引脚完成两次时间记录。
原因: CH4 的间接模式可以复用 TI3 输入,不需要把 Echo 再接到另一个 GPIO。
关键参数: CH3 选上升沿、直接模式;CH4 选下降沿、间接模式;两路都使用 PB8。
常见误区: 以为 CH4 一定会去读 PB9。间接捕获时 CH4 使用的是 TI3,外部仍只需接 PB8。
2. 参数与硬件:先确认线和电,再谈代码
2.1 引脚分配
| 功能 | STM32 引脚 | 配置 | 电平说明 |
|---|---|---|---|
| 超声波触发 | PA1 | 推挽输出,初始低 | 高电平持续 10 μs |
| 超声波回响 | PB8 | TIM4_CH3,浮空输入 | Echo 输出高、低电平 |
| 测距使能键 | PG10 | 输入,不用内部上下拉 | 按下接地,低电平有效 |
| 串口使能键 | PG6 | 输入,不用内部上下拉 | 按下接地,低电平有效 |
| 告警使能键 | PG8 | 输入,不用内部上下拉 | 按下接地,低电平有效 |
| 测距状态灯 | PF8 | 推挽输出 | 低电平点亮 |
| 打印状态灯 | PF9 | 推挽输出 | 低电平点亮 |
| 告警状态灯 | PF10 | 推挽输出 | 低电平点亮 |
| 高亮告警灯 | PB1 | 推挽输出 | 高电平点亮 |
| 串口日志 | PA9 / PA10 | USART1 TX / RX | 115200,8N1 |
参考板上,按键带有 10 kΩ 外部上拉和 100 nF 小电容,所以 GPIO 可以不启用内部上下拉。高亮 LED 由 NPN 三极管驱动:PB1 经 1 kΩ 基极电阻控制三极管,LED 支路串有 470 Ω 限流电阻,因此它与三个状态灯的有效电平相反。
这里最容易被忽略的不是代码,而是供电:
- 优先选择明确支持 3.3 V 供电的 HC-SR04 版本;
- 模块用 5 V 供电时,Echo 高电平通常也接近 5 V。不要拿"STM32 有些引脚能耐受 5 V"去赌所有引脚和所有开发板;
- 若只能使用 5 V 模块,应使用电平转换器,或用合适阻值的分压网络把 Echo 降到 3.3 V;
- STM32 与模块必须共地,否则高电平没有一个共同参考。
2.2 主要参数
| 参数 | 取值 | 对项目的影响 |
|---|---|---|
| HC-SR04 有效量程 | 2~400 cm | 小于 2 cm 或大于 400 cm 视为无效 |
| Trigger 高电平 | 至少 10 μs | 太短可能不触发 |
| 模块内部发射 | 8 个 40 kHz 脉冲 | Echo 宽度表示往返时间 |
| 建议测量周期 | 60 ms 左右 | 给上一次回波留出衰减时间 |
| 声速 | 约 343 m/s | 换算系数约 0.01715 cm/μs |
| 定时器分辨率 | 1 μs | 距离分辨率约 0.017 cm,传感器自身误差更大 |
对于 400 cm 的目标,往返时间约为:
text
t = 2 × 4.00 m ÷ 343 m/s
= 0.02332 s
≈ 23324 μs
所以用 1 μs 计数完全够用。真正限制量程的是模块,不是 16 位定时器。
3. 配置:让定时器、引脚和中断各司其职
3.1 时钟和 GPIO
在 CubeMX 里选择外部 8 MHz 晶振,把系统时钟配置到 72 MHz,调试接口选 Serial Wire。APB1 定时器时钟应仍是 72 MHz,这一点直接决定后面 PSC 的取值。
GPIO 的关键状态如下:
- PA1:Output Push Pull,初始 Low;
- PB1:Output Push Pull,初始 Low;
- PF8、PF9、PF10:Output Push Pull,初始 High,让三个状态灯默认熄灭;
- PG10、PG6、PG8:Input,No pull,按下时由外部上拉电阻提供高电平,按键接地后变低。
3.2 TIM4:输入捕获主角
| 配置项 | 值 |
|---|---|
| Channel 3 | Input Capture Direct,Rising Edge |
| Channel 4 | Input Capture Indirect,Falling Edge |
| Prescaler | 71 |
| Counter Period / ARR | 65535 |
| Input Filter | 0 |
| Capture Prescaler | No division |
| NVIC | TIM4 global interrupt 使能 |
| 捕获引脚 | PB8 |
PSC = 71 时:
text
计数频率 = 72 MHz ÷ (71 + 1) = 1 MHz
每个计数 = 1 μs
ARR = 65535 时,计数器从 0 数到 65535,共 65536 个计数:
text
最大计时 = 65536 × 1 μs = 65.536 ms
理论最大距离 = 0.065536 × 343 ÷ 2 ≈ 11.24 m
模块只能测到 4 m,因此定时器余量充足。
#mermaid-svg-w0RXsbhkQpGhyZRi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-w0RXsbhkQpGhyZRi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-w0RXsbhkQpGhyZRi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-w0RXsbhkQpGhyZRi .error-icon{fill:#552222;}#mermaid-svg-w0RXsbhkQpGhyZRi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-w0RXsbhkQpGhyZRi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-w0RXsbhkQpGhyZRi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-w0RXsbhkQpGhyZRi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-w0RXsbhkQpGhyZRi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-w0RXsbhkQpGhyZRi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-w0RXsbhkQpGhyZRi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-w0RXsbhkQpGhyZRi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-w0RXsbhkQpGhyZRi .marker.cross{stroke:#333333;}#mermaid-svg-w0RXsbhkQpGhyZRi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-w0RXsbhkQpGhyZRi p{margin:0;}#mermaid-svg-w0RXsbhkQpGhyZRi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-w0RXsbhkQpGhyZRi .cluster-label text{fill:#333;}#mermaid-svg-w0RXsbhkQpGhyZRi .cluster-label span{color:#333;}#mermaid-svg-w0RXsbhkQpGhyZRi .cluster-label span p{background-color:transparent;}#mermaid-svg-w0RXsbhkQpGhyZRi .label text,#mermaid-svg-w0RXsbhkQpGhyZRi span{fill:#333;color:#333;}#mermaid-svg-w0RXsbhkQpGhyZRi .node rect,#mermaid-svg-w0RXsbhkQpGhyZRi .node circle,#mermaid-svg-w0RXsbhkQpGhyZRi .node ellipse,#mermaid-svg-w0RXsbhkQpGhyZRi .node polygon,#mermaid-svg-w0RXsbhkQpGhyZRi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-w0RXsbhkQpGhyZRi .rough-node .label text,#mermaid-svg-w0RXsbhkQpGhyZRi .node .label text,#mermaid-svg-w0RXsbhkQpGhyZRi .image-shape .label,#mermaid-svg-w0RXsbhkQpGhyZRi .icon-shape .label{text-anchor:middle;}#mermaid-svg-w0RXsbhkQpGhyZRi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-w0RXsbhkQpGhyZRi .rough-node .label,#mermaid-svg-w0RXsbhkQpGhyZRi .node .label,#mermaid-svg-w0RXsbhkQpGhyZRi .image-shape .label,#mermaid-svg-w0RXsbhkQpGhyZRi .icon-shape .label{text-align:center;}#mermaid-svg-w0RXsbhkQpGhyZRi .node.clickable{cursor:pointer;}#mermaid-svg-w0RXsbhkQpGhyZRi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-w0RXsbhkQpGhyZRi .arrowheadPath{fill:#333333;}#mermaid-svg-w0RXsbhkQpGhyZRi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-w0RXsbhkQpGhyZRi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-w0RXsbhkQpGhyZRi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-w0RXsbhkQpGhyZRi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-w0RXsbhkQpGhyZRi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-w0RXsbhkQpGhyZRi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-w0RXsbhkQpGhyZRi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-w0RXsbhkQpGhyZRi .cluster text{fill:#333;}#mermaid-svg-w0RXsbhkQpGhyZRi .cluster span{color:#333;}#mermaid-svg-w0RXsbhkQpGhyZRi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-w0RXsbhkQpGhyZRi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-w0RXsbhkQpGhyZRi rect.text{fill:none;stroke-width:0;}#mermaid-svg-w0RXsbhkQpGhyZRi .icon-shape,#mermaid-svg-w0RXsbhkQpGhyZRi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-w0RXsbhkQpGhyZRi .icon-shape p,#mermaid-svg-w0RXsbhkQpGhyZRi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-w0RXsbhkQpGhyZRi .icon-shape .label rect,#mermaid-svg-w0RXsbhkQpGhyZRi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-w0RXsbhkQpGhyZRi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-w0RXsbhkQpGhyZRi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-w0RXsbhkQpGhyZRi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 72 MHz 定时器时钟
PSC = 71
分频到 1 MHz
CNT 每 1 μs 加 1
ARR = 65535
65.536 ms 后回绕
2~400 cm 只需约 23.3 ms
一轮测量不会数满
图 5:时基配置的因果关系。先得到 1 μs 计数,再确认量程小于回绕周期。
知识小结
结论: TIM4 用 PSC=71 得到 1 μs 计数,用 ARR=65535 得到 65.536 ms 的计数周期,足够覆盖 HC-SR04 的 4 m 量程。
原因: 72 MHz 经过 72 分频变成 1 MHz,计一个数正好 1 μs;16 位计数器最多容纳 65536 个数。
关键参数: PSC、ARR、定时器输入时钟、传感器最大量程。400 cm 往返约 23324 μs。
常见误区: 只看公式说"最大 11 m"就以为传感器也能测 11 m;实际有效量程由 HC-SR04 限制为 4 m。
3.3 TIM6 和 TIM7:一个管按键,一个管触发
基本定时器没有对外引脚,适合做周期性的后台节拍。
| 定时器 | PSC | ARR | 溢出周期 | 用途 |
|---|---|---|---|---|
| TIM6 | 71 | 999 | 1 ms | 每 1 ms 扫描一次按键 |
| TIM7 | 71 | 9 | 10 μs | 结束 PA1 的触发高脉冲 |
TIM6 的设置很直观:
text
72 MHz ÷ 72 = 1 MHz
1 MHz ÷ 1000 = 1 kHz
1 kHz 的周期就是 1 ms
按键消抖需要连续 20 次扫描都为按下,所以实际确认时间是 20 ms。这个 20 不是随便写的延时,而是把"1 ms 扫描节拍"当成了计数单位。
TIM7 只负责一件小事:
- 主循环先把 PA1 拉高;
- 启动 TIM7;
- 10 μs 后 TIM7 溢出中断,把 PA1 拉低并停止自己。
因为中断响应还会带来一点延迟,实际高电平会略大于 10 μs。HC-SR04 要求的是"至少 10 μs",所以这是可以接受的。若产品要求脉冲宽度分毫不差,可以改用定时器单脉冲输出,让硬件直接产生波形。
知识小结
结论: TIM7 负责产生触发脉冲的结束时刻,TIM6 负责 1 ms 按键节拍,二者把周期性工作从主循环中分离出来。
原因: 基本定时器没有 GPIO 输出,但能稳定产生固定周期中断;10 μs 和 1 ms 都不适合靠软件空循环保证。
关键参数: TIM7 为 10 μs,TIM6 为 1 ms;按键连续 20 次一致后确认,消抖时间就是 20 ms。
常见误区: 用
HAL_Delay(10)产生触发脉宽。普通延时受中断和编译器优化影响,时间不精确,还会阻塞其他任务。
3.4 串口与 PB8 重映射
USART1 配成 115200-8-N-1,打开 TX/RX。若沿用某些参考配置里的"8 数据位 + 奇校验",CubeMX 可能显示为 9 bits including parity,此时串口助手也必须选择奇校验,否则会看到乱码。本文后面的代码统一使用更常见的 8N1。
还有一个必须检查的坑:CubeMX 在选择 TIM4_CH3 时,有时会默认落到 PD14,而不是目标板实际连接的 PB8。生成代码前一定要在 Pinout 视图确认:
text
PB8 -> TIM4_CH3
如果工具确实自动选了 PD14,就手动改到 PB8,并确认 PB8 没有被其他 GPIO 功能占用。生成代码后,也可以在 MX_TIM4_Init() 和 HAL_TIM_MspPostInit() 里检查是否出现了 PB8 的复用初始化。
3.5 HAL 关键函数:只展开这条主线会用到的部分
HAL_TIM_IC_Start_IT 的签名是:
c
HAL_StatusTypeDef HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim,
uint32_t Channel);
参数是指向定时器句柄的指针和通道号,返回值可能是 HAL_OK、HAL_ERROR、HAL_BUSY 或 HAL_TIMEOUT。它主要做三件事:使能对应捕获通道、打开通道中断、启动计数器。简化后的逻辑可以理解为:
c
if (Channel == TIM_CHANNEL_3) {
__HAL_TIM_ENABLE_IT(htim, TIM_IT_CC3); /* 允许 CCR3 捕获中断 */
} else if (Channel == TIM_CHANNEL_4) {
__HAL_TIM_ENABLE_IT(htim, TIM_IT_CC4); /* 允许 CCR4 捕获中断 */
}
TIM_CCxChannelCmd(htim->Instance, Channel, TIM_CCx_ENABLE);
/* 置位 CCER.CCxE,使能捕获通道 */
__HAL_TIM_ENABLE(htim); /* 置位 CR1.CEN,让 CNT 开始计数 */
HAL_TIM_ReadCapturedValue 的签名是:
c
uint32_t HAL_TIM_ReadCapturedValue(const TIM_HandleTypeDef *htim,
uint32_t Channel);
它根据通道返回 CCR3 或 CCR4 的值。简化后就是读比较/捕获寄存器:
c
return __HAL_TIM_GET_COMPARE(htim, Channel);
HAL_TIM_Base_Start_IT 用于 TIM6 和 TIM7:
c
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
它打开更新中断并启动计数器。简化逻辑是:
c
__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE); /* 允许溢出中断 */
__HAL_TIM_ENABLE(htim); /* 启动 CNT */
HAL_GPIO_WritePin 的签名是:
c
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin,
GPIO_PinState PinState);
它没有返回值。置高和置低最终会写到 BSRR 或 BRR,比"先读 IDR、再改 ODR"的写法更安全,因为不会误改同组的其他引脚。
初始化顺序可以记成下面这条链:
#mermaid-svg-I0eT99Cxbm55ZTbe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I0eT99Cxbm55ZTbe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I0eT99Cxbm55ZTbe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I0eT99Cxbm55ZTbe .error-icon{fill:#552222;}#mermaid-svg-I0eT99Cxbm55ZTbe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I0eT99Cxbm55ZTbe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I0eT99Cxbm55ZTbe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I0eT99Cxbm55ZTbe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I0eT99Cxbm55ZTbe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I0eT99Cxbm55ZTbe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I0eT99Cxbm55ZTbe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I0eT99Cxbm55ZTbe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I0eT99Cxbm55ZTbe .marker.cross{stroke:#333333;}#mermaid-svg-I0eT99Cxbm55ZTbe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I0eT99Cxbm55ZTbe p{margin:0;}#mermaid-svg-I0eT99Cxbm55ZTbe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-I0eT99Cxbm55ZTbe .cluster-label text{fill:#333;}#mermaid-svg-I0eT99Cxbm55ZTbe .cluster-label span{color:#333;}#mermaid-svg-I0eT99Cxbm55ZTbe .cluster-label span p{background-color:transparent;}#mermaid-svg-I0eT99Cxbm55ZTbe .label text,#mermaid-svg-I0eT99Cxbm55ZTbe span{fill:#333;color:#333;}#mermaid-svg-I0eT99Cxbm55ZTbe .node rect,#mermaid-svg-I0eT99Cxbm55ZTbe .node circle,#mermaid-svg-I0eT99Cxbm55ZTbe .node ellipse,#mermaid-svg-I0eT99Cxbm55ZTbe .node polygon,#mermaid-svg-I0eT99Cxbm55ZTbe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-I0eT99Cxbm55ZTbe .rough-node .label text,#mermaid-svg-I0eT99Cxbm55ZTbe .node .label text,#mermaid-svg-I0eT99Cxbm55ZTbe .image-shape .label,#mermaid-svg-I0eT99Cxbm55ZTbe .icon-shape .label{text-anchor:middle;}#mermaid-svg-I0eT99Cxbm55ZTbe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-I0eT99Cxbm55ZTbe .rough-node .label,#mermaid-svg-I0eT99Cxbm55ZTbe .node .label,#mermaid-svg-I0eT99Cxbm55ZTbe .image-shape .label,#mermaid-svg-I0eT99Cxbm55ZTbe .icon-shape .label{text-align:center;}#mermaid-svg-I0eT99Cxbm55ZTbe .node.clickable{cursor:pointer;}#mermaid-svg-I0eT99Cxbm55ZTbe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-I0eT99Cxbm55ZTbe .arrowheadPath{fill:#333333;}#mermaid-svg-I0eT99Cxbm55ZTbe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-I0eT99Cxbm55ZTbe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-I0eT99Cxbm55ZTbe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I0eT99Cxbm55ZTbe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-I0eT99Cxbm55ZTbe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I0eT99Cxbm55ZTbe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-I0eT99Cxbm55ZTbe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-I0eT99Cxbm55ZTbe .cluster text{fill:#333;}#mermaid-svg-I0eT99Cxbm55ZTbe .cluster span{color:#333;}#mermaid-svg-I0eT99Cxbm55ZTbe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-I0eT99Cxbm55ZTbe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-I0eT99Cxbm55ZTbe rect.text{fill:none;stroke-width:0;}#mermaid-svg-I0eT99Cxbm55ZTbe .icon-shape,#mermaid-svg-I0eT99Cxbm55ZTbe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I0eT99Cxbm55ZTbe .icon-shape p,#mermaid-svg-I0eT99Cxbm55ZTbe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-I0eT99Cxbm55ZTbe .icon-shape .label rect,#mermaid-svg-I0eT99Cxbm55ZTbe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I0eT99Cxbm55ZTbe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-I0eT99Cxbm55ZTbe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-I0eT99Cxbm55ZTbe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HSE 8 MHz
PLL 到 72 MHz
配置 GPIO 与复用功能
TIM4:CH3 上升沿
CH4 下降沿
TIM6:1 ms
TIM7:10 μs
USART1:115200
打开 NVIC
App_Init:启动 TIM6
和两路输入捕获
主循环按状态机触发测量
图 6:从时钟到应用初始化的顺序。底层的 MX_*_Init() 由 CubeMX 生成,应用层只需启动外设并调度。
3.6 回调与状态机:别让主循环"干等"
HAL 在两个时刻通知应用层:
- TIM6 或 TIM7 溢出:进入
HAL_TIM_PeriodElapsedCallback(); - TIM4 的 CH3 或 CH4 捕获到边沿:进入
HAL_TIM_IC_CaptureCallback()。
一个回调会被多个定时器共用,所以不能看到回调就认为"一定是 TIM6"。必须先比较句柄:
c
if (htim == my_key_timer) {
/* TIM6:扫描按键 */
} else if (htim == my_trigger_timer) {
/* TIM7:拉低 PA1 */
}
捕获回调还要比较 htim->Channel,判断这次是上升沿还是下降沿。
测距过程存在多个等待状态,用状态机表达最清楚:
#mermaid-svg-pe87HcrGa8sTIUlh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pe87HcrGa8sTIUlh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pe87HcrGa8sTIUlh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pe87HcrGa8sTIUlh .error-icon{fill:#552222;}#mermaid-svg-pe87HcrGa8sTIUlh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pe87HcrGa8sTIUlh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pe87HcrGa8sTIUlh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pe87HcrGa8sTIUlh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pe87HcrGa8sTIUlh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pe87HcrGa8sTIUlh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pe87HcrGa8sTIUlh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pe87HcrGa8sTIUlh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pe87HcrGa8sTIUlh .marker.cross{stroke:#333333;}#mermaid-svg-pe87HcrGa8sTIUlh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pe87HcrGa8sTIUlh p{margin:0;}#mermaid-svg-pe87HcrGa8sTIUlh defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-pe87HcrGa8sTIUlh g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-pe87HcrGa8sTIUlh g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-pe87HcrGa8sTIUlh g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-pe87HcrGa8sTIUlh g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-pe87HcrGa8sTIUlh g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-pe87HcrGa8sTIUlh .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-pe87HcrGa8sTIUlh .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-pe87HcrGa8sTIUlh .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-pe87HcrGa8sTIUlh .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-pe87HcrGa8sTIUlh .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-pe87HcrGa8sTIUlh .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-pe87HcrGa8sTIUlh .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-pe87HcrGa8sTIUlh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pe87HcrGa8sTIUlh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pe87HcrGa8sTIUlh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pe87HcrGa8sTIUlh .edgeLabel .label text{fill:#333;}#mermaid-svg-pe87HcrGa8sTIUlh .label div .edgeLabel{color:#333;}#mermaid-svg-pe87HcrGa8sTIUlh .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-pe87HcrGa8sTIUlh .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-pe87HcrGa8sTIUlh .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-pe87HcrGa8sTIUlh .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-pe87HcrGa8sTIUlh .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-pe87HcrGa8sTIUlh .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pe87HcrGa8sTIUlh .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pe87HcrGa8sTIUlh #statediagram-barbEnd{fill:#333333;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pe87HcrGa8sTIUlh .cluster-label,#mermaid-svg-pe87HcrGa8sTIUlh .nodeLabel{color:#131300;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-pe87HcrGa8sTIUlh .note-edge{stroke-dasharray:5;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-note text{fill:black;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram-note .nodeLabel{color:black;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagram .edgeLabel{color:red;}#mermaid-svg-pe87HcrGa8sTIUlh #dependencyStart,#mermaid-svg-pe87HcrGa8sTIUlh #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-pe87HcrGa8sTIUlh .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pe87HcrGa8sTIUlh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每 60 ms 触发一次,CNT 清零
CH3 捕获上升沿
CH4 捕获下降沿并置完成标志
50 ms 内没有上升沿
50 ms 内没有下降沿
IDLE
WAIT_RISE
WAIT_FALL
图 7:没有回波时不能永远停住,所以必须有超时回到 IDLE。
中断只做很短的事:
- 记录时间戳;
- 改变状态;
- 置位"测量完成"标志。
串口打印、LED 判断和距离换算放到主循环。这样中断不会因为 printf 或长延时把系统卡住。
知识小结
结论: 应用层用"等待上升沿 → 等待下降沿 → 完成或超时"的状态机组织测距,HAL 回调只负责记录事件。
原因: 传感器回波是异步事件,主循环无法预知它何时到来;状态机既能避免死等,也能识别没有回波的情况。
关键参数: 测量周期 60 ms,回波超时 50 ms,捕获状态和完成标志需要在中断与主循环之间共享。
常见误区: 在捕获回调里调用
printf、HAL_Delay或做复杂浮点运算;这些操作会拉长中断时间,影响下一次边沿捕获。
4. 代码:先跑通一次,再补齐控制逻辑
4.1 最小可运行核心代码
下面的代码假设 CubeMX 已经生成 htim4、htim7、huart1,并把 TIM4 配成 CH3 上升沿、CH4 间接下降沿。它只做"每 60 ms 测一次并打印",先不考虑按键和 LED。
把代码放进 main.c 的用户代码区,并在进入 while (1) 前启动两路捕获:
c
/* 放在 main.c 的 USER CODE BEGIN 0 区域 */
#include <stdio.h>
static volatile uint16_t s_rise_count;
static volatile uint16_t s_fall_count;
static volatile uint8_t s_rise_ok;
static volatile uint8_t s_ready;
static volatile uint8_t s_waiting;
static volatile uint32_t s_trigger_tick;
static uint32_t s_last_trigger_tick;
/* TIM7 溢出:高电平已经持续约 10 μs,现在拉低 PA1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM7) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
HAL_TIM_Base_Stop_IT(&htim7); /* 每次只产生一个触发脉冲 */
}
}
/* TIM4 捕获:CH3 记起点,CH4 记终点 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim != &htim4) {
return;
}
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_3) {
/* 先保存上升沿,否则后面无法计算时间差 */
s_rise_count = (uint16_t)HAL_TIM_ReadCapturedValue(
&htim4, TIM_CHANNEL_3);
s_rise_ok = 1;
} else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_4 && s_rise_ok) {
s_fall_count = (uint16_t)HAL_TIM_ReadCapturedValue(
&htim4, TIM_CHANNEL_4);
s_ready = 1;
s_waiting = 0;
s_rise_ok = 0;
}
}
/* 发一次 10 μs 触发脉冲;TIM7 的 ARR=9,溢出周期为 10 μs */
static void Trigger_OneShot(void)
{
s_ready = 0;
s_rise_ok = 0;
s_waiting = 1;
s_trigger_tick = HAL_GetTick();
__HAL_TIM_SET_COUNTER(&htim4, 0); /* 每轮从 0 开始,读数更直观 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
__HAL_TIM_SET_COUNTER(&htim7, 0); /* 防止上次残留计数导致脉宽变短 */
__HAL_TIM_CLEAR_FLAG(&htim7, TIM_FLAG_UPDATE);
HAL_TIM_Base_Start_IT(&htim7);
}
/* 一行串口输出,避免依赖 printf 重定向 */
static void Print_CentiCm(uint32_t centi_cm)
{
char msg[32];
int len = snprintf(msg, sizeof(msg), "distance: %lu.%02lu cm\r\n",
(unsigned long)(centi_cm / 100U),
(unsigned long)(centi_cm % 100U));
if (len > 0) {
HAL_UART_Transmit(&huart1, (uint8_t *)msg, (uint16_t)len, 100);
}
}
在 main() 的外设初始化之后加上:
c
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_3);
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_4);
主循环只做触发、超时和显示:
c
while (1)
{
uint32_t now = HAL_GetTick();
/* 400 cm 的往返时间约 23.3 ms,50 ms 仍无回波就认为失败 */
if (s_waiting && (now - s_trigger_tick) >= 50U) {
s_waiting = 0;
s_rise_ok = 0;
s_ready = 0;
}
if (s_ready) {
uint16_t pulse_us;
uint32_t centi_cm;
uint32_t distance_mm;
s_ready = 0;
pulse_us = (uint16_t)(s_fall_count - s_rise_count);
/* 距离(cm) = pulse_us × 0.01715;用整数计算保留两位小数 */
centi_cm = ((uint32_t)pulse_us * 343U + 100U) / 200U;
distance_mm = centi_cm / 10U;
/* 小于 2 cm 或大于 400 cm 不是可信距离,直接丢弃 */
if (distance_mm >= 20U && distance_mm <= 4000U) {
Print_CentiCm(centi_cm);
} else {
const char *bad = "distance: --\r\n";
HAL_UART_Transmit(&huart1, (uint8_t *)bad,
(uint16_t)(sizeof("distance: --\r\n") - 1U),
100);
}
}
/* 先处理本轮结果,再启动下一轮,避免新触发把完成标志清掉 */
if (now - s_last_trigger_tick >= 60U) {
s_last_trigger_tick = now;
Trigger_OneShot();
}
}
这里用整数运算而不是浮点,是为了让没有打开浮点格式化支持的工程也能正常打印。343 / 200 正好等于"乘声速、除 2、再放大 100 倍"的系数:
text
centi_cm = t_us × 0.01715 × 100
= t_us × 343 ÷ 200
把一次最小测距跑通后,再加入按键、状态灯和告警,排查会轻松得多。
4.2 完整应用层代码
下面的文件把按键、状态灯、超时、测距调度和日志都封装起来。CubeMX 生成的时钟、GPIO、TIM 底层初始化仍然保留,不需要手抄;只需要把应用层加入工程,并在 main.c 中调用。
app_ultrasonic.h
c
#ifndef APP_ULTRASONIC_H
#define APP_ULTRASONIC_H
#include "stm32f1xx_hal.h"
#include <stdbool.h>
/* 统一接收定时器和串口句柄,避免应用层直接依赖全局变量 */
bool App_Ultrasonic_Init(TIM_HandleTypeDef *trigger_timer,
TIM_HandleTypeDef *key_timer,
TIM_HandleTypeDef *capture_timer,
UART_HandleTypeDef *log_uart);
/* 非阻塞调度:由主循环持续调用 */
void App_Ultrasonic_Loop(void);
#endif
app_ultrasonic.c
c
#include "app_ultrasonic.h"
#include <stdio.h>
#define TRIG_PORT GPIOA
#define TRIG_PIN GPIO_PIN_1
#define KEY_COUNT 3U
#define KEY_DEBOUNCE_MS 20U
#define MEASURE_PERIOD_MS 60U
#define ECHO_TIMEOUT_MS 50U
#define MIN_VALID_CENTI_CM 200U
#define MAX_VALID_CENTI_CM 40000U
#define WARN_DISTANCE_CENTI_CM 1000U
typedef enum {
MEAS_IDLE = 0,
MEAS_WAIT_RISE,
MEAS_WAIT_FALL
} measure_state_t;
static TIM_HandleTypeDef *s_trigger_timer;
static TIM_HandleTypeDef *s_key_timer;
static TIM_HandleTypeDef *s_capture_timer;
static UART_HandleTypeDef *s_log_uart;
/* 参考板:三个按键都接在 GPIOG,按下为低电平 */
static GPIO_TypeDef *const s_key_ports[KEY_COUNT] = {
GPIOG, GPIOG, GPIOG
};
static const uint16_t s_key_pins[KEY_COUNT] = {
GPIO_PIN_10, GPIO_PIN_6, GPIO_PIN_8
};
static uint8_t s_key_pressed[KEY_COUNT];
static uint8_t s_key_filter[KEY_COUNT];
/* 中断和主循环共享的状态必须加 volatile */
static volatile bool s_range_enabled;
static volatile bool s_print_enabled;
static volatile bool s_warning_enabled;
static volatile measure_state_t s_state;
static volatile uint16_t s_rise_count;
static volatile uint16_t s_fall_count;
static volatile uint32_t s_pulse_us;
static volatile bool s_measure_ready;
static volatile bool s_measure_timeout;
static volatile uint32_t s_start_tick;
static uint32_t s_last_trigger_tick;
/* 三个状态灯在参考板上是低电平点亮 */
static void Set_Status_LED(GPIO_TypeDef *port, uint16_t pin, bool on)
{
HAL_GPIO_WritePin(port, pin, on ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
static void Refresh_Status_LEDs(void)
{
Set_Status_LED(GPIOF, GPIO_PIN_8, s_range_enabled);
Set_Status_LED(GPIOF, GPIO_PIN_9, s_print_enabled);
Set_Status_LED(GPIOF, GPIO_PIN_10, s_warning_enabled);
}
/* 高亮 LED 经过 NPN 三极管,PB1 高电平点亮 */
static void Set_Warning_LED(bool on)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1,
on ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
static void Handle_Key_Press(uint8_t key)
{
switch (key) {
case 0:
s_range_enabled = !s_range_enabled;
if (!s_range_enabled) {
s_state = MEAS_IDLE;
s_measure_ready = false;
}
break;
case 1:
s_print_enabled = !s_print_enabled;
break;
case 2:
s_warning_enabled = !s_warning_enabled;
if (!s_warning_enabled) {
Set_Warning_LED(false);
}
break;
default:
break;
}
Refresh_Status_LEDs();
}
/* 由 TIM6 每 1 ms 调用一次;连续 20 ms 状态一致才确认 */
static void Scan_Keys_1ms(void)
{
for (uint32_t i = 0; i < KEY_COUNT; i++) {
bool raw_pressed =
(HAL_GPIO_ReadPin(s_key_ports[i], s_key_pins[i])
== GPIO_PIN_RESET);
if ((raw_pressed ? 1U : 0U) != s_key_pressed[i]) {
s_key_filter[i]++;
if (s_key_filter[i] >= KEY_DEBOUNCE_MS) {
s_key_filter[i] = 0;
s_key_pressed[i] = raw_pressed ? 1U : 0U;
/* 只在"松开变为按下"时执行一次,避免连按 */
if (raw_pressed) {
Handle_Key_Press((uint8_t)i);
}
}
} else {
s_key_filter[i] = 0;
}
}
}
static void Start_Measurement(uint32_t now)
{
s_measure_ready = false;
s_measure_timeout = false;
s_state = MEAS_WAIT_RISE;
s_start_tick = now;
/* 每次触发前清零,保证 CCR 中的值就是本轮相对时间 */
__HAL_TIM_SET_COUNTER(s_capture_timer, 0U);
HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET);
__HAL_TIM_SET_COUNTER(s_trigger_timer, 0U);
__HAL_TIM_CLEAR_FLAG(s_trigger_timer, TIM_FLAG_UPDATE);
HAL_TIM_Base_Start_IT(s_trigger_timer);
}
static void Send_Text(const char *text)
{
uint16_t len = 0;
while (text[len] != '\0') {
len++;
}
if (len > 0) {
HAL_UART_Transmit(s_log_uart, (uint8_t *)text, len, 100);
}
}
static void Publish_Result(void)
{
uint32_t centi_cm = 0;
bool valid = false;
if (!s_measure_timeout) {
/* centi_cm = pulse_us × 0.01715 × 100 = pulse_us × 343 / 200 */
centi_cm = ((uint32_t)s_pulse_us * 343U + 100U) / 200U;
valid = (centi_cm >= MIN_VALID_CENTI_CM &&
centi_cm <= MAX_VALID_CENTI_CM);
}
if (!valid) {
centi_cm = 0;
}
/* 只有"告警开启、数据有效且距离不超过 10 cm"才点亮高亮灯 */
Set_Warning_LED(s_warning_enabled && valid &&
centi_cm <= WARN_DISTANCE_CENTI_CM);
if (!s_print_enabled) {
return;
}
if (s_measure_timeout) {
Send_Text("distance: -- (no echo)\r\n");
} else if (!valid) {
Send_Text("distance: -- (out of 2-400 cm)\r\n");
} else {
char msg[48];
int len = snprintf(msg, sizeof(msg),
"distance: %lu.%02lu cm\r\n",
(unsigned long)(centi_cm / 100U),
(unsigned long)(centi_cm % 100U));
if (len > 0) {
HAL_UART_Transmit(s_log_uart, (uint8_t *)msg,
(uint16_t)len, 100);
}
}
}
bool App_Ultrasonic_Init(TIM_HandleTypeDef *trigger_timer,
TIM_HandleTypeDef *key_timer,
TIM_HandleTypeDef *capture_timer,
UART_HandleTypeDef *log_uart)
{
if (trigger_timer == NULL || key_timer == NULL ||
capture_timer == NULL || log_uart == NULL) {
return false;
}
s_trigger_timer = trigger_timer;
s_key_timer = key_timer;
s_capture_timer = capture_timer;
s_log_uart = log_uart;
s_range_enabled = false;
s_print_enabled = false;
s_warning_enabled = false;
s_state = MEAS_IDLE;
s_measure_ready = false;
s_measure_timeout = false;
s_last_trigger_tick = 0;
HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET);
Set_Warning_LED(false);
Refresh_Status_LEDs(); /* 初始状态灯全部熄灭 */
if (HAL_TIM_Base_Start_IT(s_key_timer) != HAL_OK) {
return false;
}
if (HAL_TIM_IC_Start_IT(s_capture_timer, TIM_CHANNEL_3) != HAL_OK) {
return false;
}
if (HAL_TIM_IC_Start_IT(s_capture_timer, TIM_CHANNEL_4) != HAL_OK) {
return false;
}
return true;
}
void App_Ultrasonic_Loop(void)
{
uint32_t now = HAL_GetTick();
/* 关闭测距时立即停下状态机,也关掉高亮灯 */
if (!s_range_enabled) {
s_state = MEAS_IDLE;
s_measure_ready = false;
s_measure_timeout = false;
Set_Warning_LED(false);
return;
}
/* 没有回波时必须退出等待,否则下一次测距永远触发不了 */
if ((s_state == MEAS_WAIT_RISE || s_state == MEAS_WAIT_FALL) &&
(now - s_start_tick) >= ECHO_TIMEOUT_MS) {
s_state = MEAS_IDLE;
s_measure_timeout = true;
s_measure_ready = true;
}
/* 先发布本轮结果,再判断是否启动下一轮 */
if (s_measure_ready) {
s_measure_ready = false;
Publish_Result();
}
/* 每 60 ms 启动一轮,避免连续发射互相干扰 */
if (s_state == MEAS_IDLE &&
(now - s_last_trigger_tick) >= MEASURE_PERIOD_MS) {
s_last_trigger_tick = now;
Start_Measurement(now);
}
}
/* TIM6 扫描按键;TIM7 结束 10 μs 触发脉冲 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == s_key_timer) {
Scan_Keys_1ms();
} else if (htim == s_trigger_timer) {
HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET);
HAL_TIM_Base_Stop_IT(s_trigger_timer);
}
}
/* TIM4 的双通道捕获回调 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
/* 关闭测距后不再接收旧回波,避免下次开启时显示过期结果 */
if (htim != s_capture_timer || !s_range_enabled) {
return;
}
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_3 &&
s_state == MEAS_WAIT_RISE) {
s_rise_count = (uint16_t)HAL_TIM_ReadCapturedValue(
s_capture_timer, TIM_CHANNEL_3);
s_state = MEAS_WAIT_FALL;
} else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_4 &&
s_state == MEAS_WAIT_FALL) {
s_fall_count = (uint16_t)HAL_TIM_ReadCapturedValue(
s_capture_timer, TIM_CHANNEL_4);
/* 无符号 16 位减法可容忍一次计数回绕 */
s_pulse_us = (uint16_t)(s_fall_count - s_rise_count);
s_state = MEAS_IDLE;
s_measure_timeout = false;
s_measure_ready = true;
}
}
main.c 中需要补入的调用
c
/* USER CODE BEGIN Includes */
#include "app_ultrasonic.h"
/* USER CODE END Includes */
/* 在 MX_TIM4_Init、MX_TIM6_Init、MX_TIM7_Init、
MX_USART1_UART_Init 等函数调用之后加入 */
if (!App_Ultrasonic_Init(&htim7, &htim6, &htim4, &huart1)) {
Error_Handler(); /* 任一外设启动失败就不要继续运行 */
}
/* USER CODE BEGIN WHILE */
while (1)
{
App_Ultrasonic_Loop();
}
/* USER CODE END WHILE */
如果工程其他模块已经定义了 HAL_TIM_PeriodElapsedCallback 或 HAL_TIM_IC_CaptureCallback,不要重复定义;把这里的判断分支合并进现有回调即可。C 链接器不会允许两个同名强符号同时存在。
5. 验证:先看波形,再看数字
5.1 上电后的默认状态
上电后三个状态灯应全部熄灭,高亮 LED 也熄灭。串口只有在你按下第二个按键后才会输出。这是有意设计的,便于确认"是否使能"真的由按键控制。
按键功能如下:
| 按键 | 引脚 | 作用 | 对应状态灯 |
|---|---|---|---|
| K0 | PG10 | 打开或关闭测距 | PF8 |
| K1 | PG6 | 打开或关闭串口输出 | PF9 |
| K2 | PG8 | 打开或关闭近距告警 | PF10 |
正常测量时,串口应周期性输出:
text
distance: 17.15 cm
distance: 17.16 cm
distance: 17.14 cm
有微小跳动是正常的,传感器本身、障碍物表面和空气流动都会影响回波。把平整纸板放在 10 cm 内、并打开告警后,高亮 LED 应点亮;移远后熄灭。
5.2 用示波器核对关键波形
建议按下面顺序看两个点:
- 探头接 PA1,确认每次测量都有一小段约 10 μs 的高脉冲;
- 探头接 PB8,确认 Echo 高电平宽度随距离增加而增加。
如果手边没有示波器,也可以用逻辑分析仪,或者先把串口输出跑通,再借助已知距离反推脉宽。
| 目标距离 | 理论上 Echo 高电平 | 代码应显示的厘米值 |
|---|---|---|
| 10 cm | 约 583 μs | 10.00 cm |
| 50 cm | 约 2915 μs | 50.00 cm |
| 100 cm | 约 5831 μs | 100.00 cm |
| 400 cm | 约 23324 μs | 400.00 cm |
实测值受目标形状、角度、温度和模块个体差异影响,表中数值用于判断量级,不用于校准仪器。
5.3 验证状态机的完成与超时
把障碍物拿远到超出量程,或暂时断开模块的 Echo 线,串口应出现"无回波"或"超出量程"的提示,而不是整个程序停住。障碍物重新出现后,下一轮测量应自动恢复。这说明超时路径确实回到了 MEAS_IDLE。
6. 排错:按信号链定位,不要一上来改代码
#mermaid-svg-GIwEMrlwqCvQkL4M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GIwEMrlwqCvQkL4M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GIwEMrlwqCvQkL4M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GIwEMrlwqCvQkL4M .error-icon{fill:#552222;}#mermaid-svg-GIwEMrlwqCvQkL4M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GIwEMrlwqCvQkL4M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GIwEMrlwqCvQkL4M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GIwEMrlwqCvQkL4M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GIwEMrlwqCvQkL4M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GIwEMrlwqCvQkL4M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GIwEMrlwqCvQkL4M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GIwEMrlwqCvQkL4M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GIwEMrlwqCvQkL4M .marker.cross{stroke:#333333;}#mermaid-svg-GIwEMrlwqCvQkL4M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GIwEMrlwqCvQkL4M p{margin:0;}#mermaid-svg-GIwEMrlwqCvQkL4M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GIwEMrlwqCvQkL4M .cluster-label text{fill:#333;}#mermaid-svg-GIwEMrlwqCvQkL4M .cluster-label span{color:#333;}#mermaid-svg-GIwEMrlwqCvQkL4M .cluster-label span p{background-color:transparent;}#mermaid-svg-GIwEMrlwqCvQkL4M .label text,#mermaid-svg-GIwEMrlwqCvQkL4M span{fill:#333;color:#333;}#mermaid-svg-GIwEMrlwqCvQkL4M .node rect,#mermaid-svg-GIwEMrlwqCvQkL4M .node circle,#mermaid-svg-GIwEMrlwqCvQkL4M .node ellipse,#mermaid-svg-GIwEMrlwqCvQkL4M .node polygon,#mermaid-svg-GIwEMrlwqCvQkL4M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GIwEMrlwqCvQkL4M .rough-node .label text,#mermaid-svg-GIwEMrlwqCvQkL4M .node .label text,#mermaid-svg-GIwEMrlwqCvQkL4M .image-shape .label,#mermaid-svg-GIwEMrlwqCvQkL4M .icon-shape .label{text-anchor:middle;}#mermaid-svg-GIwEMrlwqCvQkL4M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GIwEMrlwqCvQkL4M .rough-node .label,#mermaid-svg-GIwEMrlwqCvQkL4M .node .label,#mermaid-svg-GIwEMrlwqCvQkL4M .image-shape .label,#mermaid-svg-GIwEMrlwqCvQkL4M .icon-shape .label{text-align:center;}#mermaid-svg-GIwEMrlwqCvQkL4M .node.clickable{cursor:pointer;}#mermaid-svg-GIwEMrlwqCvQkL4M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GIwEMrlwqCvQkL4M .arrowheadPath{fill:#333333;}#mermaid-svg-GIwEMrlwqCvQkL4M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GIwEMrlwqCvQkL4M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GIwEMrlwqCvQkL4M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GIwEMrlwqCvQkL4M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GIwEMrlwqCvQkL4M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GIwEMrlwqCvQkL4M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GIwEMrlwqCvQkL4M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GIwEMrlwqCvQkL4M .cluster text{fill:#333;}#mermaid-svg-GIwEMrlwqCvQkL4M .cluster span{color:#333;}#mermaid-svg-GIwEMrlwqCvQkL4M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GIwEMrlwqCvQkL4M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GIwEMrlwqCvQkL4M rect.text{fill:none;stroke-width:0;}#mermaid-svg-GIwEMrlwqCvQkL4M .icon-shape,#mermaid-svg-GIwEMrlwqCvQkL4M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GIwEMrlwqCvQkL4M .icon-shape p,#mermaid-svg-GIwEMrlwqCvQkL4M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GIwEMrlwqCvQkL4M .icon-shape .label rect,#mermaid-svg-GIwEMrlwqCvQkL4M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GIwEMrlwqCvQkL4M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GIwEMrlwqCvQkL4M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GIwEMrlwqCvQkL4M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
否
是
否
是
否
串口没有距离
PA1 有 10 μs 高脉冲吗
查 TIM7、回调函数、触发标志
PB8 有 Echo 高电平吗
查模块供电、共地、Trigger 接线和模块好坏
TIM4 捕获回调进入吗
查 PB8 是否被重映射、NVIC 和两路启动
CNT 差值与距离量级相符吗
查 CNT 清零、上升沿标志和计数器回绕
换算和打印是否正确
查整数公式、有效范围和串口参数
图 8:从 MCU 到模块再回到 MCU 的排错顺序。
6.1 串口乱码或完全没有输出
- 先确认已经按下 K1,打印使能打开;
- 检查波特率是否为 115200;
- 如果工程配置了奇校验,串口助手也必须选同样的数据位和校验位;
- 检查 USART1 的 TX/RX 是否交叉连接,以及开发板的串口跳线;
- 确认系统时钟确实为 72 MHz,否则波特率会产生偏差。
6.2 距离一直是 0
- 用示波器先看 PA1,没有 10 μs 脉冲就查 TIM7 是否启动、
HAL_TIM_PeriodElapsedCallback是否被调用; - PA1 正常但 PB8 没有回波,重点查模块 VCC、GND、Trig 和 Echo 是否接反;
- PB8 有波形但回调不进入,检查 CubeMX 是否把 TIM4_CH3 落到了 PD14,以及
HAL_TIM_IC_Start_IT是否启动了两路; - 距离小于 2 cm 或大于 400 cm 会被代码主动判为无效,这是量程保护,不是坏了。
6.3 只有第一次能测,后面不再更新
常见原因有三个:
- 触发前没有清零 TIM4 的
CNT; - 上一轮的"测量完成"标志没有清除;
- 没有回波时一直停在等待状态,没有超时分支。
代码中的 Start_Measurement() 和 ECHO_TIMEOUT_MS 分别解决前两个问题和第三个问题。若你改造代码,这三个动作要一起保留。
6.4 数值周期性跳变
- 让测量周期保持在 60 ms 左右,不要连续高频触发;
- 给模块电源加去耦电容,缩短接线,远离电机和开关电源;
- 让障碍物尽量垂直于声波方向,表面不要过于柔软或倾斜;
- 需要更平滑的数值时,可以对连续 3 次有效测量取中值,但先确认单次测量和超时路径都正常。
6.5 LED 逻辑反了
参考板上的 PF8、PF9、PF10 状态灯是低电平点亮,PB1 高亮灯是高电平点亮。它们不是同一种有效电平,不能直接照搬同一个 on/off 判断。若你的板子电路不同,应以原理图为准修改 Set_Status_LED() 和 Set_Warning_LED()。
7. 总结
7.1 一页纸总结
这条测距链路只有五个动作:
- 触发:PA1 输出至少 10 μs 高脉冲;
- 等待:HC-SR04 拉高 Echo,PB8 等待上升沿;
- 捕获 :TIM4_CH3 记录
CNT_rise,TIM4_CH4 间接捕获CNT_fall; - 换算 :
脉宽 = CNT_fall - CNT_rise,距离(cm) = 脉宽(μs) × 0.01715; - 输出:有效距离送串口和告警逻辑,超时或超量程则丢弃。
真正需要记住的工程思想是:
- 计时交给硬件,算距离交给主循环;
- 触发、回波和按键扫描是三个不同时间尺度的任务,所以分别用 TIM7、TIM4 和 TIM6;
- 异步事件一定要有状态和超时,不能假设每次都有回波;
- 有效电平、引脚复用和共地,往往比公式更容易让实验失败。
7.2 关键参数表
| 项目 | 参数 |
|---|---|
| 主频与定时器时钟 | 72 MHz |
| 触发引脚 | PA1,高电平至少 10 μs |
| 回波引脚 | PB8,TIM4_CH3 |
| 捕获模式 | CH3 直接上升沿,CH4 间接下降沿 |
| TIM4 分频与周期 | PSC=71,ARR=65535 |
| 捕获分辨率 | 1 μs/计数 |
| 距离公式 | d_cm = t_us × 0.0343 / 2 |
| 代码整数公式 | centi_cm = t_us × 343 / 200 |
| 有效量程 | 2~400 cm |
| 告警阈值 | 2~10 cm |
| 测量周期 | 约 60 ms |
| 回波超时 | 50 ms |
| 按键扫描 | TIM6,1 ms 一次,20 ms 确认 |
| 串口 | USART1,115200,8N1 |
| 状态灯有效电平 | 低电平点亮 |
| 高亮 LED 有效电平 | 高电平点亮 |
7.3 自测问题
- Echo 高电平持续 2000 μs 时,按 343 m/s 计算,距离大约是多少厘米?
- 为什么 TIM4 的 CH4 选择间接输入捕获后,外部仍然只需要连接 PB8?
- PSC=71、ARR=65535 时,一个计数代表多长时间?为什么这个范围足够测 4 m?
- 为什么每次触发前要清零 TIM4 的计数器?如果不清零,可能出现什么现象?
- 如果 HC-SR04 没有返回 Echo,代码应该怎样从等待状态恢复?一直等下去会带来什么问题?
- 为什么 PA1 的 10 μs 脉冲适合交给 TIM7,而按键扫描适合交给 TIM6?
