Verilog入门笔记
Verilog 概述
Verilog是一种硬件描述语言,以文本形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。
Verilog 和 VHDL 区别
这两种语言都是用于数字电路系统设计的硬件描述语言,而且都已经是IEEE的标准。 VHDL 1987年成为标准,而 Verilog是 1995年才成为标准的。这是因 为 VHDL是美国军方组织开发的,而 Verilog是由一个公司的私有财产转化而来。为什么 Verilog能成为 IEEE标呢?它一定有其独特的优越性才行,所以说Verilog有更强的生命力。这两者有其共同的特点:
- 能形式化的抽象表示电路的行为和结构;
- 支持逻辑设计中层次与范围的描述;
- 可借用高级语言的精巧结构来简化电路行为和结构;
- 支持电路描述由高层到低层的综合转换;
- 硬件描述和实现工艺无关。
Verilog 和 C 的区别
Verilog是硬件描述语言,在编译下载到 FPGA之后,FPGA会生成电路,所以 Verilog全部是并行处理与运行的;C语言是软件语言,编译下载到 单片机 /CPU之后,还是软件指令,而不会根据你的代码生成相应的硬件电路,而单片机 /CPU处理软件指令需要取址 、译码、执行,是串行执行的。
Verilog和 C的 区别也是 FPGA和 单片机 /CPU的区别, 由于 FPGA全部并行处理, 所以处理速度非常
快 ,这个是 FPGA的 最大优势,这一点是单片机 /CPU替代不了的。
Verilog 基础知识
Verilog 的逻辑值
在逻辑电路中有四种值,即四种状态:
- 逻辑 0:表示低电平
- 逻辑 1:表示高电平
- 逻辑 X:表示未知态,可能为高电平,也可能为低电平
- 逻辑 Z:表示高阻态,在外部没有激励信号的时候处于悬空状态
Verilog 的标识符
Verilog的标识符可以是任意一组字母、数字、$和 _(下划线 )符号的组合,但标识符的第一个字符必须是字母或者下划线。另外,标识符是区分大小写的,以下是一些书写规范的要求:
- 用有意义的有效的名字如 sum、cpu_addr等
- 用下划线区分词语组合,如 cpu_addr
- 采用一些前缀或后缀,比如:时钟采用 clk前缀: clk_50m clk_cpu;低电平采用 _n后缀: enable_n
- 统一缩写,如全局复位信号 rst
- 同一信号在不同层次保持一致性,如同一时钟信号必须在各模块保持一致
- 自定义的标识符不能与保留字(关键词)同名
- 参数统一采用大写,如定义参数 使用 SIZE
Verilog 的数字进制格式
- 二进制表示如下:4’b0101表示4位 二进制数字 0101
- 十进制表示如下:4’d2表示4位十进制数字2(二进制 0010)
- 十六进制表示如下:4’ha表示4位十六进制数字 a(二进制 1010),十六进制 的计数方式为 0 1 2…9,a b c d e f 最大计数为 f( f:十进制表示为 15)。
- 当代码中没有指定数字的位宽与进制时,默认为32位的十进制,比如 100,实际上表示的值为 32’d100
Verilog 的数据类型
寄存器类型(reg)
- 寄存器类型表示一个抽象的数据存储单元,它只能在always语句和 initial语句中被赋值,并且它的值从一个赋值到另一个赋值过程中被保存下来
- 如果该过程语句描述的是时序逻辑,即 always语句带有时钟信号,则该寄存器变量对应为寄存器;
- 如果该过程语句描述的是组合逻辑,即 always语句不 带有时钟信号,则该寄存器变量对应为硬件连线;寄存器类型的缺省值是 x(未知状态)。
//reg define
reg [31:0] delay_cnt; //延时计数器
reg key_flag ; //按键标志
线网类型(wire)
- 线网表示Verilog结构化元件间的物理连线
- 它的值由驱动元件的值决定,例如连续赋值或门的输出
- 如果没有驱动元件连接到线网,线网的缺省值为 z(高阻态)
//wire define
wire data_en; //数据使能信号
wire [7:0] data ; //数据
参数类型(parameter)
参数其实就是一个常量,常被用于定义状态机的状态、数据位宽和延迟大小等,由于它可以在编译时修改参数的值,因此它又常被用于一些参数可调的模块中,使用户在实例化模块时,可以根据需要配置参数。在定义参数时,我们可以一次定义多个参数,参数与参数之间需要用逗号隔开。这里我们需要注意的是参数的定义是局部的,只在当前模块中有效。它的使用方法如下:
//parameter define
parameter DATA_WIDTH = 8; //数据位宽为8位
Verilog 的运算符
Verilog中的运算符按照 功能 可以分为下述类型:
- 算术运算符:+ - * / %
- 关系运算符:> < >= <= == !=
- 逻辑运算符:&& || !
- 条件运算符:a ? b : c 如果a为真,那么选择b,否则选择c
- 位运算符:~ & | ^
- 移位运算符:<< >>
- 拼接运算符:{a, b} 将a和b拼接起来,作为一个新信号
Verilog 程序框架
注释
- 单行注释使用 //
- 多行注释使用 /* */
常用关键词
关键词 | 含义 |
---|---|
module | 模块开始定义 |
input | 输入端口定义 |
output | 输出端口定义 |
inout | 双向端口定义 |
parameter | 信号的参数定义 |
wire | wire信号定义 |
reg | reg信号定义 |
always | 产生reg信号语句的关键词 |
assign | 产生wire信号语句的关键词 |
begin | 语句的起始标志 |
end | 语句的结束标志 |
posedge/negedge | 时序电路的标志 |
case | case语句起始标记 |
default | case语句的默认分支标志 |
endcase | case语句结束标志 |
if | if/else语句标志 |
else | if/else语句标志 |
for | for语句标志 |
endmodule | 模块结束定义 |
程序框架
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
- 第1行为模块定义 ,模块定义以module开始,endmodule结束,如59行所示 。
- 其次2到5行为端口定义,需要定义led模块的输入信号和输出信号此处输入信号为系统时钟和复位信号,输出为led控制信号。
- 7到9行为参数 parameter定义语法如7到9行所示,定义parameter的好处是可以灵活改变参数数字就能控制一些 计数器最大计数值或者信号位宽的最大位宽。
- 12到14行为reg信号定义reg信号一般情况下代表寄存器比如此处控制0.5秒使能信号的计数器counter。
- 16到17行为wire信号定义wire信号就是硬件连线,比如此处的counter_en代表计数到最大值时产生高电平使能,本质上是一个硬件连线,其实代表的是一些计数器/寄存器做逻辑判断的结果。
- 19到21行为moudle开始的注释,不添加工具综合也不会报错,但是我们推荐添加,作为 一个良好的编程规范。
- 23到24行为assign语句的样式,条件成立选择1,否则选择0。
- 26到34行是always语句的样式,27行代表在时钟上升沿或者复位的下降沿进行信号触发。begin/end代表语句的开始和结束。
- 28到33行为if/else语句,和C语言是比较类似的。
- 29行的“<=”标记代表信号是非阻塞赋值,信号赋值有非阻塞赋值和阻塞赋值两个方式。
- 36和42行也是一个always语句,和26到34行类似。
- 44和57行也是一个always语句,不过这个always语句中嵌入了一个case语句,case语句的语法如49到55行所示,需要一个case关键字开始endcase关键字结束 default作为 默认分支,和C语言也是类似的。当然case语句也可以用在不带时钟的always语句中,不过本例子的always都是带有时钟的。不带时钟的always和带时钟的always语句的差异这个我们后面也会详细解释。
- 59行是endmodule标记代表模块的结束。
Verilog 高级知识点
阻塞赋值(Blocking) “=”
阻塞赋值,顾名思义即在一个always块中,后面的语句会受到前语句的影响,具体来说 就是 在同一个always中,一条阻塞赋值语句如果没有执行结束,那么该语句后面的语句就不能被执行,即被 “阻塞 ”。也就是说 always块内的语句是一种顺序关系,这里和 C语言很类似 。符号“ “=”用于阻塞的赋值(如 :b =阻塞赋值“ “=”在begin和 end之间的语句是顺序执行,属于串行语句。
在这里定义两个缩写:
- RHS:赋值等号右边的表达式或变量可以写作 RHS表达式或 RHS变量
- LHS 赋值等号左边的表达式或变量可以写作 LHS表达式或 LHS变量
阻塞赋值的执行可以认为是只有一个步骤的操作,即计算RHS的值并更新 LHS,此时不允许任何其他语句的干扰,所谓的阻塞的概念就是值在同一个 always块中,其后面的赋值语句从概念上来讲是在前面一条语句赋值完成后才执行的。
//阻塞赋值代码
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
a = 1;
b = 2;
c = 3;
end
else begin
a = 0;
b = a;
c = b;
end
end
代码中使用的是阻塞赋值语句,从波形图中可以看到,在复位的时候(rst_n=0 a=1 b=2 c=3;而结束复位之后(波形图中的 0时刻),当 clk的上升沿到来时(波形图中的 2时刻), a=0 b=0 c=0。这是因为阻塞赋值是在当前语句执行完成之后,才会执行后面的赋值语句,因此首先执行的是 a=0,赋值完成后将 a的值赋值给 b,由于此时 a的值已经为 0,所以 b=a=0,最后执行的是将 b的值赋值给 c,而 b的值已经赋值为 0,所以 c的值同样等于 0。
非阻塞赋值(Non-Blocking) “<=”
符号 “<=” 用于非阻塞赋值(如 :b <= a;),非阻塞赋值是由时钟节拍决定,在时钟上升到来时,执行赋值语句右边,然后将 begin end之间的所有赋值语句同时赋值到赋值语句的左边,注意:是 begin end之间的所有语句,一起执行,且 一个时钟只执行一次 ,属于并行执行语句。这个 是和 C语言 最大的一个差异点,大家要逐步理解并行执行的概念。
- 非阻塞赋值的操作过程可以看作两个步骤:
- 赋值开始的时候,计算 RHS
- 赋值结束的时候,更新 LHS。
所谓的非阻塞的概念是指,在计算非阻塞赋值的RHS以及 LHS期间,允许其它的非阻塞赋值语句同时计算 RHS和更新 LHS。
//非阻塞赋值代码
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
a <= 1;
b <= 2;
c <= 3;
end
else begin
a <= 0;
b <= a;
c <= b;
end
end
代码中使用的是非阻塞赋值语句,从波形图中可以看到,在复位的时候(rst_n=0 a=1 b=2 c=3而结束复位之后(波形图中的 0时刻),当 clk的上升沿到来时(波形图中的 2时刻), a=0 b=1 c=2。这是因为非阻塞赋值在计算 RHS和更新 LHS期间,允许其它的非阻塞赋值语句同时计算 RHS和更新 LHS。在波形图中的 2时刻, RHS的表达是 0、 a、 b,分别等于 0、 1、 2,这三条语句是同时更新 LHS,所以 a、b、 c的值分别等于 0、 1、 2。
阻塞和非阻塞赋值使用总结
在描述组合逻辑电路的时候,使用阻塞赋值,比如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 区别
- assign语句使用时不能带时钟
- always语句可以带时钟也可以不带时钟。在 always不带时钟时 ,逻辑功能和 assign完全 一致,都是只 产生组合逻辑 。比较简单的组合逻辑推荐使用 assign语句,比较复杂的组合逻辑推荐使用always语句。
assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1 : 1'b0;
always @(*) 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 : led = 4'b0000;
endcase
end
带时钟和不带时钟的 always
always语句可以带时钟也可以不带时钟 。 在 always不带时钟时 ,逻辑功能和 assign完全 一致,虽然
产生的信号定义还是 reg类型 ,但是该 语句产生的还是组合逻辑。
assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1 : 1'b0;
always @(*) 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 : led = 4'b0000;
endcase
end
在always带时钟信号时 ,这个逻辑语句才能产生真正的寄存器,如下示例 counter就是真正的寄存器 。
//用于产生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
latch(锁存器)
latch是指锁存器 ,是一种对脉冲电平敏感的存储单元电路。锁存器和寄存器都是基本存储单元,锁存器是电平触发的存储器,寄存器是边沿触发的存储器。两者的基本功能是一样的,都可以存储数据。锁存器是组合逻辑产生的,而寄存器 是在时序电路中使用,由时钟触发产生的。
latch的主要危害是会产生毛刺( glitch),这种毛刺对下一级电路是很危险的。并且其隐蔽性很强,不易查出。因此,在设计中,应尽量避免 latch的使用 。代码里面出现latch的两个原因是在组合逻辑中, if或者case语句不完整的描述, 比如 if缺少 else分支,case缺少 default分支 ,导致代码在综合过程中出现了latch。解决办法就是 if必须带 else分支, case必须带default分支 。
只有不带时钟的 always语句 if或者 case语句不完整才会产生 latch ,带时钟的语句 if或者 case语句不完整描述不会产生 latch。下面为缺少 else分支的带时钟的 always语句和不带时钟的always语句, 通过实际产生的电路图可以看到第二个是有一个 latch的 ,第 一个仍然是普通的带有时钟的寄存器。
- 缺少else的带时钟的always语句电路图
- 缺少else的不带时钟的always语句电路图
状态机
状态机,全称是有限状态机( Finite State Machine,缩写为 FSM),是一种在有限个状态之间按一定规律转换的时序电路,可以认为是组合逻辑和 时序 逻辑的一种组合。 状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显,因此基本上都会用到状态机,如 SDRAM控制器等。根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore)型状态机和米勒 (Mealy)型状态机。
- Mealy状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。
- Moore 状态机:组合逻辑的输出只取决于当前状态。
Mealy 状态机
米勒状态机的模型如下图所示,模型中第一个方框是指产生下一状态的组合逻辑F,F 是当前状态和输入信号的函数,状态是否改变、如何改变,取决于组合逻辑F 的输出;第二框图是指状态寄存器,其由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在时钟的跳边沿;第三个框图是指产生输出的组合逻辑G,状态机的输出是由输出组合逻辑G 提供的,G 也是当前状态和输入信号的函数。
Moore 状态机
摩尔状态机的模型如下图所示,对比米勒状态机的模型可以发现,其区别在于米勒状态机的输出由当前状态和输入条件决定的,而摩尔状态机的输出只取决于当前状态。
三段式状态机
根据状态机的实际写法,状态机还可以分为一段式、二段式和三段式状态机。
- 一段式:整个状态机写到一个always 模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。不推荐采用这种状态机,因为从代码风格方面来讲,一般都会要求把组合逻辑和时序逻辑分开;从代码维护和升级来说,组合逻辑和时序逻辑混合在一起不利于代码维护和修改,也不利于约束。
- 二段式:用两个always 模块来描述状态机,其中一个always 模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。不同于一段式状态机的是,它需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑。
- 三段式:在两个always 模块描述方法基础上,使用三个always 模块,一个always 模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个 always模块描述状态输出 (可以用组合电路输出,也可以时序电路输出 )。
实际应用中三段式状态机 使用最多, 因为三段式状态机将组合逻辑和时序分开,有利于综合器分析优化以及程序的维护;并且三段式状态机将状态转移与状态输出分开,使代码看上去更加清晰易懂,提高了代码的可读性。
三段式状态机的基本格式是:
- 第一个always语句实现同步状态跳转
- 第二个always语句 采用组合逻辑判断状态转移条件
- 第三个always语句描述状态输出 (可以用组合电路输出,也可以时序电路输出 )
举例(7分频)
- 首先画出状态跳转图
- 通过parameter来定义不同状态的参数
//采用独热码定义方式
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;
- 定义两个2个7位的寄存器,用来表示当前状态和下个状态
reg [6:0] curr_st ; //当前状态
reg [6:0] next_st ; //下一个状态
-
最后使用3个always语句来编写状态机代码
-
第一个 always采用同步时序描述状态转移
always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) curr_st <= S0; else curr_st <= next_st; end
-
第二个 always采用组合逻辑判断状态转移条件
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是 描述状态输出
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
-
从代码中可以看出,输出的分频时钟clk_divide_7只与当前状态( curr_st)有关,而与输入状态无关所以属于摩尔型状态机。状态机的第一段对应摩尔状态机模型的状态寄存器,用来记忆状态机当前所处的状态;状态机的第二段对应摩尔状态机模型产生下一状态的组合逻辑 F;状态机的第三段对应摩尔状态机产生输出的组合逻辑 G,因为采用时序电路输出有很大的优势,所以这里第三段状态机是由时序电路输的。
状态机采用时序逻辑输出的状态机模型如下图所示:
采用这种描述方法虽然代码结构复杂了一些,但是这样做的好处是可以有效地滤去组合逻辑输出的毛
刺,同时也可以更好的进行时序计算与约束,另外对于总线形式的输出信号来说,容易使总线数据对齐,
减小总线数据间的偏移,从而降低接收端数据采样出错的频率。
模块化设计
在进行模块化设计中,对于复杂的数字系统,我们一般采用自顶向下的设计方式。可以把系统划分成几个功能模块,每个功能模块再划分成下一层的子模块;每个模块的设计对应一个module,一个module 设计成一个Verilog 程序文件。因此,对一个系统的顶层模块,我们采用结构化的设计,即顶层模块分别调用了各个功能模块。
下面以一个实例(静态数码管显示实验 )来说明模块和模块之间的例化方法。
在静态数码管显示实验中,我们根据功能 将 FPGA顶层例化了以下两个模块:计时模块( 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
-
模块的例化
-
模块参数的例化
-
模块参数例化注意点
- 在对参数进行例化时,在模块名的后面加上“#”,表示后面跟着的是参数列表
- 计时模块定义的MAX_NUM 和顶层模块的TIME_SHOW 都是等于25000_000,当在顶层模块定义TIME_SHOW=12500_000时,那么子模块的MAX_NUM的值实际上也等于 12500_000。当然即使子模块包含参数,在做模块的例化时也可以不添加对参数的例化,这样的话,子模块的参数值等于该模块内部实际定义的值
- Verilog语法中的 localparam代表的意思同样是参数定义,用法和 parameter基本一致,区别在于 parameter定义的参数可以做例化,而 localparam定义的参数是指本地参数,上层模块不可以对localparam定义的参数做例化
Verilog 编程规范
工程架构(Vivado软件)
工程的组织形式一般包括如下几个部分 ,分别 是 doc、 par、 rtl和 sim四个部分。
xxx工程名
|–doc
|–par
|–rtl
|–sim
- doc:一般存放工程相关的文档,包括该项目用到的 datasheet(数据手册)、设计方案等
- par:主要存放工程文件和使用 到的一些 IP文件
- rtl:主要存放工程的 rtl代码,这是工程的核心,文件名与 module名称应当一致,建议按照模块的层次分开存放
- sim:主要存放工程的仿真代码 复杂的工程里面,仿真也是不可或缺的部分 可以极大减少调试的工作量
输入输出定义
module led(
input sys_clk , //系统时钟
input sys_rst_n, //系统复位,低电平有效
output reg [3:0] led //4位LED灯
);
- 一行只定义一个信号
- 信号全部对齐
- 同一组的信号放在一起
parameter 定义
//parameter define
parameter WIDTH = 25 ;
parameter COUNT_MAX = 25_000_000; //板载50M时钟=20ns,0.5s/20ns=25000000,需要25bit //位宽
- module中的 parameter声明,不建议随处乱放
- 将 parameter定义放在紧跟着 module的输入输出定义之后
- parameter等常量命名全部使用大写
wire / reg 定义
//reg define
reg [WIDTH-1:0] counter ;
reg [1:0] led_ctrl_cnt;
//wire define
wire counter_en ;
- 将 reg与 wire的定义放在紧跟着 parameter之后
- 建议具有相同功能的信号集中放在一起
- 信号需要对齐,reg和位宽需要空 2格,位宽和信号名字至少空四格
- 位宽使用降序描述,[6:0]
- 时钟使用前缀 clk,复位使用后缀 rst
- 不能使用 Verilog关键字作为信号名字
- 一行只定义一个信号
信号命名
- 信号命名需要体现其意义,比如 fifo_wr代表 FIFO读写使能
- 可以使用“ “_”隔开信号,比如 sys_clk
- 内部信号不要使用大写,也不要使用大小写混合,建议全部使用小写
- 模块名字使用小写
- 低电平有效的信号,使用 _n作为信号后缀
- 异步信号,使用 _a作为信号后缀
- 纯延迟打拍信号使用 _dly作为后缀
always 块描述方式
//用于产生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
- if需要空四格
- 一个 always需要配一个 begin和 end
- always前面需要有注释
- beign建议和 always放在同一行
- 一个 always和下一个 always空一行即可,不要空多行
- 时钟复位触发描述使用 posedge sys_clk和 negedge sys_rst_n
- 一个 always块只包含一个时钟和复位
- 时序逻辑使用非阻塞赋值
assign 块描述方式
//计数到最大值时产生高电平使能信号
assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1 : 1'b0;
- assign的逻辑不能太复杂,否则易读性不好
- assign前面需要有注释
- 组合逻辑使用阻塞赋值
空格和TAB
- 由于不同的解释器对于TAB翻译不一致,所以建议不使用 TAB,全部使用空格
注释
//用于产生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_en为1时,counter清0
counter <= 1'b0;
else
counter <= counter + 1'b1;
end
- 注释描述需要清晰、简洁
- 注释描述不要废话,冗余
- 注释描述需要使用 “//”
- 注释描述需要对齐
- 核心代码和信号定义之间需要增加注释
模块例化
//例化计时模块
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 )
);
- moudle模块例化使用 u_xx表示
其它注意事项
- 代码写的越简单越好,方便他人阅读和理解
- 不使用 repeat等循环语句
- RTL级别代码里面不使用 initial语句,仿真代码除外
- 避免产生 Latch锁存器, 比如组合逻辑里面 的 if不带 else分支 、case缺少 default语句
ys_rst_n == 1’b0)
counter <= 1’b0;
else if (counter_en) // counter_en为1时,counter清0
counter <= 1’b0;
else
counter <= counter + 1’b1;
end
- 注释描述需要清晰、简洁
- 注释描述不要废话,冗余
- 注释描述需要使用 “//”
- 注释描述需要对齐
- 核心代码和信号定义之间需要增加注释
### 模块例化
```verilog
//例化计时模块
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 )
);
- moudle模块例化使用 u_xx表示
其它注意事项
- 代码写的越简单越好,方便他人阅读和理解
- 不使用 repeat等循环语句
- RTL级别代码里面不使用 initial语句,仿真代码除外
- 避免产生 Latch锁存器, 比如组合逻辑里面 的 if不带 else分支 、case缺少 default语句
- 避免使用太复杂和少见的语法,可能造成语法综合器优化力度较低