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
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
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
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.