ARM Cortex-M系列之中断向量表

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语言定义。以下列出两种方式的示例程序。

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
};

Assembly

这里以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 

中断向量表偏移寄存器 (Vector Table Offset Register)

ARM Cortex-M默认的中断向量表地址位于闪存起始地址处。但是ARM Cortex-M3/4系列提供了一个中断向量表偏移寄存器(Vector Table Offset Reigster)。系统中中断向量表的位置是0x00000000加上偏移寄存器的值。上电复位后这个寄存器值为0,所以中断向量表默认位于0x00000000闪存起始处。这个寄存器的目的是为了让开发者可以重新设置中断向量表的位置。

  • 中断向量表偏移寄存器第29位(bit 29)定义了中断向量表的位置。0表示位于闪存程序(code)中,1表示位于内存中(SRAM)。
  • 中断向量表寄存器低7位(bit 6~0)为系统预留位。
  • 偏移地址寄存器值有对齐要求。这个要求和系统中断数量或者说中断向量表长度相关。偏移地址寄存器值至少是128 (32words = 128bytes)的整数倍,这也意味着中断偏移寄存器中地址的低7位始终会是0。如果系统中断数量大于16个,则总中断数为ARM预留的16个中断加上n个系统中断。如果(n+2)不为2的指数,则向上找到最近的2的指数m。每个地址为4bytes所以对齐要求为m*4。例如系统有21个中断,加上ARM预留的16个中断位,则中断向量表有效长度为37words。最近的2的指数值为64words = 256bytes。所以偏移寄存器的值必须为256的整数倍。

中断向量表偏移寄存器应用

中断向量表偏移寄存器最普遍的用法是在系统上电复位后将中断向量表指向内存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();
}

你可能感兴趣的:(ARM,Cortex,M,嵌入式,arm)