4. BFM与Driver设计:总线功能模型概念、AXI4-Lite Driver实现、读写事务封装
好,咱们今天聊聊BFM和Driver。这两个概念,说白了就是验证环境里跟DUT打交道的“手”和“嘴”。
我记得刚入行那会儿,总觉得BFM和Driver差不多,都是发激励嘛。后来被一个老前辈点醒了——BFM是“怎么发”,Driver是“发什么”。这个区别,你品,你细品。
4.1 总线功能模型(BFM)的概念
BFM,Bus Functional Model,总线功能模型。名字挺唬人,其实没那么玄乎。
BFM的核心职责:把高层的transaction(事务)转换成具体的总线信号时序。说白了,就是翻译官。
举个例子,你要写一个AXI4-Lite的写操作。在高层,你只需要说“往地址0x1000写数据0xDEAD_BEEF”。但到了信号层面,你得拉高AWVALID、WVALID,等BVALID回来,再拉低...这一堆时序,BFM帮你搞定。
BFM的三个关键特征:
- 信号级操作:直接操作interface的各个信号
- 时序精确:严格按照协议时序驱动信号
- 无状态:BFM本身不保存数据,只负责传输
我在项目中遇到过一种情况:有人把BFM写得特别复杂,里面塞了各种数据检查、计分板逻辑。结果呢?换个项目复用的时候,改得那叫一个痛苦。BFM就该干BFM的活,别越界。
4.2 Driver的角色与定位
Driver,驱动。它跟BFM是什么关系?
我个人习惯这样理解:Driver是BFM的上层封装。Driver负责接收sequence发来的transaction,然后调用BFM去执行。
打个比方:
- BFM = 司机的“手脚”,负责踩油门、打方向盘
- Driver = 司机的“大脑”,负责决定“去哪”、“怎么走”
在UVM里,Driver通常继承自uvm_driver,它有一个核心方法——run_phase。在这个方法里,Driver不停地从sequencer拿transaction,然后驱动给BFM。
我的经验:Driver里不要写任何跟协议时序相关的代码。时序的事,全交给BFM。这样,如果你从AXI4-Lite换成AHB-Lite,只需要换BFM,Driver基本不用动。
4.3 AXI4-Lite Driver实现
好,咱们来点干货。手写一个AXI4-Lite的Driver。
先看接口定义:
// AXI4-Lite Interface
interface axi4_lite_if #(
parameter ADDR_WIDTH = 32,
parameter DATA_WIDTH = 32
) (
input logic aclk,
input logic aresetn
);
// 写地址通道
logic [ADDR_WIDTH-1:0] awaddr;
logic awvalid;
logic awready;
// 写数据通道
logic [DATA_WIDTH-1:0] wdata;
logic [DATA_WIDTH/8-1:0] wstrb;
logic wvalid;
logic wready;
// 写响应通道
logic [1:0] bresp;
logic bvalid;
logic bready;
// 读地址通道
logic [ADDR_WIDTH-1:0] araddr;
logic arvalid;
logic arready;
// 读数据通道
logic [DATA_WIDTH-1:0] rdata;
logic [1:0] rresp;
logic rvalid;
logic rready;
// modport for driver
modport driver (
output awaddr, awvalid,
input awready,
output wdata, wstrb, wvalid,
input wready,
input bresp, bvalid,
output bready,
output araddr, arvalid,
input arready,
input rdata, rresp, rvalid,
output rready
);
endinterface
接下来是BFM的核心——写事务:
class axi4_lite_bfm;
virtual axi4_lite_if vif;
function new(virtual axi4_lite_if vif);
this.vif = vif;
endfunction
// 写事务 - 核心时序
task write(input logic [31:0] addr,
input logic [31:0] data,
input logic [3:0] strb = 4'hF);
// 1. 发送地址
@(posedge vif.aclk);
vif.awaddr <= addr;
vif.awvalid <= 1'b1;
vif.wdata <= data;
vif.wstrb <= strb;
vif.wvalid <= 1'b1;
vif.bready <= 1'b1;
// 2. 等待握手
wait(vif.awready && vif.wready);
@(posedge vif.aclk);
vif.awvalid <= 1'b0;
vif.wvalid <= 1'b0;
// 3. 等待写响应
wait(vif.bvalid);
@(posedge vif.aclk);
vif.bready <= 1'b0;
// 4. 检查响应
if (vif.bresp != 2'b00)
$error("AXI4-Lite write error: addr=%h, resp=%b", addr, vif.bresp);
endtask
// 读事务 - 核心时序
task read(input logic [31:0] addr,
output logic [31:0] data);
// 1. 发送地址
@(posedge vif.aclk);
vif.araddr <= addr;
vif.arvalid <= 1'b1;
vif.rready <= 1'b1;
// 2. 等待握手
wait(vif.arready);
@(posedge vif.aclk);
vif.arvalid <= 1'b0;
// 3. 等待读数据
wait(vif.rvalid);
data = vif.rdata;
@(posedge vif.aclk);
vif.rready <= 1'b0;
// 4. 检查响应
if (vif.rresp != 2'b00)
$error("AXI4-Lite read error: addr=%h, resp=%b", addr, vif.rresp);
endtask
endclass
注意:上面的代码为了清晰,省略了超时处理。实际项目中,一定要加超时!我曾经在一个项目里,DUT的awready信号因为一个bug一直拉不高,仿真卡死了三天才发现。从那以后,我所有的BFM都加了超时断言。
好,BFM有了,Driver就简单了:
class axi4_lite_driver extends uvm_driver #(axi4_lite_transaction);
`uvm_component_utils(axi4_lite_driver)
axi4_lite_bfm bfm;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
axi4_lite_transaction tr;
forever begin
// 从sequencer拿transaction
seq_item_port.get_next_item(tr);
// 根据transaction类型驱动
if (tr.op == WRITE) begin
bfm.write(tr.addr, tr.data, tr.strb);
end else begin
bfm.read(tr.addr, tr.data);
end
// 告诉sequencer我干完了
seq_item_port.item_done();
end
endtask
endclass
4.4 读写事务封装
事务封装,说白了就是把读写操作包装成一个对象。这样sequence层就不用关心信号细节了。
一个典型的AXI4-Lite事务:
class axi4_lite_transaction extends uvm_sequence_item;
`uvm_object_utils(axi4_lite_transaction)
// 操作类型
typedef enum {READ, WRITE} op_t;
rand op_t op;
// 地址和数据
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic [3:0] strb;
// 约束
constraint addr_aligned {
addr[1:0] == 2'b00;
}
constraint strb_valid {
strb inside {4'hF, 4'h3, 4'h1};
}
function new(string name = "axi4_lite_transaction");
super.new(name);
endfunction
// 打印
function string convert2string();
return $sformatf("op=%s addr=%h data=%h strb=%b",
op.name(), addr, data, strb);
endfunction
endclass
我的习惯:事务类里一定要加convert2string方法。调试的时候,一句`uvm_info(get_type_name(), tr.convert2string(), UVM_MEDIUM)就能看到所有信息,比翻波形快多了。
有了事务封装,sequence写起来就清爽了:
class simple_write_seq extends uvm_sequence #(axi4_lite_transaction);
`uvm_object_utils(simple_write_seq)
function new(string name = "simple_write_seq");
super.new(name);
endfunction
virtual task body();
axi4_lite_transaction tr;
// 写操作
`uvm_do_with(tr, {op == WRITE; addr == 32'h1000; data == 32'hDEAD_BEEF;})
// 读操作 - 验证写是否成功
`uvm_do_with(tr, {op == READ; addr == 32'h1000;})
// 检查读回的数据
if (tr.data !== 32'hDEAD_BEEF)
`uvm_error("SEQ", $sformatf("Read back mismatch: expected %h, got %h",
32'hDEAD_BEEF, tr.data))
endtask
endclass
4.5 设计要点与避坑指南
最后,分享几个我在实际项目中踩过的坑:
- 时序问题:AXI4-Lite虽然简单,但握手信号的处理一定要严谨。我曾经在写操作中忘了等awready和wready同时有效,结果数据写错了地址。
- 复位处理:BFM在复位期间不要驱动任何信号。我习惯在BFM里加一个
wait(vif.aresetn);,确保复位释放后再干活。 - 超时机制:每个事务都要加超时。我一般设1000个时钟周期,超时了就报错并打印当前状态。
- 事务复用:事务类里用rand,这样sequence可以随机生成各种组合。但要注意约束别写死,留点灵活性。
嗯,BFM和Driver这块,说白了就是“把复杂的事情简单化”。你封装得越好,上层sequence写起来就越爽。下次咱们聊聊Monitor和Scoreboard,看看怎么把数据“抓”回来并做检查。