4. Verilog基础语法(下):组合逻辑设计、时序逻辑设计、状态机基础、仿真与Testbench编写

好,咱们接着往下聊。上一章我们把Verilog的基本语法框架搭起来了,这一章要动真格的了——组合逻辑、时序逻辑、状态机,还有仿真验证。这些都是FPGA设计的核心基本功,说白了,你以后写的每一行代码,都跑不出这几个范畴。

我个人习惯把这一章叫做“从代码到硬件”的桥梁。你写出来的Verilog,最终要变成真实的逻辑门和触发器。如果脑子里没有硬件概念,写出来的代码大概率是跑不起来的。

4.1 组合逻辑设计

组合逻辑,就是输出只取决于当前输入,跟历史状态没关系。没有记忆,没有时钟,纯纯的“即时反应”。

我在项目中遇到过最典型的组合逻辑就是地址译码器。比如你要从总线上选通某个外设,就得根据地址信号来产生片选信号。

4.1.1 用assign实现组合逻辑

最简单的写法,就是用assign连续赋值。比如一个2选1多路选择器:

module mux2to1(
    input       a,
    input       b,
    input       sel,
    output      y
);
    assign y = sel ? b : a;
endmodule

嗯,这里要注意:assign左边的变量必须是wire类型。如果你用reg,编译器会报错。我刚开始学的时候就被这个坑过,折腾了半天才发现是类型写错了。

4.1.2 用always块实现组合逻辑

有时候组合逻辑比较复杂,用assign写起来很费劲。这时候可以用always @(*)块,里面的变量用reg类型。注意,这里的reg只是语法要求,并不代表综合成寄存器。

module decoder_3to8(
    input  [2:0] in,
    output reg [7:0] out
);
    always @(*) begin
        case(in)
            3'b000: out = 8'b0000_0001;
            3'b001: out = 8'b0000_0010;
            3'b010: out = 8'b0000_0100;
            3'b011: out = 8'b0000_1000;
            3'b100: out = 8'b0001_0000;
            3'b101: out = 8'b0010_0000;
            3'b110: out = 8'b0100_0000;
            3'b111: out = 8'b1000_0000;
            default: out = 8'b0000_0000;
        endcase
    end
endmodule
警告:组合逻辑的always块中,敏感列表必须写@(*)或者把所有输入信号都列全。漏掉一个,仿真结果可能对,但综合出来的硬件会多出意想不到的锁存器。我曾经因为这个原因,让一块板子在调试时怎么都不对,查了两天才发现是敏感列表少写了一个信号。

4.2 时序逻辑设计

时序逻辑就不一样了,它要“记住”东西。靠的是时钟沿和触发器。你想想看,如果FPGA里全是组合逻辑,那数据怎么保持?怎么实现状态跳转?

4.2.1 同步复位与异步复位

写时序逻辑,最常用的就是always @(posedge clk)。复位方式有两种:

// 同步复位
always @(posedge clk) begin
    if(rst)
        q <= 1'b0;
    else
        q <= d;
end

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

我个人习惯用异步复位、同步释放的方式。为什么呢?因为异步复位能保证上电瞬间寄存器处于已知状态,而同步释放又能避免复位撤除时的亚稳态问题。这个技巧在高速设计中特别实用。

4.2.2 阻塞赋值与非阻塞赋值

这是新手最容易搞混的地方。我直接说结论:

  • 组合逻辑用阻塞赋值(=)
  • 时序逻辑用非阻塞赋值(<=)

为什么会这样?因为非阻塞赋值在时钟沿到来时,会先把右边的表达式都计算好,然后统一赋值。这模拟了真实触发器的行为——所有寄存器同时更新。而阻塞赋值是顺序执行的,用在时序逻辑里会综合出意想不到的硬件。

避坑指南:我曾经在一个项目中,把移位寄存器的代码写成了阻塞赋值。仿真看起来没问题,但下载到板子上,数据就是移不对。后来用示波器一看,发现多个寄存器在同一个时钟沿上互相覆盖了。从那以后,我写时序逻辑时都会默念一遍“非阻塞、非阻塞、非阻塞”。

4.3 状态机基础

状态机是FPGA设计的灵魂。你想想看,任何复杂的控制逻辑,本质上都可以抽象成状态机。激光运动控制更是如此——什么时候加速、什么时候匀速、什么时候减速,都是状态在驱动。

4.3.1 三段式状态机

我强烈推荐使用三段式写法。为什么?因为代码清晰,综合出来的硬件也规整。

// 第一段:状态跳转
always @(posedge clk or posedge rst) begin
    if(rst)
        state <= IDLE;
    else
        state <= next_state;
end

// 第二段:次态逻辑(组合逻辑)
always @(*) begin
    case(state)
        IDLE:   if(start) next_state = RUN;
                else      next_state = IDLE;
        RUN:    if(done)  next_state = IDLE;
                else      next_state = RUN;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(组合逻辑或时序逻辑)
always @(posedge clk or posedge rst) begin
    if(rst)
        out <= 1'b0;
    else begin
        case(state)
            IDLE: out <= 1'b0;
            RUN:  out <= 1'b1;
        endcase
    end
end

小技巧:状态编码尽量用独热码(one-hot),虽然浪费触发器,但译码逻辑简单,时序容易收敛。在激光控制这种对实时性要求高的场景,独热码是首选。

4.4 仿真与Testbench编写

写代码只是第一步,验证才是重头戏。我见过太多人,代码写完了直接上板子,结果一跑就崩。仿真不是为了走形式,是真的能帮你省下大把调试时间。

4.4.1 Testbench的基本结构

一个标准的Testbench长这样:

`timescale 1ns / 1ps

module tb_mux2to1();

    reg  a, b, sel;
    wire y;

    // 实例化待测模块
    mux2to1 uut(
        .a(a),
        .b(b),
        .sel(sel),
        .y(y)
    );

    // 产生激励
    initial begin
        a = 0; b = 0; sel = 0;
        #10;
        a = 1; b = 0; sel = 0;
        #10;
        a = 0; b = 1; sel = 1;
        #10;
        a = 1; b = 1; sel = 1;
        #10;
        $finish;
    end

    // 监控输出
    initial begin
        $monitor("time=%0t, a=%b, b=%b, sel=%b, y=%b", $time, a, b, sel, y);
    end

endmodule

4.4.2 时钟与复位生成

仿真时最常用的就是生成时钟和复位信号:

// 生成50MHz时钟
reg clk;
initial clk = 0;
always #10 clk = ~clk;  // 周期20ns

// 生成复位信号
reg rst;
initial begin
    rst = 1;
    #100;
    rst = 0;
    #1000;
    rst = 1;
end

经验之谈:写Testbench时,我习惯把激励和检查分开。激励用initial块,检查用always块或者$monitor。这样代码结构清晰,出了问题也容易定位。另外,别忘了加$stop或者$finish,不然仿真会一直跑下去。

4.5 本章知识体系

下面这张图,是我自己总结的本章知识脉络。你可以把它当作一个检查清单,看看自己是不是都掌握了。

Verilog基础语法(下) 组合逻辑设计 assign连续赋值 always @(*) 块 case/if-else语句 时序逻辑设计 同步/异步复位 阻塞 vs 非阻塞赋值 寄存器与移位寄存器 状态机基础 三段式写法 独热码编码 状态跳转逻辑 仿真与Testbench Testbench结构 时钟/复位生成

这张图把本章的四个核心模块串起来了。组合逻辑和时序逻辑是基础,状态机是控制核心,仿真验证是质量保证。四者缺一不可。

我的建议:学完这一章,别急着往下走。找个小项目练练手,比如做一个简单的按键消抖模块,或者一个PWM发生器。把组合逻辑、时序逻辑、状态机、仿真全走一遍,你才能真正理解它们是怎么配合的。

好了,这一章的内容就到这儿。记住,写Verilog不是写C语言,脑子里时刻要有硬件画面。下一章我们会把这些知识用到激光运动控制的具体场景中,到时候你就知道今天学的东西有多重要了。


专注资料整理