stm32标准库开发(二):EXTI外部中断
概述
EXTI(External Interrupt,外部中断)是stm32用于处理外部信号的重要模块,中断允许CPU暂停执行当前程序,转而去执行中断程序,处理完了继续返回主程序执行,中断的机制提高了CPU执行任务的灵活度,许多常见的任务离不开外部中断的支持,它通过引脚外部电平触发中断,例如按键触发中断、热敏/光敏电阻的感应控制电路、红外对射管计数、旋转编码器计数等,以下记录了EXTI标准库的相关配置。
中断向量表
stm32f10x系列芯片提供了68个可屏蔽中断通道,也即68个中断源,其中13个是内核的中断,用于处理内核的异常,在裸机开发中很少使用;其他常用的包括EXTI中断,定时器中断(定时任务)、DMA中断(通知转运数据)、串口、SPI、I2C、其他功能性中断(如看门狗中断、电压检测)等,说明几乎所有外设都能享用主控的中断资源。
因为硬件上的限制,stm32f10x通过固定的编址以存储中断程序位置,中断向量表是中断的一个重要数据结构,记录了中断服务和中断程序地址的映射关系,部分如下: 在裸机开发中通常不必关心地址值,但对操作系统和内核而言,这是重要的。
NVIC嵌套中断向量控制器
在裸机开发中,需要配置好EXTI服务程序、中断优先级,再配置相应引脚就能开启中断服务,负责前两个任务的是NVIC(Next Vector Interrupt Controoler)结构。 NVIC是服务于CPU的结构的中断仲裁结构,负责调配中断顺序,当多个机构申请中断时,NVIC会让最高优先级的中断通过CPU执行。
中断优先级(抢占与响应优先级)
stm32的中断优先级分为两种,分别是抢占优先级和响应优先级;抢占优先级是针对中断抢占行为的,例如一个中断抢占优先级低的程序可以再次中断,去执行抢占优先级高的中断程序,这种行为称嵌套中断。响应优先级是针对抢占优先级相同时,CPU首先响应响应优先级更高的中断。当抢占、响应优先级均相同时,CPU通常按照中断号顺序响应中断。stm32的优先级寄存器是4位,即最多16种优先等级,且无论抢占还是响应,0代表最高优先级,15代表最低优先级。
4位寄存器被自由分成抢占和响应优先级使用,为了细化,stm32定义了5种不同的分组用于描述不同的划分方法,这个和配置密切相关:
EXTI细节
stm32f10x系列的EXTI资源包含20个,其中16个是引脚电平,其余四种分别是外加PVD输出(电压检测)、RTC闹钟、USB唤醒、以太网唤醒等,主要用于低功耗设计,本文主要介绍GPIO触发EXTI中断。
其次stm32的EXTI中断响应分为两种,一种是CPU的中断响应,代表CPU接收了中断,去执行某些逻辑任务;另外一种是事件响应,不会经过CPU处理,而是直接连接到其他片内外设(如ADC等)执行任务。EXTI的逻辑连接如图: 其中AFIO用于引脚的复用,可以理解成中断引脚的选择,逻辑上有16个中断引脚,但是实际引脚数目大于这个数目,比如PA/PB/PC组就有48个引脚了,因此只能通过AFIO将其中某些引脚映射到者16个中断引脚,因此规定同一个pin(数字相同)不能复用,例如PA0已经链接到EXTI0,PB0/PC0都不应该再作为中断引脚了。
其次在stm32中,虽然存在16个中断引脚,但是实际引脚响应只有7个函数,其中0——4仍然独享中断函数EXTI0——EXTI4,5——9引脚和10——15引脚分别共享EXTI9_5和EXTI15_10;
此外,EXTI支持的触发方式有四种:上升沿、下降沿、双边沿、软件触发;
EXTI中断实现旋转编码器计数
EXTI中断配置步骤如下,设置GPIO引脚、开启AFIO的复用,将设置EXTI中断源、EXTI配置,使用NVIC配置中断优先级和中断函数服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39void Encoder_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //配置GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //配置AFIO时钟,负责中断引脚的选择
//配置PB0/PB1
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//EXTI中断源,20个,其中引脚16个,GPIO_PortSourceA-G,PinSource0—15
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
//类似GPIO结构体配置
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line=EXTI_Line0|EXTI_Line1; //20个中断源EXTI_Line0——19
EXTI_InitStruct.EXTI_LineCmd=ENABLE; //开启
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt; //中断响应/事件响应,前者
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Falling; //下降沿触发
EXTI_Init(&EXTI_InitStruct);
//优先级分组2
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel=EXTI1_IRQn; //中断1通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1; //抢占优先级(分组2,取0—3)
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE; //开启
NVIC_InitStruct.NVIC_IRQChannelSubPriority=2; //响应优先级(分组2,取0—3)
NVIC_Init(&NVIC_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel=EXTI0_IRQn; //中断0通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1; //抢占优先级(分组2,取0—3)
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE; //开启
NVIC_InitStruct.NVIC_IRQChannelSubPriority=1; //响应优先级(分组2,取0—3)
NVIC_Init(&NVIC_InitStruct);
}Library/stm32f10x_exti.c
,NVIC属于内核的杂项配置,来自Library/misc.c
,其中NVIC_InitStruct.NVIC_IRQChannel
的配置比较特殊,因为和内核密切相关,因此要参考Start/stm32f10x.h
,还需要从f10系下不同密度分类选择正确的宏定义,例如c8t6属于STM32F10X_MD
,应该从此定义下寻找中断函数定义。
中断处理函数定义于汇编文件Start/startup_stm32f10x_md.s
,然后对应将中断函数完善在头文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30int16_t EncoderNum=0;
//中断源0的处理程序(只要中断发生就自动调用)
void EXTI0_IRQHandler(void){
if(EXTI_GetITStatus(EXTI_Line0)==SET){ //确认0号引脚中断
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)==0){ //去抖,delay函数延迟高,任务去抖
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0) //检测电平
EncoderNum++;
EXTI_ClearITPendingBit(EXTI_Line0); //清除中断,防止一直中断
}
}
}
//中断源1的处理程序(只要中断发生就自动调用)
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)==1) //检测电平
EncoderNum--;
EXTI_ClearITPendingBit(EXTI_Line1);//清除中断,防止一直中断
}
}
}
int16_t Encoder_Get(void){
int16_t Num;
Num=EncoderNum; //获取变化值,主程序累计变化值计数
EncoderNum=0;
return Num;
}
在中断处理函数中,习惯再次判断中断是否产生请求,使用
EXTI_GetITStatus
会检查对应寄存器标志位是否置位,处理完需要使用EXTI_ClearITPendingBit
复位。EXTI文件下另一组相似的函数是:1
2ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);这两个同样查看中断标志位,但一般不用于中断程序中,而是用于轮询系统(主程序)中判断某些外部中断是否发生。1
2FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
void EXTI_ClearFlag(uint32_t EXTI_Line);旋转编码器的原理 这里的旋转编码器是一种正交编码器,产生的信号如图,A相、B相的相位差为90°和-90°,因此能够识别编码器是正转还是反转,方波的频率也是编码器的速率,但是计数编码器一般不关心这个速率。
另一种可测方向、速率的编码器是使用方波来衡量速度,另一个信号用高低电平来表示方向正转还是反转,称带方向编码器,了解即可。
因此这里每次中断发生后,需要判断另一个相的电平,实际上判断高电平还是低电平均可,因为90°相位差二者总各占一半。
main函数:累计计数即可 1
2
3
4
5
6
7
8
9//main.c
OLED_Init();
Encoder_Init();
int16_t count = 0;
while(1){
OLED_ShowString(1,1,"Num:");
count += Encoder_Get(); //累计
OLED_ShowSignedNum(2,1,count,4);
}