STM32输入捕获采集超声波模块HC-SR04响应的高电平
所用单片机:STM32F103C8T6
超声波测距模块链接(拼多多):https://mobile.yangkeduo.com/mall_page.html?ps=WIvk5gnEbW
一、模块使用说明
该链接的超声波模块---HC-SR04支持3种接口模式:GPIO、UART、IIC。本文章仅对常用的GPIO口驱动模式进行说明,读者可自行验证其它接口的可行性。
1.1 特性说明
1.2 工作方式
如果要用其它接口模式的话,需要对模块的电阻进行配置,如下图:
GPIO口驱动的工作方式为,单片机给超声波模块Trig引脚发送一个大于10us的高电平,触发HC-SR04发出8个40kHz的方波,并自动检测是否有信号返回,如果有信号返回,就会通过Echo对单片机输出一个高电平,高电平的持续时间就是超声波从发射到返回的时间。 如下时序图;
必须要注意的一点是,在下面的时序图中也可以看出,单片机给Trig引脚发送10us的高电平后,紧接着要给这个引脚发送低电平;周期最小是200ms!!!也就是说给Trig引脚发送高电平后,不能立刻就读取Echo引脚返回的高电平持续时间!!
等待Trig一个周期完后,检测Echo引脚高电平的持续时间T,如前所说,高电平的持续时间T就是超声波从发射到返回的时间;那么只要知道声波的速度C,用T*C/2就能得到设备与障碍物的距离D。商家提供的声速计算如下:
二、定时器输入捕获检测Echo高电平的原理
定时器输入捕获原理看B站这个up,几分钟就说明白了:【【STM32】动画讲解输入捕获 并实现超声波测距】https://www.bilibili.com/video/BV1HM4m1R75B/?share_source=copy_web&vd_source=91acb6de0cf2d9aa6a14eec0c8d82cdb
假定定时器工作在向上计数模式,上图中的t1~t2时间段就是我们要测量的高电平持续时间。
首先设置定时器输入通道X为上升沿捕获,这样t1时刻就会捕获到当前的cnt值。为了方便后续的计算,此时要将计数器的值清零,并且设置定时器输入通道X为下降沿捕获。等到t2时刻,又会触发下降沿捕获,此时的CNT值就是除去溢出时间外的高电平持续时间,记作CCR,再将输入通道X设为上升沿捕获,如此循环。。。。
如果障碍物距离实在太远,可能导致Echo引脚返回的高电平时间实在太长,以至于超出了定时器重装载计数器的最大值0xffff,此时就会发生溢出。如果代码没有设计考虑溢出的话,就会导致误差过大(整个溢出时间的高电平)。
因此,驱动这个模块,不仅要使用定时器输入捕获中断,还要使用定时器更新中断,记录溢出的次数。最终的脉宽时间 = 溢出次数 *(计数器重装载值+1)+ (CCR+1)
三、参考代码
3.1 硬件驱动实现
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "stdbool.h"
#include "delay.h"
#define TIM_PERIOD 0XFFFF
static void GpioCofig(void)
{
/*GPIO时钟使能*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/*GPIO引脚配置*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; //超声波Trig触发引脚
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_WriteBit(GPIOB, GPIO_Pin_13, Bit_RESET);//默认上电不触发
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPD;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8; //超声波Echo引脚,默认下拉输入0
GPIO_Init(GPIOA, &GPIO_InitStruct);
}
static void TimerCofig(void)
{
/*- - - - - - - -使能定时器时钟- - - - - - - - */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
TIM_InternalClockConfig(TIM1);
/*- - - - - - - -时基单元初始化- - - - - - - - */
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStruct.TIM_Period = TIM_PERIOD;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
/*- - - - - - - -输入捕获结构体初始化- - - - - - - - */
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICStructInit(&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;//上升沿捕获
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;//影子寄存器cc1映射到捕获通道TI1
TIM_ICInit(TIM1, &TIM_ICInitStruct);
/*设置中断*/
TIM_ClearFlag(TIM1, TIM_FLAG_Update | TIM_IT_CC1);//清除更新和捕获中断标志位
TIM_ITConfig(TIM1, TIM_IT_Update | TIM_IT_CC1, ENABLE);
/*- - - - - - - -NVIC配置- - - - - - - - */
// NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//一个工程只分组一次
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = TIM1_CC_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel = TIM1_UP_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
NVIC_Init(&NVIC_InitStruct);
/*- - - - - - - -定时器使能,开始计数- - - - - - - - */
TIM_Cmd(TIM1, ENABLE);
}
/**
***************************************************
* @brief 超声波测距驱动初始化
* @param
* @return
***************************************************
*/
void UltrasoundDrvInit(void)
{
GpioCofig();
TimerCofig();
}
3.2 功能实现代码
这里我没有使用温度与声速的关系式计算声速,而是直接采取了20℃下的典型值,只是为了方便验证,如果要求更准确可以加温度校准。
typedef struct
{
uint8_t capFinishFlag; //捕获结束标志位
uint8_t capStartFlag; //捕获开始标志位
uint16_t capCcrValue; //捕获寄存器的值
uint16_t capPeriods; //自动重装载寄存器更新溢出次数
}TIM_ICUserValueType_t;
static TIM_ICUserValueType_t g_icValueType =
{
.capFinishFlag = 0,
.capStartFlag = 0,
.capCcrValue = 0,
.capPeriods = 0,
};
/**
***************************************************
* @brief 触发超声波探测
* @param
* @return
***************************************************
*/
void UltrasoundProc(void)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_13, Bit_SET);
DelayNus(20);
GPIO_WriteBit(GPIOB, GPIO_Pin_13, Bit_RESET);
/*应当保证低电平时间符合周期要求,这里我有其它办法,就不用延时死等了*/
}
static bool CalPulseWidth(uint32_t* pulseWidth)//触发探测后,计算高电平的持续时间
{
if (g_icValueType.capFinishFlag == 0)
{
return false;
}
*pulseWidth = g_icValueType.capPeriods * (TIM_PERIOD + 1) + (g_icValueType.capCcrValue + 1);
g_icValueType.capFinishFlag = 0;//捕获结束标志位清零
printf("capPeriods is %d\n",g_icValueType.capPeriods);
return true;
}
/**
***************************************************
* @brief 获取最终和障碍物的距离,cm
* @param
* @return
***************************************************
*/
void GetDistance(float* distance)
{
uint32_t pulseWidth = 0;
// while ( CalPulseWidth(&pulseWidth) == false );
(void)CalPulseWidth(&pulseWidth);
*distance = ((float)pulseWidth*1.0f/10.0f*0.34f)/2.0f;
}
/**
***************************************************
* @brief 定时器1更新中断服务函数
* @param
* @return
***************************************************
*/
void TIM1_UP_IRQHandler(void)
{
if ( TIM_GetITStatus(TIM1, TIM_IT_Update) )
{
/*实现功能*/
g_icValueType.capPeriods ++;
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
}
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
}
/**
***************************************************
* @brief 定时器1输入捕获中断服务函数
* @param
* @return
***************************************************
*/
void TIM1_CC_IRQHandler(void)
{
if ( TIM_GetITStatus(TIM1, TIM_IT_CC1) != RESET )
{
//第一次捕获
if ( g_icValueType.capStartFlag == 0 )
{
TIM_SetCounter(TIM1,0);//计数器清零
g_icValueType.capPeriods = 0;//之前的自动重装载寄存器溢出次数清零
g_icValueType.capCcrValue =0;//存放捕获比较寄存器值的变量清零
TIM_OC1PolarityConfig(TIM1, TIM_ICPolarity_Falling);//第一次捕获到上升沿后就把捕获设置为下降沿
g_icValueType.capStartFlag = 1;//捕获开始标志置一
}
//表示为下降沿捕获中断
else
{
g_icValueType.capCcrValue = TIM_GetCapture1(TIM1);//获取捕获比较寄存器的数值,这就是除去溢出外的高电平时间
TIM_OC1PolarityConfig(TIM1, TIM_ICPolarity_Rising);//把捕获设置为上升沿
g_icValueType.capStartFlag = 0;//开始捕获标志清零
g_icValueType.capFinishFlag = 1;//捕获结束标志置一
}
}
TIM_ClearITPendingBit(TIM1, TIM_IT_CC1);//无条件,必须要
}
3.3 .h头文件
#ifndef _ULTRASOUND_DRV_H_
#define _ULTRASOUND_DRV_H_
#include "stdint.h"
/**
***************************************************
* @brief 超声波测距驱动初始化
* @param
* @return
***************************************************
*/
void UltrasoundDrvInit(void);
/**
***************************************************
* @brief 触发超声波探测,两次触发之间最好间隔60ms,因此函数60ms调用一次
* @param
* @return
***************************************************
*/
void UltrasoundProc(void);
/**
***************************************************
* @brief 获取最终和障碍物的距离,cm
* @param
* @return
***************************************************
*/
void GetDistance(float* distance);
#endif
3.4 主函数测试代码
能够显示数值,验证功能就行,下面的代码仅供参考,需要自己根据自己的工程进行修改。
注意,代码中有个60ms的延时一定是要放在UltrasoundProc();后不可改位置不可删除的,用于保证Trig的触发信号不会影响到Echo的回响信号,也就是之前提到的200ms周期!!
static void DrvInit(void)
{
DelayInit();
SystickInit();
BleDrvInit();
CarDrvInit();
SteeringDrvInit();
UltrasoundDrvInit();
}
static void AppInit(void)
{
RegTaskScheduleCb(TaskScheduleCb);
}
int main(void)
{
DrvInit();
AppInit();
float distance = 1.0f;
while(1)
{
TaskHandler();//这个不用管,忽略,你们不用写
UltrasoundProc();
DelayNms(60);//这个延时十分重要,不可省略,保证周期(虽然不是200ms,也能用)
GetDistance(&distance);
printf("DISTANCE:%.2fcm\r\n\n",distance);//需要会串口,不会可以搜文章、视频,或者用其它显示
distance = 0;
DelayNms(2000);
}
}