Matching a signal against a bitvector


#1

In VHDL I sometimes used this way to match a signal against a
pattern specified in a bitvector:

signal mysig : std_logic_vector(7 downto 0);

if mysig = “01-1----” then

I find it much easier to read and understand than masking out the
interesting bits and comparing them against the matching value.

In MyHDL I have written a function match(signal, pattern) which
does the same thing, so I can write

if match(mysig, “01-0----”):

but of course this works only for simuation.

Is it possible to implement a comparison block or function
that does this and can be synthesized?

Thanks,
Thomas


#2

Not obvious that I can think of, but others might have an idea.

I’m curious, what’s the use case for this? I ask because my development flow is almost always to slice out the signals that are of specific interest and operate on those directly.


#3

The use case is to decode commands like these, where it is not really convenient to slice in advance because the nunber of bits that contain the opcode is not the same:

– 0WAAAAA - read/write access using PCS6, 6 address bits.
– 1W00AAAA - read/write access using PCS3, 4 address bits.
– 1W01AAAA- read/write access using serial output
– 1W10AAAA - read/write access using PCS2, 4 address bits.
– 1W11AAAA - read/write access using notPROGRAM, 4 address bits.

W - is the read/write bit, A is the address to read from or write to, and
the remaininng bits form some kind of opcode.


#4

I missed this discussion at the time, but a link appeared today on discourse. (Thanks go to @hgomersall) Actually making this work in MyHDL should not be that difficult. As would conversion to VHDL (I don’t know of Verilog)

if mysig == "01-1----" :
   ...

would easily convert to

if mysig = "01-1----" then
  ...

The simulation would essentially replicate what you do in match(mysig, "01-1----")


#5

Yeah, interesting. We had a chat about this on gitter and it seems there isn’t an obvious solution in myhdl currently.

That said, I often find that solving this sort of problem in the outside scope gives a neater all around solution. So, the meta-programming approach; something like you populate a set of alternative paths according to the instruction using a general block that takes a suitable argument. This is basically impossible in V*, but is nice because it allows you to solve the problem in the Python domain rather than the convertible subset domain. IMO it can be considered as a workaround, but is actually a much more powerful way of working.

As an example, I had a discussion with the migen people about the metaprogramming capabilities of myhdl (which they’re rather disdainful of), and was challenged to write a general round-robin arbiter for arbitrary channels. If you want to solve it like you’d solve it in the V* domain, it’s rather hard (though I have an idea on this), but if you change the way you think about it, it becomes quite neat:

That design pattern is quite common in myhdl (though that’s a rather involved example), and one I use a lot for general purpose and flexible code.


#6
if mysig == "01-1----":

could be made to work (for simulation) by monkeypatching or patching MyHDL’s _Signal.__eq__(self, other) method to call my match()function if a bitvector string is supplied.

In synthesis however, the above code is converted to this VHDL:

    if (mysig = string'("01-1----")) then

I do not know enough VHDL to understand what this code does, but it is probably not what I want.

PS: josyb: whiich link do you mean?


#7

@hgomersall posted a link to this topic in a discussion on https://gitter.im/myhdl/myhdl

The string’() should not be there, so you will have to patch _toVHDL() too. in def visit_Str(self, node): you will have to check whether it is a valid binary string and then leave out the string'(). Note that this may create ambiguity in case we actually want the real string.


#8

@theller
Here is a block that does what you want :

class MatchPattern():
    def __init__(self, pattern="----"):
        self.pattern = pattern
    
        self.hdl = self.hdl1
        
        
    @block
    def hdl1(self, 
             
            i_Clk,
 
            i_DataEn,
            i_Data,
             
            o_Match) :
         
        assert len(i_Data) == len(self.pattern)
         
        data_length = len(i_Data)
        
        xor_init = 0
        and_init = 0
        for i,c in enumerate(reversed(self.pattern)) :
            if c == '1' :
                xor_init |= 1 << i
                and_init |= 1 << i
            elif c == '0' :
                and_init |= 1 << i
            elif c == '-' :
                pass
            else :
                raise ValueError("Value should be '1', '0' or '-'")
        
        xor_vect = Signal(intbv(xor_init)[data_length:])
        xor_vect.read   = True
        xor_vect.driven = True
        
        and_vect = Signal(intbv(and_init)[data_length:])
        and_vect.read   = True
        and_vect.driven = True
         
        @always_seq(i_Clk.posedge, reset=None)
        def compare() :
            if i_DataEn :
                cmp = True
                for i in range(data_length) :
                    if bool((i_Data[i] ^ xor_vect[i]) & and_vect[i]) :
                        cmp = False
                o_Match.next = cmp

        return compare

You can use it this way :

match_inst = MatchPattern("01-10--").hdl(clk, data_en, data, match)

During elaboration, we can do everything we want. Here, we create masking vectors.
xor_vect and and_vect should be constants but we don’t have this in MyHDL for now.

Another version of the compare() function is this one :

        @always_seq(i_Clk.posedge, reset=None)
        def compare() :
            if i_DataEn :
                cmp_vect = intbv(0)[data_length:]
                for i in range(data_length) :
                    cmp_vect[i] = bool((i_Data[i] ^ xor_vect[i]) & and_vect[i])
                
                if cmp_vect == 0 :
                    o_Match.next = True
                else :
                    o_Match.next = False

Here we create a vector which content is compared to 0 to get the result.
I don’t know which version is best. I believe it will depend on the toolchain synthesiser.

This is not an optimised design since all bits are computed while they should not. With the following pattern “01–0--”, only 3 bits need to be computed.

This can be done in the following way :

        @always_seq(i_Clk.posedge, reset=None)
        def compare() :
            if i_DataEn :
                cmp_vect = intbv(0)[nb_bits_to_cmp:]
                cmp_index = 0
                for i in range(data_length) :
                    if and_vect[i] :
                        cmp_vect[cmp_index] = bool(i_Data[i] ^ xor_vect[i])
                        cmp_index += 1
                
                if cmp_vect == 0 :
                    o_Match.next = True
                else :
                    o_Match.next = False

This works in simulation but I think it will not synthesise (I have not tried).


VHDL constant value overflow
#9

Of course! That’s a great idea!
Here is the code that I probably will use (there is no need to create masking vectors; integers will do as well:

@block
def Matcher(data, pattern, match):
    
    xor_mask = and_mask = 0

    for i, c in enumerate(reversed(pattern)) :
        if c == '1':
            xor_mask |= 1 << i
            and_mask |= 1 << i
        elif c == '0':
            and_mask |= 1 << i
        elif c == '-':
            pass
        else :
            raise ValueError("Value should be '1', '0' or '-'")

    @always_comb
    def logic():
        match.next = 0 if (data ^ xor_mask) & and_mask == 0 else 1
        
    return logic

BTW: Why do you use a class for your MatchPattern, instead of a simple decorated function? Is there any advantage that I do not see?


#10

I use classes for all my modules.

  • configuration, if any, is passed in init ()
  • logic is instantiated in method(s) which allows fully configurable module

For example :

class fifo():
     def _init__(self, dual_clk=False) :
        self.dual_clk = dual_clk
        if dual_clk :
            self.hdl = self.hdl1
        else :
            self.hdl = self.hdl2

    @block
    def hdl1(self,
             i_clk1,
             i_wr_en,
             i_wr_data,

             i_clk2,
             i_rd_en,
             o_rd_data,

             o_full,
             o_empty) :

    @block
    def hdl2(self,
             i_clk,

             i_wr_en,
             i_wr_data,

             i_rd_en,
             o_rd_data,

             o_full,
             o_empty) :
        ... 

When configuration changes, module ports also automatically change to fit the requested functionality.

Even when modules don’t need all this, I use to write modules the same way.