3. 硬件描述语言入门(Verilog/VHDL)

好,咱们直接进入正题。硬件描述语言,说白了就是用来「画」电路的代码。你写下的每一行 Verilog 或 VHDL,最终都会变成实实在在的寄存器、连线、逻辑门。我刚开始学的时候,总觉得这玩意儿跟 C 语言差不多,后来被仿真结果狠狠教育了一顿——嗯,它俩完全是两码事。

3.1 模块化设计:搭积木的思路

一个复杂的 FPGA 项目,没人会从头写到尾。我习惯把整个系统拆成若干个小模块,每个模块只干一件事。比如一个计数器模块、一个状态机模块、一个数据通路模块。这样调试起来也方便,哪个模块出问题,直接定位就行。

模块化设计的核心就是 模块声明。在 Verilog 里,每个模块都以 module 开头,endmodule 结尾。模块的端口就是它跟外界打交道的接口。

// 一个简单的 8 位计数器模块
module counter (
    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

你看,这个模块的输入输出清清楚楚。我在项目中遇到过有人把所有逻辑都塞到一个模块里,结果仿真跑一次要半小时,改个 bug 找半天——千万别学他。

3.2 数据类型:wire 和 reg 的区别

这是新手最容易搞混的地方。我直接说结论:

  • wire:表示连线,只能被 assign 赋值,不能存储值。
  • reg:表示寄存器,可以在 always 块中被赋值,能存储值。

但注意!reg 不一定会综合成寄存器。如果你在组合逻辑的 always 块里用 reg,它综合出来只是一根线。我刚开始就被这个坑过——明明写了 reg,结果综合报告里说没有寄存器。

类型 赋值方式 综合结果 典型用途
wire assign 连线 模块间连接、组合逻辑输出
reg always 块内 寄存器或连线 时序逻辑、组合逻辑(always @(*))
我的习惯: 组合逻辑用 wire + assign,时序逻辑用 reg + always。这样一眼就能看出代码的意图。

3.3 运算符:跟 C 语言有点像,但又不完全一样

Verilog 的运算符分几类:算术、逻辑、位运算、关系、移位、拼接。大部分跟 C 语言一样,但有几个特别的地方:

  • 拼接运算符 { }:把多个信号拼在一起。比如 {a, b} 把 a 和 b 拼成一个更宽的信号。
  • 缩减运算符 &、|、^:对一个向量的所有位做与、或、异或。比如 &data 相当于 data 所有位相与。
  • 相等比较 == 和 ===:=== 可以比较 x 和 z 状态,仿真时很有用。
// 运算符示例
wire [7:0] a = 8'b1010_1010;
wire [7:0] b = 8'b1100_1100;

wire [7:0] and_result = a & b;   // 位与:1000_1000
wire [7:0] or_result  = a | b;   // 位或:1110_1110
wire [15:0] concat    = {a, b};  // 拼接:1010_1010_1100_1100
wire       reduce_and = &a;      // 缩减与:0(因为有 0)
注意: 在 Verilog 里,== 遇到 x 或 z 会返回 x(未知),而 === 会精确比较。我曾经在仿真时用 == 比较状态机状态,结果状态是 x,比较结果也是 x,导致仿真卡死——后来全改成 === 才解决。

3.4 assign 与 always 块:两种赋值方式

这是 Verilog 的核心。你写代码时,要么用 assign,要么用 always,没有第三种选择。

assign:连续赋值

assign 用于组合逻辑。只要右边信号变化,左边立刻更新。说白了就是一根导线。

assign sum = a + b;
assign max = (a > b) ? a : b;

always:过程赋值

always 块可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表:

  • 组合逻辑:always @(*) 或 always @(a or b)
  • 时序逻辑:always @(posedge clk) 或 always @(posedge clk or negedge rst_n)
// 组合逻辑:多路选择器
always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        default: out = 8'd0;
    endcase
end

// 时序逻辑:带复位的寄存器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 8'd0;
    else
        q <= d;
end
关键区别: 组合逻辑用 =(阻塞赋值),时序逻辑用 <=(非阻塞赋值)。混用会导致仿真和综合结果不一致。我见过有人把 = 用在时序逻辑里,结果仿真对了,上板跑出来全是错的——血的教训。

3.5 结构化建模:把模块拼起来

模块写好了,怎么把它们连起来?这就是结构化建模。说白了就是实例化(instantiation)。

假设我们有一个加法器模块 adder 和一个寄存器模块 reg8,现在想做一个累加器:

// 加法器模块
module adder (
    input  [7:0] a, b,
    output [7:0] sum
);
    assign sum = a + b;
endmodule

// 8位寄存器模块
module reg8 (
    input        clk, rst_n,
    input  [7:0] d,
    output [7:0] q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) q <= 8'd0;
        else        q <= d;
    end
endmodule

// 顶层模块:累加器
module accumulator (
    input        clk, rst_n,
    input  [7:0] data_in,
    output [7:0] result
);

    wire [7:0] sum_wire;

    // 实例化加法器
    adder u_adder (
        .a   (data_in),
        .b   (result),
        .sum (sum_wire)
    );

    // 实例化寄存器
    reg8 u_reg (
        .clk   (clk),
        .rst_n (rst_n),
        .d     (sum_wire),
        .q     (result)
    );

endmodule

你看,顶层模块里没有写任何逻辑,只是把两个模块「拼」在一起。这就是结构化建模的精髓——你像搭积木一样,把功能模块组合成更大的系统。

命名规范: 我习惯用 u_ 前缀表示实例化名称,比如 u_adder、u_reg。这样在仿真波形里一眼就能看出哪个信号属于哪个模块。

3.6 知识体系总览

下面这张图概括了本章的核心内容。你可以把它当作一个「地图」,以后写代码时对照着看。

硬件描述语言入门知识体系 Verilog / VHDL 入门 模块化设计 module / endmodule 端口声明 (input/output) 数据类型 wire(连线) reg(寄存器) 运算符 算术 / 逻辑 / 位运算 拼接 / 缩减 / 关系 assign / always 连续赋值 (组合逻辑) 过程赋值 (时序/组合) 结构化建模 模块实例化 端口连接 (.name) 阻塞 vs 非阻塞 = 阻塞赋值 (组合) <= 非阻塞赋值 (时序) 核心:写代码 = 画电路,每一行都有物理意义

嗯,这张图把本章的知识点串起来了。你写代码时,心里要时刻想着:我写的这个 assign 对应什么电路?这个 always 块综合出来是寄存器还是组合逻辑?想清楚了,代码就不会跑偏。

可可精品资料库