一、 中断定义及相关概念
中断是处理器中的一种机制,用于响应和处理突发事件或紧急事件。当发生中断时,当前正在执行的程序会被暂时中止,处理器会跳转到中断处理程序(也称为中断服务例程),对中断事件进行处理。处理完中断后,处理器再返回到被中断的程序继续执行。
- 中断分类:
中断可以分为内部中断和外部中断。
a. 内部中断:由处理器内部的模块或事件引发,例如定时器溢出、串口接收缓冲区非空等。内部中断可以用于定期执行特定任务、检测状态变化等。
b. 外部中断:由外部设备或外部信号引发,例如按键按下、外部传感器信号变化等。外部中断用于响应外部事件,并及时处理相关任务。 - 中断向量表(Interrupt Vector Table):
中断向量表是一个存储中断处理程序地址的表格。每个中断都有一个特定的向量,当中断发生时,微控制器会根据中断向量表中相应中断的地址跳转到对应的中断处理程序。 - 中断请求(Interrupt Request):
也称为中断源或中断事件,是指外部设备或内部模块发出的信号,通知微控制器发生了一个特定的事件,例如按键按下、定时器溢出等。 - 中断控制器(Interrupt Controller):
中断控制器负责对中断请求进行管理和分配优先级。 - 中断优先级(Interrupt Priority):
每个中断都具有一个优先级,用于确定中断处理程序的执行顺序。较高优先级的中断会打断正在执行的较低优先级中断或主程序。 - 中断处理程序(Interrupt Service Routine,ISR):
也称为中断服务函数,是一段用于处理特定中断的代码。当中断发生时,微控制器会跳转到相应的中断处理程序执行相关操作,处理完毕后返回主程序。 - 中断使能(Interrupt Enable):
通过设置相应的中断使能位,可以启用或禁用特定的中断。禁用中断后,即使中断请求发生,微控制器也不会响应。 - 中断标志(Interrupt Flag):
中断标志用于指示特定中断是否已经发生。在处理完中断后,需要清除相应的中断标志,以便再次触发相同中断。
中断机制在计算机系统中起着至关重要的作用,它能够提高系统的实时性、并发处理能力、资源共享能力,并为系统的可靠性和可维护性提供支持。
二、 过程描述
具体过程分为进入中断和退出中断,其具体流程如下。
- 进入中断前:
a. 处理器自动保存现场到堆栈里:按照{PC, xPSR, R0-R3, R12, LR}的顺序压入堆栈;
b. 入栈结束,中断服务例程(ISR)便可开始执行;
c. 晚到的中断会重新取ISR地址,但无需再次保存现场。 - 退出中断后:
a. 中断前的现场被自动从堆栈中恢复;
b. 一旦出栈完成,继续执行被中断打断的指令;
c. 出栈的过程也可被打断,使得随时可以响应新的中断, 而不再进行;
d. 现场保存。
处理器根据中断事件的优先级来确定执行的顺序,这里举一个生活中的例子,比如学习STM32(主进程),此时狐朋狗友说英雄联盟的无限火力模式开始了(中断1请求),邀请你一起去玩,经过一番思想斗争后决定还是先放松一下,但为了回来后可以接着目前的进度继续学习,你插入了一个书签,将做的笔记在电脑上进行了保存(现场保存),打开英雄联盟开始玩(执行中断服务程序1);一个小时后,女朋友打电话让你陪她逛街(中断2请求),琢磨了一下还是女朋友比较重要(即中断2比中断1的优先级高),于是将狐朋狗友晾在了一边,去陪女朋友,哈哈哈,典型的重色轻友型。但如果考虑后觉得游戏比较重要,选择无视女朋友(即将中断1的优先级调高,使用NVIC,具体方法见下一节),这种情况怎么可能有女朋友。当两件邀约都执行完毕后,回来继续从书签位置学习(退出中断,恢复现场)。
注:
PC——程序计数器
xPSR——程序状态字寄存器组
R0-R3, R12——通用寄存器
LR——连接寄存器
三、 关于嵌套向量中断控制器(NVIC)
STM32(Cortex-M3)中有两个优先级的概念——抢占式优先级和响应优先级,有人把响应优先级称作“亚优先级”或“副优先级”,每个中断源都需要被指定这两种优先级。
具有高抢占式优先级的中断可以在具有低抢占式优先级的中断处理过程中被响应,即中断嵌套,或者说高抢占式优先级的中断可以嵌套低抢占式优先级的中断。
当两个中断源的抢占式优先级相同时,这两个中断将没有嵌套关系,当一个中断到来后,如果正在处理另一个中断,这个后到来的中断就要等到前一个中断处理完之后才能被处理。如果这两个中断同时到达,则中断控制器根据他们的响应优先级高低来决定先处理哪一个;如果他们的抢占式优先级和响应优先级都相等,则根据他们在中断表中的排位顺序决定先处理哪一个。
NVIC(Nested Vectored Interrupt Controller)是一种中断控制器,用于管理中断系统和中断请求在ARM Cortex-M处理器中的操作。通过使用NVIC,可以对中断系统进行灵活配置和管理,调整中断优先级、使能或禁用中断,并实现中断处理程序的跳转和嵌套。这样能够使处理器能够高效地响应和处理中断请求,提高系统的实时性和并发处理能力。
分组方式 | AIRCR[10:8] | Bit[7:4]分配情况 | 抢占优先级 | 响应优先级 |
---|---|---|---|---|
分组0 | 111 | 0:4 | 0位,取值为0 | 4位,取值为0~15 |
分组1 | 110 | 1:3 | 1位,取值为0~1 | 3位,取值为0~7 |
分组2 | 101 | 2:2 | 2位,取值为0~3 | 2位,取值为0~3 |
分组3 | 100 | 3:1 | 3位,取值为0~7 | 1位,取值为0~1 |
分组4 | 011 | 4:0 | 4位,取值为0~15 | 0位,取值为0 |
相关寄存器:
- 中断使能寄存器组(Interrupt set-enable registers,NVIC_ISERx),共8个32位的寄存器,但只用了前两个,ISER[0]的 bit0-bit31 分别对应中断 0-31,ISER[1]的 bit0-27 对应中中断32~59;这样总共 60 个中断就分别对应上了。你要使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能。
- 中断除能寄存器组(Interrupt Clear-Enable Registers,NVIC_ICERx): 8个32位寄存器来控制,同样只用到前两个,每个位控制一个中断,除能设置相应ICER位为1。
- 中断挂起控制寄存器组(Interrupt Set-Pending Registers,NVIC_ISPRx):8个32位寄存器来控制,每个位控制一个中断,中断挂起设置相应ISPR位为1。
- 中断解挂控制寄存器组(Interrupt Clear-Pending Registers,NVIC_ICPRx):8个32位寄存器来控制,每个位控制一个中断,中断解挂设置相应ICPR位为1。
- 中断激活标志位(Interrupt Active Bit Registers,NVIC_IABRx):8个32位寄存器来控制,每个位控制一个中断,只读,当前正在执行的中断被设置成1,执行完后自动清0。
- 中断优先级控制寄存器组(Interrupt Priority Registers,NVIC_IPRx):240个8位的寄存器来控制,STM32F1可屏蔽中断只用到了前68个(IP[67]~IP[0]),每个8位寄存器只用到了高4位( bit[7:4] ),抢占优先级在前,响应优先级在后,优先级各占几个位根据 SCB->AIRCR [10:8] 来决定。
中断向量表:
四、 外部中断/事件控制器(EXTI)
这里的外部中断是指由外部条件触发例如按键触发(GPIO),对于互联型产品(F107),外部中断/事件控制器由20个产生事件/中断请求的边沿检测器组成,对于其它产品(我们这里是F103),则有19个能产生事件/中断请求的边沿检测器。每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求。下图为EXTI的基本结构。
根据上图可以得到EXTI功能设置的基本流程:
- AFIO中断引脚选择
该系统总过包含有16条外部中断/事件线,每组GPIO引脚(即GPIOAGPIOG)包含16个引脚,即GPIOx[0]GPIOx[15],需要使用AFIO将GPIO引脚完成复用功能引脚重映射及中断引脚选择,映射情况如图所示,中断线每次只能连接到 1 个IO 口上,这样就需要通过配置来决定对应的中断线配置到哪个 GPIO 上了。
另外四个EXTI线的连接方式如下:
EXTI线16连接到PVD输出;
EXTI线17连接到RTC闹钟事件;
EXTI线18连接到USB唤醒事件;
EXTI线19连接到以太网唤醒事件(只适用于互联型产品)。
注:通过AFIO_EXTICRx配置GPIO线上的外部中断/事件,必须先使能AFIO时钟。 - EXTI边沿检测及控制设置
如图所示为EXTI边沿检测及控制设置的框图,图中包含了响应中断和事件,本文只探讨外部中断即红色箭头所含流程。
外部信号输入后,通过边沿检测电路进行检测,当检测到所选择边沿或软件事件到来时,进入中断挂起状态,等待处理器响应,当该中断未被屏蔽时,信号被输出至NVIC控制器进行后续处理。
图中上升/下降沿触发选择寄存器可以对硬件触发信号类型进行选择,也可以对软件中断事件寄存器进行定义从而使用软件方式进行触发。
- NVIC初始化
根据工程需要对中断线进行选择,并设置其抢占优先级和响应优先级。
五、 例程分析
本文结合江科协所给旋转编码器计次的历程进行分析。先大概讲一下旋转编码器的原理。视频讲解连接:[5-2] 对射式红外传感器计次&旋转编码器计次_哔哩哔哩_bilibili
- 关于编码器
编码器(Encoder)是一种用于测量旋转运动或位置的设备。编码器通常与定时器模块一起使用,以便在微控制器中获取和计算旋转的脉冲数量,从而确定物体的运动方向和距离。编码器在许多应用中都很有用,例如机器人运动控制、电机位置反馈和位置传感。本文使用的是AB相编码器。
AB相编码器,也称为正交编码器或增量式编码器,是一种常用于测量旋转位置和方向的设备。它基于两个输出信号通道,通常称为A相和B相,这两个信号在相位上相差90度,用于确定旋转方向和计数。当旋转运动发生时,A相和B相信号的脉冲数量发生变化。这两个信号相位差90度,可以通过监测它们的变化来确定旋转方向和计数值。
增量式编码器的输出是增量脉冲,需要进行计数和积分以获得位置信息。可以使用两个通道的信号来测量速度。
由图中可知,由于两相之间存在90°的相位差,可以通过判断其中一相上升/下降沿时,另一相是高或者低电平判断转动方向。 - 代码分析
(1) 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);//引脚功能复用使能
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);//传统艺能,定义GPIO引脚方向及使能,这里选用GPIOB_PIN_0和GPIOB_PIN_1
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);//用于配置EXIT外部中断/事件的GPIO中断源,传入的参数是GPIO和相应的IO口,框图第一步
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);//框图第二步,将IO口映射到中断线EXTI_LINE0和EXTI_LINE1,选择模式为中断,触发方式为下降沿触发并使能
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);//框图第三步,对NVIC进行定义,设置抢占优先级和响应优先级
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStructure);
}
int16_t Encoder_Get(void) //返回计数变化方向,正转为+1,反转为-1,在主函数中进行累计
{
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}
void EXTI0_IRQHandler(void)//反向旋转
{
if (EXTI_GetITStatus(EXTI_Line0) == SET)//检测到A相的下降沿
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)//防抖,A相低电平
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)//B相低电平
{
Encoder_Count --;//计次数减1
}
}
EXTI_ClearITPendingBit(EXTI_Line0);//注意!!!手动清除中断标志位,否则会一直在这个中断里
}
}
void EXTI1_IRQHandler(void)//正向旋转
{
if (EXTI_GetITStatus(EXTI_Line1) == SET)//检测到B相下降沿
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)//防抖,B相低电平
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)//A相低电平
{
Encoder_Count ++;//计次数加1
}
}
EXTI_ClearITPendingBit(EXTI_Line1);//注意!!!手动清除中断标志位,否则会一直在这个中断里
}
}
(2) 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:");//屏幕固定内容显示
while (1)
{
Num += Encoder_Get();//计次累加/减
OLED_ShowSignedNum(1, 5, Num, 5);//计次数显示
}
}
六、 总结
本文分析了STM32的中断系统,形象描述了中断的重要概念,结合寄存器描述了NVIC及EXTI,围绕旋转编码器旋转计次的例程分析了外部中断的使用流程。