1. 从一颗芯片的“门禁系统”说起如果你做过 SoC 设计一定遇到过这样的场景芯片里有好几个主设备——CPU、DMA、GPU、各种加速器——它们都挂在 AXI 总线上共享同一片片上内存On-Chip Memory比如 SRAM 或 Tightly Coupled Memory。平时大家各跑各的相安无事。但某一天一个本该只读配置寄存器的 DMA 通道因为固件里一个指针写错直接往代码段的内存地址写了一串数据。结果就是系统跑飞调试器连不上日志一片空白你只能对着波形抓耳挠腮。这类问题在裸机系统里尤其致命因为根本没有 MMU 帮你兜底。ARM Cortex-M 系列、RISC-V 裸核、各种 DSP 和 NPU 的片上 SRAM通常都是物理地址直连谁都能访问。这时候MPUMemory Protection Unit内存保护单元就是那道最后的门禁。它不像 MMU 那样做地址翻译只做一件事检查这次访问有没有权限。有放行没有报错。但问题来了——给 AXI 总线加 MPU不是写几行 RTL 就完事的。AXI 是全握手协议五个通道各自独立读写地址通道、数据通道、响应通道时序关系复杂。你要在哪个环节拦截拦截之后怎么返回错误响应错误响应会不会导致总线死锁多个主设备并发访问时权限检查会不会成为时序瓶颈这些问题每一个都能让你在仿真器里耗上一整天。我最近刚完成了一个 AXI MPU 的 RTL 项目从需求拆解、寄存器设计、RTL 编码、UVM 验证到最终综合整个流程走了一遍。过程中踩了不少坑也积累了一些用 AI 辅助研发的实战经验。这篇文章就把整个项目的思路、关键细节、实操步骤和避坑心得完整分享出来。无论你是刚接触 AXI 协议的验证工程师还是正在做 SoC 安全加固的 RTL 设计者相信都能从中找到可以直接复用的东西。2. 整体方案设计为什么要在 AXI 上做 MPU2.1 需求拆解到底要保护什么先明确需求。这个项目的目标是在 AXI 总线矩阵和片上内存之间插入一个 MPU 模块对访问片上内存的读写操作进行权限检查。具体来说需要满足以下几个核心需求区域划分把片上内存的地址空间划分为多个区域Region每个区域可以独立配置起始地址、结束地址和访问权限。权限粒度每个区域支持读、写、执行三种权限的独立控制。执行权限主要用于区分代码段和数据段。主设备标识不同主设备Master可以有不同的权限视图。比如 CPU 可以读写所有区域DMA 只能读写数据区某个加速器只能读配置区。错误响应非法访问需要返回 AXI SLVERR 响应并且记录错误信息访问地址、主设备 ID、读写方向供软件查询。低延迟权限检查不能成为关键路径瓶颈理想情况下组合逻辑延迟控制在 2ns 以内。这些需求看起来直白但每一条背后都有设计取舍。比如“区域划分”这一条区域数量定多少8 个还是 16 个区域多了寄存器面积大区域少了灵活性不够。我最终选了 8 个区域理由是绝大多数 SoC 的片上内存分区不会超过 8 个而且 8 个区域的比较器树深度可控时序容易收敛。2.2 架构选型拦截点放在哪里AXI 协议有五个通道读地址通道AR、读数据通道R、写地址通道AW、写数据通道W、写响应通道B。权限检查的拦截点选择直接决定了设计的复杂度和时序表现。我考虑过三种方案方案一在地址通道拦截。在 AR 和 AW 通道的握手阶段就做权限检查如果非法直接不往下传自己返回错误响应。这个方案的好处是拦截早不会产生实际的内存访问。但难点在于AXI 协议规定一旦 AW 通道握手完成后续的 W 通道数据必须被接收否则主设备会一直等。所以你不能简单地“不往下传”而是需要自己消化掉写数据然后返回 B 通道的 SLVERR 响应。方案二在数据通道拦截。等 W 通道数据到达后再检查。这个方案时序更差因为数据通道通常位宽大64 位或 128 位比较逻辑更复杂。而且读操作没有数据通道的“预检查”机会读地址已经发出去了内存可能已经返回数据了。方案三在地址通道做检查但用状态机管理非法访问的后续处理。这是最终采用的方案。具体来说AR/AW 通道握手时权限检查逻辑并行工作。如果合法正常转发如果非法进入一个“错误处理状态”对于读操作直接返回 R 通道的 SLVERR 和空数据对于写操作接收 W 通道数据但丢弃然后返回 B 通道的 SLVERR。这个方案的关键在于状态机的设计。因为 AXI 是流水线协议地址通道可以连续发出多个请求所以错误处理状态必须能够缓存多个非法请求的信息。我用了两个 FIFO一个存非法读请求一个存非法写请求。FIFO 深度设为 4足够覆盖大多数突发场景。2.3 寄存器规划SystemRDL 的优势寄存器设计我用了 SystemRDL。相比手写 RTL 寄存器SystemRDL 的好处是一份描述可以自动生成 RTL、C 头文件、UVM 寄存器模型和文档。这个项目里MPU 的寄存器包括全局控制寄存器使能位、默认权限、错误中断使能。区域配置寄存器每个区域一组包括起始地址、结束地址、权限位、主设备掩码。错误状态寄存器记录最近一次非法访问的地址、主设备 ID、读写方向、时间戳。错误清除寄存器写 1 清除错误状态。用 SystemRDL 描述后通过 PeakRDL 工具链生成 RTL 和 UVM RAL 模型。这里有个细节SystemRDL 的addrmap定义中寄存器的sw访问属性要仔细设置。比如错误状态寄存器是只读的sw r但错误清除寄存器是只写的sw w这些属性会直接影响生成的 RTL 和 UVM 模型的行为。注意SystemRDL 生成的 RTL 默认是 APB 或 AXI-Lite 接口。如果你的 MPU 配置接口也是 AXI-Lite需要确认生成的接口时序是否符合你的总线矩阵要求。我遇到过生成的 AXI-Lite 接口在AWREADY和WREADY的依赖关系上与预期不符的情况后来手动调整了生成的 RTL。3. 核心细节解析RTL 设计中的关键决策3.1 地址比较逻辑的优化权限检查的核心是地址比较判断当前访问地址落在哪个区域内。最直接的做法是遍历所有区域逐个比较。但 8 个区域的比较器如果串行排列延迟会累积。我采用了并行比较 优先级编码的结构// 并行地址比较简化示意 genvar i; generate for (i 0; i 8; i i 1) begin : gen_region_match assign region_match[i] (addr region_start[i]) (addr region_end[i]); end endgenerate // 优先级编码编号小的区域优先级高 always (*) begin hit_region 3b111; // 默认无匹配 hit_valid 1b0; for (int j 7; j 0; j--) begin if (region_match[j]) begin hit_region j[2:0]; hit_valid 1b1; end end end这里有个取舍区域重叠时怎么办我的设计是编号小的区域优先级高。这样软件配置时可以把更具体的区域比如某个外设的寄存器区配成小号把大范围的默认区域配成大号。这个规则需要在文档里写清楚否则软件工程师会踩坑。另一个优化点是地址比较的位宽。片上内存通常只有几百 KB 到几 MB地址位宽不需要全 32 位比较。我截取了地址的 [23:0] 进行比较高位默认相同。这样比较器面积减少了约 30%时序也有改善。但前提是软件配置的起始和结束地址必须在这个范围内否则需要额外的检查逻辑。3.2 权限检查的状态机设计前面提到非法访问需要状态机来管理后续的响应。这个状态机的设计是整个 MPU 最复杂的部分。我把它分成三个子状态IDLE正常转发阶段。AR/AW 通道握手时权限检查结果并行产生。如果合法直接转发如果非法把请求信息推入错误 FIFO并跳转到对应的错误处理状态。RD_ERR处理非法读请求。从错误 FIFO 中取出请求信息生成 R 通道的 SLVERR 响应。注意AXI 的读数据通道有RLAST信号对于突发读需要正确生成最后一个数据的RLAST。WR_ERR处理非法写请求。接收 W 通道的所有数据丢弃然后生成 B 通道的 SLVERR 响应。状态机的难点在于AXI 的地址通道和数据通道是解耦的。一个非法写请求的 AW 通道握手后W 通道的数据可能过几个周期才到。如果此时又有新的合法请求进来状态机需要能够同时处理。我的做法是错误 FIFO 只存地址和控制信息W 通道数据由独立的“数据接收器”处理收到WLAST后触发 B 通道响应。// 写错误处理状态机简化 typedef enum logic [1:0] { WR_IDLE, WR_RECV_DATA, WR_SEND_RESP } wr_err_state_t; always (posedge clk or negedge rst_n) begin if (!rst_n) begin wr_err_state WR_IDLE; end else begin case (wr_err_state) WR_IDLE: begin if (err_wr_fifo_valid) begin wr_err_state WR_RECV_DATA; end end WR_RECV_DATA: begin if (w_valid w_ready w_last) begin wr_err_state WR_SEND_RESP; end end WR_SEND_RESP: begin if (b_valid b_ready) begin wr_err_state WR_IDLE; end end endcase end end注意WLAST信号的生成必须严格遵循 AXI 协议。如果主设备发出的写突发长度是 4那么WLAST必须在第 4 个数据拍拉高。我在仿真中遇到过主设备发完数据但WLAST没拉高的情况导致状态机卡死。后来在验证环境中加了协议检查器才定位到问题。3.3 错误记录与中断机制错误记录寄存器需要保存最近一次非法访问的详细信息。这里有个细节如果连续发生多次非法访问是只记录第一次还是每次都覆盖我的设计是第一次非法访问时记录后续非法访问只增加错误计数不覆盖详细信息。软件读取错误状态后写清除寄存器才能记录下一次。中断机制方面我设计了一个电平中断信号mpu_error_irq只要错误状态寄存器非空就拉高。软件清除后自动拉低。这个中断信号可以连接到中断控制器让 CPU 及时响应。错误计数器的位宽选了 8 位溢出后饱和。这个选择基于经验如果非法访问超过 255 次说明系统已经严重异常具体次数意义不大。4. 实操过程从 RTL 到验证的完整流程4.1 环境搭建与工具选型这个项目用的工具链是RTL 仿真ModelSim 或 VCS。我主要用 ModelSim 做功能调试因为它的波形查看和信号追踪比较方便。VCS 用于最终回归因为编译速度快。验证方法学UVM。用 UVM 搭建验证环境包括 AXI VIP、寄存器模型、记分板。寄存器生成SystemRDL PeakRDL。综合Design Compiler工艺库是 28nm。关于 ModelSim 能不能查看 RTL 电路图答案是ModelSim 本身不提供 RTL 综合后的电路图查看功能它主要是仿真工具。如果你想看综合后的电路结构需要用 Design Compiler 的design_vision或者 Synopsys 的HDL Designer。不过 ModelSim 可以查看 RTL 的层次结构和信号连接对于调试来说已经够用了。4.2 UVM 验证环境搭建验证环境的结构如下AXI VIP作为主设备产生读写激励。我用了 Synopsys 的 AXI VIP配置为 Master 模式支持乱序响应。寄存器模型由 SystemRDL 生成的 RAL 模型用于配置 MPU 寄存器和检查错误状态。记分板比较 VIP 的访问结果和 MPU 的预期行为。合法访问应该正常返回非法访问应该返回 SLVERR。覆盖率收集包括功能覆盖率和代码覆盖率。功能覆盖率主要关注不同区域的权限组合、不同主设备的访问、读写混合场景、错误注入场景。这里有个关键点AXI VIP 的bvalid信号处理。在非法写访问时MPU 需要返回 B 通道的 SLVERR。VIP 作为主设备会等待bvalid和bready握手。如果 MPU 的 B 通道响应延迟太大VIP 可能会超时。我在验证环境中把 VIP 的超时时间从默认的 1000 个周期调整到了 5000 个周期避免误报。4.3 测试用例设计测试用例分成了几个层次基础用例单个主设备单个区域读写权限检查。验证合法访问通过非法访问返回 SLVERR。区域边界用例访问区域的起始地址、结束地址、起始地址减一、结束地址加一。验证边界条件的正确性。多主设备用例两个主设备同时访问一个合法一个非法。验证 MPU 能够正确区分主设备 ID并且错误记录不混淆。并发用例主设备连续发出多个读写请求其中穿插非法访问。验证错误 FIFO 不溢出状态机不卡死。错误注入用例通过 UVM 的uvm_error注入机制模拟 VIP 发出非法请求。验证 MPU 的错误响应和中断信号。这里分享一个踩坑经验在并发用例中我遇到过错误 FIFO 溢出的情况。原因是主设备连续发出了 5 个非法读请求而 FIFO 深度只有 4。后来我把 FIFO 深度增加到 8并且在 FIFO 满时返回 SLVERR 而不是丢弃请求。这个修改需要在 RTL 中增加 FIFO 满的判断逻辑。4.4 AI 辅助研发的实践这个项目里我尝试用 AI 工具辅助了几个环节RTL 代码生成用 AI 生成 AXI 接口的握手逻辑模板。AI 生成的代码在valid/ready握手的基本结构上是对的但在WLAST和RLAST的生成逻辑上需要手动修正。我的经验是AI 适合生成框架代码但协议细节必须人工审查。UVM 测试用例生成用 AI 生成测试用例的骨架包括 sequence 和 test 类。AI 生成的代码在结构上没问题但具体的激励参数比如突发长度、地址范围需要根据实际设计调整。日志分析仿真日志动辄几万行用 AI 做根因定位可以快速筛选出错误相关的行。我试过把 UVM 的日志喂给 AI让它找出SLVERR相关的记录效果不错。但 AI 有时会误判把正常的SLVERR响应当成错误。所以最终的判断还是需要人工确认。注意AI 生成的 RTL 代码必须经过严格的协议检查。我遇到过 AI 生成的 AXI 代码在AWREADY和WREADY的依赖关系上不符合协议的情况导致仿真挂死。建议用 AXI 协议检查器如 VIP 自带的 checker做自动检查。5. 常见问题与排查技巧实录5.1 仿真挂死问题排查现象仿真运行到某个时刻不再前进波形上所有信号停止翻转。排查思路首先检查 AXI 握手信号。用 ModelSim 的examine命令查看ARVALID、ARREADY、RVALID、RREADY等信号的状态。如果某个VALID拉高但READY一直不拉高说明握手卡住了。检查状态机是否卡在某个状态。查看 MPU 内部状态机的state信号确认它是否在等待某个永远不会到来的条件。检查 FIFO 是否满。如果错误 FIFO 满了而状态机还在等待 FIFO 有空间就会卡死。常见原因WLAST信号未正确生成导致写错误状态机一直等待。RLAST信号未正确生成导致读数据通道无法完成。FIFO 满时没有反压机制导致请求丢失。解决方法在 RTL 中增加超时计数器如果某个状态停留超过 1000 个周期强制返回错误响应并复位状态机。这个超时机制在验证阶段非常有用可以避免仿真挂死。5.2 权限检查误报问题现象合法访问被误判为非法返回 SLVERR。排查思路检查区域配置寄存器的值是否正确写入。用 UVM 的寄存器模型read操作回读配置值。检查地址比较逻辑的边界条件。特别是当访问地址正好等于区域结束地址时比较运算符是还是。检查主设备 ID 的匹配逻辑。如果主设备掩码配置错误合法主设备可能被拒绝。常见原因区域重叠时优先级规则不清晰导致访问落入了错误的区域。地址位宽截断导致高位地址被忽略。主设备 ID 的位宽不匹配比如 VIP 发出的 ID 是 4 位而 MPU 只比较了低 2 位。解决方法在验证环境中增加断言assertion检查每次访问的地址和权限配置是否匹配预期。断言可以在仿真过程中实时报警比事后查波形效率高得多。5.3 时序不收敛问题现象综合后时序报告显示setup违例关键路径在地址比较逻辑上。排查思路用 Design Compiler 的report_timing命令查看关键路径的详细延迟。检查地址比较器的级数。8 个区域的并行比较 优先级编码组合逻辑延迟可能超过 2ns。检查 FIFO 的读写指针逻辑跨时钟域同步是否引入了额外延迟。解决方法把地址比较逻辑流水线化在比较器后面加一级寄存器。代价是增加一个周期的延迟但时序可以显著改善。减少区域数量从 8 个降到 4 个。如果软件不需要那么多区域这是一个有效的取舍。用case语句替代if-else链综合工具通常能更好地优化case结构。5.4 常见问题速查表问题现象可能原因排查方法解决方案仿真挂死握手信号卡住查看 VALID/READY 信号增加超时计数器权限误报区域配置错误回读寄存器值检查边界条件和优先级时序违例比较逻辑延迟大report_timing流水线化或减少区域数错误 FIFO 溢出并发非法请求过多查看 FIFO 满信号增加深度或反压B 通道响应超时状态机卡在 WR_SEND_RESP查看状态机信号检查 BVALID/BREADY 握手中断不触发错误状态寄存器未更新查看中断信号检查清除逻辑6. 写在最后的一些实操心得这个项目从启动到最终签核前后花了大约六周时间。其中 RTL 编码用了两周UVM 验证用了三周剩下的一周做综合和时序收敛。如果让我重新做一遍有几个地方我会调整。第一错误 FIFO 的深度应该一开始就设成 8 而不是 4。深度 4 在大多数场景下够用但在并发压力测试中很容易溢出。溢出后的反压逻辑会增加状态机的复杂度不如一开始就留足余量。第二地址比较逻辑应该从一开始就做流水线化。我最初为了省一个周期的延迟把比较逻辑做成了纯组合电路。结果综合后发现时序违例又回头改 RTL浪费了几天时间。后来想明白了MPU 的权限检查延迟增加一个周期对系统性能的影响微乎其微但时序收敛的难度会大幅降低。第三UVM 寄存器模型的使用要尽早。我一开始手动写寄存器配置的 sequence后来换成 RAL 模型后配置效率和可维护性都提升了很多。特别是错误状态寄存器的回读用 RAL 模型的read操作比手动拼地址方便太多。关于 AI 辅助研发我的体会是AI 在生成框架代码和日志分析上确实能提效但在协议细节和时序优化上还是得靠人的经验。AXI 协议的握手规则、UVM 的相位机制、综合工具的时序约束这些都需要扎实的基本功。AI 可以帮你写得更快但不能帮你写得更对。最后分享一个小技巧在 ModelSim 中调试 AXI 协议时可以用add wave -radix hex把地址和数据信号显示为十六进制这样查看地址范围更直观。另外用virtual function把常用的信号分组比如把所有 AR 通道信号放在一组所有 R 通道信号放在另一组波形查看效率会高很多。这些细节看起来不起眼但在长时间调试中能省下不少精力。