- • 前言
- • SV的优劣
- • 最方便的信号类型(logic)
- • 定义复杂信号的优雅方式(struct)
- • 最省心的状态机利器(enum)
- • 提前发现多驱
- • 让数据结构更清爽的秘诀(typedef)
- • 最常用的批量信号写法(packed array)
- • 模块通信的终极级懒人包(interface)
- • 最优雅的头文件(package)
- • 最懂工程师意图的过程块定义(always)
- • 最安全的决策语句(case)
- • 更可靠的分支判断(unique)
前言
在数字电路设计领域,通常我们认为Verilog是一种设计语言,而SystemVerilog是专门用于验证的语言,不能用于设计。然而,这种观念是不准确的!事实上,SystemVerilog同样适用于设计,在某些方面甚至比Verilog更为便利。SystemVerilog在其设计之初,主要目标之一是更准确地创建可综合(synthesizable)的复杂硬件设计模型,并且使用更少的代码行数来实现这一目标。SystemVerilog-2005 并不是一种独立的语言,它只是 Verilog-2005 之上的一组扩展。IEEE在2009年,已将Verilog正式更名为SystemVerilog。

SV的优劣
优势
- • 大多数主流EDA工具中都被支持,如Vivado、Quarturs、VCS、Modelsim都支持SystemVerilog设计。
- • SystemVerilog是Verilog的扩展,因此兼容Verilog。这意味着现有的Verilog代码可以逐步迁移到SystemVerilog,而且系统设计团队可以逐步采用SystemVerilog,无需彻底改变整个设计流程。
- • SystemVerilog引入了许多高级建模特性,如类、接口、和多态性。这些特性使得对复杂硬件结构进行更抽象和结构化的建模变得更为容易,减少了代码量,提高了可读性和可维护性。
- • SystemVerilog在某种程度上可以帮助减少编码时的拼写错误(coding typo)。
劣势
一些SystemVerilog特性可能会导致生成的硬件设计占用更多的资源。使用某些高级特性可能增加逻辑的复杂性,从而影响实际的硬件资源消耗。
总体分析
总的来说,虽然SystemVerilog存在一些被批评的缺点,我们可以通过在设计上引入一些限制来避免这些问题,就像在编写Verilog时会避免使用for循环来设计电路一样。同时,由于SystemVerilog具备向下兼容性,意味着verilog不需要改任何代码,我们可以轻松地从“.v”文件切换到“.sv”文件,这为团队提供了更灵活的选择。
最方便的信号类型
logic 传统的Verilog对于信号类型有严格的限制,通过reg和wire来描述一个信号。有时候,由于笔误或者混淆,我们可能会误将reg和wire搞混,从而导致编译错误。而SystemVerilog简化了这一过程,所有的信号都可以使用logic进行声明,使得代码更加清晰和一致。
module chip(inputwire in1,inputwire in2,outputreg out1,outputwire out2);
module chip(inputlogic in1,inputlogic in2,outputlogic out1,outputlogic out2);
定义复杂信号的优雅方式
结构体在SystemVerilog中的作用是将多个变量捆绑在一起,形成一个单一的结构。通过结构体,我们可以将相关信号组织在一个统一的名称下,提高了代码的清晰度和可读性。这种方式可以大幅减少代码行数,使得代码更为简洁。同时,结构体的使用也降低了因为声明不匹配而引起的错误的风险。结构体在设计周期较晚才能发现的一些错误,比如模块间不匹配或者遗漏的赋值,也可以得到有效的排除。这些优势共同使得结构体成为SystemVerilog中一种强大的工具。
struct {logic [ 7:0] opcode;logic [31:0] data;logic status;} operation;operation = ’{8’h55, 1024, 1’b0};operation.data = 32’hFEEDFACE;
最省心的状态机利器
在设计状态机时,需要定义状态参数。在状态参数的定义中,如果存在重复,普通的Verilog编写是无法在编译时检测出的。这种问题可能在仿真或者上板测试之后才会被发现。然而,使用enum(枚举)类型可以在编译阶段及时发现这类问题,从而提高了代码的可靠性和调试的效率。
枚举(enum)类型在SystemVerilog中有一些严格的规则:
- • 标签值必须与变量相同大小:枚举类型在定义时会指定每个标签(即枚举值)的二进制表示的大小。在使用时,赋给枚举变量的值必须与该大小一致。这确保了枚举变量的合法取值范围。
- • 只能被赋予枚举列表中的标签:枚举变量只能被赋予在枚举类型定义中列举的标签值。这防止了变量被赋予无效或不相关的值。
- • 可以赋值为相同枚举类型变量的标签值:允许将一个枚举变量赋值为同一枚举类型的另一个变量的标签值。这有助于在程序中传递和比较枚举状态。
- • 其他赋值操作是非法的:除了上述规定的情况,任何其他的赋值操作都是非法的。这种限制确保了枚举类型的严格性,防止不同类型之间的混淆或错误的赋值。
类似下面的代码,WAIT和DONE的值是相同的,显然这是错误的,但是verilog是无法检测出的,而使用enum,在编译时或者通过语法检查工具能立刻检查出来。
verilog写法
parameter [2:0] WAIT = 3'b001, LOAD = 3'b010, DONE = 3'b001;parameter [1:0] READY = 3'b101, SET = 3'b010, GO = 3'b110;reg [2:0] state, next_state; reg [2:0] mode_control; always @(posedge clk ornegedge rstN)if (!resetN) state <= 0;else state <= next_state;always @(state) // next state decodercase (state) WAIT : next_state = state + 1; LOAD : next_state = state + 1; DONE : next_state = state + 1;endcasealways @(state) // output decodercase (state) WAIT : mode_control = READY; LOAD : mode_control = SET; DONE : mode_control = DONE;endcase
SystemVerilog写法
enumlogic [2:0] { WAIT = 3'b001, LOAD = 3'b010, DONE = 3'b001}state, next_state;enumlogic [1:0]{ READY = 3'b101, SET = 3'b010, GO = 3'b110}mode_control;always_ff @(posedge clk ornegedge rstN)if (!resetN) state <= 0;else state <= next_state;always_comb// next state decodercase (state) WAIT : next_state = state + 1; LOAD : next_state = state + 1; DONE : next_state = state + 1;endcasealways_comb// output decodercase (state) WAIT : mode_control = READY; LOAD : mode_control = SET; DONE : mode_control = DONE;endcase
提前发现多驱
在 Verilog 中,多驱(multiple drivers)的问题通常只能在综合后才能被发现,这会延迟问题的暴露,增加调试成本。而在SystemVerilog中,借助其更严格的类型系统和增强的编译检查机制,我们可以在仿真或编译阶段就及时捕捉到多驱错误,极大提升了设计的安全性和开发效率。
module multi_driver_sv;logic a;initial a = 1'b0;always #5 a = ~a; // 第一个驱动always #10 a = 1'b1; // 第二个驱动(编译或仿真直接报错)endmodule
让数据结构更清爽的秘诀
SystemVerilog 引入了 用户自定义类型(User-Defined Types) 的概念,进一步扩展了 Verilog 的类型系统。通过 typedef 关键字,开发者可以基于内建类型或其他自定义类型创建新的类型别名,从而实现更灵活、可读性更强的数据建模方式。
typedef 的优势
简化复杂类型:只需定义一次,即可在多个位置引用,避免重复代码。
增强代码可维护性:修改类型时只需改动 typedef 处,便于统一管理。
保证类型一致性:在模块内保持数据类型一致,减少因类型不一致导致的设计或仿真错误。
提升代码可读性:使用具备语义的类型名(如 addr_t, pkt_t 等),比使用裸类型更直观。
示例
typedeflogic [31:0] bus32_t;typedefenum [7:0] {ADD, SUB, MULT, DIV, SHIFT, ROT, XOR, NOP} opcodes_t; typedefenumlogic {FALSE, TRUE} boolean_t;typedefstruct { opcodes_t opcode; bus32_t data; boolean_t status;} operation_t;
module ALU (input operation_t operation,output bus32_t result);operation_t registered_op;...endmodule
最常用的批量信号写法
在 SystemVerilog 中,数组是构建模块接口、并行信号组、缓存结构等场景中不可或缺的工具。根据数据的排列与存储方式不同,数组可以分为打包数组(packed array)和非打包数组(unpacked array) ,各有特点与适用场景。
打包数组——紧凑高效的数据表示方式(推荐)
打包数组(packed array)是指将多个元素紧密排列在一个连续的向量中,在位级上表现为一个连续的比特流。这使得它特别适合表示位宽固定、结构紧凑的数据,比如数据总线、寄存器堆等。
logic [3:0][7:0] b; // 表示 4 个 8-bit 宽的数据,总共 32 位
实际上,这会被等效视为logic[31:0],即4个打包的8-bit子字段。这种结构在综合和仿真中都更高效,波形显示也更清晰。
非打包数组——灵活的数据容器
非打包数组(unpacked array)允许你存储更复杂的数据类型,如结构体、枚举、甚至是用户自定义类型,是表达「多个对象」时非常自然的选择。
logic [7:0] a [3:0]; // 表示 4 个 8-bit 宽的元素,非打包数组
每个元素在内存中独立存在,更像是C语言中的数组。
import:
- • 仿真工具通常无法一次性完整记录所有非打包数组元素的波形,特别是在元素较多时。这可能导致调试困难或波形不全的问题。
- • 综合器处理非打包数组时资源利用率可能较高,不如打包数组紧凑。
模块通信的终极懒人包
在 SystemVerilog 中,interface就像是一个多功能插座板,你可以把很多根线(信号)统一插进去,然后只用一个插头(接口)连接到模块上,大大简化了模块之间的连线。
传统方式下,如果模块之间要传输很多信号,得一根根地接线(一个端口一个信号),不仅容易出错,维护起来也很麻烦。而有了interface,就像提前打好一束线缆,把相关信号、功能甚至断言都打包在一起,插上即用,清晰又高效。
此外,接口还有一个特别实用的功能叫modport,它可以规定这个插座的插头和插孔哪些能输入、哪些能输出,避免接反,让设计更安全。
示例一:打包信号,简化模块连接(插座板场景)
// 定义一个 interface,打包常用总线信号interface bus_if;logic clk;logic rst_n;logic [31:0] addr;logic [31:0] data;logic valid;logic ready;endinterface// 生产模块module producer(bus_if bus);always_ff @(posedge bus.clkornegedge bus.rst_n) beginif (!bus.rst_n) bus.valid <= 0;elsebegin bus.addr <= 32'h1000; bus.data <= 32'hDEADBEEF; bus.valid <= 1;endendendmodule// 消费模块module consumer(bus_if bus);always_ff @(posedge bus.clk) beginif (bus.valid) begin$display("Received addr: %h, data: %h", bus.addr, bus.data);endendendmodule// 顶层模块实例化module top;logic clk, rst_n; bus_if bus(); // 创建 interface 实例assign bus.clk = clk;assign bus.rst_n = rst_n; producer u_prod(.bus(bus)); consumer u_cons(.bus(bus));endmodule
示例二:modport 限定权限(定义插孔和插头)
interface bus_if; logic clk; logic [7:0] data; modport master (input clk, output data); // 主端写数据 modport slave (input clk, input data); // 从端只读数据endinterfacemodule master(bus_if.master bus); always_ff @(posedge bus.clk) bus.data <= $random;endmodulemodule slave(bus_if.slave bus); always_ff @(posedge bus.clk) $display("Received data: %0d", bus.data);endmodule
最优雅的头文件
在SystemVerilog中,package 是一种用于组织共享定义的机制,能够提升代码的一致性、复用性和可维护性,即便在可综合设计中也广泛适用。
你可以将package看作是更智能的头文件,不仅能存放常量、类型、函数、参数等定义,还能提供清晰的作用域管理,不会像 \include 那样引入重复、难以管理的代码。
在RTL设计中为什么推荐使用package
- • 作用域清晰:不会像 \include` 一样全局展开,易管理,避免重复定义。
- • 代码复用好:多模块共享类型定义和常量,避免重复写。
- • 便于维护:修改统一入口即可更新所有模块。
- • 规范工程结构:更适合团队协作和大型项目。
如何理解作用域清晰
package 提供命名空间(namespace)机制,你可以选择性地引入其中的内容,避免命名冲突。
而 Verilog 的 \include` 文件是直接将文本插入源文件,所有定义会全局可见,容易与其他文件中的名称冲突。
在include中
// file1.vhtypedefenumlogic [1:0] { IDLE, RUN, STOP} state_t;typedefenumlogic [1:0] { IDLE, RUN, STOP} state_n;
// file2.vhtypedefenumlogic [1:0] { RED, GREEN, BLUE} state_t;typedefenumlogic [1:0] { RED, GREEN, BLUE} state_n;
// top.sv`include "file1.vh"`include "file2.vh"module top; state_t current_state; // 编译报错:state_t 重复定义或冲突endmodule
使用package可以避免冲突
// pkg1.svpackage pkg1;typedefenumlogic [1:0] { IDLE, RUN, STOP } state_t;typedefenumlogic [1:0] { IDLE, RUN, STOP } state_n;endpackage
// pkg2.svpackage pkg2;typedefenumlogic [1:0] { RED, GREEN, BLUE } state_t;typedefenumlogic [1:0] { RED, GREEN, BLUE } state_n;endpackage
// top.svimport pkg1::state_t;import pkg2::state_n;module top; state_t current_state; // 使用 pkg1 的 state_t,pkg2 的无干扰endmodule
模块化机制的增强
你可以把一组常量、类型、函数、任务打包在一个 package 中,这就像一个逻辑模块,方便组织和复用。
比如创建一个用于 AXI 接口的工具包:
package axi_pkg;typedefenumlogic [1:0] { OKAY, SLVERR, DECERR } axi_resp_t;functionlogic [31:0] align_addr(inputlogic [31:0] addr);return addr & 32'hFFFFFFFC;endfunctionendpackage
你可以在任何地方这样用:
import axi_pkg::*;axi_resp_t resp;logic [31:0] addr_aligned = align_addr(addr);
package示例:定义一个通用参数和类型包
// file: my_defs_pkg.svpackage my_defs_pkg;// 可综合常量parameterint DATA_WIDTH = 32;parameterint ADDR_WIDTH = 16;// 类型定义typedefenumlogic [1:0] { IDLE, READ, WRITE, ERROR } fsm_state_t;// 组合逻辑函数functionlogic is_even(inputlogic [3:0] val);return ~val[0];endfunctionendpackage
在模块中使用:
import my_defs_pkg::*;module my_module #(parameter WIDTH = DATA_WIDTH) (...); fsm_state_t state;always_combbeginif (is_even(addr))// ...endendmodule
最懂工程师意图的过程块定义
在传统 Verilog 中,always @(*) 是组合逻辑的标准写法,always @(posedge clk) 是时序逻辑的写法,但这些语句本身并不能明确表达“这段代码是组合逻辑”还是“时序逻辑”,综合工具必须猜测工程师的意图,可能导致不符合预期的综合结果。
为了解决这个问题,SystemVerilog 引入了三种专门面向硬件设计的过程块:
always_comb:专用于组合逻辑
always_ff:专用于时序逻辑(如触发器)
always_latch:专用于锁存器,在设计中不推荐
传统 Verilog
这段代码中工具需要推断是组合逻辑还是锁存器逻辑,容易出错。
always @(mode)if (!mode) o1 = a + b;else o2 = a - b;
SystemVerilog
现在工具知道你在写组合逻辑,并会检查你是否遗漏了分支、定义错误、阻塞赋值是否合理等组合逻辑规则。
always_combif (!mode) o1 = a + b;else o2 = a - b;
最可靠的决策语句
在传统 Verilog 中,使用casex和casez时,x/z/? 可能被错误地当作don’t care,这会导致危险的匹配错误,尤其在表达式本身含有未知值时,容易产生不可预期的行为,严重影响仿真和综合一致性。
为了解决这一隐患,SystemVerilog 引入了更安全的匹配方式
case () inside:
- • 取代
casex和casez的使用 - • case 表达式本身中的 x/z 不会被忽略,仍视为实际值,避免匹配混淆
- • 若无匹配,可设置 default 显式报错,增强健壮性
casex
你可能希望它只在 sel 为 1x00(即 1000 或 1100)时才匹配Match A,但如果 sel 是 4'bxx00 呢?casex 会把表达式中的 x 也当成 don't care,结果是:0x00 也会被错误匹配上
logic [3:0] sel = 4'b1x00;casex (sel)4'b1x00: $display("Match A"); // 你可能以为只会匹配1x004'b1000: $display("Match B");default: $display("No Match");endcase
case() inside
case() inside 只允许 case项中有 don’t care,表达式的 x 仍被当作真实值,所以,如果 sel = 4'bxx00,它根本不会匹配 4'b1?00,而是触发default
logic [3:0] sel = 4'b1x00;case (sel) inside4'b1?00: $display("Match A"); // 只匹配高位为1的情况4'b1000: $display("Match B");default: $display("No Match");endcase
更可靠的分支判断
在 SystemVerilog 中,unique、unique0 和 priority 是用于增强 case 语句决策安全性的关键字。它们提供一种更清晰、更可综合的写法,用于取代传统的 parallel_case 和 full_case 合成指令(pragma)。相比传统 pragma,这些关键字具有如下优势:
- • 自动启用 full_case 和 parallel_case 合成行为
- • 运行时检查是否存在多个分支同时匹配(违反 parallel_case)
- • 运行时检查是否有任何情况未被覆盖(违反 full_case)
- • 避免因 case 判断不完整或不唯一而导致综合结果与仿真行为不一致的问题
使用 unique / unique0 / priority 能显著增强 RTL 的可读性、综合可控性和仿真一致性,推荐在正式设计中逐步替代 full_case 和 parallel_case 指令。
always_combuniquecase (state) RDY: ...; SET: ...; GO : ...;endcase
unique
表示所有分支(case 或 if-else)互斥,且必须命中其中一个。也就是说,在任意给定输入下,只能有一个分支为真,否则仿真将产生运行时错误。
logic [1:0] sel;logic out;always_combuniquecase (sel)2'b00: out = 1;2'b01: out = 0;2'b1?: out = 1; // 这会覆盖 10 和 11endcase
如果 sel = 2'b10 和 sel = 2'b11 都同时匹配两个分支(某种条件下被误写成重复匹配),仿真时会 报错,帮助你发现设计上的问题。
unique0
与 unique 类似,但放宽了一个限制:允许没有分支命中(即所有条件都不成立) 。这在某些状态下,允许无操作或空操作的场景下很有用。
logic [1:0] sel;logic out;always_combunique0case (sel)2'b00: out = 1;2'b01: out = 0;2'b10: out = 1;// 2'b11 不匹配任何分支,但不会报错endcase
sel = 2'b00、2'b01、2'b10:命中一个分支,正常;
sel = 2'b11:不命中任何分支,不会报错,但 out 值不确定(可能保留旧值),建议补充default或完善 case。
priority
表示所有判断是优先级顺序执行的,如果多个条件为真,只执行第一个满足条件的分支,不会报错。也就是说,允许多个条件为真,但只取最上面的。
logic [1:0] sel;logic out;always_combprioritycase (sel)2'b1?: out = 1; // 优先匹配,只要最高位为1,无论01/11 都命中2'b01: out = 0; // 即使也匹配,也不会执行(被上面盖住)2'b00: out = 0;endcase
sel = 2'b10 or 2'b11:首先命中 2'b1?,优先级高;sel = 2'b01:命中第二个分支;sel = 2'b00:命中最后一个;
评论区
登录后即可参与讨论
立即登录