Verilog HDL学习笔记(三)| 第3章 基本概念(上)——词法约定与数据类型

发布时间:2026/9/2 5:51:15
Verilog HDL学习笔记(三)| 第3章 基本概念(上)——词法约定与数据类型 3.1 词法约定Lexical Conventions词法约定是任何编程语言最基础的书写规则——包括空白符怎么用、注释怎么写、数字怎么表示、名字怎么起。Verilog的语法和C语言非常相似有C语言基础的同学会感觉很亲切。3.1.1 空白符White Space空白符包括空格␣、制表符Tab键 →\t、换行符Enter键 →\n。规则Verilog编译器在编译时会忽略空白符它们仅用于分隔标识符提高代码可读性。// 以下三种写法在Verilog编译器看来是完全等价的 // 写法1紧凑不推荐可读性差 module M(a,b,c);output c;input a,b;and(c,a,b);endmodule // 写法2有空格和换行推荐 module M(a, b, c); output c; input a, b; and (c, a, b); endmodule // 写法3大量空白编译器也会忽略 module M ( a , b , c ) ; output c ; input a , b ; and ( c , a , b ) ; endmodule特别注意字符串用双引号括起来的内容中的空白符不会被忽略。Hello World和HelloWorld是两个不同的字符串。3.1.2 注释Comments注释是写给人看的编译器会完全忽略。Verilog支持两种注释风格// // 文件名full_adder.v // 功能一位全加器 // 作者张三 // 日期2024年1月1日 // module full_adder(sum, c_out, a, b, c_in); // ----- 端口声明 ----- output sum, c_out; // sum是和c_out是进位输出 input a, b, c_in; // a和b是加数c_in是进位输入 /* * 以下是内部逻辑实现 * 使用两个异或门和两个与门加一个或门 */ wire s1, c1, c2; // 内部连线 xor (s1, a, b); // s1 a ^ b and (c1, a, b); // c1 a b xor (sum, s1, c_in); // sum s1 ^ c_in and (c2, s1, c_in); // c2 s1 c_in or (c_out, c2, c1); // c_out c2 | c1 endmodule重要限制多行注释/* ... */不允许嵌套/* 这是 /* 嵌套注释 */ 错误 */ // ❌ 编译报错 /* 这是 // 合法注释 */ // ✅ 单行注释可以嵌套在多行注释中3.1.3 操作符OperatorsVerilog的操作符按操作数的个数分为三类// 三种操作符的对比 reg a, b, c, d, sel; initial begin a 1b1; b 1b0; c ~a; // 单目c 0 d a b; // 双目d 0 y sel ? a : b; // 三目如果sel1则ya否则yb end3.1.4 数字声明Number SpecificationVerilog中数字的表示方法非常灵活这是初学者需要重点掌握的内容。① 指明位数的数字Full Specification语法格式位宽基数格式数值// 指明位数的数字示例 4b1111 // 4位二进制数 1111十进制15 8hA5 // 8位十六进制数 A5十进制165 12d255 // 12位十进制数 255二进制 000011111111 16o1234 // 16位八进制数 1234② 不指明位数的数字Implicit Specification如果不指定位宽Verilog会使用默认位宽通常为32位具体取决于仿真器和平台23456 // 32位十进制数 23456 hC3 // 32位十六进制数 C3高位补0 o21 // 32位八进制数 21不指明位数的数字在拼接操作中是非法的因为拼接操作必须知道每个操作数的确切位宽。③ X 和 Z 值Verilog用两个特殊符号来表示不确定状态和高阻状态这是硬件描述语言区别于普通编程语言的重要特征12h13x // 12位十六进制数最低4位为x不确定 6hx // 6位十六进制数全部6位为x即 6bxxxxxx 32bz // 32位全部为z高阻态 4b10?? // ? 是 z 的另一种表示等价于 4b10zz位宽扩展规则如果最高位是 0、x 或 zVerilog会分别使用 0、x 或 z填充更高的位如果最高位是 1Verilog会使用0填充更高的位4b1010 // 就是 1010 8bz // 扩展为 8bzzzzzzzz全部用z填充 8b1 // 扩展为 8b00000001最高位是1用0填充④ 负数和下划线-6d3 // 6位二进制补码形式存储的 -3 4d-2 // ❌ 非法负号必须放在位宽前面 12b1111_0000_1010 // 下划线只用于提高可读性编译器会忽略 // 等价于 12b1111000010103.1.5 字符串Strings字符串是用双引号括起来的字符序列必须写在一行内不能跨行。Hello Verilog World // 合法的字符串 a / b // 合法的字符串 Hello // ❌ 非法字符串不能跨行 World字符串中可以用转义字符表示特殊符号// 字符串中的特殊字符 $display(Hello\nWorld); // 输出两行Hello 换行 World $display(50%% discount); // 输出50% discount3.1.6 标识符Identifiers和关键字Keywords标识符是程序员给变量、模块、信号等起的名字。关键字是Verilog语言预留的、有特殊含义的标识符全部为小写。// 合法的标识符 reg clk; // 时钟信号 reg [7:0] data_bus; // 数据总线 wire _internal; // 以下划线开头合法 wire addr_1; // 包含数字合法但不在首位 // 非法的标识符 reg 1st_signal; // ❌ 以数字开头 reg data bus; // ❌ 包含空格 reg always; // ❌ always 是关键字 // 区分大小写的例子 reg CLK; // 这是 CLK大写 reg clk; // 这是 clk小写 // 两者是完全不同的信号3.1.7 转义标识符Escaped Identifiers如果实在想用关键字作为标识符虽然不推荐或者标识符中包含特殊字符可以用反斜线\开头\data_bus // 转义标识符 \*mult* // 包含特殊字符 \always // 使用关键字作为标识符不推荐转义标识符以反斜线开始以空白符结束。反斜线和空白符之间的所有字符都被当作标识符的一部分。3.2 数据类型Data Types数据类型是Verilog描述硬件的基础。理解每种数据类型的用途是写出正确代码的前提。3.2.1 值的种类Value SetVerilog采用四值逻辑来模拟实际电路中的信号状态除了逻辑值Verilog还使用强度值来解决多个驱动源之间的竞争问题竞争规则如果两个信号驱动同一个线网强度高的信号胜出。如果强度相同结果为x不确定。// 强度示例不要求初学者掌握了解即可 // 如果 strong1 和 weak0 同时驱动同一个线网strong1 胜出 // 如果 strong1 和 strong0 同时驱动结果为 x3.2.2 线网Nets线网Net代表硬件电路中的物理连线用于连接各个元件。它由驱动源如门输出、模块输出持续驱动。// 最常用的线网类型wire wire a; // 声明一个1位的wire wire [7:0] bus; // 声明一个8位的wire向量总线线网的特性Verilog中的线网类型wire是最常用的// wire的声明和使用 wire data; // 声明一个wire assign data a b; // 用assign语句驱动wire // supply0和supply1的用法 supply0 gnd; // 声明一个地线 supply1 vdd; // 声明一个电源线 assign a vdd; // a 恒为 1 assign b gnd; // b 恒为 03.2.3 寄存器Registers寄存器reg代表一个数据存储单元可以保持值直到被重新赋值。注意Verilog中的reg不一定是硬件中的触发器Flip-Flop它只是一个能保持值的变量。reg reset; // 声明一个1位的reg reg [7:0] count; // 声明一个8位的reg向量 initial begin reset 1b1; // 初始化为1 #100 reset 1b0; // 100个时间单位后变为0 end带符号寄存器用于有符号数运算reg signed [63:0] m; // 64位带符号寄存器 integer i; // 32位带符号整数见下节3.2.4 向量Vectors线网和寄存器都可以声明为向量位宽大于1。如果没有指定位宽默认为标量1位。wire a; // 标量1位等价于 wire [0:0] a; wire [7:0] bus; // 8位向量总线bus[7]是最高位MSB wire [31:0] busA, busB, busC; // 三条32位总线 reg clock; // 标量1位 reg [0:40] virtual_addr; // 41位向量第0位是最高位注意[7:0]表示最高位是7最低位是0。[0:7]也是合法的表示第0位是最高位。但习惯上使用[n:0]的形式高位在前。向量域选择Part Select可以选取向量的某一位或某几位wire [7:0] bus; bus[7] // 第7位最高位 bus[2:0] // 最低3位bus[2]、bus[1]、bus[0] bus[0:2] // ❌ 非法高位必须在左边可变的向量域选择Verilog-2001新增reg [255:0] data1; // data1[255]是最高位 reg [7:0] byte; // [起始位 : 宽度]从起始位开始递增 byte data1[31 : 8]; // 相当于 data1[31:24] // [起始位 -: 宽度]从起始位开始递减 byte data1[31 -: 8]; // 相当于 data1[31:24] // 应用用循环遍历向量的所有位 for (j 0; j 32; j j 1) byte data1[(j * 8) : 8]; // 依次取 data1[7:0], [15:8], [23:16]...3.2.5 整数、实数和时间寄存器除了reg类型Verilog还提供了几种专用的寄存器数据类型用于特定场景。① 整数Integerinteger counter; // 32位有符号整数 integer i, j, k; // 声明多个整数 initial begin counter -1; // 可以存储负数 for (i 0; i 16; i i 1) begin // 循环体内代码 end endreg vs integerreg是无符号数integer是有符号数。如果需要处理负数用integer。② 实数Realreal delta; real frequency; initial begin delta 2.13; // 十进制小数 frequency 3e6; // 科学计数法3,000,000 // 如果赋给整数会取整 integer i delta; // i 22.13取整 end③ 时间寄存器Timetime save_time; // 64位时间变量 initial begin save_time $time; // 保存当前仿真时间 $display(Current time %t, save_time); end3.2.6 数组ArraysVerilog允许声明reg、integer、time、real等类型的一维或多维数组。integer count[0:7]; // 8个整数的数组 reg bool[31:0]; // 32个1位寄存器的数组 time chk_point[1:100]; // 100个时间变量的数组 reg [4:0] port_id[0:7]; // 8个5位寄存器的数组 integer matrix[4:0][0:255]; // 二维整数数组数组 vs 向量的区别常混淆// 数组的访问 count[5] 0; // 访问第5个元素 port_id[3] 5b01010; // 访问第3个元素赋5位值 matrix[1][0] 33559; // 访问二维数组的(1,0)位置 // ❌ 非法操作 port_id 0; // 不能对整个数组赋值 matrix[1] 0; // 不能对整个行赋值二维数组3.2.7 存储器Memory在数字电路仿真中经常需要对寄存器文件Register File、RAM和ROM建模。Verilog使用寄存器的一维数组来表示存储器。reg mem1bit[0:1023]; // 1K × 1位的存储器 reg [7:0] membyte[0:1023]; // 1K × 8位的存储器字节寻址 // 访问存储器中的特定字 membyte[511] 8hA5; // 向地址511写入数据 data membyte[511]; // 从地址511读取数据存储器 数组reg [7:0] mem[0:1023]就是一个1024个元素的数组每个元素是8位宽。3.2.8 参数Parametersparameter用于定义常量在编译时确定运行时不能修改。但每个模块实例的参数值可以被重载覆盖这是参数最重要的用途。parameter port_id 5; // 常数5 parameter cache_line_width 256; // 常数256 parameter signed [15:0] WIDTH; // 带符号的16位参数 // 局部参数不可被重载 localparam state1 4b0001; localparam state2 4b0010;为什么要用参数3.2.9 字符串Strings as Registers字符串可以存储在reg类型的变量中每个字符占用8位1字节。reg [8*18:1] string_value; // 18字节足够存储 Hello Verilog World initial begin string_value Hello Verilog World; // 存储时字符串占 18×8 144 位 end规则如果寄存器宽度 字符串长度高位补0如果寄存器宽度 字符串长度截去最左边的高位为什么不直接用reg [143:0]使用8*n:1的形式让宽度计算更清晰可读性更好。3.3 系统任务和编译指令系统任务System Tasks和编译指令Compiler Directives是Verilog提供的内置工具函数。系统任务以$开头编译指令以反引号开头。3.3.1 系统任务System Tasks系统任务是Verilog预定义的函数用于执行输入输出、仿真控制、调试辅助等操作。它们类似于C语言中的标准库函数如printf、scanf。① 显示任务 ——$display$display是Verilog中最常用的输出命令用于在仿真控制台打印信息类似于C语言的printf。语法$display(p1, p2, p3, ..., pn);参数说明参数可以是字符串用双引号括起来或变量/信号名字符串中的%是格式说明符用于控制变量的显示格式// 基本用法 $display(Hello Verilog); // 输出Hello Verilog $display(The value is %d, count); // 输出The value is 5 // 多个变量 $display(a %b, b %b, sum %b, a, b, sum);格式说明符一览表// 综合示例 reg [3:0] a 4b1010; reg [7:0] b 8hA5; reg [31:0] c 42; string name Counter; $display(a %b (二进制), %d (十进制), a, a); // a 1010 (二进制), 10 (十进制) $display(b %h (十六进制), b); // b A5 (十六进制) $display(c %d (十进制), %h (十六进制), c, c); // c 42 (十进制), 2A (十六进制) $display(Module: %m, ); // Module: top.stimulus自动换行$display会在输出末尾自动添加换行符。如果不想换行使用$write。② 不换行输出 ——$write$write的用法与$display完全一样唯一的区别是不自动换行。// 对比 $display(Hello); // 输出 Hello 并换行 $display(World); // 输出 World 并换行 // 结果 // Hello // World $write(Hello); // 输出 Hello 不换行 $write(World); // 输出 World 不换行 // 结果HelloWorld实际应用用于格式化输出表格或连续打印信息。// 打印表头 $write(Time\t); // 制表符分隔 $write(Value\n); // 最后换行 // 打印数据 $write(%0d\t, $time); $write(%b\n, count);%0d中的0表示不填充空格紧凑格式输出5而不是5前面有空格。③ 监视任务 ——$monitor$monitor是Verilog中最强大的调试工具之一。它会持续监视指定的信号每当任何一个信号发生变化时自动打印所有参数的值。语法$monitor(p1, p2, p3, ..., pn);特点只需调用一次在整个仿真过程中持续有效任何被监视的信号发生变化时自动输出同一时刻只能有一个$monitor有效后调用的会覆盖之前的// 监视时钟和计数器 $monitor($time, clk %b, reset %b, q %d, clk, reset, q); // 每当 clk、reset 或 q 变化时自动打印 // 输出示例 // 0 clk 0, reset 1, q 0 // 10 clk 1, reset 1, q 0 // 20 clk 0, reset 0, q 1 // 30 clk 1, reset 0, q 1$monitor的控制开关initial begin $monitor($time, q %d, q); #100 $monitoroff; // 前100个时间单位监视 #200 $monitoron; // 300个时间单位后重新开启 end$displayvs$monitor// 对比示例 initial begin // 每10个时间单位手动打印一次 forever #10 $display($time, q %d, q); // 自动监视q变化时打印更高效 $monitor($time, q %d, q); end④ 仿真控制任务 ——$stop和$finishinitial begin #100 $stop; // 100个时间单位后暂停查看信号状态 #200 $finish; // 再经过200个时间单位后结束仿真 end$stop在交互式仿真中特别有用——你可以暂停仿真查看当前所有信号的值然后决定是否继续。系统任务实战完整的Testbench示例module stimulus; reg clk, reset; wire [3:0] q; // 实例化被测试模块 ripple_carry_counter r1(q, clk, reset); // ----- 时钟发生器 ----- initial begin clk 1b0; forever #5 clk ~clk; // 周期10ns end // ----- 测试流程 ----- initial begin // 第1阶段复位 $display( Starting Test ); $display(Applying reset...); reset 1b1; #20 reset 1b0; $display(Reset released at time %0t, $time); // 第2阶段正常计数 #100; $display(Counting for 100 time units); // 第3阶段再次复位 #50; $display(Applying reset again...); reset 1b1; #10 reset 1b0; // 第4阶段结束 #50; $display( Test Complete ); $finish; end // ----- 监视输出 ----- initial begin $monitor($time, clk%b reset%b q%d, clk, reset, q); end endmodule仿真输出 Starting Test Applying reset... 0 clk0 reset1 qx 5 clk1 reset1 qx 10 clk0 reset1 qx 15 clk1 reset1 qx 20 clk0 reset0 q0 Reset released at time 20 25 clk1 reset0 q0 30 clk0 reset0 q1 35 clk1 reset0 q1 40 clk0 reset0 q2 ... Counting for 100 time units ... Applying reset again... ... Test Complete 3.3.2 编译指令Compiler Directives编译指令以反引号开头在编译阶段生效用于控制编译器的行为。它们类似于C语言中的#define、#include、#ifdef等预处理指令。⚠️注意反引号和单引号是不同的很多初学者会把define写成define导致编译错误。①define—— 文本宏定义define用于定义文本宏在编译时进行文本替换。这是Verilog中最常用的编译指令。语法define 宏名 替换文本使用define WORD_SIZE 32 // 定义宏 reg [WORD_SIZE-1:0] data; // 展开为 reg [31:0] data; define S $stop; // 定义别名 define WIDTH 8 // 常用定义示例 define TRUE 1b1 define FALSE 1b0 define VDD 1b1 define GND 1b0define的典型应用// 1. 定义常量 define DATA_WIDTH 32 define ADDR_WIDTH 16 reg [DATA_WIDTH-1:0] data_bus; reg [ADDR_WIDTH-1:0] addr_bus; // 2. 定义状态机状态编码 define IDLE 3b000 define READ 3b001 define WRITE 3b010 define WAIT 3b011 // 3. 定义测试参数 define CLK_PERIOD 10 define RESET_TIME 20 define SIM_TIME 1000注意事项宏定义不受作用域限制一旦定义在整个文件中都有效宏定义通常写在文件开头宏名通常用大写便于识别②include—— 文件包含include用于在编译时将另一个Verilog文件的内容插入到当前文件中。语法include 文件名典型应用创建一个头文件defines.v存放公共定义// defines.v define DATA_WIDTH 32 define ADDR_WIDTH 16 define TRUE 1b1 define FALSE 1b0 define IDLE 3b000 define READ 3b001 define WRITE 3b010在主文件中包含头文件// top_module.v include defines.v module top_module; reg [DATA_WIDTH-1:0] data; // 使用宏定义 reg [ADDR_WIDTH-1:0] addr; always (posedge clk) begin if (state IDLE) begin // 状态机逻辑 end end endmodule实际工程中的建议把所有的define集中放在一个或多个头文件中使用include在需要的地方引入这样修改参数只需改一处所有文件同步更新③ifdef/else/endif—— 条件编译条件编译允许你根据是否定义了某个宏有选择地编译某段代码。语法ifdef 宏名 // 如果宏已定义编译这部分代码 else // 如果宏未定义编译这部分代码 endif典型应用define DEBUG // 注释掉这一行可以关闭调试信息 module test; initial begin ifdef DEBUG $display(Debug: simulation started); $monitor($time, q %d, q); else // 不输出调试信息 endif end endmodule多分支条件编译Verilog-2001define SIM_MODE 1 ifdef SIM_MODE // 仿真模式使用行为模型 memory_behavioral u_mem(...); elsif SYNTH_MODE // 综合模式使用门级模型 memory_gate u_mem(...); else // 默认模式 memory_default u_mem(...); endif实际工程案例// project_config.vh define FPGA // 当前目标平台是FPGA // define ASIC // 如果改为ASIC注释掉上面这行 // design.v include project_config.vh module my_design(...); ifdef FPGA // FPGA版本使用PLL pll u_pll(...); else // ASIC版本使用外部时钟 // 不使用PLL endif endmodule④timescale—— 时间尺度timescale用于指定仿真时间单位和时间精度。这在仿真中非常重要它决定了#10到底代表多长时间。语法timescale 时间单位 / 时间精度合法值时间单位和时间精度必须是1、10 或 100的倍数。示例timescale 1ns / 1ps // 时间单位1ns精度1ps #10 // 延迟10ns timescale 10ns / 1ns // 时间单位10ns精度1ns #10 // 延迟100ns timescale 1us / 10ns // 时间单位1us精度10ns #5.5 // 延迟5.5us精度10ns实际为5.50us时间精度Time Precision的作用时间精度决定了延迟值取整的精度timescale 1ns / 100ps #5.45 // 实际延迟 5.4ns精度为100ps四舍五入到最近的100ps倍数 #5.567 // 实际延迟 5.6ns$time的单位$time返回的时间值是以当前模块的时间单位为基准的不同模块的timescale不同$time的返回值也可能不同。timescale 10ns / 1ns module M1; initial begin #5 $display($time); // 输出5单位10ns即50ns end endmodule timescale 1ns / 1ps module M2; initial begin #5 $display($time); // 输出5单位1ns即5ns end endmodule完整学习示例带注释的计数器仿真// // 文件名counter_with_testbench.v // 功能四位计数器 完整的Testbench // 说明综合了本章所有的系统任务和编译指令 // timescale 1ns / 10ps // 时间单位1ns精度10ps define CLK_PERIOD 20 // 时钟周期20ns define RESET_TIME 15 // 复位持续时间15ns define SIM_TIME 200 // 仿真总时长200ns define DEBUG // 开启调试模式 // 设计模块 module counter_4bit(q, clk, reset); output reg [3:0] q; input clk, reset; // 同步复位计数器 always (posedge clk) begin if (reset) q 4b0; else q q 1b1; end endmodule // 测试模块 module testbench; reg clk, reset; wire [3:0] q; // 实例化设计模块 counter_4bit uut(q, clk, reset); // ----- 时钟发生器 ----- initial begin clk 1b0; forever #(CLK_PERIOD/2) clk ~clk; end // ----- 测试流程 ----- initial begin $display(); $display( 4-bit Counter Testbench); $display(); $display(Clock Period %0d ns, CLK_PERIOD); $display(Reset Time %0d ns, RESET_TIME); $display(Sim Time %0d ns, SIM_TIME); $display(\n); ifdef DEBUG $display([DEBUG] Starting simulation at time %0t, $time); endif // 应用复位 reset 1b1; #(RESET_TIME) reset 1b0; ifdef DEBUG $display([DEBUG] Reset released at time %0t, $time); endif // 等待计数 #(SIM_TIME - RESET_TIME); ifdef DEBUG $display([DEBUG] Simulation complete at time %0t, $time); endif $display(\n); $display( Simulation Complete!); $display(); $finish; end // ----- 监视输出 ----- initial begin $monitor(%0t ns: clk%b reset%b q%d, $time, clk, reset, q); end // ----- 条件调试输出 ----- initial begin ifdef DEBUG #1 $display([DEBUG] Initial q value %d, q); #(RESET_TIME 5) $display([DEBUG] First count should appear soon...); #(CLK_PERIOD) $display([DEBUG] Count value %d, q); #(CLK_PERIOD * 4) $display([DEBUG] After 5 counts: q %d, q); #(CLK_PERIOD * 10) $display([DEBUG] After 15 counts: q %d, q); endif end endmodule仿真输出部分 4-bit Counter Testbench Clock Period 20 ns Reset Time 15 ns Sim Time 200 ns [DEBUG] Starting simulation at time 0 [DEBUG] Initial q value x 0 ns: clk0 reset1 qx 5 ns: clk1 reset1 qx 10 ns: clk0 reset1 qx 15 ns: clk1 reset0 q0 [DEBUG] Reset released at time 15000 [DEBUG] First count should appear soon... 20 ns: clk0 reset0 q0 25 ns: clk1 reset0 q0 30 ns: clk0 reset0 q1 [DEBUG] Count value 1 35 ns: clk1 reset0 q1 40 ns: clk0 reset0 q2 45 ns: clk1 reset0 q2 50 ns: clk0 reset0 q3 55 ns: clk1 reset0 q3 ... [DEBUG] After 5 counts: q 5 ... [DEBUG] After 15 counts: q 15 ... [DEBUG] Simulation complete at time 200000 Simulation Complete!