news 2026/9/29 7:28:32

Verilog高频语法速查:从reg/wire到状态机的工程清单

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Verilog高频语法速查:从reg/wire到状态机的工程清单

写Verilog这些年,我最深的体会是:语法不难,难的是在真正写代码时,总能因为一些“记不牢”的细节反复翻手册。比如reg和wire到底什么时候用、<=和=为什么不能乱换、模块例化时端口总是漏连信号,甚至有时候花半小时写出一个模块,结果编译一下报出十几个错误,看着满屏的 error 不知道从哪下手。后来我总结了一套自己的做法:把日常开发里高频用到的 Verilog 常用语法整理成清单,配合 Icarus Verilog 这类开源工具做语法检查,在写代码的时候就顺手把问题消掉,而不是等问题堆到最后一起爆发。这篇内容就是这份清单的完整版,包含语法模块、功能模板、编译检查流程,还有一份可以直接抄的自查表。适合刚学 Verilog 语言入门教程的初学者,也适合准备笔试、面试或者临时要写一段可综合代码的从业者,照着它写代码,能少走很多弯路。

1. 这份语法清单的定位:写给谁、解决什么问题

1.1 从“背语法”到“查语法”的思路转变

我见过不少初学者学 Verilog,第一个动作就是下载一本几百页的语法手册,然后从module开始一路往下背。结果背到generate和task的时候已经晕了,真正打开编辑器写代码,还是不知道怎么开头。这里我想先说一个反直觉的观点:Verilog 语法不适合背,适合“查”。

所谓“查”,不是让你每次都去翻几百页的手册,而是把项目里反复用到的语法元素,按模块、数据类型、操作符、过程块、子程序这几个方向整理成一张表。写代码前扫一眼框架,写完之后用工具跑一遍语法检查,哪里不对,编译器的报错信息会直接告诉你。

我自己把这份清单定位成三层:

  1. 快速上手层:知道一个 Verilog 文件的基本骨架是什么,端口怎么写,信号怎么声明。
  2. 编码实战层:积累一些高频场景的代码模板,比如计数器、状态机、task、function,直接套用。
  3. 排错检查层:编译报错之后,能快速看懂 error 和 warning 到底在说什么,并通过一条固定的检查流程定位问题。

这篇文章就是围绕这三层展开的。你不需要从头到尾读完,完全可以把它当成一份放在手边的速查文档,用到哪一节就翻哪一节。

1.2 工具环境的准备:Icarus Verilog 与 GTKWave

既然涉及“语法检查”,就必须有一个趁手的检查工具。我推荐使用 Icarus Verilog,也就是常说的iverilog。它是个开源的 Verilog 仿真与编译工具,轻量、免费,编译速度快,而且对于语法错误的提示比较直观,特别适合用来做代码风格的快速校验。

在 Ubuntu 或 Debian 系统上安装就是一条命令:

sudo apt install iverilog gtkwave

Windows 上可以从 Icarus Verilog 官网下载安装包,安装后把安装目录下的bin路径加入环境变量,一样能在命令行里用。Mac 用户可以用brew install icarus-verilog。

安装完之后,最简单的语法检查方式是这样的。假设你有一个文件叫counter.v:

iverilog -o counter.vvp counter.v

如果文件里有语法错误,终端会直接输出error:开头的提示,并且告诉你出错的文件名和行号。如果没有输出,说明文件通过了编译阶段。后面我还会专门讲怎么用iverilog配合vvp做仿真,以及用 GTKWave 看波形,这里先不做展开。

2. 模块骨架与端口声明:所有工程的第一步

2.1 ANSI 风格的端口声明

Verilog 的模块定义,老式写法是把端口名全部列在括号里,再在模块内部挨个声明方向。VHDL 和 Verilog 等工具发展这么多年,目前的代码风格基本都推荐采用 ANSI 风格,也就是在模块括号里直接写明方向和类型。一句话总结:外部端口用 input、output、inout,内部信号用 wire、reg、integer 等类型。

一个标准的计数模块骨架长这样:

module counter #( parameter WIDTH = 8 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] cnt ); always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= {WIDTH{1'b0}}; else if (en) cnt <= cnt + 1'b1; end endmodule

注意三个细节:

  • #(...)用来定义参数,后面例化时可以重新赋值,这是实现参数递增、位宽可配置的基础。
  • output reg直接声明输出端口是寄存器类型,省去在模块内部再写一行reg cnt;的重复操作。
  • 复位信号rst_n带了下划线后缀,是一种约定俗成的低电平复位命名,代码读起来更清晰。

2.2 parameter 与 localparam:参数化的两种用法

parameter是可以在模块外部修改的,localparam只能在模块内部使用,外部不可修改。这个区别经常有人忽略,但我认为它是衡量一个模块设计是否专业的重要分界线。

举个例子,你想要设计一个支持不同位宽的计数器:

module counter #( parameter WIDTH = 8 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] cnt ); localparam MAX_COUNT = (1 << WIDTH) - 1; always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= {WIDTH{1'b0}}; else if (cnt == MAX_COUNT) cnt <= {WIDTH{1'b0}}; else if (en) cnt <= cnt + 1'b1; end endmodule

这里MAX_COUNT是内部常量,用它来表示计数上限,避免在多个地方写同样的魔数。如果这段代码被例化很多次,localparam不会暴露到外部,也防止调用者不小心修改内部状态,是一种更好的封装习惯。

2.3 模块例化:按名字连接端口的唯一推荐方式

模块写完之后总要在顶层或者其他模块里例化。我见过很老的代码,例化时只按端口顺序传信号,像这样:

counter u_counter(clk, rst_n, en, cnt);

这种方式一旦端口顺序写错,或者有人改动了模块的端口列表,顶层代码就会悄悄连错,而且很难查。我的建议是永远使用按名字例化:

counter #( .WIDTH (16) ) u_counter ( .clk (clk), .rst_n (rst_n), .en (en), .cnt (cnt) );

例化时的两个括号要区分清楚:#(...)用于参数配置,后面那对括号用于端口连接。按名字例化确实会多写几行,但可读性和可维护性完全不一样,尤其是在大型工程里,这几个字符的差距会直接影响排查问题的速度。

3. 数据类型与操作符:reg/wire 的边界和运算符的坑

3.1 reg、wire、integer、parameter 怎么选

很多初学者纠结reg和wire的区别,其实可以这样理解:wire是线,负责把两个逻辑点连起来;reg是变量,只能在过程块(always、initial)里被赋值。判断一个信号该用哪种类型的标准很简单:看它出现在什么语句里。出现在assign右边、模块例化的输入端口、或者连续赋值语句左边的,用wire;出现在always块里被赋值的,用reg。

这里要特别纠正一个常见误解:reg类型的信号综合后并不一定是寄存器。比如:

reg [3:0] tmp; always @(*) begin tmp = in_a + in_b; end

always @(*)是组合逻辑块,里面的tmp虽然是reg类型,但综合出来只是一组组合逻辑加法器,不会生成真正的触发器。

再看一下三四类常见声明的应用场景:

类型典型用途可综合性
wire组合逻辑连线、模块端口连接是
regalways 或 initial 中赋值的信号是
integer循环变量、通用整数运算综合时不推荐作为大规模硬件
parameter / localparam常量、参数配置是
genvargenerate 循环中的循环变量是(用于生成语句)

另外提一个容易漏掉的类型:signed。如果你的信号需要参与有符号运算,建议在声明时写清楚:

wire signed [7:0] data_a;

不然两个“看起来是有符号数”的[7:0]信号做比较时,会被 Verilog 当作无符号数处理,负数比较的结果就会完全不对。

3.2 操作符速查表

Verilog 操作符的总体使用频率很高,我把日常写 RTL 时真正用得上的列在一张表里,剩下的零散操作符等你实际用到时再查也不迟:

类别操作符说明与示例
算术+ - * / %a + b,乘法注意位宽翻倍
位运算`&^ ~`
逻辑`&&
归约`&^`
移位<< >> <<< >>>算术左移/右移用<<<>>>
比较== != > < >= <=注意===可以比较不定态 x 和高阻态 z
拼接{}{a, b}把多个信号拼成一个向量
复制{{n{bit}}}{8{1'b0}}生成 8 位全零向量
三目?:条件选择,可综合

拼接和复制这两个操作符在写位宽扩展、时钟分频、移位寄存器时特别好用。比如生成一个 8 位全零向量{8{1'b0}},或者把两个 4 位信号拼成 8 位{a, b},语法上远比手动声明多个信号清爽得多。

3.3 位宽与符号:最容易翻车的地方

位宽问题是很多编译警告的根源,也是代码跑仿真时结果不对劲的高频原因。

一个经典的例子:

wire [7:0] a = 8'd200; wire [7:0] b = 8'd100; wire [7:0] c; assign c = a + b; // c 等于 8'd44,而不是 300

原因很简单:两个 8 位信号相加,结果在赋值给 8 位信号时自然截断。如果你需要得到 300,就必须把相加结果的位宽定成 9 位:

wire [8:0] c; assign c = a + b;

乘法的位宽更要留心:两个 N 位无符号数相乘,结果需要 2N 位才能全部容纳。比如两个 8 位乘法:

wire [7:0] a, b; wire [15:0] product; assign product = a * b;

product声明为 16 位,这样才不会丢数据。我个人的习惯是:在做算术运算前,先心里算一遍最终结果的位宽,再写信号的声明,这样编译警告会少一大半。

3.4 阻塞赋值与非阻塞赋值:写错了后患无穷

这块是 Verilog 语法里最值得反复强调的规则,尤其是在笔试和面试里几乎是必问题。我把结论放在最前面:

  • 时序逻辑块里用非阻塞赋值<=。
  • 组合逻辑块里用阻塞赋值=。
  • 同一个 always 块里不要混用<=和=。

为什么时序逻辑里要用<=?因为非阻塞赋值的语义是“先计算右边表达式的值,再在时间步的末尾统一赋给左边”。这就模拟了真实触发器的行为:所有寄存器都在同一个时钟沿采样旧值,然后同时更新。

举个例子,一个移位寄存器:

always @(posedge clk) begin shift_reg[0] <= data_in; shift_reg[1] <= shift_reg[0]; shift_reg[2] <= shift_reg[1]; end

如果这里误写成阻塞赋值=,那第二条语句里shift_reg[1]拿到的就是shift_reg[0]刚更新过的新值,仿真结果会变成一次性把所有位填成data_in,和真实硬件行为完全不符。这就是我在实际项目中强调最多的一条纪律。

4. 过程块与逻辑描述:assign、always、initial 的职责划分

4.1 三种语句块各自适合干什么

Verilog 描述逻辑的方式主要就是三大类:assign连续赋值、always过程块、initial初始化块。

  • assign只能描述组合逻辑,而且语句之间是并行执行的,没有先后顺序,永远处于“持续赋值”的状态。
  • always可以描述组合逻辑,也可以描述时序逻辑,靠敏感列表来触发执行。
  • initial只在仿真开始时执行一次,常用于 testbench 里的信号初始化,不可综合,所以永远不要指望把 initial 写进可综合的 RTL 代码。

日常开发里我的选择逻辑是:简单的组合逻辑,比如信号连接、译码、拼接,用assign;复杂一些的时序逻辑,比如寄存器、计数器、状态机,用always配合posedge时钟;需要仿真激励时,在 testbench 里用initial生成复位波形和输入数据。

一个常见的组合逻辑小例子:

assign data_out = (sel == 2'b00) ? data_a : (sel == 2'b01) ? data_b : (sel == 2'b10) ? data_c : data_d;

这种写法比一大堆if/else清晰多了,综合出来的多路选择器结构也一目了然。

4.2 时序逻辑模板:时钟与复位

我写时序逻辑时基本固定使用下面这套模板,它可以覆盖九成以上的寄存器逻辑场景:

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin // 复位赋值 reg_a <= {WIDTH{1'b0}}; reg_b <= 1'b0; end else begin // 正常逻辑 reg_a <= next_value_a; reg_b <= next_value_b; end end

这里的敏感列表posedge clk or negedge rst_n表示异步复位,也就是说复位信号一变低,寄存器立刻被复位,不需要等时钟沿。如果你想要同步复位,敏感列表里只需要写posedge clk,然后把if (!rst_n)放在时钟沿触发的块内部。

还有一个细节容易被忽略:异步复位的复位信号如果会产生毛刺,会导致寄存器误复位。所以在实际工程里,复位信号通常需要经过异步复位、同步释放的处理,这里不展开,但你必须知道复位不是一个简单的高电平或低电平信号。

4.3 组合逻辑与锁存器的规避

组合逻辑用always @(*)写时,最容易踩的坑是产生锁存器。什么叫锁存器?简单说,就是组合逻辑块里,某些分支下信号没有被赋值,综合工具为了“保持上一次的值”,就会自动推断出一个锁存结构。锁存器在时序分析里很难处理,所以一般要尽量避免。

规避锁存的规则和组合逻辑的完备性有关:要么给所有可能的分支都赋值,要么在 always 块开头给信号一个默认值。

always @(*) begin data_out = 1'b0; // 默认值,防止锁存 if (en) data_out = data_in; end

这段代码里data_out = 1'b0就是关键一行。没有它,当en为 0 时,data_out没有赋值,综合工具就可能会推断出锁存器。有了默认值,无论哪个分支,信号都被明确赋值,组合逻辑就是纯组合逻辑。

4.4 编译指令:define、include、ifdef 与 celldefine

Verilog 里以反引号开头的编译指令也属于语法的一部分,很多人容易把它们跟普通关键字搞混。我这里挑几个常用的说一下。

`define用来定义宏:

`define WIDTH 8

使用宏时同样要带反引号:`WIDTH。宏在做全局参数、仿真开关切换时比较方便,但我不建议在可综合代码里大规模使用宏来替代parameter。因为宏是全局生效的,一旦多个模块里用了同一个宏名,预编译阶段的替换容易相互干扰。

`include用来包含文件。多个模块要复用头文件时可以这样写:

`include "defines.v"

`ifdef常用于仿真环境的条件编译:

`ifdef SIMULATION // 仿真专用的逻辑 `else // 综合专用的逻辑 `endif

还有一个经常听说但 RTL 里很少用到的指令:`celldefine和`endcelldefine。这两个指令主要用来标记标准单元库或者延迟模型中的单元定义,一般出现在厂商提供的库文件里。普通 FPGA 或 ASIC 前端设计基本不需要手动写它,我看到不少人在这里犯迷糊,所以提一句:如果你在写业务逻辑,碰到`celldefine通常意味着你正在看库文件,而不是自己的 RTL 代码。

5. 高频功能块语法模板:计数器、状态机、task/function

5.1 计数器与参数递增

计数器是 Verilog 里出现频率最高的模块之一,像分频、定时、滑动窗口滤波的风向采集、地址递增,本质都离不开计数逻辑。

一个最简单的可参数化递增计数器,前面已经给过代码。这里再加上一个“使能门控”的版本,注意它的结构:

module counter #( parameter WIDTH = 8 )( input wire clk, input wire rst_n, input wire count_en, output reg [WIDTH-1:0] cnt ); always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= {WIDTH{1'b0}}; else if (count_en) cnt <= cnt + {{WIDTH-1{1'b0}}, 1'b1}; end endmodule

注意递增部分我用了{{WIDTH-1{1'b0}}, 1'b1},它实际就是生成一个位宽为WIDTH的数值 1。如果你直接写cnt + 1,编译器也能推断出 1 的位宽,但如果把cnt做成参数化位宽,使用复制操作符可以更明确地表达意图,阅读代码的人不会误解。

滑动窗口滤波在 Verilog 里的典型实现,本质上就是一组移位寄存器,把采样数据逐拍往后移,形成一个窗口:

reg [DATA_WIDTH-1:0] window [0:WINDOW_SIZE-1]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (int i = 0; i < WINDOW_SIZE; i = i + 1) window[i] <= {DATA_WIDTH{1'b0}}; end else begin window[0] <= data_in; for (int i = 1; i < WINDOW_SIZE; i = i + 1) window[i] <= window[i-1]; end end

这里的for循环是可综合的,但它的本质是在仿真编译阶段把逻辑展开,相当于把多个寄存器的赋值语句全部列出来。这也就是为什么for (int i = 0; ...)里那个int变量可以被综合工具接受的原因,它是用来生成电路结构,而不是真正在硬件里跑循环。

5.2 状态机的三段式写法

状态机可以说是数字逻辑设计里最核心的时序模板。三段式状态机是我个人强烈推荐的一种风格,它把状态迁移、次态逻辑、输出逻辑分成三个 always 块,逻辑清晰,时序也更可控。

// 第一段:状态寄存器 reg [STATE_WIDTH-1:0] state; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end // 第二段:次态组合逻辑 always @(*) begin next_state = state; case (state) IDLE: if (start) next_state = RUN; RUN: if (done) next_state = IDLE; default: next_state = IDLE; endcase end // 第三段:输出逻辑 always @(*) begin data_valid = 1'b0; case (state) RUN: data_valid = 1'b1; endcase end

这里面的关键点有两个。第一,第二段用了always @(*)和阻塞赋值=,因为它是纯组合逻辑;第三段同样用always @(*),并且给输出设置了默认值,防止产生锁存器。第二,第二段里我给next_state先赋值state,再进入case,这样就算某个状态没有完全覆盖所有分支,也不会让next_state悬空。

状态机的状态编码建议用localparam定义:

localparam IDLE = 3'd0; localparam RUN = 3'd1;

像parameter和localparam配合状态机的场景,我几乎在每个项目里都要写上一遍。

5.3 task 与 function:子程序的两种形态

task和function是 Verilog 里用来封装子程序的语法,但在使用上差异很大。一句话概括:function 适合做纯组合逻辑计算,task 更像一段完整的过程,可以包含时序控制。

function至少有一个输入,可以有返回值。比如定义一个求位宽的辅助函数:

function integer clog2; input integer value; integer tmp; begin clog2 = 0; tmp = value - 1; while (tmp > 0) begin clog2 = clog2 + 1; tmp = tmp >> 1; end end endfunction

这个函数可以在参数计算里直接使用:

localparam ADDR_WIDTH = clog2(DEPTH);

task则可以包含@(posedge clk)、#10等仿真事件控制,非常适合在 testbench 里写激励序列。比如读写 EEPROM 的 I2C 激励,一个写字节的 task 可以写成:

task i2c_write_byte; input [7:0] data; begin // 产生起始条件 // 移位发送 8 位数据 // 等待 ACK end endtask

当然,这里的task在仿真里用很自然;如果在可综合 RTL 里使用task,必须确保所有语句都是可综合的,比如不要在里面写#10延迟,也不要写文件输出。实际做 I2C 读写 EEPROM、QSPI 读写 Flash 这类协议逻辑时,我更推荐用状态机来实现协议时序,用task只是在 testbench 里模拟主机行为,这样仿真和综合的代码职责更清楚。

5.4 协议类代码的语法框架:I2C 与 QSPI 的共性问题

很多人在网上一搜“I2C 读写 EEPROM verilog”或“QSPI 读写 Flash verilog”,能搜到一堆完整代码,但缺少一个语法规整的视角。我在这类协议代码里总结出三个最共通的语法点。

第一,移位寄存器是绝对的骨架。无论是 I2C 的 SDA 还是 QSPI 的数据线,本质都是一位一位地把数据移出去,再一位一位地把数据采样进来,所以assign加always的配合是少不了的:

reg [7:0] shift_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) shift_reg <= 8'h00; else if (shift_en) shift_reg <= {shift_reg[6:0], 1'b0}; // 左移补 0 end

第二,三态门控制在双向端口(inout)上非常典型。I2C 的 SDA 以及 QSPI 的 IO 线都是双向的,所以输出逻辑通常写成:

assign sda = sda_out_en ? sda_out : 1'bz;

这里1'bz表示高阻态,释放总线,让外部设备能够拉低 SDA。很多初学者第一次写inout端口时总是漏掉这个高阻态赋值,导致仿真时总线始终被驱动,读不到正确数据。

第三,位计数和字节计数是状态跳转的核心。在语法层面,就是cnt信号加一个case判断的问题,但这个计数粒度必须提前想清楚:是每移一位跳一态,还是每个字节跳一态。我见过不少协议代码,状态机本身没错,错在计数器的清零和使能时机没有设计好,导致波形里多出半个时钟周期的毛刺。

6. 语法检查的完整闭环:用开源工具快速定位错误

6.1 为什么推荐 Icarus Verilog 做第一道闸

我一直认为,iverilog是 Verilog 语法检查性价比最高的工具。它比很多商业 IDE 的语法提示更快,比在线 Verilog 网站更严谨,而且完全本地运行,代码不会上传到第三方服务器。它的定位是仿真和编译工具,但用来做语法检查绰绰有余。

完整的语法检查和功能仿真流程,我习惯分成三步:

  1. 编译:iverilog -o test.vvp test.v tb.v
  2. 仿真:vvp test.vvp
  3. 看波形:gtkwave test.vcd

如果只是检查语法,跑完第一步就够了。如果还想验证逻辑功能,再执行第二三步。

6.2 编译报错的典型场景与定位方法

iverilog的报错信息格式一般是文件名:行号: error: 具体原因。不要一看到 error 就慌,先看行号,再看信息,九成情况都能快速解决。

我整理了几类高频报错,基本都是初学者和从业者都容易犯的:

报错场景常见原因修复思路
找不到模块xxx被例化模块的文件没有包含进编译列表在命令行末尾加上模块文件;或检查模块名是否拼错
undeclared variable信号没有声明类型补上wire或reg,同时检查位宽
procedural assignment to a non-reg在 always 里给 wire 赋值把左侧信号改成reg类型
continuous assignment to a reg在 assign 里给 reg 赋值换成wire类型,或改用 always
near "xyz": syntax error往往有拼写错误、漏掉分号、begin/end 不配对检查报错行往前一片的 begin/end
endmodulemissing模块没闭合在文件末尾补上endmodule

其中begin/end不配对是我自己遇到最多的问题。Verilog 允许单条语句不写begin/end,但多条语句必须写,一旦漏写,编译器会把逻辑归属搞错,报错位置也和真正问题相差十万八千里。我的习惯是:任何语句块,哪怕只有一条语句,也统一写begin/end,多了几行,但一眼就能看出结构,排错会轻松很多。

6.3 仿真与波形验证:不只是“编译过了就想当然”

语法检查通过只代表书写正确,不代表功能正确。我之前遇到过一个案例,代码编译零错误,但状态机跑起来就是不对。最后通过vvp生成 VCD 波形,再用 GTKWave 打开,才发现是复位信号释放时机和输出使能差了一个周期。这类问题如果只看代码,智力成本极高,但看一眼波形立刻清楚。

仿真流程里,testbench 是最重要的一环。一个最小化的仿真文件长这样:

`timescale 1ns / 1ps module tb_counter; reg clk; reg rst_n; reg en; wire [7:0] cnt; counter #(.WIDTH(8)) u_counter ( .clk (clk), .rst_n (rst_n), .en (en), .cnt (cnt) ); initial begin clk = 0; rst_n = 0; en = 0; #100; rst_n = 1; en = 1; #1000; $finish; end always #5 clk = ~clk; initial begin $dumpfile("tb_counter.vcd"); $dumpvars(0, tb_counter); end endmodule

注意这里必须写`timescale,否则某些版本的仿真器对延迟时间的解释可能不符合预期。$dumpfile和$dumpvars用来导出波形数据,GTKWave 打开后才能看到各个信号的随时间变化曲线。

6.4 编译顺序与 include 路径:多文件工程的坑

当工程里有多个 Verilog 文件时,iverilog的编译顺序理论上不强制,因为它会解析模块之间的依赖关系,但文件缺了就会报找不到模块。我的做法是把所有需要编译的文件一次列全:

iverilog -o sim.vvp tb_counter.v counter.v defines.v

如果用到`include指令且文件不在当前目录,记得加-I参数指定搜索路径:

iverilog -I headers -o sim.vvp tb_counter.v counter.v

这里有个冷门细节:Verilog 里的`include不像 C 语言那样会自动防止重复包含,所以如果多个文件都 include 了同一个头文件,而且头文件里又有宏定义,预编译时会告警。一般我会在头文件中加条件编译保护,虽然 Verilog 里不强制,但在复杂工程里能少很多莫名其妙的冲突。

7. 一份可以照抄的自查清单

最后这部分,我把它当成是整篇文章的浓缩版。写完一个模块之后,不要急着仿真,先对着这份清单检查一遍,能帮你省下大量排错时间。

检查项具体内容
模块名称模块名和文件名是否一致,方便查找
端口方向每个端口是否都有明确的方向声明
信号类型端口信号用 wire,always 内赋值的信号用 reg
位宽所有向量信号是否声明了位宽,运算是否可能截断
时钟与复位时序逻辑敏感列表是否包含所有必要的时钟沿和异步复位
阻塞非阻塞时序逻辑是否用<=,组合逻辑是否用=
begin/end 配对多语句块是否都有 begin/end 包裹
组合逻辑完备性是否所有分支都有赋值,默认值是否设置
状态机是否覆盖 default 分支,next_state 是否先赋当前值
参数化常量定义使用 localparam,外部可配置使用 parameter
例化方式是否按名字例化,端口是否一一对应
双向端口inout 信号释放时是否赋予高阻态1'bz
编译检查iverilog是否零错误输出

我个人在提交代码前,一定会从头到尾把这些项目过一遍。说实话,这张表看起来简单,但每一条都是从实际项目踩坑里提炼出来的。比如位宽截断,早期我以为编译器总会自动扩展,结果在仿真里算出一个诡异的溢出的数字,查了半天才发现是两个窄位宽信号直接相加造成的。又比如双向端口的高阻态,逻辑上就少写了一行,整个 I2C 读操作就是读不到数据。这些经验用一句话讲就是:语法检查工具能帮你排除拼写和结构错误,但逻辑和位宽问题,要靠自己的代码纪律来守住。

最后再分享一个小习惯:我每完成一个模块,会单独建一个check目录,把模块文件和最小 testbench 放进去,用iverilog编译一遍。这看起来多了一步,但实际上比最后整个工程一起报错、面对几十条 error 节省太多时间。语法这东西,写得多了自然就熟了,但有了工具和清单,你不用把所有细节都背下来,也能写出稳定、规范、可维护的 Verilog 代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 7:26:59

芯片设计方法演化史:从标准单元到Chiplet的五大实战拐点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 7:26:31

数据库管理工具怎么选?Navicat安装连接与排查全攻略

搞数据库的朋友应该都有这种感觉&#xff1a;能从命令行把SQL写利索的人不少&#xff0c;但从“会写SQL”到“把一套库管起来”&#xff0c;中间隔着一个工具链的距离。“数据库及navicat工具”这个话题最近被问得非常多&#xff0c;我发现大多数人卡住的根本不是SQL语法&#…

作者头像 李华
网站建设 2026/9/29 7:23:08

DeepAgents+MCP+A2A+Skills:多智能体集群实战与踩坑指南

1. 为什么我开始搭多智能体集群&#xff1a;单Agent的瓶颈前几天帮团队把一个内部工具站整体改版&#xff0c;最开始图省事&#xff0c;直接用单个Agent挂着十几个MCP工具从头跑到尾。结果改了首页忘了侧边栏&#xff0c;改完筛选逻辑回头又碰坏了登录态&#xff0c;Agent自己都…

作者头像 李华
网站建设 2026/9/29 7:21:48

网络安全态势感知:从日志归一化到自动响应的自防御闭环

简介&#xff1a;《基于网络安全态势感知的网络系统自防御体系》是一篇面向网络安全研究人员、网络管理员及中小型机构技术决策者的参考文献&#xff0c;聚焦利用态势感知技术构建主动防御模型&#xff0c;以应对规模化、复杂化网络攻击。资源为PDF格式&#xff0c;共1个文件&a…

作者头像 李华
网站建设 2026/9/29 7:20:42

ROS中激光雷达/scan话题的稳定订阅与实时处理指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 7:19:31

网络设备安全加固实战:从telnet到SSH、AAA与ACL配置指南

简介&#xff1a;《网络设备安全加固方案》1.0版是一份面向网络运维与安全从业者的实操型文档&#xff0c;针对内网设备普遍缺乏登录限制、Con口未加密、telnet可被任意终端访问等隐患&#xff0c;给出从身份认证、访问控制到权限管理的完整加固思路。资源包共1个docx文件&…

作者头像 李华