4. Verilog基础回顾:模块化设计、组合逻辑与时序逻辑、状态机编写规范

各位同学,咱们今天聊点实在的。做数控系统加减速控制,Verilog是绕不开的工具。但说实话,很多新手一上来就想着写多复杂的代码,结果综合出来一堆问题。我个人的经验是——先把基础打牢,后面才能飞得起来。

这一节,咱们就回顾三个核心点:模块化设计组合逻辑与时序逻辑状态机编写规范。这些都是我这些年做项目反复踩坑又爬出来的经验,希望能帮你少走弯路。

核心观点:好的Verilog设计,不是代码写得有多花哨,而是结构清晰、逻辑严谨、可读性强。数控系统对时序要求极高,任何一点马虎,都可能让电机抖得像筛子。

Verilog基础回顾 模块化设计 接口清晰 · 功能独立 · 可复用 组合逻辑 assign · always@(*) · 无记忆 时序逻辑 always@(posedge clk) · 寄存器 状态机编写规范 三段式 · 可读性 · 避免竞争 端口定义规范 实例化与层次 状态编码 跳转条件 输出逻辑

4.1 模块化设计——别把所有代码塞进一个文件

我记得刚入行那会儿,特别喜欢把整个设计写在一个模块里。结果呢?代码上千行,自己都看晕了。后来带我的老工程师说了一句话,我记到现在:「一个模块只做一件事,做好它。」

模块化设计说白了,就是把大系统拆成小功能块。每个模块有明确的输入输出,内部实现对外部透明。这样做的好处太多了:

  • 可读性强——别人看你的代码,一眼就知道每个模块是干嘛的
  • 可复用——加减速模块写好了,换个项目直接拿来用
  • 调试方便——出问题了,定位到具体模块,不用大海捞针

举个简单的例子,一个加减速控制器,我会拆成这样:

// 顶层模块:加减速控制器
module accel_decel_controller (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        start,
    input  wire [15:0] target_speed,
    output reg  [15:0] current_speed,
    output reg         done
);

    // 子模块实例化
    wire [15:0] accel_rate;
    wire [15:0] decel_rate;
    
    // 速度计算模块
    speed_calculator u_speed_calc (
        .clk(clk),
        .rst_n(rst_n),
        .start(start),
        .target_speed(target_speed),
        .accel_rate(accel_rate),
        .decel_rate(decel_rate),
        .current_speed(current_speed),
        .done(done)
    );
    
    // 加减速曲线生成模块
    curve_generator u_curve_gen (
        .clk(clk),
        .rst_n(rst_n),
        .target_speed(target_speed),
        .current_speed(current_speed),
        .accel_rate(accel_rate),
        .decel_rate(decel_rate)
    );

endmodule

我的小技巧:每个模块的端口尽量控制在10个以内。超过这个数,说明这个模块可能太复杂了,该拆了。我在做五轴联动数控系统时,加减速模块拆成了7个小模块,每个模块的代码不超过100行,调试起来特别爽。

4.2 组合逻辑与时序逻辑——搞清楚谁是谁

这个问题,我见过太多人搞混了。你想想看,组合逻辑就是「输入一变,输出马上变」,没有时钟参与。时序逻辑则是「等到时钟沿来了,才更新输出」

为什么会搞混?因为Verilog里都用always块,但敏感列表不一样。

组合逻辑:always@(*)

组合逻辑用always@(*),或者用assign语句。这里要注意,组合逻辑里不能有寄存器赋值,否则综合出来就是锁存器。

// 组合逻辑示例:加减速状态判断
always @(*) begin
    if (current_speed < target_speed)
        state_next = ACCEL;
    else if (current_speed > target_speed)
        state_next = DECEL;
    else
        state_next = IDLE;
end

避坑指南:我曾经在一个项目里,组合逻辑的always块漏写了敏感列表里的一个信号,结果仿真和综合结果完全不一样。查了两天才发现,气得我差点砸电脑。所以,组合逻辑一律用always@(*),别自己手写敏感列表!

时序逻辑:always@(posedge clk)

时序逻辑的核心是寄存器。数控系统里,速度值、位置值、状态值,这些都需要寄存器来保持。

// 时序逻辑示例:速度寄存器更新
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_speed <= 16'd0;
    else if (load_enable)
        current_speed <= speed_next;
    else
        current_speed <= current_speed;  // 保持
end

这里有个关键点:时序逻辑用非阻塞赋值 <=,组合逻辑用阻塞赋值 =。这个规矩别打破,否则仿真结果会把你搞疯。

类型 敏感列表 赋值方式 典型应用
组合逻辑 always@(*) = (阻塞赋值) 状态判断、数据选择、译码
时序逻辑 always@(posedge clk) <= (非阻塞赋值) 寄存器、计数器、状态机状态

4.3 状态机编写规范——三段式是王道

状态机在数控系统里太常见了。加减速过程本身就是个状态机:空闲→加速→匀速→减速→停止。我见过有人用一段式、两段式,但我个人强烈推荐三段式状态机

三段式就是:

  1. 第一段:时序逻辑,描述状态跳转
  2. 第二段:组合逻辑,描述下一状态
  3. 第三段:组合逻辑或时序逻辑,描述输出

这样做的好处是——结构清晰,可读性强,而且不容易产生竞争冒险。

// 三段式状态机示例:加减速控制
// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= state_next;
end

// 第二段:下一状态组合逻辑
always @(*) begin
    case (state)
        IDLE:   if (start)      state_next = ACCEL;
                else            state_next = IDLE;
        ACCEL:  if (speed_ok)   state_next = RUN;
                else            state_next = ACCEL;
        RUN:    if (stop)       state_next = DECEL;
                else            state_next = RUN;
        DECEL:  if (speed_zero) state_next = IDLE;
                else            state_next = DECEL;
        default: state_next = IDLE;
    endcase
end

// 第三段:输出逻辑
always @(*) begin
    case (state)
        IDLE:   begin
                    accel_en = 1'b0;
                    decel_en = 1'b0;
                    done     = 1'b0;
                end
        ACCEL:  begin
                    accel_en = 1'b1;
                    decel_en = 1'b0;
                    done     = 1'b0;
                end
        RUN:    begin
                    accel_en = 1'b0;
                    decel_en = 1'b0;
                    done     = 1'b0;
                end
        DECEL:  begin
                    accel_en = 1'b0;
                    decel_en = 1'b1;
                    done     = 1'b0;
                end
        default: begin
                    accel_en = 1'b0;
                    decel_en = 1'b0;
                    done     = 1'b0;
                end
    endcase
end

重点提醒:状态编码尽量用独热码(One-Hot)或者格雷码(Gray Code)。独热码适合状态数少于8个的情况,速度快;格雷码适合状态数多的情况,功耗低。我在数控系统里一般用独热码,因为加减速状态通常就4-5个,独热码的译码逻辑最简单。

我的经验:写状态机时,一定要把default写全。别觉得所有情况都覆盖了就不写default。我曾经在一个项目里漏了default,结果综合出来一个奇怪的锁存器,仿真怎么都跑不对。从那以后,我每个case都老老实实写default,哪怕只是回到IDLE状态。

嗯,这一节的内容就到这里。模块化设计让你代码更清爽,组合逻辑和时序逻辑让你搞清楚信号什么时候变,三段式状态机让你写出来的代码别人看得懂、自己也放心。这些都是基本功,但基本功越扎实,后面做加减速控制的时候就越顺手。


专注资料整理