摘要
在 FPGA 验证中,总线接口(如 Wishbone)的握手时序最容易被忽视,也最容易导致"波形对但逻辑错"的隐性问题。本文将拆解一个我在实际项目中使用的 Wishbone Master BFM(总线功能模型),涵盖接口定义、任务封装、字节使能控制与自动化比对。核心目标:把繁琐的总线握手封装成"调用即走"的任务,让测试用例聚焦业务逻辑,让自检融入每一次读写。
一、Wishbone 总线协议"温故"------只讲必须用的那点事
Wishbone 标准(B4/B8)最核心的握手信号就这几根:
-
cyc:总线周期有效,Master 发起。 -
stb:选通信号,表示当前周期有数据传输。 -
we:写使能(1 为写,0 为读)。 -
adr、dat、sel(be):地址、数据、字节使能。 -
ack:Slave 回传,表示读写完成。
标准单次读写的时序逻辑:
-
Master 将
cyc和stb拉高,同时给出adr、we、sel和dat(写时)。 -
Slave 收到后处理,完成后将
ack拉高至少一个周期。 -
Master 检测到
ack后,在下一个时钟沿拉低cyc和stb,结束传输。
经验坑 :
ack可能不连续(多个周期后才有),也可能连续拉高(流水线)。BFM 必须用wait(wb_ack == 1'b1)阻塞等待,而不是简单地在posedge后直接采样。
二、BFM 在验证环境中的"身位"------为什么要自己写?
我见过不少验证工程师在 testbench 里直接用 force 或 assign 强行驱动总线,结果导致:
-
每次修改总线时序都要重新写一遍信号赋值;
-
多字节访问(8/16/32 位)的
be计算分散在多个地方; -
断言和比对混在驱动代码里,极难维护。
BFM 的核心价值在于封装 。它把总线时序"锁"在任务内部,对外暴露的只有 wb_wr(addr, data) 或 wb_rd(addr) 这种语义级接口。这样,测试用例的编写者完全不需要关心 cyc 何时拉高、ack 何时回来,只需要关注"我要写什么、我要读什么、结果对不对"。
同时,BFM 是自动化自检的第一道防线------因为所有的总线操作都必须经过同一套驱动代码,只要把这套代码做稳,后续的比对和断言就有了可信的基础。
三、Wishbone Master BFM 详细设计思路
3.1 接口定义:显式声明,不依赖外部信号
我的 BFM 使用 wire 和 reg 显式声明所有总线信号,并把时钟设为 wire 输入,从外部驱动。这样 BFM 可以独立编译,也方便在多个 testbench 中复用。
-
wb_clk:由上层 testbench 产生,BFM 内部只是采样和驱动。 -
wb_cyc/wb_stb/wb_we:Master 输出,均为reg,初始为 0。 -
wb_adr/wb_wdat/wb_be:Master 输出,位宽固定。 -
wb_rdat/wb_ack:Slave 输出,Master 输入,为wire。
这种"输入输出分离"的设计,让 BFM 天生支持与 DUT 直接连接,也支持在虚拟环境(如 UVM 的 driver )中通过 config_db 传递。
3.2 任务封装:单次写、单次读、读比较
我设计了三个核心任务:
-
wb_wr:完成一次写操作,支持 8/16/32 位写入(通过wr_bat控制)。 -
wb_rd:完成一次读操作,返回读到的 32 位数据。 -
wb_rd_cmp:读回数据并与期望值比对,自动打印 Pass/Fail。
每个任务都遵循同样的时序结构:
-
等待时钟上升沿(
@ (posedge wb_clk)); -
驱动信号并加入
#1延迟(避免零延时竞争); -
阻塞等待
wb_ack == 1'b1; -
采样数据后,在下一个时钟沿撤销所有控制信号。
3.3 字节使能(BE)的工程处理
Wishbone 的 sel 信号(我这里用 wb_be )决定了哪个字节有效。对于非对齐访问和 8/16 位操作,be 的计算必须根据 adr[1:0] 灵活生成。
我的实现逻辑是:
-
32 位:
wb_be = 4'b1111 -
16 位:根据
wb_adr[1]判断是低 16 位(0011)还是高 16 位(1100) -
8 位:根据
wb_adr[1:0]的 4 种取值分别给0001、0010、0100、1000
实践注意 :这里的地址 wb_adr 我统一按字节地址输入(rd_adr_byte ),这样在 8 位访问时,wb_adr[1:0] 天然对应字节偏移,不需额外换算。这是很多新手容易搞混的地方。
四、WB_BFM 代码
vbnet
// ==================================================
// 1. 声明 BFM 内部交互的全局线网(严格定义位宽)
// ==================================================
wire wb_clk;
reg wb_cyc;
reg wb_stb;
reg wb_we;
reg [2:0] wb_cti;
reg [31:0] wb_adr;
reg [3:0] wb_be;
reg [31:0] wb_wdat;
wire [31:0] wb_rdat;
wire wb_ack;
// ==================================================
// 2. 标准 Wishbone 总线任务(位宽全部显式声明)
// ==================================================
task wb_wr;
input [1:0] wr_bat; // 32/16/8 位写
input [31:0] wr_adr;
input [31:0] wr_dat;
begin
@ (posedge wb_clk);
#1;
wb_stb = 1'b1;
wb_cyc = 1'b1;
wb_we = 1'b1;
wb_cti = 3'b000;
wb_adr = wr_adr;
wb_wdat = wr_dat;
case(wr_bat)
2'b00: wb_be = 4'b1111; // 32位写
2'b01: wb_be = wb_adr[1] ? 4'b1100 : 4'b0011; // 16位写
2'b10: begin // 8位写
case(wb_adr[1:0])
2'd0: wb_be = 4'b0001;
2'd1: wb_be = 4'b0010;
2'd2: wb_be = 4'b0100;
2'd3: wb_be = 4'b1000;
endcase
end
endcase
wait(wb_ack == 1'b1);
@ (posedge wb_clk);
#1;
wb_stb = 1'b0;
wb_cyc = 1'b0;
wb_we = 1'b0;
wb_cti = 3'b000;
wb_adr = 32'b0;
wb_wdat = 32'b0;
wb_be = 4'b0;
end
endtask
task wb_rd;
input [1:0] rd_bat;
input [31:0] rd_adr_byte;
output [31:0] rdat_32;
// 局部变量显式声明
reg [31:0] rdat_16;
reg [31:0] rdat_8;
begin
@ (posedge wb_clk);
#1;
wb_stb = 1'b1;
wb_cyc = 1'b1;
wb_we = 1'b0;
wb_cti = 3'b000;
wb_adr = rd_adr_byte;
case(rd_bat)
2'b00: wb_be = 4'b1111; // 32位读
2'b01: wb_be = wb_adr[1] ? 4'b1100 : 4'b0011; // 16位读
2'b10: begin // 8位读
case(wb_adr[1:0])
2'd0: wb_be = 4'b0001;
2'd1: wb_be = 4'b0010;
2'd2: wb_be = 4'b0100;
2'd3: wb_be = 4'b1000;
endcase
end
endcase
wait(wb_ack == 1'b1);
@ (posedge wb_clk);
#1;
case(rd_bat)
2'b00: rdat_32 = wb_rdat;
2'b01: rdat_32 = wb_adr[1] ? wb_rdat[31:16] : wb_rdat[15:0];
2'b10: begin
case(wb_adr[1:0])
2'd0: rdat_8 = wb_rdat[7:0];
2'd1: rdat_8 = wb_rdat[15:8];
2'd2: rdat_8 = wb_rdat[23:16];
2'd3: rdat_8 = wb_rdat[31:24];
endcase
rdat_32 = rdat_8;
end
endcase
wb_stb = 1'b0;
wb_cyc = 1'b0;
wb_we = 1'b0;
wb_cti = 3'b000;
wb_adr = 32'b0;
wb_be = 4'b0;
end
endtask
task wb_rd_cmp;
input [31:0] rd_adr_byte;
input [31:0] rd_cmp_data;
reg [31:0] rdat_32;
begin
@ (posedge wb_clk);
#1;
wb_stb = 1'b1;
wb_cyc = 1'b1;
wb_we = 1'b0;
wb_cti = 3'b000;
wb_adr = rd_adr_byte;
wb_be = 4'b1111;
wait(wb_ack == 1'b1);
@ (posedge wb_clk);
#1;
rdat_32 = wb_rdat;
wb_stb = 1'b0;
wb_cyc = 1'b0;
wb_we = 1'b0;
wb_cti = 3'b000;
wb_adr = 32'b0;
wb_be = 4'b0;
if(rdat_32 != rd_cmp_data) begin
$display("[ERROR] 地址 %h 读回不一致!期望 %h,实际得到 %h", rd_adr_byte, rd_cmp_data, rdat_32);
end else begin
$display("[INFO ] 地址 %h 读回验证通过: %h", rd_adr_byte, rdat_32);
end
end
endtask
五、功能扩展与典型踩坑
5.1 突发传输的预留设计
上面代码中 wb_cti 固定为 3'b000(经典循环)。如果要做突发传输(如连续读多个 32 位数据),只需改为:
-
第一个周期
wb_cti = 3'b001(常量地址突发)或3'b010(增量突发); -
中间周期保持
wb_cti = 3'b001/010; -
最后一个周期
wb_cti = 3'b111(结束)。
我通常会在 wb_rd 中增加一个 burst_len 参数,循环执行内部逻辑,但要注意:突发传输下 ack 可能不是每个周期都拉高 ,而 wait(wb_ack == 1'b1) 只适用于单次传输。实战中我会单独封装一个 wb_burst_rd 任务,在每次 ack 后再驱动下一个地址。
5.2 等待周期的插入
有些总线测试需要验证 Slave 在 ack 之前插入多个等待周期(stall 状态)。我的 BFM 不需要额外修改,因为 wait(wb_ack == 1'b1) 会自然阻塞,无论等待几个周期。如果需要模拟 Master 主动插入等待(比如降低 stb ),则需在 @ (posedge wb_clk) 前加入 repeat(N) @ (posedge wb_clk) 来强制拉长控制信号。但实际工作中,我不建议在 Master BFM 里主动插等待 ,因为这是 Slave 的行为;Master 应尽量快速驱动,让 Slave 全权控制时序。
六、测试用例中的实际调用
下面是在一个具体 testbench 中的调用示例,展示了如何用 BFM 完成自动化写读比对:
vbnet
module tb_wishbone_example;
// 例化 DUT 和 BFM 接口信号
wire wb_clk;
// ... (省略时钟生成和 DUT 例化)
initial begin
// 初始化 BFM 控制信号
wb_cyc = 0; wb_stb = 0; wb_we = 0;
wb_cti = 0; wb_adr = 0; wb_wdat = 0; wb_be = 0;
// 等待复位释放
@ (posedge wb_clk);
#100;
// 1. 32 位写操作
wb_wr(2'b00, 32'h1000, 32'hA5A5A5A5);
// 2. 32 位读并自动比对
wb_rd_cmp(32'h1000, 32'hA5A5A5A5);
// 3. 16 位写(低 16 位)
wb_wr(2'b01, 32'h1004, 32'h00001234);
// 4. 读回低 16 位并比对
wb_rd_cmp(32'h1004, 32'h00001234);
// 5. 8 位写(字节偏移 2)
wb_wr(2'b10, 32'h1006, 32'h000000AA);
// 6. 读回字节偏移 2 并比对(实际读 32 位,但只关注低 8 位)
wb_rd_cmp(32'h1006, 32'h000000AA);
$display("[DONE] 所有测试验证通过!");
$finish;
end
endmodule
经验之谈 :
wb_rd_cmp任务里固定用了wb_be = 4'b1111来读全部 32 位。对于 8/16 位验证,我推荐读回完整 32 位后再在测试用例里做掩码比对,而不是在 BFM 里截断 ------ 这样可以同时检查其他字节是否意外被修改,属于"过度验证"中的有效手段。
总结
Wishbone BFM 的设计价值不在于"写得多复杂",而在于"封装得有多干净"。这套 BFM 我用了近三年,从最初的单次读写扩展到现在的多粒度访问和自动化比对。它让我再也不用在每次仿真结束后手扒波形去核对寄存器值,而是直接在仿真日志里看到明确的 [INFO] 或 [ERROR]。
我的核心原则 :BFM 是一次性投入,但换来的是所有测试用例的持续复用和自动化自检。写好 BFM 的那天,就是验证效率质变的那天。