Instanciate undefined Signal()

Unlike VHDL, myhdl module signals are defined outside modules.

def module1(i_clk, o_data) :
    ...

def module2(i_clk, i_data) :
    ...

def module3() :
    i_clk  = Signal(bool(0))
    data   = Signal(bool(0))
   
    inst1 = module1(i_clk, data)
    inst2 = module2(i_clk, data)

    ...

In the above code, i_clk is declared/instantiated as a boolean in module3 and this is the right place to do this.
The problem is with data. It is also declared/instantiated in module3. The problem is that module3 should not have any knowledge of what module1 want this signal to be.

One consequence is that the default value is not controlled by module1 but by module3. This is not good behaviour.
Another consequence is that module3 must have some knowledge of module1 internals to declare the correct signal. This is not pythonic.

One solution could be to be able to declare a signal as undefined : my_signal = Signal()
The first time my_signal is assigned a value, it’s type is defined.
This allows to create “connector” signals between module without knowledge of their type. Like between module1 and module2 that are instantiated in module3.

Of course, in this very basic example, there is low interest for undefined Signal(). But I have designs where data come in the FPGA and are successively filtered by many modules. The width of the data changes at each module output and is dependent on module parameters and previous module data width.

@DrPi thanks for the input - couple suggestions, I would avoid comments like “this is not good behavior” and “this is not pythonic” unless you are going to invest the time to support these arguments.

For port declaration, couple thoughts I can add at this point. This is how python works, python is dynamic and does not enforce (the compiler) what arguments are passed. The user can do lots of checking as far as type etc. For my perspective, this ends up being tremendously useful in creating modular and scalable blocks. One of the things we have to be careful is that we don’t try and force a VHDL or Verilog approaches on MyHDL.

Things I would think about: start using interfaces, then you can define what the default interface looks like.

Another pattern I use is to attached a portmap to the function but I typically only do this for top-levels when they are fixed definitions.

@myhdl.block
def myblock(p1, p2, p3):
    # my block implementation stuff ...
    return myhdl.instances()

myblock.portmap = {
    p1 = Signal(bool(0)),
    p2 = SIgnal(intbv(0)[8:0])
    p3 = Signal(bool(1)),
}   

inst = myblock(**myblock.portmap)

# or only get the default for a subset 
p2 = myblock.portmap['p2']

In your case when you say the input and output types are all based on parameters, simple functions could be used to get ports.

import mymodule1
import mymodule2

@myhld.block 
def mytop():
    data1_i, data1_o = mymodule1.get_data_types(parameters)
    data2_i, data2_o = mymodule2.get_data_types(parameters) 

    inst1 = mymodule1.myblock(data1_i, data2_o, parameters)
    inst2 = mymodule2.myblock(data2_i, data2_o, parameters)

   return inst1, inst2

The above can be made more generic and modular / parameterizable …

For this type of stuff, python provides all kinds of tools (maybe too much) to help manage this type of complexity. This is very common for folks learning myhdl, forcing VHDL/Verilog design approach with Python sprinkles.

@cfelton I do not try to use myhdl as VHDL. I just make a proposition to make myhdl even better.
You can’t consider myhdl is a pythonic way of programming from a purist point of view. If it was the case, then outputs signals would be output with the “return” statement. Then comes the problem of i/o signals. So, obviously, there are trade-off.
I am happy with the myhdl way of programming.
Back to my proposition.
Interfaces are great but do not cover all situations and are a “heavy” solution. Further more, they do not solve the problem. Neither portmap.

Please, consider the following example :

def module1(i_clk, 
            i_data, 
            i_mult, 
            o_data, 
            DIVISOR_NB_BITS=7) :
    data = Signal(intbv(0)[len(i_data)+len(i_mult)-DIVISOR_NB_BITS]:0)
    ...
    @always_comb
    def logic() :
        o_data.next = data

    return ..., logic

def main_module(i_clk,
                i_data,
                i_mult1,
                i_mult2,
                o_data,
                divisor1,
                divisor2) :
    data = Signal(intbv(0)[?:])

    inst1 = module1(i_clk, i_data, i_mult1, data, divisor1)
    inst2 = module1(i_clk, data, i_mult2, o_data, divisor2)

    return inst1, inst2

How do you find data size ?

Ok, I already manage this in VHDL, but this is a real nightmare. It is not much better with myhdl.

What why, based on what? Signals to a module block are not the same as arguments to a function.

In some cases, it makes sense that the signals that are driven from a block are derived from a set of parameters that a block also uses. In other cases defining the outputs is the parameters to the block, e.g. I want a fifo with 8 bits in and 9 bits out, I only need to set the ports and not redundantly parameters. I don’t agree that the signals driven by a block (i.e. output ports) are always defined by the block. In some cases it is useful but there are other options to handle these cases.

This is probably a topic that was discussed in the early days of myhdl, searching the old mailing-list my give you some background on the subject.

I will comment on the “how do you find data size” when I have a little more time.

I will not go further this since every one has a different point of view on this and this is not the goal of the topic.

I totally agree with you. This is the way myhdl currently works.
Why not enhance myhdl to also support the opposite ?

Absolutely! that is why I suggested not to make statements like “not pythonic” etc. because that gets into a deep philosophical debates that can be avoided.

The question is, what is the best way to support output port types that are derived from details in a block. One possibility is to add a method to the Signal class to allow redefinition of the signal type: Signal.redefine(val) or something (not sure what the best name would be). That would allow a block to redefine the type that the Signal is carrying. In that case you could use y = Signal(None) and then in the block y.redefine(intbv(0, min=new_min, max=new_max)).

.chris

@DrPi, @cfelton
I don’t think you will get a Signal.redefine(val) past our BDFL :slight_smile:
I currently use the approach I described here: https://gist.github.com/josyb/afd84c9a06fdec77f2fd

Regards,
Josy

I don’t like the name redefine but this has been brought up before, when we moved to block it removed the ability to return “ports” which was being used by some users. To have the ability for a block to fill in the type of a signal seems reasonable, I would guess it has a 50-50 shot of be shot down (accepted) :slight_smile:

Yes and yes, I agree there are other ways to handle this situation and approaches / patterns that would be preferred IMO but on the same token I do see limited use of the empty signal as the OP mentioned. An enhancement like this would need someone to champion it and write the MEP etc., @jos.huisken I believe was the user that used the return ports before and might be interested in a feature like this.

@cfelton I agree there are other ways to manage the problem. Using classes is one way. I still think a native way to manage the problem would be better.
@josyb Your Register class is very smart. I would have not thought this kind of use could work.