4. Verilog基础回顾(下):always块、时序逻辑与组合逻辑、状态机基础

好,咱们接着聊。上一节我们把Verilog的门面功夫过了一遍,这一节要动真格的了——always块时序逻辑组合逻辑,还有状态机。这些是FPGA设计的骨架,也是你写套利信号检测逻辑时,天天要打交道的玩意儿。

我个人习惯把always块比作“FPGA的神经中枢”。你想想看,所有硬件行为,最终都要靠它来描述。但很多新手一上来就乱用,结果综合出来的电路跟自己想的完全两码事。嗯,咱们今天就把这事彻底捋清楚。

核心要点:always块不是软件里的“死循环”,它是硬件描述语言里的一种“敏感列表驱动”的并行执行体。每次敏感列表里的信号发生变化,它就被触发一次。

4.1 always块:组合逻辑 vs 时序逻辑

先看一个最简单的例子。组合逻辑的always块长这样:

// 组合逻辑:用always实现一个2选1多路器
module mux2to1(
    input  wire a, b, sel,
    output reg y
);

always @(*) begin
    if (sel)
        y = b;
    else
        y = a;
end

endmodule

注意看,敏感列表写的是 @(*),意思是“所有输入信号变化都触发”。这里我用的是阻塞赋值 =。为什么?因为组合逻辑的输出只依赖当前输入,不需要记忆。

那时序逻辑呢?看这个:

// 时序逻辑:带同步复位的D触发器
module dff_sync(
    input  wire clk, rst_n, d,
    output reg q
);

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

endmodule

这里敏感列表是 @(posedge clk),只在时钟上升沿触发。赋值用的是非阻塞赋值 <=。这是铁律——时序逻辑用非阻塞,组合逻辑用阻塞。我在项目中遇到过一位同事,把这两个搞反了,仿真怎么跑都对,上板子就乱跳。查了两天才发现是赋值方式的问题。

避坑指南:我曾经在一个高频交易信号处理模块里,因为组合逻辑里用了非阻塞赋值,导致一个简单的加法器变成了流水线寄存器。信号延迟多了一个时钟周期,整个套利策略的时序全乱了。记住:组合逻辑用 = ,时序逻辑用 <=,别问为什么,先养成习惯。

4.2 组合逻辑的两种写法

组合逻辑其实有两种常见写法。一种是 assign 连续赋值,另一种就是上面说的 always @(*)。我个人更倾向于用 always @(*) 写复杂逻辑,因为可读性更好,尤其是带 if-else 或 case 的时候。

举个例子,一个简单的译码器:

// 用always实现3-8译码器
module decoder3to8(
    input  wire [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_0001;
    endcase
end

endmodule

你看,这样写是不是很直观?每个输入组合对应一个输出。如果用 assign 写,你得写8条语句,累死。

小技巧:写组合逻辑时,一定要保证所有分支都被覆盖。if 要有 else,case 要有 default。否则综合工具会给你生成一个锁存器(latch),这玩意儿在FPGA里是定时炸弹。我见过有人因为漏了default,结果综合出来的电路多了一堆莫名其妙的latch,功耗和面积都炸了。

4.3 时序逻辑:寄存器与计数器

时序逻辑的核心是寄存器。说白了,就是带记忆的单元。每个时钟沿,它把输入“拍”到输出上。咱们做套利信号检测时,经常需要把信号延迟几个周期来做对齐,这时候寄存器就派上用场了。

看一个简单的移位寄存器:

// 3级移位寄存器,用于信号延迟对齐
module shift_reg3(
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,
    output reg [2:0] dout
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        dout <= 3'b0;
    else begin
        dout[0] <= din;
        dout[1] <= dout[0];
        dout[2] <= dout[1];
    end
end

endmodule

这个模块可以把输入信号延迟3个时钟周期输出。在套利系统里,我们经常用这种结构来对齐不同路径上的数据,确保它们在同一个时钟周期被处理。

计数器也是时序逻辑的经典应用。比如我们做套利信号检测时,需要统计某个事件在固定时间窗口内发生的次数:

// 8位计数器,带使能
module counter8(
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [7:0] count
);

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

endmodule

注意:时序逻辑的敏感列表里,一般只写时钟和异步复位。不要画蛇添足把其他信号加进去,否则综合工具会报错或者生成奇怪的东西。

4.4 状态机基础:三段式写法

状态机是FPGA设计的灵魂。套利信号检测里,从“空闲”到“检测”到“确认”再到“触发”,每一步都需要状态机来管理。我个人强烈推荐三段式状态机写法,结构清晰,不易出错。

三段式是哪三段?

  • 第一段:时序逻辑,描述状态跳转(当前状态 → 次态)
  • 第二段:组合逻辑,描述次态逻辑(根据输入和当前状态,计算下一个状态)
  • 第三段:组合逻辑或时序逻辑,描述输出逻辑

看一个简单的例子:检测串行数据中是否出现“101”序列。

// 三段式状态机:检测"101"序列
module seq_detector(
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,
    output reg        dout
);

// 状态编码
localparam IDLE = 2'b00;
localparam S1   = 2'b01;
localparam S10  = 2'b10;
localparam S101 = 2'b11;

reg [1:0] state, next_state;

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

// 第二段:次态逻辑(组合逻辑)
always @(*) begin
    next_state = state;
    case (state)
        IDLE: begin
            if (din == 1'b1)
                next_state = S1;
            else
                next_state = IDLE;
        end
        S1: begin
            if (din == 1'b0)
                next_state = S10;
            else
                next_state = S1;
        end
        S10: begin
            if (din == 1'b1)
                next_state = S101;
            else
                next_state = IDLE;
        end
        S101: begin
            if (din == 1'b1)
                next_state = S1;
            else
                next_state = IDLE;
        end
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(组合逻辑)
always @(*) begin
    if (state == S101)
        dout = 1'b1;
    else
        dout = 1'b0;
end

endmodule

你看,三段式把“状态跳转”、“次态计算”、“输出”分得清清楚楚。我在项目中遇到过有人把这三段混在一起写,结果状态机跑飞了,查bug查到怀疑人生。后来我强制团队都用三段式,问题少了一大半。

个人经验:状态编码尽量用独热码(one-hot)或者格雷码。独热码在FPGA里综合效率高,格雷码适合跨时钟域。别用二进制编码,除非你状态数很少。我曾经在一个128状态的状态机里用了二进制编码,结果组合逻辑路径延迟太大,时序收敛不了。换成独热码后,立马过了。

4.5 本章知识体系

下面这张图把本章的核心逻辑串起来了。你看一眼,心里就有谱了。

Verilog基础回顾(下)知识体系 always块 组合逻辑 阻塞赋值 = always @(*) 时序逻辑 非阻塞赋值 <= @(posedge clk) 寄存器/计数器 状态机(三段式) 状态跳转(时序) 次态逻辑(组合) 输出逻辑(组合) 核心原则 组合逻辑用阻塞赋值 · 时序逻辑用非阻塞赋值 · 状态机用三段式

这张图把本章的三个核心模块串起来了。always块是根,往下分出组合逻辑和时序逻辑,状态机则是两者的综合应用。你写代码时,时刻想着这张图,就不容易跑偏。

最后说一句:Verilog不是写出来的,是“想”出来的。你写每一行代码前,脑子里要先有电路图。always块对应什么硬件?赋值语句对应什么连线?状态机对应什么状态寄存器?想清楚了再动键盘,事半功倍。


公众号:蓝海资料掘金营,微信deep3321