1. 从零开始:为什么加法器是数字世界的基石
如果你刚开始接触数字逻辑或者FPGA,可能会觉得加法器这东西太基础了,不就是个“1+1=2”的电路吗?我刚开始学的时候也是这么想的,觉得直接跳过算了。但后来在项目里踩过几次坑才明白,加法器远不止是计算工具,它其实是理解整个数字系统设计思想的一把“万能钥匙”。从最底层的晶体管开关,到我们手机上运行的复杂应用,几乎所有的运算最终都会分解成无数个加法操作。CPU里的算术逻辑单元(ALU)、图形处理器(GPU)的像素计算,甚至加密解密算法,其核心都离不开高效、可靠的加法器。
所以,咱们这次的学习之旅,就从一个看似简单的全加器开始。我会带你像玩“头歌实训”那样的闯关游戏一样,从最原始、最“笨”的门级描述,一步步升级到优雅、强大的参数化模块设计。这个过程不仅仅是学会写几行Verilog代码,更重要的是让你建立起“硬件描述语言”的思维方式——我们不是在写软件程序,而是在用代码“画”出电路的结构。我见过很多新手,一上来就用最高级的行为描述,代码是简洁了,但综合出来的电路面积大、速度慢,自己还一头雾水。咱们反其道而行之,从底层开始,把每一根“线”和每一个“门”都搞清楚,这样以后遇到复杂设计,你才能心里有底,知道编译器背后到底干了啥。
咱们的目标很明确:通过设计一个加法器,把Verilog里最核心、最常用的语法点全部串起来。等你跟着走完这一路,再回头看其他组合逻辑电路(比如比较器、编码器)甚至时序逻辑电路(比如计数器、状态机),你会发现思路都是相通的。好了,废话不多说,咱们这就进入第一关,从最基础的“搭积木”开始。
2. 第一关:门级结构描述——像搭乐高一样构建电路
2.1 理解全加器的“五脏六腑”
在动手写代码之前,咱们得先搞清楚全加器到底是个啥。你可以把它想象成一个处理“三位数”相加的小机器。这三位数分别是:加数A、加数B,以及从低位传上来的进位Cin。它要输出两个结果:本位和Sum,以及向高位产生的进位Cout。
它的工作原理,用逻辑表达式写出来就是两行:
- Sum = A ⊕ B ⊕ Cin (三个输入做异或)
- Cout = (A & B) | (A & Cin) | (B & Cin) (任意两个输入同时为1,就产生进位)
光看公式可能有点抽象,咱们来看它的门级电路图。它本质上就是由两个异或门(XOR)、三个与门(AND)和一个或门(OR) 巧妙地连接而成的。我刚开始学的时候,喜欢用笔画一画信号的流向:A和B先进入第一个异或门,出来的结果再和Cin进入第二个异或门,最终得到Sum。同时,A和B、A和Cin、B和Cin分别进入三个与门,这三个与门的输出再进入一个三输入的或门,最终得到Cout。这个过程就像水管工在布置管道,确保数据流能正确地从入口流向出口。
2.2 门级实例化:Verilog的“基础砖块”
理解了结构,我们就可以用Verilog最底层的描述方式——门级原始结构(Gate-Level Primitive)来搭建它。这种方式非常直观,就是直接调用Verilog语言内置的基本逻辑门单元,相当于直接使用设计好的“标准零件”。语法长这样:gate_type instance_name (output, input1, input2, ...);
举个例子,一个两输入的与门这么写:and U1 (out, in1, in2);。这里的 and 是门类型,U1是你给这个门起的实例名(方便调试和定位),括号里第一个信号是输出,后面的是输入。
现在,咱们来把电路图“翻译”成代码。我们需要声明所有的输入输出端口,然后定义几条内部的连线(wire)来把各个门连接起来。最后,就是按图索骥,实例化每一个逻辑门。
module full_adder_gate (
input A,
input B,
input Cin,
output Sum,
output Cout
);
// 声明内部连线,就像给电路中的电线贴标签
wire w1, w2, w3, w4;
// 第一个异或门:计算 A xor B
xor U1 (w1, A, B);
// 第二个异或门:计算 w1 xor Cin,得到最终的和 Sum
xor U2 (Sum, w1, Cin);
// 三个与门,分别计算产生进位的三种情况
and U3 (w2, A, B);
and U4 (w3, A, Cin);
and U5 (w4, B, Cin);
// 一个或门,汇总三种进位情况,得到最终的进位 Cout
or U6 (Cout, w2, w3, w4);
endmodule
写完这段代码,你可以自豪地说:“我亲手用最基础的逻辑门搭出了一个全加器!” 虽然代码看起来有点啰嗦,但它的好处是完全可控。综合工具(比如Vivado、Quartus)几乎会原封不动地按照你的描述生成电路,和你画的电路图一一对应。这对于理解硬件底层和做某些极致优化特别有用。当然,日常开发我们不会这么写,但这一步是建立硬件思维不可逾越的基石,我强烈建议你亲手敲一遍,感受一下“连线”的过程。
3. 第二关:连续赋值语句——用方程式描述电路
3.1 从“连线”到“赋值”
用门级描述就像用汇编语言编程,虽然控制力强,但效率太低。对于全加器这种逻辑关系明确的电路,Verilog提供了更高效的描述方式:连续赋值(Continuous Assignment),使用 assign 关键字。
你可以把 assign 语句理解为一个永久的、自动更新的连接关系。等式右边(RHS)信号有任何风吹草动,等式左边(LHS)的线网(wire)信号会立刻跟着变化,这完美模拟了组合逻辑电路的行为。它不再是实例化一个个具体的门,而是直接写出输出和输入之间的逻辑表达式,让综合工具自己去决定用什么门电路来实现。
3.2 用assign重构全加器
现在,我们用 assign 来重新描述全加器。根据之前的逻辑表达式,代码变得异常简洁:
module full_adder_assign (
input A,
input B,
input Cin,
output Sum,
output Cout
);
// 连续赋值:和 Sum 等于三个输入的异或
assign Sum = A ^ B ^ Cin; // ‘^’ 是Verilog中的异或运算符
// 连续赋值:进位 Cout 等于三种情况的“或”
assign Cout = (A & B) | (A & Cin) | (B & Cin); // ‘&’是与,‘|’是或
endmodule
看,代码量瞬间少了一大半!这就是抽象带来的力量。assign 语句是描述组合逻辑最常用、最推荐的方式之一。它比门级描述更易读、更易维护,同时综合工具也能很好地对其进行优化。在实际项目中,绝大多数简单的组合逻辑(比如多路选择器、译码器)我都会用 assign 来实现。
这里有个小坑我当年踩过:assign 只能驱动 wire 类型的变量,不能驱动 reg 类型。好在模块的端口如果声明为 output,默认就是 wire 型,所以直接使用没问题。如果你想在 always 块里对变量赋值,那就得声明为 reg 型,这是后话了。
4. 第三关:行为级描述——像写软件一样设计硬件
4.1 拥抱抽象:行为描述的魅力
如果觉得写逻辑表达式还不够“高级”,别急,Verilog还有更抽象的描述层次:行为级描述(Behavioral Description)。我们可以使用 always 块和丰富的运算符(如算术运算符 +、- 等),像写软件算法一样来描述电路功能。
对于加法器,我们可以直接使用 + 这个算术运算符。综合工具看到这个符号,会自动调用其内置的优化过的加法器单元来生成电路,这通常比我们自己写的门级或赋值级电路更高效、更节省面积。
4.2 位拼接与算术运算的妙用
在这一关,我们还会用到一个非常实用的运算符:位拼接运算符 {}。它可以把多个信号按顺序拼接成一个更宽的向量。对于全加器,我们可以把 Cout 和 Sum 拼成一个2位的向量 {Cout, Sum},而这个向量的值,正好等于三个输入(A, B, Cin)的算术和(0, 1, 2或3)。看代码就一目了然了:
module full_adder_behavioral (
input A,
input B,
input Cin,
output Sum,
output Cout
);
// 定义一个2位的线网来接收拼接后的结果
wire [1:0] result; // result[1]是高位,result[0]是低位
// 核心代码:直接将三个输入相加,结果赋值给2位向量
assign result = A + B + Cin; // 综合工具会识别这是一个加法器
// 将结果的低位赋值给Sum,高位赋值给Cout
assign Sum = result[0];
assign Cout = result[1];
endmodule
甚至,我们可以写得更简洁,直接在一行里完成:
assign {Cout, Sum} = A + B + Cin;
这行代码是Verilog描述全加器最优雅的形式,没有之一。它清晰无误地表达了“输出是输入之和”这个核心功能。行为级描述的优势在于意图明确,开发效率高。当你设计一个32位加法器时,难道要写32个全加器实例吗?当然不,直接用 assign sum = a + b + cin; 就行了。综合工具背后的优化算法非常强大,会为你生成一个在速度、面积、功耗之间取得平衡的电路。
注意:行为级描述虽然方便,但也把具体电路结构的决定权完全交给了综合工具。有时为了满足严格的时序或面积要求,我们可能还需要回到底层描述进行手动优化。但对于90%的应用场景,行为级描述已经完全够用且是首选。
5. 第四关:模块实例化——构建4位行波进位加法器
5.1 理解模块化:从零件到系统
学会了制造一个“全加器”零件,接下来我们就要用这个零件来搭建更复杂的机器了。这就是模块实例化(Module Instantiation),它是数字系统设计的核心思想:自底向上,层层抽象。
我们要构建一个4位二进制加法器,它可以计算像 1101 + 0110 这样的加法。最直接的方法就是将4个全加器串联起来,低位的进位输出连接到高位的进位输入。这种结构叫做行波进位加法器(Ripple Carry Adder, RCA)。就像水波一样,进位信号从最低位开始,一位一位地向上“涟漪”传递。
5.2 动手实例化:连接四个全加器
首先,我们需要有一个设计好的全加器模块文件,比如叫 full_adder.v。然后,在一个新的顶层模块中,我们通过实例化它四次来构建4位加法器。
这里的关键点是端口映射(Port Mapping)。有两种方式:按顺序连接和按名称连接。新手我强烈建议使用按名称连接,虽然代码长一点,但一目了然,极难出错。
// 假设我们已经有一个名为 full_adder 的模块(用任意一种方式实现均可)
`include "full_adder.v" // 包含模块定义文件
module adder_4bit_ripple (
input [3:0] A, // 4位加数A
input [3:0] B, // 4位加数B
input Cin, // 来自外部的进位输入(通常为0)
output [3:0] Sum, // 4位和
output Cout // 最终的进位输出
);
// 声明内部进位线,用于连接各级全加器
wire [3:0] carry; // carry[0], carry[1], carry[2], carry[3]
// 实例化第一个全加器(最低位),使用按名称连接,清晰无比
full_adder FA0 (
.A (A[0]), // 连接到输入A的第0位
.B (B[0]), // 连接到输入B的第0位
.Cin (Cin), // 连接到外部进位输入
.Sum (Sum[0]), // 输出和的最低位置于Sum[0]
.Cout (carry[0]) // 产生的进位输出给下一位
);
// 实例化第二个全加器
full_adder FA1 (
.A (A[1]),
.B (B[1]),
.Cin (carry[0]), // 进位来自前一位FA0
.Sum (Sum[1]),
.Cout (carry[1])
);
// 实例化第三个全加器
full_adder FA2 (
.A (A[2]),
.B (B[2]),
.Cin (carry[1]),
.Sum (Sum[2]),
.Cout (carry[2])
);
// 实例化第四个全加器(最高位)
full_adder FA3 (
.A (A[3]),
.B (B[3]),
.Cin (carry[2]),
.Sum (Sum[3]),
.Cout (Cout) // 最终的进位输出给模块外部
);
endmodule
通过这个例子,你应该深刻体会到模块化的威力。我们把一个复杂问题(4位加法)分解成了4个相同的简单问题(1位加法)。每个全加器模块就像是一个封装好的黑盒子,我们只需要关心它的接口(端口),而不需要每次都去重复实现内部的逻辑。这是管理大型数字系统(比如一个完整的CPU)的唯一可行方法。
6. 第五关:参数化设计——打造通用的n位加法器
6.1 告别重复劳动:引入parameter
如果你需要8位、16位、32位的加法器,难道要像上面那样写8个、16个、32个实例化语句吗?那太可怕了。Verilog提供了 parameter 关键字,允许我们定义模块参数,从而实现参数化设计。
我们可以创建一个位宽可配置的加法器,在实例化时再决定它到底是几位。这极大地提高了代码的复用性和可维护性。
6.2 实现参数化n位加法器
我们来设计一个支持有符号数加法,并能进行溢出检测的n位加法器。这里我们采用行为级描述,直接使用算术运算符 +。
module adder_n_bit #(
parameter N = 16 // 默认位宽为16位,实例化时可修改
)(
input [N-1:0] A, // N位有符号加数A
input [N-1:0] B, // N位有符号加数B
input Cin, // 进位输入
output [N-1:0] Sum, // N位有符号和
output Cout, // 进位输出
output Overflow // 溢出标志
);
// 为了计算进位和溢出,我们需要一个N+1位的中间结果
wire [N:0] temp_result; // 多出一位用来存放最终的进位
// 核心算术运算:将A, B和Cin相加,结果存入N+1位的temp_result
assign temp_result = {A[N-1], A} + {B[N-1], B} + Cin; // 这里做了符号位扩展,确保加法正确
// 另一种更常见的写法是:assign temp_result = A + B + Cin;
// 输出赋值:和的低N位给Sum,最高位给Cout
assign Sum = temp_result[N-1:0];
assign Cout = temp_result[N];
// 溢出检测:方法二,判断最高位的进位Cn和次高位的进位Cn-1是否不同
// 对于有符号数,这是最常用的溢出判断方法之一
assign Overflow = temp_result[N] ^ temp_result[N-1]; // ‘^’ 异或,不同则为1表示溢出
endmodule
6.3 实例化与参数重载
现在,我们可以轻松创建任意位宽的加法器了:
// 实例化一个默认的16位加法器
adder_n_bit my_adder_16bit (
.A (a_bus),
.B (b_bus),
.Cin(1‘b0),
.Sum(sum_bus),
.Cout(carry_out),
.Overflow(ovf_flag)
);
// 实例化一个8位加法器,使用‘defparam’或在实例化时直接重载参数
adder_n_bit #(.N(8)) my_adder_8bit ( // 这是推荐的“命名参数重载”方式
.A (a_8bit),
.B (b_8bit),
.Cin(1‘b0),
.Sum(sum_8bit),
.Cout(),
.Overflow()
);
参数化设计是专业FPGA/ASIC工程师的必备技能。它让你的代码从一个只能解决特定问题的“脚本”,升级为一个可以灵活配置的“工具库”。在大型项目中,所有通用模块(如FIFO、RAM控制器、串口收发器)都应该是参数化的。
7. 第六关:应用拓展——8421 BCD码加法器设计
7.1 跳出二进制:十进制运算的需求
我们之前设计的都是二进制加法器,但人类更习惯十进制。在数字系统中,常用8421 BCD码来表示十进制数(用4位二进制表示0-9)。那么,如何用我们已有的二进制加法器来实现十进制加法呢?
问题来了:4位二进制能表示0-15,但BCD码只用到0-9(0000-1001)。如果两个BCD码相加的结果在10-15之间(1010-1111),或者产生了进位,这个结果对于十进制来说就是无效的。例如,9 (1001) + 3 (0011) = 12 (1100),1100在BCD码里没有定义。
7.2 修正算法:加6修正法
解决办法是“加6修正”。规则很简单:
- 先用一个普通的4位二进制加法器对两个BCD码进行加法,得到一个4位的中间结果
Z和一个进位C。 - 判断修正条件:如果
Z > 9或者C == 1(即原始加法结果大于等于10),那么就需要修正。 - 修正方法:在中间结果
Z上再加上6 (0110)。这个修正操作可能会产生新的进位,需要加到原来的进位上。
7.3 Verilog实现BCD加法器
我们可以用两个4位二进制加法器(可以用我们之前设计的 adder_4bit_ripple)和一个判断逻辑来实现。
module bcd_adder_1digit (
input [3:0] A_bcd, // 第一个BCD码输入
input [3:0] B_bcd, // 第二个BCD码输入
input Cin, // 来自低十进制位的进位
output [3:0] S_bcd, // 修正后的BCD码和
output Cout // 向高十进制位的进位
);
wire [3:0] Z; // 二进制加法中间结果
wire carry_bin; // 二进制加法的进位
wire need_correction; // 是否需要修正的标志
wire [3:0] corrected_sum; // 修正后的和
wire carry_correction; // 修正产生的进位
// 第一个加法器:执行二进制加法
adder_4bit_ripple bin_adder (
.A (A_bcd),
.B (B_bcd),
.Cin (Cin),
.Sum (Z),
.Cout(carry_bin)
);
// 判断是否需要修正:Z > 9 或者 有进位
assign need_correction = (Z > 4‘d9) | carry_bin;
// 第二个加法器:执行加6修正(如果需要)
// 如果不需要修正,就加0
adder_4bit_ripple correction_adder (
.A (Z),
.B (need_correction ? 4‘d6 : 4‘d0), // 条件运算符:需要修正则加6,否则加0
.Cin (1‘b0),
.Sum (corrected_sum),
.Cout(carry_correction)
);
// 最终输出:和为修正后的结果,进位为两个加法器进位的“或”
assign S_bcd = corrected_sum;
assign Cout = carry_bin | carry_correction;
endmodule
这个例子完美展示了如何用已有的基础模块(二进制加法器)和简单的组合逻辑,构建出功能更特殊的模块(BCD加法器)。它体现了数字系统设计的另一个重要思想:通过组合和修正来构建新功能。你可以用多个这样的1位BCD加法器,级联成一个多位十进制加法器,甚至可以设计减法、乘法等更复杂的运算单元。
走完这六关,我希望你收获的不仅仅是六个Verilog模块,更是一套从微观到宏观、从具体到抽象的数字电路设计方法论。下次当你面临一个新的数字功能需求时,不妨也试试这条“从门级到模块化”的思考路径:先理解底层逻辑,再用高效的方式描述,最后用模块化的方式搭建和扩展。这条路我走了十年,至今依然受用。

7462

被折叠的 条评论
为什么被折叠?



