引子
还记得在给WS2812B写驱动时,最开始采用的方式是使用DMA向定时器传输数据以满足WS2812B单极性归零码的传输要求,但是实际运行时却发现第一个灯珠的颜色出现了异常,当时真的耽搁了很久,最后也没有找出来是为什么,现在我想就很可能是在配置DMA时发生了中断引起了许多不可控的因素。
中断概述
简单来说,中断及其外围部件包含了中断向量表(用于储存中断的入口地址),NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器,用于控制中断使能、优先级等),如果是由外部触发的话还包括EXIT(Extended interrupt and event controller,外部中断事件控制器,用于外部触发中断,这是以STM32为例,其他厂商单片机可能并不是这样设计)。
中断向量表
中断向量表在对应的位置存放中断函数的地址(32位),因此中断向量表其实就相当于一个32位的数组,在启动文件进入主函数之前就需要将中断向量表初始化,并且中断向量表是从0x00000000开始存放的:
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
MSP
其中在0x00000000
处第一个存放的不是任何中断入口,而是__initial_sp
(MSP,即SP寄存器的初始值),这个标号在之前定义栈(stack)时出现,代表着栈顶,即栈结束的地址(栈由高向低生长):
; Stack Configuration
; Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
Stack_Size EQU 0x00002000
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
复位中断
第二个地址是复位中断(软或硬)的入口函数,这个复位中断函数在启动文件中已经有定义(仅为若定义,可以再他处重新定义),这个函数就做了两件事,一件是初始化时钟等(SystemInit
),并进入主函数(__main
):
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
而实际在任何一次的启动过程中都是先将第一个地址装入SP寄存器,然后执行第二个地址的函数(实际将该地址装入PC寄存器),从而实现系统启动。需要注意的是,这里虽然叫复位中断但是实际表现并不像普通的中断,而更类似于普通运行程序,具体可以看硬汉嵌入式论坛 – 【不是问题的问题】为什么复位中断服务程序里面直接调用的main函数,难道所有程序都在复位中断里面执行的?。
不可屏蔽中断NMI
随后是NMI(不可屏蔽中断)是由于时钟或者Flash错误等引起的。
Fault中断
HardFault
HardFault是所有类型的错误,这个错误通常是由于内存溢出引起的,一般修改栈的大小即可,也可能是指针越界,未对齐等问题引发。硬 fault 状态寄存器(HFSR),地址:0xE000ED2C
。
MemManage
MemManage是储存器错误引发中断,一般因为某次访问触犯了 MPU 设置的保护规范。另外,某些非法访问,例如,在不可执行的存储器区域试图取指,也会触发一个MemManage fault,存储器管理 fault 状态寄存器(MFSR),地址:0xE000ED28
。
BusFault
BusFault是总线错误中断,例如预取指令失败、访问储存器失败。总线 fault 状态寄存器(BFSR),地址:0xE000ED29
。
UsageFault
UsageFault一般是由于使用未定义的指令或非法状态引起,用法 fault 状态寄存器(UFSR),地址:0xE000ED2A
。
其他系统中断
剩下的是SVC(SWI调用的系统系统服务),DebugMon(调试监控器),PendSV(可挂起的系统服务),SysTick(系统滴答定时器,用于提供一个基本定时器),剩下的部分就都属于外设中断了,总结如下:
NVIC
NVIC是内核的一个外设,通常通常并不会完整实现,NVIC主要功能是使能或禁用中断,悬起与解悬,设置中断优先级。在发生中断时会优先执行高优先级的中断程序,若优先级相同则比较硬件中断编号,编号小的优先执行。并且在中断过程中不能被同级或更低优先级的中断打断,此时新产生的中断被悬起。
EXIT
EXTI管理中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。它不仅可以产生中断,也可以产生事件(比如DMA传输,ADC采样)。中断/事件线一般包含外部引脚、RTC的功能、以太网、USB的唤醒等。一般需要先连接到EXIT,再配置对应的EXIT。
MDK中Fault标志位查看
MDK提供了一个方便的方法来查看Fault的各个标志位,使用方法为在调试时打开菜单栏的Peripherals -> Core Peripherals -> Fault Reports。
各标志位的意义也可以在ARM的手册中找到。
中断屏蔽
__disable_irq()
与__enable_irq()
或__set_PRIMASK(1)
与__set_PRIMASK(0)
(cmsis_armcc.h
)可以控制禁用中断和使能中断,在执行临界操作时需要加上防止因为中断导致配置错误等。
/**
\brief Set Priority Mask
\details Assigns the given value to the Priority Mask Register.
\param [in] priMask Priority Mask
*/
__STATIC_INLINE void __set_PRIMASK(uint32_t priMask)
{
register uint32_t __regPriMask __ASM("primask");
__regPriMask = (priMask);
}
实际上是操控中断屏蔽特殊寄存器PRIMASK来实现的,PRIMASK用于禁止除复位、NMI和HardFalut外的所有异常和中断。实际也可以使用CPS指令:
CPSIE i ;清除PRIMASK(使能中断)
CPSID i ;设置PRIMASK(禁止中断)