资讯动态

STM32驱动HC-SR04超声波测距:输入捕获模式实战详解

发布时间:2026/8/8 23:04:28 来源:尧图企业网站定制
1. 项目概述从零到一驱动超声波测距在嵌入式开发尤其是机器人、智能小车或者简单的避障系统中超声波测距模块HC-SR04几乎是入门必玩的一个外设。它成本低廉、原理直观但很多新手在第一次用STM32驱动它时往往会卡在几个关键点上CubeMX里定时器怎么配回响信号的高电平时间怎么精准捕获HAL库的延时函数准不准我最初做这个项目时也踩过不少坑比如用HAL_Delay去测时间导致精度全无或者没处理好中断优先级导致测距结果跳动得厉害。这篇文章我就以一个过来人的身份带你用STM32CubeMX和HAL库完整地走一遍HC-SR04的驱动流程。我们不止步于“点亮”更要深究每一步配置背后的“为什么”并分享那些调试过程中积累下来的、数据手册里不会写的实战经验。无论你是刚接触STM32的新手还是想优化现有代码的老手相信都能找到有用的东西。2. 核心思路与方案选型为什么是“输入捕获”拿到HC-SR04模块首先要吃透它的工作原理。模块有四个引脚VCC、GND、Trig触发和Echo回响。工作流程非常固定单片机给Trig引脚一个至少10us的高电平脉冲作为触发信号模块内部发出8个40kHz的超声波脉冲如果前方有障碍物超声波被反射回来模块接收到后会在Echo引脚输出一个高电平这个高电平的持续时间正好等于超声波从发射到返回所经过的时间。所以核心任务就变成了精确测量Echo引脚高电平的持续时间。有了这个时间t单位微秒根据声速约340m/s即0.034cm/us距离d (t * 0.034) / 2。除以2是因为时间是往返的。那么在STM32上如何精确测时间通常有几种思路轮询GPIO状态 系统滴答定时器SysTick这是最朴素的方法。触发后不断读取Echo引脚电平用HAL_GetTick()之类的函数记录时间差。这种方法问题很大HAL_Delay()和HAL_GetTick()默认精度是1ms远大于我们需要的微秒级精度而且CPU被死死绑在循环里效率极低。外部中断 定时器计数将Echo引脚配置为外部中断。上升沿中断开启一个定时器计数下降沿中断停止计数并读取值。这种方法比轮询好但频繁进入中断对系统有干扰且需要处理中断嵌套和优先级问题。定时器输入捕获模式推荐这是最专业、最精准的方案。将Echo引脚连接到STM32的一个定时器通道并将该通道配置为输入捕获模式。当Echo上升沿到来时硬件会自动将当前定时器计数器的值锁存到捕获/比较寄存器CCR中并可以产生中断下降沿再来时再次锁存。我们只需要在中断里读取这两个CCR值其差值就是高电平期间定时器计数的次数再根据定时器的计数频率就能换算成精确时间。整个过程由硬件完成不占用CPU精度极高。毫无疑问方案3是我们的最佳选择。而STM32CubeMX正是配置这种复杂外设的利器它能可视化地帮我们完成定时器模式、引脚、中断等繁琐的底层设置让我们专注于应用逻辑。3. 硬件连接与CubeMX工程创建在打开软件之前先搞定硬件连接。这看似简单但连接错误是后续一切问题的根源。硬件连接清单STM32最小系统板以STM32F103C8T6为例HC-SR04超声波模块杜邦线若干3.3V/5V电源注意HC-SR04的VCC接5V性能更稳定但其Echo引脚输出的是5V TTL电平。大多数STM32的GPIO引脚耐受电压是5V但为了保险最好确认你的芯片型号是否支持5V容忍如F1系列很多IO是FT的即5V容忍。如果不确定或者使用3.3V IO的芯片需要在Echo信号线上串联一个1k-2k的电阻进行分压或者使用电平转换模块。连接方式HC-SR04.VCC - 5VHC-SR04.GND - GNDHC-SR04.Trig - 连接到STM32的任意一个GPIO输出引脚如PA0HC-SR04.Echo - 连接到STM32的一个具有定时器输入捕获功能的引脚如PA6对应TIM3_CH1注意选择Echo引脚不是随意的必须查阅你的STM32芯片数据手册或CubeMX的引脚分配图找到支持定时器输入捕获功能的引脚。通常一个定时器如TIM3会有多个通道CH1, CH2...每个通道对应特定的引脚。CubeMX工程创建步骤新建项目打开STM32CubeMX点击“New Project”。在芯片选择器中输入你的型号如STM32F103C8选中并点击“Start Project”。系统核心配置SYS在“Pinout Configuration”标签页左侧找到“System Core” - “SYS”。将“Debug”改为“Serial Wire”如果你使用ST-Link调试器。这是为了启用SWD下载调试接口非常重要。时钟配置RCC找到“System Core” - “RCC”。将“High Speed Clock (HSE)”选择为“Crystal/Ceramic Resonator”。这告诉芯片我们使用外部高速晶振这是获得精确时钟的基础。配置Trig引脚为GPIO输出在右侧的芯片图形上找到你计划用于Trig的引脚如PA0。点击它选择“GPIO_Output”。然后在左侧“System Core” - “GPIO”中点击刚配置的PA0可以设置其初始输出电平为低电平并可以重命名用户标签为“TRIG”方便代码阅读。配置Echo引脚为定时器输入捕获在芯片图形上找到计划用于Echo的引脚如PA6。点击PA6在弹出的功能列表中找到“TIM3_CH1”或者其他TIMx_CHy。选择它这个引脚就被配置为定时器3的通道1了。此时左侧会多出一个“TIM3”的配置项。4. CubeMX关键外设配置详解工程骨架有了现在进入最核心的配置部分。这里每一步的设置都直接影响后续代码的简洁度和性能。4.1 定时器TIM3输入捕获配置点击左侧“Timers” - “TIM3”进入详细配置。时钟源与分频“Clock Source” 保持默认的“Internal Clock”即可。定时器会使用APB总线提供的时钟。切换到“Parameter Settings”标签页。Prescaler (PSC)预分频器。这是决定定时器计数精度的关键参数。定时器计数时钟 定时器时钟源 / (PSC 1)。我们的目标是实现微秒级计时。假设系统主频为72MHzSTM32F103常见配置APB1定时器时钟也是72MHz。如果我们设置PSC71则计数时钟频率 72MHz / (711) 1MHz。这意味着计数器每增加1代表时间过去了1微秒计算距离时非常方便。这里我们填71。Counter Mode计数模式选择“Up”向上计数。Counter Period (ARR)自动重装载值。计数器从0计数到这个值后溢出归零。为了能测量更长的距离这个值要足够大。HC-SR04最大测距约4米对应高电平时间最大约4m * 2 / 0.034 cm/us ≈ 23529 us。为了留有余量我们可以设置ARR为最大值6553516位定时器。这里填65535。这样我们的定时器可以测量最长65535微秒约65ms的高电平远超模块量程。auto-reload preload使能“Enable”。这样可以在运行时更新ARR值而不会立即生效避免计数异常。输入捕获通道配置在下方找到“Channel 1”对应我们连接的PA6。点击下拉菜单选择“Input Capture direct mode”。随后会多出“IC1”的配置项点击它。ICPolarity输入捕获极性。我们希望在Echo信号的上升沿和下降沿都进行捕获。但CubeMX一个通道只能配置一种极性。标准的做法是将通道配置为上升沿捕获然后在代码中动态切换极性来捕获下降沿。所以这里先选择“Rising Edge”。ICSelection选择“Direct”。表示捕获的信号直接来自本通道的引脚。ICPrescaler输入捕获预分频器选择“No division”。每个边沿都捕获。ICFilter输入滤波器。可以设置一个数字滤波值用于滤除引脚上的高频毛刺。如果布线良好可以设为0。如果环境干扰大测距值偶尔跳动可以适当增大如设为4。初始建议设为0。中断与DMA配置切换到“NVIC Settings”标签页。找到“TIM3 global interrupt”勾选“Enabled”。这样定时器发生更新事件溢出和捕获事件时才能触发中断让我们在中断服务函数里处理数据。可选DMA如果需要极高效率、连续不断的测距可以考虑使用DMA将捕获值直接搬运到内存。但对于初学者和大多数应用中断方式完全足够。4.2 时钟树配置定时器的精度根源于系统时钟。点击上方“Clock Configuration”标签页。 对于STM32F103一个常见的配置是HSE8MHz - PLL倍频9倍 - 得到72MHz的系统时钟SYSCLK。APB1预分频器设为2则APB1总线时钟为36MHz。但STM32有一个特性当APB1分频系数不为1时挂载在APB1上的定时器时钟会乘以2。所以此时TIM3的时钟实际是36MHz * 2 72MHz。这与我们之前计算PSC71得到1MHz计数频率的前提吻合。你需要根据你的芯片和晶振实际情况配置出一个稳定的、已知频率的系统时钟。4.3 生成工程代码项目设置点击上方“Project Manager”标签页。“Project”子标签下设置项目名称、存储路径、IDE如MDK-ARM V5。“Code Generator”子标签下强烈建议勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”。这会将每个外设的初始化代码生成独立的文件使工程结构非常清晰。生成代码点击右上角的“GENERATE CODE”。CubeMX会生成完整的初始化代码和工程文件。5. 驱动代码编写与逻辑实现打开生成的工程如Keil MDK我们主要需要修改和添加两个文件的内容main.c和tim.c或tim.h。5.1 宏定义与变量声明在main.c文件顶部用户代码区添加以下定义和变量/* Private variables ---------------------------------------------------------*/ // 定义Trig引脚根据你的实际连接修改 #define TRIG_PIN GPIO_PIN_0 #define TRIG_PORT GPIOA // 用于存储测量结果 volatile uint32_t g_capture_start 0; // 上升沿捕获值 volatile uint32_t g_capture_end 0; // 下降沿捕获值 volatile uint8_t g_is_captured 0; // 完成一次测量的标志 volatile uint32_t g_distance_mm 0; // 计算出的距离毫米 /* Private function prototypes -----------------------------------------------*/ void Trigger_Sonic(void); // 触发函数声明 uint32_t Get_Distance(void); // 获取距离函数声明5.2 触发函数与主循环逻辑在main.c文件中while(1)循环之前添加触发函数/** * brief 产生一个至少10us的高电平脉冲触发HC-SR04 * param None * retval None */ void Trigger_Sonic(void) { HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); // 拉高Trig HAL_Delay(1); // 延时1ms远大于10us确保触发。注意这里用HAL_Delay是可以的因为对精度要求不高。 HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); // 拉低Trig }在main.c的while(1)主循环中添加测距逻辑while (1) { // 1. 启动一次测量 g_is_captured 0; // 清除上一次的完成标志 Trigger_Sonic(); // 发送触发脉冲 // 2. 等待测量完成超时处理 uint32_t timeout HAL_GetTick(); while(g_is_captured 0) { if((HAL_GetTick() - timeout) 100) // 等待超时例如100ms { // 超时处理可以设置一个错误距离如9999 g_distance_mm 9999; break; } } // 3. 获取并处理距离数据 if(g_is_captured 1) { uint32_t distance Get_Distance(); // 计算距离 // 这里可以添加滤波算法比如连续采样5次取中值 // 然后将最终的距离值用于显示、控制等 // 例如通过串口打印printf(Distance: %lu mm\r\n, distance); } // 4. 两次测量之间需要至少60ms的间隔防止信号干扰 HAL_Delay(60); }5.3 输入捕获中断回调函数这是驱动代码的灵魂位于stm32f1xx_it.c或者你可以在main.c中重写HAL库的弱定义回调函数。更推荐后者因为逻辑更集中。在main.c中找到/* USER CODE BEGIN 4 */和/* USER CODE END 4 */之间添加以下回调函数/** * brief 输入捕获中断回调函数 * param htim : 定时器句柄 * retval None */ void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { // 判断是否是TIM3的通道1发生捕获中断 if(htim-Instance TIM3 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { if(g_is_captured 0) // 第一次捕获即上升沿 { // 读取上升沿时刻的计数器值 g_capture_start HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 重要将捕获极性切换为下降沿为下一次捕获做准备 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); g_is_captured 1; // 标记已捕获到上升沿 } else if(g_is_captured 1) // 第二次捕获即下降沿 { // 读取下降沿时刻的计数器值 g_capture_end HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 计算高电平期间计数器的差值 uint32_t pulse_width_ticks; if(g_capture_end g_capture_start) { pulse_width_ticks g_capture_end - g_capture_start; } else { // 处理计数器溢出的情况ARR65535 如果上升沿在65000下降沿在1000说明发生了溢出 pulse_width_ticks (65535 - g_capture_start) g_capture_end; } // 将计数值转换为时间微秒和距离毫米 // 因为我们设置PSC71时钟1MHz1个tick1us uint32_t time_us pulse_width_ticks; // 直接等于微秒数 g_distance_mm (time_us * 0.034f) / 2.0f * 10.0f; // 距离毫米 (时间 * 声速) / 2 * 10 (cm转mm) // 一次测量完成重置状态并将极性改回上升沿等待下一次触发 g_is_captured 2; // 可以设置为2表示完成或者0重新开始 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); // 可选清除计数器为下一次测量做准备 __HAL_TIM_SET_COUNTER(htim, 0); } } }5.4 定时器更新中断处理除了捕获中断定时器溢出Update中断也需要处理以防在测量过程中计数器归零导致计算错误。在同一个区域添加/** * brief 定时器溢出更新中断回调函数 * param htim : 定时器句柄 * retval None */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim-Instance TIM3) { // 如果正在等待下降沿的过程中发生了溢出 if(g_is_captured 1) { // 可以在这里设置一个标志表示本次测量超时距离太远或没收到回波 // 例如g_measurement_timeout 1; // 然后重置状态 __HAL_TIM_SET_CAPTUREPOLARITY(htim3, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); g_is_captured 0; __HAL_TIM_SET_COUNTER(htim3, 0); } } }5.5 初始化与启动最后在main.c的main()函数中用户代码区域启动定时器和输入捕获中断/* USER CODE BEGIN 2 */ // 启动定时器3的基础计数功能用于溢出中断 HAL_TIM_Base_Start_IT(htim3); // 启动定时器3的通道1输入捕获功能并开启捕获中断 HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); /* USER CODE END 2 */6. 调试技巧与常见问题排查代码写完了下载到板子上可能一次成功更可能遇到各种问题。下面是我在调试中总结的一些经验和常见问题的排查思路。6.1 基础检查清单电源与连接确保HC-SR04的VCC接的是5VGND共地。用万用表测量电压是否稳定。Echo引脚是否接到了正确的、支持输入捕获的IO上。CubeMX配置复查定时器时钟是否使能在“Clock Configuration”中确认TIM3的时钟源是亮的。PSC和ARR值是否计算正确用示波器或逻辑分析仪看Trig引脚发出的脉冲宽度是否约为10us用HAL_Delay(1)实际是1ms足够但如果你想精确10us需要用定时器或NOP指令实现。Echo引脚配置的模式是否正确是“Input Capture”而不是普通的“GPIO Input”。中断优先级如果系统中还有其他中断如串口需要合理配置NVIC优先级。超声波测距对实时性要求高可以给TIM3中断设置一个较高的抢占优先级。6.2 典型问题与解决方案问题现象可能原因排查与解决思路距离值始终为0或极小1. 未收到回波。2. 捕获中断未进入。3. 计算逻辑错误。1.检查硬件确保模块前方有障碍物且距离在2cm-4m内。用示波器或逻辑分析仪探头同时测量Trig和Echo引脚看触发后是否有Echo高电平脉冲。这是最直接的诊断方法。2.检查中断在HAL_TIM_IC_CaptureCallback函数入口设置断点或者翻转一个LED灯看能否进入。如果不能检查CubeMX中TIM3全局中断是否使能代码中是否调用了HAL_TIM_IC_Start_IT。3.检查变量查看g_capture_start和g_capture_end的值是否有变化。如果没变说明没捕获到边沿。距离值固定为一个很大的数如65535计数器溢出处理不当。在HAL_TIM_IC_CaptureCallback中如果g_capture_end g_capture_start说明在两次捕获间计数器溢出了。必须按照“差值 (ARR - start) end”的公式计算。确保你的代码包含了溢出处理分支。距离值跳动剧烈±50mm以上1. 环境声波干扰。2. 电源噪声。3. 软件滤波不足。1.硬件滤波在HC-SR04的VCC和GND引脚就近并联一个10uF和0.1uF的电容。在Echo信号线上串联一个100欧姆电阻并并联一个几十pF的电容到地RC低通滤波。2.软件滤波不要只采样一次。在主循环中连续采样5-10次然后去掉最大最小值取平均或者采用中值滤波。这是提升稳定性的最有效手段。3.调整定时器滤波器在CubeMX中适当增加TIM3通道的“ICFilter”值如设为4。测量频率很低反应慢主循环中HAL_Delay(60)等待时间过长。HC-SR04手册要求两次测量间隔至少60ms这是为了防止上次回波干扰。如果你需要更高频率可以尝试缩短这个间隔到40ms甚至更短但需要观察测量结果是否开始出错。更高级的做法是使用定时器定时触发并用DMA搬运数据实现完全非阻塞的连续测距。短距离2cm测量不准或失效HC-SR04有盲区通常2cm-3cm。这是模块物理限制。如果项目需要测很近的距离需要换用其他原理的传感器如TOF激光测距或红外测距。6.3 进阶优化建议使用DMA进行连续测量对于需要极高数据刷新率的应用如高速避障可以将定时器配置为PWM输出模式产生Trig脉冲同时用另一个通道的输入捕获配合DMA自动将捕获值搬运到数组。CPU只需处理数组中的数据即可几乎零开销。多路超声波传感器如果需要驱动多个HC-SR04不建议复制多套定时器。可以共用Trig触发线所有模块同时触发但Echo线分别接到不同的定时器通道。关键在于处理可能同时到来的多个中断以及防止通道间串扰。更简单的方案是使用一个模拟开关如CD4051轮流选通不同模块的Echo信号到一个定时器通道。温度补偿声速受温度影响。公式V 331.4 0.6 * TT为摄氏温度。如果对精度要求高如±1mm可以加入温度传感器如DS18B20进行实时补偿。驱动HC-SR04本身是一个简单的任务但把它做稳、做准却涉及硬件连接、时钟配置、定时器原理、中断处理、软件滤波等一系列嵌入式开发的核心知识点。通过这个项目你真正掌握的不仅仅是一个模块的用法而是一套解决类似“时间测量”或“脉冲信号处理”问题的通用方法论。下次遇到需要测频率、占空比或者编码器信号时你会惊喜地发现思路是完全相通的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价