1 中断系统
1.1 中断简介
中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行。
比如:对于外部中断而言,可以是引脚电平发生跳变;对于定时器而言,可以是定时事件到了。对于串口通信而言,可以是接收到了数据。
中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。(自己设置的)
中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。
1.2 中断流程图
程序由硬件电路自动跳转到中断程序。
保存现场,还原现场(使用C语言编程不用我们考虑,操作系统做)
正常情况下,程序在主函数中执行,当中断条件满足时,主程序会暂停,然后自动跳转到中断程序;当中断程序执行完后,再返回主程序继续执行。一般中断程序都是在一个子函数的,这个函数不需要我们调用,当中断来临时,由硬件自动调用这个函数。
1.3 STM32的中断
68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设。
(中断通道就是中断源的意思)
EXTI:外部中断
TIM:定时器
ADC:模数转换器
USART:串口
SPI通信
I2C通信
RTC实时时钟
使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级
灰色的是内核中断。比如第一个:复位中断,当产生复位中断时,程序就会自动执行复位中断函数。
外部中断对应的中断资源
6 | 13 | 可设置 | EXTI0 | EXTI线0中断 | 0x0000_0058 |
7 | 14 | 可设置 | EXTI1 | EXTI线1中断 | 0x0000_005C |
8 | 15 | 可设置 | EXTI2 | EXTI线2中断 | 0x0000_0060 |
9 | 16 | 可设置 | EXTI3 | EXTI线3中断 | 0x0000_0064 |
10 | 17 | 可设置 | EXTI4 | EXTI线4中断 | 0x0000_0068 |
23 | 30 | 可设置 | EXTI9_5 | EXTI线[9:5]中断 | 0x0000_009C |
40 | 47 | 可设置 | EXTI15_10 | EXTI线[15:10]中断 | 0x0000_00E0 |
最右边是地址,因为中断函数的地址由编译器来分配的,它是不固定的,但是中断跳转,由于硬件的限制,只能跳转到固定的地址执行程序,所以为了能让硬件跳转到一个不固定的中断函数中,这里需要在内存中定义一个地址的列表,这个列表地址是固定的,中断发生后,就跳转到这个固定的位置。然后这个固定位置由编译器,再加上一条跳转 到中断函数的代码,这样中断跳转就可以跳转到任意位置了。这个中断地址的列表就叫中断向量表。(相当于中断跳转的跳板,使用C语言编程不需要关心)
1.4 NVIC的基本结构
NVIC:Nest Vector Interrupt Controller,嵌套中断向量控制器,是用来管理中断嵌套的,核心任务在于其优先级的管理。NVIC给每个中断赋予先占优先级(抢占优先级)和次占优先级(响应优先级)。
NVIC是内核外设,是CPU的小助手。
一个外设可能会同时占用多个中断通道,所以有n条线。
NVIC只有一个输出口,NVIC根据每个中断的优先级分配中断的先后顺序,之后通过一个输出口告诉CPU该处理哪个中断(医生-CPU,叫号系统-NVIC)
1.5 NVIC优先级分组
NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为:
高n位的抢占优先级和低4-n位的响应优先级(插队)
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队(插队),抢占优先级和响应优先级均相同的按中断号排队
分组方式 | 抢占优先级 | 响应优先级 |
分组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 |
值越小,优先级越高,0是最高优先级。
分组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;
数值小的优先响应。
1.6 EXTI外部中断
- EXTI(Extern Interrupt)外部中断;
- EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序;
- 支持的触发方式:上升沿(低->高)/下降沿/双边沿/软件触发;
- 支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断(PA0和PB0不能同时用);
- 通道数:16个GPIO_Pin(0-15),外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒;
- 触发响应方式:中断响应/事件响应。
中断响应:申请中断,让CPU执行中断函数;
事件响应:STM32对外部中断新增的额外功能;当外部中断检测到引脚电平变化时,正常的流程是选择触发中断,但是在STM32中也可以选择触发一个事件。如果选择触发事件,那外部的中断信号就不会通向CPU了,而是通向其他外设,用来触发其他外设的操作,比如触发ADC转换,触发DMA等。
总结:中断响应是正常的流程,引脚电平变化触发中断
事件响应不会触发中断,而是触发别的外设操作,属于外设间的联合工作
外部中断有个功能,就是从低功耗模式的停止模式下唤醒STM32;对于PVD电压电压检测,当电源从电压过低恢复时,就需要PVD借助外部中断退出停止模式;对于RTC闹钟而言,有时候为了省电,RTC定一个闹钟之后,STM32会进入停止等待模式,等到闹钟响的时候再唤醒,这也需要借助外部中断。
1.7 EXTI的基本结构
最左边是GPIO口的外设(GPIOA等,每个GPIO有16个引脚),前面介绍了EXTI模块只有16个GPIO的通道,但是这里每个GPIO都有16个引脚,如果每个引脚占用一个通道,那EXTI的16个通道显然就不够用了,所以在这里,会有一个AFIO中断引脚选择的电路模块,AFIO是一个数据选择器,它可以在这前面3个GPIO外设的16个引脚里选择其中一个连接到
后面EXTI的通道里,所以说相同的Pin不能同时触发中断(PA0和PB0不能同时用),然后通过AFIO选择之后的16个通道,就接到了EXTI边沿检测及控制电路上。同时下面PVD、RTC、USB、ETH外设也是并列进来的,加起来就组成了EXTI的20个输入信号。然后进过EXTI电路之后,分为两种输出,上面的接到了NVIC,是用来触发中断的(外部中断的EXTI9-5会触发同一个中断函数,EXTI15-10会触发同一个中断函数,编程的时候,需要再根据标志位区分到底是哪个中断进来的)
疑问:为什么不是5-9,10-15呢
答:数字越小,优先级越高,排在后面?
下面的20条输出线路到了其他外设,这是用来触发其他外设操作的(事件响应)。
1.8 AFIO复用IO口
AFIO主要用于引脚复用功能的选择和重定义(数据选择器)
在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
一系列的数据选择器。
1.9 EXTI框图
右下角是输入线,进入边沿检测电路,在上面的上升沿触发选择寄存器和下降沿触发选择寄存器选择是上升沿触发还是下降沿触发,还是都触发。接着进入或门输出端,引脚触发和软件中断读接到这个或门上。触发信号通过或门之后,兵分两路,上一路是触发中断的,下一路是触发事件的。
触发中断首先会置一个挂起寄存器,相当于中断标志位,读取这个寄存器判断是哪个通道出发的中断;如果中断寄存器置1,它就会继续向左走,和中断屏蔽寄存器共同进入一个与门(中断屏蔽寄存器置1,另一个直接输出,即允许中断。中断屏蔽寄存器置0则相反,即屏蔽中断),然后至NVIC中断控制器。
接下来是事件的输出部分,与事件屏蔽寄存器进入到与门,实现开关控制,最后通过一个脉冲发生器到其他外设。
2 硬件电路
使用外部中断的特性:对于STM32而言,想要获取的信号是外部驱动很快的突发信号,比如旋转编码器的输出信号,可能很久不拧它,此时不需要STM32做任何事;但是一拧它,就会有很多脉冲波形需要STM32接收,信号是突发的,STM32不知道什么时候会来;同时它是外部驱动的,STM32只能被动读取。最后,这个信号非常快,STM32稍微晚一点来读取就会错过很多波形。有脉冲过来,STM32立即进入到中断函数处理。
还有比如:红外遥控接收头的输出
不推荐读取按键,无法处理抖动,可以在主循环中读取及定时器读取。
2.1 旋转编码器简介
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向。
类型:机械触点式/霍尔传感器式/光栅式
当光栅编码盘转动时,传感器的激光就会出现遮挡、透过、遮挡、透过的现象,输出的电平是高低电平交替的方波。方波的个数代表转过的角度,方波的频率表示转速。不过这个模块只有一路输出,正转和反转波形无法区分,所以这种测速方式只能测位置和速度,不能测旋转方向。
拆解
左右是两部分开关触点,内测的两个细的触点都是和中间引脚相连,外侧的左边接A,右边接B。金属盘是设计好的,让两侧触点的通断产生一个90°的相位差,最终配合外部电路,这个编码器的两个输出就会输出这样的波形。正转时(B相滞后90°,反向旋转时,B相超前90°)就可以测方向了:
2.2 硬件电路
先看左边电路,接了一个10K的上拉电阻,默认没有旋转的情况下,这个点被上拉为高电平,通过R3电阻,输出的也是高电平。
当旋转时,内部触点导通,这个点就被拉倒GND了,再通过A端口输出就是低电平了
R3限流电阻,防止模块引脚电流过大,C1是滤波电容,防止输出抖动信号。
右边一样,中间C接到GND。
2.3 手册
NVIC是内核外设,所以在《STM32F10xxx Cortex-M3编程手册》(内核相关的)查找
中断分组配置
中文版《Cortex-M3权威指南》
中断介绍-《STM32F10xxx参考手册(中文)》
AFIO外设
3 EXTI外部中断之对射式红外传感器计次
3.1 接线图
3.2 封装模块
按这个图来配置
AFIO的配置函数。数据选择器输入为GPIOB的14号引脚,输出端固定连接的是EXTI14
// 在GPIO库文件中
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);/*** @brief Selects the GPIO pin used as EXTI Line.* @param GPIO_PortSource: selects the GPIO port to be used as source for EXTI lines.* This parameter can be GPIO_PortSourceGPIOx where x can be (A..G).* @param GPIO_PinSource: specifies the EXTI line to be configured.* This parameter can be GPIO_PinSourcex where x can be (0..15).* @retval None*/
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
{uint32_t tmp = 0x00;/* Check the parameters */assert_param(IS_GPIO_EXTI_PORT_SOURCE(GPIO_PortSource));assert_param(IS_GPIO_PIN_SOURCE(GPIO_PinSource));tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
}
配置EXTI,选择边沿触发方式(上升沿/下降沿/双边沿),选择响应方式(中断/事件)
void EXTI_DeInit(void); // 复位
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct); // 初始化
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct); // 结构体初始化
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line); // 软件触发外部中断// 在主函数中查看和清除标志位
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line); // 获取指定的标志位是否被置1
void EXTI_ClearFlag(uint32_t EXTI_Line); // 对置1的标志位清除// 在中断函数中查看和清除标志位
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
配置NVIC,给中断设置一个优先级
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); // 中断分组
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct); // 初始化NVIC
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset); // 设置中断向量表
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState); // 系统低功耗配置
中断函数是固定的,每个通道对应一个中断函数,参考启动文件。
基本框架写完后,使用调试模式测试
CountSensor模块
#include "stm32f10x.h" // Device header
#include "Delay.h"uint16_t CountSensor_Count;// CountSensor初始化模块
void CountSensor_Init(void)
{// 配置外部中断// 1配置GPIO/AFIO,把时钟外设都打开RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);// EXTI和NVIC的时钟都打开着不手动开启// 2配置GPIO,选择端口为输入模式GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);// 3配置AFIO。数据选择器输入为GPIOB的14号引脚,输出端固定连接的是EXTI14GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);// 4配置EXTI,选择边沿触发方式(上升沿/下降沿/双边沿),选择响应方式(中断/事件)EXTI_InitTypeDef EXTI_InitStructure;EXTI_InitStructure.EXTI_Line = EXTI_Line14; // 指定配置中断线14EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 选择中断模式EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发,遮挡的时候触发
// EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发,离开的时候触发
// EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; // 遮挡和离开的时候触发EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 开启EXTIEXTI_Init(&EXTI_InitStructure);// 配置NVIC,给中断一个优先级NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占,2位响应,整个工程只有一种NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; // 指定中断通道,选的是14NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 指定抢占优先级NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 指定响应优先级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能NVIC_Init(&NVIC_InitStructure);
}// 中断函数,无参无返回值
void EXTI15_10_IRQHandler(void)
{// 先判断中断标志位,确保是设置的中断源触发的这个函数if (EXTI_GetITStatus(EXTI_Line14) == SET){// 执行中断程序
// Delay_ms(1000); // 消除抖动CountSensor_Count++; // 计数// 中断程序结束后,清除中断标志位;不清除就会一直申请中断,卡死在中断函数里EXTI_ClearITPendingBit(EXTI_Line14);}
}// 返回计数
uint16_t CountSensor_GetCount(void)
{return CountSensor_Count;
}
这里上升沿触发,遮挡的时候触发,下降沿触发,离开的时候触发。
3.3 主函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"int main()
{CountSensor_Init(); // 初始化计数配置OLED_Init(); // 初始化OLEDOLED_ShowString(1, 1, "Count:"); // 显示字符串while (1){OLED_ShowNum(1, 7, CountSensor_GetCount(), 5); // 显示计数}
}
4 EXTI外部中断之旋转编码器计次
4.1 接线图
4.2 封装模块
EnCoder模块
#include "stm32f10x.h" // Device headerint16_t EnCoder_Count = 0;// EnCoder初始化模块
void EnCoder_Init(void)
{// 配置外部中断// 1配置GPIO/AFIO,把时钟外设都打开RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);// EXTI和NVIC的时钟都打开着不手动开启// 2配置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);// 3配置AFIO。数据选择器输入为GPIOB的0和1号引脚,输出端固定连接的是EXTI0和EXTI1GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);// 4配置EXTI,选择边沿触发方式(上升沿/下降沿/双边沿),选择响应方式(中断/事件)EXTI_InitTypeDef EXTI_InitStructure;EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; // 指定配置中断线0和1EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 选择中断模式EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发,遮挡的时候触发
// EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发,离开的时候触发
// EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; // 遮挡和离开的时候触发EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 开启EXTIEXTI_Init(&EXTI_InitStructure);// 配置NVIC,给中断一个优先级NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占,2位响应,整个工程只有一种NVIC_InitTypeDef NVIC_InitStructure;// 指定0通道NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; // 指定中断通道,选的是0NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 指定抢占优先级NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 指定响应优先级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能NVIC_Init(&NVIC_InitStructure);// 指定1通道NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; // 指定中断通道,选的是1NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 指定抢占优先级NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; // 指定响应优先级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能NVIC_Init(&NVIC_InitStructure);
}// 中断函数
void EXTI0_IRQHandler(void)
{// 先判断中断标志位,确保是设置的中断源触发的这个函数if (EXTI_GetITStatus(EXTI_Line0) == SET){// 执行中断程序// 判断GPIO_Pin_1输出是0if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){EnCoder_Count--;}// 中断程序结束后,清除中断标志位;不清除就会一直申请中断,卡死在中断函数里EXTI_ClearITPendingBit(EXTI_Line0);}}// 中断函数
void EXTI1_IRQHandler(void)
{// 先判断中断标志位,确保是设置的中断源触发的这个函数if (EXTI_GetITStatus(EXTI_Line1) == SET){// 执行中断程序// 判断GPIO_Pin_0输出是0if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0){EnCoder_Count++;}// 中断程序结束后,清除中断标志位;不清除就会一直申请中断,卡死在中断函数里EXTI_ClearITPendingBit(EXTI_Line1);}
}// get函数
int16_t EnCoder_Get(void)
{int16_t temp = EnCoder_Count;EnCoder_Count = 0;return temp;
}
4.3 主函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "EnCoder.h"int16_t num;int main()
{EnCoder_Init(); // 初始化计数配置OLED_Init(); // 初始化OLEDOLED_ShowString(1, 1, "Data:"); // 显示字符串while (1){num += EnCoder_Get();OLED_ShowSignedNum(1, 6, num, 5); // 显示数据}
}
往右拧,数字变大,往左拧,数字变小。
(1)在中断函数中,最好不要执行耗时过长的代码;
(2)最好不要在中断函数和主函数调用相同的函数或者操作同一个硬件