Skip to content
This repository has been archived by the owner on Aug 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2111 from chipsalliance/fpga-backend
Browse files Browse the repository at this point in the history
Add -fpga flag to enable FPGA-oriented compilation strategies (currently for memories)
  • Loading branch information
albert-magyar authored Apr 5, 2021
2 parents ca8b670 + 1afa3b4 commit ed5e03f
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 68 deletions.
113 changes: 72 additions & 41 deletions src/main/scala/firrtl/backends/verilog/VerilogEmitter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1065,64 +1065,95 @@ 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) {
val data = memPortField(sx, w, "data")
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 _ =>
}
}
Expand Down
47 changes: 32 additions & 15 deletions src/main/scala/firrtl/passes/memlib/InferReadWrite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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
Expand All @@ -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
Expand Down
74 changes: 74 additions & 0 deletions src/main/scala/firrtl/passes/memlib/SeparateWriteClocks.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
57 changes: 57 additions & 0 deletions src/main/scala/firrtl/passes/memlib/SetDefaultReadUnderWrite.scala
Original file line number Diff line number Diff line change
@@ -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.")
}
}
}
Loading

0 comments on commit ed5e03f

Please sign in to comment.