The Universal MUX Building Block


The next example in the series of generic building blocks is a multiplexer. This is a combinatorial block - if we need pipelining we can always add that separately to keep it as generic as possible - with an input port I of N elements, an UNSIGNED SEL port and an output port O, which is one of the N elements of the input port I selected by SEL. We want of course the I and SEL ports to be unconstrained arrays. The most generic solution would be one with a generic base type T also passed as an argument to the MUX module instantiation like we have seen in the generic DELAY post but this is VHDL-2008 only and is not supported by Vivado synthesis yet. The next best thing is a different generic MUX module for every different base type, one for STD_LOGIC, one for BOOLEAN, one for STD_LOGIC_VECTOR, SIGNED, UNSIGNED, SFIXED, CFIXED and so on.

Since all the vector types mentioned are actually arrays of STD_LOGIC we could create first a generic STD_LOGIC MUX module and then build all the other ones as parallel instantiations of STD_LOGIC MUXes with a for generate. So let's start with a generic STD_LOGIC MUX:


library IEEE; 
use IEEE.STD_LOGIC_1164.all;

entity MUX is port(I:in STD_LOGIC_VECTOR; – unconstrained vector of STD_LOGIC
       SEL:in UNSIGNED;       – unconstrained size selection port
       O:out STD_LOGIC);

TEST of MUX is
assert (2**SEL'length<=I'length) and (I'length<2**(SEL'length+1)) report "Ports I and SEL have inconsistent sizes!" severity warning; – severity level can be note, warning or error
  O<=I(TO_INTEGER(SEL)+I'low); – I is not necessarily 0 based so we add I'low to SEL
end TEST;


Since the I and SEL input ports are unconstrained, it is a good coding practice to add assert statement(s) to check for port size consistency. The assert is ignored during synthesis, at most you might get a message in the synthesis log, but during functional simulation you will definitely get a message and depending on the severity level and simulator you can even stop the simulation and inspect the design to see why things are not as expected.


The other notable thing is that the MUX itself is just one line of behavioral VHDL code, all we need to do is convert the UNSIGNED SEL to INTEGER, add I'low in case the actual signal connected to the I port is not zero based and then select the I element to assign to O. Now we can build any type of multiplexer by instantiating one MUX for every STD_LOGIC bit of the O output. As an example here is how an SFIXED multiplexer called SMUX would look like:


library IEEE; 
use IEEE.STD_LOGIC_1164.all;

use work.TYPES_PKG.all; – we need this packge we introduced earlier for SFIXED type support 

entity SMUX is port(I:in SFIXED_VECTOR; – unconstrained vector of SFIXED, itself an unconstrained vector of STD_LOGIC
       SEL:in UNSIGNED;    – unconstrained size selection port
       O:out SFIXED);
end SMUX; 

architecture TEST of SMUX is
  lk:for K in 0 to O'length-1 generate
signal II:STD_LOGIC_VECTOR(I(K)'length-1 downto 0);
       lj:for J in II'range generate
            II(J)<=I(K+I'low)(J+I(K)'low); – I might not be zero based and I(K) might not be zero based either
end generate;
entity work.MUX port map(I=>II,  – we need the intermediate II signal to convert from SFIXED to STD_LOGIC_VECTOR
                                   SEL=>SEL, O=>O(K+O'low)); – O might not be zero based
end generate;
end TEST;


The intermediate II signal has two roles, to convert from SFIXED to STD_LOGIC and to avoid problems when I(K) has negative range since STD_LOGIC_VECTOR is restricted to positive ranges only. Muxes for other types like STD_LOGIC_VECTOR, SIGNED, UNSIGNED, CFIXED and so on are very similar and all are based on the same STD_LOGIC MUX module instantiated many times.


The question we need to ask now is if all this two-level hierarchical decomposition of a generic multiplexer is really required, beyond its educational aspects? After all, we can index an input port or internal signal I which is an array of virtually any user-defined base type in just one line of code, by indexing the I array using TO_INTEGER(SEL) directly. The answer to this question is yes and no.


If the synthesis tool gives you an optimal implementation of such a multiplexer inferred from one line of behavioral code then that is ideal - one line of code is simple to understand and maintain, faster to simulate and all the two-level hierarchy of generic mixes is unnecessarily complicated. But if the synthesis result is not optimal, for example uses too many LUT6es or has too many logic levels and cannot meet the desired clock speed then the coding style I have just introduced can become a life saver. But before even asking if the behavioral synthesis result is optimal or not, we need to be able to set up expectations and be able to answer questions like "how many LUT6es and logic levels are required to implement a 1024-input mux (or any other size)". Answering this question properly requires an understanding of the configurable logic block (CLB) slice, in particular a little know resource called FxMUXes.


This will make the object of my next weekly post.


Back to the top: The Art of FPGA Design