STM32F103的中断系统
说到中断就离不开嵌套向量中断控制器(NVIC),NVIC控制整个芯片的中断相关问题,NVIC属于是内核里面的一个外设,因此与其说是STM32F103的中断系统,倒不如说是Cortex-M3(STM32F103的内核)的中断系统。
Cortex-M3内核支持256个中断,其中16个内部中断,240个外部中断。以及256级中断优先级的设置。
而STM32可支持85个中断,其中16个内部中断,68个外部中断。以及16级中断优先级的设置。
下图出自《STM32F10xxx参考手册(中文)》,STM32支持68个中断通道(中断通道!=中断)。
中断优先级
STM32或者说Cortex-M3中的优先级分为两种,分别是抢占优先级和响应优先级。
如果一个中断事件触发了,那么会打断主函数,进入中断函数。但此时另一个中断事件也触发了,如果后来的中断事件的抢占式优先级更高,那么第二个中断事件会打断第一个中断,进入第二个中断函数里,这也被称为中断嵌套。这就是抢占优先级的作用。
响应优先级相对鸡肋一些,如果两个中断事件的抢占优先级不一样,那么响应优先级派不上用场。
在抢占优先级一样的情况下,如果两个中断事件来的时间不一样,那么响应优先级也是派不上用场的。
响应优先级的作用是,当两个抢占优先级一样的中断事件同时触发的时候,响应优先级高的事件优先触发。
当两种优先级相同的两个事件同时触发的时候,触发的顺序按照中断表(参考手册的P132)中的排位顺序触发。
在STM32中用来配置两种优先级的寄存器用到4位,也就是说两个优先级的等级全靠4个bit。
所以我们有五种选择来分配两种优先级的数值。
分别是:
4位用于指定抢占优先级,0位用于指定响应优先级。
3位用于指定抢占优先级,1位用于指定响应优先级。
2位用于指定抢占优先级,2位用于指定响应优先级。
1位用于指定抢占优先级,3位用于指定响应优先级。
0位用于指定抢占优先级,4位用于指定响应优先级。
可以配置的优先级等级为2^(位数),所以可以知道可分配的优先级最多为16级,但是另外一种优先级就无法配置了。
一般我们是抢占优先级和响应优先级各分2位,或者是抢占优先级多一位。
分配的优先级数值越小,优先级越高。
外部中断/事件控制器
以下两图取自《STM32F10xxx参考手册(中文)》P135和P137
主要看一看第二个图,可以看的出来一共有16个可映射的外部中断线EXTIn(n=0~15)。
但是每个通道的左侧对应的确实GPIOXn(X=A~G,n=0~15),因此可以知道GPIOA的0号引脚和GPIOB的0号引脚共用一个中断线EXTI0,以此类推。
同一个时刻,只能有一个端口的n号引脚映射到外部中断线中,并且引脚号需要和外部中断线的号一样。也就是说我们用了GPIOA的0号引脚作为外部中断,那么就不能用GPIOB的0号引脚作为外部中断了。
另外,映射到外部中断线的引脚需要配置为输入模式,一般不配置为浮空输入,多为下拉输入或上拉输入。
如果我们使用GPIO口来映射到中断线的话,我们需要使用AFIO,打开对应的时钟以及进行对应的设置。
但如果配置的中断不是GPIO口的外部中断的话则不用。
固件库函数
配置AFIO映射
参数一选择要映射到中断线的GPIO端口,可选参数为GPIO_PortSourceGPIOX(X=A~G)
参数二选择要映射到中断线的GPIO引脚号,可选参数为GPIO_PinSourceX(X=0~15)
虽然这个函数名的开头是GPIO,但是其实是属于AFIO寄存器的。
分配优先级位数
参数选择分组,可选的选项有NVIC_PriorityGroupX(X=0~4)。
X为为抢占优先级分配的位数,而响应优先级分配的位数就为4-X了。一般五五开,各占两位。
不论整个程序用了多少个中断,这个函数仅需调用一次,因为我们指定优先级的位数就只有4位,指定一次即全局使用了。
初始化中断通道的优先级
这个初始化函数的参数只有一个结构体变量。
NVIC_IRQChannel:指定中断通道。在stm32f10x.h文件中可以找到。
找到对应的STM型号,我这里是STM32F103C8T6属于MD的,因此我找到对应的位置。
此外在所有#ifdef之上还有所有型号通用的中断通道。
NVIC_IRQChannelPreemptionPriority:指定抢占优先级。根据分配的位数来确定填入参数的范围(0~(2^n)-1)n为分配的位数。数值越小,优先级越高
NVIC_IRQChannelSubPriority:指定响应优先级。
NVIC_IRQChannelCmd:使能,ENABLE或是DISABLE。
配置外部中断
这个函数的参数同样只有一个结构体变量。
EXTI_Line:指定中断线,参数为EXTI_LineX(X为0~15),使用的GPIO哪个引脚就选哪条中断线。
EXTI_Mode:指定模式,可选中断模式或者事件模式,一般都是中断模式(我没用过事件模式),EXTI_Mode_Interrupt。
EXTI_Trigger:选择触发的方式,可选上升沿EXTI_Trigger_Rising,下降沿EXTI_Trigger_Falling,上升沿和下降沿都行EXTI_Trigger_Rising_Falling。
EXTI_LineCmd:ENABLE或者DISABLE。
中断函数
可以在startup_stm32f10x_md.s文件里找到对应中断线的中断函数。
中断函数要求都是无参无返回值的。
中断一旦发生,那么中断标志位都会被置位,需要我们软件手动清除,如果不在中断函数里清除中断标志位的话,那么一旦触发了一次中断,由于中断标志位始终处于置位状态,那么CPU会不断地进入中断函数。
获取中断标志位
参数为中断线,我们可以通过这个函数知道中断是否是由某条中断线触发的。参数为EXTI_LineX(X=0~19)
清除中断标志位
参数同上,通过这个函数清除中断标志位。
其他中断
稍微小总结一下,外部中断需要配置AFIO+NVIC+EXTI。不过内部中断仅需要NVIC加上对应的中断启动函数即可。
通常我们都可以在对应的头文件中找到中断启动函数。
ADC的中断配置函数:
USART的中断配置函数:
I2C的中断配置函数:
可以总结出来,不同的内部中断配置函数都有一个共同点,那就是函数名以ITConfig结尾,具体的配置方法就参考具体的文件里的注释了。
接线
两个按钮分别接在了GPIOA的6号引脚和7号引脚上,6号引脚的按钮的另一端接在高电平,7号引脚的按钮的另一端接在低电平。
6号按钮按下,则会产生上升沿电平进入触发中断,因此GPIOA的6号引脚应该配置为下拉输入,即默认是低电平。
7号按钮按下,则会产生下降沿电平进入触发中断,因此GPIOA的7号引脚应该配置为上拉输入,即默认是高电平。
现象
代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
uint16_t count1=0;
uint16_t count2=0;
int main(void){
OLED_Init();
//无需打开EXTI和NVIC的时钟.
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //打开AFIO引脚映射寄存器的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开GPIOA的时钟
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource6); //将GPIO口的引脚映射到中断线上
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource7); //虽然函数名为GPIO开头,实际上属于AFIO
//将GPIOA的6号引脚配置为下拉输入模式,默认读取的电平为低电平(逻辑0)
GPIO_InitTypeDef gitd;
gitd.GPIO_Mode=GPIO_Mode_IPD;
gitd.GPIO_Pin=GPIO_Pin_6;
gitd.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_Init(GPIOA,&gitd);
//将GPIOA的7号引脚配置为上拉输入模式,默认读取的电平为高电平(逻辑1)
gitd.GPIO_Mode=GPIO_Mode_IPU;
gitd.GPIO_Pin=GPIO_Pin_7;
GPIO_Init(GPIOA,&gitd);
//配置NVIC优先级位数分组,抢占优先级和响应优先级各两位,数值范围为0~3.
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//配置中断通道的优先级
NVIC_InitTypeDef nitd;
nitd.NVIC_IRQChannel=EXTI9_5_IRQn;
nitd.NVIC_IRQChannelCmd=ENABLE;
nitd.NVIC_IRQChannelPreemptionPriority=1; //抢占优先级
nitd.NVIC_IRQChannelSubPriority=1; //响应优先级
NVIC_Init(&nitd);
//配置外部中断
EXTI_InitTypeDef eitd;
eitd.EXTI_Line=EXTI_Line6; //GPIOA的6号引脚对应6号中断线
eitd.EXTI_LineCmd=ENABLE;
eitd.EXTI_Mode=EXTI_Mode_Interrupt; //选择中断模式
eitd.EXTI_Trigger=EXTI_Trigger_Rising; //上升沿触发,因为GPIO口配置为了下拉输入
EXTI_Init(&eitd);
eitd.EXTI_Line=EXTI_Line7; //GPIOA的7号引脚对应7号中断线
eitd.EXTI_Trigger=EXTI_Trigger_Falling; //下降沿触发,因为GPIO口配置为了上拉输入
EXTI_Init(&eitd);
OLED_ShowString(1,1,"count1: ");
OLED_ShowString(2,1,"count2: ");
while(1){
OLED_ShowNum(1,9,count1,4);
OLED_ShowNum(2,9,count2,4);
}
}
//在startup_stm32f10x_md.s文件里找到对应中断线的中断函数
void EXTI9_5_IRQHandler(void){
if(EXTI_GetFlagStatus(EXTI_Line6)){ //因为两条中断线用的都是同一个函数,因此通过获取中断标志位来判断是谁触发的中断
++count1;
Delay_ms(10); //消除机械按键的抖动
while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)==1);
Delay_ms(10);
EXTI_ClearITPendingBit(EXTI_Line6); //需要手动清除中断标志位,否则会一直重复触发.
}else if(EXTI_GetFlagStatus(EXTI_Line7)){
++count2;
Delay_ms(10);
while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_7)==0);
Delay_ms(10);
EXTI_ClearITPendingBit(EXTI_Line7);
}
}
参考
b站江科大自化协
《STM32F10xxx参考手册(中文)》
《ARM Cortex-M3 嵌入式原理及应用 基于STM32F103微控制器 》