☰
外设访问控制IP设计实战:AXI安全过滤与权限原子更新机制
2026/9/29 5:22:17 网站建设 项目流程

最近在做一个 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、类型三把锁

请求进来以后,判定逻辑按以下顺序走:

  1. 地址空间初步过滤,把不属于任何配置 region 的请求直接定义为无效访问
  2. 地址精确匹配到某个 region,取出该 region 的权限配置
  3. 检查 master 身份,验证请求的 AXI ID 或者系统分配的主设备标签是否在允许访问的清单里
  4. 检查事务属性,读写类型是否被允许, secure 事务是否被降级, burst 长度是否超出区域余量
  5. 全部通过才真正放行到目标外设,否则返回错误响应

这套流程如果每个请求都从头到尾完整执行,就会带来不小的流水线深度和时序压力。我为每个 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 直连 env,env 内含一个 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 期间非法请求漏过未添加请求冻结窗口入口加 FIFO,commit 时暂停新事务,拷贝结束再恢复
随机验证时权限配置被意外破坏缺少 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 步步为营的设计步骤

我自己这次用的流程,按节点拆成八步,每一步都有明确输出:

  1. 需求规格定义:列清楚支持的 master 数量、外设 region 数量、事务类型、安全属性,这是后面所有工作的锚点
  2. 微架构文档:画出数据通路、状态机转移图、寄存器列表,这一步占到整个研发周期的 30%,别省
  3. AI 生成 RTL 初稿:把微架构文档打包给 AI,生成基础代码,记得同步让它产出注释文档
  4. 人工 review 和补边界逻辑:重点看 burst 地址连续性、并发窗口、默认值安全性
  5. 独立验证环境搭建:UVM 环境、寄存器模型、断言、功能覆盖点
  6. 随机仿真与定向用例补全:这个环节覆盖率工具说了算
  7. FPGA 原型验证或硬件加速验证:我这次用 FPGA 原型验证平台跑通,验证时间比纯仿真快两个量级
  8. 交付与集成文档: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 <= 1'b0; write_done <= 1'b0; end else begin if (PENABLE && PWRITE && PSEL) begin write_data_q <= PWDATA; write_pending <= 1'b1; write_done <= 1'b0; end else if (write_pending) begin // 这里是核心:写数据是组合逻辑的反馈,需要一个稳定周期 mem[PADDR[$clog2(REG_COUNT)-1:0]] <= write_data_q; write_pending <= 1'b0; write_done <= 1'b1; 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 里,出现的是那种最难定位的偶发安全违例问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询