ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

舵机超声波测距避障系统深度解析:PWM控制与源码实战

2026/9/16 15:54:43 拓冰建站 浏览量
舵机超声波测距避障系统深度解析:PWM控制与源码实战 简介面向STM32开发者的舵机超声波测距源码工程以STM32F103ZE系列单片机为核心通过舵机旋转带动HC-SR04模块扫描四周实现实时测距、距离范围可设置并将距离数据经串口显示适合嵌入式入门、电子设计竞赛和DIY项目等场景。代码基于STM32标准外设库编写模块化程度高整个工程目录划分明确源码注释清晰便于二次开发和移植到其他F103型号。资源包采用7z压缩格式共208个文件约34.37MB涵盖C语言源码、头文件、Keil工程配置及编译链接文件并包含STM32固件库使用手册、STM32中文参考手册和HC-SR04用户手册三份PDF方便边读手册边对照代码。已有1487位学习者在CSDN学习下载。通过这份资料读者可快速搭建舵机云台测距原型理解定时器PWM驱动、超声波回波测量、串口通信与阈值设置等关键模块节省自行查阅手册和调试的时间。1. 拿到“舵机加超声波测距源码.7z”之前先想清楚它要干什么很多人在网上下到“舵机加超声波测距源码.7z”这类压缩包第一反应是解压、打开main.c、找避障逻辑结果看完也不知道舵机为什么要转、超声波和它怎么配合。这个标题实际上描述了一个嵌入式里非常经典的组合用舵机改变超声波探头的朝向把“测一段距离”扩展成“扫一个平面”最终做成小车防撞、机械臂防碰撞或者最简雷达。它适合 STM32、51、Arduino 的入门到进阶玩家也适合机器人竞赛里做快速原型。下载代码只是第一步真正有价值的是把 PWM 脉宽、测距时序、扫描策略和供电这四件事串起来遇到问题才知道往哪里查。2. 舵机 PWM 和 HC-SR04 测距的基本原理与选型2.1 标准舵机和总线舵机源码里的“舵机”是哪一种标题里的“舵机”最常见的落点是 SG90、MG996R 这类标准舵机。它们的控制信号是 50Hz 的 PWM周期 20ms脉宽决定转角。市场上也有所谓“总线舵机机械臂”用的总线舵机那是半双工串口通信引脚和协议完全不同不能直接套用普通舵机源码。判断方法很简单看源码初始化里有没有配置定时器 PWM 输出有就是标准舵机项目。SG90 这类舵机的脉宽范围不同厂商定义略有差异。我一般按 0.5ms 到 2.5ms 做满量程也就是 0° 到 180°中间值 1.5ms 对应 90°。有些舵机 Datasheet 写的是 1ms 到 2ms这种就要在源码的参数表里单独改否则会转不到两端甚至堵转。舵机型号PWM 周期0° 脉宽90° 脉宽180° 脉宽控制方式SG9020ms0.5ms1.5ms2.5msPWMMG996R20ms0.5ms1.5ms2.5msPWM总线舵机不适用不适用不适用不适用UART 指令2.2 HC-SR04 的测距时序与距离换算HC-SR04 有四个引脚VCC、GND、Trig、Echo。测距过程是先把 Trig 拉高至少 10us模块内部发射 8 个 40kHz 超声波脉冲然后把 Echo 拉高。Echo 高电平持续的时间等于超声波从发射到碰到障碍物再返回的总时间。距离换算用声速 340m/s单程距离等于时间乘声速再除以 2。为了方便嵌入式运算通常写成距离(cm) Echo高电平时间(us) * 0.017。举个例子如果测到 Echo 高电平持续 5800us那就是 94.6cm 左右符合日常经验。注意 HC-SR04 标称测量范围是 2cm 到 400cm实际超过 4m 的回波很弱代码里一般把 30ms 以上的高电平当成超时处理。2.3 STM32CubeMX 生成 PWM 与输入捕获的配置顺序用 STM32CubeMX 配这个问题很省事关键是两个定时器的分工。我一般这样做选一个定时器通道做舵机 PWM比如 TIM2_CH1PSC 设 71、ARR 设 19999。这样 72MHz 主频下计数频率是 1MHz计数周期 20msCCR 写入 1500 就是 1.5ms 脉宽对应 90°。再开一个定时器做 Echo 高电平计时比如 TIM3 的输入捕获通道或者干脆开一个 1us 周期的普通定时器用外部中断在 Echo 上升沿清零、下降沿读值。Trig 引脚配推挽输出Echo 引脚配浮空输入或上拉输入。如果 Echo 直接接 3.3V 单片机要考虑电平问题后面第 4.4 节专门说。这个配置方式在 Keil5 里看源码时特别有用看到PSC71, ARR19999基本就知道作者是按 50Hz PWM 设计的CCR 的单位是微秒。如果用 51 单片机没有硬件 PWM 就得用定时器中断翻转 IO用 Arduino 的话Servo.h已经封装好了角度写入Echo 可以用pulseIn()读取。树莓派 Pico 用 MicroPython 也能跑通只要把 PWM 频率设成 50Hz 即可。3. 把 .7z 解压出来后怎么读源码目录结构与三个关键文件3.1 7z 解压后先看根目录别急着点开 MDK 工程拿到带 .7z 扩展名的源码包我先做两件事第一用 7-Zip 或7z x解压到纯英文路径避免 Keil 因为中文目录出幺蛾子第二打开根目录看层级。常见的目录结构大概是下面这样servo_ultrasonic/ ├── User/ │ └── main.c ├── HARDWARE/ │ ├── Servo/ │ │ ├── Servo.c │ │ └── Servo.h │ └── Ultrasonic/ │ ├── Ultrasonic.c │ └── Ultrasonic.h ├── MDK-ARM/ │ └── project.uvprojx └── 文档/ └── 接线说明.txt这个结构的好处是把硬件驱动和主逻辑分开。Servo.c只管输出 PWMUltrasonic.c只管测一次距离main.c决定什么时候转舵机、什么时候测距。如果解压后只有一个庞大的main.c那就要做好读代码时手动分块的准备。3.2 Servo.c把角度换算成 CCR 时要先限位再写比较寄存器舵机驱动代码核心就是把“角度”映射到“CCR 值”。我用 STM32 HAL 库写的话大概是这个样子#define PWM_PERIOD_US 20000u // 20ms 对应 50Hz #define ANGLE_MIN_CCR 500u // 0.5ms 对应 0 度 #define ANGLE_MAX_CCR 2500u // 2.5ms 对应 180 度 void servo_set_angle(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t angle) { uint32_t ccr; if (angle 180) { angle 180; } // 注释限位后做线性映射避免写入超出舵机机械范围的值 ccr ANGLE_MIN_CCR (uint32_t)angle * (ANGLE_MAX_CCR - ANGLE_MIN_CCR) / 180; __HAL_TIM_SET_COMPARE(htim, channel, ccr); }关键在__HAL_TIM_SET_COMPARE这一行。它只做一件事把 CCR 寄存器改成目标值真正的 PWM 波形由定时器硬件持续产生不需要软件每次翻转 IO。所以你在while(1)里反复调用servo_set_angle也不会造成波形毛刺前提是参数合法。3.3 Ultrasonic.c测距函数必须带超时保护不带的超声波测距函数迟早会卡死主循环。原因是 Echo 引脚如果悬空、模块损坏或者前方就是一堆吸音材料高电平可能一直不结束。我一般这样写#define ECHO_TIMEOUT_US 30000u // 30ms 对应约 5 米以上属于异常情况 uint8_t ultrasonic_measure(uint16_t *distance_cm) { uint32_t echo_us; // 触发拉高 Trig 15us模块要求至少 10us HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(15); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); // 等待 Echo 变为高电平刚上电时可能残留旧电平这里也要防卡 while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_RESET) { // 如果长时间不拉高直接返回 1 表示错误 } // 开始计时这里以定时器计数值换算微秒 timer_clear_and_start(); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_SET) { if (timer_get_us() ECHO_TIMEOUT_US) { timer_stop(); return 1; // 超时本次测量作废 } } echo_us timer_stop(); *distance_cm (uint16_t)(echo_us * 0.017f); return 0; // 0 表示成功 }这个函数返回值很重要0 是有效距离1 是超时。调用方不能拿到返回值就直接用否则超时数据会被当成几十米塞进避障逻辑小车会以为前方永远空旷。常见做法是在避障主循环里连续读到超时才放弃当前方向单次超时先用上次有效值。3.4 main 里的 while(1) 调度方式决定了扫描帧率很多源码的主循环长这样while (1) { for (angle 0; angle 180; angle 5) { servo_set_angle(htim2, TIM_CHANNEL_1, angle); delay_ms(30); // 给舵机一点反应时间 if (ultrasonic_measure(dist) 0) { printf(%d,%d\r\n, angle, dist); } } }这段代码简单直接但帧率很低。从 0° 到 180°、步进 5°一共 37 个点。每个点等舵机稳定 30ms测距和串口输出再占用 20ms 左右一帧超过 1.8 秒。如果车在移动舵机还没扫完一轮车可能已经撞上了。看到源码里延时是delay_ms(100)这种说明作者更看重舵机到位精度不太看重实时性。如果想让扫描和测距不互相拖累可以把for循环改成状态机当前角度只在定时器中断里更新测距结果通过标志位通知主循环。这样舵机可以持续运动测距也能在固定间隔执行。4. 把舵机和超声波拼成避障系统需要改的只有四个地方4.1 扫描角度范围和步进角先定义正前方是哪一边拿到源码不要先改距离阈值先确认 0° 在物理上指向哪里。舵机机械零点未必是正前方有的安装方式把 0° 设在了车体左侧。如果直接套用“前方障碍”的判定左转右转会完全反掉。我一般用三个宏定义来控制#define SCAN_STEP 5 // 步进角5 度扫描有重叠 #define SCAN_LEFT_ANG 30 // 最左扫描角度 #define SCAN_RIGHT_ANG 145 // 最右扫描角度 #define FRONT_ANGLE 90 // 正前方对应的舵机角度扫描范围不一定要 0° 到 180°。超声波探头有大约 15° 的波束角左右两端各留 20° 余量通常只扫 30° 到 150° 就够了。这样一帧从 37 个点降到 25 个点帧率提高不少。4.2 遇到障碍先停转向再继续简单的三段式避障最常见的控制逻辑是距离小于阈值就停车、扫描左右、往远的一面转。伪 C 代码如下#define WARN_DIST_CM 30 if (front_dist WARN_DIST_CM) { motor_stop(); d_left scan_once(60); // 左前方 60 度 d_right scan_once(120); // 右前方 120 度 if (d_left d_right) { motor_turn_left(); } else { motor_turn_right(); } } else { motor_forward(); }这里的scan_once(60)不是简单的servo_set_angle加ultrasonic_measure。舵机转到新位置后需要 30ms 到 50ms 稳定时间立刻测距经常拿到一个偏大的值。我一般让scan_once内部先设置角度、延时 50ms、连续测三次取中值。中值滤波比均值滤波更适合应对超声波偶尔冒出的离群点。4.3 左右搜索找“最远方向”而不是“有路就行”只比较左右两个方向有个问题两边都小于阈值时小车会原地反复转向。更稳的做法是扫一个扇形取所有角度里距离最大的那个方向走。比如左右各扫 5 个点拿到 10 个距离值取最大值对应的角度。这时代码里要维护一个距离数组uint16_t dists[11]; uint16_t max_dist 0; uint8_t best_idx 0; for (i 0; i 10; i) { angle SCAN_LEFT_ANG i * SCAN_STEP; dists[i] scan_once(angle); if (dists[i] max_dist) { max_dist dists[i]; best_idx i; } } if (max_dist WARN_DIST_CM) { motor_backward(); // 最远方向也不够先倒车 } else { turn_to_angle(SCAN_LEFT_ANG best_idx * SCAN_STEP); }取最远方向比取“第一个大于阈值的角度”更抗干扰代价是舵机要多转几个位置。这一套思路同样可以用在四足机器人和机械臂防碰撞上只是机械臂还要额外考虑末端执行器的位姿。4.4 供电与接线顺序舵机一转单片机就复位的排查舵机启动瞬间电流很大。SG90 空载电流几十毫安堵转时能到 650mA 左右MG996R 堵转可以冲到 2.5A。如果从开发板的 5V 引脚直接给舵机供电板载 LDO 根本扛不住结果就是“程序一跑舵机一动单片机立刻复位”。部件电源地信号单片机板5V/3.3VGND不接舵机电源舵机独立 5V 电源GND 与单片机共地PWM IOHC-SR04VCC5VGND 共地Trig/Echo IO接线的关键顺序是先把所有 GND 连在一起再给舵机供独立电源最后插信号线。HC-SR04 的 Echo 输出 5V 电平如果单片机是 3.3V 的 STM32建议用两个电阻分压到 3.3V 再接引脚避免长期运行损伤 IO。有 PCA9685 舵机控制板的话可以把舵机信号统一接到板上但 PCA9685 的 V 也要独立供电不能指望 STM32 的 3.3V 输出带多个舵机。5. 超声波测距跳变和舵机抖动的 5 个排查点现象概率最高的原因验证方法距离一直显示 0Trig 没触发或模块供电异常逻辑分析仪看 Trig 和 Echo距离一直满量程Echo 悬空或模块损坏用手在探头前晃动数值在几十厘米间乱跳舵机启动拉低电源万用表测舵机转动时 VCC舵机到位后嗡嗡响PWM 频率或脉宽不对检查 PSC/ARR 和起始脉宽测距值偶尔偏大很多超声波波束打到斜面或遮罩固定探头做静态测试5.1 距离显示 0cm 或永远满量程先看 Echo 空闲电平正常工作时Echo 空闲应该是低电平触发后拉高。如果每次测距立刻返回 0先看 Trig 引脚有没有像样的 10us 高电平脉冲很多移植代码把delay_us在 72MHz 下写错实际只延时了 1us 都不到模块不认。如果一直返回满量程把 Echo 引脚断开用手触摸排针如果距离突然变化说明 Echo 内部已经拉高大概率是模块损坏或 Trig 噪声导致误启动。5.2 数值在 40cm 和 80cm 之间乱跳优先怀疑电源而不是滤波滤波能改善数据但治不了根。舵机转动瞬间电流陡增会把 5V 拉低到 4.7V 甚至更低HC-SR04 的参考电压跟着抖测出来的时间自然五花八门。用一个万用表或者示波器在舵机从 0° 甩到 180° 时看 HC-SR04 的 VCC 引脚跌落超过 0.2V 就要换供电方案。独立电源加上粗一点的杜邦线往往比在代码里加十层滑动平均更有效。5.3 舵机到位后嗡嗡响PWM 频率和脉宽范围不对舵机对 PWM 周期敏感标准工作是 50Hz 也就是 20ms。如果配置成了 2ms 周期即使脉宽写 1.5ms占空比也远超正常范围舵机不断往一边顶然后找位置就会一直嗡嗡响。检查定时器频率的方法是PWM频率 定时器时钟 / ((PSC1) * (ARR1))算出来不是 50Hz 就先改这里。另外脉宽不要小于 0.5ms 或大于 2.5ms超出机械行程后舵机电流飙升抖动和发热会同时出现。5.4 Echo 接到输入捕获引脚却没有捕获中断初始化顺序颠倒有些源码把 Echo 接到输入捕获通道但在HAL_TIM_IC_Start_IT之前先配置了 GPIO 复用功能中断却始终不触发。排查时先确认 GPIO 的复用功能打开再确认 NVIC 里对应中断已勾选。更简单的做法是绕开输入捕获直接用普通 GPIO 读取 Echo 电平搭配一个 1us 定时器查询计数值代码好懂也不容易受输入捕获极性配置影响。5.5 有没有必要做温度修正室内项目和室外项目的分界线声速随温度变化公式是v 331.4 0.607 * T单位 m/s。如果环境温度从 20°C 变到 30°C同样的时间距离误差约 1.7%1 米距离差 1.7cm。室内恒温环境下这个误差远小于超声波本身的波束反射误差不用管。室外巡检车或者无人机定高想要稳定一点可以加一个温度传感器float sound_speed 331.4f 0.607f * temperature_celsius; *distance_cm echo_us * sound_speed / 2.0f / 10000.0f;注意这个修正只对声速有效HC-SR04 的计时精度本身有限不要指望修正后变成毫米级。6. 进阶用串口把舵机扫描结果画成极坐标雷达图6.1 单片机端串口输出协议想让舵机转轴和超声波探头变成一个简易雷达串口输出要带角度和距离两个维度。我习惯的协议是每行输出角度,距离角度单位度距离单位厘米一帧结束后单独输出一行E。单片机端只要在扫描循环里加一句printf(%d,%d\r\n, angle, dist);波特率用 115200角度精度 5°一帧 37 个点数据量不到 1KB完全够用。用串口助手先看数据确认有规律了再画图。6.2 Python 上位机实时画极坐标用 Python 的串口库加 matplotlib可以实时看到极坐标图。脚本要处理帧尾和脏数据import serial import numpy as np import matplotlib.pyplot as plt ser serial.Serial(COM5, 115200, timeout1) plt.ion() fig, ax plt.subplots(subplot_kw{projection: polar}) angles [] dists [] for line in ser: line line.decode(errorsignore).strip() if line E: ax.clear() ax.scatter(np.radians(angles), dists, cdists, cmapviridis, s20) ax.set_ylim(0, max(dists [1]) * 1.2) plt.pause(0.05) angles.clear() dists.clear() else: try: a, d map(float, line.split(,)) except ValueError: continue angles.append(a) dists.append(d)这里ax用的是 polar 投影角度要转成弧度所以调用np.radians。每遇到E就清空重画不会让旧扫描点越积越多。max(dists [1])是防止第一帧还没有数据时列表为空导致报错。如果串口号在 Linux 下把COM5换成/dev/ttyUSB0。6.3 把修改后的源码连同绘图脚本一起重新打进 7z改完参数、写完上位机源码就变多了。重新打包时用命令行更可控Windows 和 Linux 通用的 7-Zip 命令是7z a -t7z -mx9 -mmton servo_ultrasonic_20260601.7z MDK-ARM HARDWARE User docs-mx9是极限压缩嵌入式源码文本压缩率很高-mmton开启多线程。打包后顺手算一下哈希值分发或者存网盘不容易被静默损坏7z h servo_ultrasonic_20260601.7z接收方解压后7z h -scrcsha256比对能挡住下载损坏和误改版本的问题。Linux 下解压这种包需要先装 p7zip-full然后7z x 文件名.7z -o输出目录解压时保留目录层级可以防止源码散落一地。本文还有配套的精品资源点击获取