stm32中的外部中断

目录

中断的介绍

HAL中GPIO中断服务程序业务逻辑分层(先看!!!)

中断线->ISR(一对一或多对一)

一对一的关系

多对一的关系

STM32F411CEU6中断线和ISR对应关系

GPIO中断的使用流程

1. 使能 GPIO 外设时钟:

2. 初始化 GPIO 引脚配置:

3. 配置 NVIC(嵌套向量中断控制器):

4. 确定GPIO引脚对应的ISR:

5. 声明并实现对应的ISR

6. HAL_GPIO_EXTI_IRQHander函数(不用声明&定义)

7. 实现用户回调函数Callback(不用声明,只用重写)

中断各部分代码书写位置

1. 时钟使能GPIO外设,初始化GPIO引脚,配置NVIC

2. 声明实现对应的ISR

3. EXTI通用处理函数HAL_GPIO_EXTI_IRQHandler()(不需要声明,不需要定义)

4. 编写HAL_GPIO_EXTI_Callback 函数(不需要声明,只需要定义(实现))


中断的介绍

中断的分类

  • 外部中断: 由微控制器引脚上的电平变化触发(如STM32的EXTI)。

  • 内部中断: 由微控制器内部外设(如定时器溢出、串口接收/发送完成、ADC转换完成、SPI/I2C事件、DMA完成等)触发。

HAL中GPIO中断服务程序业务逻辑分层(先看!!!)

硬件中断事件发生 (例如:GPIO引脚电平变化)——>

NVIC/CPU 响应中断 (根据中断向量表查找ISR)——>

stm32f4xx_it.c 中的 ISR,例如:EXTI0_IRQHandler()——>

HAL 库通用中断处理函数:HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);——>

用户回调函数:HAL_GPIO_EXTI_Callback(GPIO_Pin); 

GPIO引脚->外部中断线(EXTI)

STM32的每个通用输入/输出(GPIO)端口都有多达16个引脚,编号从0到15(例如PA0-PA15,PB0-PB15等等)。这些引脚除了作为普通的数字输入/输出外,还可以配置为外部中断源

然而,STM32并不是为每个GPIO引脚都分配一根独立的中断线。相反,它采用了一种复用(multiplexing)的机制,通过一个叫做外部中断/事件控制器(EXTI)的模块来管理这些中断。

  • EXTI模块与中断线: EXTI模块提供了16条独立的中断/事件请求线(EXTI0到EXTI15)。这些线是真正能触发中断的硬件线路。

  • 引脚到中断线的映射: STM32的设计是,所有端口中相同编号的引脚,都共享同一条EXTI中断线

    • 例如:PA0、PB0、PC0、PD0等等,它们都连接到 EXTI0 这条中断线上。

    • PA1、PB1、PC1、PD1等等,它们都连接到 EXTI1 这条中断线上。

    • 以此类推,直到PA15、PB15、PC15等等,它们都连接到 EXTI15 这条中断线上。

在硬件层面,每条EXTI Line 内部都有一个多路复用器。这个多路复用器只能选择某一个特定端口的引脚来作为该EXTI Line 的实际输入。

EXTI中断线的独占性 由于这种共享机制,在同一时刻,一条EXTI中断线(比如EXTI0)只能选择一个GPIO引脚作为其输入源

也就是说,你不能同时将PA0和PB0都配置为外部中断,并期望它们都能通过EXTI0触发。

举个栗子

假设你的系统中有两个按钮:一个接在PA0上,另一个接在PB5上。这两个中断线是独立的,可以同时被启用并触发中断。但如果你想再把PC0也配置为外部中断,你就不能同时使用PA0和PC0,因为它们都争用EXTI0

 

  • 如果你的代码尝试同时将PA0PC0都配置为外部中断源,让它们都连接到 EXTI0 这条中断线上,那么会发生“后来者居上”的情况: 简而言之,最后一次成功的配置会生效
     
  • 如果你在CUBEMX中尝试这样配置的话,是不可以的,当你配好了PA0再去配置PC0的时候,PA0的引脚配置将会自动被取消。可以去try一下,非常有趣,嘻嘻

避免这种冲突的方法很简单:确保每条EXTI中断线在任何时候都只被一个GPIO引脚使用

外部中断线->ISR(一对一 + 多对一)

一对一 + 多对一的关系

当一个外部中断线被触发时,它会向嵌套向量中断控制器(NVIC)发出一个中断请求。NVIC负责管理所有中断的优先级和调度,最终将控制权交给相应的中断服务程序(ISR)。

ISR的共享: STM32为了优化中断向量表的空间和管理,并不是每根EXTI中断线都对应一个独立的ISR。有的EXTI中断线会独占一个ISR有的多条EXTI中断线可能会共享同一个ISR

独立ISR

通常,EXTI0、EXTI1、EXTI2、EXTI3、EXTI4这几根中断线会分别有自己独立的ISR,例如:

  • EXTI0_IRQHandler()
  • EXTI1_IRQHandler()
  • EXTI2_IRQHandler()
  • EXTI3_IRQHandler()
  • EXTI4_IRQHandler()

这意味着如果EXTI0触发了中断,系统会直接跳转到EXTI0_IRQHandler()中执行。

共享ISR(中断线分组)

而接下来的中断线则会被分组,共享一个ISR。这是为了节省NVIC的中断向量表条目很朴实无华的理由!!!

  • EXTI5到EXTI9(即EXTI5, EXTI6, EXTI7, EXTI8, EXTI9)通常共享一个ISR,比如命名为:EXTI9_5_IRQHandler()。
     
  • EXTI10到EXTI15(即EXTI10, EXTI11, EXTI12, EXTI13, EXTI14, EXTI15)通常共享另一个ISR,比如命名为:EXTI15_10_IRQHandler()。

STM32F411CEU6中断线和ISR对应关系

EXTI0 线——EXTI4 线,都有着对应单独的中断处理函数(ISR)

比如:EXTI0 线: 对应 EXTI0_IRQHandler() 这个 ISR。

任何配置为 EXTI0 的 GPIO 引脚(PA0, PB0, PC0, PD0, PE0, PH0)触发中断,都会调用 EXTI0_IRQHandler()。

EXTI5 到 EXTI9 线共享一个 ISR,即 EXTI9_5_IRQHandler()。

这意味着当 EXTI5、EXTI6、EXTI7、EXTI8 或 EXTI9 中的任何一条线触发中断时,都会调用同一个 EXTI9_5_IRQHandler()。

EXTI10 到 EXTI15 线共享一个 ISR,即 EXTI15_10_IRQHandler()。

共享ISR VS 独占ISR

独占ISR(例如EXTI0_IRQHandler

对于独占ISR,函数本身的名字就明确指出了中断来源(例如EXTI0_IRQHandler就意味着是EXTI0触发的)。因此,你可以直接认为中断是由与该中断线关联的GPIO引脚(例如连接到EXTI0的引脚GPIO_PIN0)触发的,然后将这个引脚传递给统一中断处理函数。

void EXTI0_IRQHandler(void)
{

  // 调用HAL库的通用GPIO外部中断处理函数
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); // 直接指定是GPIO_PIN_0触发了EXTI0中断

}

共享ISR(例如EXTI9_5_IRQHandler

在这种情况下,中断触发后,你不知道是EXTI5,EXTI6,EXTI7,EXTI8,EXTI9中的具体哪条中断线(例如EXTI5、EXTI6等)触发了中断。此时需要将这几个引脚都传递过去。这可能说的有点抽象,没关系,下面我们用一个例子说明:

假如我们配置了PA6和PA7都为外部中断,CubeMX帮我们生成的代码是下面这样的:

void EXTI9_5_IRQHandler(void)
{

  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_6);
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_7);

}

void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
  /* EXTI line interrupt detected */
  if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
  {
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
    HAL_GPIO_EXTI_Callback(GPIO_Pin);
  }
}

代码解释
 

EXTI9_5_IRQHandler 逐个调用 HAL_GPIO_EXTI_IRQHandler,每次传入一个可能的引脚(GPIO_PIN_6,然后是 GPIO_PIN_7)。注意这里并不是对于 5-9 都需要传入检测,因为我们根本没有配置8和9,这个EXTI9_5_IRQHandler共享式ISR只可能被6/7引脚调用,所以只需要传入6和7就行


思考:
你可能会想:那么无论是GPIO_PIN_6引脚出现了中断还是GPIO_PIN_7引脚出现了中断,都会调用两次HAL_GPIO_EXTI_IRQHandler进行中断处理,这样逻辑不是有问题吗?

OK,你想的没错,无论是GPIO_PIN_6引脚出现了中断还是GPIO_PIN_7引脚出现了中断,都会调用两次HAL_GPIO_EXTI_IRQHandler分别传入GPIO_PIN_6和GPIO_PIN_7进行中断处理,但是是没有问题的,爽!原因如下:

因为HAL_GPIO_EXTI_IRQHandler中的__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) 这个宏会去检测传入的 GPIO_Pin 是否真的触发了中断(即检查 EXTI_PR 寄存器中对应的位)。

  • 如果检测到该引脚确实触发了中断,才会进入 if 语句块,执行 清除 (__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin)) 和 回调 (HAL_GPIO_EXTI_Callback(GPIO_Pin))。
     
  • 人如果该引脚没有触发中断,那么久不会执行后续的中断处理操作

GPIO中断的使用流程

1. 使能 GPIO 外设时钟:

使能 GPIO 外设时钟是第一步。

因为所有外设,包括GPIO端口本身,都需要先使能时钟才能工作。

示例:

__HAL_RCC_GPIOA_CLK_ENABLE();

()里面的参数书写GPIO端口

2. 初始化 GPIO 引脚配置:

这包括配置引脚为外部中断模式(EXTI),选择上升沿、下降沿或双边沿触发。同时,可以配置是否使用内部上拉或下拉电阻。

3. 配置 NVIC(嵌套向量中断控制器):

设置中断优先级: 包括抢占优先级和子优先级。

使能对应的中断通道: 告诉NVIC允许响应该中断。

HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0); // 设置中断优先级
HAL_NVIC_EnableIRQ(EXTI0_IRQn);       // 使能中断
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);

IRQn_Type  IRQn:中断请求号 (Interrupt Request Number),这是个枚举值

含义: 这个参数指定了你想要设置优先级的是哪一个具体的中断源。每个外设(比如USART、SPI、定时器、GPIO外部中断线等)都有一个唯一的中断请求号。

uint32_t  PreemptPriority:抢占优先级 (Preemption Priority)

含义: 这个参数定义了中断的抢占级别。
 

范围: 可用的抢占优先级位数以及响应优先级位数都取决于,我们之前设置的优先级分组(通过 HAL_NVIC_SetPriorityGrouping() 函数)。

对于优先级来说,值越小,优先级越高

uint32_t  SubPriority:响应优先级 (Subpriority)

含义: 这个参数定义了中断的相应级别优先级。

补充一下设置中断优先级分组的HAL库函数:

HAL_NVIC_SetPriorityGrouping()

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

只要是我们使用了CubeMX来生成了初始化代码即使我们在 CubeMX 中没有明确配置任何中断,HAL_NVIC_SetPriorityGrouping() 这个设置中断优先级分组的函数通常也会被调用。

        这是因为 HAL_NVIC_SetPriorityGrouping() 是 STM32 HAL 库系统初始化的一部分,它在内部会被 HAL_Init() 函数调用(或者在 CubeMX 生成的 main.c 中直接调用),而 HAL_Init() 是每个基于 HAL 库的 STM32 项目启动时都会执行的。

所以我们使用CubeMX生成的代码一般不需要我们手动设置优先级,一般使用默认划分就可以了,除非对于优先级的划分有着明确的要求。
 

CubeMX的对于HAL_NVIC_SetPriorityGrouping的参数设置常用默认值是:

  • 为抢占优先级分2位,响应优先级分2位
     
  • 或者为抢占优先级分4位,响应优先级分0位

4. 确定GPIO引脚对应的ISR:

如果是引脚设置了中断,那么需要确定引脚对应调用的中断处理函数(ISR),然后声明并定义出对应的ISR,(如果是Cubemx的话这步都会自动帮我们做了)。

上面我们也说过了,并不是每一条中断线对应都有着独属于它的中断服务函数(ISR),会出现中断线——>ISR为多对一的情况。

所以我们需要知道引脚所处中断线对应的ISR,(查阅相关资料比如芯片原理图获得)。

5. 声明并实现对应的ISR

确定好了后需要将对应的ISR声明并且实现。

这里需要注意的是:每个中断线对应的ISR名字是固定的,不能随便定义名字!!!

如果使用CubeMX配置引脚为中断模式的话,它会自动帮我们进行配置好的(会自动在stm32f4xx_it.h中声明这个ISR函数,在stm32f4xx_it.c中,定义这个ISR函数)。

如果是我们手动配置引脚模式的话,关于ISR的声明以及内部定义(实现)需要我们手动操作。

我们说到了关于中断线对应的ISR名字是固定的(系统已经定义好了),在启动文件中, 如下图所示:

我们通常会在外部中断的ISR中,调用HAL_GPIO_EXTI_TRQHander()这个外部中断处理函数。

值得注意的是HAL_GPIO_EXTI_TRQHander()这个外部中断处理函数需要接受传入一个参数(具体的引脚号)。函数原型如下图所示:

为什么HAL_GPIO_EXTI_IRQHandler需要传入触发中断的引脚号(uint16_t GPIO_Pin)作为参数?
 

STM32 的外部中断控制器(EXTI)是按 中断线(EXTI Line)设计的,而不是按单个GPIO引脚设计的。例如:

所有GPIO端口的引脚0(PA0, PB0, PC0等)都连接到 EXTI Line 0。

这意味着,当 EXTI0_IRQHandler 被触发时,它只知道是 EXTI Line 0 发生了中断,但不知道具体是 PA0、PB0 还是 PC0 等触发的。

为了解决这个问题,HAL 库在具体的 EXTI 中断服务函数(ISR)中(例如 EXTI0_IRQHandler()、EXTI1_IRQHandler() 等),会调用 HAL_GPIO_EXTI_IRQHandler() 并传入相应的引脚号

通过这种方式,HAL_GPIO_EXTI_IRQHandler() 函数才能知道具体是哪个引脚触发了中断,从而正确地清除中断标志位,并将该信息传递给回调函数 HAL_GPIO_EXTI_Callback(),让我们的应用程序能够针对特定引脚的中断进行处理。

6. HAL_GPIO_EXTI_IRQHander函数(不用声明&定义)

无论是使用 CubeMX 还是手动编写代码,我们都不需要自己编写 HAL_GPIO_EXTI_IRQHandler() 这个函数。

它的代码已经包含在了stm32f4xx_hal_gpio.c中,函数体的具体实现已经固定下来了,可以当成一个HAL库函数直接调用,类似HAL_GPIO_Init()这样,可以直接调用。

所有通过 GPIO 引脚 触发的外部中断,其在 HAL 库层的统一处理函数就是 HAL_GPIO_EXTI_IRQHandler()

HAL_GPIO_EXTI_IRQHander函数的功能

中我们首先需要做Clear操作,防止重复调用。

在 HAL_GPIO_EXTI_IRQHandler中进行 清除中断挂起标志位(Clear Pending Bit) 的操作,是为了防止中断重复触发,从而避免系统崩溃或进入无限循环。

在 HAL 库 中会使用类似 __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN) 这样的宏或函数来清除相应的 EXTI 挂起标志位。

然后我们需要调用Callback这个通用回调函数,在回调函数中会真正实现我们的中断处理逻辑。

7. 实现用户回调函数Callback(不用声明,只用重写)

这是我们实现中断响应逻辑的地方。

例如,当按键按下时,我们想做什么操作(点亮LED、发送数据等),就写在这个回调函数里。

这个函数的声明,STM官方已经定下来了,是一个弱函数的形式,不需要我们进行定义了,我们通常只需要在main.c中对其进行实现就可以了! 

GPIO 外部中断的回调函数统一是 HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin),所有通过 外部中断/事件控制器 (EXTI) 触发的 GPIO 外部中断,都会统一调用这一个回调函数。

中断各部分代码书写位置

1. 时钟使能GPIO外设,初始化GPIO引脚,配置NVIC

在 CubeMX 生成的代码中: 这部分通常在 main.c 中的 MX_GPIO_Init() 函数内部。CubeMX 会根据您在图形界面中的配置自动生成这些代码。

如果是手动编写的话,我们也强烈推荐模仿HAL库的使用方式:将时钟使能、GPIO引脚初始化和NVIC配置这三个部分的代码定义在 gpio.c中,比如在MX_GPIO_Init()内部实现这个代码。

这样书写的优点

  • 将所有与GPIO相关的初始化和配置代码集中在一个地方,使得项目结构清晰,易于理解。
     
  • 如果需要修改或查看GPIO的配置,只需打开 gpio.c 文件即可,而不需要在 main.c 或其他文件中搜索。

2. 声明实现对应的ISR

  • ISR 的声明通常在stm32f4xx_it.h中,
  • ISR 的定义(实际函数体)通常在 stm32f4xx_it.c 文件中。

如果使用了CubeMX 配置了GPIO引脚为中断模式,会自动为我们生成ISR的声明和定义,在ISR的实现中调用 HAL 库的通用中断处理函数xx_Hander。

如果没有使用CubeMX 配置,需要我们自己在stm32f4xx_it.h文件中进行ISR的声明,以及在 stm32f4xx_it.c 文件中进行ISR的定义(实现)

我们也知道中断线对应的ISR函数名称是固定的,不能由我们随便定义,所以我们在ISR的声明和定义的时候必须严格使用这个名字,各中断线对应的ISR名字在启动文件中定义了,如下图所示:

3. EXTI通用处理函数HAL_GPIO_EXTI_IRQHandler()(不需要声明,不需要定义)

无论是使用 CubeMX 还是手动编写代码,我们都不需要自己编写 HAL_GPIO_EXTI_IRQHandler() 这个函数。它的代码已经包含在了stm32f4xx_hal_gpio.c中

函数体已经固定下来了,可以当成一个HAL库函数直接调用,类似HAL_GPIO_Init()这样,可以直接调用。

所有通过 GPIO 引脚 触发的外部中断,其在 HAL 库层的统一处理函数就是 HAL_GPIO_EXTI_IRQHandler()

4. 编写HAL_GPIO_EXTI_Callback 函数(不需要声明,只需要定义(实现))

实现用户回调函数,最佳实践: 应该在 main.c 文件中。这是因为 HAL_GPIO_EXTI_Callback 函数在 HAL 库内部是使用 __weak(弱定义)修饰的。这意味着如果我们在自己的代码中定义了同名的函数,编译器会优先使用我们的版本,而不是库中空的弱定义版本。

GPIO 外部中断的回调函数统一是 HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin),所有通过 外部中断/事件控制器 (EXTI) 触发的 GPIO 外部中断,都会统一调用这一个回调函数。

还有就是当代码出现错误的时候如何进行调试工作,分模块调试,这里主要是3块,led,按键,中断,分别进行调试,排查可能出错的地方

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值