3.1.1 任务分析 该任务需要设计一个应用程序,以实现智能汽车循环的周期性状态。任务需要反向使用 作为智能电传感器电路板作为智能光电传感器电路板实现了智能汽车的巡线前进功能。反射红外 如图所示 3-1-1 所示。
反射光电传感器有多种光源,常用的有可见光、红外光和激光。 单反射红外光电传感器配备红外光源和光接收装置,光源发出的光通过待测物体反射 被光接收装置(光敏元件)接收,然后通过信号调理电路处理获取所需信息,一般为数字信息 1或00。利用传感器的这一特性,可以检测地面的亮度和颜色的变化,也可以检测是否有 接近物体。 根据上述反射红外光电传感器的工作原理作原理进行分析,并在智能汽车底盘前部安装反射 红外光电传感器电路板(跟踪电路板) 8 反射红外光电传感器。智能汽车跟踪 具体工作原理如下: ? 红外发射管将光发射到路面,光遇到白底后反射,接收管收到反射光,通过信号调整 处理电路后输出高电平; ? 光线被黑底吸收,接收管未收到反射光,信号调节电路处理后输出低电平; ? 循迹电路板共输出 8 从数字信号到智能汽车的主控板,数字信号1表示白色路面, 数字信号0表示黑色路面; ? 智能小车主控板将收到 8 道路数字信号作为判断车身当前状态的依据,进一步控制电源 机器旋转,使车身返回位置。试验现场路面有黑白两种颜色,其中黑色路面为循环 迹线,宽 30mm,智能汽车可以沿着这条线前进。测试现场如图所示 3-1-2 所示。
综上所述,如果要求智能汽车以一定的速度沿着一定的速度如果黑色跑道向前移动,微控制器需要在短周期内频繁获得循环电路板 8 路数字信号,然后才 及时判断车身状态,调整电机转向。智能汽车的要求可以通过定时器的基本定时功能来实现 因此,本任务涉及的知识点有: ? STM32F4 系列微控制器定时器的基本功能特性; ? STM32F4 编程配置方法的基本定时功能系列微控制器定时器。
3.1.2 知识链接 1.STM32F4 微控制器系列定时器概述 STM32F4 共有系列微控制器 14 定时器,编号为 TIM1~TIM14,其中包括 2 高级控制 时器、10 通用定时器和 2 基本定时器。 上述 3 在定时器类型中,基本定时器功能最少,只有基本定时功能和驱动数模转换器 (Digital to Analog Converter,DAC)没有外部通道的功能。通用定时器和高级控制定时器 功能强,如有独立的外部通道,可用于输入捕获、输出比较、脉宽调制(Pulse Width Modulation,PWM)支持正交编码器、霍尔传感器等电路的信号输出。 3-1-1 对各定时器 总结了功能特性,查看此表时要注意区分 3 类型定时器的功能差异
表 3-1-1 只列出各种定时器存在差异的指标,没有列出参数相同的指标。
此外,定时器时钟(TIMxCLK)频率与 PCLKx 关系如下。 如果 APB 预分频器(RCC_CFGR 中的 PPRE1、PPRE2)分频系数配置为 1,则 TIMxCLK= PCLKx;否则,定时器的时钟频率将与定时器连接 APB 域的两倍频率。定时器时钟 频率与 PCLKx 的关系如图 3-1-3 如中蓝色阴影和红色方框所示。
编者使用的 STM32F4 开发板上的微控制器型号是 STM32F407ZGT6,在任 务 1.3 的学习中, 我们配置 HCLK 为 168 MHz,PCLK1 为 42 MHz,PCLK2 为 84 MHz,即 APB1 和 APB2 的分 频系数分别为 4 和 2.根据上述关系,定时器时钟的频率 TIMxCLK = 2 × PCLKx。以 TIM13 例如,如果连接外设总线 APB1 = 42 MHz,那么 TIMxCLK = 2 × 42 MHz= 84 MHz。 对于STM32F42xxx 和STM32F43xxx 根据系列微控制器,《STM32F4xx 中文参考手册, 定时器时钟预分频器由定时器时钟预分频器通过 RCC 配备寄存器的专用时钟(RCC_DCKCFGR)的“TIMPRE”位段进行 配置(STM32F40xxx 系列微控制器无此寄存器),寄存器的定义如图所示 3-1-4 所示。
从图 3-1-4 可以看到,RCC 专用时钟配置置寄存器 24 位“TIMPRE有效,它被称为 选择定时器时钟预分频器。默认值为0,TIMxCLK 频率配置如前所述。 当该位置为1时,如果 APB 预分频器(RCC_CFGR 中的 PPRE1、PPRE2)的分 频系数配置为 1、2 或 4,则 TIMxCLK = HCLK;否则,定时器时钟频率将设置为与定时器相 连的 APB 域的频率的 4 倍:TIMxCLK = 4 x PCLKx。以 TIM13 为例,APB1 分频系外设总线 数为 4,因此 TIMxCLK =HCLK= 168 MHz。 2.基本定时器的功能框图分析 根据 3.1.1 根据节的任务分析结果,本任务需要定时器的基本定时功能,因此选择 STM32F4 系列微控制器的基本定时器。通过基本定时器的功能框图学习其各个部分的功能 其功能框图如图所示 3-1-5 所示。
从图 3-1-5 可以看出,基本定时器包括 3 部分:时钟源、控制器模块和时基单元。 自动重载寄存器和预分频器(PSC)长方形框有阴影,这意味着这两个寄存器有阴影寄存 存器。 自动重载寄存器左侧有一个事件标志,表示在更新事件时使用预装载值更新自动 重载影子寄存器。右侧的事件标志和中断和 DMA 计数器寄存器值和输出标志 当自动重载寄存器值相等时,将发生事件、中断和 DMA 输出。 下面分别介绍基本定时器各部分的组成。 (1)时钟源 基本定时器的时钟源只能来自内部时钟(CK_INT),即 RCC 的 TIMxCLK。关于 TIMxCLK 上述内容已阐述了频率的具体计算方法,此处不再赘述。 除了内部时钟,通用定时器和高级控制定时器的时钟源也可以来自外部时钟或其他定时器 时器等。 (2)控制器模块 控制定时器复位、使能和计数或触发基本定时器的控制器模块 DAC 的转换 使能等。 (3)时基单元 基本定时器的时基单元由一个 16 位递增计数器(图 3-1-5 中的 CNT 计数器)及其相关性 由自动重载寄存器组成,计数器时钟通过预分频器分频。计数器寄存器、预分频器寄存器和 软件可以读写自动重载寄存器。即使在计数器运行时,也可以读写。 时基单元包括以下内容 3 部分。
① 计数器寄存器 计数器寄存器(TIMx_CNT)中存储了定时器当前的计数值。 ② 预分频器寄存器 从图 3-1-5 可以看出,预分频器的输入是 CK_PSC(等于 CK_INT),分频后,输出为 CK_CNT,分频系数由 16 预分频器寄存器(TIMx_PSC)中值决定,介于 1~65536。CK_CNT 时钟频率与 CK_PSC 时钟频率关系如下。 f CK_CNT = f CK_PSC / (TIMx_PSC 1) 计数器由 CK_CNT 提供时钟,预分频器寄存器可以实时更改,因为它有缓冲,但是新的分频器 在下一个更新事件发生时,将采用频系数。 3-1-6 由预分频器显示的分频系数 1 变为 2 时 计数器时序图。
从图 3-1-6 可见计数器计数到F8”时,在 TIMx_PSC 新值1写在图中 原值为0的中蓝色阴影处。 f CK_CNT = f CK_PSC 。但在写入新值后,预分频器的分频系统 数并没有马上变为 2.更新事件发生时(图中橙色阴影处), f CK_CNT 频率变成了 f CK_PSC 的 2 分频。 ③ 自动重载寄存器 自动重载寄存器(TIMx_ARR)它由预装载寄存器和影子寄存器两部分组成。真正起作用 阴影寄存器支持预装载,每次尝试读写时都会访问预装载寄存器。预装载寄存器。 装载寄存器的内容可以直接传输到阴影寄存器或每次更新事件(UEV)的时 等待传输到影子寄存器取决于 TIMx_CR1 自动重载预装载使能位(APRE),其工作特 性如下。 ? 当 APRE=0 时,TIMx_ARR 没有缓冲,预装载寄存器的值直接传输到影子寄存器,如 图 3-1-7 所示。 ? 当 APRE=1 时,TIMx_ARR 预装载(缓冲),更新后预装载寄存器 如图所示,将值传输到影子寄存器 3-1-8 所示。
时基单元的计数过程总结如下。 ? 计数器从 0 当其值变为自动重载值时,开始计数(TIMx_ARR 当计数器的值)发生时, 上溢”。 ? 计数器每次溢出都会产生更新事件(UEV)同时更新所有寄存器,并将更新 断标志位”(TIMx_SR 中的 UIF 位)置 1。 ? 更新所有寄存器的动作包括: ? 使用预装载值(TIMx_PSC 预分频器缓冲区重新装载; ? 使用预装载值(TIMx_ARR 的内容)更新自动重载影子寄存器。
重新从计数器 0 开始计数。 以下是计数器工作时序示意图(如图所示) 3-1-9 说明上述计数过程,在此示意中 图中 TIMx_PSC 的值为 1(即 2 分频 ),TIMx_ARR 的值为 36。
介绍定时器的基本初始结构 STM32F4 为定时器外设提供标准外设库 4 这个任务只用于定时器
typedef struct {
uint16_t TIM_Prescaler; // 预分频器
uint16_t TIM_CounterMode; // 计数模式
uint32_t TIM_Period; // 定时器周期
uint16_t TIM_ClockDivision; // 时钟分频
uint8_t TIM_RepetitionCounter; // 重复计数器
} TIM_TimeBaseInitTypeDef;
对结构体各成员变量的作用介绍如下。 (1)TIM_Prescaler 定时器预分频器分频系数配置,它被用于配置预分频器寄存器的值,可配置的范围为 0~65535,对应 1~65536 分频。 (2)TIM_CounterMode 定时器计数方式配置,可配置的参数如下: 向上计数(TIM_CounterMode_Up); 向下计数(TIM_CounterMode_Down); 中心对齐模式 1(TIM_CounterMode_CenterAligned1); 中心对齐模式 2(TIM_CounterMode_CenterAligned2); 中心对齐模式 3(TIM_CounterMode_CenterAligned3)。 对于基本定时器而言,只能使用向上计数的方式,因此无须配置该项,使用默认值即可。
(3)TIM_Period 定时器周期配置,实际上本成员变量配置的是自动重载寄存器的值,事件生成时更新到影子 寄存器,可配置的范围为 0~65535。 (4)TIM_ClockDivision 时钟分频配置,它被用于配置定时器内部时钟频率与数字滤波器采样时钟频率的分频比,基 本定时器不具备输入捕获功能,可不用进行时钟分频配置。 (5)TIM_RepetitionCounter 重复计数器配置,高级控制定时器专用,基本定时器可不用进行该配置。 4.定时器中断功能的编程配置步骤 掌握了基本定时器的功能特性与初始化结构体的配置内容后,我们可进行完整的定时器中断 功能编程配置的学习。接下来我们将学习如何配置定时器 6,使之按时产生中断,并在中断服务 函数中完成相应的工作。具体的编程配置步骤如下。 (1)开启 TIM6 时钟 根据表 3-1-1 可知,TIM6 挂载在 APB1 上,因此需要调用 ABP1 的时钟使能函数以开启 TIM6 的时钟。具体代码如下: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); // 使能 TIM6 时钟 (2)配置定时器的工作参数 首先配置“定时器基本初始化结构体(TIM_TimeBaseInitTypeDef)”的各成员变量,然后调 用 TIM_TimeBaseInit()函数以完成参数的初始化。 我们通过一个实例来学习具体的配置方法。如在实际应用中,要求每隔 1s 采集一次环境温 湿度信息,使用定时器 6 实现,该如何配置定时器? 在上述实例中,我们可先配置 CK_CNT 频率。TIM6 挂载在 APB1 上,定时器时钟源频率 (CK_INT = CK_PSC)为 42 MHz×2=84 MHz。可将 TIMx_PSC 配置为 8399,根据计算公式可得: f CK_CNT = 84 MHz/ (8399 + 1) = 10000 Hz(周期为 100μs) 配置 TIMx_ARR 的值为 9999,进而即可将定时器配置为每隔 1s 产生更新中断。间隔时间 的计算方法如下:
100 μs×(TIMx_ARR + 1) = 1000000 μs = 1s 实现上述配置要求的代码块如下:
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); TIM_TimeBaseStructure.TIM_Period = 10000-1; // 配置 TIMx_ARR TIM_TimeBaseStructure.TIM_Prescaler = 8400-1; // 配置 TIMx_PSC TIM_TimeBaseInit(TIM6,&TIM_TimeBaseStructure);
另外可根据以下公式计算定时器的溢出时间( T out )。 T out (μs) = [(TIMx_ARR + 1)×(TIMx_PSC + 1)]÷ f CK_PSC (MHz) 在上述公式中, T out 的单位为 μs, f CK_PSC 为定时器的工作频率,单位为 MHz。将实例中的各 个数字代入上式后可得: 1s = 1000000 μs =(10000×8400)÷84(MHz) (3)配置允许定时器产生更新中断 本任务要求在到达定时时间后,产生更新事件并将中断标志位置 1,因此需要配置允许 TIM6 产生更新中断。STM32F4 标准外设库使用 TIM_ITConfig()函数来进行定时器中断使能,它的函 数原型定义如下: void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState); 第一个参数为定时器编号,取值为 TIM1~TIM14。 第二个参数用于指明要使能的定时器中断的类型,在本任务中使用更新中断,因此要配置为 “TIM_IT_Update”。 第三个参数指明使能(ENABLE)或失能(DISABLE)。 例如要使能 TIM6 的更新中断,具体代码如下: TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE); (4)配置定时器中断优先级 与任务 2.2 中的“按键中断”优先级和任务 2.3 中的“串口接收中断”优先级配置类似,使 能定时器的“更新中断”后,也须对 NVIC 进行优先级配置。使用以下代码可配置 TIM6 的中断 优先级。
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* TIM6 NVIC 配置 */
NVIC_InitStructure.NVIC_IRQChannel = TIM6_DAC_IRQHandler; // 定时器 6 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01; // 抢占优先级 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; // 子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
(5)使能定时器 完成定时器的相关配置以后,须开启定时器才能使其开始工作。这里通过将控制寄存器 (TIMx_CR1)的“CEN”位段置 1 实现。STM32F4 标准外设库通过调用 TIM_Cmd()函数实现, 该函数的原型定义如下: void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState); 使用实例如下: TIM_Cmd(TIM6, ENABLE); (6)编写定时器中断服务函数 定时器中断服务函数用于处理中断发生后的事件。定时器支持多种中断类型,但中断的入口 函数一般是统一的,如 TIM6 的中断服务函数为“TIM6_DAC_IRQHandler()”。因此在进入中断 服务函数后,首先应通过 TIMx_SR 判断中断类型,然后再执行相应的后续操作。STM32F4 标准 外设库提供了获取中断类型的函数,它的函数原型定义如下: ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT); 该函数的作用是:判断当前定时器发生了哪种类型的中断。具体使用实例如下: if(TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET){}; 上述语句用于判断当前 TIM6 是否发生了“更新中断(TIM_IT_Update)”。 另外,处理完中断之后,应将相应的中断标志位清除,即向 TIMx_SR 的相应位写入 0。 STM32F4 标准外设库提供了清除中断标志位的函数,函数原型定义如下:
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT); 该函数的作用是:清除定时器相应的中断标志位。具体使用实例如下: TIM_ClearITPendingBit(TIM6, TIM_IT_Update); 上述语句用于清除 TIM6 的“更新中断标志位”。 下面给出定时器中断服务函数的一般框架,其中入口函数名需要根据实际定时器进行修改, 如定时器 6 的中断入口函数名为“TIM6_DAC_IRQHandler()”。
void TIMx_IRQHandler(void)
{
if(TIM_GetITStatus(TIMx,TIM_IT_Update) == SET) // 如果产生了更新中断
{
/* DoSomething */
}
TIM_ClearITPendingBit(TIMx,TIM_IT_Update); // 清除更新中断的标志位
}
3.1.3 任务实施 1.根据任务要求计算 TIMx_ARR 与 TIMx_PSC 值 3.1.1 节的任务要求智能小车以一定的速度沿着黑色跑道前进,因此微控制器需要以较短的 周期频繁地获取循迹电路板上 8 路光电传感器的实时状态。可将 TIM6 的定时中断时间配置为 10 ms,然后在中断服务函数中获取循迹电路板的状态数据,以作为应用程序控制电机的依据。可 配置定时器的工作参数为 TIMx_PSC =8399,TIMx_ARR = 99。 根据计算公式可得: f CK_CNT = 84 MHz/ (8399 + 1) = 10000 Hz(周期为 100 μs) 定时中断时间为: 100 μs×(TIMx_ARR + 1) = 10000 μs = 0.01s = 10 ms 2.编写 TIM6 定时中断初始化程序 复制一份 task2.3_USART_WaterFlow_LED 工程,并将其重命名为“task3.1_Timer_Interrupt_ GetTrackData”。在“HARDWARE”文件夹下新建“TIMER”子文件夹,新建“timer6.c”和“timer6.h” 两个文件,将它们加入工程中,并配置头文件包含路径。 在“timer6.c”文件中输入以下代码:
#include "timer6.h"
/**
* @brief TIM6 定时中断功能初始化
* @param arr: 自动重装载值, psc: 定时器预分频值
* @retval None
*/
void TIM6_Int_Init(uint16_t arr, uint16_t psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);// 使能 TIM6 时钟
TIM_TimeBaseInitStructure.TIM_Period = arr; // 自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler = psc; // 定时器预分频值
TIM_TimeBaseInit(TIM6,&TIM_TimeBaseInitStructure); // 初始化 TIM6
TIM_ClearITPendingBit(TIM6, TIM_IT_Update); // 清除更新中断请求位
TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE); // 允许定时器 6 更新中断
TIM_Cmd(TIM6,ENABLE); // 使能定时器 6
NVIC_InitStructure.NVIC_IRQChannel = TIM6_DAC_IRQn; // 定时器 6 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01; // 抢占优先级 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; // 子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
在“timer6.h”文件中输入以下代码:
#ifndef __TIMER6_H
#define __TIMER6_H
#include "sys.h"
void TIM6_Int_Init(uint16_t arr, uint16_t psc);
#endif
3.编写循迹电路板连接端口初始化程序 在“HARDWARE”文件夹下新建“TRACK”子文件夹,新建“track.c”和“track.h”两个 文件,将它们加入工程中,并配置头文件包含路径。 在“track.c”文件中输入以下代码:
#include "track.h"
#include "led.h"
#include "usart.h"
/* 存放循迹电路板上传来的 8 位二进制数 */
uint8_t trackData = 0;
uint16_t tCount = 0;
/**
* @brief 循迹电路板端口初始化
* @param None
* @retval None
*/
void TrackBoard_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);// 使能 GPIOF 时钟
/* PF0~PF7 端口初始化设置 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3 \
|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; // 输入模式
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed; // 引脚速率 100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 上拉
GPIO_Init(GPIOF, &GPIO_InitStructure);
}
在“track.h”文件中输入以下代码:
#ifndef __TRACK_H
#define __TRACK_H
#include "sys.h"
void TrackBoard_Init(void); // 循迹电路板端口初始化
#endif
4.编写 TIM6 的定时中断服务函数 TIM6 的定时中断服务函数理论上可放置在应用程序中的任意一个源代码文件中,但中断服 务函数中通常包括数据处理程序,因此从编程的便利性来说应将中断服务函数与数据处理程序放 置在同一个源代码文件中。本任务将其编写在“track.c”文件中,具体代码如下:
/**
* @brief TIM6 定时中断服务函数
* @param None
* @retval None
*/
void TIM6_DAC_IRQHandler(void)
{
if(TIM_GetITStatus(TIM6,TIM_IT_Update) == SET)// 若发生更新中断
{
tCount++;
/* 获取 GPIOF 16 bit 数据中的低 8 位 (bit0 ~ bit7) */
trackData = GPIO_ReadInputData(GPIOF) & 0xFF;
if(tCount >= 200) // 每隔 2s 翻转 LED1 ,打印信息
{
LED1 = ~LED1;
printf("trackData is 0x%x\r\n",trackData);
tCount = 0;
}
}
TIM_ClearITPendingBit(TIM6,TIM_IT_Update);// 清除更新中断标志位
}
5.编写 main()函数 在“main.c”文件中输入以下代码:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "timer6.h"
#include "track.h"
int main(void)
{
delay_init(168); // 延时函数初始化
LED_Init(); //LED 端口初始化
USART1_Init(115200); //USART1 初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置 TIM6 定时中断时间为 10 ms */
TIM6_Int_Init(100-1, 8400-1);
TrackBoard_Init();
printf("System Started...\r\n");
while(1)
{
}
}
6.观察试验现象 本任务中 8 路循迹电路板数据的输出端口与 STM32F4 系列微控制器的 PF0~PF7 引脚相连, 为了更加方便地观察试验现象,已在 TIM6 的定时中断服务函数中将每隔 2s 获取的循迹电路板 数据通过 USART1 发送到上位机。应用程序编译无误后,下载至开发板运行,打开上位机的串 口调试助手,可观察到如图 3-1-10 所示的定时器中断试验现象。 由于循迹电路板端口初始化为输入模式,默认上拉,因此从图 3-1-10 中可以看到默认获取 到的 8 位数据全为“1”,即十六进制 0xff。使用杜邦线将 PF0 端口接地后,获取到的数据变为 0xfd,即最低位被清零。