STM32F10x按键中断,你的单片机里发生了什么

本文详细介绍了如何在STM32F10x单片机中使用中断功能处理按键输入,涉及偏移地址和基地址的概念,EXTI寄存器的配置,以及NVIC寄存器的配合使用,包括中断模式、触发信号和硬件消抖电路的应用。
摘要由CSDN通过智能技术生成

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 = 0x00EXTI_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)
	{
	}
}

按键中断

  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32按键中断例程主要是指针对STM32单片机,通过按键来触发中断服务程序,并对相关的中断进行响应和处理的一个示例代码程序。以下是关于STM32按键中断例程的详细讲解: 1. 程序文件及配置:需要引用头文件"stm32f10x.h"、"stm32f10x_gpio.h"和"stm32f10x_exti.h",同时配置相关的中断和IO口。 2. 按键中断的初始化:主要包括GPIO口初始化、EXTI中断初始化、NVIC中断向量表配置和GPIO口中断处理函数的设置。 3. 接收按键中断信号:通过构造EXTI中断服务程序,实现对按键按下事件的捕捉并进行相应的处理动作。在处理函数中,涉及到的主要操作包含:清除中断标志位、调用相关回调函数、应答外设等操作。 4. 回调函数的设置:中断服务程序需要实现一个回调函数,对中断事件进行处理。回调函数一般存在于主局面中,对按键的响应和业务逻辑进行处理。 5. 应用场景:STM32按键中断例程可应用于各种嵌入式应用场景,包括遥控器、汽车电子、智能家居等领域,实现对按键操作的监听和相应动作的处理。其优点是响应速度快、功耗低、可靠性高,广泛应用于工业控制、通信、医疗等各个领域。 综上所述,STM32按键中断例程是一种基于STM32单片机中断服务程序,通过对按键事件进行监听并进行相关处理,实现了对嵌入式系统的控制和交互,广泛应用于各种应用场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值