4. Verilog基础(下):组合逻辑与时序逻辑设计

好,咱们接着聊。上一章我们把Verilog的基本语法和数据类型过了一遍,这一章要动真格的了——直接上手写电路。我个人觉得,学Verilog最忌讳的就是把它当软件来写。你写的每一行代码,最终都会变成实实在在的硬件。所以,脑子里要时刻有电路图。

这一章我们分三块:组合逻辑、时序逻辑、状态机。这三块是数字设计的基石,你想想看,再复杂的芯片,拆开来看也无非就是这些东西的组合。

本章知识体系 组合逻辑设计 加法器 · 多路选择器 · 译码器 时序逻辑设计 计数器 · 分频器 · 移位寄存器 状态机设计 Moore · Mealy 核心设计原则 • 组合逻辑:输出只取决于当前输入,没有记忆功能 • 时序逻辑:输出取决于当前输入和过去状态,有记忆功能

4.1 组合逻辑设计

组合逻辑,说白了就是没有记忆的电路。输入一变,输出立马跟着变,中间不带任何延迟(理想情况下)。实际电路中当然有门延迟,但那是物理特性,不是逻辑功能。

4.1.1 加法器

加法器是算术运算的基础。最简单的就是半加器,只加两个bit,不考虑进位。全加器则多了一个进位输入。我刚开始学的时候,总觉得全加器挺绕的,后来画了个真值表,一下就明白了。

全加器真值表

ABCinSCout
00000
00110
01010
01101
10010
10101
11001
11111

用Verilog实现全加器,最直接的方式就是用连续赋值语句:

// 全加器 - 数据流建模
module full_adder (
    input  a, b, cin,
    output s, cout
);
    assign s    = a ^ b ^ cin;
    assign cout = (a & b) | (a & cin) | (b & cin);
endmodule

当然,你也可以用行为级描述,用always块。但我个人习惯,简单的组合逻辑用assign就够了,代码更简洁,综合出来的结果也差不多。

如果要做一个多位加法器,最简单的办法就是例化多个全加器,或者直接用加号。嗯,这里要注意:直接用加号虽然方便,但综合工具会推断出进位链,位宽越大,延迟越大。我在一个项目中做过128位的加法器,直接用加号,结果时序跑不过。后来改成了进位选择加法器(Carry-Select Adder),才把时序搞定。

4.1.2 多路选择器

多路选择器,简称MUX,就是从多个输入中选一个出来。2选1 MUX是最基本的:

// 2选1多路选择器
module mux2to1 (
    input  [7:0] a, b,
    input        sel,
    output [7:0] y
);
    assign y = sel ? b : a;
endmodule

三目运算符在这里特别好用。如果是4选1,可以用case语句:

// 4选1多路选择器
module mux4to1 (
    input  [7:0] d0, d1, d2, d3,
    input  [1:0] sel,
    output reg   [7:0] y
);
    always @(*) begin
        case (sel)
            2'b00: y = d0;
            2'b01: y = d1;
            2'b10: y = d2;
            2'b11: y = d3;
        endcase
    end
endmodule

小技巧:写case语句时,一定要把所有的分支都覆盖到。如果有些组合不会出现,用default兜底,赋值成'bx或者'bz。我曾经因为漏了default,综合出来一堆锁存器,查了半天才找到原因。

4.1.3 译码器

译码器就是把一个编码转换成另一个编码。最常用的是3-8译码器:3位输入,8位输出,每次只有一位输出有效。

// 3-8译码器
module decoder3to8 (
    input  [2:0] in,
    output reg   [7:0] out
);
    always @(*) begin
        out = 8'b0;
        out[in] = 1'b1;
    end
endmodule

你看,这个写法多简洁。直接用in作为索引,把对应位置1。不过要注意,这种写法要求输入不能是'x或'z,否则综合出来会有问题。

4.2 时序逻辑设计

时序逻辑和组合逻辑最大的区别是什么?它有时钟,有记忆。说白了,就是电路的状态会随着时钟边沿发生变化。写时序逻辑,核心就是always块配合posedge clk。

4.2.1 计数器

计数器是时序逻辑里最经典的例子。一个简单的二进制计数器:

// 8位二进制计数器
module counter (
    input        clk,
    input        rst_n,
    output reg   [7:0] cnt
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 8'b0;
        else
            cnt <= cnt + 1'b1;
    end
endmodule

这里我用了异步复位,rst_n低电平有效。为什么用低电平?很多FPGA的复位引脚默认就是低电平复位,这是硬件决定的。另外注意,时序逻辑里赋值要用非阻塞赋值(<=),这个和组合逻辑的阻塞赋值(=)要区分开。搞混了的话,仿真结果会非常诡异。

避坑指南:我曾经在一个项目里,把always块里的非阻塞赋值写成了阻塞赋值,结果仿真波形看起来是对的,但上板跑就是不对。后来用Vivado的RTL分析一看,综合出来的电路和我预想的完全不一样。所以,记住:时序逻辑用 <=,组合逻辑用 =。

4.2.2 分频器

分频器本质上就是计数器。比如要把50MHz的时钟分成1Hz,那就数50,000,000个时钟周期,翻转一次输出。但实际项目中,我很少用计数器做分频,因为PLL(锁相环)才是正经的分频方案,精度高、抖动小。

不过,如果是简单的二分频,用寄存器取反就行:

// 二分频器
module div2 (
    input  clk,
    input  rst_n,
    output reg clk_out
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            clk_out <= 1'b0;
        else
            clk_out <= ~clk_out;
    end
endmodule

嗯,这里要提醒一下:分频出来的时钟,最好不要作为其他模块的时钟源。因为它的相位和原始时钟不同,容易引起时序问题。更好的做法是用时钟使能信号。

4.2.3 移位寄存器

移位寄存器在串行通信中特别常用。比如SPI接口,就是通过移位寄存器把并行数据转成串行发出去。

// 8位移位寄存器
module shift_reg (
    input        clk,
    input        rst_n,
    input        din,
    output reg   [7:0] q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 8'b0;
        else
            q <= {q[6:0], din};
    end
endmodule

这个写法,每次时钟上升沿,数据向左移一位,新数据从低位进来。如果你想从高位进,那就反过来:q <= {din, q[7:1]}。

4.3 状态机设计

状态机是数字设计的灵魂。你想想看,任何复杂的控制逻辑,都可以用状态机来描述。状态机分两种:Moore型和Mealy型。区别很简单:Moore型的输出只取决于当前状态,Mealy型的输出还取决于输入。

4.3.1 Moore型状态机

Moore型状态机的输出,只和当前状态有关。所以它的输出在时钟边沿之后才会变化,比输入晚一个周期。这种状态机比较稳定,不容易出现毛刺。

// Moore型状态机示例:检测序列"101"
module moore_fsm (
    input  clk,
    input  rst_n,
    input  din,
    output reg dout
);
    // 状态编码
    localparam IDLE = 2'b00,
               S1   = 2'b01,
               S2   = 2'b10,
               S3   = 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: next_state = din ? S1 : IDLE;
            S1:   next_state = din ? S1 : S2;
            S2:   next_state = din ? S3 : IDLE;
            S3:   next_state = din ? S1 : S2;
        endcase
    end
    
    // 输出逻辑(Moore型:只和当前状态有关)
    always @(*) begin
        dout = (state == S3) ? 1'b1 : 1'b0;
    end
endmodule

你看,我把状态机分成了三部分:状态寄存器、次态逻辑、输出逻辑。这种三段式写法是我最推荐的,代码清晰,综合结果也好。

4.3.2 Mealy型状态机

Mealy型的输出和输入有关,所以它可以在同一个时钟周期内对输入做出反应。说白了,就是响应更快。

// Mealy型状态机示例:检测序列"101"
module mealy_fsm (
    input  clk,
    input  rst_n,
    input  din,
    output reg dout
);
    localparam IDLE = 2'b00,
               S1   = 2'b01,
               S2   = 2'b10;
    
    reg [1:0] state, next_state;
    
    // 状态寄存器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= IDLE;
        else
            state <= next_state;
    end
    
    // 次态逻辑和输出逻辑(Mealy型:输出和输入有关)
    always @(*) begin
        next_state = state;
        dout = 1'b0;
        case (state)
            IDLE: begin
                if (din) next_state = S1;
                else     next_state = IDLE;
            end
            S1: begin
                if (din) next_state = S1;
                else     next_state = S2;
            end
            S2: begin
                if (din) begin
                    next_state = S1;
                    dout = 1'b1;  // 检测到"101"
                end else begin
                    next_state = IDLE;
                end
            end
        endcase
    end
endmodule

Moore vs Mealy 对比

特性Moore型Mealy型
输出依赖只依赖当前状态依赖当前状态和输入
输出时机时钟边沿后变化输入变化时立即变化
状态数量通常较多通常较少
响应速度慢一个周期快,组合逻辑直接输出
毛刺风险高(输出是组合逻辑)

我个人在实际项目中,更倾向于用Moore型。为什么?因为它的输出是寄存的,没有毛刺风险。Mealy型虽然响应快,但输出是组合逻辑,容易出毛刺。除非对时序要求特别苛刻,否则我建议你用Moore。

经验之谈:写状态机的时候,状态编码也有讲究。小状态机用二进制编码就行,状态多了(比如几十个),可以考虑用独热码(One-Hot)。独热码虽然浪费寄存器,但译码逻辑简单,跑得快。我在一个通信协议解析的项目里,用了16个状态的独热码状态机,时序轻松跑过200MHz。

好了,这一章的内容就到这里。组合逻辑、时序逻辑、状态机,这三块是FPGA设计的核心。你把这些吃透了,后面学什么接口协议、什么IP核,都会轻松很多。记住,多写代码,多仿真,多上板验证。纸上谈兵是学不会FPGA的。


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