Wishbone BFM 设计与实现:从手写总线到自动化自检

摘要

在 FPGA 验证中,总线接口(如 Wishbone)的握手时序最容易被忽视,也最容易导致"波形对但逻辑错"的隐性问题。本文将拆解一个我在实际项目中使用的 Wishbone Master BFM(总线功能模型),涵盖接口定义、任务封装、字节使能控制与自动化比对。核心目标:把繁琐的总线握手封装成"调用即走"的任务,让测试用例聚焦业务逻辑,让自检融入每一次读写。

一、Wishbone 总线协议"温故"------只讲必须用的那点事

Wishbone 标准(B4/B8)最核心的握手信号就这几根:

  • cyc:总线周期有效,Master 发起。

  • stb:选通信号,表示当前周期有数据传输。

  • we:写使能(1 为写,0 为读)。

  • adrdatselbe:地址、数据、字节使能。

  • ack:Slave 回传,表示读写完成。

标准单次读写的时序逻辑

  1. Mastercycstb 拉高,同时给出 adrweseldat(写时)。

  2. Slave 收到后处理,完成后将 ack 拉高至少一个周期。

  3. Master 检测到 ack 后,在下一个时钟沿拉低 cycstb,结束传输。

经验坑ack 可能不连续(多个周期后才有),也可能连续拉高(流水线)。BFM 必须用 wait(wb_ack == 1'b1) 阻塞等待,而不是简单地在 posedge 后直接采样。

二、BFM 在验证环境中的"身位"------为什么要自己写?

我见过不少验证工程师在 testbench 里直接用 forceassign 强行驱动总线,结果导致:

  • 每次修改总线时序都要重新写一遍信号赋值;

  • 多字节访问(8/16/32 位)的 be 计算分散在多个地方;

  • 断言和比对混在驱动代码里,极难维护。

BFM 的核心价值在于封装 。它把总线时序"锁"在任务内部,对外暴露的只有 wb_wr(addr, data)wb_rd(addr) 这种语义级接口。这样,测试用例的编写者完全不需要关心 cyc 何时拉高、ack 何时回来,只需要关注"我要写什么、我要读什么、结果对不对"。

同时,BFM 是自动化自检的第一道防线------因为所有的总线操作都必须经过同一套驱动代码,只要把这套代码做稳,后续的比对和断言就有了可信的基础。

三、Wishbone Master BFM 详细设计思路

3.1 接口定义:显式声明,不依赖外部信号

我的 BFM 使用 wirereg 显式声明所有总线信号,并把时钟设为 wire 输入,从外部驱动。这样 BFM 可以独立编译,也方便在多个 testbench 中复用。

  • wb_clk :由上层 testbench 产生,BFM 内部只是采样和驱动。

  • wb_cyc / wb_stb / wb_weMaster 输出,均为 reg,初始为 0。

  • wb_adr / wb_wdat / wb_beMaster 输出,位宽固定。

  • wb_rdat / wb_ackSlave 输出,Master 输入,为 wire

这种"输入输出分离"的设计,让 BFM 天生支持与 DUT 直接连接,也支持在虚拟环境(如 UVMdriver )中通过 config_db 传递。

3.2 任务封装:单次写、单次读、读比较

我设计了三个核心任务:

  • wb_wr :完成一次写操作,支持 8/16/32 位写入(通过 wr_bat 控制)。

  • wb_rd:完成一次读操作,返回读到的 32 位数据。

  • wb_rd_cmp :读回数据并与期望值比对,自动打印 Pass/Fail

每个任务都遵循同样的时序结构:

  1. 等待时钟上升沿(@ (posedge wb_clk));

  2. 驱动信号并加入 #1 延迟(避免零延时竞争);

  3. 阻塞等待 wb_ack == 1'b1

  4. 采样数据后,在下一个时钟沿撤销所有控制信号。

3.3 字节使能(BE)的工程处理

Wishbonesel 信号(我这里用 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 种取值分别给 0001001001001000

实践注意 :这里的地址 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 等待周期的插入

有些总线测试需要验证 Slaveack 之前插入多个等待周期(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 的那天,就是验证效率质变的那天。

相关推荐
刘棕霆2 天前
30—AI Skill 怎么写才可测:Skill 编写规范与设计方法论
aigc·ai编程·测试
ClouGence3 天前
Selenium、Playwright、CueCast 深度对比:Web 自动化测试工具怎么选
selenium·测试
刘棕霆3 天前
29—AI Skill 测评集如何保持有效:从线上负反馈到 regression 用例
aigc·ai编程·测试
得物技术3 天前
AI UITester:AI Native 的 UI 自动化测试新范式|得物技术
llm·aigc·测试
析数塔3 天前
AI 时代测试开发新范式:从用例验证到 Agent 评测体系
agent·测试·aiops
ClouGence4 天前
Vibe Coding 之后,UI 测试如何跟上开发速度?
测试·vibecoding
刘棕霆4 天前
27—AI Skill 测评如何避免确认偏误:盲测对比与解盲分析
aigc·ai编程·测试
狂师4 天前
比 Playwright 更给力,推荐一个AI Agent的浏览器自动化开源项目!
前端·开源·测试
Apifox5 天前
Apifox 6 月更新|Apifox CLI 全面升级、导入导出优化、OAuth 2.0 支持自动刷新令牌
前端·后端·测试