是什么:AMBA(Advanced Microcontroller Bus Architecture)是 ARM 在 1996 年推出的片上总线(on-chip bus)开放标准,定义了 SoC 内部各 IP 之间"怎么连线、怎么握手、怎么传数据"。它不是一根总线,而是一族协议:最常用的三兄弟是 APB(Advanced Peripheral Bus,外设慢速总线)、AHB(Advanced High-performance Bus,系统总线)、AXI(Advanced eXtensible Interface,高性能总线)。三者按带宽/复杂度递增、按功耗/实现成本递减排列:APB 只有约 10 根信号、两拍一次传输;AHB 半双工地址数据复用、支持流水线;AXI 按 5 个独立通道(读地址/读数据/写地址/写数据/写响应)全双工跑,支持乱序、突发、多笔 outstanding。
为什么:一颗 SoC 里 CPU 要访问 DDR、NPU 这些"大胃王",也要配置 UART、GPIO、定时器这些"小慢件"。如果全部用 AXI 互联,慢速外设也要承担高复杂度接口的面积功耗;如果全部用一根慢总线,高速模块又会被拖死。AMBA 的答案是分层异构:高速模块挂 AXI/AHB,慢速外设挂 APB,中间用桥(bridge)把两层"翻译"起来 —— 各取所长,面积功耗最省。
怎么做(在 SoC 里的共存结构):典型结构是"CPU/ DMA 挂 AHB(或 AXI)系统总线 → AHB→APB 桥作为 APB 域里唯一的主机 → UART/GPIO/Timer/看门狗/NPU 配置寄存器等作为 APB 从机挂在 APB 总线上"。CPU 写 NPU 的控制寄存器时,先在 AHB 上发一笔写,桥收到后把它转成 APB 的 SETUP→ACCESS 两拍时序送达 NPU 寄存器。
| 协议 | 定位 | 典型挂载对象 | 关键特性 | 复杂度/功耗 |
|---|---|---|---|---|
| APB | 外设慢速总线 | UART、GPIO、Timer、WDT、NPU 配置寄存器 | 非流水线,每笔至少 2 拍(SETUP+ACCESS),无突发 | 最低/最低 |
| AHB | 系统总线 | CPU、DMA、SRAM、Flash 控制器 | 地址数据复用、流水线、突发传输、多主机仲裁 | 中/中 |
| AXI | 高性能总线 | DDR、NPU/GPU、视频编解码 | 5 通道全双工、突发、乱序、outstanding、寄存器切片 | 高/高 |
是什么:APB2/APB3 的接口总共只有 10 根核心信号(外加 APB4 可选的 PPROT/PSTRB)。分三组:时钟复位组(PCLK/PRESETn)、主机发出组(PADDR/PSELx/PENABLE/PWRITE/PWDATA)、从机返回组(PRDATA/PREADY/PSLVERR)。记住"主机给地址选通和写数据,从机给读数据和两个应答"就不会乱。
为什么(时序注意的由来):APB 是同步总线,所有信号都在 PCLK 上升沿被采样;除 PRDATA/PREADY/PSLVERR 由从机在 ACCESS 拍给出外,其余都要求在采样沿前稳定(setup time)。"除 PENABLE 外,地址/写数据等控制信号必须从 SETUP 拍保持到传输结束"是规范里最容易考的约束。
| 信号 | 方向 | 作用 | 时序注意 |
|---|---|---|---|
| PCLK | 时钟 | APB 域时钟,所有信号以其上升沿为节拍 | 常为 AHB 时钟的整数分频(如 1/2、1/4),桥内做跨域同步 |
| PRESETn | 输入(低有效) | APB 域复位,复位后主机应驱动 PSEL=0/PENABLE=0 | 与 PCLK 异步进入、同步释放(参见 08-04 页 CDC 去复位) |
| PADDR | 主机→从机 | 字节地址,最大 32 位;从机内部用它做寄存器偏移译码 | SETUP 拍起有效,且必须保持稳定直到传输完成(PREADY=1) |
| PSELx | 主机→从机(每从机一根) | 片选:译码器根据 PADDR 高位生成 one-hot,选中唯一从机 | SETUP 拍拉高、整个传输保持;完成后随 PENABLE 一起撤销 |
| PENABLE | 主机→从机 | 指示第二拍(ACCESS):为 1 时所有控制信号有效、等待应答 | SETUP 拍必须为 0,下一拍才拉高;每次传输只允许一个 ACCESS 拍前导 |
| PWRITE | 主机→从机 | 方向:1=写,0=读 | 与 PADDR 同约束:SETUP 起保持稳定至传输完成 |
| PWDATA | 主机→从机 | 写数据总线(8/16/32 位),写传输时有效 | SETUP 拍起有效并保持;读传输时主机可置 0 |
| PRDATA | 从机→主机 | 读数据总线,读传输时由被选中从机驱动 | 必须在 PREADY=1 的那个(最后一个)ACCESS 拍给出有效数据 |
| PREADY | 从机→主机 | 从机应答:1=本拍完成传输;0=插入等待状态(wait state) | 主机在 ACCESS 拍每个时钟沿采样;为 0 时必须保持全部控制信号不变 |
| PSLVERR | 从机→主机 | 错误响应(APB3 引入):访问非法地址/只读寄存器写等 | 仅在 PSEL&PENABLE&PREADY 同时为 1 的拍才有意义,其余拍忽略 |
是什么:APB 主机(或桥)的行为就是一个三状态(工程上常扩展成四状态)有限状态机:IDLE(默认态,PSEL=0) → SETUP(1 拍:PSEL=1,PENABLE=0,给出地址/方向/写数据) → ACCESS(PENABLE=1,保持所有控制信号,采样 PREADY 等待握手)。PREADY=1 的那拍传输完成:若无后续交易回 IDLE;若"背靠背"有下一笔,可从 ACCESS 直接进新一笔的 SETUP。
为什么必须有 SETUP:SETUP 拍的作用是给"片选+地址"一个先于 PENABLE 一拍的稳定窗口:①让译码器/从机在 PENABLE 拉高前完成 PSEL 选择和地址锁存准备;②保证 PENABLE 相对 PSEL 至少晚一个时钟沿,从机用它做"新传输开始"的边沿检测;③若没有这拍,从机无法区分"新的一笔"和"上一笔的延续",背靠背传输将无从谈起。这是 APB 面试出镜率最高的一问。
怎么做:从机侧判断"有效传输"用组合条件 PSEL && PENABLE && PREADY(写再加 PWRITE);判断"新传输的第一拍"用 PSEL && !PENABLE(即 SETUP 拍,可在此锁存地址)。等待状态则是在 ACCESS 态里 PREADY=0 时原地踏步:控制信号全部保持,只等从机应答,因此工程上常把它画成第四个状态 WAIT(实际是 ACCESS 的自循环)。
PSEL&&PENABLE&&PREADY 三者同时为 1,漏掉 PSEL 判断会把别的从机的传输"偷"过来;③IDLE 态必须驱动 PSEL=0、PENABLE=0,PADDR 可以为任意值但很多检查器会要求无 X 态。是什么:一次零等待的 APB 写传输占用 3 个时钟沿、2 个完整拍:T1 拍是 SETUP(PSEL 拉高、PENABLE 保持 0,地址/方向/写数据上线),T2 拍是 ACCESS(PENABLE 拉高,从机在 T3 上升沿前把 PREADY 置 1),T3 上升沿主机采样 PREADY=1,传输完成;T4 拍起 PSEL/PENABLE 撤销回 IDLE(或背靠背进下一笔 SETUP)。全程 PWRITE=1,PWDATA 携带要写的数据。
为什么这么设计:两拍结构让"选通"与"应答"解耦:第一拍只解决"选谁、给什么地址",第二拍才要求从机响应数据 —— 从机逻辑简单到可以纯组合译码+寄存器写入;对主机而言,写数据在 SETUP 拍就上线,与地址一起保持,从机在完成沿一次性采样,不需要额外的数据有效信号。
怎么做(逐拍读图):①T1 沿后:PSELx↑、PWRITE=1、PADDR=0x10、PWDATA=0xAB,PENABLE 仍为 0(SETUP);②T2 沿后:PENABLE↑ 进入 ACCESS,所有控制信号必须保持不变;③T3 沿:从机已把 PREADY 置 1,主机采样到握手成功 —— 这一沿从机同时把 PWDATA 写入寄存器;④T4 沿:PSELx/PENABLE 已在 T3 后撤销,总线回 IDLE,从机寄存器里已经是 0xAB。下图中橙色竖带就是 ACCESS 窗口,高亮的 PENABLE 行是协议的"节拍器"。
是什么:读传输与写传输共用同一套三状态流程,差别只有两点:PWRITE=0;数据方向反转,由从机在最后一个 ACCESS 拍把 PRDATA 驱动有效,主机在 PREADY=1 的时钟沿采样。为了把"慢速外设"也讲透,下图故意画了一个等待状态:T2 拍进入 ACCESS 后从机先把 PREADY 拉低 1 拍(表示"我还没准备好"),T3 拍起 PREADY=1 且 PRDATA=0x5A 有效,T4 沿完成采样 —— 这笔读传输总共 3 拍(SETUP + 2 个 ACCESS)。
为什么用读传输讲等待:真实外设里"慢"几乎都发生在读方向 —— 例如 NPU 状态寄存器后面挂着需要若干周期汇聚的状态总线、UART 读接收缓冲要同步串行移位结果;写方向从机几乎总能"当场收下"。把等待状态放在读例子里,正好覆盖 90% 的真实场景。
怎么做(逐拍读图):①T1 拍(SETUP):PSEL↑、PWRITE=0、PADDR=0x14;②T2 拍(ACCESS 第 1 拍):PENABLE↑,从机还没就绪 → PREADY=0,主机被迫保持全部控制信号(这就是等待状态);③T3 拍(ACCESS 第 2 拍):从机把 PREADY 置 1,同时驱动 PRDATA=0x5A;④T4 沿:主机采样 PREADY=1 与 PRDATA,完成传输,PSEL/PENABLE 撤销。注意 PRDATA 只需在"最后一个 ACCESS 拍"有效,等待期间从机可以输出任何值。
是什么:等待状态(wait state)是从机用 PREADY=0 在 ACCESS 阶段"原地踏步"的机制:主机看到 PREADY=0 就必须保持 PSEL/PENABLE/PADDR/PWRITE/PWDATA 不动,直到某拍 PREADY=1 完成传输。等待拍数没有上限,完全由从机决定。PSLVERR(APB3 引入)是从机对"这笔传输失败了"的汇报:访问了不存在的地址、写只读寄存器、外设内部出错等,主机在完成拍(PSEL&PENABLE&PREADY)采样它,再决定上报总线错误、触发中断或忽略重试。
为什么需要这两个信号:没有 PREADY,APB 只能容忍"2 拍完成"的外设,慢速 IP(UART、I2C、NPU 状态机)就没法挂在同一根总线上;没有 PSLVERR,CPU 访问空地址会"假装成功读回 0",固件永远发现不了硬件接线/地址映射错误 —— 调试时这就是灾难。两者共同构成 APB 的完整握手语义:"没好(PREADY=0)/好了(PREADY=1)/好了但是错的(PREADY=1&PSLVERR=1)"。
怎么做(合法时序规则):①PREADY=0 的拍数任意,但 PSEL 必须保持;②PSLVERR 在等待期间允许变化,只在 PREADY=1 的完成拍被采样;③写传输错误时从机不执行写入,读传输错误时 PRDATA 数据无意义;④错误响应也要走完整握手(不缩短拍数),主机不能因为 PSLVERR=1 就提前撤 PSEL。下面用一张逐拍表把"等 1 拍 + 出错"的极端情况钉死:
| 拍 | PSEL | PENABLE | PREADY | PSLVERR | 主机动作 / 从机动作 |
|---|---|---|---|---|---|
| T1(SETUP) | 1 | 0 | 任意 | 任意 | 主机给出 PADDR/PWRITE/PWDATA;从机可预译码 |
| T2(ACCESS#1) | 1 | 1 | 0 | 可变(不采样) | 从机未就绪;主机保持全部控制信号,原地等待 |
| T3(ACCESS#2) | 1 | 1 | 1 | 1(有效) | 完成沿:主机采样到 PREADY=1&PSLVERR=1 → 判定失败;从机不写入/不给出有效读数 |
| T4 | 0 | 0 | — | — | 主机撤销 PSEL 回 IDLE(或软件记录错误后重试) |
是什么:APB 是共享总线,一组 PADDR/PWDATA/PRDATA 挂多个从机,靠"每个从机一根片选"区分谁参与传输。译码器(通常在 AHB→APB 桥内部)把地址高位与各从机的基址比较,生成 one-hot 的 PSEL0/PSEL1/…;每个从机只在自己 PSELx=1 时把接口接进总线(未选中时输出高阻/忽略)。例如 4KB 一个槽位:0x1000_0000 起 4KB 给 UART、0x1001_0000 起 4KB 给 GPIO、0x1300_0000 起 4KB 给 NPU 寄存器。
为什么用 one-hot 而不是编码:one-hot 片选让每个从机的使能条件就是一根线,无需再解一遍编码,时序好、功耗与地址布局无关;更重要的是它天然避免了"两个从机同时驱动 PRDATA"的冲突 —— 任何时刻最多一个 PSELx 为 1。基址比较用高位等值比较(而非范围大于小于),综合出来就是一级比较器,是面积最小的译码实现。
怎么做(桥侧译码代码骨架):
// ---- AHB→APB 桥内部: PSELx 译码(组合逻辑) ----
// 约定: 每个 APB 从机占 4KB 地址槽位, 用 PADDR 高位比较
wire sel_uart = (PADDR[31:12] == 20'h10010); // UART 基址 0x1001_0000 起 4KB
wire sel_gpio = (PADDR[31:12] == 20'h10011); // GPIO 基址 0x1001_1000 起 4KB
wire sel_npu = (PADDR[31:12] == 20'h13000); // NPU 寄存器基址 0x1300_0000
// only-one-hot: 任意时刻至多一个为 1(前提: 地址映射不重叠)
assign PSEL_uart = psel_phase & sel_uart; // psel_phase = 状态机处于 SETUP/ACCESS
assign PSEL_gpio = psel_phase & sel_gpio;
assign PSEL_npu = psel_phase & sel_npu;
// 读数据在桥侧汇流: 被选中的从机贡献 PRDATA, 其余置 0
assign PRDATA = ({32{PSEL_uart}} & prdata_uart)
| ({32{PSEL_gpio}} & prdata_gpio)
| ({32{PSEL_npu}} & prdata_npu);
是什么:下面是一个完整可综合的 APB 从机 apb_slave_regs:内部 4 个 32 位寄存器(CTRL/STATUS/DATA/VERSION),对慢速寄存器(CTRL/DATA)自动插入 1 拍 PREADY 等待模拟慢速外设,对越界地址(0x10 以上)在完成拍返回 PSLVERR。它就是将来挂到 NPU 芯片上、给 08-10 页「NPU 控制模块」当配置/状态接口的那块 IP 的最小原型。
寄存器映射(图⑤):
| 偏移 | 名称 | 属性 | 复位值 | 字段说明 | 等待 |
|---|---|---|---|---|---|
| 0x00 | CTRL | RW | 0x0000_0000 | [0] enable 启动 NPU · [1] mode 工作模式 · [2] irq_en 中断使能 · [31:3] 保留 | 1 拍(慢速) |
| 0x04 | STATUS | RO | 0x0000_0000 | [0] busy 运算中 · [1] done 完成 · [2] err 错误 · [31:3] 保留 | 0 拍 |
| 0x08 | DATA | RW | 0x0000_0000 | 数据端口:写=命令/参数,读=最近结果(示例中简化为镜像寄存器) | 1 拍(慢速) |
| 0x0C | VERSION | RO | 0x2026_0905 | IP 版本号(固件启动时读它做兼容性检查) | 0 拍 |
| ≥0x10 | — | — | — | 未映射:完成拍 PSLVERR=1 | — |
完整 Verilog(逐段编号,与下方讲解对应):
//============================================================
// apb_slave_regs.v —— APB 从机寄存器模块(可综合)
// 0x00 CTRL(RW) 0x04 STATUS(RO) 0x08 DATA(RW) 0x0C VERSION(RO)
// 行为: 慢速寄存器插入 1 拍等待; 越界访问 PSLVERR 报错
//============================================================
module apb_slave_regs (
input wire PCLK,
input wire PRESETn,
// ---- APB 从机接口 ----
input wire PSEL,
input wire PENABLE,
input wire [11:0] PADDR, // 桥已掩掉基址, 这里只剩片内偏移
input wire PWRITE,
input wire [31:0] PWDATA,
output wire [31:0] PRDATA,
output wire PREADY,
output wire PSLVERR,
// ---- 应用侧端口(模拟真实外设的功能逻辑) ----
output wire [31:0] ctrl_o, // CTRL 当前值 → 驱动外设行为
input wire [31:0] status_i, // 外设状态 → STATUS 寄存器
output wire [31:0] data_o // DATA 当前值
);
// ---- (1) 偏移译码: PADDR[7:2] 选 32 位寄存器(4 字节对齐) ----
localparam R_CTRL = 6'h00; // 0x00
localparam R_STATUS = 6'h01; // 0x04
localparam R_DATA = 6'h02; // 0x08
localparam R_VERSION = 6'h03; // 0x0C
wire [5:0] reg_idx = PADDR[7:2];
wire hit_slow = (reg_idx == R_CTRL) || (reg_idx == R_DATA); // 慢速区
wire hit_legal= (PADDR[11:4] == 8'h00); // 仅 0x00~0x0F 已映射
// ---- (2) SETUP 拍锁存"本笔是否慢速", 完成拍清除 ----
wire xfer_done = PSEL & PENABLE & PREADY; // 协议完成条件
reg slow_flag;
always @(posedge PCLK or negedge PRESETn) begin
if (!PRESETn) slow_flag <= 1'b0;
else if (PSEL & !PENABLE) slow_flag <= hit_slow; // SETUP 锁存
else if (xfer_done) slow_flag <= 1'b0;
end
// ---- (3) 等待标志: ACCESS 第 1 拍置位 → 第 2 拍放行 PREADY ----
reg waited;
always @(posedge PCLK or negedge PRESETn) begin
if (!PRESETn) waited <= 1'b0;
else if (!PSEL || (PSEL & !PENABLE)) waited <= 1'b0; // 新传输前复位
else if (xfer_done) waited <= 1'b0;
else if (PSEL & PENABLE & slow_flag) waited <= 1'b1; // 第 1 个 ACCESS 拍
end
// PREADY: 仅慢速访问的第 1 个 ACCESS 拍为 0(即插入 1 拍等待)
assign PREADY = ~(PSEL & PENABLE & slow_flag & ~waited);
// ---- (4) 读多路器: 组合输出, 完成拍被主机采样 ----
reg [31:0] prdata_r;
always @(*) begin
case (reg_idx)
R_CTRL: prdata_r = ctrl_q;
R_STATUS: prdata_r = status_i; // 直通外设实时状态
R_DATA: prdata_r = data_q;
R_VERSION: prdata_r = 32'h2026_0905;
default: prdata_r = 32'h0; // 越界读返回 0(配合 PSLVERR)
endcase
end
assign PRDATA = prdata_r;
// ---- (5) 写寄存器: 只在完成拍写入(此时 PSLVERR 已定) ----
reg [31:0] ctrl_q, data_q;
wire wr_en = xfer_done & PWRITE & ~PSLVERR; // 出错的写不生效
always @(posedge PCLK or negedge PRESETn) begin
if (!PRESETn) begin
ctrl_q <= 32'h0;
data_q <= 32'h0;
end else if (wr_en) begin
case (reg_idx)
R_CTRL: ctrl_q <= PWDATA;
R_DATA: data_q <= PWDATA;
default: ; // STATUS/VERSION 只读, 忽略写
endcase
end
end
assign ctrl_o = ctrl_q;
assign data_o = data_q;
// ---- (6) 错误响应: 越界地址在完成拍报 PSLVERR ----
assign PSLVERR = xfer_done & ~hit_legal;
endmodule
逐段讲解:①偏译码用 PADDR[7:2]:32 位寄存器按 4 字节对齐,最低 2 位恒 0,所以寄存器号从第 2 位开始取 —— 这是所有寄存器 IP 的通用套路;②slow_flag 在 SETUP 拍锁存,而不是 ACCESS 拍现算,保证等待行为整笔传输一致(等待期间 PADDR 不变,其实现算也行,但锁存写法对后面扩展"等待拍数可配置"更稳);③waited 标志让 PREADY 形状是"第 1 个 ACCESS 拍 0、第 2 拍 1",且在下一笔 SETUP 拍自动复位,背靠背传输也正确;④读多路器是纯组合,APB 从机读路径不许加一拍寄存器(否则最后一个 ACCESS 拍数据还没出来,除非配套把 PREADY 拉低一拍补偿);⑤写在完成拍才生效,并且用 ~PSLVERR 把错误写挡掉;⑥PSLVERR 定义为"完成拍 + 非法地址",与 2.3 节的合法时序表完全一致。
wire xfer_done = PSEL & PENABLE & PREADY; 这根线,面试官就知道你真懂协议。PSEL & PENABLE & PWRITE 而漏了 PREADY → 等待第 1 拍就把数据写进去(重复写/早写);③PSLVERR 写成时序输出晚一拍 → 主机采样不到;④复位只复位了寄存器组、忘了 slow_flag/waited → 复位释放后第一笔传输可能异常等待。是什么:从机有了,还得有人"点菜"。下面的 apb_master_fsm 是一个极简 APB 主机:上层(NPU 控制模块/固件代理)通过 req_valid* 通道发起一笔读或写,FSM 自动走 IDLE→SETUP→ACCESS 三状态,等 PREADY 握手完成后置起 req_ready 并带回读数据;若完成时又来了新请求,直接背靠背进下一笔 SETUP,不浪费回 IDLE 的一拍。在 08-10 页里,它将被 NPU 控制模块用来把权重缩放系数、层数配置等逐条写入上述从机寄存器。
为什么自己写主机而不是直接接桥:真实 SoC 里 APB 域主机一般就是桥(翻译 AHB/AXI 事务)。但在 IP 原型验证、FPGA 子系统、片内"小配置总线"场景,经常需要一个本地 APB 主机去初始化另一颗芯片或外设 —— 比如用 FPGA 逻辑自动配置 NPU 的寄存器序列。这个 80 行的 FSM 就是这类"配置序列发生器"的引擎。
//============================================================
// apb_master_fsm.v —— APB 主控三状态 FSM(可综合)
// 上层握手: req_valid/req_ready 一次握手 = 一笔完整 APB 读写
// 为 08-10 页「NPU 控制模块配置寄存器」预留的主机引擎
//============================================================
module apb_master_fsm #(
parameter ADDR_W = 12
)(
input wire PCLK,
input wire PRESETn,
// ---- 上层请求通道(如 NPU 控制模块) ----
input wire req_valid, // 请求一笔传输
input wire req_write, // 1=写 0=读
input wire [ADDR_W-1:0] req_addr, // 目标寄存器地址
input wire [31:0] req_wdata, // 写数据
output reg req_ready, // 本笔完成脉冲(1 拍)
output reg [31:0] req_rdata, // 读返回数据
output wire req_err, // 从机报错透传
// ---- APB 主机接口 ----
output reg PSEL,
output reg PENABLE,
output reg [ADDR_W-1:0] PADDR,
output reg PWRITE,
output reg [31:0] PWDATA,
input wire [31:0] PRDATA,
input wire PREADY,
input wire PSLVERR
);
localparam S_IDLE = 2'd0;
localparam S_SETUP = 2'd1; // 固定 1 拍
localparam S_ACCESS = 2'd2; // 停留到 PREADY=1
reg [1:0] state, nxt;
// ---- (1) 状态转移 ----
always @(posedge PCLK or negedge PRESETn) begin
if (!PRESETn) state <= S_IDLE;
else state <= nxt;
end
always @(*) begin
nxt = state;
case (state)
S_IDLE: if (req_valid) nxt = S_SETUP;
S_SETUP: nxt = S_ACCESS; // 只停 1 拍
S_ACCESS: if (PREADY) nxt = req_valid ? S_SETUP : S_IDLE; // 背靠背
default: nxt = S_IDLE;
endcase
end
// ---- (2) 输出与数据动作 ----
always @(posedge PCLK or negedge PRESETn) begin
if (!PRESETn) begin
{PSEL, PENABLE, PWRITE} <= 3'b000;
{PADDR, PWDATA} <= 0;
{req_ready, req_rdata} <= 0;
end else begin
req_ready <= 1'b0; // 默认单拍脉冲
case (state)
S_IDLE: begin
PSEL <= 1'b0;
PENABLE <= 1'b0;
if (req_valid) begin // 锁存请求 → 下一拍进 SETUP
PADDR <= req_addr;
PWRITE <= req_write;
PWDATA <= req_wdata;
end
end
S_SETUP: begin
PSEL <= 1'b1; // SETUP 拍: 仅片选, 使能保持 0
PENABLE <= 1'b0;
end
S_ACCESS: begin
PSEL <= 1'b1;
PENABLE <= 1'b1; // ACCESS 拍: 使能拉高
if (PREADY) begin // 从机应答 → 本笔完成
req_ready <= 1'b1;
if (!PWRITE) req_rdata <= PRDATA; // 读: 捕获数据
if (req_valid) begin // 背靠背: 直接进下一笔 SETUP
PSEL <= 1'b1;
PENABLE <= 1'b0;
PADDR <= req_addr;
PWRITE <= req_write;
PWDATA <= req_wdata;
end else begin // 无后续: 回 IDLE
PSEL <= 1'b0;
PENABLE <= 1'b0;
end
end
end
default: begin
PSEL <= 1'b0;
PENABLE <= 1'b0;
end
endcase
end
end
assign req_err = PSLVERR; // 错误透传给上层决策
endmodule
逐段讲解:①状态转移用组合 nxt + 时序 state 的两段式写法,三状态一眼可查;②S_SETUP 分支只做一件事:PSEL=1、PENABLE=0 —— 严格复刻"SETUP 固定 1 拍";③S_ACCESS 里 PENABLE 保持 1,直到 if (PREADY) 成立才动手:读则锁 PRDATA、置 req_ready 单拍脉冲;④背靠背优化在完成拍"顺路"锁存新请求并直接置 PSEL=1/PENABLE=0,下一拍就是新一笔的 SETUP,省去一次 IDLE 往返(零等待下一笔仍按协议最少 2 拍,但少 1 拍空闲);⑤req_err 把 PSLVERR 透传,上层(NPU 控制模块)可以决定重发或报警 —— 这正好演示了"主机如何消费错误响应"。
PREADY ? (req_valid ? SETUP : IDLE) 的转移条件 + 完成拍顺路锁存。加分点:①指出 SETUP 拍 PENABLE 必须为 0;②说明读数据在 PREADY=1 沿捕获;③提到 PSLVERR 上浮给软件。PENABLE<=0,导致"新 SETUP 拍 PENABLE 还是 1" —— 直接协议违例;③req_ready 做成电平而非单拍脉冲,上层一次请求被当成两笔;④FSM 在 ACCESS 中响应 req_valid 改地址 —— 违反"传输中控制信号保持"规则,必须等完成拍才能换。是什么:APB 验证的目标是证明"从机在任何合法主机激励下协议行为正确、功能结果正确"。最小 testbench = 时钟复位 + 两个任务(apb_write/apb_read)+ 一个自动比较任务 chk,再加五类定向用例:①写读回读(写 CTRL=0xFF 再读回,验证读改写链路与慢速等待路径);②边界地址(最后合法寄存器 0x0C、第一个未映射地址 0x10,验证译码边界与 PSLVERR);③PREADY 等待(慢速寄存器访问时用波形/计数确认恰好插 1 拍、控制信号保持);④错误响应(越界读/只读寄存器写,确认 PSLVERR=1 且写入不生效);⑤复位与背靠背(复位中途发传输、完成后立刻发下一笔,验证 FSM 不卡死、PSEL 时序合规)。
为什么这么排:①是基本功能面,②④是译码/异常面,③⑤是时序面 —— 三面合起来正好覆盖 APB 的协议状态空间;更进阶的做法是加协议断言(如"PSEL=0 时 PENABLE 必须为 0""PENABLE=1 后 PADDR/PWDATA 不得变化直到 PREADY=1")让仿真器自动抓违例,而不是靠肉眼盯波形。
`timescale 1ns/1ps
//============================================================
// tb_apb_slave.v —— APB 从机 testbench 骨架(本文 3.2 从机配套)
// 用例: 写读回读 / 边界地址 / PREADY 等待 / PSLVERR 错误
//============================================================
module tb_apb_slave;
reg PCLK = 1'b0;
reg PRESETn;
reg PSEL, PENABLE, PWRITE;
reg [11:0] PADDR;
reg [31:0] PWDATA;
wire [31:0] PRDATA;
wire PREADY, PSLVERR;
wire [31:0] ctrl_o, data_o;
reg [31:0] rdata;
// 例化被测从机(此处用 SystemVerilog .* 简写, Verilog-2001 请逐端口连接)
apb_slave_regs dut (
.PCLK(PCLK), .PRESETn(PRESETn),
.PSEL(PSEL), .PENABLE(PENABLE), .PADDR(PADDR),
.PWRITE(PWRITE), .PWDATA(PWDATA),
.PRDATA(PRDATA), .PREADY(PREADY), .PSLVERR(PSLVERR),
.ctrl_o(ctrl_o), .status_i(32'h5A5A_0002), .data_o(data_o)
);
always #5 PCLK = ~PCLK; // 100MHz
// ---- 任务1: APB 写(SETUP→ACCESS→自动等 PREADY) ----
task apb_write(input [11:0] addr, input [31:0] data);
begin
@(posedge PCLK);
PSEL = 1; PENABLE = 0; // SETUP 拍
PWRITE = 1; PADDR = addr; PWDATA = data;
@(posedge PCLK);
PENABLE = 1; // ACCESS 拍
@(posedge PCLK);
while (!PREADY) @(posedge PCLK); // 等待状态自动适应
PSEL = 0; PENABLE = 0;
end
endtask
// ---- 任务2: APB 读, 返回完成拍采样到的 PRDATA ----
task apb_read(input [11:0] addr, output [31:0] rdata);
begin
@(posedge PCLK);
PSEL = 1; PENABLE = 0; // SETUP 拍
PWRITE = 0; PADDR = addr;
@(posedge PCLK);
PENABLE = 1; // ACCESS 拍
@(posedge PCLK);
while (!PREADY) @(posedge PCLK);
rdata = PRDATA; // 完成沿采样读数据
PSEL = 0; PENABLE = 0;
end
endtask
// ---- 任务3: 自动比较 ----
task chk(input [31:0] got, input [31:0] exp, input [8*16-1:0] name);
begin
if (got !== exp) $display("[FAIL] %0s: got=%h exp=%h", name, got, exp);
else $display("[PASS] %0s = %h", name, got);
end
endtask
initial begin
// 用例0: 复位
PRESETn = 0; {PSEL, PENABLE, PWRITE} = 3'b0; PADDR = 0; PWDATA = 0;
repeat (4) @(posedge PCLK);
PRESETn = 1;
// 用例1: 写读回读(CTRL 是慢速寄存器, 写/读各应插入 1 拍等待)
apb_write(12'h000, 32'h0000_00FF);
apb_read (12'h000, rdata);
chk(rdata, 32'h0000_00FF, "CTRL 回读");
// 用例2: 只读寄存器写忽略 + 边界合法地址
apb_write(12'h00C, 32'hDEAD_BEEF); // 写 VERSION
apb_read (12'h00C, rdata);
chk(rdata, 32'h2026_0905, "VERSION 不被写坏");
// 用例3: 第一个未映射地址 → PSLVERR
apb_read (12'h010, rdata);
if (PSLVERR !== 1'b1) $display("[FAIL] 0x10 越界读未报 PSLVERR");
else $display("[PASS] 越界读 PSLVERR=1");
$display("=== APB 从机回归结束 ===");
$finish;
end
endmodule
关键检查点(对波形/日志):①CTRL 写传输的 PENABLE 高电平应持续 2 拍(1 等待 + 1 完成),VERSION 访问只 1 拍;②等待期间 PADDR/PWDATA/PWRITE 必须纹丝不动;③越界写(可自行加用例 12'h010)后 ctrl_o/data_o 不得变化;④背靠背两笔写之间 PSEL 不得提前撤销或多拍悬空。
while(!PREADY) 等待却忘了先过一拍才采样 —— 任务里"@posedge 后先给 SETUP 再谈等待"的拍序要和波形图对齐;②只测零等待路径 —— 慢速路径(本文 CTRL/DATA)才是 bug 高发区;③忘记比较 !==(四态)而用 != —— X/Z 态会漏检;④回归只有" happy path",没有复位中途激励、背靠背激励,FSM 死锁上线才暴露。本页协议时序与代码均以下列一手资料为准 —— 优先读 ARM 官方规范 IHI0024,中文文章用于快速建立直觉,开源仓库用于对照自己的 RTL。