琼海市网站建设_网站建设公司_SSG_seo优化
2025/12/30 4:18:10 网站建设 项目流程

从零开始:用Verilog在FPGA上实现半加器——新手也能懂的硬件入门实战

你有没有想过,计算机是怎么做加法的?
不是打开计算器点几下,而是从最底层的晶体管和逻辑门出发,靠电流“算”出来的那种。

今天我们就来动手实现一个最简单的加法单元——半加器(Half Adder)。它虽然小,却是所有现代处理器中加法功能的起点。更重要的是,我们将用Verilog HDL把这个电路“写”出来,并部署到真实的 FPGA 芯片上运行。

这不仅是一次编码练习,更是一场从软件思维向硬件设计跃迁的启蒙之旅。


为什么从半加器开始?

初学 FPGA 或数字电路时,很多人一上来就想搞图像处理、跑神经网络。结果呢?卡在第一个时钟信号就动不了了。

其实,真正该做的第一件事是:理解组合逻辑的本质

而半加器,就是通往这个世界的钥匙。

它只做一件简单的事:把两个比特AB相加,输出它们的“和”与“进位”。听起来像小学数学题,但它背后藏着三个关键知识点:

  1. 布尔代数的实际应用(异或、与运算)
  2. 无状态的组合逻辑行为
  3. 模块化设计的基本范式

更重要的是,它的结构清晰、验证简单、结果直观——非常适合点亮你的第一个LED灯前先点亮大脑。


半加器的工作原理:不只是公式

我们来看一组真值表:

ABSumCarry
0000
0110
1010
1101

观察一下:
- 当输入不同(0+1),Sum=1,Carry=0;
- 当输入相同且为1(1+1),Sum=0,Carry=1 —— 这正是二进制进位!

所以我们可以写出两个表达式:

Sum = A ⊕ B Carry = A · B

是不是很简洁?但这不是重点。重点是:这些符号如何变成真实世界里的电路?

答案就是——Verilog。


用 Verilog 描述硬件:数据流建模方式

在 FPGA 设计中,我们不“调用函数”,而是“搭建电路”。下面这段代码就是在“画一张电路图”:

module half_adder ( input wire A, input wire B, output wire Sum, output wire Carry ); assign Sum = A ^ B; // 异或门出“和” assign Carry = A & B; // 与门出“进位” endmodule

关键细节解读:

  • input wire/output wire:声明端口方向和类型。注意这里都用了wire,因为这是纯组合逻辑输出。
  • assign:连续赋值语句,专用于wire类型。只要 A 或 B 变化,Sum 和 Carry 就立即重新计算。
  • 没有always块,没有时钟,也没有寄存器 —— 完全符合组合逻辑特征。

✅ 提示:如果你在这里用了reg并放在always @(*)中赋值,也不是错,但对初学者容易混淆组合/时序逻辑边界。建议从assign开始建立正确直觉。

这段代码经过综合工具(如 Vivado 或 Quartus)处理后,会被映射成 FPGA 内部的 LUT(查找表)资源,物理上等效于一个异或门加一个与门。


更贴近硬件的写法:门级描述

如果你想看得更“透”一点,可以用内置原语直接例化逻辑门:

module half_adder_gate ( input wire A, input wire B, output wire Sum, output wire Carry ); xor(Sum, A, B); // 使用Verilog内置XOR原语 and(Carry, A, B); // 使用AND原语 endmodule

这种写法更像是在搭积木:
-xor(Sum, A, B)表示:“接一个异或门,输入是 A 和 B,输出连到 Sum”
- 工具会直接将其绑定到底层硬件单元

📌适用场景:教学演示、精确控制门延迟分析、或者你想告诉别人“我确实只用了两个门”。

但在实际工程中,推荐使用数据流建模(assign,因为它更简洁、可读性强,且综合效果一样优秀。


如何验证你的设计?别跳过仿真!

写完代码就烧进板子?No no no。硬件开发的第一法则:能仿则仿。

我们需要一个测试平台(Testbench),来自动检查四种输入组合是否全部通过。

`timescale 1ns / 1ps module tb_half_adder; reg A, B; wire Sum, Carry; // 实例化被测模块 half_adder uut ( .A(A), .B(B), .Sum(Sum), .Carry(Carry) ); initial begin $monitor("Time=%0t | A=%b B=%b | Sum=%b Carry=%b", $time, A, B, Sum, Carry); // 测试所有输入组合 A = 0; B = 0; #10; A = 0; B = 1; #10; A = 1; B = 0; #10; A = 1; B = 1; #10; $finish; end endmodule

说说这个 Testbench 的精妙之处:

  • $monitor:每当时序推进,自动打印当前状态。比手动插入$display更省事。
  • #10:表示等待10个时间单位(由timescale定义为1ns),模拟信号切换间隔。
  • 最终输出应如下:
Time=0 | A=0 B=0 | Sum=0 Carry=0 Time=10 | A=0 B=1 | Sum=1 Carry=0 Time=20 | A=1 B=0 | Sum=1 Carry=0 Time=30 | A=1 B=1 | Sum=0 Carry=1

如果看到最后一行 Sum=0、Carry=1,恭喜你!你的半加器已经通过了功能验证。

🔍 小技巧:很多初学者漏掉某个输入组合,导致 Carry 错误未被发现。一定要遍历全部可能输入!


常见坑点与避坑指南

我在带学生做这个实验时,发现以下几个问题反复出现:

❌ 误区一:给组合逻辑输出声明为reg,却不放在always块里

比如这样写:

output reg Sum; // 错误!声明为reg却没有过程赋值

但后面还是用assign赋值 → 综合报错或产生意外锁存器。

✅ 正确做法:
- 输出是组合逻辑 → 用wire
- 若必须用reg(例如在always中赋值),则要配合always @(*)

❌ 误区二:误加时钟,把组合逻辑写成时序逻辑

有人习惯性加上clkalways @(posedge clk),结果输出延迟一个周期。

记住一句话:半加器没有记忆,不需要时钟

一旦加了时钟,你就不再是“即时响应”的加法器,而是一个带延迟的状态机了。

❌ 误区三:忽略端口连接顺序,例化时出错

尤其是在使用门级原语时:

xor(Sum, A, B); // 正确:第一个是输出,后两个是输入

但如果写反了:xor(A, Sum, B);—— 那就完全乱套了。


在真实FPGA上跑起来:下一步做什么?

当你完成仿真并确认逻辑正确后,就可以进入物理实现阶段了:

  1. 创建工程,添加half_adder.v和约束文件(.xdc 或 .qsf)
  2. 分配引脚:比如让 A 接按键 SW0,B 接 SW1,Sum 和 Carry 接 LED0、LED1
  3. 综合 → 实现 → 生成比特流
  4. 下载到开发板

然后动手试试:
- 按下不同组合的开关,观察LED亮灭情况
- 当两个输入都为高时,Carry 对应的 LED 应该亮起

那一刻你会感受到一种独特的成就感:我写的代码,真的在“通电运算”


从半加器出发,你能走多远?

别看它简单,全加器(Full Adder)其实就是两个半加器拼起来再加一个或门

你可以尝试扩展:
- 把两个 half_adder 组合成 full_adder
- 级联四个 full_adder 构成 4 位加法器
- 加入进位链优化,挑战超前进位加法器(Carry-Lookahead Adder)

每一步都在复刻真实CPU内部的ALU演化路径。

甚至在未来,你可以基于这套思想去实现:
- 快速乘法器
- CRC校验生成器
- 浮点运算单元的定点部分


写在最后:硬件思维的觉醒

写完这篇,我想强调一点:

学FPGA不是学会写Verilog语法就够了,而是要学会用硬件的方式思考问题

当你看到A + B,不再想到“调用add函数”,而是想到“一组并行传播的逻辑门网络”,你就入门了。

而这一切,可以从一个只有两行赋值语句的半加器开始。


💡动手建议清单
- [ ] 编写 half_adder 模块
- [ ] 搭建 testbench 并运行仿真
- [ ] 观察波形图(可用 GTKWave 或 Vivado Simulator)
- [ ] 下载到开发板验证
- [ ] 尝试改写为门级结构
- [ ] 用两个半加器构建全加器(进阶挑战)

如果你正在学习数字系统设计,不妨把今天的代码保存下来。未来某天回看,你会感谢那个认真对待“最简单电路”的自己。

欢迎在评论区分享你的实现截图或遇到的问题,我们一起调试、一起成长。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询