(声明:这时本人的个人归纳总结,为后期复习学习用)
一、中断系统
1、什么是中断
中断是在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
2、中断优先级
当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
3、中断嵌套
当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
4、中断执行流程
中断的程序如图右边的C语言所示上面是主函数,while(1)死循环里就是主程序,正常情况下,程序就是在主程序里不断循环执行,当中断条件满足时,主程序就会暂停,然后自动跳转到中断程序里运行,中断顺序执行完之后,在返回主程序继续执行(一般中断程序都是在一个子函数里的,这个函数不需要我们调用,当中断来临时,由硬件自动调用这个函数)
二、STM32中断
68个可屏蔽的中断通道:68个中断源,这个是F1系列最多的中断数量对于一个具体的型号没有这么多中断,这个数量看看就行,具体以对应信号的数据手册为准
在表中的深灰色的部分是内核中的中断一般我们用不到,所以了解即可。剩下的部分就是STM32的外设中断,
1、NVIC
(1)基本结构
NVIC:(嵌套中断向量控制器),在STM32中,它是用来统一分配中断优先级和管理中断的,NVIC它是一个内核外设,是CPU的小组手
NVIC有很多输入口,你有多少个中断线路,都可以接过来,在NVIC左边的输入口有很多的不同类型的中断,每一个中断都有可能占用多个中断通道,所以这里有N条线;
NVIC只有一个输出端口,NVIC根据每个中断的优先级分配中断的先后顺序,然后通过右边的一个输出端口告诉CPU该处理哪个中断,对于中断先后分配的任务,CPU不需要知道
(2)优先级分组
响应优先级:类比:医生在病房正给一个病人在看病,病房外面等了很多的病人,当上一个病人看完后,紧急的病人即使是后来的,也会最先进去看病,这种类似于插队的优先级,叫做响应优先级;
抢占优先级:同样类比,如果在外面等候的病人更加紧急,并且此时已经有人在看病了,他可以不等上个人看完,直接冲到医生的屋里,让上一个病人靠边站等着,先给他看病,等他看完了,然后上一个病人在继续,上一个病人结束了,叫号系统在看有没有人来,这种形式的优先就是之前讲到的中断嵌套,这种决定是不是可以中断嵌套的优先级,就叫抢占优先级。
2、EXTI外部中断
(1)基本结构
EXTI:可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
支持的触发方式:上升沿/下降沿/双边沿/软件触发支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断
通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
触发响应方式:中断响应/事件响应
APIO:中断引脚选择的电路模块,就是一个数据选择器,可以在前面3个GPIO外设的16个引脚里选择其中一个连接到后面EXTI的通道里。
(2)AFIO基本结构
AFIO只要用于引脚复用功能的选择和重定义
在STM32中,AFIO主要完成两个任务,复用功能引脚映射,中断引脚选择
(3)EXTI内部结构
三、对射式红外传感器计次
1、接线图
2、原理分析
在上面的介绍中我们知道了中断的工作原理,也就是说在下面的这张图中我们只要把这个外部中断从GPIO到NVIC这一路中出现的外设模块都配置号,把这条信号电路给打通就行了。
3、实现步骤
(1)配置RCC
把我们这里涉及的外设的时钟都打开,这个别忘了不打开时钟外设是没法工作的。
(2)配置GPIO
选择我们的端口为输入模式。
(3)配置AFIO
选择我们用的这一路GPIO ,连接到后面的EXTI。
(4)配置EXTI
选择边沿触发方式,比如上升沿、下降沿或者双边沿,还有选择触发响应方式,可以选择中断响应和事件响应。当然我们一般都是中断响应。
(5)配置NVIC
给这个中断选择一个合适的优先级。通过NVIC,外部中断信号就能进入CPU。了。这样CPU才能收到中断信号,才能跳转到中断函数里执行中断程序。
这五步就是外部中断的配置流程,这里涉及的外设比较多。
CountSensor.c
(1)开启时钟
我们在使用外设的时候首先第一步就是开始时钟,不打开它外设是无法使用,这里我们开启了GPIO和AFIO时钟,因为NVIC是内核的外设不需要我们操作开启,EXTI是时钟都是开启的,
(2)GPIO的配置
//配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//如果不知道应该是什么输入方式可在参考手册中查找,EXTI输入模式下的GPIO的设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
在GPIO输入模式中如果不知道要选定什么模式我们可以参考手册中查找
(3)AFIOP配置
配置完成,执行完这个函数后,AFIO的第14个数据选择器就拨好了;其中输入端被拨到了GPIOB外设上,对应的就是PB14号引脚,输出端固定连接的是EXTI的第十四个中断线路
这样PB14号引脚的电平信号就可以顺利通过AFIO,进入到后级EXTI电路了;
AFIO的其他函数:
配置AFIO,它没有分配专门的库函数文件而是和GPIO(gpio.h)在一个文件里,相关函数可以在那个文件中查看
void GPIO_AFIODeInit(void)
用来复位AFIO外设的,调用一下这个函数,FPIO外设的配置就会全部清除;
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
用来锁定GPIO配置的,调用这个函数,参数指定某个引脚,这个引脚就会被锁定,防止意外更改;也是GPIO的外设,用的时候不多。了解即可
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
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的数据选择器,来选择我们想要的中断引脚
第一个参数GPIO_PortSource,选择某个GPIO外设作为外部中断源,参数可以是GPIO_PortSourceGPIOX,其中X可以是A到G;
第二个参数GPIO_PinSource,指定要配置的外部中断线,参数可以是GPIO_PinSourceX,其中X可以是0到15;
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);
与以太网有关的,我们这个芯片没有以太网外设,所有用不到;
(4)EXTI配置
将EXTI的第14个线路配置为中断模式,下降沿触发,然后开启中断
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_Falling;//选择上升沿还是下降沿触发,触发的模式
EXTI_Init(&EXTI_InitStructure);
函数介绍:
//void EXTI_DeInit(void)
调用它就可以把 EXTI的配置都清除,恢复成上电默认的状态
//void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct)
调用这个函数,就可以根据这个结构体里的参数配置EXTI外设,初始化EXTI只要用到的就是这个函数,使用方法和GPIO_Init也是一样的
//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);清除中断挂起标志位
(5)NVIC配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//这个分组的具体选择哪一个,根据我们的需求来选,抢占和优先,这里先选择2位抢占和2位优先
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);
函数介绍
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);系统低功耗配置
(6)中断函数
//中断函数可以参考一个启动文件_md的文件,可以看到里面定义的中断向量表以IRQHandler结尾的就是中断函数的名字
void EXTI15_10_IRQHandler(void)//中断函数:中断函数都是无参无返回值
{
//在中断函数里一般要进行一个中断标志位的判断,确保是我们想要的中断源发的这个函数,因为这个函数EXTI10---EXTI15都能进来,所以要判断一下是不是我们想要的EXTI14进来的
if (EXTI_GetITStatus(EXTI_Line14) == SET)
{
Delay_ms(500);
CountSensor_Count ++;
EXTI_ClearITPendingBit(EXTI_Line14);//因为中有中断标志位置1了,程序就会跳转到中断函数,如果不清楚中断标志位,会一直申请中断,程序就会不断响应中断,执行中断函数,程序就会卡死在中断函数中
}
}
在这里我说明一下延时函数因为传感器灵敏,和按键一样进行消抖,手抖的现象,以免出现遮挡一次会计数很多的现象。
(7)CountSensor.h
#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_H
void CountSensor_Init(void);
uint16_t CountSensor_Get(void);
#endif
(8)主函数
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "CountSensor.h"
int main(void)
{
OLED_Init();
CountSensor_Init();
OLED_ShowString(1,3, "Count:");
while(1)
{
OLED_ShowNum(1,7,CountSensor_Get(),5);
}
}
四、旋转编码器计次
这个旋转编码器原理大致是一样的,只是有些参数稍作修改
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号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
Encoder.c
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
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
}
}