4. 硬件描述语言Verilog入门(下):组合逻辑建模、时序逻辑建模与测试文件编写

好,咱们接着聊。上一节我们把Verilog的基本语法和模块结构捋了一遍,今天要讲的是真正开始“干活”的部分——怎么用Verilog描述出实际的数字电路。

我个人觉得,学Verilog最忌讳的就是把它当成软件来写。你写的是C语言,编译器帮你安排寄存器和内存;你写Verilog,每一个assign、每一个always块,最终都会变成实实在在的门电路和触发器。脑子里得有这根弦。

4.1 组合逻辑建模:case与if-else

组合逻辑,说白了就是输出只取决于当前输入,跟历史没关系。没有时钟,没有记忆。最常见的写法就是两种:assign连续赋值,或者always @(*)过程赋值。

4.1.1 用if-else实现优先级编码器

if-else结构在综合时会生成优先级逻辑。什么意思呢?就是条件判断从上到下,优先级依次降低。我在项目中遇到过一个问题:一个多路中断控制器的设计,因为if-else的顺序写反了,导致高优先级的中断一直被低优先级抢断。查了两天才发现,嗯,就是顺序问题。

module priority_encoder (
    input  [3:0] req,    // 请求信号,req[3]优先级最高
    output reg [1:0] code // 编码输出
);

always @(*) begin
    if (req[3])      code = 2'b11;
    else if (req[2]) code = 2'b10;
    else if (req[1]) code = 2'b01;
    else if (req[0]) code = 2'b00;
    else             code = 2'b00; // 无请求时默认输出
end

endmodule

小提示:写if-else时,最后一定要加一个else分支。哪怕你觉得“所有情况都覆盖了”,也要加。为什么?因为综合工具会给你生成一个锁存器(latch)来保持未覆盖情况下的输出值。锁存器在数字设计中通常是要避免的,时序难分析,还容易出毛刺。

4.1.2 用case实现多路选择器

case语句生成的是并行选择逻辑,说白了就是一个多路选择器(MUX)。所有分支的优先级是平等的,综合出来的电路也是并行的。

module mux_4to1 (
    input  [1:0] sel,
    input  [3:0] a, b, c, d,
    output reg [3:0] out
);

always @(*) begin
    case (sel)
        2'b00:   out = a;
        2'b01:   out = b;
        2'b10:   out = c;
        2'b11:   out = d;
        default: out = 4'b0; // 安全兜底
    endcase
end

endmodule

注意:case语句一定要写default分支。我曾经见过一个同事,case里只写了三个分支,漏了一个,结果综合出来一个巨大的锁存器阵列,面积翻了一倍。血的教训。

4.1.3 if-else vs case:怎么选?

对比项 if-else case
综合结果 优先级逻辑(串行) 并行选择逻辑(MUX)
面积 条件多时较大 相对较小且固定
时序 关键路径可能较长 路径延迟较均衡
适用场景 中断优先级、仲裁器 指令译码、状态选择

我个人习惯:如果条件之间有明确的优先级关系,用if-else;如果只是平等地选一个,用case。别混着用,容易把自己绕晕。

4.2 时序逻辑建模:同步复位与异步复位

时序逻辑,就是带记忆的电路。核心元件是触发器(Flip-Flop)。在Verilog里,时序逻辑用always @(posedge clk)来描述。

4.2.1 同步复位

同步复位,意思是复位信号只在时钟上升沿到来时才生效。说白了,复位信号必须和时钟同步。

module sync_reset_counter (
    input        clk,
    input        rst_n,  // 低电平有效复位
    input        en,
    output reg [7:0] cnt
);

always @(posedge clk) begin
    if (!rst_n)      cnt <= 8'b0;
    else if (en)     cnt <= cnt + 1'b1;
    // 注意:没有else时,cnt保持原值
end

endmodule

同步复位的优点:抗毛刺能力强。复位信号上的小毛刺,只要不在时钟沿附近,就影响不到触发器。缺点:需要时钟才能复位。如果时钟停了,复位就失效了。

4.2.2 异步复位

异步复位,复位信号一有效,输出立刻清零,不用等时钟。敏感列表里要同时列出时钟和复位。

module async_reset_counter (
    input        clk,
    input        rst_n,
    input        en,
    output reg [7:0] cnt
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)      cnt <= 8'b0;
    else if (en)     cnt <= cnt + 1'b1;
end

endmodule

关键区别:敏感列表不同。同步复位只列posedge clk,异步复位要列posedge clk or negedge rst_n。写错了,综合工具会报warning,甚至综合出错误电路。

4.2.3 我踩过的坑:异步复位的释放问题

我曾经在一个项目中用了异步复位,结果芯片上电后偶尔会跑飞。查了很久才发现,复位释放的时候,不同触发器对复位信号的释放时间有微小差异,导致系统进入非法状态。这就是所谓的“复位释放竞争”。

解决方案:加一个同步释放电路。说白了,就是先把异步复位信号用两级触发器同步一下,再送给系统。

module rst_sync (
    input  clk,
    input  rst_async_n,
    output rst_sync_n
);

reg rst_meta, rst_sync;

always @(posedge clk or negedge rst_async_n) begin
    if (!rst_async_n) begin
        rst_meta <= 1'b0;
        rst_sync <= 1'b0;
    end else begin
        rst_meta <= 1'b1;
        rst_sync <= rst_meta;
    end
end

assign rst_sync_n = rst_sync;

endmodule

现在我做设计,一律用异步复位+同步释放。既保证了复位能快速响应,又避免了释放时的竞争问题。

4.3 测试文件(testbench)编写

写好了设计,怎么验证对不对?写testbench。说白了,就是模拟一个“测试环境”,给设计喂信号,看它输出对不对。

4.3.1 一个完整的testbench结构

`timescale 1ns / 1ps  // 时间单位/精度

module tb_counter;

    // 1. 声明信号
    reg        clk;
    reg        rst_n;
    reg        en;
    wire [7:0] cnt;

    // 2. 实例化待测模块
    sync_reset_counter u_dut (
        .clk  (clk),
        .rst_n(rst_n),
        .en   (en),
        .cnt  (cnt)
    );

    // 3. 生成时钟
    initial begin
        clk = 0;
        forever #5 clk = ~clk;  // 周期10ns
    end

    // 4. 生成激励
    initial begin
        // 初始化
        rst_n = 0;
        en    = 0;
        #20;  // 等待20ns

        // 释放复位
        rst_n = 1;
        #10;

        // 使能计数
        en = 1;
        #50;

        // 暂停计数
        en = 0;
        #20;

        // 再次使能
        en = 1;
        #30;

        // 结束仿真
        $finish;
    end

    // 5. 监控输出(可选)
    initial begin
        $monitor("Time=%0t, rst_n=%b, en=%b, cnt=%d", $time, rst_n, en, cnt);
    end

endmodule

4.3.2 写testbench的几个要点

  • 时间尺度:`timescale定义仿真时间单位,别忘了一开始就写。
  • 时钟生成:forever循环生成连续时钟,别手动一个个翻转。
  • 激励顺序:#延迟控制时间,模拟真实场景。
  • 自动检查:别光看波形,用$displayassert自动比对结果。

我的习惯:每个testbench里至少加一个$monitor,打印关键信号的变化。这样仿真跑完,看一眼打印信息就知道有没有明显错误,不用每次都开波形查看器。

4.4 本章知识体系

下面这张图,把本章的核心知识点串起来了。你写代码的时候,可以对照着看看自己走到哪一步了。

Verilog建模知识体系 组合逻辑建模 时序逻辑建模 if-else(优先级) case(并行选择) 同步复位 异步复位 测试文件(Testbench) 时钟/激励生成 实例化待测模块 结果监控/比对

嗯,这一章内容不少。组合逻辑是基础,时序逻辑是核心,testbench是验证手段。三者缺一不可。你写代码的时候,多想想“这个电路综合出来长什么样”,慢慢就有感觉了。

专注资料整理