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 @(*)) |
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)
== 遇到 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 知识体系总览
下面这张图概括了本章的核心内容。你可以把它当作一个「地图」,以后写代码时对照着看。
嗯,这张图把本章的知识点串起来了。你写代码时,心里要时刻想着:我写的这个 assign 对应什么电路?这个 always 块综合出来是寄存器还是组合逻辑?想清楚了,代码就不会跑偏。
可可精品资料库