MyHDL Legacy code migration notes

Some scenarios might be duplicates of myhdl_changes.ipynb

Terminology

As hardware generators in general are considered functions that are decorated using a

  • @always, @always_comb, @always_seq

as opposed to simulation constructs not generating hardware in particular: @instance

Thumb rules

  • Hierarchical resolution and (optional) strict typing in place
  • Everything outside a hardware generator is executed and 'pythonic':
    • Internal representation constructs allow references to logical combinations, like: a = c ^ b
    • Everything instanced by a variable in the @block context can be digital logic, signal, or reference and may be resolved into hardware
  • Hardware generators are AST-translated to IRL (Intermediate Representation Language) and then executed for translation/transpilation.

With this in mind, you'll have to:

  • Turn global variables determining conditional compilation/translation into function parameters (before the * PEP570 construct)
  • Eliminate loop constructs (for ...) inside generators, see Loop issues.
  • Clean up boolean constructs (see Boolean Logic operations):
    • Make sure to use boolean logic only within conditional statements
    • Translate to binary logic operations when assigning to a signal (MIND THE OPERATOR PRECEDENCE!)
  • Turn sig.next[i] into sig[i].next, see Member assignment
  • Clean up variable usage (see Variable usage):
    • First occurence of a variable assignment to a static type (within a generator context only) defines its data type
    • Avoid using variables where possible, rather reserve an auxiliary signal or use a reference (outside the generator)
  • Function interface:
    • @blocks only allow dedicated inputs or outputs, no inout signals (except TristateSignals)
    • Signals passed as output can not be read from
    • Optional: strict typing for port types and generics that should resolve to HDL, see Interfaces
  • Handle class derival hierarchies:
    • Classes containing signals only are resolved as in/out upon their driver state
    • Strictly interface-typed classes must be decorated, see Classes
  • Revisit function calls (see Functions):
    • Keep in mind that undecorated functions are called and their logic constructs will unroll entirely, i.e. no function definition is created in the resulting HDL
    • Turn function calls into HDL @block wherever possible
  • Arithmetics: Handle bit sizes according to translation errors/warnings. See also Arithmetic Pitfalls.

Test unit

The following block is a simple example to start from. Modifications of this base will exhibit a few pitfalls during migration. First, we import from emulation:

In [1]:
from myirl.emulation.myhdl import *

Then we define an auxiliary for cheap unit testing using VHDL-93 dialect and GHDL for reference testing:

In [2]:
from myirl.test.common_test import run_ghdl

def test(uut, param = (), debug = False):
    inst = uut(*param)
    vhdl93 = targets.vhdl.VHDL93()
    f = inst.elab(vhdl93, elab_all=True)
    run_ghdl(f, inst, debug = debug, std = '93', vcdfile = inst.name + '.vcd')
    return f
In [3]:
@block
def unit():
    a = Signal(intbv(0xaa)[8:])
    a.init = True
    q = Signal(bool())

    @instance
    def stim():
        q.next = False
        if a[0] == True and a[1] == False and a[7] == False:  # True boolean evaluation
            q.next = True
            
        yield delay(1)
        assert q == False
    
    return instances()

test(unit)
Creating sequential 'unit/stim' 
 Elaborating component unit 
 Writing 'unit' to file /tmp/myirl_top_unit_dtng0n7w/unit.vhdl 
 Creating library file /tmp/myirl_module_defs_pq7fpoqr/module_defs.vhdl 
Out[3]:
['/tmp/myirl_top_unit_dtng0n7w/unit.vhdl',
 '/tmp/myirl_module_defs_pq7fpoqr/module_defs.vhdl']

Variations

Instead of creating an auxiliary signal, we can create references to signal combinations inside the @block context. However, this may create redundant code when resolving to a HDL, as the reference is a true Python IRL object.

In [4]:
@block
def unit():
    a = Signal(intbv(0xaa)[8:])
    a.init = True
    p, q = [ Signal(bool()) for _ in range(2) ]
    
    z = (a[7] == True) & (a[6] == False) & (a[0] == True) # Reference to binary combination of boolean expressions
    
    zb = a[7] & ~a[6] & a[0]  # True binary combination
    
    zs = Signal(bool())
    
    # New wiring 'generator' construct:
    wires = [
        zs.set(zb)
    ]

    @instance
    def stim():
        q.next = False
        p.next = True
        
        yield delay(1)
        if a[7] == True and a[6] == False and a[0] == True:  # True boolean evaluation
            q.next = True
            
        yield delay(1)
            
        if z:  # Evaluate reference
            q.next = True
            
        yield delay(1)

        if zb: # Evaluate binary op reference
            q.next = True
            
        if zs == True: # Check signal
            q.next = True
            
        yield delay(1)
        assert q == False

        a.next = 0xa1
        yield delay(1)
        p.next = z
        yield delay(1)

        assert p == True
    
    return instances()


f = test(unit, debug = True)
Creating sequential 'unit/stim' 
 Elaborating component unit 
 Writing 'unit' to file /tmp/myirl_top_unit_oagfb67y/unit.vhdl 
 Creating library file /tmp/myirl_module_defs_q1n_jyjw/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_unit_oagfb67y/unit.vhdl
elaborate unit

==== COSIM stderr ====

==== COSIM stdout ====

==== COSIM stderr ====

In [5]:
!cat -n {f[0]}
     1	-- File generated from source:
     2	--     /tmp/ipykernel_894/3287474469.py
     3	-- (c) 2016-2021 section5.ch
     4	-- Modifications may be lost, edit the source file instead.
     5	
     6	library IEEE;
     7	use IEEE.std_logic_1164.all;
     8	use IEEE.numeric_std.all;
     9	
    10	library work;
    11	
    12	use work.txt_util.all;
    13	use work.myirl_conversion.all;
    14	
    15	entity unit is
    16	end entity unit;
    17	
    18	architecture MyIRL of unit is
    19	    -- Local type declarations
    20	    -- Signal declarations
    21	    signal q : std_ulogic;
    22	    signal p : std_ulogic;
    23	    signal a : unsigned(7 downto 0) := x"aa";
    24	    signal zs : std_ulogic;
    25	begin
    26	    
    27	stim:
    28	    process
    29	    begin
    30	        q <= '0';
    31	        p <= '1';
    32	        wait for 1 ns;
    33	        if (((a(7) = '1') and (a(6) = '0')) and (a(0) = '1')) then
    34	            q <= '1';
    35	        end if;
    36	        wait for 1 ns;
    37	        if (((a(7) = '1') and (a(6) = '0')) and (a(0) = '1')) then
    38	            q <= '1';
    39	        end if;
    40	        wait for 1 ns;
    41	        if (((a(7) and not a(6)) and a(0))) = '1' then
    42	            q <= '1';
    43	        end if;
    44	        if (zs = '1') then
    45	            q <= '1';
    46	        end if;
    47	        wait for 1 ns;
    48	        assert (q = '0')
    49	            report "Failed in /tmp/ipykernel_894/3287474469.py:unit():41" severity failure;
    50	        a <= x"a1";
    51	        wait for 1 ns;
    52	        p <= from_bool((((a(7) = '1') and (a(6) = '0')) and (a(0) = '1')));
    53	        wait for 1 ns;
    54	        assert (p = '1')
    55	            report "Failed in /tmp/ipykernel_894/3287474469.py:unit():48" severity failure;
    56	        wait;
    57	    end process;
    58	    zs <= ((a(7) and not a(6)) and a(0));
    59	end architecture MyIRL;
    60	

Variable usage

Under scrutiny. For now, avoid complicated variable scenarios. Variable usage in hardware generation will be unsupported for non-VHDL targets, however it is safe to use them for simulation constructs.

  • bool() types may not resolve to std_logic when mixed with signals
  • A Variable type assigned to False yields a boolean type in the resulting HDL, whereas a Signal(bool()) type assigned to a Python bool results in a std_logic output.

Note: In general, do not try generating hardware with variables. Use signals where possible, or use references to logic combinations.

Configuration variables

Make sure to put variables that are supposed to be generic parameters past the * and ensure they are given either a default (when desired in the HDL output) or a type hint:

In [6]:
@block
def unit1(a : Signal, b: Signal.Output, * , PARAM : bool = False):
    @always_comb
    def worker():
        if a == 5:
            b.next = 0
        elif PARAM:
            b.next = 1
    return instances()

@block
def tb(unit):
    a, b = (Signal(intbv()[5:]) for _ in range(2))
    uut = unit(a, b, PARAM = True)
    
    return instances()
In [7]:
f = test(tb, (unit1, ), debug = False)
 Elaborating component unit1_s5_s5_1 
 Writing 'unit1' to file /tmp/myirl_top_tb_tmvefa0v/unit1.vhdl 
 Elaborating component tb__wrapped_wrapper 
 Writing 'tb' to file /tmp/myirl_top_tb_tmvefa0v/tb.vhdl 
 Creating library file /tmp/myirl_module_defs_ownrrxsp/module_defs.vhdl 
In [8]:
!grep -A 5 generic {f[0]}
    generic (
        PARAM: boolean := FALSE
    );
    port (
        a : in unsigned(4 downto 0);
        b : out unsigned(4 downto 0)

Run-time parameter variant

When PARAM is not separated by a *, it will not be inferred to HDL but resolved. This is used for conditional compilation.

In [9]:
@block
def unit2(a : Signal, b: Signal.Output, PARAM : bool = False):
    @always_comb
    def worker():
        if a == 5:
            b.next = 0
        elif PARAM:
            b.next = 1
    return instances()
In [10]:
f = test(tb, (unit2, ), debug = False)
 Elaborating component unit1_s5_s5_1 
 Writing 'unit1' to file /tmp/myirl_top_tb_tmvefa0v/unit1.vhdl 
 Elaborating component tb__wrapped_wrapper 
 Writing 'tb' to file /tmp/myirl_top_tb_tmvefa0v/tb.vhdl 
 Creating library file /tmp/myirl_module_defs_4rep3_4w/module_defs.vhdl 

Note that PARAM is not converted into a generic, and occurences in the HDL code are resolved statically.

In [11]:
!cat {f[0]}
-- File generated from source:
--     /tmp/ipykernel_894/771871232.py
-- (c) 2016-2021 section5.ch
-- Modifications may be lost, edit the source file instead.

library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;

library work;

use work.txt_util.all;
use work.myirl_conversion.all;

entity unit1 is
    generic (
        PARAM: boolean := FALSE
    );
    port (
        a : in unsigned(4 downto 0);
        b : out unsigned(4 downto 0)
    );
end entity unit1;

architecture MyIRL of unit1 is
    -- Local type declarations
    -- Signal declarations
begin
    
worker:
    process(a)
    begin
        if (a = "00101") then
            b <= "00000";
        elsif PARAM then
            b <= "00001";
        end if;
    end process;

end architecture MyIRL;

Loop issues

MyHDL allows loops inside hardware descriptions and creates generate statements in the resulting HDL.

In [12]:
Bool = Signal.Type(bool)

@block
def unit_loop0(b : Bool, N : int = 5):
    a = [ Signal(bool()) for _ in range(N) ]
    
    @always_comb
    def worker():
        a[0].next = b
        for i in range(1, 5):
            a[i].next = ~a[i-1]
        
    return instances()

This is no longer supported, except in simulation constructs implemented inside @instance functions. Migration strategy:

  • Move loop to the @block level, use procedural instancing
  • Implement using @process or @genprocess constructs in the IRL

Replace by:

In [13]:
@block
def unit_loop1(b : Bool):
    a = [ Signal(bool()) for _ in range(5) ]
    
    wires = [ a[0].set(b) ]
    wires += [ a[i].set(a[i-1]) for i in range(1, 5)]       
    return instances()

or within a library, using IRL:

In [14]:
import myirl


@myirl.block
def unit_loop2(b: Bool, N = 5):
    a = [ Signal(bool()) for _ in range(N) ]
    
    @myirl.genprocess()
    def worker():
        yield [ a[0].set(b) ]
        yield [ a[i].set(~a[i-1]) for i in range(1, N) ]
            
    return instances()

Examine VHDL output:

In [15]:
def test_loop(unit):
    b = Bool()
    uut = unit(b, 8)
    f = uut.elab(targets.VHDL)
    return f
    
f = test_loop(unit_loop2)
 Writing 'unit_loop2' to file /tmp/myirl_top_unit_loop2_83oxa5hm/unit_loop2.vhdl 
In [16]:
! grep -A 30 architecture {f[0]}
architecture MyIRL of unit_loop2 is
    -- Local type declarations
    -- Signal declarations
    signal a0 : std_ulogic;
    signal a1 : std_ulogic;
    signal a2 : std_ulogic;
    signal a3 : std_ulogic;
    signal a4 : std_ulogic;
    signal a5 : std_ulogic;
    signal a6 : std_ulogic;
    signal a7 : std_ulogic;
begin
    
worker:
    process(b, a0, a1, a2, a3, a4, a5, a6)
    begin
        a0 <= b;
        a1 <= not a0;
        a2 <= not a1;
        a3 <= not a2;
        a4 <= not a3;
        a5 <= not a4;
        a6 <= not a5;
        a7 <= not a6;
    end process;

end architecture MyIRL;

Obviously, loop statements unroll explicitely. Thus, this is not intended for iterating through a large array. See also Loops.

Classes

Recommended approach for porting legacy class constructs containing signals:

  • Derive a class from your existing, undecorated signal class
  • Decorate this derived class, depending on desired interface resolving:
    • @container() for a bidirectional class (creates container structures)
    • @container(mode=LEGACY_CLASS) for legacy class behaviour (resolves each member of the signal)
    • @bulkwrapper(target_list) for unidirectional internal types supported by specific targets only

For bidirectional classes, you need to define input, output and auxiliary ports (always inputs).

This allows to still use your legacy class constructs from older myHDL code.

The derived classes can be used for type specification in the interface.