【stm32】输入捕获-超声波测距

🔥铅笔小新z:个人主页

🎬博客专栏:stm32学习

💫滴水不绝,可穿石;步履不休,能至渊。


STM32 输入捕获实战:用 HC-SR04 做出可按键控制的超声波测距

  • 适用平台:STM32F103ZET6、STM32CubeMX、STM32 HAL 库,系统主频 72 MHz
  • 外设与技术点:HC-SR04、TIM4 输入捕获、TIM6/TIM7 基本定时器、GPIO、USART1
  • 最终效果:三个按键分别控制测距、串口打印和近距告警;串口显示厘米距离,距离进入 2~10 cm 时点亮高亮 LED
  • 适合读者:已经会写 C 语言、了解 GPIO 和定时器计数,但还没有独立完成过"传感器 + 中断 + 串口"小项目的人

这篇不讲"把外设都配一遍",只回答两个问题:

  1. 怎样稳定地产生 HC-SR04 需要的 10 μs 触发脉冲?
  2. 怎样准确测出 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 个知识点:

  1. HC-SR04 的 Echo 脉宽为什么代表往返时间。
  2. 输入捕获与普通 GPIO 轮询的本质区别。
  3. 为什么 PB8 一个引脚就能同时捕获上升沿和下降沿。
  4. PSC、ARR、CCR 在这个项目里各自负责什么。
  5. 为什么触发脉冲和按键扫描要交给两个基本定时器。
  6. HAL 回调、状态机和超时处理怎样把整条链路串起来。

1. 原理:先把"时间"变成"距离"

1.1 HC-SR04 到底做了什么

HC-SR04 正面有两个像"眼睛"的金属圆筒:一个发射超声波,一个接收回波。模块本身不直接输出厘米数,它只完成三件事:

  1. 收到触发信号后,发射 8 个连续的 40 kHz 超声脉冲;
  2. 同时把 Echo 引脚拉高;
  3. 收到回波后,把 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 只负责一件小事:

  1. 主循环先把 PA1 拉高;
  2. 启动 TIM7;
  3. 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 用示波器核对关键波形

建议按下面顺序看两个点:

  1. 探头接 PA1,确认每次测量都有一小段约 10 μs 的高脉冲;
  2. 探头接 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 一页纸总结

这条测距链路只有五个动作:

  1. 触发:PA1 输出至少 10 μs 高脉冲;
  2. 等待:HC-SR04 拉高 Echo,PB8 等待上升沿;
  3. 捕获 :TIM4_CH3 记录 CNT_rise,TIM4_CH4 间接捕获 CNT_fall;
  4. 换算 :脉宽 = CNT_fall - CNT_rise,距离(cm) = 脉宽(μs) × 0.01715;
  5. 输出:有效距离送串口和告警逻辑,超时或超量程则丢弃。

真正需要记住的工程思想是:

  • 计时交给硬件,算距离交给主循环;
  • 触发、回波和按键扫描是三个不同时间尺度的任务,所以分别用 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 自测问题

  1. Echo 高电平持续 2000 μs 时,按 343 m/s 计算,距离大约是多少厘米?
  2. 为什么 TIM4 的 CH4 选择间接输入捕获后,外部仍然只需要连接 PB8?
  3. PSC=71、ARR=65535 时,一个计数代表多长时间?为什么这个范围足够测 4 m?
  4. 为什么每次触发前要清零 TIM4 的计数器?如果不清零,可能出现什么现象?
  5. 如果 HC-SR04 没有返回 Echo,代码应该怎样从等待状态恢复?一直等下去会带来什么问题?
  6. 为什么 PA1 的 10 μs 脉冲适合交给 TIM7,而按键扫描适合交给 TIM6?

相关推荐
Zw-awa1 小时前
PID 的花样:先看懂模糊 PID 和单神经元自适应 PID
stm32
菩提树下的凡夫1 小时前
机器视觉LED光源控制器
单片机·嵌入式硬件
铅笔小新z1 小时前
【stm32】全彩led灯(PWM)
stm32·单片机·嵌入式硬件
小鸡脚来咯2 小时前
【无标题】
单片机·嵌入式硬件
澄纸气2 小时前
STM32 外部中断实战:对射式计数器和编码器计数器,两个实验跑通 EXTI
stm32·单片机·嵌入式硬件
2501_922639582 小时前
率能半导体 SS6286L直流双向马达驱动芯片:18V/7A,覆盖电子锁、玩具、无线充电、机器人四大应用场景
stm32·单片机·嵌入式硬件·硬件工程
搁浅小泽3 小时前
IGBT 与散热片间未加绝缘垫片导致桥臂直通原理分析
单片机·嵌入式硬件·设计规范
动词ing4 小时前
【51单片机】开始软件安装和构成
单片机·嵌入式硬件·51单片机
铅笔小新z4 小时前
【stm32】PWM呼吸灯
stm32·单片机·嵌入式硬件