一:超声波模块知识分享:
HC-SR04超声波传感器是通过发送和接收超声波,利用时间差和声音传播速度,计算出模块到前方障碍物的距离的一种传感器模块。与蝙蝠寻找猎物的回声定位基本原理相同,可以广泛应用于无损测量、水文液位测量、工业场地等领域。
二:实物图片如下所示:

模块工作原理如下所示:
在 TRIG 管脚输入一个 10US 以上的高电平(一般建议 50Us 左右),芯片(TP,TN管脚)便可发出8个 40KHZ的超声波脉冲,然后(RP,RN)检测回波信号。当检测到回波信号后,通过 ECHO 管脚输出,如图所示。

根据 ECHO 管脚输出高电平的持续时间可以计算距离值。即距离值为:(高电平时间*340m/s)/2。
当测量距离超过测量范围时,CS100A 仍会通过 ECHO 管脚输出高电平的信号,高电平的宽度约为 33ms。如下图所示:

测量周期:当芯片通过 ECHO 管脚输出的高电平脉冲后,便可进行下一次测量,所以测量周期取决于测量距离,当测距很近时,ECHO 返回的脉冲宽度较窄,测量周期就很短;当测距较远时,ECHO 返回的脉冲宽度较宽,测量周期也就相应的变长。最坏情况下,被测物体超出测量范围,此时返回的脉冲宽度最长,约为33ms,所以最坏情况下的测量周期大于 33ms 即可(比如测量周期可取 50ms)。
三:程序编写流程如下:
3.1 程序编写:
3.1.1初始化CPU外设信息,串口1完成数据的输出显示
3.1.2.初始化定时器,同时配置 TRIG 和 ECHO 脚的配置,采用外部中断与定时器计时功能。
3.1.3.TRIG 脚循环输出一个大于 10us 的高电平,触发 CS100A 芯片发射超声波
3.1.4.联合外部中断的上升沿和下降沿时间,带入公式 距离(m)=高电平持续时间(s)*声速(340m/s)/2,算出距离值
3.1.5.串口输出位移测量的距离信息,同时可以测量出距离信息。
四:软件代码实现如下:
总体思路:
ECHO 引脚配置为 GPIO 输入并开启双边沿外部中断,
CTIMER0 定时器以 1MHz 频率(1 个计数 = 1us)自由运行;
当 ECHO 出现上升沿时进入中断并读取定时器计数值作为计时起点,出现下降沿时再次进入中断读取计数值作为计时终点,两者之差即为回波高电平持续时间;
最后代入公式 距离(m) = 高电平持续时间(s) × 声速(340m/s) ÷ 2 计算距离,并通过串口输出。
4.1 硬件连接与资源分配
开发板外设资源分配如下表所示(TRIG / ECHO 引脚可按实际接线修改,见 4.2 代码顶部宏定义):
4.2 完整工程代码(main.c)
本工程基于 MCUXpresso SDK 的 FRDM-MCXA153 例程(如 hello_world)修改
/*
* 文件名:main.c
* 适用平台:NXP FRDM-MCXA153(MCXA153VLH,Cortex-M33,96MHz)
* 功能说明:读取 HC-SR04 超声波模块,测量并串口输出距离
* 实现思路:GPIO 外部中断 + CTIMER0 定时器计时(对应第三章 3.1.1 ~ 3.1.5)
* - ECHO 上升沿中断:读取 CTIMER0 计数值(计时起点)
* - ECHO 下降沿中断:读取 CTIMER0 计数值(计时终点)
* - 距离 = 高电平时间(us) × 声速(340m/s) ÷ 2
*/
#include "fsl_common.h"
#include "fsl_gpio.h"
#include "fsl_port.h"
#include "fsl_ctimer.h"
#include "fsl_debug_console.h"
#include "clock_config.h"
#include "pin_mux.h"
/*============================================================================
* 宏定义:引脚与测量参数(按实际接线修改)
*==========================================================================*/
/* TRIG 触发脚:GPIO 输出 */
#define HCSR04_TRIG_PORT PORT3
#define HCSR04_TRIG_GPIO GPIO3
#define HCSR04_TRIG_PIN 28U
/* ECHO 回波脚:GPIO 输入 + 双边沿外部中断 */
#define HCSR04_ECHO_PORT PORT3
#define HCSR04_ECHO_GPIO GPIO3
#define HCSR04_ECHO_PIN 27U
#define HCSR04_ECHO_IRQ GPIO3_IRQn /* ECHO 所在端口的 NVIC 中断号 */
#define HCSR04_ECHO_IRQ_HANDLER GPIO3_IRQHandler /* 外部中断服务函数名 */
/* CTIMER0:32 位定时器,1MHz 计数(1 个计数 = 1us) */
#define HCSR04_TIMER CTIMER0
#define HCSR04_TIMER_PRESCALE 11U /* 12MHz / (11+1) = 1MHz */
#define HCSR04_TRIG_PULSE_US 20U /* 触发高电平宽度,必须大于 10us */
#define HCSR04_MEASURE_PERIOD_US 50000U /* 测量周期 50ms,大于最大回波 33ms */
#define HCSR04_ECHO_TIMEOUT_US 40000U /* 回波超时 40ms,防止程序卡死 */
#define HCSR04_MIN_PULSE_US 50U /* 有效回波最小脉宽(约 0.9cm 盲区) */
#define HCSR04_MAX_PULSE_US 30000U /* 有效回波最大脉宽(约 5.1m 量程) */
/* 声速 340m/s(20℃ 空气中近似值) */
#define SOUND_SPEED 340U
/*============================================================================
* 全局变量(供中断服务函数与主循环共享,需加 volatile)
*==========================================================================*/
static volatile uint32_t s_echoStartTick = 0U; /* 上升沿时刻计数值 */
static volatile uint32_t s_echoEndTick = 0U; /* 下降沿时刻计数值 */
static volatile bool s_echoValid = false;/* 本次回波测量是否完成 */
static volatile uint8_t s_echoState = 0U; /* 0=空闲 1=等待上升沿 2=等待下降沿 */
/*============================================================================
* CTIMER0 初始化:配置为自由运行计数,计数频率 1MHz
*==========================================================================*/
static void HCSR04_TimerInit(void)
{
ctimer_config_t config;
/* 为 CTIMER0 提供 12MHz 时钟(FRO12M),并配置预分频
* (若所用 SDK 版本枚举名不同,以 Config Tools 生成结果为准) */
CLOCK_SetClockDiv(kCLOCK_DivCTimer0, 1U);
CLOCK_AttachClk(kFRO12M_to_CTIMER0);
CTIMER_GetDefaultConfig(&config);
config.prescale = HCSR04_TIMER_PRESCALE; /* 12MHz/12 = 1MHz,即 1us 计数一次 */
CTIMER_Init(HCSR04_TIMER, &config);
CTIMER_StartTimer(HCSR04_TIMER); /* 启动自由运行计数 */
}
/*============================================================================
* GPIO 初始化:TRIG 输出、ECHO 输入 + 双边沿外部中断
*==========================================================================*/
static void HCSR04_GPIOInit(void)
{
gpio_pin_config_t trigConfig = { kGPIO_DigitalOutput, 0U };
gpio_pin_config_t echoConfig = { kGPIO_DigitalInput, 0U };
/* 引脚复用为 GPIO(复位后默认为 GPIO,可省略) */
PORT_SetPinMux(HCSR04_TRIG_PORT, HCSR04_TRIG_PIN, kPORT_MuxAsGpio);
PORT_SetPinMux(HCSR04_ECHO_PORT, HCSR04_ECHO_PIN, kPORT_MuxAsGpio);
GPIO_PinInit(HCSR04_TRIG_GPIO, HCSR04_TRIG_PIN, &trigConfig);
GPIO_PinInit(HCSR04_ECHO_GPIO, HCSR04_ECHO_PIN, &echoConfig);
/* ECHO 配置双边沿外部中断,并打开 NVIC 总开关 */
GPIO_SetPinInterruptConfig(HCSR04_ECHO_GPIO, HCSR04_ECHO_PIN, kGPIO_InterruptEitherEdge);
EnableIRQ(HCSR04_ECHO_IRQ);
GPIO_PinWrite(HCSR04_TRIG_GPIO, HCSR04_TRIG_PIN, 0U);
}
/*============================================================================
* ECHO 外部中断服务函数:上升沿记录计时起点,下降沿记录计时终点
*==========================================================================*/
void HCSR04_ECHO_IRQ_HANDLER(void)
{
uint32_t flags = GPIO_PortGetInterruptFlags(HCSR04_ECHO_GPIO);
if (flags & (1UL << HCSR04_ECHO_PIN))
{
uint32_t pinState = GPIO_PinRead(HCSR04_ECHO_GPIO, HCSR04_ECHO_PIN);
switch (s_echoState)
{
case 1U: /* 等待上升沿 */
if (pinState != 0U)
{
s_echoStartTick = CTIMER0->TC; /* 上升沿:记录计时起点 */
s_echoState = 2U;
}
break;
case 2U: /* 等待下降沿 */
if (pinState == 0U)
{
s_echoEndTick = CTIMER0->TC; /* 下降沿:记录计时终点 */
s_echoValid = true;
s_echoState = 0U;
}
break;
default:
break;
}
/* 清除中断标志,避免重复进入中断 */
GPIO_GpioClearInterruptFlags(HCSR04_ECHO_GPIO, 1UL << HCSR04_ECHO_PIN);
}
SDK_ISR_EXIT_BARRIER;
}
/*============================================================================
* 触发一次测量并等待回波完成(带超时保护)
* 返回回波高电平持续时间(us);超时返回 0
*==========================================================================*/
static uint32_t HCSR04_MeasurePulse(void)
{
uint32_t timeoutStart;
s_echoValid = false;
s_echoState = 0U;
GPIO_GpioClearInterruptFlags(HCSR04_ECHO_GPIO, 1UL << HCSR04_ECHO_PIN);
/* 若 ECHO 当前为低电平,则准备等待上升沿 */
if (GPIO_PinRead(HCSR04_ECHO_GPIO, HCSR04_ECHO_PIN) == 0U)
{
s_echoState = 1U;
}
/* 3.1.3:TRIG 输出大于 10us 的高电平,触发芯片发射超声波 */
GPIO_PinWrite(HCSR04_TRIG_GPIO, HCSR04_TRIG_PIN, 1U);
SDK_DelayAtLeastUs(HCSR04_TRIG_PULSE_US, SystemCoreClock);
GPIO_PinWrite(HCSR04_TRIG_GPIO, HCSR04_TRIG_PIN, 0U);
/* 等待外部中断完成计时(上升沿 + 下降沿),或超时退出 */
timeoutStart = CTIMER0->TC;
while (!s_echoValid)
{
if ((uint32_t)(CTIMER0->TC - timeoutStart) > HCSR04_ECHO_TIMEOUT_US)
{
s_echoState = 0U;
return 0U; /* 超时:无回波 */
}
}
return s_echoEndTick - s_echoStartTick; /* 高电平持续时间(us) */
}
/*============================================================================
* 主函数
*==========================================================================*/
int main(void)
{
BOARD_InitHardware(); /* 3.1.1:CPU 外设、时钟与串口初始化 */
PRINTF("\r\n========== FRDM-MCXA153 HC-SR04 超声波测距 ==========\r\n");
HCSR04_TimerInit(); /* 3.1.2:初始化定时器 */
HCSR04_GPIOInit(); /* 3.1.2:配置 TRIG/ECHO 脚与外部中断 */
while (1)
{
uint32_t pulseUs = HCSR04_MeasurePulse();
if (pulseUs == 0U)
{
PRINTF("无回波(超时)\r\n");
}
else if (pulseUs < HCSR04_MIN_PULSE_US)
{
PRINTF("距离过近(盲区)\r\n");
}
else if (pulseUs > HCSR04_MAX_PULSE_US)
{
PRINTF("超出测量范围\r\n");
}
else
{
/* 3.1.4:距离(m) = 高电平时间(s) × 声速(340m/s) ÷ 2
* 换算成十分之一厘米:十分之一厘米 = 脉宽(us) × 340 ÷ 2000
* 例:脉宽 1494us → 1494×340÷2000 = 254,即 25.4cm */
uint32_t distTenthsCm = (uint32_t)(((uint64_t)pulseUs * SOUND_SPEED) / 2000U);
/* 3.1.5:串口输出距离信息 */
PRINTF("Distance: %u.%u cm (Echo pulse: %u us)\r\n",
distTenthsCm / 10U,
distTenthsCm % 10U,
(unsigned int)pulseUs);
}
/* 测量周期 50ms:大于最坏情况下的回波宽度 33ms */
SDK_DelayAtLeastUs(HCSR04_MEASURE_PERIOD_US, SystemCoreClock);
}
}4.3 代码与第三章流程的对应说明
3.1.1 初始化CPU外设信息,串口1完成数据的输出显示 —— main 函数中调用 BOARD_InitHardware() 完成系统时钟、引脚复用等外设初始化;调试串口使用板载 USB 虚拟串口(LPUART0,115200-8-N-1),代码中使用 PRINTF 输出测距数据。
3.1.2 初始化定时器,配置 TRIG 和 ECHO 脚,采用外部中断与定时器计时功能 —— HCSR04_TimerInit() 将 CTIMER0 配置为 1MHz 自由运行计数;HCSR04_GPIOInit() 将 TRIG 配置为 GPIO 输出、ECHO 配置为 GPIO 输入并开启双边沿外部中断(本例为 GPIO3_IRQn)。
3.1.3 TRIG 脚循环输出一个大于 10us 的高电平,触发芯片发射超声波 —— HCSR04_MeasurePulse() 中令 TRIG 输出 20us 高电平(大于 10us),触发 HC-SR04 芯片发射 8 个 40kHz 超声波脉冲,同时 ECHO 被拉高开始回波计时。
3.1.4 联合外部中断的上升沿和下降沿时间,带入公式计算距离 —— 中断服务函数 HCSR04_ECHO_IRQ_HANDLER 中:上升沿时读取 CTIMER0 计数值作为计时起点,下降沿时读取计数值作为计时终点,脉宽 = 终点 − 起点;主循环将脉宽代入 距离 = 脉宽 × 声速 ÷ 2 计算距离值。
3.1.5 串口输出位移测量的距离信息 —— 主循环每 50ms 触发一次测量,通过 PRINTF 串口输出距离值;测量周期大于第二章所述最坏情况 33ms 的回波宽度,保证每次测量互不干扰。
4.4 编译烧录与运行结果
编译烧录步骤:
在 keil中导入 FRDM-MCXA153 的 hello_world 例程(SDK 26.x)。
将工程中的 main.c 替换为 4.2 节代码;CTIMER0 时钟(FRO 12MHz、预分频 11)与引脚配置也可用 Config Tools 图形化生成。
编译并烧录到开发板,打开串口助手(波特率 115200,8N1)。
注意事项:
若 40ms 内未收到回波,串口输出“无回波(超时)”;ECHO 高电平超过 30ms 时判为“超出测量范围”(对应第二章介绍的约 33ms 回波情况)。
TRIG / ECHO 引脚按实际接线修改代码顶部宏定义;若 ECHO 改接到其他 GPIO 端口,中断号与中断服务函数名需同步修改(GPIO0_IRQn/GPIO0_IRQHandler … GPIO3_IRQn/GPIO3_IRQHandler)。
计时精度:1MHz 计数(1us)分辨率下,单个计数的距离误差约 0.017cm(340m/s × 1us ÷ 2),与第五章分析一致;实际误差还受环境温度(声速变化)与 ECHO 边沿抖动影响。
若希望进一步降低 CPU 开销,可改用 CTIMER0 的输入捕获功能(硬件自动记录边沿时刻,无需在中断中读取计数值),可参考 SDK 中 ctimer 输入捕获例程。
五:感想如下:
这里使用的定时器来读取返回的时候,之前使用定时器做过基本的计数,PWM输出可调脉宽、频率,输入捕获等等功能,在本次任务中,本想使用定时器的输入捕获功能,但是考虑到编写代码对于一些刚刚接触STM32的,不方便理解,这里我是用定时器+外部中断的方式来获取时间,即检测到某个IO口的上升沿来启动定时器,检测到下降沿的时候关闭定时器,同时读取定时器内部的计数值。由于超声波是往返过程,需要进行平分处理,得到当前超声波检测的数据即可。在开始调试代码的时候,发现定时器配置参数不合适,倒是检测的误差有点大,最后测试在1us的时间间隔内进行进入定时器的中断计数,这样误差在每个脉冲0.034cm。由于超声波模块测量的最大距离是5.6m。考虑到超声波在空气中传播速度因素,以及定时器计数的时间间隔因素,都是影响测量的精度。
我要赚赏金
