STM32舵机扫描测距实战:HC-SR04超声波与定时器输入捕获详解
2026/9/16 18:56:06 网站建设 项目流程

简介:一套基于STM32F103ZE的舵机控制与超声波测距工程源码,面向嵌入式初学者及需要快速实现测距功能的开发者,解决舵机旋转扫描与HC-SR04实时测距联动的控制问题,测距范围可自由设置,结果通过串口输出显示。源码基于标准外设库编写,清晰展示定时器PWM驱动舵机、超声波回波捕获、串口打印等关键流程,且不局限于特定型号,其他STM32系列稍作移植即可使用。压缩包内共208个文件,除大量C源文件和头文件外,还包含编译过程生成的.o及调试配置文件,另附3份PDF文档——STM32固件库使用手册(中文版)、STM32中文参考手册_V10及HC-SR04模块用户手册V2,方便对照查阅。资源整体大小约34.37MB,已有1487人学习下载,实用性强。配套文档与源码结合,既能用于课程设计,也可作为项目原型快速验证,独立版权保障后续开发无隐患。

1. 为什么说“舵机加超声波测距”是一套完整的扫描测距起点

平时做测距,大多数项目只回答“前方有没有东西”;加上舵机之后,问题就变成“前方哪个方向有东西、距离多远”,这是避障小车、安防云台、机械臂避让这类场景的基础能力。这套源码基于 STM32F103ZE,用 HC-SR04 做超声波测距,用舵机旋转改变探测方向,并把角度和距离通过串口显示出来。源码里保留了“测距范围可设置”的处理逻辑,超过设定区间就按越界或报警处理,不是只有测距的裸 demo。工程使用标准外设库而不是 HAL,代码里直接操作寄存器和外设结构体,适合想从跑马灯进阶到“传感器加执行器联动”的开发者,也方便把逻辑移植到其它 F1 系列型号,前提是引脚和定时器做对应调整。

2. HC-SR04 时序拆解与定时器输入捕获配置

2.1 Trig 和 Echo:为什么最好用输入捕获而不是外部中断加延时

HC-SR04 的工作时序很简单:给 Trig 引脚一个至少 10us 的高电平,模块内部会发一串 40kHz 脉冲,然后 Echo 引脚拉高。Echo 高电平持续的时间,等于声波从模块到障碍物再返回的来回时间,所以把高电平时间乘上声速再除以 2,就得到单程距离。这个“高电平持续多久”是整套测距逻辑的关键测量对象。

很多入门教程会建议用外部中断测量 Echo 高电平时间,做法是:上升沿进入中断记一个ticks,然后空转等待下降沿,再取差值。这种办法在中断响应时间不稳定、编译器优化等级变化时会引入几十微秒甚至几百微秒的误差。超声波在空气中的传播速度大约是 340m/s,1us 对应的距离误差约 0.017cm,看似不大,但若误差达到 200us,就是 3.4cm 的波动,对 10cm 以内的近距离判断影响非常大。

常见做法是把 Echo 接到通用定时器的输入捕获引脚,让定时器硬件记录上升沿和下降沿时刻,中断里只做两个值的搬运,不参与计时要。这样测量精度不受 CPU 打断影响。源码包里出现了stm32f10x_tim.c,说明定时器这部分确实是工程重点。我一般把 Trig 配置为普通推挽输出,Echo 配置为输入捕获,两个信号分开处理,尽量避免用同一个定时器既输出 PWM 又输入捕获,否则会在改捕获极性时误动 PWM 输出。

下表是一份常见的引脚分配方案,实际接线以工程里的宏定义为准:

信号模块引脚STM32F103ZE 引脚配置方式
Trig超声波 TrigPB0GPIO_Mode_Out_PP,推挽输出
Echo超声波 EchoPA0TIM2_CH1,输入捕获
PWMSG90 舵机信号线PB1TIM3_CH2,复用推挽输出

2.2 初始化定时器输入捕获,并处理上升沿和下降沿两个边沿

先初始化 GPIO 和定时器时基,再把 TIM2_CH1 配置成输入捕获。下面的代码是标准外设库风格的初始化片段,和源码包里stm32f10x_tim.c的组织方式一致:

void HC_SR04_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x01; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }

这里把 TIM2 预分频设为 72 减 1,意思是按定时器时钟 72MHz 计算,分频后计数频率为 1MHz,一个计数值代表 1us。TIM_Period = 0xFFFF是把计数器溢出周期设为最大值 65535us,足够覆盖 HC-SR04 的满量程 4 米范围。Echo 高电平最大时间大约 23ms,小于 65.5ms 的溢出周期,所以正常测量不会发生周期翻转混乱。TIM_ICFilter设置为 0x01,用来过滤 Echo 信号上的毛刺,但注意滤波值太大会把窄脉冲滤掉,所以不要超过 0x04。

2.3 在中断里翻转捕获极性,再按 58 这个系数换算距离

输入捕获中断里不能做复杂运算,只记录上升沿和下降沿对应的计数器值。捕获极性的切换用的是TIM_OC1PolarityConfig,虽然名字里带 OC,但它同样可以改变输入捕获的边沿选择。中断处理写成这样:

volatile uint16_t echo_start = 0; volatile uint16_t echo_end = 0; volatile uint8_t echo_ready = 0; void TIM2_IRQHandler(void) { static uint8_t cap_rising = 1; uint16_t capture_value; if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) { capture_value = TIM_GetCapture1(TIM2); if (cap_rising) { echo_start = capture_value; TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Falling); cap_rising = 0; } else { echo_end = capture_value; TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Rising); cap_rising = 1; echo_ready = 1; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }

一帧测量完成后,在主循环里判断echo_ready,然后用(echo_end - echo_start) & 0xFFFF得到高电平持续时间,单位是 us。转换成厘米时,距离等于高电平时间 / 58。这个 58 的来源是:340m/s 的声速,按 20°C 下的速度来算,1us 里声波走 0.034cm,往返 1cm 需要的时间大约是 58us,所以 58 是往返时间常数,而不是单程。

注意,源码包里同时出现了stm32f10x_tim.cstm32f10x_adc.c等文件,这并不代表 ADC 一定被用到,标准外设库工程往往会包含整库文件。编译时只要没有符号冲突就可以正常使用,不需要因为看到多余的外设文件而怀疑工程完整性。

提示:HC-SR04 的 Echo 引脚实际输出是 5V 电平,STM32F103 的 GPIO 虽然标注 5V 耐受,但最稳妥的方式还是串一个 1k 电阻后再进 PA0,避免模块和 MCU 之间地电位差较大时损坏引脚。

3. 用 PWM 驱动舵机扫测,串起测距状态机

3.1 舵机要的是 20ms 周期脉宽,不是 50Hz 这么简单

普通 180° 舵机,比如 SG90,控制信号是一个周期 20ms 的 PWM,高电平脉宽决定转角。0.5ms 对应 0°,1.5ms 对应 90°,2.5ms 对应 180°。这里最容易被忽略的是“周期固定、只调高电平宽度”。如果把 PWM 频率直接写成 50Hz,但预分频算错,导致周期变成 10ms 或者 30ms,舵机就会出现抖动、发热甚至始终转到某个方向不动。

工程里的舵机控制建议单独使用一个通用定时器,例如 TIM3。下面是初始化 TIM3_CH2 输出 PWM 的标准外设库写法:

void Servo_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; TIM_TimeBaseStructure.TIM_Period = 20000 - 1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_Pulse = 1500; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC2Init(TIM3, &TIM_OCInitStructure); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }

这里预分频同样是 72 减 1,计数频率 1MHz,自动重载值设为 20000 减 1,所以完整周期是 20ms。TIM_Pulse = 1500代表初始默认 1.5ms 高电平,舵机停在 90°。改变角度时,只需要修改 TIM3 的 CCR2 寄存器,范围是 500 到 2500。

舵机目标角度高电平脉宽CCR2 写入值
0.5ms500
45°1.0ms1000
90°1.5ms1500
135°2.0ms2000
180°2.5ms2500

3.2 扫测状态机:先做位置稳定,再触发测距

舵机不是步进电机,PWM 值改变后需要几十毫秒才能稳定到目标角度。直接改完 CCR 立刻读超声波,很可能得到的是上一帧角度下的距离,或者读到舵机转动过程中产生的抖动数据。所以扫测动作适合写成“先转角度,再等待,再触发测距”的简单状态机。

角度间隔建议从 15° 开始,每次测量等待 200ms 以上。如果需要更快,可以把间隔改成 10° 或 5°,但超声波模块本身一个完整测量周期大约需要 60ms,因此建议最短等待时间不要低于 80ms,否则 Echo 会永远接不到下降沿。

void Servo_SetAngle(uint16_t angle) { uint16_t pulse = 500 + angle * 2000 / 180; TIM_SetCompare2(TIM3, pulse); } void Scan_Range(void) { uint16_t angle; uint16_t distance_cm; for (angle = 0; angle <= 180; angle += 15) { Servo_SetAngle(angle); delay_ms(200); distance_cm = HC_SR04_GetDistance(); if (distance_cm != 0) { printf("ANGLE:%d DIST:%dcm\r\n", angle, distance_cm); } } }

500 + angle * 2000 / 180就是把 0° 到 180° 线性映射到 500 到 2500 的两个端点。这个公式要放在计算时而不是做成查表,原因是不同舵机在中位附近可能有一些偏差,保留公式方便现场改成别的极限脉宽。超声波的返回值distance_cm如果为 0,说明本次没有捕获到有效 Echo,可能是目标太近或者线缆接触不良,此时不要进入报警判断,直接跳过本帧。

这块源码里提到的“实时测距”,本质是舵机每停到一个方向就刷新一次距离,并把结果通过串口发出去。对于秒级巡检足够,但不要指望用一个普通舵机能做到雷达那样连续扫描。

注意:舵机一定要从外部电源取电,不要直接挂在 STM32 开发板的 3.3V LDO 输出上。舵机转动瞬间电流可能超过 500mA,电压跌落会导致 MCU 复位,这是这类项目最常见的现场翻车点。

4. 串口显示与测距范围设置,把源码改成可用产品逻辑

4.1 USART1 串口初始化,配合重定向输出调试信息

串口输出的价值不只是看距离值,还能在系统跑飞时定位卡在哪个循环。工程里已经有stm32f10x_usart.c,这正好对应标准外设库的 USART 驱动。初始化代码不复杂:使能 GPIOA 和 USART1 时钟,把 PA9 配置为复用推挽输出,PA10 配置为浮空输入,波特率一般选 115200。

为了在源码里直接用printf打印,需要重定向标准库的fputc

int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; }

在 Keil 工程中,还要勾选 MicroLIB,重定向才会生效,否则会链接出大量文件系统代码。每次发送一个字节后死等 TXE 标志,意味着阻塞式打印。如果测距循环要求严格定时,建议把打印改成 DMA 发送,或者把字符串拼到缓冲区,用USART_DMACmd触发发送。这个项目里串口主要做监视,阻塞打印问题不大。

4.2 可设置测距范围,需要设计一个失效保护的阈值检查

很多测距 demo 只用if (distance < 20)来报警,这种硬编码阈值在改现场距离时特别痛苦。这个源码包里强调“测距范围可设置”,我按工程习惯会拆成两个部分:一是默认范围常量,二是串口指令实时修改。

默认范围用结构体保存:

typedef struct { uint16_t min_cm; uint16_t max_cm; } RangeConfig; RangeConfig range = {20, 80};

报警判断在扫描状态机里,测到距离后先和上下限比较:

uint8_t CheckDistanceRange(uint16_t distance_cm) { if (distance_cm == 0) { return 0; } if (distance_cm < range.min_cm || distance_cm > range.max_cm) { printf("[WARN]ANGLE:%d DIST:%dcm\r\n", current_angle, distance_cm); return 1; } return 0; }

判断顺序有一点细节:必须先判断distance_cm == 0,否则 0 会触发distance_cm < range.min_cm的误报警。HC-SR04 在障碍物贴近模块小于 2cm 时,经常返回不可靠数据,所以最小值不要设成 0,建议range.min_cm最低设到 2。

串口实时修改范围,最直接的做法是用sscanf解析:

void ParseSerialCommand(char *cmd) { int min_cm, max_cm; if (sscanf(cmd, "RANGE %d %d", &min_cm, &max_cm) == 2) { if (min_cm >= 2 && max_cm <= 400 && min_cm < max_cm) { range.min_cm = (uint16_t)min_cm; range.max_cm = (uint16_t)max_cm; printf("RANGE OK:%d-%d\r\n", range.min_cm, range.max_cm); } else { printf("RANGE ERR\r\n"); } } }

sscanf会带来一定的代码体积膨胀,但在这个资源里不值得关系性能,毕竟命令只会在配置阶段出现。对参数做边界判断是为了防止把最大距离设成 500cm,超过 HC-SR04 实际能力导致虚假报警。我见过有人把阈值设成最大值 400cm,结果在走廊尽头测到 380cm 就直接报警,实际上是模块量程导致的波动,不是真实阻挡。

串口调试数据的格式最好保持机器可读,方便在 PC 端解析:

串口输出内容含义
ANGLE:0 DIST:15cm当前舵机角度和距离值
RANGE OK:20-80距离范围设置成功
RANGE ERR范围参数非法,保持原值
WARN ANGLE:45 DIST:8cm超出设置范围的报警输出

如果要做更正式的产品逻辑,还可以把范围设置保存到片内 Flash,利用源码包里已有的stm32f10x_flash.c,掉电后不丢失。保存时建议连同一个校验字段一起写入,比如把 min、max 和固定魔数拼成一个结构体,再按半字写入 Flash。读取时先校验魔数,校验失败就恢复默认范围,防止 Flash 全 FF 状态导致阈值变成 65535。

5. Keil5 编译烧写与实物验证:先看这几个调试点

拿到源码后第一件事不是打开工程看代码,而是把硬件和电源准备好。这个项目用到 5V 超声波模块和 5V 舵机,建议用一个 5V 2A 左右的电源给传感器和舵机供电,开发板用独立的 USB 3.3V 供电,两组电源共地即可。不要在调试中让舵机和超声波共用一根杜邦线从板上取电。

在 Keil5 里打开工程后,先检查 Target 选项卡里的时钟频率是不是 72MHz。源码里出现了system_stm32f10x.c,正常情况下SystemCoreClock = 72000000。如果开发板用了 8MHz 晶振,这个值没有问题;如果换成 25M 晶振或其它频率,需要先修改system_stm32f10x.c里的 PLL 配置,否则定时器预分频按 72MHz 算出来的 PWM 周期会偏掉。最容易观察的现象是舵机角度不准,但串口测距数值可能是正常的,因为 HC-SR04 的 58us/cm 换算只依赖同一路捕获分频。

下载算法也要确认。项目文件名里带着Demo.axf,这是一个旧的编译输出文件,不一定是当前工程最新产物。建议用 ST-Link 或 J-Link 重新编译并配置 Flash Download,使用默认的 STM32F10x Med-density 或 High-density 算法,取决于具体芯片型号。F103ZE 是 512KB Flash,属 High-density,选错算法会导致下载地址越界。

实物验证时我会按下面这张表和现象对照排查:

现象描述优先检查点
串口没有任何输出串口 RX 是否接到 PA10,GND 是否共地;波特率是否与上位机一致
串口有输出但距离一直是 0Echo 是否接到 PA0,Trig 是否接到 PB0,检查模块供电是否为 5V
舵机只颤抖不转动PWM 周期是否准确为 20ms;CCR 值是否在 500 到 2500 范围内
舵机转到某个角度后乱抖外部电源功率不足,尝试改用独立 5V 电源
近距离测距回波丢失目标物体可能在 2cm 内,超声波测量盲区,调整安装位置

还有一个很容易忽略的点:源码包里同时存在stm32f10x_tim.cstm32f10x_can.cstm32f10x_i2c.c等文件,这通常是标准外设库整库加入工程后的结果。编译时如果出现某个不相关内容的外部符号重复定义,不要急着删文件,先查看是不是在stm32f10x_conf.h里把没有被用到的外设头文件也#include了。用到的只是定时器、GPIO、USART 和延时,多余外设不会参与执行,但会占用一点 Flash 空间。

最后验证测距范围设置时,可以在串口调试助手里发送RANGE 20 80,正常会返回RANGE OK:20-80,然后把一个障碍物放到 10cm 位置,观察是否输出 WARN。如果没反应,检查串口命令结尾是否带了回车间,程序解析时需要把\r\n过滤掉,避免sscanf匹配不到指令。这套源码的边界能力其实很清楚:普通舵机加 HC-SR04,适合做慢速机械扫描测距,但如果换成总线舵机机械臂联动场景,PWM 驱动部分需要换掉,串口测距和范围判断逻辑仍然可以继续复用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询