UVM组件故事版 · driver:那个把"指令"翻译成"电信号"的人
想象一下打仗。
将军坐在指挥部里,写了一份作战命令:"明天凌晨三点,从A点向B点发起进攻,炮火掩护从C方向过来。"
这份命令写得很清楚,但问题来了------士兵在战壕里,拿到的是一张写满字的纸。他们听不懂"炮火掩护从C方向"是什么意思,他们只知道:什么时候该扣扳机,往哪个方向开枪,火力往哪打。
中间需要一个人,把将军的命令翻译成士兵能执行的具体动作。
这个翻译官,就是driver。
driver到底在干什么
在UVM的世界里,这个场景是这样的:
arduino
sequence(将军)→ 发送一个transaction:"我想要你发一个读命令,读地址0x1000"
↓
sequencer(传令兵)→ 把transaction传递给driver
↓
driver(翻译官)→ 把transaction转换成具体的时序信号,送到DUT的接口上
↓
DUT(士兵)→ 收到电信号,执行实际操作
driver做的事情本质上是两件:
1. 接收上层传来的数据对象(transaction)
2. 把这个对象"翻译"成DUT能看懂的时序信号
比如DUT是一个UART模块,它的接口信号长这样:
uart_tx → 发送数据的信号
baud_rate → 波特率信号
driver从sequence那里拿到一条"发数据0x55"的消息,driver要做的事情是:
markdown
1. 把0x55转成二进制:01010101
2. 按照UART协议加上起始位、校验位、停止位
3. 在对应的时钟沿上,把这些bit一个个送到uart_tx信号上
这中间涉及到很精确的时序控制------早了不行,晚了不行,数据宽度不对不行。driver就是那个必须分毫不差的人。
为什么会有人把driver写错
见过两种典型错误:
第一种:把所有事情都塞给driver
有人觉得driver是唯一能接触DUT的人,所以恨不得把所有的判断逻辑都写在driver里:
kotlin
driver里写:if (data == 0x55) { 发送A } else if (data == 0xAA) { 发送B }
这是新手常干的事情。driver不是用来判断"做什么"的地方,那是sequence和test的工作。driver的职责是:我收到了一个任务,把它正确地执行下去。
第二种:不懂得和sequencer配合
driver和sequencer之间有一个握手机制:
arduino
driver发送req给sequencer:"我准备好了,给我下一个任务"
sequencer把下一个transaction传给driver
driver执行,然后再次请求
这个握手叫seq_item_port,是UVM里最常见的组件间通信方式。很多人写driver的时候跳过了这个理解,导致driver只跑一次就卡住了,或者跑起来之后和sequence的节奏完全对不上。
driver的正确打开方式
一个正确的driver,通常长这样:
scss
class uart_driver extends uvm_driver #(uart_transaction);
virtual uart_if vif; // 硬件接口的虚接口
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(virtual uart_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "virtual interface must be set");
endfunction
// driver的核心:不断从sequencer拿任务,执行它
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req); // 从sequencer拿任务(阻塞等待)
drive_transaction(req); // 执行任务
seq_item_port.item_done(); // 告诉sequencer任务完成了
end
endtask
// 把transaction翻译成时序信号
virtual protected task drive_transaction(uart_transaction tr);
vif.rst_n <= 0; // 假设要reset一下
#10;
vif.rst_n <= 1;
// 发送起始位
vif.uart_tx <= 1'b0;
#(BIT_PERIOD);
// 发送8位数据
for (int i = 0; i < 8; i++) begin
vif.uart_tx <= tr.data[i];
#(BIT_PERIOD);
end
// 停止位
vif.uart_tx <= 1'b1;
#(BIT_PERIOD);
endtask
endclass
核心循环就三步:
scss
get_next_item() → drive_transaction() → item_done()
理解了这三步,driver就不难了。
打个比方收尾
sequence是将军,写作战计划。sequencer是传令官,把计划传递给翻译官。driver是翻译官,把将军的命令翻译成士兵能执行的具体动作------几时几分,从哪个方向,开什么枪。
翻译官不需要知道为什么要打这一仗,那是将军的事。翻译官只需要把命令准确无误地执行到位。
driver在UVM验证环境里的角色,就是这样:精确执行,不多不少。
下篇预告:monitor------那个躲在角落里,把一切看见的都记下来的人。
UVM军事系列 · 第一篇:driver------那个把命令翻译成战场语言的人
特种作战行动开始前,指挥部传来一份任务书:
"凌晨0300,A组从东侧突入,B组掩护,C组接应。炮火支援在C组进入位置后30秒启动。"
这份任务书写得清清楚楚,但问题是------前线的士兵拿到的是无线电里嘈杂的指令,他们听不懂"炮火支援在C组进入位置后30秒启动"这种协调语言,他们只知道:什么时候冲,往哪开枪,枪口抬多高。
中间需要一个人,把指挥部的命令翻译成前线士兵能执行的具体动作。
这个人叫通讯兵 ,也叫传译官。
在UVM的世界里,这个角色,叫driver。
driver到底在干什么
特种部队的通讯兵,是指挥部和前线之间唯一的翻译通道。
他的工作看起来很简单:收到命令,执行命令。但细看下去,每一步都藏着细节:
mission_order(任务书)→ 作战参谋排序 → 通讯兵接收任务 → 翻译成战场信号 → 士兵执行
指挥部 调度官 通讯兵 物理动作
driver做的事情本质上只有两件:
第一,接收上层传来的指令对象(mission order)。
第二,把这个指令翻译成DUT能看懂的时序信号。
比如,DUT是一个通信电台模块,它的物理接口信号长这样:
css
tx_data[7:0] → 8位数据线
tx_valid → 数据有效信号(高电平表示data有效)
tx_ready → 电台准备好了(握手信号)
clk → 时钟信号
driver从sequencer那里拿到一条"发送数据0xA5"的消息,driver要做的事情是:
arduino
第一步:在tx_valid上拉高一个时钟周期,同时把0xA5放到tx_data上
第二步:等待tx_ready握手信号(告诉士兵"我收到命令了")
第三步:清空tx_valid,一个命令执行完毕
这中间涉及精确的时序控制------早了,电台还没准备好,数据丢了;晚了,士兵的火力窗口错过了。driver就是那个必须分毫不差的人。
为什么会有人把通讯兵派错岗位
见过两种典型的错误:
第一种:让通讯兵去决定打不打。
有人觉得通讯兵是唯一能接触前线的人,所以把"战术判断"也塞给他:
arduino
driver里写:if (urgent_mission) { 立即执行 } else { 排队等 }
这是新手常干的事。通讯兵不是战术决策者,那是作战参谋的活。通讯兵的职责是:我收到命令了,准确地翻译和执行。 翻译官不需要知道为什么要打这一仗,那是将军的事。
第二种:通讯兵不听参谋的调度,擅自行动。
driver和sequencer之间有一个握手机制:
arduino
通讯兵(driver):"我执行完了,下一个任务是什么?"
作战参谋(sequencer):"收到,给你新的任务。"
通讯兵执行新任务,再问:"执行完了,下一个呢?"
......循环往复
这个握手在UVM里叫seq_item_port,是UVM中最常见的通讯方式。多数人写driver的时候跳过了这个理解,导致通讯兵只跑一轮就停在那里,或者自顾自地一直发,完全不听参谋的节奏。
driver的正确打开方式
一个正确的通讯兵(driver),是这样的:
scala
class comm_driver extends uvm_driver #(mission_item);
virtual radio_if vif; // 电台物理接口
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 从配置池里拿到电台接口
if (!uvm_config_db #(virtual radio_if)::get(this, "", "vif", vif))
`uvm_fatal("NORADIO", "电台接口未配置,通讯兵没有电台怎么打仗?")
endfunction
// 通讯兵的日常:等命令 → 执行 → 报告完成 → 等下一个
task run_phase(uvm_phase phase);
forever begin
// 从作战参谋那里拿到任务(阻塞等待,没有任务就待命)
seq_item_port.get_next_item(req);
transmit_mission(req); // 执行任务
seq_item_port.item_done(); // 报告参谋:任务完成
end
endtask
// 具体的翻译工作:把任务对象变成电台信号
virtual protected task transmit_mission(mission_item tr);
// 第一步:把数据放到数据线上,同时拉高有效信号
@(posedge vif.clk);
vif.tx_valid <= 1'b1;
vif.tx_data <= tr.payload;
// 第二步:等待电台握手确认
do begin
@(posedge vif.clk);
end while (vif.tx_ready == 1'b0); // 电台没ready就一直等
// 第三步:任务完成,清除有效信号
@(posedge vif.clk);
vif.tx_valid <= 1'b0;
endtask
endclass
核心循环就三步:
scss
get_next_item() → transmit_mission() → item_done()
从参谋拿任务 执行翻译 报告完成
理解这三步,通讯兵就不难当了。
打个比方收尾
sequence是将军,写作战命令。sequencer是作战参谋,把命令排序整理。driver是通讯兵,把参谋整理好的命令翻译成前线士兵能执行的具体动作------几点几分,从哪个方向,打什么目标,打几发。
通讯兵不需要知道为什么要打这一仗,那是将军的事。通讯兵只需要把命令准确无误地传达下去。
迟一秒不行,早一秒也不行,信号错了更不行。
这就是driver在UVM验证环境里的角色:精确执行,不多不少。
下篇预告:monitor------那个躲在暗处,把战场上发生的一切都记录下来的人。侦察兵不上前线,但战场上没有人比他更清楚发生了什么。