From be9d1a95a9507d734ccdabf29f8b118a37f5e987 Mon Sep 17 00:00:00 2001 From: Maarten Boersma Date: Tue, 30 Sep 2025 08:16:01 +0200 Subject: [PATCH 1/3] add Csa object to create carry-save-adder circuitry --- src/main/scala/chisel3/util/CircuitMath.scala | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) 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 + } + } +} From 7ba409583ed4210b68d0a02e796f445b343601ae Mon Sep 17 00:00:00 2001 From: Maarten Boersma Date: Thu, 11 Jun 2026 13:06:29 +0200 Subject: [PATCH 2/3] test correctness of Csa function --- .../scala/chiselTests/CarrySaveAdder.scala | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/test/scala/chiselTests/CarrySaveAdder.scala 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)) + } + } +} From fff1499a8b2ddbb058bae88302783236af9b3160 Mon Sep 17 00:00:00 2001 From: Maarten Boersma Date: Mon, 27 Jul 2026 15:06:17 +0200 Subject: [PATCH 3/3] (1) API also takes care of final addition. (2) Avoid constant-0 MSB in carry term. (3) Add support for inserting single-bit terms in the carry LSB holes (4) Warning for unknown input width --- .../main/scala/chisel3/internal/Warning.scala | 1 + src/main/scala/chisel3/util/CircuitMath.scala | 155 ++++++++++++++++-- .../scala/chiselTests/CarrySaveAdder.scala | 46 ++++-- 3 files changed, 175 insertions(+), 27 deletions(-) diff --git a/core/src/main/scala/chisel3/internal/Warning.scala b/core/src/main/scala/chisel3/internal/Warning.scala index a08dd595bc9..95848642dd2 100644 --- a/core/src/main/scala/chisel3/internal/Warning.scala +++ b/core/src/main/scala/chisel3/internal/Warning.scala @@ -21,6 +21,7 @@ private[chisel3] object WarningID extends Enumeration { val ExtractFromVecSizeZero = Value(6) val BundleLiteralValueTooWide = Value(7) val AsTypeOfReadOnly = Value(8) + val CsaUnknownInputWidth = Value(9) } import WarningID.WarningID diff --git a/src/main/scala/chisel3/util/CircuitMath.scala b/src/main/scala/chisel3/util/CircuitMath.scala index b5fefd144fa..b188733662b 100644 --- a/src/main/scala/chisel3/util/CircuitMath.scala +++ b/src/main/scala/chisel3/util/CircuitMath.scala @@ -6,6 +6,8 @@ package chisel3.util import chisel3._ +import chisel3.internal.{Builder, Warning, WarningID} +import chisel3.experimental.SourceInfo /** Returns the base-2 integer logarithm of an UInt. * @@ -42,27 +44,154 @@ 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. +/** Carry-save adder circuit generation functions. + * Constructs a tree to reduce an arbitrary number of input terms to two terms. * - * 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. + * Example resulting circuit topology if applied to 8 4-bit-wide terms, a...h: + * Rank 1: create groups of 3 terms, reducing each group to 2 terms; i,j,k,l,g,h + * GROUP0 GROUP1 GROUP2 + * aaaa dddd gggg + * bbbb eeee hhhh + * cccc ffff + * _____+ _____+ _____+ + * iiii kkkk gggg + * jjjj. llll. hhhh + * Rank 2: create groups of 3 terms, reducing each group to 2 terms: m,n,o,p + * GROUP0 GROUP1 + * iiii llll. + * jjjj. gggg + * kkkk hhhh + * ______+ ______+ + * mmmmm ooooo + * nnnn. pppp. + * Rank 3: create groups of 3 terms, reducing each group to 2 terms: q,r,p + * GROUP0 GROUP1 + * mmmmm pppp. + * nnnn. + * ooooo + * ______+ ______+ + * qqqqq pppp. + * rrrrr. + * Rank 4: reduce last group of 3 terms to 2 terms: s,t + * qqqqq + * rrrrr. + * pppp. + * ______+ + * ssssss + * tttt.. + * + * Every . is an empty spot introduced by the topology, constant zero in the carry LSB. + * If the input has single-bit terms, insert those there. + * In the example tree above, there are six spots where we can insert a single-bit term: j,l,n,p,r,t + * + * Future improvement suggestion (not implemented yet): + * - add support for negative weights at arbitrary bit positions; required to support subtraction. + * - circuitry returns two UInt hardware terms; final addition includes a signed constant offset. */ 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. + /** Adds an arbitrary-length sequence of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + * The bits from the *bits parameter are inserted in the tree with LSB weight, without adding logic depth. + */ + def apply(terms: Seq[UInt], bits: Seq[Bool])(implicit sourceInfo: SourceInfo): UInt = { + val sumRedundant = sumCarry(terms, bits) + terms.length match { + case 0 => 0.U(0.W) + case 1 => terms.head // avoid +& because it widens result by 1 bit + case _ => sumRedundant._1 +& sumRedundant._2 // final carry-propagate addition of the two output terms + } + } + + /** Adds the arbitrary-length sequence of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + */ + def apply(terms: Seq[UInt])(implicit sourceInfo: SourceInfo): UInt = apply(terms, Seq.empty[Bool]) + + /** Adds an arbitrary number of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + */ + def apply(firstTerm: UInt, moreTerms: UInt*)(implicit sourceInfo: SourceInfo): UInt = apply(firstTerm +: moreTerms) + + /** Adds the arbitrary-length sequence of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + * Inserts an additional bit in the LSB bit position, without adding logic depth. + */ + def apply(terms: Seq[UInt], bit: Bool)(implicit sourceInfo: SourceInfo): UInt = apply(terms, Seq(bit)) + + /** Adds the arbitrary-length sequence of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + * Inserts additional bits in the LSB bit position, without adding logic depth. + */ + def apply(terms: Seq[UInt], bit: Bool, moreBits: Bool*)(implicit sourceInfo: SourceInfo): UInt = + apply(terms, bit +: moreBits) + + /** Adds the arbitrary-length sequence of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + * The bits from the *bits parameter are inserted in the tree with LSB weight, without adding logic depth. + * Returns the sum in redundant format as sum/carry tuple. + */ + def sumCarry(terms: Seq[UInt], bits: Seq[Bool])(implicit sourceInfo: SourceInfo): (UInt, UInt) = { + val bitsIt = bits.iterator + val result = carrySaveRec(terms, bitsIt) + require( + !bitsIt.hasNext, + "Not enough 3:2 reduction stages to accommodate all single-bit terms from the second argument" + ) + terms.filterNot(_.isWidthKnown).foreach { t => + Builder.warning( + Warning( + WarningID.CsaUnknownInputWidth, + s"Cannot optimize width of carry vector because width of input term ${t} is unknown." + ) + ) + } + result.length match { // Recursive function returns a Seq, convert to tuple + case 0 => (0.U(0.W), 0.U(0.W)) + case 1 => (result.head, 0.U(0.W)) + case 2 => (result.head, result.last) + } + } + + /** Adds the arbitrary-length sequence of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + * Returns the sum in redundant format as sum/carry tuple. + */ + def sumCarry(terms: Seq[UInt])(implicit sourceInfo: SourceInfo): (UInt, UInt) = sumCarry(terms, Seq.empty[Bool]) + + /** Adds an arbitrary number of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + * Returns the sum in redundant format as sum/carry tuple. + */ + def sumCarry(firstTerm: UInt, moreTerms: UInt*)(implicit sourceInfo: SourceInfo): (UInt, UInt) = + sumCarry(firstTerm +: moreTerms, Seq.empty[Bool]) + + /** Adds the arbitrary-length sequence of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + * Inserts an additional bit in the LSB bit position, without adding logic depth. + * Returns the sum in redundant format as sum/carry tuple. + */ + def sumCarry(terms: Seq[UInt], bit: Bool)(implicit sourceInfo: SourceInfo): (UInt, UInt) = sumCarry(terms, Seq(bit)) + + /** Adds the arbitrary-length sequence of UInts in an area- and timing-efficient way, by using a Carry-Save Adder tree. + * Inserts additional bits in the LSB bit position, without adding logic depth. + * Returns the sum in redundant format as sum/carry tuple. + */ + def sumCarry(terms: Seq[UInt], bit: Bool, moreBits: Bool*)(implicit sourceInfo: SourceInfo): (UInt, UInt) = + sumCarry(terms, bit +: moreBits) + + /** Recursive function to construct a carry save adder tree (Wallace tree). Carry LSB holes are filled with Bools, if provided + * Sum is returned as two UInts, sum and carry. These terms are still to be added by a carry-propagate adder. */ - def apply(x: Seq[UInt]): Seq[UInt] = { - x.length match { + private def carrySaveRec(terms: Seq[UInt], bitsIt: Iterator[Bool]): Seq[UInt] = { + terms.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 1 => terms + case 2 => terms + case 3 => + val sum = terms(0) ^ terms(1) ^ terms(2) + val carry = (terms(0) & terms(1) | terms(0) & terms(2) | terms(1) & terms(2)) << 1 + val carryLsb = bitsIt.nextOption().getOrElse(false.B) + if (terms.forall(_.isWidthKnown)) { + val carryWidth = terms.map(_.getWidth).sorted.apply(1) + 1 + Seq(sum, carry(carryWidth - 1, 0) | carryLsb) + } else + Seq(sum, carry | carryLsb) case _ => - Csa(x.grouped(3).map(xyz => Csa(xyz)).reduce(_ ++ _)) // every group of 3 reduces to 2. Result to next level + // Create groups of 3, reduce every group to 2. Result to next level. + carrySaveRec(terms.grouped(3).map(xyz => carrySaveRec(xyz, bitsIt)).reduce(_ ++ _), bitsIt) } } } diff --git a/src/test/scala/chiselTests/CarrySaveAdder.scala b/src/test/scala/chiselTests/CarrySaveAdder.scala index 30c068bc049..8be6a4cb2cd 100644 --- a/src/test/scala/chiselTests/CarrySaveAdder.scala +++ b/src/test/scala/chiselTests/CarrySaveAdder.scala @@ -7,12 +7,13 @@ import scala.util.Random import chisel3._ import chisel3.simulator.scalatest.ChiselSim import chisel3.simulator.stimulus.RunUntilFinished -import chisel3.util.{Counter, Csa} +import chisel3.util.{Counter, Csa, PopCount} import chisel3.util.random.LFSR import org.scalatest.propspec.AnyPropSpec import org.scalatest.matchers.should.Matchers +import circt.stage.ChiselStage -class CsaTester(termWidths: Seq[Int]) extends Module { +class CsaTester(termWidths: Seq[Int], boolCount: Int) extends Module { // Cannot exhaustively simulate all input combinations. // Instead: 1- test correctness around min-and-max input values @@ -22,36 +23,53 @@ class CsaTester(termWidths: Seq[Int]) extends Module { 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 (termDecrementPtr, _) = Counter(0 until termWidths.length + boolCount) val termsCounting = termWidths.zipWithIndex.map { case (tW, idx) => val term = RegInit(0.U(tW.W)) when(idx.U === termDecrementPtr) { term := term - 1.U } term } + val bools = Seq.fill(boolCount)(RegInit(false.B)) + bools.zipWithIndex.foreach { case (b, idx) => + when(idx.U === termDecrementPtr - termWidths.length.U) { b := ~b } + } // 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") + testCases.foreach { csaInput => // parallel testing circuitry for both tests + val csaResult = Csa(csaInput, bools) + val (sum, cry) = Csa.sumCarry(csaInput, bools) + val refResult = csaInput.reduce((a, b) => a +& b) +& PopCount(bools) + assert(csaInput.forall(_.isWidthKnown), "Testcase error: should know the width of input terms") + assert( + csaResult.getWidth <= refResult.getWidth, + s"csaResult width ${csaResult.getWidth} should not exceed refResult width ${refResult.getWidth}\n" + ) + assert(csaResult === refResult, s"Wrong result of CSA final sum, $csaInput") + assert((sum +& cry) === refResult, s"Wrong result of CSA output in redundant form, $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)) +class CsaSpec extends AnyPropSpec with PropertyUtils with ChiselSim with Matchers with LogUtils { + property(s"Carry-Save Adder (10 inputs, 20-bit-wide each + some bools) should return correct result") { + simulate(new CsaTester(Seq.fill(10)(20), 5))(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)) + property(s"Carry-Save Adder with $n input terms + ${n / 3} bools should return correct result") { + simulate(new CsaTester(testCsaTermWidths, n / 3))(RunUntilFinished(1000)) } } + + property("Carry-Save Adder should warn about unknown-width input terms") { + val (log, _) = grabLog(ChiselStage.emitCHIRRTL(new RawModule { + val myUInts = Seq.fill(4)(Wire(UInt())) + Csa(myUInts) + })) + log should include("Cannot optimize width of carry vector because width of input term ") + } }