
从第一次在项目里遇到“不可信IP偷偷读走了安全内存”的问题到最终把AXI MPU落到RTL并流片整个过程走了不少弯路。这篇博文想把这个研发过程完整拆给你看为什么要在AXI总线上给片上内存做权限检查、MPU怎么设计、地址比较和AXI时序细节怎么处理以及我实际用AI辅助编码和验证的完整实践。内容偏硬件工程但我会把原理和步骤写清楚适合做SoC集成、总线设计、安全子系统的工程师参考也适合刚入门IC设计、想了解MPU怎么工作的同学。1. 为什么需要给片上内存加一道锁需求拆解与安全模型1.1 多主设备共享内存问题从哪来现代SoC里挂在AXI总线上的主设备远不止CPU核一个。DMA引擎、GPU、NPU、图像ISP、Modem、Debug Access Port调试访问端口都会通过AXI互联矩阵去访问同一块片上SRAM或者DDR。大多数时候我们默认这些主设备“都是自己人”但在真实的系统里情况没那么乐观。我遇到过这么一种场景一颗集成了第三方通信IP的芯片该IP内部有一组看起来人畜无害的AXI主接口用于搬数据。它的功能文档写的是“只访问指定的共享缓冲区”但实际上由于该IP的固件和寄存器配置可能被外部攻击者控制一旦有一天寄存器被配了错地址它就可以通过对AXI总线的合法访问直接把芯片安全子系统的密钥存储区域读走。这不是假设行业中真实发生过类似的事故。另一个常见场景是调试接口JTAG / SWD通过调试总线可以直接读片上内存如果不加限制产线上或者实验室里的测量设备就能把一整个安全固件镜像dump出来。所以核心需求就变成了在共享的AXI总线路径上为片上内存准确说是内存映射的地址区间加一道“安检门”。这个安检门要能识别每一个AXI事务的主设备身份和权限属性再决定放行还是拦下。业界标准的叫法就是MPUMemory Protection Unit在AXI总线上实现时就是AXI MPU。1.2 AXI协议本身能做什么、不能做什么AXI协议并不是完全“裸奔”的它在协议层定义了一些和安全相关的信号主要是AxPROT。每个AXI读写事务在地址通道上都会携带ARPROT或AWPROT总共有3个bitAxPROT[0]特权/非特权访问标志。1表示特权privileged0表示非特权unprivileged。AxPROT[1]安全/非安全访问标志。1表示非安全non-secure0表示安全secure。AxPROT[2]指令/数据访问标志。1表示指令instruction0表示数据data。这套信号实际上是ARM TrustZone架构下常用的安全标识。理论上总线上的Slave端设备可以通过AxPROT来判断当前事务是否来自安全世界、是否特权模式。但是这里有一个很现实的问题AxPROT只是主设备主动申报的“身份”协议并没有强制要求每个主设备都如实申报。如果某个主设备把AxPROT的secure位随便拉高或拉低总线本身不会纠正它。真正能确保安全语义的是总线路径上有某个模块对申报的身份进行裁决允许还是丢弃。这正是AXI MPU要负责的核心工作之一它不只是看AxPROT还要结合配置好的地址区域和权限规则形成真正的强制执行。另外AXI协议本身还支持通过AxUSER信号扩展自定义信息可以是主设备ID、域IDdomain ID、进程ID等。我们实际做的MPU就是用AxUSER低8bit作为主设备标识然后和安全策略表去匹配。可以说AXI只是提供了“托运单上写什么”的字段MPU才是那个检查托运单、决定放不放行的安检员。1.3 权限模型怎么定谁在访问、能访问哪里、能干什么做MPU之前首要任务不是写代码而是把权限模型定义清楚。我们当时整理了芯片里所有主设备列表给每个主设备分配了一个ID然后为每一段内存区域定访问规则。典型的规则项长这样主设备ID主设备名区域基地址区域大小读权限写权限安全/非安全要求0x01CPU Secure0x0000_000064KB允许允许仅安全事务0x02CPU NonSecure0x0000_000064KB拒绝拒绝-0x10DMA Agent0x2000_00001MB允许允许任意安全级别0x10DMA Agent0x0000_000064KB拒绝拒绝-这个表看着简单真正落地时会有几个容易扯皮的点默认拒绝还是默认允许很多项目组为了省事把没有命中的区域默认允许。但安全设计要求恰恰相反未明确允许的区域一律拒绝。项目里跟软件团队反复确认后才明确下来“默认拒绝”虽然会带来一些配置遗漏导致系统断电后的排查成本但这才符合安全基线。读和写权限要不要各自独立必须独立。有些内存区域允许CPU读但绝对不允许DMA改写否则DMA一个错误配置可能把关键控制结构冲掉。非安全世界能否读安全数据通常不能。但有些场景比如非安全端需要读取安全侧产生的统计信息这时就得专门开放。权限表设计时要允许这种例外。把这些规则理清楚MPU硬件才能有据可依。很多做芯片的人一上来就写RTL到验证阶段才发现权限模型没定清来回返工特别浪费时间。2. AXI MPU架构选型串接位置、region表与拒绝策略2.1 MPU放在哪从接口侧还是主接口侧AXI MPU的物理位置有几种选择各有利弊。我当时优先考虑两种方案方案A放在互联矩阵的每个Slave接口前面memory侧。也就是把MPU做到从端口Slave port上所有要访问片上内存的主设备都经过它。这种做法的好处是只需要在内存入口做一遍检查配置简单规则只针对“目标地址区间”。缺点是如果内存有多个Slave端口比如分为几个bank每个端口都要配一个MPU实例逻辑有重复。方案B放在每个主设备后面master side。在每个AXI主设备发起的事务路径上单独加MPU。优点是能精确地针对每个主设备做差异化策略对访问源头的控制最直接。缺点是芯片里主设备多每个都要塞MPU代价高而且如果主设备数量多配置寄存器也要跟着翻倍。我们实际选择的是折中紧贴片上内存RCU内存控制器前一级的AXI互联矩阵内部做一个Centralized AXI MPU所有访问片上内存从端口的事务先统一经过MPU做裁决再往下游发。原因有两个片上内存是真正需要保护的资产把门设在资产门口最直接第二个原因是后续想加对DDR区域的保护MPU的region表可以同时覆盖SRAM和DDR的地址空间做成统一的“安全地址防火墙”。综合来看如果你的项目里主设备数量多、共享内存只有一个统一入口Centralized MPU是性价比比较高的方案。如果主设备数量少、内存区域分散可以按方案A在每个从端口做。2.2 region表怎么设计base/mask、权限位、优先级Region区域是MPU内部的一组比较规则。每个region至少需要包括基地址base、掩码mask或大小size、允许的读/写权限、允许的安全等级、使能位。我们做的MPU支持16个region每个region配置成一个48bit物理地址空间中的一段。关键选择是用basemask还是basesizebasesize直观软件配置时直接写“基地址0x10000大小0x8000”硬件比较器做addr base addr base size。比较器面积稍大因为要同时走“大于等于”和“小于”。basemask软件配置成“基地址0x10000掩码0xF8000”意思是地址的高17bit与基地址高17bit匹配即命中。比较器只需做(addr mask) (base mask)逻辑更简单面积更小。我们最终选了basemask方案原因不只是面积还因为它天然适合做“对齐区间”判断——只要保证base在掩码覆盖的粒度上对齐就不会出现半区间的后半段归属决策难题。这在RTL实现上省了一组加法器时序也更友好。Region还有一个优先级设计。多个region可能重叠比如一个广义的大区域和其中一个更精细的关键子区域此时应该让更细粒度的region优先生效。我们在每个region配置里放了一个3bit优先级字段比较时按高优先级优先匹配优先级相同按region编号从小到大匹配。工程上这里有一个容易踩的坑优先级字段和region编号混在一起处理会让验证用例很难定位“为什么这个事务被拒绝了”。建议做成独立优先级矩阵在寄存器手册里简单写清规则不要让软件去猜。2.3 非法访问怎么处理拦截、错误应答还是降级MPU判定一个事务非法后的处理方式直接决定了系统行为和调试难度。有三种常见策略策略一拦截block。事务不往下游发AW/AR通道不响应。但这种做法会导致主设备一直挂在VALID1等READY系统直接卡死。除非还要配合超时机制否则不推荐单独使用。策略二错误应答error response。在本地直接给主设备返回SLVERR。读事务返回RRESP SLVERR写事务返回BRESP SLVERR事务生命周期正常结束主设备收到错误码后可以走软件异常处理。这是实际工程中最常用、也最推荐的做法。策略三降级secure-non-secure映射。有些系统允许把非安全访问“映射”到一段假的/空的区域返回填充数据但不泄露真实内容。这个做法常用于对付“探测型攻击”——攻击者尝试读取时他拿到的全是0或随机数无法分辨是真的还是假的。我们另外还扩展了一个“降级写丢弃”的功能非法写请求本地直接返回OKAY但数据不写入真实内存这在调试期和蜜罐场景下非常有用。最终系统支持了两种模式通过全局配置寄存器选择标准模式返回SLVERR降级模式返回OKAY。注意如果返回OKAY主设备会以为写成功了后续读到旧数据时可能产生诡异行为所以这个模式一般只在调试或蜜罐场景用量产默认必须是SLVERR模式。3. 地址比较与AXI时序处理核心实现细节3.1 地址掩码比较的对齐约束与实现地址掩码比较的核心公式是target_address region_mask region_base region_mask。这里必须注意一个对齐陷阱region_base的低位必须在mask为0的那些位上为0。举个例子如果你配置region_base0x10000mask0xF8000那么比较时实际生效的是bit[19:15]这段地址而bit[14:0]不参与比较相当于region覆盖了0x10000~0x17FFF整个32KB空间。此时base低15位全部为0配置没问题。但如果配置region_base0x10001、mask0xF8000那比较行为会变成“bit[19:15]等于0b10000”即可命中低15位全部忽略和配置人员想象的可能完全不一样。我们当时在寄存器接口上做了一个硬件保护逻辑写region_base时如果发现低bit存在与mask不匹配的位直接返回SLVERR并拒绝写入。这个保护逻辑一开始是被软件团队吐槽“你为什么不让写”但跑了几轮验证后所有人都说这个保护救了大命因为它把配置阶段最容易犯的错在源头堵死了。另外地址比较的硬件实现还有一个可选的优化点对地址做pre-decoded比较而不是全量比较器。如果region数量固定16就可以把地址的每个高位bit分别与16个region的对应bit比较再做排列组合缩减。工程上其实综合工具会帮你做逻辑优化写RTL时保持简洁更重要。3.2 burst跨region的边界问题AXI协议的burst传输一次最多可以访问256个数据beatINCR burst如果AxSIZE2一次最多1KB。一个burst很可能会跨越两个region的边界。这时候怎么判断权限需要先定义清楚规则否则验证时会发现预期二义性。我们的做法是以事务首地址为判断基准。也就是说非法判断只看AxADDR命中的region。如果首地址落在允许区域整个burst都放行即使后半段实际会超出该region边界。这个做法的好处是硬件简单、时序路径短但它有安全漏洞一个首地址合法的INCR burst长度足够长可能会越界读到不允许区域的数据。为了解决这个漏洞我们做了两件事软件配置约束在系统集成手册中强制要求与安全相关的region其配置大小必须任何可能burst的最大长度并且region边界对齐到burst长度。这样只要首地址合法整个burst不会跨出region。硬件兜底如果检测到首地址命中的region的剩余可用长度小于当前burst长度计算需要用到AxLEN和AxSIZE则直接按非法处理。这个检查逻辑需要一组加法器region_base region_size - first_address与burst_bytes比较。面积代价不大但能堵住那个漏洞。如果你们的设计跑的是大burst比如64-256字节且region划分很细那我强烈建议加上第二种硬件兜底不要仅仅依赖软件约定——作为芯片设计者把安全兜底交给软件自觉风险很高。3.3 错误应答如何融入AXI握手读路径与写路径有了错误策略最关键的实现问题来了当MPU决定拒绝事务时如何确保AXI握手时序完整结束而不死锁。先看读路径。正常情况下AR通道收到一个ARVALID事务后MPU做判断。如果非法的MPU不往下游的Slave端口发送AR请求而是自己充当“伪Slave”在R通道上产生一个读响应事务RRESPSLVERR、RDATA0、RLAST1。但问题在于AXI读事务可以有多个beatARLEN决定MPU要根据ARLEN计算需要返回几个RVALID拍每一拍都回SLVERR最后一次置RLAST。这个状态机不难但容易漏的是RVALID与RREADY的握手顺序——必须等主设备RREADY为高才能完成一拍。写路径更麻烦。AW通道判断为非法时MPU同样不向下游发AW请求但主设备随后还会在W通道把整包数据传过来。MPU必须通过WREADY把W通道的数据全部收掉直到收到WLAST最后在B通道返回BRESPSLVERR。这里有两种实现实现一MPU把AW判断结果记录下来等W通道来数据时直接接收并丢弃不写真实内存收到WLAST后返回SLVERR。实现二MPU在AW阶段就返回AWREADY给主设备主设备后续发W时MPU照单全收再在B通道返回SLVERR。用实现二会有一个细节AWREADY可以立刻给也可以等判断完再给。由于MPU判断逻辑只消耗组合逻辑我们直接把判断结果和AWREADY放在同一个周期给出去W通道上WREADY始终等于1除非内部FIFO满从而最大程度降低对正常事务延迟的影响。写路径还有一个容易出的bug——当AW被判定非法后,如果主设备因某种原因不再发W数据怎么办?那就会造成长久占用W通道需要设计一个超时机制或者在W通道上强制拉WREADY把僵尸事务清掉。这点我们是在验证阶段被“时序死锁”用例打出来的后面才补上。4. AI辅助研发从RTL骨架到验证调试的实用经验4.1 用AI生成SystemVerilog骨架的正确姿势这个项目的“AI辅助研发”并不是把整个MPU丢给AI去生成而是把它当作一个“非常懂AXI时序的结对工程师”。我实际操作时第一步是让AI生成SystemVerilog骨架。我给AI的提示词大概是这个框架请用SystemVerilog实现一个AXI4从接口的MPU模块接口信号包括AR/AW/W/R/B的完整AXI4握手信号支持最多16个region每个region有base、mask、读写权限、安全属性非法读在R通道返回SLVERR非法写在B通道返回SLVERR要求用状态机实现读错误应答和写丢弃逻辑地址比较用(addr mask) (base mask)。AI很快就给出一段结构完整的SystemVerilog说实话状态机的骨架比我自己从零敲快得多尤其是模块端口声明、中间信号wiring这些机械工作AI几乎零错误。但我从头到尾review了一遍发现了至少三个问题AI把ARLEN的decoded结果直接用在了错误读响应的beat计数上但它没有考虑ARSIZE与数据总线位宽的关系。如果数据总线128bit而ARSIZE0b0104字节每个AXI beat是16字节那么RLAST产生的时机就错了。AI在写路径上默认WREADY1但没有考虑AXI4的WVALID与WREADY重叠时的边界处理。AI没有给“非法写但W通道数据有WSTRB部分选通”的情况做适配直接丢弃所有写数据——这个行为本身上没大问题但是响应SLVERR之后软件可能不知道哪些字节实际被丢弃需要在寄存器里多留一个状态位。所以我的建议很明确用AI生成骨架可以但必须按“模块端口、内部逻辑、时序状态、边界异常”四级去逐行review。我的规矩是AI生成的代码必须要能通过代码走读评审再由验证工程师写测试用例验证不允许直接合入。4.2 AI辅助搭UVM验证环境和定向用例UVM验证环境的搭建是一个工作量很大的环节——sequence、driver、monitor、scoreboard、config_db连接全是模板化代码。这一步让AI参与效率提升非常明显。我给AI的指令通常是这样请用UVM搭建一个验证AXI MPU的环境顶层环境包括AXI master agent用于发起事务和AXI slave agent用于响应正常路径需要支持随机读写事务、错误注入、region配置寄存器序列scoreboard需要根据配置的region表动态判断期望响应OKAY还是SLVERR。AI生成的UVM环境跑起来后我做了两处关键修改把scoreboard的“期望结果”从“事务首地址命中即OKAY”改成了“事务首地址命中burst不跨region边界条件下才OKAY”。AI默认没有理解这一层安全约束所以期望值计算逻辑一开始是错的。增加了非法事务的直接统计功能因为在验证后期我需要快速知道“这个测试里有多少个非法读被正确拒绝了”而不是事后去翻日志。另外AI生成的定向用例也帮我省了不少事。我给它指定了“覆盖region边界地址base、base-1、basesize-1、basesize”这样的条件它把sequence写得很规范。但定向用例里的随机种子管理是我自己补的因为只有固定随机种子才能复现某个疑似bug。4.3 AI辅助分析仿真波形与日志真能省时间这一步是我最初最怀疑、最终最惊喜的地方。项目里有段时间一个并发场景的仿真失败日志只显示“AXI write transaction error response while expected OKAY”定位非常费劲。我尝试把仿真日志大概几百行关键AW/AR/W/B/R事务打印直接粘贴给AI让它帮我找规律。AI给出的分析方向确实靠谱它指出日志中有两个不同主设备ID的事务在地址通道上重叠其中一个是非法访问被MPU拒绝后返回SLVERR另一个是正常事务两条事务的通道握手时序在日志打印里被交织肉眼很容易误判是MPU错判了。顺着这个思路我去波形里把两组信号的awid和awuser分开看很快就确认MPU行为正确问题出在验证环境里其中一个sequence在连续两次写之间没有等待上一个事务结束属于激励本身违规。这个经验告诉我AI在日志模式识别上确实比人眼快尤其适合“大量事务打印交错出现”的场景。但要注意AI的判断只能作为分析线索定位结论必须由工程师在波形里确认不能直接把AI给的结论写进bug单。4.4 AI生成代码的翻车点必须人工复核的清单整个项目用AI辅助编码的过程里我总结了一份“AI生成代码翻车点清单”分享给你AXI版本混用AXI3和AXI4对写通道的交织支持、锁定访问、QoS信号定义有差异。AI很容易把AXI4的从接口写出AXI3特有行为。握手依赖缺失比如B通道返回时没有考虑BREADY的等待或者在R通道的RLAST上没有严格与RVALID同拍。AI写的代码对这种“一拍都不能错”的协议约束经常腿软。WSTRB处理不完整当非法写事务被丢弃时某些字节写入被丢弃后软件无法区分。AI没有加“丢弃字节记录”这种故障诊断机制。FIFO满反压AI生成代码时默认W通道都能及时接收不会主动建模反压路径这在正常路径长延迟时会造成FIFO溢出。寄存器读写地址解码AI生成的配置寄存器逻辑经常没有处理地址非对齐写入的边界情况。所以尽管AI能大幅提高编码效率但最终的安全责任在人和流程不在AI。我的经验是AI辅助研发的正确姿势是让AI去写重复度高、模板化强、协议边界清晰的部分然后由人用更严格的验证环境去兜底。这个项目里AI帮助把整体编码和验证工作量压缩了大约三成但每一次合入前的人工review和仿真验证一步都没有省略。5. 功能覆盖、时序代价与最终落地5.1 测试计划与覆盖率哪些case必须跑MPU的验证彻底程度直接决定这个安全功能可不可信。我们列出的测试计划里有几类case是绝对不可少的region边界扫描对每个region验证base-1、base、basesize-1、basesize四个边界事务的响应。每一个都要覆盖读和写、安全和非安全、特权和非特权。mask非对齐配置场景故意用非对齐的base/mask组合写入配置寄存器验证MPU是否拒绝该配置硬件保护逻辑。burst跨边界场景随机生成burst长度、起始地址的组合让burst覆盖边界位置。如果配置的region大小比burst长度大则应该全部通过如果发生溢出应该被系统策略处理。并发事务场景两个主设备同时发起访问其中一个非法验证MPU对不同ID事务过滤的隔离性。非法后恢复场景非法事务被拒绝之后紧接着发起合法事务验证MPU不会“卡”在错误响应状态能正常继续处理后续事务。覆盖率方面我们主要收集了地址-事务类型-安全的交叉覆盖率cross coverage。重点观察有没有出现“安全读命中非安全region”和“非安全写命中安全region”这类重要事件没有被覆盖到。5.2 时序与面积MPU插进去的代价MPU在每个AXI事务的地址通道上插入了一级组合比较逻辑这必然会影响路径时延。我们当时在互联矩阵里的时钟频率目标是1GHz周期1ns地址比较逻辑包含region表匹配、优先级选择、非法判断。整个比较链如果全部用组合逻辑一次做完路径会非常紧张。我的处理方式是把地址比较拆成两级流水线。第一级做region命中比较16组(addr mask)结果第二级做优先级裁决和非法判断。这样一次访问会增加1个cycle的地址通道延迟但时序能收敛。在读事务总延迟动辄几十个cycle的系统中这1个cycle的代价基本可以接受。如果你做的是高频率、低延迟敏感的设计可以考虑在ARM/CMN互联里用带缓存的比较结果来减小路径。面积上实现16个region的Centralized AXI MPU用7nm工艺综合下来大概占几千门不到一个标准CPU核的百分之一。功耗更不用操心主要就是组合逻辑翻转对比系统整体功耗几乎可忽略。真正值得关注的是配置寄存器数量每个region要占一组控制寄存器16个region会占掉一段不小的地址空间做寄存器手册时要规划好。5.3 落地之后系统集成时还要注意什么MPU本身跑通功能、时序收敛不代表整个系统的安全目标就完成了。集成阶段还有三件事容易被忽略中断与异常处理路径的权限CPU在处理安全异常时需要访问一段专门的安全异常向量表。如果MPU没有给“异常处理模式”对应的特权访问放行CPU会fall到一个硬fault系统直接崩溃。我们当时专门为此加了一个“CPU核心错误处理region”只允许特权安全模式访问。电池域/低功耗域访问芯片休眠后有些主设备比如唤醒控制器仍然需要访问固定地址的内存。如果MPU配置没有覆盖到低功耗场景唤醒路径会直接失败。所以MPU的regionNVRAM配置要能在低功耗状态下保持有效。MPU配置本身的保护如果一个非安全世界的软件可以随意修改MPU的region配置寄存器那这个MPU就形同虚设了。因此MPU配置寄存器的写入通道本身也必须走安全权限判断只有安全特权软件可以写非安全访问直接返回错误。我个人在实际操作中还有一个体会做一个安全组件上下游团队的协作比RTL实现本身更费时间。你要跟软件团队核对每一个region的OS地址布局跟验证团队敲定每一个交叉覆盖率的bin定义跟SoC集成工程师确认低功耗模式下MPU的时钟和复位策略。这个过程中AI能帮我们写代码、找日志模式但跨团队的安全需求对齐只能靠人和人把话说透。最后再说一个小技巧如果你也要做类似的AXI权限检查模块建项目的第一天就把“默认拒绝”和“非法访问必须留痕迹打印/中断”这两条规则写进设计文档后面所有讨论都会省很多事。