做 FPGA/SoC 的工程师应该都遇到过类似场景一段共享的片上内存被某个不守规矩的 AXI 外设硬生生写穿CPU 这边还在正常跑数据却已经花了。我这次项目目标很直接——给片上内存加一道权限检查用 AXI MPU 把非法访问挡在门外整个研发过程尝试了 AI 辅助编码和验证。这篇文章把完整设计思路、关键决策和集成坑位都记录下来给打算在 SoC 总线上做权限保护的朋友一个参考。1. 为什么我要给片上内存“上锁”一个两天才定位的故障1.1 故障现场配置区被 DMA 写崩片上内存这个“片”在 FPGA 里是 BRAM/URAM在 SoC 里是片内 SRAM。它经常同时挂好几个 AXI masterCPU、DMA、硬件加速器、调试接口甚至以太网 MAC。默认情况下所有 master 访问地址空间没有任何限制——谁拿到地址谁就能读写。这种设计在原型验证阶段非常省事但一旦外设驱动写错了地址后果很直接。我当时遇到的问题是一个 DMA 引擎在初始化时把寄存器配置中的偏移量算错一个 burst 直接打进了相邻的关键配置区。配置区被改写后硬件状态机开始乱跳系统出现完全随机的中断和总线错误。我一开始怀疑是软件指针越界花了一天查 driver后来又怀疑是时序问题挂了 ILA 抓总线最后才定位到是 DMA 把一个原本应该写数据缓冲区的 burst送到了配置区地址上。这一天多的时间换来的教训只有一个片上内存没有权限保护等于把所有 master 都放进同一个房间任何一个人的脚滑都可能砸坏最贵的东西。1.2 为什么选择 AXI MPU 而不是其他方案有几种方案可以阻止类似问题地址隔离把内存区域映射给不同 master、增加 TrustZone 类安全扩展、或者干脆在软件层做访问控制。但在我这个场景下硬件 masterDMA、硬件加速器根本不吃软件那套它们的行为完全由寄存器配置决定一旦寄存器配错越权访问已经发生在总线上。AXI MPU 是最轻量、最直接的解法。它本质上是插在 AXI 路径上的一个门卫监听地址通道判断当前请求是否落在受保护区域内是否来自有权限的 master然后决定放行还是返回错误响应。它不修改数据路径不影响正常传输吞吐又能同时约束 CPU 和所有硬件 master。这类结构的典型框图并不复杂但真正要注意的是它插入的位置、身份识别方式以及错误响应的处理。这些我会在下一节展开。![不再使用图表结构示意以文字说明为主]2. 权限模型与位置设计在总线哪一层做检查最合适2.1 谁是“主人”用 AXI ID 和 PROT 信号区分访问者MPU 要判断“谁在访问”必须要有身份信息。AXI 协议里有两个现成信号可以用AXID事务 ID和 AxPROT访问属性。AXID 是最可靠的身份来源。大多数 AXI Interconnect 会保留 master 的 ID用于实现乱序传输和多个 outstanding 事务。MPU 设计里我通常只用 ID 的低 4 位作为 master 标识高位留给互联逻辑分配。每个 master 的合法 ID 范围可以预先把配置写到寄存器里之后每次比较地址通道上的 ID 即可。AxPROT 是辅助信息。它包含特权等级、安全位和指令/数据访问标记。但注意在 FPGA 原型的开源 SoC 里不少外设把 AxPROT 固定拉成 0这个信号有时不可信。所以我的建议是权限判断以 ID 为主PROT 只能作为追加条件不能作为唯一依据。2.2 为什么只在 AW/AR 地址通道做检查AXI 有五个通道写地址AW、写数据W、写响应B、读地址AR、读数据R。权限检查最自然的切入点是 AW 和 AR因为地址在发起请求的时刻就已经确定权限判断不需要等数据。如果你试图在 W 通道上做检查问题会变得很难办。一个写 burst 可能有多拍数据W 通道看不到地址你只能靠内部状态记录“当前正在传输哪个事务”再回查地址是否合法。这意味着你需要在 MPU 里维护一个深度的跟踪表而且一旦有多个 outstanding 写事务交错状态机复杂度会急剧上升。在地址通道检查还有另一个好处不会增加数据路径上的组合逻辑延迟。我们做 FPGA 原型时时序收敛压力本来就大如果 MPU 需要在 R 或 W 路径上插入比较器最坏情况会吃掉一两个 LUT 级影响片上内存的工作频率。2.3 串接方式与死锁陷阱非法请求不能被“晾着”设计 MPU 时有一个很容易被忽视的基础规则AXI 要求每个地址通道握手必须最终被响应。如果 MPU 发现非法请求后选择不拉高 AWREADY 来“堵住”它这在仿真里看似可行实际在总线上会导致 master 永远等待造成互联死锁。因为 master 发出 AWVALID 后会占用内部资源等待 READY其他事务也会被阻塞。正确做法是MPU 必须以普通 slave 的身份接受这个非法请求回 AWREADY但拒绝把请求转发给真实内存然后在 B 通道上返回一个错误响应。对于读事务同理接受 AR在 R 通道上返回一个错误读数据并给出 RLAST 结束这个 burst。正是这个响应机制成了我这次项目里 AI 生成 RTL 的第一处翻车点——它生成的代码在检测到非法地址后直接不拉 READY导致整个总线的写通道卡死。这一点我会在第三章详细讲。3. 用 AI 辅助搭框架一份需求提示词和一版可用的 RTL3.1 我给 AI 的第一份需求描述先把“AI 辅助”这件事说清楚。我用的不是定制工具就是常见的通用大模型全程对话式开发。真正的难点在于能不能把设计需求描述到让模型生成接近可用的代码。我第一段 prompt 大致是这样写的设计一个 AXI4-to-AXI4 Memory Protection Unit挂在某个 AXI slave 前面。 需求点 1. 在 AW 通道上检查写访问在 AR 通道上检查读访问。 2. 支持 4 个配置区域每个区域有 base、mask、master_id_mask、读写使能位。 3. 只有 ID 与区域配置匹配的 master 才允许访问。 4. 命中非法访问时AW 通道必须正常握手但不要向前转发 在 B 通道返回 SLVERRW 通道需要吸收所有写数据直到 WLAST。 5. 读非法时接受 AR在 R 通道返回单拍错误数据并回 RLAST。 6. 所有输入输出均为 AXI4 接口信号使用 Verilog 编写。这里我把第四点“吸收 W 数据”提前写进了需求是因为我知道这是最容易漏掉的设计点。但即便如此AI 给出的代码仍然不能直接上板。3.2 AI 生成代码与现代码修补记录AI 生成的 V1 版本端口定义和模块边界相当完整直接搭出了一个可综合的骨架至少省了我半天写模块例化和接口声明的时间。它的区域比较逻辑也很规整用了掩码方式比较 base一眼就能看出 intent。但真正集成进仿真环境后立刻暴露了三个问题第一错误响应 BID 与事务 ID 不对应。AI 用了一个简单的 reg 记录“最近一次非法事务的 ID”。如果同一周期内有一个非法 AW 和一个合法 AW 同时 outstanding那么非法事务的 BID 可能被合法事务覆盖。要修这个最保守的方式是把 MPU 的每个通道做成一次只接受一笔事务的简单模式非法事务的 ID 用 FIFO 暂存。如果要求高性能就得为每个 outstanding 事务维护单独的 error 队列。第二burst 跨越区域边界时只能检查起始地址。AI 的代码用addr与区域掩码比较但 AXI burst 的长度是可变的尤其是 INCR 类型最后一拍地址可能是start_addr 8 * len - 1。如果起始地址在合法区域但 burst 末尾跨进了非法区域V1 版本会放行整个事务。修复方式是把地址与addr 8*burst_len - 1同时参与比较两者都合法才放行。第三W 通道吸收逻辑与 WLAST 对齐错误。AI 写了一个discard_w状态位但它忽略了被拒绝的写事务的 WLAST 可能与 AW 握手不在同一个周期到达的情况。如果 WLAST 因为 W 通道冲突被延后state 机可能提前退出导致后续数据没有吸收。最终我改成在 AW 被拒绝时直接把discard_w保持到接收到 WLAST 为止在握手周期里用WVALID WREADY作为推进条件。3.3 AI 最擅长与最不擅长的地方AI 辅助编码的价值不在于“它一次写对”而在于“它能快速生成一个粗糙但完整的参考实现”。我们可以把精力集中在协议语义和边界情况审查而不是从零开始逐行写端口。但它对这种带全局时序约束的状态机确实容易犯“局部正确、整体错误”的毛病。比如它经常会默认 AXI master 会等上一个事务的 B 响应后才发下一笔这是很多初学者的理解。但 AXI4 支持多个 outstanding 事务master 可以连续发多个 AW不需要等 B。AI 如果不明确收到约束就会把 MPU 隐式实现成串行处理这在低负载下仿真看不出问题一旦压到满带宽读延迟和写响应时间都会崩。所以我的结论是AI 可以用来生成代码但它生成的是“起点”不是“终点”。每一处握手、每一根与响应相关的信号必须由工程师亲自以协议手册为标准过一遍。4. 关键实现拆解区域寄存器、地址比较器与错误响应4.1 寄存器配置接口MPU 区域配置寄存器我用 AXI-Lite 接口提供这样 CPU 可以通过简单映射配置权限。每个区域的寄存器布局如下字段位宽含义BASE32区域基地址MASK32地址掩码1 表示忽略该位ID_FILTER8允许的 master ID 掩码RW_EN2bit0 读使能bit1 写使能REGION_EN1区域有效位地址比较采用掩码方式而不是上下限方式因为在 FPGA 中掩码比较只需要(addr ~MASK) (BASE ~MASK)一组 LUT不需要减法器。对大多数片上内存区域按 2 的幂次对齐完全够用。4.2 简化地址比较逻辑核心比较逻辑的 Verilog 大概长这样wire [31:0] addr_comp s_axi_awaddr ~region_mask_0; wire [31:0] base_comp region_base_0 ~region_mask_0; wire hit_region_0 region_en_0 (addr_comp base_comp); wire master_ok_0 (s_axi_awid[3:0] id_filter_0) id_filter_0; wire write_ok_0 hit_region_0 master_ok_0 rw_en_0[1]; assign write_allowed write_ok_0 || write_ok_1 || write_ok_2 || write_ok_3;这里有一点要注意ID_FILTER我采用的是“位掩码匹配”而不是“相等匹配”。比如 CPU 发出的 ID 低 4 位既可能是 0也可能是 8当软件配置id_filter 4b1001时只有 ID 为 0 或 8 的请求才能通过。这样对 master ID 枚举比较灵活。但如果你希望严格按值匹配换成比较即可两者在综合资源上差别不大。4.3 错误响应的三种处理策略被拒绝的事务我支持三种后续处理方式返回 SLVERR 但不中断 CPU适合普通外设非法访问系统可以忽略继续运行。产生中断并记录 fault 信息把被拒绝的地址、ID、读写类型、时间戳存入寄存器供软件事后分析。这一次项目我强烈依赖这个功能否则那些随机越权请求根本没办法定位。触发系统级错误处理适合安全关键场景比如把错误响应连接到中断控制器让 CPU 进入错误处理流程。读通道的错误响应比较容易在 AR 被非法命中时MPU 作为 slave 接受 AR然后在 R 通道返回一拍RVALID数据自愿RRESP置为SLVERR同时RLAST拉高。对单拍请求这样做天然正确。写通道错误响应是 AI 翻车的重灾区我再强调一遍关键点AW 被非法拒绝后W 通道的数据还是要继续被 MPU 吃掉的。这里的“吃掉”指的是 MPU 要拉高 WREADY接收数据但不把数据写入内存一直吃到 WLAST 为止。吃干净后MPU 才能回 BVALID并且把 BID 设置成被拒绝事务的 ID。如果漏掉这个“吸收数据”的环节master 会因为 W 通道没有 READY 而卡死在半路如果吸收逻辑提前结束又可能在 WLAST 还没到的时候误吞下一个事务的数据。这类问题在纯功能仿真里极难发现因为大多数测试平台不允许 master 发出与地址无关的连续写数据。5. 验证这门“门卫”定向用例、随机激励与 AI 断言5.1 用 SystemVerilog 搭建带真实内存模型的测试平台MPU 本身逻辑不大但验证难点在于它必须模拟真实的总线行为。我搭的测试平台分三层AXI master 一侧可配置的激励生成器能发单拍、burst、乱序 ID 的事务。MUT被测模块就是这个 AXI MPU。AXI slave 一侧一个真正可读写的内存模型并额外引出一个monitor用来记录“哪些地址被实际访问了”。关键不是测 MPU 的响应而是测MPU 之外的受保护内存是否真的没有被非法请求动过。我用一个全局断言检查任何 ID 不在合法列表里、地址在保护区内的事务绝不允许在 downstream slave 端口上出现对应的 AWVALID 或 ARVALID。这条断言比单纯检查 B 通道错误响应要严格得多因为有的实现会错误转发后又“补救性”地返回错误这在直观上好像没问题但已经破坏了保护语义。5.2 随机激励抓到的两个真 bug随机激励是我这次项目里性价比最高的投入。我让随机生成器专门打击边界条件地址随机落在区域边界前后 16 字节内burst 长度随机为 1、4、8、16且跨越边界ID 随机覆盖合法、非法、以及“合法 ID 加高位脏比特”的情况区域尽量现场切换使能模拟 CPU 运行中重配权限随机跑了一晚上之后抓到了两个仿真定向用例没暴露的问题。第一个是 burst 跨区域问题。虽然我在第三章已经人工修补了起始地址长度的比较但随机跑到一个 16 拍的 burst起始地址在合法区最后一个字节恰好越过区域掩码的边界时比较逻辑仍然放行了。原因是我的修补补在了 AW 通道一个“组合判断”上但这个判断没有把AxSIZE考虑进去。AxSIZE决定每拍数据字节数8 * len只有在固定SIZE38字节时才是正确的末端偏移。随机测试中出现了SIZE24字节和SIZE3混合的场景末端地址计算错误。修正后我直接改成一个基于addr (len SIZE) - 1的简单公式再在验证环境里单独做了一轮针对SIZE的扫描测试。第二个 bug 和 ID 漂移有关。随机测试里我把 master 的 ID 设成一个高 4 位非零的值比如0x83配置寄存器里写的却是0x03。MPU 的掩码匹配没问题能拦截但错误响应记录里存下的 fault 地址和 ID 对不上导致中断处理程序看到的是错误来源。这个问题在仿真里只是“寄存器记录不准确”在真实系统里就是“明明知道有人越权却找不到是谁”。解决方式是把 ID_FILTER 的匹配逻辑分为两层先对高位做“无关位”屏蔽再对低 4 位做精确匹配。5.3 SVA 断言与 AI 辅助把规范变成可机检的动作我还做了一份比较完整的 SVA 断言清单一部分由 AI 生成初稿我再逐条校对。比较核心的是下面几条property p_no_illegal_downstream_aw; (posedge aclk) disable iff (!arstn) !(downstream_awvalid $past(illegal_aw_hit)); endproperty property p_read_error_has_rlast; (posedge aclk) disable iff (!arstn) (illegal_ar_hit |- ##[1:$] RVALID RLAST (RRESP inside {SLVERR})); endproperty property p_wwrite_discard_until_wlast; (posedge aclk) disable iff (!arstn) (discard_w WVALID WREADY |- until (WLAST WVALID WREADY)); endpropertyAI 生成断言的效率很高但有两处我修正后才好用。一是它默认 SVA 的disable iff能自动处理复位实际上还要自己检查复位释放时序。二是它写|-的时序逻辑时经常把事件序列写短无法覆盖多周期 outstanding 场景。用 AI 辅助写断言的正确姿势是把自己维护的断言清单发给它让它按规范生成 SVA 模板然后人工核对属性里所有时钟和复位引用。当作一个快速输入助手很好但不能省掉协议理解。6. 从 RTL 到 SoC 集成踩坑记录与 AI 辅助的边界6.1 AXID 在真实系统中的漂移仿真环境里我可以直接控制 master 的 ID 值但接进整个 SoC 后AXI 互联可能会对 ID 做重映射。比如系统里有一个 AXI SmartConnect 或者 Crossbar它可能把 master ID 合并、扩展甚至在多 master 仲裁后重新编码 ID。我第一次上板后发现一个本来完全合法的 DMA 访问被 MPU 拦截了。用 ILA 抓总线看到的 ID 是0x10但我配置的合法 ID 掩码是0x00。看起来是互联把多个 master 的 ID 重新映射过已然不是原始 master 的 ID 值。解决方式很简单在把所有 master 的请求合并到同一个 MPU 前直接用一组独立的protect_id信号或一个只读寄存器回读互联重映射后的 ID。如果互联支持 parameter 配置就直接把ID_WIDTH调整到与 MPU 配置一致而不是盲信 ID。对于连接方式我也做了一个调整。原来打算在全局共享的 AXI slave 前挂一个 MPU统一保护所有 master。后来发现这会让 MPU 承载所有 master 的复杂 ID 映射配置表会很难维护。最终改成每个受保护的 master 独立一个 MPU 实例权限配置各自独立。这样区域配置更清晰某个 master 的 ID 漂移只影响它自己的 MPU不会把其他 master 一起带崩。6.2 默认策略必须是 fail closedMPU 在没有任何区域配置生效时应该放行还是拒绝很多“方便型”设计会选择默认放行这样系统上电后不需要软件配置也能跑起来。但这次项目让我彻底放弃了这种思路。如果默认放行那么只要软件在配置 MPU 之前发生一次非法访问保护就是无效的。而且上电后到配置完成之间这段时间是一个完全敞开的窗口DMA 这类硬件可能已经在上电自检时就访问了受保护区域。安全关键的设计必须 fail closed——默认拒绝只有显式配置的区域和 ID 才能访问。当然 fail closed 的代价是要保证软件启动阶段能正确配置所有区域。我的做法是允许编译时通过 parameter 指定一套“初始保护表”比如把 CPU 和调试接口设为默认白名单其他 master 全部拒绝然后软件运行后再动态调整。这样既保证上电安全又不会因为配置没到位一开机就崩。从实际验证看MPU 加入后对片上内存带宽的影响几乎可以忽略。因为它只作用在地址通道组合比较逻辑大约增加几个 LUT 的路径延迟对 BRAM 和 URAM 的工作频率没有明显影响。如果你担心时序还可以把比较结果打一拍但代价是地址通道多一个 cycle 的流水延迟这对大多数 memory-mapped 场景来说不敏感。6.3 三条关于 AI 辅助研发的个人体会这次项目最让我感慨的不是 MPU 本身而是 AI 参与流程后整个团队的节奏发生了变化。第一条体会是AI 最擅长的是把“需求文档”翻译成“参考实现”最不擅长的是把“协议规范”翻译成“正确时序”。我给它提供需求点列表时它生成的代码很像样一旦让它自己理解 AXI outstanding、burst 边界、错误响应这类复杂交互它就暴露出典型的“局部时序正确”问题。第二条体会是验证成本比编码成本高得多AI 生成代码越激进验证要求越要激进。因为 AI 不会像人一样每写一段就自动权衡“这个会不会影响其他通道”它通常只是满足当前 prompt 和当前代码上下文。所以与其花大把时间让 AI 改代码不如把时间花在约束随机激励和断言上。我在这次项目里让 AI 生成代码只用了大半天但验证环境、随机用例、断言清单的前后迭代用掉了两天半。第三条体会是AI 辅助最有效的位置其实在“生成表格化配置”和“生成测试用例”上而不是生成核心状态机。我后来把所有 master 的访问权限整理成一张 CSV 表让 AI 直接转成 SystemVerilog 配置宏、寄存器默认值、甚至 SVA 里的条件表达式准确率和效率都远超让它直接写 MPU。如果你想在类似项目里试水 AI 辅助研发我建议从这个角度切入比硬让它写总线桥靠谱得多。最后再分享一个我在整个项目里收获最大的小技巧在真正开始写 RTL 之前先把访问矩阵定义清楚哪根 master 能读写哪块区间用一张表画出来。这张表不仅是给 AI 的 prompt也是后面验证断言和审查代码的统一依据。我这次就是靠着这张表硬把一个隐藏在随机边界里的权限漏洞挖了出来。