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

loadMemoryFromFile() should place readmemh() inline for better compatibility #1293

Open
antonblanchard opened this issue Jan 3, 2020 · 18 comments · May be fixed by #2260
Open

loadMemoryFromFile() should place readmemh() inline for better compatibility #1293

antonblanchard opened this issue Jan 3, 2020 · 18 comments · May be fixed by #2260
Assignees

Comments

@antonblanchard
Copy link

loadMemoryFromFile() creates a separate file and uses a SV bind statement. A simple example:

import chisel3._
import chisel3.util.experimental.loadMemoryFromFile

class Foo(val bits: Int, val size: Int, filename: String) extends Module {
  val io = IO(new Bundle {
    val nia = Input(UInt(bits.W))
    val insn = Output(UInt(32.W))
  })

  val memory = Mem(size, UInt(32.W))
  io.insn := memory(io.nia >> 2);
  loadMemoryFromFile(memory, filename)
}

object FooObj extends App {
  chisel3.Driver.execute(Array[String](), () => new Foo(32, 1024, "insns.hex"))
}
# cat Foo.Foo.memory.v
module BindsTo_0_Foo(
  input         clock,
  input         reset,
  input  [31:0] io_nia,
  output [31:0] io_insn
);

initial begin
  $readmemh("insns.hex", Foo.memory);
end
                      endmodule

bind Foo BindsTo_0_Foo BindsTo_0_Foo_Inst(.*);

Yosys doesn't like this, and likely there are other tools that don't either. Is there any reason we don't just place it inline?

@chick chick self-assigned this Mar 16, 2020
@carlosedp
Copy link
Contributor

Same here while trying to make a RAM module:

class SinglePortRAM(sizeKB: Int = 1, width: Int = 32, memoryFile: String = "") extends Module {
  val addrWidth = chiselTypeOf((sizeKB * 1024).U)
  val io = IO(new Bundle {
    val addr1 = Input(addrWidth)
    val dataIn1 = Input(UInt(width.W))
    val en1 = Input(Bool())
    val we1 = Input(Bool())
    val dataOut1 = Output(UInt(width.W))
  })
  println(s"Single-port Memory Parameters:")
  println(s"  Size: $sizeKB KB")
  println(s"  Width: $width bit")
  println(s"  Addr Width: " + io.addr1.getWidth + " bit")

  val mem = SyncReadMem(sizeKB * 1024, UInt(width.W))
  if (memoryFile.trim().nonEmpty) {
      loadMemoryFromFile(mem, memoryFile)
  }

  when (io.we1) {
    mem.write(io.addr1, io.dataIn1)
  }
  io.dataOut1 := mem.read(io.addr1, io.en1)
}

If memfile is provided, it gets written to a separate file that is not synthesizable.

@ekiwi
Copy link
Contributor

ekiwi commented Feb 18, 2021

We have a MemoryInitAnnotation now which will inline the contents into the generated Verilog.
This should be compatible with the memory inference pass in yosys.

For examples on how to use it, see:

Would that work for you?

@carlosedp
Copy link
Contributor

Hey Kevin, thanks for your response.

I understand I cannot pass the .hex file I have to be loaded at synthesis by using the annotation, right since it only supports BigInts?

What's the method do do that?

@ekiwi
Copy link
Contributor

ekiwi commented Feb 19, 2021

I understand I cannot pass the .hex file I have to be loaded at synthesis by using the annotation, right since it only supports BigInts?

Yeah, so the method that I suggested assumes that you have the data in Scala. You could of course try and write a function that opens a .hex file from Scala and returns the content as a list of BigInt.

What's the method do do that?

There is no separate file involved. You just provide the memory init content in your Chisel generator and the Verilog we produce will contain an initial block that normally works well with yosys. I prefer this way to having a separate file, but I can understand, that it might be a little annoying if you want to change your memory image without re-elaborating your Chisel.

@carlosedp
Copy link
Contributor

As someone new to HDLs, I found these methods way more complex than just $readmemh(mem, "file") in Verilog...
I believe that it should have a somewhat similar syntax...

@ekiwi
Copy link
Contributor

ekiwi commented Feb 19, 2021

@carlosedp, could you try to see if the following would address your problem?

import chisel3.experimental.{ChiselAnnotation, annotate}
import firrtl.annotations.{MemoryArrayInitAnnotation, MemoryLoadFileType}

import scala.io.Source

object loadMemoryFromFileInline {
  def apply(mem: MemBase[_], filename: String, hexOrBinary: MemoryLoadFileType.FileType = MemoryLoadFileType.Hex): Unit = {
    val radix = hexOrBinary match {
      case MemoryLoadFileType.Hex => 16
      case MemoryLoadFileType.Binary => 2
    }
    val src = Source.fromFile(filename)
    val lines = src.getLines().toList
    src.close()
    val data = lines.map(_.trim).filter(_.nonEmpty).map(BigInt(_, radix))
    val missingWords = mem.length - data.length
    require(missingWords >= 0, s"$filename contained ${data.length} items, but memory only fits ${mem.length}")
    val padding = Seq.tabulate(missingWords.toInt)(_ => BigInt(0))
    annotate(new ChiselAnnotation {
      override def toFirrtl = MemoryArrayInitAnnotation(mem.toTarget, data ++ padding)
    })
  }
}

You should be able to just replace loadMemoryFromFile with loadMemoryFromFileInline in the example that you posted above.

I know that this isn't 100% what you want, but it would be fairly straight forward to add this helper function from your code base and might be a good stop-gap measure until we come up with a more permanent solution.

@carlosedp
Copy link
Contributor

Thanks @ekiwi ... just a heads-up is that since the memory gets initialized by directly setting the memory array in Verilog so a 1k Scala module with an 11k .hex became a 282k verilog...

-rw-r--r-- 1 cdepaula staff  11K Feb 19 18:41 hello_world.hex
-rw-r--r-- 1 cdepaula staff 282K Feb 19 18:55 SinglePortRAM.v
-rw-r--r-- 1 cdepaula staff 1.1K Feb 19 18:55 SinglePortRAM.fir
-rw-r--r-- 1 cdepaula staff 147K Feb 19 18:55 SinglePortRAM.anno.json
❯ ll src/main/scala/utils
total 12K
-rw-r--r-- 1 cdepaula staff 1.1K Feb 19 18:53 LoadMemory.scala
-rw-r--r-- 1 cdepaula staff  999 Feb 19 18:55 SingleportRAM.scala

@ekiwi
Copy link
Contributor

ekiwi commented Feb 19, 2021

just a heads-up is that since the memory gets initialized by directly setting the memory array in Verilog so a 1k Scala module with an 11k .hex became a 282k verilog...

Thanks for trying it out. I definitely didn't consider that this might blow up significantly in size (20x .. wow).

@carlosedp
Copy link
Contributor

It's instantiated into a 16KB ram that would be pretty much empty.. but still quite a grow.
I still need to test it thru all synthesis... i'll let u know the results.

@carlosedp
Copy link
Contributor

carlosedp commented Mar 9, 2021

@antonblanchard check out chipsalliance/firrtl#2107. I've added a new annotation allowing inline readmem in Verilog emiter.

Your sample code would become:

import chisel3._
import chisel3.experimental.{ChiselAnnotation, annotate}
import firrtl.annotations.{MemoryFileInlineAnnotation}


class Foo(val bits: Int, val size: Int, filename: String) extends Module {
  val io = IO(new Bundle {
    val nia = Input(UInt(bits.W))
    val insn = Output(UInt(32.W))
  })

  val memory = Mem(size, UInt(32.W))
  io.insn := memory(io.nia >> 2);
  annotate(new ChiselAnnotation {
    override def toFirrtl = MemoryFileInlineAnnotation(memory.toTarget, filename)
  })
}

object FooObj extends App {
  chisel3.Driver.execute(Array[String](), () => new Foo(32, 1024, "insns.hex"))
}

that would generate the appropriate:

initial begin
  $readmemh("insns.hex", memory);
end

It would allow also configuration for binary files instead of hex, like:

import firrtl.annotations.{MemoryFileInlineAnnotation, MemoryLoadFileType}
...
  annotate(new ChiselAnnotation {
    override def toFirrtl = MemoryFileInlineAnnotation(memory.toTarget, "filename.bin", MemoryLoadFileType.Binary)
  })
...

and output

initial begin
  $readmemb("filename.bin", memory);
end

@antonblanchard
Copy link
Author

@carlosedp That looks great! Nice work

@carlosedp
Copy link
Contributor

carlosedp commented Apr 5, 2021

Now there is also a method in chisel for this:

import chisel3.util.experimental.loadMemoryFromFileInline
...
  annotate(new ChiselAnnotation {
    override def toFirrtl =
      MemorySynthInit
  })

  val mem = SyncReadMem(1024, UInt(width.W))
  loadMemoryFromFileInline(mem, memoryFile)

I believe this could be closed now. Any objections?

@ekiwi
Copy link
Contributor

ekiwi commented Apr 5, 2021

I believe this could be closed now. Any objections?

There is still the question of whether we should move this API out of experimental and maybe make the inline version the default.

@carlosedp
Copy link
Contributor

Ah yes, that would be great!

@carlosedp
Copy link
Contributor

Any news on this? Should we move on and close or promote to non-experimental API?

@ekiwi
Copy link
Contributor

ekiwi commented Nov 23, 2021

Any news on this? Should we move on and close or promote to non-experimental API?

Would you mind making a PR that makes loadMemoryFromFile available in the standard chisel3._ package and changes the behavior to that of loadMemoryFromFileInline?

Then we can discuss the issue on the PR.

@carlosedp
Copy link
Contributor

carlosedp commented Nov 24, 2021

Sure, do I keep the previous behavior of loadMemoryFromFile (using external SV file) to another function?

Also should it be in chisel3 or chisel3.util?

carlosedp added a commit to carlosedp/chisel3 that referenced this issue Nov 24, 2021
@carlosedp carlosedp linked a pull request Nov 24, 2021 that will close this issue
14 tasks
@carlosedp
Copy link
Contributor

Opened #2260 to address this.

carlosedp added a commit to carlosedp/chisel3 that referenced this issue Dec 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants