摘要
上一期我拆解了Wishbone BFM ,这次把目光移到更"野"的EMIF (External Memory Interface)。EMIF的痛点在于多阶段时序(Setup/Strobe/Hold)和双向三态数据总线 ,在手动验证时极易因时序参数或片选冲突而出错。本文分享我封装的一套EMIF Master BFM ,涵盖参数化时序配置、动态等待轮询与自动化比对。目标是让"写个Flash"和"读个寄存器"变成一句任务调用,不再需要手动摆弄**oe_n和 we_n**。
一、EMIF协议回顾:只讲必须用的那几根线
EMIF 是TI DSP、ZYNQ 及大多数SoC 外扩存储器的标准接口。核心信号清单如下(低有效信号带_n后缀):
-
emif_ce_n(片选):4位,分别对应4个不同的片选空间,低有效。 -
emif_addr/emif_data:地址和数据总线,数据总线一般为双向三态(emif_edio)。 -
emif_oe_n(输出使能):读操作时拉低,驱动数据从DUT 流向Master。 -
emif_we_n(写使能):写操作时拉低,驱动数据从Master 流向DUT。 -
emif_be_n(字节使能):控制写操作时写入哪几个字节,低有效。 -
emif_rwn:有些IP用rwn区分读写(0为写,1为读)。 -
emif_wait(等待信号):最容易被忽略 ,外部DUT 拉低此信号表示当前访问需延长周期,直到DUT释放。
标准读写时序分为三个阶段:
-
建立阶段(Setup) :拉低片选**
ce_n和地址addr**,稳定地址。 -
选通阶段(Strobe) :拉低**
oe_n(读)或we_n(写),进行数据传输。在这个阶段,数据总线由 Master**(写)或DUT(读)驱动。 -
保持阶段(Hold) :拉高**
oe_n/we_n**,保持地址和片选一段时间后释放。
踩坑提醒 :
emif_wait信号可以在Strobe 阶段的任意时刻被DUT 拉低,且拉低后通常需要一直保持 到数据传输结束。BFM 必须在Strobe 阶段内部循环轮询该信号,而不是在Strobe结束后去检查。
二、验证环境中,我为什么坚持要写EMIF BFM?
EMIF验证的难点不在协议本身,而在"信号驱动者的身份切换":
-
写操作时,Master 要把数据输出到总线,需要持续驱动**
emif_edio**。 -
读操作时,Master 要释放三态总线,切换为高阻
z,等DUT把数据摆上来再采样。 -
片选**
ce_n的4个空间切换、字节掩码be_n的合并,分散在 testbench里就是一堆重复的assign和force**。
封装BFM 之后,我在测试用例里只需写一行**emif_32bit_write(2, 0x1000, 0xA5A5A5A5),BFM自动处理 ce_n映射、 be_n掩码、三态切换、 rwn**翻转和等待信号轮询。测试用例从"信号级调试"升级为"事务级驱动",效率至少翻倍。
三、EMIF BFM接口定义:严格位宽与三态控制
我的BFM 采用外部**wire输入时钟,所有控制信号声明为 reg,数据总线声明为双向线网 wire**:
vbnet
// ==========================================================
// 【接口声明】
// ==========================================================
wire emif_clk;
reg [31:0] emif_addr;
reg [3:0] emif_ce_n; // 低有效片选,4个CE空间
reg [3:0] emif_be_n; // 字节使能,低有效
reg emif_we_n; // 写使能
reg emif_oe_n; // 输出使能(读)
reg emif_rwn; // 0为写,1为读
reg [31:0] emif_data_out; // BFM输出缓存
reg emif_edoe; // 1为BFM驱动输出,0为侦听读取
wire [31:0] emif_edio = emif_edoe ? emif_data_out : 32'bz; // 三态总线
wire emif_wait_i; // 从DUT读取的Wait信号
关键技术点 :三态总线用条件运算符实现 emif_edoe ? emif_data_out : 32'bz 。写操作时 emif_edoe = 1 ,读操作时 emif_edoe = 0 。这种显式控制比单纯的 assign 更利于在任务内部按阶段切换,避免由于连续赋值导致的竞争冒险。
四、读写任务封装:参数化时序与动态等待
4.1 32位写任务
vbnet
task emif_32bit_write;
input [7:0] setup_clk; // 建立时间
input [7:0] strobe_clk; // 选通时间
input [7:0] hold_clk; // 保持时间
input [1:0] cs; // 片选空间 2'b00~2'b11
input [31:0] address;
input [31:0] wr_data;
begin
@ (posedge emif_clk);
// -- 片选与信号初始化 --
case(cs)
2'b00 : emif_ce_n = 4'b1110;
2'b01 : emif_ce_n = 4'b1101;
2'b10 : emif_ce_n = 4'b1011;
2'b11 : emif_ce_n = 4'b0111;
endcase
emif_addr = address;
emif_be_n = 4'b0000; // 全字32位写,字节掩码全有效
emif_rwn = 1'b0; // 写操作
emif_data_out = wr_data;
emif_edoe = 1'b1; // 输出使能驱动总线
emif_we_n = 1'b1;
// -- 建立阶段 (Setup) --
repeat(setup_clk) @(posedge emif_clk);
// -- 选通阶段 (Strobe) --
emif_we_n = 1'b0;
repeat(strobe_clk) @(posedge emif_clk);
// 【关键】轮询 DUT 的 wait 信号(低有效)
// 若 DUT 拉低 wait,说明访问外部存储器需要延长总线周期
while(~emif_wait_i) @(posedge emif_clk);
// -- 保持阶段 (Hold) --
emif_we_n = 1'b1;
repeat(hold_clk) @(posedge emif_clk);
// -- 释放总线 --
emif_ce_n = 4'b1111;
emif_addr = 32'h0;
emif_be_n = 4'b1111;
emif_edoe = 1'b0;
emif_data_out= 32'h0;
@(posedge emif_clk);
end
endtask
自我纠错 :早期的版本里我把**wait轮询放在了选通阶段之外,结果 DUT拉低 wait时总线早已结束。实战中发现 wait必须在 Strobe阶段 内部循环检测,且 低有效**,所以采用了 while(~emif_wait_i) 的结构。
4.2 32位读任务
读任务与写任务思路类似,主要区别在于总线驱动方向的切换:
vbnet
task emif_32bit_read;
input [7:0] setup_clk;
input [7:0] strobe_clk;
input [7:0] hold_clk;
input [1:0] cs;
input [31:0] address;
output [31:0] rd_data;
begin
@ (posedge emif_clk);
// ... (片选和地址配置同写操作)
emif_rwn = 1'b1; // 读操作
emif_edoe = 1'b0; // 释放数据总线,等待 DUT 驱动
emif_oe_n = 1'b1;
// -- 建立阶段 --
repeat(setup_clk) @(posedge emif_clk);
// -- 选通阶段 (Strobe) --
emif_oe_n = 1'b0;
repeat(strobe_clk) @(posedge emif_clk);
// 轮询 DUT 的 wait 信号
while(~emif_wait_i) @(posedge emif_clk);
// 【关键】在选通阶段稳定后采样数据
rd_data = emif_edio;
// -- 保持阶段 --
emif_oe_n = 1'b1;
repeat(hold_clk) @(posedge emif_clk);
// -- 释放总线 --
emif_ce_n = 4'b1111;
emif_addr = 32'h0;
emif_be_n = 4'b1111;
@(posedge emif_clk);
end
endtask
核心差异 :在读操作的选通阶段,emif_edoe 必须为 0(释放总线),否则 Master 和 DUT 同时驱动数据总线,会导致仿真 X 态或电路烧毁风险。数据采样时刻务必在 while(~emif_wait_i) 结束之后,确保 wait 已经释放,数据已稳定输出。
五、自动化比对任务:让验证闭环
我专门设计了一个 emif_32bit_read_cmp 任务,内部直接调用了读任务,并在返回后做 if-else 比对:
vbnet
task emif_32bit_read_cmp;
input [7:0] setup_clk;
input [7:0] strobe_clk;
input [7:0] hold_clk;
input [1:0] cs;
input [31:0] address;
input [31:0] rd_cmp_data;
reg [31:0] rd_data;
begin
emif_32bit_read(setup_clk, strobe_clk, hold_clk, cs, address, rd_data);
if(rd_data != rd_cmp_data) begin
$display("[ERROR] EMIF 读回不一致!地址: %h, 期望: %h, 实际: %h", address, rd_cmp_data, rd_data);
end else begin
$display("[INFO ] EMIF 读回验证通过!地址: %h, 数据: %h", address, rd_data);
end
end
endtask
这样的封装,使得我在测试用例里只需一行代码,就能完成"写数据 → 读回 → 自动判定正确性"的闭环,极大降低了通过人工去翻波形对比数据的劳动量。
六、实战踩坑与自我纠错总结
基于这套BFM,我总结出三个一定要划重点的经验,以及我在调试过程中发现的逻辑缺陷:
-
wait信号轮询位置别放错 :一定要放在repeat(strobe_clk)循环内部 ,每过一个周期都要检查。一旦检测到wait低有效,必须用while无限阻塞直到释放,否则访问外部慢速存储器时DUT会直接丢数据。 -
片选
ce_n和字节使能be_n注意低有效 :这是EMIF 最容易出问题的点。我在代码里习惯用4'b1110给ce_n赋值,这意味只有ce_n[0]拉低,其他三个空间高电平。而**be_n =** **4'b0000**表示全部字节使能。上板调不通时,第一反应就是查ce_n和be_n的极性。 -
emif_rwn的极性统一 :不同厂商的IP对读写信号的极性定义各不相同。我固定为 0写1读 ,这样从代码一眼就能分清当前是写还是读。如果遇到厂商IP是反的(1写0读),只需在任务内部对emif_rwn做一次取反,而不是在testbench里到处打补丁。
七、附:完整修正版 BFM 代码(emif_bfm.v)
以下为我完整 BFM 源码,可直接嵌入你的验证环境使用:
vbnet
// ==========================================================
// 【一、声明 BFM 与 DUT 交互用的物理线网】
// ==========================================================
wire emif_clk;
reg [31:0] emif_addr;
reg [3:0] emif_ce_n; // 低有效片选,4个CE空间
reg [3:0] emif_be_n; // 字节使能信号,低有效
reg emif_we_n;
reg emif_oe_n;
reg emif_rwn; // 0为写,1为读
reg [31:0] emif_data_out; // BFM输出缓存
reg emif_edoe; // 1为BFM驱动输出,0为侦听读取
wire [31:0] emif_edio = emif_edoe ? emif_data_out : 32'bz; // 三态总线
wire emif_wait_i; // 从 DUT 读取回来的 Wait 信号
// ==========================================================
// 【二、EMIF 单周期 32位写任务】
// ==========================================================
task emif_32bit_write;
input [7:0] setup_clk; // 建立时间
input [7:0] strobe_clk; // 选通时间
input [7:0] hold_clk; // 保持时间
input [1:0] cs; // 片选空间 2'b00~2'b11
input [31:0] address;
input [31:0] wr_data;
begin
@ (posedge emif_clk);
// -- 片选与信号初始化 --
case(cs)
2'b00 : emif_ce_n = 4'b1110;
2'b01 : emif_ce_n = 4'b1101;
2'b10 : emif_ce_n = 4'b1011;
2'b11 : emif_ce_n = 4'b0111;
default : emif_ce_n = 4'b1111;
endcase
emif_addr = address;
emif_be_n = 4'b0000; // 全字32位写
emif_rwn = 1'b0; // 写操作
emif_data_out = wr_data;
emif_edoe = 1'b1; // 输出使能驱动总线
emif_we_n = 1'b1;
emif_oe_n = 1'b1; // [修正] 显式拉高oe_n,避免X态
// -- 建立阶段 (Setup) --
repeat(setup_clk) @(posedge emif_clk);
// -- 选通阶段 (Strobe) --
emif_we_n = 1'b0;
repeat(strobe_clk) begin
@(posedge emif_clk);
// [修正] 每一拍都轮询 DUT 的 wait 信号(低有效),动态延长Strobe
while(~emif_wait_i) @(posedge emif_clk);
end
// -- 保持阶段 (Hold) --
emif_we_n = 1'b1;
repeat(hold_clk) @(posedge emif_clk);
// -- 释放总线 --
emif_ce_n = 4'b1111;
emif_addr = 32'h0;
emif_be_n = 4'b1111;
emif_edoe = 1'b0; // 释放数据总线
emif_data_out= 32'h0;
@(posedge emif_clk);
end
endtask
// ==========================================================
// 【三、EMIF 单周期 32位读任务】
// ==========================================================
task emif_32bit_read;
input [7:0] setup_clk;
input [7:0] strobe_clk;
input [7:0] hold_clk;
input [1:0] cs;
input [31:0] address;
output [31:0] rd_data;
reg [31:0] rd_data;
begin
@ (posedge emif_clk);
case(cs)
2'b00 : emif_ce_n = 4'b1110;
2'b01 : emif_ce_n = 4'b1101;
2'b10 : emif_ce_n = 4'b1011;
2'b11 : emif_ce_n = 4'b0111;
default : emif_ce_n = 4'b1111;
endcase
emif_addr = address;
emif_be_n = 4'b0000; // 全字32位读
emif_rwn = 1'b1; // 读操作
emif_edoe = 1'b0; // 释放数据总线,等待 DUT 驱动
emif_oe_n = 1'b1;
emif_we_n = 1'b1; // [修正] 写使能保持高电平,避免X态
// -- 建立阶段 --
repeat(setup_clk) @(posedge emif_clk);
// -- 选通阶段 (Strobe) --
emif_oe_n = 1'b0;
repeat(strobe_clk) begin
@(posedge emif_clk);
// [修正] 每一拍轮询 wait,动态延长选通
while(~emif_wait_i) @(posedge emif_clk);
end
// [修正] 在轮询结束后,额外等待一个时钟上升沿,确保总线数据已绝对稳定
@(posedge emif_clk);
#1;
rd_data = emif_edio;
// -- 保持阶段 --
emif_oe_n = 1'b1;
repeat(hold_clk) @(posedge emif_clk);
// -- 释放总线 --
emif_ce_n = 4'b1111;
emif_addr = 32'h0;
emif_be_n = 4'b1111;
@(posedge emif_clk);
end
endtask
// ==========================================================
// 【四、EMIF 单周期 32位读自动比对任务】
// ==========================================================
task emif_32bit_read_cmp;
input [7:0] setup_clk;
input [7:0] strobe_clk;
input [7:0] hold_clk;
input [1:0] cs;
input [31:0] address;
input [31:0] rd_cmp_data;
reg [31:0] rd_data;
begin
emif_32bit_read(setup_clk, strobe_clk, hold_clk, cs, address, rd_data);
if(rd_data != rd_cmp_data) begin
$display("[ERROR] EMIF 读回不一致!地址: %h, 期望: %h, 实际: %h", address, rd_cmp_data, rd_data);
end else begin
$display("[INFO ] EMIF 读回验证通过!地址: %h, 数据: %h", address, rd_data);
end
end
endtask