1 中断系统
(1)中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
(2)中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
(3)中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
2 STM32中断
68个可屏蔽中断通道,包含EXTI外部中断、TIM定时器、ADC模数转换器、USART串口、SPI通信、I2C通信、RTC实时时钟等多个外设
使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级
灰色为内核中断,白色为外设中断
(1)窗口看门狗:检测程序运行状态。如果程序卡死没有及时喂狗,窗口看门狗就会申请中断,在中断程序进行错误检查
(2)PVD电源电压检测:如果供电电压不足,PVD就会申请中断
(…)
3 NVIC嵌套中断向量控制器
NVIC嵌套中断向量控制器用来统一分配中断优先级和管理中断,是内核外设。n表示一个外设同时占用多个中断通道,NVIC只有一个输出口,会根据每个中断的优先级分配中断的先后顺序,最后通过一个输出口告诉CPU要处理哪个中断。
4 NVIC优先级分组
NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队
1 EXTI简介
EXTI(Extern Interrupt)外部中断
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
支持的触发方式:上升沿/下降沿/双边沿/软件触发
支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断
通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
触发响应方式:中断响应/事件响应(外部中断信号不会通向CPU而是通向其他外设,用来触发其他外设)
2 EXTI基本结构
最左边是GPIO口的外设,每个GPIO外设有16个引脚就进来16条线。但EXTI模块只有16个GPIO通道,所以AFIO中断引脚选择模块会充当数据选择器,在不同GPIO外设的16个引脚里选择其中一个连接到EXTI通道。接到EXTI边沿检测及控制电路会与PVD、RTC、USB、ETH四个蹭网外设并列连接,共同组成20个输入信号,然后经过EXTI分为两种输出。一部分接到NVIC,其中外部中断的9~5、15~10分到一个通道里,分别会触发一个中断函数,编程时在这两个中断函数里再根据标志位区分到底是哪个中断进来;另一部分有20条输出线路连接到其他外设,触发其他外设操作(事件响应)。
3 AFIO复用IO口
AFIO主要用于引脚复用功能的选择和重定义
在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
图一:数据选择器(梯形,多个输入一个输出,侧面有选择控制端,从输入选择一个接到输出)选择PA0~PG0其中一个连接到EXTI0上
4 EXTI框图
右下角为EXTI的20根输入线,输入线首先进入边沿检测电路,在上面的上升沿寄存器和下降沿寄存器可以选择是上升沿触发或者是下降沿触发又或者是两个都触发,然后触发信号与软件中断事件寄存器接到或门(月亮形,可以有多个输入但只有一个输出,在输入端只要有一个是高电平1,输出就是高电平1,只有全部输入低电0才输出低电平0)的输入端,通过或门之后兵分两路,上一路触发中断,下一路触发事件。触发中断会置一个挂起寄存器,相当于中断标志位,可以读取寄存器判断哪个通道触发,挂起寄存器置1就继续左走,和中断屏蔽寄存器(相当于开关)共同经过与门(半圆形,可以有多个输入但只有一个输出,在输入端只要有一个是低电平0,输出就是低电平0,只有全部输入高电平1才输出高电平1)(非门,三角形,一个输入一个输出,输入1就输出0,输入0就输出1),然后到NVIC中断控制器。事件输出部分经过事件屏蔽寄存器进行开关控制,最后通过脉冲发生器给一个电平脉冲,触发其他外设动作
1 旋转译码器介绍
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
类型:机械触点式/霍尔传感器式/光栅式
2 硬件电路
图一:旋转轴旋转时,两个触点以相位相差90度的方式交替导通,因为只是开关信号,所以需要配合外围电路才能输出高低电平。左边接了10K的上拉电阻,默认没旋转时A点为高电平。旋转时内部触点导通则被拉到GND,A端口就为低电平。R3为输出限流电阻,防止模块引脚电流过大;C1为输出滤波电容,防止输出信号电容抖动。
外部中断配置步骤
1.配置RCC,把涉及外设时钟打开
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,选择用的GPIO连接到EXTI
与AFIO有关库函数:
void GPIO_AFIODeInit(void);//复位AFIO外设,复位时AFIO外设配置全部清除
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);//配置AFIO事件输出功能
void GPIO_EventOutputCmd(FunctionalState NewState);//配置AFIO事件输出功能
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);//进行引脚重映射(选择重映射方式,新的状态)
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);//配置AFIO的数据选择器,选择想要的中断引脚
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);//和以太网有关
4.配置EXTI,如上升沿、下降沿、双边沿,并选择触发响应方式,选择中断响应和事件响应
与EXTI有关库函数:
void EXTI_DeInit(void);//配置清除,恢复成上电默认的状态
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);//初始化EXTI,根据EXTI_InitStruct结构体里的参数配置EXTI
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);//获取中断标志位是否被置1(中断函数)
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);//清除中断挂起标志位(中断函数)
使用EXTI_Init函数初始化:
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line14;//要配置的中断线
EXTI_InitStruct.EXTI_LineCmd = ENABLE或DISABLE;//指定选择中断线的新状态
EXTI_InitStruct.EXTI_Mode = //指定外部中断线的模式
EXTI_Mode_Interrupt 中断模式,EXTI_Mode_Event 事件模式
EXTI_InitStruct.EXTI_Trigger = //指定触发信号的有效边沿
EXTI_Trigger_Rising 上升沿触发,EXTI_Trigger_Falling 下降沿触发
EXTI_Trigger_Rising_Falling 上升下降沿触发
EXTI_Init(&EXTI_InitStruct);
5.配置NVIC,为中断选择优先级,最后通过NVIC外部中断信号就能进入CPU
与NVIC有关库函数:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);//中断分组(中断分组方式)
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);//根据结构体指定的函数初始化
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);//设置中断向量表
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);//系统低功耗配置
使用NVIC_Init函数初始化:
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = ;//指定中断通道开启或关闭
NVIC_InitStruct.NVIC_IRQChannelCmd = ;//指定中断通道使能或失能
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = ;//指定抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = ;//指定响应优先级
NVIC_Init(&NVIC_InitStruct);
程序源码
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.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
}
}