ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32舵机扫描测距实战:HC-SR04超声波与定时器输入捕获详解

STM32舵机扫描测距实战:HC-SR04超声波与定时器输入捕获详解 简介一套基于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/s1us 对应的距离误差约 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.c和stm32f10x_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°0.5ms50045°1.0ms100090°1.5ms1500135°2.0ms2000180°2.5ms25003.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打印需要重定向标准库的fputcint 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 是否接到 PA10GND 是否共地波特率是否与上位机一致串口有输出但距离一直是 0Echo 是否接到 PA0Trig 是否接到 PB0检查模块供电是否为 5V舵机只颤抖不转动PWM 周期是否准确为 20msCCR 值是否在 500 到 2500 范围内舵机转到某个角度后乱抖外部电源功率不足尝试改用独立 5V 电源近距离测距回波丢失目标物体可能在 2cm 内超声波测量盲区调整安装位置还有一个很容易忽略的点源码包里同时存在stm32f10x_tim.c、stm32f10x_can.c、stm32f10x_i2c.c等文件这通常是标准外设库整库加入工程后的结果。编译时如果出现某个不相关内容的外部符号重复定义不要急着删文件先查看是不是在stm32f10x_conf.h里把没有被用到的外设头文件也#include了。用到的只是定时器、GPIO、USART 和延时多余外设不会参与执行但会占用一点 Flash 空间。最后验证测距范围设置时可以在串口调试助手里发送RANGE 20 80正常会返回RANGE OK:20-80然后把一个障碍物放到 10cm 位置观察是否输出 WARN。如果没反应检查串口命令结尾是否带了回车间程序解析时需要把\r\n过滤掉避免sscanf匹配不到指令。这套源码的边界能力其实很清楚普通舵机加 HC-SR04适合做慢速机械扫描测距但如果换成总线舵机机械臂联动场景PWM 驱动部分需要换掉串口测距和范围判断逻辑仍然可以继续复用。本文还有配套的精品资源点击获取
返回列表