前言
EXTI中断来判断按键按下
EXTI即外部中断/事件控制器,总共支持19个中断/事件请求。每一条中断线都有独立的使能和产生中断后的标志位。
上图可见,中断/时间线0-15,总共16条线分配给了IO,通过设置AFIO的AFIO_EXTICR1、AFIO_EXTICR2、AFIO_EXTICR3、AFIO_EXTICR4这四个寄存器来配置要选择哪一组pin作为外部中断输入,比如 AFIO_EXTICR1这个寄存器里面分为了:
意思是,如果我想设置为PA0,那就把 AFIO_EXTICR1 的0-3位设置为0000 ,如果想要设置PC3,那就设置AFIO_EXTICR1 的12-15位 为0010,以此类推,每个AFIO_EXTICRx寄存器控制的是4组Pin,AFIO_EXTICR1控制的是Pin0-Pin3,AFIO_EXTICR2控制的是Pin4-Pin7,AFIO_EXTICR3控制的是Pin8-P11,AFIO_EXTICR4控制的是Pin12-Pin15 。需要设置不同的Port就是根据当前要操作的Pin找到需要操作哪个AFIO_EXTICRx寄存器,然后根据需要设置的Port(即PA或PB或PC等…)把相应EXTIx的值填好。
写到这里心里面产生一个疑问,比如我只需要设置PA0为EXTI0输入线的输入信号,那么在配置的时候其他输入线应该也是有初值在的。后面应该是需要通过关闭不需要的输入信号的中断来实现只响应PA0的变化中断。
我这边新建了一个bsp_exti.c文件,内容如下:
#include "bsp_exti.h"
static void EXTI_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
void exti_key_gpio_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
EXTI_InitTypeDef EXTI_InitStruct;
//配置中断优先级
EXTI_NVIC_Config();
RCC_APB2PeriphClockCmd(GPIO_KEY_RCC,ENABLE);
GPIO_InitStruct.GPIO_Pin = EXTI_GPIO_PIN;
GPIO_InitStruct.GPIO_Mode = EXTI_GPIO_MODE;
GPIO_Init(EXTI_GPIO,&GPIO_InitStruct);
//初始化EXTI
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //GPIO用作EXTI中断,必须打开AFIO时钟
GPIO_EXTILineConfig(EXTI_SOURCE,EXTI_PIN_SOURCE); //选择的输入线为 GPIOA.Pin15
EXTI_InitStruct.EXTI_Line = EXTI_LINE;
EXTI_InitStruct.EXTI_Mode = EXTI_MODE;
EXTI_InitStruct.EXTI_Trigger = EXTI_TRI;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
}
头文件如下:
#ifndef __BSP_EXTI_H
#define __BSP_EXTI_H
#include "stm32f10x.h"
#define GPIO_KEY_RCC RCC_APB2Periph_GPIOA
#define EXTI_GPIO GPIOA
#define EXTI_GPIO_PIN GPIO_Pin_15
#define EXTI_GPIO_MODE GPIO_Mode_IPU
#define EXTI_SOURCE GPIO_PortSourceGPIOA
#define EXTI_PIN_SOURCE GPIO_PinSource15
#define EXTI_LINE EXTI_Line15
#define EXTI_MODE EXTI_Mode_Interrupt
#define EXTI_TRI EXTI_Trigger_Falling
void exti_key_gpio_init(void);
#endif
EXTI_NVIC_Config();这个函数负责配置中断优先级,因为这个函数是专门新建给EXTI用的,所以在EXTI的c文件里面静态声明一下,不允许其他地方调用。
首先就是像配置IO一样声明一个结构体变量,用于传参进去NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);函数,这个函数的参就是NVIC_InitTypeDef类型的结构体的地址,所以新建一个结构体变量,名字为NVIC_InitStruct。配置响应的参数给这个结构体变量。
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); 这个函数是用于给中断分组,在NVIC_IPRx这个中断优先级寄存器中,高四位代表主优先级和子优先级,但是4个位中哪些位是代表主优先级哪些位代表子优先级,这个又是通过分组来控制的。如果选择的分组是0,那么NVIC_IPRx的高四位中,四位全是代表子优先级,主优先级默认是0,所以别的中断要跟EXTI中断比较的话,只有主优先级为0且子优先级的数字比EXTI的子优先级数字小的才能产生嵌套中断(主优先级和子优先级都相同的情况下,比较两个中断的序号,序号越前的优先级越高,但是这种情况几率很小,这里不讨论)。
这里我配置的NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); 即给EXTI中断分组到分组1去,那么NVIC_IPRx寄存器的高四位中的最高位,即第七位为0或为1,控制的是EXTI的主优先级为0或者1,NVIC_IPRx的第4-6位控制是子优先级,子优先级有8级 (0-7)。
NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn; 这个配置的是EXTI产生的中断通道是 EXTI10-15通道(因为我用的按键是PA15,所以我选择的输入源就是PA15,输入线即为EXTI15,库函数规定输入线为10-15时,NVIC_IRQChannel的值要设置为EXTI15_10_IRQn)
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
这两句则是设置主优先级为1 子优先级为1
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
开对应Channel的NVIC中断
然后再调用初始化函数 把结构体变量的地址作为形参传进去NVIC_Init(&NVIC_InitStruct);
NVIC初始化函数就写好了,接下来就是初始化按键 初始化EXTI,并且调用EXTI_NVIC_Config();函数配置好EXTI中断的优先级和通道并开启中断响应……
-
第一步–配置按键。先开按键所处的Port的所处的APB时钟线时钟。然后配置相应的Pin为输入上拉模式,最后调用GPIO_Init(EXTI_GPIO,&GPIO_InitStruct);函数,初始化相应的Pin。
-
第二步–开始初始化EXTI
首先配置时钟。调用RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);开启时钟,因为选择IO作为EXTI输入,需要用到AFIO,所以这里开时钟开的是AFIO的时钟。
GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
这个函数第一个参数:GPIO_PortSource: selects the GPIO port to be used as source for EXTI lines.This parameter can be GPIO_PortSourceGPIOx where x can be (A…G).
也就是说这里需要填选择哪一个Port作为EXTI输入第二个参数:GPIO_PinSource: specifies the EXTI line to be configured.This parameter can be GPIO_PinSourcex where x can be (0…15).
也就是这里要填哪一个Pin作为EXTI输入其实这个GPIO_EXTILineConfig()函数就是根据我们传入的参,判断之后帮我们操作了AFIO_EXTICRx寄存器。
-
第三步–配置EXTI结构体变量,调用初始化EXTI函数
EXTI_Line : 选择是哪一天EXTI输入线,我这里是PA15,所以这里要填EXTI_Line15
EXTI_Mode:选择输入信号产生符合要求的变化之后,是发生中断还是事件
EXTI_Trigger:选择输入信号发生什么动作会产生中断/事件,可以选择上升沿、下降沿以及双边沿
EXTI_LineCmd:选择是否使能EXTI产生中断/事件
调用EXTI_Init(&EXTI_InitStruct);即可初始化EXTI中断。
在主函数中初始化EXTI按键中断:
unsigned char i=0;
int main(void)
{
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
led_gpio_init();
exti_key_gpio_init();
while(1)
{
}
}
在中断服务函数的.c文件中:
void EXTI15_10_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line15) != RESET) //确定产生了中断
{
GPIOA->ODR ^= GPIO_Pin_8;
}
EXTI_ClearITPendingBit(EXTI_Line15);
}
通过调用EXTI_GetITStatus();得到返回值来判断相应的Pin是否产生中断来判断按键是否按下。
EXTI_GetITStatus()这个函数其实就是根据我们传入的参(我们选择的EXTI线-EXTI15)。
首先判断一下我们是否打开了EXTI14这条EXTI线的中断,主要就是判断EXTI_IMR这个寄存器的对应位,这个寄存器的0-19位代表了EXTI0-19位是否使能中断。(如果设置为符合要求就产生事件,那么这函数判断的是EXTI_EMR这个寄存器的0-19位。)
然后再根据 挂起寄存器(EXTI_PR)的0-19位来判断相应的EXTI线是否产生了触发请求。
最后判断一下 如果当前的EXTI线允许中断/事件,且挂起寄存器里面的对应位被置1,即产生了触发请求。两者同时成立,那么就会产生中断/事件,我这里配置的是产生中断,所以当我按下按键之后,就会产生一个中断,并调用EXTI15_10_IRQHandler这个服务函数。值得注意的是EXTI15_10_IRQHandler()这个函数名不是乱起的,跟51单片机的中断入口的序号一样,这个名字是写在启动文件里面的,如果这个名字没写对,那么MCU会调用库里面弱定义的EXTI15_10_IRQHandler()函数,内容是while(1),即永远停在这个服务函数里面。
如果是EXTI8,即PA8、PB8……作为输入信号,那么中断服务函数需要用EXTI9_5_IRQHandler。
那么如果我们PA7、PA9同时作为输入信号,按下任意一个按键都能进入EXTI9_5_IRQHandler这个服务函数,那么就需要额外判断一下输入源,根据GPIO->IDR这个寄存器判断是哪个按键按下了,然后再执行相应的操作。
以上是EXTI中断的方法。
滴答定时器产生定时中断
SysTick—系统定时器是属于 CM3 内核中的一个外设,内嵌在 NVIC 中。系统定时器是一个 24bit
的向下递减的计数器,计数器每计数一次的时间为 1/SYSCLK,一般我们设置系统时钟 SYSCLK
等于 72M。当当前数值寄存器的值递减到 0 的时候,系统定时器就产生一次中断,以此循环往
复。
因为 SysTick 是属于 CM3 内核的外设,所以所有基于 CM3 内核的单片机都具有这个系统定时器,
使得软件在 CM3 单片机中可以很容易的移植。系统定时器一般用于操作系统,用于产生时基,维
持操作系统的心跳。
SysTick—系统定时器有 4 个寄存器,简要介绍如下。在使用 SysTick 产生定时的时候,只需要配
置前三个寄存器,最后一个校准寄存器不需要使用。
因为是内核里面的外设,配置的东西比较少,赋好值之后使能中断、使能定时器之后就可以产生中断。
我这里是新建了一个bsp_systick.c文件,内容如下:
#include "bsp_systick.h"
uint32_t delay_time=0;
void systick_init(uint32_t ms)
{
if(SysTick_Config(ms)) //SysTick_Config(uint32_t ticks) 这个函数回返回一个uint32_t类型的变量 如果为1:说明滴答定时器的重载寄存器赋的初值太大 进入报错处理 为0说明正常
{ //uint32_t ticks 这个形参传入的就是为重载寄存器赋的初值 范围是0-2^24(24位寄存器)
while (1) //函数的作用就是初始化系统的滴答定时器,并且开启中断,时间就按照传入的值来定
{
;
}
}
}
void delay_xms(uint32_t xms)
{
delay_time = xms;
while (delay_time != 0)
{
;
}
}
void delay_time_sub(void)
{
if(delay_time > 0)
{
delay_time--;
}
}
因为时钟输入是72M,那么计数一次的时间就是1/72000000,如果我想要计时10ms,则是需要填720000。公式是 计数值/频率(Hz)=时间(s)。那么就是计数值=时间x频率=0.01(秒)x72000000Hz=720000次。
所以在main函数初始化时就规定了,产生一次系统定时器中断的时间就是10ms,如果需要改,那么重新改变SysTick_Config()的形参(LOAD 重载寄存器的值)就可以改变产生一次中断的时间间隔。
main函数如下:
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_systick.h"
unsigned char i=0;
int main(void)
{
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
led_gpio_init();
systick_init(720000); //10ms中断一次
while(1)
{
for(i=0;i<2;i++)
{
delay_xms(100);
GPIO_ResetBits(GPIOA,GPIO_Pin_8);
delay_xms(100);
GPIO_SetBits(GPIOA,GPIO_Pin_8);
}
SysTick -> CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
}
执行效果就是 PA8这里的一颗红色LED每隔一秒切换一下状态,亮灭两次之后,长灭。(灯是低电平点亮)
是因为我把滴答定时器关掉了,所以灯在第二次灭之后就不在点亮了。
如果再想把滴答定时器打开,那么就SysTick -> CTRL |= SysTick_CTRL_ENABLE_Msk; 就可以重新打开滴答定时器了
额外说下这个滴答定时器配置函数,这个函数是系统写好的,传入的参就是重载寄存器的数值,直接影响到中断一次的间隔。
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1); /* Reload value impossible */
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); /* set Priority for Cortex-M0 System Interrupts */
SysTick->VAL = 0; /* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0); /* Function successful */
}
查了手册:
SysTick_CTRL_TICKINT_Msk 这个位代表的是 systick下数到0之后会不会产生异常请求,如果这个位为1就会产生异常请求,然后进入服务函数。
SysTick_CTRL_ENABLE_Msk 这个位代表的是 是否开启systick定时器