4. HDL
Primary approach to digital logic design in FPGAs and ASICs. We use it to describe functionality and/or behavior of digital circuits. It is how abstract, design and develop at a higher level.
The most popular are VHDL, Verilog and SystemVerilog. VHDL seems to be the preferred language in Defense industry. Verilog for tech/chip/commerical.. the rest of the world. SystemVerilog also in tech/commercial, and primarily/intially used for verification.
VHDL enforces stricter rules and is typed centric? verbose. wordy, lengthy, less permissive. Strict. libraries..
Verilog is more C like, compact. loosely typed.. Don’t have to do component instantiation.
SystemVerilog, even more compact code. More object oriented like? higher level of abstraction. Helps with verification environment and approaches.
I’m not going to list all the differences under the sun..
I feel like.. HDL should just be purely HDL like basic syntaxs and templates… with out application. then as we talk about the different logic operation or circuits… we’ll provide snippets of the HDL code which will synthesize into these circuits. HDL sprinked across the sections……
The HDL language is expansive? but for FPGA synthesis, you can only use a subset of the language. The language can be used for both development and verification. Alot of the verification syntax/HDL is not synthesizable. We will first focus on synthesizable and return for test bench, validation, verification.
4.1. VHDL
just some of the basic stuff… examples and templates.
4.1.1. libraries and use
1 LIBRARY IEEE;
2 USE IEEE.std_logic_1164.ALL;
3 USE IEEE.numeric_std.ALL;
Warning
- DO USE
IEEE.numeric_std.ALL;
- DO NOT USE
IEEE.std_logic_arith
4.1.2. entity
this is how we abstract digital components and modules. this is how you encapsulate functionality, or create black box.
1 LIBRARY IEEE;
2 USE IEEE.std_logic_1164.ALL;
3 USE IEEE.numeric_std.ALL;
4
5 entity fpga_top is port (
6 clk : in std_logic;
7 rst : in std_logic;
8 somein : in std_logic;
9 anotherin : in std_logic_vector(7 downto 0);
10 someout : out std_logic;
11 anotherout : out std_logic_vector(15 downto 0) -- notice no ';'
12 ); -- notice ';'
13 end fpga_top;
14
15 architecture rtl of fpga_top is
16
17 --signal declarations
18 --component declarations
19
20 begin
21
22 -- RTL body.
23
24 end rtl;
in the following, which can be in some other file, we create more entities. i usually keep one entity per file for organization, but here i am just listing two
1 LIBRARY IEEE;
2 USE IEEE.std_logic_1164.ALL;
3 USE IEEE.numeric_std.ALL;
4
5 entity some_component is port (
6 clk : in std_logic;
7 rst : in std_logic;
8 someout : out std_logic
9 );
10 end some_component;
11
12 architecture rtl of some_component is
13
14 --signal declarations
15 --component declarations
16
17 begin
18
19 -- RTL body.
20
21 end rtl;
22
23 -- just for example...
24 entity another_comp is port (
25 clk : in std_logic;
26 rst : in std_logic;
27 someout : out std_logic
28 );
29 end another_comp;
30
31 architecture rtl of another_comp is
32
33 --signal declarations
34 --component declarations
35
36 begin
37
38 -- RTL body.
39
40 end rtl;
4.1.2.1. architecture vs structure vs behavior
4.1.3. component
you create your component with entity directive? (see entity section)
then you declare its usage, in another entity or testbench.
then you instantiate the component where it is used and label it.
map port signals
re-using the fpga_top entity we created earlier..
1 LIBRARY IEEE;
2 USE IEEE.std_logic_1164.ALL;
3 USE IEEE.numeric_std.ALL;
4
5 entity fpga_top is port (
6 clk100 : in std_logic;
7 clk150 : in std_logic;
8 rst : in std_logic;
9 dout1 : out std_logic;
10 dout2 : out std_logic
11 );
12 end fpga_top;
13
14 architecture rtl of fpga_top is
15 --signal declarations
16
17 -- 2. component declarations -- for code readability, can create a separate component.vhd file and declare them all there.
18 -- actually, i'd recommend doing so, it prevents this section from becoming unnecessarily long
19 component some_component is port (
20 clk : in std_logic;
21 rst : in std_logic;
22 someout : out std_logic
23 );
24 end component some_component;
25
26 component another_comp is port (
27 clk : in std_logic;
28 rst : in std_logic;
29 someout : out std_logic
30 );
31 end component another_comp;
32
33 begin
34 -- 3. component instantiation
35 DUT1_label : some_component port map (
36 clk => clk100,
37 rst => rst,
38 someout => dout1
39 );
40
41 DUT2_label : another_comp port map (
42 clk => clk150,
43 rst => rst,
44 someout => dout2
45 );
46 end rtl;
By instantiating components within other entities, you create a hierarchy and organization for your design.
Note
Notice => used to assign signals to ports. verus <= to assign values or signals to signals!
4.1.4. Object Types
signals
variable
constants
4.1.4.1. Signals
These are the common ones I’ve used.
std_logic
std_logic_vector
unsigned
signed
- integer
range natural 0 to
natural
arrays
custom
4.1.4.2. Assignment/Assigning
<= signal assignment
:= variable assignment, signal initialization.
4.1.4.3. Conversions and Cast/Casting
first, make sure you are using IEEE.numeric_std.ALL
second, the ones to take notice of are the integer related ones. because the integer uses the system bit rep, most likely 32bits. we’ll need to specify how many bits/registers we want to use to represent a numeric value when entering the circuit/hw/bit level.
4.1.4.3.1. TO UNSIGNED
4.1.4.3.1.1. integer to unsigned
1 unsigned_signal <= to_unsigned(integer_signal, desired_unsign_length);
4.1.4.3.1.2. signed to unsigned
4.1.4.3.1.3. std_logic_vector to unsigned
1 unsigned_signal <= unsigned(std_logic_vector_signal);
4.1.4.3.2. TO SIGNED
4.1.4.3.2.1. integer to signed
1 signed_signal <= to_signed(integer_signal, desired_sign_length);
4.1.4.3.2.2. unsigned to signed
keep in mind the needed extra bit for the sign!
1 signed_signal <= signed(signed_signal);
4.1.4.3.2.3. std_logic_vector to signed
1 signed_signal <= signed(std_logic_vector_signal);
4.1.4.3.3. TO STD_LOGIC_VECTOR
4.1.4.3.3.1. integer to std_logic_vector
1 -- only positive integers
2 SLV_signal <= std_logic_vector(to_unsigned(integer_signal, unsigned_length));
3
4 -- positive and negative integers
5 SLV_signal <= std_logic_vector(to_signed(integer_signal, signed_length));
4.1.4.3.3.2. unsigned to std_logic_vector
1 SLV_signal <= std_logic_vector(unsigned_signal);
4.1.4.3.3.3. signed to std_logic_vector
1 SLV_signal <= std_logic_vector(signed_signal);
4.1.4.3.4. TO INTEGER
i dont think i ever really use this.. maybe in verification?
4.1.4.3.4.1. std_logic_vector to integer
but remember whether signed or unsigned, it is just a set of registers, a vector.. signed/unsigned is just how we interpret the vector. we need to tell the system how to interpret our SLV.
1 int_signal <= integer(unsigned(std_logic_vector_signal));
2
3 int_signal <= integer(signed(std_logic_vector_signal));
4.1.4.3.4.2. unsigned to integer
1 int_signal <= to_integer(unsigned_signal);
4.1.4.3.4.3. signed to integer
1 int_signal <= to_integer(signed_signal);
4.1.4.4. resize
Again, make sure you’re using IEEE.numeric_std.ALL Use this to resize “sign extend” your vector/register that holds your signed/unsigned value. This is because the MSB in a signed value needs to be repeated to maintain original value. Use before doing arithmetic.
1 resize (signed; size);
2
3 resize (unsigned; size);
4.1.5. process
You’ll be writing/using this, alot.
1 process (sensitivity list) begin
2
3 -- RTL code
4
5 end process;
The sensitivity list is a list of signals, that we tell the process to monitor and to act upon/ do something when said signals change. basically, execute the process when change in sensitivity signals detected.
a process can be combinational or sequential.
when writing combinational process, you must list all the input signals to logic function. leaving or forgetting signals, results in inferred latches and combinational loops. in generally, neither are desired.
1 LIBRARY IEEE;
2 USE IEEE.std_logic_1164.ALL;
3 USE IEEE.numeric_std.ALL;
4
5 entity another_component is port (
6 clk : in std_logic;
7 rst : in std_logic;
8 someout : out std_logic
9 );
10 end another_component;
11
12 architecture rtl of fpga_top is
13 --signal declarations
14 --component declarations
15 begin
16 --combinational, you must list all the input signals
17 process (sensitivity signals) begin
18 if () then
19 else
20 end if;
21 end process;
22
23 --combinational
24 process (all) begin --VHDL2008, to use all
25 if () then
26 else
27 end if;
28 end process;
29
30 --sequential, sync reset
31 process (clk) begin
32 if () then
33 else
34 end if;
35 end process;
36
37 --sequential - w async reset
38 process (clk, rst) begin
39 if () then
40 else
41 end if;
42 end process;
43
44 end rtl;
4.1.6. if else
very similar to what many see in ‘software’ programming. syntactically similar, but different end result. this is probably redundant, but in programming, code is executed sequentially by the processor. in FPGAs, they result result in encoders and muxes, LUTs. they represent a logic function.
1 -- this is in a process block, with all signals listed or all in VHDL2008
2 -- sequential version
3 process (din1, din2, sel) begin
4 if (sel = '1') then
5 dout <= din1;
6 else
7 dout <= din2;
8 end if;
9 end process;
10
11 -- will result in priority encoding
12 process (all) begin --VHDL2008
13 if (wen) then
14 --some assignment
15 elsif (ren) then
16 --some assignment
17 else
18 --some assignment
19 end if;
20 end process;
Warning
- There is no ternary if-else shorthand, seen in C and verilog
dout <= sel ? din1 : din2;
4.1.7. when else
i’ve never really used this, but have started to see it more often
1 -- concurrent version.
2 -- this doesn't have to be in a process block.
3 dout <= din1 when sel else din2;
4
5 b <= "1000" when a = "00" else
6 "0100" when a = "01" else
7 "0010" when a = "10" else
8 "0001" when a = "11";
4.1.8. with select
i’ve never really used this..
1 with a select b <=
2 "1000" when "00",
3 "0100" when "01",
4 "0010" when "10",
5 "0001" when "11";
4.1.9. case
another similar to software programming, but again end result not the same.
Generally cases is used for and/or will become.. MUX vs. FSM
i dont think you can synth a priority encoder with the VHDL case statement, per the requirement of the syntax. The case ‘sel’ variable/signal and when condition width have to match for comparison.
1 --case used for MUX
2 process (all) begin --VHDL2008
3 case sel is
4 when "00" =>
5 dataout <= datain1;
6 when "01" =>
7 dataout <= datain1;
8 when "10" =>
9 dataout <= datain1;
10 when "11" =>
11 dataout <= datain1;
12 when others =>
13 dataout <= 0;
14 end case;
15 end process;
16
17 -- case used in FSM
18 type state_type is (IDLE, WRITE, WR_WAIT, READ); -- one hot 4 states = 4bits.
19 signal state : state_type;
20
21 process (clk) begin
22 if (rising_edge(clk)) then
23 if (rst) then -- sync reset
24 state <= IDLE;>
25 else
26 case state is
27 when IDLE =>
28
29 -- some output
30 busy <= '0';>
31
32 --next state condition
33 if (some input condition) then -- next state depends on current state and some input condition, maybe write enable.
34 state <= WRITE;
35 end if;
36
37 when WRITE =>
38
39 -- some output
40 data_reg <= data_reg[14 downto 1] & datain;
41 busy <= '1';
42
43 --next state condition
44 if (some input condition) then -- maybe counter value, bits written
45 state <= WR_WAIT;
46 end if;
47
48 when PARITY =>
49 -- some output
50 parity_reg <= datain xor something;
51
52 --next state condition
53 if (some input condition) then -- maybe read en.
54 state <= READ;
55 end if;
56
57 when READ =>
58
59 dataout <= mem[i]; -- depends on current state
60
61 --next state condition
62 if (some condition) then -- maybe counter, bits read.
63 state <= IDLE;
64 end if;
65
66 when others =>
67 state <= IDLE;>
68
69 end case;
70 end if;
71 end if;
72 end process;
4.1.10. generics
Is used to parametrize design. Enable re-use/customization. Often seen for bit width, among other block settings.
1 component some_component is
2 generic (N : integer := 6);
3 port (
4 clk : in std_logic;
5 en : in std_logic;
6 rst : in std_logic;
7 datain : in std_logic_vector(13 downto 0);
8 );
9 end component some_component;
4.1.11. generate
Use/Synthesize block of code if condition is true
1 if (some condition is true) generate
2 --constants
3 begin
4 --some code, processes etc.
5 end generate;
6
7 if (some condition is true) generate
8 --constants
9 begin
10 --some code, processes etc.
11 end generate;
4.1.12. package
for organizing and centralizing re-use constants, records, functions
1 package some_package is
2 --records
3 --constants
4 --function declarations
5 end some_package;
6
7 package body some_package IS
8 -- body
9 --function definition
10 end some_package;
4.1.13. record
Define/create them in the packages. Can be used as a type of signal that is a collection of signals.
1 type eth_packet is
2 record
3 datain : std_logic_vector(127 downto 0);
4 keep : std_logic_vector(15 downto 0);
5 end : std_logic;
6 start : std_logic;
7 valid : std_logic;
8 end record;
4.1.14. for loop
They are not sequentially executed as in software programming. they are used to reduce, simplify your code. you have to think about it from a hardware perspective. the for loop is rolling up? the code, but when synthesized, it is ‘unrolled’/ flattened
1 -- for instance, this is saying we have 8 data_ff
2 -- our datain is also 8 bits wide,
3 -- we want to connect one input bit/signal to the data_ff
4 -- so instead of writing this 8 times,
5 -- we write it once.
6
7 for i in 0 to 7 loop
8 data_ff[i] <= datain[i];
9 end loop;
10
11 for i in 0 to 7 loop
12 DUT[i] <= datain[i];
13 end loop;
4.1.15. arrays
used to store data, can become RAM.
1 --1D array is your std_logic_vector
2
3 --2D array (N depth, by 16 SL) or 1D of 16bit SLVs
4 type mem is array (0 to N) of std_logic_vector(15 downto 0);
5
6 --2D array of SLVs. N width/column, M depth/height/row
7 --3D array because each indice is not a bit, but a vector,
8 type mem is array (0 to N, 0 to M) of std_logic_vector(15 downto 0);
9
10 --for instance in video/imaging
11 --1920x1080 = (height x width) or (row x column) "array or matrix", but each index holds a pixel.
12 --if it is an RGB pixel, then it is 8bit x 8bit x 8bit = 24bit per pixel = std_logic_vector(23 downto 0)
13 -- 8x8x8 = 512 color levels.
4.1.16. operators
1
4.1.17. functions
They are combinational! I never really see this in designs, more in verification/validation.
1 function <function_name> (
2 input parameters : type
3 input parameters : type
4 ) return <return_type> is
5 --constant_or_variable_declaration
6 begin
7 --HDL code here
8
9 return <value>
10 end function;
4.1.18. Template
Putting it all together, template!
1 LIBRARY IEEE;
2 USE IEEE.std_logic_1164.ALL;
3 USE IEEE.numeric_std.ALL;
4
5 entity is port (
6 clk : in std_logic;
7 rst : in std_logic;
8 someout : out std_logic
9 );
10 end fpga_top;
11
12 architecture rtl of fpga_top is
13 --signal declarations
14 --component declarations
15 begin
16 process (sensitivity) begin
17 if () then
18 else
19 end if;
20 end process;
21
22 process (clk) begin
23 if () then
24 else
25 end if;
26 end process;
27 end rtl;
4.2. Verilog
Later..
4.3. SystemVerilog
Later.. as I dont use enough.
4.4. HDL PT. 2
This section is to emphasize HDL on FPGAs or vendor specific (primarily Xilinx bc that is what I use at the moment). It should be revisited after reading about combinatorial and sequential circuits. I will probably discuss some of it there too, so there will be some redundancy in information depending where your entry is.
Or continue if you’re already familiar.
4.4.1. Register/FlipFlops (FF)
There is only D FF in an FPGA.. other styles FF you learn in digital logic class do not exist. If you try implementing other flavors (SR, JK, T), you’ll just use the available DFF and surrounding LUTs to realize their functionality.
Do initialize at the top. Not every register needs a reset condition. Know if register state matters when reset.. means trace how it is used and specifically when.
Code for active high.. avoid active low. ie. make clock enables high. if so and so = ‘1’ then .. blah blah.
4.4.2. Reset
Asynchronously setting or resetting registers are synthesized into preset or clear registers.
avoid async reset on BRAM (block ram) DSPs
Sequential functionality in device resources, such as block RAM components and DSP blocks, can be set or reset synchronously only.
Do not describe flip-flops with both a set and a reset. No flip-flop primitives feature both a set and a reset, whether synchronous or asynchronous. Flip-flop primitives featuring both a set and a reset can adversely affect area and performance.
Avoid operational set/reset logic whenever possible. There can be other, less expensive, ways to achieve the desired effect, such as taking advantage of the circuit global reset by defining an initial content. Always describe the clock enable, set, and reset control inputs of flip-flop primitives as active-High. If they are described as active-Low, the resulting inverter logic penalizes circuit performance.
1 -- sometimes i write like this
2 process(clk)
3 if(rst) then
4
5 ...
6
7 -- sometimes i write like this
8 process(clk)
9 if(rst = '1') then
10
11 ...
4.4.3. Inferring and Instantiation
SRL, F7 F8 F9 mux, CL multiplier and counters using DSP BUFG IO reg (SDR) input DDR reg Select IO
4.4.4. Synthesis/Implementation
I want to focus on HDL, RTL and implementation results. Think hardware.
know the difference between if-else vs. case statement with regards to implementation. if-else becomes priority encoder. whatever is at the top of the if else becomes whatever is closest to the output. or into the register. if the first else case statement is true it is executed and the others dont matter.
if your control register is for instance 4 bits wide. and you only use one of each bit as the control signal. that means they are not mutually exclusive.
mutually exclusive means unique. each if-else or case statement is unique. so it doesn’t matter if you use if-else or case.. because you can create priority or parallel with either one. it depends how the statements are… but for good pratice.. if-else is usually used for priority encoding. case for parallel mux, where decision is mutually exclusive. mutually exclusive means only one decision or branch can be true at any given time. show example of code of everything you’re saying here. explicitly!!
while case statement is generally used for muxes, improper use can create a priority mux. if the conditions of an if-else are mutually exclusive, it will create a true mux. if it is not, it will most likely synthesize a priority encoder. basically in both case it depends how you write the conditions.
4.4.5. Using Dedicated Hardware
Like what it means to use dedicated hardware, inference(ing) vs. LUT.
You’ll want to write code such that it will utilize dedicated hardware when you can such as….
RAM: BRAM vs distributed memory.. , DSP, for adding/subtracting larger vectors, multiplying large vectors, FIR filters SRL, shift registers
Data is written synchronously into the RAM for both types. The primary difference between distributed RAM (made from LUT/FF = LUTRAM) and dedicated block RAM lies in the way data is read from the RAM. See the following table.
Action Distributed RAM Dedicated Block RAM
Write Synchronous Synchronous
Read Asynchronous Synchronous
Generally you will always want to take advantage of RAM, DSP, SRL, MUX? over their LUT equivalents.. better performance. they are tightly stiched already.. “dedicated hardware/circuits” their area or real estate is already in place. if you dont use it you lose it. its already there for you.
Dedicated resource/hardware consumes less power, is faster than LUTs/flip flops and their timing have already been done/taken care of.
4.4.6. Finate State Machine
Vivado synthesis supports specification of Finite State Machine (FSM) in both Moore and Mealy form. An FSM consists of the following:
A state register A next state function An outputs function
Mealy depends on current state and input. Moore depends only on current state. “More is less.”
One hot encoding - use this in most case, tool with recognize.
Gray state encoding - use this when passing value such as pointer counter across clock domains.