aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAlbert Magyar2021-04-05 14:26:34 -0700
committerGitHub2021-04-05 14:26:34 -0700
commited5e03f960d89c8b5c999e030b2ae4586fa4a976 (patch)
treee67a43a77c4c0fe4b729705d2c725c9e0c11943f /src
parentca8b670eac0b0def66249738e52ef8137d30a8b5 (diff)
parent1afa3b40f78d781ca1f242b49ca3a56d6cbc57e4 (diff)
Merge pull request #2111 from chipsalliance/fpga-backend
Add -fpga flag to enable FPGA-oriented compilation strategies (currently for memories)
Diffstat (limited to 'src')
-rw-r--r--src/main/scala/firrtl/backends/verilog/VerilogEmitter.scala113
-rw-r--r--src/main/scala/firrtl/passes/memlib/InferReadWrite.scala47
-rw-r--r--src/main/scala/firrtl/passes/memlib/SeparateWriteClocks.scala74
-rw-r--r--src/main/scala/firrtl/passes/memlib/SetDefaultReadUnderWrite.scala57
-rw-r--r--src/main/scala/firrtl/passes/memlib/VerilogMemDelays.scala41
-rw-r--r--src/main/scala/firrtl/stage/FirrtlCli.scala3
-rw-r--r--src/main/scala/firrtl/stage/FirrtlCompilerTargets.scala52
-rw-r--r--src/main/scala/firrtl/transforms/SimplifyMems.scala8
-rw-r--r--src/test/scala/firrtlTests/InferReadWriteSpec.scala51
-rw-r--r--src/test/scala/firrtlTests/InfoSpec.scala10
-rw-r--r--src/test/scala/firrtlTests/SeparateWriteClocksSpec.scala68
11 files changed, 456 insertions, 68 deletions
diff --git a/src/main/scala/firrtl/backends/verilog/VerilogEmitter.scala b/src/main/scala/firrtl/backends/verilog/VerilogEmitter.scala
index f854b33a..66df12e0 100644
--- a/src/main/scala/firrtl/backends/verilog/VerilogEmitter.scala
+++ b/src/main/scala/firrtl/backends/verilog/VerilogEmitter.scala
@@ -1065,32 +1065,53 @@ class VerilogEmitter extends SeqTransform with Emitter {
val decl = if (fullSize > (1 << 29)) "reg /* sparse */" else "reg"
declareVectorType(decl, sx.name, sx.dataType, sx.depth, sx.info)
initialize_mem(sx, options)
- if (sx.readLatency != 0 || sx.writeLatency != 1)
+ // Currently, no idiomatic way to directly emit write-first RW ports
+ val hasComplexRW = (sx.readwriters.nonEmpty &&
+ (sx.readLatency != 1 || sx.readUnderWrite == ReadUnderWrite.New))
+ if (sx.readLatency > 1 || sx.writeLatency != 1 || hasComplexRW)
throw EmitterException(
- "All memories should be transformed into " +
- "blackboxes or combinational by previous passses"
+ Seq(
+ s"Memory ${sx.name} is too complex to emit directly.",
+ "Consider running VerilogMemDelays to simplify complex memories.",
+ "Alternatively, add the --repl-seq-mem flag to replace memories with blackboxes."
+ ).mkString(" ")
)
+ def createMemWire(name: String, tpe: Type, rhs: InfoExpr): Unit = {
+ declare("wire", name, tpe, MultiInfo(sx.info, rhs.info), rhs.expr)
+ }
+
for (r <- sx.readers) {
val data = memPortField(sx, r, "data")
val addr = memPortField(sx, r, "addr")
- // Ports should share an always@posedge, so can't have intermediary wire
-
- declare("wire", LowerTypes.loweredName(data), data.tpe, sx.info)
- declare("wire", LowerTypes.loweredName(addr), addr.tpe, sx.info)
- // declare("wire", LowerTypes.loweredName(en), en.tpe)
-
- //; Read port
- assign(addr, netlist(addr))
- // assign(en, netlist(en)) //;Connects value to m.r.en
- val mem = WRef(sx.name, memType(sx), MemKind, UnknownFlow)
- val memPort = WSubAccess(mem, addr, sx.dataType, UnknownFlow)
+ val en = memPortField(sx, r, "en")
+ val memPort = WSubAccess(WRef(sx), addr, sx.dataType, UnknownFlow)
val depthValue = UIntLiteral(sx.depth, IntWidth(sx.depth.bitLength))
val garbageGuard = DoPrim(Geq, Seq(addr, depthValue), Seq(), UnknownType)
- if ((sx.depth & (sx.depth - 1)) == 0)
- assign(data, memPort, sx.info)
- else
- garbageAssign(data, memPort, garbageGuard, sx.info)
+ val clkSource = netlist(memPortField(sx, r, "clk")).expr
+
+ createMemWire(LowerTypes.loweredName(en), en.tpe, netlist(en))
+
+ if (sx.readLatency == 1 && sx.readUnderWrite != ReadUnderWrite.Old) {
+ val InfoExpr(addrInfo, addrDriver) = netlist(addr)
+ declare("reg", LowerTypes.loweredName(addr), addr.tpe, sx.info)
+ initialize(WRef(LowerTypes.loweredName(addr), addr.tpe), zero, zero)
+ update(addr, addrDriver, clkSource, en, addrInfo)
+ } else {
+ createMemWire(LowerTypes.loweredName(addr), addr.tpe, netlist(addr))
+ }
+
+ if (sx.readLatency == 1 && sx.readUnderWrite == ReadUnderWrite.Old) {
+ declare("reg", LowerTypes.loweredName(data), data.tpe, sx.info)
+ initialize(WRef(LowerTypes.loweredName(data), data.tpe), zero, zero)
+ update(data, memPort, clkSource, en, sx.info)
+ } else {
+ declare("wire", LowerTypes.loweredName(data), data.tpe, sx.info)
+ if ((sx.depth & (sx.depth - 1)) == 0)
+ assign(data, memPort, sx.info)
+ else
+ garbageAssign(data, memPort, garbageGuard, sx.info)
+ }
}
for (w <- sx.writers) {
@@ -1098,31 +1119,41 @@ class VerilogEmitter extends SeqTransform with Emitter {
val addr = memPortField(sx, w, "addr")
val mask = memPortField(sx, w, "mask")
val en = memPortField(sx, w, "en")
- //Ports should share an always@posedge, so can't have intermediary wire
- // TODO should we use the info here for anything?
- val InfoExpr(_, clk) = netlist(memPortField(sx, w, "clk"))
-
- declare("wire", LowerTypes.loweredName(data), data.tpe, sx.info)
- declare("wire", LowerTypes.loweredName(addr), addr.tpe, sx.info)
- declare("wire", LowerTypes.loweredName(mask), mask.tpe, sx.info)
- declare("wire", LowerTypes.loweredName(en), en.tpe, sx.info)
-
- // Write port
- assign(data, netlist(data))
- assign(addr, netlist(addr))
- assign(mask, netlist(mask))
- assign(en, netlist(en))
-
- val mem = WRef(sx.name, memType(sx), MemKind, UnknownFlow)
- val memPort = WSubAccess(mem, addr, sx.dataType, UnknownFlow)
- update(memPort, data, clk, AND(en, mask), sx.info)
+
+ val clkSource = netlist(memPortField(sx, w, "clk")).expr
+
+ createMemWire(LowerTypes.loweredName(data), data.tpe, netlist(data))
+ createMemWire(LowerTypes.loweredName(addr), addr.tpe, netlist(addr))
+ createMemWire(LowerTypes.loweredName(mask), mask.tpe, netlist(mask))
+ createMemWire(LowerTypes.loweredName(en), en.tpe, netlist(en))
+
+ val memPort = WSubAccess(WRef(sx), addr, sx.dataType, UnknownFlow)
+ update(memPort, data, clkSource, AND(en, mask), sx.info)
+ }
+
+ for (rw <- sx.readwriters) {
+ val rdata = memPortField(sx, rw, "rdata")
+ val wdata = memPortField(sx, rw, "wdata")
+ val addr = memPortField(sx, rw, "addr")
+ val en = memPortField(sx, rw, "en")
+ val wmode = memPortField(sx, rw, "wmode")
+ val wmask = memPortField(sx, rw, "wmask")
+ val memPort = WSubAccess(WRef(sx), addr, sx.dataType, UnknownFlow)
+
+ val clkSource = netlist(memPortField(sx, rw, "clk")).expr
+
+ createMemWire(LowerTypes.loweredName(wdata), wdata.tpe, netlist(wdata))
+ createMemWire(LowerTypes.loweredName(addr), addr.tpe, netlist(addr))
+ createMemWire(LowerTypes.loweredName(wmode), wmode.tpe, netlist(wmode))
+ createMemWire(LowerTypes.loweredName(wmask), wmask.tpe, netlist(wmask))
+ createMemWire(LowerTypes.loweredName(en), en.tpe, netlist(en))
+
+ declare("reg", LowerTypes.loweredName(rdata), rdata.tpe, sx.info)
+ initialize(WRef(LowerTypes.loweredName(rdata), rdata.tpe), zero, zero)
+ update(rdata, memPort, clkSource, en, sx.info)
+ update(memPort, wdata, clkSource, AND(en, AND(wmode, wmask)), sx.info)
}
- if (sx.readwriters.nonEmpty)
- throw EmitterException(
- "All readwrite ports should be transformed into " +
- "read & write ports by previous passes"
- )
case _ =>
}
}
diff --git a/src/main/scala/firrtl/passes/memlib/InferReadWrite.scala b/src/main/scala/firrtl/passes/memlib/InferReadWrite.scala
index 0bb94452..39c79bc6 100644
--- a/src/main/scala/firrtl/passes/memlib/InferReadWrite.scala
+++ b/src/main/scala/firrtl/passes/memlib/InferReadWrite.scala
@@ -78,6 +78,16 @@ object InferReadWritePass extends Pass {
case sx => sx
}
+ /* If the ports share the same address in an undefined-collision SyncReadMem, reads issued while the write
+ * is enabled are *always* undefined; we may treat the read as if it were gated by the complement of w.en.
+ * Though not a strict requirement, this currently applies only to single-cycle read/write memories.
+ * N.B. for aggregate-typed memories, the spec is conservative and 'undefined' is not a function of the
+ * write mask, allowing optimization regardless of mask value. This must be revisited if the spec changes.
+ */
+ private def canOptimizeCollidingRW(mem: DefMemory): Boolean = {
+ mem.readUnderWrite == ReadUnderWrite.Undefined && mem.readLatency == 1 && mem.writeLatency == 1
+ }
+
def inferReadWriteStmt(connects: Connects, repl: Netlist, stmts: Statements)(s: Statement): Statement = s match {
// infer readwrite ports only for non combinational memories
case mem: DefMemory if mem.readLatency > 0 =>
@@ -94,7 +104,10 @@ object InferReadWritePass extends Pass {
val proofOfMutualExclusion = wenProductTerms.find(a => renProductTerms.exists(b => checkComplement(a, b)))
val wclk = getOrigin(connects)(memPortField(mem, w, "clk"))
val rclk = getOrigin(connects)(memPortField(mem, r, "clk"))
- if (weq(wclk, rclk) && proofOfMutualExclusion.nonEmpty) {
+ val waddr = getOrigin(connects)(memPortField(mem, w, "addr"))
+ val raddr = getOrigin(connects)(memPortField(mem, r, "addr"))
+ val optimizeCollision = (weq(waddr, raddr) && canOptimizeCollidingRW(mem))
+ if (weq(wclk, rclk) && (proofOfMutualExclusion.nonEmpty || optimizeCollision)) {
val rw = namespace.newName("rw")
val rwExp = WSubField(WRef(mem.name), rw)
readwriters += rw
@@ -104,28 +117,32 @@ object InferReadWritePass extends Pass {
repl(memPortField(mem, r, "en")) = EmptyExpression
repl(memPortField(mem, r, "addr")) = EmptyExpression
repl(memPortField(mem, r, "data")) = WSubField(rwExp, "rdata")
- repl(memPortField(mem, w, "clk")) = EmptyExpression
- repl(memPortField(mem, w, "en")) = EmptyExpression
- repl(memPortField(mem, w, "addr")) = EmptyExpression
+ repl(memPortField(mem, w, "clk")) = WSubField(rwExp, "clk")
repl(memPortField(mem, w, "data")) = WSubField(rwExp, "wdata")
repl(memPortField(mem, w, "mask")) = WSubField(rwExp, "wmask")
- stmts += Connect(NoInfo, WSubField(rwExp, "wmode"), proofOfMutualExclusion.get)
- stmts += Connect(NoInfo, WSubField(rwExp, "clk"), wclk)
stmts += Connect(
NoInfo,
WSubField(rwExp, "en"),
DoPrim(Or, Seq(connects(memPortField(mem, r, "en")), connects(memPortField(mem, w, "en"))), Nil, BoolType)
)
- stmts += Connect(
- NoInfo,
- WSubField(rwExp, "addr"),
- Mux(
- connects(memPortField(mem, w, "en")),
- connects(memPortField(mem, w, "addr")),
- connects(memPortField(mem, r, "addr")),
- UnknownType
+ if (optimizeCollision) {
+ repl(memPortField(mem, w, "en")) = WSubField(rwExp, "wmode")
+ repl(memPortField(mem, w, "addr")) = WSubField(rwExp, "addr")
+ } else {
+ repl(memPortField(mem, w, "en")) = EmptyExpression
+ repl(memPortField(mem, w, "addr")) = EmptyExpression
+ stmts += Connect(NoInfo, WSubField(rwExp, "wmode"), proofOfMutualExclusion.get)
+ stmts += Connect(
+ NoInfo,
+ WSubField(rwExp, "addr"),
+ Mux(
+ connects(memPortField(mem, w, "en")),
+ connects(memPortField(mem, w, "addr")),
+ connects(memPortField(mem, r, "addr")),
+ UnknownType
+ )
)
- )
+ }
}
}
if (readwriters.isEmpty) mem
diff --git a/src/main/scala/firrtl/passes/memlib/SeparateWriteClocks.scala b/src/main/scala/firrtl/passes/memlib/SeparateWriteClocks.scala
new file mode 100644
index 00000000..f526f64b
--- /dev/null
+++ b/src/main/scala/firrtl/passes/memlib/SeparateWriteClocks.scala
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package firrtl.passes
+package memlib
+
+import firrtl._
+import firrtl.ir._
+import firrtl.passes.LowerTypes
+import firrtl.options.{Dependency, OptionsException}
+
+/**
+ * This transform introduces an intermediate wire on the clock field of each write port of synchronous-read memories
+ * that have *multiple* write/readwrite ports and undefined read-under-write collision behavior. Ultimately, the
+ * introduction of these intermediate wires does not change which clock net clocks each port; therefore, the purpose of
+ * this transform is to help generate Verilog that is more amenable to inference of RAM macros with multiple write
+ * ports in FPGA synthesis flows. This change will cause each write and each readwrite port to be emitted in a separate
+ * clocked procedure, yielding multiple benefits:
+ *
+ * 1) Separate write procedures avoid implicitly constraining cross-port read-write and write-write collision behaviors
+ * 2) The preference for separate clocked procedures for each write port is explicitly specified by Intel and Xilinx
+ *
+ * While this feature is not intended to be vendor-specific, inference of *multiple-write* RAM macros from behavioral
+ * Verilog or VHDL requires both advanced underlying RAM primitives and advanced synthesis tools. Currently, mapping
+ * such memories to programmable devices beyond modern Intel and Xilinx architectures can be prohibitive for users.
+ *
+ * Though the emission of separate processes for write ports could be absorbed into the Verilog emitter, the use of a
+ * pure-FIRRTL transform reduces implementation complexity and enhances reliability.
+ */
+class SeparateWriteClocks extends Transform with DependencyAPIMigration {
+ override def prerequisites = Seq(Dependency(passes.RemoveCHIRRTL), Dependency(passes.ExpandConnects))
+ override def optionalPrerequisites = Seq(Dependency[InferReadWrite])
+ override def optionalPrerequisiteOf = Seq(Dependency[SetDefaultReadUnderWrite])
+ override def invalidates(a: Transform): Boolean = a match {
+ case ResolveFlows => true
+ case _ => false
+ }
+
+ private type ExprMap = collection.mutable.HashMap[WrappedExpression, Reference]
+
+ private def onExpr(replaceExprs: ExprMap)(expr: Expression): Expression = expr match {
+ case wsf: WSubField if (replaceExprs.contains(WrappedExpression(wsf))) =>
+ replaceExprs(WrappedExpression(wsf))
+ case e => e.mapExpr(onExpr(replaceExprs))
+ }
+
+ private def isMultiWriteSyncReadUndefinedRUW(mem: DefMemory): Boolean = {
+ (mem.writers.size + mem.readwriters.size) > 1 &&
+ mem.readLatency == 1 && mem.writeLatency == 1 &&
+ mem.readUnderWrite == ReadUnderWrite.Undefined
+ }
+
+ private def onStmt(replaceExprs: ExprMap, ns: Namespace)(stmt: Statement): Statement = stmt match {
+ case mem: DefMemory if isMultiWriteSyncReadUndefinedRUW(mem) =>
+ val clockRefs = (mem.writers ++ mem.readwriters).map { p => MemPortUtils.memPortField(mem, p, "clk") }
+ val clockWireMap = clockRefs.map { pClk =>
+ WrappedExpression(pClk) -> DefWire(mem.info, ns.newName(LowerTypes.loweredName(pClk)), ClockType)
+ }
+ val clockStmts = clockWireMap.flatMap {
+ case (pClk, clkWire) => Seq(clkWire, Connect(mem.info, pClk.e1, Reference(clkWire)))
+ }
+ replaceExprs ++= clockWireMap.map { case (pClk, clkWire) => pClk -> Reference(clkWire) }
+ Block(mem +: clockStmts)
+ case Connect(i, lhs, rhs) => Connect(i, onExpr(replaceExprs)(lhs), rhs)
+ case PartialConnect(i, lhs, rhs) => PartialConnect(i, onExpr(replaceExprs)(lhs), rhs)
+ case IsInvalid(i, invalidated) => IsInvalid(i, onExpr(replaceExprs)(invalidated))
+ case s => s.mapStmt(onStmt(replaceExprs, ns))
+ }
+
+ override def execute(state: CircuitState): CircuitState = {
+ val c = state.circuit
+ val cPrime = c.copy(modules = c.modules.map(m => m.mapStmt(onStmt(new ExprMap, Namespace(m)))))
+ state.copy(circuit = cPrime)
+ }
+}
diff --git a/src/main/scala/firrtl/passes/memlib/SetDefaultReadUnderWrite.scala b/src/main/scala/firrtl/passes/memlib/SetDefaultReadUnderWrite.scala
new file mode 100644
index 00000000..d5646099
--- /dev/null
+++ b/src/main/scala/firrtl/passes/memlib/SetDefaultReadUnderWrite.scala
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package firrtl.passes
+package memlib
+
+import firrtl._
+import firrtl.ir._
+import firrtl.options.{Dependency, OptionsException}
+import firrtl.annotations.NoTargetAnnotation
+
+sealed trait DefaultReadUnderWriteAnnotation extends NoTargetAnnotation
+
+/** This annotation directs the [[SetDefaultReadUnderWrite]] transform to assign a default value of 'old' (read-first
+ * behavior) to all synchronous-read memories with 'undefined' read-under-write parameters.
+ */
+case object DefaultReadFirstAnnotation extends DefaultReadUnderWriteAnnotation
+
+/** This annotation directs the [[SetDefaultReadUnderWrite]] transform to assign a default value of 'new' (write-first
+ * behavior) to all synchronous-read memories with 'undefined' read-under-write parameters.
+ */
+case object DefaultWriteFirstAnnotation extends DefaultReadUnderWriteAnnotation
+
+/**
+ * Adding a [[DefaultReadUnderWriteAnnotation]] and running the [[SetDefaultReadUnderWrite]] transform will cause all
+ * synchronous-read memories with 'undefined' read-under-write parameters to be assigned a default parameter value,
+ * either 'old' (read-first behavior) or 'new' (write-first behavior). This can help generate Verilog that is amenable
+ * to RAM macro inference for various FPGA tools, or it can be used to satisfy other downstream design constraints.
+ */
+class SetDefaultReadUnderWrite extends Transform with DependencyAPIMigration {
+ override def prerequisites = firrtl.stage.Forms.HighForm
+ override def optionalPrerequisites = Seq(Dependency[InferReadWrite])
+ override def optionalPrerequisiteOf = Seq(Dependency(VerilogMemDelays))
+ override def invalidates(a: Transform): Boolean = false
+
+ private def onStmt(defaultRUW: ReadUnderWrite.Value)(stmt: Statement): Statement = stmt match {
+ case mem: DefMemory if (mem.readLatency > 0 && mem.readUnderWrite == ReadUnderWrite.Undefined) =>
+ mem.copy(readUnderWrite = defaultRUW)
+ case s => s.mapStmt(onStmt(defaultRUW))
+ }
+
+ override def execute(state: CircuitState): CircuitState = {
+ val c = state.circuit
+ val ruwDefaults = state.annotations
+ .collect({
+ case DefaultReadFirstAnnotation => ReadUnderWrite.Old
+ case DefaultWriteFirstAnnotation => ReadUnderWrite.New
+ })
+ .toSet
+ if (ruwDefaults.size == 0) {
+ state
+ } else if (ruwDefaults.size == 1) {
+ state.copy(circuit = c.copy(modules = c.modules.map(m => m.mapStmt(onStmt(ruwDefaults.head)))))
+ } else {
+ throw new OptionsException("Conflicting default read-under-write settings.")
+ }
+ }
+}
diff --git a/src/main/scala/firrtl/passes/memlib/VerilogMemDelays.scala b/src/main/scala/firrtl/passes/memlib/VerilogMemDelays.scala
index 143b925a..a9b42eba 100644
--- a/src/main/scala/firrtl/passes/memlib/VerilogMemDelays.scala
+++ b/src/main/scala/firrtl/passes/memlib/VerilogMemDelays.scala
@@ -10,12 +10,22 @@ import firrtl.Mappers._
import firrtl.traversals.Foreachers._
import firrtl.transforms
import firrtl.options.Dependency
+import firrtl.annotations.NoTargetAnnotation
import MemPortUtils._
import WrappedExpression._
import collection.mutable
+/**
+ * Adding this annotation will allow the [[VerilogMemDelays]] transform to let 'simple' synchronous-read memories to
+ * pass through without explicitly breaking them apart into combinational-read memories and pipeline registers. Here,
+ * 'simple' memories are defined as those that have one-cycle read and write latencies AND either no readwrite ports or
+ * read-under-write behavior that is either 'undefined' or 'old'. This second restriction avoids the particularly
+ * complex case of blending FIRRTL readwrite port semantics with cross-port 'bypassing' of new data on collisions.
+ */
+case object PassthroughSimpleSyncReadMemsAnnotation extends NoTargetAnnotation
+
object MemDelayAndReadwriteTransformer {
// Representation of a group of signals and associated valid signals
case class WithValid(valid: Expression, payload: Seq[Expression])
@@ -77,13 +87,14 @@ object MemDelayAndReadwriteTransformer {
*
* @note The final transformed module is found in the (sole public) field [[transformed]]
*/
-class MemDelayAndReadwriteTransformer(m: DefModule) {
+class MemDelayAndReadwriteTransformer(m: DefModule, passthroughSimpleSyncReadMems: Boolean = false) {
import MemDelayAndReadwriteTransformer._
private val ns = Namespace(m)
private val netlist = new collection.mutable.HashMap[WrappedExpression, Expression]
private val exprReplacements = new collection.mutable.HashMap[WrappedExpression, Expression]
private val newConns = new mutable.ArrayBuffer[Connect]
+ private val passthroughMems = new collection.mutable.HashSet[WrappedExpression]
private def findMemConns(s: Statement): Unit = s match {
case Connect(_, loc, expr) if (kind(loc) == MemKind) => netlist(we(loc)) = expr
@@ -95,7 +106,15 @@ class MemDelayAndReadwriteTransformer(m: DefModule) {
case ex => ex
}
+ def canPassthrough(mem: DefMemory): Boolean = {
+ (mem.readLatency <= 1 && mem.writeLatency == 1 &&
+ (mem.readwriters.isEmpty || (mem.readLatency == 1 && mem.readUnderWrite != ReadUnderWrite.New)))
+ }
+
private def transform(s: Statement): Statement = s.map(transform) match {
+ case mem: DefMemory if passthroughSimpleSyncReadMems && canPassthrough(mem) =>
+ passthroughMems += WRef(mem)
+ mem
case mem: DefMemory =>
// Per-memory bookkeeping
val portNS = Namespace(mem.readers ++ mem.writers)
@@ -163,7 +182,13 @@ class MemDelayAndReadwriteTransformer(m: DefModule) {
newConns ++= (readStmts ++ writeStmts).flatMap(_.conns)
Block(newMem +: (readStmts ++ writeStmts).flatMap(_.decls))
- case sx: Connect if kind(sx.loc) == MemKind => EmptyStmt // Filter old mem connections
+ case sx: Connect if kind(sx.loc) == MemKind =>
+ val (memRef, _) = Utils.splitRef(sx.loc)
+ // Filter old mem connections for *transformed* memories only
+ if (passthroughMems(WrappedExpression(memRef)))
+ sx
+ else
+ EmptyStmt
case sx => sx.map(swapMemRefs)
}
@@ -188,6 +213,14 @@ object VerilogMemDelays extends Pass {
case _ => false
}
- def transform(m: DefModule): DefModule = (new MemDelayAndReadwriteTransformer(m)).transformed
- def run(c: Circuit): Circuit = c.copy(modules = c.modules.map(transform))
+ private def transform(m: DefModule): DefModule = (new MemDelayAndReadwriteTransformer(m)).transformed
+
+ @deprecated("VerilogMemDelays will change from a Pass to a Transform in FIRRTL 1.6.", "FIRRTL 1.5")
+ def run(c: Circuit): Circuit = c.copy(modules = c.modules.map(transform))
+
+ override def execute(state: CircuitState): CircuitState = {
+ val enablePassthrough = state.annotations.contains(PassthroughSimpleSyncReadMemsAnnotation)
+ def transform(m: DefModule) = (new MemDelayAndReadwriteTransformer(m, enablePassthrough)).transformed
+ state.copy(circuit = state.circuit.copy(modules = state.circuit.modules.map(transform)))
+ }
}
diff --git a/src/main/scala/firrtl/stage/FirrtlCli.scala b/src/main/scala/firrtl/stage/FirrtlCli.scala
index 8be5fb74..9cfa6be9 100644
--- a/src/main/scala/firrtl/stage/FirrtlCli.scala
+++ b/src/main/scala/firrtl/stage/FirrtlCli.scala
@@ -22,7 +22,8 @@ trait FirrtlCli { this: Shell =>
NoCircuitDedupAnnotation,
WarnNoScalaVersionDeprecation,
PrettyNoExprInlining,
- DisableFold
+ DisableFold,
+ OptimizeForFPGA
)
.map(_.addOptions(parser))
diff --git a/src/main/scala/firrtl/stage/FirrtlCompilerTargets.scala b/src/main/scala/firrtl/stage/FirrtlCompilerTargets.scala
new file mode 100644
index 00000000..662f3dc0
--- /dev/null
+++ b/src/main/scala/firrtl/stage/FirrtlCompilerTargets.scala
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package firrtl.stage
+
+import firrtl.transforms._
+import firrtl.passes.memlib._
+import firrtl.options.{HasShellOptions, ShellOption}
+
+/**
+ * This flag enables a set of options that guide the FIRRTL compilation flow to ultimately generate Verilog that is
+ * more amenable to using for synthesized FPGA designs. Currently, this flag affects only memories, as the need to emit
+ * memories that support downstream inference of hardened RAM macros. These options are not intended to be specialized
+ * to any particular vendor; instead, they aim to emit simple Verilog that more closely reflects traditional
+ * human-written definitions of synchronous-read memories.
+ *
+ * 1) Enable the [[firrtl.passes.memlib.InferReadWrite]] transform to reduce port count, where applicable.
+ *
+ * 2) Use the [[firrtl.transforms.SimplifyMems]] transform to Lower aggregate-typed memories with always-high masks to
+ * packed memories without splitting them into multiple independent ground-typed memories.
+ *
+ * 3) Use the [[firrtl.passes.memlib.SeparateWriteClocks]] transform to ensure that each write port of a
+ * multiple-write, synchronous-read memory with 'undefined' collision behavior ultimately maps to a separate clocked
+ * process in the emitted Verilog. This avoids the issue of implicitly constraining cross-port collision and write
+ * ordering behavior and helps simplify inference of true dual-port RAM macros.
+ *
+ * 4) Use the [[firrtl.passes.memlib.SetDefaultReadUnderWrite]] to specify that memories with undefined
+ * read-under-write behavior should map to emitted microarchitectures characteristic of "read-first" ports by
+ * default. This eliminates the difficulty of inferring a RAM macro that matches the strict semantics of
+ * "write-first" ports.
+ *
+ * 5) Add a [[firrtl.passes.memlib.PassthroughSimpleSyncReadMemsAnnotation]] to allow some synchronous-read memories
+ * and readwrite ports to pass through [[firrtl.passes.memlib.VerilogMemDelays]] without introducing explicit
+ * pipeline registers or splitting ports.
+ */
+object OptimizeForFPGA extends HasShellOptions {
+ private val fpgaAnnos = Seq(
+ InferReadWriteAnnotation,
+ RunFirrtlTransformAnnotation(new InferReadWrite),
+ RunFirrtlTransformAnnotation(new SeparateWriteClocks),
+ DefaultReadFirstAnnotation,
+ RunFirrtlTransformAnnotation(new SetDefaultReadUnderWrite),
+ RunFirrtlTransformAnnotation(new SimplifyMems),
+ PassthroughSimpleSyncReadMemsAnnotation
+ )
+ val options = Seq(
+ new ShellOption[Unit](
+ longOption = "target:fpga",
+ toAnnotationSeq = a => fpgaAnnos,
+ helpText = "Choose compilation strategies that generally favor FPGA targets"
+ )
+ )
+}
diff --git a/src/main/scala/firrtl/transforms/SimplifyMems.scala b/src/main/scala/firrtl/transforms/SimplifyMems.scala
index 8ecc484a..92e19f7e 100644
--- a/src/main/scala/firrtl/transforms/SimplifyMems.scala
+++ b/src/main/scala/firrtl/transforms/SimplifyMems.scala
@@ -6,6 +6,7 @@ package transforms
import firrtl.ir._
import firrtl.Mappers._
import firrtl.annotations._
+import firrtl.options.Dependency
import firrtl.passes._
import firrtl.passes.memlib._
import firrtl.stage.Forms
@@ -21,9 +22,12 @@ import ResolveMaskGranularity._
class SimplifyMems extends Transform with DependencyAPIMigration {
override def prerequisites = Forms.MidForm
- override def optionalPrerequisites = Seq.empty
+ override def optionalPrerequisites = Seq(Dependency[InferReadWrite])
override def optionalPrerequisiteOf = Forms.MidEmitters
- override def invalidates(a: Transform) = false
+ override def invalidates(a: Transform) = a match {
+ case InferTypes => true
+ case _ => false
+ }
def onModule(c: Circuit, renames: RenameMap)(m: DefModule): DefModule = {
val moduleNS = Namespace(m)
diff --git a/src/test/scala/firrtlTests/InferReadWriteSpec.scala b/src/test/scala/firrtlTests/InferReadWriteSpec.scala
index 1fb24297..62969df5 100644
--- a/src/test/scala/firrtlTests/InferReadWriteSpec.scala
+++ b/src/test/scala/firrtlTests/InferReadWriteSpec.scala
@@ -177,4 +177,55 @@ circuit sram6t :
// Check correctness of firrtl
res should containLine(s"mem.rw.wmode <= wen")
}
+
+ def sameAddr(ruw: String): String = {
+ s"""
+ |circuit sram6t :
+ | module sram6t :
+ | input clock : Clock
+ | output io : { flip addr : UInt<11>, flip valid : UInt<1>, flip write : UInt<1>, flip dataIn : UInt<32>, dataOut : UInt<32>}
+ |
+ | mem mem:
+ | data-type => UInt<4>
+ | depth => 64
+ | reader => r
+ | writer => w
+ | read-latency => 1
+ | write-latency => 1
+ | read-under-write => ${ruw}
+ |
+ | mem.r.clk <= clock
+ | mem.r.addr <= io.addr
+ | mem.r.en <= io.valid
+ | io.dataOut <= mem.r.data
+ |
+ | node wen = and(io.valid, io.write)
+ | mem.w.clk <= clock
+ | mem.w.addr <= io.addr
+ | mem.w.en <= wen
+ | mem.w.mask <= UInt(1)
+ | mem.w.data <= io.dataIn""".stripMargin
+ }
+
+ "Infer ReadWrite Ports" should "infer readwrite ports from shared addresses with undefined readUnderWrite" in {
+ val input = sameAddr("undefined")
+ val annos = Seq(memlib.InferReadWriteAnnotation)
+ val res = compileAndEmit(CircuitState(parse(input), HighForm, annos))
+ // Check correctness of firrtl
+ res should containLine(s"mem.rw.wmode <= wen")
+ }
+
+ Seq("old", "new").foreach { ruw =>
+ "Infer ReadWrite Ports" should s"not infer readwrite ports from shared addresses with '${ruw}' readUnderWrite" in {
+ val input = sameAddr(ruw)
+ val annos = Seq(memlib.InferReadWriteAnnotation)
+ intercept[Exception] {
+ compileAndEmit(CircuitState(parse(input), ChirrtlForm, annos))
+ } match {
+ case CustomTransformException(_: InferReadWriteCheckException) => // success
+ case _ => fail()
+ }
+ }
+ }
+
}
diff --git a/src/test/scala/firrtlTests/InfoSpec.scala b/src/test/scala/firrtlTests/InfoSpec.scala
index 43fb6ee1..db4828f6 100644
--- a/src/test/scala/firrtlTests/InfoSpec.scala
+++ b/src/test/scala/firrtlTests/InfoSpec.scala
@@ -91,11 +91,11 @@ class InfoSpec extends FirrtlFlatSpec with FirrtlMatchers {
result should containTree { case DefMemory(Info1, "m", _, _, _, _, _, _, _, _) => true }
result should containLine(s"reg [7:0] m [0:31]; //$Info1")
result should containLine(s"wire [7:0] m_r_data; //$Info1")
- result should containLine(s"wire [4:0] m_r_addr; //$Info1")
- result should containLine(s"wire [7:0] m_w_data; //$Info1")
- result should containLine(s"wire [4:0] m_w_addr; //$Info1")
- result should containLine(s"wire m_w_mask; //$Info1")
- result should containLine(s"wire m_w_en; //$Info1")
+ result should containLine(s"wire [4:0] m_r_addr = addr; //$Info1")
+ result should containLine(s"wire [7:0] m_w_data = 8'h0; //$Info1")
+ result should containLine(s"wire [4:0] m_w_addr = addr; //$Info1")
+ result should containLine(s"wire m_w_mask = 1'h0; //$Info1")
+ result should containLine(s"wire m_w_en = 1'h0; //$Info1")
result should containLine(s"assign m_r_data = m[m_r_addr]; //$Info1")
result should containLine(s"m[m_w_addr] <= m_w_data; //$Info1")
}
diff --git a/src/test/scala/firrtlTests/SeparateWriteClocksSpec.scala b/src/test/scala/firrtlTests/SeparateWriteClocksSpec.scala
new file mode 100644
index 00000000..476a3ae2
--- /dev/null
+++ b/src/test/scala/firrtlTests/SeparateWriteClocksSpec.scala
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package firrtlTests
+
+import firrtl._
+import firrtl.ir._
+import firrtl.passes.memlib.SeparateWriteClocks
+import firrtl.testutils._
+import firrtl.testutils.FirrtlCheckers._
+
+class SeparateWriteClocksSpec extends FirrtlFlatSpec {
+ def transform(input: String): CircuitState = {
+ val csx = (new SeparateWriteClocks).execute(CircuitState(parse(input), MidForm))
+ val emittedCirc = EmittedFirrtlCircuit("top", csx.circuit.serialize, ".fir")
+ csx.copy(annotations = Seq(EmittedFirrtlCircuitAnnotation(emittedCirc)))
+ }
+
+ behavior.of("SeparateWriteClocks")
+
+ it should "add intermediate wires to clocks of multi-write sync-read memories" in {
+ val result = transform(s"""
+ |circuit top:
+ | module top:
+ | input clk: Clock
+ | input raddr: UInt<10>
+ | output rdata: UInt<8>[4]
+ | input waddr_a: UInt<10>
+ | input we_a: UInt<1>
+ | input wdata_a: UInt<8>[4]
+ | input waddr_a: UInt<10>
+ | input we_a: UInt<1>
+ | input wdata_a: UInt<8>[4]
+ |
+ | mem m:
+ | data-type => UInt<8>
+ | depth => 1024
+ | reader => r
+ | writer => w_a
+ | writer => w_b
+ | read-latency => 1
+ | write-latency => 1
+ | read-under-write => undefined
+ |
+ | m.r.clk <= clk
+ | m.r.addr <= raddr
+ | m.r.en <= UInt(1)
+ | rdata <= m.r.data
+ |
+ | m.w_a.clk <= clk
+ | m.w_a.addr <= waddr_a
+ | m.w_a.en <= we_a
+ | m.w_a.mask <= UInt(1)
+ | m.w_a.data <= wdata_a
+ |
+ | m.w_b.clk <= clk
+ | m.w_b.addr <= waddr_b
+ | m.w_b.en <= we_b
+ | m.w_b.mask <= UInt(1)
+ | m.w_b.data <= wdata_b""".stripMargin)
+
+ println(result.circuit.serialize)
+ result should containLine("m.r.clk <= clk")
+ result should containLine("m.w_a.clk <= m_w_a_clk")
+ result should containLine("m.w_b.clk <= m_w_b_clk")
+ result shouldNot containLine("m.w_a.clk <= clk")
+ result shouldNot containLine("m.w_b.clk <= clk")
+ }
+}