Converting enum code (integer) to EnumItem (original) (raw)

February 19, 2025, 9:54pm 1

Hi guys,

I have been working with MyHDL for a few months and I find it really great. Thanks for bringing joy of hardware programing back :wink:

Today I’ve revisited an issue I gave up in the past and I still can’t convince myself there isn’t a nice way of doing.

I have a protocol implemented in FPGA where I use MyHDL for the full development cycle, simulation, conversion to VHDL and then syntesis to Lattice device. Within this protocol I have a simple ā€œcommandā€ field, for which I used enum so I can see pretty names on simulation. Example:

t_CMD = enum("NONE", "RD", "WR", "ID", "PROBE", "ADDR", encoding="binary")

When a ā€œpacketā€ is received, the integer code for the command is extracted from header with bit slicing like:

header = Signal(intbv(0)[16:])
(...)
cmd.next = CtrlCmds.getCmd(header[7:])

where cmd.next is expect to be like t_CMD.NONE (EnumItem type).

The problem lies in the ā€œgetCmd()ā€ implementation… I can’t think of a good and syntetizable way of geting EnumItem back from integer code that is not an ugly sequence of if’s like:

def getCmd(val):
    if val == CMD_NONE:
        return t_CMD.NONE
    elif val == CMD_RD:
        return t_CMD.RD
    (...)

I have tried a few ideas, like iterating through enum reftype() and then using getattr(t_CMD, name) or preloading all t_CMD values to a list, but they all failed to convert. (although some work on simulation)

Am I missing some clever way of doing this lookup / type conversion?

Thanks!

Miguel

josyb April 14, 2025, 12:05pm 2

Hi Miguel,

Sorry for the late reply, I just didn’t see it - being (too) busy :slight_smile:

After looking in the code: you can use this work around:

>>> from myhdl import enum

>>> e = enum('One', 'Two', 'Three', 'Four')

>>> e
enum('One','Two','Three','Four')

>>> vars(e)
{ '_names': ('One', 'Two', 'Three', 'Four'), '_nrbits': 2, '_nritems': 4, 
 '_codedict': {'One': '00', 'Two': '01', 'Three': '10', 'Four': '11'},
 '_encoding': None, '_name': None, 
 'One': 'One', 'Two': 'Two', 'Three': 'Three', 'Four': 'Four'
}

>>> e._names
('One', 'Two', 'Three', 'Four')

>>> e._names[1]
'Two'

>>> e.Two
'Two'

So e._names[val] will achieve what you are aiming at in def getCmd(val):

NOTE: this will not synthesize, but is fine for MyHDL test-benches

We may perhaps add indexing to enum; so e[val] would do the job?

Best regards,
Josy

josyb April 14, 2025, 12:23pm 3

After looking a bit deeper:
Use e.__dict__[e._names[val]] not e._names[val]

>>> type(e.__dict__[e._names[1]])
<class 'myhdl._enum.enum.<locals>.EnumItem'>

>>> e.__dict__[e._names[1]]
'Two'

mfreitas April 14, 2025, 6:03pm 4

Hi @josyb ! Thanks for replying. What I was trying to achieve specifically is a synthesizable solution. I did manage to implement alternatives that work on test-bench only, but they fail to synthesize.

And the only synthesizable solution I’ve got so far is that ugly sequence of if’s checking every possible value…

josyb April 14, 2025, 6:23pm 5

Hi Miguel,

can you show me a complete, still small, excerpt of your code - so I, being lazy, can copy it it and try a bit more?

josyb April 14, 2025, 7:33pm 6

Hi Miguel,

I made a QAD simple test myself …

'''
Created on 14 apr. 2025

@author: josy
'''

from myhdl import block, Signal, intbv, enum, always_seq, instances, Constant

t_CMD = enum("NOP", "RD", "WR", "ID", "PROBE", "ADDR", encoding="binary")


@block
def tryenum(Clk, D, Q):

    choices = [Constant(t_CMD.__dict__[t_CMD._names[i]]) for i in range(t_CMD._nritems)]

    @always_seq(Clk.posedge, reset=None)
    def synch():
        if D == 0:
            Q.next = t_CMD.NOP
        elif D == 1:
            Q.next = t_CMD.RD
        else:
            Q.next = choices[D]

    return instances()


if __name__ == '__main__':
    Clk = Signal(bool(0))
    D = Signal(intbv(0)[3:])
    Q = Signal(t_CMD.NOP)

    dfc = tryenum(Clk, D, Q)
    dfc.convert(hdl='VHDL')

and this gives this result:

-- File: tryenum.vhd
-- Generated by MyHDL 0.11.51
-- Date:    Mon Apr 14 19:25:06 2025 UTC

package pck_tryenum is

    attribute enum_encoding : string;

    type t_enum_t_CMD_1 is (
        NOP,
        RD,
        WR,
        ID,
        PROBE,
        ADDR
    );

    attribute enum_encoding of t_enum_t_CMD_1 : type is "000 001 010 011 100 101";

end package pck_tryenum;

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

use work.pck_myhdl_011.all;

use work.pck_tryenum.all;

entity tryenum is
    port(
        Clk : in  std_logic;
        D   : in  unsigned(2 downto 0);
        Q   : out t_enum_t_CMD_1
    );
end entity tryenum;

architecture MyHDL of tryenum is

    type t_array_choices is array (0 to 6 - 1) of t_enum_t_CMD_1;
    constant choices : t_array_choices := (
        NOP,
        RD,
        WR,
        ID,
        PROBE,
        ADDR);

begin

    synch : process(Clk) is
    begin
        if rising_edge(Clk) then
            case D is
                when "000" =>
                    Q <= NOP;
                when "001" =>
                    Q <= RD;
                when others =>
                    Q <= choices(to_integer(D));
            end case;
        end if;
    end process synch;

end architecture MyHDL;

Will this work for you?

We will have to think on how to improve on t_CMD.__dict__[t_CMD._names[i]] because that is certainly not beautiful at all :slight_smile:

mfreitas April 15, 2025, 12:17pm 7

Sure! I copied a small part of my code that shows what works (getCmd) and what doesn’t (getCmd2).

from myhdl import *

t_CMD = enum("NONE", "RD", "WR", encoding="binary")

CMD_NONE = 0
CMD_RD = 1
CMD_WR = 2

def getCmd(val):
    if val == CMD_NONE:
        return t_CMD.NONE
    elif val == CMD_RD:
        return t_CMD.RD
    elif val == CMD_WR:
        return t_CMD.WR
    return t_CMD.NONE

def getCmd2(val):
    for item in enumerate(t_CMD.reftype()[1]):
        idx = item[0]
        name = item[1]
        if val == idx:
            return getattr(t_CMD, name)
    return t_CMD.NONE


@block
def spictrl(clk_in=Signal(False),
            din=Signal(False), dout=Signal(False),
            ):
    header = Signal(intbv(0)[16:])
    header_bit_cnt = Signal(intbv(0, min=0, max=16))
    cmd = Signal(t_CMD.NONE)

    @always(clk_in.posedge)
    def logic_rising():
        header.next[16:1] = header[15:]
        header.next[0] = din
        dout.next = False
        if header_bit_cnt != 15:
            header_bit_cnt.next = header_bit_cnt + 1
            if header_bit_cnt == 8:
                cmd.next = getCmd(header[7:])   # works
                #cmd.next = getCmd2(header[7:])  # myhdl.ConversionError: Not supported: method call: 'reftype'
            elif header_bit_cnt == 9:
                if cmd == t_CMD.RD:
                    dout.next = True
        else:
            header_bit_cnt.next = 0

    return instances()


def convert():
    clk_in, din, dout = [Signal(bool(0)) for i in range(3)]

    spictrl_inst = spictrl(clk_in=clk_in, din=din, dout=dout)
    spictrl_inst.convert(hdl='VHDL')
    spictrl_inst.verify_convert()


convert()

mfreitas April 15, 2025, 12:32pm 8

I’ve tried a couple variations based on your suggestion, but couldn’t get any to work:

def getCmd3(val):
    return Constant(t_CMD.__dict__[t_CMD._names[val]])

    Can't infer return type
def getCmd3(val):
    return t_CMD.__dict__[t_CMD._names[val]]

AttributeError: 'dict' object has no attribute '_toVHDL'
def getCmd3(val):
    for i in range(t_CMD._nritems):
        if i == val:
            return Constant(t_CMD.__dict__[t_CMD._names[i]])
    return t_CMD.NONE

    Can't infer return type
def getCmd3(val):
    for i in range(t_CMD._nritems):
        if i == val:
            return t_CMD.__dict__[t_CMD._names[i]]
    return t_CMD.NONE

    Return type mismatch
def getCmd3(val):
    choices = [Constant(t_CMD.__dict__[t_CMD._names[i]]) for i in range(t_CMD._nritems)]
    return choices[val]

    Unsupported list comprehension form: should be [intbv()[n:] for i in range(m)]

josyb April 15, 2025, 5:54pm 9

The best I can do today seems to be:

@block
def spictrl(clk_in=Signal(False),
            din=Signal(False), dout=Signal(False),
            ):
    header = Signal(intbv(0)[16:])
    header_bit_cnt = Signal(intbv(0, min=0, max=16))
    cmd = Signal(t_CMD.NONE)
    choices = [Constant(t_CMD.__dict__[t_CMD._names[i]]) for i in range(t_CMD._nritems)]

    @always_seq(clk_in.posedge, reset=None)
    def logic_rising():
        header.next[16:1] = header[15:]
        header.next[0] = din
        dout.next = False
        if header_bit_cnt != 15:
            header_bit_cnt.next = header_bit_cnt + 1
            if header_bit_cnt == 8:
                cmd.next = choices[header[7:]]  # works

            elif header_bit_cnt == 9:
                if cmd == t_CMD.RD:
                    dout.next = True

        else:
            header_bit_cnt.next = 0

    return instances()

Of course this defeats your desire to make it a function.

Assuming you want to re-use the function in other processes I came up with:

@block
def getCmd4(val, cmd):
    choices = [Constant(t_CMD.__dict__[t_CMD._names[i]]) for i in range(t_CMD._nritems)]

    @always_comb
    def comb():
        if val < 3:
            cmd.next = choices[val]
        else:
            cmd.next = t_CMD.NONE
        # ternary operator fails!
        # because we don't have an overload returning a t_enum_t_CMD
        # otherwise it would have been a nice one-line instead of an additional process
        # cmd.next = choices[val] if val < 3 else t_CMD.NONE

    return instances()


@block
def spictrl(clk_in=Signal(False),
            din=Signal(False), dout=Signal(False),
            ):
    header = Signal(intbv(0)[16:])
    header_bit_cnt = Signal(intbv(0, min=0, max=16))
    ncmd, cmd = [Signal(t_CMD.NONE) for __ in range(2)]

    getcmd = getCmd4(header(7, 0), ncmd)

    @always_seq(clk_in.posedge, reset=None)
    def logic_rising():
        header.next[16:1] = header[15:]
        header.next[0] = din
        dout.next = False
        if header_bit_cnt != 15:
            header_bit_cnt.next = header_bit_cnt + 1
            if header_bit_cnt == 8:
                cmd.next = ncmd

            elif header_bit_cnt == 9:
                if cmd == t_CMD.RD:
                    dout.next = True

        else:
            header_bit_cnt.next = 0

    return instances()

Ideally we would like to see this in the VHDL code:

                if (header_bit_cnt = 8) then
                    cmd <= t_enum_t_CMD'val(to_integer(header(7 - 1 downto 0)));

Perhaps raise an issue in MyHDL Git: Issues so we came back to this later.

Regards,
Josy

mfreitas April 15, 2025, 8:39pm 10

Thanks @josyb ! It works! šŸ™‚

Actually I don’t mind it not being a function. I just wanted to get rid of that ugliness of having to declare and maintain a lot of redundant code.