一、时钟系统
时钟系统是CPU的脉搏, 由于STM32 本身非常复杂,外设非常的多,并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及 RTC 只需要几十 k 的时钟即可,因此STM32F4 的时钟系统比较复杂,不像简单的 51 单片机一个系统时钟就可以解决一切。
如下图所示,STM32F4有5个最重要的时钟源,分别为HSI(高速内部时钟)、HSE(高速外部时钟)、LSI(低速内部时钟)、LSE(低速外部时钟)、PLL(锁相环倍频输出)。在这 5 个中 HSI,HSE 以及 PLL 是高速时钟,LSI 和 LSE 是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 HSE 和LSE 是外部时钟源,其他的是内部时钟源。
LSI时钟:
LSI 是低速内部时钟,RC 振荡器,频率为 32kHz 左右。供独立看门狗和自动唤醒单元使用;
LSE时钟:
LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。主要是 RTC 的时钟源;
HSE时钟:
HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~26MHz。HSE 也可以直接做为系统时钟或者 PLL 输入;
HSI 时钟:
HSI 是高速内部时钟,RC 振荡器产生,频率为 16MHz。可以直接作为系统时钟或者用作 PLL输入。
PLL配置:
STM32F4具有两个 PLL:
- 主 PLL (PLL) 由 HSE 或 HSI 振荡器提供时钟信号,并具有两个不同的输出时钟:
- 第一个输出用于生成高速系统时钟(最高达 168 MHz)
- 第二个输出用于生成 USB OTG FS 的时钟 (48 MHz)、随机数发生器的时钟 (48 MHz) 和 SDIO 时钟 ( 48 MHz)。
- 专用 PLL (PLLI2S) 用于生成精确时钟,从而在 I2S 接口实现高品质音频性能。
如上图所示,主 PLL 时钟的时钟源要先经过一个分频系数为 M 的分频器,然后经过
倍频系数为 N 的倍频器出来之后的时候还需要经过一个分频系数为 P(第一个输出 PLLP)或者 Q(第二个输出 PLLQ)的分频器分频之后,最后才生成最终的主 PLL 时钟。假如我们的外部晶振选择 8MHz。同时我们设置相应的分频器 M=8,倍频器倍频系数 N=336,分频器分频系数 P=2,那么主 PLL 生成的第一个输出高速时钟 PLLP 为:PLL=8MHz * N/ (M*P)=8MHz* 336 /(8*2) = 168MHz
根据上文分析,系统时钟SYSCLK可来源于三个时钟源:HSI时钟,HSE时钟及PLL时钟。而PLL是由HSI或HSE产生。
由上文图片中可以看出来,任何一个外设资源在使用之前都必须先使能相应的时钟。
系统时钟的配置在main()函数之前,也就是说,在运行main()函数之前,必须保证系统时钟已经配置好了。但是,我们在实际的使用过程中,好像从来没有主动配置过时钟,这是因为系统已经配置好了,在启动项配置中,我们可以看到在执行main()函数之前,先执行了系统函数SystemInit(void),该函数主要对系统时钟进行了配置。
二、中断
通常,把CPU内部产生的紧急事件叫做异常,如非法指令(除零)、地址访问越界等,把来自CPU外部的片上外设产生的紧急事件叫做中断,如GPIO引脚电平变化,定时器溢出等。异常和中断的效果基本一致,都是暂停当前任务,优先执行紧急事件。因此,一般将中断和异常统称为中断。
CM4 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断。 STM32F40xx/STM32F41xx 总共有 92 个中断,这92 个中断里面,包括 10 个内核中断和 82 个可屏蔽中断,具有 16 级可编程的中断优先级,而我们常用的就是这 82 个可屏蔽中断。
这么多中断,应该如何进行管理呢?我们通过中断控制器NVIC进行管理。
2.1 NVIC(嵌套向量中断控制器)
NVIC 英文全称是 Nested Vectored Interrupt Controller,它通过配置分组对中断进行管理。如下图组0~4所示,同时,它对每个中断设置一个抢占优先级和一个响应优先级。
分组配置是在寄存器SCB->AIRCR中配置;该寄存器的10~8为设置了中断的组,每个组确定之后,该中断的寄存器IP(7~4位)的分配情况也就确定了,如下图所示,如果中断设置为组别3,那么寄存器IP的分配情况就是IP配置中有3位用来设置抢占优先级,1位用来配置响应优先级。
抢占优先级和响应优先级:
- 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的
- 抢占优先级相同的中断,高响应优先级不可以打断低响应优先级的中断
- 抢占优先级相同的中断,当两个中断同时发生的时候,哪个响应优先级高,哪个先执行;
- 如果两个中断的抢占优先级和响应优先级都是一样的,那么看哪个中断先发生就先执行。
例:假定设置中断优先级分组为2,然后设置:中断3(RTC中断)的抢占优先级为2,响应优先级为1。中断6(外部中断0)的抢占优先级为3,响应优先级为0,中断7(外部中断1)的抢占优先级为2,响应优先级为0.
分析,首先判断抢占优先级,哪个抢占优先级高,那么其中断优先级就高,可以看但中断3和中断7抢占优先级都是2,那么中断3和中断7之间不存在相互打断。而中断7的响应优先级是0,中断3的响应优先级是1,中断7的响应优先级高于中断3,因此,当中断7和中断3同时发生中断时,中断7优先响应。
那么,这三个中断的优先级顺序为:中断7>中断3>中断6
特别说明:
一般情况下,系统代码执行过程中,只设置一次中断优先级分组,比如设置号分组2之后,一般不会再改变分组了,否则,随意改变分组会导致中断管理混乱,程序出现意想不到的执行结果。
这是因为,不同的分组,抢占优先级和响应优先级位数是不同的,若贸然改变,其代表的数值会被打乱。
程序中,可以调用下面的函数设置中断优先级分组:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup) { /* Check the parameters */ assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup)); /* Set the PRIGROUP[10:8] bits according to NVIC_PriorityGroup value */ SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup; }
设置好中断分组之后,如何设置单个中断的抢占优先级和响应优先级呢,我们通过结构体NVIC_Type来进行设置,如下所示:
typedef struct
{
__IO uint32_t ISER[8]; /*!< 中断使能寄存器组 */
uint32_t RESERVED0[24];
__IO uint32_t ICER[8]; /*!< 中断失能寄存器组 */
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8]; /*!< 中断挂起寄存器组 */
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8]; /*!< 中断解挂寄存器组 */
uint32_t RESERVED3[24];
__IO uint32_t IABR[8]; /*!< 中断激活标志位寄存器组 */
uint32_t RESERVED4[56];
__IO uint8_t IP[240]; /*!< 中断优先级控制的寄存器组 */
uint32_t RESERVED5[644];
__O uint32_t STIR; /*!< Software Trigger Interrupt Register */
} NVIC_Type;
通过上述结构体,可以完成对中断优先级的设置:
中断优先级控制的寄存器组是:IP[240]
这240个8位寄存器,每个中断使用其中一个寄存器来确定优先级。根据中断分组的不同,每个IP寄存器的高4位用来设置抢占优先级和响应优先级。低四位没有用到。
再库函数中,我们使用如下函数来初始化中断
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
该函数的入口参数NVIC_InitTypeDef是一个结构体:
typedef struct
{
uint8_t NVIC_IRQChannel;
uint8_t NVIC_IRQChannelPreemptionPriority;
uint8_t NVIC_IRQChannelSubPriority;
FunctionalState NVIC_IRQChannelCmd;
} NVIC_InitTypeDef;
NVIC_InitTypeDef结构体属性如下:
NVIC_IRQChannel:定义初始化的是哪个中断,这个我们可以在 stm32f4xx.h 中定义的枚举类 型 IRQn 的成员变量中可以找到每个中断对应的名字。例如串口 1 对应USART1_IRQn。
NVIC_IRQChannelPreemptionPriority:定义这个中断的抢占优先级别。
NVIC_IRQChannelSubPriority:定义这个中断的响应优先级别。
NVIC_IRQChannelCmd:该中断通道是否使能。
中断优先级设置步骤:
1. 系统运行后先设置中断优先级分组,调用函数:NVIC_PriorityGroupConfig();再整个系统运行的过程中,只设置一次中断分组;
2. 针对每一个中断,设置对应的抢占优先级和响应优先级;调用函数NVIC_Init();
2.2 外部中断
STM32F4 的每个 IO 都可以作为外部中断的中断输入口,这点也是 STM32F4 的强大之处。STM32F407 的中断控制器支持 22 个外部中断/事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。STM32F407的 22 个外部中断为:
- EXTI 线 0~15:对应外部 IO 口的输入中断。
- EXTI 线 16:连接到 PVD 输出。
- EXTI 线 17:连接到 RTC 闹钟事件。
- EXTI 线 18:连接到 USB OTG FS 唤醒事件。
- EXTI 线 19:连接到以太网唤醒事件。
- EXTI 线 20:连接到 USB OTG HS(在 FS 中配置)唤醒事件。
- EXTI 线 21:连接到 RTC 入侵和时间戳事件。
- EXTI 线 22:连接到 RTC 唤醒事件。
从上文可以看出,STM32F4供IO使用的中断线只有16个(每个中断线才能产生一个中断请求),但是 STM32F4 的 IO 口却远远不止 16 个,那么 STM32F4 是怎么把 16 个中断线和 IO 口一一对应起来的呢?
如下图所示,STM32是这样设计的:GPIO 的管脚GPIOx.0~GPIOx.15(x=A,B,C,D,E,F,G,H,I)分别对应中断线 0~15。这样每个中断线对应了最多 9 个 IO 口,以线 0 为例:它对应了GPIOA.0、GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0、GPIOG.0,GPIOH.0,GPIOI.0。而中断线每次只能连接到 1 个 IO口上,这样就需要通过配置来决定对应的中断线配置到哪个 GPIO 上了。
有16个中断线EXTI 线 0~15,是否意味着有16个中断服务函数呢?不是的,IO口外部中断在中断向量表中只分配了7个中断向量,也就是 只能使用7个中断服务函数,如下表所示:
从上表中可以看出,外部中断线5~9分配了一个中断向量,共用一个服务函数,外部中断线10~15分配了一个中断向量,共用一个中断服务函数。中断服务函数列表如下:
- EXTI0_IRQHandler
- EXTI1_IRQHandler
- EXTI2_IRQHandler
- EXTI3_IRQHandler
- EXTI4_IRQHandler
- EXTI9_5_IRQHandler
- EXTI15_10_IRQHandler
外部中断库函数配置过程:
1. 使能 IO 口时钟,初始化 IO 口为输入
2. 开启 SYSCFG 时钟,设置 IO 口与中断线的映射关系:
初始化IO口之后,我们需要配置GPIO与中断线的映射关系,首先需要打开SYSCFG 时钟,只要使用到外部中断,就必须打开SYSCFG时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);//使能 SYSCFG 时钟
然后配置GPIO与中断线的映射关系,调用如下函数:
void SYSCFG_EXTILineConfig( uint8_t EXTI_PortSourceGPIOx, uint8_t EXTI_PinSourcex ); // 使用范例 SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
3. 初始化线上中断,设置出发条件等:
中断线上中断的初始化是通过函数 EXTI_Init()实现的。EXTI_Init()函数的定义是:
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
其输入参数EXTI_InitTypeDef结构体成员函数如下所示:
typedef struct { // 中断标号,对于外部中断,取值范围为EXTI_Line0~EXTI_Line15 uint32_t EXTI_Line; // 要初始化的中断线 // 中断模式,可选为中断EXTI_Mode_Interrupt 和事件 EXTI_Mode_Event。 EXTIMode_TypeDef EXTI_Mode; // 中断出发方式,可以是下降沿触发EXTI_Trigger_Falling,上升沿触发 EXTI_Trigger_Rising,或者任意电平(上升沿和下降沿)触发 EXTI_Trigger_Rising_Falling, EXTITrigger_TypeDef EXTI_Trigger; // 使能中断线 FunctionalState EXTI_LineCmd; }EXTI_InitTypeDef;
4. 配置中断分组(NVIC)并使能中断
NVIC的配置如上文所述,此处给出一个配置示例,如下所示:
NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //使能外部中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2, NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //响应优先级2 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); //中断优先级分组初始化
5. 编写中断服务函数
在配置完中断优先级之后,接着要做的就是编写中断服务函数。在编写中断服务函数的时候会经常使用到两个函数,第一个函数是判断某个中断线上的中断是否发生(标志位是否置位):
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
这个函数一般使用在中断服务函数的开头判断中断是否发生。另一个函数是清除某个中断线上的中断标志位:
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
中断配置示例:
#include "exti.h"
#include "led.h"
#include "SysTick.h"
#include "beep.h"
/**
* IO口外部中断
*/
void KEYExti_Init()
{
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
//1.使能IO
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量
//时钟使能
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);
GPIO_InitStructure.GPIO_Pin=KEY0_PIN|KEY1_PIN|KEY2_PIN|KEY3_PIN;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN; //GPIO模式为普通输入模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //100M
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOF, &GPIO_InitStructure); //初始化GPIOF6,7,8,9
//2.开启SYSCFC时钟,以便设置IO口与中断线之间的映射关系。
//只要使用外部中断,就必须打开SYSCFG时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG,ENABLE);
//配置IO口与中断线之间的映射关系
//配置IO口与中断线之间的映射关系
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOF,EXTI_PinSource9);//将中断线9与GPIOF映射
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOF,EXTI_PinSource8);//将中断线8与GPIOF映射
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOF,EXTI_PinSource7);//将中断线7与GPIOF映射
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOF,EXTI_PinSource6);//将中断线6与GPIOF映射
//3.配置中断分组,使能中断
NVIC_InitStructure.NVIC_IRQChannel=EXTI9_5_IRQn; //中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; //中断通道使能
//中断优先级分组初始化
NVIC_Init(&NVIC_InitStructure);
//4.初始化EXTI,设置中断触发条件
EXTI_InitStructure.EXTI_Line=EXTI_Line9|EXTI_Line8|EXTI_Line7|EXTI_Line6; //中断线标号,
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt; //中断模式,可选的值为中断和事件
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling; //触发方式:下降沿触发或上升沿触发
EXTI_InitStructure.EXTI_LineCmd=ENABLE;
EXTI_Init(&EXTI_InitStructure);
}
//中断函数
void EXTI9_5_IRQHandler()
{
if(EXTI_GetITStatus(EXTI_Line9) != RESET)
{
GPIO_SetBits(GPIOG,GPIO_Pin_7);//复位F9 点亮D1
delay_ms(500); //精确延时500ms
GPIO_ResetBits(GPIOG,GPIO_Pin_7);
delay_ms(500); //精确延时500ms
EXTI_ClearITPendingBit(EXTI_Line9);
}
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
GPIO_ResetBits(GPIOE,GPIO_Pin_3);//复位F9 点亮D1
GPIO_SetBits(GPIOE,GPIO_Pin_4);
GPIO_SetBits(GPIOG,GPIO_Pin_9);
EXTI_ClearITPendingBit(EXTI_Line8);
}
if(EXTI_GetITStatus(EXTI_Line7) != RESET)
{
GPIO_ResetBits(GPIOE,GPIO_Pin_4);//复位F9 点亮D1
GPIO_SetBits(GPIOE,GPIO_Pin_3);
GPIO_SetBits(GPIOG,GPIO_Pin_9);
EXTI_ClearITPendingBit(EXTI_Line7);
}
if(EXTI_GetITStatus(EXTI_Line6) != RESET)
{
GPIO_ResetBits(GPIOG,GPIO_Pin_9);//复位F9 点亮D1
GPIO_SetBits(GPIOE,GPIO_Pin_3);
GPIO_SetBits(GPIOE,GPIO_Pin_4);
EXTI_ClearITPendingBit(EXTI_Line6);
}
}
三、定时器
3.1 三种定时器区别
3.2 通用定时器
STM32F4 的通用定时器包含一个 16 位或 32 位自动重载计数器(CNT),该计数器由可编程预分频器(PSC)驱动。STM32F4 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)等;
STM3 的通用 TIMx (TIM2~TIM5 和TIM9~TIM14)定时器功能包括:
- 16 位/32 位(仅 TIM2 和TIM5)向上、向下、向上/向下自动装载计数器(TIMx_CNT)
- 16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~65535 之间的任意数值。 (预分频器配置几个时钟计数一次)
-
4 个独立通道(TIMx_CH1~4,TIM9~TIM14 最多 2 个通道),这些通道可以用来作为:
- A.输入捕获
- B.输出比较
- C.PWM 生成(边缘或中间对齐模式)
- D.单脉冲模式输出
- 可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外一个定时器)的同步电路。
-
如下事件发生时产生中断/DMA(TIM9~TIM14 不支持 DMA):
- A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
- B.触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
- C.输入捕获
- D.输出比较
- E.支持针对定位的增量(正交)编码器和霍尔传感器电路(TIM9~TIM14 不支持)
- F.触发输入作为外部时钟或者按周期的电流管理(TIM9~TIM14 不支持)
通用定时器框图如下所示:
以上框图可以分为几个部分,其中:
1. 左上部分为时钟来源,通用定时器的时钟来源有四种可选:
- 内部时钟(CK_INT)
- 外部时钟模式1:外部输入引脚TIx(x=1,2,3,4)
- 外部时钟模式 2:外部触发输入 ETR
- 内部触发输入(ITRx(x=0,1,2,3)):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1作为另一个定时器Timer2的预分频器。
2. 时基单元
通用定时器时基单元包括 3 个寄存器,分别是计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)。
预分频器寄存器(TIMx_PSC),用于对计数器时钟频率进行分频,通过寄存器内的相应位设置,分频系数值可在 1 到 65536 之间。
通用定时器计数方式有向上计数、向下计数、向上向下计数(中心对齐计数)。
3. 输入捕获
输入捕获可以对输入的信号的上升沿,下降沿或者双边沿进行捕获,通常用于测量输入信号的脉宽、测量 PWM 输入信号的频率及占空比。
4. 输出比较
输出比较就是通过定时器的外部引脚对外输出控制信号,可以输出有效电平、无效电平、翻转、强制变为无效电平、强制变为有效电平、 PWM1 和 PWM2等模式,具体使用哪种模式由寄存器 CCMRx 的位 OCxM[2:0]配置。其中 PWM 模式是输出比较中的特例,使用的也最多。
内部时钟的选择:
在默认调用SystemInit函数的情况下:
SYSCLK=168M
AHB1时钟=168M
APB1时钟=42M
所以,APB1的分频系数=AHB/APB1时钟=4,所以通用定时器时钟CK_INT=2*42=84M
3.3 定时器配置过程(以TIM3为例)
1. TIM3时钟使能:
TIM3 是挂载在 APB1 之下,所以我们通过 APB1 总线下的使能使能函数来使能 TIM3。调用的函数是:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); ///使能 TIM3 时钟
2. 初始化定时器参数,设置自动重装值,分频系数,计数方式等:
在库函数中,定时器的初始化参数是通过初始化函数 TIM_TimeBaseInit 实现的:
void TIM_TimeBaseInit(
TIM_TypeDef*TIMx,
TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct
);
第一个参数是确定是哪个定时器。第二个参数是定时器初始化参数结构体指针,结构体类型为 TIM_TimeBaseInitTypeDef,如下所示:
{
uint16_t TIM_Prescaler; // 设置分频系数
uint16_t TIM_CounterMode; // 设置计数方式(向上,向下,中心)
uint16_t TIM_Period; // 自动重载计数周期值
uint16_t TIM_ClockDivision; // 时钟分频因子
uint8_t TIM_RepetitionCounter;
} TIM_TimeBaseInitTypeDef;
3. 定时器中断类型设置
因为我们要使用 TIM3 的更新中断,寄存器的相应位便可使能更新中断。在库函数里面定时器中断使能是通过 TIM_ITConfig 函数来实现的:
void TIM_ITConfig(
TIM_TypeDef* TIMx, // 定时器号,取值TIM1~TIM7
uint16_t TIM_IT, // 指定定时器中断类型,包括更新中断TIM_IT_Update、触发中断TIM_IT_Trigger,以及输入捕获中断等
FunctionalState NewState // 使能或失能
);
4. TIM3中断优先级配置
在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器,设置中
断优先级。
5. 使能定时器
光配置好定时器还不行,没有开启定时器,照样不能用。我们在配置完后要开启定时器,通过 TIM3_CR1 的CEN 位来设置。在固件库里面使能定时器的函数是通过 TIM_Cmd 函数来实现的:
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)
// 使用示例
TIM_Cmd(TIM3, ENABLE); //使能 TIMx 外设
6. 编写中断服务函数
在最后,还是要编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,所以在状态寄存器 SR 的最低位。在处理完中断之后应该向 TIM3_SR 的最低位写 0,来清除该中断标志。
在固件库函数里面,用来读取中断状态寄存器的值判断中断类型的函数是:
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t)
该函数的作用是,判断定时器 TIMx 的中断类型 TIM_IT 是否发生中断。比如,我们要判断定时器 3 是否发生更新(溢出)中断,方法为:
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){}
固件库中清除中断标志位的函数是:
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT)
该函数的作用是,清除定时器 TIMx 的中断 TIM_IT 标志位。使用起来非常简单,比如我们在
TIM3 的溢出中断发生后,我们要清除中断标志位,方法是:
TIM_ClearITPendingBit(TIM3, TIM_IT_Update );
配置示例:
/**
* per:定时器数值
* psc:分频系数
*/
void TIM4_Init(u16 per,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//1.使能定时器时钟 TIM4
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE); //使能
//2.初始化定时器参数,包含自动重装载、分频系数、计数方式等
TIM_TimeBaseInitStructure.TIM_Period=per; //自动装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //分频系数
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; //时钟分频因子
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //设置向上计数模式
TIM_TimeBaseInit(TIM4,&TIM_TimeBaseInitStructure);
//3.设置定时器中断类型
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE); //开启定时器中断
TIM_ClearITPendingBit(TIM4,TIM_IT_Update);
//4.设置定时器中断优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;//定时器中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure);
//5.开启定时器
TIM_Cmd(TIM4,ENABLE); //使能定时器
}
/*******************************************************************************
* 函 数 名 : TIM4_IRQHandler
* 函数功能 : TIM4中断函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void TIM4_IRQHandler(void)
{
if(TIM_GetITStatus(TIM4,TIM_IT_Update))
{
GPIO_SetBits(GPIOG,GPIO_Pin_7);//复位F9 点亮D1
delay_ms(500); //精确延时500ms
GPIO_ResetBits(GPIOG,GPIO_Pin_7);
delay_ms(500); //精确延时500ms
}
TIM_ClearITPendingBit(TIM4,TIM_IT_Update);
}
3.4 PWM输出
PWM 是 Pulse Width Modulation 的缩写,中文意思就是脉冲宽度调制,简称脉宽调制。它是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,其控制简单、灵活和动态响应好等优点而成为电力电子技术最广泛应用的控制方式,其应用领域包括测量,通信,功率控制与变换,电动机控制、伺服控制、调光、开关电源,甚至某些音频放大器,因此学习 PWM 具有十分重要的现实意义。参考:PWM概念。
PWM原理图下图所示:
在上图中,我们假定定时器工作在向上计数PWM模式,那么,当CNT<CCRx时,输出0,当CNT>=CCRx时输出1。那么就可以得到下发的PWM示意图:当CNT值小于CCRx的时候,IO输出低电平,当CNT值大于等于CCRx的时候,IO输出高电平。当CNT达到ARR值得时候,重新归零,然后重新向上计数,一次循环。通过改变CCRx的值,就可以改变PWM输出的占空比。改变ARR的值,就可以改变PWM输出的频率,这就是PWM输出的原理。
PWM工作过程:
上图中:
- CCR1:捕获比较寄存器(x=1,2,3,4),用来设置比较的值。
- CCMR1:捕获/比较模式设置寄存器,OC1M[2:0]位:对于PWM方式下,用于设置PWM模式,可设置位模式1[110]或模式2[111],这两种模式的区别就是输出电平的极性相反。
- CCER:CC1P位,表示输入/捕获1的输出极性位,若为0表示高电平有效,为1表示低电平有效。
- CCER:CC1E位,表示输入/捕获1的输出使能位,若为0表示关闭,为1表示打开。
上文分析:
上文描述中,CCR1寄存器是比较值设置寄存器,用于定时器计数器CNT与该寄存器进行比较,来决定输出的电平。
CCMR1寄存器是模式设置寄存器,通过设置寄存器0C1M[2:0]位,可以配置PWM是模式1还是模式2,PWM模式1,在向上计数时,CNT<CCR1时通道1为有效电平,否则为无效电平,在向下计数时,CNT>CCR1时,通道1为无效电平,否则为有效电平。PWM模式2,在向上计数时,CNT<CCR1时通道1为无效电平,否则为有效电平,在向下计数时,CNT>CCR1时,通道1为有效电平,否则为无效电平。
那么,有效电平是高电平还是低电平呢?不一定!有效电平由寄存器CCER寄存器的CC1P位决定,当该位为0,表示高电平是有效电平。该位为1,则表示低电平有效。
PWM配置过程(以TIM14为例):
1. 开启 TIM14 和 GPIO 时钟,配置 PF9 选择复用功能 AF9(TIM14)输出。
2. 初始化 TIM14,设置 TIM14 的 ARR 和 PSC 等参数
在开启了 TIM14 的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来控制输出 PWM的周期。
3. 设置 TIM14_CH1 的 PWM 模式,使能 TIM14 的 CH1 输出
接下来,要设置 TIM14_CH1 为PWM 模式(默认是冻结的),在库函数中,PWM 通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来初始化的,不同的通道的设置函数不一样。
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
其中入参结构体格式如下:
typedef struct { uint16_t TIM_OCMode; // 设置模式是PWM还是输出比较,此处为PWM uint16_t TIM_OutputState; // 设置比较输出使能,也就是使能PWM输出到端口 uint16_t TIM_OutputNState; */ uint16_t TIM_Pulse; // 比较的值 uint16_t TIM_OCPolarity; // 用来设置极性是高还是低 uint16_t TIM_OCNPolarity; uint16_t TIM_OCIdleState; uint16_t TIM_OCNIdleState; } TIM_OCInitTypeDef; // 应用示例如下 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择模式 PWM TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性低 TIM_OC1Init(TIM14, &TIM_OCInitStructure); //根据T 指定的参数初始化外设TIM1 4OC1
4. 使能TIM14
5. 修改TIM14_CCR1来控制占空比
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare2);
示例程序:文章来源:https://www.toymoban.com/news/detail-407405.html
#include "pwm.h"
void TIM14_CH1_PWM_Init(u16 per,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
//1.使能定时器时钟 TIM4
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14,ENABLE);//使能TIM14时钟
//2.设置引脚复用器映射
//由于TIM14_CH1对应的输出引脚并不是我们要用的LED引脚,所以使用通道映射将其映射到LED所在的IO口
GPIO_PinAFConfig(GPIOF,GPIO_PinSource11,GPIO_AF_TIM14);//管脚复用
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF; //复用输出模式
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_11; //管脚设置
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;//速度为100M
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOF,&GPIO_InitStructure); //初始化结构体
//3.初始化PWM输出参数。
TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_Low;
TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable;
TIM_OC1Init(TIM14,&TIM_OCInitStructure); //输出比较通道1初始化
//4.开启定时器
TIM_TimeBaseInitStructure.TIM_Period=per; //自动装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //分频系数
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //设置向上计数模式
TIM_TimeBaseInit(TIM14,&TIM_TimeBaseInitStructure);
TIM_OC1PreloadConfig(TIM14,TIM_OCPreload_Enable); //使能TIMx在 CCR1 上的预装载寄存器
TIM_ARRPreloadConfig(TIM14,ENABLE);//使能预装载寄存器
TIM_Cmd(TIM14,ENABLE); //使能定时
}
文章来源地址https://www.toymoban.com/news/detail-407405.html
到了这里,关于STM32F4基础:时钟系统、中断及定时器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!