Allow kwargs in module interfaces

Hi!

There are use cases where not all members of an interface are known at coding time, let’s think about a highly parameterizable component that can modify it’s interface depending on the value of certain parameters, such as a bus interconnect or a multiple inputs logic gate. MyHDL uses the function prototype as the component interface, but it’s only able to convert protototypes where each member of the interface is explicitly declared with a name. Since Python allows us the use of variable arguments and keyword arguments, it would be a great advantage if we could use Python kwargs to model interfaces like we are doing now with named arguments.

I will show an example of a function that can take advantage of the proposed feature. I’ts a function called “instance” that create instances of generic components without needing to previously know the component’s interfaces. In a more complex example, it could hipotetically read the interface’s description at runtime from xml, yaml, etc…

This feature can be very useful when writing libraries and frameworks that can handle component libraries for MyHDL. Else, we should have known the component interface from the root of the calltrace at coding time…

from myhdl import *

@block
def instance(url, **params):
    return find_package(url, **params)

@block
def find_package(url, **params):
    return download_package(url, **params)

@block
def download_package(url, **params):
    if url.startswith("http"):
        return download_http(url, **params)
    else:
        return load_local_class(url, **params)

@block
def download_http(url, **params):
    # omitted for brevity, the call trace would have been much more complex
    # than the local class load example shown below
    pass
 
@block
def load_local_class(classname, **params):
    classobj = globals()[classname]
    mod = classobj()
    ins = mod.generate_instance(**params)
    ins.name == classname
    return ins 
    
class MyObj(object):
   def __init__(self):
      self.x = Signal(intbv(0)[8:])
      self.y = Signal(intbv(0)[4:])
      self.z = Signal(intbv(0)[9:])
      
class mod(object):
    @block
    def generate_instance(self, **kwargs):
        return self.select_version(**kwargs)
    
    @block
    def select_version(self, **kwargs):
        if kwargs["version"] == 1: 
            return self.mod_example1(**kwargs)
        else:
            return self.mod_example2(kwargs["xyz"], kwargs["clk"])
        
    @block
    def mod_example1(self, **kwargs):
        xyz = kwargs["xyz"]
        @always(kwargs["clk"])
        def hdl():
            xyz.z.next = xyz.x + xyz.y
        return hdl
    
    @block
    def mod_example2(self, xyz, clk):
        @always(clk)
        def hdl():
            xyz.z.next = xyz.x + xyz.y
        return hdl
    
instance("mod", **{"version": 1, "xyz":MyObj(), "clk":Signal(False)}).convert(hdl='Verilog', name="modv1")
instance("mod", **{"version": 2, "xyz":MyObj(), "clk":Signal(False)}).convert(hdl='Verilog', name="modv2")

The current behaviour of MyHDL generates the following:

// File: modv1.v
// Generated by MyHDL 0.11
// Date: Wed Dec 11 14:42:08 2019


`timescale 1ns/10ps

module modv1 (

);



reg [8:0] find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_z;
wire [7:0] find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_x;
wire [3:0] find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_y;

assign find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_x = 8'd0;
assign find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_y = 4'd0;


always @(None) begin: MODV1_FIND_PACKAGE0_DOWNLOAD_PACKAGE0_LOAD_LOCAL_CLASS0_MOD0_GENERATE_INSTANCE0_MOD1_SELECT_VERSION0_MOD2_MOD_EXAMPLE10_HDL
    find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_z <= (find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_x + find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_y);
end

endmodule

As you can see, the module interface is empty and the input signals are driven within the module. I think that the expected behaviour, if kwargs were supported, would have been the following:

// File: modv1.v
// Generated by MyHDL 0.11
// Date: Wed Dec 11 14:45:28 2019


`timescale 1ns/10ps

module modv1 (
    clk,
    find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_x,
    find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_y,
    find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_z
);


input clk;
input [7:0] find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_x;
input [3:0] find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_y;
output [8:0] find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_z;
reg [8:0] find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_z;




always @(clk) begin: MODV1_FIND_PACKAGE0_DOWNLOAD_PACKAGE0_LOAD_LOCAL_CLASS0_MOD0_GENERATE_INSTANCE0_MOD1_SELECT_VERSION0_MOD2_MOD_EXAMPLE10_HDL
    find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_z <= (find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_x + find_package0_download_package0_load_local_class0_mod0_generate_instance0_mod1_select_version0_mod2_mod_example10_xyz_y);
end

endmodule

I have made modifications to MyHDL in my fork to achieve this functionality, as I needed it for the development of Evercore. I’d like to know what do you think and if there are alternatives to reach the same. If MyHDL devs agree, I can share a PR with my changes so they can be taken into consideration.


Jose

Finally, I decided to make a pull request on github. Hope this can be accepted at some point so as to allow the development of component integration third party frameworks based on MyHDL.

I found it was a key feature for Evercore that was missing in MyHDL.

@jvillar In the following example, 2 interfaces are created dynamically from a dictionary.
Is this this kind of thing you try to achieve ?

# coding: utf-8

from myhdl import block, Signal, intbv, always, always_seq

interface_elements_1 = {"w" : True,
                        "x" : {"val" : 0, "min" : 0, "max" : 8},
                        "y" : False,
                        "z" : {"val" : 0, "min" : -8, "max" : 7}}

interface_elements_2 = {"w" : False,
                        "x" : {"val" : 5, "min" : 0, "max" : 32},
                        "y" : True,
                        "z" : {"val" : -5, "min" : -16, "max" : 15}}

class my_interface() :
    def __init__(self, elements) :
        for k,v in elements.items() :
            if isinstance(v, bool) :
                setattr(self, k, Signal(v))
            elif isinstance(v, dict) :
                setattr(self, k, Signal(intbv(**v)))

@block
def DUT(i_clock, i_interface, o_interface):

    @always_seq(i_clock.posedge, reset=None)
    def proc():
        o_interface.w.next = i_interface.w
        o_interface.x.next = i_interface.x
        o_interface.y.next = i_interface.y
        o_interface.z.next = i_interface.z
    
    return proc

if __name__ == "__main__" :

    interface_1 = my_interface(interface_elements_1)
    interface_2 = my_interface(interface_elements_2)
    clock = Signal(bool(0))


    print("Elaborate")
    test_inst = DUT(clock, interface_1, interface_2)
    test_inst.convert(hdl='VERILOG', name="test_dyn_interface", path=".", initial_values=True)
    print("Convert")
    #test_inst.convert(hdl='VHDL', name="test_dyn_interface", path=".", initial_values=True)
    print("Done")

Hi DrPi,

It’s a similar feature but there are several side effects in the use of MyHDL interfaces instead of supporting the native Python kwargs. I’ll explain my case. Currently I’m developing a MyHDL based framework called Evercore to make it easier reusing and sharing hardware components. The concept is something similar to software package repositories. The framework should help people to make use of third party components with as little effort as possible. A key point of this framework is that it allows to describe the component interfaces, but not just fixed interfaces. The interface description format also allows to describe highly parameterizable dynamic interfaces depending on the input parameters.

Due to the way that MyHDL converts the code, the conversion is different when a port is part of an interface or it’s declared as a local name in the function’s local scope. My problem is that I have two levels, the framework level, where I intensively make use of **kwargs to pass the interface to deeper levels, and the component level, which is where users will implement the actual hardware components. Here is where I need simple idioms to make it as easier as possible for people to write components.

Sometimes, there are components where there is a set of ports that we know in advance (clk, reset, etc…) but there migh be other ports that depending on other parameters will be present or not. If I put everything in an interface dict, these clk, rst, etc… will be affected in conversion by the interface name, and coding will be a lot less concise and less readable since we will need to refer them by accessing them through the interface. If kwargs were supported, we could refer to these known signals just by their name and the rest of dynamic ports through **ports parameter.
What my framework do is to pack all port signals in a dict and then pass it to the methods that implement hardware using the ** operator. Then, the Python interpreter is who unpacks the dict in the named parameters that the component developer used in the method prototype.

Currently, this PR allows me to support both, named args and kwargs.

I will show an example of how a hardware description using my framework looks like and what do I get:

PyLFSR.py: This is a component description that is packaged and stored in an online public repository

from myhdl import *
from evercore import *

class PyLFSR(MyHDLModule):
   def setup (self, length, polynomial):
        self.set_length(length)
        self.set_polynomial(polynomial)

    def set_length(self, length):
        self.set_parameter("length", length)

    def set_polynomial(self, polynomial):
        length = self.get_parameter("length").get_result()
        for elem in polynomial:
            if elem >= length:
                raise ValueError("Polynomial element " + elem + " is out of bounds (" + length + ")")  
        self.set_parameter("polynomial", polynomial)
    
    @block(keepname=False)
    def instance(self, clk, rst, **ports):
        length = len(ports)
        polynomial = self.get_parameter("polynomial").get_result()
        regs = [inst("PyFF", keephier=self.keep_hierarchy) for i in range(length)] # Array of Flip-Flop componentes/objects
        regs[0].set_parameter("reset_value", 1) # and set initial value to 1 (lfsr seed)
        feedback = Signal(bool(0))
        # at the fist stage
        regs[0].sig_connect('d', feedback) # connect the xor feedback
       
        xor = inst("PyXOR", input_width=len(polynomial), keephier=True)
        xor.sig_connect('out', feedback)
        x_inputs = []
        for i in range(len(polynomial)):
            pos = polynomial[i]
            xor.sig_connect("in%s" % i, ports["out%s" % pos])
        
        for i in range(length): # For every stage
            regs[i].sig_connect('rst', rst)
            regs[i].sig_connect('clk', clk)
            regs[i].sig_connect('q', ports["out%s" % i]) 
            if i < length-1:
                regs[i+1].sig_connect('d', ports["out%s" % i]) #  if this is no the last stage, connect the output with the next input  
                
        modules = module_instances()
        return instances()

test.py: This is what a user should write to make use of the previous component:

from myhdl import *
from evercore import *

def design_top(length, polynomial):    
    lfsr = myhdl_local_inst(PyLFSR, keephier=False)
    lfsr.setup(length=length, polynomial=polynomial)
    lfsr.sig_connect('clk', Signal(bool(0)))
    lfsr.sig_connect('rst', Signal(bool(0)))
    for i in range(length):
        lfsr.sig_connect('out%s' % i, Signal(bool(0)))
        
    project = XSTProject("/home/jose/testproject", lfsr)
    project.clean_project()
    project.render_rtl(language="verilog")

if __name__ == '__main__':
    design_top(length=16, polynomial=[15,13,12,10])

This test generates the following 2 files: the top PyLFSR.v and PyXOR0.v, I’ts split in two files since I’ve implemented a method of selectively keeping hierarchy at Evercore level (the XOR gate keeps hierarchy but flip-flops don’t)

Thanks for your reply and best regards!

PyLFSR.v

// File: hw_inst.v
// Generated by MyHDL 0.11
// Date: Tue Jan 21 13:36:19 2020


`timescale 1ns/10ps

module PyLFSR (
    clk,
    rst,
    out0,
    out1,
    out2,
    out3,
    out4,
    out5,
    out6,
    out7,
    out8,
    out9,
    out10,
    out11,
    out12,
    out13,
    out14,
    out15
);


input clk;
input rst;
output out0;
reg out0;
output out1;
reg out1;
output out2;
reg out2;
output out3;
reg out3;
output out4;
reg out4;
output out5;
reg out5;
output out6;
reg out6;
output out7;
reg out7;
output out8;
reg out8;
output out9;
reg out9;
output out10;
reg out10;
output out11;
reg out11;
output out12;
reg out12;
output out13;
reg out13;
output out14;
reg out14;
output out15;
reg out15;

wire PyLFSR0_0_inst0_feedback;



always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF0_0_INST0_LOGIC
    if ((rst == 1)) begin
        out0 <= 1;
    end
    else begin
        out0 <= PyLFSR0_0_inst0_feedback;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_0_INST0_LOGIC
    if ((rst == 1)) begin
        out1 <= 1'b0;
    end
    else begin
        out1 <= out0;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_1_INST0_LOGIC
    if ((rst == 1)) begin
        out2 <= 1'b0;
    end
    else begin
        out2 <= out1;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_2_INST0_LOGIC
    if ((rst == 1)) begin
        out3 <= 1'b0;
    end
    else begin
        out3 <= out2;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_3_INST0_LOGIC
    if ((rst == 1)) begin
        out4 <= 1'b0;
    end
    else begin
        out4 <= out3;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_4_INST0_LOGIC
    if ((rst == 1)) begin
        out5 <= 1'b0;
    end
    else begin
        out5 <= out4;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_5_INST0_LOGIC
    if ((rst == 1)) begin
        out6 <= 1'b0;
    end
    else begin
        out6 <= out5;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_6_INST0_LOGIC
    if ((rst == 1)) begin
        out7 <= 1'b0;
    end
    else begin
        out7 <= out6;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_7_INST0_LOGIC
    if ((rst == 1)) begin
        out8 <= 1'b0;
    end
    else begin
        out8 <= out7;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_8_INST0_LOGIC
    if ((rst == 1)) begin
        out9 <= 1'b0;
    end
    else begin
        out9 <= out8;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_9_INST0_LOGIC
    if ((rst == 1)) begin
        out10 <= 1'b0;
    end
    else begin
        out10 <= out9;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_10_INST0_LOGIC
    if ((rst == 1)) begin
        out11 <= 1'b0;
    end
    else begin
        out11 <= out10;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_11_INST0_LOGIC
    if ((rst == 1)) begin
        out12 <= 1'b0;
    end
    else begin
        out12 <= out11;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_12_INST0_LOGIC
    if ((rst == 1)) begin
        out13 <= 1'b0;
    end
    else begin
        out13 <= out12;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_13_INST0_LOGIC
    if ((rst == 1)) begin
        out14 <= 1'b0;
    end
    else begin
        out14 <= out13;
    end
end


always @(posedge clk, posedge rst) begin: HW_INST_PYLFSR0_0_INST0_PYFF1_14_INST0_LOGIC
    if ((rst == 1)) begin
        out15 <= 1'b0;
    end
    else begin
        out15 <= out14;
    end
end


PyXOR0 PyXOR0_0 (
    .in0(out15),
    .in1(out13),
    .in2(out12),
    .in3(out10),
    .out(PyLFSR0_0_inst0_feedback)
    );

endmodule

PyXOR0.v

// File: PyXOR0.v
// Generated by MyHDL 0.11
// Date: Tue Jan 21 13:36:19 2020


`timescale 1ns/10ps

module PyXOR0 (
    in0,
    in1,
    in2,
    in3,
    out
);


input in0;
input in1;
input in2;
input in3;
output out;
wire out;




assign out = in0 ^ in1 ^ in2 ^ in3;

endmodule