在数字芯片验证领域,PCIe协议的验证工作一直是验证工程师面临的重要挑战之一。今天我们要探讨的是PCIe验证中最基础但至关重要的MemRd(Memory Read)和MemWr(Memory Write)事务的UVM验证方法。这两个基础事务构成了PCIe设备间通信的基石,掌握它们的验证方法对于构建完整的PCIe验证环境至关重要。
作为一名有十年经验的验证工程师,我发现很多新手在刚开始接触PCIe验证时,往往会被复杂的协议细节所困扰。实际上,只要掌握了MemRd/MemWr这两个基础事务的验证方法,就能为后续更复杂的TLP(Transaction Layer Packet)验证打下坚实基础。本文将分享我在实际项目中总结的UVM序列编写技巧和数据比对方法,这些经验已经帮助团队成功验证了多个PCIe Gen3/Gen4设备。
MemRd和MemWr是PCIe协议中最常用的两种TLP类型,它们分别用于实现内存读和内存写操作。在实际验证中,我们需要关注以下几个关键特性:
一个典型的PCIe UVM验证环境包含以下组件:
code复制┌───────────────────────┐
│ Testbench │
│ ┌───────────────────┐ │
│ │ UVM Test │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ UVM Sequence │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Driver │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Monitor │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Scoreboard │ │
│ └───────────────────┘ │
└───────────────────────┘
在这个架构中,序列(Sequence)负责生成激励,驱动器(Driver)将TLP发送到DUT,监视器(Monitor)捕获DUT的响应,记分板(Scoreboard)则进行数据比对。
首先我们需要创建一个基础的MemRd/MemWr序列类。这里我推荐使用uvm_sequence作为基类,因为它提供了丰富的序列控制功能。
systemverilog复制class pcie_base_sequence extends uvm_sequence #(pcie_tlp_item);
`uvm_object_utils(pcie_base_sequence)
// 序列参数
rand int unsigned num_transactions = 10;
rand bit [63:0] base_address;
rand int unsigned min_data_length = 1;
rand int unsigned max_data_length = 256;
// 约束条件
constraint c_valid_range {
base_address % 4 == 0; // 确保地址对齐
min_data_length <= max_data_length;
max_data_length <= 256;
}
function new(string name="pcie_base_sequence");
super.new(name);
endfunction
virtual task body();
`uvm_info(get_type_name(), "Starting sequence", UVM_LOW)
repeat(num_transactions) begin
pcie_tlp_item tlp;
`uvm_do_with(tlp, {
if ($urandom_range(0,1)) tlp.tlp_type == MEM_RD;
else tlp.tlp_type == MEM_WR;
tlp.address inside {[base_address:base_address+32'hFFFF]};
tlp.length inside {[min_data_length:max_data_length]};
})
end
endtask
endclass
在实际项目中,我们还需要考虑更复杂的场景:
systemverilog复制constraint c_address_dist {
// 30%概率选择低地址区域
address_range dist {
[32'h0000_0000:32'h000F_FFFF] :/ 30,
[32'h0010_0000:32'hFFFF_FFFF] :/ 70
};
}
systemverilog复制virtual task inject_error();
pcie_tlp_item err_tlp;
`uvm_do_with(err_tlp, {
tlp_type == MEM_RD;
// 故意设置非法地址
address == 64'hDEAD_BEEF_DEAD_BEEF;
// 设置错误的ECRC
ecrc_err == 1;
})
endtask
systemverilog复制class pcie_complex_sequence extends pcie_base_sequence;
task body();
// 先执行一些正常操作
super.body();
// 然后注入错误
inject_error();
// 最后执行边界测试
boundary_test();
endtask
endclass
PCIe验证中的数据比对需要考虑以下几个关键点:
一个典型的记分板实现如下:
systemverilog复制class pcie_scoreboard extends uvm_scoreboard;
`uvm_component_utils(pcie_scoreboard)
// 存储模型
bit [7:0] mem_model[bit [63:0]];
uvm_analysis_imp #(pcie_tlp_item, pcie_scoreboard) item_export;
function new(string name, uvm_component parent);
super.new(name, parent);
item_export = new("item_export", this);
endfunction
function void write(pcie_tlp_item tlp);
case(tlp.tlp_type)
MEM_WR: process_memwr(tlp);
MEM_RD: process_memrd(tlp);
CPL: process_cpl(tlp);
default: `uvm_error("SCBD", $sformatf("Unknown TLP type: %s", tlp.tlp_type.name()))
endcase
endfunction
function void process_memwr(pcie_tlp_item tlp);
foreach(tlp.data[i]) begin
if(tlp.byte_enable[i]) begin
mem_model[tlp.address + i] = tlp.data[i];
end
end
endfunction
function void process_memrd(pcie_tlp_item tlp);
// 存储读请求,等待完成包
pending_reads[tlp.tag] = tlp;
endfunction
function void process_cpl(pcie_tlp_item tlp);
pcie_tlp_item original_rd;
if(!pending_reads.exists(tlp.tag)) begin
`uvm_error("SCBD", $sformatf("Unexpected completion for tag %0h", tlp.tag))
return;
end
original_rd = pending_reads[tlp.tag];
foreach(tlp.data[i]) begin
if(mem_model[original_rd.address + i] != tlp.data[i]) begin
`uvm_error("SCBD", $sformatf(
"Data mismatch at address %0h: expected %0h, got %0h",
original_rd.address + i,
mem_model[original_rd.address + i],
tlp.data[i]
))
end
end
pending_reads.delete(tlp.tag);
endfunction
endclass
在实际项目中,我们还需要考虑以下优化点:
systemverilog复制// 使用关联数组代替静态数组
typedef bit [7:0] byte_t;
byte_t mem_model[bit [63:0]];
systemverilog复制// 在发现错误时提供更多上下文信息
`uvm_error("SCBD", $sformatf(
"Data mismatch for read request:\n%s\nCompletion:\n%s\nMemory dump around address %0h:\n%s",
original_rd.sprint(),
tlp.sprint(),
original_rd.address,
dump_memory_region(original_rd.address - 16, 32)
))
systemverilog复制// 在记分板中收集功能覆盖率
covergroup pcie_cg;
address_alignment: coverpoint tlp.address % 4 {
bins aligned[] = {0,1,2,3};
}
data_length: coverpoint tlp.length {
bins small = {[1:16]};
bins medium = {[17:64]};
bins large = {[65:256]};
}
endgroup
在MemRd/MemWr验证过程中,我们经常会遇到以下几类问题:
提示:PCIe规范要求DW地址对齐,即地址必须是4的倍数。在序列中需要添加约束确保地址正确对齐。
systemverilog复制// 正确的长度计算方式
tlp.length = (data_size + 3) / 4; // 向上取整到DW数
注意:如果发现MemRd请求后没有收到完成包,首先检查DUT的配置寄存器是否使能了内存空间,然后确认地址映射是否正确。
根据我的项目经验,以下调试技巧非常有用:
systemverilog复制// 在Monitor中添加TLP解码打印
`uvm_info("MON", $sformatf(
"Received TLP: Type=%s, Addr=%0h, Length=%0d, Data=%p",
tlp.tlp_type.name(),
tlp.address,
tlp.length,
tlp.data
), UVM_HIGH)
tlp_valid:TLP有效信号tlp_type:TLP类型字段tlp_addr:地址字段tlp_data:数据字段tlp_byte_en:字节使能systemverilog复制// 在测试用例中添加以下语句可以增强调试能力
// 设置全局报告级别
uvm_top.set_report_verbosity_level(UVM_DEBUG);
// 打印组件拓扑结构
uvm_top.print_topology();
// 设置特定组件的调试级别
env.scoreboard.set_report_verbosity_level(UVM_FULL);
一个完整的PCIe验证环境需要包含以下测试用例:
systemverilog复制class pcie_basic_test extends uvm_test;
task run_phase(uvm_phase phase);
pcie_base_sequence seq = pcie_base_sequence::type_id::create("seq");
seq.start(env.sequencer);
endtask
endclass
systemverilog复制class pcie_boundary_test extends pcie_basic_test;
task run_phase(uvm_phase phase);
pcie_boundary_sequence seq = pcie_boundary_sequence::type_id::create("seq");
seq.min_data_length = 1;
seq.max_data_length = 256;
seq.start(env.sequencer);
endtask
endclass
systemverilog复制class pcie_error_test extends pcie_basic_test;
task run_phase(uvm_phase phase);
pcie_error_sequence seq = pcie_error_sequence::type_id::create("seq");
seq.error_prob = 0.3; // 30%的错误注入概率
seq.start(env.sequencer);
endtask
endclass
为了确保验证的完备性,我们需要收集以下覆盖率:
systemverilog复制covergroup pcie_tlp_cg;
tlp_type: coverpoint tlp.tlp_type {
bins mem_rd = {MEM_RD};
bins mem_wr = {MEM_WR};
bins cpl = {CPL};
}
address_range: coverpoint tlp.address {
bins low = {[0:32'h000F_FFFF]};
bins mid = {[32'h0010_0000:32'h7FFF_FFFF]};
bins high = {[32'h8000_0000:32'hFFFF_FFFF]};
}
data_length: coverpoint tlp.length {
bins small = {[1:16]};
bins medium = {[17:64]};
bins large = {[65:256]};
}
endgroup
systemverilog复制// 示例断言
property p_memwr_response;
@(posedge clk) disable iff(!reset_n)
(tlp_valid && tlp.tlp_type == MEM_WR) |-> ##[1:16] check_memwr_response(tlp);
endproperty
在大型PCIe验证项目中,仿真速度常常成为瓶颈。以下是我总结的优化技巧:
systemverilog复制// 使用TLM接口代替Pin级接口
uvm_tlm_analysis_port #(pcie_tlp_item) item_port;
systemverilog复制// 使用稀疏存储模型
class sparse_memory_model;
bit [7:0] mem[bit [63:0]];
function void write(bit [63:0] addr, bit [7:0] data);
mem[addr] = data;
endfunction
endclass
systemverilog复制// 动态调整日志级别
function void set_verbosity(int level);
uvm_top.set_report_verbosity_level(level);
env.set_report_verbosity_level(level);
endfunction
systemverilog复制// 创建可配置的序列库
class pcie_sequence_lib extends uvm_sequence_library #(pcie_tlp_item);
`uvm_object_utils(pcie_sequence_lib)
`uvm_sequence_library_utils(pcie_sequence_lib)
function new(string name="pcie_sequence_lib");
super.new(name);
init_sequence_library();
endfunction
endclass
systemverilog复制// 在测试用例中重载特定组件
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 重载标准驱动器为性能优化版本
pcie_driver::type_id::set_type_override(pcie_fast_driver::get_type());
endfunction
systemverilog复制// 使用多个并行序列
task run_phase(uvm_phase phase);
fork
seq1.start(env.sequencer1);
seq2.start(env.sequencer2);
join
endtask
在实际PCIe验证项目中,我总结了以下几点重要经验:
systemverilog复制// 添加调试钩子
function void start_of_simulation();
// 设置默认波形记录
$fsdbDumpfile("wave.fsdb");
$fsdbDumpvars(0, top);
// 设置UVM回调
uvm_callbacks #(pcie_driver, pcie_driver_cb)::add(env.driver, driver_cb);
endfunction
systemverilog复制// 自动化回归测试脚本
module regress;
initial begin
string test_names[] = {
"pcie_basic_test",
"pcie_boundary_test",
"pcie_error_test"
};
foreach(test_names[i]) begin
run_test(test_names[i]);
check_coverage();
end
end
endmodule
为了确保DUT完全符合PCIe规范,我们需要进行协议一致性验证:
systemverilog复制class pcie_protocol_checker extends uvm_component;
// 检查TLP格式
function void check_tlp_format(pcie_tlp_item tlp);
if(tlp.tlp_type == MEM_WR && tlp.length == 0)
`uvm_error("PROT", "MemWr with length 0 is illegal")
endfunction
// 检查地址对齐
function void check_address_alignment(pcie_tlp_item tlp);
if(tlp.address % 4 != 0)
`uvm_error("PROT", $sformatf("Unaligned address: %0h", tlp.address))
endfunction
endclass
除了功能验证外,性能验证也是PCIe验证的重要部分:
systemverilog复制// 测量吞吐量的序列
class pcie_throughput_sequence extends pcie_base_sequence;
real throughput;
task body();
time start_time, end_time;
int byte_count;
start_time = $time;
super.body();
end_time = $time;
byte_count = calculate_byte_count();
throughput = byte_count / (end_time - start_time);
`uvm_info("SEQ", $sformatf("Throughput: %0.2f MB/s", throughput), UVM_MEDIUM)
endtask
endclass
systemverilog复制// 在记分板中测量延迟
function void process_memrd(pcie_tlp_item tlp);
tlp.start_time = $time;
pending_reads[tlp.tag] = tlp;
endfunction
function void process_cpl(pcie_tlp_item tlp);
pcie_tlp_item rd = pending_reads[tlp.tag];
real latency = ($time - rd.start_time) / 1ns;
latency_stats.add_sample(latency);
endfunction
对于大型PCIe验证项目,良好的版本控制至关重要:
code复制verification/
├── env/ # UVM环境代码
├── tests/ # 测试用例
├── sequences/ # 序列库
├── models/ # 参考模型
├── scripts/ # 脚本文件
└── docs/ # 文档
main:稳定版本dev:开发分支feature/*:功能开发分支bugfix/*:问题修复分支建立自动化验证流程:
bash复制#!/bin/bash
# 自动化验证脚本示例
# 运行回归测试
vcs -R -l run.log +UVM_TESTNAME=pcie_regression_test
# 检查仿真结果
grep "UVM_ERROR" run.log
if [ $? -eq 0 ]; then
echo "Test failed!"
exit 1
fi
# 检查覆盖率
urg -dir simv.vdb -report coverage_report
if [ $? -ne 0 ]; then
echo "Coverage target not met!"
exit 1
fi
echo "All tests passed!"
exit 0
完善的文档应包括:
在验证MemRd/MemWr这类基础PCIe事务时,我发现最有效的调试方法是在Monitor和Scoreboard中添加详细的日志信息,同时配合波形调试。对于复杂的时序问题,我通常会创建一个最小化的测试用例来重现问题,这样可以避免其他因素的干扰。另外,建议在项目初期就建立完善的功能覆盖率模型,随着验证的进行不断调整,确保覆盖所有关键场景。