3. 硬件描述语言基础
各位同学,今天我们来啃一块硬骨头——硬件描述语言的基础。说实话,很多初学者觉得Verilog或者VHDL就是写代码,跟C语言差不多。嗯,这个想法很危险。我见过太多人把FPGA当单片机写,结果综合出来的电路完全不是那么回事。
硬件描述语言,说白了就是「用文本画电路图」。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。这个思维转变,是入门的第一道坎。
3.1 模块化设计——把大电路拆成小积木
我在项目中带团队时,最怕看到的就是一个模块写了上千行。调试起来简直噩梦。模块化设计,就是让你像搭积木一样构建系统。
一个典型的Verilog模块长这样:
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
每个模块都有明确的输入输出接口。内部怎么实现,外部不用管。这就是封装的思想。我个人习惯把功能相关的信号放在同一个模块里,比如一个UART模块,就把波特率发生器、接收器、发送器全包进去。
3.2 数据类型——wire和reg,别搞混了
Verilog里最基础的数据类型就两个:wire和reg。但很多新手在这里栽跟头。
wire——说白了就是一根导线。它不存储值,只是把驱动端的值传递出去。assign语句赋值的目标必须是wire。
reg——寄存器?不一定。在always块里赋值的变量,必须声明为reg。但综合出来可能是寄存器,也可能是组合逻辑。这个要看你怎么写。
举个例子:
wire a, b, c;
assign c = a & b; // c是wire,组合逻辑
reg d, e;
always @(*) begin
d = a | b; // d是reg,但综合成组合逻辑
end
always @(posedge clk) begin
e <= a & b; // e是reg,综合成触发器
end
我曾经在项目中遇到一个同事,把所有信号都定义成reg。结果综合报告里一堆警告,说某些reg被优化掉了。为什么?因为那些信号实际上只是连线,不需要存储。
| 类型 | 赋值方式 | 综合结果 | 典型用途 |
|---|---|---|---|
| wire | assign | 组合逻辑 | 模块间连线、组合逻辑输出 |
| reg | always块内赋值 | 组合逻辑或时序逻辑 | 状态机、计数器、数据通路 |
3.3 运算符——不只是加减乘除
Verilog的运算符跟C语言很像,但有些细节完全不同。你想想看,C语言里a = b + c,就是算个数。但在Verilog里,这代表一个加法器硬件。
常用的运算符:
- 算术运算符:+、-、*、/、%。注意,乘除法综合出来很耗资源。我一般用移位代替乘除,除非精度要求很高。
- 逻辑运算符:&&、||、!。用于条件判断,结果只有0或1。
- 位运算符:&、|、^、~。按位操作,这个在硬件里用得最多。
- 关系运算符:>、<、==、!=。比较大小,综合出来是比较器。
- 移位运算符:<<、>>。左移一位相当于乘2,右移一位相当于除2。硬件实现几乎不花资源。
3.4 assign与always块——两种描述方式
assign和always,是Verilog里最重要的两种描述方式。它们各有各的用途。
assign——连续赋值。只要右边表达式有变化,左边立即更新。适合描述简单的组合逻辑。
assign sum = a + b;
assign flag = (data == 8'hFF) ? 1'b1 : 1'b0;
always——过程赋值。可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。
// 组合逻辑:敏感列表用 @(*)
always @(*) begin
if (sel)
out = a;
else
out = b;
end
// 时序逻辑:敏感列表用 @(posedge clk)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里有个坑:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。为什么?因为非阻塞赋值能避免竞争冒险。我刚开始学的时候也不理解,直到有一次仿真结果跟实际电路对不上,查了两天才发现是赋值方式用错了。
3.5 组合逻辑与时序逻辑——本质区别
这两个概念,是数字电路的核心。搞清楚了,FPGA设计就入门了。
组合逻辑:输出只取决于当前输入。没有记忆功能。比如与门、或门、加法器、多路选择器。
时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。有记忆功能。比如触发器、计数器、状态机。
用一张图来理解:
在实际项目中,组合逻辑和时序逻辑往往是混在一起的。比如一个计数器:
module counter_example (
input wire clk,
input wire rst_n,
input wire inc,
output reg [3:0] count,
output wire overflow
);
// 时序逻辑:计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'd0;
else if (inc)
count <= count + 1'b1;
end
// 组合逻辑:溢出判断
assign overflow = (count == 4'd15) & inc;
endmodule
你看,count是时序逻辑,由时钟驱动。overflow是组合逻辑,由count和inc实时计算。这就是典型的混合设计。
核心要点:
- 组合逻辑用assign或always @(*),用阻塞赋值(=)
- 时序逻辑用always @(posedge clk),用非阻塞赋值(<=)
- 不要把组合逻辑和时序逻辑混在一个always块里
- 复位信号要放在敏感列表里,异步复位
嗯,到这里,硬件描述语言的基础就讲完了。这些东西看起来简单,但真正用好需要大量实践。我建议你每学一个知识点,就写一个小模块验证一下。比如写个4位加法器,再写个8位计数器,仿真看看波形对不对。
记住,FPGA设计不是写软件,是在画电路。你的每一行代码,最终都会变成硅片上的晶体管。带着这个思维去学习,进步会快很多。
可可精品资料库