细心的小伙伴肯定能发现,前几篇文章中频繁出现一个名词 —— 向量表。这到底是个什么东西?
今天我们就来深入了解一下向量表以及向量表的重定位。
当 Cortex-M 处理器接受了某个异常请求后,处理器需要确定该异常处理(若为中断则是 ISR)的起始地址。而这个信息就存储在存储器的向量表中。
向量表默认从地址 0 开始,向量地址则为异常编号乘 4 ,如下图所示:
向量表一般被定义在微控制器供应商提供的启动代码中,以 STM32F4 为例,在其 startup_stm32f407xx.s 中有如下一段代码:
__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
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
DCD RTC_WKUP_IRQHandler ; RTC Wakeup through the EXTI line
DCD FLASH_IRQHandler ; FLASH
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line0
DCD EXTI1_IRQHandler ; EXTI Line1
DCD EXTI2_IRQHandler ; EXTI Line2
DCD EXTI3_IRQHandler ; EXTI Line3
DCD EXTI4_IRQHandler ; EXTI Line4
DCD DMA1_Stream0_IRQHandler ; DMA1 Stream 0
DCD DMA1_Stream1_IRQHandler ; DMA1 Stream 1
DCD DMA1_Stream2_IRQHandler ; DMA1 Stream 2
DCD DMA1_Stream3_IRQHandler ; DMA1 Stream 3
DCD DMA1_Stream4_IRQHandler ; DMA1 Stream 4
DCD DMA1_Stream5_IRQHandler ; DMA1 Stream 5
DCD DMA1_Stream6_IRQHandler ; DMA1 Stream 6
DCD ADC_IRQHandler ; ADC1, ADC2 and ADC3s
DCD CAN1_TX_IRQHandler ; CAN1 TX
DCD CAN1_RX0_IRQHandler ; CAN1 RX0
DCD CAN1_RX1_IRQHandler ; CAN1 RX1
DCD CAN1_SCE_IRQHandler ; CAN1 SCE
DCD EXTI9_5_IRQHandler ; External Line[9:5]s
DCD TIM1_BRK_TIM9_IRQHandler ; TIM1 Break and TIM9
DCD TIM1_UP_TIM10_IRQHandler ; TIM1 Update and TIM10
DCD TIM1_TRG_COM_TIM11_IRQHandler ; TIM1 Trigger and Commutation and TIM11
DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
DCD TIM2_IRQHandler ; TIM2
DCD TIM3_IRQHandler ; TIM3
DCD TIM4_IRQHandler ; TIM4
DCD I2C1_EV_IRQHandler ; I2C1 Event
DCD I2C1_ER_IRQHandler ; I2C1 Error
DCD I2C2_EV_IRQHandler ; I2C2 Event
DCD I2C2_ER_IRQHandler ; I2C2 Error
DCD SPI1_IRQHandler ; SPI1
DCD SPI2_IRQHandler ; SPI2
DCD USART1_IRQHandler ; USART1
DCD USART2_IRQHandler ; USART2
DCD USART3_IRQHandler ; USART3
DCD EXTI15_10_IRQHandler ; External Line[15:10]s
DCD RTC_Alarm_IRQHandler ; RTC Alarm (A and B) through EXTI Line
DCD OTG_FS_WKUP_IRQHandler ; USB OTG FS Wakeup through EXTI line
DCD TIM8_BRK_TIM12_IRQHandler ; TIM8 Break and TIM12
DCD TIM8_UP_TIM13_IRQHandler ; TIM8 Update and TIM13
DCD TIM8_TRG_COM_TIM14_IRQHandler ; TIM8 Trigger and Commutation and TIM14
DCD TIM8_CC_IRQHandler ; TIM8 Capture Compare
DCD DMA1_Stream7_IRQHandler ; DMA1 Stream7
DCD FMC_IRQHandler ; FMC
DCD SDIO_IRQHandler ; SDIO
DCD TIM5_IRQHandler ; TIM5
DCD SPI3_IRQHandler ; SPI3
DCD UART4_IRQHandler ; UART4
DCD UART5_IRQHandler ; UART5
DCD TIM6_DAC_IRQHandler ; TIM6 and DAC1&2 underrun errors
DCD TIM7_IRQHandler ; TIM7
DCD DMA2_Stream0_IRQHandler ; DMA2 Stream 0
DCD DMA2_Stream1_IRQHandler ; DMA2 Stream 1
DCD DMA2_Stream2_IRQHandler ; DMA2 Stream 2
DCD DMA2_Stream3_IRQHandler ; DMA2 Stream 3
DCD DMA2_Stream4_IRQHandler ; DMA2 Stream 4
DCD ETH_IRQHandler ; Ethernet
DCD ETH_WKUP_IRQHandler ; Ethernet Wakeup through EXTI line
DCD CAN2_TX_IRQHandler ; CAN2 TX
DCD CAN2_RX0_IRQHandler ; CAN2 RX0
DCD CAN2_RX1_IRQHandler ; CAN2 RX1
DCD CAN2_SCE_IRQHandler ; CAN2 SCE
DCD OTG_FS_IRQHandler ; USB OTG FS
DCD DMA2_Stream5_IRQHandler ; DMA2 Stream 5
DCD DMA2_Stream6_IRQHandler ; DMA2 Stream 6
DCD DMA2_Stream7_IRQHandler ; DMA2 Stream 7
DCD USART6_IRQHandler ; USART6
DCD I2C3_EV_IRQHandler ; I2C3 event
DCD I2C3_ER_IRQHandler ; I2C3 error
DCD OTG_HS_EP1_OUT_IRQHandler ; USB OTG HS End Point 1 Out
DCD OTG_HS_EP1_IN_IRQHandler ; USB OTG HS End Point 1 In
DCD OTG_HS_WKUP_IRQHandler ; USB OTG HS Wakeup through EXTI
DCD OTG_HS_IRQHandler ; USB OTG HS
DCD DCMI_IRQHandler ; DCMI
DCD 0 ; Reserved
DCD HASH_RNG_IRQHandler ; Hash and Rng
DCD FPU_IRQHandler ; FPU
很明显,其结构和上图是完全对应的。
其次可以看到,向量表中还包含主栈指针(MSP)的初始值,这种设计是非常有必要的,根据我们之前的文章,在复位后可能会立马产生 NMI 等异常,因此需要为这些异常的执行准备好运行环境。向量表中的 MSP 可以让处理器在启动时就获取并设置好栈指针,从而有能力应对上述可能发生的异常。详细原理可以参照下文:
资深嵌入式工程师的自我修养 —— Cortex-M3 的复位与复位流程
需要注意的是,Cortex-M 处理器中的向量表和 ARM7TDMI 等传统 ARM 处理器的向量表不同。对于传统的 ARM 处理器,向量表中存在跳转到相应处理的指令,而 Cortex-M 处理器的向量表则为异常处理的起始地址。
一般来说,最开头的地址(0x00000000)应该是启动地址,它可以是 FLASH 存储器或者 ROM 设备,并且在运行时不能对其进行修改。然而,有些应用中需要能够在运行时对异常向量表进行修改或重新定义。为了处理这种操作,Cortex-M3 和 Cortex-M4 处理器实现了一种名为向量表重定位的特性。
向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。该寄存器定义了被用作向量表的内存起始地址,如下图所示:
需要注意的是,改寄存器在 Cortex-M3 的版本 r2p0 和 r2p1 间稍微有点不同。对于 Cortex-M3 r2p0 或之前的版本,向量表只能位于 CODE 和 SRAM 区域,而这个限制在 Cortex-M3 r2p1 和 Cortex-M4 中已经不存在了。
VTOR 的复位值为 0,如果使用符合 CMSIS 的设备驱动库进行应用编程,则可以通过 SCB->VTOR 来访问该寄存器。想要将向量表重定位到 SRAM 区域的开头处,可以使用以下代码:
// 复制向量表到 SRAM 开头处(0x20000000)的代码示例
// 注意,下面使用的存储器屏障指令(DMB/DSB)是基于架构建议的,
// Cortex-M3 和 Cortex-M4 并非强制要求。
// 字访问指令
#define HW32_REG(ADDRESS) (*((volatile unsigned long *)(ADDRESS)))
#define VTOR_NEW_ADDR 0x20000000
int i; // 循环计数
// 在设置 VTOR 寄存器之前先将原来的向量表拷贝到 SRAM
for (i = 0; i < 48; i++) {
// 将每个向量表项拷贝到 SRAM
HW32_REG((VTOR_NEW_ADDR + (i << 2))) = HW32_REG(i << 2);
}
__DMB(); // 数据存储器屏障, 确保数据已经写入
SCB->VTOR = VTOR_NEW_ADDR; // 设置向量表偏移寄存器为新的向量表地址
__DSB(); // 数据同步屏障, 确保接下来的所有指令都是用新的配置
当使用 VTOR 时,需要将向量表大小扩展为下一个 2 的整数次方,且新向量表的基地址必须要对齐到这个数值。接下来给出两个示例以参考:
示例一:微控制器有 32 个中断源
向量表大小为(32(用于中断)+16(用于系统异常))× 4(每个向量所占用的字节数)= 192(0xC0)。将其扩展为下一个 2 的整数次方即 256 字节。因此,向量表的地址可被设置为 0x00000000、0x00000100以及 0x00000200 等。
示例二:微控制器有 75 个中断源
向量表大小为(75(用于中断)+16(用于系统异常))× 4(每个向量所占用的字节数) = 364(0x16C)。将其扩展为下一个 2 的整数次方就得到 512 字节。因此,向量表的地址可被设置为 0x00000000、0x00000200 以及 0x00000400 等。
由于中断的最小数量为 1,最小的向量表对齐值为 128 字节。因此,VTOR 的最低 7 位保留,且被强制为 0。
向量表的重定位常用于以下几种情形:
-
具有 Bootloader 的设备
有些微控制器具有多个程序存储器,启动 ROM 和用户 FLASH 存储器。微控制器生产商一般会将 Bootloader 预先写到启动 ROM 中,这样在微控制器启动时,启动 ROM 中的 Bootloader 就会首先执行,此时使用的是启动 ROM 中的向量表,而在跳转到用户 FLASH 的应用程序前,VTOR 需要被设置为指向用户 FLASH 存储器的向量表处,使用用户 FLASH 中的向量表(在一个程序存储器中实现 Bootloader 也类似)。
2. 应用程序加载到 RAM 运行
有些情况下,应用程序可能需要从外部设备加载到 RAM 中执行,其本身可能位于 SD 卡中,或者需要通过网络传输。在这种情况下,存储在片上存储器中用于启动的程序需要初始化一些硬件、复制位于外部设备中的应用程序到 RAM,并更新 VTOR 后执行应用程序。
3. 动态修改向量表
有些情况下,ROM 中可能会有一个中断的多个处理实例,可能需要在应用的不同阶段在它们之间进行切换。在这种情况下,可以将向量表从程序存储器复制到 SRAM,并且设置 VTOR 指向 SRAM 中的向量表。由于 SRAM 中的内容可以在任意时间修改,因此可以轻易地在应用的不同阶段修改中断向量。
向量表至少要提供 MSP 的初始值以及用于系统启动的复位向量。另外,对于一些应用,若设备在启动时有触发 NMI 的可能,则还需要加入 NMI 向量和用于错误处理的 HardFault 向量。