Basic Logic, Wires, and Memories

Wires define the relationship between logic blocks in PyRTL. They are treated like normal wires in traditional RTL systems except the Register wire. Logic is then created when wires are combined with one another using the provided operators. For example, if a and b are both of type WireVector, the a + b will make an adder, plug a and b into the inputs of that adder, and return a new WireVector which is the output of that adder. Wires provide the basic input and output interfaces to the generated Block which stores the description of the hardware as you build it.

WireVector

class pyrtl.wire.WireVector(bitwidth=None, name=u'', block=None)

Bases: object

The main class for describing the connections between operators.

WireVectors act much like a list of wires, except that there is no “contained” type, each slice of a wirevector is itself a wirevector (even if it just contains a single “bit” of information). The least significant bit of the wire is at index 0 and normal list slicing syntax applies (i.e. myvector[0:5] makes a new vector from the bottom 5 bits of myvector, myvector[-1] takes the most significant bit, and myvector[-4:] takes the 4 most significant bits).

Operation Syntax Function
Addition a + b Creates an adder, returns WireVector
Subtraction a - b Subtraction (twos complement)
Multiplication a * b Creates an multiplier, returns WireVector
Xor a ^ b Bitwise XOR, returns WireVector
Or a | b Bitwise OR, returns WireVector
And a & b Bitwise AND, returns WireVector
Invert ~a Bitwise invert, returns WireVector
Less Than a < b Less than, return 1-bit WireVector
Less or Eq. a <= b Less than or Equal to, return 1-bit WireVector
Greater Than a > b Greater than, return 1-bit WireVector
Greater or Eq. a >= b Greater or Equal to, return 1-bit WireVector
Equality a == b Hardware to check equality, return 1-bit WireVector
Not Equal a != b Inverted equality check, return 1-bit WireVector
Bitwidth len(a) Return bitwidth of the wirevector
Assignment a <<= b Connect from b to a (see note below)
Bit Slice a[3:6] Selects bits from wirevector, in this case bits 3,4,5

A note on <<= asssignment: This operator is how you “drive” an already created wire with an existing wire. If you were to do a = b it would lose the old value of a and simply overwrite it with a new value, in this case with a reference to wirevector b. In contrast a <<= b does not overwrite a, but simply wires the two together.

__init__(bitwidth=None, name=u'', block=None)

Construct a generic WireVector

Parameters:
  • bitwidth (int) – If no bitwidth is provided, it will be set to the minimum number of bits to represent this wire
  • block (Block) – The block under which the wire should be placed. Defaults to the working block
  • name (String) – The name of the wire referred to in some places. Must be unique. If none is provided, one will be autogenerated
Returns:

a wirevector object representing a const wire

name

A property holding the name (a string) of the WireVector, can be read or written. For example: print(a.name) or a.name = ‘mywire’.

__add__(other)

Creates a LogicNet that adds two wires together into a single wirevector.

Return Wirevector:
 Returns the result wire of the operation. The resulting wire has one more bit than the longer of the two input wires.

Addition is compatible with two’s complement signed numbers.

__sub__(other)

Creates a LogicNet that subtracts the right wire from the left one.

Return Wirevector:
 Returns the result wire of the operation. The resulting wire has one more bit than the longer of the two input wires.

Subtraction is compatible with two’s complement signed numbers.

__mul__(other)

Creates a LogicNet that multiplies two different wirevectors.

Return Wirevector:
 Returns the result wire of the operation. The resulting wire’s bitwidth is the sum of the two input wires’ bitwidths.

Multiplication is not compatible with two’s complement signed numbers.

__len__()

Get the bitwidth of a WireVector.

Return integer:Returns the length (i.e. bitwidth) of the WireVector in bits.

Note that WireVectors do not need to have a bitwidth defined when they are first allocated. They can get it from a <<= assignment later. However, if you check the len of WireVector with undefined bitwidth it will throw PyrtlError.

nand(other)

Creates a LogicNet that bitwise nands two wirevector together to a single wirevector.

Return WireVector:
 Returns wirevector of the nand operation.
bitmask

A property holding a bitmask of the same length as this WireVector. Specifically it is an integer with a number of bits set to 1 equal to the bitwidth of the WireVector.

It is often times useful to “mask” an integer such that it fits in the the number of bits of a WireVector. As a convenience for this, the bitmask property is provided. As an example, if there was a 3-bit WireVector a, a call to a.bitmask() should return 0b111 or 0x7.

sign_extended(bitwidth)

Generate a new sign extended wirevector derived from self.

Return WireVector:
 Returns a new WireVector equal to the original WireVector sign extended to the specified bitwidth.

If the bitwidth specified is smaller than the bitwidth of self, then PyrtlError is thrown.

zero_extended(bitwidth)

Generate a new zero extended wirevector derived from self.

Return WireVector:
 Returns a new WireVector equal to the original WireVector zero extended to the specified bitwidth.

If the bitwidth specified is smaller than the bitwidth of self, then PyrtlError is thrown.

Input, Output, Const, and Register

The classes Input, Output, Const, and Register are all derived from WireVector, but extend it with (or restrict it from) with certain functionality. The Input and Output classes are for those values that will be external to the entire system once complete (e.g. driving a signal off-chip, rather than the interface to some particular sub-block of the design). The Const class is useful for specifying hard-wired values, while Register is how sequential elements are created (the all have an implict clock).

class pyrtl.wire.Input(bitwidth=None, name=u'', block=None)

Bases: pyrtl.wire.WireVector

A WireVector type denoting inputs to a block (no writers)

class pyrtl.wire.Output(bitwidth=None, name=u'', block=None)

Bases: pyrtl.wire.WireVector

A WireVector type denoting outputs of a block (no readers) Even though Output seems to have valid ops such as __or__ , using them will throw an error.

class pyrtl.wire.Const(val, bitwidth=None, block=None)

Bases: pyrtl.wire.WireVector

A WireVector representation of a constant value

Converts from bool, integer, or verilog-style strings to a constant of the specified bitwidth. If the bitwidth is too short to represent the specified constant then an error is raised. If a possitive integer is specified the bitwidth can be infered from the constant. If a negative integer is provided in the simulation, it is converted to a two’s complement representation of the specified bitwidth.

__init__(val, bitwidth=None, block=None)

Construct a constant implementation at initialization

Parameters:or str val (int) – The value for the const wirevector
Returns:a wirevector object representing a const wire

Descriptions for all parameters not listed above can be found at py:method:: WireVector.__init__()

class pyrtl.wire.Register(bitwidth, name=u'', block=None)

Bases: pyrtl.wire.WireVector

A WireVector with a register state element embedded.

Registers only update their outputs on posedge of an implicit clk signal. The “value” in the current cycle can be accessed by just referencing the Register itself. To set the value for the next cycle (after the next posedge) you write to the property .next with the <<= operator. For example, if you want to specify a counter it would look like: “a.next <<= a + 1”

next

This property is the way to set what the wirevector will be the next cycle (aka, it is before the D-Latch)

MemBlock

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

Bases: pyrtl.memory._MemReadBase

MemBlock is the object for specifying block memories. In can be indexed like an array for both reading and writing. Writes under a conditional are automatically converted to enabled writes. For example, consider the following examples where addr, data, and we are all WireVectors.

Usage:

data <<= memory[addr]  (infer read port)
memory[addr] <<= data  (infer write port)
mem[address] = MemBlock.EnabledWrite(data,enable=we)

When the address of a memory is assigned to using a EnableWrite object items will only be written to the memory when the enable WireVector is set to high (1)

class EnabledWrite(data, enable)

Bases: tuple

Allows for an enable bit for each write port, where data (the first field in the tuple) is the normal data address, and enable (the second field) is a one bit signal specifying that the write should happen (i.e. active high).

data

Alias for field number 0

enable

Alias for field number 1

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

Create a PyRTL read-write memory.

Parameters:
  • bitwidth (int) – Defines the bitwidth of each element in the memory
  • addrwidth (int) – The number of bits used to address an element of the memory. This also defines the size of the memory
  • name (basestring) – The identifier for the memory
  • max_write_ports (max_read_ports,) – limits the number of read and write ports each block can create; passing None to either indicates there is no limit
  • asynchronous (bool) – If false make sure that memory reads are only done using values straight from a register. (aka make sure that the read is synchronous)
  • name – Name of the memory. Defaults to an autogenerated name
  • block – The block to add it to, defaults to the working block

It is best practice to make sure your block memory/fifos read/write operations start on a clock edge if you want them to synthesize into efficient hardware. MemBlocks will enforce this by making sure that you only address them with a register or input, unless you explicitly declare the memory as asynchronous with asynchronous=True flag. Note that asynchronous mems are, while sometimes very convenient and tempting, rarely a good idea. They can’t be mapped to block rams in FPGAs and will be converted to registers by most design tools even though PyRTL can handle them with no problem. For any memory beyond a few hundred entries it is not a realistic option.

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 and 1-write port, but to keep designs efficient by default, but those values can be set as options. Note that memories with high numbers of ports may not be possible to map to physical memories such as block rams or existing memory hardware macros.

RomBlock

class pyrtl.memory.RomBlock(bitwidth, addrwidth, romdata, name=u'', max_read_ports=2, build_new_roms=False, asynchronous=False, block=None)

Bases: pyrtl.memory._MemReadBase

PyRTL Read Only Memory.

RomBlocks are the read only memory block for PyRTL. They support the same read interface and normal memories, but they are cannot be written to (i.e. there are no write ports). The ROM must be initialized with some values and construction through the use of the romdata which is the memory for the system.

__init__(bitwidth, addrwidth, romdata, name=u'', max_read_ports=2, build_new_roms=False, asynchronous=False, block=None)

Create a Python Read Only Memory.

Parameters:
  • bitwidth (int) – The bitwidth of each item stored in the ROM
  • addrwidth (int) – The bitwidth of the address bus (determines number of addresses)
  • romdata – This can either be a function or an array (iterable) that maps an address as an input to a result as an output
  • name (str) – The identifier for the memory
  • max_read_ports – limits the number of read ports each block can create; passing None indicates there is no limit
  • build_new_roms (bool) – indicates whether to create and pass new RomBlocks during __getitem__ to avoid exceeding max_read_ports
  • asynchronous (bool) – If false make sure that memory reads are only done using values straight from a register. (aka make sure that reads are synchronous)
  • block – The block to add to, defaults to the working block