(一)红外接收计数
(1)对射式红外接收模块
对射式红外接收模块,通过红外传感来输出信号,如果有遮挡物在发射和接收的间隙之间,其数字输出口(DO口)就会输出高电平,无则输出低电平,我们将DO口连接到PB14口,配好GND和VCC,可以实现一个检测遮挡物通过该间隙的次数;
为了检测遮挡物通过次数,我们需要知道遮挡物何时开始遮挡红外对射模块,何时离开,当遮挡物开始遮挡时,红外接收模块的数字输出口输出由低电平转为高电平,离开时会由高电平恢复到低电平,那我们就可以通过电平跳变来实现判断,在这里我使用的时下降沿来判断,也就是遮挡物离开的时候会记一次,我们可以有两种方法来实现判断何时有电平的跳变,一种方法是用主程序的while循环来不断扫描这个IO口是否有电平跳变,如果这样单片机就干不了其他事情了,第二种方法是用单片机的外部中断来实现电平的检测,在电平发生变化时产生中断,单片机停止主程序来处理中断,中断处理完后再返回主程序,这样可以提高CPU的效率,让单片机可以处理更多事情;
(2)外部中断实现计次
我们的单片机外部有多个引脚,如果外部中断的申请都由这些引脚直接发送给CPU的话会非常浪费CPU资源,因此在CPU之前有一个嵌套向量中断控制器(Nested Vectored Interrupt Controller,简写为NVIC)帮助CPU来处理发送过来的信号,并把这些信号按照优先级发送给单片机,这样所有的外部触发就可以通过这个NVIC一根线引入CPU中;
但是如此多的外部中断连接到NVIC中需要其很多的引脚,因此我们在前面再添加一个控制端口,这就是AFIO,我们可以暂且把它看成一个数据选择器,其可以帮助我们选择pin口输出
从这里也可以看到,如果我们选择了PA1口,那我们就不能再选择PB1,PC1口作为外部中断口了,我们在AFIO和NVIC之间还需要一个外部中断/事件控制器(External interrupt/event controller,简写EXTI),用于配置每个中断的属性,比如之前提到的上升沿中断还是下降沿中断,就要在这里对每根线进行配置,那我们使用中断的逻辑就应该是这样的
我们的单片机的外部时钟默认是关闭的,现在我们要使用外部中断,就要打开相应的时钟,并对GPIO、AFIO、EXTI和NVIC进行初始化和配置;
void infrared_interrupt_and_afio_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING;
gpio_init.GPIO_Pin = GPIO_Pin_14;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOB, &gpio_init);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14); //afio init
}
这里已经再熟悉不过了,我们先对GPIOB的时钟打开,要注意的是AFIO也需要打开时钟,且也是在RCC_APB2PeriphClockCmd中打开,这我们可以跳转到函数定义来查看,初始化完成后,我们要配置AFIO用来选择我们触发中断口,AFIO的函数定义在gpio文件夹内,我们打开文件夹,拉到最下端找到GPIO_EXTILineConfig函数,跳转到定义
可以看到这里有两个参数用于配置中断端口,由于我们选择的是PB14端口,因此我们第一个参数给GPIO_PortSourceGPIOB,第二个参数给GPIO_PinSource14,这样我们就配置好了GPIO和AFIO,接下来要配置的就是EXTI;
我们可以先打卡exti.h文件夹,查看其函数声明
(1)第一个函数EXTI_DeInit()是用于把exti恢复为默认值的,这里不会用到;
(2)第二个函数EXTI_Init(...)是初始化exti的,这里只要传递一个结构体就可以初始化exti,这种传递结构体的方法和之前的GPIO、AFIO的初始化方法都是一样的;
(3)第三个函数EXTI_StructInit(...)是给初始化结构体默认值的,这里也没有用到;
(4)第四个函数EXTI_GenerateSWInterrupt(...)触发软件中断的,如果我们在软件中想要触发这个中断,就可以在代码中加入这句话,这里没有用到;
(5)后面的四个执行的都是一样的功能,它们分别为读取是否是某一条线路的中断和清除中断标志位,由于中断线路有复用的情况,因此我们在中断中要读取具体是哪条线路产生的中断,由于中断置1后需要软件手动清除,因此我们在进入某个中断中一定要调用中断标志位清除函数,这里四个的区别只是前两个是在普通函数中调用的,而后两个则在中断函数中调用;
了解完成后,我们目前需要的就是初始化EXTI,我们直接点击结构体变量,跳转到结构体变量的定义中
这里第一个选的是线路,我们可以查看线路选择的宏定义,简单来说就是我们选择x口作为中断口,那么就选择EXTI_Linex就行了;
第二个是选择中断的方式,这里提供了两种方式,一种是中断(EXTI_Mode_Interrupt),一种是事件(EXTI_Mode_Event),中断有CPU的参与,CPU暂停其他程序来处理中断程序,中断程序处理完成后再恢复到之前的程序中继续执行,而事件不需要CPU的参与,直接由硬件自动完成预设的操作,这里我们需要接受通过次数并执行变量增加,我们选择触发中断
第三个参数是选择触发的类型,这里的类型有上升沿触发,下降沿触发和上升下降沿都触发,我们选择下降沿触发
最后一个选择使能或不使能,我们初始化自然选择使能,因此选择ENABLE
void infrared_exti_init()
{
EXTI_InitTypeDef exti_init;
exti_init.EXTI_Line = EXTI_Line14;
exti_init.EXTI_LineCmd = ENABLE;
exti_init.EXTI_Mode = EXTI_Mode_Interrupt;
exti_init.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&exti_init);
}
配置好exti后,只有最后一个nvic需要初始化配置了,值得注意的是nvic并没有特定的文件,其函数声明放在了misc.h文件中,我们打开misc.h文件,拉到最下端
这里我们主要用的就是第一个函数和第二个函数,用来选择优先级分组和初始化nvic,第一个函数是用于配置优先级分组的,这里具体的分组可以待会跳转到定义中查看,第二个函数就是初始化nvic的
先跳转到分组的函数定义中
这个函数主要是选择优先级分组,选择不同的分组将四位不同地分配到抢占优先级(pre-emption priority)和子优先级(subpriority),抢占优先级高的中断可以在抢占优先级的中断执行过程中打断该中断,先执行抢占优先级高的中断,如果同时有多个中断在等待系统的响应,那么子优先级高的中断将会先得到响应,但是我们这里只有一个中断,不会产生同时触发中断或中断排队的情况,这个分组可以随便选,这里就选分组2了;
设置好分组后,就可以对nvic进行初始化了,这里和前面一样,初始化只要一个结构体变量的地址,因此我们直接跳转到该结构体,查看结构体里面的变量该如何配置
第一个我们要选择一条线路,但是这个线路定义并不在本文件夹里,我们要打开stm32f10x.h这个文件夹,找到IRQn枚举类型
这里包含了所有以stm32f10x开头的单片机的中断口,在最前面的是所有系列stm32f10x单片机都有的,而后面的则是一些单片机独有的,我使用的是中等闪存stm32F103c8,因此应该看的是结尾为MD的定义,打开
这里10-15口、5-9口是共用一条线路,我们用的是14口,因此我们的线路选择为EXTI15_10_IRQn
配置好第一个参数后,后面的参数都比较简单了,第二个第三个参数分别是配置抢占优先级和子优先级的,之前也说了,这里只有一个中断,因此随意配置,但是我们还是看一些优先级分组会对抢占优先级和子优先级什么影响
从这里可以看到优先级分组对抢占优先级和子优先级的字节分配带来的影响,如果选择的是分组1,将会有1个字节给抢占优先级,那抢占优先级只有0和1可以选择,3个字节给子优先级,那么子优先级就有2^3=8个子优先级选择,这里我们选择的是分组2,抢占优先级和子优先级都只有4个可选,我们都选1就配置好了第二和第三个参数;
第四个参数是是否使能,我们选择ENABLE使能
void infrared_nvic_init()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef nvic_init;
nvic_init.NVIC_IRQChannel = EXTI15_10_IRQn;
nvic_init.NVIC_IRQChannelPreemptionPriority = 1;
nvic_init.NVIC_IRQChannelSubPriority = 1;
nvic_init.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_init);
}
最后就到了我们写中断函数的时候了,这里中断函数都是有固定的命名的,这些名字在startup_stm32f10x_md.s文件中,找到以Handler结尾的函数名,找到10-15的中断函数名就可以了
接着我们写中断函数
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14))
{
infrared_count++;
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
中断函数无非就两件事,第一是进入中断后次数加1,第二是把中断标志位清零,如果没有清零操作,程序会一直响应中断,而主程序也无法把数字显示了;
最后我们还需要一个函数把记到的次数返回,我们整个外部中断模块如下:
#include "stm32f10x.h" // Device header
unsigned int infrared_count = 0;
void infrared_interrupt_and_afio_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING;
gpio_init.GPIO_Pin = GPIO_Pin_14;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOB, &gpio_init);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14); //afio init
}
void infrared_exti_init()
{
EXTI_InitTypeDef exti_init;
exti_init.EXTI_Line = EXTI_Line14;
exti_init.EXTI_LineCmd = ENABLE;
exti_init.EXTI_Mode = EXTI_Mode_Interrupt;
exti_init.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&exti_init);
}
void infrared_nvic_init()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef nvic_init;
nvic_init.NVIC_IRQChannel = EXTI15_10_IRQn;
nvic_init.NVIC_IRQChannelPreemptionPriority = 1;
nvic_init.NVIC_IRQChannelSubPriority = 1;
nvic_init.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_init);
}
unsigned int infrared_get_count()
{
return infrared_count;
}
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14))
{
infrared_count++;
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
头文件声明一下
#ifndef __INTERRUPT_H__
#define __INTERRUPT_H__
void infrared_interrupt_and_afio_init(void);
void infrared_exti_init(void);
void infrared_nvic_init(void);
unsigned int infrared_get_count(void);
#endif
主函数只要对所有用到的东西初始化,在调用取值函数提取计数值,最后在oled屏幕上打印次数就可以了,主函数如下:
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "infrared_interrput.h"
int main()
{
unsigned int num = 0;
OLED_Init();
infrared_interrupt_and_afio_init();
infrared_exti_init();
infrared_nvic_init();
OLED_ShowString(1, 1, "count:");
while(1)
{
num = infrared_get_count();
OLED_ShowSignedNum(1, 7, num, 5);
}
return 0;
}
(二)外部中断实现旋转编码器
(1)旋转编码器
旋转编码器有两个输出方波,可以通过输出相位的不同来判断旋转编码器的旋转方向,这里设计一个顺时针数值增加,逆时针数值减小的功能
可以看到,当旋转编码器顺时针转时,A口输出的方波相位超前B口90度,在A为下降沿时B永远是低电平,当旋转编码器逆时针转是,B口输出的方波相位要超前A口90度,在B为下降沿时A为低电平,我们通过下降沿产生的中断结合AB口的高低电平来判断旋转编码器的正反转;
旋转编码器模块代码和之前大同小异,如下:
#include "stm32f10x.h" // Device header
int rotary_count = 0;
void rotary_encounter_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Pin = GPIO_Pin_14 | GPIO_Pin_15;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOB, &gpio_init);
}
void rotary_afio_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource15);
}
void rotary_exti_init()
{
EXTI_InitTypeDef exti_init;
exti_init.EXTI_Line = EXTI_Line14;
exti_init.EXTI_LineCmd = ENABLE;
exti_init.EXTI_Mode = EXTI_Mode_Interrupt;
exti_init.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&exti_init);
exti_init.EXTI_Line = EXTI_Line15;
exti_init.EXTI_LineCmd = ENABLE;
exti_init.EXTI_Mode = EXTI_Mode_Interrupt;
exti_init.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&exti_init);
}
void rotary_nvic_init()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef nvic_init;
nvic_init.NVIC_IRQChannel = EXTI15_10_IRQn;
nvic_init.NVIC_IRQChannelCmd = ENABLE;
nvic_init.NVIC_IRQChannelPreemptionPriority = 1;
nvic_init.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&nvic_init);
}
int rotary_update_number()
{
return rotary_count;
}
void EXTI15_10_IRQHandler()
{
if (EXTI_GetITStatus(EXTI_Line14) == SET)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) == 0)
{
rotary_count++;
}
EXTI_ClearITPendingBit(EXTI_Line14);
}
else if (EXTI_GetITStatus(EXTI_Line15) == SET)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
rotary_count--;
}
EXTI_ClearITPendingBit(EXTI_Line15);
}
}
这里我把旋转编码器的两个口分别接到PB14和PB15口,这里要注意一下有些函数是不能用或(|)来实现同时初始化的,如果程序出现只有转向一边才有反应,那么应该检查一下这个地方;
头文件声明
#ifndef __ENCOUTER_H__
#define __ENCOUTER_H__
void rotary_encounter_init(void);
void rotary_afio_init(void);
void rotary_exti_init(void);
void rotary_nvic_init(void);
int rotary_update_number(void);
#endif
主程序调用
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "rotary_encounter.h"
int main()
{
unsigned int count = 0;
OLED_Init();
rotary_encounter_init();
rotary_afio_init();
rotary_exti_init();
rotary_nvic_init();
OLED_ShowString(1, 1, "count:");
while(1)
{
count = rotary_update_number();
OLED_ShowSignedNum(1, 7, count, 5);
}
return 0;
}
(三)总结
通过使用外部的对射红外接收模块和旋转编码器,我们了解了stm32的外部中断,并且对外部中断的各个环节(GPIO、AFIO、EXTI、NVIC)进行了配置,并了解了其功能。