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 9e4890a9857..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. * @@ -41,3 +43,155 @@ object Log2 { private def divideAndConquerThreshold = 4 } + +/** Carry-save adder circuit generation functions. + * Constructs a tree to reduce an arbitrary number of input terms to two terms. + * + * 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 { + + /** 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. + */ + private def carrySaveRec(terms: Seq[UInt], bitsIt: Iterator[Bool]): Seq[UInt] = { + terms.length match { + case 0 => Seq(0.U(0.W)) + 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 _ => + // 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 new file mode 100644 index 00000000000..8be6a4cb2cd --- /dev/null +++ b/src/test/scala/chiselTests/CarrySaveAdder.scala @@ -0,0 +1,75 @@ +// 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, 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], boolCount: 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 + 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 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 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"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 ") + } +}