Skip to content

Commit

Permalink
Move information to experimental and add ref
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosedp committed Apr 5, 2021
1 parent 2c43285 commit 9984413
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 166 deletions.
179 changes: 155 additions & 24 deletions docs/src/appendix/experimental-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,56 +143,187 @@ Consider a Interval with a binary point of 3: aaa.bbb
| shiftLeftBinaryPoint(2) | a.aabbb | 5 | X | X | increase the precision |
| shiftRighBinaryPoint(2) | aaaa.b | 1 | X | X | reduce the precision |

## Loading Memories in simulation <a name="loading-memories"></a>
## Loading Memories in simulation or FPGA initialization <a name="loading-memories"></a>

Chisel now supports an experimental method for annotating memories to be loaded from a text file containing
hex or binary numbers. When using verilog simulation it uses the `$readmemh` or `$readmemb`
verilog extension. The treadle simulator can also load memories using the same annotation.
Chisel supports multiple experimental methods for annotating memories to be loaded from a text file containing hex or binary data. When using verilog simulation it uses the `$readmemh` or `$readmemb` verilog extension. The treadle simulator can also load memories using the same annotation.

### How to annotate
Assuming you have a memory in a Module
```scala mdoc:invisible
### Inline initialization with external file

Memories can be initialized by generating inline `readmemh` or `readmemb` statements in the output Verilog.

The function `loadMemoryFromFileInline` from `chisel3.util.experimental` allows the memory to be initialized by the synthesis software from the specified file. Chisel does not validate the file contents nor it's location. Both the memory initialization file and the Verilog source should be accessible for the toolchain.

```scala mdoc:silent
import chisel3._
val memoryDepth = 8
val memoryType = Bool()
import chisel3.util.experimental.loadMemoryFromFileInline

class InitMemInline(memoryFile: String = "") extends Module {
val width: Int = 32
val io = IO(new Bundle {
val enable = Input(Bool())
val write = Input(Bool())
val addr = Input(UInt(10.W))
val dataIn = Input(UInt(width.W))
val dataOut = Output(UInt(width.W))
})

val mem = SyncReadMem(1024, UInt(width.W))
// Initialize memory
if (memoryFile.trim().nonEmpty) {
loadMemoryFromFileInline(mem, memoryFile)
}
io.dataOut := DontCare
when(io.enable) {
val rdwrPort = mem(io.addr)
when (io.write) { rdwrPort := io.dataIn }
.otherwise { io.dataOut := rdwrPort }
}
}
```
```scala
val memory = Mem(memoryDepth, memoryType)

The default is to use `$readmemh` (which assumes all numbers in the file are in ascii hex),
but to use ascii binary there is an optional `hexOrBinary` argument which can be set to `MemoryLoadFileType.Hex` or `MemoryLoadFileType.Binary`. You will need to add an the additional import.

The inline initialization can generate the memory `readmem` statement inside a `ifndef SYNTHESIS` block, which suits ASIC workflow or outside the block suiting FPGA workflows.

Some synthesis tools (like Synplify and Yosys) define `SYNTHESIS` so the `readmem` statement is not read when inside this block.

To control this, one can use the `MemoryNoSynthInit` and `MemorySynthInit` annotations from `firrtl.annotations`. The former which is the default setting when no annotation is present generates `readmem` inside the block. Using the latter, the statement are generated outside the `ifndef` block so it can be used by FPGA synthesis tools.

Below an example for initialization suited for FPGA workflows:

```scala mdoc:silent
import chisel3._
import chisel3.util.experimental.loadMemoryFromFileInline
import chisel3.experimental.{annotate, ChiselAnnotation}
import firrtl.annotations.MemorySynthInit

class InitMemInlineFPGA(memoryFile: String = "") extends Module {
val width: Int = 32
val io = IO(new Bundle {
val enable = Input(Bool())
val write = Input(Bool())
val addr = Input(UInt(10.W))
val dataIn = Input(UInt(width.W))
val dataOut = Output(UInt(width.W))
})

// Notice the annotation below
annotate(new ChiselAnnotation {
override def toFirrtl =
MemorySynthInit
})

val mem = SyncReadMem(1024, UInt(width.W))
if (memoryFile.trim().nonEmpty) {
loadMemoryFromFileInline(mem, memoryFile)
}
io.dataOut := DontCare
when(io.enable) {
val rdwrPort = mem(io.addr)
when (io.write) { rdwrPort := io.dataIn }
.otherwise { io.dataOut := rdwrPort }
}
}
```

At the top of your file just add the import
#### SystemVerilog Bind Initialization

Chisel also can initialize memories by generating a SV bind module with `readmemh` or `readmemb` statements by using the function `loadMemoryFromFile` from `chisel3.util.experimental`.

```scala mdoc:silent
import chisel3._
import chisel3.util.experimental.loadMemoryFromFile

class InitMemBind(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)
}
```
Now just add the memory annotation using
```scala
loadMemoryFromFile(memory, "/workspace/workdir/mem1.txt")

Which generates the bind module:

```verilog
module BindsTo_0_Foo(
input clock,
input reset,
input [31:0] io_nia,
output [31:0] io_insn
);
initial begin
$readmemh("test.hex", Foo.memory);
end
endmodule
bind Foo BindsTo_0_Foo BindsTo_0_Foo_Inst(.*);
```
The default is to use `$readmemh` (which assumes all numbers in the file are in ascii hex),
bu to use ascii binary there is an optional third argument. You will need to add an additional import.
```scala mdoc:silent
import firrtl.annotations.MemoryLoadFileType

#### Inline initialization with embedded file contents

Memories can also be initialized embedding the file contents in the generated Verilog.

Data can be read from a single `BigInt` or an array of `BigInt` by using the following annotations:

```scala mdoc:invisible
import scala.io.Source
import chisel3._
import chisel3.experimental.{annotate, ChiselAnnotation}
import firrtl.annotations.MemoryScalarInitAnnotation
```
```scala
loadMemoryFromFile(memory, "/workspace/workdir/mem1.txt", MemoryLoadFileType.Binary)
```scala mdoc:silent
// Read data into a BigInt
val src = Source.fromFile("filename")
val lines = src.getLines().toList
src.close()
val data = lines.map(_.trim).filter(_.nonEmpty).map(BigInt(_, radix))
val memory = Mem(32, UInt(32.W))

// Uses the above `data` BigInt
annotate(new ChiselAnnotation {
override def toFirrtl = MemoryScalarInitAnnotation(memory.toTarget, data)
})

// Or read from an array
annotate(new ChiselAnnotation {
override def toFirrtl = MemoryArrayInitAnnotation(memory.toTarget, Seq[0])
})
```

### Notes on files

There is no simple answer to where to put this file. It's probably best to create a resource directory somewhere and reference that through a full path or place the file beside the generated Verilog. Another option is adding the path to the memory file in the synthesis tool path. Because these files may be large, Chisel does not copy them.
> Don't forget there is no decimal option, so a 10 in an input file will be 16 decimal
See: [ComplexMemoryLoadingSpec.scala](https://freechipsproject/chisel-testers/src/test/scala/examples/ComplexMemoryLoadingSpec.scala) and
[LoadMemoryFromFileSpec.scala](https://github.com/freechipsproject/chisel-testers/src/test/scala/examples/LoadMemoryFromFileSpec.scala)
for working examples.

### Notes on files
There is no simple answer to where to put this file. It's probably best to create a resource directory somewhere and reference that through a full path. Because these files may be large, we did not want to copy them.
> Don't forget there is no decimal option, so a 10 in an input file will be 16 decimal

### Aggregate memories

Aggregate memories are supported but in bit of a clunky way. Since they will be split up into a memory per field, the following convention was adopted. When specifying the file for such a memory the file name should be regarded as a template. If the memory is a Bundle e.g.

```scala mdoc:compile-only
class MemDataType extends Bundle {
val a = UInt(16.W)
val b = UInt(32.W)
val c = Bool()
}
```

The memory will be split into `memory_a`, `memory_b`, and `memory_c`. Similarly if a load file is specified as `"memory-load.txt"` the simulation will expect that there will be three files, `"memory-load_a.txt"`, `"memory-load_b.txt"`, `"memory-load_c.txt"`

> Note: The use of `_` and that the memory field name is added before any file suffix. The suffix is optional but if present is considered to be the text after the last `.` in the file name.





150 changes: 8 additions & 142 deletions docs/src/explanations/memories.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ where `inits` is a sequence of initial `Data` literals that initialize the ROM.

We can create an *n* value sine lookup table using a ROM initialized as follows:

``` scala mdoc:silent
```scala mdoc:silent
def sinTable(amp: Double, n: Int) = {
val times =
(0 until n).map(i => (i*2*Pi)/(n.toDouble-1) - Pi)
Expand Down Expand Up @@ -58,6 +58,7 @@ If the same memory address is both written and sequentially read on the same clo
Values on the read data port are not guaranteed to be held until the next read cycle. If that is the desired behavior, external logic to hold the last read value must be added.

#### Read port/write port

Ports into `SyncReadMem`s are created by applying a `UInt` index. A 1024-entry SRAM with one write port and one read port might be expressed as follows:

```scala mdoc:silent
Expand All @@ -81,9 +82,11 @@ class ReadWriteSmem extends Module {

Below is an example waveform of the one write port/one read port `SyncReadMem` with [masks](#masks). Note that the signal names will differ from the exact wire names generated for the `SyncReadMem`. With masking, it is also possible that multiple RTL arrays will be generated with the behavior below.

![read/write ports example waveform](https://svg.wavedrom.com/github/freechipsproject/www.chisel-lang.org/master/docs/src/main/tut/chisel3/memories_waveforms/smem_read_write.json)
![read/write ports example waveform](https://svg.wavedrom.com/github/freechipsproject/www.chisel-lang.org/master/docs/src/main/resources/json/smem_read_write.json)


#### Single-ported

Single-ported SRAMs can be inferred when the read and write conditions are mutually exclusive in the same `when` chain:

```scala mdoc:silent
Expand Down Expand Up @@ -112,7 +115,7 @@ class RWSmem extends Module {

Here is an example single read/write port waveform, with [masks](#masks) (again, generated signal names and number of arrays may differ):

![read/write ports example waveform](https://svg.wavedrom.com/github/freechipsproject/www.chisel-lang.org/master/docs/src/main/tut/chisel3/memories_waveforms/smem_rw.json)
![read/write ports example waveform](https://svg.wavedrom.com/github/freechipsproject/www.chisel-lang.org/master/docs/src/main/resources/json/smem_rw.json)

### `Mem`: combinational/asynchronous-read, sequential/synchronous-write

Expand Down Expand Up @@ -178,144 +181,7 @@ class MaskedRWSmem extends Module {

### Memory Initialization

Chisel memories can be initialized from an external binary or hex file emitting proper Verilog. There are multiple modes of initialization.

#### Inline initialization with external file

Memories can be initialized by generating inline `readmemh` or `readmemb` statements in the output Verilog.

The function `loadMemoryFromFileInline` from `chisel3.util.experimental` allows the memory to be initialized by the synthesis software from the specified file. Chisel does not validate the file contents nor it's location. Both the memory initialization file and the Verilog source should be accessible for the toolchain.

```scala mdoc:silent
import chisel3._
import chisel3.util.experimental.loadMemoryFromFileInline
class InitMemInline(memoryFile: String = "") extends Module {
val width: Int = 32
val io = IO(new Bundle {
val enable = Input(Bool())
val write = Input(Bool())
val addr = Input(UInt(10.W))
val dataIn = Input(UInt(width.W))
val dataOut = Output(UInt(width.W))
})

val mem = SyncReadMem(1024, UInt(width.W))
// Initialize memory
if (memoryFile.trim().nonEmpty) {
loadMemoryFromFileInline(mem, memoryFile)
}
io.dataOut := DontCare
when(io.enable) {
val rdwrPort = mem(io.addr)
when (io.write) { rdwrPort := io.dataIn }
.otherwise { io.dataOut := rdwrPort }
}
}
```

The inline initialization can generate the memory `readmem` statement inside a `ifndef SYNTHESIS` block, which suits ASIC workflow or outside the block suiting FPGA workflows.

Some synthesis tools (like Synplify and Yosys) define `SYNTHESIS` so the `readmem` statement is not read when inside this block.

To control this, one can use the `MemoryNoSynthInit` and `MemorySynthInit` annotations from `firrtl.annotations`. The former which is the default setting when no annotation is present generates `readmem` inside the block. Using the latter, the statement are generated outside the `ifndef` block so it can be used by FPGA synthesis tools.

Below an example for initialization suited for FPGA workflows:

```scala mdoc:silent
import chisel3._
import chisel3.util.experimental.loadMemoryFromFileInline
import chisel3.experimental.{annotate, ChiselAnnotation}
import firrtl.annotations.MemorySynthInit

class InitMemInlineFPGA(memoryFile: String = "") extends Module {
val width: Int = 32
val io = IO(new Bundle {
val enable = Input(Bool())
val write = Input(Bool())
val addr = Input(UInt(10.W))
val dataIn = Input(UInt(width.W))
val dataOut = Output(UInt(width.W))
})

// Notice the annotation below
annotate(new ChiselAnnotation {
override def toFirrtl =
MemorySynthInit
})

val mem = SyncReadMem(1024, UInt(width.W))
if (memoryFile.trim().nonEmpty) {
loadMemoryFromFileInline(mem, memoryFile)
}
io.dataOut := DontCare
when(io.enable) {
val rdwrPort = mem(io.addr)
when (io.write) { rdwrPort := io.dataIn }
.otherwise { io.dataOut := rdwrPort }
}
}
```

#### SystemVerilog Bind Initialization

Chisel also can initialize memories by generating a SV bind module with `readmemh` or `readmemb` statements by using the function `loadMemoryFromFile` from `chisel3.util.experimental`.
Chisel memories can be initialized from an external `binary` or `hex` file emitting proper Verilog for synthesis or simulation. There are multiple modes of initialization.

```scala mdoc:silent
import chisel3._
import chisel3.util.experimental.loadMemoryFromFile

class InitMemBind(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)
}
```

Which generates the bind module:

```verilog
module BindsTo_0_Foo(
input clock,
input reset,
input [31:0] io_nia,
output [31:0] io_insn
);
initial begin
$readmemh("test.hex", Foo.memory);
end
endmodule
bind Foo BindsTo_0_Foo BindsTo_0_Foo_Inst(.*);
```

#### Inline initialization with embedded file contents

Memories can also be initialized embedding the file contents in the generated Verilog.

Data can be read from a single `BigInt` or an array of `BigInt` by using the following annotations:

```scala
// Read data into a BigInt
val src = Source.fromFile("filename")
val lines = src.getLines().toList
src.close()
val data = lines.map(_.trim).filter(_.nonEmpty).map(BigInt(_, radix))
val memory = Mem(32, UInt(32.W))

// Uses the above `data` BigInt
annotate(new ChiselAnnotation {
override def toFirrtl = MemoryScalarInitAnnotation(memory.toTarget, data)
})

// Or read from an array
annotate(new ChiselAnnotation {
override def toFirrtl = MemoryArrayInitAnnotation(memory.toTarget, Seq[0])
})
```
For more information, check the experimental docs on [Loading Memories](../appendix/experimental-features#loading-memories) feature.

0 comments on commit 9984413

Please sign in to comment.