总结
通过基本定时器中断实现LED闪烁
1)LED初始化
(2)main中的中断分组
(3)NVIC初始化
NVIC_InitTypeDef NVIC_InitStructure;配置NVIC初始化结构体
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM6_DAC_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);定时器使能/通电
(4)TIM初始化
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;配置TIM时基结构体
TIM_TimeBaseStructure.TIM_Period = 10000-1; // 查到这些数发送中断
TIM_TimeBaseStructure.TIM_Prescaler = 8400-1; // 分频系数
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上查数
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure); 写入寄存器中
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);开启中断使能
TIM_IT指定中断来源,Update指明是更新中断,还有捕获比较的通道(1/2/3/4)中断等
TIM6 and TIM7 only the parameter TIM_IT_Update can be used
4. TIM_Cmd(TIM6, ENABLE);定时器开始工作。
通过系统滴答定时器实现延时函数
通过系统滴答定时器实现延时函数
SysTick->CTRL |= 1<<0; // 使能
SysTick->CTRL &= ~(1<<1); // 如果设置为1,那么产生异常请求,需要写异常处理函数 0的话无动作
SysTick->CTRL &= ~(1<<2); // 时钟源头选择
// 数21个数字
SysTick->LOAD = 21000; // 通过当前时钟计算LOAD的值
// SysTick->VAL = ;
// 计时完成之后会变为1
while(!(SysTick->CTRL&1<<16)){}
// 计时完成
SysTick->CTRL &= ~(1<<0); // 关闭SystemClock
// 定时器,说白了就是一个计数的,通过频率来估算,每一次计数所需要的时间,从而通过计算数值*每次计数所需要的时间来获得经过的时间。
本章节叫做定时器的基本功能,那么定时器的基本功能是什么呢?定时器的基本功能就是定时。我们可以给定时器配置一定的时间,从而实现每隔一定的时间,就产生一次定时器中断(与之前的外部中断区分),然后我们就可以在定时器的中断中去做一些处理。定时器中断与之前的外部中断不同,它也属于中断的一种,在之前的小节中,我们以外部中断为例介绍了中断应该如何使用,本节我们就以定时器的中断为例。
定时器的基本功能是定时,那么他还有什么功能呢?其实定时器还有很多其他功能,例如输出比较,输入捕获和编码器模式等等,下一章节我们会介绍定时器的输出比较和输入捕获这两个功能,编码器模式我们会在pid算法中讲解。
STM32为什么需要精确的计算时间
- 同步芯片内的各个模块
芯片中的各个模块需要进行协调和同步以确保他们在正确的时间执行各自的操作,就像春晚一样,各个节目的时间需要掐的很准,不然就串台了,所以需要一个精准的时间基准,来协调各个部件之间的先后处理关系。 - 同步数据传输
确定发送方和接收方从何时开始传输和接受数据,以确保在正确的时间间隔内完成数据传输,没有时钟信号的同步,数据传输可能会因为接受和发送方的时序不一致而产生错误。 - 节省能量
STM32的定时器
STM32F4定时器分类(共14个):
● 高级定时器:TIM1,TIM8
● 通用定时器:TIM2-TIM5,TIM9-TIM14
● 基本定时器:TIM6,TIM7
控制器上所有定时器都是彼此独立的,不共享任何资源。就功能上来说,通用定时器包含所有基本定时器功能,而高级控制定时器包含通用定时器所有功能,所以高级控制定时器功能繁多。
STM32的基本定时器
基本特性
还记的我们在时钟那一章节中讲过的定时器的频率吗?在这里就用到了那一章节的知识点,我们可以根据APB的分频系数,决定到底是否需要2,计算出定时器的时钟频率。下图的APBx定时器时钟是图2CK_INT的源头,而在时钟那一章节我们了解到,默认情况下,APB1是422的频率,APB2是84*2的频率。
- 定时器时钟经过PSC 预分频器之后,即CK_CNT,用来驱动计数器计数。
- 计数器CNT 是一个16 位的计数器,向上,向下,向上/下计数模式(基本定时器只能递增计数),最大计数值为65535(2的16次方)。当计数达到自动重装载寄存器的时候产生更新事件,并清零从头开始计数。
- 自动重装载寄存器ARR 是一个16位的寄存器,这里面装着计数器能计数的最大数值。当计数到这个值的时候,如果使能了中断的话,定时器就产生溢出中断。
基本定时器详解F407为例
- 有十六位自动重载递增计数器计数最大值为65535
- 十六位可编程预分频器在APBx(*2?)的基础上分频
- 除法DAC的同步电路
- 发生计数器上溢后会生成中断/DMA请求
STM32通用定时器
通用定时器与基本定时器的区别
- 第一部分是定时器时钟源的选择,比基本定时器有了更多的选择方式,比较有特点的是支持外部时钟模式,这样就可以把N个定时器接一起串联起来,一个定时器的输出是另一个定时器的输入,不过用的比较少,最常用的还是选择内部时钟源。
- 第二部分跟基本大致定时器一致,不过多了一些更灵活的功能。高级和通用定时器在计数方式上有三种计数模式,分别为递增计数模式、递减计数模式和递增/递减(中心对齐) 计数模式。
- 第三部分是输入捕获。输入捕获可以对输入的信号的上升沿、下降沿或者双边沿进行捕获,常用的有测量输入信号的脉宽和测量PWM 输入信号的频率和占空比这两种。(后面会讲解)
- 第四部分是输出比较。输出比较就是通过定时器的外部引脚对外输出控制信号(后面会讲解)
通过定时器中断实现LED闪烁
// main.c
#include "stm32f4xx.h"
#include "Led.h"
#include "Tim.h"
int main()
{
LED_Init();
LED0_ON();
LED1_ON();
// 定时器初始化
TimeBaseInit();
while(1)
{
}
}
// tim.c
#include "Tim.h"
// Tim2是通用定时器
//
void TimeBaseInit(void)
{
// 1秒更新一次中断
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 使能TIM
TIM_InternalClockConfig(TIM2); // 选择内部时钟,还可以选择中断实现的外部时钟,其他定时器,以及TIX捕获通道
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 滤波器使用的频率
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 10000-1 ; // 确定1s的时间
TIM_TimeBaseStructure.TIM_Prescaler = 8400-1; // 计数器使用的分频系数
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // 重复计数,高级定时器才拥有的,因此直接使用0
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure); // 写入寄存器
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); // 开启时钟中断,这样就配置好了更新中断到NVIC的通路
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置异常分组
// 配置NVIC通道
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStruct);
TIM_Cmd(TIM2,ENABLE);
}
// stm32fxx_it.c
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_FLAG_Update)==SET)
{
TIM_ClearITPendingBit(TIM2,TIM_FLAG_Update);
GPIO_ToggleBits(GPIOF,GPIO_Pin_9);
}
}
系统滴答定时器
系统滴答定时器,基本没有使用的标准库函数接口,因此我们只能通过直接操作寄存器的方式来使用系统滴答定时器。
SysTick—系统定时有4个寄存器,简要介绍如下。在使用SysTick产生定时的时候,只需要配置前三个寄存器,最后一个校准寄存器不需要使用。
// SystemClock.c
#include "public.h"
// 已知频率为21000000HZ,也就是说,每秒数21000000个数字
// 1微秒级别的延时
// 那么每个毫秒数21000个数字
void Delay_1ms() //查询1us的Delay
{
SysTick->CTRL |= 1<<0; // 使能
SysTick->CTRL &= ~(1<<1); // 如果设置为1,那么产生异常请求,需要写异常处理函数。0的话无动作
SysTick->CTRL &= ~(1<<2); // 时钟源头选择为8分频
// 数21个数字
SysTick->LOAD = 21000;
// SysTick->VAL = ; 获得当前查到的值
// 计时完成之后会变为1
while(!(SysTick->CTRL&1<<16))
{}
// 计时完成
SysTick->CTRL &= ~(1<<0); // 关闭SystemClock
}
void Delay_ms(uint32_t ntime) // 最大值779ms左右
{
for(int i=0;i<ntime;i++)
{
Delay_1ms();
}
}
// 已知频率为21000000HZ,也就是说,每秒数21000000个数字
// 1微秒级别的延时
// 那么每个微秒数21个数字
void Delay_1us() //查询1us的Delay
{
SysTick->CTRL |= 1<<0; // 使能
SysTick->CTRL &= ~(1<<1); // 如果设置为1,那么产生异常请求,需要写异常处理函数 0的花误动作
SysTick->CTRL &= ~(1<<2); // 时钟源头选择
// 数21个数字
SysTick->LOAD = 0x15; // 0000 0000 0000 0000 0001 0101
// SysTick->VAL = ;
// 计时完成之后会变为1
while(!(SysTick->CTRL&1<<16))
{}
// 计时完成
SysTick->CTRL &= ~(1<<0); // 关闭SystemClock
}
void Delay_us(uint32_t ntime) // 最大值779us左右
{
for(int i=0;i<ntime;i++)
{
Delay_1us();
}
}
问题
- 如何精确延时?
答:通过系统滴答定时器实现 - 在定时器计数的时候,改变了自动重装载寄存器ARR的值,会发生什么事情?
答:查看影子寄存器 - TIM_GetITStatus与TIM_GetFlagStatus 有什么区别 ?
答:
● TIM_GetITStatus 关注中断标志,检测是否触发了中断。
● TIM_GetFlagStatus 关注定时器的状态标志,检测定时器的各种事件状态(例如,溢出、更新等)。
● TIM_GetITStatus 主要用于处理中断服务例程 (ISR),用于中断相关的判断。
● TIM_GetFlagStatus 用于普通的状态检测,可以用于轮询方式检查定时器事件的发生。 - 为什么通过定时器实现LED闪烁代码版本1中,当我们给单片机上电后,会立刻先进入一次中断服务函数?
答:问题出现在TIM_TimeBaseInit这个函数,这个函数会立刻手动生成一个更新事件,因此这就会导致更新事件与更新中断同时产生,而更新中断会置更新中断标志位,因此会直接进入中断服务函数。 - TIM_TimeBaseInit这个函数为什么要手动生成一个更新事件?
答:我们知道,我们配置的预分频值以及计数值只有在更新事件发生之后才会起作用,而在初始化的时候,为了让值立刻更新,就手动生成了一次更新事件。 - 如何解决我们给单片机上电后,会立刻先进入一次中断服务函数的问题?
答:在TIM_TimeBaseInit之后,执行函数TIM_ClearFlag(TIM2,TIM_FLAG_Update);就可以解决。