diff --git a/README.md b/README.md index b8c3218..9bd40c6 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,7 @@ Concretely, this project contains: - Being able to work with the detailed bundles inside your components - Compliant with [Tydi-standard](https://abs-tudelft.github.io/tydi/specification/physical.html) for communication with components created outside of Chisel - Simple stream connection + - Stream compatibility checks - Helper functions for common signal use-cases - A stream-processing component chaining syntax - Testing utilities @@ -252,7 +253,6 @@ Concretely, this project contains: - Create interoperability with [Fletcher](https://github.com/abs-tudelft/fletcher) - Investigate adoption of other hardware description languages - Library - - Stream compatibility checks - Better Union support - Interoperability with other streaming standards - Improved error handling and design rule checks diff --git a/library/src/main/scala/nl/tudelft/tydi_chisel/TydiLib.scala b/library/src/main/scala/nl/tudelft/tydi_chisel/TydiLib.scala index d321c22..b4ae148 100644 --- a/library/src/main/scala/nl/tudelft/tydi_chisel/TydiLib.scala +++ b/library/src/main/scala/nl/tudelft/tydi_chisel/TydiLib.scala @@ -198,6 +198,17 @@ object BitsEl { def apply(width: Width): BitsEl = new BitsEl(width) } +object CompatCheck extends Enumeration { + val Params, Strict = Value +} + +object CompatCheckResult extends Enumeration { + val Warning, Error = Value +} + +final case class TydiStreamCompatException(private val message: String = "", private val cause: Throwable = None.orNull) + extends Exception(message, cause) + /** * Physical stream signal definitions. * @param e Element type @@ -206,9 +217,11 @@ object BitsEl { * @param c Complexity * @param u User signals */ -abstract class PhysicalStreamBase(private val e: TydiEl, val n: Int, val d: Int, val c: Int, private val u: Data) +sealed abstract class PhysicalStreamBase(private val e: TydiEl, val n: Int, val d: Int, val c: Int, private val u: Data) extends TydiEl { override val isStream: Boolean = true + override val elWidth: Int = e.getDataElementsRec.map(_.getWidth).sum + val userElWidth: Int = u.getWidth require(n >= 1) require(1 <= c && c <= 8) @@ -320,6 +333,157 @@ abstract class PhysicalStreamBase(private val e: TydiEl, val n: Int, val d: Int, /** [[strb]] signal as a boolean vector */ def strbVec: Vec[Bool] = VecInit(strb.asBools) + protected def printWarning(message: String): Unit = { + // ANSI escape codes for bold and orange text + val bold = "\u001b[1m" + val orange = "\u001b[38;5;214m" + val reset = "\u001b[0m" + + // Print the formatted message + Console.err.println(s"$bold$orange$message$reset") + } + + protected def reportProblem(problemStr: String, compatCheckResult: CompatCheckResult.Value): Unit = { + compatCheckResult match { + case CompatCheckResult.Error => throw TydiStreamCompatException(problemStr) + case CompatCheckResult.Warning => printWarning(problemStr) + } + } + + /** + * Check if the parameters of a source and sink stream match. + * @param toConnect Source stream to drive this stream with. + */ + def paramCheck(toConnect: PhysicalStreamBase, compatCheckResult: CompatCheckResult.Value): Unit = { + // Number of lanes should be the same + if (toConnect.n != this.n) { + reportProblem( + s"Number of lanes between source and sink is not equal. ${this} has n=${this.n}, ${toConnect + .toString()} has n=${toConnect.n}", + compatCheckResult + ) + } + // Dimensionality should be the same + if (toConnect.d != this.d) { + reportProblem( + s"Dimensionality of source and sink is not equal. ${this} has d=${this.d}, ${toConnect} has d=${toConnect.d}", + compatCheckResult + ) + } + // Sink C >= source C for compatibility + if (toConnect.c > this.c) { + reportProblem( + s"Complexity of source stream > sink. ${this} has c=${this.c}, ${toConnect} has c=${toConnect.c}", + compatCheckResult + ) + } + } + + def elementCheck(toConnect: PhysicalStreamBase, compatCheckResult: CompatCheckResult.Value): Unit = { + if (this.elWidth != toConnect.elWidth) { + reportProblem( + s"Size of stream elements is not equal. ${this} has |e|=${this.elWidth}, ${toConnect} has |e|=${toConnect.elWidth}", + compatCheckResult + ) + } + if (this.userElWidth != toConnect.userElWidth) { + reportProblem( + s"Size of stream elements is not equal. ${this} has |u|=${this.userElWidth}, ${toConnect} has |u|=${toConnect.userElWidth}", + compatCheckResult + ) + } + } + + def elementCheckTyped( + toConnect: PhysicalStreamBase, + typeCheck: CompatCheck.Value, + compatCheckResult: CompatCheckResult.Value + ): Unit = { + if (typeCheck == CompatCheck.Strict) { + if (this.getDataType.getClass != toConnect.getDataType.getClass) { + reportProblem( + s"Type of stream elements is not equal. ${this} has e=${this.getDataType.getClass}, ${toConnect} has e=${toConnect.getDataType.getClass}", + compatCheckResult + ) + } + if (this.getUserType.getClass != toConnect.getUserType.getClass) { + reportProblem( + s"Type of user elements is not equal. ${this} has u=${this.getUserType.getClass}, ${toConnect} has u=${toConnect.getUserType.getClass}", + compatCheckResult + ) + } + } else { + elementCheck(toConnect, compatCheckResult) + } + } + + /** + * Meta-connect function. Connects all metadata signals but not the data or user signals. + * @param bundle Source stream to drive this stream with. + * @param compatCheckResult Whether to report stream compatibility issues as errors or warnings. + */ + def :@=( + bundle: PhysicalStreamBase + )(implicit compatCheckResult: CompatCheckResult.Value = CompatCheckResult.Error): Unit = { + paramCheck(bundle, compatCheckResult) + // This could be done with a :<>= but I like being explicit here to catch possible errors. + this.endi := bundle.endi + this.stai := bundle.stai + this.strb := bundle.strb + this.valid := bundle.valid + bundle.ready := this.ready + + if (this.d > 0) { + (this.last, bundle.last) match { + case (_: UInt, _: UInt) | (_: Vec[_], _: Vec[_]) => this.last := bundle.last + case (_: UInt, _: Vec[_]) => this.last := bundle.last.asUInt + case (thisLast: Vec[_], bundleLast: UInt) => + for ((lastLane, i) <- thisLast.zipWithIndex) { + // Assign a slice of the bitvector to the respective lane in the vector + lastLane := bundleLast((i + 1) * d - 1, i * d) + } + } + } else { + this.last := DontCare + bundle.last := DontCare + } + } + + /** + * Shortcut for stream mounting with weak type check. + * @param bundle Source stream to drive this stream with. + * @param errorReporting Whether to report stream compatibility issues as errors or warnings. + */ + def :~=( + bundle: PhysicalStreamBase + )(implicit errorReporting: CompatCheckResult.Value = CompatCheckResult.Error): Unit = { + implicit val errorReporting: CompatCheck.Value = CompatCheck.Params + this := bundle + } + + /** + * Stream mounting function. + * @param bundle Source stream to drive this stream with. + * @param errorReporting Whether to report stream compatibility issues as errors or warnings. + * @param typeCheck Whether to conduct a strong or weak type check. A weak type check only verifies the number of bits. + */ + def :=(bundle: PhysicalStreamBase)(implicit + typeCheck: CompatCheck.Value = CompatCheck.Strict, + errorReporting: CompatCheckResult.Value = CompatCheckResult.Error + ): Unit = { + this :@= bundle // Connect meta signals and check parameters + elementCheckTyped(bundle, typeCheck, errorReporting) // Check data types + // Call the right connect method + (this, bundle) match { + case (x: PhysicalStream, y: PhysicalStream) => x.connectSimple(y, typeCheck, errorReporting) + case (x: PhysicalStream, y: PhysicalStreamDetailed[_, _]) => x.connectDetailed(y, typeCheck, errorReporting) + case (x: PhysicalStreamDetailed[_, _], y: PhysicalStream) => x.connectSimple(y, typeCheck, errorReporting) + case (x: PhysicalStreamDetailed[_, _], y: PhysicalStreamDetailed[_, _]) => + x.connectDetailed(y, typeCheck, errorReporting) + case _ => throw new Exception("Could not determine data connection method.") + } + } + def tydiCode: String = { val elName = e.fingerprint val usName = u.fingerprint @@ -355,13 +519,11 @@ abstract class PhysicalStreamBase(private val e: TydiEl, val n: Int, val d: Int, * @param c Complexity * @param u User signals */ -class PhysicalStream(private val e: TydiEl, n: Int = 1, d: Int = 0, c: Int, private val u: Data = Null()) +class PhysicalStream(private val e: TydiEl, n: Int = 1, d: Int = 1, c: Int, private val u: Data = Null()) extends PhysicalStreamBase(e, n, d, c, u) { - override val elWidth: Int = e.getDataElementsRec.map(_.getWidth).sum - val userElWidth: Int = u.getWidth - val data: UInt = Output(UInt((elWidth * n).W)) - val user: UInt = Output(UInt(userElWidth.W)) - val last: UInt = Output(UInt(lastWidth.W)) + val data: UInt = Output(UInt((elWidth * n).W)) + val user: UInt = Output(UInt(userElWidth.W)) + val last: UInt = Output(UInt(lastWidth.W)) def lastVec: Vec[UInt] = { if (d > 0) @@ -370,15 +532,17 @@ class PhysicalStream(private val e: TydiEl, n: Int = 1, d: Int = 0, c: Int, priv VecInit.tabulate(n)(i => 0.U(0.W)) } - // Stream mounting function - def :=[Tel <: TydiEl, Tus <: Data](bundle: PhysicalStreamDetailed[Tel, Tus]): Unit = { - // This could be done with a :<>= but I like being explicit here to catch possible errors. - this.endi := bundle.endi - this.stai := bundle.stai - this.strb := bundle.strb - this.last := bundle.last.asUInt - this.valid := bundle.valid - bundle.ready := this.ready + /** + * Stream mounting function. + * @param bundle Source stream to drive this stream with. + * @tparam Tel Element signal type. + * @tparam Tus User signal type. + */ + private[tydi_chisel] def connectDetailed[Tel <: TydiEl, Tus <: Data]( + bundle: PhysicalStreamDetailed[Tel, Tus], + typeCheck: CompatCheck.Value = CompatCheck.Strict, + errorReporting: CompatCheckResult.Value + ): Unit = { if (elWidth > 0) { this.data := bundle.getDataConcat } else { @@ -391,16 +555,17 @@ class PhysicalStream(private val e: TydiEl, n: Int = 1, d: Int = 0, c: Int, priv } } - def :=(bundle: PhysicalStream): Unit = { - // This could be done with a :<>= but I like being explicit here to catch possible errors. - this.endi := bundle.endi - this.stai := bundle.stai - this.strb := bundle.strb - this.last := bundle.last - this.valid := bundle.valid - bundle.ready := this.ready - this.data := bundle.data - this.user := bundle.user + /** + * Stream mounting function. + * @param bundle Source stream to drive this stream with. + */ + private[tydi_chisel] def connectSimple( + bundle: PhysicalStream, + typeCheck: CompatCheck.Value, + errorReporting: CompatCheckResult.Value + ): Unit = { + this.data := bundle.data + this.user := bundle.user } def processWith[T <: SubProcessorSignalDef](module: => T)(implicit parentModule: TydiModuleMixin): PhysicalStream = { @@ -428,7 +593,7 @@ class PhysicalStream(private val e: TydiEl, n: Int = 1, d: Int = 0, c: Int, priv } object PhysicalStream { - def apply(e: TydiEl, n: Int = 1, d: Int = 0, c: Int, u: Data = Null()): PhysicalStream = + def apply(e: TydiEl, n: Int = 1, d: Int = 1, c: Int, u: Data = Null()): PhysicalStream = new PhysicalStream(e, n, d, c, u) } @@ -446,7 +611,7 @@ object PhysicalStream { class PhysicalStreamDetailed[Tel <: TydiEl, Tus <: Data]( private val e: Tel, n: Int = 1, - d: Int = 0, + d: Int = 1, c: Int, var r: Boolean = false, private val u: Tus = Null() @@ -520,72 +685,46 @@ class PhysicalStreamDetailed[Tel <: TydiEl, Tus <: Data]( io } - // Stream mounting function - def :=[TBel <: TydiEl, TBus <: Data](bundle: PhysicalStreamDetailed[TBel, TBus]): Unit = { - // This could be done with a :<>= but I like being explicit here to catch possible errors. - if (bundle.r && !this.r) { - this.endi := bundle.endi - this.stai := bundle.stai - this.strb := bundle.strb - this.last := bundle.last - this.valid := bundle.valid - bundle.ready := this.ready - // The following would work if we would know with certainty that the signals are oriented the right way, - // but we do not -.- - // (this.data: Data) :<>= (bundle.data: Data) - - // Using the recursive function leads to duplicate connections when connecting sub-streams, but the non-recursive - // version cannot be used, since non-stream elements could still contain stream items. - for ((thisData, bundleData) <- this.getDataElementsRec.zip(bundle.getDataElementsRec)) { - thisData :<>= bundleData - } - for ( - (thisStream: PhysicalStreamDetailed[_, _], bundleStream: PhysicalStreamDetailed[_, _]) <- this.getStreamElements - .zip(bundle.getStreamElements) - ) { - thisStream := bundleStream - } - (this.user: Data) :<>= (bundle.user: Data) - } else { - bundle.endi := this.endi - bundle.stai := this.stai - bundle.strb := this.strb - bundle.last := this.last - bundle.valid := this.valid - this.ready := bundle.ready - // The following would work if we would know with certainty that the signals are oriented the right way, - // but we do not -.- - // (bundle.data: Data) :<>= (this.data: Data) - - // Using the recursive function leads to duplicate connections when connecting sub-streams, but the non-recursive - // version cannot be used, since non-stream elements could still contain stream items. - for ((thisData, bundleData) <- this.getDataElementsRec.zip(bundle.getDataElementsRec)) { - bundleData :<>= thisData - } - for ( - (thisStream: PhysicalStreamDetailed[_, _], bundleStream: PhysicalStreamDetailed[_, _]) <- this.getStreamElements - .zip(bundle.getStreamElements) - ) { - bundleStream := thisStream - } - (bundle.user: Data) :<>= (this.user: Data) + // Require the element and user signal types to be the same as this stream in the function signature. + /** + * Stream mounting function. + * @param bundle Source stream to drive this stream with. + */ + private[tydi_chisel] def connectDetailed[TBel <: TydiEl, TBus <: Data]( + bundle: PhysicalStreamDetailed[TBel, TBus], + typeCheck: CompatCheck.Value = CompatCheck.Strict, + errorReporting: CompatCheckResult.Value + ): Unit = { + if (!bundle.r || this.r) { + throw new Exception("Attempting to connect an input to an output. Reverse the connection.") + } + // The following would work if we would know with certainty that the signals are oriented the right way, + // but we do not -.- + // (this.data: Data) :<>= (bundle.data: Data) + + // Using the recursive function leads to duplicate connections when connecting sub-streams, but the non-recursive + // version cannot be used, since non-stream elements could still contain stream items. + for ((thisData, bundleData) <- this.getDataElementsRec.zip(bundle.getDataElementsRec)) { + thisData :<>= bundleData + } + for ( + (thisStream: PhysicalStreamDetailed[_, _], bundleStream: PhysicalStreamDetailed[_, _]) <- this.getStreamElements + .zip(bundle.getStreamElements) + ) { + thisStream := bundleStream } + (this.user: Data) :<>= (bundle.user: Data) } - def :=(bundle: PhysicalStream): Unit = { - this.endi := bundle.endi - this.stai := bundle.stai - this.strb := bundle.strb - // There are only last bits if there is dimensionality - if (d > 0) { - for ((lastLane, i) <- this.last.zipWithIndex) { - lastLane := bundle.last((i + 1) * d - 1, i * d) - } - } else { - this.last := DontCare - } - this.valid := bundle.valid - bundle.ready := this.ready + /** + * Stream mounting function. + * @param bundle Source stream to drive this stream with. + */ + private[tydi_chisel] def connectSimple( + bundle: PhysicalStream, + typeCheck: CompatCheck.Value = CompatCheck.Strict, + errorReporting: CompatCheckResult.Value + ): Unit = { // Connect data bitvector back to bundle for ((dataLane, i) <- this.data.zipWithIndex) { val dataWidth = bundle.elWidth @@ -611,7 +750,7 @@ object PhysicalStreamDetailed { def apply[Tel <: TydiEl, Tus <: Data]( e: Tel, n: Int = 1, - d: Int = 0, + d: Int = 1, c: Int, r: Boolean = false, u: Tus = Null() diff --git a/library/src/main/scala/nl/tudelft/tydi_chisel/examples/timestamped_message/TimestampedMessage.scala b/library/src/main/scala/nl/tudelft/tydi_chisel/examples/timestamped_message/TimestampedMessage.scala index c5919b4..943cf76 100644 --- a/library/src/main/scala/nl/tudelft/tydi_chisel/examples/timestamped_message/TimestampedMessage.scala +++ b/library/src/main/scala/nl/tudelft/tydi_chisel/examples/timestamped_message/TimestampedMessage.scala @@ -24,7 +24,7 @@ class TimestampedMessageBundle extends Group { val a: UInt = UInt(8.W) val b: Bool = Bool() }*/ - val message = new PhysicalStreamDetailed(BitsEl(charWidth), n = 3, d = 1, c = 7) + val message = new PhysicalStreamDetailed(BitsEl(charWidth), n = 3, d = 2, c = 7) } // Declaration of the module @@ -67,7 +67,7 @@ class TimestampedMessageModuleOut extends TydiModule { } class TimestampedMessageModuleIn extends TydiModule { - val io1 = IO(Flipped(new PhysicalStream(new TimestampedMessageBundle, n = 1, d = 2, c = 7, u = new Null()))) + val io1 = IO(Flipped(new PhysicalStream(new TimestampedMessageBundle, n = 1, d = 1, c = 7, u = new Null()))) val io2 = IO(Flipped(new PhysicalStream(BitsEl(8.W), n = 3, d = 2, c = 7, u = new Null()))) io1 :<= DontCare io1.ready := DontCare diff --git a/testing/src/test/scala/nl/tudelft/tydi_chisel/StreamCompatCheckTest.scala b/testing/src/test/scala/nl/tudelft/tydi_chisel/StreamCompatCheckTest.scala new file mode 100644 index 0000000..d1676e1 --- /dev/null +++ b/testing/src/test/scala/nl/tudelft/tydi_chisel/StreamCompatCheckTest.scala @@ -0,0 +1,116 @@ +package nl.tudelft.tydi_chisel + +import chisel3._ +import chiseltest._ +import org.scalatest.flatspec.AnyFlatSpec + +class StreamCompatCheckTest extends AnyFlatSpec with ChiselScalatestTester { + class MyBundle extends Group { + val a: UInt = UInt(8.W) + val b: Bool = Bool() + } + + class MyBundle2 extends MyBundle + + class StreamConnectMod( + in: PhysicalStream, + out: PhysicalStream, + typeCheckSelect: CompatCheck.Value = CompatCheck.Strict, + errorReporting: CompatCheckResult.Value = CompatCheckResult.Error + ) extends TydiModule { + val inStream: PhysicalStream = IO(Flipped(in)) + val outStream: PhysicalStream = IO(out) + + { + implicit val typeCheckImplicit: CompatCheck.Value = typeCheckSelect + implicit val typeCheckResultImplicit: CompatCheckResult.Value = errorReporting + outStream := inStream + } + } + + class DetailedStreamConnectMod[TIel <: TydiEl, TIus <: Data, TOel <: TydiEl, TOus <: Data]( + in: PhysicalStreamDetailed[TIel, TIus], + out: PhysicalStreamDetailed[TOel, TOus], + typeCheckSelect: CompatCheck.Value = CompatCheck.Strict + ) extends TydiModule { + val inStream: PhysicalStreamDetailed[TIel, TIus] = IO(Flipped(in)).flip + val outStream: PhysicalStreamDetailed[TOel, TOus] = IO(out) + + { + implicit val typeCheckImplicit: CompatCheck.Value = typeCheckSelect + outStream := inStream + } + } + + class DataBundle extends Bundle { + val c: UInt = UInt(10.W) + val d: Bool = Bool() + } + + private val myBundleStream = new PhysicalStreamDetailed(new MyBundle, c = 8) + private val myBundle2Stream = new PhysicalStreamDetailed(new MyBundle2, c = 8) + + behavior of "Stream compatibility check" + + it should "check type" in { + test(new DetailedStreamConnectMod(myBundleStream, myBundleStream)) { _ => } + intercept[TydiStreamCompatException] { + test(new DetailedStreamConnectMod(myBundleStream, myBundle2Stream)) { _ => } + } + intercept[TydiStreamCompatException] { + test(new StreamConnectMod(PhysicalStream(new MyBundle, c = 1), PhysicalStream(new MyBundle2, c = 1))) { _ => } + } + } + + it should "weak check type" in { + test(new DetailedStreamConnectMod(myBundleStream, myBundle2Stream, CompatCheck.Params)) { _ => } + test( + new StreamConnectMod( + PhysicalStream(new MyBundle, c = 1), + PhysicalStream(new MyBundle2, c = 1), + CompatCheck.Params + ) + ) { _ => } + } + + it should "check parameters" in { + val baseStream = PhysicalStream(new MyBundle, n = 1, d = 1, c = 1, new DataBundle) + + // Same parameters + test(new StreamConnectMod(baseStream, PhysicalStream(new MyBundle, n = 1, d = 1, c = 1, new DataBundle))) { _ => } + + // Test unequal n parameter + intercept[TydiStreamCompatException] { + test(new StreamConnectMod(baseStream, PhysicalStream(new MyBundle, n = 2, d = 1, c = 1, new DataBundle))) { _ => } + } + + // Test unequal d parameter + intercept[TydiStreamCompatException] { + test(new StreamConnectMod(baseStream, PhysicalStream(new MyBundle, n = 1, d = 2, c = 1, new DataBundle))) { _ => } + } + + // Test c parameter inequality. Csink >= Csource is okay, else an exception is thrown. + test(new StreamConnectMod(baseStream, PhysicalStream(new MyBundle, n = 1, d = 1, c = 7, new DataBundle))) { _ => } + intercept[TydiStreamCompatException] { + test(new StreamConnectMod(PhysicalStream(new MyBundle, n = 1, d = 2, c = 7, new DataBundle), baseStream)) { _ => } + } + } + + it should "print warnings" in { + val baseStream = PhysicalStream(new MyBundle, n = 1, d = 1, c = 1, new DataBundle) + + // Create a module with warning output and give it incompatible streams. + val stream = new java.io.ByteArrayOutputStream() + Console.withErr(stream) { + test( + new StreamConnectMod( + baseStream, + PhysicalStream(new MyBundle, n = 2, d = 1, c = 1, new DataBundle), + errorReporting = CompatCheckResult.Warning + ) + ) { _ => } + } + // Check if error was written to console + assert(stream.toString contains "Number of lanes between source and sink is not equal.") + } +} diff --git a/testing/src/test/scala/nl/tudelft/tydi_chisel/utils/ComplexityConverterTest.scala b/testing/src/test/scala/nl/tudelft/tydi_chisel/utils/ComplexityConverterTest.scala index ff6ca09..a935f06 100644 --- a/testing/src/test/scala/nl/tudelft/tydi_chisel/utils/ComplexityConverterTest.scala +++ b/testing/src/test/scala/nl/tudelft/tydi_chisel/utils/ComplexityConverterTest.scala @@ -200,7 +200,7 @@ class ComplexityConverterTest extends AnyFlatSpec with ChiselScalatestTester { } it should "work with fancy wrapper" in { - val stream = PhysicalStream(new MyEl, n = 1, d = 1, c = 7) + val stream = PhysicalStream(new MyEl, n = 1, d = 1, c = 8) // test case body here test(new ManualComplexityConverterFancyWrapper(new MyEl, stream, 10)) { c =>