一、数字逻辑基础:组合逻辑与时序逻辑
说实话,很多初学者一上来就啃Verilog语法,结果写出来的代码要么综合出一堆莫名其妙的Latch,要么时序跑不过。我当年也踩过这个坑。所以咱们先花点时间,把数字逻辑的两种基本形态搞清楚。
1.1 组合逻辑
组合逻辑,说白了就是「输入一变,输出立马变」。没有记忆功能,没有时钟参与。就像你按开关,灯就亮——中间没有任何「等一等」的过程。
我在项目中遇到过最典型的组合逻辑就是地址译码器。输入一个地址,输出对应的片选信号。如果地址变了,片选信号几乎同时跟着变(当然,实际有门延迟)。
组合逻辑的特点:
- 输出只取决于当前输入
- 没有存储单元(没有寄存器)
- 没有时钟边沿触发
- 容易产生毛刺(glitch)
注意:组合逻辑如果没写全所有输入条件,综合工具会给你「补」一个锁存器(Latch)。这玩意儿在FPGA里通常不是你想要的东西。我曾经因为一个case语句没写default,白白浪费了两天调试时间。
1.2 时序逻辑
时序逻辑就不同了。它有个「记忆」功能,靠时钟边沿来更新状态。你想想看,如果整个芯片全是组合逻辑,那数据怎么流水?怎么实现状态机?
时序逻辑的核心是触发器(Flip-Flop)。每个时钟上升沿,它把输入采样进来,然后保持住,直到下一个时钟沿。嗯,这里要注意:建立时间和保持时间这两个概念,是时序逻辑的命根子。
时序逻辑的特点:
- 输出不仅取决于当前输入,还取决于之前的状态
- 有存储单元(寄存器)
- 依赖时钟边沿触发
- 可以消除大部分毛刺
我个人习惯把时序逻辑想象成「拍照」。时钟沿就是快门,按下的一瞬间,把当前输入定格下来。然后这张照片一直显示,直到下一次快门按下。
二、Verilog基本语法速览
Verilog其实不难,核心语法就那么几个。你只要搞懂module、wire、reg、assign、always这五个关键词,就能写出七八成的代码了。
2.1 module:你的设计单元
每个Verilog文件就是一个module。它就像芯片里的一个黑盒子,有输入引脚、输出引脚,里面装着你的逻辑。
module adder (
input [3:0] a, // 4位输入
input [3:0] b, // 4位输入
output [4:0] sum // 5位输出(考虑进位)
);
// 内部逻辑写在这里
endmodule
2.2 wire与reg:连线 vs 变量
这两个初学者最容易搞混。我简单说:
- wire:就是一根导线。它只能被assign赋值,不能存值。
- reg:是一个变量。它可以在always块里被赋值,可以存值。
小技巧:别被名字骗了。reg不一定会综合成寄存器。如果你在组合逻辑的always块里用reg,它综合出来还是wire。只有时序逻辑的always块里的reg,才会变成真正的触发器。
2.3 assign:连续赋值
assign用来描述组合逻辑。它右边的表达式一变,左边立刻更新。
wire [3:0] y;
assign y = a & b; // y = a与b,纯组合逻辑
2.4 always:过程赋值
always块是Verilog的灵魂。它有两种写法:
组合逻辑写法:
always @(*) begin
// 所有输入变化时执行
// 这里用 = 阻塞赋值
end
时序逻辑写法:
always @(posedge clk or negedge rst_n) begin
// 时钟上升沿或复位下降沿触发
// 这里用 <= 非阻塞赋值
end
踩坑提醒:我曾经在同一个always块里混用阻塞赋值和非阻塞赋值,结果仿真和综合结果完全不一样。记住一条铁律:时序逻辑用非阻塞(<=),组合逻辑用阻塞(=)。别问为什么,先记住。
三、实战:加法器与计数器设计
光说不练假把式。咱们直接上手两个经典设计。
3.1 4位加法器
加法器是数字电路的基础。最简单的就是行波进位加法器,每个位的进位像波浪一样传下去。
module ripple_adder_4bit (
input [3:0] a,
input [3:0] b,
input cin, // 进位输入
output [3:0] sum,
output cout // 进位输出
);
wire c1, c2, c3; // 内部进位
// 第0位
assign {c1, sum[0]} = a[0] + b[0] + cin;
// 第1位
assign {c2, sum[1]} = a[1] + b[1] + c1;
// 第2位
assign {c3, sum[2]} = a[2] + b[2] + c2;
// 第3位
assign {cout, sum[3]} = a[3] + b[3] + c3;
endmodule
你看,这就是纯组合逻辑。输入一变,sum和cout立刻跟着变。但实际项目中,我们通常会在加法器后面加一级寄存器,把结果存起来,避免毛刺影响后面的电路。
3.2 二进制计数器
计数器是时序逻辑的经典例子。每个时钟沿,计数值加1。
module counter_8bit (
input clk,
input rst_n, // 低电平复位
output reg [7:0] cnt // 8位计数器
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 8'd0; // 复位清零
else
cnt <= cnt + 1'b1; // 每个时钟加1
end
endmodule
个人经验:计数器溢出是FPGA里最常见的bug之一。比如你计数器最大到255,结果你判断条件写成了cnt == 256,那永远等不到。我建议用 cnt == 8'hFF 这种写法,一眼就能看出最大值。
四、知识体系总览
下面这张图,把本章的核心知识点串起来了。你对照着看,心里就有谱了。
五、本章小结
这一章我们干了三件事:
- 搞清楚了组合逻辑和时序逻辑的区别——一个没记忆,一个有记忆
- 学会了Verilog的五个核心关键词——module、wire、reg、assign、always
- 动手写了加法器和计数器——从理论到实践
嗯,到这里你已经有了FPGA设计的基本概念。下一章我们会深入时序逻辑的细节,讲讲状态机和流水线设计。但今天先消化这些,把加法器和计数器自己动手敲一遍,仿真跑一跑。纸上得来终觉浅,绝知此事要躬行。
课后练习:
- 把4位加法器改成8位,仿真验证进位是否正确
- 给计数器加一个使能信号(en),只有en=1时才计数
- 试试用always块写一个组合逻辑的加法器,看看和assign版本有什么区别
公众号:蓝海资料掘金营,微信deep3321