LV040-CortexM3的NVIC中断控制
一、NVIC简介
Cortex-M3内核有一个专门管理中断的外设NVIC( Nested Vectored Interrupt Controller,嵌套向量中断控制器) , 通过优先级控制中断的嵌套和调度。NVIC是一个总的中断控制器, 无论是来在内核的异常还是外设的外部中断, 都由NVIC统一进行管理。 是 Cortex‐M3 不可分离的一部分, 它与 CM3 内核的逻辑紧密耦合, 有一部分甚至水乳交融在一起。
NVIC 与 CM3 内核同声相应,同气相求,相辅相成,里应外合,共同完成对中断的响应。 NVIC作为在内核里的外设,NVIC 的寄存器以存储器映射的方式来访问, 除了包含控制寄存器和中断处理的控制逻辑之外, NVIC 还包含了 MPU的控制寄存器、 SysTick 定时器以及调试控制。
NVIC 共支持 1 至 240 个外部中断输入(通常外部中断写作 IRQs)。 具体的数值由芯片厂商在设计芯片时决定。此外, NVIC 还支持一个“永垂不朽”的不可屏蔽中断(NMI)输入。NMI 的实际功能亦由芯片制造商决定。在某些情况下, NMI 无法由外部中断源控制。NVIC 的访问地址是 0xE000_E000。 所有NVIC 的中断控制/状态寄存器都只能在特权级下访问。 不过有一个例外——软件触发中断寄存器可以在用户级下访问以产生软件中断。
所有的中断控制/状态寄存器均可按字/半字/字节的方式访问。 此外, 有几个中断屏蔽寄存器也与中断控制密切相关,它们是“特殊功能寄存器”, 只能通过 MRS/MSR及 CPS 来访问。
二、中断配置
1. 中断配置基础
每个外部中断都在 NVIC 的下列寄存器中“挂号”:
(1)使能与除能寄存器
(2)悬起与“解悬”寄存器
(3)优先级寄存器
(4)活动状态寄存器
另外,下列寄存器也对中断处理有重大影响
(1)异常掩蔽寄存器(PRIMASK, FAULTMASK 以及 BASEPRI)
(2)向量表偏移量寄存器
(3)软件触发中断寄存器
(4)优先级分组位段
2. 中断的使能与除能
中断的使能与除能分别使用各自的寄存器来控制——这与传统的, 使用单一比特的两个状态来表达使能与除能是不同的。 CM3 中可以有 240 对使能位/除能位,每个中断拥有一对。这 240 个对子分布在 8 对 32 位寄存器中(最后一对没有用完)。欲使能一个中断,需要写 1 到对应 SETENA 的位中;欲除能一个中断,需要写 1 到对应的 CLRENA 位中;如果往它们中写 0,不会有任何效果。通过这种方式,使能/除能中断时只需把“当事位”写成1,其它的位可以全部为零。再也不用像以前那样,害怕有些位被写入 0 而破坏其对应的中断设置(因为现在写 0 没有效果),从而实现每个中断都可以自顾地设置,而互不侵犯——只需单一的写指令,不再需要读‐改‐写。
如上所述, SETENA 位和 CLRENA 位可以有 240 对,对应的 32 位寄存器可以有 8 对,因此使用数字后缀来区分这些寄存器,如 SETENA0, SETENA1…SETENA7,如下表所示(SETENAs: xE000_E100 – 0xE000_E11C ; CLRENAs:0xE000E180 - 0xE000_E19C )。
| 名称 | 类型 | 地址 | 复位值 | 描述 |
|---|---|---|---|---|
| SETENA0 | R/W | 0xE000_E100 | 0 | 中断 0‐31 的使能寄存器,共 32 个使能位 位[n],中断#n 使能(异常号 16+n) |
| SETENA1 | R/W | 0xE000_E104 | 0 | 中断 32‐63 的使能寄存器,共 32 个使能位 |
| … | … | … | … | … |
| SETENA7 | R/W | 0xE000_E11C | 0 | 中断 224‐239 的使能寄存器,共 16 个使能位 |
| CLRENA0 | R/W | 0xE000_E180 | 0 | 中断 0‐31 的除能寄存器,共 32 个除能位 位[n],中断#n 除能(异常号 16+n) |
| CLRENA1 | R/W | 0xE000_E184 | 0 | 中断 32‐63 的除能寄存器,共 32 个除能位 |
| … | … | … | … | … |
| CLRENA7 | R/W | 0xE000_E19C | 0 | 中断 224‐239 的除能寄存器,共 16 个除能位 |
但是在特定的芯片中,只有该芯片实现的中断,其对应的位才有意义。因此,如果使用的芯片支持 32 个中断,则只有 SETENA0/CLRENA0 才需要使用。SETENA/CLRENA 可以按字/半字/字节的方式来访问。又因为前 16 个异常已经分配给系统异常,故而中断 0 的异常号是 16。
3. 中断的悬起与解悬
如果中断发生时,正在处理同级或高优先级异常,或者被掩蔽,则中断不能立即得到响应。此时中断被悬起。中断的悬起状态可以通过“中断设置悬起寄存器(SETPEND)”和“中断悬起清除寄存器(CLRPEND)”来读取,还可以写它们来手工悬起中断。
悬起寄存器和“解悬”寄存器也可以有 8 对,其用法和用量都与前面介绍的使能/除能寄存器完全相同, 如下表(SETPENDs:0xE000_E200 – 0xE000_E21C ; CLRPENDs:0xE000E280 - 0xE000_E29C ):
| 名称 | 类型 | 地址 | 复位值 | 描述 |
|---|---|---|---|---|
| SETPEND0 | R/W | 0xE000_E200 | 0 | 中断 0‐31 的悬起寄存器,共 32 个悬起位 位[n],中断#n 悬起(异常号 16+n) |
| SETPEND1 | R/W | 0xE000_E204 | 0 | 中断 32‐63 的悬起寄存器,共 32 个悬起位 |
| … | … | … | … | … |
| SETPEND7 | R/W | 0xE000_E21C | 0 | 中断 224‐239 的悬起寄存器,共 16 个悬起位 |
| CLRPEND0 | R/W | 0xE000_E280 | 0 | 中断 0‐31 的解悬寄存器,共 32 个解悬位 位[n],中断#n 解悬(异常号 16+n) |
| CLRPEND1 | R/W | 0xE000_E284 | 0 | 中断 32‐63 的解悬寄存器,共 32 个解悬位 |
| … | … | … | … | … |
| CLRPEND7 | R/W | 0xE000_E29C | 0 | 中断 224‐239 的解悬寄存器,共 16 个解悬位 |
4. 优先级
4.1 优先级位数设置
通过应用中断和复位控制寄存器( Application Interrupt and Reset Control Register, AIRCR) 的Bits[10:8]( PRIGROUP)将优先级分组。分组决定每个可编程中断的PRI_n的Bits[7:0]的高低位分配,从而影响抢占优先和亚优先级的级数
| PRIGROUP | 抢占优先级位 | 亚优先级位 | 抢占优先级级数 | 亚优先级级数 | |
|---|---|---|---|---|---|
| 0 | [7:1] | [0] | 128 | 2 | |
| 1 | [7:2] | [1:0] | 64 | 4 | |
| 2 | [7:3] | [2:0] | 32 | 8 | |
| 3 | [7:4] | [3:0] | 16 | 16 | |
| 4 | [7:5] | [4:0] | 8 | 32 | |
| 5 | [7:6] | [5:0] | 4 | 64 | |
| 6 | [7] | [6:0] | 2 | 128 | |
| 7 | None | [7:0] | 1 | 256 |
假设将优先级分组( PRIGROUP)设置为2,此时每个中断可设置抢占优先级范围为0~32,亚优先级范围为0~8, 比如某中断的抢占优先级为2, 亚优先级为3。
4.2 优先级下的执行顺序
所有可编程的中断都需要指定抢占优先级和亚优先级, 抢占优先级决定是否可以产生中断嵌套,亚优先级决定中断响应顺序,若两种优先级一样则看中断在中断异常表中的位置,越靠前越先响应。抢占优先级高(值小) 的中断可以中断抢占优先级低(值大) 的中断处理函数,进而执行高优先级的中断处理函数,执行完毕后再继续执行被中断的低优先级的处理函数。
当两个中断的抢占优先级相同时,即这两个中断将没有嵌套关系,当一个中断到来后,若此时CPU正在处理另一个中断,则这个后到来的中断就要等到前一个中断处理函数处理完毕后才能被处理,当这两个中断同时到达,则中断控制器会根据它们的亚优先级决定先处理哪个。
如果两个中断的优先级都设置为一样了,那么谁先触发的就谁先执行;如果是同时触发的,那么就根据中断异常表的位置(靠前) 来决定谁先执行。
4.3 外部中断优先级控制
每个外部中断都有一个对应的优先级寄存器,每个寄存器占用 8 位,但是允许最少只使用最高 3 位。 4 个相临的优先级寄存器拼成一个 32 位寄存器。如前所述,根据优先级组设置,优先级可以被分为高低两个位段,分别是抢占优先级和亚优先级。优先级寄存器都可以按字节访问,当然也可以按半字/字来访问。有意义的优先级寄存器数目由芯片厂商实现的中断数目决定,优先级配置相关寄存器如下表:
- 中断优先级寄存器阵列 (0xE000_E400 – 0xE000_E4EF)
| 名称 | 类型 | 地址 | 复位值 | 描述 |
|---|---|---|---|---|
| PRI_0 | R/W | 0xE000_E400 | 0(8 位) | 外中断#0 的优先级 |
| PRI_1 | R/W | 0xE000_E401 | 0(8 位) | 外中断#1 的优先级 |
| … | … | … | … | … |
| PRI_239 | R/W | 0xE000_E4EF | 0(8 位) | 外中断#239 的优先级 |
- 系统异常优先级寄存器阵列 (0xE000_ED18 - 0xE000_ED23 )
| 地址 | 名称 | 类型 | 复位值 | 描述 |
|---|---|---|---|---|
| 0xE000_ED18 | PRI_4 | 存储器管理 fault 的优先级 | ||
| 0xE000_ED19 | PRI_5 | 总线 fault 的优先级 | ||
| 0xE000_ED1A | PRI_6 | 用法 fault 的优先级 | ||
| 0xE000_ED1B | ‐ | ‐ | ‐ | ‐ |
| 0xE000_ED1C | ‐ | ‐ | ‐ | ‐ |
| 0xE000_ED1D | ‐ | ‐ | ‐ | ‐ |
| 0xE000_ED1E | ‐ | ‐ | ‐ | ‐ |
| 0xE000_ED1F | PRI_11 | SVC 优先级 | ||
| 0xE000_ED20 | PRI_12 | 调试监视器的优先级 | ||
| 0xE000_ED21 | ‐ | ‐ | ‐ | ‐ |
| 0xE000_ED22 | PRI_14 | PendSV 的优先级 | ||
| 0xE000_ED23 | PRI_15 | SysTick 的优先级 |
三、活动状态
每个外部中断都有一个活动状态位。在处理器执行了其 ISR 的第一条指令后,它的活动位就被置 1,并且直到 ISR 返回时才硬件清零。由于支持嵌套,允许高优先级异常抢占某个ISR。然而,哪怕一个中断被抢占,其活动状态也依然为 1(请仔细琢磨前文讲到的“直到 ISR返回时才清零)。活动状态寄存器的定义,与前面讲的使能/除能和悬起/解悬寄存器相同,只是不再成对出现。它们也能按字/半字/字节访问,但他们是只读的,如下表。
- ACTIVE 寄存器族 (0xE000_E300_0xE000_E31C )
| 名称 | 类型 | 地址 | 复位值 | 描述 |
|---|---|---|---|---|
| ACTIVE0 | RO | 0xE000_E300 | 0 | 中断 0‐31 的活动状态寄存器,共 32 个状态位 位[n],中断#n 活动状态(异常号 16+n) |
| ACTIVE1 | RO | 0xE000_E304 | 0 | 中断 32‐63 的活动状态寄存器,共 32 个状态位 |
| … | … | … | … | … |
| ACTIVE7 | RO | 0xE000_E31C | 0 | 中断 224‐239 的活动状态寄存器,共 16 个状态 位 |
四、其他寄存器
后边还有一些其他的寄存器,这里就不写了,后边用到的话可以再查手册。
五、中断建立全过程的演示
下面给出一个简单的例子,以演示如何建立一个外部中断 :
(1)当系统启动后,先设置优先级组寄存器。缺省情况下使用组0(7位抢占优先级, 1位亚优先级)。
(2)如果需要重定位向量表,先把硬fault和NMI服务例程的入口地址写到新表项所在的地址中。
(3)配置向量表偏移量寄存器,使之指向新的向量表(如果有重定位的话)。
(4)为该中断建立中断向量。因为向量表可能已经重定位了,保险起见需要先读取向量表偏移量寄存器的值,再根据该中断在表中的位置,计算出服务例程入口地址应写入的表项,再填写之。如果一直使用ROM中的向量表,则无需此步骤。
(5)为该中断设置优先级。
(6)使能该中断
另外,如果优先级组设置使得中断嵌套层次可以很深,则务必请确认主堆栈空间足够用。因为异常服务程序总是使用MSP,为安全起见,主堆栈的容量应是最大可能需求的量(嵌套最深时需要的量)。如果应用程序储存在ROM中,并且不需要改变异常服务程序,则我们可以把整个向量表编码到ROM的起始区域(从0地址开始的那段)。在这种情况下,向量表的偏移量将一直为0,并且中断向量一直在ROM中,因此上例可以大大简化,只需3步:
(1)建立优先级组
(2)为该中断指定优先级
(3)使能该中断
如果在I/O密集型系统中,软件需要控制大量的硬件设备,则可能必须要考虑如下因素:
该芯片支持的中断数
该芯片中表达优先级的位数
在CM3的NVIC中,有一个名为“中断控制器类型寄存器”,它提供了该芯片中支持的中断数目,粒度是32的整数倍,(如下表所示)。如果嫌它太粗枝大叶,也可以通过对每个SETENA位进行先写后读的测试,来获取支持的中断的精确数目(往各SETENA中写1,不支持的中断将永远读回0,求出第1个0的位置即可),亦可使用SETPEND等其它位来做此测试。这主要用于需要适应不同芯片的程序。如果已经确定使用固定的芯片,则无需多此一举。
| 位段 | 名称 | 类型 | 复位值 | 描述 |
|---|---|---|---|---|
| 4:0 | INTLINESUM | R | ‐ | 中断输入的数量,以 32 为粒度,如 0=1 至 32;1=33 至 64;2=65 至 96 … |
为了判定正在使用的芯片使用了多少位来表达优先级,也可使用类似的方法:往某个优先级寄存器中写入0xFF,再读回来。则从MSB开始,有多少位是1就有多少位表达优先级。最少要使用3个位,此时读回的是0xE0。
六、软件中断
软件中断,包括手工产生的普通中断,能以多种方式产生。最简单的就是使用相应的SETPEND寄存器;而更专业更快捷的作法,则是通过使用软件触发中断寄存器STIR,如下表。
- 软件触发中断寄存器STIR(地址:0xE000_EF00)
| 位段 | 名称 | 类型 | 复位值 | 描述 |
|---|---|---|---|---|
| 8:0 | INTID | W | ‐ | 影响编号为 INTID 的外部中断,其悬起位被置位。 例如,写入 8,则悬起 IRQ #8 |
注意:系统异常(NMI, faults, PendSV等),不能用此法悬起。而且缺省时就不允许用户程序改动NVIC寄存器的值。如果确实需要,必须先在NVIC的配置和控制寄存器(0xE000_ED14)中,把比特1(USERSETMPEND)置位,才能允许用户级下访问NVIC的STIR。