diff --git a/src/main/scala/chisel3/util/CircuitMath.scala b/src/main/scala/chisel3/util/CircuitMath.scala index 9e4890a9857..b5fefd144fa 100644 --- a/src/main/scala/chisel3/util/CircuitMath.scala +++ b/src/main/scala/chisel3/util/CircuitMath.scala @@ -41,3 +41,28 @@ object Log2 { private def divideAndConquerThreshold = 4 } + +/** Create a carry save adder built from full adders. If more then 3 input terms, construct a Wallace tree. + * + * The function returns 2 output terms, which are still to be added by a carry-propagate adder. + * Future improvement idea: add support for negative weights at arbitrary bit positions; required to support subtraction. + * Function can return two UInt hardware terms together with a signed constant offset, computed at elaboration-time. + */ + +object Csa { + + /** Create a carry save adder built from full adders. If more then 3 input terms, construct a Wallace tree. + * The function returns 2 output terms (unless called with less than 2 inputs, then it returns 1). + * The outputs are still to be added by a carry-propagate adder. + */ + def apply(x: Seq[UInt]): Seq[UInt] = { + x.length match { + case 0 => Seq(0.U(0.W)) + case 1 => x + case 2 => x + case 3 => Seq(x(0) ^ x(1) ^ x(2), (x(0) & x(1) | x(0) & x(2) | x(1) & x(2)) << 1) // sum, carry + case _ => + Csa(x.grouped(3).map(xyz => Csa(xyz)).reduce(_ ++ _)) // every group of 3 reduces to 2. Result to next level + } + } +} diff --git a/src/test/scala/chiselTests/CarrySaveAdder.scala b/src/test/scala/chiselTests/CarrySaveAdder.scala new file mode 100644 index 00000000000..30c068bc049 --- /dev/null +++ b/src/test/scala/chiselTests/CarrySaveAdder.scala @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chiselTests + +import scala.util.Random + +import chisel3._ +import chisel3.simulator.scalatest.ChiselSim +import chisel3.simulator.stimulus.RunUntilFinished +import chisel3.util.{Counter, Csa} +import chisel3.util.random.LFSR +import org.scalatest.propspec.AnyPropSpec +import org.scalatest.matchers.should.Matchers + +class CsaTester(termWidths: Seq[Int]) extends Module { + + // Cannot exhaustively simulate all input combinations. + // Instead: 1- test correctness around min-and-max input values + // 2- test correctness for random input values + + val (_, expired) = Counter(0 to 200) + when(expired) { stop() } + + // Directed test. Each term starts at zero. Decrement one term at a time, round-robin. + val (termDecrementPtr, _) = Counter(0 until termWidths.length) + val termsCounting = termWidths.zipWithIndex.map { case (tW, idx) => + val term = RegInit(0.U(tW.W)) + when(idx.U === termDecrementPtr) { term := term - 1.U } + term + } + + // Random test. LFSR does not work for bitwidths 0 and 1 + val termsRandom = termWidths.map { tW => if (tW >= 2) LFSR(tW) else tW.U } + val testCases = Seq(termsCounting, termsRandom) + + testCases.foreach { csaInput => // parallel testing circuitry foreach + val csaOutput = Csa(csaInput) + assert(csaOutput.length <= 2, s"CSA tree has more than 2 output terms") + val csaResult = csaOutput.reduce((a, b) => a +& b) + val refResult = csaInput.reduce((a, b) => a +& b) + assert(csaResult === refResult, s"Wrong result at CSA output, $csaInput") + } +} + +class CsaSpec extends AnyPropSpec with PropertyUtils with ChiselSim { + property(s"CSA adder reduction tree (10 inputs, 20-bit-wide each) should return the correct result") { + simulate(new CsaTester(Seq.fill(10)(20)))(RunUntilFinished(1000)) + } + + val prng = new Random(seed = 1234567) + for (n <- ((1 to 5) ++ (10 to 25 by 5))) { // number of CSA input terms + val testCsaTermWidths = prng.shuffle(Seq.range(0, 31)).take(n) // constrained random width of each CSA input term + property(s"CSA adder reduction tree with $n input terms of different widths should return the correct result") { + simulate(new CsaTester(testCsaTermWidths))(RunUntilFinished(1000)) + } + } +}