Skip to content

Confusing bundle connection behavior with SRAM API #4945

@tymcauley

Description

@tymcauley

Type of issue: Bug Report

Please provide the steps to reproduce the problem:

The scala-cli snippet below gives a few different errors that you can cause:

  • Set readerUnintError to true
  • Set writerUnintError to true
  • Comment out the existing myMem.writePorts connection, and use one of the other commented-out options
//> using scala 2.13.16
//> using repository https://s01.oss.sonatype.org/content/repositories/snapshots
//> using dep org.chipsalliance::chisel:7.0.0-RC1+28-2f256a83-SNAPSHOT
//> using plugin org.chipsalliance:::chisel-plugin:7.0.0-RC1+28-2f256a83-SNAPSHOT
//> using options -unchecked -deprecation -language:reflectiveCalls -feature -Xcheckinit -Xfatal-warnings -Ywarn-dead-code -Ywarn-unused -Ymacro-annotations

import chisel3._
import chisel3.util._

class Reader(readPort: MemoryReadPort[UInt], uninitError: Boolean = false) extends Module {
  // `Flipped` because default `MemoryReadPort` alignment is `Input`
  val port = IO(Flipped(readPort))

  if (uninitError) {
    port :<= DontCare
  } else {
    port.address :<= DontCare
    port.enable :<= DontCare
  }
}

class Writer(writePort: MemoryWritePort[UInt], uninitError: Boolean = false) extends Module {
  // `Flipped` because default `MemoryWritePort` alignment is `Input`
  val port = IO(Flipped(writePort))

  if (uninitError) {
    port :<= DontCare
  } else {
    port.address :<= DontCare
    port.data :<= DontCare
    port.enable :<= DontCare
  }
}

class Foo extends Module {
  val myMem = SRAM(8, UInt(5.W), 1, 1, 0)

  val readPort = chiselTypeOf(myMem.readPorts.head)
  val writePort = chiselTypeOf(myMem.writePorts.head)

  // If set to `true`, firtool error: sink * not fully initialized in "Reader"
  val readerUnintError = false

  // If set to `true`, firtool error: sink * not fully initialized in "Writer"
  val writerUnintError = false

  val reader = Module(new Reader(readPort, readerUnintError))
  val writer = Module(new Writer(writePort, writerUnintError))

  // This works
  reader.port :<>= myMem.readPorts.head

  // firtool error: sink * not fully initialized in "Foo"
  // myMem.writePorts.head :<= writer.port

  // Chisel error: port.* in Writer cannot be written from module Foo.
  // myMem.writePorts.head :<>= writer.port

  // This works, but the LHS-vs-RHS isn't intuitive.
  // writer.port :>= myMem.writePorts.head

  // This works, has the LHS-vs-RHS I would expect, but is inconvenient due to enumerating each bundle element
  myMem.writePorts.head.address :<= writer.port.address
  myMem.writePorts.head.data :<= writer.port.data
  myMem.writePorts.head.enable :<= writer.port.enable
}

object Main extends App {
  import _root_.circt.stage.ChiselStage
  println(
    ChiselStage.emitSystemVerilog(
      new Foo,
      firtoolOpts = Array("-disable-all-randomization", "-strip-debug-info", "-default-layer-specialization=enable")
    )
  )
}

What is the current behavior?

I'm unable to make connections to the Memory*Port bundles that I would expect, perhaps due to their default alignment being Input rather than Output:

/** A bundle of signals representing a memory read port.
*
* @tparam tpe The data type of the memory port
* @param addrWidth The width of the address signal
*/
class MemoryReadPort[T <: Data](tpe: T, addrWidth: Int) extends Bundle {
val address = Input(UInt(addrWidth.W))
val enable = Input(Bool())
val data = Output(tpe)
}
/** A bundle of signals representing a memory write port.
*
* @tparam tpe The data type of the memory port
* @param addrWidth The width of the address signal
* @param masked Whether this read/write port should have an optional mask.
*
* @note `masked` is only valid if tpe is a Vec; if this is not the case no mask will be initialized
* regardless of the value of `masked`.
*/
class MemoryWritePort[T <: Data](tpe: T, addrWidth: Int, masked: Boolean) extends Bundle {
val address = Input(UInt(addrWidth.W))
val enable = Input(Bool())
val data = Input(tpe)
val mask: Option[Vec[Bool]] = if (masked) {
val maskSize = tpe match {
case vec: Vec[_] => vec.size
case _ => 0
}
Some(Input(Vec(maskSize, Bool())))
} else {
None
}
}
/** A bundle of signals representing a memory read/write port.
*
* @tparam tpe The data type of the memory port
* @param addrWidth The width of the address signal
* @param masked Whether this read/write port should have an optional mask.
*
* @note `masked` is only valid if tpe is a Vec; if this is not the case no mask will be initialized
* regardless of the value of `masked`.
*/
class MemoryReadWritePort[T <: Data](tpe: T, addrWidth: Int, masked: Boolean) extends Bundle {
val address = Input(UInt(addrWidth.W))
val enable = Input(Bool())
val isWrite = Input(Bool())
val readData = Output(tpe)
val writeData = Input(tpe)
val mask: Option[Vec[Bool]] = if (masked) {
val maskSize = tpe match {
case vec: Vec[_] => vec.size
case _ => 0
}
Some(Input(Vec(maskSize, Bool())))
} else {
None
}
}

Rather than being able to make connections like port :<= DontCare, I either need to break out the fields of port and connect to them individually (port.address :<= DontCare; port.enable :<= DontCare; ...), or I need to use an unintuitive LHS-vs-RHS style: DontCare :>= port. Is this the intended behavior, or is this just an annoying edge case in the Connectable (or SRAM) API?

What is the expected behavior?

I should be able to connect bundles with matching alignment, and put the producer and consumer on the sides of the Connectable operator that I would expect.

Please tell us about your environment:
- version: 7.0.0-RC1+28-2f256a83-SNAPSHOT
- OS: macOS 15.5

Other Information

What is the use case for changing the behavior?

Making the Connectable API more convenient for users.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions