从零开始:用Verilog在FPGA上“造”一个同或门
你有没有想过,计算机是怎么判断两个数据是否相等的?
别急着说“这还不简单”,其实背后藏着最基础、也最关键的数字逻辑单元之一——同或门(XNOR Gate)。它就像电路世界的“裁判员”,专门负责回答一个问题:“这两个信号,一模一样吗?”
今天,我们就来亲手用Verilog在FPGA上实现一个同或门。这不是简单的代码抄写,而是一次完整的硬件设计实战:从真值表出发,到代码编写、仿真验证,再到实际应用场景拓展。哪怕你是第一次接触HDL,也能一步步走完全程。
同或门到底是什么?别被名字吓到
先别管那些复杂的术语,“同或”其实就是“相同才为真”。它的行为非常直观:
| A | B | 输出 Y |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
看出规律了吗?只要两个输入一样,输出就是1;不一样就输出0。换句话说,它是异或门(XOR)的反相结果:
$$
Y = A \odot B = \overline{A \oplus B}
$$
也可以展开成与或表达式:
$$
Y = AB + \bar{A}\bar{B}
$$
这意味着我们可以用“与门+非门+或门”搭出来。但在FPGA里,这一切都由查找表(LUT)自动搞定——你只需要告诉综合器“我要什么功能”,剩下的交给工具链。
为什么要在FPGA上做这个?
因为FPGA不是单片机,它不跑程序,而是“变成电路”。你在Verilog里写的每一行逻辑,最终都会映射成实实在在的硬件资源。而像XNOR这样的基础门,在现代FPGA中通常只占用1个LUT6(比如Xilinx Artix-7系列),几乎不占地方,却能并行执行成千上万个。
更重要的是,掌握这种最小粒度的设计思维,是你构建复杂系统的第一步。
Verilog实现:三种写法,哪种最好?
我们先来看最核心的功能模块代码:
module xnor_gate ( input wire A, input wire B, output wire Y ); assign Y = A ~^ B; endmodule就这么四行?对,就这么简单。
关键点解析:
wire是线网类型,用于组合逻辑连接;assign是连续赋值语句,适用于纯组合逻辑;~^是Verilog内置的按位同或操作符,直接对应XNOR逻辑。
✅ 推荐使用
A ~^ B,语义清晰,可读性强,综合效果最优。
但如果你看到别人写成这样呢?
assign Y = ~(A ^ B); // XOR后取反或者更“教科书式”的结构化写法:
assign Y = (A & B) | (~A & ~B);这三种写法有什么区别?
| 写法 | 特点 | 建议场景 |
|---|---|---|
A ~^ B | 最简洁,意图明确 | 日常开发首选 |
~(A ^ B) | 逻辑等价,稍绕一步 | 兼容老标准或教学演示 |
(A&B)\|(~A&~B) | 完全还原布尔公式 | 初学者理解原理时使用 |
📌重点提醒:虽然写法不同,但在综合阶段,现代EDA工具(如Vivado、Quartus)会自动优化为相同的底层LUT配置。所以优先选择可读性高的写法,而不是“看起来更底层”的结构化描述。
怎么证明你的电路是对的?靠仿真!
写完代码只是第一步,关键是要验证功能正确。这就需要一个测试平台(Testbench)。
下面是完整的testbench代码:
`timescale 1ns / 1ps module tb_xnor; reg A, B; wire Y; // 实例化被测模块 xnor_gate uut ( .A(A), .B(B), .Y(Y) ); initial begin $dumpfile("xnor_wave.vcd"); $dumpvars(0, tb_xnor); // 施加所有输入组合 A = 0; B = 0; #10; A = 0; B = 1; #10; A = 1; B = 0; #10; A = 1; B = 1; #10; $finish; end // 控制台实时监控 initial begin $monitor("Time=%0t | A=%b B=%b | Y=%b", $time, A, B, Y); end endmodule运行后你会看到输出:
Time= 0 | A=0 B=0 | Y=1 Time=10 | A=0 B=1 | Y=0 Time=20 | A=1 B=0 | Y=0 Time=30 | A=1 B=1 | Y=1完全符合真值表!🎉
再打开波形文件(.vcd)用GTKWave查看,你会发现每个信号的变化都精准对齐时间轴——这就是硬件仿真的魅力:时间就是一切。
别小看它,同或门能干大事!
你以为这只是个玩具案例?错。同或门是很多高级功能的基础构件。举几个真实应用场景:
1. 8位数据比较器:判断两数是否相等
module comparator_8bit ( input [7:0] A, input [7:0] B, output equal ); wire [7:0] cmp; genvar i; generate for (i = 0; i < 8; i = i + 1) begin : bit_comp xnor_gate single (.A(A[i]), .B(B[i]), .Y(cmp[i])); end endgenerate assign equal = &cmp; // 所有位都匹配才算相等 endmodule👉 每一位做XNOR,最后来个“归约与”(AND reduction)。只要有一位不同,equal就是0。
这类结构广泛用于地址匹配、状态同步、冗余校验等场景。
2. 二值神经网络(BNN)中的乘法替代
在AI边缘计算中,为了降低功耗和面积,有些模型把权重和激活值都压缩成+1/-1或1/0。这时候传统的乘法可以用XNOR + 计数来近似:
1 x 1 = 1→ XNOR得11 x 0 = 0→ XNOR得00 x 1 = 0→ XNOR得00 x 0 = 1→ XNOR得1
你会发现:当两个比特相同时,XNOR输出为1,正好对应乘积为1的情况!
于是整个矩阵乘法就可以转化为大量并行的XNOR运算 + 统计1的个数(POP_COUNT)。这种方法在FPGA上效率极高,已经被用于低功耗AI加速器设计。
3. 双核锁步系统的故障检测
在航天、汽车电子等高可靠性系统中,常用双核锁步架构(Lockstep Dual-Core):两个CPU同时运行相同代码,输出实时比对。
怎么比对?用的就是一大排XNOR门!
一旦发现某个时刻输出不一致(即某位XNOR结果为0),立即触发错误中断,防止系统失控。
开发实践中要注意这些“坑”
别以为写了代码就能顺利烧板子。以下是新手常踩的雷区:
❌ 忘记覆盖所有输入分支 → 综合出锁存器(Latch)
如果你写的是组合逻辑但没处理所有条件,例如:
always @(*) begin if (A == 1) Y = B; // 没写else! end综合器会认为你需要“记住”上次的值,从而推断出latch。这在同步设计中可能引发亚稳态问题。
✅ 正确做法:要么补全else,要么用assign写纯组合逻辑。
✅ 最佳实践清单
| 建议 | 说明 |
|---|---|
使用命名端口连接.A(A) | 避免接错线,提升可读性 |
添加$dumpvars和$monitor | 调试利器,尤其适合初学者 |
| 信号命名要有意义 | 如data_match,bit_eq而非tmp1,sig_a |
合理使用generate结构 | 构建重复逻辑时更清晰 |
| 开启综合警告(Synthesis Warnings) | 查看是否有未连接信号、悬空输出等问题 |
写在最后:小门背后的大世界
你可能会觉得:“就为了一个同或门写了这么多?”
但正是这种看似简单的练习,教会了我们最重要的东西:
- 硬件思维:不是“顺序执行”,而是“并发存在”;
- 模块化设计:从最小单元出发,层层搭建复杂系统;
- 验证先行:没有仿真的设计等于赌博;
- 抽象与映射:你写的
A ~^ B,最终变成了FPGA里的一个LUT配置比特流。
下一步你可以尝试:
- 把同或门扩展成多输入版本(树形结构);
- 加一个使能端,做成可控制的比较器;
- 结合D触发器,做一个带采样的状态监测电路;
- 试着用SystemVerilog重写,并加入随机测试激励。
随着FPGA越来越多地应用于自动驾驶、智能传感、5G通信等领域,对底层逻辑的掌控能力反而变得更加重要。越高级的应用,越依赖扎实的基本功。
而这个小小的同或门,正是你通往硬件设计自由之路的第一块基石。
如果你正在学习FPGA,欢迎在评论区分享你的第一个成功仿真的瞬间——那往往是热爱开始的地方。