首页 / 资讯中心 / 文章详情

ARM Cortex-A9体系结构深度解析:从流水线、缓存到运行模式

ARM Cortex-A9体系结构深度解析:从流水线、缓存到运行模式 ★ FEATURED ARTICLE
1. 从一颗芯片的性格说起为什么ARM Cortex-A9值得单独拎出来讲如果你拆开过任何一台2010年前后的智能手机、平板电脑、车载娱乐主机甚至某些工业控制面板大概率会在主板上看到一颗印着ARM字样的芯片。而Cortex-A9就是那个年代最耀眼的明星之一。它不是一颗具体的芯片型号而是一个处理器架构设计的模板——半导体厂商拿到这个模板之后可以加上自己的外设、缓存配置、总线接口做成各自品牌的SoC。这种授权模式正是ARM公司在嵌入式领域攻城略地的核心武器。很多初学者在接触嵌入式的时候容易把ARM和某款具体芯片混为一谈。比如有人会说我学的是ARM9其实他学的可能是三星的S3C2440那颗芯片的内核是ARM920T属于ARMv4T指令集架构。而Cortex-A9属于ARMv7-A架构是应用级处理器Application Profile的代表作。这两者之间的差距不是一代两代的问题而是设计理念上的根本性跨越。Cortex-A9支持乱序执行、超标量流水线、NEON SIMD扩展、TrustZone安全扩展这些特性在ARM9时代是完全不可想象的。我之所以想从Cortex-A9切入来讲ARM体系结构是因为它处在一个非常微妙的位置往前看它继承了ARM经典系列的精简哲学往后看它开启了Cortex-A系列高性能处理器的黄金时代。更关键的是Cortex-A9的设计资料相对丰富很多国产芯片厂商至今仍在使用基于Cortex-A9的IP核做工业级产品。对于嵌入式学习者来说搞懂Cortex-A9的体系结构等于拿到了一把理解现代ARM处理器的钥匙。这篇文章会从ARM的发展脉络讲起然后深入到Cortex-A9的内部结构、工作状态、运行模式和存储格式。我不会只给你干巴巴的定义而是会结合我在实际调试中遇到的情况把每个概念背后的为什么讲清楚。不管你是刚入门的嵌入式小白还是已经用过几款ARM芯片但没系统梳理过体系结构的老手应该都能从中找到有价值的东西。2. ARM的发展脉络从Acorn的一间小屋到全球嵌入式霸主2.1 从Acorn到ARM公司一个被迫创业的故事ARM的故事开始于1980年代的英国。当时一家叫Acorn Computers的公司想做一款性能强劲的个人电脑但市面上能找到的处理器要么太贵要么太慢。于是他们决定自己设计一颗处理器这就是后来的ARM1。有意思的是ARM这个名字最初是Acorn RISC Machine的缩写后来公司独立出来才改成Advanced RISC Machine。这里有一个关键的技术选择值得注意Acorn的工程师在设计ARM1时刻意追求了极致的精简。他们发现当时很多处理器的指令集非常复杂一条指令要做很多事情导致硬件电路庞大、功耗高、频率上不去。于是他们反其道而行之采用RISC精简指令集计算思路让每条指令只做一件简单的事但把流水线做得非常高效。这个设计哲学一直延续到今天。1990年ARM公司正式成立但它的商业模式非常独特自己不生产芯片只做处理器架构设计然后把设计授权给半导体厂商。厂商拿到授权后可以加入自己的外设和优化做成自己的芯片。这种模式让ARM的技术迅速扩散因为半导体厂商不需要从零开始设计处理器内核只需要专注于自己擅长的外设和系统集成。2.2 ARM架构的版本演进从v1到v7的漫长征途ARM架构的版本号从ARMv1一路演进到今天的ARMv9但真正在嵌入式领域产生深远影响的是ARMv4到ARMv7这几个版本。我整理了一个简表方便你快速建立时间线认知架构版本代表内核典型特征典型应用场景ARMv4TARM7TDMI三级流水线Thumb指令集早期嵌入式控制ARMv5TEARM9E五级流水线DSP扩展工业控制、网络设备ARMv6ARM11八级流水线SIMD扩展早期智能手机ARMv7-ACortex-A8/A9/A15乱序执行NEONTrustZone智能手机、平板、车载ARMv7-RCortex-R系列实时性优化低延迟中断汽车刹车、硬盘控制ARMv7-MCortex-M系列微控制器优化低功耗IoT设备、传感器节点从这张表可以看出ARMv7是一个分水岭。在ARMv7之前ARM内核主要面向嵌入式控制领域性能有限。到了ARMv7ARM公司把架构分成了三个ProfileAApplication、RReal-time、MMicrocontroller分别面向不同的应用场景。Cortex-A9就是ARMv7-A Profile下的第二代应用处理器内核第一代是Cortex-A8。2.3 为什么Cortex-A9是一个转折点Cortex-A8是ARM第一款支持乱序执行的应用处理器但它有一个明显的短板只能单核。Cortex-A9则引入了多核支持最多可以配置四个核心并且采用了更高效的超标量流水线设计。这意味着在同样的工艺节点下Cortex-A9可以做到更高的性能和更好的能效比。我在实际项目中用过基于Cortex-A9的双核芯片做工业网关跑Linux系统同时处理Modbus协议转换、数据加密和网络通信。那颗芯片的主频只有800MHz但双核配合NEON指令集做AES加密吞吐量完全能满足工业现场的需求。相比之下如果用早期的ARM11做同样的事情可能连基本的实时性都保证不了。Cortex-A9的另一个重要特性是支持TrustZone安全扩展。这个特性在嵌入式安全领域非常关键它允许处理器在硬件层面划分出安全世界和普通世界敏感操作比如密钥管理、安全启动在安全世界中执行普通应用程序无法访问。我在做支付终端项目时就是利用TrustZone来保护密钥的比纯软件方案可靠得多。3. Cortex-A9的内部结构流水线、缓存与多核协同3.1 超标量乱序流水线让指令并行奔跑Cortex-A9的流水线设计是它性能的核心来源。与早期ARM内核的简单顺序流水线不同Cortex-A9采用了超标量乱序执行架构。这两个词听起来很专业但用生活化的类比就很好理解。想象一个餐厅厨房。顺序流水线就像只有一个厨师他必须按照订单顺序做完一道菜再做下一道。如果第一道菜需要炖煮他就只能等着后面的菜都做不了。而超标量乱序流水线就像有多个厨师而且他们可以灵活调整做菜顺序先把需要炖煮的菜放到火上然后利用等待时间去做凉菜最后再回来处理炖菜。这样整体效率就高多了。具体到Cortex-A9它的流水线分为多个阶段包括取指、译码、寄存器重命名、发射、执行、写回等。指令在译码后会被拆分成微操作然后根据数据依赖关系和执行单元的空闲情况动态调度。这种设计让Cortex-A9在同样的时钟频率下能比顺序执行的内核完成更多的工作。但乱序执行也带来了一个麻烦指令的执行顺序被打乱了但程序逻辑要求某些操作必须按顺序完成。比如你写了一个寄存器然后马上读它读到的必须是新值。Cortex-A9通过寄存器重命名和重排序缓冲区来解决这个问题保证最终结果和顺序执行一致。这套机制在硬件层面是透明的程序员不需要手动干预但在写汇编代码做极致优化时理解这套机制能帮你写出更高效的代码。3.2 缓存层次L1、L2与数据一致性Cortex-A9的缓存设计也是值得细说的。每个核心有自己私有的L1指令缓存和L1数据缓存通常各32KB。然后多个核心共享一个L2缓存容量可以配置常见的是512KB或1MB。L1缓存的作用是减少核心访问内存的延迟。你可以把L1缓存想象成你办公桌上的抽屉常用的文件放在抽屉里伸手就能拿到。L2缓存则像是办公室里的文件柜比抽屉远一点但比去档案室主存快得多。在多核场景下缓存一致性是一个必须解决的问题。假设核心0修改了地址A的数据这个数据在核心0的L1缓存里被更新了但核心1的L1缓存里可能还保留着旧值。如果核心1去读地址A就会读到错误的数据。Cortex-A9通过MESI协议来维护缓存一致性当一个核心修改了某个缓存行其他核心中对应的缓存行会被标记为无效下次读取时会从L2或主存重新加载。我在调试一个双核通信问题时就遇到过缓存一致性的坑。两个核心通过共享内存传递数据核心0写完数据后发中断给核心1核心1读到的却是旧数据。后来发现是核心0的写操作还在写缓冲区里没刷到L2核心1就已经去读了。解决办法是在写完之后加一个内存屏障指令DMB确保写操作对全局可见后再发中断。这个坑在单核系统里是遇不到的只有多核才会暴露出来。3.3 多核协同SCU与ACPCortex-A9的多核协同主要靠两个组件SCUSnoop Control Unit和ACPAccelerator Coherency Port。SCU负责维护多核之间的缓存一致性。它监听每个核心的L1缓存访问请求当发现某个核心要访问的数据在其他核心的缓存里被修改过就会协调数据的转移或无效化。SCU还负责管理核心之间的中断分发比如一个核心可以通过SCU向其他核心发送软件生成的中断SGI。ACP是一个可选接口允许外部加速器比如DMA控制器或GPU以缓存一致的方式访问主存。没有ACP的话DMA传输的数据可能和CPU缓存里的数据不一致需要软件手动做缓存维护操作。有了ACPDMA控制器可以直接参与缓存一致性协议大大简化了驱动程序的编写。我在做视频采集项目时用ACP让DMA控制器直接把摄像头数据写入内存CPU核心通过缓存读取时自动获得最新数据省去了手动刷缓存的步骤。不过ACP也有代价它会增加总线流量和功耗所以在低功耗场景下需要权衡是否启用。4. 工作状态与运行模式处理器在不同场景下的身份切换4.1 两种工作状态ARM状态与Thumb状态Cortex-A9支持两种主要的工作状态ARM状态和Thumb状态。这两种状态的区别在于指令的编码长度。ARM状态下所有指令都是32位宽。这种编码方式的好处是指令功能强大一条指令能完成复杂的操作但代码密度低同样的功能占用的存储空间大。Thumb状态下指令是16位宽Thumb-2扩展后支持16位和32位混合代码密度高但单条指令的功能相对受限。你可以把ARM状态想象成精装版——每页字大、内容详细但书很厚Thumb状态则是口袋版——字小、内容精简但携带方便。在实际嵌入式开发中如果存储空间紧张比如有些芯片的片上Flash只有几百KB用Thumb状态能省下不少空间。如果追求极致性能关键代码段可以用ARM状态。Cortex-A9还支持ThumbEE状态也叫Jazelle RCT这是专门为托管运行时环境比如Java虚拟机设计的。不过这个特性在实际项目中用得不多大多数嵌入式Linux系统都跑在ARM或Thumb状态下。状态切换是通过BX、BLX等分支指令实现的。比如你用BX指令跳转到一个地址如果目标地址的最低位是1处理器就切换到Thumb状态如果是0就保持在ARM状态。这个设计很巧妙因为指令地址总是半字对齐的最低位本来不用于寻址正好拿来当状态标志。4.2 八种运行模式特权级别的精细划分Cortex-A9有八种运行模式每种模式有不同的权限和用途。这个设计是为了在硬件层面支持操作系统的特权级隔离。运行模式缩写权限级别典型用途用户模式USR非特权普通应用程序系统模式SYS特权操作系统内核管理模式SVC特权复位和软中断中止模式ABT特权内存访问异常未定义模式UND特权未定义指令异常中断模式IRQ特权普通中断快速中断模式FIQ特权高速中断监控模式MON特权TrustZone安全监控用户模式是唯一非特权的模式应用程序运行在这个模式下不能直接访问硬件资源也不能修改关键的系统寄存器。如果应用程序需要操作系统提供服务必须通过系统调用通常是SVC指令切换到管理模式由内核代为执行。FIQ模式比较特殊它有自己独立的寄存器组R8到R12中断发生时不需要保存这些寄存器所以响应速度比IRQ模式快。我在做高速数据采集时就把最紧急的中断分配到FIQ确保数据不丢失。监控模式是TrustZone引入的用于在安全世界和普通世界之间切换。当普通世界需要调用安全世界的服务时会触发监控模式异常由安全监控程序决定是否允许切换。这个模式的存在让Cortex-A9能在硬件层面实现安全隔离比纯软件方案可靠得多。4.3 模式切换的实际场景理解运行模式最好的方式是看一个实际的切换流程。假设一个用户程序要读取一个文件程序在用户模式下运行调用read()库函数。库函数最终执行一条SVC指令触发管理模式异常。处理器切换到管理模式跳转到异常向量表中的SVC入口。内核的SVC处理程序保存用户程序的上下文寄存器、返回地址等。内核根据系统调用号执行相应的文件读取操作。操作完成后内核恢复用户程序的上下文切换回用户模式。用户程序继续执行拿到读取结果。这个流程在每次系统调用时都会发生虽然看起来步骤很多但硬件切换的速度非常快通常只需要几十个时钟周期。我在做性能分析时测过一次简单的系统调用开销大约在几百纳秒级别对于大多数应用来说完全可以接受。5. 存储格式大小端、对齐与数据类型5.1 大小端模式一个看似简单却容易踩坑的问题Cortex-A9支持大端和小端两种存储格式具体用哪种由芯片设计时决定也可以通过软件配置切换。大小端的区别在于多字节数据在内存中的排列顺序。小端模式下最低有效字节存放在最低地址。比如一个32位整数0x12345678在内存中从低地址到高地址依次是0x78、0x56、0x34、0x12。大端模式则相反最低地址存放最高有效字节依次是0x12、0x34、0x56、0x78。大多数嵌入式Linux系统默认使用小端模式因为x86也是小端网络协议和文件格式的移植比较方便。但有些网络协议比如TCP/IP的头部字段规定使用大端所以在处理网络数据时需要做字节序转换。我在调试一个串口通信协议时就因为大小端问题浪费了半天时间。发送方是大端设备接收方是小端处理器我直接把接收到的四字节数据当成整数解析结果数值完全不对。后来用ntohl()函数做了转换才正常。这个教训告诉我只要涉及跨设备的数据交换第一件事就是确认双方的字节序。5.2 数据对齐性能与正确性的双重考量Cortex-A9对数据对齐有明确要求。所谓对齐是指数据的地址必须是其大小的整数倍。比如一个32位整数地址应该是4的倍数一个16位短整数地址应该是2的倍数。如果访问未对齐的数据Cortex-A9的行为取决于具体配置。在某些配置下未对齐访问会触发异常对齐错误导致程序崩溃在另一些配置下硬件会自动处理未对齐访问但性能会下降。这是因为未对齐访问可能需要两次内存访问才能完成而且可能跨越缓存行边界。我在写DMA驱动程序时就特别注意缓冲区的对齐。DMA控制器通常要求源地址和目的地址都按特定边界对齐比如32字节或64字节否则传输效率会大打折扣甚至无法正常工作。解决办法是用__attribute__((aligned(64)))来声明缓冲区或者用posix_memalign()动态分配对齐内存。5.3 数据类型与寄存器宽度Cortex-A9的通用寄存器是32位宽支持的数据类型包括字节8位有符号和无符号半字16位有符号和无符号字32位有符号和无符号双字64位需要两个寄存器配合在汇编层面加载和存储指令需要指定数据类型。比如LDRB加载一个字节LDRH加载一个半字LDR加载一个字。如果数据类型和指令不匹配可能会读到错误的值或者触发异常。C语言中的数据类型映射到ARM汇编时int通常是32位short是16位char是8位long在32位ARM上是32位long long是64位。这些映射关系在写跨平台代码时很重要因为不同架构的long可能宽度不同。我在移植代码时习惯用stdint.h里的int32_t、uint16_t这类明确宽度的类型避免因为平台差异导致问题。6. 实操中的经验与避坑指南6.1 启动流程中的模式切换陷阱Cortex-A9上电复位后处理器从管理模式开始执行PC指向复位向量。这个时候缓存和MMU通常都是关闭的代码直接跑在物理地址上。很多初学者在写启动代码时容易忽略模式切换的顺序问题。我见过一个典型的错误在还没有设置好栈指针的时候就使能了中断。结果中断一来处理器切换到IRQ模式但IRQ模式的栈指针还是默认值通常是0导致压栈操作写到了非法地址系统直接跑飞。正确的做法是在使能中断之前先为所有可能用到的模式设置好栈指针。另一个常见问题是模式切换时的寄存器保存。从用户模式切换到管理模式时某些寄存器是共享的某些是模式私有的。如果切换前没有保存共享寄存器的值切换回来后就找不到了。ARM架构通过分组寄存器来解决这个问题但程序员需要清楚哪些寄存器是分组的哪些是共享的。6.2 缓存维护的时机与方式在Cortex-A9上做DMA传输时缓存维护是绕不开的环节。如果DMA要读取CPU刚写入的数据必须先把缓存里的数据刷到主存clean操作如果DMA要写入数据到内存而CPU缓存里可能有旧数据必须先无效化对应的缓存行invalidate操作。刷缓存和无效化缓存的粒度是缓存行Cortex-A9的缓存行通常是32字节或64字节。如果只刷了一个字节硬件实际上会把整个缓存行都刷出去。所以做缓存维护时地址要对齐到缓存行边界长度要按缓存行大小对齐否则可能影响相邻数据。我在调试一个音频播放问题时遇到声音断断续续的情况。排查后发现是DMA缓冲区没有做缓存维护CPU写入的音频数据还在缓存里DMA读到的还是旧数据。加上clean操作后问题解决。这个坑的隐蔽性在于单次传输可能碰巧正确因为缓存刚好被其他操作刷出去了但长时间运行后必然出问题。6.3 大小端配置的注意事项虽然Cortex-A9支持大小端切换但在实际项目中我强烈建议不要随意切换字节序。原因很简单一旦切换所有已经编译好的代码、数据结构、协议解析逻辑都可能需要调整。而且很多外设的寄存器定义是固定字节序的切换后读写会出错。如果确实需要处理不同字节序的数据正确的做法是在数据交换的边界做转换而不是改变整个处理器的字节序。比如网络通信时用htonl()和ntohl()做转换文件读写时按照文件格式规定的字节序解析。这样处理器的字节序保持不变代码的可维护性更好。6.4 运行模式与中断优先级的配合Cortex-A9的中断分为IRQ和FIQ两类FIQ优先级更高。在实际项目中合理分配中断类型能显著改善系统响应。我的经验是把对时间最敏感的中断分配到FIQ比如高速ADC采样完成、电机编码器脉冲捕获等。这些中断的处理程序要尽量短小只做最紧急的数据搬运把复杂处理留给IRQ或任务上下文。FIQ模式下有独立的寄存器组切换开销小适合高频中断。但要注意FIQ只有一个如果多个中断都抢FIQ反而会互相干扰。所以FIQ应该留给真正最紧急的那一个中断源。其他中断按优先级分配到IRQ通过中断控制器的优先级寄存器来管理。7. 从体系结构到实际开发一些个人体会搞懂Cortex-A9的体系结构最大的价值不是应付考试或面试而是在遇到问题时能快速定位到硬件层面的原因。我见过太多开发者遇到多核数据不一致就怀疑操作系统有bug遇到未对齐访问崩溃就以为是编译器问题其实根源都在体系结构的细节里。举个例子有一次同事调试一个多线程程序两个线程分别跑在两个核心上共享一个计数器。加了锁之后计数结果还是不对。他查了半天锁的实现最后发现是编译器把计数器变量优化到了寄存器里两个核心各自操作自己的寄存器副本锁根本保护不到。解决办法是用volatile关键字或者内存屏障强制每次访问都从内存读取。这个问题如果不懂缓存一致性和编译器优化是很难想到的。另一个体会是体系结构的知识能帮你写出更高效的代码。比如知道Cortex-A9的缓存行是64字节你就会注意把频繁访问的数据结构对齐到缓存行边界避免伪共享false sharing。知道NEON是128位SIMD你就会把图像处理、音频编解码的循环改写成向量化形式性能提升可能有好几倍。嵌入式开发这个领域越往底层走体系结构的重要性越明显。上层的应用框架、中间件会变但底层的处理器架构原理是相对稳定的。花时间把Cortex-A9的体系结构吃透后面再看Cortex-A53、A72甚至更新的内核会发现很多设计思想是一脉相承的。最后分享一个我常用的学习方法对着芯片手册和架构参考手册把关键概念用自己的话写一遍然后画一张图把各个概念之间的关系串起来。比如把工作状态、运行模式、寄存器组、异常向量表画在一张图上标注它们之间的切换关系。这个过程能帮你发现理解上的盲区比单纯读文档有效得多。
阅读完成 · 觉得有帮助?
咨询建站