From 0e2dce1fefc55040d9e4b610d433154194595ee3 Mon Sep 17 00:00:00 2001 From: zhutmost Date: Wed, 23 Sep 2026 21:23:38 +0800 Subject: [PATCH 01/26] feat!: prepare scala3 environment --- .scalafmt.conf | 3 +- build.mill | 53 +-- chipmunk/resources/regneg/Chipmunk_RegNeg.sv | 2 + .../resources/regneg/Chipmunk_RegNegInit.sv | 16 +- chipmunk/src/DataExtensions.scala | 64 +++ chipmunk/src/IsMasterSlave.scala | 73 ++-- chipmunk/src/MapBundle.scala | 35 -- chipmunk/src/Misc.scala | 14 + chipmunk/src/MuxPriorityDefault.scala | 36 -- chipmunk/src/RegNeg.scala | 213 ++++++---- chipmunk/src/ResetSync.scala | 80 ---- chipmunk/src/StateMachine.scala | 266 ------------ chipmunk/src/VerilogIO.scala | 71 ---- chipmunk/src/amba/ApbIO.scala | 66 --- chipmunk/src/amba/ApbVerilogIO.scala | 59 --- chipmunk/src/amba/AxiEnum.scala | 28 -- chipmunk/src/amba/AxiIO.scala | 135 ------ chipmunk/src/amba/AxiLiteIO.scala | 74 ---- chipmunk/src/amba/AxiLiteToAxi4Bridge.scala | 49 --- chipmunk/src/amba/AxiLiteVerilogIO.scala | 77 ---- chipmunk/src/amba/AxiVerilogIO.scala | 224 ---------- chipmunk/src/component/acorn/AcornIO.scala | 67 --- .../acorn/AcornToAxiLiteBridge.scala | 48 --- .../component/acorn/Axi4ToAcornBridge.scala | 193 --------- chipmunk/src/component/spi/SpiDebugger.scala | 304 -------------- chipmunk/src/component/spi/SpiIO.scala | 21 - chipmunk/src/package.scala | 70 ---- chipmunk/src/regbank/RegBank.scala | 216 ---------- chipmunk/src/regbank/RegField.scala | 136 ------ chipmunk/src/regbank/RegFieldAccessType.scala | 309 -------------- chipmunk/src/stream/Flow.scala | 202 --------- chipmunk/src/stream/FlowFork.scala | 113 ----- chipmunk/src/stream/Stream.scala | 386 ------------------ chipmunk/src/stream/StreamArbiter.scala | 43 -- chipmunk/src/stream/StreamDelay.scala | 103 ----- chipmunk/src/stream/StreamDemux.scala | 47 --- chipmunk/src/stream/StreamFork.scala | 152 ------- chipmunk/src/stream/StreamJoin.scala | 74 ---- chipmunk/src/stream/StreamMux.scala | 42 -- chipmunk/src/stream/StreamQueue.scala | 58 --- chipmunk/src/stream/package.scala | 16 - chipmunk/src/tester/TesterAPI.scala | 39 -- chipmunk/test/resources/amba/NicExample0.sv | 132 ------ chipmunk/test/resources/amba/NicExample1.sv | 136 ------ chipmunk/test/resources/amba/NicExample2.sv | 75 ---- chipmunk/test/resources/amba/NicExample3.sv | 44 -- chipmunk/test/src/BaseSpec.scala | 15 - chipmunk/test/src/BitsSpec.scala | 58 --- chipmunk/test/src/IsMasterSlaveSpec.scala | 65 --- .../test/src/MuxPriorityDefaultSpec.scala | 41 -- chipmunk/test/src/RegBankSpec.scala | 130 ------ chipmunk/test/src/StateMachineSpec.scala | 165 -------- chipmunk/test/src/TesterSpec.scala | 75 ---- chipmunk/test/src/amba/ApbVerilogIOSpec.scala | 57 --- .../test/src/amba/AxiLiteVerilogIOSpec.scala | 50 --- chipmunk/test/src/amba/AxiVerilogIOSpec.scala | 91 ----- .../test/src/stream/StreamArbiterSpec.scala | 52 --- .../test/src/stream/StreamDelaySpec.scala | 98 ----- chipmunk/test/src/stream/StreamIOSpec.scala | 24 -- mylib/src/RtlEmitter.scala | 32 +- 60 files changed, 312 insertions(+), 5305 deletions(-) create mode 100644 chipmunk/src/DataExtensions.scala delete mode 100644 chipmunk/src/MapBundle.scala create mode 100644 chipmunk/src/Misc.scala delete mode 100644 chipmunk/src/MuxPriorityDefault.scala delete mode 100644 chipmunk/src/ResetSync.scala delete mode 100644 chipmunk/src/StateMachine.scala delete mode 100644 chipmunk/src/VerilogIO.scala delete mode 100644 chipmunk/src/amba/ApbIO.scala delete mode 100644 chipmunk/src/amba/ApbVerilogIO.scala delete mode 100644 chipmunk/src/amba/AxiEnum.scala delete mode 100644 chipmunk/src/amba/AxiIO.scala delete mode 100644 chipmunk/src/amba/AxiLiteIO.scala delete mode 100644 chipmunk/src/amba/AxiLiteToAxi4Bridge.scala delete mode 100644 chipmunk/src/amba/AxiLiteVerilogIO.scala delete mode 100644 chipmunk/src/amba/AxiVerilogIO.scala delete mode 100644 chipmunk/src/component/acorn/AcornIO.scala delete mode 100644 chipmunk/src/component/acorn/AcornToAxiLiteBridge.scala delete mode 100644 chipmunk/src/component/acorn/Axi4ToAcornBridge.scala delete mode 100644 chipmunk/src/component/spi/SpiDebugger.scala delete mode 100644 chipmunk/src/component/spi/SpiIO.scala delete mode 100644 chipmunk/src/package.scala delete mode 100644 chipmunk/src/regbank/RegBank.scala delete mode 100644 chipmunk/src/regbank/RegField.scala delete mode 100644 chipmunk/src/regbank/RegFieldAccessType.scala delete mode 100644 chipmunk/src/stream/Flow.scala delete mode 100644 chipmunk/src/stream/FlowFork.scala delete mode 100644 chipmunk/src/stream/Stream.scala delete mode 100644 chipmunk/src/stream/StreamArbiter.scala delete mode 100644 chipmunk/src/stream/StreamDelay.scala delete mode 100644 chipmunk/src/stream/StreamDemux.scala delete mode 100644 chipmunk/src/stream/StreamFork.scala delete mode 100644 chipmunk/src/stream/StreamJoin.scala delete mode 100644 chipmunk/src/stream/StreamMux.scala delete mode 100644 chipmunk/src/stream/StreamQueue.scala delete mode 100644 chipmunk/src/stream/package.scala delete mode 100644 chipmunk/src/tester/TesterAPI.scala delete mode 100644 chipmunk/test/resources/amba/NicExample0.sv delete mode 100644 chipmunk/test/resources/amba/NicExample1.sv delete mode 100644 chipmunk/test/resources/amba/NicExample2.sv delete mode 100644 chipmunk/test/resources/amba/NicExample3.sv delete mode 100644 chipmunk/test/src/BaseSpec.scala delete mode 100644 chipmunk/test/src/BitsSpec.scala delete mode 100644 chipmunk/test/src/IsMasterSlaveSpec.scala delete mode 100644 chipmunk/test/src/MuxPriorityDefaultSpec.scala delete mode 100644 chipmunk/test/src/RegBankSpec.scala delete mode 100644 chipmunk/test/src/StateMachineSpec.scala delete mode 100644 chipmunk/test/src/TesterSpec.scala delete mode 100644 chipmunk/test/src/amba/ApbVerilogIOSpec.scala delete mode 100644 chipmunk/test/src/amba/AxiLiteVerilogIOSpec.scala delete mode 100644 chipmunk/test/src/amba/AxiVerilogIOSpec.scala delete mode 100644 chipmunk/test/src/stream/StreamArbiterSpec.scala delete mode 100644 chipmunk/test/src/stream/StreamDelaySpec.scala delete mode 100644 chipmunk/test/src/stream/StreamIOSpec.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index 2f12d19..e9e04df 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,7 +1,6 @@ version = "3.11.5" -runner.dialect = "scala213" +runner.dialect = "scala3" -encoding = "UTF-8" project.git = true preset=IntelliJ diff --git a/build.mill b/build.mill index e749959..a48cc6c 100644 --- a/build.mill +++ b/build.mill @@ -2,57 +2,46 @@ package build import mill.scalalib.* import mill.scalalib.publish.* -import mill.scalalib.scalafmt.* object Dependencies { - val scalaVersion = "2.13.16" + val scalaVersion = "3.8.4" - val chiselVersion = "7.4.0" + val chiselVersion = "7.15.0+10-33fe25e2-SNAPSHOT" val chisel = mvn"org.chipsalliance::chisel:$chiselVersion" val chiselPlugin = mvn"org.chipsalliance:::chisel-plugin:$chiselVersion" val scalaTest = mvn"org.scalatest::scalatest:3.2.20" } -trait ChiselCircuitsModule extends ScalaModule with ScalafmtModule { +trait ChiselModule extends ScalaModule { override def scalaVersion = Dependencies.scalaVersion override def scalacOptions = super.scalacOptions() ++ Seq( - "-deprecation", // Emit warning and location for usages of deprecated APIs. - "-explaintypes", // Explain type errors in more detail. - "-Werror", // Fail the compilation if there are any warnings. - "-feature", // Emit warning and location for usages of features that should be imported explicitly. - "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. - "-language:higherKinds", // Allow higher-kinded types - "-language:implicitConversions", // Allow definition of implicit functions called views - "-language:reflectiveCalls", // Allow reflective access to members of structural types - "-unchecked", // Enable additional warnings where generated code depends on assumptions. - - "-Xlint:_,-delayedinit-select", // Suppress DelayedInit warnings because it is still used by StateMachine. - "-Wdead-code", // Warn when dead code is identified. - "-Wextra-implicit", // Warn when more than one implicit parameter section is defined. - "-Wmacros:both", // Lints code before and after applying a macro - "-Wnumeric-widen", // Warn when numerics are widened. - "-Woctal-literal", // Warn on obsolete octal syntax. - "-Wunused:_", // Warn when imports, local and private vals, vars, defs, and types are unused. - - "-Ybackend-parallelism", - Math - .min(Runtime.getRuntime.availableProcessors(), 8) - .toString, // Enable parallelization — change to desired number! - "-Ycache-plugin-class-loader:last-modified", // Enables caching of classloaders for compiler plugins - "-Ycache-macro-class-loader:last-modified" // and macro definitions. This can lead to performance improvements. + "-encoding", + "UTF-8", + "-deprecation", + "-feature", + "-unchecked", + "-Werror", + "-explain-types", + + "-Wsafe-init", + "-Wunused:all", + "-Wimplausible-patterns" ) - override def mvnDeps = Seq(Dependencies.chisel) - override def scalacPluginMvnDeps = Seq(Dependencies.chiselPlugin) + override def repositories = + super.repositories() ++ Seq("https://central.sonatype.com/repository/maven-snapshots") + + override def mvnDeps = super.mvnDeps() ++ Seq(Dependencies.chisel) + override def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ Seq(Dependencies.chiselPlugin) object test extends ScalaTests with TestModule.ScalaTest { override def mvnDeps = super.mvnDeps() ++ Seq(Dependencies.scalaTest) } } -object chipmunk extends ChiselCircuitsModule with PublishModule { +object chipmunk extends ChiselModule with PublishModule { def publishVersion = "0.1-SNAPSHOT" def pomSettings = PomSettings( @@ -65,6 +54,6 @@ object chipmunk extends ChiselCircuitsModule with PublishModule { ) } -object mylib extends ChiselCircuitsModule { +object mylib extends ChiselModule { override def moduleDeps: Seq[ScalaModule] = Seq(chipmunk) } diff --git a/chipmunk/resources/regneg/Chipmunk_RegNeg.sv b/chipmunk/resources/regneg/Chipmunk_RegNeg.sv index d928a18..c6ba2a5 100644 --- a/chipmunk/resources/regneg/Chipmunk_RegNeg.sv +++ b/chipmunk/resources/regneg/Chipmunk_RegNeg.sv @@ -16,3 +16,5 @@ module Chipmunk_RegNeg #( end endmodule: Chipmunk_RegNeg + +`default_nettype wire diff --git a/chipmunk/resources/regneg/Chipmunk_RegNegInit.sv b/chipmunk/resources/regneg/Chipmunk_RegNegInit.sv index a004ef6..500e604 100644 --- a/chipmunk/resources/regneg/Chipmunk_RegNegInit.sv +++ b/chipmunk/resources/regneg/Chipmunk_RegNegInit.sv @@ -1,13 +1,13 @@ `default_nettype none module Chipmunk_RegNegInit #( - parameter int WIDTH = 1, - parameter string RESET_ASYNC = "true" + parameter int WIDTH = 1, + parameter bit RESET_ASYNC = 1'b1 + parameter logic [WIDTH-1:0] INIT = '0 )( input var logic clock, input var logic reset, input var logic en, - input var logic [WIDTH-1:0] init, input var logic [WIDTH-1:0] d, output var logic [WIDTH-1:0] q ); @@ -15,17 +15,19 @@ module Chipmunk_RegNegInit #( assign q = r; generate - if (RESET_ASYNC == "true") begin: g_reset_async + if (RESET_ASYNC) begin: g_async always_ff @(negedge clock, posedge reset) begin - if (reset) r <= init; + if (reset) r <= INIT; else if (en) r <= d; end - end else if (RESET_ASYNC == "false") begin: g_reset_sync + end else begin: g_sync always_ff @(negedge clock) begin - if (reset) r <= init; + if (reset) r <= INIT; else if (en) r <= d; end end endgenerate endmodule: Chipmunk_RegNegInit + +`default_nettype wire diff --git a/chipmunk/src/DataExtensions.scala b/chipmunk/src/DataExtensions.scala new file mode 100644 index 0000000..7d888bd --- /dev/null +++ b/chipmunk/src/DataExtensions.scala @@ -0,0 +1,64 @@ +package chipmunk + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.{Fill, PopCount} + +extension [T <: Bits](c: T) { + + /** Returns the `n` most significant bits as a UInt. */ + def msBits(n: Int = 1)(using SourceInfo): UInt = + c.head(n) + + /** Returns the `n` least significant bits as a UInt. */ + def lsBits(n: Int = 1)(using SourceInfo): UInt = + c.take(n) + + /** Returns the most significant bit as a Bool. */ + def msBit(using SourceInfo): Bool = + c.head(1).asBool + + /** Returns the least significant bit as a Bool. */ + def lsBit(using SourceInfo): Bool = + c(0) + + /** Returns the same type filled with every bit driven by `b`. + * + * `b` may be a hardware signal; it need not be a literal. + */ + def filledWith(b: Bool)(using SourceInfo): T = + Fill(c.getWidth, b).asTypeOf(c) + + /** Returns the same type filled with a constant Scala Boolean. */ + def filledWith(b: Boolean)(using SourceInfo): T = + c.filledWith(b.B) + + /** Returns the same type with every bit set to one. */ + def filledOnes(using SourceInfo): T = + c.filledWith(true.B) + + /** Returns the same type with every bit set to zero. */ + def filledZeros(using SourceInfo): T = + c.filledWith(false.B) + + /** Drives this signal with ones. */ + def assignOnes()(using SourceInfo): Unit = + c := c.filledOnes + + /** Drives this signal with zeros. */ + def assignZeros()(using SourceInfo): Unit = + c := c.filledZeros + + /** Returns true exactly when one bit of this signal is set. */ + def isOneHot(using SourceInfo): Bool = + PopCount.equalTo(1, c.asUInt) +} + +extension [T <: Data](c: T) { + + /** Marks bound hardware with Chisel's dontTouch annotation and returns the same value for chaining. Same as + * `chisel3.dontTouch(x)`. + */ + def dontTouch: T = + chisel3.dontTouch(c) +} diff --git a/chipmunk/src/IsMasterSlave.scala b/chipmunk/src/IsMasterSlave.scala index 332d6e3..f03b6fb 100644 --- a/chipmunk/src/IsMasterSlave.scala +++ b/chipmunk/src/IsMasterSlave.scala @@ -1,46 +1,63 @@ package chipmunk -import chisel3._ +import chisel3.* -/** Can be connected as master/slave interface. +/** Marks a Record whose fields have a declared master or slave orientation. * - * [[Bundle]] mixes this trait so that it can use [[Master]] and [[Slave]] to indicate the direction of transmission. - * The function [[isMaster]] needs to be set true or false to indicate whether the bundle is Master or Slave. + * `isMaster` describes how the fields are declared in this type. It does not inspect the effective direction of a + * bound Chisel port. * - * @example - * {{{ - * class SomeIO extends Bundle with IsMasterSlave { - * val otherPorts = Input(UInt(2.W)) - * override def isMaster: Boolean = true + * Use [[Master]] or [[Slave]] when placing this type in another interface. Subfields may themselves be wrapped with + * Master or Slave. + * + * Example: + * {{{ + * class SramIO extends Bundle with IsMasterSlave { + * override def isMaster: Boolean = false + * val addr = Input(UInt(16.W)) + * val data = Output(UInt(32.W)) * } - * }}} + * }}} */ trait IsMasterSlave { - this: Record {} => + this: Record => - /** Override this method to set the bundle as Master or Slave. */ + /** Whether this type's fields are declared from the master's perspective. */ def isMaster: Boolean - // A flag indicating whether this Bundle has been wrapped with Master/Slave(_). - private[chipmunk] var _wrapFlag: Option[Boolean] = None + // Guards against applying Master/Slave twice to the same Record instance. + // This is library bookkeeping, not Chisel direction information. + private[chipmunk] var _roleWrapped: Boolean = false } -/** Set a bundle as a master interface. */ -object Master { - def apply[T <: Record with IsMasterSlave](bundle: => T): T = { - require(bundle._wrapFlag.isEmpty, "Bundles cannot be nested-ly wrapped with Master/Slave(_).") - val b: T = if (bundle.isMaster) bundle else Flipped(bundle) - b._wrapFlag = Some(true) - b +private[chipmunk] object DirectedRecord { + + def apply[T <: Record & IsMasterSlave](record: => T, asMaster: Boolean): T = { + // A by-name argument must be evaluated exactly once. + val original = record + + require(!original._roleWrapped, "The same Record cannot be wrapped twice with Master/Slave.") + + val directed: T = + if (original.isMaster == asMaster) original + else Flipped(original) + + // Flipped may return a clone. Mark both the supplied instance and + // the returned instance so neither can be wrapped again directly. + original._roleWrapped = true + directed._roleWrapped = true + directed } } -/** Set a bundle as a slave interface. */ +/** Presents a [[Record]] from the master's perspective. */ +object Master { + def apply[T <: Record & IsMasterSlave](record: => T): T = + DirectedRecord(record, asMaster = true) +} + +/** Presents a [[Record]] from the slave's perspective. */ object Slave { - def apply[T <: Record with IsMasterSlave](bundle: => T): T = { - require(bundle._wrapFlag.isEmpty, "Bundles cannot be nested-ly wrapped with Master/Slave(_).") - val b: T = if (!bundle.isMaster) bundle else Flipped(bundle) - b._wrapFlag = Some(false) - b - } + def apply[T <: Record & IsMasterSlave](record: => T): T = + DirectedRecord(record, asMaster = false) } diff --git a/chipmunk/src/MapBundle.scala b/chipmunk/src/MapBundle.scala deleted file mode 100644 index 9ad49b9..0000000 --- a/chipmunk/src/MapBundle.scala +++ /dev/null @@ -1,35 +0,0 @@ -package chipmunk - -import chisel3._ -import chisel3.experimental.requireIsChiselType -import chisel3.reflect.DataMirror - -import scala.collection.immutable.SeqMap - -/** Base class of a bundle that can be indexed by a string. - * - * To create a named [[MapBundle]], extend this class and pass in pairs of keys and [[Data]] subtypes. The [[Data]] - * signals can then be accessed with the string keys. - * - * @note - * Extending this class as a anonymous class may result in compilation error. - * - * @example - * {{{ - * class MyBundle extends MapBundle[UInt]( - * "foo" -> UInt(8.W), - * "bar" -> UInt(16.W), - * ) - * val myBundle = Wire(new MyBundle) - * val foo = myBundle("foo") - * myBundle("bar") := 3.U - * }}} - */ -abstract class MapBundle[T <: Data](elts: (String, T)*) extends Record { - val elements = SeqMap(elts.map { case (field, elt) => - requireIsChiselType(elt) - field -> DataMirror.internal.chiselTypeClone(elt) - }: _*) - - def apply(key: String): T = elements(key) -} diff --git a/chipmunk/src/Misc.scala b/chipmunk/src/Misc.scala new file mode 100644 index 0000000..75c5513 --- /dev/null +++ b/chipmunk/src/Misc.scala @@ -0,0 +1,14 @@ +package chipmunk + +import chisel3.* +import chisel3.util.{PriorityMux} + +/** A Bundle with no payload fields. Useful as a typed empty payload. */ +final class EmptyBundle extends Bundle + +/** Alias for [[chisel3.util.PriorityMux]]. + * + * @note + * Chisel has many muxes named `MuxXxx`, but this is the only one that is named as `XxxMux`. + */ +val MuxPriority: PriorityMux.type = PriorityMux diff --git a/chipmunk/src/MuxPriorityDefault.scala b/chipmunk/src/MuxPriorityDefault.scala deleted file mode 100644 index 5496e42..0000000 --- a/chipmunk/src/MuxPriorityDefault.scala +++ /dev/null @@ -1,36 +0,0 @@ -package chipmunk - -import chisel3._ - -/** Builds a Mux tree under the assumption that multiple select signals can be enabled. Priority is given to the first - * select signal. If no select signal is enabled, the default value is returned. - * - * @see - * [[chisel3.util.PriorityMux]], [[MuxPriority]] - */ -object MuxPriorityDefault { - def apply[T <: Data](in: Seq[(Bool, T)], default: T): T = { - in.size match { - case 1 => - Mux(in.head._1, in.head._2, default) - case _ => - Mux(in.head._1, in.head._2, apply(in.tail, default)) - } - } - - def apply[T <: Data](sel: Seq[Bool], in: Seq[T], default: T): T = apply(sel.zip(in), default) - - def apply[T <: Data](sel: Bits, in: Seq[T], default: T): T = apply(in.indices.map(sel(_)), in, default) -} - -/** Returns the bit position of the least-significant high bit of the input bitvector. If no bits are high, the default - * value is returned. - * - * @see - * [[chisel3.util.PriorityEncoder]] - */ -object PriorityEncoderDefault { - def apply(in: Seq[Bool], default: UInt): UInt = MuxPriorityDefault(in.zipWithIndex.map(x => x._1 -> x._2.U), default) - - def apply(in: Bits, default: UInt): UInt = apply(in.asBools, default) -} diff --git a/chipmunk/src/RegNeg.scala b/chipmunk/src/RegNeg.scala index daeb979..83fe9fc 100644 --- a/chipmunk/src/RegNeg.scala +++ b/chipmunk/src/RegNeg.scala @@ -1,149 +1,216 @@ package chipmunk -import chisel3._ -import chisel3.experimental.requireIsHardware -import chisel3.util._ +import chisel3.* +import chisel3.experimental.{requireIsHardware, SourceInfo} +import chisel3.reflect.DataMirror -private[chipmunk] class RegNegBbox(width: Int) extends BlackBox(Map("WIDTH" -> width)) with HasBlackBoxResource { - val io = IO(new Bundle { +/** External module implementing a falling-edge register without reset. + * + * @param bitwidth + * bitwidth of the stored data; must be positive + */ +private[chipmunk] final class RegNegBbox(bitwidth: Int) extends ExtModule(Map("WIDTH" -> IntParam(BigInt(bitwidth)))) { + require(bitwidth > 0, "RegNegBbox requires a positive data bitwidth.") + + val io = FlatIO(new Bundle { val clock = Input(Clock()) val en = Input(Bool()) - val d = Input(UInt(width.W)) - val q = Output(UInt(width.W)) + val d = Input(UInt(bitwidth.W)) + val q = Output(UInt(bitwidth.W)) }) - override def desiredName = "Chipmunk_RegNeg" - addResource(f"/regneg/$desiredName.sv") + override def desiredName: String = "Chipmunk_RegNeg" + + addResource(s"/regneg/$desiredName.sv") } -private[chipmunk] class RegNegInitBbox(width: Int, isResetAsync: Boolean = true) - extends BlackBox(Map("WIDTH" -> width, "RESET_ASYNC" -> isResetAsync.toString)) - with HasBlackBoxResource { - val io = IO(new Bundle { +/** External module implementing a falling-edge register with reset. + * + * INIT is a static SystemVerilog parameter rather than a runtime input. + * + * @param bitwidth + * bitwidth of the stored data; must be positive + * @param isResetAsync + * true for asynchronous reset, false for synchronous reset + * @param initBits + * unsigned bit pattern of the reset value + */ +private[chipmunk] final class RegNegInitBbox(bitwidth: Int, isResetAsync: Boolean, initBits: BigInt) + extends ExtModule( + Map( + "WIDTH" -> IntParam(BigInt(bitwidth)), + "RESET_ASYNC" -> IntParam(if (isResetAsync) BigInt(1) else BigInt(0)), + "INIT" -> RawParam(s"${bitwidth}'h${initBits.toString(16)}") + ) + ) { + require(bitwidth > 0, "RegNegInitBbox requires a positive data bitwidth.") + require( + initBits >= 0 && initBits.bitLength <= bitwidth, + "RegNegInitBbox.initBits must be an unsigned value fitting bitwidth." + ) + + val io = FlatIO(new Bundle { val clock = Input(Clock()) - val reset = Input(Reset()) - val init = Input(UInt(width.W)) - val en = Input(Bool()) - val d = Input(UInt(width.W)) - val q = Output(UInt(width.W)) + + // Give the external port the same concrete reset type as the wrapper. + val reset: Reset = + if (isResetAsync) Input(AsyncReset()) else Input(Bool()) + + val en = Input(Bool()) + val d = Input(UInt(bitwidth.W)) + val q = Output(UInt(bitwidth.W)) }) - override def desiredName = "Chipmunk_RegNegInit" - addResource(f"/regneg/$desiredName.sv") + override def desiredName: String = "Chipmunk_RegNegInit" + + addResource(s"/regneg/$desiredName.sv") } -/** Register triggered on falling clock edge. This Module is just a wrapper of [[RegNegBbox]] and [[RegNegInitBbox]], so - * that users don't need to assign clock and reset explicitly. +/** Connects a packable Chisel data type to a falling-edge register. * - * @param gen - * The Chisel type of the register. - * @param haveReset - * Whether the register has a reset. If true, the register will have a active-high reset. - * @param isResetAsync - * Whether the reset is asynchronous. Chisel cannot infer this, so users should specify it. + * The data must have a known, positive bitwidth and support asUInt/asTypeOf. initBits is None for a register without + * reset. When it is defined, isResetAsync determines the wrapper's concrete reset type. */ -private[chipmunk] class RegNeg[T <: Data](gen: T, haveReset: Boolean, isResetAsync: Boolean = true) extends Module { +private[chipmunk] final class RegNeg[T <: Data](gen: T, initBits: Option[BigInt], isResetAsync: Boolean) + extends Module { + + // Module reads resetType while constructing its implicit reset port. + // Therefore, the choice must be available from the constructor parameters. + override def resetType: Module.ResetType.Type = + initBits match { + case Some(_) if isResetAsync => Module.ResetType.Asynchronous + case Some(_) => Module.ResetType.Synchronous + case None => Module.ResetType.Default + } + + require(gen.isWidthKnown && gen.getWidth > 0, "RegNeg data must have a known, positive bitwidth.") + val io = IO(new Bundle { - val init = if (haveReset) Some(Input(gen)) else None - val en = Input(Bool()) - val d = Input(gen) - val q = Output(gen) + val en = Input(Bool()) + val d = Input(gen) + val q = Output(gen) }) - val width = io.d.getWidth - - val q = if (haveReset) { - val uRegNegBbox = Module(new RegNegInitBbox(width, isResetAsync)) - uRegNegBbox.io.clock := clock - uRegNegBbox.io.reset := reset - uRegNegBbox.io.en := io.en - uRegNegBbox.io.d := io.d.asUInt - uRegNegBbox.io.init := io.init.get.asUInt - uRegNegBbox.io.q.asTypeOf(gen) - } else { - val uRegNegBbox = Module(new RegNegBbox(width)) - uRegNegBbox.io.clock := clock - uRegNegBbox.io.en := io.en - uRegNegBbox.io.d := io.d.asUInt - uRegNegBbox.io.q.asTypeOf(gen) - } - io.q := q + private val bitwidth = gen.getWidth + initBits match { + case Some(bits) => + val reg = Module(new RegNegInitBbox(bitwidth, isResetAsync, bits)) + reg.io.clock := clock + reg.io.reset := reset + reg.io.en := io.en + reg.io.d := io.d.asUInt + io.q := reg.io.q.asTypeOf(gen) + + case None => + val reg = Module(new RegNegBbox(bitwidth)) + reg.io.clock := clock + reg.io.en := io.en + reg.io.d := io.d.asUInt + io.q := reg.io.q.asTypeOf(gen) + } } object RegNegNext { - /** Returns a falling-edge triggered register with no reset initialization. + /** Returns a falling-edge register without reset. * * It has a similar function to [[chisel3.RegNext]] but it is triggered on the falling clock edge. * * @example * {{{ - * val regNeg = RegNegNext(nextVal) + * val q = RegNegNext(nextValue) * }}} */ - def apply[T <: Data](next: T): T = { + def apply[T <: Data](next: T)(using SourceInfo): T = RegNegEnable(next, true.B) - } - /** Returns a falling-edge triggered register with active-high reset initialization. The reset can be synchronous or - * asynchronous, according to its type. + /** Returns a falling-edge register with active-high reset. * * It has a similar function to [[chisel3.RegNext]] but it is triggered on the falling clock edge. * + * The reset value must be a complete Chisel literal with the same type and bitwidth as next. The reset mode must be + * specified explicitly. + * + * @param next + * value captured on the falling clock edge + * @param init + * constant value loaded on reset * @param isResetAsync - * Whether the reset is asynchronous. Chisel cannot infer this from the Scala context, so users should specify it. + * true for asynchronous reset, false for synchronous reset * @example * {{{ - * val regNeg = RegNegNext(nextVal) + * val q = RegNegNext(nextValue, 0.U(8.W), isResetAsync = false) * }}} */ - def apply[T <: Data](next: T, init: T, isResetAsync: Boolean = true): T = { + def apply[T <: Data](next: T, init: T, isResetAsync: Boolean)(using SourceInfo): T = RegNegEnable(next, init, true.B, isResetAsync) - } } object RegNegEnable { - /** Returns a falling-edge triggered register with update enable gate and no reset initialization. + /** Returns a falling-edge register with enable and without reset. * * It has a similar function to [[chisel3.util.RegEnable]] but it is triggered on the falling clock edge. * * @example * {{{ - * val regNeg = RegNegEnable(nextVal, ena) + * val q = RegNegEnable(nextValue, enable) * }}} */ - def apply[T <: Data](next: T, enable: Bool): T = { + def apply[T <: Data](next: T, enable: Bool)(using SourceInfo): T = { requireIsHardware(next, "RegNegEnable.next") requireIsHardware(enable, "RegNegEnable.enable") - val reg = Module(new RegNeg(chiselTypeOf(next), haveReset = false)) + val reg = Module(new RegNeg(chiselTypeOf(next), None, isResetAsync = false)) reg.io.en := enable reg.io.d := next reg.io.q } - /** Returns a falling-edge triggered register with update enable gate and active-high reset initialization. The reset - * can be synchronous or asynchronous, according to its type. + /** Returns a falling-edge register with enable and active-high reset. * * It has a similar function to [[chisel3.util.RegEnable]] but it is triggered on the falling clock edge. * + * The reset value must be a complete, static Chisel literal with the same type and bitwidth as next. In particular, + * a runtime signal cannot be used as init. + * + * @param next + * value captured when enable is true + * @param init + * constant value loaded on reset + * @param enable + * register update enable * @param isResetAsync * Whether the reset is asynchronous. Chisel cannot infer this from the Scala context, so users should specify it. * @example * {{{ - * val regNeg = RegNegEnable(nextVal, initVal, ena) + * val q = RegNegEnable(nextValue, 0.U(8.W), enable, isResetAsync = false) * }}} */ - def apply[T <: Data](next: T, init: T, enable: Bool, isResetAsync: Boolean = true): T = { + def apply[T <: Data](next: T, init: T, enable: Bool, isResetAsync: Boolean)(using SourceInfo): T = { requireIsHardware(next, "RegNegEnable.next") requireIsHardware(init, "RegNegEnable.init") requireIsHardware(enable, "RegNegEnable.enable") - val reg = Module(new RegNeg(chiselTypeOf(next), haveReset = true, isResetAsync = isResetAsync)) - reg.io.en := enable - reg.io.d := next - reg.io.init.get := init + require(next.isWidthKnown && next.getWidth > 0, "RegNegEnable.next must have a known, positive bitwidth.") + require( + init.isWidthKnown && init.getWidth == next.getWidth, + "RegNegEnable.init must have the same bitwidth as next." + ) + require(DataMirror.checkTypeEquivalence(next, init), "RegNegEnable.init must have the same Chisel type as next.") + require(init.isLit, "RegNegEnable.init must be a Chisel literal.") + + val bitwidth = next.getWidth + + // Mask negative SInt literals into their bitwidth-wide two's-complement + // representation. Aggregate litValue uses the corresponding packed order. + // litValue also rejects aggregate literals containing DontCare. + val mask = (BigInt(1) << bitwidth) - 1 + val initBits = init.litValue & mask + + val reg = Module(new RegNeg(chiselTypeOf(next), Some(initBits), isResetAsync)) + reg.io.en := enable + reg.io.d := next reg.io.q } } diff --git a/chipmunk/src/ResetSync.scala b/chipmunk/src/ResetSync.scala deleted file mode 100644 index b6d4371..0000000 --- a/chipmunk/src/ResetSync.scala +++ /dev/null @@ -1,80 +0,0 @@ -package chipmunk - -import chisel3._ -import chisel3.util._ - -/** Asynchronous reset synchronizer. Note that only high-active reset is supported. - * - * It can synchronize the dessert of the implicit reset signal with the implicit clock. Generally, if an externally - * input reset signal is to be used for asynchronous reset, it should be synchronized by this module first. - * {{{ - * _______ _______ - * resetChainIn ---> |D Q|-...-> |D Q|---> resetChainOut - * | | | | - * |> R | |> R | - * ------- ------- - * | | - * this.reset ------------------------- - * }}} - * - * @param bufferDepth - * The stage number of the synchronizer. - */ -class ResetSync(val bufferDepth: Int = 2) extends Module with RequireAsyncReset { - val io = IO(new Bundle { - val resetChainIn = Input(Bool()) - val resetChainOut = Output(AsyncReset()) - }) - - val resetValue = true.B - - val resetSynced = ShiftRegister(io.resetChainIn, bufferDepth, resetValue, true.B) - - io.resetChainOut := resetSynced.asAsyncReset -} - -object AsyncResetSyncDessert { - - /** Synchronize the implicit reset to the implicit clock using [[ResetSync]]. Note that only high-active reset is - * supported. - * - * @param bufferDepth - * The stage number of the reset synchronizer buffer. - * @return - * The synchronized reset signal. - */ - def withImplicitClockDomain(resetChainIn: AsyncReset = null, bufferDepth: Int = 2): AsyncReset = { - val uRstSync = Module(new ResetSync(bufferDepth)) - if (resetChainIn != null) { - uRstSync.io.resetChainIn := resetChainIn.asBool - } else { - uRstSync.io.resetChainIn := false.B - } - uRstSync.io.resetChainOut - } - - /** Synchronize the given reset to the given clock using [[ResetSync]]. Note that only high-active reset is supported. - * - * @param clock - * The target clock. - * @param resetAsync - * The input asynchronous reset signal needed to be synchronized with the target clock. - * @param resetChainIn - * The predecessor of reset chain. Only used in some multi-clock scenarios where reset removal must be ordered and - * proper sequence. Leave it blank to use VSS. - * @param bufferDepth - * The stage number of the synchronizer. - * @return - * The synchronized reset signal. - */ - def withSpecificClockDomain( - clock: Clock, - resetAsync: Reset, - resetChainIn: AsyncReset = null, - bufferDepth: Int = 2 - ): AsyncReset = { - withClockAndReset(clock, resetAsync.asAsyncReset) { - withImplicitClockDomain(resetChainIn, bufferDepth) - } - } -} diff --git a/chipmunk/src/StateMachine.scala b/chipmunk/src/StateMachine.scala deleted file mode 100644 index 56eb9ea..0000000 --- a/chipmunk/src/StateMachine.scala +++ /dev/null @@ -1,266 +0,0 @@ -package chipmunk - -import chisel3._ - -import scala.annotation.nowarn -import scala.collection.mutable.ListBuffer - -/** This trait indicates the entry point state of a finite state machine (FSM, [[StateMachine]]). It should be mixed in - * with a [[State]]. - * - * There should be only one entry point state in each FSM. - * - * @see - * [[StateMachine]] - */ -trait EntryPoint extends State - -/** State in a finite state machine (FSM, [[StateMachine]]). - * - * @param id - * The user-defined encoding of this state. The default value is null (If so, it is determined by - * [[StateMachine.defaultEncoding]]). - * - * @param sm - * The FSM that this state belongs to. It is implicitly passed by the host FSM. - * - * @see - * [[StateMachine]] - */ -class State(val id: UInt = null)(implicit sm: StateMachineAccessor) { - if (id != null) { - require(id.litOption.isDefined, "The id of a state should be a hardware literal.") - } - - private[chipmunk] val tasksWhenIsActive = ListBuffer[() => Unit]() - private[chipmunk] val tasksWhenIsInactive = ListBuffer[() => Unit]() - private[chipmunk] val tasksWhenIsEntering = ListBuffer[() => Unit]() - private[chipmunk] val tasksWhenIsExiting = ListBuffer[() => Unit]() - - def whenIsActive(body: => Unit): this.type = { - tasksWhenIsActive += (() => body) - this - } - - def whenIsInactive(body: => Unit): this.type = { - tasksWhenIsInactive += (() => body) - this - } - - def whenIsEntering(body: => Unit): this.type = { - tasksWhenIsEntering += (() => body) - this - } - - def whenIsExiting(body: => Unit): this.type = { - tasksWhenIsExiting += (() => body) - this - } - - // Add this state to the host FSM - sm.register(this) -} - -/** The entry point of a finite state machine (FSM, [[StateMachine]]). It is the same as `State with EntryPoint`. - */ -final class StateEntryPoint(implicit sm: StateMachineAccessor) extends State with EntryPoint - -/** Provides the finite state machine ([[StateMachine]], FSM) APIs visible to its states. */ -private[chipmunk] trait StateMachineAccessor { - def register(state: State): Unit -} - -object Sequential extends StateMachineEncoding { - def encode(index: Int): Int = index -} - -object OneHot extends StateMachineEncoding { - def encode(index: Int): Int = 1 << index -} - -object Gray extends StateMachineEncoding { - def encode(index: Int): Int = index ^ (index >> 1) - - override def stateToUInt(states: List[State]): Map[State, UInt] = { - states.zipWithIndex.map { case (state, index) => - require(state.id == null, "Gray encoding does not support user-defined state id.") - state -> encode(index).U - }.toMap - } -} - -abstract class StateMachineEncoding { - def encode(index: Int): Int - - def stateToUInt(states: List[State]): Map[State, UInt] = { - val userForcedIds = states.zipWithIndex - .filter(_._1.id != null) - .map { case (state, index) => - index -> state.id.litOption.get.toInt - } - .toMap - var j = 0 - states.zipWithIndex.map { case (state, index) => - val id: Int = userForcedIds.get(index) match { - case Some(id) => id - case None => - var id = 0 - while ({ - id = encode(j) - j += 1 - userForcedIds.values.toList.contains(id) - }) {} - id - } - state -> id.U - }.toMap - } -} - -/** Finite state machine (FSM). - * - * @param defaultEncoding - * The default state encoding (The binary values of the states) of the FSM states. The default value is - * [[Sequential]]. - * - * @param autoStart - * Whether the FSM should automatically start (i.e., transit to the entry point state) when the reset is de-asserted. - * The default value is true. - * - * @note - * Each FSM should have one and only one entry point state ([[StateEntryPoint]]). - * - * @example - * {{{ - * val fsm = new StateMachine { - * val s1 = new State with EntryPoint - * val s2 = new State - * val s3 = new State - * - * s1 - * .whenIsActive { - * when(io.a) { - * goto(s2) - * } - * } - * s2 - * .whenIsActive { - * when(io.b) { - * goto(s3) - * } - * } - * s3 - * .whenIsActive { - * when(io.c) { - * goto(s1) - * } - * } - * } - * }}} - * - * @see - * [[State]] - */ -@nowarn("""cat=deprecation&origin=scala\.DelayedInit""") -class StateMachine(defaultEncoding: StateMachineEncoding = Sequential, val autoStart: Boolean = true) - extends StateMachineAccessor - with DelayedInit { - implicit val implicitFsm: StateMachine = this - - // When a State is instantiated, it will call register() to add itself to statesBuffer. - private val statesBuffer = ListBuffer[State]() - - def states: List[State] = statesBuffer.toList - - // The default state when the FSM is reset. The FSM will transit from this state to the entry point when startFsm() is - // called. - val stateBoot: State = new State { - if (autoStart) { - whenIsActive { - startFsm() - } - } - } - - def stateEntry: State = { - val entries = states.filter(_.isInstanceOf[EntryPoint]) - assert(entries.length == 1, s"There should be 1 entry point in the FSM, but got ${entries.length}.") - entries.head - } - - var stateToUInt: Map[State, UInt] = _ - - val stateCurr: UInt = Wire(UInt()).dontTouch - val stateNext: UInt = Wire(UInt()).dontTouch - - /** Transits to the specified state. */ - def goto(state: State): Unit = { - stateNext := stateToUInt(state) - } - - /** Returns whether this FSM is in the specified state. */ - def isActive(state: State): Bool = { - stateCurr === stateToUInt(state) - } - - /** Returns whether this FSM is NOT in the specified state. */ - def isInactive(state: State): Bool = { - stateCurr =/= stateToUInt(state) - } - - /** Returns whether this FSM is entering the specified state. */ - def isEntering(state: State): Bool = { - stateNext === stateToUInt(state) && stateCurr =/= stateToUInt(state) - } - - /** Returns whether this FSM is exiting the specified state. */ - def isExiting(state: State): Bool = { - stateNext =/= stateToUInt(state) && stateCurr === stateToUInt(state) - } - - /** Add a state to the FSM. This method is automatically called by the constructor of [[State]]. */ - def register(state: State): Unit = { - statesBuffer += state - } - - /** Transit to the entry point state. */ - def startFsm(): Unit = { - goto(stateEntry) - } - - /** Transit back to the BOOT state. */ - def finishFsm(): Unit = { - goto(stateBoot) - } - - private var isGenerated: Boolean = false - - def delayedInit(body: => Unit): Unit = { - body - - // TODO: It is a workaround to avoid FSM generating before States are ready. - // I don't know why the delayedInit is called multiple times. - if (states.length <= 1 || isGenerated) return - isGenerated = true - - stateToUInt = defaultEncoding.stateToUInt(states) - - stateCurr := RegNext(stateNext, stateToUInt(stateBoot)) - stateNext := stateCurr - - for (state <- states) { - when(isActive(state)) { - state.tasksWhenIsActive.foreach(_()) - } - when(isInactive(state)) { - state.tasksWhenIsInactive.foreach(_()) - } - when(isEntering(state)) { - state.tasksWhenIsEntering.foreach(_()) - } - when(isExiting(state)) { - state.tasksWhenIsExiting.foreach(_()) - } - } - } -} diff --git a/chipmunk/src/VerilogIO.scala b/chipmunk/src/VerilogIO.scala deleted file mode 100644 index 1a2d17f..0000000 --- a/chipmunk/src/VerilogIO.scala +++ /dev/null @@ -1,71 +0,0 @@ -package chipmunk - -import chisel3._ -import chisel3.experimental.requireIsChiselType -import chisel3.reflect.DataMirror - -import scala.collection.immutable.SeqMap - -object PortNameTransform { - - /** Append the port names with a postfix (e.g., AWADDR -> AWADDR_abc). */ - def stringPostfix(s: String): String => String = { pn => pn + s } - - /** Prepend the port names with a prefix (e.g., AwAddr -> i2c_awaddr). */ - def stringPrefix(s: String): String => String = { pn => s + pn } - - /** Convert the port names to lower case (e.g., AwAddr -> awaddr). */ - def lowerCase: String => String = _.toLowerCase - - /** Convert the port names to upper case (e.g., AwAddr -> AWADDR). */ - def upperCase: String => String = _.toUpperCase - - /** Toggle the case of the port names (e.g., AwAddr -> aWaDDR). */ - def toggleCase: String => String = - _.map { - case c if c.isUpper => c.toLower - case c => c.toUpper - } - - /** Override the port names with the given map (e.g., Map("AwAddr" -> "i2c_awaddr") will convert AwAddr to - * i2c_awaddr). If the port name is not in the map, it will remain unchanged. - */ - def overrideName(overrideNames: Map[String, String]): String => String = { pn => - overrideNames.getOrElse(pn, pn) - } -} - -/** A [[MapBundle]]-like bundle for blackbox-friendly interface generation. - * - * See [[chipmunk.amba.Axi4VerilogIO]] for an example of usage. - */ -abstract class VerilogIO[+T <: Data](portNameTransforms: Seq[String => String] = Seq.empty)(portMap: (String, T)*) - extends Record { - private def formatPortName(portName: String): String = { - portNameTransforms.foldLeft(portName) { (name, transform) => - transform(name) - } - } - - val elements = SeqMap(portMap.collect { case (field, elt) => - requireIsChiselType(elt) - formatPortName(field) -> DataMirror.internal.chiselTypeClone(elt) - }: _*) - - def apply(field: String): T = elements(formatPortName(field)) - -} - -trait WithVerilogIO[T <: VerilogIO[_]] { - self: Data => - - /** Create a [[VerilogIO]] with the given port name transforms and port map. - * - * @param portNameTransforms - * A sequence of functions to transform the port names. We provide some common transforms in [[PortNameTransform]]. - * - * @see - * [[VerilogIO]] - */ - def createVerilogIO(portNameTransforms: Seq[String => String] = Seq.empty): T -} diff --git a/chipmunk/src/amba/ApbIO.scala b/chipmunk/src/amba/ApbIO.scala deleted file mode 100644 index 0ac0afb..0000000 --- a/chipmunk/src/amba/ApbIO.scala +++ /dev/null @@ -1,66 +0,0 @@ -package chipmunk -package amba - -import chisel3._ - -private[amba] abstract class ApbIOBase( - val dataWidth: Int, - val addrWidth: Int, - val hasProt: Boolean = false, - val hasStrb: Boolean = false -) extends Bundle - with IsMasterSlave { - override def isMaster = true - - val strobeWidth: Int = dataWidth / 8 - - def allowedDataWidth = List(8, 16, 32) - require(allowedDataWidth contains dataWidth, s"Data width can only be 8, 16, or 32, but got $dataWidth.") - - require(addrWidth > 0, s"Address width must be at least 1, but got $addrWidth.") - - val addr = Output(UInt(addrWidth.W)) - val selx = Output(UInt(4.W)) - val enable = Output(Bool()) - val write = Output(Bool()) - val wdata = Output(UInt(dataWidth.W)) - val ready = Input(Bool()) - val rdata = Input(UInt(dataWidth.W)) - val slverr = Input(Bool()) - - // The below signals are optional in APB4 but not in APB3 - val prot = if (hasProt) Some(Output(UInt(3.W))) else None - val strb = if (hasStrb) Some(Output(UInt(strobeWidth.W))) else None -} - -/** AMBA3 APB IO bundle. - * - * @param dataWidth - * The bit width of the bus data. It can only be 8, 16, or 32. - * @param addrWidth - * The bit width of the bus address. - */ -class Apb3IO(dataWidth: Int, addrWidth: Int) extends ApbIOBase(dataWidth, addrWidth) with WithVerilogIO[ApbVerilogIO] { - def createVerilogIO(portNameTransforms: Seq[String => String] = Seq.empty) = { - new ApbVerilogIO(dataWidth, addrWidth)(portNameTransforms) - } -} - -/** AMBA4 APB IO bundle. - * - * @param dataWidth - * The bit width of the bus data. It can only be 8, 16, or 32. - * @param addrWidth - * The bit width of the bus address. - * @param hasProt - * Whether the bus has prot signals. - * @param hasStrb - * Whether the bus has strobe signals. - */ -class Apb4IO(dataWidth: Int, addrWidth: Int, hasProt: Boolean = false, hasStrb: Boolean = false) - extends ApbIOBase(dataWidth, addrWidth, hasProt, hasStrb) - with WithVerilogIO[ApbVerilogIO] { - def createVerilogIO(portNameTransforms: Seq[String => String] = Seq.empty) = { - new ApbVerilogIO(dataWidth, addrWidth, hasProt, hasStrb)(portNameTransforms) - } -} diff --git a/chipmunk/src/amba/ApbVerilogIO.scala b/chipmunk/src/amba/ApbVerilogIO.scala deleted file mode 100644 index cc0315f..0000000 --- a/chipmunk/src/amba/ApbVerilogIO.scala +++ /dev/null @@ -1,59 +0,0 @@ -package chipmunk -package amba - -import chisel3._ -import chisel3.experimental.dataview.DataView - -class ApbVerilogIO(val dataWidth: Int, val addrWidth: Int, val hasProt: Boolean = false, val hasStrb: Boolean = false)( - portNameTransforms: Seq[String => String] = Seq.empty -) extends VerilogIO(portNameTransforms)({ - val strobeWidth: Int = dataWidth / 8 - - val axiPorts = Seq( - "PADDR" -> Output(UInt(addrWidth.W)), - "PSELX" -> Output(UInt(4.W)), - "PENABLE" -> Output(Bool()), - "PWRITE" -> Output(Bool()), - "PWDATA" -> Output(UInt(dataWidth.W)), - "PREADY" -> Input(Bool()), - "PRDATA" -> Input(UInt(dataWidth.W)), - "PSLVERR" -> Input(Bool()), - "PPROT" -> (if (hasProt) Some(Output(UInt(3.W))) else None), - "PSTRB" -> (if (hasStrb) Some(Output(UInt(strobeWidth.W))) else None) - ).collect { - case (key, Some(value: Data)) => key -> value - case (key, value: Data) => key -> value - } - axiPorts - }: _*) - with IsMasterSlave { - override def isMaster = true -} - -object ApbVerilogIO { - implicit val apb3View: DataView[ApbVerilogIO, Apb3IO] = - DataView.mapping(rc => new Apb3IO(rc.dataWidth, rc.addrWidth), (rc, b) => mapPorts(rc, b)) - - implicit val apb4View: DataView[ApbVerilogIO, Apb4IO] = - DataView.mapping(rc => new Apb4IO(rc.dataWidth, rc.addrWidth, rc.hasProt, rc.hasStrb), (rc, b) => mapPorts(rc, b)) - - private def mapPorts(rc: ApbVerilogIO, b: ApbIOBase) = { - var portPairs = Seq( - rc("PADDR") -> b.addr, - rc("PSELX") -> b.selx, - rc("PENABLE") -> b.enable, - rc("PWRITE") -> b.write, - rc("PWDATA") -> b.wdata, - rc("PREADY") -> b.ready, - rc("PRDATA") -> b.rdata, - rc("PSLVERR") -> b.slverr - ) - if (rc.hasProt) { - portPairs ++= Seq(rc("PPROT") -> b.prot.get) - } - if (rc.hasStrb) { - portPairs ++= Seq(rc("PSTRB") -> b.strb.get) - } - portPairs - } -} diff --git a/chipmunk/src/amba/AxiEnum.scala b/chipmunk/src/amba/AxiEnum.scala deleted file mode 100644 index 12c8675..0000000 --- a/chipmunk/src/amba/AxiEnum.scala +++ /dev/null @@ -1,28 +0,0 @@ -package chipmunk -package amba - -import chisel3._ - -object AxiBurstType extends ChiselEnum { - val BURST_FIXED = Value(0.U) - val BURST_INCR = Value(1.U) - val BURST_WRAP = Value(2.U) -} - -object AxiBurstSize extends ChiselEnum { - val SIZE1 = Value(0.U) - val SIZE2 = Value(1.U) - val SIZE4 = Value(2.U) - val SIZE8 = Value(3.U) - val SIZE16 = Value(4.U) - val SIZE32 = Value(5.U) - val SIZE64 = Value(6.U) - val SIZE128 = Value(7.U) -} - -object AxiResp extends ChiselEnum { - val RESP_OKAY = Value(0.U) - val RESP_EXOKAY = Value(1.U) - val RESP_SLVERR = Value(2.U) - val RESP_DECERR = Value(3.U) -} diff --git a/chipmunk/src/amba/AxiIO.scala b/chipmunk/src/amba/AxiIO.scala deleted file mode 100644 index bf1893d..0000000 --- a/chipmunk/src/amba/AxiIO.scala +++ /dev/null @@ -1,135 +0,0 @@ -package chipmunk -package amba - -import stream.{Stream, StreamIO} - -import chisel3._ - -private[amba] trait HasAxiId extends Bundle { - val idWidth: Int - val id = if (idWidth > 0) Some(UInt(idWidth.W)) else None -} - -class AxiWriteAddrChannel( - addrWidth: Int, - val idWidth: Int, - val lenWidth: Int = 8, - val lockWidth: Int = 1, - hasQos: Boolean = false, - hasRegion: Boolean = false -) extends AxiLiteAddrChannel(addrWidth) - with HasAxiId { - require(List(4, 8) contains lenWidth, "Bit width of AxLEN can only be 4 (AXI3) or 8 (AXI4).") - require(List(1, 2) contains lockWidth, "Bit width of AxLOCK can only be 2 (AXI3) or 1 (AXI4).") - - val size = AxiBurstSize() - val len = UInt(lenWidth.W) - val burst = AxiBurstType() - val cache = UInt(4.W) - val lock = UInt(lockWidth.W) - val qos = if (hasQos) Some(UInt(4.W)) else None - val region = if (hasRegion) Some(UInt(4.W)) else None - - // The bellow AxCACHE are defined according to AXI4 specification. - def cacheBufferable: Bool = cache(0).asBool - def cacheModifiable: Bool = cache(1).asBool - def cacheOtherAllocated: Bool = cache(2).asBool - def cacheAllocated: Bool = cache(3).asBool - - def lockNormal: Bool = lock === 0.U - def lockExclusive: Bool = lock === 1.U - def lockLocked: Bool = if (lockWidth == 2) lock === 2.U else false.B -} - -class AxiReadAddrChannel( - addrWidth: Int, - idWidth: Int, - lenWidth: Int = 8, - lockWidth: Int = 1, - hasQos: Boolean = false, - hasRegion: Boolean = false -) extends AxiWriteAddrChannel(addrWidth, idWidth, lenWidth, lockWidth, hasQos, hasRegion) { - override def cacheAllocated: Bool = cache(2).asBool - override def cacheOtherAllocated: Bool = cache(3).asBool -} - -class AxiWriteDataChannel(dataWidth: Int, val idWidth: Int = 0) - extends AxiLiteWriteDataChannel(dataWidth) - with HasAxiId { - val last = Bool() -} - -class AxiWriteRespChannel(val idWidth: Int) extends AxiLiteWriteRespChannel() with HasAxiId - -class AxiReadDataChannel(dataWidth: Int, val idWidth: Int) extends AxiLiteReadDataChannel(dataWidth) with HasAxiId { - val last = Bool() -} - -private[amba] abstract class AxiIOBase(val dataWidth: Int, val addrWidth: Int, val idWidth: Int) - extends Bundle - with IsMasterSlave { - override def isMaster = true - - def allowedDataWidth = List(8, 16, 32, 64, 128, 256, 512, 1024) - require(allowedDataWidth contains dataWidth, s"Data width can only be 8, 16, ..., or 1024, but got $dataWidth.") - - require(addrWidth > 0, s"Address width must be at least 1, but got $addrWidth.") - require(idWidth >= 0, s"ID width of AXI bus must be at least 0, but got $idWidth.") - - val strobeWidth: Int = dataWidth / 8 - val hasId: Boolean = idWidth > 0 - - val aw: StreamIO[AxiWriteAddrChannel] - val w: StreamIO[AxiWriteDataChannel] - val b: StreamIO[AxiWriteRespChannel] - val ar: StreamIO[AxiReadAddrChannel] - val r: StreamIO[AxiReadDataChannel] -} - -/** AMBA4 AXI IO bundle. - * - * @param dataWidth - * The bit width of the bus data. - * @param addrWidth - * The bit width of the bus address. - * @param idWidth - * The bit width of the bus id. - * @param hasQos - * Whether the bus has QoS signals. - * @param hasRegion - * Whether the bus has region signals. - */ -class Axi4IO(dataWidth: Int, addrWidth: Int, idWidth: Int, hasQos: Boolean = false, hasRegion: Boolean = false) - extends AxiIOBase(dataWidth, addrWidth, idWidth) - with WithVerilogIO[Axi4VerilogIO] { - val aw = Master(Stream(new AxiWriteAddrChannel(addrWidth, idWidth, hasQos = hasQos, hasRegion = hasRegion))) - val w = Master(Stream(new AxiWriteDataChannel(dataWidth))) // AXI4 does NOT have WID. - val b = Slave(Stream(new AxiWriteRespChannel(idWidth))) - val ar = Master(Stream(new AxiReadAddrChannel(addrWidth, idWidth, hasQos = hasQos, hasRegion = hasRegion))) - val r = Slave(Stream(new AxiReadDataChannel(dataWidth, idWidth))) - - def createVerilogIO(portNameTransforms: Seq[String => String]) = - new Axi4VerilogIO(dataWidth, addrWidth, idWidth, hasQos, hasRegion)(portNameTransforms) -} - -/** AMBA3 AXI IO bundle. - * - * @param dataWidth - * The bit width of the bus data. - * @param addrWidth - * The bit width of the bus address. - * @param idWidth - * The bit width of the bus id. - */ -class Axi3IO(dataWidth: Int, addrWidth: Int, idWidth: Int) - extends AxiIOBase(dataWidth, addrWidth, idWidth) - with WithVerilogIO[Axi3VerilogIO] { - val aw = Master(Stream(new AxiWriteAddrChannel(addrWidth, idWidth, lenWidth = 4, lockWidth = 2))) - val w = Master(Stream(new AxiWriteDataChannel(dataWidth, idWidth))) - val b = Slave(Stream(new AxiWriteRespChannel(idWidth))) - val ar = Master(Stream(new AxiReadAddrChannel(addrWidth, idWidth, lenWidth = 4, lockWidth = 2))) - val r = Slave(Stream(new AxiReadDataChannel(dataWidth, idWidth))) - - def createVerilogIO(portNameTransforms: Seq[String => String]) = - new Axi3VerilogIO(dataWidth, addrWidth, idWidth)(portNameTransforms) -} diff --git a/chipmunk/src/amba/AxiLiteIO.scala b/chipmunk/src/amba/AxiLiteIO.scala deleted file mode 100644 index 2cfceb2..0000000 --- a/chipmunk/src/amba/AxiLiteIO.scala +++ /dev/null @@ -1,74 +0,0 @@ -package chipmunk -package amba - -import stream.{Stream, StreamIO} - -import chisel3._ - -class AxiLiteAddrChannel(val addrWidth: Int) extends Bundle { - val addr = UInt(addrWidth.W) - val prot = UInt(3.W) - - def protPrivileged: Bool = prot(0).asBool - def protNonsecure: Bool = prot(1).asBool - def protInstruction: Bool = prot(2).asBool -} - -class AxiLiteWriteDataChannel(val dataWidth: Int) extends Bundle { - val strobeWidth: Int = dataWidth / 8 - - val data = UInt(dataWidth.W) - val strb = UInt(strobeWidth.W) -} - -class AxiLiteWriteRespChannel() extends Bundle { - val resp = AxiResp() -} - -class AxiLiteReadDataChannel(val dataWidth: Int) extends Bundle { - val data = UInt(dataWidth.W) - val resp = AxiResp() -} - -private[amba] abstract class AxiLiteIOBase(val dataWidth: Int, val addrWidth: Int) extends Bundle with IsMasterSlave { - override def isMaster = true - - def allowedDataWidth: List[Int] = List(32, 64) - - // Do not check dataWidth, because many implementations violate this. - // require( - // allowedDataWidth contains dataWidth, - // s"Data width can only be 32 or 64, but got $dataWidth" - // ) - - require(addrWidth > 0, s"Address width must be at least 1, but got $addrWidth") - - val strobeWidth: Int = dataWidth / 8 - - val aw: StreamIO[AxiLiteAddrChannel] - val w: StreamIO[AxiLiteWriteDataChannel] - val b: StreamIO[AxiLiteWriteRespChannel] - val ar: StreamIO[AxiLiteAddrChannel] - val r: StreamIO[AxiLiteReadDataChannel] -} - -/** AMBA4 AXI-Lite IO bundle. - * - * @param dataWidth - * The bit width of the bus data. It can only be 64 or 32. - * @param addrWidth - * The bit width of the bus address. - */ -class Axi4LiteIO(dataWidth: Int, addrWidth: Int) - extends AxiLiteIOBase(dataWidth, addrWidth) - with WithVerilogIO[Axi4LiteVerilogIO] { - val aw = Master(Stream(new AxiLiteAddrChannel(addrWidth))) - val w = Master(Stream(new AxiLiteWriteDataChannel(dataWidth))) - val b = Slave(Stream(new AxiLiteWriteRespChannel())) - val ar = Master(Stream(new AxiLiteAddrChannel(addrWidth))) - val r = Slave(Stream(new AxiLiteReadDataChannel(dataWidth))) - - def createVerilogIO(portNameTransforms: Seq[String => String] = Seq.empty) = { - new Axi4LiteVerilogIO(dataWidth, addrWidth)(portNameTransforms) - } -} diff --git a/chipmunk/src/amba/AxiLiteToAxi4Bridge.scala b/chipmunk/src/amba/AxiLiteToAxi4Bridge.scala deleted file mode 100644 index c4bb3c2..0000000 --- a/chipmunk/src/amba/AxiLiteToAxi4Bridge.scala +++ /dev/null @@ -1,49 +0,0 @@ -package chipmunk -package amba - -import chisel3._ -import chisel3.util._ - -class AxiLiteToAxi4Bridge(dataWidth: Int, addrWidth: Int, idWidth: Int, writeId: Int = 0, readId: Int = 0) - extends Module { - val io = IO(new Bundle { - val sAxiL = Slave(new Axi4LiteIO(dataWidth, addrWidth)) - val mAxi4 = Master(new Axi4IO(dataWidth, addrWidth, idWidth)) - }) - - io.mAxi4.aw handshakeFrom io.sAxiL.aw - io.mAxi4.aw.bits.addr := io.sAxiL.aw.bits.addr - io.mAxi4.aw.bits.prot := io.sAxiL.aw.bits.prot - io.mAxi4.aw.bits.size := AxiBurstSize(log2Ceil(io.sAxiL.strobeWidth).U) - io.mAxi4.aw.bits.len := 0.U - io.mAxi4.aw.bits.burst := AxiBurstType.BURST_INCR - io.mAxi4.aw.bits.lock := 0.U - io.mAxi4.aw.bits.cache := 3.U // Recommended by Xilinx UG1037 - io.mAxi4.aw.bits.qos.foreach(_ := 0.U) - io.mAxi4.aw.bits.region.foreach(_ := 0.U) - io.mAxi4.aw.bits.id.foreach(_ := writeId.U) - - io.mAxi4.ar handshakeFrom io.sAxiL.ar - io.mAxi4.ar.bits.addr := io.sAxiL.ar.bits.addr - io.mAxi4.ar.bits.prot := io.sAxiL.ar.bits.prot - io.mAxi4.ar.bits.size := AxiBurstSize(log2Ceil(io.sAxiL.strobeWidth).U) - io.mAxi4.ar.bits.len := 0.U - io.mAxi4.ar.bits.burst := AxiBurstType.BURST_INCR - io.mAxi4.ar.bits.lock := 0.U - io.mAxi4.ar.bits.cache := 3.U // Recommended by Xilinx UG1037 - io.mAxi4.ar.bits.qos.foreach(_ := 0.U) - io.mAxi4.ar.bits.region.foreach(_ := 0.U) - io.mAxi4.ar.bits.id.foreach(_ := readId.U) - - io.mAxi4.w handshakeFrom io.sAxiL.w - io.mAxi4.w.bits.data := io.sAxiL.w.bits.data - io.mAxi4.w.bits.strb := io.sAxiL.w.bits.strb - io.mAxi4.w.bits.last := true.B - - io.sAxiL.r handshakeFrom io.mAxi4.r - io.sAxiL.r.bits.data := io.mAxi4.r.bits.data - io.sAxiL.r.bits.resp := io.mAxi4.r.bits.resp - - io.sAxiL.b handshakeFrom io.mAxi4.b - io.sAxiL.b.bits.resp := io.mAxi4.b.bits.resp -} diff --git a/chipmunk/src/amba/AxiLiteVerilogIO.scala b/chipmunk/src/amba/AxiLiteVerilogIO.scala deleted file mode 100644 index 24c29d8..0000000 --- a/chipmunk/src/amba/AxiLiteVerilogIO.scala +++ /dev/null @@ -1,77 +0,0 @@ -package chipmunk -package amba - -import chisel3._ -import chisel3.experimental.dataview.DataView - -/** Generate a [[Axi4LiteIO]] interface with blackbox-friendly names. - * - * @example - * {{{ - * val s = FlatIO(Slave(new Axi4LiteIORtlConnector(..., postfix = Some("abc")))) - * val m = IO(Master(new Axi4LiteIO(...))) - * val sView = s0.viewAs[Axi4LiteIO] - * m <> sView - * }}} - * - * @param dataWidth - * The bit width of the bus data. - * @param addrWidth - * The bit width of the bus address. - * @param portNameTransforms - * A sequence of functions to transform the port names. See [[VerilogIO]] for more details. - */ -class Axi4LiteVerilogIO(val dataWidth: Int, val addrWidth: Int)(portNameTransforms: Seq[String => String] = Seq.empty) - extends VerilogIO(portNameTransforms)( - "AWADDR" -> Output(UInt(addrWidth.W)), - "AWPROT" -> Output(UInt(3.W)), - "AWVALID" -> Output(Bool()), - "AWREADY" -> Input(Bool()), - "WDATA" -> Output(UInt(dataWidth.W)), - "WSTRB" -> Output(UInt((dataWidth / 8).W)), - "WVALID" -> Output(Bool()), - "WREADY" -> Input(Bool()), - "BRESP" -> Input(AxiResp()), - "BVALID" -> Input(Bool()), - "BREADY" -> Output(Bool()), - "ARADDR" -> Output(UInt(addrWidth.W)), - "ARPROT" -> Output(UInt(3.W)), - "ARVALID" -> Output(Bool()), - "ARREADY" -> Input(Bool()), - "RDATA" -> Input(UInt(dataWidth.W)), - "RRESP" -> Input(AxiResp()), - "RVALID" -> Input(Bool()), - "RREADY" -> Output(Bool()) - ) - with IsMasterSlave { - override def isMaster = true -} - -object Axi4LiteVerilogIO { - implicit val axi4LiteView: DataView[Axi4LiteVerilogIO, Axi4LiteIO] = - DataView.mapping( - rc => new Axi4LiteIO(rc.dataWidth, rc.addrWidth), - (rc, b) => - Seq( - rc("AWADDR") -> b.aw.bits.addr, - rc("AWPROT") -> b.aw.bits.prot, - rc("AWVALID") -> b.aw.valid, - rc("AWREADY") -> b.aw.ready, - rc("WDATA") -> b.w.bits.data, - rc("WSTRB") -> b.w.bits.strb, - rc("WVALID") -> b.w.valid, - rc("WREADY") -> b.w.ready, - rc("BRESP") -> b.b.bits.resp, - rc("BVALID") -> b.b.valid, - rc("BREADY") -> b.b.ready, - rc("ARADDR") -> b.ar.bits.addr, - rc("ARPROT") -> b.ar.bits.prot, - rc("ARVALID") -> b.ar.valid, - rc("ARREADY") -> b.ar.ready, - rc("RDATA") -> b.r.bits.data, - rc("RRESP") -> b.r.bits.resp, - rc("RVALID") -> b.r.valid, - rc("RREADY") -> b.r.ready - ) - ) -} diff --git a/chipmunk/src/amba/AxiVerilogIO.scala b/chipmunk/src/amba/AxiVerilogIO.scala deleted file mode 100644 index 028077d..0000000 --- a/chipmunk/src/amba/AxiVerilogIO.scala +++ /dev/null @@ -1,224 +0,0 @@ -package chipmunk -package amba - -import chisel3._ -import chisel3.experimental.dataview.DataView - -/** Generate a [[Axi4IO]] or [[Axi3IO]] interface with blackbox-friendly port names. - * - * In many AXI implementations generated by 3rd-party tools (such as ARM NIC or AMD Vivado), the ports are named in the - * form "AWADDR" or "AWADDR_some_postfix", however Chisel users prefer to use hierarchical Bundles (that is, - * [[Axi4IO]]/[[Axi3IO]]). However, the latter will generate signal naming similar to "aw_bits_addr", which is - * difficult to align with blackbox's RTL. You can use this class instead of [[Axi4IO]]/[[Axi3IO]] if you need - * flattened port naming (such as when creating a Blackbox from a external RTL file). - * - * @note - * This class only supports optional name suffixes. Prefixes can be implemented naturally through signal naming. If - * you don't need the prefix part of the port names, consider using [[FlatIO]] instead of [[IO]]. - * - * @example - * {{{ - * val s = FlatIO(Slave(new Axi4IORtlConnector(..., postfix = Some("abc")))) - * val m = IO(Master(new Axi4IO(...))) - * val sView = s0.viewAs[Axi4IO] - * m <> sView - * }}} - * - * @param dataWidth - * The bit width of the [[Axi4IO]]/[[Axi3IO]] bus data. - * @param addrWidth - * The bit width of the [[Axi4IO]]/[[Axi3IO]] bus address. - * @param idWidth - * The bit width of the [[Axi4IO]]/[[Axi3IO]] bus id. - * @param hasQos - * Whether the bus has QoS signals. Default is false. - * @param hasRegion - * Whether the bus has region signals. Default is false. - * @param axi3 - * Whether the bus is AXI3. Default is false (i.e., AXI4). - * @param portNameTransforms - * A sequence of functions to transform the port names. See [[VerilogIO]] for more details. - */ -private[amba] class AxiVerilogIO( - val dataWidth: Int, - val addrWidth: Int, - val idWidth: Int, - val hasQos: Boolean = false, - val hasRegion: Boolean = false, - axi3: Boolean = false -)(portNameTransforms: Seq[String => String] = Seq.empty) - extends VerilogIO(portNameTransforms)({ - val strobeWidth: Int = dataWidth / 8 - val lockWidth: Int = if (axi3) 2 else 1 - val lenWidth: Int = if (axi3) 4 else 8 - - val axiPorts = Seq( - "AWID" -> (if (idWidth != 0) Some(Output(UInt(idWidth.W))) else None), - "AWADDR" -> Output(UInt(addrWidth.W)), - "AWLEN" -> Output(UInt(lenWidth.W)), - "AWSIZE" -> Output(AxiBurstSize()), - "AWBURST" -> Output(AxiBurstType()), - "AWLOCK" -> Output(UInt(lockWidth.W)), - "AWCACHE" -> Output(UInt(4.W)), - "AWPROT" -> Output(UInt(3.W)), - "AWQOS" -> (if (hasQos) Some(Output(UInt(4.W))) else None), - "AWREGION" -> (if (hasRegion) Some(Output(UInt(4.W))) else None), - "AWVALID" -> Output(Bool()), - "AWREADY" -> Input(Bool()), - "WID" -> (if (idWidth != 0 && axi3) Some(Output(UInt(idWidth.W))) else None), - "WDATA" -> Output(UInt(dataWidth.W)), - "WSTRB" -> Output(UInt(strobeWidth.W)), - "WLAST" -> Output(Bool()), - "WVALID" -> Output(Bool()), - "WREADY" -> Input(Bool()), - "BID" -> (if (idWidth != 0) Some(Input(UInt(idWidth.W))) else None), - "BRESP" -> Input(AxiResp()), - "BVALID" -> Input(Bool()), - "BREADY" -> Output(Bool()), - "ARID" -> (if (idWidth != 0) Some(Output(UInt(idWidth.W))) else None), - "ARADDR" -> Output(UInt(addrWidth.W)), - "ARLEN" -> Output(UInt(lenWidth.W)), - "ARSIZE" -> Output(AxiBurstSize()), - "ARBURST" -> Output(AxiBurstType()), - "ARLOCK" -> Output(UInt(lockWidth.W)), - "ARCACHE" -> Output(UInt(4.W)), - "ARPROT" -> Output(UInt(3.W)), - "ARQOS" -> (if (hasQos) Some(Output(UInt(4.W))) else None), - "ARREGION" -> (if (hasRegion) Some(Output(UInt(4.W))) else None), - "ARVALID" -> Output(Bool()), - "ARREADY" -> Input(Bool()), - "RID" -> (if (idWidth != 0) Some(Input(UInt(idWidth.W))) else None), - "RDATA" -> Input(UInt(dataWidth.W)), - "RRESP" -> Input(AxiResp()), - "RLAST" -> Input(Bool()), - "RVALID" -> Input(Bool()), - "RREADY" -> Output(Bool()) - ).collect { - case (key, Some(value: Data)) => key -> value - case (key, value: Data) => key -> value - } - axiPorts - }: _*) - with IsMasterSlave { - if (axi3) { - require(!hasQos, "AXI3 does not support QoS signals.") - require(!hasRegion, "AXI3 does not support region signals.") - } - - override def isMaster = true -} - -object AxiVerilogIO { - private def mapPorts(rc: AxiVerilogIO, b: AxiIOBase) = { - var portPairs = Seq( - rc("AWADDR") -> b.aw.bits.addr, - rc("AWLEN") -> b.aw.bits.len, - rc("AWSIZE") -> b.aw.bits.size, - rc("AWBURST") -> b.aw.bits.burst, - rc("AWLOCK") -> b.aw.bits.lock, - rc("AWCACHE") -> b.aw.bits.cache, - rc("AWPROT") -> b.aw.bits.prot, - rc("AWVALID") -> b.aw.valid, - rc("AWREADY") -> b.aw.ready, - rc("WDATA") -> b.w.bits.data, - rc("WSTRB") -> b.w.bits.strb, - rc("WLAST") -> b.w.bits.last, - rc("WVALID") -> b.w.valid, - rc("WREADY") -> b.w.ready, - rc("BRESP") -> b.b.bits.resp, - rc("BVALID") -> b.b.valid, - rc("BREADY") -> b.b.ready, - rc("ARADDR") -> b.ar.bits.addr, - rc("ARLEN") -> b.ar.bits.len, - rc("ARSIZE") -> b.ar.bits.size, - rc("ARBURST") -> b.ar.bits.burst, - rc("ARLOCK") -> b.ar.bits.lock, - rc("ARCACHE") -> b.ar.bits.cache, - rc("ARPROT") -> b.ar.bits.prot, - rc("ARVALID") -> b.ar.valid, - rc("ARREADY") -> b.ar.ready, - rc("RDATA") -> b.r.bits.data, - rc("RRESP") -> b.r.bits.resp, - rc("RLAST") -> b.r.bits.last, - rc("RVALID") -> b.r.valid, - rc("RREADY") -> b.r.ready - ) - if (rc.hasRegion) { - portPairs ++= Seq(rc("AWREGION") -> b.aw.bits.region.get, rc("ARREGION") -> b.ar.bits.region.get) - } - if (rc.hasQos) { - portPairs ++= Seq(rc("AWQOS") -> b.aw.bits.qos.get, rc("ARQOS") -> b.ar.bits.qos.get) - } - if (rc.idWidth != 0) { - portPairs ++= Seq( - rc("AWID") -> b.aw.bits.id.get, - rc("BID") -> b.b.bits.id.get, - rc("ARID") -> b.ar.bits.id.get, - rc("RID") -> b.r.bits.id.get - ) - if (b.w.bits.id.isDefined) { // AXI3 - portPairs ++= Seq(rc("WID") -> b.w.bits.id.get) - } - } - portPairs - } - - implicit val axi4View: DataView[Axi4VerilogIO, Axi4IO] = DataView.mapping( - rc => new Axi4IO(rc.dataWidth, rc.addrWidth, rc.idWidth, rc.hasQos, rc.hasRegion), - (rc, b) => mapPorts(rc, b) - ) - - implicit val axi3View: DataView[Axi3VerilogIO, Axi3IO] = - DataView.mapping(rc => new Axi3IO(rc.dataWidth, rc.addrWidth, rc.idWidth), (rc, b) => mapPorts(rc, b)) -} - -/** Generate a [[Axi4IO]] interface with blackbox-friendly port names. - * - * @example - * {{{ - * val s = FlatIO(Slave(new Axi4IORtlConnector(..., postfix = Some("abc")))) - * val m = IO(Master(new Axi4IO(...))) - * val sView = s0.viewAs[Axi4IO] - * m <> sView - * }}} - * @param dataWidth - * The bit width of the [[Axi4IO]] bus data. - * @param addrWidth - * The bit width of the [[Axi4IO]] bus address. - * @param idWidth - * The bit width of the [[Axi4IO]] bus id. - * @param hasQos - * Whether the bus has QoS signals. Default is false. - * @param hasRegion - * Whether the bus has region signals. Default is false. - * @param portNameTransforms - * A sequence of functions to transform the port names. See [[VerilogIO]] for more details. - * @see - * [[AxiVerilogIO]] - */ -class Axi4VerilogIO(dataWidth: Int, addrWidth: Int, idWidth: Int, hasQos: Boolean = false, hasRegion: Boolean = false)( - portNameTransforms: Seq[String => String] = Seq.empty -) extends AxiVerilogIO(dataWidth, addrWidth, idWidth, hasQos, hasRegion)(portNameTransforms) - -/** Generate a [[Axi3IO]] interface with blackbox-friendly port names. - * - * @example - * {{{ - * val s = FlatIO(Slave(new Axi3IORtlConnector(..., postfix = Some("abc")))) - * val m = IO(Master(new Axi3IO(...))) - * val sView = s0.viewAs[Axi3IO] - * m <> sView - * }}} - * @param dataWidth - * The bit width of the [[Axi3IO]] bus data. - * @param addrWidth - * The bit width of the [[Axi3IO]] bus address. - * @param idWidth - * The bit width of the [[Axi3IO]] bus id. - * @param portNameTransforms - * A sequence of functions to transform the port names. See [[VerilogIO]] for more details. - * @see - * [[AxiVerilogIO]] - */ -class Axi3VerilogIO(dataWidth: Int, addrWidth: Int, idWidth: Int)(portNameTransforms: Seq[String => String] = Seq.empty) - extends AxiVerilogIO(dataWidth, addrWidth, idWidth, axi3 = true)(portNameTransforms) diff --git a/chipmunk/src/component/acorn/AcornIO.scala b/chipmunk/src/component/acorn/AcornIO.scala deleted file mode 100644 index 9d999f3..0000000 --- a/chipmunk/src/component/acorn/AcornIO.scala +++ /dev/null @@ -1,67 +0,0 @@ -package chipmunk -package component.acorn - -import stream._ - -import chisel3._ - -class AcornWrCmdChannel(val dataWidth: Int, val addrWidth: Int) extends Bundle { - - /** The write address. */ - val addr = UInt(addrWidth.W) - - /** The write data. */ - val data = UInt(dataWidth.W) - - /** The byte mask. */ - val strobe = UInt(AcornIO.strobeWidth(dataWidth).W) -} - -class AcornWrRspChannel() extends Bundle { - - /** Whether the write access fails. */ - val error = Bool() -} - -class AcornRdCmdChannel(val addrWidth: Int) extends Bundle { - - /** The read address. */ - val addr = UInt(addrWidth.W) -} - -class AcornRdRspChannel(val dataWidth: Int) extends Bundle { - - /** The responded read-back data. */ - val data = UInt(dataWidth.W) - - /** Whether the read access fails. */ - val error = Bool() -} - -class AcornIO(val dataWidth: Int, val addrWidth: Int) extends Bundle with IsMasterSlave { - override def isMaster = true - - require( - dataWidth > 0 && (dataWidth & (dataWidth - 1)) == 0, - s"Data width of Acorn bus must be power of 2, but got $dataWidth." - ) - require(addrWidth > 0, s"Address width of Acorn bus must be at least 1, but got $addrWidth.") - - val strobeWidth: Int = AcornIO.strobeWidth(dataWidth) - - val rd = new Bundle { - val cmd = Master(Stream(new AcornRdCmdChannel(addrWidth))) - val rsp = Slave(Stream(new AcornRdRspChannel(dataWidth))) - } - val wr = new Bundle { - val cmd = Master(Stream(new AcornWrCmdChannel(dataWidth, addrWidth))) - val rsp = Slave(Stream(new AcornWrRspChannel())) - } -} - -object AcornIO { - def strobeWidth(dataWidth: Int, maskUnit: Int = 8): Int = { - val strobeWidth: Int = math.ceil(dataWidth.toDouble / maskUnit).toInt - strobeWidth - } -} diff --git a/chipmunk/src/component/acorn/AcornToAxiLiteBridge.scala b/chipmunk/src/component/acorn/AcornToAxiLiteBridge.scala deleted file mode 100644 index 2fda79a..0000000 --- a/chipmunk/src/component/acorn/AcornToAxiLiteBridge.scala +++ /dev/null @@ -1,48 +0,0 @@ -package chipmunk -package component.acorn - -import amba._ - -import chisel3._ - -class AcornToAxiLiteBridge(dataWidth: Int = 32, addrWidth: Int = 32) extends Module { - val io = IO(new Bundle { - val sAcorn = Slave(new AcornIO(dataWidth, addrWidth)) - val mAxiL = Master(new Axi4LiteIO(dataWidth, addrWidth)) - }) - - // --------------------------------------------------------------------------- - // Write (aw, w & b) - - val waitWriteChannel = RegInit(false.B) - when(io.mAxiL.aw.fire && !io.mAxiL.w.fire) { - waitWriteChannel := true.B - }.elsewhen(io.mAxiL.w.fire) { - waitWriteChannel := false.B - } - - io.mAxiL.aw.valid := io.sAcorn.wr.cmd.valid && !waitWriteChannel - - io.sAcorn.wr.cmd.ready := io.mAxiL.w.fire && (waitWriteChannel || io.mAxiL.aw.fire) - io.mAxiL.w.valid := io.sAcorn.wr.cmd.valid - - io.mAxiL.aw.bits.addr := io.sAcorn.wr.cmd.bits.addr - io.mAxiL.aw.bits.prot := 0.U - - io.mAxiL.w.bits.data := io.sAcorn.wr.cmd.bits.data - io.mAxiL.w.bits.strb := io.sAcorn.wr.cmd.bits.strobe - - io.sAcorn.wr.rsp handshakeFrom io.mAxiL.b - io.sAcorn.wr.rsp.bits.error := io.mAxiL.b.bits.resp =/= AxiResp.RESP_OKAY - - // --------------------------------------------------------------------------- - // Read (ar & r) - - io.mAxiL.ar handshakeFrom io.sAcorn.rd.cmd - io.mAxiL.ar.bits.addr := io.sAcorn.rd.cmd.bits.addr - io.mAxiL.ar.bits.prot := 0.U - - io.sAcorn.rd.rsp handshakeFrom io.mAxiL.r - io.sAcorn.rd.rsp.bits.data := io.mAxiL.r.bits.data - io.sAcorn.rd.rsp.bits.error := io.mAxiL.r.bits.resp =/= AxiResp.RESP_OKAY -} diff --git a/chipmunk/src/component/acorn/Axi4ToAcornBridge.scala b/chipmunk/src/component/acorn/Axi4ToAcornBridge.scala deleted file mode 100644 index 98dc6aa..0000000 --- a/chipmunk/src/component/acorn/Axi4ToAcornBridge.scala +++ /dev/null @@ -1,193 +0,0 @@ -package chipmunk -package component.acorn - -import amba._ - -import chisel3._ -import chisel3.util._ - -class Axi4ToAcornBridge(dataWidth: Int = 32, addrWidth: Int = 32, idWidth: Int = 0) extends Module { - val io = IO(new Bundle { - val sAxi4 = Slave(new Axi4IO(dataWidth = dataWidth, addrWidth = addrWidth, idWidth = idWidth)) - val mAcorn = Master(new AcornIO(dataWidth = dataWidth, addrWidth = addrWidth)) - }) - - def burstAddrNext(addr: UInt, burst: AxiBurstType.Type, len: UInt): UInt = { - val addrNext = Wire(UInt(addrWidth.W)) - addrNext := MuxLookup(burst, addr) { - Seq( - AxiBurstType.BURST_FIXED -> addr, - AxiBurstType.BURST_INCR -> { - addr + io.sAxi4.strobeWidth.U - }, - AxiBurstType.BURST_WRAP -> { - val wrapSize = Wire(UInt(32.W)) - wrapSize := len << log2Ceil(io.sAxi4.strobeWidth) - val wrapEnable = (addr & wrapSize) === wrapSize - Mux(wrapEnable, addr - wrapSize, addr + io.sAxi4.strobeWidth.U) - } - ) - } - addrNext - } - - // --------------------------------------------------------------------------- - // Read - - object ReadState extends ChiselEnum { - val IDLE, BUSY = Value - } - - val readCmdState = RegInit(ReadState.IDLE) - val readRespState = RegInit(ReadState.IDLE) - val readCmdCnt = RegInit(0.U(8.W)) - val readRespCnt = RegInit(0.U(8.W)) - - val arAddr = RegInit(0.U(io.sAxi4.addrWidth.W)) - val arBurst = RegInit(AxiBurstType.BURST_INCR) - val arLen = RegInit(0.U(8.W)) - val rId = if (io.sAxi4.hasId) Some(RegInit(0.U(io.sAxi4.idWidth.W))) else None - - io.mAcorn.rd.cmd.valid := readCmdState === ReadState.BUSY - io.mAcorn.rd.cmd.bits.addr := arAddr - - io.sAxi4.ar.ready := readCmdState === ReadState.IDLE && readRespState === ReadState.IDLE - - io.sAxi4.r handshakeFrom io.mAcorn.rd.rsp - io.sAxi4.r.bits.data := io.mAcorn.rd.rsp.bits.data - io.sAxi4.r.bits.resp := Mux(io.mAcorn.rd.rsp.bits.error, AxiResp.RESP_SLVERR, AxiResp.RESP_OKAY) - io.sAxi4.r.bits.last := readRespCnt === arLen - if (io.sAxi4.hasId) { io.sAxi4.r.bits.id.get := rId.get } - - switch(readCmdState) { - is(ReadState.IDLE) { - readCmdCnt := 0.U - when(io.sAxi4.ar.fire) { - readCmdState := ReadState.BUSY - arAddr := io.sAxi4.ar.bits.addr - arBurst := io.sAxi4.ar.bits.burst - arLen := io.sAxi4.ar.bits.len - if (io.sAxi4.hasId) { - rId.get := io.sAxi4.ar.bits.id.get - } - } - } - is(ReadState.BUSY) { - when(io.mAcorn.rd.cmd.fire) { - readCmdCnt := readCmdCnt + 1.U - when(readCmdCnt === arLen) { - readCmdState := ReadState.IDLE - } - arAddr := burstAddrNext(arAddr, arBurst, arLen) - } - } - } - - switch(readRespState) { - is(ReadState.IDLE) { - readRespCnt := 0.U - when(io.sAxi4.ar.fire) { - readRespState := ReadState.BUSY - } - } - is(ReadState.BUSY) { - when(io.mAcorn.rd.rsp.fire) { - readRespCnt := readRespCnt + 1.U - when(readRespCnt === arLen) { - readRespState := ReadState.IDLE - } - } - } - } - - // --------------------------------------------------------------------------- - // Write - - object WriteState extends ChiselEnum { - val IDLE, BUSY, WAIT_RESP, ACK_RESP = Value - } - - val writeCmdState = RegInit(WriteState.IDLE) - - // val writeCmdBusy = RegInit(false.B) - val writeRespBusy = RegInit(false.B) - val writeCmdCnt = RegInit(0.U(8.W)) - val writeRespCnt = RegInit(0.U(8.W)) - - val awAddr = RegInit(0.U(io.sAxi4.addrWidth.W)) - val awBurst = RegInit(AxiBurstType.BURST_INCR) - val awLen = RegInit(0.U(8.W)) - - // val wLast = RegInit(false.B) - - val bValid = RegInit(false.B) - val bId = if (idWidth > 0) Some(RegInit(0.U(io.sAxi4.idWidth.W))) else None - val bResp = RegInit(AxiResp.RESP_OKAY) - - io.sAxi4.aw.ready := writeCmdState === WriteState.IDLE - io.sAxi4.w.ready := writeCmdState === WriteState.BUSY && io.mAcorn.wr.cmd.ready - io.sAxi4.b.valid := bValid - io.sAxi4.b.bits.resp := bResp - if (io.sAxi4.hasId) { io.sAxi4.b.bits.id.get := bId.get } - - io.mAcorn.wr.cmd.valid := writeCmdState === WriteState.BUSY && io.sAxi4.w.valid - io.mAcorn.wr.cmd.bits.addr := awAddr - io.mAcorn.wr.cmd.bits.data := io.sAxi4.w.bits.data - io.mAcorn.wr.cmd.bits.strobe := io.sAxi4.w.bits.strb - - io.mAcorn.wr.rsp.ready := io.sAxi4.b.ready - - switch(writeCmdState) { - is(WriteState.IDLE) { - bValid := false.B - when(io.sAxi4.aw.fire) { - writeCmdState := WriteState.BUSY - awAddr := io.sAxi4.aw.bits.addr - awLen := io.sAxi4.aw.bits.len - writeCmdCnt := 0.U - if (io.sAxi4.hasId) { - bId.get := io.sAxi4.aw.bits.id.get - } - } - } - is(WriteState.BUSY) { - when(io.sAxi4.w.fire) { - writeCmdCnt := writeCmdCnt + 1.U - when(writeCmdCnt === awLen) { - writeCmdState := WriteState.WAIT_RESP - } - awAddr := burstAddrNext(awAddr, awBurst, awLen) - } - } - is(WriteState.WAIT_RESP) { - when(writeRespBusy && io.mAcorn.wr.rsp.fire && writeRespCnt === awLen) { - when(io.sAxi4.b.isStarving) { - writeCmdState := WriteState.IDLE - bValid := true.B - }.otherwise { - writeCmdState := WriteState.ACK_RESP - } - } - } - is(WriteState.ACK_RESP) { - when(io.sAxi4.b.fire) { - writeCmdState := WriteState.IDLE - bValid := false.B - } - } - } - - when(!writeRespBusy) { - writeRespCnt := 0.U - when(io.sAxi4.aw.fire) { - writeRespBusy := true.B - } - }.otherwise { - when(io.mAcorn.wr.rsp.fire) { - writeRespCnt := writeRespCnt + 1.U - when(writeRespCnt === awLen) { - writeRespBusy := false.B - } - } - } -} diff --git a/chipmunk/src/component/spi/SpiDebugger.scala b/chipmunk/src/component/spi/SpiDebugger.scala deleted file mode 100644 index a50ca94..0000000 --- a/chipmunk/src/component/spi/SpiDebugger.scala +++ /dev/null @@ -1,304 +0,0 @@ -package chipmunk -package component.spi - -import component.acorn._ -import regbank._ - -import chisel3._ -import chisel3.util._ - -/** Chip debugger, which allows users to access the on-chip bus through SPI. - * - * The bus access interface is a parameter-configurable [[AcornIO]] master interface. It can be connected to a AMBA AXI - * interconnect via [[AcornToAxiLiteBridge]]. - * - * @param spiClockPriority - * Clock priority of SPI, also known as CPOL. If true, idle state of SCK is high, otherwise low. - * @param spiClockPhase - * Clock phase of SPI, also known as CPHA. If true, transmission occurs on SCK transition from active to idle, - * otherwise from idle to active. - * @param busAddrWidth - * The bit width of the bus address. - */ -class SpiDebugger( - hasMisoValid: Boolean = false, - spiClockPriority: Boolean = false, - spiClockPhase: Boolean = false, - busAddrWidth: Int = 32 -) extends Module { - require(busAddrWidth <= 64, s"busAddrWidth must be less than or equal to 64 but got $busAddrWidth") - - val io = IO(new Bundle { - val sSpi = Slave(new SpiIO(hasMisoValid)) - val mDbg = Master(new AcornIO(dataWidth = 32, addrWidth = busAddrWidth)) - }) - - // --------------------------------------------------------------------------- - // SPI input signal synchronization - - val spiSsnSyncRegs = ShiftRegisters(io.sSpi.ssn, 3, true.B, true.B) - val spiSckSyncRegs = ShiftRegisters(io.sSpi.sck, 3, spiClockPriority.B, true.B) - - val spiMosiSync = ShiftRegister(io.sSpi.mosi, 3, false.B, true.B) - - val spiSckPosEdge = !spiSckSyncRegs(2) && spiSckSyncRegs(1) - val spiSckNegEdge = !spiSckSyncRegs(1) && spiSckSyncRegs(2) - val spiSsnPosEdge = !spiSsnSyncRegs(2) && spiSsnSyncRegs(1) - val spiSsnNegEdge = !spiSsnSyncRegs(1) && spiSsnSyncRegs(2) - - // If CPOL = CPHA = 0/1, use SCK rising edge as sample edge, otherwise use falling edge - val spiRxSampleEdge = if (spiClockPriority == spiClockPhase) spiSckPosEdge else spiSckNegEdge - val spiTxSampleEdge = if (spiClockPriority != spiClockPhase) spiSckPosEdge else spiSckNegEdge - - // Delay the sample edge to trigger a internal register update - val spiRxSampleEdgeDelay = spiTxSampleEdge - // val spiRxSampleEdgeDelay = ShiftRegister(spiRxSampleEdge, 1, false.B, true.B) - - // --------------------------------------------------------------------------- - // SPI controlling FSM - - object SpiState extends ChiselEnum { - val IDLE, CMD, REG_WR_DATA, REG_RD_DATA, BUS_ADDR, BUS_WR_DATA, BUS_RD_DUMMY, BUS_RD_DATA = Value - } - - object SpiCmd extends ChiselEnum { - val NOP, REG_WR, REG_RD, BUS_WR, BUS_RD = Value - } - - val spiStateCurr = RegInit(SpiState.IDLE) - val spiStateNext = WireDefault(spiStateCurr) - spiStateCurr := spiStateNext - - val spiStateCnt = RegInit(0.U(5.W)) - val spiStateCntSck8 = spiStateCnt === 7.U - val spiStateCntSck32 = spiStateCnt === 31.U - val spiStateCntFull = MuxLookup(spiStateCurr, false.B)( - Seq( - SpiState.CMD -> spiStateCntSck8, - SpiState.REG_WR_DATA -> spiStateCntSck32, - SpiState.REG_RD_DATA -> spiStateCntSck32, - SpiState.BUS_ADDR -> spiStateCntSck32, - SpiState.BUS_WR_DATA -> spiStateCntSck32, - SpiState.BUS_RD_DUMMY -> spiStateCntSck8, - SpiState.BUS_RD_DATA -> spiStateCntSck32 - ) - ) - val spiStateCntDone = spiStateCntFull && spiRxSampleEdgeDelay - when(spiStateCurr === SpiState.IDLE || spiStateCntDone) { - spiStateCnt := 0.U - }.elsewhen(spiRxSampleEdgeDelay) { - spiStateCnt := spiStateCnt + 1.U - } - - // Declare here to avoid "not fully initialized" error - val spiCmdNext = WireDefault(SpiCmd.NOP) - val spiCmdCurr = RegEnable(spiCmdNext, SpiCmd.NOP, spiStateCntDone && spiStateCurr === SpiState.CMD) - - when(spiSsnPosEdge) { - spiStateNext := SpiState.IDLE - }.otherwise { - switch(spiStateCurr) { - is(SpiState.IDLE) { - when(spiSsnNegEdge) { - spiStateNext := SpiState.CMD - } - } - is(SpiState.CMD) { - when(spiStateCntDone) { - spiStateNext := MuxLookup(spiCmdNext, SpiState.CMD)( - Seq( - SpiCmd.NOP -> SpiState.IDLE, - SpiCmd.REG_WR -> SpiState.REG_WR_DATA, - SpiCmd.REG_RD -> SpiState.REG_RD_DATA, - SpiCmd.BUS_WR -> SpiState.BUS_ADDR, - SpiCmd.BUS_RD -> SpiState.BUS_ADDR - ) - ) - } - } - is(SpiState.BUS_ADDR) { - when(spiStateCntDone) { - spiStateNext := MuxLookup(spiCmdCurr, SpiState.IDLE)( - Seq(SpiCmd.BUS_RD -> SpiState.BUS_RD_DUMMY, SpiCmd.BUS_WR -> SpiState.BUS_WR_DATA) - ) - } - } - is(SpiState.REG_WR_DATA, SpiState.REG_RD_DATA, SpiState.BUS_WR_DATA, SpiState.BUS_RD_DATA) { - when(spiStateCntDone) { - spiStateNext := SpiState.IDLE - } - } - is(SpiState.BUS_RD_DUMMY) { - when(spiStateCntDone) { - spiStateNext := SpiState.BUS_RD_DATA - } - } - } - } - - // --------------------------------------------------------------------------- - // SPI RX data - - val spiRxEnable = spiStateCurr.isOneOf(SpiState.CMD, SpiState.REG_WR_DATA, SpiState.BUS_WR_DATA, SpiState.BUS_ADDR) - - val spiRxData = RegInit(0.U(32.W)) - when(!spiRxEnable) { - spiRxData := 0.U - }.elsewhen(spiRxSampleEdge) { - spiRxData := spiRxData(30, 0) ## spiMosiSync - } - - // --------------------------------------------------------------------------- - // SPI cmd decoding - - val spiCmdRaw = spiRxData.lsBits(8) - when(spiCmdRaw === 0x80.U) { - spiCmdNext := SpiCmd.BUS_WR - }.elsewhen(spiCmdRaw === 0xc0.U) { - spiCmdNext := SpiCmd.BUS_RD - }.elsewhen(!spiCmdRaw(7)) { - spiCmdNext := Mux(spiCmdRaw(6), SpiCmd.REG_RD, SpiCmd.REG_WR) - } - - // --------------------------------------------------------------------------- - // Register file - - object RegFileAddrMap extends ChiselEnum { - val BUS_ADDR_L, BUS_ADDR_H, BUS_WR_RESP, BUS_RD_RESP, BUS_WR_DATA, BUS_RD_DATA, BUS_WR_MASK = Value - - val TEST = Value(0x3f.U(6.W)) - } - - val regFileWrEnable = spiStateCntDone && spiStateCurr === SpiState.REG_WR_DATA - val regFileWrAddr = RegEnable(spiCmdRaw.lsBits(6), 0.U, spiStateCntDone && spiStateCurr === SpiState.CMD) - val regFileRdAddr = spiCmdRaw.lsBits(6) - - val regFileConfigs: Seq[RegElementConfig] = { - import RegFieldAccessType._ - import RegFileAddrMap._ - - val regFileConfigAddrH: Seq[RegElementConfig] = if (busAddrWidth > 32) { - Seq(RegElementConfig("BUS_ADDR_H", addr = BUS_ADDR_H.litValue, bitCount = 32)) - } else Seq() - - regFileConfigAddrH ++ Seq( - RegElementConfig("BUS_ADDR_L", addr = BUS_ADDR_L.litValue, bitCount = busAddrWidth min 32, backdoorUpdate = true), - RegElementConfig( - "BUS_WR_RESP", - addr = BUS_WR_RESP.litValue, - bitCount = 1, - accessType = ReadOnly, - backdoorUpdate = true - ), - RegElementConfig( - "BUS_RD_RESP", - addr = BUS_RD_RESP.litValue, - bitCount = 1, - accessType = ReadOnly, - backdoorUpdate = true - ), - RegElementConfig("BUS_WR_DATA", addr = BUS_WR_DATA.litValue, bitCount = 32, backdoorUpdate = true), - RegElementConfig("BUS_RD_DATA", addr = BUS_RD_DATA.litValue, bitCount = 32, backdoorUpdate = true), - RegElementConfig( - "BUS_WR_MASK", - addr = BUS_WR_MASK.litValue, - bitCount = io.mDbg.strobeWidth, - initValue = ((2 << io.mDbg.strobeWidth) - 1).U - ), - RegElementConfig("TEST", addr = TEST.litValue, bitCount = 32, initValue = 0x3f1b_00e5.U(32.W)) - ) - } - - val uSpiRegFile = Module(new RegBank(addrWidth = 6, dataWidth = 32, regs = regFileConfigs)) - - // One-cycle command valid is enough, because response channel is always ready. - uSpiRegFile.io.access.wr.cmd.valid := regFileWrEnable - uSpiRegFile.io.access.wr.cmd.bits.addr := regFileWrAddr - uSpiRegFile.io.access.wr.cmd.bits.data := spiRxData - uSpiRegFile.io.access.wr.rsp.ready := true.B - uSpiRegFile.io.access.wr.cmd.bits.strobe.assignOnes() - - uSpiRegFile.io.access.rd.cmd.valid := true.B - uSpiRegFile.io.access.rd.cmd.bits.addr := regFileRdAddr - uSpiRegFile.io.access.rd.rsp.ready := true.B - - val regRdData = uSpiRegFile.io.access.rd.rsp.bits.data - - uSpiRegFile.io.fields("BUS_RD_DATA").backdoorUpdate.get.valid := io.mDbg.rd.rsp.fire - uSpiRegFile.io.fields("BUS_RD_DATA").backdoorUpdate.get.bits := io.mDbg.rd.rsp.bits.data - - uSpiRegFile.io.fields("BUS_WR_RESP").backdoorUpdate.get.valid := io.mDbg.wr.rsp.fire - uSpiRegFile.io.fields("BUS_WR_RESP").backdoorUpdate.get.bits := io.mDbg.wr.rsp.bits.error.asUInt - - uSpiRegFile.io.fields("BUS_RD_RESP").backdoorUpdate.get.valid := io.mDbg.rd.rsp.fire - uSpiRegFile.io.fields("BUS_RD_RESP").backdoorUpdate.get.bits := io.mDbg.rd.rsp.bits.error.asUInt - - val busAddrBufUpdate = spiStateCntDone && spiStateCurr === SpiState.BUS_ADDR - uSpiRegFile.io.fields("BUS_ADDR_L").backdoorUpdate.get.valid := busAddrBufUpdate - uSpiRegFile.io.fields("BUS_ADDR_L").backdoorUpdate.get.bits := spiRxData.lsBits(busAddrWidth min 32) - - val busWrDataBufUpdate = spiStateCntDone && spiStateCurr === SpiState.BUS_WR_DATA - uSpiRegFile.io.fields("BUS_WR_DATA").backdoorUpdate.get.valid := busWrDataBufUpdate - uSpiRegFile.io.fields("BUS_WR_DATA").backdoorUpdate.get.bits := spiRxData - - // --------------------------------------------------------------------------- - // Bus access - - // It does NOT check whether the previous request is finished before issuing a new request. - // If a new request is issued before the previous one is finished, it may cause some unexpected behavior. To avoid - // this, users should ensure that the SPI clock frequency is much slower than the on-chip bus access. - - val busWrReqFast = spiStateCurr === SpiState.BUS_WR_DATA && spiStateNext === SpiState.IDLE - val busRdReqFast = spiStateCurr === SpiState.BUS_ADDR && spiStateNext === SpiState.BUS_RD_DUMMY - val busWrReqNorm = uSpiRegFile.io.fields("BUS_WR_RESP").isBeingWritten - val busRdReqNorm = uSpiRegFile.io.fields("BUS_RD_RESP").isBeingWritten - - val busWrReqValid: Bool = RegInit(false.B) - when(busWrReqFast || busWrReqNorm) { - busWrReqValid := true.B - }.elsewhen(io.mDbg.wr.cmd.fire) { - busWrReqValid := false.B - } - - val buRdReqValid: Bool = RegInit(false.B) - when(busRdReqFast || busRdReqNorm) { - buRdReqValid := true.B - }.elsewhen(io.mDbg.rd.cmd.fire) { - buRdReqValid := false.B - } - - io.mDbg.wr.cmd.valid := busWrReqValid - io.mDbg.rd.cmd.valid := buRdReqValid - - io.mDbg.wr.cmd.bits.data := uSpiRegFile.io.fields("BUS_WR_DATA").value - io.mDbg.wr.cmd.bits.strobe := uSpiRegFile.io.fields("BUS_WR_MASK").value - - if (busAddrWidth > 32) { - io.mDbg.wr.cmd.bits.addr := uSpiRegFile.io.fields("BUS_ADDR_H").value ## uSpiRegFile.io.fields("BUS_ADDR_L").value - io.mDbg.rd.cmd.bits.addr := uSpiRegFile.io.fields("BUS_ADDR_H").value ## uSpiRegFile.io.fields("BUS_ADDR_L").value - } else { - io.mDbg.wr.cmd.bits.addr := uSpiRegFile.io.fields("BUS_ADDR_L").value - io.mDbg.rd.cmd.bits.addr := uSpiRegFile.io.fields("BUS_ADDR_L").value - } - - io.mDbg.wr.rsp.ready := true.B - io.mDbg.rd.rsp.ready := true.B - - // --------------------------------------------------------------------------- - // SPI TX data - - val spiTxEnable = spiStateCurr.isOneOf(SpiState.BUS_RD_DATA, SpiState.REG_RD_DATA) - val spiTxData = RegInit(0.U(32.W)) - when(spiStateCurr === SpiState.CMD && spiStateNext === SpiState.REG_RD_DATA) { - spiTxData := regRdData - }.elsewhen(spiStateCurr === SpiState.BUS_RD_DUMMY && spiStateNext === SpiState.BUS_RD_DATA) { - spiTxData := uSpiRegFile.io.fields("BUS_RD_DATA").value - }.elsewhen(spiTxEnable && spiTxSampleEdge) { - spiTxData := spiTxData << 1 - } - - io.sSpi.miso := spiTxData(31) - if (hasMisoValid) { - io.sSpi.misoValid.get := RegNext(io.sSpi.ssn === false.B) - } -} diff --git a/chipmunk/src/component/spi/SpiIO.scala b/chipmunk/src/component/spi/SpiIO.scala deleted file mode 100644 index 056615a..0000000 --- a/chipmunk/src/component/spi/SpiIO.scala +++ /dev/null @@ -1,21 +0,0 @@ -package chipmunk -package component.spi - -import chisel3._ - -/** Four-wire SPI IO. - * - * @param hasMisoValid - * whether the MISO signal has a valid signal for tri-state IO-cell control. Note that only the Slave-side SPI may - * have this signal. - */ -class SpiIO(val hasMisoValid: Boolean = false) extends Bundle with IsMasterSlave { - - val sck = Output(Bool()) - val ssn = Output(Bool()) - val mosi = Output(Bool()) - val miso = Input(Bool()) - val misoValid = if (hasMisoValid) Some(Input(Bool())) else None - - def isMaster = true -} diff --git a/chipmunk/src/package.scala b/chipmunk/src/package.scala deleted file mode 100644 index fad79e7..0000000 --- a/chipmunk/src/package.scala +++ /dev/null @@ -1,70 +0,0 @@ -import chisel3._ -import chisel3.util._ - -package chipmunk { - - /** A Bundle with no elements. */ - final class EmptyBundle extends Bundle -} - -package object chipmunk { - - implicit class AddMethodsToBits[T <: Bits](c: T) { - - /** Returns the most significant n bits. */ - def msBits(n: Int = 1): UInt = c.head(n) - - /** Returns the least significant n bits. */ - def lsBits(n: Int = 1): UInt = c(n - 1, 0) - - /** Returns the most significant bit as Bool. */ - def msBit: Bool = c.head(1).asBool - - /** Returns the least significant bit as Bool. */ - def lsBit: Bool = c(0).asBool - - /** Returns a signal having the same type as this signal, but with all bits set to the specified Bool literal. */ - def filledWith(b: Bool): T = Fill(c.getWidth, b).asTypeOf(c) - - /** Returns a signal having the same type as this signal, but with all bits set to the specified Boolean literal. */ - def filledWith(b: Boolean): T = filledWith(b.B) - - /** Returns a signal having the same type as this signal, but with all bits set to True. */ - def filledOnes: T = filledWith(true) - - /** Returns a signal having the same type as this signal, but with all bits set to False. */ - def filledZeros: T = filledWith(false) - - /** Assign all bits to True. */ - def assignOnes(): Unit = c := c.filledOnes - - /** Assign all bits to False. */ - def assignZeros(): Unit = c := c.filledZeros - - /** Returns whether the number is one-hot encoded. */ - def isOneHot: Bool = { - val parity = Wire(Vec(c.getWidth, Bool())) - parity(0) := c(0) - for (i <- 1 until c.getWidth) { - parity(i) := parity(i - 1) ^ c(i) - } - val ret = parity(c.getWidth - 1) && (parity.asUInt | (~c).asUInt).andR - ret - } - } - - implicit class AddMethodsToData[T <: Data](c: T) { - - /** Mark this signal as an optimization barrier to Chisel and FIRRTL. Same as `chisel3.dontTouch(x)`. */ - def dontTouch: T = { - chisel3.dontTouch(c) - } - } - - /** Alias for [[chisel3.util.PriorityMux]]. - * - * @note - * Chisel has many muxes named `MuxXxx`, but this is the only one that is named as `XxxMux`. - */ - val MuxPriority = PriorityMux -} diff --git a/chipmunk/src/regbank/RegBank.scala b/chipmunk/src/regbank/RegBank.scala deleted file mode 100644 index 21ed385..0000000 --- a/chipmunk/src/regbank/RegBank.scala +++ /dev/null @@ -1,216 +0,0 @@ -package chipmunk -package regbank - -import component.acorn.AcornIO - -import chisel3._ -import chisel3.util._ - -/** Configuration of a register element in [[RegBank]]. - * - * @param name - * Name of the register element. It should be unique in a [[RegBank]]. Only alphanumeric and underscore are allowed. - * @param addr - * Address of the register element. - * @param fields - * The configuration of register fields in the register element. A register element should have at least one field. - */ -case class RegElementConfig(name: String, addr: BigInt, fields: Seq[RegFieldConfig]) { - require(fields.nonEmpty, "RegElement should have at least one field") - require(name matches "[\\w]+", s"Register element name `$name` is invalid") - - if (fields.length >= 2) { - require( - fields.sortBy(_.baseOffset).combinations(2).forall(x => x.head.endOffset < x.last.baseOffset), - "Bit range overlap is not allowed between two fields" - ) - } -} - -object RegElementConfig { - - /** Create a register element configuration with only one field. - * - * It has only one field with the name "UNNAMED". - * - * @see - * [[RegElementConfig]] - */ - def apply( - name: String, - addr: BigInt, - bitCount: Int, - initValue: UInt = 0.U, - accessType: RegFieldAccessType = RegFieldAccessType.ReadWrite, - collisionMode: RegFieldCollisionMode.Value = RegFieldCollisionMode.BackdoorWriteRead, - backdoorUpdate: Boolean = false - ): RegElementConfig = { - RegElementConfig( - name = name, - addr = addr, - Seq(RegFieldConfig("UNNAMED", 0, bitCount, initValue, accessType, collisionMode, backdoorUpdate)) - ) - } -} - -/** Backdoor access interface of [[RegBank]]. - * - * Use "ElementName_FieldName" to access a register field. For example, "STATUS_VERSION" means the field "VERSION" in - * "STATUS". If the element has only one field, "ElementName" also can be used to access this field. - * - * @param regsConfig - * The configuration of register elements in [[RegBank]]. - */ -class RegBankFieldIO(regsConfig: Seq[RegElementConfig]) - extends MapBundle[RegFieldBackdoorIO]( - regsConfig.flatMap(elemConfig => - elemConfig.fields.map(fieldConfig => { - s"${elemConfig.name}_${fieldConfig.name}" -> Master(new RegFieldBackdoorIO(fieldConfig)) - }) - ): _* - ) - with IsMasterSlave { - override def apply(key: String): RegFieldBackdoorIO = { - if (elements contains key) { - elements(key) - } else { - // If the element has only one field, the field name can be omitted. - val existsFieldName: Option[String] = regsConfig.collectFirst { - case elem if elem.name == key && elem.fields.length == 1 => elem.fields.head.name - } - if (existsFieldName.isDefined) { - elements(s"${key}_${existsFieldName.get}") - } else { - throw new NoSuchElementException - } - } - } - - def isMaster = true -} - -/** Memory-mapped register bank. - * - * A register bank is a collection of register elements, which can be accessed through a memory-mapped interface - * [[AcornIO]]. Each register element can have multiple register fields [[RegField]]. These fields can be read (and - * even updated) by a backdoor interface [[RegBankFieldIO]]. - * - * @param addrWidth - * The bit width of write/read address. - * @param dataWidth - * The bit width of write/read data. - * @param regs - * The configuration of register elements in the register bank. - * @example - * {{{ - * val uRegBank = Module(new RegBank(32, 32, regs = Seq( - * RegElementConfig("STATUS", addr = 0, fields = Seq( - * RegFieldConfig("VERSION", baseOffset = 0, bitCount = 4, accessType = ReadOnly, initValue = 0x1.U(4.W))), - * RegFieldConfig("ERROR", baseOffset = 4, bitCount = 1, backdoorUpdate = true)), - * ) - * )) - * uRegBank.io.access <> ... // memory-mapped access - * - * // Read the value of field STATUS.VERSION - * val version = uRegBank.io.fields("STATUS_VERSION").value - * - * // Update the value of field STATUS.ERROR from backdoor - * uRegBank.io.fields("STATUS_ERROR").backdoorUpdate.get.valid := true.B - * uRegBank.io.fields("STATUS_ERROR").backdoorUpdate.get.bits := 1.U - * }}} - */ -class RegBank(addrWidth: Int, dataWidth: Int, regs: Seq[RegElementConfig]) extends Module { - if (regs.length >= 2) { - require( - regs.combinations(2).forall(x => x.head.addr != x.last.addr), - "address overlap is not allowed between two register elements" - ) - } - - val regsConfig = regs - - val io = IO(new Bundle { - val access = Slave(new AcornIO(dataWidth = dataWidth, addrWidth = addrWidth)) - val fields = new RegBankFieldIO(regs) - }) - - val wrRespPending: Bool = RegInit(false.B) - val rdRespPending: Bool = RegInit(false.B) - - io.access.wr.cmd.ready := io.access.wr.rsp.fire || !wrRespPending - io.access.rd.cmd.ready := io.access.rd.rsp.fire || !rdRespPending - io.access.wr.rsp.valid := wrRespPending - io.access.rd.rsp.valid := rdRespPending - - val elemWrAddrHits = regs.map(io.access.wr.cmd.bits.addr === _.addr.U) - val elemRdAddrHits = regs.map(io.access.rd.cmd.bits.addr === _.addr.U) - val wrAddrNoMatch = !elemWrAddrHits.reduce(_ || _) - val rdAddrNoMatch = !elemRdAddrHits.reduce(_ || _) - - val wrRespError = RegInit(false.B) - val rdRespError = RegInit(false.B) - - when(io.access.wr.cmd.fire) { - wrRespPending := true.B - wrRespError := wrAddrNoMatch - }.elsewhen(io.access.wr.rsp.fire) { - wrRespPending := false.B - wrRespError := false.B - } - - when(io.access.rd.cmd.fire) { - rdRespPending := true.B - rdRespError := rdAddrNoMatch - }.elsewhen(io.access.rd.rsp.fire) { - rdRespPending := false.B - rdRespError := false.B - } - - val elemRdDatas = Wire(Vec(regs.length, UInt(dataWidth.W))) - val rdDataNext = MuxLookup(io.access.rd.cmd.bits.addr, 0.U)(regs.map(_.addr.U) zip elemRdDatas) - - val rdRespData = RegInit(0.U(io.access.dataWidth.W)) - when(io.access.rd.cmd.fire) { - rdRespData := rdDataNext - } - - io.access.wr.rsp.bits.error := wrRespError - io.access.rd.rsp.bits.error := rdRespError - io.access.rd.rsp.bits.data := rdRespData - - for (idxElem <- regs.indices) { - val elemConfig = regs(idxElem) - val elemWrEnable = io.access.wr.cmd.fire && elemWrAddrHits(idxElem) - val elemRdEnable = io.access.rd.cmd.fire && elemRdAddrHits(idxElem) - val elemWrBitMask = { - val mask: UInt = io.access.wr.cmd.bits.strobe - Cat(mask.asBools.reverse.flatMap(b => Seq.fill(8)(b))) - } - - val elemFields = elemConfig.fields - .sortBy(_.baseOffset) - .map(fieldConfig => { - val uField = Module(new RegField(fieldConfig)) - uField.io.frontdoor.wrEnable := elemWrEnable - uField.io.frontdoor.rdEnable := elemRdEnable - uField.io.frontdoor.wrData := io.access.wr.cmd.bits.data(fieldConfig.endOffset, fieldConfig.baseOffset) - uField.io.frontdoor.wrBitMask := elemWrBitMask(fieldConfig.endOffset, fieldConfig.baseOffset) - - io.fields(s"${elemConfig.name}_${fieldConfig.name}") <> uField.io.backdoor - - uField - }) - - elemRdDatas(idxElem) := { - var currOffset = 0 - val dataPieces = for (idx <- elemFields.indices) yield { - val fieldConfig = elemFields(idx).config - val fieldData = elemFields(idx).io.frontdoor.rdData.asUInt - val zeros = 0.U((fieldConfig.baseOffset - currOffset).W) - currOffset = fieldConfig.endOffset + 1 - fieldData ## zeros - } - Cat(dataPieces.reverse) - } - } -} diff --git a/chipmunk/src/regbank/RegField.scala b/chipmunk/src/regbank/RegField.scala deleted file mode 100644 index 3e3ce4d..0000000 --- a/chipmunk/src/regbank/RegField.scala +++ /dev/null @@ -1,136 +0,0 @@ -package chipmunk -package regbank - -import stream.Flow - -import chisel3._ -import chisel3.experimental.requireIsHardware -import chisel3.util._ - -/** Collision priority of a [[RegField]]. It decides which value should be updated to the register field when multiple - * update requests happen in a single cycle. - * - * There are three update sources: write, read, and backdoor. - * - The "write" update source is the write request from the memory-mapped interface. It does not exist when the - * write request has no effects on the field value (i.e., the access type is one of ReadOnly, ReadClear, and - * ReadSet.) - * - The "read" update source is the read request from the memory-mapped interface. It does not exist when the read - * access type has no effects on the field value (i.e., the access type is one of ReadOnly, ReadWrite, WriteOnly, - * WriteSet, WriteOnlySet, WriteZeroSet, WriteClear, WriteOnlyClear, WriteOneClear, WriteOneSet, WriteZeroClear, - * WriteOneToggle, and WriteZeroToggle). - * - The "backdoor" update source is the backdoor update request from the backdoor interface. It exists only when the - * field configuration allows backdoor update (i.e., `backdoorUpdate` is true). - * - * The collision priority is decided by the collision mode. There are six collision modes: - * - [[WriteReadBackdoor]]: write > read > backdoor - * - [[WriteBackdoorRead]]: write > backdoor > read - * - [[ReadBackdoorWrite]]: read > backdoor > write - * - [[ReadWriteBackdoor]]: read > write > backdoor - * - [[BackdoorWriteRead]]: backdoor > write > read - * - [[BackdoorReadWrite]]: backdoor > read > write - */ -object RegFieldCollisionMode extends Enumeration { - val WriteReadBackdoor, WriteBackdoorRead, ReadBackdoorWrite, ReadWriteBackdoor, BackdoorWriteRead, BackdoorReadWrite = - Value -} - -/** Configuration of a register field used in [[RegElementConfig]]. - * - * @param name - * Name of the register field. It should be unique in a [[RegElementConfig]]. Only alphanumeric and underscore are - * allowed. - * @param baseOffset - * Base offset of the register field. The least significant bit of the register field is at this offset. - * @param bitCount - * Bit width of the register field. - * @param initValue - * Initial value of the register field. Default is 0. - * @param accessType - * Access type of the register field. Default is [[RegFieldAccessType.ReadWrite]]. - * @param collisionMode - * Collision priority of the register field. Default is [[RegFieldCollisionMode.BackdoorWriteRead]]. - * @param backdoorUpdate - * Whether the register field can be updated through backdoor. Default is false. - */ -case class RegFieldConfig( - name: String, - baseOffset: Int, - bitCount: Int, - initValue: UInt = 0.U, - accessType: RegFieldAccessType = RegFieldAccessType.ReadWrite, - collisionMode: RegFieldCollisionMode.Value = RegFieldCollisionMode.BackdoorWriteRead, - backdoorUpdate: Boolean = false -) { - require(name matches "[\\w]+", s"Register field name `$name` is invalid") - require(bitCount > 0, s"Register field should have at least 1 bit, but got $bitCount") - require(baseOffset >= 0, s"Base offset of field should be >= 0, but got $baseOffset") - - requireIsHardware(initValue, "RegFieldConfig.initValue") - - val endOffset = baseOffset + bitCount - 1 -} - -private[regbank] class RegFieldBackdoorIO(fieldConfig: RegFieldConfig) extends Bundle with IsMasterSlave { - val value = Output(UInt(fieldConfig.bitCount.W)) - - val isBeingWritten = Output(Bool()) - val isBeingRead = Output(Bool()) - - val backdoorUpdate = - if (fieldConfig.backdoorUpdate) - Some(Slave(Flow(UInt(fieldConfig.bitCount.W)))) - else None - - def isMaster = true -} - -private[regbank] class RegField(val config: RegFieldConfig) extends Module { - val io = IO(new Bundle { - val frontdoor = new Bundle { - val wrEnable = Input(Bool()) - val rdEnable = Input(Bool()) - val wrData = Input(UInt(config.bitCount.W)) - val wrBitMask = Input(UInt(config.bitCount.W)) - val rdData = Output(UInt(config.bitCount.W)) - } - val backdoor = Master(new RegFieldBackdoorIO(config)) - }) - val r = RegInit(UInt(config.bitCount.W), config.initValue) - - val wrEnableValid: Bool = io.frontdoor.wrEnable && io.frontdoor.wrBitMask =/= 0.U - - val writeUpdateDataOption: Option[UInt] = - config.accessType.writeUpdateData(r, io.frontdoor.wrData, wrEnableValid) - val readUpdateDataOption: Option[UInt] = - config.accessType.readUpdateData(r, io.frontdoor.rdEnable) - - val dataNextChoices: Map[String, Option[(Bool, UInt)]] = Map( - "write" -> writeUpdateDataOption.map { data => - io.frontdoor.wrEnable -> { - (data & io.frontdoor.wrBitMask) | (r & (~io.frontdoor.wrBitMask).asUInt) - } - }, - "read" -> readUpdateDataOption.map { data => io.frontdoor.rdEnable -> data }, - "backdoor" -> io.backdoor.backdoorUpdate.map { flow => flow.fire -> flow.bits }, - "default" -> Some(true.B -> r) - ) - - val dataNextChoicesPriority = config.collisionMode match { - case RegFieldCollisionMode.WriteReadBackdoor => Seq("write", "read", "backdoor", "default") - case RegFieldCollisionMode.WriteBackdoorRead => Seq("write", "backdoor", "read", "default") - case RegFieldCollisionMode.ReadBackdoorWrite => Seq("read", "backdoor", "write", "default") - case RegFieldCollisionMode.ReadWriteBackdoor => Seq("read", "write", "backdoor", "default") - case RegFieldCollisionMode.BackdoorWriteRead => Seq("backdoor", "write", "read", "default") - case RegFieldCollisionMode.BackdoorReadWrite => Seq("backdoor", "read", "write", "default") - case _ => throw new IllegalArgumentException("Invalid collision mode") - } - - r := PriorityMux(dataNextChoicesPriority.flatMap(dataNextChoices(_).toList)) - - io.backdoor.value := r - - io.backdoor.isBeingRead := io.frontdoor.rdEnable && !config.accessType.cannotRead.B - io.backdoor.isBeingWritten := wrEnableValid - - io.frontdoor.rdData := (if (config.accessType.cannotRead) 0.U else r.asUInt) -} diff --git a/chipmunk/src/regbank/RegFieldAccessType.scala b/chipmunk/src/regbank/RegFieldAccessType.scala deleted file mode 100644 index 63e1fb0..0000000 --- a/chipmunk/src/regbank/RegFieldAccessType.scala +++ /dev/null @@ -1,309 +0,0 @@ -package chipmunk -package regbank - -import chisel3._ - -/** Access types of a [[RegField]]. By extending this class, you can define your own access type. */ -abstract class RegFieldAccessType { - - /** Whether the register field can be read by frontdoor. - * - * For an unreadable (write-only) register field, it should be set to true, and the responded data is always 0 during - * reading. Although it is unreadable, it can still be accessed by backdoor. - */ - val cannotRead: Boolean = false - - /** The updated value of the register field when writing. - * - * @param curr - * The current value of the register field. - * @param wrData - * The written value from the frontdoor bus access. - * @param wrEnable - * Whether a valid frontdoor write request is received (i.e., current register element is being written and at - * least one corresponding mask bits are non-zero). - * @return - * The data value to be updated to the register field. If it is not None, it will be updated to the field registers - * when a frontdoor writing request is received (i.e., `wrEnable === true.B`). Leave it as None if the field is - * kept stationary during being written. - */ - def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = None - - /** The updated value of the register field when reading. - * - * @note - * This method is to update the register field when a frontdoor read request is received. To implement an - * unreadable (write-only) field, you should set [[cannotRead]] to true. - * - * @param curr - * The current value of the register field. - * @param rdEnable - * Whether a valid frontdoor write request is received (i.e., current register element is being read). - * @return - * The data value to be updated to the register field. If it is not None, it will be updated to the field registers - * when a frontdoor read request is received (i.e., `rdEnable === true.B`). Leave it as None if the field is kept - * stationary during being read. - */ - def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = None -} - -/** The 25 register modes pre-defined by UVM. */ -object RegFieldAccessType { - - /** RO (Read Only). - * - * Write: no effect. Read: No effect. - */ - object ReadOnly extends RegFieldAccessType - - /** RW (Read, write). - * - * Write: changed to written value. Read: no effect. - */ - object ReadWrite extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(wrData) - } - - /** RC (Read Clears All). - * - * Write: no effect. Read: sets all bits to 0’s. - */ - object ReadClear extends RegFieldAccessType { - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(0.U) - } - - /** RS (Read Sets All). - * - * Write: no effect. Read: sets all bits to 1’s. - */ - object ReadSet extends RegFieldAccessType { - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(curr.filledOnes) - } - - /** WRC (Write, Read Clears All). - * - * Write: changed to written value. Read: sets all bits to 0’s. - */ - object WriteReadClear extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(wrData) - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(0.U) - } - - /** WRS (Write, Read Sets All). - * - * Write: changed to written value. Read: sets all bits to 1’s. - */ - object WriteReadSet extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(wrData) - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(curr.filledOnes) - } - - /** WC (Write Clears All). - * - * Write: sets all bits to 0’s. Read: no effect. - */ - object WriteClear extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(0.U) - } - - /** WS (Write Sets All). - * - * Write: sets all bits to 1’s. Read: no effect. - */ - object WriteSet extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr.filledOnes) - } - - /** WSRC (Write Sets All, Read Clears All). - * - * Write: sets bits to 1’s. Read: sets bits to 0’s. - */ - object WriteSetReadClear extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr.filledOnes) - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(0.U) - } - - /** WCRS (Write Clears All, Read Sets All). - * - * Write: sets bits to 0’s. Read: sets bits to 1’s. - */ - object WriteClearReadSet extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(0.U) - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(curr.filledOnes) - } - - /** W1C (Write One to Clear). - * - * Write: clear the field bits only if the corresponding written value bits is 1. Read: no effect. - */ - object WriteOneClear extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr & (~wrData).asUInt) - } - - /** W1S (Write One to Set). - * - * Write: set the field bits only if the corresponding written value bits is 1. Read: no effect. - */ - object WriteOneSet extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr | wrData) - } - - /** W1T (Write One to Toggle). - * - * Write: toggle the field bits only if the corresponding written value bits is 1. Read: no effect. - */ - object WriteOneToggle extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr ^ wrData) - } - - /** W0C (Write Zero to Clear). - * - * Write: clear the field bits only if the corresponding written value bits is 0. Read: no effect. - */ - object WriteZeroClear extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr & wrData) - } - - /** W0S (Write Zero to Set). - * - * Write: toggle the field bits only if the corresponding written value bits is 0. Read: no effect. - */ - object WriteZeroSet extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr | (~wrData).asUInt) - } - - /** W0T (Write Zero to Toggle). - * - * Write: toggle the field bits only if the corresponding written value bits is 0. Read: no effect. - */ - object WriteZeroToggle extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr ^ (~wrData).asUInt) - } - - /** W1SRC (Write One to Set, Read Clears All). - * - * Write: set the field bits only if the corresponding written value bits is 1. Read: sets all bits to 0’s. - */ - object WriteOneSetReadClear extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr | wrData) - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(0.U) - } - - /** W1CRS (Write One to Clear, Read Sets All). - * - * Write: clear the field bits only if the corresponding written value bits is 1. Read: sets all bits to 1’s. - */ - object WriteOneClearReadSet extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr & (~wrData).asUInt) - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(curr.filledOnes) - } - - /** W0SRC (Write Zero to Set, Read Clears All). - * - * Write: set the field bits only if the corresponding written value bits is 0. Read: sets all bits to 0’s. - */ - object WriteZeroSetReadClear extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr | (~wrData).asUInt) - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(0.U) - } - - /** W0CRS (Write Zero to Clear, Read Sets All). - * - * Write: clear the field bits only if the corresponding written value bits is 0. Read: sets all bits to 1’s. - */ - object WriteZeroClearReadSet extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(curr & wrData) - override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = - Some(curr.filledOnes) - } - - /** WO (Write Only). - * - * Write: changed to written value. Read: no effect, and read-back result is 0. - */ - object WriteOnly extends RegFieldAccessType { - override val cannotRead: Boolean = true - - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(wrData) - } - - /** WOC (Write Only Clears All). - * - * Write: sets all bits to 0’s. Read: no effect, and read-back result is 0. - */ - object WriteOnlyClear extends RegFieldAccessType { - override val cannotRead: Boolean = true - - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = - Some(0.U) - } - - /** WOS (Write Only Sets All). - * - * Write: sets all bits to 1’s. Read: no effect, and read-back result is 0. - */ - object WriteOnlySet extends RegFieldAccessType { - override val cannotRead: Boolean = true - - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = { - Some(curr.filledOnes) - } - } - - /** W1 (Write Once). - * - * Write: changed to written value only when the 1st write after reset. Read: no effect. - */ - object WriteOnce extends RegFieldAccessType { - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = { - val haveWritten = RegInit(0.U(curr.getWidth.W)) - when(wrEnable) { - haveWritten := true.B - } - Some(((~haveWritten).asUInt & wrData) | (haveWritten & curr)) - } - } - - /** WO1 (Write Only, Once). - * - * Write: changed to written value only when the 1st write after reset. Read: no effect, and read-back result is 0. - */ - object WriteOnlyOnce extends RegFieldAccessType { - override val cannotRead: Boolean = true - - override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = { - val haveWritten = RegInit(0.U(curr.getWidth.W)) - when(wrEnable) { - haveWritten := true.B - } - Some(((~haveWritten).asUInt & wrData) | (haveWritten & curr)) - } - } -} diff --git a/chipmunk/src/stream/Flow.scala b/chipmunk/src/stream/Flow.scala deleted file mode 100644 index 6da4db6..0000000 --- a/chipmunk/src/stream/Flow.scala +++ /dev/null @@ -1,202 +0,0 @@ -package chipmunk -package stream - -import chisel3._ -import chisel3.experimental.{requireIsChiselType, requireIsHardware} -import chisel3.util._ - -class FlowIO[T <: Data](gen: T) extends Valid[T](gen) with IsMasterSlave { - override def isMaster = true - - /** Drive this FlowIO from that. */ - def <<(that: FlowIO[T]): FlowIO[T] = connectFrom(that) - - /** Drive that FlowIO from this. */ - def >>(that: FlowIO[T]): FlowIO[T] = { - that << this - that - } - - /** Drive this FlowIO from that, where the forward valid/bits paths are cut by registers (see [[pipeForward]]). */ - def <-<(that: FlowIO[T]): FlowIO[T] = { - this << that.pipeForward() - that - } - - /** Drive that FlowIO from this, where the forward valid/bits paths are cut by registers (see [[pipeForward]]). */ - def >->(that: FlowIO[T]): FlowIO[T] = { - that <-< this - } - - /** Drive the handshake signal (valid) of this FlowIO from that. */ - def handshakeFrom[T2 <: Data](that: FlowIO[T2]): Unit = { - this.valid := that.valid - } - - /** Drive the all signals of this FlowIO from that. */ - def connectFrom(that: FlowIO[T]): FlowIO[T] = { - handshakeFrom(that) - this.bits := that.bits - that - } - - /** Map the payload (bits) to something else. - * - * @param f - * The payload mapping function. - * @return - * The new FlowIO whose payload is converted from this. - * @example - * {{{ - * val newFlow = oldFlow.payloadMap(x => { - * val y = Wire(Bool()) - * y := (x + 1.U).asBool - * y - * }) - * }}} - */ - def payloadMap[T2 <: Data](f: T => T2): FlowIO[T2] = { - val payload = f(bits) - val ret = Wire(Flow(chiselTypeOf(payload))) - ret.handshakeFrom(this) - ret.bits := payload - ret - } - - /** Replace this FlowIO's payload (bits) with something else. - * - * If you want to cast the payload to another type, consider using [[payloadCast]]. - * - * @param p - * The new payload. - * @example - * {{{ - * val oldFlow = Flow(UInt(8.W)) - * val newFlow = oldFlow.payloadReplace(12.S(8.W)) - * }}} - */ - def payloadReplace[T2 <: Data](p: T2): FlowIO[T2] = { - requireIsHardware(p) - payloadMap(_ => p) - } - - /** Cast this FlowIO's payload (bits) to another type. - * - * If you want to replace the payload with other hardware signals, use [[payloadReplace]]. - * - * @param gen - * Target chisel type. - * @param checkWidth - * Whether to check the bit width of the payload is the same as the target type. If false, the payload may be - * padded or truncated. If true, throw an exception when the bit width is not the same. - * @return - * The new FlowIO whose payload is converted from this. - * @example - * {{{ - * val oldFlow = Flow(UInt(8.W)) - * val newFlow = oldStream.payloadCast(SInt(8.W)) - * }}} - */ - def payloadCast[T2 <: Data](gen: T2, checkWidth: Boolean = false): FlowIO[T2] = { - requireIsChiselType(gen) - if (checkWidth) { - require(gen.getWidth == bits.getWidth, s"Payload width mismatch: ${gen.getWidth} != ${bits.getWidth}") - } - payloadMap(_.asTypeOf(gen)) - } - - /** Do nothing, but it is nice to separate signals for combinatorial transformations. */ - def pipePassThrough(): FlowIO[T] = this - - /** Cut the valid & payload (bits) path by registers for better timing. - * - * It does not affect throughput, but introduces extra latency. Its area overhead is mainly (N + 1) registers and a - * little combinatorial logic. - * - * @return - * FlowIO with registers inserted. - */ - def pipeForward(): FlowIO[T] = { - val ret = Wire(Flow(gen)) - ret.valid := RegNext(valid, false.B) - ret.bits := RegEnable(bits, valid) - ret - } - - /** Alias of [[pipeForward]]. */ - def stage(n: Int = 1): FlowIO[T] = { - require(n >= 0, "The stage number must be greater than or equal to 0.") - var ret = this - for (_ <- 0 until n) { - ret = ret.pipeForward() - } - ret - } - - /** Throw transactions when `cond` is False. */ - def takeWhen(cond: Bool): FlowIO[T] = { - val ret = Wire(Flow(gen)) - ret.valid := this.fire && cond - ret.bits := bits - ret - } - - /** Throw transactions when `cond` is True. */ - def throwWhen(cond: Bool): FlowIO[T] = { - this takeWhen (!cond) - } - - /** Create a StreamIO whose valid & payload (bits) signals are driven from this FlowIO directly. - * - * This is a very dangerous conversion! Because FlowIO cannot be back-pressured by the ready signal, the result - * StreamIO may drop data when its ready signal is low. The ready signal should be always high, or it needs to be - * handled very carefully. - * - * To avoid overflow, consider using [[StreamIO.queue]] together. - * - * @return - * The result StreamIO. - */ - def asStream: StreamIO[T] = { - val ret = Wire(stream.Stream(gen)) - ret.valid := valid - ret.bits := bits - ret - } - -// def toStream(queueSize: Int = 0, overflowThreshold: Int = 0): (StreamIO[T], Bool) = { -// val ret = Stream(gen) -// val overflow = WireInit(false.B) -// ret.valid := valid -// ret.bits := bits -// overflow := ret.isPending -// (ret, overflow) -// } -} - -/** FlowIO factory. */ -object Flow { - def apply[T <: Data](gen: ValidIO[T]): FlowIO[T] = { - val ret = Wire(Flow(chiselTypeOf(gen.bits))) - ret - } - - def apply[T <: Data](gen: T) = new FlowIO(gen) - - /** Returns a [[FlowIO]] interface with no payload. */ - def apply(): FlowIO[Data] = new FlowIO(new EmptyBundle) - - /** Returns a [[FlowIO]] interface with no payload. */ - def empty: FlowIO[EmptyBundle] = new FlowIO(new EmptyBundle) -} - -object RegFlow { - - /** Create registers whose type is `FlowIO(gen)`. */ - def apply[T <: Data](gen: T): FlowIO[T] = { - val ret = Wire(Flow(gen)) - ret.valid := RegInit(false.B) - ret.bits := Reg(gen) - ret - } -} diff --git a/chipmunk/src/stream/FlowFork.scala b/chipmunk/src/stream/FlowFork.scala deleted file mode 100644 index 1f386fb..0000000 --- a/chipmunk/src/stream/FlowFork.scala +++ /dev/null @@ -1,113 +0,0 @@ -package chipmunk -package stream - -import chisel3.util._ -import chisel3._ - -/** Fork a FlowIO into multiple FlowIOs. */ -object FlowFork { - - /** Fork a FlowIO into multiple FlowIOs, where their payloads (bits) are unconnected. Payload needs to be connected - * separately. - * - * @param in - * The input FlowIO. - * @param num - * The number of forked downstream FlowIOs. - * @return - * The output downstream FlowIOs driven by the input FlowIO. - * @example - * {{{ - * val in = Flow(UInt(32.W)) - * val outs = FlowFork.withoutPayload(in, 2) - * }}} - */ - def withoutPayload[T <: Data](in: FlowIO[T], num: Int): Vec[FlowIO[EmptyBundle]] = { - require(num >= 2, "FlowFork must have at least 2 output FlowIOs.") - val emptyOuts = Wire(Vec(num, Flow.empty)) - emptyOuts.foreach(_.handshakeFrom(in)) - emptyOuts - } - - /** Fork a FlowIO into multiple FlowIOs, where their payloads (bits) are the same as the input FlowIO. - * - * @param in - * The input FlowIO. - * @param num - * The number of forked downstream FlowIOs. - * @return - * The output downstream FlowIOs driven by the input FlowIO. - * @example - * {{{ - * val in = Flow(UInt(32.W)) - * val outs = FlowFork.duplicate(in, 2) - * }}} - */ - def duplicate[T <: Data](in: FlowIO[T], num: Int): Vec[FlowIO[T]] = { - val emptyOuts = withoutPayload(in, num) - VecInit(emptyOuts.map(_.payloadReplace(in.bits))) - } - - /** Fork a FlowIO of `Vec` into a multiple FlowIOs, where their payloads (bits) are each element in the `Vec` - * respectively. The number of forked downstream FlowIOs is the same as the length of the `Vec`. - * - * @param in - * The input FlowIO. - * @return - * The output downstream FlowIOs driven by the input FlowIO. - * @example - * {{{ - * val in = Flow(Vec(16, UInt(32.W))) - * val outs = FlowFork.vecSplit(in) - * }}} - */ - def vecSplit[T <: Data](in: FlowIO[Vec[T]]): Vec[FlowIO[T]] = { - val emptyOuts = withoutPayload(in, in.bits.length) - VecInit(emptyOuts zip in.bits map { case (o, p) => - o.payloadReplace(p) - }) - } - - /** Fork a FlowIO of `MixedVec` into a multiple FlowIOs, where their payloads (bits) are each element in the - * `MixedVec` respectively. The number of forked downstream FlowIOs is the same as the length of the `MixedVec`. - * - * Because there are some bugs in the implementation of `MixedVecInit`, it returns `IndexedSeq` instead of - * `MixedVec`. - * - * @param in - * The input FlowIO. - * @return - * The output downstream FlowIOs driven by the input FlowIO. - * @example - * {{{ - * val in = Flow(MixedVec(true.B, 12.U(8.W))) - * val outs = FlowFork.vecSplit(in) - * }}} - */ - def vecSplit[T <: Data](in: FlowIO[MixedVec[T]]): IndexedSeq[FlowIO[T]] = { - val emptyOuts = withoutPayload(in, in.bits.length) - emptyOuts zip in.bits map { case (o, p) => - o.payloadReplace(p) - } - } - - /** Fork a FlowIO of `Vec` into a multiple FlowIOs, where their payloads (bits) are each element in the `MixedVec` - * generated from `fn(in.bits)`. The number of forked downstream FlowIOs is the same as the length of the `MixedVec`. - * - * @param in - * The input FlowIO. - * @param fn - * The function to map the payload of the input FlowIO to a MixedVec, whose each element drives the payload of one - * output FlowIO. - * @return - * The output downstream FlowIOs driven by the input FlowIO. - * @example - * {{{ - * val in = Flow(UInt(32.W)) - * val outs = FlowFork.vecMap(in, { x => MixedVecInit(x + 1.U, x + 2.U) }) - * }}} - */ - def vecMap[T <: Data, T2 <: Data](in: FlowIO[T], fn: T => MixedVec[T2]): IndexedSeq[FlowIO[T2]] = { - vecSplit(in.payloadMap(fn)) - } -} diff --git a/chipmunk/src/stream/Stream.scala b/chipmunk/src/stream/Stream.scala deleted file mode 100644 index 0f71669..0000000 --- a/chipmunk/src/stream/Stream.scala +++ /dev/null @@ -1,386 +0,0 @@ -package chipmunk -package stream - -import chisel3._ -import chisel3.experimental.{requireIsChiselType, requireIsHardware} -import chisel3.util._ - -class StreamIO[T <: Data](gen: T) extends DecoupledIO[T](gen) with IsMasterSlave { - def isMaster = true - - /** Drive this StreamIO from that. */ - def <<(that: StreamIO[T]): StreamIO[T] = { - connectFrom(that) - that - } - - /** Drive that StreamIO from this. */ - def >>(that: StreamIO[T]): StreamIO[T] = { - that << this - that - } - - /** Drive this StreamIO from that, where the forward valid/bits paths are cut by registers (see [[pipeForward]]). */ - def <-<(that: StreamIO[T]): StreamIO[T] = { - this << that.pipeForward() - that - } - - /** Drive that StreamIO from this, where the forward valid/bits paths are cut by registers (see [[pipeForward]]). */ - def >->(that: StreamIO[T]): StreamIO[T] = { - that <-< this - that - } - - /** Drive this StreamIO from that, where the backward ready path is cut by registers (see [[pipeBackward]]). */ - def <|<(that: StreamIO[T]): StreamIO[T] = { - this << that.pipeBackward() - that - } - - /** Drive that StreamIO from this, where the backward ready path is cut by registers (see [[pipeBackward]]). */ - def >|>(that: StreamIO[T]): StreamIO[T] = { - that <|< this - that - } - - /** Drive this StreamIO from that, where the valid/ready/bits paths are cut by registers. */ - def <+<(that: StreamIO[T]): StreamIO[T] = { - this << that.pipeAll() - that - } - - /** Drive that StreamIO from this, where the valid/ready/bits paths are cut by registers. */ - def >+>(that: StreamIO[T]): StreamIO[T] = { - that <+< this - that - } - - /** Drive the handshake signals (valid & ready) of this StreamIO from that. */ - def handshakeFrom[T2 <: Data](that: StreamIO[T2]): Unit = { - this.valid := that.valid - that.ready := this.ready - } - - /** Drive the all signals of this StreamIO from that. */ - def connectFrom(that: StreamIO[T]): Unit = { - handshakeFrom(that) - this.bits := that.bits - } - - /** Map the payload (bits) to something else. - * - * @param f - * The payload mapping function. - * @return - * The new StreamIO whose payload is converted from this. - * @example - * {{{ - * val oldStream = Stream(UInt(8.W)) - * val newStream = oldStream.payloadMap(x => { - * val y = Wire(Bool()) - * y := (x + 1.U) >= 3.U - * y - * }) - * }}} - */ - def payloadMap[T2 <: Data](f: T => T2): StreamIO[T2] = { - val payload = f(bits) - val ret = Wire(Stream(chiselTypeOf(payload))) - ret.handshakeFrom(this) - ret.bits := payload - ret - } - - /** Replace this stream's payload (bits) with something else. If you want to cast the payload to another type, use - * [[payloadCast]]. - * - * @param p - * The new payload. - * @example - * {{{ - * val oldStream = Stream(UInt(8.W)) - * val newStream = oldStream.payloadReplace(12.S(8.W)) - * }}} - */ - def payloadReplace[T2 <: Data](p: T2): StreamIO[T2] = { - requireIsHardware(p) - payloadMap(_ => p) - } - - /** Cast this StreamIO's payload (bits) to another type. If you want to replace the payload with other hardware - * signals, use [[payloadReplace]]. - * - * @param gen - * Target chisel type. - * @param checkWidth - * Whether to check the bit width of the payload is the same as the target type. If false, the payload may be - * padded or truncated. If true, throw an exception when the bit width is not the same. - * @return - * The new StreamIO whose payload is converted from this. - * @example - * {{{ - * val oldStream = Stream(UInt(8.W)) - * val newStream = oldStream.payloadCast(SInt(8.W)) - * }}} - */ - def payloadCast[T2 <: Data](gen: T2, checkWidth: Boolean = false): StreamIO[T2] = { - requireIsChiselType(gen) - if (checkWidth) { - require(gen.getWidth == bits.getWidth, s"Payload width mismatch: ${gen.getWidth} != ${bits.getWidth}") - } - payloadMap(_.asTypeOf(gen)) - } - - /** True if data are present but the ready is still low (i.e. valid && !ready). */ - def isPending: Bool = valid && !ready - - /** True if the stream is ready to receive data but no data is present (i.e. !valid && ready). */ - def isStarving: Bool = !valid && ready - - /** Do nothing, but it is nice to separate signals for combinatorial transformations. */ - def pipePassThrough(): StreamIO[T] = this - - /** Cut the valid & payload (bits) path by registers for better timing. - * - * It does not affect throughput, but introduces extra latency. Its area overhead is mainly (N + 1) registers and a - * little combinatorial logic. - * - * @param bubbleCollapse - * Collapse bubbles in the forward path with tiny combinatorial logic. - * @return - * StreamIO with registers inserted. - */ - def pipeForward(bubbleCollapse: Boolean = true): StreamIO[T] = { - val ret = Wire(Stream(gen)) - val rValid = RegEnable(valid, false.B, ret.ready) - val rBits = RegEnable(bits, this.fire) - - if (bubbleCollapse) { - ready := ret.ready || !ret.valid - } else { - ready := ret.ready - } - - ret.valid := rValid - ret.bits := rBits - ret - } - - /** Cut the ready path by registers for better timing. - * - * It does not affect throughput or latency. Its area overhead is mainly (N + 1) registers and N 2-to-1 multiplexers, - * where N is the bit width of payload. - * - * @return - * StreamIO with registers inserted. - */ - def pipeBackward(): StreamIO[T] = { - val ret = Wire(Stream(gen)) - val rValid = RegInit(false.B) - val rBits = RegEnable(bits, this.fire && !ret.ready) - - when(ret.ready) { - rValid := false.B - }.elsewhen(valid) { - rValid := true.B - } - - ready := !rValid - - // If rBits/rValid is nonempty, the data in them are taken in first. - ret.valid := valid || rValid - ret.bits := Mux(rValid, rBits, bits) - ret - } - - /** Cut the valid & ready & payload (bits) path by registers for better timing. - * - * Alias of this.pipeForward().pipeBackward(). It does not affect throughput, but introduces extra latency. Its area - * overhead is mainly (2N + 2) registers and a little combinatorial logic, where N is the bit width of payload. - * - * @return - * StreamIO with registers inserted. - */ - def pipeAll(): StreamIO[T] = pipeForward().pipeBackward() - - /** Cut the ready, valid & payload (bits) path by registers for better timing. - * - * It does a handshake every other cycle at most, losing half of the bandwidth. Its area overhead is mainly (N + 2) - * registers, where N is the bit width of payload. - * - * If the bandwidth loss is unacceptable, consider using [[pipeAll]] instead. - * - * @return - * StreamIO with registers inserted. - */ - def pipeSimple(): StreamIO[T] = { - val ret = Wire(Stream(gen)) - val rValid = RegInit(false.B) - val rBits = RegEnable(bits, this.fire) - - when(ret.fire) { - rValid := false.B - }.elsewhen(valid) { - rValid := true.B - } - - ready := !rValid - - ret.valid := rValid - ret.bits := rBits - ret - } - - /** Cut the valid path by registers for better timing. - * - * Its area overhead is mainly 1 register. - * - * @return - * StreamIO with registers inserted. - */ - def pipeValid(): StreamIO[T] = { - val ret = Wire(Stream(gen)) - val rValid = RegInit(false.B) - - when(ret.fire) { - rValid := false.B - }.elsewhen(valid) { - rValid := true.B - } - - ready := ret.fire - - ret.valid := rValid - ret.bits := bits - ret - } - - /** Alias of [[pipeForward]]. */ - def stage(n: Int = 1): StreamIO[T] = { - require(n >= 0, "The stage number must be greater than or equal to 0.") - var ret = this - for (_ <- 0 until n) { - ret = ret.pipeForward() - } - ret - } - - /** Block this stream when `cond` is False. */ - def continueWhen(cond: => Bool): StreamIO[T] = { - val ret = Wire(Stream(gen)) - ret.valid := this.valid && cond - this.ready := ret.ready && cond - ret.bits := this.bits - ret - } - - /** Block this stream when `cond` is True. */ - def haltWhen(cond: => Bool): StreamIO[T] = continueWhen(!cond) - - /** Throw transactions when `cond` is True. */ - def throwWhen(cond: => Bool): StreamIO[T] = { - val ret = Stream(gen) - ret << this - when(cond) { - ret.valid := false.B - this.ready := true.B - } - ret - } - - /** Throw transactions when `cond` is False. */ - def takeWhen(cond: => Bool): StreamIO[T] = throwWhen(!cond) - - /** Wrap valid & payload (bits) signals of this StreamIO as a FlowIO, and the ready signal is ignored (unconnected and - * unused). - * - * This is a dangerous conversion! FlowIO's firing only relies on the valid signal of this StreamIO, regardless of - * the ready signal is high or low. If you want the transaction of FlowIO depends on `valid && ready` of this - * StreamIO, consider using [[toFlow]]. - * - * @return - * The result FlowIO. - */ - def asFlow: FlowIO[T] = { - val ret = Wire(Flow(gen)) - ret.valid := valid - ret.bits := bits - ret - } - - /** Convert this StreamIO to FlowIO. - * - * The FlowIO fires only when this StreamIO fires. That is to say, the valid signal of the result FlowIO is driven - * from `valid && ready` of this StreamIO. - * - * @param readyFreeRun - * Whether to set the ready signal of this StreamIO always high by default. If not, the ready signal is kept - * unconnected. - * @return - * The result FlowIO. - */ - def toFlow(readyFreeRun: Boolean = false): FlowIO[T] = { - val ret = Wire(Flow(gen)) - ret.valid := this.fire - ret.bits := bits - if (readyFreeRun) { - ready := true.B - } - ret - } - - /** Push this StreamIO to [[Queue]] and return its pop StreamIO. - * - * @param queueSize - * The Queue depth. - * @param queueFlow - * Whether the inputs can be consumed on the same cycle (the inputs "flow" through the queue immediately). The - * `valid` signals are coupled. - * @param queuePipe - * Whether a single entry queue can run at full throughput (like a pipeline). The `ready` signals are - * combinatorial-ly coupled. - * @return - * The pop StreamIO of the queue. - */ - def queue(queueSize: Int, queueFlow: Boolean, queuePipe: Boolean): StreamIO[T] = { - val q: DecoupledIO[T] = Queue(this, entries = queueSize, pipe = queueFlow, flow = queuePipe, useSyncReadMem = true) - q.toStream - } - - /** Delay the StreamIO by a fixed number of cycles. - * - * @param cycles - * The number of cycles to delay. - * @return - * The result StreamIO. - */ - def delayFixed(cycles: Int): StreamIO[T] = { - StreamDelay.fixed(this, cycles) - } - - /** Delay the StreamIO by a random number of cycles. - * - * @param maxCycles - * The maximum number of cycles to delay. - * @param minCycles - * The minimum number of cycles to delay. - * @return - * The result StreamIO. - */ - def delayRandom(maxCycles: Int, minCycles: Int = 0): StreamIO[T] = { - StreamDelay.random(this, maxCycles, minCycles) - } -} - -/** StreamIO factory. */ -object Stream { - - def apply[T <: Data](gen: T) = new StreamIO(gen) - - /** Returns a [[StreamIO]] interface with no payload. */ - def apply(): StreamIO[EmptyBundle] = new StreamIO(new EmptyBundle) - - /** Returns a [[StreamIO]] interface with no payload. */ - def empty: StreamIO[EmptyBundle] = new StreamIO(new EmptyBundle) -} diff --git a/chipmunk/src/stream/StreamArbiter.scala b/chipmunk/src/stream/StreamArbiter.scala deleted file mode 100644 index f821359..0000000 --- a/chipmunk/src/stream/StreamArbiter.scala +++ /dev/null @@ -1,43 +0,0 @@ -package chipmunk -package stream - -import chisel3._ -import chisel3.util._ - -/** Wrapper of chisel's builtin RRArbiter, but its grant register has an initialized value. */ -private[chipmunk] class StreamArbiterRR[T <: Data](gen: T, n: Int) extends RRArbiter(gen, n) { - override lazy val lastGrant: UInt = RegEnable(io.chosen, 0.U, io.out.fire) -} - -object StreamArbiter { - def roundRobin[T <: Data](ins: Seq[StreamIO[T]]): StreamIO[T] = { - val uArb = Module(new StreamArbiterRR(chiselTypeOf(ins.head.bits), ins.length)) - (uArb.io.in zip ins).foreach { x => - x._1.valid := x._2.valid - x._1.bits := x._2.bits - x._2.ready := x._1.ready - } - uArb.io.out.toStream - } - - def lowerFirst[T <: Data](ins: Seq[StreamIO[T]]): StreamIO[T] = { - val uArb = Module(new Arbiter(chiselTypeOf(ins.head.bits), ins.length)) - (uArb.io.in zip ins).foreach { x => - x._1.valid := x._2.valid - x._1.bits := x._2.bits - x._2.ready := x._1.ready - } - uArb.io.out.toStream - } -} - -/** Combine a stream and a flow to a new flow. If both input sources fire, the flow will be preferred. */ -object StreamFlowArbiter { - def apply[T <: Data](inStream: StreamIO[T], inFlow: FlowIO[T]): FlowIO[T] = { - val output = Wire(Flow(inFlow)) - output.valid := inFlow.valid || inStream.valid - inStream.ready := !inFlow.valid - output.bits := Mux(inFlow.valid, inFlow.bits, inStream.bits) - output - } -} diff --git a/chipmunk/src/stream/StreamDelay.scala b/chipmunk/src/stream/StreamDelay.scala deleted file mode 100644 index bd5547c..0000000 --- a/chipmunk/src/stream/StreamDelay.scala +++ /dev/null @@ -1,103 +0,0 @@ -package chipmunk -package stream -import chisel3._ -import chisel3.util._ -import chisel3.util.random.LFSR - -object StreamDelay { - def fixed[T <: Data](in: StreamIO[T], cycles: Int): StreamIO[T] = { - require(cycles >= 0, "The delay cycles must be greater than or equal to 0.") - if (cycles == 0) { - in - } else { - val uStreamDelay = Module(new StreamDelay(chiselTypeOf(in.bits), delayWidth = log2Ceil(cycles + 1))) - uStreamDelay.io.in << in - uStreamDelay.io.targetDelay := cycles.U - uStreamDelay.io.out - } - } - - def random[T <: Data](in: StreamIO[T], maxCycles: Int, minCycles: Int = 0): StreamIO[T] = { - require(minCycles >= 0, "The minimum delay must be greater than or equal to 0.") - require(maxCycles >= minCycles, "The maximum delay must be greater than or equal to the minimum delay.") - if (maxCycles == minCycles) { - fixed(in, maxCycles) - } else { - val uStreamDelay = Module(new StreamDelay(chiselTypeOf(in.bits), log2Ceil(maxCycles + 1))) - - val randomDelayRange: Int = maxCycles - minCycles - 1 - val randomDelayWidth: Int = log2Ceil(randomDelayRange) - val lfsrValue: UInt = LFSR(randomDelayWidth, increment = uStreamDelay.io.targetDelayUpdate) - val randomDelay: UInt = Mux(lfsrValue > randomDelayRange.U, lfsrValue - randomDelayRange.U, lfsrValue) - val targetDelay: UInt = randomDelay + minCycles.U - - uStreamDelay.io.in << in - uStreamDelay.io.targetDelay := targetDelay - uStreamDelay.io.out - } - } -} - -/** Delay a stream by a fixed/random number of cycles. - * - * @param gen - * The type of the data in the stream. - * @param delayWidth - * The width of the delay counter. It should be `log2Ceil(maxDelay + 1)`. - */ -class StreamDelay[T <: Data](gen: T, delayWidth: Int) extends Module { - require(delayWidth >= 1, "The delayWidth must be greater than or equal to 1.") - - val io = IO(new Bundle { - val in = Slave(Stream(gen)) - val out = Master(Stream(gen)) - - val targetDelay = Input(UInt(delayWidth.W)) - val targetDelayUpdate = Output(Bool()) - }) - io.out.bits := io.in.bits - - val counterOverflow: Bool = Wire(Bool()) - - val fsm = new StateMachine { - val sIdle = new State with EntryPoint - val sCount = new State - val sPend = new State - sIdle.whenIsActive { - when(io.in.valid) { - goto(sCount) - when(io.targetDelay === 0.U) { - when(io.out.ready) { - goto(sIdle) - }.otherwise { - goto(sPend) - } - }.elsewhen(io.targetDelay === 1.U) { - goto(sPend) - } - } - } - sCount.whenIsActive { - when(counterOverflow) { - goto(sPend) - } - } - sPend.whenIsActive { - when(io.out.ready) { - goto(sIdle) - } - } - } - import fsm.{sCount, sIdle, sPend} - - val counterInc: Bool = fsm.isActive(sCount) - val counterReset: Bool = fsm.isActive(sIdle) || fsm.isEntering(sPend) - val counter: (UInt, Bool) = Counter(1 until (1 << delayWidth), enable = counterInc, reset = counterReset) - counterOverflow := counter._1 === io.targetDelay - 1.U - - io.targetDelayUpdate := fsm.isActive(sCount) && counterOverflow - - io.out.valid := fsm.isActive(sPend) || (fsm.isActive(sIdle) && io.targetDelay === 0.U && io.in.valid) - io.in.ready := io.out.fire - -} diff --git a/chipmunk/src/stream/StreamDemux.scala b/chipmunk/src/stream/StreamDemux.scala deleted file mode 100644 index 99cf825..0000000 --- a/chipmunk/src/stream/StreamDemux.scala +++ /dev/null @@ -1,47 +0,0 @@ -package chipmunk -package stream - -import chisel3._ -import chisel3.util._ - -/** Demultiplex one stream into multiple output streams, always selecting only one at a time. */ -object StreamDemux { - def apply[T <: Data](in: StreamIO[T], select: UInt, num: Int): Vec[StreamIO[T]] = { - val c = Module(new StreamDemux(chiselTypeOf(in.bits), num)) - c.io.in << in - c.io.select := select - c.io.outs - } - - def apply[T <: Data](in: StreamIO[T], select: StreamIO[UInt], num: Int): Vec[StreamIO[T]] = { - val c = Module(new StreamDemux(chiselTypeOf(in.bits), num)) - c.io.in << in - select >> c.io.createSelectStream() - c.io.outs - } -} - -class StreamDemux[T <: Data](dataType: T, num: Int) extends Module { - val io = IO(new Bundle { - val select = Input(UInt(log2Ceil(num).W)) - val in = Slave(Stream(dataType)) - val outs = Vec(num, Master(Stream(dataType))) - - def createSelectStream(): StreamIO[UInt] = { - val stream = Wire(Stream(UInt(log2Ceil(num).W))) - val regFlow = stream.haltWhen(in.isPending).toFlow(readyFreeRun = true) - select := RegEnable(regFlow.bits, 0.U, regFlow.fire) - stream - } - }) - io.in.ready := false.B - for (i <- 0 until num) { - io.outs(i).bits := io.in.bits - when(io.select =/= i.U) { - io.outs(i).valid := false.B - } otherwise { - io.outs(i).valid := io.in.valid - io.in.ready := io.outs(i).ready - } - } -} diff --git a/chipmunk/src/stream/StreamFork.scala b/chipmunk/src/stream/StreamFork.scala deleted file mode 100644 index f53705e..0000000 --- a/chipmunk/src/stream/StreamFork.scala +++ /dev/null @@ -1,152 +0,0 @@ -package chipmunk -package stream - -import chisel3.util._ -import chisel3._ - -/** Fork a StreamIO into multiple StreamIOs. */ -object StreamFork { - - /** Fork a StreamIO into multiple StreamIOs, where their payloads (bits) are unconnected. Payload needs to be - * connected separately. - * - * @param in - * The input StreamIO. - * @param num - * The number of forked downstream StreamIOs. - * @return - * The output downstream StreamIOs driven by the input StreamIO. - * @example - * {{{ - * val in = Stream(UInt(32.W)) - * val outs = StreamFork.withoutPayload(in, 2) - * }}} - */ - def withoutPayload[T <: Data](in: StreamIO[T], num: Int): Vec[StreamIO[EmptyBundle]] = { - val uFork = Module(new StreamFork(num)) - uFork.io.in.handshakeFrom(in) - uFork.io.outs - } - - /** Fork a StreamIO into multiple StreamIOs, where their payloads (bits) are the same as the input StreamIO. - * - * @param in - * The input StreamIO. - * @param num - * The number of forked downstream StreamIOs. - * @return - * The output downstream StreamIOs driven by the input StreamIO. - * @example - * {{{ - * val in = Stream(UInt(32.W)) - * val outs = StreamFork.duplicate(in, 2) - * }}} - */ - def duplicate[T <: Data](in: StreamIO[T], num: Int): Vec[StreamIO[T]] = { - val emptyOuts = withoutPayload(in, num) - VecInit(emptyOuts.map(_.payloadReplace(in.bits))) - } - - /** Fork a StreamIO of `Vec` into a multiple StreamIOs, where their payloads (bits) are each element in the `Vec` - * respectively. The number of forked downstream StreamIOs is the same as the length of the `Vec`. - * - * @param in - * The input StreamIO. - * @return - * The output downstream StreamIOs driven by the input StreamIO. - * @example - * {{{ - * val in = Stream(Vec(16, UInt(32.W))) - * val outs = StreamFork.vecSplit(in) - * }}} - */ - def vecSplit[T <: Data](in: StreamIO[Vec[T]]): Vec[StreamIO[T]] = { - val emptyOuts = withoutPayload(in, in.bits.length) - VecInit(emptyOuts zip in.bits map { case (o, p) => - o.payloadReplace(p) - }) - } - - /** Fork a StreamIO of `MixedVec` into a multiple StreamIOs, where their payloads (bits) are each element in the - * `MixedVec` respectively. The number of forked downstream StreamIOs is the same as the length of the `MixedVec`. - * - * @param in - * The input StreamIO. - * @return - * The output downstream StreamIOs driven by the input StreamIO. - * @example - * {{{ - * val in = Stream(MixedVec(Seq(UInt(32.W), UInt(32.W)))) - * val outs = StreamFork.vecSplit(in) - * }}} - */ - def vecSplit[T <: Data](in: StreamIO[MixedVec[T]]): IndexedSeq[StreamIO[T]] = { - val emptyOuts = withoutPayload(in, in.bits.length) - emptyOuts zip in.bits map { case (o, p) => - o.payloadReplace(p) - } - } - - /** Fork a StreamIO of `Vec` into a multiple StreamIOs, where their payloads (bits) are each element in the `MixedVec` - * generated from `fn(in.bits)`. The number of forked downstream StreamIOs is the same as the length of the - * `MixedVec`. - * - * @param in - * The input StreamIO. - * @return - * The output downstream StreamIOs driven by the input StreamIO. - * @example - * {{{ - * val in = Stream(UInt(32.W)) - * val outs = StreamFork.vecMap(in, { x => MixedVecInit(x + 1.U, x + 2.U) }) - * }}} - */ - def vecMap[T <: Data, T2 <: Data](in: StreamIO[T], fn: T => MixedVec[T2]): IndexedSeq[StreamIO[T2]] = { - vecSplit(in.payloadMap(fn)) - } -} - -/** Connects the input StreamIO handshake to the all of `num` output StreamIO handshakes. For each input transaction, - * every output stream handshakes exactly once. The input StreamIO only fires when all output StreamIOs have fired, but - * the output streams do not have to handshake simultaneously. - * - * The payload is not included in the output StreamIOs. Please connect them with [[StreamIO.payloadReplace]] manually - * outside of this module. - * - * @param num - * The number of StreamIOs to fork. - * @return - * The forked StreamIOs. - */ -class StreamFork(num: Int) extends Module { - require(num >= 2, "StreamFork must have at least 2 output Streams.") - val io = IO(new Bundle { - val in = Slave(Stream.empty) - val outs = Vec(num, Master(Stream.empty)) - }) - - val linkEnable = RegInit(VecInit(Seq.fill(num)(true.B))) - - // Ready is true when every output stream takes or has taken its value - io.in.ready := true.B - for (i <- 0 until num) { - when(!io.outs(i).ready && linkEnable(i)) { - io.in.ready := false.B - } - } - - // Outputs are valid if the input is valid and they haven't taken their value yet. - // When an output fires, mark its value as taken. - for (i <- 0 until num) { - io.outs(i).valid := io.in.valid && linkEnable(i) - io.outs(i).bits := io.in.bits - when(io.outs(i).fire) { - linkEnable(i) := false.B - } - } - - // Reset the storage for each new value - when(io.in.ready) { - linkEnable.foreach(_ := true.B) - } -} diff --git a/chipmunk/src/stream/StreamJoin.scala b/chipmunk/src/stream/StreamJoin.scala deleted file mode 100644 index 7325466..0000000 --- a/chipmunk/src/stream/StreamJoin.scala +++ /dev/null @@ -1,74 +0,0 @@ -package chipmunk -package stream - -import chisel3._ -import chisel3.util._ - -object StreamJoin { - - /** Join multiple StreamIOs into a single StreamIO, where its payload (bits) are unconnected. Payload needs to be - * connected separately. The output handshake happens only once all inputs are valid. - * - * @param ins - * The input StreamIOs. - * @return - * The output StreamIO driven by the input StreamIOs. - * @example - * {{{ - * val in0 = Stream(UInt(32.W)) - * val in1 = Stream(UInt(32.W)) - * val out = StreamJoin.withoutPayload(Seq(in0, in1)) - * }}} - */ - def withoutPayload[T <: Data](ins: IndexedSeq[StreamIO[T]]): StreamIO[EmptyBundle] = { - val uJoin = Module(new StreamJoin(ins.length)) - uJoin.io.ins.zip(ins).foreach { case (joinIn, in) => joinIn.handshakeFrom(in) } - uJoin.io.out - } - - /** Join multiple StreamIOs into a single StreamIO, where its payload (bits) are unconnected. Payload needs to be - * connected separately. The output handshake happens only once all inputs are valid. - * - * @param in0 - * The input StreamIOs. - * @param ins - * more input StreamIOs. - * @return - * The output StreamIO driven by the input StreamIOs. - * @example - * {{{ - * val in0 = Stream(UInt(32.W)) - * val in1 = Stream(UInt(32.W)) - * val out = StreamJoin.withoutPayload(in0, in1) - * }}} - */ - def withoutPayload[T <: Data](in0: StreamIO[T], ins: StreamIO[T]*): StreamIO[EmptyBundle] = { - withoutPayload(in0 +: ins.toIndexedSeq) - } - - def vecMerge[T <: Data](in0: StreamIO[T], ins: StreamIO[T]*): StreamIO[MixedVec[T]] = { - val emptyOut = withoutPayload(in0, ins: _*) - emptyOut.payloadReplace(MixedVecInit(in0.bits +: ins.map(_.bits))) - } - - def vecMerge[T <: Data](ins: IndexedSeq[StreamIO[T]]): StreamIO[MixedVec[T]] = { - val emptyOut = withoutPayload(ins) - emptyOut.payloadReplace(MixedVecInit(ins.map(_.bits))) - } -} - -/** Joins `num` upstream StreamIOs to a single StreamIO. The output handshake happens only once all inputs are valid. - * - * @param num - * The number of StreamIOs to join. - */ -class StreamJoin[T <: Data](num: Int) extends Module { - require(num >= 2, "StreamJoin must have at least 2 input StreamIOs.") - val io = IO(new Bundle { - val ins = Vec(num, Slave(Stream.empty)) - val out = Master(Stream.empty) - }) - - io.out.valid := io.ins.map(_.valid).reduce(_ & _) - io.ins.foreach(_.ready := io.out.fire) -} diff --git a/chipmunk/src/stream/StreamMux.scala b/chipmunk/src/stream/StreamMux.scala deleted file mode 100644 index 233acb0..0000000 --- a/chipmunk/src/stream/StreamMux.scala +++ /dev/null @@ -1,42 +0,0 @@ -package chipmunk -package stream - -import chisel3._ -import chisel3.util._ - -/** Multiplex multiple streams into a single one, always only processing one at a time. */ -object StreamMux { - def apply[T <: Data](select: UInt, ins: Vec[StreamIO[T]]): StreamIO[T] = { - val uMux = Module(new StreamMux(chiselTypeOf(ins(0).bits), ins.length)) - (uMux.io.ins zip ins).foreach(x => x._1 << x._2) - uMux.io.select := select - uMux.io.out - } - - def apply[T <: Data](select: StreamIO[UInt], ins: Vec[StreamIO[T]]): StreamIO[T] = { - val uMux = Module(new StreamMux(chiselTypeOf(ins(0).bits), ins.length)) - (uMux.io.ins zip ins).foreach(x => x._1 << x._2) - select >> uMux.io.createSelectStream() - uMux.io.out - } -} - -class StreamMux[T <: Data](gen: T, num: Int) extends Module { - val io = IO(new Bundle { - val select: UInt = Input(UInt(log2Ceil(num).W)) - val ins: Vec[StreamIO[T]] = Vec(num, Slave(Stream(gen))) - val out: StreamIO[T] = Master(Stream(gen)) - - def createSelectStream(): StreamIO[UInt] = { - val stream = Wire(Stream(UInt(log2Ceil(num).W))) - val regFlow = stream.haltWhen(out.isPending).toFlow(readyFreeRun = true) - select := RegEnable(regFlow.bits, 0.U, regFlow.fire) - stream - } - }) - for ((input, index) <- io.ins.zipWithIndex) { - input.ready := io.select === index.U && io.out.ready - } - io.out.valid := io.ins(io.select).valid - io.out.bits := io.ins(io.select).bits -} diff --git a/chipmunk/src/stream/StreamQueue.scala b/chipmunk/src/stream/StreamQueue.scala deleted file mode 100644 index 4ebc36f..0000000 --- a/chipmunk/src/stream/StreamQueue.scala +++ /dev/null @@ -1,58 +0,0 @@ -package chipmunk -package stream - -import chisel3._ -import chisel3.util._ - -object StreamQueue { - - /** Create a [[Queue]] and supply a [[StreamIO]] containing the product. - * - * It is a wrapper of [[Queue]] with [[StreamIO]] instead of [[DecoupledIO]]. - * - * @param enq - * input (enqueue) interface to the queue, also determines type of queue elements. - * @param entries - * depth (number of elements) of the queue - * @param pipe - * True if a single entry queue can run at full throughput (like a pipeline). The `ready` signals are - * combinationally coupled. - * @param flow - * True if the inputs can be consumed on the same cycle (the inputs "flow" through the queue immediately). The - * `valid` signals are coupled. - * @param useSyncReadMem - * True uses SyncReadMem instead of Mem as an internal memory element. - * @param flush - * Optional [[Bool]] signal, if defined, the [[Queue.hasFlush]] will be true, and connect correspond signal to - * [[Queue]] instance. - * @return - * output (dequeue) interface from the queue. - * - * @example - * {{{consumer.io.in <> Queue(producer.io.out, 16)}}} - */ - def apply[T <: Data]( - enq: ReadyValidIO[T], - entries: Int = 2, - pipe: Boolean = false, - flow: Boolean = false, - useSyncReadMem: Boolean = false, - flush: Option[Bool] = None - ): StreamIO[T] = { - val ret: StreamIO[T] = Wire(Stream(chiselTypeOf(enq.bits))) - val deq: ReadyValidIO[T] = if (entries == 0) { - enq - } else { - val q = Module(new Queue(chiselTypeOf(enq.bits), entries, pipe, flow, useSyncReadMem, flush.isDefined)) - q.io.flush.zip(flush).foreach(f => f._1 := f._2) - q.io.enq.valid := enq.valid // not using <> so that override is allowed - q.io.enq.bits := enq.bits - enq.ready := q.io.enq.ready - q.io.deq - } - ret.bits := deq.bits - ret.valid := deq.valid - deq.ready := ret.ready - ret - } -} diff --git a/chipmunk/src/stream/package.scala b/chipmunk/src/stream/package.scala deleted file mode 100644 index e60cf51..0000000 --- a/chipmunk/src/stream/package.scala +++ /dev/null @@ -1,16 +0,0 @@ -package chipmunk - -import chisel3._ -import chisel3.util._ - -package object stream { - implicit class ImplicitStreamCreator[T <: Data](d: DecoupledIO[T]) { - def toStream: StreamIO[T] = { - val ret = Wire(new StreamIO(chiselTypeOf(d.bits))) - ret.bits := d.bits - ret.valid := d.valid - d.ready := ret.ready - ret - } - } -} diff --git a/chipmunk/src/tester/TesterAPI.scala b/chipmunk/src/tester/TesterAPI.scala deleted file mode 100644 index fadc364..0000000 --- a/chipmunk/src/tester/TesterAPI.scala +++ /dev/null @@ -1,39 +0,0 @@ -package chipmunk -package tester - -import chisel3._ -import chisel3.simulator._ - -trait TesterAPI { - self: PeekPokeAPI => - - implicit class DataTestablePortExtension[T <: Data](target: T)(implicit toTestable: T => Pokable[T]) { - - /** Assign a Chisel literal to the port. */ - def #=(value: T): Unit = toTestable(target).poke(value) - } - - implicit class BoolTestablePortExtension(bool: Bool) { - - /** Assign a random value to the port. */ - def randomize(): Unit = { - bool.poke(scala.util.Random.nextBoolean().B) - } - } - - implicit final class UIntTestablePortExtension(uint: UInt)(implicit toTestable: UInt => Pokable[UInt]) { - - /** Assign a random value to the port. */ - def randomize(): Unit = { - uint.poke(BigInt(uint.getWidth, scala.util.Random).U) - } - } - - implicit final class SIntTestablePortExtension(sint: SInt)(implicit toTestable: SInt => Pokable[SInt]) { - - /** Assign a random value to the port. */ - def randomize(): Unit = { - sint.poke((BigInt(sint.getWidth, scala.util.Random) - (BigInt(1) << (sint.getWidth - 1))).S) - } - } -} diff --git a/chipmunk/test/resources/amba/NicExample0.sv b/chipmunk/test/resources/amba/NicExample0.sv deleted file mode 100644 index 19cab99..0000000 --- a/chipmunk/test/resources/amba/NicExample0.sv +++ /dev/null @@ -1,132 +0,0 @@ -`default_nettype none - -module NicExample0 ( - input var logic ACLOCK, - input var logic ARESETN, - - // Slave: s_axi_spi - - input var logic [1:0] AWID_s_axi_spi, - input var logic [31:0] AWADDR_s_axi_spi, - input var logic [7:0] AWLEN_s_axi_spi, - input var logic [2:0] AWSIZE_s_axi_spi, - input var logic [1:0] AWBURST_s_axi_spi, - input var logic AWLOCK_s_axi_spi, - input var logic [3:0] AWCACHE_s_axi_spi, - input var logic [2:0] AWPROT_s_axi_spi, - input var logic AWVALID_s_axi_spi, - output var logic AWREADY_s_axi_spi, - - input var logic [127:0] WDATA_s_axi_spi, - input var logic [15:0] WSTRB_s_axi_spi, - input var logic WLAST_s_axi_spi, - input var logic WVALID_s_axi_spi, - output var logic WREADY_s_axi_spi, - - output var logic [1:0] BID_s_axi_spi, - output var logic [1:0] BRESP_s_axi_spi, - output var logic BVALID_s_axi_spi, - input var logic BREADY_s_axi_spi, - - input var logic [1:0] ARID_s_axi_spi, - input var logic [31:0] ARADDR_s_axi_spi, - input var logic [7:0] ARLEN_s_axi_spi, - input var logic [2:0] ARSIZE_s_axi_spi, - input var logic [1:0] ARBURST_s_axi_spi, - input var logic ARLOCK_s_axi_spi, - input var logic [3:0] ARCACHE_s_axi_spi, - input var logic [2:0] ARPROT_s_axi_spi, - input var logic ARVALID_s_axi_spi, - output var logic ARREADY_s_axi_spi, - - output var logic [1:0] RID_s_axi_spi, - output var logic [127:0] RDATA_s_axi_spi, - output var logic [1:0] RRESP_s_axi_spi, - output var logic RLAST_s_axi_spi, - output var logic RVALID_s_axi_spi, - input var logic RREADY_s_axi_spi, - - // Master: m_axi_sram - - output var logic [1:0] AWID_m_axi_sram, - output var logic [31:0] AWADDR_m_axi_sram, - output var logic [7:0] AWLEN_m_axi_sram, - output var logic [2:0] AWSIZE_m_axi_sram, - output var logic [1:0] AWBURST_m_axi_sram, - output var logic AWLOCK_m_axi_sram, - output var logic [3:0] AWCACHE_m_axi_sram, - output var logic [2:0] AWPROT_m_axi_sram, - output var logic AWVALID_m_axi_sram, - input var logic AWREADY_m_axi_sram, - - output var logic [127:0] WDATA_m_axi_sram, - output var logic [15:0] WSTRB_m_axi_sram, - output var logic WLAST_m_axi_sram, - output var logic WVALID_m_axi_sram, - input var logic WREADY_m_axi_sram, - - input var logic [1:0] BID_m_axi_sram, - input var logic [1:0] BRESP_m_axi_sram, - input var logic BVALID_m_axi_sram, - output var logic BREADY_m_axi_sram, - - output var logic [1:0] ARID_m_axi_sram, - output var logic [31:0] ARADDR_m_axi_sram, - output var logic [7:0] ARLEN_m_axi_sram, - output var logic [2:0] ARSIZE_m_axi_sram, - output var logic [1:0] ARBURST_m_axi_sram, - output var logic ARLOCK_m_axi_sram, - output var logic [3:0] ARCACHE_m_axi_sram, - output var logic [2:0] ARPROT_m_axi_sram, - output var logic ARVALID_m_axi_sram, - input var logic ARREADY_m_axi_sram, - - input var logic [1:0] RID_m_axi_sram, - input var logic [127:0] RDATA_m_axi_sram, - input var logic [1:0] RRESP_m_axi_sram, - input var logic RLAST_m_axi_sram, - input var logic RVALID_m_axi_sram, - output var logic RREADY_m_axi_sram -); - - assign AWID_m_axi_sram = AWID_s_axi_spi; - assign AWADDR_m_axi_sram = AWADDR_s_axi_spi; - assign AWLEN_m_axi_sram = AWLEN_s_axi_spi; - assign AWSIZE_m_axi_sram = AWSIZE_s_axi_spi; - assign AWBURST_m_axi_sram = AWBURST_s_axi_spi; - assign AWLOCK_m_axi_sram = AWLOCK_s_axi_spi; - assign AWCACHE_m_axi_sram = AWCACHE_s_axi_spi; - assign AWPROT_m_axi_sram = AWPROT_s_axi_spi; - assign AWVALID_m_axi_sram = AWVALID_s_axi_spi; - assign AWREADY_s_axi_spi = AWREADY_m_axi_sram; - - assign WDATA_m_axi_sram = WDATA_s_axi_spi; - assign WSTRB_m_axi_sram = WSTRB_s_axi_spi; - assign WLAST_m_axi_sram = WLAST_s_axi_spi; - assign WVALID_m_axi_sram = WVALID_s_axi_spi; - assign WREADY_s_axi_spi = WREADY_m_axi_sram; - - assign BID_s_axi_spi = BID_m_axi_sram; - assign BRESP_s_axi_spi = BRESP_m_axi_sram; - assign BVALID_s_axi_spi = BVALID_m_axi_sram; - assign BREADY_m_axi_sram = BREADY_s_axi_spi; - - assign ARID_m_axi_sram = ARID_s_axi_spi; - assign ARADDR_m_axi_sram = ARADDR_s_axi_spi; - assign ARLEN_m_axi_sram = ARLEN_s_axi_spi; - assign ARSIZE_m_axi_sram = ARSIZE_s_axi_spi; - assign ARBURST_m_axi_sram = ARBURST_s_axi_spi; - assign ARLOCK_m_axi_sram = ARLOCK_s_axi_spi; - assign ARCACHE_m_axi_sram = ARCACHE_s_axi_spi; - assign ARPROT_m_axi_sram = ARPROT_s_axi_spi; - assign ARVALID_m_axi_sram = ARVALID_s_axi_spi; - assign ARREADY_s_axi_spi = ARREADY_m_axi_sram; - - assign RID_s_axi_spi = RID_m_axi_sram; - assign RDATA_s_axi_spi = RDATA_m_axi_sram; - assign RRESP_s_axi_spi = RRESP_m_axi_sram; - assign RLAST_s_axi_spi = RLAST_m_axi_sram; - assign RVALID_s_axi_spi = RVALID_m_axi_sram; - assign RREADY_m_axi_sram = RREADY_s_axi_spi; - -endmodule diff --git a/chipmunk/test/resources/amba/NicExample1.sv b/chipmunk/test/resources/amba/NicExample1.sv deleted file mode 100644 index cf6117e..0000000 --- a/chipmunk/test/resources/amba/NicExample1.sv +++ /dev/null @@ -1,136 +0,0 @@ -`default_nettype none - -module NicExample1 ( - input var logic clock, - input var logic resetn, - - // Slave: s_axi_spi - - input var logic [1:0] s_axi_spi_AWID, - input var logic [31:0] s_axi_spi_AWADDR, - input var logic [7:0] s_axi_spi_AWLEN, - input var logic [2:0] s_axi_spi_AWSIZE, - input var logic [1:0] s_axi_spi_AWBURST, - input var logic s_axi_spi_AWLOCK, - input var logic [3:0] s_axi_spi_AWCACHE, - input var logic [2:0] s_axi_spi_AWPROT, - input var logic s_axi_spi_AWVALID, - output var logic s_axi_spi_AWREADY, - - input var logic [127:0] s_axi_spi_WDATA, - input var logic [15:0] s_axi_spi_WSTRB, - input var logic s_axi_spi_WLAST, - input var logic s_axi_spi_WVALID, - output var logic s_axi_spi_WREADY, - - output var logic [1:0] s_axi_spi_BID, - output var logic [1:0] s_axi_spi_BRESP, - output var logic s_axi_spi_BVALID, - input var logic s_axi_spi_BREADY, - - input var logic [1:0] s_axi_spi_ARID, - input var logic [31:0] s_axi_spi_ARADDR, - input var logic [7:0] s_axi_spi_ARLEN, - input var logic [2:0] s_axi_spi_ARSIZE, - input var logic [1:0] s_axi_spi_ARBURST, - input var logic s_axi_spi_ARLOCK, - input var logic [3:0] s_axi_spi_ARCACHE, - input var logic [2:0] s_axi_spi_ARPROT, - input var logic s_axi_spi_ARVALID, - output var logic s_axi_spi_ARREADY, - - output var logic [1:0] s_axi_spi_RID, - output var logic [127:0] s_axi_spi_RDATA, - output var logic [1:0] s_axi_spi_RRESP, - output var logic s_axi_spi_RLAST, - output var logic s_axi_spi_RVALID, - input var logic s_axi_spi_RREADY, - - // Master: m_axi_sram - - output var logic [1:0] m_axi_sram_AWID, - output var logic [31:0] m_axi_sram_AWADDR, - output var logic [7:0] m_axi_sram_AWLEN, - output var logic [2:0] m_axi_sram_AWSIZE, - output var logic [1:0] m_axi_sram_AWBURST, - output var logic m_axi_sram_AWLOCK, - output var logic [3:0] m_axi_sram_AWCACHE, - output var logic [2:0] m_axi_sram_AWPROT, - output var logic [3:0] m_axi_sram_AWREGION, - output var logic m_axi_sram_AWVALID, - input var logic m_axi_sram_AWREADY, - - output var logic [127:0] m_axi_sram_WDATA, - output var logic [15:0] m_axi_sram_WSTRB, - output var logic m_axi_sram_WLAST, - output var logic m_axi_sram_WVALID, - input var logic m_axi_sram_WREADY, - - input var logic [1:0] m_axi_sram_BID, - input var logic [1:0] m_axi_sram_BRESP, - input var logic m_axi_sram_BVALID, - output var logic m_axi_sram_BREADY, - - output var logic [1:0] m_axi_sram_ARID, - output var logic [31:0] m_axi_sram_ARADDR, - output var logic [7:0] m_axi_sram_ARLEN, - output var logic [2:0] m_axi_sram_ARSIZE, - output var logic [1:0] m_axi_sram_ARBURST, - output var logic m_axi_sram_ARLOCK, - output var logic [3:0] m_axi_sram_ARCACHE, - output var logic [2:0] m_axi_sram_ARPROT, - output var logic [3:0] m_axi_sram_ARREGION, - output var logic m_axi_sram_ARVALID, - input var logic m_axi_sram_ARREADY, - - input var logic [1:0] m_axi_sram_RID, - input var logic [127:0] m_axi_sram_RDATA, - input var logic [1:0] m_axi_sram_RRESP, - input var logic m_axi_sram_RLAST, - input var logic m_axi_sram_RVALID, - output var logic m_axi_sram_RREADY -); - - assign m_axi_sram_AWID = s_axi_spi_AWID; - assign m_axi_sram_AWADDR = s_axi_spi_AWADDR; - assign m_axi_sram_AWLEN = s_axi_spi_AWLEN; - assign m_axi_sram_AWSIZE = s_axi_spi_AWSIZE; - assign m_axi_sram_AWBURST = s_axi_spi_AWBURST; - assign m_axi_sram_AWLOCK = s_axi_spi_AWLOCK; - assign m_axi_sram_AWCACHE = s_axi_spi_AWCACHE; - assign m_axi_sram_AWPROT = s_axi_spi_AWPROT; - assign m_axi_sram_AWREGION = '0; - assign m_axi_sram_AWVALID = s_axi_spi_AWVALID; - assign s_axi_spi_AWREADY = m_axi_sram_AWREADY; - - assign m_axi_sram_WDATA = s_axi_spi_WDATA; - assign m_axi_sram_WSTRB = s_axi_spi_WSTRB; - assign m_axi_sram_WLAST = s_axi_spi_WLAST; - assign m_axi_sram_WVALID = s_axi_spi_WVALID; - assign s_axi_spi_WREADY = m_axi_sram_WREADY; - - assign s_axi_spi_BID = m_axi_sram_BID; - assign s_axi_spi_BRESP = m_axi_sram_BRESP; - assign s_axi_spi_BVALID = m_axi_sram_BVALID; - assign m_axi_sram_BREADY = s_axi_spi_BREADY; - - assign m_axi_sram_ARID = s_axi_spi_ARID; - assign m_axi_sram_ARADDR = s_axi_spi_ARADDR; - assign m_axi_sram_ARLEN = s_axi_spi_ARLEN; - assign m_axi_sram_ARSIZE = s_axi_spi_ARSIZE; - assign m_axi_sram_ARBURST = s_axi_spi_ARBURST; - assign m_axi_sram_ARLOCK = s_axi_spi_ARLOCK; - assign m_axi_sram_ARCACHE = s_axi_spi_ARCACHE; - assign m_axi_sram_ARPROT = s_axi_spi_ARPROT; - assign m_axi_sram_ARREGION = '0; - assign m_axi_sram_ARVALID = s_axi_spi_ARVALID; - assign s_axi_spi_ARREADY = m_axi_sram_ARREADY; - - assign s_axi_spi_RID = m_axi_sram_RID; - assign s_axi_spi_RDATA = m_axi_sram_RDATA; - assign s_axi_spi_RRESP = m_axi_sram_RRESP; - assign s_axi_spi_RLAST = m_axi_sram_RLAST; - assign s_axi_spi_RVALID = m_axi_sram_RVALID; - assign m_axi_sram_RREADY = s_axi_spi_RREADY; - -endmodule diff --git a/chipmunk/test/resources/amba/NicExample2.sv b/chipmunk/test/resources/amba/NicExample2.sv deleted file mode 100644 index c0f7184..0000000 --- a/chipmunk/test/resources/amba/NicExample2.sv +++ /dev/null @@ -1,75 +0,0 @@ -`default_nettype none - -module NicExample2 #( - parameter int S00_AW = 32, - parameter int S00_DW = 32, - parameter int M00_AW = 32, - parameter int M00_DW = 32 -)( - input var logic clock, - input var logic resetn, - - input var logic [S00_AW-1 : 0] s00_axi_awaddr, - input var logic [2 : 0] s00_axi_awprot, - input var logic s00_axi_awvalid, - output var logic s00_axi_awready, - input var logic [S00_DW-1 : 0] s00_axi_wdata, - input var logic [(S00_DW/8)-1 : 0] s00_axi_wstrb, - input var logic s00_axi_wvalid, - output var logic s00_axi_wready, - output var logic [1 : 0] s00_axi_bresp, - output var logic s00_axi_bvalid, - input var logic s00_axi_bready, - input var logic [S00_AW-1 : 0] s00_axi_araddr, - input var logic [2 : 0] s00_axi_arprot, - input var logic s00_axi_arvalid, - output var logic s00_axi_arready, - output var logic [S00_DW-1 : 0] s00_axi_rdata, - output var logic [1 : 0] s00_axi_rresp, - output var logic s00_axi_rvalid, - input var logic s00_axi_rready, - - output var logic [S00_AW-1 : 0] m00_axi_awaddr, - output var logic [2 : 0] m00_axi_awprot, - output var logic m00_axi_awvalid, - input var logic m00_axi_awready, - output var logic [S00_DW-1 : 0] m00_axi_wdata, - output var logic [(S00_DW/8)-1 : 0] m00_axi_wstrb, - output var logic m00_axi_wvalid, - input var logic m00_axi_wready, - input var logic [1 : 0] m00_axi_bresp, - input var logic m00_axi_bvalid, - output var logic m00_axi_bready, - output var logic [S00_AW-1 : 0] m00_axi_araddr, - output var logic [2 : 0] m00_axi_arprot, - output var logic m00_axi_arvalid, - input var logic m00_axi_arready, - input var logic [S00_DW-1 : 0] m00_axi_rdata, - input var logic [1 : 0] m00_axi_rresp, - input var logic m00_axi_rvalid, - output var logic m00_axi_rready -); - - always_comb begin - m00_axi_awaddr = s00_axi_awaddr; - m00_axi_awprot = s00_axi_awprot; - m00_axi_awvalid = s00_axi_awvalid; - s00_axi_awready = m00_axi_awready; - m00_axi_wdata = s00_axi_wdata; - m00_axi_wstrb = s00_axi_wstrb; - m00_axi_wvalid = s00_axi_wvalid; - s00_axi_wready = m00_axi_wready; - s00_axi_bresp = m00_axi_bresp; - s00_axi_bvalid = m00_axi_bvalid; - m00_axi_bready = s00_axi_bready; - m00_axi_araddr = s00_axi_araddr; - m00_axi_arprot = s00_axi_arprot; - m00_axi_arvalid = s00_axi_arvalid; - s00_axi_arready = m00_axi_arready; - s00_axi_rdata = m00_axi_rdata; - s00_axi_rresp = m00_axi_rresp; - s00_axi_rvalid = m00_axi_rvalid; - m00_axi_rready = s00_axi_rready; - end - -endmodule : NicExample2 diff --git a/chipmunk/test/resources/amba/NicExample3.sv b/chipmunk/test/resources/amba/NicExample3.sv deleted file mode 100644 index e239a2b..0000000 --- a/chipmunk/test/resources/amba/NicExample3.sv +++ /dev/null @@ -1,44 +0,0 @@ -`default_nettype none - -module NicExample3 #( - parameter int S00_AW = 32, - parameter int S00_DW = 32, - parameter int M00_AW = 32, - parameter int M00_DW = 32 -)( - input var logic clock, - input var logic resetn, - - input var logic [S00_AW-1 : 0] s00_paddr, - input var logic [2 : 0] s00_pprot, - input var logic [3 : 0] s00_pselx, - input var logic s00_penable, - input var logic s00_pwrite, - input var logic [S00_DW-1 : 0] s00_pwdata, - input var logic [S00_DW/8-1 : 0] s00_pstrb, - output var logic s00_pready, - output var logic s00_pslverr, - output var logic [S00_DW-1 : 0] s00_prdata, - - output var logic [M00_AW-1 : 0] m00_paddr, - output var logic [3 : 0] m00_psel, - output var logic m00_penable, - output var logic m00_pwrite, - output var logic [M00_DW-1 : 0] m00_pwdata, - input var logic m00_pready, - input var logic m00_pslverr, - input var logic [M00_DW-1 : 0] m00_prdata -); - - always_comb begin - m00_paddr = s00_paddr; - m00_psel = s00_pselx; - m00_penable = s00_penable; - m00_pwrite = s00_pwrite; - m00_pwdata = s00_pwdata; - s00_pready = m00_pready; - s00_pslverr = m00_pslverr; - s00_prdata = m00_prdata; - end - -endmodule : NicExample3 diff --git a/chipmunk/test/src/BaseSpec.scala b/chipmunk/test/src/BaseSpec.scala deleted file mode 100644 index b81ecee..0000000 --- a/chipmunk/test/src/BaseSpec.scala +++ /dev/null @@ -1,15 +0,0 @@ -package chipmunk.test - -import chipmunk.tester.TesterAPI -import chisel3.simulator.scalatest.ChiselSim -import org.scalatest.Assertions -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers - -abstract class ChipmunkFlatSpec extends AnyFlatSpec with ChiselSim with TesterAPI with Assertions with Matchers - -abstract class ChipmunkFreeSpec extends AnyFreeSpec with ChiselSim with TesterAPI with Assertions with Matchers - -abstract class ChipmunkFunSpec extends AnyFunSpec with ChiselSim with TesterAPI with Assertions with Matchers diff --git a/chipmunk/test/src/BitsSpec.scala b/chipmunk/test/src/BitsSpec.scala deleted file mode 100644 index 8b9aa61..0000000 --- a/chipmunk/test/src/BitsSpec.scala +++ /dev/null @@ -1,58 +0,0 @@ -package chipmunk.test - -import chipmunk._ -import chisel3._ - -class BitsSpec extends ChipmunkFlatSpec { - "Bits" should "have extended methods" in { - simulate(new Module { - val io = IO(new Bundle { - val in = Input(UInt(4.W)) - val lsBit = Output(Bool()) - val msBit = Output(Bool()) - val lsBits = Output(UInt(2.W)) - val msBits = Output(UInt(2.W)) - val isOneHot = Output(Bool()) - val allOnes = Output(UInt(4.W)) - val allZeros = Output(UInt(4.W)) - val filledLsb = Output(UInt(4.W)) - val assignBits = Output(UInt(4.W)) - }) - io.lsBit := io.in.lsBit - io.msBit := io.in.msBit - io.lsBits := io.in.lsBits(2) - io.msBits := io.in.msBits(2) - io.isOneHot := io.in.isOneHot - io.allOnes := io.in.filledOnes - io.allZeros := io.in.filledZeros - io.filledLsb := io.in.filledWith(io.in(0)) - when(io.lsBit) { - io.assignBits.assignOnes() - }.otherwise { - io.assignBits.assignZeros() - } - }) { dut => - dut.io.in poke 0b0101.U - dut.io.lsBit expect true.B - dut.io.msBit expect false.B - dut.io.lsBits expect 1.U - dut.io.msBits expect 1.U - dut.io.isOneHot expect false.B - dut.io.allOnes expect 0b1111.U - dut.io.allZeros expect 0.U - dut.io.filledLsb expect 0b1111.U - dut.io.assignBits expect 0b1111.U - dut.clock.step() - dut.io.in poke 0b1000.U - dut.io.lsBit expect false.B - dut.io.msBit expect true.B - dut.io.lsBits expect 0.U - dut.io.msBits expect 2.U - dut.io.isOneHot expect true.B - dut.io.allOnes expect 0b1111.U - dut.io.allZeros expect 0.U - dut.io.filledLsb expect 0b0000.U - dut.io.assignBits expect 0b0000.U - } - } -} diff --git a/chipmunk/test/src/IsMasterSlaveSpec.scala b/chipmunk/test/src/IsMasterSlaveSpec.scala deleted file mode 100644 index 5b015b2..0000000 --- a/chipmunk/test/src/IsMasterSlaveSpec.scala +++ /dev/null @@ -1,65 +0,0 @@ -package chipmunk.test - -import chipmunk._ -import chipmunk.stream._ -import chisel3._ - -class IsMasterSlaveSpec extends ChipmunkFlatSpec { - "IsMasterSlave" should "help create master/slave bundles" in { - class MyBundle extends Bundle with IsMasterSlave { - val a = Input(UInt(3.W)) - val b = Output(UInt(3.W)) - - override def isMaster: Boolean = true - } - simulate(new Module { - val io = IO(new Bundle { - val m = Master(new MyBundle) - val s = Slave(new MyBundle) - }) - io.m.b := io.s.b - io.s.a := io.m.a - }) { dut => - dut.clock.step() - } - } - - it should "help create hierarchical master/slave bundles" in { - class MyBundle extends Bundle with IsMasterSlave { - val a = Slave(Stream(UInt(3.W))) - - override def isMaster: Boolean = true - } - simulate(new Module { - val io = IO(new Bundle { - val x = Master(new MyBundle) - val y = Master(Stream(UInt(3.W))) - }) - io.y.valid := io.x.a.valid - io.x.a.ready := io.y.ready - io.y.bits := io.x.a.bits - }) { dut => - dut.clock.step() - } - } - - it should "throw Exception when nested-ly decorated" in { - class MyBundle extends Bundle with IsMasterSlave { - val a = Input(UInt(3.W)) - val b = Output(UInt(3.W)) - - override def isMaster: Boolean = true - } - assertThrows[IllegalArgumentException] { - simulate(new Module { - val io = IO(new Bundle { - val m = Master(Master(new MyBundle)) - val s = Slave(new MyBundle) - }) - io.m <> io.s - }) { dut => - dut.clock.step() - } - } - } -} diff --git a/chipmunk/test/src/MuxPriorityDefaultSpec.scala b/chipmunk/test/src/MuxPriorityDefaultSpec.scala deleted file mode 100644 index e909c7c..0000000 --- a/chipmunk/test/src/MuxPriorityDefaultSpec.scala +++ /dev/null @@ -1,41 +0,0 @@ -package chipmunk.test - -import chipmunk._ -import chisel3._ - -class MuxPriorityDefaultSpec extends ChipmunkFlatSpec { - "MuxPriorityDefault" should "return the same result as MuxPriority, or the default value if no sel signal is asserted" in { - simulate(new Module { - val io = IO(new Bundle { - val sel = Input(UInt(3.W)) - val result = Output(Vec(4, UInt(2.W))) - }) - val choices = Seq(1.U, 2.U, 3.U) - io.result(0) := MuxPriorityDefault(io.sel, choices, default = 0.U) - io.result(1) := MuxPriorityDefault(io.sel.asBools, choices, default = 0.U) - io.result(2) := MuxPriorityDefault( - Seq(io.sel(0).asBool -> 1.U, io.sel(1).asBool -> 2.U, io.sel(2).asBool -> 3.U), - default = 0.U - ) - io.result(3) := MuxPriority(io.sel, choices) - }) { dut => - dut.io.sel poke 0.U - dut.io.result(0) expect 0.U - dut.io.result(1) expect 0.U - dut.io.result(2) expect 0.U - // dut.io.result(3) is undefined when no sel asserted - dut.clock.step() - dut.io.sel poke 0b010.U - dut.io.result(0) expect 2.U - dut.io.result(1) expect 2.U - dut.io.result(2) expect 2.U - dut.io.result(3) expect 2.U - dut.clock.step() - dut.io.sel poke 0b011.U - dut.io.result(0) expect 1.U - dut.io.result(1) expect 1.U - dut.io.result(2) expect 1.U - dut.io.result(3) expect 1.U - } - } -} diff --git a/chipmunk/test/src/RegBankSpec.scala b/chipmunk/test/src/RegBankSpec.scala deleted file mode 100644 index 48a7026..0000000 --- a/chipmunk/test/src/RegBankSpec.scala +++ /dev/null @@ -1,130 +0,0 @@ -package chipmunk.test - -import chipmunk.regbank._ -import chisel3._ - -class RegBankSpec extends ChipmunkFlatSpec { - "RegBank" should "fail to compile when element addresses conflict" in { - assertThrows[IllegalArgumentException] { - val regsConfig = - Seq( - RegElementConfig("R1", addr = 0, bitCount = 16), - RegElementConfig("R2", addr = 2, bitCount = 16), - RegElementConfig("R3", addr = 2, bitCount = 16) - ) - simulate(new RegBank(addrWidth = 8, dataWidth = 16, regs = regsConfig)) { dut => - dut.clock.step() - } - } - } - - it should "fail to compile when field bit ranges overlap" in { - assertThrows[IllegalArgumentException] { - val regsConfig = - Seq( - RegElementConfig( - "R1", - addr = 0, - fields = Seq( - RegFieldConfig("F1", baseOffset = 0, bitCount = 16), - RegFieldConfig("F2", baseOffset = 8, bitCount = 16) - ) - ) - ) - simulate(new RegBank(addrWidth = 8, dataWidth = 16, regs = regsConfig)) { dut => - dut.clock.step() - } - } - } - - it should "allow field access by either frontdoor or backdoor" in { - val regsConfig = - Seq( - RegElementConfig("R1", addr = 0, bitCount = 16, initValue = 0x1234.U), - RegElementConfig( - "R2", - addr = 1, - fields = Seq( - RegFieldConfig("F1", baseOffset = 0, bitCount = 8, initValue = 0x56.U, backdoorUpdate = true), - RegFieldConfig("F2", baseOffset = 8, bitCount = 8, initValue = 0x78.U) - ) - ) - ) - simulate(new RegBank(addrWidth = 8, dataWidth = 16, regs = regsConfig)) { dut => - dut.io.access.wr.rsp.ready #= true.B - dut.io.access.rd.rsp.ready #= true.B - dut.io.access.wr.cmd.bits.strobe #= 0x3.U - dut.io.fields("R1").value expect 0x1234.U - dut.io.fields("R2_F1").value expect 0x56.U - dut.io.fields("R2_F2").value expect 0x78.U - dut.io.fields("R2_F1").backdoorUpdate.get.valid #= true.B - dut.io.fields("R2_F1").backdoorUpdate.get.bits #= 0xab.U - dut.clock.step() - dut.io.fields("R2_F1").backdoorUpdate.get.valid #= false.B - dut.io.access.wr.cmd.valid #= true.B - dut.io.access.wr.cmd.bits.addr #= 1.U - dut.io.access.wr.cmd.bits.data #= 0x1234.U - dut.io.fields("R2_F1").value expect 0xab.U - dut.clock.step() - dut.io.access.wr.cmd.valid #= false.B - dut.io.access.rd.cmd.valid #= true.B - dut.io.access.rd.cmd.bits.addr #= 1.U - dut.io.fields("R2_F1").value expect 0x34.U - dut.io.fields("R2_F2").value expect 0x12.U - dut.clock.step() - dut.io.access.rd.rsp.bits.data expect 0x1234.U - } - } - - it should "allow field frontdoor writing with mask" in { - val regsConfig = - Seq(RegElementConfig("R1", addr = 0, bitCount = 16, initValue = 0x1234.U)) - simulate(new RegBank(addrWidth = 8, dataWidth = 16, regs = regsConfig)) { dut => - dut.io.access.wr.rsp.ready #= true.B - dut.io.access.rd.rsp.ready #= true.B - dut.io.access.wr.cmd.valid #= true.B - dut.io.access.wr.cmd.bits.addr #= 0.U - dut.io.access.wr.cmd.bits.strobe #= 0x1.U - dut.io.access.wr.cmd.bits.data #= 0x5678.U - dut.io.fields("R1").value expect 0x1234.U - dut.clock.step() - dut.io.fields("R1").value expect 0x1278.U - } - } - - it should "update field values according to access types" in { - val regsConfig = - Seq( - RegElementConfig("R1", addr = 0, bitCount = 16, accessType = RegFieldAccessType.WriteSetReadClear), - RegElementConfig("R2", addr = 1, bitCount = 16, accessType = RegFieldAccessType.WriteOneToggle), - RegElementConfig("R3", addr = 2, bitCount = 16, accessType = RegFieldAccessType.ReadOnly) - ) - simulate(new RegBank(addrWidth = 8, dataWidth = 16, regs = regsConfig)) { dut => - dut.io.access.wr.rsp.ready #= true.B - dut.io.access.rd.rsp.ready #= true.B - dut.io.access.wr.cmd.valid #= true.B - dut.io.access.wr.cmd.bits.addr #= 0.U - dut.io.access.wr.cmd.bits.strobe #= 0x1.U - dut.io.access.wr.cmd.bits.data #= 0x1234.U - dut.io.fields("R1").value expect 0.U - dut.clock.step() - dut.io.access.wr.cmd.valid #= false.B - dut.io.access.rd.cmd.valid #= true.B - dut.io.access.rd.cmd.bits.addr #= 0.U - dut.io.fields("R1").value expect 0x00ff.U - dut.clock.step() - dut.io.access.rd.cmd.valid #= false.B - dut.io.access.wr.cmd.valid #= true.B - dut.io.access.wr.cmd.bits.addr #= 1.U - dut.io.access.wr.cmd.bits.strobe #= 0x3.U - dut.io.access.wr.cmd.bits.data #= 0x5555.U - dut.io.fields("R2").value expect 0.U - dut.clock.step() - dut.io.access.wr.cmd.bits.addr #= 2.U - dut.io.access.wr.cmd.bits.data #= 0x5555.U - dut.io.fields("R2").value expect 0x5555.U - dut.clock.step() - dut.io.fields("R3").value expect 0.U - } - } -} diff --git a/chipmunk/test/src/StateMachineSpec.scala b/chipmunk/test/src/StateMachineSpec.scala deleted file mode 100644 index a2b29bd..0000000 --- a/chipmunk/test/src/StateMachineSpec.scala +++ /dev/null @@ -1,165 +0,0 @@ -package chipmunk.test - -import chipmunk._ -import chisel3._ - -class StateMachineSpec extends ChipmunkFlatSpec { - "StateMachine" should "work in a elegant style" in { - simulate(new Module { - val io = IO(new Bundle { - val a = Input(Bool()) - val b = Input(Bool()) - val c = Input(Bool()) - val state = Output(UInt(2.W)) - val cIsActive = Output(Bool()) - val bIsEntering = Output(Bool()) - val cIsExiting = Output(Bool()) - }) - val fsm = new StateMachine { - val sA = new State with EntryPoint - val sB = new State - val sC = new State - sA - .whenIsActive { - when(io.a) { - goto(sB) - } - } - sB - .whenIsActive { - when(io.b) { - goto(sC) - } - } - sC - .whenIsActive { - when(io.c) { - goto(sA) - } - } - } - io.state := fsm.stateCurr - io.cIsActive := fsm.isActive(fsm.sC) - io.bIsEntering := fsm.isEntering(fsm.sB) - io.cIsExiting := fsm.isExiting(fsm.sC) - }) { dut => - // a _ 1 1 1 _ _ _ - // b _ _ _ 1 _ _ _ - // c _ _ _ _ _ 1 _ - // state A A B B C C A - dut.reset #= true.B - dut.io.a #= false.B - dut.io.b #= false.B - dut.io.c #= false.B - dut.clock.step(5) - dut.reset #= false.B - dut.clock.step() - dut.io.state expect 1.U - dut.io.a #= true.B - dut.io.bIsEntering expect true.B - dut.clock.step(2) - dut.io.state expect 2.U - dut.io.b #= true.B - dut.clock.step() - dut.io.state expect 3.U - dut.io.a #= false.B - dut.io.b #= false.B - dut.clock.step() - dut.io.state expect 3.U - dut.io.cIsActive expect true.B - dut.io.c #= true.B - dut.io.cIsExiting expect true.B - dut.clock.step() - dut.io.state expect 1.U - dut.io.c #= false.B - } - } - - protected class StateMachineWithEncoding(encoding: StateMachineEncoding) extends Module { - val io = IO(new Bundle { - val state = Output(UInt(4.W)) // 4-bit is enough for all the following encodings - }) - val fsm = new StateMachine(defaultEncoding = encoding) { - val sA = new State with EntryPoint - val sB = new State - val sC = new State - sA.whenIsActive { - goto(sB) - } - sB.whenIsActive { - goto(sC) - } - sC.whenIsActive { - goto(sA) - } - } - io.state := fsm.stateCurr - } - - it should "allow encode FSM states in a binary-sequential encoding" in { - simulate(new StateMachineWithEncoding(Sequential)) { dut => - dut.io.state expect 0.U - dut.clock.step() - dut.io.state expect 1.U - dut.clock.step() - dut.io.state expect 2.U - dut.clock.step() - dut.io.state expect 3.U - } - } - - it should "allow encode FSM states in a binary-onehot encoding" in { - simulate(new StateMachineWithEncoding(OneHot)) { dut => - dut.io.state expect "b0001".U - dut.clock.step() - dut.io.state expect "b0010".U - dut.clock.step() - dut.io.state expect "b0100".U - dut.clock.step() - dut.io.state expect "b1000".U - } - } - - it should "allow encode FSM states in a binary-gray encoding" in { - simulate(new StateMachineWithEncoding(Gray)) { dut => - dut.io.state expect "b00".U - dut.clock.step() - dut.io.state expect "b01".U - dut.clock.step() - dut.io.state expect "b11".U - dut.clock.step() - dut.io.state expect "b10".U - } - } - - it should "allow encode FSM states with user-specified state ids" in { - simulate(new Module { - val io = IO(new Bundle { - val state = Output(UInt(5.W)) // 5-bit for user-defined state ids - }) - val fsm = new StateMachine { - val sA = new State(2.U) with EntryPoint - val sB = new State - val sC = new State(18.U) - sA.whenIsActive { - goto(sB) - } - sB.whenIsActive { - goto(sC) - } - sC.whenIsActive { - goto(sA) - } - } - io.state := fsm.stateCurr - }) { dut => - dut.io.state expect 0.U // BOOT - dut.clock.step() - dut.io.state expect 2.U // sA - dut.clock.step() - dut.io.state expect 1.U // sB - dut.clock.step() - dut.io.state expect 18.U // sC - } - } -} diff --git a/chipmunk/test/src/TesterSpec.scala b/chipmunk/test/src/TesterSpec.scala deleted file mode 100644 index 53a85e6..0000000 --- a/chipmunk/test/src/TesterSpec.scala +++ /dev/null @@ -1,75 +0,0 @@ -package chipmunk.test - -import chisel3._ -import chisel3.simulator.HasSimulator -import chisel3.testing.HasTestingDirectory - -import java.nio.file.FileSystems - -class TesterSimpleSpec extends ChipmunkFlatSpec { - "Tester" should "compile a DUT and run its testbench to generate a waveform" in { - implicit val verilatorWithVcd: HasSimulator = HasSimulator.simulators.verilator(verilatorSettings = - svsim.verilator.Backend.CompilationSettings.default.withTraceStyle({ - import svsim.verilator.Backend.CompilationSettings._ - Some(TraceStyle(TraceKind.Vcd)) - }) - ) - simulate(new Module { - val io = IO(new Bundle { - val a = Input(SInt(3.W)) - val b = Output(SInt(3.W)) - }) - io.b := io.a - }) { dut => - enableWaves() - dut.clock.step() - dut.io.a #= -1.S(3.W) - dut.clock.step() - dut.io.b expect -1 - } - - val vcdFile = FileSystems.getDefault - .getPath(implicitly[HasTestingDirectory].getDirectory.toString, "workdir-verilator", "trace.vcd") - .toFile - vcdFile should exist - } - - it should "throw Exception when DUT compilation fails" in { - assertThrows[ChiselException] { - simulate(new Module { - val io = IO(new Bundle { - val a = Input(SInt(3.W)) - val b = Output(UInt(3.W)) - }) - io.b := io.a // Type mismatch - }) { dut => - dut.clock.step() - } - } - } -} - -class TesterAPISpec extends ChipmunkFlatSpec { - "TesterAPI" should "peek or poke signal values of DUT ports" in { - simulate(new Module { - val io = IO(new Bundle { - val a = Input(SInt(3.W)) - val b = Output(UInt(3.W)) - val c = Input(Vec(2, SInt(3.W))) - val d = Output(Vec(2, UInt(3.W))) - }) - io.b := io.a.asUInt - io.d.zip(io.c).foreach { case (o, i) => o := i.asUInt } - }) { dut => - dut.clock.step() - dut.io.a #= -1.S(3.W) - dut.io.b.peekValue() shouldBe svsim.Simulation.Value(bitCount = 3, asBigInt = 7) - dut.io.b expect 7 - dut.io.b expect 7.U - dut.io.c(0) #= 1.S - dut.io.c(1) #= 2.S(3.W) - dut.io.d(0) expect 1 - dut.io.d(1) expect 2.U - } - } -} diff --git a/chipmunk/test/src/amba/ApbVerilogIOSpec.scala b/chipmunk/test/src/amba/ApbVerilogIOSpec.scala deleted file mode 100644 index 6560284..0000000 --- a/chipmunk/test/src/amba/ApbVerilogIOSpec.scala +++ /dev/null @@ -1,57 +0,0 @@ -package chipmunk.test -package amba - -import chipmunk._ -import chipmunk.amba._ -import chisel3._ -import chisel3.experimental.ExtModule -import chisel3.util.HasExtModuleResource -import chisel3.experimental.dataview._ - -class NicExample3Bbox(dw: Int = 32, aw: Int = 32) - extends ExtModule(Map("S00_DW" -> dw, "S00_AW" -> aw, "M00_DW" -> dw, "M00_AW" -> aw)) - with HasExtModuleResource { - val clock = IO(Input(Clock())) - val resetn = IO(Input(Reset())) - - val s0 = - IO( - Slave( - new Apb4IO(dataWidth = dw, addrWidth = aw, hasProt = true, hasStrb = true) - .createVerilogIO(Seq(PortNameTransform.toggleCase)) - ) - ).suggestName("s00") - val m0 = - IO( - Master( - new Apb3IO(dataWidth = dw, addrWidth = aw) - .createVerilogIO(Seq(PortNameTransform.overrideName(Map("PSELX" -> "PSEL")), PortNameTransform.lowerCase)) - ) - ).suggestName("m00") - - override def desiredName = "NicExample3" - addResource("amba/NicExample3.sv") -} - -class ApbVerilogIOSpec extends ChipmunkFlatSpec { - "ApbIORtlConnector" should "generate blackbox-friendly APB interfaces with specific prefix naming" in { - simulate(new Module { - val io = IO(new Bundle { - val s0 = Slave(new Apb4IO(dataWidth = 32, addrWidth = 32, hasProt = true, hasStrb = true)) - val m0 = Master(new Apb3IO(dataWidth = 32, addrWidth = 32)) - }) - val uNic = Module(new NicExample3Bbox(32, 32)) - uNic.clock := clock - uNic.resetn := !reset.asBool - val s0View = uNic.s0.viewAs[Apb4IO] - val m0View = uNic.m0.viewAs[Apb3IO] - io.s0 <> s0View - io.m0 <> m0View - }) { dut => - dut.io.s0.addr.randomize() - dut.io.s0.wdata.randomize() - dut.io.m0.addr expect dut.io.s0.addr.peek() - dut.io.s0.wdata expect dut.io.m0.wdata.peek() - } - } -} diff --git a/chipmunk/test/src/amba/AxiLiteVerilogIOSpec.scala b/chipmunk/test/src/amba/AxiLiteVerilogIOSpec.scala deleted file mode 100644 index 3e62c37..0000000 --- a/chipmunk/test/src/amba/AxiLiteVerilogIOSpec.scala +++ /dev/null @@ -1,50 +0,0 @@ -package chipmunk.test -package amba - -import chipmunk._ -import chipmunk.amba._ -import chisel3._ -import chisel3.experimental.ExtModule -import chisel3.util.HasExtModuleResource -import chisel3.experimental.dataview._ - -class NicExample2Bbox(dw: Int = 32, aw: Int = 32) - extends ExtModule(Map("S00_DW" -> dw, "S00_AW" -> aw, "M00_DW" -> dw, "M00_AW" -> aw)) - with HasExtModuleResource { - val clock = IO(Input(Clock())) - val resetn = IO(Input(Reset())) - - val s0 = - IO(Slave(new Axi4LiteIO(dataWidth = dw, addrWidth = aw).createVerilogIO(Seq(PortNameTransform.toggleCase)))) - .suggestName("s00_axi") - val m0 = - IO(Master(new Axi4LiteIO(dataWidth = dw, addrWidth = aw).createVerilogIO(Seq(PortNameTransform.toggleCase)))) - .suggestName("m00_axi") - - override def desiredName = "NicExample2" - addResource("amba/NicExample2.sv") -} - -class AxiLiteVerilogIOSpec extends ChipmunkFlatSpec { - "AxiLiteIORtlConnector" should "generate blackbox-friendly AXI-Lite interfaces with specific prefix naming" in { - simulate(new Module { - val io = IO(new Bundle { - val s0 = Slave(new Axi4LiteIO(dataWidth = 32, addrWidth = 32)) - val m0 = Master(new Axi4LiteIO(dataWidth = 32, addrWidth = 32)) - }) - val uNic = Module(new NicExample2Bbox(32, 32)) - uNic.clock := clock - uNic.resetn := !reset.asBool - val s0View = uNic.s0.viewAs[Axi4LiteIO] - val m0View = uNic.m0.viewAs[Axi4LiteIO] - io.s0 <> s0View - io.m0 <> m0View - }) { dut => - dut.io.s0.aw.bits.addr.randomize() - dut.io.s0.aw.bits.prot.randomize() - dut.io.m0.aw.ready #= true.B - dut.io.m0.aw.bits.addr expect dut.io.s0.aw.bits.addr.peek() - dut.io.s0.aw.ready expect dut.io.m0.aw.ready.peek() - } - } -} diff --git a/chipmunk/test/src/amba/AxiVerilogIOSpec.scala b/chipmunk/test/src/amba/AxiVerilogIOSpec.scala deleted file mode 100644 index 261b67d..0000000 --- a/chipmunk/test/src/amba/AxiVerilogIOSpec.scala +++ /dev/null @@ -1,91 +0,0 @@ -package chipmunk.test -package amba - -import chipmunk._ -import chipmunk.amba._ -import chisel3._ -import chisel3.experimental.ExtModule -import chisel3.util.HasExtModuleResource -import chisel3.experimental.dataview._ - -class NicExample0Bbox extends ExtModule with HasExtModuleResource { - val clock = IO(Input(Clock())).suggestName("ACLOCK") - val resetn = IO(Input(Reset())).suggestName("ARESETN") - val s0 = FlatIO( - Slave( - new Axi4IO(dataWidth = 128, addrWidth = 32, idWidth = 2) - .createVerilogIO(Seq(PortNameTransform.stringPostfix("_s_axi_spi"))) - ) - ) - val m0 = - FlatIO( - Master( - new Axi4VerilogIO(dataWidth = 128, addrWidth = 32, idWidth = 2)( - Seq(PortNameTransform.stringPostfix("_m_axi_sram")) - ) - ) - ) - - override def desiredName = "NicExample0" - addResource("amba/NicExample0.sv") -} - -class NicExample1Bbox extends ExtModule with HasExtModuleResource { - val clock = IO(Input(Clock())) - val resetn = IO(Input(Reset())) - - val s0 = IO(Slave(new Axi4VerilogIO(dataWidth = 128, addrWidth = 32, idWidth = 2)())).suggestName("s_axi_spi") - val m0 = IO(Master(new Axi4VerilogIO(dataWidth = 128, addrWidth = 32, idWidth = 2, hasRegion = true)())) - .suggestName("m_axi_sram") - - override def desiredName = "NicExample1" - addResource("amba/NicExample1.sv") -} - -class AxiVerilogIOSpec extends ChipmunkFlatSpec { - "AxiIORtlConnector" should "generate blackbox-friendly AXI interfaces with specific postfix naming" in { - simulate(new Module { - val io = IO(new Bundle { - val s0 = Slave(new Axi4IO(dataWidth = 32, addrWidth = 32, idWidth = 2)) - val m0 = Master(new Axi4IO(dataWidth = 32, addrWidth = 32, idWidth = 2)) - }) - val uNic = Module(new NicExample0Bbox) - uNic.clock := clock - uNic.resetn := !reset.asBool - val s0View = uNic.s0.viewAs[Axi4IO] - val m0View = uNic.m0.viewAs[Axi4IO] - io.s0 <> s0View - io.m0 <> m0View - }) { dut => - dut.io.s0.aw.bits.id.get.randomize() - dut.io.s0.aw.bits.addr.randomize() - dut.io.m0.aw.ready #= true.B - dut.io.m0.aw.bits.addr expect dut.io.s0.aw.bits.addr.peek() - dut.io.m0.aw.bits.id.get expect dut.io.s0.aw.bits.id.get.peek() - dut.io.s0.aw.ready expect dut.io.m0.aw.ready.peek() - } - } - - it should "generate blackbox-friendly AXI interfaces with specific prefix naming" in { - simulate(new Module { - val io = IO(new Bundle { - val s0 = Slave(new Axi4IO(dataWidth = 32, addrWidth = 32, idWidth = 2)) - val m0 = Master(new Axi4IO(dataWidth = 32, addrWidth = 32, idWidth = 2, hasRegion = true)) - }) - val uNic = Module(new NicExample1Bbox) - uNic.clock := clock - uNic.resetn := !reset.asBool - val s0View = uNic.s0.viewAs[Axi4IO] - val m0View = uNic.m0.viewAs[Axi4IO] - io.s0 <> s0View - io.m0 <> m0View - }) { dut => - dut.io.s0.aw.bits.id.get.randomize() - dut.io.s0.aw.bits.addr.randomize() - dut.io.m0.aw.ready #= true.B - dut.io.m0.aw.bits.addr expect dut.io.s0.aw.bits.addr.peek() - dut.io.m0.aw.bits.id.get expect dut.io.s0.aw.bits.id.get.peek() - dut.io.s0.aw.ready expect dut.io.m0.aw.ready.peek() - } - } -} diff --git a/chipmunk/test/src/stream/StreamArbiterSpec.scala b/chipmunk/test/src/stream/StreamArbiterSpec.scala deleted file mode 100644 index bd3b426..0000000 --- a/chipmunk/test/src/stream/StreamArbiterSpec.scala +++ /dev/null @@ -1,52 +0,0 @@ -package chipmunk.test -package stream - -import chipmunk._ -import chipmunk.stream._ -import chisel3._ - -class StreamArbiterSpec extends ChipmunkFlatSpec { - "StreamArbiter" should "decide which input stream to fire" in { - simulate(new Module { - val io = IO(new Bundle { - val in0 = Slave(Flow(UInt(8.W))) - val in1 = Slave(Flow(UInt(8.W))) - val outRR = Master(Stream(UInt(8.W))) - val outLF = Master(Stream(UInt(8.W))) - }) - io.outRR << StreamArbiter.roundRobin(Seq(io.in0.asStream, io.in1.asStream)) - io.outLF << StreamArbiter.lowerFirst(Seq(io.in0.asStream, io.in1.asStream)) - }) { dut => - dut.reset #= true.B - dut.io.outRR.ready #= true.B - dut.io.outLF.ready #= true.B - dut.io.in0.valid #= false.B - dut.io.in1.valid #= false.B - dut.clock.step() - dut.reset #= false.B - dut.io.in0.valid #= true.B - dut.io.in0.bits #= 10.U - dut.io.in1.valid #= true.B - dut.io.in1.bits #= 20.U - dut.io.outRR.valid expect true.B - dut.io.outRR.bits expect 20.U - dut.io.outLF.valid expect true.B - dut.io.outLF.bits expect 10.U - dut.clock.step() - dut.io.in0.bits #= 11.U - dut.io.in1.bits #= 21.U - dut.io.outRR.bits expect 11.U - dut.io.outLF.bits expect 11.U - dut.clock.step() - dut.io.in0.bits #= 12.U - dut.io.in1.bits #= 22.U - dut.io.outRR.bits expect 22.U - dut.io.outLF.bits expect 12.U - dut.clock.step() - dut.io.in0.bits #= 13.U - dut.io.in1.bits #= 23.U - dut.io.outRR.bits expect 13.U - dut.io.outLF.bits expect 13.U - } - } -} diff --git a/chipmunk/test/src/stream/StreamDelaySpec.scala b/chipmunk/test/src/stream/StreamDelaySpec.scala deleted file mode 100644 index 71c76e2..0000000 --- a/chipmunk/test/src/stream/StreamDelaySpec.scala +++ /dev/null @@ -1,98 +0,0 @@ -package chipmunk.test -package stream - -import chipmunk._ -import chipmunk.stream._ -import chisel3._ - -class StreamDelaySpec extends ChipmunkFlatSpec { - "StreamDelay" should "delay the input stream by fixed cycles" in { - simulate(new Module { - val io = IO(new Bundle { - val in = Slave(Stream(UInt(8.W))) - val out = Master(Stream(UInt(8.W))) - }) - io.out << io.in.delayFixed(cycles = 2) - }) { dut => - // inValid _ 1 1 1 _ - // inReady _ _ _ 1 _ - // outValid _ _ _ 1 _ - // outReady 1 1 1 1 1 - dut.io.in.valid #= false.B - dut.io.in.bits #= 0.U - dut.io.out.ready #= true.B - dut.clock.step() - dut.io.in.valid #= true.B - dut.io.in.bits #= 42.U - dut.clock.step(2) - dut.io.out.valid expect true - dut.io.out.bits expect 42.U - dut.io.in.valid #= false.B - dut.io.in.bits #= 0.U - dut.clock.step() - dut.io.out.valid expect false - } - } - - it should "have no stall when delay cycle is 0" in { - simulate(new Module { - val io = IO(new Bundle { - val in = Slave(Stream(UInt(8.W))) - val out = Master(Stream(UInt(8.W))) - }) - io.out << io.in.delayFixed(cycles = 0) - }) { dut => - for (_ <- 0 until 10) { - dut.io.in.valid.randomize() - dut.io.in.bits.randomize() - dut.io.out.ready #= true.B - if (dut.io.in.valid.peekBoolean()) { - dut.io.out.valid expect true - dut.io.in.ready expect true - dut.io.out.bits expect dut.io.in.bits.peek() - } - dut.clock.step() - } - } - } - - it should "delay the input stream by random cycles" in { - simulate(new Module { - val io = IO(new Bundle { - val in = Slave(Stream(UInt(8.W))) - val out = Master(Stream(UInt(8.W))) - }) - io.out << io.in.delayRandom(minCycles = 2, maxCycles = 8) - }) { dut => - dut.io.in.valid #= false.B - dut.io.in.bits #= 0.U - dut.io.out.ready #= true.B - dut.clock.step() - for (i <- 0 until 10) { - dut.io.in.valid #= true.B - dut.io.in.bits #= i.U - - // The first 2 cycles should have no valid output - for (_ <- 0 until 2) { - dut.io.out.valid expect false - dut.clock.step() - } - - // The next 7 cycles should have one valid output - var outValids: List[Boolean] = List() - for (_ <- 0 until 7) { - val outValid: Boolean = dut.io.out.valid.peekBoolean() - outValids = outValids :+ outValid - if (outValid) { - dut.io.out.bits expect i.U - dut.io.in.valid #= false.B - dut.io.in.bits #= 0.U - } - dut.clock.step() - } - assert(outValids.count(_ == true) == 1) // outValids is one-hot and has only one true - dut.clock.step() - } - } - } -} diff --git a/chipmunk/test/src/stream/StreamIOSpec.scala b/chipmunk/test/src/stream/StreamIOSpec.scala deleted file mode 100644 index a11a9a4..0000000 --- a/chipmunk/test/src/stream/StreamIOSpec.scala +++ /dev/null @@ -1,24 +0,0 @@ -package chipmunk.test -package stream - -import chipmunk._ -import chipmunk.stream._ -import chisel3._ -import chisel3.util.DecoupledIO - -class StreamIOSpec extends ChipmunkFlatSpec { - "Stream" should "be created from DecoupledIO or Chisel types" in { - simulate(new Module { - val io = IO(new Bundle { - val in = Flipped(DecoupledIO(UInt(8.W))) - val out = Master(Stream(UInt(8.W))) // created from Chisel types - }) - val inStream = io.in.toStream // created from DecoupledIO - val outStream = Wire(Stream(chiselTypeOf(io.in.bits))) - outStream connectFrom inStream - io.out <> outStream - }) { dut => - dut.clock.step() - } - } -} diff --git a/mylib/src/RtlEmitter.scala b/mylib/src/RtlEmitter.scala index 6b6b231..a2e06cb 100644 --- a/mylib/src/RtlEmitter.scala +++ b/mylib/src/RtlEmitter.scala @@ -12,21 +12,21 @@ class MyIncrement extends Module { io.sink := RegNext(io.source + 1.U, init = 0.U) } -class MyChipTop extends RawModule { - val coreClock = IO(Input(Clock())) - val coreReset = IO(Input(AsyncReset())) - val coreSource = IO(Input(SInt(3.W))) - val coreSink = IO(Output(UInt(3.W))) - - implicit val clockSys: Clock = coreClock - implicit val resetSys: AsyncReset = AsyncResetSyncDessert.withSpecificClockDomain(clockSys, coreReset) - - withClockAndReset(clockSys, resetSys) { - val uIncrement = Module(new MyIncrement) - uIncrement.io.source <> coreSource.asUInt - uIncrement.io.sink <> coreSink - } -} +// class MyChipTop extends RawModule { +// val coreClock = IO(Input(Clock())) +// val coreReset = IO(Input(AsyncReset())) +// val coreSource = IO(Input(SInt(3.W))) +// val coreSink = IO(Output(UInt(3.W))) + +// implicit val clockSys: Clock = coreClock +// implicit val resetSys: AsyncReset = AsyncResetSyncDessert.withSpecificClockDomain(clockSys, coreReset) + +// withClockAndReset(clockSys, resetSys) { +// val uIncrement = Module(new MyIncrement) +// uIncrement.io.source <> coreSource.asUInt +// uIncrement.io.sink <> coreSink +// } +// } object RtlEmitter extends App { val targetDir = "generate/hw" @@ -36,6 +36,6 @@ object RtlEmitter extends App { Array("--disable-all-randomization", "-repl-seq-mem", f"-repl-seq-mem-file=seq-mem.conf") ChiselStage - .emitSystemVerilogFile(new MyChipTop, chiselArgs, firtoolOpts) + .emitSystemVerilogFile(new MyIncrement, chiselArgs, firtoolOpts) println(f">>> RTL emitted in \"$targetDir\" directory.") } From 7aca18f81fc6c82a0e3e8140f1048563dd583b9a Mon Sep 17 00:00:00 2001 From: zhutmost Date: Wed, 23 Sep 2026 23:39:34 +0800 Subject: [PATCH 02/26] feat: add trace & multi-clock support for simulation --- .../resources/regneg/Chipmunk_RegNegInit.sv | 2 +- chipmunk/src/tester/MultiClockSupport.scala | 129 ++++++++++++++++++ chipmunk/src/tester/TesterAPI.scala | 36 +++++ chipmunk/src/tester/TraceSupport.scala | 76 +++++++++++ chipmunk/test/src/ChipmunkSim.scala | 47 +++++++ chipmunk/test/src/MultiClockSpec.scala | 61 +++++++++ chipmunk/test/src/RegNegSpec.scala | 117 ++++++++++++++++ chipmunk/test/src/TraceSpec.scala | 52 +++++++ 8 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 chipmunk/src/tester/MultiClockSupport.scala create mode 100644 chipmunk/src/tester/TesterAPI.scala create mode 100644 chipmunk/src/tester/TraceSupport.scala create mode 100644 chipmunk/test/src/ChipmunkSim.scala create mode 100644 chipmunk/test/src/MultiClockSpec.scala create mode 100644 chipmunk/test/src/RegNegSpec.scala create mode 100644 chipmunk/test/src/TraceSpec.scala diff --git a/chipmunk/resources/regneg/Chipmunk_RegNegInit.sv b/chipmunk/resources/regneg/Chipmunk_RegNegInit.sv index 500e604..d896cc9 100644 --- a/chipmunk/resources/regneg/Chipmunk_RegNegInit.sv +++ b/chipmunk/resources/regneg/Chipmunk_RegNegInit.sv @@ -2,7 +2,7 @@ module Chipmunk_RegNegInit #( parameter int WIDTH = 1, - parameter bit RESET_ASYNC = 1'b1 + parameter bit RESET_ASYNC = 1'b1, parameter logic [WIDTH-1:0] INIT = '0 )( input var logic clock, diff --git a/chipmunk/src/tester/MultiClockSupport.scala b/chipmunk/src/tester/MultiClockSupport.scala new file mode 100644 index 0000000..f5843ef --- /dev/null +++ b/chipmunk/src/tester/MultiClockSupport.scala @@ -0,0 +1,129 @@ +package chipmunk +package tester + +import chisel3.{Clock, UInt} +import chisel3.simulator.PeekPokeAPI + +object MultiClockSupport: + + /** Edge direction produced by a simulated clock transition. */ + enum Edge: + case Rising, Falling + + /** Configuration of one clock domain. + * + * All clocks start low. The first rising edge occurs after `phase + halfPeriod` logical ticks; subsequent edges are + * separated by `halfPeriod` ticks. + * + * @param name + * stable name used in returned events + * @param bit + * bit position in the packed clock-level input + * @param halfPeriod + * number of logical ticks between adjacent edges + * @param phase + * additional delay before this domain starts + */ + final case class ClockDomain(name: String, bit: Int, halfPeriod: Int, phase: Int = 0): + require(name.nonEmpty, "clock-domain name must not be empty") + require(bit >= 0, s"clock-domain bit must be non-negative, got $bit") + require(halfPeriod > 0, s"clock-domain half-period must be positive, got $halfPeriod") + require(phase >= 0, s"clock-domain phase must be non-negative, got $phase") + + /** One clock transition within a scheduler event. */ + final case class Transition(domain: ClockDomain, edge: Edge) + + /** All clock transitions occurring at one logical timestamp. */ + final case class ClockEvent(time: Int, transitions: Seq[Transition], levels: BigInt): + def rising: Seq[Transition] = transitions.filter(_.edge == Edge.Rising) + def falling: Seq[Transition] = transitions.filter(_.edge == Edge.Falling) + +/** Deterministic multi-clock support for ChiselSim. + * + * A harness exposes all DUT clock levels as one packed `UInt` input and converts individual bits to clocks with + * `.asClock`. Updating the packed value makes edges at the same timestamp visible to the simulator in one evaluation. + */ +trait MultiClockSupport: + this: PeekPokeAPI => + + import MultiClockSupport.* + + /** Create a deterministic scheduler for clocks represented by `clockLevels`. + * + * `timebaseClock` is used only to advance simulator time. It must not clock state in the DUT. One logical scheduler + * tick corresponds to two simulator timesteps; this allows every positive logical interval to be represented by a + * legal ChiselSim clock period. + */ + final def multiClockScheduler( + clockLevels: UInt, + timebaseClock: Clock, + domains: Seq[ClockDomain] + ): MultiClockScheduler = new MultiClockScheduler(clockLevels, timebaseClock, domains) + + final class MultiClockScheduler private[tester] (clockLevels: UInt, timebaseClock: Clock, domains: Seq[ClockDomain]): + + require(domains.nonEmpty, "at least one clock domain is required") + require(domains.map(_.name).distinct.size == domains.size, "clock-domain names must be unique") + require(domains.map(_.bit).distinct.size == domains.size, "clock-domain bit positions must be unique") + require( + domains.map(_.bit).max < clockLevels.getWidth, + s"clock-domain bit does not fit in ${clockLevels.getWidth}-bit clock-level port" + ) + + private final class DomainState(val domain: ClockDomain): + var high: Boolean = false + var nextEdge: Int = Math.addExact(domain.phase, domain.halfPeriod) + + private val states = domains.sortBy(_.bit).map(domain => new DomainState(domain)) + private var initialized = false + private var logicalTime = 0 + private var packedLevels = BigInt(0) + + /** Current logical timestamp. */ + def time: Int = logicalTime + + /** Current packed clock levels. */ + def levels: BigInt = packedLevels + + /** Timestamp of the next scheduled clock event. */ + def nextEventTime: Int = states.map(_.nextEdge).min + + /** Drive every managed clock low and evaluate the harness without advancing time. */ + def initialize(): Unit = + if !initialized then + toTestableUInt(clockLevels).poke(packedLevels) + toTestableClock(timebaseClock).step(0) + initialized = true + + /** Advance to and apply the next set of simultaneous clock edges. */ + def advance(): ClockEvent = + initialize() + + val eventTime = nextEventTime + val elapsed = Math.subtractExact(eventTime, logicalTime) + require(elapsed >= 0, "multi-clock scheduler moved backwards") + + if elapsed > 0 then + val period = Math.multiplyExact(elapsed, 2) + toTestableClock(timebaseClock).step(cycles = 1, period = period) + logicalTime = eventTime + + val activeStates = states.filter(_.nextEdge == eventTime) + val transitions = activeStates.map { state => + state.high = !state.high + packedLevels = + if state.high then packedLevels.setBit(state.domain.bit) + else packedLevels.clearBit(state.domain.bit) + state.nextEdge = Math.addExact(state.nextEdge, state.domain.halfPeriod) + Transition(state.domain, if state.high then Edge.Rising else Edge.Falling) + } + + toTestableUInt(clockLevels).poke(packedLevels) + toTestableClock(timebaseClock).step(0) + + ClockEvent(logicalTime, transitions, packedLevels) + + /** Process exactly `eventCount` scheduler timestamps. */ + def run(eventCount: Int)(observe: ClockEvent => Unit = _ => ()): Unit = + require(eventCount >= 0, s"event count must be non-negative, got $eventCount") + for _ <- 0 until eventCount do observe(advance()) diff --git a/chipmunk/src/tester/TesterAPI.scala b/chipmunk/src/tester/TesterAPI.scala new file mode 100644 index 0000000..d8b97fe --- /dev/null +++ b/chipmunk/src/tester/TesterAPI.scala @@ -0,0 +1,36 @@ +package chipmunk +package tester + +import chisel3.* +import chisel3.simulator.* + +/** Small convenience extensions built on top of ChiselSim's peek/poke API. + */ +trait TesterAPI: + this: PeekPokeAPI => + + extension [T <: Data](target: T) + + /** Assign a Chisel literal to a DUT port. */ + def #=(value: T): Unit = + (target, value) match + case (reset: AsyncReset, literal: Reset) => toTestableReset(reset).poke(literal) + case _ => toTestableData(target).poke(value) + + extension (bool: Bool) + + /** Assign a random value to a `Bool` port. */ + def randomize(): Unit = toTestableBool(bool).poke(scala.util.Random.nextBoolean()) + + extension (uint: UInt) + + /** Assign a uniformly distributed random bit pattern to a `UInt` port. */ + def randomize(): Unit = toTestableUInt(uint).poke(BigInt(uint.getWidth, scala.util.Random)) + + extension (sint: SInt) + + /** Assign a uniformly distributed two's-complement bit pattern to a `SInt` port. */ + def randomize(): Unit = + require(sint.getWidth > 0, "cannot randomize a zero-width SInt") + val value = BigInt(sint.getWidth, scala.util.Random) - (BigInt(1) << (sint.getWidth - 1)) + toTestableSInt(sint).poke(value) diff --git a/chipmunk/src/tester/TraceSupport.scala b/chipmunk/src/tester/TraceSupport.scala new file mode 100644 index 0000000..bfe9521 --- /dev/null +++ b/chipmunk/src/tester/TraceSupport.scala @@ -0,0 +1,76 @@ +package chipmunk +package tester + +import chisel3.simulator.ControlAPI +import chisel3.testing.HasTestingDirectory + +import java.nio.file.{Files, Path} +import scala.jdk.CollectionConverters.* + +trait TraceSupport: + this: ControlAPI => + + protected def fstTraceStyle: svsim.verilator.Backend.CompilationSettings.TraceStyle = + svsim.verilator.Backend.CompilationSettings + .TraceStyle(kind = svsim.verilator.Backend.CompilationSettings.TraceKind.Fst()) + + final def withWaves[A](body: => A): A = + enableWaves() + var primaryFailure = Option.empty[Throwable] + + try body + catch + case error: Throwable => + primaryFailure = Some(error) + throw error + finally + try disableWaves() + catch + case error: Throwable => + primaryFailure match + case Some(primary) => primary.addSuppressed(error) + case None => throw error + +/** Backend-neutral access to files produced for the current ScalaTest test. */ +object SimulationArtifacts: + + private val traceFileExtensions = Set("fst", "vcd", "vpd", "fsdb") + private val logFileNames = Set("compilation-log.txt", "simulation-log.txt") + private val replayFileNames = Set("execution-script.txt") + + /** Root directory assigned by ChiselSim to the current ScalaTest test. */ + def testDirectory(using testingDirectory: HasTestingDirectory): Path = testingDirectory.getDirectory + + /** Waveform files below the current test directory, independent of simulator backend. */ + def traceFiles(using testingDirectory: HasTestingDirectory): Seq[Path] = + findFiles(path => + val fileName = path.getFileName.toString + traceFileExtensions.exists(extension => fileName.endsWith(s".$extension")) + ) + + /** Compilation and simulation logs below the current test directory. */ + def logFiles(using testingDirectory: HasTestingDirectory): Seq[Path] = + findFiles(path => logFileNames.contains(path.getFileName.toString)) + + /** Scripts that reproduce individual simulator invocations. */ + def replayScripts(using testingDirectory: HasTestingDirectory): Seq[Path] = + findFiles(path => replayFileNames.contains(path.getFileName.toString)) + + /** All waveform and diagnostic artifacts normally useful after a test failure. */ + def debugFiles(using testingDirectory: HasTestingDirectory): Seq[Path] = + (traceFiles ++ logFiles ++ replayScripts).sortBy(_.toString) + + private def findFiles(select: Path => Boolean)(using testingDirectory: HasTestingDirectory): Seq[Path] = + val root = testDirectory + if !Files.isDirectory(root) then Seq.empty + else + val paths = Files.walk(root) + try + paths + .iterator() + .asScala + .filter(path => Files.isRegularFile(path)) + .filter(select) + .toVector + .sortBy(_.toString) + finally paths.close() diff --git a/chipmunk/test/src/ChipmunkSim.scala b/chipmunk/test/src/ChipmunkSim.scala new file mode 100644 index 0000000..68e04fd --- /dev/null +++ b/chipmunk/test/src/ChipmunkSim.scala @@ -0,0 +1,47 @@ +package chipmunk.test + +import chipmunk.tester.{MultiClockSupport, TesterAPI, TraceSupport} +import chisel3.simulator.scalatest.{ChiselSim, Cli} +import chisel3.simulator.scalatest.HasCliOptions.CliOption +import org.scalatest.TestSuite +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers + +trait ChipmunkSim + extends ChiselSim + with Cli.Simulator + with Cli.EmitFsdb + with Cli.EmitVpd + with TesterAPI + with TraceSupport + with MultiClockSupport: + this: TestSuite => + + addOption( + CliOption.flag( + name = "emitFst", + help = "compile Verilator with FST waveform support and start dumping waves at time zero", + updateCommonSettings = options => + options.copy(simulationSettings = options.simulationSettings.copy(enableWavesAtTimeZero = true)), + updateBackendSettings = { + case options: svsim.verilator.Backend.CompilationSettings => + options.withTraceStyle(Some(fstTraceStyle)) + + case _: svsim.vcs.Backend.CompilationSettings => + throw new IllegalArgumentException( + "VCS does not support FST waveforms; use -DemitFsdb=1, -DemitVpd=1, or -DemitVcd=1." + ) + + case other => + throw new IllegalArgumentException(s"${other.getClass.getName} does not support Chipmunk FST configuration.") + } + ) + ) + +abstract class ChipmunkFlatSpec extends AnyFlatSpec with Matchers with ChipmunkSim + +abstract class ChipmunkFreeSpec extends AnyFreeSpec with Matchers with ChipmunkSim + +abstract class ChipmunkFunSpec extends AnyFunSpec with Matchers with ChipmunkSim diff --git a/chipmunk/test/src/MultiClockSpec.scala b/chipmunk/test/src/MultiClockSpec.scala new file mode 100644 index 0000000..44dff4f --- /dev/null +++ b/chipmunk/test/src/MultiClockSpec.scala @@ -0,0 +1,61 @@ +package chipmunk.test + +import chipmunk.tester.MultiClockSupport.{ClockDomain, ClockEvent, Edge} +import chisel3.* + +import scala.collection.mutable.ArrayBuffer + +private object MultiClockSpecDut: + + final class Harness extends Module: + val io = IO(new Bundle: + val clockLevels = Input(UInt(2.W)) + val fastCount = Output(UInt(8.W)) + val slowCount = Output(UInt(8.W))) + + io.fastCount := counter(io.clockLevels(0).asClock) + io.slowCount := counter(io.clockLevels(1).asClock) + + private def counter(domainClock: Clock): UInt = + withClockAndReset(domainClock, reset.asAsyncReset): + val value = RegInit(0.U(8.W)) + value := value + 1.U + value + +class MultiClockSpec extends ChipmunkFlatSpec: + + "MultiClockSupport" should "schedule different frequencies and coalesce simultaneous edges deterministically" in: + simulate(new MultiClockSpecDut.Harness): dut => + val fast = ClockDomain(name = "fast", bit = 0, halfPeriod = 2) + val slow = ClockDomain(name = "slow", bit = 1, halfPeriod = 3, phase = 1) + val scheduler = multiClockScheduler(dut.io.clockLevels, dut.clock, Seq(slow, fast)) + + scheduler.initialize() + dut.io.fastCount.expect(0.U) + dut.io.slowCount.expect(0.U) + + val expectedCounts = Seq((1, 0), (1, 1), (2, 1), (2, 1), (2, 1), (3, 2)) + val events = ArrayBuffer.empty[ClockEvent] + scheduler.run(6): event => + val (fastCount, slowCount) = expectedCounts(events.size) + events.addOne(event) + dut.io.clockLevels.expect(event.levels) + dut.io.fastCount.expect(fastCount.U) + dut.io.slowCount.expect(slowCount.U) + () + + events.map(_.time).toSeq shouldBe Seq(2, 4, 6, 7, 8, 10) + events.map(_.levels).toSeq shouldBe Seq(1, 2, 3, 1, 0, 3).map(value => BigInt(value)) + + events(1).transitions.map(transition => transition.domain.name -> transition.edge) shouldBe + Seq("fast" -> Edge.Falling, "slow" -> Edge.Rising) + events(1).falling.map(_.domain.name) shouldBe Seq("fast") + events(1).rising.map(_.domain.name) shouldBe Seq("slow") + events(5).transitions.map(transition => transition.domain.name -> transition.edge) shouldBe + Seq("fast" -> Edge.Rising, "slow" -> Edge.Rising) + + scheduler.time shouldBe 10 + scheduler.levels shouldBe BigInt(3) + scheduler.nextEventTime shouldBe 12 + dut.io.fastCount.expect(3.U) + dut.io.slowCount.expect(2.U) diff --git a/chipmunk/test/src/RegNegSpec.scala b/chipmunk/test/src/RegNegSpec.scala new file mode 100644 index 0000000..362de8f --- /dev/null +++ b/chipmunk/test/src/RegNegSpec.scala @@ -0,0 +1,117 @@ +package chipmunk.test + +import chipmunk.{RegNegEnable, RegNegNext} +import chisel3.* + +private object RegNegSpecDut: + + final class NoResetHarness extends Module: + val io = IO(new Bundle: + val clockLevels = Input(UInt(1.W)) + val data = Input(UInt(8.W)) + val enable = Input(Bool()) + val nextValue = Output(UInt(8.W)) + val enabled = Output(UInt(8.W))) + + io.nextValue := withClock(io.clockLevels(0).asClock): + RegNegNext(io.data) + io.enabled := withClock(io.clockLevels(0).asClock): + RegNegEnable(io.data, io.enable) + + final class AsyncResetHarness extends Module: + val io = IO(new Bundle: + val clockLevels = Input(UInt(1.W)) + val domainReset = Input(Bool()) + val data = Input(UInt(8.W)) + val output = Output(UInt(8.W))) + + io.output := withClockAndReset(io.clockLevels(0).asClock, io.domainReset.asAsyncReset): + RegNegNext(io.data, 0xa5.U(8.W), isResetAsync = true) + + final class SyncResetHarness extends Module: + val io = IO(new Bundle: + val clockLevels = Input(UInt(1.W)) + val domainReset = Input(Bool()) + val data = Input(UInt(8.W)) + val output = Output(UInt(8.W))) + + io.output := withClockAndReset(io.clockLevels(0).asClock, io.domainReset): + RegNegNext(io.data, 0xa5.U(8.W), isResetAsync = false) + +class RegNegSpec extends ChipmunkFlatSpec: + + private def driveClock(clockLevels: UInt, timebaseClock: Clock, high: Boolean): Unit = + clockLevels #= (if high then 1.U else 0.U) + timebaseClock.step(0) + + "RegNegNext and RegNegEnable" should "capture only on falling edges and honor enable" in: + simulate(new RegNegSpecDut.NoResetHarness): dut => + dut.io.data #= 0x12.U + dut.io.enable #= true.B + driveClock(dut.io.clockLevels, dut.clock, high = false) + + driveClock(dut.io.clockLevels, dut.clock, high = true) + driveClock(dut.io.clockLevels, dut.clock, high = false) + dut.io.nextValue.expect(0x12.U) + dut.io.enabled.expect(0x12.U) + + dut.io.data #= 0x34.U + driveClock(dut.io.clockLevels, dut.clock, high = true) + dut.io.nextValue.expect(0x12.U) + dut.io.enabled.expect(0x12.U) + driveClock(dut.io.clockLevels, dut.clock, high = false) + dut.io.nextValue.expect(0x34.U) + dut.io.enabled.expect(0x34.U) + + dut.io.enable #= false.B + dut.io.data #= 0x56.U + driveClock(dut.io.clockLevels, dut.clock, high = true) + driveClock(dut.io.clockLevels, dut.clock, high = false) + dut.io.nextValue.expect(0x56.U) + dut.io.enabled.expect(0x34.U) + + it should "apply asynchronous reset without waiting for a clock edge" in: + simulate(new RegNegSpecDut.AsyncResetHarness): dut => + dut.io.domainReset #= false.B + dut.io.data #= 0x31.U + driveClock(dut.io.clockLevels, dut.clock, high = false) + + driveClock(dut.io.clockLevels, dut.clock, high = true) + driveClock(dut.io.clockLevels, dut.clock, high = false) + dut.io.output.expect(0x31.U) + + dut.io.domainReset #= true.B + dut.clock.step(0) + dut.io.output.expect(0xa5.U) + + dut.io.domainReset #= false.B + dut.io.data #= 0x52.U + driveClock(dut.io.clockLevels, dut.clock, high = true) + dut.io.output.expect(0xa5.U) + driveClock(dut.io.clockLevels, dut.clock, high = false) + dut.io.output.expect(0x52.U) + + it should "apply synchronous reset on the next falling edge" in: + simulate(new RegNegSpecDut.SyncResetHarness): dut => + dut.io.domainReset #= false.B + dut.io.data #= 0x31.U + driveClock(dut.io.clockLevels, dut.clock, high = false) + + driveClock(dut.io.clockLevels, dut.clock, high = true) + driveClock(dut.io.clockLevels, dut.clock, high = false) + dut.io.output.expect(0x31.U) + + dut.io.domainReset #= true.B + dut.clock.step(0) + dut.io.output.expect(0x31.U) + + driveClock(dut.io.clockLevels, dut.clock, high = true) + dut.io.output.expect(0x31.U) + driveClock(dut.io.clockLevels, dut.clock, high = false) + dut.io.output.expect(0xa5.U) + + dut.io.domainReset #= false.B + dut.io.data #= 0x73.U + driveClock(dut.io.clockLevels, dut.clock, high = true) + driveClock(dut.io.clockLevels, dut.clock, high = false) + dut.io.output.expect(0x73.U) diff --git a/chipmunk/test/src/TraceSpec.scala b/chipmunk/test/src/TraceSpec.scala new file mode 100644 index 0000000..9ea9e88 --- /dev/null +++ b/chipmunk/test/src/TraceSpec.scala @@ -0,0 +1,52 @@ +package chipmunk.test + +import chipmunk.tester.SimulationArtifacts +import chisel3.* +import chisel3.simulator.HasSimulator + +import java.nio.file.Files + +private object TraceSpecDut: + final class Harness extends Module: + val io = IO(new Bundle: + val input = Input(UInt(8.W)) + val output = Output(UInt(8.W))) + + val sampled = RegInit(0.U(8.W)) + sampled := io.input + io.output := sampled + +class TraceSpec extends ChipmunkFlatSpec { + "TraceSupport" should "write a non-empty FST for an explicitly enabled waveform window" in { + given fstSimulator: HasSimulator = HasSimulator.simulators.verilator(verilatorSettings = + svsim.verilator.Backend.CompilationSettings.default.withTraceStyle(Some(fstTraceStyle)) + ) + + simulate(new TraceSpecDut.Harness, subdirectory = Some("fst-window")): dut => + dut.io.input #= 0x11.U + dut.clock.step() + dut.io.output.expect(0x11.U) + + val result = withWaves: + for value <- Seq(0x22, 0x33, 0x44) do + dut.io.input #= value.U + dut.clock.step() + dut.io.output.expect(value.U) + "window-complete" + + result shouldBe "window-complete" + + // Activity outside this block must remain legal after withWaves has disabled tracing. + dut.io.input #= 0x55.U + dut.clock.step() + dut.io.output.expect(0x55.U) + + val fstFiles = SimulationArtifacts.traceFiles.filter(_.getFileName.toString.endsWith(".fst")) + withClue(s"trace files: ${SimulationArtifacts.traceFiles.mkString(", ")}"): + fstFiles should not be empty + fstFiles.foreach(path => Files.size(path) should be > 1L) + + SimulationArtifacts.logFiles should not be empty + SimulationArtifacts.replayScripts should not be empty + } +} From 7dc97717962fd768141669a5e8d7cec7e5cf43a7 Mon Sep 17 00:00:00 2001 From: zhutmost Date: Wed, 30 Sep 2026 22:57:23 +0800 Subject: [PATCH 03/26] style: format code with scalafmt --- .scalafmt.conf | 41 ++- build.mill | 17 +- chipmunk/src/IsMasterSlave.scala | 2 +- chipmunk/src/Misc.scala | 2 +- chipmunk/src/RegNeg.scala | 10 +- chipmunk/src/tester/MultiClockSupport.scala | 277 +++++++++++++------- chipmunk/src/tester/TesterAPI.scala | 25 +- chipmunk/src/tester/TraceSupport.scala | 76 ------ chipmunk/test/src/ChipmunkSim.scala | 46 ++-- chipmunk/test/src/MultiClockSpec.scala | 105 +++++--- chipmunk/test/src/RegNegSpec.scala | 66 +++-- chipmunk/test/src/TraceSpec.scala | 87 +++--- mylib/src/RtlEmitter.scala | 3 +- 13 files changed, 447 insertions(+), 310 deletions(-) delete mode 100644 chipmunk/src/tester/TraceSupport.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index e9e04df..a65f881 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,14 +1,49 @@ version = "3.11.5" runner.dialect = "scala3" +encoding = "UTF-8" project.git = true preset=IntelliJ align.preset = more maxColumn = 120 +lineEndings = unix assumeStandardLibraryStripMargin = true -comments.wrap = standalone -comments.wrapSingleLineMlcAsSlc = true -comments.wrapStandaloneSlcAsSlc = true +danglingParentheses.preset = true +rewrite.trailingCommas.style = multiple + +comments { + wrap = standalone + wrapStandaloneSlcAsSlc = true + wrapSingleLineMlcAsSlc = true +} + +rewrite.rules = [ + Imports, + SortModifiers +] + +rewrite.sortModifiers.preset = styleGuide + +rewrite.imports { + selectors = fold + sort = scalastyle + groups = [ + ["java\\..*", "javax\\..*"], + ["scala\\..*"], + [ + "chisel3\\..*", + "circt\\..*", + "firrtl\\..*", + "mill\\..*", + "org\\..*", + "com\\..*", + "io\\..*" + ], + ["chipmunk\\..*"] + ] + sortCatchallGroup = tail + removeRedundantSelectors = true +} diff --git a/build.mill b/build.mill index a48cc6c..b88b082 100644 --- a/build.mill +++ b/build.mill @@ -6,7 +6,7 @@ import mill.scalalib.publish.* object Dependencies { val scalaVersion = "3.8.4" - val chiselVersion = "7.15.0+10-33fe25e2-SNAPSHOT" + val chiselVersion = "7.15.0+12-14b890ce-SNAPSHOT" val chisel = mvn"org.chipsalliance::chisel:$chiselVersion" val chiselPlugin = mvn"org.chipsalliance:::chisel-plugin:$chiselVersion" @@ -22,12 +22,13 @@ trait ChiselModule extends ScalaModule { "-deprecation", "-feature", "-unchecked", - "-Werror", + // "-Werror", "-explain-types", - "-Wsafe-init", "-Wunused:all", - "-Wimplausible-patterns" + "-Wimplausible-patterns", + // Scala3's indent-style blocks often cause scalafmt/syntax-highlight issues, so disable it + "-no-indent", ) override def repositories = @@ -36,9 +37,8 @@ trait ChiselModule extends ScalaModule { override def mvnDeps = super.mvnDeps() ++ Seq(Dependencies.chisel) override def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ Seq(Dependencies.chiselPlugin) - object test extends ScalaTests with TestModule.ScalaTest { + object test extends ScalaTests with TestModule.ScalaTest: override def mvnDeps = super.mvnDeps() ++ Seq(Dependencies.scalaTest) - } } object chipmunk extends ChiselModule with PublishModule { @@ -50,10 +50,9 @@ object chipmunk extends ChiselModule with PublishModule { url = "https://github.com/zhutmost/chipmunk", licenses = Seq(License.MIT), versionControl = VersionControl.github("zhutmost", "chipmunk"), - developers = Seq(Developer("zhutmost", "Haozhe Zhu", "https://github.com/zhutmost")) + developers = Seq(Developer("zhutmost", "Haozhe Zhu", "https://github.com/zhutmost")), ) } -object mylib extends ChiselModule { +object mylib extends ChiselModule: override def moduleDeps: Seq[ScalaModule] = Seq(chipmunk) -} diff --git a/chipmunk/src/IsMasterSlave.scala b/chipmunk/src/IsMasterSlave.scala index f03b6fb..7b9c7ad 100644 --- a/chipmunk/src/IsMasterSlave.scala +++ b/chipmunk/src/IsMasterSlave.scala @@ -39,7 +39,7 @@ private[chipmunk] object DirectedRecord { require(!original._roleWrapped, "The same Record cannot be wrapped twice with Master/Slave.") val directed: T = - if (original.isMaster == asMaster) original + if original.isMaster == asMaster then original else Flipped(original) // Flipped may return a clone. Mark both the supplied instance and diff --git a/chipmunk/src/Misc.scala b/chipmunk/src/Misc.scala index 75c5513..00fd78c 100644 --- a/chipmunk/src/Misc.scala +++ b/chipmunk/src/Misc.scala @@ -1,7 +1,7 @@ package chipmunk import chisel3.* -import chisel3.util.{PriorityMux} +import chisel3.util.PriorityMux /** A Bundle with no payload fields. Useful as a typed empty payload. */ final class EmptyBundle extends Bundle diff --git a/chipmunk/src/RegNeg.scala b/chipmunk/src/RegNeg.scala index 83fe9fc..cf5cbe6 100644 --- a/chipmunk/src/RegNeg.scala +++ b/chipmunk/src/RegNeg.scala @@ -39,14 +39,14 @@ private[chipmunk] final class RegNegInitBbox(bitwidth: Int, isResetAsync: Boolea extends ExtModule( Map( "WIDTH" -> IntParam(BigInt(bitwidth)), - "RESET_ASYNC" -> IntParam(if (isResetAsync) BigInt(1) else BigInt(0)), - "INIT" -> RawParam(s"${bitwidth}'h${initBits.toString(16)}") + "RESET_ASYNC" -> IntParam(if isResetAsync then BigInt(1) else BigInt(0)), + "INIT" -> RawParam(s"${bitwidth}'h${initBits.toString(16)}"), ) ) { require(bitwidth > 0, "RegNegInitBbox requires a positive data bitwidth.") require( initBits >= 0 && initBits.bitLength <= bitwidth, - "RegNegInitBbox.initBits must be an unsigned value fitting bitwidth." + "RegNegInitBbox.initBits must be an unsigned value fitting bitwidth.", ) val io = FlatIO(new Bundle { @@ -54,7 +54,7 @@ private[chipmunk] final class RegNegInitBbox(bitwidth: Int, isResetAsync: Boolea // Give the external port the same concrete reset type as the wrapper. val reset: Reset = - if (isResetAsync) Input(AsyncReset()) else Input(Bool()) + if isResetAsync then Input(AsyncReset()) else Input(Bool()) val en = Input(Bool()) val d = Input(UInt(bitwidth.W)) @@ -195,7 +195,7 @@ object RegNegEnable { require(next.isWidthKnown && next.getWidth > 0, "RegNegEnable.next must have a known, positive bitwidth.") require( init.isWidthKnown && init.getWidth == next.getWidth, - "RegNegEnable.init must have the same bitwidth as next." + "RegNegEnable.init must have the same bitwidth as next.", ) require(DataMirror.checkTypeEquivalence(next, init), "RegNegEnable.init must have the same Chisel type as next.") require(init.isLit, "RegNegEnable.init must be a Chisel literal.") diff --git a/chipmunk/src/tester/MultiClockSupport.scala b/chipmunk/src/tester/MultiClockSupport.scala index f5843ef..b2b0041 100644 --- a/chipmunk/src/tester/MultiClockSupport.scala +++ b/chipmunk/src/tester/MultiClockSupport.scala @@ -4,126 +4,217 @@ package tester import chisel3.{Clock, UInt} import chisel3.simulator.PeekPokeAPI -object MultiClockSupport: - - /** Edge direction produced by a simulated clock transition. */ - enum Edge: - case Rising, Falling - - /** Configuration of one clock domain. - * - * All clocks start low. The first rising edge occurs after `phase + halfPeriod` logical ticks; subsequent edges are - * separated by `halfPeriod` ticks. - * - * @param name - * stable name used in returned events - * @param bit - * bit position in the packed clock-level input - * @param halfPeriod - * number of logical ticks between adjacent edges - * @param phase - * additional delay before this domain starts - */ - final case class ClockDomain(name: String, bit: Int, halfPeriod: Int, phase: Int = 0): - require(name.nonEmpty, "clock-domain name must not be empty") - require(bit >= 0, s"clock-domain bit must be non-negative, got $bit") - require(halfPeriod > 0, s"clock-domain half-period must be positive, got $halfPeriod") - require(phase >= 0, s"clock-domain phase must be non-negative, got $phase") - - /** One clock transition within a scheduler event. */ - final case class Transition(domain: ClockDomain, edge: Edge) +/** Timing of one simulated clock. + * + * All clocks initially stay low. By default, the low phase lasts `periodTicks / 2` ticks, and the first rising edge + * occurs when that initial low phase ends. For an odd period, the high phase gets the extra tick. + * + * `-1` means "use the default" for `firstRiseAt` and `lowTicks`. The scheduler uses the resolved fields below. + * + * @param name + * stable name used to identify this clock in events + * @param periodTicks + * number of logical ticks between consecutive rising edges + * @param firstRiseAt + * timestamp of the first rising edge, or `-1` for the default + * @param lowTicks + * duration of each low phase, or `-1` for the default + */ +final case class ClockSpec(name: String, periodTicks: Long, firstRiseAt: Long = -1L, lowTicks: Long = -1L) { + require(name.nonEmpty, "clock name must not be empty") + require(periodTicks >= 2, s"clock period must be at least 2 ticks, got $periodTicks") + require( + lowTicks == -1L || (lowTicks > 0 && lowTicks < periodTicks), + s"low duration must be between 1 and ${periodTicks - 1}, got $lowTicks", + ) + require(firstRiseAt == -1L || firstRiseAt > 0, s"first rising edge must occur after time zero, got $firstRiseAt") + + /** Low-phase duration after resolving the optional default. */ + val effectiveLowTicks: Long = + if lowTicks == -1L then periodTicks / 2 else lowTicks + + /** High-phase duration. */ + val highTicks: Long = periodTicks - effectiveLowTicks + + /** First rising-edge timestamp after resolving the optional default. */ + val effectiveFirstRiseAt: Long = + if firstRiseAt == -1L then effectiveLowTicks + else firstRiseAt +} + +/** Direction of a simulated clock edge. */ +enum ClockEdge { + case Rising, Falling +} + +/** One clock edge within a scheduler event. */ +final case class ClockTransition(domain: ClockSpec, edge: ClockEdge) + +/** All clock edges occurring at one logical timestamp. + * + * `transitions` follows the order of the clock specifications supplied to the scheduler. + */ +final case class ClockEvent(time: Long, transitions: Vector[ClockTransition], levels: BigInt) { + def rising: Vector[ClockTransition] = + transitions.filter(_.edge == ClockEdge.Rising) - /** All clock transitions occurring at one logical timestamp. */ - final case class ClockEvent(time: Int, transitions: Seq[Transition], levels: BigInt): - def rising: Seq[Transition] = transitions.filter(_.edge == Edge.Rising) - def falling: Seq[Transition] = transitions.filter(_.edge == Edge.Falling) + def falling: Vector[ClockTransition] = + transitions.filter(_.edge == ClockEdge.Falling) +} -/** Deterministic multi-clock support for ChiselSim. +/** Deterministic multi-clock stimulus for ChiselSim. * - * A harness exposes all DUT clock levels as one packed `UInt` input and converts individual bits to clocks with - * `.asClock`. Updating the packed value makes edges at the same timestamp visible to the simulator in one evaluation. + * The test harness exposes clock levels as a packed `UInt` input and converts each bit to a DUT clock with `.asClock`. + * The first supplied clock uses bit 0, the second uses bit 1, and so on. */ -trait MultiClockSupport: +trait MultiClockSupport { this: PeekPokeAPI => - import MultiClockSupport.* - - /** Create a deterministic scheduler for clocks represented by `clockLevels`. + /** Drives the packed clock input according to the supplied clocks. + * + * `timebaseClock` only advances simulation time. It must not clock state in the DUT. One logical tick corresponds to + * two simulator timesteps. * - * `timebaseClock` is used only to advance simulator time. It must not clock state in the DUT. One logical scheduler - * tick corresponds to two simulator timesteps; this allows every positive logical interval to be represented by a - * legal ChiselSim clock period. + * The scheduler can be constructed inside a test that mixes in `MultiClockSupport`: + * + * {{{ + * val scheduler = new MultiClockScheduler( + * dut.io.clockLevels, + * dut.io.timebaseClock, + * Seq( + * ClockSpec("read", periodTicks = 4), + * ClockSpec("write", periodTicks = 6, firstRiseAt = 2, lowTicks = 2), + * ), + * ) + * }}} */ - final def multiClockScheduler( - clockLevels: UInt, - timebaseClock: Clock, - domains: Seq[ClockDomain] - ): MultiClockScheduler = new MultiClockScheduler(clockLevels, timebaseClock, domains) - - final class MultiClockScheduler private[tester] (clockLevels: UInt, timebaseClock: Clock, domains: Seq[ClockDomain]): + final class MultiClockScheduler(clockLevels: UInt, timebaseClock: Clock, domains: Seq[ClockSpec]) { + private val specs = domains.toVector - require(domains.nonEmpty, "at least one clock domain is required") - require(domains.map(_.name).distinct.size == domains.size, "clock-domain names must be unique") - require(domains.map(_.bit).distinct.size == domains.size, "clock-domain bit positions must be unique") + require(specs.nonEmpty, "at least one clock is required") + require(specs.map(_.name).distinct.size == specs.size, "clock names must be unique") require( - domains.map(_.bit).max < clockLevels.getWidth, - s"clock-domain bit does not fit in ${clockLevels.getWidth}-bit clock-level port" + clockLevels.getWidth == specs.size, + s"clock-level port has width ${clockLevels.getWidth}, " + + s"but ${specs.size} clocks were supplied", ) - private final class DomainState(val domain: ClockDomain): - var high: Boolean = false - var nextEdge: Int = Math.addExact(domain.phase, domain.halfPeriod) - - private val states = domains.sortBy(_.bit).map(domain => new DomainState(domain)) - private var initialized = false - private var logicalTime = 0 - private var packedLevels = BigInt(0) + private val plan = new ClockPlan(specs) + private var initialized = false /** Current logical timestamp. */ - def time: Int = logicalTime + def time: Long = plan.time - /** Current packed clock levels. */ - def levels: BigInt = packedLevels + /** Current packed clock levels. All bits are initially low. */ + def levels: BigInt = plan.levels - /** Timestamp of the next scheduled clock event. */ - def nextEventTime: Int = states.map(_.nextEdge).min + /** Timestamp of the next set of clock edges. */ + def nextEventTime: Long = plan.nextEventTime - /** Drive every managed clock low and evaluate the harness without advancing time. */ + /** Drives all clocks low and evaluates the harness at time zero. + * + * Calling this more than once has no additional effect. `advance()` calls it automatically if necessary. + */ def initialize(): Unit = - if !initialized then - toTestableUInt(clockLevels).poke(packedLevels) + if !initialized then { + toTestableUInt(clockLevels).poke(BigInt(0)) toTestableClock(timebaseClock).step(0) initialized = true + } - /** Advance to and apply the next set of simultaneous clock edges. */ - def advance(): ClockEvent = + /** Advances to the next timestamp with at least one clock edge. + * + * All edges at that timestamp are applied with one packed `poke`. Returns after the resulting circuit state has + * been evaluated. + */ + def advance(): ClockEvent = { initialize() + val targetTime = plan.nextEventTime + waitTicks(Math.subtractExact(targetTime, plan.time)) + + val event = plan.advance() + toTestableUInt(clockLevels).poke(event.levels) + toTestableClock(timebaseClock).step(0) + event + } + + /** Processes exactly `eventCount` edge timestamps. + * + * An event may contain edges from several clocks. + */ + def runEvents(eventCount: Int)(observe: ClockEvent => Unit = _ => ()): Unit = { + require(eventCount >= 0, s"event count must be non-negative, got $eventCount") + for _ <- 0 until eventCount do observe(advance()) + } + + /** Advances time in chunks whose doubled period fits in `Int`. */ + private def waitTicks(ticks: Long): Unit = { + require(ticks > 0, s"next clock event must be in the future, got $ticks ticks") + + val maxChunkTicks = Int.MaxValue.toLong / 2 + var remaining = ticks + + while remaining > 0 do { + val chunk = math.min(remaining, maxChunkTicks).toInt + toTestableClock(timebaseClock).step(cycles = 1, period = chunk * 2) + remaining -= chunk + } + } + } + + /** Calculates clock events without accessing ChiselSim. */ + private final class ClockPlan(specs: Vector[ClockSpec]) { + private final class DomainState(val spec: ClockSpec, val bit: Int) { + var high: Boolean = false + var nextEdgeAt: Long = spec.effectiveFirstRiseAt + } + + private val states = + specs.zipWithIndex.map { case (spec, bit) => + new DomainState(spec, bit) + } + + private var currentTime: Long = 0L + private var packedLevels = BigInt(0) + + def time: Long = currentTime + + def levels: BigInt = packedLevels + + def nextEventTime: Long = + states.iterator.map(_.nextEdgeAt).min + + def advance(): ClockEvent = { val eventTime = nextEventTime - val elapsed = Math.subtractExact(eventTime, logicalTime) - require(elapsed >= 0, "multi-clock scheduler moved backwards") + require(eventTime > currentTime, "clock plan did not advance in time") - if elapsed > 0 then - val period = Math.multiplyExact(elapsed, 2) - toTestableClock(timebaseClock).step(cycles = 1, period = period) - logicalTime = eventTime + val active = states.filter(_.nextEdgeAt == eventTime) - val activeStates = states.filter(_.nextEdge == eventTime) - val transitions = activeStates.map { state => - state.high = !state.high - packedLevels = - if state.high then packedLevels.setBit(state.domain.bit) - else packedLevels.clearBit(state.domain.bit) - state.nextEdge = Math.addExact(state.nextEdge, state.domain.halfPeriod) - Transition(state.domain, if state.high then Edge.Rising else Edge.Falling) + // Calculate every following edge before changing any state. + val updates = active.map { state => + val becomesHigh = !state.high + val duration = + if becomesHigh then state.spec.highTicks + else state.spec.effectiveLowTicks + + val followingEdgeAt = Math.addExact(eventTime, duration) + (state, becomesHigh, followingEdgeAt) } - toTestableUInt(clockLevels).poke(packedLevels) - toTestableClock(timebaseClock).step(0) + val transitions = updates.map { case (state, becomesHigh, followingEdgeAt) => + state.high = becomesHigh + state.nextEdgeAt = followingEdgeAt - ClockEvent(logicalTime, transitions, packedLevels) + packedLevels = + if becomesHigh then packedLevels.setBit(state.bit) + else packedLevels.clearBit(state.bit) - /** Process exactly `eventCount` scheduler timestamps. */ - def run(eventCount: Int)(observe: ClockEvent => Unit = _ => ()): Unit = - require(eventCount >= 0, s"event count must be non-negative, got $eventCount") - for _ <- 0 until eventCount do observe(advance()) + ClockTransition(state.spec, if becomesHigh then ClockEdge.Rising else ClockEdge.Falling) + } + + currentTime = eventTime + ClockEvent(eventTime, transitions, packedLevels) + } + } +} diff --git a/chipmunk/src/tester/TesterAPI.scala b/chipmunk/src/tester/TesterAPI.scala index d8b97fe..3943642 100644 --- a/chipmunk/src/tester/TesterAPI.scala +++ b/chipmunk/src/tester/TesterAPI.scala @@ -6,31 +6,34 @@ import chisel3.simulator.* /** Small convenience extensions built on top of ChiselSim's peek/poke API. */ -trait TesterAPI: +trait TesterAPI { this: PeekPokeAPI => - extension [T <: Data](target: T) - + extension [T <: Data](target: T) { /** Assign a Chisel literal to a DUT port. */ def #=(value: T): Unit = - (target, value) match + (target, value) match { case (reset: AsyncReset, literal: Reset) => toTestableReset(reset).poke(literal) case _ => toTestableData(target).poke(value) + } + } - extension (bool: Bool) - + extension (bool: Bool) { /** Assign a random value to a `Bool` port. */ def randomize(): Unit = toTestableBool(bool).poke(scala.util.Random.nextBoolean()) + } - extension (uint: UInt) - + extension (uint: UInt) { /** Assign a uniformly distributed random bit pattern to a `UInt` port. */ def randomize(): Unit = toTestableUInt(uint).poke(BigInt(uint.getWidth, scala.util.Random)) + } - extension (sint: SInt) - + extension (sint: SInt) { /** Assign a uniformly distributed two's-complement bit pattern to a `SInt` port. */ - def randomize(): Unit = + def randomize(): Unit = { require(sint.getWidth > 0, "cannot randomize a zero-width SInt") val value = BigInt(sint.getWidth, scala.util.Random) - (BigInt(1) << (sint.getWidth - 1)) toTestableSInt(sint).poke(value) + } + } +} diff --git a/chipmunk/src/tester/TraceSupport.scala b/chipmunk/src/tester/TraceSupport.scala deleted file mode 100644 index bfe9521..0000000 --- a/chipmunk/src/tester/TraceSupport.scala +++ /dev/null @@ -1,76 +0,0 @@ -package chipmunk -package tester - -import chisel3.simulator.ControlAPI -import chisel3.testing.HasTestingDirectory - -import java.nio.file.{Files, Path} -import scala.jdk.CollectionConverters.* - -trait TraceSupport: - this: ControlAPI => - - protected def fstTraceStyle: svsim.verilator.Backend.CompilationSettings.TraceStyle = - svsim.verilator.Backend.CompilationSettings - .TraceStyle(kind = svsim.verilator.Backend.CompilationSettings.TraceKind.Fst()) - - final def withWaves[A](body: => A): A = - enableWaves() - var primaryFailure = Option.empty[Throwable] - - try body - catch - case error: Throwable => - primaryFailure = Some(error) - throw error - finally - try disableWaves() - catch - case error: Throwable => - primaryFailure match - case Some(primary) => primary.addSuppressed(error) - case None => throw error - -/** Backend-neutral access to files produced for the current ScalaTest test. */ -object SimulationArtifacts: - - private val traceFileExtensions = Set("fst", "vcd", "vpd", "fsdb") - private val logFileNames = Set("compilation-log.txt", "simulation-log.txt") - private val replayFileNames = Set("execution-script.txt") - - /** Root directory assigned by ChiselSim to the current ScalaTest test. */ - def testDirectory(using testingDirectory: HasTestingDirectory): Path = testingDirectory.getDirectory - - /** Waveform files below the current test directory, independent of simulator backend. */ - def traceFiles(using testingDirectory: HasTestingDirectory): Seq[Path] = - findFiles(path => - val fileName = path.getFileName.toString - traceFileExtensions.exists(extension => fileName.endsWith(s".$extension")) - ) - - /** Compilation and simulation logs below the current test directory. */ - def logFiles(using testingDirectory: HasTestingDirectory): Seq[Path] = - findFiles(path => logFileNames.contains(path.getFileName.toString)) - - /** Scripts that reproduce individual simulator invocations. */ - def replayScripts(using testingDirectory: HasTestingDirectory): Seq[Path] = - findFiles(path => replayFileNames.contains(path.getFileName.toString)) - - /** All waveform and diagnostic artifacts normally useful after a test failure. */ - def debugFiles(using testingDirectory: HasTestingDirectory): Seq[Path] = - (traceFiles ++ logFiles ++ replayScripts).sortBy(_.toString) - - private def findFiles(select: Path => Boolean)(using testingDirectory: HasTestingDirectory): Seq[Path] = - val root = testDirectory - if !Files.isDirectory(root) then Seq.empty - else - val paths = Files.walk(root) - try - paths - .iterator() - .asScala - .filter(path => Files.isRegularFile(path)) - .filter(select) - .toVector - .sortBy(_.toString) - finally paths.close() diff --git a/chipmunk/test/src/ChipmunkSim.scala b/chipmunk/test/src/ChipmunkSim.scala index 68e04fd..4cd87f8 100644 --- a/chipmunk/test/src/ChipmunkSim.scala +++ b/chipmunk/test/src/ChipmunkSim.scala @@ -1,7 +1,7 @@ -package chipmunk.test +package chipmunk +package test -import chipmunk.tester.{MultiClockSupport, TesterAPI, TraceSupport} -import chisel3.simulator.scalatest.{ChiselSim, Cli} +import chisel3.simulator.scalatest.{ChiselSim, Cli, HasCliOptions} import chisel3.simulator.scalatest.HasCliOptions.CliOption import org.scalatest.TestSuite import org.scalatest.flatspec.AnyFlatSpec @@ -9,15 +9,10 @@ import org.scalatest.freespec.AnyFreeSpec import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers -trait ChipmunkSim - extends ChiselSim - with Cli.Simulator - with Cli.EmitFsdb - with Cli.EmitVpd - with TesterAPI - with TraceSupport - with MultiClockSupport: - this: TestSuite => +import chipmunk.tester.{MultiClockSupport, TesterAPI} + +trait EmitFst { + this: HasCliOptions => addOption( CliOption.flag( @@ -27,18 +22,33 @@ trait ChipmunkSim options.copy(simulationSettings = options.simulationSettings.copy(enableWavesAtTimeZero = true)), updateBackendSettings = { case options: svsim.verilator.Backend.CompilationSettings => - options.withTraceStyle(Some(fstTraceStyle)) + options.withTraceStyle( + Some( + svsim.verilator.Backend.CompilationSettings + .TraceStyle(kind = svsim.verilator.Backend.CompilationSettings.TraceKind.Fst()) + ) + ) case _: svsim.vcs.Backend.CompilationSettings => - throw new IllegalArgumentException( - "VCS does not support FST waveforms; use -DemitFsdb=1, -DemitVpd=1, or -DemitVcd=1." - ) + throw new IllegalArgumentException("VCS does not support FST; use FSDB, VPD, or VCD.") case other => - throw new IllegalArgumentException(s"${other.getClass.getName} does not support Chipmunk FST configuration.") - } + throw new IllegalArgumentException(s"${other.getClass.getName} does not support FST.") + }, ) ) +} + +trait ChipmunkSim + extends ChiselSim + with Cli.Simulator + with Cli.EmitFsdb + with Cli.EmitVpd + with EmitFst + with TesterAPI + with MultiClockSupport { + this: TestSuite => +} abstract class ChipmunkFlatSpec extends AnyFlatSpec with Matchers with ChipmunkSim diff --git a/chipmunk/test/src/MultiClockSpec.scala b/chipmunk/test/src/MultiClockSpec.scala index 44dff4f..9525ec2 100644 --- a/chipmunk/test/src/MultiClockSpec.scala +++ b/chipmunk/test/src/MultiClockSpec.scala @@ -1,61 +1,104 @@ package chipmunk.test -import chipmunk.tester.MultiClockSupport.{ClockDomain, ClockEvent, Edge} -import chisel3.* - import scala.collection.mutable.ArrayBuffer -private object MultiClockSpecDut: +import chisel3.* + +import chipmunk.tester.{ClockEdge, ClockEvent, ClockSpec} - final class Harness extends Module: - val io = IO(new Bundle: +private object MultiClockSpecDut { + final class Harness extends Module { + val io = IO(new Bundle { val clockLevels = Input(UInt(2.W)) val fastCount = Output(UInt(8.W)) - val slowCount = Output(UInt(8.W))) + val slowCount = Output(UInt(8.W)) + }) io.fastCount := counter(io.clockLevels(0).asClock) io.slowCount := counter(io.clockLevels(1).asClock) private def counter(domainClock: Clock): UInt = - withClockAndReset(domainClock, reset.asAsyncReset): + withClockAndReset(domainClock, reset.asAsyncReset) { val value = RegInit(0.U(8.W)) value := value + 1.U value + } + } +} -class MultiClockSpec extends ChipmunkFlatSpec: +class MultiClockSpec extends ChipmunkFlatSpec { + "MultiClockSupport" should "schedule different frequencies and coalesce simultaneous edges deterministically" in { + simulate(new MultiClockSpecDut.Harness) { dut => + val fast = ClockSpec(name = "fast", periodTicks = 4) + val slow = ClockSpec(name = "slow", periodTicks = 6, firstRiseAt = 4) - "MultiClockSupport" should "schedule different frequencies and coalesce simultaneous edges deterministically" in: - simulate(new MultiClockSpecDut.Harness): dut => - val fast = ClockDomain(name = "fast", bit = 0, halfPeriod = 2) - val slow = ClockDomain(name = "slow", bit = 1, halfPeriod = 3, phase = 1) - val scheduler = multiClockScheduler(dut.io.clockLevels, dut.clock, Seq(slow, fast)) + // The sequence index is the corresponding bit in clockLevels. + val scheduler = + new MultiClockScheduler(dut.io.clockLevels, dut.clock, Seq(fast, slow)) scheduler.initialize() - dut.io.fastCount.expect(0.U) - dut.io.slowCount.expect(0.U) + dut.io.fastCount expect 0.U + dut.io.slowCount expect 0.U val expectedCounts = Seq((1, 0), (1, 1), (2, 1), (2, 1), (2, 1), (3, 2)) val events = ArrayBuffer.empty[ClockEvent] - scheduler.run(6): event => + + scheduler.runEvents(6) { event => val (fastCount, slowCount) = expectedCounts(events.size) events.addOne(event) - dut.io.clockLevels.expect(event.levels) - dut.io.fastCount.expect(fastCount.U) - dut.io.slowCount.expect(slowCount.U) - () - events.map(_.time).toSeq shouldBe Seq(2, 4, 6, 7, 8, 10) - events.map(_.levels).toSeq shouldBe Seq(1, 2, 3, 1, 0, 3).map(value => BigInt(value)) + dut.io.clockLevels expect event.levels + dut.io.fastCount expect fastCount.U + dut.io.slowCount expect slowCount.U + } + + events.map(_.time).toSeq shouldBe Seq(2L, 4L, 6L, 7L, 8L, 10L) + events.map(_.levels).toSeq shouldBe + Seq(1, 2, 3, 1, 0, 3).map(BigInt(_)) - events(1).transitions.map(transition => transition.domain.name -> transition.edge) shouldBe - Seq("fast" -> Edge.Falling, "slow" -> Edge.Rising) + events(1).transitions.map(t => t.domain.name -> t.edge) shouldBe + Seq("fast" -> ClockEdge.Falling, "slow" -> ClockEdge.Rising) events(1).falling.map(_.domain.name) shouldBe Seq("fast") events(1).rising.map(_.domain.name) shouldBe Seq("slow") - events(5).transitions.map(transition => transition.domain.name -> transition.edge) shouldBe - Seq("fast" -> Edge.Rising, "slow" -> Edge.Rising) - scheduler.time shouldBe 10 + events(5).transitions.map(t => t.domain.name -> t.edge) shouldBe + Seq("fast" -> ClockEdge.Rising, "slow" -> ClockEdge.Rising) + + scheduler.time shouldBe 10L scheduler.levels shouldBe BigInt(3) - scheduler.nextEventTime shouldBe 12 - dut.io.fastCount.expect(3.U) - dut.io.slowCount.expect(2.U) + scheduler.nextEventTime shouldBe 12L + dut.io.fastCount expect 3.U + dut.io.slowCount expect 2.U + } + } + + it should "use the configured low duration for the initial and subsequent low phases" in { + simulate(new MultiClockSpecDut.Harness) { dut => + val fast = ClockSpec(name = "fast", periodTicks = 100) + val slow = ClockSpec(name = "slow", periodTicks = 6, lowTicks = 4) + + val scheduler = + new MultiClockScheduler(dut.io.clockLevels, dut.clock, Seq(fast, slow)) + + val expectedSlowCounts = Seq(1, 1, 2, 2) + val events = ArrayBuffer.empty[ClockEvent] + + scheduler.runEvents(4) { event => + val slowCount = expectedSlowCounts(events.size) + events.addOne(event) + + dut.io.clockLevels expect event.levels + dut.io.fastCount expect 0.U + dut.io.slowCount expect slowCount.U + } + + // Initial low: 4 ticks. High: 2 ticks. Later low: 4 ticks. + events.map(_.time).toSeq shouldBe Seq(4L, 6L, 10L, 12L) + events.map(_.levels).toSeq shouldBe Seq(2, 0, 2, 0).map(BigInt(_)) + events.map(_.transitions.head.edge).toSeq shouldBe + Seq(ClockEdge.Rising, ClockEdge.Falling, ClockEdge.Rising, ClockEdge.Falling) + + scheduler.nextEventTime shouldBe 16L + } + } +} diff --git a/chipmunk/test/src/RegNegSpec.scala b/chipmunk/test/src/RegNegSpec.scala index 362de8f..2126174 100644 --- a/chipmunk/test/src/RegNegSpec.scala +++ b/chipmunk/test/src/RegNegSpec.scala @@ -1,51 +1,64 @@ package chipmunk.test -import chipmunk.{RegNegEnable, RegNegNext} import chisel3.* -private object RegNegSpecDut: +import chipmunk.{RegNegEnable, RegNegNext} + +private object RegNegSpecDut { - final class NoResetHarness extends Module: - val io = IO(new Bundle: + final class NoResetHarness extends Module { + val io = IO(new Bundle { val clockLevels = Input(UInt(1.W)) val data = Input(UInt(8.W)) val enable = Input(Bool()) val nextValue = Output(UInt(8.W)) - val enabled = Output(UInt(8.W))) + val enabled = Output(UInt(8.W)) + }) - io.nextValue := withClock(io.clockLevels(0).asClock): + io.nextValue := withClock(io.clockLevels(0).asClock) { RegNegNext(io.data) - io.enabled := withClock(io.clockLevels(0).asClock): + } + io.enabled := withClock(io.clockLevels(0).asClock) { RegNegEnable(io.data, io.enable) + } + } - final class AsyncResetHarness extends Module: - val io = IO(new Bundle: + final class AsyncResetHarness extends Module { + val io = IO(new Bundle { val clockLevels = Input(UInt(1.W)) val domainReset = Input(Bool()) val data = Input(UInt(8.W)) - val output = Output(UInt(8.W))) + val output = Output(UInt(8.W)) + }) - io.output := withClockAndReset(io.clockLevels(0).asClock, io.domainReset.asAsyncReset): + io.output := withClockAndReset(io.clockLevels(0).asClock, io.domainReset.asAsyncReset) { RegNegNext(io.data, 0xa5.U(8.W), isResetAsync = true) + } + } - final class SyncResetHarness extends Module: - val io = IO(new Bundle: + final class SyncResetHarness extends Module { + val io = IO(new Bundle { val clockLevels = Input(UInt(1.W)) val domainReset = Input(Bool()) val data = Input(UInt(8.W)) - val output = Output(UInt(8.W))) + val output = Output(UInt(8.W)) + }) - io.output := withClockAndReset(io.clockLevels(0).asClock, io.domainReset): + io.output := withClockAndReset(io.clockLevels(0).asClock, io.domainReset) { RegNegNext(io.data, 0xa5.U(8.W), isResetAsync = false) + } + } +} -class RegNegSpec extends ChipmunkFlatSpec: +class RegNegSpec extends ChipmunkFlatSpec { - private def driveClock(clockLevels: UInt, timebaseClock: Clock, high: Boolean): Unit = + private def driveClock(clockLevels: UInt, timebaseClock: Clock, high: Boolean): Unit = { clockLevels #= (if high then 1.U else 0.U) timebaseClock.step(0) + } - "RegNegNext and RegNegEnable" should "capture only on falling edges and honor enable" in: - simulate(new RegNegSpecDut.NoResetHarness): dut => + "RegNegNext and RegNegEnable" should "capture only on falling edges and honor enable" in { + simulate(new RegNegSpecDut.NoResetHarness) { dut => dut.io.data #= 0x12.U dut.io.enable #= true.B driveClock(dut.io.clockLevels, dut.clock, high = false) @@ -69,9 +82,11 @@ class RegNegSpec extends ChipmunkFlatSpec: driveClock(dut.io.clockLevels, dut.clock, high = false) dut.io.nextValue.expect(0x56.U) dut.io.enabled.expect(0x34.U) + } + } - it should "apply asynchronous reset without waiting for a clock edge" in: - simulate(new RegNegSpecDut.AsyncResetHarness): dut => + it should "apply asynchronous reset without waiting for a clock edge" in { + simulate(new RegNegSpecDut.AsyncResetHarness) { dut => dut.io.domainReset #= false.B dut.io.data #= 0x31.U driveClock(dut.io.clockLevels, dut.clock, high = false) @@ -90,9 +105,11 @@ class RegNegSpec extends ChipmunkFlatSpec: dut.io.output.expect(0xa5.U) driveClock(dut.io.clockLevels, dut.clock, high = false) dut.io.output.expect(0x52.U) + } + } - it should "apply synchronous reset on the next falling edge" in: - simulate(new RegNegSpecDut.SyncResetHarness): dut => + it should "apply synchronous reset on the next falling edge" in { + simulate(new RegNegSpecDut.SyncResetHarness) { dut => dut.io.domainReset #= false.B dut.io.data #= 0x31.U driveClock(dut.io.clockLevels, dut.clock, high = false) @@ -115,3 +132,6 @@ class RegNegSpec extends ChipmunkFlatSpec: driveClock(dut.io.clockLevels, dut.clock, high = true) driveClock(dut.io.clockLevels, dut.clock, high = false) dut.io.output.expect(0x73.U) + } + } +} diff --git a/chipmunk/test/src/TraceSpec.scala b/chipmunk/test/src/TraceSpec.scala index 9ea9e88..cb68dd7 100644 --- a/chipmunk/test/src/TraceSpec.scala +++ b/chipmunk/test/src/TraceSpec.scala @@ -1,52 +1,63 @@ package chipmunk.test -import chipmunk.tester.SimulationArtifacts -import chisel3.* -import chisel3.simulator.HasSimulator - import java.nio.file.Files -private object TraceSpecDut: - final class Harness extends Module: - val io = IO(new Bundle: +import scala.jdk.CollectionConverters.* + +import chisel3.* +import org.scalatest.ConfigMap + +private object TraceSpecDut { + final class Harness extends Module { + val io = IO(new Bundle { val input = Input(UInt(8.W)) - val output = Output(UInt(8.W))) + val output = Output(UInt(8.W)) + }) val sampled = RegInit(0.U(8.W)) sampled := io.input io.output := sampled + } +} class TraceSpec extends ChipmunkFlatSpec { - "TraceSupport" should "write a non-empty FST for an explicitly enabled waveform window" in { - given fstSimulator: HasSimulator = HasSimulator.simulators.verilator(verilatorSettings = - svsim.verilator.Backend.CompilationSettings.default.withTraceStyle(Some(fstTraceStyle)) - ) - - simulate(new TraceSpecDut.Harness, subdirectory = Some("fst-window")): dut => - dut.io.input #= 0x11.U - dut.clock.step() - dut.io.output.expect(0x11.U) - - val result = withWaves: - for value <- Seq(0x22, 0x33, 0x44) do - dut.io.input #= value.U - dut.clock.step() - dut.io.output.expect(value.U) - "window-complete" - - result shouldBe "window-complete" - - // Activity outside this block must remain legal after withWaves has disabled tracing. - dut.io.input #= 0x55.U - dut.clock.step() - dut.io.output.expect(0x55.U) - - val fstFiles = SimulationArtifacts.traceFiles.filter(_.getFileName.toString.endsWith(".fst")) - withClue(s"trace files: ${SimulationArtifacts.traceFiles.mkString(", ")}"): + override def configMap: ConfigMap = + super.configMap + + ("simulator" -> "verilator") + + ("emitFst" -> "1") + + "EmitFst" should "write a non-empty FST" in { + simulate(new TraceSpecDut.Harness, subdirectory = Some("fst-window")) { dut => + for value <- Seq(0x11, 0x22, 0x33, 0x44) do { + dut.io.input #= value.U + dut.clock.step() + dut.io.output.expect(value.U) + } + } + + val root = implementation.getDirectory.resolve("fst-window") + withClue(s"Simulation directory: $root") { + Files.isDirectory(root) shouldBe true + } + + val stream = Files.walk(root) + val fstFiles = + try + stream + .iterator() + .asScala + .filter(path => Files.isRegularFile(path)) + .filter(path => path.getFileName.toString.endsWith(".fst")) + .toVector + finally stream.close() + + withClue(s"FST files under $root: ${fstFiles.mkString(", ")}") { fstFiles should not be empty - fstFiles.foreach(path => Files.size(path) should be > 1L) - - SimulationArtifacts.logFiles should not be empty - SimulationArtifacts.replayScripts should not be empty + } + fstFiles.foreach { path => + withClue(s"FST file $path: ") { + Files.size(path) should be > 1L + } + } } } diff --git a/mylib/src/RtlEmitter.scala b/mylib/src/RtlEmitter.scala index a2e06cb..51b3b42 100644 --- a/mylib/src/RtlEmitter.scala +++ b/mylib/src/RtlEmitter.scala @@ -1,9 +1,10 @@ package mylib -import chipmunk._ import chisel3._ import circt.stage._ +import chipmunk._ + class MyIncrement extends Module { val io = IO(new Bundle { val source = Input(UInt(3.W)) From dad92fc626b34ff66d2236e1d34902cb0031408e Mon Sep 17 00:00:00 2001 From: zhutmost Date: Thu, 1 Oct 2026 23:34:33 +0800 Subject: [PATCH 04/26] feat: add reset synchronization support for asynchronous resets --- chipmunk/src/AsyncResetSync.scala | 58 +++++++++++++++++++++++++++++++ mylib/src/RtlEmitter.scala | 36 +++++++++---------- 2 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 chipmunk/src/AsyncResetSync.scala diff --git a/chipmunk/src/AsyncResetSync.scala b/chipmunk/src/AsyncResetSync.scala new file mode 100644 index 0000000..e65afd9 --- /dev/null +++ b/chipmunk/src/AsyncResetSync.scala @@ -0,0 +1,58 @@ +package chipmunk + +import chisel3.* +import chisel3.util.ShiftRegister + +/** Synchronize the deassertion of an active-high asynchronous reset. + * + * Assertion of the implicit reset asserts `resetOut` asynchronously. Deassertion propagates through `stages` registers + * on the implicit clock. After assertion, `resetOut` remains asserted while that clock is stopped. + * + * @param stages + * number of synchronization stages; must be at least two + */ +final class AsyncResetSync(val stages: Int = 2) extends Module with RequireAsyncReset { + require(stages >= 2, "AsyncResetSync requires at least two stages.") + + val io = IO(new Bundle { + val resetOut = Output(AsyncReset()) + }) + + private val resetSynced = ShiftRegister(false.B, stages, true.B, true.B) + + io.resetOut := resetSynced.asAsyncReset +} + +object AsyncResetSync { + + /** Synchronize the implicit reset to the implicit clock using [[AsyncResetSync]]. Note that only high-active reset is + * supported. + * + * @param stages + * The stage number of the reset synchronizer buffer. + * @return + * The synchronized reset signal. + */ + def apply(stages: Int = 2): AsyncReset = { + val uResetSync = Module(new AsyncResetSync(stages)) + uResetSync.io.resetOut + } + + /** Synchronize the given reset to the given clock using [[AsyncResetSync]]. Note that only high-active reset is + * supported. + * + * @param clock + * The target clock. + * @param resetAsync + * The input asynchronous reset signal needed to be synchronized with the target clock. + * @param stages + * The stage number of the synchronizer. + * @return + * The synchronized reset signal. + */ + def withSpecificClockDomain(clock: Clock, resetAsync: Reset, stages: Int = 2): AsyncReset = { + withClockAndReset(clock, resetAsync.asAsyncReset) { + apply(stages) + } + } +} diff --git a/mylib/src/RtlEmitter.scala b/mylib/src/RtlEmitter.scala index 51b3b42..0052a0c 100644 --- a/mylib/src/RtlEmitter.scala +++ b/mylib/src/RtlEmitter.scala @@ -13,23 +13,23 @@ class MyIncrement extends Module { io.sink := RegNext(io.source + 1.U, init = 0.U) } -// class MyChipTop extends RawModule { -// val coreClock = IO(Input(Clock())) -// val coreReset = IO(Input(AsyncReset())) -// val coreSource = IO(Input(SInt(3.W))) -// val coreSink = IO(Output(UInt(3.W))) - -// implicit val clockSys: Clock = coreClock -// implicit val resetSys: AsyncReset = AsyncResetSyncDessert.withSpecificClockDomain(clockSys, coreReset) - -// withClockAndReset(clockSys, resetSys) { -// val uIncrement = Module(new MyIncrement) -// uIncrement.io.source <> coreSource.asUInt -// uIncrement.io.sink <> coreSink -// } -// } - -object RtlEmitter extends App { +class MyChipTop extends RawModule { + val coreClock = IO(Input(Clock())) + val coreReset = IO(Input(AsyncReset())) + val coreSource = IO(Input(SInt(3.W))) + val coreSink = IO(Output(UInt(3.W))) + + implicit val clockSys: Clock = coreClock + implicit val resetSys: AsyncReset = AsyncResetSync.withSpecificClockDomain(clockSys, coreReset) + + withClockAndReset(clockSys, resetSys) { + val uIncrement = Module(new MyIncrement) + uIncrement.io.source <> coreSource.asUInt + uIncrement.io.sink <> coreSink + } +} + +@main def rtlEmitter(): Unit = { val targetDir = "generate/hw" val chiselArgs = Array(f"--target-dir=$targetDir", "--split-verilog") @@ -37,6 +37,6 @@ object RtlEmitter extends App { Array("--disable-all-randomization", "-repl-seq-mem", f"-repl-seq-mem-file=seq-mem.conf") ChiselStage - .emitSystemVerilogFile(new MyIncrement, chiselArgs, firtoolOpts) + .emitSystemVerilogFile(new MyChipTop, chiselArgs, firtoolOpts) println(f">>> RTL emitted in \"$targetDir\" directory.") } From 038f0723dd600a6070bbfea79df5fa855ab0a051 Mon Sep 17 00:00:00 2001 From: zhutmost Date: Fri, 2 Oct 2026 17:32:52 +0800 Subject: [PATCH 05/26] feat: add MuxPriority utility with overloads --- chipmunk/src/Misc.scala | 25 +++++- chipmunk/test/src/MuxPrioritySpec.scala | 101 ++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 chipmunk/test/src/MuxPrioritySpec.scala diff --git a/chipmunk/src/Misc.scala b/chipmunk/src/Misc.scala index 00fd78c..0798ed1 100644 --- a/chipmunk/src/Misc.scala +++ b/chipmunk/src/Misc.scala @@ -1,14 +1,33 @@ package chipmunk import chisel3.* +import chisel3.experimental.SourceInfo import chisel3.util.PriorityMux /** A Bundle with no payload fields. Useful as a typed empty payload. */ final class EmptyBundle extends Bundle -/** Alias for [[chisel3.util.PriorityMux]]. +/** Alias for [[chisel3.util.PriorityMux]] with additional overloads accepting an explicit default. + * + * When a default is provided, it is selected if no select signal is asserted. * * @note - * Chisel has many muxes named `MuxXxx`, but this is the only one that is named as `XxxMux`. + * Uses the `MuxXxx` naming convention for consistency with other Chisel mux utilities. */ -val MuxPriority: PriorityMux.type = PriorityMux +object MuxPriority { + + def apply[T <: Data](in: Seq[(Bool, T)])(using SourceInfo): T = PriorityMux(in) + + def apply[T <: Data](sel: Seq[Bool], in: Seq[T])(using SourceInfo): T = PriorityMux(sel, in) + + def apply[T <: Data](sel: Bits, in: Seq[T])(using SourceInfo): T = PriorityMux(sel, in) + + def apply[T <: Data](in: Seq[(Bool, T)], default: T)(using SourceInfo): T = + PriorityMux(in :+ (true.B -> default)) + + def apply[T <: Data](sel: Seq[Bool], in: Seq[T], default: T)(using SourceInfo): T = + apply(sel.zip(in), default) + + def apply[T <: Data](sel: Bits, in: Seq[T], default: T)(using SourceInfo): T = + apply(sel.asBools.zip(in), default) +} diff --git a/chipmunk/test/src/MuxPrioritySpec.scala b/chipmunk/test/src/MuxPrioritySpec.scala new file mode 100644 index 0000000..1f349dc --- /dev/null +++ b/chipmunk/test/src/MuxPrioritySpec.scala @@ -0,0 +1,101 @@ +package chipmunk.test + +import chisel3.* + +import chipmunk.MuxPriority + +private object MuxPrioritySpecDut { + + final class Harness(size: Int) extends Module { + val io = IO(new Bundle { + val select = Input(UInt(size.W)) + val data = Input(Vec(size, UInt(8.W))) + val defaultValue = Input(UInt(8.W)) + val withoutDefault = Output(Vec(3, UInt(8.W))) + val withDefault = Output(Vec(3, UInt(8.W))) + val emptyPairs = Output(UInt(8.W)) + val emptySelectors = Output(UInt(8.W)) + }) + + private val selectors = io.select.asBools + private val inputs = io.data + private val pairs = selectors.zip(inputs) + + io.withoutDefault(0) := MuxPriority(pairs) + io.withoutDefault(1) := MuxPriority(selectors, inputs) + io.withoutDefault(2) := MuxPriority(io.select, inputs) + + io.withDefault(0) := MuxPriority(pairs, default = io.defaultValue) + io.withDefault(1) := MuxPriority(selectors, inputs, default = io.defaultValue) + io.withDefault(2) := MuxPriority(io.select, inputs, default = io.defaultValue) + + io.emptyPairs := MuxPriority(Seq.empty[(Bool, UInt)], default = io.defaultValue) + + io.emptySelectors := MuxPriority(Seq.empty[Bool], Seq.empty[UInt], default = io.defaultValue) + } +} + +class MuxPrioritySpec extends ChipmunkFlatSpec { + + private def check(dut: MuxPrioritySpecDut.Harness, data: Seq[Int], defaultValue: Int, mask: Int): Unit = { + dut.io.select #= mask.U + dut.io.defaultValue #= defaultValue.U + + for index <- data.indices do { + dut.io.data(index) #= data(index).U + } + + dut.clock.step(0) + + // Calculate expected values independently: smaller indices have higher priority. + val selectedIndex = data.indices.find(index => ((mask >>> index) & 1) != 0) + + val expectedWithoutDefault = + selectedIndex.map(index => data(index)).getOrElse(data.last) + + val expectedWithDefault = + selectedIndex.map(index => data(index)).getOrElse(defaultValue) + + withClue(s"mask=$mask, data=$data, default=$defaultValue: ") { + for overload <- 0 until 3 do { + withClue(s"overload=$overload: ") { + dut.io.withoutDefault(overload).expect(expectedWithoutDefault.U) + dut.io.withDefault(overload).expect(expectedWithDefault.U) + } + } + + dut.io.emptyPairs.expect(defaultValue.U) + dut.io.emptySelectors.expect(defaultValue.U) + } + } + + "MuxPriority" should "select the first asserted input across all overloads" in { + simulate(new MuxPrioritySpecDut.Harness(size = 4)) { dut => + val patterns = Seq( + (Seq(0x11, 0x22, 0x44, 0x88), 0xe7), + (Seq(0xff, 0x80, 0x01, 0x00), 0x5a), + (Seq(0x00, 0x00, 0x00, 0x00), 0xff), + ) + + for { + (data, defaultValue) <- patterns + mask <- 0 until 16 + } { + check(dut, data, defaultValue, mask) + } + } + } + + it should "support single inputs and explicit defaults for empty mappings" in { + simulate(new MuxPrioritySpecDut.Harness(size = 1)) { dut => + val patterns = Seq((0x00, 0xff), (0x5a, 0xa5), (0xff, 0x00)) + + for { + (value, defaultValue) <- patterns + mask <- 0 until 2 + } { + check(dut, Seq(value), defaultValue, mask) + } + } + } +} From 701ba9783def3db09ad68db8635bdd97025e85c5 Mon Sep 17 00:00:00 2001 From: zhutmost Date: Fri, 2 Oct 2026 17:34:42 +0800 Subject: [PATCH 06/26] ci: update checkFormat step --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a399696..7ed063f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,10 @@ jobs: run: ./mill __.compile - name: Check formatting - run: ./mill __.checkFormat + run: | + ./mill mill.scalalib.scalafmt/checkFormatAll || ( + ./mill mill.scalalib.scalafmt/ && git diff --exit-code + ) test: runs-on: ubuntu-latest From 371d1780ead3f785928de6ccba92230c1b49dd0b Mon Sep 17 00:00:00 2001 From: zhutmost Date: Fri, 2 Oct 2026 23:57:14 +0800 Subject: [PATCH 07/26] feat: add `StateMachine` utility --- chipmunk/src/StateMachine.scala | 134 +++++++++ chipmunk/test/src/StateMachineSpec.scala | 348 +++++++++++++++++++++++ 2 files changed, 482 insertions(+) create mode 100644 chipmunk/src/StateMachine.scala create mode 100644 chipmunk/test/src/StateMachineSpec.scala diff --git a/chipmunk/src/StateMachine.scala b/chipmunk/src/StateMachine.scala new file mode 100644 index 0000000..68407d5 --- /dev/null +++ b/chipmunk/src/StateMachine.scala @@ -0,0 +1,134 @@ +package chipmunk + +import scala.collection.mutable + +import chisel3.* +import chisel3.experimental.SourceInfo + +/** A finite state machine whose states are literals of a ChiselEnum. + * + * The state register follows the clock and reset signals at the StateMachine call site. Reset selects the initial + * state directly. States without a transition hold their value; self-transitions do not produce entry or exit events. + */ +final class StateMachine[S <: EnumType] private ( + private val states: Vector[S], + private val stateReg: S, + private val stateNext: S, +) { + + /** A read-only view of the current state. */ + def current(using SourceInfo): S = stateReg.readOnly + + /** Returns True while the current state is s. */ + def is(s: S)(using SourceInfo): Bool = { + checkState(s) + stateReg === s + } + + /** Returns True before the clock edge that changes the state from another state to s. */ + def entering(s: S)(using SourceInfo): Bool = { + checkState(s) + stateReg =/= s && stateNext === s + } + + /** Returns True before the clock edge that changes the state from s to another state. */ + def leaving(s: S)(using SourceInfo): Bool = { + checkState(s) + stateReg === s && stateNext =/= s + } + + /** True when the state register contains a declared enum value. */ + def valid(using SourceInfo): Bool = stateReg.isValid + + private def checkState(s: S)(using sourceInfo: SourceInfo): Unit = { + require( + states.exists(_ eq s), + s"StateMachine requires a declared enum literal, got $s${sourceInfo.makeMessage(location => s" $location")}.", + ) + } +} + +object StateMachine { + + /** Build an FSM immediately in the current Chisel clock/reset scope. + * + * Define the FSM outside hardware when/switch blocks. An omitted on block means the corresponding state holds. Each + * state may have at most one on block. Ordinary Chisel last-connect priority applies to goto assignments. + */ + def apply[E <: ChiselEnum](states: E)( + initial: states.Type + )(body: Builder[states.Type] ?=> Unit)(using sourceInfo: SourceInfo): StateMachine[states.Type] = { + val values = states.all.toVector + require(values.nonEmpty, "StateMachine requires at least one state.") + require( + values.exists(_ eq initial), + s"StateMachine initial state must be a declared enum literal${sourceInfo.makeMessage(location => s" $location")}.", + ) + + val stateReg = RegInit(initial).suggestName("stateCurr") + val stateNext = WireDefault(stateReg).suggestName("stateNext") + val machine = new StateMachine[states.Type](values, stateReg, stateNext) + + stateReg := stateNext + + val builder = new Builder[states.Type](machine) + try body(using builder) + finally builder.close() + + machine + } + + /** Generate hardware enabled while the FSM is in s. */ + def on[S <: EnumType](s: S)(body: StateScope[S] ?=> Unit)(using builder: Builder[S], sourceInfo: SourceInfo): Unit = + builder.define(s)(body) + + /** Select the state for the next clock edge. Only available within an on block. */ + def goto[S <: EnumType](target: S)(using scope: StateScope[S], sourceInfo: SourceInfo): Unit = + scope.transition(target) + + /** Context supplied to a StateMachine definition block. */ + final class Builder[S <: EnumType] private[StateMachine] (machine: StateMachine[S]) { + private val defined = mutable.HashSet.empty[BigInt] + private var open = true + private var inStateBody = false + + private[StateMachine] def define(s: S)(body: StateScope[S] ?=> Unit)(using sourceInfo: SourceInfo): Unit = { + val location = sourceInfo.makeMessage(value => s" $value") + require(open, s"StateMachine definition has already finished$location.") + require(!inStateBody, s"StateMachine on blocks must not be nested$location.") + machine.checkState(s) + require(defined.add(s.litValue), s"StateMachine state $s is defined more than once$location.") + + val scope = new StateScope[S](machine) + inStateBody = true + try { + when(machine.stateReg === s) { + body(using scope) + } + } finally { + scope.close() + inStateBody = false + } + } + + private[StateMachine] def close(): Unit = open = false + } + + /** Context supplied only while elaborating an on block. */ + final class StateScope[S <: EnumType] private[StateMachine] (machine: StateMachine[S]) { + private var open = true + + private[StateMachine] def transition(target: S)(using sourceInfo: SourceInfo): Unit = { + require( + open, + s"StateMachine goto must execute while elaborating its on block${sourceInfo.makeMessage(value => s" $value")}.", + ) + machine.checkState(target) + machine.stateNext := target + } + + private[StateMachine] def close(): Unit = open = false + } +} + +export StateMachine.{goto, on} diff --git a/chipmunk/test/src/StateMachineSpec.scala b/chipmunk/test/src/StateMachineSpec.scala new file mode 100644 index 0000000..38f0db5 --- /dev/null +++ b/chipmunk/test/src/StateMachineSpec.scala @@ -0,0 +1,348 @@ +package chipmunk.test + +import chisel3.* +import circt.stage.ChiselStage + +import chipmunk.* + +private object StateMachineSpecDut { + object States extends ChiselEnum { + val Idle, Run, Done, Park = Value + } + + final class Harness extends Module { + import States.* + + val io = IO(new Bundle { + val start = Input(Bool()) + val abort = Input(Bool()) + val finish = Input(Bool()) + val clear = Input(Bool()) + val park = Input(Bool()) + val self = Input(Bool()) + val state = Output(States()) + val active = Output(Vec(States.all.size, Bool())) + val entering = Output(Vec(States.all.size, Bool())) + val leaving = Output(Vec(States.all.size, Bool())) + val valid = Output(Bool()) + val busy = Output(Bool()) + val done = Output(Bool()) + val count = Output(UInt(8.W)) + }) + + val count = RegInit(99.U(8.W)) + io.busy := false.B + io.done := false.B + + val fsm = StateMachine(States)(initial = Idle) { + on(Idle) { + when(io.start) { goto(Run) } + when(io.abort) { goto(Done) } + } + on(Run) { + io.busy := true.B + count := count + 1.U + when(io.abort) { goto(Idle) } + .elsewhen(io.finish) { goto(Done) } + .elsewhen(io.self) { goto(Run) } + } + on(Done) { + io.done := true.B + when(io.clear) { goto(Idle) } + .elsewhen(io.park) { goto(Park) } + } + // Park intentionally has no on block. + } + + when(fsm.entering(Run)) { count := 0.U } + + io.state := fsm.current + io.valid := fsm.valid + io.count := count + for ((state, index) <- States.all.zipWithIndex) { + io.active(index) := fsm.is(state) + io.entering(index) := fsm.entering(state) + io.leaving(index) := fsm.leaving(state) + } + } + + object SparseStates extends ChiselEnum { + val Idle = Value(3.U) + val Run = Value(9.U) + val Done = Value(17.U) + } + + final class SparseHarness extends Module { + import SparseStates.* + + val io = IO(new Bundle { + val finish = Input(Bool()) + val state = Output(SparseStates()) + val valid = Output(Bool()) + }) + + val fsm = StateMachine(SparseStates)(initial = Run) { + on(Run) { when(io.finish) { goto(Done) } } + } + io.state := fsm.current + io.valid := fsm.valid + } + + final class InvalidHarness(mode: String) extends Module { + val io = IO(new Bundle { + val dynamic = Input(States()) + val state = Output(States()) + }) + + var later: () => Unit = () => () + val initial = if (mode == "dynamic initial") io.dynamic else States.Idle + val fsm = StateMachine(States)(initial = initial) { + mode match { + case "duplicate state" => + on(States.Idle) {} + on(States.Idle) {} + case "nested on" => + on(States.Idle) { on(States.Run) {} } + case "dynamic state" => + on(io.dynamic) {} + case "dynamic target" => + on(States.Idle) { goto(io.dynamic) } + case "escaped goto" => + on(States.Idle) { later = () => goto(States.Run) } + case "dynamic initial" => () + case _ => throw new IllegalArgumentException(s"Unknown test mode: $mode") + } + } + if (mode == "escaped goto") later() + io.state := fsm.current + } +} + +class StateMachineSpec extends ChipmunkFlatSpec { + import StateMachineSpecDut.* + import States.* + + private def initialize(dut: Harness): Unit = { + dut.io.start #= false.B + dut.io.abort #= false.B + dut.io.finish #= false.B + dut.io.clear #= false.B + dut.io.park #= false.B + dut.io.self #= false.B + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + } + + private def expectState(dut: Harness, expected: States.Type): Unit = { + dut.io.state expect expected + dut.io.valid expect true.B + dut.io.busy expect (expected == Run).B + dut.io.done expect (expected == Done).B + for ((state, index) <- States.all.zipWithIndex) { + dut.io.active(index) expect (state == expected).B + } + } + + private def expectEvents( + dut: Harness, + entering: Option[States.Type] = None, + leaving: Option[States.Type] = None, + ): Unit = { + for ((state, index) <- States.all.zipWithIndex) { + dut.io.entering(index) expect entering.contains(state).B + dut.io.leaving(index) expect leaving.contains(state).B + } + } + + private def startRun(dut: Harness): Unit = { + dut.io.start #= true.B + dut.clock.step() + dut.io.start #= false.B + expectState(dut, Run) + dut.io.count expect 0.U + } + + "StateMachine" should "reset to the initial state and hold without a transition" in { + simulate(new Harness) { dut => + initialize(dut) + expectState(dut, Idle) + expectEvents(dut) + dut.clock.step(5) + expectState(dut, Idle) + expectEvents(dut) + dut.io.count expect 99.U + } + } + + it should "change state on clock edges and expose transition events before those edges" in { + simulate(new Harness) { dut => + initialize(dut) + + dut.io.start #= true.B + expectState(dut, Idle) + expectEvents(dut, entering = Some(Run), leaving = Some(Idle)) + dut.io.count expect 99.U + dut.clock.step() + dut.io.start #= false.B + expectState(dut, Run) + expectEvents(dut) + dut.io.count expect 0.U + + dut.clock.step(3) + expectState(dut, Run) + dut.io.count expect 3.U + + dut.io.finish #= true.B + expectState(dut, Run) + expectEvents(dut, entering = Some(Done), leaving = Some(Run)) + dut.clock.step() + dut.io.finish #= false.B + expectState(dut, Done) + expectEvents(dut) + dut.io.count expect 4.U + dut.clock.step(2) + expectState(dut, Done) + dut.io.count expect 4.U + + dut.io.clear #= true.B + expectState(dut, Done) + expectEvents(dut, entering = Some(Idle), leaving = Some(Done)) + dut.clock.step() + dut.io.clear #= false.B + expectState(dut, Idle) + expectEvents(dut) + } + } + + it should "produce no entry or exit event for a self transition" in { + simulate(new Harness) { dut => + initialize(dut) + startRun(dut) + dut.io.self #= true.B + expectEvents(dut) + dut.clock.step(3) + expectState(dut, Run) + expectEvents(dut) + dut.io.count expect 3.U + } + } + + it should "give the last active goto priority in separate when blocks" in { + simulate(new Harness) { dut => + initialize(dut) + dut.io.start #= true.B + dut.io.abort #= true.B + expectState(dut, Idle) + expectEvents(dut, entering = Some(Done), leaving = Some(Idle)) + dut.clock.step() + dut.io.start #= false.B + dut.io.abort #= false.B + expectState(dut, Done) + expectEvents(dut) + dut.io.count expect 99.U + } + } + + it should "preserve when and elsewhen priority" in { + simulate(new Harness) { dut => + initialize(dut) + startRun(dut) + dut.io.abort #= true.B + dut.io.finish #= true.B + expectEvents(dut, entering = Some(Idle), leaving = Some(Run)) + dut.clock.step() + dut.io.abort #= false.B + dut.io.finish #= false.B + expectState(dut, Idle) + expectEvents(dut) + } + } + + it should "hold a state whose on block is omitted" in { + simulate(new Harness) { dut => + initialize(dut) + startRun(dut) + dut.io.finish #= true.B + dut.clock.step() + dut.io.finish #= false.B + expectState(dut, Done) + dut.io.park #= true.B + expectEvents(dut, entering = Some(Park), leaving = Some(Done)) + dut.clock.step() + expectState(dut, Park) + + dut.io.start #= true.B + dut.io.abort #= true.B + dut.io.finish #= true.B + dut.io.clear #= true.B + dut.io.self #= true.B + dut.clock.step(4) + expectState(dut, Park) + expectEvents(dut) + } + } + + it should "keep synchronous reset priority without masking combinational transition events" in { + simulate(new Harness) { dut => + initialize(dut) + startRun(dut) + dut.clock.step(2) + dut.io.count expect 2.U + + dut.reset #= true.B + dut.io.start #= true.B + expectState(dut, Run) // Synchronous reset takes effect at the next edge. + dut.clock.step() + expectState(dut, Idle) + dut.io.count expect 99.U + expectEvents(dut, entering = Some(Run), leaving = Some(Idle)) + dut.clock.step(3) + expectState(dut, Idle) + dut.io.count expect 99.U + expectEvents(dut, entering = Some(Run), leaving = Some(Idle)) + + dut.reset #= false.B + dut.clock.step() + dut.io.start #= false.B + expectState(dut, Run) + dut.io.count expect 0.U + expectEvents(dut) + } + } + + it should "support sparse ChiselEnum encodings and a non-first initial state" in { + simulate(new SparseHarness) { dut => + dut.io.finish #= false.B + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + dut.io.state expect SparseStates.Run + dut.io.valid expect true.B + dut.clock.step(2) + dut.io.state expect SparseStates.Run + dut.io.finish #= true.B + dut.clock.step() + dut.io.state expect SparseStates.Done + dut.io.valid expect true.B + } + } + + private val invalidCases = Seq( + "duplicate state" -> "defined more than once", + "nested on" -> "must not be nested", + "dynamic initial" -> "initial state must be a declared enum literal", + "dynamic state" -> "requires a declared enum literal", + "dynamic target" -> "requires a declared enum literal", + "escaped goto" -> "goto must execute while elaborating its on block", + ) + + for ((mode, message) <- invalidCases) { + it should s"reject $mode during elaboration" in { + val error = intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new InvalidHarness(mode)) + } + error.getMessage should include(message) + } + } +} From 32909a7464042a870a75615de5d0c79aabe26b3c Mon Sep 17 00:00:00 2001 From: zhutmost Date: Sun, 4 Oct 2026 00:31:58 +0800 Subject: [PATCH 08/26] feat: add Flow & Stream interfaces --- chipmunk/src/stream/FlowIO.scala | 219 +++++++++++++ chipmunk/src/stream/StreamDelay.scala | 133 ++++++++ chipmunk/src/stream/StreamIO.scala | 443 ++++++++++++++++++++++++++ 3 files changed, 795 insertions(+) create mode 100644 chipmunk/src/stream/FlowIO.scala create mode 100644 chipmunk/src/stream/StreamDelay.scala create mode 100644 chipmunk/src/stream/StreamIO.scala diff --git a/chipmunk/src/stream/FlowIO.scala b/chipmunk/src/stream/FlowIO.scala new file mode 100644 index 0000000..b66e7c3 --- /dev/null +++ b/chipmunk/src/stream/FlowIO.scala @@ -0,0 +1,219 @@ +package chipmunk +package stream + +import chisel3.* +import chisel3.experimental.{requireIsChiselType, requireIsHardware, SourceInfo} +import chisel3.experimental.BundleLiterals.* +import chisel3.util.{RegEnable, Valid} + +import chipmunk.{EmptyBundle, IsMasterSlave} + +/** A valid-only interface extending Chisel's [[ValidIO]]. + * + * Each cycle with `valid` asserted represents one transaction. There is no backpressure: the receiver must accept + * every valid transaction. Payload `bits` is meaningful only when `valid` is asserted. + */ +class FlowIO[T <: Data](gen: T) extends Valid[T](gen) with IsMasterSlave { + override def isMaster: Boolean = true + + /** Connect from `that` and returns `that` for right-to-left chaining. */ + def <<(that: FlowIO[T])(using SourceInfo): FlowIO[T] = { + connectFrom(that) + that + } + + /** Connect to `that` and return `that` for left-to-right chaining. */ + def >>(that: FlowIO[T])(using SourceInfo): FlowIO[T] = { + that.connectFrom(this) + that + } + + /** Connect from `that` through [[pipeForward]], cutting the forward `valid`/`bits` paths with registers. */ + def <-<(that: FlowIO[T])(using SourceInfo): FlowIO[T] = { + connectFrom(that.pipeForward()) + that + } + + /** Connect to `that` through [[pipeForward]], cutting the forward `valid`/`bits` paths with registers. */ + def >->(that: FlowIO[T])(using SourceInfo): FlowIO[T] = { + that.connectFrom(pipeForward()) + that + } + + /** Connect `valid` from `that`, without driving `bits`. */ + infix def handshakeFrom[U <: Data](that: Valid[U])(using SourceInfo): Unit = { + valid := that.valid + } + + /** Connect `valid` and `bits` from `that`. */ + infix def connectFrom(that: Valid[T])(using SourceInfo): Unit = { + handshakeFrom(that) + bits := that.bits + } + + /** Transform the payload `bits` using `f` without changing handshake timing. + * + * The returned interface is read-only. + * + * @param f + * The payload mapping function, which is evaluated once during elaboration and must return hardware. + * @example + * {{{ + * val oldFlow = Wire(Flow(UInt(8.W))) + * val newFlow = oldFlow.payloadMap(x => { + * val y = Wire(Bool()) + * y := (x + 1.U) >= 3.U + * y + * }) + * }}} + */ + def payloadMap[U <: Data](f: T => U)(using SourceInfo): FlowIO[U] = { + val payload = f(bits) + requireIsHardware(payload) + + val ret = Wire(Flow(chiselTypeOf(payload))) + ret handshakeFrom this + ret.bits := payload + ret.readOnly + } + + /** Apply [[payloadMap]], returning a read-only FlowIO. */ + override def map[U <: Data](f: T => U): FlowIO[U] = payloadMap(f) + + /** Replace the payload `bits` with `p` without changing handshake timing. + * + * The returned interface is read-only. + * + * @param p + * The new hardware payload. + * @example + * {{{ + * val oldFlow = Wire(Flow(UInt(8.W))) + * val newFlow = oldFlow.payloadReplace(12.S(8.W)) + * }}} + */ + def payloadReplace[U <: Data](p: U)(using SourceInfo): FlowIO[U] = { + payloadMap(_ => p) + } + + /** Cast the payload `bits` to another type `gen` without changing handshake timing. + * + * This is a bit reinterpretation, not an arithmetic conversion. To replace the payload with other hardware signals, + * use [[payloadReplace]] or [[payloadMap]]. The returned interface is read-only. + * + * @param gen + * Unbound target payload type. + * @param checkWidth + * If true, require known and equal source and target widths. If false, the payload may be padded or truncated. + * @example + * {{{ + * val oldFlow = Wire(Flow(UInt(8.W))) + * val newFlow = oldFlow.payloadCast(SInt(8.W)) + * }}} + */ + def payloadCast[U <: Data](gen: U, checkWidth: Boolean = false)(using SourceInfo): FlowIO[U] = { + requireIsChiselType(gen) + + if (checkWidth) { + require(bits.isWidthKnown && gen.isWidthKnown, "Payload width checking requires known source and target widths.") + require(bits.getWidth == gen.getWidth, s"Payload width mismatch: ${bits.getWidth} != ${gen.getWidth}") + } + + payloadMap(_.asTypeOf(gen)) + } + + /** Return this interface unchanged, without adding hardware. */ + def pipePassThrough(): FlowIO[T] = this + + /** Cut the forward `valid`/`bits` paths with a one-entry buffer. + * + * Minimum latency: one cycle. Maximum throughput: one transaction per cycle. For an N-bit payload, uses N + 1 + * register bits. Only `valid` is reset to false; `bits` is updated on valid input cycles. + */ + def pipeForward()(using SourceInfo): FlowIO[T] = { + val ret = Wire(Flow(chiselTypeOf(bits))) + ret.valid := RegNext(valid, false.B) + ret.bits := RegEnable(bits, valid) + ret + } + + /** Insert `n` forward pipeline stages using [[pipeForward]]. + * + * Adds `n` cycles of latency. If `n` is zero, return this interface unchanged. + * + * @param n + * Number of pipeline stages; must be nonnegative. + */ + def stage(n: Int = 1)(using SourceInfo): FlowIO[T] = { + require(n >= 0, "The number of pipeline stages must be nonnegative.") + + var ret = this + for (_ <- 0 until n) { + ret = ret.pipeForward() + } + ret + } + + /** Drop transactions when `cond` is false. */ + def takeWhen(cond: Bool)(using SourceInfo): FlowIO[T] = { + val ret = Wire(Flow(chiselTypeOf(bits))) + ret.valid := valid && cond + ret.bits := bits + ret + } + + /** Drop transactions when `cond` is true. */ + def throwWhen(cond: Bool)(using SourceInfo): FlowIO[T] = { + takeWhen(!cond) + } + + /** Convert this Flow to a Stream by forwarding `valid` and `bits`. + * + * This method adds no buffering and cannot backpressure the Flow. The consumer must drive `ready` high whenever + * `valid` is high to avoid losing transactions. + */ + def asStream(using SourceInfo): StreamIO[T] = { + val ret = Wire(Stream(chiselTypeOf(bits))) + ret.valid := valid + ret.bits := bits + ret + } +} + +/** Factories for FlowIO types and connected interfaces. */ +object Flow { + + /** Create an unbound interface type, suitable for IO, Wire, and Reg. */ + def apply[T <: Data](gen: T): FlowIO[T] = new FlowIO(gen) + + /** Create an unbound interface type with no payload. */ + def apply(): FlowIO[EmptyBundle] = new FlowIO(new EmptyBundle) + + /** Create an unbound interface type with no payload. */ + def empty: FlowIO[EmptyBundle] = apply() + + /** Create an unbound interface type from the payload type of existing hardware. */ + def like[T <: Data](source: Valid[T]): FlowIO[T] = { + requireIsHardware(source) + apply(chiselTypeOf(source.bits)) + } + + /** Create and connect a FlowIO wire from existing valid-only hardware. */ + def from[T <: Data](source: Valid[T])(using SourceInfo): FlowIO[T] = { + val ret = Wire(like(source)) + ret.connectFrom(source) + ret + } +} + +/** Factory for writable FlowIO registers. */ +object RegFlow { + + /** Create a writable FlowIO register. + * + * Only `valid` is reset to false; `bits` is not reset. Fields retain their values unless assigned. + */ + def apply[T <: Data](gen: T)(using SourceInfo): FlowIO[T] = { + RegInit(Flow(gen).Lit(_.valid -> false.B)) + } +} diff --git a/chipmunk/src/stream/StreamDelay.scala b/chipmunk/src/stream/StreamDelay.scala new file mode 100644 index 0000000..62ef269 --- /dev/null +++ b/chipmunk/src/stream/StreamDelay.scala @@ -0,0 +1,133 @@ +package chipmunk +package stream + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.log2Ceil +import chisel3.util.random.LFSR + +import chipmunk.{goto, on, Master, Slave, StateMachine} + +/** Fixed and pseudorandom delays for StreamIO transactions. */ +object StreamDelay { + + /** Delay each transaction's output presentation by `cycles` clock cycles. + * + * Input is held until output transfers; consumer stalls may extend the transfer delay. Zero returns `in` unchanged. + */ + def fixed[T <: Data](in: StreamIO[T], cycles: Int)(using SourceInfo): StreamIO[T] = { + require(cycles >= 0, "Delay cycles must be nonnegative.") + + if (cycles == 0) { + in + } else { + val delay = Module(new StreamDelay(chiselTypeOf(in.bits), log2Ceil(BigInt(cycles) + 1))) + delay.io.in << in + delay.io.targetDelay := cycles.U + delay.io.out + } + } + + /** Choose a pseudorandom presentation delay in the inclusive range `[minCycles, maxCycles]`. + * + * A new delay is selected for each transaction, including zero-delay transfers. The sequence repeats after reset and + * its distribution is not necessarily uniform. Consumer stalls may extend the transfer delay. + */ + def random[T <: Data](in: StreamIO[T], maxCycles: Int, minCycles: Int = 0)(using SourceInfo): StreamIO[T] = { + require(minCycles >= 0, "Minimum delay must be nonnegative.") + require(maxCycles >= minCycles, "Maximum delay must be at least the minimum delay.") + + if (maxCycles == minCycles) { + fixed(in, maxCycles) + } else { + val delay = Module(new StreamDelay(chiselTypeOf(in.bits), log2Ceil(BigInt(maxCycles) + 1))) + val choices = BigInt(maxCycles) - minCycles + 1 + val randomWidth = math.max(2, log2Ceil(choices + 1)) + val value = LFSR(randomWidth, increment = delay.io.targetDelayUpdate, seed = Some(BigInt(1))) + + // XOR LFSRs exclude zero. Subtract one so the minimum delay is reachable. + // The chosen width keeps candidate below 2 * choices, so one fold suffices. + val candidate = value - 1.U + val offset = Mux(candidate >= choices.U, candidate - choices.U, candidate) + + delay.io.in << in + delay.io.targetDelay := offset +& minCycles.U + delay.io.out + } + } +} + +/** Delay output presentation without buffering payload or accepting input early. + * + * A transaction first offered while idle in cycle `k` is presented at output in cycle `k + targetDelay`. The delay is + * sampled once per transaction; later changes affect the next transaction. Zero permits same-cycle transfer. The + * producer must hold `valid`/`bits` until transfer, including during the delay. Reset returns to idle and restarts the + * delay of any still-offered transaction. + * + * @param gen + * payload type + * @param delayWidth + * width of the unsigned delay in cycles; must be positive + */ +class StreamDelay[T <: Data](gen: T, delayWidth: Int) extends Module { + require(delayWidth >= 1, "Delay width must be positive.") + + val io = IO(new Bundle { + val in = Slave(Stream(gen)) + val out = Master(Stream(gen)) + + /** Delay for the next transaction; sampled while idle when input is valid. */ + val targetDelay = Input(UInt(delayWidth.W)) + + /** Pulse when a transaction's delay is selected, including zero and one cycle. */ + val targetDelayUpdate = Output(Bool()) + }) + + private object States extends ChiselEnum { + val Idle, Counting, Pending = Value + } + + // Loaded before entering Counting; its reset value is never observed. + private val remaining = Reg(UInt(delayWidth.W)) + + io.out.bits := io.in.bits + io.out.valid := false.B + io.in.ready := io.out.fire + io.targetDelayUpdate := false.B + + StateMachine(States)(States.Idle) { + on(States.Idle) { + io.out.valid := io.in.valid && io.targetDelay === 0.U + + when(io.in.valid) { + io.targetDelayUpdate := true.B + + when(io.targetDelay === 0.U) { + when(!io.out.ready) { + goto(States.Pending) + } + }.elsewhen(io.targetDelay === 1.U) { + goto(States.Pending) + }.otherwise { + remaining := io.targetDelay - 1.U + goto(States.Counting) + } + } + } + + on(States.Counting) { + when(remaining === 1.U) { + goto(States.Pending) + }.otherwise { + remaining := remaining - 1.U + } + } + + on(States.Pending) { + io.out.valid := true.B + when(io.out.fire) { + goto(States.Idle) + } + } + } +} diff --git a/chipmunk/src/stream/StreamIO.scala b/chipmunk/src/stream/StreamIO.scala new file mode 100644 index 0000000..5b0fc2d --- /dev/null +++ b/chipmunk/src/stream/StreamIO.scala @@ -0,0 +1,443 @@ +package chipmunk +package stream + +import chisel3.* +import chisel3.experimental.{requireIsChiselType, requireIsHardware, SourceInfo} +import chisel3.util.{DecoupledIO, Queue, ReadyValidIO, RegEnable} + +import chipmunk.{EmptyBundle, IsMasterSlave} + +/** A ready/valid interface extending Chisel's [[DecoupledIO]]. + * + * A transaction transfers when `valid && ready`. Producers must keep offered `valid`/`bits` stable until transfer. + * Forward `valid` must not depend combinationally on backward `ready`. Payload `bits` is meaningful only when `valid` + * is asserted. + */ +class StreamIO[T <: Data](gen: T) extends DecoupledIO[T](gen) with IsMasterSlave { + override def isMaster: Boolean = true + + /** Connect from `that` and return `that` for right-to-left chaining. */ + def <<(that: StreamIO[T])(using SourceInfo): StreamIO[T] = { + connectFrom(that) + that + } + + /** Connect to `that` and return `that` for left-to-right chaining. */ + def >>(that: StreamIO[T])(using SourceInfo): StreamIO[T] = { + that.connectFrom(this) + that + } + + /** Connect from `that` through [[pipeForward]], where the forward `valid`/`bits` paths are cut by registers. */ + def <-<(that: StreamIO[T])(using SourceInfo): StreamIO[T] = { + connectFrom(that.pipeForward()) + that + } + + /** Connect to `that` through [[pipeForward]], where the forward `valid`/`bits` paths are cut by registers. */ + def >->(that: StreamIO[T])(using SourceInfo): StreamIO[T] = { + that connectFrom pipeForward() + that + } + + /** Connect from `that` through [[pipeBackward]], where the backward `ready` path is cut by registers. */ + def <|<(that: StreamIO[T])(using SourceInfo): StreamIO[T] = { + connectFrom(that.pipeBackward()) + that + } + + /** Connect to `that` through [[pipeBackward]], where the backward `ready` path is cut by registers. */ + def >|>(that: StreamIO[T])(using SourceInfo): StreamIO[T] = { + that connectFrom pipeBackward() + that + } + + /** Connect from `that` through [[pipeAll]], where the `valid`/`ready`/`bits` paths are cut by registers. */ + def <+<(that: StreamIO[T])(using SourceInfo): StreamIO[T] = { + connectFrom(that.pipeAll()) + that + } + + /** Connect to `that` through [[pipeAll]], where the `valid`/`ready`/`bits` paths are cut by registers. */ + def >+>(that: StreamIO[T])(using SourceInfo): StreamIO[T] = { + that connectFrom pipeAll() + that + } + + /** Connect `valid` from `that` and `ready` to `that`, leaving the payload `bits` unconnected. */ + infix def handshakeFrom[U <: Data](that: ReadyValidIO[U])(using SourceInfo): Unit = { + valid := that.valid + that.ready := ready + } + + /** Connect `valid`/`bits` from `that` and `ready` to `that`. */ + infix def connectFrom(that: ReadyValidIO[T])(using SourceInfo): Unit = { + handshakeFrom(that) + bits := that.bits + } + + /** Transform the payload `bits` to something else through function `f` without changing handshake timing. + * + * @param f + * The payload mapping function, which is evaluated once during elaboration and must return hardware. Its result + * must remain stable while the output is stalled. + * @example + * {{{ + * val oldStream = Wire(Stream(UInt(8.W))) + * val newStream = oldStream.payloadMap(x => { + * val y = Wire(Bool()) + * y := (x + 1.U) >= 3.U + * y + * }) + * }}} + */ + def payloadMap[U <: Data](f: T => U)(using SourceInfo): StreamIO[U] = { + val payload = f(bits) + requireIsHardware(payload) + + val ret = Wire(Stream(chiselTypeOf(payload))) + ret handshakeFrom this + ret.bits := payload + ret + } + + /** Apply [[payloadMap]], preserving StreamIO as the inherited `map` return type. */ + override def map[U <: Data](f: T => U): StreamIO[U] = payloadMap(f) + + /** Replace the payload `bits` with `p` without changing handshake timing. + * + * @param p + * The new hardware payload that remains stable while the output is stalled. + * @example + * {{{ + * val oldStream = Wire(Stream(UInt(8.W))) + * val newStream = oldStream.payloadReplace(12.S(8.W)) + * }}} + */ + def payloadReplace[U <: Data](p: U)(using SourceInfo): StreamIO[U] = { + payloadMap(_ => p) + } + + /** Cast the payload `bits` to another type `gen` without changing handshake timing. + * + * Note that this is not an arithmetic conversion. If you want to replace the payload with other hardware signals, + * use [[payloadReplace]] or [[payloadMap]]. + * + * @param gen + * Unbound target payload type. + * @param checkWidth + * Require known, equal source and target widths. If false, Chisel may pad or truncate bits. + * @example + * {{{ + * val oldStream = Wire(Stream(UInt(8.W))) + * val newStream = oldStream.payloadCast(SInt(8.W)) + * }}} + */ + def payloadCast[U <: Data](gen: U, checkWidth: Boolean = false)(using SourceInfo): StreamIO[U] = { + requireIsChiselType(gen) + + if (checkWidth) { + require(bits.isWidthKnown && gen.isWidthKnown, "Payload width checking requires known source and target widths.") + require(bits.getWidth == gen.getWidth, s"Payload width mismatch: ${bits.getWidth} != ${gen.getWidth}") + } + + payloadMap(_.asTypeOf(gen)) + } + + /** True if a transaction is offered but the ready is still low (i.e. `valid && !ready`). */ + def isPending(using SourceInfo): Bool = valid && !ready + + /** True if the consumer is ready but no transaction is offered (i.e. `!valid && ready`). */ + def isStarving(using SourceInfo): Bool = !valid && ready + + /** Return this interface unchanged, without adding hardware. */ + def pipePassThrough(): StreamIO[T] = this + + /** Cut the forward `valid`/`bits` paths with a one-entry buffer. + * + * Minimum latency: one cycle. Maximum throughput: one transaction per cycle. For an N-bit payload, uses N + 1 + * register bits. + * + * @param bubbleCollapse + * If true, an empty buffer can accept input even when output `ready` is low. If false, input `ready` follows + * output `ready`, so the consumer must assert `ready` without waiting for output `valid`. + */ + def pipeForward(bubbleCollapse: Boolean = true)(using SourceInfo): StreamIO[T] = { + val ret = Wire(Stream(chiselTypeOf(bits))) + val rValid = RegInit(false.B) + val canAccept = if (bubbleCollapse) ret.ready || !rValid else ret.ready + + ready := canAccept + when(canAccept) { + rValid := valid + } + + ret.valid := rValid + ret.bits := RegEnable(bits, this.fire) + ret + } + + /** Cut the backward `ready` path with a one-entry buffer. + * + * Minimum latency: zero cycles. Maximum throughput: one transaction per cycle. When a buffered transaction + * transfers, the input remains stalled for that cycle. For an N-bit payload, uses N + 1 register bits and an N-bit + * 2-to-1 multiplexer. + */ + def pipeBackward()(using SourceInfo): StreamIO[T] = { + val ret = Wire(Stream(chiselTypeOf(bits))) + val rValid = RegInit(false.B) + val rBits = RegEnable(bits, this.fire && !ret.ready) + + ready := !rValid + when(ret.ready) { + rValid := false.B + }.elsewhen(this.fire) { + rValid := true.B + } + + ret.valid := rValid || valid + ret.bits := Mux(rValid, rBits, bits) + ret + } + + /** Cut the forward `valid`/`bits` and backward `ready` paths with a one-entry buffer. Equivalent to + * `pipeForward().pipeBackward()`. + * + * Minimum latency: one cycle. Maximum throughput: one transaction per cycle. For an N-bit payload, uses 2N + 2 + * register bits and an N-bit 2-to-1 multiplexer. + */ + def pipeAll()(using SourceInfo): StreamIO[T] = pipeForward().pipeBackward() + + /** Cut the both forward `valid`/`bits` and backward `ready` paths with a one-entry buffer that cannot refill while + * draining. + * + * Minimum latency: one cycle. Maximum throughput: one transaction every two cycles. The buffer cannot accept input + * in the cycle its stored transaction transfers. For an N-bit payload, uses N + 1 register bits. + * + * If the bandwidth loss is unacceptable, consider using [[pipeAll]] instead. + */ + def pipeSimple()(using SourceInfo): StreamIO[T] = { + val ret = Wire(Stream(chiselTypeOf(bits))) + val rValid = RegInit(false.B) + + ready := !rValid + when(ret.fire) { + rValid := false.B + }.elsewhen(this.fire) { + rValid := true.B + } + + ret.valid := rValid + ret.bits := RegEnable(bits, this.fire) + ret + } + + /** Cut the `valid` path with a register. + * + * Output `valid` becomes high one cycle after input `valid` is first observed. Input and output transfer in the same + * cycle. Maximum throughput: one transaction every two cycles. Uses one register bit and does not store the payload. + */ + def pipeValid()(using SourceInfo): StreamIO[T] = { + val ret = Wire(Stream(chiselTypeOf(bits))) + val rValid = RegInit(false.B) + + when(ret.fire) { + rValid := false.B + }.elsewhen(valid) { + rValid := true.B + } + + ready := ret.fire + ret.valid := rValid + ret.bits := bits + ret + } + + /** Insert `n` forward stages; zero returns this interface unchanged. */ + def stage(n: Int = 1)(using SourceInfo): StreamIO[T] = { + require(n >= 0, "The number of pipeline stages must be nonnegative.") + + var ret = this + for (_ <- 0 until n) { + ret = ret.pipeForward() + } + ret + } + + /** Block transactions when `cond` is False. + * + * `cond` must remain unchanged while output `valid` is high and output `ready` is low. + */ + def continueWhen(cond: => Bool)(using SourceInfo): StreamIO[T] = { + val allow = cond + val ret = Wire(Stream(chiselTypeOf(bits))) + ret.valid := valid && allow + ready := ret.ready && allow + ret.bits := bits + ret + } + + /** Block transactions when `cond` is True. + * + * `cond` must remain unchanged while output `valid` is high and output `ready` is low. + */ + def haltWhen(cond: => Bool)(using SourceInfo): StreamIO[T] = continueWhen(!cond) + + /** Drop transactions when `cond` is True. + * + * `cond` must remain unchanged while output `valid` is high and output `ready` is low. + */ + def throwWhen(cond: => Bool)(using SourceInfo): StreamIO[T] = { + val drop = cond + val ret = Wire(Stream(chiselTypeOf(bits))) + ret.valid := valid && !drop + ready := ret.ready || drop + ret.bits := bits + ret + } + + /** Drop transactions when `cond` is False. + * + * `cond` must remain unchanged while output `valid` is high and output `ready` is low. + */ + def takeWhen(cond: => Bool)(using SourceInfo): StreamIO[T] = throwWhen(!cond) + + /** Convert this Stream to a Flow by forwarding `valid` and `bits`. + * + * This method does not drive `ready`. A stalled Stream transaction remains valid on the Flow and may be consumed + * repeatedly. Use [[toFlow]] to forward only completed Stream transfers. + * + * The returned Flow is read-only. + */ + def asFlow(using SourceInfo): FlowIO[T] = { + val ret = Wire(Flow(chiselTypeOf(bits))) + ret.valid := valid + ret.bits := bits + ret.readOnly + } + + /** Convert this Stream to a Flow by forwarding `fire` as `valid` and forwarding `bits`. + * + * The Flow is valid only when the Stream transfers. The returned Flow is read-only. + * + * @param readyFreeRun + * If true, drive this Stream's `ready` high. If false, leave its driver unchanged. + */ + def toFlow(readyFreeRun: Boolean = false)(using SourceInfo): FlowIO[T] = { + if (readyFreeRun) { + ready := true.B + } + + val ret = Wire(Flow(chiselTypeOf(bits))) + ret.valid := this.fire + ret.bits := bits + ret.readOnly + } + + /** Buffer transactions in a Chisel [[Queue]]; zero depth returns this interface unchanged. + * + * @param queueSize + * Number of storage entries, i.e., the queue depth. + * @param queueFlow + * Whether the inputs can be consumed on the same cycle (the inputs "flow" through the queue immediately). The + * `valid` signals are coupled. + * @param queuePipe + * Whether a single entry queue can run at full throughput (like a pipeline). The `ready` signals are + * combinatorial-ly coupled. + * @param useSyncReadMem + * use SyncReadMem for payload storage + * @param flush + * clear buffered transactions on the active clock edge; requires positive depth + */ + def queue( + queueSize: Int, + queueFlow: Boolean = false, + queuePipe: Boolean = false, + useSyncReadMem: Boolean = false, + flush: Option[Bool] = None, + )(using SourceInfo): StreamIO[T] = { + require(queueSize >= 0, "Queue depth must be nonnegative.") + require(queueSize > 0 || flush.isEmpty, "A zero-depth queue cannot be flushed.") + + if (queueSize == 0) { + this + } else { + Stream.from( + Queue( + this, + entries = queueSize, + pipe = queuePipe, + flow = queueFlow, + useSyncReadMem = useSyncReadMem, + flush = flush, + ) + ) + } + } + + /** Delay each transaction by `cycles` clock cycles. + * + * The delay starts when input `valid` is first observed. Input and output transfer in the same cycle; backpressure + * may delay the transfer further. The payload is not buffered. + * + * If `cycles` is zero, return this interface unchanged. + * + * @param cycles + * Delay in clock cycles; must be nonnegative. + */ + + def delayFixed(cycles: Int)(using SourceInfo): StreamIO[T] = StreamDelay.fixed(this, cycles) + + /** Delay each transaction by a pseudorandom number of clock cycles. + * + * A delay between `minCycles` and `maxCycles`, inclusive, is selected when input `valid` is first observed. The + * distribution is not guaranteed to be uniform. Input and output transfer in the same cycle; backpressure may delay + * the transfer further. The payload is not buffered. + * + * If the bounds are equal, this is an equivalent to `delayFixed(maxCycles)`. + * + * @param maxCycles + * Maximum delay in clock cycles; must be greater than or equal to `minCycles`. + * @param minCycles + * Minimum delay in clock cycles; must be nonnegative. + */ + def delayRandom(maxCycles: Int, minCycles: Int = 0)(using SourceInfo): StreamIO[T] = { + StreamDelay.random(this, maxCycles, minCycles) + } +} + +/** StreamIO factory. */ +object Stream { + + /** Create an unbound interface type, suitable for IO and Wire. */ + def apply[T <: Data](gen: T): StreamIO[T] = new StreamIO(gen) + + /** Create an unbound interface type with no payload. */ + def apply(): StreamIO[EmptyBundle] = new StreamIO(new EmptyBundle) + + /** Create an unbound interface type with no payload. */ + def empty: StreamIO[EmptyBundle] = apply() + + /** Create an unbound interface type from the payload type of existing hardware. */ + def like[T <: Data](source: ReadyValidIO[T]): StreamIO[T] = { + requireIsHardware(source) + apply(chiselTypeOf(source.bits)) + } + + /** Wrap and connect existing ready/valid hardware without buffering. + * + * The source must already satisfy the StreamIO protocol; this adapter does not add stability guarantees. The + * consumer drives the returned `ready`. + */ + def from[T <: Data](source: ReadyValidIO[T])(using SourceInfo): StreamIO[T] = { + val ret = Wire(like(source)) + ret connectFrom source + ret + } +} + +extension [T <: Data](source: ReadyValidIO[T]) { + + /** Wrap and connect this interface as a StreamIO; equivalent to [[Stream.from]]. */ + def toStream(using SourceInfo): StreamIO[T] = Stream.from(source) +} From 5f4cadaa227820ad376f290d94385bc0888ce571 Mon Sep 17 00:00:00 2001 From: zhutmost Date: Sun, 4 Oct 2026 01:12:30 +0800 Subject: [PATCH 09/26] test: add comprehensive tests for FlowIO and StreamIO interfaces --- chipmunk/test/src/FlowIOSpec.scala | 338 ++++++++++++ chipmunk/test/src/StreamIOSpec.scala | 754 +++++++++++++++++++++++++++ 2 files changed, 1092 insertions(+) create mode 100644 chipmunk/test/src/FlowIOSpec.scala create mode 100644 chipmunk/test/src/StreamIOSpec.scala diff --git a/chipmunk/test/src/FlowIOSpec.scala b/chipmunk/test/src/FlowIOSpec.scala new file mode 100644 index 0000000..8e035b2 --- /dev/null +++ b/chipmunk/test/src/FlowIOSpec.scala @@ -0,0 +1,338 @@ +package chipmunk.test + +import scala.util.Random + +import chisel3.* +import chisel3.util.Valid +import circt.stage.ChiselStage + +import chipmunk.{Master, Slave} +import chipmunk.stream.* + +private object FlowIOSpecDut { + final class Payload extends Bundle { + val tag = UInt(3.W) + val data = UInt(8.W) + } + + final class CombinationalHarness extends Module { + val io = IO(new Bundle { + val in = Slave(Flow(UInt(8.W))) + val raw = Flipped(Valid(new Payload)) + val condition = Input(Bool()) + val replacement = Input(SInt(8.W)) + val mapped = Master(Flow(Bool())) + val inherited = Master(Flow(UInt(9.W))) + val replaced = Master(Flow(SInt(8.W))) + val signed = Master(Flow(SInt(8.W))) + val truncated = Master(Flow(UInt(4.W))) + val padded = Master(Flow(UInt(12.W))) + val nibbles = Master(Flow(Vec(2, UInt(4.W)))) + val taken = Master(Flow(UInt(8.W))) + val thrown = Master(Flow(UInt(8.W))) + val stream = Master(Stream(UInt(8.W))) + val adapted = Vec(3, Master(Flow(new Payload))) + val handshake = Master(Flow(UInt(8.W))) + val emptyIn = Vec(2, Slave(Flow())) + val emptyOut = Vec(2, Master(Flow.empty)) + }) + + var evaluations = 0 + io.mapped << io.in.payloadMap { value => + evaluations += 1 + value >= 128.U + } + require(evaluations == 1, "payloadMap must evaluate its callback once") + val inherited: FlowIO[UInt] = io.in.map(_ +& 1.U) + io.inherited << inherited + io.replaced << io.in.payloadReplace(io.replacement) + io.signed << io.in.payloadCast(SInt(8.W), checkWidth = true) + io.truncated << io.in.payloadCast(UInt(4.W)) + io.padded << io.in.payloadCast(UInt(12.W)) + io.nibbles << io.in.payloadCast(Vec(2, UInt(4.W)), checkWidth = true) + io.taken << io.in.takeWhen(io.condition) + io.thrown << io.in.throwWhen(io.condition) + io.stream << io.in.asStream + + io.adapted(0) << Flow.from(io.raw) + val similar = Wire(Flow.like(io.raw)) + similar connectFrom io.raw + io.adapted(1) << similar + io.adapted(2) connectFrom io.raw + io.handshake handshakeFrom io.raw + io.handshake.bits := 0xa5.U + for (index <- 0 until 2) io.emptyOut(index) << io.emptyIn(index) + } + + val paths = Seq( + "pass-through" -> 0, + "forward" -> 1, + "stage zero" -> 0, + "stage default" -> 1, + "stage three" -> 3, + "left chain" -> 0, + "right chain" -> 0, + "left pipe" -> 2, + "right pipe" -> 2, + ) + + final class PipelineHarness extends Module { + val io = IO(new Bundle { + val in = Slave(Flow(UInt(8.W))) + val out = Vec(paths.size, Master(Flow(UInt(8.W)))) + }) + + for (((name, _), index) <- paths.zipWithIndex) { + val out = io.out(index) + val middle = Wire(Flow(UInt(8.W))) + // Give the unused middle wire a driver for paths without a connection chain. + middle.valid := false.B + middle.bits := 0.U + name match { + case "pass-through" => + require(io.in.pipePassThrough() eq io.in) + out << io.in.pipePassThrough() + case "forward" => out << io.in.pipeForward() + case "stage zero" => + require(io.in.stage(0) eq io.in) + out << io.in.stage(0) + case "stage default" => out << io.in.stage() + case "stage three" => out << io.in.stage(3) + case "left chain" => require((out << middle << io.in) eq io.in) + case "right chain" => require((io.in >> middle >> out) eq out) + case "left pipe" => require((out <-< middle <-< io.in) eq io.in) + case "right pipe" => require((io.in >-> middle >-> out) eq out) + case _ => throw new IllegalArgumentException(name) + } + } + } + + final class RegisterHarness extends Module { + val io = IO(new Bundle { + val writeValid = Input(Bool()) + val writeBits = Input(Bool()) + val valid = Input(Bool()) + val bits = Input(UInt(8.W)) + val out = Master(Flow(UInt(8.W))) + }) + val stored = RegFlow(UInt(8.W)) + when(io.writeValid) { stored.valid := io.valid } + when(io.writeBits) { stored.bits := io.bits } + io.out << stored + } + + final class InvalidHarness(mode: String) extends Module { + val in = IO(Slave(Flow(UInt(8.W)))) + mode match { + case "negative stages" => in.stage(-1) + case "unequal widths" => in.payloadCast(UInt(7.W), checkWidth = true) + case "unknown width" => in.payloadCast(UInt(), checkWidth = true) + case "unknown source width" => + val unknown = Wire(Flow(UInt())) + unknown.payloadCast(UInt(8.W), checkWidth = true) + case "unbound mapping" => in.payloadMap(_ => UInt(8.W)) + case "unbound replacement" => in.payloadReplace(UInt(8.W)) + case "bound cast type" => in.payloadCast(in.bits) + case "unbound factory source" => Flow.like(Valid(UInt(8.W))) + case "write mapped" => in.payloadMap(_ + 1.U).bits := 0.U + case "write inherited" => in.map(_ + 1.U).valid := false.B + case "write replaced" => in.payloadReplace(0.U(8.W)).bits := 0.U + case "write cast" => in.payloadCast(UInt(8.W)).bits := 0.U + case _ => throw new IllegalArgumentException(mode) + } + } +} + +class FlowIOSpec extends ChipmunkFlatSpec { + import FlowIOSpecDut.* + + "FlowIO" should "transform, filter and adapt payloads without adding latency or backpressure" in { + simulate(new CombinationalHarness) { dut => + for { + value <- 0 until 256 + valid <- Seq(false, true) + } { + val condition = (value & 1) == 0 + val replacement = value - 128 + dut.io.in.valid #= valid.B + dut.io.in.bits #= value.U + dut.io.condition #= condition.B + dut.io.replacement #= replacement.S + dut.io.stream.ready #= condition.B + dut.io.raw.valid #= (!valid).B + dut.io.raw.bits.tag #= (value & 7).U + dut.io.raw.bits.data #= (255 - value).U + for (index <- 0 until 2) dut.io.emptyIn(index).valid #= (valid ^ (index == 1)).B + dut.clock.step(0) + + withClue(s"value=$value, valid=$valid: ") { + Seq( + dut.io.mapped.valid, + dut.io.inherited.valid, + dut.io.replaced.valid, + dut.io.signed.valid, + dut.io.truncated.valid, + dut.io.padded.valid, + dut.io.nibbles.valid, + dut.io.stream.valid, + ).foreach(_.expect(valid.B)) + if (valid) { + dut.io.mapped.bits expect (value >= 128).B + dut.io.inherited.bits expect (value + 1).U + dut.io.replaced.bits expect replacement.S + dut.io.signed.bits expect (if (value < 128) value else value - 256).S + dut.io.truncated.bits expect (value & 15).U + dut.io.padded.bits expect value.U + dut.io.nibbles.bits(0) expect (value & 15).U + dut.io.nibbles.bits(1) expect (value >>> 4).U + dut.io.stream.bits expect value.U + } + dut.io.taken.valid expect (valid && condition).B + dut.io.thrown.valid expect (valid && !condition).B + if (valid && condition) dut.io.taken.bits expect value.U + if (valid && !condition) dut.io.thrown.bits expect value.U + for (out <- dut.io.adapted) { + out.valid expect (!valid).B + if (!valid) { + out.bits.tag expect (value & 7).U + out.bits.data expect (255 - value).U + } + } + dut.io.handshake.valid expect (!valid).B + dut.io.handshake.bits expect 0xa5.U + for (index <- 0 until 2) dut.io.emptyOut(index).valid expect (valid ^ (index == 1)).B + } + } + } + } + + it should "preserve bubbles and full-rate traffic through pipelines and connection chains" in { + simulate(new PipelineHarness) { dut => + dut.io.in.valid #= false.B + dut.io.in.bits #= 0.U + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + val random = new Random(0xf10L) + var history = Vector.empty[Option[Int]] + val held = Array.fill[Option[Int]](paths.size)(None) + + for (cycle <- 0 until 160) { + val valid = cycle < 24 || (cycle < 150 && random.nextBoolean()) + val value = random.nextInt(256) + dut.io.in.valid #= valid.B + dut.io.in.bits #= value.U + dut.clock.step(0) + for (((name, latency), index) <- paths.zipWithIndex) { + val expected = if (latency == 0) Option.when(valid)(value) else history.lift(latency - 1).flatten + withClue(s"$name, cycle=$cycle: ") { + dut.io.out(index).valid expect expected.isDefined.B + expected.foreach { bits => + dut.io.out(index).bits expect bits.U + held(index) = Some(bits) + } + // Registered payloads hold the last valid value across bubbles; their reset value is unspecified. + if (latency > 0 && expected.isEmpty) held(index).foreach(bits => dut.io.out(index).bits.expect(bits.U)) + } + } + dut.clock.step() + history = (Option.when(valid)(value) +: history).take(3) + } + + // Reset while every stage contains a transaction, then check that no old valid escapes. + dut.io.in.valid #= true.B + dut.io.in.bits #= 0x5a.U + dut.clock.step(4) + dut.io.in.valid #= false.B + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + for (_ <- 0 until 4) { + dut.io.out.foreach(_.valid.expect(false.B)) + dut.clock.step() + } + } + } + + it should "reset RegFlow valid and retain independently written fields" in { + simulate(new RegisterHarness) { dut => + dut.io.writeValid #= false.B + dut.io.writeBits #= false.B + dut.io.valid #= false.B + dut.io.bits #= 0.U + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + dut.io.out.valid expect false.B + + dut.io.writeValid #= true.B + dut.io.writeBits #= true.B + dut.io.valid #= true.B + dut.io.bits #= 0x37.U + dut.clock.step() + dut.io.out.valid expect true.B + dut.io.out.bits expect 0x37.U + + dut.io.writeValid #= false.B + dut.io.bits #= 0xa9.U + dut.clock.step() + dut.io.out.valid expect true.B + dut.io.out.bits expect 0xa9.U + + dut.io.writeBits #= false.B + dut.io.bits #= 0xff.U + dut.io.valid #= false.B + dut.clock.step(3) + dut.io.out.valid expect true.B + dut.io.out.bits expect 0xa9.U + + dut.io.writeValid #= true.B + dut.clock.step() + dut.io.out.valid expect false.B + dut.io.out.bits expect 0xa9.U + dut.io.valid #= true.B + dut.clock.step() + dut.io.out.valid expect true.B + + dut.io.writeValid #= false.B + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + dut.io.out.valid expect false.B + dut.io.out.bits expect 0xa9.U + } + } + + for ( + (mode, message) <- Seq( + "negative stages" -> "nonnegative", + "unequal widths" -> "Payload width mismatch", + "unknown width" -> "known source and target widths", + "unknown source width" -> "known source and target widths", + ) + ) { + it should s"reject $mode during elaboration" in { + val error = intercept[IllegalArgumentException] { ChiselStage.emitCHIRRTL(new InvalidHarness(mode)) } + error.getMessage should include(message) + } + } + + for (mode <- Seq("unbound mapping", "unbound replacement", "unbound factory source")) { + it should s"reject $mode during elaboration" in { + intercept[ExpectedHardwareException] { ChiselStage.emitCHIRRTL(new InvalidHarness(mode)) } + } + } + + it should "require an unbound payload cast type" in { + intercept[ExpectedChiselTypeException] { ChiselStage.emitCHIRRTL(new InvalidHarness("bound cast type")) } + } + + for (mode <- Seq("write mapped", "write inherited", "write replaced", "write cast")) { + it should s"reject attempts to $mode read-only flows" in { + val error = intercept[ChiselException] { + ChiselStage.emitCHIRRTL(new InvalidHarness(mode), args = Array("--throw-on-first-error")) + } + error.getMessage should include("Cannot connect to read-only value") + } + } +} diff --git a/chipmunk/test/src/StreamIOSpec.scala b/chipmunk/test/src/StreamIOSpec.scala new file mode 100644 index 0000000..72c509a --- /dev/null +++ b/chipmunk/test/src/StreamIOSpec.scala @@ -0,0 +1,754 @@ +package chipmunk.test + +import scala.collection.mutable +import scala.util.Random + +import chisel3.* +import chisel3.util.Decoupled +import circt.stage.ChiselStage + +import chipmunk.{Master, Slave} +import chipmunk.stream.* + +private object StreamIOSpecDut { + final class Payload extends Bundle { + val tag = UInt(3.W) + val data = UInt(8.W) + } + + final class CombinationalHarness extends Module { + val io = IO(new Bundle { + val in = Vec(7, Slave(Stream(UInt(8.W)))) + val replacement = Input(SInt(8.W)) + val mapped = Master(Stream(Bool())) + val inherited = Master(Stream(UInt(9.W))) + val replaced = Master(Stream(SInt(8.W))) + val signed = Master(Stream(SInt(8.W))) + val truncated = Master(Stream(UInt(4.W))) + val padded = Master(Stream(UInt(12.W))) + val nibbles = Master(Stream(Vec(2, UInt(4.W)))) + val filterIn = Vec(4, Slave(Stream(UInt(8.W)))) + val filterOut = Vec(4, Master(Stream(UInt(8.W)))) + val condition = Input(Bool()) + val statusIn = Slave(Stream(UInt(8.W))) + val statusReady = Input(Bool()) + val pending = Output(Bool()) + val starving = Output(Bool()) + val fire = Output(Bool()) + val offered = Master(Flow(UInt(8.W))) + val transferred = Master(Flow(UInt(8.W))) + val freeIn = Slave(Stream(UInt(8.W))) + val freeFlow = Master(Flow(UInt(8.W))) + val raw = Vec(3, Flipped(Decoupled(new Payload))) + val adapted = Vec(3, Master(Stream(new Payload))) + val handshakeIn = Flipped(Decoupled(new Payload)) + val handshake = Master(Stream(UInt(8.W))) + val emptyIn = Vec(2, Slave(Stream())) + val emptyOut = Vec(2, Master(Stream.empty)) + }) + + var evaluations = 0 + io.mapped << io.in(0).payloadMap { value => + evaluations += 1 + value >= 128.U + } + require(evaluations == 1, "payloadMap must evaluate its callback once") + val inherited: StreamIO[UInt] = io.in(1).map(_ +& 1.U) + io.inherited << inherited + io.replaced << io.in(2).payloadReplace(io.replacement) + io.signed << io.in(3).payloadCast(SInt(8.W), checkWidth = true) + io.truncated << io.in(4).payloadCast(UInt(4.W)) + io.padded << io.in(5).payloadCast(UInt(12.W)) + io.nibbles << io.in(6).payloadCast(Vec(2, UInt(4.W)), checkWidth = true) + io.filterOut(0) << io.filterIn(0).continueWhen(io.condition) + io.filterOut(1) << io.filterIn(1).haltWhen(io.condition) + io.filterOut(2) << io.filterIn(2).throwWhen(io.condition) + io.filterOut(3) << io.filterIn(3).takeWhen(io.condition) + io.statusIn.ready := io.statusReady + io.pending := io.statusIn.isPending + io.starving := io.statusIn.isStarving + io.fire := io.statusIn.fire + io.offered << io.statusIn.asFlow + io.transferred << io.statusIn.toFlow() + io.freeFlow << io.freeIn.toFlow(readyFreeRun = true) + io.adapted(0) << Stream.from(io.raw(0)) + io.adapted(1) << io.raw(1).toStream + val similar = Wire(Stream.like(io.raw(2))) + similar connectFrom io.raw(2) + io.adapted(2) << similar + io.handshake handshakeFrom io.handshakeIn + io.handshake.bits := 0xa5.U + for (index <- 0 until 2) io.emptyOut(index) << io.emptyIn(index) + } + + // Latency and initiation interval with a continuously valid producer and an always-ready consumer. + val paths = Seq( + ("pass-through", 0, 1), + ("forward", 1, 1), + ("forward without collapse", 1, 1), + ("backward", 0, 1), + ("all", 1, 1), + ("simple", 1, 2), + ("valid", 1, 2), + ("stage zero", 0, 1), + ("stage default", 1, 1), + ("stage three", 3, 1), + ("queue zero", 0, 1), + ("queue one", 1, 2), + ("queue piped", 1, 1), + ("queue flowing", 0, 1), + ("queue piped flowing", 0, 1), + ("queue four", 1, 1), + ("queue synchronous", 1, 1), + ("queue synchronous flowing", 0, 1), + ("left chain", 0, 1), + ("right chain", 0, 1), + ("left forward", 2, 1), + ("right forward", 2, 1), + ("left backward", 0, 1), + ("right backward", 0, 1), + ("left all", 2, 1), + ("right all", 2, 1), + ) + + final class PipelineHarness extends Module { + val io = IO(new Bundle { + val in = Vec(paths.size, Slave(Stream(UInt(16.W)))) + val out = Vec(paths.size, Master(Stream(UInt(16.W)))) + }) + for (((name, _, _), index) <- paths.zipWithIndex) { + val in = io.in(index) + val out = io.out(index) + name match { + case "pass-through" => + require(in.pipePassThrough() eq in) + out << in.pipePassThrough() + case "forward" => out << in.pipeForward() + case "forward without collapse" => out << in.pipeForward(bubbleCollapse = false) + case "backward" => out << in.pipeBackward() + case "all" => out << in.pipeAll() + case "simple" => out << in.pipeSimple() + case "valid" => out << in.pipeValid() + case "stage zero" => + require(in.stage(0) eq in) + out << in.stage(0) + case "stage default" => out << in.stage() + case "stage three" => out << in.stage(3) + case "queue zero" => + require(in.queue(0) eq in) + out << in.queue(0) + case "queue one" => out << in.queue(1) + case "queue piped" => out << in.queue(1, queuePipe = true) + case "queue flowing" => out << in.queue(1, queueFlow = true) + case "queue piped flowing" => out << in.queue(1, queueFlow = true, queuePipe = true) + case "queue four" => out << in.queue(4) + case "queue synchronous" => out << in.queue(4, useSyncReadMem = true) + case "queue synchronous flowing" => out << in.queue(4, queueFlow = true, useSyncReadMem = true) + case _ => + val middle = Wire(Stream(UInt(16.W))) + name match { + case "left chain" => require((out << middle << in) eq in) + case "right chain" => require((in >> middle >> out) eq out) + case "left forward" => require((out <-< middle <-< in) eq in) + case "right forward" => require((in >-> middle >-> out) eq out) + case "left backward" => require((out <|< middle <|< in) eq in) + case "right backward" => require((in >|> middle >|> out) eq out) + case "left all" => require((out <+< middle <+< in) eq in) + case "right all" => require((in >+> middle >+> out) eq out) + case _ => throw new IllegalArgumentException(name) + } + } + } + } + + val queues = Seq( + (4, false, false, false), + (4, false, false, true), + (4, true, false, true), + (1, false, true, false), + (1, true, true, false), + ) + + final class FlushHarness extends Module { + val io = IO(new Bundle { + val in = Vec(queues.size, Slave(Stream(UInt(16.W)))) + val out = Vec(queues.size, Master(Stream(UInt(16.W)))) + val flush = Input(Bool()) + }) + for (((depth, flow, pipe, sync), index) <- queues.zipWithIndex) { + io.out(index) << io.in(index).queue(depth, flow, pipe, sync, flush = Some(io.flush)) + } + } + + // Equal bounds exercise delayRandom's fixed-delay delegation, including zero. + val delays = Seq( + (false, 0, 0), + (false, 1, 1), + (false, 3, 3), + (true, 0, 0), + (true, 2, 2), + (true, 0, 1), + (true, 0, 3), + (true, 2, 5), + ) + + final class DelayHarness extends Module { + val io = IO(new Bundle { + val in = Vec(delays.size, Slave(Stream(UInt(16.W)))) + val out = Vec(delays.size, Master(Stream(UInt(16.W)))) + }) + for (((random, min, max), index) <- delays.zipWithIndex) { + val in = io.in(index) + if (min == 0 && max == 0) { + require((if (random) in.delayRandom(max, min) else in.delayFixed(max)) eq in) + } + io.out(index) << (if (random) in.delayRandom(max, min) else in.delayFixed(max)) + } + } + + final class InvalidHarness(mode: String) extends Module { + val in = IO(Slave(Stream(UInt(8.W)))) + mode match { + case "negative stages" => in.stage(-1) + case "negative queue" => in.queue(-1) + case "zero-depth flush" => in.queue(0, flush = Some(false.B)) + case "negative fixed delay" => in.delayFixed(-1) + case "negative random minimum" => in.delayRandom(2, -1) + case "reversed random bounds" => in.delayRandom(1, 2) + case "unequal widths" => in.payloadCast(UInt(7.W), checkWidth = true) + case "unknown width" => in.payloadCast(UInt(), checkWidth = true) + case "unknown source width" => + val unknown = Wire(Stream(UInt())) + unknown.payloadCast(UInt(8.W), checkWidth = true) + case "unbound mapping" => in.payloadMap(_ => UInt(8.W)) + case "unbound replacement" => in.payloadReplace(UInt(8.W)) + case "bound cast type" => in.payloadCast(in.bits) + case "unbound factory source" => Stream.like(Decoupled(UInt(8.W))) + case "write offered flow" => in.asFlow.bits := 0.U + case "write transferred flow" => in.toFlow().valid := false.B + case _ => throw new IllegalArgumentException(mode) + } + } +} + +class StreamIOSpec extends ChipmunkFlatSpec { + import StreamIOSpecDut.* + + "StreamIO" should "preserve handshake timing through payload transformations and ready/valid adapters" in { + simulate(new CombinationalHarness) { dut => + val outputs = Seq( + dut.io.mapped, + dut.io.inherited, + dut.io.replaced, + dut.io.signed, + dut.io.truncated, + dut.io.padded, + dut.io.nibbles, + ) + for { + value <- Seq(0x00, 0x01, 0x7f, 0x80, 0xa5, 0xff) + valid <- Seq(false, true) + ready <- Seq(false, true) + condition <- Seq(false, true) + } { + dut.io.replacement #= (value - 128).S + dut.io.condition #= condition.B + for (in <- dut.io.in) { + in.valid #= valid.B + in.bits #= value.U + } + outputs.foreach(_.ready #= ready.B) + for (in <- dut.io.filterIn) { + in.valid #= valid.B + in.bits #= value.U + } + dut.io.filterOut.foreach(_.ready #= ready.B) + dut.io.statusIn.valid #= valid.B + dut.io.statusIn.bits #= value.U + dut.io.statusReady #= ready.B + dut.io.freeIn.valid #= valid.B + dut.io.freeIn.bits #= value.U + for (in <- dut.io.raw) { + in.valid #= valid.B + in.bits.tag #= (value & 7).U + in.bits.data #= value.U + } + dut.io.adapted.foreach(_.ready #= ready.B) + dut.io.handshakeIn.valid #= valid.B + dut.io.handshakeIn.bits.tag #= 0.U + dut.io.handshakeIn.bits.data #= 0.U + dut.io.handshake.ready #= ready.B + for (index <- 0 until 2) { + dut.io.emptyIn(index).valid #= (valid ^ (index == 1)).B + dut.io.emptyOut(index).ready #= (ready ^ (index == 1)).B + } + dut.clock.step(0) + + withClue(s"value=$value, valid=$valid, ready=$ready, condition=$condition: ") { + outputs.foreach(_.valid.expect(valid.B)) + dut.io.in.foreach(_.ready.expect(ready.B)) + if (valid) { + dut.io.mapped.bits expect (value >= 128).B + dut.io.inherited.bits expect (value + 1).U + dut.io.replaced.bits expect (value - 128).S + dut.io.signed.bits expect (if (value < 128) value else value - 256).S + dut.io.truncated.bits expect (value & 15).U + dut.io.padded.bits expect value.U + dut.io.nibbles.bits(0) expect (value & 15).U + dut.io.nibbles.bits(1) expect (value >>> 4).U + } + val allowed = Seq(condition, !condition, !condition, condition) + for (index <- allowed.indices) { + dut.io.filterOut(index).valid expect (valid && allowed(index)).B + val inputReady = if (index < 2) ready && allowed(index) else ready || !allowed(index) + dut.io.filterIn(index).ready expect inputReady.B + if (valid && allowed(index)) dut.io.filterOut(index).bits expect value.U + } + dut.io.statusIn.ready expect ready.B + dut.io.pending expect (valid && !ready).B + dut.io.starving expect (!valid && ready).B + dut.io.fire expect (valid && ready).B + dut.io.offered.valid expect valid.B + dut.io.transferred.valid expect (valid && ready).B + dut.io.freeIn.ready expect true.B + dut.io.freeFlow.valid expect valid.B + if (valid) { + dut.io.offered.bits expect value.U + dut.io.freeFlow.bits expect value.U + if (ready) dut.io.transferred.bits expect value.U + } + for (index <- 0 until 3) { + dut.io.raw(index).ready expect ready.B + dut.io.adapted(index).valid expect valid.B + if (valid) { + dut.io.adapted(index).bits.tag expect (value & 7).U + dut.io.adapted(index).bits.data expect value.U + } + } + dut.io.handshakeIn.ready expect ready.B + dut.io.handshake.valid expect valid.B + dut.io.handshake.bits expect 0xa5.U + for (index <- 0 until 2) { + dut.io.emptyOut(index).valid expect (valid ^ (index == 1)).B + dut.io.emptyIn(index).ready expect (ready ^ (index == 1)).B + } + } + } + } + } + + private def resetPipelines(dut: PipelineHarness): Unit = { + for (index <- paths.indices) { + dut.io.in(index).valid #= false.B + dut.io.in(index).bits #= 0.U + dut.io.out(index).ready #= true.B + } + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + } + + it should "meet documented latency and throughput and discard buffered transactions on reset" in { + simulate(new PipelineHarness) { dut => + resetPipelines(dut) + val accepted = Array.fill(paths.size)(0) + for (cycle <- 0 until 40) { + for (index <- paths.indices) { + dut.io.in(index).valid #= true.B + dut.io.in(index).bits #= accepted(index).U + } + dut.clock.step(0) + for (((name, latency, interval), index) <- paths.zipWithIndex) { + val valid = cycle >= latency && (cycle - latency) % interval == 0 + withClue(s"$name, cycle=$cycle: ") { + dut.io.out(index).valid expect valid.B + if (valid) dut.io.out(index).bits expect ((cycle - latency) / interval).U + } + if (dut.io.in(index).ready.peek().litToBoolean) accepted(index) += 1 + } + dut.clock.step() + } + + // Fill every buffering path and leave valid-only delays pending before asserting reset. + dut.io.out.foreach(_.ready #= false.B) + for (index <- paths.indices) dut.io.in(index).bits #= accepted(index).U + dut.clock.step(6) + resetPipelines(dut) + for (_ <- 0 until 6) { + dut.io.out.foreach(_.valid.expect(false.B)) + dut.clock.step() + } + } + } + + it should "honor the distinct buffering and refill rules of each pipeline" in { + simulate(new PipelineHarness) { dut => + def select(name: String): (StreamIO[UInt], StreamIO[UInt]) = { + resetPipelines(dut) + val index = paths.indexWhere(_._1 == name) + (dut.io.in(index), dut.io.out(index)) + } + + for (collapse <- Seq(true, false)) { + val (in, out) = select(if (collapse) "forward" else "forward without collapse") + in.valid #= true.B + in.bits #= 0x31.U + out.ready #= false.B + in.ready expect collapse.B + out.valid expect false.B + dut.clock.step() + out.valid expect collapse.B + if (!collapse) { + out.ready #= true.B + dut.clock.step() + out.ready #= false.B + } + in.bits #= 0x32.U // The first transaction has now been accepted. + in.ready expect false.B + out.valid expect true.B + out.bits expect 0x31.U + dut.clock.step(3) + out.bits expect 0x31.U + out.ready #= true.B + in.ready expect true.B + dut.clock.step() + out.valid expect true.B + out.bits expect 0x32.U + } + + val (backIn, backOut) = select("backward") + backIn.valid #= true.B + backIn.bits #= 0x41.U + backOut.ready #= false.B + backIn.ready expect true.B + backOut.valid expect true.B + backOut.bits expect 0x41.U + dut.clock.step() + backIn.bits #= 0x42.U + backIn.ready expect false.B + backOut.bits expect 0x41.U + dut.clock.step(3) + backOut.ready #= true.B + backIn.ready expect false.B // Draining the skid buffer cannot also accept input. + dut.clock.step() + backIn.ready expect true.B + backOut.valid expect true.B + backOut.bits expect 0x42.U + + val (allIn, allOut) = select("all") + allOut.ready #= false.B + allIn.valid #= true.B + allIn.bits #= 0x51.U + allIn.ready expect true.B + dut.clock.step() + allIn.bits #= 0x52.U + allIn.ready expect true.B + dut.clock.step() + allIn.bits #= 0x53.U + allIn.ready expect false.B + allOut.bits expect 0x51.U + allOut.ready #= true.B + allIn.ready expect false.B + dut.clock.step() + allIn.ready expect true.B + allOut.bits expect 0x52.U + dut.clock.step() + allOut.bits expect 0x53.U + + val (simpleIn, simpleOut) = select("simple") + simpleIn.valid #= true.B + simpleIn.bits #= 0x61.U + simpleOut.ready #= false.B + simpleIn.ready expect true.B + dut.clock.step() + simpleIn.bits #= 0x62.U + simpleIn.ready expect false.B + simpleOut.valid expect true.B + simpleOut.bits expect 0x61.U + simpleOut.ready #= true.B + simpleIn.ready expect false.B + dut.clock.step() + simpleOut.valid expect false.B + simpleIn.ready expect true.B + + val (validIn, validOut) = select("valid") + validIn.valid #= true.B + validIn.bits #= 0x71.U + validOut.ready #= false.B + validIn.ready expect false.B + validOut.valid expect false.B + dut.clock.step() + validIn.ready expect false.B + validOut.valid expect true.B + validOut.bits expect 0x71.U + dut.clock.step(3) + validOut.ready #= true.B + validIn.ready expect true.B // Payload is held at the producer until this simultaneous transfer. + dut.clock.step() + validIn.bits #= 0x72.U + validIn.ready expect false.B + validOut.valid expect false.B + } + } + + it should "preserve every accepted transaction and stalled offer under bubbles and backpressure" in { + simulate(new PipelineHarness) { dut => + resetPipelines(dut) + val random = new Random(0x57eaL) + val expected = Array.fill(paths.size)(mutable.Queue.empty[Int]) + val offered = Array.fill[Option[Int]](paths.size)(None) + val stalled = Array.fill[Option[Int]](paths.size)(None) + val sent = Array.fill(paths.size)(0) + val received = Array.fill(paths.size)(0) + + for (cycle <- 0 until 420) { + val draining = cycle >= 340 + for (index <- paths.indices) { + if (!draining && offered(index).isEmpty && (cycle < 30 || random.nextInt(4) != 0)) { + offered(index) = Some(sent(index) + 1) + } + dut.io.in(index).valid #= offered(index).isDefined.B + dut.io.in(index).bits #= offered(index).getOrElse(0).U + val ready = draining || (cycle >= 30 && (cycle >= 50 || index % 2 == 0) && random.nextBoolean()) + dut.io.out(index).ready #= ready.B + } + dut.clock.step(0) + + for (((name, _, _), index) <- paths.zipWithIndex) { + val inReady = dut.io.in(index).ready.peek().litToBoolean + val outValid = dut.io.out(index).valid.peek().litToBoolean + val outReady = dut.io.out(index).ready.peek().litToBoolean + withClue(s"$name, cycle=$cycle: ") { + // Enqueue before comparing output so zero-latency paths can transfer in this cycle. + if (offered(index).isDefined && inReady) { + expected(index).enqueue(offered(index).get) + sent(index) += 1 + offered(index) = None + } + stalled(index).foreach { bits => + dut.io.out(index).valid expect true.B + dut.io.out(index).bits expect bits.U + } + if (outValid && outReady) { + expected(index).nonEmpty shouldBe true + dut.io.out(index).bits expect expected(index).dequeue().U + received(index) += 1 + } + stalled(index) = Option.when(outValid && !outReady)(dut.io.out(index).bits.peek().litValue.toInt) + } + } + dut.clock.step() + } + for (((name, _, _), index) <- paths.zipWithIndex) { + withClue(s"$name after draining: ") { + expected(index) shouldBe empty + offered(index) shouldBe None + received(index) shouldBe sent(index) + received(index) should be > 40 + dut.io.out(index).valid expect false.B + } + } + } + } + + it should "flush full queues, including synchronous memories and simultaneous input/output transfers" in { + simulate(new FlushHarness) { dut => + dut.io.flush #= false.B + for (index <- queues.indices) { + dut.io.in(index).valid #= false.B + dut.io.in(index).bits #= 0.U + dut.io.out(index).ready #= false.B + } + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + for (cycle <- 0 until 4) { + for (((depth, _, _, _), index) <- queues.zipWithIndex) { + dut.io.in(index).valid #= (cycle < depth).B + dut.io.in(index).bits #= (0x100 + cycle).U + dut.io.in(index).ready expect (cycle < depth).B + } + dut.clock.step() + } + dut.io.in.foreach(_.valid #= false.B) + for (index <- queues.indices) { + dut.io.in(index).ready expect false.B + dut.io.out(index).valid expect true.B + dut.io.out(index).bits expect 0x100.U + } + dut.io.flush #= true.B + dut.clock.step() + dut.io.flush #= false.B + dut.io.out.foreach(_.valid.expect(false.B)) + dut.io.in.foreach(_.ready.expect(true.B)) + + dut.io.in.foreach { in => + in.valid #= true.B + in.bits #= 0x200.U + } + dut.clock.step() + dut.io.in.foreach(_.valid #= false.B) + dut.io.out.foreach(_.bits.expect(0x200.U)) + dut.io.out.foreach(_.valid.expect(true.B)) + dut.io.out.foreach(_.ready #= true.B) + dut.clock.step() + dut.io.out.foreach(_.valid.expect(false.B)) + + // Enqueue a transaction, then flush on the edge that transfers it and offers its successor. + dut.io.out.foreach(_.ready #= false.B) + dut.io.in.foreach { in => + in.valid #= true.B + in.bits #= 0x300.U + } + dut.clock.step() + dut.io.in.foreach(_.bits #= 0x301.U) + dut.io.out.foreach(_.ready #= true.B) + dut.io.flush #= true.B + for (index <- queues.indices) { + dut.io.in(index).ready expect true.B + dut.io.out(index).valid expect true.B + dut.io.out(index).bits expect 0x300.U + } + dut.clock.step() + dut.io.in.foreach(_.valid #= false.B) + dut.io.flush #= false.B + for (_ <- 0 until 5) { + dut.io.out.foreach(_.valid.expect(false.B)) + dut.clock.step() + } + } + } + + private def resetDelays(dut: DelayHarness): Unit = { + for (index <- delays.indices) { + dut.io.in(index).valid #= false.B + dut.io.in(index).bits #= 0.U + dut.io.out(index).ready #= true.B + } + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + dut.clock.step(3) // Idle cycles must not start the per-transaction timer. + dut.io.out.foreach(_.valid.expect(false.B)) + } + + private def delayRound(dut: DelayHarness, stall: Boolean): Vector[Vector[Int]] = { + val observed = Array.fill(delays.size)(Vector.empty[Int]) + for (transaction <- 0 until 32) { + val done = Array.fill(delays.size)(false) + val first = Array.fill[Option[Int]](delays.size)(None) + var cycle = 0 + while (!done.forall(identity) && cycle < 12) { + val ready = !stall || cycle >= 8 + for (index <- delays.indices) { + dut.io.in(index).valid #= (!done(index)).B + dut.io.in(index).bits #= (transaction + 1).U + dut.io.out(index).ready #= ready.B + } + dut.clock.step(0) + for (((_, min, max), index) <- delays.zipWithIndex if !done(index)) { + val valid = dut.io.out(index).valid.peek().litToBoolean + withClue(s"delay=${delays(index)}, transaction=$transaction, cycle=$cycle, stall=$stall: ") { + dut.io.in(index).ready expect (valid && ready).B + if (min == max) dut.io.out(index).valid expect (cycle >= min).B + if (valid) { + dut.io.out(index).bits expect (transaction + 1).U + if (first(index).isEmpty) { + cycle should be >= min + cycle should be <= max + first(index) = Some(cycle) + observed(index) :+= cycle + } + } + if (first(index).isDefined) dut.io.out(index).valid expect true.B + if (valid && ready) done(index) = true + } + } + dut.clock.step() + cycle += 1 + } + withClue(s"transaction=$transaction, stall=$stall: ") { done.forall(identity) shouldBe true } + } + dut.io.in.foreach(_.valid #= false.B) + dut.clock.step() + dut.io.out.foreach(_.valid.expect(false.B)) + observed.toVector + } + + it should "delay presentation within inclusive bounds, hold under backpressure and replay after reset" in { + simulate(new DelayHarness) { dut => + resetDelays(dut) + val first = delayRound(dut, stall = false) + for (((random, min, max), index) <- delays.zipWithIndex if random && min != max) { + withClue(s"delay=${delays(index)}: ") { + first(index) should contain(min) + first(index) should contain(max) + } + } + resetDelays(dut) + delayRound(dut, stall = false) shouldBe first + delayRound(dut, stall = true) + + // A still-offered transaction restarts its fixed delay when reset interrupts the countdown. + val index = delays.indexOf((false, 3, 3)) + dut.io.in(index).valid #= true.B + dut.io.in(index).bits #= 0x456.U + dut.io.out(index).ready #= false.B + dut.clock.step(2) + dut.io.out(index).valid expect false.B + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + for (_ <- 0 until 3) { + dut.io.out(index).valid expect false.B + dut.io.in(index).ready expect false.B + dut.clock.step() + } + dut.io.out(index).valid expect true.B + dut.io.out(index).bits expect 0x456.U + dut.io.out(index).ready #= true.B + dut.io.in(index).ready expect true.B + dut.clock.step() + dut.io.in(index).valid #= false.B + dut.io.out(index).valid expect false.B + } + } + + for ( + (mode, message) <- Seq( + "negative stages" -> "nonnegative", + "negative queue" -> "nonnegative", + "zero-depth flush" -> "cannot be flushed", + "negative fixed delay" -> "nonnegative", + "negative random minimum" -> "nonnegative", + "reversed random bounds" -> "at least the minimum", + "unequal widths" -> "Payload width mismatch", + "unknown width" -> "known source and target widths", + "unknown source width" -> "known source and target widths", + ) + ) { + it should s"reject $mode during elaboration" in { + val error = intercept[IllegalArgumentException] { ChiselStage.emitCHIRRTL(new InvalidHarness(mode)) } + error.getMessage should include(message) + } + } + + for (mode <- Seq("unbound mapping", "unbound replacement", "unbound factory source")) { + it should s"reject $mode during elaboration" in { + intercept[ExpectedHardwareException] { ChiselStage.emitCHIRRTL(new InvalidHarness(mode)) } + } + } + + it should "require an unbound payload cast type" in { + intercept[ExpectedChiselTypeException] { ChiselStage.emitCHIRRTL(new InvalidHarness("bound cast type")) } + } + + for (mode <- Seq("write offered flow", "write transferred flow")) { + it should s"reject attempts to $mode through a read-only view" in { + val error = intercept[ChiselException] { + ChiselStage.emitCHIRRTL(new InvalidHarness(mode), args = Array("--throw-on-first-error")) + } + error.getMessage should include("Cannot connect to read-only value") + } + } +} From 5d60a667ae3a2c764ec136c8916ea27b9dcd3e47 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Sun, 4 Oct 2026 10:58:25 +0800 Subject: [PATCH 10/26] feat(stream): migrate remaining stream components to Scala 3 Hold arbiter and external routing selections under backpressure. Add transaction-paired selection tokens, independent broadcast forks, typed joins, Flow fan-out, and a Chisel Queue adapter. Document the Stream protocol and migrated APIs. Add component simulations and pass all 46 Stream/Flow regression tests with the branch's Scala 3.8.4 and Chisel snapshot dependencies. --- chipmunk/src/stream/FlowFork.scala | 47 ++ chipmunk/src/stream/StreamArbiter.scala | 78 +++ chipmunk/src/stream/StreamDemux.scala | 76 +++ chipmunk/src/stream/StreamFork.scala | 77 +++ chipmunk/src/stream/StreamJoin.scala | 59 +++ chipmunk/src/stream/StreamMux.scala | 74 +++ chipmunk/src/stream/StreamQueue.scala | 28 + chipmunk/test/src/StreamComponentsSpec.scala | 519 +++++++++++++++++++ docs/stream.md | 85 ++- 9 files changed, 1038 insertions(+), 5 deletions(-) create mode 100644 chipmunk/src/stream/FlowFork.scala create mode 100644 chipmunk/src/stream/StreamArbiter.scala create mode 100644 chipmunk/src/stream/StreamDemux.scala create mode 100644 chipmunk/src/stream/StreamFork.scala create mode 100644 chipmunk/src/stream/StreamJoin.scala create mode 100644 chipmunk/src/stream/StreamMux.scala create mode 100644 chipmunk/src/stream/StreamQueue.scala create mode 100644 chipmunk/test/src/StreamComponentsSpec.scala diff --git a/chipmunk/src/stream/FlowFork.scala b/chipmunk/src/stream/FlowFork.scala new file mode 100644 index 0000000..9e8d3e0 --- /dev/null +++ b/chipmunk/src/stream/FlowFork.scala @@ -0,0 +1,47 @@ +package chipmunk +package stream + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.MixedVec + +/** Combinational fan-out of an unbackpressured Flow. Every output must accept every valid cycle. */ +object FlowFork { + + /** Broadcast validity, leaving empty payloads for subsequent [[FlowIO.payloadReplace]] calls. */ + def withoutPayload[T <: Data](in: FlowIO[T], num: Int)(using SourceInfo): Vec[FlowIO[EmptyBundle]] = { + require(num >= 1, "FlowFork requires at least one output.") + val outs = Wire(Vec(num, Flow.empty)) + outs.foreach(_.handshakeFrom(in)) + outs + } + + /** Broadcast validity and payload without adding storage or latency. */ + def duplicate[T <: Data](in: FlowIO[T], num: Int)(using SourceInfo): Vec[FlowIO[T]] = { + require(num >= 1, "FlowFork requires at least one output.") + val outs = Wire(Vec(num, Flow.like(in))) + outs.foreach(_.connectFrom(in)) + outs + } + + /** Split a homogeneous Vec payload into unbackpressured element Flows. */ + def vecSplit[T <: Data](in: FlowIO[Vec[T]])(using SourceInfo): Vec[FlowIO[T]] = { + require(in.bits.length >= 1, "FlowFork requires at least one output.") + val outs = Wire(Vec(in.bits.length, Flow(chiselTypeOf(in.bits(0))))) + outs.zip(in.bits).foreach { case (out, bits) => + out.handshakeFrom(in) + out.bits := bits + } + outs + } + + /** Split a MixedVec payload, preserving each element's hardware type and width. */ + def vecSplit[T <: Data](in: FlowIO[MixedVec[T]])(using SourceInfo): IndexedSeq[FlowIO[T]] = { + require(in.bits.length >= 1, "FlowFork requires at least one output.") + in.bits.indices.map(index => in.payloadReplace(in.bits(index))).toIndexedSeq + } + + /** Evaluate a MixedVec mapping once during elaboration, then distribute its elements. */ + def vecMap[T <: Data, U <: Data](in: FlowIO[T], f: T => MixedVec[U])(using SourceInfo): IndexedSeq[FlowIO[U]] = + vecSplit(in.payloadMap(f)) +} diff --git a/chipmunk/src/stream/StreamArbiter.scala b/chipmunk/src/stream/StreamArbiter.scala new file mode 100644 index 0000000..43261dd --- /dev/null +++ b/chipmunk/src/stream/StreamArbiter.scala @@ -0,0 +1,78 @@ +package chipmunk +package stream + +import scala.collection.Seq + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.{Mux1H, PriorityEncoder} + +/** Arbitration of ordered inputs whose producers obey the StreamIO stability contract. */ +object StreamArbiter { + + /** Round-robin arbitration, with deterministic reset priority and no added latency. + * + * The last grant resets to zero, so input one is preferred initially when all inputs are valid. The pointer advances + * only on output transfer. A choice offered to a stalled consumer is held until transfer. + */ + def roundRobin[T <: Data](ins: Seq[StreamIO[T]])(using SourceInfo): StreamIO[T] = + arbitrate(ins, roundRobin = true) + + /** Give lower input indices priority, holding an offered choice until transfer. */ + def lowerFirst[T <: Data](ins: Seq[StreamIO[T]])(using SourceInfo): StreamIO[T] = + arbitrate(ins, roundRobin = false) + + private def arbitrate[T <: Data](ins: Seq[StreamIO[T]], roundRobin: Boolean)(using SourceInfo): StreamIO[T] = { + require(ins.nonEmpty, "An arbiter requires at least one input.") + + if (ins.length == 1) { + ins.head + } else { + val out = Wire(Stream.like(ins.head)) + val width = chisel3.util.log2Ceil(ins.length) + val locked = RegInit(false.B) + val heldChoice = Reg(UInt(width.W)) + val choice = Wire(UInt(width.W)) + val selected = Mux(locked, heldChoice, choice) + val requests = VecInit(ins.map(_.valid).toVector) + + if (roundRobin) { + val lastGrant = RegInit(0.U(width.W)) + val masked = VecInit(ins.zipWithIndex.map { case (in, index) => in.valid && index.U > lastGrant }.toVector) + choice := PriorityEncoder(Mux(masked.asUInt.orR, masked.asUInt, requests.asUInt)) + when(out.fire) { + lastGrant := selected + } + } else { + choice := PriorityEncoder(requests.asUInt) + } + + val grants = ins.indices.map(index => selected === index.U) + out.valid := ins.zip(grants).map { case (in, grant) => in.valid && grant }.reduce[Bool](_ || _) + out.bits := Mux1H(grants, ins.map(_.bits).toVector) + ins.zip(grants).foreach { case (in, grant) => in.ready := out.ready && grant } + + when(out.fire) { + locked := false.B + }.elsewhen(out.valid && !out.ready) { + locked := true.B + heldChoice := selected + } + + out + } + } +} + +/** Merge a Stream and a Flow into an unbackpressured Flow, giving the Flow priority. */ +object StreamFlowArbiter { + + /** The Flow consumer must accept every valid cycle; the Stream waits whenever the input Flow is valid. */ + def apply[T <: Data](inStream: StreamIO[T], inFlow: FlowIO[T])(using SourceInfo): FlowIO[T] = { + val out = Wire(Flow.like(inFlow)) + out.valid := inFlow.valid || inStream.valid + out.bits := Mux(inFlow.valid, inFlow.bits, inStream.bits) + inStream.ready := !inFlow.valid + out.readOnly + } +} diff --git a/chipmunk/src/stream/StreamDemux.scala b/chipmunk/src/stream/StreamDemux.scala new file mode 100644 index 0000000..c7975bb --- /dev/null +++ b/chipmunk/src/stream/StreamDemux.scala @@ -0,0 +1,76 @@ +package chipmunk +package stream + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.{log2Ceil, Mux1H} + +/** Route each input transaction to exactly one output without buffering its payload. */ +object StreamDemux { + + /** Use an external selector, holding an offered destination until transfer. + * + * Out-of-range selections stall input and make every output invalid. Full selector width is checked, so high bits + * are not silently truncated by this factory. + */ + def apply[T <: Data](in: StreamIO[T], select: UInt, num: Int)(using SourceInfo): Vec[StreamIO[T]] = { + require(num >= 1, "StreamDemux requires at least one output.") + val outs = Wire(Vec(num, Stream.like(in))) + connect(in, select, outs) + outs + } + + /** Consume one destination token together with each input transaction. + * + * Both sources must obey the StreamIO contract. An out-of-range token stalls without consuming either source. + */ + def apply[T <: Data](in: StreamIO[T], select: StreamIO[UInt], num: Int)(using SourceInfo): Vec[StreamIO[T]] = { + require(num >= 1, "StreamDemux requires at least one output.") + val outs = Wire(Vec(num, Stream.like(in))) + val matches = outs.indices.map(index => select.bits === index.U) + val ready = Mux1H(matches, outs.map(_.ready)) + + in.ready := select.valid && ready + select.ready := in.valid && ready + outs.zip(matches).foreach { case (out, hit) => + out.valid := in.valid && select.valid && hit + out.bits := in.bits + } + outs + } + + private[stream] def connect[T <: Data](in: StreamIO[T], select: UInt, outs: Vec[StreamIO[T]])(using + SourceInfo + ): Unit = { + val locked = RegInit(false.B) + val held = Reg(UInt(math.max(1, log2Ceil(outs.length)).W)) + val selected = Mux(locked, held, select) + val matches = outs.indices.map(index => selected === index.U) + + in.ready := Mux1H(matches, outs.map(_.ready)) + outs.zip(matches).foreach { case (out, hit) => + out.valid := in.valid && hit + out.bits := in.bits + } + + when(in.fire) { + locked := false.B + }.elsewhen(in.valid && matches.reduce(_ || _) && !in.ready) { + locked := true.B + held := selected + } + } +} + +/** Module form of [[StreamDemux]], using an external selector. */ +class StreamDemux[T <: Data](gen: T, num: Int) extends Module { + require(num >= 1, "StreamDemux requires at least one output.") + + val io = IO(new Bundle { + val select = Input(UInt(math.max(1, log2Ceil(num)).W)) + val in = Slave(Stream(gen)) + val outs = Vec(num, Master(Stream(gen))) + }) + + StreamDemux.connect(io.in, io.select, io.outs) +} diff --git a/chipmunk/src/stream/StreamFork.scala b/chipmunk/src/stream/StreamFork.scala new file mode 100644 index 0000000..369cd7b --- /dev/null +++ b/chipmunk/src/stream/StreamFork.scala @@ -0,0 +1,77 @@ +package chipmunk +package stream + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.MixedVec + +/** Broadcast each transaction exactly once to every output, allowing independent output handshakes. */ +object StreamFork { + + /** Fork only the handshake, leaving output payloads empty for subsequent [[StreamIO.payloadReplace]] calls. */ + def withoutPayload[T <: Data](in: StreamIO[T], num: Int)(using SourceInfo): Vec[StreamIO[EmptyBundle]] = { + val fork = Module(new StreamFork(num)) + fork.io.in.handshakeFrom(in) + fork.io.outs + } + + /** Broadcast the same payload to every output. Input transfers only after every output has accepted it. */ + def duplicate[T <: Data](in: StreamIO[T], num: Int)(using SourceInfo): Vec[StreamIO[T]] = { + val control = withoutPayload(in, num) + val outs = Wire(Vec(num, Stream.like(in))) + outs.zip(control).foreach { case (out, handshake) => + out.handshakeFrom(handshake) + out.bits := in.bits + } + outs + } + + /** Distribute a homogeneous Vec payload, with exactly one transfer to each element's output. */ + def vecSplit[T <: Data](in: StreamIO[Vec[T]])(using SourceInfo): Vec[StreamIO[T]] = { + val control = withoutPayload(in, in.bits.length) + val outs = Wire(Vec(in.bits.length, Stream(chiselTypeOf(in.bits(0))))) + outs.zip(control).zip(in.bits).foreach { case ((out, handshake), bits) => + out.handshakeFrom(handshake) + out.bits := bits + } + outs + } + + /** Distribute a MixedVec payload while retaining each element's hardware type and width. */ + def vecSplit[T <: Data](in: StreamIO[MixedVec[T]])(using SourceInfo): IndexedSeq[StreamIO[T]] = { + val control = withoutPayload(in, in.bits.length) + in.bits.indices.map(index => control(index).payloadReplace(in.bits(index))).toIndexedSeq + } + + /** Evaluate a MixedVec mapping once during elaboration, then distribute its elements. */ + def vecMap[T <: Data, U <: Data](in: StreamIO[T], f: T => MixedVec[U])(using SourceInfo): IndexedSeq[StreamIO[U]] = + vecSplit(in.payloadMap(f)) +} + +/** Handshake-only fork. Payload is held by the input producer until all branches complete. + * + * Branches may transfer before the input transfers. This is a broadcast, not a simultaneous/atomic transfer to all + * outputs. Reset abandons partially distributed transactions and must be coordinated with the connected endpoints. + */ +class StreamFork(num: Int) extends Module { + require(num >= 1, "StreamFork requires at least one output.") + + val io = IO(new Bundle { + val in = Slave(Stream.empty) + val outs = Vec(num, Master(Stream.empty)) + }) + + private val done = RegInit(VecInit(Seq.fill(num)(false.B))) + + io.in.ready := io.outs.zip(done).map { case (out, accepted) => accepted || out.ready }.reduce[Bool](_ && _) + io.outs.zipWithIndex.foreach { case (out, index) => + out.valid := io.in.valid && !done(index) + out.bits := io.in.bits + when(out.fire) { + done(index) := true.B + } + } + when(io.in.fire) { + done.foreach(_ := false.B) + } +} diff --git a/chipmunk/src/stream/StreamJoin.scala b/chipmunk/src/stream/StreamJoin.scala new file mode 100644 index 0000000..2788d0f --- /dev/null +++ b/chipmunk/src/stream/StreamJoin.scala @@ -0,0 +1,59 @@ +package chipmunk +package stream + +import scala.collection.Seq + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.{MixedVec, MixedVecInit, ReadyValidIO} + +/** Join input transactions by position; all inputs transfer together with the output. */ +object StreamJoin { + + /** Join handshakes with heterogeneous payloads, without buffering or checking transaction identities. */ + def withoutPayload(ins: Seq[ReadyValidIO[Data]])(using SourceInfo): StreamIO[EmptyBundle] = { + require(ins.nonEmpty, "StreamJoin requires at least one input.") + val join = Module(new StreamJoin(ins.length)) + join.io.ins.zip(ins).foreach { case (sink, source) => sink.handshakeFrom(source) } + join.io.out + } + + /** Varargs form of [[withoutPayload]]. Every producer must obey the StreamIO stability contract. */ + def withoutPayload(in: ReadyValidIO[Data], ins: ReadyValidIO[Data]*)(using SourceInfo): StreamIO[EmptyBundle] = + withoutPayload(in +: ins) + + /** Combine two differently typed payloads using a callback evaluated once during elaboration. */ + def map[A <: Data, B <: Data, R <: Data](a: StreamIO[A], b: StreamIO[B])(f: (A, B) => R)(using + SourceInfo + ): StreamIO[R] = withoutPayload(a, b).payloadMap(_ => f(a.bits, b.bits)) + + /** Merge homogeneous payloads into a Vec in input order. */ + def vecMerge[T <: Data](ins: Seq[StreamIO[T]])(using SourceInfo): StreamIO[Vec[T]] = + withoutPayload(ins).payloadReplace(VecInit(ins.map(_.bits).toVector)) + + /** Varargs form of [[vecMerge]]. */ + def vecMerge[T <: Data](in: StreamIO[T], ins: StreamIO[T]*)(using SourceInfo): StreamIO[Vec[T]] = + vecMerge(in +: ins) + + /** Merge differently typed or sized payloads into a MixedVec in input order. */ + def mixedVecMerge[T <: Data](ins: Seq[ReadyValidIO[T]])(using SourceInfo): StreamIO[MixedVec[T]] = + withoutPayload(ins).payloadReplace(MixedVecInit(ins.map(_.bits).toVector)) + + /** Varargs form of [[mixedVecMerge]]. */ + def mixedVecMerge[T <: Data](in: ReadyValidIO[T], ins: ReadyValidIO[T]*)(using SourceInfo): StreamIO[MixedVec[T]] = + mixedVecMerge(in +: ins) +} + +/** Handshake-only join. All producers must hold offered transactions until transfer. */ +class StreamJoin(num: Int) extends Module { + require(num >= 1, "StreamJoin requires at least one input.") + + val io = IO(new Bundle { + val ins = Vec(num, Slave(Stream.empty)) + val out = Master(Stream.empty) + }) + + io.out.valid := io.ins.map(_.valid).reduce[Bool](_ && _) + io.out.bits := DontCare + io.ins.foreach(_.ready := io.out.fire) +} diff --git a/chipmunk/src/stream/StreamMux.scala b/chipmunk/src/stream/StreamMux.scala new file mode 100644 index 0000000..22701ee --- /dev/null +++ b/chipmunk/src/stream/StreamMux.scala @@ -0,0 +1,74 @@ +package chipmunk +package stream + +import scala.collection.Seq + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.{log2Ceil, Mux1H} + +/** Select one input stream without buffering its payload. */ +object StreamMux { + + /** Use an external selector. Once output is offered and stalled, its input choice is held until transfer. + * + * An out-of-range selection stalls all inputs and makes output invalid. A single input still honors selection zero. + */ + def apply[T <: Data](select: UInt, ins: Seq[StreamIO[T]])(using SourceInfo): StreamIO[T] = { + require(ins.nonEmpty, "StreamMux requires at least one input.") + val out = Wire(Stream.like(ins.head)) + connect(select, ins, out) + out + } + + /** Consume one selection token together with each selected data transaction. + * + * Both sources must obey the StreamIO contract. Selection tokens are not updates to a configuration register. + * Out-of-range tokens stall without consuming either source. + */ + def apply[T <: Data](select: StreamIO[UInt], ins: Seq[StreamIO[T]])(using SourceInfo): StreamIO[T] = { + require(ins.nonEmpty, "StreamMux requires at least one input.") + val out = Wire(Stream.like(ins.head)) + val matches = ins.indices.map(index => select.bits === index.U) + val offered = ins.zip(matches).map { case (in, hit) => in.valid && hit }.reduce(_ || _) + + out.valid := select.valid && offered + out.bits := Mux1H(matches, ins.map(_.bits).toVector) + select.ready := out.ready && offered + ins.zip(matches).foreach { case (in, hit) => in.ready := out.ready && select.valid && hit } + out + } + + private[stream] def connect[T <: Data](select: UInt, ins: Seq[StreamIO[T]], out: StreamIO[T])(using + SourceInfo + ): Unit = { + val locked = RegInit(false.B) + val held = Reg(UInt(math.max(1, log2Ceil(ins.length)).W)) + val selected = Mux(locked, held, select) + val matches = ins.indices.map(index => selected === index.U) + + out.valid := ins.zip(matches).map { case (in, hit) => in.valid && hit }.reduce[Bool](_ || _) + out.bits := Mux1H(matches, ins.map(_.bits).toVector) + ins.zip(matches).foreach { case (in, hit) => in.ready := out.ready && hit } + + when(out.fire) { + locked := false.B + }.elsewhen(out.valid && !out.ready) { + locked := true.B + held := selected + } + } +} + +/** Module form of [[StreamMux]], using an external selector and preserving an offered choice while stalled. */ +class StreamMux[T <: Data](gen: T, num: Int) extends Module { + require(num >= 1, "StreamMux requires at least one input.") + + val io = IO(new Bundle { + val select = Input(UInt(math.max(1, log2Ceil(num)).W)) + val ins = Vec(num, Slave(Stream(gen))) + val out = Master(Stream(gen)) + }) + + StreamMux.connect(io.select, io.ins, io.out) +} diff --git a/chipmunk/src/stream/StreamQueue.scala b/chipmunk/src/stream/StreamQueue.scala new file mode 100644 index 0000000..c563e91 --- /dev/null +++ b/chipmunk/src/stream/StreamQueue.scala @@ -0,0 +1,28 @@ +package chipmunk +package stream + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.{Queue, ReadyValidIO} + +/** Stream-returning factory for Chisel Queue. Queue storage and handshake behavior are implemented by Chisel. */ +object StreamQueue { + + /** Buffer transactions, optionally coupling ready (`pipe`) or bypassing an empty queue (`flow`). + * + * A zero-depth queue is an unbuffered adapter: its producer must already obey the StreamIO protocol. Flush cancels + * queued transactions on the active edge, including an offered output; users must coordinate cancellation. + */ + def apply[T <: Data]( + enq: ReadyValidIO[T], + entries: Int = 2, + pipe: Boolean = false, + flow: Boolean = false, + useSyncReadMem: Boolean = false, + flush: Option[Bool] = None, + )(using SourceInfo): StreamIO[T] = { + require(entries >= 0, "Queue depth must be nonnegative.") + require(entries > 0 || flush.isEmpty, "A zero-depth queue cannot be flushed.") + Stream.from(Queue(enq, entries, pipe, flow, useSyncReadMem, flush)) + } +} diff --git a/chipmunk/test/src/StreamComponentsSpec.scala b/chipmunk/test/src/StreamComponentsSpec.scala new file mode 100644 index 0000000..956d6ac --- /dev/null +++ b/chipmunk/test/src/StreamComponentsSpec.scala @@ -0,0 +1,519 @@ +package chipmunk.test + +import scala.collection.mutable.ArrayBuffer +import scala.util.Random + +import chisel3.* +import chisel3.util.{MixedVec, MixedVecInit} +import circt.stage.ChiselStage + +import chipmunk.{Master, Slave} +import chipmunk.stream.* + +private object StreamComponentsSpecDut { + final class ArbiterHarness(num: Int, roundRobin: Boolean) extends Module { + val io = IO(new Bundle { + val ins = Vec(num, Slave(Stream(UInt(8.W)))) + val out = Master(Stream(UInt(8.W))) + }) + io.out << (if (roundRobin) StreamArbiter.roundRobin(io.ins) else StreamArbiter.lowerFirst(io.ins)) + } + + final class RoutingHarness(num: Int, tokens: Boolean) extends Module { + val io = IO(new Bundle { + val muxIns = Vec(num, Slave(Stream(UInt(8.W)))) + val muxSelect = Slave(Stream(UInt(4.W))) + val muxOut = Master(Stream(UInt(8.W))) + val demuxIn = Slave(Stream(UInt(8.W))) + val demuxSelect = Slave(Stream(UInt(4.W))) + val demuxOuts = Vec(num, Master(Stream(UInt(8.W)))) + }) + if (tokens) { + io.muxOut << StreamMux(io.muxSelect, io.muxIns) + io.demuxOuts.zip(StreamDemux(io.demuxIn, io.demuxSelect, num)).foreach { case (sink, source) => sink << source } + } else { + io.muxSelect.ready := false.B + io.demuxSelect.ready := false.B + io.muxOut << StreamMux(io.muxSelect.bits, io.muxIns) + io.demuxOuts.zip(StreamDemux(io.demuxIn, io.demuxSelect.bits, num)).foreach { case (sink, source) => + sink << source + } + } + } + + final class ForkHarness(num: Int) extends Module { + val io = IO(new Bundle { + val in = Slave(Stream(UInt(8.W))) + val outs = Vec(num, Master(Stream(UInt(8.W)))) + }) + io.outs.zip(StreamFork.duplicate(io.in, num)).foreach { case (sink, source) => sink << source } + } + + final class JoinHarness(num: Int) extends Module { + val io = IO(new Bundle { + val ins = Vec(num, Slave(Stream(UInt(8.W)))) + val out = Master(Stream(Vec(num, UInt(8.W)))) + }) + io.out << StreamJoin.vecMerge(io.ins) + } + + final class JoinedPayload extends Bundle { + val addr = UInt(4.W) + val data = SInt(8.W) + } + + final class PayloadHarness extends Module { + val io = IO(new Bundle { + val addr = Slave(Stream(UInt(4.W))) + val data = Slave(Stream(SInt(8.W))) + val joined = Master(Stream(new JoinedPayload)) + val splitIn = Slave(Stream(Vec(2, UInt(8.W)))) + val splitOuts = Vec(2, Master(Stream(UInt(8.W)))) + val mixedIn = Slave(Stream(MixedVec(Seq(UInt(4.W), UInt(9.W))))) + val mixedLow = Master(Stream(UInt(4.W))) + val mixedHigh = Master(Stream(UInt(9.W))) + val mixedA = Slave(Stream(UInt(4.W))) + val mixedB = Slave(Stream(Bool())) + val mixedJoin = Master(Stream(MixedVec(Seq(UInt(4.W), Bool())))) + val flow = Slave(Flow(UInt(8.W))) + val copies = Vec(2, Master(Flow(UInt(8.W)))) + val flowLow = Master(Flow(UInt(4.W))) + val flowHigh = Master(Flow(UInt(9.W))) + val stream = Slave(Stream(UInt(8.W))) + val priorityIn = Slave(Flow(UInt(8.W))) + val priority = Master(Flow(UInt(8.W))) + }) + var evaluations = 0 + io.joined << StreamJoin.map(io.addr, io.data) { (addr, data) => + evaluations += 1 + val result = Wire(new JoinedPayload) + result.addr := addr + result.data := data + result + } + require(evaluations == 1) + io.splitOuts.zip(StreamFork.vecSplit(io.splitIn)).foreach { case (sink, source) => sink << source } + val mixedOuts = StreamFork.vecMap(io.mixedIn, bits => MixedVecInit(bits(0), bits(1))) + io.mixedLow << mixedOuts(0) + io.mixedHigh << mixedOuts(1) + io.mixedJoin << StreamJoin.mixedVecMerge(io.mixedA, io.mixedB) + io.copies.zip(FlowFork.duplicate(io.flow, 2)).foreach { case (sink, source) => sink << source } + val mappedFlow = FlowFork.vecMap(io.flow, bits => MixedVecInit(bits(3, 0), bits +& 1.U)) + io.flowLow << mappedFlow(0) + io.flowHigh << mappedFlow(1) + io.priority << StreamFlowArbiter(io.stream, io.priorityIn) + } + + final class QueueHarness extends Module { + val io = IO(new Bundle { + val in = Slave(Stream(UInt(8.W))) + val out = Master(Stream(UInt(8.W))) + val flush = Input(Bool()) + val zeroIn = Slave(Stream(UInt(8.W))) + val zeroOut = Master(Stream(UInt(8.W))) + }) + io.out << StreamQueue(io.in, entries = 2, pipe = true, flush = Some(io.flush)) + io.zeroOut << StreamQueue(io.zeroIn, entries = 0) + } + + final class InvalidHarness(mode: String) extends Module { + private val stream = Wire(Stream(UInt(8.W))) + private val flow = Wire(Flow(UInt(8.W))) + stream.valid := false.B + stream.bits := 0.U + flow.valid := false.B + flow.bits := 0.U + mode match { + case "arbiter" => StreamArbiter.roundRobin(Seq.empty[StreamIO[UInt]]) + case "mux" => StreamMux(0.U, Seq.empty[StreamIO[UInt]]) + case "demux" => StreamDemux(stream, 0.U, 0) + case "fork" => StreamFork.duplicate(stream, 0) + case "flow fork" => FlowFork.duplicate(flow, 0) + case "join" => StreamJoin.withoutPayload(Seq.empty) + case "queue" => StreamQueue(stream, -1) + case "zero flush" => StreamQueue(stream, 0, flush = Some(false.B)) + case "mux module" => Module(new StreamMux(UInt(8.W), 0)) + case "demux module" => Module(new StreamDemux(UInt(8.W), 0)) + case other => throw new IllegalArgumentException(other) + } + } +} + +class StreamComponentsSpec extends ChipmunkFlatSpec { + import StreamComponentsSpecDut.* + + private def reset(dut: Module): Unit = { + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + } + + for (roundRobin <- Seq(false, true)) { + "StreamArbiter" should s"hold choices and transfer exactly one source under random stalls (roundRobin=$roundRobin)" in { + simulate(new ArbiterHarness(3, roundRobin)) { dut => + dut.io.ins.foreach { in => in.valid #= false.B; in.bits #= 0.U } + dut.io.out.ready #= false.B + reset(dut) + + // Offer input two, stall it, then introduce higher-priority requests. + dut.io.ins(2).valid #= true.B + dut.io.ins(2).bits #= 0x80.U + dut.io.out.bits expect 0x80.U + dut.clock.step() + for (i <- 0 until 2) { + dut.io.ins(i).valid #= true.B + dut.io.ins(i).bits #= (i * 64).U + } + dut.clock.step(3) + dut.io.out.valid expect true.B + dut.io.out.bits expect 0x80.U + dut.io.ins.foreach(_.ready.expect(false.B)) + + val random = new Random(0xa7b1L) + val accepted = Array.fill(3)(0) + val offered = Array.fill(3)(true) + var held = Option(2) + var last = 0 + var cycles = 0 + while (accepted.exists(_ < 24) && cycles < 500) { + for (i <- 0 until 3) { + if (!offered(i) && accepted(i) < 24 && random.nextBoolean()) offered(i) = true + dut.io.ins(i).valid #= offered(i).B + dut.io.ins(i).bits #= (i * 64 + accepted(i)).U + } + val ready = cycles > 400 || random.nextBoolean() + dut.io.out.ready #= ready.B + val order = if (roundRobin) (1 to 3).map(offset => (last + offset) % 3) else 0 until 3 + val choice = held.orElse(order.find(offered(_))) + dut.io.out.valid expect choice.nonEmpty.B + choice.foreach(i => dut.io.out.bits.expect((i * 64 + accepted(i)).U)) + // Ready on an invalid source is unconstrained; check which offered transaction can transfer. + for (i <- 0 until 3 if offered(i)) dut.io.ins(i).ready.expect((choice.contains(i) && ready).B) + if (choice.nonEmpty && ready) { + val i = choice.get + accepted(i) += 1 + offered(i) = false + last = i + held = None + } else if (choice.nonEmpty) held = choice + dut.clock.step() + cycles += 1 + } + accepted.toSeq shouldBe Seq(24, 24, 24) + } + } + } + + "StreamMux and StreamDemux" should "hold external selections while stalled and reject full-width invalid indices" in { + simulate(new RoutingHarness(3, tokens = false)) { dut => + dut.io.muxIns.zipWithIndex.foreach { case (in, i) => in.valid #= true.B; in.bits #= (0x30 + i).U } + dut.io.muxSelect.valid #= false.B + dut.io.muxSelect.bits #= 2.U + dut.io.muxOut.ready #= false.B + dut.io.demuxIn.valid #= true.B + dut.io.demuxIn.bits #= 0x55.U + dut.io.demuxSelect.valid #= false.B + dut.io.demuxSelect.bits #= 1.U + dut.io.demuxOuts.foreach(_.ready #= false.B) + reset(dut) + dut.clock.step() + dut.io.muxSelect.bits #= 0.U + dut.io.demuxSelect.bits #= 2.U + dut.clock.step(3) + dut.io.muxOut.bits expect 0x32.U + dut.io.demuxOuts(1).valid expect true.B + dut.io.demuxOuts(2).valid expect false.B + dut.io.muxOut.ready #= true.B + dut.io.demuxOuts(1).ready #= true.B + dut.io.muxIns(2).ready expect true.B + dut.io.muxIns(0).ready expect false.B + dut.io.demuxIn.ready expect true.B + dut.clock.step() + dut.io.muxOut.bits expect 0x30.U + dut.io.demuxOuts(2).valid expect true.B + dut.io.demuxOuts(2).ready #= true.B + dut.clock.step() + for (invalid <- Seq(3, 4, 8, 15)) { + dut.io.muxSelect.bits #= invalid.U + dut.io.demuxSelect.bits #= invalid.U + dut.io.muxOut.valid expect false.B + dut.io.muxIns.foreach(_.ready.expect(false.B)) + dut.io.demuxIn.ready expect false.B + dut.io.demuxOuts.foreach(_.valid.expect(false.B)) + dut.clock.step() + } + } + } + + it should "consume selection tokens only with their data transactions" in { + simulate(new RoutingHarness(3, tokens = true)) { dut => + dut.io.muxIns.zipWithIndex.foreach { case (in, i) => in.valid #= true.B; in.bits #= (0x40 + i).U } + dut.io.muxSelect.valid #= false.B + dut.io.muxSelect.bits #= 2.U + dut.io.muxOut.ready #= true.B + dut.io.demuxIn.valid #= false.B + dut.io.demuxIn.bits #= 0x66.U + dut.io.demuxSelect.valid #= true.B + dut.io.demuxSelect.bits #= 1.U + dut.io.demuxOuts.foreach(_.ready #= true.B) + reset(dut) + dut.io.muxOut.valid expect false.B + dut.io.muxIns.foreach(_.ready.expect(false.B)) + dut.io.demuxSelect.ready expect false.B + dut.io.muxSelect.valid #= true.B + dut.io.muxOut.ready #= false.B + dut.io.demuxIn.valid #= true.B + dut.io.demuxOuts(1).ready #= false.B + dut.clock.step(3) + dut.io.muxOut.valid expect true.B + dut.io.muxOut.bits expect 0x42.U + dut.io.muxSelect.ready expect false.B + dut.io.demuxSelect.ready expect false.B + dut.io.demuxIn.ready expect false.B + dut.io.muxOut.ready #= true.B + dut.io.demuxOuts(1).ready #= true.B + dut.io.muxSelect.ready expect true.B + dut.io.muxIns(2).ready expect true.B + dut.io.demuxSelect.ready expect true.B + dut.io.demuxIn.ready expect true.B + dut.clock.step() + dut.io.muxSelect.bits #= 8.U + dut.io.demuxSelect.bits #= 8.U + dut.io.muxOut.valid expect false.B + dut.io.muxSelect.ready expect false.B + dut.io.demuxSelect.ready expect false.B + dut.io.demuxOuts.foreach(_.valid.expect(false.B)) + } + } + + "StreamFork" should "broadcast exactly once per branch under independent randomized backpressure" in { + simulate(new ForkHarness(3)) { dut => + dut.io.in.valid #= false.B + dut.io.in.bits #= 0.U + dut.io.outs.foreach(_.ready #= false.B) + reset(dut) + val random = new Random(0xf07aL) + val seen = Array.fill(3)(ArrayBuffer.empty[Int]) + var next = 0 + var cycles = 0 + while ((next < 40 || seen.exists(_.length < 40)) && cycles < 600) { + dut.io.in.valid #= (next < 40).B + dut.io.in.bits #= next.U + dut.io.outs.foreach(_.ready #= (cycles > 500 || random.nextBoolean()).B) + for (i <- 0 until 3) { + if (dut.io.outs(i).valid.peek().litToBoolean && dut.io.outs(i).ready.peek().litToBoolean) { + val value = dut.io.outs(i).bits.peek().litValue.toInt + value shouldBe seen(i).length + seen(i) += value + } + } + if (next < 40 && dut.io.in.ready.peek().litToBoolean) next += 1 + dut.clock.step() + cycles += 1 + } + next shouldBe 40 + seen.foreach(_.toSeq shouldBe (0 until 40)) + dut.io.in.valid #= true.B + dut.io.in.bits #= 0xaa.U + dut.io.outs.zipWithIndex.foreach { case (out, i) => out.ready #= (i == 0).B } + dut.clock.step() + dut.io.outs(0).valid expect false.B + dut.io.in.valid #= false.B + reset(dut) + dut.io.in.valid #= true.B + dut.io.outs.foreach(_.valid.expect(true.B)) + } + } + + "StreamJoin" should "pair input transactions in order without consuming early arrivals" in { + simulate(new JoinHarness(3)) { dut => + dut.io.ins.foreach { in => in.valid #= false.B; in.bits #= 0.U } + dut.io.out.ready #= false.B + reset(dut) + val random = new Random(0x701aL) + val accepted = Array.fill(3)(0) + val offered = Array.fill(3)(false) + var received = 0 + var cycles = 0 + while (received < 30 && cycles < 600) { + for (i <- 0 until 3) { + if (!offered(i) && accepted(i) < 30 && (cycles > 500 || random.nextBoolean())) offered(i) = true + dut.io.ins(i).valid #= offered(i).B + dut.io.ins(i).bits #= (i * 64 + accepted(i)).U + } + val ready = cycles > 500 || random.nextBoolean() + val valid = offered.forall(identity) + dut.io.out.ready #= ready.B + dut.io.out.valid expect valid.B + for (i <- 0 until 3) dut.io.ins(i).ready.expect((valid && ready).B) + if (valid) for (i <- 0 until 3) dut.io.out.bits(i).expect((i * 64 + received).U) + if (valid && ready) { + received += 1 + for (i <- 0 until 3) { accepted(i) += 1; offered(i) = false } + } + dut.clock.step() + cycles += 1 + } + received shouldBe 30 + accepted.toSeq shouldBe Seq(30, 30, 30) + } + } + + "Stream payload helpers" should "preserve typed joins, Vec and MixedVec splits, Flow broadcasts and Flow priority" in { + simulate(new PayloadHarness) { dut => + dut.io.addr.valid #= true.B + dut.io.addr.bits #= 9.U + dut.io.data.valid #= false.B + dut.io.data.bits #= (-7).S + dut.io.joined.ready #= false.B + dut.io.splitIn.valid #= true.B + dut.io.splitIn.bits(0) #= 0x12.U + dut.io.splitIn.bits(1) #= 0x34.U + dut.io.splitOuts.foreach(_.ready #= false.B) + dut.io.mixedIn.valid #= true.B + dut.io.mixedIn.bits(0) #= 0xa.U + dut.io.mixedIn.bits(1) #= 0x1ab.U + dut.io.mixedLow.ready #= false.B + dut.io.mixedHigh.ready #= false.B + dut.io.mixedA.valid #= true.B + dut.io.mixedA.bits #= 0xb.U + dut.io.mixedB.valid #= true.B + dut.io.mixedB.bits #= true.B + dut.io.mixedJoin.ready #= false.B + dut.io.flow.valid #= true.B + dut.io.flow.bits #= 0xff.U + dut.io.stream.valid #= true.B + dut.io.stream.bits #= 0x44.U + dut.io.priorityIn.valid #= true.B + dut.io.priorityIn.bits #= 0x55.U + reset(dut) + dut.io.joined.valid expect false.B + dut.io.addr.ready expect false.B + dut.io.data.valid #= true.B + dut.clock.step(3) + dut.io.joined.valid expect true.B + dut.io.joined.bits.addr expect 9.U + dut.io.joined.bits.data expect (-7).S + dut.io.splitOuts(0).bits expect 0x12.U + dut.io.splitOuts(1).bits expect 0x34.U + dut.io.mixedLow.bits expect 0xa.U + dut.io.mixedHigh.bits expect 0x1ab.U + dut.io.mixedJoin.bits(0).expect(0xb.U) + dut.io.mixedJoin.bits(1).expect(true.B) + dut.io.copies.foreach { out => out.valid expect true.B; out.bits expect 0xff.U } + dut.io.flowLow.bits expect 0xf.U + dut.io.flowHigh.bits expect 0x100.U + dut.io.priority.bits expect 0x55.U + dut.io.stream.ready expect false.B + dut.io.priorityIn.valid #= false.B + dut.io.priority.bits expect 0x44.U + dut.io.stream.ready expect true.B + dut.io.splitOuts(0).ready #= true.B + dut.io.mixedLow.ready #= true.B + dut.clock.step() + dut.io.splitOuts(0).valid expect false.B + dut.io.splitOuts(1).valid expect true.B + dut.io.mixedLow.valid expect false.B + dut.io.mixedHigh.valid expect true.B + dut.io.splitIn.ready expect false.B + dut.io.mixedIn.ready expect false.B + dut.io.splitOuts(1).ready #= true.B + dut.io.mixedHigh.ready #= true.B + dut.io.joined.ready #= true.B + dut.io.mixedJoin.ready #= true.B + dut.io.splitIn.ready expect true.B + dut.io.mixedIn.ready expect true.B + dut.io.addr.ready expect true.B + dut.io.data.ready expect true.B + dut.io.mixedA.ready expect true.B + dut.io.mixedB.ready expect true.B + } + } + + "StreamQueue" should "buffer and flush responses and adapt zero depth without latency" in { + simulate(new QueueHarness) { dut => + dut.io.in.valid #= false.B + dut.io.in.bits #= 0.U + dut.io.out.ready #= false.B + dut.io.flush #= false.B + dut.io.zeroIn.valid #= true.B + dut.io.zeroIn.bits #= 0x77.U + dut.io.zeroOut.ready #= false.B + reset(dut) + dut.io.zeroOut.valid expect true.B + dut.io.zeroOut.bits expect 0x77.U + dut.io.zeroIn.ready expect false.B + dut.io.zeroOut.ready #= true.B + dut.io.zeroIn.ready expect true.B + for (value <- Seq(0x11, 0x22)) { + dut.io.in.valid #= true.B + dut.io.in.bits #= value.U + dut.io.in.ready expect true.B + dut.clock.step() + } + dut.io.in.bits #= 0x33.U + dut.io.in.ready expect false.B + dut.clock.step(3) + dut.io.out.valid expect true.B + dut.io.out.bits expect 0x11.U + dut.io.out.ready #= true.B + dut.io.in.ready expect true.B + dut.clock.step() + dut.io.in.valid #= false.B + dut.io.out.bits expect 0x22.U + dut.clock.step() + dut.io.out.bits expect 0x33.U + dut.io.out.ready #= false.B + dut.io.flush #= true.B + dut.clock.step() + dut.io.flush #= false.B + dut.io.out.valid expect false.B + } + } + + "Stream components" should "support a single source or destination" in { + simulate(new RoutingHarness(1, tokens = true)) { dut => + dut.io.muxIns(0).valid #= true.B + dut.io.muxIns(0).bits #= 0x11.U + dut.io.muxSelect.valid #= true.B + dut.io.muxSelect.bits #= 0.U + dut.io.muxOut.ready #= true.B + dut.io.demuxIn.valid #= true.B + dut.io.demuxIn.bits #= 0x22.U + dut.io.demuxSelect.valid #= true.B + dut.io.demuxSelect.bits #= 0.U + dut.io.demuxOuts(0).ready #= true.B + reset(dut) + dut.io.muxOut.bits expect 0x11.U + dut.io.muxIns(0).ready expect true.B + dut.io.demuxOuts(0).bits expect 0x22.U + dut.io.demuxIn.ready expect true.B + } + ChiselStage.emitCHIRRTL(new ArbiterHarness(1, roundRobin = true)) should include("module ArbiterHarness") + ChiselStage.emitCHIRRTL(new ForkHarness(1)) should include("module ForkHarness") + ChiselStage.emitCHIRRTL(new JoinHarness(1)) should include("module JoinHarness") + ChiselStage.emitCHIRRTL(new StreamMux(UInt(8.W), 1)) should include("module StreamMux") + ChiselStage.emitCHIRRTL(new StreamDemux(UInt(8.W), 1)) should include("module StreamDemux") + } + + it should "reject empty topologies and invalid queue configuration during elaboration" in { + for ( + mode <- Seq( + "arbiter", + "mux", + "demux", + "fork", + "flow fork", + "join", + "queue", + "zero flush", + "mux module", + "demux module", + ) + ) { + withClue(s"mode=$mode: ") { + intercept[IllegalArgumentException] { ChiselStage.emitCHIRRTL(new InvalidHarness(mode)) } + } + } + } +} diff --git a/docs/stream.md b/docs/stream.md index 0972f1b..1f28b14 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -11,6 +11,8 @@ Ready/Valid 握手协议由三部分信号组成: - `ready`:表示数据可被消费,即下游已经准备好接收数据; - `bits`(即 `payload`):表示数据本身。 +`StreamIO` 要求生产者在 `valid && !ready` 时保持 `valid` 和 `bits`,直到事务完成握手。复位或明确约定的 flush 可以取消事务,但需要与连接的模块协调。Chisel 的 `DecoupledIO` 本身没有这一稳定性保证;`Stream.from` 和 `toStream` 只负责接线,不会增加缓冲或自动实现该保证。 + 其中,`valid` 和 `bits` 信号由上游产生,`ready` 信号由下游产生。当 `valid` 和 `ready` 信号同时有效时,`bits` 信号被传递,即数据被消费。利用这一协议,我们可以将数据流的生产和消费解耦:上游模块只需要在准备好数据后将 `valid` 和 `bits` 信号置为有效,而无需关心下游模块内部的状态;而下游模块只需要在准备好接收数据后将 `ready` 信号置为有效,当 `valid` 和 `ready` 信号同时有效时取走 `bits` 数据。通过使用 Ready/Valid 握手协议,开发者可以将注意力集中在自己负责的电路逻辑上,而无需过度关心其他模块的内部实现,从而提高开发效率。 需要注意的是,为了避免组合逻辑环路,我们要求 `valid`(及 `bits`)和 `ready` 信号不能同时依赖对方的当前状态。因此,在 Chipmunk(和其他绝大多数数字电路设计)中,我们约定 `valid` 不允许依赖于 `ready` 的当前状态,即上游模块不允许根据下游模块此刻是否 `ready` 来决定当前周期是否将 `valid` 置为有效。简单地说,用于 `valid` 产生的组合逻辑表达式中不能出现下游模块的 `ready` 信号。 @@ -30,7 +32,7 @@ Chisel 提供了 `DecoupledIO` 和 `IrrevocableIO` 两个 Ready/Valid 握手协 很显然,上述 API 不足以应付实际的设计需求。 -Chipmunk 在 `chisel3.ReadyValidIO` 的基础上提供了 `chipmunk.StreamIO`,它继承自 `chisel3.ReadyValidIO`,并提供了一系列常用的 API 和特性,包括: +Chipmunk 提供的 `chipmunk.stream.StreamIO` 继承自 `chisel3.util.DecoupledIO`,并提供了一系列常用的 API 和特性,包括: - 一系列类似`fire` 的语法糖,展现数据传输的不同状态; - 符号化的连接方法,类似 `a >> b >-> c`,可以很容易地看出数据的流向; - 针对 `bits` 的一系列操作,包括 Map、Cast 等; @@ -50,10 +52,12 @@ val myStream = Wire(Stream(UInt(32.W))) - 创建一个 `StreamIO`,其 `payload` 类型和另一个 `DecoupledIO` 的 `bits` 一致: ```scala val myDecoupled = Wire(Decoupled(UInt(32.W))) -val myStream = Wire(Stream(myDecoupled)) +val myStream = Wire(Stream.like(myDecoupled)) ``` 请注意这里 `myStream` 不会与 `myDecoupled` 有任何电路连接(比如共享 `bits`),而是会创建一个新的 `UInt` 作为 `payload`。 +如果需要连接现有接口,可以使用 `Stream.from(myDecoupled)` 或 `myDecoupled.toStream`。其生产者必须已经遵守上述稳定性约定。 + - 创建一个空的 `StreamIO`(即其 `payload` 类型为 `EmptyBundle`): ```scala val myStream = Wire(Stream.empty) @@ -183,8 +187,79 @@ StreamFork 可以将一个上游 `StreamIO` 分叉成多个下游 `StreamIO`, StreamJoin 可以将多个上游 `StreamIO` 合并成一个下游 `StreamIO`,当且仅当所有上游 `StreamIO` 的 `valid` 都有效时,才会发生握手。 -在 `object StreamFork`/`object StreamJoin` 中,Chipmunk 提供了多个接口略有差别的方法。用户可以阅读代码中的注释,根据自己的需求选择合适的方法。 +Fork 不保存 payload,输入生产者必须保持当前事务,直到所有分支接收完毕。它保证每个输出分别接收一次,并不保证所有输出在同一周期接收。若希望上游先握手、后续再逐步分发,可以在 Fork 前增加 Queue。 + +```scala +val copies = StreamFork.duplicate(input, 3) +val elements = StreamFork.vecSplit(vectorInput) +val handshakeOnly = StreamFork.withoutPayload(input, 2) +``` + +`vecSplit` 同时支持 `Vec` 和 `MixedVec`;前者返回 `Vec`,后者返回保留各元素类型及位宽的 `IndexedSeq`。`vecMap` 可以先用一个 elaboration 回调生成 `MixedVec`,再拆分。 + +Join 按输入事务的位置配对,不检查业务上的事务 ID,也不保存早到的 payload。`withoutPayload` 只合并握手;`vecMerge` 将同类型 payload 按输入顺序合成 `Vec`;`mixedVecMerge` 将不同类型或位宽的 payload 合成 `MixedVec`。 + +```scala +val words = StreamJoin.vecMerge(Seq(a, b, c)) +val mixed = StreamJoin.mixedVecMerge(address, data) + +val command = StreamJoin.map(address, data) { (addr, value) => + val result = Wire(new WriteCommand) + result.addr := addr + result.data := value + result +} +``` + +`map` 的回调只在 elaboration 时执行一次,返回硬件 payload;输入同时握手,输出等待期间保持稳定。Fork 和 Join 都允许一个端口,拒绝零端口。 + +`FlowFork` 提供对应的 `duplicate`、`withoutPayload`、`vecSplit` 和 `vecMap`,但 Flow 无背压,每个输出都必须接收每一个有效周期。 + +### StreamMux/StreamDemux + +Mux 从多个输入中选择一个;Demux 把输入送到一个输出。两者都不保存 payload,支持单端口和非二次幂端口数。 + +```scala +val selected = StreamMux(select, inputs) +val routed = StreamDemux(input, destination, num = 3) +``` + +选择参数有两种明确的语义: + +| 参数类型 | 语义 | +|----------|------| +| `UInt` | 外部选择信号;一旦向下游提供事务但未被接收,组件会锁定当前来源或目的端,直到该事务完成。| +| `StreamIO[UInt]` | 每个数据事务配一个选择 token;选择 token 与相应数据同时完成握手。| + +流式选择参数不用于更新配置寄存器。Mux 的 token 在被选输入有效且输出接收时消费;Demux 的 token 与输入数据在目的端接收时一起消费。两路生产者都必须保持等待中的事务。 + +越界选择会使输出无效并阻塞输入,不会截断选择值的高位或误送到其他端口。越界 token 不会被消费,需要上层保证 token 合法;复位可以取消等待中的非法 token。 + +需要显式模块时,可以使用 `new StreamMux(gen, num)` 或 `new StreamDemux(gen, num)`。模块形式使用外部 `UInt` 选择信号。 + +### StreamArbiter + +```scala +val roundRobin = StreamArbiter.roundRobin(Seq(a, b, c)) +val priority = StreamArbiter.lowerFirst(Seq(a, b, c)) +``` + +两种仲裁器都没有额外流水延迟。下游等待时保持已提供的来源,直到完成握手;此时新出现的高优先级请求不能替换等待中的事务。 + +`lowerFirst` 优先选择下标较小的输入;`roundRobin` 在输出握手时更新轮询指针。轮询的 last grant 复位为零,与 Chisel `RRArbiter(initLastGrant = true)` 一致:多个输入都有效时,复位后的首次选择优先从输入一开始。一个输入直接通过,零输入非法。 + +`StreamFlowArbiter(stream, flow)` 返回无背压的 Flow,并优先选择 Flow。Flow 有效时阻塞 Stream;输出消费者必须接收每个有效周期。 + +### StreamQueue + +`input.queue(...)` 和 `StreamQueue(input, ...)` 都复用 Chisel Queue。独立工厂允许传入 `ReadyValidIO`,并返回 `StreamIO`: + +```scala +val buffered = StreamQueue(input, entries = 4) +val pipeline = StreamQueue(input, entries = 1, pipe = true) +val flushable = StreamQueue(input, entries = 4, flush = Some(cancel)) +``` -### StreamMux/Demux/... +`pipe=true` 允许满队列在出队的同一周期接受输入,会组合连接 ready 路径;`flow=true` 允许输入在队列为空时直接到达输出。`useSyncReadMem=true` 使用同步读存储。 -TODO +零深度只增加接线,生产者必须已经满足 Stream 协议;负深度和零深度 flush 在 elaboration 时被拒绝。Flush 在时钟沿取消所有缓冲事务,包括尚未完成握手的输出,因此使用方必须协调取消语义。 From 6b22feb7df049686218cdef2db3aba7a2ea448ba Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Sun, 4 Oct 2026 11:13:24 +0800 Subject: [PATCH 11/26] style: format TesterAPI --- chipmunk/src/tester/TesterAPI.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chipmunk/src/tester/TesterAPI.scala b/chipmunk/src/tester/TesterAPI.scala index 3943642..87f0698 100644 --- a/chipmunk/src/tester/TesterAPI.scala +++ b/chipmunk/src/tester/TesterAPI.scala @@ -10,6 +10,7 @@ trait TesterAPI { this: PeekPokeAPI => extension [T <: Data](target: T) { + /** Assign a Chisel literal to a DUT port. */ def #=(value: T): Unit = (target, value) match { @@ -19,16 +20,19 @@ trait TesterAPI { } extension (bool: Bool) { + /** Assign a random value to a `Bool` port. */ def randomize(): Unit = toTestableBool(bool).poke(scala.util.Random.nextBoolean()) } extension (uint: UInt) { + /** Assign a uniformly distributed random bit pattern to a `UInt` port. */ def randomize(): Unit = toTestableUInt(uint).poke(BigInt(uint.getWidth, scala.util.Random)) } extension (sint: SInt) { + /** Assign a uniformly distributed two's-complement bit pattern to a `SInt` port. */ def randomize(): Unit = { require(sint.getWidth > 0, "cannot randomize a zero-width SInt") From a42ca2839af94fa13260efcd687ddde46c667346 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Sun, 4 Oct 2026 15:51:09 +0800 Subject: [PATCH 12/26] Add Acorn interconnect components for Scala 3 Introduce chipmunk.acorn IO, Mux, Demux, Crossbar, and ErrorPoint with configurable address maps, local address translation, and ordered responses. Keep shared routing logic private and split configuration from components. Split Acorn tests into Config, Mux, Demux, and Crossbar specs with shared test helpers. Update Acorn documentation for the current protocol, package, configuration, and timing constraints. Validation: strict Acorn and test compilation, six passing Acorn tests, and scalafmt checks. --- chipmunk/src/acorn/AcornConfig.scala | 81 ++++++ chipmunk/src/acorn/AcornCrossbar.scala | 73 ++++++ chipmunk/src/acorn/AcornDemux.scala | 40 +++ chipmunk/src/acorn/AcornErrorPoint.scala | 32 +++ chipmunk/src/acorn/AcornIO.scala | 59 +++++ chipmunk/src/acorn/AcornMux.scala | 35 +++ chipmunk/src/acorn/AcornRouting.scala | 53 ++++ chipmunk/test/src/acorn/AcornConfigSpec.scala | 25 ++ .../test/src/acorn/AcornCrossbarSpec.scala | 240 +++++++++++++++++ chipmunk/test/src/acorn/AcornDemuxSpec.scala | 53 ++++ chipmunk/test/src/acorn/AcornMuxSpec.scala | 60 +++++ .../test/src/acorn/AcornSpecSupport.scala | 38 +++ docs/acorn.md | 246 ++++++++++++------ 13 files changed, 951 insertions(+), 84 deletions(-) create mode 100644 chipmunk/src/acorn/AcornConfig.scala create mode 100644 chipmunk/src/acorn/AcornCrossbar.scala create mode 100644 chipmunk/src/acorn/AcornDemux.scala create mode 100644 chipmunk/src/acorn/AcornErrorPoint.scala create mode 100644 chipmunk/src/acorn/AcornIO.scala create mode 100644 chipmunk/src/acorn/AcornMux.scala create mode 100644 chipmunk/src/acorn/AcornRouting.scala create mode 100644 chipmunk/test/src/acorn/AcornConfigSpec.scala create mode 100644 chipmunk/test/src/acorn/AcornCrossbarSpec.scala create mode 100644 chipmunk/test/src/acorn/AcornDemuxSpec.scala create mode 100644 chipmunk/test/src/acorn/AcornMuxSpec.scala create mode 100644 chipmunk/test/src/acorn/AcornSpecSupport.scala diff --git a/chipmunk/src/acorn/AcornConfig.scala b/chipmunk/src/acorn/AcornConfig.scala new file mode 100644 index 0000000..f9d6fc7 --- /dev/null +++ b/chipmunk/src/acorn/AcornConfig.scala @@ -0,0 +1,81 @@ +package chipmunk +package acorn + +/** A byte-addressed, naturally aligned, fixed-word interface. */ +final case class AcornParams(dataWidth: Int, addrWidth: Int) { + require(dataWidth >= 8 && (dataWidth & (dataWidth - 1)) == 0, "Acorn data width must be a power of two >= 8.") + require(addrWidth >= 1, "Acorn address width must be positive.") + val bytesPerWord: Int = dataWidth / 8 +} + +/** Separate capacities for accepted commands whose responses have not yet transferred. */ +final case class AcornOutstanding(read: Int = 4, write: Int = 4) { + require(read >= 1 && write >= 1, "Acorn outstanding capacities must be positive.") +} + +enum AcornArbitration { + case RoundRobin, LowerFirst +} + +/** One slave's global byte-address window [base, base + size). + * + * The endpoint sees localBase + (address - base). Its address width is inferred from the local window unless + * localAddrWidth is specified. All ports have the common bus data width; width conversion is a separate adapter. + */ +final case class AcornSlaveConfig( + name: String, + base: BigInt, + size: BigInt, + localBase: BigInt = 0, + localAddrWidth: Option[Int] = None, + readable: Boolean = true, + writable: Boolean = true, +) { + require(name.trim.nonEmpty, "An Acorn slave needs a name.") + require(base >= 0 && localBase >= 0 && size > 0, "Address bases must be nonnegative and size must be positive.") + require(readable || writable, "An Acorn slave must support at least one direction.") + + val end: BigInt = base + size + val portAddrWidth: Int = localAddrWidth.getOrElse(math.max(1, (localBase + size - 1).bitLength)) + require(portAddrWidth >= 1, "A slave address width must be positive.") + require(localBase + size <= (BigInt(1) << portAddrWidth), s"Local address window overflows slave $name.") +} + +/** Static crossbar configuration. Slave sequence order is the order of io.outs. + * + * connections=None connects every master to every slave. Otherwise there must be one set of slave names per master; an + * empty set disables all accesses from that master. Disallowed accesses receive normal ordered error responses. + * masterOutstanding limits each master's in-flight commands; slaveOutstanding limits each slave's aggregate load. + */ +final case class AcornCrossbarConfig( + params: AcornParams, + numMasters: Int, + slaves: Seq[AcornSlaveConfig], + masterOutstanding: AcornOutstanding = AcornOutstanding(), + slaveOutstanding: AcornOutstanding = AcornOutstanding(), + arbitration: AcornArbitration = AcornArbitration.RoundRobin, + connections: Option[Seq[Set[String]]] = None, +) { + require(numMasters >= 1 && slaves.nonEmpty, "An Acorn crossbar needs at least one master and one slave.") + private val names = slaves.map(_.name).toSet + require(names.size == slaves.size, "Acorn slave names must be unique.") + for (slave <- slaves) { + require(slave.end <= (BigInt(1) << params.addrWidth), s"Global address window overflows slave ${slave.name}.") + require( + slave.base % params.bytesPerWord == 0 && slave.size % params.bytesPerWord == 0 && + slave.localBase % params.bytesPerWord == 0, + s"Address window for ${slave.name} must be word aligned.", + ) + } + require( + slaves.sortBy(_.base).sliding(2).forall(pair => pair.size < 2 || pair.head.end <= pair.last.base), + "Acorn slave address windows must not overlap.", + ) + connections.foreach { sets => + require(sets.size == numMasters, "connections must contain one set per master.") + require(sets.forall(_.subsetOf(names)), "connections contains an unknown slave name.") + } + + def canAccess(master: Int, slave: Int): Boolean = + connections.forall(_(master).contains(slaves(slave).name)) +} diff --git a/chipmunk/src/acorn/AcornCrossbar.scala b/chipmunk/src/acorn/AcornCrossbar.scala new file mode 100644 index 0000000..f66a0ce --- /dev/null +++ b/chipmunk/src/acorn/AcornCrossbar.scala @@ -0,0 +1,73 @@ +package chipmunk +package acorn + +import chisel3.* +import chisel3.util.MixedVec + +/** Per-master Demux followed by per-slave Mux. Different slaves arbitrate independently. + * + * Address misses, unaligned addresses, direction permissions, and disconnected routes use one private error endpoint + * per master. Errors participate in the same response ordering as successful accesses. There are no payload queues + * between Demux and Mux: an accepted external command transfers to its endpoint in that same cycle, and both route + * records are updated atomically. A slow or stalled response can cause head-of-line blocking on shared slaves. + */ +class AcornCrossbar(val config: AcornCrossbarConfig) extends Module { + private val params = config.params + private val numSlaves = config.slaves.size + val io = IO(new Bundle { + val ins = Vec(config.numMasters, Slave(new AcornIO(params))) + val outs = MixedVec(config.slaves.map { slave => + Master(new AcornIO(params.dataWidth, slave.portAddrWidth)) + }) + }) + + /** Select an output by its configured name. */ + def slave(name: String): AcornIO = { + val index = config.slaves.indexWhere(_.name == name) + require(index >= 0, s"Unknown Acorn slave: $name") + io.outs(index) + } + + private def decode(addr: UInt, master: Int, write: Boolean): UInt = { + val aligned = (addr & (params.bytesPerWord - 1).U) === 0.U + val hits = config.slaves.zipWithIndex.map { case (slave, index) => + val enabled = config.canAccess(master, index) && (if (write) slave.writable else slave.readable) + val hit = enabled.B && aligned && addr >= slave.base.U && addr <= (slave.end - 1).U + hit -> index.U + } + chisel3.util.MuxCase(numSlaves.U, hits) + } + + private val demuxes = Seq.tabulate(config.numMasters) { master => + val demux = Module(new AcornDemux(params, numSlaves + 1, config.masterOutstanding)) + demux.io.in <> io.ins(master) + demux.io.rdSelect := decode(io.ins(master).rd.cmd.bits.addr, master, write = false) + demux.io.wrSelect := decode(io.ins(master).wr.cmd.bits.addr, master, write = true) + val errors = Module(new AcornErrorPoint(params, config.masterOutstanding)) + errors.io.access <> demux.io.outs(numSlaves) + demux + } + + for ((region, target) <- config.slaves.zipWithIndex) { + val localParams = AcornParams(params.dataWidth, region.portAddrWidth) + val mux = Module(new AcornMux(localParams, config.numMasters, config.slaveOutstanding, config.arbitration)) + io.outs(target) <> mux.io.out + for (master <- 0 until config.numMasters) { + val source = demuxes(master).io.outs(target) + mux.io.ins(master).rd.cmd << source.rd.cmd.payloadMap { command => + val local = Wire(new AcornRdCmdChannel(region.portAddrWidth)) + local.addr := (command.addr - region.base.U) +& region.localBase.U + local + } + mux.io.ins(master).wr.cmd << source.wr.cmd.payloadMap { command => + val local = Wire(new AcornWrCmdChannel(params.dataWidth, region.portAddrWidth)) + local.addr := (command.addr - region.base.U) +& region.localBase.U + local.data := command.data + local.strobe := command.strobe + local + } + source.rd.rsp << mux.io.ins(master).rd.rsp + source.wr.rsp << mux.io.ins(master).wr.rsp + } + } +} diff --git a/chipmunk/src/acorn/AcornDemux.scala b/chipmunk/src/acorn/AcornDemux.scala new file mode 100644 index 0000000..d3e23b7 --- /dev/null +++ b/chipmunk/src/acorn/AcornDemux.scala @@ -0,0 +1,40 @@ +package chipmunk +package acorn + +import chisel3.* +import chisel3.util.log2Ceil + +/** Route a master to slaves, preserving its read and write response order across all destinations. + * + * rdSelect and wrSelect are independent selectors. An offered destination is held until its command transfers. Invalid + * selectors stall; AcornCrossbar supplies a valid error-endpoint index for invalid addresses instead. This component + * does not translate addresses. Do not insert independent command queues between it and AcornMux: cross-target issue + * reordering can introduce cyclic response waits. Pipeline insertion needs a separate design. + */ +class AcornDemux(params: AcornParams, numSlaves: Int, outstanding: AcornOutstanding = AcornOutstanding()) + extends Module { + require(numSlaves >= 1, "AcornDemux needs at least one slave.") + val io = IO(new Bundle { + val in = Slave(new AcornIO(params)) + val outs = Vec(numSlaves, Master(new AcornIO(params))) + val rdSelect = Input(UInt(math.max(1, log2Ceil(numSlaves)).W)) + val wrSelect = Input(UInt(math.max(1, log2Ceil(numSlaves)).W)) + }) + + AcornRouting.route( + io.in.rd.cmd, + io.outs.map(_.rd.cmd), + io.rdSelect, + io.outs.map(_.rd.rsp), + io.in.rd.rsp, + outstanding.read, + ) + AcornRouting.route( + io.in.wr.cmd, + io.outs.map(_.wr.cmd), + io.wrSelect, + io.outs.map(_.wr.rsp), + io.in.wr.rsp, + outstanding.write, + ) +} diff --git a/chipmunk/src/acorn/AcornErrorPoint.scala b/chipmunk/src/acorn/AcornErrorPoint.scala new file mode 100644 index 0000000..b79b5aa --- /dev/null +++ b/chipmunk/src/acorn/AcornErrorPoint.scala @@ -0,0 +1,32 @@ +package chipmunk +package acorn + +import chisel3.* +import chisel3.util.Queue + +/** Accept commands and return ordered error responses. Read errors return zero data. + * + * Read and write have independent response capacities. Responses remain stable under backpressure. + */ +class AcornErrorPoint(params: AcornParams, outstanding: AcornOutstanding = AcornOutstanding()) extends Module { + val io = IO(new Bundle { + val access = Slave(new AcornIO(params)) + }) + // Only response occupancy is stored; all error payloads are constant. + private val reads = Module(new Queue(Bool(), outstanding.read, pipe = false, flow = false)) + reads.io.enq.valid := io.access.rd.cmd.valid + reads.io.enq.bits := false.B + io.access.rd.cmd.ready := reads.io.enq.ready + io.access.rd.rsp.valid := reads.io.deq.valid + io.access.rd.rsp.bits.data := 0.U + io.access.rd.rsp.bits.error := true.B + reads.io.deq.ready := io.access.rd.rsp.ready + + private val writes = Module(new Queue(Bool(), outstanding.write, pipe = false, flow = false)) + writes.io.enq.valid := io.access.wr.cmd.valid + writes.io.enq.bits := false.B + io.access.wr.cmd.ready := writes.io.enq.ready + io.access.wr.rsp.valid := writes.io.deq.valid + io.access.wr.rsp.bits.error := true.B + writes.io.deq.ready := io.access.wr.rsp.ready +} diff --git a/chipmunk/src/acorn/AcornIO.scala b/chipmunk/src/acorn/AcornIO.scala new file mode 100644 index 0000000..8e18e60 --- /dev/null +++ b/chipmunk/src/acorn/AcornIO.scala @@ -0,0 +1,59 @@ +package chipmunk +package acorn + +import chisel3.* + +import chipmunk.stream.* + +class AcornWrCmdChannel(val dataWidth: Int, val addrWidth: Int) extends Bundle { + val addr = UInt(addrWidth.W) + val data = UInt(dataWidth.W) + val strobe = UInt(AcornIO.strobeWidth(dataWidth).W) +} + +class AcornWrRspChannel extends Bundle { + val error = Bool() +} + +class AcornRdCmdChannel(val addrWidth: Int) extends Bundle { + val addr = UInt(addrWidth.W) +} + +class AcornRdRspChannel(val dataWidth: Int) extends Bundle { + val data = UInt(dataWidth.W) + val error = Bool() +} + +/** Four independent Stream channels, declared from the master's perspective. + * + * Every accepted command has exactly one response. Each direction returns in command order; read and write have no + * implicit ordering relative to one another. All producers hold valid/payload until transfer. Strobe bit i enables + * data bits [8*i+7:8*i]. A write response means that the endpoint-defined write operation has completed. + * + * These routers register transaction metadata. An endpoint must accept a command without waiting for that command's + * response to transfer, and must retain any response until accepted. All connected components share clock and reset; + * reset cancels outstanding transactions and must reset the endpoints consistently. + */ +class AcornIO(val dataWidth: Int, val addrWidth: Int) extends Bundle with IsMasterSlave { + def this(params: AcornParams) = this(params.dataWidth, params.addrWidth) + + val params: AcornParams = AcornParams(dataWidth, addrWidth) + val strobeWidth: Int = params.bytesPerWord + override def isMaster: Boolean = true + + val rd = new Bundle { + val cmd = Master(Stream(new AcornRdCmdChannel(addrWidth))) + val rsp = Slave(Stream(new AcornRdRspChannel(dataWidth))) + } + val wr = new Bundle { + val cmd = Master(Stream(new AcornWrCmdChannel(dataWidth, addrWidth))) + val rsp = Slave(Stream(new AcornWrRspChannel)) + } +} + +object AcornIO { + def strobeWidth(dataWidth: Int): Int = { + require(dataWidth >= 8 && dataWidth % 8 == 0, "Acorn strobes select whole bytes.") + dataWidth / 8 + } +} diff --git a/chipmunk/src/acorn/AcornMux.scala b/chipmunk/src/acorn/AcornMux.scala new file mode 100644 index 0000000..fe1cfb5 --- /dev/null +++ b/chipmunk/src/acorn/AcornMux.scala @@ -0,0 +1,35 @@ +package chipmunk +package acorn + +import chisel3.* + +/** Merge masters into one slave. Capacities are aggregate across all masters, separately for reads and writes. */ +class AcornMux( + params: AcornParams, + numMasters: Int, + outstanding: AcornOutstanding = AcornOutstanding(), + arbitration: AcornArbitration = AcornArbitration.RoundRobin, +) extends Module { + require(numMasters >= 1, "AcornMux needs at least one master.") + val io = IO(new Bundle { + val ins = Vec(numMasters, Slave(new AcornIO(params))) + val out = Master(new AcornIO(params)) + }) + + AcornRouting.merge( + io.ins.map(_.rd.cmd), + io.out.rd.cmd, + io.out.rd.rsp, + io.ins.map(_.rd.rsp), + outstanding.read, + arbitration, + ) + AcornRouting.merge( + io.ins.map(_.wr.cmd), + io.out.wr.cmd, + io.out.wr.rsp, + io.ins.map(_.wr.rsp), + outstanding.write, + arbitration, + ) +} diff --git a/chipmunk/src/acorn/AcornRouting.scala b/chipmunk/src/acorn/AcornRouting.scala new file mode 100644 index 0000000..f767229 --- /dev/null +++ b/chipmunk/src/acorn/AcornRouting.scala @@ -0,0 +1,53 @@ +package chipmunk +package acorn + +import chisel3.* +import chisel3.experimental.SourceInfo +import chisel3.util.{log2Ceil, PriorityEncoder, Queue} + +import chipmunk.stream.* + +private[acorn] object AcornRouting { + private def transferredIndex[T <: Data](ports: scala.collection.Seq[StreamIO[T]])(using SourceInfo): UInt = + if (ports.size == 1) 0.U else PriorityEncoder(VecInit(ports.map(_.fire).toVector).asUInt) + + def merge[C <: Data, R <: Data]( + ins: scala.collection.Seq[StreamIO[C]], + out: StreamIO[C], + response: StreamIO[R], + responses: scala.collection.Seq[StreamIO[R]], + depth: Int, + arbitration: AcornArbitration, + )(using SourceInfo): Unit = { + val owners = Module(new Queue(UInt(math.max(1, log2Ceil(ins.size)).W), depth, pipe = false, flow = false)) + // Capacity depends only on registered occupancy. While a command is stalled, no other command can consume it. + val eligible = ins.map(_.continueWhen(owners.io.enq.ready)) + val selected = arbitration match { + case AcornArbitration.RoundRobin => StreamArbiter.roundRobin(eligible) + case AcornArbitration.LowerFirst => StreamArbiter.lowerFirst(eligible) + } + out << selected + owners.io.enq.valid := out.fire + owners.io.enq.bits := transferredIndex(ins) + + val routed = StreamDemux(response, Stream.from(owners.io.deq), ins.size) + responses.zip(routed).foreach { case (sink, source) => sink << source } + } + + def route[C <: Data, R <: Data]( + in: StreamIO[C], + outs: scala.collection.Seq[StreamIO[C]], + select: UInt, + responses: scala.collection.Seq[StreamIO[R]], + response: StreamIO[R], + depth: Int, + )(using SourceInfo): Unit = { + val targets = Module(new Queue(UInt(math.max(1, log2Ceil(outs.size)).W), depth, pipe = false, flow = false)) + val routed = StreamDemux(in.continueWhen(targets.io.enq.ready), select, outs.size) + outs.zip(routed).foreach { case (sink, source) => sink << source } + targets.io.enq.valid := in.fire + // Record the destination that actually transferred, including a selector held by StreamDemux during a stall. + targets.io.enq.bits := transferredIndex(outs) + response << StreamMux(Stream.from(targets.io.deq), responses) + } +} diff --git a/chipmunk/test/src/acorn/AcornConfigSpec.scala b/chipmunk/test/src/acorn/AcornConfigSpec.scala new file mode 100644 index 0000000..db45fe7 --- /dev/null +++ b/chipmunk/test/src/acorn/AcornConfigSpec.scala @@ -0,0 +1,25 @@ +package chipmunk.test.acorn + +import chipmunk.acorn.* +import chipmunk.test.ChipmunkFlatSpec + +class AcornConfigSpec extends ChipmunkFlatSpec { + "AcornCrossbarConfig" should "reject ambiguous, unaligned, overflowing, and unknown routes" in { + val params = AcornParams(32, 16) + val a = AcornSlaveConfig("a", 0x1000, 0x100) + a.portAddrWidth shouldBe 8 + AcornSlaveConfig("a", 0, 0x100, localBase = 0x80).portAddrWidth shouldBe 9 + intercept[IllegalArgumentException](AcornParams(4, 16)) + intercept[IllegalArgumentException](AcornOutstanding(read = 0)) + intercept[IllegalArgumentException](AcornSlaveConfig("a", 0, 0x100, localAddrWidth = Some(7))) + intercept[IllegalArgumentException](AcornCrossbarConfig(params, 0, Seq(a))) + intercept[IllegalArgumentException](AcornCrossbarConfig(params, 1, Seq(a, a.copy(base = 0x2000)))) + intercept[IllegalArgumentException](AcornCrossbarConfig(params, 1, Seq(a, a.copy(name = "b", base = 0x1080)))) + intercept[IllegalArgumentException](AcornCrossbarConfig(params, 1, Seq(a.copy(base = 0x1001)))) + intercept[IllegalArgumentException](AcornCrossbarConfig(params, 1, Seq(a.copy(base = 0x10000)))) + intercept[IllegalArgumentException](AcornCrossbarConfig(params, 1, Seq(a.copy(localBase = 1)))) + intercept[IllegalArgumentException](AcornCrossbarConfig(params, 2, Seq(a), connections = Some(Seq(Set("a"))))) + intercept[IllegalArgumentException](AcornCrossbarConfig(params, 1, Seq(a), connections = Some(Seq(Set("b"))))) + AcornCrossbarConfig(params, 1, Seq(a, a.copy(name = "b", base = 0x1100))).slaves.size shouldBe 2 + } +} diff --git a/chipmunk/test/src/acorn/AcornCrossbarSpec.scala b/chipmunk/test/src/acorn/AcornCrossbarSpec.scala new file mode 100644 index 0000000..57fe35b --- /dev/null +++ b/chipmunk/test/src/acorn/AcornCrossbarSpec.scala @@ -0,0 +1,240 @@ +package chipmunk.test.acorn + +import scala.collection.mutable +import scala.util.Random + +import chisel3.* + +import chipmunk.acorn.* + +class AcornCrossbarSpec extends AcornSpecSupport { + "AcornCrossbar" should "support a full global window, one master, and a wider local address with carry" in { + val config = AcornCrossbarConfig( + AcornParams(8, 4), + 1, + Seq(AcornSlaveConfig("memory", 0, 16, localBase = 15)), + masterOutstanding = AcornOutstanding(1, 1), + slaveOutstanding = AcornOutstanding(1, 1), + ) + config.slaves.head.portAddrWidth shouldBe 5 + simulate(new AcornCrossbar(config)) { dut => + initMaster(dut.io.ins(0)) + initSlave(dut.io.outs(0)) + reset(dut) + dut.io.ins(0).rd.cmd.valid #= true.B + dut.io.ins(0).rd.cmd.bits.addr #= 15.U + dut.io.outs(0).rd.cmd.ready #= true.B + dut.io.outs(0).rd.cmd.valid expect true.B + dut.io.outs(0).rd.cmd.bits.addr expect 30.U + dut.clock.step() + dut.io.ins(0).rd.cmd.valid #= false.B + dut.io.outs(0).rd.rsp.valid #= true.B + dut.io.outs(0).rd.rsp.bits.data #= 0xa5.U + dut.io.ins(0).rd.rsp.ready #= true.B + dut.io.ins(0).rd.rsp.valid expect true.B + dut.io.ins(0).rd.rsp.bits.data expect 0xa5.U + dut.clock.step() + } + } + + private case class Write(addr: BigInt, data: BigInt, strobe: BigInt) + private case class ReadResult(data: BigInt, error: Boolean) + private case class PendingRead(due: Int, result: ReadResult) + private case class PendingWrite(due: Int, error: Boolean) + + for (depth <- Seq(1, 4)) { + "AcornCrossbar" should s"route concurrent reads and writes under random stalls with capacity $depth" in { + val regions = Seq( + AcornSlaveConfig("bank0", 0x1000, 0x100), + AcornSlaveConfig("bank1", 0x2000, 0x100, localBase = 0x80), + AcornSlaveConfig("status", 0x3000, 0x100, writable = false), + ) + val config = AcornCrossbarConfig( + AcornParams(32, 16), + 3, + regions, + masterOutstanding = AcornOutstanding(depth, depth), + slaveOutstanding = AcornOutstanding(depth, depth), + connections = Some(Seq(Set("bank0", "bank1", "status"), Set("bank1"), Set("bank0", "bank1", "status"))), + ) + val reads = Array.tabulate(3) { master => + Vector.tabulate(24) { index => + val offset = BigInt(4 * ((master * 7 + index) % 64)) + index % 8 match { + case 0 | 4 => BigInt(0x1000) + offset + case 1 | 5 => BigInt(0x2000) + offset + case 2 | 6 => BigInt(0x3000) + offset + case 3 => BigInt(0x4000) + offset + case _ => BigInt(0x1001) + offset + } + } + } + val writes = Array.tabulate(3) { master => + reads(master).zipWithIndex.map { case (addr, index) => + Write(addr, BigInt(master * 256 + index), BigInt(index % 16)) + } + } + def target(master: Int, addr: BigInt, write: Boolean): Option[Int] = { + if (addr % 4 != 0) None + else + regions.indices.find { index => + val region = regions(index) + addr >= region.base && addr < region.end && config.canAccess(master, index) && + (if (write) region.writable else region.readable) + } + } + def local(index: Int, addr: BigInt): BigInt = addr - regions(index).base + regions(index).localBase + def readResult(index: Int, addr: BigInt): ReadResult = ReadResult((BigInt(index + 1) << 24) | addr, false) + def writeError(index: Int, addr: BigInt, data: BigInt, strobe: BigInt): Boolean = + ((addr ^ data ^ strobe ^ index) & 1) != 0 + + simulate(new AcornCrossbar(config)) { dut => + dut.io.ins.foreach(initMaster) + dut.io.outs.foreach(initSlave) + reset(dut) + val random = new Random(0xac07L + depth) + val readSent = Array.fill(3)(0) + val writeSent = Array.fill(3)(0) + val readReceived = Array.fill(3)(0) + val writeReceived = Array.fill(3)(0) + val expectedReads = Array.fill(3)(mutable.Queue.empty[ReadResult]) + val expectedWrites = Array.fill(3)(mutable.Queue.empty[Boolean]) + val pendingReads = Array.fill(3)(mutable.Queue.empty[PendingRead]) + val pendingWrites = Array.fill(3)(mutable.Queue.empty[PendingWrite]) + val heldReads = Array.fill[Option[ReadResult]](3)(None) + val heldWrites = Array.fill[Option[Boolean]](3)(None) + var parallelReads = false + var cycle = 0 + + while ((readReceived.exists(_ < 24) || writeReceived.exists(_ < 24)) && cycle < 3000) { + for (master <- 0 until 3) { + val port = dut.io.ins(master) + port.rd.cmd.valid #= (readSent(master) < 24).B + if (readSent(master) < 24) port.rd.cmd.bits.addr #= reads(master)(readSent(master)).U + port.wr.cmd.valid #= (writeSent(master) < 24).B + if (writeSent(master) < 24) { + val command = writes(master)(writeSent(master)) + port.wr.cmd.bits.addr #= command.addr.U + port.wr.cmd.bits.data #= command.data.U + port.wr.cmd.bits.strobe #= command.strobe.U + } + port.rd.rsp.ready #= (cycle > 2400 || random.nextInt(4) != 0).B + port.wr.rsp.ready #= (cycle > 2400 || random.nextInt(3) != 0).B + } + for (index <- regions.indices) { + val port = dut.io.outs(index) + port.rd.cmd.ready #= (pendingReads(index).size < 8 && (cycle > 2400 || random.nextBoolean())).B + port.wr.cmd.ready #= (pendingWrites(index).size < 8 && (cycle > 2400 || random.nextBoolean())).B + val read = pendingReads(index).headOption.filter(_.due <= cycle) + port.rd.rsp.valid #= read.nonEmpty.B + port.rd.rsp.bits.data #= read.map(_.result.data).getOrElse(BigInt(0)).U + port.rd.rsp.bits.error #= read.exists(_.result.error).B + val write = pendingWrites(index).headOption.filter(_.due <= cycle) + port.wr.rsp.valid #= write.nonEmpty.B + port.wr.rsp.bits.error #= write.exists(_.error).B + } + + // Retire responses before adding this cycle's new expectations; no zero-latency route bypass exists. + for (master <- 0 until 3) { + val port = dut.io.ins(master) + if (bit(port.rd.rsp.valid)) { + val actual = ReadResult(number(port.rd.rsp.bits.data), bit(port.rd.rsp.bits.error)) + expectedReads(master).nonEmpty shouldBe true + actual shouldBe expectedReads(master).front + heldReads(master).foreach(_ shouldBe actual) + if (bit(port.rd.rsp.ready)) { + expectedReads(master).dequeue() + readReceived(master) += 1 + heldReads(master) = None + } else heldReads(master) = Some(actual) + } else heldReads(master) shouldBe None + if (bit(port.wr.rsp.valid)) { + val actual = bit(port.wr.rsp.bits.error) + expectedWrites(master).nonEmpty shouldBe true + actual shouldBe expectedWrites(master).front + heldWrites(master).foreach(_ shouldBe actual) + if (bit(port.wr.rsp.ready)) { + expectedWrites(master).dequeue() + writeReceived(master) += 1 + heldWrites(master) = None + } else heldWrites(master) = Some(actual) + } else heldWrites(master) shouldBe None + } + for (index <- regions.indices) { + val port = dut.io.outs(index) + if (bit(port.rd.rsp.valid) && bit(port.rd.rsp.ready)) pendingReads(index).dequeue() + if (bit(port.wr.rsp.valid) && bit(port.wr.rsp.ready)) pendingWrites(index).dequeue() + } + + val acceptedReads = mutable.Map.empty[Int, BigInt] + val acceptedWrites = mutable.Map.empty[Int, Write] + for (master <- 0 until 3) { + val port = dut.io.ins(master) + if (bit(port.rd.cmd.valid) && bit(port.rd.cmd.ready)) { + val addr = reads(master)(readSent(master)) + val destination = target(master, addr, write = false) + expectedReads(master).enqueue( + destination + .map { index => + acceptedReads.contains(index) shouldBe false + val address = local(index, addr) + acceptedReads(index) = address + readResult(index, address) + } + .getOrElse(ReadResult(0, true)) + ) + readSent(master) += 1 + } + if (bit(port.wr.cmd.valid) && bit(port.wr.cmd.ready)) { + val command = writes(master)(writeSent(master)) + val destination = target(master, command.addr, write = true) + expectedWrites(master).enqueue( + destination + .map { index => + acceptedWrites.contains(index) shouldBe false + val translated = command.copy(addr = local(index, command.addr)) + acceptedWrites(index) = translated + writeError(index, translated.addr, translated.data, translated.strobe) + } + .getOrElse(true) + ) + writeSent(master) += 1 + } + } + if (acceptedReads.size >= 2) parallelReads = true + for (index <- regions.indices) { + val port = dut.io.outs(index) + (bit(port.rd.cmd.valid) && bit(port.rd.cmd.ready)) shouldBe acceptedReads.contains(index) + acceptedReads.get(index).foreach { addr => + port.rd.cmd.bits.addr expect addr.U + pendingReads(index).enqueue( + PendingRead(cycle + 2 + index * 3 + random.nextInt(4), readResult(index, addr)) + ) + } + (bit(port.wr.cmd.valid) && bit(port.wr.cmd.ready)) shouldBe acceptedWrites.contains(index) + acceptedWrites.get(index).foreach { command => + port.wr.cmd.bits.addr expect command.addr.U + port.wr.cmd.bits.data expect command.data.U + port.wr.cmd.bits.strobe expect command.strobe.U + pendingWrites(index).enqueue( + PendingWrite( + cycle + 1 + random.nextInt(4), + writeError(index, command.addr, command.data, command.strobe), + ) + ) + } + } + dut.clock.step() + cycle += 1 + } + readReceived.toSeq shouldBe Seq(24, 24, 24) + writeReceived.toSeq shouldBe Seq(24, 24, 24) + expectedReads.foreach(_ shouldBe empty) + expectedWrites.foreach(_ shouldBe empty) + pendingReads.foreach(_ shouldBe empty) + pendingWrites.foreach(_ shouldBe empty) + parallelReads shouldBe true + } + } + } +} diff --git a/chipmunk/test/src/acorn/AcornDemuxSpec.scala b/chipmunk/test/src/acorn/AcornDemuxSpec.scala new file mode 100644 index 0000000..562c419 --- /dev/null +++ b/chipmunk/test/src/acorn/AcornDemuxSpec.scala @@ -0,0 +1,53 @@ +package chipmunk.test.acorn + +import chisel3.* + +import chipmunk.acorn.* + +class AcornDemuxSpec extends AcornSpecSupport { + "AcornDemux" should "record a held selector and preserve response order across destinations" in { + simulate(new AcornDemux(AcornParams(32, 12), 3, AcornOutstanding(3, 3))) { dut => + initMaster(dut.io.in) + dut.io.outs.foreach(initSlave) + dut.io.rdSelect #= 0.U + dut.io.wrSelect #= 0.U + reset(dut) + dut.io.in.rd.cmd.valid #= true.B + dut.io.in.rd.cmd.bits.addr #= 0x40.U + dut.clock.step() + dut.io.rdSelect #= 1.U + dut.io.outs(0).rd.cmd.ready #= true.B + dut.io.outs(1).rd.cmd.ready #= true.B + dut.io.outs(0).rd.cmd.valid expect true.B + dut.io.outs(1).rd.cmd.valid expect false.B + dut.clock.step() + + dut.io.in.rd.cmd.bits.addr #= 0x80.U + dut.io.outs(1).rd.cmd.valid expect true.B + dut.clock.step() + dut.io.in.rd.cmd.valid #= false.B + dut.io.in.rd.rsp.ready #= true.B + dut.io.outs(1).rd.rsp.valid #= true.B + dut.io.outs(1).rd.rsp.bits.data #= 0x2222.U + dut.io.in.rd.rsp.valid expect false.B + dut.io.outs(1).rd.rsp.ready expect false.B + dut.clock.step(3) + dut.io.outs(0).rd.rsp.valid #= true.B + dut.io.outs(0).rd.rsp.bits.data #= 0x1111.U + dut.io.in.rd.rsp.bits.data expect 0x1111.U + dut.clock.step() + dut.io.outs(0).rd.rsp.valid #= false.B + dut.io.in.rd.rsp.bits.data expect 0x2222.U + dut.clock.step() + dut.io.outs(1).rd.rsp.valid #= false.B + + dut.io.in.rd.cmd.valid #= true.B + dut.io.rdSelect #= 3.U + dut.io.in.rd.cmd.ready expect false.B + dut.io.outs.foreach(_.rd.cmd.valid.expect(false.B)) + dut.clock.step(2) + dut.io.rdSelect #= 0.U + dut.io.in.rd.cmd.ready expect true.B + } + } +} diff --git a/chipmunk/test/src/acorn/AcornMuxSpec.scala b/chipmunk/test/src/acorn/AcornMuxSpec.scala new file mode 100644 index 0000000..fede238 --- /dev/null +++ b/chipmunk/test/src/acorn/AcornMuxSpec.scala @@ -0,0 +1,60 @@ +package chipmunk.test.acorn + +import chisel3.* + +import chipmunk.acorn.* + +class AcornMuxSpec extends AcornSpecSupport { + "AcornMux" should "hold a stalled grant and return outstanding responses to their original masters" in { + simulate(new AcornMux(AcornParams(32, 12), 2, AcornOutstanding(2, 2))) { dut => + dut.io.ins.foreach(initMaster) + initSlave(dut.io.out) + reset(dut) + + dut.io.ins(0).rd.cmd.valid #= true.B + dut.io.ins(0).rd.cmd.bits.addr #= 0x40.U + dut.io.out.rd.cmd.valid expect true.B + dut.clock.step() + dut.io.ins(1).rd.cmd.valid #= true.B + dut.io.ins(1).rd.cmd.bits.addr #= 0x80.U + dut.clock.step(2) + dut.io.out.rd.cmd.bits.addr expect 0x40.U + dut.io.out.rd.cmd.ready #= true.B + dut.io.ins(0).rd.cmd.ready expect true.B + dut.io.ins(1).rd.cmd.ready expect false.B + dut.clock.step() + dut.io.ins(0).rd.cmd.valid #= false.B + dut.io.out.rd.cmd.bits.addr expect 0x80.U + dut.clock.step() + dut.io.ins(1).rd.cmd.valid #= false.B + + // The route queue is full; no third request can be accepted. + dut.io.ins(0).rd.cmd.valid #= true.B + dut.io.ins(0).rd.cmd.bits.addr #= 0xc0.U + dut.io.ins(0).rd.cmd.ready expect false.B + dut.io.out.rd.rsp.valid #= true.B + dut.io.out.rd.rsp.bits.data #= 0x1234.U + dut.io.ins(1).rd.rsp.ready #= true.B + dut.io.ins(0).rd.rsp.valid expect true.B + dut.io.ins(1).rd.rsp.valid expect false.B + dut.io.out.rd.rsp.ready expect false.B + dut.clock.step(3) + dut.io.ins(0).rd.rsp.bits.data expect 0x1234.U + + dut.io.ins(0).rd.rsp.ready #= true.B + dut.clock.step() + dut.io.out.rd.rsp.bits.data #= 0x5678.U + dut.io.ins(1).rd.rsp.valid expect true.B + dut.io.ins(1).rd.rsp.bits.data expect 0x5678.U + dut.io.ins(0).rd.cmd.ready expect true.B + dut.clock.step() + dut.io.ins(0).rd.cmd.valid #= false.B + dut.io.out.rd.rsp.bits.data #= 0x9abc.U + dut.io.ins(0).rd.rsp.valid expect true.B + dut.io.ins(0).rd.rsp.bits.data expect 0x9abc.U + dut.clock.step() + dut.io.out.rd.rsp.valid #= false.B + dut.io.ins.foreach(_.rd.rsp.valid.expect(false.B)) + } + } +} diff --git a/chipmunk/test/src/acorn/AcornSpecSupport.scala b/chipmunk/test/src/acorn/AcornSpecSupport.scala new file mode 100644 index 0000000..0582dc5 --- /dev/null +++ b/chipmunk/test/src/acorn/AcornSpecSupport.scala @@ -0,0 +1,38 @@ +package chipmunk.test.acorn + +import chisel3.* + +import chipmunk.acorn.AcornIO +import chipmunk.test.ChipmunkFlatSpec + +private[acorn] trait AcornSpecSupport extends ChipmunkFlatSpec { + protected def reset(dut: Module): Unit = { + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + } + + protected def initMaster(port: AcornIO): Unit = { + port.rd.cmd.valid #= false.B + port.rd.cmd.bits.addr #= 0.U + port.rd.rsp.ready #= false.B + port.wr.cmd.valid #= false.B + port.wr.cmd.bits.addr #= 0.U + port.wr.cmd.bits.data #= 0.U + port.wr.cmd.bits.strobe #= 0.U + port.wr.rsp.ready #= false.B + } + + protected def initSlave(port: AcornIO): Unit = { + port.rd.cmd.ready #= false.B + port.rd.rsp.valid #= false.B + port.rd.rsp.bits.data #= 0.U + port.rd.rsp.bits.error #= false.B + port.wr.cmd.ready #= false.B + port.wr.rsp.valid #= false.B + port.wr.rsp.bits.error #= false.B + } + + protected def bit(value: Bool): Boolean = value.peek().litToBoolean + protected def number(value: UInt): BigInt = value.peek().litValue +} diff --git a/docs/acorn.md b/docs/acorn.md index f1189cd..59170d3 100644 --- a/docs/acorn.md +++ b/docs/acorn.md @@ -1,92 +1,170 @@ # 🌰 Acorn Bus -Acorn Bus 是 Chipmunk 预置的一款片上总线,它旨在提供一款简单但高效的片上总线的硬件实现。相比 AMBA AXI 等成熟的片上总线协议,Acorn Bus 简化了许多功能,更加易于硬件实现。 - -Acorn Bus 的读写信号是分离的,读和写分别有一组 Command 通道和 Response 通道。Command 通道用于 Master 向 Slave 发送读或写的请求,Response 通道用于 Slave 向 Master 返回读或写的响应。Command 和 Response 通道都采用 ready-valid 握手协议来解耦,即当且仅当该通道的 ready 和 valid 信号都有效时完成一次传输。 - -```mermaid -sequenceDiagram - participant M as Master - participant S as Slave - M ->> S: Command 0 - S -->> M: Response 0 - M ->> S: Command 1 - M ->> S: Command 2 - S -->> M: Response 1 - S -->> M: Response 2 +Acorn 是用于模块内部地址访问的轻量接口,适合配置寄存器、SRAM 和多 bank 存储连接。 +读写分别使用 Command 和 Response 通道,写地址、数据和字节掩码放在同一个 Command 中。 +所有通道采用 Stream ready/valid 握手,在 `valid && ready` 时完成传输。 + +## 协议约定 + +- 地址以字节为单位,每条命令访问一个完整数据字,并要求自然对齐。 + 例如,64 位接口的数据字起始地址为 `0x00、0x08、0x10…`。 +- `valid` 生效后,发送方必须保持 `valid` 和整个 payload,直到握手完成。 + 向下游发送的 valid 不依赖下游 ready 的组合值。 +- 每个接受的命令恰好对应一个响应,包括错误访问。端点不能在没有对应命令时发起响应。 +- 每个 master 的读响应按其读命令顺序返回,写响应按其写命令顺序返回。 + 读写之间及不同 master 之间没有隐含的全局顺序。 +- 写响应表示端点定义的本次写操作已完成。需要“先写后读”时,master 应收到写响应后再发读命令。 +- `strobe(i)=1` 使能 `data(8*i+7, 8*i)`。全零 strobe 不修改数据,仍对应一个写响应。 +- `error=false` 表示成功,`error=true` 表示失败。Crossbar 的错误端点返回零读数据。 + +### 通道和字段 + +| 通道 | Payload 字段 | master 侧 payload / valid 方向 | +| --- | --- | --- | +| `rd.cmd` | `addr` | 输出 | +| `rd.rsp` | `data, error` | 输入 | +| `wr.cmd` | `addr, data, strobe` | 输出 | +| `wr.rsp` | `error` | 输入 | + +每个通道的 ready 方向与其 valid 方向相反。数据宽度为不小于 8 的二次幂,strobe 宽度为数据字节数。 + +## 组件 + +使用 `import chipmunk.acorn.*`。源码在 `chipmunk/src/acorn/`,测试在 +`chipmunk/test/src/acorn/`,测试包名为 `chipmunk.test.acorn`。 + +| 文件 | 职责 | +| --- | --- | +| `AcornIO.scala` | IO 和读写命令、响应 payload | +| `AcornConfig.scala` | 参数、容量、仲裁方式和静态地址映射配置 | +| `AcornRouting.scala` | 内部共用的来源/目标 FIFO 及 Stream 路由逻辑 | +| `AcornMux.scala` | 多 master 到一个 slave 的命令仲裁和响应分发 | +| `AcornDemux.scala` | 一个 master 到多个 slave 的命令分发和响应排序 | +| `AcornCrossbar.scala` | 每 master 一个 Demux、每 slave 一个 Mux,并完成地址转换 | +| `AcornErrorPoint.scala` | 可独立实例化的错误响应端点 | + +Mux 的来源 FIFO 保存命令由哪个 master 发出;Demux 的目标 FIFO 保存命令发往哪个 slave。 +它们在命令握手时记录路由,在相应响应握手时移除记录。不同 slave 独立仲裁,可以同时接受不同 master 的访问。 + +## Crossbar 的配置 + +| 配置 | 含义 | +| --- | --- | +| `params` | 公共数据宽度及 master 侧字节地址宽度;数据宽度为不小于 8 的二次幂 | +| `numMasters` | master 数量 | +| `slaves` | 按输出端口顺序排列的 slave 配置;名字必须唯一,地址窗口不能重叠 | +| `name / base / size` | slave 名字和全局地址区间 `[base, base + size)` | +| `localBase` | slave 看到的地址是 `globalAddr - base + localBase`,默认从零开始 | +| `localAddrWidth` | 可选的 slave 地址宽度;默认由本地地址窗口自动推导 | +| `readable / writable` | slave 支持的访问方向,默认均支持 | +| `masterOutstanding` | 每个 master 每方向的总在途容量,跨所有目标统计 | +| `slaveOutstanding` | 每个 slave 每方向的总在途容量,跨所有 master 统计 | +| `arbitration` | RoundRobin 默认,或 LowerFirst 固定低索引优先 | +| `connections` | 可选的每 master 可达 slave 名字集合;None 表示全部连接 | + +全局窗口、本地基址以及窗口大小都必须按一个数据字对齐。窗口覆盖范围必须同时适配全局和本地地址宽度。 +一个 slave 当前对应一个连续窗口。交错 bank、多个窗口别名以及数据宽度转换留给独立映射或适配组件。 + +```scala +import chisel3.* +import chipmunk.acorn.* + +val bus = AcornParams(dataWidth = 64, addrWidth = 32) +val config = AcornCrossbarConfig( + params = bus, + numMasters = 2, + slaves = Seq( + AcornSlaveConfig("bank0", base = BigInt("10000000", 16), size = 0x10000), + AcornSlaveConfig("bank1", base = BigInt("10010000", 16), size = 0x10000), + AcornSlaveConfig("csr", base = BigInt("20000000", 16), size = 0x1000) + ), + masterOutstanding = AcornOutstanding(read = 4, write = 4), + slaveOutstanding = AcornOutstanding(read = 8, write = 4), + arbitration = AcornArbitration.RoundRobin, + connections = Some(Seq( + Set("bank0", "bank1", "csr"), // master 0 + Set("bank0", "bank1") // master 1 + )) +) +val crossbar = Module(new AcornCrossbar(config)) + +// crossbar.io.ins(0) <> cpu.io.acorn +// crossbar.io.ins(1) <> dma.io.acorn +// crossbar.slave("bank0") <> bank0.io.access +// crossbar.slave("bank1") <> bank1.io.access +// crossbar.slave("csr") <> registers.io.access ``` -## 信号定义 - -### Write Command Channel (`wr.cmd`) - -| Signal | Direction | Bit Width | Description | -|----------|-----------|----------------|------------------------------------------| -| `addr` | Output | ADDR_WIDTH | The address to be written. | -| `data` | Output | DATA_WIDTH | The data to be written. | -| `strobe` | Output | DATA_WIDTH / 8 | The byte mask of `wdata`. | -| `valid` | Output | 1 | The valid signal of the command channel. | -| `ready` | Input | 1 | The ready signal of the command channel. | - -### Write Response Channel (`wr.rsp`) - -| Signal | Direction | Bit Width | Description | -|---------|-----------|-----------|-------------------------------------------| -| `error` | Output | 1 | Whether the write access fails. | -| `valid` | Output | 1 | The valid signal of the response channel. | -| `ready` | Input | 1 | The ready signal of the response channel. | - -### Read Command Channel (`rd.cmd`) - -| Signal | Direction | Bit Width | Description | -|---------|-----------|------------|------------------------------------------| -| `addr` | Output | ADDR_WIDTH | The address to be read. | -| `valid` | Output | 1 | The valid signal of the command channel. | -| `ready` | Input | 1 | The ready signal of the command channel. | - -### Read Response Channel (`rd.rsp`) - -| Signal | Direction | Bit Width | Description | -|---------|-----------|------------|-------------------------------------------| -| `rdata` | Output | DATA_WIDTH | The readout data. | -| `error` | Output | 1 | Whether the read access fails. | -| `valid` | Output | 1 | The valid signal of the response channel. | -| `ready` | Input | 1 | The ready signal of the response channel. | - -以上信号方向(Direction)是对于 Master 侧而言的,对于 Slave 侧而言则相反(下同)。 - -其中,写掩码信号 `wr.cmd.strobe` 用于指示写数据的有效字节,其行为和 AMBA AXI 的 `strb` 信号类似。当且仅当 `strobe` 的某一位为 1 时,对应的 `wr.cmd.data` 的对应字节会被写入。 例如,当 `strobe` 的第 0 bit 为低时,`data` 的最低字节(最低 8 bit)有效;当 `strobe` 的第 1 bit 为高时,`data` 的第 8 ~ 15 位有效,以此类推。 - -读/写响应状态信号 `wr/rd.rsp.error` 用于指示读/写操作是否成功,`error` 为 0 时表示操作成功。 - -### 传输时序 - -如下图所示,Master 先后完成了: -1. 写入数据 `0xA` 到地址 `0x0`; -2. 读取地址 `0x1` 的数据,返回 `0xB`; -3. 读取地址 `0x2` 的数据,返回 `0xC`。 - -![acorn-transaction-waveform](./assets/acorn-transaction-waveform.jpg) - -为了简洁,图中未绘出 `strobe` 信号和 `error` 信号。 - -传输过程中一些关键的要点: -- Command 和 Response 通道均要求发起侧不撤回(irrevocable),即 `valid` 一旦生效直到 `ready` 生效前其他信号不可发生改变,`valid` 也不能拉低。 -- 和其他的 ready-valid 握手协议一样,Command 和 Response 通道的 `valid` 不能依赖于 `ready`,以避免组合逻辑环。 -- 在没有从 Command 通道收到有效请求时,Slave 不可以发起 Response 通道的传输请求,即不得拉高 `valid`。 - -## 在 Chipmunk 中的实现 - -Acorn Bus 的 IO 定义:`chipmunk.acorn.AcornIO` +本例三个 slave 的地址宽度分别推导为 16、16、12 位;slave 端口是 MixedVec,允许不同地址宽度。 +数据宽度始终为 64 位。如果端点有固定地址宽度,设置 `localAddrWidth = Some(width)`。 +设置 `localBase = base` 可让端点继续看到全局地址,相应本地地址宽度必须足够。 + +## 单独使用 Mux 和 Demux + +```scala +val mux = Module(new AcornMux( + params = AcornParams(64, 16), + numMasters = 2, + outstanding = AcornOutstanding(8, 4) +)) +// mux.io.ins(0) <> master0 +// mux.io.ins(1) <> master1 +// slave <> mux.io.out + +val demux = Module(new AcornDemux( + params = AcornParams(64, 32), + numSlaves = 4, + outstanding = AcornOutstanding(4, 4) +)) +demux.io.rdSelect := demux.io.in.rd.cmd.bits.addr(17, 16) +demux.io.wrSelect := demux.io.in.wr.cmd.bits.addr(17, 16) +// demux.io.in <> master +// 各端点连接 demux.io.outs;Demux 本身保留原地址,不做地址转换。 +``` -此外,Chipmunk 还提供了 Acorn Bus 与 AXI、AXI-Lite 等其他总线的转换桥接模块。 +裸 Demux 的无效 selector 会阻塞命令;Crossbar 自动将未映射、未对齐、方向不允许或连接不允许的命令送到 +该 master 私有的错误端点,因此这些情况仍产生恰好一个按序错误响应。 -## 与 AMBA AXI 总线的差异 +## 单独使用错误端点 -Acorn Bus 与 AMBA AXI 相比,在功能上有所简化。主要区别如下: -- Acorn Bus 和 AXI 一样,同样实现了读写分离,但 AXI 的写数据和写地址是独立传输的,而 Acorn Bus 的写数据和其他写控制信号是一起传输的; -- Acorn Bus 支持多周期连续传输,但 Command 通道的每个有效握手周期 Master 都需要给出一个地址。 -- Acorn Bus 不支持乱序返回,即 Slave 必须按照 Master 的请求顺序返回数据。 -- Acorn Bus 和 AXI 一样,都支持 Write Byte Mask (即 strobe)、Outstanding Transaction 等特性。 +```scala +val errorPoint = Module(new AcornErrorPoint( + AcornParams(64, 32), AcornOutstanding(read = 4, write = 4) +)) +// errorPoint.io.access <> master +``` -简单(但不准确)地说,用户可以将 Acorn Bus 当作一个 AW 和 W 通道合并且缺少 `id`、`burst`、`prot` 等控制信号(和他们对应的高级特性)的 AXI 总线。 +每个已接受的读写命令都会产生一个 `error=true` 的响应,读响应的数据为零。 + +## 时序和组合约束 + +- 每个 master 的读响应按其读命令顺序返回,写响应按写命令顺序返回;读写之间和不同 master 之间没有全局顺序。 +- 每个 slave 必须按各方向的命令接受顺序返回响应;每个已接受的命令恰好有一个响应。 +- valid 生效后,发送方在握手前必须保持 valid 和 payload。写 strobe 为字节使能;全零 strobe 仍产生写响应。 +- Mux 仲裁器只在命令阻塞期间锁定选择;命令接受后可以仲裁其他 master,响应归属由 FIFO 保存。 +- 命令可组合通过路由器;路由记录是寄存的。因此即使端点立即提供响应,首次向上游返回也要等路由记录可见。 + 端点不能等待这次响应握手才接受命令,响应必须能保持到 ready。 +- 记录 FIFO 使用 `pipe=false, flow=false`,满时不会利用同周期响应释放的空间。深度 1 会限制流水吞吐; + 按端点延迟增大深度可以提高吞吐,首版默认深度 4。 +- 在 Crossbar 中,master 接受、slave 接受和两层路由记录是同一次命令握手。不要在 Demux 和 Mux 之间任意加入 + 独立接收命令的 Queue;不同目标上的命令发出顺序变化可能形成循环响应等待。 +- 响应背压和跨目标按序返回会造成队首阻塞。首版不提供 master 之间的完全阻塞隔离。 +- 所有组件工作于同一时钟域,连接端点应一致复位;复位取消在途事务。跨时钟桥后续单独设计。 + +Mux 的在途上限是所有 master 的合计,Demux 的上限是一个 master 跨所有目标的合计。 +单端口 SRAM 的读写仲裁、同地址读写冲突、寄存器字段副作用等,由端点负责。 +首版不包含 burst、线上 ID、乱序返回或原子读改写操作。 + +## 验证 + +测试覆盖来源归属、阻塞时锁定、跨目标按序返回、非法 selector、非零本地基址进位、全地址空间窗口、 +深度 1/4 的多 master 并行读写与随机命令/响应背压,以及配置和访问错误。 +使用项目现有的 `ChipmunkFlatSpec`、`#=` 和 `expect`。 + +```bash +mill chipmunk.test.testOnly \ + chipmunk.test.acorn.AcornConfigSpec \ + chipmunk.test.acorn.AcornMuxSpec \ + chipmunk.test.acorn.AcornDemuxSpec \ + chipmunk.test.acorn.AcornCrossbarSpec +``` From fc3f322ffcd414a2f690da5229251726866bd50a Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Sun, 4 Oct 2026 17:07:56 +0800 Subject: [PATCH 13/26] Port RegBank to Scala 3 and Acorn with validated configuration Use a per-field Bool for write-once locking, and exclude zero-mask and locked software writes from update arbitration. Keep hardware assignments available after a software write-once lock. Add pure Scala configuration with BigInt reset values and local byte offsets. Validate names, addresses, field ranges, reset values, and field IO aliases. Implement field IO with Record and retain the three-argument RegBank constructor and existing field keys. Add configuration and protocol regression tests, shorten Scaladoc, and document byte addressing, response timing, and collision semantics. Validation: strict RegBank and test compilation, scalafmt checks, and all 86 Chipmunk tests passing (including 12 new RegBank tests). --- chipmunk/src/regbank/RegBank.scala | 67 +++ chipmunk/src/regbank/RegBankConfig.scala | 80 ++++ chipmunk/src/regbank/RegBankFieldIO.scala | 27 ++ chipmunk/src/regbank/RegField.scala | 62 +++ chipmunk/src/regbank/RegFieldAccessType.scala | 160 ++++++++ .../test/src/regbank/RegBankConfigSpec.scala | 50 +++ chipmunk/test/src/regbank/RegBankSpec.scala | 384 ++++++++++++++++++ .../test/src/regbank/RegBankSpecSupport.scala | 62 +++ docs/acorn.md | 2 + docs/regbank.md | 78 ++++ 10 files changed, 972 insertions(+) create mode 100644 chipmunk/src/regbank/RegBank.scala create mode 100644 chipmunk/src/regbank/RegBankConfig.scala create mode 100644 chipmunk/src/regbank/RegBankFieldIO.scala create mode 100644 chipmunk/src/regbank/RegField.scala create mode 100644 chipmunk/src/regbank/RegFieldAccessType.scala create mode 100644 chipmunk/test/src/regbank/RegBankConfigSpec.scala create mode 100644 chipmunk/test/src/regbank/RegBankSpec.scala create mode 100644 chipmunk/test/src/regbank/RegBankSpecSupport.scala create mode 100644 docs/regbank.md diff --git a/chipmunk/src/regbank/RegBank.scala b/chipmunk/src/regbank/RegBank.scala new file mode 100644 index 0000000..3058ca7 --- /dev/null +++ b/chipmunk/src/regbank/RegBank.scala @@ -0,0 +1,67 @@ +package chipmunk +package regbank + +import chisel3.* +import chisel3.util.{Cat, Mux1H} + +import chipmunk.acorn.{AcornIO, AcornParams} + +/** Acorn register bank with independent one-entry read and write response buffers. + * + * Commands update fields and sample read data when accepted. Read side effects return the previous value; simultaneous + * reads and writes also read the previous value. Responses stay stable until accepted. + */ +class RegBank(val config: RegBankConfig) extends Module { + def this(addrWidth: Int, dataWidth: Int, regs: Seq[RegElementConfig]) = + this(RegBankConfig(AcornParams(dataWidth, addrWidth), regs)) + + val regsConfig: Seq[RegElementConfig] = config.regs + val io = IO(new Bundle { + val access = Slave(new AcornIO(config.params)) + val fields = Master(new RegBankFieldIO(config.regs)) + }) + + private val wrPending = RegInit(false.B) + private val rdPending = RegInit(false.B) + private val wrError = RegInit(false.B) + private val rdError = RegInit(false.B) + private val rdData = RegInit(0.U(config.params.dataWidth.W)) + + io.access.wr.cmd.ready := !wrPending || io.access.wr.rsp.ready + io.access.rd.cmd.ready := !rdPending || io.access.rd.rsp.ready + io.access.wr.rsp.valid := wrPending + io.access.rd.rsp.valid := rdPending + io.access.wr.rsp.bits.error := wrError + io.access.rd.rsp.bits.error := rdError + io.access.rd.rsp.bits.data := rdData + + private val wrHits = config.regs.map(reg => io.access.wr.cmd.bits.addr === reg.addr.U) + private val rdHits = config.regs.map(reg => io.access.rd.cmd.bits.addr === reg.addr.U) + private val wrBitMask = Cat(io.access.wr.cmd.bits.strobe.asBools.reverse.map(bit => Cat(Seq.fill(8)(bit)))) + private val readWords = config.regs.zipWithIndex.map { case (reg, index) => + val pieces = reg.fields.map { field => + val instance = Module(new RegField(field)) + instance.io.frontdoor.wrEnable := io.access.wr.cmd.fire && wrHits(index) + instance.io.frontdoor.rdEnable := io.access.rd.cmd.fire && rdHits(index) + instance.io.frontdoor.wrData := io.access.wr.cmd.bits.data(field.endOffset, field.baseOffset) + instance.io.frontdoor.wrBitMask := wrBitMask(field.endOffset, field.baseOffset) + io.fields(s"${reg.name}_${field.name}") <> instance.io.backdoor + (instance.io.frontdoor.rdData.pad(config.params.dataWidth) << field.baseOffset)(config.params.dataWidth - 1, 0) + } + pieces.reduce(_ | _) + } + + when(io.access.wr.cmd.fire) { + wrPending := true.B + wrError := !wrHits.reduce(_ || _) + }.elsewhen(io.access.wr.rsp.fire) { + wrPending := false.B + } + when(io.access.rd.cmd.fire) { + rdPending := true.B + rdError := !rdHits.reduce(_ || _) + rdData := Mux(rdHits.reduce(_ || _), Mux1H(rdHits, readWords), 0.U) + }.elsewhen(io.access.rd.rsp.fire) { + rdPending := false.B + } +} diff --git a/chipmunk/src/regbank/RegBankConfig.scala b/chipmunk/src/regbank/RegBankConfig.scala new file mode 100644 index 0000000..88c6ec6 --- /dev/null +++ b/chipmunk/src/regbank/RegBankConfig.scala @@ -0,0 +1,80 @@ +package chipmunk +package regbank + +import chipmunk.acorn.AcornParams + +/** Priority of simultaneous software write, read side effect, and hardware update. */ +enum RegFieldCollisionMode { + case WriteReadBackdoor, WriteBackdoorRead, ReadBackdoorWrite + case ReadWriteBackdoor, BackdoorWriteRead, BackdoorReadWrite +} + +/** A field's bit range, reset value, access rule, and optional hardware update port. */ +final case class RegFieldConfig( + name: String, + baseOffset: Int, + bitCount: Int, + initValue: BigInt = 0, + accessType: RegFieldAccessType = RegFieldAccessType.ReadWrite, + collisionMode: RegFieldCollisionMode = RegFieldCollisionMode.BackdoorWriteRead, + backdoorUpdate: Boolean = false, +) { + require(name.matches("[A-Za-z_][A-Za-z0-9_]*"), s"Invalid register field name: $name") + require(baseOffset >= 0 && bitCount > 0, s"Field $name needs a nonnegative offset and positive width.") + require(baseOffset.toLong + bitCount - 1 <= Int.MaxValue, s"Bit range overflows field $name.") + require(initValue >= 0 && initValue.bitLength <= bitCount, s"Reset value does not fit field $name.") + val endOffset: Int = (baseOffset.toLong + bitCount - 1).toInt +} + +/** One register at a local byte offset. Unused bits read as zero and ignore writes. */ +final case class RegElementConfig(name: String, addr: BigInt, fields: Seq[RegFieldConfig]) { + require(name.matches("[A-Za-z_][A-Za-z0-9_]*"), s"Invalid register name: $name") + require(addr >= 0, s"Register $name needs a nonnegative byte offset.") + require(fields.nonEmpty, s"Register $name needs at least one field.") + require(fields.map(_.name).distinct.size == fields.size, s"Duplicate field name in register $name.") + require( + fields.sortBy(_.baseOffset).sliding(2).forall(pair => pair.size < 2 || pair.head.endOffset < pair.last.baseOffset), + s"Overlapping fields in register $name.", + ) +} + +object RegElementConfig { + + /** A register with one field, starting at bit zero. */ + def apply( + name: String, + addr: BigInt, + bitCount: Int, + initValue: BigInt = 0, + accessType: RegFieldAccessType = RegFieldAccessType.ReadWrite, + collisionMode: RegFieldCollisionMode = RegFieldCollisionMode.BackdoorWriteRead, + backdoorUpdate: Boolean = false, + ): RegElementConfig = + RegElementConfig( + name, + addr, + Seq(RegFieldConfig("UNNAMED", 0, bitCount, initValue, accessType, collisionMode, backdoorUpdate)), + ) +} + +/** Register map for a naturally aligned, byte-addressed Acorn endpoint. */ +final case class RegBankConfig(params: AcornParams, regs: Seq[RegElementConfig]) { + require(regs.nonEmpty, "RegBank needs at least one register.") + RegBankConfig.validateNames(regs) + require(regs.map(_.addr).distinct.size == regs.size, "Register byte offsets must be unique.") + for (reg <- regs) { + require(reg.addr.bitLength <= params.addrWidth, s"Byte offset overflows register ${reg.name}.") + require(reg.addr % params.bytesPerWord == 0, s"Register ${reg.name} must be word aligned.") + require(reg.fields.forall(_.endOffset < params.dataWidth), s"Field exceeds the data width in register ${reg.name}.") + } +} + +object RegBankConfig { + private[regbank] def validateNames(regs: Seq[RegElementConfig]): Unit = { + require(regs.map(_.name).distinct.size == regs.size, "Register names must be unique.") + val keys = regs.flatMap(reg => reg.fields.map(field => s"${reg.name}_${field.name}")) + require(keys.distinct.size == keys.size, "Register and field names produce duplicate IO keys.") + val aliases = regs.filter(_.fields.size == 1).map(_.name) + require(!aliases.exists(keys.contains), "A single-field alias conflicts with a register field IO key.") + } +} diff --git a/chipmunk/src/regbank/RegBankFieldIO.scala b/chipmunk/src/regbank/RegBankFieldIO.scala new file mode 100644 index 0000000..fc3075a --- /dev/null +++ b/chipmunk/src/regbank/RegBankFieldIO.scala @@ -0,0 +1,27 @@ +package chipmunk +package regbank + +import scala.collection.immutable.SeqMap + +import chisel3.* + +/** Hardware access by REG_FIELD, or by REG for a single-field register. */ +class RegBankFieldIO(regsConfig: Seq[RegElementConfig]) extends Record with IsMasterSlave { + RegBankConfig.validateNames(regsConfig) + override val isMaster: Boolean = true + override val elements: SeqMap[String, RegFieldBackdoorIO] = SeqMap.from( + regsConfig.flatMap(reg => + reg.fields.map(field => s"${reg.name}_${field.name}" -> Master(new RegFieldBackdoorIO(field))) + ) + ) + + def apply(key: String): RegFieldBackdoorIO = + elements.getOrElse( + key, + regsConfig + .collectFirst { + case reg if reg.name == key && reg.fields.size == 1 => elements(s"${reg.name}_${reg.fields.head.name}") + } + .getOrElse(throw new NoSuchElementException(s"Unknown register field: $key")), + ) +} diff --git a/chipmunk/src/regbank/RegField.scala b/chipmunk/src/regbank/RegField.scala new file mode 100644 index 0000000..7f83561 --- /dev/null +++ b/chipmunk/src/regbank/RegField.scala @@ -0,0 +1,62 @@ +package chipmunk +package regbank + +import chisel3.* +import chisel3.util.PriorityMux + +import chipmunk.stream.Flow + +/** Field value, software access pulses, and an optional hardware assignment port. */ +class RegFieldBackdoorIO(fieldConfig: RegFieldConfig) extends Bundle with IsMasterSlave { + override val isMaster: Boolean = true + val value = Output(UInt(fieldConfig.bitCount.W)) + + /** Accepted software write with a nonzero field mask, including writes ignored by the access rule. */ + val isBeingWritten = Output(Bool()) + + /** Accepted software read of a readable field. */ + val isBeingRead = Output(Bool()) + val backdoorUpdate = + if (fieldConfig.backdoorUpdate) Some(Slave(Flow(UInt(fieldConfig.bitCount.W)))) else None +} + +private[regbank] class RegField(val config: RegFieldConfig) extends Module { + val io = IO(new Bundle { + val frontdoor = new Bundle { + val wrEnable = Input(Bool()) + val rdEnable = Input(Bool()) + val wrData = Input(UInt(config.bitCount.W)) + val wrBitMask = Input(UInt(config.bitCount.W)) + val rdData = Output(UInt(config.bitCount.W)) + } + val backdoor = Master(new RegFieldBackdoorIO(config)) + }) + + private val value = RegInit(config.initValue.U(config.bitCount.W)) + private val written = if (config.accessType.writeOnce) Some(RegInit(false.B)) else None + private val wrTransfer = io.frontdoor.wrEnable && io.frontdoor.wrBitMask =/= 0.U + private val wrAllowed = wrTransfer && !written.getOrElse(false.B) + written.foreach { flag => when(wrTransfer) { flag := true.B } } + + private val choices: Map[String, Option[(Bool, UInt)]] = Map( + "write" -> config.accessType.writeUpdateData(value, io.frontdoor.wrData, wrAllowed).map { data => + wrAllowed -> ((data & io.frontdoor.wrBitMask) | (value & ~io.frontdoor.wrBitMask)) + }, + "read" -> config.accessType.readUpdateData(value, io.frontdoor.rdEnable).map(data => io.frontdoor.rdEnable -> data), + "backdoor" -> io.backdoor.backdoorUpdate.map(flow => flow.fire -> flow.bits), + ) + private val priority = config.collisionMode match { + case RegFieldCollisionMode.WriteReadBackdoor => Seq("write", "read", "backdoor") + case RegFieldCollisionMode.WriteBackdoorRead => Seq("write", "backdoor", "read") + case RegFieldCollisionMode.ReadBackdoorWrite => Seq("read", "backdoor", "write") + case RegFieldCollisionMode.ReadWriteBackdoor => Seq("read", "write", "backdoor") + case RegFieldCollisionMode.BackdoorWriteRead => Seq("backdoor", "write", "read") + case RegFieldCollisionMode.BackdoorReadWrite => Seq("backdoor", "read", "write") + } + value := PriorityMux(priority.flatMap(name => choices(name).toList) :+ (true.B -> value)) + + io.backdoor.value := value + io.backdoor.isBeingWritten := wrTransfer + io.backdoor.isBeingRead := io.frontdoor.rdEnable && !config.accessType.cannotRead.B + io.frontdoor.rdData := (if (config.accessType.cannotRead) 0.U else value) +} diff --git a/chipmunk/src/regbank/RegFieldAccessType.scala b/chipmunk/src/regbank/RegFieldAccessType.scala new file mode 100644 index 0000000..9a84f66 --- /dev/null +++ b/chipmunk/src/regbank/RegFieldAccessType.scala @@ -0,0 +1,160 @@ +package chipmunk +package regbank + +import chisel3.* + +/** Software access rules. RegField applies the byte mask and collision priority. */ +abstract class RegFieldAccessType { + + /** Write-only fields read as zero. Hardware can still access their value. */ + val cannotRead: Boolean = false + + /** Lock software writes after the first accepted write with a nonzero field mask. */ + val writeOnce: Boolean = false + + /** Unmasked next value for a software write; None ignores the write. */ + def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = None + + /** Next value after a software read; None leaves the value unchanged. */ + def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = None +} + +object RegFieldAccessType { + + /** RO: ignore writes. */ + object ReadOnly extends RegFieldAccessType + + /** RW: assign written bits. */ + object ReadWrite extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(wrData) + } + + /** RC: clear after reading. */ + object ReadClear extends RegFieldAccessType { + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(0.U) + } + + /** RS: set all bits after reading. */ + object ReadSet extends RegFieldAccessType { + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(curr.filledOnes) + } + + /** WRC: assign on write, clear after read. */ + object WriteReadClear extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(wrData) + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(0.U) + } + + /** WRS: assign on write, set after read. */ + object WriteReadSet extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(wrData) + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(curr.filledOnes) + } + + /** WC: clear written bits, regardless of write data. */ + object WriteClear extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(0.U) + } + + /** WS: set written bits, regardless of write data. */ + object WriteSet extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr.filledOnes) + } + + /** WSRC: set written bits, clear after read. */ + object WriteSetReadClear extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr.filledOnes) + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(0.U) + } + + /** WCRS: clear written bits, set after read. */ + object WriteClearReadSet extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(0.U) + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(curr.filledOnes) + } + + /** W1C: clear bits written as one. */ + object WriteOneClear extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr & ~wrData) + } + + /** W1S: set bits written as one. */ + object WriteOneSet extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr | wrData) + } + + /** W1T: toggle bits written as one. */ + object WriteOneToggle extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr ^ wrData) + } + + /** W0C: clear bits written as zero. */ + object WriteZeroClear extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr & wrData) + } + + /** W0S: set bits written as zero. */ + object WriteZeroSet extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr | ~wrData) + } + + /** W0T: toggle bits written as zero. */ + object WriteZeroToggle extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr ^ ~wrData) + } + + /** W1SRC: set bits written as one, clear after read. */ + object WriteOneSetReadClear extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr | wrData) + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(0.U) + } + + /** W1CRS: clear bits written as one, set after read. */ + object WriteOneClearReadSet extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr & ~wrData) + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(curr.filledOnes) + } + + /** W0SRC: set bits written as zero, clear after read. */ + object WriteZeroSetReadClear extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr | ~wrData) + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(0.U) + } + + /** W0CRS: clear bits written as zero, set after read. */ + object WriteZeroClearReadSet extends RegFieldAccessType { + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr & wrData) + override def readUpdateData(curr: UInt, rdEnable: Bool): Option[UInt] = Some(curr.filledOnes) + } + + /** WO: assign written bits; reads return zero. */ + object WriteOnly extends RegFieldAccessType { + override val cannotRead: Boolean = true + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(wrData) + } + + /** WOC: clear written bits; reads return zero. */ + object WriteOnlyClear extends RegFieldAccessType { + override val cannotRead: Boolean = true + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(0.U) + } + + /** WOS: set written bits; reads return zero. */ + object WriteOnlySet extends RegFieldAccessType { + override val cannotRead: Boolean = true + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(curr.filledOnes) + } + + /** W1: the first masked software write locks the whole field until reset. */ + object WriteOnce extends RegFieldAccessType { + override val writeOnce: Boolean = true + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(wrData) + } + + /** WO1: write once; reads return zero. */ + object WriteOnlyOnce extends RegFieldAccessType { + override val cannotRead: Boolean = true + override val writeOnce: Boolean = true + override def writeUpdateData(curr: UInt, wrData: UInt, wrEnable: Bool): Option[UInt] = Some(wrData) + } +} diff --git a/chipmunk/test/src/regbank/RegBankConfigSpec.scala b/chipmunk/test/src/regbank/RegBankConfigSpec.scala new file mode 100644 index 0000000..21e771d --- /dev/null +++ b/chipmunk/test/src/regbank/RegBankConfigSpec.scala @@ -0,0 +1,50 @@ +package chipmunk.test.regbank + +import chipmunk.acorn.AcornParams +import chipmunk.regbank.* +import chipmunk.test.ChipmunkFlatSpec + +class RegBankConfigSpec extends ChipmunkFlatSpec { + private val params = AcornParams(16, 8) + private val field = RegFieldConfig("F", 0, 8) + private val reg = RegElementConfig("R", 0, Seq(field)) + + "RegBankConfig" should "accept reusable Scala descriptions and boundary values" in { + val high = RegElementConfig("HIGH", 254, Seq(RegFieldConfig("F", 8, 8, initValue = 255))) + RegBankConfig(params, Seq(reg, high)).regs.last.addr shouldBe 254 + RegBankConfig(params, Seq(reg, high)).regs.last.fields.head.initValue shouldBe 255 + RegBankConfig(AcornParams(32, 16), Seq(reg)).regs shouldBe Seq(reg) + } + + it should "reject invalid names, bit ranges, and reset values" in { + intercept[IllegalArgumentException](field.copy(name = "bad-name")) + intercept[IllegalArgumentException](reg.copy(name = "")) + intercept[IllegalArgumentException](field.copy(baseOffset = -1)) + intercept[IllegalArgumentException](field.copy(bitCount = 0)) + intercept[IllegalArgumentException](field.copy(baseOffset = Int.MaxValue, bitCount = 2)) + intercept[IllegalArgumentException](field.copy(initValue = -1)) + intercept[IllegalArgumentException](field.copy(initValue = 256)) + intercept[IllegalArgumentException](reg.copy(fields = Seq.empty)) + intercept[IllegalArgumentException](reg.copy(fields = Seq(field, field.copy(baseOffset = 8)))) + intercept[IllegalArgumentException](reg.copy(fields = Seq(field, field.copy(name = "G", baseOffset = 7)))) + intercept[IllegalArgumentException](RegBankConfig(params, Seq(reg.copy(fields = Seq(field.copy(bitCount = 17)))))) + } + + it should "reject empty maps, duplicate registers, and invalid byte offsets" in { + intercept[IllegalArgumentException](RegBankConfig(params, Seq.empty)) + intercept[IllegalArgumentException](RegBankConfig(params, Seq(reg, reg.copy(addr = 2)))) + intercept[IllegalArgumentException](RegBankConfig(params, Seq(reg, reg.copy(name = "S")))) + intercept[IllegalArgumentException](reg.copy(addr = -2)) + intercept[IllegalArgumentException](RegBankConfig(params, Seq(reg.copy(addr = 1)))) + intercept[IllegalArgumentException](RegBankConfig(params, Seq(reg.copy(addr = 256)))) + } + + it should "reject flattened IO key collisions and ambiguous single-field aliases" in { + val first = RegElementConfig("A_B", 0, Seq(field.copy(name = "C"))) + val second = RegElementConfig("A", 2, Seq(field.copy(name = "B_C"))) + intercept[IllegalArgumentException](RegBankConfig(params, Seq(first, second))) + val named = RegElementConfig("A", 0, Seq(field.copy(name = "B"))) + val alias = RegElementConfig("A_B", 2, Seq(field)) + intercept[IllegalArgumentException](RegBankConfig(params, Seq(named, alias))) + } +} diff --git a/chipmunk/test/src/regbank/RegBankSpec.scala b/chipmunk/test/src/regbank/RegBankSpec.scala new file mode 100644 index 0000000..e4cbe84 --- /dev/null +++ b/chipmunk/test/src/regbank/RegBankSpec.scala @@ -0,0 +1,384 @@ +package chipmunk.test.regbank + +import chisel3.* + +import chipmunk.Slave +import chipmunk.acorn.* +import chipmunk.regbank.* +import chipmunk.regbank.RegFieldAccessType.* + +private object RegBankSpecDut { + class CrossbarHarness extends Module { + val io = IO(new Bundle { + val ins = Vec(2, Slave(new AcornIO(16, 16))) + val values = Output(Vec(2, UInt(16.W))) + }) + private val crossbar = Module( + new AcornCrossbar(AcornCrossbarConfig(AcornParams(16, 16), 2, Seq(AcornSlaveConfig("csr", 0x1000, 0x100)))) + ) + private val bank = Module(new RegBank(8, 16, Seq(RegElementConfig("A", 0, 16), RegElementConfig("B", 2, 16)))) + crossbar.io.ins <> io.ins + bank.io.access <> crossbar.slave("csr") + io.values(0) := bank.io.fields("A").value + io.values(1) := bank.io.fields("B").value + } +} + +class RegBankSpec extends RegBankSpecSupport { + "RegBank" should "map fields, preserve gaps, and mask writes across byte boundaries" in { + val config = RegBankConfig( + AcornParams(16, 8), + Seq( + RegElementConfig( + "R", + 2, + Seq( + RegFieldConfig("HIGH", 12, 4, initValue = 0xd, accessType = ReadOnly), + RegFieldConfig("LOW", 4, 8, initValue = 0xab, backdoorUpdate = true), + ), + ), + RegElementConfig("S", 0, 16, initValue = 0x1234), + ), + ) + simulate(new RegBank(config)) { dut => + init(dut) + dut.io.fields("S").value expect 0x1234.U + dut.io.fields("S_UNNAMED").value expect 0x1234.U + read(dut, 2, 0xdab0) + write(dut, 2, 0x1234, strobe = 1) + dut.io.fields("R_LOW").value expect 0xa3.U + read(dut, 2, 0xda30) + write(dut, 2, 0x5600, strobe = 2) + dut.io.fields("R_LOW").value expect 0x63.U + read(dut, 2, 0xd630) + val update = dut.io.fields("R_LOW").backdoorUpdate.get + update.valid #= true.B + update.bits #= 0x89.U + dut.clock.step() + update.valid #= false.B + read(dut, 2, 0xd890) + } + } + + it should "implement all built-in access rules with byte masks and read side effects" in { + case class Rule(access: RegFieldAccessType, partial: Int, full: Int, afterRead: Int, readable: Boolean = true) + val rules = Seq( + Rule(ReadOnly, 0x55aa, 0x55aa, 0x55aa), + Rule(ReadWrite, 0x5533, 0xff00, 0xff00), + Rule(ReadClear, 0x55aa, 0x55aa, 0), + Rule(ReadSet, 0x55aa, 0x55aa, 0xffff), + Rule(WriteReadClear, 0x5533, 0xff00, 0), + Rule(WriteReadSet, 0x5533, 0xff00, 0xffff), + Rule(WriteClear, 0x5500, 0, 0), + Rule(WriteSet, 0x55ff, 0xffff, 0xffff), + Rule(WriteSetReadClear, 0x55ff, 0xffff, 0), + Rule(WriteClearReadSet, 0x5500, 0, 0xffff), + Rule(WriteOneClear, 0x5588, 0x0088, 0x0088), + Rule(WriteOneSet, 0x55bb, 0xffbb, 0xffbb), + Rule(WriteOneToggle, 0x5599, 0xaa99, 0xaa99), + Rule(WriteZeroClear, 0x5522, 0x5500, 0x5500), + Rule(WriteZeroSet, 0x55ee, 0x55ff, 0x55ff), + Rule(WriteZeroToggle, 0x5566, 0x5599, 0x5599), + Rule(WriteOneSetReadClear, 0x55bb, 0xffbb, 0), + Rule(WriteOneClearReadSet, 0x5588, 0x0088, 0xffff), + Rule(WriteZeroSetReadClear, 0x55ee, 0x55ff, 0), + Rule(WriteZeroClearReadSet, 0x5522, 0x5500, 0xffff), + Rule(WriteOnly, 0x5533, 0xff00, 0xff00, readable = false), + Rule(WriteOnlyClear, 0x5500, 0, 0, readable = false), + Rule(WriteOnlySet, 0x55ff, 0xffff, 0xffff, readable = false), + Rule(WriteOnce, 0x5533, 0x5533, 0x5533), + Rule(WriteOnlyOnce, 0x5533, 0x5533, 0x5533, readable = false), + ) + val regs = rules.zipWithIndex.map { case (rule, index) => + RegElementConfig(s"R$index", 2 * index, 16, initValue = 0x55aa, accessType = rule.access) + } + simulate(new RegBank(8, 16, regs)) { dut => + init(dut) + for ((rule, index) <- rules.zipWithIndex) { + val field = dut.io.fields(s"R$index") + write(dut, 2 * index, 0xffff, strobe = 0) + field.value expect 0x55aa.U + write(dut, 2 * index, 0x0f33, strobe = 1) + field.value expect rule.partial.U + write(dut, 2 * index, 0xff00) + field.value expect rule.full.U + read(dut, 2 * index, if (rule.readable) rule.full else 0) + field.value expect rule.afterRead.U + read(dut, 2 * index, if (rule.readable) rule.afterRead else 0) + } + } + } + + it should "unlock both write-once variants on reset" in { + val regs = Seq( + RegElementConfig("R", 0, 16, accessType = WriteOnce), + RegElementConfig("W", 2, 16, accessType = WriteOnlyOnce), + ) + simulate(new RegBank(8, 16, regs)) { dut => + init(dut) + for ((name, addr) <- Seq("R" -> 0, "W" -> 2)) { + write(dut, addr, 0x55aa) + write(dut, addr, 0xaa55) + dut.io.fields(name).value expect 0x55aa.U + } + reset(dut) + for ((name, addr) <- Seq("R" -> 0, "W" -> 2)) { + dut.io.fields(name).value expect 0.U + write(dut, addr, 0xaa55) + dut.io.fields(name).value expect 0xaa55.U + } + } + } + + it should "exclude zero masks and locked writes from collision arbitration" in { + val regs = Seq( + RegElementConfig( + "RC", + 0, + 16, + initValue = 5, + accessType = WriteReadClear, + collisionMode = RegFieldCollisionMode.WriteReadBackdoor, + ), + RegElementConfig( + "HW", + 2, + 16, + initValue = 5, + backdoorUpdate = true, + collisionMode = RegFieldCollisionMode.WriteReadBackdoor, + ), + RegElementConfig( + "ONCE", + 4, + 16, + accessType = WriteOnce, + backdoorUpdate = true, + collisionMode = RegFieldCollisionMode.WriteReadBackdoor, + ), + ) + simulate(new RegBank(8, 16, regs)) { dut => + init(dut) + val port = dut.io.access + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 0.U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 0.U + port.wr.cmd.bits.strobe #= 0.U + dut.io.fields("RC").isBeingWritten expect false.B + dut.clock.step() + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + port.rd.rsp.bits.data expect 5.U + dut.io.fields("RC").value expect 0.U + dut.clock.step() + val hardware = dut.io.fields("HW").backdoorUpdate.get + hardware.valid #= true.B + hardware.bits #= 0x5a.U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 2.U + dut.clock.step() + port.wr.cmd.valid #= false.B + hardware.valid #= false.B + dut.io.fields("HW").value expect 0x5a.U + dut.clock.step() + write(dut, 4, 0x5555) + val once = dut.io.fields("ONCE").backdoorUpdate.get + once.valid #= true.B + once.bits #= 0xabcd.U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 4.U + port.wr.cmd.bits.data #= 0xaaaa.U + port.wr.cmd.bits.strobe #= 3.U + dut.clock.step() + port.wr.cmd.valid #= false.B + once.valid #= false.B + dut.io.fields("ONCE").value expect 0xabcd.U + dut.clock.step() + } + } + + it should "honor all six collision priorities while returning the previous read value" in { + val modes = Seq( + RegFieldCollisionMode.WriteReadBackdoor -> 7, + RegFieldCollisionMode.WriteBackdoorRead -> 7, + RegFieldCollisionMode.ReadBackdoorWrite -> 0, + RegFieldCollisionMode.ReadWriteBackdoor -> 0, + RegFieldCollisionMode.BackdoorWriteRead -> 9, + RegFieldCollisionMode.BackdoorReadWrite -> 9, + ) + val regs = modes.zipWithIndex.map { case ((mode, _), index) => + RegElementConfig( + s"R$index", + 2 * index, + 16, + initValue = 5, + accessType = WriteReadClear, + collisionMode = mode, + backdoorUpdate = true, + ) + } + simulate(new RegBank(8, 16, regs)) { dut => + init(dut) + val port = dut.io.access + for (((_, expected), index) <- modes.zipWithIndex) { + val field = dut.io.fields(s"R$index") + val update = field.backdoorUpdate.get + update.valid #= true.B + update.bits #= 9.U + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= (2 * index).U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= (2 * index).U + port.wr.cmd.bits.data #= 7.U + port.wr.cmd.bits.strobe #= 3.U + dut.clock.step() + update.valid #= false.B + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + port.rd.rsp.bits.data expect 5.U + field.value expect expected.U + dut.clock.step() + } + } + } + + it should "hold response snapshots under backpressure and execute side effects once" in { + val regs = Seq( + RegElementConfig("RC", 0, 16, initValue = 0x1234, accessType = ReadClear, backdoorUpdate = true), + RegElementConfig("RW", 2, 16), + ) + simulate(new RegBank(8, 16, regs)) { dut => + init(dut) + val port = dut.io.access + port.rd.rsp.ready #= false.B + port.wr.rsp.ready #= false.B + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 0.U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 2.U + port.wr.cmd.bits.data #= 0x5678.U + port.wr.cmd.bits.strobe #= 3.U + dut.clock.step() + port.rd.cmd.bits.addr #= 2.U + port.wr.cmd.bits.addr #= 1.U + dut.io.fields("RC").value expect 0.U + dut.io.fields("RW").value expect 0x5678.U + val hardware = dut.io.fields("RC").backdoorUpdate.get + hardware.valid #= true.B + hardware.bits #= 0xcafe.U + dut.clock.step() + hardware.valid #= false.B + for (_ <- 0 until 3) { + port.rd.cmd.ready expect false.B + port.wr.cmd.ready expect false.B + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect 0x1234.U + port.rd.rsp.bits.error expect false.B + port.wr.rsp.valid expect true.B + port.wr.rsp.bits.error expect false.B + dut.io.fields("RC").value expect 0xcafe.U + dut.clock.step() + } + port.rd.rsp.ready #= true.B + port.wr.rsp.ready #= true.B + port.rd.cmd.ready expect true.B + port.wr.cmd.ready expect true.B + dut.clock.step() + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + port.rd.rsp.bits.data expect 0x5678.U + port.rd.rsp.bits.error expect false.B + port.wr.rsp.bits.error expect true.B + dut.clock.step() + port.rd.rsp.valid expect false.B + port.wr.rsp.valid expect false.B + read(dut, 6, 0, error = true) + read(dut, 1, 0, error = true) + write(dut, 6, 0xffff, error = true) + dut.io.fields("RW").value expect 0x5678.U + dut.io.fields("RC").value expect 0xcafe.U + } + } + + it should "accept one read and write per cycle and read the previous value" in { + simulate(new RegBank(8, 16, Seq(RegElementConfig("R", 0, 16)))) { dut => + init(dut) + val port = dut.io.access + port.rd.cmd.valid #= true.B + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.strobe #= 3.U + for (data <- 1 to 8) { + port.wr.cmd.bits.data #= data.U + port.rd.cmd.ready expect true.B + port.wr.cmd.ready expect true.B + dut.clock.step() + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect (data - 1).U + port.wr.rsp.valid expect true.B + dut.io.fields("R").value expect data.U + } + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + dut.clock.step() + port.rd.rsp.valid expect false.B + port.wr.rsp.valid expect false.B + } + } + + it should "serve two masters through AcornCrossbar with local byte offsets" in { + simulate(new RegBankSpecDut.CrossbarHarness) { dut => + dut.io.ins.foreach(initMaster) + reset(dut) + val writeSent = Array.fill(2)(false) + val writeDone = Array.fill(2)(false) + for (cycle <- 0 until 16) { + for (master <- 0 until 2) { + val port = dut.io.ins(master) + port.wr.cmd.valid #= (!writeSent(master)).B + port.wr.cmd.bits.addr #= (0x1000 + 2 * master).U + port.wr.cmd.bits.data #= (0x1234 + master).U + port.wr.cmd.bits.strobe #= 3.U + port.wr.rsp.ready #= (cycle > 3).B + } + for (master <- 0 until 2) { + val port = dut.io.ins(master) + if (port.wr.cmd.ready.peek().litToBoolean && !writeSent(master)) writeSent(master) = true + if (port.wr.rsp.valid.peek().litToBoolean && cycle > 3) { + writeDone(master) shouldBe false + port.wr.rsp.bits.error expect false.B + writeDone(master) = true + } + } + dut.clock.step() + } + writeSent.toSeq shouldBe Seq(true, true) + writeDone.toSeq shouldBe Seq(true, true) + dut.io.values(0) expect 0x1234.U + dut.io.values(1) expect 0x1235.U + val readSent = Array.fill(2)(false) + val readDone = Array.fill(2)(false) + for (cycle <- 0 until 16) { + for (master <- 0 until 2) { + val port = dut.io.ins(master) + port.rd.cmd.valid #= (!readSent(master)).B + port.rd.cmd.bits.addr #= (0x1002 - 2 * master).U + port.rd.rsp.ready #= (cycle > 3).B + } + for (master <- 0 until 2) { + val port = dut.io.ins(master) + if (port.rd.cmd.ready.peek().litToBoolean && !readSent(master)) readSent(master) = true + if (port.rd.rsp.valid.peek().litToBoolean && cycle > 3) { + readDone(master) shouldBe false + port.rd.rsp.bits.data expect (0x1235 - master).U + port.rd.rsp.bits.error expect false.B + readDone(master) = true + } + } + dut.clock.step() + } + readSent.toSeq shouldBe Seq(true, true) + readDone.toSeq shouldBe Seq(true, true) + } + } +} diff --git a/chipmunk/test/src/regbank/RegBankSpecSupport.scala b/chipmunk/test/src/regbank/RegBankSpecSupport.scala new file mode 100644 index 0000000..9f44f98 --- /dev/null +++ b/chipmunk/test/src/regbank/RegBankSpecSupport.scala @@ -0,0 +1,62 @@ +package chipmunk.test.regbank + +import chisel3.* + +import chipmunk.acorn.AcornIO +import chipmunk.regbank.RegBank +import chipmunk.test.ChipmunkFlatSpec + +private[regbank] trait RegBankSpecSupport extends ChipmunkFlatSpec { + protected def initMaster(port: AcornIO): Unit = { + port.rd.cmd.valid #= false.B + port.rd.cmd.bits.addr #= 0.U + port.rd.rsp.ready #= true.B + port.wr.cmd.valid #= false.B + port.wr.cmd.bits.addr #= 0.U + port.wr.cmd.bits.data #= 0.U + port.wr.cmd.bits.strobe #= 0.U + port.wr.rsp.ready #= true.B + } + + protected def reset(dut: Module): Unit = { + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + } + + protected def init(dut: RegBank): Unit = { + initMaster(dut.io.access) + dut.io.fields.elements.values.foreach(_.backdoorUpdate.foreach { flow => + flow.valid #= false.B + flow.bits #= 0.U + }) + reset(dut) + } + + protected def write(dut: RegBank, addr: BigInt, data: BigInt, strobe: BigInt = 3, error: Boolean = false): Unit = { + val port = dut.io.access.wr + port.cmd.valid #= true.B + port.cmd.bits.addr #= addr.U + port.cmd.bits.data #= data.U + port.cmd.bits.strobe #= strobe.U + port.cmd.ready expect true.B + dut.clock.step() + port.cmd.valid #= false.B + port.rsp.valid expect true.B + port.rsp.bits.error expect error.B + dut.clock.step() + } + + protected def read(dut: RegBank, addr: BigInt, data: BigInt, error: Boolean = false): Unit = { + val port = dut.io.access.rd + port.cmd.valid #= true.B + port.cmd.bits.addr #= addr.U + port.cmd.ready expect true.B + dut.clock.step() + port.cmd.valid #= false.B + port.rsp.valid expect true.B + port.rsp.bits.data expect data.U + port.rsp.bits.error expect error.B + dut.clock.step() + } +} diff --git a/docs/acorn.md b/docs/acorn.md index 59170d3..552c480 100644 --- a/docs/acorn.md +++ b/docs/acorn.md @@ -43,6 +43,8 @@ Acorn 是用于模块内部地址访问的轻量接口,适合配置寄存器 | `AcornCrossbar.scala` | 每 master 一个 Demux、每 slave 一个 Mux,并完成地址转换 | | `AcornErrorPoint.scala` | 可独立实例化的错误响应端点 | +配置寄存器端点可使用 [RegBank](regbank.md)。 + Mux 的来源 FIFO 保存命令由哪个 master 发出;Demux 的目标 FIFO 保存命令发往哪个 slave。 它们在命令握手时记录路由,在相应响应握手时移除记录。不同 slave 独立仲裁,可以同时接受不同 master 的访问。 diff --git a/docs/regbank.md b/docs/regbank.md new file mode 100644 index 0000000..1dfd31b --- /dev/null +++ b/docs/regbank.md @@ -0,0 +1,78 @@ +# Register Bank + +`chipmunk.regbank.RegBank` is a memory-mapped register endpoint for `chipmunk.acorn`. +Each register contains one or more fields with independent access rules and optional hardware updates. + +## Configuration + +Configurations are ordinary Scala values and can be created outside a Chisel Module. +Addresses are local byte offsets: a 32-bit bank uses offsets `0x00`, `0x04`, `0x08`, and so on. +Crossbar translates global addresses into these local offsets. + +```scala +import chisel3.* +import chipmunk.acorn.AcornParams +import chipmunk.regbank.* +import chipmunk.regbank.RegFieldAccessType.* + +val config = RegBankConfig(AcornParams(32, 12), Seq( + RegElementConfig("CONTROL", addr = 0x00, bitCount = 32), + RegElementConfig("STATUS", addr = 0x04, fields = Seq( + RegFieldConfig("VERSION", 0, 8, initValue = 1, accessType = ReadOnly), + RegFieldConfig("ERROR", 8, 1, accessType = WriteOneClear, backdoorUpdate = true) + )) +)) + +// Inside a Module: +val bank = Module(new RegBank(config)) +// bank.io.access <> master +val control = bank.io.fields("CONTROL").value +val error = bank.io.fields("STATUS_ERROR") +error.backdoorUpdate.get.valid := hardwareUpdate +error.backdoorUpdate.get.bits := hardwareValue +``` + +The existing `new RegBank(addrWidth, dataWidth, regs)` constructor is also available. +Reset values are `BigInt`, so use `initValue = 0x1234` rather than `0x1234.U`. +`RegFieldCollisionMode` is a Scala enum; its six existing priority names are retained. + +The map must be nonempty. Register names and addresses are unique; field names are unique within each register. +Names start with a letter or underscore and contain only letters, digits, and underscores. +Addresses must fit the address width and align to a data word. Field ranges cannot overlap or exceed the data width, +and reset values must fit their fields. Flattened `REG_FIELD` names and single-field aliases must not conflict. +Unused bits read as zero and ignore writes. + +## Software access + +- Every accepted read or write command has one response. Unmapped or unaligned addresses return an error; read errors return zero. +- Reads sample the current value when the command transfers. Read-clear and read-set side effects occur at that same transfer, + and the response contains the value before the side effect. A simultaneous read and write also reads the previous value. +- Writes use Acorn byte strobes. A zero field mask leaves that field unchanged and does not participate in update arbitration. + An all-zero strobe still receives a normal write response. +- Read-only fields ignore writes; write-only fields read as zero. These accesses do not produce bus errors. +- WriteOnce and WriteOnlyOnce lock the entire field on its first accepted software write with a nonzero field mask, + including a partial write. Zero-mask writes do not lock it. Reset unlocks it; hardware updates remain allowed. +- Read and write responses have separate one-entry buffers. They remain stable under backpressure; side effects are not repeated. + With responses accepted continuously, the bank can accept one read and one write per cycle. Read and write have no implicit ordering. + +## Hardware access + +Use `fields("REG_FIELD")`, or `fields("REG")` for a single-field register. +`value` exposes the current stored value. `isBeingRead` pulses on an accepted read of a readable field; +`isBeingWritten` pulses on an accepted write with a nonzero field mask, even if the access rule ignores it. + +With `backdoorUpdate = true`, the optional Flow port assigns the whole field on each valid cycle. +It is a hardware access port with no backpressure. Concurrent updates use `collisionMode`, which orders software write, +read side effect, and hardware assignment; default priority is hardware, then write, then read. +A zero-mask or locked software write does not block other updates. All signals share the bank's clock and reset. + +## Tests + +```bash +mill chipmunk.test.testOnly \ + chipmunk.test.regbank.RegBankConfigSpec \ + chipmunk.test.regbank.RegBankSpec +``` + +The tests cover configuration errors, all 25 built-in access rules, byte masks, write-once reset, update collisions, +response backpressure, read snapshots, and two-master access through AcornCrossbar. From 035d05056a3a68b07592ca303f95c7654868e99c Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Sun, 4 Oct 2026 22:05:15 +0800 Subject: [PATCH 14/26] Add MapBundle alongside EmptyBundle and reuse it in RegBank Group EmptyBundle and MapBundle in Bundles.scala. Validate nonempty, unique keys and clone unbound field types while preserving their order, widths, and directions. Refactor RegBankFieldIO to use MapBundle while retaining existing field lookup aliases. Add tests for template reuse, IO flips, aggregate cloning, invalid fields, and empty Stream payloads. Validation: strict changed-source and test compilation, scalafmt checks, and all 16 Bundles/RegBank tests passing. --- chipmunk/src/Bundles.scala | 27 ++++++ chipmunk/src/Misc.scala | 3 - chipmunk/src/regbank/RegBankFieldIO.scala | 20 ++--- chipmunk/test/src/BundlesSpec.scala | 105 ++++++++++++++++++++++ 4 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 chipmunk/src/Bundles.scala create mode 100644 chipmunk/test/src/BundlesSpec.scala diff --git a/chipmunk/src/Bundles.scala b/chipmunk/src/Bundles.scala new file mode 100644 index 0000000..bedb75e --- /dev/null +++ b/chipmunk/src/Bundles.scala @@ -0,0 +1,27 @@ +package chipmunk + +import scala.collection.immutable.SeqMap + +import chisel3.* +import chisel3.experimental.requireIsChiselType +import chisel3.reflect.DataMirror + +/** A Bundle with no payload fields. */ +final class EmptyBundle extends Bundle + +/** A Record indexed by unique, nonempty keys, preserving field order and type. + * + * Fields must be unbound Chisel types. Each is cloned to preserve its directions and allow type templates to be + * reused. + */ +abstract class MapBundle[T <: Data](fields: (String, T)*) extends Record { + require(fields.forall(_._1.nonEmpty), "MapBundle keys must be nonempty.") + require(fields.map(_._1).distinct.size == fields.size, "MapBundle keys must be unique.") + + override val elements: SeqMap[String, T] = SeqMap.from(fields.map { case (name, gen) => + requireIsChiselType(gen, s"MapBundle field $name") + name -> DataMirror.internal.chiselTypeClone(gen) + }) + + def apply(key: String): T = elements(key) +} diff --git a/chipmunk/src/Misc.scala b/chipmunk/src/Misc.scala index 0798ed1..e3e8f68 100644 --- a/chipmunk/src/Misc.scala +++ b/chipmunk/src/Misc.scala @@ -4,9 +4,6 @@ import chisel3.* import chisel3.experimental.SourceInfo import chisel3.util.PriorityMux -/** A Bundle with no payload fields. Useful as a typed empty payload. */ -final class EmptyBundle extends Bundle - /** Alias for [[chisel3.util.PriorityMux]] with additional overloads accepting an explicit default. * * When a default is provided, it is selected if no select signal is asserted. diff --git a/chipmunk/src/regbank/RegBankFieldIO.scala b/chipmunk/src/regbank/RegBankFieldIO.scala index fc3075a..a6828ad 100644 --- a/chipmunk/src/regbank/RegBankFieldIO.scala +++ b/chipmunk/src/regbank/RegBankFieldIO.scala @@ -1,21 +1,17 @@ package chipmunk package regbank -import scala.collection.immutable.SeqMap - -import chisel3.* - /** Hardware access by REG_FIELD, or by REG for a single-field register. */ -class RegBankFieldIO(regsConfig: Seq[RegElementConfig]) extends Record with IsMasterSlave { - RegBankConfig.validateNames(regsConfig) - override val isMaster: Boolean = true - override val elements: SeqMap[String, RegFieldBackdoorIO] = SeqMap.from( - regsConfig.flatMap(reg => - reg.fields.map(field => s"${reg.name}_${field.name}" -> Master(new RegFieldBackdoorIO(field))) +class RegBankFieldIO(regsConfig: Seq[RegElementConfig]) + extends MapBundle[RegFieldBackdoorIO]( + regsConfig + .flatMap(reg => reg.fields.map(field => s"${reg.name}_${field.name}" -> Master(new RegFieldBackdoorIO(field))))* ) - ) + with IsMasterSlave { + RegBankConfig.validateNames(regsConfig) + override val isMaster: Boolean = true - def apply(key: String): RegFieldBackdoorIO = + override def apply(key: String): RegFieldBackdoorIO = elements.getOrElse( key, regsConfig diff --git a/chipmunk/test/src/BundlesSpec.scala b/chipmunk/test/src/BundlesSpec.scala new file mode 100644 index 0000000..e4a835a --- /dev/null +++ b/chipmunk/test/src/BundlesSpec.scala @@ -0,0 +1,105 @@ +package chipmunk.test + +import chisel3.* +import chisel3.reflect.DataMirror + +import chipmunk.{EmptyBundle, IsMasterSlave, MapBundle, Master, Slave} +import chipmunk.stream.{Stream, StreamIO} + +private object BundlesSpecDut { + class UIntFields(entries: Seq[(String, UInt)]) extends MapBundle[UInt](entries*) + + class Streams(width: Int) + extends MapBundle[StreamIO[UInt]]( + "forward" -> Master(Stream(UInt(width.W))), + "reverse" -> Slave(Stream(UInt((width + 1).W))), + ) + with IsMasterSlave { + override val isMaster: Boolean = true + } + + class Harness extends Module { + val io = IO(new Bundle { + val in = Slave(new Streams(8)) + val out = Master(new Streams(8)) + val valuesIn = Input(new UIntFields(Seq("small" -> UInt(8.W), "wide" -> UInt(16.W)))) + val valuesOut = Output(new UIntFields(Seq("small" -> UInt(8.W), "wide" -> UInt(16.W)))) + val eventIn = Slave(Stream(new EmptyBundle)) + val eventOut = Master(Stream(new EmptyBundle)) + }) + val copies = Wire(Vec(2, chiselTypeOf(io.valuesIn))) + copies(0) := io.valuesIn + copies(1) := copies(0) + io.valuesOut := copies(1) + io.out <> io.in + io.eventOut << io.eventIn + } + + class BoundField extends Module { + val input = IO(Input(UInt(8.W))) + val invalid = new UIntFields(Seq("bound" -> input)) + } +} + +class BundlesSpec extends ChipmunkFlatSpec { + "MapBundle" should "preserve field order, types, widths, and directions while cloning templates" in { + val template = Input(UInt(8.W)) + val fields = new BundlesSpecDut.UIntFields(Seq("z" -> template, "a" -> template, "wide" -> Output(UInt(16.W)))) + fields.elements.keys.toSeq shouldBe Seq("z", "a", "wide") + val typed: UInt = fields("wide") + typed.getWidth shouldBe 16 + fields("z").getWidth shouldBe 8 + (fields("z") eq template) shouldBe false + (fields("z") eq fields("a")) shouldBe false + DataMirror.specifiedDirectionOf(fields("z")) shouldBe SpecifiedDirection.Input + DataMirror.specifiedDirectionOf(fields("wide")) shouldBe SpecifiedDirection.Output + intercept[NoSuchElementException](fields("missing")) + } + + it should "allow an empty field map" in { + new BundlesSpecDut.UIntFields(Seq.empty).elements shouldBe empty + } + + it should "reject empty or duplicate keys and hardware values" in { + intercept[IllegalArgumentException](new BundlesSpecDut.UIntFields(Seq("" -> UInt(8.W)))) + intercept[IllegalArgumentException](new BundlesSpecDut.UIntFields(Seq("x" -> UInt(8.W), "x" -> UInt(16.W)))) + intercept[ExpectedChiselTypeException](new BundlesSpecDut.UIntFields(Seq("literal" -> 0.U))) + intercept[ExpectedChiselTypeException] { + simulate(new BundlesSpecDut.BoundField) { dut => dut.clock.step() } + } + } + + "MapBundle and EmptyBundle" should "support IO flips, aggregate cloning, bulk connections, and empty Stream payloads" in { + simulate(new BundlesSpecDut.Harness) { dut => + dut.io.valuesIn("small") #= 0xa5.U + dut.io.valuesIn("wide") #= 0xabcd.U + dut.io.valuesOut("small") expect 0xa5.U + dut.io.valuesOut("wide") expect 0xabcd.U + + dut.io.in("forward").valid #= true.B + dut.io.in("forward").bits #= 0x5a.U + dut.io.out("forward").ready #= false.B + dut.io.out("forward").valid expect true.B + dut.io.out("forward").bits expect 0x5a.U + dut.io.in("forward").ready expect false.B + + dut.io.out("reverse").valid #= true.B + dut.io.out("reverse").bits #= 0x1ab.U + dut.io.in("reverse").ready #= true.B + dut.io.in("reverse").valid expect true.B + dut.io.in("reverse").bits expect 0x1ab.U + dut.io.out("reverse").ready expect true.B + + dut.io.eventIn.valid #= true.B + dut.io.eventOut.ready #= false.B + dut.io.eventOut.valid expect true.B + dut.io.eventIn.ready expect false.B + dut.clock.step() + dut.io.out("forward").ready #= true.B + dut.io.in("forward").ready expect true.B + dut.io.eventOut.ready #= true.B + dut.io.eventIn.ready expect true.B + dut.clock.step() + } + } +} From ed62f8be95d10711d471e79d6067e73ab2c3ed1e Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Sun, 4 Oct 2026 22:50:56 +0800 Subject: [PATCH 15/26] Rename Misc.scala to MuxPriority.scala --- chipmunk/src/{Misc.scala => MuxPriority.scala} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename chipmunk/src/{Misc.scala => MuxPriority.scala} (100%) diff --git a/chipmunk/src/Misc.scala b/chipmunk/src/MuxPriority.scala similarity index 100% rename from chipmunk/src/Misc.scala rename to chipmunk/src/MuxPriority.scala From e2ddddf3cbdba6c918de9c13a6302a7927b2a98d Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Mon, 5 Oct 2026 11:58:53 +0800 Subject: [PATCH 16/26] Add masked Acorn SRAM adapter and simulations Support byte-masked 1RW and 1R1W SRAM interfaces with reserved read-response capacity and ordered responses. Add asynchronous-reset, backpressure, arbitration, and throughput tests, and document the SRAM backend requirements. Validation: all 111 Chipmunk tests pass; strict adapter and test compilation and Scalafmt checks pass. --- chipmunk/src/acorn/AcornSramAdapter.scala | 144 ++++++ .../test/src/acorn/AcornSramAdapterSpec.scala | 426 ++++++++++++++++++ docs/acorn.md | 47 +- 3 files changed, 616 insertions(+), 1 deletion(-) create mode 100644 chipmunk/src/acorn/AcornSramAdapter.scala create mode 100644 chipmunk/test/src/acorn/AcornSramAdapterSpec.scala diff --git a/chipmunk/src/acorn/AcornSramAdapter.scala b/chipmunk/src/acorn/AcornSramAdapter.scala new file mode 100644 index 0000000..8cb2812 --- /dev/null +++ b/chipmunk/src/acorn/AcornSramAdapter.scala @@ -0,0 +1,144 @@ +package chipmunk +package acorn + +import chisel3.* +import chisel3.util.{log2Ceil, Queue, ShiftRegister, SRAMInterface} + +enum AcornSramPortMode { + case SinglePort, OneReadOneWrite +} + +/** SRAM geometry, fixed read latency, and response capacities. */ +final case class AcornSramConfig( + params: AcornParams, + numWords: BigInt, + portMode: AcornSramPortMode = AcornSramPortMode.SinglePort, + readLatency: Int = 1, + outstanding: AcornOutstanding = AcornOutstanding(), +) { + require(numWords >= 1, "An Acorn SRAM needs at least one word.") + require(readLatency >= 1, "SRAM read latency must be positive.") + + val sizeBytes: BigInt = numWords * params.bytesPerWord + require(sizeBytes <= (BigInt(1) << params.addrWidth), "SRAM capacity exceeds the Acorn address range.") +} + +/** Connect an Acorn slave to a synchronous, byte-masked 1RW or 1R1W SRAM. + * + * The SRAM must accept an enabled access each cycle, commit writes at that clock edge, and return each read's value + * after exactly `readLatency` cycles. Read responses add one FIFO cycle. Conflicting reads/writes are arbitrated + * round-robin; a 1R1W SRAM can access different words concurrently. Addresses are zero-based local byte offsets. + * Unaligned/out-of-range accesses return errors without accessing SRAM. Zero-strobe writes succeed without writing. + * Reset cancels outstanding transactions but does not clear SRAM. The upstream master must not issue commands during + * reset; reset the connected Acorn routers consistently. + */ +final class AcornSramAdapter(val config: AcornSramConfig) extends Module { + private val params = config.params + private val singlePort = config.portMode == AcornSramPortMode.SinglePort + + val io = IO(new Bundle { + val access = Slave(new AcornIO(params)) + val sram = Flipped( + new SRAMInterface( + memSize = config.numWords, + tpe = Vec(params.bytesPerWord, UInt(8.W)), + numReadPorts = if (singlePort) 0 else 1, + numWritePorts = if (singlePort) 0 else 1, + numReadwritePorts = if (singlePort) 1 else 0, + masked = true, + ) + ) + }) + + private val readResponses = Module( + new Queue(new AcornRdRspChannel(params.dataWidth), entries = config.outstanding.read, pipe = false, flow = false) + ) + private val writeResponses = Module( + new Queue(new AcornWrRspChannel, entries = config.outstanding.write, pipe = false, flow = false) + ) + + io.access.rd.rsp.valid := readResponses.io.deq.valid + io.access.rd.rsp.bits := readResponses.io.deq.bits + readResponses.io.deq.ready := io.access.rd.rsp.ready + + io.access.wr.rsp.valid := writeResponses.io.deq.valid + io.access.wr.rsp.bits := writeResponses.io.deq.bits + writeResponses.io.deq.ready := io.access.wr.rsp.ready + + // Count both pending SRAM results and buffered responses before issuing another read. + private val readCount = RegInit(0.U(log2Ceil(BigInt(config.outstanding.read) + 1).W)) + private val readSpace = readCount < config.outstanding.read.U + private val writeSpace = writeResponses.io.enq.ready + + private def addressError(address: UInt): Bool = { + val aligned = (address & (params.bytesPerWord - 1).U) === 0.U + !aligned || address >= config.sizeBytes.U + } + + private val readError = addressError(io.access.rd.cmd.bits.addr) + private val writeError = addressError(io.access.wr.cmd.bits.addr) + private val readWord = io.access.rd.cmd.bits.addr >> log2Ceil(params.bytesPerWord) + private val writeWord = io.access.wr.cmd.bits.addr >> log2Ceil(params.bytesPerWord) + private val readNeedsSram = !readError + private val writeNeedsSram = !writeError && io.access.wr.cmd.bits.strobe.orR + + private val readCandidate = readSpace && io.access.rd.cmd.valid + private val writeCandidate = writeSpace && io.access.wr.cmd.valid + private val samePortOrWord = if (singlePort) true.B else readWord === writeWord + private val conflict = readCandidate && writeCandidate && readNeedsSram && writeNeedsSram && samePortOrWord + private val preferWrite = RegInit(false.B) + + io.access.rd.cmd.ready := readSpace && (!conflict || !preferWrite) + io.access.wr.cmd.ready := writeSpace && (!conflict || preferWrite) + + when(conflict && (io.access.rd.cmd.fire || io.access.wr.cmd.fire)) { + preferWrite := !preferWrite + } + + private val doRead = io.access.rd.cmd.fire && readNeedsSram + private val doWrite = io.access.wr.cmd.fire && writeNeedsSram + private val writeData = io.access.wr.cmd.bits.data.asTypeOf(Vec(params.bytesPerWord, UInt(8.W))) + private val writeMask = VecInit(io.access.wr.cmd.bits.strobe.asBools) + private val readData = Wire(UInt(params.dataWidth.W)) + + if (singlePort) { + val port = io.sram.readwritePorts(0) + port.enable := doRead || doWrite + port.isWrite := doWrite + port.address := Mux(doWrite, writeWord, readWord) + port.writeData := writeData + port.mask.get := writeMask + readData := port.readData.asUInt + } else { + val readPort = io.sram.readPorts(0) + val writePort = io.sram.writePorts(0) + readPort.enable := doRead + readPort.address := readWord + writePort.enable := doWrite + writePort.address := writeWord + writePort.data := writeData + writePort.mask.get := writeMask + readData := readPort.data.asUInt + } + + // Error reads use the same latency pipeline so they cannot overtake valid reads. + private val readReturned = ShiftRegister(io.access.rd.cmd.fire, config.readLatency, false.B, true.B) + private val returnedError = ShiftRegister(readError, config.readLatency, false.B, true.B) + + readResponses.io.enq.valid := readReturned + readResponses.io.enq.bits.data := Mux(returnedError, 0.U, readData) + readResponses.io.enq.bits.error := returnedError + + writeResponses.io.enq.valid := io.access.wr.cmd.fire + writeResponses.io.enq.bits.error := writeError + + when(io.access.rd.cmd.fire =/= io.access.rd.rsp.fire) { + when(io.access.rd.cmd.fire) { + readCount := readCount + 1.U + }.otherwise { + readCount := readCount - 1.U + } + } + + assert(!readResponses.io.enq.valid || readResponses.io.enq.ready, "SRAM read response has no reserved space.") +} diff --git a/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala b/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala new file mode 100644 index 0000000..314a975 --- /dev/null +++ b/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala @@ -0,0 +1,426 @@ +package chipmunk.test.acorn + +import scala.collection.mutable +import scala.util.Random + +import chisel3.* +import chisel3.util.{ShiftRegister, SRAM} + +import chipmunk.* +import chipmunk.acorn.* +import chipmunk.test.ChipmunkFlatSpec + +private object AcornSramAdapterSpecDut { + final class Harness(config: AcornSramConfig) extends Module with RequireAsyncReset { + val io = IO(new Bundle { + val access = Slave(new AcornIO(config.params)) + val readEnable = Output(Bool()) + val writeEnable = Output(Bool()) + val readWord = Output(UInt(config.params.addrWidth.W)) + val writeWord = Output(UInt(config.params.addrWidth.W)) + }) + + val adapter = Module(new AcornSramAdapter(config)) + adapter.io.access <> io.access + + private val single = config.portMode == AcornSramPortMode.SinglePort + private val memory = SRAM.masked( + config.numWords, + Vec(config.params.bytesPerWord, UInt(8.W)), + numReadPorts = if (single) 0 else 1, + numWritePorts = if (single) 0 else 1, + numReadwritePorts = if (single) 1 else 0, + ) + + if (config.readLatency == 1) { + adapter.io.sram <> memory + } + + if (single) { + val source = adapter.io.sram.readwritePorts(0) + val target = memory.readwritePorts(0) + if (config.readLatency > 1) { + target.address := source.address + target.enable := source.enable + target.isWrite := source.isWrite + target.writeData := source.writeData + target.mask.get := source.mask.get + source.readData := ShiftRegister(target.readData, config.readLatency - 1) + } + io.readEnable := source.enable && !source.isWrite + io.writeEnable := source.enable && source.isWrite + io.readWord := source.address + io.writeWord := source.address + } else { + val readSource = adapter.io.sram.readPorts(0) + val writeSource = adapter.io.sram.writePorts(0) + val readTarget = memory.readPorts(0) + val writeTarget = memory.writePorts(0) + if (config.readLatency > 1) { + readTarget.address := readSource.address + readTarget.enable := readSource.enable + readSource.data := ShiftRegister(readTarget.data, config.readLatency - 1) + writeTarget.address := writeSource.address + writeTarget.enable := writeSource.enable + writeTarget.data := writeSource.data + writeTarget.mask.get := writeSource.mask.get + } + io.readEnable := readSource.enable + io.writeEnable := writeSource.enable + io.readWord := readSource.address + io.writeWord := writeSource.address + } + } +} + +class AcornSramAdapterSpec extends ChipmunkFlatSpec { + private case class Write(address: Int, data: BigInt, strobe: Int) + private case class Result(data: BigInt, error: Boolean) + + for { + mode <- AcornSramPortMode.values.toSeq + (width, addressWidth, words) <- Seq((8, 3, 8), (32, 2, 1), (64, 4, 2)) + } { + "AcornSramAdapter" should s"handle $width-bit words and a full $addressWidth-bit address range with $mode" in { + val config = AcornSramConfig(AcornParams(width, addressWidth), words, mode) + simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => + init(dut) + val port = dut.io.access + val address = config.sizeBytes - config.params.bytesPerWord + val data = (BigInt(1) << width) - 0x53 + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= address.U + port.wr.cmd.bits.data #= data.U + port.wr.cmd.bits.strobe #= ((BigInt(1) << config.params.bytesPerWord) - 1).U + port.wr.cmd.ready expect true.B + dut.clock.step() + port.wr.cmd.valid #= false.B + port.wr.rsp.valid expect true.B + port.wr.rsp.bits.error expect false.B + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= address.U + port.rd.cmd.ready expect true.B + dut.clock.step() + port.rd.cmd.valid #= false.B + dut.clock.step() + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect data.U + port.rd.rsp.bits.error expect false.B + dut.clock.step() + } + } + } + + private def init(dut: AcornSramAdapterSpecDut.Harness): Unit = { + val port = dut.io.access + port.rd.cmd.valid #= false.B + port.rd.cmd.bits.addr #= 0.U + port.rd.rsp.ready #= true.B + port.wr.cmd.valid #= false.B + port.wr.cmd.bits.addr #= 0.U + port.wr.cmd.bits.data #= 0.U + port.wr.cmd.bits.strobe #= 0.U + port.wr.rsp.ready #= true.B + dut.reset #= true.B + dut.clock.step() + dut.reset #= false.B + } + + private def write(dut: AcornSramAdapterSpecDut.Harness, address: Int, data: BigInt): Unit = { + val port = dut.io.access.wr + port.cmd.valid #= true.B + port.cmd.bits.addr #= address.U + port.cmd.bits.data #= data.U + port.cmd.bits.strobe #= 15.U + port.cmd.ready expect true.B + dut.clock.step() + port.cmd.valid #= false.B + port.rsp.valid expect true.B + port.rsp.bits.error expect false.B + dut.clock.step() + } + + for { + mode <- AcornSramPortMode.values.toSeq + latency <- Seq(1, 3) + capacity <- Seq(1, 4) + } { + "AcornSramAdapter" should s"preserve data and ordering with $mode, latency $latency, capacity $capacity" in { + val config = AcornSramConfig(AcornParams(32, 6), 5, mode, latency, AcornOutstanding(capacity, capacity)) + simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => + init(dut) + val memory = Array.tabulate(5)(index => BigInt("12345600", 16) + index) + memory.indices.foreach(index => write(dut, index * 4, memory(index))) + val random = new Random(421 + latency * 17 + capacity) + val reads = Vector.tabulate(100) { index => + index % 8 match { + case 0 => 1 + case 1 => 20 + case 2 => 60 + case _ => random.nextInt(5) * 4 + } + } + val writes = Vector.tabulate(100) { index => + val address = index % 9 match { + case 0 => 3 + case 1 => 20 + case 2 => 60 + case _ => random.nextInt(5) * 4 + } + Write(address, BigInt(32, random), index % 16) + } + val expectedReads = mutable.Queue.empty[Result] + val expectedWrites = mutable.Queue.empty[Boolean] + var heldRead = Option.empty[Result] + var heldWrite = Option.empty[Boolean] + var readSent = 0 + var writeSent = 0 + var readReceived = 0 + var writeReceived = 0 + var cycle = 0 + + def valid(address: Int): Boolean = address % 4 == 0 && address < 20 + + while ((readReceived < reads.size || writeReceived < writes.size) && cycle < 5000) { + val port = dut.io.access + port.rd.cmd.valid #= (readSent < reads.size).B + if (readSent < reads.size) port.rd.cmd.bits.addr #= reads(readSent).U + port.wr.cmd.valid #= (writeSent < writes.size).B + if (writeSent < writes.size) { + val command = writes(writeSent) + port.wr.cmd.bits.addr #= command.address.U + port.wr.cmd.bits.data #= command.data.U + port.wr.cmd.bits.strobe #= command.strobe.U + } + val readReady = cycle >= 40 && (cycle > 4000 || random.nextBoolean()) + val writeReady = cycle >= 65 && (cycle > 4000 || random.nextBoolean()) + port.rd.rsp.ready #= readReady.B + port.wr.rsp.ready #= writeReady.B + + val readFire = readSent < reads.size && port.rd.cmd.ready.peek().litToBoolean + val writeFire = writeSent < writes.size && port.wr.cmd.ready.peek().litToBoolean + val readEnable = dut.io.readEnable.peek().litToBoolean + val writeEnable = dut.io.writeEnable.peek().litToBoolean + readEnable shouldBe (readFire && valid(reads(readSent))) + writeEnable shouldBe (writeFire && valid(writes(writeSent).address) && writes(writeSent).strobe != 0) + if (readEnable && writeEnable) { + mode shouldBe AcornSramPortMode.OneReadOneWrite + dut.io.readWord.peek().litValue should not be dut.io.writeWord.peek().litValue + } + + if (port.rd.rsp.valid.peek().litToBoolean) { + expectedReads.nonEmpty shouldBe true + val current = Result(port.rd.rsp.bits.data.peek().litValue, port.rd.rsp.bits.error.peek().litToBoolean) + current shouldBe expectedReads.head + heldRead.foreach(_ shouldBe current) + if (readReady) { + expectedReads.dequeue() + readReceived += 1 + heldRead = None + } else heldRead = Some(current) + } else heldRead shouldBe None + + if (port.wr.rsp.valid.peek().litToBoolean) { + expectedWrites.nonEmpty shouldBe true + val current = port.wr.rsp.bits.error.peek().litToBoolean + current shouldBe expectedWrites.head + heldWrite.foreach(_ shouldBe current) + if (writeReady) { + expectedWrites.dequeue() + writeReceived += 1 + heldWrite = None + } else heldWrite = Some(current) + } else heldWrite shouldBe None + + if (readFire) { + val address = reads(readSent) + expectedReads.enqueue(if (valid(address)) Result(memory(address / 4), false) else Result(0, true)) + readSent += 1 + } + if (writeFire) { + val command = writes(writeSent) + expectedWrites.enqueue(!valid(command.address)) + if (valid(command.address)) { + val index = command.address / 4 + for (byte <- 0 until 4 if (command.strobe & (1 << byte)) != 0) { + val mask = BigInt(255) << (byte * 8) + memory(index) = (memory(index) & ~mask) | (command.data & mask) + } + } + writeSent += 1 + } + dut.clock.step() + cycle += 1 + } + readReceived shouldBe reads.size + writeReceived shouldBe writes.size + expectedReads shouldBe empty + expectedWrites shouldBe empty + dut.io.access.rd.cmd.valid #= false.B + dut.io.access.wr.cmd.valid #= false.B + dut.clock.step(latency + 4) + dut.io.access.rd.rsp.valid expect false.B + dut.io.access.wr.rsp.valid expect false.B + } + } + } + + for (mode <- AcornSramPortMode.values.toSeq) { + "AcornSramAdapter" should s"sustain one read per cycle with $mode" in { + val config = AcornSramConfig(AcornParams(32, 4), 4, mode) + simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => + init(dut) + val data = Vector.tabulate(4)(index => BigInt(0x12345000 + index)) + data.indices.foreach(index => write(dut, index * 4, data(index))) + val port = dut.io.access.rd + for (index <- 0 until 16) { + port.cmd.valid #= true.B + port.cmd.bits.addr #= ((index % 4) * 4).U + port.cmd.ready expect true.B + port.rsp.valid expect (index >= 2).B + if (index >= 2) { + port.rsp.bits.data expect data((index - 2) % 4).U + port.rsp.bits.error expect false.B + } + dut.clock.step() + } + port.cmd.valid #= false.B + for (index <- 14 until 16) { + port.rsp.valid expect true.B + port.rsp.bits.data expect data(index % 4).U + port.rsp.bits.error expect false.B + dut.clock.step() + } + port.rsp.valid expect false.B + } + } + + "AcornSramAdapter" should s"resolve same-word conflicts fairly and keep read snapshots with $mode" in { + val config = AcornSramConfig(AcornParams(32, 4), 4, mode, 1, AcornOutstanding(4, 4)) + simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => + init(dut) + write(dut, 0, 0x11223344) + val port = dut.io.access + port.rd.rsp.ready #= false.B + port.wr.rsp.ready #= false.B + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 0.U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 0.U + port.wr.cmd.bits.data #= BigInt("aabbccdd", 16).U + port.wr.cmd.bits.strobe #= 15.U + for (cycle <- 0 until 4) { + port.rd.cmd.ready expect (cycle % 2 == 0).B + port.wr.cmd.ready expect (cycle % 2 == 1).B + dut.clock.step() + } + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + dut.clock.step(3) + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect 0x11223344.U + port.rd.rsp.ready #= true.B + dut.clock.step() + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect BigInt("aabbccdd", 16).U + dut.clock.step() + port.rd.rsp.valid expect false.B + port.wr.rsp.ready #= true.B + dut.clock.step(2) + port.wr.rsp.valid expect false.B + } + } + + "AcornSramAdapter" should s"cancel pending responses on reset while retaining SRAM contents with $mode" in { + val config = AcornSramConfig(AcornParams(32, 4), 4, mode, 3, AcornOutstanding(4, 4)) + simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => + init(dut) + write(dut, 0, 0x12345678) + val port = dut.io.access + port.rd.rsp.ready #= false.B + port.wr.rsp.ready #= false.B + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 0.U + dut.clock.step() + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 4.U + port.wr.cmd.bits.data #= 0x44332211.U + port.wr.cmd.bits.strobe #= 15.U + dut.clock.step() + port.wr.cmd.valid #= false.B + dut.reset #= true.B + dut.io.readEnable expect false.B + dut.io.writeEnable expect false.B + port.rd.rsp.valid expect false.B + port.wr.rsp.valid expect false.B + dut.clock.step() + dut.reset #= false.B + port.rd.rsp.ready #= true.B + port.wr.rsp.ready #= true.B + dut.clock.step(5) + port.rd.rsp.valid expect false.B + port.wr.rsp.valid expect false.B + for ((address, data) <- Seq(0 -> BigInt(0x12345678), 4 -> BigInt(0x44332211))) { + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= address.U + port.rd.cmd.ready expect true.B + dut.clock.step() + port.rd.cmd.valid #= false.B + dut.clock.step(3) + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect data.U + port.rd.rsp.bits.error expect false.B + dut.clock.step() + } + } + } + } + + "AcornSramAdapter" should "access different words concurrently and drain responses independently with 1R1W" in { + val config = AcornSramConfig(AcornParams(32, 4), 4, AcornSramPortMode.OneReadOneWrite) + simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => + init(dut) + write(dut, 0, 0x11223344) + write(dut, 4, 0x55667788) + val port = dut.io.access + port.rd.rsp.ready #= false.B + port.wr.rsp.ready #= false.B + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 0.U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 4.U + port.wr.cmd.bits.data #= BigInt("aabbccdd", 16).U + port.wr.cmd.bits.strobe #= 5.U + port.rd.cmd.ready expect true.B + port.wr.cmd.ready expect true.B + dut.io.readEnable expect true.B + dut.io.writeEnable expect true.B + dut.clock.step() + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + port.wr.rsp.valid expect true.B + port.wr.rsp.bits.error expect false.B + dut.clock.step() + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect 0x11223344.U + port.wr.rsp.ready #= true.B + dut.clock.step() + port.wr.rsp.valid expect false.B + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect 0x11223344.U + port.rd.rsp.ready #= true.B + dut.clock.step() + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 4.U + port.rd.cmd.ready expect true.B + dut.clock.step() + port.rd.cmd.valid #= false.B + dut.clock.step() + port.rd.rsp.valid expect true.B + port.rd.rsp.bits.data expect BigInt("55bb77dd", 16).U + port.rd.rsp.bits.error expect false.B + dut.clock.step() + } + } +} diff --git a/docs/acorn.md b/docs/acorn.md index 552c480..343074d 100644 --- a/docs/acorn.md +++ b/docs/acorn.md @@ -42,6 +42,7 @@ Acorn 是用于模块内部地址访问的轻量接口,适合配置寄存器 | `AcornDemux.scala` | 一个 master 到多个 slave 的命令分发和响应排序 | | `AcornCrossbar.scala` | 每 master 一个 Demux、每 slave 一个 Mux,并完成地址转换 | | `AcornErrorPoint.scala` | 可独立实例化的错误响应端点 | +| `AcornSramAdapter.scala` | 字节掩码同步 SRAM 的 1RW / 1R1W 适配器及配置 | 配置寄存器端点可使用 [RegBank](regbank.md)。 @@ -138,6 +139,49 @@ val errorPoint = Module(new AcornErrorPoint( 每个已接受的读写命令都会产生一个 `error=true` 的响应,读响应的数据为零。 +## SRAM adapter + +`AcornSramAdapter` 的 `io.access` 是 Acorn slave,`io.sram` 是 Chisel 的 +`SRAMInterface[Vec[UInt]]`,每个 Vec 元素对应一个字节。支持一个读写口(1RW)或各一个读口、写口(1R1W)。 +SRAM 必须与 adapter 共用时钟、支持字节写掩码,并能每拍接受一次端口操作;写在使能的时钟沿完成, +读数据在配置的 `readLatency` 拍后返回。 + +```scala +import chisel3.* +import chisel3.util.SRAM +import chipmunk.acorn.* + +val config = AcornSramConfig( + params = AcornParams(64, 16), + numWords = 8192, + portMode = AcornSramPortMode.SinglePort, + readLatency = 1, + outstanding = AcornOutstanding(read = 4, write = 4) +) +val adapter = Module(new AcornSramAdapter(config)) +val memory = SRAM.masked( + config.numWords, + Vec(config.params.bytesPerWord, UInt(8.W)), + numReadPorts = 0, + numWritePorts = 0, + numReadwritePorts = 1 +) +adapter.io.sram <> memory +// adapter.io.access <> master +``` + +1R1W 使用 `AcornSramPortMode.OneReadOneWrite`,对应 SRAM 的端口数量为 `(1, 1, 0)`。 +上述 `SRAM.masked` 默认读延迟为一拍;替换为其他 SRAM 后端时,`readLatency` 必须与其实际延迟一致。 +SRAM 容量为 `numWords * bytesPerWord`,adapter 地址是从零开始的局部字节偏移。越界或未对齐的命令返回错误, +不访问 SRAM;合法的零 strobe 写不修改 SRAM,仍返回成功响应。 + +读命令接受时为返回数据预留空间,直到 `rd.rsp.fire` 才释放;响应 FIFO 增加一拍读延迟。 +读写各自按序返回。1RW 的读写请求,以及 1R1W 的同字地址读写请求,采用 round-robin 仲裁; +1R1W 的不同地址读写可同时执行。写响应表示 SRAM 写已完成,而不是仅接收了命令。 + +reset 只用于状态复位,不门控握手或 SRAM 使能。上游在复位期间不得发出命令,并应与 adapter 一致复位。 +复位取消在途事务,但不清除 SRAM 内容,也不撤销已经执行的写。 + ## 时序和组合约束 - 每个 master 的读响应按其读命令顺序返回,写响应按写命令顺序返回;读写之间和不同 master 之间没有全局顺序。 @@ -168,5 +212,6 @@ mill chipmunk.test.testOnly \ chipmunk.test.acorn.AcornConfigSpec \ chipmunk.test.acorn.AcornMuxSpec \ chipmunk.test.acorn.AcornDemuxSpec \ - chipmunk.test.acorn.AcornCrossbarSpec + chipmunk.test.acorn.AcornCrossbarSpec \ + chipmunk.test.acorn.AcornSramAdapterSpec ``` From c8584bc6adbed6b07d9e92e8a1e60fef9092b497 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Mon, 5 Oct 2026 12:50:57 +0800 Subject: [PATCH 17/26] Buffer Acorn SRAM commands to isolate bus timing Add two-entry read and write command queues without flow or pipe bypass. Derive SRAM control from buffered commands and local state, and reserve response capacity at bus acceptance so outstanding limits also cover queued commands. Update latency and arbitration checks, verify bus isolation and response backpressure, and ensure asynchronous reset cancels queued accesses without undoing committed writes. Document the extra command cycle and capacity accounting. Validation: all 115 Chipmunk tests pass, including 25 SRAM tests; strict adapter and test compilation, Scalafmt, and whitespace checks pass. --- chipmunk/src/acorn/AcornSramAdapter.scala | 75 ++++-- .../test/src/acorn/AcornSramAdapterSpec.scala | 219 ++++++++++++++++-- docs/acorn.md | 5 +- 3 files changed, 250 insertions(+), 49 deletions(-) diff --git a/chipmunk/src/acorn/AcornSramAdapter.scala b/chipmunk/src/acorn/AcornSramAdapter.scala index 8cb2812..5844244 100644 --- a/chipmunk/src/acorn/AcornSramAdapter.scala +++ b/chipmunk/src/acorn/AcornSramAdapter.scala @@ -8,7 +8,7 @@ enum AcornSramPortMode { case SinglePort, OneReadOneWrite } -/** SRAM geometry, fixed read latency, and response capacities. */ +/** SRAM geometry, fixed read latency, and outstanding transaction capacities. */ final case class AcornSramConfig( params: AcornParams, numWords: BigInt, @@ -25,8 +25,10 @@ final case class AcornSramConfig( /** Connect an Acorn slave to a synchronous, byte-masked 1RW or 1R1W SRAM. * - * The SRAM must accept an enabled access each cycle, commit writes at that clock edge, and return each read's value - * after exactly `readLatency` cycles. Read responses add one FIFO cycle. Conflicting reads/writes are arbitrated + * Two-entry read/write command queues isolate the bus from SRAM control, without combinational bypass or ready + * propagation. A command takes at least one cycle to reach SRAM; each port can still sustain one access per cycle. The + * SRAM must accept an enabled access each cycle, commit writes at that clock edge, and return each read's value after + * exactly `readLatency` cycles. Read responses add one FIFO cycle. Conflicting reads/writes are arbitrated * round-robin; a 1R1W SRAM can access different words concurrently. Addresses are zero-based local byte offsets. * Unaligned/out-of-range accesses return errors without accessing SRAM. Zero-strobe writes succeed without writing. * Reset cancels outstanding transactions but does not clear SRAM. The upstream master must not issue commands during @@ -50,6 +52,28 @@ final class AcornSramAdapter(val config: AcornSramConfig) extends Module { ) }) + // Reserve response capacity at bus acceptance, including commands still buffered here. + private val readCount = RegInit(0.U(log2Ceil(BigInt(config.outstanding.read) + 1).W)) + private val writeCount = RegInit(0.U(log2Ceil(BigInt(config.outstanding.write) + 1).W)) + private val readSpace = readCount < config.outstanding.read.U + private val writeSpace = writeCount < config.outstanding.write.U + + private val readCommands = Module( + new Queue(new AcornRdCmdChannel(params.addrWidth), entries = 2, pipe = false, flow = false) + ) + private val writeCommands = Module( + new Queue(new AcornWrCmdChannel(params.dataWidth, params.addrWidth), entries = 2, pipe = false, flow = false) + ) + readCommands.io.enq.valid := io.access.rd.cmd.valid && readSpace + readCommands.io.enq.bits := io.access.rd.cmd.bits + io.access.rd.cmd.ready := readCommands.io.enq.ready && readSpace + writeCommands.io.enq.valid := io.access.wr.cmd.valid && writeSpace + writeCommands.io.enq.bits := io.access.wr.cmd.bits + io.access.wr.cmd.ready := writeCommands.io.enq.ready && writeSpace + + private val readCommand = readCommands.io.deq + private val writeCommand = writeCommands.io.deq + private val readResponses = Module( new Queue(new AcornRdRspChannel(params.dataWidth), entries = config.outstanding.read, pipe = false, flow = false) ) @@ -65,40 +89,35 @@ final class AcornSramAdapter(val config: AcornSramConfig) extends Module { io.access.wr.rsp.bits := writeResponses.io.deq.bits writeResponses.io.deq.ready := io.access.wr.rsp.ready - // Count both pending SRAM results and buffered responses before issuing another read. - private val readCount = RegInit(0.U(log2Ceil(BigInt(config.outstanding.read) + 1).W)) - private val readSpace = readCount < config.outstanding.read.U - private val writeSpace = writeResponses.io.enq.ready - private def addressError(address: UInt): Bool = { val aligned = (address & (params.bytesPerWord - 1).U) === 0.U !aligned || address >= config.sizeBytes.U } - private val readError = addressError(io.access.rd.cmd.bits.addr) - private val writeError = addressError(io.access.wr.cmd.bits.addr) - private val readWord = io.access.rd.cmd.bits.addr >> log2Ceil(params.bytesPerWord) - private val writeWord = io.access.wr.cmd.bits.addr >> log2Ceil(params.bytesPerWord) + private val readError = addressError(readCommand.bits.addr) + private val writeError = addressError(writeCommand.bits.addr) + private val readWord = readCommand.bits.addr >> log2Ceil(params.bytesPerWord) + private val writeWord = writeCommand.bits.addr >> log2Ceil(params.bytesPerWord) private val readNeedsSram = !readError - private val writeNeedsSram = !writeError && io.access.wr.cmd.bits.strobe.orR + private val writeNeedsSram = !writeError && writeCommand.bits.strobe.orR - private val readCandidate = readSpace && io.access.rd.cmd.valid - private val writeCandidate = writeSpace && io.access.wr.cmd.valid + private val readCandidate = readCommand.valid + private val writeCandidate = writeResponses.io.enq.ready && writeCommand.valid private val samePortOrWord = if (singlePort) true.B else readWord === writeWord private val conflict = readCandidate && writeCandidate && readNeedsSram && writeNeedsSram && samePortOrWord private val preferWrite = RegInit(false.B) - io.access.rd.cmd.ready := readSpace && (!conflict || !preferWrite) - io.access.wr.cmd.ready := writeSpace && (!conflict || preferWrite) + readCommand.ready := !conflict || !preferWrite + writeCommand.ready := writeResponses.io.enq.ready && (!conflict || preferWrite) - when(conflict && (io.access.rd.cmd.fire || io.access.wr.cmd.fire)) { + when(conflict && (readCommand.fire || writeCommand.fire)) { preferWrite := !preferWrite } - private val doRead = io.access.rd.cmd.fire && readNeedsSram - private val doWrite = io.access.wr.cmd.fire && writeNeedsSram - private val writeData = io.access.wr.cmd.bits.data.asTypeOf(Vec(params.bytesPerWord, UInt(8.W))) - private val writeMask = VecInit(io.access.wr.cmd.bits.strobe.asBools) + private val doRead = readCommand.fire && readNeedsSram + private val doWrite = writeCommand.fire && writeNeedsSram + private val writeData = writeCommand.bits.data.asTypeOf(Vec(params.bytesPerWord, UInt(8.W))) + private val writeMask = VecInit(writeCommand.bits.strobe.asBools) private val readData = Wire(UInt(params.dataWidth.W)) if (singlePort) { @@ -122,14 +141,14 @@ final class AcornSramAdapter(val config: AcornSramConfig) extends Module { } // Error reads use the same latency pipeline so they cannot overtake valid reads. - private val readReturned = ShiftRegister(io.access.rd.cmd.fire, config.readLatency, false.B, true.B) + private val readReturned = ShiftRegister(readCommand.fire, config.readLatency, false.B, true.B) private val returnedError = ShiftRegister(readError, config.readLatency, false.B, true.B) readResponses.io.enq.valid := readReturned readResponses.io.enq.bits.data := Mux(returnedError, 0.U, readData) readResponses.io.enq.bits.error := returnedError - writeResponses.io.enq.valid := io.access.wr.cmd.fire + writeResponses.io.enq.valid := writeCommand.fire writeResponses.io.enq.bits.error := writeError when(io.access.rd.cmd.fire =/= io.access.rd.rsp.fire) { @@ -140,5 +159,13 @@ final class AcornSramAdapter(val config: AcornSramConfig) extends Module { } } + when(io.access.wr.cmd.fire =/= io.access.wr.rsp.fire) { + when(io.access.wr.cmd.fire) { + writeCount := writeCount + 1.U + }.otherwise { + writeCount := writeCount - 1.U + } + } + assert(!readResponses.io.enq.valid || readResponses.io.enq.ready, "SRAM read response has no reserved space.") } diff --git a/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala b/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala index 314a975..bd5c3af 100644 --- a/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala +++ b/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala @@ -18,6 +18,8 @@ private object AcornSramAdapterSpecDut { val writeEnable = Output(Bool()) val readWord = Output(UInt(config.params.addrWidth.W)) val writeWord = Output(UInt(config.params.addrWidth.W)) + val writeData = Output(UInt(config.params.dataWidth.W)) + val writeMask = Output(UInt(config.params.bytesPerWord.W)) }) val adapter = Module(new AcornSramAdapter(config)) @@ -51,6 +53,8 @@ private object AcornSramAdapterSpecDut { io.writeEnable := source.enable && source.isWrite io.readWord := source.address io.writeWord := source.address + io.writeData := source.writeData.asUInt + io.writeMask := source.mask.get.asUInt } else { val readSource = adapter.io.sram.readPorts(0) val writeSource = adapter.io.sram.writePorts(0) @@ -69,6 +73,8 @@ private object AcornSramAdapterSpecDut { io.writeEnable := writeSource.enable io.readWord := readSource.address io.writeWord := writeSource.address + io.writeData := writeSource.data.asUInt + io.writeMask := writeSource.mask.get.asUInt } } } @@ -95,6 +101,8 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { port.wr.cmd.ready expect true.B dut.clock.step() port.wr.cmd.valid #= false.B + port.wr.rsp.valid expect false.B + dut.clock.step() port.wr.rsp.valid expect true.B port.wr.rsp.bits.error expect false.B port.rd.cmd.valid #= true.B @@ -102,7 +110,7 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { port.rd.cmd.ready expect true.B dut.clock.step() port.rd.cmd.valid #= false.B - dut.clock.step() + dut.clock.step(2) port.rd.rsp.valid expect true.B port.rd.rsp.bits.data expect data.U port.rd.rsp.bits.error expect false.B @@ -135,6 +143,8 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { port.cmd.ready expect true.B dut.clock.step() port.cmd.valid #= false.B + port.rsp.valid expect false.B + dut.clock.step() port.rsp.valid expect true.B port.rsp.bits.error expect false.B dut.clock.step() @@ -169,7 +179,10 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { } Write(address, BigInt(32, random), index % 16) } - val expectedReads = mutable.Queue.empty[Result] + val expectedReads = mutable.Queue.empty[Int] + val readResults = Array.fill(reads.size)(Option.empty[Result]) + val pendingReads = mutable.Queue.empty[Int] + val pendingWrites = mutable.Queue.empty[Write] val expectedWrites = mutable.Queue.empty[Boolean] var heldRead = Option.empty[Result] var heldWrite = Option.empty[Boolean] @@ -201,8 +214,24 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { val writeFire = writeSent < writes.size && port.wr.cmd.ready.peek().litToBoolean val readEnable = dut.io.readEnable.peek().litToBoolean val writeEnable = dut.io.writeEnable.peek().litToBoolean - readEnable shouldBe (readFire && valid(reads(readSent))) - writeEnable shouldBe (writeFire && valid(writes(writeSent).address) && writes(writeSent).strobe != 0) + if (readEnable) { + pendingReads.nonEmpty shouldBe true + val index = pendingReads.dequeue() + dut.io.readWord.peek().litValue shouldBe BigInt(reads(index) / 4) + readResults(index) = Some(Result(memory(reads(index) / 4), false)) + } + if (writeEnable) { + pendingWrites.nonEmpty shouldBe true + val command = pendingWrites.dequeue() + dut.io.writeWord.peek().litValue shouldBe BigInt(command.address / 4) + dut.io.writeData.peek().litValue shouldBe command.data + dut.io.writeMask.peek().litValue shouldBe BigInt(command.strobe) + val index = command.address / 4 + for (byte <- 0 until 4 if (command.strobe & (1 << byte)) != 0) { + val mask = BigInt(255) << (byte * 8) + memory(index) = (memory(index) & ~mask) | (command.data & mask) + } + } if (readEnable && writeEnable) { mode shouldBe AcornSramPortMode.OneReadOneWrite dut.io.readWord.peek().litValue should not be dut.io.writeWord.peek().litValue @@ -211,7 +240,7 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { if (port.rd.rsp.valid.peek().litToBoolean) { expectedReads.nonEmpty shouldBe true val current = Result(port.rd.rsp.bits.data.peek().litValue, port.rd.rsp.bits.error.peek().litToBoolean) - current shouldBe expectedReads.head + Some(current) shouldBe readResults(expectedReads.head) heldRead.foreach(_ shouldBe current) if (readReady) { expectedReads.dequeue() @@ -234,21 +263,19 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { if (readFire) { val address = reads(readSent) - expectedReads.enqueue(if (valid(address)) Result(memory(address / 4), false) else Result(0, true)) + expectedReads.enqueue(readSent) + if (valid(address)) pendingReads.enqueue(readSent) + else readResults(readSent) = Some(Result(0, true)) readSent += 1 } if (writeFire) { val command = writes(writeSent) expectedWrites.enqueue(!valid(command.address)) - if (valid(command.address)) { - val index = command.address / 4 - for (byte <- 0 until 4 if (command.strobe & (1 << byte)) != 0) { - val mask = BigInt(255) << (byte * 8) - memory(index) = (memory(index) & ~mask) | (command.data & mask) - } - } + if (valid(command.address) && command.strobe != 0) pendingWrites.enqueue(command) writeSent += 1 } + readSent - readReceived should be <= capacity + writeSent - writeReceived should be <= capacity dut.clock.step() cycle += 1 } @@ -256,6 +283,8 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { writeReceived shouldBe writes.size expectedReads shouldBe empty expectedWrites shouldBe empty + pendingReads shouldBe empty + pendingWrites shouldBe empty dut.io.access.rd.cmd.valid #= false.B dut.io.access.wr.cmd.valid #= false.B dut.clock.step(latency + 4) @@ -266,6 +295,117 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { } for (mode <- AcornSramPortMode.values.toSeq) { + "AcornSramAdapter" should s"isolate SRAM signals from live bus inputs with $mode" in { + val config = AcornSramConfig(AcornParams(32, 4), 4, mode) + simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => + init(dut) + val port = dut.io.access + + def snapshot(): Seq[BigInt] = Seq( + dut.io.readEnable, + dut.io.writeEnable, + dut.io.readWord, + dut.io.writeWord, + dut.io.writeData, + dut.io.writeMask, + port.rd.cmd.ready, + port.wr.cmd.ready, + ).map(_.peek().litValue) + + val idle = snapshot() + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 0.U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 4.U + port.wr.cmd.bits.data #= 0x12345678.U + port.wr.cmd.bits.strobe #= 5.U + snapshot() shouldBe idle + dut.clock.step() + // Present subsequent commands while the buffered pair drives SRAM. + val issued = snapshot() + dut.io.readEnable expect true.B + dut.io.writeEnable expect (mode == AcornSramPortMode.OneReadOneWrite).B + dut.io.readWord expect 0.U + dut.io.writeData expect 0x12345678.U + dut.io.writeMask expect 5.U + for (index <- 0 until 8) { + port.rd.cmd.valid #= (index % 2 == 0).B + port.rd.cmd.bits.addr #= index.U + port.wr.cmd.valid #= (index % 2 == 1).B + port.wr.cmd.bits.addr #= (15 - index).U + port.wr.cmd.bits.data #= (BigInt("fedcba98", 16) + index).U + port.wr.cmd.bits.strobe #= index.U + port.rd.rsp.ready #= (index % 2 == 0).B + port.wr.rsp.ready #= (index % 2 == 1).B + snapshot() shouldBe issued + } + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + port.rd.rsp.ready #= true.B + port.wr.rsp.ready #= true.B + dut.clock.step(4) + port.rd.rsp.valid expect false.B + port.wr.rsp.valid expect false.B + } + } + + "AcornSramAdapter" should s"reserve capacity at acceptance and isolate response backpressure with $mode" in { + val config = AcornSramConfig(AcornParams(32, 4), 4, mode, 1, AcornOutstanding(1, 1)) + simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => + init(dut) + write(dut, 0, 0x12345678) + val port = dut.io.access + port.rd.rsp.ready #= false.B + port.wr.rsp.ready #= false.B + var reads = 0 + var writes = 0 + for (_ <- 0 until 12) { + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 0.U + port.wr.cmd.valid #= true.B + port.wr.cmd.bits.addr #= 4.U + port.wr.cmd.bits.data #= 0x44332211.U + port.wr.cmd.bits.strobe #= 15.U + if (port.rd.cmd.ready.peek().litToBoolean) reads += 1 + if (port.wr.cmd.ready.peek().litToBoolean) writes += 1 + dut.clock.step() + } + reads shouldBe 1 + writes shouldBe 1 + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + port.rd.cmd.ready expect false.B + port.wr.cmd.ready expect false.B + dut.io.readEnable expect false.B + dut.io.writeEnable expect false.B + // Releasing backpressure must not propagate through the queues in this cycle. + port.rd.rsp.ready #= true.B + port.wr.rsp.ready #= true.B + port.rd.cmd.ready expect false.B + port.wr.cmd.ready expect false.B + dut.io.readEnable expect false.B + dut.io.writeEnable expect false.B + var readResponses = 0 + var writeResponses = 0 + for (_ <- 0 until 20) { + if (port.rd.rsp.valid.peek().litToBoolean) { + port.rd.rsp.bits.data expect 0x12345678.U + port.rd.rsp.bits.error expect false.B + readResponses += 1 + } + if (port.wr.rsp.valid.peek().litToBoolean) { + port.wr.rsp.bits.error expect false.B + writeResponses += 1 + } + dut.clock.step() + } + readResponses shouldBe reads + writeResponses shouldBe writes + port.rd.cmd.ready expect true.B + port.wr.cmd.ready expect true.B + } + } + "AcornSramAdapter" should s"sustain one read per cycle with $mode" in { val config = AcornSramConfig(AcornParams(32, 4), 4, mode) simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => @@ -277,15 +417,16 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { port.cmd.valid #= true.B port.cmd.bits.addr #= ((index % 4) * 4).U port.cmd.ready expect true.B - port.rsp.valid expect (index >= 2).B - if (index >= 2) { - port.rsp.bits.data expect data((index - 2) % 4).U + dut.io.readEnable expect (index >= 1).B + port.rsp.valid expect (index >= 3).B + if (index >= 3) { + port.rsp.bits.data expect data((index - 3) % 4).U port.rsp.bits.error expect false.B } dut.clock.step() } port.cmd.valid #= false.B - for (index <- 14 until 16) { + for (index <- 13 until 16) { port.rsp.valid expect true.B port.rsp.bits.data expect data(index % 4).U port.rsp.bits.error expect false.B @@ -309,9 +450,26 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { port.wr.cmd.bits.addr #= 0.U port.wr.cmd.bits.data #= BigInt("aabbccdd", 16).U port.wr.cmd.bits.strobe #= 15.U + port.rd.cmd.ready expect true.B + port.wr.cmd.ready expect true.B + dut.io.readEnable expect false.B + dut.io.writeEnable expect false.B + dut.clock.step() + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B for (cycle <- 0 until 4) { - port.rd.cmd.ready expect (cycle % 2 == 0).B - port.wr.cmd.ready expect (cycle % 2 == 1).B + if (cycle == 0) { + // Keep both queues nonempty during the first three arbitration decisions. + port.rd.cmd.valid #= true.B + port.wr.cmd.valid #= true.B + port.rd.cmd.ready expect true.B + port.wr.cmd.ready expect true.B + } else { + port.rd.cmd.valid #= false.B + port.wr.cmd.valid #= false.B + } + dut.io.readEnable expect (cycle % 2 == 0).B + dut.io.writeEnable expect (cycle % 2 == 1).B dut.clock.step() } port.rd.cmd.valid #= false.B @@ -336,6 +494,7 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => init(dut) write(dut, 0, 0x12345678) + write(dut, 8, 0x55667788) val port = dut.io.access port.rd.rsp.ready #= false.B port.wr.rsp.ready #= false.B @@ -348,6 +507,14 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { port.wr.cmd.bits.data #= 0x44332211.U port.wr.cmd.bits.strobe #= 15.U dut.clock.step() + port.rd.cmd.valid #= true.B + port.rd.cmd.bits.addr #= 8.U + port.wr.cmd.bits.addr #= 8.U + port.wr.cmd.bits.data #= BigInt("aabbccdd", 16).U + port.rd.cmd.ready expect true.B + port.wr.cmd.ready expect true.B + dut.clock.step() + port.rd.cmd.valid #= false.B port.wr.cmd.valid #= false.B dut.reset #= true.B dut.io.readEnable expect false.B @@ -361,13 +528,13 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { dut.clock.step(5) port.rd.rsp.valid expect false.B port.wr.rsp.valid expect false.B - for ((address, data) <- Seq(0 -> BigInt(0x12345678), 4 -> BigInt(0x44332211))) { + for ((address, data) <- Seq(0 -> BigInt(0x12345678), 4 -> BigInt(0x44332211), 8 -> BigInt(0x55667788))) { port.rd.cmd.valid #= true.B port.rd.cmd.bits.addr #= address.U port.rd.cmd.ready expect true.B dut.clock.step() port.rd.cmd.valid #= false.B - dut.clock.step(3) + dut.clock.step(4) port.rd.rsp.valid expect true.B port.rd.rsp.bits.data expect data.U port.rd.rsp.bits.error expect false.B @@ -394,11 +561,15 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { port.wr.cmd.bits.strobe #= 5.U port.rd.cmd.ready expect true.B port.wr.cmd.ready expect true.B - dut.io.readEnable expect true.B - dut.io.writeEnable expect true.B + dut.io.readEnable expect false.B + dut.io.writeEnable expect false.B dut.clock.step() port.rd.cmd.valid #= false.B port.wr.cmd.valid #= false.B + dut.io.readEnable expect true.B + dut.io.writeEnable expect true.B + port.wr.rsp.valid expect false.B + dut.clock.step() port.wr.rsp.valid expect true.B port.wr.rsp.bits.error expect false.B dut.clock.step() @@ -416,7 +587,7 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { port.rd.cmd.ready expect true.B dut.clock.step() port.rd.cmd.valid #= false.B - dut.clock.step() + dut.clock.step(2) port.rd.rsp.valid expect true.B port.rd.rsp.bits.data expect BigInt("55bb77dd", 16).U port.rd.rsp.bits.error expect false.B diff --git a/docs/acorn.md b/docs/acorn.md index 343074d..ca36cf4 100644 --- a/docs/acorn.md +++ b/docs/acorn.md @@ -175,7 +175,10 @@ adapter.io.sram <> memory SRAM 容量为 `numWords * bytesPerWord`,adapter 地址是从零开始的局部字节偏移。越界或未对齐的命令返回错误, 不访问 SRAM;合法的零 strobe 写不修改 SRAM,仍返回成功响应。 -读命令接受时为返回数据预留空间,直到 `rd.rsp.fire` 才释放;响应 FIFO 增加一拍读延迟。 +读写命令各经过一个两项队列,关闭 `flow` 和 `pipe`,隔断总线命令到 SRAM 控制以及仲裁到总线 `ready` 的组合路径。 +SRAM 地址、数据和控制由队列中的命令及本地状态产生。无阻塞时,命令缓冲增加一拍延迟,连续访问仍可每拍一次。 +`outstanding` 限制已接受、尚未完成响应传输的事务总数,包括命令队列中的请求、SRAM 在途读和已缓存的响应。 +命令在入口握手时预留响应容量,直到对应的 `rsp.fire` 才释放;响应 FIFO 再增加一拍读延迟。 读写各自按序返回。1RW 的读写请求,以及 1R1W 的同字地址读写请求,采用 round-robin 仲裁; 1R1W 的不同地址读写可同时执行。写响应表示 SRAM 写已完成,而不是仅接收了命令。 From 0ed7672b1c174357d128de94c6c5c4c87a497c7e Mon Sep 17 00:00:00 2001 From: zhutmost Date: Mon, 5 Oct 2026 18:56:53 +0800 Subject: [PATCH 18/26] feat: add AMBA4 AXI & AXI-Lite IO --- chipmunk/src/amba/ApbIO.scala | 48 +++++++++++++++++ chipmunk/src/amba/AxiEnum.scala | 28 ++++++++++ chipmunk/src/amba/AxiIO.scala | 89 +++++++++++++++++++++++++++++++ chipmunk/src/amba/AxiLiteIO.scala | 50 +++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 chipmunk/src/amba/ApbIO.scala create mode 100644 chipmunk/src/amba/AxiEnum.scala create mode 100644 chipmunk/src/amba/AxiIO.scala create mode 100644 chipmunk/src/amba/AxiLiteIO.scala diff --git a/chipmunk/src/amba/ApbIO.scala b/chipmunk/src/amba/ApbIO.scala new file mode 100644 index 0000000..0bcf2ac --- /dev/null +++ b/chipmunk/src/amba/ApbIO.scala @@ -0,0 +1,48 @@ +package chipmunk +package amba + +import chisel3._ + +private[amba] abstract class ApbIOBase( + val dataWidth: Int, + val addrWidth: Int, + val hasProt: Boolean = false, + val hasStrb: Boolean = false, +) extends Bundle + with IsMasterSlave { + override def isMaster = true + + val strobeWidth: Int = dataWidth / 8 + + def allowedDataWidth = List(8, 16, 32) + require(allowedDataWidth contains dataWidth, s"Data width can only be 8, 16, or 32, but got $dataWidth.") + + require(addrWidth > 0, s"Address width must be at least 1, but got $addrWidth.") + + val addr = Output(UInt(addrWidth.W)) + val selx = Output(UInt(4.W)) + val enable = Output(Bool()) + val write = Output(Bool()) + val wdata = Output(UInt(dataWidth.W)) + val ready = Input(Bool()) + val rdata = Input(UInt(dataWidth.W)) + val slverr = Input(Bool()) + + // The below signals are optional in APB4 but not in APB3 + val prot = if (hasProt) Some(Output(UInt(3.W))) else None + val strb = if (hasStrb) Some(Output(UInt(strobeWidth.W))) else None +} + +/** AMBA4 APB IO bundle. + * + * @param dataWidth + * The bit width of the bus data. It can only be 8, 16, or 32. + * @param addrWidth + * The bit width of the bus address. + * @param hasProt + * Whether the bus has prot signals. + * @param hasStrb + * Whether the bus has strobe signals. + */ +class Apb4IO(dataWidth: Int, addrWidth: Int, hasProt: Boolean = false, hasStrb: Boolean = false) + extends ApbIOBase(dataWidth, addrWidth, hasProt, hasStrb) {} diff --git a/chipmunk/src/amba/AxiEnum.scala b/chipmunk/src/amba/AxiEnum.scala new file mode 100644 index 0000000..12c8675 --- /dev/null +++ b/chipmunk/src/amba/AxiEnum.scala @@ -0,0 +1,28 @@ +package chipmunk +package amba + +import chisel3._ + +object AxiBurstType extends ChiselEnum { + val BURST_FIXED = Value(0.U) + val BURST_INCR = Value(1.U) + val BURST_WRAP = Value(2.U) +} + +object AxiBurstSize extends ChiselEnum { + val SIZE1 = Value(0.U) + val SIZE2 = Value(1.U) + val SIZE4 = Value(2.U) + val SIZE8 = Value(3.U) + val SIZE16 = Value(4.U) + val SIZE32 = Value(5.U) + val SIZE64 = Value(6.U) + val SIZE128 = Value(7.U) +} + +object AxiResp extends ChiselEnum { + val RESP_OKAY = Value(0.U) + val RESP_EXOKAY = Value(1.U) + val RESP_SLVERR = Value(2.U) + val RESP_DECERR = Value(3.U) +} diff --git a/chipmunk/src/amba/AxiIO.scala b/chipmunk/src/amba/AxiIO.scala new file mode 100644 index 0000000..70cb823 --- /dev/null +++ b/chipmunk/src/amba/AxiIO.scala @@ -0,0 +1,89 @@ +package chipmunk +package amba + +import chisel3._ + +import stream.{Stream, StreamIO} + +final case class AxiParams( + dataWidth: Int, + addrWidth: Int, + idWidth: Int = 0, + hasQos: Boolean = false, + hasRegion: Boolean = false, +) { + require( + dataWidth >= 8 && dataWidth <= 1024 && (dataWidth & (dataWidth - 1)) == 0, + "AXI4 data width must be a power of two from 8 to 1024.", + ) + require(addrWidth >= 1, "AXI4 address width must be positive.") + require(idWidth >= 0, "AXI4 ID width must be nonnegative.") + + val strobeWidth: Int = dataWidth / 8 + val hasId: Boolean = idWidth > 0 + + /** AxSIZE encoding for a transfer using the full data width. */ + val fullSize: Int = Integer.numberOfTrailingZeros(strobeWidth) +} + +class AxiWriteAddrChannel(val params: AxiParams) extends Bundle { + val addr = UInt(params.addrWidth.W) + val size = AxiBurstSize() + val len = UInt(8.W) + val burst = AxiBurstType() + val id = if (params.idWidth > 0) Some(UInt(params.idWidth.W)) else None + val prot = UInt(3.W) + val cache = UInt(4.W) + val lock = UInt(1.W) + val qos = if (params.hasQos) Some(UInt(4.W)) else None + val region = if (params.hasRegion) Some(UInt(4.W)) else None + + def protPrivileged: Bool = prot(0).asBool + def protNonsecure: Bool = prot(1).asBool + def protInstruction: Bool = prot(2).asBool + + def cacheBufferable: Bool = cache(0).asBool + def cacheModifiable: Bool = cache(1).asBool + def cacheOtherAllocated: Bool = cache(2).asBool + def cacheAllocated: Bool = cache(3).asBool + + def lockNormal: Bool = lock === 0.U + def lockExclusive: Bool = lock === 1.U +} + +final class AxiReadAddrChannel(params: AxiParams) extends AxiWriteAddrChannel(params) { + override def cacheAllocated: Bool = cache(2).asBool + override def cacheOtherAllocated: Bool = cache(3).asBool +} + +final class AxiWriteDataChannel(val params: AxiParams) extends Bundle { + val data = UInt(params.dataWidth.W) + val strb = UInt(params.strobeWidth.W) + val last = Bool() +} + +final class AxiWriteRespChannel(val params: AxiParams) extends Bundle { + val resp = AxiResp() + val id = if (params.idWidth > 0) Some(UInt(params.idWidth.W)) else None +} + +final class AxiReadDataChannel(val params: AxiParams) extends Bundle { + val data = UInt(params.dataWidth.W) + val resp = AxiResp() + val last = Bool() + val id = if (params.idWidth > 0) Some(UInt(params.idWidth.W)) else None +} + +/** AMBA4 AXI IO bundle. */ +final class AxiIO(val params: AxiParams) extends Bundle with IsMasterSlave { + def this(dataWidth: Int, addrWidth: Int, idWidth: Int = 0, hasQos: Boolean = false, hasRegion: Boolean = false) = + this(AxiParams(dataWidth, addrWidth, idWidth, hasQos, hasRegion)) + + override def isMaster = true + + val aw = Master(Stream(new AxiWriteAddrChannel(params))) + val w = Master(Stream(new AxiWriteDataChannel(params))) + val b = Slave(Stream(new AxiWriteRespChannel(params))) + val ar = Master(Stream(new AxiReadAddrChannel(params))) + val r = Slave(Stream(new AxiReadDataChannel(params))) +} diff --git a/chipmunk/src/amba/AxiLiteIO.scala b/chipmunk/src/amba/AxiLiteIO.scala new file mode 100644 index 0000000..4b5fa99 --- /dev/null +++ b/chipmunk/src/amba/AxiLiteIO.scala @@ -0,0 +1,50 @@ +package chipmunk +package amba + +import chisel3._ + +import stream.{Stream, StreamIO} + +final case class AxiLiteParams(dataWidth: Int, addrWidth: Int) { + require(dataWidth == 32 || dataWidth == 64, "AXI4-Lite data width must be 32 or 64.") + require(addrWidth >= 1, "AXI4-Lite address width must be positive.") + + val strobeWidth: Int = dataWidth / 8 +} + +final class AxiLiteAddrChannel(val params: AxiLiteParams) extends Bundle { + val addr = UInt(params.addrWidth.W) + val prot = UInt(3.W) + + def protPrivileged: Bool = prot(0).asBool + def protNonsecure: Bool = prot(1).asBool + def protInstruction: Bool = prot(2).asBool +} + +final class AxiLiteWriteDataChannel(val params: AxiLiteParams) extends Bundle { + val data = UInt(params.dataWidth.W) + val strb = UInt(params.strobeWidth.W) +} + +final class AxiLiteWriteRespChannel(val params: AxiLiteParams) extends Bundle { + val resp = AxiResp() +} + +final class AxiLiteReadDataChannel(val params: AxiLiteParams) extends Bundle { + val data = UInt(params.dataWidth.W) + val resp = AxiResp() +} + +/** AMBA4 AXI-Lite IO bundle. */ +final class AxiLiteIO(val params: AxiLiteParams) extends Bundle with IsMasterSlave { + def this(dataWidth: Int, addrWidth: Int) = + this(AxiLiteParams(dataWidth, addrWidth)) + + override def isMaster = true + + val aw = Master(Stream(new AxiLiteAddrChannel(params))) + val w = Master(Stream(new AxiLiteWriteDataChannel(params))) + val b = Slave(Stream(new AxiLiteWriteRespChannel(params))) + val ar = Master(Stream(new AxiLiteAddrChannel(params))) + val r = Slave(Stream(new AxiLiteReadDataChannel(params))) +} From 612f92221d1d8018b95677ed09309cff978bbeba Mon Sep 17 00:00:00 2001 From: zhutmost Date: Mon, 5 Oct 2026 22:52:34 +0800 Subject: [PATCH 19/26] feat: name AXI/AXI-Lite-generated ports with common conventions --- chipmunk/src/VerilogIO.scala | 115 +++++++++++++++++++++++++++ chipmunk/src/VerilogIOFactory.scala | 50 ++++++++++++ chipmunk/src/amba/ApbIO.scala | 48 ----------- chipmunk/src/amba/AxiIO.scala | 2 +- chipmunk/src/amba/AxiLiteIO.scala | 2 +- chipmunk/src/amba/AxiVerilogIO.scala | 23 ++++++ 6 files changed, 190 insertions(+), 50 deletions(-) create mode 100644 chipmunk/src/VerilogIO.scala create mode 100644 chipmunk/src/VerilogIOFactory.scala delete mode 100644 chipmunk/src/amba/ApbIO.scala create mode 100644 chipmunk/src/amba/AxiVerilogIO.scala diff --git a/chipmunk/src/VerilogIO.scala b/chipmunk/src/VerilogIO.scala new file mode 100644 index 0000000..f75b003 --- /dev/null +++ b/chipmunk/src/VerilogIO.scala @@ -0,0 +1,115 @@ +package chipmunk + +import scala.collection.immutable.SeqMap + +import chisel3.* +import chisel3.experimental.{requireIsChiselType, requireIsHardware, SourceInfo} +import chisel3.experimental.dataview.* +import chisel3.reflect.DataMirror + +/** Supplies RTL names for leaf paths relative to this interface. */ +trait HasVerilogIO { + this: Record => + + def generatePortName(path: Seq[String]): String +} + +extension [T <: Record & HasVerilogIO](gen: T) { + + /** Converts an unbound interface type to a flat RTL type. */ + def createVerilogIO(f: String => String = identity)(using factory: VerilogIOFactory[T]): factory.Out = + factory.create(gen, f) +} + +/** A flat interface type whose fields and view are derived from one original interface. + * + * Bind with IO or FlatIO before calling viewAsChiselIO. Supports digital leaves in Records and Vecs. + */ +class VerilogIO[T <: Record & HasVerilogIO] private[chipmunk] (private val gen: T, private val rename: String => String) + extends Record { + requireIsChiselType(gen, "VerilogIO interface") + + private val template = DataMirror.internal.chiselTypeClone(gen) + private val fields = VerilogIO.leaves(template) + private val names = fields.map(field => rename(template.generatePortName(field.path))) + + require(fields.map(_.gen).distinct.size == fields.size, "VerilogIO does not support aliased leaf fields.") + names.foreach { name => + require(name != null && name.matches("[A-Za-z_][A-Za-z0-9_]*"), s"Invalid RTL field name: $name") + } + require(names.distinct.size == names.size, "RTL field names must be unique after renaming.") + + override val elements: SeqMap[String, Data] = SeqMap.from(fields.zip(names).map { case (field, name) => + name -> (if (field.isInput) Input(field.gen) else Output(field.gen)) + }) + + /** Returns the original interface API over bound RTL fields, without adding hardware. */ + def viewAsChiselIO(using SourceInfo): T = { + requireIsHardware(this, "VerilogIO view target") + + given DataView[VerilogIO[T], T] = DataView.mapping[VerilogIO[T], T]( + rtl => { + val view = DataMirror.internal.chiselTypeClone(template) + val actualInputs = names.map { name => + DataMirror.directionOf(rtl.elements(name)) match { + case ActualDirection.Input => true + case ActualDirection.Output => false + case other => throw new IllegalArgumentException(s"Unsupported RTL field direction: $other") + } + } + val originalInputs = fields.map(_.isInput) + // Follow flips and coercion applied after flattening, including enclosing Bundles. + DataMirror.specifiedDirectionOf(rtl) match { + case SpecifiedDirection.Input => Input(view) + case SpecifiedDirection.Output => Output(view) + case _ if actualInputs == originalInputs => view + case _ if actualInputs == originalInputs.map(!_) => Flipped(view) + case _ if actualInputs.forall(identity) => Input(view) + case _ if actualInputs.forall(!_) => Output(view) + case _ => throw new IllegalArgumentException("RTL field directions no longer match the original interface.") + } + }, + (rtl, view) => { + val viewFields = VerilogIO.leaves(view).map(field => field.path -> field.gen).toMap + require(viewFields.keySet == fields.map(_.path).toSet, "Interface changed during cloning.") + fields.zip(names).map { case (field, name) => rtl.elements(name) -> viewFields(field.path) } + }, + ) + + this.viewAs[T] + } +} + +private[chipmunk] object VerilogIO { + private[chipmunk] final case class Leaf(path: Vector[String], gen: Element, isInput: Boolean) + + private[chipmunk] def leaves( + data: Data, + path: Vector[String] = Vector.empty, + parentDirection: SpecifiedDirection = SpecifiedDirection.Unspecified, + ): Vector[Leaf] = { + val location = if (path.isEmpty) "" else path.mkString(".") + require(!DataMirror.hasProbeTypeModifier(data), s"VerilogIO does not support Probe at $location.") + val direction = SpecifiedDirection.fromParent(parentDirection, DataMirror.specifiedDirectionOf(data)) + + data match { + case record: Record => + record.elements.toVector.reverse.flatMap { case (name, field) => + require(name.nonEmpty, s"VerilogIO does not support opaque records at $location.") + leaves(field, path :+ name, direction) + } + + case vector: Vec[?] => + vector.zipWithIndex.toVector.flatMap { case (field, index) => + leaves(field, path :+ index.toString, direction) + } + + case leaf: (Bits | EnumType | Clock | Reset) => + val isInput = direction == SpecifiedDirection.Input || direction == SpecifiedDirection.Flip + Vector(Leaf(path, leaf, isInput)) + + case other => + throw new IllegalArgumentException(s"VerilogIO does not support ${other.getClass.getSimpleName} at $location.") + } + } +} diff --git a/chipmunk/src/VerilogIOFactory.scala b/chipmunk/src/VerilogIOFactory.scala new file mode 100644 index 0000000..f210b04 --- /dev/null +++ b/chipmunk/src/VerilogIOFactory.scala @@ -0,0 +1,50 @@ +package chipmunk + +import scala.util.NotGiven + +import chisel3.* +import chisel3.reflect.DataMirror + +/** Selects a flat record while preserving optional interface capabilities. */ +trait VerilogIOFactory[T <: Record & HasVerilogIO] { + type Out <: VerilogIO[T] + + def create(gen: T, rename: String => String): Out +} + +object VerilogIOFactory { + type Aux[T <: Record & HasVerilogIO, R <: VerilogIO[T]] = VerilogIOFactory[T] { type Out = R } + + given plain[T <: Record & HasVerilogIO](using NotGiven[T <:< IsMasterSlave]): Aux[T, VerilogIO[T]] = + new VerilogIOFactory[T] { + type Out = VerilogIO[T] + + override def create(gen: T, rename: String => String): Out = new VerilogIO(gen, rename) + } + + given masterSlave[T <: Record & IsMasterSlave & HasVerilogIO]: Aux[T, MasterSlaveVerilogIO[T]] = + new VerilogIOFactory[T] { + type Out = MasterSlaveVerilogIO[T] + + override def create(gen: T, rename: String => String): Out = { + val result = new MasterSlaveVerilogIO(gen, rename) + result._roleWrapped = gen._roleWrapped + result + } + } +} + +/** Adds Master/Slave support only when the original interface provides it. */ +final class MasterSlaveVerilogIO[T <: Record & IsMasterSlave & HasVerilogIO] private[chipmunk] ( + private val gen: T, + private val rename: String => String, +) extends VerilogIO[T](gen, rename) + with IsMasterSlave { + // The root direction of gen has already been absorbed into the flat fields. + override val isMaster: Boolean = DataMirror.specifiedDirectionOf(gen) match { + case SpecifiedDirection.Unspecified => gen.isMaster + case SpecifiedDirection.Flip => !gen.isMaster + case other => + throw new IllegalArgumentException(s"MasterSlaveVerilogIO requires an uncoerced interface, found $other.") + } +} diff --git a/chipmunk/src/amba/ApbIO.scala b/chipmunk/src/amba/ApbIO.scala deleted file mode 100644 index 0bcf2ac..0000000 --- a/chipmunk/src/amba/ApbIO.scala +++ /dev/null @@ -1,48 +0,0 @@ -package chipmunk -package amba - -import chisel3._ - -private[amba] abstract class ApbIOBase( - val dataWidth: Int, - val addrWidth: Int, - val hasProt: Boolean = false, - val hasStrb: Boolean = false, -) extends Bundle - with IsMasterSlave { - override def isMaster = true - - val strobeWidth: Int = dataWidth / 8 - - def allowedDataWidth = List(8, 16, 32) - require(allowedDataWidth contains dataWidth, s"Data width can only be 8, 16, or 32, but got $dataWidth.") - - require(addrWidth > 0, s"Address width must be at least 1, but got $addrWidth.") - - val addr = Output(UInt(addrWidth.W)) - val selx = Output(UInt(4.W)) - val enable = Output(Bool()) - val write = Output(Bool()) - val wdata = Output(UInt(dataWidth.W)) - val ready = Input(Bool()) - val rdata = Input(UInt(dataWidth.W)) - val slverr = Input(Bool()) - - // The below signals are optional in APB4 but not in APB3 - val prot = if (hasProt) Some(Output(UInt(3.W))) else None - val strb = if (hasStrb) Some(Output(UInt(strobeWidth.W))) else None -} - -/** AMBA4 APB IO bundle. - * - * @param dataWidth - * The bit width of the bus data. It can only be 8, 16, or 32. - * @param addrWidth - * The bit width of the bus address. - * @param hasProt - * Whether the bus has prot signals. - * @param hasStrb - * Whether the bus has strobe signals. - */ -class Apb4IO(dataWidth: Int, addrWidth: Int, hasProt: Boolean = false, hasStrb: Boolean = false) - extends ApbIOBase(dataWidth, addrWidth, hasProt, hasStrb) {} diff --git a/chipmunk/src/amba/AxiIO.scala b/chipmunk/src/amba/AxiIO.scala index 70cb823..aef4521 100644 --- a/chipmunk/src/amba/AxiIO.scala +++ b/chipmunk/src/amba/AxiIO.scala @@ -75,7 +75,7 @@ final class AxiReadDataChannel(val params: AxiParams) extends Bundle { } /** AMBA4 AXI IO bundle. */ -final class AxiIO(val params: AxiParams) extends Bundle with IsMasterSlave { +final class AxiIO(val params: AxiParams) extends Bundle with IsMasterSlave with HasAxiVerilogIO { def this(dataWidth: Int, addrWidth: Int, idWidth: Int = 0, hasQos: Boolean = false, hasRegion: Boolean = false) = this(AxiParams(dataWidth, addrWidth, idWidth, hasQos, hasRegion)) diff --git a/chipmunk/src/amba/AxiLiteIO.scala b/chipmunk/src/amba/AxiLiteIO.scala index 4b5fa99..6b703c7 100644 --- a/chipmunk/src/amba/AxiLiteIO.scala +++ b/chipmunk/src/amba/AxiLiteIO.scala @@ -36,7 +36,7 @@ final class AxiLiteReadDataChannel(val params: AxiLiteParams) extends Bundle { } /** AMBA4 AXI-Lite IO bundle. */ -final class AxiLiteIO(val params: AxiLiteParams) extends Bundle with IsMasterSlave { +final class AxiLiteIO(val params: AxiLiteParams) extends Bundle with IsMasterSlave with HasAxiVerilogIO { def this(dataWidth: Int, addrWidth: Int) = this(AxiLiteParams(dataWidth, addrWidth)) diff --git a/chipmunk/src/amba/AxiVerilogIO.scala b/chipmunk/src/amba/AxiVerilogIO.scala new file mode 100644 index 0000000..f90bd64 --- /dev/null +++ b/chipmunk/src/amba/AxiVerilogIO.scala @@ -0,0 +1,23 @@ +package chipmunk +package amba + +import java.util.Locale + +import chisel3.Record +import chisel3.experimental.SourceInfo + +/** Shared leaf naming for AXI and AXI-Lite. */ +private[amba] trait HasAxiVerilogIO extends HasVerilogIO { + this: Record => + + override final def generatePortName(path: Seq[String]): String = { + val channels = Set("aw", "w", "b", "ar", "r") + val name = path match { + case Seq(channel, "bits", field) if channels.contains(channel) => channel + field + case Seq(channel, signal) if channels.contains(channel) && (signal == "valid" || signal == "ready") => + channel + signal + case _ => throw new IllegalArgumentException(s"Unsupported AXI RTL path: ${path.mkString(".")}") + } + name.toUpperCase(Locale.ROOT) + } +} From a17d9460e6576ed028200ceeb40367f2abae1c6c Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Tue, 6 Oct 2026 12:07:35 +0800 Subject: [PATCH 20/26] test: cover VerilogIO port generation and interface views --- chipmunk/src/amba/AxiVerilogIO.scala | 1 - chipmunk/test/src/VerilogIOSpec.scala | 543 ++++++++++++++++++++++++++ 2 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 chipmunk/test/src/VerilogIOSpec.scala diff --git a/chipmunk/src/amba/AxiVerilogIO.scala b/chipmunk/src/amba/AxiVerilogIO.scala index f90bd64..1e61bd4 100644 --- a/chipmunk/src/amba/AxiVerilogIO.scala +++ b/chipmunk/src/amba/AxiVerilogIO.scala @@ -4,7 +4,6 @@ package amba import java.util.Locale import chisel3.Record -import chisel3.experimental.SourceInfo /** Shared leaf naming for AXI and AXI-Lite. */ private[amba] trait HasAxiVerilogIO extends HasVerilogIO { diff --git a/chipmunk/test/src/VerilogIOSpec.scala b/chipmunk/test/src/VerilogIOSpec.scala new file mode 100644 index 0000000..1769f89 --- /dev/null +++ b/chipmunk/test/src/VerilogIOSpec.scala @@ -0,0 +1,543 @@ +package chipmunk.test + +import java.util.Locale + +import scala.util.Random + +import chisel3.* +import chisel3.experimental.Analog +import chisel3.probe.Probe +import chisel3.reflect.DataMirror +import circt.stage.ChiselStage + +import chipmunk.* +import chipmunk.amba.* + +private object VerilogIOSpecDut { + def sourceName(name: String): String = "s_axi_" + name.toLowerCase(Locale.ROOT) + def sinkName(name: String): String = name + "_out" + + final class AxiHarness(p: AxiParams, flat: Boolean = false, outerFlipped: Boolean = false) extends Module { + private def ports = new Bundle { + val sourceBefore = Slave(new AxiIO(p)).createVerilogIO(f = sourceName) + val sourceAfter = Slave(new AxiIO(p).createVerilogIO(f = sourceName)) + val sinkBefore = Master(new AxiIO(p)).createVerilogIO(f = sinkName) + val sinkAfter = Master(new AxiIO(p).createVerilogIO(f = sinkName)) + } + private def gen = if (outerFlipped) Flipped(ports) else ports + val io = if (flat) FlatIO(gen) else IO(gen) + + val sourceBefore: AxiIO = io.sourceBefore.viewAsChiselIO + val sourceAfter: AxiIO = io.sourceAfter.viewAsChiselIO + val sinkBefore: AxiIO = io.sinkBefore.viewAsChiselIO + val sinkAfter: AxiIO = io.sinkAfter.viewAsChiselIO + if (outerFlipped) { + sourceBefore :<>= sinkAfter + sourceAfter :<>= sinkBefore + } else { + sinkAfter :<>= sourceBefore + sinkBefore :<>= sourceAfter + } + + // Exercise original interface methods as well as its preserved EnumType fields. + require(sourceBefore.aw.bits.protPrivileged.getWidth == 1) + val burst: AxiBurstType.Type = sinkAfter.aw.bits.burst + val response: AxiResp.Type = sourceAfter.b.bits.resp + + val pairs: Seq[(VerilogIO[AxiIO], VerilogIO[AxiIO])] = + Seq(io.sourceBefore -> io.sinkAfter, io.sourceAfter -> io.sinkBefore) + } + + final class LiteHarness(p: AxiLiteParams, flat: Boolean = false) extends Module { + private def ports = new Bundle { + val sourceBefore = Slave(new AxiLiteIO(p)).createVerilogIO(f = sourceName) + val sourceAfter = Slave(new AxiLiteIO(p).createVerilogIO(f = sourceName)) + val sinkBefore = Master(new AxiLiteIO(p)).createVerilogIO(f = sinkName) + val sinkAfter = Master(new AxiLiteIO(p).createVerilogIO(f = sinkName)) + } + val io = if (flat) FlatIO(ports) else IO(ports) + val sourceBefore: AxiLiteIO = io.sourceBefore.viewAsChiselIO + val sourceAfter: AxiLiteIO = io.sourceAfter.viewAsChiselIO + val sinkBefore: AxiLiteIO = io.sinkBefore.viewAsChiselIO + val sinkAfter: AxiLiteIO = io.sinkAfter.viewAsChiselIO + sinkAfter :<>= sourceBefore + sinkBefore :<>= sourceAfter + + require(sinkBefore.ar.bits.protNonsecure.getWidth == 1) + val response: AxiResp.Type = sinkAfter.r.bits.resp + val pairs: Seq[(VerilogIO[AxiLiteIO], VerilogIO[AxiLiteIO])] = + Seq(io.sourceBefore -> io.sinkAfter, io.sourceAfter -> io.sinkBefore) + } + + final class ExternalLite(p: AxiLiteParams) extends ExtModule { + val io = FlatIO(Slave(new AxiLiteIO(p).createVerilogIO(f = _ + "_ip"))) + } + + final class ExternalHarness(p: AxiLiteParams) extends Module { + val io = FlatIO(Slave(new AxiLiteIO(p).createVerilogIO(f = sourceName))) + val external = Module(new ExternalLite(p)) + external.io.viewAsChiselIO :<>= io.viewAsChiselIO + } + + final class ReusedTypeHarness(template: MasterSlaveVerilogIO[AxiLiteIO]) extends Module { + val source = IO(Slave(template)) + val sink = IO(Master(new AxiLiteIO(32, 24).createVerilogIO())) + sink.viewAsChiselIO :<>= source.viewAsChiselIO + } + + final class IndependentAxiHarness(p: AxiParams) extends Module { + val source = FlatIO(Slave(new AxiIO(p).createVerilogIO())) + val sink = IO(Master(new AxiIO(p))) + sink :<>= source.viewAsChiselIO + } + + final class DeclaredSlaveIO extends Bundle with IsMasterSlave with HasVerilogIO { + override def isMaster: Boolean = false + val request = Input(UInt(8.W)) + val response = Output(UInt(8.W)) + override def generatePortName(path: Seq[String]): String = path.mkString("_").toUpperCase(Locale.ROOT) + } + + final class DeclaredSlaveHarness(roleBefore: Boolean) extends Module { + val source = IO( + if (roleBefore) Slave(new DeclaredSlaveIO).createVerilogIO() + else Slave(new DeclaredSlaveIO().createVerilogIO()) + ) + val sink = IO( + if (roleBefore) Master(new DeclaredSlaveIO).createVerilogIO() + else Master(new DeclaredSlaveIO().createVerilogIO()) + ) + val sourceView = source.viewAsChiselIO + val sinkView = sink.viewAsChiselIO + sinkView.request := sourceView.request + sourceView.response := sinkView.response + } + + final class CoercedAxiHarness(inputs: Boolean, outer: Boolean) extends Module { + private def ports = new Bundle { + val axi = new AxiIO(32, 24).createVerilogIO() + } + val io = + if (outer) IO(if (inputs) Input(ports) else Output(ports)) + else + IO(new Bundle { + val axi = + if (inputs) Input(new AxiIO(32, 24).createVerilogIO()) + else Output(new AxiIO(32, 24).createVerilogIO()) + }) + val view: AxiIO = io.axi.viewAsChiselIO + if (!inputs) view := DontCare + } + + final class DigitalIO extends Bundle with HasVerilogIO { + val domainClock = Input(Clock()) + val syncReset = Input(Reset()) + val asyncReset = Input(AsyncReset()) + val signedIn = Input(SInt(9.W)) + val signedOut = Output(SInt(9.W)) + override def generatePortName(path: Seq[String]): String = path.mkString("_") + } + + final class DigitalHarness extends Module { + val rtl = IO(new DigitalIO().createVerilogIO()) + val view: DigitalIO = rtl.viewAsChiselIO + view.signedOut := view.signedIn + } + + final class UnsupportedIO(kind: String) extends Bundle with HasVerilogIO { + val value: Data = kind match { + case "Analog" => Analog(1.W) + case "Probe" => Output(Probe(UInt(8.W))) + case _ => throw new IllegalArgumentException(s"Unknown field kind: $kind") + } + override def generatePortName(path: Seq[String]): String = path.mkString("_") + } + + final class PlainIO extends Bundle with HasVerilogIO { + val request = Input(Vec(2, Bool())) + val result = Output(Vec(2, UInt(8.W))) + val status = Output(AxiResp()) + + override def generatePortName(path: Seq[String]): String = path.mkString("_").toUpperCase(Locale.ROOT) + } + + final class PlainHarness(mode: String) extends Module { + val rtl: VerilogIO[PlainIO] = mode match { + case "normal" => IO(new PlainIO().createVerilogIO()) + case "flipped before" => IO(Flipped(new PlainIO).createVerilogIO()) + case "flipped after" => IO(Flipped(new PlainIO().createVerilogIO())) + case "input before" => IO(Input(new PlainIO).createVerilogIO()) + case "input after" => IO(Input(new PlainIO().createVerilogIO())) + case "output before" => IO(Output(new PlainIO).createVerilogIO()) + case "output after" => IO(Output(new PlainIO().createVerilogIO())) + case _ => throw new IllegalArgumentException(s"Unknown plain interface mode: $mode") + } + val view: PlainIO = rtl.viewAsChiselIO + for (field <- view.request if DataMirror.directionOf(field) == ActualDirection.Output) field := false.B + for (field <- view.result if DataMirror.directionOf(field) == ActualDirection.Output) field := 0.U + if (DataMirror.directionOf(view.status) == ActualDirection.Output) view.status := AxiResp.RESP_OKAY + } + + final class InvalidHarness(mode: String) extends Module { + mode match { + case "bound conversion" => IO(new AxiLiteIO(32, 32)).createVerilogIO() + case "unbound view" => new AxiLiteIO(32, 32).createVerilogIO().viewAsChiselIO + case "repeated role" => Slave(Slave(new AxiLiteIO(32, 32)).createVerilogIO()) + case _ => throw new IllegalArgumentException(s"Unknown invalid mode: $mode") + } + } +} + +class VerilogIOSpec extends ChipmunkFlatSpec { + import VerilogIOSpecDut.* + + private val masterInputs = + Set("AWREADY", "WREADY", "BID", "BRESP", "BVALID", "ARREADY", "RID", "RDATA", "RRESP", "RLAST", "RVALID") + + // These are independent expectations for the protocol, not generated from VerilogIO's traversal. + private def liteWidths(p: AxiLiteParams): Map[String, Int] = Map( + "AWADDR" -> p.addrWidth, + "AWPROT" -> 3, + "AWVALID" -> 1, + "AWREADY" -> 1, + "WDATA" -> p.dataWidth, + "WSTRB" -> p.strobeWidth, + "WVALID" -> 1, + "WREADY" -> 1, + "BRESP" -> 2, + "BVALID" -> 1, + "BREADY" -> 1, + "ARADDR" -> p.addrWidth, + "ARPROT" -> 3, + "ARVALID" -> 1, + "ARREADY" -> 1, + "RDATA" -> p.dataWidth, + "RRESP" -> 2, + "RVALID" -> 1, + "RREADY" -> 1, + ) + + private def axiWidths(p: AxiParams): Map[String, Int] = { + val address = Map( + "ADDR" -> p.addrWidth, + "SIZE" -> 3, + "LEN" -> 8, + "BURST" -> 2, + "PROT" -> 3, + "CACHE" -> 4, + "LOCK" -> 1, + "VALID" -> 1, + "READY" -> 1, + ) + val base = Seq("AW", "AR") + .flatMap(channel => + address.map { case (field, width) => + (channel + field) -> width + } + ) + .toMap ++ Map( + "WDATA" -> p.dataWidth, + "WSTRB" -> p.strobeWidth, + "WLAST" -> 1, + "WVALID" -> 1, + "WREADY" -> 1, + "BRESP" -> 2, + "BVALID" -> 1, + "BREADY" -> 1, + "RDATA" -> p.dataWidth, + "RRESP" -> 2, + "RLAST" -> 1, + "RVALID" -> 1, + "RREADY" -> 1, + ) + base ++ + (if (p.hasId) Seq("AWID", "BID", "ARID", "RID").map(_ -> p.idWidth).toMap else Map.empty) ++ + (if (p.hasQos) Map("AWQOS" -> 4, "ARQOS" -> 4) else Map.empty) ++ + (if (p.hasRegion) Map("AWREGION" -> 4, "ARREGION" -> 4) else Map.empty) + } + + private def checkPort[T <: Record & HasVerilogIO]( + port: VerilogIO[T], + master: Boolean, + widths: Map[String, Int], + rename: String => String, + ): Unit = { + port.elements.keySet shouldBe widths.keySet.map(rename) + for ((name, width) <- widths) { + withClue(s"$name: ") { + val field = port.elements(rename(name)) + field.getWidth shouldBe width + DataMirror.directionOf(field) shouldBe + (if (masterInputs.contains(name) == master) ActualDirection.Input else ActualDirection.Output) + } + } + } + + private def checkRtl(rtl: String, widths: Map[String, Int], flat: Boolean): Unit = { + val prefix = if (flat) "" else "io_" + val pairs = Seq("sourceBefore" -> "sinkAfter", "sourceAfter" -> "sinkBefore") + for ((source, sink) <- pairs; name <- widths.keys) { + val sourcePort = s"$prefix${source}_${sourceName(name)}" + val sinkPort = s"$prefix${sink}_${sinkName(name)}" + val (to, from) = if (masterInputs.contains(name)) (sourcePort, sinkPort) else (sinkPort, sourcePort) + withClue(s"$name: ") { + rtl should include(s"assign $to = $from;") + } + } + rtl should not include "_bits_" + } + + private def drive(field: Data, value: BigInt): Unit = field match { + case bool: Bool => bool #= (value != 0).B + case uint: UInt => uint #= value.U(uint.getWidth.W) + case valueType: EnumType => valueType.poke(value) + case other => fail(s"Unexpected AXI field type: ${other.getClass.getSimpleName}") + } + + private def expectValue(field: Data, value: BigInt): Unit = field match { + case bool: Bool => bool expect (value != 0).B + case uint: UInt => uint expect value.U(uint.getWidth.W) + case valueType: EnumType => valueType.expect(value) + case other => fail(s"Unexpected AXI field type: ${other.getClass.getSimpleName}") + } + + private def exercise[T <: Record & HasVerilogIO]( + pairs: Seq[(VerilogIO[T], VerilogIO[T])], + widths: Map[String, Int], + clock: Clock, + ): Unit = { + val paths = for { + (pair, pairIndex) <- pairs.zipWithIndex + name <- widths.keys.toSeq.sorted + } yield { + val (source, sink) = pair + val s = source.elements(sourceName(name)) + val m = sink.elements(sinkName(name)) + val (input, output) = if (masterInputs.contains(name)) (m, s) else (s, m) + (s"pair=$pairIndex $name", name, input, output, widths(name)) + } + + // Activate one field at a time to detect cross-channel and cross-interface aliasing. + paths.foreach { case (_, _, input, _, _) => drive(input, 0) } + for (selected <- paths.indices) { + val (_, _, input, _, _) = paths(selected) + drive(input, 1) + for (((label, _, _, output, _), index) <- paths.zipWithIndex) { + withClue(s"selected=$selected $label: ") { expectValue(output, if (index == selected) 1 else 0) } + } + drive(input, 0) + } + + val random = new Random(0x415849L) + for (_ <- 0 until 16) { + val values = paths.map { case (_, name, input, _, width) => + val value = if (name.endsWith("BURST")) BigInt(random.nextInt(3)) else BigInt(width, random) + drive(input, value) + value + } + clock.step() + for (((label, _, _, output, _), value) <- paths.zip(values)) { + withClue(s"$label: ") { expectValue(output, value) } + } + } + } + + "VerilogIO" should "derive all AXI ports, optional fields, widths, and both role orders" in { + for { + idWidth <- Seq(0, 3) + qos <- Seq(false, true) + region <- Seq(false, true) + flat <- Seq(false, true) + outerFlipped <- Seq(false, true) + } { + val p = AxiParams(64, 40, idWidth, qos, region) + var dut: AxiHarness = null + ChiselStage.emitCHIRRTL { dut = new AxiHarness(p, flat, outerFlipped); dut } + withClue(s"params=$p flat=$flat outerFlipped=$outerFlipped: ") { + for ((source, sink) <- dut.pairs) { + checkPort(source, master = outerFlipped, axiWidths(p), sourceName) + checkPort(sink, master = !outerFlipped, axiWidths(p), sinkName) + } + dut.sourceBefore.params shouldBe p + dut.sourceAfter.params shouldBe p + dut.sinkBefore.params shouldBe p + dut.sinkAfter.params shouldBe p + dut.burst.getWidth shouldBe 2 + dut.response.getWidth shouldBe 2 + } + } + } + + it should "derive precisely the AXI-Lite ports at both supported data widths" in { + for (dataWidth <- Seq(32, 64); flat <- Seq(false, true)) { + val p = AxiLiteParams(dataWidth, 24) + var dut: LiteHarness = null + ChiselStage.emitCHIRRTL { dut = new LiteHarness(p, flat); dut } + for ((source, sink) <- dut.pairs) { + checkPort(source, master = false, liteWidths(p), sourceName) + checkPort(sink, master = true, liteWidths(p), sinkName) + } + dut.sourceBefore.params shouldBe p + dut.sinkAfter.params shouldBe p + dut.response.getWidth shouldBe 2 + } + } + + for (flat <- Seq(false, true)) { + it should s"emit the renamed AXI ports and direct view connections with flat=$flat" in { + val p = AxiParams(64, 40, idWidth = 3, hasQos = true, hasRegion = true) + checkRtl(ChiselStage.emitSystemVerilog(new AxiHarness(p, flat)), axiWidths(p), flat) + } + + it should s"emit the renamed AXI-Lite ports and direct view connections with flat=$flat" in { + val p = AxiLiteParams(32, 24) + checkRtl(ChiselStage.emitSystemVerilog(new LiteHarness(p, flat)), liteWidths(p), flat) + } + } + + it should "transfer every AXI field through views with different names and both role orders" in { + val p = AxiParams(64, 40, idWidth = 3, hasQos = true, hasRegion = true) + simulate(new AxiHarness(p)) { dut => exercise(dut.pairs, axiWidths(p), dut.clock) } + } + + it should "transfer every AXI-Lite field through views with different names and both role orders" in { + val p = AxiLiteParams(64, 24) + simulate(new LiteHarness(p)) { dut => exercise(dut.pairs, liteWidths(p), dut.clock) } + } + + it should "use transformed names directly on ExtModule pins" in { + val p = AxiLiteParams(32, 24) + val rtl = ChiselStage.emitSystemVerilog(new ExternalHarness(p)) + for (name <- liteWidths(p).keys) { + withClue(s"$name: ") { + ("\\." + name + "_ip\\s*\\(\\s*" + sourceName(name) + "\\s*\\)").r.findFirstIn(rtl) should not be empty + } + } + } + + it should "clone interface types created outside a Module without sharing leaf instances" in { + val gen = new AxiLiteIO(32, 24) + val first = gen.createVerilogIO() + val second = gen.createVerilogIO() + first.elements.keys.toSeq shouldBe second.elements.keys.toSeq + for (name <- first.elements.keys) { + (first.elements(name) eq second.elements(name)) shouldBe false + } + DataMirror.specifiedDirectionOf(gen) shouldBe SpecifiedDirection.Unspecified + gen.isMaster shouldBe true + ChiselStage.emitSystemVerilog(new ReusedTypeHarness(first)) should include("source_AWADDR") + } + + it should "support ordinary Bundles and Vecs without requiring IsMasterSlave" in { + val modes = + Seq("normal", "flipped before", "flipped after", "input before", "input after", "output before", "output after") + for (mode <- modes) { + var dut: PlainHarness = null + ChiselStage.emitCHIRRTL { dut = new PlainHarness(mode); dut } + dut.rtl.isInstanceOf[IsMasterSlave] shouldBe false + dut.rtl.elements.keySet shouldBe Set("REQUEST_0", "REQUEST_1", "RESULT_0", "RESULT_1", "STATUS") + for ((name, field) <- dut.rtl.elements) { + val input = + if (mode.startsWith("input")) true + else if (mode.startsWith("output")) false + else name.startsWith("REQUEST") != mode.startsWith("flipped") + DataMirror.directionOf(field) shouldBe (if (input) ActualDirection.Input else ActualDirection.Output) + } + DataMirror.directionOf(dut.view.request(0)) shouldBe DataMirror.directionOf(dut.rtl.elements("REQUEST_0")) + DataMirror.directionOf(dut.view.result(1)) shouldBe DataMirror.directionOf(dut.rtl.elements("RESULT_1")) + DataMirror.directionOf(dut.view.status) shouldBe DataMirror.directionOf(dut.rtl.elements("STATUS")) + } + } + + it should "reject duplicate names after applying the user transform" in { + val error = intercept[IllegalArgumentException] { new AxiLiteIO(32, 32).createVerilogIO(f = _ => "same") } + error.getMessage should include("unique") + } + + it should "reject empty, malformed, and null field names" in { + for (name <- Seq("", "1ADDR", "AW-ADDR", "AW.ADDR", "AW ADDR", null)) { + val error = intercept[IllegalArgumentException] { new AxiLiteIO(32, 32).createVerilogIO(f = _ => name) } + error.getMessage should include("Invalid RTL field name") + } + } + + it should "require an unbound interface when converting to VerilogIO" in { + intercept[ExpectedChiselTypeException] { ChiselStage.emitCHIRRTL(new InvalidHarness("bound conversion")) } + } + + it should "require bound hardware when constructing the original interface view" in { + intercept[ExpectedHardwareException] { ChiselStage.emitCHIRRTL(new InvalidHarness("unbound view")) } + } + + it should "preserve the guard against applying a role twice across conversion" in { + val error = intercept[IllegalArgumentException] { ChiselStage.emitCHIRRTL(new InvalidHarness("repeated role")) } + error.getMessage should include("wrapped twice") + } + + it should "preserve AXI widths at the minimum and maximum supported data widths" in { + for (dataWidth <- Seq(8, 1024); addrWidth <- Seq(1, 65)) { + val p = AxiParams(dataWidth, addrWidth, idWidth = 1) + var dut: AxiHarness = null + ChiselStage.emitCHIRRTL { dut = new AxiHarness(p); dut } + for ((source, sink) <- dut.pairs) { + checkPort(source, master = false, axiWidths(p), sourceName) + checkPort(sink, master = true, axiWidths(p), sinkName) + } + } + } + + it should "connect an independent FlatIO to a hierarchical AXI port using default RTL names" in { + val p = AxiParams(32, 24) + var dut: IndependentAxiHarness = null + val rtl = ChiselStage.emitSystemVerilog { dut = new IndependentAxiHarness(p); dut } + checkPort(dut.source, master = false, axiWidths(p), identity) + rtl should include("assign sink_aw_bits_addr = AWADDR;") + rtl should include("assign AWREADY = sink_aw_ready;") + rtl should include("assign RDATA = sink_r_bits_data;") + rtl should not include "source_AWADDR" + } + + it should "support interfaces originally declared from the slave perspective" in { + for (roleBefore <- Seq(false, true)) { + var dut: DeclaredSlaveHarness = null + val rtl = ChiselStage.emitSystemVerilog { dut = new DeclaredSlaveHarness(roleBefore); dut } + DataMirror.directionOf(dut.source.elements("REQUEST")) shouldBe ActualDirection.Input + DataMirror.directionOf(dut.source.elements("RESPONSE")) shouldBe ActualDirection.Output + DataMirror.directionOf(dut.sink.elements("REQUEST")) shouldBe ActualDirection.Output + DataMirror.directionOf(dut.sink.elements("RESPONSE")) shouldBe ActualDirection.Input + rtl should include("assign sink_REQUEST = source_REQUEST;") + rtl should include("assign source_RESPONSE = sink_RESPONSE;") + } + } + + it should "follow Input and Output coercion applied to the flat record or its enclosing Bundle" in { + for (inputs <- Seq(false, true); outer <- Seq(false, true)) { + var dut: CoercedAxiHarness = null + ChiselStage.emitCHIRRTL { dut = new CoercedAxiHarness(inputs, outer); dut } + val expected = if (inputs) ActualDirection.Input else ActualDirection.Output + for (field <- dut.io.axi.elements.values) DataMirror.directionOf(field) shouldBe expected + for (field <- Seq(dut.view.aw.bits.addr, dut.view.aw.ready, dut.view.b.bits.resp, dut.view.r.ready)) { + DataMirror.directionOf(field) shouldBe expected + } + } + } + + it should "preserve signed, clock, and reset leaf types in the flat record and view" in { + var dut: DigitalHarness = null + ChiselStage.emitCHIRRTL { dut = new DigitalHarness; dut } + dut.rtl.elements("domainClock").isInstanceOf[Clock] shouldBe true + dut.rtl.elements("syncReset").isInstanceOf[Reset] shouldBe true + dut.rtl.elements("asyncReset").isInstanceOf[AsyncReset] shouldBe true + dut.rtl.elements("signedIn").isInstanceOf[SInt] shouldBe true + dut.rtl.elements("signedOut").getWidth shouldBe 9 + DataMirror.directionOf(dut.view.signedIn) shouldBe ActualDirection.Input + DataMirror.directionOf(dut.view.signedOut) shouldBe ActualDirection.Output + } + + for (kind <- Seq("Analog", "Probe")) { + it should s"reject unsupported $kind fields explicitly" in { + val error = intercept[IllegalArgumentException] { new UnsupportedIO(kind).createVerilogIO() } + error.getMessage should include(s"does not support $kind") + } + } +} From d28f2376a5a894809bdf77ada5d125ac971afa88 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Tue, 6 Oct 2026 14:10:14 +0800 Subject: [PATCH 21/26] feat(amba): add combinational AXI-Lite to AXI bridge Adapt the current AXI interfaces with fixed IDs and single-beat fields, without buffering or added channel latency. Check response IDs, RLAST, and unexpected EXOKAY responses. Add ChipmunkFlatSpec coverage for 32/64-bit transfers, optional fields, both AW/W arrival orders, independent stalls, and parameter validation. --- chipmunk/src/amba/AxiLiteToAxiBridge.scala | 63 ++++++ .../src/amba/AxiLiteToAxiBridgeSpec.scala | 189 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 chipmunk/src/amba/AxiLiteToAxiBridge.scala create mode 100644 chipmunk/test/src/amba/AxiLiteToAxiBridgeSpec.scala diff --git a/chipmunk/src/amba/AxiLiteToAxiBridge.scala b/chipmunk/src/amba/AxiLiteToAxiBridge.scala new file mode 100644 index 0000000..ab42210 --- /dev/null +++ b/chipmunk/src/amba/AxiLiteToAxiBridge.scala @@ -0,0 +1,63 @@ +package chipmunk +package amba + +import chisel3.* + +/** Adapt AXI4-Lite to same-width, single-beat AXI4 transactions with fixed IDs. + * + * All five channels are connected combinationally, without buffering or added latency. AW and W remain independent. + * Transactions in each direction use one ID and retain their order. + */ +final class AxiLiteToAxiBridge(val params: AxiParams, writeId: BigInt = 0, readId: BigInt = 0) extends Module { + private val liteParams = AxiLiteParams(params.dataWidth, params.addrWidth) + + require(writeId >= 0 && writeId.bitLength <= params.idWidth, "Write ID must fit the AXI ID width.") + require(readId >= 0 && readId.bitLength <= params.idWidth, "Read ID must fit the AXI ID width.") + + val io = IO(new Bundle { + val sAxiLite = Slave(new AxiLiteIO(liteParams)) + val mAxi = Master(new AxiIO(params)) + }) + + io.mAxi.aw handshakeFrom io.sAxiLite.aw + driveAddress(io.mAxi.aw.bits, io.sAxiLite.aw.bits, writeId) + + io.mAxi.ar handshakeFrom io.sAxiLite.ar + driveAddress(io.mAxi.ar.bits, io.sAxiLite.ar.bits, readId) + + io.mAxi.w handshakeFrom io.sAxiLite.w + io.mAxi.w.bits.data := io.sAxiLite.w.bits.data + io.mAxi.w.bits.strb := io.sAxiLite.w.bits.strb + io.mAxi.w.bits.last := true.B + + io.sAxiLite.b handshakeFrom io.mAxi.b + io.sAxiLite.b.bits.resp := io.mAxi.b.bits.resp + + io.sAxiLite.r handshakeFrom io.mAxi.r + io.sAxiLite.r.bits.data := io.mAxi.r.bits.data + io.sAxiLite.r.bits.resp := io.mAxi.r.bits.resp + + when(io.mAxi.b.fire) { + io.mAxi.b.bits.id.foreach(id => assert(id === writeId.U, "Unexpected AXI write response ID.")) + assert(io.mAxi.b.bits.resp =/= AxiResp.RESP_EXOKAY, "Normal AXI writes must not return EXOKAY.") + } + + when(io.mAxi.r.fire) { + io.mAxi.r.bits.id.foreach(id => assert(id === readId.U, "Unexpected AXI read response ID.")) + assert(io.mAxi.r.bits.last, "Single-beat AXI reads must assert RLAST.") + assert(io.mAxi.r.bits.resp =/= AxiResp.RESP_EXOKAY, "Normal AXI reads must not return EXOKAY.") + } + + private def driveAddress(out: AxiWriteAddrChannel, in: AxiLiteAddrChannel, id: BigInt): Unit = { + out.addr := in.addr + out.prot := in.prot + out.size := AxiBurstSize(params.fullSize.U) + out.len := 0.U + out.burst := AxiBurstType.BURST_INCR + out.lock := 0.U + out.cache := 0.U + out.id.foreach(_ := id.U(params.idWidth.W)) + out.qos.foreach(_ := 0.U) + out.region.foreach(_ := 0.U) + } +} diff --git a/chipmunk/test/src/amba/AxiLiteToAxiBridgeSpec.scala b/chipmunk/test/src/amba/AxiLiteToAxiBridgeSpec.scala new file mode 100644 index 0000000..8fa9aa1 --- /dev/null +++ b/chipmunk/test/src/amba/AxiLiteToAxiBridgeSpec.scala @@ -0,0 +1,189 @@ +package chipmunk.test.amba + +import scala.util.Random + +import chisel3.* +import circt.stage.ChiselStage + +import chipmunk.amba.* +import chipmunk.test.ChipmunkFlatSpec + +final class AxiLiteToAxiBridgeSpec extends ChipmunkFlatSpec { + private val responses = Seq(AxiResp.RESP_OKAY, AxiResp.RESP_SLVERR, AxiResp.RESP_DECERR) + + for ( + params <- Seq(AxiParams(32, 16), AxiParams(64, 16, idWidth = 40, hasQos = true, hasRegion = true)); + wFirst <- Seq(true, false) + ) { + s"AxiLiteToAxiBridge(${params.dataWidth}, ${params.idWidth}, W first = $wFirst)" should + "adapt transactions without added latency under independent channel stalls" in { + val writeId = if (params.hasId) (BigInt(1) << 39) + 3 else BigInt(0) + val readId = if (params.hasId) BigInt(17) else BigInt(0) + simulate(new AxiLiteToAxiBridge(params, writeId, readId)) { dut => + val random = new Random(42) + val count = 48 + var awSent, wSent, arSent = 0 + var awReceived, wReceived, arReceived = 0 + var bProduced, rProduced = 0 + var bDelivered, rDelivered = 0 + var cycle = 0 + var heldAw, heldAr, heldW, heldB, heldR = Option.empty[Seq[BigInt]] + + def address(index: Int): BigInt = BigInt(index * params.strobeWidth) + def data(index: Int): BigInt = (BigInt(1) << (params.dataWidth - 2)) + index + def strobe(index: Int): BigInt = + if (index == count - 1) (BigInt(1) << params.strobeWidth) - 1 + else BigInt(index) & ((BigInt(1) << params.strobeWidth) - 1) + def response(index: Int): AxiResp.Type = responses(index % responses.size) + def readData(index: Int): BigInt = (BigInt(1) << (params.dataWidth - 1)) + index + def bool(value: Bool): Boolean = value.peek().litToBoolean + def sampleAddress(bits: AxiWriteAddrChannel): Seq[BigInt] = + Seq(bits.addr, bits.prot, bits.size, bits.len, bits.burst, bits.lock, bits.cache) + .map(_.peek().litValue) ++ + bits.id.toSeq.map(_.peek().litValue) ++ + bits.qos.toSeq.map(_.peek().litValue) ++ + bits.region.toSeq.map(_.peek().litValue) + def checkHeld(previous: Option[Seq[BigInt]], valid: Bool, payload: => Seq[BigInt]): Unit = + previous.foreach { expected => + assert(bool(valid), "VALID was withdrawn under backpressure") + assert(payload == expected, "Payload changed under backpressure") + } + + while (bDelivered < count || rDelivered < count) { + assert(cycle < 2000, "Traffic did not complete") + // Exercise both AW/W arrival orders with different initial channel stalls. + dut.io.sAxiLite.aw.valid.poke((awSent < count && (!wFirst || cycle >= 5)).B) + dut.io.sAxiLite.aw.bits.addr.poke(address(awSent).U) + dut.io.sAxiLite.aw.bits.prot.poke((awSent % 8).U) + dut.io.sAxiLite.w.valid.poke((wSent < count && (wFirst || cycle >= 5)).B) + dut.io.sAxiLite.w.bits.data.poke(data(wSent).U) + dut.io.sAxiLite.w.bits.strb.poke(strobe(wSent).U) + dut.io.sAxiLite.ar.valid.poke((arSent < count).B) + dut.io.sAxiLite.ar.bits.addr.poke(address(arSent).U) + dut.io.sAxiLite.ar.bits.prot.poke((arSent % 8).U) + + val awReady = (!wFirst || cycle >= 20) && random.nextBoolean() + val wReady = (wFirst || cycle >= 20) && random.nextBoolean() + val arReady = random.nextBoolean() + val bReady = cycle >= 100 && random.nextBoolean() + val rReady = cycle >= 100 && random.nextBoolean() + dut.io.mAxi.aw.ready.poke(awReady.B) + dut.io.mAxi.w.ready.poke(wReady.B) + dut.io.mAxi.ar.ready.poke(arReady.B) + dut.io.sAxiLite.b.ready.poke(bReady.B) + dut.io.sAxiLite.r.ready.poke(rReady.B) + + // The target generates exactly one response for each accepted transaction. + dut.io.mAxi.b.valid.poke((bProduced < math.min(awReceived, wReceived)).B) + dut.io.mAxi.b.bits.resp.poke(response(bProduced)) + dut.io.mAxi.b.bits.id.foreach(_.poke(writeId.U)) + dut.io.mAxi.r.valid.poke((rProduced < arReceived).B) + dut.io.mAxi.r.bits.data.poke(readData(rProduced).U) + dut.io.mAxi.r.bits.resp.poke(response(rProduced)) + dut.io.mAxi.r.bits.last.poke(true.B) + dut.io.mAxi.r.bits.id.foreach(_.poke(readId.U)) + + val awPayload = sampleAddress(dut.io.mAxi.aw.bits) + val arPayload = sampleAddress(dut.io.mAxi.ar.bits) + val wPayload = Seq( + dut.io.mAxi.w.bits.data.peek().litValue, + dut.io.mAxi.w.bits.strb.peek().litValue, + dut.io.mAxi.w.bits.last.peek().litValue, + ) + val bPayload = Seq(dut.io.sAxiLite.b.bits.resp.peek().litValue) + val rPayload = Seq(dut.io.sAxiLite.r.bits.data.peek().litValue, dut.io.sAxiLite.r.bits.resp.peek().litValue) + checkHeld(heldAw, dut.io.mAxi.aw.valid, awPayload) + checkHeld(heldAr, dut.io.mAxi.ar.valid, arPayload) + checkHeld(heldW, dut.io.mAxi.w.valid, wPayload) + checkHeld(heldB, dut.io.sAxiLite.b.valid, bPayload) + checkHeld(heldR, dut.io.sAxiLite.r.valid, rPayload) + + // Every channel forwards VALID and READY without adding a cycle. + for ( + (inputValid, outputValid, inputReady, outputReady) <- Seq( + (dut.io.sAxiLite.aw.valid, dut.io.mAxi.aw.valid, dut.io.sAxiLite.aw.ready, dut.io.mAxi.aw.ready), + (dut.io.sAxiLite.w.valid, dut.io.mAxi.w.valid, dut.io.sAxiLite.w.ready, dut.io.mAxi.w.ready), + (dut.io.sAxiLite.ar.valid, dut.io.mAxi.ar.valid, dut.io.sAxiLite.ar.ready, dut.io.mAxi.ar.ready), + (dut.io.mAxi.b.valid, dut.io.sAxiLite.b.valid, dut.io.mAxi.b.ready, dut.io.sAxiLite.b.ready), + (dut.io.mAxi.r.valid, dut.io.sAxiLite.r.valid, dut.io.mAxi.r.ready, dut.io.sAxiLite.r.ready), + ) + ) { + assert(bool(inputValid) == bool(outputValid), "VALID did not pass through") + assert(bool(inputReady) == bool(outputReady), "READY did not pass through") + } + + def checkAddress(bits: AxiWriteAddrChannel, index: Int, id: BigInt): Unit = { + bits.addr.expect(address(index).U) + bits.prot.expect((index % 8).U) + assert(bits.size.peek().litValue == params.fullSize) + bits.len.expect(0.U) + bits.burst.expect(AxiBurstType.BURST_INCR) + bits.lock.expect(0.U) + bits.cache.expect(0.U) + bits.id.foreach(_.expect(id.U)) + bits.qos.foreach(_.expect(0.U)) + bits.region.foreach(_.expect(0.U)) + } + val awFire = bool(dut.io.mAxi.aw.valid) && awReady + val wFire = bool(dut.io.mAxi.w.valid) && wReady + val arFire = bool(dut.io.mAxi.ar.valid) && arReady + val bFire = bool(dut.io.sAxiLite.b.valid) && bReady + val rFire = bool(dut.io.sAxiLite.r.valid) && rReady + val awInputFire = bool(dut.io.sAxiLite.aw.valid) && bool(dut.io.sAxiLite.aw.ready) + val wInputFire = bool(dut.io.sAxiLite.w.valid) && bool(dut.io.sAxiLite.w.ready) + val arInputFire = bool(dut.io.sAxiLite.ar.valid) && bool(dut.io.sAxiLite.ar.ready) + val bInputFire = bool(dut.io.mAxi.b.valid) && bool(dut.io.mAxi.b.ready) + val rInputFire = bool(dut.io.mAxi.r.valid) && bool(dut.io.mAxi.r.ready) + if (awFire) checkAddress(dut.io.mAxi.aw.bits, awReceived, writeId) + if (arFire) checkAddress(dut.io.mAxi.ar.bits, arReceived, readId) + if (wFire) { + dut.io.mAxi.w.bits.data.expect(data(wReceived).U) + dut.io.mAxi.w.bits.strb.expect(strobe(wReceived).U) + dut.io.mAxi.w.bits.last.expect(true.B) + } + if (bFire) dut.io.sAxiLite.b.bits.resp.expect(response(bDelivered)) + if (rFire) { + dut.io.sAxiLite.r.bits.data.expect(readData(rDelivered).U) + dut.io.sAxiLite.r.bits.resp.expect(response(rDelivered)) + } + heldAw = Option.when(bool(dut.io.mAxi.aw.valid) && !awReady)(awPayload) + heldAr = Option.when(bool(dut.io.mAxi.ar.valid) && !arReady)(arPayload) + heldW = Option.when(bool(dut.io.mAxi.w.valid) && !wReady)(wPayload) + heldB = Option.when(bool(dut.io.sAxiLite.b.valid) && !bReady)(bPayload) + heldR = Option.when(bool(dut.io.sAxiLite.r.valid) && !rReady)(rPayload) + dut.clock.step() + if (awInputFire) awSent += 1 + if (wInputFire) wSent += 1 + if (arInputFire) arSent += 1 + if (awFire) awReceived += 1 + if (wFire) wReceived += 1 + if (arFire) arReceived += 1 + if (bInputFire) bProduced += 1 + if (rInputFire) rProduced += 1 + if (bFire) bDelivered += 1 + if (rFire) rDelivered += 1 + cycle += 1 + } + assert( + Seq(awSent, wSent, arSent, awReceived, wReceived, arReceived, bProduced, rProduced, bDelivered, rDelivered) + .forall(_ == count) + ) + } + } + } + + "AxiLiteToAxiBridge" should "reject unsupported widths and IDs during elaboration" in { + for ( + (params, writeId, readId) <- Seq( + (AxiParams(128, 16), BigInt(0), BigInt(0)), + (AxiParams(32, 16), BigInt(1), BigInt(0)), + (AxiParams(32, 16, idWidth = 3), BigInt(8), BigInt(0)), + (AxiParams(32, 16, idWidth = 3), BigInt(0), BigInt(-1)), + ) + ) { + intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new AxiLiteToAxiBridge(params, writeId, readId)) + } + } + } +} From f4b620877136b4bf596875457dc9b61a9d520be6 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Tue, 6 Oct 2026 16:28:51 +0800 Subject: [PATCH 22/26] feat: add Acorn AXI bridges and width adapter Add full-width AXI-to-Acorn burst conversion and single-beat Acorn-to-AXI and Acorn-to-AXI-Lite bridges with independent read/write control. Add AcornWidthAdapter with registered transactions, byte-lane conversion, and first-error termination. Document read-footprint and split-access requirements without adding resizing permission switches. Add ChipmunkFlatSpec coverage for independent handshakes, backpressure, bursts, byte masks, error responses, and parameter validation. --- chipmunk/src/acorn/AcornWidthAdapter.scala | 154 +++++++ chipmunk/src/amba/AcornToAxiBridge.scala | 146 +++++++ chipmunk/src/amba/AcornToAxiLiteBridge.scala | 44 ++ chipmunk/src/amba/AxiToAcornBridge.scala | 211 ++++++++++ .../src/acorn/AcornWidthAdapterSpec.scala | 228 ++++++++++ .../test/src/amba/AcornToAxiBridgesSpec.scala | 273 ++++++++++++ .../test/src/amba/AxiToAcornBridgeSpec.scala | 390 ++++++++++++++++++ 7 files changed, 1446 insertions(+) create mode 100644 chipmunk/src/acorn/AcornWidthAdapter.scala create mode 100644 chipmunk/src/amba/AcornToAxiBridge.scala create mode 100644 chipmunk/src/amba/AcornToAxiLiteBridge.scala create mode 100644 chipmunk/src/amba/AxiToAcornBridge.scala create mode 100644 chipmunk/test/src/acorn/AcornWidthAdapterSpec.scala create mode 100644 chipmunk/test/src/amba/AcornToAxiBridgesSpec.scala create mode 100644 chipmunk/test/src/amba/AxiToAcornBridgeSpec.scala diff --git a/chipmunk/src/acorn/AcornWidthAdapter.scala b/chipmunk/src/acorn/AcornWidthAdapter.scala new file mode 100644 index 0000000..74d17b8 --- /dev/null +++ b/chipmunk/src/acorn/AcornWidthAdapter.scala @@ -0,0 +1,154 @@ +package chipmunk +package acorn + +import chisel3.* +import chisel3.util.log2Ceil + +/** Convert naturally aligned Acorn words while preserving byte addresses and lane order. + * + * Unaligned commands return a local error without accessing the endpoint; error reads return zero. + * + * The endpoint must permit the expanded read footprint or non-atomic split reads, as applicable, with no read side + * effects. Writes require byte-strobe semantics independent of access width and permission for non-atomic splitting. + * The entire converted word must lie within the authorized endpoint region. These are integration requirements, not + * properties the adapter can discover or enforce from Acorn commands. + * + * Narrow-to-wide accesses select one lane of a wider word. Wide-to-narrow accesses visit all subwords in increasing + * address order, including zero-strobe writes, and stop at the first error. Earlier writes are not rolled back. No + * read-modify-write, request merging, or reuse of earlier read data is performed. Read/write directions are + * independent, with one command each until its upstream response transfers. Endpoint requests and upstream responses + * are buffered; all connected components share clock/reset and must cancel outstanding transactions consistently on + * reset. + */ +final class AcornWidthAdapter(val inputParams: AcornParams, val outputParams: AcornParams) extends Module { + require(inputParams.addrWidth == outputParams.addrWidth, "Width adaptation must preserve the byte-address width.") + require( + inputParams.addrWidth >= log2Ceil(math.max(inputParams.bytesPerWord, outputParams.bytesPerWord)), + "The address width must cover the larger word.", + ) + + private val split = inputParams.dataWidth > outputParams.dataWidth + private val expand = inputParams.dataWidth < outputParams.dataWidth + private val segments = if (split) inputParams.dataWidth / outputParams.dataWidth else 1 + private val indexBits = math.max(1, log2Ceil(segments)) + private val addrMask = ((BigInt(1) << outputParams.addrWidth) - outputParams.bytesPerWord).U + + val io = IO(new Bundle { + val sAcorn = Slave(new AcornIO(inputParams)) + val mAcorn = Master(new AcornIO(outputParams)) + }) + + private object Phase extends ChiselEnum { + val Idle, Send, Wait, Respond = Value + } + + private val readPhase = RegInit(Phase.Idle) + private val readAddress = Reg(UInt(inputParams.addrWidth.W)) + private val readIndex = Reg(UInt(indexBits.W)) + private val readData = Reg(UInt(inputParams.dataWidth.W)) + private val readError = Reg(Bool()) + + io.sAcorn.rd.cmd.ready := readPhase === Phase.Idle + when(io.sAcorn.rd.cmd.fire) { + val invalid = !aligned(io.sAcorn.rd.cmd.bits.addr) + readAddress := io.sAcorn.rd.cmd.bits.addr + readIndex := 0.U + readData := 0.U + readError := invalid + readPhase := Mux(invalid, Phase.Respond, Phase.Send) + } + + io.mAcorn.rd.cmd.valid := readPhase === Phase.Send + io.mAcorn.rd.cmd.bits.addr := endpointAddress(readAddress, readIndex) + when(io.mAcorn.rd.cmd.fire) { + readPhase := Phase.Wait + } + + io.mAcorn.rd.rsp.ready := readPhase === Phase.Wait + when(io.mAcorn.rd.rsp.fire) { + val returned = if (split) { + readData | (io.mAcorn.rd.rsp.bits.data << bitOffset(readIndex))(inputParams.dataWidth - 1, 0) + } else if (expand) { + (io.mAcorn.rd.rsp.bits.data >> laneOffset(readAddress))(inputParams.dataWidth - 1, 0) + } else { + io.mAcorn.rd.rsp.bits.data + } + readData := Mux(io.mAcorn.rd.rsp.bits.error, 0.U, returned) + readError := io.mAcorn.rd.rsp.bits.error + when(io.mAcorn.rd.rsp.bits.error || readIndex === (segments - 1).U) { + readPhase := Phase.Respond + }.otherwise { + readIndex := readIndex + 1.U + readPhase := Phase.Send + } + } + + io.sAcorn.rd.rsp.valid := readPhase === Phase.Respond + io.sAcorn.rd.rsp.bits.data := readData + io.sAcorn.rd.rsp.bits.error := readError + when(io.sAcorn.rd.rsp.fire) { + readPhase := Phase.Idle + } + + private val writePhase = RegInit(Phase.Idle) + private val writeAddress = Reg(UInt(inputParams.addrWidth.W)) + private val writeIndex = Reg(UInt(indexBits.W)) + private val writeData = Reg(UInt(inputParams.dataWidth.W)) + private val writeStrobe = Reg(UInt(inputParams.bytesPerWord.W)) + private val writeError = Reg(Bool()) + + io.sAcorn.wr.cmd.ready := writePhase === Phase.Idle + when(io.sAcorn.wr.cmd.fire) { + val invalid = !aligned(io.sAcorn.wr.cmd.bits.addr) + writeAddress := io.sAcorn.wr.cmd.bits.addr + writeIndex := 0.U + writeData := io.sAcorn.wr.cmd.bits.data + writeStrobe := io.sAcorn.wr.cmd.bits.strobe + writeError := invalid + writePhase := Mux(invalid, Phase.Respond, Phase.Send) + } + + io.mAcorn.wr.cmd.valid := writePhase === Phase.Send + io.mAcorn.wr.cmd.bits.addr := endpointAddress(writeAddress, writeIndex) + if (split) { + io.mAcorn.wr.cmd.bits.data := (writeData >> bitOffset(writeIndex))(outputParams.dataWidth - 1, 0) + io.mAcorn.wr.cmd.bits.strobe := + (writeStrobe >> (writeIndex << log2Ceil(outputParams.bytesPerWord)))(outputParams.bytesPerWord - 1, 0) + } else if (expand) { + io.mAcorn.wr.cmd.bits.data := (writeData << laneOffset(writeAddress))(outputParams.dataWidth - 1, 0) + io.mAcorn.wr.cmd.bits.strobe := + (writeStrobe << writeAddress(log2Ceil(outputParams.bytesPerWord) - 1, 0))(outputParams.bytesPerWord - 1, 0) + } else { + io.mAcorn.wr.cmd.bits.data := writeData + io.mAcorn.wr.cmd.bits.strobe := writeStrobe + } + when(io.mAcorn.wr.cmd.fire) { + writePhase := Phase.Wait + } + + io.mAcorn.wr.rsp.ready := writePhase === Phase.Wait + when(io.mAcorn.wr.rsp.fire) { + writeError := io.mAcorn.wr.rsp.bits.error + when(io.mAcorn.wr.rsp.bits.error || writeIndex === (segments - 1).U) { + writePhase := Phase.Respond + }.otherwise { + writeIndex := writeIndex + 1.U + writePhase := Phase.Send + } + } + + io.sAcorn.wr.rsp.valid := writePhase === Phase.Respond + io.sAcorn.wr.rsp.bits.error := writeError + when(io.sAcorn.wr.rsp.fire) { + writePhase := Phase.Idle + } + + private def aligned(address: UInt): Bool = (address & (inputParams.bytesPerWord - 1).U) === 0.U + + private def endpointAddress(address: UInt, index: UInt): UInt = + if (split) address + (index << log2Ceil(outputParams.bytesPerWord)) else address & addrMask + + private def bitOffset(index: UInt): UInt = index << log2Ceil(outputParams.dataWidth) + + private def laneOffset(address: UInt): UInt = address(log2Ceil(outputParams.bytesPerWord) - 1, 0) << 3 +} diff --git a/chipmunk/src/amba/AcornToAxiBridge.scala b/chipmunk/src/amba/AcornToAxiBridge.scala new file mode 100644 index 0000000..2d7286f --- /dev/null +++ b/chipmunk/src/amba/AcornToAxiBridge.scala @@ -0,0 +1,146 @@ +package chipmunk +package amba + +import chisel3.* + +import chipmunk.acorn.{AcornIO, AcornParams} + +/** Convert Acorn word accesses to same-width, single-beat, non-exclusive AXI4 transactions. + * + * Each direction accepts one command and retains its payload until the AXI request has transferred. AW and W complete + * independently. AXI responses are buffered and held until Acorn accepts them; a direction accepts its next command + * only after that response transfers. Read and write directions operate independently. + * + * Unaligned commands return a local error without accessing AXI; rejected reads return zero. Write strobes, including + * zero strobes, are forwarded unchanged. CACHE/QoS/REGION are zero; PROT and the fixed IDs are configured statically. + * OKAY maps to success and every other response maps to an error. Unexpected IDs, missing RLAST, and EXOKAY are + * checked by assertions. All endpoints share clock/reset; reset cancels outstanding transactions consistently. + */ +final class AcornToAxiBridge( + val params: AxiParams, + writeId: BigInt = 0, + readId: BigInt = 0, + writeProt: Int = 0, + readProt: Int = 0, +) extends Module { + require(params.addrWidth >= params.fullSize, "AXI address width must cover one full-width word.") + require(writeId >= 0 && writeId.bitLength <= params.idWidth, "Write ID must fit the AXI ID width.") + require(readId >= 0 && readId.bitLength <= params.idWidth, "Read ID must fit the AXI ID width.") + require(writeProt >= 0 && writeProt < 8, "Write PROT must fit three bits.") + require(readProt >= 0 && readProt < 8, "Read PROT must fit three bits.") + + private val acornParams = AcornParams(params.dataWidth, params.addrWidth) + + val io = IO(new Bundle { + val sAcorn = Slave(new AcornIO(acornParams)) + val mAxi = Master(new AxiIO(params)) + }) + + private val readActive = RegInit(false.B) + private val arPending = RegInit(false.B) + private val readAddress = Reg(UInt(params.addrWidth.W)) + private val readResponseValid = RegInit(false.B) + private val readResponseData = Reg(UInt(params.dataWidth.W)) + private val readError = Reg(Bool()) + + io.sAcorn.rd.cmd.ready := !readActive + when(io.sAcorn.rd.cmd.fire) { + val aligned = wordAligned(io.sAcorn.rd.cmd.bits.addr) + readActive := true.B + arPending := aligned + readAddress := io.sAcorn.rd.cmd.bits.addr + readResponseValid := !aligned + readResponseData := 0.U + readError := !aligned + } + + io.mAxi.ar.valid := readActive && arPending + driveAddress(io.mAxi.ar.bits, readAddress, readId, readProt) + when(io.mAxi.ar.fire) { + arPending := false.B + } + + io.mAxi.r.ready := readActive && !arPending && !readResponseValid + when(io.mAxi.r.fire) { + io.mAxi.r.bits.id.foreach(id => assert(id === readId.U, "Unexpected AXI read response ID.")) + assert(io.mAxi.r.bits.last, "Single-beat AXI reads must assert RLAST.") + assert(io.mAxi.r.bits.resp =/= AxiResp.RESP_EXOKAY, "Normal AXI reads must not return EXOKAY.") + readResponseValid := true.B + readResponseData := io.mAxi.r.bits.data + readError := io.mAxi.r.bits.resp =/= AxiResp.RESP_OKAY + } + + io.sAcorn.rd.rsp.valid := readResponseValid + io.sAcorn.rd.rsp.bits.data := readResponseData + io.sAcorn.rd.rsp.bits.error := readError + when(io.sAcorn.rd.rsp.fire) { + readActive := false.B + readResponseValid := false.B + } + + private val writeActive = RegInit(false.B) + private val awPending = RegInit(false.B) + private val wPending = RegInit(false.B) + private val writeAddress = Reg(UInt(params.addrWidth.W)) + private val writeData = Reg(UInt(params.dataWidth.W)) + private val writeStrobe = Reg(UInt(params.strobeWidth.W)) + private val writeResponseValid = RegInit(false.B) + private val writeError = Reg(Bool()) + + io.sAcorn.wr.cmd.ready := !writeActive + when(io.sAcorn.wr.cmd.fire) { + val aligned = wordAligned(io.sAcorn.wr.cmd.bits.addr) + writeActive := true.B + awPending := aligned + wPending := aligned + writeAddress := io.sAcorn.wr.cmd.bits.addr + writeData := io.sAcorn.wr.cmd.bits.data + writeStrobe := io.sAcorn.wr.cmd.bits.strobe + writeResponseValid := !aligned + writeError := !aligned + } + + io.mAxi.aw.valid := writeActive && awPending + driveAddress(io.mAxi.aw.bits, writeAddress, writeId, writeProt) + when(io.mAxi.aw.fire) { + awPending := false.B + } + + io.mAxi.w.valid := writeActive && wPending + io.mAxi.w.bits.data := writeData + io.mAxi.w.bits.strb := writeStrobe + io.mAxi.w.bits.last := true.B + when(io.mAxi.w.fire) { + wPending := false.B + } + + io.mAxi.b.ready := writeActive && !awPending && !wPending && !writeResponseValid + when(io.mAxi.b.fire) { + io.mAxi.b.bits.id.foreach(id => assert(id === writeId.U, "Unexpected AXI write response ID.")) + assert(io.mAxi.b.bits.resp =/= AxiResp.RESP_EXOKAY, "Normal AXI writes must not return EXOKAY.") + writeResponseValid := true.B + writeError := io.mAxi.b.bits.resp =/= AxiResp.RESP_OKAY + } + + io.sAcorn.wr.rsp.valid := writeResponseValid + io.sAcorn.wr.rsp.bits.error := writeError + when(io.sAcorn.wr.rsp.fire) { + writeActive := false.B + writeResponseValid := false.B + } + + private def wordAligned(address: UInt): Bool = (address & (params.strobeWidth - 1).U) === 0.U + + private def driveAddress(out: AxiWriteAddrChannel, address: UInt, id: BigInt, prot: Int): Unit = { + out.addr := address + out.prot := prot.U + out.size := AxiBurstSize(params.fullSize.U) + out.len := 0.U + out.burst := AxiBurstType.BURST_INCR + out.lock := 0.U + out.cache := 0.U + out.id.foreach(_ := id.U(params.idWidth.W)) + out.qos.foreach(_ := 0.U) + out.region.foreach(_ := 0.U) + } +} diff --git a/chipmunk/src/amba/AcornToAxiLiteBridge.scala b/chipmunk/src/amba/AcornToAxiLiteBridge.scala new file mode 100644 index 0000000..a97c073 --- /dev/null +++ b/chipmunk/src/amba/AcornToAxiLiteBridge.scala @@ -0,0 +1,44 @@ +package chipmunk +package amba + +import chisel3.* + +import chipmunk.acorn.{AcornIO, AcornParams} + +/** Convert Acorn word accesses to same-width AXI4-Lite transactions. + * + * Reuses [[AcornToAxiBridge]] transaction control with no IDs and projects its single-beat channels onto AXI4-Lite. + * Each direction retains one command until its Acorn response transfers. AW/W are independent, responses are buffered, + * and unaligned commands return local errors. Field projection adds no buffering or latency. + */ +final class AcornToAxiLiteBridge(val params: AxiLiteParams, writeProt: Int = 0, readProt: Int = 0) extends Module { + val io = IO(new Bundle { + val sAcorn = Slave(new AcornIO(AcornParams(params.dataWidth, params.addrWidth))) + val mAxiLite = Master(new AxiLiteIO(params)) + }) + + private val bridge = Module( + new AcornToAxiBridge(AxiParams(params.dataWidth, params.addrWidth), writeProt = writeProt, readProt = readProt) + ) + bridge.io.sAcorn :<>= io.sAcorn + + io.mAxiLite.aw handshakeFrom bridge.io.mAxi.aw + io.mAxiLite.aw.bits.addr := bridge.io.mAxi.aw.bits.addr + io.mAxiLite.aw.bits.prot := bridge.io.mAxi.aw.bits.prot + + io.mAxiLite.w handshakeFrom bridge.io.mAxi.w + io.mAxiLite.w.bits.data := bridge.io.mAxi.w.bits.data + io.mAxiLite.w.bits.strb := bridge.io.mAxi.w.bits.strb + + bridge.io.mAxi.b handshakeFrom io.mAxiLite.b + bridge.io.mAxi.b.bits.resp := io.mAxiLite.b.bits.resp + + io.mAxiLite.ar handshakeFrom bridge.io.mAxi.ar + io.mAxiLite.ar.bits.addr := bridge.io.mAxi.ar.bits.addr + io.mAxiLite.ar.bits.prot := bridge.io.mAxi.ar.bits.prot + + bridge.io.mAxi.r handshakeFrom io.mAxiLite.r + bridge.io.mAxi.r.bits.data := io.mAxiLite.r.bits.data + bridge.io.mAxi.r.bits.resp := io.mAxiLite.r.bits.resp + bridge.io.mAxi.r.bits.last := true.B +} diff --git a/chipmunk/src/amba/AxiToAcornBridge.scala b/chipmunk/src/amba/AxiToAcornBridge.scala new file mode 100644 index 0000000..a1b5ab5 --- /dev/null +++ b/chipmunk/src/amba/AxiToAcornBridge.scala @@ -0,0 +1,211 @@ +package chipmunk +package amba + +import chisel3.* +import chisel3.util.{log2Ceil, Queue} + +import chipmunk.acorn.{AcornIO, AcornOutstanding, AcornParams} +import chipmunk.stream.StreamQueue + +/** Split same-width AXI4 bursts into ordered Acorn word accesses. + * + * Supports naturally aligned, full-width INCR/FIXED/WRAP accesses, including partial and zero write strobes. Narrow, + * unaligned, or overflowing accesses return SLVERR without issuing Acorn commands. One burst per direction is active; + * reads and writes proceed independently. W has a two-entry queue and may arrive before AW. The read capacity reserves + * space for both pending Acorn responses and buffered AXI R beats; the write capacity limits pending Acorn responses. + * B is held until accepted and is generated only after every Acorn write response has been collected. + * + * PROT/CACHE/QoS/REGION are not interpreted. Exclusive accesses execute as ordinary accesses and never return EXOKAY. + * Protocol violations, including invalid burst lengths, crossing 4KB, and inconsistent WLAST, are checked by + * assertions. All endpoints share clock/reset; reset cancels outstanding transactions and must reset the endpoints + * consistently. + */ +final class AxiToAcornBridge(val params: AxiParams, outstanding: AcornOutstanding = AcornOutstanding()) extends Module { + require(params.addrWidth >= params.fullSize, "AXI address width must cover one full-width word.") + + private val acornParams = AcornParams(params.dataWidth, params.addrWidth) + + val io = IO(new Bundle { + val sAxi = Slave(new AxiIO(params)) + val mAcorn = Master(new AcornIO(acornParams)) + }) + + private val readActive = RegInit(false.B) + private val readRejected = Reg(Bool()) + private val readAddress = Reg(UInt(params.addrWidth.W)) + private val readBurst = Reg(AxiBurstType()) + private val readWrapMask = Reg(UInt(params.addrWidth.W)) + private val readBeats = Reg(UInt(9.W)) + private val readIssued = Reg(UInt(9.W)) + private val readReturned = Reg(UInt(9.W)) + private val readId = if (params.hasId) Some(Reg(UInt(params.idWidth.W))) else None + private val readReserved = RegInit(0.U(log2Ceil(BigInt(outstanding.read) + 1).W)) + + private val readResponses = Module( + new Queue(new AxiReadDataChannel(params), outstanding.read, pipe = false, flow = false) + ) + + io.sAxi.ar.ready := !readActive + private val readSupported = validateAddress(io.sAxi.ar.bits, io.sAxi.ar.fire) + when(io.sAxi.ar.fire) { + readActive := true.B + readRejected := !readSupported + readAddress := io.sAxi.ar.bits.addr + readBurst := io.sAxi.ar.bits.burst + readWrapMask := wrapMask(io.sAxi.ar.bits.len) + readBeats := io.sAxi.ar.bits.len +& 1.U + readIssued := 0.U + readReturned := 0.U + readId.foreach(_ := io.sAxi.ar.bits.id.get) + } + + io.mAcorn.rd.cmd.valid := + readActive && !readRejected && readIssued < readBeats && readReserved < outstanding.read.U + io.mAcorn.rd.cmd.bits.addr := readAddress + when(io.mAcorn.rd.cmd.fire) { + readIssued := readIssued + 1.U + readAddress := nextAddress(readAddress, readBurst, readWrapMask) + } + + readResponses.io.enq.valid := + readActive && Mux(readRejected, readReturned < readBeats, io.mAcorn.rd.rsp.valid) + readResponses.io.enq.bits.data := Mux(readRejected, 0.U, io.mAcorn.rd.rsp.bits.data) + readResponses.io.enq.bits.resp := + Mux(readRejected || io.mAcorn.rd.rsp.bits.error, AxiResp.RESP_SLVERR, AxiResp.RESP_OKAY) + readResponses.io.enq.bits.last := readReturned + 1.U === readBeats + readResponses.io.enq.bits.id.foreach(_ := readId.get) + io.mAcorn.rd.rsp.ready := readActive && !readRejected && readResponses.io.enq.ready + + when(readResponses.io.enq.fire) { + readReturned := readReturned + 1.U + } + when(io.mAcorn.rd.rsp.fire) { + assert(readReturned < readIssued || io.mAcorn.rd.cmd.fire, "Acorn returned an unsolicited read response.") + } + + io.sAxi.r.valid := readResponses.io.deq.valid + io.sAxi.r.bits := readResponses.io.deq.bits + readResponses.io.deq.ready := io.sAxi.r.ready + private val readReleased = io.sAxi.r.fire && !readRejected + when(io.mAcorn.rd.cmd.fire =/= readReleased) { + readReserved := Mux(io.mAcorn.rd.cmd.fire, readReserved + 1.U, readReserved - 1.U) + } + when(io.sAxi.r.fire && io.sAxi.r.bits.last) { + readActive := false.B + } + + private val writeActive = RegInit(false.B) + private val writeRejected = Reg(Bool()) + private val writeAddress = Reg(UInt(params.addrWidth.W)) + private val writeBurst = Reg(AxiBurstType()) + private val writeWrapMask = Reg(UInt(params.addrWidth.W)) + private val writeBeats = Reg(UInt(9.W)) + private val writeTaken = Reg(UInt(9.W)) + private val writeReturned = Reg(UInt(9.W)) + private val writeId = if (params.hasId) Some(Reg(UInt(params.idWidth.W))) else None + private val writeError = Reg(Bool()) + private val writePending = RegInit(0.U(log2Ceil(BigInt(outstanding.write) + 1).W)) + private val writeResponse = RegInit(false.B) + private val writeData = StreamQueue(io.sAxi.w, entries = 2, pipe = false, flow = false) + + io.sAxi.aw.ready := !writeActive + private val writeSupported = validateAddress(io.sAxi.aw.bits, io.sAxi.aw.fire) + when(io.sAxi.aw.fire) { + writeActive := true.B + writeRejected := !writeSupported + writeAddress := io.sAxi.aw.bits.addr + writeBurst := io.sAxi.aw.bits.burst + writeWrapMask := wrapMask(io.sAxi.aw.bits.len) + writeBeats := io.sAxi.aw.bits.len +& 1.U + writeTaken := 0.U + writeReturned := 0.U + writeError := !writeSupported + writeId.foreach(_ := io.sAxi.aw.bits.id.get) + } + + private val writeCanIssue = writeActive && writeTaken < writeBeats + private val writeSpace = writePending < outstanding.write.U + io.mAcorn.wr.cmd.valid := writeCanIssue && !writeRejected && writeData.valid && writeSpace + io.mAcorn.wr.cmd.bits.addr := writeAddress + io.mAcorn.wr.cmd.bits.data := writeData.bits.data + io.mAcorn.wr.cmd.bits.strobe := writeData.bits.strb + writeData.ready := writeCanIssue && Mux(writeRejected, true.B, writeSpace && io.mAcorn.wr.cmd.ready) + + when(writeData.fire) { + val last = writeTaken + 1.U === writeBeats + assert(writeData.bits.last === last, "AXI WLAST must agree with AWLEN.") + writeTaken := writeTaken + 1.U + when(writeRejected && last) { + writeResponse := true.B + } + } + when(io.mAcorn.wr.cmd.fire) { + writeAddress := nextAddress(writeAddress, writeBurst, writeWrapMask) + } + + // Consume every Acorn response independently of AXI BREADY, including the last beat's error. + io.mAcorn.wr.rsp.ready := writeActive && !writeRejected && !writeResponse + when(io.mAcorn.wr.rsp.fire) { + assert(writeReturned < writeTaken || io.mAcorn.wr.cmd.fire, "Acorn returned an unsolicited write response.") + writeReturned := writeReturned + 1.U + writeError := writeError || io.mAcorn.wr.rsp.bits.error + when(writeReturned + 1.U === writeBeats) { + writeResponse := true.B + } + } + when(io.mAcorn.wr.cmd.fire =/= io.mAcorn.wr.rsp.fire) { + writePending := Mux(io.mAcorn.wr.cmd.fire, writePending + 1.U, writePending - 1.U) + } + + io.sAxi.b.valid := writeResponse + io.sAxi.b.bits.resp := Mux(writeError, AxiResp.RESP_SLVERR, AxiResp.RESP_OKAY) + io.sAxi.b.bits.id.foreach(_ := writeId.get) + when(io.sAxi.b.fire) { + writeResponse := false.B + writeActive := false.B + } + + private def wrapMask(length: UInt): UInt = ((length +& 1.U) << params.fullSize) - 1.U + + private def nextAddress(address: UInt, burst: AxiBurstType.Type, mask: UInt): UInt = { + val incremented = Wire(UInt(params.addrWidth.W)) + incremented := address + params.strobeWidth.U + Mux( + burst === AxiBurstType.BURST_FIXED, + address, + Mux(burst === AxiBurstType.BURST_WRAP, (address & ~mask) | (incremented & mask), incremented), + ) + } + + private def validateAddress(address: AxiWriteAddrChannel, accepted: Bool): Bool = { + val fixed = address.burst === AxiBurstType.BURST_FIXED + val incr = address.burst === AxiBurstType.BURST_INCR + val wrap = address.burst === AxiBurstType.BURST_WRAP + val legalLength = (!fixed || address.len < 16.U) && + (!wrap || Seq(1, 3, 7, 15).map(length => address.len === length.U).reduce(_ || _)) + val legalBurst = (fixed || incr || wrap) && legalLength + val legalSize = address.size.asUInt <= params.fullSize.U + + // Extend the calculation so an overflow cannot silently become a valid address. + val width = math.max(params.addrWidth + 1, 16) + val start = address.addr.pad(width) + val transferMask = ((1.U(8.W) << address.size.asUInt) - 1.U).pad(width) + val alignedStart = start & ~transferMask + val span = ((address.len +& 1.U) << address.size.asUInt).pad(width) + val boundary = alignedStart & ~(span - 1.U) + val lastByte = Mux(fixed, alignedStart + transferMask, Mux(wrap, boundary + span - 1.U, alignedStart + span - 1.U)) + val wrapAligned = !wrap || (start & transferMask) === 0.U + val withinPage = (start >> 12) === (lastByte >> 12) + val withinRange = lastByte < (BigInt(1) << params.addrWidth).U + val fullWidth = address.size.asUInt === params.fullSize.U + val wordAligned = (address.addr & (params.strobeWidth - 1).U) === 0.U + + when(accepted) { + assert(legalSize, "AXI transfer size exceeds the bus data width.") + assert(legalBurst, "AXI burst type or length is invalid.") + assert(wrapAligned, "AXI WRAP start address must be aligned to the transfer size.") + assert(withinPage, "AXI burst crosses a 4KB boundary.") + } + legalSize && legalBurst && wrapAligned && withinPage && withinRange && fullWidth && wordAligned + } +} diff --git a/chipmunk/test/src/acorn/AcornWidthAdapterSpec.scala b/chipmunk/test/src/acorn/AcornWidthAdapterSpec.scala new file mode 100644 index 0000000..1d9a869 --- /dev/null +++ b/chipmunk/test/src/acorn/AcornWidthAdapterSpec.scala @@ -0,0 +1,228 @@ +package chipmunk.test.acorn + +import scala.collection.mutable +import scala.util.Random + +import chisel3.* +import circt.stage.ChiselStage + +import chipmunk.acorn.* +import chipmunk.test.ChipmunkFlatSpec + +final class AcornWidthAdapterSpec extends ChipmunkFlatSpec { + private case class Request(address: BigInt, data: BigInt, strobe: BigInt, aligned: Boolean) + private case class Segment(address: BigInt, data: BigInt, strobe: BigInt, error: Boolean) + private case class Response(due: Int, data: BigInt, error: Boolean) + + private def memoryWord(address: BigInt, bytes: Int): BigInt = + (0 until bytes).foldLeft(BigInt(0))((value, lane) => value | (((address + lane) * 17 + 53) & 255) << (8 * lane)) + + private def runTraffic(inputWidth: Int, outputWidth: Int): Unit = { + val input = AcornParams(inputWidth, 12) + val output = AcornParams(outputWidth, 12) + val split = inputWidth > outputWidth + val count = if (split) inputWidth / outputWidth else 1 + val requests = (0 until 24).map { index => + val aligned = input.bytesPerWord == 1 || !Set(3, 7, 19)(index) + val address = + if (index == 23) BigInt(4096 - input.bytesPerWord) + else BigInt((index + 8) * input.bytesPerWord + (if (aligned) 0 else 1)) + val strobe = index % 3 match { + case 0 => BigInt(0) + case 1 => BigInt(1) << (index % input.bytesPerWord) + case 2 => (BigInt(1) << input.bytesPerWord) - 1 + } + Request(address, memoryWord(address + 123, input.bytesPerWord), strobe, aligned) + } + + def converted(request: Request, index: Int): Seq[Segment] = { + if (!request.aligned) Seq.empty + else { + val failure = + if (index % 4 != 1) -1 + else + (index / 4) % 3 match { + case 0 => 0 + case 1 => count / 2 + case 2 => count - 1 + } + (0 until count).take(if (failure < 0) count else failure + 1).map { segment => + val address = + if (split) request.address + segment * output.bytesPerWord + else request.address & ~(BigInt(output.bytesPerWord) - 1) + val byteOffset = if (split) segment * output.bytesPerWord else 0 + val laneOffset = if (inputWidth < outputWidth) (request.address - address).toInt else 0 + val dataMask = (BigInt(1) << outputWidth) - 1 + val strobeMask = (BigInt(1) << output.bytesPerWord) - 1 + Segment( + address, + ((request.data >> (8 * byteOffset)) << (8 * laneOffset)) & dataMask, + ((request.strobe >> byteOffset) << laneOffset) & strobeMask, + segment == failure, + ) + } + } + } + val readParts = requests.zipWithIndex.map((request, index) => converted(request, index)) + val writeParts = requests.zipWithIndex.map((request, index) => converted(request, index)) + val expectedReads = readParts.flatten + val expectedWrites = writeParts.flatten + + simulate(new AcornWidthAdapter(input, output)) { dut => + val random = new Random(20261006) + val readResponses, writeResponses = mutable.Queue.empty[Response] + var readSent, writeSent, readReceived, writeReceived, readDelivered, writeDelivered, cycle = 0 + var readWait, writeWait = 0 + var capturedRead, capturedWrite, readBeforeWrite = false + var heldReadCmd, heldWriteCmd, heldReadRsp, heldWriteRsp = Option.empty[Seq[BigInt]] + + def bool(value: Bool): Boolean = value.peek().litToBoolean + def checkHeld(previous: Option[Seq[BigInt]], valid: Bool, payload: Seq[BigInt]): Unit = + previous.foreach { expected => + assert(bool(valid), "VALID was withdrawn under backpressure") + assert(payload == expected, "Payload changed under backpressure") + } + + while (readDelivered < requests.size || writeDelivered < requests.size) { + assert(cycle < 8000, "Width conversion did not complete") + val readRequest = requests(math.min(readSent, requests.size - 1)) + val writeRequest = requests(math.min(writeSent, requests.size - 1)) + dut.io.sAcorn.rd.cmd.valid.poke((readSent < requests.size).B) + dut.io.sAcorn.rd.cmd.bits.addr.poke(readRequest.address.U) + dut.io.sAcorn.wr.cmd.valid.poke((writeSent < requests.size).B) + dut.io.sAcorn.wr.cmd.bits.addr.poke(writeRequest.address.U) + dut.io.sAcorn.wr.cmd.bits.data.poke(writeRequest.data.U) + dut.io.sAcorn.wr.cmd.bits.strobe.poke(writeRequest.strobe.U) + val readCmdReady = random.nextBoolean() + val writeCmdReady = cycle >= 40 && random.nextBoolean() + dut.io.mAcorn.rd.cmd.ready.poke(readCmdReady.B) + dut.io.mAcorn.wr.cmd.ready.poke(writeCmdReady.B) + val readResponseValid = readResponses.headOption.exists(_.due <= cycle) + val writeResponseValid = writeResponses.headOption.exists(_.due <= cycle) + dut.io.mAcorn.rd.rsp.valid.poke(readResponseValid.B) + dut.io.mAcorn.rd.rsp.bits.data.poke(readResponses.headOption.fold(BigInt(0))(_.data).U) + dut.io.mAcorn.rd.rsp.bits.error.poke(readResponses.headOption.exists(_.error).B) + dut.io.mAcorn.wr.rsp.valid.poke(writeResponseValid.B) + dut.io.mAcorn.wr.rsp.bits.error.poke(writeResponses.headOption.exists(_.error).B) + val readValid = bool(dut.io.sAcorn.rd.rsp.valid) + val writeValid = bool(dut.io.sAcorn.wr.rsp.valid) + if (readValid) readWait += 1 else readWait = 0 + if (writeValid) writeWait += 1 else writeWait = 0 + val readRspReady = cycle >= 80 && readWait >= 5 && random.nextBoolean() + val writeRspReady = cycle >= 130 && writeWait >= 5 && random.nextBoolean() + dut.io.sAcorn.rd.rsp.ready.poke(readRspReady.B) + dut.io.sAcorn.wr.rsp.ready.poke(writeRspReady.B) + + val readCmdPayload = Seq(dut.io.mAcorn.rd.cmd.bits.addr.peek().litValue) + val writeCmdPayload = Seq( + dut.io.mAcorn.wr.cmd.bits.addr.peek().litValue, + dut.io.mAcorn.wr.cmd.bits.data.peek().litValue, + dut.io.mAcorn.wr.cmd.bits.strobe.peek().litValue, + ) + val readRspPayload = + Seq(dut.io.sAcorn.rd.rsp.bits.data.peek().litValue, dut.io.sAcorn.rd.rsp.bits.error.peek().litValue) + val writeRspPayload = Seq(dut.io.sAcorn.wr.rsp.bits.error.peek().litValue) + checkHeld(heldReadCmd, dut.io.mAcorn.rd.cmd.valid, readCmdPayload) + checkHeld(heldWriteCmd, dut.io.mAcorn.wr.cmd.valid, writeCmdPayload) + checkHeld(heldReadRsp, dut.io.sAcorn.rd.rsp.valid, readRspPayload) + checkHeld(heldWriteRsp, dut.io.sAcorn.wr.rsp.valid, writeRspPayload) + + val readCmdFire = bool(dut.io.sAcorn.rd.cmd.valid) && bool(dut.io.sAcorn.rd.cmd.ready) + val writeCmdFire = bool(dut.io.sAcorn.wr.cmd.valid) && bool(dut.io.sAcorn.wr.cmd.ready) + val endpointRead = bool(dut.io.mAcorn.rd.cmd.valid) && readCmdReady + val endpointWrite = bool(dut.io.mAcorn.wr.cmd.valid) && writeCmdReady + val returnedRead = readResponseValid && bool(dut.io.mAcorn.rd.rsp.ready) + val returnedWrite = writeResponseValid && bool(dut.io.mAcorn.wr.rsp.ready) + + if (endpointRead) { + assert(readReceived < expectedReads.size, "Extra read or a read after an earlier error") + dut.io.mAcorn.rd.cmd.bits.addr.expect(expectedReads(readReceived).address.U) + } + if (endpointWrite) { + assert(writeReceived < expectedWrites.size, "Extra write or a write after an earlier error") + val expected = expectedWrites(writeReceived) + dut.io.mAcorn.wr.cmd.bits.addr.expect(expected.address.U) + dut.io.mAcorn.wr.cmd.bits.data.expect(expected.data.U) + dut.io.mAcorn.wr.cmd.bits.strobe.expect(expected.strobe.U) + } + if (readValid) { + val error = readParts(readDelivered).isEmpty || readParts(readDelivered).exists(_.error) + dut.io.sAcorn.rd.rsp.bits.error.expect(error.B) + dut.io.sAcorn.rd.rsp.bits.data + .expect((if (error) BigInt(0) else memoryWord(requests(readDelivered).address, input.bytesPerWord)).U) + dut.io.sAcorn.rd.cmd.ready.expect(false.B) + } + if (writeValid) { + val error = writeParts(writeDelivered).isEmpty || writeParts(writeDelivered).exists(_.error) + dut.io.sAcorn.wr.rsp.bits.error.expect(error.B) + dut.io.sAcorn.wr.cmd.ready.expect(false.B) + } + heldReadCmd = Option.when(bool(dut.io.mAcorn.rd.cmd.valid) && !readCmdReady)(readCmdPayload) + heldWriteCmd = Option.when(bool(dut.io.mAcorn.wr.cmd.valid) && !writeCmdReady)(writeCmdPayload) + heldReadRsp = Option.when(readValid && !readRspReady)(readRspPayload) + heldWriteRsp = Option.when(writeValid && !writeRspReady)(writeRspPayload) + dut.clock.step() + + if (readCmdFire) readSent += 1 + if (writeCmdFire) writeSent += 1 + if (returnedRead) { + readResponses.dequeue() + capturedRead ||= !readRspReady + } + if (returnedWrite) { + writeResponses.dequeue() + capturedWrite ||= !writeRspReady + } + if (endpointRead) { + val expected = expectedReads(readReceived) + readResponses.enqueue( + Response(cycle + 2 + random.nextInt(4), memoryWord(expected.address, output.bytesPerWord), expected.error) + ) + readReceived += 1 + } + if (endpointWrite) { + writeResponses.enqueue(Response(cycle + 2 + random.nextInt(4), 0, expectedWrites(writeReceived).error)) + writeReceived += 1 + } + if (readValid && readRspReady) { + readDelivered += 1 + readBeforeWrite ||= writeDelivered == 0 + } + if (writeValid && writeRspReady) writeDelivered += 1 + assert(readSent - readDelivered >= 0 && readSent - readDelivered <= 1) + assert(writeSent - writeDelivered >= 0 && writeSent - writeDelivered <= 1) + assert(readResponses.size <= 1 && writeResponses.size <= 1) + cycle += 1 + } + assert(readReceived == expectedReads.size && writeReceived == expectedWrites.size) + assert(readResponses.isEmpty && writeResponses.isEmpty) + assert(expectedReads.isEmpty || capturedRead, "Read responses did not enter the local buffer under backpressure") + assert( + expectedWrites.isEmpty || capturedWrite, + "Write responses did not enter the local buffer under backpressure", + ) + assert(readBeforeWrite, "Read progress was coupled to the stalled write") + // Neither a late retry nor a speculative extra subword may appear after completion. + dut.io.sAcorn.rd.cmd.valid.poke(false.B) + dut.io.sAcorn.wr.cmd.valid.poke(false.B) + dut.clock.step(3) + dut.io.mAcorn.rd.cmd.valid.expect(false.B) + dut.io.mAcorn.wr.cmd.valid.expect(false.B) + } + } + + for ((input, output) <- Seq((8, 64), (32, 64), (64, 32), (256, 32), (64, 8), (32, 32))) { + s"AcornWidthAdapter($input, $output)" should "convert byte lanes and stop on errors through independent stalls" in { + runTraffic(input, output) + } + } + + "AcornWidthAdapter" should "validate address geometry" in { + intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new AcornWidthAdapter(AcornParams(32, 16), AcornParams(64, 15))) + } + intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new AcornWidthAdapter(AcornParams(32, 2), AcornParams(64, 2))) + } + } +} diff --git a/chipmunk/test/src/amba/AcornToAxiBridgesSpec.scala b/chipmunk/test/src/amba/AcornToAxiBridgesSpec.scala new file mode 100644 index 0000000..774bef2 --- /dev/null +++ b/chipmunk/test/src/amba/AcornToAxiBridgesSpec.scala @@ -0,0 +1,273 @@ +package chipmunk.test.amba + +import scala.collection.mutable +import scala.util.Random + +import chisel3.* +import circt.stage.ChiselStage + +import chipmunk.* +import chipmunk.acorn.{AcornIO, AcornParams} +import chipmunk.amba.* +import chipmunk.test.ChipmunkFlatSpec + +private object AcornToAxiBridgesSpecDut { + final class Harness(params: AxiParams, lite: Boolean, writeId: BigInt, readId: BigInt, writeProt: Int, readProt: Int) + extends Module { + val io = IO(new Bundle { + val sAcorn = Slave(new AcornIO(AcornParams(params.dataWidth, params.addrWidth))) + val mAxi = Master(new AxiIO(params)) + }) + if (lite) { + val bridge = Module( + new AcornToAxiLiteBridge(AxiLiteParams(params.dataWidth, params.addrWidth), writeProt, readProt) + ) + val adapter = Module(new AxiLiteToAxiBridge(params, writeId, readId)) + bridge.io.sAcorn :<>= io.sAcorn + adapter.io.sAxiLite :<>= bridge.io.mAxiLite + io.mAxi :<>= adapter.io.mAxi + } else { + val bridge = Module(new AcornToAxiBridge(params, writeId, readId, writeProt, readProt)) + bridge.io.sAcorn :<>= io.sAcorn + io.mAxi :<>= bridge.io.mAxi + } + } +} + +final class AcornToAxiBridgesSpec extends ChipmunkFlatSpec { + private case class Request(address: BigInt, data: BigInt, strobe: BigInt, response: AxiResp.Type, aligned: Boolean) + private case class ReadResponse(due: Int, data: BigInt, response: AxiResp.Type) + private case class WriteResponse(due: Int, response: AxiResp.Type) + private val responses = Seq(AxiResp.RESP_OKAY, AxiResp.RESP_SLVERR, AxiResp.RESP_DECERR) + + private def runTraffic(params: AxiParams, lite: Boolean, wFirst: Boolean): Unit = { + val writeId = if (params.hasId) (BigInt(1) << (params.idWidth - 1)) + 7 else BigInt(0) + val readId = if (params.hasId) BigInt(5) else BigInt(0) + val writeProt = if (params.hasId) 5 else 0 + val readProt = if (params.hasId) 7 else 0 + val requests = (0 until 24).map { index => + val aligned = params.strobeWidth == 1 || !Set(5, 12, 21)(index) + val address = BigInt((index + 8) * params.strobeWidth + (if (aligned) 0 else 1)) + val mask = (BigInt(1) << params.dataWidth) - 1 + val data = ((BigInt(1) << (params.dataWidth - 1)) | (BigInt(index) * 23 + 3)) & mask + val strobe = index % 3 match { + case 0 => BigInt(0) + case 1 => BigInt(1) << (index % params.strobeWidth) + case 2 => (BigInt(1) << params.strobeWidth) - 1 + } + Request(address, data, strobe, responses(index % responses.size), aligned) + } + val forwarded = requests.filter(_.aligned) + + simulate(new AcornToAxiBridgesSpecDut.Harness(params, lite, writeId, readId, writeProt, readProt)) { dut => + val random = new Random(20261006) + val readResponses = mutable.Queue.empty[ReadResponse] + val writeResponses = mutable.Queue.empty[WriteResponse] + var readSent, writeSent, awReceived, wReceived, arReceived, bScheduled, bReturned, rReturned = 0 + var readDelivered, writeDelivered, cycle, readWait, writeWait = 0 + var capturedReadUnderStall, capturedWriteUnderStall, readCompletedBeforeWrite = false + var heldAw, heldW, heldAr, heldRead, heldWrite = Option.empty[Seq[BigInt]] + + def bool(value: Bool): Boolean = value.peek().litToBoolean + def held(previous: Option[Seq[BigInt]], valid: Bool, payload: Seq[BigInt]): Unit = + previous.foreach { expected => + assert(bool(valid), "VALID was withdrawn under backpressure") + assert(payload == expected, "Payload changed under backpressure") + } + def sampleAddress(bits: AxiWriteAddrChannel): Seq[BigInt] = + Seq(bits.addr, bits.prot, bits.size, bits.len, bits.burst, bits.lock, bits.cache).map(_.peek().litValue) ++ + bits.id.toSeq.map(_.peek().litValue) ++ bits.qos.toSeq.map(_.peek().litValue) ++ + bits.region.toSeq.map(_.peek().litValue) + def checkAddress(bits: AxiWriteAddrChannel, request: Request, id: BigInt, prot: Int): Unit = { + bits.addr.expect(request.address.U) + bits.prot.expect(prot.U) + bits.len.expect(0.U) + assert(bits.size.peek().litValue == params.fullSize) + bits.burst.expect(AxiBurstType.BURST_INCR) + bits.lock.expect(0.U) + bits.cache.expect(0.U) + bits.id.foreach(_.expect(id.U)) + bits.qos.foreach(_.expect(0.U)) + bits.region.foreach(_.expect(0.U)) + } + + while (readDelivered < requests.size || writeDelivered < requests.size) { + assert(cycle < 5000, "Traffic did not complete") + val readRequest = requests(math.min(readSent, requests.size - 1)) + val writeRequest = requests(math.min(writeSent, requests.size - 1)) + // Once accepted, the producer offers a different payload while the bridge is still busy. + dut.io.sAcorn.rd.cmd.valid.poke((readSent < requests.size).B) + dut.io.sAcorn.rd.cmd.bits.addr.poke(readRequest.address.U) + dut.io.sAcorn.wr.cmd.valid.poke((writeSent < requests.size).B) + dut.io.sAcorn.wr.cmd.bits.addr.poke(writeRequest.address.U) + dut.io.sAcorn.wr.cmd.bits.data.poke(writeRequest.data.U) + dut.io.sAcorn.wr.cmd.bits.strobe.poke(writeRequest.strobe.U) + + val awReady = (!wFirst || cycle >= 35) && random.nextBoolean() + val wReady = (wFirst || cycle >= 35) && random.nextBoolean() + val arReady = random.nextBoolean() + dut.io.mAxi.aw.ready.poke(awReady.B) + dut.io.mAxi.w.ready.poke(wReady.B) + dut.io.mAxi.ar.ready.poke(arReady.B) + val bValid = writeResponses.headOption.exists(_.due <= cycle) + val rValid = readResponses.headOption.exists(_.due <= cycle) + dut.io.mAxi.b.valid.poke(bValid.B) + dut.io.mAxi.b.bits.resp.poke(writeResponses.headOption.fold(AxiResp.RESP_OKAY)(_.response)) + dut.io.mAxi.b.bits.id.foreach(_.poke(writeId.U)) + dut.io.mAxi.r.valid.poke(rValid.B) + dut.io.mAxi.r.bits.data.poke(readResponses.headOption.fold(BigInt(0))(_.data).U) + dut.io.mAxi.r.bits.resp.poke(readResponses.headOption.fold(AxiResp.RESP_OKAY)(_.response)) + dut.io.mAxi.r.bits.id.foreach(_.poke(readId.U)) + dut.io.mAxi.r.bits.last.poke(true.B) + + val readValid = bool(dut.io.sAcorn.rd.rsp.valid) + val writeValid = bool(dut.io.sAcorn.wr.rsp.valid) + if (readValid) readWait += 1 else readWait = 0 + if (writeValid) writeWait += 1 else writeWait = 0 + // Responses must become valid and be captured even when Acorn initially holds READY low. + val readReady = cycle >= 80 && readWait >= 5 && random.nextBoolean() + val writeReady = cycle >= 100 && writeWait >= 5 && random.nextBoolean() + dut.io.sAcorn.rd.rsp.ready.poke(readReady.B) + dut.io.sAcorn.wr.rsp.ready.poke(writeReady.B) + + val awPayload = sampleAddress(dut.io.mAxi.aw.bits) + val arPayload = sampleAddress(dut.io.mAxi.ar.bits) + val wPayload = Seq( + dut.io.mAxi.w.bits.data.peek().litValue, + dut.io.mAxi.w.bits.strb.peek().litValue, + dut.io.mAxi.w.bits.last.peek().litValue, + ) + val readPayload = + Seq(dut.io.sAcorn.rd.rsp.bits.data.peek().litValue, dut.io.sAcorn.rd.rsp.bits.error.peek().litValue) + val writePayload = Seq(dut.io.sAcorn.wr.rsp.bits.error.peek().litValue) + held(heldAw, dut.io.mAxi.aw.valid, awPayload) + held(heldAr, dut.io.mAxi.ar.valid, arPayload) + held(heldW, dut.io.mAxi.w.valid, wPayload) + held(heldRead, dut.io.sAcorn.rd.rsp.valid, readPayload) + held(heldWrite, dut.io.sAcorn.wr.rsp.valid, writePayload) + + val readCmdFire = bool(dut.io.sAcorn.rd.cmd.valid) && bool(dut.io.sAcorn.rd.cmd.ready) + val writeCmdFire = bool(dut.io.sAcorn.wr.cmd.valid) && bool(dut.io.sAcorn.wr.cmd.ready) + val awFire = bool(dut.io.mAxi.aw.valid) && awReady + val wFire = bool(dut.io.mAxi.w.valid) && wReady + val arFire = bool(dut.io.mAxi.ar.valid) && arReady + val bFire = bValid && bool(dut.io.mAxi.b.ready) + val rFire = rValid && bool(dut.io.mAxi.r.ready) + val readRspFire = readValid && readReady + val writeRspFire = writeValid && writeReady + + if (awFire) { + assert(awReceived < forwarded.size, "An extra AXI AW was sent") + checkAddress(dut.io.mAxi.aw.bits, forwarded(awReceived), writeId, writeProt) + } + if (wFire) { + assert(wReceived < forwarded.size, "An extra AXI W was sent") + dut.io.mAxi.w.bits.data.expect(forwarded(wReceived).data.U) + dut.io.mAxi.w.bits.strb.expect(forwarded(wReceived).strobe.U) + dut.io.mAxi.w.bits.last.expect(true.B) + } + if (arFire) { + assert(arReceived < forwarded.size, "An extra AXI AR was sent") + checkAddress(dut.io.mAxi.ar.bits, forwarded(arReceived), readId, readProt) + } + if (readValid) { + val expected = requests(readDelivered) + dut.io.sAcorn.rd.rsp.bits.data.expect((if (expected.aligned) expected.data else BigInt(0)).U) + dut.io.sAcorn.rd.rsp.bits.error.expect((!expected.aligned || expected.response != AxiResp.RESP_OKAY).B) + dut.io.sAcorn.rd.cmd.ready.expect(false.B) + } + if (writeValid) { + val expected = requests(writeDelivered) + dut.io.sAcorn.wr.rsp.bits.error.expect((!expected.aligned || expected.response != AxiResp.RESP_OKAY).B) + dut.io.sAcorn.wr.cmd.ready.expect(false.B) + } + + heldAw = Option.when(bool(dut.io.mAxi.aw.valid) && !awReady)(awPayload) + heldW = Option.when(bool(dut.io.mAxi.w.valid) && !wReady)(wPayload) + heldAr = Option.when(bool(dut.io.mAxi.ar.valid) && !arReady)(arPayload) + heldRead = Option.when(readValid && !readReady)(readPayload) + heldWrite = Option.when(writeValid && !writeReady)(writePayload) + dut.clock.step() + + if (readCmdFire) readSent += 1 + if (writeCmdFire) writeSent += 1 + if (awFire) awReceived += 1 + if (wFire) wReceived += 1 + if (arFire) { + val expected = forwarded(arReceived) + readResponses.enqueue(ReadResponse(cycle + 3 + random.nextInt(4), expected.data, expected.response)) + arReceived += 1 + } + if (rFire) { + readResponses.dequeue() + rReturned += 1 + capturedReadUnderStall ||= !readReady + } + if (bFire) { + writeResponses.dequeue() + bReturned += 1 + capturedWriteUnderStall ||= !writeReady + } + while (bScheduled < math.min(awReceived, wReceived)) { + writeResponses.enqueue(WriteResponse(cycle + 3 + random.nextInt(4), forwarded(bScheduled).response)) + bScheduled += 1 + } + if (readRspFire) { + readDelivered += 1 + readCompletedBeforeWrite ||= writeDelivered == 0 + } + if (writeRspFire) writeDelivered += 1 + assert(readSent - readDelivered >= 0 && readSent - readDelivered <= 1) + assert(writeSent - writeDelivered >= 0 && writeSent - writeDelivered <= 1) + cycle += 1 + } + assert(readSent == requests.size && writeSent == requests.size) + assert(awReceived == forwarded.size && wReceived == forwarded.size && arReceived == forwarded.size) + assert(bReturned == forwarded.size && rReturned == forwarded.size) + assert(readResponses.isEmpty && writeResponses.isEmpty) + assert(capturedReadUnderStall && capturedWriteUnderStall, "AXI responses did not enter the local buffers") + assert(readCompletedBeforeWrite, "Read progress was coupled to the stalled write") + } + } + + for ( + lite <- Seq(false, true); + params <- Seq(AxiParams(32, 16), AxiParams(64, 16, idWidth = 40, hasQos = true, hasRegion = true)); + wFirst <- Seq(true, false) + ) { + s"AcornTo${if (lite) "AxiLite" else "Axi"}Bridge(${params.dataWidth}, W first = $wFirst)" should + "preserve transactions through independent stalls and local alignment errors" in { + runTraffic(params, lite, wFirst) + } + } + + for (width <- Seq(8, 128, 1024)) { + s"AcornToAxiBridge($width)" should "support AXI data widths beyond AXI-Lite" in { + runTraffic(AxiParams(width, 16), lite = false, wFirst = true) + } + } + + "AcornToAxiBridge" should "validate ID widths, PROT values, and address geometry" in { + for ( + (params, writeId, readId, writeProt, readProt) <- Seq( + (AxiParams(32, 16), BigInt(1), BigInt(0), 0, 0), + (AxiParams(32, 16), BigInt(0), BigInt(1), 0, 0), + (AxiParams(32, 16, idWidth = 4), BigInt(-1), BigInt(0), 0, 0), + (AxiParams(32, 16, idWidth = 4), BigInt(0), BigInt(16), 0, 0), + (AxiParams(32, 16), BigInt(0), BigInt(0), 8, 0), + (AxiParams(32, 16), BigInt(0), BigInt(0), 0, -1), + (AxiParams(64, 2), BigInt(0), BigInt(0), 0, 0), + ) + ) { + intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new AcornToAxiBridge(params, writeId, readId, writeProt, readProt)) + } + } + } + + "AcornToAxiLiteBridge" should "validate its static PROT settings through the shared controller" in { + intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new AcornToAxiLiteBridge(AxiLiteParams(32, 16), readProt = 8)) + } + } +} diff --git a/chipmunk/test/src/amba/AxiToAcornBridgeSpec.scala b/chipmunk/test/src/amba/AxiToAcornBridgeSpec.scala new file mode 100644 index 0000000..bd03bec --- /dev/null +++ b/chipmunk/test/src/amba/AxiToAcornBridgeSpec.scala @@ -0,0 +1,390 @@ +package chipmunk.test.amba + +import scala.collection.mutable +import scala.util.Random + +import chisel3.* +import circt.stage.ChiselStage + +import chipmunk.* +import chipmunk.acorn.AcornOutstanding +import chipmunk.amba.* +import chipmunk.test.ChipmunkFlatSpec + +private object AxiToAcornBridgeSpecDut { + final class ImmediateResponse extends Module { + val params = AxiParams(32, 16, idWidth = 4) + val io = IO(new Bundle { + val sAxi = Slave(new AxiIO(params)) + }) + private val bridge = Module(new AxiToAcornBridge(params, AcornOutstanding(1, 1))) + bridge.io.sAxi :<>= io.sAxi + bridge.io.mAcorn.rd.cmd.ready := bridge.io.mAcorn.rd.rsp.ready + bridge.io.mAcorn.rd.rsp.valid := bridge.io.mAcorn.rd.cmd.valid + bridge.io.mAcorn.rd.rsp.bits.data := bridge.io.mAcorn.rd.cmd.bits.addr + bridge.io.mAcorn.rd.rsp.bits.error := false.B + bridge.io.mAcorn.wr.cmd.ready := bridge.io.mAcorn.wr.rsp.ready + bridge.io.mAcorn.wr.rsp.valid := bridge.io.mAcorn.wr.cmd.valid + bridge.io.mAcorn.wr.rsp.bits.error := bridge.io.mAcorn.wr.cmd.bits.data(0) + } +} + +final class AxiToAcornBridgeSpec extends ChipmunkFlatSpec { + private case class Burst( + address: BigInt, + beats: Int, + burst: AxiBurstType.Type, + id: BigInt, + size: Int, + supported: Boolean = true, + errors: Set[Int] = Set.empty, + exclusive: Boolean = false, + ) + private case class ReadBeat( + address: BigInt, + data: BigInt, + error: Boolean, + last: Boolean, + id: BigInt, + supported: Boolean, + ) + private case class WriteBeat( + address: BigInt, + data: BigInt, + strobe: BigInt, + last: Boolean, + supported: Boolean, + error: Boolean, + ) + private case class ReadResponse(due: Int, data: BigInt, error: Boolean) + private case class WriteResponse(due: Int, error: Boolean) + + private def addresses(burst: Burst, bytes: Int): Seq[BigInt] = { + val span = BigInt(bytes) * burst.beats + val base = (burst.address / span) * span + (0 until burst.beats).map { beat => + burst.burst.litValue.toInt match { + case 0 => burst.address + case 1 => burst.address + beat * bytes + case 2 => base + (burst.address - base + beat * bytes) % span + case other => throw new IllegalArgumentException(s"Unexpected AXI burst encoding $other") + } + } + } + + private def traffic(params: AxiParams, smallRange: Boolean): Seq[Burst] = { + def id(index: Int): BigInt = + if (!params.hasId) BigInt(0) + else ((BigInt(1) << (params.idWidth - 1)) + index) & ((BigInt(1) << params.idWidth) - 1) + val bytes = params.strobeWidth + val full = params.fullSize + if (smallRange) { + Seq( + Burst(24, 1, AxiBurstType.BURST_INCR, id(0), full), + Burst(24, 2, AxiBurstType.BURST_INCR, id(1), full, supported = false), + Burst(0, 4, AxiBurstType.BURST_WRAP, id(2), full), + ) + } else { + val bursts = Seq( + Burst(0, 16, AxiBurstType.BURST_INCR, id(0), full, errors = Set(4)), + Burst(0x1000, 16, AxiBurstType.BURST_FIXED, id(1), full), + Burst(0x2000, math.min(256, 4096 / bytes), AxiBurstType.BURST_INCR, id(2), full, errors = Set(0)), + Burst(0x3000, 4, AxiBurstType.BURST_INCR, id(3), full, errors = Set(3)), + Burst(0x3400, 1, AxiBurstType.BURST_INCR, id(4), full, exclusive = true), + ) ++ Seq(2, 4, 8, 16).zipWithIndex.map { case (beats, index) => + // Start at the last word of a 4KB page: WRAP must return to its own window. + Burst(0x4000 - bytes, beats, AxiBurstType.BURST_WRAP, id(index + 5), full) + } + if (full == 0) bursts + else + bursts ++ Seq( + Burst(0x5000, 5, AxiBurstType.BURST_INCR, id(9), full - 1, supported = false), + Burst(0x5101, 3, AxiBurstType.BURST_INCR, id(10), full, supported = false), + Burst(0x5200, 1, AxiBurstType.BURST_INCR, id(11), full), + ) + } + } + + private def runTraffic(params: AxiParams, capacity: AcornOutstanding, wFirst: Boolean, smallRange: Boolean): Unit = { + val bursts = traffic(params, smallRange) + val dataMask = (BigInt(1) << params.dataWidth) - 1 + def data(index: Int): BigInt = ((BigInt(1) << (params.dataWidth - 1)) | (BigInt(index) * 37 + 11)) & dataMask + var readCommandIndex = 0 + val expectedReads = bursts.flatMap { burst => + addresses(burst, params.strobeWidth).zipWithIndex.map { case (address, beat) => + val value = if (burst.supported) { + val value = data(readCommandIndex) + readCommandIndex += 1 + value + } else BigInt(0) + ReadBeat( + address, + value, + !burst.supported || burst.errors(beat), + beat == burst.beats - 1, + burst.id, + burst.supported, + ) + } + } + val readCommands = expectedReads.filter(_.supported) + val expectedWrites = bursts + .flatMap { burst => + addresses(burst, params.strobeWidth).zipWithIndex.map { case (address, beat) => + (burst, address, beat) + } + } + .zipWithIndex + .map { case ((burst, address, beat), index) => + val strobe = index % 3 match { + case 0 => BigInt(0) + case 1 => BigInt(1) << (index % params.strobeWidth) + case 2 => (BigInt(1) << params.strobeWidth) - 1 + } + WriteBeat(address, data(index), strobe, beat == burst.beats - 1, burst.supported, burst.errors(beat)) + } + val writeCommands = expectedWrites.filter(_.supported) + val writeEnds = bursts.scanLeft(0)((total, burst) => total + (if (burst.supported) burst.beats else 0)).tail + + simulate(new AxiToAcornBridge(params, capacity)) { dut => + val random = new Random(20261006) + val reads = mutable.Queue.empty[ReadResponse] + val writes = mutable.Queue.empty[WriteResponse] + var awSent, arSent, wSent, readIssued, writeIssued, readReturned, writeReturned, rDelivered, bDelivered = 0 + var readReserved, writePending, peakReads = 0 + var cycle = 0 + var bWait = 0 + var heldRead, heldWrite, heldR, heldB = Option.empty[Seq[BigInt]] + + def bool(value: Bool): Boolean = value.peek().litToBoolean + def held(previous: Option[Seq[BigInt]], valid: Bool, payload: Seq[BigInt]): Unit = + previous.foreach { expected => + assert(bool(valid), "VALID was withdrawn under backpressure") + assert(payload == expected, "Payload changed under backpressure") + } + def driveAddress(channel: AxiWriteAddrChannel, burst: Burst): Unit = { + channel.addr.poke(burst.address.U) + channel.len.poke((burst.beats - 1).U) + channel.size.poke(AxiBurstSize.all(burst.size)) + channel.burst.poke(burst.burst) + channel.id.foreach(_.poke(burst.id.U)) + channel.lock.poke((if (burst.exclusive) 1 else 0).U) + channel.prot.poke(7.U) + channel.cache.poke(3.U) + channel.qos.foreach(_.poke(9.U)) + channel.region.foreach(_.poke(5.U)) + } + + while (rDelivered < expectedReads.size || bDelivered < bursts.size) { + assert(cycle < 15000, "Transactions did not complete") + dut.io.sAxi.ar.valid.poke((arSent < bursts.size).B) + driveAddress(dut.io.sAxi.ar.bits, bursts(math.min(arSent, bursts.size - 1))) + dut.io.sAxi.aw.valid.poke((awSent < bursts.size && (!wFirst || cycle >= 8)).B) + driveAddress(dut.io.sAxi.aw.bits, bursts(math.min(awSent, bursts.size - 1))) + dut.io.sAxi.w.valid.poke((wSent < expectedWrites.size && (wFirst || cycle >= 8)).B) + val offeredWrite = expectedWrites(math.min(wSent, expectedWrites.size - 1)) + dut.io.sAxi.w.bits.data.poke(offeredWrite.data.U) + dut.io.sAxi.w.bits.strb.poke(offeredWrite.strobe.U) + dut.io.sAxi.w.bits.last.poke(offeredWrite.last.B) + + val readReady = random.nextBoolean() + val writeReady = random.nextBoolean() + dut.io.mAcorn.rd.cmd.ready.poke(readReady.B) + dut.io.mAcorn.wr.cmd.ready.poke(writeReady.B) + val readValid = reads.headOption.exists(_.due <= cycle) + val writeValid = writes.headOption.exists(_.due <= cycle) + dut.io.mAcorn.rd.rsp.valid.poke(readValid.B) + dut.io.mAcorn.rd.rsp.bits.data.poke(reads.headOption.fold(BigInt(0))(_.data).U) + dut.io.mAcorn.rd.rsp.bits.error.poke(reads.headOption.exists(_.error).B) + dut.io.mAcorn.wr.rsp.valid.poke(writeValid.B) + dut.io.mAcorn.wr.rsp.bits.error.poke(writes.headOption.exists(_.error).B) + + val rReady = cycle >= 180 && random.nextBoolean() + dut.io.sAxi.r.ready.poke(rReady.B) + // Model a master that waits for BVALID before ever asserting BREADY. + val bValid = bool(dut.io.sAxi.b.valid) + if (bValid) bWait += 1 else bWait = 0 + val bReady = bWait >= 12 && random.nextBoolean() + dut.io.sAxi.b.ready.poke(bReady.B) + + val readPayload = Seq(dut.io.mAcorn.rd.cmd.bits.addr.peek().litValue) + val writePayload = Seq( + dut.io.mAcorn.wr.cmd.bits.addr.peek().litValue, + dut.io.mAcorn.wr.cmd.bits.data.peek().litValue, + dut.io.mAcorn.wr.cmd.bits.strobe.peek().litValue, + ) + val rPayload = Seq( + dut.io.sAxi.r.bits.data.peek().litValue, + dut.io.sAxi.r.bits.resp.peek().litValue, + dut.io.sAxi.r.bits.last.peek().litValue, + ) ++ dut.io.sAxi.r.bits.id.toSeq.map(_.peek().litValue) + val bPayload = + Seq(dut.io.sAxi.b.bits.resp.peek().litValue) ++ dut.io.sAxi.b.bits.id.toSeq.map(_.peek().litValue) + held(heldRead, dut.io.mAcorn.rd.cmd.valid, readPayload) + held(heldWrite, dut.io.mAcorn.wr.cmd.valid, writePayload) + held(heldR, dut.io.sAxi.r.valid, rPayload) + held(heldB, dut.io.sAxi.b.valid, bPayload) + + val awFire = bool(dut.io.sAxi.aw.valid) && bool(dut.io.sAxi.aw.ready) + val arFire = bool(dut.io.sAxi.ar.valid) && bool(dut.io.sAxi.ar.ready) + val wFire = bool(dut.io.sAxi.w.valid) && bool(dut.io.sAxi.w.ready) + val readFire = bool(dut.io.mAcorn.rd.cmd.valid) && readReady + val writeFire = bool(dut.io.mAcorn.wr.cmd.valid) && writeReady + val readResponseFire = readValid && bool(dut.io.mAcorn.rd.rsp.ready) + val writeResponseFire = writeValid && bool(dut.io.mAcorn.wr.rsp.ready) + val rFire = bool(dut.io.sAxi.r.valid) && rReady + val bFire = bValid && bReady + + if (readFire) { + assert(readIssued < readCommands.size, "An extra Acorn read was issued") + dut.io.mAcorn.rd.cmd.bits.addr.expect(readCommands(readIssued).address.U) + } + if (writeFire) { + assert(writeIssued < writeCommands.size, "An extra Acorn write was issued") + val expected = writeCommands(writeIssued) + dut.io.mAcorn.wr.cmd.bits.addr.expect(expected.address.U) + dut.io.mAcorn.wr.cmd.bits.data.expect(expected.data.U) + dut.io.mAcorn.wr.cmd.bits.strobe.expect(expected.strobe.U) + } + if (rFire) { + val expected = expectedReads(rDelivered) + dut.io.sAxi.r.bits.data.expect(expected.data.U) + dut.io.sAxi.r.bits.resp.expect(if (expected.error) AxiResp.RESP_SLVERR else AxiResp.RESP_OKAY) + dut.io.sAxi.r.bits.last.expect(expected.last.B) + dut.io.sAxi.r.bits.id.foreach(_.expect(expected.id.U)) + } + if (bValid) { + assert(writeReturned >= writeEnds(bDelivered), "BVALID preceded the final Acorn write response") + val burst = bursts(bDelivered) + dut.io.sAxi.b.bits.resp + .expect(if (!burst.supported || burst.errors.nonEmpty) AxiResp.RESP_SLVERR else AxiResp.RESP_OKAY) + dut.io.sAxi.b.bits.id.foreach(_.expect(burst.id.U)) + } + + heldRead = Option.when(bool(dut.io.mAcorn.rd.cmd.valid) && !readReady)(readPayload) + heldWrite = Option.when(bool(dut.io.mAcorn.wr.cmd.valid) && !writeReady)(writePayload) + heldR = Option.when(bool(dut.io.sAxi.r.valid) && !rReady)(rPayload) + heldB = Option.when(bValid && !bReady)(bPayload) + dut.clock.step() + + if (awFire) awSent += 1 + if (arFire) arSent += 1 + if (wFire) wSent += 1 + if (readResponseFire) { reads.dequeue(); readReturned += 1 } + if (writeResponseFire) { writes.dequeue(); writeReturned += 1; writePending -= 1 } + if (readFire) { + val expected = readCommands(readIssued) + reads.enqueue(ReadResponse(cycle + 7 + random.nextInt(9), expected.data, expected.error)) + readIssued += 1 + readReserved += 1 + } + if (writeFire) { + writes.enqueue(WriteResponse(cycle + 7 + random.nextInt(9), writeCommands(writeIssued).error)) + writeIssued += 1 + writePending += 1 + } + if (rFire) { + if (expectedReads(rDelivered).supported) readReserved -= 1 + rDelivered += 1 + } + if (bFire) bDelivered += 1 + assert(readReserved >= 0 && readReserved <= capacity.read, "Read response reservations exceeded capacity") + assert(writePending >= 0 && writePending <= capacity.write, "Pending writes exceeded capacity") + peakReads = math.max(peakReads, readReserved) + cycle += 1 + } + assert(arSent == bursts.size && awSent == bursts.size && wSent == expectedWrites.size) + assert(readIssued == readCommands.size && readReturned == readCommands.size) + assert(writeIssued == writeCommands.size && writeReturned == writeCommands.size) + assert(reads.isEmpty && writes.isEmpty && readReserved == 0 && writePending == 0) + if (!smallRange) assert(peakReads == capacity.read, "Read credits were not fully exercised") + } + } + + for ( + (params, capacity) <- Seq( + (AxiParams(32, 16), AcornOutstanding(1, 1)), + (AxiParams(64, 16, idWidth = 40, hasQos = true, hasRegion = true), AcornOutstanding(3, 5)), + (AxiParams(128, 16, idWidth = 4), AcornOutstanding(4, 4)), + ); + wFirst <- Seq(true, false) + ) { + s"AxiToAcornBridge(${params.dataWidth}, W first = $wFirst)" should + "split bursts and preserve responses under independent stalls" in { + runTraffic(params, capacity, wFirst, smallRange = false) + } + } + + for (width <- Seq(8, 1024)) { + s"AxiToAcornBridge($width)" should "support the AXI data-width limits" in { + runTraffic(AxiParams(width, 16), AcornOutstanding(), wFirst = true, smallRange = false) + } + } + + "AxiToAcornBridge" should "reject address overflow without issuing truncated Acorn commands" in { + runTraffic(AxiParams(64, 5, idWidth = 4), AcornOutstanding(3, 2), wFirst = false, smallRange = true) + } + + it should "accept Acorn responses in the same cycle as commands" in { + simulate(new AxiToAcornBridgeSpecDut.ImmediateResponse) { dut => + def address(channel: AxiWriteAddrChannel, id: Int): Unit = { + channel.addr.poke(0x20.U) + channel.len.poke(3.U) + channel.size.poke(AxiBurstSize.SIZE4) + channel.burst.poke(AxiBurstType.BURST_INCR) + channel.id.foreach(_.poke(id.U)) + channel.lock.poke(0.U) + channel.prot.poke(0.U) + channel.cache.poke(0.U) + } + dut.io.sAxi.aw.valid.poke(true.B) + dut.io.sAxi.ar.valid.poke(true.B) + address(dut.io.sAxi.aw.bits, 2) + address(dut.io.sAxi.ar.bits, 3) + dut.io.sAxi.w.valid.poke(false.B) + dut.io.sAxi.w.bits.data.poke(0.U) + dut.io.sAxi.w.bits.strb.poke(15.U) + dut.io.sAxi.w.bits.last.poke(false.B) + dut.io.sAxi.r.ready.poke(false.B) + dut.io.sAxi.b.ready.poke(false.B) + dut.clock.step() + dut.io.sAxi.aw.valid.poke(false.B) + dut.io.sAxi.ar.valid.poke(false.B) + var written, read = 0 + var complete = false + var bReturned = false + for (cycle <- 0 until 60 if !complete) { + dut.io.sAxi.w.valid.poke((written < 4).B) + dut.io.sAxi.w.bits.data.poke((if (written == 3) 1 else 0).U) + dut.io.sAxi.w.bits.last.poke((written == 3).B) + dut.io.sAxi.r.ready.poke((cycle >= 8).B) + val bValid = dut.io.sAxi.b.valid.peek().litToBoolean + dut.io.sAxi.b.ready.poke(bValid.B) + val wFire = written < 4 && dut.io.sAxi.w.ready.peek().litToBoolean + val rFire = cycle >= 8 && dut.io.sAxi.r.valid.peek().litToBoolean + if (rFire) { + dut.io.sAxi.r.bits.data.expect((0x20 + read * 4).U) + dut.io.sAxi.r.bits.last.expect((read == 3).B) + dut.io.sAxi.r.bits.resp.expect(AxiResp.RESP_OKAY) + dut.io.sAxi.r.bits.id.get.expect(3.U) + } + if (bValid) { + dut.io.sAxi.b.bits.resp.expect(AxiResp.RESP_SLVERR) + dut.io.sAxi.b.bits.id.get.expect(2.U) + } + dut.clock.step() + if (wFire) written += 1 + if (rFire) read += 1 + if (bValid) bReturned = true + complete = bReturned && read == 4 + } + assert(written == 4 && read == 4 && complete) + } + } + + it should "reject address widths that cannot represent a whole word" in { + intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new AxiToAcornBridge(AxiParams(64, 2))) + } + } +} From b48b60de543f08b14bc4d88f1c63262dcf1f46f8 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Tue, 6 Oct 2026 16:29:52 +0800 Subject: [PATCH 23/26] refactor(acorn): consolidate configuration and use strobeWidth Move AcornParams and AcornOutstanding into AcornIO.scala. Move arbitration, slave address windows, and crossbar configuration into AcornCrossbar.scala, and remove AcornConfig.scala. Rename AcornParams.bytesPerWord to strobeWidth throughout adapters, RegBank configuration, Specs, and the Acorn documentation. --- chipmunk/src/acorn/AcornConfig.scala | 81 ------------------- chipmunk/src/acorn/AcornCrossbar.scala | 69 +++++++++++++++- chipmunk/src/acorn/AcornIO.scala | 14 +++- chipmunk/src/acorn/AcornSramAdapter.scala | 12 +-- chipmunk/src/acorn/AcornWidthAdapter.scala | 16 ++-- chipmunk/src/regbank/RegBankConfig.scala | 2 +- .../test/src/acorn/AcornSramAdapterSpec.scala | 8 +- .../src/acorn/AcornWidthAdapterSpec.scala | 24 +++--- docs/acorn.md | 9 +-- 9 files changed, 116 insertions(+), 119 deletions(-) delete mode 100644 chipmunk/src/acorn/AcornConfig.scala diff --git a/chipmunk/src/acorn/AcornConfig.scala b/chipmunk/src/acorn/AcornConfig.scala deleted file mode 100644 index f9d6fc7..0000000 --- a/chipmunk/src/acorn/AcornConfig.scala +++ /dev/null @@ -1,81 +0,0 @@ -package chipmunk -package acorn - -/** A byte-addressed, naturally aligned, fixed-word interface. */ -final case class AcornParams(dataWidth: Int, addrWidth: Int) { - require(dataWidth >= 8 && (dataWidth & (dataWidth - 1)) == 0, "Acorn data width must be a power of two >= 8.") - require(addrWidth >= 1, "Acorn address width must be positive.") - val bytesPerWord: Int = dataWidth / 8 -} - -/** Separate capacities for accepted commands whose responses have not yet transferred. */ -final case class AcornOutstanding(read: Int = 4, write: Int = 4) { - require(read >= 1 && write >= 1, "Acorn outstanding capacities must be positive.") -} - -enum AcornArbitration { - case RoundRobin, LowerFirst -} - -/** One slave's global byte-address window [base, base + size). - * - * The endpoint sees localBase + (address - base). Its address width is inferred from the local window unless - * localAddrWidth is specified. All ports have the common bus data width; width conversion is a separate adapter. - */ -final case class AcornSlaveConfig( - name: String, - base: BigInt, - size: BigInt, - localBase: BigInt = 0, - localAddrWidth: Option[Int] = None, - readable: Boolean = true, - writable: Boolean = true, -) { - require(name.trim.nonEmpty, "An Acorn slave needs a name.") - require(base >= 0 && localBase >= 0 && size > 0, "Address bases must be nonnegative and size must be positive.") - require(readable || writable, "An Acorn slave must support at least one direction.") - - val end: BigInt = base + size - val portAddrWidth: Int = localAddrWidth.getOrElse(math.max(1, (localBase + size - 1).bitLength)) - require(portAddrWidth >= 1, "A slave address width must be positive.") - require(localBase + size <= (BigInt(1) << portAddrWidth), s"Local address window overflows slave $name.") -} - -/** Static crossbar configuration. Slave sequence order is the order of io.outs. - * - * connections=None connects every master to every slave. Otherwise there must be one set of slave names per master; an - * empty set disables all accesses from that master. Disallowed accesses receive normal ordered error responses. - * masterOutstanding limits each master's in-flight commands; slaveOutstanding limits each slave's aggregate load. - */ -final case class AcornCrossbarConfig( - params: AcornParams, - numMasters: Int, - slaves: Seq[AcornSlaveConfig], - masterOutstanding: AcornOutstanding = AcornOutstanding(), - slaveOutstanding: AcornOutstanding = AcornOutstanding(), - arbitration: AcornArbitration = AcornArbitration.RoundRobin, - connections: Option[Seq[Set[String]]] = None, -) { - require(numMasters >= 1 && slaves.nonEmpty, "An Acorn crossbar needs at least one master and one slave.") - private val names = slaves.map(_.name).toSet - require(names.size == slaves.size, "Acorn slave names must be unique.") - for (slave <- slaves) { - require(slave.end <= (BigInt(1) << params.addrWidth), s"Global address window overflows slave ${slave.name}.") - require( - slave.base % params.bytesPerWord == 0 && slave.size % params.bytesPerWord == 0 && - slave.localBase % params.bytesPerWord == 0, - s"Address window for ${slave.name} must be word aligned.", - ) - } - require( - slaves.sortBy(_.base).sliding(2).forall(pair => pair.size < 2 || pair.head.end <= pair.last.base), - "Acorn slave address windows must not overlap.", - ) - connections.foreach { sets => - require(sets.size == numMasters, "connections must contain one set per master.") - require(sets.forall(_.subsetOf(names)), "connections contains an unknown slave name.") - } - - def canAccess(master: Int, slave: Int): Boolean = - connections.forall(_(master).contains(slaves(slave).name)) -} diff --git a/chipmunk/src/acorn/AcornCrossbar.scala b/chipmunk/src/acorn/AcornCrossbar.scala index f66a0ce..1ea34ab 100644 --- a/chipmunk/src/acorn/AcornCrossbar.scala +++ b/chipmunk/src/acorn/AcornCrossbar.scala @@ -4,6 +4,73 @@ package acorn import chisel3.* import chisel3.util.MixedVec +enum AcornArbitration { + case RoundRobin, LowerFirst +} + +/** One slave's global byte-address window [base, base + size). + * + * The endpoint sees localBase + (address - base). Its address width is inferred from the local window unless + * localAddrWidth is specified. All ports have the common bus data width; width conversion is a separate adapter. + */ +final case class AcornSlaveConfig( + name: String, + base: BigInt, + size: BigInt, + localBase: BigInt = 0, + localAddrWidth: Option[Int] = None, + readable: Boolean = true, + writable: Boolean = true, +) { + require(name.trim.nonEmpty, "An Acorn slave needs a name.") + require(base >= 0 && localBase >= 0 && size > 0, "Address bases must be nonnegative and size must be positive.") + require(readable || writable, "An Acorn slave must support at least one direction.") + + val end: BigInt = base + size + val portAddrWidth: Int = localAddrWidth.getOrElse(math.max(1, (localBase + size - 1).bitLength)) + require(portAddrWidth >= 1, "A slave address width must be positive.") + require(localBase + size <= (BigInt(1) << portAddrWidth), s"Local address window overflows slave $name.") +} + +/** Static crossbar configuration. Slave sequence order is the order of io.outs. + * + * connections=None connects every master to every slave. Otherwise there must be one set of slave names per master; an + * empty set disables all accesses from that master. Disallowed accesses receive normal ordered error responses. + * masterOutstanding limits each master's in-flight commands; slaveOutstanding limits each slave's aggregate load. + */ +final case class AcornCrossbarConfig( + params: AcornParams, + numMasters: Int, + slaves: Seq[AcornSlaveConfig], + masterOutstanding: AcornOutstanding = AcornOutstanding(), + slaveOutstanding: AcornOutstanding = AcornOutstanding(), + arbitration: AcornArbitration = AcornArbitration.RoundRobin, + connections: Option[Seq[Set[String]]] = None, +) { + require(numMasters >= 1 && slaves.nonEmpty, "An Acorn crossbar needs at least one master and one slave.") + private val names = slaves.map(_.name).toSet + require(names.size == slaves.size, "Acorn slave names must be unique.") + for (slave <- slaves) { + require(slave.end <= (BigInt(1) << params.addrWidth), s"Global address window overflows slave ${slave.name}.") + require( + slave.base % params.strobeWidth == 0 && slave.size % params.strobeWidth == 0 && + slave.localBase % params.strobeWidth == 0, + s"Address window for ${slave.name} must be word aligned.", + ) + } + require( + slaves.sortBy(_.base).sliding(2).forall(pair => pair.size < 2 || pair.head.end <= pair.last.base), + "Acorn slave address windows must not overlap.", + ) + connections.foreach { sets => + require(sets.size == numMasters, "connections must contain one set per master.") + require(sets.forall(_.subsetOf(names)), "connections contains an unknown slave name.") + } + + def canAccess(master: Int, slave: Int): Boolean = + connections.forall(_(master).contains(slaves(slave).name)) +} + /** Per-master Demux followed by per-slave Mux. Different slaves arbitrate independently. * * Address misses, unaligned addresses, direction permissions, and disconnected routes use one private error endpoint @@ -29,7 +96,7 @@ class AcornCrossbar(val config: AcornCrossbarConfig) extends Module { } private def decode(addr: UInt, master: Int, write: Boolean): UInt = { - val aligned = (addr & (params.bytesPerWord - 1).U) === 0.U + val aligned = (addr & (params.strobeWidth - 1).U) === 0.U val hits = config.slaves.zipWithIndex.map { case (slave, index) => val enabled = config.canAccess(master, index) && (if (write) slave.writable else slave.readable) val hit = enabled.B && aligned && addr >= slave.base.U && addr <= (slave.end - 1).U diff --git a/chipmunk/src/acorn/AcornIO.scala b/chipmunk/src/acorn/AcornIO.scala index 8e18e60..8da083b 100644 --- a/chipmunk/src/acorn/AcornIO.scala +++ b/chipmunk/src/acorn/AcornIO.scala @@ -5,6 +5,18 @@ import chisel3.* import chipmunk.stream.* +/** A byte-addressed, naturally aligned, fixed-word interface. */ +final case class AcornParams(dataWidth: Int, addrWidth: Int) { + require(dataWidth >= 8 && (dataWidth & (dataWidth - 1)) == 0, "Acorn data width must be a power of two >= 8.") + require(addrWidth >= 1, "Acorn address width must be positive.") + val strobeWidth: Int = dataWidth / 8 +} + +/** Separate capacities for accepted commands whose responses have not yet transferred. */ +final case class AcornOutstanding(read: Int = 4, write: Int = 4) { + require(read >= 1 && write >= 1, "Acorn outstanding capacities must be positive.") +} + class AcornWrCmdChannel(val dataWidth: Int, val addrWidth: Int) extends Bundle { val addr = UInt(addrWidth.W) val data = UInt(dataWidth.W) @@ -38,7 +50,7 @@ class AcornIO(val dataWidth: Int, val addrWidth: Int) extends Bundle with IsMast def this(params: AcornParams) = this(params.dataWidth, params.addrWidth) val params: AcornParams = AcornParams(dataWidth, addrWidth) - val strobeWidth: Int = params.bytesPerWord + val strobeWidth: Int = params.strobeWidth override def isMaster: Boolean = true val rd = new Bundle { diff --git a/chipmunk/src/acorn/AcornSramAdapter.scala b/chipmunk/src/acorn/AcornSramAdapter.scala index 5844244..265ecf4 100644 --- a/chipmunk/src/acorn/AcornSramAdapter.scala +++ b/chipmunk/src/acorn/AcornSramAdapter.scala @@ -19,7 +19,7 @@ final case class AcornSramConfig( require(numWords >= 1, "An Acorn SRAM needs at least one word.") require(readLatency >= 1, "SRAM read latency must be positive.") - val sizeBytes: BigInt = numWords * params.bytesPerWord + val sizeBytes: BigInt = numWords * params.strobeWidth require(sizeBytes <= (BigInt(1) << params.addrWidth), "SRAM capacity exceeds the Acorn address range.") } @@ -43,7 +43,7 @@ final class AcornSramAdapter(val config: AcornSramConfig) extends Module { val sram = Flipped( new SRAMInterface( memSize = config.numWords, - tpe = Vec(params.bytesPerWord, UInt(8.W)), + tpe = Vec(params.strobeWidth, UInt(8.W)), numReadPorts = if (singlePort) 0 else 1, numWritePorts = if (singlePort) 0 else 1, numReadwritePorts = if (singlePort) 1 else 0, @@ -90,14 +90,14 @@ final class AcornSramAdapter(val config: AcornSramConfig) extends Module { writeResponses.io.deq.ready := io.access.wr.rsp.ready private def addressError(address: UInt): Bool = { - val aligned = (address & (params.bytesPerWord - 1).U) === 0.U + val aligned = (address & (params.strobeWidth - 1).U) === 0.U !aligned || address >= config.sizeBytes.U } private val readError = addressError(readCommand.bits.addr) private val writeError = addressError(writeCommand.bits.addr) - private val readWord = readCommand.bits.addr >> log2Ceil(params.bytesPerWord) - private val writeWord = writeCommand.bits.addr >> log2Ceil(params.bytesPerWord) + private val readWord = readCommand.bits.addr >> log2Ceil(params.strobeWidth) + private val writeWord = writeCommand.bits.addr >> log2Ceil(params.strobeWidth) private val readNeedsSram = !readError private val writeNeedsSram = !writeError && writeCommand.bits.strobe.orR @@ -116,7 +116,7 @@ final class AcornSramAdapter(val config: AcornSramConfig) extends Module { private val doRead = readCommand.fire && readNeedsSram private val doWrite = writeCommand.fire && writeNeedsSram - private val writeData = writeCommand.bits.data.asTypeOf(Vec(params.bytesPerWord, UInt(8.W))) + private val writeData = writeCommand.bits.data.asTypeOf(Vec(params.strobeWidth, UInt(8.W))) private val writeMask = VecInit(writeCommand.bits.strobe.asBools) private val readData = Wire(UInt(params.dataWidth.W)) diff --git a/chipmunk/src/acorn/AcornWidthAdapter.scala b/chipmunk/src/acorn/AcornWidthAdapter.scala index 74d17b8..33d000f 100644 --- a/chipmunk/src/acorn/AcornWidthAdapter.scala +++ b/chipmunk/src/acorn/AcornWidthAdapter.scala @@ -23,7 +23,7 @@ import chisel3.util.log2Ceil final class AcornWidthAdapter(val inputParams: AcornParams, val outputParams: AcornParams) extends Module { require(inputParams.addrWidth == outputParams.addrWidth, "Width adaptation must preserve the byte-address width.") require( - inputParams.addrWidth >= log2Ceil(math.max(inputParams.bytesPerWord, outputParams.bytesPerWord)), + inputParams.addrWidth >= log2Ceil(math.max(inputParams.strobeWidth, outputParams.strobeWidth)), "The address width must cover the larger word.", ) @@ -31,7 +31,7 @@ final class AcornWidthAdapter(val inputParams: AcornParams, val outputParams: Ac private val expand = inputParams.dataWidth < outputParams.dataWidth private val segments = if (split) inputParams.dataWidth / outputParams.dataWidth else 1 private val indexBits = math.max(1, log2Ceil(segments)) - private val addrMask = ((BigInt(1) << outputParams.addrWidth) - outputParams.bytesPerWord).U + private val addrMask = ((BigInt(1) << outputParams.addrWidth) - outputParams.strobeWidth).U val io = IO(new Bundle { val sAcorn = Slave(new AcornIO(inputParams)) @@ -94,7 +94,7 @@ final class AcornWidthAdapter(val inputParams: AcornParams, val outputParams: Ac private val writeAddress = Reg(UInt(inputParams.addrWidth.W)) private val writeIndex = Reg(UInt(indexBits.W)) private val writeData = Reg(UInt(inputParams.dataWidth.W)) - private val writeStrobe = Reg(UInt(inputParams.bytesPerWord.W)) + private val writeStrobe = Reg(UInt(inputParams.strobeWidth.W)) private val writeError = Reg(Bool()) io.sAcorn.wr.cmd.ready := writePhase === Phase.Idle @@ -113,11 +113,11 @@ final class AcornWidthAdapter(val inputParams: AcornParams, val outputParams: Ac if (split) { io.mAcorn.wr.cmd.bits.data := (writeData >> bitOffset(writeIndex))(outputParams.dataWidth - 1, 0) io.mAcorn.wr.cmd.bits.strobe := - (writeStrobe >> (writeIndex << log2Ceil(outputParams.bytesPerWord)))(outputParams.bytesPerWord - 1, 0) + (writeStrobe >> (writeIndex << log2Ceil(outputParams.strobeWidth)))(outputParams.strobeWidth - 1, 0) } else if (expand) { io.mAcorn.wr.cmd.bits.data := (writeData << laneOffset(writeAddress))(outputParams.dataWidth - 1, 0) io.mAcorn.wr.cmd.bits.strobe := - (writeStrobe << writeAddress(log2Ceil(outputParams.bytesPerWord) - 1, 0))(outputParams.bytesPerWord - 1, 0) + (writeStrobe << writeAddress(log2Ceil(outputParams.strobeWidth) - 1, 0))(outputParams.strobeWidth - 1, 0) } else { io.mAcorn.wr.cmd.bits.data := writeData io.mAcorn.wr.cmd.bits.strobe := writeStrobe @@ -143,12 +143,12 @@ final class AcornWidthAdapter(val inputParams: AcornParams, val outputParams: Ac writePhase := Phase.Idle } - private def aligned(address: UInt): Bool = (address & (inputParams.bytesPerWord - 1).U) === 0.U + private def aligned(address: UInt): Bool = (address & (inputParams.strobeWidth - 1).U) === 0.U private def endpointAddress(address: UInt, index: UInt): UInt = - if (split) address + (index << log2Ceil(outputParams.bytesPerWord)) else address & addrMask + if (split) address + (index << log2Ceil(outputParams.strobeWidth)) else address & addrMask private def bitOffset(index: UInt): UInt = index << log2Ceil(outputParams.dataWidth) - private def laneOffset(address: UInt): UInt = address(log2Ceil(outputParams.bytesPerWord) - 1, 0) << 3 + private def laneOffset(address: UInt): UInt = address(log2Ceil(outputParams.strobeWidth) - 1, 0) << 3 } diff --git a/chipmunk/src/regbank/RegBankConfig.scala b/chipmunk/src/regbank/RegBankConfig.scala index 88c6ec6..b732419 100644 --- a/chipmunk/src/regbank/RegBankConfig.scala +++ b/chipmunk/src/regbank/RegBankConfig.scala @@ -64,7 +64,7 @@ final case class RegBankConfig(params: AcornParams, regs: Seq[RegElementConfig]) require(regs.map(_.addr).distinct.size == regs.size, "Register byte offsets must be unique.") for (reg <- regs) { require(reg.addr.bitLength <= params.addrWidth, s"Byte offset overflows register ${reg.name}.") - require(reg.addr % params.bytesPerWord == 0, s"Register ${reg.name} must be word aligned.") + require(reg.addr % params.strobeWidth == 0, s"Register ${reg.name} must be word aligned.") require(reg.fields.forall(_.endOffset < params.dataWidth), s"Field exceeds the data width in register ${reg.name}.") } } diff --git a/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala b/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala index bd5c3af..a229410 100644 --- a/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala +++ b/chipmunk/test/src/acorn/AcornSramAdapterSpec.scala @@ -19,7 +19,7 @@ private object AcornSramAdapterSpecDut { val readWord = Output(UInt(config.params.addrWidth.W)) val writeWord = Output(UInt(config.params.addrWidth.W)) val writeData = Output(UInt(config.params.dataWidth.W)) - val writeMask = Output(UInt(config.params.bytesPerWord.W)) + val writeMask = Output(UInt(config.params.strobeWidth.W)) }) val adapter = Module(new AcornSramAdapter(config)) @@ -28,7 +28,7 @@ private object AcornSramAdapterSpecDut { private val single = config.portMode == AcornSramPortMode.SinglePort private val memory = SRAM.masked( config.numWords, - Vec(config.params.bytesPerWord, UInt(8.W)), + Vec(config.params.strobeWidth, UInt(8.W)), numReadPorts = if (single) 0 else 1, numWritePorts = if (single) 0 else 1, numReadwritePorts = if (single) 1 else 0, @@ -92,12 +92,12 @@ class AcornSramAdapterSpec extends ChipmunkFlatSpec { simulate(new AcornSramAdapterSpecDut.Harness(config)) { dut => init(dut) val port = dut.io.access - val address = config.sizeBytes - config.params.bytesPerWord + val address = config.sizeBytes - config.params.strobeWidth val data = (BigInt(1) << width) - 0x53 port.wr.cmd.valid #= true.B port.wr.cmd.bits.addr #= address.U port.wr.cmd.bits.data #= data.U - port.wr.cmd.bits.strobe #= ((BigInt(1) << config.params.bytesPerWord) - 1).U + port.wr.cmd.bits.strobe #= ((BigInt(1) << config.params.strobeWidth) - 1).U port.wr.cmd.ready expect true.B dut.clock.step() port.wr.cmd.valid #= false.B diff --git a/chipmunk/test/src/acorn/AcornWidthAdapterSpec.scala b/chipmunk/test/src/acorn/AcornWidthAdapterSpec.scala index 1d9a869..c94fbe0 100644 --- a/chipmunk/test/src/acorn/AcornWidthAdapterSpec.scala +++ b/chipmunk/test/src/acorn/AcornWidthAdapterSpec.scala @@ -23,16 +23,16 @@ final class AcornWidthAdapterSpec extends ChipmunkFlatSpec { val split = inputWidth > outputWidth val count = if (split) inputWidth / outputWidth else 1 val requests = (0 until 24).map { index => - val aligned = input.bytesPerWord == 1 || !Set(3, 7, 19)(index) + val aligned = input.strobeWidth == 1 || !Set(3, 7, 19)(index) val address = - if (index == 23) BigInt(4096 - input.bytesPerWord) - else BigInt((index + 8) * input.bytesPerWord + (if (aligned) 0 else 1)) + if (index == 23) BigInt(4096 - input.strobeWidth) + else BigInt((index + 8) * input.strobeWidth + (if (aligned) 0 else 1)) val strobe = index % 3 match { case 0 => BigInt(0) - case 1 => BigInt(1) << (index % input.bytesPerWord) - case 2 => (BigInt(1) << input.bytesPerWord) - 1 + case 1 => BigInt(1) << (index % input.strobeWidth) + case 2 => (BigInt(1) << input.strobeWidth) - 1 } - Request(address, memoryWord(address + 123, input.bytesPerWord), strobe, aligned) + Request(address, memoryWord(address + 123, input.strobeWidth), strobe, aligned) } def converted(request: Request, index: Int): Seq[Segment] = { @@ -48,12 +48,12 @@ final class AcornWidthAdapterSpec extends ChipmunkFlatSpec { } (0 until count).take(if (failure < 0) count else failure + 1).map { segment => val address = - if (split) request.address + segment * output.bytesPerWord - else request.address & ~(BigInt(output.bytesPerWord) - 1) - val byteOffset = if (split) segment * output.bytesPerWord else 0 + if (split) request.address + segment * output.strobeWidth + else request.address & ~(BigInt(output.strobeWidth) - 1) + val byteOffset = if (split) segment * output.strobeWidth else 0 val laneOffset = if (inputWidth < outputWidth) (request.address - address).toInt else 0 val dataMask = (BigInt(1) << outputWidth) - 1 - val strobeMask = (BigInt(1) << output.bytesPerWord) - 1 + val strobeMask = (BigInt(1) << output.strobeWidth) - 1 Segment( address, ((request.data >> (8 * byteOffset)) << (8 * laneOffset)) & dataMask, @@ -149,7 +149,7 @@ final class AcornWidthAdapterSpec extends ChipmunkFlatSpec { val error = readParts(readDelivered).isEmpty || readParts(readDelivered).exists(_.error) dut.io.sAcorn.rd.rsp.bits.error.expect(error.B) dut.io.sAcorn.rd.rsp.bits.data - .expect((if (error) BigInt(0) else memoryWord(requests(readDelivered).address, input.bytesPerWord)).U) + .expect((if (error) BigInt(0) else memoryWord(requests(readDelivered).address, input.strobeWidth)).U) dut.io.sAcorn.rd.cmd.ready.expect(false.B) } if (writeValid) { @@ -176,7 +176,7 @@ final class AcornWidthAdapterSpec extends ChipmunkFlatSpec { if (endpointRead) { val expected = expectedReads(readReceived) readResponses.enqueue( - Response(cycle + 2 + random.nextInt(4), memoryWord(expected.address, output.bytesPerWord), expected.error) + Response(cycle + 2 + random.nextInt(4), memoryWord(expected.address, output.strobeWidth), expected.error) ) readReceived += 1 } diff --git a/docs/acorn.md b/docs/acorn.md index ca36cf4..2a4d777 100644 --- a/docs/acorn.md +++ b/docs/acorn.md @@ -35,12 +35,11 @@ Acorn 是用于模块内部地址访问的轻量接口,适合配置寄存器 | 文件 | 职责 | | --- | --- | -| `AcornIO.scala` | IO 和读写命令、响应 payload | -| `AcornConfig.scala` | 参数、容量、仲裁方式和静态地址映射配置 | +| `AcornIO.scala` | IO、读写 payload、接口参数和在途容量 | | `AcornRouting.scala` | 内部共用的来源/目标 FIFO 及 Stream 路由逻辑 | | `AcornMux.scala` | 多 master 到一个 slave 的命令仲裁和响应分发 | | `AcornDemux.scala` | 一个 master 到多个 slave 的命令分发和响应排序 | -| `AcornCrossbar.scala` | 每 master 一个 Demux、每 slave 一个 Mux,并完成地址转换 | +| `AcornCrossbar.scala` | 仲裁与地址映射配置,以及 Crossbar 的路由和地址转换 | | `AcornErrorPoint.scala` | 可独立实例化的错误响应端点 | | `AcornSramAdapter.scala` | 字节掩码同步 SRAM 的 1RW / 1R1W 适配器及配置 | @@ -161,7 +160,7 @@ val config = AcornSramConfig( val adapter = Module(new AcornSramAdapter(config)) val memory = SRAM.masked( config.numWords, - Vec(config.params.bytesPerWord, UInt(8.W)), + Vec(config.params.strobeWidth, UInt(8.W)), numReadPorts = 0, numWritePorts = 0, numReadwritePorts = 1 @@ -172,7 +171,7 @@ adapter.io.sram <> memory 1R1W 使用 `AcornSramPortMode.OneReadOneWrite`,对应 SRAM 的端口数量为 `(1, 1, 0)`。 上述 `SRAM.masked` 默认读延迟为一拍;替换为其他 SRAM 后端时,`readLatency` 必须与其实际延迟一致。 -SRAM 容量为 `numWords * bytesPerWord`,adapter 地址是从零开始的局部字节偏移。越界或未对齐的命令返回错误, +SRAM 容量为 `numWords * strobeWidth`,adapter 地址是从零开始的局部字节偏移。越界或未对齐的命令返回错误, 不访问 SRAM;合法的零 strobe 写不修改 SRAM,仍返回成功响应。 读写命令各经过一个两项队列,关闭 `flow` 和 `pipe`,隔断总线命令到 SRAM 控制以及仲裁到总线 `ready` 的组合路径。 From c18389a47156fa73a0a11631f285f6a9206525c5 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Tue, 6 Oct 2026 19:34:35 +0800 Subject: [PATCH 24/26] feat: add PriorityEncoderDefault and update Chisel Scala 3 snapshot Add Seq[Bool] and Bits overloads in MuxPriority.scala, returning the explicit default when no input is asserted. Use 7.16.0+3-a2d66312-SNAPSHOT for both the Chisel library and compiler plugin while retaining Scala 3.8.4. --- build.mill | 2 +- chipmunk/src/MuxPriority.scala | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/build.mill b/build.mill index b88b082..c28a38c 100644 --- a/build.mill +++ b/build.mill @@ -6,7 +6,7 @@ import mill.scalalib.publish.* object Dependencies { val scalaVersion = "3.8.4" - val chiselVersion = "7.15.0+12-14b890ce-SNAPSHOT" + val chiselVersion = "7.16.0+3-a2d66312-SNAPSHOT" val chisel = mvn"org.chipsalliance::chisel:$chiselVersion" val chiselPlugin = mvn"org.chipsalliance:::chisel-plugin:$chiselVersion" diff --git a/chipmunk/src/MuxPriority.scala b/chipmunk/src/MuxPriority.scala index e3e8f68..fbcc90d 100644 --- a/chipmunk/src/MuxPriority.scala +++ b/chipmunk/src/MuxPriority.scala @@ -28,3 +28,16 @@ object MuxPriority { def apply[T <: Data](sel: Bits, in: Seq[T], default: T)(using SourceInfo): T = apply(sel.asBools.zip(in), default) } + +/** Encode the lowest-index asserted input, returning default when none are asserted. + * + * For Bits, bit zero has highest priority. An empty sequence returns default. + */ +object PriorityEncoderDefault { + + def apply(in: Seq[Bool], default: UInt)(using SourceInfo): UInt = + MuxPriority(in.zipWithIndex.map { case (active, index) => active -> index.U }, default) + + def apply(in: Bits, default: UInt)(using SourceInfo): UInt = + apply(in.asBools, default) +} From c4076640a4e02ed33c6cfee72e07566883aabde7 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Tue, 6 Oct 2026 20:35:55 +0800 Subject: [PATCH 25/26] feat: migrate SPI debugger with stable Acorn transactions Add SpiIO and SpiDebugger under chipmunk.spi, preserving the command encodings and register indices while adapting accesses to the byte-addressed RegBank. Snapshot requests, serialize bus transactions, and expose completion and rejection status. Return 0xDEAD_BEEF for an early fast read while retaining late responses for polling. Handle incomplete frames and address errors. Document the 8:1 system/SCK clock requirement and add 11 passing simulation tests covering all SPI modes, clock phases, backpressure, late and same-cycle responses, aborted frames, address widths, and reset. --- chipmunk/src/spi/SpiDebugger.scala | 332 ++++++++++++++++ chipmunk/src/spi/SpiIO.scala | 21 + chipmunk/test/src/spi/SpiDebuggerSpec.scala | 420 ++++++++++++++++++++ docs/spi-debugger.md | 49 ++- 4 files changed, 811 insertions(+), 11 deletions(-) create mode 100644 chipmunk/src/spi/SpiDebugger.scala create mode 100644 chipmunk/src/spi/SpiIO.scala create mode 100644 chipmunk/test/src/spi/SpiDebuggerSpec.scala diff --git a/chipmunk/src/spi/SpiDebugger.scala b/chipmunk/src/spi/SpiDebugger.scala new file mode 100644 index 0000000..5ba64ba --- /dev/null +++ b/chipmunk/src/spi/SpiDebugger.scala @@ -0,0 +1,332 @@ +package chipmunk +package spi + +import chisel3.* +import chisel3.util.{is, switch, Cat, ShiftRegister, ShiftRegisters} + +import chipmunk.acorn.{AcornIO, AcornParams} +import chipmunk.regbank.{RegBank, RegBankConfig, RegElementConfig, RegFieldAccessType} + +object SpiDebugger { + + /** Six-bit SPI register indices; the internal RegBank byte offset is index * 4. */ + object Registers { + val BUS_ADDR_L = 0x00 + val BUS_ADDR_H = 0x01 + val BUS_WR_RESP = 0x02 + val BUS_RD_RESP = 0x03 + val BUS_WR_DATA = 0x04 + val BUS_RD_DATA = 0x05 + val BUS_WR_MASK = 0x06 + val STATUS = 0x07 + val TEST = 0x3f + } + + /** STATUS bit masks. An admitted request clears DONE, REJECTED and EARLY_READ. */ + object Status { + val BUSY = 1 << 0 + val RD_DONE = 1 << 1 + val WR_DONE = 1 << 2 + val REJECTED = 1 << 3 + val EARLY_READ = 1 << 4 + } + + val earlyReadValue: BigInt = BigInt("DEADBEEF", 16) +} + +/** SPI slave debugger with a 32-bit, byte-addressed Acorn master. + * + * Transfers are MSB-first, with one command per active-low SSN frame. The system clock frequency must be at least + * eight times the SCK frequency, assuming approximately 50% SCK duty cycle and sufficient IO setup/hold margin. SSN + * must be observed active before the first sample edge and remain active through the last sample edge. These are host + * timing requirements, not checks enforced by this module. + * + * Commands: REG_WR = 00iiiiii + data32; REG_RD = 01iiiiii + data32; BUS_WR = 0x80 + address32 + data32; BUS_RD = 0xc0 + * + address32 + dummy8 + data32. Other 1xxxxxxx commands are NOPs. BUS_ADDR_H supplies upper address bits when + * busAddrWidth > 32. Writing BUS_WR_RESP or BUS_RD_RESP triggers a staged bus write or read. + * + * Only one bus transaction may be pending. Requests snapshot address/data/strobe and hold them through command + * backpressure. A trigger while BUSY sets REJECTED without replacing that transaction. Unaligned or overflowing + * addresses complete locally with an error. RESP bit zero reports the last completed operation's error. + * + * Poll STATUS.RD_DONE before reading BUS_RD_DATA for arbitrary bus latency. A fast BUS_RD snapshots its result at the + * end of the dummy phase; if its own result is unavailable, it returns 0xDEAD_BEEF and sets EARLY_READ. A late + * response still updates BUS_RD_DATA and RD_DONE. Check STATUS: 0xDEAD_BEEF can also be valid bus data. + * + * An incomplete REG_WR does not write its target register; an incomplete BUS_WR does not issue a bus write. Fast + * commands update address staging when their address word completes. Deselecting SPI does not cancel a submitted bus + * request. Reset cancels transactions and must reset the connected Acorn endpoints consistently. + * + * @param hasMisoValid + * expose a MISO IO-cell output enable, qualified directly by SSN for immediate release on deselection + * @param cpol + * idle SCK level + * @param cpha + * sample on the trailing edge when true, otherwise on the leading edge + * @param busAddrWidth + * Acorn address width, from 2 to 64 bits + */ +class SpiDebugger(hasMisoValid: Boolean = false, cpol: Boolean = false, cpha: Boolean = false, busAddrWidth: Int = 32) + extends Module { + require(busAddrWidth >= 2 && busAddrWidth <= 64, "SpiDebugger busAddrWidth must be between 2 and 64.") + + import SpiDebugger.Registers.* + + private val busParams = AcornParams(32, busAddrWidth) + val io = IO(new Bundle { + val sSpi = Slave(new SpiIO(hasMisoValid)) + val mDbg = Master(new AcornIO(busParams)) + }) + + // Two synchronizer stages followed by edge history; MOSI uses the corresponding delayed sample. + private val ssn = ShiftRegisters(io.sSpi.ssn, 3, true.B, true.B) + private val sck = ShiftRegisters(io.sSpi.sck, 3, cpol.B, true.B) + private val mosi = ShiftRegister(io.sSpi.mosi, 3, false.B, true.B) + private val selected = !ssn(1) + private val select = ssn(2) && !ssn(1) + private val rising = !sck(2) && sck(1) + private val falling = sck(2) && !sck(1) + private val sample = selected && (if (cpol == cpha) rising else falling) + private val launch = selected && (if (cpol == cpha) falling else rising) + + private object State extends ChiselEnum { + val Idle, Command, RegWrite, RegRead, BusAddress, BusWrite, ReadDummy, BusRead, Done = Value + } + private val state = RegInit(State.Idle) + private val count = RegInit(0.U(5.W)) + private val rx = RegInit(0.U(32.W)) + private val rxNext = Cat(rx(30, 0), mosi) + private val bytePhase = state.isOneOf(State.Command, State.ReadDummy) + private val last = count === Mux(bytePhase, 7.U, 31.U) + private val receiving = state.isOneOf(State.Command, State.RegWrite, State.BusAddress, State.BusWrite) + private val transmitting = state.isOneOf(State.RegRead, State.BusRead) + private val active = !state.isOneOf(State.Idle, State.Done) + private val wordDone = sample && active && last + private val addressDone = wordDone && state === State.BusAddress + private val writeDone = wordDone && state === State.BusWrite + private val frameRead = RegInit(false.B) + private val frameAddress = RegInit(0.U(64.W)) + private val frameOverflow = RegInit(false.B) + private val fastReadAdmitted = RegInit(false.B) + + private val tx = RegInit(0.U(32.W)) + private val txActive = RegInit(false.B) + private val txSampled = RegInit(false.B) + io.sSpi.miso := tx(31) + // Only pad output enable uses raw SSN; protocol state uses synchronized inputs. + io.sSpi.misoValid.foreach(_ := !io.sSpi.ssn) + + private val registerConfigs = { + import RegFieldAccessType.ReadOnly + val high = if (busAddrWidth > 32) { + Seq(RegElementConfig("BUS_ADDR_H", BUS_ADDR_H * 4, busAddrWidth - 32)) + } else Seq.empty + high ++ Seq( + RegElementConfig("BUS_ADDR_L", BUS_ADDR_L * 4, busAddrWidth min 32, backdoorUpdate = true), + RegElementConfig("BUS_WR_RESP", BUS_WR_RESP * 4, 1, accessType = ReadOnly, backdoorUpdate = true), + RegElementConfig("BUS_RD_RESP", BUS_RD_RESP * 4, 1, accessType = ReadOnly, backdoorUpdate = true), + RegElementConfig("BUS_WR_DATA", BUS_WR_DATA * 4, 32, backdoorUpdate = true), + RegElementConfig("BUS_RD_DATA", BUS_RD_DATA * 4, 32, backdoorUpdate = true), + RegElementConfig( + "BUS_WR_MASK", + BUS_WR_MASK * 4, + busParams.strobeWidth, + initValue = (BigInt(1) << busParams.strobeWidth) - 1, + ), + RegElementConfig("STATUS", STATUS * 4, 5, accessType = ReadOnly, backdoorUpdate = true), + RegElementConfig("TEST", TEST * 4, 32, initValue = BigInt("3f1b00e5", 16)), + ) + } + private val bank = Module(new RegBank(RegBankConfig(AcornParams(32, 8), registerConfigs))) + private def value(name: String): UInt = bank.io.fields(name).value + private def update(name: String, enable: Bool, data: UInt): Unit = { + val port = bank.io.fields(name).backdoorUpdate.get + port.valid := enable + port.bits := data + } + + private val regIndex = RegInit(0.U(6.W)) + private val regWrite = RegInit(false.B) + private val regRead = RegInit(false.B) + private val regData = RegInit(0.U(32.W)) + bank.io.access.wr.cmd.valid := regWrite + bank.io.access.wr.cmd.bits.addr := Cat(regIndex, 0.U(2.W)) + bank.io.access.wr.cmd.bits.data := regData + bank.io.access.wr.cmd.bits.strobe := ((BigInt(1) << busParams.strobeWidth) - 1).U + bank.io.access.wr.rsp.ready := true.B + bank.io.access.rd.cmd.valid := regRead + bank.io.access.rd.cmd.bits.addr := Cat(regIndex, 0.U(2.W)) + bank.io.access.rd.rsp.ready := true.B + when(bank.io.access.wr.cmd.fire) { regWrite := false.B } + when(bank.io.access.rd.cmd.fire) { regRead := false.B } + + // Retain overflow information that narrow address fields otherwise discard on software writes. + private val lowOverflow = RegInit(false.B) + private val highOverflow = RegInit(false.B) + private def overflowLow(data: UInt): Bool = + if (busAddrWidth < 32) data(31, busAddrWidth).orR else false.B + private def overflowHigh(data: UInt): Bool = + if (busAddrWidth > 32 && busAddrWidth < 64) data(31, busAddrWidth - 32).orR else false.B + when(bank.io.fields("BUS_ADDR_L").isBeingWritten) { lowOverflow := overflowLow(regData) } + if (busAddrWidth > 32) { + when(bank.io.fields("BUS_ADDR_H").isBeingWritten) { highOverflow := overflowHigh(regData) } + } + private val upperAddress = if (busAddrWidth > 32) value("BUS_ADDR_H").pad(32) else 0.U(32.W) + private val stagedAddress = Cat(upperAddress, value("BUS_ADDR_L").pad(32)) + private val receivedAddress = Cat(upperAddress, rxNext) + when(addressDone) { + frameAddress := receivedAddress + frameOverflow := overflowLow(rxNext) || highOverflow + lowOverflow := overflowLow(rxNext) + } + update("BUS_ADDR_L", addressDone, rxNext(busAddrWidth.min(32) - 1, 0)) + update("BUS_WR_DATA", writeDone, rxNext) + + private val busy = RegInit(false.B) + private val rdDone = RegInit(false.B) + private val wrDone = RegInit(false.B) + private val rejected = RegInit(false.B) + private val earlyRead = RegInit(false.B) + private val readResult = RegInit(0.U(32.W)) + update("STATUS", true.B, Cat(earlyRead, rejected, wrDone, rdDone, busy)) + + private val fastRead = addressDone && frameRead + private val fastWrite = writeDone + private val stagedRead = bank.io.fields("BUS_RD_RESP").isBeingWritten + private val stagedWrite = bank.io.fields("BUS_WR_RESP").isBeingWritten + private val trigger = fastRead || fastWrite || stagedRead || stagedWrite + private val requestRead = fastRead || stagedRead + private val requestAddress = Mux(fastRead, receivedAddress, Mux(fastWrite, frameAddress, stagedAddress)) + private val requestOverflow = + Mux(fastRead, overflowLow(rxNext) || highOverflow, Mux(fastWrite, frameOverflow, lowOverflow || highOverflow)) + private val invalid = requestOverflow || requestAddress(1, 0).orR + private val admit = trigger && !busy + private val invalidRead = admit && invalid && requestRead + private val invalidWrite = admit && invalid && !requestRead + + private val commandPending = RegInit(false.B) + private val awaitingResponse = RegInit(false.B) + private val pendingRead = RegInit(false.B) + private val pendingAddress = RegInit(0.U(busAddrWidth.W)) + private val pendingData = RegInit(0.U(32.W)) + private val pendingStrobe = RegInit(0.U(busParams.strobeWidth.W)) + io.mDbg.rd.cmd.valid := commandPending && pendingRead + io.mDbg.rd.cmd.bits.addr := pendingAddress + io.mDbg.wr.cmd.valid := commandPending && !pendingRead + io.mDbg.wr.cmd.bits.addr := pendingAddress + io.mDbg.wr.cmd.bits.data := pendingData + io.mDbg.wr.cmd.bits.strobe := pendingStrobe + io.mDbg.rd.rsp.ready := busy && pendingRead && (awaitingResponse || io.mDbg.rd.cmd.fire) + io.mDbg.wr.rsp.ready := busy && !pendingRead && (awaitingResponse || io.mDbg.wr.cmd.fire) + private val readResponse = io.mDbg.rd.rsp.fire + private val writeResponse = io.mDbg.wr.rsp.fire + private val responseData = Mux(io.mDbg.rd.rsp.bits.error, 0.U, io.mDbg.rd.rsp.bits.data) + + when(io.mDbg.rd.cmd.fire || io.mDbg.wr.cmd.fire) { + commandPending := false.B + awaitingResponse := true.B + } + when(readResponse || writeResponse) { + busy := false.B + awaitingResponse := false.B + } + when(readResponse) { + rdDone := true.B + readResult := responseData + } + when(writeResponse) { wrDone := true.B } + when(trigger) { + when(busy) { + rejected := true.B + }.otherwise { + busy := !invalid + commandPending := !invalid + awaitingResponse := false.B + pendingRead := requestRead + pendingAddress := requestAddress(busAddrWidth - 1, 0) + pendingData := Mux(fastWrite, rxNext, value("BUS_WR_DATA")) + pendingStrobe := value("BUS_WR_MASK") + rdDone := invalid && requestRead + wrDone := invalid && !requestRead + rejected := false.B + earlyRead := false.B + when(invalid && requestRead) { readResult := 0.U } + } + } + update("BUS_RD_DATA", readResponse || invalidRead, Mux(invalidRead, 0.U, responseData)) + update("BUS_RD_RESP", readResponse || invalidRead, (invalidRead || io.mDbg.rd.rsp.bits.error).asUInt) + update("BUS_WR_RESP", writeResponse || invalidWrite, (invalidWrite || io.mDbg.wr.rsp.bits.error).asUInt) + + // Shift only after the preceding data bit has been sampled; the first launch keeps the preloaded MSB. + when(launch && txActive && txSampled) { + tx := Cat(tx(30, 0), false.B) + txSampled := false.B + } + when(!selected) { + state := State.Idle + count := 0.U + rx := 0.U + tx := 0.U + txActive := false.B + txSampled := false.B + fastReadAdmitted := false.B + }.elsewhen(select) { + state := State.Command + count := 0.U + rx := 0.U + txActive := false.B + txSampled := false.B + fastReadAdmitted := false.B + }.elsewhen(sample && active) { + count := Mux(last, 0.U, count + 1.U) + when(receiving) { rx := Mux(last, 0.U, rxNext) } + when(transmitting) { txSampled := true.B } + when(last) { + switch(state) { + is(State.Command) { + regIndex := rxNext(5, 0) + when(rxNext(7, 0) === 0x80.U || rxNext(7, 0) === 0xc0.U) { + frameRead := rxNext(6) + state := State.BusAddress + }.elsewhen(!rxNext(7)) { + when(rxNext(6)) { + regRead := true.B + state := State.RegRead + }.otherwise { + state := State.RegWrite + } + }.otherwise { state := State.Done } + } + is(State.RegWrite) { + regData := rxNext + regWrite := true.B + state := State.Done + } + is(State.BusAddress) { + when(frameRead) { + fastReadAdmitted := !busy + state := State.ReadDummy + }.otherwise { state := State.BusWrite } + } + is(State.BusWrite) { state := State.Done } + is(State.ReadDummy) { + val available = fastReadAdmitted && (rdDone || readResponse) + tx := Mux(available, Mux(readResponse, responseData, readResult), SpiDebugger.earlyReadValue.U(32.W)) + txActive := true.B + txSampled := false.B + when(!available) { earlyRead := true.B } + state := State.BusRead + } + is(State.RegRead, State.BusRead) { + txActive := false.B + state := State.Done + } + } + } + } + when(bank.io.access.rd.rsp.fire && selected && state === State.RegRead) { + tx := bank.io.access.rd.rsp.bits.data + txActive := true.B + txSampled := false.B + } +} diff --git a/chipmunk/src/spi/SpiIO.scala b/chipmunk/src/spi/SpiIO.scala new file mode 100644 index 0000000..68da9c8 --- /dev/null +++ b/chipmunk/src/spi/SpiIO.scala @@ -0,0 +1,21 @@ +package chipmunk +package spi + +import chisel3.* + +/** Four-wire SPI IO. + * + * @param hasMisoValid + * whether the MISO signal has a valid signal for tri-state IO-cell control. Note that only the Slave-side SPI may + * have this signal. + */ +class SpiIO(val hasMisoValid: Boolean = false) extends Bundle with IsMasterSlave { + + val sck = Output(Bool()) + val ssn = Output(Bool()) + val mosi = Output(Bool()) + val miso = Input(Bool()) + val misoValid = if (hasMisoValid) Some(Input(Bool())) else None + + def isMaster = true +} diff --git a/chipmunk/test/src/spi/SpiDebuggerSpec.scala b/chipmunk/test/src/spi/SpiDebuggerSpec.scala new file mode 100644 index 0000000..5bddddf --- /dev/null +++ b/chipmunk/test/src/spi/SpiDebuggerSpec.scala @@ -0,0 +1,420 @@ +package chipmunk.test.spi + +import scala.collection.mutable + +import chisel3.* +import circt.stage.ChiselStage + +import chipmunk.* +import chipmunk.acorn.{AcornIO, AcornParams} +import chipmunk.spi.{SpiDebugger, SpiIO} +import chipmunk.spi.SpiDebugger.{Registers, Status} +import chipmunk.test.ChipmunkFlatSpec + +private object SpiDebuggerSpecDut { + // The module clock only advances simulation time; the debugger uses a separately driven system clock. + final class Harness(cpol: Boolean, cpha: Boolean, addressWidth: Int = 32, hasMisoValid: Boolean = true) + extends Module { + val io = IO(new Bundle { + val systemLevel = Input(Bool()) + val spi = Slave(new SpiIO(hasMisoValid)) + val bus = Master(new AcornIO(AcornParams(32, addressWidth))) + }) + private val debugger = withClock(io.systemLevel.asClock) { + Module(new SpiDebugger(hasMisoValid, cpol, cpha, addressWidth)) + } + debugger.io.sSpi :<>= io.spi + io.bus :<>= debugger.io.mDbg + } +} + +final class SpiDebuggerSpec extends ChipmunkFlatSpec { + private case class Request(read: Boolean, address: BigInt, data: BigInt, strobe: BigInt) + private case class Response(due: Int, data: BigInt, error: Boolean) + + private final class Driver(dut: SpiDebuggerSpecDut.Harness, cpol: Boolean, cpha: Boolean) { + // Four logical ticks per system period allow quarter-period SCK phase sweeps. + private val systemPeriod = 4 + private var time, cycle = 0 + private var resetting = false + private val reads = mutable.Queue.empty[Response] + private val writes = mutable.Queue.empty[Response] + private var heldRead, heldWrite = Option.empty[Seq[BigInt]] + val requests = mutable.ArrayBuffer.empty[Request] + val memory = mutable.Map.empty[BigInt, BigInt] + var readReady, writeReady, returnReads, returnWrites = true + var zeroCycle = false + var responseDelay = 2 + var readError, writeError = false + var responseCount = 0 + var idleTicks, activeTicks = 16 // 8:1 system/SCK frequency ratio, 50% duty cycle. + + private def bit(signal: Bool): Boolean = signal.peek().litToBoolean + private def number(signal: UInt): BigInt = signal.peek().litValue + def expectedRead(address: BigInt): BigInt = + memory.getOrElse(address, BigInt("81234567", 16) ^ (address & BigInt("ffffffff", 16))) + + dut.io.systemLevel #= false.B + dut.io.spi.ssn #= true.B + dut.io.spi.sck #= cpol.B + dut.io.spi.mosi #= false.B + reset() + + private def systemRise(): Unit = { + dut.io.bus.rd.cmd.ready #= (readReady && !resetting).B + dut.io.bus.wr.cmd.ready #= (writeReady && !resetting).B + val readFire = !resetting && readReady && bit(dut.io.bus.rd.cmd.valid) + val writeFire = !resetting && writeReady && bit(dut.io.bus.wr.cmd.valid) + val readAddress = number(dut.io.bus.rd.cmd.bits.addr) + val writeAddress = number(dut.io.bus.wr.cmd.bits.addr) + val writeData = number(dut.io.bus.wr.cmd.bits.data) + val strobe = number(dut.io.bus.wr.cmd.bits.strobe) + val immediateRead = zeroCycle && readFire && returnReads + val immediateWrite = zeroCycle && writeFire && returnWrites + val readValid = !resetting && returnReads && (reads.headOption.exists(_.due <= cycle) || immediateRead) + val writeValid = !resetting && returnWrites && (writes.headOption.exists(_.due <= cycle) || immediateWrite) + val rdResponse = reads.headOption.getOrElse(Response(cycle, expectedRead(readAddress), readError)) + val wrResponse = writes.headOption.getOrElse(Response(cycle, 0, writeError)) + dut.io.bus.rd.rsp.valid #= readValid.B + dut.io.bus.rd.rsp.bits.data #= rdResponse.data.U + dut.io.bus.rd.rsp.bits.error #= rdResponse.error.B + dut.io.bus.wr.rsp.valid #= writeValid.B + dut.io.bus.wr.rsp.bits.error #= wrResponse.error.B + + val rdPayload = Seq(readAddress) + val wrPayload = Seq(writeAddress, writeData, strobe) + heldRead.foreach { expected => + assert(bit(dut.io.bus.rd.cmd.valid) && rdPayload == expected, "Read request changed while stalled") + } + heldWrite.foreach { expected => + assert(bit(dut.io.bus.wr.cmd.valid) && wrPayload == expected, "Write request changed while stalled") + } + heldRead = Option.when(!resetting && bit(dut.io.bus.rd.cmd.valid) && !readReady)(rdPayload) + heldWrite = Option.when(!resetting && bit(dut.io.bus.wr.cmd.valid) && !writeReady)(wrPayload) + val readResponse = readValid && bit(dut.io.bus.rd.rsp.ready) + val writeResponse = writeValid && bit(dut.io.bus.wr.rsp.ready) + + dut.io.systemLevel #= true.B + dut.clock.step(0) + if (readResponse && reads.nonEmpty) reads.dequeue() + if (writeResponse && writes.nonEmpty) writes.dequeue() + if (readResponse) responseCount += 1 + if (writeResponse) responseCount += 1 + if (readFire) { + requests += Request(true, readAddress, 0, 0) + if (!immediateRead) reads.enqueue(Response(cycle + responseDelay, expectedRead(readAddress), readError)) + } + if (writeFire) { + requests += Request(false, writeAddress, writeData, strobe) + var result = memory.getOrElse(writeAddress, BigInt(0)) + for (byte <- 0 until 4 if strobe.testBit(byte)) { + val mask = BigInt(255) << (byte * 8) + result = (result & ~mask) | (writeData & mask) + } + if (!writeError) memory(writeAddress) = result + if (!immediateWrite) writes.enqueue(Response(cycle + responseDelay, 0, writeError)) + } + cycle += 1 + } + + def tick(ticks: Int = 1): Unit = { + for (_ <- 0 until ticks) { + dut.clock.step(cycles = 1, period = 2) + time += 1 + if (time % systemPeriod == 2) systemRise() + else if (time % systemPeriod == 0) { + dut.io.systemLevel #= false.B + dut.clock.step(0) + } + } + } + + def reset(): Unit = { + resetting = true + reads.clear() + writes.clear() + heldRead = None + heldWrite = None + dut.io.spi.ssn #= true.B + dut.io.spi.sck #= cpol.B + dut.reset #= true.B + tick(12) + dut.reset #= false.B + resetting = false + tick(12) + } + + def begin(phase: Int = -1): Unit = { + if (phase >= 0) tick((phase - time % systemPeriod + systemPeriod) % systemPeriod) + dut.io.spi.ssn #= false.B + dut.io.spi.misoValid.foreach(_ expect true.B) + tick(16) + } + def end(): Unit = { + dut.io.spi.ssn #= true.B + dut.io.spi.misoValid.foreach(_ expect false.B) + tick(16) + } + + def transfer(data: BigInt, bits: Int): BigInt = { + var result = BigInt(0) + for (index <- (0 until bits).reverse) { + dut.io.spi.mosi #= data.testBit(index).B + if (!cpha) tick(idleTicks) + dut.io.spi.sck #= (!cpol).B + if (!cpha) result = (result << 1) | (if (bit(dut.io.spi.miso)) 1 else 0) + tick(activeTicks) + dut.io.spi.sck #= cpol.B + if (cpha) { + result = (result << 1) | (if (bit(dut.io.spi.miso)) 1 else 0) + tick(idleTicks) + } + } + result + } + def writeRegister(index: Int, data: BigInt, phase: Int = -1): Unit = { + begin(phase) + transfer(index, 8) + transfer(data, 32) + end() + } + def readRegister(index: Int, phase: Int = -1): BigInt = { + begin(phase) + transfer(0x40 | index, 8) + val result = transfer(0, 32) + end() + result + } + def busWrite(address: BigInt, data: BigInt): Unit = { + begin() + transfer(0x80, 8) + transfer(address, 32) + transfer(data, 32) + end() + } + def busRead(address: BigInt): BigInt = { + begin() + transfer(0xc0, 8) + transfer(address, 32) + transfer(0, 8) + val result = transfer(0, 32) + end() + result + } + } + + for (cpol <- Seq(false, true); cpha <- Seq(false, true)) { + s"SpiDebugger mode ${2 * (if (cpol) 1 else 0) + (if (cpha) 1 else 0)}" should + "transfer complete words at 8:1 across system-clock phases and preserve register indices" in { + simulate(new SpiDebuggerSpecDut.Harness(cpol, cpha, addressWidth = 45)) { dut => + val spi = new Driver(dut, cpol, cpha) + assert(spi.readRegister(Registers.TEST) == BigInt("3f1b00e5", 16)) + assert(spi.readRegister(Registers.BUS_WR_MASK) == 15) + val patterns = Seq("80000001", "13579bdf", "deadbeef", "ffffffff").map(BigInt(_, 16)) + for (phase <- 0 until 4) { + spi.writeRegister(Registers.TEST, patterns(phase), phase) + assert(spi.readRegister(Registers.TEST, phase) == patterns(phase)) + spi.writeRegister(Registers.BUS_ADDR_H, 0x123) + spi.writeRegister(Registers.BUS_WR_MASK, 5) + val address = BigInt(0x100 + phase * 4) + val wideAddress = (BigInt(0x123) << 32) | address + spi.busWrite(address, patterns(phase)) + assert(spi.requests.last == Request(false, wideAddress, patterns(phase), 5)) + assert(spi.busRead(address) == spi.memory(wideAddress)) + assert(spi.requests.last == Request(true, wideAddress, 0, 0)) + assert(spi.readRegister(Registers.STATUS) == Status.RD_DONE) + } + // Unequal half periods and a non-integral frequency ratio also sweep edge phase during the word. + spi.idleTicks = 17 + spi.activeTicks = 19 + spi.writeRegister(Registers.TEST, BigInt("a55a8001", 16)) + assert(spi.readRegister(Registers.TEST) == BigInt("a55a8001", 16)) + assert(spi.responseCount == spi.requests.size) + } + } + } + + "SpiDebugger" should "hold snapshots through stalls and reject triggers until the response completes" in { + simulate(new SpiDebuggerSpecDut.Harness(false, true)) { dut => + val spi = new Driver(dut, false, true) + spi.writeReady = false + spi.returnWrites = false + spi.busWrite(0x100, BigInt("fedcba98", 16)) + dut.io.bus.wr.cmd.valid expect true.B + spi.writeRegister(Registers.BUS_ADDR_L, 0x240) + spi.writeRegister(Registers.BUS_WR_DATA, 0x13579) + spi.writeRegister(Registers.BUS_WR_MASK, 3) + assert(spi.busRead(0x300) == SpiDebugger.earlyReadValue) + assert(spi.readRegister(Registers.STATUS) == (Status.BUSY | Status.REJECTED | Status.EARLY_READ)) + assert(spi.requests.isEmpty) + spi.writeReady = true + spi.tick(32) + assert(spi.requests.toSeq == Seq(Request(false, 0x100, BigInt("fedcba98", 16), 15))) + dut.io.bus.wr.cmd.valid expect false.B + spi.writeRegister(Registers.BUS_RD_RESP, 0) + assert(spi.requests.size == 1) + assert((spi.readRegister(Registers.STATUS) & Status.BUSY) != 0) + spi.returnWrites = true + spi.tick(32) + assert(spi.readRegister(Registers.STATUS) == (Status.WR_DONE | Status.REJECTED | Status.EARLY_READ)) + spi.writeRegister(Registers.BUS_ADDR_L, 0x100) + spi.writeRegister(Registers.BUS_RD_RESP, 0) + assert(spi.readRegister(Registers.STATUS) == Status.RD_DONE) + assert(spi.readRegister(Registers.BUS_RD_DATA) == BigInt("fedcba98", 16)) + spi.writeRegister(Registers.BUS_WR_RESP, 0) + assert(spi.requests.last == Request(false, 0x100, 0x13579, 3)) + assert(spi.readRegister(Registers.STATUS) == Status.WR_DONE) + } + } + + it should "return an EARLY_READ snapshot and retain a response arriving during SPI transmission" in { + simulate(new SpiDebuggerSpecDut.Harness(true, false)) { dut => + val spi = new Driver(dut, true, false) + spi.returnReads = false + spi.begin() + spi.transfer(0xc0, 8) + spi.transfer(0x400, 32) + spi.transfer(0, 8) + val firstByte = spi.transfer(0, 8) + spi.returnReads = true + val remainder = spi.transfer(0, 24) + spi.end() + assert((firstByte << 24 | remainder) == SpiDebugger.earlyReadValue) + assert(spi.readRegister(Registers.STATUS) == (Status.RD_DONE | Status.EARLY_READ)) + assert(spi.readRegister(Registers.BUS_RD_DATA) == spi.expectedRead(0x400)) + spi.memory(BigInt(0x404)) = SpiDebugger.earlyReadValue + assert(spi.busRead(0x404) == SpiDebugger.earlyReadValue) + assert(spi.readRegister(Registers.STATUS) == Status.RD_DONE) + assert(spi.requests.size == 2 && spi.responseCount == 2) + } + } + + it should "discard incomplete writes, ignore extra clocks and keep submitted reads across deselection" in { + simulate(new SpiDebuggerSpecDut.Harness(false, false)) { dut => + val spi = new Driver(dut, false, false) + for (bits <- Seq(0, 1, 7)) { + spi.begin() + spi.transfer(BigInt(Registers.TEST) >> (8 - bits), bits) + spi.end() + } + for (bits <- Seq(0, 1, 15, 31)) { + spi.begin() + spi.transfer(Registers.TEST, 8) + spi.transfer(BigInt("12345678", 16) >> (32 - bits), bits) + spi.end() + assert(spi.readRegister(Registers.TEST) == BigInt("3f1b00e5", 16)) + } + for (bits <- Seq(0, 1, 31)) { + spi.begin() + spi.transfer(0x80, 8) + spi.transfer(0x100, 32) + spi.transfer(BigInt("ffffffff", 16) >> (32 - bits), bits) + spi.end() + } + spi.begin() + spi.transfer(0xc0, 8) + spi.transfer(0x100 >> 1, 31) + spi.end() + assert(spi.requests.isEmpty) + spi.begin() + spi.transfer(0x80, 8) + spi.transfer(0x100, 32) + spi.transfer(BigInt("80000001", 16), 32) + spi.transfer(0x80, 8) + spi.transfer(0x200, 32) + spi.transfer(0xffff, 32) + spi.end() + assert(spi.requests.toSeq == Seq(Request(false, 0x100, BigInt("80000001", 16), 15))) + spi.returnReads = false + spi.begin() + spi.transfer(0xc0, 8) + spi.transfer(0x100, 32) + spi.end() + assert(spi.requests.size == 2) + assert(spi.readRegister(Registers.STATUS) == Status.BUSY) + spi.returnReads = true + spi.tick(32) + assert(spi.readRegister(Registers.STATUS) == Status.RD_DONE) + assert(spi.readRegister(Registers.BUS_RD_DATA) == BigInt("80000001", 16)) + } + } + + it should "complete bad addresses locally, preserve overflow checks and cancel pending state on reset" in { + simulate(new SpiDebuggerSpecDut.Harness(false, false, addressWidth = 12)) { dut => + val spi = new Driver(dut, false, false) + for (address <- Seq(3, 0x1000)) { + assert(spi.busRead(address) == 0) + assert(spi.readRegister(Registers.BUS_RD_RESP) == 1) + assert(spi.readRegister(Registers.STATUS) == Status.RD_DONE) + spi.busWrite(address, 123) + assert(spi.readRegister(Registers.BUS_WR_RESP) == 1) + assert(spi.readRegister(Registers.STATUS) == Status.WR_DONE) + } + spi.writeRegister(Registers.BUS_ADDR_L, 0x1000) + assert(spi.readRegister(Registers.BUS_ADDR_L) == 0) + spi.writeRegister(Registers.BUS_WR_RESP, 1) + assert(spi.readRegister(Registers.BUS_WR_RESP) == 1) + assert(spi.requests.isEmpty) + assert(spi.readRegister(Registers.BUS_ADDR_H) == 0) + assert(spi.readRegister(8) == 0) + spi.writeRegister(Registers.BUS_ADDR_L, 0x100) + spi.writeRegister(Registers.BUS_WR_MASK, 0) + spi.writeRegister(Registers.BUS_WR_RESP, 1) + assert(spi.requests.last == Request(false, 0x100, 123, 0)) + spi.writeReady = false + spi.busWrite(0x200, 9) + dut.io.bus.wr.cmd.valid expect true.B + spi.reset() + dut.io.bus.wr.cmd.valid expect false.B + assert(spi.readRegister(Registers.STATUS) == 0) + assert(spi.readRegister(Registers.BUS_WR_MASK) == 15) + assert(spi.readRegister(Registers.TEST) == BigInt("3f1b00e5", 16)) + spi.writeReady = true + spi.busWrite(0x200, 9) + assert(spi.requests.last == Request(false, 0x200, 9, 15)) + spi.readError = true + assert(spi.busRead(0x200) == 0) + assert(spi.readRegister(Registers.BUS_RD_RESP) == 1) + spi.writeError = true + spi.busWrite(0x200, 10) + assert(spi.readRegister(Registers.BUS_WR_RESP) == 1) + } + } + + it should "reject upper-address overflow and accept the highest representable aligned address" in { + simulate(new SpiDebuggerSpecDut.Harness(true, true, addressWidth = 45)) { dut => + val spi = new Driver(dut, true, true) + spi.writeRegister(Registers.BUS_ADDR_H, 0x2000) + spi.writeRegister(Registers.BUS_RD_RESP, 0) + assert(spi.readRegister(Registers.BUS_RD_RESP) == 1) + assert(spi.requests.isEmpty) + assert(spi.busRead(0xfffffffcL) == 0) + assert(spi.requests.isEmpty) + spi.writeRegister(Registers.BUS_ADDR_H, 0x1fff) + spi.writeRegister(Registers.BUS_ADDR_L, 0xfffffffcL) + spi.writeRegister(Registers.BUS_RD_RESP, 0) + assert(spi.requests.last.address == (BigInt(1) << 45) - 4) + assert(spi.readRegister(Registers.STATUS) == Status.RD_DONE) + } + } + + it should "handle same-cycle responses and 64-bit addresses without an optional MISO enable" in { + simulate(new SpiDebuggerSpecDut.Harness(false, true, addressWidth = 64, hasMisoValid = false)) { dut => + val spi = new Driver(dut, false, true) + spi.zeroCycle = true + spi.writeRegister(Registers.BUS_ADDR_H, 0xffffffffL) + spi.busWrite(0xfffffffcL, BigInt("01234567", 16)) + assert(spi.busRead(0xfffffffcL) == BigInt("01234567", 16)) + assert(spi.requests.forall(_.address == (BigInt(1) << 64) - 4)) + assert(spi.responseCount == 2) + assert(spi.readRegister(Registers.STATUS) == Status.RD_DONE) + } + } + + it should "validate address widths and elaborate the minimum width" in { + for (width <- Seq(0, 1, 65)) { + intercept[IllegalArgumentException] { ChiselStage.emitCHIRRTL(new SpiDebugger(busAddrWidth = width)) } + } + assert(ChiselStage.emitCHIRRTL(new SpiDebugger(busAddrWidth = 2)).contains("module SpiDebugger")) + } +} diff --git a/docs/spi-debugger.md b/docs/spi-debugger.md index bd6d30d..159b698 100644 --- a/docs/spi-debugger.md +++ b/docs/spi-debugger.md @@ -2,13 +2,15 @@ SPI Debugger 允许用户通过 SPI 接口访问片上总线,从而调试 SoC 的各种外设。 -它的输入侧端口是一组 SPI 的 Slave 接口,输入侧端口是一组 Acorn Dp Bus 的 Master 接口,后者可以容易地转换成 AXI 等接口连接到SoC 总线上。 +`chipmunk.spi.SpiDebugger` 的 `sSpi` 是 SPI Slave 接口,`mDbg` 是 32 位 Acorn Master 接口,可以经桥接连接到 AXI 总线。SPI 使用 MSB first,`cpol`、`cpha` 参数选择四种标准模式。 + +Acorn 地址以字节为单位,要求 4 字节对齐;`busAddrWidth` 支持 2~64 位。SPI 命令里的 6 位寄存器索引保持不变,内部 RegBank 字节地址为 `index * 4`。 ## Register Map SPI Debugger 内部寄存器定义如下: -### `BUS_ADDR_H` (0x00) +### `BUS_ADDR_H` (0x01) 该寄存器仅当配置选项 `busAddrWidth` > 32 时才会存在,且只有其低 (`busAddrWidth`-32) bit 可被访问。 @@ -20,7 +22,7 @@ SPI Debugger 内部寄存器定义如下: 表中的例子是 `busAddrWidth` = 45 时的情况。 -### `BUS_ADDR_L` (0x01) +### `BUS_ADDR_L` (0x00) 当配置选项 `busAddrWidth` > 32 时,寄存器 `BUS_ADDR_L` 与该寄存器的值组合在一起作为总线访问地址;否则,该寄存器的低 `busAddrWidth` bit 作为总线访问地址(`wr/rd.cmd.bits.addr`)。 @@ -30,7 +32,7 @@ SPI Debugger 内部寄存器定义如下: ### `BUS_WR_RESP` (`0x02`) -读取该寄存器,其最低 bit 表示上一次总线写操作是否成功(`wr.resp.bits.status`),0 表示成功,1 表示有错误发生。 +读取该寄存器,其最低 bit 表示上一次总线写操作的错误状态(`wr.rsp.bits.error`),0 表示成功,1 表示有错误发生。 | Field | Bit width | Bit slice | Access | |---------------|-----------|-----------|--------| @@ -40,7 +42,7 @@ SPI Debugger 内部寄存器定义如下: ### `BUS_RD_RESP` (`0x03`) -读取该寄存器,其最低 bit 表示上一次总线读操作是否成功(`rd.resp.bits.status`),0 表示成功,1 表示有错误发生。 +读取该寄存器,其最低 bit 表示上一次总线读操作的错误状态(`rd.rsp.bits.error`),0 表示成功,1 表示有错误发生。 | Field | Bit width | Bit slice | Access | |---------------|-----------|-----------|--------| @@ -50,7 +52,7 @@ SPI Debugger 内部寄存器定义如下: ### `BUS_WR_DATA` (`0x04`) -该寄存器的值作为总线写操作的数据(`wr.cmd.bits.wdata`)。 +该寄存器的值作为总线写操作的数据(`wr.cmd.bits.data`)。 | Field | Bit width | Bit slice | Access | |---------------|-----------|-----------|--------| @@ -58,7 +60,7 @@ SPI Debugger 内部寄存器定义如下: ### `BUS_RD_DATA` (`0x05`) -读取该寄存器,将返回上一次总线读操作的返回数据(`rd.resp.bits.rdata`)。 +读取该寄存器,将返回上一次总线读操作的数据(`rd.rsp.bits.data`)。发生读错误时保存零值。 | Field | Bit width | Bit slice | Access | |---------------|-----------|-----------|--------| @@ -66,15 +68,33 @@ SPI Debugger 内部寄存器定义如下: ### `BUS_WR_MASK` (`0x06`) -该寄存器的值作为总线写操作的数据掩码(`wr.cmd.bits.wmask`)。 +该寄存器的值作为总线写操作的数据掩码(`wr.cmd.bits.strobe`),复位值为 `0xf`。零掩码仍发起总线写请求。 | Field | Bit width | Bit slice | Access | |---------------|-----------|-----------|--------| | `BUS_WR_MASK` | 4 | 3:0 | R/W | +### `STATUS` (`0x07`) + +| Field | Bit | Access | Meaning | +|---|---:|---|---| +| BUSY | 0 | RO | 请求已锁存,命令或响应尚未完成 | +| RD_DONE | 1 | RO | 最近获准的读操作已经完成,包括本地地址检查失败 | +| WR_DONE | 2 | RO | 最近获准的写操作已经完成,包括本地地址检查失败 | +| REJECTED | 3 | RO | 忙时收到新触发,该触发被拒绝 | +| EARLY_READ | 4 | RO | 快速读的数据阶段开始时,本次结果尚不可用 | + +每次新请求获准时清除 DONE、REJECTED 和 EARLY_READ,之后相应标志保持到下一次获准请求或复位;读取 STATUS 不清除标志。忙时被拒绝的请求不替换正在执行的请求,也不修改其返回结果。 + +读写共用一个未完成事务槽,请求获准时锁存地址、数据和 strobe。之后可以改写暂存寄存器,正在等待握手的 Acorn 命令保持不变。RESP 中的错误位在操作完成时更新,不能单独用它判断是否完成。 + +任意总线延迟下的读流程:等待 BUSY 清零,写入地址,写 BUS_RD_RESP 触发,检查 REJECTED 并轮询 RD_DONE,最后读取 BUS_RD_RESP 和 BUS_RD_DATA。写操作对应轮询 WR_DONE。 + +地址不对齐或超出配置宽度时,不向 Acorn 发命令,在本地设置 DONE 和对应 RESP 错误位。窄地址寄存器中未实现的高位读为零,但写入的溢出信息会保留,直到重写对应地址部分;不会静默访问截断后的地址。 + ### `TEST` (`0x3F`) -该寄存器用于 SPI 的 Loopback 读写测试,其值对总线不会有任何影响。 +该寄存器用于 SPI 的 Loopback 读写测试,复位值为 `0x3f1b00e5`,其值对总线不会有任何影响。 | Field | Bit width | Bit slice | Access | |--------|-----------|-----------|--------| @@ -117,7 +137,11 @@ SPI Debugger 支持的 Command 包括: 其传输序列为:`Command` (8b) + `Addr` (32b) + `Dummy` (8b) + `RdData` (32b) -在 `Addr` 阶段,Master 需要将 32 bit 的待读地址发送给 SPI Debugger。SPI Debugger 会将其填入寄存器 `BUS_ADDR_L`,然后立刻发起一次总线读操作。在 `RdData` 阶段,SPI Debugger 将总线返回的读出数据通过 MISO 线发送给 SPI Master。 +在 `Addr` 阶段,Master 发送 32 bit 的地址低位。SPI Debugger 更新 `BUS_ADDR_L`,结合可选的 `BUS_ADDR_H` 发起一次总线读操作。 + +收齐 8 bit Dummy 时锁存本次结果,整字发送期间不再改变。如果本次结果尚未到达,或者请求因忙被拒绝,则发送 `0xDEAD_BEEF` 并设置 EARLY_READ。迟到的响应仍更新 BUS_RD_DATA 和 RD_DONE,可随后通过寄存器读取;不会返回上一次事务的数据。 + +`0xDEAD_BEEF` 也可能是合法的总线数据,应通过 STATUS 判断。固定 Dummy 时间不保证任意总线响应延迟,推荐使用上面的轮询读流程。 ### `NOP` @@ -129,4 +153,7 @@ SPI Debugger 支持的 Command 包括: 1. 即使需要连续多次访问,期间也必须先拉高 SSN 结束当前 SPI 传输,再拉低 SSN 开始下一次传输; 2. 在拉高 SSN 结束当前 SPI 传输前,请务必确定已经完成了整个操作序列所需的数据传输(即等待足够的时间); -3. SPI 的传输速率不宜过高,原则上 SCK 的频率应当比系统时钟至少低四倍。 +3. 系统时钟频率至少为 SCK 的 8 倍,假定 SCK 占空比接近 50%,且 IO 建立、保持及传播延迟有足够余量。占空比偏离时需按较短的半周期重新评估。这是主机使用条件,RTL 不强制检查外部时序;SSN 必须在首个采样边沿前被同步逻辑观察到,并保持到最后一个采样边沿之后。 +4. REG_WR 的数据未收齐不写目标寄存器,BUS_WR 的数据未收齐不发总线写请求;快速命令收齐地址字后仍会更新 BUS_ADDR_L。总线读在地址收齐后发起,此后撤销 SSN 不取消请求。完整命令后的多余 SCK 被忽略,下一条命令需重新拉高、拉低 SSN。 +5. 复位会取消本模块的未完成事务,必须同步复位相关 Acorn 端点。片选撤销不会取消事务;模块没有总线超时取消机制。 +6. `hasMisoValid` 为真时,misoValid 直接由原始 SSN 限定,供 IO-cell 控制输出使能;实际三态由 IO-cell 实现。 From 9851b93dc9a5d3ee8ce3a44caf98eda4900d9132 Mon Sep 17 00:00:00 2001 From: Haozhe Zhu Date: Tue, 6 Oct 2026 21:38:23 +0800 Subject: [PATCH 26/26] docs: refresh component references for 0.2.0-SNAPSHOT Update the README and reference pages to match the current APIs and behavior. Add documentation for state machines, reset synchronization, AMBA bridges, flat RTL ports, and ChiselSim helpers. Document Acorn width adaptation and register-field access rules. Keep installation instructions focused on Mill. Set the publish version to 0.2.0-SNAPSHOT. Omit the Scala 3 migration guide and Scala 2 maintenance discussion. Validation: compile 44 documentation excerpts, elaborate 42 hardware examples, and check all 56 internal documentation links. --- README.md | 201 +++++++++++++++++++++--------------------- build.mill | 2 +- docs/README.md | 27 ++++-- docs/acorn.md | 49 ++++++++-- docs/amba.md | 90 +++++++++++++++++++ docs/bits-misc.md | 60 ++++++++++--- docs/master-slave.md | 77 +++++++--------- docs/regbank.md | 54 +++++++++++- docs/regneg.md | 67 ++++++++------ docs/reset-sync.md | 38 ++++++++ docs/spi-debugger.md | 30 +++++-- docs/state-machine.md | 60 +++++++++++++ docs/stream.md | 80 +++++++++++++---- docs/tester.md | 67 ++++++++++++++ docs/verilog-io.md | 41 +++++++++ 15 files changed, 719 insertions(+), 224 deletions(-) create mode 100644 docs/amba.md create mode 100644 docs/reset-sync.md create mode 100644 docs/state-machine.md create mode 100644 docs/tester.md create mode 100644 docs/verilog-io.md diff --git a/README.md b/README.md index 75dcbd2..9b6035b 100644 --- a/README.md +++ b/README.md @@ -4,147 +4,146 @@ ![Build & Test](https://github.com/zhutmost/chipmunk/actions/workflows/ci.yml/badge.svg?branch=main) -CHIPMUNK is a Scala package to extend the functionality of [CHISEL](https://chisel-lang.org). It features: -- Extra convenient methods for CHISEL's built-in types, -- Several syntactic sugar to sweeten your CHISEL experience, -- A set of commonly used components and interfaces. +CHIPMUNK extends [Chisel](https://chisel-lang.org) with convenient hardware APIs, interfaces, and reusable components: -**WARNING**: The code contained in this repo are provided AS IS, and I cannot make any guarantees for availability and correctness. Most of them have only been silicon-verified in academic research (and some even not). You should carefully review every line of code before using it in production. +- Bits/Data extensions, priority selection, Master/Slave directions, and flat RTL interfaces. +- Falling-edge registers, enum-based state machines, and asynchronous-assert/synchronous-deassert reset synchronization. +- Stream/Flow pipelines, queues, routing, and arbitration. +- Acorn routing, SRAM and width adapters, register banks, AXI4/AXI4-Lite bridges, and an SPI debugger. +- ChiselSim helpers for port stimulus and multiple simulated clocks. + +**WARNING**: This code is provided AS IS, without guarantees of availability or correctness. Review and verify each component in its intended integration before production use. Simulation coverage does not establish silicon verification of the current implementation. Please open an issue if you have any questions. -## Installation +## Build and installation -CHIPMUNK is an extension of Chisel, so it needs to be used together with CHISEL. +This source tree targets Scala 3. The versions currently selected in [build.mill](build.mill) are: -Use the bundled [Mill](https://mill-build.org) bootstrap script to build and publish CHIPMUNK. +| Component | Version | +| --- | --- | +| CHIPMUNK | `0.2.0-SNAPSHOT` | +| Scala | `3.8.4` | +| Chisel library and compiler plugin | `7.16.0+3-a2d66312-SNAPSHOT` | +| ScalaTest | `3.2.20` | +| Bundled Mill launcher default | `1.1.9` | -```shell -./mill chipmunk.publishLocal -``` +Use the same Chisel version for the library and compiler plugin, with the plugin cross-published for the full Scala version. The snapshot repository is configured in `build.mill`; replacing this dependency with another Chisel build requires checking both Scala 3 artifacts and compiler-plugin compatibility. -Then add CHIPMUNK to your build file. -```scala -// Mill -def ivyDeps = Agg(..., ivy"com.zhutmost::chipmunk:0.1-SNAPSHOT") -// SBT -libraryDependencies ++= Seq(..., "com.zhutmost" %% "chipmunk" % "0.1-SNAPSHOT") -``` +Run commands from the repository root. Tests use Verilator and require it on `PATH`. -Import it as well as `chisel3` in your Scala RTL code. -```scala -import chisel3._ -import chisel3.util._ -import chipmunk._ +```shell +./mill __.compile +./mill chipmunk.test +./mill mill.scalalib.scalafmt/checkFormatAll +./mill mylib.run ``` -## Documentation - -CHIPMUNK documents are provided in the [docs folder](docs/README.md). +The last command emits the example design into `generate/hw`. To use CHIPMUNK from another project, first publish this checkout to the local Ivy repository: -### Extra convenient Methods for Chisel types -[View detailed document](docs/bits-misc) - -Code example: -```scala -val myUInt = Wire(UInt(3.W)).dontTouch // equivalent to `DontTouch(...)`, but more convenient -when(...) { - myUInt.setAllTo(someBits.lsBit) // set all bits to true or false -} otherwise { - myUInt.clearAll() -} -val emptyStream = Decoupled(new EmptyBundle) // Bundle without elements +```shell +./mill chipmunk.publishLocal ``` -### Define Bundle Direction with Master/Slave -[View detailed document](docs/master-slave) +This publishes `com.zhutmost:chipmunk_3:0.2.0-SNAPSHOT` locally; it does not publish the package to a public repository. An example Mill 1.x consumer `build.mill` is: -Code example: ```scala -class AxiIO extends Bundle with IsMasterSlave { - val aw = Master(new AxiWriteAddrChannelIO) - val ar = Master(new AxiReadAddrChannelIO) - val r = Slave(new AxiReadDataChannelIO) - val w = Master(new AxiWriteDataChannelIO) - val b = Slave(new AxiWriteRespChannelIO) - def isMaster = true // indicate this bundle is a Master -} - -class AxiSlave extends Module { - val io = IO(new Bundle { - val axi = Slave(new AxiIO) // automatically flip the signal directions - }) - // ... +package build + +import mill.scalalib.* + +object rtl extends ScalaModule { + def scalaVersion = "3.8.4" + val chiselVersion = "7.16.0+3-a2d66312-SNAPSHOT" + + override def repositories = super.repositories() ++ Seq( + "ivy2Local", + "https://central.sonatype.com/repository/maven-snapshots" + ) + override def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.chipsalliance::chisel:$chiselVersion", + mvn"com.zhutmost::chipmunk:0.2.0-SNAPSHOT" + ) + override def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ Seq( + mvn"org.chipsalliance:::chisel-plugin:$chiselVersion" + ) } ``` -### Registers triggered on the falling clock edge -[View detailed document](docs/regneg) +See the [Mill publishing guide](https://mill-build.org/mill/1.0.x/javalib/publishing.html) for local publishing options. + +Import the packages needed by your design: -Code example: ```scala -withClockAndReset(clock, reset) { - val regNeg1 = RegNegNext(nextVal) - val regNeg2 = RegNegEnable(nextVal, initVal, enable) -} +import chisel3.* +import chisel3.util.* +import chipmunk.* +import chipmunk.stream.* +import chipmunk.acorn.* +import chipmunk.amba.* +import chipmunk.regbank.* +import chipmunk.spi.* ``` -### Finite State Machine +## Examples -[View detailed document]() +The following hardware snippets belong inside a Chisel Module, with the imports above. -Code example: +Bits/Data extensions: ```scala -val fsm = new StateMachine { - val s1 = new State with EntryPoint - val s2 = new State - s1 - .whenIsActive { - when(io.a) { - goto(s2) - } - } - s2 - .whenIsActive { - when(io.b) { - goto(s1) - } - } -} -io.out := fsm.isExiting(fsm.s2) +val word = Wire(UInt(8.W)).dontTouch +word := word.filledWith(true) +val lowNibble = word.lsBits(4) +val selected = PriorityEncoderDefault(word, default = 8.U(4.W)) ``` -### Asynchronous Reset Synchronous Dessert -[View detailed document]() +A Stream pipeline with a transformed payload: -Code example: ```scala -val reset1 = AsyncResetSyncDessert.withImplicitClockDomain() -val reset2 = AsyncResetSyncDessert.withSpecificClockDomain(clockSys, coreReset, resetChainIn = reset1) +class StreamIncrement extends Module { + val io = IO(new Bundle { + val in = Slave(Stream(UInt(8.W))) + val out = Master(Stream(UInt(8.W))) + }) + io.out << io.in.payloadMap(_ + 1.U).pipeAll() +} ``` -### StreamIO/FlowIO: Decouple Dataflow with Handshake -[View detailed document](docs/stream) +An enum-based state machine: -Code example: ```scala -TODO +object States extends ChiselEnum { + val Idle, Run = Value +} +val fsm = StateMachine(States)(States.Idle) { + on(States.Idle) { + when(start) { goto(States.Run) } + } + on(States.Run) { + when(done) { goto(States.Idle) } + } +} +val busy = fsm.is(States.Run) ``` -### Clock Domain Crossing Blocks -[View detailed document]() +`start` and `done` above are application-provided `Bool` signals. Falling-edge registers use `RegNegNext(next)` or `RegNegEnable(next, enable)`; initialized overloads also require an explicit `isResetAsync` argument. Reset synchronization uses `AsyncResetSync.withSpecificClockDomain(clock, resetAsync, stages = 2)`. -Code example: -```scala -TODO -``` +## Documentation -(Not all above document pages are ready yet.) +The [documentation index](docs/README.md) links all reference pages. Documentation is primarily in Chinese, with some English pages. -I am sorry they are written in Chinese (Machine translation driven by AI is good enough now :D). +| Area | Documentation | +| --- | --- | +| Bits/Data, priority selection, and basic records | [Bits/Data helpers](docs/bits-misc.md) | +| Interface directions and RTL port names | [Master/Slave](docs/master-slave.md), [VerilogIO](docs/verilog-io.md) | +| Registers, state machines, and reset | [RegNeg](docs/regneg.md), [StateMachine](docs/state-machine.md), [AsyncResetSync](docs/reset-sync.md) | +| Stream/Flow | [Handshake and components](docs/stream.md) | +| Acorn | [Protocol, routing, SRAM, and width adaptation](docs/acorn.md) | +| AMBA bridges | [AXI4/AXI4-Lite and Acorn conversion](docs/amba.md) | +| Register endpoints and debug | [RegBank](docs/regbank.md), [SPI debugger](docs/spi-debugger.md) | +| Verification helpers | [ChiselSim and multiple clocks](docs/tester.md) | ## Acknowledgement -CHIPMUNK is standing on the shoulder of giants. -Thanks for [CHISEL](https://chisel-lang.org), [SpinalHDL](https://github.com/SpinalHDL/SpinalHDL) and many other open-sourced projects. +CHIPMUNK is standing on the shoulders of giants. Thanks to [Chisel](https://chisel-lang.org), [SpinalHDL](https://github.com/SpinalHDL/SpinalHDL), and many other open-source projects. diff --git a/build.mill b/build.mill index c28a38c..3a7c05a 100644 --- a/build.mill +++ b/build.mill @@ -42,7 +42,7 @@ trait ChiselModule extends ScalaModule { } object chipmunk extends ChiselModule with PublishModule { - def publishVersion = "0.1-SNAPSHOT" + def publishVersion = "0.2.0-SNAPSHOT" def pomSettings = PomSettings( description = "CHIPMUNK: Enhance CHISEL for Smooth and Comfortable Chip Design", diff --git a/docs/README.md b/docs/README.md index 140051e..e0bcff2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,15 +1,24 @@ # 🐿️ CHIPMUNK Docs -Here are the documents for CHIPMUNK. +These pages describe the current Scala 3 source tree. Build versions, consumer configuration, and commands are in the [project README](../README.md#build-and-installation). -## Chisel Extensions +## Chisel extensions -- [Extra convenient Methods for Chisel types](./bits-misc.md) -- [Define Bundle Direction with Master/Slave](./master-slave.md) -- [Registers triggered on the falling clock edge](./regneg.md) -- [StreamIO/FlowIO: Decouple Dataflow with Handshake](./stream.md) +- [Bits/Data helpers, priority selection, and basic records](bits-misc.md) +- [Master/Slave interface directions](master-slave.md) +- [Flat RTL interfaces with VerilogIO](verilog-io.md) +- [Falling-edge registers](regneg.md) +- [Enum-based state machines](state-machine.md) +- [Asynchronous-assert/synchronous-deassert reset synchronization](reset-sync.md) -## Common Hardware Components +## Interfaces and hardware components -- [Acorn Bus](./acorn.md) -- [SPI Debugger](./spi-debugger.md) +- [Stream/Flow handshake, pipelines, routing, and arbitration](stream.md) +- [Acorn protocol, crossbar, SRAM, and width adaptation](acorn.md) +- [AXI4/AXI4-Lite interfaces and Acorn bridges](amba.md) +- [Register bank](regbank.md) +- [SPI debugger](spi-debugger.md) + +## Verification + +- [ChiselSim helpers and multiple simulated clocks](tester.md) diff --git a/docs/acorn.md b/docs/acorn.md index 2a4d777..520e947 100644 --- a/docs/acorn.md +++ b/docs/acorn.md @@ -8,11 +8,14 @@ Acorn 是用于模块内部地址访问的轻量接口,适合配置寄存器 - 地址以字节为单位,每条命令访问一个完整数据字,并要求自然对齐。 例如,64 位接口的数据字起始地址为 `0x00、0x08、0x10…`。 + - `valid` 生效后,发送方必须保持 `valid` 和整个 payload,直到握手完成。 向下游发送的 valid 不依赖下游 ready 的组合值。 + - 每个接受的命令恰好对应一个响应,包括错误访问。端点不能在没有对应命令时发起响应。 - 每个 master 的读响应按其读命令顺序返回,写响应按其写命令顺序返回。 读写之间及不同 master 之间没有隐含的全局顺序。 + - 写响应表示端点定义的本次写操作已完成。需要“先写后读”时,master 应收到写响应后再发读命令。 - `strobe(i)=1` 使能 `data(8*i+7, 8*i)`。全零 strobe 不修改数据,仍对应一个写响应。 - `error=false` 表示成功,`error=true` 表示失败。Crossbar 的错误端点返回零读数据。 @@ -42,8 +45,9 @@ Acorn 是用于模块内部地址访问的轻量接口,适合配置寄存器 | `AcornCrossbar.scala` | 仲裁与地址映射配置,以及 Crossbar 的路由和地址转换 | | `AcornErrorPoint.scala` | 可独立实例化的错误响应端点 | | `AcornSramAdapter.scala` | 字节掩码同步 SRAM 的 1RW / 1R1W 适配器及配置 | +| `AcornWidthAdapter.scala` | 完整字访问的扩展或拆分,不做读改写 | -配置寄存器端点可使用 [RegBank](regbank.md)。 +`AcornParams.strobeWidth = dataWidth / 8`,表示数据字节数以及写掩码位数;原 `bytesPerWord` 名称已移除。配置寄存器端点可使用 [RegBank](regbank.md),AXI4/AXI4-Lite 转换见 [AMBA 桥接](amba.md)。 Mux 的来源 FIFO 保存命令由哪个 master 发出;Demux 的目标 FIFO 保存命令发往哪个 slave。 它们在命令握手时记录路由,在相应响应握手时移除记录。不同 slave 独立仲裁,可以同时接受不同 master 的访问。 @@ -65,7 +69,7 @@ Mux 的来源 FIFO 保存命令由哪个 master 发出;Demux 的目标 FIFO | `connections` | 可选的每 master 可达 slave 名字集合;None 表示全部连接 | 全局窗口、本地基址以及窗口大小都必须按一个数据字对齐。窗口覆盖范围必须同时适配全局和本地地址宽度。 -一个 slave 当前对应一个连续窗口。交错 bank、多个窗口别名以及数据宽度转换留给独立映射或适配组件。 +一个 slave 当前对应一个连续窗口。交错 bank 和多个窗口别名需要独立映射;数据宽度转换可接 AcornWidthAdapter,Crossbar 本身不转换数据宽度。 ```scala import chisel3.* @@ -184,6 +188,37 @@ SRAM 地址、数据和控制由队列中的命令及本地状态产生。无阻 reset 只用于状态复位,不门控握手或 SRAM 使能。上游在复位期间不得发出命令,并应与 adapter 一致复位。 复位取消在途事务,但不清除 SRAM 内容,也不撤销已经执行的写。 +## AcornWidthAdapter + +Acorn 不携带读 size 或读 strobe,每个读命令访问完整数据字。`AcornWidthAdapter(inputParams, outputParams)` 转换两侧的数据宽度;字节地址宽度必须相同,并足以表示较宽的数据字。当前构造函数没有 `allowReadResize` / `allowWriteResize` 参数,位宽不同即执行对应转换。 + +```scala +val widthAdapter = Module(new AcornWidthAdapter( + inputParams = AcornParams(dataWidth = 32, addrWidth = 16), + outputParams = AcornParams(dataWidth = 64, addrWidth = 16) +)) +// widthAdapter.io.sAcorn <> narrowMaster +// wideEndpoint <> widthAdapter.io.mAcorn +``` + +| 宽度关系 | 读 | 写 | +| --- | --- | --- | +| 输入窄、输出宽 | 访问对齐后的完整宽字,抽取原地址对应的 lane | 扩展 data/strobe 到相应 lane,其他字节 strobe 为零 | +| 输入宽、输出窄 | 按地址递增读取所有子字并合并 | 按地址递增写所有子字,包括 strobe 全零的子写 | +| 两侧相同 | 保持地址和数据,经过事务缓冲 | 同左 | + +输入命令必须按输入字自然对齐;不对齐时在本地返回错误,不访问端点。宽度转换保持低地址对应低字节的 lane 顺序。读写各容纳一个命令,直到上游响应握手才接受下一条;请求与响应均缓冲,读写方向独立。 + +端点报错时停止剩余子访问,读返回 error 和零数据,写返回 error;已经完成的子写不能回滚。不做读改写、访问合并或旧读数据复用。 + +接入前必须由系统设计保证: + +- 扩展后的整个宽字都落在允许访问的端点区域内。窄到宽读会读取原本未请求的相邻字节,可能触发读清零/FIFO 弹出等副作用。 +- 端点允许非原子的拆分读写。宽到窄读不保证同一时刻的快照;拆分写可能只完成一部分,其他 master 可在子访问之间介入。 +- 写副作用符合字节 strobe 语义,并允许不同访问宽度;不能仅因 strobe 全零就假设端点没有任何事务副作用。 + +这些语义无法从 Acorn 命令中自动识别或检查。普通 SRAM 适合宽度适配;有读写副作用的 MMIO 应保持相同位宽,或使用明确了解寄存器语义的专用桥。 + ## 时序和组合约束 - 每个 master 的读响应按其读命令顺序返回,写响应按写命令顺序返回;读写之间和不同 master 之间没有全局顺序。 @@ -192,10 +227,13 @@ reset 只用于状态复位,不门控握手或 SRAM 使能。上游在复位 - Mux 仲裁器只在命令阻塞期间锁定选择;命令接受后可以仲裁其他 master,响应归属由 FIFO 保存。 - 命令可组合通过路由器;路由记录是寄存的。因此即使端点立即提供响应,首次向上游返回也要等路由记录可见。 端点不能等待这次响应握手才接受命令,响应必须能保持到 ready。 + - 记录 FIFO 使用 `pipe=false, flow=false`,满时不会利用同周期响应释放的空间。深度 1 会限制流水吞吐; 按端点延迟增大深度可以提高吞吐,首版默认深度 4。 + - 在 Crossbar 中,master 接受、slave 接受和两层路由记录是同一次命令握手。不要在 Demux 和 Mux 之间任意加入 独立接收命令的 Queue;不同目标上的命令发出顺序变化可能形成循环响应等待。 + - 响应背压和跨目标按序返回会造成队首阻塞。首版不提供 master 之间的完全阻塞隔离。 - 所有组件工作于同一时钟域,连接端点应一致复位;复位取消在途事务。跨时钟桥后续单独设计。 @@ -207,13 +245,14 @@ Mux 的在途上限是所有 master 的合计,Demux 的上限是一个 master 测试覆盖来源归属、阻塞时锁定、跨目标按序返回、非法 selector、非零本地基址进位、全地址空间窗口、 深度 1/4 的多 master 并行读写与随机命令/响应背压,以及配置和访问错误。 -使用项目现有的 `ChipmunkFlatSpec`、`#=` 和 `expect`。 +宽度适配测试还覆盖 lane 顺序、扩展/拆分、局部地址错误、首错停止以及命令/响应背压。使用项目现有的 `ChipmunkFlatSpec`、`#=` 和 `expect`,辅助 API 见 [测试文档](tester.md)。 ```bash -mill chipmunk.test.testOnly \ +./mill chipmunk.test.testOnly \ chipmunk.test.acorn.AcornConfigSpec \ chipmunk.test.acorn.AcornMuxSpec \ chipmunk.test.acorn.AcornDemuxSpec \ chipmunk.test.acorn.AcornCrossbarSpec \ - chipmunk.test.acorn.AcornSramAdapterSpec + chipmunk.test.acorn.AcornSramAdapterSpec \ + chipmunk.test.acorn.AcornWidthAdapterSpec ``` diff --git a/docs/amba.md b/docs/amba.md new file mode 100644 index 0000000..7fb2d84 --- /dev/null +++ b/docs/amba.md @@ -0,0 +1,90 @@ +# 🐿️ AXI4 / AXI4-Lite 与 Acorn 桥接 + +使用 `import chipmunk.amba.*`,方向包装见 [Master/Slave](master-slave.md)。所有桥接模块的两端共用时钟和复位,不承担时钟域转换;复位取消在途事务,需要连接端点一致复位。 + +## 接口参数 + +| 参数 | AXI4 (`AxiParams`) | AXI4-Lite (`AxiLiteParams`) | +| --- | --- | --- | +| `dataWidth` | 8~1024 位的二次幂 | 32 或 64 位 | +| `addrWidth` | 正整数,字节地址宽度 | 正整数,字节地址宽度 | +| `strobeWidth` | `dataWidth / 8`,派生值 | `dataWidth / 8`,派生值 | +| `idWidth` | 默认 0,此时 ID 字段为 `None` | 无 ID | +| `hasQos` / `hasRegion` | 默认 false,启用后为 `Some` 字段 | 无对应字段 | + +`AxiIO(params)` 和 `AxiLiteIO(params)` 包含 AW/W/B/AR/R 五个 Stream 通道,按 master 视角声明。AXI4 的 `fullSize` 是完整数据字对应的 AxSIZE 编码。可选字段必须通过 `foreach`/`get` 等处理,不能当作无条件存在的 UInt。 + +## 桥接选择 + +| 模块 | 输入 → 输出 | 事务与容量 | +| --- | --- | --- | +| `AcornToAxiBridge` | `io.sAcorn` → `io.mAxi` | 单拍完整字访问;读写各一个在途命令,包含响应等待时间 | +| `AcornToAxiLiteBridge` | `io.sAcorn` → `io.mAxiLite` | 复用上面的事务控制,投影为 AXI4-Lite | +| `AxiLiteToAxiBridge` | `io.sAxiLite` → `io.mAxi` | 五通道组合映射,不增加缓冲或延迟 | +| `AxiToAcornBridge` | `io.sAxi` → `io.mAcorn` | 每方向一个 AXI burst,拆成有序 Acorn 命令;内部 Acorn 在途量可配置 | + +这些模块保持地址和数据位宽。需要 Acorn 数据位宽变化时,另外使用 [AcornWidthAdapter](acorn.md#acornwidthadapter)。 + +## Acorn → AXI4 / AXI4-Lite + +```scala +import chisel3.* +import chipmunk.* +import chipmunk.acorn.* +import chipmunk.amba.* + +class AcornAxiBridge extends Module { + val params = AxiParams(dataWidth = 64, addrWidth = 32, idWidth = 4) + val io = IO(new Bundle { + val acorn = Slave(new AcornIO(AcornParams(64, 32))) + val axi = Master(new AxiIO(params)) + }) + val bridge = Module(new AcornToAxiBridge(params, writeId = 1, readId = 2)) + bridge.io.sAcorn :<>= io.acorn + io.axi :<>= bridge.io.mAxi +} +``` + +AW 与 W 独立完成握手;输入命令和返回响应均缓冲,并在背压期间保持稳定。读写独立进行,新命令要等本方向的上一响应被 Acorn 接收。 + +- 仅发送单拍、自然对齐、完整字大小的普通访问:AxLEN=0、AxSIZE=`fullSize`、INCR、LOCK=0。 +- 未对齐的 Acorn 命令在本地产生错误,不访问 AXI;该类读错误返回零。AXI 返回错误时,Acorn 的 data 保留 AXI 返回值,使用方必须检查 error。 +- 字节写掩码原样传递,包括全零 strobe,不做读改写。 +- `writeId` / `readId` 为静态 ID(默认 0);`writeProt` / `readProt` 默认 0,范围为 0~7。CACHE/QoS/REGION 为零。 +- OKAY 映射为成功,其他 RESP 映射为错误。意外的 ID、读响应缺少 RLAST、普通访问返回 EXOKAY 会触发断言。 + +`AcornToAxiLiteBridge(AxiLiteParams(...), writeProt, readProt)` 沿用这些事务语义,接口没有 ID。 + +### 直接桥与经 AXI-Lite 组合的区别 + +`AcornToAxiLiteBridge + AxiLiteToAxiBridge` 使用相同的 Acorn 事务控制,额外的 Lite → AXI 投影没有流水延迟。二者都产生单拍访问,组合方案不会因此获得 burst 或更多在途能力。 + +直接 `AcornToAxiBridge` 支持 AXI4 的全部上述数据宽度,可直接配置 ID/PROT。组合路径受 AXI4-Lite 的 32/64 位限制;ID 在 Lite → AXI 层配置。已有 AXI-Lite 连接点时可使用组合路径,直接接 AXI4 时使用直接桥更简单。 + +## AXI4-Lite → AXI4 + +`AxiLiteToAxiBridge(params, writeId = 0, readId = 0)` 的 `params` 为 `AxiParams`,但数据宽度因 Lite 侧限制只能为 32/64。它组合连接五个通道,AW/W 保持独立,添加固定 ID、单拍完整字大小、INCR、LOCK=0,以及零 CACHE/QoS/REGION。PROT 和 RESP 原样传递。 + +本模块不自行限制在途事务数;连接的 master/slave 决定容量。同一方向使用固定 ID 保持顺序,返回 ID、RLAST、EXOKAY 有相应断言。 + +## AXI4 → Acorn + +`AxiToAcornBridge(params, outstanding = AcornOutstanding())` 支持自然对齐、完整字大小的 INCR/FIXED/WRAP burst,含部分或全零 WSTRB。每个读响应保留原 ARID,写响应保留原 AWID。 + +- 窄访问、未对齐或地址溢出的访问返回 SLVERR,不向 Acorn 发命令。拒绝的读仍返回要求数量的 R beat,data 为零;拒绝的写仍接收整段 W 数据后返回 B。 +- Acorn 读错误逐拍映射为 SLVERR,data 原样返回;写错误在整个 burst 内累积,所有写响应收齐后才生成 B。已经完成的写不会回滚。 +- `outstanding.read` 同时覆盖待返回的 Acorn 读和已缓存但 AXI 尚未接收的 R beat;`outstanding.write` 限制未收齐的 Acorn 写响应。默认各为 4。 +- W 入口有两项队列,可先于 AW 到达。每方向只允许一个活动 burst,读写之间独立,不提供跨方向排序。 +- PROT/CACHE/QoS/REGION 不解释;exclusive 作为普通访问执行,不返回 EXOKAY。不能借此实现 exclusive 原子语义。 +- 超过总线宽度的 AxSIZE、非法 burst 类型/长度、跨 4KB、WRAP 起始对齐错误和不一致 WLAST 属于协议违规,使用断言检查;不能把它们当作可依赖的正常错误处理接口。 + +该桥拆分 burst,不把多个 beat 合并成一个更宽访问,也不为 MMIO 提供额外原子性。 + +源码:[amba 目录](../chipmunk/src/amba)。验证: + +```shell +./mill chipmunk.test.testOnly \ + chipmunk.test.amba.AxiToAcornBridgeSpec \ + chipmunk.test.amba.AxiLiteToAxiBridgeSpec \ + chipmunk.test.amba.AcornToAxiBridgesSpec +``` diff --git a/docs/bits-misc.md b/docs/bits-misc.md index 68bf29b..4459860 100644 --- a/docs/bits-misc.md +++ b/docs/bits-misc.md @@ -6,11 +6,11 @@ `chisel3.Bits` 是 Chisel 的 `UInt`/`SInt`/`Bool` 等类型的父类,因此以下方法可以被这些类型的实例调用。 -这些方法是在 `chipmunk.AddMethodsToBits` 这个隐式类中实现的。 +这些方法由 [DataExtensions.scala](../chipmunk/src/DataExtensions.scala) 中的 Scala 3 extension 实现,通过 `import chipmunk.*` 导入。以下硬件片段位于 Chisel 模块内,并假定已经导入 `chisel3.*` 和 `chisel3.util.*`。 ### `msBits`/`lsBits` — 获取信号的最高/低 n 比特 -`chisel3.Bits` 已提供了类似的方法 `x.head(n)` 和 `x.tail(n)`,其中前者是获取该信号的高 n 比特,后者是去掉该信号的高 n 比特(相当于 `x.head(x.getWidth - n)`)。这两个方法显然是来自函数式编程语言处理队列的习惯(~~Haskell 用户狂喜~~),但和绝大多数硬件工程师的习惯很不一致,且导致代码可读性降低。硬件工程师习惯用“某某信号的高N比特/低N比特”来对多比特信号进行切片。 +`chisel3.Bits` 已提供了类似的方法 `x.head(n)` 和 `x.tail(n)`,其中前者是获取该信号的高 n 比特,后者是去掉该信号的高 n 比特(保留低 `x.getWidth - n` 比特)。这两个方法显然是来自函数式编程语言处理队列的习惯(~~Haskell 用户狂喜~~),但和绝大多数硬件工程师的习惯很不一致,且导致代码可读性降低。硬件工程师习惯用“某某信号的高N比特/低N比特”来对多比特信号进行切片。 因此,Chipmunk 提供了 `x.msBits(n)` 和 `x.lsBits(n)` 两组方法,它们的功能正如方法名(most/least significant bits):前者 `x.msBits(n)` 和 `x.head(n)` 行为类似,后者 `x.lsBits(n)` 则是获取该信号的低 n 比特。 @@ -18,12 +18,15 @@ - `def msBits(n: Int = 1): UInt` 返回当前信号实例的最高 n 比特。 + - `def lsBits(n: Int = 1): UInt` 返回当前信号实例的最低 n 比特。 + - `def msBit: Bool` 返回当前信号实例的最高 1 比特,注意返回类型为 `Bool`,与 `msBits(n)` 不同。 + - `def lsBit: Bool` -返回当前信号实例的最高 1 比特,注意返回类型为 `Bool`,与 `lsBits(n)` 不同。 +返回当前信号实例的最低 1 比特,注意返回类型为 `Bool`,与 `lsBits(n)` 不同。 ### `filledOnes`/`filledZeros`/`filledWith`/`assignOnes`/`assignZeros` — 将信号的所有比特赋 1 或 0 @@ -43,19 +46,23 @@ x2 := ~ 0.U // ~ 0.S if SInt - `def filledWith(b: Bool): T` 返回一个和当前信号相同类型的信号,但该信号的所有比特都是为 `b`。 + - `def filledWith(b: Boolean): T` 返回一个和当前信号相同类型的信号,但该信号的所有比特都是为 b 的 `Bool` 字面量(即`b.B`)。 -- `def filledOnes(): T` + +- `def filledOnes: T` 返回一个和当前信号相同类型的信号,但该信号的所有比特都是为 `true.B`。 -- `def filledZeros(): T` + +- `def filledZeros: T` 返回一个和当前信号相同类型的信号,但该信号的所有比特都是为 `false.B`。 上述方法的共同特点是,它们会返回一个新的信号,不会改变当前信号的赋值。如果需要将一个信号的所有比特都赋值为全 0 或全 1,需要使用 `assignOnes`/`assignZeros`: - `def assignOnes(): Unit`: - 将当前信号的所有比特都赋值为 `true.B`,相当于 `x := x.filledOnes()`。 + 将当前信号的所有比特都赋值为 `true.B`,相当于 `x := x.filledOnes`。 + - `def assignZeros(): Unit`: - 将当前信号的所有比特都赋值为 `false.B`,相当于 `x := x.filledZeros()`。 + 将当前信号的所有比特都赋值为 `false.B`,相当于 `x := x.filledZeros`。 ### `isOneHot` — 判断当前信号是否为独热码(one-hot) @@ -65,11 +72,11 @@ x2 := ~ 0.U // ~ 0.S if SInt `chisel3.Data` 是 Chisel 的各硬件类型的父类。 -这些方法是在 `chipmunk.AddMethodsToData` 这个隐式类中实现的。 +这些方法同样由 `DataExtensions.scala` 的 Scala 3 extension 实现。 -### `dontTouch` — `dontTouch()` 的语法糖 +### `dontTouch` — `chisel3.dontTouch(x)` 的方法形式 -`chisel3.dontTouch` 可以阻止 Chisel 对该信号进行优化,常用于保留某中间信号的命名、保留与模块输出无关的电路等。这是一个单例对象,下面是 Chisel 提供的一个用法等例子: +`chisel3.dontTouch` 可以阻止 Chisel 对该信号进行优化,常用于保留某中间信号的命名、保留与模块输出无关的电路等。这是一个单例对象,下面是 Chisel 提供的一个用法例子: ```scala class MyModule extends Module { val io = IO(new Bundle { @@ -78,7 +85,7 @@ class MyModule extends Module { }) io.b := io.a val dead = RegNext(io.a +% 1.U) // normally dead would be pruned by DCE - dontTouch(dead) // Marking it as such will preserve it + chisel3.dontTouch(dead) // Marking it as such will preserve it } ``` @@ -93,3 +100,34 @@ class MyModule extends Module { val dead = RegNext(io.a +% 1.U).dontTouch } ``` + + +## 显式默认值的优先选择 + +`MuxPriority` 是 Chisel `PriorityMux` 的别名,并提供带显式 default 的重载;序列低下标、Bits 的低比特优先。 + +```scala +val result = MuxPriority(Seq(aValid -> aData, bValid -> bData), default = 0.U(8.W)) +val index = PriorityEncoderDefault(requests, default = 8.U(4.W)) +``` + +上例假定 `aValid`/`bValid` 为 Bool、`aData`/`bData` 为 8 位 UInt、`requests` 为 8 位 UInt。没有选择信号为真时,带 default 的 Mux 返回 default。`PriorityEncoderDefault` 返回最低置位比特的下标;没有置位或输入为一个空 `Seq[Bool]` 时返回 default。返回 UInt 位宽包含 default 所需的位宽,因此用接口需要的显式宽度声明 default。 + +不带 default 的 `MuxPriority` 沿用 Chisel 的行为,不额外承诺无命中时的结果。选择和数据序列应一一对应。 + +源码:[MuxPriority.scala](../chipmunk/src/MuxPriority.scala)。 + +## EmptyBundle / MapBundle + +`EmptyBundle` 是零 payload 字段的 Bundle,适用于只传递事件 token 的 Stream/Flow。它不是等待后续指定类型的占位符。 + +`MapBundle[T]` 是按字符串索引字段的抽象 Record,保留字段顺序并克隆传入的类型模板。键必须非空且唯一;字段模板必须尚未绑定为硬件。定义一个具体子类后,通过 IO/Wire 绑定,并用 `record("key")` 访问。 + +```scala +class Counters extends MapBundle[UInt]( + "cycles" -> Output(UInt(32.W)), + "events" -> Output(UInt(16.W)) +) +``` + +源码:[Bundles.scala](../chipmunk/src/Bundles.scala)。 diff --git a/docs/master-slave.md b/docs/master-slave.md index dcd1eb5..4f8536c 100644 --- a/docs/master-slave.md +++ b/docs/master-slave.md @@ -1,62 +1,47 @@ -# 🐿️ 用 Master/Slave 定义 Bundle 方向 +# 🐿️ 用 Master/Slave 定义接口方向 -`chipmunk.IsMasterSlave` 使用户在定义 `Bundle` 时可同时规定其属于数据的生产者(Master)或消费者(Slave)。当以 `Master()` 定义的 `Bundle` 被以 `Slave()` 形式例化时,内部信号的方向会自动翻转,反之亦然。 +`chipmunk.IsMasterSlave` 为 `Record`(包括 `Bundle`)声明接口的 master/slave 视角。`Master(...)` 和 `Slave(...)` 根据这个声明保留或翻转字段方向,使使用方可以直接表达当前端口的角色。 -## Why? -`chisel3.Bundle` 在例化时,可以使用 `Flipped()` 翻转其内部信号的方向。在许多Chisel项目中,我们约定 `Bundle` 作为模块端口时默认是生产者,如果我们希望它作为消费者,则显式地用 `Flipped()` 把它包裹起来。然而,很多用户并不总是遵守这一约定,所以用户经常搞不清楚端口需不需要用 `Flipped()` 包裹起来。实践中,对于别人代码中的模块接口,通常需要阅读其代码和文档才能判断这些 `Bundle` 属于 Master 还是 Slave(是否需要翻转其方向)。 +```scala +import chisel3.* +import chipmunk.* -例如,如下代码片段中的 SRAM 读写接口 `SramReadWriteIO` 看起来非常合理,但根据上述约定,这一接口既然属于Slave,其内部信号方向应该反过来定义(比如 `enable` 应当是 `Output()`)。不遵守约定的结果,就是其他用户在调用这一 `Bundle` 的的时候经常会陷入困惑:我应不应该套上 `Flipped()`? +class SramWriteIO extends Bundle with IsMasterSlave { + val enable = Input(Bool()) + val addr = Input(UInt(16.W)) + val dataIn = Input(UInt(32.W)) + override def isMaster: Boolean = false +} -```scala -class SramReadWriteIO extends Bundle { - val enable = Input(Bool()) - val read = Input(Bool()) - val addr = Input(UInt(16.W)) - val dataIn = Input(UInt(32.W)) - val dataOut = Output(UInt(32.W)) +class SramWriter extends Module { + val io = IO(new Bundle { + val write = Master(new SramWriteIO) + }) + io.write.enable := false.B + io.write.addr := 0.U + io.write.dataIn := 0.U } ``` -## Usage +这里 `SramWriteIO` 按 slave 视角声明,因此 `Master(new SramWriteIO)` 将三个输入翻转为输出;`Slave(new SramWriteIO)` 则保持声明方向。包装后仍需用 `IO(...)`、`Wire(...)` 等将类型绑定为硬件。 -`chipmunk.IsMasterSlave` 是一个特质,用户在定义 `Bundle` 时可以选择混入这一特质。它要求额外定义一方法 `def isMaster: Boolean`,`true` 表示该 `Bundle` 当前的信号方向属于 Master,反之属于 Slave。 +## 声明视角与实际端口方向 -```scala -class SramWriteIO extends Bundle with IsMasterSlave { - val enable = Input(Bool()) - val addr = Input(UInt(16.W)) - val dataIn = Input(UInt(32.W)) - def isMaster = false // This is a Slave Bundle -} -``` +`isMaster` 描述类型定义中的字段方向,不查询一个已绑定端口的实际方向,也不会因为套上 `Slave(...)` 就改写该声明。需要检查实际方向时,应使用 Chisel 的方向查询 API。 -如此一来,在例化它时,用户可以用 `Master()` 或 `Slave()` 来选择它内部信号的方向。对于一个套上 `Slave()` 的 Master `Bundle`,其内部信号方向会相应取反。用户不需要关心何时需要 `Flipped`,只需要记住这一 `Bundle` 在当前模块的数据传输关系。 +同一个 `Record` 实例不能被 `Master`/`Slave` 重复包装,例如 `Master(Slave(new SramWriteIO))` 会被拒绝。各个端口应分别创建新实例;不同层级的子接口可以各自包装。 -```scala -class Sram extends Module { - val io = new Bundle { - val rw = Slave(new SramReadWriteIO) - } - // ... -} -``` +## 嵌套接口 -此外,`Master`/`Slave` 还支持嵌套使用,从而允许用户构造出如下 AMBA AXI 总线这样的复杂例子。 +AXI 的地址、写数据由 master 发送,读数据和写响应由 slave 发送;这种多通道接口不能简单理解为所有字段都属于同一个数据生产者。现有的 `AxiIO` 已声明完整方向,可以直接使用: ```scala -class AxiIO extends Bundle with IsMasterSlave { - val aw = Master(new AxiWriteAddrChannelIO) - val ar = Master(new AxiReadAddrChannelIO) - val r = Slave(new AxiReadDataChannelIO) - val w = Master(new AxiWriteDataChannelIO) - val b = Slave(new AxiWriteRespChannelIO) - def isMaster = true -} +import chipmunk.amba.* -class AxiSlave extends Module { - val io = IO(new Bundle { - val axi = Slave(new AxiIO) - }) - // ... -} +val masterPort = IO(Master(new AxiIO(AxiParams(dataWidth = 64, addrWidth = 32, idWidth = 4)))) +val slavePort = IO(Slave(new AxiIO(AxiParams(dataWidth = 64, addrWidth = 32, idWidth = 4)))) ``` + +以上两行位于模块内,展示端口声明;各通道还需连接到应用逻辑。对于 Stream/Flow、Acorn、AXI 和 SPI,可以沿用相同的角色包装方式。 + +源码:[IsMasterSlave.scala](../chipmunk/src/IsMasterSlave.scala)。展平端口时的方向处理见 [VerilogIO](verilog-io.md)。 diff --git a/docs/regbank.md b/docs/regbank.md index 1dfd31b..eb3499f 100644 --- a/docs/regbank.md +++ b/docs/regbank.md @@ -1,6 +1,6 @@ # Register Bank -`chipmunk.regbank.RegBank` is a memory-mapped register endpoint for `chipmunk.acorn`. +`chipmunk.regbank.RegBank` is a memory-mapped register endpoint for [Acorn](acorn.md). Each register contains one or more fields with independent access rules and optional hardware updates. ## Configuration @@ -47,14 +47,51 @@ Unused bits read as zero and ignore writes. - Every accepted read or write command has one response. Unmapped or unaligned addresses return an error; read errors return zero. - Reads sample the current value when the command transfers. Read-clear and read-set side effects occur at that same transfer, and the response contains the value before the side effect. A simultaneous read and write also reads the previous value. + - Writes use Acorn byte strobes. A zero field mask leaves that field unchanged and does not participate in update arbitration. An all-zero strobe still receives a normal write response. + - Read-only fields ignore writes; write-only fields read as zero. These accesses do not produce bus errors. - WriteOnce and WriteOnlyOnce lock the entire field on its first accepted software write with a nonzero field mask, including a partial write. Zero-mask writes do not lock it. Reset unlocks it; hardware updates remain allowed. + - Read and write responses have separate one-entry buffers. They remain stable under backpressure; side effects are not repeated. With responses accepted continuously, the bank can accept one read and one write per cycle. Read and write have no implicit ordering. +## Built-in access rules + +Write actions below apply only to byte-enabled field bits. Read side effects apply to the entire field, after sampling the response value. + +| Access type | Write action | Read action | +| --- | --- | --- | +| `ReadOnly` | Ignore | Return value | +| `ReadWrite` | Assign | Return value | +| `ReadClear` | Ignore | Return value, then clear | +| `ReadSet` | Ignore | Return value, then set | +| `WriteReadClear` | Assign | Return value, then clear | +| `WriteReadSet` | Assign | Return value, then set | +| `WriteClear` | Clear regardless of data | Return value | +| `WriteSet` | Set regardless of data | Return value | +| `WriteSetReadClear` | Set regardless of data | Return value, then clear | +| `WriteClearReadSet` | Clear regardless of data | Return value, then set | +| `WriteOneClear` | Clear bits written as one | Return value | +| `WriteOneSet` | Set bits written as one | Return value | +| `WriteOneToggle` | Toggle bits written as one | Return value | +| `WriteZeroClear` | Clear bits written as zero | Return value | +| `WriteZeroSet` | Set bits written as zero | Return value | +| `WriteZeroToggle` | Toggle bits written as zero | Return value | +| `WriteOneSetReadClear` | Set bits written as one | Return value, then clear | +| `WriteOneClearReadSet` | Clear bits written as one | Return value, then set | +| `WriteZeroSetReadClear` | Set bits written as zero | Return value, then clear | +| `WriteZeroClearReadSet` | Clear bits written as zero | Return value, then set | +| `WriteOnly` | Assign | Return zero | +| `WriteOnlyClear` | Clear regardless of data | Return zero | +| `WriteOnlySet` | Set regardless of data | Return zero | +| `WriteOnce` | Assign first masked write, then lock | Return value | +| `WriteOnlyOnce` | Assign first masked write, then lock | Return zero | + +The rule implementations are in [RegFieldAccessType.scala](../chipmunk/src/regbank/RegFieldAccessType.scala). + ## Hardware access Use `fields("REG_FIELD")`, or `fields("REG")` for a single-field register. @@ -66,10 +103,23 @@ It is a hardware access port with no backpressure. Concurrent updates use `colli read side effect, and hardware assignment; default priority is hardware, then write, then read. A zero-mask or locked software write does not block other updates. All signals share the bank's clock and reset. +| Collision mode | Priority, highest first | +| --- | --- | +| `WriteReadBackdoor` | Write, read side effect, hardware | +| `WriteBackdoorRead` | Write, hardware, read side effect | +| `ReadBackdoorWrite` | Read side effect, hardware, write | +| `ReadWriteBackdoor` | Read side effect, write, hardware | +| `BackdoorWriteRead` (default) | Hardware, write, read side effect | +| `BackdoorReadWrite` | Hardware, read side effect, write | + +Arbitration selects one next value for the entire field; it does not combine simultaneous updates. A write-once field locks on its first accepted nonzero-mask software write even if a higher-priority update wins that cycle. + +Configuration names are defined in [RegBankConfig.scala](../chipmunk/src/regbank/RegBankConfig.scala). + ## Tests ```bash -mill chipmunk.test.testOnly \ +./mill chipmunk.test.testOnly \ chipmunk.test.regbank.RegBankConfigSpec \ chipmunk.test.regbank.RegBankSpec ``` diff --git a/docs/regneg.md b/docs/regneg.md index eee9885..3de5b2e 100644 --- a/docs/regneg.md +++ b/docs/regneg.md @@ -1,42 +1,59 @@ # 🐿️ 下降沿触发寄存器 RegNeg -`chipmunk` 提供了在时钟负沿触发的寄存器 `RegNegNext` 和 `RegNegEnable`。它们具有和`chisel3.RegNext`、`chisel3.util.RegEnable` 类似的接口,唯一的区别是其在时钟的下降沿(而不是上升沿)完成数据锁存。 +`chipmunk.RegNegNext` 和 `chipmunk.RegNegEnable` 在时钟下降沿锁存数据。实现使用随库提供的 SystemVerilog 外部模块;时钟和复位来自当前 Chisel 时钟域。 -## Why? +## API -Chisel 已经提供了一系列的时钟上升沿触发的寄存器,包括 `RegInit`、`RegNext`、`RegEnable` 等。有些场合下我们需要生成一些时钟下降沿采样的寄存器,但现阶段在 Chisel 中我们只能采用 BlackBox 的方法实现。 +以下签名省略了 `T <: Data` 和 Chisel 的 `SourceInfo` 参数: -关于 Chisel 是否应该添加对时钟下降沿触发寄存器的原生支持,社区在 FIRRTL 时代就进行过一些讨论(如 [#695](https://github.com/chipsalliance/firrtl/issues/695))。尽管 Chisel 现在的后端 CIRCT Firtool 是支持生成 `negedge clock` 电路的,但目前 Chisel 前端还没有对应的结构。 - -## Usage - -`chipmunk` 提供了四种不同签名的下降沿触发寄存器: ```scala RegNegNext(next: T) -RegNegNext(next: T, init: T, isResetAsync: Boolean = true) +RegNegNext(next: T, init: T, isResetAsync: Boolean) RegNegEnable(next: T, enable: Bool) -RegNegEnable(next: T, init: T, enable: Bool, isResetAsync: Boolean = true) +RegNegEnable(next: T, init: T, enable: Bool, isResetAsync: Boolean) ``` -它们的参数功能如下: -- `next`:待锁存的输入数据,会在时钟的下降沿被采样。 -- `init`:复位初始数据;需要上下文中存在复位信号。 -- `enable`:锁存使能信号,当它为`true.B`时`next`会被锁存。 -- `isResetAsync`:复位信号是否为异步复位(`AsyncReset`);默认为`true`。 +| 参数 | 含义 | +| --- | --- | +| `next` | 在下降沿采样的数据 | +| `init` | 复位值,必须是完整 Chisel 字面量,且与 `next` 类型、位宽一致 | +| `enable` | 下降沿为高时更新,否则保持 | +| `isResetAsync` | 有 `init` 时必须显式指定;`true` 使用异步复位,`false` 使用同步复位 | + +不带 `init` 的重载不生成复位逻辑。数据必须具有已知的正位宽。带初始化的聚合类型必须是所有字段均指定的 Bundle/Vec literal;运行时信号、`DontCare` 或 `VecInit` 生成的硬件不能充当复位字面量。负数 `SInt` 初始化按该位宽的补码表示。 -用户不需要在参数列表中给定时钟和复位信号,和 Chisel 的 `RegXXX` 一样,它们会根据上下文使用对应的时钟和复位信号。 +`isResetAsync` 没有默认值,选择的模式应与当前时钟域的复位类型一致:同步复位在下降沿采样;异步复位不等待时钟沿。 -唯一需要注意的是参数`isResetAsync`。由于 Chisel 的类型系统限制,同步复位和异步复位的类型都是`ResetType`(除非显式声明为`AsyncReset()`,但这种情况不常见),Chisel 会在 Elaboration 阶段再推导决定它们属于哪种类型的复位信号并赋予对应的类型:`Bool` 或 `AsyncReset`。Chisel 没有提供公开 API 判断复位类型,因此需要用户通过设置 `isResetAsync` 告诉 `RegNeg` 最终使用哪种复位。 - +## 示例 -下面给出一些实际用例供参考。 ```scala -val nextVal0 = Wire(Vec(8, UInt(3.W))) -val nextVal1 = Wire(SInt(3.W)) -val enable1 = Wire(Bool()) +import chisel3.* +import chipmunk.* + +class FallingEdgeRegisters extends Module { + val io = IO(new Bundle { + val next = Input(SInt(8.W)) + val enable = Input(Bool()) + val noReset = Output(SInt(8.W)) + val syncReset = Output(SInt(8.W)) + }) + io.noReset := RegNegNext(io.next) + io.syncReset := RegNegEnable(io.next, -3.S(8.W), io.enable, isResetAsync = false) +} +``` + +异步复位版本应在明确的异步复位域中创建,例如在模块内使用: -withClockAndReset(clock, reset) { - val r0 = RegNegNext(io.nextVal0, init = VecInit(Seq.fill(8)(1.U(3.W)))) - val r1 = RegNegEnable(io.nextVal1, -3.S, io.enable1) +```scala +withClockAndReset(clock, reset.asAsyncReset) { + val value = RegNegNext(next, 0.U(8.W), isResetAsync = true) } ``` + +其中 `next` 为应用提供的 8 位 `UInt`。外部仿真或综合流程需包含生成目录里的 RegNeg SystemVerilog 资源。 + +源码:[RegNeg.scala](../chipmunk/src/RegNeg.scala)。回归测试: + +```shell +./mill chipmunk.test.testOnly chipmunk.test.RegNegSpec +``` diff --git a/docs/reset-sync.md b/docs/reset-sync.md new file mode 100644 index 0000000..6ef079a --- /dev/null +++ b/docs/reset-sync.md @@ -0,0 +1,38 @@ +# 🐿️ AsyncResetSync + +`chipmunk.AsyncResetSync` 将高有效复位同步到目标时钟域:复位异步置位,释放经过 `stages` 个目标时钟上升沿,默认两级,要求至少两级。 + +```scala +import chisel3.* +import chipmunk.* + +class ResetDomain extends RawModule { + val clockIn = IO(Input(Clock())) + val resetIn = IO(Input(AsyncReset())) + val resetOut = IO(Output(AsyncReset())) + resetOut := AsyncResetSync.withSpecificClockDomain(clockIn, resetIn, stages = 2) +} +``` + +## API + +| 用法 | 时钟/复位来源 | +| --- | --- | +| `Module(new AsyncResetSync(stages))` | 当前隐式时钟域;模块要求异步复位,输出为 `io.resetOut` | +| `AsyncResetSync(stages = 2)` | 当前隐式时钟域,直接返回 `AsyncReset` 输出 | +| `AsyncResetSync.withSpecificClockDomain(clock, resetAsync, stages = 2)` | 显式时钟域;将输入 reset 转为 `AsyncReset` | + +用于创建其他模块的复位域时,在模块内写: + +```scala +val syncedReset = AsyncResetSync.withSpecificClockDomain(clock, reset) +withClockAndReset(clock, syncedReset) { + val value = RegInit(0.U(8.W)) +} +``` + +复位置位时无需时钟运行;释放需要目标时钟,停钟会使已经置位的复位保持。级数描述 RTL 的同步延迟,实际实现还需满足工艺与时序要求。 + +旧的 `AsyncResetSyncDessert`/`ResetSync` API 已被替换。当前接口没有 `resetChainIn`,也没有锁相环稳定等待或跨域顺序释放控制;有这些需求时,应在系统复位控制器中明确设计,并为每个目标时钟域分别同步释放。 + +这是复位同步器,不提供数据或总线的跨时钟传输。源码:[AsyncResetSync.scala](../chipmunk/src/AsyncResetSync.scala)。 diff --git a/docs/spi-debugger.md b/docs/spi-debugger.md index 159b698..354c63d 100644 --- a/docs/spi-debugger.md +++ b/docs/spi-debugger.md @@ -24,12 +24,14 @@ SPI Debugger 内部寄存器定义如下: ### `BUS_ADDR_L` (0x00) -当配置选项 `busAddrWidth` > 32 时,寄存器 `BUS_ADDR_L` 与该寄存器的值组合在一起作为总线访问地址;否则,该寄存器的低 `busAddrWidth` bit 作为总线访问地址(`wr/rd.cmd.bits.addr`)。 +当配置选项 `busAddrWidth` > 32 时,寄存器 `BUS_ADDR_L` 的值作为低 32 位,与 `BUS_ADDR_H` 的值组合在一起作为总线访问地址;否则,该寄存器的低 `busAddrWidth` bit 作为总线访问地址(`wr/rd.cmd.bits.addr`)。 | Field | Bit width | Bit slice | Access | |--------------|-----------|-----------|--------| | `BUS_ADDR_L` | 32 | 31:0 | R/W | +表中为 `busAddrWidth >= 32` 的情况;更窄的地址只实现低 `busAddrWidth` 位,其余位读为零。 + ### `BUS_WR_RESP` (`0x02`) 读取该寄存器,其最低 bit 表示上一次总线写操作的错误状态(`wr.rsp.bits.error`),0 表示成功,1 表示有错误发生。 @@ -38,7 +40,7 @@ SPI Debugger 内部寄存器定义如下: |---------------|-----------|-----------|--------| | `BUS_WR_RESP` | 1 | 0:0 | RO | -写入该寄存器(无论写 1 或者 0),会发起一次总线写操作。 +写入该寄存器(无论写 1 或者 0),会尝试发起一次总线写操作;BUSY 时拒绝,地址错误在本地完成。 ### `BUS_RD_RESP` (`0x03`) @@ -48,7 +50,7 @@ SPI Debugger 内部寄存器定义如下: |---------------|-----------|-----------|--------| | `BUS_RD_RESP` | 1 | 0:0 | RO | -写入该寄存器(无论写 1 或者 0),会发起一次总线读操作。 +写入该寄存器(无论写 1 或者 0),会尝试发起一次总线读操作;BUSY 时拒绝,地址错误在本地完成。 ### `BUS_WR_DATA` (`0x04`) @@ -109,11 +111,12 @@ SPI Debugger 内部寄存器定义如下: 4. 拉高 SSN 结束 SPI 传输。 SPI Debugger 支持的 Command 包括: -- `REG_WR` (`0x00xx_xxxx`):写内部寄存器,低 6 bit 用于区分寄存器; -- `REG_RD` (`0x01xx_xxxx`):读内部寄存器,低 6 bit 用于区分寄存器; + +- `REG_WR` (`0b00ii_iiii`):写内部寄存器,低 6 bit 用于区分寄存器; +- `REG_RD` (`0b01ii_iiii`):读内部寄存器,低 6 bit 用于区分寄存器; - `BUS_WR` (`0b1000_0000`):写总线; - `BUS_RD` (`0b1100_0000`):读总线; -- `NOP` (`0b1xxx_xxxx`):空操作。 +- `NOP`:最高 bit 为 1 且不是 `0x80` / `0xC0` 的其他编码;其中 `i` 表示寄存器索引 bit。 ### `REG_WR` @@ -131,13 +134,13 @@ SPI Debugger 支持的 Command 包括: 其传输序列为:`Command` (8b) + `Addr` (32b) + `WrData` (32b) -在 `Addr` 和 `WrData` 阶段,Master 需要分别将 32 bit 的待写地址和待写数据发送给 SPI Debugger。SPI Debugger 会将它们填入寄存器 `BUS_ADDR_L` 和 `BUS_WR_DATA`,然后立刻发起一次总线写操作。 +在 `Addr` 和 `WrData` 阶段,Master 需要分别将 32 bit 的待写地址和待写数据发送给 SPI Debugger。SPI Debugger 会将它们填入寄存器 `BUS_ADDR_L` 和 `BUS_WR_DATA`,然后尝试发起一次总线写操作;忙时拒绝,地址不合法时在本地报告错误。 ### `BUS_RD` 其传输序列为:`Command` (8b) + `Addr` (32b) + `Dummy` (8b) + `RdData` (32b) -在 `Addr` 阶段,Master 发送 32 bit 的地址低位。SPI Debugger 更新 `BUS_ADDR_L`,结合可选的 `BUS_ADDR_H` 发起一次总线读操作。 +在 `Addr` 阶段,Master 发送 32 bit 的地址低位。SPI Debugger 更新 `BUS_ADDR_L`,结合可选的 `BUS_ADDR_H` 尝试发起一次总线读操作;忙时拒绝,地址不合法时在本地报告错误。 收齐 8 bit Dummy 时锁存本次结果,整字发送期间不再改变。如果本次结果尚未到达,或者请求因忙被拒绝,则发送 `0xDEAD_BEEF` 并设置 EARLY_READ。迟到的响应仍更新 BUS_RD_DATA 和 RD_DONE,可随后通过寄存器读取;不会返回上一次事务的数据。 @@ -157,3 +160,14 @@ SPI Debugger 支持的 Command 包括: 4. REG_WR 的数据未收齐不写目标寄存器,BUS_WR 的数据未收齐不发总线写请求;快速命令收齐地址字后仍会更新 BUS_ADDR_L。总线读在地址收齐后发起,此后撤销 SSN 不取消请求。完整命令后的多余 SCK 被忽略,下一条命令需重新拉高、拉低 SSN。 5. 复位会取消本模块的未完成事务,必须同步复位相关 Acorn 端点。片选撤销不会取消事务;模块没有总线超时取消机制。 6. `hasMisoValid` 为真时,misoValid 直接由原始 SSN 限定,供 IO-cell 控制输出使能;实际三态由 IO-cell 实现。 + + +## 接口与验证 + +四线接口 [SpiIO.scala](../chipmunk/src/spi/SpiIO.scala) 的默认方向为 master,调试器通过 `Slave(new SpiIO(...))` 使用;`hasMisoValid` 仅用于 slave 侧 IO-cell 控制。总线接入方式见 [Acorn](acorn.md) 和 [AMBA 桥接](amba.md)。 + +```shell +./mill chipmunk.test.testOnly chipmunk.test.spi.SpiDebuggerSpec +``` + +测试覆盖四种 SPI 模式、寄存器和快速访问、未完成帧、总线背压/延迟、忙时拒绝、EARLY_READ、地址错误和响应快照。 diff --git a/docs/state-machine.md b/docs/state-machine.md new file mode 100644 index 0000000..0350b24 --- /dev/null +++ b/docs/state-machine.md @@ -0,0 +1,60 @@ +# 🐿️ StateMachine + +`chipmunk.StateMachine` 使用 `ChiselEnum` 描述状态,在当前时钟/复位域生成状态寄存器。状态定义和跳转是 elaboration 时构建的电路;没有跳转时保持当前状态。 + +```scala +import chisel3.* +import chipmunk.* + +class Controller extends Module { + val io = IO(new Bundle { + val start = Input(Bool()) + val done = Input(Bool()) + val busy = Output(Bool()) + val enteringRun = Output(Bool()) + val leavingRun = Output(Bool()) + }) + object States extends ChiselEnum { + val Idle, Run = Value + } + val fsm = StateMachine(States)(States.Idle) { + on(States.Idle) { + when(io.start) { goto(States.Run) } + } + on(States.Run) { + when(io.done) { goto(States.Idle) } + } + } + io.busy := fsm.is(States.Run) + io.enteringRun := fsm.entering(States.Run) + io.leavingRun := fsm.leaving(States.Run) +} +``` + +## 查询与事件 + +| API | 含义 | +| --- | --- | +| `fsm.current` | 当前状态,只读 | +| `fsm.is(state)` | 当前状态是否为指定状态 | +| `fsm.entering(state)` | 当前状态不是该状态,组合下一状态是该状态 | +| `fsm.leaving(state)` | 当前状态是该状态,组合下一状态不是该状态 | +| `fsm.valid` | 当前状态是否为声明的枚举值之一 | + +`entering`/`leaving` 在执行跳转的时钟沿之前组合有效,不是时钟沿之后的延迟脉冲。跳转到自身不会产生这两个事件。查询逻辑不受 reset 门控,复位期间也可能随输入变化;应用需要复位时禁止输出事件时,应显式门控。 + +状态寄存器复位到 `initial`。`valid` 仅报告有效性,不提供非法状态自动恢复机制。 + +## 构建约束 + +- 在模块内无条件创建 StateMachine,不要把整个构建过程放在硬件 `when`/`switch` 内。 +- `initial`、`on`、`goto` 和查询目标必须是同一个枚举里声明的状态字面量。 +- 每个状态最多一个 `on`,不允许嵌套 `on`;没有 `on` 的状态仍可保持。 +- `goto` 只能出现在 `on` 内,可以由 Chisel `when` 条件控制。同一周期有多个有效跳转赋值时遵循 Chisel 最后连接优先规则。 +- Builder/StateScope 由 Scala 3 上下文参数传递;通常通过 `import chipmunk.*` 使用导出的 `on`/`goto` 即可。 + +源码:[StateMachine.scala](../chipmunk/src/StateMachine.scala)。验证: + +```shell +./mill chipmunk.test.testOnly chipmunk.test.StateMachineSpec +``` diff --git a/docs/stream.md b/docs/stream.md index 1f28b14..d65184e 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -1,12 +1,15 @@ # 🐿️ DecoupledIO 增强版之 StreamIO -在数字电路设计中,我们经常使用 Ready/Valid 握手协议来解耦数据流,Chisel 提供了 `DecoupledIO` 用以实现这一协议。然而,`DecoupledIO` 仅是一个预置的 `Bundle`,缺少与之配套的一系列常用组件(例如寄存器切片、Mux/Demux 等),此外它也未能搭配 `chipmunk.IsMasterSlave`。`chipmunk` 为 `DecoupledIO` 提供了一个“威力加强版”的 Ready/Valid 握手协议——`chipmunk.StreamIO`。 +在数字电路设计中,我们经常使用 Ready/Valid 握手协议来解耦数据流,Chisel 提供了 `DecoupledIO` 用以实现这一协议。然而,`DecoupledIO` 仅是一个预置的 `Bundle`,缺少与之配套的一系列常用组件(例如寄存器切片、Mux/Demux 等),此外它也未能搭配 `chipmunk.IsMasterSlave`。`chipmunk` 为 `DecoupledIO` 提供了一个“威力加强版”的 Ready/Valid 握手协议——`chipmunk.stream.StreamIO`。 + +使用 `import chipmunk.stream.*`,角色包装用 `import chipmunk.*`。以下硬件片段位于模块内,并导入 `chisel3.*` / `chisel3.util.*`。`Stream(...)` 和 `Flow(...)` 工厂创建的是未绑定类型,需放入 IO/Wire/Reg;`from` 工厂则创建已连接的 Wire。 ## Ready/Valid 握手协议 ![Ready/Valid Interface](./assets/stream-ready-valid-protocol.png) Ready/Valid 握手协议由三部分信号组成: + - `valid`:表示数据有效,即上游数据已经准备好,可以被消费; - `ready`:表示数据可被消费,即下游已经准备好接收数据; - `bits`(即 `payload`):表示数据本身。 @@ -23,16 +26,18 @@ Ready/Valid 握手协议由三部分信号组成: ### Why? -Chisel 提供了 `DecoupledIO` 和 `IrrevocableIO` 两个 Ready/Valid 握手协议的 `Bundle` 实现,它们都继承自 `chisel3.ReadyValidIO`。Chisel 并未为这两个接口提供太多 API,基本上只有: +Chisel 提供了 `DecoupledIO` 和 `IrrevocableIO` 两个 Ready/Valid 握手协议的 `Bundle` 实现,它们都继承自 `chisel3.util.ReadyValidIO`。Chisel 并未为这两个接口提供太多 API,基本上只有: + - `def fire: Bool = ready && valid`,表示当前周期是否发生了数据传输; - `class Queue`,以 `ReadyValidIO` 为接口的同步 FIFO; -- `def map[T](f: T => T2): T`,用于对 `bits` 进行变换。 +- `map(f)`,用于变换 `bits`;Chipmunk 的重载保留 `StreamIO` 返回类型。 `IrrevocableIO` 其实在实现上 `DecoupledIO` 并无区别,只是约定它的 `bits` 在 `valid` 有效但 `ready` 无效时不会改变,即数据不可撤回。两者的实际差别有赖于具体模块中的电路实现。 很显然,上述 API 不足以应付实际的设计需求。 Chipmunk 提供的 `chipmunk.stream.StreamIO` 继承自 `chisel3.util.DecoupledIO`,并提供了一系列常用的 API 和特性,包括: + - 一系列类似`fire` 的语法糖,展现数据传输的不同状态; - 符号化的连接方法,类似 `a >> b >-> c`,可以很容易地看出数据的流向; - 针对 `bits` 的一系列操作,包括 Map、Cast 等; @@ -62,7 +67,7 @@ val myStream = Wire(Stream.like(myDecoupled)) ```scala val myStream = Wire(Stream.empty) ``` -空的 `StreamIO` 一般用于声明时没有确定 `payload` 类型的情况,后续可以搭配 `payloadReplace` 等 API 赋予具体的 `payload`。 +空的 `StreamIO` 传递不带 payload 的事件 token。`payloadReplace` 可以派生具有新 payload 类型的 Stream,但不会更改原接口的静态类型。 #### 流控与状态指示 @@ -83,6 +88,8 @@ val myStream = Wire(Stream.empty) | `x.throwWhen(cond)` | 当 `cond` 为 `True` 时,数据传输会被丢弃,即上游模块发起的数据传输都会成功握手,但下游模块不会收到相应的数据。 | | `x.takeWhen(cond)` | 相当于 `x.throwWhen(!cond)` | +上述条件没有内部锁存。在输出 `valid && !ready` 期间,`cond` 必须保持不变,否则可能撤回已经提供的事务。 + #### Payload 变换 ![StreamIO Payload Operation](./assets/stream-payload-operation.png) @@ -95,25 +102,25 @@ def payloadReplace[T2 <: Data](p: T2): StreamIO[T2] def payloadCast[T2 <: Data](gen: T2, checkWidth: Boolean = false): StreamIO[T2] ``` -上述方法会返回一个新的 `StreamIO`,其 `payload` 会变成变换后、类型为 `T2` 的新信号,同时其 `ready` 和 `valid` 信号会连接到当前 `StreamIO` 的 `ready` 和 `valid` 信号。 +上述方法会返回一个新的 `StreamIO`,其 `payload` 为变换后的硬件信号,握手时序不变。回调只在 elaboration 时执行一次,返回值必须是硬件;替换/映射后的 payload 在输出等待期间也必须保持稳定。 - `payloadMap` 可以将函数 `f` 作用于 `payload`,比如: ```scala val streamOld = Wire(Stream(UInt(32.W))) -val StreamNew = streamOld.payloadMap(_ + 1.U) +val streamNew = streamOld.payloadMap(_ + 1.U) ``` - `payloadReplace` 会将 `payload` 替换为 `p`,相当于 `payloadMap(_ => p)`,比如: ```scala val streamOld = Wire(Stream(UInt(32.W))) val anotherBool = Wire(Bool()) -val StreamNew = streamOld.payloadReplace(anotherBool) +val streamNew = streamOld.payloadReplace(anotherBool) ``` - `payloadCast` 会对 `payload` 进行强制类型转换,相当于 `payloadMap(_.asTypeOf(gen))`,比如: ```scala val streamOld = Wire(Stream(UInt(32.W))) -val StreamNew = streamOld.payloadCast(SInt(32.W), checkWidth = true) +val streamNew = streamOld.payloadCast(SInt(32.W), checkWidth = true) ``` 其中参数 `checkWidth = true` 允许检查类型转换前后的位宽是否一致,如果不一致抛出异常。该参数默认为 `false`,即不会检查位宽,如果位宽不一致会自动截断或扩展。 @@ -127,30 +134,35 @@ val StreamNew = streamOld.payloadCast(SInt(32.W), checkWidth = true) ```scala val streamNew = streamOld.pipeForward() ``` -它在性能上不会引入吞吐下降(虽然会增加 1 周期延时),在面积上代价是 N + 1 个寄存器(N 是 `bits` 位宽)以及少量组合逻辑。 +最小延迟一拍,最高每拍传输一次,使用 N + 1 个寄存器位(N 是 payload 位宽)。默认 `bubbleCollapse=true`,空缓冲可在输出 ready 为低时接收输入;设为 false 时,输入 ready 跟随输出 ready,消费者必须能够主动提供 ready。 + - `pipeBackward`:后向寄存器切片,切断从下游模块到上游模块的 `ready` 通路的组合逻辑路径。 ```scala val streamNew = streamOld.pipeBackward() ``` -它在性能上不会引入额外的延时和吞吐下降,在面积上代价是 N + 1 个寄存器(N 是 `bits` 位宽)、N 个 2:1 MUX 以及少量组合逻辑。 +最小延迟零拍,最高每拍传输一次,使用 N + 1 个寄存器位以及 N 位 2:1 MUX。缓存事务排出那一拍,输入仍保持阻塞;发生背压时可增加延迟。 + - `pipeAll`:双向寄存器切片,同时切断 `valid`、`bits`、`ready` 通路的组合逻辑路径。 ```scala val streamNew = streamOld.pipeAll() ``` 相当于 `streamOld.pipeForward().pipeBackward()`,因此它会引入 2N + 2 个寄存器(N 是 `bits` 位宽)、N 个 2:1 MUX 以及少量组合逻辑。 + - `pipeSimple`:双向寄存器切片,同时切断 `valid`、`bits`、`ready` 通路的组合逻辑路径。 ```scala val streamNew = streamOld.pipeSimple() ``` -它至多每隔一个周期握手一次,因此会引起最高可达一半的吞吐损失,但面积上只需要 N + 2 个寄存器以及少量的组合逻辑。与 `pipeAll` 相比,它的面积代价更小,但会引入额外的性能损失。 +它至多每隔一个周期握手一次,因此会引起最高可达一半的吞吐损失,但面积上只需要 N + 1 个寄存器位以及少量的组合逻辑。与 `pipeAll` 相比,它的面积代价更小,但会引入额外的性能损失。 + - `pipeValid`:仅切断 `valid` 通路的组合逻辑路径。 ```scala val streamNew = streamOld.pipeValid() ``` -面积上仅需要 1 个寄存器和少量组合逻辑。 -- `pipPassThrough`:什么都不做,返回当前的 `StreamIO`。 +输出 valid 在首次观察到输入 valid 后一拍置位;输入和输出在同一拍握手,最高每两拍一次。仅使用一个 valid 寄存器,不保存 payload,因此依赖输入生产者保持事务。 + +- `pipePassThrough`:什么都不做,返回当前的 `StreamIO`。 ```scala -val streamNew = streamOld.pipPassThrough() +val streamNew = streamOld.pipePassThrough() ``` #### 符号化连接 @@ -172,7 +184,7 @@ val streamNew = streamOld.pipPassThrough() 借助 `<<`、`>>` 等流操作符,我们可以实现可读性较强的 `StreamIO` 连接甚至级连,比如: ```scala -val s1 = Stream(UInt(8.W)) +val s1 = Wire(Stream(UInt(8.W))) s1 <-< uAnotherModule.io.outStream s1.haltWhen(somethingEnable) >> uSomeModule.io.inStream ``` @@ -185,7 +197,7 @@ Chipmunk 包括了一系列 `StreamIO` 的配套组件。 StreamFork 可以将一个上游 `StreamIO` 分叉成多个下游 `StreamIO`,每个下游 `StreamIO` 会分别完成仅一次握手,上游 `StreamIO` 会在所有下游 `StreamIO` 完成握手后才完成握手。为了提高性能,每个下游 `StreamIO` 是否握手是独立进行的,当它 `ready` 有效时即完成握手,而不需要等到所有下游 `StreamIO` 的 `ready` 都有效。 -StreamJoin 可以将多个上游 `StreamIO` 合并成一个下游 `StreamIO`,当且仅当所有上游 `StreamIO` 的 `valid` 都有效时,才会发生握手。 +StreamJoin 可以将多个上游 `StreamIO` 合并成一个下游 `StreamIO`,所有上游 `StreamIO` 的 `valid` 都有效,且下游 `ready` 有效时,各输入与输出同时握手。 Fork 不保存 payload,输入生产者必须保持当前事务,直到所有分支接收完毕。它保证每个输出分别接收一次,并不保证所有输出在同一周期接收。若希望上游先握手、后续再逐步分发,可以在 Fork 前增加 Queue。 @@ -263,3 +275,39 @@ val flushable = StreamQueue(input, entries = 4, flush = Some(cancel)) `pipe=true` 允许满队列在出队的同一周期接受输入,会组合连接 ready 路径;`flow=true` 允许输入在队列为空时直接到达输出。`useSyncReadMem=true` 使用同步读存储。 零深度只增加接线,生产者必须已经满足 Stream 协议;负深度和零深度 flush 在 elaboration 时被拒绝。Flush 在时钟沿取消所有缓冲事务,包括尚未完成握手的输出,因此使用方必须协调取消语义。 + + +### StreamDelay + +```scala +val delayed = input.delayFixed(cycles = 3) +val randomDelay = StreamDelay.random(input, maxCycles = 5, minCycles = 1) +``` + +延迟从空闲状态首次观察到输入 valid 的周期开始计算;输入和输出在同一周期握手,背压可进一步推迟完成。组件不保存 payload,不提前接收输入,生产者必须在整个等待期间保持 valid/bits。零延迟直接通过。 + +显式模块 `new StreamDelay(gen, delayWidth)` 在每个事务开始时采样 `io.targetDelay`,后续变化只影响下一事务。随机延迟包含上下界,每次事务选择一次;序列在复位后重现,分布不保证均匀。复位返回空闲,仍然提供的事务重新计时。 + +## FlowIO 与 Stream 转换 + +`FlowIO` 继承 Chisel 的 valid-only 接口;每个 valid 为高的周期都是一次事务,接收方必须接收,没有 ready。使用 `Wire(Flow(gen))`、`Flow.empty`、`Flow.like(source)` / `Flow.from(source)`,类型绑定与 Stream 工厂规则相同。 + +Flow 提供 payloadMap/Replace/Cast、pipeForward、stage、takeWhen/throwWhen;pipeForward 增加一拍延迟,最高每拍一次,仅 valid 复位。`RegFlow(gen)` 创建可赋值的 Flow 寄存器,仅 valid 复位为 false,bits 在未赋值时保持;它不自动转发输入。 + +| 转换 | 行为 | +| --- | --- | +| `stream.asFlow` | 转发 valid/bits,不驱动 ready;等待中的 Stream 可能被 Flow 重复消费 | +| `stream.toFlow()` | 只把已完成的 fire 转为 Flow valid,保留原 ready 驱动 | +| `stream.toFlow(readyFreeRun = true)` | 同上,并将 Stream ready 驱动为 true | +| `flow.asStream` | 直接转发 valid/bits,没有缓冲,也无法向 Flow 背压;输出 valid 时消费者必须 ready,才能避免丢失 | + +Flow 的 payloadMap/Replace/Cast 与 Stream 的 asFlow/toFlow 返回只读视图,不能把这些结果当作可写寄存器;需要可写存储时使用 RegFlow。 + +源码:[stream 目录](../chipmunk/src/stream)。验证: + +```shell +./mill chipmunk.test.testOnly \ + chipmunk.test.StreamIOSpec \ + chipmunk.test.FlowIOSpec \ + chipmunk.test.StreamComponentsSpec +``` diff --git a/docs/tester.md b/docs/tester.md new file mode 100644 index 0000000..39b1e49 --- /dev/null +++ b/docs/tester.md @@ -0,0 +1,67 @@ +# 🐿️ ChiselSim 测试辅助 + +`chipmunk.tester` 的 `TesterAPI` 和 `MultiClockSupport` 基于 ChiselSim 的 `PeekPokeAPI`。把这两个 trait 混入自己的 ChiselSim 测试基类即可使用;它们不创建硬件,也不提供旧 chiseltest 的 fork/join 调度。 + +项目内的 [ChipmunkSim.scala](../chipmunk/test/src/ChipmunkSim.scala) 提供 `ChipmunkFlatSpec`、`ChipmunkFreeSpec` 和 `ChipmunkFunSpec`,用于本仓库测试。这个文件位于 test 源目录,不包含在发布的库中;外部项目需自行定义相应基类。 + +## 端口 stimulus + +`#=` 是 Chisel literal 的 poke 简写,`randomize()` 支持 Bool/UInt/SInt;`expect`、`peek` 和 `clock.step` 使用 ChiselSim API。在继承对应测试基类的 `simulate` 回调内,可以写: + +```scala +simulate(new MyDut) { dut => + dut.io.input #= 0x12.U + dut.clock.step() + dut.io.output.expect(0x12.U) + dut.io.input.randomize() +} +``` + +这里 `MyDut` 是应用自己的模块;上例假定它在一拍后输出输入值。`#=` 的右侧需匹配端口 Chisel 类型;AsyncReset 端口可使用对应 Reset literal。随机 UInt 按位宽生成,随机 SInt 覆盖补码有符号范围,零位宽 SInt 被拒绝。 + +## 多时钟调度 + +测试 harness 将各 DUT 时钟的电平汇总成一个输入 UInt,再通过每个 bit 的 `.asClock` 驱动各时钟域。另提供一个只推进仿真时间的 `timebaseClock`,不能用它驱动 DUT 的状态。 + +在混入 `MultiClockSupport` 的 `simulate` 回调中创建一个调度器管理所有域: + +```scala +val scheduler = new MultiClockScheduler( + dut.io.clockLevels, + dut.clock, + Seq( + ClockSpec("fast", periodTicks = 4), + ClockSpec("slow", periodTicks = 6, firstRiseAt = 4, lowTicks = 2) + ) +) +scheduler.initialize() +scheduler.runEvents(10) { event => + println(s"time=${event.time}, levels=${event.levels}") +} +``` + +示例中的 `dut.clock` 仅作 timebase。`clockLevels(0)` 对应 fast,`clockLevels(1)` 对应 slow;端口位宽必须与域数量完全一致,域名必须唯一。 + +| ClockSpec 参数 | 语义 | +| --- | --- | +| `periodTicks` | 相邻上升沿间的逻辑 tick 数,至少 2 | +| `lowTicks` | 低相持续时间;默认 `periodTicks / 2` 向下取整,显式值必须在 1 到 period−1 之间 | +| `firstRiseAt` | 第一次上升沿时间;默认与低相持续时间相同,显式值必须大于零 | + +所有时钟从低电平开始;奇数周期默认将多出的一 tick 分给高相。一个逻辑 tick 对应两个 simulator timestep。 + +`advance()` 前进到下一个含时钟沿的时间点,返回 `ClockEvent`;`runEvents(n)` 执行 n 个这样的时间点,而不是 n 个周期。同一时间点的所有时钟沿通过一次 packed poke 同时施加;事件中的 transitions 保持配置顺序,`rising`/`falling` 可过滤边沿。`initialize()` 可重复调用,也会在首次 advance 时自动执行。 + +## 波形和回归命令 + +本仓库测试基类通过 ChiselSim CLI options 选择 simulator、FSDB/VPD 和 FST。自定义 `EmitFst` 位于 test 源目录,使用 Verilator;它不是已发布的 `TraceSupport` API。外部测试工程可参考该实现。 + +```shell +./mill chipmunk.test +./mill chipmunk.test.testOnly chipmunk.test.MultiClockSpec +./mill chipmunk.test.testOnly chipmunk.test.TraceSpec +``` + +[TraceSpec.scala](../chipmunk/test/src/TraceSpec.scala) 给出 `configMap` 中 `simulator -> verilator`、`emitFst -> 1` 的完整设置。FSDB/VPD 是否可用由仿真后端决定;本仓库的 FST 回归使用 Verilator。 + +源码:[TesterAPI.scala](../chipmunk/src/tester/TesterAPI.scala)、[MultiClockSupport.scala](../chipmunk/src/tester/MultiClockSupport.scala)。 diff --git a/docs/verilog-io.md b/docs/verilog-io.md new file mode 100644 index 0000000..1e94404 --- /dev/null +++ b/docs/verilog-io.md @@ -0,0 +1,41 @@ +# 🐿️ VerilogIO:展平 RTL 端口 + +`HasVerilogIO` 为接口的叶字段生成 RTL 名称;`createVerilogIO` 从未绑定的接口类型构造展平 Record;绑定后用 `viewAsChiselIO` 恢复原来的 Chisel 接口 API。视图只提供映射,不增加寄存器、缓冲或延迟。 + +以下完整模块把 AXI4 slave 端口展平,并连接到 Acorn 桥: + +```scala +import chisel3.* +import chipmunk.* +import chipmunk.acorn.* +import chipmunk.amba.* + +class FlatAxiBridge extends Module { + val params = AxiParams(dataWidth = 32, addrWidth = 16, idWidth = 2) + val axiRtl = FlatIO(Slave(new AxiIO(params)).createVerilogIO(name => s"S_AXI_$name")) + val acorn = IO(Master(new AcornIO(AcornParams(32, 16)))) + + val bridge = Module(new AxiToAcornBridge(params)) + bridge.io.sAxi :<>= axiRtl.viewAsChiselIO + acorn :<>= bridge.io.mAcorn +} +``` + +AXI/AXI-Lite 已实现叶字段命名,例如 `aw.bits.addr` → `AWADDR`、`aw.valid` → `AWVALID`;上述 rename 函数进一步得到 `S_AXI_AWADDR`。`FlatIO` 避免增加外层 Bundle 名字;普通 `IO(...)` 也可绑定,但生成的端口名可能包含根接口前缀。模块的 clock/reset 不属于这个 AXI 接口,不受该 rename 函数影响。 + +## 自定义接口与方向 + +自定义接口混入 `HasVerilogIO` 并实现 `generatePortName(path: Seq[String]): String`;路径是相对接口根节点的字段路径,Vec 下标以十进制字符串表示。默认 rename 为 identity。 + +- 源类型必须未绑定;不要把已有 `IO`/`Wire` 传给 `createVerilogIO`。 +- `viewAsChiselIO` 的调用对象必须已经绑定为硬件,不能在未绑定类型上建立视图。 +- 支持 Record/Vec 内的数字叶字段,包括 Bits、枚举、Clock 和 Reset;不支持 Analog、Probe、opaque Record 或别名叶字段。 +- 生成并重命名后的叶名称必须唯一,并满足 `[A-Za-z_][A-Za-z0-9_]*`。 +- 原接口含 `IsMasterSlave` 时,展平类型保留相应能力;角色包装仍只能对同一个实例应用一次。通常像上例一样先指定 `Slave(...)`,再展平。 +- 视图跟随实际绑定时的方向翻转/强制方向,不通过原类型的 `isMaster` 去猜测已绑定端口。 + +这是 Scala 3 类型工厂 API,旧的手动 `new VerilogIO` 用法需要迁移。源码:[VerilogIO.scala](../chipmunk/src/VerilogIO.scala)、[VerilogIOFactory.scala](../chipmunk/src/VerilogIOFactory.scala)。验证: + +```shell +./mill chipmunk.test.testOnly chipmunk.test.VerilogIOSpec +```