[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 计数来测量脉冲宽度。这种方法直接且好理解:
- Trig:将一个 GPIO 设置为输出口,每隔一定时间(比如 60ms)在代码中拉高 10µs 再拉低,以触发HC-SR04;
- 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 驱动简单,但在多任务或需要更高精度的场景里可能不够快和稳定。于是我们可以使用两个定时器来进行这个工作。
- PB3 (TMR2_CH2) → PWM输出,每60ms自动发一个10µs小脉冲作为Trig;
- 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。
- 极性切换宏:操作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)
- 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);
}
- 中断服务
捕获逻辑实现。
/**
* @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);
}
}
- 计算距离
在需要计算距离的地方调用计算函数。
/**
* @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方式:

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

逻辑分析仪测得高电平持续时间是:3515us,程序实测也是。
两种方案精度都足以应对一般用途。若需要更高精度或更少的 CPU 干预,定时器“硬件捕获”方案更具优势。
配合串口,我们即可打印相应的距离测量值至串口助手啦:

6 小结:擒贼先擒王,测距先“测时”
无论 GPIO 方案还是定时器方案,算法核心都是捕获 Echo 高电平宽度,进而由 d = (v × t)/2 得到距离。大家可以根据场景需求自由选择实现方式:
• 仅做简单验证,可直接用 GPIO 拉高/拉低、空转等待;
• 需要更专业、更稳定的测距环境,或要让 MCU 有更多时间去运行其他任务,就可以采用“PWM 触发 + 硬件输入捕获”双定时器方案,最大化利用硬件资源。
以上,便是对 APM32F402R-EVB 和 HC-SR04 混搭测距的经验分享,欢迎在评论区留言讨论!若本文对您有所帮助,还请点赞支持~
附件:APM32F402_403_SDK_V1.0.1_HC_SR04.zip
@21小跑堂 #申请原创#