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 | |
| 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')
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") + } +} |
