Skip to content

硬件描述语言

Vivado 相关

为 IP 核创建自定义参数

可以通过模块名后的 #(parameter ) 实现:

module myHeartbeat #(parameter nbits = 25)(
    input clk,
    output reg heartbeat = 0
    );

    reg [nbits-1:0]divider = 0;
    always @(posedge clk) begin
        if (divider == 0) begin
            heartbeat <= !heartbeat;
        end
        divider <= divider+1;
    end    
endmodule

Verilog

推荐资源

  • 书籍:
    • Verilog by Example: A Concise Introduction for FPGA Design
    • Verilog 数字系统设计
  • 练习:
    • HDL Bits

运算符

  • 按位运算符 6 个:~ | ^ & ~^ ~&
    • ~^(或者 ^~ 都可以)是异或非,即 XNOR,也称为同或
    • 长度不一时,右端对齐,补零
    • 都可以归约,接受一个向量操作数
  • 逻辑运算符 && || !
    • 例子:如果 a b 各是两个向量,要把它们的每一位或起来,用 a || b 即可。
  • 赋值运算符 = <=
  • 拼接 {}
    • 需要知道每一个数的大小,{1,2,3} 是没有大小的常数
    • 赋值的左右均可
  • 重复:{num{a,b,c}}
    • 注意,两层大括号包裹的整体代表重复,嵌套时不要忽略外面的大括号,写法如 {3'd5, {2{3'd6}}}
  • 位移:<< >>
  • 算术位移:<<< >>>
  • 条件:condition ? value_if_true : value_if_false

数据类型

  • wire
    • 也被称为 signal,
    • 有方向:source(driver) → sink
    • assign 连续赋值
    • 不能有超过一个 driver,无 driver 值未定义
  • 向量
    • 声明时维度在名字前面,选择时维度在名字后面
    • 声明在名字后面时也称为 unpacked array,一般用于内存数组
    • 赋值大小不一致时,零扩展或截断

隐式网表

assign 和模块端口会隐式生成未定义的网表,它们都是 1 位的

添加指令 `default_nettype none 可以禁用隐式生成,防止你漏掉 wire

状态机

例子
module state_machine (
    input clk,
    input reset,
    input go,
    input kill,
    output done
);

reg [6:0] count;
reg       done;
reg [1:0] state_reg;

// states
parameter idle   = 2'b00;
parameter active = 2'b01;
parameter finish = 2'b10;
parameter abort  = 2'b11;

// machine
always @ ( posedge clk or posedge reset )
begin
    if ( reset )
    begin
        state_reg <= idle;
        count <= 7'h00;
        done <= 1'b0;
    end
    else
    case ( state_reg )
    idle:
        begin
        count <= 7'h00;
        done <= 1'b0;
        if ( go )
            state_reg <= active;
        end
    active:
        begin
        count <= count + 1;
        done <= 1'b0;
        if ( kill )
            state_reg <= abort;
        else if ( count == 7'd100 )
            state_reg <= finish;
        end
    //...
    default:
        begin
        count <= 7'h00;
        done <= 1'b0;
        state_reg <= idle;
        end
    endcase
end
endmodule

一个状态机的大致写法如下:

  • 状态寄存器、计数器等的寄存器声明
  • 状态码分配 parameter
    • 二进制码、格雷码(功耗较低)、独热码(更节省组合逻辑,增加速度和可靠性)
  • case 状态机
    • 每个状态自己做的事情
    • 每个状态到其他状态的转移

这是用一个 always 块描述整个状态机的方法。课上讲解了三段式状态机:

  • 双寄存器,当前状态和下个状态
  • 第一段:always 块,负责移动到下个状态
    • 非阻塞赋值
  • 第二段:always 块,负责根据当前状态进行操作,并决定下个状态
    • 阻塞赋值
  • 第三段:assign 赋值语句,定义输出

三段式状态机其实就是把状态迁移的时序独立出来了。它还可以在组合逻辑后再加一级寄存器,滤去组合逻辑的毛刺。


下面的部分不完善

算术运算

或许你会因为 Verilog 中数据运算究竟是考虑无符号数还是补码而感到困惑。这里是一些值得注意的地方:

  • 首先,和 C 语言一样,数据具体的位模式是不重要的,重要的是解读这个位模式的方式。
  • 从 Verilog-1995 以来,integer 类型时有符号的,regnet 都是无符号的。
  • Verilog 的加法和乘法操作会先对操作数扩展成相同的位宽
  • I/O、总线。
  • reg 型需要结构化过程语句如 always 进行。
  • 所有可综合的寄存器都应当使用非阻塞赋值 <=

下面是一个带复位的 D-flop:

always @(posedge clk or posedge rst) 
    begin
        if (rst) 
            q <= 1'b0;
        else 
            q <= d;
    end

FPGA 开发中好的习惯

  • 总是使用一个全局的异步复位信号。
  • 使用 parameter 为状态命名。
  • 使用 case 语句,必须要有 default 分支(综合程序的要求)。
  • 模块化设计:IP 核、原语核。

模块实例化时允许空置的输出

总是使用指定名称的端口连接

  • 内存: 控制器,FPGA 不能实现。
    • FPGA 可实现的有:SRAM、FIFO、LIFO、DP 等。
    • 可以由综合程序推断、厂商原语、厂商的专用工具生成。
    • 深度和宽度。
    • 通常利用片内存储 RAM 块,我们需要写控制逻辑。
    • reg [7:0] mem [0:255]; 表示 256 个 8 位的寄存器。
    • 简单双口内存、全双口内存、单口内存的实现。
    • 读写时序。当读写地址相同时,读写操作哪个需要设计。
    • 三态门实现双向总线。
  • 测试模块:
    • initialinteger#wait() 一般只用于测试模块。
module tb_sim_sample_1();
    integer i;
    parameter CLK_PERIOD = 10;

    initial sim_clk= 1'b0;

    always #(CLK_PERIOD/2) 
        sim_clk = ~sim_clk;

    initial
    begin
        random_num = $random(1)
        wait(reset);
        wait(~reset);
        @(posedge sim_clk);
        for(j = 0; j < 20; j = j + 1)
        begin
            @(posedge sim_clk);
        end
        forever
        begin
            @(posedge sim_clk);
            enable = 1'b0;
        end
    end
endmodule
  • always 块实现组合逻辑,在其中使用 if-elsecase 语句。此时使用 = 阻塞赋值,因为组合逻辑不关心执行顺序。