SystemVerilog FPGA 入门 – 第 1 节 – Verilog 基础语法及其结构

Verilog 基础语法及其结构

参考资料:Verilog Tutorial

本节可能导致学习的混乱,若看不明白或有疑问,请忽略本节部分内容并移至下一节直接开始学习 SystemVerilog。

上节写了一个简单的计数器模块 counter.sv。

module counter (
    input  logic       clk,
    input  logic       rst,
    output logic [3:0] count
);

    always_ff @(posedge clk) begin
        if (rst) count <= 4'd0;
        else count <= count + 4'd1;
    end

endmodule

这个代码使用 System Verilog 语法定义了一个计数器模块。模块有三个端口:时钟输入 clk、复位输入 rst 和 4 位输出 count。在时钟上升沿触发的 always_ff 块中,如果复位信号为高电平,计数器清零,否则计数器加一。

现在看一个 Verilog 风格的代码:

module ctr (
    input               up_down,
                        clk,
                        rstn,
    output reg [2:0]    out
);

    always @ (posedge clk)
        if (!rstn)
            out <= 0;
        else begin
            if (up_down)
                out <= out + 1;
            else
                out <= out - 1;
        end
endmodule

同样的,使用 module 定义了一个计数器模块 ctr,包含时钟输入 clk、复位输入 rstn、方向控制输入 up_down 和 3 位输出 out。在时钟上升沿触发的 always 块中,如果复位信号为低电平,计数器清零,否则根据 up_down 信号决定计数器是加一还是减一。

通过简单对比,不难发现语法中 begin 和 end 是相似的,都是用来包裹多条语句的块。always_ff 是 System Verilog 的语法,用于描述时序逻辑,而 always 是 Verilog 的语法,也可以描述时序逻辑,但在 System Verilog 中推荐使用 always_ff 来明确表示这是一个时序逻辑块。声明输出信号时,System Verilog 使用 logic 类型,而 Verilog 使用 reg 类型。声明输入信号时,System Verilog 使用 logic 类型,而 Verilog 使用默认的 wire 类型。

不要以为…

“Verilog 是顺序执行的”

借用参考资料所说,Verilog 与 C 语言等的执行方式截然不同。Verilog 中 assign 语句与 always 是并行的。只要有信号发生变化,assign 语句就会立即执行,而 always 块则会在满足触发条件时执行。Verilog 的执行是基于事件驱动的,而不是像 C 语言那样顺序执行。

例如,下面的代码:

always @(posedge clk) a = 1;
always @(posedge clk) b = a;

只要 clk 上升沿触发,a 会被赋值为 1,而 b 会被赋值为 a 的旧值(在这个时钟周期之前的值),不能保证 b 也变成 1。

Verilog 涉及非阻塞赋值(<=)和阻塞赋值(=)的区别。非阻塞赋值用于时序逻辑,允许在同一个时钟周期内同时更新多个寄存器,而阻塞赋值用于组合逻辑,按顺序执行语句。

“wire 会存储状态”

wire 在 Verilog 里表示连线,不是变量,也不是寄存器。它必须被某个东西持续驱动。如果 wire w 没有被任何东西驱动,它的值就是高阻态(z)。同样,导线在现实中也不是记忆器件,它们只是连接不同模块的通路。

“认为 for 会依次循环”

for 循环在 Verilog 中是一个综合工具,它会被综合为硬件电路,而不是像软件语言那样顺序执行。for 循环的迭代次数必须是已知的常量,不能依赖于运行时的变量。

比如:

always @(*) begin
    for (i = 0; i < 8; i = i + 1)
        y[i] = a[i] & b[i];
end

它不是“用一个与门算 8 个周期”,而是描述 8 个与门同时工作。如果循环 100 次,就会倾向于生成 100 份逻辑,面积会变大。其对应的是硬件的复制而不是硬件的顺序执行。

Hello World

Verilog 显示一个 Hello World 几乎没用,不过可以认识一下基础的结构。

module tb;
    initial 
        $display("Hello World!");
endmodule

这个 Hello World 模块 tb 使用 initial 块在仿真开始时执行 $display 语句,输出 Hello World! 到仿真控制台。

写好了之后,用 verilator 直接编译为一个可执行的仿真程序:

cd src
verilator --binarv ./hello.v

生成一个 obj_dir 文件夹,里面有一个 Vhello 可执行文件,运行它:

cd obj_dir
./Vhello

执行得到结果:

Hello World!
- S i m u l a t i o n   R e p o r t: Verilator 5.032 2025-01-01
- Verilator: end at 0s; walltime 0.001 s; speed 0.000 s/s
- Verilator: cpu 0.001 s on 1 threads; alloced 57 MB

现在使用这个 verilator 比较麻烦,并且侵占了 src 目录,为了隔离并方便地编译 Verilog 文件,可以添加一个 just 项:

run-v FILE:
    verilator --binary --top-module tb -Mdir build/vobj src/{{FILE}}
    ./build/vobj/Vtb

这样可以使用:

just run-v hello.v

来编译并运行 hello.v 文件。

注释、缩进与空格

与大多数语言一样,Verilog 也支持单行注释 // 和多行注释 /* ... */。在编写 Verilog 代码时,建议遵循良好的缩进和注释习惯,以提高代码的可读性和可维护性。

Verilog 中空格除字符串中大多可忽略。

reg [8*9:1] str = "114514ikz";

[8*9:1] 表示 9 个字节的寄存器,8*9 是位宽,1 是最低位索引,等同于 [8*9-1:0]。在 Verilog 中,位宽的表示方式可以使用表达式计算。

运算符

x = ~y; // 按位取反
x = y & z; // 按位与
x = y ? z : w; // 三元运算符

一元运算符

  • ! 逻辑非运算符,用于取反布尔值。
  • ~ 按位取反运算符,用于对每一位进行取反操作。
  • &、~& 归约与、归约与非运算符,将操作数的所有位归约为一位。
  • |、~| 归约或、归约或非运算符,将操作数的所有位归约为一位。
  • ^、~^、^~ 归约异或、归约同或运算符,将操作数的所有位归约为一位。
  • + 一元加运算符,通常用于表示正数。
  • - 一元减运算符,通常用于表示负数。

二元运算符

  • + 加法运算符,用于两个数值的加法操作。
  • - 减法运算符,用于两个数值的减法操作。
  • * 乘法运算符,用于两个数值的乘法操作。
  • / 除法运算符,用于两个数值的除法操作。
  • % 取模运算符,用于两个数值的取模操作。
  • <<、>>、<<<、>>> 左移、右移、算术左移、算术右移运算符。
  • &、|、^、~^、^~ 按位与、或、异或、同或运算符。
  • &&、|| 逻辑与、逻辑或运算符。
  • ==、!= 相等、不等运算符;如果操作数包含 x 或 z,结果可能为 x。
  • ===、!== 全等、不全等运算符,会把 x 和 z 作为可比较的值。
  • < 小于运算符,用于比较两个数值的大小关系。
  • > 大于运算符,用于比较两个数值的大小关系。
  • <= 小于等于运算符,用于比较两个数值的大小关系。
  • >= 大于等于运算符,用于比较两个数值的大小关系。

其中 <= 在时序逻辑中还表示非阻塞赋值,具体含义取决于它所在的语法位置。

三元运算符

  • ? : 条件运算符,用于根据条件选择两个值中的一个。

条件复用器(Conditional Multiplexer Logic)是数字电路中常用的组件,它根据选择信号的值来选择输出。三元运算符在 Verilog 中可以用来实现条件复用器的功能。例如:

hit = (sel) ? data1 : data0;

数

Verilog 模拟器数字表示方法为 <位宽>'<进制><数值>,其中 <位宽> 是数字的位数,<进制> 可以是 b(二进制)、o(八进制)、d(十进制)、h(十六进制),而 <数值> 是具体的数字。有时位宽可以省略。

x = 4'b1010; // 二进制表示的 4 位数
y = 8'd255;  // 十进制表示的 8 位数
z = 16'h1A3F; // 十六进制表示的 16 位数
w = 8'o77; // 八进制表示的 8 位数

注意不要使用 0b、0o、0x 这样的前缀表示二进制、八进制和十六进制。如需要表示负数,可以使用补码表示法,例如 -8'sd5 表示一个 8 位的有符号数,其值为 -5。如写作 -8'd5,则表示一个 8 位的无符号数,其值为 251(即 256 – 5)。

字符串

Verilog 中的字符串是由双引号包围的字符序列。字符串可以包含 ASCII 字符,也可以包含转义字符。Verilog 中的字符串通常用于显示信息或作为参数传递给模块。

reg [8*9:1] str = "114514ikz"; // 定义一个 9 字节的寄存器,并初始化为字符串 "114514ikz"

不要用 - 作标识符

不允许使用 - 作为标识符的一部分,因为它会被解释为减号运算符。例如,my-variable 会被解析为 my 减去 variable,而不是一个单独的标识符。正确的做法是使用下划线 _ 或驼峰命名法来命名标识符,例如 my_variable 或 myVariable。

wire my_variable; // 正确的标识符命名
wire my-variable; // 错误的标识符命名,编译器会报错

此外,Verilog 中的标识符必须以字母或下划线开头,后续可以包含字母、数字和下划线。标识符区分大小写,因此 myVariable 和 myvariable 是两个不同的标识符。

其余诸多关键词也不能作为标识符,在此不多赘述。

module

module 与 endmodule 用于定义一个模块,模块是 Verilog 的基本构建块。模块可以包含端口、参数、信号、实例化其他模块以及描述逻辑行为的语句。

// Basic module syntax with port list
module <name> ([port_list]);
    // Variable declarations (wires, regs, integers, etc.)
    // Dataflow statements (assign, always, initial blocks)
    // Function and task definitions
    // Sub-module instantiations
endmodule

// A module can have an empty portlist (typically for testbenches)
module testbench_top;
    // Testbench logic without external ports
endmodule

一个基础的模块定义包括模块名、端口列表、变量声明、数据流语句、函数和任务定义以及子模块实例化。模块可以有一个空的端口列表,通常用于测试平台(testbench)。

示例:同步低有效复位 D 触发器

graph LR d["d"] --> dff rst_n["rst_n"] --> dff clk["clk"] --> dff dff["dff"] --> q["q"] style dff fill:#6fa8dc,stroke:#3d85c6,stroke-width:1px,color:#ffffff

此模块有三个输入:

  • d:数据输入
  • clk:时钟输入
  • rst_n:低有效复位输入

一个输出:

  • q:数据输出
module dff (
    input       d,
    input       clk,
    input       rst_n,
    output reg  q
);

    always @(posedge clk) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

input 省略类型时默认是 wire,output reg 表示输出信号 q 是一个寄存器类型。always @(posedge clk) 块在时钟上升沿触发,如果复位信号为低电平(!rst_n),则输出 q 清零,否则将输入 d 的值传递给输出 q。

“低有效复位”意味着当复位信号为低电平时,模块处于复位状态。

flowchart LR subgraph COMB["组合逻辑(复位选择)"] direction TB RSTN[/"rst_n"/] -->|"S"| MUX["RTL_MUX<br/>q_i"] ZERO(("常数<br/>1'b0")) -->|"I0:S=0 时选中"| MUX end subgraph SEQ["时序逻辑(同步寄存器)"] direction TB CLK[/"clk"/] -->|"C"| REG["RTL_REG_SYNC<br/>q_reg"] RSTN -->|"RST"| REG end MUX -->|"O"| REG REG -->|"Q"| Q[/"q"/] REG -.->|"I1:反馈 Q"| MUX

上面这张图展示了 dff 模块对应的 RTL 结构,把它拆成组合逻辑和时序逻辑两部分来理解:

  • 组合逻辑(复位选择):核心是一个多路选择器 RTL_MUX,其选择端 S 由复位信号 rst_n 驱动,用于决定寄存器下一个时钟沿要写入的值。
  • 时序逻辑(同步寄存器):RTL_REG_SYNC 是实际存储状态的寄存器,C 接时钟 clk,RST 接复位信号 rst_n,在 clk 上升沿把输入 O 采样到输出 Q。
  • MUX 的输出 O 接到寄存器的数据输入,寄存器的 Q 又反向反馈回 > MUX 的 I1,构成反馈路径。
  • 输出 q 直接来自寄存器的 Q,因此 q 只能是 0 或保持值,属于寄存器输出。

对应代码 if (!rst_n) q <= 1'b0; else q <= d;:当 rst_n = 0(低有效复位)时,S = 0 选中 I0,即常数 1'b0,于是时钟沿把 0 写入寄存器;当 rst_n = 1 时,复位路径被关闭,寄存器不再被强制清零。

注意:图中 I1 画的是“反馈 Q”(即保持当前值),而代码里非复位分支写入的其实是输入 d。二者表达的是同一个选择结构的两种画法:把 I1 换成 d 即与代码完全一致,画成反馈 Q 则表示“复位外的其他情况保持原值”。

模块的目的

模块类似于函数,封装了特定的功能,可以被实例化和复用。模块的设计目标是实现特定的逻辑功能,同时提供清晰的接口,使得不同模块之间可以通过端口进行通信。模块化设计有助于提高代码的可读性、可维护性和可扩展性。

比如说,上述 dff 可以被集成为一个移位寄存器。

module shift_reg (
    input       clk,
    input       rst_n,
    input       d,
    output reg  q
);

    wire [2:0] q_net;

    dff u0 (
        .d(d),
        .clk(clk),
        .rst_n(rst_n),
        .q(q_net[0])
    );
    dff u1 (
        .d(q_net[0]),
        .clk(clk),
        .rst_n(rst_n),
        .q(q_net[1])
    );
    dff u2 (
        .d(q_net[1]),
        .clk(clk),
        .rst_n(rst_n),
        .q(q_net[2])
    );
    dff u3 (
        .d(q_net[2]),
        .clk(clk),
        .rst_n(rst_n),
        .q(q)
    );
flowchart LR clk([clk]) d([d]) rst_n([rst_n]) subgraph u0["dff u0"] direction TB u0_clk[clk] u0_d[d] u0_rstn[rstn] u0_q[q] end subgraph u1["dff u1"] direction TB u1_clk[clk] u1_d[d] u1_rstn[rstn] u1_q[q] end subgraph u2["dff u2"] direction TB u2_clk[clk] u2_d[d] u2_rstn[rstn] u2_q[q] end subgraph u3["dff u3"] direction TB u3_clk[clk] u3_d[d] u3_rstn[rstn] u3_q[q] end clk --> u0_clk & u1_clk & u2_clk & u3_clk rst_n --> u0_rstn & u1_rstn & u2_rstn & u3_rstn d --> u0_d u0_q -->|q_net#91;0#93;| u1_d u1_q -->|q_net#91;1#93;| u2_d u2_q -->|q_net#91;2#93;| u3_d u3_q --> q([q])

模块一般按照 “底层” 到 “顶层” 的顺序进行设计。底层模块放上面实现基本功能,其次写中间模块,然后再写主模块。

注意:模块中不能再定义模块,Verilog 不支持嵌套模块。

端口

Verilog 支持三种端口类型:

端口类型 描述 使用场景
input 输入端口,用于接收外部信号 时钟、数据输入、控制、复位
output 输出端口,用于向外部输出信号 计算结果、数据输出
inout 双向端口,可以作为输入或输出使用 数据总线、I2C / SPI 数据线、三态逻辑总线

语法

input  [net_type] [range] list_of_names;    // 输入端口
inout  [net_type] [range] list_of_names;    // 双向输入输出端口
output [net_type] [range] list_of_names;    // 由 wire 驱动的输出端口
output [var_type] [range] list_of_names;    // 由变量驱动的输出端口(reg)

示例

module example (
    input wire      clk,    // 时钟输入(显式 wire 标注)
    input           en,     // 使能输入(隐式 wire 标注)
    inout           rw,     // 读写控制(隐式 wire 标注)
    input [15:0]    data,   // 16 位双向数据总线
    output          int     // 中断输出(隐式 wire 标注)
);
    // 模块实现
endmodule

声明的标识符默认为无符号的(unsigned),若要声明为有符号的(signed),需要显式地使用 signed 关键字。

module example_signed (
    input  signed [7:0]  a, // 有符号 8 位输入
    output signed [7:0]  b  // 有符号 8 位输出
);
    // ...
endmodule
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇