中断
1.处理器系统在执行代码的时候,会从存储器依次取出指令和数据,这种能力需要在处理器里保存一个存储器地址,就是所谓的程序计数器(Program Counter,PC),也叫程序指针
2.当外部中断(Extern Interrupt,EXTI)事件发生的时候,单片机有一定的策略保护好当前执行的状态,将PC跳转到中断服务函数(即回调函数),然后执行相应的用户自定义编写的处理策略
3.完成中断后,恢复原有执行
上述过程依赖嵌套中断向量控制器(Nested Vectored Interrupt Controller,NVIC)
中断事件有两类优先级
- 抢占优先级:高的可以直接中断掉低优先级的中断服务函数直接处理
- 子优先级:在多中断事件发生时 ,高的可以插队到低的前面先处理
虚函数
函数声明的前缀有_weak,表示该函数是虚函数
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
UNUSED(GPIO_Pin);
}
_weak
本质上是一个宏定义,实际上的文本对应的是编译指令_attribute((weak))
- 虚函数
- 有虚函数指令——weak的函数声明
- 虚函数可以有很多个
- 实函数
- 没有虚函数指令_weak的函数声明,也就是普通的函数,都是实函数
- 实函数至多只有一个,多了会编译报错(如果是C++可以兼容函数重载)
- 如果实函数不存在,则随机选取一个幸运的虚函数(对我们而言是随机的。但对编译器而言有一定的选取规则)
工程配置
1.根据原理图或者PCB图,找到开发板上按键开关对应的单片机引脚。
2.将单片机开关引脚设置为外部中断0号通道
3.检测模式选择下降沿
4.输入模式选择上拉输入,用于维持平时的高电平
5.使能嵌套中断向量控制器
6.中断的两种优先级默认配置即可
it.c文件里就是各种各样的中断
HAL_GPIO_EXTI_IRQHandler(GPIOI_PIN_0);
go to defination
发现当触发中断时调用回调函数HAL_GPIO_EXTI_Callback(GPIO_Pin);
而这个函数是虚函数,所以我们在main中定义该函数,再次调用的时候就是调用我们自己定义的函数了。(实函数的优先级大于虚函数)
按键消抖方法:
1.通过延迟10ms,略过了不稳定的时间,等稳定下来之后再检测状态
缺点:不能同时调用多个,否则会事与愿违;
2.硬件上再按键两端并联一个电容器,滤除高频率的杂波
缺点:要重新设计电路板
3.不用外部中断,定时直接读取引脚信息,用定时器中断(充分利用了人类的反应时间比计算机慢得多的特点。只需要十几毫秒查询一次即可,同样节省了计算性能)
中断机制补充
在STM32工程中,即使main
函数中没有直接调用中断函数,中断条件触发时仍会调用中断函数,这是由单片机的硬件和中断机制决定的。以下是对这一现象的解释以及单片机执行代码的顺序:
-
中断函数的调用原理
- 中断向量表:STM32 微控制器内部有一个中断向量表,其中包含了各种中断类型及其对应的中断服务程序(ISR)的地址。当中断条件触发时,硬件会自动根据中断类型从中断向量表中查找并跳转到相应的 ISR 去执行。
- 中断优先级:STM32 支持中断优先级设置,高优先级的中断可以打断低优先级的中断执行。当多个中断同时触发时,硬件会根据优先级来决定先处理哪个中断。
- 自动调用机制:一旦中断条件满足,硬件会自动保存当前 CPU 的执行状态,然后跳转到对应的中断服务程序入口处开始执行,不需要在
main
函数中手动调用。
-
单片机执行代码的顺序
- 上电或复位:系统上电或复位后,PC 指针指向固化在 FLASH 中的启动代码,开始执行初始化操作,如设置堆栈指针、初始化时钟等。
- 进入
main
函数:启动代码执行完毕后,跳转到用户编写的main
函数开始执行应用程序的主体代码。 - 中断触发与响应:在
main
函数执行过程中,如果发生了中断条件触发,CPU 会暂停当前的代码执行,自动保存现场环境(如寄存器的值等),并根据中断向量表找到对应的中断服务程序入口地址,然后跳转去执行中断服务程序。 - 中断返回:中断服务程序执行完成后,CPU 会恢复之前保存的现场环境,继续执行被中断的
main
函数中的代码,就好像中断从未发生过一样。
综上所述,STM32 微控制器通过中断向量表和硬件自动调用机制实现了中断函数的调用,而无需在 main 函数中显式地调用它们。同时,单片机执行代码的顺序是固定的,从上电或复位开始,经过启动代码的执行,最终进入 main 函数,并在需要时响应中断。这种机制确保了单片机能够高效、可靠地处理各种外部事件和异常情况。
NVIC补充讲解
嵌套中断向量控制器(Nested Vectored Interrupt Controller, NVIC)是 ARM Cortex-M 系列处理器的核心组件,负责管理中断和异常的优先级、嵌套和调度。其设计目标是实现低延迟、高灵活性的中断处理,满足实时系统的需求。以下从架构、寄存器、优先级机制、中断处理流程及优化技术等方面进行专业级分析。
1. NVIC 架构与核心特性
(1) 硬件架构
- 集成于 Cortex-M 内核:与处理器紧耦合,直接控制中断响应逻辑。
- 向量中断机制:每个中断/异常有唯一的向量地址(存储在向量表中),支持快速跳转。
- 优先级动态配置:支持软件实时修改中断优先级。
- 抢占与尾链优化:硬件自动处理中断嵌套时的现场保存与恢复,减少延迟。
(2) 关键特性
- 支持 1~240 个外部中断(具体数量由芯片厂商定义)。
- 4~256 级可编程优先级(优先级位数由芯片实现决定,如 Cortex-M3/M4 支持 8 级优先级,即 3 位)。
- 优先级分组:将优先级分为抢占优先级(Preemption)和子优先级(Subpriority)。
- 中断屏蔽:通过
PRIMASK
、FAULTMASK
和BASEPRI
寄存器实现中断全局或条件屏蔽。
2. NVIC 寄存器详解
NVIC 通过内存映射寄存器配置,关键寄存器如下(以 Cortex-M3/M4 为例):
(1) 中断使能寄存器
- ISERx (Interrupt Set Enable Register):写 1 到某位使能对应中断。
- ICERx (Interrupt Clear Enable Register):写 1 到某位禁用对应中断。
(2) 中断挂起寄存器
- ISPRx (Interrupt Set Pending Register):手动触发中断挂起。
- ICPRx (Interrupt Clear Pending Register):清除挂起状态。
(3) 优先级寄存器
- IPRx (Interrupt Priority Register):配置中断优先级,每个中断占用 8 位(实际使用位数由芯片决定)。
示例:若使用 4 位优先级(高 4 位有效),优先级字段格式为:
Priority = (Preemption Priority << (8 - Priority_Width)) | Subpriority
(4) 系统异常寄存器
- SHPRx (System Handler Priority Register):配置系统异常(如 SysTick、PendSV)的优先级。
3. 优先级分组与抢占机制
(1) 优先级分组寄存器 (AIRCR)
-
AIRCR[10:8] (PRIGROUP):定义优先级分组方式,将优先级位划分为抢占和子优先级。
分组规则:
抢占优先级位数 = PRIGROUP[2:0]
子优先级位数 = Total_Priority_Bits - 抢占优先级位数
示例:若总优先级位数为 4(即 16 级优先级),设置
PRIGROUP=5
(二进制101),则:
抢占优先级位数 = 3
(高3位为抢占优先级),
子优先级位数 = 1
(低1位为子优先级)。
(2) 抢占规则
- 若新中断的抢占优先级高于当前执行中断的抢占优先级,则发生抢占。
- 子优先级仅在多个中断同时挂起且抢占优先级相同时决定执行顺序。
4. 中断处理流程
(1) 中断触发
- 中断源激活:外设触发中断信号(如定时器溢出)。
- 挂起状态设置:NVIC 将中断标记为挂起(Pending)。
- 优先级裁决:NVIC 比较当前执行任务的优先级与新中断的抢占优先级。
(2) 响应阶段
- 抢占判断:若允许抢占,CPU 暂停当前任务,保存现场(自动压栈:PC, PSR, R0-R3, R12, LR)。
- 向量表跳转:根据中断号从向量表获取 ISR 入口地址。
- ISR 执行:执行用户编写的中断服务程序。
(3) 中断返回
- 尾链优化 (Tail-Chaining):若挂起中断的优先级高于当前 ISR,直接跳转到新 ISR,跳过恢复现场。
- 现场恢复:通过
BX LR
或POP {PC}
指令触发异常返回,硬件自动恢复现场。
5. 中断延迟与优化
(1) 关键延迟指标
- 中断响应时间:从中断触发到 ISR 第一条指令执行的时间(Cortex-M 典型值为 12 周期)。
- 上下文切换时间:现场保存/恢复的周期数(通常 12~20 周期)。
(2) 优化技术
- 尾链优化:避免不必要的现场恢复/保存,减少连续中断的延迟。
- 晚到中断处理 (Late Arrival):高优先级中断在保存现场期间到达时,优先处理高优先级中断。
- 优先级压缩:合理分配优先级,避免过多抢占导致堆栈溢出。
6. 高级功能
(1) 动态优先级修改
- 通过写
NVIC_IPRx
寄存器实时修改中断优先级。 - 应用场景:动态调整任务重要性(如实时任务优先级提升)。
(2) 中断屏蔽
- PRIMASK:全局关闭中断(
CPSID I
)。 - BASEPRI:屏蔽优先级低于某阈值的中断(如
BASEPRI = 0x40
屏蔽优先级 ≥ 4 的中断)。
(3) 软件触发中断
- 通过写
NVIC_STIR
寄存器(Software Trigger Interrupt Register)触发中断。
7. 调试与问题排查
(1) 常见问题
- 优先级配置错误:导致预期外的中断嵌套或阻塞。
- 中断服务程序过长:引发其他高优先级中断的响应延迟。
- 共享资源冲突:未使用临界区保护导致数据竞争。
(2) 调试工具
- CMSIS-Core 函数:如
__get_IPSR()
获取当前中断号。 - 调试器监控:通过 IDE 查看 NVIC 寄存器状态和中断挂起情况。
8. 实际代码示例(基于寄存器操作)
(1) 配置外部中断 EXTI0
// 使能 EXTI0 中断(假设 EXTI0 对应中断号 6)
NVIC_ISER0 |= (1 << 6); // ISER0 对应中断 0-31
// 设置优先级(抢占优先级 2,子优先级 1,优先级位数为 4)
uint8_t priority = (2 << (8 - 4)) | 1; // 假设优先级位数为 4
NVIC_IPR6 = priority << 4; // IPR6 对应中断号 6,优先级值左移 4 位(高4位有效)
// 设置优先级分组(抢占3位,子1位)
SCB->AIRCR = (0x5FA << 16) | (0x5 << 8); // PRIGROUP=5
(2) 临界区保护
#define ENTER_CRITICAL() __asm volatile ("cpsid i")
#define EXIT_CRITICAL() __asm volatile ("cpsie i")
NVIC 是 Cortex-M 实时性的核心保障,其设计在硬件层面实现了:
- 低延迟中断响应:通过向量化跳转和尾链优化。
- 灵活的优先级管理:支持动态调整和分组配置。
- 安全的嵌套机制:硬件自动处理现场保存与恢复。
深入掌握 NVIC 需结合芯片手册(如 STM32 参考手册)和 ARMv7-M 架构文档,重点关注寄存器操作、优先级策略及调试技巧。
定时器
TIM定时器分类:
- 基本定时器:
- 预分频、自动装载值、自定义计数方向、自定义触发事件
- 单个脉冲输出
- 可以产生DMA请求定期搬运数据
- 通用定时器:
- 具有基本定时器的全部功能
- 四个独立的GPIO通道可以进行输入捕获、输出比较、PWM生成、编码模式等
- 高级定时器
- 通用定时器的全部功能
- 八个GPIO通道可以支持反向或互补PWM输出
- 一个GPIO通道可以支持外部输入信号引入刹车重启等功能
CubeMX配置
使能内部时钟
- Prescaler(PSC,预分频):定时器输入频率从时钟树的APB总线获取,但定时器本身可能不需要太高的频率,因此需要对定时器输入频率进行分频,得到定时器的主频
- Counter Mode(计数模式)
- 定时器有个计数器(Counter,CNT),根据其主频没产生一次脉冲,计数器改变1
- Up(向上计数):+1
- Down(向下计数):-1
- Center Aligned model(中心对齐模式,双向计数):既可以+1也可以-1
- Center Aligned model1 (中心对齐模式 1):计数器交替地向上和向下计数,输出比较中断标志位只在计数器 向下计数时被设置。
- Center Aligned model2 (中心对齐模式 2):计数器交替地向上和向下计数,输出比较中断标志位只在计数器向上计数时被设置。
- Center Aligned model3(中心对齐模式 3):计数器交替地向上和向下计数,输出比较中断标志位在计数器向下和向上计数时均被设置。
- Counter Period(Auto Reload Register,ARR,自动装载值):
- 定时器的计数器只能在0到ARR之间变动
- 向上计数,从0到ARR,然后再开启一轮循环
- 向下计数,从ARR到0,然后再开启一轮循环
- 双向计数(中心对齐),从0到ARR-1,然后再从ARR计数到1,然后再开启一轮循环
- 当计数器向上到达ARR或向下到达0时,下一个定时器主频周期会产生溢出中断。
- Internal Clock Division(内部时钟分频)
- 内部时钟分频是指对定时器的输入时钟源进行分频,以得到定时器计数器所使用的时钟信号频率。通过设置不同的分频系数,可以改变定时器的计数速度和时间基准。
- 根据设置的分频系数,将定时器的输入时钟源进行相应的分频。例如,如果设置不分频,则定时器计数器的时钟信号频率等于输入时钟源的频率;如果设置为分频 2,则定时器计数器的时钟信号频率为输入时钟源频率的一半。
- auto-reload preload(自动装载预装载)
- Enable:新的自动重载值不会立即生效,而是先被写入预装载寄存器。此时计数器会继续按照原来旧的重载值进行计数,直到发生更新事件(如计数器溢出)。
- Disable: 如果当前正在计数,写入的新值会立刻成为当前计数周期的重载值,计数器将按照新的重载值重新开始计数。
HAL_TIM_Base_Start_IT开始TIM中断检测
重定义TIM中断虚函数
Prescaler(TIM定时器的预分频)
-
基本概念
- 定义:预分频器是一种用于对输入的时钟信号进行分频的电路或功能模块。在STM32的TIM定时器中,它位于时钟源与计数器之间,通过对时钟源输出的时钟进行分频,得到定时器的计数时钟。例如,如果时钟源的频率为72MHz,通过预分频器设置为0(即不分频),则定时器的计数时钟频率就是72MHz;若设置为71(实际分频系数为72),则定时器的计数时钟频率为1MHz。
- 作用:由于微控制器的系统时钟通常较高,而某些应用场景可能不需要如此高的频率来进行定时操作,预分频器可以将高频的时钟信号降低到合适的频率,以满足不同的定时需求。比如在一些需要较长定时间隔的场景中,通过预分频可以得到较低的定时器时钟频率,从而实现更长时间的定时[1]。
-
工作原理
- 计数与更新:定时器时钟源每来一个脉冲(即tick一次),预分频器的计数值就会加1。当预分频器的计数值达到预设的值(即预分频器的设定值,范围通常是0至65535)时,预分频器会将计数值重置为0,并输出一个脉冲信号给计数器,使计数器的值加1。这个过程不断重复,从而实现了对时钟源的分频。
- 示例:假设预分频器的值为71,那么时钟源需要72个脉冲后,预分频器才会完成一次计数周期并复位,同时计数器的值加1。这意味着定时器的计数时钟频率变为了原来的1/72。
-
相关寄存器
- TIMx_PSC:这是预分频器的寄存器,用来设置预分频器的值。它是一个16位的寄存器,可设置的范围为0至65535,对应的分频系数实际上是1至65536。例如,如果要将定时器时钟频率除以8,就需要将TIMx_PSC的值设置为7。
- 影子寄存器:TIMx_PSC寄存器存在影子寄存器(官方翻译为缓冲功能)。当定时器启动后更改TIMx_PSC的值并不会立即影响当前定时器的时钟频率,要等到下一个更新事件(UEV)发生时才会生效。更新事件由TIMx_CR1寄存器中的UDIS位控制,可通过计数器上溢或手动将TIMx_EGR寄存器中的UG位设置为1来触发。
-
应用场景举例
- 精确定时:在一些需要精确控制时间间隔的应用中,如精确的数据采集、通信协议的实现等,可以通过合理设置预分频器的值来获得所需的定时精度。例如,若要实现每隔1ms产生一次中断,当系统时钟为72MHz时,可将预分频器设置为71,这样定时器的计数时钟频率为1MHz,每次计数1000个周期即为1ms。
- 节能控制:在低功耗应用中,当不需要频繁地进行定时操作时,可以通过增大预分频器的值来降低定时器的时钟频率,从而减少芯片的功耗。例如,在待机状态下,将预分频器设置为较大的值,使定时器的计数时钟频率降低,减少不必要的时钟切换和动态功耗。
Internal Clock Division(内部时钟分频)
-
基本概念
- 定义:在 STM32 的定时器模块中,内部时钟分频是指对定时器的输入时钟源进行分频,以得到定时器计数器所使用的时钟信号频率。通过设置不同的分频系数,可以改变定时器的计数速度和时间基准。
- 相关寄存器:主要涉及到 TIMx_CR1 寄存器中的 CKD 位字段,用于设置定时器时钟(CK_INT)频率与数字滤波器(ETR、TIx)使用的采样时钟之间的分频比。
-
工作原理
- 分频操作:根据设置的分频系数,将定时器的输入时钟源进行相应的分频。例如,如果设置不分频,则定时器计数器的时钟信号频率等于输入时钟源的频率;如果设置为分频 2,则定时器计数器的时钟信号频率为输入时钟源频率的一半。
- 与数字滤波器的关系:在一些应用场景中,如定时器的输入捕获功能,内部时钟分频会影响数字滤波器的采样频率。数字滤波器用于对输入信号进行滤波处理,去除高频干扰等。通过合理设置内部时钟分频,可以调整数字滤波器的采样频率,从而更好地实现对输入信号的准确采集和处理。
-
应用场景举例
- 输入捕获:在测量外部信号的频率、占空比或脉宽等参数时,需要使用定时器的输入捕获功能。通过合理设置内部时钟分频,可以使数字滤波器以合适的采样频率对输入信号进行采样,从而准确地检测到信号的有效电平变化。例如,在测量一个频率较高的脉冲信号时,可以选择较小的分频系数,使数字滤波器的采样频率较高,以便更精确地捕捉到脉冲信号的变化。
- 按键消抖:在检测按键按下和松开的过程中,由于机械结构的原因,会产生抖动现象,即按键信号会出现短暂的不稳定状态。利用定时器的输入捕获功能和内部时钟分频,可以设置合适的采样频率和滤波参数,过滤掉按键抖动产生的干扰信号,从而准确地判断按键的有效按下和松开动作。
Auto Reload Register(自动重装载寄存器)
-
基本概念
- 定义:在STM32等微控制器的定时器中,自动重装载寄存器用于设置定时器的自动重装载值,从而决定定时器的计数周期。它是一个16位的寄存器,里面装着计数器能计数的最大数值。当计数到这个值的时候,如果使能了中断的话,定时器就会产生溢出中断。
- 相关寄存器:在物理上,自动重装载寄存器对应两个寄存器,一个是程序员可以写入或读出的预装载寄存器(preload register),另一个是程序员看不见的、但在操作中真正起作用的影子寄存器(shadow register)。根据TIMx_CR1寄存器中ARPE位的设置,预装载寄存器的内容可以随时传送到影子寄存器,即两者是连通的(permanently),或者在每一次更新事件(UEV)时才把预装载寄存器的内容传送到影子寄存器。
-
工作原理
- 计数模式
- 向上计数模式:定时器从0开始计数,当计数值达到自动重装载寄存器的值时,定时器会产生一个溢出事件,并将计数值重置为0,然后重新开始计数。
- 向下计数模式:定时器从自动重装载寄存器的值开始计数,当计数值减到0时,定时器会产生一个溢出事件,并将计数值重置为自动重装载寄存器的值,然后重新开始计数。
- 更新事件:当计数器的值达到自动重装载寄存器的值时,会产生一个更新事件(UEV)。如果此时TIMx_CR1寄存器中的UDIS位等于0,更新事件会被发送。此外,也可以通过软件主动生成更新事件。
- 计数模式
-
配置方法
- 禁用预装载寄存器:修改自动重装载寄存器的值会直接操作影子寄存器,新的值将立即生效。例如,使用
TIM_ARRPreloadConfig(TIM4, DISABLE);
函数可以禁用TIM4的预装载寄存器。 - 使能预装载寄存器:修改自动重装载寄存器的值会先操作预装载寄存器,直到下一次更新事件后,预装载寄存器的值才会被拷贝到影子寄存器中。例如,使用
TIM_ARRPreloadConfig(TIM4, ENABLE);
函数可以启用TIM4的预装载寄存器。
- 禁用预装载寄存器:修改自动重装载寄存器的值会直接操作影子寄存器,新的值将立即生效。例如,使用
Auto Reload Preload(自动重装载预加载)
-
基本概念
- 定义:在 STM32 的 TIM 定时器中,自动重装载寄存器(TIMx_ARR)用于设置定时器的计数周期。而自动重装载预加载功能则是对该寄存器的一种操作模式控制,它可以决定写入新的自动重载值后何时生效。
- 相关寄存器:主要涉及到 TIMx_CR1 寄存器中的 ARPE 位。当 ARPE 位设置为 0 时,表示禁用自动重装载预加载功能;当 ARPE 位设置为 1 时,表示启用自动重装载预加载功能。
-
工作原理
- 禁用预加载(ARPE=0)
- 当 ARPE 位为 0 时,对 TIMx_ARR 寄存器进行写操作,新的自动重载值会立即生效。也就是说,如果当前正在计数,写入的新值会立刻成为当前计数周期的重载值,计数器将按照新的重载值重新开始计数。
- 例如,假设定时器正在从 0 开始向上计数,当前计数值为 50,此时如果将 TIMx_ARR 的值从 100 修改为 80,并且 ARPE 位为 0,那么计数器会立即停止当前的计数,以 80 作为新的计数上限重新开始计数。
- 启用预加载(ARPE=1)
- 当 ARPE 位为 1 时,对 TIMx_ARR 寄存器进行写操作,新的自动重载值不会立即生效,而是先被写入预装载寄存器。此时计数器会继续按照原来旧的重载值进行计数,直到发生更新事件(如计数器溢出)。
- 当更新事件发生时,预装载寄存器中的新值才会被传送到影子寄存器,从而成为下一个计数周期的重载值。这样可以确保在更新事件产生后再开始新的计数周期,保护了原来的计数周期不受影响。
- 例如,同样在定时器从 0 开始向上计数,当前计数值为 50,将 TIMx_ARR 的值从 100 修改为 80,且 ARPE 位为 1,那么计数器会继续按照 100 进行计数,直到计数到 100 发生溢出事件后,下一个计数周期才会以 80 作为计数上限。
- 禁用预加载(ARPE=0)
-
应用场景
- 多定时器同步输出:在多个定时器同时输出信号的场景中,自动重装载预加载功能非常有用。通过设置相同的更新事件和预加载值,可以确保多个定时器的输出信号在同一个时刻发生变化,实现同步输出。这对于一些需要精确时序控制的应用场景,如通信协议中的时钟同步、多个外设的协同工作等,具有重要意义。
- 避免计数周期干扰:在一些实时性要求较高的应用中,可能需要在定时器运行过程中动态地改变计数周期。如果直接修改 TIMx_ARR 的值可能会导致当前计数周期的混乱,影响系统的正常运行。而启用自动重装载预加载功能后,可以在不影响当前计数周期的情况下,安全地更新计数周期,避免了计数周期的干扰和异常。
日落山水静,为君起松声。 —王勃