STM32F10x按键中断,你的单片机里发生了什么
前言
之前的文章里讲了按键输入,但是使用的是按键扫描的方式实现的,这将会占用CPU资源,毕竟每次扫描按键也要执行相应的代码花费一定的时间,如果你的代码有十行,说不定扫描按键的那一行就要分走1/10的CPU资源。本文里我们使用stm32的中断功能来实现按键输入的功能,顺便聊聊这个过程中你的单片机都干了些什么。
偏移地址与基地址
进入正题前,先聊一下这两个概念。
STM32有非常多的寄存器,如果一定要直接通过绝对地址来访问寄存器不是不行,为了方便STM32标准库里常用偏移地址+基地址的方式来访问寄存器。
这就好比,为了管理你的文件,你通常会建立一个文件夹把一些相关的文件放在一起,STM32里一组相关的寄存器
的地址是连续的,所以可以定义一个基本地址
作为这一组寄存器的起始点
,然后这一组里的每一个寄存器都有一个相对于起始点(也就是基地址)的偏移地址
,想要访问这一组寄存器里的某一个寄存器,只要拿起始地址(也就是基地址)加上偏移地址就能得到这个寄存器的绝对地址,进而实现对这个寄存器的一系列操作。
stm32f10x.h 文件里定义了EXTI寄存器的基地址
#define EXTI_BASE (APB2PERIPH_BASE + 0x0400)
透过EXTI_Init()
看外部中断初始化
EXTI_Init()
函数定义如下:
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct)
{
uint32_t tmp = 0;
/* Check the parameters */
assert_param(IS_EXTI_MODE(EXTI_InitStruct->EXTI_Mode));
assert_param(IS_EXTI_TRIGGER(EXTI_InitStruct->EXTI_Trigger));
assert_param(IS_EXTI_LINE(EXTI_InitStruct->EXTI_Line));
assert_param(IS_FUNCTIONAL_STATE(EXTI_InitStruct->EXTI_LineCmd));
tmp = (uint32_t)EXTI_BASE;
if (EXTI_InitStruct->EXTI_LineCmd != DISABLE)
{
/* Clear EXTI line configuration */
EXTI->IMR &= ~EXTI_InitStruct->EXTI_Line;
EXTI->EMR &= ~EXTI_InitStruct->EXTI_Line;
tmp += EXTI_InitStruct->EXTI_Mode;
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
/* Clear Rising Falling edge configuration */
EXTI->RTSR &= ~EXTI_InitStruct->EXTI_Line;
EXTI->FTSR &= ~EXTI_InitStruct->EXTI_Line;
/* Select the trigger for the selected external interrupts */
if (EXTI_InitStruct->EXTI_Trigger == EXTI_Trigger_Rising_Falling)
{
/* Rising Falling edge */
EXTI->RTSR |= EXTI_InitStruct->EXTI_Line;
EXTI->FTSR |= EXTI_InitStruct->EXTI_Line;
}
else
{
tmp = (uint32_t)EXTI_BASE;
tmp += EXTI_InitStruct->EXTI_Trigger;
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
}
}
else
{
tmp += EXTI_InitStruct->EXTI_Mode;
/* Disable the selected external lines */
*(__IO uint32_t *) tmp &= ~EXTI_InitStruct->EXTI_Line;
}
}
在正式开始之前,再给大家介绍一下*(__IO uint32_t *) tmp
是个什么东西。
根据注释可以看出前面的__IO
相当于获得一个读写的权限,通过(__IO uint32_t *) tmp
把tmp这个变量强制转换成了一个32位的指针再在前面加上*
号使得它指向这个tmp这个地址所指向的值。
如果你感兴趣,可以自己搜一下volatile
这个关键字的作用,本文里不展开讨论。
在初始化函数里,定义了tmp这个变量,然后给它赋值为EXTI_BASE
的值,也就是说此时tmp相当于EXTI_BASE
的地址的值,然后后面
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
这句代码相当于获得读写权限后对寄存器的内容进行赋值。
再回到咱们的话题上。
咱们的外部中断配置主要和这几个寄存器有关:
- 中断屏蔽寄存器(EXTI_IMR)
- 事件屏蔽寄存器(EXTI_EMR)
- 上升沿触发选择寄存器(EXTI_RTSR)
- 下降沿触发选择寄存器(EXTI_FTSR)
- 软件中断事件寄存器(EXTI_SWIER)
- 挂起寄存器(EXTI_PR)
当然NVIC寄存器也有关,咱们后面再说;由于本文里是使用按键来触发外部中断,所以不使用和事件相关的寄存器。需要注意的是EXTI
指的是外部中断/事件控制器
,而NVIC
所指的是嵌套向量中断控制器
,后面还会提到外部中断配置寄存器(EXTICR)
,注意区分。
我们在使用库函数配置中断的时候一般采用这样的方式:
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line=EXTI_Line4;
EXTI_InitStruct.EXTI_LineCmd=ENABLE;
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStruct);
在这里,当你打算配置外部中断时通常会选择:
EXTI_Init_Structure.EXTI_Mode=EXTI_Mode_Interrupt;
在stm32f10x_exti.h
里,你能找到下面的代码:
typedef struct
{
uint32_t EXTI_Line; /*!< Specifies the EXTI lines to be enabled or disabled.
This parameter can be any combination of @ref EXTI_Lines */
EXTIMode_TypeDef EXTI_Mode; /*!< Specifies the mode for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */
EXTITrigger_TypeDef EXTI_Trigger; /*!< Specifies the trigger signal active edge for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */
FunctionalState EXTI_LineCmd; /*!< Specifies the new state of the selected EXTI lines.
This parameter can be set either to ENABLE or DISABLE */
}EXTI_InitTypeDef;
再往前翻,能看到:
typedef enum
{
EXTI_Mode_Interrupt = 0x00,
EXTI_Mode_Event = 0x04
}EXTIMode_TypeDef;
这里EXTI_Mode_Interrupt = 0x00
和EXTI_Mode_Event = 0x04
刚好对应了EXTI寄存器里中断屏蔽寄存器(EXTI_IMR)
和事件屏蔽寄存器(EXTI_EMR)
的偏移地址,如下图所示:
在EXTI_Init()
里定义的临时变量tmp
经过了如下的赋值操作:
uint32_t tmp = 0;
tmp = (uint32_t)EXTI_BASE;
tmp += EXTI_InitStruct->EXTI_Mode;
这意味着tmp
的值为EXTI
的基地址
加上一个由你设置的mode所决定的偏移地址
。
如果你设置的mode是中断模式,那么tmp
此时的值就是中断屏蔽寄存器(EXTI_IMR)
的地址值,如果是事件中断,那么就是事件屏蔽寄存器(EXTI_EMR)
的地址值,然后执行:
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
将会给相应寄存器的相应的位置位以开启相应的中断请求。
在后续设置触发中断的边沿信号的过程也是同样的方式,如果是升降沿触发就直接配置了上升沿触发选择寄存器(EXTI_RTSR)
和下降沿触发选择寄存器(EXTI_FTSR)
的相应位。
相应代码也在stm32f10x_exti.h
里:
typedef enum
{
EXTI_Trigger_Rising = 0x08,
EXTI_Trigger_Falling = 0x0C,
EXTI_Trigger_Rising_Falling = 0x10
}EXTITrigger_TypeDef;
读者不妨自己分析一下,思路和模式配置的方式一样。
挂起寄存器(EXTI_PR)
到现在还没有被提及,因为它的作用是记录中断请求,当在外部中断线上发生了选择的边沿事件,该位被置1,在该位中写入1可以清除它,嗯,你没看错,这里我也没写错,就是有触发信号时会被置1,并且要写入1才能清除它,是不是很违背直觉呢。
通常,我们会在中断服务函数里写清除中断标志位的代码:
EXTI_ClearITPendingBit(EXTI_Line_x);
这个函数内部就是通过向相应位写1来清除标志位的:
void EXTI_ClearITPendingBit(uint32_t EXTI_Line)
{
/* Check the parameters */
assert_param(IS_EXTI_LINE(EXTI_Line));
EXTI->PR = EXTI_Line;
}
从EXTI->PR = EXTI_Line;
这一句你就能看出来,写0是没用滴,因为这是一句直接赋值的语句,而不是|=
。
至此,外部中断的配置还不算完,还需要把引脚与相应的中断线相映射在一起。
使用的函数是:
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
{
uint32_t tmp = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_EXTI_PORT_SOURCE(GPIO_PortSource));
assert_param(IS_GPIO_PIN_SOURCE(GPIO_PinSource));
tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
}
与之相关的寄存器有4个:
- 外部中断配置寄存器 1(AFIO_EXTICR1)
- 外部中断配置寄存器 2(AFIO_EXTICR2)
- 外部中断配置寄存器 3(AFIO_EXTICR3)
- 外部中断配置寄存器 4(AFIO_EXTICR4)
其实还有几个寄存器,不过它们和配置外部中断关系不大,有兴趣的话自己去了解一下吧。
typedef struct
{
__IO uint32_t EVCR;
__IO uint32_t MAPR;
__IO uint32_t EXTICR[4];
uint32_t RESERVED0;
__IO uint32_t MAPR2;
} AFIO_TypeDef;
AFIO_TypeDef
的结构体里有一个数组__IO uint32_t EXTICR[4]
这个数组对应了前面提到的4个外部中断配置寄存器
。
tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
上面这两句代码很有意思,需要参考GPIO_PinSource
的定义以及外部中断配置寄存器
的说明结合来看:
#define GPIO_PinSource0 ((uint8_t)0x00)
#define GPIO_PinSource1 ((uint8_t)0x01)
#define GPIO_PinSource2 ((uint8_t)0x02)
#define GPIO_PinSource3 ((uint8_t)0x03)
#define GPIO_PinSource4 ((uint8_t)0x04)
#define GPIO_PinSource5 ((uint8_t)0x05)
#define GPIO_PinSource6 ((uint8_t)0x06)
#define GPIO_PinSource7 ((uint8_t)0x07)
#define GPIO_PinSource8 ((uint8_t)0x08)
#define GPIO_PinSource9 ((uint8_t)0x09)
#define GPIO_PinSource10 ((uint8_t)0x0A)
#define GPIO_PinSource11 ((uint8_t)0x0B)
#define GPIO_PinSource12 ((uint8_t)0x0C)
#define GPIO_PinSource13 ((uint8_t)0x0D)
#define GPIO_PinSource14 ((uint8_t)0x0E)
#define GPIO_PinSource15 ((uint8_t)0x0F)
这里只截取了两个外部中断配置寄存器
的说明,从上面的说明,不难发现每个外部中断配置寄存器
控制了每个端口的4个引脚。
外部中断配置寄存器1
控制的是每个端口的0-3
号引脚
外部中断配置寄存器2
控制的是每个端口的4-7
号引脚
外部中断配置寄存器3
控制的是每个端口的8-11
号引脚
外部中断配置寄存器4
控制的是每个端口的12-15
号引脚
GPIO_PinSource >> 0x02
能够获取管理GPIO_PinSource
的对应外部中断配置寄存器
,比如我的GPIO_PinSource
是6号引脚,那么开头这句代码的值就是
0110 >> 2 = 0001
6号引脚应当由外部中断配置寄存器2
来管理,刚好AFIO->EXTICR[1]
所指代的就是外部中断配置寄存器2
。
同时GPIO_PinSource & (uint8_t)0x03
的作用相当于GPIO_PinSource
对4求余数,为啥呀?因为对一个数字&0x03(二进制0011)
就相当把0x10(二进制0100)
以上的值全部忽略,只看小于0x10
部分的内容,那它要是为0就说明这个值能够被4整除,不为零说明有余数,这个余数很关键,它能告诉你它在外部中断配置寄存器
所对应哪四个位。
还是以6号引脚为例子:
GPIO_PinSource & 0x03 = 0110 & 0011 = 0010 = 2
执行
tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));
即为
0x0F << (4 * 2) = 0x0F00 (也就是二进制 0000 1111 0000 0000)
6号引脚所对应的就是寄存器里的8-11位
那么后面这句
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
它的意义就请你自己结合下面的宏定义推理一下吧
#define GPIO_PortSourceGPIOA ((uint8_t)0x00)
#define GPIO_PortSourceGPIOB ((uint8_t)0x01)
#define GPIO_PortSourceGPIOC ((uint8_t)0x02)
#define GPIO_PortSourceGPIOD ((uint8_t)0x03)
#define GPIO_PortSourceGPIOE ((uint8_t)0x04)
#define GPIO_PortSourceGPIOF ((uint8_t)0x05)
#define GPIO_PortSourceGPIOG ((uint8_t)0x06)
特别提醒:想要使用AFIO寄存器
不要忘记使能相应的时钟哦。
NVIC
这个寄存器的内容很丰富,但是限于篇幅,我打算以后有机会再来详细介绍这个寄存器,不过还是有一些需要注意的点想在这里与大家分享一下。
在我们初始化中断时,需要配置这个寄存器,对于外部中断,想必大家都看过下面这个结构框图吧。
按键初始化的时候,你会可能会使用下面这种代码:
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel=EXTI4_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStruct);
上面代码里的NVIC_IRQChannel
取值是取你需要的引脚的号码,但是当你查看相关宏定义的时候会发现:
EXTI0_IRQn = 6, /*!< EXTI Line0 Interrupt */
EXTI1_IRQn = 7, /*!< EXTI Line1 Interrupt */
EXTI2_IRQn = 8, /*!< EXTI Line2 Interrupt */
EXTI3_IRQn = 9, /*!< EXTI Line3 Interrupt */
EXTI4_IRQn = 10, /*!< EXTI Line4 Interrupt */
只有5条通道,但是一个端口的引脚数目可不止5个啊,其它的在哪?
别急,往下找,你会找到:
EXTI9_5_IRQn = 23, /*!< External Line[9:5] Interrupts */
EXTI15_10_IRQn = 40, /*!< External Line[15:10] Interrupts */
同时你需要更改你的中断服务函数,比如:
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_LineX) == SET)
{
EXTI_ClearITPendingBit(EXTI_LineX);//这里的X需要根据需要自己决定取值
}
}
我在这里的中断服务函数里加入了判断中断线的代码,但是在之前的代码里没加,因为之前的代码使用的是0-4号引脚对应的中断号,它们分别有自己的中断服务函数:
void EXTI0_IRQHandler(void)
void EXTI1_IRQHandler(void)
void EXTI2_IRQHandler(void)
void EXTI3_IRQHandler(void)
void EXTI4_IRQHandler(void)
剩下的就没有太多需要注意的东西了。
硬件消抖电路
插一句题外话,我在课上学习到了一个具有消抖效果且又能够防止静电的电路,又联想到了以前学过的数电,画了一个电路,虽然你可能实际上还是会选择软件消抖(貌似确实没必要为了一个简单的按键搞得很复杂),但是也许这能给你提供一点灵感(搞电子的多少都喜欢折腾,哈哈哈)。
这里在输入端加入了一个触发器,当按键没按下的时候打在R端,S端为高电平,R端为低电平,触发器输出为低电平,经过一个非门之后成为高电平,二极管不导通,MCU的输入端检测为高电平;当按键按下之后,S端为低电平,R端为高电平,触发器输出为高电平,经过一个非门之后为低电平,二极管导通,此时MCU检测为低电平;在中间的过渡阶段里,按键抖动时由于S与R端均为高电平,触发器状态保持,故而MCU所检测到的电平不会发生波动。
如果输入端发生意外击穿了二极管,也能保护一下MCU。
感谢您的阅读
以上就是本文的全部内容了,希望对您有所帮助,欢迎大佬们勘误。
最后附上一份按键中断代码和视频:
//中断相关初始化代码
void Key_Interrupt_Cfg(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource9);
EXTI_InitStructure.EXTI_Line=EXTI_Line9;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
//中断服务函数
u8 LED_state = LED_ON;
void EXTI9_5_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line9) == SET)
{
u8 tmp = GPIO_ReadInputDataBit(KEY_PORT,KEY_Pin);
if(tmp==(uint8_t)KEY_PRESSED)
{
//pass
}
else if(tmp==(uint8_t)KEY_RELEASEED)
{
LED_state = 1 - LED_state;
LED_Set(LED_state);
}
EXTI_ClearITPendingBit(EXTI_Line9);
}
}
//主函数
extern u8 LED_state;
int main(void)
{
delay_init();
KEY_GPIO_Init();
Key_Interrupt_Cfg();
LED_Init();
LED_Set(LED_state);
while (1)
{
}
}
按键中断