STM32 GPIO中断机制详解(进阶篇)
什么是中断
所谓中断,就是CPU在执行某个任务的时候,遇到了突发事件,CPU需要先处理好这个突发事件,而“中断”就是这样一个突发事件,只有当CPU执行完这个突发事件才能继续之前的任务。
说到这,你肯定有疑问,CPU为什么不把这个“中断”在最开始的时候就成为CPU的主线任务呢,而非得半路杀出个程咬金式的。
这还真有些讲究,从CPU的角度来讲,我堂堂CPU,是单片机最主要的模块,朕平时有大量的工程要跑,你这“外部中断”的事儿又不是集中在某一时间段,还得朕等着你“中断”的号召,朕才能开始干你那活,太没有效率了;再者,站在“中断”的角度来说,我这个只需要CPU你过来处理一会儿就行,速度很快,一直用CPU来处理反而是小题大做。一般速度很快的程序都会放在中断函数里,靠CPU的话无法跟上。所以,一般在中断函数内要避免使用Delay这类耗时的延迟函数
怎么使用GPIO的外部中断
先明确一点,要执行中断函数的是CPU,被执行的是中断。
那么中断怎么才能被CPU执行呢?
在stm32中有很多的中断,而CPU的任务主要是运算,这时候就需要一个“管家”来管理这些中断的优先级,好让CPU知道它该执行哪个中断,这个管家的名字叫做“NVIC”.这些外设的中断最终都会先通过NVIC,然后再交给CPU执行
对于NVIC,它需要选择一个组,每个优先级要选取值是几,然后按照下图方式的优先级顺序依次让CPU执行相应中断,下面的几位代表着他们的优先顺序,抢占优先级高于响应优先级(后面写代码时会提及这部分怎么写)
分组方式 | 抢占优先级 | 响应优先级 |
---|---|---|
分组0 | 0位,取值为0 | 4位,取值为0~15 |
分组1 | 1位,取值为0~1 | 3位,取值为0~7 |
分组2 | 2位,取值为0~3 | 2位,取值为0~3 |
分组3 | 3位,取值为0~7 | 1位,取值为0~1 |
分组4 | 4位,取值为0~15 | 0位,取值为0 |
有人会觉得,感觉还是不太理解怎么让中断产生,然后让CPU执行呢,下面以外部中断为例子来讲解
外部中断的介绍
•EXTI(Extern Interrupt)外部中断
•EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
•支持的触发方式:上升沿/下降沿/双边沿/软件触发
•支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断,看下面的图就知道,GPIOA、GPIOB\GPIOC······这些只有一个能由AFIO通过那里的“16”出来,不会出现pinA0和PINB0同时产生中断的现象
•通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
•触发响应方式:中断响应/事件响应(右下角处的20条通道通向的其他外设)
注意:5 ~ 9为一个通道,10~15为一个通道,涉及具体是哪个的时候需要在NVIC另外判断
AFIO的介绍
•在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
下图是数据选择器选择一个口,然后对应输出一个数据,总共选择16个
下面举几个例子来实操上面说的
案例1:由红外传感器进行数字加减,数字显示在OLED上
模块化红外传感器代码
先复习GPIO的使用方法
1.使用RCC开启GPIO的时钟
2.使用GPIO_Init函数初始化GPIO
定义一个结构体,模式选8种中的一个,引脚选择所需要的,速度可以选50的
3.使用输出或输入的函数控制GPIO口
在这里AFIO没有初始化,不知道为什么,但EXTI、NVIC还是需要上述全部流程
#include "stm32f10x.h" // Device header
#include "Delay.h"
uint16_t CountSensor_Count;
void CountSensor_Init()
{
//1.使用RCC开启GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //选择GPIOB进入AFIO,因为自己红外传感器在面包板上的接线口在B14引脚
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //对AFIO打开使能
//2.建立一个GPIO_InitTypeDef类型的结构体,命名为GPIO_InitStructure
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//选择上拉输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;//选择24引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度一般就选50MHZ
GPIO_Init(GPIOB,&GPIO_InitStructure);//调用GPIO的初始化函数
//配置AFIO的数据选择器,选择我们需要的中断引脚,在这里是GPIOB14
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
//EXTI的初始化
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line =EXTI_Line14 ;
EXTI_InitStructure.EXTI_LineCmd =ENABLE ;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;//决定触发方式
EXTI_Init(&EXTI_InitStructure);
//NVIC选择哪组(这里选哪个都一样,没特殊要求)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//对NVIC初始化
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;//选通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//打开使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =1 ;//跟选的组有关
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //跟选的组有关
NVIC_Init(&NVIC_InitStructure);
}
//通过这个函数获取每一次触发中断后的计数值
uint16_t CountSensor_Get()
{
return CountSensor_Count;
}
//GPIOB14的外部中断函数
void EXTI15_10_IRQHandler()
{
if(EXTI_GetITStatus(EXTI_Line14) == SET )
{
CountSensor_Count++;
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
注意:中断函数不需要声明
main.c
#include "stm32f10x.h" // Device header
#include"Delay.h"
#include"OLED.h"
#include "CountSensor.h"
int main(void)
{
OLED_Init();
CountSensor_Init();
OLED_ShowString(1,1,"Count:");
while(1)
{
OLED_ShowNum(1,7,CountSensor_Get(),5);
}
}
案例2:旋转控制数字变化
在这里,我用的是GPIOB1和GPIOB0着两个引脚
#include "stm32f10x.h" // Device header
int16_t Encoder_Count;
void Encoder_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
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_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
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_Rising;
EXTI_Init(&EXTI_InitStructure);
//设置一个组就行
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_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()
{
int16_t temp;
temp = Encoder_Count;
Encoder_Count = 0;
return temp;
}
void EXTI0_IRQHandler()
{
if(EXTI_GetITStatus(EXTI_Line0) == SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0 )
{
Encoder_Count--;
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void EXTI1_IRQHandler()
{
if(EXTI_GetITStatus(EXTI_Line1) == SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0) == 0 )
{
Encoder_Count++;
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
main.c
#include "stm32f10x.h" // Device header
#include"Delay.h"
#include"OLED.h"
#include "Encoder.h"
int16_t Number;
int main(void)
{
OLED_Init();
Encoder_Init();
OLED_ShowString(1,1,"hello wrld!");
while(1)
{
Number += Encoder_Get();
OLED_ShowSignedNum(2,1,Number,5);
}
}
总结
对于单片机的学习不能止步于抄写他人的代码,重要的是自己也能看懂代码中的原理,学会自己从理论的知识变成可以运行的代码,路很长,我想我还需要继续学习,与诸君共勉。
作者:light to dawn