第三讲:硬件描述语言基础(Verilog)——模块结构、组合逻辑、时序逻辑与状态机

各位同学好,今天我们聊聊Verilog最核心的几个概念。说实话,很多新手一上来就被各种语法吓住了,其实没那么复杂。你想想看,硬件描述语言说白了就是「画电路图的文字版」。我们不是在写软件,而是在描述硬件该长什么样、该怎么动。

一、模块结构——芯片的「积木块」

Verilog里最基本的单元就是模块(module)。每个模块就像一块乐高积木,有接口(端口),有内部逻辑。我习惯把模块想象成一个黑盒子——外面的人只关心你输入什么、输出什么,里面怎么实现的,那是你自己的事。

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    input  wire       en,       // 使能
    output reg  [7:0] count     // 8位计数器输出
);

    // 内部逻辑写在这里

endmodule

注意看,端口声明有input、output、inout三种。数据类型常用wire和reg。很多初学者搞不清什么时候用wire、什么时候用reg。我给大家一个简单粗暴的判断方法:assign语句左边必须是wire,always块里被赋值的变量必须是reg。记住这个,至少能避开80%的编译错误。

小技巧:我个人习惯把模块的端口声明和内部信号分开写。端口用input/output/inout,内部信号用wire/reg单独定义。这样代码结构清晰,别人看你的代码也舒服。

二、组合逻辑——用assign搭电路

组合逻辑,就是输出只取决于当前输入,跟历史状态无关。说白了,就是一堆逻辑门连在一起。Verilog里用assign关键字来描述组合逻辑。

// 一个简单的加法器
module adder (
    input  wire [3:0] a,
    input  wire [3:0] b,
    output wire [4:0] sum
);

    assign sum = a + b;

endmodule

你看,一行assign就描述了一个加法器。但要注意,assign是连续赋值,只要右边信号变化,左边立刻更新。这跟软件里的赋值完全不一样——软件是顺序执行,硬件是并行执行的。

我在项目中遇到过一个问题:有人用assign写了一个多级组合逻辑,结果时序跑不过。为什么呢?因为组合逻辑路径太长了,信号传播需要时间。所以,复杂的组合逻辑一定要加流水线寄存器,否则上板就翻车。

避坑指南:我曾经在一个项目中,用assign写了32位的加法器,后面还跟了一堆比较器。综合后时序报告一片红。后来拆成两级流水,每级加一个寄存器,问题就解决了。记住:组合逻辑不是不能多,但要控制深度。

三、时序逻辑——用always块「记住」状态

时序逻辑跟组合逻辑最大的区别是:它有记忆。它依赖时钟边沿来更新状态。Verilog里用always @(posedge clk)来描述时序逻辑。

// 一个带同步复位的D触发器
module dff (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       d,
    output reg        q
);

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

endmodule

注意这里用的是非阻塞赋值 <=,不是=。这是新手最容易踩的坑。非阻塞赋值的意思是:所有赋值操作在时钟沿到来时同时计算,然后同时更新。而阻塞赋值=是顺序执行的,用在时序逻辑里会出大问题。

我给大家一个铁律:时序逻辑用 <=,组合逻辑用 =。别问为什么,先记住,等你写多了自然就懂了。

核心要点:always块里可以同时描述组合逻辑和时序逻辑,但一定要分清楚。我习惯把时序逻辑和组合逻辑写在不同的always块里,这样仿真和调试都方便。

四、状态机——让电路「有脑子」

状态机是数字电路设计的灵魂。说白了,就是让电路知道「我现在在干什么,下一步该干什么」。最常见的写法是三段式状态机,我强烈推荐大家用这种写法。

// 三段式状态机示例:一个简单的交通灯控制器
module traffic_light (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [1:0] light  // 00:红, 01:黄, 10:绿
);

    // 第一段:状态定义
    localparam IDLE   = 2'b00;
    localparam RED    = 2'b01;
    localparam YELLOW = 2'b10;
    localparam GREEN  = 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;
        light = 2'b00;
        case (state)
            IDLE: begin
                next_state = RED;
                light = 2'b00;
            end
            RED: begin
                next_state = YELLOW;
                light = 2'b01;
            end
            YELLOW: begin
                next_state = GREEN;
                light = 2'b10;
            end
            GREEN: begin
                next_state = RED;
                light = 2'b11;
            end
            default: begin
                next_state = IDLE;
                light = 2'b00;
            end
        endcase
    end

endmodule

三段式的优点很明显:状态转移、次态逻辑、输出逻辑分得清清楚楚。调试的时候,看波形一眼就知道问题出在哪一段。我曾经接手过一个项目,前人用两段式写的状态机,代码乱成一锅粥,我花了三天才理清楚。从那以后,我坚持用三段式。

经验之谈:写状态机时,一定要加default分支。为什么?因为综合工具会认为你没覆盖到的状态是「无关项」,可能会综合出锁存器。锁存器是硬件设计的大忌,时序难控、面积又大。加个default,把未定义状态引导到安全状态,这是好习惯。

五、知识体系总览

下面这张图是我自己总结的Verilog基础结构关系图,大家看看就明白了:

Verilog 模块 (module) 组合逻辑 (assign) 时序逻辑 (always) 状态机 (FSM) 连续赋值 wire 类型 非阻塞赋值 <= reg 类型 三段式写法 状态转移图 核心原则:组合逻辑用 assign/阻塞赋值 时序逻辑用 always/非阻塞赋值 记住这个结构,写Verilog就像搭积木一样简单

这张图把今天讲的内容串起来了。你看,一个模块内部无非就是组合逻辑、时序逻辑、状态机这三样东西。组合逻辑用assign搭,时序逻辑用always块写,状态机用三段式组织。搞清楚了这些,Verilog就算入门了。

嗯,今天就讲到这里。记住我强调的那几个关键点:wire和reg的用法、阻塞和非阻塞赋值的区别、三段式状态机的写法。这些都是我踩过坑之后总结出来的,你们少走弯路。

可可精品资料库