IC验证面试常问-4
- 1.11 struct和union的异同
- 1.13 rose 和posedge 的区别?
- 1.14 semaphore的用处是什么?
- 1.15 类中的静态方法使用注意事项有哪些?
- 1.16 initial和final的区别? s t o p , stop, stop,finish的区别
- 1.17 logic,wire和reg的区别?
- 1.18 抽象类和抽象接口是什么?
- 1.19 always@ always_comb always_ff的区别
- 1.20 parameter、define和typedef之间的区别?
- 1.21 solve..before如何使用
- 1.22 mailbox和队列的异同;
- 1.23 动态automa 和静态 static的生命周期;
- 1.24 值传参和引用传参
- 1.25 class和struct的异同,class和module的异同;
- 1.26 对象创建的初始化顺序
- 1.27 new(),coypy()和clone()的区别?
【博客首发与微信公众号《漫谈芯片与编程》,欢迎专注一下】
本篇博客继续介绍IC验证基本功–SV常问问题;
1.11 struct和union的异同
相同点:
struct和union都是用于组合多个不同类型的数据成员,来定义复合数据类型;
声明方式都是使用关键字struct或union,后面跟一对花括号{}来定义数据成员;
不同点:
1.存储方式:
- struct: 结构体中的每个数据成员都有自己独立的存储空间,它们在内存中是连续存储的。结构体的大小是其所有成员大小的总和,加上可能的填充字节(为了对齐)。
- union: 联合体中的所有数据成员共享同一块内存空间,因此在同一时间只能存储一个数据成员的值。意味着在任一时刻,联合体只能存储其中一个成员的值。
2.访问方式
- sruct: 通过成员符.来访问结构体中的每个数据成员;
- union:由于联合体中的所有数据成员共享同一块内存空间,因此只能访问当前存储在联合体中的数据成员。
//在这个结构体中,data、address和enable都有自己的存储空间,可以同时存储不同的值。
struct {logic [7:0] data;logic [2:0] address;logic enable;
} my_struct;//在这个联合体中,data、address和enable共享同一块内存空间,同一时间只能存储一个数据成员的值。如果存储了data,那么address和enable的值就会被覆盖。
union {logic [7:0] data;logic [2:0] address;logic enable;
} my_union;
【总结】
struct用于组合多个不同类型的数据成员,每个成员都有自己的存储空间。
union用于在不同时间存储不同类型的数据成员,但所有成员共享同一块内存空间。
1.13 rose 和posedge 的区别?
在项目中,很少用到$rose这个用法,在这里简单了解下;
- $rose 是一个系统函数,用于检测信号是否从 0 变为 1(即上升沿), $rose(signal) 返回一个布尔值,如果 signal 在当前时间步从 0 变为 1,则返回 1(真),否则返回 0(假)。可以在组合逻辑、任务、函数等中使用。
- posedge:是一个事件控制关键字,用于检测信号的上升沿;posedge signal 通常用在 always 块或事件控制中,当 signal 从 0 变为 1 时触发相应的代码块,用于同步模块;
// 使用$posedge检测时钟上升沿
always @(posedge clk) begin// 在时钟上升沿执行的代码
end// 使用$rose检测信号的正向边沿变化
always @(posedge clk) beginif ($rose(enable)) begin// 在enable信号变为1时执行的代码end
end
1.14 semaphore的用处是什么?
semaphore 是一种同步机制,用于控制对共享资源的访问同步机制。它主要用于多线程或多进程环境中,以防止多个线程或进程同时访问同一个资源,从而避免竞态条件(race conditions)和数据不一致的问题。semaphore 本质上是一个整数计数器,初始值为一个正整数,表示可用资源的数量。
主要用途:
- 资源管理:信号量可以用来管理有限的资源,例如硬件资源或内存空间。通过限制同时访问资源的线程数量,可以防止资源被过度使用或耗尽。
- 线程同步:信号量可以用于同步不同线程或进程的执行,确保某些操作按预期顺序执行。例如,一个线程可以等待信号量的值变为非零,然后获取资源的访问权;而另一个线程可以在使用完资源后释放信号量,使其值增加。
- 互斥访问:信号量可以实现互斥访问,即同一时间只有一个线程可以访问共享资源。这可以通过将信号量的初始值设置为1,并在访问资源前获取信号量、访问结束后释放信号量来实现。
semaphore 的基本操作:
获取(get):线程尝试获取 semaphore 的一个令牌(token)。如果 semaphore 的计数大于 0,计数减 1,线程继续执行;如果计数为 0,线程将阻塞,直到其他线程释放令牌。
释放(put):线程释放 semaphore 的一个令牌,计数加 1,如果有等待的线程,唤醒其中一个线程
program test;import uvm_pkg::*;`include "uvm_macros.svh"class my_thread extends uvm_thread;`uvm_object_utils(my_thread)semaphore my_semaphore;function new(string name = "my_thread");super.new(name);my_semaphore = new(1); // 初始化信号量,允许一个线程访问资源endfunctiontask run();forever beginmy_semaphore.get(); // 获取信号量// 访问共享资源的代码$display("Thread %s is accessing the shared resource", this.get_name());#10ns; // 模拟资源使用时间my_semaphore.put(); // 释放信号量endendtaskendclassinitial beginmy_thread t1, t2;t1 = new("thread1");t2 = new("thread2");t1.start();t2.start();end
endprogram
1.15 类中的静态方法使用注意事项有哪些?
类的静态方法:静态方法是属于类本身而不是类的实例的方法;这意味着你可以在没有创建类的实例的情况下调用静态方法;静态方法通常用于执行与类相关的操作,而不需要访问类的实例数据。
- 访问权限:静态方法不能访问非静态成员,为这些成员需要类的实例才能存在。静态方法只能访问类的静态成员;
- 调用方式:无需实例化:静态方法可以通过类名直接调用,而不需要创建类的实例。例如,my_class::my_static_method();实例化后也可调用;
- 声明周期:静态方法在整个仿真周期都存在;
- 主要用途:常用于工具函数、工厂方法和单例模式;
class My_Class;// 静态变量static int count = 0;// 非静态变量int id;// 构造函数function new(int _id);id = _id;count++;endfunction// 静态方法static function int get_count();return count;endfunction// 非静态方法function void display();$display("ID: %0d, Count: %0d", id, get_count());endfunction
endclassmodule tb;initial begin// 通过类名调用静态方法$display("Initial count: %0d", My_Class::get_count());// 创建类的实例My_Class obj1 = new(1);My_Class obj2 = new(2);// 通过实例调用静态方法$display("Count through instance: %0d", obj1.get_count());// 通过类名调用静态方法$display("Final count: %0d", My_Class::get_count());// 调用非静态方法obj1.display();obj2.display();end
endmodule
1.16 initial和final的区别? s t o p , stop, stop,finish的区别
initial和final是两个不同的过程块,它们用于在仿真开始和结束时执行特定的代码。 s t o p 和 stop和 stop和finish是两个系统任务,用于在仿真过程中停止或结束仿真;
initial 块:
用途:initial 块用于定义仿真开始时执行的代码。每个 initial 块中的代码在仿真开始时并行执行。
执行时机:initial 块中的代码在仿真时间 0 时开始执行。
使用场景:通常用于初始化信号、启动仿真过程、生成激励信号等。
final 块:
用途:final 块用于定义仿真结束时执行的代码。每个 final 块中的代码在仿真结束时并行执行。
执行时机:final 块中的代码在仿真结束时执行,即在 $finish 被调用之后。
使用场景:通常用于清理资源、打印最终结果、执行最后的检查等。
module tb;initial begin$display("Simulation started at time %0t", $time);// 仿真开始时执行的代码#100; // 仿真延迟 100 个时间单位$finish; // 结束仿真endfinal begin$display("Simulation ended at time %0t", $time);// 仿真结束时执行的代码end
endmodule
$stop 和 $finish
$stop:
用途:暂停仿真,进入交互模式。
效果:调用 $stop 会使仿真暂停,用户可以查看当前状态并进行调试。仿真可以在调试器中继续执行。
使用场景:通常用于调试,当仿真达到某个特定状态时暂停,以便检查变量和信号的值。
$finish:
用途:终止仿真。
效果:调用 $finish 会立即终止仿真,退出仿真器。
使用场景:通常用于正常结束仿真,或者在检测到严重错误时提前终止仿真。
module tb;initial begin$display("Simulation started at time %0t", $time);// 仿真开始时执行的代码#50; // 仿真延迟 50 个时间单位$display("Stopping simulation at time %0t", $time);$stop; // 暂停仿真#50; // 仿真延迟 50 个时间单位$display("Finishing simulation at time %0t", $time);$finish; // 结束仿真endfinal begin$display("Simulation ended at time %0t", $time);// 仿真结束时执行的代码end
endmodule//以下方式可以用于调试
$stop(2); // 停止仿真,退出状态为2
$finish(0); // 结束仿真,退出状态为0
1.17 logic,wire和reg的区别?
logic、wire和reg是用于声明信号类型的关键字。
logic:
logic是SystemVerilog中引入的一种新的数据类型,它可以替代传统的reg和wire类型。
logic类型的信号可以用于组合逻辑和时序逻辑,可以被连续赋值(assign)、过程赋值(always)或在模块端口声明中使用。
logic类型的信号默认初始值为x(未知),这与reg类型不同,reg类型默认初始值为0。
wire:
wire类型用于表示组合逻辑信号,即信号的值是由其驱动源(如逻辑门、连续赋值语句)即时决定的。
wire类型的信号只能用于连续赋值语句,不能用于过程赋值语句(如always块)。
wire类型的信号默认初始值为z(高阻态)。
reg:
reg类型用于表示时序逻辑信号,即信号的值可以在时钟边沿触发时更新。但最后综合是不是寄存器还是线网要看具体always语句块;
reg类型的信号可以用于过程赋值语句,如always块,但不能用于连续赋值语句。
reg类型的信号默认初始值为0。
【总结】
logic类型可以用于组合逻辑和时序逻辑,默认初始值为x。
wire类型用于组合逻辑,默认初始值为z。
reg类型用于时序逻辑,默认初始值为0。
module example;logic clk;wire reset;reg [7:0] data;// 组合逻辑assign reset = ~clk;// 时序逻辑always @(posedge clk) begindata <= data + 1;end
endmodule
1.18 抽象类和抽象接口是什么?
OOP特性中的抽象:就是具体由抽象类和抽象接口机制来实现的;
抽象类(Abstract Class)和抽象接口(Abstract Interface)是用于定义一组方法的模板,但这些方法的具体实现留给继承或实现它们的子类或接口实现者。抽象类和抽象接口的主要目的是提供一种方式来强制实现特定的行为,同时允许具体的实现细节有所不同。
抽象类(Abstract Class):
抽象类是使用virtual关键字声明的类,其中至少包含一个纯虚方法(Pure Virtual Method)。
纯虚方法是没有实现的方法,它只有方法声明,没有方法体。纯虚方法使用virtual关键字和= 0来表示。
抽象类不能被实例化,它只能作为其他类的基类,用于继承和实现其纯虚方法。
子类必须实现抽象类中的所有纯虚方法,否则该子类也将成为一个抽象类。
用途:
定义模板:抽象类提供了一个模板,派生类可以继承并实现抽象方法。
强制实现:通过定义纯虚方法,抽象类可以强制派生类实现这些方法。
代码重用:抽象类可以包含通用的方法和属性,这些可以被派生类继承和使用。
// 抽象类示例
virtual class Animal;pure virtual function void makeSound();
endclassclass Dog extends Animal;function void makeSound();$display("Woof!");endfunction
endclassclass Cat extends Animal;function void makeSound();$display("Meow!");endfunction
endclass
抽象接口(Abstract Interface):
抽象接口是使用interface关键字声明的接口,其中可以包含方法声明、变量声明和其他接口声明。
抽象接口中的方法可以是纯虚方法,也可以是有默认实现的方法。
抽象接口不能被实例化,它只能作为其他类或接口的模板,用于继承和实现其方法。
实现抽象接口的类或接口必须实现其中的所有纯虚方法,否则该类或接口也将成为一个抽象类或接口。
用途:
定义协议:抽象接口定义了一组方法签名,实现该接口的类必须提供这些方法的具体实现。
解耦合:抽象接口可以解耦合类的定义和实现,使得类的设计更加灵活和模块化。
// 抽象接口
interface AnimalInterface;// 纯虚方法pure virtual function string make_sound();
endinterface// 实现类
class Dog implements AnimalInterface;// 实现纯虚方法virtual function string make_sound();return "Woof!";endfunction
endclass// 测试平台
module tb;initial beginDog d = new();$display("Dog says: %s", d.make_sound());end
endmodule
1.19 always@ always_comb always_ff的区别
always: 是最通用的过程块,可以用于描述组合逻辑、时序逻辑;
always_comb:always_comb是用于描述组合逻辑的过程块;不需要显式指定敏感列表,编译器会自动推断并生成敏感列表。
always_ff: always_ff是用于描述时序逻辑的过程块,特别是寄存器(flip-flop)。
精细化always_comb/ff 可以帮助工具更好地理解和优化设计中
// 使用 always 描述时序逻辑
always @(posedge clk or negedge reset_n) beginif (!reset_n) beginq <= 0;end else beginq <= d;end
end// 使用 always_ff 描述时序逻辑
always_ff @(posedge clk or negedge reset_n) beginif (!reset_n) beginq <= 0;end else beginq <= d;end
end// 使用 always 描述组合逻辑
always @(a, b, sel) beginif (sel) beginout = a;end else beginout = b;end
end// 使用 always_comb 描述组合逻辑
always_comb beginif (sel) beginout = a;end else beginout = b;end
end
1.20 parameter、define和typedef之间的区别?
parameter、define和typedef是用于定义常量、宏和类型别名的关键字。
parameter:parameter 用于定义常量,通常用于模块或接口中的参数化设计;在编译时确定,并且在整个仿真过程中保持不变。parameter 的作用域通常是模块或接口的本地作用域,localparam 限制为块级作用域。parameter 可以在实例化模块时被重新定义,以实现参数化设计。
module my_module #(parameter int WIDTH =4, parameter string NAME = "default") (input logic [WIDTH-1:0] in,output logic [WIDTH-1:0] out
);// 模块逻辑
endmodule
`define: 用于定义预处理宏的预处理指令,通常用于文本替换;作用域是全局的,在整个设计中使用;适用于全局定义常量或宏函数的情况;
`define WIDTH 8
module my_module (input logic [`WIDTH-1:0] data_in, output logic [`WIDTH-1:0] data_out);// 使用define定义的宏assign data_out = data_in;
endmodule
typedef: 用于定义类型别名的关键字,可以简化复杂的类型声明,或者用于定义新的数据类型;
typedef struct packed {logic [7:0] byte1;logic [7:0] byte2;
} my_struct;typedef enum {RED, GREEN, BLUE} color_t;module top;// 使用 typedef 定义的类型my_struct s;color_t c;initial begins.byte1 = 8'hAA;s.byte2 = 8'hBB;c = RED;$display("s.byte1 = %h, s.byte2 = %h, c = %s", s.byte1, s.byte2, c.name());end
endmodule
1.21 solve…before如何使用
solve…before语句是约束随机化中指定约束之间的优先级的关键字;通常用于解决在约束块中存在多个约束条件时,可能会出现的求解顺序问题。通过使用solve…before,你可以明确指定某些约束应该在其他约束之前求解,从而确保约束求解的正确性和一致性。
class MyClass;rand int a;rand int b;constraint c1 {a inside {[1:10]};b == a * 2;}// 解决依赖关系solve a before b;
endclassmodule tb;initial beginMyClass obj = new();// 随机化对象if (obj.randomize()) begin$display("a = %0d, b = %0d", obj.a, obj.b);end else begin$display("Randomization failed");endend
endmodule
1.22 mailbox和队列的异同;
mailbox主要是用于在不同线程或进程之间传递数据的容器;
queue主要用于单个进程内部的数据管理;
实现方式:
mailbox:是一种基于事件的通信机制,它使用一个共享的内存区域来存储数据。当一个线程向mailbox中放入数据时,它会通知等待在该mailbox上的线程,这些线程会被唤醒并尝试从mailbox中取出数据。
queue:是一种基于数组的数据结构,它可以动态地增长和收缩。queue中的数据是按照它们被插入的顺序存储的,并且可以在任何时候被访问和修改。
使用场景:
mailbox:通常用于需要异步通信的场景,例如在测试平台中,不同的线程可以通过mailbox来交换数据和控制信息。
queue:通常用于需要顺序处理数据的场景,例如在处理器设计中,指令队列可以使用queue来存储待执行的指令。
操作方式:
mailbox:提供了put、get和peek等操作,用于向mailbox中放入数据、从mailbox中取出数据以及查看mailbox中的下一个数据。
queue:提供了push_back、pop_front、front和back等操作,用于向queue的末尾插入数据、从queue的前端取出数据以及查看queue的前端和后端数据。
//mailbox
mailbox mbx;
initial beginmbx = new();forkbeginint data = 10;mbx.put(data); // 发送数据endbeginint data;mbx.get(data); // 接收数据$display("Received data: %0d", data);endjoin
end//queuequeue<int> q;
initial beginq.push_back(10); // 添加数据q.push_back(20);q.push_back(30);while (q.size() > 0) beginint data = q.pop_front(); // 移除并获取第一个数据$display("Popped data: %0d", data);end
end
1.23 动态automa 和静态 static的生命周期;
automatic 和 static 是用于控制变量生命周期的关键字。它们决定了变量在仿真过程中的存在时间和作用域。
默认声明是:automatic类型;声明周期在变量被创建时存在,在执行完毕后销毁;automatic 变量存储在栈内存中,不需要手动管理内存,因为它们在块执行完毕后自动销毁
static变量–需要显示声明:static变量生命周期从仿真开始到仿真结束。static变量存储在静态内存区域;
1.24 值传参和引用传参
值传参:
值传参是指将实际参数的值复制一份传递给函数或任务中的形式参数。
在函数或任务内部对形式参数的修改不会影响到实际参数的值。
值传参适用于需要保护实际参数不被修改的情况,或者需要传递不可变数据的情况。
引用传参:
引用传参是指将实际参数的内存地址传递给函数或任务中的形式参数。
在函数或任务内部对形式参数的修改会直接影响到实际参数的值。
引用传参适用于需要修改实际参数值的情况,或者需要传递可变数据的情况。
值传参:值传递确保了函数或任务内部的操作不会影响外部变量,有助于保持代码的独立性和安全性。
缺点:对于大型数据结构,值传递会导致额外的内存开销和复制时间。
module parameter_pass;// 值传参示例function void pass_by_value(int a, int b);int temp;temp = a;a = b;b = temp;$display("Inside pass_by_value: a = %0d, b = %0d", a, b);endfunction// 引用传参示例function void pass_by_reference(ref int a, ref int b);int temp;temp = a;a = b;b = temp;$display("Inside pass_by_reference: a = %0d, b = %0d", a, b);endfunctionint x, y;initial beginx = 10;y = 20;$display("Before calling functions: x = %0d, y = %0d", x, y);// 调用值传参函数pass_by_value(x, y);$display("After calling pass_by_value: x = %0d, y = %0d", x, y);// 调用引用传参函数pass_by_reference(x, y);$display("After calling pass_by_reference: x = %0d, y = %0d", x, y);end
endmodule
1.25 class和struct的异同,class和module的异同;
class和struct的异同:
默认访问控制:
struct中的成员默认是public的,这意味着它们可以在struct的外部被直接访问。
class中的成员默认是private的,这意味着它们不能在class的外部被直接访问,需要通过class提供的方法来访问。
继承:
class支持继承,允许一个class继承另一个class的成员和方法。
struct不支持继承,它只能包含数据成员,不能包含方法。
动态内存分配:
class支持动态内存分配,可以使用new操作符在运行时创建class的实例。
struct不支持动态内存分配,它通常在编译时被实例化。
封装性:
class提供了更好的封装性,因为它的成员默认是私有的,需要通过方法来访问。
struct的封装性较差,因为它的成员默认是公有的,可以被直接访问。
//================class
class MyClass;int data;function new(int val);data = val;endfunctionfunction void print_data();$display("Data: %0d", data);endfunction
endclassmodule tb;initial beginMyClass obj = new(10);obj.print_data(); // 输出: Data: 10end
endmodule//==============struct
typedef struct {int data;
} MyStruct;module tb;initial beginMyStruct s;s.data = 10;$display("Data: %0d", s.data); // 输出: Data: 10end
endmodule
class 和 module 的异同:
class:主要用于面向对象编程,封装数据和行为,支持继承、多态和随机化。
module:主要用于描述硬件逻辑,包括组合逻辑和时序逻辑,是 SystemVerilog 中的基本构建块。
//=========================class
class MyClass;int data;function new(int val);data = val;endfunctionfunction void print_data();$display("Data: %0d", data);endfunction
endclassmodule tb;initial beginMyClass obj = new(10);obj.print_data(); // 输出: Data: 10end
endmodule//=========================module
module MyModule (input logic clk,input logic rst_n,input logic [7:0] in,output logic [7:0] out
);always_ff @(posedge clk or negedge rst_n) beginif (!rst_n) beginout <= 8'd0;end else beginout <= in;endend
endmodulemodule tb;logic clk;logic rst_n;logic [7:0] in;logic [7:0] out;MyModule uut (.clk(clk),.rst_n(rst_n),.in(in),.out(out));initial beginclk = 0;rst_n = 0;in = 8'd10;#10 rst_n = 1;#10 $display("Output: %0d", out); // 输出: Output: 10endalways #5 clk = ~clk;
endmodule
class 和 struct:
class 用于面向对象编程,支持成员函数、构造函数、继承和随机化。
struct 用于简单数据封装,不支持成员函数和继承。
class 和 module:
class 用于面向对象编程,支持随机化和继承,主要用于描述算法和行为。
module 用于描述硬件逻辑,支持时序逻辑和组合逻辑,是 SystemVerilog 中的基本构建块。
1.26 对象创建的初始化顺序
对象的创建和初始化顺序是非常重要的,尤其是在类中包含多个成员变量和构造函数;
初始化顺序
变量声明:在对象创建之前,所有的变量声明都会被处理。这包括类成员变量、局部变量等。
构造函数调用:在变量声明之后,构造函数会被调用。构造函数用于初始化对象的成员变量。
父类构造函数调用:如果当前类继承自其他类,那么在调用当前类的构造函数之前,会先调用父类的构造函数。这是通过super关键字来实现的。
成员变量初始化:在构造函数内部,可以对成员变量进行初始化。这些初始化语句会按照它们在类定义中的出现顺序执行。
构造函数体执行:在完成所有成员变量的初始化之后,构造函数的体(即花括号内的代码)会被执行。
class Parent;int x;function new();x = 10;$display("Parent constructor: x = %0d", x);endfunction
endclassclass Child extends Parent;int y;function new();super.new(); // 调用父类的构造函数y = 20;$display("Child constructor: y = %0d", y);endfunction
endclassmodule test;initial beginChild c = new();$display("After object creation: x = %0d, y = %0d", c.x, c.y);end
endmodule
在这个示例中,当创建Child类的对象c时,初始化顺序如下:
- 声明Child类的成员变量y。
- 调用Parent类的构造函数,初始化x为10。
- 初始化Child类的成员变量y为20。
- 执行Child类构造函数的体。
这个示例展示了SystemVerilog中对象创建的初始化顺序,包括变量声明、构造函数调用、父类构造函数调用、成员变量初始化和构造函数体执行。
1.27 new(),coypy()和clone()的区别?
new():用于创建一个新的对象实例。它会分配内存,并初始化对象的成员变量。
copy():用于创建一个浅拷贝。它不会分配新的内存,而是复制现有对象的状态。
clone():用于创建一个深拷贝。用于创建一个新的对象实例,并将现有对象的成员变量值复制到新对象中。
class MyClass;int x;int y;function new();x = 10;y = 20;endfunctionfunction MyClass copy(MyClass original);MyClass new_obj = new();new_obj.x = original.x;new_obj.y = original.y;return new_obj;endfunctionfunction MyClass clone();MyClass new_obj = new();new_obj.x = x;new_obj.y = y;return new_obj;endfunction
endclassmodule test;initial beginMyClass obj1 = new();MyClass obj2;obj2 = obj1.copy(); // 使用copy方法复制对象$display("obj2.x = %0d, obj2.y = %0d", obj2.x, obj2.y);obj2 = obj1.clone(); // 使用clone方法复制对象$display("obj2.x = %0d, obj2.y = %0d", obj2.x, obj2.y);end
endmodule
至此,SV的基本功常见问题就先到此结束了,接下来介绍IC验证的基本功–UVM常见问题。