ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SoC存储体系详解:从寄存器到UFS的类型差异与工程实践

SoC存储体系详解:从寄存器到UFS的类型差异与工程实践 最近在嵌入式论坛和各类测评区泡久了发现一个很有意思的现象大家聊SoC十有八九在聊跑分、天梯图、核心数量、GPU频率而真正影响系统下限的“存储系统”却很少有人系统讲清楚。我做过几年的SoC验证与嵌入式驱动相关的工作踩过很多存储相关的坑——启动假死、cache不命中导致性能崩掉、DDR训练失败、eMMC和UFS选型失误等等——这些问题的根子几乎全都埋在“SoC各类存储类型、用途及核心差异”这个看似基础的话题里。这篇就把我脑子里的那本账翻出来从寄存器一路讲到UFS把容量、延迟、用途和那些文档里不会写的实操心得一次说透。1. SoC存储整体拓扑这笔账的源头1.1 一张“存储金字塔”看清层级芯片设计领域有个经典的存储金字塔从塔尖到塔底依次是寄存器、Cache、片上SRAM/TCM、DRAM、以及NAND/eMMC/UFS这类非易失存储。塔尖越快、越贵、容量越小塔底则相反。这个金字塔的本质是“速度”和“经济性”之间的反复博弈。拿我前几年做的一颗车规级SoC来说CPU核是四个Cortex-A55每个核有32KB一级指令Cache和32KB一级数据Cache四个核共享一个1MB的二级Cache。核里面大概有几十个通用寄存器编译器把它们当作最热门的临时变量仓库。芯片外面挂的是两颗LPDDR4总共4GB容量带宽大概能做到十几GB/s。再往外是板载的一片eMMC用来存系统镜像和用户数据。整套体系从寄存器到eMMC延迟差了好几个数量级寄存器访问也就是一两个纳秒DRAM访问要到几十到一百纳秒eMMC随机写在最差情况下甚至可能到几十毫秒。这里有个新手最容易懵的点很多人以为“存储”就是“内存”。实际上CPU里面那几十个寄存器也是存储Cache也是存储甚至芯片里的Boot ROM、一次性可编程的OTP熔丝都是存储。不同存储的形态、访问方式和生命周期完全不同。把它们全部划进“内存”这个概念里去理解后面做性能分析的时候一定会错得离谱。金字塔模型还有一个工程意义它决定了数据流动的方向。程序运行时指令和数据先从硬盘或闪存被搬到DRAM再被Cache缓存最后被CPU取到寄存器里计算。任何一级存储的命中率或带宽出了问题都会成为整条链路的瓶颈。我见过不少团队优化性能时只顾着看CPU频率把存储层次完全忽略结果是频率拉高了百分之二十整体性能反而没动——因为瓶颈在DDR带宽或者Cache miss上。1.2 不同存储的关键参数对照与选择逻辑表格是理解存储差异最直接的载体。我把自己在实际项目中常用的对照表贴出来这些数字不是教科书上的理想值而是我实测过的量级范围存储介质典型容量访问延迟主要特点CPU寄存器几十到几百字节不到1ns最快需编译器和指令显式管理L1 Cache32KB~1MB约1ns极快与CPU频率同步L2 Cache256KB~16MB3~10ns片上SRAM实现多核共享TCM紧耦合内存128KB~几MB1~2个CPU周期确定性访问无Cache一致性开销片上SRAM几十KB~几十MB2~5ns灵活性好可被DMA和外设直接访问DRAM/DDR512MB~几十GB80~120ns容量和成本折中系统主存NOR Flash1MB~256MB读约100ns支持片内执行XIP常用于启动eMMC4GB~512GB读约100us级写毫秒级封装NAND控制器移动设备标配UFS32GB~1TB顺序读写可到GB/s级高性能外存支持命令队列OTP/Fuse几KB到几十KB类似寄存器访问一次性可编程存密钥和配置这张表的价值在于选型。我每次在设计一个新系统时第一件事是先按这张表把硬件上的存储层次列出来然后问三个问题启动时最前面的几百KB代码放在哪运行时的关键数据需要多快的确定性访问掉电后哪些数据必须保存这三个问题的答案基本就圈定了存储选型的方向。选择背后还有成本因素。同样是1MB存储放在Cache里和放在片上SRAM里的工艺成本差一大截。DRAM虽然便宜但它是外部器件需要控制器和物理层还会占用PCB空间和功耗预算。所以SoC设计本质上是在“性能需求”和“成本利润”之间做取舍不存在绝对最好的存储只有最合适的组合。2. 易失性存储让CPU高速运转的内部机制2.1 寄存器与流水线缓冲微观世界里的快与慢寄存器和流水线缓冲是整个存储体系里最容易被忽略、却又最致命的一层。寄存器文件通常由几十个入口组成每个入口宽度与CPU字长一致比如64位架构下每个寄存器能存8字节。它们被安排在CPU核的最核心区域离运算单元只有几十微米中间没有什么复杂的总线协议所以访问延迟几乎可以忽略不计。但寄存器数量极其有限编译器必须不停做“寄存器溢出”的操作——把暂时用不到的数据搬回Cache或者栈内存里等需要时再读回来。这个溢出操作如果太频繁性能会成倍下降。我在做性能分析的时候常看两个指标每周期执行的指令数IPC以及Cache miss率。如果发现IPC异常低第一反应就是去看汇编里是不是有大量的内存读写指令在挤占寄存器资源。有一次为了优化一段矩阵运算代码把循环展开后临时变量暴增结果寄存器溢出严重性能反而比原来更差。最后强制编译器把循环体拆小反而从2.1 IPC涨到了2.8 IPC。这里还要提一个概念写缓冲器。CPU在写回数据时不会每次都直接穿透到内存而是先写进一个小的写缓冲队列再由内存子系统负责把数据刷出去。写缓冲器本质上也是存储在一些总线仲裁和Cache一致性场景里它的深度和刷新策略直接影响性能峰值和极端情况下的行为。2.2 SRAM与Cache容量和延迟折中的产物SRAM是静态随机存储靠触发器锁存数据只要有电就记得住不需要像DRAM那样反复刷新。它的特点是速度快、接口简单但面积大、成本高所以SoC里只舍得把它用在最核心的场景Cache、片上缓冲、以及各类硬件队列。Cache是SRAM用得最经典的地方。一级Cache跟CPU核同步时钟延迟控制在1纳秒左右容量通常在几十KB。二级Cache放在片上几个核共享容量到几MB延迟上升到几个纳秒。三级Cache在一些PC级SoC上才会出现容量能到几十MB已经是众多核共享的“大仓库”。Cache的关键在于命中率。假设IPC是2.5时钟是2GHz那么5纳秒左右就执行完一条指令。如果数据在Cache里没命中需要去DRAM拿白白浪费一百纳秒相当于一次cache miss白白耗掉了二十条指令的预算。所以Cache不仅仅是个存储它背后的替换算法像LRU、预取器、非阻塞Cache设计都是掩盖DRAM延迟的手段。做底层开发的人绝对绕不开Cache的维护操作。Cortex-A系列上常见的clean和invalidate操作就是告诉Cache“这行数据你得刷回内存”或者“这行数据作废下次别用”。如果驱动DMA传输前忘了clean外设读到的是Cache里的旧数据DMA写完后忘了invalidateCPU又读到了旧缓存表现就是数据传输老是错乱。这种问题不是每次必现往往跑几十万次才出现一次调试起来极其上头。2.3 TCM里的隐蔽角色确定性访问的保障TCM全称紧耦合内存是SRAM的一种特殊形态。它与普通SRAM最大的差异是不走Cache路径也不做一致性协议处理直接挂在CPU的私属总线上因此访问延迟固定、可预测。为什么需要这种“不走Cache”的存储因为在实时性要求极高的场景里Cache的行为是不可预测的。Cache命中了访问快Cache不命中就要去外面搬数据时间抖动极大。对于中断处理程序、实时控制代码、启动早期代码这类“must run”的场景你宁可牺牲一点点平均性能也要保证最坏情况下的延迟确定。Cortex-M系列里TCM非常常见但很多人开发Cortex-A时反而忽略了它。ARM核其实可以通过内存映射把一部分地址空间指定为“不可缓存”区域配合TCM使用。我做底层引导时习惯把启动早期阶段最关键的初始化代码放到TCM里跑这样能保证不依赖DDR初始化状态和Cache配置先把存储控制器最快地拉起来。2.4 DRAM给系统兜底的那个“大力士”DRAM之所以能成为系统主存核心原因是容量与成本的平衡。DRAM每个单元只需要一个晶体管加一个电容单位bit的制造成本远低于SRAM所以能轻松做到几个GB的容量。代价是访问DRAM需要经过复杂的时序先激活行再读列然后预充电中间还包括刷新操作来防止电容漏电丢数据。这些时序加起来就是几十到上百纳秒的延迟。DRAM的性能不仅取决于颗粒本身还严重依赖SoC里的DDR控制器和物理层PHY。控制器负责把CPU和总线发来的访问请求转换成DRAM时序PC里叫内存控制器SoC里同样如此。控制器里面还有一堆调度策略比如Bank组的选择、读写切换、优先级仲裁。这些策略写得好不好直接决定实际带宽是否能接近理论峰值。我调过一颗支持LPDDR4的SoC硬件板子回来后最先要干的事就是做DDR训练。训练说白了就是要找到CPU和颗粒之间最佳的读写采样窗口——因为信号在PCB走线、DDR颗粒参数之影响下会有波动不训练数据就可能采错。整个训练流程包括ZQ校准、读写延迟调整、电压等级设置等任何一步出错系统要么无法完成自检要么在高压低温测试时随机宕机。这个环节非常考验耐心也是最容易被新手忽略的“新板必踩坑”环节。3. 非易失性存储系统上电后的第一块拼图3.1 NOR、NAND、eMMC、UFS外存五兄弟怎么选非易失性存储的世界里NOR Flash、NAND Flash、eMMC、UFS、以及更传统的SD卡因为接口和用途不同活成了完全不同风格的五兄弟。NOR Flash最突出的特点是支持“片内执行”缩写XIP。它可以直接连接到总线上 CPU能像读内存一样直接读NOR里面的代码不用先把代码拷贝到RAM里。这在系统启动早期特别关键因为那时候DDR可能还没完成初始化外部大容量存储控制器也没就绪CPU唯一能可靠执行的代码就放在NOR或Boot ROM里。NOR的缺点是写慢、擦除粒度大、容量做不大所以只适合存启动代码和一小部分关键参数不适合当主力存储。NAND Flash则是另一套逻辑存储密度高、单位容量成本低但需要按页读写、按块擦除还必须有坏块管理和纠错算法。因为NAND裸片本身不保证数据的长期可靠性SoC或外部控制器必须有ECC校验能力否则时间一长位翻转会越来越多。把NAND控制逻辑封装成标准接口就成了eMMC内部集成了NAND Flash和MMC控制器外部通过SD/MMC协议访问。eMMC的好处是宿主系统只需要处理标准命令坏块管理、磨损均衡、ECC都由颗粒里的控制器负责大大简化了软件。UFS则更进一步它采用串行高速接口支持全双工和命令队列顺序读性能能跑到每秒上千兆比特随机访问也远好于eMMC。这也是为什么这几年手机SoC平台已经把存储配置从eMMC升级到UFS 3.1甚至UFS 4.0因为系统响应速度、App启动速度、甚至游戏贴图加载速度都直接受外存影响。eMMC时代加载一个大型游戏可能要几十秒UFS 4.0时代可以把等待感压到几秒内。3.2 Boot ROM启动流程SoC开机后的第一步聊完非易失存储必须回到SoC最玄妙的环节上电后CPU到底从哪里执行第一条指令。大部分SoC把第一段不可更改的程序固化在芯片内部的一小块掩膜ROM里叫Boot ROM。CPU上电后PC指针直接指向Boot ROM地址开始执行里面的代码。Boot ROM里面会检测启动引脚的电平状态或者读取OTP熔丝里的启动源配置决定接下来从NOR、SD卡、eMMC、UFS还是SPI接口加载下一级引导程序。这里有个常规文档很少提的设计细节Boot ROM代码在启动早期会把自身需要的临时数据放在片上SRAM里因为在DDR还没有初始化之前外部DRAM一片混沌根本不能访问。片上SRAM在这里充当的是“早期临时存储”的角色。Boot ROM去外存读一个固定长度的引导镜像把它加载到SRAM中校验签名或CRC后跳转执行。这个镜像通常叫Bootloader比如U-Boot阶段一它负责初始化DDR、时钟、存储控制器等关键外设然后再把真正的系统内核从eMMC/UFS里加载到DDR中运行。我在调试自己的Linux开发板时高频率碰到“似乎断电重启后随机卡住”的情况。查来查去最终定位在Boot ROM从eMMC读镜像时因电源波动导致数据读取失败而Boot ROM又没有重试机制于是死等。对策就是检查供电时序、降低启动时钟频率或者强化存储电源纹波。这些坑不在真实硬件上跑一段时间根本体会不到。3.3 OTP与安全熔丝出厂就写死的隐形存储SoC里还有一种常被忽略的非易失存储OTP全称一次性可编程存储经常做成熔丝或反熔丝结构。它的特点是出厂后只能写入一次之后永久锁定所以天然适合存密钥、启动源配置、芯片唯一ID、安全策略开关这些“必须不可篡改”的数据。OTP的容量普遍很小常见几KB到几十KB。你可能觉得这么点空间能干什么但它承载的东西分量极重。比如安全启动用的根公钥哈希比如DRAM训练参数备份比如是否禁止从UART刷机的安全标志。如果攻击者能改OTP整个信任链立刻崩塌反之只要OTP不可被软件重写芯片的信任根就能建立起来。我做过一次OTP烧写的项目整个过程非常考验细心烧写前要把所有需要固化的值列成表格逐位核对烧写时使用专门的工具通过JTAG或专用接口写入烧完后再读回来比对。一旦烧错某个bit这颗芯片在某些场景下就等于报废——因为无法回退。所以量产前我们会在多颗样片上反复验证OTP内容的正确性绝不直接在最后的批次上试错。4. 安全架构与系统优化存储差异化的工程思考4.1 安全世界里的存储隔离现代SoC几乎都标配了某种安全隔离机制ARM体系下最常见的是TrustZone它把芯片的运行环境分成安全世界和普通世界。存储在这个架构里不是简单按地址划分而是需要完整的硬件隔离策略。安全世界的代码和普通世界的代码虽然跑在同一个CPU核上但安全世界访问的内存区域在普通世界眼中是完全不可见的。Cache和TLB专门有安全位标记防止普通世界通过缓存侧信道偷看安全数据。SRAM可能被划分出安全SRAM区外存也可能有加密区——密钥在写数据时由芯片内部加密引擎处理配合OTP里存储的根密钥实现硬件级别的数据保护。我做安全启动验证的时候最常干的事就是检查内存区域的访问权限矩阵。比如某个内存区间配置为“仅安全世界可读”那么普通世界一旦发起读请求总线就应该返回错误。这个错误不只是软件层的访问拒绝硬件总线事务层就要拦截得住才行。测试需要用专门的测试样例去遍历各种组合普通读、普通写、DMA读、调试接口访问等等。任何一条路径没堵住都可能成为整个安全体系里的漏洞。4.2 Cache一致性与总线事务的复杂账SoC内部不只是CPU在访问存储还有GPU、NPU、音视频编解码器、DMA等大量外设也在读写内存。多主设备共享同一块内存Cache一致性就成了绕不开的话题。一致性协议保证的是“某个CPU改了数据其他需要访问该数据的设备能看到最新值”。常见做法是一致性互连比如ARM的CCI或CMN总线它会在各Cache之间广播一致性消息。外设如果支持一致性接口可以直接接入这个互连网络不支持的话CPU必须通过显式的clean和invalidate操作来配合否则数据会“各看一眼”。总线事务层的细节也很关键。AXI协议里读写事务有独立的通道还有burst模式、outstanding能力、时延容忍度等参数。这些参数直接决定了存储访问的实际效率。比如某个NPU要连续读取一大块特征图如果它的访问请求outstanding数量设得低总线流水线就难以打满性能会直接卡在事务发起速度上而不是内存带宽上。我做过一次性能摸底同样一个神经网络推理任务把NPU的总线outstanding配置从4提到32端到端延迟直接降低了将近百分之三十。这种优化不做光看主频和带宽指标永远找不到瓶颈。4.3 选型思考eMMC还是UFSDDR还是LPDDR到了产品定义阶段关于存储的选择通常不是技术人员的个人偏好而是由功耗、成本、散热、供应链综合决定的。我参与过好几款智能硬件产品的存储选型评审最常被问到的就是“为什么用LPDDR而不用DDR”。LPDDR是低功耗版DRAM电压更低、内部架构针对移动省电优化适合电池供电的设备。DDR则更适合对功耗不太敏感的网关、服务器、工控机。两者的引脚定义和初始化时序还不一样选错会让整块PCB大改。类似地eMMC适合成本敏感的入门设备UFS则适合性能敏感的中高端设备起步容量差异、功耗差异、控制器复杂度差异都要一起评估。从我的经验看选型切忌只看理论带宽。UFS虽然顺序性能远快于eMMC但如果系统主控端存储控制器有缺陷或者驱动没有开启命令队列实际体验可能和高端eMMC差不多。硬件选型的决策必须在拿到“参考设计和真实测试数据”之后再拍板不能光看宣传页上的数字。5. 实践踩坑与性能排查实录5.1 启动阶段的“假死”问题排查启动假死是我在嵌入式开发里碰过最多的疑难杂症。所谓假死就是系统看起来完全没反应但电源、时钟都是正常的CPU并没有真死只是卡在某一步等一个永远不会来的数据。有一次排查Linux开发板偶发无法启动从Boot ROM到U-Boot再到内核每一步都有打印还是偶发挂掉。后来我在U-Boot里增加了时戳打印发现卡在DDR控制器初始化返回后的第一次内存访问上。进一步查是低温环境下DDR训练结果不稳定训练时采样的窗口和后续实际运行环境不一致导致冷启动时第一个内存读就失败。解决方向有两个一是修改训练流程增加重复训练和电压裕量验证二是改软件在DDR初始化后执行一段内存自检模式发现异常就重新训练。最终是做完整验证后把训练参数适配到了实际工作温度范围。启动排查还有一个重点早期调试输出依赖硬件引脚比如JTAG或串口而Boot ROM阶段往往没有来得及初始化调试口。这时就需要芯片提供的“调试模式”或者“启动恢复模式”。很多SoC允许通过OTP或引脚配置强制从特殊介质启动比如从USB下载模式进入烧写流程。遇到Boot ROM损坏或外存损坏这种恢复方式几乎是唯一救砖路径。5.2 验证阶段怎么检查存储访问的正确性做SoC验证的人对存储问题有更敏锐的嗅觉。验证阶段最常用的方法是构造地址随机化的读写样例既覆盖边界地址也覆盖跨页访问、非对齐访问、burst边界等特殊场景。还要同时注入多个主设备发起访问制造总线竞争看仲裁逻辑会不会产生死锁或数据丢失。一个典型的场景是验证DMA与CPU同时访问同一个SRAM时的一致性。CPU先写一段数据然后触发DMA搬走DMA搬完后CPU再读目标缓冲区验证。如果Cache没及时刷新CPU可能把旧值写回覆盖掉DMA写进的目标区数据。验证代码里必须有意识地创建这种竞态条件单纯按顺序读写永远测不出这类问题。除了功能验证性能验证也很重要。用性能计数器记录Cache命中率、总线带宽使用率、DDR队列深度能判断系统离极限还有多远。我习惯在每次改版后先跑一遍“存储压力基线”例如读取大数组做校验和把实际带宽和理论带宽对比。如果实测带宽远低于理论值优先检查配置比如是否开启了DDR的自动刷新冲突规避、是否打满了AXI outstanding、是否用错了CMA内存分配区。5.3 新手最容易踩的几个存储配置坑给刚入行的朋友提几个最容易踩的坑都是我亲眼看到或亲身体验过的。第一个坑是忘记配置MMU/Cache属性。在Cortex-A上同一个物理地址不同页表项的Cache属性可能不同。如果DMA缓冲区的页表项被标记为Cacheable而DMA操作又不做Cache维护数据就是随机错乱。典型表现是“跑一次对一次循环跑十次错一次”。第二个坑是DMA缓冲区地址没有对齐。很多DMA控制器要求缓冲区地址按32字节或64字节对齐否则会出现无法描述的传输错误或者性能下降。开发早期就把“对齐”刻进代码规范里能省掉大量调试时间。第三个坑是内存分配器用错。Linux内核里有的内存区适合DMA比如CMA有的内存区只适合CPU访问。如果驱动把普通kmalloc内存直接丢给DMA虽然能运行但会有一致性问题或者因内存无法被稳定锁定导致偶尔失败。第四个坑是忽视中断上下文里的存储访问。中断处理函数运行在特殊上下文里不能调用可能睡眠的分配函数也要避免访问慢速存储。如果中断里硬要去读eMMC系统延迟会爆炸甚至导致高优先级中断被拖垮。所以中断处理函数一定要保持极简复杂工作在进程上下文或工作队列里做。5.4 我的存储性能测试方法论最后分享一套我自己常用的存储性能测试流程适合在做产品方案评估或系统调优时用。第一步是明确测试目标是测峰值带宽还是测真实场景延迟两者完全不同前者用连续的burst读写就能打满后者要在随机地址、小粒度、读改写混合的模式下才有参考价值。第二步是搭好测试环境把CPU频率、DDR频率、总线频率固定下来关闭省电降频不要用跑分软件这种黑盒子而是自己写一段裸机或Linux内核态测试代码精确记录循环次数和时间戳寄存器。第三步是分层定位如果整体性能不好先把Cache打开和关闭的结果都跑一遍看看瓶颈在哪一层。全Cacheable时性能很好说明DDR没问题关掉Cache就变慢多少可以折算出DDR实际带宽和延迟。再看外存性能用iozone或fio这类工具测随机小文件读写这才是用户能感知的响应速度。第四步是记录和对比把每次修改前后的数据存档包括温度、电压、固件版本、测试命令最好用脚本自动记录。没有存档的性能优化等于没优化因为后面根本说不清哪一项改动带来了收益。我个人在实际操作中最深的体会是存储系统的优化没有“一键加速”它是在容量、速度、成本、功耗和确定性之间不断寻找平衡。不要迷信任何一个单一指标而是先把你自己的业务场景摸清搞清楚热点数据流向再去动那些调度和配置参数。很多问题在“跑一跑压力测试”之后都会现出原形——所以真遇到性能玄学关掉缓存先试一把再把DDR频率降一档对比往往比盯着代码发呆有效得多。还有个小技巧想分享给大家调试启动早期代码时给每一步存储访问加一个独立的GPIO翻转信号用示波器量这些信号之间的间隔比加打印日志要精确得多。打印本身要占用串口和存储会影响时序GPIO翻转几乎零开销你把几个关键节点的间隔测出来哪一步卡了、哪一步慢了一眼就能看清楚。这个方法我们内部用了很多年遇到启动慢、DDR训练不稳这类难缠问题时它比逻辑分析仪和软件time命令都直接。
返回列表