Fun stuff

This notebook is a collection of a few WTF moments you could have as Python enthusiast. Bottomline: This is a debatable code style.

HDL style signal assignment

To mimic VHDL or Verilog assignment style, you could be tempted to redefine your assignments to emulate:

a <= x - 1

This is simply done by inheriting from the Signal class:

In [1]:
from myirl import *
from myirl import targets
from myhdl import intbv

class HDLSignal(Signal):
    def __le__(self, other):
        return base.GenAssign(self, other)
In [2]:
a, b = [ HDLSignal(intbv()[5:], name=n) for n in ['a', 'b'] ]

logic = kernel.sensitivity.LogicContext()
logic += [
    a <= 5,
    b <= a & b
]
In [3]:
d = DummyVHDLModule()
for stmt in logic:
    stmt.emit(d)
a <= "00101";
b <= (a and b);

However, there's a catch: You will not be able to use the <= operator for instancing of a comparator. With this abuse of the new @ (matrix multiplication) operator, we could get some remedy (with side effects):

In [4]:
class MyOp(base.ConvertibleExpr):
    def __init__(self, func):
        self.func = func
    def __rmatmul__(self, other):
        return MyOp(lambda t, self=self, other=other : self.func(other, t))
    def __matmul__(self, other):
        return self.func(other)
    
ge = MyOp(lambda x, y: base.Ge(x, y)) # Generate an operator
le = MyOp(lambda x, y: base.Ge(y, x)) # swapped
In [5]:
a @le@ b
Out[5]:
b >= a
In [6]:
expr = a @ge@ b

To verify it converts correctly, check:

In [7]:
expr, expr._convert(targets.VHDL, in_condition = False)
Out[7]:
(a >= b, '(a >= b)')

However, we could also alter the <= operator into >= by this cheap hack:

In [8]:
class Chameleon(base.BoolOp):
    _opid = "ge"
    def __init__(self, lhs, rhs):
        super().__init__(rhs, lhs) # Note swapped RHS/LHS, because we use '>=' instead '<=':
    
    def emit(self, ctx):
        tmp = base.GenAssign(self.right, self.left)
        tmp.emit(ctx)
          
class HDLSignal(Signal):
    def __le__(self, other):
        return Chameleon(self, other)

To make sure it combines with boolean expressions, too:

In [9]:
a, b = [ HDLSignal(intbv()[5:], name=n) for n in ['a', 'b'] ]

logic = kernel.sensitivity.LogicContext()
logic.If = targets.vhdl.VHDLIf

# Here's the user's RTL:
logic += [
    a <= 5,
    b <= a & b,
    logic.If((a > 3) & (a <= 5)).Then(
        b <= 1
    ).Else(
        b <= (a <= 2)
    )
]

Note that a <= 2 works as expression, because it is internally swapped to a >=.

In [10]:
d = DummyVHDLModule()
for stmt in logic:
    stmt.emit(d)
a <= TO_UNSIGNED(5, 5);
b <= (a and b);
if ((a > "00011") and ("00101" >= a)) then
    b <= TO_UNSIGNED(1, 5);
else
    b <= from_bool(("00010" >= a));
end if;

Note: Earlier kernel versions were unable to properly collect the operands respectively source and destination from this construct. This is now solved.

Example from the 'library'

To use this style, we must make sure to use the import the style_hdl module as follows (this overrides Signal and process by a derived functionality).

In [11]:
from myirl.library.style_hdl import *
from myirl.test.common_test import run_ghdl, clkgen
from myirl import targets, simulation

@block
def unit1():
    a, b = [ Signal(intbv()[8:]) for _ in range(2) ]
    q = Signal(intbv(19)[9:])
    clk = ClkSignal(name = "master_clock")
    rst = ResetSignal(ResetSignal.NEG_ASYNC)
    en = Signal(bool())

    thresh = Signal(bool(True))

    oscillator = clkgen(clk, 2)

    @genprocess(clk, EDGE=clk.POS, RESET=rst)
    def worker1():
        yield [
            worker1.If(en == True).Then(
                q <= a + b
            )
        ]

    @genprocess(clk, EDGE=clk.POS, RESET=rst)
    def worker2():
        yield [
            worker2.If(q <= 4).Then(
                thresh <= '1'
            ).Else(
                thresh <= '0'
            )
        ]

    @simulation.generator
    def seq1():
        yield [
            rst.set(False),
            simulation.wait('1 ns'),
            rst.set(True),
            simulation.assert_(thresh == True, "#1 '<=' test failed"),

            simulation.wait(clk.posedge),
            simulation.assert_(q == 19, "failed to reset"),
            a.set(2), b.set(1), en.set(True),
            simulation.wait(clk.posedge),
            simulation.wait(clk.posedge),
            simulation.wait('1 ns'),
            simulation.assert_(thresh == True, "#2 '<=' test failed"),
            a.set(2), b.set(3), en.set(True),
            simulation.wait(clk.posedge),
            simulation.wait(clk.posedge),
            simulation.assert_(q == 5, "failed to add"),
            simulation.wait('1 ns'),
            simulation.assert_(thresh == False, "#3 '<=' test failed"),
            simulation.print_(a, b, q)
        ]

    return instances()
In [12]:
def test_unit1():
    inst = unit1()

    files = inst.elab(targets.VHDL, elab_all = True)
    run_ghdl(files, inst, debug = True, vcdfile="unit1.vcd")

test_unit1()
Creating sequential 'unit1/seq1' 
 Elaborating component clkgen_s1_2 
 Writing 'clkgen' to file /tmp/myirl_top_unit1_jjippy9l/clkgen.vhdl 
 Elaborating component unit1 
 Writing 'unit1' to file /tmp/myirl_top_unit1_jjippy9l/unit1.vhdl 
 Creating library file /tmp/myirl_module_defs_yrieudgv/module_defs.vhdl 
==== COSIM stdout ====

==== COSIM stderr ====

==== COSIM stdout ====
analyze /home/testing/.local/lib/python3.9/site-packages/myirl-0.0.0-py3.9-linux-x86_64.egg/myirl/targets/../test/vhdl/txt_util.vhdl
analyze /home/testing/.local/lib/python3.9/site-packages/myirl-0.0.0-py3.9-linux-x86_64.egg/myirl/targets/libmyirl.vhdl
analyze /tmp/myirl_top_unit1_jjippy9l/clkgen.vhdl
analyze /tmp/myirl_top_unit1_jjippy9l/unit1.vhdl
elaborate unit1

==== COSIM stderr ====

==== COSIM stdout ====
0x02 0x03 0x005
/tmp/unit1:info: simulation stopped by --stop-time @1us

==== COSIM stderr ====

Two way association

To make complex wirings for interface bulk signal types more readable, we define a different operator class in particular for connections. For example, a Port class may be in/out from the source, out/in ('reverse') from the destination. Within a module though, we may have to distribute the signals of a Port to several instances.

In [13]:
class MyOpX(base.ConvertibleExpr):
    def __init__(self, func):
        self.func = func
    def __rlshift__(self, other):
        return MyOpX(lambda t, self=self, other=other : self.func(other, t))
    def __rshift__(self, other):
        return self.func(other)

Create a SpecialOps derivative and pass that as TYPE parameter to your bulk signal class. The @hdlmacro generates the connections between self and the other port class.

In [14]:
from myirl.library.bulksignals import *

class SpecialOps(ContainerGen):
    twoway = MyOpX(lambda x, y: x.assign(y)) # Generate an operator

@bulkwrapper(targets.vhdl, TYPE=SpecialOps)
class Port:
    _inputs = ['input']
    _outputs = ['output']
    _other = []
    
    def __init__(self):
        self.input = Signal(bool())
        self.output = Signal(bool())
    
    @hdlmacro
    def assign(self, other):
        "Do the two way connection between peers"
        yield [
            self.output.set(other.input),
            other.output.set(self.input)
        ]
        
    @hdlmacro    
    def __le__(self, other):
        "Wire signals members one way to peer"
        yield [
            self.input.set(other.input),
            self.output.set(other.output)
        ]
In [15]:
p, q0, q1 = [ Port(name=n) for n in ['p', 'p0', 'p1'] ]
quiet = p.rename('p'), q0.rename('q0'), q1.rename('q1')
In [16]:
connections = [
    p     <<Port.twoway>>     q0,
    q1    <=                   p
]
In [17]:
for stmt in connections:
    print("----------------")
    stmt.emit(d)
----------------
p_out.output <= p0_in.input;
p0_out.output <= p_in.input;
----------------
p1_in.input <= p_in.input;
p1_out.output <= p_out.output;

Coding style issues

Derived classes may redefine the __le__ method to implement custom assignments, such as flexbv types performing latency and precision verification behind the curtains. The .set Method may also be overriden by some BulkSignal types internally. So there are a few ways to shoot yourself into the foot.

A guideline to keep it clean:

  • Use .set for 1:1 assignment in synchronous or asynchronous processes
  • Use .wireup for direct connections (outside process)
  • Use <= for one way assignments only
  • Use <<custom.operator>> style for two way custom connections between signal containers
  • Pipeline signals may inherit specific properties using custom setters

Pitfalls

The <= style assignment, when used on vector data types and tuple notation, exhibits a pitfall:

In [18]:
from myirl.vector import VectorSignal

v, w = [ VectorSignal(2, intbv()[5:]) for _ in range(2) ]

v <= w[0] + w[1], v[0] + v[1]
Out[18]:
(<myirl.vector.VectorAssign at 0x7f2260b75a40>, ADD(s_9c63, s_9c63))

This results in the last expression being a tuple instead of type assignment, plus not do what's desired, effectively it is: v.set(w[0] + w[1] and a new expression is created after the ,.

Correct would be:

In [19]:
v <= (w[0] + w[1], v[0] + v[1])
Out[19]:
<myirl.vector.VectorAssign at 0x7f2260b75180>