Spin HDL - 09 - timing logic

Keywords: Scala SV

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 syntaxDescription 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:

SyntaxDescription
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:

SyntaxDescriptionReturn
mem(address) := dataSynchronous write
mem(x)Asynchronous readT
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 readingT
mem.readSync(address [enable] [readUnderWrite] [clockCrossing])Synchronous reading with optional enable, read-under-write policy and clockCrossing modeT
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.

KindsDescription
dontCareDon't care about the read value when the case occurs
readFirstReading will get the old value (before writing)
writeFirstReading 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:

SyntaxDescription
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:

KindsDescription
blackboxAllBlack box all memory. An error is thrown on non black box memory.
blackboxAllWhatsYouCanBlack box all the memory of the black box.
`blackboxRequestedAndUninferableBlackbox memory specified by the user and memory known to be unable to infer (mixed width,...). An error is thrown on non black box memory.
blackboxOnlyIfRequestedThe 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

Posted by Terminator on Mon, 29 Nov 2021 03:05:48 -0800