STM32 HAL库驱动HC-SR04超声波模块:CubeMX配置与精准测距实战
2026/8/8 23:46:33 网站建设 项目流程

1. 项目概述与核心价值

最近在做一个智能小车避障的项目,核心的测距传感器选用了经典的HC-SR04超声波模块。这个模块成本低、原理简单,但要想在STM32上稳定、精准地驱动它,还是有不少细节需要琢磨。直接操作寄存器固然可以,但对于快速原型开发或者项目维护来说,使用ST官方力推的HAL库配合CubeMX图形化配置工具,无疑是更高效、更规范的选择。这次我就来详细拆解一下,如何从零开始,用CubeMX配置一个STM32工程,并编写代码驱动HC-SR04,最终实现可靠的距离测量。整个过程不仅适用于避障小车,对于任何需要非接触式测距的场景,比如液位检测、物体存在性判断等都很有参考价值。

HC-SR04的工作原理大家应该不陌生:触发引脚(Trig)给一个至少10us的高电平脉冲,模块会自动发射8个40kHz的超声波脉冲,然后回声引脚(Echo)会输出一个高电平,其持续时间与超声波往返时间成正比。所以,核心任务就变成了STM32如何精准产生这个触发信号,以及如何高精度地测量Echo引脚高电平的持续时间。我们将使用通用定时器(TIM)的PWM输出模式来产生触发脉冲,用输入捕获功能来测量高电平脉宽,这是最经典且稳定的方案。下面,我就结合CubeMX的配置和HAL库的编程,把每一步的要点和踩过的坑都分享出来。

2. 硬件连接与CubeMX工程创建

2.1 硬件接口定义

首先明确硬件连接。HC-SR04模块通常有四个引脚:VCC、Trig、Echo、GND。

  • VCC: 接3.3V或5V。虽然STM32的IO是3.3V电平,但HC-SR04的Echo引脚输出高电平是5V。这是一个关键点:不能直接将5V的Echo引脚接到STM32的GPIO上,否则可能损坏芯片。稳妥的做法是使用一个简单的电平转换电路,比如两个电阻分压(例如1kΩ和2kΩ),或者使用电平转换芯片。在要求不严、距离较短的测试中,也有人直接连接,但长期使用有风险,不推荐。
  • GND: 共地。
  • Trig: 触发引脚,连接STM32的一个GPIO,配置为推挽输出模式。
  • Echo: 回声引脚,连接STM32的一个GPIO,配置为浮空输入或上拉输入模式,用于捕获高电平。

在我的设计中,我选择:

  • Trig-> PA8 (后续配置为TIM1的CH1输出PWM)
  • Echo-> PA0 (后续配置为TIM2的CH1输入捕获) 使用不同的定时器是为了避免资源冲突和逻辑耦合。

2.2 CubeMX基础工程配置

打开CubeMX,新建工程,选择你的STM32型号(例如STM32F103C8T6)。

  1. 系统核心(SYS): 在SYS选项卡中,将Debug设置为Serial Wire。如果你的调试器是ST-Link,这是必须的,否则可能无法下载和调试。
  2. 时钟(RCC): 在RCC选项卡中,将High Speed Clock (HSE)设置为Crystal/Ceramic Resonator。这样我们才能使用外部高速晶振,为定时器提供精准的时钟源。
  3. 时钟树配置: 点击Clock Configuration标签。这是CubeMX的核心之一。我们的目标是为定时器提供较高的计数频率,以提高测量精度。以STM32F103为例,通常先设置HSE为8MHz,然后通过PLL倍频到72MHz作为系统时钟(SYSCLK)。接着,将APB1定时器时钟和APB2定时器时钟都设置为72MHz(注意:APB1总线时钟最高36MHz,但连接到定时器的时钟经过了一个倍频器,如果APB1预分频系数不为1,则定时器时钟是APB1时钟的2倍)。确保你使用的定时器(如TIM1在APB2, TIM2在APB1)的时钟源是72MHz。清晰的时钟树是精准定时的基础。
  4. 项目管理: 在Project Manager选项卡中,设置项目名称、路径,将Toolchain / IDE选为MDK-ARM(如果你用Keil)。关键一步是在Code Generator部分,勾选Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral。这会将每个外设的初始化代码生成独立的文件,让工程结构更清晰,方便管理。

3. 定时器配置:PWM触发与输入捕获

这是本次配置的核心,我们分两个定时器进行。

3.1 TIM1配置:产生触发脉冲(PWM模式)

我们的目标是用TIM1的一个通道输出一个精确的、宽度大于10us的单次脉冲。

  1. 引脚分配: 在芯片引脚图上找到PA8,其复用功能是TIM1_CH1。点击PA8引脚,选择TIM1_CH1
  2. 定时器参数设置: 左侧切换到Timers->TIM1
    • Clock Source: 选择Internal Clock
    • Parameter Settings
      • Prescaler(预分频器): 设置为71。定时器时钟是72MHz,分频后计数频率为 72MHz / (71+1) = 1MHz。即计数器每1us递增1。
      • Counter ModeUp(向上计数)。
      • Counter Period: 设置为19。我们希望产生一个20us的脉冲(满足HC-SR04的10us最小要求并留有余量)。因为计数器从0开始计数到19,总共是20个时钟周期,即20us。
      • auto-reload preloadEnable。使能自动重装载预装载寄存器,可以避免更新参数时的毛刺。
    • PWM生成通道配置: 切换到Channel 1
      • Mode: 选择PWM Generation CH1
      • Pulse: 设置为18。这个参数决定了PWM高电平的宽度。我们希望高电平宽度是20us,但这里设置的是比较寄存器的值。在向上计数模式下,当计数器值小于Pulse时,输出有效电平(可配置)。我们设置Pulse为18,结合下面的CH Polarity配置,可以输出一个20us的高脉冲。
      • CH Polarity: 设置为High。这意味着PWM的有效电平(Active Level)为高。
      • 关键理解: 计数器从0开始向上计数。当计数值 <Pulse(18)时,输出有效电平(高);当计数值 >=Pulse(18)时,输出无效电平(低)。计数器计数到Period(19)后溢出,产生更新事件,并清零重新开始。因此,会输出一个从计数开始到18为止的高电平(持续19个计数周期?这里需要校准)。实际上,为了得到一个精确的20us高脉冲,我们需要仔细计算。更直观的做法是:设置Period为19(20个计数周期),设置Pulse为19。这样,在计数器从0计数到18期间(19个周期),输出高电平;当计数器等于19时,仅持续一个周期就溢出,通常这个周期输出低。但这样高电平是19us。为了得到20us,我们可以设置Period为20-1=19,Pulse为20-1=19?不对。让我们重新思考:我们需要一个单次脉冲,不是连续的PWM。更好的方法是利用One Pulse Mode(单脉冲模式)。
  3. 启用单脉冲模式: 回到Parameter Settings,将One Pulse Mode设置为Enable。在该模式下,定时器在收到触发事件后,输出一个指定宽度的脉冲,然后停止。这完美符合我们的需求。
    • 重新调整参数:Prescaler=71Period=19(20个计数周期,20us)。
    • 此时Pulse值就代表高电平的宽度。我们想要20us高电平,那么Pulse应该设置为19?在单脉冲模式下,Pulse是捕获比较寄存器的值。当计数器值小于Pulse时,输出有效电平。如果我们想要计数器从0到19计数期间都输出高电平(20us),那么Pulse应该设置为19?但计数器等于19时,下一个周期就溢出了。实际上,在单脉冲模式下,通常Pulse的值决定了高电平的持续时间。设置Pulse = 19Period = 19, 则高电平时间为 (Pulse+1) * 时钟周期 = 20 * 1us = 20us。可以这样理解。
    • 经过实测和查阅资料,一个可靠的配置是:Prescaler=71Period=199Pulse=19。这样,定时器时钟1MHz,周期200us,高电平宽度20us。我们通过软件启动一次定时器,它输出一个20us高脉冲后停止。这个Period值只要大于Pulse即可,设置大一些没关系,因为输出脉冲宽度只由Pulse决定。但为了节能和快速响应,可以设小一点,比如Period = 39(40us周期),Pulse = 19(20us高电平)。
  4. 生成触发信号: 配置好后,在代码中,我们只需要在需要测距时,启动一次TIM1即可。
    // 启动一次单脉冲输出 HAL_TIM_OnePulse_Start(&htim1, TIM_CHANNEL_1);
    这句代码会启动定时器,输出一个设定好宽度的脉冲后自动停止。

3.2 TIM2配置:测量回波脉宽(输入捕获模式)

Echo引脚的高电平持续时间就是超声波往返时间。我们用TIM2的输入捕获功能来测量它。

  1. 引脚分配: 找到PA0,其复用功能是TIM2_CH1。点击PA0,选择TIM2_CH1
  2. 定时器参数设置: 左侧切换到Timers->TIM2
    • Clock Source: 选择Internal Clock
    • Parameter Settings
      • Prescaler: 设置为71。同样得到1MHz的计数频率,1us的计数分辨率。对于测量几毫秒的脉宽(对应几米的距离),这个分辨率足够了。如果想更高精度,可以减小分频值,比如设置为35,得到2MHz计数频率,0.5us分辨率。
      • Counter ModeUp
      • Counter Period: 设置为最大值65535(因为TIM2是16位定时器)。超声波在空气中传播速度约340m/s,测量1米的往返时间约为5.88ms,即5880us。1MHz下计数值约为5880,远小于65535,所以溢出风险很低。但如果测量距离超过5米,往返时间可能超过29ms,计数值会超过30000,仍然小于65535。为了保险,我们可以开启定时器溢出中断,或者在捕获回调函数中处理溢出情况。
      • auto-reload preloadEnable
  3. 输入捕获通道配置: 切换到Channel 1
    • Mode: 选择Input Capture direct mode
    • IC SelectionDirect。表示捕获直接连接到通道的输入。
    • IC Polarity: 选择Rising Edge。我们首先在上升沿(Echo变高)捕获一次计数器值。
    • IC Selection: 保持Direct
    • IC PrescalerNo division。每个有效边沿都捕获。
    • IC Filter: 可以设置为一个较小的值,如4,用于滤除可能的高频毛刺。HC-SR04输出信号比较干净,可以不滤波。
  4. 开启中断: 在NVIC Settings选项卡中,勾选TIM2 global interrupt。这样在捕获事件发生时,才能进入中断服务函数进行处理。

注意: 这里有一个非常重要的细节。我们只设置了上升沿捕获。为了测量高电平的宽度,我们需要在上升沿记录一个时间戳t1,然后在下降沿记录另一个时间戳t2。宽度就是t2 - t1。如何捕获下降沿呢?我们可以在上升沿捕获的中断里,动态改变捕获极性为下降沿。HAL库提供了相应的函数。

4. 代码编写与驱动逻辑实现

CubeMX生成代码后,我们打开工程,主要在main.cstm32f1xx_it.c(中断文件)中添加业务逻辑。

4.1 宏定义与全局变量

main.c的顶部用户变量区添加:

/* Private variables ---------------------------------------------------------*/ // 超声波测量相关变量 volatile uint32_t g_ultrasound_capture_start = 0; // 上升沿捕获值 volatile uint32_t g_ultrasound_capture_width = 0; // 高电平脉宽(计数值) volatile uint8_t g_ultrasound_measure_done = 0; // 测量完成标志 volatile uint8_t g_ultrasound_overflow_count = 0; // 定时器溢出次数(用于长距离测量)

volatile关键字告诉编译器这些变量可能被中断修改,避免优化错误。

4.2 输入捕获中断回调函数

这是核心逻辑所在。HAL库采用了回调函数机制。我们需要重写输入捕获的回调函数HAL_TIM_IC_CaptureCallback

main.c中,找到/* USER CODE BEGIN 4 *//* USER CODE END 4 */之间,或者新建一个用户文件,添加以下函数:

/** * @brief 输入捕获中断回调函数 * @param htim: 定时器句柄 * @retval None */ void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { // 判断是否是TIM2的通道1发生捕获中断 if (htim->Instance == TIM2) { if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { // 第一次捕获(上升沿) if (g_ultrasound_capture_start == 0) { // 记录上升沿时刻的计数器值 g_ultrasound_capture_start = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 改变捕获极性为下降沿 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); // 清零溢出计数,开始计算可能的中断溢出 g_ultrasound_overflow_count = 0; } // 第二次捕获(下降沿) else { // 记录下降沿时刻的计数器值 uint32_t capture_end = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 计算高电平期间的计数值(考虑溢出) // 公式:脉宽 = (溢出次数 * 定时器周期) + (下降沿值 - 上升沿值) // 由于我们Period设置很大(65535),且测量距离有限,通常不会溢出,这里简化处理 g_ultrasound_capture_width = capture_end - g_ultrasound_capture_start; // 标志测量完成 g_ultrasound_measure_done = 1; // 停止捕获,并重置状态,准备下一次测量 HAL_TIM_IC_Stop_IT(htim, TIM_CHANNEL_1); // 重置捕获极性为上升沿 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); // 清零起始值 g_ultrasound_capture_start = 0; } } } }

同时,我们还需要处理定时器溢出中断,以应对长距离测量(计数值超过65535)。重写定时器溢出回调函数:

/** * @brief 定时器溢出中断回调函数 * @param htim: 定时器句柄 * @retval None */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { // 如果已经捕获到上升沿(即正在测量高电平期间),则溢出计数加1 if (g_ultrasound_capture_start != 0) { g_ultrasound_overflow_count++; // 如果溢出次数太多,可以认为超时,强制结束测量(可选) if (g_ultrasound_overflow_count > 10) // 例如超过10次溢出,约655ms { g_ultrasound_measure_done = 2; // 超时标志 HAL_TIM_IC_Stop_IT(&htim2, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); g_ultrasound_capture_start = 0; } } } }

4.3 超声波测距函数

我们封装一个函数,用于触发一次测量并等待结果。

/** * @brief 执行一次超声波测距 * @param p_distance_cm: 用于存储距离值(单位:厘米)的指针 * @retval 状态:0-成功,1-超时,2-测量错误(脉宽太短) */ uint8_t Ultrasound_Measure(uint32_t *p_distance_cm) { // 1. 清零标志和变量 g_ultrasound_measure_done = 0; g_ultrasound_capture_width = 0; g_ultrasound_capture_start = 0; g_ultrasound_overflow_count = 0; // 2. 启动输入捕获(上升沿) HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); // 3. 发送触发脉冲(启动TIM1的单脉冲输出) HAL_TIM_OnePulse_Start(&htim1, TIM_CHANNEL_1); // 4. 等待测量完成或超时 uint32_t timeout = HAL_GetTick() + 100; // 设置100ms超时 while (g_ultrasound_measure_done == 0) { if (HAL_GetTick() > timeout) { HAL_TIM_IC_Stop_IT(&htim2, TIM_CHANNEL_1); // 停止捕获 return 1; // 超时 } } // 5. 判断测量结果 if (g_ultrasound_measure_done == 2) { return 1; // 超时(在溢出回调中标记) } // 6. 计算距离 // 高电平时间 t_us = g_ultrasound_capture_width * (1 / 计数频率) // 计数频率 = 72MHz / (Prescaler+1) = 1MHz, 所以每个计数代表1us。 uint32_t time_us = g_ultrasound_capture_width; // 单位:微秒 // 超声波在空气中速度约340m/s,即0.034cm/us。距离 = (时间 * 速度) / 2 // 距离(厘米) = (time_us * 0.034) / 2 = time_us * 0.017 // 为了免去浮点数运算,可以近似为:距离(厘米) = time_us / 58 // 因为 1 / 58 ≈ 0.01724,误差很小。或者使用更精确的:距离 = time_us * 17 / 1000 *p_distance_cm = time_us * 17 / 1000; // 使用整数运算 // 7. 有效性检查(可选):HC-SR04最小测量距离约2cm,对应脉宽约116us。 // 最大测量距离约4m,对应脉宽约23200us。可以在此范围内判断。 if (*p_distance_cm < 2 || *p_distance_cm > 400) { return 2; // 测量结果超出有效范围 } return 0; // 成功 }

4.4 主循环逻辑

main函数的while (1)循环中,我们可以周期性地进行测距。

while (1) { uint32_t distance_cm = 0; uint8_t status = Ultrasound_Measure(&distance_cm); if (status == 0) { // 测量成功,使用distance_cm printf("Distance: %lu cm\r\n", distance_cm); } else if (status == 1) { printf("Measurement timeout!\r\n"); } else { printf("Measurement error!\r\n"); } HAL_Delay(100); // 每次测量间隔至少60ms,给模块留出处理时间 }

记得在main函数初始化部分,启动TIM2的基计数(但不启动捕获):

/* USER CODE BEGIN 2 */ // 启动TIM2的基计数器,用于输入捕获的时基 HAL_TIM_Base_Start(&htim2); /* USER CODE END 2 */

5. 调试技巧与常见问题排查

即使按照上述步骤配置,在实际调试中也可能遇到各种问题。下面分享一些我踩过的坑和解决方法。

5.1 完全没有捕获到信号

  • 现象: 程序一直卡在Ultrasound_Measure函数的等待循环中,超时返回。
  • 排查步骤
    1. 检查硬件连接: 确保VCC、GND连接正确且牢固。用万用表测量Echo引脚,在触发后是否有电压变化(从0V跳变到5V左右)。如果没有,可能是模块损坏或触发信号问题。
    2. 检查触发信号: 使用示波器或逻辑分析仪查看Trig引脚(PA8)是否有20us左右的高电平脉冲。如果没有,检查TIM1的配置和启动代码HAL_TIM_OnePulse_Start是否执行。
    3. 检查Echo信号: 用示波器看Echo引脚。正常触发后,应该会先有一段低电平(模块内部处理时间),然后产生一个高电平脉冲。如果根本没有高电平,可能是模块前方有障碍物太近(<2cm)或太远(>4m),或者模块本身有问题。
    4. 检查输入捕获配置: 确认TIM2的通道1正确映射到PA0,且配置为输入捕获模式。检查NVIC中断是否使能。
    5. 检查代码逻辑: 确保在发送触发脉冲之前,已经启动了输入捕获中断HAL_TIM_IC_Start_IT。顺序很重要。

5.2 测量值不稳定或误差大

  • 现象: 距离值跳动很大,或者与实际距离有固定偏差。
  • 排查与解决
    1. 电源噪声: HC-SR04对电源比较敏感。确保电源能提供足够的电流(峰值可达15mA),并在VCC和GND之间并联一个10uF和0.1uF的电容进行滤波。
    2. 环境干扰: 超声波易受温度和气流影响。公式中的声速(340m/s)是在15°C下的值。温度变化时,声速会变(v = 331.4 + 0.6 * T, T为摄氏度)。对于精度要求高的场合,可以加入温度传感器进行补偿。
    3. 测量原理误差: 我们用的公式距离 = 时间 * 声速 / 2。声速本身随温度变化,同时模块的发射和接收探头有一定距离(约1-2cm),对于极近的物体,这会带来三角测量误差。一般模块的标称盲区是2cm,就是这个原因。
    4. 定时器分辨率: 我们设置的分频是71,得到1MHz计数频率,理论分辨率是1us,对应距离分辨率约0.017cm,这已经足够。如果误差是固定倍数,检查定时器时钟配置是否正确,PrescalerPeriod计算是否有误。
    5. 多次测量取平均: 在代码中,连续进行5-10次测量,去掉最大最小值后取平均,可以显著平滑数据,减少随机误差。
      #define MEASURE_TIMES 7 uint32_t sum = 0; uint32_t buf[MEASURE_TIMES]; for(int i=0; i<MEASURE_TIMES; i++) { Ultrasound_Measure(&buf[i]); HAL_Delay(30); } // 简单的排序和去极值后求平均 // ... (排序算法) for(int i=1; i<MEASURE_TIMES-1; i++) { // 去掉一个最高一个最低 sum += buf[i]; } distance_cm = sum / (MEASURE_TIMES-2);

5.3 输入捕获中断只进入一次

  • 现象: 第一次测量正常,后续测量无法进入捕获回调函数。
  • 原因与解决: 这通常是因为在回调函数中停止了定时器通道HAL_TIM_IC_Stop_IT,但下一次测量前没有重新启动它。仔细检查Ultrasound_Measure函数,在每次测量开始时,都调用了HAL_TIM_IC_Start_IT。同时,确保在测量结束或超时处理中,没有错误地关闭了定时器基计数器HAL_TIM_Base_Stop。基计数器在整个程序运行期间应该只启动一次。

5.4 长距离测量不准或溢出

  • 现象: 测量距离超过1-2米后,数据混乱。
  • 解决: 我们的代码中简化处理了溢出。如果测量距离可能超过4-5米(对应计数值>29000),就需要完善溢出处理逻辑。在HAL_TIM_IC_CaptureCallback中计算脉宽时,公式应为:
    g_ultrasound_capture_width = (g_ultrasound_overflow_count * (htim->Instance->ARR + 1)) + (capture_end - g_ultrasound_capture_start);
    其中ARR是自动重装载值(即Period)。同时,要确保g_ultrasound_overflow_count在每次测量开始时被清零,并在定时器溢出回调函数HAL_TIM_PeriodElapsedCallback中正确递增。

5.5 使用DMA提升效率(进阶)

上述方法需要CPU介入中断处理。如果系统繁忙,可能会丢失捕获边沿。对于更高要求,可以使用定时器的PWM输入模式(一个通道测量周期和占空比)或者使用两个通道(一个上升沿,一个下降沿)配合DMA,将捕获值直接存储到内存,完全无需中断。但这需要更复杂的配置,且HC-SR04的回波是单次脉冲而非周期信号,PWM输入模式可能不直接适用。一个可行的方案是使用一个定时器通道的输入捕获,并开启DMA到循环缓冲区,在下降沿触发DMA传输完成中断,再去缓冲区计算脉冲宽度。这属于更高级的用法,可以极大减轻CPU负担。

6. 项目优化与扩展思路

基础功能实现后,可以考虑以下优化,让项目更健壮、更实用。

6.1 添加温度补偿

如前所述,声速受温度影响大。可以连接一个DS18B20之类的温度传感器,实时获取环境温度,修正声速公式。

float temperature = Read_Temperature(); // 读取温度,单位摄氏度 float sound_speed_cm_per_us = (331.4f + 0.6f * temperature) / 10000.0f; // 转换为 cm/us float distance_cm = (time_us * sound_speed_cm_per_us) / 2.0f;

6.2 多传感器管理

在自动导航小车中,往往需要多个超声波传感器(比如左、前、右)。可以为每个传感器分配一对定时器通道(一个触发PWM,一个输入捕获),或者使用一个定时器的多个通道。需要注意的是,HC-SR04模块之间如果同时触发,声波可能会互相干扰。通常采用分时复用的策略,依次触发各个模块,并留出足够的间隔时间(>60ms)。

6.3 与上层应用整合

将超声波驱动封装成一个独立的.c/.h文件,提供简洁的API,如Ultrasound_Init(),Ultrasound_GetDistanceCM()。这样在主业务逻辑中,只需调用接口即可,实现驱动与应用的解耦。

6.4 探索其他测距方案

HC-SR04性价比高,但也有一些缺点:测量周期长(>60ms)、易受环境干扰、角度大导致精度一般。对于更高速、更精准的应用,可以考虑:

  • ToF传感器: 如VL53L0X,使用激光飞行时间原理,精度高、响应快、抗干扰强,但价格稍贵,测量距离较短(通常2米以内)。
  • 毫米波雷达: 如AWR1642,可以测距、测速,穿透性强,不受光线和天气影响,但成本高,开发复杂。 选择哪种方案,需要根据项目的具体需求(成本、精度、速度、环境)来权衡。

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

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

立即咨询