目的:我手上有STM32F407ZET6开发板和HC-SR04的超声波模块,串口线,电脑。现在超声波的Trig接PB6、 Echo接PE6。超声波测得距离通过串口一在电脑端串口调试助手上显示出来,用hal库和cubemx来实现。

第一步:STM32CubeMX配置(图形化生成基础代码)

这是整个开发的基础,按照你的引脚定义来配置。

  1. 新建工程与芯片选择:打开CubeMX,新建工程,搜索并选择你的芯片 STM32F407ZET6

  2. 配置时钟源 (RCC)

    • 在 System Core -> RCC 中,将 High Speed Clock (HSE) 设置为 Crystal/Ceramic Resonator。这是为了使用你开发板上的外部晶振,以获得精确的时钟。

  3. 配置时钟树 (Clock Configuration)

    • 在 Clock Configuration 标签页,将 HCLK 设置为 168 MHz(这是STM32F407的最高主频)。CubeMX会自动计算分频系数,确保配置无误即可。

  4. 配置串口1 (USART1)

    • 在 Connectivity -> USART1 中,将 Mode 设置为 Asynchronous(异步通信)。

    • 在下方 Parameter Settings 中,通常保持默认的 9600 Bits/s、8位数据、无校验、1位停止位。你电脑端的串口助手需要和这个设置保持一致。

    • 此时,CubeMX会自动将USART1的TX和RX引脚分配到开发板上对应的引脚(通常是PA9和PA10)。

  5. 配置超声波Trig引脚 (PB6)

    • 在 Pinout 视图或 System Core -> GPIO 中,找到 PB6 引脚。

    • 将其设置为 GPIO_Output 模式。这个引脚将用于给超声波模块发送触发信号。

  6. 配置超声波Echo引脚 (PE6)(核心!)

    • Echo引脚需要测量高电平脉冲宽度,这是通过定时器的输入捕获功能实现的。我们选择任意一个空闲定时器,配置如下:TIM9

    • 在 Timers -> TIM9 中,将 Channel1 设置为 Input Capture direct mode。  在 Timers -> TIM9 中,将 Channel2 设置为 Input Capture direct mode

    • 在 Parameter Settings 中,进行最关键的三步配置:

      • 预分频器 (Prescaler):我们希望定时器每计数一次代表1微秒。系统时钟是168MHz,APB2定时器时钟通常也是168MHz。所以预分频器值需要设置为 168-1 = 167,这样定时器计数频率就变成了 168MHz / 168 = 1MHz,即每1微秒计数器加1。

      • 计数器周期 (Counter Period):设置为最大值 65535。这决定了我们最大能测量的时间,在1MHz计数频率下,约为65.5毫秒,足够覆盖HC-SR04的有效量程。

      • 触发极性 (Polarity Selection):先保持默认的 Rising Edge(上升沿触发),我们会在代码中动态切换它。

    • 在 NVIC Settings 中,务必勾选 TIM9 global interrupt 的中断使能,否则捕获不到信号。

  7. 生成代码

    • 在 Project Manager 中,设置好工程名、路径和IDE(MDK-ARM)。

    • 点击 GENERATE CODE,生成初始工程。

💻 第二步:编写应用程序代码(在Keil中实现逻辑)

生成代码后,用Keil打开工程,我们需要添加自己的逻辑。

  1. 添加printf支持(方便调试)

    • 在 usart.c 文件的 /* USER CODE BEGIN 0 */ 和 /* USER CODE END 0 */ 之间,添加fputc函数的重定向代码:

      c

      /* USER CODE BEGIN 0 */
      #include <stdio.h>
      int fputc(int ch, FILE *f)
      {
        HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
        return ch;
      }
      /* USER CODE END 0 */
    • 别忘了在 usart.h 中包含头文件 #include <stdio.h>

  2. 编写超声波驱动逻辑
    在 main.c 文件的 /* USER CODE BEGIN 0 */ 区域,添加以下全局变量和函数定义:

    c

    /* USER CODE BEGIN 0 */
    // 用于存储捕获时间和状态的全局变量
    static uint32_t ic_rising_tick = 0;
    static uint32_t ic_falling_tick = 0;
    static uint8_t ic_capture_complete = 0;
    static uint8_t ic_capture_state = 0; // 0: 等待上升沿, 1: 等待下降沿
    
    // 微秒级延时函数 (使用SysTick实现)
    void delay_us(uint32_t us)
    {
        uint32_t tick = HAL_GetTick();
        uint32_t wait_tick = us / 1000; // 粗略转换,如果需要精确us延时,建议使用定时器
        while((HAL_GetTick() - tick) < wait_tick);
    }
    
    // 发送Trig触发信号
    void HCSR04_Trigger(void)
    {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
        delay_us(20);  // 触发脉冲 >10us
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
    }
    
    // 定时器输入捕获中断回调函数 (由HAL库自动调用)
    void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
    {
        if (htim->Instance == TIM9)
        {
            if (ic_capture_state == 0) // 捕获到上升沿
            {
                ic_rising_tick = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
                // 将捕获极性改为下降沿,准备捕获脉冲结束
                __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
                ic_capture_state = 1;
            }
            else if (ic_capture_state == 1) // 捕获到下降沿
            {
                ic_falling_tick = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
                // 将捕获极性改回上升沿,以备下次触发
                __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
                ic_capture_state = 0;
                ic_capture_complete = 1; // 标记一次测量完成
            }
        }
    }
    /* USER CODE END 0 */
  3. 在主函数中实现测距循环
    在 main 函数的 /* USER CODE BEGIN 2 */ 和 /* USER CODE BEGIN WHILE */ 部分添加代码:

    c

    int main(void)
    {
        HAL_Init();
        SystemClock_Config();
        MX_GPIO_Init();
        MX_USART1_UART_Init();
        MX_TIM9_Init();
    
        /* USER CODE BEGIN 2 */
        printf("HC-SR04 Ultrasonic Ranging Test Started...\r\n");
        // 启动定时器的输入捕获中断
        HAL_TIM_IC_Start_IT(&htim9, TIM_CHANNEL_1);
        /* USER CODE END 2 */
    
        while (1)
        {
            /* USER CODE BEGIN WHILE */
            // 1. 发送触发信号
            HCSR04_Trigger();
    
            // 2. 等待捕获完成,设置超时防止死锁 (5ms超时)
            uint32_t timeout = HAL_GetTick();
            while (ic_capture_complete == 0)
            {
                if ((HAL_GetTick() - timeout) > 5) {
                    break; // 超时,跳出等待
                }
            }
    
            // 3. 如果捕获完成,计算并打印距离
            if (ic_capture_complete)
            {
                // 计算高电平持续时间,单位微秒 (因为定时器配置为1MHz)
                uint32_t pulse_width_us = 0;
                if (ic_falling_tick >= ic_rising_tick) {
                    pulse_width_us = ic_falling_tick - ic_rising_tick;
                } else {
                    // 处理定时器溢出情况(如果测量距离很长,这里需要更复杂的逻辑)
                    // 本例中,F407的主频和计数器周期足以覆盖近距离,暂不处理溢出。
                    pulse_width_us = ic_falling_tick + (65535 - ic_rising_tick);
                }
    
                // 计算距离,公式:距离(cm) = 脉冲时间(us) * 0.034 / 2
                float distance = pulse_width_us * 0.017; // 0.017 = 340m/s / 2 / 1000000 * 100
                printf("Distance: %.2f cm\r\n", distance);
    
                // 清除完成标志,准备下一次测量
                ic_capture_complete = 0;
            }
            else
            {
                printf("Measurement timeout.\r\n");
                ic_capture_complete = 0; // 清除标志,准备下次
                ic_capture_state = 0; // 重置状态机
                // 确保捕获极性是上升沿
                __HAL_TIM_SET_CAPTUREPOLARITY(&htim9, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
            }
    
            HAL_Delay(200); // 两次测量间隔建议大于60ms[citation:2][citation:6]
            /* USER CODE END WHILE */
        }
    }

⚠️ 第三步:接线与测试

  1. 硬件接线

    • HC-SR04的 VCC 接开发板的 3V3

    • HC-SR04的 GND 接开发板的 GND

    • HC-SR04的 Trig 接开发板的 PB6

    • HC-SR04的 Echo 接开发板的 PE6

    • 特别注意:HC-SR04是5V逻辑,而STM32是3.3V逻辑。虽然STM32的IO口通常容忍5V,但最稳妥的做法是在Echo引脚上加一个电阻分压(例如1k和2k电阻)将5V降到3.3V左右。

  2. 编译下载:编译代码,下载到开发板。

  3. 打开串口助手:设置波特率为9600,打开对应串口。你应该能看到不断打印出的距离数据

四、总结问题

图片1:串口一直打印"Measurement timeout."

text

Measurement timeout.
Measurement timeout.
Measurement timeout.
...

当时诊断:程序在跑,但Echo没信号,怀疑硬件或配置问题。


图片2:TIM9 Prescaler: 0

text

TIM9 Prescaler: 0
TIM9 Period: 65535

当时诊断:预分频器没生效!计数频率168MHz(太快),计数器很快溢出,导致捕获失败。

解决:在tim.c里手动加htim9.Instance->PSC = 167;,Prescaler终于变成167。


图片3:Echo一直打印0

text

Echo: 0
Echo: 0
Echo: 0
...

当时诊断:硬件信号没到PE6?但标准库代码能工作,证明硬件没问题 → 问题锁定在HAL库配置。


图片4:TIM9_CH1配置截图(第一次检查引脚)

text

TIM9_CH1
PE6
...

当时判断:引脚配置正确,继续排查中断。


图片5:关键转折点——TIM9_CH2配置截图

text

TIM9_CH2
PE6
...

真相大白:原来PE6配的是 TIM9_CH2,但代码里一直用 TIM9_CH1


🎯 最终原因总结

问题层面 具体原因 发现时间
预分频器 CubeMX生成的代码里Prescaler=0,导致计数频率168MHz(太快) 图片2
引脚配置 PE6实际配成 TIM9_CH2,但代码里用的是 TIM9_CH1 图片5(关键!)

一句话总结:硬件信号进了CH2的门,软件却在CH1门口等,加上预分频器没生效,双重bug导致中断方式一直失败。


✅ 解决方案

改代码(最快):把代码里所有 TIM_CHANNEL_1 改成 TIM_CHANNEL_2

改CubeMX配置:把PE6从CH2改成CH1,重新生成代码

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐