第八章 Verilog HDL语法
本章在讲什么
从这一章开始,我们正式进入"语法篇"。前面几章里,我们已经用 Vivado 跑通了第一个 LED 工程,但当时的代码更多是"照着抄"。要真正做到独立设计电路,就必须系统地掌握硬件描述语言——本教程使用的是 Verilog HDL。
Verilog 是一种硬件描述语言(Hardware Description Language),它以文本形式描述数字系统硬件的结构和行为。它借鉴了 C 语言的部分语法形式(如 if/else、case 等控制语句),入门门槛不高——有 C 语言基础的读者,经过一个月左右的练习即可掌握。但它与 C 语言有本质区别:
- C 语言:软件语言,编译后在 CPU 上取址、译码、执行,是串行的指令流,不会生成任何硬件电路;
- Verilog:硬件描述语言,综合下载到 FPGA 后,会生成真实的硬件电路,所有电路并行运行。
"并行执行"正是 FPGA 处理速度快的根本原因,也是初学者最需要转变的思维方式——写 Verilog 不是在"写程序",而是在"画电路"。
本章内容包括五大部分:
- Verilog 概述:Verilog 与 VHDL、C 语言的区别;
- Verilog 基础知识:逻辑值、标识符、数字进制、数据类型、运算符;
- Verilog 程序框架:注释、关键字、以 LED 流水灯为例的程序结构;
- Verilog 高级知识点:阻塞/非阻塞赋值、assign 与 always、latch、状态机、模块化设计;
- Verilog 编程规范:工程组织、文件头、命名、代码风格等。
核心概念拆解
8.1 Verilog 概述:为什么需要它
发展简史:Verilog 最初是 Gateway Design Automation 公司于 1983 年为其仿真器产品开发的硬件建模语言,1990 年面向公众发布,1995 年正式成为 IEEE 标准(IEEE Std 1364-1995,即 Verilog-95)。此后修正扩展为 IEEE Std 1364-2001(Verilog-2001),新增了敏感列表、多维数组、生成语句块、命名端口连接等实用功能,是目前最主流的版本,本教程代码全部遵循 Verilog-2001 语法。
为什么取代了原理图设计:早期 FPGA 设计常用原理图输入方式,直观但难以维护。随着电路规模增大,原理图方式无法满足项目需求,Verilog 凭借结构化描述能力、可复用性以及与 EDA 自动化工具的高度适配性,成为绝对主流。
Verilog 与 VHDL:两者都是 IEEE 标准硬件描述语言,共同点是都能形式化抽象表示电路行为与结构、支持层次化描述、支持高层到低层的综合转换、与实现工艺无关。区别在于 Verilog 语法类似 C、易学易用,拥有更广泛的设计群体(在美国高层次数字系统设计中 Verilog 与 VHDL 的使用率约为 80% vs 20%,国内 IC 设计公司也普遍采用 Verilog),因此本教程推荐学习 Verilog。
Verilog 与 C 语言:这是 FPGA 与单片机/CPU 的区别的本质——FPGA 全并行处理,速度极快;CPU 串行执行指令。理解了这一点,才算真正入了 FPGA 的门。
8.2 Verilog 基础知识
逻辑值:4 种状态
数字电路中,信号有四种逻辑值:
| 逻辑值 | 含义 | 对应电路状态 |
|---|---|---|
| 0 | 低电平 | 对应电路的 GND |
| 1 | 高电平 | 对应电路的 VCC |
| X | 未知 | 可能是高电平也可能是低电平 |
| Z | 高阻态 | 外部没有激励信号时处于悬空状态 |

X 和 Z 这两个值在仿真调试中非常常见:信号未复位或未赋值时常表现为 X,信号悬空(比如未接的上拉/下拉)常表现为 Z,看到它们要能立刻联想到对应的电路状态。
标识符
标识符(Identifier)用于定义模块名、端口名和信号名等,可以是字母、数字、$ 和下划线 _ 的组合,但第一个字符必须是字母或下划线,并且区分大小写(Count 与 COUNT 是两个不同的标识符)。
推荐写法:简洁、清晰、易懂,如 count、fifo_wr。具体规范:
- 用有意义的名字,如
sum、cpu_addr; - 用下划线区分单词组合;
- 采用前缀/后缀约定:时钟用
clk前缀(clk_50m),低电平有效用_n后缀(enable_n); - 普通内部信号全小写,参数全大写;
- 自定义标识符不能与关键字同名。
数字进制格式
Verilog 支持二进制、八进制、十进制、十六进制,常用格式为 <位宽>'<进制><数值>:
- 二进制:
4'b0101表示 4 位二进制数 0101; - 十进制:
4'd2表示 4 位十进制数 2(即二进制 0010); - 十六进制:
4'ha表示 4 位十六进制数 a(即二进制 1010),十六进制计数为 0~9、a~f,f 对应十进制 15。
注意:当代码中没有指定位宽与进制时,默认为 32 位十进制,例如直接写 100,实际表示 32'd100。
数据类型:三大类
真正对应硬件电路的是寄存器类型和线网类型,参数类型则是编译期的常量。
1)寄存器类型(reg):表示一个抽象的数据存储单元,只能在 always 语句和 initial 语句中被赋值,值在两次赋值之间保持不变。有一个非常容易混淆的点:
- 若 always 语句带时钟(描述时序逻辑),reg 变量综合为真正的寄存器;
- 若 always 语句不带时钟(描述组合逻辑),reg 变量综合后其实只是硬件连线。
寄存器类型的缺省值是 x(未知)。最常用的是 reg 类型:
//reg define
reg [31:0] delay_cnt; //延时计数器
reg key_flag ; //按键标志
2)线网类型(wire):表示结构化元件之间的物理连线,其值由驱动元件决定(如 assign 连续赋值或门的输出)。没有驱动元件连接时,缺省值为 z(高阻态)。最常用的是 wire 类型:
//wire define
wire data_en; //数据使能信号
wire [7:0] data ; //数据
3)参数类型(parameter):参数是一个常量,常用于定义状态机的状态编码、数据位宽和延迟大小等。由于可以在编译时(例化时)修改参数值,它常被用于"参数可调"的模块。参数定义是局部的,只在当前模块内有效:
//parameter define
parameter DATA_WIDTH = 8; //数据位宽为8位
运算符
Verilog 运算符按功能分为 7 类:
1)算术运算符:+、-、*、/、%(取余)。注意:Verilog 实现乘除法(尤其是除法)非常浪费组合逻辑资源。2 的指数次幂的乘除法应使用移位运算完成;非 2 的指数次幂的乘除法一般调用工具提供的现成 IP(其底层仍由与或非门等组合逻辑搭建)。
2)关系运算符:>、<、>=、<=、==、!=,用于条件判断,关系为真返回 1,为假返回 0。优先级低于算术运算符。
3)逻辑运算符:!(逻辑非)、&&(逻辑与)、||(逻辑或),用于连接多个关系表达式实现更复杂的判断。只有 a 和 b 都为 1 时 a && b 才为 1;a 或 b 有一个为 1 时 a || b 即为 1。
4)条件运算符:a ? b : c——a 为真选 b,否则选 c,功能等同于 always 中的 if-else。
5)位运算符:~(按位取反)、&(按位与)、|(按位或)、^(按位异或),直接对应数字逻辑中的与、或、非门等逻辑门,对两个信号的对应位逐一运算。
易混淆点:位运算符与逻辑运算符长得像但用途不同——逻辑运算符(
&&、||、!)一般用在条件判断上,位运算符(&、|、~)一般用在信号赋值上。
6)移位运算符:<<(左移)、>>(右移),移出的空位一律补 0。例如 8bit 的 a = 3(二进制 00000011),a << 2 后仍是 8bit,最高 2bit 被移出丢弃,结果为 00001100 即 12。一般用左移代替乘法、右移代替除法,但仅限 2 的指数次幂。
7)拼接运算符:{a, b} 将两个或多个信号的某些位拼接起来作为一个新信号,这是 C 语言中没有的特殊运算符。
优先级(从高到低):!、~ → *、/、% → +、- → <<、>> → <、<=、>、>= → ==、!= → & → ^ → | → && → || → ?(最低)。记不住也没关系,写代码时多用括号明确优先级即可。
8.3 Verilog 程序框架
注释与关键字
注释有两种:/* ... */ 块注释(可跨多行)和 // 行注释。建议统一使用 //。
关键字是语言保留字,如 module、input、output、reg、wire、always、assign、begin、end、posedge/negedge、case、if/else 等。注意:只有小写的关键字才是保留字,always 是关键字而 ALWAYS 不是。
常用关键字速查:
| 关键字 | 含义 | 关键字 | 含义 |
|---|---|---|---|
| module / endmodule | 模块开始/结束定义 | assign | 产生 wire 信号语句的关键字 |
| input / output / inout | 输入/输出/双向端口 | begin / end | 语句的起始/结束标志 |
| parameter | 参数定义 | posedge / negedge | 时序电路的标志(上升沿/下降沿) |
| wire / reg | 线网/寄存器信号定义 | case / default / endcase | case 语句起止与默认分支 |
| always | 产生 reg 信号语句的关键字 | if / else | 条件分支 |
程序框架:以 LED 流水灯为例
module led(
input sys_clk , //系统时钟
input sys_rst_n, //系统复位,低电平有效
output reg [3:0] led //4位LED灯
);
//parameter define
parameter WIDTH = 25 ;
parameter COUNT_MAX = 25_000_000; //板载50M时钟=20ns,0.5s/20ns=25000000,需要25bit位宽
//reg define
reg [WIDTH-1:0] counter ;
reg [1:0] led_ctrl_cnt;
//wire define
wire counter_en ;
//***********************************************************************************
//** main code
//***********************************************************************************
//计数到最大值时产生高电平使能信号
assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1 : 1'b0;
//用于产生0.5秒使能信号的计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (sys_rst_n == 1'b0)
counter <= 1'b0;
else if (counter_en)
counter <= 1'b0;
else
counter <= counter + 1'b1;
end
//led流水控制计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (sys_rst_n == 1'b0)
led_ctrl_cnt <= 2'b0;
else if (counter_en)
led_ctrl_cnt <= led_ctrl_cnt + 2'b1;
end
//通过控制IO口的高低电平实现发光二极管的亮灭
always @(posedge sys_clk or negedge sys_rst_n) begin
if (sys_rst_n == 1'b0)
led <= 4'b0;
else begin
case (led_ctrl_cnt)
2'd0 : led <= 4'b0001;
2'd1 : led <= 4'b0010;
2'd2 : led <= 4'b0100;
2'd3 : led <= 4'b1000;
default : ;
endcase
end
end
endmodule
逐段解读这段代码的"骨架":
- 模块定义:以
module开始、endmodule结束; - 端口定义:紧跟模块名后的括号内声明输入/输出信号(本例输入为时钟和低有效复位,输出为 4 位 led);
- 参数定义:
parameter放在端口之后,方便灵活调整计数最大值、位宽等(25_000_000中的下划线只是为了增强可读性); - reg/wire 定义:集中声明,各司其职;
- main code 注释分隔线:不添加也不会报错,但作为良好的编程规范推荐添加;
- assign 语句:组合逻辑,用条件运算符产生计数使能;
- always 语句:
always @(posedge sys_clk or negedge sys_rst_n)表示时钟上升沿或复位下降沿触发;begin/end是语句块起止;<=是非阻塞赋值(稍后详解);case以endcase结束、以default兜底。
begin/end 什么时候必须加:if 条件后只有一条赋值语句时,加不加都可以(推荐不加,更简洁);超过一条赋值语句时必须加:
//一条语句:两种写法皆可,推荐前者
if(en == 1'b1)
a <= 1'b1;
//多条语句:必须加begin/end
if(en == 1'b1) begin
b <= 1'b1;
c <= 1'b1;
end
8.4 Verilog 高级知识点
阻塞赋值与非阻塞赋值
这是 Verilog 最重要的概念之一,也是初学者最容易出问题的地方。
阻塞赋值(Blocking,符号 =):在同一个 always 块中,后面的语句受前面语句影响——一条阻塞赋值语句没有执行结束,后面的语句就不能执行,即被"阻塞"。begin/end 之间的语句是顺序执行的串行语句,这一点和 C 语言很像。
阻塞赋值的执行可看作一个步骤:计算 RHS(等号右边)的值并立即更新 LHS(等号左边),期间不允许其他语句干扰。
我们用一段时序逻辑下的阻塞赋值来看效果——功能要求:复位时 a=1、b=2、c=3;不复位时 a 清零,并把 a 赋给 b、b 赋给 c:


从波形图看:复位时 a=1、b=2、c=3;结束复位后,时钟上升沿到来(波形图中 2 时刻)时,a=0、b=0、c=0。因为阻塞赋值先执行 a=0,执行完后才把 a 的值(已经是 0)赋给 b,再把 b 的值(已经是 0)赋给 c——一条接一条串行完成。
非阻塞赋值(Non-Blocking,符号 <=):由时钟节拍决定。时钟上升沿到来时,先计算所有语句的 RHS,然后 begin/end 之间的所有赋值语句同时更新 LHS——一个时钟只执行一次,属于并行执行。这是与 C 语言最大的差异点。
非阻塞赋值可看作两个步骤:
- 赋值开始时,计算 RHS;
- 赋值结束时,更新 LHS。
"非阻塞"指的是:在计算 RHS 和更新 LHS 期间,允许其他非阻塞赋值语句同时计算 RHS 和更新 LHS。
同样的功能用非阻塞赋值实现:


从波形图看:复位时同样是 a=1、b=2、c=3;结束复位后时钟上升沿到来时,a=0、b=1、c=2——与阻塞赋值结果完全不同!因为三条语句的 RHS(0、a、b,即 0、1、2)是同时计算的、同时更新 LHS 的,b 和 c 拿到的是"旧值"。
使用场景总结(务必牢记):
- 描述组合逻辑时使用阻塞赋值
=——如 assign 语句、不带时钟的 always 语句:
//示例1:assign赋值语句
assign data = (data_en == 1'b1) ? 8'd255 : 8'd0;
//示例2:不带时钟的always语句
always @(*) begin
if (en) begin
a = a0;
b = b0;
end
else begin
a = a1;
b = b1;
end
end
- 描述时序逻辑时使用非阻塞赋值
<=——如带时钟的 always 语句:
//示例3:带时钟的always语句
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
a <= 1'b0;
b <= 1'b0;
end
else begin
a <= c;
b <= d;
end
end
assign 和 always 的区别
两者都是 Verilog 的基本语句,关键差异在"带不带时钟":
- assign 不能带时钟,只能产生组合逻辑;
- always 既可以不带时钟也可以带时钟。不带时钟时(
always @(*)),逻辑功能与 assign 完全一致,都是纯组合逻辑(虽然信号还定义成 reg 类型,但综合出来只是连线)。
经验法则:简单的组合逻辑用 assign,复杂的组合逻辑(如多层 if/case 嵌套)用不带时钟的 always;带时钟的 always 产生的才是真正的寄存器(时序逻辑)。
什么是 latch(锁存器)
latch 是电平敏感的存储单元,而寄存器是边沿触发的存储单元,两者都能存储数据,但来源不同:锁存器由组合逻辑意外产生,寄存器由时钟触发产生于时序电路。
latch 的主要危害是产生毛刺(glitch),对下一级电路非常危险,且隐蔽性强、不易查出,所以设计中应尽量避免 latch。
产生 latch 的原因:在不带时钟的 always 组合逻辑中,if 缺少 else 分支、或 case 缺少 default 分支,导致信号在某些输入条件下"保持原值",综合器只能生成锁存器来实现这种保持。
解决办法很简单:if 必须带 else,case 必须带 default。



对比上面两张综合电路图可以直观看到:缺少 else 的带时钟 always 综合出的 q 仍是普通的时钟寄存器(图 8.4.6);而缺少 else 的不带时钟 always 综合出的电路中出现了一个 latch(图 8.4.7)。这一点要特别注意——只有不带时钟的 always 才会因 if/case 不完整产生 latch。
状态机(FSM)
硬件电路是并行执行的,当需要按照流程或步骤完成某个功能时,如果用大量 if 嵌套来实现,代码复杂度会陡增、可读性会变差。这时就该用状态机了:它把一项功能的完成分解为若干步,每一步对应一个状态,通过预先设计的顺序在各状态之间转换,状态转换的过程就是实现逻辑功能的过程。
有限状态机(Finite State Machine,FSM)是一种在有限个状态之间按一定规律转换的时序电路,可视为组合逻辑与时序逻辑的组合。复杂流程控制(如 SDRAM 控制器)几乎都用状态机实现。
两大类模型(按输出是否与输入相关划分):
- Mealy(米勒)型状态机:输出不仅取决于当前状态,还取决于输入信号;
- Moore(摩尔)型状态机:输出只取决于当前状态,与输入无关。
三种实现写法:
- 一段式:整个状态机写在一个 always 模块里,既描述状态转移又描述输入输出。不推荐——组合逻辑与时序逻辑混在一起,不利于代码维护、修改和约束;
- 二段式:两个 always 模块,一个用同步时序描述状态转移,另一个用组合逻辑判断转移条件并描述输出。需要定义现态和次态两个状态;
- 三段式:三个 always 模块——第一段同步时序描述状态转移,第二段组合逻辑判断转移条件,第三段描述状态输出(可用组合电路或时序电路输出)。
实际应用中三段式状态机使用最多:它把组合逻辑和时序逻辑分开,有利于综合器分析优化和程序维护;又把状态转移与状态输出分开,代码更清晰易读。
三段式状态机的基本格式:
- 第一个 always:同步状态跳转;
- 第二个 always:组合逻辑判断状态转移条件;
- 第三个 always:描述状态输出。
编写状态机前一般先画出状态跳转图,思路会清晰很多。下面以 7 分频为例(实际分频功能未必需要状态机,此处仅演示写法):

状态编码采用独热码(每个状态只有一位为 1,也可以直接用十进制 0~7 编码),随后定义当前状态和下一个状态两个寄存器:
parameter S0 = 7'b0000001; //独热码定义方式
parameter S1 = 7'b0000010;
parameter S2 = 7'b0000100;
parameter S3 = 7'b0001000;
parameter S4 = 7'b0010000;
parameter S5 = 7'b0100000;
parameter S6 = 7'b1000000;
reg [6:0] curr_st ; //当前状态
reg [6:0] next_st ; //下一个状态
完整的三段式状态机代码如下:
module divider7_fsm (
//系统时钟与复位
input sys_clk ,
input sys_rst_n ,
//输出时钟
output reg clk_divide_7
);
//parameter define
parameter S0 = 7'b0000001; //独热码定义方式
parameter S1 = 7'b0000010;
parameter S2 = 7'b0000100;
parameter S3 = 7'b0001000;
parameter S4 = 7'b0010000;
parameter S5 = 7'b0100000;
parameter S6 = 7'b1000000;
//reg define
reg [6:0] curr_st ; //当前状态
reg [6:0] next_st ; //下一个状态
//*****************************************************
//** main code
//*****************************************************
//状态机的第一段采用同步时序描述状态转移
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
curr_st <= S0;
else
curr_st <= next_st;
end
//状态机的第二段采用组合逻辑判断状态转移条件
always @(*) begin
case (curr_st)
S0: next_st = S1;
S1: next_st = S2;
S2: next_st = S3;
S3: next_st = S4;
S4: next_st = S5;
S5: next_st = S6;
S6: next_st = S0;
default: next_st = S0;
endcase
end
//状态机的第三段描述状态输出(这里采用时序电路输出)
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_divide_7 <= 1'b0;
else if ((curr_st == S0) | (curr_st == S1) | (curr_st == S2) | (curr_st == S3))
clk_divide_7 <= 1'b0;
else if ((curr_st == S4) | (curr_st == S5) | (curr_st == S6))
clk_divide_7 <= 1'b1;
else
;
end
endmodule
代码要点解读:
- 第一段(同步时序):复位时回到 S0 状态,否则每个时钟沿把 next_st 更新到 curr_st——对应 Moore 模型中的"状态寄存器";
- 第二段(组合逻辑):根据当前状态判断下一状态——对应"产生下一状态的组合逻辑 F"。实际应用中一般根据输入条件决定跳转或保持,case 末尾的 default 用于防止状态机跑飞到异常状态;
- 第三段(状态输出):推荐使用时序电路输出而非组合电路输出,因为同步时序设计能提高稳定性、消除毛刺。采用时序输出的模型在输出组合逻辑 G 后再增加一级输出寄存器。

时序输出的好处:有效滤除组合逻辑输出的毛刺;更利于时序计算与约束;对总线形式的输出,更容易使总线数据对齐、减小偏移,降低接收端采样出错概率。
从代码可以看出,输出 clk_divide_7 只与当前状态 curr_st 有关、与输入无关,所以这个状态机属于 Moore 型。
模块化设计与模块例化
模块化设计是 FPGA 设计中极为重要的技巧:不同团队可专注各自的子模块(分工协作);可对单个子模块精准仿真调试;修改某一子模块不影响其他模块(维护升级方便)。划分模块的基本原则是:子模块功能相对独立、模块内部联系尽量紧密、模块间的连接尽量简单。
复杂系统一般采用自顶向下的设计方式:系统划分成几个功能模块,每个功能模块再划分成下一层子模块,每个模块对应一个 module、一个 Verilog 文件。顶层模块一般只做例化(调用其他模块),不做逻辑。

模块例化(模块调用/实例化)是连接子模块与高层模块的手段,既简化每个模块的代码,又便于后期维护。以静态数码管为例:驱动数码管静态显示需要两个模块——计时模块(time_count)和数码管静态显示模块(seg_led_static),再加一个顶层模块:

两个子模块的端口定义(节选):
//计时模块
module time_count(
input clk , // 时钟信号
input rst_n , // 复位信号
output reg flag // 一个时钟周期的脉冲信号
);
//parameter define
parameter MAX_NUM = 25000_000; // 计数器最大计数值
……
endmodule
//数码管静态显示模块
module seg_led_static (
input clk , // 时钟信号
input rst_n , // 复位信号(低有效)
input add_flag, // 数码管变化的通知信号
output reg [5:0] sel , // 数码管位选
output reg [7:0] seg_led // 数码管段选
);
……
endmodule
顶层模块对两个子模块进行例化:
module seg_led_static_top (
input sys_clk , // 系统时钟
input sys_rst_n, // 系统复位信号(低有效)
output [5:0] sel , // 数码管位选
output [7:0] seg_led // 数码管段选
);
//parameter define
parameter TIME_SHOW = 25'd25000_000; // 数码管变化的时间间隔0.5s
//wire define
wire add_flag; // 数码管变化的通知信号
//*****************************************************
//** main code
//*****************************************************
//例化计时模块
time_count #(
.MAX_NUM (TIME_SHOW)
) u_time_count(
.clk (sys_clk ),
.rst_n (sys_rst_n),
.flag (add_flag )
);
//例化数码管静态显示模块
seg_led_static u_seg_led_static (
.clk (sys_clk ),
.rst_n (sys_rst_n),
.add_flag (add_flag ),
.sel (sel ),
.seg_led (seg_led )
);
endmodule
例化的两个关键点:
- 端口例化:
.子模块端口(顶层信号)一一对应连接,对应信号的位宽必须保持一致;例化模块名一般命名为u_+ 子模块名(同一模块可被例化多次,用不同例化名区分,如u_、u1_); - 参数例化(参数传递):在模块名后加
#跟参数列表,如上例中顶层用TIME_SHOW覆盖了子模块的MAX_NUM。参数例化后,子模块内该参数的实际值以顶层传递的为准;若不例化参数,则使用子模块内部定义的值。
补充两个相关语法:
- localparam:本地参数,用法与 parameter 基本一致,区别是 localparam 不能被上层模块例化,适合定义模块内部使用的常量;
- 顶层模块例化子模块时,只需要知道子模块的端口信号名,不需要关心其内部实现——这正是模块化设计"接口先行"思想的体现。
8.5 Verilog 编程规范
良好的编程规范是 FPGA 工程师的必备素质:多人协作时需要互相检视代码,没有统一规范,检视、维护、重用的工作量不可想象;即使是自己写的代码,几个月后再看也会变得陌生。规范能让新人尽快融入项目,也让别人更容易看懂你的代码。
工程组织形式:一个工程目录一般包含四部分:
XX工程名
|--doc //存放工程相关文档:datasheet(数据手册)、设计方案等
|--prj //存放工程文件和用到的IP文件
|--rtl //存放工程rtl代码(工程核心),文件名与module名一致,按模块层次分开存放
|--sim //存放工程仿真代码,复杂工程的仿真不可或缺,能极大减少调试工作量
文件头声明:每个 Verilog 文件开头必须有版权、作者、创建日期、内容介绍等声明文字。建议一个 .v 文件只包含一个 module,保持清晰易懂。
输入输出定义:推荐采用 Verilog-2001 语法格式(端口直接写在 module 括号内),建议一行只定义一个信号、信号全部对齐、同一组信号放在一起。
parameter/wire/reg 定义:parameter 紧跟端口定义之后、常量命名全大写;wire/reg 集中定义、同功能信号放在一起、信号对齐、位宽用降序 [6:0]、时钟用 clk 前缀、复位用 rst 后缀、不能用关键字作信号名、一行只定义一个信号。
信号命名:命名要体现意义(如 fifo_wr_en 表示 FIFO 写使能);用 _ 分隔单词(如 sys_clk);内部信号全小写、模块名小写;低电平有效用 _n 后缀、异步信号用 _a 后缀、纯延迟打拍信号用 _dly 后缀。
always 块:一个 always 配一对 begin/end(begin 与 always 同行);always 前加注释;时钟复位触发统一写 posedge sys_clk or negedge sys_rst_n;一个 always 块只包含一个时钟和复位;时序逻辑用非阻塞赋值。
assign 块:逻辑不能太复杂(否则可读性差);assign 前加注释;组合逻辑用阻塞赋值。
其他规范:不用 TAB 全用空格(不同解释器对 TAB 翻译不一致);注释清晰简洁不废话、统一用 //、注释对齐;核心代码和信号定义之间需要增加注释;模块例化用 u_xx 命名。
其他注意事项:
- 代码写得越简单越好,方便他人阅读和理解;
- 不使用 repeat 等循环语句;
- RTL 级代码不使用 initial 语句(仿真代码除外);
- 避免产生 latch:组合逻辑里的 if 必须带 else、case 必须带 default;
- 避免使用太复杂和少见的语法,否则可能降低综合器优化力度。
初学者容易踩的坑
- 用 C 语言的串行思维理解 Verilog:begin/end 之间用阻塞赋值时才是顺序关系,带时钟 always 里的非阻塞赋值是并行同时发生的。
b <= a拿到的是"旧"的 a。时刻提醒自己:写的是电路,不是程序。 - 阻塞/非阻塞赋值混用或用反:口诀"组合逻辑用 =,时序逻辑用 <="。在带时钟的 always 里用
=,或者在同一个 always 里混用两种赋值,都会造成仿真与综合行为不一致的隐蔽 bug。 - reg 类型被误认为一定是寄存器:不带时钟的 always 中定义的 reg,综合出来只是组合逻辑连线。判断标准是看 always 带不带时钟,而不是看类型声明。
- 组合逻辑 if 缺 else、case 缺 default,产生 latch:latch 带来的毛刺很难排查。养成习惯——不带时钟的 always 里 if 必配 else、case 必配 default(哪怕分支内容为空语句)。
- 例化时位宽不匹配:模块端口与顶层连接信号的位宽必须一致,否则会出现隐式的截位/扩展,行为难以预期。
- 记不住运算符优先级又不加括号:不同类型运算符混用时务必用括号明确优先级,避免歧义。
- 状态机没画状态跳转图就动手写代码:先画图后写码,思路清晰不易漏状态;case 记得写 default,防止状态机进入异常状态后无法恢复。
小结 & 下一步
本章是全书语法篇的核心,覆盖了 Verilog 从基础到进阶的完整知识链:
- 四个逻辑值(0/1/X/Z)、标识符命名规则、进制表示(默认 32 位十进制);
- 三大数据类型:reg(时序为寄存器、组合为连线)、wire(物理连线)、parameter(可例化的常量);
- 七类运算符,重点是位运算与逻辑运算的区别、移位代替 2 的幂次乘除、拼接运算符
{}; - 程序框架:module 端口 + parameter + reg/wire + assign/always;
- 核心难点:阻塞赋值(组合、串行)与非阻塞赋值(时序、并行)的波形级差异;assign 与 always 的分工;latch 的成因(组合逻辑 if/case 不完整)与规避;
- 状态机:Mealy/Moore 两大模型,一段式/二段式/三段式三种写法,重点掌握三段式(时序跳转 + 组合译码 + 时序输出);
- 模块化设计:顶层只做例化不做逻辑,掌握端口例化与参数例化(
#)语法; - 编程规范:doc/prj/rtl/sim 工程组织、命名约定、代码风格,是一个专业 FPGA 工程师的基本素养。
语法规则只有落到代码里才能内化。下一步进入第九章"数字电路基础",补齐进制转换、逻辑门、组合/时序逻辑等硬件设计的理论地基,之后我们就可以开始一个个真正的外设实验了。
本笔记基于正点原子《领航者 ZYNQ 之 FPGA 开发指南》第八章整理,图片版权归原厂所有。