VHDL: The real code

From UConn PAN
Jump to navigation Jump to search
VHDL Tutorial
Section three of the tutorial, focusing on coding the body of your design.
< prev next >

Now that we've framed the whole thing, you're probably wondering what the actual code is. Now we're going to discuss that. We'll go through how the code works, showing examples from the DAC emulator components, and eventually build up to the full emulator.

A very important warning

The first thing you have to understand is that VHDL is not software. It is a hardware description language. Many people look at VHDL as a software tool to program the hardware and run into all sorts of problems. Remember to think hardware. The main pitfall is demonstrated in this bit of code:

statement_1;
statement_2;
for i = 1 to 5
    statement_3;
statement_4;

A software engineer looks at this and thinks it's quite simple. First to execute is statement_1, followed by statement_2. Then the loop is executed: the first iteration, followed by the second, all the way down to the fifth being the last part of the loop. Finally statement_4 is executed and the program terminates. In software you'd be correct.

Now let's consider how this would run in VHDL. Very simply: everything happens at once. If statement_1 modifies signal X from 7 to 19 and statement_2 reads signal X, what value will statement_2 read? 7. Not 19, because the two statements are simultaneous. The same is true of the loop. Iteration 1 comes no sooner than (and no later than) iteration 5. So you need to keep that in mind: everything is simultaneous, regardless of where and how you type it into the code. Forget that and you may never figure out why your circuit is trying to eat itself.

The basics: combinational logic

Let's start simple. Combinational logic just takes the lines and mixes them together to create a new set of lines. You have the whole range of usual logical operators: AND, OR, NOR, NAND, XOR, XNOR, NOT, and whatever else you can dream up and assemble. To see an example of combinational logic at work, take a look at the code for the 5-to-32 demultiplexer included in the emulator.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity DAC_demux is
    Port ( S : in  STD_LOGIC_VECTOR (4 downto 0);    -- 5-bit select
           D : in  STD_LOGIC;                        -- data to demux
           Q : out  STD_LOGIC_VECTOR (31 downto 0)); -- 32 output lines
end DAC_demux;

architecture DAC_demux_arch of DAC_demux is
begin
	-- combinational logic to implement demux
	Q(0)	<= D and not (S(4)) and not(S(3)) and not(S(2)) and not(S(1)) and not(S(0));
	Q(1)	<= D and not (S(4)) and not(S(3)) and not(S(2)) and not(S(1)) and S(0);
	Q(2)	<= D and not (S(4)) and not(S(3)) and not(S(2)) and S(1) and not(S(0));
	Q(3)	<= D and not (S(4)) and not(S(3)) and not(S(2)) and S(1) and S(0);
	Q(4)	<= D and not (S(4)) and not(S(3)) and S(2) and not(S(1)) and not(S(0));
	Q(5)	<= D and not (S(4)) and not(S(3)) and S(2) and not(S(1)) and S(0);
	Q(6)	<= D and not (S(4)) and not(S(3)) and S(2) and S(1) and not(S(0));
	Q(7)	<= D and not (S(4)) and not(S(3)) and S(2) and S(1) and S(0);
	Q(8)	<= D and not (S(4)) and S(3) and not(S(2)) and not(S(1)) and not(S(0));
	Q(9)	<= D and not (S(4)) and S(3) and not(S(2)) and not(S(1)) and S(0);
	Q(10)	<= D and not (S(4)) and S(3) and not(S(2)) and S(1) and not(S(0));
	Q(11)	<= D and not (S(4)) and S(3) and not(S(2)) and S(1) and S(0);
	Q(12)	<= D and not (S(4)) and S(3) and S(2) and not(S(1)) and not(S(0));
	Q(13)	<= D and not (S(4)) and S(3) and S(2) and not(S(1)) and S(0);
	Q(14)	<= D and not (S(4)) and S(3) and S(2) and S(1) and not(S(0));
	Q(15)	<= D and not (S(4)) and S(3) and S(2) and S(1) and S(0);
	Q(16)	<= D and S(4) and not(S(3)) and not(S(2)) and not(S(1)) and not(S(0));
	Q(17)	<= D and S(4) and not(S(3)) and not(S(2)) and not(S(1)) and S(0);
	Q(18)	<= D and S(4) and not(S(3)) and not(S(2)) and S(1) and not(S(0));
	Q(19)	<= D and S(4) and not(S(3)) and not(S(2)) and S(1) and S(0);
	Q(20)	<= D and S(4) and not(S(3)) and S(2) and not(S(1)) and not(S(0));
	Q(21)	<= D and S(4) and not(S(3)) and S(2) and not(S(1)) and S(0);
	Q(22)	<= D and S(4) and not(S(3)) and S(2) and S(1) and not(S(0));
	Q(23)	<= D and S(4) and not(S(3)) and S(2) and S(1) and S(0);
	Q(24)	<= D and S(4) and S(3) and not(S(2)) and not(S(1)) and not(S(0));
	Q(25)	<= D and S(4) and S(3) and not(S(2)) and not(S(1)) and S(0);
	Q(26)	<= D and S(4) and S(3) and not(S(2)) and S(1) and not(S(0));
	Q(27)	<= D and S(4) and S(3) and not(S(2)) and S(1) and S(0);
	Q(28)	<= D and S(4) and S(3) and S(2) and not(S(1)) and not(S(0));
	Q(29)	<= D and S(4) and S(3) and S(2) and not(S(1)) and S(0);
	Q(30)	<= D and S(4) and S(3) and S(2) and S(1) and not(S(0));
	Q(31)	<= D and S(4) and S(3) and S(2) and S(1) and S(0);
end DAC_demux_arch;

You can see the use statements, the entity declaration, and the framework we previous discussed for the architecture. You see that there are no signals or components declared for the architecture. Then you see a whole list of combinational logic. You can see that the logical operators AND and NOT are used extensively. Also note the indexing for the STD_LOGIC_VECTORs. There is something which appears to be an assignment operator: <=. This is not called an assignment; it is verbalized as "flows to" (or "flows from") to emphasize the hardware nature of VHDL. So take the last line:

Q(31)	<= D and S(4) and S(3) and S(2) and S(1) and S(0);

The most significant bit of the Q bus flows from the logical AND of line D and all lines in bus S. Pretty simple, right? There is an order of operations to VHDL (feel free to run a Google search), but you can enforce your own ordering through the use of parentheses.

Any questions? Hopefully not, because we're moving on.

The next step: sequential logic

Now we're moving into the realm of sequential logic. As you may recall, that means we're adding a clock. This is also where you usually add conditional statements (if, case, etc) and loops (for, etc). So we get to add a new idea: the process. I mentioned processes before, because that's where variables live. Any time you have conditional statements or loops, they must be enclosed within a process. A process tells VHDL that some signal(s) need to be watched and that this block needs to be updated if those signals change. The list of signals to watch is called the "process sensitivity list". Like an architecture, a process has a framework (giving it a name and a sensitivity list), a declaration region, and a code region. Let's look at the code for the shift register in our emulator:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity DAC_shifter is
    Port ( SCLK	    : in  STD_LOGIC; -- clock
           invRESET : in  STD_LOGIC; -- asynchronous, active-low reset
           Enable   : in  STD_LOGIC; -- shift enable
           D_in     : in  STD_LOGIC; -- serial in
           Addr     : out STD_LOGIC_VECTOR (4 downto 0); -- address portion of output (5MSB)
           Code     : out STD_LOGIC_VECTOR (13 downto 0)); -- code portion of output (14LSB)
end DAC_shifter;

architecture DAC_shift_arch of DAC_shifter is
    signal SReg : STD_LOGIC_VECTOR (18 downto 0); -- internal resistor value
    signal delayed : STD_LOGIC;
      -- delay serial input due to delay caused by 19-cycle hold block
begin
    delayer : process (SCLK, D_in, delayed) -- delay serial input
    begin
        if (rising_edge(SCLK)) then
            delayed <= D_in;
        else
            delayed <= delayed;
        end if;
    end process delayer;

    shift_reg : process (SCLK, invRESET, Enable, D_in)
    begin
        if (invRESET = '0') then
            -- asynchronous, active-low reset
            SReg <= "0000000000000000000";
        else
            if (Enable = '1') then
                if (rising_edge(SCLK)) then
                    -- shift up when shift is enabled
                    for i in 0 to 17 loop
                        SReg(i+1) <= SReg(i);
                    end loop;
                    SReg(0) <= delayed;
                else
                    SReg <= SReg;
                end if;
            else
                SReg <= SReg;
            end if;
        end if;
    end process shift_reg;
	
    Addr <= SReg(18 downto 14); -- split parallel output
    Code <= SReg(13 downto 0);  -- split parallel output
end DAC_shift_arch;

So what do we see? Well, a whole lot. So let's start small:

delayer : process (SCLK, D_in, delayed)  -- delay serial input
begin
    if (rising_edge(SCLK)) then
        delayed <= D_in;
    else
        delayed <= delayed;
    end if;
end process delayer;

Okay, now what? Well, this is a process. It is called "delayer". After the name we say it's a process and give the sensitivity list. That means that if SCLK, D_in, or delayed changes, then the synthesizer needs to check this process again to see if anything else changes. We had to put this inside of a process because it makes use of the if-else construction. You can see the format of the if-else:

if (condition) then
    statement_1
else
    statement_2
end if;

You also see a shorthand that has been pre-defined for you: rising_edge. There is also falling_edge. These combine the event command with a specification of which direction the event happened (transition up or transition down). Sequential logic makes extensive use of rising_edge and falling_edge. Also note the else clause. What is the point of having delayed loop back on itself? This is to help the synthesizer along. VHDL has some quirks and doesn't appreciate an if without an else. So even if nothing happens, you always want to put that else clause in there and have all the signals in question loop back on themselves. Otherwise the synthesizer goes crazy and adds latches left and right which at best make your design hideously large and unwieldy and at worst make your design completely nonfunctional.

I should also note some of the format for condition statements. The equality check operator is a single equals sign. To check a line, the value needs to be in single quotes, e.g. X = '1'. To check a bus, the values need to be in double quotes, e.g. Y = "0110".

Now let's take a look at the second process.

shift_reg : process (SCLK, invRESET, Enable, D_in)
begin
    if (invRESET = '0') then
        -- asynchronous, active-low reset
        SReg <= "0000000000000000000";
    else
        if (Enable = '1') then
            if (rising_edge(SCLK)) then
                -- shift up when shift is enabled
                for i in 0 to 17 loop
                    SReg(i+1) <= SReg(i);
                end loop;
                SReg(0) <= delayed;
            else
                SReg <= SReg;
            end if;
        else
            SReg <= SReg;
        end if;
    end if;
end process shift_reg;

This one is a little more complicated. First we see that the invRESET signal takes effect outside of the rising_edge condition. That makes it an asynchronous signal; that is, it takes effect instantly instead of waiting for the next clock cycle to happen. Then inside of the rising_edge condition we see a for loop. The format of such a loop is

for i in a to b loop
    statements
end loop;

Here i is actually a variable (and can be named anything you want it to be named). However it is a read-only variable, so don't bother trying to write to it. The bounds of the loop are set by a and b, which must be static at this point in the program.

So how are you feeling about all of this code? Take a look at the other components and see if you can figure out what they're doing as some practice. The files can be found at:

Putting it together: components

Now let's start to assemble this mess. But first a question: why make everything into components in separate files? Why not just cut and paste the code from each file into the body of the emulator? We most certainly could have done that. There's absolutely no problem with it. However by making components we get several advantages:

  • Clarity of design: We can easily compare the VHDL code and the schematic it generates to the functional block diagram and see what each block does and how it connects. Otherwise we'd have this huge mess of gates and flip-flops all over the place and not the foggiest idea what any of it does or why it's there (or if some of it is extra garbage the synthesizer added because we messed up the code).
  • Reusability: Recall that I said a while back that many components are general purpose sorts of things that you can use over and over again in different designs. Well, if I design each functional block as a separate component then I can just add that component to another design. In fact exactly that happened: the DAC_hold19 block came in handy for the DAC controller. I just had to include the component in that design because it was already built and tested.
  • Multiplicity: Recall the functional block diagram. How many terminal registers where there? Thirty-two. Do you want to type (or cut & paste) the code for a register 32 times? Me neither. I make one component and include it 32 times. And if there's an error, I only have to fix one set of code, not make the same correction 32 times.

Having said that, feel free to ignore me and just put every process into one file; the code will run just the same, the FPGA will be programmed just the same. It's a convenience to the designer, not a requirement.

So how do we add components? There are two steps: the first is to declare the component and the second is to map the component. For this discussion, take a look at the full code of the emulator.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity DAC_emulator is
    Port ( SCLK     : in STD_LOGIC; -- SCLK
           invRESET : in STD_LOGIC; -- /RESET
           invSYNC  : in STD_LOGIC; -- /SYNC
           D_in     : in STD_LOGIC; -- D_in
	   -- 32 14-bit output channels
           ch00 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch01 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch02 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch03 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch04 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch05 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch06 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch07 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch08 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch09 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch10 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch11 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch12 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch13 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch14 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch15 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch16 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch17 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch18 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch19 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch20 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch21 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch22 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch23 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch24 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch25 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch26 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch27 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch28 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch29 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch30 : out  STD_LOGIC_VECTOR (13 downto 0);
           ch31 : out  STD_LOGIC_VECTOR (13 downto 0));
end DAC_emulator;

architecture DAC_emul_arch of DAC_emulator is
    -- declare component wrappers
    component DAC_demux
    Port (	S : in  STD_LOGIC_VECTOR (4 downto 0);
		D : in  STD_LOGIC;
		Q : out  STD_LOGIC_VECTOR (31 downto 0));
    end component;

    component DAC_follow
        Port (	CLK      : in  STD_LOGIC;
		invReset : in  STD_LOGIC;
		D        : in  STD_LOGIC;
		Q        : out STD_LOGIC);
    end component;

    component DAC_hold19
        Port (	CLK      : in  STD_LOGIC;
		invReset : in  STD_LOGIC;
		invBegin : in  STD_LOGIC;
		Go       : out STD_LOGIC);
    end component;
	
    component DAC_shifter
        Port (	SCLK     : in  STD_LOGIC;
		invRESET : in  STD_LOGIC;
		Enable   : in  STD_LOGIC;
		D_in     : in  STD_LOGIC;
		Addr     : out STD_LOGIC_VECTOR (4 downto 0);
		Code     : out STD_LOGIC_VECTOR (13 downto 0));
    end component;
	
    component DAC_register
        Port (	CLK    : in  STD_LOGIC;
		invRST : in  STD_LOGIC;
		Enable : in  STD_LOGIC;
		D      : in  STD_LOGIC_VECTOR (13 downto 0);
		Q      : out STD_LOGIC_VECTOR (13 downto 0));
    end component;
	
    -- declare internal signals to connect components
    signal Enable : STD_LOGIC;
    signal Reg_Enable : STD_LOGIC;
    signal Reg32_Enable : STD_LOGIC_VECTOR (31 downto 0);
    signal Addr : STD_LOGIC_VECTOR (4 downto 0);
    signal Code : STD_LOGIC_VECTOR (13 downto 0);
begin
    -- port maps to create component instances
    u1: DAC_demux   port map (Addr, Reg_Enable, Reg32_Enable);
    u2: DAC_follow  port map (SCLK, invReset, Enable, Reg_Enable);
    u3: DAC_hold19  port map (SCLK, invReset, invSync, Enable);
    u4: DAC_shifter port map (SCLK, invReset, Enable, D_in, Addr, Code);
    u500: DAC_register port map (SCLK, invReset, Reg32_Enable(0),  Code, ch00);
    u501: DAC_register port map (SCLK, invReset, Reg32_Enable(1),  Code, ch01);
    u502: DAC_register port map (SCLK, invReset, Reg32_Enable(2),  Code, ch02);
    u503: DAC_register port map (SCLK, invReset, Reg32_Enable(3),  Code, ch03);
    u504: DAC_register port map (SCLK, invReset, Reg32_Enable(4),  Code, ch04);
    u505: DAC_register port map (SCLK, invReset, Reg32_Enable(5),  Code, ch05);
    u506: DAC_register port map (SCLK, invReset, Reg32_Enable(6),  Code, ch06);
    u507: DAC_register port map (SCLK, invReset, Reg32_Enable(7),  Code, ch07);
    u508: DAC_register port map (SCLK, invReset, Reg32_Enable(8),  Code, ch08);
    u509: DAC_register port map (SCLK, invReset, Reg32_Enable(9),  Code, ch09);
    u510: DAC_register port map (SCLK, invReset, Reg32_Enable(10), Code, ch10);
    u511: DAC_register port map (SCLK, invReset, Reg32_Enable(11), Code, ch11);
    u512: DAC_register port map (SCLK, invReset, Reg32_Enable(12), Code, ch12);
    u513: DAC_register port map (SCLK, invReset, Reg32_Enable(13), Code, ch13);
    u514: DAC_register port map (SCLK, invReset, Reg32_Enable(14), Code, ch14);
    u515: DAC_register port map (SCLK, invReset, Reg32_Enable(15), Code, ch15);
    u516: DAC_register port map (SCLK, invReset, Reg32_Enable(16), Code, ch16);
    u517: DAC_register port map (SCLK, invReset, Reg32_Enable(17), Code, ch17);
    u518: DAC_register port map (SCLK, invReset, Reg32_Enable(18), Code, ch18);
    u519: DAC_register port map (SCLK, invReset, Reg32_Enable(19), Code, ch19);
    u520: DAC_register port map (SCLK, invReset, Reg32_Enable(20), Code, ch20);
    u521: DAC_register port map (SCLK, invReset, Reg32_Enable(21), Code, ch21);
    u522: DAC_register port map (SCLK, invReset, Reg32_Enable(22), Code, ch22);
    u523: DAC_register port map (SCLK, invReset, Reg32_Enable(23), Code, ch23);
    u524: DAC_register port map (SCLK, invReset, Reg32_Enable(24), Code, ch24);
    u525: DAC_register port map (SCLK, invReset, Reg32_Enable(25), Code, ch25);
    u526: DAC_register port map (SCLK, invReset, Reg32_Enable(26), Code, ch26);
    u527: DAC_register port map (SCLK, invReset, Reg32_Enable(27), Code, ch27);
    u528: DAC_register port map (SCLK, invReset, Reg32_Enable(28), Code, ch28);
    u529: DAC_register port map (SCLK, invReset, Reg32_Enable(29), Code, ch29);
    u530: DAC_register port map (SCLK, invReset, Reg32_Enable(30), Code, ch30);
    u531: DAC_register port map (SCLK, invReset, Reg32_Enable(31), Code, ch31);
end DAC_emul_arch;

In the declaration section, near the signals, you can see the component declarations:

component component_name
    ---------------------------------------------------------
    -- copy the port list from the entity declaration here --
    ---------------------------------------------------------
end component;

It's that simple! Just make sure that the component name and the port list match between the component declaration here and the entity declaration in the other VHDL file, and you're all set. Now to map the component:

u531: DAC_register port map (SCLK, invReset, Reg32_Enable(31), Code, ch31);

You give the component an identifier. Customarily engineers use the U numbering system (U followed by a number, starting at 1 and going until you run out of components). But you can give the component map any identifier you see fit. Then you give the component name, the words "port map", then a list of signals or pins. The signal listed first in the port map will be connected to the pin listed first in the component declaration, and so on down the line. And that's all you have to do to include components in your design. The synthesizer will retrieve the appropriate code and make the connections for you.