声明:本人跟随b站江科大学习,本文章是观看完视频后的一些个人总结和经验分享。(仅仅是个人的理解和看法,可能有误,也希望大家能帮忙纠正,谢谢大家❤️)
文章目录
理论部分
1.中断系统的介绍
中断一般用于,STM32想要获取外部获取并且突发很快的信号
eg.红外遥控,红外接收头接收到信息只需几毫秒可能一个接收的信息波就过去了,但是此时程序还在处理其他东西,接收到时候可能信息波就会不完整,或者根本就没接收到信息波,就会产生误差
2.STM32中断
a.能够产生中断的外设(STM32的中断资源)
灰色的是内核中断
b.NVIC,抢占优先级,响应优先级
由于CPU主要用来进行运算作用,相当于CPU是医生,我们的NVIC则相当于一个叫号系统,各种需要运行的程序则是病人,NVIC的作用就是安排这些病人看病的顺序
NVIC:由于中断外设非常多,如果全部接在CPU上则会非常麻烦,所以全部接在NVIC上,NVIC有非常多的接口但是输出只有一个,最后NVIC分配好中断优先级后,告诉CPU需要处理哪一个就行
响应优先级:可以使中断优先级更加靠前,即看医生可以插队到别人前面,但是不能打断正在看医生的病人
抢占优先级:可以实现中断嵌套,即看医生可以打断正在看医生的病人,等自己看完医生,别人接着看
PS:如果响应优先级和抢占优先级均相同,则中断号小的先看医生(先处理)
分组0没有中断嵌套,全是中断优先级;分组4全是中断嵌套,没有中断优先级;不同分组方式实现了不同个数的中断嵌套和优先级的分配
3.EXTI中断
a.EXTI简介
- 双边沿:上升沿和下降沿都可以触发
- 软件触发:使用程序写代码直接触发,无需上升沿或者下降沿
- 中断响应:引脚电平触发中断,进入CPU处理(进入了中断函数)
- 事件响应:引脚电平或软件触发中断,不进入CPU,而是触发其他外设操作,属于外设之间的联合工作
PS:相同的pin不能同时触发中断。GPIO有很多个外设,像GPIOA,GPIOB等等,每个GPIO都有16个引脚,也就是PA1,
PA2,PB1,PB2这样的,就有PAx和PBx等等,x就是pin的意思,每个GPIO16个pin引脚经过AFIO的选择之后只有一个
引脚能够被输出到EXTI,外加PVD,RTC,USB,ETH,所以EXTI一共有20个通道。
PS:由于经过AFIO选择后只有一个pin能够进入EXTI,所以像PA0和PB0这样pin相同的引脚就不能同时触发中断
b.EXTI的基本结构
由于EXTI输出连接到NVIC接口太多,比较浪费NVIC通道资源,所以将5-9和10-15合并了,即触发后会进入同一个中断函数,如果要判断是哪个通道,还需要在中断函数内设置标志位
PS:下面还有20条线连接其他外设,也就是事件响应
c.AFIO复用IO口
右边这个图是AFIO第二个作用:中断引脚选择
第一个作用:引脚复用功能的选择和重定义,即将默认复用功能,改成重定义功能
d.EXTI内部框图
请求挂起寄存器的作用:相当于中断标志位,即读取该寄存器可以知道是哪个通道引发了中断,有中断则输出1,然后和屏蔽寄存器输出一起进入与门
屏蔽寄存器的作用:即通过输入高低电平以及和"与门"来实现中断的关闭和开启,类似51单片机的ET0,ET1的作用,屏蔽寄存器若给0,那无论请求挂起寄存器输出什么,最终经过与门处理,输出给NVIC中断控制器的都是0,那么就不会进入中断
4.旋转编码器
a.简介
第一幅图就是最早的光栅式旋转编码器,通过上面的孔旋转时遮住红外线,以此来输出高低电平信号的波形如下图:
这里方波的频率表示旋转速度,方波个数可以表示旋转角度。我们一般使用中断捕获上升和下降沿来判断速度和角度。
由于输出比较单一,所以不能够测量其旋向,这种方式叫单相输出
机械触点式对应第二个图:
这里用触点代替了光栅(左触点连接A,右触点连接B),而且这个轮盘是特殊设计过的,配合外部电路,旋转时A和B产生两种波,两个波的相位差为90°,像上面那个方波图中的蓝虚线就是表示90°相位差的意思。这种相位差为90°的方波也叫正交波形,带正交波形信号输出的编码器可以测旋向,这种方式叫两相正交输出,正交输出可以抗噪声,因为有两相,假如一相不变,另一相发生变化,说明此时产生了噪声,
这就是正转和反转时A,B两相波形,如果把一相(A)的下降沿作为进入中断的信号,那么在中断里面读取另一相(B)的电平,高电平就为正转,低电平就为反转。但是由于反转比正转延迟90°的相位差,所以正转可能只要你一扭编码器,就会进入中断,但是反转需要扭一下,扭到正确触发中断位置才能进入中断,反转就会有点延迟的效果。
所以我们设置成A相和B相下降沿都能触发中断,但是只有A为低电平B为下降沿时才会算正转,A为下降沿B为低电平为反转,这样无论正转还是反转都需要扭到位才能够进入自己对应的中断
这个就是最后的总结,看右边的两个表格明显可以发现,当一相处于上升沿时,反转和反转的另一相状态是相反的,根据这个我们就可以写出编码器的代码,也就是检测一相的边沿,测另一相的电平,状态在上面那个表就是正转,反之是反转
b.硬件电路
1是滤波电容,2是上拉电阻,3是按键电路
原理:旋钮触点没有连接时,A直接连到上拉电阻部分,A处电平就为高电平,当触点连接时,就连通到C处的GND,此时A处为低电平,这样触点的不断连接和断开就让A处输出不同的高低电平;B处也同理,但是会输出90°相位差的方波
代码部分
1.接线图
2.思路
第一步:打开控制GPIO外设的时钟RCC
第二步:配置GPIO,选择端口为输入模式
第三步:配置AFIO,选择要使用的GPIO连接到EXTI
第四步:配置EXTI,选择中断触发方式和响应方式
第五步:配置NVIC,分配合适的中断优先级
3.代码一
注意事项:
- EXTI无需开启时钟,
- 在STM32中,EXTI(外部中断/事件控制器)通常不需要配置RCC(复位和时钟控制)的原因如下:
EXTI的主要功能是检测外部引脚的电平变化或者边沿触发事件来产生中断或事件信号。它的工作是基于GPIO引脚的输入信号变化检测,而不是像一些其他外设(如定时器、SPI等)需要特定的时钟信号来驱动其内部电路的时序逻辑。GPIO引脚本身的时钟是通过RCC进行配置使能的,一旦GPIO时钟使能,其基本的输入功能就可以工作。EXTI利用已经使能时钟的GPIO引脚作为输入源,检测这些引脚的状态变化来触发中断,所以一般情况下不需要单独为EXTI配置RCC时钟。 - NVIC作为内核外设,无需开启时钟
uint16_t CountSensor_Count; //全局变量,用于计数
/**
* 函 数:计数传感器初始化
* 参 数:无
* 返 回 值:无
*/
void CountSensor_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_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB14引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
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 = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线
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外设
}
/**
* 函 数:获取计数传感器的计数值
* 参 数:无
* 返 回 值:计数值,范围:0~65535
*/
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
/**
* 函 数:EXTI15_10外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) //判断是否是外部中断14号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count ++; //计数值自增一次
}
EXTI_ClearITPendingBit(EXTI_Line14); //清除外部中断14号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
4.代码二
#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号线触发的中断,这里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号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
告诫
- 不要在中断函数里面写delay或者延时性长的函数,否则主函数会受到严重阻塞
- 主函数和中断函数不要操作同一个硬件或者调用函数,尤其是和硬件相关的函数,比如中断函数和主函数里面同时调用OLED显示函数,就会出错
- 可以在中断里面操作变量和标志位,在中断返回后,在主函数里面进行显示和操作,保证不产生硬件操作冲突