超声测距模块 HC-SR04P。工作电压3-5V,有效距离2-400cm,分辨率1mm。注意型号末尾的“P”:有另一个外观、接口、工作方式一样的型号 HC-SR04,但工作电压为5V。我这个模块 PCB正面印刷的是HC-SR04,反面印刷 HC-SR04P:

有4个引脚,VCC/GND供电,TRIG触发测距,ECHO接收测量结果。向TRIG给一个10us以上(规格书建议40-50us)的正脉冲触发信号,模块开始工作,发射超声波并接收回声。从发射到接收到回声的时间,以回响电平的形式从ECHO输出;电平持续时间等于声波往返模块到障碍物的时间:

常温下,声音在空气中的传播速度约343m/s = 3.43mm/10us = .8575mm/2.5us。初步想法是,启动一个Timer作为时间基,计数频率 2.5us = 400kHz,以满足10us触发信号延时和1mm测量分辨率的需求。接口方式,触发信号用GPIO output,回响信号用GPIO外部中断。如下图,回响信号接PC7(Arduino兼容Pin D9),其EXTI line编号为7:

使用基本Timer TIM6,时钟源为PCLK1 x 2=72MHz。设PSC=179,得计数频率 CK_CNT = 72MHz / (1+179) = 400kHz。设 ARR = 63999,则 UEV 频率为 400kHz /(1+63999) = 6.25Hz = 160ms。
计时及有关工具函数实现如下。全局变量 _App_timestamp 记录时间戳,在TIM6 UEV 中断回调函数中更新,因此其精度为160ms。获取当前时间戳时,将_App_timestamp 与 TIM6 CNT寄存器值相加,可达到 2.5us 精度:
typedef struct {
uint32_t milli;
uint16_t micro;
} _App_Timestamp;
volatile _App_Timestamp _App_timestamp = { 0, 0 };
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (&htim6 == htim) {
_App_timestamp.milli += 160; // 160 ms/tick
}
}
#define _App_GetDuration(from, to) ( 1000 * ((to).milli - (from).milli) + (to).micro - (from).micro )
void _App_GetTimestamp(_App_Timestamp *ts) {
*ts = _App_timestamp;
uint32_t micro = 5 * htim6.Instance->CNT / 2;
ts->milli += (micro / 1000);
ts->micro += (micro % 1000);
}
void _App_DelayMicro(uint32_t micro) {
_App_Timestamp t1, t2;
_App_GetTimestamp(&t1);
for (;;) {
_App_GetTimestamp(&t2);
if (_App_GetDuration(t1, t2) >= micro) {
break;
}
}
}
ECHO Pin启用外部中断,在中断回调函数内记录上升沿和下降沿的时间戳:
typedef struct {
uint8_t state;
uint32_t duration; // in us
_App_Timestamp _risingTs;
} _App_Measure;
volatile _App_Measure _App_measure = { 0 };
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (D9_ECHO_Pin == GPIO_Pin) {
if (GPIO_PIN_SET == HAL_GPIO_ReadPin(D9_ECHO_GPIO_Port, D9_ECHO_Pin)) {
// Rising edge
_App_Timestamp now;
_App_GetTimestamp(&now);
_App_measure._risingTs = now;
_App_measure.state = 0;
} else {
// Falling edge
_App_Timestamp now;
_App_GetTimestamp(&now);
_App_measure.duration = _App_GetDuration(_App_measure._risingTs,
now);
_App_measure.state = 1;
}
}
}
主循环如下。首先发送TRIG脉冲,时间设为20us。距离的计算注意将声波往返距离除以 2 才是到障碍物的距离:
void App_Loop() {
_App_Trig(20);
HAL_Delay(500);
if (_App_measure.state) {
uint32_t dur = _App_measure.duration;
uint32_t dist = (uint32_t) (.343f * dur / 2);
if (dist > 25 && dist < 5000) {
char msg[80];
sprintf(msg, "%d mm\n", (int) dist);
HAL_UART_Transmit(&huart2, (uint8_t *) msg, strlen(msg),
HAL_MAX_DELAY);
}
}
}
void _App_Trig(uint32_t micro) {
HAL_GPIO_WritePin(D8_TRIG_GPIO_Port, D8_TRIG_Pin, GPIO_PIN_SET);
_App_DelayMicro(micro);
HAL_GPIO_WritePin(D8_TRIG_GPIO_Port, D8_TRIG_Pin, GPIO_PIN_RESET);
}
测得的距离从串口 USART2 输出,单位为mm: