Ch2 计算机组成与结构 计算机辅助设计与Verilog

CAD: It works!

Posted by R1NG on October 27, 2020 Viewed Times

Ch2 计算机辅助设计与 Verilog

在本章中, 我们将阐述计算机辅助设计的基本概念, 并介绍一门描述性计算机辅助设计语言: Verilog.

1. 计算机辅助设计

随着科技的不断发展和对性能的持续要求, 面向数字系统的设计不可避免地变得愈发复杂, 以至于我们无法再使用传统的纸笔绘图设计极其复杂的电路, 例如包含上亿晶体管的处理器. 为了避免这一局限性, 我们必须使用计算机辅助我们进行电路设计.

1.1 必要性和优越性

  1. 必要性: 设计复杂度的不断提升
  2. 优越性: 计算机在重复执行大量简易指令方面相比人工操作具有无可比拟的优越性

基于以上现实, 为了利用计算机的这一特征实现计算机辅助设计绕过人的局限性对数字系统发展的限制, 我们需要将数字电路设计划分为以下的数个步骤:

  1. 基本的电路设计
  2. 对电路设计的模拟
  3. 将逻辑化的电路设计进行转译和编译
  4. 将电路设计转化为实际的电路布局
  5. 对电路设计进行测试和分析


1.2 电路图和符号

通过使用电路图和符号, 我们可以将硬件设计图形化. 电路图可以很好地显示部件之间的连接, 适用于更为抽象化的高级电路设计. 同时, 电路图也可以用于表示位于较底层的门电路级别的线路连接.

抽象化的符号表示了它所包含的一切层级. 绝大多数情况下, 符号只是一些简单的图形. 符号所具有的重要功能在于, 它通常通过名称代号和所代表的基础设计相关联, 并且符号常常都具有和线路相连的针脚.


1.3 测试和模拟

对电路的测试常常由某个指定了测试顺序, 测试方法的文件所定义. 虽然可交互性测试也是理论上可行的,但在实际测试过程中, 随着不断地在设计中发现-测试-修复问题, 这种方法的耗时将会显著增加. 因此, 静态模拟仍然是必要的.


2. 硬件描述语言: Verilog

硬件描述语言旨在简化漫长而枯燥的硬件设计过程. 它具有以下的基本特点:

  1. HDL 允许使用文字性的语言对硬件进行定义
  2. HDL 不需要使用者了解过多的硬件细节
  3. HDL 便于参数化, 易于维护
  4. HDL 具有更强的可读性 在本课程中, 我们将简要学习一门被广泛应用的硬件描述语言: Verilog. 我们将使用 Verilog 进行不可合成的电路设计与模拟, 和描述可被合成入 FPGA 的硬件设计.

2.1 Verilog 模组

模组是描述数字系统的基本组成单位, 它和描述某个设计的电路图或符号等价:

20201105191758

在上图的设计中, 该设计具有输入和输出, 以及可以基于输入执行一些指令. 模组就是对这个设计的文字化的表述: 模组定义了它的输入和和输出, 以及它基于输入将要执行的指令内容. Verilog 的模组, 和面向对象程序设计原则中的 “类” 是非常相似的.
Verilog 中, 由于不同的语句可以实现同样有效的功能, 我们最好使用清晰的表示法来表示模组.
模块是一种逻辑功能,类似于上图,并且可以用相同的方式用符号表示。
因此,我们可以在原理图中使用 Verilog 模块,其中符号用于高级原理图中,而低级内容在Verilog 中进行描述。
模块以关键字 module开头;其后是模块的名称,该名称应反映其功能。声明输入和输出变量的方法如下:

  1. 模块以关键字 endmodule 结尾。
  2. 模块的主体由任何内部信号的声明和定义逻辑功能的语句组成。

为了清晰明确地表示模组, 我们常常遵循以下的规则:

  1. 确保模组的功能直接明了, 并且可读性强
  2. 使用不影响编译结果的缩进与空格增强代码的可读性.
  3. 使用合适的注释语句备注代码:
    1
    2
    3
    4
    5
    
     /* <comment goes here >*/
        
     or
    
     //<comments goes here>
    

对模组的定义一般如下:

1
2
3
4
5
module [module name] (i/o definations);

main body of the module, where variables are assigned with values

endmodule

注: 我们可以使用新的代码风格: 将变量的输入和输出直接定义在模组头而非模组体中. 两种定义方式同样有效.


2.2 Verilog 声明

和其他编程语言一样, 任何一个变量在被赋值前必须声明其类型.

Verilog 中, 变量类型有且只有以下两种:

  1. wire: 默认类型, 声明组件之间的连接
    • 用于声明连续赋值
    • 输出简单的组合逻辑函数
    • 用于更高级别的组件连接定义
  2. reg: 既可以用于声明组件间的连接线, 也可用于声明某个寄存器:
    • 用于声明阻塞赋值(=)
    • 用于复杂的组合逻辑中
    • 也可用于声明非阻塞赋值(<=)
    • 用于顺序(锁定)输出

注:

  1. Verilog 大小写敏感!
  2. Verilog 的默认数字类型为 十进制. 要表示二进制, 十六进制, 我们需要在数字前分别附上标记: ‘b’h.


2.3 Verilog 算符

和其他编程语言一样, Verilog 也具有多种算符:

  1. 算数算符:
    add '+', subtract '-', division '/', modulus '%'
  2. 二元逻辑算符:
    complement '~', AND '&', OR '|', XOR '^'
  3. 逻辑算符:
    complement '!', AND '&&', OR '||', NOT equal '!='
  4. 移位算符:
    shiftright ‘>>’, shiftleft '<<', concatenation {.}
  5. 关系算符:
    greater_than '>' less_than '<' equal '==' nequal '!=' greater_or_eq '>=' less_than_or_eq '<='


2.4 Verilog 控制语句

和其他编程语言一样, Verilog 也有条件控制语句, 控制代码执行流程. 和其他编程语言不同的是, 由于只有if...else可被简单地转化为硬件, 因而在 Verilog 中也只有这种控制语句最为有效. 通常我们也有限度地使用 for... 循环, 但主要以测试目的居多, 并非用来表示硬件设计.

一个典型的 if...else 示例如下:

1
2
3
4
if (sig_in == 00000000)     //Test Condition
    is_zero = 1;     //if true then...
else
    is_zero = 0;     //id false then...

注:

  1. if ... else 语句必须位于一个 Initial 块或 Always块中。
  2. 只有在少数情况下, 省略表达式中的 else 并不会导致意想不到的结果. 为确保万无一失, 在编写 if ... else 语句时, 最好包含并考虑所有的可能输入情况.


2.5 Verilog 组合逻辑赋值 (Assignments)

模组的功能是定义某个电路或设计的行为. 因此, 它必须根据输入信号的变化而分配输出信号的值, 或者能够决定输出的信号是什么, 这也正是 Verilog 要提供控制语句的原因所在. Verilog 提供了三种赋值方式:

  1. continuous assignment 连续赋值
  2. blocking assignment 阻塞赋值
  3. non-blocking assignment 非阻塞赋值

随着我们所定义的电路类型的不同和定义方式的不同, 我们使用的赋值方式也要有所变化.

Verilog 编程准则:

  1. 使用 连续赋值 或 阻塞赋值 为组合逻辑建模.
  2. 为顺序系统, 如 寄存器, 触发器等 建模时, 使用 非阻塞赋值.
  3. 若在同一个 always 块中同时包含了组合逻辑和顺序系统, 使用 非阻塞赋值.
  4. 切勿 将阻塞赋值和非阻塞赋值在同一个 always block 中混用!
  5. 切勿 在多个 always 块中对同一个变量赋值!


2.5.1 Continuous Assignment

连续赋值 Continuous Assignment 用于简洁地为组合逻辑建模. 关键词为 assign:

1
2
3
4
5
assign <variable> = <logical statement>;
assign s = a ^ b ^ c_in;
assign c_out = (a & b) | (a & c_in) | (b & c_in);

assign a = b && (c || d);   //a is given by c ORed d, result ANDed with b

在上述代码块的第 23 行中, 我们定义了一个全加器. 它不是从半加器开始组合成全加器的, 而是使用连续赋值直接定义的, 由此可见连续赋值可以有效地简化一些组合逻辑的定义.

在连续赋值语句中, 我们可以用轮询算符 ? 表示选择声明:

1
2
3
4
5
6
assign q = (sel == 0)? x:y;

it means: 

if (sel == 0)   assign q = x
else            assign q = y

这是一个 2:1 多路复用器. 注意: x, y 既可以是连线, 也可以是总线. (但是都需要声明为 wire 类型).

注:

  1. 连续赋值只能赋值 wire 数据类型. 故在上述例子中, q 必须被声明为 wire, 如此 assign 才是合法的.
  2. 连续赋值在模块体中被执行. 连续赋值不能在 alwaysinitial 体中被执行!

2.5.2 Blocking Assignment

阻塞赋值 Blocking Assignment 为一类按照顺序自顶向下执行的语句:

1
2
3
4
5
6
a = (c || d);   //1st do c+d
e = a && b;     //then AND the result with b

// can be used with if...else statements:
if (sel == 0) q = x;    
else          q = y;

阻塞赋值的应用范围在于:

  1. 在测试文件中, 为变量赋值. (显然需要按照顺序执行测试)
  2. 描述复杂的组合逻辑

注:

  1. 阻塞赋值所赋值的变量 必须声明为 reg 类型, 无论该变量是否有保存状态的功能!
  2. 阻塞赋值语句 只能被用于 initialalways 块中!

阻塞赋值允许一个变量在同一个代码块中被多次赋值. 需要注意的是, 所有对同一个变量进行赋值的语句必须被存放在同一个 “代码区块” 中, 当一些先决条件得到满足的时候, 这个区块就会被执行.

在两种情况下, 这样的情形才会发生:

  1. initial
    • 代码区块仅仅在初始, 时间0时被执行一次
    • 通常用于初始化
    • 一般无法被合成为硬件
  2. always @ (<signal list>)
    • 每次 <signal list> 中的信号发生变化时代码块都会被执行
    • 一般而言所有输入信号都应被纳入<signal list> 中以确保这个代码块是组合的
    • 不在 <signal list> 中的信号如果发生变化不会触发代码块的执行

例: Verilog 条件语句 case

1
2
3
4
5
6
7
8
9
10
// case statement is similar to Java's switch
//sel is a 2-bit bus
always @ (sel, w, x, y, z)
    case (sel)
        0: q = w;
        1: q = x;
        2: q = y:
        3: q = z;
        default: 'bX;   //Send Null signal for alert
    endcase

Verilog 中, case 条件体有以下特征:

  1. 关键词为 case
  2. 每一个条件都以 数值 的方式定义
  3. 若需要实现更多功能, 可使用 beginend 括号.
  4. case 声明 不可用于连续赋值!
  5. case 声明在我们需要阻塞赋值构建电路时可能是最有效的声明.
  6. 为了避免合成器添加多余的锁存器, 我们在使用case 时可以定义 default.


2.5.4 Always Block

我们使用 always 块来描述当一些事件 (一般是某些变量的变化或时钟电平的变化) 发生时将要执行的语句. 其用法如下:

1
2
3
4
always @ (<condition>)
begin
    //statements to be executed
end

注:

  1. always 关键字的行为正如其所述, 它将从程序运行的一开始就总是生效并基于条件决定是否执行这些语句, 可将其视为一个无限循环, 这一特性和 initial 很相近, 二者都是从程序运行一开始就执行. 相较于连续赋值, 它为设计人员提供了更高的抽象程度, 便于实现更复杂的逻辑表达式.
  2. 在块中的语句的执行与否取决于 <condition>. 这将放在后文详细讨论. 一般 always 块都会提供一个敏感度列表 (sensitivity list), 用于决定什么时候这个always 块会被评估.
  3. always 块中, 我们可以进行 阻塞赋值和非阻塞赋值, 而不能执行连续赋值!
  4. 一个模组可以有任意多的 always 块, 以及 initial 块. 所有这两种块都会同时开始执行: 电流从物理上就是并发的. 事件不会像软件代码一样串行发生, 而是并行发生.
  5. 程序化的声明可以通过 阻塞赋值和非阻塞赋值实现.


2.5.5 敏感度列表

位于 always 块头括号内的条件被统称为 敏感度列表 (sensitivity list):

1
2
3
4
always @ (sel, w, x, y, z)  //sensitivity list
begin
    //operations, do sth
end

注:

  1. 块体内的陈述将会在任何敏感度列表中的变量发生改变的时候执行.
  2. 这样的一个 always 块通常会被合成入一个组合逻辑块中.
  3. 我们可以使用 always @ (*) 将全部输入变量一次性划入敏感度列表中.

如前文所述, 我们也可以令 always 块对时钟信号敏感. 我们通过在敏感度列表中使用关键词实现这一点:

  • posedge 信号0->1时执行
  • nededge 信号1->0时执行

例:

1
2
3
4
5
6
//execute the always block when
//input signal clock has a rising edge
always @ (posedge clock)
begin
    //operations, do sth
end

2.5.6 Non-Blocking Assignments

非阻塞赋值 Non-Blocking Assignments并行 的, 这意味着它们将会被同时执行! 我们使用 <= 符号指代非阻塞赋值:

1
2
3
a <= 1;
b <= a;
c <= b;

在上述的示例代码中, 当该代码块执行后:

  1. a = 1
  2. b将被赋上 a原来的值!
  3. c将被赋上 b原来的值!

注意: 非阻塞赋值在每一次执行块时都会分配一次变量. 以下列代码为例:

1
2
3
4
5
// count's init value == 8, final value == 9
// count's init value == 9, final value == 10
// count's init value == 10, final value CANNOT BE DEFINED
count <= count + 1;
if (count == 10) count <= 0;

因此, 任何非阻塞赋值都应该是互斥的. 下列代码就没有问题:

1
2
if (count == 9) count <= 0;
else            count <= count + 1;

[TODO: 笔记P81, 日后再看]


2.7 Verilog 设计分区

单个模块 module 中可以有多个 assignalways 块. 我们可以用这些块对我们的设计进行合理的分区:

  1. 确保每个块是简单的
  2. 只将一个函数封装在一个区域中 并且注意:
  3. 在同一个块中只赋值一个变量
  4. 切勿在 always 块中错误地混用阻塞赋值和非阻塞赋值.
  5. 确保代码可读性
  6. 合理注释代码


2.8 Verilog 并行

由于阻塞赋值和非阻塞赋值在组合意义上和时序意义上的不同, 用错误的方式应用它们会导致意料之外的结果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(1)
module demo (input clk, a, output reg c);
reg b;
always @ (posedge clk) 
    b <= a;
always @ (posedge clk) 
    c <= b;
endmodule

(2)   
module demo (input clk, a, output reg c);
reg b;
always @ (posedge clk)
    begin
    b <= a; 
    c <= b; 
    end
endmodule

(3)   
module demo (input clk, a, output reg c);
reg b;
always @ (posedge clk)
    begin
    c <= b; 
    b <= a; 
    end
endmodule

可见, 以上四段代码都应用了非阻塞赋值, 而实际上它们的行为也是完全相同的. 然而, 若我们换用阻塞赋值, 由于此时必须考虑代码执行顺序的问题, 不同的代码段行为将会发生变化.
对于时序相关的电路, 最好不要使用阻塞赋值.


2.9 Verilog 合成

需要注意, 不是所有的 Verilog 代码都可以被正确地合成为硬件电路设计. 实际上, 只有一部分的电路描述语句可以被很好地合成为硬件:
OK:

  • always, assignments, most operators, if...else statement

NO:

  • initial, #nn, for, while, $display, some operators(*,/)