一、UVM验证

1 常见组件

1.1 sequencer

  • uvm_sequencer 是一个参数化类,其参数是 my_transaction。
  • 在 sequencer 的帮助下,sequence 产生的 transaction 才能最终送给 driver。
  • sqr 和 drv 相连接:drv.seq_item_port.connect(sqr.seq_item_export) 将 sqr 的 trans 传递给 drv。

1.2 driver

  • uvm_driver 是一个参数化类,定义 driver 时需指明参数 transaction。
  • uvm_driver 具有两个参数:request / response。默认情况下二者类型一致,一般指定为用户定义的 transaction 类型;它们也是 uvm_driver 自带 seq_item_port 的参数类型。
  • 环境中 driver 组件的变量值会保持至仿真结束(与 phase 执行相关)。

注意

  • get_next_item() 是阻塞的:一直等待 trans 到来(get_next_item → item_done)。
  • try_next_item() 是非阻塞的:若无新 trans 直接返回,req == null;否则 req = trans。
1
2
3
4
5
6
7
8
9
while (1) begin
seq_item_port.try_next_item(req);
if (req == null)
@(posedge vif.clk);
else begin
drive_one_pkt(req);
seq_item_port.item_done();
end
end

1.3 transaction

  • sequence 相当于一个弹夹,里面的子弹相当于 transaction,而 sequencer 相当于手枪。
  • transaction 使用 uvm_object_utils 宏注册到 factory 中(有生命周期)。

注意 transaction 的句柄每次发送给其它组件时,需要 new() 或 type_id::create 开辟新空间,避免所有句柄指向同一空间。

1.4 测试用例

测试 DUT 是否按照预期工作,需要施加不同的激励,这些激励称为测试向量或 pattern。

1.5 uvm_pkg

uvm_pkg 中包含 uvm 内建的变量、方法、机制,需在 UVM 验证环境中导入使用:

1
import uvm_pkg::*;

1.6 scoreboard

scoreboard 可以订阅来自被测设计(DUT)和预期输出的事务,主要依赖 UVM 提供的消息传递与事件通知机制来处理和比较事务。由于事件驱动模型不依赖时钟信号,因此 scoreboard 不需要时钟,即 scoreboard 的 main_phase 不需要时钟信号。

2 sequence

  • sequence 相当于一个参数化类,用于产生激励。
  • sequence 使用 uvm_object_utils 宏注册到 factory 中;当 trans 都发送完后,sequence 生命周期结束(有生命周期)。
  • 每一个 sequence 都应该派生自 uvm_sequence,定义时需指定要产生的 transaction。sequence 启动后会执行 body 任务;除 body 外,还会自动调用 pre_body 与 post_body。
  • sequence 启动后,再次启动不需要重新声明与例化(原表述有误,同一 sequence 可以多次启动)。
  • sequence 例化多份时,各实例内部变量值互不干扰(不同 class 实例)。
  • sequence 中可以使用 rand 类型定义变量,变量名尽量不与 trans 一样。

sequence 随机 transaction 的方法:

  • 通过 start_item / finish_item(常用,最透明):
1
2
3
my_trans = new("my_trans");
start_item(my_trans);
finish_item(my_trans);

注意 start_item 和 finish_item 之间避免添加延时:start_item 返回后,该 sequence 赢得仲裁并可访问 sequencer/driver;若在 finish_item 之前加入延时,会延长占用时间,导致其它 sequence 延迟执行。

  • 在 start_item / finish_item 之间做随机与赋值(常用写法):
1
2
3
4
5
my_trans req;
req = my_trans::type_id::create("req");
start_item(req);
void'(req.randomize());
finish_item(req);
  • 在 start_item / finish_item 之间增加内嵌约束(randomize() with { ... } 控制某一次发包):详见 2.2

  • 将“创建/随机/发送”拆成宏(写起来更短,但可见性更差):

    • uvm_create(req):用 factory 在当前 sequencer 上创建 item(等价于 type_id::create/create_item)
    • uvm_send(req):只负责 start_item(req) + finish_item(req),不做 randomize
    • uvm_rand_send(req):start_item + randomize + finish_item
    • uvm_do(req):uvm_create(req) + uvm_rand_send(req)(最常见的一行发包宏)
    • uvm_do_with(req, { ... }):uvm_do(req) 但 randomize 时带 with 约束
    • uvm_do_on(req, sqr) / uvm_do_on_with(req, sqr, { ... }):指定在哪个 sequencer 上跑
    • uvm_do_pri(req, pri) / uvm_do_pri_with(req, pri, { ... }):指定 sequencer 仲裁优先级

uvm_do 系列宏“内部做了哪些操作”(等价展开思路):

  • 1)创建 item(factory create,便于 override)
  • 2)start_item:向 sequencer 申请仲裁并等待 driver 拉取
  • 3)randomize:对 item 随机(可带 with 内嵌约束)
  • 4)finish_item:将 item 交给 sequencer,driver 侧 get_next_item 能拿到并驱动
1
2
3
4
5
my_trans req;
req = my_trans::type_id::create("req");
start_item(req);
if (!req.randomize()) `uvm_fatal("RANDFAIL", "randomize failed")
finish_item(req);

补充:调用 start_item/finish_item 前需要先例化 transaction;随机化只要发生在 finish_item 之前即可。

sequence 的启动方法:

  1. 手动启动(常用):创建一个 my_sequence 实例 seq,再调用 start 任务。start 的参数是一个 sequencer 指针;不指明则 sequence 不知道将产生的 transaction 交给哪个 sequencer。
  2. 自动启动:sequence 和 sequencer 关联,例如:
1
2
3
uvm_config_db#(uvm_object_wrapper)::set(
xx, xx, "default_sequence", sequence::type_id::get()
);
  1. 其它启动方式:可参考后续内容。

2.2 约束与随机化(constraint random)

约束随机验证的核心是:用约束把随机空间“剪”成你关心的子空间,再通过大量随机种子覆盖状态空间;当覆盖出现空洞时,再通过调整约束引导命中。

2.2.1 rand / randc 与 constraint 块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class pkt extends uvm_sequence_item;
`uvm_object_utils(pkt)

rand bit [1:0] cmd;
rand bit [31:0] addr;
randc bit [7:0] tag;

constraint c_basic {
cmd inside {[0:3]};
addr[1:0] == 0;
}

function new(string name = "pkt");
super.new(name);
endfunction
endclass

要点:

  • rand:每次随机相互独立
  • randc:循环随机,遍历完取值空间后再重新洗牌(空间过大时不现实)
  • 约束块可拆成多个,便于按场景开关(见 constraint_mode())

2.2.2 内嵌约束(randomize with)

适用:只想在“某一次发包”施加额外约束,不改类本身的默认约束。

1
2
3
4
5
pkt req;
req = pkt::type_id::create("req");
start_item(req);
if (!req.randomize() with { cmd == 2; addr[15:12] inside {[4:8]}; }) `uvm_fatal("RANDFAIL", "randomize failed")
finish_item(req);

对应的宏写法:

1
2
pkt req;
`uvm_do_with(req, { cmd == 2; addr[15:12] inside {[4:8]}; })

2.2.3 常用约束语法速查

inside / 集合:

1
2
constraint c0 { cmd inside {0, 2, 3}; }
constraint c1 { addr inside {[32'h1000:32'h1fff]}; }

dist / 分布:

1
2
3
constraint c_dist {
cmd dist { 0 := 50, 1 := 10, [2:3] := 40 };
}

dist 里 := 与 :/ 的区别:

  • :=:权重按“每个取值”分配(per-value weight)。区间写法会把权重平均摊到区间内每个值上
  • :/:权重按“整个区间/整个集合”分配(per-range weight)。区间整体拿到该权重,再在区间内均匀取值

例子:下面两条约束的概率不同

1
2
3
4
5
6
7
constraint c_dist_per_value {
cmd dist { [0:3] := 40, 4 := 60 };
}

constraint c_dist_per_range {
cmd dist { [0:3] :/ 40, 4 :/ 60 };
}

直觉对比(假设 cmd 只允许 0~4):

  • :=:P(cmd==4)=60%,剩下 40% 平均到 0/1/2/3,每个约 10%
  • :/:P(cmd in 0..3)=40%,区间内再均匀;P(cmd==4)=60%

soft / 软约束(可被更强约束覆盖):

1
constraint c_soft { soft cmd == 0; }

solve ... before ... / 解算顺序(常用于数组长度/依赖关系):

1
2
3
4
5
6
7
rand int unsigned len;
rand byte payload[];
constraint c_len {
len inside {[1:256]};
solve len before payload;
payload.size() == len;
}

foreach / 数组元素约束:

1
2
3
constraint c_each {
foreach (payload[i]) payload[i] inside {[8'h00:8'hff]};
}

|| / or(二选一、多选一约束):

适用:需要表达“满足 A 或满足 B”,而不是把条件都同时满足(AND)。

1
2
3
4
5
6
7
8
9
constraint c_or {
(cmd == 0) || (cmd == 1) || (cmd == 2);
}

constraint c_or2 {
(cmd == 0 && addr[3:0] == 0) ||
(cmd == 1 && addr[3:0] == 4) ||
(cmd == 2 && addr[3:0] inside {8, 12});
}

find with(先筛选候选集合,再用 inside 约束随机):

适用:手上先有一堆“可选项”,你只想从其中“符合条件的那部分”里随机挑一个。

数通芯片例子:交换芯片有 64 个 egress port,但你这次只想从“链路 up 且没被屏蔽”的端口里随机挑一个去打流量。

1
2
3
4
5
6
7
8
int unsigned cand_q[$];
int unsigned all_q[$];
int unsigned pick;

all_q = '{32'h1000, 32'h1004, 32'h1008, 32'h1010, 32'h2000};
cand_q = all_q.find with (item[7:0] inside {[8'h00:8'h1f]});

void'(std::randomize(pick) with { pick inside {cand_q}; });

使用条件:

  • find with 是“过程代码里筛选集合”的语义,更适合在 sequence/test 里先筛出 cand_q,再用 inside {cand_q} 进行约束随机
  • 如果筛选结果可能为空,需要先检查 cand_q.size() > 0,否则 inside {cand_q} 会导致 randomize 失败

2.2.4 约束开关:constraint_mode / rand_mode

适用:同一个 transaction 类,默认约束用于大多数 case;特殊 case 需要临时关闭某一组约束或某个字段随机。

1
2
3
4
pkt req;
req = pkt::type_id::create("req");
req.c_basic.constraint_mode(0);
req.addr.rand_mode(0);

2.2.5 randomize 失败的排查套路

1
if (!req.randomize()) `uvm_fatal("RANDFAIL", "randomize failed")

常见原因(按出现频率):

  • 约束互相冲突(默认约束 + 内嵌约束叠加后无解)
  • 字段不是 rand 却被约束
  • 动态数组 size() 约束缺少 solve before
  • 写了不可能的位切片范围或写错了 inside 区间

2.2.6 覆盖驱动约束的思路(填洞)

做法:先定义 coverpoint/cross -> 看 coverage report 的 hole -> 用 dist/inside 或者把洞对应的组合变成“可达且高概率”。

2.1 摘记:sequence / transaction / pull 握手(LeslieQ)

参考原文:笔记 sequence

  • UVM 为什么好用:核心是约束随机验证(constrained random),用仿真随机向量探索状态空间
    • 约束随机更容易“撞出” bug(尤其是 corner case)
    • 自动生成激励,适合长时间跑回归
  • 验证的 CCC:
    • checker:自动检查结果是否正确
    • coverage:量化“跑到了多少场景”,暴露覆盖空洞
    • constraint:通过约束控制随机空间;有覆盖空洞时,通常通过调整/添加约束来引导命中

2.1.1 agent(可复用单元)结构与事务流

  • 每个 DUT 主要接口通常配 1 个 agent(总线/分组/串行等)
  • agent 的典型“下游/上游”分工:
    • 下游:sequencer + driver 生成激励并注入 DUT
    • 上游:monitor 采样引脚,组装为 transaction,分发到 checker/coverage 等分析组件
flowchart TD
    AGT[agent]
    SEQ[sequencer] -->|item| DRV[driver]
    DRV -->|pins| DUT[DUT interface]
    DUT -->|pins| MON[monitor]
    MON -->|transaction| CHK[checker/scoreboard]
    MON -->|transaction| COV[coverage]

2.1.2 sequence / sequencer 的“动态 vs 准静态”

  • sequence 是 object:运行时动态执行(典型在 run_phase),在 sequencer 上运行并生成 transaction
  • sequence 的 body 不止能发 transaction:也常用来 start 子 sequence,或通过寄存器模型读写寄存器
  • sequencer 是 component:准静态,在 build_phase 0 时刻创建并驻留在组件树中

2.1.3 sequencer-driver:TLM pull 接口(图片重画)

  • driver 与 sequencer 的连接是事务级(TLM),常见形式是 “pull”:driver 主动拉取 item
  • 连接形态上:
    • driver 侧是 port(提出“我需要 get_next_item/item_done 这一套接口”)
    • sequencer 侧是 export(声明“我提供这一套接口的实现”)
  • 直觉上会觉得 transaction 从左到右流动,但控制流(函数调用)常是从右到左发起
flowchart LR
    DRV[driver] -->|connect| P["seq_item_port<br/>uvm_seq_item_pull_port"]
    P --> E["seq_item_export<br/>uvm_seq_item_pull_export"]
    E --> SEQ[sequencer]

2.1.4 transaction(sequence_item)的建议写法

  • transaction 通常继承 uvm_sequence_item
  • 用 uvm_object_utils 注册到工厂(transaction 不是 component,没有 parent)
  • 字段加 rand,并加合法性约束保证随机结果“至少像样”
  • 建议实现 convert2string(),便于 log/compare/调试
1
2
3
4
5
6
7
8
9
10
class my_trans extends uvm_sequence_item;
`uvm_object_utils(my_trans)
rand bit [7:0] cmd;
rand bit [31:0] addr;
rand bit [31:0] data;
constraint legal_c { cmd inside {[0:3]}; }
function new(string name = "my_trans");
super.new(name);
endfunction
endclass

2.1.5 典型发包四步:create → start_item → randomize → finish_item

  • 最小 sequence 往往就是“发 1 笔 transaction”,常见四步如下
  • uvm_do(req) 是宏封装,初学阶段更建议显式四步,便于理解握手点在哪里
1
2
3
4
5
6
7
8
9
10
11
12
13
class one_tx_seq extends uvm_sequence#(my_trans);
`uvm_object_utils(one_tx_seq)
function new(string name = "one_tx_seq");
super.new(name);
endfunction
virtual task body();
my_trans req;
req = my_trans::type_id::create("req");
start_item(req);
void'(req.randomize());
finish_item(req);
endtask
endclass

2.1.6 start_item / finish_item 与 get_next_item / item_done 的握手(图片重画)

  • 同步点 1:start_item 与 get_next_item
  • 同步点 2:finish_item 与 item_done
  • 谁先调用决定“阻塞发生在哪一端”,但协议是同一套
sequenceDiagram
    participant S as sequence
    participant D as driver
    Note over S,D: Case A:sequence 先 start_item
    S->>D: start_item(req) (block)
    D->>S: get_next_item(req)
    S->>S: randomize/赋值
    S->>D: finish_item(req) (block)
    D->>D: drive pins
    D->>S: item_done()
    S->>S: finish_item 返回
    Note over S,D: Case B:driver 先 get_next_item
    D->>S: get_next_item(req) (block)
    S->>D: start_item(req) (立刻返回)
    S->>S: randomize/赋值
    S->>D: finish_item(req)
    D->>D: drive pins
    D->>S: item_done()

2.1.7 启动子 sequence 与随机稳定性

  • 父 sequence 启动子 sequence:child.start(m_sequencer, this)
  • 更完整(强调随机稳定性/phase 语义)的启动步骤:
    • create 子 sequence
    • set_item_context(this, m_sequencer)(让子 sequence “绑定”到将要运行的 sequencer 上下文)
    • randomize 子 sequence(若其自身有 rand 参数)
    • set_starting_phase(phase)(让该 sequence 可对 phase raise/drop objection)

2.1.8 virtual sequence:跨多个 agent 的编排器(图片重画)

  • virtual sequence 的核心价值:统一编排多个接口上的激励行为
  • 一般不直接产生 transaction;更多是启动多个子 sequence
flowchart TD
    VSEQ[virtual sequence] -->|start| SEQ1[agent1 sequencer]
    VSEQ -->|start| SEQ2[agent2 sequencer]
    SEQ1 --> DRV1[agent1 driver]
    SEQ2 --> DRV2[agent2 driver]
    DRV1 --> DUT1[DUT if1]
    DRV2 --> DUT2[DUT if2]

2.1.9 从 sequence 取配置:以“当前 sequencer”为锚点

  • 常见思路:环境 build_phase 把配置对象 set 到与某个 agent/sequencer 路径绑定的位置
  • sequence 运行时通过 get_sequencer() 拿到当前 sequencer,再按该路径 get 配置
1
2
3
4
5
6
7
8
9
10
class cfg_seq extends uvm_sequence#(my_trans);
`uvm_object_utils(cfg_seq)
function new(string name = "cfg_seq");
super.new(name);
endfunction
virtual task body();
my_cfg cfg;
void'(uvm_config_db#(my_cfg)::get(null, get_sequencer().get_full_name(), "cfg", cfg));
endtask
endclass

基于配置生成多笔事务(文章示例思路:cfg.count 决定循环次数,并用内联约束绑定字段):

1
2
3
4
5
6
7
for (int i = 0; i < cfg.count; i++) begin
my_trans req;
req = my_trans::type_id::create("req");
start_item(req);
void'(req.randomize() with { data == i; });
finish_item(req);
end

2.1.10 TLM 三套常用方法:put / get / write(阻塞与数据/控制流)

  • put/get:阻塞(task),适合传递“不会丢、不覆盖”的事务流
    • put:生产者 push 给消费者;直到对端准备好接收才返回
    • get:消费者从生产者 pull;直到生产者准备好返回事务才返回;返回后“下次 get 得到下一个”
  • write:非阻塞(function),常用于 analysis 通道广播;接收方必须“立刻处理并返回”

push vs pull 的“数据流/控制流”对比:

flowchart LR
    PUSH["push (put)"]
    P1[producer] -->|control put| C1[consumer]
    P1 -->|data tx| C1
    PULL["pull (get)"]
    C2[consumer] -->|control get| P2[producer]
    P2 -->|data tx| C2

2.1.11 port / export / imp:方法调用如何穿过层级(图片重画)

  • port:让组件“向外”发起对外部方法的调用
  • export:让组件“向内”接收并转发方法调用(中继点)
  • imp:终点,拥有 imp 的组件需要提供该方法的最终实现

层级穿透的直觉:port 把调用一路“往上抛”,到顶层再对接 export,再沿 export “往下传”,直到落到 imp。

flowchart TD
    UP["Call up through ports"]
    CHILD[child] --> PORT_C[port]
    PORT_C --> PORT_P[parent port]
    PORT_P --> PORT_T[top port]
    PORT_T --> EXP_T[top export]
    DOWN["Call down through exports to imp"]
    EXP_T --> EXP_P[parent export]
    EXP_P --> IMP[imp]
    IMP --> LEAF[leaf implements method]

2.1.12 analysis_port:广播式 write(monitor→agent→订阅者)(图片重画)

  • analysis_port 的特性:0/1/多 连接;monitor/agent 不需要知道 transaction 去向
  • 只提供 write(tx):非阻塞、只读语义(不应修改 tx),订阅者被回调顺序不可控
flowchart LR
    MON[monitor] -->|write| AP0[analysis_port]
    AP0 --> AP1[agent analysis_port]
    AP1 -->|write| SUB1[subscriber1]
    AP1 -->|write| SUB2[subscriber2]
    AP1 -->|write| SUB3[subscriber3]

2.1.13 subscriber / 多路分析流 / analysis_fifo:三种常见落地方式

  • 单路接收:用 uvm_subscriber(内置 analysis_imp),实现对应的 write(tx) 做 checker/coverage
  • 多路接收:同一组件要处理多路 analysis 流时,用 uvm_analysis_imp_decl(_x) 生成多个 imp 类型与 write_x 方法,绕开 SV 不支持函数重载的问题
  • 延迟处理:不想在 write 里立即处理时,可用 uvm_tlm_analysis_fifo 缓存,再在 run_phase 用阻塞 get 拉取处理
  • analysis_port / analysis_fifo 的更细代码示例:可对照本笔记的 18.1 / 18.2
flowchart TD
    MON[monitor] -->|write| AP[analysis_port]
    AP -->|write| FIFO[uvm_tlm_analysis_fifo]
    FIFO -->|get| WORKER[run_phase loop]

3 seq / sqr / drv 通信

在UVM(Universal Verification Methodology)中,Driver和Sequencer是两个重要的组件,用于实现测试环境中的数据传输和控制。
Sequence的主要作用是生成和控制测试用例的执行流程。它可以定义一系列需要执行的事务,并指定它们的顺序、重复次数、延迟等属性。Sequence可以包含条件语句、循环语句和分支语句,以实现复杂的测试场景。
Driver是一个用于驱动输入信号到被测设计(DUT)的组件。它负责将事务(transaction)从Sequencer获取,并将其转换为适当的信号或协议操作来驱动DUT。
Sequencer是一个用于生成和管理事务的组件。它负责生成测试用例中的事务,并将其传递给Driver以驱动DUT。Driver和Sequencer之间的通信是通过UVM提供的基于事务的接口进行的。(阻塞)

flowchart LR
    TEST["test or vseq"]
    SEQ["sequence uvm_sequence"] -->|start| SQR["sequencer uvm_sequencer"]
    DRV["driver uvm_driver"] -->|connect| SQR
    DRV -->|pins| DUT[DUT]

3.1 driver 和 sequencer 的通信

在UVM中,Driver和Sequencer之间的通信机制如下:
1、Sequencer接收一个事务,并将其放入一个事务队列中。
2、Driver通过轮询或其他机制检查事务队列是否有新的事务。
3、如果Driver检测到有新的事务,它会从事务队列中取出事务,并将其转换为适当的信号或协议操作来驱动DUT。
4、Driver完成对事务的驱动后,可以通过事务接口向Sequencer发送一些响应信息,例如完成状态或错误信息。
5、Sequencer可以根据Driver的响应信息来调整发送下一个事务。

sequenceDiagram
    participant SEQ as sequence
    participant SQR as sequencer
    participant DRV as driver
    participant DUT as DUT
    SEQ->>SQR: start()
    DRV->>SQR: get_next_item(req)
    SQR-->>DRV: req
    DRV->>DUT: drive pins
    DRV->>SQR: item_done(rsp_ok)

3.2 sequence 和 sequencer 的通信

Sequence通过调用Sequencer的start()方法来启动。

body 是什么:

  • sequence 的核心入口是 virtual task body(),sequence 被 start 之后,就会执行 body(严格来说会按 pre_body -> body -> post_body 顺序执行)
  • body 里一般做三类事:发 transaction(start_item/finish_item 或 uvm_do 宏)、启动子 sequence、做寄存器模型访问/场景编排(分支/循环/约束随机)
  • body 的“通信点”本质就是调用 sequencer 的接口:start_item/finish_item 内部会走 wait_for_grant / send_request / wait_for_item_done,从而和 driver 的 get_next_item/item_done 对齐
1
2
3
4
5
6
7
8
9
10
class my_seq extends uvm_sequence#(my_trans);
`uvm_object_utils(my_seq)
virtual task body();
my_trans req;
req = my_trans::type_id::create("req");
start_item(req);
void'(req.randomize());
finish_item(req);
endtask
endclass

它是什么 phase:

  • sequence 是 uvm_object,不是 uvm_component,因此它本身没有 run_phase/main_phase/... 这种 phase 回调
  • sequence 的执行发生在“启动它的那个 component 的 task phase”里:最常见是在 test/virtual sequence 的 main_phase(也有人放 run_phase)
  • 如果把 sequence 配成某个 phase 的 default_sequence,则 UVM 会在该 phase 自动启动它,并把 starting_phase 指向那个 phase
  • 需要用 objection 控制仿真时,建议在 sequence 里用 starting_phase 起落 objection(仅当它非空时)
1
2
3
4
5
6
7
virtual task pre_body();
if (starting_phase != null) starting_phase.raise_objection(this);
endtask

virtual task post_body();
if (starting_phase != null) starting_phase.drop_objection(this);
endtask

经验 常在 case 的 main_phase 里 start 启动 sequence(drive 主要功能在 main_phase),控制 sequence 的 body 任务执行;driver 不断接收 sequence 发送的 trans(fork…join 包含两部分:接收 trans / 驱动 DUT)。待 sequence 的 body 执行完后(结束 start_item/finish_item),case 的 seq.start(sqr) 语句执行完毕。

注意 sequence / driver / sequencer 之间传递的 trans 需要保持同一类型;若不同 trans 类型,需要转换。

扩展:sequence-driver-sequencer communication approaches

sequence 的执行步骤:

  1. sequence 启动,使 m_sequencer 指向 sequencer。
  2. 使用 m_sequencer.wait_for_grant 仲裁多个 sequence(优先级)—— start_item。
  3. 使用 m_sequencer.send_request 发送 item(trans)至 REQ_FIFO,并等待 wait_for_item_done 完成 —— finish_item。

4 set 和 get 寄收信

UVM普遍采用这种方式存(set)、取(get)信息。对于config_db机制,set()方法把需要的资源放进全局可见的数据库;get()方法从全局可见的数据库输出需要的资源。(不用UVM树形成就可调用set方法)

4.1 寄信方格式(set)

原型:

1
2
3
4
5
6
uvm_config_db#(T)::set(
uvm_component cntxt,
string inst_name,
string filed_name,
T value
);

数据类型T可以是:virtual interface,int,string,enum,对象等;
第一个参数:参数类型uvm_component需要是一个实例句柄,可以是null;
第二个参数:数据类型string需要是字符串,相对此实例的路径;
第三个参数:数据类型string需要是字符串,与get的第三个参数一致;
第四个参数:传递的数据内容。

示例:

1
uvm_config_db#(邮件类型)::set(A, "B", "邮件名字", 邮件内容);

4.2 收信方格式(get)

原型:

1
2
3
4
5
6
uvm_config_db#(T)::get(
uvm_component cntxt,
string inst_name,
string filed_name,
inout T value
);

参数的意义与set方法一致。

示例:

1
uvm_config_db#(邮件类型)::get(A, "B", "邮件名字", 邮件内容);

注意 set() 和 get() 中前三个参数所指向的目标变量路径必须保持一致,才能保证 set/get 作用于同一变量。(若 A+B 组合后的路径不存在,只要 get 的第 1、2 个参数组合后与 set 的第 1、2 个参数一致,仍可传递数据内容)

4.3 直线 / 非直线 获取

  • 直线:通常 tb_top 往 drv 寄信,在 drv 收信。
  • 非直线:tb_top 往 drv 寄信,agt 也能得到寄往 drv 的信。收信时只要前两参数合并指向同一地址,且类型与信件名字一致,通讯就建立起来。

注意

  • set/get 通常只用于 component 间通信。
  • 调用 set 时第一个参数尽量使用 this;在无法得到 this 指针的情况下(如 top_tb),使用 null 或 uvm_root::get()(二者等价)。
  • 寄/收 virtual interface 时传递的是句柄:interface 内容变化,get 到该 interface 的组件也能感知;寄/收非句柄(变量值等)传递的是变量值,后续变化不会自动同步到其它组件。

5.1 UVM 基础结构

常用的派生自uvm_object的类:
uvm_sequence_item:所有的transaction都要从uvm_sequence_item派生。

uvm_sequence:所有sequence要从uvm_sequence派生。sequence就是sequence_item的组合。sequence直接与sequencer打交道,当driver向sequencer索要数据时,sequencer会检查是否有sequence要发送数据。当发现有sequence_item待发送时,会把此sequence_item交给driver。

uvm_reg_map、uvm_mem、uvm_reg_field、uvm_reg、uvm_reg_fild、uvm_reg_block等与寄存器相关的众多的类都是派生自uvm_object,它们都是用于register model。

常用的派生自uvm_component的类:
构成环境的组件都从uvm_component类继承而来,这是因为它们都从uvm_component类继承了phase机制,都会经历各个phase阶段。常见组件如下:

uvm_sequencer:所有的sequencer都要派生自uvm_sequencer。
sequencer功能:组织管理sequence,driver申请数据时,它就把sequence生成的sequence_item发给driver。

uvm_driver:所有的driver都要派生自uvm_driver。
driver功能:向sequencer申请sequence_item(数据包),并将包里的信息按照总线协议规定驱动到DUT的端口上。

uvm_monitor:所有的的monitor都要派生自uvm_monitor。
monitor功能:从DUT接收数据,并将其转成transaction级别的sequence_item,发送给scoreboard,供其比较。

uvm_agent:所有的agent要派生自uvm_agent。
agent功能:将sequencer、driver和monitor封装在一起,agent模块的使用提高了代码的可重用性。(UVM_ACTIVE/UVM_PASSIVE、两种模式)

uvm_scoreboard:一般的scoreboard都要派生自uvm_scoreboard。
scoreboard功能:比较reference model和monitor分别发送来的数据,根据比较结果判断DUT是否正确工作。

uvm_env:所有的env要派生自uvm_env。
env功能:将平台上的component组件封装在一起,实现一个环境多个用例。运行不同用例时,在其中实例化env即可。

uvm_test:所有的测试用例要派生自uvm_test或其派生类。
任何一个派生的测试用例中,都要实例化env。这样当用例运行时,才能把数据发给DUT,并接收DUT的数据。

reference model:UVM中并没有针对reference model定义一个类。通常,reference model直接派生自uvm_component。
作用就是“模仿DUT,完成与DUT相同的功能”。DUT是用Verilog写成的时序电路,而reference model则可以直接使用verilog高级语言的特性,同时还可以通过DPI等接口调用其他语言来完成与DUT相同的功能。

5.2 UVM 树的构建

uvm_object是UVM中最基本的类,几乎全部的类都是由uvm_object类派生出来,其中包含uvm_component。

uvm_component有两大特性是uvm_object所不具备的。一是通过在new的时候指定parent参数来产生一种树形的组织结构,二是有phase的自动执行特点。UVM树的结点都是由uvm_component构成的,只有基于uvm_component派生出的类才有可能是UVM树的结点。
典型的UVM树如下图:(uvm_root是唯一“树根“)

6 UVM 的打印

uvm_fatal、uvm_error和uvm_warning等宏只有两个参数,而uvm_info就是三个参数。

6.1 uvm_info 的打印等级

UVM_NONE的冗余度最低,即无论平台将UVM_VERBOSITY设置为什么级别,都会打印UVM_NONE。(uvm_warning、uvm_error和uvm_fatal默认等级为uvm_none,一定打印)
(1)UVM中,默认的冗余度阈值是UVM_MEDIUM,所有低于等于UVM_MEDIUM(如UVM_LOW和UVM_MEDIUM)的信息都会被打印出来。
(2)UVM提供set_report_verbosity_level函数来设置某个特定component的默认冗余度阈值,如env.i_agt.drv.set_report_verbosity_level(UVM_HIGH)表示为DRV组件中,低于等于UVM_HIGH冗余度阈值的都会打印。
(3)UVM提供set_report_verbosity_level_hier函数来指定某个component下所有的component的冗余度阈值阈值。如env.i_agt.set_report_verbostiry_level_hier(UVM_HIGH)表示i_agt及其下所有的component的冗余度阈值将配置为UVM_HIGH。

注意 一般在 case 里配置(需要在 connect_phase 后),也可以在组件内配置(任意位置)。另外,打印与函数位置有关:函数位置后按设置的打印等级生效;函数位置前仍按原先等级打印。

6.2 uvm 位置打印函数

uvm打印中常使用下述函数表示打印所在的位置:

  • get_type_name():返回对象 class 的名字。
  • get_name():返回创建对象时传入的字符串。
  • get_full_name():返回当前 component 的完整 UVM 树路径;object 类的返回值与 get_name() 一样(object 不是 UVM 树节点,所以路径没有层次)。

总结:上述被调用时,get_name()返回创建对象时的字符串。get_type_name()返回对象class名字。get_full_name()返回组件UVM树的完整路径。

6.3 $sprintf 与 $sformatf

$sprintf 和 $sformatf 都是 verilog 中用于格式化字符串的系统函数。它们可以使用格式说明符指定输出字符串的格式(如宽度、精度、基数等)。区别在于:

  • $sprintf:返回一个 string
  • $sformatf:不返回值(直接得到格式化结果)
1
$sprintf("info... value=%0d", value);

6.4 重载打印严重性

1
aaa.bbb.ccc.set_report_severity_override(UVM_WARNING, UVM_ERROR);

该方法可以将组件 ccc 的 UVM_WARNING 打印信息重载为 UVM_ERROR。

7 phase 执行顺序

UVM树的执行顺序有三种:自上而下、自下而上和随机序。
UVM内所有的phase如下图所示,不同phase间的执行顺序从时间上讲是从上往下执行的(所有的phase,包括function phase和task phase)

7.1 function phase 执行顺序

对于function phase来说,即不耗费仿真时间的phase(function phase),同一类phase,除了build phase自上而下执行之外,其它的function phase都是自下而上执行的。
例如:对于connect_phase即先执行driver和monitor的connect_phase,再执行agent的connect_phase。
(build_phase一般用于实例化组件,如果自下而上,那么组件还没有实例化,若调用其方法,则会报错,如in_agent)
对于同一层次的、具有兄弟关系的component,它们的phase的执行顺序是按照字典序执行的,这里字典的排序依据new时指定的名字。

7.2 task phase 执行顺序

对于同一类 run phase,其执行顺序是自下而上执行,同时在运行(并行执行)。对于同一组件的 run phase(不同 task phase),其执行顺序是自上而下执行的。

由上可知,run phase是同时执行的,待同一层的task phase执行完后,再同时执行下一层的task phase,中间可能会存在空白等待时间。如上,同时执行A和B的main_phase,待A的main_phase和B的main_phase都执行完后,再同时执行A和B的post_main_phase,待A的post_main_phase和B的post_main_phase都执行完后,在接着往下执行。

对于run_phase,是通过fork….join_none包裹起来了,虽然都是“并行”执行,但其实存在先后顺序的。也就是说,类似run_phase、main_phase等task_phase也都是按照自下而上的顺序执行的,虽然同一层此task phase是同时启动的。

9 UVM 类的继承

10 前门 / 后门访问

10.1 寄存器模型前/后门访问

寄存器模型针对模块里的寄存器,UVM提供了基于寄存器模型的两种访问方式,分为前门访问和后门访问:

  • 前门访问:通过模拟 CPU 在总线上发出读/写指令,进行真实物理总线操作。常用于验证寄存器读写是否正常。
  • 后门访问:不通过物理总线,直接层次化引用方式访问寄存器。常用于测试与调试。
特性 前门访问 后门访问
访问方式 通过总线物理信号时序实现寄存器读写 通过 UVM DPI 直接操作 DUT 内部寄存器变量
仿真时间 消耗仿真时间,等待总线返回应答后结束 瞬时生效,不消耗仿真时间
域字段操作 只能对整个位宽读写,不能单独操作域字段 可以单独对域字段读写
时序 正确反映寄存器操作的时序关系 不受硬件时序控制,可能与硬件行为产生时序冲突
预测方式 依靠监测总线对寄存器模型做预测 依靠自动预测方式做预测

另外,对于寄存器的后门访问,UVM提供了两类后门访问的函数:一是UVM_BACKDOOR形式的read和write,二是peek和poke方式。前者会在操作时模仿DUT的行为,后者完全不管DUT的行为。如对一个只读寄存器进行写操作,前者模拟DUT的只读行为,所以写不进去,后者可以写进去(poke)。

注:force属于后门访问的一种方法,不仅可以访问reg类型,也可以访问wire类型。

10.2 后门访问 DUT 内部信号(常用)

针对寄存器使用的 DPI/PLI 后门访问提供接口。

变量

  • UVM_HDL_MAX_WIDTH:设置后门访问数据位宽,可在编译选项中调整宏定义最大位宽。

方法

  • uvm_hdl_check_path:检查给定 HDL 路径是否存在
  • uvm_hdl_deposit:将给定 HDL 路径设置为指定值
  • uvm_hdl_force:强制给定路径上的值(常用)
  • uvm_hdl_force_time:强制给定路径上的值为指定数量的 force_time
  • uvm_hdl_release_and_read:释放先前 force 的值并读取
  • uvm_hdl_release:释放先前 force 的值(常用)
  • uvm_hdl_read:读取给定路径的值(常用)
1
2
3
uvm_hdl_force($sformatf(""), value);
uvm_hdl_release($sformatf(""));
uvm_hdl_read($sformatf(""), value);

注意 使用 DPI 后门访问时,string 字符串为 DUT 内信号路径,需要保持一致;调用 DPI 函数时可加入返回值判断,确认是否调用成功。

注意 uvm_hdl_force 一个信号后再 uvm_hdl_release:若信号未被驱动,值保持释放前的值;若被驱动,则变为驱动后的值。一般通过“路径”方式(而非总线方式)处理 DUT 信号的,都可广义视为后门访问。

10.3 读取数组信号(示例)

使用 uvm_hdl_read 读取 DUT 信号(如 reg[1:0] counter[3:0],一维/多维数组)时,可用以下方式(uvm_hdl_force 类似):

方法 1:逐个读取

1
2
3
4
5
bit [1:0] read_cnt[3:0];
uvm_hdl_read("xxx.xxx.xxx.counter[0]", read_cnt[0]);
uvm_hdl_read("xxx.xxx.xxx.counter[1]", read_cnt[1]);
uvm_hdl_read("xxx.xxx.xxx.counter[2]", read_cnt[2]);
uvm_hdl_read("xxx.xxx.xxx.counter[3]", read_cnt[3]);

方法 2:读取整个数组

1
2
bit [1:0] read_cnt[3:0];
uvm_hdl_read("xxx.xxx.xxx.counter", read_cnt);

11 覆盖率合并

  • 使用 urg -dir aaa.vdb -dir bbb.vdb -dbname ccc.vdb:合并 aaa/bbb 的 vdb,输出 ccc.vdb。ccc.vdb 覆盖率为 aaa+bbb 的集合(不同公司可能需要 bsub -Is 提交任务,如 )。
  • 使用 urg -full64 -f aaa.list -dbname bbb.vdb:合并 aaa.list 中列出的所有 .vdb,输出 bbb.vdb(不同公司可能需要 bsub -Is 提交任务,如 )。
  • 查看 Makefile,使用 bsub -Is make cov_merge 生成 vdb(特有)。

12 命令行传参

当需要构造一些固定参数的case时(常见为定向case),且其它参数不需要较大变动即可使用外部传参。

命令行执行

1
-run_opt +uvm_set_config_int="uvm_test_top,xxx,value"
  • uvm_test_top:传入 case 的目标实例
  • xxx:参数名
  • value:参数值
  • 多参数:继续追加 +uvm_set_config_int="uvm_test_top,xxx,value",不需要空格

回归执行

1
;+uvm_set_config_int="uvm_test_top,xxx,value";

指定运行次数

1
;+uvm_set_config_int="uvm_test_top,xxx,value";n

case 中获取命令行参数:

1
uvm_config_db#(uvm_bitstream_t)::get(this, "", "xxx", value);

13 uvm 重载

13.1 virtual 关键字

  • 在基类中用 virtual 声明成员(函数/任务)后,派生类可重写该成员;运行时会按对象实际类型调用对应实现(多态)。
  • 若基类成员未声明为 virtual,则派生类无法重写,只能继承基类实现。
  • super 关键字:派生类可在复用父类方法的同时追加逻辑。

13.2 重载方法

常用思路:用 factory 的 override,把“创建 father_class 时实际创建成 son_class”。

set_type_override_by_type(全局类型重载)

1
2
3
4
5
uvm_factory::get().set_type_override_by_type(
father_class_name::get_type(),
son_class_name::get_type(),
1
);

对 father_class 的全部实例进行重载。

set_inst_override_by_type(实例路径重载)

1
2
3
4
5
6
7
8
9
10
11
uvm_factory::get().set_inst_override_by_type(
relative_inst_path,
father_class_name::get_type(),
son_class_name::get_type()
);

uvm_factory::get().set_inst_override_by_type(
"env.fw1_req_env.credit_data_in_agt.drv",
credit_data_in_drv::get_type(),
fw1_srst_in_drv::get_type()
);

对指定实例路径进行重载。

set_type_override / set_inst_override

  • 与上面两种方法等价,只是用字符串代替 xxx::get_type()。

set_type_override_by_name / set_inst_override_by_name(按类名字符串)

1
2
3
4
5
6
7
8
9
10
11
uvm_factory::get().set_type_override_by_name(
"father_class_name",
"son_class_name",
1
);

uvm_factory::get().set_inst_override_by_name(
"env.agt.drv",
"father_class_name",
"son_class_name"
);

实例代码 1:重载 driver(type override)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class base_drv extends uvm_driver#(my_trans);
`uvm_component_utils(base_drv)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass

class fast_drv extends base_drv;
`uvm_component_utils(fast_drv)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass

class base_test extends uvm_test;
`uvm_component_utils(base_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction

function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_factory::get().set_type_override_by_type(base_drv::get_type(), fast_drv::get_type(), 1);
endfunction
endclass

实例代码 2:只重载某一个实例(inst override)

1
2
3
4
5
6
7
8
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_factory::get().set_inst_override_by_type(
"env.agt.drv",
base_drv::get_type(),
fast_drv::get_type()
);
endfunction

注意事项(容易踩坑):

  • 必须走 factory 创建:component 用 type_id::create,object 用 type_id::create;如果直接 new(),override 不会生效
  • father/son 都必须注册到 factory:component 用 uvm_component_utils,object 用 uvm_object_utils
  • override 必须发生在 create 之前:一般放在 test 的 build_phase 早期(env/agent/driver 创建之前),或放在 default_sequence 启动前
  • inst_override 的路径用“最终实例的完整层级路径”,需要和实际 get_full_name() 对得上

13.3 factory 重载要求

重点 需要在类创建之前调用重载函数(避免实例已创建导致重载不生效)。

  • 被重载类与重载类都需要注册到 factory:
    • component 使用 uvm_component_utils()
    • object 使用 uvm_object_utils()
  • 被重载类实例化需要走 factory,而不能用 new():
1
xxx_inst = xxx::type_id::create("xxx");
  • 重载类与被重载类必须有派生关系:重载类派生自被重载类(被重载类为父类)。

在 base case 的 end_of_elaboration_phase 打印当前 factory 注册与重载情况(便于确认 override 生效):

1
2
3
4
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
uvm_factory::get().print();
endfunction

14 编译选项

在 UVM 验证环境中,常见的编译选项/参数用于控制验证环境、组件以及 DUT 的行为与功能:

选项 含义 常见用途
UVM_OPT UVM 相关选项 控制 UVM 功能、配置组件行为、设置消息冗余度等
DUT_OPT DUT 相关选项 配置 DUT 参数/功能开关等
COMP_OPT 组件编译选项 配置组件功能、宏定义等
ELAB_OPT elaboration 选项 控制实例化/综合阶段的配置与优化策略等

14.1 VCS 常用编译选项(vcs)

分类 选项示例 说明
64 位 -full64 64 位编译
verilog -sverilog 启用 verilog
文件列表 -f filelist.f 从 filelist 读取 RTL/库/选项
include 路径 +incdir+./tb 追加 include 搜索路径(可多条)
宏定义 +define+UVM_NO_DEPRECATED 追加宏定义(可多条)
timescale -timescale=1ns/1ps 统一 timescale(也可在源码中 \timescale`)
顶层 -top tb_top 指定 top(多 top 场景常用)
输出 -o simv 指定生成的仿真可执行文件名
日志 -l compile.log 编译日志
增量编译 -Mupdate 只重编变化文件(回归常用)
并行 -j 8 编译并行度
调试 -debug_access+all 便于波形/交互调试(会增加编译体积)
调试数据库 -kdb 生成调试数据库(常配合 Verdi/DVE)
覆盖率 -cm line+cond+branch+tgl+fsm 编译打开覆盖率(配合 run 生成数据)
覆盖率输出 -cm_dir ./cov.vdb 覆盖率数据库目录
覆盖率命名 -cm_name regress_001 覆盖率 run 名称

常见编译命令形态:

1
vcs -full64 -sverilog -f filelist.f -top tb_top -o simv -l compile.log

14.2 VCS 常用运行选项(./simv)

分类 选项示例 说明
日志 -l sim.log 仿真日志
UVM 用例 +UVM_TESTNAME=smoke_test 选择 test
UVM 详细度 +UVM_VERBOSITY=UVM_MEDIUM 控制信息打印量
随机种子 +ntb_random_seed=123 固定随机种子便于复现
超时 +UVM_TIMEOUT=1000000 UVM 超时(时间单位依赖 timescale)
GUI -gui 打开 DVE(若环境支持)

常见运行命令形态:

1
./simv -l sim.log +UVM_TESTNAME=smoke_test +ntb_random_seed=1

14.3 波形(Verdi/FSDB)常见选项

常见做法:编译阶段加载 FSDB PLI,运行阶段指定 dump 文件。

1
2
vcs -full64 -sverilog -f filelist.f -debug_access+all -kdb -o simv
./simv -l sim.log +fsdbfile+wave.fsdb

14.4 后仿(门级/后布局)常见选项(GLS / SDF)

后仿常见输入:gate_netlist.v(门级网表)、stdcell_lib.v(库模型)、可选 dut.sdf(时延回标)。

编译侧常见要点:

  • 把门级网表与标准单元库模型加入编译(通常通过 -f 或 -v/-y/+libext 管理)
  • 按需要选择 delay 模式:+maxdelays / +typdelays / +mindelays
  • 纯功能门仿(不关心时序)时常用:+notimingcheck

运行侧常见要点:

  • 用 -sdf 回标 SDF 到指定实例
  • 结合 delay 模式与日志,快速定位回标/时序检查报错
1
2
vcs -full64 -sverilog -f gls_filelist.f -top tb_top -o simv_gls -l gls_compile.log
./simv_gls -l gls_sim.log +maxdelays -sdf typ:tb_top.dut:./dut.sdf

15 UVM 仿真停止

在UVM中,若在end_of_elaboration_phase及其前的phase中,如果出现一个或多个UVM_ERROR,那么UVM会认为出现了致命错误,会调用uvm_fatal结束仿真。

16 seq 仲裁机制

若同一sequencer上同时启动多个sequence,在默认情况下sequencer的仲裁算法是SEQ_ARB_FIFO。

  • SEQ_ARB_FIFO:严格 FIFO 仲裁
  • SEQ_ARB_STRICT_FIFO:按优先级仲裁;同优先级按 FIFO
  • SEQ_ARB_STRICT_RANDOM:按优先级仲裁;同优先级随机选择

使用步骤:

  • 设置 sequencer 仲裁算法
  • 设置 sequence 优先级
1
2
3
env.i_agt.sqr.set_arbitration(SEQ_ARB_STRICT_FIFO);
seq1.start(env.i_agt.sqr, null, 100);
seq2.start(env.i_agt.sqr, null, 200);

若seq2和seq1同时启动,且同时挂载在同一sqr上,则seq1先启动。

17 virtual sequence

virtual sequence (vseq) 本质还是 sequence, 但它通常不直接发 transaction, 而是统一编排多个 agent 上的多个 sequence, 用来描述跨接口的场景流程.

典型使用场景:

  • 多端口联动: 同一用例同时驱动多个接口或多个方向
  • 多 agent 协同: 同时启动多个 seq, 控制并发与先后关系
  • 性能流量: 多流按比例发包, 支持突发与背压注入, 统一收敛结束条件

核心概念:

  • virtual sequencer (vsqr): 只保存各 agent 的 sequencer 句柄, 自身不驱动 DUT
  • virtual sequence (vseq): 运行在 vsqr 上, 在 body 里用 p_sequencer.xxx_sqr 取到真实 sequencer, 然后 start 子 sequence
  • 真实 transaction 的路径: vseq -> 子 sequence -> 真实 sequencer -> driver -> DUT

结构示意:

flowchart TD
    T[test] -->|start| V[vseq]
    V -->|runs_on| VS[vsqr]
    VS --> SA[sqr_a]
    VS --> SB[sqr_b]
    SA --> DA[drv_a]
    SB --> DB[drv_b]
    DA --> IA[dut_if_a]
    DB --> IB[dut_if_b]

使用步骤 (建议工程做法):

    1. env 里创建 vsqr (component), connect_phase 把各 agent 的 sequencer 句柄挂到 vsqr
    1. 写 vseq (object), 用 uvm_declare_p_sequencer(vsqr_type) 得到类型正确的 p_sequencer
    1. vseq.body() 里创建并启动子 sequence (串行 start 或 fork 并发)
    1. test 的 main_phase (或 run_phase) 里 vseq.start(env.vsqr) 启动整个场景

示例: vsqr 声明句柄, env 负责连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class my_vsqr extends uvm_sequencer#(uvm_sequence_item);
`uvm_component_utils(my_vsqr)
my_sqr sqr_a;
my_sqr sqr_b;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass

class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent agt_a;
my_agent agt_b;
my_vsqr vsqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agt_a = my_agent::type_id::create("agt_a", this);
agt_b = my_agent::type_id::create("agt_b", this);
vsqr = my_vsqr::type_id::create("vsqr", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
vsqr.sqr_a = agt_a.sqr;
vsqr.sqr_b = agt_b.sqr;
endfunction
endclass

示例: vseq 在 body 里并发启动子 sequence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class my_vseq extends uvm_sequence#(uvm_sequence_item);
`uvm_object_utils(my_vseq)
`uvm_declare_p_sequencer(my_vsqr)
function new(string name = "my_vseq");
super.new(name);
endfunction

virtual task pre_body();
if (starting_phase != null) starting_phase.raise_objection(this);
endtask

virtual task post_body();
if (starting_phase != null) starting_phase.drop_objection(this);
endtask

virtual task body();
my_seq_a seq_a;
my_seq_b seq_b;
seq_a = my_seq_a::type_id::create("seq_a");
seq_b = my_seq_b::type_id::create("seq_b");
fork
seq_a.start(p_sequencer.sqr_a);
seq_b.start(p_sequencer.sqr_b);
join
endtask
endclass

时序示意 (vseq 并发启动两个子 sequence):

sequenceDiagram
    participant T as test
    participant V as vseq
    participant A as seq_a
    participant B as seq_b
    participant SA as sqr_a
    participant SB as sqr_b
    T->>V: start(vsqr)
    V->>A: start(sqr_a)
    A->>SA: start_item and finish_item
    V->>B: start(sqr_b)
    B->>SB: start_item and finish_item

注意事项:

  • vsqr 里放的是句柄, 不是新创建的 sequencer; 真实 sequencer 仍在 agent 内 create
  • vseq 与子 sequence 都建议用 type_id::create, 便于 factory override
  • 如果用 default_sequence 自动启动, 用 starting_phase 管理 objection

18 TLM 通信

18.1 analysis 端口通信

analysis_port(analysis_export)与 imp 之间的通信是一对多的通信,且对于 analysis_port 和 analysis_export 来说,没有阻塞/非阻塞的区别(只有一种操作:write)。analysis_imp 所在的 component 必须定义名为 write 的函数;当 analysis_port/export 执行 write 时,对应的 write 函数会自动调用。

大致步骤:(以rm->scb为例)

1
2
3
4
5
6
7
8
// rm 组件
uvm_analysis_port#(trans) a_ap;

// scb 组件
uvm_analysis_imp#(trans, class) b_imp;

// env 组件
rm.a_ap.connect(scb.b_imp);

拓展:考虑到有多路数据接入scb,而write函数只有一个,因此,UVM定义的宏uvm_analysis_imp_decl(a)可用来定义新的uvm_analysis_imp类型,同时会存在write_a函数,其会自动执行。如使用uvm_analysis_imp_decl(_rm_chk),则新定义一个数据类型uvm_analysis_imp_rm_chk,其本质还是uvm_analysis_imp类型,同时有数据发送时,会自动调用write_rm_chk函数。

(2)其它 put/get 通信:略

18.2 FIFO 通信

在 agent 和 scoreboard 之间添加 uvm_analysis_fifo。FIFO 本质是“缓存 + 两个 imp”:

  • monitor -> FIFO:monitor 仍是 analysis_port,FIFO 为 uvm_analysis_imp,数据流/控制流方向相同
  • scoreboard <-> FIFO:scoreboard 使用 blocking_get_port,FIFO 使用 get_export,控制流从 scoreboard 到 FIFO,数据流从 FIFO 到 scoreboard

大致步骤是:(以mon采集DUT,与scb通信为例,其余类似)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// agent 方向
uvm_analysis_port#(trans) ap;
ap = new("ap", this);

// scoreboard 方向
uvm_blocking_get_port#(trans) act_port;
act_port = new("act_port", this);

// env 方向
uvm_tlm_analysis_fifo #(trans) agt_scb_fifo;
agt_scb_fifo = new("agt_scb_fifo", this);

// connect_phase 连接
o_agt.ap.connect(agt_scb_fifo.analysis_export);
scb.act_port.connect(agt_scb_fifo.blocking_get_export);

同理的话,rm和i_agt之间的通信,rm和scb之间的通信都是如此。

注意 除了 TLM FIFO 需要例化,两边的通信端口也需要例化。接收端使用 get 获取 trans 会阻塞,但不影响主进程。

特别重要 使用 fork…join_none 进行 get 采样时,同时刻采样多个 trans 可能乱序,尽量避免用 fork…join_none 采样。

tlm_analysis_fifo.flush()可以清空tlm fifo的内部存储数据。

18.3 TLM1 端口选型与连接规则速查(摘记)

参考原文:向UVM-TLM通信发起决战

TLM 在 UVM 里更像“事务血液循环”:没有端口/连接,transaction 很难在 driver/monitor/scoreboard/reference model 等组件间流动,导致环境搭不起来、代码也难读。

18.3.1 TLM1.0 vs TLM2.0(UVM1.2 视角)

  • UVM 的 uvm_pkg.sv 中同时包含 TLM1 和 TLM2
  • TLM2 不是在 TLM1 上“完善/优化”的同一套类,而是一套新的接口与方法
  • TLM1 常用于功能验证:put/get/peek 等基础传输方法,对时序支持有限
  • TLM2 更强调时序建模:通过 nb_transport_fw/nb_transport_bw 等接口支持更复杂、更精确的建模

18.3.2 端口家族与“能不能连”

  • 端口必须例化在 component 内,端口本身不能脱离 component 单独存在
  • 端口分两大类:带 analysis 与不带 analysis
    • 带 analysis:analysis_port / analysis_export / analysis_imp,一对多;仅有 write() 方法
    • 不带 analysis:port / export / imp,点对点
  • 两类端口不互通:analysis 端口只能连 analysis 端口;非 analysis 端口只能连非 analysis 端口

18.3.3 port / export / imp 的角色(更偏“控制流”)

  • port 端口所在组件通常是主动方(master),发起 put/get/peek 等操作
  • imp 端口所在组件通常是被动方(slave),最终落到该组件内部方法实现
  • export 更多承担“转发/桥接”的角色
flowchart TD
    A[master component] --> P["uvm_*_port"]
    P -->|connect| E["uvm_*_export"]
    E -->|connect| I["uvm_*_imp"]
    I --> B["slave component 实现"]

连接规则要点:

  • 连接的发起方必须是更高“地位”的端口:port > export > imp
  • connect 调用要有始有终:开始一定是 port 发起,结束一定落到 imp

18.3.4 阻塞/非阻塞方法

分类 方法 说明
阻塞(task) put/get/peek 阻塞当前进程,直到发送/获取成功
非阻塞(function) try_put/try_get/try_peek 尝试操作,成功返回 1,失败返回 0
非阻塞(function) can_put/can_get/can_peek 检查对端是否具备条件
analysis 专属 write 一对多广播式写入

18.3.5 以 put 为例的调用链

sequenceDiagram
    participant M as master component
    participant P as port
    participant E as export
    participant I as imp
    participant S as slave component
    M->>P: put(t)
    P->>E: put(t)
    E->>I: put(t)
    I->>S: put(t) (最终实现)

18.3.6 端口类名的选型直觉(以 put 为例)

  • uvm_blocking_put_port:只包含 put()
  • uvm_nonblocking_put_port:只包含 try_put/can_put
  • uvm_put_port:同时包含阻塞与非阻塞 put 相关方法

18.3.7 connect 组合速查(谁能 connect 谁)

  • 非 analysis(点对点):
    • port.connect(port/export/imp)
    • export.connect(export/imp)
    • imp 不能发起 connect
  • analysis(一对多):
    • analysis_port.connect(analysis_export/analysis_imp)
    • analysis_export.connect(analysis_export/analysis_imp)
    • analysis_imp 不能发起 connect

18.3.8 port/export/imp:控制流与数据流是两回事

  • port/export/imp 更偏“控制流”分层(谁发起方法调用,谁承接/转发调用)
  • put 操作的数据流:port → imp(像 master 写)
  • get 操作的数据流:imp → port(像 master 读)
  • analysis 的 write:广播式 push,接收方需要立即处理并返回
flowchart LR
    PUT["put:数据 port→imp"]
    P1[port] -->|data: tx| I1[imp]
    GET["get:数据 imp→port"]
    I2[imp] -->|data: tx| P2[port]

18.3.9 源码视角:端口类的“基类骨架”(简化)

  • uvm_tlm_if_base#(T1,T2):TLM1 常用 put/get/peek/try_*/can_*/write 等方法的接口基类(virtual class)
  • uvm_sqr_if_base#(REQ,RSP):sequence/sequencer/driver 专用接口(get_next_item/item_done 等)
  • uvm_port_base#(IF):几乎所有 port/export/imp 的核心基类(connect 的核心也在这里)
  • uvm_*port / uvmexport / uvm_imp:同前缀三件套,封装的方法集合不同(blocking/nonblocking/combined)

端口三件套前缀需要匹配,否则 connect 会失败,例如:

  • uvm_nonblocking_get_port 只能连 uvm_nonblocking_get_export/imp

18.3.10 FIFO:端口 vs 组件

  • 端口(port/export/imp/analysis_*)是“组件的门”,必须例化在 component 里,本身不是 component
  • FIFO(uvm_tlm_fifo / uvm_tlm_analysis_fifo)是 component,可作为跨线程/解耦的缓冲区
  • uvm_tlm_analysis_fifo:在 FIFO 基础上增加 analysis_export,适配 analysis 链路(monitor→fifo→后处理)
  • 典型连线代码示例:可对照本笔记的 18.2
flowchart TD
    A[component A] -->|put| FIFO[uvm_tlm_fifo]
    FIFO -->|get| B[component B]

18.3.11 sequence 机制的特殊端口:seq_item_pull_*

  • driver/sequencer 之间不是 uvm_tlm_if_base,而是基于 uvm_sqr_if_base 的专用接口
  • 典型连接:drv.seq_item_port.connect(sqr.seq_item_export)
  • 典型调用链:drv.get_next_item → sqr.get_next_item → 等待 sequence 发 item;drv.item_done → sqr.item_done
  • 握手时序图:可对照本笔记的 2.1.6

18.3.12 端口选型的实用规则(够用版)

  • 先决定通信语义:push(put) / pull(get) / 广播(write)
  • 再决定是否阻塞:blocking_* / nonblocking_* / 同时支持(无前缀或 combined)
  • 最后按“方法集合”选类:组件只需要哪些方法,就选只暴露这些方法的端口类

19 phase 跳转

在 function phase 和 task phase 中,使用 phase.jump(uvm_reset_phase::get()) 跳转至 reset_phase,注意:

  • 不能跳转至 run_phase
  • 只能跳转 uvm_pre_reset_phase 之后的所有 phase
  • 可以向当前 phase 之前跳转,也可以向之后跳转

注:常用于二次复位测试

扩展:
sequencer的stop_sequences()方法,作用是告知sequencer终止当前在sequencer上运行的所有sequence和子sequence,并删除当前排队的所有请求、锁定和响应。这实际上是将sequencer复位到空闲状态。
但是,stop_sequences()并不检查driver是否正在驱动任何sequence_items,因此可能会报get_next_item()和item_done()未成对的fatal,所以要保证先调用get_next_item()获取待处理的事务,再调用item_done()来标记事务的处理完成。
除此之外,在 main_phase 中使用 phase.jump() 后,若后面不跟 clock,则会直接执行到 drop_objection;若跟 clock,则不会执行到 drop_objection,直接跳转。

20 callback 回调

以 drive 组件为例:

20.1 注册与放置回调

1
2
3
4
class drive extends uvm_driver#(trans);
`uvm_register_cb(drive, drive_callback)
`uvm_do_callbacks(drive, drive_callback, fun(tr) / task(tr))
endclass

20.2 定义回调基类

1
2
3
4
class drive_callback extends uvm_callback;
virtual function void fun(ref trans tr); endfunction
virtual task task_fn(ref trans tr); endtask
endclass

20.3 扩展回调类

1
2
3
4
class my_drive_callback extends drive_callback;
virtual function void fun(ref trans tr); endfunction
virtual task task_fn(ref trans tr); endtask
endclass

20.4 在 test 中例化并注册

1
2
3
4
5
6
7
class test extends base_test;
function void connect_phase(uvm_phase phase);
my_drive_callback my_drv_callback;
my_drv_callback = my_drive_callback::type_id::create("my_drv_callback", this);
uvm_callbacks#(drive, drive_callback)::add(env.i_agt.drv, my_drv_callback);
endfunction
endclass

callback 回调步骤:

  • 将 UVM Callback 方法内嵌入组件中
  • 创建最基本的 UVM Callback 类以供扩展
  • 从上一步的类中扩展出所需的 callback 类
  • 在顶层实例化并注册 Callback 对象

21 uvm_report_server

在UVM中,uvm_report_server是一个用于处理和管理报告消息的组件,其主要作用是收集来自测试环境中的各个组件(如测试用例、测试类、环境等)发出的报告消息。
一般使用方法:

  • 在 report_phase 中声明 uvm_report_server server;
  • server = uvm_report_server::get_server() 或 server = get_report_server() 获取 server
  • 使用 server.get_severity_count(...) 获取统计,再汇总报告
1
2
3
4
5
6
7
uvm_report_server server;
int error, fatal, warning;

server = uvm_report_server::get_server();
error = server.get_severity_count(UVM_ERROR);
fatal = server.get_severity_count(UVM_FATAL);
warning = server.get_severity_count(UVM_WARNING);

22 print_topology

在verilog中,print_topology通常是指在UVM测试环境中使用一种方法或命令,用于打印出UVM测试环境中各个组件(components)之间的层次结构或拓扑结构。如:uvm_top.print_topology可以打印出UVM测试环境中各个组件之间的连接关系。

1
uvm_top.print_topology();

23 sequence 相关宏及其实现

23.1 常用用法

1
2
3
4
5
6
7
8
9
seq = new();
seq.randomize() with { };
seq.start(sqr);

transaction trans;
trans = new();
trans.randomize();
start_item(trans);
finish_item(trans);

23.2 `uvm_do 系列(默认 sqr 为 m_sequencer)

此系列宏实际上集成度较高,宏定义并没有给出seq/tr在哪个sequencer上启动,因此该系列宏都是从默认的m_sequencer上启动的,也就是通过seq.start函数传递的sqr指针。

宏 参数说明
`uvm_do(tr_or_seq) 1 个参数:transaction 或 sequence
`uvm_do_with(tr_or_seq, {cons}) 2 个参数:tr/seq + 约束
`uvm_do_pri(tr_or_seq, priority) 2 个参数:tr/seq + 优先级
`uvm_do_pri_with(tr_or_seq, pri, {cons}) 3 个参数:tr/seq + 优先级 + 约束

23.3 `uvm_do_on 系列(可指定 sqr)

uvm_do_on系列宏可以指定sequence或者transaction在哪个sqr上启动,特别是如果系统中包含多个sequencer,顶层通过virtual sequencer控制,可以通过在sequence中使用uvm_declare_p_sequencer(my_sqr)来指定sequence的启动位置。

宏 参数说明
`uvm_do_on(tr_or_seq, sqr) 2 个参数:第二个为 sqr 指针
`uvm_do_on_with(tr_or_seq, sqr, {cons}) 3 个参数:sqr + 约束
`uvm_do_on_pri(tr_or_seq, sqr, pri) 3 个参数:第三个为优先级
`uvm_do_on_pri_with(tr_or_seq, sqr, pri, {cons}) 4 个参数:优先级 + 约束

无论是uvm_do系列宏还是uvm_do_on系列宏,底层都是使用`uvm_do_on_pri_with这个参数最全的宏定义,不一样的宏分别对这个宏做了参数固化。上述中如果sequence是参数,那么不需要将sequence例化了。

23.4 uvm_create 与 uvm_send

使用uvm_create宏生成trans,再使用uvm_send宏将trans发送出去。

注意 使用内嵌约束 randomize() with { CONSTRAINT } 时,约束体里变量名的查找顺序默认从被随机化对象开始,逐级向上查找同名变量。如果 randomize() 的局部域中也存在同名变量,需要使用 local:: 显式声明变量来源。

另外使用class.randomize(a)with{a==valuex;};时,指明对a进行随机化,因此class中的b、c…等等的会保持初始的值。未指明则是对class所有随机变量进行随机,指明了则对指明的随机变量进行随机。

重点 当 body 执行完成时,该 sequence 即完成使命,任务结束。

若sequence宏的启动方式,参数是trans,则会自动调用start_item和finish_item,若是sequence则会调用start任务,启动sequence。

24 p_sequencer 与 m_sequencer

  • m_sequencer(uvm_sequencer_base 类型)定义在 uvm_sequence_item 类中,每个 uvm_sequence 默认有一个 m_sequencer。uvm_sequence.start(uvm_sequencer) 后,m_sequencer 指向该 uvm_sequencer(父类指向子类)。

  • uvm_declare_p_sequencer(SEQUENCER):本质是声明一个 SEQUENCER 类型的成员变量。

uvm_declare_p_sequencer(my_sequencer) 的源码(节选):

1
2
3
4
5
6
7
`define uvm_declare_p_sequencer(SEQUENCER) \
SEQUENCER p_sequencer; \
virtual function void m_set_p_sequencer();\
super.m_set_p_sequencer(); \
if( !$cast(p_sequencer, m_sequencer)) \ //子---->父
`uvm_fatal("DCLPSQ", "xxxxxxx")\
endfunction

之后会自动将my_sequencer通过cast转换成p_sequencer。这个过程在pre_body()之前就完成了,这样就可以直接使用my_sequencer内的成员变量了。

细节:
(1)m_sequencer是sequence的成员变量,属于uvm_sequencer_base类型,sequence启动后,m_sequencer指向uvm_sequencer(父指向子),由此搭建了sequence和sequencer的桥梁,关于m_sequencer的使用就由uvm自动完成。

(2)对于复杂模型,多个sequence启动时,为方便统一管理,使用virtual_sequencer管理多个sequencer,virtual_sequence包含多个sequence,此时virtual_sequence启动,m_sequencer指向virtual_sequencer,此时使用`uvm_declare_p_sequencer(virtual_sequencer),则相当于$cast(p_sequencer,m_sequencer)向下类型转换,p_sequencer指向virtual_sequencer,则virtual_sequence里面的各sequencex可以start(p_sequencer.sqrx)来启动,由此完成sequence的统一管理,简化代码量。

9.11总结:
(1)sequence继承uvm_sequence类,里面定义了m_sequencer(属于uvm_sequcner_base类),sequence启动后,m_sequencer则指向了sequencer(属于uvm_sequencer),此处是父类指向子类,由此实现sequence和sequencer的联系。
(2)p_sequencer(属于uvm_sequencer)的目的是为实现sequence读取sequencer中的变量,使用父类指向子类的向上转换,$cast的向下转换。常用在virtual_sequencer和virtual_sequence的使用,因为virtual_sequencer中有sqr1,sqr2,而virtual_sequence中seq1,seq2启动后,想得到sqr1和sqr2,则需要先m_sequencer指向virtual_sequencer,然后再p_sequencer指向m_sequencer,这样p_sequencer指向virtual_sequencer,则再virtual_sequence中,可以通过p_sequencer得到sqr1,sqr2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class a;
int val = 13;
endclass

class b extends a;
int val = 10;
endclass

initial begin
b b1;
b b2;
a a1;

b1 = new();
b1.val = 44;
a1 = b1;
$cast(b2, a1);
$display("val:%0d", b2.val);
end

25 最大 UVM_ERROR 个数停止

当uvm_fatal出现时,会自动结束仿真。同时,UVM也支持UVM_ERROR达到一定数量时结束仿真,即set_report_max_quit_count函数,调用方式:

1
set_report_max_quit_count(n);

也支持在命令行加入:UVM_MAX_QUIT_COUNT=n。

26 uvm_re_match / uvm_glob_to_re

函数原型:

1
2
function int uvm_re_match(string re, string str);
function string uvm_glob_to_re(string glob);

使用:

  • uvm_glob_to_re:输入想匹配的字符串,返回正则表达式
  • uvm_re_match:输入正则 re 与目标 str,匹配成功返回 0,否则返回 1

其它

(1)component组件的new函数有两个参数、objection的new函数有一个参数;
对于component组件,在type_id::create例化时,参数会加this;而非component,在使用type_id::create例化时,不用加this。

(2)UVM环境的启动顺序是:(1)tb_top全局的run_test;(2)根据UVM_TESTNAME产生my_case0的实例,(3)依次执行build_phase,形成完整的UVM树;(4)顺序执行UVM树各结点的connect_phase、main_phase等;(5)所有的phase执行完毕,结束仿真。

(4)冒烟测试就是DUT版本最基本的功能测试,如果通过测试,才会进入下一步的测试(功能测试,集成测试,系统测试)。

(6)build_phase函数用于组件例化,比如调用了其它类,然后new或者type_id例化,还有收信get也在build_phase中;而connect_phase内部是用来组件与组件之间的通信的,比如driver和sequencer的相互连接,比如env内connect的TLM通信(ap,export,port等等)。

(7)build_phase的执行顺序是自上而下,connect_phase的执行顺序是自下而上。

(8)driver负责驱动transaction,不负责产生,因此要做成一个无限循环while(1)的形式。这与monitor、reference model和scoreboard的情况非常类似。因为都在main_phase中做循环,因此在raise_objection时提起,在drop_objection撤销,则仿真结束。

(9)在interface中,里面的各个变量如果没有被赋值的话,那么就会默认是X态。(logic为四值状态,默认初态为X态)

(10)环境中模拟FIFO等存储模块时,需要在环境中,同RTL的入口wr和数据对齐,另外出口rd与数据也要对齐,这样在环境中队列深度才可能对齐DUT。(时序模型)

(11)经验理解:在搭建UVM环境时,如DRV,RM,MON,SCB等时,可以在reset_phase将组件内用到的变量初始化,这样可以在以后使用jump来跳转phase。
同时尽量使用全局变量,尽量不要使用静态变量static,可以方便复位。

(12)全局变量a可以作用在全局,如果一个task或function没有定义a,那么就使用全局变量a,如果task或function有定义a,那么就使用自己内部定义的a操作。

(13)scoreboard用于检查测试结果,一般并不需要在内部使用时钟,所有不需要加@(posedge); 这是因为scoreboard的输入和输出与被测模块的输入和输出相连接,在测试过程中,输入信号的变化会自动触发scoreboard的操作并计算结果。

(14)在环境中每个时钟使用interface赋值给interface,会出现类似于“毛刺”波形(从verdi波形,0变为0,但是0==0),若右侧有至少一个变量,那么就不会出现这种情况。

(15)logic类型是一种四状态值的变量:X(未知)、Z(高阻)、0(逻辑低)和1(逻辑高)。另外,还有integer,reg和wire都是四值逻辑。

(16)bit、byte、shortint、int和longint为双状态的数据类型,只有0(逻辑低)和1(逻辑高)两种状态。注意:双状态变量被赋值x或z时,会被默认赋为0。

(17)objection机制:raise_objection()必须要在第一个消耗仿真时间之前。
drop_objection()必须要在最后一个消耗仿真时间之后。
注意:不消耗仿真时间的语句可以随机放置,其都会执行。消耗仿真时间的语句若随意放置,可能不会执行且报错。如在raise_objection语句前或在drop_objection语句后执行耗时语句,则此不会执行。

(18)仿真时间单位timescale 定义是:时间单位/时间精度(timescale time_unit/time_precision)。例如延迟的时间#a,那么需要延时a*time_unit,同时需要四舍五入,与time_precision有关,为其整数倍。
注意:时间精度小于时间单位。

(19)UVM环境中,想当拍拿到DUT的信号,方法1:使用assign后门访问DUT信号,再#1可以拿到当拍数据;方法2:另外也可#1后,使用uvm_hdl_read的方法后门访问DUT信号。(此方法不行,还是需要#来延时)

(20)验证环境需要在解复位之前,将接口变量初始化(可用SV的系统函数deposit初始化变量,可在reset_phase将接口初始化),否则可能会出现X态的传播(DUT内部X保存着)。

(21)task phase中,可以多次提起/撤销objection,每个raise_objection->drop_objection都可执行一次。
注意:当phase内的所有raise_objection->drop_objection都执行完成后,则此phase结束。

(24)使用TLM通信时,每次通过write的方式发送trans(句柄)给RM或者SCB时,都需要new()来新开辟trans空间,否则write过去的多个句柄都指向同一片空间,内容是一样的,都是最后一次修改值。如下例子:

上述表示一个句柄存入多个队列或多个句柄指向同一空间,若空间的内容发生变化,则所有指向此片空间的句柄,其取的空间内容也会发生变化。
(25)激励和驱动(seq和drv)的构造,需要考虑(四大天王):
1、数据包个数;
2、背靠背的情况;
3、数据的内部字段填充;
4、响应DUT的反压;
(26)phase的new函数,不能使用virtual进行修饰。
寄存器模型
1、寄存器模型介绍
在UVM平台中,为了便于寄存器的操作和验证,我们通过寄存器模型对DUT中具有地址映射的寄存器和存储器进行建模,从而得到寄存器模型。
(1)寄存器模型可来源于寄存器描述文件,该文件由系统工程师编写并维护,设计、验证和软件都使用相同的描述文件,确保对寄存器理解的一致性。如REGDB
(2)寄存器模型可通过脚本工具自动化生成,确保寄存器模型正确性,并提高升级维护的效率。如寄存器模型更新
(3)寄存器模型集成在验证环境中,它与DUT中的寄存器模块中包含的寄存器信息完全一致,形成一个抽象的层次化的寄存器列表,它所包含的地址、域、属性等信息都与DUT中的寄存器完全相同。
(4)在验证环境中,寄存器模型封装了寄存器的访问,将直接总线访问寄存器抽象为寄存器模型的访问,对于寄存器地址更改、域的添加、属性变更等寄存器后期的变化,都不会对已有激励产生影响,大大提高了高层测试激励的复用性。
(5)寄存器模型提供了前门总线访问模式和后门访问模式,并内嵌多个sequence实现对寄存器的扫描。

2、寄存器模型的构成
(1)uvm_reg_field为寄存器模型中的最小单位,用寄存器的功能域来构建对应的bit位。
(2)uvm_reg与单个register对应,其可以例化多个uvm_reg_field对象。
(3)uvm_reg_map用来存储寄存器列表的各个寄存器的偏移地址。当寄存器模型使用前门访问方式来实现读或写操作时,uvm_reg_map就会将地址转换成绝对地址,启动读或写的sequence,并将读或写的结果返回。
(4)uvm_mem用来匹配硬件(DUT)存储模型。
(5)uvm_reg_block可容纳多个uvm_reg、uvm_mem和uvm_reg_map类,而且可以嵌套其它的uvm_reg_block。其中的uvm_reg_map类的对象default_map用来进行地址映射,以及用来完成寄存器的前后门访问。

上述类均继承自uvm_object类,将uvm_reg_field、uvm_reg、uvm_mem放在uvm_reg_block中,就组成了完整的register model。

如的plane_bitmap寄存器的uvm_reg如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ral_reg_OXU_PLANE_BITMAP extends uvm_reg;
rand uvm_reg_field UC;
rand uvm_reg_field MC;
rand uvm_reg_field TDM;

function new(string name = "OXU_PLANE_BITMAP");
super.new(name, 32,build_coverage(UVM_NO_COVERAGE));
endfunction: new

virtual function void build();
this.UC = uvm_reg_field::type_id::create("UC",,get_full_name());
this.UC.configure(this, 8, 0, "RW", 0, 8'h00, 1, 1, 1);
this.MC = uvm_reg_field::type_id::create("MC",,get_full_name());
this.MC.configure(this, 8, 8, "RW", 0, 8'h00, 1, 1, 1);
this.TDM = uvm_reg_field::type_id::create("TDM",,get_full_name());
this.TDM.configure(this, 8, 16, "RW", 0, 8'h00, 1, 1, 1);
endfunction: build

`uvm_object_utils(ral_reg_OXU_PLANE_BITMAP)
endclass : ral_reg_OXU_PLANE_BITMAP

其中,uvm_reg_field是一个类,其里面的方法configure中的参数的意义分别是:
eg:寄存器plane_bitmap

1
2
3
4
5
6
this.UC = uvm_reg_field::type_id::create("UC",,get_full_name());
this.UC.configure(this, 8, 0, "RW", 0, 8'h00, 1, 1, 1);
this.MC = uvm_reg_field::type_id::create("MC",,get_full_name());
this.MC.configure(this, 8, 8, "RW", 0, 8'h00, 1, 1, 1);
this.TDM = uvm_reg_field::type_id::create("TDM",,get_full_name());
this.TDM.configure(this, 8, 16, "RW", 0, 8'h00, 1, 1, 1);

1、第一个参数为此域的父类,即此域位于哪个寄存器中。
2、第二个参数是此域的宽度,上述每个域段各占8bit。
3、第三个参数是此域的最低位在整个寄存器中的位置。
4、第四个参数是此域的读与写的属性。
5、第五个参数是否是易失的(volatile),这个参数一般不会使用。
6、第六个参数是此域上电复位后的默认值。
7、第七个参数是此域是否有复位值,一般寄存器上电后都有复位值。
8、第八个参数是此域是否可以随机化,主要用于对寄存器进行随机写测试。
9、第九个参数是此域是否可以单独读写。

中uvm_reg_block中build函数中对uvm_reg寄存器plane_bitmap的例化和映射部分内容如下:(需要对uvm_reg_block下的所有uvm_reg寄存器都执行一下操作)

1
2
3
4
5
6
7
8
this.default_map = create_map("", 0, 4, UVM_LITTLE_ENDIAN, 0);
this.PLANE_BITMAP = ral_reg_OXU_PLANE_BITMAP::type_id::create("PLANE_BITMAP",,get_full_name());
this.PLANE_BITMAP.configure(this, null, "");
this.PLANE_BITMAP.build();
this.PLANE_BITMAP.add_hdl_path('{
'{"plane_bitmap", -1, -1}
});
this.default_map.add_reg(this.PLANE_BITMAP, `UVM_REG_ADDR_WIDTH'h11E, "RW", 0);

其中,调用create_map函数可以返回一个uvm_reg_map类,即default_map。uvm_reg_map作用是存储所有寄存器的地址,因此通过add_reg将实例化的PLANE_BITMAP加入到map映射中。

3、寄存器模型集成
验证环境中(ENV环境中)集成寄存器模型需要定义两个成员变量,第一个是寄存器模型reg_model类,第二个是转换器adapter类(内部有:reg2hbus和hbus2reg),之后步骤如下:
(1)声明与例化reg_model后,调用寄存器模型reg_model的configure函数。
(2)调用reg_model的build函数,将uvm_reg_block的所有uvm_reg、uvm_reg_field和uvm_reg_map进行例化,并将所有寄存器地址存储进uvm_reg_map。
(3)调用reg_model的lock_model函数,表示不再添加新的寄存器。
(4)调用reg_model的reset函数,将寄存器的初值转为设置的复位值,否则reg_model中所有寄存器初值为0.
(5)reg_model的前门访问操作由uvm_reg_map完成,因此在connect_phase中,需要将转换器adapter和hbus_sequencer通过set_sequencer函数告知reg_model的default_map,并将default_map设置为自动预测状态。

uvm_reg_block调用完后,需要配置后门访问路径 set_hdl_paht_root()。

常见使用:
在verilog中,寄存器模型提供了write_reg_by_name方法来按照寄存器名称动态地更新某一个寄存器的值。方法如下:reg_model.write_reg_by_name(reg_name,value);
同理read_reg_by_name可以按照寄存器名称动态读寄存器的值。

通过regmodel对hostreg进行读写时,若regdb和hostreg对不上,可能通过regmodel进行读写时,地址对应上不确定的reg,从而导致读写不期望的reg。(regmodel会给hbus_if地址、数据、有效信号等,然后对hostreg进行读写,由于regdb与hostreg对不上,所以地址与期望不符,所以会出现读写错误)

二、System Verilog

1 队列

1.1 常用操作

  • 队列声明使用带 $ 的下标:[$]。队列元素编号从 0 到 $。
  • 在范围表达式中:$ 在左侧表示最小值,$ 在右侧表示最大值。
  • q.push_back(m):在末尾插入 m,长度 +1。
  • q.pop_front():取出第 0 个元素,长度 -1。
  • q.push_front(m):在开头插入 m,长度 +1。
  • q.pop_back():取出末尾元素,长度 -1。
  • q.insert(i, m):在第 i 位之后插入 m。
  • q.delete():删除队列所有元素(清空),也可指定删除第 i 个元素。

注意

  • 队列未 push 数据时,默认 size == 0;读取队列的数据读到的也是 0。
  • 往队列 push 数据 0 后,size == 1,读第 0 个数据也是 0。

1.2 扩展

  • q[$] 表示队列最后一个元素。
  • 队列合并:
1
2
int q1[$] = {2, 3}, q2[$] = {5, 6};
q1 = {q1, q2}; // 结果:2,3,5,6
  • 多维队列:int que[num][$] 表示有 num 个队列。
  • 查找(所有 find 方法返回的都是 queue):
  • find():返回满足表达式的所有元素
  • find_index():返回满足表达式的所有索引
  • find_first():返回第一个满足表达式的元素
  • find_first_index():返回第一个满足表达式的索引
  • find_last():返回最后一个满足表达式的元素
  • find_last_index():返回最后一个满足表达式的索引
1
2
que.find       with (item > 3);  // 返回所有大于 3 的元素
que.find_index with (item == 3); // 返回所有等于 3 的索引
  • q.shuffle():随机打乱队列元素顺序。

2 数组

2.1 合并数组

  • 示例:bit [n-1:0][m-1:0] array 表示 n 个元素、每个元素位宽为 m;以整体连续存储、整体赋值。
  • 数组大小定义格式必须是 [msb:lsb],而不是 [size]。
1
2
bit [n-1:0][m-1:0] array;
bit [2:0][7:0] array5;

2.2 非合并数组

  • 示例:bit [n-1:0] array[m] 表示 m 个元素、每个元素位宽为 n;分开存储、按维度赋值。
  • 特性:可为任意数据类型;数组大小写在名字后;bit 组在存储上不连续。
1
2
bit [7:0] array4[2:0];
bit [7:0] array4[3];

注 合并数组和非合并数组可以混用,例如:

1
bit [3:0][7:0] barry[3];

总结 合并数组是连续存储的,非合并数组不是连续存储的。

2.3 关联数组

动态数组容量过大且存储数据“稀疏”时,可以使用关联数组节省存储空间。

  • 关联数组没有固定大小,仿真器可采用树或哈希表存放。
  • 定义示例:
1
bit [63:0] assoc[bit [63:0]];
  • foreach 遍历时,只遍历存储数据的部分。
  • .num 表明关联数组大小(仅包含已存储数据的部分)。
  • .delete() 删除关联数组全部内容。

2.4 数组初始化

SV 中数组可用 {} 列表初始化:

  • 使用 for/foreach 循环初始化
  • 使用默认初始化:
1
2
a = '{default: 0};
a = '{default: 'x};

扩展:数组的 inside 操作,value inside arry 返回布尔值,表示 value 是否在 arry 中。

3 函数与任务

3.1 function

  • function 不消耗仿真时间。
  • function 一般不调用 task;但若 task 不含时间消耗,function 可调用该 task。
  • function 可有返回值,也可无返回值(void)。
  • function 可有输入/输出,也可无输入/输出。
  • function 若使用 void 无返回值,则调用结果不能赋值给其它变量(包括 string)。
  • 函数名可以作为返回值返回。

注意 调用 function 时,若无 return 语句,函数名可以当作返回值返回(给函数名赋值即可);若有 return,以 return 的值为准。执行到 return 后会退出 function。

3.2 task

  • task 可以有 return;执行到 return 后会退出 task。
  • task 内可以消耗时间。

拓展:task 和 function 通常按参数定义顺序传递;也可使用命名参数传递,提高可读性并跳过不需要的参数。

1
fun_a(.a(val1), .b(val2), .sum(val3));

4 位拼接操作符

位拼接是由一个或多个表达式产生的bit位连接在一起的结果。
常见拼接操作符:
eg:{a, b[3:0], w, 3’b101}; ==={a, b[3], b[2], b[1], b[0], w, 1’b1, b’b0, 1’b1}
复制操作符:
eg:{b, {3{a, b}}}====={b, {a,b}, {a,b}, {a,b}}=={b,a,b,a,b,a,b}
字符串连接:
赋值的左侧不允许字符串连接,只允许作为表达式使用。
eg:string hello = “hello”;

1
{hello, “ “, “world”};

=====hello world
eg:{4{a}} ===>{a,a,a,a},{4{a[3:0]}}===>{a[3:0],a[3:0],a[3:0],a[3:0]},从外往内进行“拆分”,一层一层分。

(2)注意拼接运算符:
eg:{1’b1,31{1’b0}}这个是错误的,正确的形式是{1’b1,{31{1’b0}}}

(3)使用parameter的方式拼接

1
2
3
parameter A = 10;
bit[9:0] value;
value = {1’b1,{(A-2){1b’0}},1’b1};

结果是10’h10000_00001。
6、循环语句
(1)for循环

执行顺序:1==>2==>3==>4==>2==>3==>4==>2==>…….循环进行,直到条件2不满足即可退出。

注意:在for循环里面定义了int i=0,那么这个i变量只会用在这个for循环内,在for之外会被释放掉。若没定义i,则会使用全部变量的i。

(2)foreach循环
在foreach循环中,只需要指定数组名并在其后面的方括号中给出索引变量,verilog便会自动遍历数组中的元素。索引变量将自动声明,并只在循环内有效。foreach退出后,索引会被释放掉。foreach循环的条件常见为数组的大小。

注意:对于bit[3:0] data; //使用foreach(data[i])来循环,循环四次,i是从3开始的,即3,2,1,0;
而对于bit[3:0] data[3:0]; //使用foreach(data[i])来循环,循环四次,i是从3开始的,即3,2,1,0;
而对于bit[3:0] data[4]; //使用foreach(data[i])来循环,循环四次,i是从0开始的,即0,1,2,3;

注意:对于二维数组的循环,使用foreach循环的话,需要一层一层的循环,即foreach(a[i][j])的i和j有一个必须是确定的,因此二维数组使用foreach循环如下:

1
2
3
4
5
6
foreach(a[i]) //确定多少行循环
foreach(a[i][j]) //确定该行有多少列循环
xxxx //总共循环i*j次

foreach(a[i,j])//遍历a所有元素
xxxx //总共循环i*j次

注意:foreach进行循环时,对于多维数组使用foreach进行循环,若使用foreach([i,j,k…])来循环,语法是正确的,且i、j、k…变量属于foreach函数内部,不受外部影响;若使用foreach([i][j][k][.][.])来循环,语法是错误的,错误会显示i,j,k…未声明定义,因此需要外部定义i、j、k…..的值,只保留一个变量用于循环,则不会报错,其i、j、k….受外部影响,最后一个变量不受外部影响。

拓展:循环的顺序按照下述顺序

例子:

7、随机函数
randomize 、pre_randomize和post_randomize介绍:
randomize()、pre_randomize()和post_randomize()函数都是System Verilog的内置函数。
1、其中randomize()函数不是VMM和UVM的函数,可以随时调用,只会对代码中的rand和randc属性的变量做随机初始化,若执行成功返回值1,执行失败返回值0。
(1)randomize执行失败后,不会再执行post_randomize()函数。
(2)randomize()函数不能被重写。
(3)randomize()函数的随机种子可以被srandom()函数修改。

2、三个函数的执行顺序是pre_randomize()-randomize-post_randomize(),在执行randomize()之前或者之后会分别执行pre和post这两个函数,因此pre和post可以看作是randomize()函数的回调函数(callback function)
pre_randomize()和post_randomize()函数可以被用户重写。

3、pre_randomize()和post_randomize()函数可以被用户修改,用户使用pre_randomize()函数,常在随机前修改代码中非随机变量的值,如上下限、权重等,post_randmize()函数可以在随机后修改随机值或做一些其它的计算。pre_randomize()和post_randmize()是function类型,里面不能消耗时间,不能阻塞。

注意:
(1)随机函数对变量随机化,变量要是可随机化属性的。也就是说需要是rand和randc类型的才可以使用随机函数randomize随机化。
(2)randomize(a,b,c)可以指定随机变量,即传递参数变量,只有参数列表的变量才会被随机化。若参数列表中有非随机化变量,即使传递也不会修改非随机化变量的值。

拓展:
assert形式随机化:assert(class.randomize()with{});//随机失败返回0且打印log,否则返回1。

8、随机数函数
1、$urandom_range
语法如下:

1
2
function int unsigned $urandom_range(int unsigned maxval,
int unsigned minval = 0);

参数最小值min可以省略,且最大最小值的位置是可以互换的。
用法如下:

1
2
3
Val1 = $urandom_range(7,0);
Val2 = $urandom_range(7);
Val3 = $urandom_range(0,7);

注意:返回指定范围的32bit无符号数。

2、$random()和$urandom()
$random()平均分布,返回32bit有符号随机数。
$urandom()平均分布,返回32bit位无符号随机数。

注意:上述随机数函数是随机一个数值后,再赋值给对应变量。

3、std::randomize() with 静态约束(内嵌约束)
可对非 rand/randc 变量直接施加约束并随机化:

1
2
3
4
int a;
int b = 10;
int c = 20;
void'(std::randomize(a) with { a inside {[b:c]}; });

9、随机变量
(1)关键词rand声明的随机变量,随机变量的值在指定范围内均匀分布;如果不添加约束,随机变量可以是指定有效范围内的任何值。(可重复)
(2)关键词randc声明的周期性随机变量,周期性随机变量的值在有效范围周期性出现,其数据类型只可以是bit或者enum型。

注意:1、randc对单一变量来说,具有唯一性,即每个变量在随机函数randomize随机后,都会有唯一值。比如randc bit[7:0] a,对a随机64次,那么每一次的值都会不一样。
2、对于数组类型而言,则可能重复。比如randc bit[7:0] a[3],其中a[0]、a[1]和a[2]都是一个单一变量,那么在随机函数randomize随机后,a[0]、a[1]和a[2]可能相等。

因此产生唯一数组的方法:
(1)使用rand产生具有唯一元素的数组,使用foreach。

1
2
3
4
5
6
7
rand bit[7:0] ua[64];
constraint c{
foreach(ua[i])
foreach(ua[j])
if(i != j)
ua[i] != ua[j];
} //约束使用{}

(2)使用randc产生唯一元素数组。

1
2
3
4
5
6
bit[7:0] ua[64];
randc bit[7:0] val;
foreach(ua[64])begin
randomize();
ua[i] = val;
end

注意:randc变量随机时,不能设置权重。
10、内嵌约束
产生一个随机数,可以使用随机数函数,同时也可以使用randomize()with{}来做约束:
比如:
std::randomize(a)with{a inside {[b:c]};};
表示a的值从b和c的值中选。其中a不需要指定是rand或randc类型。
又比如:
std::randomize(a)with{a dist {[b:c]:/d,[e:f]:/g};};
上述表示a的值按照百分比来选择。相比随机函数,内嵌约束可以选择变量值的占比等。

注意:内嵌约束需要使用{}来包裹约束内容,即约束块应该使用{}来包裹。同时前面的变量也需要用()包裹起来。
11、常用约束
声明约束时必须采用分段的表达,且必须具有固定的顺序。

(1)constraint的使用

constraint约束表达式的求解是由SV的约束求解器完成,满足约束的值由伪随机数发生器(PRNG)产生。

(2)inside的使用

inside是常见的约束运算符,表示变量应属于某个集合,除非存在别的约束,否则随机变量在集合里取值的概率是相等的。

拓展:
{val inside {[a:b]};}; //表示在指定范围内;
{!(val inside {[a:b]});} //表示在指定范围外。

(3)dist的使用

关键词dist可以在约束中产生随机数的权重分布,符号用 := 或者 :/
(1):=表示取值范围内的每一个值的权重都是相同的。
(2):/表示权重平均分到取值范围内的每一个值。

(4)solve A before B
表示在约束语句块B之前,先约束语句块A。使用solve…before约束后,将会改变随机数值的出现几率,使得某些特定的取值情况更易出现。
使用solve before不能是randc型,且变量需要是整数型。

(5)关系操作(双向约束)
->操作用于表示约束表达式中的分配操作,将约束表达式的左侧变量与右侧表达式进行关联。
如约束块中(x==0)->(y==0);表示x=0则y=0,反之亦然。
(1)当x = 0的时候y一定为0;
(2)当x != 0的时候,y的值随意;
(3)当y == 0的时候,x值是随意的;
(4)当 y != 0的时候,x一定不为0;

注意:关系操作->是双向的。

(6)常用约束操作符
以下是verilog中常用的约束关系操作符:
相等关系:==、!=
大小关系:<、<=、>、>=
范围关系:inside、outside
包含关系:inside
逻辑关系:&&、||

(7)数组约束
a.sum() with (int’(item)) == b 表示的意思是数组a进行求和,求和的大小sum为b,这么做可以防止数据求和后的溢出。

(8)软约束soft
soft为软约束,在constraint约束块中,可使用soft修饰约束表达式。soft软约束相对于其它未使用soft约束块的优先级要低,若与未soft修饰的约束表达式冲突,则soft修饰的约束表达式会失效。总结如下:
1、soft约束若与其它硬约束冲突的话,则soft约束会失效。
2、soft约束与其它硬约束有交集,则soft软约束和其它硬约束的交集则为约束值。
3、若硬约束之间有冲突的话,仿真器会报错。

(9)条件约束

注意:上述约束块都是由{}包裹的。

(10)unique约束
可以用于生成变量集的唯一值或数组的唯一元素。如下所示:(生成多个不相同变量,或生成元素不相同的数组)

1
2
3
4
rand bit[3:0] var1,var2,var3;
rand bit[7:0] array[6];
constraint cons_var1 {unique {var1,var2,var3};}
constraint cons_var2 {unique {array};}

12、new函数
两者的相同点就是都申请内存并初始化变量。
(1)new()函数仅创建一个对象,new()可以使用参数设置对象的数值,即参数传递。
(2)new[]操作则建立一个含有多个元素的数组,而new[]只需使用一个数值来设置数组的大小。
13、cast使用
$cast(child,father)为向下类型转换(向上类型转换:父类指向子类),仅当父类指针指向的对象和待转换的类型一致时,$cast才会成功。eg

1
2
3
4
5
6
7
8
9
10
11
12
13
class father;
xxx
endclass

class child extends father;
xxxx
endclass

father f1;
child c1,c2;
c1 = new();
f1 = c1; //f1句柄指向c1空间
$cast(c2,f1); //c2句柄指向c1空间

使用场景:uvm中的p_sequencer通过$cast向下转换$cast(p_sequencer,m_sequencer),指向virtual_sequencer。其中p_sequencer和virtual_sequencer都属于uvm_sequencer类型。
14、class类
1、属性和方法
(1)class类中的变量为属性;类中的程序也称为方法(task/function)。
(2)类外定义方法需先将方法的原型定义(方法名和参数),并加入extern。在类外部需在方法名前加入类名和两个冒号(::作用域操作符,表示属于某个class类)

注意:在类中定义方法不需要加::作用域,如classa::taska();在类外定义方法需要加::作用域表面方法属于哪一个类。

2、class的声明和例化
(1)SV通过new构造函数来创建对象。派生类的new函数会先调用父类的new函数。
(2)1、类的声明是声明一个句柄,其句柄指向类,即指向此类的指针。2、例化是创建对象,分配内存空间,并将句柄指向这一段内存空间。如下:

1
2
3
4
class A;
endclass
A a; //声明了一个a句柄指向A。
a = new(); //创建一个A类的实例,并将其分配给a句柄。

总结:声明一个句柄,例化一个对象,使句柄指向开辟的空间。

3、对象销毁
当一个对象的空间没有句柄指向时,即不被“需要”,SV的内存管理会释放此空间。

4、extends继承
extends关键字用于创建一个类的继承关系。通过extends可以使类派生另一个类,并继承其属性和方法。(1)派生类中可以重写(重定义)基类的方法(task/function)和属性(variable);(2)派生类中可以新定义新方法和属性,以实现自己的特定行为。
super关键字可用于引用父类中的成员、参数等。若派生类对基类的方法A重写了,又想使用基类的方法A,可以使用super.A()来引用父类的方法A。

总结:子类继承于父类,子类可使用父类的方法和属性并重写,或子类自新增属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base;
int a = 3;
function void myFunction();
$display("Base class %d",a);
endfunction
endclass

class Derived extends Base;
int a = 10;
function void myFunction();
a = 12;
$display("Derived class %d",a);
endfunction
endclass

若子类Derived被声明且例化后,子类空间存在两个变量,即super.a和this.a;

1
2
3
4
5
6
derived = new();
base = derived;
derived.myFunction();
base.myFunction();
$display("base_test %d",base.a);
$display("drv_test %d",derived.a);

若父类指向子类,则打印结果为:

1
2
3
4
Derived class     12
Base class 3 //父类类无virtual,所以不调用子类方法
base_test 3 //父类句柄指向子类空间,父类见不到子类的this.a,找的super.a
drv_test 12 //子类调用自己的this.a

注意:(1)若父类指向子类,子类新增的方法和属性对于父类是不可见的;
注意:super只能用一层,多层使用不行。

5、virtual虚方法
virtual虚方法就是一个基本的多态结构,父类使用virtual后,则可以“见到”子类的方法。

父类中使用virtual定义方法function时,父类句柄在指向子类对象后,可以动态索引到子类重写的子类方法。子类若存在相同的方法function,那么父类会直接调用子类的方法function。若父类中无virtual定义方法function,那么父类只能调用自己的方法function。

(1)在父类的方法定义时,若方法日后会覆盖和重载,那么加virtual。
(2)virtual只需要声明一次,子类可以无需声明。
(3)成员变量不能添加virtual,不能使用父类句柄找子类中的成员变量。
(4)虚方法尽量定义在最底层的父类中。

注意:可以小指针指向大对象(父类=子类),即父类句柄指向子类空间,指向后,父类只能访问子类中属于自己的成员变量;但是不能大指针指向小对象(子类=父类),即子类句柄指向父类空间,否则会报错(除非cast转换)。

UVM中interface为硬件与软件的接口,因此要加virtual来定义interface

扩展:
子类继承父类,父类的方法A使用了virtual来声明,则调用父类的方法A时,会直接调用子类的同名方法A,此时若子类里新增一个属性(变量a),则子类的方法A可以直接使用属性a来做逻辑。
15、宏定义
(1)宏定义(define A xxx) define A xxx是在编译时完成的,而不是执行时,也就是说并会不因为某一个分支(比如if….else if…..条件分支)未执行而不进行宏定义。如果是条件编译即可(ifdef….else…条件编译),如下:

1
2
3
4
5
6
7
8
9
10
11
12
				`ifdef MACRO_A
`define A xxx
`else
`define A yyy
`endif

`ifdef MACRO_A
`define A xxx
`elsif MACRO_B
`define A yyy
……
`endif

可以对宏定义进行选择。(在编译选项中选择定义MACRO_A或MACRO_B来`define A的值)

若没有定义宏MACRO_A和宏MACRO_B,则可使用:

1
2
3
4
5
`ifndef MACRO_A
`ifndef MACRO_B
`define A xxx
`endif
`endif

(2)跨行的宏定义
使用反引号\(续行符)连接,定义一个交换数值的多行宏,使用反斜杠连接不同行,比如:

1
2
3
4
5
6
#define SWAP(a,b) do { \
int t = 0;\
t = a; \
a = b; \
b = t; \
} while(0)

调用时:`SWAP(a,b)
注意:在行尾的反斜杠“ \ ”之后不能有空格或字符,否则编译器会报错
注意:续行符的作用是将宏定义的一行代码分多行书写,提高代码的可读性。若不加续行符则宏定义只有一行,影响美观。

(3)防止头文件被重复包含

1
2
3
4
#ifndef BODYDEF_H 
#define BODYDEF_H
//头文件内容
#endif

注意:若define A 1后,之后使用宏A的值时,需要A来获取宏A的值。

(4)带参数的宏(宏文本)
1、使用str(``双引号,分隔符)区分引用内部的变量和其它部分。比如:

1
`define fnoc_path(str) dut.side_in.``str``.data

调用时`fnoc_path(fifo)则表示dut.side_in.fifo.data

2、使用”str”(`反引号,”双引号)表示引用内部的变量也会被替换。比如:

1
`define fnoc_path(str) “str.data”

调用时`fnoc_path(side_in)则不会替换:”str.data”

1
`define fnoc_path(str) `”str.data`”

调用时`fnoc_path(side_in)则可以替换:”side_in.data”

1
`define fnoc_path(str) `”data_str_data`”

调用时`fnoc_path(data)则不会替换:”data_str_data”

1
`define fnoc_path(str) `”data_``str``_data`”

调用时`fnoc_path(data)则可以替换:”data_data_data”

注意:宏定义传参的本质是替换

扩展:
用法:`undef MACRO
作用:解除宏定义
16、@和wait
1、电平敏感事件:wait(<条件表达式>)是电平触发,即括号中的内容为真时触发。
eg:

1
wait(enable == 1)begin xxx end; /*等待且阻塞后续进程*/

等待enable信号,当条件表达式为“真”时,则立刻执行语句块;否则仿真程序进入等待状态,直到条件表达式为真。

2、边沿触发事件:@(<事件表达式>)是边沿触发,即括号中的事件发生跳变时触发。
eg:

1
@(posedge clk)begin xxx end; /*等待且阻塞后续进程*/

等待时钟敏感,时钟发生正跳变后退出等待状态,继续执行下一条语句
eg:

1
@(a)begin xxx end; /*等待且阻塞后续进程*/

等待a敏感,a发生边沿跳变时退出等待状态,继续执行下一条语句

总结:
(1)wait和@的使用:wait等待电平敏感;@等待边沿敏感。
(2)wait常放在fork….join中,此时不会阻塞其它并行语句的执行。
(3)@(clk)常放在while循环中,一直随clk边沿触发。

注意:在@的使用中,多个while语句块的执行过程中,语句块的语句执行后,若while未退出,每次执行完“一次语句块”后,都会在@处等待下一次语句块的执行。
17、时钟块
(1)clocking……endclocking块是对所在的module/interface/checker/program里信号的采样和驱动的timing属性进行声明。
(2)同一个module/interface/checker/program中可以声明多个clocking…endclocking,同一个信号也可以出现在不同的clocking….endclocking中。
(3)Default的clocking skew作用在所有未明确声明的信号。对于声明为input的信号,clocking skew是指在clock event到来之前多少时刻对其进行取样。对于output信号,则是指clock event发生后多少时刻对其进行驱动。

(1) SV中的时间单位step等于全局时间精度,如果未指明,则default input skew是1step,default output skew是0。

18、枚举类型
枚举类型的定义:enum 枚举标签类型 {A = value, B, C,,,,} enum_e;
在枚举标签类型和value缺省的情况下,默认枚举标签类型为int类型,value的值从0开始依次加1,即A=0,B=1,C=2,,,,。若B=value,则C=value+1,A等于0

注意:没有特别指出的话,枚举类型会默认当成int类型存储。

拓展:枚举类型可以作为数组的索引,如:int arry[enum_e];则arry[A]、arry[B]和arry[C]等都为一个数组元素,且以枚举列表标签为索引。(enum_e必须typedef 枚举标签类型 {A=value,B,C,,,} enum_e这么定义才可以int arry[enum_e]这么定义数组)

注意:arry[enum_e]在未赋值时,$size(arry)大小为0,赋值多少则size大小为多少,数组最大size,则由枚举标签类型决定。(自动变化)

19、typedef/struct/union
(1)typedef是创建新的类型(创建的类型可以用来定义新的变量,有点类似于`define),
用法:typedef 已有数据类型 新类型;

(2)
1、packed为填充(压缩的)的,unpacked为非填充的(非压缩的,以4个字节为一部分)。默认情况下为非填充的类型,使用关键字packed则为填充的类型。若为填充的结构体,压缩结构体按照指定的顺序以相邻的位来存储结构体成员。压缩结构体被当做一个向量存储,结构体的第一个成员在向量的最左边。(还有压缩联合体,压缩数组等)

2、packed structure的成员可以通过成员名引用(.)也可以使用结构体向量的相应位来引用([M:N])。

(3)struct是创建结构体(结构体内可以由多个不同类型变量组成。),即描述一组相关的数据,常见用法typedef struct packed{}xxx_t;,可以通过.成员变量的方法引用。

(4)联合体union。
1、联合体只存储一个元素,但该元素可以有多种表示方法,每种表示方法可以是不同数据类型
2、union声明及成员引用方法同struct,关键词为union。
3、union内的成员公用同一存储空间。所以对其中一个成员赋值,其他成员也会相应变化,只是数据类型不同而已。(不能有多驱动,即对多个元素驱动)

扩展:结构体初始化,可以是用str = ‘{default:0}; 使用此方法结构体则全部初始化为0。

注意:压缩的(合并的)结构体/联合体,若内部有非合并的成员(如非合并数组),则编译器会报错。(Unpacked member found in a packed structure or union:
Packed structure ‘str’ contains unpacked members.)

注意:合并数组中,若某时刻对合并数组多次赋值操作,使用interface阻塞采样合并数组变量中的单bit成员,可能会出现X态,原因可能是合并数组的成员变量,在存储上是相连的,采样导致地址冲突。
20、常用逻辑操作
1、取反
表示按位取反,而!表示逻辑非(非真即假)。比如!7=0,6=~110=001

2、取余%
%(取模,即取余数),例如3%7=3,7%3=1,即余数的意思,即经过除操作后,剩下的余数。

21、流操作
{<<{}}和{>>{}}属于stream operation流操作符
(1)流操作{<<{}}按照从右到左的形式转换成流;
(2)流操作{>>{}}按照从左到右的形式转换成流。

22、fork语句
begin……end语句以顺序方式执行,而fork…join中的语句则以并发方式执行,在所有语句执行完成后,会退出fork….join语句块去执行后面的语句。fork….join内的语句,如果多个语句“同时并行执行”,其实是“顺序”执行的,而如果加了延时的话,那么就会有先后,时间少的先执行,时间多的后执行。
1、fork…join
语句块是并行执行的,多个语句块在同一时刻执行,那么会先执行前一个语句块,再执行后一个语句块,但是所有的语句块都是在一个clk内执行完成。
2、fork…join_none
在调度其块内语句时,父线程继续执行,不等待。其余与fork…join相似。
3、fork…join_any
对块内语句进行调度时,当第一个语句完成后,父线程才继续执行,其它停顿的线程也得以继续。其余与fork…join相似。

注意:在fork语句块内若存在一个while条件循环,如while(a>10),若初始时判断while条件不满足,则表明此while(a>10)不会再执行,即使之后存在的其它语句块使a>10,其也不会执行。

扩展1:for循环中使用fork…..join_none

1
2
3
4
5
for(int i=0; i<3; i++)begin
fork
$display(“i=%0d”,i);
join_none
end

$display打印的三次值都为3,因为i就一个地址,当i等于3时,再执行$display打印(3个进程)

1
2
3
4
5
6
for(int i=0; i<3; i++)begin
fork
automatic int k = i;
$display(“k=%0d”,k);
join_none
end

$display打印的三次值分别为0、1和2,因为automatic每次循环会初始化一个变量k,即一个新的地址并拷贝当前i值,三次各不一样,再执行$display打印(3个进程)。

扩展2:wait fork的使用

1
2
3
4
5
6
fork
check_trans(tr1);
check_trans(tr2);
check_trans(tr3);
join_none
wait fork

语句wait fork等待上述fork创建的并行线程结束。(作用范围:wait fork与fork同一层级)

扩展3:disable的使用

1
2
3
4
5
6
fork
check_trans(tr1);
check_trans(tr2);
check_trans(tr3);
join_none
#1 disable fork

语句disable fork在#1后,终止fork进程以及其所有子线程。(作用范围:disable fork与fork同一层级,如果放在fork内,不会生效)

1
2
3
4
5
6
7
8
9
10
fork:fork1
begin:begin1
check_trans(tr1);
end
begin:begin2
check_trans(tr2);
#1 disable fork1/begin1/begin2
end
join_none
wait fork

语句disable fork1/begin1/begin2(发在同级或内部其它地方,前提是此进程被执行过,否则不会生效。)在#1后,会终止指定进程fork1/begin1/begin2。如果这里写disable fork则实际不会终止fork进程。
23、break和continue
1、break直接退出包含它的循环,不再执行break后的语句。
2、continue结束本次包含它的循环,即continue之后的语句不再执行,结束当前本次循环,不影响之后的循环。

多层循环嵌套:break退出包含它的循环(同层级循环),如果在里层循环,则退出里层循环,如果在外层循环,则退出外出整个循环。(continue类似)
24、event事件
定义:event eve; //eve是一个事件。
事件是静态的同步对象句柄,可以像参数一样在子程序中传递,可以用来同步多个并发的进程,比如某个进程等待着事件,另一个进程则触发这个事件。

事件的触发和阻塞等待
触发:->eve 或 ->> eve;
等待:@(eve) 或 wait(eve.triggered())

wait_order的使用
wait_order阻塞等待多个事件的触发,并且要求这多个事件按照用户决定顺序触发。wait_order可以和else一同使用,当多个事件按顺序触发时,执行wait_order后的语句,否则执行else后的语句。

wait_order里面的a,b,c需要注意事件顺序,否则会报SV错误。
event事件和task任务的区别:
1、 触发事件后,主程序可以继续往下执行,不受触发的事件何时结束的影响;
2、 在调用一个任务后,需要等待任务执行完毕后,才可以继续往下执行。

25、系统函数
(1)deposit函数
在 verilog 中,$deposit 是一个系统函数,用于在仿真过程中将一个值存储到指定的变量中。使用方法:$deposit(variable, value);
常用于在初始时刻,给输入赋一个初值,常赋值为0。

(2)$onehot(BUS)、$onehot0(BUS) 和 $countbits(exp, x, x)
1、$onehot(BUS):BUS 中有且仅有 1 bit 为 1,其它为 0,则返回真,否则返回假。
$onehot(expression) 等价于 $countbits(expression, 1) == 1,返回 true (1'b1),否则返回 false (1'b0)。

2、$onehot0(BUS):BUS 中不超过 1 bit 为 1(允许全 0),则返回真,否则返回假。
$onehot0(expression) 等价于 $countbits(expression, 1) <= 1,返回 true (1'b1),否则返回 false (1'b0)。

3、$countbits(expression, control_bit, control_bit):计算位向量中指定的 0/1/x/z 数量,返回值为整数。
$countbits(expression, 1):返回表达式中 1 的数量;
$countbits(expression, 0, 1):返回表达式中 0 和 1 的总数量;
$countbits(expression, x, z):返回表达式中 x 和 z 的总数量;

扩展:可以用于约束,比如约束nbit变量中,有多少bit拉高。比如:
std::randomize(signal)with{$countbits(signal,1) ==3;}; //将约束变量signal仅3bit拉高。

(3)文本系统函数
1、$fopen:fd = $fopen("file_name", "mode"); 其中 file_name 为文件名,mode 常见 r/w/a,fd 为文件句柄。
2、$fwrite:$fwrite(fd, "%d,%h", a, b); 向文件写数据;也可用 $sformatf("x=%0d", x) 生成字符串后写入。
3、$fclose:$fclose(fd); 关闭文件句柄。

(4)$typename系统函数
1、系统函数 $typename() 用于在运行时获取变量或表达式的数据类型,并以字符串形式返回类型名。用法:string_a = $typename(a);

(5)$timeformat使用
用法:$timeformat(units_number, precision_number, suffix_string, minimum_field_width);
1、units_number:0 ~ -15 之间的整数,表示打印时间单位:0 秒 s,-3 毫秒 ms,-6 微秒 us,-9 纳秒 ns,-12 皮秒 ps,-15 飞秒 fs。
2、precision_number:小数点后保留位数,默认 0。
3、suffix_string:时间值后缀字符串,默认空。
4、minimum_field_width:最小字段宽度,默认 20。

(6)、$clog2()使用
用法:$clog2(n);
n为整数,其功能就是对输入整数实现以2为底对数,其结果向上取整。

(7)$value$plusargs(user_string,variable)使用
$value$plusargs(user_string, variable) 用于在仿真时,将参数传递到环境内部。
例如:$value$plusargs("string_t=%s", variable),同时在命令行中带 +string_t=xxx(以 Makefile 为准),最后 variable 得到 xxx 字符串。

注:可以用来传递其它变量,如int、bit、long等类型。

(8)$test$plusargs(string)使用
$test$plusargs(string) 用于在命令行查找字符串 string,若查到则返回 1,否则返回 0。命令行仿真时需要将 +string 加入命令行(以 Makefile 为准)。

(9)$readmemh和$readmemb
读取的内容只包括:空白位置,注释行、二进制或十六进制的数字。
数字中不能包含位宽说明和格式说明,其中readmemb要求每个数字是二进制数,readmemh要求每个数字必须是十六进制数。数字中不定值x或X,高阻值z或Z,和下划线(_)的使用方法和代表意义与一般Verilog HDL程序中的用法一致。

在Verilog语法中,一共有以下六种用法:
(1)$readmemb(“<数据文件名>”,<存储器名>);
(2)$readmemb(“<数据文件名>”,<存储器名>,<起始地址>);
(3)$readmemb(“<数据文件名>”,<存储器名>,<起始地址>,<终止地址>);
(4)$readmemh(“<数据文件名>”,<存储器名>);
(5)$readmemh(“<数据文件名>”,<存储器名>,<起始地址>);
(6)$readmemh(“<数据文件名>”,<存储器名>,<起始地址>,<终止地址>);

$readmemb和$readmemh是把数据文件的内容读到存储器中
数据文件的格式:1111 1010 0101 1x1z 1_1_ 1_111
或者:
1111
1010
0101
1x1z
1_1_
1_111

(10)$writememh和$writememb
同$readmemh和$readmemb的用法类似,作用是将存储器的数据写入数据文件中。
26、force与release
force语句的用法:force <寄存器变量或者线网> = <<赋值表达式>>; //左边输出,右边输入
release语句的用法:release <寄存器变量或者线网>
force语句应用于寄存器变量或者线网变量时,则在force语句执行后,该变量将强制由<赋值表达式>进行连续驱动,此时将忽略较低优先级的驱动,比如assign,直到有一条release语句执行为止。
27、时钟块
时钟块(clocking block)是一种用于描述时钟和时序约束的结构,用于控制同步信号的时序。
1)时钟块默认格式为:

1
2
3
4
5
clocking cb(posedge clk);
default input #1step output #0;
input a;
output b;
endclocking

上述a为采样DUT的信号,b为驱动DUT的信号。时钟块的默认时序是在#1step延时之后采样输入信号(在前一个时间片的Postponed区域),在#0延时之后驱动输出信号。
2)指定setup、hold下的采样与驱动

1
2
3
4
5
clocking cb@(posedge clk);
default input #t1 output #t2;
input a;
output b;
endclocking
上述表示a在时钟沿之前的t1时刻进行采样(Setup_time),b在时钟沿之后的t2时刻进行驱动(hold_time)。

总结:时钟块在边沿采样时,采样setup_time前的信号值,而在驱动时,时钟块的值在hold_time后再驱动DUT。(采样setup_time的值,值等hold_time后驱动)

注意:在时钟块中应当使用同步驱动,即“<=”操作符来驱动信号。
28、package包
使用如下:

1
2
3
4
5
6
package pkg_all;
import pkg1::*;
import pkg2::*;
`include “file1.sv”
`include “file2.sv”
endpackage

总体来说,对于SV文件的编译,要么将文件在filelist中列出,要么在top外include进去。对于嵌套型的pkg,pkg中的内容只对当前pkg可见,不可跨多层。(重要)

注意:
(1)不能在package中include “interface.sv”,否则报错;
(2)不能在module和package内include “pkg.sv”,否则报错;
(3)可以在module和package内使用import pkg::*
(4)注意文件放置的顺序,文件的编译与顺序也有关
(5)方式1:filelist中列出文件路径,再结合package中的相对路径一同作用,使编译通过。(通常使用这种方法)方式2:不用filelist列出文件路径,package中写绝对路径,使用编译通过。

注意使用import xxx_pkg.sv进行导包后,;不能忘记,应该是import xxx_pkg.sv;

29、fopen文件操作
verilog中,文件操作用于读取外部数据或写入结果。
1)$fopen用于打开一个文件,并返回一个文件句柄(integer类型)。如果文件无法打开,则返回值为 0
语法:integer $fopen(string file_name, string mode);
file_name:要打开的文件名(包括路径)。
mode:文件打开模式,常用的模式有:
“r”:以只读模式打开文件(读取文件内容)。
“w”:以写入模式打开文件(如果文件存在,则清空内容;如果文件不存在,则创建新文件)。
“a”:以追加模式打开文件(如果文件存在,则在文件末尾追加内容;如果文件不存在,则创建新文件)。
返回值
成功打开文件时,返回一个非零的文件句柄(integer类型)。
如果文件无法打开(如文件不存在或权限不足),返回0。

2)$feof用于检查文件是否已经到达末尾。到达末尾,则返回1;否则返回0。
语法:integer $feof(integer fd);
fd:文件句柄,由$fopen返回。
返回值
如果文件到达末尾,返回1。
如果文件未到达末尾,返回0。

3)$fscanf用于从文件中读取格式化的数据,并将其存储到指定的变量中。
语法:integer $fscanf(integer fd, string format, output args, …);
fd:文件句柄,由$fopen返回。
format:格式化字符串,用于指定如何解析文件中的数据。常用的格式说明符包括:
%d:读取十进制整数。
%x:读取十六进制整数。
%f:读取浮点数。
%s:读取字符串。
%c:读取单个字符。
%%:匹配百分号本身。
output args:输出变量,用于存储从文件中读取的数据。
返回值
返回成功读取的字段数量。
如果文件结束或发生错误,返回-1。

代码例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fd = $fopen("file.sv","r");
if (fd == 0) begin //xxx end
////循环读取文件的每一行
//while (!$feof(fd)) begin // $feof(fd) 检查是否到达文件末尾
// if ($fgets(line, fd)) begin // $fgets 读取一行内容
// $display("Line: %s", line);
// end else begin
// $display("Error reading file.");
// end
//end
while (!$feof(fd)) begin // $feof(fd) 检查是否到达文件末尾
result = $fscanf(fd, "%s", str);
$display("str:%s",str);
end
$fclose(fd); // 关闭文件

30、参数化类
通用的类可以在例化的时候修改数组大小和数据类型等
常用方式1:

1
2
3
4
5
6
7
class packet #(int size = 1);
bit[size-1:0] var;
………
endclass

packet #(.size(16)) p1; //var为16bit变量
packet #(16) p2; //var为16bit变量

常用方式2:

1
2
3
4
5
6
7
class packet #(type T=int);
T t;
……..
endclass

packet#(bit[3:0]) p1; //T的类型为bit[3:0]
packet#(.T(bit[3:0])) p2; //T的类型为bit[3:0]

常用方式3:

1
2
3
class packet_t#(type A) extends packet#(bit[7:0]);
A a;
endclass

packet_t子类使用bit[7:0]扩展了基类packet,本身也是一个参数化的子类。

31、set_drain_time使用
作用:使phase在drop_objection后,再执行一段设置的时间。一个phase对应一个drain_time,并不是所有的phase共享一个drain_time。

使用方法:phase.phase_done.set_drain_time(this, time);

篇外1:SVA 断言

断言是对设计行为属性的描述。在仿真过程中,如果一个被检查的属性与预期不符,那么断言失败。SVA 常见的使用场景有:接口时序规则类检测(如 APB/AHB 等时序关系)、特定规则类检查(如 soc 和 eoc 之间 vld 必须为高)、延时类检查(如向 BMU 申请地址后必须 3 拍返回地址)、白盒验证(验证设计意图时)等。

SVA 语法主要分为五个结构层次:

  • 最底层:布尔表达式
  • 第二层:序列(sequence),可包含操作符(如 ## 时隙延迟、重复操作符、序列操作符等);序列是封装格式,可在不同地方复用
  • 第三层:属性(property),重要的封装方式,可封装 sequence,内部可定义蕴涵操作符(|->、|=>)
  • 第四层:断言指示层,采用 assert 对特定属性/序列做行为检查,或采用 cover 做覆盖率统计
  • 第五层:断言的最后封装,可通过 module/program/interface 封装

断言的建立过程与格式:

断言的三种定义:

  • 断言成功:满足断言条件,并得到期望结果
  • 断言失败:满足断言条件,但未得到期望结果
  • 空成功:断言条件始终不满足,断言未触发(伪成功)

1 交叠蕴含 / 非交叠蕴含

  • 交叠蕴含 |->:起因序列与结果序列处于同一时钟周期(类似 if…then)
  • 非交叠蕴含 |=>:结果序列处于起因序列的下一个时钟周期(类似 if…next then)

蕴含操作符只能用于属性定义中,不能用于序列中。

蕴含操作符(仅当if满足时,才执行then,if不满足,则“空成功”)
交叠蕴含“|->”:表示起因序列与结果序列处于同一时钟周期,相当于if…..then…

非交叠蕴含“|=>”:表示结果序列处于起因序列的下一个时钟周期,相当于if….下一个时钟周期then….。

2 sequence 序列

序列是描述一种信号时序关系的基本语句,表示一些事情先后发生顺序,大多数序列用##运算描述符。
格式:

1
2
3
sequence name_s;
<test expression>;
endsequence

3 ## 时隙延时运算符

图一表示,A=1时,继续检查,2个clock后,B也为1。同理,图2和图3也类似。
eg:

1
2
3
4
5
6
7
property prty;
@(posedge clk) a |-> ##[1:3] b;
endproperty

property prty;
@(posedge clk) a ##[2:3] b;
endproperty

注意:此时A执行,过##clk后,B再执行。

4 重复运算符

  • [*n]:连续重复 n 次
  • [*m:n]:连续重复 m 到 n 次(最少 m 次,最多 n 次)
  • [=n]:非连续(无所谓跟随)重复操作;data [=8] data 表示 data 被采样为真 8 次,期间可以不相邻
  • [->n]:非连续跟随操作符;data [->4] data 表示 data 被采样为真 4 次(可连续或间断),后续表达式在第 4 次采样后马上生效

5 disable iff

disable iff后面的条件为停止检查的判断条件。
Eg:property p;

1
2
3
4
5
		@(posedge clk)
disable iff(reset) a |-> ##1 b[->3] ##1 c;
endproperty
a:assert property(p);
上面在reset为低时会检查a |-> ##1 b[->3] ##1 c,直到reset为高,停止检查。

(6)常见函数

6 常见函数

Function Description Example
$onehot 只有 1bit 为 1 assert property(@(posedge clk) $onehot0({A,B,C})); 同一时刻最多只有 1bit 为高
$onehot0 只有 1bit 为 1 或全为 0
$countones 返回 1 的个数 assert property(@(posedge clk) $countones(A&B) <= 4);
$countzeros 返回 0 的个数
$stable 判断信号稳定不变 assert property(@(posedge clk) $stable(a));
$past 前时刻的值 $past(a, n):a 的前 n 个时钟周期的值
$rose 上升沿 `assert property(@(posedge clk) req
$fell 下降沿 `assert property(@(posedge clk) $fell(done)

$past 使用:(默认情况下,提供信号在前一个时钟周期的值)

基本语法:$past(signal_name, number_of_clock_cycles)

例子:

1
2
3
4
5
property ab_high_two_clk_cd_high;
@(posedge clk)
(c && d) |-> $past((a && b), 2);
endproperty
assert property(ab_high_two_clk_cd_high);

如果表达式 (c && d) 为真,那么两个周期前,表达式 (a && b) 为真。

7 and / or 用法

二进制运算符“and”可以用来组合两个序列。当两个序列都成功时整个属性才成功。

1
2
3
property proty_and;
@(posedge clk) seq1 and seq2;
endproperty

二进制运算符“or”可以用来组合两个序列。只要其中一个序列成功,整个属性就成功。

1
2
3
property proty_or;
@(posedge clk) seq1 or seq2;
endproperty

8 SVA 的写法

两种方法:

8.1 方法 1:SVA + binding

需要两个文件。

demo.sva 示例:

1
2
3
4
5
module demo(
input wire signal1,
input wire signal2
);
endmodule

binding.sva 示例:

1
2
3
4
bind tb_top.DUT demo demo_inst(
.signal1(tb_top.DUT.signal1),
.signal2(tb_top.DUT.signal2)
);

最后将这两个文件 `include 到 tb_top.sv 中。注意:写在 tb_top 的 module 外。

8.2 方法 2:仅 SVA(不使用 binding)

只写 demo.sva,不需要 binding 文件。demo.sva 中先声明变量,再通过 assign 连接 DUT 信号。

1
2
logic signal1;
assign signal1 = tb_top.DUT.signal1;

最后将此文件 `include 到 tb_top.sv 中。注意:写在 tb_top 的 module 内。

方法2相当于将SVA检验器定义在模块(module)中。
方法1相当于将SVA检验器与模块、模块的实例或者一个模块的多个实例进行bind绑定。

显示断言成功的方法:

第一步:FSDB 查看 SVA 成功与否(FSDB 中会显示断言成功/失败):

build_cfg 配置:

1
2
COMP_OPT += -assert enable_diag
SIM_OPT += +fsdb+sva_success

第二步:sim.do 中:

1
fsdbDumpSVA 0 "xxx_tb_top"

断言成功的地方显示绿色向上箭头,失败的地方显示红色向下箭头。


9 常见写法示例

9.1 属性块与 check 分开

并发断言使用关键字 property…endproperty 描述事件。

1
2
3
4
5
6
7
8
9
10
property p_name;
@(posedge clk)
disable iff(!rstn)
!(a && b);
endproperty

s_name: assert property(p_name)
else begin
`uvm_error("xxxx");
end

9.2 属性块与 check 合并

略。

9.3 generate…for 生成式写法

1
2
3
4
5
6
7
8
9
10
11
12
property pro_inst_name(int a, b);
@(posedge clk) disable iff(!srst_n) xxxxxxxx;
endproperty

generate
for (genvar i = 0; i < 8; i++) begin : gen_inst_name
ast_inst_name: assert property(pro_inst_name(a, b))
else begin
`uvm_error("xxxxxxxx")
end
end
endgenerate

9.4 function 的使用

可以在属性块property中使用function来实现一些复杂的操作,具体是将function的返回值(可以是return返回,也可以是函数名返回值),如下例子:

1
2
3
4
5
6
7
function fun_n(logic a, b);
fun_n = xxx;
endfunction

property pro_inst;
@(posedge clk) disable iff(!srst_n) fun_n |-> xxxx;
endproperty

10 bind 绑定

SVA检验器通过关键字bind可以与设计中的任何模块(module)或着实例(instance)绑定。实现绑定时,使用的是设计中的实际信号,语法如下:

1
2
bind <module_name or instance_name> <checker_name> <checker_instance_name>
<design_signal>

11 cover 语法

cover可以用来获得属性property的执行情况,即覆盖属性(cover property),SVA提供的关键词。

12 其它

12.1 SVA 开关

验证环境中控制assertion开关
(1)使用define来控制。方法是:使用宏定义的方法,用宏包裹assert property来达到隔离的目的。
(2)使用向量结合disable iff()来控制。方法是:在top上构造一个信号,并在testcase中用uvm_hdl_force(path,value)进行赋值,进而满足disable assertion的条件,达到某个case打开断言检查的目的。
29、mailbox信箱
mailbox是SV中原生FIFO,在线程之间做数据通信或数据缓存时可考虑使用此类型。
(1)mailbox和队列queue有相近之处。
(2)mailbox是一个对象,必须使用new()来例化,例化时可选参数size来限定存储的最大数量。如果size是0或者未指定,则默认无限大。
(3)使用put()可以把数据放入mailbox,使用get()可以从mailbox移除数据。
(4)如果信箱为满,则put()会阻塞;如果信箱为空,则get()会阻塞。
(5)peek任务可以获取对信箱里数据的拷贝而不移除它。
(6)信箱未指明类型时,可放入任何混合类型的数据。(尽量只放入同一类型)

1
2
3
4
5
6
使用方法:mailbox #(类型) mbx;    //相当于一个队列
mbx.put(value)是将数据放入mailbox中,若为满则语句一直阻塞。
mbx.try_put(value)是将数据放入mailbox中,若为满语句不会阻塞。
mbx.get(value)是将数据取出,若为空则语句一直阻塞。
mbx.try_get(value)是将数据取出,若为空语句不会阻塞。
mbx.num();可以获取信箱当前存储的数据个数。

30、#延时
(1)#0延时用于控制同一时刻运行的两个线程的执行先后顺序,无#0的先执行,eg:

1
2
3
4
fork 
task1() #0 endtask
task2() endtask
join //则同一时刻,task2先执行,task1后执行。

(2)#10为延迟10个时间单位。
(3)##与时钟块使用,##10 if.cb.a<=1表示延时10个时钟周期且a赋值1。

31、静态变量&静态方法
静态变量和静态方法可以在类没有实例时直接调用。
(1)静态变量static variable
普通的实例类属性,每个变量都有单独的内存空间,相互独立。而若使用关键字static修饰变量,则类的所有实例都共享此静态变量。eg:

1
2
3
class A; static value; endclass
class B; A a1; endclass; //类B中声明定义了类A
class C; A a2; endclass; //类C中声明定义了类A

则A、B和C共享静态变量value的内存空间。即value变化,被所有A实例共享。

总结如下:
1.与硬件领域例如module,interface不同的是,在class中声明的变量其默认类型为动态变量,即其声明周期在仿真开始后的某时间点开始到某时间点结束。具体来讲,其声明周期开始于对象创建,终于对象销毁。
2.如果使用关键字static来声明class内的变量时,则其为静态变量。静态变量的生命开始于编译阶段,贯穿于整个仿真阶段。
3.如果在类中声明了静态变量,那么可以直接引用该变量class::var,或者通过例化对象引用object.var。类中的静态变量声明以后,无论例化多少个对象,只可以共享一个同名的静态变量,因此类的静态变量在使用时需要注意共享资源的保护。

(2)静态方法
1.类似于静态变量,在class中定义的方法默认类型是动态方法,而我们也可以通过关键词static修改其类型为静态方法。
2.静态方法内可以声明并使用动态变量,但是不能使用类的动态成员变量。原因是因为在调用静态方法时,可能并没有创建具体的对象,也因此没有为动态成员变量开辟空间,因此在静态方法中使用类的动态成员变量时禁止的,可能会造成内存泄漏,但是静态方法可以使用类的静态变量,因为静态方法同静态变量一样在编译阶段就已经为其分配好了内存空间。
32、ref参数引用
在verilog中,向子程序传递参数时,通常使用复制的方式,而使用ref引用方式可以获取仿真的最佳性能。如果希望子程序不改变参数内容,可以使用const ref类型,此时,编译器会检查参数内容以保证不被修改。
ref引用传参(by reference),参数的引用(地址,即共用同一物理地址)被传递给任务/函数,内部对参数的修改会影响到调用任务的代码中的变量。如下:

扩展:若参数传递的是结构体,结构体内有非合并数组,且传递的结构体为结构体数组的某个元素,则会报错。(大概率是结构体的非合并数组,编译器不好地址划分)比如:

常见问题:
使用ref类型时,若有参数缺省方向,则可能出现bug。如下所示:

1
task sticky(ref int array[50], int a,b); //那么a和b都会被编译器认为是ref类型的。

若指明方向,则不会出问题:

1
task sticky(ref int array[50], input int a,b); //明确指明方向,不会出问题。

33、timeslot时序区域
verilog中,Time slot的simulation regions包含Preponed、Active、Inactive、NBA、Observed、Reactive、Re-Inactive、Re-NBA和Postponed regions。再根据active region set和reactive region set的划分,可以将一个time slot的simulation regions分为五个主要region:Preponed、Active、Observed、Reactive和Postponed。如下图所示:

Preponed:在一个timeslot内,Prepone region只会执行一次,当仿真时间进入当前timeslot时,它是第一个被执行的region。
Active:分为几个小的regions:active,inactive和NBA(nonblocking assignment update) regions。
1)Active region进行当前active region set events的评估求值。所有blocking assignments语句在Active region执行。对于nonblocking assignments,RHS的评估求值发生在Active region,而赋值发生在NBA region。
2)Inactive region处理完所有active events后。显式的#0延迟控制要求将这个event安排到当前time slot的inactive region进行,以便在下一个inactive到active迭代中恢复process。
3)NBA region进行处理完所有inactive events后。Nonblocking assignment(非阻塞赋值的缩写)会在当前或以后仿真时间的NBA region中创建events。
Observed:Observed用于触发property expressions(用于concurrent assertions)时对其求值。在property求值后,pass/fail的信息将在当前time slot的Reactive region报告出来。
Reactive:Reactive包含了Re-active,Re-inactive和Re-NBA regions。
1)Re-active进行当前reactive region set的评估求值,并且可以以任何顺序进行处理。Re-active region和active region其实是对偶关系的。只不过active处理design,re-active处理program。
2)Re-inactive进行处理完所有re-active events后。Program里显示的#0延迟控制要求将这个event安排到当前timeslot的re-inactive region进行,以便在下一个inactive到active迭代中恢复process。同样的,它也是inactive region的对偶关系。
3)Re-NBA进行处理完所有re-inactive events后要。Program里nonblocking assignment会在当前或以后仿真时间的re-NBA region中创建events。
Postponed:postponed是当前timeslot的最后1个region。$monitor, $strobe和其它相似的events会安排在这个region执行(注意$display event是在active region和reactive region(如果在program里调用)执行的)。一旦到达postponed region,就不允许在当前time slot中发生新的值更改。在此region内,向任何net或variable进行写入值或在当前region调度任何先前region的event都是非法的。Postponed region PLI events也在这里调度。

阻塞赋值和非阻塞赋值:
1)非阻塞赋值(<=)的行为:
非阻塞赋值的值计算基于赋值语句执行时的变量值,而不是赋值语句之后变量的变化。
非阻塞赋值的效果会在当前时间的末尾生效,但赋值的值在赋值语句执行时就已经确定。
2)执行顺序:
阻塞赋值(=)是立即生效的。
非阻塞赋值(<=)的值计算发生在赋值语句执行时,但实际赋值操作会被延迟到当前时间的末尾

9.12补充:两个非阻塞赋值同时给一个变量赋值,那么只会执行最后一个,eg:
a <= b;
a <= c;
那么之后执行最后的a <= c;

编译选项:

1
2
COMP_OPT += +fsdb+region
SIM_OPT += +fsdb+delta

第一个编译选项是在波形上打开timeslot区域,第二个运行选项是打开可观测信号跳变的具体时刻。
其它
(1)在constraint中对数组和队列进行约束时,对数组和队列的size大小进行约束的语句不能放在foreach下。
(2)constraint对多个数组和队列随机化,可以使用其它随机化的数组和队列的一些“信息“,比如长度等来约束自身。因为constraint是并行执行的。
(3)在VerilogSystem中2^8-1并不是2的8次方减1,而是2与8-1的异或。(重点)
(4)在数组array和队列queue通过.sum来进行求和的过程中,即array.sum和queue.sum的过程中,要注意数据位宽,不然可能出现溢出的情况。(重点)
eg:bit[8:0] a[$];

1
a = {xxx,xxx,xxx,xxx…..};

a.sum的求和大于2的9次方减1就会出现溢出。
(5)函数和任务中,变量的定义放在函数和任务内的前面几行,不然会报错。
(6)一个sv文件只能被include到一个文件中,但是可以用pkg将其它pkg导进去。
(7)带rand的变量,在使用了randomize后,其值会在规定范围内随机变化。
(8)如果对class的变量进行赋初值,在例化的时候,其值保持初值,可以在例化后,对其进行赋新值,得到的则是新值。
(9)*表示通配符,匹配任意字段。
(10)macro:宏
(11)在SV语法中,通常都是用begin….end来表示一个语句块的,在约束中使用{….}来表示一个语句块。
(12)一个结构体变量a,里面的元素有数组,联合体等变量,如果a定义在class里面,那么在task使用时,将b传递给a,那么可能数据不能全部传递给a;如果a定义在task里面,同时在task内将b传递给a,那么是可以将b给a的。(碰到的奇怪情况)
(13)UVM验证环境中尽量使用同一中赋值,阻塞=和非阻塞<=选择其一,使用阻塞的话,那么fork….join里面的两个task的执行顺序是先上后下,如果使用非阻塞的话,那么这两个task是并行执行的。(前提是这两个task是用一个时钟)。注意注意(20220320:有时候碰到复杂的情况,可能并不会先上后下的顺序执行,具体问题需要具体分析,一般同时刻自上而下执行)。
以后在搭环境的时候,可以往每个task的里加入打印(就打印task/function名),以此来确定哪个task先执行。另外,可以在下降沿产生某信号,而在上升沿使用此信号。
task所处的位置可能会“骗人”,但是时间是不会骗人的。
(14)if((a=1))表示判断为真,编译时会显示warning(显示为执行了赋值操作),但是不会报错。因此,只要if((a=x)),其中x为非0,则判断为真。
(15)在SV中,使用value[a:0]和value[0+:a]进行截位(a为变量),即动态的截位,SV编译器会报错,因此可以使用for(int i=0; i<a; i++)的方式对每bit进行判断后再截位,另外也可&动态宽度的1来进行截位,即value & {a{1’b1}}
(16)在interface上定义一个数组类型的变量,如logic[n:m] singlea[q]; 通过后门访问DUT的信号后(assign singlea = dutb)。注意singlea的取值顺序为0,1,2…..q-1,即从小到大,若dutb定义的顺序是dutb[q-1:0],那么顺序相反,即singlea[0:q-1] == dutb[q-1:0]。
(17)$display()可以直接打印数字,如$display(123987)会打印出123987,但是$display(abcfcg)则会报错。另外,$display(que)可以直接打印队列。
(18)function函数或者task任务,变量的定义需要放置最前面,否则会报错
(19)多个进程同时调用一个任务时,这个任务的资源是共享的(即任务内的值改变,可以影响到其它进程的任务,需要注意)。比如FNOC的RM,up、dn、lt和rt都调用side_in的task。
(20)使用位或|时,若是对数组使用位或会报错,如bit[1:0] arry[3],使用|arry则会报错。
(21)tb.flist中要包含TB编译的各文件pkg或(.sv/.svh等),另外pkg中需要注意包含所需组件的所有要编译文件。

篇外2:功能覆盖率

功能覆盖率的两个重要部分是采样的数据和数据被采样的时刻:

  • 采样时刻:使用 wait/@ 阻塞赋值;或使用 sample() 方法
  • 采样数据:建立 covergroup 与 bins。covergroup 可定义在类/程序块/模块中;使用前必须实例化(通常在类的 new() 中实例化)

covergroup 常规写法:

1
2
3
4
5
6
7
8
9
covergroup cov_name();
cp_name: coverpoint signal_name {
bins bins_name0 = {[a:b]};
bins bins_name1 = {c};
xx
xxx
xxx
}
endgroup

自己的理解:1、首先建覆盖组covergroup cov_name…..endgroup,2、然后需要对覆盖组进行声明与例化new(),3、同时需要确定wait或@或sample对覆盖点的signal_name进行采样,最后使用bins来建仓,收集覆盖率。4、包含覆盖组的类需要被例化到环境中,一般是ENV中。

注意:1、一个covergroup覆盖组可以包含一个或多个coverpoint覆盖点;2、coverpoint内不能写begin……..end,否则会报错。3、可以使用options.auto_bin_max = num;来定义默认建仓个数,默认64

(1)iff关键字
iff构造中的表达式指定了一个可选的条件,该条件可以禁用覆盖点的覆盖。如果iff(表达式)在采样点计算为false,则忽略覆盖点。

(2)default关键字
default关键字,定义了一个不与任何已定义值仓相关联的仓。默认仓捕获覆盖点的值,不属于任何已定义的仓。
比如:bins a0 = default;//表示a0仓代表剩余的所有值。
(3)bins关键字

注意:bins a = {[num0:num1]};表示的是a的取值范围来自num0~num1,满足则覆盖。
bins a[] = {[num0:num1]};表示的是a有num1-num0+1个仓,使用时需要注意num0与num1的先后顺序,小的写前面,其覆盖的数为num0~num1。

(5)翻转覆盖
bins bins_name[] = (a=>b), (c=>d);的作用(翻转覆盖率)
答:其中a将被自动划分为两个仓,分别记录a=>b和c=>d值变化状态的仓。其中:
1、a=>b:这表示一个状态从a转换到b的值变化过程。
2、c=>d:这表示一个状态从c转换到d的值变化过程。
扩展:1、可以用于状态机的覆盖;2、可指定连续的翻转次数,即值保持的周期个数。
3、=>表示连续(a=>b,表示a跳转到b的过程中不能出现其它值),->表示离散(a->b,表示a跳转到b的过程中可以出现其它值)

注意:此处bins不需要加{}来包裹

解释:

1
2
bins b4 = (0[*15]);
illegal_bins b5 = (1[*2]);

表示0出现15次;表示1连续出现两次则非法。
(7)不合法的仓
illegal_bins的作用是对非法的数值进行检测,用法是illegal_bins bins_name = {[a,b]};//表示的意思是需要覆盖的值的取值范围不应该在a与b的范围内。

(8)with约束
类似于附加约束,包含再bin中,使最终bins的取值为满足给定表达式的值。
比如:
bins mod[] = {[0:255]} with (item % 3 == 0)//表示此bins定义为0~255中能被3整除的所有值。
(9)忽略数值
忽略一些不关心的信号取值,ignore_bins不会用于计算覆盖率。
写法:

1
2
ignore_bins ign_n1= {a,b,c};
ignore_bins ign_n2=(a=>b=>c);

不会覆盖a,b,c的值;不会覆盖a翻转到b再翻转到c的值。

注意:ignore_bins关键字应该与具体的交叉项一起使用,而不是与default关键字一起使用。
(4)交叉覆盖
对于多个覆盖点组合的覆盖情况,有两种方法:
1、一种方法是预处理后,即先判断组合情况后,再建仓进行覆盖测试。
2、另外一种则是使用交叉覆盖进行测试。
以下为交叉覆盖:
交叉覆盖是在覆盖点或变量之间指定的,必须先指定覆盖点,然后才能定义覆盖点之间的交叉覆盖。如果想记录在某一时刻,多个变量之间值的组合情况, 需要使用交叉(cross)覆盖率。

解释:上述的交叉覆盖建仓1616个,即a的仓数b的仓数

在交叉覆盖中,除了使用上面自动创建的bins之外,还可以用户自定义交叉bins,这就用到关键字binsof和intersect,如下图。
注意,binsof使用的是小括号() ,而intersect 指定的是一个范围 ,所以使用大括号!

解释:binsof()是取coverpoint的bins;intersect是指定取值或范围;type_option.weight表示不关注此coverpoint覆盖点。

总结:(1)先在covergroup中创建coverpoint覆盖点(名字需唯一);(2)对多个coverpoint进行cross操作,即组合情况的覆盖

(6)covergroup传参
covergroup cov_name(xxx)…..endgroup使用new()例化时,可以在()内传参。
可用于下面场景:
测试接口变量每1bit翻转
答:可以声明一个covergroup cov_name(xxx)……endgroup类型的数组,如cov_name cov_name_inst[10],然后循环使用new()例化cov_name_inst 10次,通过new(参数)传参来测试每bit的翻转情况。

option.per_instance的作用???
sample()的作用???
番外3:覆盖率分析
(1)批量exclude相同代码
1、使用exclude屏蔽代码后,在file选项选择save exclusions not saved/save all exclusions保存文件为xxx.el。
2、打开xxx.el文件,修改屏蔽内容后(相似代码exclude),可在下次打开覆盖率文件后,load exclude后,可看到相似代码被批量exclude了。

save exclusions not saved/save all exclusions的作用:
save exclusions not saved的作用是保存之前未save的exclusion文件,及exclude到recalculation这段时间所waive的部分。
save all exclusions:保存所有exclusion的文件。
番外4:gvim/linux使用
1、GVIM常用操作
(1)指令模式(命令模式):
x:删除某个字符
(2)编辑模式
i、o、a:进入编辑模式
(3)命令模式
esc:退出到命令模式
:w:保存文本
:q:退出gvim
:wq:保存并退出
(4)命令模式下,:/string + 回车:可以显示要查找的string,另外按n或者N可以搜索下一个string。set hlsearch:查找的string全部高亮显示。noh:即可取消的高亮显示。
(5)在命令模式下,按gf(go file)可以跳转到另一个.sv的文件里。同时使用Ctrl+^可以回到原文件中。
(6)在命令模式下,:sp是水平分屏,:vsp是垂直分屏,:vs是垂直分屏。
(5)gvimdiff 文件1 文件2,可以打开GVIM,然后高亮对比两个文件的不同。
(7)GVIM中的替换命令:1):%s/xxx/yyy/g;2):a,bs/xxx/yyy/g;(a行到b行的替换)
步骤进行操作:

  1. 打开该文件并进入 gvim 的编辑模式(例如,可以使用命令 gvim example.txt)。
  2. 按下 : 键,进入命令模式。
  3. 在命令模式下输入 :%s/fox/cat/g,其中,% 表示对整个文件的所有行进行替换,s 表示替换命令,/xxx/yyy/g 表示使用字符串 xxx 替换字符串 yyy 并且全局替换。g 则是一个可选参数,表示全局替换,即会替换所有符合条件的字符串
  4. 按下Enter键,gvim 将会执行替换操作。此时,所有的字符串 xxx 都被替换成了字符串 yyy。
    (8)在GVIM中,使用shift+`可以将字符串的大小写发生转换。
    (9)GVIM中复制多行,首先将光标停在准备复制的初始行,按v键,之后滑动滚轮,或者最右边的滑条,之后选择复制,也可以按y键复制。
    (10)GVIM在行首或行尾添加或删除内容:
    1、在行首添加相同的内容::%s/^/添加内容/g
    2、在行尾添加相同的内容::%s/$/添加内容/g
    (11)在gvim中,可以使用“shift+3”来标记变量高亮,也可以使用“shift+鼠标单击”来标记变量高亮,后者标记高亮不会跳转。
    (12)在GVIM中,使用“shift+r”来刷新目录。
    (13)在命令模式,鼠标左击,然后再在某处鼠标右击,可以赋值左击和右击之间的文本。
    (14)在gvim的文本中,使用gg可以跳转至文本头,使用G,即shift+g可跳转文本尾,使用0可以跳转行的头,使用$,即shift+4可跳转至行的尾。
    (15)统计GVIM中某个字符串的个数,可以在命令模式下,输入:%s/your_string//gn,其中%s为替换命令,g表示全局搜索,n表示只统计匹配到的次数而不进行实际替换。按回车后即可看到统计结果。
    (16)使用“Ctrl+v”进入块操作,然后按上、下、左、右来选择块,之后使用“shift+i”来插入要输入的内容,再按Esc,则选中的块将全部插入相同的内容。
    (17)使用“Ctrl+v”进入块操作,然后按上、下、左、右来选择块,之后按“y”来复制选中的块,之后同样使用“Ctrl+v”进入块操作,按上、下、左、右来选择块,再按“p”将选中的块粘贴,则完成了块的复制与粘贴。
    (18)使用“Ctrl+v”进入块操作,使用shift+u可以将小写字母转换为大写字母。
    (19)gvim中的命令模式下,按下Ctrl+v,再按Enter,字符^M表示换行。
    (20)如果要删除a行到b行的所有数据,可以在命令行下使用:a,bd来删除a行到b行。
    (21)bcompare a b的撤销功能是ctrl+z键,或者点击edit后,选择undo copy to side
    (22)按esc进入命令模式,然后输入nyy,则可以复制当前光标下n行,然后到需要复制的文本中,输入p,则可以粘贴n行到该文本下。
    2、Linux常用命令
    (1)setenv命令
    Linux中setenv命令用于查询或显示环境变量,setenv为c shell中查询或设置环境变量的指令。
    语法:setenv [变量名称] [变量值]
    显示环境变量:echo $变量名称
    (2)$钱号
    变量替换的代表符号,取值符号。例子:setenv vrs=123,则echo $vrs,那么vrs=123。
    (3)在linux的命令行,点击鼠标的滑轮可以将已经复制的命令直接粘贴到命令行,相当于右击粘贴操作。
    (4)diff命令
    比较两个文件的差异,命令格式:diff [参数] [文件1或目录1] [文件2或目录2]。diff命令能比较单个文件或者目录内容。如果指定比较的是文件,则只有当输入为文本文件时才有效。以逐行的方式,比较文本文件的异同处。如果指定比较的是目录的的时候,diff 命令会比较两个目录下名字相同的文本文件。
    (5)linux中,“ctrl+a”可回到命令的头,“ctrl+e”可回到命令的尾
    (6)cd -命令的作用是返回到上一次的路径,比如上一次路径是/a/b/c/d,当前路径是1/2/3/4,那么使用cd -后,就可以回到路径/a/b/c/d下。
    (7)becompare 文件1 文件2,这个命令是寻找两个文件的不同,并高亮显示。
    (8)linux上使用ps -ef | grep xxxx | grep xxxx查找服务器上的进程PID,然后使用kill PID即可清除进程。
    ps -ef命令用于列出所有正在运行的进程。
    grep命令用于在输入中搜索指定的字符串。
    命令管道|用于将管道符左边命令的输出作为管道符右边命令的输入。
    eg:ps -ef | grep dalao | grep xxx
    ps -ef查找服务器的所有进程,然后使用grep查找包含dalao的进程,再在筛选下的进程找到xxx的进行。最后使用kill xxx,清除进程。
    同时可以使用ctrl+v来选择多少行多少列,再按delete来进行删除行与列的操作。

(9)rm(remove):删除文件或目录。
常用参数:
-r:递归删除,目录下的文件也全部删除。
-f:force,忽略警告、不存在的文件,强制删除。
rm -fr一定要小心使用,避免删除一些重要文件。

(10)mv(move file):用于文件或目录的改名和移动存放位置。

(11)mkdir(make directory):创建目录。

(12)cp(copy file):复制文件或目录
常用参数:
-r:递归复制,用于目录的复制。

(13)find:在指定目录下查找文件,重点掌握。
find ./ -name “*.v”:在当前目录及其子目录下所有后缀为.v的文件列出来。
find ./ -ctime n:n天状态发生变化的文件。

find ./ -maxdepth 1 | wc:在当前目录(不包含子目录),统计所有文件个数

(14)du(disk usage):显示文件或目录的大小。
常用参数:
-s:仅显示总数。
-l:重复计算硬件连接的文件。
-h:以K,M,G为单位。
常用:du -sh显示当前目录下的使用总空间。
du -sh xxx显示xxx的大小,可以是文件或文件夹。
du -ah显示所有文件的大小
du -lh –max-depth=1查看当前目录下一级文件和子目录占用的磁盘容量

(15)In(link files):为某一个文件在另一个位置建立一个同步链接,分硬链接和软链接。
(当需要在不同的目录下,用相同的文件时,可以在某个固定的目录下放该文件,其它目录下用In命令link它就行了,可节省磁盘空间。)
常用参数:
-s:软链接(符号链接)
In -s [源文件或目录] [目标文件或目录]
In -s aaa bbb,那么bbb->aaa(后指向前)

grep:用于查找文件里符合条件的字符串。常用于搜索关键词,查找定义文件,查找error。
常用参数:
-i:忽略大小写进行匹配。
-r:递归查找子目录中的文件。
-n:显示匹配的行号。
-s:不显示错误信息。
-l:仅显示匹配内容的文件名,不显示实际匹配的具体内容。
grep -irsn “xxxx”表示递归且不区分大小写在当前文件夹下查找字符串xxxx,且显示行数。

同时,可以使用grep “要搜索的字符” 源文件 > 目标文件此命令,作用是搜索结果后导入到另一个文件中。eg:grep -isrn “xxxx” ./ > yyy.txt

和>>命令:重定向操作符之一,用于将命令的输出重定向到文件中,或用于创建新文件并将输出写入该文件。
符号用于将输出重定向到文件中;>>符号用于将输出重定向到文件的末尾。

脚本案例 1(bash)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

# 指定要搜索的目录
search_dir="path"

# 找到所有包含 block_en = 1 << block_id 的文件并处理
files=$(grep -rl "block_en = 1 << block_id" "$search_dir")

for file in $files; do
# 替换 [0:15] 为 [0:11]
sed -i 's/\[0:15\]/[0:11]/g' "$file"
echo "Replaced in $file"
done

echo "Replacement done."

脚本案例 2(tcsh)

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/tcsh

set files = `grep -nr --include=\*.sv --include=\*.svh --include=\*.v -E "|Mellanox|Synopsys|Candence|Mentor|Orchip" -lr`
foreach file ($files)
sed -i 's/Mellanox/GT/g' $file
sed -i 's/Synopsys/GT/g' $file
sed -i 's/Candence/GT/g' $file
sed -i 's/Mentor/GT/g' $file
sed -i 's/Orchip/GT/g' $file
echo "Replaced $file."
end
echo "Replace Done"

chmod命令:修改file文件权限,其分为所有者、所属者和其他用户的权限。
0:没有权限(—)
1:执行权限(–x)
2:写权限(-w-)
3:写和执行权限(-wx)
4:读权限(r–)
5:读和执行权限(r-x)
6:读和写权限(rw-)
7:读、写和执行权限(rwx)
常用===>chmod 755 file,在执行py文件时,需要文件可执行。

ctrl+r命令:进入搜索历史命令模式,可以输入关键词,自动选择历史命令。

sed命令:主要用来自动编辑一个或多个文件、简化对文件的反复操作、编写转换程序等。
基本语法:sed [选项] ‘command’ file
选项:
-i:直接修改文件内容,常用于替换操作。(加-i会修改文件,不加不会修改)
-e:允许多个编辑命令。
命令:
s/regexp/replacement/:替换模式,将匹配正则表达式regexp的内容替换为replacement。
d:删除命令,删除匹配的行。
p:打印命令,打印匹配的行。
i:插入命令,在行之前插入文本。
a:添加命令,在行之后追加文本。

sed -i ‘s/old_text/new_text/g’ filename
sed -I -e ‘s/old_text1/new_text1/g’ -e ‘s/old_text2/new_text2/g’ filename

在Linux下,可以使用sed命令来批量替换多个文件中的文本内容。
常见使用:sed -i ‘s/old_string/new_string/g’ file_path
比如:
sed -i ‘s/case_cfg.data_return_num/case_cfg.ibe_inch_num/g’ /path/to/files/.txt
在这个命令中:
1)-i:选项表示直接修改文件内容,而不是输出到标准输出。
2)’s/old_text/new_text/g’是sed 命令的替换语法,其中old_text是要被替换的文本,new_text 是替换后的文本,g表示全局替换。(若替换的字符串有’符号,可以使用””包含表达式以作区分)
3)/path/to/files/
.txt 是包含需要替换的文件的路径。可根据实际情况修改路径和文件名的通配符。
注意:在使用 sed -i 命令时,-i 选项会直接修改文件内容,请确保在操作前备份文件以防止意外情况发生。

扩展:
1)替换当前目录子目录下所有文件字符:
命令格式:
find 目标目录 -type f -exec sed -i ‘s/旧字符串/信字符串/g’ {} +
含义:
find 目标目录:指定要递归查找的目录(如.表示当前目录,/home/user/docs表示绝对路径);
-type f:只处理普通文件(排除目录、符号链接等);
-exec sed … {} +:对找到的每个文件执行sed替换({}代表找到的文件,+比;更高效)
sed -i:原地替换(直接修改文件内容,无临时文件);
s/旧字符串/信字符串/g:替换规则(s=substitute,g=global全局替换,不加g只替换每一行第一个匹配项)。

番外5:脚本语法
番外3-1:python语法(需要多学习)
1、语法
(1)双引号
字符串可以用双引号开始和结束,就像用单引号一样。使用双引号的一个好处,就是字符串中可以使用单引号字符。在交互式环境中输入以下代码:

spam = “That is Alice’s cat.”
因为字符串以双引号开始,所以 Python 知道单引号是字符串的一部分,而不是表示字符串的结束。
(2)转义字符
转义字符在编程语言中用于表示一些特殊字符或具有特殊含义的字符序列,转义字符通常在字符串(string)中使用,用于表示特殊字符或字符序列。(此处verilog通用)

(3)原始字符串
在字符串开始的引号之前加上r,使它成为原始字符串。“原始字符串”完全忽略所有的转义字符,打印出字符串中所有的倒斜杠。(转义字符串\无效)

print(r’That is Carol's cat.’)
That is Carol's cat.
因为是原始字符串,Python认为倒斜杠是字符串的一部分,而不是转义字符的开始。
(4)findall方法
findall函数的语法:re.findall(pattern,string,flags=0)
其中,pattern是要匹配的正则表达式模式,string是要搜索的字符串,flags是可选的标志参数,用于控制正则表达式的匹配方式。
findall函数会从字符串中找到所有与模式匹配的子串,并将它们以列表的形式返回。如果没有找到匹配的子串,返回的列表将为空。
import re
string = “Hello, my name is John. My email is john@example.com. Please contact me at john@example.com.”
pattern = r’\b\w+@\w+.\w+\b’ #####匹配电子邮件地址的正则表达式模式
emails = re.findall(pattern, string)
print(emails) #####[‘john@example.com‘, ‘john@example.com‘]
(5)if语句
在Python中,if是一个条件语句,用于根据条件的真假来执行不同的代码块。语法如下:
if condition:
# 如果条件为真,执行这里的代码块
# 可以是一行或多行代码
elif condition2:
# 如果第一个条件为假,但第二个条件为真,执行这里的代码块
# 可以有多个 elif 分支
else:
# 如果以上条件都不满足,执行这里的代码块
2、正则表达式
正则表达式(简称regex)是一种在大多数编程语言中通用的工具,但在具体的实现和用法上可能会有一些差异,总体上是通用的。
普通字符:普通字符按照字面意义进行匹配,例如匹配字母”a”将匹配到文本中的”a”字符。
元字符:元字符具有特殊的含义,例如\d匹配任意一个数字字符,\w匹配任意一个字母字符,mode{n}匹配mode模式n次,.匹配任意一个字符(除了换行符)等。
/为斜杆,\为反斜杠
(1)括号()
圆括号()用于创建子组或捕获组,即用于分组。如:
(\d{3})-(\d{4})表示两个分组,第一个圆括号为三个数字组成,第二个圆括号为四个数组组成。
对于字符串,正则表达式将匹配()内的字符。
(2)竖线|
竖线|是一个元字符,表示逻辑上的或操作。用于在正则表达式中指定多个模式中的任意一个匹配。如下:
正则表达式:[0-9]|a|b ###匹配:数字0-9、a或b

注意:使用|时,会按顺序逐个匹配(从左至右),一旦找到匹配模式,则停止继续匹配。要匹配真正的竖线,需要|。
(3)量词
问号?
问号?是一个元字符,用于指定前面的模式是可选的,可以出现零次或一次。比如:
正则表达式:colou?r ###匹配:color 或 colour

*表示本身。
星号

星号用于指定前面的模式可以重复出现零次或多次,通常用于匹配任意的字符或子模式。比如:
正则表达式:ab
c ###匹配:ac、abc、abbc、abbbc 等。
加号+
加号+用于指定前面的模式必须重复出现至少一次或多次。通常用于匹配至少一个或多个字符或子模式。比如:
正则表达式:ab+c ###匹配:abc、abbc、abbbc 等
花括号{}
花括号{}用于精确地指定前面的模式重复出现的次数,可以指定一个固定的次数、一个范围或一个范围的最小和最大次数。
1、指定固定次数:
正则表达式:a{3} ###匹配:aaa
2、指定范围:
正则表达式:a{2,4} ###匹配:aa、aaa、aaaa
3、指定最小次数:
正则表达式:a{2,} ###匹配:aa、aaa、aaaa 等(至少出现两次)
4、指定最大次数:
正则表达式:a{,3} ###匹配:空字符串、a、aa、aaa(最多出现三次)

问号?等同于{0,1}; *等同于{0,}; +等同于{1,}
(4)贪心和非贪心匹配
贪心匹配和非贪心匹配是指在重复模式匹配时的不同行为。贪心匹配是指量词尽可能多地匹配字符;非贪心匹配是指量词尽可能少地匹配字符(即在量词后加上一个?)。比如:
正则表达式a.b,匹配字符串aabab,贪心匹配,匹配整个字符串aabab
正则表达式a.
?b,匹配字符串aabab,非贪心匹配,匹配字符串ab

注意:?用在量词后,为非贪心匹配。用于字符后,为匹配0次或1次。
(5)字符分类
常用字符分类的缩写代码
缩写字符分类 表示
\d 0到9的任何数字
\D 除0到9的数字以外的任何字符
\w 任何字母、数字或下划线字符
\W 除字母、数字和下划线以外的任何字符
\s 空格、制表符或换行符(匹配“空白”字符)
\S 除空格、制表符或换行符以外的任何字符
比如:
one:正则表达式\d+\s\w+,(\d匹配一个数字,+前面\d出现1次或多次,\s匹配一个空白字符,\w匹配一个字母、数字或下划线,+前面\w出现1次或多次)
two:字符串’12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7 swans, 6 geese, 5 rings, 4 birds, 3 hens, 2 doves, 1 partridge’
three:通过findall匹配多个字符串[‘12 drummers’, ‘11 pipers’, ‘10 lords’, ‘9 ladies’, ‘8 maids’, ‘7 swans’, ‘6 geese’, ‘5 rings’, ‘4 birds’, ‘3 hens’, ‘2 doves’, ‘1 partridge’]
[]和[^]的作用:
[]为匹配括号内任意一个字符,[]内的正则表达式不会被解释。例如[abc]为匹配字符a、b和c;[0-5.]匹配0,1,2,3,4,5和.
[^]为匹配除括号内的字符以外的任意一个字符。例如[^abc]匹配除字符a、b和c以外的任意字符。

(6)美元符号$
$用于匹配字符串的结束位置。当$出现在正则表达式的末尾时,表示匹配必须以字符串的结尾结束。比如:
xyz$匹配以xyz结尾的字符串。
(7)尖括号^
^用于匹配字符串的开始位置。当^出现在正则表达式的开头时,表示匹配必须从字符串的开头开始,比如:
^abc匹配以abc开头的字符串。
(8)通配字符.
.(句号)字符称为通配符,匹配除了换行符之外的所有字符,匹配的是单个字符。
常用的.*为匹配任意文本;(匹配除换行外的所有字符)
.转义为.
(9)正则表达式符号复习
正则表达式符号复习:
?匹配零次或一次前面的分组。
匹配零次或多次前面的分组。
+匹配一次或多次前面的分组。
{n}匹配 n 次前面的分组。
{n,}匹配 n 次或更多前面的分组。
{,m}匹配零次到 m 次前面的分组。
{n,m}匹配至少 n 次、至多 m 次前面的分组。
{n,m}?或
?或+?对前面的分组进行非贪心匹配。
^spam 意味着字符串必须以spam开始。
spam$意味着字符串必须以spam结束。
.匹配所有字符,换行符除外。
\d、\w和\s分别匹配数字、单词和空格。
\D、\W和\S分别匹配出数字、单词和空格外的所有字符。
[abc]匹配方括号内的任意字符(诸如 a、b 或 c)。
[^abc]匹配不在方括号内的任意字符。

(4)边界匹配
\b为匹配单词边界
\B为匹配非单词边界
(10)应用
用法1:

1
2
3
4
.oxu_ixu0_ctl_credit_inc,     	(oxu_ixu0_ctl_credit_inc),                    
.ixu1_oxu_ctl_vld, (ixu1_oxu_ctl_vld),
.ixu1_oxu_ctl_ibe, (ixu1_oxu_ctl_ibe),
.ixu1_oxu_ctl_chn, (ixu1_oxu_ctl_chn),

比如需要删除中间的逗号(,),那么就可以使用正则表达式,如,(.*),表示逗号,和逗号,之间的所有字符,在gvim中,则可以linex,lineys/,(.*),/ \1,/s,那么删除掉中间的逗号,

用法2:

1
:81,101s/\d\+/\=submatch(0)+512/g

表示将81行到101行的,所有数字加512
3、code学习
if name == “main“: 表示如果这个脚本被直接运行而不是被导入到其他脚本中,则执行下面的代码块。否则如果是被导入到其它脚本中,则不会执行

1
2
3
4
5
6
7
8
9
import sys
sys.argv[0]表示脚本名称
sys.argv[1]表示执行脚本时,命令行的第一个参数,sys.argv[2]为第二个参数,依次类推

if __name__ == "__main__":
if(2!=len(sys.argv)):
sys.exit("exit") #命令行参数+1个数不为2,则退出
else:
function_a(sys.argv[1])

class类

1
2
3
def __init__(self,a,b):  #构造函数,创造属性
self.a = a
self.b = b

print打印

1
print(f"Name:{self.name},Age:{self.age}") #f表示是一个f-string带表达式的字符串,{}用来引用变量或表达式。

with使用
通常用于管理资源,如文件操作或数据库连接,以确保资源在使用完毕后被正确释放(关闭)。
用法:with expression as variable:

代码块

例子:
#打开文件并写入内容

1
2
with open("example.txt", "w") as file: #打开文件,并将文件句柄给到file
file.write("Hello, World!")

#文件已经在这里被关闭

1
2
3
4
with open(“example.txt”,”r”) as file: #以读的方式打开文件
for line in file.readlines(): #读取文件的每一行
line = line.strip() #去除每行两侧的空白字符(空格、制表符、换行符等)
split1 = line.split(“=”,1) #以字符中=划分两部分,最多分割1次
1
2
import os #python的标准库
files = os.listdir #用于返回当前目录中文件和子目录的名称列表

列表推导式
根据已有的可迭代对象创建一个新的列表,实现常见的转换和筛选操作。
[expression for item in iterable if condition]
含义:
expression:用来处理和转换每个 item 的表达式,生成新的列表元素。
item:来自可迭代对象的元素。
iterable:可迭代的对象,如列表、元组、集合等。
condition:(可选)用来过滤元素的条件,只有满足条件的元素才会包含在最终的列表中
例子:

1
2
3
numbers = [1, 2, 3, 4, 5]
squared_numbers = [x**2 for x in numbers]
print(squared_numbers) # 输出:[1, 4, 9, 16, 25]
1
2
3
import os
os.path.abspath(file_path) #获取file文件的绝对路径
file_path.replace(‘-’,’_’) #将file_path路径中-替换为_
1
2
3
4
5
6
7
8
import xml.dom.minidom as minidom
dom = minidom.parse(file) #将xml文件解析为DOM(Document object Model)对象。DOM是一种树形结构,可以表示XML文档的层次结构。
root = dom.documentElement #获取DOM对象的根元素,即XML文档的顶层元素。
elm_name = root.getElementsByTagName('elm_name') #从根元素中获取所有名为elm_name的元素(相当于获取根元素下的名为elm_name的子元素)

for num in range(len(root.getElementsByTagName('registerdb:Name'))): #循环根元素下子元素
reg_name = root.getElementsByTagName('registerdb:Name')[num] #得到每个元素内容
reg_name_ = reg_name.childNodes[0].data.lower() #获取文本并将其转换为小写字母形式,获取第一个子节点childNodes的内容data

print(reg_name_1)

字典(Dictionary)
my_dict= {} #创建一个空字典
my_dict = {‘key1’:’value1’, ‘key2’:’value2’} #创建一个带有初始键值对的字典
my_dict[‘new_key’] = ‘new_value’ #添加新的键值对
my_dict[‘key1’] = ‘updated_value’ #更新现有键的值
del my_dict[‘key1’] #删除指定键值对
my_dict.clear() #清空字典
for key in my_dict: #遍历所有键
print(key)
for value in my_dict.values(): #遍历所有值
print(value)
for key, value in my_dict.items(): #遍历所有键值对
print(key, value)

 
code1:提取宏的python脚本
import re
def extract_macros(file_path):
macros = { #字典
‘ifdef’: set(), #set确保内部变量唯一性
‘ifndef’: set(),
‘define’: set()
}

with open(file_path, 'r') as file:
    for line in file:
        line = line.strip() ##去除每行两侧的空白字符(空格,制表符,换行符等)
        #匹配 `ifdef 和 `ifndef
        if line.startswith('`ifdef'):
            macro_name = line.split('`ifdef')[1].strip()
            macros['ifdef'].add(macro_name)
        elif line.startswith('`ifndef'):
            macro_name = line.split('`ifndef')[1].strip()
            macros['ifndef'].add(macro_name)
        #匹配 `define
        elif line.startswith('`define'):
            parts = line.split('`define')[1].strip().split() ##以`define分割两部分,选取第2部分,删除字符串首尾空格,然后以空格分割两部分
            if parts: #非空执行
                macro_name = parts[0]
                macros['define'].add(macro_name)
#返回字典内容
return macros

def main():
file_path = input(“请输入要提取宏的Verilog文件路径:”)
macros = extract_macros(file_path)

print("提取的宏:")
for macro_type, names in macros.items():
    print(f"{macro_type}: {', '.join(names)}")

if name == “main“:
main()
番外3-2:shell脚本
Shell是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。
Shell 脚本(shell script),是一种为 shell 编写的脚本程序。
(1)shell脚本与python一样使用#注释。
(2)shell脚本的开头#!/bin/bash,#!表示其后所用的为哪一种shell。
(3)创建扩展名为sh的文件后,使用chmod赋予文件可执行权限,即chmod +x file.sh。执行文件的命令为./file.sh,此为以二进制的方式执行
(1)shell变量
变量的取名:
1、只能包含数字、字母(大小敏感)和下划线;2、不能以数字开头;3、通常以大写字母表示常量。如:
your_name=”lee” ##注意:变量名和等号之间不能有空格。
echo “${your_name}good” ##加{}以指定变量,不加则取$your_namegood为空
unset your_name ##删除变量your_name
字符串的使用:
字符串是shell中最常用的数据类型,使用单引号’string’和双引号”string”来表示。单引号里的任意字符都会原样输出(字符串中的变量是无效的);双引号里可以出现变量和转义字符。
string=”abcd”
获取字符串长度${#string} #得到字符串string长度
提取字符串长度${string:1:3} #提取字符串”bcd”,第一个索引为0

数组的使用:
在shell中,用括号来表示数组,数组元素用空格符号分割开。如:
数组名={value1 value2 value3 …. valuen}
读取某个元素:${数组名[下标]} #读取数组的某个元素
读取所有元素:${数组名[@]} #读取数组的所有元素
获取数组长度:$[#数组名[@]] #获取数组元素个数
获取数组元素的长度:$[#数组名[n]] #获取第n个数组长度

shell基本运算符:

注意:条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]。

命令替换:命令替换是一种在shell脚本中执行命令并将其输出结果赋值给变量的技术。在shell脚本中,有两种主要的命令替换语法:
使用反引号():在较旧的shell脚本中,命令替换通常使用反引号来实现。例如: result=command`
使用$():较新的shell脚本通常使用$()语法来进行命令替换。例如:
result=$(command)

if使用:if语句用于在特定条件满足时执行特定的代码块。if语句的基本语法如下:
if [ condition ]
then
# Code block to be executed if condition is true
fi
下面是一个简单的示例,演示了如何在Shell脚本中使用if语句:
#!/bin/bash

Check if a number is greater than 10

num=15
if [ $num -gt 10 ]
then
echo “The number is greater than 10.”
fi
在这个示例中,如果变量num的值大于10,则打印”The number is greater than 10.”。
除了基本的if语句之外,还可以使用elif(else if)和else来处理多个条件。以下是一个带有elif和else的示例:
#!/bin/bash

Check if a number is positive, negative, or zero

num=-5
if [ $num -gt 0 ]
then
echo “The number is positive.”
elif [ $num -lt 0 ]
then
echo “The number is negative.”
else
echo “The number is zero.”
fi
在这个示例中,根据变量num的值,会输出数字是正数、负数还是零。

for使用:for循环用于遍历一系列的值或文件。这是处理多个项的一个常用方法。for循环的基本语法如下:
for variable in list
do
# Code block to be executed for each item in the list
done

遍历一个数组
#!/bin/bash

Define an array

fruits=(“apple” “banana” “orange”)

Iterate over the array

for fruit in “${fruits[@]}”
do
echo “Fruit: $fruit”
done
在这个例子中,for循环遍历数组fruits中的每一个元素,并打印出每种水果的名称。

遍历文件中的行
#!/bin/bash

Loop through each line of a file

for line in $(cat myfile.txt)
do
echo “Line: $line”
done

while使用:while循环用于在条件为真时重复执行一段代码块。while循环的基本语法如下:

  1. 基本语法
    while [ condition ]
    do

    Code block to be executed as long as the condition is true

done

例子 1:基本的while循环
#!/bin/bash
count=1
while [ $count -le 5 ]
do
echo “Count: $count”
((count++))
done
在这个例子中,while循环会在count小于等于5时重复执行,每次输出当前的计数值,并递增count。

例子2 :在while循环中,你也可以使用break和continue关键字来控制循环的执行,与for循环类似。
break用于立即退出循环。
continue用于跳过当前迭代并进入下一个迭代。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
count=1
while [ $count -le 5 ]
do
if [ $count -eq 3 ]; then
((count++))
continue # Skip count 3
fi
echo "Count: $count"
((count++))
done

在这个例子中,continue 语句跳过了计数为 3 的情况,输出将是 1, 2, 4, 5。

注意 脚本开发完,需要 chmod 增加执行权限。

番外6:SV 与 C 交互(待完善)

verilog 是一种硬件描述语言,主要用于描述数字电路和硬件行为,而不是数学计算。因此在 verilog 中没有直接支持三角函数的内置函数。可以通过查找表(Lookup Table)和 DPI-C(调用 C 的内置三角函数)来实现三角函数或其它数学计算。

need study

五、Verdi 工具

1 Verdi 的使用

  • view 选项下的 signal list 可以查看双击文件的各个信号;Source 选项下可 find 目标信号/字符串。
  • 选项卡中 D(Driver)表示信号被谁驱动,L(load)表示信号加载谁;可 next/previous 浏览驱动与加载信号。
  • 波形窗口:鼠标左键确定一个时钟点,鼠标滑轮确定另一个时钟点;两点之间即为时钟差值。
  • waveform 工具栏的 Set Search Value 可搜索信号具体数值;位宽过大可截位后再设置,或模糊搜索 *value*。
  • Tools → Waveform compare:比较两个信号。
  • view → grid options:统计上升沿和下降沿次数。
  • 双击波形变化点,可在代码区定位导致该变化的代码。
  • 波形框快捷键:h 显示绝对路径;c/t 高亮信号;k 截尾位宽;代码区按 g 可跳转指定行。
  • 波形框支持 logic operation:logic operation → 逻辑操作 → create/modify。
  • 多波形文件时,”Ctrl + w” 添加信号默认加到主波形;以 Verdi 18 为例,可在 nWave 菜单右侧 “P” 图标(shift+P)设置主波形窗口。
  • nWave 的 “By:” 右侧箭头可按任意值/上升沿/下降沿/指定值等方式检索;选中信号后按 N/n 向前/后搜索。
  • 波形数据右键:2’s complement 为补码;1’s complement 为反码;signed magnitude 为符号位 + 绝对值。
  • SoC 顶层分析:source → Find_Scope,搜索模块后再具体分析。
  • 截位操作:create bus(快捷键 k)截位;expand as sub-bus 可将位宽划分为多份。
  • xxx(示例):编译 DUT → 编译 comp 环境 → elab/接口衔接 → SIM 仿真生成波形。
  • view → signal event report:查看上升沿/下降沿次数。
  • ctrl+f:将变量复制到搜索框中。

1.1 Dump 命令(FSDB)

  • fsdbDumpfile:指定生成 fsdb 文件名
  • fsdbDumpvar:指定 dump 的 instance 和层级(0/1/2…),instance 为 DUT 下模块路径名称
  • fsdbDumpSVA:dump 断言处信号变化
  • fsdbDumpMDA:dump 多维数组
  • fsdbDumpon / fsdbDumpoff:打开/关闭 FSDB dump

不同公司用法位置可能不同,通常写在 initial begin ... end 中:

1
2
3
4
fsdbDumpfile "fsdb_name.fsdb";
fsdbDumpvars 0 "tb_top";
fsdbDumpMDA 0 "tb_top";
fsdbDumpSVA 0 "tb_top";
  • 使用 -gui 1 调试时,左上角可指定调试时间点;选择 absolute time 后输入具体时间即可跳转。
  • 生成 fsdb 文件后,window → Interactive Debug Mode 可直接进入调试模式,无需重新编译。

2 Verdi 波形截取

对仿真波形的截取命令:

1
fsdbextract source.fsdb -bt 10ns -et 20ns -o output.fsdb

source.fsdb 为需要截取的波形;-bt/-et 分别为开始/结束时刻;-o 为输出文件。

3 Verdi 生成文件

  • novas.rc:Verdi 配置文件,包含外观、窗口布局、颜色方案、快捷键绑定、仿真工具集成等配置项。通常位于 Verdi 安装目录下的 share/novas.rc 或类似路径。

4 Verdi timeslot 区域查看

  1. 加入编译/运行选项:
1
2
COMP_OPT += +fsdb+region
SIM_OPT += +fsdb+delta
  1. 进入 Verdi 界面后:view → Expand Delat → Region Mode / Expand/Collapse Time at Cursor,查看变量赋值的时间区域。
  2. 选中变量值跳变处,判断赋值发生在 active 区域(阻塞赋值)或 NBA 区域(非阻塞赋值)。

5 Verdi 自带 VC Apps

  • 不合理波形处双击跳变沿查看驱动;若驱动较多,可看 message 窗口列出所有驱动位置。
  • 使用 VC Apps Toolbox:Design Exploration → Get Force Statement → OK 导出到文件。

6 用例定位与断点调试

declaration 窗口按 ctrl+f 打开搜索窗口,搜索用例后即可断点调试。

六、常用知识点

1、异步FIFO空满判断
1、同步rd_cntr至clk_write时钟域,再与wr_cntr进行对比来决定FIFO是否满;(判断满wr_cntr-rd_cntr)
2、同步wr_cntr至clk_read时钟域,再与rd_cntr进行对比来决定FIFO是否空;(判断空是rd_cntr==wr_cntr)
为什么要这么判断?因为在写时钟域去判断满,当full时,告诉FIFO不要再写了,而在读时钟域去判断空,当empty时,告诉FIFO已经空了,不能再读了。

(2)APB的写与读,写就是写,读就是读,写的对面不是读;读的对面不是写。读和写操作的那面就是Master,而被读和写的就是Slave。
2、回归
在芯片开发过程中,一旦RTL代码做了修改,就有可能引入新的问题,所以这个时候就需要把已经完成了的验证用例重新跑一下,以确保RTL代码的修改没有对已经验证过的功能造成影响,这个过程叫做回归验证(或只叫回归,regress)。

regression:回归

3、硬复位和软复位
(1)软复位:常用于复位逻辑模块,非配置类寄存器值,如count类寄存器。
(2)硬复位:常用于配置寄存器模块,配置信号同步模块.
(3)硬复位有效会导致软复位有效,而通过soc总线配置软复位有效时硬复位不受影响。
4、同步复位和异步复位
1、同步复位是在时钟边沿时刻处理复位信号,即当时钟信号发生变化时,同时对电路的所有触发器进行重置。同步复位常用于时序电路中需要同步控制信号的场景。

同步复位
2、异步复位(或称异步清零)是在任意时刻处理复位信号,即不需要等待时钟信号变化就可以对电路进行复位,一般是通过一个异步或非时序的输入信号来控制。异步复位常用于需要立即停止电路操作的场景,例如发生错误时需要立即停止一个电路的运行。

异步复位
5、秒的单位
秒(Second)是国际单位制(SI)中的基本时间单位。它表示时间的持续或间隔。除了秒本身,还有一些常见的时间单位是基于秒的衍生单位。以下是一些常见的时间单位:
毫秒(Millisecond):1毫秒等于0.001秒,即1秒的千分之一 。
微秒(Microsecond):1微秒等于0.000001秒,即1秒的百万分之一。
纳秒(Nanosecond):1纳秒等于0.000000001秒,即1秒的十亿分之一。
皮秒(Picosecond):1皮秒等于0.000000000001秒,即1秒的万亿分之一。
飞秒(Femtosecond):1飞秒等于0.000000000000001秒,即1秒的千万亿分之一。
等量关系(常用换算):

  • $1\ \mathrm{s} = 10^3\ \mathrm{ms} = 10^6\ \mathrm{us} = 10^9\ \mathrm{ns}$
  • $1\ \mathrm{s} = 10^{12}\ \mathrm{ps} = 10^{15}\ \mathrm{fs}$
  • $1\ \mathrm{ms} = 10^3\ \mathrm{us} = 10^6\ \mathrm{ns}$
  • $1\ \mathrm{us} = 10^3\ \mathrm{ns} = 10^6\ \mathrm{ps}$
    这些单位通常用于测量非常短暂的时间间隔,例如电子器件的响应时间、光信号的传播时间等。对于一般的日常时间测量,秒是最常用的单位。
    6、赫兹的单位
    赫兹(Hz)是国际单位制(SI)中表示频率的单位,表示“每秒发生多少个周期”。

数学关系:

  • $1\ \mathrm{Hz} = 1\ \mathrm{s}^{-1}$
  • $f = \dfrac{1}{T}$(频率 $f$ 与周期 $T$ 互为倒数)

常用单位(科学计数法):

  • $1\ \mathrm{kHz} = 10^3\ \mathrm{Hz}$
  • $1\ \mathrm{MHz} = 10^6\ \mathrm{Hz}$
  • $1\ \mathrm{GHz} = 10^9\ \mathrm{Hz}$

等量关系:

  • $1\ \mathrm{GHz} = 10^3\ \mathrm{MHz} = 10^6\ \mathrm{kHz} = 10^9\ \mathrm{Hz}$
    7、tape-out
    流片(tape-out)是指通过一系列工艺步骤在流水线上制造芯片,是集成电路设计的最后环节。流片即为“试生产”,是把电路设计变为ASIC芯片的过程。
    8、RAM类型
    RAM分为单口RAM、伪双口RAM和双口RAM。
    (1)单口RAM:只有一组数据线和地址线,不能同时读写,读写都使用这个口访问RAM。

(2)伪双端口RAM:一个端口只能写,一个端口只能读(两组数据线和地址线,一组是写数据线,一组是读数据线),一般FIFO内部例化的都是伪双端口RAM。

(3)真双端口RAM:两个读写端口,每个端口可独立发起读或者写,一组是读或者写数据线,另外一组也是读或者写数据线。

9、SDF文件
SDF文件为标准延迟格式(standard Delay Format,时序文件),将时序文件SDF反标到门级网表上,针对带有时序信息的网表进行仿真,则为时序仿真。

TB 中回标(吃)SDF 的常见写法:在 top TB 的 initial 中调用 $sdf_annotate,把 SDF 文件绑定到 DUT 的层级实例上。

示例 1:固定文件名 + 固定实例

1
2
3
4
5
6
7
module tb_top;
dut u_dut();

initial begin
$sdf_annotate("dut.sdf", u_dut, , , "TYPICAL");
end
endmodule

示例 2:用 plusargs 传 SDF 路径(回归更方便)

1
2
3
4
5
6
7
8
9
module tb_top;
dut u_dut();

initial begin
string sdf_file;
if (!$value$plusargs("SDF_FILE=%s", sdf_file)) sdf_file = "dut.sdf";
$sdf_annotate(sdf_file, u_dut, , , "MAXIMUM");
end
endmodule

示例 3:多实例回标(不同模块对应不同 SDF)

1
2
3
4
5
6
7
8
module tb_top;
dut u_dut();

initial begin
$sdf_annotate("dut_top.sdf", u_dut, , , "TYPICAL");
$sdf_annotate("pcie.sdf", u_dut.u_pcie, , , "TYPICAL");
end
endmodule

要点:

  • $sdf_annotate 的第 2 个参数是“层级实例”,必须和门级网表的实例路径一致
  • MTM 选择常见用 MINIMUM/TYPICAL/MAXIMUM,和使用的 SDF 内容、库角有关

八、验证环境

1 验证环境与 Test Bench

验证环境就是各组件组合而成的UVM环境,Test Bench就是整体的验证框架(测试平台)。

2 寄存器模型更新

目录下。

3 编译命令

4 其它

可以在 build.cfg 文件中加入编译选项,可以是 UVM_OPT 或 COMP_OPT 选项,例如:

1
2
3
COMP_OPT += +define+宏定义
UVM_OPT += +define+UVM_HDL_MAX_WIDTH=2048
UVM_OPT += +define+UVM_VERBOSITY=UVM_LOW

在仿真前,先加入编译选项来屏蔽或提供一些后续的操作。如1:加入宏定义以此达到屏蔽一些check的目的(也可提供if来屏蔽);如2:提供force数据的最大位宽;如3:屏蔽一些不必要的UVM_INFO打印。

九、XXX

1 IPD 项目流程

芯片设计项目
DR:Design Ready(DE完成详细设计)
CR:Code Ready(DE完成编码)
BC:Basic Check(DV完成basic case验证)
MF:Major Function(DV完成主要功能验证)VO 75%?

TR4:DV完成(DV完成验证,前端/软件验证项目关闭)V0100% COV 100%
TR5:RTL Freeze
TR6:Tape out

BC交付标准:BT完成主体和高优先级功能测试,完成checklist中BC节点及以前检查项,完成VO checklist刷新,部分模块BC评审遗留项及CHECKLIST等;
MF交付标准:BT完成剩余低优先功能及动态,异常,性能和白盒调试等,完成checklist中MF及以前节点检查,功能覆盖率根据策略收集完成等;

1、 SVN
1、svn up
svn up -r 版本号:更新目录下的所有文件到版本号。
svn up 文件 -r 版本号:更新文件夹下的具体文件到某一版本。
2、svn checkout
svn co 目录路径:将目录路径下的文件check out到本目录文件下。(co=checkout)
等同于svn checkout https://xxxxxx文件
3、svn commit/add/delete
svn ci 文件 目录路径:将文件提交到目录中。(ci=commit)
等同于svn commit -m “文件注释” xxxx文件。

svn add file:增加文件至库上,最后需要svn commit file,则完成上传
svn delete file:删除库上文件,最后需要svn commit file,则完成删除

4、svn switch –relocate
svn switch –relocate 旧svn地址 新svn地址:将svn旧地址更新为svn新地址。
3、svn log
svn log用于查看日志,如svn log -l 5 xxx.file,表示文件xxx.flie最近5次的更新改动。
4、svn更新冲突
svn更新出现冲突的解决方法
Select: (p) postpone, (df) diff-full, (e) edit,
(mc) mine-conflict, (tc) theirs-conflict,
(s) show all options:

(e) edit - change merged file in an editor #直接进入编辑
(df) diff-full - show all changes made to merged file #显示更改至目标文件的所有变化
(r) resolved - accept merged version of file
(dc) display-conflict - show all conflicts (ignoring merged version) #显示所有冲突
(mc) mine-conflict - accept my version for all conflicts (same) #冲突以本地为准
(tc) theirs-conflict - accept their version for all conflicts (same) #冲突以服务器为准
(mf) mine-full - accept my version of entire file (even non-conflicts) #完全以本地为准
(tf) theirs-full - accept their version of entire file (same) #完全以服务器为准
(p) postpone - mark the conflict to be resolved later #标记冲突,稍后解决
(l) launch - launch external tool to resolve conflict
(s) show all - show this list

5、svn st
svn st命令用于显示工作副本中文件和目录的状态,以及这些文件和目录在版本控制系统中的状态。
通过运行 svn st 命令,您可以查看以下信息:
未修改的文件(Unmodified):显示为 “ “(空格)。
已修改的文件(Modified):显示为 “M”。
新添加的文件(Added):显示为 “A”。
已删除的文件(Deleted):显示为 “D”。
有冲突的文件(Conflict):显示为 “C”。
未受版本控制的文件(Unversioned):显示为 “?”。
已被调度删除的文件(Scheduled for deletion):显示为 “!”。
6、svn status
svn status命令与svn st命令等效,都用于查看 Subversion(SVN)版本控制系统中工作副本的状态,显示工作副本中文件和目录的状态信息,以及这些文件和目录在版本控制系统中的状态。
7、svn revert
svn revert命令用于撤销对工作副本中文件或目录的修改,将其还原为最近一次提交到版本库中的状态。
使用 svn revert 命令时,需要指定要还原的文件或目录的路径。如果指定的是文件路径,则该文件的本地修改将被撤销;如果指定的是目录路径,则该目录及其所有内容的本地修改都将被撤销。(本地修改会被永久丢弃)

8、svn diff
svn diff -rA
对比本地和任意版本A的差异
svn diff -rA:B
对比任意历史版本A和任意历史版本B的差异,B相对于A新增或削减的内容

十、Verilog

1 generate

generate主要两种用法,第一是构造循环结构,例如多次实例化某个模块,或者是进行连线;第二种是通过if-generate或者case-generate来在多个代码块之间最多选择一个作为综合的rtl代码。
注意,generate for begin后面的名字是必须要有的,之后仿真器会通过这个标识来生成结构。

2 [A+:B] 或 [A-:B]

一般形式是data[A+:B]或data[A-:B],此时A是base,B是width。
表示的地址范围:
[A+:B] ——AA+B-1;
[A-:B]——–A-B+1
A;
例如:a[3+:4]表示的就是a[6:3],而a[7-:4]表示的就是a[7:4],a[7-:5]表示的是a[7:3]
注:SV中a[7- :4]中间有空格会报错 。这里实际是个 等差数列。可以根据情况 写出通项找规律

注意:B必须是一个常量,若是变量或者运行中才可确认的表达式,编译器无法确认位宽且会报错。

3 同步时钟与异步时钟

(1)同步时钟:数字设计中,频率相同或频率比为整数倍、且相位相同或相位差固定的两个时钟为同步时钟。
(2)异步时钟:时钟相位关系不可控制。

4 跨时钟处理

跨时钟与处理:
多bit传输:异步FIFO
单bit传输:慢到快—延迟打拍;快到慢—握手协议

十二、C语言

1 const 的用法

在C语言中,const是一个关键字,用于声明一个常量。通过使用const关键字,可以将变量声明为只读,即其值在初始化后不能被修改,否则编译器快速报错(好处)。例子:
函数参数中使用 const:

1
void printArray(const int arr[], int size) {//函数体}

在函数参数中使用const指示该参数是只读的,即函数内部不能修改该参数的值。在上述示例中,arr是一个指向int类型的常量数组,函数体内部不能修改数组元素的值。
const 修饰指针:

1
2
3
const int* ptr; //修饰常量,指针指向变量值不可变,指针指向的地址可变。
int* const ptr; //修饰指针,指针不能指向其它地址,指针指向的变量值可变。
const int* const ptr; //常量指针指向常量。

注意:const 关键字只是在编译时提供了一种静态的只读保证,但并不能完全阻止通过其他方式修改变量的值。

2 malloc 内存分配

1
2
头文件:#include<malloc.h>或#include<alloc.h>
原型:extern void *malloc(unsigned int num_bytes);

功能:分配长度为num_bytes字节的内存块
说明:如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。返回类型是void类型,表示未确定类型的指针,C/C++规定,void类型可以强制转换为任何其它类型的指针。当内存不再使用时,应使用free()函数将内存块释放。

1
用法:int *p; p=(int *)malloc(a); //分配a大小字节的内存空间,并强制转换为int类型指针。

十一、小知识

1 质数

质数是指在大于1的自然数中,除了1和本身以外,不再有其它因数。
2、CRC校验
循环冗余校验CRC(Cyclic Redundancy Check)是数据通信领域常用的一种数据传输检错技术。通过在发送端对数据按照某种算法计算出校验码,并将得到的校验码附在数据帧的后面,一起发送到接收端。接收端对收到的数据和校验码按照相同算法进行验证,以此判断接收到的数据是否正确、完整。
模2除法
下面以一个简单示例来展示CRC的计算过程:
以g(x)为CRC-4=X4+X+1为例,此时除数P=10011。假设源数据M为10110011。
在发送端将M左移4位,然后除以P。

发送端CRC计算示例

计算得到的余数就是0100,也就是CRC校验码。将0100附加到原始数据帧10110011后,组成新帧101100110100发送给接收端。接收端接收到该帧后,会用该帧去除以上面选定的除数P,验证余数是否为0,如果为0,则表示数据在传输过程中没有出现差错。

接收端CRC校验示例

的CRC校验码生成:
1、 通过工具生成的Verilog代码,信元头不使用,提取payload
2、 有一个old_crc,其表示上一次进行运算生成的CRC码,即new_crc
3、 new_crc会作为下一次的old_crc,再与flit进行运算(信元存在多个flit),又得到new_crc
4、 直到最后一个flit后,得到的new_crc即为CRC校验码。

接收方得到接收数据后,按照相同的处理方式进行CRC比较即可。(不像网上那样,网上是将数据+CRC一起去与除数进行除,最后得到零则表示正确。依公司而定)

3、ECC校验
xxxxx need study
4、NA
NA:Not Applicable(N/A)表示不适用。
5、Xprop检查
(1)-xprop:X态传播(X-propagation)。
(2)verilog和VHDL RTL仿真器会忽略控制信号上的X不定态,在输出上会分配一个固定的数值。由于缺少X的传播,RTL仿真器往往无法检测到和X态相关的设计问题。
(3)-xprop用来解决控制信号x态暴露问题的。
(4)Standard RTL simulations ignore the uncertainty of X-valued control signals and assign predictable output values. As a result, RTL simulations often fail to detect design problems related to the lack of X-Propagation. However, the same design problems can be detected in gate-level simulations. With X-Propagation support in RTL simulations, engineers can save time and effort in debugging differences between RTL and gate-level simulation results.—user guide
翻译:标准RTL模拟忽略X值控制信号的不确定性,并分配可预测的输出值。结果,RTL模拟经常不能检测到与缺乏X传播相关的设计问题。然而,在门级仿真中可以检测到相同的设计问题。借助RTL模拟中的X传播支持,工程师可以节省调试RTL和门级模拟结果之间差异的时间和精力

注意:若仿真波形出现X态,则需要找到X态传播的根源,以解决X态问题。
6、表格操作
1、表格填充相同数据
鼠标拖动法:在数据区域起始单元格中输入需要的数据,然后移动鼠标至该单元格右下角,会出现黑色的小十字光标,之后拖动到其它单元格就可以实现相同数据填充了。
命令法:在数据区域的起始单元格输入需要的数据,再选择需要的单元格,点击“开始”菜单选项卡,找到“填充”命令,在选择对应填充命令。
2、表格筛选相同元素的行
先选着需要筛选的列,然后在开始选项下选“排序与筛选”下的筛选,之后再根据筛选,选择要筛选的内容即可。

7、stopat方法
在芯片设计中,”stopat” 是一种用于处理内部信号的调试工具。它允许设计工程师在测试环境中指定一个特定的时钟周期或事件,以停止芯片的运行。当芯片达到指定的时钟周期或事件时,它会停止执行,并且可以提供有关芯片状态的信息,例如寄存器的值、信号的状态等。
“stopat” 是一种调试工具,通常在测试环境中使用,而不是在最终产品中。它提供了一种方便的方法来观察和分析芯片的内部行为,但在实际产品中不会使用这种调试功能。
8、X86和X64的区别
X86就是一般用的32位的系统,而X64就是64位的系统。
9、clock_gate的作用
“clock_gate” 是一种电路设计技术,用于控制时钟信号的传递,以实现功耗优化和电路性能改进。它的作用是在特定条件下关闭或打开时钟信号的传递,从而控制电路的工作状态。
以下是 clock_gate 技术的一些作用和优势:
1、功耗优化:通过控制时钟信号的传递,clock_gate 技术可以降低电路的功耗。当电路处于空闲或不需要工作的状态时,可以关闭时钟信号的传递,从而减少电路的功耗。
2、时钟域划分:clock_gate 技术可以用于实现时钟域划分,将电路划分为多个时钟域,并根据需要控制时钟信号在不同时钟域之间的传递。这有助于提高电路的可靠性和时序性能。
3、动态电源管理:通过控制时钟信号的传递,clock_gate 技术可以与动态电源管理(Dynamic Power Management)技术结合使用,实现对电路的动态功耗管理和调整。
4、时钟门控:clock_gate 技术可以用于实现时钟门控(Clock Gating),即根据特定的逻辑条件控制时钟信号的传递。这可以提高电路的能效和性能,并减少功耗。
总的来说,clock_gate 技术通过控制时钟信号的传递,实现功耗优化、时钟域划分、动态电源管理和时钟门控等功能
10、NOC网络

11、热插拔
12、大型组网验证方法
一个大型SOC进行仿真,因服务器资源、CPU核数等原因,仿真时长不可预估,大都可以使用如下方法进行加速仿真。
1)将大型TB进行拆分,分为TB1,TB2,TB3等等,TB1的输出数据存储进文件1,TB2的输入数据为TB1的输出数据,同理TB3的输入数据为TB2的输出数据。
2)由于CPU的客观原因,TB1、TB2和TB3的仿真时间不能同步,即TB间传输可能数据出现X态,因此使用一种同步方法,即TB1、TB2和TB3的数据传输需要保证在一个时间内,当某个TBx仿真快时,需要停止对应TBx的仿真,使其等待较慢的TBy,以此来实现TB间的同步。
13、静态验证和动态验证的区别
1、动态验证方法是在运行设计的情况下对其进行验证的方法。在硬件设计验证中,动态验证通常指的是通过模拟器执行测试程序,检查设计的行为是否符合预期。
仿真验证(Simulation Verification):仿真验证是最常见的动态验证方法之一。通过在仿真器中执行测试程序,驱动输入信号到设计中,并检查输出信号是否符合预期。仿真验证可以帮助验证设计的功能性和时序行为。
1)随机测试(Random Testing):随机测试是一种动态验证方法,通过生成随机输入序列来测试设计的行为。随机测试可以帮助发现设计中的边界条件错误和意外情况。
2)约束随机测试(Constrained Random Testing):约束随机测试是在随机测试的基础上引入约束条件,以确保生成的测试序列覆盖设计的关键功能和场景。约束随机测试可以提高测试覆盖率。
3)事务级建模(Transaction-Level Modeling):事务级建模是一种动态验证方法,通过建模设计的高层事务行为来验证设计的功能性。事务级建模可以帮助简化测试环境的开发和提高仿真效率。
4)功能覆盖率分析(Functional Coverage Analysis):功能覆盖率分析是一种动态验证方法,用于评估测试用例对设计功能的覆盖程度。通过分析功能覆盖率,可以确定测试用例是否足够全面地覆盖了设计的功能。
5)时序分析(Timing Analysis):时序分析是一种动态验证方法,用于验证设计的时序要求是否得到满足。时序分析可以帮助检测时序违例和时序故障。

2、静态验证是一种在不运行设计的情况下对设计进行验证的方法。它通常涉及对设计本身的分析、检查和验证,以发现设计中的潜在问题和错误。
1)代码审查(Code Review):代码审查是一种静态验证方法,通过审查设计文档、源代码或规范来发现潜在的设计错误、风险和不一致性。代码审查可以由团队成员、同行或专门的审查团队进行。
2)静态代码分析(Static Code Analysis):静态代码分析是使用自动化工具对源代码进行分析,以检测潜在的编程错误、代码质量问题和安全漏洞。这种方法可以帮助发现代码中的潜在问题,如未定义的行为、内存泄漏、未使用的变量等。
形式验证(Formal Verification):形式验证是一种基于数学推理的验证方法,用于验证设计是否满足特定的性质或规范。形式验证可以帮助发现设计中的功能性错误、死锁、活锁等问题。
3)模型检查(Model Checking):模型检查是一种自动化验证技术,用于检查设计模型是否满足特定的性质。模型检查通常用于有限状态系统的验证,可以帮助发现设计中的状态空间爆炸问题和性质违反。
4)静态分析工具(Static Analysis Tools):静态分析工具是一种自动化工具,用于对设计文档、规范或代码进行静态分析,以发现潜在的问题和错误。这些工具可以帮助提高设计的质量和可靠性。
14 、查看代码行数
cloc全称:count lines of code,它是一款使用Perl语言开发的开源代码统计工具。
使用方法:cloc *.v或者cloc ./

工具位置:/ic/tools/cloc/1.64/bin/cloc
15、RR代码思路
第一种:固定时隙RR
此RR轮询只需要if…else if….else if….else即可满足,且每次记录下一次需要调度的通道即可,另外也可以使用case….endcase的方法,都是类似的,重点是记录下一次调度的通道。
伪代码:

1
2
3
4
5
6
if(chan == 0)begin
chan = 1;
end else if(chan == 1)begin
chan = 2;
end else if……
end

第二种:RR轮询
此RR轮询每拍需要调度一个有效通道,因此可以按如下步骤进行编码:
1)先判断哪些通道有效(可以与&&上反压等情况)。

1
if(chan1 || chan2 || chan3 ||…..)

2)若存在有效通道,则进入while(1)循环,语句块内可以case….endcase来循环有效通道,找到可输出的有效通道后,记录下一次调度的通道,退出while循环。
//while(1) case 找到有效通道,记录下一次通道,break循环。endcase

1
2
3
4
5
6
7
while(1)begin
case(chan)
chan1:if(chan1…) chan = 2; break; else….
chan2:if(chan2…) chan = 3; break; else….
……….
endcase
end

3)新开始一次。
16、WRR代码思路
以项目的WRR代码来阐述,步骤如下:
1)判断哪些通道有效,哪些通道权重未达到。
2)通道有效且通道权重未达到,则优先调度输出,否则依次调度,占用共享权重。同时记录下次调度的通道。
3)总权重达标可重置权重
以下述代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//0:权重达到上限,1:权重没达到上限
wrr_count_en[0] = (weight_cnt[0] < weight[0]) ? 1'b1 : 1'b0;
wrr_count_en[1] = (weight_cnt[1] < weight[1]) ? 1'b1 : 1'b0;
wrr_count_en[2] = (weight_cnt[2] < weight[2]) ? 1'b1 : 1'b0;
median = ((enable & wrr_count_en) >= 1) ? 2'b11 : 2'b10;
for(int i=0; i<3; i++) begin
if({enable[wrr_last_sel],wrr_count_en[wrr_last_sel]} >= median)begin
weight_cnt[wrr_last_sel] = weight_cnt[wrr_last_sel] + 1'b1;
plane_sel = wrr_last_sel; //调度的通道
wrr_last_sel = (wrr_last_sel >= 2) ? 0 : wrr_last_sel+ 1'b1;
break;
end
wrr_last_sel = (wrr_last_sel >= 2) ? 0 : wrr_last_sel + 1'b1;
end

17、XML文件
XML(可扩展标记语言)是一种用于标记电子文档结构的通用标记语言。它是一种可扩展的标记语言,旨在提高数据的可读性,并且支持数据的结构化描述。XML 主要用于在计算机系统之间传输和存储数据,以及在不同系统之间共享结构化数据。
以下是 XML 文件的一些特点:
标记性:XML 使用标记来描述数据的结构和含义。标记以尖括号 < > 来表示,并且标记可以嵌套在彼此之内,形成层次结构。
可读性:XML 使用文本格式编写,便于人类阅读和创建。XML 文件中的标签和属性通常使用英语单词或自定义标识符,使得数据结构更易于理解。
扩展性:XML 具有灵活性,因为它是可扩展的。用户可以定义自己的标记来适应特定的应用程序或数据需求。
通用性:XML 不依赖于任何特定的领域或平台,并因此被广泛用于各种领域,如Web开发、数据交换、配置文件等。
在软件开发中,XML 文件常用于配置文件、数据交换、Web服务通信等。许多标准的数据交换格式(如RSS、SOAP等)和文档格式(如OpenDocument、SVG等)都使用 XML。
18、两数大小判断

1
2
3
bit[N:0] a,b;
bit[N:0] c;
c = a - b;

若c[N]=0,则a>b;反之若c[N]=1,则a<b;
举例子:a=4’b0011,b=4’b1101,则a-b=4’b0110;
(计算方式1:3-13=-10—–>11010—->10101—->10110——>0110)
(计算方式2:通过二进制计算计算)

1
2
3
0011
1101
0110

第0位:1-1=0;
第1位:1-0=1;
第2位:0-1+2(向第3位借位2’b10)=1;
第3位:0-1-1(被第2位借位)+2(向第4位借位2’b10)=0

19、中断测试
1、断言检查;2、特殊用例检查(一定会出现,直接检查);3、时序模型

20、Gbps计算
1)比特/秒(bps):最基本的单位,表示每秒传输的比特数量。
2)千比特/秒(kbps):表示每秒传输的千比特数量,通常用于测量较慢的互联网连接速度。
3)兆比特/秒(Mbps):表示每秒传输的百万比特数量,通常用于衡量较快的互联网连接速度。
4)千兆比特/秒(Gbps):表示每秒传输的十亿比特数量,通常用于衡量高速互联网连接或局域网(LAN)速度
5)太比特/秒(Tbps):表示每秒传输的万亿比特数量,通常用于描述极高速网络连接的速度。

计算方法:数据量/时间
eg:12个clk中,发送数据量322B,时钟频率1.2GHz
则Gbp的计算公式:(3228)/(120.833)

常用换算(以每 clk 发送 N 字节为例):

  • 时钟周期:Tclk = 1 / Fclk
  • 吞吐(Gbps):Gbps = N * 8 * Fclk / 1e9
  • 包转发率(pps):pps = Fclk / 每包消耗clk数
  • 由包转发率得到吞吐:Gbps = pps * 每包字节数 * 8 / 1e9

Mpps 说明:

  • pps:packets per second,每秒转发包数
  • Mpps:Mega pps,1 Mpps = 1e6 pps
  • Gpps:Giga pps,1 Gpps = 1e9 pps = 1000 Mpps
  • 从 Mpps 换算吞吐(Gbps):Gbps = Mpps * 每包字节数 * 8 / 1000
  • 例:500 Mpps、每包 528 B,则 Gbps = 500 * 528 * 8 / 1000 = 2112 Gbps

288B 入口 data bus 计算例子(假设单入口同一时刻只发一个包,不做多包拼接):

  • bus 宽度:W = 288 B/clk
  • 包头:H = 16 B
  • payload:P 随机 64~512 B
  • 每包消耗 clk 数:Nclk = ceil((H + P) / W)
  • 总线利用率:Util = (H + P) / (Nclk * W)
  • 总线有效吞吐(含包头,Gbps):Gbps_total = (H + P) * 8 * Fclk / (Nclk * 1e9)
  • payload 吞吐(不含包头,Gbps):Gbps_payload = P * 8 * Fclk / (Nclk * 1e9)

Case A:payload = 512B

  • 每包总字节:H + P = 16 + 512 = 528 B
  • Nclk = ceil(528 / 288) = 2 clk
  • 第 1 拍:发送 288 B(满拍)
  • 第 2 拍:发送剩余 528 - 288 = 240 B(未填满,空余 288 - 240 = 48 B)
  • Util = 528 / (2 * 288) = 91.67%
  • pps = Fclk / 2;若 Fclk = 1GHz,则 pps = 500 Mpps
  • 若 Fclk = 1GHz:Gbps_total = 528 * 8 / 2 = 2112 Gbps;Gbps_payload = 512 * 8 / 2 = 2048 Gbps

Case B:payload = 64B

  • 每包总字节:H + P = 16 + 64 = 80 B
  • Nclk = ceil(80 / 288) = 1 clk
  • 1 拍内仅占用 80 B,有 288 - 80 = 208 B 空余
  • Util = 80 / 288 = 27.78%
  • pps = Fclk / 1 = Fclk;若 Fclk = 1GHz,则 pps = 1000 Mpps
  • 若 Fclk = 1GHz:Gbps_total = 80 * 8 = 640 Gbps;Gbps_payload = 64 * 8 = 512 Gbps

数通芯片常说的“线速 / wire speed”:

  • 线速通常指端口在给定协议开销下,能够在不丢包/不阻塞的前提下持续跑满“物理线上的速率”
  • 需要区分三类速率:line rate(物理层)、throughput(链路层/总线层含开销)、goodput(payload有效负载)
  • 对于串行 SerDes:line_rate = baud_rate * bits_per_symbol * lanes
  • 编码与 FEC 会吃掉有效带宽:goodput ≈ line_rate * coding_eff * fec_eff * protocol_eff
    • 64b/66b 编码效率 coding_eff = 64/66
    • 8b/10b 编码效率 coding_eff = 8/10

以以太网口为例的“线速转发率(Mpps)”思路:

  • 以太网 wire 上除了帧本体,还包含 preamble+SFD(8B) 与 IFG(12B)
  • 最小帧长 64B(包含 FCS,不含 preamble/IFG),则 wire 上每帧占用 84B
  • 若端口线速为 R bps,最小包线速转发率:pps_wire = R / (84 * 8)
    • 10GbE:pps_wire = 10e9 / (84*8) = 14.88 Mpps
    • 100GbE:pps_wire = 100e9 / (84*8) = 148.8 Mpps
  • 帧变大(例如 1518B/9KB jumbo)时,pps 下降,但 payload goodput 更接近 line rate

IC 验证环境中(UVM)如何处理“线速/性能”:

  • 明确测量口径:Gbps_total(含包头/协议字段)、Gbps_payload(仅payload)、pps/Mpps、Util,统一放到同一张统计表
  • 激励侧按目标速率发包:
    • 接口按 valid/ready 时序驱动时,用 driver 在每个 clk 根据目标 Util/pps 决定是否发起新包
    • 多流量场景用 virtual sequence 控制包长分布(64~512B)、突发/间隔、不同流的比例
  • 监测侧统计速率:
    • monitor 按握手成功计数:beat_bytes、pkt_cnt、cycle_cnt,按窗口(例如每 N 个 clk)或按包结束计算 Gbps/Mpps/Util
    • 若 DUT 有性能计数器(吞吐/丢包/背压周期),在 scoreboard 对齐“软件计数 vs 硬件计数”
  • 判定“线速”是否达成:
    • 在无背压、输入持续有效的条件下,检查 output pps/Gbps 是否达到理论上限的某个阈值(例如 ≥ 99%)
    • 在有背压/拥塞场景下,检查丢包策略、队列深度、ECN/mark 等是否符合 spec

UVM 统计计算示例(monitor 内按 cycle 统计):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
longint unsigned byte_cnt;
longint unsigned cycle_cnt;
longint unsigned pkt_cnt;

function void sample_beat(int unsigned beat_bytes, bit eop);
byte_cnt += beat_bytes;
if (eop) pkt_cnt++;
endfunction

function void sample_cycle();
cycle_cnt++;
endfunction

function real calc_gbps_total(real fclk_hz);
if (cycle_cnt == 0) return 0.0;
return (byte_cnt * 8.0 * fclk_hz) / (cycle_cnt * 1e9);
endfunction

function real calc_mpps(real fclk_hz);
if (cycle_cnt == 0) return 0.0;
return (pkt_cnt * fclk_hz) / (cycle_cnt * 1e6);
endfunction

21、CDC验证
1、主要通过正向形式化验证工具进行检查(如spyglass)

22、命名规则
1、字母、数字和下划线组成变量名,数字不能为变量名首。

23、fanout扇出
定义:扇出(Fanout) 是指一个信号驱动的下游负载(loads)数量,即该信号连接到多少个其他逻辑单元或寄存器的输入端。

扇出影响:

  1. 时序影响:高扇出信号会导致信号延迟增加,因为驱动大负载需要更大的驱动能力。可能造成setup/hold违例(violation)。
  2. 功耗影响:高扇出信号会增加开关活动(switching activity),从而增加动态功耗。
  3. 综合与布局布线(P&R)影响:综合工具通常会对高扇出信号插入缓冲器(buffer)或驱动单元(drive cell),以改善驱动能力。
    其它:
    (1)反压与跨级反压需注意,反压后数据能否合理吸收进缓存。
    (2)NoC的全称network on chip,NoC网络中的每一个节点是一个单独的路由器模块; 同一类的路由器节点组成一个路由网络。
    (3)filelist中,-f是针对文件夹的,若.v则不用-f(否则报错)

24、UVM/SV 在 IC 验证(数通芯片)常见处理
1)延时(latency)统计口径

  • 常见定义:从 ingress(输入口)sop 握手成功到 egress(输出口)sop/eop 握手成功的时间差
  • 度量单位:cycle 或 time(推荐 cycle,便于跨时钟频率对齐)
  • 关键点:必须有可匹配的 pkt_id(或 {src_port,dst_port,seq_id} 组合键),否则无法做端到端关联

2)包转发率(pps/Mpps)与吞吐(Gbps)统计口径

  • pps = pkt_cnt / time_window_s
  • Mpps = pps / 1e6
  • Gbps_total = byte_cnt_total * 8 / time_window_s / 1e9
  • Gbps_payload = byte_cnt_payload * 8 / time_window_s / 1e9
  • 对于 valid/ready 总线,建议以“握手成功”为准计数(valid && ready),避免把 backpressure 期间的无效周期计入吞吐

3)credit(信用)与背压常见处理

  • credit-based flow control 的基本约束:发送端消耗 credit,接收端在释放 buffer 后返还 credit
  • 检查点:credit 不得为负;返还 credit 不得超过上限;在无异常注入时不应出现 credit 泄漏导致吞吐归零
  • 常见 bug:包长/beat 计算错误导致 credit 扣减不匹配;多 VC/多端口 credit 串线;reset/flush 时 credit 未对齐

4)UVM 里常见落地方式(建议)

  • monitor:负责组包(beat->packet)、标注 sop/eop/byte_cnt/pkt_id、通过 analysis_port 广播
  • scoreboard/perf 组件:做端到端匹配与统计,输出 min/avg/max/p99 latency、Gbps/Mpps/Util,并与 DUT 性能计数器对齐
  • virtual sequence:控制包长分布、突发/间隔、背压/拥塞、不同流比例,形成可重复的性能回归用例

代码片段写在哪儿(建议):

  • perf_win:放在 perf 统计组件(scoreboard 或独立 perf_subscriber)里;monitor 只负责把每个包的 bytes_total/bytes_payload 与 eop 等信息通过 analysis_port 发出来
  • latency_stat:放在 scoreboard/perf 组件里(需要 ingress/egress 两边的观测点做匹配);ingress_monitor/egress_monitor 各自上报 pkt_id,在同一组件里做端到端关联
  • credit_chk:放在“能看到 credit 扣减/返还”的那一侧 monitor 或 scoreboard
    • 若 credit 是接口级显式信号(credit_valid/credit_cnt),放在对应 monitor 里直接做合法性检查
    • 若 credit 是协议语义(由 buffer/ready/返还包隐式表达),放在 scoreboard/perf 组件里根据观测到的行为推导并检查
  • sample_cycle() 的调用位置:放在对应组件的 run_phase/main_phase 里,每个 posedge clk 调一次(或按你定义的 window 节拍调)

具体落地做法(推荐工程结构):

  • env 里创建 perf_stat 常驻组件,并把 ingress/egress monitor 的 analysis_port 都 connect 进来
  • 普通功能 case:perf_enable=1 只做统计并 report(不做阈值失败)
  • 性能 case:perf_enable=1 + perf_check_enable=1 + 配好目标 expected_gbps/expected_mpps/latency_limit,在窗口或结束时判定达标

代码骨架:perf/stat 组件(放 env 下,接收 monitor 广播,并自己跑时钟 tick)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
`uvm_analysis_imp_decl(_ing)
`uvm_analysis_imp_decl(_egr)

class perf_stat extends uvm_component;
`uvm_component_utils(perf_stat)

uvm_analysis_imp_ing#(my_pkt, perf_stat) ing_imp;
uvm_analysis_imp_egr#(my_pkt, perf_stat) egr_imp;

virtual my_if vif;

bit enable;
bit check_enable;
int unsigned window_cycles;
real fclk_hz;
real expected_gbps_min;
real expected_mpps_min;

perf_win win;
latency_stat lat;

function new(string name, uvm_component parent);
super.new(name, parent);
endfunction

function void build_phase(uvm_phase phase);
super.build_phase(phase);
ing_imp = new("ing_imp", this);
egr_imp = new("egr_imp", this);
void'(uvm_config_db#(virtual my_if)::get(this, "", "vif", vif));
void'(uvm_config_db#(bit)::get(this, "", "perf_enable", enable));
void'(uvm_config_db#(bit)::get(this, "", "perf_check_enable", check_enable));
void'(uvm_config_db#(int unsigned)::get(this, "", "perf_window_cycles", window_cycles));
void'(uvm_config_db#(real)::get(this, "", "perf_fclk_hz", fclk_hz));
void'(uvm_config_db#(real)::get(this, "", "perf_expected_gbps_min", expected_gbps_min));
void'(uvm_config_db#(real)::get(this, "", "perf_expected_mpps_min", expected_mpps_min));
if (window_cycles == 0) window_cycles = 10000;
if (fclk_hz == 0.0) fclk_hz = 1.0e9;
win = new(fclk_hz);
lat = new();
win.reset();
lat.reset();
endfunction

task run_phase(uvm_phase phase);
super.run_phase(phase);
if (!enable) return;
forever begin
@(posedge vif.clk);
win.sample_cycle();
lat.sample_cycle();
if (win.cycle_cnt >= window_cycles) begin
report_window();
win.reset();
end
end
endtask

function void write_ing(my_pkt pkt);
lat.on_ingress(pkt.pkt_id);
endfunction

function void write_egr(my_pkt pkt);
win.sample_pkt(pkt.bytes_total, pkt.bytes_payload);
lat.on_egress(pkt.pkt_id);
endfunction

function void report_window();
real gbps;
real mpps;
gbps = win.calc_gbps_total();
mpps = win.calc_mpps();
`uvm_info("PERF", $sformatf("win: Gbps_total=%.3f Mpps=%.3f avg_lat(cyc)=%.3f", gbps, mpps, lat.avg_latency_cyc()), UVM_LOW)
if (check_enable) begin
if ((expected_gbps_min > 0.0) && (gbps < expected_gbps_min)) `uvm_error("PERF", "Gbps below expected")
if ((expected_mpps_min > 0.0) && (mpps < expected_mpps_min)) `uvm_error("PERF", "Mpps below expected")
end
endfunction
endclass

env 里创建与连接(示意)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent agt;
perf_stat perf;

function void build_phase(uvm_phase phase);
super.build_phase(phase);
agt = my_agent::type_id::create("agt", this);
perf = perf_stat::type_id::create("perf", this);
endfunction

function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
agt.ing_mon.ap.connect(perf.ing_imp);
agt.egr_mon.ap.connect(perf.egr_imp);
endfunction
endclass

test 里区分“功能 case / 性能 case”的配置(示意)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class perf_test extends uvm_test;
`uvm_component_utils(perf_test)
my_env env;

function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
uvm_config_db#(bit)::set(this, "env.perf", "perf_enable", 1);
uvm_config_db#(bit)::set(this, "env.perf", "perf_check_enable", 0);
uvm_config_db#(int unsigned)::set(this, "env.perf", "perf_window_cycles", 10000);
uvm_config_db#(real)::set(this, "env.perf", "perf_fclk_hz", 1.0e9);
endfunction
endclass

class perf_line_rate_test extends perf_test;
`uvm_component_utils(perf_line_rate_test)
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(bit)::set(this, "env.perf", "perf_check_enable", 1);
uvm_config_db#(real)::set(this, "env.perf", "perf_expected_gbps_min", 2000.0);
uvm_config_db#(real)::set(this, "env.perf", "perf_expected_mpps_min", 500.0);
endfunction
endclass

5)代码片段:按 cycle 统计 Gbps/Mpps(滑窗/区间统计)

  • 在哪儿调用:
    • sample_cycle():在 perf/stat 组件(或某个 monitor)的 run_phase/main_phase 时钟线程里,每拍 @(posedge clk) 调一次
    • sample_pkt(bytes_total, bytes_payload):在“看到包完整输出”的地方调用,通常是 egress_monitor 组包完成(eop)后,通过 analysis_port 把 pkt 发给 perf/stat,perf/stat 的 write_egr(pkt) 内调用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      class perf_win;
      longint unsigned byte_cnt_total;
      longint unsigned byte_cnt_payload;
      longint unsigned pkt_cnt;
      longint unsigned cycle_cnt;
      real fclk_hz;

      function new(real fclk_hz);
      this.fclk_hz = fclk_hz;
      endfunction

      function void reset();
      byte_cnt_total = 0;
      byte_cnt_payload = 0;
      pkt_cnt = 0;
      cycle_cnt = 0;
      endfunction

      function void sample_cycle();
      cycle_cnt++;
      endfunction

      function void sample_pkt(int unsigned bytes_total, int unsigned bytes_payload);
      pkt_cnt++;
      byte_cnt_total += bytes_total;
      byte_cnt_payload += bytes_payload;
      endfunction

      function real calc_mpps();
      if (cycle_cnt == 0) return 0.0;
      return (pkt_cnt * fclk_hz) / (cycle_cnt * 1e6);
      endfunction

      function real calc_gbps_total();
      if (cycle_cnt == 0) return 0.0;
      return (byte_cnt_total * 8.0 * fclk_hz) / (cycle_cnt * 1e9);
      endfunction

      function real calc_gbps_payload();
      if (cycle_cnt == 0) return 0.0;
      return (byte_cnt_payload * 8.0 * fclk_hz) / (cycle_cnt * 1e9);
      endfunction
      endclass

6)代码片段:端到端 latency(cycle)统计(基于 pkt_id 关联)

  • 在哪儿调用:
    • sample_cycle():同上,在 perf/stat 组件的时钟线程里每拍调用
    • on_ingress(pkt_id):在 ingress_monitor 观测到 sop 握手成功并解析出 pkt_id 时上报;perf/stat 的 write_ing(pkt) 内调用
    • on_egress(pkt_id):在 egress_monitor 观测到同一包 sop 或 eop 握手成功时上报(按你的 latency 定义选择 sop/eop);perf/stat 的 write_egr(pkt) 内调用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      class latency_stat;
      longint unsigned cyc;
      longint unsigned lat_min;
      longint unsigned lat_max;
      longint unsigned lat_sum;
      longint unsigned lat_cnt;
      longint unsigned t_in_by_id[longint unsigned];

      function void reset();
      cyc = 0;
      lat_min = '1;
      lat_max = 0;
      lat_sum = 0;
      lat_cnt = 0;
      t_in_by_id.delete();
      endfunction

      function void sample_cycle();
      cyc++;
      endfunction

      function void on_ingress(longint unsigned pkt_id);
      t_in_by_id[pkt_id] = cyc;
      endfunction

      function void on_egress(longint unsigned pkt_id);
      longint unsigned t0;
      longint unsigned d;
      if (!t_in_by_id.exists(pkt_id)) return;
      t0 = t_in_by_id[pkt_id];
      d = (cyc >= t0) ? (cyc - t0) : 0;
      lat_cnt++;
      lat_sum += d;
      if (d < lat_min) lat_min = d;
      if (d > lat_max) lat_max = d;
      t_in_by_id.delete(pkt_id);
      endfunction

      function real avg_latency_cyc();
      if (lat_cnt == 0) return 0.0;
      return lat_sum * 1.0 / lat_cnt;
      endfunction
      endclass

7)代码片段:credit 计数与合法性检查(按 VC)

  • 在哪儿调用:
    • init_vc(vc, maxc):在 build_phase/end_of_elaboration 或 reset 释放后初始化(从 spec/寄存器/参数拿到每个 VC 的 credit 上限)
    • consume(vc, n):在“发送端握手成功并实际占用 buffer/发送 beat/发送 flit”的时刻调用(例如 valid && ready 的 beat 事件)
    • giveback(vc, n):在“接收端释放 buffer 并返回 credit”的时刻调用(例如观测到 credit_valid,或观测到 credit-return 通道上的 token/报文)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      class credit_chk;
      int unsigned credit_max[int unsigned];
      int signed credit_cur[int unsigned];

      function void init_vc(int unsigned vc, int unsigned maxc);
      credit_max[vc] = maxc;
      credit_cur[vc] = maxc;
      endfunction

      function void consume(int unsigned vc, int unsigned n = 1);
      credit_cur[vc] -= n;
      if (credit_cur[vc] < 0) `uvm_fatal("CREDIT", "credit underflow")
      endfunction

      function void giveback(int unsigned vc, int unsigned n = 1);
      credit_cur[vc] += n;
      if (credit_cur[vc] > int'(credit_max[vc])) `uvm_fatal("CREDIT", "credit overflow")
      endfunction
      endclass