Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for verilog memory loading. #840

Merged
merged 24 commits into from
Aug 31, 2018
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
521d77e
Ability to load memories at simulation startup
chick Jun 12, 2018
76a9b91
Work in progress
chick Jun 12, 2018
118bb7d
Support for LoadMemory annotation
chick Jun 25, 2018
6cc1353
Support for LoadMemory annotation
chick Jun 25, 2018
0e5cc30
Support for LoadMemory annotation
chick Jun 26, 2018
4f55af1
Support for LoadMemory annotation
chick Jun 26, 2018
14cea70
Merge branch 'master' into load-mem
chick Jun 26, 2018
28580f9
Support for LoadMemory annotation
chick Jul 19, 2018
24571c7
Support for LoadMemory annotation
chick Jul 19, 2018
8a3bd21
Support for LoadMemory annotation
chick Jul 19, 2018
8df88d9
Support for LoadMemory annotation
chick Jul 20, 2018
66fc4ca
Support for LoadMemory annotation
chick Jul 20, 2018
88bf995
Merge branch 'master' into load-mem
chick Jul 26, 2018
754eb7d
Support for LoadMemory annotation
chick Jul 26, 2018
0712c3a
Support for LoadMemory annotation
chick Jul 26, 2018
0b15393
Merge remote-tracking branch 'origin/load-mem' into load-mem
chick Jul 26, 2018
09896d0
Merge branch 'master' into load-mem
chick Aug 15, 2018
cbf1d49
Merge branch 'master' into load-mem
chick Aug 23, 2018
5a1161c
Load memory from file
chick Aug 24, 2018
a6d8ee5
Merge branch 'master' into load-mem
chick Aug 24, 2018
38a1343
Merge branch 'master' into load-mem
chick Aug 30, 2018
60e9627
Loading memories from files
chick Aug 31, 2018
ffb8d2b
Merge remote-tracking branch 'origin/load-mem' into load-mem
chick Aug 31, 2018
9d09b52
Loading memories from files
chick Aug 31, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions src/main/scala/chisel3/util/LoadMemoryTransform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// See LICENSE for license details.

package chisel3.util

import chisel3.MemBase
import chisel3.core.{ChiselAnnotation, RunFirrtlTransform, annotate}
import chisel3.internal.{Builder, InstanceId}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Things in package chisel3.util should try to be like what any library would look like so it should not reach into chisel3.core (import stuff directly from chisel3_ and chisel3.experimental._ instead).

Also it should avoid use of chisel3.internal, and avoid private[chisel3] stuff (like the Builder). I think the only use of Builder is for a warning below that should probably just be an exception, or better yet, a requirement below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, I had to add experimental.annotate and chisel3.InstanceId to the chisel package. But I think that is correct as these are part of the public API

import firrtl.annotations.{MemoryLoadFileType, _}
import firrtl.ir.{Module => _, _}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should this PR follow the style guide suggestion of explicit imports for anything not import chisel3._?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I've seen Jack using import firrtl.{ir => fir} which I kind of liked. Perhaps consider that and you can avoid the need to exclude Module and then use it's full package path below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which style guide are you referring to here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, didn't respond... The one sitting in doc/style.md.

import firrtl.transforms.BlackBoxInlineAnno
import firrtl.{AnnotationSeq, CircuitForm, CircuitState, EmitCircuitAnnotation, LowForm, Transform, VerilogEmitter}

import scala.collection.mutable

/**
* chisel implementation for load memory
* @param target memory to load
* @param fileName name of input file
* @param hexOrBinary use $readmemh or $readmemb
*/
case class ChiselLoadMemoryAnnotation(
target: InstanceId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to make the target InstanceId or should it be the more precise MemBase as in loadMemoryFromFile.apply below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not have any luck changing this. Could not get it to compile despite a number of attempts to work with MemBase type parameters

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a diff that accomplishes what I would like to see

diff --git a/src/main/scala/chisel3/util/LoadMemoryTransform.scala b/src/main/scala/chisel3/util/LoadMemoryTransform.scala
index 40aeb007..480d0467 100644
--- a/src/main/scala/chisel3/util/LoadMemoryTransform.scala
+++ b/src/main/scala/chisel3/util/LoadMemoryTransform.scala
@@ -2,8 +2,8 @@

 package chisel3.util

-import chisel3.MemBase
-import chisel3.core.{ChiselAnnotation, RunFirrtlTransform, annotate}
+import chisel3._
+import chisel3.experimental.{ChiselAnnotation, RunFirrtlTransform, annotate}
 import chisel3.internal.{Builder, InstanceId}
 import firrtl.annotations.{MemoryLoadFileType, _}
 import firrtl.ir.{Module => _, _}
@@ -18,8 +18,8 @@ import scala.collection.mutable
   * @param fileName      name of input file
   * @param hexOrBinary   use $readmemh or $readmemb
   */
-case class ChiselLoadMemoryAnnotation(
-  target:      InstanceId,
+case class ChiselLoadMemoryAnnotation[T <: Data](
+  target:      MemBase[T],
   fileName:    String,
   hexOrBinary: MemoryLoadFileType.FileType = MemoryLoadFileType.Hex
 )
@@ -40,8 +40,8 @@ case class ChiselLoadMemoryAnnotation(


 object loadMemoryFromFile {
-  def apply(
-    memory: MemBase[_],
+  def apply[T <: Data](
+    memory: MemBase[T],
     fileName: String,
     hexOrBinary: MemoryLoadFileType.FileType = MemoryLoadFileType.Hex
   ): Unit = {

fileName: String,
hexOrBinary: MemoryLoadFileType.FileType = MemoryLoadFileType.Hex
)
extends ChiselAnnotation with RunFirrtlTransform {

if(fileName.isEmpty) {
Builder.warning(
s"""LoadMemory from file annotations file empty file name"""
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is supposed to be an error if it gets to post-elaboration toFirrtl (LoadMemoryAnnotation is supposed to be treating this as an exception). I'd suggest either not emitting a warning or emitting an error. I'm tempted to go with the former as I don't think there's significant time savings in failing during elaboration vs. post elaboration. @jackkoenig may be able to comment on the utility of this type of elaboration failure for large designs.

Note: the supposed to be qualifier above is due to me looking at how LoadMemoryAnnotation works. However, an empty filename isn't resulting in Nil as I think it's supposed to. See:

scala> "".split("""\.""").toList == Nil
res15: Boolean = false

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably just be:

require(fileName.nonEmpty, "LoadMemory from file annotations file empty file name")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the easy way out here and Changed to Builder.error. I don't think it hurts to do it sooner. I don't think it's a particularly likely case anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it an exception, mostly to get rid of the Builder reference.
This is a relatively unlikely (and clearly wrong) edge case. I'd rather keep the fixes simple as possible.


def transformClass: Class[LoadMemoryTransform] = classOf[LoadMemoryTransform]

def toFirrtl: LoadMemoryAnnotation = {
LoadMemoryAnnotation(target.toNamed.asInstanceOf[ComponentName], fileName, hexOrBinary)
}
}


object loadMemoryFromFile {
def apply(
memory: MemBase[_],
fileName: String,
hexOrBinary: MemoryLoadFileType.FileType = MemoryLoadFileType.Hex
): Unit = {
annotate(ChiselLoadMemoryAnnotation(memory, fileName))
}
}

/**
* This transform only is activated if verilog is being generated
* (determined by presence of the proper emit annotation)
* when activated it creates additional verilog files that contain
* modules bound to the modules that contain an initializable memory
*
* Currently the only non-verilog based simulation that can support loading
* memory from a file is treadle but it does not need this transform
* to do that.
*/
//noinspection ScalaStyle
class LoadMemoryTransform extends Transform {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expected that this would be in the FIRRTL tree as opposed to Chisel. Not that big of a deal, but that seems more logical to me for things which are FIRRTL-proper as opposed to libraries. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kind of in the opposite camp. I am more interested in examples of how to write things that are more like external libraries that do not require changes to the underlying firrtl code. This was a bit of a fail in that it required changes to firrtl to support generating modules bound to other module, but this should lead to exposing bound modules as an API for library developers. So hopefully this is sufficient justification for where this is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you're saying. Nothing like that exists in the Chisel tree (as far as I'm aware). External libraries show it, but that's all.

I guess then there's the reverse question of why LoadMemoryAnnotation and MemoryLoadFileType aren't in Chisel?

I think this occurred to me as reading the code I was bouncing back and forth between the repos: a chisel3.util.experimental.ChiselLoadMemoryAnnotation generates a firrtl.LoadMemoryAnnotation which is processed by a chisel3.util.experimental.LoadMemoryTransform which interacts with the the firrtl.VerilogEmitter.

Copy link
Contributor Author

@chick chick Aug 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to write as much of this stuff as a library without changing firrtl.
It's true that for this project firrtl did get changed but that was because we needed some low level support for access to verilog module headers to match signatures for module binding, but I think that's justifiable because it is the beginning of an API for other work with verilog binding.
Bottom line, I'd prefer things to stay where they are now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Yeah, not the low level stuff. I'm only narrowly talking about the contents of FIRRTL's LoadMemoryAnnotation.scala. Moving that (and only that) over here would make this entirely encapsulated in Chisel. Maybe a subsequent PR? (Or, I can slam a PR through right now if that's something you want in this PR). No reason to hold this up for that, though.

def inputForm: CircuitForm = LowForm
def outputForm: CircuitForm = LowForm

private var memoryCounter: Int = -1

private val bindModules: mutable.ArrayBuffer[BlackBoxInlineAnno] = new mutable.ArrayBuffer()

private val verilogEmitter: VerilogEmitter = new VerilogEmitter

/**
* run the pass
* @param circuit the circuit
* @param annotations all the annotations
* @return
*/
def run(circuit: Circuit, annotations: AnnotationSeq): Circuit = {

val memoryAnnotations = {
annotations.collect{ case m: LoadMemoryAnnotation => m }.map { ma => ma.target.serialize -> ma }.toMap
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this is equivalent to the serialize method of ComponentName.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, I'm going to leave this for now pending a more general discussion of what a ComponentName actually is.


val modulesByName = {
circuit.modules.collect { case m: firrtl.ir.Module => m }.map { module => module.name -> module }.toMap
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This doesn't have the possibility of a name collision, but you could consider using a groupBy (and modifying the code below`).

circuit.modules.collect { case m: firrtl.ir.Module => m }.groupBy(_.name)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only issue with groupBy is that it returns a Seq, this could still be simpler as:

circuit.modules.collect { case m: firrtl.ir.Module => m.name -> m }.toMap

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, @jackkoenig. 🏌️‍♂️ This looks fine.


/**
* walk the module and for memories that have LoadMemory annotations
* generate the bindable modules for verilog emission
*
* @param myModule module being searched for memories
*/
def processModule(myModule: DefModule): Unit = {

def makePath(componentName: String): String = {
circuit.main + "." + myModule.name + "." + componentName
}

def processMemory(name: String): Unit = {
val fullMemoryName = makePath(s"$name")

memoryAnnotations.get(fullMemoryName) match {
case Some(lma @ LoadMemoryAnnotation(ComponentName(componentName, moduleName), _, hexOrBinary, _)) =>
val writer = new java.io.StringWriter

modulesByName.get(moduleName.name).foreach { module =>
val moduleMap = circuit.modules.map(m => m.name -> m).toMap
val renderer = verilogEmitter.getRenderer(module, moduleMap)(writer)
val loadFileName = lma.getFileName

memoryCounter += 1
val bindsToName = s"BindsTo_${memoryCounter}_${moduleName.name}"
renderer.emitVerilogBind(bindsToName,
s"""
|initial begin
| $$readmem$hexOrBinary("$loadFileName", ${myModule.name}.$componentName);
|end
""".stripMargin)
val inLineText = writer.toString + "\n" +
s"""bind ${myModule.name} $bindsToName ${bindsToName}_Inst(.*);"""

val blackBoxInline = BlackBoxInlineAnno(
moduleName,
moduleName.serialize + "." + componentName + ".v",
inLineText
)

bindModules += blackBoxInline
}

case _ =>
}
}

def processStatements(statement: Statement): Unit = {
statement match {
case block: Block =>
block.stmts.foreach { subStatement =>
processStatements(subStatement)
}
case m: DefMemory => processMemory(m.name)
case _ =>
}
}

myModule match {
case module: firrtl.ir.Module =>
processStatements(module.body)
case _ =>
}
}

circuit.modules.foreach(processModule)
circuit
}

def execute(state: CircuitState): CircuitState = {
val isVerilog = state.annotations.exists {
case EmitCircuitAnnotation(emitter) =>
emitter == classOf[VerilogEmitter]
case _ =>
false
}
if(isVerilog) {
run(state.circuit, state.annotations)
state.copy(annotations = state.annotations ++ bindModules)
}
else {
state
}
}
}
58 changes: 58 additions & 0 deletions src/test/scala/chiselTests/ComplexMemoryLoadingSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// See LICENSE for license details.

package chiselTests

import java.io.File

import chisel3._
import chisel3.util.{loadMemoryFromFile, log2Ceil}
import firrtl.FirrtlExecutionSuccess
import firrtl.annotations.MemoryLoadFileType
import org.scalatest.{FreeSpec, Matchers}

class MemoryShape extends Bundle {
val a = UInt(8.W)
val b = SInt(8.W)
val c = Bool()
}

class HasComplexMemory(memoryDepth: Int) extends Module {
val io = IO(new Bundle {
val address = Input(UInt(log2Ceil(memoryDepth).W))
val value = Output(new MemoryShape)
})

val memory = Mem(memoryDepth, new MemoryShape)

loadMemoryFromFile(memory, "./mem", MemoryLoadFileType.Hex)

io.value := memory(io.address)
}


class ComplexMemoryLoadingSpec extends FreeSpec with Matchers {
val testDirName = "test_run_dir/complex_memory_load"

"Users can specify a source file to load memory from" in {
val result = Driver.execute(
args = Array("-X", "verilog", "--target-dir", testDirName),
dut = () => new HasComplexMemory(memoryDepth = 8)
)

result match {
case ChiselExecutionSuccess(_, emitted, Some(FirrtlExecutionSuccess(emitType, firrtlEmitted))) =>
val dir = new File(testDirName)
val memoryElements = Seq("a", "b", "c")

memoryElements.foreach { element =>
val file = new File(dir, s"HasComplexMemory.HasComplexMemory.memory_$element.v")
file.exists() should be (true)
val fileText = io.Source.fromFile(file).getLines().mkString("\n")
fileText should include (s"""$$readmemh("./mem_$element", HasComplexMemory.memory_$element);""")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

}

case _=>
fail(s"Failed compile")
}
}
}
82 changes: 82 additions & 0 deletions src/test/scala/chiselTests/LoadMemoryFromFileSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// See LICENSE for license details.

package chiselTests

import java.io.{ByteArrayOutputStream, File, PrintStream}

import chisel3._
import chisel3.util.loadMemoryFromFile
import firrtl.FirrtlExecutionSuccess
import org.scalatest.{FreeSpec, Matchers}

//noinspection TypeAnnotation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to prevent IntelliJ from getting to commit to the repo... Is it okay to remove this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm in the other camp, where I really like to have my files "style clean", with annotations like this for when I intentionally violate the rules. I have changed this to
//scalastyle:off method.length
Which is more specific and sufficient to make the file get a ✅
I hope that's ok.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scalastyle comments lgtm.

class UsesMem(memoryDepth: Int, memoryType: Data) extends Module {
val io = IO(new Bundle {
val address = Input(UInt(memoryType.getWidth.W))
val value = Output(memoryType)
val value2 = Output(memoryType)
})

val memory = Mem(memoryDepth, memoryType)
loadMemoryFromFile(memory, "./mem1")

io.value := memory(io.address)

val low = Module(new UsesMemLow(memoryDepth, memoryType))

low.io.address := io.address
io.value2 := low.io.value
}

class UsesMemLow(memoryDepth: Int, memoryType: Data) extends Module {
val io = IO(new Bundle {
val address = Input(UInt(memoryType.getWidth.W))
val value = Output(memoryType)
})

val memory = Mem(memoryDepth, memoryType)

loadMemoryFromFile(memory, "./mem2")

io.value := memory(io.address)
}

//noinspection TypeAnnotation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

class FileHasSuffix(memoryDepth: Int, memoryType: Data) extends Module {
val io = IO(new Bundle {
val address = Input(UInt(memoryType.getWidth.W))
val value = Output(memoryType)
val value2 = Output(memoryType)
})

val memory = Mem(memoryDepth, memoryType)

loadMemoryFromFile(memory, "./mem1.txt")

io.value := memory(io.address)

val low = Module(new UsesMemLow(memoryDepth, memoryType))

low.io.address := io.address
io.value2 := low.io.value
}

class LoadMemoryFromFileSpec extends FreeSpec with Matchers {
val testDirName = "test_run_dir/load_memory_spec"
"Users can specify a source file to load memory from" in {
val result = Driver.execute(
args = Array("-X", "verilog", "--target-dir", testDirName),
dut = () => new UsesMem(memoryDepth = 8, memoryType = UInt(16.W))
)

result match {
case ChiselExecutionSuccess(_, _, Some(FirrtlExecutionSuccess(_, _))) =>
val dir = new File(testDirName)
new File(dir, "UsesMem.UsesMem.memory.v").exists() should be (true)
new File(dir, "UsesMem.UsesMemLow.memory.v").exists() should be (true)
new File(dir, "firrtl_black_box_resource_files.f").exists() should be (true)
case _=>
throw new Exception("Failed compile")
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see this PR add some end-to-end testing of memories. I.e., add some resources of readmemh/readmemb format and then make sure that the data matches up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have end to end testing in an as-yet-unpushed PR on chisel-testers because of all the heavy lifting that is done there to invoke verilator with all the right arguments and file names. Because of the testers2 project Richard is working on I'm not really sure where that is all going to live come 3.2, but I think it is as yet un-PR'd chisel3

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great. I should have expected that you had something lined up...