1 前言
系统上电后,PC会指向复位向量,即向量表中的Reset_Handler,而系统就是通过Vector Table Offset Register (VTOR)的值加上4字节来找到复位向量的入口的。
因为地址 0 处应该存储引导代码,所以它通常映射到 Flash 或者是 ROM 器件,并且它们的值不得在运行时改变。然而,为了支持动态重分发中断, CM3 允许向量表重定位——从其它地址处开始定位各异常向量。这些地址对应的区域可以是代码区,但更多是在 RAM 区。在 RAM 区就可以修改向量的入口地址了。为了实现这个功能, NVIC 中有一个寄存器,称为“向量表偏移量寄存器”(在地址 0xE000_ED08 处),通过修改它的值就能重定位向量表。 ---引自《Cortex-M3权威指南》
简单来说,系统上电后默认中断向量表在地址0,通过VTOR将向量表重定向到RAM中,就可以对其进行操作了,可以在运行时更改其中的中断服务函数。 在Cortex-M7中,支持重定向的偏移地址范围为0x00000000-0xFFFFFF80;此外,如果重定位后的向量表位域可缓存(cacheable)区域,则在使用STR或LDR指令时需要用DSB,ISB等同步隔离指令来清洗流水线。
2 中断向量表偏移寄存器(VTOR)
Vector Table Offset Register,VTOR的值表示中断向量表相对于地址0x00000000的偏移量。该寄存器在特权级下可读写,其地址为0xE000ED08,复位后为不确定值。
图1 Vector Table Offset Register
如图1所示,该寄存器的bit0~bit6为预留位,这些bit默认为0,bit7~bit31记录了偏移量。在设置向量表偏移地址时,必须按照向量表中的异常数制定对齐规则,即将向量表的向量个数向上圆整成2的k次方幂,然后计算处所需要的大小,并按该大小进行字节对齐。例如当有16个系统中断和21个外部中断时,总计37个异常,将向量表的向量个数向上圆整成2的6次方幂64,每个向量大小为4字节,所以向量表的偏移值必须按4*64 = 256 = 0x100字节进行对齐。
低7位默认为0,也就意味着偏移量至少是按128 = 32*4字节对齐的,这样意味着CPU至少需要支持32个异常(包括16个系统异常和16个外部中断)。
至于为什么这么做呢?应该是与中断向量的硬件机制有关。有一种猜测,是该机制取了对齐地址的全0位的个数作为向量表的大小(向量个数),以便于取向量时使用或运算,而不是加法运算。
3 VTOR的相关示例
3.1 VTOR的初始化
在系统上电后,通常需要对VTOR进行初始化。例如,可以在选择在SystemInit函数关中断后,随即进行VTOR的初始化,而这个函数在Reset_Handler中调用。
/*----------------------------------------------------------------------------
System initialization function
*----------------------------------------------------------------------------*/
void SystemInit (void)
{
__disable_irq(); // disable interrupts
#if defined (__VTOR_PRESENT) && (__VTOR_PRESENT == 1U)
SCB->VTOR = (uint32_t) &__VECTOR_TABLE; // init the VTOR
#endif
#if defined (__FPU_USED) && (__FPU_USED == 1U)
SCB->CPACR |= ((3U << 10U*2U) | /* enable CP10 Full Access */
(3U << 11U*2U) ); /* enable CP11 Full Access */
#endif
#ifdef UNALIGNED_SUPPORT_DISABLE
SCB->CCR |= SCB_CCR_UNALIGN_TRP_Msk;
#endif
// MPU CONFIG and ENABLE
#if defined (__MPU_PRESENT) && (__MPU_PRESENT == 1U)
MPUInit();
#endif
// CACHE ENABLE
#if defined (__ICACHE_PRESENT) && (__ICACHE_PRESENT == 1U)
SCB_InvalidateICache();
SCB_EnableICache();
#endif
#if defined (__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U)
SCB_InvalidateDCache();
// SCB_EnableDCache();
#endif
SystemCoreClockUpdate();
__enable_irq(); // enable interrupts
}
3.2 VTOR的访问
VTOR初始化完成后,软件就可以在特权级模式下访问该寄存器,从而找到中断向量表的位置。 例如,freeRTOS的函数prvPortStartFirstTask中,就是通过地址0xE000ED08找到向量表首地址,然后读取向量表的第一个向量的值(MSP的栈顶地址,起始地址),然后将MSP复位为该初始值(主堆栈的栈顶地址):
/*-----------------------------------------------------------*/
static void prvPortStartFirstTask( void )
{
/* Start the first task. This also clears the bit that indicates the FPU is
* in use in case the FPU was used before the scheduler was started - which
* would otherwise result in the unnecessary leaving of space in the SVC stack
* for lazy saving of FPU registers. */
__asm volatile (
" ldr r0, =0xE000ED08 \n"/* Use the NVIC offset register to locate the stack. */
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n"/* Set the msp back to the start of the stack. */
" mov r0, #0 \n"/* Clear the bit that indicates the FPU is in use, see comment above. */
" msr control, r0 \n"
" cpsie i \n"/* Globally enable interrupts. */
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n"/* System call to start first task. */
" nop \n"
" .ltorg \n"
);
}
/*-----------------------------------------------------------*/
cmsis中也提供了VTOR相关的标准接口,从中实现也可以清晰的看到中断向量表的实现就是一个一维数组:
/**
\brief Set Interrupt Vector
\details Sets an interrupt vector in SRAM based interrupt vector table.
The interrupt number can be positive to specify a device specific interrupt,
or negative to specify a processor exception.
VTOR must been relocated to SRAM before.
\param [in] IRQn Interrupt number
\param [in] vector Address of interrupt handler function
*/
__STATIC_INLINE void __NVIC_SetVector(IRQn_Type IRQn, uint32_t vector)
{
uint32_t vectors = (uint32_t )SCB->VTOR;
(* (int *) (vectors + ((int32_t)IRQn + NVIC_USER_IRQ_OFFSET) * 4)) = vector;
__DSB();
}
/**
\brief Get Interrupt Vector
\details Reads an interrupt vector from interrupt vector table.
The interrupt number can be positive to specify a device specific interrupt,
or negative to specify a processor exception.
\param [in] IRQn Interrupt number.
\return Address of interrupt handler function
*/
__STATIC_INLINE uint32_t __NVIC_GetVector(IRQn_Type IRQn)
{
uint32_t vectors = (uint32_t )SCB->VTOR;
return (uint32_t)(* (int *) (vectors + ((int32_t)IRQn + NVIC_USER_IRQ_OFFSET) * 4));
}