4. 硬件描述语言基础:Verilog/VHDL 模块化设计

好,咱们开始聊硬件描述语言。说实话,很多刚入行的朋友觉得 Verilog 和 VHDL 就是写代码,跟 C 语言差不多。嗯,这个想法其实挺危险的。

我当年第一次用 Verilog 写了个计数器,仿真跑得飞起,结果综合出来占了大半个芯片。为什么?因为我用 C 语言的思维在写硬件。硬件描述语言,说白了就是「画电路」的另一种方式。你写的每一行代码,最终都会变成实实在在的门电路和触发器。

4.1 模块化设计:搭积木的艺术

模块化设计,我个人习惯叫它「搭积木」。一个复杂的收发器系统,你不可能在一个模块里写完所有逻辑。那会把自己逼疯的。

举个例子,一个简单的 SPI 收发器,我会拆成这样:

  • 顶层模块:负责连接各个子模块
  • 时钟分频模块:产生 SPI 的 SCLK
  • 数据发送模块:处理 MOSI 数据
  • 数据接收模块:处理 MISO 数据
  • 控制状态机模块:管理收发流程

每个模块只做一件事,做好一件事。我在项目中遇到过有人把整个收发器写在一个 always 块里,3000 行代码。调试的时候,他看代码的眼神就像在看天书。

核心原则:一个模块的代码量最好控制在 200 行以内。超过这个数,你就该考虑拆分了。

4.2 数据类型:Verilog 的四值逻辑

Verilog 的数据类型跟 C 语言完全不一样。它用的是四值逻辑:0、1、X(未知)、Z(高阻)。

你想想看,在数字电路里,除了 0 和 1,还有什么时候会出现 X?

  • 寄存器没初始化的时候
  • 两个驱动源同时驱动一根线的时候
  • 时序违规的时候

我曾经在调试一个高速收发器时,发现数据总线上偶尔出现 X。查了两天才发现,是复位信号没处理好,导致某个寄存器在启动时处于不确定状态。

数据类型 说明 默认值
wire 线网类型,用于 assign 赋值 Z(高阻)
reg 寄存器类型,用于 always 块 X(未知)
integer 32 位有符号整数 X
parameter 常量定义 用户指定

注意:reg 类型不一定会综合成寄存器。如果 always 块是组合逻辑,reg 综合出来就是连线。这个坑我踩过好几次。

4.3 运算符:位宽匹配很重要

Verilog 的运算符跟 C 语言很像,但有个关键区别:位宽。

看这个例子:

reg [7:0] a, b;
reg [3:0] c;

assign c = a + b;  // 结果只取低 4 位

我刚开始做设计时,总觉得这样写没问题。直到有一次,一个加法器的结果总是对不上,查了半天才发现是位宽截断导致的。

常用的运算符分类:

  • 算术运算符:+ - * / %
  • 逻辑运算符:&& || !
  • 位运算符:& | ^ ~
  • 关系运算符:> < == !=
  • 移位运算符:<< >>

个人建议:做算术运算时,先把结果位宽算清楚。比如两个 8 位数相加,结果需要 9 位。别问我怎么知道的,问就是吃过亏。

4.4 always/process 块:时序与组合的区分

always 块是 Verilog 的核心。它有两种用法:

组合逻辑:

always @(*) begin
    c = a & b;
end

时序逻辑:

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

这里有个关键点:组合逻辑用阻塞赋值 =,时序逻辑用非阻塞赋值 <=。为什么?

我解释一下:非阻塞赋值在 always 块结束时才更新,这模拟了触发器的行为——数据在时钟沿到来时同时更新。而阻塞赋值是立即生效的,适合组合逻辑。

我曾经见过有人把非阻塞赋值用在组合逻辑里,结果仿真和综合结果完全不一样。嗯,那场面挺尴尬的。

4.5 assign 语句:连续赋值

assign 语句用来描述组合逻辑,它跟 always 块的区别在于:

  • assign 只能赋值给 wire 类型
  • assign 是连续赋值,输入一变,输出立刻变
  • assign 不能描述时序逻辑

举个例子:

wire [7:0] sum;
assign sum = a + b;

这相当于在硬件里画了一个加法器。输入 a 和 b 的任何变化,都会立即反映到 sum 上。

实用技巧:简单的组合逻辑用 assign,复杂的用 always @(*)。这样代码更清晰,也更容易维护。

4.6 知识体系总览

下面这张图,是我自己总结的 Verilog 基础学习路径。你跟着这个顺序走,基本不会迷路。

Verilog 基础学习路径 模块化设计 数据类型 运算符 always/process块 assign语句 综合与仿真 收发器实战 关键区别 assign → wire 类型 always → reg 类型 组合逻辑用 = 时序逻辑用 <= 模块化:200行以内 位宽匹配很重要 避免 X 和 Z 状态 避坑:综合与仿真 结果可能不一致

这张图把今天讲的内容串起来了。你从模块化设计开始,然后掌握数据类型和运算符,接着学会 always 块和 assign 语句,最后把它们综合起来,就能写出真正的硬件了。

好了,这一章的内容就到这。记住,写 Verilog 的时候,脑子里要想着电路,而不是软件。这个思维转变过来了,后面的路就好走了。

可可精品资料库