【HDL系列】Sklansky加法器的Verilog实现与优化策略

张开发
2026/6/26 18:09:01 15 分钟阅读
【HDL系列】Sklansky加法器的Verilog实现与优化策略
1. 从行波进位到Sklansky加法器为什么我们需要更快做硬件设计的同学都知道加法器是数字电路中最基础的运算单元之一。传统的行波进位加法器Ripple Carry Adder结构简单但有个致命缺点——进位信号需要像波浪一样从最低位 ripple到最高位。我曾经在一个FPGA项目里实测过32位行波进位加法器的延迟能达到10ns以上直接成了整个系统的性能瓶颈。这时候就需要并行进位加法器登场了。Sklansky加法器就是其中一种经典结构它通过巧妙的树形结构将进位传播路径从O(N)降到O(logN)。举个生活中的例子就像快递配送行波进位是快递员挨家挨户送串行而Sklansky加法器是建立多个配送中心分级处理并行。实测在Xilinx Artix-7上16位Sklansky加法器比行波进位版本快3倍以上。2. Sklansky加法器的核心原理2.1 进位选择加法器CSA的工作机制Sklansky加法器的核心组件是进位选择加法器Conditional-Sum Adder简称CSA。我第一次看这个结构时觉得特别巧妙——它用空间换时间提前计算好两种可能的进位情况。具体实现是这样的两个并行的行波进位加法器一个假设输入进位为0另一个假设为1多路选择器根据实际进位值选择正确的结果// 4位进位选择加法器示例 module CSA_4bit ( input [3:0] A, B, input Cin, output [3:0] Sum, output Cout ); wire [3:0] sum0, sum1; wire cout0, cout1; // 进位为0的情况 RCA_4bit rca0(.A(A), .B(B), .Cin(1b0), .Sum(sum0), .Cout(cout0)); // 进位为1的情况 RCA_4bit rca1(.A(A), .B(B), .Cin(1b1), .Sum(sum1), .Cout(cout1)); // 根据实际进位选择结果 assign Sum Cin ? sum1 : sum0; assign Cout Cin ? cout1 : cout0; endmodule2.2 Sklansky的树形结构创新Sklansky在1959年提出的创新点在于分层分组策略。我画过很多次它的结构图发现它的精妙之处在于层级化处理将N位加法器分成log2(N)层进位预计算每一层都提前计算不同进位假设下的结果选择器网络通过多路选择器逐级传递正确的进位以16位加法器为例其树形结构可以分为4层T0-T3。我在实现时发现每一层的模块其实可以复用只是位宽和连接方式不同。这为参数化设计提供了可能。3. Verilog实现详解3.1 模块化设计策略在实际编码时我习惯将Sklansky加法器拆分成三个主要部分module Sklansky_Adder #(parameter WIDTH16) ( input [WIDTH-1:0] A, B, input Cin, output [WIDTH-1:0] Sum, output Cout ); // 1. 预处理层生成传播(P)和生成(G)信号 wire [WIDTH-1:0] P, G; assign P A ^ B; assign G A B; // 2. 进位计算树 wire [WIDTH:0] C; assign C[0] Cin; // 这里需要实现多级进位计算网络 // 3. 求和层 assign Sum P ^ C[WIDTH-1:0]; assign Cout C[WIDTH]; endmodule3.2 参数化实现技巧经过几个项目的实践我总结出几个参数化设计的要点可配置位宽使用parameter定义位宽方便复用自动层级计算用函数计算需要的层级数生成语句用generate块自动实例化所需模块// 参数化CSA模块示例 module CSA #(parameter SIZE4) ( input [SIZE-1:0] A, B, input Cin, output [SIZE-1:0] Sum, output Cout ); // 根据SIZE大小自动选择实现方式 generate if (SIZE 1) begin // 1位全加器实现 full_adder fa(.A(A), .B(B), .Cin(Cin), .Sum(Sum), .Cout(Cout)); end else begin // 递归实现更大位宽 wire [SIZE/2-1:0] sum0, sum1; wire cout0, cout1; CSA #(SIZE/2) csa_low0 (.A(A[SIZE/2-1:0]), .B(B[SIZE/2-1:0]), .Cin(1b0), .Sum(sum0), .Cout(cout0)); CSA #(SIZE/2) csa_low1 (.A(A[SIZE/2-1:0]), .B(B[SIZE/2-1:0]), .Cin(1b1), .Sum(sum1), .Cout(cout1)); // 高位部分 wire carry Cin ? cout1 : cout0; CSA #(SIZE/2) csa_high (.A(A[SIZE-1:SIZE/2]), .B(B[SIZE-1:SIZE/2]), .Cin(carry), .Sum(Sum[SIZE-1:SIZE/2]), .Cout(Cout)); // 选择低位结果 assign Sum[SIZE/2-1:0] Cin ? sum1 : sum0; end endgenerate endmodule4. 优化策略与实战经验4.1 速度与面积的权衡在FPGA上实现时我遇到过几个典型的优化点流水线设计对关键路径插入寄存器优点可大幅提高时钟频率缺点增加延迟周期和寄存器开销选择器优化用LUT6实现4:1选择器Xilinx的每个SLICE包含4个LUT6一个LUT6可以实现2位4:1选择器进位网络简化对于特定应用可以删减部分进位计算比如图像处理中可能不需要全精度4.2 实测数据对比我在Xilinx Vivado 2022.1下综合了不同实现方式Artix-7 xc7a100tcsg324-1实现方式位宽LUT使用量延迟(ns)时钟频率(MHz)行波进位16328.2122Sklansky基础版161423.1322Sklansky流水线版161862.1476从数据可以看出虽然Sklansky版本消耗更多LUT资源但性能提升非常明显。在需要高速运算的场景这种trade-off通常是值得的。4.3 常见坑点与调试技巧在调试Sklansky加法器时我踩过几个坑位宽不匹配特别是在分层实现时容易搞错子模块的位宽建议用SystemVerilog的assert做自动检查选择器控制信号延迟高层级的选择信号可能成为新的关键路径解决方案对选择信号也做流水处理仿真与综合不一致行为仿真正确但综合后出错检查是否有多驱动问题确认所有信号在always块中被完整赋值这里分享一个实用的调试方法在Vivado中设置mark_debug属性把内部进位信号拉到ILA观察set_property MARK_DEBUG true [get_nets {sklansky_adder_inst/*carry*}]

更多文章