VHDL: The real code

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:
 * DAC files
 * Temperature sensor files
 * ADC files

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

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.