逻辑值规则

Verilog中有四种逻辑状态:

  1. 逻辑0:表示低电平GND。
  2. 逻辑1:表示高电平VCC。
  3. 逻辑x:未定义状态,有可能是高电平,也可能是低电平。
  4. 逻辑z:高阻态状态,没有外部激励信号的悬空状态。

image.png

标识符规则

  1. 标识符用于定义模块名、端口名等。
  2. 标识符可以是包括字母、数字、$、下划线的任意字符串。
  3. 标识符的第一个字符只能是字母或下划线。
  4. 标识符区分大小写。
  5. 普通内部信号标识符建议小写,参数建议大写。

    数字进制格式

  6. Verilog支持二进制、八进制、十进制、十六进制这几种数字进制格式。

  • 4'b1010二进制四位宽的1010(十进制10)
  • 4'd2十进制四位宽的数字2
  • 5'h0b十六进制五位宽数字0x0b(十进制11,二进制01011)
  1. 当没有指定数字位宽和进制时,默认使用32位宽的十进制数字。
  • 100表示十进制32位宽数字100

    变量数据类型

    数据类型可以分为三大类,包括寄存器类型、线网类型、参数类型。

    reg - 寄存器类型

  • 寄存器类型标识一个抽象的存储单元,它只能在always语句和initial语句中被赋值。它的值在赋值过程中被保存下来。

  • 如果赋值语句在时序逻辑电路中,即在always语句中带有时钟信号,则该寄存器被综合为触发器。
  • 如果赋值语句在组合逻辑电路中,即在always语句中没有时钟信号,则该寄存器被综合为硬连线。
  • 寄存器数据类型有很多种,如regintegerreal等,常用的是reg类型。
  • 寄存器类型的缺省值为x,未知状态。

    1. reg [15:0] counter; // 16位宽寄存器类型

    wire - 线网类型

  • 线网类型表示元器件之间的物理连线。

  • 线网类型的变量的指由驱动元件的值决定,如果没有驱动输入,缺省值为z高阻态。
  • 线网类型有很多种,如triwire等,常用的是wire

    1. wire data_en; // 数据使能信号

    parameter - 参数类型

  • 参数类型其实就是一个常量,常被用于定义状态机的状态、数据位宽和延迟大小等。

  • 参数类型常被用于一些参数可调的模块中。
  • 在定义参数时,可以一次定义多个参数,参数与参数之间需要用逗号隔开。
  • 参数的定义是局部的,只在当前模块module中可用。
    1. //parameter define
    2. parameter DATA_WIDTH = 8; //数据位宽为8位

    运算符规则

    Verilog的运算符包括:算术运算符、关系运算符、逻辑运算符、条件运算符、位运算符、移位运算符、拼接运算符这7类。

    1. 算术运算符

    | 符号 | 使用方法 | 说明 | | —- | —- | —- | | + | a + b | a 加上 b | | - | a - b | a 减去 b | | | a b | a 乘以 b | | / | a / b | a 除以 b | | % | a % b | a 取模(取余) b |

需要说明的是,Verilog的乘除法实现起来比较耗费资源,所以在以2的指数次幂的乘除法时,一般使用移位方式运算;非2的指数次幂的乘除法一般调用现成的IP来实现。

2. 关系运算符

符号 使用方法 说明
> a > b a 大于 b
< a < b a 小于 b
>= a >= b a 大于等于 b
<= a <= b a 小于等于 b
== a == b a 等于 b
!= a != b a 不等于 b

3. 逻辑运算符

符号 使用方法 说明
! !a 非a,a的值为0则结果为1
&& a && b a 与 b,如果a和b都是1,结果才是1,否则结果为0
|| a || b a 或 b,如果a和b之中有一个1,结果就是1;否则a和b全是0时,结果为0

4. 条件运算符

符号 使用方法 说明
? : a ? b : c 如果a是真,结果就是b,否则结果是c。

5. 位运算符

符号 使用方法 说明
~ ~a 将a的每一位取反
& a & b 按位与,将a的每一位和b对应的位相与
| a | b 按位或,将a的每一位和b对应的位相或
^ a ^ b 按位异或,将a的每一位和b对应的位相异或。异或是指如果两个操作数不同,异或结果是1;如果两个操作数相同,异或结果就是0。

6. 移位运算符

符号 使用方法 说明
<< a << b 将a的值左移b位
>> a >> b 将a的值右移b位

移位运算符常用于乘除法的运算。数据位丢失时使用0补齐。

7. 拼接运算符

符号 使用方法 说明
{} c = {a, b} 将a与b拼接起来的数值赋值到c中

8. 运算符优先级

image.png

程序编写框架

注释

  1. // 这是单行注释
  2. /*
  3. 这是多行注释
  4. 这是注释块里的内容
  5. */

关键字(共103个)

and always assign begin buf bufif0 bufif1 case
casex casez cmos deassign default defparam disable edge
else end endcase endfunction endprimitive endmodule endspecify endtable
endtask event for force forever fork function highz0
highz1 if ifnone initial inout input integer join
large macromodule medium module nand negedge nor not
notif0 notif1 nmos or output parameter pmos posedge
primitive pulldown pullup pull0 pull1 rcmos real realtime
reg release repeat rnmos rpmos rtran rtranif0 rtranif1
scalared small specify specparam strength strong0 strong1 supply0
supply1 table task tran tranif0 tranif1 time tri
triand trior trireg tri0 tri1 vectoted wait wand
weak0 weak1 while wire wor xnor xor

常用的一些关键字及其含义如下:

关键字 含义
module / endmodule 模块开始定义、结束定义
input / output / inout 输入端口定义、输出端口定义、双向端口定义
parameter 信号参数定义
wire 线束信号定义
reg 寄存器信号定义
always 产生寄存器信号的语句关键字
assign 产生线束信号语句的关键字
begin / end 语句的开始标志、结束标志
posedge / negedge 时序电路上升沿、下降沿
case / default / endcase case语句的其实标志、默认分支、结束标志
if / else 条件判断语句标志
for 循环语句标志

高级知识点

阻塞赋值和非阻塞赋值

1. 阻塞赋值

阻塞赋值是指在同一个always语句块中,前面的语句的执行会影响右面的语句。只有当前面的语句执行结束,后面的各条语句才能执行,相当于是逐条语句执行的,是一种顺序执行关系
符号=用于阻塞赋值,在同一个always块中,在begin和end之间的语句顺序执行。
我们进行一个测试用来验证阻塞赋值过程:

  1. always @(posedge sys_clk or negedge sys_rst_n) begin
  2. if(!sys_rst_n) begin
  3. a = 1;
  4. b = 2;
  5. c = 3;
  6. end
  7. else begin
  8. a = 0;
  9. b = a;
  10. c = b;
  11. end
  12. end

仿真结果如下图(图来自正点原子FPGA手册):
image.png
上图看出:

  • 在第0个位置,复位信号拉高
  • 在第2个位置,遇到第一次时钟信号上升沿,进入else语句段
  • 阻塞赋值,导致abc三个值都是0。

    2. 非阻塞赋值

    非阻塞赋值是指由时钟节拍决定的并行赋值过程,在同一个always语句块中,前面语句的执行不会影响后面的语句。在一个时钟的上升沿,同时执行所有语句右边的代码,并同时将赋值语句右边的表达式赋值给语句左边的变量。相当于多条语句同时执行,是一种并行执行关系
    符号<=用于非阻塞赋值,在同一个always块中,在begin和end之间的语句同时执行。

    1. always @(posedge sys_clk or negedge sys_rst_n) begin
    2. if(!sys_rst_n) begin
    3. a <= 1;
    4. b <= 2;
    5. c <= 3;
    6. end
    7. else begin
    8. a <= 0;
    9. b <= a;
    10. c <= b;
    11. end
    12. end

    仿真结果如下图(图来自正点原子FPGA手册):
    image.png
    上图看出:

  • 在第0个位置,复位信号拉高

  • 在第2个位置,遇到第1次时钟信号上升沿,第1次进入else语句段。由于是非阻塞赋值,赋值语句同时执行,导致a=0,b=a=1,c=b=2
  • 在第4个位置,遇到第2次时钟信号上升沿,第2次进入else语句段。由于是非阻塞赋值,赋值语句同时执行,导致a=0,b=a=0,c=b=1
  • 在第6个位置,遇到第3次时钟信号上升沿,第3次进入else语句段。由于是非阻塞赋值,赋值语句同时执行,导致a=0,b=a=0,c=b=0

在描述组合逻辑电路时,使用阻塞赋值。(如assign语句和组合逻辑时的always语句)
在描述时许逻辑电路时,使用非阻塞赋值。(如时序逻辑时的always语句)

assign和always语句

  1. assign语句和always语句都是用来产生组合逻辑或者时序逻辑的语句。
  2. assign语句只能用来产生组合逻辑,always在不带时钟时,功能与assign一样,都只能产生组合逻辑。
  3. 比较简单的组合逻辑常用assign,逻辑复杂的话常用always。
  4. always带时钟时,产生时许逻辑电路,reg类型被综合成真正的寄存器。

    latch锁存器注意事项

  5. 锁存器和存储器一样都是存储单元。锁存器是电平触发,寄存器是边沿触发。

  6. 锁存器在组合逻辑中产生,寄存器在时序逻辑中使用。
  7. 锁存器的坏处是会产生毛刺(glitch),毛刺对下级电路是很危险的,并且不容易被察觉,所以尽量避免使用锁存器。
  8. 在不带时钟的always语句中,如果if/else或case/default语句不完整(即缺少else或default分支),就会导致产生锁存器。因此,在不带时钟的always语句(即组合逻辑)中,必须补全else或default分支。
  9. 在带有时钟的always语句(即时序逻辑)中,语句分支不完整也不会产生锁存器,因为锁存器只会在组合逻辑中产生。

    状态机分类

    状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。
    状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂。
    在控制复杂流程时,状态机的优势就非常明显。
    根据状态机的输出是否与输入条件相关,可将状态机分为两大类:Moore状态机和Mealy状态机。
  • 摩尔(Moore)型状态机:组合逻辑的输出只取决于当前状态。

image.png

  • 米勒(Mealy)型状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。

image.png

三段式状态机

三段式状态机的基本格式是:

  • 第一个always语句实现同步状态跳转;
  • 第二个always语句采用组合逻辑判断状态转移条件;
  • 第三个always语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。 ```verilog 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

  1. <a name="OrGGl"></a>
  2. ### 模块化设计
  3. 模块化设计能够使一个大型设计的分工协作、仿真测试更加容易,代码维护或升级更加便利,当更改某个子模块时,不会影响其它模块的实现结果。 <br />。。。。
  4. <a name="IH7Qq"></a>
  5. ## 编程规范
  6. <a name="SCiPF"></a>
  7. ### 代码组织结构
  8. 正点原子的代码组织方式为:
  9. ```verilog
  10. 项目工程名
  11. ├── doc
  12. ├── par
  13. ├── rtl
  14. └── sim
  • doc目录:存放工程相关的文档,包括数据手册、设计方案等。
  • par目录:主要存放工程文件和使用到的IP核文件。
  • rtl目录:主要存放rtl代码,这是工程的核心,文件名与模块名一致,建议按照模块的层次分开存放。
  • sim目录:主要存放工程的仿真代码。

    头文件声明

    参照正点原子开发文档改写的头文件声明如下:
    1. //---------------------- Copyright (c) ----------------------
    2. // Copyright(C) ZHJ0125 2022
    3. // All rights reserved
    4. //-----------------------------------------------------------
    5. // File Name : touch_led.v
    6. // Created Date : 2022-04-15 17:54
    7. // Modified Date : 2022-04-15 17:54
    8. // Created By : ZHJ0125
    9. // Last Version : V0.1
    10. // Descriptions : 使用触摸按键控制LED。
    11. // 开发板上电后LED为熄灭状态,手指触摸后LED点亮;
    12. // 再次触摸,LED熄灭,以此循环。
    13. //-----------------------------------------------------------

    输入输出定义

    模块的输入输出定义需要注意以下几点:
  1. 每行只定义一个信号。
  2. 信号名要全部对齐。
  3. 同一组的信号放在一起。

    1. module led (
    2. input sys_clk, // 系统时钟
    3. input sys_rst_n, // 系统复位
    4. output reg [3:0] led // 4位LED灯
    5. );

    参数类型定义

  4. 将参数类型parameter声明,紧跟着module的输入输出定义之后。

  5. 参数类型parameter声明的常量全部使用大写。

    1. // parameter define
    2. parameter WIDTH = 25;
    3. parameter COUNT_MAX = 25_000_000;

    线束类型/寄存器类型定义

  6. 每行只定义一个信号。

  7. 在一个module中,wire和reg变量要集中放在一起。
  8. wire/reg变量声明要紧跟在parameter参数变量声明之后。
  9. 信号名需要对齐,reg和位宽需要空2格,位宽和信号名至少空4格。
  10. 位宽使用降序描述方式[15:0]。
  11. 时钟信号缩写clk,复位信号缩写rst,低电平有效加后缀n

    1. // reg define
    2. reg [WIDTH-1:0] counter;
    3. reg [1:0] led_ctrl_cnt;
    4. // wire define
    5. wire counter_en;

    信号命名规则

  12. 信号命名要体现实际意义。

  13. 可以使用_下划线隔开。
  14. 内部信号全部使用小写,不要大小写混用。
  15. 模块名字也要使用小写。
  16. 低电平有效信号使用_n后缀。
  17. 一部信号使用_a后缀。
  18. 纯延时节拍信号使用_dly后缀。

    always块描述规则

  19. if需要4个空格。

  20. 每个always都要有begin和end,并且always和begin放在同一行。
  21. 每个always前面都要有一行注释。
  22. 一个always块只包含一个时钟和复位。
  23. 时序逻辑使用非阻塞赋值<=

    assign块描述规则

  24. assign描述的逻辑不要太复杂,否则不宜阅读。

  25. 组合逻辑使用阻塞赋值=

    空格和TAB规则

    建议将Tab全部转换成空格,一个Tab转换成4个空格。

    注释规则

  26. 简洁清晰,不说废话

  27. 建议使用//

    模块例化规则

    模块例化使用u_xxxxx表示,即使用u_前缀。 ```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),

  1. .add_flag (add_flag ),
  2. .sel (sel ),
  3. .seg_led (seg_led )

); ```

其他注意事项

  1. 代码不要过分简介,要方便他人阅读理解。
  2. 不要使用repeat等循环语句。
  3. RTL级别里不使用initial语句,但在代码仿真的激励文件里可以使用。
  4. 避免产生锁存器Latch,在组合逻辑里要补全if/else、case/default分支。
  5. 避免使用罕见的语法,不利于语法综合时的优化。