ARM Cortex-M架构的芯片的中断向量表(Interrupt Vector Table)前16位的中断由ARM核设定。16位以后的中断为芯片厂商自行定义。ARM Cortex-M架构芯片一般带有片上闪存(flash)。ARM Cortex-M手册规定在片上闪存起始地址处需要有一个有效的中断向量表。芯片上电或复位后首先从中向量表中读出入口函数地址和栈指针。将入口函数地址和栈指针装载入寻址寄存器(PC)和栈指针(SP)寄存器后,开始执行程序。
中断向量表每一位为一个32bit的地址。每一个地址对应一个中断函数的地址(第一位除外)。除了第一位以外,所有地址的目标都为寻址寄存器(PC)。当相应中断触发时,ARM Cortex-M硬件会自动把中断向量表中相应的中断函数地址装载入寻址寄存器(PC)然后开始执行中断函数。如上所述,前16位为ARM保留的系统中断,建议读者熟记。之后的中断为芯片自定义的外部中断,可以在使用时查询手册或者厂商提供的驱动程序。
Exception Number | IRQ Number | 中断 | 注释 |
---|---|---|---|
NA | NA | SP | 初始栈指针 |
1 | NA | Reset | 复位函数地址 |
2 | -14 | NMI | 不可屏蔽中断 |
3 | -13 | Hard fault | |
4 | -12 | Memory fault (Reserved in CM0/0+) | 内存管理错误中断 |
5 | -11 | Bus fault (Reserved in CM0/0+) | 总线错误中断 |
6 | -10 | Usage fault (Reserved in CM0/0+) | 使用错误中断 |
7 | -9 | Reserved | 保留位(未使用) |
8 | -8 | Reserved | 保留位(未使用) |
9 | -7 | Reserved | 保留位(未使用) |
10 | -6 | Reserved | 保留位(未使用) |
11 | -5 | SVC | 通常用于请求privileged模式,或者在OS中用于请求系统资源 |
12 | -4 | Reserved | 保留位(调试用) |
13 | -3 | Reserved | 保留位(未使用) |
14 | -2 | PendSV | 通常用于在OS中切换任务 |
15 | -1 | SysTick | 系统节拍时钟中断 |
中断向量表可以通过汇编语言定义也可以通过C语言定义。以下列出两种方式的示例程序。
这里我们定义了一个数组,数组的每一项对应相应的中断函数。如上所述,数组的第一项为初始栈指针,第二项为入口函数地址。余下的所有中断都指向了一个通用中断函数。开发者可以根据需求替代相应的中断函数。
上文还提到中断向量表需要放置于闪存起始地址处。这里__attribute__((section(".vectors")))
为gcc的特定语法(如果开发者使用IAR或者其他编译器,语法有所不同),目的是告诉编译器在链接所有对象文件(objects)时把_vector[]
数组放在链接脚本(linker script)中的.vectors
段落(section
)。在链接脚本中,.vector
段落被定义于闪存起始处。
#ifndef ARMV7M_PERIPHERAL_INTERRUPTS
# error ARMV7M_PERIPHERAL_INTERRUPTS must be defined to the number of I/O interrupts to be supported
#endif
extern void exception_common(void);
unsigned _vectors[] __attribute__((section(".vectors"))) =
{
/* Initial stack */
IDLE_STACK,
/* Reset exception handler */
(unsigned)&__start,
/* Vectors 2 - n point directly at the generic handler */
[2 ... (15 + ARMV7M_PERIPHERAL_INTERRUPTS)] = (unsigned)&exception_common
};
这里以STM32Cube生成的startup_stm32f401retx.S
为示例。同样的,汇编程序中也定义了默认中断函数。所有中断也都指向了默认的中断函数Default_Handler
(默认为无限循环)。
.section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler
之后是中断向量表。这里为了缩减示例代码长度,略去了中间的中断函数定义。注意在初始处 .section .isr_vector,"a",%progbits
语句指定了g_pfnVectors
在链接是需要被放置在isr_vector
段落,也就是闪存起始地址处。
/******************************************************************************
*
* The minimal vector table for a Cortex M3. Note that the proper constructs
* must be placed on this to ensure that it ends up at physical address
* 0x0000.0000.
*
*******************************************************************************/
.section .isr_vector,"a",%progbits
.type g_pfnVectors, %object
.size g_pfnVectors, .-g_pfnVectors
g_pfnVectors:
.word _estack
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
.word MemManage_Handler
.word BusFault_Handler
.word UsageFault_Handler
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler
.word DebugMon_Handler
.word 0
.word PendSV_Handler
.word SysTick_Handler
/* External Interrupts */
.word WWDG_IRQHandler /* Window WatchDog */
.word PVD_IRQHandler /* PVD through EXTI Line detection */
.word TAMP_STAMP_IRQHandler /* Tamper and TimeStamps through the EXTI line */
.word RTC_WKUP_IRQHandler /* RTC Wakeup through the EXTI line */
.word FLASH_IRQHandler /* FLASH */
.word RCC_IRQHandler /* RCC */
.word EXTI0_IRQHandler /* EXTI Line0 */
... /* 省略 */
.word FPU_IRQHandler /* FPU */
.word 0 /* Reserved */
.word 0 /* Reserved */
.word SPI4_IRQHandler /* SPI4 */
在中断向量表的定义之后,程序还将所有函数定义为.weak
。也就是说如果开发者在其他地方重新定义了同样名称的中断函数,那么默认的中断函数实现会被自动覆盖。weak
是GNU GCC编译器定义的关键词,如果采用其他编译器会有对应的关键词。
/*******************************************************************************
*
* Provide weak aliases for each Exception handler to the Default_Handler.
* As they are weak aliases, any function with the same name will override
* this definition.
*
*******************************************************************************/
.weak NMI_Handler
.thumb_set NMI_Handler,Default_Handler
.weak HardFault_Handler
.thumb_set HardFault_Handler,Default_Handler
.weak MemManage_Handler
.thumb_set MemManage_Handler,Default_Handler
.weak BusFault_Handler
.thumb_set BusFault_Handler,Default_Handler
.weak UsageFault_Handler
.thumb_set UsageFault_Handler,Default_Handler
.weak SVC_Handler
.thumb_set SVC_Handler,Default_Handler
.weak DebugMon_Handler
.thumb_set DebugMon_Handler,Default_Handler
... /* 省略 */
.weak FPU_IRQHandler
.thumb_set FPU_IRQHandler,Default_Handler
.weak SPI4_IRQHandler
.thumb_set SPI4_IRQHandler,Default_Handler
ARM Cortex-M默认的中断向量表地址位于闪存起始地址处。但是ARM Cortex-M3/4系列提供了一个中断向量表偏移寄存器(Vector Table Offset Reigster)。系统中中断向量表的位置是0x00000000加上偏移寄存器的值。上电复位后这个寄存器值为0,所以中断向量表默认位于0x00000000闪存起始处。这个寄存器的目的是为了让开发者可以重新设置中断向量表的位置。
中断向量表偏移寄存器最普遍的用法是在系统上电复位后将中断向量表指向内存RAM地址空间。如上所述,系统上电复位后默认中断向量表位于闪存起始地址处。闪存的缺点是在系统运行时不能方便的修改任意字节。大部风芯片闪存采取的是NOR Flash。修改特定字节需要先将整个Bank的数据复制入内存RAM,擦除Bank中所有数据后,将RAM中特定字节更新后将整个Bank重新写入闪存。一般NOR Flash Bank的大小为512bytes到2Kbytes。这里就会存在很大的数据丢失或者损坏的风险。所以如果开发者希望在系统运行中更新闪存中的中断向量表中中断函数地址会很不方便而且风险很大。这时可以在系统复位后将闪存中的中断向量表复制进内存地址空间。然后利用中断向量表偏移寄存器将中断向量表指向位于内存中的中断向量表。这以后开发者就可以在程序运行过程中随时更新中断向量表中中断函数地址。
在移植实时操作系统RTOS时,RTOS常常会在系统初始化时将中断向量表指向内存,然后将除ARM预留系统中断外,所有中断函数都指向统一的操作系统中断函数。中断触发后先调用操作系统中断函数,然后再调用开发者定义的中断函数。
/****************************************************************************
* Name: up_ramvec_initialize
*
* Description:
* Copy vectors to RAM an configure the NVIC to use the RAM vectors.
*
****************************************************************************/
void up_ramvec_initialize(void)
{
const up_vector_t *src;
up_vector_t *dest;
int i;
/* The vector table must be aligned */
DEBUGASSERT(((uint32_t)g_ram_vectors & ~NVIC_VECTAB_TBLOFF_MASK) == 0);
/* Copy the ROM vector table at address zero to RAM vector table.
*
* This must be done BEFORE the MPU is enable if the MPU is being used to
* protect against NULL pointer references.
*/
src = (const CODE up_vector_t *)getreg32(NVIC_VECTAB);
dest = g_ram_vectors;
irqinfo("src=%p dest=%p\n", src, dest);
for (i = 0; i < ARMV7M_VECTAB_SIZE; i++)
{
*dest++ = *src++;
}
/* Now configure the NVIC to use the new vector table. */
putreg32((uint32_t)g_ram_vectors, NVIC_VECTAB);
/* The number bits required to align the RAM vector table seem to vary
* from part-to-part. The following assertion will catch the case where
* the table alignment is insufficient.
*/
irqinfo("NVIC_VECTAB=%08x\n", getreg32(NVIC_VECTAB));
DEBUGASSERT(getreg32(NVIC_VECTAB) == (uint32_t)g_ram_vectors);
}
这样的优势是操作系统可以在调用用户中断函数前后进行个性化操作。比如中断函数本身并没有参数,如果操作系统调用用户中断函数,其就可以传递用户参数。以下为从开源RTOS Nuttx
中摘录并简化的一个操作系统中断函数。中断触发后,系统用中断号搜索由操作系统创建的中断函数列表。如果用户在列表中注册了自定义的中断函数并且设置的中断函数参数,则系统会调用中断函数并传递用户预设的参数。如果列表中对应中断函数为空,系统会执行默认中断函数。
void irq_dispatch(int irq, FAR void *context)
{
xcpt_t vector = irq_unexpected_isr;
void *arg = NULL;
unsigned int ndx = irq;
#if NR_IRQS > 0
if ((unsigned)irq < NR_IRQS)
{
if (g_irqvector[ndx].handler)
{
vector = g_irqvector[ndx].handler;
arg = g_irqvector[ndx].arg;
}
INCR_COUNT(ndx);
}
#endif
/* Then dispatch to the interrupt handler */
CALL_VECTOR(ndx, vector, irq, context, arg);
UNUSED(ndx);
/* Record the new "running" task. g_running_tasks[] is only used by
* assertion logic for reporting crashes.
*/
g_running_tasks[this_cpu()] = this_task();
}