3. Verilog基础回顾:模块化设计、组合逻辑与时序逻辑、状态机编写技巧、Testbench编写

各位同学,欢迎来到第三章。说实话,每次开课讲到Verilog基础,总有人觉得“这我早会了”。但我在伺服控制项目里摸爬滚打这么多年,发现很多bug的根源,恰恰是基础不牢。今天咱们不搞枯燥的语法罗列,我挑几个在FPGA伺服控制中真正高频、真正容易踩坑的点,好好捋一遍。

3.1 模块化设计:别把鸡蛋放在一个篮子里

模块化设计,说白了就是“分而治之”。一个复杂的伺服控制算法,比如电流环的PID计算,你要是全写在一个always块里,那调试起来简直要命。我个人习惯,把功能拆成独立的module,每个module只干一件事。

为什么必须模块化?

  • 可读性:别人看你的代码,一眼就知道哪个模块是干嘛的。
  • 可复用性:写好的PID模块,下次换个项目直接拿来用。
  • 可调试性:出问题了,定位到具体模块,不用从头翻到尾。
我的经验:在伺服控制中,我通常把模块分为三层:顶层控制层(状态机)、算法运算层(PID、坐标变换)、接口层(ADC读取、PWM输出)。这样层次清晰,改起来也方便。

来看一个简单的模块化示例。假设我们要做一个加法器模块:

// 加法器模块
module adder #(
    parameter WIDTH = 16
)(
    input  wire [WIDTH-1:0] a,
    input  wire [WIDTH-1:0] b,
    output reg  [WIDTH-1:0] sum
);
    always @(*) begin
        sum = a + b;
    end
endmodule

然后在顶层例化它:

module top (
    input  wire [15:0] data_in1,
    input  wire [15:0] data_in2,
    output wire [15:0] result
);
    adder #(.WIDTH(16)) u_adder (
        .a(data_in1),
        .b(data_in2),
        .sum(result)
    );
endmodule

你看,这样多清爽。每个模块的接口清晰,内部实现细节被隐藏了。这就是模块化的精髓。

3.2 组合逻辑与时序逻辑:分清“现在”和“未来”

这是新手最容易混淆的地方。组合逻辑的输出只取决于当前输入,而时序逻辑的输出还取决于之前的状态。在伺服控制中,组合逻辑常用于计算误差、生成使能信号;时序逻辑则用于状态寄存、计数器累加。

组合逻辑:用 assign 或 always @(*)

// 组合逻辑:计算误差
assign error = target - current;

时序逻辑:用 always @(posedge clk)

// 时序逻辑:累加误差
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        error_sum <= 0;
    else
        error_sum <= error_sum + error;
end
避坑指南:我曾经在项目中犯过一个低级错误——在时序逻辑里用了阻塞赋值(=)。结果仿真时数据总是对不上,查了两天才发现是赋值方式搞错了。记住:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。这是铁律。

为什么会这样?因为非阻塞赋值是“并行”的,它会在所有赋值语句计算完成后才更新寄存器。而阻塞赋值是“顺序”的,会立即更新。在时序逻辑里用阻塞赋值,很容易产生意想不到的竞争冒险。

3.3 状态机编写技巧:让控制逻辑井井有条

状态机是伺服控制的核心。比如电机启动、停止、急停、故障处理,这些都需要状态机来管理。我个人偏爱三段式状态机,因为它结构清晰,且组合逻辑和时序逻辑分离,不容易出错。

三段式状态机模板:

// 第一段:时序逻辑,状态跳转
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

// 第二段:组合逻辑,下一状态判断
always @(*) begin
    case (current_state)
        IDLE: begin
            if (start_signal)
                next_state = RUN;
            else
                next_state = IDLE;
        end
        RUN: begin
            if (stop_signal)
                next_state = STOP;
            else
                next_state = RUN;
        end
        STOP: begin
            next_state = IDLE;
        end
        default: next_state = IDLE;
    endcase
end

// 第三段:时序逻辑,输出控制
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        motor_enable <= 0;
        fault_clear  <= 0;
    end else begin
        case (current_state)
            IDLE: begin
                motor_enable <= 0;
                fault_clear  <= 0;
            end
            RUN: begin
                motor_enable <= 1;
                fault_clear  <= 0;
            end
            STOP: begin
                motor_enable <= 0;
                fault_clear  <= 1;
            end
        endcase
    end
end

你想想看,这样写的好处是什么?状态跳转逻辑和输出逻辑完全分开。改输出时不会影响状态跳转,改状态跳转时也不会误触输出。我在做多轴同步控制时,就是靠这种结构,轻松管理了六个电机的协同状态。

关键点:状态机的编码方式也很重要。对于状态数少于8个的,用独热码(One-hot)或二进制码都可以。但状态数多时,我建议用格雷码,能有效减少毛刺。

3.4 Testbench编写:别让仿真成为摆设

很多同学写完RTL代码就直接上板,结果一跑就炸。其实,花半小时写个Testbench,能省下半天调试时间。Testbench说白了就是给设计“喂”激励,然后看它输出对不对。

一个基本的Testbench结构:

`timescale 1ns / 1ps

module tb_adder;

    // 信号定义
    reg  [15:0] a, b;
    wire [15:0] sum;

    // 例化待测模块
    adder #(.WIDTH(16)) uut (
        .a(a),
        .b(b),
        .sum(sum)
    );

    // 生成时钟(如果需要)
    reg clk;
    always #5 clk = ~clk; // 10ns周期

    // 测试激励
    initial begin
        // 初始化
        a = 0; b = 0;
        #10;

        // 测试用例1
        a = 16'd10; b = 16'd20;
        #10;
        if (sum !== 16'd30) $display("Error: 10+20 != 30");

        // 测试用例2
        a = 16'd100; b = 16'd200;
        #10;
        if (sum !== 16'd300) $display("Error: 100+200 != 300");

        // 测试完成
        #10;
        $finish;
    end

    // 波形输出
    initial begin
        $dumpfile("wave.vcd");
        $dumpvars(0, tb_adder);
    end

endmodule
我的习惯:写Testbench时,我会先列一个测试用例清单。比如边界值、溢出、复位等。然后逐个写激励,并加$display打印关键信息。这样仿真跑完,一眼就能看出哪里错了。

另外,别忘了用`timescale定义时间单位。我见过有人忘了写,结果仿真时间全乱套了。嗯,这里要注意,时间精度要足够小,比如1ps,否则高精度仿真会出问题。

知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

Verilog基础回顾:知识体系 Verilog基础 模块化设计 组合与时序逻辑 状态机编写 Testbench编写 分而治之 接口清晰 阻塞赋值 非阻塞赋值 三段式 独热码 激励生成 结果检查 每个分支都是伺服控制算法硬件加速的基石

这张图把本章的四个核心知识点串起来了。模块化是骨架,组合与时序逻辑是血肉,状态机是大脑,Testbench是体检仪。缺了哪个,你的设计都跑不稳。

好了,这一章的内容就到这里。记住,Verilog基础不是背语法,而是理解硬件思维。多写、多仿、多踩坑,自然就熟了。


专注资料整理