Registers and Memories¶
Registers¶
- class pyrtl.Register(bitwidth, name='', reset_value=None, block=None)[source]¶
Bases:
WireVectorA 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.nextproperty 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_valuecannot fit into the specifiedbitwidthfor this register.- Parameters:
bitwidth (
int) – Number of bits to represent this register.name (
str, default:'') – The name of the register’s current value (reg, notreg.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_valuepassed toRegister’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]¶
MemBlockis the object for specifying block memories.MemBlockcan 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
EnabledWriteobject, data will only be written to the memory whenEnabledWrite.enableis high (1). In the following example, theMemBlockis only written whenwrite_addris 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 toEnabledWrites.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
MemBlocksare synchronous by default (asynchronous=False).MemBlockswill enforce this by checking that all their inputs are ready at each rising clock edge. This implies that allMemBlockinputs - the address to read/write, the data to write, and the write-enable bit - must beRegisters,Inputs, orConsts, unless you explicitly declare the memory as asynchronous withasynchronous=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_portsandmax_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, allMemBlocksare zero-initialized by default. Initial data can be specified for each MemBlock inSimulation.__init__()’smemory_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 theMemBlock, 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
MemBlocksto Hardware¶Synchronous
MemBlockscan generally be mapped to FPGA block RAMs and similar hardware, but there are many pitfalls:asynchronous=Falseis 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=Falseonly requires register inputs.Block RAMs may offer more or less read and write ports than
MemBlock’s defaults.Block RAMs may not zero-initialize by default.
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
WireVectorindicating if a write should occur.
-
data:
- __getitem__(addr)[source]¶
Create a read port to read data from the
MemBlock.- Parameters:
addr (
WireVector|int|str|bool) –MemBlockaddress to read. AWireVector, or any type that can be coerced toWireVectorbyas_wires().- Return type:
- Returns:
A
WireVectorcontaining the data read from theMemBlockat addressaddr.
- __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 store2 ** addrwidthelements.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; passingNoneindicates there is no limit.max_write_ports (
int, default:1) – limits the number of write ports each block can create; passingNoneindicates there is no limit.asynchronous (
bool, default:False) – IfFalse, 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:
addr (
WireVector|int|str|bool) –MemBlockaddress to write. AWireVector, or any type that can be coerced toWireVectorbyas_wires().data (
EnabledWrite|WireVector|int|str|bool) –MemBlockdata to write. AnEnabledWrite,WireVector, or any type that can be coerced toWireVectorbyas_wires().
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:
MemBlockPyRTL Read Only Memory (ROM).
RomBlocksare PyRTL’s read only memory block. They support the same read interface asMemBlock, but they cannot be written to (i.e. there are no write ports). The ROM’s contents are specified when the ROM is constructed, asromdata.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_romswas specified, create a new copy of theRomBlockif the number of read ports exceedsmax_read_ports.- Parameters:
addr (
WireVector) –MemBlockaddress to read.- Raises:
PyrtlError – If
addris anint.RomBlockshold constant data, so they don’t need to be read when the read address is statically known. Create aConstwith the data at the read address instead.- Return type:
- Returns:
A
WireVectorcontaining the data read from theRomBlockat addressaddr.
- __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 store2 ** addrwidthelements.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; passingNoneindicates there is no limit.build_new_roms (
bool, default:False) – Indicates whetherRomBlock.__getitem__()should create copies of theRomBlockto avoid exceedingmax_read_ports.asynchronous (
bool, default:False) – IfFalse, ensure that allRomBlockinputs are registers, inputs, or constants. See Asynchronous Memories.pad_with_zeros (
bool, default:False) – IfTrue, fill any missingromdatawith zeros so all accesses to the ROM are well defined. Otherwise,Simulationwill 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 toTruewill help), however for testing and simulation it useful to know if you are accessing an unspecified value (which is why it isFalseby default).block (
Block, default:None) – The block to add to, defaults to the working_block.