




已阅读5页,还剩16页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
ARM 的历史以及有哪些种类的内核就不必再赘述 我们就直接从 ARM7 指令 集 V4 讲起 关于 ARM 到底属于什么类型的 CPU 一直以来说法不一 有说他们是 RISC 至少他们自己这么认为 Advanced Risc Machines 也有说他们是类 RISC 的 说类 RISC 的理由就是 ARM 的指令并不是定长的 而且并没有实现真正的流 水线 当然这些区别现在已经变得越来越不重要 甚至两个领域都在互相渗透 比如 MIPS 的 14K 和 24K 系列已经引入了 16 位指令 而较新的 ARM 架构也 已经开始支持乱序执行 可能有人会反驳 怎么可以这么说呢 ARM7 中实现了三级流水线 请注意 这里所说的是真正的流水线 如果能对 MIPS 有所了解或者看过 MIPS 阵营的 经典著作 see mips run 就能明白真正意义的流水线的含义 在 RISC 领域 有一个概念叫做 delay slot 也就是我们所说的延迟槽 出现这个概念的原因是 因为在 RISC 中 绝大多数指令的执行时间是可预测的 就是一个周期 而有 些指令譬如 load jmp 之类的无法在一个周期内完成 这样就造成一个问题 在执行 jmp load 时 处于译码阶段的指令就要多等一个周期 等前面的指令 执行结束它才能进入执行阶段 早期的 ARM 就是采用这种方式 而在真正的 流水线架构中 遇到这种情况 可以把 jmp load 之后的指令在 jmp load 进 入执行阶段准备但是没有真正执行的时候提前执行 这样就节省了之前被浪费 的那个周期 处于延迟槽中被提前执行的指令必须是对后续指令无害的 这个 由编译器来决定 当无法插入无害指令时 编译器会向其中插入 NOP 千万不要小看这个周期 因为在真正的执行中 内存读写以及跳转操作随处可 见 这点效率的提升导致了 MIPS 在整数运算性能上的优势 我的经历简单得很 上学时老师的项目和师弟帮忙找的私活 51 96 pic 的简 单控制电路和汇编程序 毕业后进入华为 开始学习的 c 和 mips 当时负责基 于 MIPS VxWorks 的 bsp 和驱动 得感谢 Broadcom 的 FAE 送给我们的一本 See MIPS Run 从这本书开始才真正把学过的计算机体系结构和软硬件结 合到一起 再后来进入一家 IC 外企 做基于 MIPS Nucleus 的 bootloader BSP 和驱动 从这开始对操作系统有点接触 在华为时由于分得 太细 没能够对 VxWorks 更多了解 再后来转战 ARM ucos linux 固件 芯片验证等等 这才开始每天跟 cpu 核心打交道 也对操作系统更加深入 目 前是要回到一个老东家那里继续做 mips 我运气好的地方是正好工作内容基本上都是我每个阶段想接触的东西 没有参 加过任何任何培训 几本好书加上官方文档陪伴着走过这段职业生涯 所以很 多时候别人问我怎么学我都不知道怎么回答 因为没有为了学而学过 都是工 作需要 实际回过头来看看 两大块知识对于嵌入式至关重要 一个是计算机体系结构 一个是操作系统原理 囊括了我个人目前见到的所有东西 都脱不出这个范畴 至于各种接口 都是比较分散的知识点 需要花时间去学习掌握 而支持 ARM 属于 RISC 的理由在于四点 1 使用了精简指令集 Reduced Instruction Set Computer 虽然 ARM 的指令 相对其他 RISC 比还是算多的 但毕竟出于可接受范围 不像 x86 那样浩瀚无 穷 RISC 的目的就是让硬件尽量简单 更多的处理交给编译器和软件 这样 就可以让 kernel 结构简单 比较容易实现低功耗高频率 2 实现了一定程度的流水线 学过 51 的人应该还记得 51 的工作方式 指令都 是一条一条地取指译码执行 只有上一条指令执行结束 下一条指令才能进入 取指阶段 3 ARM 核心中有较多的通用寄存器 总共有 31 个 不算状态寄存器 但是它 们并不能同时存在 有些寄存器是模式专有的 只有处理器处于那种模式才能 够对其进行操作 术语称为 Banked 在固定模式下可见的通用寄存器的个数 是 16 个 4 采用了 Load Store 的工作方式 所谓的 Load Store 方式 也就是说一切的 计算操作都只针对寄存器 内存中的数据需要先 load 到寄存器 进行计算之后 再 store 回内存 而在 cisc 中 内存数据是可以直接参与计算的 这是双方一 个巨大的差别 个人所看过的好书 最推崇的是 See Mips Run 第二版 结合 linux 对 mips 做了非常全面而且深刻的讲解 而你从这本书中得到的将不仅仅是 linux 和 mips 涉及到了很多操作系统以及计算机体系结构的共通问题 推荐看英文版 做软件开发到一定程度 不管你是做了项目管理还是研发 都会不可避免地用 到脚本语言去写一些自动化的工具 因为身处芯片行业 所以用的就是 perl 推荐读 perl 语言入门 和 intermediate perl 就可以处理大多数问题 这一 系列中的 精通 perl 没有读 因为没到那个层次 如果有兴趣有精力强烈推荐 精通正则表达式 这本书 尽管属于杂项 但是 对于软件人员来说 读过这本书绝对令人心旷神怡 你在 vi ue makefile 甚 至 arm 的 scatter 和 gcc 的 ld 中到处可以见到它的身影 读懂了正则表达式 就会明白这些工具中生涩难记的命令原来也是有规则可循 他们都在遵循着正 则表达式 那本 操作系统原理与设计 好像是这么叫的 如果只是翻翻会很无聊 甚至 你会觉得都不如 深入 linux 内核 好 而你要是能够一字一句的看进去 就会 发现前者是理论的高度 后者只是在实现层面略作讲解 至于软件工程方面的书 人月神话 和 解析极限编程 对我启发更大 代 码大全 和 人件 就没有太多感觉了 了解流水线对于软件人员来说 一个比较重要的意义就是当出现问题时 能够 精确定位到产生问题的那句话 但是有些情况是例外的 体制上就不能保证精 确定位 比如说有 write buffer 的时候 你的写指令即使是发生了错误 也是在 数个周期之后才会激起异常 为了速度匹配问题 写数据会通过 write buffer 操 作 发生异常的时刻已经离发出写指令的时刻不匹配了 当发生异常的时候 犯罪分子和发生异常时所在模式的 pc 的匹配关系如下 Data abort pc 8 因为 data abort 只有进入执行阶段才能被发现 这时第三 条指令已经被取指 IRQ pc 8 IRQ 发生时刻不可预知 在当前指令执行完之后响应 所 以 pc 也是第三条的地址 FIQ pc 8 原理同上 Prefetch Abort pc 4 这个最绕 是预取指时发生错误造成的 但是如果这 条指令不进入执行阶段就不会造成异常 比如说前面一条是 jmp 所以我猜测 在它进入执行之前 kernel 已经知道出错 pc 不在更新 保持为它下一条指令 的位置 SWI 和 UNDEF 和上一条一样 都是在译码阶段 kernel 已经知道会发生异常 不再更新 pc 但是进入执行阶段才会激起异常 所以异常地址也都是 pc 4 今天来讨论一下对齐问题 在 ARM7 即 arm v4 中 规定如果访问 int 类型数 据时给出的指针地址的低 2 位不为 0 的话 系统会自动将低两位的 1 抹平 即 强制四字节对其 这样的问题就是如果你的数据偏偏就是不对齐的 cpu 拿到 的数据就是不正确的 bug 由此产生 在 ARM9 我接触的是 arm926ejs 中 如果访问 int 型数据 低 2 位不为 0 那么 cpu 直接挂住 一个 data abort 在 ARM11 之后的版本中支持了非对其访问 是在总线上拆分然后拼接来实现 的 也就是说如果你访问 int 时给出的地址不是 4 字节对齐 那么总线上会出 现两个 int 访问 然后把数据拼接起来送给 cpu 这些对于 cpu 是不可见的 但 是会导致速度下降 总线占用率上升 此功能可以通过修改 cp15 中来关闭 实现跟以前版本一样的对齐方式 需要说明的是这里的 int 是 4 字节而不是 2 字节 另外一点是对齐问题并不是 专指四字节对齐 很多初学者或者有一定经验的人都会犯这个错误 认为只有 四字节对齐才会出问题 实际上对齐指的是数据边界对齐 也就是说 long long 数据要 8 字节对齐 int 要 4 字节对齐 short 要 2 字节对齐 byte 自然是怎么 对都齐了 主流的 ARM 核心会包含很多种模式 除了定位为 MCU 的 Cortex M 系列 这 些模式包括如下几种 1 中断模式 IRQ 2 快速中断模式 FIQ 此模式在 ARM7 中并未实现 3 用户模式 USR 4 系统模式 SYS 5 软中端模式 SVC 6 数据终止模式 ABT 包含了数据终止和预取中止 7 未定义指令模式 UNDEF 8 监视模式 Monitor 这个模式是比较新的架构中才添加的 应用场景比较 特殊 我还没有完全搞懂 系统复位后 CPU 的模式是 SVC 模式 一般情况下会在切到用户程序之后切 换到 USR 模式 但是最近有个同事跟我讲三星 2440 的例程中 CPU 是始终工 作在 SVC 模式下的 这种方式非常古怪 保护模式的操作系统下 一般的原理是用户程序工作在 USR 模式下 当你要 做系统调用的时候就激起软中断 软中断服务在判断你的请求合法之后调用内 核函数 如果不合法就置上错误号 返回给用户程序 内核中的大部分函数工 作在 SYS 模式下 USR 模式和 SYS 模式公用相同的寄存器 只是特权等级不 一样 所谓的不一样也主要是 SYS 模式下可以操作 CPSR 而 usr 模式不允许 另外 USR 和 SYS 模式下是没有 SPSR 的 因为这两种模式不是异常模式 是 常规的操作模式 IRQ 和 FIQ 模式就不用再多说了 做过单片机的人会觉得很熟悉 ABT 模式就是当发生了地址或者数据异常时进入的模式 这种模式既可以做 为数据的保护方式 当你有 MMU MPU 时 也可以实现虚拟地址的扩展 还有可以帮助我们保存 bug 现场 方便调试 一般裸奔或者非保护式系统中 ABT 模式的处理就是死循环 因为在这类系统中如果发生了 ABT 就是不可恢 复的错误 UNDEF 是指 CPU 遇到了无法解析的指令 发生这种状况一般来说有以下几 种场景 1 在 ARM THUMB 混编情况下 ARM 状态下执行了 THUMB 指令 或者 THUMB 状态下执行了 ARM 指令 2 堆栈异常导致 PC 返回值异常 程序跑飞 执行了不存在的指令 3 函数指针错误 4 有意的指令扩展 这种就需要在 UNDEF 异常处理中添加自己的指令扩展处 理程序 5 代码区被异常改写 这里要补充一点 各种异常模式下的 spsr 是用来存储进入异常时前一种模式的 cpsr 而且只有真正发生异常时才会把前一种模式的 cpsr 写入 spsr 通过修改 cpsr 来切换模式并不会导致 spsr 的改写 Cache 逐渐成为现代 CPU 中越来越重要的角色 但到目前为止 依然有很多 生产出来的 ARM 芯片是不带 cache 的 所以我们在这里只对 cache 做个简要 的介绍 Cache 从其本质来说也是 SRAM 只不过是离 cpu 最近的 SRAM Cache 与 外部 这里的内外是针对 CPU 的 kernel 而言 而不是针对 SoC SRAM 最大 的不同不在于速度 而在于它与 cpu 的交互方式 cpu 访问 cache 的时候是直 接通过其内部的 cache 控制器 而访问外部 SRAM 时首先要向总线发出请求 当总线仲裁器允许 CPU 访问总线后 CPU 发出地址信息并等待 SRAM 返回数 据 关于总线的详细信息可以到 ARM 官网上去下载 AMBA 的文档来学习 TCM 和 cache 并列为 ARM 核心的一级 memory TCM 的设计为 ARM 提供了 另外一种快速访问的方式 并且 TCM 的速度是恒定的 不像 cache 会有 invalidate 和 flush 等操作 在访问 cache 前你并不知道你所要访问的数据是否 在 cache 中 而在访问 TCM 的时候 你所需要的东西一定在 TCM 内 这是因 为你在配置 TCM 的时候已经设置好了它的地址范围 只有在访问这一段地址 的时候才会访问 TCM Cache 和 TCM 的操作都是通过 cp15 寄存器指令来实现的 指令格式略显繁琐 而且无法用 c 语言来实现 不过还好 照着 arm 手册依样画葫芦就行了 基础知识的最后我们需要讲一下向量表 我相信做过单片机的朋友对向量这个 词应该不陌生 也就是发生了这个中断或者异常的情况下 cpu 我认为 MCU 也是 cpu 会把 pc 直接指向某个固定的地址 在那个地址有个跳转语句或者一 小段处理 比如说 mips 为每种异常提供了 128 字节的处理空间 而你的系统 所有的异常处理的那个地址表也就是我们所说的向量表 ARM 的向量表有两种 选择 0 x0 起始或者 0 xffff0000 起始 选择哪个地址作为可以通过修改 CP15 中的相应控制位来实现 一个例外就是 Cortex M3 M0 和 M4 没接触过 它 把很多传统 ARM 中 CP15 的 system control 寄存器拿到了直接可寻址的地址 空间 可以像操作普通寄存器那样修改向量表起始地址 至于为什么允许修改向量表地址 这个问题就跟操作系统的实现相关了 在 linux 和 windows 中 0 地址位于用户空间 意味着整个向量表都在用户空间的 起始地址 当有异常发生时 cpu 自动就去用户空间拿指令 这样就会导致用 户空间可以轻易地获得 cpu 的特权 所以异常模式都是特权模式 而 0 xffff0000 处于内核空间就不会导致这样的问题 所以在有保护式操作系统的 应用环境中 cpu 启动时首先会默认 0 地址是向量表起始 而在系统启动前 启动代码会把向量表切换到高位 以保证操作系统的实现 ARM 的基础知识我们先讲这些 其他需要扩展的内容或者回头想到落下的东西 再补充 我们从复位开始讲起 复位又被分为很多种 什么上电复位 重启复位 冷复 位热复位的 我们不考虑那么多 只从最根本的上电复位说 其他类型的当我 们对 cpu 熟悉之后都可以自然地延伸 复位之后 pc 指向一个固定的地址 也就是向量表的起始地址 0 x0 这个地址必 须是有确定的内容 启动代码 为什么不用另外一种说法 这里必须是 NV memory 呢 因为有些公司确实实现了 0 地址是 SRAM 的做法 他们为了允许 客户不使用昂贵的 ROM 或者 NOR flash SOC 中用逻辑实现了 NAND flash 的驱动 上电之后立即从 NAND 中读出前面几 K 的内容拷贝到 SRAM 的 0 地 址 这样 cpu 去 0 地址取到的实际就是预先写到 NAND 中的内容而不是随机的 SRAM 复位内容 个人猜测他们的实现机制是这样 上电之后 SOC 的逻辑将 cpu 的复位拉住 使 cpu 始终处于复位状态 同时去拷贝代码 当代码拷贝完 成后松开 cpu 复位线 这时就保证了 cpu 能够读到确定内容 下面是一段很典型的简单嵌入式系统的启动代码 没有 MMU 没有 cache 也 没有外接 RAM 所有代码数据都在 SoC 自带的 SRAM 中 我会将其中的重点 一一讲解 而且启动的代码的讲解要配合上分散加载文件 才能显得更清晰 这是 scatter 文件的内容 分散加载的具体语法和用法请参阅 ARM 链接器的官 方文档 ROM LOAD 0 x20000000 说明加载域位于 0 x2000 0000 ROM EXEC 0 x20000000 说明根执行域也是从 0 x2000 0000 开始 vectors o Vect First vectors o 是生成文件的起始部分 保证向量表 在 0 x0 地址 RO 剩下的 RO 段放在 vector 的后面 包括代码和只读数据 RAM 1 0 RW ZI 非零全局数据和 bss 段放在 RO 段之后 HEAP 16 init o Heap 这段是 HEAP 段 STACK 0 x2002FFFC init o MyStacks 系统 RAM 的最高地址是 0 x2002FFFF 所以 stack 从此向下 根执行域必须同加载域是重合的 并且是处于开始位置的可执行代码 因为要 用根执行域的代码来完成分散加载部分的其他工作 这是 Init s 的内容 EXPORT Reset Handler 这里要让 vectors s 看到 所以要 export Reset Handler 如果需要做地址空间转换的话 就会执行这段代码 为什么做地址空间转换 呢 至少有两个理由的 首先 0 x0 地址一般是 bootloader 的地址 bootloader 会用某种方式将 runtime image 加载进内存 一切处理好之后 bootloader 会 把控制权交给 runtime image 进行二次复位 两次复位的目的和执行的代码不 一样 所以需要做一次地址空间转换 有的代码会做一些通用性处理让两次复 位执行同样的函数 但是同样会有灵活性以及执行速度的需求要求向量表在 SRAM 中 所以在设计 CPU 时对于这里要仔细考虑好 加了没有任何害处 不加就可能日后造成困扰 IF EF ROM RAM REMAP LDR pc Instruct 2 Instruct 2 Remap by setting Remap bit of the CM ctl register LDR r1 CM ctl reg LDR r0 r1 ORR r0 r0 Remap bit STR r0 r1 ENDIF 这里是个比较绕的过程 我尽量解释 如果还是有不懂的地方 可以继续提出 来讨论 比如说这段启动代码在一块 ROM 中 如果需要做地址空间转换 IC design 的 人会在系统复位后赋予这个 ROM 两个地址 一个 0 x0 另外一个是类似于 0 x4000 0000 这样的其他非零地址 第二个地址在做地址空间转换前后都可以 访问 0 x0 只能在地址空间转换之前访问 当链接的时候 我们告诉链接器 这段代码在 0 x4000 0000 然后我们把这段代码固化到 ROM 中 这样在复位 时 pc 指向 0 取到的是这段代码 而当执行完 LDR pc Instruct 2 之后 pc 已经变成了 0 x4000 0004 而这个地址也是可以正确访问 ROM 的 这之后再 去写地址转换寄存器也不会导致读不到正确的 ROM 内容了 可以想象一下 如果不做前面所说的这个似乎多余的步骤 PC 始终是按照 0 4 8 c 这个顺序执行下去 做完地址空间转换 拿的就是 RAM 的内容 必死 无疑 Initialize stack pointer registers BL InitStack IMPORT main Now enter the C code B main InitStack 就不用废话了 操作 cpsr 切换到各种模式 然后设置各种模式下的堆 栈 就可以允许跑 c 代码了 设置完之后调用 ARM 提供的 main 库函数 如 果你使用了这个库函数 那么你必须保证你的 c 代码中有 main 函数 main 在做完分散加载 内存初始化 代码拷贝等工作之后默认跳到 main 函数 因 为我只用过 ARM 官方的编译链接器 所以不知道其他人是怎么做的 不过我 估计大概意思都差不多 以前用 MIPS 的时候用的 GCC 也一样要自己写好 LD gcc 的分散加载描述文件 不过当时分散加载 代码拷贝和内存初始化都 是自己用汇编完成的 不清楚 gcc 是否也提供了类似于 arm 这样方便的工具 为什么用 B main 呢 因为这个函数是不会返回的 直接把控制权交给 main 而不是交还给调用它的初始化代码 InitStack MOV R0 LR Build the FIQ stack MSR CPSR c 0 xd1 LDR SP StackFiq Build the IRQ stack MSR CPSR c 0 xd2 LDR SP StackIrq Build the DATAABORT stack MSR CPSR c 0 xd7 LDR SP StackAbt Build the UDF stack MSR CPSR c 0 xdb LDR SP StackUnd Build the SVC stack MSR CPSR c 0 xd3 uCOS starts with SVC mode LDR SP StackSvc Build the SYS stack MSR CPSR c 0 xd3 LDR SP StackUsr Return MOV PC R0 must be 8 byte aligned FIQ STACK LEGTH EQU 128 IRQ STACK LEGTH EQU 2048 ABT STACK LEGTH EQU 128 UND STACK LEGTH EQU 128 SVC STACK LEGTH EQU 2048 StackAbt DCD top of stack UND STACK LEGTH IRQ STACK LEGTH FIQ STACK LEGTH SVC STACK LEGTH StackSvc DCD top of stack UND STACK LEGTH IRQ STACK LEGTH FIQ STACK LEGTH StackFiq DCD top of stack UND STACK LEGTH IRQ STACK LEGTH StackIrq DCD top of stack UND STACK LEGTH StackUnd DCD top of stack AREA MyStacks DATA NOINIT top of stack SPACE 4 此处的 space 定义可以不用管 只是为了确定 top of stack 位置 EXPORT bottom of heap AREA Heap DATA NOINIT bottom of heap SPACE 1 道理同上 END 以上部分我给大家看的是 runtime image 部分的启动代码 下面再看一下 bootloader 部分的启动代码 略微有些差别 因为绝大多数情况下 bootloader 中我们不想启用中断 也绝不会去处理异常 所以在 bootloader 中就没有去写中断向量表 但是 没有中断向量表并不意味 着 cpu 就不去响应异常 如果你的程序中有 bug 比如说读写了不存在的地址 或者函数指针错误 这样一定还是会激起异常 cpu 照常去 0 xc prefetch abort 或者 0 x10 去拿指令执行 即使这里已经不是中断向量表了 但是 cpu 依然按照内置的逻辑去执行 所以 bootloader 要小心再小心 那种固化在 SoC 内部的 bootloader 一旦出问题 你生产出来的芯片就是块石头 下面是一款 bootloader 的 scatter 文件 scatter 的解析可以参照上一个例子 LOAD ROM 0 x10000000 0 x002000 EXEC ROM 0 x10000000 0 x002000 init o init First 可以看到 我们在这里没有 vectors o 因为不需要 RO RAM 0 x30080000 0 x7000 RW ZI STACKS 0 x30088000 0 x1000 stack o ZI 这是相应的 init s 的内容 ENTRY Perform ROM RAM remapping if required IF EF ROM RAM REMAP On reset an aliased copy of ROM is at 0 x0 Continue execution from real ROM rather than aliased copy LDR pc Instruct 2 Instruct 2 LDR r1 Remap ctl reg LDR r0 Remap value STR r0 r1 ENDIF Reset Handler IMPORT top of stacks defined in stack s and located by scatter file LDR r0 top of stacks MSR CPSR c Mode SVC OR I Bit OR F Bit No interrupts SUB sp r0 Offset SVC Stack IMPORT load firmware B load firmware END 我们可以看出来 基本上 runtime 的启动文件基本相同 只是最后跳转的地方 不一样 但是 这只是假象 由于 scatter 文件的不同 两个工程生成的可执行 文件有着根本的不同 runtime 那个在 0 地址储存的是向量表 供 cpu 在发生 异常时取用 而 bootloader 的在 0 地址直接就是顺序执行的初始化语句 因为 bootloader 只关心一件事 把 runtime image 以某种方式加载进来 如果有需 要的话还要解压 当这些做完之后再把控制权交给 runtime image 中的可执行 域就行了 至于以后的异常处理等等问题不在它考虑范围内 还有一个不同是我们用了 B load firmware 而不是 B main 这样做的一个 好处是链接时就不会把 ARM 的库函数包进来 能节省 1K 左右的 ROM 空间 而坏处就是你要仔细对待你的代码 因为缺少了一个初始化函数 所以内存中 的值就没有人给你赋值 也就是说你的代码中不允许有带初值的全局变量 这 一点切记 很多人在这上面吃亏 从 bootloader 到 main 函数之间一般要经历如下的步骤 1 地址空间转换 可选 2 初始化供 bootloader 的 c 代码使用的栈空间 3 等待从主机侧下载镜像文件或者直接从自己挂载的存储器拿 4 解析校验镜像 没问题的话将它放到预定的位置 有需要的话还得解 压 5 将 pc 指向镜像文件的入口 即镜像文件的 reset handler 二次复位 6 进行板级初始化的第一步骤 包括硬件 堆栈等等 7 控制权交给分散加载解释函数 完成软件系统的初始化 8 模式切换 从 svc 到 sys 最好有这一步 9 控制权交给 main 函数 需要注意的是 在一般情况下 整个这个过程中断是关闭的 除非你有特殊需 要 记住 这个部分越简单越直观就越好 现在开始进入大家都熟悉的部分 大多数人工程的开始点 main 函数 在 main 函数中主要进行以下工作 1 完成硬件初始化 2 软件数据结构的初始化 3 任务初始化 4 打开中断 关于打开中断要多说两句 ARM 中断的使能必须至少有两级 Cortex M 除外 不属于传统 ARM 中断控制器的使能和 CPSR 中 I 位的使能 而 CPSR 的使 能才是最根本的使能 很多新手会卡在这儿 不停地问为啥我初始化了中断控 制器却没有中断发生 在 ARM 架构中 中断控制器是 cpu 之外的东西 它只 是总线上一个可读写的模块 中断状态寄存器一般情况下会用 32 个状态位表 示最多 32 个中断源 不过这不是限制 可以做几级中断控制 这样就可以表 达更多的信息 所有已经使能的中断源的状态位或之后 连接到 CPU 最根本 的中断异常使能 如果此时 cpu 的中断异常也是使能的 就会激起 cpu 的异常 响应 这段描述听着有点别扭 学过数电的人应该还可以理解 当把任务 中断 优先级 以及消息队列 任务堆栈等都配置好以后 就可以 跑一些简单的操作系统了 类似于 ucos 这类普通操作系统的任务切换只要依 靠二个途径 1 任务主动让出 cpu 而导致低优先级任务得到执行 2 中断导致高优先级任务就绪 退出中断时高优先级任务获得执行权 因为 ucos 不存在同级优先级的任务 所以也就不存在时间片到时切换这种模 式 术语称为 Round Robin 或者简写为 RR 这样做是为了使操作系统的实 现简单化 但是在某些特定的场合确实让人很别扭 需要仔细设计以规避带来 的问题 Main 函数在设置完任务并调用了 os start 之类的函数后就退出了舞台 正 常情况下不会再返回到 main 函数 main 函数不属于操作系统 操作系统运转 起来之后就不需要它了 过河拆桥 而完整的保护式操作系统则是一个完全不一样的概念 为什么叫保护式操作系 统呢 个人理解至少有两个原因 1 每个进程只能看到自己的地址空间 其他进程的数据和代码都是不可 见 每个进程都拥有自己的世界 2 用户进程不能直接调用内核函数或者读写内核数据 必须通过 API 调 用这种间接方式向内核请求 在获得允许的情况下才能执行 可能有朋友会问 既然用户空间不能执行内核函数 那如何进行 API 调用呢 这种情况我们就需要 SWI 即软中断的帮助 从 cpu 的角度来看 用户空间调用 系统 API 的流程如下 1 按照寄存器参数传递规则 用户空间首先将需要的服务号 参数等填 入各个寄存器或者数据结构 此时工作于 usr 模式 2 激起 SWI 异常 3 CPU 进入 SVC 模式 检查服务号 参数等是否合法 若不合法则置 失败标识原因等 退出到用户模式 如若合法则会调用相应的系统服务函数 4 执行结束之后从 SVC 模式填好返回值 返回到用户模式 这种执行过程 保证了内核在没有 bug 的情况下 不会被恶意用户程序轻易获 得 cpu 特权从而为所欲为 当然没有 bug 的软件是不存在的 ARM 的状态切换是一个比较容易出问题的地方 ARM 为了实现效率和成本的 平衡 允许 cpu 在 32 位指令编码 ARM 和 16 位指令编码 THUMB 之间 切换 因为确实有些简单指令完全没必要用 32 位来实现 而在某些对性能要 求比较严格的场合 由于 16 位编码的限制 又导致了性能的下降 此时 cpu 就可以切换到 32 位去执行 公平地说 ARM 的这个设计是相当成功的 虽然我 更喜欢 MIPS 在以控制为主的系统中可以大量使用 THUMB 编码 只有在大 量计算的模块使用 ARM 编码 实现起来非常方便 在 ARMCC 编译器下 只 需要打开 interworking 选项 在需要 ARM 编码的模块标注 pragma ARM 在 需要 THUMB 编码的模块标注 pragma THUMB 就搞定了 C 语言编码级别的 ARM THUMB 态转换由编译器链接器插入一些称为 veneer 的小模块来解决 这些小模块在 c 层面上不可见 是 armcc 为了方便实现长跳转以及状态切换加 入的 状态切换从 cpu 指令级别来看 常用的有两种方式 1 通过某些特殊指令将 spsr 恢复到 cpsr 来实现状态切换 2 通过类似于 blx 或者 bx 这样的跳转指令 当目的地址的最低位为 1 时 切换到 THUMB 态 若其为 0 则应该切换到 ARM 态 这时你可能会问即使是 THUMB 态也是 16 位对齐 怎么会最低位为 0 不用担心 这个由链接器来处 理 它会把所有标记为 THUMB 态的函数地址的最低位置 1 这样就不用担心 了 当你编写自己的汇编语言代码 并且系统中存在 ARM THUMB 切换时 你就用 非常小心 这个时候全靠你自己了 因为不论是在 ARM 态下执行 THUMB 代 码 还是在 THUMB 态下执行 ARM 代码 都会遭遇一个问题 undefined instruction 也就是说 cpu 不认识那些指令了 某著名的嵌入式 os 的 2 89 版 就有这样的问题 在做中断处理的时候没有考虑到 ARM THUMB 混编的情况 导致某些情况下 cpu 在 ARM 态执行 THUMB 代码 等有空把他们本来的代 码和我修改的部分贴出来比较下大家就能看明白了 当时联系了他们的 FAE 也发给了他们的总部 不过一直没见修改 不知道现在怎么样了 昨天调了一整天的 bootloader 脑袋都快木了 跳出了一个又一个的坑 并且 目标板上没有提供 Jtag 接口 大大增加了调试难度 不过也由此想到了一些话 题 首先的一个话题是动态加载 以前的根 bootloader 和用于升级的 bootloader 使 用两套代码 自然 Makefile 也是两套 可是既然都是 bootloader 那么实际上 大部分代码都是相同的 使用两套代码以及 Makefile 就等于是两个工程 这样 会造成日后维护量的上升以及发生错误的可能性 按照多位软件大师的对于软 件质量的看法 在确定的软件需求之下 当你尽可能地让自己的代码简单 你 也就极大可能地减少了 bug 的数量 而且我也坚信一点 整个过程中如果需要 人为参与的工作量更少 日后出现弱智性错误的几率就越小 出于这些考虑 我接到这个需求之后就考虑把原有工程合并 但是首先需要解 决的一个问题就是要解决动态加载的问题 因为根 bootloader 是直接可读写的 所以升级用的 bootloader 不能使用与它重叠的地址空间 而如果使用同样的 Makefile 和分散加载 ARM 链接器里面是 scatter GNU 的链接器里面叫做 ld 或者 lds 文件 这样的话就出现了一个问题 升级用的 bootloader 和根 bootloader 编译时是一样的 执行时却在不一样的地址 这就要求升级用 bootloader 必须有一种灵活的机制 使自己能够在不同的地址上都能执行 直 观点说就是当 pc 从入口跳入执行之后 其内部的跳转都是相对跳转 并且不使 用固定地址的全局变量等 实际上 c 语言级别的实现很简单 只需要在编译选 项中加入一个开关 fpic 就行 开始时我想到了这点但是还是执行有问题 后 来又查汇编部分 发现在跳转时使用了 jal 指令 由于这几年做 arm 做多了 已经遗忘了 MIPS 中实现了单指令的绝对跳转 多次放过这条指令 后来去查 手册才知道应该改成 b 或者 bal 于是第一个坑算是解决了 不懂汇编急死人 啊 之后几天一直在于 GCC 做斗争 以后可能要长期使用这种编译器了 所以即 使我再不喜欢它 也要开始了解了 GCC 对 MIPS 的编译做了一些特殊的处理 有一些专门为 MIPS 的编译选项 这在 GNU 的官方网站上都可以查得到 我在这里简单说明一下 虽然不是 ARM 但是日后会和 ARM 做比较 在对 MIPS 的编译中 不能简单地使用 fpic 来达到位置无关的目的 最重要的 一个开关是 mabicalls 使能 System V r4 调用格式 使用了这个开关 并且 再搭配上 mshare 或者 fpic 才能达到位置无关的目的 而在链接时是否使用位 置无关选项并不重要 即使选择了静态链接 依然能够给出位置无关代码 还有一点需要注意的是如果你的工程是类似于 bootloader 这样的汇编代码和 c 代码混编的工程 就一定要注意处理好汇编与 c 代码间调用的接口 在 SVr4 针对 MIPS 的寄存器调用规则中 t9 寄存器保存当前函数的地址 这一点要切 记切记 所以在汇编代码跳往 c 代码之前 要把 t9 设置好 最好用如下格式的 跳转 la t9 start jalr t9 也一定要注意在这两句之前 要把 gp 寄存器的值设为你动态加载时 image 的 首地址 la t9 start 这句话会分解为几句汇编代码 利用 gp 寄存器的值查找 GOT 的位置 同时也要注意更新 GOT 的值 因为编译完的结果都只是相对于 0 地址的值 你要把这个值更新为你动态加载后的地址 这些内容不只是 MIPS 相关 所有系统的动态加载都会使用差不多的方法 而 ARM 的编译器在处理 这些问题的时候还是稍微聪明点 小范围内跳转都是直接使用相对偏移跳转 不会借助于 GOT MIPS 的专业编译器没有使用过 今天在公司的邮件中 几个大牛关于如何设计中断控制器有了争论 所以觉得 关于中断的话题 还是有必要再多说点 毕竟这对于系统的性能和稳定性至关 重要 RISC 架构中 包括 ARM 和 MIPS 一般情况下采用的清中断机制是软件手动 清理中断标志 而不是像早期 CISC 架构中所采用的响应中断后硬件自动清理 的方法 至于 CISC 现在用什么机制 已经完全没有概念了 所以在写 ARM 或者 MIPS 的中断处理程序时 一定要注意在退出中断前清理中断标志位 否 则就会在退出后再马上回来 形成一个死循环 在 ARM 的中断相应流程中 一旦 kernel 开始响应一个中断 就会自动将 CPSR 中的 I 位置位 也就是说此时不会再响应其他中断 此时没有中断嵌套 一说 除非软件在 ISR 中主动将 I 位清零 但是由于 ARM 多种模式机制的一 个问题 在某种极限情况下会导致简单嵌套处理无 常工作 如果想实现嵌套 需要写很多额外的处理代码 请注意 这里的 ARM 不包括 M 系列 完全不一 样的机制 ARM 公司提供了 VIC 模块 就是 vectored interrupt controller 向量中断控制 器 一定程度上实现了中断优先级处理 其机制是每种中断初始化的时候会将 自己这个中断的 ISR 地址通知 VIC 模块 当有中断产生时 VIC 模块会取此时 有中断产生的最高优先级中断的 ISR 地址填到一个寄存器中 在异常级别的中 断处理只需要简单的一句话 LDR PC Vector ISR 直接将这个寄存器的内容 写入到 pc 就实现了 ISR 的调用 既实现了优先级管理又加快了中断响应速度 但是要注意 即使实现了 VIC 如果不做特殊处理 依然没有中断嵌套 也就 是当你响应中断时 即使有更高优先级中断进来 也要等你的 ISR 执行完再说 ARM 核心中断响应的伪代码大致如下 这是我从软件人员的角度总结的 真正 的 ic 设计是并行的 在判断中断状态时是把所有中断线或在一起连接到异常级 别的中断线 if I bit in cleared if 0 interrupt status assert interrupt line to invoke exception 至于中断采取沿中断还是电平中断跟软件人员关系不大 一般情况下硬件人员 更喜欢采用电平方式 因为电平方式就不存在中断采不到的问题了 在 RISC 体系中 进入中断后的现场保护一般是由软件来完成的 如果是 C 代 码 编译器就会替你决定将哪些寄存器入栈 而如果你的中断处理函数是汇编 级别的 那就要处理哪些寄存器需要保护了 而且由于中断的执行非常频繁 你要是把所有寄存器都入栈将会是一个比较大的开销 正好合适是最好的 有 人可能要说 不管入栈多少个寄存器 我只需要一条 stm 就行了 很快的 这 只是个错觉 stm 指令的执行还是一个个内存单元的写操作 入栈多少个寄存 器 总线上就会有多少个针对内存单元的写操作 对于 DRAM 搭建的系统来说 这可是很慢的 当然 stm 有可能是 burst 写 比你多条指令写还是快一点的 cpu 在响应中断时 会将中断产生时的 PC 写入终端模式下的 lr 寄存器 并由 硬件自动将 cpsr 中的中断使能关闭 寄存器组切换到中断模式 寄存器组的切 换中最重要的是 sp 的切换 在某种模式下就会使用当前模式的栈指针 所以要 在 bootloader 或者 bsp 中初始化所有必要的模式的 sp 寄存器 下面讲解一些为什么 arm 实现中断嵌套会比较麻烦 我们首先描述一下一般情 况下其他架构怎么做 1 中断产生 ISR 开始服务中断 2 此时有更高级别中断产生 将当前 ISR 必要信息入栈 响应更高级别中断 3 高优先级中断处理完毕 将低优先级中断信息恢复 继续处理低优先级中断 而由于 arm 架构的特殊问题 导致在某种极端情况下 这种处理方式会出现致 命错误 请看如下描述 1 中断 int0 产生 ISR0 开始服务中断 2 修改 crpsr 使能中断 使之可以中断嵌套 3 ISR0 运行到某处进行一个函数调用 func0 由于此时在中断模式下 所以 在函数调用的时候会将函数调用的返回值填入到中断模式下的 lr 寄存器 4 在执行 bl 或者 blx 的瞬间 另外一个中断 int1 产生 此时 int1 中断会等待 bl 或者 blx 执行完毕 也就是被调用函数连一句都没有被执行到时就抢走 cpu 进入 ISR1 而此时硬件会自从将 lr 值修改为子函数中的地址 5 于是问题产生了 ISR0 中调用 func0 的返回地址 lr 在保存之前就被中断 isr1 给覆盖了 这导致 func0 中记录下来的 lr 是个错误的地址 程序因此也就 会乱套 这从根本上来说是 arm 在设计时的一个瑕疵 mips 用了一个非常聪明的设计解 决了这个问题 而 arm 不知道出于什么原因这么多年来始终没有修改这个问题 而只是用一种比较繁琐的方式去规避这个问题 明天继续讲 mips 的解决手段 和 arm 的规避方法 在 MIPS 中 对于这个问题的处理实际上非常简单 就是如果异常发生在跳转 指令执行的时候 那么在处理完异常返回之后会重新执行一遍这个跳转指令 也就是说如果异常发生在跳转指令上 那么跳转指令就被抛弃了 看似效率低 而正是由于这个简单的重新执行 避免了寄存器被复写的问题 为什么这种设 计就会防止上面那种问题的发生呢 我们可以模拟一下这个流程 1 中断 int0 产生 ISR0 开始服务中断 2 ISR 中使能中断 使之可以中断嵌套 3 ISR0 运行到某处进行一个函数调用 func0 对于 arm 来说 由于此时在中 断模式下 所以在函数调用的时候会将函数调用的返回值填入到中断模式下的 lr 寄存器 而对于 mips 来说 没有模式一说 都是使用通用寄存器 所以会将 返回地址填入 ra 4 在执行 bl 或者 blx 的瞬间 另外一个中断 int1 产生 此时 int1 中断会等待 跳转指令执行完毕 也就是被调用函数连一句都没有被执行到时就抢走 cpu 进入 ISR1 而此时硬件会自动将 lr 值修改为 ISR0 中跳转语句 8 的位置 对 于 MIPS 来说就是将 ra 值修改 5 对于 ARM 来说 ISR0 中调用 func0 的返回地址 lr 在保存之前就被中断 isr1 给覆盖了 程序因此也就会乱套 而对于 MIPS 来说则完全没有问题 因 为架构保证在跳转语句执行时发生异常的话 跳转语句就还要被执行一遍 从 根本上消除了这个问题 ARM 对这个问题的一种规避方法如下流程 1 中断响应进入 ISR0 2 在 ISR0
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 药店安全活动月活动方案
- 河工团员考试题及答案
- 贵阳会考试题及答案
- 古镇培训考试题及答案
- 供应专员考试题及答案
- 飞行原理考试题及答案
- 反诈骗考试题及答案
- 健康监测报告统计表
- (正式版)DB15∕T 3390-2024 《设施番茄灰叶斑病综合防控技术规程》
- 电务联锁考试题及答案
- 计算机安全知识试题
- 石墨材料生产工艺
- 成人雾化吸入护理团体标准
- 2025年彩焰蜡烛项目可行性研究报告
- 中医秋季养生宣讲
- 635MPa级热轧带肋高强钢筋应用技术规程
- 第三单元小数除法(单元复习讲义)教师版-2024-2025学年五年级上册(人教版)
- 人教版(2024新版)七年级上册英语Starter Unit1单元测试卷(含答案)
- 化粪池清底服务合同
- 幼儿园的食品安全工作计划
- (新教科版)科学五年级上册全册教学反思
评论
0/150
提交评论