对射式红外传感器

对射式红外传感器的DO数字输出端,随意选择一个GPIO口接上就行,图中接到了PB14的端口。
程序要求与设计
程序要求:
当我们的挡光片或者编码盘在这个对射式红外传感器中间经过时,这个DO就会输出电平变化信号,然后这个电平跳变信号就会触发STM32 PB14号口的中断,我们在中断函数里,执行变量++的程序 。然后主函数循环里调用OLED显示这个变量。
根据程序要求,我们需要用到中断的相关知识:

第一步,配置RCC,把我们这里涉及的外设的时钟都打开。
第二步,配置GPIO,选择我们的端口为输入模式。
第三步,配置AFIO,选择我们用的这一路GPIO,连接到后面的EXTI。
第四步,配置EXTI,选择边沿触发方式,比如上升沿、下降沿或者双边沿。还有选择触发响应方式,可以选择中断响应和事件响应。
第五步,配置NVIC,给我们这个中断选择一个合适的优先级。
最后,通过NVIC,外部中断信号就能进入CPU了,这样CPU才能收到中断信号,才能跳转到中断函数里执行中断程序。这五步就是外部中断配置的流程。
这里涉及外设比较多,有RCC、GPIO、AFIO、EXTI、NVIC。
第一步,配置RCC,把我们这里涉及的外设的时钟都打开:
EXTI、NVIC这两个外设的时钟是一直都打开的,不需要我们再开启时钟了,EXTI作为一个独立的外设,按理说应该是需要开启时钟的,但是寄存器里面却没有EXTI时钟的控制位,这个原因手册也没有找到,网上也没有确切的答案,推测可能是和EXTI唤醒有关,或者是其它的一些电路设计上的考虑,那我们暂时就不用管了,知道它不用开启时钟就行了。另外NVIC也不需要开启时钟,是因为NVIC是内核的外设,内核外设都是不需要开启时钟的,人家和CPU一起,都是住在“皇宫”里的,而RCC管的都是内核外的外设,所以RCC管不着NVIC。
第二步,配置GPIO,选择我们的端口为输入模式:
对于外部中断来说,要选择浮空输入、上拉输入、或者下拉输入,这其中的一个模式,当然像这种其他外设使用GPIO的情况,如果你不清楚该配置成什么模式,可以看STM32F10XXX参考手册,在GPIO这一章,找一下外设的GPIO配置表,里面有写每个外设的各个引脚都需要配置成什么模式,在表中可以看到EXTI输入线给的推荐配置就是浮空、上拉或者下拉。




第三步,配置AFIO,这个AFIO外设,ST公司并没有给它分配专门的库函数文件,它的库函数文件是和GPIO在一个文件里的。
GPIO_AFIODeInit();这个函数是用来复位AFIO外设的,调用一下这个函数,AFIO外设的配置就会全部清除。
GPIO_PinRemapConfig();这个函数可以用来进行引脚的重映射,第一个参数可以选择你要重映射的方式,第二个参数是新的状态。
GPIO_EXTILineConfig();调用这个函数,就可以配置AFIO的数据选择器,来选择我们想要的中断引脚。
这个函数虽然是GPIO开头,但实际上里面操作的是AFIO的寄存器,所以这个函数实际上是AFIO的函数。
GPIO_EXTILineConfig(PortSourceGPIOx,GPIO_PinSourcex);
第一个参数是GPIO_PortSource,选择某个GPIO外设作为外部中断源,这个参数可以是GPIO——PortSourceGPIOx(x可以是A到G)。我们用的是PB14号引脚,所以把x改成B就行了。
第二个参数是GPIO_PinSource,指定要配置的外部中断线,这个参数可以是GPIO_PinSourcex(x可以是0~15)。
GPIO_EXTILineConfig(PortSourceGPIOB,GPIO_PinSource14);代表连接PB14号口的第14个中断线路。
到这里,AFIO外部中断的引脚选择配置就完成了,就这一个函数就行了,当执行完这个函数后,AFIO的第14个数据选择器就拨好了,其中输入端被拨到了GPIOB外设上,对应的就是PB14号引脚,输出端固定连接的是EXTI的第14个中断线路,这样,PB14号引脚的电平信号就可以顺利通过AFIO,进入到后级EXTI电路了。
第四步,配置EXTI。
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);这个函数是用来软件触发外部中断的,调用这个函数,参数给一个指定的中断线,就能软件触发一次这个外部中断。
因为在外设运行的过程中,会产生一些状态标志位,比如外部中断来了,会有一个挂起寄存器置了一个标志位,对于其他外设,比如串口收到数据,会置标志位,定时器时间到了,也会置标志位,这些标志位都是放在状态寄存器的,当程序想要看这些标志为时,就可以看这些函数。(在STM32微控制器中,状态寄存器(Status Register)并不是指单一的寄存器,而是一类寄存器的统称。每个外设(如串口、定时器、外部中断等)都有自己的状态寄存器,用于指示该外设的各种状态和事件)
以下4个函数是EXTI关于状态寄存器的函数:
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_InitTypeDef EXTI_InitStructure;
//创建出一个结构体。
EXTI_InitStructure.EXTI_Line=EXTI_Line14;
//指定要使能或禁用的EXTI线路。此参数可以是 @ref EXTI_Lines 的任意组合。
EXTI_InitStructure.EXTI_LineCmd=ENABLE;
//指定所选EXTI线路的新状态。此参数可以设置为 ENABLE 或 DISABLE 。
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;
// 指定EXTI线路的模式。此参数可以是 @ref EXTITrigger_TypeDef 的一个值 。
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising;
//指定EXTI线路的触发信号有效边沿。此参数可以是 @ref EXTITrigger_TypeDef 的一个值 。
EXTI_Init(&EXTI_InitStructure);
//根据 EXTI_InitStruct 中指定的参数初始化 EXTI 外设。
通过以上代码,PB14的电平信号就能通过EXTI通向下一级的NVIC了。
第五步,配置NVIC。
因为NVIC是内核外设,所以它的库函数是被ST发配到了杂项这里来,在misc.h文件的最后有关于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);
//系统低功耗配置
在配置中断之前,先指定一下中断的分组,然后使用NVIC _Init初始化一下NVIC就行了。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_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);
中断函数
在STM32中,中断函数的名字都是固定的,每个中断通道都对应一个中断函数,中断函数的名字我们可以参考一下启动文件,打开启动文件后(startup_stm32f10x_md.s),找到定义的中断向量表,以IRQHandler结尾的字符串就是中断函数名称。
例如,我们可以找到EXTI15_10_IRQHandler这一项,这就是EXTI15到EXTI10的中断函数(EXTI10~15公用一个中断函数)。我们硬件上使用的是PB14所以连接的是EXTI14。
在中断函数中,一般都是先进行一个中断标志位的判断,确保是我们想要的中断源触发的这个函数。因为这个函数EXTI10到EXTI15都能进来,所以要判断一下是不是我们想要的EXTI14进来的。
当执行中断程序后,要在中断函数中调用一下清除中断标志位的函数,因为只要有中断标志位置1了,程序就会跳转到中断函数,如果你不清除中断标志位,那它就会一直申请中断,这样程序就会不断响应中断,执行中断函数,那程序就卡死在中断函数中了,所以我们每次中断结束后,都应该清除一下中断标志位。
void EXTI15_10_IRQHandler(void){
if(EXTI_GetITStatus(EXTI_Line14) ==SET){
/* 写中断程序 */
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
这样中断函数的逻辑就没问题了,我们想要中断函数实现各种功能,修改中断函数对应的位置就可以了。
我们想要对射式红外传感器实现计数,就需要一个变量,变量在中断函数中自增,因为中断函数并无法返回这个变量,所以需要额外写一个返回变量的函数。
uint16_t CountSensor_Count;
uint16_t CountSensor_Get()
{
return CountSensor_Count;
}
void EXTI15_10_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line14) ==SET){
/* 写中断程序 */
CountSensor_Count++;
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
这样主函数通过调用CountSensor_Get(),就可以获取变量的数值。
旋转编码器计次
程序要求与设计
要求旋转编码器上的按钮,OLED显示器会出现增加减少的数字。

对于编码器而言,正向旋转时,A、B相输出的是这样的波形(上A下B):

反向旋转时,输出的是这样的波形(上A下B):

如果把如果把一相的下降沿做触发中断,在中断的时刻读取另一相得电平,那么正转的另一相就是高电平,反转的另一相就是低电平,这样就能区分旋转的方向了。
只不过这样在操作上有一些小瑕疵,比如你正转的时候,由于A相先出现下降沿,所以你刚开始动,就进中断了,而反转时是A相后出现下降沿,就是你转到位了才会进入中断。这样实际上也没问题,但在实际操作中正转反转效果不同会有一些令人不适。
所以我准备的就是,A、B相都触发中断,只有在B相下降沿和A相低电平时,才判断为正转;在A相下降沿和B相低电平时,才判断为反转,这样就能保证正转反转都转到位了,才执行数字加减操作。这样就会有两个中断初始化代码。
易错点
在这个程序现象中,我们使用了两个中断函数,在配置AFIO时,可能会直接这样写:
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1 | GPIO_PinSource0);
这样是错误的,GPIO_EXTILineConfig 需要单独配置每个引脚,正确的应该是这样写:
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
AFIO进行单独配置。
//Ecoder.c中代码
#include "stm32f10x.h" // Device header
int16_t Encoder_Count = 0;
void Encoder_Init()
{
//开启外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//配置GPIO口
GPIO_InitTypeDef GPIO_InitStrcture;
GPIO_InitStrcture.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStrcture.GPIO_Pin=GPIO_Pin_0 | GPIO_Pin_1;
GPIO_Init(GPIOB,&GPIO_InitStrcture);
//配置AFIO
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
//配置EXTI
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_Falling;
EXTI_Init(&EXTI_InitStructure);
//配置NVIC
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);
//通过以上代码A相和B相都是下降沿触发各自的中断函数。
}
void EXTI0_IRQHandler(void){
if(EXTI_GetITStatus(EXTI_Line0) == SET){
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0){
Encoder_Count--;
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void EXTI1_IRQHandler(void){
if(EXTI_GetITStatus(EXTI_Line1) == SET){
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0) == 0){
Encoder_Count++;
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
int16_t Encoder_Get(){
int16_t count = Encoder_Count;
Encoder_Count = 0;
return count;
}
//主函数中代码
#include "stm32f10x.h"
#include "OLED.h"
#include "Encoder.h"
int main(void){
OLED_Init();
Encoder_Init();
int16_t num = 0;
OLED_ShowString(1,6,"AOOFOO");//字符串
OLED_ShowString(2,1,"COUNT:");//字符串
while(1){
num = num + Encoder_Get();
OLED_ShowSignedNum(2,8,num,5); //十进制有符号数
}
}
中断使用建议
最后提几个中断编程的建议:
一是在中断函数中,最好不要执行耗时过长的代码,中断函数要简短快速,别刚进中断就执行一个Delay多少毫秒这样的代码,因为中断时处理突发事情,如果你为了一个突发的事情待着中断里不出来了,那主程序就会收到严重的阻塞。
二是最好不要在中断函数和主函数调用相同的函数或操作同一个硬件,尤其是硬件相关的函数,比如OLED显示函数。如果你既在主程序里调用OLED,又在中断里调用OLED,OLED就会显示错误。为什么呢?因为你想,在主程序中,OLED刚显示一半,啪,进中断了,结果中断里还是OLED显示函数,那OLED就挪到其他地方显示了,这时还没问题,但当中断结束之后,需要继续原来的显示,这时就出问题了,因为硬件的显示位置被挪到其他地方了,所以再回来的时候,继续显示的内容就会跟着跑其他地方去。这就会造成问题,虽然在中断进入和退出的时候,会有保护现场和恢复现场,但这只能保证CPU程序能正常返回不出问题,对于外部硬件的话,并没有在进入中断时,进行现场保护,所以中断返回后,就出问题了,那为了避免这样的问题,就最好不要在主程序和中断程序里,操作可能冲突的硬件,在实现功能的时候,可以向我们这样,在中断里操作变量或者标志位,当中断返回时,我们再对这个变量进行显示和操作。这样既能保证中断函数的简短快速,又能保证不产生冲突的硬件操作,这就是中断程序设计的注意事项。
那在其他地方,大家也都可以多用用变量或者标志位,来减少代码之间的耦合性,让各部分代码互相独立,仅使用变量、标志位或者函数作为接口,这样能让更加清晰,代码更加强健。