Converting enum code (integer) to EnumItem

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

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

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'

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…

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?

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:

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()

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)]

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

Thanks @josyb ! It works! :slightly_smiling_face:

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.