2. Verilog基础与组合逻辑:模块结构、数据类型、assign语句、always块、组合逻辑电路设计
各位同学好,我是你们的FPGA讲师。今天咱们来聊聊Verilog的基础,特别是组合逻辑这块。说实话,很多初学者一上来就被各种语法吓住了,其实没那么复杂。你想想看,Verilog说白了就是描述硬件的一种语言,跟C语言写软件完全是两码事。
我刚开始学的时候也犯过迷糊——总觉得代码写出来就能跑,结果综合出来一堆莫名其妙的锁存器。嗯,今天咱们就把这些坑一个个填上。
2.1 模块结构——Verilog的基本单元
每个Verilog设计,都是从模块开始的。模块就像是一个黑盒子,有输入、有输出,内部实现具体的功能。我在项目中习惯把每个功能块都封装成一个模块,这样后期调试和复用都方便。
一个典型的模块结构长这样:
module adder (
input [3:0] a, // 4位输入
input [3:0] b, // 4位输入
output [4:0] sum // 5位输出(考虑进位)
);
// 功能描述
assign sum = a + b;
endmodule
注意看,module和endmodule是成对出现的。端口声明里,input、output、inout三种类型要分清。我个人习惯把位宽写在端口声明里,这样一眼就能看出数据宽度。
2.2 数据类型——wire和reg的区别
这是新手最容易搞混的地方。我简单说:
- wire:线网类型,用于组合逻辑。说白了就是一根导线,连续赋值。
- reg:寄存器类型,用于时序逻辑。但注意,在always块里赋值必须用reg。
等等,这里有个坑——reg不一定是寄存器!在组合逻辑的always块里,reg只是语法要求,综合出来还是组合电路。我曾经被这个坑过,以为always块里赋值就一定是触发器,结果综合出来一堆组合逻辑。
看个例子:
wire [3:0] data_in; // 组合逻辑信号
reg [3:0] data_out; // 可以是组合,也可以是时序
// 组合逻辑的always块
always @(*) begin
data_out = data_in; // 这里data_out虽然是reg,但综合出来是wire
end
2.3 assign语句——连续赋值
assign是组合逻辑最常用的语句。它表示连续驱动,只要右边表达式变化,左边立即更新。我习惯用assign实现简单的逻辑运算、数据拼接、三态门等。
举个例子:
assign sum = a + b; // 加法器
assign sel_out = sel ? a : b; // 二选一多路器
assign {carry, result} = a + b + cin; // 带进位的加法
这里要注意,assign的左边必须是wire类型。如果你用reg,综合器会报错。嗯,我刚开始也犯过这个错。
2.4 always块——过程赋值
always块是Verilog的核心。它有两种主要用法:
- 组合逻辑:
always @(*),敏感列表用星号,表示所有输入信号变化都触发。 - 时序逻辑:
always @(posedge clk),只在时钟上升沿触发。
我个人建议:组合逻辑用阻塞赋值=,时序逻辑用非阻塞赋值<=。这个规则一定要遵守,否则仿真和综合结果可能不一致。
看个组合逻辑的例子:
always @(*) begin
if (sel)
y = a;
else
y = b;
end
这个代码综合出来就是一个二选一多路器。注意,if-else必须写全,否则会生成锁存器。我曾经在项目里偷懒,只写了if没写else,结果综合报告里多了一堆警告。
2.5 组合逻辑电路设计实战
好了,理论讲完了,咱们来点实际的。我挑两个最常用的组合逻辑电路:加法器和多路选择器。
2.5.1 加法器
加法器是数字电路的基础。最简单的就是直接用+运算符,但要注意位宽扩展。
module adder_4bit (
input [3:0] a,
input [3:0] b,
input cin, // 进位输入
output [3:0] sum, // 和
output cout // 进位输出
);
assign {cout, sum} = a + b + cin;
endmodule
这里用花括号{}做位拼接,把进位和结果拼在一起。我在实际项目中,经常用这种写法,简洁又高效。
如果追求高性能,可以用进位选择加法器或超前进位加法器。但初学者先掌握这个基础版本就够了。
2.5.2 多路选择器
多路选择器(MUX)也是常用电路。用assign或always都可以实现。
用assign实现:
module mux_4to1 (
input [3:0] data0, data1, data2, data3,
input [1:0] sel,
output [3:0] y
);
assign y = (sel == 2'b00) ? data0 :
(sel == 2'b01) ? data1 :
(sel == 2'b10) ? data2 :
data3;
endmodule
用always实现:
always @(*) begin
case (sel)
2'b00: y = data0;
2'b01: y = data1;
2'b10: y = data2;
2'b11: y = data3;
default: y = 4'b0; // 一定要有default!
endcase
end
- 组合逻辑中,case语句必须写全所有分支,或者加default。
- if-else必须成对出现,否则生成锁存器。
- assign和always @(*)都可以实现组合逻辑,看个人习惯。
2.6 知识体系总览
下面这张图是我自己画的,把本章的知识点串起来了。你看一眼就能明白各个概念之间的关系。
这张图里,组合逻辑设计是核心,四个分支分别是模块结构、数据类型、assign语句和always块。最下面是两个实战电路——加法器和多路选择器。你看,整个知识体系其实很清晰。
2.7 避坑指南
最后,我总结几个常见坑,都是我在项目里踩过的:
- 锁存器推断:组合逻辑中,if没有else,case没有default,都会生成锁存器。解决办法:写全所有分支。
- 位宽不匹配:加法运算时,结果位宽要足够。比如两个4位数相加,结果要用5位来存,否则进位会丢失。
- 阻塞与非阻塞混用:同一个always块里,不要同时用=和<=。我见过有人这么写,仿真结果完全不对。
- 敏感列表不全:如果用always @(a, b)但漏了c,综合结果可能和仿真不一致。建议直接用@(*)。
好了,这一章的内容就到这里。记住,组合逻辑是FPGA设计的基础,多写多练自然就熟了。下一章咱们会讲时序逻辑,到时候再聊触发器和状态机。
可可精品资料库