嵌入式ARM+Linux中断全流程讲解
- ARM处理器级别的中断(异常)
- ARM的异常类型
- CPU如何检测到异常信息、进入异常处理后怎么返回
- 前置知识:流水线,指令周期,机器周期,时钟周期
- 复位。复位就直接从程序第一条开始重新执行,不会有其他流程。
- 未定义指令
- 软件中断
- Boot loader级别的中断(异常)
- Linux级别的中断(异常)
-
ARM处理器级别的中断(异常)
ARM的异常类型
我们常说的IRQ中断和FIQ中断属于ARM异常的两种,下面统一用异常来表示,ARM有七种异常。如图1所示。
图1. ARM的七种异常
CPU如何检测到异常信息、进入异常处理后怎么返回
异常处理的源头,首先在于CPU检测到了异常信号。下面分别介绍这七种异常分别在什么时候被检测到,又在什么时候进行处理。
前置知识:流水线,指令周期,机器周期,时钟周期
我们站在汇编语言的角度来看待程序的执行,一条汇编指令从内存中取出该指令,到整个指令执行完毕,总花费的时间称为指令周期,如果是ARM7的三级流水线,每条指令的运行又分为取指、译码、执行三个阶段,如果是ARM9的五级流水线,每条指令的运行分为取指、译码、执行、访问内存、执行结果写入寄存器,后面两级是针对于内存的加载和存储指令的,‘执行’仍然在第三级上。像取指、译码、执行等操作,每完成一个这样的操作的时间叫做机器周期 ,当然每个操作的机器周期不一定相同,每个机器周期可能有包含若干个时钟周期,就是晶振频率的倒数,是计算机最基本的时间单元。还有注意一点,取指、译码、执行等操作都是在我们通常认为的CPU中进行的,任何一个阶段检测到异常信号,就等同于CPU检测到了异常信号。
复位。复位就直接从程序第一条开始重新执行,不会有其他流程。
未定义指令
假设有ins1、ins2、ins3、ins4、ins5,这五个汇编指令,其中ins3是未定义的指令,假设流水线是ARM9的五级(三级也一样,关键是执行这一级在哪个位置),假设流水线已经满载。在指令周期1中,当ins1在执行时,ins2在译码,ins3在取指,此时PC的值为ins1的地址+8(一般说法是PC指向下一条将要执行的指令的地址,这里的执行是概念上的执行,可以理解为运行,运行的第一步是取指,所以PC实际上是指向下一条将要取指的指令的地址);指令周期2中,ins2开始执行,ins3开始译码,ins4开始取指令,此时PC的值为ins2的地址+8,但是这里问题来了,由于ins3是未定义的,也就是说指令集里没有这个指令,那么在译码时就会出错,引发未定义指令异常。总结:未定义指令在译码阶段就会出现异常信号。 我们经常在各种书上看到一句"CPU在每条指令执行完后检测是否有异常信号"。我个人是这么理解的,由于执行阶段一般耗时比译码阶段长,所以ins3译码出问题后,会等待ins2执行完毕,这符合每条指令执行完毕就检测异常信号的说法。这样ins2执行完毕后,检测到未定义异常,ARM CPU核会自动做以下事情:
(1)将当前PC的值复制给LR_und,注意此时PC的值是ins2地址加8,也就是当前刚刚执行完毕的指令地址加8,也就是ins4的地址。
(2)将CPSR的值复制给SPSR_und
(3)设置CPSR为und模式
(4)将PC的值设置为und在异常向量表中的地址
程序员需要编写程序完成以下事情:
(1)在und异常向量表的地址放跳转指令,跳到具体处理und的代码
(2)在具体处理und的代码中:
(2.1)保存现场,将各种寄存器都压入栈
(2.2)处理und
(2.3)返回:将LR_und的值直接赋给PC。为什么是直接赋给PC呢?,这里返回正常是想继续让ins2的下一条指令,也就是ins3得到执行,但是ins3是未定义的,不需要执行,所以希望执行ins4,而此时PC的值正好就是ins4的地址。注意: 可能有人会疑惑之前ins2执行的时候,ins4不是已经取指了吗,直接进行译码不就行了?这是因为通过PC值进行无条件转移时会导致流水线清空,所以得从新取指令。
软件中断
跟上文一样,假设ins3是软中断指令,个人假设,由于cpu是知道这类swi指令必然引发软中断异常的,所以在译码阶段就能识别这是软中断指令,从而引发异常。总结:软中断指令和未定义指令一样,在译码阶段检测到异常 。同时由于译码完成,也已经知道了软中断调用的系统调用号,这时ins2执行完毕后检测到异常信号,跳转去执行软中断的处理,过程和上面类似,在处理完成后返回时,仍然是将LR_svc的值直接赋给PC,因为不需要实际运行ins3,我们知道它是swi指令,知道它要运行的系统调用,我们已经在swi中断中处理了,所以直接运行ins4。
https://blog.csdn.net/weixin_43625081/article/details/108622207
https://blog.csdn.net/Setul/article/details/53992920
https://blog.51cto.com/embedjee/739830
https://blog.csdn.net/lee244868149/article/details/49488575?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1-49488575-blog-75193885.pc_relevant_multi_platform_whitelistv1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1-49488575-blog-75193885.pc_relevant_multi_platform_whitelistv1&utm_relevant_index=1
Boot loader级别的中断(异常)
以uboot为例子,uboot在没启动内核之前,可能需要用到中断,比如USB传输数据需要用到中断,这时可以在启动文件中做一些中断的处理,这部分和单板中断没有区别。但是如果uboot马上要启动内核了,内核启动之前必须禁止中断,因为内核启动过程中虽然会有对中断的初始化,但是如果还没有初始化好,处在一个中间阶段,这时出现了中断,会跳到uboot的中断代码,这样uboot运行后不一定能再次正确的启动内核。
Linux级别的中断(异常)
检测到异常信号时,CPU怎么知道异常向量表在哪里呢?是不是在芯片手册中固定死了的?这个
Linux中异常、中断、系统调用、陷入的概念区分
参考1:https://blog.csdn.net/clp_csdnid/article/details/50964641
参考2:https://www.6miu.com/read-3186100.html
参考3:https://blog.csdn.net/linuxweiyh/article/details/106891526
参考4:https://blog.csdn.net/icyfire0105/article/details/1898523
一个原则:不管Linux是怎样的,Linux最终还是要运行在具体的硬件上,比如用户态和内核态,最终还是区别在arm模式的区别。对于中断和异常,arm芯片有自己的定义(异常包含中断,七种异常,七种模式),Linux的运行必定是依赖于arm芯片的。
系统调用: 系统调用通过SWI软中断指令实现,比如我们在应用程序(运行在用户空间)调用read()函数,这个函数实际调用C库中的read函数,C库中的read函数引发SWI软中断,一般单板用SWI后面会指定一个立即数,该立即数是系统调用号,注意所有的系统调用都用软中断,软中断会跳转到处理软中断的中断处理程序,处理程序需要系统调用号来区别这个软中断具体需要执行什么,比如read和write都是软中断,也就是说系统调用号是来区分open、read、write这种的。回忆我们在用户程序中对read的调用,我们调用的read函数还带有几个参数,比如读的文件名字、读的大小等等,因为SWI加立即数已经占满了一条指令32位的空间,所以这些参数是通过寄存器传递的,C库会负责将参数放到指定的寄存器中,再去调用具体的系统调用,系统调用处理函数首先根据系统调用号选择操作类型,然后根据寄存器内容进行具体操作。当然,结果得返回也是通过寄存器进行传递的。
上图的sys_read()就会根据传入的文件名找到其主从设备号,进而找到对应驱动,调用驱动中的read函数。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)