kai迪皮 发表于 2025-6-27 14:55

APM32F402 & HC-SR04超声测距宝典:GPIO+定时器双驾齐驱

本帖最后由 kai迪皮 于 2025-6-27 14:59 编辑

# 1 引言:平价超声波模块 HC-SR04 的“江湖地位”

超声波测距在机器人、四轴飞行器和智能家居等领域非常常见,对于较近距离(2~300 cm)且对环境精度要求相对一般的场合,HC-SR04 可谓是“物美价廉”的超声波测距的经典代表,它只需一个触发脉冲,就会发出 40 kHz 超声并返回一个长度与距离对应的 Echo 脉冲,高度简化了超声测距的实现流程。而 APM32F402这种常见的 ARM 内核 MCU,则拥有丰富的 GPIO 口及定时器资源,非常适宜来驱动 HC-SR04。

本文将给大家分享在APM32F402R-EVB板卡结合 HC-SR04模块进行测距的一些收获。

# 2 HC-SR04 原理重温:发声、收声,再算距离

本小结我们一起简要回顾一下 HC-SR04 的工作原理:

* Trig 引脚:只要输入一个 >10µs 的高电平脉冲,模块就会发射约 40kHz 的超声波。
* Echo 引脚:当模块检测到反射回的超声波后,Echo 从低电平变为高,在回波结束或超时后再变回低电平。其高电平持续时间 = 超声波往返的时间。
* 测距离公式:

d = (v × T)/2

其中 v 为声速(约 350 m/s,视温度/湿度而微调),T 即 Echo 高电平持续的总时间。若你捕获到 Echo 高电平为 3 ms,则 d = (350 × 0.003)/2 = 0.525 m 左右。

* 模块优势与不足:

• 优势:成本低、易上手,适合 2 cm~300/400 cm 以内的一般测距需求。

• 不足:精度随环境温度、湿度、气流等波动较大;盲区约 2 cm;若超过 4~5 m,成功率和精度迅速下降;Echo 默认输出 5V,需确保不会伤及 3.3V MCU 引脚。

# 3 GPIO 驱动 HC-SR04:上手最快的方案

在最初学阶段,很多人用普通 GPIO 口产生 Trig 脉冲,再用另一个 GPIO 口捕获 Echo 的上升/下降沿,通过TMR 计数来测量脉冲宽度。这种方法直接且好理解:

1. Trig:将一个 GPIO 设置为输出口,每隔一定时间(比如 60ms)在代码中拉高 10µs 再拉低,以触发HC-SR04;
2. Echo:将另一个 GPIO 配置成输入模式 Echo 的上升、下降沿。上升沿来时记录当前计数,下降沿来时再记录,做差即为脉冲宽度。

代码示例便是基于此思路:

• Trig = PB3 (输出)

• Echo = PB4 (输入)

```c
/*!
* @brief   Initialize GPIO-based HC-SR04 measurement.
* @param   None
* @retvalNone
*/
void GPIO_HCSR04_Init(void)
{
    GPIO_Config_T GPIO_ConfigStruct = {0U};

    /* Enable the TRIG/ECHO Clock */
    RCM_EnableAPB2PeriphClock(TRIG_GPIO_CLK);
    RCM_EnableAPB2PeriphClock(ECHO_GPIO_CLK);
    RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_AFIO);

    GPIO_ConfigPinRemap(GPIO_REMAP_SWJ_JTAGDISABLE);

    /* Configure the TRIG pin */
    GPIO_ConfigStruct.pin = TRIG_PIN;
    GPIO_ConfigStruct.mode = GPIO_MODE_OUT_PP;
    GPIO_ConfigStruct.speed = GPIO_SPEED_50MHz;

    GPIO_Config(TRIG_GPIO_PORT, &GPIO_ConfigStruct);

    /* Configure the ECHO pin */
    GPIO_ConfigStruct.pin = ECHO_PIN;
    GPIO_ConfigStruct.mode = GPIO_MODE_IN_PD;

    GPIO_Config(ECHO_GPIO_PORT, &GPIO_ConfigStruct);


    /* Initialize TMR3 for 1us base */
    TMR3_Config();
}

/**
* @briefConfigure TMR3 as a 1 MHz counter to measure echo pulse width.
*         TMR3 clock = APB1 Clock (e.g. 72 MHz) / Prescaler -> 1 MHz => 1 us per tick.
* @paramNone
* @retval None
*/
static void TMR3_Config(void)
{
    /* Enable TMR3 clock */
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_TMR3);

    TMR_BaseConfig_T tmrConfig;

    // Prescaler to get 1 MHz from e.g. 120 MHz APB1 clock
    tmrConfig.division      = 120-1;            // PSC = 119
    tmrConfig.countMode   = TMR_COUNTER_MODE_UP;
    tmrConfig.period      = 0xFFFF;            // Max period for 16-bit timer
    tmrConfig.clockDivision = TMR_CLOCK_DIV_1;
    TMR_ConfigTimeBase(TMR3, &tmrConfig);

    // Enable timer
    TMR_Enable(TMR3);
}

/**
* @briefTrigger an ultrasonic pulse using GPIO and measure the echo width using TMR3.
* @NOTE   1. TMR3 is configured as 1us time base in TMR3_Config().
*         2. The function will return a float distance in mm.
*         3. If echo signal fails or distance exceeds 4m, it returns 0.0f.
* @retval Distance in millimeters (float).
*/
float sonar_mm_gpio(void)
{
    uint32_t time_end   = 0;
    float distance_mm   = 0.0f;

    // 1) Trigger >10us pulse
    TRIG_GPIO_PIN_High();
    BOARD_Delay_Us(15); // This function can be a rough delay
    TRIG_GPIO_PIN_Low();

    // 2) Wait for Echo rising edge
    //    Wait until the ECHO pin becomes high
    while ((GPIO_ReadInputBit(ECHO_GPIO_PORT, ECHO_PIN) == BIT_RESET))
    {
      // Optional: Add a timeout check if needed
    }

    // 3) Reset TMR3 counter to 0 once rising edge is detected
    TMR3->CNT = 0;

    // 4) Wait for Echo falling edge
    //    Wait until the ECHO pin becomes low
    while ((GPIO_ReadInputBit(ECHO_GPIO_PORT, ECHO_PIN) == BIT_SET))
    {
      // Optional: Add timeout check if needed (ECHO_TIMEOUT_US)
      if (TMR3->CNT > ECHO_TIMEOUT_US)
      {
            return 0.0f; // Echo timeout
      }
    }

    // 5) Read pulse width in microseconds
    time_end = TMR3->CNT;

    // 6) Calculate distance (in mm)
    //    Speed of sound ~350 m/s => 0.35 mm/us (round trip => half => 0.175 mm/us)
    distance_mm = time_end * SOUND_SPEED_COEF;

    // If over 4m => invalid measurement
    if (distance_mm > 4000.0f)
    {
      distance_mm = 0.0f;
    }

    return (distance_mm);
}

```

***这种方法的优点是简单易懂、上手快;缺点是需要在软件里忙于等待和计时,对于多任务或实时性要求较高的场景会受到影响。此外如果测距很多次,CPU就会频繁陷入等待状态,不够高效。***

# 4 双定时器驱动:让硬件定时器“包办一切”

虽然 GPIO 驱动简单,但在多任务或需要更高精度的场景里可能不够快和稳定。于是我们可以使用两个定时器来进行这个工作。

1. PB3 (TMR2\_CH2) → PWM输出,每60ms自动发一个10µs小脉冲作为Trig;
2. PB4 (TMR3\_CH1) → 输入捕获检测Echo脉冲时长,计数器频率1MHz(精度1us)。

这里跟刚才的“GPIO硬拉”不同,PWM会自动完成拉高/拉低,而捕获也由硬件寄存器把上升沿、下降沿时间全记下。咱们在中断里只要把这些时间戳做个差,就知道Echo持续多久了。

## 4.1 TMR2\_CH2:定时输出启动脉冲

首先是TMR\_CH2的时间输出,通过下面的代码,每隔60ms,TMR2\_CH2会自动“抡”一下引脚,给你奉上10us脉冲:

```c
/**
* @briefConfigures TMR2 CH2 on PB3 to output a PWM pulse of 15~16us
*         every 60ms. This pulse is fed to the HC-SR04 trigger pin.
* @paramNone
* @retval None
*/
void TMR2_PWM_Trigger_Init(void)
{
    GPIO_Config_T   gpioConfig;
    TMR_BaseConfig_TtmrBaseConfig;
    TMR_OCConfig_T    tmrOCConfig;

    /* 1) Enable AFIO clock if needed and PB3 GPIO clock */
    RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_AFIO);
    RCM_EnableAPB2PeriphClock(TMR_TRIG_GPIO_CLK);

    /* 2) Configure PB3 as Alternate Function Push-Pull for TMR2_CH2 */
    gpioConfig.speed = GPIO_SPEED_50MHz;
    gpioConfig.mode= GPIO_MODE_AF_PP;    // Alternate function push-pull
    gpioConfig.pin   = TMR_TRIG_PIN;
    GPIO_Config(TMR_TRIG_GPIO_PORT, &gpioConfig);

    /* 3) Remap if the MCU requires switching TMR2_CH2 to PB3 */
    TMR_TRIG_GPIO_AF();

    /* 4) Enable TMR2 clock */
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_TMR2);

    /*
   * 5) Configure TMR2 base
   *    - Assume TMR2 clock source = 120 MHz
   *    - We want 1 MHz => PSC = 119 => 120 MHz / (119+1) = 1 MHz
   *    - Period = 59999 => 60,000 counts => 60 ms
   *    => Timer overflows every 60 ms
   */
    tmrBaseConfig.countMode         = TMR_COUNTER_MODE_UP;
    tmrBaseConfig.clockDivision   = TMR_CLOCK_DIV_1;
    tmrBaseConfig.period            = 59999;   // ARR
    tmrBaseConfig.division          = 119;       // PSC
    tmrBaseConfig.repetitionCounter = 0;         // Not used for general-purpose timers
    TMR_ConfigTimeBase(TMR2, &tmrBaseConfig);

    /*
   * 6) Configure TMR2_CH2 for PWM mode
   *    - PWM1 mode => output is HIGH from 0 to CCR2
   *    - CCR2 = 16 => ~ 15us ~ 16us high pulse
   */
    tmrOCConfig.mode         = TMR_OC_MODE_PWM1;
    tmrOCConfig.outputState= TMR_OC_STATE_ENABLE;
    tmrOCConfig.outputNState = TMR_OC_NSTATE_DISABLE;
    tmrOCConfig.polarity   = TMR_OC_POLARITY_HIGH;
    tmrOCConfig.nPolarity    = TMR_OC_NPOLARITY_HIGH;
    tmrOCConfig.idleState    = TMR_OC_IDLE_STATE_RESET;
    tmrOCConfig.nIdleState   = TMR_OC_NIDLE_STATE_RESET;
    tmrOCConfig.pulse      = 16;   // 16 => ~16us high level
    TMR_ConfigOC2(TMR2, &tmrOCConfig);

    /*
   * 7) Enable PWM outputs if necessary
   *    For some timers (advanced timers or special configs),
   *    TMR_EnablePWMOutputs(TMR2) might be required.
   */
    TMR_EnablePWMOutputs(TMR2);

    /* 8) Enable TMR2 counter */
    TMR_Enable(TMR2);
}
```

## 4.2 TMR3\_CH1 :计算高电平持续时间

Echo脚交给TMR3来检测。先初始化成:  

• Timer3基频=1MHz;  

• CH1配置为Input Capture,先用上升沿捕获,捕获后改成下降沿,再捕获一次,然后把两次捕获值的差保存到全局变量 echoWidth\_us。

1. **极性切换宏:操作CC1POL**

在APM32F402里,输入捕获极性由CC1POL位决定:0 -> 上升沿,1 -> 下降沿。  

为了操作方便,可以给它搞两个小宏。  

```c
// macros for TMR3 CH1
#define TMR3_IC1_POLARITY_RISING_ENABLE()(TMR3->CCEN_B.CC1POL = BIT_RESET)
#define TMR3_IC1_POLARITY_FALLING_ENABLE() (TMR3->CCEN_B.CC1POL = BIT_SET)
```

2. **TMR初始化**

用 TIM3\_CH1 做输入捕获。

```c
/**
* @briefConfigures TMR3 to capture the echo pulse on PB4 (TMR3_CH1).
*         The timer runs at 1us per count; rising and falling edges
*         are captured. The difference indicates the pulse width.
* @paramNone
* @retval None
*/
void TIM3_IC_EchoInit(void)
{
    // 1) Enable TMR3 and PB4 clocks
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_TMR3);
    RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_AFIO);
    RCM_EnableAPB2PeriphClock(TMR_ECHO_GPIO_CLK);

    // 2) Configure PB4 as input (pull-down or floating as needed)
    GPIO_Config_T gpioConfig;
    gpioConfig.pin   = TMR_ECHO_PIN;
    gpioConfig.mode= GPIO_MODE_IN_PD;
    gpioConfig.speed = GPIO_SPEED_50MHz;
    GPIO_Config(TMR_ECHO_GPIO_PORT, &gpioConfig);

    // Remap or pin AF if needed
    TMR_ECHO_GPIO_AF();

    /*
   * 3) Set up TMR3 as 1us time base
   *    - TMR3 clock = 60 MHz
   *    - PSC = 120 - 1 => 119 => 120MHz/(119+1)=1MHz => 1 tick = 1us
   *    - ARR = 0xFFFF => 16-bit full range
   */
    TMR_BaseConfig_T base;
    base.countMode         = TMR_COUNTER_MODE_UP;
    base.clockDivision   = TMR_CLOCK_DIV_1;
    base.period            = 0xFFFF;   // 65535
    base.division          = (120 - 1);// PSC=119
    base.repetitionCounter = 0;
    TMR_ConfigTimeBase(TMR3, &base);

    /*
   * 4) Configure Channel 1 for input capture
   *    - Polarity: rising edge first
   *    - Then switch to falling edge after capturing rising
   */
    TMR_ICConfig_T ic;
    ic.channel   = TMR_CHANNEL_1;
    ic.polarity= TMR_IC_POLARITY_RISING;
    ic.selection = TMR_IC_SELECTION_DIRECT_TI;
    ic.prescaler = TMR_IC_PSC_1;
    ic.filter    = 0;
    TMR_ConfigIC(TMR3, &ic);

    /* 5) Enable update interrupt (for overflow) and CC1 interrupt */
    TMR_EnableInterrupt(TMR3, TMR_INT_UPDATE);
    TMR_EnableInterrupt(TMR3, TMR_INT_CC1);
    NVIC_EnableIRQRequest(TMR3_IRQn, 0xF, 0xF);

    /* 6) Start TMR3 */
    TMR_Enable(TMR3);
}
```

3. **中断服务**

捕获逻辑实现。

```c
/**
* @briefTMR3 IRQ Handler for echo pulse capture.
*         - On update event => overflowCount++
*         - On CC1 event => capture rising/falling edges and compute the difference
* @paramNone
* @retval None
*/
void TMR3_IRQHandler(void)
{
    /* 1) Check overflow => increment overflowCount */
    if (TMR_ReadIntFlag(TMR3, TMR_INT_UPDATE))
    {
      overflowCount++;
      TMR_ClearIntFlag(TMR3, TMR_INT_UPDATE);
    }

    /* 2) Check CC1 capture => read CCR1 */
    if (TMR_ReadIntFlag(TMR3, TMR_INT_CC1))
    {
      uint32_t ccrVal = TMR_ReadCaputer1(TMR3);

      /* First capture: rising edge => record CCR1, reset overflowCount,
         switch to falling edge next time */
      if (captureIndex == 0)
      {
            captureVal = ccrVal;
            captureIndex= 1;
            overflowCount = 0;

            /* Switch to falling edge for next capture */
            TMR3_IC1_POLARITY_FALLING_ENABLE();
      }
      else
      {
            /* Second capture: falling edge => compute pulse width */
            captureVal = ccrVal;
            uint32_t totalCnt = (overflowCount << 16) +
                              (captureVal - captureVal);
            /* Store the elapsed counts (each count = 1us) */
            echoWidth_us = totalCnt;

            /* Reset for next measurement */
            captureIndex= 0;
            overflowCount = 0;
            TMR3_IC1_POLARITY_RISING_ENABLE();
      }
      TMR_ClearIntFlag(TMR3, TMR_INT_CC1);
    }
}
```

4. **计算距离**

在需要计算距离的地方调用计算函数。

```c
/**
* @briefConverts the measured pulse width (in us) into distance (in mm).
*         - Speed of sound is ~350 m/s => 0.35 mm/us round trip.
*         - Divided by 2 => 0.175 mm/us one-way => multiply time by 0.175.
* @paramNone
* @retval Distance in millimeters (float).
*/
float sonar_mm_tmr(void)
{
    float distance_mm = 0.0f;

    /* Convert echoWidth_us to mm => time(us) * 0.17 mm/us */
    distance_mm = echoWidth_us * SOUND_SPEED_COEF;

    /* Discard invalid if distance > 4m */
    if (distance_mm > 4000.0f)
    {
      distance_mm = 0.0f;
    }

    return distance_mm;
}
```

# 5 效果:看看测得准不准

我们插上逻辑分析仪对固定高度进行测量,通过逻辑分析仪抓取测量ECHO波形宽度,然后对照两种的计算方式。

GPIO方式:

!(data/attachment/forum/202506/27/145246wsr92zje9pj22232.png "GPIO_image.png")

逻辑分析仪测得高电平持续时间是:3490us,程序实测也是。

定时器方式:

!(data/attachment/forum/202506/27/145259umoku9dccoz1l1ut.png "TMR_image.png")

逻辑分析仪测得高电平持续时间是:3515us,程序实测也是。

**两种方案精度都足以应对一般用途。若需要更高精度或更少的 CPU 干预,定时器“硬件捕获”方案更具优势。**

配合串口,我们即可打印相应的距离测量值至串口助手啦:

!(data/attachment/forum/202506/27/145313yl44l3xbrccnfqcx.gif "PixPin_2025-06-27_14-31-00.gif")

# 6 小结:擒贼先擒王,测距先“测时”

无论 GPIO 方案还是定时器方案,算法核心都是捕获 Echo 高电平宽度,进而由 d = (v × t)/2 得到距离。大家可以根据场景需求自由选择实现方式:

• 仅做简单验证,可直接用 GPIO 拉高/拉低、空转等待;

• 需要更专业、更稳定的测距环境,或要让 MCU 有更多时间去运行其他任务,就可以采用“PWM 触发 + 硬件输入捕获”双定时器方案,最大化利用硬件资源。

以上,便是对 APM32F402R-EVB 和 HC-SR04 混搭测距的经验分享,欢迎在评论区留言讨论!若本文对您有所帮助,还请点赞支持~[!(/source/plugin/zhanmishu_markdown/template/editor/images/upload.svg) 附件:APM32F402_403_SDK_V1.0.1_HC_SR04.zip](forum.html?mod=attachment&aid=2418946 "attachment")

kai迪皮 发表于 2025-6-28 14:40

#申请原创#@21小跑堂

VelvetNight 发表于 2025-6-29 08:22

这个讲得很明白的样子啊!
话说这个小模块的精度在什么水平?

kai迪皮 发表于 2025-6-29 17:13

VelvetNight 发表于 2025-6-29 08:22
这个讲得很明白的样子啊!
话说这个小模块的精度在什么水平?

这个小模块精度最高到3MM{:biggrin:}

DawnFervor 发表于 2025-7-2 23:42

这么分析下来,第二种方式在一般的项目应用里面就可以胜任了。
如果采用“PWM 触发 + 硬件输入捕获“的话,就更适合复杂系统设计了

kai迪皮 发表于 2025-7-3 09:23

DawnFervor 发表于 2025-7-2 23:42
这么分析下来,第二种方式在一般的项目应用里面就可以胜任了。
如果采用“PWM 触发 + 硬件输入捕获“的话, ...

是的,“PWM 触发 + 硬件输入捕获”适用于绝大多场景,更复杂的场景也是OK的。

VelvetNight 发表于 2025-7-3 10:27

如果按1MHz的Timer采样率来讲。
是不是最小颗粒度可以达到 0.175mm = ( 350 x 10^3 * 10^-6)/2
页: [1]
查看完整版本: APM32F402 & HC-SR04超声测距宝典:GPIO+定时器双驾齐驱