一、前言
在本节学习中,先带大家认识超声波的测距功能以及应用,再介绍一下配置时需要用到的STM32的输入捕获模式,最后配置代码实现超声波进行测距,完成一个超声波测距项目。在这个项目开始之前,需要掌握定时器如何配置的知识点,不会的同学可以看上一篇文章。
二、超声波简介
什么是超声波?
超声波是一种频率高于人耳可听范围(大约20 kHz以上)的声波。我们本章介绍是HC_SR04,它是一种常见的超声波传感器模块,用来测量障碍物与传感器的距离。
什么情况下使用超声波?
在人们日常生活中,测量距离(汽车的倒车雷达)、检查物体的内部结构(像超声波检查胎儿那样),甚至清洁精密的物品。
HC_SR04测距实现
接下来我们将详细介绍如何实现超声波测距。
超声波测距原理
超声波测距的基本原理是首先发出一定频率的超声波,超声波遇到障碍物后反射回来,而当模块接收到反射回来的超声波后,只要将超声波从发送到接收的时间差乘以声速除以2就可以测到与障碍物的距离了。
公式:
距离=时间差×声速÷2
其中,声速在空气中的速度约为340米/秒(取决于温度和湿度),时间差是超声波从发射到接收的总时间,除以2是因为超声波往返经过了该距离两次。
详细给大家讲一下这个时间差是如何得到的?(配合代码)
(1)HC_SR04上的Trig引脚发送一个高脉冲信号(如上图代码所示)时,Echo瞬间拉高电平
(2)此时在传输高脉冲信号的过程中,Trig还是低电平,Echo保持高电平
(3)当传感器接收到从物体反射回来的超声波时,Echo引脚会被拉低为低电平,Echo上高电平的持续时间就是超声波在往返路途中消耗的时间。
TRIG_Send(1);
us_delay(20);
TRIG_Send(0);
上面这三行代码表示的就是Trig发送一个高脉冲信号。
!!!需要注意的事:由于声波扩散传播的特性,如果超声波扩散范围内有比被测物体更近的障碍物,超声波就会被提前反射,测得的就是模块到障碍物的距离了。因而在在使用超声波测距时,最好只有一个被测物障碍物,这样才能提高数据的正确性。
HC_SR04硬件配置
-
超声波传感器(HC-SR04)
- Trig引脚:用于发送超声波信号的触发引脚。
- Echo引脚:用于接收反射回来的超声波信号的引脚。
- GND引脚:接地线
- VCC引脚:供给电压5v
-
STM32微控制器
- 选择任意两组GPIO引脚,一组作为Trig输出引脚,另一组作为Echo输入引脚。
- 使用定时器(比如TIM4)来计算时间间隔。
三、输入捕获模式
为什么要引用输入捕获模式?
根据上面我们需要计算的时间Echo的高电平持续时间构成的,所以我们需要去测量Echo引脚上高电平的持续时间,听着很简单,不就是去检测Echo口引脚上的电平变化吗?只要当读取到Echo引脚上是高电平时,就让定时器开始计数;当读取到Echo引脚上是低电平时,就读取定时器的数值,就能得到我们想要的时间差,但是这样也有一个很大的弊端:程序软件循环读取GPIO硬件是否检测到高/低电平,再读取此时定时器的数值,需要耗费时间,使得数据不够正确严谨。因此我们需要使用一个得到时间更加精确的方式--使用输入捕获模式。
用一句话介绍输入捕获模式:当定时器输入通道上检测到上升沿(或者下降沿时),立刻将此时计数器的数值记录到捕获寄存器中,以待程序读取。
对于通用定时器和高级定时器来说,每个输入通道都有它自己的捕获寄存器。输入捕获可以在输入信号上出现上升沿或者下降沿时,将此时计数器的值捕获到捕获寄存器中,并且要借用另一个输入通道的捕获寄存器进行输入捕获,捕获到自己的捕获寄存器中叫做直接模式,借用另一个通道捕获寄存器叫做间接模式。
高级定时器:STM32 的 TIM1, TIM8
通用定时器:STM32 的 TIM2, TIM3, TIM4, TIM5
图片来源:哔哩哔哩上keysking博主
再给大家详细的介绍一下输入捕获模式的整体运行方式:
如图,我们使用输入通道TI1的输入捕获模式,边沿检测器设为上升沿,定时器启动计数后,若输入到输入通道TI1的信号出现一个上升沿,进入输入滤波器(去除输入信号中的噪声、干扰),边沿检测器立即检测到上升沿,随后一个触发信号通过TI1FP1,进入预分频器进行处理信号频率,传递到捕获寄存器1,最后捕获寄存器检测到直接将计数器的值赋值到自身,并且将计数器设置为0,这样我们就得到了上升沿出现时定时器的时刻,但是一个边沿检测器只能设置为上升沿/下降沿,无法设为双边沿。我们就需要在边沿检测器上再引用一条线TI1FP2,借用捕获寄存器2,并且设置为下降沿捕获,获取到下降沿出现时定时器的时刻,二者相减,我们就能获取到高电平的持续时间啦。
四、代码配置
我们这里要用到main.c、main.h、HC_SR04.c、HC_SR04.h、SysTick.h、SysTick.c。SysTick.h、SysTick.c这两个文件的代码配置我不给大家介绍,但会附上相关代码。大家只要知道这个只是让我们能使用延时函数就行了。
HC_SR04.c文件
#include "HC_SR04.h"
#include "stm32f10x.h"
#include "SysTick.h"//用于延时的头文件,提供了一些所需要的延时函数
extern uint16_t mscount = 0;
void HC_SR04Config(void)
{
GPIO_InitTypeDef GPIO_hcsr04init;
TIM_TimeBaseInitTypeDef TIM_hcsr04init;
NVIC_InitTypeDef NVIC_hcsr04init;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
//Trig PB11
GPIO_hcsr04init.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_hcsr04init.GPIO_Pin = GPIO_Pin_11;//配置GPIOB的第11引脚为推挽输出模式,连接HC-SR04
//Trig引脚,用来触发超声波信号。
GPIO_hcsr04init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_hcsr04init);
//ECHO PB10
GPIO_hcsr04init.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_hcsr04init.GPIO_Pin = GPIO_Pin_10;//配置GPIOB的第10引脚为浮空输入模式,连接HC-SR04
//Echo引脚,用来接收超声波返回信号。
GPIO_Init(GPIOB, &GPIO_hcsr04init);
//配置定时器TIM4
TIM_hcsr04init.TIM_ClockDivision = TIM_CKD_DIV1 ;
TIM_hcsr04init.TIM_CounterMode = TIM_CounterMode_Up;
TIM_hcsr04init.TIM_Period = 1000-1;//定时器每计数1次表示1微秒。
TIM_hcsr04init.TIM_Prescaler = 72-1;//预分频器为71(72MHz / 72 = 1MHz)
TIM_TimeBaseInit(TIM4,&TIM_hcsr04init);
//配置定时器溢出中断,并禁用定时器(等到需要时启用)。
TIM_ITConfig(TIM4,TIM_IT_Update, ENABLE);
TIM_Cmd(TIM4,DISABLE);
//配置定时器溢出中断的NVIC优先级和使能TIM4的中断。
NVIC_hcsr04init.NVIC_IRQChannel = TIM4_IRQn ;
NVIC_hcsr04init.NVIC_IRQChannelPreemptionPriority = 0 ;
NVIC_hcsr04init.NVIC_IRQChannelSubPriority = 0;
NVIC_hcsr04init.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_hcsr04init);
}
//Open_tim4()函数启用TIM4定时器,并将计数器清零。
void Open_tim4(void)
{
TIM_SetCounter(TIM4,0);
mscount = 0;//mscount用于记录中断的次数。
TIM_Cmd(TIM4,ENABLE);
}
//Close_tim4()函数关闭TIM4定时器,停止计时。
void Close_tim4(void)
{
TIM_Cmd(TIM4,DISABLE);
}
//这是定时器溢出中断处理函数,每次定时器溢出时,mscount的值会增加1。
void TIM4_IRQHandler(void)
{
if( TIM_GetITStatus(TIM4,TIM_IT_Update)!=RESET)
{
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
mscount++;
}
}
//该函数用于获取Echo信号的返回时间,计算总的时间(以微秒为单位),并将定时器的计数器清零。
int GetEcho_time(void)
{
uint32_t t = 0;
t = mscount*1000;
t += TIM_GetCounter(TIM4);
TIM4->CNT = 0;
ms_delay(50);
return t;
}
//获取超声波的距离
float Getlength(void)
{
int i = 0;
uint32_t t = 0;
float length = 0;
float sum = 0;
while(i != 5)
{
TRIG_Send(1);
us_delay(20);
TRIG_Send(0);
while(ECHO_Reci == 0);
Open_tim4();
i = i+1;
while(ECHO_Reci == 1);
Close_tim4();
t = GetEcho_time();
length = ((float)t / 58.0);
sum = sum+length;
}
length = sum / 5.0;//取五次距离总和的平均距离
return length;
}
基于这个代码很多我给大家详细介绍一下
代码结构:
1. 启用和关闭定时器 (TIM4)
Open_tim4():
启用定时器 TIM4 并将计数器清零。定时器开始计时,用于计算从发射信号到接收信号的时间间隔。
Close_tim4():关闭定时器 TIM4,停止计时。
2. 定时器溢出中断处理 (TIM4_IRQHandler)
当定时器溢出时,mscount 会增加 1。
3. 计算回波时间 (GetEcho_time)
GetEcho_time():这个函数会返回从定时器开始计时到当前时间点的总时间。该时间是从上次定时器重置后计数的时间。此函数将定时器的计数器清零,确保下一次计时是从零开始。
4. 获取超声波的距离 (Getlength)
Getlength():这个函数是发送信号并接收回波信号,通过计算往返时间来得出距离。这里通过循环多次(5次),每次测量后取平均值来减少误差。
难点/疑惑点
extern uint16_t mscount = 0;
mscount 是一个外部变量,在
TIM4_IRQHandler
中更新,用来计算中断次数。我们得到的时间实际是用t来表示
int GetEcho_time(void)
{
uint32_t t=0;
t=mscount*1000;
t+=TIM_GetCounter(TIM4);
TIM4->CNT =0;
ms_delay(50);
return t;
}这段代码的意思实际是总时间t=中断次数*1000+计数器的时间
中断次数n*1000实际为n/毫秒
计数器的时间为不足为1的毫秒
这是定时器溢出中断处理函数,每次定时器溢出时,mscount的值会增加1。
void TIM4_IRQHandler(void)
{
if( TIM_GetITStatus(TIM4,TIM_IT_Update)!=RESET)
{
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
mscount++;
}
}if( TIM_GetITStatus(TIM4,TIM_IT_Update)!=RESET)这个代码意思是定时器溢出了(不为0)
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);清除中断标志位
while(i != 5)
{
TRIG_Send(1);//Trig引脚发送高脉冲信号
us_delay(20);
TRIG_Send(0);
while(ECHO_Reci == 0);
Open_tim4();
i = i+1;
while(ECHO_Reci == 1);
Close_tim4();
t = GetEcho_time();
length = ((float)t/58.0);
sum = sum+length;
}
length = sum/5.0;//取五次距离总和的平均距离
return length;while(ECHO_Reci == 0);当Trig发送一个高脉冲信号时,Echo立马变成高电平,此时Echo的电平为1,则这行代码通过,执行下面的代码
while(ECHO_Reci == 1);Echo接收到trig发送回来的高脉冲信号,然后Echo有下降沿(高电平1变成低电平0),才会开始执行这个while语句下面的代码
HC_SR04.h文件
#include "stm32f10x.h"
void HC_SR04Config(void);
void Open_tim4(void);//开启TIM4定时器,并将计数器清零。这用于开始测量Echo信号的持续时间。
void Close_tim4(void);//关闭TIM4定时器。这用于结束测量Echo信号的持续时间。
int GetEcho_time(void);//获取Echo信号的持续时间。通过读取定时器计数器的值和中断次数来计算Echo信号 //的时间。
float Getlength(void);//计算并返回距离。通过多次触发测量,获取多次Echo信号的时间,求平均值,提高
//测量的准确性。
void TIM4_IRQHandler(void);//IM4定时器中断处理函数,每当定时器溢出时会调用此函数。在函数内部,清 //除中断标志位并增加计数器变量 mscount,用于记录溢出次数。
#define ECHO_Reci GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10)
#define TRIG_Send(a) if(a) \
GPIO_SetBits(GPIOB, GPIO_Pin_11); \
else \
GPIO_ResetBits(GPIOB, GPIO_Pin_11);
难点/疑惑点
#define ECHO_Reci GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10)
定义了一个宏
ECHO_Reci
,用于读取GPIOB端口第10引脚的输入状态。这个宏方便了读取Echo引脚状态的操作。
#define TRIG_Send(a) if(a) \
GPIO_SetBits(GPIOB, GPIO_Pin_11); \
else \
GPIO_ResetBits(GPIOB, GPIO_Pin_11);
定义了一个宏
TRIG_Send(a)
,用来设置或重置GPIOB端口第11引脚的输出状态。这个宏通过if-else
语句实现:
- 如果
a
为真(非零),则调用GPIO_SetBits(GPIOB, GPIO_Pin_11)
将引脚设置为高电平。- 否则,调用
GPIO_ResetBits(GPIOB, GPIO_Pin_11)
将引脚设置为低电平。
main.c文件
#include "stm32f10x.h"
#include "main.h"
#include "HC_SR04.h"
#include "SysTick.h"
void delay(uint16_t time)
{
uint16_t i = 0;
while(time--)
{
i = 12000;
while(i--);
}
}
int main()
{
float Length = 0;
HC_SR04Config();
while(1)
{
Length = Getlength();
printf("%.3f\r\n",Length);//表示保留三位小数,并在每次输出后换行。
ms_delay(500);
}
}
float Length = 0;
定义了一个浮点型变量
Length
,用于存储测量到的距离值。用浮点型是因为距离可能包含小数部分,而浮点型(float
)可以存储小数值。int储存整数值
ms_delay(500);
我这里使用了
ms_delay
函数,其功能根据SysTick定时器实现的一个精确延时函数。我将在下面给代码发给大家,但我不做讲解,大家自行观看
SysTick.c文件
#include "SysTick.h"
#include "stm32f10x.h"
void ms_delay(uint32_t ms)
{
uint32_t i;
SysTick_Config(72000);
for(i=0;i<ms;i++)
{
while(!((SysTick->CTRL)&(1<<16)));
}
SysTick->CTRL &=-SysTick_CTRL_ENABLE_Msk;
}
void us_delay(uint32_t us)
{
uint32_t i;
SysTick_Config(72);
for(i=0;i<us;i++)
{
while(!((SysTick->CTRL)&(1<<16)));
}
SysTick->CTRL &=-SysTick_CTRL_ENABLE_Msk;
}
SysTick.h文件
#include "stm32f10x.h"
void ms_delay(uint32_t ms);
void us_delay(uint32_t us);
这里数据的展现,我就不给大家展示了,大家可根据上面的代码自行配置,用串口或者数码管能看到与最近的障碍物之间的距离。