简介:本资源是基于STM32F103RCT6微控制器与HC-SR04超声波传感器实现高精度实时测距的完整嵌入式项目,面向嵌入式初学者、课程设计学生及智能硬件开发者,解决非接触式距离检测在机器人避障、智能家居与自动化场景中的基础应用需求。压缩包共77个文件(36个.h头文件定义外设与模块接口,33个.c源文件含主控逻辑、HCSR04驱动、定时器捕获、串口通信及LCD/OLED显示功能),辅以Keil工程配置(uvprojx/uvoptx)、调试配置(dbgconf)、启动代码(startup_stm32f10x_hd.s)及可执行hex文件,整体体积仅327KB,结构清晰、模块解耦度高,便于理解底层寄存器操作与中断时序控制。已有1069人学习下载,提供开箱即用的Keil工程,含完整HAL或标准外设库风格代码、详细注释、多级滤波算法实现及温度补偿预留接口,助读者快速掌握超声波测距原理、STM32定时器输入捕获配置及嵌入式系统软硬件协同调试方法。
1. 项目概述:从零到一,用STM32F103RCT6驱动HCSR04超声波模块
最近在整理以前做过的嵌入式小项目,翻出来一个基于STM32F103RCT6和HCSR04超声波模块的测距系统。这个项目虽然看起来简单,就是一个“发波-收波-计时-计算”的过程,但里面涉及到的GPIO操作、定时器精准计时、中断处理以及环境因素补偿,每一个环节都藏着不少细节。很多新手朋友在复现时,要么测不准,要么干脆没数据,问题往往就出在这些细节上。今天我就把这个项目的完整实现过程、核心代码逻辑以及我踩过的坑,系统地梳理一遍,目标是让你看完就能自己动手搭起来,并且理解每一步为什么要这么做。
这个项目非常适合刚学完STM32基础,想找一个综合性小项目练手的朋友。你将用到STM32的GPIO输出触发信号、输入捕获回波信号,以及定时器的高精度计时功能。最终实现的效果是,通过串口将测得的距离数据实时打印出来,精度可以做到厘米级,响应速度也很快。我会基于最常用的标准库(虽然现在HAL库更流行,但标准库对于理解底层时序更有帮助)来讲解,开发环境是Keil MDK。当然,如果你用的是CubeMX+HAL库,核心思路是完全相通的,我会在关键地方指出差异。
2. 核心硬件与原理深度解析
2.1 主角介绍:STM32F103RCT6与HC-SR04
先来看看我们用的两个核心硬件。STM32F103RCT6,属于STM32F1系列的“增强型”产品,72MHz主频,256KB Flash,48KB RAM,自带3个高级定时器、4个通用定时器,还有2个看门狗和丰富的通信接口。对于我们这个超声波测距项目,它的性能是绰绰有余的。我选择它,一是因为资源丰富,二是因为资料多,社区生态好,出了问题容易找到解决方案。
HC-SR04超声波模块,可以说是电子爱好者入门测距的“老朋友”了。它的工作原理是声纳测距:模块的Trig引脚收到一个至少10us的高电平脉冲后,会自动发射8个40kHz的超声波脉冲;然后Echo引脚会输出一个高电平,其持续时间与超声波从发射到遇到障碍物返回的时间成正比。我们只需要在Echo引脚变高时启动计时,变低时停止计时,就能得到声波往返的时间t。
那么距离S怎么算?公式很简单:S = (t * 340) / 2。这里340是声波在空气中传播的速度,单位是米/秒。t的单位是秒,计算出来的S单位是米。实际操作中,我们计时单位常用微秒(us),声速用34000厘米/秒来换算会更直观:距离(厘米) = (时间(微秒) * 0.034) / 2,或者简化成距离(厘米) = 时间(微秒) / 58.0。这个58.0是怎么来的?因为声速340米/秒 = 34000厘米/秒 = 0.034厘米/微秒。距离是往返路程的一半,所以系数是0.034 / 2 = 0.017厘米/微秒,取其倒数就是1 / 0.017 ≈ 58.8,通常取58或59进行近似计算。我实测下来,用58.0这个系数在常温下是比较准的。
注意:这个
58.0的系数是在标准大气压、温度20℃、干燥空气的条件下得出的。声速受温度影响很大,温度每升高1℃,声速大约增加0.6米/秒。如果对精度要求极高(比如工业级应用),必须加入温度补偿。一个简单的修正公式是:V = 331.4 + 0.6 * T,其中T是摄氏温度。对于大多数室内创客项目,用固定系数问题不大。
2.2 系统连接与引脚规划
接线非常简单,但接错了肯定没反应。HC-SR04模块通常有4个引脚:VCC、Trig、Echo、GND。
- VCC:接STM32的5V或3.3V?这里有个关键点!HC-SR04的老版本模块工作电压是5V,Echo引脚输出的高电平也是5V。而STM32F103RCT6的GPIO引脚耐受电压一般是3.3V,直接接入5V信号有烧毁IO口的风险。所以,如果你的模块是5V电平的,Echo引脚必须串联一个1k-2k的电阻进行分压,或者使用电平转换电路。较新的模块有些已经支持3.3V逻辑电平了,购买时一定要看清楚规格。我的模块是5V的,所以Trig我用3.3V驱动(模块能识别),Echo回传信号通过1k电阻接到STM32的IO口。
- Trig:触发引脚,接STM32的任何一个GPIO,配置为推挽输出即可。我用的是
PA8。 - Echo:回波引脚,接STM32的GPIO,并且这个GPIO必须支持输入捕获功能,因为我们后续要用定时器来测量高电平持续时间。我选择
PA0,它对应定时器2的通道1(TIM2_CH1),方便做输入捕获。 - GND:共地,这个绝对不能忘。
我的连接方案是:
- HC-SR04.VCC -> 5V(开发板的5V引脚)
- HC-SR04.Trig -> STM32.PA8 (GPIO输出)
- HC-SR04.Echo -> STM32.PA0 (GPIO输入,串联1k电阻)
- HC-SR04.GND -> GND
为什么选PA0和PA8?一方面是我的开发板这两个引脚引出来了,方便;另一方面,PA0作为定时器2通道1,其输入捕获功能非常强大,可以直接用硬件记录Echo上升沿和下降沿的时刻,比用外部中断+软件计时要精准和稳定得多。
3. 软件设计与核心代码实现
3.1 开发环境搭建与工程配置
我使用的是Keil MDK 5,芯片包选择STM32F1系列。新建工程时,需要添加标准外设库(StdPeriph Lib)。工程结构建议清晰一点:
User/文件夹放主程序main.c、stm32f10x_it.c(中断服务函数)等。Libraries/文件夹放STM32标准外设库的CMSIS和StdPeriph_Driver。HARDWARE/文件夹放自己写的硬件驱动,比如ultrasonic.c/h。
在ultrasonic.h里,我们定义引脚和定时器:
#ifndef __ULTRASONIC_H #define __ULTRASONIC_H #include "stm32f10x.h" // 引脚定义 #define TRIG_GPIO_PORT GPIOA #define TRIG_GPIO_PIN GPIO_Pin_8 #define TRIG_GPIO_CLK RCC_APB2Periph_GPIOA #define ECHO_GPIO_PORT GPIOA #define ECHO_GPIO_PIN GPIO_Pin_0 #define ECHO_GPIO_CLK RCC_APB2Periph_GPIOA // 定时器定义,用于输入捕获 #define ECHO_TIMx TIM2 #define ECHO_TIM_APBx_CLK RCC_APB1Periph_TIM2 #define ECHO_TIM_IRQn TIM2_IRQn #define ECHO_TIM_IRQHandler TIM2_IRQHandler #define ECHO_TIM_CHANNEL TIM_Channel_1 // 声速换算系数 (厘米/微秒) 的倒数,简化计算 #define ULTRASONIC_SPEED_COEF 58.0f // 单位: us/cm // 最大测量距离和超时时间 (对应约4米) #define MEASURE_TIMEOUT (400 * 2 * ULTRASONIC_SPEED_COEF) // 单位: us void Ultrasonic_Init(void); float Ultrasonic_MeasureDistance(void); #endif这里我定义了一个超时时间MEASURE_TIMEOUT。为什么需要这个?因为如果超声波前方没有障碍物,Echo引脚可能一直不会变低,程序就会死等。设置一个超时(比如对应4米距离的时间),超过这个时间就认为测量失败,返回一个错误值(比如0),避免程序卡死。
3.2 驱动初始化:GPIO与定时器
初始化的核心有两部分:一是Trig引脚的输出初始化,二是Echo引脚对应的定时器输入捕获初始化。我们看Ultrasonic_Init函数:
void Ultrasonic_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 初始化Trig引脚为推挽输出 RCC_APB2PeriphClockCmd(TRIG_GPIO_CLK, ENABLE); GPIO_InitStructure.GPIO_Pin = TRIG_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(TRIG_GPIO_PORT, &GPIO_InitStructure); GPIO_ResetBits(TRIG_GPIO_PORT, TRIG_GPIO_PIN); // 初始化为低电平 // 2. 初始化Echo引脚为浮空输入(因为外部有上拉或下拉) RCC_APB2PeriphClockCmd(ECHO_GPIO_CLK, ENABLE); GPIO_InitStructure.GPIO_Pin = ECHO_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(ECHO_GPIO_PORT, &GPIO_InitStructure); // 3. 初始化定时器2用于输入捕获 RCC_APB1PeriphClockCmd(ECHO_TIM_APBx_CLK, ENABLE); // 时基单元配置:1MHz计数频率,即1个计数=1us TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 自动重装载值,16位最大值 TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(ECHO_TIMx, &TIM_TimeBaseStructure); // 输入捕获配置:通道1,上升沿捕获 TIM_ICInitStructure.TIM_Channel = ECHO_TIM_CHANNEL; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 先捕获上升沿 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 直接映射到TI1 TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 不分频,每个边沿都捕获 TIM_ICInitStructure.TIM_ICFilter = 0x0; // 不滤波 TIM_ICInitStructure.TIM_ICMode = TIM_ICMode_Capture; TIM_ICInit(&ECHO_TIMx, &TIM_ICInitStructure); // 使能捕获/比较1中断和更新中断 TIM_ITConfig(ECHO_TIMx, TIM_IT_CC1 | TIM_IT_Update, ENABLE); // 配置NVIC NVIC_InitStructure.NVIC_IRQChannel = ECHO_TIM_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 使能定时器 TIM_Cmd(ECHO_TIMx, ENABLE); }关键点解析:
- 定时器分频:STM32F103的APB1定时器时钟最高是72MHz。我们设置预分频器为
71(即72-1),这样定时器的计数频率就是72MHz / (71+1) = 1MHz。计数器每计一个数,时间过去1微秒(us),这非常方便我们直接读取时间值。 - 输入捕获极性:初始设置为上升沿捕获,对应Echo引脚从低变高的瞬间,也就是超声波发射的时刻。
- 滤波器:这里设置为0,即不滤波。如果你的环境干扰大,Echo信号有毛刺,可以适当增加滤波值(比如0x8),但要注意滤波会引入微小延迟。
- 中断:我们使能了捕获/比较1中断(CC1IE)和更新中断(UIE)。CC1IE用于处理边沿捕获事件,UIE用于处理计数器溢出(超过65535us,即65.535ms)。
3.3 测距流程与状态机实现
测距不是一个简单的顺序函数,而是一个由“触发-等待回波-计算”构成的异步过程。我使用一个简单的状态机在中断和主循环中配合完成。首先定义几个全局变量:
static volatile uint32_t s_rise_time = 0; // 上升沿捕获时间 static volatile uint32_t s_fall_time = 0; // 下降沿捕获时间 static volatile uint8_t s_measure_status = 0; // 测量状态: 0-空闲, 1-等待上升沿, 2-等待下降沿 static volatile uint8_t s_measure_done = 0; // 测量完成标志 static volatile uint32_t s_timeout_counter = 0; // 超时计数器然后,在Ultrasonic_MeasureDistance函数中,启动一次测量:
float Ultrasonic_MeasureDistance(void) { float distance_cm = 0.0f; // 1. 发送触发脉冲 GPIO_SetBits(TRIG_GPIO_PORT, TRIG_GPIO_PIN); Delay_us(12); // 保持高电平至少10us,这里给12us留有余量 GPIO_ResetBits(TRIG_GPIO_PORT, TRIG_GPIO_PIN); // 2. 重置状态,启动测量 s_measure_status = 1; // 进入“等待上升沿”状态 s_measure_done = 0; s_timeout_counter = 0; // 3. 切换输入捕获为上升沿触发(确保初始状态) TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel = ECHO_TIM_CHANNEL; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0; TIM_ICInitStructure.TIM_ICMode = TIM_ICMode_Capture; TIM_ICInit(ECHO_TIMx, &TIM_ICInitStructure); // 4. 等待测量完成或超时 while (!s_measure_done) { // 这里可以加入一个超时判断,防止死循环 // 简单延时并计数,如果超过预期时间(如100ms)则退出 Delay_ms(1); if (s_timeout_counter++ > 100) { // 约100ms超时 s_measure_status = 0; // 重置状态 return 0.0f; // 返回0表示测量失败 } } // 5. 计算距离 if (s_fall_time > s_rise_time) { uint32_t time_diff = s_fall_time - s_rise_time; // 单位: us // 简单的超时判断,时间过长认为无效 if (time_diff < MEASURE_TIMEOUT) { distance_cm = (float)time_diff / ULTRASONIC_SPEED_COEF; } } // 6. 重置标志,准备下一次测量 s_measure_done = 0; s_measure_status = 0; return distance_cm; }注意:这里的
Delay_us和Delay_ms需要你自己实现,或者使用系统滴答定时器(SysTick)。注意,Delay_us在72MHz下用循环实现时,需要仔细校准。一个简单的实现方法是使用SysTick定时器或一个基本定时器(如TIM6)来产生精确的微秒级延时。
3.4 中断服务函数:精准捕获时间戳
整个系统的精度核心就在定时器中断服务函数里。它负责在Echo信号的边沿到来时,记录下定时器计数器的值。
void ECHO_TIM_IRQHandler(void) { if (TIM_GetITStatus(ECHO_TIMx, TIM_IT_Update) != RESET) { // 定时器溢出中断(计数器超过65535) // 如果我们在等待下降沿时发生了溢出,需要处理时间累加 if (s_measure_status == 2) { // 状态2是已捕获上升沿,在等待下降沿 s_timeout_counter += 65536; // 溢出一次增加65536us } TIM_ClearITPendingBit(ECHO_TIMx, TIM_IT_Update); } if (TIM_GetITStatus(ECHO_TIMx, TIM_IT_CC1) != RESET) { // 捕获/比较1中断,即Echo边沿触发 if (s_measure_status == 1) { // 状态1:等待上升沿,即Echo变高,超声波发射 s_rise_time = TIM_GetCapture1(ECHO_TIMx) + (s_timeout_counter * 65536); s_measure_status = 2; // 切换到等待下降沿状态 // 改为下降沿捕获 TIM_OC1PolarityConfig(ECHO_TIMx, TIM_ICPolarity_Falling); } else if (s_measure_status == 2) { // 状态2:等待下降沿,即Echo变低,超声波返回 s_fall_time = TIM_GetCapture1(ECHO_TIMx) + (s_timeout_counter * 65536); s_measure_done = 1; // 设置测量完成标志 s_measure_status = 0; // 回到空闲状态 // 改回上升沿捕获,为下一次测量做准备 TIM_OC1PolarityConfig(ECHO_TIMx, TIM_ICPolarity_Rising); } TIM_ClearITPendingBit(ECHO_TIMx, TIM_IT_CC1); } }这里是整个项目的精髓,有几个极易出错的点:
- 计数器溢出处理:定时器是16位的,最大值65535。如果测量距离超过约3.7米(对应约22000us * 2?等一下,这里我算错了。应该是
65535us / 58 us/cm ≈ 1130cm = 11.3米的往返时间?不对,58 us/cm是单程时间?我们测量的是往返时间t,距离S = t / 58。如果t = 65535us,则S = 65535/58 ≈ 1130cm = 11.3米。但这是单次计数不溢出的最大距离。实际上,我们通过s_timeout_counter记录溢出次数,可以测量更远的距离。s_timeout_counter * 65536就是溢出部分的总时间。 - 时间戳计算:最终的时间戳 = 当前捕获值 + 溢出次数 * 65536。这样才能在计数器溢出后得到正确的时间。
- 捕获极性切换:在捕获到上升沿后,要立即将极性改为下降沿,这样才能在Echo变低时再次触发中断。测量完成后,再改回上升沿,为下一次测量做准备。
- 中断标志清除:必须在中断服务函数结束前清除相应的中断标志位,否则会连续进入中断,导致系统崩溃。
4. 主程序逻辑与数据输出
驱动写好了,主程序就非常简单了。主要就是初始化系统,然后在一个循环里不断测量并输出结果。
#include "stm32f10x.h" #include "ultrasonic.h" #include "stdio.h" // 用于printf #include "usart.h" // 串口初始化,需要你自己实现 // 简单重定向printf到串口1(可选) int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); return ch; } int main(void) { // 系统时钟初始化(默认72MHz) SystemInit(); // 初始化串口1,用于打印数据,波特率115200 USART1_Init(115200); // 初始化超声波模块 Ultrasonic_Init(); printf("STM32F103 HC-SR04 Ultrasonic Ranging System Ready.\r\n"); while (1) { float dist = Ultrasonic_MeasureDistance(); if (dist > 0.1f && dist < 400.0f) { // 过滤无效值(0-400cm) printf("Distance: %.2f cm\r\n", dist); } else { printf("Measurement Error or Out of Range.\r\n"); } // 每次测量间隔至少60ms,给传感器留出“安静”时间,防止上次回波干扰 Delay_ms(100); // 这里延时100ms,让数据输出不要太快 } }主循环里的几个技巧:
- 数据过滤:超声波模块在近距离(<2cm)和远距离(>400cm)时,测量结果可能不稳定或无效。我加了一个简单的范围判断,只输出0.1cm到400cm之间的数据。你也可以根据实际应用调整。
- 测量间隔:HC-SR04模块两次测量之间需要一定的间隔(数据手册建议至少60ms),否则前一次测量的回波可能会干扰下一次。我这里延时了100ms,既保证了模块稳定工作,也让串口输出不至于刷屏太快。
- 错误处理:当测量超时或计算出的距离异常时,打印错误信息,便于调试。
5. 系统调试与精度优化实战
代码写完了,烧录进去,可能一次成功,也可能遇到各种问题。下面是我在调试过程中总结的几个常见问题和优化方法。
5.1 常见问题排查清单
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 完全没数据,串口无输出 | 1. 电源或GND未接好。 2. Trig/Echo线接反或接触不良。 3. 串口配置错误(波特率、引脚)。 4. 程序未运行(Boot引脚配置?)。 | 1. 用万用表检查VCC和GND电压。 2. 交换Trig和Echo线试试,或检查杜邦线。 3. 用PC串口助手查看是否有任何数据,确认波特率。 4. 检查BOOT0/BOOT1引脚是否为正常启动模式(通常都接地)。 |
| 串口有输出,但一直是“Measurement Error” | 1. Echo信号未返回(模块损坏或前方无障碍物)。 2. Echo引脚电平不兼容(5V模块直接接3.3V IO)。 3. 定时器输入捕获配置错误(通道、极性)。 4. 中断未正确进入。 | 1. 用手放在模块前方近距离遮挡,看是否有数据。 2.重点检查!用示波器或逻辑分析仪看Echo引脚波形。如果没有示波器,可以尝试在Echo和STM32 IO之间串联一个1k-2.2k电阻到地,做一个简易分压。 3. 检查 TIM_ICInit函数参数,特别是通道和极性设置。4. 在中断服务函数里设置一个断点或翻转一个LED,看是否能进入。 |
| 测量数据跳动很大,不稳定 | 1. 电源噪声。 2. 环境声波干扰(其他超声波源、噪音)。 3. 测量物体表面不平或吸声。 4. 软件滤波不足。 | 1. 给模块的VCC和GND之间并联一个10uF电解电容和一个0.1uF瓷片电容,紧贴模块引脚。 2. 避开其他超声波设备(如另一个HC-SR04)。尝试在安静的室内测试。 3. 对准平整、坚硬的物体(如墙壁)测试。 4. 在软件中做滑动平均滤波:连续采样N次(如5次),去掉最大最小值,求平均。 |
| 测量值整体偏大或偏小 | 1. 声速系数不准确(受温度影响)。 2. 定时器时钟频率计算错误。 3. 触发脉冲宽度不足。 | 1. 用尺子实际测量一个固定距离(如50.0cm),根据测得的时间反算实际系数,替换ULTRASONIC_SPEED_COEF。2. 确认系统时钟和定时器分频设置。用示波器测量一个GPIO翻转的时间来验证1us延时是否准确。 3. 确保Trig高电平脉冲在10us-20us之间,可以用 Delay_us(12)。 |
| 近距离(<5cm)测不准或报错 | 1. HC-SR04本身有盲区(约2cm)。 2. 回波信号与触发信号太近,可能被模块内部电路抑制。 | 1. 这是模块硬件限制,避免测量小于2-3cm的距离。 2. 在软件中,如果计算出的时间差极小(如对应距离<2cm),直接作为无效数据处理。 |
5.2 软件滤波算法提升稳定性
原始数据难免有毛刺,加入一个简单的滑动平均滤波,效果会好很多。我们在驱动层之上做一个应用层的滤波处理。
#define FILTER_SIZE 5 typedef struct { float buffer[FILTER_SIZE]; uint8_t index; float sum; } UltrasonicFilter_t; static UltrasonicFilter_t s_filter; void Filter_Init(UltrasonicFilter_t *filter) { for (int i = 0; i < FILTER_SIZE; i++) { filter->buffer[i] = 0.0f; } filter->index = 0; filter->sum = 0.0f; } float Filter_Update(UltrasonicFilter_t *filter, float new_value) { // 减去最早的值,加上最新的值 filter->sum -= filter->buffer[filter->index]; filter->sum += new_value; filter->buffer[filter->index] = new_value; filter->index++; if (filter->index >= FILTER_SIZE) { filter->index = 0; } // 返回平均值 return filter->sum / FILTER_SIZE; }在主循环中这样使用:
// 初始化滤波器 UltrasonicFilter_t dist_filter; Filter_Init(&dist_filter); while (1) { float raw_dist = Ultrasonic_MeasureDistance(); if (raw_dist > 2.0f && raw_dist < 400.0f) { // 初步过滤 float filtered_dist = Filter_Update(&dist_filter, raw_dist); printf("Raw: %.2f cm, Filtered: %.2f cm\r\n", raw_dist, filtered_dist); } Delay_ms(100); }5.3 进阶优化:温度补偿与自动校准
如果项目用于室外或温差大的环境,温度补偿就很有必要了。可以增加一个DS18B20之类的温度传感器,实时获取环境温度,动态计算声速。
float Get_SpeedOfSound_Correction(float temperature_c) { // 声速 V = 331.4 + 0.6 * T (m/s) return 331.4f + 0.6f * temperature_c; } float Calculate_Distance_With_Temp(uint32_t time_us, float temperature_c) { float speed_ms = Get_SpeedOfSound_Correction(temperature_c); // m/s float speed_cm_per_us = speed_ms * 100.0f / 1e6f; // 转换为 cm/us float distance_cm = (speed_cm_per_us * time_us) / 2.0f; return distance_cm; }在Ultrasonic_MeasureDistance函数中,获取温度值temp,然后用Calculate_Distance_With_Temp(time_diff, temp)代替原来的简单除法。
6. 项目扩展与进阶思路
这个基础项目做好后,你可以在此基础上玩出很多花样:
- 多路超声波:如果需要同时测量多个方向的距离(比如机器人避障),可以复用同一个定时器的不同通道(如TIM2的CH1, CH2, CH3, CH4)来捕获多个Echo信号,或者使用多个定时器。注意Trig触发要分时进行,避免相互干扰。
- 与显示设备结合:将距离数据实时显示在OLED屏幕上(SSD1306),或者用LCD屏显示一个简单的雷达扫描图。
- 报警功能:设置一个距离阈值(比如小于20cm),当物体靠近时,让STM32控制一个蜂鸣器响或者LED亮,做一个简易的倒车雷达。
- 数据上传:通过串口将数据发送到上位机(PC)进行图形化显示,或者通过ESP8266/ESP32模块上传到物联网平台,实现远程监控。
- 改用HAL库:如果你习惯用CubeMX和HAL库,思路完全一样。用CubeMX配置好定时器的输入捕获通道,生成代码后,重点实现
HAL_TIM_IC_CaptureCallback这个回调函数,在里面处理上升沿和下降沿的逻辑。
最后,关于开发环境,Keil MDK是经典选择,但也可以尝试VSCode + PlatformIO或者STM32CubeIDE,后者是ST官方推出的免费IDE,基于Eclipse,集成CubeMX,用HAL库开发非常方便。选择哪个取决于你的习惯和项目需求。对于学习底层寄存器操作和理解原理,标准库和Keil的组合非常直接;对于快速原型开发,CubeMX+HAL库效率更高。
这个项目的所有代码和工程文件,我都整理好了。如果你在复现过程中遇到任何问题,或者有更好的实现思路,欢迎一起交流。嵌入式开发就是这样,从一个个小模块入手,把每个细节搞明白,积累起来就能做出更复杂的系统了。
本文还有配套的精品资源,点击获取