Electronic – How to specify “don’t care” signals in VHDL

optimizationsynthesisvhdl

In Logic Design courses we all learned that it is possible to minimize a logic function, for example by using a Karnaugh map or the Quine–McCluskey algorithm. We also learned that "Don't Care" values increase the minimization potential.

For example take a register file. The write_address and write_data signals don't really matter when the write_enable signal is '0'. Thus, they should be assigned a "Don't Care" value to allow more optimizations in the logic that is driving these signals (i.e. not in the register file itself).

What is the correct way to specify such "Don't Care" values in VHDL in order to allow the synthesis tool more room for possible optimizations?


So far I've found the following things which might be suitable. But I'm not really sure what the pros and cons of each approach are:

  • Simply not assigning the signal. This seems like it could work. However I found that it doesn't work when you want to define a "do nothing constant" of some record type, since record constants need to be fully specified (at least Modelsim tells me so).
  • The std_logic_1164 package defines the value '-' -- Don't care for std_ulogic. This looks like it is the semantically correct choice for an explicit "don't care", but I've never seen it used anywhere (except in the unrelated VHDL-2008 case? constructs).
  • Modelsim uses the value 'X' to display undefined signals. However I'm not sure if synthesis tools understand an explicit 'X'-assignment as "don't care".

Here's an oversimplified code snippet for clarification, where I've initialized the don't care signals with '-'.

As you can see, the signal control.reg_write_address can have 3 different values: "----", instruction(11 downto 8); and instruction(3 downto 0);. Now I'd expect this to be synthesized to a 2-input multiplexer if '-' is interpreted as "don't care". Had I initialized the signal with (others => '0') instead of '-', the tool would have to generate a 3-input multiplexer instead.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package mytypes is
    type control_signals_t is record
        write_enable  : std_logic;
        write_address : std_ulogic_vector(3 downto 0);
        read_address  : std_ulogic_vector(3 downto 0);
    end record;

    -- All members of this constant must be fully specified.
    -- So it's not possible to simply not assign a value.
    constant CONTROL_NOP : control_signals_t := (
        write_enable  => '0',
        write_address => (others => '-'),
        read_address  => (others => '-')
    );
end package;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
library cfx;
use cfx.mytypes.all;

entity control_unit is
    port(
        instruction : in  std_ulogic_vector(15 downto 0);
        write_data  : out std_ulogic_vector(15 downto 0);
        ctrl        : out control_signals_t
    );
end entity;

architecture rtl of control_unit is
begin
    decode_instruction : process(instruction) is
    begin
        -- Set sensible default values that do nothing.
        -- Especially all "write_enable" signals should be '0'.
        -- Everything else is mostly irrelevant (don't care).
        ctrl       <= CONTROL_NOP;
        write_data <= (others => '-');

        if instruction(15 downto 12) = "1100" then
            -- Load 8 bit of data into the register file
            ctrl.write_enable  <= '1';
            write_data         <= std_ulogic_vector(resize(signed(instruction(7 downto 0)), 16));
            ctrl.write_address <= instruction(11 downto 8);
        elsif instruction(15 downto 8) = "11111001" then
            -- Load 4 bit of data into the register file
            write_data         <= std_ulogic_vector(resize(signed(instruction(7 downto 4)), 16));
            ctrl.write_address <= instruction(3 downto 0);
        elsif instruction(15 downto 8) = "10110101" then
            -- Read from the register file. Don't use the write signals at all.
            ctrl.read_address <= instruction(3 downto 0);
        end if;
    end process;
end architecture;

Best Answer

I will leave it to an LRM expert to provide a more detailed answer, but in short, your approach should be valid - I ran a quick test with a recent version of Quartus, and it handles '-' like it's supposed to - the logic generated is reduced as expected when the output is defaulted to '-' ('X' works too, by the way). More on the approaches you listed:

  • Not assigning the signal isn't really an option for your example, of course, if you don't want latches. If it's a clocked process, you're slightly better off, but you'll still get enables where you might not need them. Maybe I'm missing your intent here.

  • '-', as previously noted, is probably the best option, for both semantic and practical reasons.

  • Depends on what you mean by "undefined". 'X' is technically "unknown". 'U' is for uninitialized signals, which ModelSim displays as "X" for hex representations. 'X' does seem to work, though, as I noted above.

Another alternative would be to do the optimization yourself and remove one case from being tested explicitly:

if instruction(15 downto 8) = "11111001" then
  write_data <= std_ulogic_vector(resize(signed(instruction(7 downto 4)), 16));
else
  write_data <= std_ulogic_vector(resize(signed(instruction(7 downto 0)), 16));
end if;

This has significant disadvantages (mostly related to code clarity), though, and I would probably opt for a more ideal solution.

Incidentally, '-' is also commonly used with std_match(), which I would consider using for your decoding, e.g.:

if std_match(instruction(15 downto 8), "1100----") then

Though at that point, you're probably better off just using case?.

Related Topic