diff --git a/core/src/main/scala/chisel3/FixedIO.scala b/core/src/main/scala/chisel3/FixedIO.scala new file mode 100644 index 00000000000..b58eb7e00c3 --- /dev/null +++ b/core/src/main/scala/chisel3/FixedIO.scala @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3 + +import chisel3.experimental.SourceInfo + +/** Internal helper for creating ports from values whose shape is described by a + * [[chisel3.experimental.dataview.DataProduct]]. + * + * For a single [[Data]] this defers to [[FlatIO]] so existing + * `FixedIOBaseModule` behavior is preserved exactly. For composite shapes + * (tuples, sequences) it recursively creates one port per contained [[Data]], + * naming each port based on its structural path (e.g. `_1`, `_2`, `_0`). + * + * The original generator value cannot be reused as the bound IO because + * [[IO]]/[[FlatIO]] may clone the provided [[Data]] (per + * [[chisel3.Data.mustClone]]). Instead, this helper rebuilds the structure of + * `A` with the cloned, port-bound values substituted in, returning a value of + * the same shape whose leaves are the live ports. + */ +private[chisel3] object FixedIO { + + /** Bind `value`'s contained [[Data]] as ports, returning a value of the same + * shape whose leaves are the bound ports. + * + * The chisel naming plugin would normally inject a `withName("io")` around + * the val RHS for `final val io = ...`, but only when the val's type is + * known to be a [[Data]] (or container thereof). Because `A` is an + * unbounded type parameter, the plugin does not insert that wrapper, so we + * must call [[chisel3.withName]] explicitly here. + * + * @param value the generator value to walk + * @param path the structural path used to name multi-IO leaves (the + * top-level call typically passes `"io"`) + */ + def bindPorts(value: Any, path: String)(implicit si: SourceInfo): Any = + chisel3.withName(path)(bindPortsImpl(value, path)) + + private def bindPortsImpl(value: Any, path: String)(implicit si: SourceInfo): Any = value match { + case d: Data => + // Top-level invocation with `path == "io"` and a single Data preserves + // existing FixedIOBaseModule behavior exactly (uses FlatIO so Bundles + // become flat top-level ports). + if (path == "io") FlatIO(d) + else { + val port = IO(d) + port.suggestName(path) + port + } + case t: Tuple1[_] => + Tuple1(bindPorts(t._1, appendPath(path, "_1"))) + case t: Tuple2[_, _] => + (bindPorts(t._1, appendPath(path, "_1")), bindPorts(t._2, appendPath(path, "_2"))) + case t: Tuple3[_, _, _] => + ( + bindPorts(t._1, appendPath(path, "_1")), + bindPorts(t._2, appendPath(path, "_2")), + bindPorts(t._3, appendPath(path, "_3")) + ) + case t: Tuple4[_, _, _, _] => + ( + bindPorts(t._1, appendPath(path, "_1")), + bindPorts(t._2, appendPath(path, "_2")), + bindPorts(t._3, appendPath(path, "_3")), + bindPorts(t._4, appendPath(path, "_4")) + ) + case t: Tuple5[_, _, _, _, _] => + ( + bindPorts(t._1, appendPath(path, "_1")), + bindPorts(t._2, appendPath(path, "_2")), + bindPorts(t._3, appendPath(path, "_3")), + bindPorts(t._4, appendPath(path, "_4")), + bindPorts(t._5, appendPath(path, "_5")) + ) + case t: Tuple6[_, _, _, _, _, _] => + ( + bindPorts(t._1, appendPath(path, "_1")), + bindPorts(t._2, appendPath(path, "_2")), + bindPorts(t._3, appendPath(path, "_3")), + bindPorts(t._4, appendPath(path, "_4")), + bindPorts(t._5, appendPath(path, "_5")), + bindPorts(t._6, appendPath(path, "_6")) + ) + case t: Tuple7[_, _, _, _, _, _, _] => + ( + bindPorts(t._1, appendPath(path, "_1")), + bindPorts(t._2, appendPath(path, "_2")), + bindPorts(t._3, appendPath(path, "_3")), + bindPorts(t._4, appendPath(path, "_4")), + bindPorts(t._5, appendPath(path, "_5")), + bindPorts(t._6, appendPath(path, "_6")), + bindPorts(t._7, appendPath(path, "_7")) + ) + case t: Tuple8[_, _, _, _, _, _, _, _] => + ( + bindPorts(t._1, appendPath(path, "_1")), + bindPorts(t._2, appendPath(path, "_2")), + bindPorts(t._3, appendPath(path, "_3")), + bindPorts(t._4, appendPath(path, "_4")), + bindPorts(t._5, appendPath(path, "_5")), + bindPorts(t._6, appendPath(path, "_6")), + bindPorts(t._7, appendPath(path, "_7")), + bindPorts(t._8, appendPath(path, "_8")) + ) + case t: Tuple9[_, _, _, _, _, _, _, _, _] => + ( + bindPorts(t._1, appendPath(path, "_1")), + bindPorts(t._2, appendPath(path, "_2")), + bindPorts(t._3, appendPath(path, "_3")), + bindPorts(t._4, appendPath(path, "_4")), + bindPorts(t._5, appendPath(path, "_5")), + bindPorts(t._6, appendPath(path, "_6")), + bindPorts(t._7, appendPath(path, "_7")), + bindPorts(t._8, appendPath(path, "_8")), + bindPorts(t._9, appendPath(path, "_9")) + ) + case t: Tuple10[_, _, _, _, _, _, _, _, _, _] => + ( + bindPorts(t._1, appendPath(path, "_1")), + bindPorts(t._2, appendPath(path, "_2")), + bindPorts(t._3, appendPath(path, "_3")), + bindPorts(t._4, appendPath(path, "_4")), + bindPorts(t._5, appendPath(path, "_5")), + bindPorts(t._6, appendPath(path, "_6")), + bindPorts(t._7, appendPath(path, "_7")), + bindPorts(t._8, appendPath(path, "_8")), + bindPorts(t._9, appendPath(path, "_9")), + bindPorts(t._10, appendPath(path, "_10")) + ) + case s: Seq[_] => + s.zipWithIndex.map { case (elt, idx) => bindPorts(elt, appendPath(path, idx.toString)) } + case other => + // Anything else (e.g. types with DataProduct.empty) has no Data to bind. + other + } + + private def appendPath(path: String, suffix: String): String = + if (path.isEmpty) suffix else s"${path}_${suffix.stripPrefix("_")}" +} diff --git a/src/main/scala-2/chisel3/FixedIOModule.scala b/src/main/scala-2/chisel3/FixedIOModule.scala index 83ca2da1c16..3c322843c9e 100644 --- a/src/main/scala-2/chisel3/FixedIOModule.scala +++ b/src/main/scala-2/chisel3/FixedIOModule.scala @@ -2,22 +2,31 @@ package chisel3 -import chisel3.experimental.hierarchy.{instantiable, public} +import chisel3.experimental.dataview.DataProduct +import chisel3.experimental.hierarchy.{Definition, Instance, IsInstantiable, Lookupable} import chisel3.experimental.{BaseModule, UnlocatableSourceInfo} +import chisel3.internal.MacroGenerated /** A module or external module whose IO is generated from a specific generator. * This module may have no additional IO created other than what is specified * by its `ioGenerator` abstract member. + * + * `A` may be any type that has a [[chisel3.experimental.dataview.DataProduct]] + * implementation, including a single [[Data]], a tuple of `Data`-containing + * types, or a `Seq` of `Data`-containing types. Each contained [[Data]] is + * turned into a port; for the single-`Data` case [[FlatIO]] is used so that + * existing modules are unaffected. */ -@instantiable -sealed trait FixedIOBaseModule[A <: Data] extends BaseModule { +sealed trait FixedIOBaseModule[A] extends BaseModule with IsInstantiable { /** A generator of IO */ protected def ioGenerator: A - @public - final val io = FlatIO(ioGenerator)(UnlocatableSourceInfo) - endIOCreation() + /** Evidence that `A` contains [[Data]] elements that can be turned into ports. */ + protected implicit def ioDataProduct: DataProduct[A] + + /** The IO of this module, of shape `A` whose contained [[Data]] are ports. */ + def io: A } @@ -26,7 +35,34 @@ sealed trait FixedIOBaseModule[A <: Data] extends BaseModule { * * @param ioGenerator */ -class FixedIORawModule[A <: Data](final val ioGenerator: A) extends RawModule with FixedIOBaseModule[A] +class FixedIORawModule[A](final val ioGenerator: A)(implicit val ioDataProduct: DataProduct[A]) + extends RawModule + with FixedIOBaseModule[A] { + final val io: A = FixedIO.bindPorts(ioGenerator, "io")(UnlocatableSourceInfo).asInstanceOf[A] + endIOCreation() +} + +object FixedIORawModule { + // Manually-written equivalent of what `@public` on `io` would produce, but + // with a `Lookupable[A]` evidence param so we can support arbitrary + // `DataProduct[A]`-shaped IOs in addition to the historical `A <: Data` case. + implicit class FixedIORawModuleDefinitionExtensions[A]( + private val ___module: Definition[FixedIORawModule[A]] + ) extends AnyVal { + def io(implicit lookup: Lookupable[A]): lookup.C = { + implicit val mg: MacroGenerated = new MacroGenerated {} + ___module._lookup(_.io) + } + } + implicit class FixedIORawModuleInstanceExtensions[A]( + private val ___module: Instance[FixedIORawModule[A]] + ) extends AnyVal { + def io(implicit lookup: Lookupable[A]): lookup.C = { + implicit val mg: MacroGenerated = new MacroGenerated {} + ___module._lookup(_.io) + } + } +} /** A Chisel module whose IO (in addition to [[clock]] and [[reset]]) is determined * by an IO generator. This module cannot have additional IO created by modules that @@ -34,7 +70,31 @@ class FixedIORawModule[A <: Data](final val ioGenerator: A) extends RawModule wi * * @param ioGenerator */ -class FixedIOModule[A <: Data](final val ioGenerator: A) extends Module with FixedIOBaseModule[A] +class FixedIOModule[A](final val ioGenerator: A)(implicit val ioDataProduct: DataProduct[A]) + extends Module + with FixedIOBaseModule[A] { + final val io: A = FixedIO.bindPorts(ioGenerator, "io")(UnlocatableSourceInfo).asInstanceOf[A] + endIOCreation() +} + +object FixedIOModule { + implicit class FixedIOModuleDefinitionExtensions[A]( + private val ___module: Definition[FixedIOModule[A]] + ) extends AnyVal { + def io(implicit lookup: Lookupable[A]): lookup.C = { + implicit val mg: MacroGenerated = new MacroGenerated {} + ___module._lookup(_.io) + } + } + implicit class FixedIOModuleInstanceExtensions[A]( + private val ___module: Instance[FixedIOModule[A]] + ) extends AnyVal { + def io(implicit lookup: Lookupable[A]): lookup.C = { + implicit val mg: MacroGenerated = new MacroGenerated {} + ___module._lookup(_.io) + } + } +} /** A Chisel blackbox whose IO is determined by an IO generator. This module * cannot have additional IO created by modules that extend it. @@ -42,6 +102,31 @@ class FixedIOModule[A <: Data](final val ioGenerator: A) extends Module with Fix * @param ioGenerator * @param params */ -class FixedIOExtModule[A <: Data](final val ioGenerator: A, params: Map[String, Param] = Map.empty[String, Param]) +class FixedIOExtModule[A]( + final val ioGenerator: A, + params: Map[String, Param] = Map.empty[String, Param] +)(implicit val ioDataProduct: DataProduct[A]) extends ExtModule(params) - with FixedIOBaseModule[A] + with FixedIOBaseModule[A] { + final val io: A = FixedIO.bindPorts(ioGenerator, "io")(UnlocatableSourceInfo).asInstanceOf[A] + endIOCreation() +} + +object FixedIOExtModule { + implicit class FixedIOExtModuleDefinitionExtensions[A]( + private val ___module: Definition[FixedIOExtModule[A]] + ) extends AnyVal { + def io(implicit lookup: Lookupable[A]): lookup.C = { + implicit val mg: MacroGenerated = new MacroGenerated {} + ___module._lookup(_.io) + } + } + implicit class FixedIOExtModuleInstanceExtensions[A]( + private val ___module: Instance[FixedIOExtModule[A]] + ) extends AnyVal { + def io(implicit lookup: Lookupable[A]): lookup.C = { + implicit val mg: MacroGenerated = new MacroGenerated {} + ___module._lookup(_.io) + } + } +} diff --git a/src/main/scala-3/chisel3/FixedIOModule.scala b/src/main/scala-3/chisel3/FixedIOModule.scala index f228911f0ad..37472471374 100644 --- a/src/main/scala-3/chisel3/FixedIOModule.scala +++ b/src/main/scala-3/chisel3/FixedIOModule.scala @@ -3,18 +3,29 @@ package chisel3 import chisel3.experimental.{BaseModule, ExtModule, Param} +import chisel3.experimental.dataview.DataProduct +import chisel3.experimental.hierarchy.{instantiable, public} /** A module or external module whose IO is generated from a specific generator. * This module may have no additional IO created other than what is specified * by its `ioGenerator` abstract member. + * + * `A` may be any type that has a [[chisel3.experimental.dataview.DataProduct]] + * implementation, including a single [[Data]], a tuple of `Data`-containing + * types, or a `Seq` of `Data`-containing types. Each contained [[Data]] is + * turned into a port; for the single-`Data` case [[FlatIO]] is used so that + * existing modules are unaffected. */ -sealed trait FixedIOBaseModule[A <: Data] extends BaseModule { +sealed trait FixedIOBaseModule[A] extends BaseModule { /** A generator of IO */ protected def ioGenerator: A - final val io = FlatIO(ioGenerator) - endIOCreation() + /** Evidence that `A` contains [[Data]] elements that can be turned into ports. */ + protected implicit def ioDataProduct: DataProduct[A] + + /** The IO of this module, of shape `A` whose contained [[Data]] are ports. */ + def io: A } @@ -23,7 +34,14 @@ sealed trait FixedIOBaseModule[A <: Data] extends BaseModule { * * @param ioGenerator */ -class FixedIORawModule[A <: Data](final val ioGenerator: A) extends RawModule with FixedIOBaseModule[A] +@instantiable +class FixedIORawModule[A](final val ioGenerator: A)(using val ioDataProduct: DataProduct[A]) + extends RawModule + with FixedIOBaseModule[A] { + @public + final val io: A = FixedIO.bindPorts(ioGenerator, "io").asInstanceOf[A] + endIOCreation() +} /** A Chisel module whose IO (in addition to [[clock]] and [[reset]]) is determined * by an IO generator. This module cannot have additional IO created by modules that @@ -31,7 +49,14 @@ class FixedIORawModule[A <: Data](final val ioGenerator: A) extends RawModule wi * * @param ioGenerator */ -class FixedIOModule[A <: Data](final val ioGenerator: A) extends Module with FixedIOBaseModule[A] +@instantiable +class FixedIOModule[A](final val ioGenerator: A)(using val ioDataProduct: DataProduct[A]) + extends Module + with FixedIOBaseModule[A] { + @public + final val io: A = FixedIO.bindPorts(ioGenerator, "io").asInstanceOf[A] + endIOCreation() +} /** A Chisel blackbox whose IO is determined by an IO generator. This module * cannot have additional IO created by modules that extend it. @@ -39,6 +64,14 @@ class FixedIOModule[A <: Data](final val ioGenerator: A) extends Module with Fix * @param ioGenerator * @param params */ -class FixedIOExtModule[A <: Data](final val ioGenerator: A, params: Map[String, Param] = Map.empty[String, Param]) +@instantiable +class FixedIOExtModule[A]( + final val ioGenerator: A, + params: Map[String, Param] = Map.empty[String, Param] +)(using val ioDataProduct: DataProduct[A]) extends ExtModule(params) - with FixedIOBaseModule[A] + with FixedIOBaseModule[A] { + @public + final val io: A = FixedIO.bindPorts(ioGenerator, "io").asInstanceOf[A] + endIOCreation() +} diff --git a/src/test/scala-2/chiselTests/FixedIOModuleSpec.scala b/src/test/scala/chiselTests/FixedIOModuleSpec.scala similarity index 88% rename from src/test/scala-2/chiselTests/FixedIOModuleSpec.scala rename to src/test/scala/chiselTests/FixedIOModuleSpec.scala index 179e7fc889b..70c8ad14a61 100644 --- a/src/test/scala-2/chiselTests/FixedIOModuleSpec.scala +++ b/src/test/scala/chiselTests/FixedIOModuleSpec.scala @@ -336,4 +336,57 @@ class FixedIOModuleSpec extends AnyFlatSpec with Matchers with FileCheck { |""".stripMargin ) } + + "FixedIORawModule" should "support a tuple IO generator with multiple Data" in { + class Foo extends FixedIORawModule[(UInt, UInt)]((Output(UInt(8.W)), Input(UInt(4.W)))) { + io._1 := io._2 + } + + ChiselStage + .emitCHIRRTL(new Foo) + .fileCheck()( + """|CHECK: module Foo : + |CHECK: output io_1 : UInt<8> + |CHECK: input io_2 : UInt<4> + |""".stripMargin + ) + } + + "FixedIORawModule" should "support a tuple IO generator containing Bundles" in { + class InBundle extends Bundle { + val a = UInt(8.W) + val b = Bool() + } + class OutBundle extends Bundle { + val x = UInt(8.W) + } + class Foo extends FixedIORawModule[(InBundle, OutBundle)]((Input(new InBundle), Output(new OutBundle))) { + io._2.x := io._1.a + } + + ChiselStage + .emitCHIRRTL(new Foo) + .fileCheck()( + """|CHECK: module Foo : + |CHECK: input io_1 : { a : UInt<8>, b : UInt<1>} + |CHECK: output io_2 : { x : UInt<8>} + |""".stripMargin + ) + } + + "FixedIORawModule" should "support a Seq IO generator" in { + class Foo extends FixedIORawModule[Seq[UInt]](Seq.fill(3)(Output(UInt(4.W)))) { + io.foreach(_ := 0.U) + } + + ChiselStage + .emitCHIRRTL(new Foo) + .fileCheck()( + """|CHECK: module Foo : + |CHECK: output io_0 : UInt<4> + |CHECK: output io_1 : UInt<4> + |CHECK: output io_2 : UInt<4> + |""".stripMargin + ) + } }