
最近在做一个 SoC 里的小 IP属于那种“不起眼但关键时刻要命”的模块外设访问控制 IP。简单说就是给总线上的外设访问加一道硬性的闸门所有主设备CPU、DMA、AI加速器访问外设寄存器和内存区域都必须先过这一关。我这次整个设计流程都拉了 AI 工具进来一起干从请求拦截策略的定义再到权限表的原子更新机制AI 承担了相当一部分骨架代码和验证环境生成的工作。这篇文章就把整个过程拆开讲重点是请求拦截的判定逻辑、权限配置怎么做到可靠更新以及 AI 辅助设计时哪些坑我已经替你踩过了。这个方向的读者应该都是做数字 IC 设计或者验证的同行。如果你正在设计总线防火墙、安全隔离模块、外设代理一类的组件或者只是好奇“AI 写 RTL 到底靠不靠谱”这篇东西都能给你一些参考。1. IP到底解决了什么问题外设访问失控的底层风险1.1 没有访问控制的SoC有多被动先举个最常见也最危险的场景。SoC 里挂了一堆外设UART、SPI、GPIO、定时器、DMA 控制器还有片内 SRAM 的某些保留区域。传统做法是所有 master 都能看到完整地址空间谁想访问谁就发起事务总线 network 一概转发。你说这是“设计简单”但代价是没有一点安全边界。比如说某个被攻破的 AI 加速器固件理论上可以直接往 DMA 控制器的描述符地址写数据然后 DMA 就去改写内存任意位置。这类攻击路径在真实产品里被爆过很多次不是危言耸听。外设访问控制 IP 要干的活就是把这层风险用硬件掐死。它的位置在总线主设备master和从设备slave之间每个访问外设的事务都必须经过它的仲裁和检查。检查通过事务放行到真正的目标外设检查不通过直接返回错误响应外设寄存器根本看不到这个请求。这里有个关键点这个 IP 不是简单的地址译码器。地址译码只是决定“路由到哪”而外设访问控制决定了“允许不允许”。一个放在 AXI Address Decode 后面的访问过滤器它在事务还没到达外设之前就要基于地址、设备 ID、安全属性、事务类型做一整套策略判断。1.2 需求拆解不只是“加一个MUX”那么简单我一开始也以为这就是给 AXI slave 端口套一层地址过滤逻辑后来做完 feature list 才发现里面全是细节。一个完整的外设访问控制 IP 至少要涵盖这些需求支持多 master 对单外设的差异化访问策略A master 能读不能写B master 只能访问特定子区域地址区间必须是可配置的用寄存器定义 region 的基址、上限、访问类型而不是写死支持 secure 和 non-secure 事务的属性区分通常要和系统级 TrustZone 或者 SoC 安全策略联动有中断输出一旦发生违规访问可以通知安全处理器做后续审计和处理权限表动态可更新且更新过程必须“原子”完成不能留下一个中间状态让非法请求钻空子最后一条是这个 IP 的灵魂。如果是固定权限用一堆组合逻辑比较器就干完了但只要权限需要软件动态配置就必须解决“配置过程中访问请求怎么办”的问题。权限更新不是写字寄存器它是一项并发控制工程设计。2. 顶层架构与关键技术选型过滤器该挂在总线哪里2.1 挂载位置的选择AXI中间层和从设备侧外设访问控制 IP 在 SoC 里的挂载方式直接决定了后面所有逻辑的设计复杂度。我这次的目标外设挂在 AXI 总线上选择了在 AXI Interconnect 和目标 slave 之间插入一个 AXI-to-AXI 过滤器而不是去改 interconnect 内部。这么选的道理很直接改 interconnect 需要动整个总线矩阵验证工作量很大而且会影响其他无关外设的事务延迟。做成一个独立的、穿在路径上的过滤器它只关心“从上游 master 过来的请求要不要放行”对自己身后的真正外设完全透明这个隔离性对模块复用非常友好。但代价是这个 IP 自己必须完整实现 AXI 协议的四通道握手AXI 的 out-standing 特性、burst 传输、narrow transfer 这些都要处理。如果只是做地址过滤很多实现可以简化但因为挂在真实总线上饿死、死锁、写响应乱序这些问题都得在架构里考虑进去不然验证环境里边跑边崩。2.2 请求拦截的判定流程地址、ID、类型三把锁请求进来以后判定逻辑按以下顺序走地址空间初步过滤把不属于任何配置 region 的请求直接定义为无效访问地址精确匹配到某个 region取出该 region 的权限配置检查 master 身份验证请求的 AXI ID 或者系统分配的主设备标签是否在允许访问的清单里检查事务属性读写类型是否被允许 secure 事务是否被降级 burst 长度是否超出区域余量全部通过才真正放行到目标外设否则返回错误响应这套流程如果每个请求都从头到尾完整执行就会带来不小的流水线深度和时序压力。我为每个 region 设计了一个小的匹配状态再用一个查询状态机串行检查基础时延控制在两拍以内。两拍对于 AXI 协议来说是完全可以接受的。2.3 为什么用“状态机 规则表”而不是纯组合逻辑很多第一次看这类 IP 的人都问直接做组合逻辑地址同时和所有 region 比较谁匹配就按谁的策略走不是更快吗纯组合的“并行比较器”方案只有在规则数量很少2~4条且不需要动态配置的前提下才合理。规则一多比较器阵列的扇出马上就爆布线延迟上去了时序收敛困难。更重要的是动态配置需要寄存器存储规则内容你需要一组可写的配置寄存器这本身就是时序路径上的大负担。我最后采用的是规则表 查询状态机。规则表存放在一组寄存器阵列中每个 region 的配置基址、上限、读使能、写使能、主设备掩码各自独立CPU 通过 APB 从接口配置。请求到达时状态机从 region 0 开始逐条匹配匹配成功后根据命中规则给出响应。它的优势很明显规则条数可以做到很大比如 32 条不影响时序配置可以动态改改完立即生效。缺点是如果规则数太多串行查询会拖慢请求响应。我这次 8 个 region 跑在 400MHz 下实测没问题。如果未来要支持上百条规则那就得上 TCAM 或者 hash 方案了那是另一个课题。3. 权限原子更新的设计细节从影子寄存器到并发窗口3.1 “原子更新”到底在防什么安全性上最微妙的点在于权限表是多个寄存器组成的。比如你要给某个 master 开放一个新外设的访问权限同时关掉它对另一个外设的权限这至少涉及两个 region 配置的改写。如果没有原子更新机制CPU 先写寄存器 A再写寄存器 B。在 A 写完、B 没写的窗口里系统要么处于“两个外设都能访问”的过宽状态要么处于“两个外设都不能访问”的过窄状态。前者可能让一个本来不该有权限的请求通过后者可能让正在进行的实时业务中断。这种“中间状态”在软件并发场景下是可以容忍的但在硬件安全场景里不行。因为总线上的请求是持续不断、完全随机的只要中间状态存在非法请求就有机可乘。权限原子更新的最终效果就是要让整个策略表“要么全部新值生效要么全部旧值保持”不存在第三种状态。3.2 影子寄存器 Commit 的双缓冲机制标准做法是双缓冲Double Buffering我拆成两个平面“影子平面”和“活动平面”。软件更改权限时先写影子平面所有修改都发生在影子区域对当前硬件判定逻辑完全不可见。当软件确认所有新配置都已写入影子平面后再对一个专用寄存器发出 commit 操作。commit 信号在内部触发一个状态机把影子平面所有内容一次性拷贝到活动平面。拷贝过程只需要几个时钟周期期间新请求进来看到的是完整的旧策略在拷贝开始前被接收或者完整的新策略在拷贝完成后绝不会看到新旧混合的法规。这里你可能会问commit 拷贝的这 2-3 拍请求被堵住了吗我的处理是让请求在那几拍里保持等待用 AXI 的 ready/valid 握手机制做 pause。下游外设不感知这个等待最坏情况就是一次事务多等三拍对于外设访问的场景完全无所谓。3.3 并发请求与更新窗口的竞态处理真正容易翻车的是边界情况commit 发起的同一拍正好有一个请求完成了地址匹配拿到了活动平面里某一条规则而这条规则正在被影子平面覆盖。虽然拷贝过程极短但这个微妙的重叠窗口必须处理。我的方案是在 commit 周期插入一个“请求冻结窗口”。commit 一开始状态机拉高内部 busy 信号所有新到达的请求先在这个信号前排队不进入匹配流程。拷贝结束后busy 撤销排队的请求按到达顺序逐个处理。这个机制实现成本很低只需要在入口加一个两级 FIFO但安全性提升很大。这之后还有一个问题如果软件在影子平面写了一半突然想放弃这次更新怎么办我的做法是提供一个软复位位能够把影子平面的内容重新载入当前活动平面的值丢弃所有未提交的修改。这个功能刚开始觉得是多余的后来在调试系统固件时救了我两次很值得做进去。4. AI 辅助设计的实际体验从 RTL 到 UVM 都让它干了4.1 用 AI 写 RTL能跑但绝不能直接信我这次用 AI 出 RTL 骨架大致流程是先把接口时序图、寄存器列表和状态机描述整理成需求文档然后让模型按照文档生成 SystemVerilog 代码。值得肯定的是AI 对典型结构寄存器读写译码、APB 从接口、基础状态机的生成质量是相当稳定的能节省至少一天的工作量。但问题也很典型AI 生成的状态机常常缺少异常路径处理比如 APB 的写 strobe 和地址错位、burst 传输中跨 region 边界、master 身份未定义时的默认动作等等。这些都是很容易被 AI 忽视的真边界一旦仿真跑起来覆盖率工具分分钟教你做人。所以我把 AI 生成的 RTL 当“初稿”而不是“成品”每一行代码都要过自己的脑子。我用了一个技巧让 AI 同时生成一个设计注释文档说明每个状态转移的条件和原因。这个注释文档带来的额外价值超出了我的预期review 代码时完全可以对着它逐条核对。4.2 用 AI 搭 UVM 验证环境大幅度降低重复劳动UVM 环境里最枯燥的部分是寄存器模型、sequence、配置类这些模板化组件。我这次也全部交给 AI 生成基础版本然后自己补充约束条件和功能覆盖点。AI 生成 register model 的准确率相当高因为这类代码的规律性很强输入输出很明确。sequence 部分也不难但要注意 AI 生成的 transaction 约束往往太宽松导致验证没跑到真正的边界条件。比如随机地址生成时它默认就是在整个地址空间均匀撒点对于访问控制 IP 来说最有价值的测试是精确覆盖 region 的基址、边界、越界一步这些约束我全部手动加上。断言assertion部分是我最克制使用 AI 的地方。AI 能写简单的 immediate assertion但复杂的并发断言比如 atomic update 期间不允许非法请求漏过它写得一塌糊涂我自己写的 SVA 反而很快。这里我强烈建议AI 可以帮你做断言框架核心安全断言一定要自己人工确认。4.3 AI 生成代码的坑两个真实翻车现场第一个翻车现场出现在 APB 接口的写时序上。AI 默认认为 APB 的 set-up、access、tear-down 三个阶段里写数据在 PWRITE 有效后立刻就可以被采样但它忽略了寄存器总线中“先译码再写入”的内部延迟导致某些配置寄存器的写入有概率失败。这个问题最后靠我添加写完成握手信号才解决AI 生成的代码整体节奏和真实 APB 信号模型是有出入的。第二个翻车现场在 burst 请求的地址更新上。AXI burst 的地址在每个节拍递增AI 写的地址判断逻辑只检查了首地址是否落在 region 内没有检查整条 burst 的地址范围是否完全覆盖在 region 内。遇到跨 region 的 burst就直接放行了半个区域外的地址这在安全上完全不能接受。修这个 bug 不复杂但要意识到AI 对“地址连续性”这件事的直觉是不够的它擅长的是单点判断不擅长范围连续判断。5. 仿真验证与问题排查实录5.1 UVM 环境搭建的三个教训这套 IP 的 UVM 环境我前后搭了大概三天踩过的坑值得说几个。第一个教训是环境层次不能太深。第一次我按公司标准分了五层结构test - test_base - env - agent - model。结果调试起来每个层级都在打印信息噪声巨大。后来简化成 test 直连 envenv 内含一个 agent 和一个 scoreboard层次的减少让 debug 效率提高非常明显。第二个教训是寄存器模型要单独做。UVM 自带的uvm_reg机制如果直接从 RTL 顶层例化会把 APB 从口的所有细节暴露给 model非常不灵活。我最后把寄存器模型单独放一个模块通过 set 和 get 接口和 scoreboard 通信IP 内部的信息对验证环境做到最小暴露。第三个教训和并发有关commit 窗口期间如果要发一个随机写权限表的 sequence必须先在环境里全局同步。最简单的做法是发一个 barrier event让所有 sequence 等待。如果忽略这个同步你会在波形里看到一些完全无法解释的临时权限配置。5.2 常见问题速查表现象根因解决思路配置影子寄存器后功能立即变化影子平面和活动平面地址映射写错检查寄存器地址 offset尤其是 commit 控制寄存器的位置burst 请求只检查首地址导致越界地址连续性判断缺失在匹配逻辑前增加 burst 地址完整区间计算commit 期间非法请求漏过未添加请求冻结窗口入口加 FIFOcommit 时暂停新事务拷贝结束再恢复随机验证时权限配置被意外破坏缺少 sequence 同步加 barrier event 或统一 sequence 优先级APB 写入偶尔不稳定写完成握手缺失确认 APB 时序增加内部写完成信号AI 生成规则表的默认值不安全默认全通上电复位默认值设为全拒绝后续由安全软件按需配置这张表是我实际调试过程的提炼如果你也做类似的安全过滤模块基本可以照着排查。5.3 覆盖率驱动的收敛策略覆盖率是另一个重头戏。外设访问控制 IP 的功能覆盖点主要集中在请求类型×权限组合、非法访问断言触发、commit 冻结窗口等待、burst 边界穿越。我这次用 SystemVerilog covergroup 收集覆盖点配合随机约束跑到仿真时间大约 2 小时行覆盖率 91%功能覆盖率 96.8%。剩下 3.2% 的功能覆盖点基本都是非常极端的组合比如 32 位 master 同时在 commit 冻结窗口里发起 burst 请求又同时撤销。这些最后用定向测试用例补齐。覆盖率收敛这件事 AI 帮不了太多因为它的随机种子策略和真实设计意图之间缺乏针对性。我花了两天手动补定向用例最后功能覆盖率到 100%行覆盖率到 97.2%对可综合 IP 来说已经算不错的交付标准。6. 从规格到交付的完整实操流程清单6.1 步步为营的设计步骤我自己这次用的流程按节点拆成八步每一步都有明确输出需求规格定义列清楚支持的 master 数量、外设 region 数量、事务类型、安全属性这是后面所有工作的锚点微架构文档画出数据通路、状态机转移图、寄存器列表这一步占到整个研发周期的 30%别省AI 生成 RTL 初稿把微架构文档打包给 AI生成基础代码记得同步让它产出注释文档人工 review 和补边界逻辑重点看 burst 地址连续性、并发窗口、默认值安全性独立验证环境搭建UVM 环境、寄存器模型、断言、功能覆盖点随机仿真与定向用例补全这个环节覆盖率工具说了算FPGA 原型验证或硬件加速验证我这次用 FPGA 原型验证平台跑通验证时间比纯仿真快两个量级交付与集成文档RTL、验证报告、覆盖率报告、集成手册全部归档6.2 关键模块的参考实现片段有些代码结构几乎每次都能复用贴两个核心部分。第一个是 APB 从接口的写握手部分这也是 AI 第一次翻车的位置// 写数据在 PADDR 有效后要等 write_done 拉高才算真正完成 logic [7:0] mem [REG_COUNT]; logic [31:0] write_data_q; logic write_pending; logic write_done; always_ff (posedge clk or negedge rst_n) begin if (!rst_n) begin write_pending 1b0; write_done 1b0; end else begin if (PENABLE PWRITE PSEL) begin write_data_q PWDATA; write_pending 1b1; write_done 1b0; end else if (write_pending) begin // 这里是核心写数据是组合逻辑的反馈需要一个稳定周期 mem[PADDR[$clog2(REG_COUNT)-1:0]] write_data_q; write_pending 1b0; write_done 1b1; end end end第二个是请求冻结窗口的简化表示logic commit_busy; logic req_freeze; assign req_freeze commit_busy request_valid; // 当 commit 时捕获当前请求等 busy 释放后继续 always_ff (posedge clk or negedge rst_n) begin if (!rst_n) saved_request 0; else if (req_freeze) saved_request request_payload; else if (!req_freeze freeze_ack) saved_request 0; end这段代码的逻辑很清晰commit_busy 拉高时新请求进不了匹配状态机冻结窗口内的请求被保存等 busy 释放后按保存的数据继续处理。实际项目中 freeze 信号可能需要多包括一个周期但这个思路是通用的。6.3 一点关于交付的额外建议最后说一点我在集成阶段学到的体会。外设访问控制 IP 交付时光给 RTL 和验证报告是不够的你还需要提供一套安全配置示例和软件驱动代码。因为最终用户怎么使用这套权限配置直接影响系统的安全性。我这次附带了一套 Linux 下的设备树配置说明和一个安全配置 API 参考集成方照着填 region 参数就能跑少了无数来回沟通成本。AI 辅助设计这个事我的态度从“试试看”变成了“必须用”但用得很克制。RTL 骨架、寄存器模型、模板 sequence 这些结构化强、变化少的活儿AI 做又快又好反观需要深层安全判断、并发窗口分析和边界条件推导的部分AI 只能给点启发最后的责任还是得设计者自己扛。这个外设访问控制 IP 从需求明确到交付完整走完大概六周对比纯手工估计能省 30% 的验证代码编写时间但主要体现在环境和模板搭建上真正的重要逻辑还是靠人。如果你也要做类似的 IP我的建议是把“原子更新”和“并发窗口”相关的问题想透再动手写代码。这块逻辑一旦有个小疏漏功能验证阶段看起来都正常一旦集成到复杂 SoC 里出现的是那种最难定位的偶发安全违例问题。