一、中断概念说明
-
中断:在主程序运行过程中,出现了特定的中断触发条件又叫中断源(比如:对于外部中断来说引脚发生了电平跳变,对于定时器来说定时的时间到了,对于串口通信来说可以是接收到了数据),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
好比你早上定了一个闹钟,定好之后就可以放心睡觉了,时间到了闹钟会提醒你,相当于产生了一个中断信号。如果没有定闹钟,那就得不间断的看时间生怕错过起床点这样就没法安心睡觉了
-
中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
也就是说中断优先级就是程序的紧急程度。当多个中断同时申请时,判断一下应该先处理哪个。如果事件非常紧急那就可以在程序中把优先级设置高一些,反之可以设置低一些。防止紧急的事件被别的中断事件耽误
-
中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。这种把中断程序再次中断的现象就叫中断嵌套。
中断嵌套也是照顾非常紧急的中断,如果cup已经在执行某个中断程序了,这时候又发生了一个非常紧急的中断,那这个非常紧急的中断可以把当前的中断程序二次中断,这样新的紧急中断就可以被立即执行了。能否进行中断嵌套也是由中断优先级决定的
二、Stm32中的中断
68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设
- 灰色的是内核的中断一般用不到可以了解一下
- 不是灰色的部分是Stm32外设的中断,外设电路检测到异常或事件需要提示一下CPU的时候可以申请中断,让程序调到对应的中断函数里执行一次用来处理这个异常或事件
- EXTI0~EXTI4&EXT9_5&EXTI15_10是外部中断对应的中断资源
- 中断地址:程序中的中断函数地址是由编译器来分配的是不固定的,但是中断跳转由于硬件限制只能跳到固定的地址执行程序。所以为了能让硬件跳转到一个不固定的中断函数里就需要再内存中定义一个固定的地址列表(中断向量表,相当于中断跳转的一个跳板),中断发生后就跳转到这个位置,然后由编译器加上一条跳转到中断函数的代码这样中断跳转就可跳到任意位置了。具体怎么跳又跳到哪儿都是有编译器去处理了解即可。
1、NVIC嵌套向量中断控制器
使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级
- NVIC是一个内核外设,是CPU的小助手
- 上面可以了解到Stm32中断非常多,如果把这些中断全部接到CPU上那CPU还得引出很多线进行适配,设计上就很麻烦而且容易产生拥堵导致CPU很难处理,所以NVIC就出现了。
- n:表示一个外设可能会占用多个中断通道
- NVIC只有一个输出口,根据每个中断的优先级分配中断的先后顺序然后通过一个输出口告诉CPU你该处理哪一个中断
- 响应优先级:比如很多人排队上厕所,然后你插队了类似于这种插队的优先级就叫响应优先级。
- 抢占优先级:不等厕所里的人上完直接冲到厕所里让刚才正在上厕所的人靠边站等你上完了刚才那个人再继续(其实就是上面说的中断嵌套)这种决定是不是可以中断嵌套的优先级就叫抢占优先级
- 上面提到每个中断通道都拥有16个可编程的优先等级,为了把这16个优先级再区分为响应优先级和抢占优先级就需要对着16个优先级进行分组。4位二进制可以表示0~15的数对应16个优先级,值越小优先级越高0是最高优先级
- 分组方式是在程序中自己选择的,选好分组方式之后配置优先级就要注意抢占优先级和响应优先级的取值范围了,不要超出下面表里规定的取值范围。
2、EXTI外部中断
- EXTI(Extern Interrupt)外部中断
- EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
- 支持的触发方式:上升沿/下降沿/双边沿/软件触发
- 支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断
- 通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
- 触发响应方式:中断响应/事件响应
- AFIO:中断引脚选择的电路模块,就是一个数据选择器,可以从ABC三个外设的16个引脚通道选择一个连接到后面的EXTI的通道里(比如PA1/PB1/PC1只能有一个连接到EXTI1通道上,这就是所有的GPIO口都可以触发中断,但相同的pin不能同时触发中断的原因)
- EXTI有20路输入,应该有20路中断的输出,20路中断输出太多了比较占用NVIC的通道资源,所以就把9_5&15_10各分到一个通道里,外部中断9_5会触发同一个中断函数15_10也是触发同一个中断函数
3、外部中断实例
- 旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信
号的频率和相位信息即可得知旋转轴的速度和方向 - 类型:机械触点式/霍尔传感器式/光栅式
选择编码器是外部驱动,信号是迅速的突发的,Stm32只能被动读取如果稍微晚一点读取就会错过很多波形。所以对于这种情况就可以考虑使用外部中断,有脉冲过来立即进入中断函数处理
- R1/R2:上拉电阻,默认没旋转的情况下被上拉为高电平,通过R3/R4(防止模块引脚电流过大)电阻输出到A/B端口也就是高电平;当选择时内部触电导通直接被拉低到GND了,再通过输出就是低电平了
- C1/C2:滤波电容,防止输出信号抖动
外部中断的配置流程:
1、配置RCC,把涉及到外设时钟都打开(不打开时钟外设是没法工作的)
2、配置GPIO,选择端口为输入模式
3、配置AFIO,选择指定的中断引脚连接到后面的EXTI
4、配置EXTI,选择边缘触发方式,比如上升沿、下降沿或双边沿及选择触发响应方式(中断响应/事件响应)
5、配置NVIC,给中断选择一个合适的优先级,最后通过NVIC外部中断信号就能进入CPU了跳转到指定的中断函数执行中断程序
外部中断配置涉及到外设有点多 其中涉及到的有RCC/GPIO/AFIO/EXTI/NVIC这些,从下面的Stm32系统架构图可以看出GPIO/AFIO/EXTI这些都是挂载在APB2总线上,其中EXTI和NVIC不要手动开启时钟,NVIC是内核的外设。
①EXTI_DeInit清除EXTI配置,恢复成上电默认状态
②EXTI_Init初始化EXTI外设
③EXTI_StructInit调用这个函数可以把参数传递给结构体赋一个默认值
④EXTI_GenerateSWInterrupt软件触发外部中断
⑤EXTI_GetFlagStatus获取指定的标志位收否被置为1了(主函数)
⑥EXTI_ClearFlag可以对置为1的标志位清楚(主函数) ⑦EXTI_GetITStatus获取中断标志位是否被置为1了(中断函数)
⑧EXTI_ClearITPendingBit清楚中断挂起标志位(中断函数)
前三个和后三个函数相当于库函数中模板函数所有的外设写法都是通用的(比如定时器、GPIO、ADC、串口等都是这种写法)
①NVIC_PriorityGroupConfig用来中断分组,参数数中断分组的方式 ②NVIC_Init根据结构体参数初始化NVIC
③NVIC_SetVectorTable用来设置中断向量表
④NVIC_SystemLPConfig设置系统低功耗配置
因为NVIC是内核外设,所以它的库函数被ST发配到杂项这里了
在Stm32中,中断函数的名字都是固定的,每个中断通道都对应一个中断函数。中断函数的名字我们可以早启动文件中找到里面定义了中断向量表。
以IRQHandler结尾的就是中断函数的名字
旋转编码器计次头文件Encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
旋转编码器计次c文件Encoder.c
#include "stm32f10x.h" // Device header
int16_t Encoder_Count; //全局变量,用于计数旋转编码器的增量值
/**
* 函 数:旋转编码器初始化
* 参 数:无
* 返 回 值:无
*/
void Encoder_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB0和PB1引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);//将外部中断的0号线映射到GPIOB,即选择PB0为外部中断引脚
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);//将外部中断的1号线映射到GPIOB,即选择PB1为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; //选择配置外部中断的0号线和1号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //选择配置NVIC的EXTI0线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; //选择配置NVIC的EXTI1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //指定NVIC线路的响应优先级为2
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:旋转编码器获取增量值
* 参 数:无
* 返 回 值:自上此调用此函数后,旋转编码器的增量值
*/
int16_t Encoder_Get(void)
{
/*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/
/*在这里,也可以直接返回Encoder_Count
但这样就不是获取增量值的操作方法了
也可以实现功能,只是思路不一样*/
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}
/**
* 函 数:EXTI0外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET) //判断是否是外部中断0号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //PB0的下降沿触发中断,此时检测另一相PB1的电平,目的是判断旋转方向
{
Encoder_Count --; //此方向定义为反转,计数变量自减
}
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除外部中断0号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
/**
* 函 数:EXTI1外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) == SET) //判断是否是外部中断1号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) //PB1的下降沿触发中断,此时检测另一相PB0的电平,目的是判断旋转方向
{
Encoder_Count ++; //此方向定义为正转,计数变量自增
}
}
EXTI_ClearITPendingBit(EXTI_Line1); //清除外部中断1号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
旋转编码器计次main文件main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"
int16_t Num; //定义待被旋转编码器调节的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Encoder_Init(); //旋转编码器初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Num:"); //1行1列显示字符串Num:
while (1)
{
Num += Encoder_Get(); //获取自上此调用此函数后,旋转编码器的增量值,并将增量值加到Num上
OLED_ShowSignedNum(1, 5, Num, 5); //显示Num
}
}