学习路线
- VHDL和Verilog都可以作为入门学习
- 刷题的网站:HDLbits。
- RTL设计主要有组合电路和时序电路两种,还有状态机。关于RTL的验证,学习者主要掌握如何写一个简单的测试平台以及如何写一些简单的测试用例。RTL学习容易犯的错误是用软件编程的思想去学习硬件语言,软件编程大多数是顺序执行的,硬件编程大多数是并行执行的。对于Verilog掌握比较熟练的朋友可以转向system Verilog语言的学习,现在业界的大公司多选用system Verilog作为开发语言,可以将system Verilog看作是c++,Verilog看作是C语言。
- 仿真工具:常用的mentor的modelsim以及Synposys的VCS
- 了解FPGA的开发流程:RTL设计 仿真验证 逻辑综合等我们需要知道为什么需要这些步骤,以及这些步骤完成什么样的功能。
- FPGA主流的开发工具:AMD的vivado还要英特尔的Quartus。熟悉FPGA的开发流程,怎么创建工程,怎么添加源文件。熟悉常用的IP的配置和使用:PLL FIFO 存储器等,这些IP可以提高我们的效率,避免重复造轮子,怎么配置和调用这些常用的IP。还有一些复杂的IP,比如NIOS,以太网控制器等,这些复杂的IP初级阶段可能用不到,可以进阶的时候再学习。熟悉一些硬件调试和时序分析的方法。
Verilog
每个语句必须以分号为结束符。
用 // 进行单行注释,用 /* 与 ***/** 进行跨行注释
Verilog 中关键字全部为小写
标识符(identifier)可以是任意一组字母、数字、**$** 符号和 _(下划线)符号的合,但标识符的第一个字符必须是字母或者下划线,不能以数字或者美元符开始。
四种基本的值来表示硬件电路中的电平逻辑:0:逻辑 0 或 “假” 1:逻辑 1 或 “真” x 或 X:未知 z 或 Z:高阻 z 意味着信号处于高阻状态,常见于信号(input, reg)没有驱动时的逻辑结果。例如一个 pad 的 input 呈现高阻状态时,其逻辑值和上下拉的状态有关系。上拉则逻辑值为 1,下拉则为 0 。
counter = 'd100 ; //一般会根据编译器自动分频位宽,常见的为32bit counter = 100 ; counter = 32'h64 ;
Verilog 最常用的 2 种数据类型就是线网(wire)与寄存器(reg)
当位宽大于 1 时,wire 或 reg 即可声明为向量的形式。
reg [3:0] counter ; //声明4bit位宽的寄存器counter
wire [32-1:0] gpio_data; //声明32bit位宽的线型变量gpio_data
wire [8:2] addr ; //声明7bit位宽的线型变量addr,位宽范围为8:2
reg [0:31] data ; //声明32bit位宽的寄存器变量data, 最高有效位为0
- 算术左移和逻辑左移时,右边低位会补 0;逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位。
- 连续赋值语句是 Verilog 数据流建模的基本语句,用于对 wire 型变量进行赋值。
assign LHS_target = RHS_expression ;
//LHS_target 必须是一个标量或者线型向量,而不能是寄存器类型。RHS_expression 的类型没有要求,可以是标量或线型或存器向量,也可以是函数调用。只要 RHS_expression 表达式的操作数有事件发生(值的变化)时,RHS_expression 就会立刻重新计算,同时赋值给 LHS_target。
//assign 为关键词,任何已经声明 wire 变量的连续赋值语句都是以 assign 开头
wire Cout, A, B ;
assign Cout = A & B ; //实现计算A与B的功能
//在 wire 型变量声明的时候同时对其赋值。wire 型变量只能被赋值一次,因此该种连续赋值方式也只能有一次。
wire A, B ;
wire Cout = A & B ;
过程结构语句有 2 种,initial 与 always 语句。它们是行为级建模的 2 种基本语句。
一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用。
这些语句在模块间并行执行,与其在模块的前后顺序没有关系。
但是 initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。
每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始。
- initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用。initial 理论上来讲是不可综合的,多用于初始化、信号检测等。
- always 语句是重复执行的。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。由于循环执行的特点,always 语句多用于仿真时钟的产生,信号行为的检测等。
过程性赋值是在 initial 或 always 语句块里的赋值,赋值对象是寄存器、整数、实数等类型,这些变量在被赋值后,其值将保持不变,直到重新被赋予新值。连续性赋值总是处于激活状态,任何操作数的改变都会影响表达式的结果;过程赋值只有在语句执行的时候,才会起作用。这是连续性赋值与过程性赋值的区别。Verilog 过程赋值包括 2 种语句:阻塞赋值与非阻塞赋值。
- 阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。阻塞赋值语句使用等号 = 作为赋值符。
- 非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。非阻塞赋值语句使用小于等于号 <= 作为赋值符。
2 大类时序控制方法:时延控制和事件控制。事件控制主要分为边沿触发事件控制与电平敏感事件控制。根据在表达式中的位置差异,时延控制又可以分为常规时延与内嵌时延。
一般时延赋值方式:遇到延迟语句后先延迟一定的时间,然后将当前操作数赋值给目标信号,并没有”惯性延迟”的特点,不会漏掉相对较窄的脉冲。内嵌时延赋值方式:遇到延迟语句后,先计算出表达式右端的结果,然后再延迟一定的时间,赋值给目标信号。
reg value_test ; reg value_general ; //一般时延法1 #10 value_general = value_test ; //一般时延法2#10 ; value_ single = value_test ; //内嵌时延 value_embed = #10 value_test ;
事件是指某一个 reg 或 wire 型变量发生了值的变化。基于事件触发的时序控制又主要分为以下几种。
- 一般事件控制:事件控制用符号 @ 表示。语句执行的条件是信号的值发生特定的变化。关键字 posedge 指信号发生边沿正向跳变,negedge 指信号发生负向边沿跳变,未指明跳变方向时,则 2 种情况的边沿变化都会触发相关事件。
- 命名事件控制:用户可以声明 event(事件)类型的变量,并触发该变量来识别该事件是否发生。命名事件用关键字 event 来声明,触发信号用 -> 表示。
- 敏感列表:当多个信号或事件中任意一个发生变化都能够触发语句的执行时,Verilog 中使用”或”表达式来描述这种情况,用关键字 or 连接多个事件或信号。这些事件或信号组成的列表称为”敏感列表”。当然,or 也可以用逗号 , 来代替。
- 电平敏感实践控制:前面所讨论的事件控制都是需要等待信号值的变化或事件的触发,使用 @+敏感列表 的方式来表示的。Verilog 中还支持使用电平作为敏感信号来控制时序,即后面语句的执行需要等待某个条件为真。Verilog 中使用关键字 wait 来表示这种电平敏感情况。
//一般事件控制 always @(clk) q <= d ; //信号clk只要发生变化,就执行q<=d,双边沿D触发器 always @(posedge clk) q <= d ; //在信号clk上升沿时刻,执行q<=d,正边沿D触发器模 always @(negedge clk) q <= d ; //在信号clk上升沿时刻,执行q<=d,正边沿D触发器 q = @(posedge clk) d ;//立刻计算d的值,并在clk上升沿时刻赋值给q,不推荐这种写法 //命名事件控制 event start_receiving ; always @( posedge clk_samp) begin //采样时钟上升沿作为时间触发时刻 -> start_receiving ; end always @(start_receiving) begin //触发时刻,对多维数据整合 data_buf = {data_if[0], data_if[1]} ; end //敏感列表 always @(posedge clk or negedge rstn) begin //带有低有效复位端的D触发器模型 //always @(posedge clk , negedge rstn) begin //也可以使用逗号陈列多个事件触发 if(! rstn)begin q <= 1'b ; end else begin q <= d ; end end