Registers and Memories

Registers

class pyrtl.Register(bitwidth, name='', reset_value=None, block=None)[source]

Bases: WireVector

A WireVector with an embedded register state element.

Registers only update their outputs on the rising edges of an implicit clock signal. The “value” in the current cycle can be accessed by referencing the Register itself. To set the value for the next cycle (after the next rising clock edge), set the Register.next property with the <<= (__ilshift__()) operator.

Registers reset to zero by default, and reside in the same clock domain.

Example:

>>> counter = pyrtl.Register(name="counter", bitwidth=2)
>>> counter.next <<= counter + 1

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple(nsteps=6)
>>> sim.tracer.trace["counter"]
[0, 1, 2, 3, 0, 1]

This builds a zero-initialized 2-bit counter. The second line sets the counter’s value in the next cycle (counter.next) to the counter’s value in the current cycle (counter), plus one.

__init__(bitwidth, name='', reset_value=None, block=None)[source]

Construct a register.

It is an error if the reset_value cannot fit into the specified bitwidth for this register.

Parameters:
  • bitwidth (int) – Number of bits to represent this register.

  • name (str, default: '') – The name of the register’s current value (reg, not reg.next). Must be unique. If none is provided, one will be autogenerated.

  • reset_value (int | None, default: None) – Value to initialize this register to during simulation and in any code (e.g. Verilog) that is exported. Defaults to 0. Can be overridden at simulation time.

  • block (Block, default: None) – The block under which the wire should be placed. Defaults to the working_block.

property next

Sets the Register’s value for the next cycle (it is before the D-Latch).

reset_value: int

The Register’s reset value.

This is the reset value’s raw bit value, which will not match the reset_value passed to Register’s constructor for negative values.

Example:

>>> pos = pyrtl.Register(bitwidth=2, reset_value=3)
>>> pos.reset_value
3

>>> neg = pyrtl.Register(bitwidth=3, reset_value=-3)
>>> neg.reset_value
5
>>> bin(neg.reset_value)
'0b101'

Memories

class pyrtl.MemBlock(bitwidth, addrwidth, name='', max_read_ports=2, max_write_ports=1, asynchronous=False, block=None)[source]

MemBlock is the object for specifying block memories.

MemBlock can be indexed like an array for reads and writes. Example:

>>> mem = pyrtl.MemBlock(bitwidth=8, addrwidth=2)

>>> # Write to each address, starting from address 1.
>>> write_addr = pyrtl.Register(name="write_addr", bitwidth=2, reset_value=1)
>>> write_addr.next <<= write_addr + 1

>>> mem[write_addr] <<= write_addr + 10  # Creates a write port.

>>> # Read from each address, starting from address 0.
>>> read_addr = pyrtl.Register(name="read_addr", bitwidth=2)
>>> read_addr.next <<= read_addr + 1

>>> read_data = pyrtl.Output(name="read_data")
>>> read_data <<= mem[read_addr]  # Creates a read port.

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple(nsteps=6)
>>> sim.tracer.trace["write_addr"]
[1, 2, 3, 0, 1, 2]
>>> sim.tracer.trace["read_addr"]
[0, 1, 2, 3, 0, 1]
>>> sim.tracer.trace["read_data"]
[0, 11, 12, 13, 10, 11]

When the address of a memory is assigned to using an EnabledWrite object, data will only be written to the memory when EnabledWrite.enable is high (1). In the following example, the MemBlock is only written when write_addr is odd:

>>> mem = pyrtl.MemBlock(bitwidth=8, addrwidth=2)

>>> write_addr = pyrtl.Register(name="write_addr", bitwidth=2)
>>> write_addr.next <<= write_addr + 1
>>> mem[write_addr] <<= pyrtl.MemBlock.EnabledWrite(
...     enable=write_addr[0], data=write_addr + 10)

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple(nsteps=6)
>>> sorted(sim.inspect_mem(mem).items())
[(1, 11), (3, 13)]

Writes under Conditional Assignment with |= (__ior__()) are automatically converted to EnabledWrites.

Asynchronous Memories

It is best practice to have memory operations start on a rising clock edge if you want them to synthesize into efficient hardware, so MemBlocks are synchronous by default (asynchronous=False). MemBlocks will enforce this by checking that all their inputs are ready at each rising clock edge. This implies that all MemBlock inputs - the address to read/write, the data to write, and the write-enable bit - must be Registers, Inputs, or Consts, unless you explicitly declare the memory as asynchronous with asynchronous=True.

Asynchronous memories are convenient, but they are rarely a good idea. They can’t be mapped to block RAMs in FPGAs and will be converted to registers by most design tools. They are not a realistic option for memories with more than a few hundred elements.

Read and Write Ports

Each read or write to the memory will create a new port (either a read port or write port respectively). By default memories are limited to 2 read ports and 1 write port, to keep designs efficient by default, but those values can be changed with max_read_ports and max_write_ports. Note that memories with many ports may not map to physical memories such as block RAMs or existing memory hardware macros.

Default Values

In PyRTL Simulation, all MemBlocks are zero-initialized by default. Initial data can be specified for each MemBlock in Simulation.__init__()’s memory_value_map.

Simultaneous Read and Write

In PyRTL Simulation, if the same address is read and written in the same cycle, the read will return the last value stored in the MemBlock, not the newly written value. Example:

>>> mem = pyrtl.MemBlock(addrwidth=1, bitwidth=1)
>>> mem[0] <<= 1
>>> read_data = pyrtl.Output(name="read_data", bitwidth=1)
>>> read_data <<= mem[0]

>>> # In the first cycle, read_data will be the default MemBlock data value
>>> # (0), not the newly written value (1).
>>> sim = pyrtl.Simulation()
>>> sim.step()
>>> sim.inspect("read_data")
0

# In the second cycle, read_data will be the newly written value (1).
>>> sim.step()
>>> sim.inspect("read_data")
1

Mapping MemBlocks to Hardware

Synchronous MemBlocks can generally be mapped to FPGA block RAMs and similar hardware, but there are many pitfalls:

  1. asynchronous=False is generally necessary, but may not be sufficient, for mapping a design to FPGA block RAMs. Block RAMs may have additional timing constraints, like requiring register outputs for each block RAM. asynchronous=False only requires register inputs.

  2. Block RAMs may offer more or less read and write ports than MemBlock’s defaults.

  3. Block RAMs may not zero-initialize by default.

  4. Block RAMs may implement simultaneous reads and writes in different ways.

class EnabledWrite(data: WireVector, enable: WireVector)[source]

Generates logic to conditionally enable a write port.

data: WireVector

Data to write.

enable: WireVector

Single-bit WireVector indicating if a write should occur.

__getitem__(addr)[source]

Create a read port to read data from the MemBlock.

Parameters:

addr (WireVector | int | str | bool) – MemBlock address to read. A WireVector, or any type that can be coerced to WireVector by as_wires().

Return type:

WireVector

Returns:

A WireVector containing the data read from the MemBlock at address addr.

__init__(bitwidth, addrwidth, name='', max_read_ports=2, max_write_ports=1, asynchronous=False, block=None)[source]

Create a PyRTL read-write memory.

Parameters:
  • bitwidth (int) – The bitwidth of each element in the memory.

  • addrwidth (int) – The number of bits used to address an element in the memory. The memory can store 2 ** addrwidth elements.

  • name (str, default: '') – Name of the memory. Defaults to an autogenerated name.

  • max_read_ports (int, default: 2) – limits the number of read ports each block can create; passing None indicates there is no limit.

  • max_write_ports (int, default: 1) – limits the number of write ports each block can create; passing None indicates there is no limit.

  • asynchronous (bool, default: False) – If False, ensure that all memory inputs are registers, inputs, or constants. See Asynchronous Memories.

  • block (Block, default: None) – The block to add the MemBlock to, defaults to the working_block.

__setitem__(addr, data)[source]

Create a write port to write data to the MemBlock.

Parameters:
id: int

A unique integer assigned to each MemBlock.

Example:

>>> mem_a = pyrtl.MemBlock(bitwidth=3, addrwidth=2)
>>> mem_a.id
0

>>> mem_b = pyrtl.MemBlock(bitwidth=4, addrwidth=5)
>>> mem_b.id
1

ROMs

class pyrtl.RomBlock(bitwidth, addrwidth, romdata, name='', max_read_ports=2, build_new_roms=False, asynchronous=False, pad_with_zeros=False, block=None)[source]

Bases: MemBlock

PyRTL Read Only Memory (ROM).

RomBlocks are PyRTL’s read only memory block. They support the same read interface as MemBlock, but they cannot be written to (i.e. there are no write ports). The ROM’s contents are specified when the ROM is constructed, as romdata.

Example that creates and reads a 4-element ROM:

>>> rom = pyrtl.RomBlock(bitwidth=3, addrwidth=2, romdata=[4, 5, 6, 7])
>>> read_addr = pyrtl.Register(name="read_addr", bitwidth=2)
>>> read_addr.next <<= read_addr + 1
>>> data = pyrtl.Output(name="data")
>>> data <<= rom[read_addr]

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple(nsteps=6)
>>> sim.tracer.trace["read_addr"]
[0, 1, 2, 3, 0, 1]
>>> sim.tracer.trace["data"]
[4, 5, 6, 7, 4, 5]
__getitem__(addr)[source]

Create a read port to read data from the RomBlock.

If build_new_roms was specified, create a new copy of the RomBlock if the number of read ports exceeds max_read_ports.

Parameters:

addr (WireVector) – MemBlock address to read.

Raises:

PyrtlError – If addr is an int. RomBlocks hold constant data, so they don’t need to be read when the read address is statically known. Create a Const with the data at the read address instead.

Return type:

WireVector

Returns:

A WireVector containing the data read from the RomBlock at address addr.

__init__(bitwidth, addrwidth, romdata, name='', max_read_ports=2, build_new_roms=False, asynchronous=False, pad_with_zeros=False, block=None)[source]

Create a PyRTL Read Only Memory.

Parameters:
  • bitwidth (int) – The bitwidth of each element in the ROM.

  • addrwidth (int) – The number of bits used to address an element in the ROM. The ROM can store 2 ** addrwidth elements.

  • romdata (Sequence | Callable[[int], int]) – Specifies the data stored in the ROM. This can be an array or a function that maps from address to data.

  • name (str, default: '') – The identifier for the memory.

  • max_read_ports (int, default: 2) – Limits the number of read ports each block can create; passing None indicates there is no limit.

  • build_new_roms (bool, default: False) – Indicates whether RomBlock.__getitem__() should create copies of the RomBlock to avoid exceeding max_read_ports.

  • asynchronous (bool, default: False) – If False, ensure that all RomBlock inputs are registers, inputs, or constants. See Asynchronous Memories.

  • pad_with_zeros (bool, default: False) – If True, fill any missing romdata with zeros so all accesses to the ROM are well defined. Otherwise, Simulation will raise an exception when accessing unintialized data. If you are generating Verilog, you will need to specify a value for every address (in which case setting this to True will help), however for testing and simulation it useful to know if you are accessing an unspecified value (which is why it is False by default).

  • block (Block, default: None) – The block to add to, defaults to the working_block.