Fpga基础入门教程 004 第三章 verilog快速入门

title: “ [FPGA] FPGA基础入门教程 - 004 - 第三章 Veilog快速入门” date: 2019-05-15 11:00:00 categories:

  • FPGA tags:
  • FPGA
  • 教程
  • Verilog keywords: FPGA, 入门, 教程, Verilog —

第三章 Verilog快速入门

概览

现在信息爆炸的时代,关于verilog的语法书籍、教程多如牛毛,但是作为FPGA的初学者,在这些让人眼花缭乱的教程中简直无所适从,根本不知道该看哪个好。就本人这些年来看的一些verilog语法书来看,其实只需要选择一个最基础的书籍即可,这本书可以是中文,也可以是因为,但是语言最好要通俗易懂,内容不能太过冗长。再配一个IEEE的verilog标准,当做工具书,这样就足以了。 我在这里不打算将verilog从头开始讲,而是打算对一个通用的设计,在其中用到的最多的语法列出来,讲一讲模块间的架构,让读者能够很快的对FPGA的代码设计有一个宏观的映像,这个时间一两天足以。至于具体的语法,可以查看我前面提到的两类教材。

常用设计结构

Module

verilog的文件后缀是.v,在一个通用设计中,所有的代码都是放在模块中的,一个模块一个.v文件,特殊情况或者写法这里不予讨论。模块的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module module_name(
//port list  
);

//code

sub_module1 sub_module1_inst_name(
    //port connect  
);
sub_module2 sub_module2_inst_name(
    //port connect  
);

endmodule

子模块声明&例化

在模块中,可以根据需要,如功能划分等,将一部分代码放入子模块中,子模块还可以包含自己的子模块。通常设计中,子模块的嵌套层数不应该过多,一个模块中的代码量也不宜过多。在上面的代码中,应该有三个.v文件。

模块端口/输入输出

在上述代码中标有//port list的地方,会添加模块的输入输出端口。所谓输入输出,其实就是一个模块对内或者对外的接口,其有三个方向:input,output,inout,分别是模块本身的输入、输出以及双向。所谓双向,就是说这个端口可以根据需要,既可以接收外部输入,也可以往外部输出信号,这两个状态需要二选一。输入输出的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
module module_name(
    input   wire        input_1,
    input   wire        input_2,
    output  reg         output_1,
    output  wire        output_2,
    output  reg   [1:0] output_3,
    inout   wire  [2:0] inout_1  
);

//...


endmodule

输入输出有4个部分,包括方向:input/output/inout,类型:wire/reg,位宽 none/n:0,以及输出输入名称。其中类型wire代表这个输入输出的是一个线连接过来,而reg代表它是一个寄存器连接过来。位宽部分不写,代表位宽是1,一个n位宽的寄存器,其定义应该为[n-1:0]。在每个输入输出用,隔开,最后一个端口不包含,

声明

在使用一个变量前,我们都需要先声明其类型、位宽。其基本语法为:

1
2
wire [1:0] wire_1;
reg        reg_1 ;

就像前面所述,wire为线网类型,可以作为一个线连接两个接口,也可以连接组合逻辑门(与或非门等等)。reg类型的变量是连接到寄存器的输出。

组合逻辑和时序逻辑

组合逻辑就是不依靠时钟来进行采样的逻辑,属于电平敏感逻辑。记住一点,组合逻辑的赋值用=,这叫阻塞赋值,时序逻辑属于利用时钟进行采样的寄存器逻辑,用<=进行赋值。

组合逻辑赋值

组合逻辑使用assign(连续赋值)进行赋值。 例如,下面是一个二输入加法器的例子:

1
2
3
4
5
wire [3:0] a;
wire [3:0] b;
wire [4:0] sum;

assign sum = a+b;

也可以直接给一个信号赋值固定值:

1
2
3
4
5
6
7
8
9
wire [1:0] a1;
wire [1:0] a2;
wire       b1;  //0或者1
wire [2:0] b2;  //最大可赋值3'b111 = 3'd7 = 3'h7 = 7
assign a1 = 2'b01;  //位宽'进制 + 值  二进制b
assign a2 = 2'h3;   //十六进制h
assign b1 = 1'b0;   //二进制b
assign b2 = 5   ;   //十进制5 = 3'd5

组合逻辑可以用always @(敏感信号表)或者always @(*),推荐使用后者

1
2
3
4
5
6
7
8
9
10
11
12
reg   [3:0]   comb_1; //always赋值必须为reg类型,这里不会实现成寄存器
wire  [3:0]   wire_1;
wire  [2:0]   wire_2;
always @(*) begin  
    comb_1 = wire_1 | wire_2; //wire_1和wire_2的每一位与之后产生的信号赋值给comb_1
end

//上述等价于
wire  [3:0]   comb_1;
...
assign comb_1 = wire_1 | wire_2;

时序逻辑

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
29
30
31
32
33
34
35
reg   [2:0]     reg_1;
reg             reg_2;
reg   [10:0]    reg_3;
wire            clk  ;
wire            reset;

//带异步复位端寄存器 : 时钟上升沿采样,复位高有效
always @(posedge clk or posedge reset) begin  
    if(reset)
        reg_1 <= 0;
    else if(condition_1)
        reg_1 <= wire_1;  //wire_1需要提前声明
    else  
        reg_1 <= reg_1 + 1'b1;
end

//同步复位端
always @(posedge clk) begin
    if(reset)
        reg_2 <= 0;
    else if(condition_2)
        reg_2 <= reg_1[2];  //reg1[2]代表寄存器1的第二位(最高位)
    else  
        reg_1 <= reg_1 + 1'b1;
end

//下降沿采样,低电平有效的异步复位的寄存器
always @(negedge clk or negedge reset) begin  
    if(!reset)
        reg_3 <= 11d0;
    else if(condition_3 == 0) //条件3不满足  
        reg_3 <= wire_2;
    else  
        reg_3 <= reg_3;   //保持现有的值不变化
end

在上述例子中,寄存器的采样边沿和复位电平的有效电平可以自由组合,但是一般推荐是上升沿。对于xilinx来说,推荐高电平复位,对于altera来说,推荐低电平复位。到底是同步复位,还是异步(同步复位需要保证时钟能采到复位信号才能复位,异步复位则是复位信号的优先级比时钟采样高),则根据实际情况来选择。前提是能用同步复位的地方就用同步复位,如果同步复位不能保证复位逻辑和期望的一样,则使用异步复位。

操作符

本章节可以参考语法书

if…else if…else语句

if语句需要再always块中,无论是always @(*)或者always @(posedge clk...) ,条件语句只需要记住基本语法即可 :

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
//example
//当有多个语句在同一个时隙执行,则需要加begin...end,好的方法是无论一个还是多个都加,这样方便扩展也不会遗漏
always @(negedge clk) begin
    if(a == 3) begin    //这里的begin...end其实可以省略,因为if里面只执行一条语句 : b = 4
        b <= 4;
    end
    else if((a >= 4) && (b <= 10)) begin    //这里的<=是比较操作符
        b <= b + 1;                         //这里的<=是赋值操作

        //下面是if的嵌套
        if(b == 8) begin
            a <= 5;
        end
        else
            a <= a;
        //if嵌套完毕
    end
    //多个else if
    else if(cond2) begin  
    //    ...
    end
    else begin      //如果else里面是保持逻辑,可以不写else,因为本身不写默认就会保持,但是建议书写上
        a <= a;
        b <= b;
    end
end

if是可以控制优先级语句。

case…endcase语句

当要并行判断某些逻辑时,使用case语句,case依旧需要放置于always块内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
wire [2:0] a;

//判断a的值
case(a):
    //a = 001
    3'b001 : begin ... end
    3'b010 : begin  
        sum = a + b;
        if((sum & a) == 3'b101) begin  
            //...
        end
        //other code
    end
    //上述没有描述的剩余的值就会执行default语句,比如a = 000或者a = 111的时候
    default:begin  
        //some code
    end
endcase

  • if中可以有case,case中也可以有if
  • 通常认为if有优先级,case没有优先级,你也可以这么认为,但是并不绝对,有兴趣可以深入研究一下

子模块

就前面所述,模块中可以有很多子模块,下面就看下子模块怎么使用

子模块的例化

在一个module内的位置可以添加子模块,子模块添加格式如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
module top_level(
    //port list
    input   wire        clk,
    output  wire        d

);   //模块其实可以没有port list

//模块连接信号声明
wire    [9:0]   wire_m1_m2_01;
wire            wire_m1_m2_02;
wire            wire_m1_m2_03;
wire            wire_m2_m1_01;


assign wire_m1_m2_01 = 10;
assign wire_m1_m2_02 = 1'b1;

//例化两个模块并将这两个模块连接起来
//模块addr_gen里面有3个输入clock a b,一个输出c
addr_gen 任意名称(      //当然是非中文
    .clock  (clk            ),  //clock为模块内部输入管脚名称 clk为外部连接的线的名称,这里clk表示和顶层的输入clk相连
    .a      (wire_m1_m2_01  ),
    .b      (wire_m1_m2_02  ),
    // .内部接口名 (外部连接的net名),
    .c      (wire_m1_m2_03  )   //最后一个依旧不加逗号结尾,和端口列表一样
);

data_out inst_name_任意名称(
    .clock_in   (clk            ),//和上一个模块连接同一个clk
    .in         (wire_m1_m2_03  ),//将模块1的输出c通过wire_m1_m2_03连接到本模块的输入"in"上面
    .out_1      (wire_m2_m1_01  ) //输出连接到wire上

);

//将data_out的输出out_1通过线wire_m2_m1_01连接到顶层输出d
assign d = wire_m2_m1_01    ;   

endmodule



实例

在下一章节中,我们将使用一个简单的例子来看看怎么使用Verilog代码进行设计的,它的一个完整的结构、设计思路等都将在其中进行详细说明。