写在前面:本系列内容均为自学笔记,参考资料为野火指南者开发板资料及芯片参考手册等,使用野火指南者开发板进行学习,该系列内容仅用于记录笔记,不做其他用途,笔记的内容可能会存在不准确或者错误等,如有大佬看到错误内容还望能够评论指正,感谢各位。
本节包括前几节的程序,请参考野火开发板资料,里面由更加清晰的教学,野火B站账号:野火官方B站账号链接。
学习目标
1、简单了解NVIC(嵌套向量中断控制器);
2、简单了解EXTI(外部中断/事件控制器);
3、写个程序,实现点灯;
电路图
本次将通过按键检测来触发中断,当中断被触发则LED灯的状态反转;
图8-1:按键点灯
笔者对于NVIC和EXTI的一些理解
先说一下笔者的理解:NVIC理解为总中断,EXTI理解为次级中断,当我们使用中断时首先要配置次级中断EXTI,使它允许使用中断,当次级中断允许以后,还需要总中断允许,这时NVIC就要起作用,我们通过配置NVIC,让它也允许使用中断,这时两个控制器都允许,所以可以正常使用和产生中断;
上面的理解不一定对,不过使用时确实是往这个方向走的,仅个人理解,欢迎纠错;
一、NVIC简介(嵌套向量中断控制器)
关于NVIC的更多详细的内容请参考《STM32F10xxx Cortex-M3编程手册》(温馨提示:这手册是全英文的),这里做简单介绍,能够大致了解NVIC的功能并且能够简单使用即可;
1、特性
关于NVIC特性的描述有两处,下面两个图片是芯片参考手册和Cortex-M3编程手册中关于NVIC特性的描述,这里将两个都放出来,供大家参考:
图8-2:芯片参考手册中的描述
图8-3:Cortex-M3编程手册中的部分描述
2、NVIC寄存器
中断分为系统异常和外部中断,其中系统异常属于内核部分,外部中断针对外设;NVIC是嵌套向量中断控制器,它主要负责内核和外设的所有中断相关的功能;在官方固件库中针对这部分内容着重注意core_cm3.h和misc.h两个文件,另外STM32F103中外部中断有60个,系统异常10个,中断总量远少于Cortex-M3所支持的256个;
core_cm3.h文件中关于NVIC寄存器的定义如下
typedef struct //NVIC各寄存器定义
{
__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]; //中断优先级寄存器,(内核编程手册中简称IPR)
uint32_t RESERVED5[644];
__O uint32_t STIR; //软件触发中断寄存器
} NVIC_Type;
上图中可以看出,NVIC一共有7个寄存器,不过本文中配置中断用的是中断使能寄存器ISER、中断清除寄存器ICER和中断优先级寄存器IP;
3、NVIC库函数
core_cm3.h文件中关于NVIC库函数的定义如下,该库文件没有对函数进行统合声明,所以要使用只能一个个找:
表8-1:core_cm3.h文件中关于NVIC库函数的定义
NVIC库函数 | 中文描述 |
---|---|
void NVIC_EnableIRQ(IRQn_Type IRQn); | 使能中断 |
void NVIC_DisableIRQ(IRQn_Type IRQn); | 失能中断 |
void NVIC_SetPendingIRQ(IRQn_Type IRQn); | 设置中断悬起位 |
void NVIC_ClearPendingIRQ(IRQn_Type IRQn); | 清除中断悬起位 |
uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn); | 获取悬起中断编号 |
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority); | 设置中断优先级 |
uint32_t NVIC_GetPriority(IRQn_Type IRQn); | 获取中断优先级 |
void NVIC_SystemReset(void); | 系统复位 |
不过上述函数的使用频率不是很大,一般情况下我们可以优先参考使用misc.h文件中的函数:
表8-2:misc.h文件中关于NVIC寄存器的声明如下
NVIC库函数 | 中文描述 |
---|---|
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); | 配置优先级分组 |
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct); | 初始化NVIC |
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset); | 设置向量表的位置和偏移量 |
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState); | 选择系统进入低功耗模式的条件 |
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource); | 配置SysTick时钟源 |
在本文中将使用misc.h中的配置优先级分组、初始化NVIC两个函数;
4、优先级
NVIC_IPRx寄存器是NVIC优先级设置寄存器,该寄存器宽度为8bit,理论上每个外部中断都可以设置0~255的优先级级别,优先级的数值越小,则优先级越高,也就是说优先级设置为0,那么优先级将是最高的,反之255优先级最小;在F103芯片中,优先级设置寄存器进行了精简,该芯片只使用了寄存器的高4位(bit4~bit7),并且这4位又被抢占优先级(主优先级)和子优先级,当多个外部中断同时响应时,抢占优先级高的优先执行,如果抢占优先级相同则比较子优先级,子优先级高的先执行,如果子优先级也相同则比较他们的硬件编号,编号越小,优先级越高,硬件编号请参考《STM32F10xx中文参考手册》第9章9.1.2中的表55中断向量表,表格太长,这里就不列举出来了;
下面看一下优先级分组的表格,该表格来自官方固件库中misc.h文件:
表8-3:NVIC优先级分组
NVIC优先级分组 | NVIC_IRQ抢占优先级 | NVIC_IRQ子优先级 | 描述 |
---|---|---|---|
NVIC_PriorityGroup_0 | 0 | 0~15 | 抢占优先级0位,子优先级4位 |
NVIC_PriorityGroup_1 | 0~1 | 0~7 | 抢占优先级1位,子优先级3位 |
NVIC_PriorityGroup_2 | 0~3 | 0~3 | 抢占优先级2位,子优先级2位 |
NVIC_PriorityGroup_3 | 0~7 | 0~1 | 抢占优先级3位,子优先级1位 |
NVIC_PriorityGroup_4 | 0~15 | 0 | 抢占优先级4位,子优先级0位 |
下图来自野火教程第17章,这里放出来是方便理解抢占优先级和子优先级的位置关系,这个图片中优先级分组从分组4(0b011)开始,第0组(0b111)在最下方一行,另外图片中的主优先级就是抢占优先级:
图8-4:节选自野火教程中关于NVIC优先级分组部分,芯片参考手册有没有这表格笔者就不是很清楚了
下方程序是关于优先级分组的声明,这个声明与图8-4中第一列的二进制数相对应,另外这部分的程序也是对void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
函数的部分声明:
#define NVIC_PriorityGroup_0 ((uint32_t)0x700) /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
0 bits for subpriority */
#define IS_NVIC_PRIORITY_GROUP(GROUP) (((GROUP) == NVIC_PriorityGroup_0) || \
((GROUP) == NVIC_PriorityGroup_1) || \
((GROUP) == NVIC_PriorityGroup_2) || \
((GROUP) == NVIC_PriorityGroup_3) || \
((GROUP) == NVIC_PriorityGroup_4))
关于表8-3在实际使用时,优先级分组用void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
函数配置,在结构体NVIC_InitTypeDef
中配置抢占优先级和子优先级,另外这个结构体中还需要配置通道和是否使能,配置好后使用void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
函数进行NVIC的初始化,至此NVIC设置完成,而后续我们写程序时也将会按照这个步骤进行配置;
二、EXTI简介(外部中断/事件控制器)
1、特性
对于互联型产品,外部中断/事件控制器由20个产生事件/中断请求的边沿检测器组成,对于其它产品,则有19个能产生事件/中断请求的边沿检测器。每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求。
EXTI特性在芯片参考手册中的描述如下:
● 每个中断/事件都有独立的触发和屏蔽
● 每个中断线都有专用的状态位
● 支持多达20个软件的中断/事件请求
● 检测脉冲宽度低于APB2时钟宽度的外部信号。参见数据手册中电气特性部分的相关参数。
2、框图
图8-5:EXTI框图(来自芯片参考手册,笔者在框图上添加了阴影和编号)
首先解释一下框图中的20:这个20表示有20个事件/中断线,这个与上面提到的EXTI的特性相关,这20个线分别对应的事件/中断请求如下:
EXTI0~EXTI15:GPIOx_Pin0~GPIOx_Pin15,其中x表示A~I;
EXTI16:PVD输出;
EXTI17:RTC闹钟事件;
EXTI18:USB唤醒事件;
EXTI19:以太网唤醒事件(只使用互联型);
①、EXTI寄存器作用简介
下列寄存器如无特殊说明,则均为bit0~bit19有效,其余位保留,且bit19只适用于互联型产品,对其他产品是保留状态,这20个位与EXTI20个事件/中断线相对应;
中断屏蔽寄存器(EXTI_IMR):当相关位置1表示开放相关位中断请求,反之清零表示屏蔽中断请求;
事件屏蔽寄存器(EXTI_EMR):当相关位置1表示开放相关位事件请求,反之清零表示屏蔽事件请求;
上升沿触发选择寄存器(EXTI_RTSR):置1表示允许输入线上的上升沿触发(中断/事件),反之清0表示禁止输入线上的上升沿触发(中断/事件);
外部唤醒线时边沿触发,这些线上不能有毛刺信号;
在写EXTI_RTSR寄存器时,在外部中断线上的上升沿信号不能被识别,挂起位也不会被置位。
在同一中断线上,可以同时设置上升沿和下降沿触发。即任一边沿都可触发中断。
下降沿触发选择寄存器(EXTI_FTSR):置1表示允许输入线上的下降沿触发(中断/事件),反之清0表示禁止输入线上的下降沿触发(中断/事件);
外部唤醒线时边沿触发,这些线上不能有毛刺信号;
在写EXTI_FTSR寄存器时,在外部中断线上的下降沿信号不能被识别,挂起位也不会被置位。
在同一中断线上,可以同时设置上升沿和下降沿触发。即任一边沿都可触发中断。
软件中断事件寄存器(EXTI_SWITR):软件中断;当相应位为0时,写1将设置EXTI_PR中的相应的挂起位;如果在EXTI_IMR和EXTI_EMR中允许产生该中断,则此时将产生一个中断;注:通过清除EXTI_PR中对应的位(写入1),可以将该位清0;
挂起寄存器(EXTI_PR):相应位为0表示没有发生触发请求,为1表示发生了选择的触发请求;当在外部中断线上发生了选择的边沿事件,该位被置1,此时在该位中写1可以清除它,也可以通过改变边沿检测的极性清除
上述寄存器详情请参考芯片参考手册(中文版)第9章的9.3节;
②、框图简介
图8-5所示框图在实际控制时有两条控制路线,一条是中断请求控制路线,第二条是事件请求控制路线;其中产生中断的路线为:①→②→③→请求挂起寄存器→④→⑤;产生事件的路线为①→②→③→⑥→⑦→⑧;
当然也可以看图8-6,下图中紫色线是产生中断的路线,绿色线是产生事件的路线;
图8-6:各电路功能(怕跟编号搞混,所以在下图展示功能,编号请看图8-5)
产生中断的路线:首先输入端检测到中断请求信号,然后将信号输入到边沿检测电路,边沿检测电路将根据触发电路的设置(上升沿还是下降沿)来检测信号的跳变,并将有效信号输出,此时信号将被输出到或门,或门检测到有效信号1后会将信号传递给请求挂起寄存器,如果该寄存器相关位被置1,且中断屏蔽寄存器置1那么将会输出中断请求信号到NVIC;
这里要注意的是:
边沿触发电路在检测信号时,如果设置上升沿触发,则电路输入端检测到信号从0变1,此时将输出有效信号1;
请求挂起寄存器和中断屏蔽寄存器的相关位为1表示可以产生中断;
产生事件的路线:首先输入端检测到中断请求信号,然后将信号输入到边沿检测电路,边沿检测电路将根据触发电路的设置(上升沿还是下降沿)来检测信号的跳变,并将有效信号输出,此时信号将被输出到与门,如果此时事件屏蔽寄存器的相关位也为1,则与门两条信号线都为1,此时与门发出有效信号1到脉冲触发器,脉冲触发器发出一次脉冲,该脉冲将被用于事件;
这里要注意的是:
脉冲触发器的输入端检测到信号1时发出脉冲,反之检测到0将不发出脉冲,它发出的脉冲将被用于事件,该事件可以是定时器,也可以是ADC的转换等;
事件屏蔽寄存器相关位为1则表示允许产生事件;
3、中断/事件线路映像
这一点直接看图,该图来自芯片参考手册第9章9.2.5节:
图8-7:
放出上图只是方便大家理解与GPIO有关的前16个中断/事件线;
三、程序
1、配置步骤
①、NVIC配置
>> 确认中断源,使能正确的中断线;
>> 确认主优先级;
>> 确认子优先级;
>> 使能NVIC;
>> 初始化NVIC,使用库函数NVIC_Init();
>> 编写中断服务函数;
②、EXTI配置
>>初始化产生中断的GPIO(本文使用按键输入,所以按照按键初始化方式来初始化GPIO);
>>选择GPIO的中断线;
>>初始化EXTI:
选择中断线;
设置上升沿触发还是下降沿触发;
设置EXTI模式为中断模式(另一个模式是事件模式);
使能EXTI;
初始化EXTI,使用库函数EXTI_Init();
>>初始化总中断(NVIC中断)
>> 编写中断服务函数;
2、程序
名称中带bsp的文件都是需要自己创建的,特此说明。
下面的程序8-1和程序8-2是对中断的初始化,步骤按照上面提到的步骤进行,另外在初始化EXTI时,程序中将总中断的初始化放到了开头,这个是不影响的,放在最后和放在开头效果一样:
程序8-1:bsp_exti.c中的程序
#include "bsp_exti.h"
static void EXTI13_NVIC_Config(void) //static:表示该函数只能在这个.c文件中使用
{
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);//配置优先级分组
NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn; //使用中断线13
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //设置主优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //设置子优先级
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能
NVIC_Init(&NVIC_InitStruct); //初始化NVIC
}
void EXTI_Key13_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct; //定义变量,方便赋值
EXTI_InitTypeDef EXTI_InitStruct;
//初始化总中断,放在开头和放在最后一行效果一样
EXTI13_NVIC_Config();
//初始化GPIO
RCC_APB2PeriphClockCmd(KEY13_INT_GPIO_CLK,ENABLE);
GPIO_InitStruct.GPIO_Pin = KEY13_INT_GPIO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入,不工作时电平由外部电路决定
GPIO_Init(KEY13_INT_GPIO_PORT, &GPIO_InitStruct);
//选择GPIO中断
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource13);
//初始化EXIT,外设中断
EXTI_InitStruct.EXTI_Line = EXTI_Line13; //按键在位13,所以设置line13
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising; //设置上升沿触发
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //模式设置为中断
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 设置允许
EXTI_Init(&EXTI_InitStruct); //定义的是变量,根据函数使用的是指针,加符号&
}
程序8-2:bsp_exti.c中的程序
#ifndef __BSP_EXTI_H
#define __BSP_EXTI_H
#include "stm32f10x.h"
#define KEY13_INT_GPIO_PIN GPIO_Pin_13
#define KEY13_INT_GPIO_PORT GPIOC
#define KEY13_INT_GPIO_CLK RCC_APB2Periph_GPIOC
void EXTI_Key13_Config(void);
#endif /*__BSP_EXIT_H*/
上面程序中,按键在位13,所以程序使用中断线13,此时在程序中写的时候EXTI初始化就直接写EXTI_Line13就行,但是NVIC初始化时要注意官方并没有定义EXTI13_IRQn这个变量,而是定义了EXTI15_10_IRQn,这个变量表示,中断线10到中断线15都使用这个变量;
另外NVIC初始化时定义了主优先级和子优先级,这两个优先级在本程序中随意定义成什么优先级都行,因为本文的程序中只有一个中断,但实际做项目时中断如果较多,则要考虑清楚再定义;
下面的程序3是中断服务函数,这个函数要放在官方固件库的stm32f10x_it.c
文件中,同时这个文件还要包含前文的bsp_exti.h
和后面的bsp_led.h
两个头文件,这里不在把完整的stm32f10x_it.c
文件中的程序放出,只放出需要添加的内容,包括头文件:
程序3:stm32f10x_it.c中需要另外添加的部分,包括头文件
#include "bsp_led.h"
#include "bsp_exti.h"
//上面两行放在文件开头
//下面的函数放在最后即可
void EXTI15_10_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line13) != RESET)
LED_G_TOGGLE;
EXTI_ClearITPendingBit(EXTI_Line13);
}
下面的程序4和程序5是关于LED灯的初始化,这里不再赘述,直接看程序:
程序8-4:bsp_led.c
//BSP:board support package 板级支持包,仅适用于野火开发板
#include "bsp_led.h"
void LED_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct; //定义变量,方便赋值
RCC_APB2PeriphClockCmd(LED_G_GPIO_CLK,ENABLE); //打开APB2时钟,GPIO挂载在APB2
GPIO_InitStruct.GPIO_Pin = (LED_G_GPIO_PIN); //设置需要用到的管脚,LED_G_GPIO_PIN看.h文件
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //设置输出模式为推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //设置输出速率为50MHz,LED_G_GPIO_CLK看.h文件
GPIO_Init(LED_G_GPIO_PORT, &GPIO_InitStruct); //加上&,方便取值 //初始化GPIO
}
程序8-5:bsp_led.h
#ifndef _BSP_LED_H
#define _BSP_LED_H
#include "stm32f10x.h" //要包含固件库的.h文件
#define LED_G_GPIO_PIN GPIO_Pin_0 //定义绿灯管脚号
#define LED_G_GPIO_PORT GPIOB //定义用到的GPIO
#define LED_G_GPIO_CLK RCC_APB2Periph_GPIOB //定义RCC时钟寄存器
//下面这个声明是用于将PIN0取反的,“^”为异或符号,同0不同1
#define LED_G_TOGGLE {LED_G_GPIO_PORT->ODR ^= LED_G_GPIO_PIN;}
void LED_GPIO_Config(void); //.c文件中的函数声明
#endif /*_BSP_LED_H*/
下面的程序6是main中的程序:
程序6:main.c
#include "stm32f10x.h" // 相当于51单片机中的 #include <reg51.h>
#include "bsp_led.h"
#include "bsp_exti.h"
int main(void)
{
LED_GPIO_Config(); //GPIO初始化
EXTI_Key_Config();
EXTI_Key13_Config();
// while(1)
// {//笔者尝试了一下,不加死循环也能用,所以这里屏蔽了
// }
}
以上程序,基本都有做标注,并且调用过的各个函数的含义建议直接去固件库中看官方的英文解释,英文解释更精准一点,所以这里不再展开解释,另外上述理论部分也是个人理解,可能会存在错误,后续如果再有新的感悟会更新一篇新的文章来写相关内容,感谢观看。