diff options
| author | Albert Magyar | 2021-04-05 14:26:34 -0700 |
|---|---|---|
| committer | GitHub | 2021-04-05 14:26:34 -0700 |
| commit | ed5e03f960d89c8b5c999e030b2ae4586fa4a976 (patch) | |
| tree | e67a43a77c4c0fe4b729705d2c725c9e0c11943f /src/main/scala/firrtl/passes | |
| parent | ca8b670eac0b0def66249738e52ef8137d30a8b5 (diff) | |
| parent | 1afa3b40f78d781ca1f242b49ca3a56d6cbc57e4 (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/main/scala/firrtl/passes')
4 files changed, 200 insertions, 19 deletions
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))) + } } |
