Registers
Creating registers with SpinalHDL is very different from creating registers with VHDL or Verilog. In SpinalHDL, there is no process / always blocking. Registers are clearly defined in the declaration. This is very different from the traditional event driven HDL:
-
You can allocate registers and connections in the same range, which means that the code does not need to be split between processes / always blocks.
-
It makes some things more flexible (see functions), and the clock and reset are handled separately.
instantiation
There are four ways to instantiate a register:
Syntax syntax | Description Description |
---|---|
Reg(type : Data) | Register of the given type |
RegInit(resetValue : Data) | Registers loaded with the given resetValue at reset |
RegNext(nextValue : Data) | Register samples the given nextValue every cycle |
RegNextWhen(nextValue : Data, cond : Bool) | When a condition occurs, the register samples the given nextValue |
The following is an example of declaring some registers:
// UInt register of 4 bits val reg1 = Reg(UInt(4 bit)) // Register that samples reg1 each cycle val reg2 = RegNext(reg1 + 1) // UInt register of 4 bits initialized with 0 when the reset occurs val reg3 = RegInit(U"0000") reg3 := reg2 when(reg2 === 5) { reg3 := 0xF } // Register that samples reg3 when cond is True val reg4 = RegNextWhen(reg3, cond)
The above code will infer the following logic:
[external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-mvmjt501-1637559987172)( https://spinalhdl.github.io/SpinalDoc-RTD/master/_images/register.svg )]
The reg3 example above shows how to allocate the value of the RegInit register. You can also use the same syntax to assign to other register types (Reg, RegNext, RegNextWhen). Like combined assignment, the rule is "the last assignment wins", but if the assignment is not completed, the register retains its value.
RegNext is an abstraction based on Reg syntax. The following two code sequences are strictly equivalent:
// Standard way val something = Bool() val value = Reg(Bool()) value := something // Short way val something = Bool() val value = RegNext(something)
Reset value reset value
In addition to directly creating a register with a reset value using the RegInit (value: Data) syntax, you can also set the reset value by calling the init (value: Data) function on the register.
// UInt register of 4 bits initialized with 0 when the reset occurs val reg1 = Reg(UInt(4 bit)) init(0)
If you have a register containing a Bundle, you can use the init function for each element of the Bundle.
case class ValidRGB() extends Bundle{ val valid = Bool() val r, g, b = UInt(8 bits) } val reg = Reg(ValidRGB()) reg.valid init(False) // Only the valid if that register bundle will have a reset value.
Initialization value for simulation
For registers that do not need reset value in RTL but need initialization value for simulation (to avoid x propagation), you can request a random initialization value by calling the randBoot() function.
// UInt register of 4 bits initialized with a random value val reg1 = Reg(UInt(4 bit)) randBoot()
RAM/ROM
grammar
To create memory with SpinalHDL, you should use the Mem class. It allows you to define a memory and add read and write ports to it.
The following table shows how to instantiate a memory:
Syntax | Description |
---|---|
Mem(type : Data, size : Int) | Create a RAM |
Mem(type : Data, initialContent : Array[Data]) | Create a ROM. If your target is an FPGA, because memory can be inferred as block memory, you can still create a write port on it. |
If you want to define a ROM, the elements of the initialContent array should only be literal values (no operators, no resizing functions). Here is an example. To give the initial value of RAM, you can also use the init function.
The following table shows how to add access ports to memory:
Syntax | Description | Return |
---|---|---|
mem(address) := data | Synchronous write | |
mem(x) | Asynchronous read | T |
mem.write(address data [enable] [mask]) | Synchronous write with optional mask. If enable is not specified, it is automatically inferred from the scope of the condition calling this function | |
mem.readAsync(address [readUnderWrite] ) | Use the optional read and write strategy for asynchronous reading | T |
mem.readSync(address [enable] [readUnderWrite] [clockCrossing]) | Synchronous reading with optional enable, read-under-write policy and clockCrossing mode | T |
mem.readWriteSync(address data enable write [mask] [readUnderWrite] [clockCrossing] ) | Inferred read / write port, Write data when enable & write. Returns the read data. Reading occurs when enable is true. | T |
If, for some reason, you need a specific memory port that is not implemented in spirit, you can always Abstract your memory by specifying a BlackBox for it. The memory port in SpinalHDL is not derived, but explicitly defined. Coding templates should not be used as in VHDL/Verilog to help compositing tools infer memory.
The following is a simple example of dual port ram (32-bit * 256):
val mem = Mem(Bits(32 bits), wordCount = 256) mem.write( enable = io.writeValid, address = io.writeAddress, data = io.writeData ) io.readData := mem.readSync( enable = io.readValid, address = io.readAddress )
Read write strategy
This policy specifies how read operations are affected when writing to the same address in the same loop.
Kinds | Description |
---|---|
dontCare | Don't care about the read value when the case occurs |
readFirst | Reading will get the old value (before writing) |
writeFirst | Reading will get a new value (provided by writing) |
The generated VHDL/Verilog is always in readFirst mode, which is compatible with dontCare but not writeFirst. To generate a design that includes this feature, you need to enable automatic memory black boxes.
Mixed-width ram
You can use the following function to specify the port to access memory, and its width is a power of one-half of the memory width:
Syntax | Description |
---|---|
mem.writeMixedWidth(address data [readUnderWrite]) | Similar to mem.write |
mem.readAsyncMixedWidth(address data [readUnderWrite] ) | Similar to mem.readAsync, but it does not return a read value, but drives the signal / object given as a data parameter |
mem.readSyncMixedWidth(address data [enable] [readUnderWrite] [clockCrossing]) | It is similar to mem.readSync, but it does not return the read value, but drives the signal / object given as a data parameter |
mem.readWriteSyncMixedWidth(address data enable write [mask] [readUnderWrite] [clockCrossing]) | Equivalent to mem.readWriteSync |
As for the read-write strategy, to use this feature, you need to enable automatic memory black box, because there is no general VHDL/Verilog language template to infer mixed width memory.
Automatic blackboxing
Since it is impossible to infer all ram types with conventional VHDL/Verilog, SpinalHDL integrates an optional automatic black box system. This system looks at all the memory in your RTL network list and replaces them with a black box. The generated code then relies on third-party IP to provide memory features, such as read during write policies and mixed width ports.
The following is an example of how to enable the memory black box by default:
def main(args: Array[String]) { SpinalConfig() .addStandardMemBlackboxing(blackboxAll) .generateVhdl(new TopLevel) }
Blackboxing policy
You can use a variety of strategies to select the memory of the black box you want and what to do when the black box is not feasible:
Kinds | Description |
---|---|
blackboxAll | Black box all memory. An error is thrown on non black box memory. |
blackboxAllWhatsYouCan | Black box all the memory of the black box. |
`blackboxRequestedAndUninferable | Blackbox memory specified by the user and memory known to be unable to infer (mixed width,...). An error is thrown on non black box memory. |
blackboxOnlyIfRequested | The Blackbox memory specified by the user throws an error on the non Blackbox memory. |
To explicitly set memory as a black box, you can use its generateasblack box function.
val mem = Mem(Rgb(rgbConfig), 1 << 16) mem.generateAsBlackBox()
You can also define your own blackboxing policy by extending the memblackboxing policy class.
Standard memory blackboxes
The following is the VHDL definition of the standard black box used in SpinalHDL:
-- Simple asynchronous dual port (1 write port, 1 read port) component Ram_1w_1ra is generic( wordCount : integer; wordWidth : integer; technology : string; readUnderWrite : string; wrAddressWidth : integer; wrDataWidth : integer; wrMaskWidth : integer; wrMaskEnable : boolean; rdAddressWidth : integer; rdDataWidth : integer ); port( clk : in std_logic; wr_en : in std_logic; wr_mask : in std_logic_vector; wr_addr : in unsigned; wr_data : in std_logic_vector; rd_addr : in unsigned; rd_data : out std_logic_vector ); end component; -- Simple synchronous dual port (1 write port, 1 read port) component Ram_1w_1rs is generic( wordCount : integer; wordWidth : integer; clockCrossing : boolean; technology : string; readUnderWrite : string; wrAddressWidth : integer; wrDataWidth : integer; wrMaskWidth : integer; wrMaskEnable : boolean; rdAddressWidth : integer; rdDataWidth : integer; rdEnEnable : boolean ); port( wr_clk : in std_logic; wr_en : in std_logic; wr_mask : in std_logic_vector; wr_addr : in unsigned; wr_data : in std_logic_vector; rd_clk : in std_logic; rd_en : in std_logic; rd_addr : in unsigned; rd_data : out std_logic_vector ); end component; -- Single port (1 readWrite port) component Ram_1wrs is generic( wordCount : integer; wordWidth : integer; readUnderWrite : string; technology : string ); port( clk : in std_logic; en : in std_logic; wr : in std_logic; addr : in unsigned; wrData : in std_logic_vector; rdData : out std_logic_vector ); end component; --True dual port (2 readWrite port) component Ram_2wrs is generic( wordCount : integer; wordWidth : integer; clockCrossing : boolean; technology : string; portA_readUnderWrite : string; portA_addressWidth : integer; portA_dataWidth : integer; portA_maskWidth : integer; portA_maskEnable : boolean; portB_readUnderWrite : string; portB_addressWidth : integer; portB_dataWidth : integer; portB_maskWidth : integer; portB_maskEnable : boolean ); port( portA_clk : in std_logic; portA_en : in std_logic; portA_wr : in std_logic; portA_mask : in std_logic_vector; portA_addr : in unsigned; portA_wrData : in std_logic_vector; portA_rdData : out std_logic_vector; portB_clk : in std_logic; portB_en : in std_logic; portB_wr : in std_logic; portB_mask : in std_logic_vector; portB_addr : in unsigned; portB_wrData : in std_logic_vector; portB_rdData : out std_logic_vector ); end component;
As you can see, blackboxes has a technical parameter. To set it, you can use the setTechnology function on the corresponding memory. There are four possible technologies:
- auto
- ramBlock
- distributedLut
- registerFile