Verilog异或运算实战:格雷码、奇偶校验与奇数分频 📅 2026/8/1 5:05:04 1. 从“异或”这个不起眼的运算符说起在Verilog的世界里我们每天都在和一堆运算符打交道加、减-、与、或|……它们就像工具箱里的扳手和螺丝刀各司其职。而“异或”^这个运算符常常被初学者忽略觉得它无非就是“不一样就出1”的逻辑门似乎远不如加法器、状态机那么“高大上”。但在我十多年的FPGA/ASIC设计经历里我越来越觉得真正的高手往往能把最基础的元件玩出花来。异或运算就是这样一个典型的“基础但强大”的工具它简洁的数学特性能在很多意想不到的地方优雅地解决复杂问题。今天我们不谈复杂的算法和架构就聚焦在这个小小的“^”符号上。我会带你深入理解异或运算在Verilog中的本质然后通过三个非常经典且实用的应用场景——格雷码转换、奇偶校验和奇数分频——来彻底掌握它。你会发现理解了它你写的代码会变得更简洁、更高效甚至在某些性能关键路径上能帮你省下宝贵的逻辑资源。无论你是正在学习Verilog的学生还是已经入行的工程师相信这篇从实战中总结的干货都能让你对HDL编码有新的认识。2. 异或运算的核心不仅仅是“不同则为1”在深入应用之前我们必须把基础打牢。异或运算逻辑表达式为 Y A ^ B其真值表是00得001得110得111得0。这个“相同为0不同为1”的规则小学生都能背。但在数字电路和Verilog中我们需要从两个更深刻的层面去理解它。2.1 位运算与按位特性首先在Verilog中^是一个按位运算符。这意味着当操作数是多比特位宽的向量时运算是逐比特独立进行的。reg [3:0] a, b, y; assign y a ^ b; // 等价于 // y[0] a[0] ^ b[0]; // y[1] a[1] ^ b[1]; // y[2] a[2] ^ b[2]; // y[3] a[3] ^ b[3];这个特性至关重要它使得异或可以并行地对整个数据总线进行操作天生适合硬件描述语言所对应的并行硬件结构。你把它想象成一条流水线上的多个工位每个工位独立处理一位数据互不干扰效率极高。2.2 关键的数学性质成为“切换”与“校验”的基石异或运算有几个独一无二的数学性质这是它能够大显身手的根本原因归零律任何数与自己异或结果为0。A ^ A 0恒等律任何数与0异或结果为其本身。A ^ 0 A交换律和结合律A ^ B B ^ A(A ^ B) ^ C A ^ (B ^ C)。自反性或可逆性这是最神奇的一条如果C A ^ B那么A C ^ B 同时B C ^ A。异或运算的逆运算就是它本身。这个自反性意味着什么意味着异或可以用来“加密”和“解密”或者更普遍地说用来“切换”状态。我给你一个非常生活化的类比想象一个电灯开关。按一下异或1灯的状态改变亮变灭或灭变亮。再按一下再异或1灯的状态又变回来了。这个“开关”行为就是异或自反性的直观体现。在格雷码和奇分频里我们会反复看到这个“开关”或“切换”思想的应用。而它的归零律和结合律则天然适合做校验。因为如果一串数据经过一系列异或操作其结果能敏锐地反映出数据中“1”的个数的奇偶性这就是奇偶校验的原理。注意在Verilog中还有一个逻辑运算符^作为一元运算符时它表示缩减异或用于判断一个向量的所有位异或起来的结果即奇偶性。例如^bus等同于bus[0] ^ bus[1] ^ ... ^ bus[n-1]。在讨论奇偶判断时我们会用到它不要和按位异或混淆。3. 应用一格雷码编码与解码——消除毛刺的艺术第一个硬核应用我们来看格雷码。格雷码是一种绝对编码方式其最大特点是任意两个相邻的码字之间只有一位二进制位发生变化。这个特性太有用了在异步FIFO的指针跨时钟域同步、旋转编码器位置读取等场景中它能从根本上避免因多位同时跳变而产生的“毛刺”或“亚稳态”风险。那么格雷码和二进制码怎么转换呢核心就是异或。3.1 二进制转格雷码一位延迟的“流水线”二进制码Binary转格雷码Gray的公式是Gray Binary ^ (Binary 1)用Verilog实现起来极其简洁module bin2gray #(parameter WIDTH 4) ( input wire [WIDTH-1:0] bin, output wire [WIDTH-1:0] gray ); assign gray bin ^ (bin 1); endmodule为什么是右移一位再异或我们来拆解一下。假设二进制数B3 B2 B1 B0右移一位后变成0 B3 B2 B1。根据公式Gray[3] B3 ^ 0 B3Gray[2] B2 ^ B3Gray[1] B1 ^ B2Gray[0] B0 ^ B1看出规律了吗格雷码的最高位等于二进制码的最高位而其他每一位都等于当前二进制位与其高一位的异或结果。这就像一条信息传递的链条每一位的格雷码都“参考”了它左边邻居的值。这种结构保证了当二进制码加1时比如从0111到1000只有变化的位及其对应的格雷码位会翻转而其他位由于“参考链”的稳定性得以保持从而实现了相邻码字仅一位变化。3.2 格雷码转二进制码逆向推导的“递推”格雷码转二进制码稍微复杂一点但核心依然是异或。公式是一个递推关系Binary[i] Gray[i] ^ Binary[i1]对于最高位Binary[MSB] Gray[MSB]用Verilog实现通常用一个generate循环或者直接展开module gray2bin #(parameter WIDTH 4) ( input wire [WIDTH-1:0] gray, output reg [WIDTH-1:0] bin ); integer i; always (*) begin bin[WIDTH-1] gray[WIDTH-1]; // 最高位直接相等 for (i WIDTH-2; i 0; i i - 1) begin bin[i] gray[i] ^ bin[i1]; // 关键递推当前位 格雷码当前位 ^ 二进制高一位 end end endmodule这里异或扮演了什么角色它充当了“解码器”。还记得异或的自反性吗A C ^ B。在这里我们已知格雷码G[i]即C和已经计算出的高一位二进制码B[i1]即B需要求B[i]即A。根据公式G[i] B[i] ^ B[i1]两边同时异或B[i1]就得到B[i] G[i] ^ B[i1]。看完美契合这个递推过程从最高位开始像多米诺骨牌一样一路推导到最低位每一步都依赖上一步的结果这正是异或自反性的经典应用。实操心得在FPGA中实现格雷码转换时这个组合逻辑电路非常小且速度快。但要注意格雷码转二进制的递推结构是一个链式结构当位宽很大时比如64位关键路径会很长可能成为时序瓶颈。在实际高速设计中如果对延迟敏感可以考虑将其流水线化或者评估是否真的需要瞬间完成全位宽转换。对于异步FIFO我们通常只同步几位宽的格雷码指针位宽不大所以直接使用这个电路是完全没有问题的。4. 应用二奇偶校验——数据传输的“守门人”第二个应用是奇偶校验这是数据通信和存储中最基础、最广泛的错误检测方法之一。它的目标很简单判断一组数据中“1”的个数是奇数还是偶数。4.1 奇偶校验的原理与生成奇偶校验分为奇校验和偶校验。假设我们有一个8位数据data[7:0]。偶校验位使得data加上校验位后总共有偶数个1。即如果data中1的个数是偶数校验位为0如果是奇数校验位为1。奇校验位使得总共有奇数个1。规则与偶校验相反。如何用异或求校验位秘诀就在于多个比特的异或结果等于这些比特中“1”的个数的奇偶性。具体来说偶数个1异或得0奇数个1异或得1。因此对于一个向量bus^bus缩减异或的结果就是它的奇校验位因为^bus为1表示总线中有奇数个1为0表示有偶数个1。那么偶校验位就是奇校验位的取反。// 生成奇偶校验位示例 module parity_gen #(parameter WIDTH 8) ( input wire [WIDTH-1:0] data, output wire parity_odd, // 奇校验位 output wire parity_even // 偶校验位 ); // 缩减异或得到奇校验位 assign parity_odd ^data; // 偶校验位是奇校验位的反 assign parity_even ~parity_odd; // 或者直接 assign parity_even ~(^data); endmodule4.2 校验与错误检测在接收端我们如何检测错误呢我们把接收到的数据位和校验位放在一起再次进行缩减异或。对于奇校验发送方发送了{data, parity_odd}其中parity_odd ^data。接收方计算^ {received_data, received_parity}。如果没有错误根据异或的归零律和结合律^ {data, parity_odd} (^data) ^ parity_odd parity_odd ^ parity_odd 0。 所以如果接收方计算结果为0则认为数据可能正确注意奇偶校验只能检测奇数个错误位结果为1则断定发生了错误。对于偶校验发送方发送{data, parity_even}其中parity_even ~(^data)。接收方计算^ {received_data, received_parity}。如果没有错误^ {data, parity_even} (^data) ^ (~(^data)) 1。 所以接收方期待的结果是1如果得到0则说明有错误。module parity_check #(parameter WIDTH 8) ( input wire [WIDTH-1:0] rx_data, input wire rx_parity, // 假设接收的是奇校验位 output wire error_flag ); // 奇校验检查所有位数据校验位异或结果为0表示无奇数个错误 // 结果为1表示检测到错误奇数个位翻转 assign error_flag ^ {rx_data, rx_parity}; // 如果是偶校验则期望结果为1错误标志应为error_flag ~(^{rx_data, rx_parity}); endmodule注意事项奇偶校验能力有限只能检测出奇数个比特的错误。如果错误比特数是偶数个例如2位同时翻转异或结果会保持不变错误就无法被检测出来这称为“漏检”。因此在对可靠性要求高的场合如内存ECC、高速串行通信需要使用更强大的校验码如CRC或LDPC。但因其实现极其简单开销极小奇偶校验在片内总线、低速接口和缓存保护中依然广泛应用。5. 应用三奇数分频电路——对称方波的生成秘诀最后一个应用可能让很多初学者感到困惑如何用数字电路产生一个占空比为50%的奇数分频时钟比如5分频、7分频用计数器直接分频得到的波形占空比不可能是50%。这时异或运算就能提供一种非常巧妙的思路。5.1 核心思想双计数器相位叠加以5分频为例。目标是得到源时钟CLK的5分频且占空比50%。思路是我们用两个计数器都在源时钟CLK上升沿计数计数范围0-4。让这两个计数器从不同的初始值开始或者用不同的边沿触发使它们产生的波形相位相差半个CLK周期。将这两个计数器产生的使能信号或时钟进行异或就能合成一个占空比50%的奇数分频时钟。为什么异或可以回想一下它的真值表输入相同时输出0输入不同时输出1。如果我们有两个相位错开、占空比不是50%的时钟信号当它们状态不同时异或输出为高电平状态相同时输出为低电平。通过精心设计两个输入信号的占空比和相位差就能让输出信号的高电平和低电平时间相等。5.2 Verilog实现与波形分析让我们用Verilog实现一个通用的奇数分频模块参数N表示分频系数奇数。module odd_divider #(parameter N 5) ( // N must be odd input wire clk, input wire rst_n, output wire clk_out ); reg [31:0] cnt1, cnt2; // 计数器位宽根据N调整 reg clk1, clk2; // 第一个计数器上升沿触发产生占空比 (N-1)/2N 的波形 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt1 0; clk1 0; end else begin if (cnt1 N-1) begin cnt1 0; end else begin cnt1 cnt1 1; end // 计数器在特定值时翻转clk1以产生非50%占空比的脉冲 if (cnt1 (N-1)/2) begin clk1 1; end else if (cnt1 N-1) begin clk1 0; end end end // 第二个计数器下降沿触发产生与clk1相位差半个clk周期的波形 always (negedge clk or negedge rst_n) begin if (!rst_n) begin cnt2 0; clk2 0; end else begin if (cnt2 N-1) begin cnt2 0; end else begin cnt2 cnt2 1; end // 逻辑与第一个计数器相同但时钟沿不同 if (cnt2 (N-1)/2) begin clk2 1; end else if (cnt2 N-1) begin clk2 0; end end end // 关键步骤将两个相位错开的时钟异或得到50%占空比的输出时钟 assign clk_out clk1 ^ clk2; endmodule波形推理以N5为例clk1在上升沿计数计数到1(5-1)/22? 这里应为 (N-1)/2 2时拉高计数到4时拉低。所以clk1高电平持续 4-213 个CLK周期低电平持续2个占空比60%。clk2的逻辑与clk1完全一样但它是在CLK的下降沿动作因此它的波形整体相对于clk1延迟了半个CLK周期。现在将clk1和clk2异或。观察两者波形它们大部分时间是相同的同为高或同为低但在每个CLK周期的某个半周期内它们的状态会不同。这个“状态不同”的区域恰好拼接成了一个周期为5个CLK、高电平持续2.5个CLK、低电平持续2.5个CLK的完美对称方波。实操心得与避坑指南参数N必须为奇数这是该方法的前提。如果是偶数分频直接用计数器取最高位即可得到50%占空比更简单。时钟偏移与抖动clk_out是由组合逻辑异或门产生的它可能带有毛刺并且相对于源时钟CLK的延迟输出路径延迟不固定。因此clk_out绝对不能直接用作其他同步时序电路的时钟这是一个常见的错误。正确的用法是将其作为时钟使能信号Clock Enable在源时钟CLK的边沿去采样它然后用它来控制数据的翻转。这就是所谓的“门控时钟”或“使能时钟”设计。替代方案对于高性能设计更推荐使用PLL或MMCM等时钟管理单元来生成精确的奇数分频时钟它们能提供更低的抖动和更稳定的相位关系。本文的方法主要适用于逻辑资源受限或无法使用时钟管理单元的场景。仿真与调试在仿真时务必仔细查看clk1、clk2和clk_out的波形验证clk_out的周期和占空比是否符合预期。理解波形是如何通过异或合成的是掌握这个方法的关键。6. 进阶思考与扩展应用掌握了以上三个核心应用你已经算是异或运算的“熟练工”了。但它的潜力不止于此。我们可以再延伸思考一下伪随机数生成线性反馈移位寄存器LFSR是生成伪随机序列的常用硬件结构其核心就是异或运算。通过将移位寄存器的某些位抽头进行异或反馈到输入端可以产生周期很长的伪随机序列。例如一个最简单的4位LFSRnext_bit reg[3] ^ reg[2]。数据加密与简单混淆利用异或的自反性可以进行简单的流加密。使用一个密钥流如LFSR生成的序列与明文数据逐位异或得到密文。接收方用相同的密钥流再次异或即可恢复明文。虽然强度不高但在某些对安全性要求不高的配置通道中仍有应用。纠错码的基石更复杂的校验和纠错码如奇偶校验的升级版——汉明码其编码和校验矩阵的运算中异或也是最基本的操作单元。CRC校验的计算本质上也是基于异或的模2除法。总线翻转编码这是一种低功耗编码技术。当总线上要传输的数据中“1”的个数超过一半时将整条总线数据取反相当于与全1异或并发送一个翻转标志位。这样可以使总线上“1”的个数尽可能少减少开关活动从而降低动态功耗。接收端根据标志位再次异或全1即可恢复数据。这里异或充当了“条件取反”的角色。7. 常见问题与调试技巧实录在实际项目中应用这些技巧时你可能会遇到一些典型问题。这里我分享几个踩过的坑和解决方法。问题1格雷码转换组合逻辑出现毛刺怎么办虽然格雷码本身是为了防毛刺但转换电路尤其是二进制转格雷码是组合逻辑。当输入二进制码变化时由于各路径延迟微小差异输出格雷码在稳定前可能出现短暂的毛刺。排查在仿真中放大看信号跳变沿检查毛刺宽度。如果毛刺很窄远小于时钟周期且后续电路是在时钟边沿采样格雷码如异步FIFO中同步器输入则通常可以忽略因为同步器能过滤掉。解决如果毛刺必须消除可以考虑对二进制输入寄存器打一拍确保其变化是同步的或者对输出的格雷码也进行寄存器输出会引入一个时钟延迟。问题2奇偶校验模块在仿真时错误标志在数据稳定后仍偶尔跳变这很可能是由于仿真时的“竞争冒险”造成的。如果data和parity位不是绝对同时变化在变化的短暂窗口内异或树会看到一个临时的错误状态。排查检查测试平台Testbench中驱动data和parity的赋值是否使用了非阻塞赋值且在同一个时钟边沿。确保它们来自于同一个寄存器输出。解决在RTL设计中确保校验位和数据位是同一组寄存器在同一时钟沿产生的。在接收端使用同步寄存器先锁存数据和校验位再用锁存后的稳定值进行异或校验这样可以避免毛刺引起的误报。问题3奇数分频输出的时钟clk_out驱动后续电路出现建立/保持时间违例这就是前面强调的“大坑”。clk_out是组合逻辑输出其路径延迟受布局布线影响大skew不好控制。排查在静态时序分析STA报告中查看clk_out到其他寄存器时钟端的路径。根本解决永远不要用组合逻辑产生的信号当时钟。改为时钟使能方案// 正确用法使能信号同步化 reg clk_out_en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_out_en 0; div_data 0; end else begin clk_out_en clk_out; // 将异或输出的使能信号用源时钟同步一拍 if (clk_out_en) begin // 使用同步后的使能信号 div_data ~div_data; // 或其他需要在分频时钟沿操作的逻辑 end end end问题4编写的奇数分频器分频系数参数化后行为不对排查重点检查计数器比较值(N-1)/2。在Verilog中整数除法是截断取整。当N为奇数时(N-1)/2是一个整数例如5-27-3这没问题。但如果N被错误地设为偶数或者参数传递错误(N-1)/2可能不是你想要的中点值。解决在模块开头增加参数合法性检查使用generate或if语句确保N为奇数或者用$error在仿真时报警。if (N % 2 0) begin $error(Error: Division factor N must be an odd number. Current N %0d, N); // 在实际综合代码中可能需要一个默认值或让输出保持常值 end问题5综合后资源占用比预想的多异或逻辑本身非常节省资源一个异或门通常只需几个LUT查找表单元。排查如果资源占用异常可能是综合工具没有识别出你的设计意图或者你的代码描述方式导致了冗余逻辑。例如奇偶校验的缩减异或^data如果data位宽很大工具可能会生成一个多级树形结构这是正常的。优化对于非常大的位宽奇偶校验如果速度要求高可以手动编写多级流水线结构的异或树平衡时序和面积。但绝大多数情况下相信综合工具的优化能力即可。使用assign parity ^data;这种直接描述工具能很好地处理。最后我个人最深刻的体会是硬件描述语言的学习绝不能停留在语法层面。像异或这样一个简单的运算符当你真正吃透它的数学本质和硬件特性后就能在系统设计中找到四两拨千斤的巧妙用法。它提醒我们在追求复杂IP和高端架构的同时也不要忘了这些构成数字世界基石的基本逻辑单元所蕴含的智慧。下次当你写代码时不妨多看一眼那些看似简单的操作想想它们背后是否藏着更优雅的实现方式。