Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/src/main/scala/chisel3/internal/Warning.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
154 changes: 154 additions & 0 deletions src/main/scala/chisel3/util/CircuitMath.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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)
}
}
}
75 changes: 75 additions & 0 deletions src/test/scala/chiselTests/CarrySaveAdder.scala
Original file line number Diff line number Diff line change
@@ -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 ")
}
}
Loading