2、硬件描述语言基础:Verilog/VHDL 的核心思想

说实话,很多刚入行的朋友问我:“FPGA 开发最难的是什么?” 我通常会回答:“不是语法,是思维方式的转变。”

你想想看,写 C 语言时,你脑子里想的是“先做这个,再做那个”。但写 Verilog 时,你得想“这个模块和那个模块同时在工作”。这就是硬件描述语言最核心的地方——并行思维

我个人习惯把 Verilog 看作是在“画电路图”,而不是在“写程序”。每一行代码,最终都会变成实实在在的门电路和触发器。嗯,这个观念很重要,咱们先把它刻在脑子里。

2.1 模块化设计思想:搭积木的艺术

我在项目中遇到过最头疼的事,就是接手一个同事写的“大平层”代码——所有逻辑都写在一个模块里,几万行代码,看得我头皮发麻。

模块化设计,说白了就是把一个复杂系统拆成多个小模块。每个模块只干一件事,但要把这件事干好。

模块化设计的三个原则:

  • 高内聚:一个模块内部的功能要紧密相关。比如一个 UART 模块,就只负责串口收发,别在里面掺和 LED 闪烁的逻辑。
  • 低耦合:模块之间的接口要简单清晰。能用 1 根线传的信号,别用 10 根。
  • 接口标准化:我建议你从一开始就统一模块的命名规则和握手协议。比如所有模块都用 valid/ready 握手,这样后期集成会省很多事。

一个典型的 Verilog 模块长这样:

module counter #(
    parameter WIDTH = 8
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [WIDTH-1:0] count
);

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

endmodule

你看,这个计数器模块的接口清清楚楚:时钟、复位、使能、计数值。内部逻辑也很纯粹——就是计数。这就是模块化的精髓。

2.2 数据类型:wire 与 reg 的“爱恨情仇”

新手最容易搞混的就是 wire 和 reg。我刚开始学的时候也犯过迷糊,后来想明白了一个道理:wire 是“连线”,reg 是“变量”

特性 wire reg
本质 物理连线 存储单元(不一定综合成寄存器)
赋值方式 assign 连续赋值 always 块内赋值
默认值 高阻态 Z 不定态 X
驱动源 只能有一个驱动 可以在多个 always 块中赋值(不推荐)

这里有个关键点:reg 不一定会综合成寄存器。如果你在 always 块里写组合逻辑,reg 综合出来就是一堆门电路,而不是触发器。我曾经见过有人把所有的 reg 都当成寄存器来用,结果综合出来的面积大得吓人。

我的个人习惯:

  • 模块的输入端口一律用 wire
  • 组合逻辑的输出用 wire,用 assign 赋值
  • 时序逻辑的输出用 reg,在 always 块里赋值
  • 中间信号看情况,能用 wire 尽量用 wire

2.3 运算符:不只是加减乘除

Verilog 的运算符和 C 语言很像,但有几个地方要特别注意。

位宽问题: 这是最容易踩坑的地方。比如:

wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b0110;
wire [3:0] c = a + b;  // 结果是 4'b0000,因为溢出了

你看,4 位加 4 位,结果还是 4 位,最高位的进位被截断了。我建议你在做加法时,先把结果位宽扩展一位:

wire [4:0] c = {1'b0, a} + {1'b0, b};  // 结果是 5'b10000

逻辑运算符 vs 位运算符:

  • &&|| 是逻辑运算符,结果只有 0 或 1
  • &| 是位运算符,对每一位分别操作

举个例子:4'b1010 && 4'b0101 结果是 1(因为两个数都不为 0),而 4'b1010 & 4'b0101 结果是 4'b0000(按位与)。

注意: 在条件判断中,一定要用逻辑运算符。我曾经见过有人写 if (a & b),结果 a 和 b 都是多位宽时,行为完全不符合预期。

2.4 assign 与 always:两种赋值方式

assign 和 always 是 Verilog 中两种最基本的赋值方式,它们的区别说白了就是:assign 是“一直连着的”,always 是“条件触发的”

assign 连续赋值:

wire [3:0] sum;
assign sum = a + b;  // 只要 a 或 b 变化,sum 立即更新

这就像一根导线,输入变了,输出马上跟着变。适合描述组合逻辑。

always 过程赋值:

always @(posedge clk) begin
    q <= d;  // 只在时钟上升沿更新
end

这就像是一个触发器,只在特定时刻采样输入。适合描述时序逻辑。

这里有个重要的区别:阻塞赋值 (=) 和非阻塞赋值 (<=)

  • 阻塞赋值 (=):顺序执行,前面的赋值会阻塞后面的。用于组合逻辑。
  • 非阻塞赋值 (<=):并行执行,所有赋值同时发生。用于时序逻辑。

我曾经犯过一个低级错误:在 always 块里用阻塞赋值写时序逻辑,结果仿真和实际行为完全对不上。后来养成了习惯:写时序逻辑只用 <=,写组合逻辑只用 =

2.5 组合逻辑与时序逻辑:本质区别

这个问题我问过很多面试者,能说清楚的不多。其实很简单:

  • 组合逻辑:输出只取决于当前输入,没有记忆功能。比如加法器、多路选择器。
  • 时序逻辑:输出不仅取决于当前输入,还取决于历史状态。比如计数器、状态机。

用一句话概括:组合逻辑没有“记忆”,时序逻辑有“记忆”

在代码层面,区分它们的方法也很直接:

  • 组合逻辑的 always 块,敏感列表里没有时钟边沿(比如 always @(*)
  • 时序逻辑的 always 块,敏感列表里有时钟边沿(比如 always @(posedge clk)

避坑指南: 我曾经在项目中遇到过一个 bug,一个信号在组合逻辑里被赋值,又在时序逻辑里被赋值,结果综合出来一堆奇怪的 latch。记住:同一个信号,不要在多个 always 块里赋值,也不要在 assign 和 always 里同时赋值。

2.6 知识体系总览

下面这张图,是我自己总结的 Verilog 基础知识点之间的关系。你看一眼,心里就有谱了。

Verilog 基础 模块化设计 高内聚、低耦合 接口标准化 数据类型 wire(连线) reg(变量) 运算符 算术、逻辑、位运算 位宽问题需注意 赋值方式 assign 连续赋值 always 过程赋值 逻辑类型 组合逻辑(无记忆) 时序逻辑(有记忆) 阻塞 vs 非阻塞 = 用于组合逻辑 <= 用于时序逻辑 核心思想:用“画电路图”的思维写代码 并行思维 + 模块化 + 区分组合/时序

嗯,这张图把今天讲的内容串起来了。你仔细看看,每个知识点之间都是有联系的。比如数据类型决定了你用 assign 还是 always,而赋值方式又决定了你写的是组合逻辑还是时序逻辑。

好了,这一章的内容就到这里。记住我今天说的:写 Verilog 不是写软件,是在设计硬件。把这个观念刻在脑子里,后面的路就好走了。


专注资料整理