Skip to content
This repository was archived by the owner on Aug 19, 2024. It is now read-only.

Reintroduce FixedPoint support #706

Open
wants to merge 6 commits into
base: 5.x
Choose a base branch
from
Open
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Revert "remove FixedPoint and Interval support (#624)"
This reverts commit d8a44b2.

We only revert FixedPoint support, not Interval.

We also do not revert changes in chiseltest.iotesters, since that codebase was ported into dsptools.
  • Loading branch information
mvanbeirendonck committed Jan 31, 2024
commit b9a40bf4a5a46e9cd15a5111678339e7429c7c0c
72 changes: 71 additions & 1 deletion src/main/scala/chiseltest/package.scala
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ import chisel3.{
import chisel3.util.{ReadyValidIO, ValidIO}
import chisel3.experimental.BundleLiterals._
import chisel3.experimental.VecLiterals._
import fixedpoint._

/** Basic interfaces and implicit conversions for testers2
*/
@@ -121,6 +122,59 @@ package object chiseltest {
}
}

/** allows access to chisel FixedPoint type signals with Scala native values */
implicit class testableFixedPoint(x: FixedPoint) {
private def asFixedPoint(value: Double): FixedPoint = value.F(Utils.getFirrtlWidth(x), x.binaryPoint)
private def asFixedPoint(value: BigDecimal): FixedPoint = value.F(Utils.getFirrtlWidth(x), x.binaryPoint)
def poke(value: FixedPoint): Unit = {
require(x.binaryPoint == value.binaryPoint, "binary point mismatch")
Utils.pokeBits(x, value.litValue)
}
def poke(value: Double): Unit = poke(asFixedPoint(value))
def poke(value: BigDecimal): Unit = poke(asFixedPoint(value))
private[chiseltest] def expectInternal(value: FixedPoint, message: Option[() => String]): Unit = {
require(x.binaryPoint == value.binaryPoint, "binary point mismatch")
// for backwards compatibility reasons, we do not support epsilon when expect is called with the FixedPoint type.
Utils.expectBits(x, value.litValue, message, Some(Utils.fixedToString(x.binaryPoint)))
}
def expect(value: FixedPoint): Unit = expectInternal(value, None)
def expect(value: FixedPoint, message: => String): Unit = expectInternal(value, Some(() => message))
private[chiseltest] def expectInternal(expected: Double, epsilon: Double, userMsg: Option[() => String]): Unit = {
Utils.expectEpsilon(x, peekDouble(), expected, epsilon, userMsg)
}
def expect(value: Double): Unit = expectInternal(value, epsilon = 0.01, None)
def expect(value: Double, epsilon: Double): Unit = expectInternal(value, epsilon = epsilon, None)
def expect(value: Double, message: => String): Unit =
expectInternal(value, epsilon = 0.01, Some(() => message))
def expect(value: Double, message: => String, epsilon: Double): Unit =
expectInternal(value, epsilon = epsilon, Some(() => message))
private[chiseltest] def expectInternal(
expected: BigDecimal,
epsilon: BigDecimal,
userMsg: Option[() => String]
): Unit = {
Utils.expectEpsilon(x, peekBigDecimal(), expected, epsilon, userMsg)
}
def expect(value: BigDecimal): Unit = expectInternal(value, epsilon = 0.01, None)
def expect(value: BigDecimal, epsilon: BigDecimal): Unit = expectInternal(value, epsilon = epsilon, None)
def expect(value: BigDecimal, message: => String): Unit =
expectInternal(value, epsilon = 0.01, Some(() => message))
def expect(value: BigDecimal, message: => String, epsilon: BigDecimal): Unit =
expectInternal(value, epsilon = epsilon, Some(() => message))
def peek(): FixedPoint = {
val multiplier = BigDecimal(2).pow(x.binaryPoint.get)
(BigDecimal(Context().backend.peekBits(x)) / multiplier).F(x.binaryPoint)
}
def peekDouble(): Double = x.binaryPoint match {
case KnownBinaryPoint(bp) => FixedPoint.toDouble(Context().backend.peekBits(x), bp)
case _ => throw new Exception("Cannot peekInterval with unknown binary point location")
}
def peekBigDecimal(): BigDecimal = x.binaryPoint match {
case KnownBinaryPoint(bp) => FixedPoint.toBigDecimal(Context().backend.peekBits(x), bp)
case _ => throw new Exception("Cannot peekInterval with unknown binary point location")
}
}

implicit class testableRecord[T <: Record](x: T) {

/** Poke the given signal with a [[Record.litValue()]]
@@ -201,6 +255,7 @@ package object chiseltest {
case (x: Bool, value: Bool) => x.poke(value)
case (x: UInt, value: UInt) => x.poke(value)
case (x: SInt, value: SInt) => x.poke(value)
case (x: FixedPoint, value: FixedPoint) => x.poke(value)
case (x: Record, value: Record) =>
require(DataMirror.checkTypeEquivalence(x, value), s"Record type mismatch")
x.elements.zip(value.elements).foreach { case ((_, x), (_, value)) =>
@@ -223,6 +278,7 @@ package object chiseltest {
case x: Bool => x.peek().asInstanceOf[T]
case x: UInt => x.peek().asInstanceOf[T]
case x: SInt => x.peek().asInstanceOf[T]
case x: FixedPoint => x.peek().asInstanceOf[T]
case x: Record =>
val elementValueFns = x.elements.map { case (name: String, elt: Data) =>
(y: Record) => (y.elements(name), elt.peek())
@@ -243,6 +299,7 @@ package object chiseltest {
case (x: Bool, value: Bool) => x.expectInternal(value.litValue, message)
case (x: UInt, value: UInt) => x.expectInternal(value.litValue, message)
case (x: SInt, value: SInt) => x.expectInternal(value.litValue, message)
case (x: FixedPoint, value: FixedPoint) => x.expectInternal(value, message)
case (x: Record, value: Record) =>
require(DataMirror.checkTypeEquivalence(x, value), s"Record type mismatch")
x.elements.zip(value.elements).foreach { case ((_, x), (_, value)) =>
@@ -325,13 +382,26 @@ package object chiseltest {
}

// helps us work around the fact that signal.width is private!
def getFirrtlWidth(signal: Bits): chisel3.internal.firrtl.Width = signal.widthOption match {
def getFirrtlWidth(signal: Data): chisel3.internal.firrtl.Width = signal.widthOption match {
case Some(value) => chisel3.internal.firrtl.KnownWidth(value)
case None => chisel3.internal.firrtl.UnknownWidth()
}

def boolBitsToString(bits: BigInt): String = (bits != 0).toString

def fixedToString(binaryPoint: BinaryPoint): BigInt => String = {
def inner(bits: BigInt): String = {
binaryPoint match {
case KnownBinaryPoint(binaryPoint) =>
val bpInteger = 1 << binaryPoint
(bits.toFloat / bpInteger).toString
case UnknownBinaryPoint => "[unknown binary point]"
}
}

inner
}

def enumToString(tpe: EnumType): BigInt => String = {
def inner(bits: BigInt): String = {
val fullName = chisel3.internaltest.EnumHelpers.valueToName(tpe, bits).getOrElse("???")
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ class HasOddWidthSInt extends Module {
out := masked === in
}

// The poke of a negative number into an SInt input that is not a standard word size
// The poke of a negative number into an SInt or FixedPoint input that is not a standard word size
// would break in verilator if the poked value was not masked to the correct number of
// bits first. This was fixed by masking those values to the proper width before poking
class NegativeInputValuesTest extends AnyFreeSpec with ChiselScalatestTester {
59 changes: 59 additions & 0 deletions src/test/scala/chiseltest/tests/ElementTest.scala
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import fixedpoint._

class ElementTest extends AnyFlatSpec with ChiselScalatestTester with Matchers {
behavior of "Testers2 with Element types"
@@ -93,4 +94,62 @@ class ElementTest extends AnyFlatSpec with ChiselScalatestTester with Matchers {
c.io.expect(false.B, true.B, false.B, true.B)
}
}

it should "work with FixedPoint" in {
test(new Module {
val io = IO(new Bundle {
val in1 = Input(FixedPoint(8.W, 2.BP))
val in2 = Input(FixedPoint(8.W, 2.BP))
val out = Output(FixedPoint(8.W, 2.BP))

def expect(in1Val: FixedPoint, in2Val: FixedPoint, outVal: FixedPoint): Unit = {
in1.poke(in1Val)
in2.poke(in2Val)
out.expect(outVal)
}
})
io.out := io.in1 + io.in2
}) { c =>
c.io.expect(0.F(2.BP), 0.F(2.BP), 0.F(2.BP))
c.io.expect(1.F(2.BP), 1.F(2.BP), 2.F(2.BP))
c.io.expect(0.5.F(2.BP), 0.5.F(2.BP), 1.F(2.BP))
c.io.expect(0.5.F(2.BP), -0.5.F(2.BP), 0.F(2.BP))

// Overflow test, treating it as a 6-bit signed int
c.io.expect(31.F(2.BP), 1.F(2.BP), -32.F(2.BP))
c.io.expect(-32.F(2.BP), -1.F(2.BP), 31.F(2.BP))

c.io.expect(31.F(2.BP), 31.F(2.BP), -2.F(2.BP))
c.io.expect(-32.F(2.BP), -32.F(2.BP), 0.F(2.BP))

// Overflow test with decimal component
c.io.expect(31.75.F(2.BP), 31.75.F(2.BP), -0.5.F(2.BP))
c.io.expect(31.75.F(2.BP), 0.25.F(2.BP), -32.F(2.BP))
}
}

it should "peek on FixedPoint correctly" in {
test(new PassthroughModule(new Bundle() {
val d1 = FixedPoint(64.W, 0.BP)
val d2 = FixedPoint(66.W, 2.BP)
})) { c =>
c.in.d1.poke(BigDecimal(Long.MaxValue).F(0.BP))
c.out.d1.peek().litToBigDecimal should be (BigDecimal(Long.MaxValue))

c.in.d1.poke(BigDecimal(Long.MinValue).F(0.BP))
c.out.d1.peek().litToBigDecimal should be (BigDecimal(Long.MinValue))

c.in.d1.poke(0.F(0.BP))
c.out.d1.peek().litToBigDecimal should be (BigDecimal(0))

c.in.d2.poke(BigDecimal(Long.MaxValue).F(2.BP))
c.out.d2.peek().litToBigDecimal should be (BigDecimal(Long.MaxValue))

c.in.d2.poke(BigDecimal(Long.MinValue).F(2.BP))
c.out.d2.peek().litToBigDecimal should be (BigDecimal(Long.MinValue))

c.in.d2.poke(0.F(2.BP))
c.out.d2.peek().litToBigDecimal should be (BigDecimal(0))
}
}
}
21 changes: 21 additions & 0 deletions src/test/scala/chiseltest/tests/FaultDecoderTest.scala
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import chiseltest._
import org.scalatest._
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import fixedpoint._

class FaultDecoderTest extends AnyFlatSpec with ChiselScalatestTester with Matchers {
behavior of "Testers2"
@@ -41,6 +42,26 @@ class FaultDecoderTest extends AnyFlatSpec with ChiselScalatestTester with Match
exc.getMessage should include ("expected=false (0, 0x0)")
}

it should "display decimal for fixedpoint" in {
val exc = intercept[exceptions.TestFailedException] {
test(new StaticModule((1.5).F(1.BP))) { c =>
c.out.expect((1).F(1.BP))
}
}
exc.getMessage should include ("1.5 (3, 0x3)")
exc.getMessage should include ("expected=1.0 (2, 0x2)")
}

it should "display decimal for negative fixedpoint" in {
val exc = intercept[exceptions.TestFailedException] {
test(new StaticModule((-2.0).F(1.BP))) { c =>
c.out.expect((-0.5).F(1.BP))
}
}
exc.getMessage should include ("-2.0 (-4, -0x4)")
exc.getMessage should include ("expected=-0.5 (-1, -0x1)")
}

ignore should "display names for enums" in { // needs better reflection support in enums
object EnumExample extends ChiselEnum {
val e0, e1, e2 = Value
102 changes: 102 additions & 0 deletions src/test/scala/chiseltest/tests/FixedPointTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: Apache-2.0

package chiseltest.tests

import chisel3._
import chiseltest._
import chiseltest.simulator.RequiresVerilator
import org.scalatest.exceptions.TestFailedException
import org.scalatest.freespec.AnyFreeSpec
import fixedpoint._

class FixedPointTests extends AnyFreeSpec with ChiselScalatestTester {
"fixed point reduce should work with BigDecimal" in {
test(new FixedPointReduce(FixedPoint(70.W, 60.BP), 10)) { dut =>
val nums = (0 until dut.size).map { _ => 0.1 }
nums.zipWithIndex.foreach { case (num, index) =>
dut.in(index).poke(num)
}
dut.clock.step()
val _ = dut.sum.peekBigDecimal()
dut.sum.expect(BigDecimal("1.000000000000000052041704279304213"))
}
}

"fixed point reduce should fail without BigDecimal" in {
val e = intercept[ChiselException] {
test(new FixedPointReduce(FixedPoint(70.W, 60.BP), 10)) { dut =>
val nums = (0 until dut.size).map { _ => 0.1 }
nums.zipWithIndex.foreach { case (num, index) =>
dut.in(index).poke(num)
}
dut.clock.step()
// The following should generate a ChiselException, losing precision trying to represent a value as a Double.
val _ = dut.sum.peekDouble()
}
}
assert(e.getMessage.contains("is too big, precision lost with > 53 bits"))
}

private def testFixedPointDivide(dut: FixedPointDivide): Unit = {
for(d <- BigDecimal(0.0) to BigDecimal(15.0) by BigDecimal(1.0 / 3.0)) {
dut.in.poke(d.toDouble)
dut.clock.step()
val _ = dut.out.peekDouble()
dut.out.expect(d.toDouble / 4.0)
}
}

"with enough bits fixed point pseudo divide should work" in {
test(new FixedPointDivide(FixedPoint(64.W, 32.BP), 2))(testFixedPointDivide)
}

"not enough bits and fixed point pseudo divide will not work" in {
assertThrows[TestFailedException] {
test(new FixedPointDivide(FixedPoint(10.W, 4.BP), 2))(testFixedPointDivide)
}
}

private def testFixedIsWhole(dut: FixedIsWhole): Unit = {
for(i <- BigDecimal(-2.75) to BigDecimal(1.75) by 0.25) {
dut.in.poke(i.toDouble)
dut.clock.step()
val _ = dut.out.peek()
dut.out.expect(i.isWhole)
}
}

"FixedPoint width 16 succeeds on verilator" taggedAs RequiresVerilator in {
test(new FixedIsWhole(16)).withAnnotations(Seq(VerilatorBackendAnnotation))(testFixedIsWhole)
}

"FixedPoint width 15 succeeds on verilator" taggedAs RequiresVerilator in {
test(new FixedIsWhole(15)).withAnnotations(Seq(VerilatorBackendAnnotation))(testFixedIsWhole)
}

"FixedPoint width 15 succeeds on treadle" in {
test(new FixedIsWhole(15))(testFixedIsWhole)
}
}


/** Regression test which used to fail due to extra high bits being poked. */
private class FixedIsWhole(w: Int) extends Module {
val in = IO(Input(FixedPoint(w.W, 2.BP)))
val out = IO(Output(Bool()))
val lsbsChopped = in.setBinaryPoint(0)
val lsbsZeroed = (lsbsChopped << 2).asFixedPoint(2.BP)
out := lsbsZeroed === in
}

private class FixedPointReduce(fixedType: FixedPoint, val size: Int) extends Module {
val in = IO(Input(Vec(size, fixedType)))
val sum = IO(Output(fixedType))
sum := in.reduce(_ + _)
}

private class FixedPointDivide(val fixedType: FixedPoint, val shiftAmount: Int) extends Module {
val in = IO(Input(fixedType))
val out = IO(Output(fixedType))
out := (in.asUInt >> shiftAmount).asFixedPoint(fixedType.binaryPoint)
}