浅析嵌入式系统之uboot详解(4.3)—异常和异常向量

uboot详解—异常和异常向量

1. 前言

  中断是一个较难掌握知识,因为它是一个过程,而不是一个结果,其中的步骤都建立在理论的层面上,需要理解。比如按下按键1会使led1亮,这个“起因-结果”的操作我想小孩子也能掌握,因为它是一个现象,但是要掌握“起因-过程-结果”却需要花一些功夫,因为这个过程需要理解。如果你认真的了解了前面两节的内容,那么现在就该到了实现“过程”的时候了。

  前面两节分析了外部中断和内部中断相关的内容,这篇文章将对处理器的异常情况作一个总结。

2. 处理器异常模式

  参考第二篇文章(浅析嵌入式系统之uboot详解(2)—CPU工作模式),ARM处理器有七种工作模式,除了用户模式和系统模式以外,其他5种模式都是异常模式。
浅析嵌入式系统之uboot详解(4.3)—异常和异常向量


  我们都知道人会生病,生病时我们可以依靠我们的免疫系统来恢复,其实计算机也会“生病”,它“生病”的时候就会进入异常工作模式,依靠异常处理程序让cpu恢复过来,那么计算机在哪些情况下会“生病”呢?

  1. 复位异常:当cpu重新上电或者接收到reset信号的时候,处理器就会识别到有复位异常发生了,它就要进入管理模式,执行对应的异常处理函数,直到恢复正常为止。比如重启按钮被按下,或者cpu复位引脚电平发生变化,或者软件设置复位寄存器的相应位。

  2. 一般中断/快速中断请求:cpu和外围设备是分别独立运行的硬件执行单元,cpu对全部设备进行管理和资源调度处理,cpu要想知道外围设备的运行状态,要么cpu定时的去查看外围设备的特定寄存器,要么外围设备自己告诉cpu发生了什么,第二种方式就是我们常说的中断请求操作,中断请求分为一般中断请求和快速中断请求,快速中断请求有最高的优先级和最小的中断延迟(看前面文章的中断处理过程就知道了),通常用于处理高速数据传输及通道中的数据恢复处理,如DMA等,但是绝大部分外设使用的是一般中断请求。所以当有中断请求的时候,处理器将进入中断模式,执行中断处理程序,直到恢复正常。

  3. 预取指令中止异常:这个异常发生在cpu流水线的取指阶段,如果目标指令地址是非法地址,处理器就会进入中止模式,执行中止异常处理程序。比如使用bl指令跳转到某个地址读取指令,但是这个地址是不可读的,或者这个地址不存在。

  4. 数据中止访问异常:这个异常发生在要访问的数据地址不存在或者非法地址时,这种情况,cpu也会进入中止模式,执行对应的处理程序。比如mov r0, A ; A中存放的地址是不存在的或者不可访问的

  5. 未定义指令异常:这个异常发生在流水线技术的译码阶段,如果当前指令不能识别为有效指令,就会产生未定义指令异常,cpu就会进入未定义指令终止模式,执行异常处理程序。比如mov r0,#1的下一条指令是abc,这是一条非法指令。

  6. 软件中断指令异常:把这个异常放到最后,是因为这个异常的产生原因比较特殊,他是应用程序自己产生的,用于用户程序申请访问硬件资源时,通过操作系统内核代码来访问外设硬件资源的一种方式。比如应用程序需要将数据打印到屏幕上,就需要使用显示器这个硬件资源,但是应用程序是没有权限使用外设硬件的,外设都是有内核进行管理的,那么怎么办呢?那么只能使用软件中断指令切换到内核态,通过操作系统内核代码来访问外设硬件,内核态是工作在特权模式下的,操作系统在特权模式下完成将用户数据打印到屏幕,所以软件中断的异常时在管理模式下进行处理的。

  上面分析了处理器在哪些情况下会进入异常模式(注:这些模式都是cpu硬件层面的),第1、2、6种异常对于我们往往是有用的,工作中经常会使用这三种异常来实现我们想要的功能,但是第3、4、5种异常我们一般是不想看到的,这往往表示我们写了不规范的代码,或者执行了错误的操作。

3. 异常的处理过程

  从上面的分析可以发现,中断其实只是cpu异常的一种,中断处理有一个过程,其实它也是异常处理过程的一个子集。下面将对异常的处理过程进行分析:

从上面的分析可以发现,中断其实只是cpu异常的一种,中断处理有一个过程,其实它也是异常处理过程的一个子集。下面将对异常的处理过程进行分析:

  1. 保护执行状态:当cpu发生异常的时候,硬件首先会将当前的工作模式记录下来,以便后面异常处理完成后可以恢复回来,异常发生时,会将当前CPSR的内容复制到发生的异常模式下的SPSR中,比如在用户模式时发生了未定义指令异常,则会将当前的CPSR保存在SPSR_und中。

  2. 模式切换:硬件自动根据当前的异常类型,将异常码写入CPSR里的M[4:0]模式位,这样cpu就进入了对应的模式下,同时cpu会关闭中断IRQ(CPSR的I位置1),防止中断进入,如果当前异常是快速中断,则关闭快速中断。

  3. 保存返回地址:当前程序被异常打断,切换到异常处理程序里,异常处理程序处理完成后,需要返回到当前模式的对应位置继续执行,所以必须保存当前执行指令的下一条指令的地址到LR_excep(后缀用于区分,无实际意义)中

  4. 跳入异常向量表:该操作是cpu硬件自动完成的,当异常发生时,cpu强制将pc的值修改为一个固定内存地址,这块固定地址称作异常向量。

      后面我们将分析每一种异常的异常处理程序是怎么将cpu“治”好的,但是在这之前,需要弄明白,当cpu进入异常模式时,它是怎么确定要执行哪一种异常处理程序的?是谁指引它去“寻医问药”的呢?这个指示器就是异常向量
    浅析嵌入式系统之uboot详解(4.3)—异常和异常向量
      上面这张图片就是一个异常向量表,从图中可以看到,每种ARM异常对应一个字长空间,正好是一条32位指令长度,比如bl指令在一个字长中的存放如下图:
    浅析嵌入式系统之uboot详解(4.3)—异常和异常向量
      当异常发生的时候,cpu强制将pc值设置为当前异常对应的固定内存地址,比如当运行0x345678地址处的指令时,发生一般中断异常,cpu就会将当前的pc值强制设置为0x00000018,并把0x345678保存到lr寄存器中,然后下一条指令将会从0x00000018处取指,如果0x00000018处的指令是bl HandleIRQ, 那么cpu将会跳转到HandleIRQ处执行一般中断处理函数,这个函数是由我们编写的,记得中断处理函数完成时,要跳回0x34567d处继续执行原来的程序。

      分析到这来,也许你会有意外,为什么异常向量表的存放地址可以是SDRAM的前4k呢?那里不是存放uboot的地方吗?如果这里存放了异常向量表,那么uboot不就被破坏掉了吗?
    浅析嵌入式系统之uboot详解(4.3)—异常和异常向量
      我们再看看第一篇文章(浅析嵌入式系统之uboot详解(1)—板子上电后uboot做了什么)中介绍的这张内存映射图,当板子从norflash启动的时候,内存映射图如左半部分,这时没有存放uboot,uboot放在norflash中,所以存放异常向量表是没有影响的;然而当从nandflash启动的时候,内存映射图如右半部分,内存的前4k在刚上电的时候是会存放uboot的,如果再存放异常向量,必然会破坏uboot的数据!但是即使如此,为什么我们的板子能够正常运行呢?这个就涉及到了代码重定向的内容(将再后面重点讲解),因为在uboot初始化异常向量表的时候,cpu已经将uboot拷贝到SDRAM中执行了,也就是说uboot已经在0x30000000地址处了,即使在初始化中断向量时有破坏uboot,对板子的正常启动是没有影响的。

      从中断向量表中可以看出,复位异常的向量指向0x0000000处,开发板的启动地址也是0x0000000处,这样要巧妙的设计是为了处理这种异常时,不用再写异常处理函数了,因为收到reset信号的时候,硬件会自动将uboot从nandflash中拷贝到stepping stone中,这样,uboot则成了复位异常的处理函数。至于norflash启动,其实过程也一样,请参考第一篇文章描述。

      不仅如此,因为快速中断的中断向量地址是0x0000001c,它是最后一个向量,后面的空间可以被使用,所以可以将快速中断的处理函数直接以这个地址为开始进行编写了,而不用像其他异常处理函数一样,需要bl跳转到其他地址进行实现,这样可以节省一个bl指令的执行时间,也就是一个时钟周期(为什么是一个时钟周期?请参考arm指令流水线技术)。

      下面是通常异常向量表中存放的指令:

    bl reset

    bl HandleUndef

    bl HandleSWI

    bl HandlePrefetchAbt

    bl HandleDataAbt

    bl HandleNoUsed

    bl HandleIRQ

    bl HandleFIQ
    这些Handle则是存放着异常处理函数的入口地址

  5. 通常找到异常向量表以后,异常向量表就会指引cpu去哪里执行异常处理程序,下面就来分析一下异常处理函数的实现过程。

    异常处理函数一般都是有程序员写的,也往往是处理异常时最有发挥空间的地方,因为有了能够给我们编写异常处理函数的余地,才能让我们实现多种多样的功能,让我们的键盘能够快速的输入,我们的鼠标能*的晃动,让各种传感器高速的捕捉信号,cpu也能及时的高效的处理这些请求。

    A. 保存执行现场
      
       异常处理程序最开始,要保存被打断程序的执行现场,程序的执行无非就是保存当前操作寄存器里的数据,可以通过下面的栈操作指令实现现场保存现场:
      
      STMFD SP_excep!, {r0 - r12, LR_excep}

      注:LR_abt,SP_excep分别对应异常模式下LR和SP
      
      需要注意的是,在跳转到异常处理程序入口时,已经切换到对应异常模式下了,因此这里的SP是异常模式下的SP_excep了,所以被打断程序现场(寄存器数据)是保存在异常模式下的栈里,上述指令将r0~r12全部都保存到了异常模式栈,最后将修改完的被打断程序返回地址入栈保存;之所以保存该返回地址就是将来可以通过mov pc,lr 的指令返回用户程序继续执行。
      
    B. 异常处理过程

      这个是异常处理的核心部分,具体问题需要具体分析和解决。但是编写时应该注意一下几点:
      
       ISR不能有返回值;ISR不能传递参数;ISR应该是短而高效的,在ISR中做浮点运算是不明智的;ISR中不应该有重入和性能上的问题,因此不应该使用pintf()函数。
      
    C.异常处理返回

      
      异常处理完成之后,返回被打断程序继续执行,具体操作如下:

    • l 恢复被打断程序运行时寄存器数据

    • l 恢复程序运行时状态CPSR

    • l 通过进入异常时保存的返回地址,返回到被打断程序继续执行

  异常发生后,进入异常处理程序时,将用户程序寄存器R0 ~ R12里的数据保存在了异常模式下栈里面,异常处理完返回时,要将栈里保存的的数据再恢复回原先R0 ~ R12里,毫无疑问在异常处理过程中必须要保证异常处理入口和出口时栈指针SP_excep要一样,否则恢复到R0 ~ R12里的数据不正确,返回被打断程序时执行现场不一致,出现问题,虽然将执行现场恢复了,但是此时还是在异常模式下,CPSR里的状态是异常模式下状态,因此要恢复SPSR_excep里的保存状态到CPSR里,SPSR_excep是被打断程序执行时的状态,在恢复SPSR_excep到CPSR的同时,CPU的模式和状态从异常模式切换回了被打断程序执行时的模式和状态。此刻程序现场恢复了,状态也恢复了,但PC里的值仍然指向异常模式下的地址空间,我们要让CPU继续执行被打断程序,因此要再手动改变PC的值为进入异常时的返回地址,该地址在异常处理入口时已经计算好,直接将PC = LR_excep即可。

  上述操作可以一步一步实现,但是通常我们可以通过一条指令实现上述全部操作:
  LDMFD SP_excp!, {r0-r12, pc}^
  注:SP_excep为对应异常模式下SP,^符号表示恢复SPSR_excep到CPSR

中断异常处理操作可以用下图来描述:
浅析嵌入式系统之uboot详解(4.3)—异常和异常向量

4. 总结

  这里用了三节内容分析了中断和异常的相关内容,包括中断的产生,中断的处理过程和中断的配置等,由于这些知识不仅是重点也是难点,后面将会在分析uboot源代码的时候继续讲解。

  经历一段小艰难后,下一篇我们将了解一下较轻快的话题——时钟分频。
浅析嵌入式系统之uboot详解(4.3)—异常和异常向量

上一篇:uboot启动流程详解


下一篇:Mini440之uboot移植之实践NOR FLASH支持(七)