第四讲——定时器输入捕获+超声波测距
往期文章
STM32LL库编程系列第一讲——Delay精准延时函数(详细,适合新手)
STM32LL库编程系列第二讲——蓝牙+USART串口通信(步骤详细、原理清晰)
STM32LL库编程系列第三讲——USART+DMA通信
文章目录
前言
本文使用的控制板为本人自行设计,核心芯片为STM32F407VET6,有很大的通用性。
编程逻辑:利用定时器9的输入捕获功能捕获超声波回传信号的双边沿,从而计算高电平持续时间,结合声速得到测距,在利用DMA将测距信息发送到USRT3的DR寄存器,从而让上位机串口助手接收到。(对DMA、USART外设不了解可参考往期文章)
本文会对每一步操作详细说明,代码逻辑清晰,希望能对大家有所帮助!!
一、定时器输入捕获逻辑
如图所示,这是定时器的一个输入捕获通道,
TI1为输入信号,
f(DTS)是由TIMx_CR1寄存器的CKD设置的,指示定时器时钟 (CK_INT) 频率与数字滤波器所使用的采样时钟 (TIx) 之间的分频比,简单理解就是 f(DTS) = f(CK_INT) /n
,n就是分频比。
ICF[3:0]设置定义 TI1 输入的采样频率和适用于 TI1 的数字滤波器带宽,简单理解就是ICF[3:0]=0~15,表示每采样到ICF[3:0]个TI1的电平信号,才会输出一个电平信号。比如ICF[3:0] = 4,则当4次采样到TI1为高电平或低电平,TI1F才为高电平或低电平。若4此采样中TI1有高电平也有低电平,则TI1F保持上一轮采样值,这样可以滤除高频干扰,故称为滤波器。
TI1F经过边沿检测器判断是上升沿还是下降沿,经过双通道输出符合要求的电平信号TI1FP1,再经过三通道输出IC1,最后经过一个分频器输出IC1PS。假设分频器为4,则表示4个IC1脉冲信号才输出一个IC1PS信号。
这里对定时器输入捕获模式进行简单讲解,下面可以具体操作了
二、使用CubeMX建立工程
这里只说明关于定时器部分的配置介绍,其他外设配置说明见往期文章
定位到TIM9的配置界面,打开内部时钟,将通道2配置为输入捕获模式,如图
参数设置如下
TIM9时钟频率为168MHz,这里进行168分频,从而 f(CK_INT) =1MHz。
向上计数
重装载值设置为20000,则最大计时时间为20000/1000000=20ms,声波传输距离为0.02*340=6.8m,从而最大测距距离为3.4m,足够了,我这是使用的超声波US100是非常普通常见的一款超神波模块,和蓝桥杯单片机的超生波模块几乎一样,因为功率低,一般距离大于3M,声波就衰减的无法检测了。
CKD就是上面介绍的定时器时钟 (CK_INT) 频率与数字滤波器所使用的采样时钟 (TIx) 之间的分频比,这里不分频。因为预分频和预装载值已经设置好需要的频率了。
使能重装载
设置边沿检测为双边沿检测
Prescaler Division Ratio为分频器分频系数,不分频。来一个我就要一个
Input Filter(4bits value)就是滤波器的带宽系数,具体解释可以看第六讲,当频率不高时可以适当滤波,高频率滤波可能会滤去脉冲信号,这里不滤波也可以使用,就先不滤波了。
使能全局中断,如图
设置超声波模块的信号触发引脚
编辑定时器引脚电平
设置串口通信引脚,如图
这里进行一点重要说明
对于输入引脚,如果没有外部上下拉,就一定要内部上下拉,避免空闲时误触发,具体上拉还是下拉根据需要的空闲状态而定。
而输出引脚可以不上下拉,当然,如果有空闲状态,最好拉置空闲状态。
配置LL库输出,至此CubeMX配置结束(这里跳了很多中间步骤,如果结合往期文章还是无法理解,结尾会给出工程下载地址,可以下下载查看)
三、keil工程代码编写
1.电平触发测距工作原理
图 5.1 表明:只需要在 Trig/TX 管脚输入一个 10US 以上的高电平,系统便可发出 8 个 40KHZ 的超声波脉冲,然后检测回
波信号。当检测到回波信号后,模块还要进行温度值的测量,然后根据当前温度对测距结果进行校正,将校正后的结果通过
Echo/RX 管脚输出。
在此模式下,模块将距离值转化为 340m/s 时的时间值的 2倍,通过 Echo 端输出一高电平,可根据此高电平的持续时间来
计算距离值。即距离值为:(高电平时间*340m/s)/2。
注:因为距离值已经经过温度校正,此时无需再根据环境温度对超声波声速进行校正,即不管温度多少,声速选择340m/s 即可。
2.US100.c
这里放入超声波Trig/TX 管脚输入一个 10US 以上的高电平函数
#include "US100.h"
#include "delay.h"
void US100_Trig(GPIO_TypeDef *GPIOx, uint32_t PinMask)
{
LL_GPIO_SetOutputPin(GPIOx,PinMask);
delay_us(15);
LL_GPIO_ResetOutputPin(GPIOx,PinMask);
}
这里稍微延时比10US多一点,避免漏检测。US100.h如下
#ifndef __US100_H
#define __US100_H
#include "main.h"
void US100_Trig(GPIO_TypeDef *GPIOx, uint32_t PinMask);
#endif
3.tim.c
再生成的初始化函数下面加入使能函数
/* USER CODE BEGIN TIM9_Init 2 */
LL_TIM_ClearFlag_CC2(TIM9);//清除通道2捕获标志位
LL_TIM_EnableIT_CC2(TIM9);//使能通道2捕获中断
LL_TIM_EnableIT_UPDATE(TIM9);//使能更新中断
LL_TIM_CC_EnableChannel(TIM9,LL_TIM_CHANNEL_CH2);//使能TIM9_CH2捕获功能
/* USER CODE END TIM9_Init 2 */
我们需要通道2的输入捕获功能,就得使能该功能和中断,使能更新中断为了到最大计数值依旧没有检测回传声波处理。
4.dma.c
编写USART3的DMA发送函数
/* USER CODE BEGIN 2 */
void usart3_DMA_init(void)
{
//DMA接收配置
LL_DMA_SetPeriphAddress(DMA1, LL_DMA_STREAM_3, (uint32_t)&(USART3->DR));//设置外设地址
LL_DMA_SetMemoryAddress(DMA1, LL_DMA_STREAM_3, (uint32_t)US100B_DIST_cm);//设置内存地址
LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_3, 6);//设置接受的数据长度
//清除中断标志位
LL_USART_ClearFlag_TC(USART3);
LL_DMA_ClearFlag_TC3(DMA1);
LL_DMA_EnableIT_TC(DMA1,LL_DMA_STREAM_3);
LL_USART_EnableDMAReq_TX(USART3);//启用串口DMA发送模式
}
/* USER CODE END 2 */
只要开启了使能就会把US100B_DIST_cm传入USART3->DR中
5.stm32f4xx_it.c
定义参数
/* USER CODE BEGIN TD */
unsigned char US100B_DIST_cm[6]={0,0,0,'c','m',(u8)10};
u16 US100B_DIST;
/* USER CODE END TD */
编写TIM9中断服务函数
/* USER CODE BEGIN TIM1_BRK_TIM9_IRQn 0 */
if(LL_TIM_IsActiveFlag_CC2(TIM9))//检测到捕获事件
{
LL_TIM_ClearFlag_CC2(TIM9);
if(LL_GPIO_IsInputPinSet(US100_OUTB_GPIO_Port,US100_OUTB_Pin)==SET)//检测为高电平,则上升沿捕获
{
LL_TIM_EnableCounter(TIM9);//开启定时器计数
}
//若一致都是高电平,在下一轮开启采集前会自动产生下降沿
if(LL_GPIO_IsInputPinSet(US100_OUTB_GPIO_Port,US100_OUTB_Pin)==RESET)//检测为低电平,则下升沿捕获
{
LL_TIM_DisableCounter(TIM9);//关闭定时器计时
US100B_DIST = LL_TIM_GetCounter(TIM9)*340/2/10000;
US100B_DIST_cm[0] = (US100B_DIST/100)+48;
US100B_DIST_cm[1] = (US100B_DIST/10%10)+48;
US100B_DIST_cm[2] = (US100B_DIST%10)+48;
LL_TIM_SetCounter(TIM9,0);//将计数器值清0
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_3);//使能DMA数据流
}
}
if(LL_TIM_IsActiveFlag_UPDATE(TIM9))
{
LL_TIM_ClearFlag_UPDATE(TIM9);
LL_TIM_DisableCounter(TIM9);//关闭定时器计时
}
/* USER CODE END TIM1_BRK_TIM9_IRQn 0 */
当触发边沿中断后先清除中断标志位,再从电平位确定是上升沿中断还是下降沿中断,再上升沿中断里开启计数器计时,到下降沿中断
里关闭计数器计时,从而得到回传信号高电平持续时间,LL_TIM_GetCounter(TIM9)用来检测计数器值,经过换算得到测距距离US100B_DIST 单位是cm,将US100B_DIST 每一位分别存入数组,+48是位了转换为ASCII码。然后将将计数器值清0,为下次采集做准备。最后使能DMA数据流传输,将US100B_DIST_cm数组值自动传输到USART3->DR中。
在非循环模式下配置数据流,传输结束后(即要传输的数据数目达到零),除非软件重新对数据流编程并重新使能数据流(通过将 DMA_SxCR 寄存器中的 EN 位置 1),否则 DMA 即会停止传输(通过硬件将 DMA_SxCR 寄存器中的 EN 位清零)并且不再响应任何
DMA 请求。故每次都发送前都需要使能LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_3);//使能DMA数据流
这也充当发送开始标志
如果到了20ms还没有检测到下降沿,表示距离过远,没收到回传声波,从而进入更新中断,在更新中断里关闭定时器计时,此时会计数达到最大值会自动清0,在下一轮开启采集前会自动产生下降沿,从而得到的US100B_DIST =0。
编写DMA中断服务函数
void DMA1_Stream3_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Stream3_IRQn 0 */
if(LL_DMA_IsActiveFlag_TC3(DMA1))
{
LL_DMA_ClearFlag_TC3(DMA1);
}
/* USER CODE END DMA1_Stream3_IRQn 0 */
/* USER CODE BEGIN DMA1_Stream3_IRQn 1 */
/* USER CODE END DMA1_Stream3_IRQn 1 */
}
当DMA数据流传输完成时就会进入TC完成中断服务函数,在中断服务函数清除标志位即可。这里的TC中断什么都没干,那为什么要留着呢?我们打开DMA串口传输数据流可以看出当传输完成时TC会硬件置1,如果不开启中断将标志位置0,那么下次传输就无法进行了。所以DMA串口发送必须使能TC中断
6.main.c
加入DMA串口发送初始化
/* USER CODE BEGIN 2 */
usart3_DMA_init();
/* USER CODE END 2 */
while (1)
{
US100_Trig(US100_INB_GPIO_Port, US100_INB_Pin);
delay_ms(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
每500ms测距一次。
四、效果展示
如图
大家根据自己的控制板逻辑进行连接,从串口助手可以看到不断有数据回传。
五、工程下载
链接:https://pan.baidu.com/s/1ULmQgEzNXqGgtEjXVVVFAg?pwd=1234
提取码:1234
尾言
对于DMA的数据收发LL库编程网上资源特别少,有那么几个也都含糊其辞,本人也是花了大量时间查看手册才弄清逻辑,大家可以结合上文,对DMA进行学习。
时间匆忙,有错误之处还望大家指出,有疑问可以在评论区发表,我看到就会回复(文章发表短期内)。