打印
[APM32F4]

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

[复制链接]
94|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主

[i=s] 本帖最后由 kai迪皮 于 2025-6-27 14:59 编辑 [/i]<br /> <br />

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 (输入)

/*!
 * [@brief](home.php?mod=space&uid=247401)   Initialize GPIO-based HC-SR04 measurement.
 * @param   None
 * @retval  None
 */
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();
}

/**
 * @brief  Configure 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.
 * @param  None
 * @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);
}

/**
 * @brief  Trigger an ultrasonic pulse using GPIO and measure the echo width using TMR3.
 * [@NOTE](home.php?mod=space&uid=536309)   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脉冲:

/**
 * @brief  Configures TMR2 CH2 on PB3 to output a PWM pulse of 15~16us
 *         every 60ms. This pulse is fed to the HC-SR04 trigger pin.
 * @param  None
 * @retval None
 */
void TMR2_PWM_Trigger_Init(void)
{
    GPIO_Config_T     gpioConfig;
    TMR_BaseConfig_T  tmrBaseConfig;
    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 -> 下降沿。  

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

// 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)
  1. TMR初始化

用 TIM3_CH1 做输入捕获。

/**
 * @brief  Configures 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.
 * @param  None
 * @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);
}
  1. 中断服务

捕获逻辑实现。

/**
 * @brief  TMR3 IRQ Handler for echo pulse capture.
 *         - On update event => overflowCount++
 *         - On CC1 event => capture rising/falling edges and compute the difference
 * @param  None
 * @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[0] = 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[1] = ccrVal;
            uint32_t totalCnt = (overflowCount << 16) +
                                (captureVal[1] - captureVal[0]);
            /* 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);
    }
}
  1. 计算距离

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

/**
 * @brief  Converts 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.
 * @param  None
 * @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方式:

GPIO_image.png

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

定时器方式:

TMR_image.png

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

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

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

PixPin_2025-06-27_14-31-00.gif

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

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

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

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

以上,便是对 APM32F402R-EVB 和 HC-SR04 混搭测距的经验分享,欢迎在评论区留言讨论!若本文对您有所帮助,还请点赞支持~upload 附件:APM32F402_403_SDK_V1.0.1_HC_SR04.zip

使用特权

评论回复
评论
kai迪皮 2025-6-27 15:00 回复TA
@21小跑堂 #申请原创# 
沙发
kai迪皮|  楼主 | 2025-6-28 14:40 | 只看该作者

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

34

主题

227

帖子

11

粉丝