type
slug
status
summary
icon
category
date
tags
password

0 程序的运行方式

  • 轮询系统
    • 指的是在程序运行时,首先对所有的硬件进行初始化,然后在主程序中写一个死循环,需要运行的功能按照顺序进行执行,轮询系统是一种简单可靠的方式,一般适用于在只需要按照顺序执行并且没有外部事件的影响的情况下。
    • 程序的运行过程中出现如按键等需要外部检测的事件,轮询系统的实时响应能力变得很差
  • 前后台系统
    • 相比于轮询系统,前后台系统增加中断的概念,如果外部事件发生,则在中断中进行处理主程序在轮询系统中运行。
    • 中断被称为前台,主程序中的while(1)就称为后台。中断会终止后台程序的运行,然后跳转到对应的中断服务函数中去处理,处理完成后,在继续执行后台的程序。
    • 使用前后台系统,可以大大提高程序的实时响应能力,避免造成外部事件的缺失
    • 中断函数的名字必须去汇编文件里面去找(复制粘贴),没有参数和返回值
  • 多任务系统
    • 多任务系统是指一个系统可以“同时”运行多个任务(任务可以理解为程序或线程)。在嵌入式系统中,这通常通过 RTOS(实时操作系统) 来实现,比如 uC/OS、FreeRTOS、RT-Thread 等。这些系统会:
      • 为每个任务分配一定的运行时间(时间片轮转、优先级调度等方式)
      • 管理任务之间的切换(上下文切换)
      • 支持任务间通信和同步(如信号量、消息队列)
    • 相比于前后台系统,多任务系统的外部事件也是在中断中进行响应,但是外部事件的处理是任务中进行处理
      • notion image
    • 任务具有优先级,优先级高的任务先处理,所以程序就会被分为一个个的任务任务是一个独立的死循环,并且不能返回,可以由操作系统进行任务的调度。
    • 程序段的实时响应能力又得到提升。
    • 注意需要创建任务:
  • 与内核相关的资源:misc.c和misc.h

1 中断系统

事件驱动机制→中断
  • 核心思想是由事件触发逻辑执行,而非顺序执行代码。系统通过监听事件(如用户输入、硬件信号、定时器超时等),在事件发生时调用对应的处理函数。其核心特点包括:
    • 非阻塞:主循环持续运行,事件处理通常异步执行。
    • 响应式:系统仅在事件发生时执行相关逻辑。
    • 高效资源利用:避免轮询(Polling),减少CPU空转
notion image
  • 中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序处理完成后返回原来被暂停的位置继续运行
    • 对于外部中断来说:引脚发生了电平跳变,定时器时间到了、串口接收到数据……
    • 这样设计可以使得CPU可以去处理其他事情,而不是一直监测是否是否有中断事件到达
  • 中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。
  • 中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。

2 中断的执行流程

notion image
  • 程序由硬件电路自动跳转到中断服务程序中,执行完中断服务程序之后再返回到主程序继续执行
  • 被暂停的地方/时刻即称为断点。中断执行前,会对程序的现场进行保护;中断执行完后,会对程序的现场进行恢复。
  • 一般中断程序在一个子函数里面,这个函数不需要我们调用,当中断来临时由硬件自动调用这个函数。

3 STM32中断源分析

notion image
  • 几乎每个外设都可以申请中断
  • NVIC统一管理中断,16个可编程优先级,可分组。
  • 灰色部分为内核的中断,白色的部分:STM32外设的中断。当外设电路监测到有什么异常/事件,就可以申请中断,让程序跳到对应的中断函数里面运行一次。
💡
中断源指的是中断发生的源头,中断源在内核中已经定义好了,也称为向量表,向量表在STM32F4 中文参考手册参考。
Cortex-M4 内核一共支持 256 个中断,其中有 16 个内核中断,240 个外部中断,只不过对于STM32F407 系列来说,只用到了一部分,包含了10个内核中断(不可屏蔽中断,无法通过软件进行控制)、82个外部中断(可屏蔽中断,可以通过软件进行控制)。共 92 个。
对于STM32的中断异常而言,分为两类:内核异常 + 外部异常,都可以参考下图进行分析
notion image
  • 中断的地址:由编译器来分配,是不固定的。但中断跳转,由于硬件的限制,就只能跳到固定的地址执行程序。所以为了让硬件跳转到一个不固定地址的中断函数中,定义了一个地址的列表。列表地址固定,中断发生后就跳转到这个列表的首地址。然后由编译器加上一条中断跳转的代码,这样就可以跳转到任意位置。中断地址的列表:中断向量表。
notion image
notion image

3.1 中断处理流程

notion image
  • 中断请求产生
    • 当中断源产生中断事件时,例如外部引脚有信号触发EXTI中断,或者USART收发数据完成产生中断请求,中断源会向NVIC发送相应的中断请求信号。
  • NVIC 处理中断请求
    • NVIC 接收到中断请求后,会根据预先设置的中断优先级等规则对中断进行处理。
      • NMI(Non-Maskable Interrupt,不可屏蔽中断)
      • 系统异常(System Exceptions):HardFault:硬件错误。| MemManage:内存管理异常。| BusFault:总线访问异常。
      • IRQ(Interrupt Request,中断请求)普通可屏蔽中断,它会判断当前处理器是否正在处理优先级更低的中断,如果是,则决定是否进行中断嵌套,即暂停当前中断处理,转而去处理新到来的更高优先级中断。
  • 处理器内核处理 :接收NVIC转发的中断请求,跳转执行相应的中断处理程序。
  • 外部器件恢复:中断服务程序运行完成后,处理器恢复外部设备的状态,继续运行主程序。
 

3.2 中断的三种状态

notion image
  • 低优先级中断处理:如果新的中断优先级更低,那么保存中断请求,将中断置于挂起状态。
  • 清除挂起状态:如果你不想处理某个中断(例如电梯按钮按下,但电梯在运行中,你不想处理这个中断),你可以清除它的挂起位。一旦挂起位被清除,该中断就不再处于挂起状态,直到下次发生请求。
  • 多个请求处理:如果同一个中断源在一个中断处理过程内发生了多次请求,通常系统会只处理一次请求。
    • 假设电梯运行中,开门按钮被按下。此时,中断请求被生成并挂起,但因为电梯正在运行,可能设置了较低的优先级,所以暂时不会处理中断。如果在电梯运行期间多次按下开门按钮,系统通常会把所有请求挂起,但不会重复处理。只有电梯停止后,挂起的请求才会被处理。若此时清除挂起状态,系统可能会再次看到请求并进行处理。
notion image
(中断请求阶段→中断挂起阶段→中断激活阶段→中断清除阶段)
上图中,中断请求X产生,(中断请求的确认引起挂起状态置位),NVIC接收到中断请求后,如果该中断未被挂起且满足响应条件,会将对应的中断挂起状态位置位。进入中断处理使挂起状态被清除且进入中断处理使激活状态置位。处理器开始执行中断服务例程(ISR)时,中断处于激活状态,表示该中断正在被处理。(中断服务程序清除外设的中断请求)在中断服务例程中,通常需要清除外设的中断请求标志位,以避免重复触发中断。
(处理器的状态变化)
同时,处理器模式从线程模式切换到处理器模式,以便执行中断服务程序。压栈和取向量 :处理器会将当前的处理器状态(如寄存器内容)压入栈中,并取中断向量以跳转到中断服务例程的入口地址。中断处理X:执行中断服务例程,处理中断请求。异常返回和出栈:中断处理完成后,执行异常返回操作,处理器会从栈中恢复之前的状态,将处理器模式切回到线程模式,继续执行被中断的主程序。
一些特殊情况:
notion image
  • 多个中断请求脉冲
    • 只处理一个请求,其他请求挂起:处理器通常只会处理优先级最高的那个中断请求。
    • 只处理一次请求,挂起的请求不会立即再次处理:如果中断请求脉冲是来自同一来源的(例如按钮连续按下),系统可能只会处理一次请求,避免重复处理相同的事件。
notion image
上图为挂起状态位被软件清除的情况。
notion image
上图为挂起状态位被软件清除后又被因为中断请求保持活跃,所以挂起状态会被重新确认。
notion image
这一幅图和上面的区别是,在中断挂起状态时,CPU从线程模式切换到处理模式,这个时候CPU执行中断服务程序,同时需要将中断挂起状态清0,(以及需要将中断活跃状态激活。)
notion image

4 NVIC(嵌套向量中断控制器)

4.1 NVIC基本结构

notion image
  • NVIC(Nested Vectored Interrupt Controller)嵌套中断向量控制器:统一分配中断优先级和管理中断(管理中断向量表)
    • 中断向量的方向:指向中断处理函数
  • 一个外设可能占用多个中断通道,即有n条线。NVIC只有一个输出口,根据每个中断的中断优先级分配处理中断的先后顺序,通过这一个输出口,告诉CPU该处理哪个中断。
  • 关于对中断执行顺序的事情,CPU并不知道。

4.2 NVIC优先级分组

notion image
抢占优先级和响应优先级
  • 先比较两个中断的抢占优先级,响应优先级更高(数字越小)的中断先执行
notion image
  • 如果两个中断的抢占优先级一样,那么再比较两个中断的响应优先级,响应优先级高的中断先执行。
notion image
  • 如果两个中断抢占优先级和响应优先级都一样,那么就按照他们在中断向量表中的顺序决定。这在我们并不关心它们的执行顺序下才会发生,正常情况下我们应该给它们设置不同的优先级。
  • 如果A中断已经执行,B中断信号突然到达,STM32会比较两个中断的抢占优先级。如果B中断抢占优先级大于A,那么就像中断打断正常执行流程那样,打断A中断。等到B中断执行完成后,再恢复A中断执行。A中断执行完成后,恢复正常流程的执行。如果B中断抢占优先级小于或者等于A中断抢占优先级,那么无法抢占即等待B中断执行完成后,执行A中断。
notion image
notion image
 

6.2 NVIC补充

  • NVIC指的是嵌套向量中断控制器,属于内核中的外设,作用是管理所有的中断,比如中断的使能或失能、中断的优先级…
    • 不管是 CortexA系列还是 CortexM 系列的内核内部均有 NVIC,通过 NVIC来管理内核异常和外部异常。
notion image
  • 包括内核异常在内的所有中断都由NVIC管理。
  • CortexM4的NVIC最多支持240个中断请求(IRQ),1个NMI,1个Systick和多个系统异常,实际数量根据具体芯片而定。
  • stm32f401支持最多82个中断请求(16个内核异常。64个外部中断IRQ0~64)
    • 内核异常(System Exceptions)16个
      • 这些是 Cortex-M 内核标准定义的,不属于 NVIC 可配置的 IRQ,(异常号从 -15 ~ 0):只有部分可配置优先级(如 SysTick、PendSV、SVCall、UsageFault 等)
    • 外部中断(External IRQ)最多支持81个IRQ编号(最多支持82个中断向量槽位/枚举IRQn_Type 枚举(但不是每个都被用上)
      • 实际可用的IRQ是多达64个外设中断通道

6.2.1 中断的使能和失能

💡
NVIC 管理中断通道的打开与关闭,可以把NVIC理解为所有中断的开关
Q:需要打开时钟吗?
A:这是一个内核的外设,使用时不需要打开时钟。只有芯片那些外设(内核外的外设):GPIO等等才需要打开时钟。
想使用中断发送中断请求,就必须提前打开中断的通道,关于NVIC的使用都存储在一个结构体中。由于NVIC属于内核中的一个外设,ST公司没有将其单独封装成一个.c和.h,但我们有内核文件misc.c和misc.h。这个结构体和NVIC函数的接口都定义在这两个文件之中。
notion image
  • 第一个成员:中断通道
    • notion image
    • 修改HSE的频率为8MHz,对应第二组的修改是传入的是84MHz
    • 下面是中断号的枚举,对应了向量表中的所有中断
      notion image
      一些是公用的,一些是独有的,这里定义的这个宏在。上图是公共部分,下图是定义了宏之后特有的中断
      notion image
      notion image
 
  • 第二个成员:抢占优先级
notion image
notion image
  • 第三个成员:响应优先级
notion image
notion image
 

6.2.2 中断优先级设置

  • NVIC利用 4bit 的优先级来管理所有的中断通道,STM32 中断的优先级分为两种:抢占式优先级(主优先级)+响应式优先级(次优先级),每种都有16个优先级(0~15),数字越小,优先级越高。
  • 意义:如果同时发生多个中断请求,但是又不能同时处理,就根据中断请求的优先级来处理和响应中断。
  • 抢占优先级(主优先级):抢占优先级高的中断可以打断抢占优先级低的中断的执行
  • 响应优先级(次优先级):在同时发生多个中断的情况下,响应优先级高的先执行。
💡
规则 (1)抢占优先级高的中断可以打断抢古优先级低的中断的执行 (2)抢占优先级一样高的中断,响应优先级高的中断不可以打断响应优先级低的中断
(3)抢占优先级一样高的中断,如果同时发生的情况下响应优先级高的先执行
(4)抢占优先级和响应优先级一样高的中断同时发生,则按照向量表中的优先级执行
为了方便用户管理和响应中断,NVIC提供一个函数接口可以对中断优先级进行分组
step1:配置优先级分组
notion image
notion image
  • 函数分析
    • 原型:void NVlC_PriorityGroupConfig(uint32 t NVlC PriorityGroup)
    • 参数一:NVIC_PriorityGroup
      • 打算设置的 NVIC优先级分组一般为 NVIC_PrigrityGroup_2.
    • 返回值:None
  • 注意:设置中断优先级分组应该在主程序运行的开头部分进行,并且不能再随意修改分组,否则会导致程序优先级管理混乱,出现未知问题。
step2:使能优先级通道
notion image
 
 
  • 中断使能/清除(ISER/ICER)
    • ISER:Interrupt set-enable register
    • ICER:Interrupt clear-enable register
  • 中断挂起(ISPR/ICPR)——>标志中断
notion image
 
  • SCB(System control block)中配置中断分组寄存器:AIRCR
notion image
STM32F401手册中,“The processor implements only bits[7:4] of each field, bits[3:0] read as zero and ignore writes.”意思是:Cortex-M4本身支持8位(bits[7:0])中断优先级字段,用于描述中断优先级。但是STM32F401 的实现只使用其中的高 4位(bits[7:4])
  • 产生的效果:STM32F401 中,一个中断优先级用8位寄存器(比如 NVIC_IPRx)表示。但实际上只有高4位有效(比如你设置 0x10,实际中断优先级就是 10x110x12……这些都视为 1)。 这意味着有效的优先级有多少种?就是2⁴ = 16种,即0 ~ 15
  • 由图还可以看出抢占优先级和子优先级都可以有占四位的情况。抢占优先级(Preemption Priority),响应优先级/子优先级(Sub Priority)
  • 注意我们可以发现,填入AIRCR[10:8]位置的正是优先级分组取反的值
notion image
notion image
notion image
这一部分的代码最终会被编译成一个8位数(中断优先级字段),0b 0110 0000,转换成十六进制为0x60(注意!!二到十六最快是四位四位看而不是转换成十进制计算)
  • NVIC_IPRx,一个IPR寄存器32位,存储4个中断的优先级
    • NVIC_IPRx(Interrupt Priority Register):NVIC中断优先级寄存器
    • 在STM32F4系列中,每个中断优先级字段是8位(1Byte)(以及只有高4位有效)。所以一个IPR可以存储四个中断的优先级字段。(也是一个IPR寄存器存储4个中断通道的优先级)
notion image
CMSIS-Core函数
notion image
 
 

5 EXTI外部中断/事件控制器

  • EXTI:External Interrupt,外部中断,触发源来自外部的中断
  • 基本功能:EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序。
  • 支持的触发方式:上升沿/下降沿/双边沿/软件触发支持
    • 上升沿:某个GPIO口读到的电平由低电平变到了高电平
    • 下降沿:某个GPIO口读到的电平由高电平变到了低电平
      • 由按键原理图可知,当按键KEY1按下时,有一个高电平到低电平的变化,所以选择下降沿触发中断。
      • notion image
      • 软件消抖:按键按下/抬起的时候,电平抖动都会出现下降沿。先延时10ms等待中断过去
        • notion image
  • 支持GPIO口:所有GPIO口,但相同的Pin不能同时触发中断(即PA0/PB0…不能同时使用),所以如果有多个中断,那么则需要多个中断引脚(PA6和PA7…)
  • 通道数:16个GPIO Pin(GPIO_Pin0~GPIO_Pin_15)、外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒(一共20个中断通道)
  • 触发响应方式:中断响应/事件响应
    • 这块区域与中断无关,而是与“事件(Event)”相关的结构
      • notion image
    • 当外部中断监测到引脚电平变化的,正常情况是选择触发中断。中断信号会抵达CPU中调用代码进行处理
    • 也可以选择触发事件,那么外部中断的信号并不是通向CPU,而是通向其他外设,来触发其他外设的操作。
 

5.1 EXTI基本结构

notion image
  • 每个GPIO外设有16个引脚,但EXTI只有16个GPIO通道→中断引脚选择模块,选择一共GPIO的16个引脚连接到后面的EXTI通道里。
    • 所以说:所有的GPIO口都可以触发中断,相同的Pin不能同时触发中断。因为PA0\PB0\PC0…通过AFIO后,其中只有一共能接到EXTI通道0上。
  • 然后16个通道接入EXTI边沿监测及控制通道(加上下面四个蹭网的通道),组成了EXTI的20个输入信号。
  • 两种输出:
    • EXTI→NVIC:触发中断。
      • 在所有的外部中断中,只有EXTI0到EXTI4拥有自己的中断向量;而EXTI15到EXTI9共享中断向量(又叫做一个通道)EXTI9_5(即触发同一个中断函数);EXITI10到EXTI15共享中断向量EXTI15_10;
      • 需要再根据标志位来区分是哪个中断
    • EXTI→其他外设:触发其他外设操作

5.1.1 AFIO复用IO口

  • 一些列的数据选择器构成,可以看到可以配置寄存器EXTI0的四位来选择选择哪个引脚来输入EXTIO
  • 在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
notion image
 

5.1.2 EXTI框图

notion image
  • 边沿检测电路
    • 检测输入的电平信号中有没有发生高低电平转换(即有无出现上升沿/下降沿)
    • 然后根据上面两个寄存器的配置,来决定是否向后输出一个高电平信号
    • 选择CubeMx中配置上升沿触发中断/下降沿,实际上是在配置这两个寄存器。(eg:前者寄存器的第12位配置为0,后者第12位配置为1)
      • 如检测到一个下降沿,触发,则向后产生一个高电平信号,此信号经过一个或门后到达请求挂起寄存器(下面描述的1和高电平等价,0和低电平等价)
  • 数字电路逻辑符号
    • (与门:半圆+竖线)(或门:半圆+曲线)(非门:三角形|数据选择器:)(梯形,多个输入一个输出)
    • 或门的特性是两个输入段只要有一个输入为1,则输出为1。与门的特性是两个输入端都是1时,输出才是1。
  • 触发信号和软件中断寄存器信号经过或门之后,两路:一路触发中断;一路触发事件
    • notion image
    • 触发中断:请求挂起寄存器接收到请求时会,将相应位置1。
      • eg:接收到第12根外部中断线来的高电平后,请求挂起寄存器的第12位置1。然后将此位输出到一个与门。根据与门的特性,中断屏蔽器的输出就掌握了此中断是否有效关键。只有中断屏蔽寄存器的相应位为1,输出高电平,请求挂起寄存器的信号才能通过与门,进入NVIC。(中断屏蔽寄存器本质上就是相当于一个中断开关)
    • 当中断信号线12的信号到达NVIC时,NVIC会找到中断向量EXTI15_10,然后按照其指向执行EXTI15_10_IRQHandler函数。
    • NVIC是一直监测某个中断线是否处于激活状态的,所以当EXTI15_10_IRQHandler函数执行结束后,若NVIC还监测到中断线激活,那么就会再次执行EXTI15_10_IRQHandler函数。因此为了让中断处理函数只执行一遍,所以需要在中断处理函数中将请求挂起寄存器的对应位清除为0。调用此函数,即根据引脚清除请求挂起寄存器的对应位。
      • notion image
        notion image
  • 同样的事件屏蔽寄存器也相当于一个开关,脉冲发生器产生脉冲控制外设
notion image
 
  • /20,代表20个通道;上面就是APB总线,然后是外设接口,可以通过这些访问寄存器
  • 核心结构:
notion image
 

6.1 EXTI补充

6.1.1 EXTI框图再探!

notion image
💡
外部中断/事件控制器包含多达23个用于产生事件/中断请求的边沿检测器。
每根输入线都可单独进行配置,以选择类型(中断或事件)和相应的触发事件(上升沿触发、下沿触发或边沿触发)。
每根输入线还可单独屏蔽。
挂起寄存器用于保持中断请求的状态线。
EXTI指的是外部中断/事件控制器,一共有23个,每个都有一个内部的边沿检测器,可以检测上升沿或者下降沿,每根线都可以产生事件或者中断。
上升沿:指的是电平信号由低变高的那一刻 下降沿:指的是电平信号由高变低的那一刻
我们再来复习一下EXTI框图,假设我们使用最简单的外部中断:按键为例,实现及时响应
notion image
💡
  • 产生中断,必须先配置好并使能中断线。根据需要的边沿检测设置2个触发寄存器,同时在中断屏蔽寄存器相应位写“1”使能中断请求。当外部中断线上出现选定信号沿时,便会产生中断请求对应的挂起位也会置1。在挂起寄存器的对应位写“1”,将清除该中断请求。(处理完中断请求后,就得清除中断请求)
  • 产生事件,必须先配置好并使能事件线。根据需要的边沿检测设置2个触发寄存器,同时在事件屏蔽寄存器的相应位写“1”允许事件请求。当事件线上出现选定信号沿时,便会产生事件脉冲,对应的挂起位不会置1。
  • 通过在软件中对软件中断/事件寄存器写“1”,也可以产生中断/事件请求
    notion image
    下面是详细分析:
    当按下按键时,通过电路图可知,从输入线进来一个0.然后根据上面两个寄存器的配置,来决定是否向后输出一个高电平信号
    notion image
    notion image
    经过或门,无论SWIER是什么,都会输出1,然后来到中断屏蔽寄存器
    notion image
    开发中断请求,即输出一个1,和前面的中断信号1,与门,
    notion image
    挂起请求寄存器就是发生就是1,没有发生就是0
    下面是事件屏蔽寄存器
    notion image
     
     

    6.1.2 外部中断/事件线映射

    想产生中断,那么事件屏蔽寄存器写0。
    • 16个外部中断线(23-16=7个外部中断线用于其他作用)
    notion image
    notion image
    • 现在想让我们这个PA0和外部中断线连接在一起,只有16根外部中断线和GPIO口有关系,同一时间只能映射一个引脚,每个端口的第一个引脚都和EXTI0相关联
    notion image
    • 通过寄存器SYSCFG_EXTICR1…进行配置
    notion image
    notion image
    💡
    注意:
    每个GPI0引脚都可以配置为外部中断,但是和GPI0相关的外部中断线一共有 16 根,分别为 EXTI0~EXTI15。STM32F407系列有114个GPIO口,那如何和外部中断线进行关联?通过映射的方式。
    注意:
    这里使用了两个外设一个是SYSCFG(系统配置控制器),SYSCFG_EXTICR1,所以这里需要包含新的文件:stm32f4xx_syscfg.c|stm32f4xx_exti.c
    notion image
    编译之后,将源文件相关联,就可以跳转了
    如果想要让外部中断线和GPIO口进行映射,需要使用一个函数接口
    notion image
    • 参数一:EXTI_PortSourceGPIOx
      • 想要映射的GPIO端口,如EXTI_PortSourceGPIOA
    • 参数二:EXTI_PinSourcex
      • 想要映射的GPIO引脚,如EXTI_PinSource0
    💡
    注意:因为建立映射的时候需要使用系统配置控制器(SYSCFG),所以在编写代码的时候必须打开 SYSCFG 外设的时钟,挂在在APB2外设总线上,调用RCC_APB2PeriphClockCmd();
    notion image
     
    notion image

    6.1.3 外部中断的代码编写

    notion image
    注意:所有GPIO端口都具有外部中断功能。要使用外部中断线,必须将端口配置为输入模式
    想要使用 EXTI 进行外部事件的检测,可以参考 stm32f4xx_exti.c源文件中的开头注释部分
    notion image
    1.应该在工程中添加 stm32f4xx_extic以及 stm32f4xx_syscfg.c 两个源文件
    2.打开 GPIO 外设时钟 +SYSCFG 外设时钟 GPIO挂载AHB1 总线 SYSCFG挂载在 APB2 总线
    3.定义 GPIO 初始化结构体,结构体配置的时候需要把引脚模式配置为输入模式 + 初始化
    4.调用 SYSCFG_EXTILineConfig()函数来对外部中断线和 GPI0 引脚建立映射关系
    5.定义 EXTI初始化结构体,需要配置外部中断线(模式、触发方式.) + 初始化
    💡
    定义结构体+配置+初始化
    notion image
    • EXTI_Line 需要使用的外部中断线 参考EXTI_Lines(跳转/CTRL+F)
      • notion image
    • EXTI_Mode 外部中断线的模式 (中断/事件模式)
      • notion image
    • EXTI_Trigger 边沿检测方式 (上升沿/下降沿/边沿)
      • notion image
    • EXTI_LinCmd 外部中断线使能
    • 将这个结构体指针传入,即可以初始化
    notion image
    • 配置NVIC IRQ中断通道 使用NVIC_Init() 套路完全一样,需要传一个结构体指针,然后给结构体赋值
      • notion image
     
    6.定义 NVIC 的初始化结构体,并进行赋值(中断通道...),+ 初始化
    notion image
    • NVIC_IRQChannel 中断通道 参考stm32f4xx.h
    notion image
    • NVIC_IRQChannelPreemptionPriority 抢占优先级 根据分组来填写
    • NVIC_IRQChannelSubPriority 响应优先级 根据分组来填写
      • notion image
    • NVIC_IRQChannelCmd 中断通道的使能 ENABLE/DISABLE
    7.编写中断服务函数 中断服务函数的名字必须从启动文件中进行拷贝
    注意:中断服务函数的格式是固定的(没有返回值、没有参数) 名字也是固定的
    notion image
    结果发现这两个获取中断标志位函数内容一模一样
    notion image
    也发现这两个清除中断标志位的函数一模一样
    notion image
     

    6 事件驱动参考资料和补充

    notion image
     
     

    6.3 Cortex M4

    内核寄存器:
    notion image
    💡
    这里可以看到和Cortex-M4寄存器布局和EOS课上课程系统讲解的一致,但是这里有一个PSP和MSP来决定SP的取值。同时还要加上CONTROL寄存器来决定使用PSP还是MSP。除此之外CONTROL寄存器需要来决定处理器的工作模式。
    特殊寄存器(中断屏蔽寄存器)
    notion image
    notion image
    notion image
    • primask
      • The PRIMASK register prevents activation of all exceptions with configurable priority。
      • 在它被置1后,就关掉所有可屏蔽的异常,只剩下Reset(-3)、NMI(-2)Hardfault(-1) 可以响应。它的缺省值是0,表示没有屏蔽。
      • 当前优先级改为0(最高可编程优先级)
    • faultmask
      • The FAULTMASK register prevents activation of all exceptions except for Non-Maskable Interrupt (NMI).
      • 当它置1时,只有NMI才能响应,它的缺省值也是 0,表示没有屏蔽。
      • 处理器在退出任何异常处理程序(除了NMI处理程序)时将FAULTMASK位清除为0。
      • The processor clears the FAULTMASK bit to 0 on exit from any exception handler except the NMI handler.
    • basepri(Base priority mask register)
      • 根据前面NVIC优先级分组使用位数,只有[7:4]位可用。
      • 定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低,即优先级比这个阙值更低的优先级将被屏蔽)。但若被设成0,则不关闭任何中断,0也是缺省值。
    • XPSR(Program status register)
      • 分类:APSR/IPSR/EPSR(Application/Interrrupt/Execution)
      • notion image
      • 应用程序程序状态寄存器(APSR)、中断程序状态寄存器(IPSR)和执行程序状态寄存器(EPSR)。三个逻辑子寄存器共享同一个物理寄存器(即xPSR),通过访问 xPSR 的不同位段来获取各自的信息。
      • 在代码中对前面三个寄存器的操作,本质上是在对CPU里面的PSR一个寄存器进行更改。
      • 💡
        为什么要再次划分三个寄存器?
        • 功能分工明确
          • APSR:主要用于记录算术和逻辑运算的结果状态,如N、Z、C、V、Q标志位。
          • IPSR:专门用于记录当前正在执行的中断服务程序编号(ISR_NUMBER)。
          • EPSR:涉及Thumb状态(T位)和IT指令执行状态(ICI/IT位)等,用于控制指令集的切换和特定指令的执行流程。
        • 硬件设计优化
          • 独立更新:将PSR划分为多个子寄存器处理器在执行指令时,只需更新相关的子寄存器。例如,算术运算只影响APSR,而不会涉及 EPSR 或IPSR。这减少了不必要的硬件操作,提高了性能。
          • 并行处理:不同的子寄存器可以由不同的硬件单元并行处理,进一步提升处理器的效率。
        • 增强系统安全性
          • 受限访问:如果直接操作PSR,可能会不小心修改到不需要改变的部分,导致意外的程序行为。而通过分别操作APSR、IPSR和EPSR,可以更精确地控制修改范围,减少对其他部分的干扰。
        • 兼容性和扩展性
          • 这种设计使得 ARM 架构在不同版本(如 Cortex-M0、M3、M4、M7)上可以保持一致的状态管理,而不需要每次修改整个 PSR 结构。
      • 可以使用寄存器名称作为MSR或MRS指令的参数,单独访问这些寄存器或组合访问任意两个或所有三个寄存器。例如:
        • 使用MRS指令使用PSR,读所有寄存器
        • 使用MSR指令向APSRN的Z、C、V和Q位写入数据。
    • CONTROL寄存器
      • Cortex-M4有两种工作模式:Thread模式(线程模式)和Handler(处理模式)
        • 异常和中断会进入Handler模式(执行没有限制),其他情况会进入Thread模式。
        • 特权级别
          • Privileged和Unpriviledged
          • 但其实看说明书上,Thread模式下可以选择Privileged和Unprivileged。Handler模式一直是Privileged。
          • Unpriviledged(非特权级别)只能通过MSR和MRS有限访问,不能使用CPS指令访问系统定时器,NVIC或者系统控制模块等。UCOSII执行期间,只会处于Handler模式下和Thread模式下的特权级别。
      • 当处理器处于线程模式时,CONTROL寄存器控制软件执行所使用的堆栈和特权级别,并指示FPU状态是否为活动。
      • notion image
      • nPRIV(第0位):控制进入特权模式或用户模式(非特权模式)
        • 0:特权模式(默认),可以执行所有指令,访问系统控制寄存器等。
        • 1:用户模式,此时不能执行所有的指令,某些系统控制寄存器不可访问。
      • SPSEL(第1位):该位决定了堆栈指针的选择。
        • 线程模式
          • 0:使用主堆栈指针(MSP)
          • 1:使用进程堆栈指针(PSP)
        • 处理模式:
          • 始终为0,进行写操作会被忽略。
          💡
          也就是说Handler模式下一直使用的是MSP作为SP,在Thread模式下可以选择使用MSP/PSP。Bit0决定Thread是否在特权级里面,Bit1决定Thread模式使用的 是MSP还是PSP。在UCOSII中我们使用Thread模式下PSP作为任务栈SP。
    • Cortex M4内存分布图
      • notion image
         

    6.4 工作模式

    • 操作模式
      • 线程模式(Thread Mode):用于执行主程序代码,即普通的应用程序代码。
      • 处理模式(Handler Mode):用于处理异常和中断服务程序。当发生中断或异常时,处理器会切换到处理者模式来执行相应的处理程序。
    • 特权级别
      • 特权级(Privileged):在此级别下,代码可以访问所有的内存区域和外设寄存器,并能执行所有指令。
      • 用户级(Unprivieged):在此级别下,代码的访问权限受限,某些特定的寄存器和内存区域可能无法访问,尝试访问这些受限资源将导致故障。
    notion image
    • 模式切换
      • 处理模式→线程模式
        • 自动恢复:中断服务程序(ISR)执行完毕后,通过BXLRPOP {PC}返回线程模式。
        • 特权级可配置:返回后线程模式的特权级由EXCRETURN值决定。
          • notion image
      • 线程模式→处理模式
        • 硬件自动切换:当发生异常(如中断、SVC指令)时,CPU自动进入处理模式(特权级)。
    • 权限切换
      • 特权级→非特权级(用户级)
        • 设置CONTROL[0]=1,进入非特权级
      • 非特权级(用户级)→特权级
        • 只能通过触发异常(如SVC)进入处理模式(特权级),在异常处理中修改CONTROL寄存器CONTROL[0]=0,进入特权级。
          • notion image
          • 注意:第一个异常处理后,便回到出现异常前的最后一个状态:非特权线程。是因为在这个异常处理中并没有修改CONTROL[0]。第二个异常中修改了CONTROL寄存器,然后使得处理完异常后进入特权线程。
          • 从处理模式到线程模式的异常返回期间,栈指针的选择可由EXC RETURN(异常返回)数值决定处理器硬件会自动更新第1位的数值。
          • SVC:SVC(Supervisor Call)是一种由软件触发的异常,通常用于实现系统调用。当用户程序需要请求操作系统的服务访问受保护的硬件资源时,会通过执行 SVC 指令来触发 SVC 异常。属于SVC的异常:系统服务调用、任务调度相关、硬件资源访问等
    💡
    MSP\PSP
    • MSPPSP 都是堆栈指针(Stack Pointer)的寄存器
    • MSP主堆栈指针。
      • 通常用于内核模式(特权模式)和异常处理。
      • 默认堆栈指针,所以系统启动的时候,通常使用MSP
    • PSP进程堆栈指针
      • 通常用于用户模式(非特权模式)。
      • 在多任务操作系统中,每个任务通常有自己的堆栈,而 PSP 指向该堆栈。
    • 通过 CONTROL 寄存器的 SPSEL 位来选择使用哪个堆栈指针。
      • 0:使用MSP
      • 1:使用PSP
    notion image
     

    6.6 保存断点

    有操作系统的板子下的中断现场保存,不是存储在中断栈上的,而是保存在SVC任务栈中。
    💡
    工作模式:
    2440:IRQ模式
    32:线程模式和处理模式
    notion image
    notion image
    无论当前线程模式是使用MSP还是PSP,当中断事件产生的时候,硬件自动切换到处理模式,都是使用MSP。最后中断退出后回到之前线程模式使用的的栈中。
    两种上下文切换: 两个LR一样:主动上下文切换,一个代表PC,一个为LR 两个LR不一样:被动的上下文切换,两个LR不一样
    重要的8个寄存器:xPSR,PC,LR,R0~R3,R12

    6.7 中断向量表

    位于地址0x00,SCB→VTOR(Vector Table Offset Register)寄存器重定位到其他地址(如RAM)。
    ISR的注册:
    在代码中定义与向量表条目名称匹配的函数,编译器链接器自动将其地址填入向量表。在启动文件中定义。
     

    6.8 中断返回

    STM32硬件自动设置EXC_RETURN,根据EXC_RETURN确定返回的模式与栈,然后自动完成恢复现场(出栈)
    notion image
    这里这个中文手册有点问题,0xFFFFFFED是返回线程模式的进程栈
    notion image

    6.9 uCOSII

    任务调度→中断可能会触发任务调度→任务重新调度
    • 首先是调度点的理解
      • 在UCOSII中发生任务调度的是红色的五种情况
      • notion image
        1)Systick中断:uC/OS-II 会周期性地调用 Systick中断服务函数 OSTickISR()。该中断服务函数负责更新任务的延时计数器OSTimeTick()。当任务务的延时时间到达时,OSTickISR()会调用任务切换函数OSSched()进行任务切换。它通过根据任务控制块中的延时计数器判断是否需要进行任务切换。
        2)OSTimeDly函数:在任务中,可以调用时间延迟函数OSTimeDly()排起当前任务并设置延时时间。当调用OSTimeDly()时,当前任务被挂起,其任务控制块中的相应信息被保存,然后调用任务切換函数 OSSehed()进行任务切换。OSSched()会选择就绪任务表中优先级最高的任务进行执行。 3)OSIntExit函数:当嵌套中断退出时,uC/OS-II 会在 OSIntExitO函效中执行任务切换。在退出中断时,如果有更高优先级的任务处于就绪状态,任务 切换会发生。 4)OSStart初始化:在实时操作系统启动的时候会调用任务调度函数进行任务切换,选取优先级最高的就绪任务执行 5)任务创建时:当一个任务被创建的时候,如果此时OS是启动的状态,那么就需要调用任务调度函数进行可能的任务切换,选取当前优先级最高的任务执行。
        💡
        调度点:
        OS_Sched()函数分布于操作系统内核各处,上图中的横线上的函数、事件中便会调用OS_Sched(),触发任务切换:
        • 操作系统启动
        • 中断退出可能改变了就绪队列。这里的中断尤其是时钟中断)
        • 新任务创建(改变了就绪队列)
        • 当前任务进入等待状态(当前任务不再占据CPU)
        • 当前任务进入冬眠状态(当前任务不再占据CPU)
        • 当前任务执行结束(当前任务不再占据CPU)
    • 需要让OS知道进入中断和退出中断
      • OSIntEnter() OSIntExit()
      • 在uCOSII下的中断函数一般如下格式:
      • 要是某个中断在运行的时候,不希望被其他中断打断,则其格式可以如下:
    • 中断处理过程中可以进行任务切换吗?
    notion image
    在EOS课上我们知道,systick会进行时间片的减少且优先级很高,从而实现调度。然后到任务A,这个时候时间片轮转发生上下文切换,然后到任务B……正常执行。
    notion image
    上图为如果在中断处理过程中(即执行中断服务程序的时候)进行任务切换,那么OS上下文切换到任务B时会出现报错:即返回线程时仍然有活跃中断(因为这个时候中断还没有执行完)。并且IRQ需要快速执行,这样会导致系统实时性降低和资源冲突。
     
     
    • 因此STM32采取的策略是:触发PendSV异常实现延迟的上下文切换。
    • 知识点补充
    💡
    • SVC(Supervisor Call):是ARM架构中用于实现系统调用的关键机制,其核心作用是从用户模式切换到特权模式,使应用程序能够安全地请求操作系统服务。
    • PendSV(Pendable Service Call,可挂起的服务调用):操作系统内核通过触发PendSV异常(通常是通过软件指令触发,设置标志位)。
    notion image
    notion image
    • PendSV几个关键位:
      • Bit28:PENDSVSET(PendSV设置挂起位)
        • 写:0:无影响;1:将PendSV异常状态设置为挂起
        • 读:0:PendSV异常未处于挂起状态;1:PendSV异常为挂起状态
      • Bit27:PENDSVCLR(PendSV清除挂起位)
        • 只写
        • 写:0无影响,移除PendSV异常状态的挂起
      • Bit26:PENDSTSET(Systick设置挂起位)
    notion image
    • 流程分析:
      • 当前A任务正在运行。如果此时发生中断(一般普通中断不会触发PenSV)或任务主动调用OS服务(如通过SVC指令),可能会导致需要进行任务切换。
        • 外部中断本质上是与具体硬件外设(如定时器、串口)绑定的,它们被 NVIC 控制。它们触发时,不会自动引发PendSV。
        • 但是在中断服务函数(ISR)里,如果你调用了某些操作系统 API,比如 OSSemPost(),导致了任务状态的变化(比如高优先级任务变成就绪态),操作系统内核会判断是否需要任务切换,如果需要,它就会主动触发 PendSV。(总结一句话就是外部中断不会触发PendSV,是否触发PendSV取决于OS的调度判断逻辑)
        • 所以正常的流程是:外设中断发生,CPU进入ISR,ISR中调用操作系统服务(如唤醒一个更高优先级的任务),操作系统判断需要任务切换,内核代码中执行SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk 来触发PendSV异常。
      • PendSV被触发后,当前任务的上下文被保存。操作系统选择下一个任务并恢复它的上下文。(PendSV_Handler中做任务上下文切换)
      • 在执行任务B的时候中断产生,跳转执行ISR。这个时候恰好碰到Systick定时触发,然后执行优先级更高的定时器中断(在OSIntExit检查到了更高优先级的就绪任务A,OS设置PendSV挂起标志位ICSR的PENDSVSET=1,挂起PendSV).
      • 然后继续执行ISR(因为PendSV被设置为最低优先级,所以应该是先执行完ISR之后在去PendSV中执行上下文切换,这样就避免了在中断执行时出现任务切换的error了)。当ISR完成时,在PendSV_Handler中进行任务上下文切换。然后切换到任务A执行。
        • 操作系统的“时钟节拍”由SysTick定时器中断提供,SysTick定时器是一个24位的倒数定时器,通常设置为每1ms触发一次中断。每次中断会进入SysTick_Handler ,调用这三个函数。
        • OSTimeTick():会遍历所有延时状态的任务,将其延时计数器-1。
        • OSIntExit()与PendSV:在Systick中断退出前,OSIntExit() 会检查是否有更高优先级的任务就绪;如果有优先级更高的任务就绪(任务切换需求),OS会触发 PendSV中断(通过设置ICSR寄存器中的PENDSVSET位);PendSV_Handler 会完成任务上下文的保存、调度器选择、以及恢复新任务的上下文。
        • 注意:时间片不是在任务中减少的,同时任务也不是跑满一个时间片后再触发Systick,而是:SysTick固定周期打断系统,然后统一减时间片、减延时、调度任务;所以时间片的计数是由Systick中断驱动的,而不是由任务执行过程驱动的。
        • notion image
     

    6.10 SVC系统调用服务

    使用SVC指令触发SVC异常
    SVC 系统编号
    notion image
    • SVC异常使得从非特权模式切换到特权模式
     

    7 事件驱动验证

    7.1 准备工作

    为了验证STM32的事件驱动机制,我们需要访问一些内核寄存器,这些内核寄存器只能通过汇编访问,使用C语言操作外设寄存器也需要对地址的封装,为了方便,直接添加STM32F401固件库中的以下文件:
    💡
    core_cm4.h 主要是⼀些宏定义,包括操作寄存器的掩码、⼀些类型定义 core_cmFunc.h 定义了Cortex-M4核心寄存器的内联汇编函数,用来访问内核寄存器 stm32f4xx.h 包含各种外设寄存器的结构体定义、地址映射
    禁用FPU(浮点运算单元)
    如果使用FPU,进入中断时还会自动压栈一系列FPU的寄存器,影响测试,所以先禁用FPU。
    魔术棒->Target→Floating Point Hardware选择Not Used,禁用浮点硬件。
    notion image
    notion image

    7.2 注册中断向量表

    在前面的学习中,我们建立了自己的startup.s文件,并用汇编进行了栈堆的初始化,建立了中断向量表,书写了Reset_Handler函数对系统时钟等进行配置最后跳转到main函数。
    但现在中断向量表中除了__initial_sp和Reset_Handler还是空的,要想将自己写的中断服务程序注册到中断向量表,首先需要定义中断向量。 查看手册,发现中断向量表的前一部分是一些与系统相关的重要中断向量,直接从以前建好的工程的startup文件中把这部分复制过来。
    notion image
    中断向量表的其余部分是外设中断向量,以USART1中断为例,在中断向量表中定义USART1_IRQHandler,标志USART1中断服务程序的地址,这样在触发中断时,硬件会根据中断向量表快速找到中断服务程序的位置,并执行中断服务程序来处理中断。由于暂时不需要其他中断,先在向量表中将这些位置空出来。
    中断向量表的定义必须按照顺序。中断向量表的编号是由硬件规定的,当触发中断且该中断没有被屏蔽时,处理器按照向量地址=向量表基地址+中断号*4的中断号索引访问中断向量表,如果没有按照中断号顺序定义中断,程序就会跳转到错误的地址,与中断服务程序的命名无关。比如USART1是37号中断,EXTI0是6号中断,在中断向量表中6号位置定义USART1_IRQHandler,37号位置定义EXTI1_IRQHandler,触发外部中断EXTI0时会执行USART1IRQHandler。显然这样是不对的。
    定义了中断向量表后,需要定义中断服务程序,这样触发中断后会跳转到ISR执行。
    EXPORT加[weak]的操作允许用户自行在外部定义ISR,且外部定义会覆盖这个定义。只有用户没有自己定义ISR时,才会执行这个函数。前面定义在向量表中的系统异常也要这样定义。

    7.2 配置中断

    • 先配置USART1->CR1寄存器开启中断,设置中断触发条件
      • USART_CR1_RXNEIE是一个标志位,用于启用接收中断。RXNE(Receive Not Empty)表示USART接收缓冲区中已经有数据,可以进行读取。开启该中断后,当USART接收到数据时,会触发中断,执行相应的中断服务程序(ISR)。
    • 然后配置SCB->AIRCR寄存器设置优先级分组
      • STM32的中断优先级是通过SCB->AIRCR寄存器来配置的,通常使用位域来设置优先级分组。
      • notion image
    • 再配置NVIC->ISER寄存器使能USART1中断
      • 一旦中断在USART1的CR1寄存器中被启用,就需要通过NVIC->ISER寄存器使能该中断,以便处理器可以响应来自USART1的中断请求。
      • 每一位对应一个中断
        • notion image
    • 配置NVIC->IP寄存器设置中断优先级
    (裸板上的验证见文档)
     
     

    8 uC/OS-II 保存现场与恢复现场

    8.1 ucOSII 中断服务程序的定义

    • 与裸板的区别
    对于中断的保护现场环节,裸机与使用操作系统最主要的区别是,在裸机中,当中断结束时最终必定返回原先的进入中断处,不需要切换上下文,因此进入中断时,只要保存8个必要的寄存器,其余寄存器由编译器根据中断服务程序中中是否使用而自动插入汇编代码保存,就能保证上下文不被破坏;而当运行操作系统时,由于中断结束时可能不返回原先的任务而是调度新任务所以必须将所有的寄存器值都保存在旧任务的栈中,以便再次调用改任务时能够恢复上下文。
    notion image
    根据了解我们知道了在stm32中是使用pendsv进行上下文切换,因为在含有ucos的板子上的中断结束后会进行任务的调度。
    思路:pendsv设置为优先级最低,它一直会挂起直到中断函数结束。一句话总结:STM32不在中断中进行任务切换,而是通过触发PendSV中断,在中断完成后安全的进行上下文切换。
    notion image
    ucOS_II中断服务程序的定义格式如下:
    • 0SIntEnter()函数用于通知操作系统进入了ISR,这个函数中会将一个标志中断嵌套层数的全局变量0SIntNesting加1;
    • 0SIntExit()函数中会将0SIntNesting变量减1,如果0SIntNesting减到0,会调用 0S_SchedNew()检查是否有更高优先级的任务需要调度,如果有,调用0SIntctxSw()进行上下文切换。

    8.2 uc/OS—II从中断中调度新任务流程

    • 对于STM32,从任务中进行上下文切换和从中断中进行上下文切换的实现是一致的。在切换函数中,会触发PendSV异常,NVIC_INT_CTRL是SCB->ICSR寄存器的地址,NVIC_PENDSVSET是触发PendSV的操作数,即把SCB->ICSR控制挂起PendSV异常的位(28位)置1,这是唯一能够触发PendSV异常的方式。
    • 代码分析:
    notion image
    • 以上是SCB->ICSR寄存器和ucos ii中的宏定义。0xE000ED04是ICSR的地址(查手册可以知道),而PENDSVSET一位是在ICSR寄存器的第28位。想在第28为写1即为(1<<28),即0b10…0(28个0,实际占0到27位,这样第28位就为1,所以省流版本就是在第n位写1,就为1<<n)所以转换成十六进制,这个值是0x10000000。虽然他们其实都在SCB模块,
    • PENDSV_PRI这个值是最低优先级(0xFF)。前面说到了在CortexM4里面8位优先级编码但是STM32只用高四位。所以最高优先级为0x00,最低优先级为0xFF。
    • 由于PendSV被设置为最低优先的异常,它会挂起到当前中断函数结束
    • 这时被调用者保存寄存器由编译器自动插入的代码恢复,调用者保存寄存器由硬件恢复,SP切换回旧任务的PSP,然后进入PendSv异常处理程序,硬件再次自动将调用者保存寄存器压入旧任务的PSP,SP随后切换到MSP。
      • 背景小知识
        • 类型
          寄存器
          说明
          调用者保存寄存器
          R0–R3, R12, LR, PC, xPSR
          被函数调用前,调用者需要保存它们;异常发生时,硬件自动保存它们
          被调用者保存寄存器
          R4–R11
          被调用者(即 ISR 或任务)如果使用了,就必须自己保存和恢复
      • 在中断服务函数返回(也就是BX LR指令执行)之前我们要把ISR执行过程中使用的寄存器内容都恢复好,否则会把污染的值带到中断前的任务中去!
      • 调用者保存寄存器(R0~R3, R12, LR, PC, xPSR)→ 由硬件自动保存+恢复
        • 当中断发生时,CPU硬件自动把这些寄存器压栈(称为“自动保存”)。
        • 中断执行完后,CPU自动从栈中恢复这些寄存器(称为“自动恢复”)。
        • 所以这部分你不需要手动编写汇编来保存恢复,硬件已经做了。
      • 被调用者保存寄存器(R4~R11)→ 由编译器生成代码保存+恢复
        • 如果在中断服务函数(ISR)中用到了R4~R11,就必须自己保存。
        • 编译器会在编译 ISR 的时候,自动在函数开头插入 PUSH {R4-R11},在函数结尾插入 POP {R4-R11}
        • 所以程序员也不需要手动写 push/pop,但这是由编译器插入的,不是硬件行为。
      • ISR执行完毕之后,CPU的SP自动会切换到旧任务的PSP。然后CPU开始进入PendSV_Handler。
      • Pend_Handler的执行是任务切换的核心:
        • 背景知识再度复习
        • 💡
          栈有两种类型
          • MSP:异常模式(中断、SVC、PendSV)时默认使用
          • PSP:线程模式(任务)时使用
          • 每个任务用独立的PSP,从而实现每个任务有独立的栈空间。而中断/PendSV 会切到MSP,但我们仍然要保存/切换的是任务的PSP!
     

    8.3 PendSV_Handler 任务切换全过程详解

    (以ARM Cortex-M4和STM32F4为例)
    下面是ucosii中PendSV异常处理程序的实现:

    8.3.0 触发PendSV异常(来自OSIntExit()OSCtxSw()

    8.3.1 关闭中断 & 设置 BASEPRI 阻止更高优先级中断

    在前面我们已经学到了
    basepri(Base priority mask register)
    • 根据前面NVIC优先级分组使用位数,只有[7:4]位可用。
    • 定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低,即优先级比这个阙值更低的优先级将被屏蔽,优先级比这个阙值更高的才会被响应)。但若被设成0,则不关闭任何中断,0也是缺省值。
    背景知识:中断屏蔽机制在Cortex-M中有两种方式
    方式
    指令
    控制内容
    全局关中断
    CPSID I / CPSIE I
    设置 PRIMASK=1,屏蔽所有可屏蔽中断(不包括 NMI 和 HardFault)
    优先级屏蔽
    MSR BASEPRI, #level
    设定优先级阈值,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低,即优先级比这个阙值更低的优先级将被屏蔽,优先级比这个阙值更高的才会被响应)。
    • CPSID I
      • 效果:设置 PRIMASK = 1,所有可屏蔽中断都被禁止。
      • 目的:任务切换中不可被打断。
    • MOV32 R2, OS_KA_BASEPRI_Boundary
      • 将一个地址常量 OS_KA_BASEPRI_Boundary加载进R2。OS_KA_BASEPRI_Boundary是什么?它通常是一个变量地址,存放着我们希望设置的BASEPRI值,比如:uint32_t OS_KA_BASEPRI_Boundary = 0x40; // 仅屏蔽优先级 <= 0x40 的中断
      • 但是也可以直接写
    • 后面的就是设置BASEPRI
      • 再详细讲一下BASEPRI:
        • 优先级数值低于设定阈值的中断被触发(即优先级更高的中断将被触发),优先级数值大于等于设定的阙值的中断会被屏蔽(即优先级更低的中断将被屏蔽)
        • 编程时需要左移四位:比如设计屏蔽优先级为4,那么要设定BASEPRI = 0x40
        • notion image
    • DSBISB
      • DSB(Data Synchronization Barrier):确保所有前面的存储操作(如设置BASEPRI已完成写入,再继续执行后续指令。
      • ISB(Instruction Synchronization Barrier):清除指令流水线,确保新设置的 BASEPRI 被 CPU 立即识别,避免指令乱序执行。
      • 如果没有这两条指令,有可能下一条指令运行时 BASEPRI 还未生效。
    • CPSIE I
      • 重新打开中断,即PRIMASK = 0
      • 这个时候屏蔽仍然生效
     

    8.3.2 PendSV_Handler 开始执行:保存旧任务的上下文

    • 当前还是旧任务的上下文,我们需要先保存旧任务的上下文。
    • 在进入PendSV_Handler时,CPU硬件会自动用MSP(中断模式下的SP)压入以下“调用者保存寄存器”到当前PSP(旧任务的栈)中:R0~R3,R12,LR,PC(被中断任务的下一条指令地址(即返回地址)),XPSR(程序状态寄存器)
    • 手动保存旧任务上下文(被调用者寄存器R4-R11 + LR)到旧任务栈(PSP)
    • 一些思考:
      • 这里的两个LR:
        • 任务主动放弃CPU,通过OS_Sched(),此时LR保存的是任务代码的返回地址,两个LR相同。
        • 任务切换被中断触发,被动的任务切换,第一个硬件自动保存的LR是原来上下文的LR,EXC_RETURN。第二个LR保存的是也异常返回模式(EXC_RETURN),用于正确退出中断。
        • notion image
      • 当中断发生时,硬件会自动将 LR 设置为一个特殊值(如 0xFFFFFFF9),称为 EXC_RETURN
      • EXC_RETURN的高28位固定为 0xFFFFFFF,低 4 位表示返回行为:
        • 是否使用 PSP 或 MSP 恢复堆栈。
        • 是否返回线程模式或处理器模式。
          • notion image
    • 之前学到的LR根据所查找的资料是这样的(存疑)
      • 在 ARM Cortex-M 处理器中,原任务上下文的 LR(函数返回地址)是否被保存,取决于任务被中断时是否处于函数调用中
      • 任务未处于函数调用中:此时 LR 可能未被使用,或存储了无关的值(例如任务代码直接操作了LR)。无需保存:因为硬件自动保存的PC直接指向被中断任务的下一条指令地址。即使 LR 未被保存,任务的恢复也仅依赖PC,与LR无关。
      • 任务处于函数调用中:任务正在执行一个函数(例如 FunctionA()),并通过 BL FunctionA 指令调用该函数。在 BL 指令执行后,LR 被自动设置为函数返回地址(即 FunctionA() 执行完毕后应返回的地址)。根据ARM函数调用规范(AAPCS),被调用函数(FunctionA())需在入口处保存LR到栈中。中断发生时:即使 LR 被硬件覆盖为 EXC_RETURN,原任务的返回地址已安全保存在任务栈中,不会丢失

      8.3.3 保存旧任务的栈顶指针PSP到OSTCBCur->OSTCBStkPtr

      注意:R1存放的是OSTCBCur指针(即旧任务TCB的地址),[R1]将R0的内容写入这个地址,这个地址的第一个成员是OSTCBStkPtr。
      到这里旧任务所有的上下文都被保存了。
      回顾一下:第一步先关闭所有中断,设置BASEPRI提高实时性。第二步进行任务切换即PendSV_Handler的执行,进入这个函数时,CPU硬件自动保存调用者保存的8个关键寄存器,R0~R3,R12,LR,PC,XPSR。然后需要手动保存剩下的寄存器,这个时候需要首先切换到当前任务栈的指针PSP,使用R0暂存,然后存入R4~R11,R14。这里使用的是R14,虽然本质上也是LR,但是这里强调的是存储的是EXC_RETURN,和前面的R4~R11,R14看起来更有规范性。(但其实写LR和R14都可以)
       

      8.3.4 保存LR到R4中,执行任务切换 Hook(可选)

      • 因为后续通过BL指令调用OSTaskSwHook,这里会自动覆盖掉LR(EXC_RETURN),所以需要提前保存LR(EXC_RETURN)到R4
      • BL:返回地址存入LR
      • 钩子函数:若任务使用FPU(浮点单元),在此保存/恢复FPU寄存器(如S0-S15)。
       

      8.3.5 更新当前优先级 & 当前任务控制块指针

      复习一下这些全局变量的含义:
      变量名
      含义
      OSPrioCur
      当前正在运行的任务的优先级
      OSPrioHighRdy
      当前就绪任务中优先级最高的那个任务的优先级
      OSTCBCur
      指向当前运行任务的 TCB(任务控制块)指针
      OSTCBHighRdy
      指向就绪任务中优先级最高的任务的 TCB 指针
      • 部分代码分析:
        • step6:R2存放的是当前就绪队列中优先级最高任务的地址
        • step7:R5 = OSTCBCur,R5存放的是OSTCBCur的地址,将R2的内容写入OSTCBCur即让OSTCBCur指向当前就绪队列中优先级最高的任务。
        • 此时R2的内容是当前就绪队列中优先级最高任务的地址
      • 这两句C语言很关键:
        • ; OSPrioCur = OSPrioHighRdy
        • ; OSTCBCur = OSTCBHighRdy
      • 注意在ucOS中优先级是以8位无符号数存储的,所以操作的时候只用操作8位(单个字节即可),这样可以节省空间和效率。LDRB,STRB(只读/写低8位,高位清0)
       

      8.3.6 准备返回新任务,切换SP和恢复新任务上下文

      • 强制设置 EXC_RETURN 的位 2 为 1,确保中断返回时使用进程栈指针(PSP)
        • notion image
      notion image
      重要的是bit[2],bit[2]为0,使用MSP;bit[2]为1时,使用PSP
      • R4 中此时保存的是一个 旧任务中的 EXC_RETURN 值(从前面保存现场时记录下来的)。
      • ORR 是按位“或”操作:把 R4 的值和 0x04 做OR运算,结果写入LR
      • 0x04 二进制为 0000 0100,只影响bit[2]。
      • 所以这一句的实际效果是:强制将 EXC_RETURN 的 bit[2] 设为 1,从而让返回时使用 PSP。
      • 此时R2的内容是当前就绪队列中优先级最高任务的地址,将这个地址上的第一个成员: OSTCBHighRdy->OSTCBStkPtr(新任务的栈指针) 加载到R0
      • 从新任务栈中恢复 R4-R11 和 LR(EXC_RETURN)
      • 更新后的栈指针R0,写入PSP,新任务将使用自己栈的空间执行代码

      8.3.7 恢复BASEPRI为0,PendSV_Handler返回

      • 将BASEPRI清0,即允许所有中断响应。CPSID I :禁用全局中断,将PRIMASK设置为1.但修改BASEPRI时,需要关闭全部中断,确保操作的原子性
      • DSB:确保所有内存访问指令(如 MSR BASEPRI)在此指令之前完成。
      • ISB:刷新指令流水线,确保执行的是最新状态。
      • CPSIE I :启用全局中断(将PRIMASK恢复为0)。关键点:此操作在 DSB 和 ISB 之后执行,确保 BASEPRI 的修改已完全生效。
      • 异常返回:这个时候是总的PendSV执行完成,返回。LR的值是EXC_RETURN,低四位决定返回值。我们需要返回的是线程,使用PSP作为指针。可以推测出EXC_RETURN的值为0xFFFFFFFD。
      • 同时的硬件行为:
        • 前面我们手动弹出了R4~R11,R14(新任务的上下文)
        • 硬件监测到EXC_RETURN的值跳转到Thread模式的PSP
        • 硬件从PSP自动弹出的是8个调用者寄存器(R0~R3,R12,PC,LR(EXC_RETURN),XPSR)
        • 跳转PC到指向的地址(新任务的代码的位置)
      💡
      • 同时我们回顾一下前面的流程:
        • 中断前:
          • 当前任务的R0~R3,R12,LR,PC,XPSR被硬件自动保存
          • 若任务中使用了 R4~R11,则在 PendSV 中手动保存这些寄存器;
        • PendSV中:
          • OS 选择新任务
          • 手动从新任务 PSP 恢复 R4~R11;
          • 设置 LR = EXC_RETURN (0xFFFFFFFD)
        • 异常返回:
          • 硬件检测到 LR = 0xFFFFFFFD,进入 Thread 模式 + 使用 PSP;
          • 从 PSP 弹出 R0~R3, R12, LR, PC, xPSR;
          • 跳转到新任务的 PC,继续执行任务代码
       

      8.4 总结

      • 在PendSV中,会使用MRS获取PSP的值,手动将被调用者寄存器压在旧任务PSP中,随后将SP切换为新任务的PSP,将上一次保存现场的被调用者保存寄存器出栈,这样就完成了任务栈的切换和新任务被调用者保存寄存器的恢复。
      • PendSV异常退出时,调用者保存寄存器自动出栈,由于现在的SP是PSP,会从新任务的PSP中出栈,这样就恢复了新任务的调用者保护寄存器。在整个过程中,有些是由编译器完成的,有些是由硬件自动完成的,有些是由操作系统软件完成的,可以归纳为一张表:
      notion image
       

      9 OS_CPU_PendSVHandler

      os_cpu.c
       
      • 为什么是MOV32 R2, OS_KA_BASEPRI_Boundary 或者MOVW和MOVT,而不是LDR R2, =OS_KA_BASEPRI_Boundary
        • 小小了解一下就可以了:LDR R2, =OS_KA_BASEPRI_Boundary,实际上实际上编译器会在当前代码附近的某个位置(literal pool)放一个常量地址值,然后用 PC 相对寻址方式去加载它。等价于LDR R2, [PC, #offset_to_pool] 在中断服务函数(如 PendSV_Handler)等对时间、空间、跳转极度敏感的代码中,不能确保常量池位置可靠。依赖 PC对链接方式、位置敏感,可能导致不可预测行为。
        • MOVW/MOVT 的优势
          • 直接将32位地址硬编码进寄存器。不依赖PC,不依赖literal pool,位置无关性好。
          • 优势:不访问内存,位置无关,执行确定、速度快,适合中断上下文、裸函数等场合。
          • MOVWMOV Wide,可以将一个16位常数装入寄存器的低16位。
          • MOVTMOV Top,可以将一个16位常数装入寄存器的高16位。
            • 执行后:
        • 第二种写法:MOV32 R2, OS_KA_BASEPRI_Boundary
          • 这是汇编器伪指令,不是硬件原生指令。
          • 编译器会自动将它转换成:
            • 好处是语法简单,自动处理高低位拼接;
            • 缺点是:某些编译器(如 KEIL、IAR)可能不支持伪指令,或者需要特别设置。
        • 这种写法是汇编器提供的语法糖,显式取某个符号地址的低16位和高16位,从而将一个32位地址或常量分两步装入一个寄存器中。
          • #:lower16:符号→取符号值的低16位
          • #:upper16:符号→取符号值的高16位
        • 后续清零的时候直接清0,MOV即可
        • OS_KA_BASEPRI_Boundary
        • 关于LDR/MRS/MSR/STR/MOV的使用
          • LDR:从内存加载数据到寄存器
            • 语法和例子
          • STR:把寄存器的数据存入内存
            • 语法和例子
          • MSR:向系统寄存器写入数据
            • 把通用寄存器的值写入一个特殊系统寄存器(例如 BASEPRI, CONTROL, PSP, MSP 等)。
            • 语法和例子
          • MRS:从系统寄存器读取数据
            • 读取系统寄存器的值到通用寄存器中。
            • 语法和例子
          • MOV:赋值寄存器
            • 使用MOV是因为它快、不访问栈、适合中断/裸函数环境
            • 语法和例子
         
         
        参考代码:
         

        10 NVIC.c NVIC.h

        NVIC.c
        NVIC.h
         
         
         
         
         
         
         
        2-4 启动ARM STM322-3 ARM STM32 时间驱动机制
        Loading...
        🐟🐟
        🐟🐟
        在坚冰还盖着北海的时候,我看到了怒放的梅花
        最新发布
        2-6-1 uC/OSII 在STM32上的移植
        2025-6-2
        Chapter2:Kernel of ERTOS
        2025-5-31
        2-2 ARM STM32 事件驱动机制
        2025-5-31
        2-5 任务上下文切换
        2025-5-31
        2-7 Systemview与任务级调试
        2025-5-31
        2-3 ARM STM32 时间驱动机制
        2025-5-29
        公告
        🎉NotionNext 3.15已上线🎉
        -- 感谢您的支持 ---
        👏欢迎更新体验👏