第一章:硬件描述语言入门(Verilog)——模块结构、数据类型、always块与assign语句、组合逻辑设计

各位同学,欢迎来到基础设施芯片开发的第一课。我是你们的讲师,一个在数字电路设计里摸爬滚打了十几年的老工程师。今天咱们聊Verilog,这门硬件描述语言。

说实话,我见过太多新人一上来就抱着语法书啃,结果越看越懵。为什么?因为Verilog不是软件编程语言,它描述的是硬件。你写的是电路,不是算法。这个观念不转变,后面会走很多弯路。

好,咱们直接进入正题。

1.1 模块结构——芯片设计的“积木块”

Verilog里最基本的设计单元叫模块(module)。你可以把它想象成一块乐高积木。每个模块都有输入、输出,内部封装了具体的逻辑功能。

一个完整的模块长这样:

module counter (
    input  wire clk,      // 时钟信号
    input  wire rst_n,    // 复位信号(低有效)
    output reg  [3:0] cnt // 4位计数器输出
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 4'b0000;
        else
            cnt <= cnt + 1'b1;
    end

endmodule

注意看,moduleendmodule是一对括号,所有代码都写在这中间。端口列表里,inputoutput指明了信号方向。我个人习惯把时钟和复位放在端口列表的最前面,这样一眼就能看到时序逻辑的驱动源。

我的习惯:模块名用下划线命名法,比如uart_txspi_master。端口声明时,每个信号单独一行,方便后期维护和代码审查。

1.2 数据类型——wire和reg,别再搞混了

很多新手问我:“老师,wire和reg到底啥区别?”

嗯,这个问题我当年也纠结过。其实很简单:

  • wire:代表连线。它本身不存储值,只是把两个端口连起来。比如模块A的输出接到模块B的输入,中间这根线就是wire。
  • reg:代表寄存器。它能存储一个值,直到被新的赋值覆盖。注意,reg不一定真的综合成寄存器,它只是行为描述里的一个变量。

举个例子:

wire a, b, c;
assign c = a & b;  // c是wire,用assign驱动

reg [7:0] data_reg;
always @(posedge clk) begin
    data_reg <= data_in;  // data_reg是reg,在always块里赋值
end

我在项目中遇到过最坑的事:有人把组合逻辑的输出声明成reg,结果综合工具报了一堆警告。其实组合逻辑用wire就够了,除非你非要在always块里写组合逻辑——那确实得用reg。

避坑指南:我曾经因为把wire和reg搞混,导致仿真和综合结果不一致。记住一条铁律:assign语句左边必须是wire,always块里被赋值的变量必须是reg

1.3 always块与assign语句——两种描述方式

Verilog里描述逻辑有两种主要方式:assignalways

assign语句:连续赋值,用于组合逻辑。只要右边信号变化,左边立即更新。

assign sum = a + b;
assign carry = a & b;

always块:过程赋值,可以描述组合逻辑和时序逻辑。敏感列表决定了它什么时候被触发。

// 组合逻辑:敏感列表里所有信号
always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

// 时序逻辑:敏感列表里只有时钟和复位
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

你想想看,为什么组合逻辑用@(*)?因为所有输入信号变化都要触发。而时序逻辑只关心时钟沿,因为寄存器只在时钟沿采样。

重要区别:组合逻辑用阻塞赋值=,时序逻辑用非阻塞赋值<=。这个规则必须遵守,否则仿真结果会完全错误。我见过有人混用,结果仿真波形乱成一团,查了三天才找到原因。

1.4 简单的组合逻辑设计——从真值表到代码

组合逻辑就是输出只取决于当前输入,没有记忆功能。比如加法器、多路选择器、译码器。

咱们设计一个4选1多路选择器:

module mux4 (
    input  wire [1:0] sel,  // 选择信号
    input  wire [3:0] a, b, c, d,  // 四个输入
    output reg  [3:0] out    // 输出
);

    always @(*) begin
        case (sel)
            2'b00: out = a;
            2'b01: out = b;
            2'b10: out = c;
            2'b11: out = d;
            default: out = 4'b0000;
        endcase
    end

endmodule

注意,我用了default分支。为什么?因为case语句如果不写全,综合工具会生成锁存器(latch)。锁存器这东西,能不用就别用。我曾经在一个项目里因为漏了default,导致芯片功耗异常,最后定位到是latch在作怪。

再来看一个加法器:

module adder (
    input  wire [7:0] a, b,
    input  wire       cin,      // 进位输入
    output wire [7:0] sum,      // 和
    output wire       cout      // 进位输出
);

    assign {cout, sum} = a + b + cin;

endmodule

这里用拼接运算符{}把进位和结果拼在一起,简洁明了。组合逻辑用assign写,代码更直观。

知识体系总览

下面这张图总结了本章的核心知识点,你可以把它当作学习路线图:

Verilog入门知识体系 模块结构 数据类型 描述方式 端口声明 (input/output) 内部信号与实例化 wire:连线 reg:寄存器/变量 assign:连续赋值 always:过程赋值 组合逻辑设计

从这张图可以看出,模块结构是外壳,数据类型是材料,描述方式是工具,最终目标就是设计出正确的组合逻辑电路。

本章小结

咱们今天讲了四个核心点:

  1. 模块结构:module/endmodule是基本框架,端口要分清楚方向。
  2. wire和reg:wire用于连线,reg用于存储。assign左边是wire,always块里被赋值的是reg。
  3. assign和always:assign描述组合逻辑,always可以描述组合和时序。组合逻辑用=,时序逻辑用<=
  4. 组合逻辑设计:从真值表到代码,注意case语句要写default,避免生成latch。

说实话,这些基础概念看起来简单,但真正用好需要大量练习。我建议你课后把今天讲的代码都敲一遍,跑一下仿真。遇到问题别怕,每个工程师都是从踩坑中成长起来的。

好,今天就到这里。记住,Verilog不是写代码,是画电路。带着这个思路去学,你会豁然开朗。


专注资料整理