It's not quite as elegant as I had hoped, but here's what I've been
doing.
Leveraging the resolved nature of the std_logic (and hence slv,
signed, and unsigned), I'm putting a procedure at the end of my
sequential assignment process for the state machine of the module that
does the address based IO.
Your example does not demonstrate any use of 'the resolved nature of
the std_logic', what you have will work just as well with the
unresolved type std_ulogic. That's OK, I wouldn't recommend using
'the resolved nature of the std_logic' in any code intended to be
synthesized...certainly not for addressable registers.
In each clock cycle the companion
combinatorial process decides the values that will be assigned to the
registers in the procedure update_regs, and then if the external logic
wants to write to a register the value written to that register will
resolve to the external logic's value.
What you've described here (about 'resolve to the external logic's
value') is not what you've shown in your code. Your example code
simply shows addressable registers...and you should ask yourself why
you think you need a separate combinatorial process, the clocked
process you have is all you need...much cleaner.
I tend to use the resolution capabilities of std_logic and derived
types pretty heavily in this way, does anyone have any comment on best
practices in this regard?
1. You're not using the resolution capabilities of std_logic...I'm not
sure why you think you are. I'm guessing that since you have an
'update_regs' (not shown) and it probably updates the same signals as
'register_io' that you think this is using resolved logic. If that's
the case then you're mistaken, the 'register_io' procedure is simply
higher priority than 'update_regs' since it will have the final word.
This is not intended as a criticism of your code, just that your
terminology about using resolved logic is not correct. Resolved logic
has to do with multiple drivers of a signal...and a single process
(even with multiple procedures) can never create multiple drivers of
any signal.
2. If you were making use of the resolution capabilities of std_logic
in your example code and that code was intended to be synthesized,
that would be a mistake. Since you're not, your code looks OK.
Note - it's easy to use signals in the module that are of greater /
smaller length than the data bus, you just need to be careful with
your conversions to signed/unsigned know what you're doing with the
unused bits. If they're greater you have to use more than 1 address,
for instance unused bits + MSb's at addr X and LSb's at addr X + 1.
I tend to define a record that defines all of the bits that are
writable whether they are used to control anything or not like this...
type t_THIS_PORT is record
Reserved: std_ulogic_vector(31 downto 24);
Pointer: std_ulogic_vector(23 downto 0);
end record t_THIS_PORT;
Next I define a pair of functions called 'to_std_ulogic_vector' and
'from_std_ulogic_vector' that work with this record type and do
exactly what the name suggests.
Repeat this for all read/write registers.
Now the register updates (such as your 'sig_a_signed <=
signed(REG_DATA_IN);') become 'sig_a <=
from_std_ulogic_vector(REG_DATA_IN'). By itself, this isn't really
much different than what you have, but it also cleanly takes care of
things like bit widths not matching the external updater as well as
when you have ports with lot of bit fields and you want to change
those definitions around a bit. In my case, all I would change is the
code inside the 'to_std_ulogic_vector' and 'from_std_ulogic_vector'
functions and re-synthesize. In fact, if all that is changing is the
widths/bit locations of existing fields (i.e. not adding/removing any
fields), then all I update is the record definition without touching
the source code for the function at all. No having to hunt through
and places where bit 9 needs to be used rather than bit 8 to control
something.
Kevin Jennings