Digital Sistem Design (PSD/DSG)


Module 1 - Setup

For Digital Sistem Design (DSG) or Perancangan Sistem Digital (PSD) Practicum, students must have already installed between Vivado only or ModelSim + Quartus Prime

Module 1 - Setup

Vivado Installation Tutorial

1.1 Vivado Explanation

Vivado is an Integrated Design Environment (IDE) developed by Xilinx (now AMD) used for designing, simulating, and implementing digital circuits on FPGAs (Field-Programmable Gate Arrays). It serves as the primary software tool to take a VHDL hardware description and turn it into a functional circuit on a physical chip. Vivado itself is a complete, integrated workshop for Xilinx FPGAs. It has all the tools you need (design, simulation, implementation) under one roof. 

Before choosing Vivado be cautious that Vivado requires atleast 60gb of storage and a demanding CPU performance.

1.2 Vivado Installation

To install Vivado, please follow this link and proceed to do the next procedure until finished. 

Step 1 : Download the Vivado Installer

Go into Vivado Archive and choose version 2022.2.

image.png

Then Scroll and choose Windows version and then click the link

image.png

image.png

The page will take you into the download center where you will fill out your information, you may just fill up only the required part to download the installer. After filling all your information, you can just download the installer on the bottom of the page.

image.png

Step 2 : Install the Vivado 

Run the installation file

image.png

Proceed to log into the same account you've just logged into/created before in step 1 and then just choose Download and install now and proceed into the next step

image.png

Choose Vivado and then proceed

image.png

Choose Vivado ML Standard and proceed

image.png

Just Check every box like in the screenshot below and make sure to have enough disk space required on the bottom left and then ou may proceed.

image.png

Just check all the agree box and you may proceed

image.png

Choose where you want Vivado to be installed and then you may proceed to the installation part

image.png

Wait until the installation is completed and then you may check in xilinx information center if the Vivado installation had completed. Make sure to check if the Version you've installed is correct.

image.png

image.png

You may open Vivado and voila you've installed Vivado

image.png

image.png

Module 1 - Setup

Vivado Simulation and Synthesis Tutorial

1.3 Vivado Tutorial

For this tutorial, we will use this code for reference : 

LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;

ENTITY AND_GATE IS
    PORT (
        A : IN  STD_LOGIC; 
        B : IN  STD_LOGIC; 
        Y : OUT STD_LOGIC  
    );
END AND_GATE;

ARCHITECTURE Behavioral OF AND_GATE IS
BEGIN
    Y <= A AND B;
END Behavioral;

1.3.1 Creating a new Vivado file

Create a new project

image.png

Enter your project name and where you want it to be saved

image.png

Choose RTL Project

image.png

Change the target language into VHDL and add your VHDL code into the project

qHiimage.png

image.png

image.png

image.png

You may skip the add constraints page and also the default part proceed into the project creation. Finally you've created a new project and this will be your screen now.

image.png

1.3.2 Simulation Tutorial

Click "Run Simulation" on the left part of the screen. And choose "Run Behavorial Simulation"

image.png

If there's any error warning, you may read and fix the error before proceeding into the simulation. 

This will be your screen after you run the simulation. 

image.png

To add a signal, you may change the value in the objects part, choose "Force Constant" and change according to what you want to do. Remember to change the INPUT not the OUTPUT

image.png

image.png

image.png

After changing the Value you may click the "Run for 10ns" on the top bar

image.png

You may see that there's a new signal after you press the button 

image.png

You may also move the yellow line with your cursor to switch to a different period of time on the waveform

image.png

NOTE : All of this is just a manual simulation tutorial. There are a way to do this automatically (Hint: Module 4).

To close simulation, you may click the top right button

image.png

1.3.3 Synthesis Tutorial

Go to the "RTL Analysis" and run "Schematic" and if there's a notification just select "ok"

image.png

Wait until the elaborated design is finished and then you may see your VHDL code schematic. 

image.png



Module 1 - Setup

Quartus Prime Installation Tutorial

1.1 Quartus Prime Explanation

Intel Quartus Prime is a comprehensive software suite from Intel used for designing, synthesizing, and programming programmable logic devices (PLDs), such as Field-Programmable Gate Arrays (FPGAs) and Complex Programmable Logic Devices (CPLDs). The software provides a complete integrated development environment (IDE) for digital circuit engineers and designers. 

1.2 Quartus Prime Installation

To install Quartus Prime, please follow this link and proceed to do the next procedure until finished. 

Quartus Prime Download Link

Step 1 : Download the Quartus Prime Installer

Go into Multiple Download and then download the Intel Quartus Prime installer : 

image.png

Step 2 : Install the Quartus Prime

Extract the zip and then Run the installation file setup.

image.png

If the installer ask you to accept the agreement just accept and proceed with the next part. And then choose the directory for where you want the Quartus Prime to be installed. 

NOTE : Do not use space in the folder naming.

image.png

Check every components and proceed with the installation. 

image.png

Wait until the installation is finished and then voila you've installed Quartus Prime. To run it just choose to run the Quartus Prime software.

image.png

Module 1 - Setup

Quartus Prime Synthesis Tutorial

1.3 Quartus Prime Tutorial

For this tutorial, we will use this code for reference : 

LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;

ENTITY AND_GATE IS
    PORT (
        A : IN  STD_LOGIC; 
        B : IN  STD_LOGIC; 
        Y : OUT STD_LOGIC  
    );
END AND_GATE;

ARCHITECTURE Behavioral OF AND_GATE IS
BEGIN
    Y <= A AND B;
END Behavioral;

1.3.1 Creating a New Quartus Prime Project

Create a new project by clicking new on file tab 

image.png

Select New Quartus Prime Project 

image.png

Select the directory where you want the project to be saved and also give the project a name and name your Top-Level Entity

NOTE : Remember to name your Top-Level Entity into the same name as your Top-Level entity on your .vhdl

image.png

Choose empty project 

image.png

Add your .vhdl/.vhd file into the project 

image.png

Just select the default setting and proceed to finish

image.png

1.3.2 Synthesis Tutorial

Run the "Start Compilation" button on top bar 

image.png

Wait until the startup is finish and then go into Tools -> Netlist Viewers -> RTL Viewer

image.png

You may see your VHDL schematic

image.png

Module 1 - Setup

ModelSim Installation Tutorial

Module 1 - Setup

ModelSim Simulation Tutorial

Module 2 - Dataflow Style

Module 2 - Dataflow Style

1. Introduction to VHDL

1.1 What is VHDL

VHDL is an acronym for VHSIC HDL or, more completely, Very High-Speed Integrated Circuit Hardware Description Language. VHDL is a language used to describe hardware, so its writing style cannot be equated with high/low-level programming languages. A VHDL model or program can be translated into an actual digital circuit quickly with the help of software and, of course, according to specific needs; this process is known as synthesis. The resulting circuit can also be tested using VHDL to ensure it works according to the user's requirements. Before proceeding to the main material, readers are reminded to understand the concepts of Basic Digital Circuits (DSD) for ease in describing and designing hardware.

1.2 VHDL Syntax

Because VHDL is a language used to describe hardware, in addition to the correct output, tidy program writing is also needed to make it easier to understand. The following are VHDL writing conventions that need to be observed:

1.3 VHDL Operator

Operators in VHDL are grouped into 7 types: logical, relational, shift, adding, sign, multiplying, and others. The order of this list also describes the precedence of the operators. The following is a complete description of these operators:

Operator Type





Logical and or nand nor xor xnor
Operator Name Explanation
A = B equivalence is A equivalent to B?
A /= B non-equivalence is A not equivalent to B?
A < B less than is A less than B?
A <= B less than or equal is A less than or equal to B?
A > B greater than is A greater than B?
A >= B greater than or equal is A greater than or equal to B?
  Operator Name Example Result
logical sll shift left logical result <= "10010101" sll 2 "01010100"
  srl shift right logical result <= "10010101" srl 3 "00010010"
arithmetic sla shift left arithmetic result <= "10010101" sla 3 "10101111"
  sra shift right arithmetic result <= "10010101" sra 2 "11100101"
rotate rol rotate left result <= "10100011" rol 2 "10001110"
  ror rotate right result <= "10100011" ror 2 "11101000"
  Operator Name Comment
adding + addition  
  - subtraction  
  & concatenation
can operate only on specific types
sign + identity unary operator
  - negation unary operator
multiplying * multiplication  
  / division
often limited to powers of two
  mod modulus
can operate only on specific types
  rem remainder
can operate only on specific types
miscellaneous ** exponentiation
often limited to powers of two
  abs absolute value  

1.4 Design Units

There are two important parts when designing a digital circuit using VHDL: "entity" and "architecture." These two units form a hierarchical design consisting of a black box and the components within it.

image.png

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

-- The ENTITY describes the "black box" interface.
-- It defines the input and output ports.
entity Logic_Circuit is
    port (
        A, B, C : in  STD_LOGIC;  -- The three input pins
        Y       : out STD_LOGIC   -- The single output pin
    );
end Logic_Circuit;

 

image.png

-- The ARCHITECTURE describes the internal logic.
-- It explains HOW the inputs are used to create the output.
architecture Dataflow of Logic_Circuit is
begin
    -- This single line of code represents the AND gate followed by the OR gate.
    Y <= (A and B) or C;
end Dataflow;

1.5 Data Objects

In VHDL, there are 4 types of Data Objects: signals, variables, constants, and files. The way to declare a data object for all types is more or less the same, as follows:

VHDL data object Declaration form
Signal signal sig_name : sig_type:=initial_value;
Variable variable var_name : var_type:=initial_value;
Constant constant const_name : const_type:=initial_value;

1.5.1 VHDL Signals

A signal in VHDL is the primary way to model a physical wire or connection in a hardware design. Declared within an architecture, its main purpose is to communicate data between different concurrent components, such as processes or logic gates. A signal is assigned a value using the <= operator, and this assignment is not immediate. it is scheduled to occur at a specific future time, which accurately reflects the signal propagation delay found in real-world circuits. This delayed behavior is essential for correctly modeling how different parts of a hardware design interact with each other. Signals is also only Declared in the declarative part of an architecture or package. There are two ways to declare signals :

Port Signal

A port signal is declared in the entity section of a VHDL design and represents an external connection. Its purpose is to define how the module sends and receives data from the outside world. Think of ports as the plugs and sockets on an appliance—they are the only way to interact with what's inside. You must specify a direction (or mode) for each port, such as in, out, inout, or buffer.

-- Port signals are declared inside the ENTITY
entity D_Flip_Flop is
    port (
        d, clk, rst : in  STD_LOGIC;  -- Input ports
        q           : out STD_LOGIC   -- Output port
    );
end D_Flip_Flop;

architecture Behavioral of D_Flip_Flop is
-- ... logic using the ports ...
end Behavioral;
Intermediate Signal

An intermediate signal is declared in the declarative part of the architecture and acts as an internal wire within your design. It is not visible from outside the module. Intermediate signals are essential for connecting different internal processes or concurrent statements, breaking down complex logic into simpler steps, or holding a value that needs to be used in multiple places within the architecture.

entity Complex_Gate is
    port (
        a, b, c, d : in  STD_LOGIC;
        y          : out STD_LOGIC
    );
end Complex_Gate;

-- Intermediate signal is declared inside the ARCHITECTURE
architecture Dataflow of Complex_Gate is
    -- This signal is an internal "wire" to hold a temporary result
    signal and_result_1 : STD_LOGIC;
begin
    -- The intermediate signal connects the output of the first AND gate
    -- to the input of the OR gate.
    and_result_1 <= a and b;
    y <= and_result_1 or (c and d);
end Dataflow;

1.5.2 VHDL Variables

A variable acts as temporary, local storage for calculations and does not represent a physical wire. It can only be declared and used inside a sequential block, such as a process (this will be explained more in module 3 : Behavioral Style) . The key distinction of a variable is its immediate update behavior. when a value is assigned using the := operator, the variable changes instantly and can be used in the very next line of code with its new value. This makes variables ideal for complex, multi-step algorithms where you need to store intermediate results without the delay and hardware overhead associated with a signal. Variables is also only Declared only inside a process, function, or procedure. It cannot be used to connect different processes. 

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

-- Entity defines the inputs and outputs
entity Bit_Counter is
    port (
        data_in  : in  STD_LOGIC_VECTOR(7 downto 0); -- 8-bit input bus
        count_out: out INTEGER range 0 to 8          -- The final count
    );
end Bit_Counter;

-- Architecture shows the internal logic
architecture Behavioral of Bit_Counter is
begin
    -- A process is needed to use a variable
    count_process: process(data_in)
        -- 1. The variable is declared here, inside the process.
        -- It is initialized to 0 every time the process runs.
        variable bit_count : INTEGER := 0;
    begin
        -- Loop through each bit of the input signal
        for i in data_in'range loop
            if data_in(i) = '1' then
                -- 2. The variable is updated IMMEDIATELY.
                bit_count := bit_count + 1;
            end if;
        end loop;
        
        -- 3. The final result is assigned to the output signal.
        count_out <= bit_count;
        
    end process count_process;
end Behavioral;

1.5.3 VHDL Constant

A constant in VHDL is a data object that holds a fixed value which cannot be changed after it is declared using the constant NAME : TYPE := VALUE; syntax. Its primary purpose is to improve code readability and maintainability by assigning a descriptive name to a value, like using DATA_WIDTH instead of the number 8. This practice makes a design much easier to update, as changing the constant's value in its single declaration will automatically apply that change everywhere it's used, effectively acting like a labeled, unchangeable setting for your entire project. Constant can be declared in various places like a package, entity, architecture, or process.

-- First, define the constants in a package
package My_Design_Package is
    constant DATA_WIDTH : integer := 8;
    constant CLK_FREQ_HZ: integer := 50_000_000; -- 50 MHz
end package My_Design_Package;

-- Then, use the package in your design
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use work.My_Design_Package.all; -- Makes our constants visible

entity My_Register_File is
    port (
        clk      : in  std_logic;
        data_in  : in  std_logic_vector(DATA_WIDTH - 1 downto 0); -- Uses the constant
        data_out : out std_logic_vector(DATA_WIDTH - 1 downto 0)  -- Uses the constant
    );
end My_Register_File;

1.5.5 Standard Data Types

In VHDL (VHSIC Hardware Description Language), there are various data types used to define the properties and types of variables, signals, and other objects in a hardware design. Here are some common data types in VHDL:

-- Declaration
signal setpoint_signed    : SIGNED(7 downto 0);
signal feedback_signed    : SIGNED(7 downto 0);
signal error_value_signed : SIGNED(7 downto 0);

--  Usage Example: Calculate the difference between two signed values
error_value_signed <= setpoint_signed - feedback_signed;
-- Declaration
signal program_counter : UNSIGNED(15 downto 0);

-- Usage Example: Increment the counter in a clocked process
if rising_edge(clk) then
    program_counter <= program_counter + 1;
end if;
-- Declaration
signal clk   : STD_LOGIC;
signal rst   : STD_LOGIC;
signal q_out : STD_LOGIC;

-- Usage Example: Using a reset signal to clear a register
if rst = '1' then
    q_out <= '0';
elsif rising_edge(clk) then
    -- ... other logic ...
end if;
-- Declaration
signal control_register : STD_LOGIC_VECTOR(7 downto 0);

-- Usage Example: Assigning a hexadecimal value to a control register
control_register <= x"A5"; -- Assigns the bit pattern "10100101"
-- Usage within a process
process(clk)
    -- Declaration (variable declared inside a process)
    variable i : INTEGER;
begin
    -- Usage Example: Controlling a loop a specific number of times
    for i in 0 to 15 loop
        -- ... perform an operation ...
    end loop;
end process;
-- Declaration
signal fifo_is_not_empty : BOOLEAN;
signal read_enable       : STD_LOGIC;

-- Usage Example: Using a boolean flag to control an operation
if fifo_is_not_empty = True then
    read_enable <= '1';
else
    read_enable <= '0';
end if;

1.6 VHDL Architecture Models

In VHDL, there are several approaches to explaining or describing an architecture. Hardware can be described using these styles or models according to its needs and complexity. These approaches are divided into three types:

Module 2 - Dataflow Style

2. Dataflow Style in VHDL

2.1 What is Dataflow Style

The Dataflow style is built on concurrency because the central idea is to model the system as a set of concurrent operations on signals, which directly reflects the physical reality of hardware. In any integrated circuit, all components are active and processing data in parallel; they don't execute in a step-by-step sequence. To accurately describe this, VHDL uses concurrent statements as the foundation. This approach ensures that the model's behavior matches the hardware's true, simultaneous nature, where multiple data transformations happen at the same time.

This is why the Dataflow style describes a circuit by focusing on the flow of data and the transformations applied to it, rather than specifying the exact gate-level structure. Each concurrent statement you write whether it's a simple logical operation or a conditional assignment defines one of these parallel transformations. This method allows you to build a functional description of the circuit by defining the paths and changes that signals undergo. By focusing on this "flow," you let the synthesis tool determine the best gate-level implementation, making it an intuitive and powerful way to translate a high-level circuit diagram into code.

2.2 Concurrent Statement

In general, digital circuits are concurrent in nature (working in parallel or simultaneously). A change in the output of a concurrent circuit is directly influenced by a change in an input. Therefore, VHDL also adopts the concept of concurrency when processing the inputs and outputs of a circuit. A program in VHDL cannot be equated to software programming where instructions are executed sequentially. Instructions in VHDL are executed directly and simultaneously. However, with certain techniques, VHDL can also describe sequential circuits whose processes are executed in order.

A concurrent statement is a method used to describe the parallel operation of hardware in VHDL. There are four ways to describe a concurrent statement:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

-- Entity defines the inputs and outputs (the "black box")
entity Logic_Gates is
    port (
        a, b, c : in  STD_LOGIC;          -- Three inputs
        y_and   : out STD_LOGIC;          -- Output for the AND gate
        y_nand  : out STD_LOGIC           -- Output for the NAND gate
    );
end Logic_Gates;

-- Architecture describes what happens inside the box
architecture Behavioral of Logic_Gates is
begin
    -- These two statements are CONCURRENT. They happen at the same time.
    y_and  <= a and b and c;
    y_nand <= a nand b and c;

end Behavioral;
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

-- Entity for a 2-to-1 Multiplexer
entity Mux_2_to_1_Conditional is
    port (
        i0, i1 : in  STD_LOGIC;  -- The two data inputs
        sel    : in  STD_LOGIC;  -- The select line
        y      : out STD_LOGIC   -- The single output
    );
end Mux_2_to_1_Conditional;

-- Architecture using the "when/else" concurrent statement
architecture Behavioral of Mux_2_to_1_Conditional is
begin
    -- The output 'y' gets the value of 'i0' WHEN 'sel' is '0',
    -- ELSE it gets the value of 'i1'.
    y <= i0 when sel = '0' else i1;

end Behavioral;
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

-- Entity for a 2-to-1 Multiplexer
entity Mux_2_to_1_Selected is
    port (
        i0, i1 : in  STD_LOGIC;  -- The two data inputs
        sel    : in  STD_LOGIC;  -- The select line
        y      : out STD_LOGIC   -- The single output
    );
end Mux_2_to_1_Selected;

-- Architecture using the "with/select" concurrent statement
architecture Behavioral of Mux_2_to_1_Selected is
begin
    -- WITH the value of 'sel', SELECT the output 'y'
    with sel select
        y <= i0 when '0',       -- When sel is '0', y gets i0
             i1 when '1',       -- When sel is '1', y gets i1
             'X' when others;  -- For any other value (like 'U' or 'Z'), output 'X' (unknown)

end Behavioral;

Module 3 - Behavioural Style

A behavioral style in VHDL describes a digital system by specifying its functionality using high-level algorithms and sequential statements without detailing the underlying hardware structure.

Module 3 - Behavioural Style

Understanding Behavioral Style

One of the three architecture models is the behavioral style. Unlike the data-flow style, a VHDL program written in behavioral style does not need to describe how the circuit will be implemented when synthesized. Instead, the behavioral style describes how the circuit’s output will react to the inputs given to the circuit. The core of writing a program using the behavioral style is the process statement.

Using behavioral style in VHDL is like writing a recipe. You don’t explain how the kitchen is built or how the oven is wired (the hardware implementation); instead, you describe the steps the cook should follow and how the dish will turn out depending on the ingredients (the inputs). The process statement is like the main set of instructions in the recipe that guides the entire cooking process.

icegif-292.gif

Module 3 - Behavioural Style

Process Statement

A process statement is a concurrent command that consists of a label, sensitivity list, declaration area, begin–end (body) area, and sequential statements. An example of a process statement description in VHDL is:

process (<Sensitivity List>)
  -- Variable declaration area
begin
  -- VHDL statement block here
end process;

The difference between a concurrent signal assignment statement and a process statement lies in the sequential statements. The syntax or statements inside the begin–end (body) section are executed sequentially, line by line, just like in general programming. The process label itself is simply a self-descriptive naming form to help us recognize which process is being executed in that section, so the naming can be changed or even omitted.

In a concurrent statement, every time a change occurs in the input, the output is re-evaluated. In a behavioral style model using a process statement, whenever a change occurs in a signal listed in the sensitivity list of the process, all the sequential statements within the process body are re-evaluated.

Since a process statement is itself a concurrent statement, if there are two processes in the architecture body, the execution of both processes will be carried out concurrently.

Module 3 - Behavioural Style

Sequential Statement

In a process, the execution of sequential statements will be initiated when there is a change in the signals listed in the sensitivity list. In general, the execution of statements in the process body will be carried out continuously until the end of the process sequentially. There are two types of sequential statements that will be discussed in this module:
● If statement
The if statement is used to create a branch in the execution flow of sequential statements. In VHDL, the if statement can only be used inside the process body. Example of a NAND gate with if statement:

library ieee;
use ieee.std_logic_1164.all;

entity nand_gate is
    port(
        A, B : IN STD_LOGIC;
        Y    : OUT STD_LOGIC
    );
end nand_gate;

architecture behavioral of nand_gate is
begin
    nand_proc : process (A, B) is
    begin
        if (A = '1' AND B = '1') then
            Y <= '0';
        else
            Y <= '1';
        end if;
    end process nand_proc;
end behavioral;

● Case statement
The case statement works in a way similar to the previous if statement. The difference is that the case statement will be more efficient to use when there are many variations of values. Example of a NAND gate using case statement:

library ieee;
use ieee.std_logic_1164.all;

entity nand_gate is
    port(
        A, B : IN STD_LOGIC;
        Y    : OUT STD_LOGIC
    );
end nand_gate;

architecture behavioral of nand_gate is
begin
    AB <= A & B;  -- combining signals A and B

    nand_proc : process (A, B) is
    begin
        case (AB) is
            when "11"    => Y <= '0';
            when others  => Y <= '1';
        end case;
    end process nand_proc;
end behavioral;

When using behavioral style, nested sequential statements are common and often used. This is also what makes behavioral style more powerful than data-flow style. However, even though behavioral style statements are used like programming in general, it should be noted that VHDL is a hardware description language, not a programming language. Also, try to always keep the process statement in the description you create as simple as possible to make hardware design easier when the circuit becomes more complex.

Module 3 - Behavioural Style

Wait Statements

Wait Statements
Wait statements are used to make a process wait for a certain condition, signal/variable, or a specific time interval. The following wait statements are used:

Wait until [condition] and wait on [signal]
wait until [condition] will block the process while checking whether a condition is true or false. The process will remain blocked until the condition being checked becomes true. Meanwhile, wait on [signal] will wait until there is a change in the specified signal. The syntax of wait until and wait on can be synthesized.

process is
begin
    signal1 <= '1';
    signal2 <= '0';

    wait until rst <= '1';

    signal1 <= '0';
    signal2 <= '1';

    wait on clk;
end process;

● Wait for [time period]
wait for [time period] will block the process for the specified time period. The syntax of wait for cannot be synthesized but can be simulated in the waveform using ModelSim. Therefore, wait for is commonly used for a testbench, which will be studied in the next module.

process is
begin
    signal1 <= '1';
    signal2 <= '0';

    wait for 50 ps;

    signal1 <= '0';
    signal2 <= '1';

    wait for 100 ps;
end process;
Module 3 - Behavioural Style

Report Statements

In VHDL, the report statement is used to generate text messages during simulation. This statement is useful for providing information about the status or certain values during simulation. The generated report will appear in the transcript to help with debugging.

‘Image Attribute
In VHDL, the 'image attribute is used to convert a certain data type into a string data type. This attribute is useful when you want to combine or display values of different data types in the form of a string. In a report statement, this attribute is used so that variables/signals can be printed to the transcript, which only accepts strings.

Here is an example of a report statement that also uses the 'image attribute:

LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.STD_LOGIC_ARITH.ALL;

ENTITY test IS
PORT (
    clk : IN STD_LOGIC
);
END test;

ARCHITECTURE Behavioral OF test IS
    SIGNAL counter : UNSIGNED(7 DOWNTO 0);
BEGIN
    PROCESS
    BEGIN
        WAIT FOR 50 ps;
        LOOP
            WAIT UNTIL rising_edge(clk);
            REPORT "Counter = " & INTEGER'image(conv_integer(counter));
            counter <= counter + 1;
        END LOOP;
    END PROCESS;
END Behavioral;

Example simulation:

Screenshot 2025-09-09 071418.png

Module 4 - Testbench

Module 4 - Testbench

1. Understanding Testbench in VHDL

A VHDL testbench is a non-synthesizable VHDL entity used to simulate and verify the functionality of another VHDL entity, often referred to as the Unit Under Test (UUT). Think of it as a virtual lab environment where you can apply a sequence of inputs (stimulus) to your design and observe its behavior and outputs over time. Since the testbench itself is not meant to be turned into a physical chip, it can use more abstract and powerful VHDL constructs that are not available for synthesizable hardware descriptions.

A testbench has many uses, including:

Module 4 - Testbench

2. Components of a Testbench

2.1 Entity Declaration

The testbench entity is declared without any ports. It's a self-contained module because it doesn't connect to any higher-level design; it is the top-level entity for the simulation.

entity project_tb is
    -- Empty because testbench doesn't have any port
end project_tb

2.2 Architecture Declaration

2.2.1 UUT Component Declaration

Inside the architecture, we first declare the entity we want to test (the UUT) as a component. The component declaration must match the entity declaration of the UUT.

For example, if we have a UUT with these entity declaration:

entity earth_destroyer is
    Port (
        clk, rst    : IN STD_LOGIC;
        input       : IN STD_LOGIC VECTOR(7 downto 0);
        mode        : IN STD_LOGIC_VECTOR(3 downto 0);
        output      : OUT STD_LOGIC_VECTOR(7 downto 0)
    );
end earth_destroyer;

The UUT component declaration for that entity will be:

component earth_destroyer is
    Port (
        clk, rst    : IN STD_LOGIC;
        input       : IN STD_LOGIC VECTOR(7 downto 0);
        mode        : IN STD_LOGIC_VECTOR(3 downto 0);
        output      : OUT STD_LOGIC_VECTOR(7 downto 0)
    );
end component;

As you can see, component declaration is almost the exact same as entity declaration. Just make sure you change entity to component and use end component instead of end <entity name>, and you're good to go.

2.2.2 Signals Declaration

We must declare internal signals within the testbench architecture. You should at least declare all the signals that corresponds to the entity input/output ports. These signals will be connected to the ports of the UUT to drive its inputs and monitor its outputs.

For example, if we have earth_destroyer entity as in Part 2.2.1, we should declare these signals:

signal clk_tb    : STD_LOGIC := '0';
signal rst_tb    : STD_LOGIC;
signal input_tb  : STD_LOGIC VECTOR(7 downto 0);
signal mode_tb   : STD_LOGIC_VECTOR(3 downto 0);
signal output_tb : STD_LOGIC_VECTOR(7 downto 0);

The name of the signal doesn't really matter, as long as its data type match the port it corresponds to.

2.3 Port Map

In VHDL, a port map is part of the component instantiation within an architecture that maps the input and output ports of an entity to local signals. By using port map, we can connect a testbench to the entity being tested, allowing inputs to be driven and outputs to be observed through the testbench.

The general syntax of port map in a testbench is:

UUT : entity_name port map (entity_port_name => local_signal_name);

For example, if we want to instantiate the component as in Part 2.2, we can write it like this:

UUT : earth_destroyer port map (
    clk    => clk_tb,
    rst    => rst_tb,
    input  => input_tb,
    mode   => mode_tb,
    output => output_tb
);
Module 4 - Testbench

3. Testbench Architecture Models

3.1 Testbench for Combinational Circuit

There are three architectural models for changing the value of inputs in a testbench. For example, if we want to make a testbench for a half adder entity below, we could use three methods:

library IEEE;
use IEEE.STD_LOGIC_1164.all;

entity half_adder is
    Port (
        a, b       : in STD_LOGIC;
        sum, carry : out STD_LOGIC
    );
end half_adder;
    
architecture arch of half_adder is
begin
    
    sum <= a xor b;
    carry <= a and b;
    
end arch;

3.1.1 Simple Testbench

This method is very similar to the dataflow style of VHDL programming. Values are directly assigned to input signals using the <= symbol. The difference is that each value assignment is separated by the after keyword, which indicates that the value will change after the testbench has run for the specified amount of time. For example, '1' after 60 ns means the value of the signal will change from '0' to '1' after the simulation has run for 60 ns from the very beginning.

library IEEE;
use IEEE.STD_LOGIC_1164.all;

entity half_adder_tb is
end half_adder_tb;
    
architecture tb of half_adder_tb is
    
    -- component declaration for half_adder
    component half_adder is
        Port (
            a, b       : in STD_LOGIC;
            sum, carry : out STD_LOGIC
        );
    end component;
    
    -- signal declaration for input/output stimulus
    signal a, b       : STD_LOGIC;    -- Input
    signal sum, carry : STD_LOGIC;    -- Output

begin
    -- component instantiation to connect tb to entity
    UUT: half_adder port map (
        a => a,
        b => b,
        sum => sum,
        carry => carry
    );
    
    -- input assignment (simple testbench)
    a <= '0', '1' after 20 ns, '0' after 40 ns, '1' after 60 ns;
    b <= '0', '1' after 40 ns;

end tb;

3.1.2 Testbench Using a process Statement

This method is similar to the behavioral style of VHDL programming, which uses a process statement. In this approach, every line of code inside the process is executed sequentially, one by one, much like in a typical programming language.

-- input assignment (process testbench)
tb1: process
    constant period    : time := 20 ns;
    begin
        a <= '0';
        b <= '0';
        wait for period;
            
        a <= '0';
        b <= '1';
        wait for period;
            
        a <= '1';
        b <= '0';
        wait for period;
            
        a <= '1';
        b <= '1';
        wait for period;
        
        wait; -- wait until the end of time
    end process; 

3.1.3 Testbench Using a Look-up Table

This method is an extension of the process-based testbench. Instead of assigning each combination of values one by one, this method works by storing the combinations in a separate variable, called a look-up table (which can be a signal or a constant).

-- input assignment (look-up table testbench)
tb1: process
    constant period    : time := 20 ns;
    constant stream_a  : STD_LOGIC_VECTOR(0 to 3) := ('0', '1', '0', '1');
    constant stream_b  : STD_LOGIC_VECTOR(0 to 3) := ('0', '0', '1', '1');

    begin
        a <= stream_a(0);
        b <= stream_b(0);
        wait for period;
            
        a <= stream_a(1);
        b <= stream_b(1);
        wait for period;
            
        a <= stream_a(2);
        b <= stream_b(2);
        wait for period;
            
        a <= stream_a(3);
        b <= stream_b(3);
        wait for period;
        
        wait; -- wait until the end of time
    end process; 

You can also use for loop for a more efficient implementation, but more of it will be covered in Module 6 and it's not recommended for this module.

3.2 Testbench for Sequential Circuit

A testbench for a sequential circuit is not much different from a testbench for a combinational circuit. The main difference is the need for several additional inputs, including a Clock and a Reset.

3.2.1 Clock Process

The clock is the heartbeat of a sequential circuit. In a testbench, the clock signal must be generated continuously throughout the simulation to drive the Unit Under Test (UUT). This is almost always done using a dedicated, independent process.

-- Inside of testbench architecture
    constant period    : time := 20 ns;    -- clock period
    signal clk         : STD_LOGIC;        -- clock signal

begin
    clk_generator : process
    begin
        clk <= '0';
        wait for period/2;
        clk <= '1';
        wait for period/2;
    end process;

3.2.2 Example of Testbench for Sequential Circuit

For example, we want to make a testbench for this updown counter entity:

library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;

entity counter_updown is
    Port (
        RESET, CLK, LD, UP : in std_logic;
        DIN                : in std_logic_vector (3 downto 0);
        COUNT              : out std_logic_vector (3 downto 0)
    );
end counter_updown;

architecture my_count of counter_updown is
    signal t_cnt : unsigned(3 downto 0); -- internal counter signal
begin
    process (CLK, RESET)
    begin
        if (RESET = '1') then
            t_cnt <= (others => '0'); -- clear
        elsif (rising_edge(CLK)) then
            if (LD = '1') then t_cnt <= unsigned(DIN); -- load
            else
                if (UP = '1') then t_cnt <= t_cnt + 1; -- incr
                else t_cnt <= t_cnt - 1; -- decr
                end if;
            end if;
        end if;
    end process;
    COUNT <= std_logic_vector(t_cnt);
end my_count;

In this case, the testbench combines the three architectural models that were previously explained. The Clock signal uses a process statement, while the Reset signal uses the simple assignment method. The other inputs are assigned directly in the declaration section because their values will not change.

Additionally, it is important to note the presence of the max_clk variable, which is used to limit the number of clock cycles run by the testbench. If the clock cycles are not limited, the testbench will continue to run indefinitely unless the program is stopped manually.

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

entity counter_updown_tb is
end counter_updown_tb;

architecture arch of counter_updown_tb is
    component counter_updown is
        Port (
            RESET, CLK, LD, UP : in std_logic;
            DIN                : in std_logic_vector (3 downto 0);
            COUNT              : out std_logic_vector (3 downto 0)
        );
    end component;

    constant period    : time := 20 ns;    -- clock period
    constant max_clk   : integer := 11;    -- maximum clock cycle
    signal cnt         : integer := 0;     -- clock cycle counter

    constant LD        : std_logic := '0';              -- input
    constant UP        : std_logic := '1';              -- input
    signal DIN         : std_logic_vector(3 downto 0);  -- input
    signal RESET       : std_logic;                     -- input
    signal CLK         : std_logic;                     -- input
    signal COUNT       : std_logic_vector(3 downto 0);  -- output

begin
    UUT : counter_updown port map (RESET, CLK, LD, UP, DIN, COUNT);
        
    -- reset = '1' at the first clock cycle, then change to '0'
    reset <= '1', '0' after period/2;

    CLK_generator : process
    begin
        CLK <= '0';
        wait for period/2;
        CLK <= '1';
        wait for period/2;

        if(cnt < max_clk) then cnt <= cnt + 1;
        else wait;
        end if;
    end process;
end arch;

Here is the simulation result of the testbench above: Result

Module 4 - Testbench

4. Assert and Report Statement

4.1 Assert Statement

The assert statement is used for creating self-checking testbenches. It acts like an automated check that constantly monitors a condition. If the condition is false, it "asserts" a message, alerting us to a problem without requiring us to manually inspect the waveforms.

The full syntax of an assert statement is:

ASSERT condition REPORT "message" SEVERITY level;

For example, if we want to implement the assert statement in our testbench from section 3.1.2, we can implement it as below:

tb1: process
    constant period    : time := 20 ns;
    begin
        a <= '0';
        b <= '0';
        wait for period;
            
        assert ((sum = '0') and (carry = '0'))
        report "tes gagal pada testcase ke-1" severity error;
            
        a <= '0';
        b <= '1';
        wait for period;
            
        assert ((sum = '1') and (carry = '0'))
        report "tes gagal pada testcase ke-2" severity error;
            
        a <= '1';
        b <= '0';
        wait for period;
            
        assert ((sum = '1') and (carry = '0'))
        report "tes gagal pada testcase ke-3" severity error;
            
        a <= '1';
        b <= '1';
        wait for period;
            
        assert ((sum = '0') and (carry = '1'))
        report "tes gagal pada testcase ke-4" severity error;
        
        wait; -- wait until the end of time
    end process; 

The following is the output produced by the testbench: TB Result

4.2 Severity Level

The severity level tells the simulator how serious the failed assertion is. There are four standard levels:

Level Description Simulator Action
NOTE Informational message. Used for debugging, tracing, or indicating progress. Prints the message and continues simulation.
WARNING Non-critical issue. Something is unexpected or out of spec, but the design might still function. Prints a warning and continues simulation. Increments a warning counter.
ERROR Functional failure. The design's output is incorrect. This is the standard for a failed test. Prints an error and continues simulation. Increments an error counter.
FAILURE Catastrophic/Fatal error. Something is fundamentally broken, making further simulation pointless. Prints a failure message and immediately halts the simulation.

Note that if we don't explicitly specify which severity level used in a report statement, it automatically defaults to note.

Module 4 - Testbench

5. File I/O in VHDL

In VHDL, we can perform file handling using the TextIO library. This feature is very useful for documenting the results of a program that has been created. To use the TextIO library, we need to add it at the beginning of our program as follows:

use std.textio.all;
use ieee.std_logic_textio.all; -- For std_logic types

5.1 Read File

When repeatedly simulating a design, changing the value of each input one by one can be time-consuming and inefficient. Therefore, we can use a feature from the TextIO library that can read inputs from a file to be used in the design simulation.

Here is how to read input from a file in VHDL. First, we can define the file to be read within a process statement and open it in read_mode:

process
    -- Define the file and its open mode
    file text_file   : text open read_mode is "filename.txt";

    -- Variable to receive data from the file
    variable fileinp : integer;

    -- Line type variable to hold a row from the text file
    variable row     : line;

    -- Variale to store file reading status
    variable ok      : boolean

Then, we can read the file using the readline and read procedures from the TextIO library as follows:

begin
    while not endfile(text_file) loop --loop until the end of the text file
        readline(text_file, row); --reads the line from the file
        
        -- Skip empty lines or comments that start with '#'
        if row.all'length = 0 or row.all(1) = '#' then
            next;
        end if;

        -- Reads a variable from the line and puts it into fileinp
        read(row, fileinp, ok); 
        
        assert ok
        report "Read 'sel' failed for line: " & row.all
        severity failure;  --report if the file read fails
         
        wait for delay; --delay for the read iteration
    end loop;
end process;

5.2 Write File

Besides reading a set of inputs to be used in a testbench, we can also use the TextIO library to save the results of our testbench to a file. This allows us to analyze the results without having to use a simulator like ModelSim or Vivado.

Here are the steps required to write the results of a testbench to a file. First, we can define the file to be created and open it in write_mode:

process
    -- Open "filename.txt" in write mode
    file text_file      : text open write_mode is "filename.txt";

    -- Line type variable to build a row for the text file
    variable row        : line;

    -- Variable holding the data to be written to the file
    variable data_write : integer;

Then, we can use the write and writeline procedures rom the TextIO library to write data to the file as follows:

begin
    -- writes data to the file, left-justified, 15 characters
    write(row, data_write, left, 15); 

    writeline(text_file, row); -- writes the line to the file
end process;
Module 4 - Testbench

Extra: Array in VHDL

6.1 Array

In VHDL, an array is a collection of elements that share the same data type. You can think of an array as a variable that holds many elements of the same type, and these elements are indexed to be accessed. The index can be a number or another indexable type, such as integer, natural, or std_logic_vector. Arrays can have one dimension (one-dimensional array) or more (two-dimensional, three-dimensional, and so on). Two-dimensional arrays are often used to represent tables or matrices.

6.2 Type

A type is a definition used to declare a new data type in VHDL. A type can be used to define complex data types, such as arrays or records, or as a type used to declare variables, ports, or signals. Types can also be used to describe the properties and structure of data.

VHDL has predefined data types, such as std_logic, std_logic_vector, integer, and others, but we can also create our own custom data types. Types that are predefined or embedded in VHDL libraries are called "built-in types," while types that we define ourselves are called "user-defined types."

Here is an example of using type and array to create a bank of 8-bit registers:

type RegisterArray is array (0 to 7) of std_logic_vector(7 downto 0);
signal registers : RegisterArray := (others => (others => '0'));

In the example above, we are defining a structure that could be used in an entity like a RegisterBank which has eight 8-bit registers. These registers are represented by the registers array, which has 8 elements, each with a length of 8 bits.

Module 5 - Structural Programming

Module 5 - Structural Programming

1. Structural Programming in VHDL

1.1 Structural Style

Structural Style Programming is an approach in VHDL that allows designers to create digital circuits by using basic components connected to each other to form a more complex system. In this approach, a circuit is represented as a collection of entities linked in a specific way to achieve the desired function.


1.2 Port Mapping

Port mapping is the process of associating (mapping) the ports of a component (entity) in VHDL with the signals present in the architecture. This allows us to connect a defined entity to the actual circuit in the design.

Some important points:

Port Mapping Example

-- Entity definition
entity AND2 is
  port (
    A, B: in std_logic;
    Y: out std_logic
  );
end entity;

-- Port mapping in architecture (this is actually the entity's behavior)
architecture RTL of AND2 is
begin
  Y <= A and B;
end architecture;

-- Using the entity with port mapping in a higher-level design
D1: AND2 port map (
  A => input_signal_A,
  B => input_signal_B,
  Y => output_signal
);
Module 5 - Structural Programming

2. Generic Map

2.1 Generic Map Explanation

A generic map is the process of associating a generic value in an entity with a value in the architecture. Generics are parameters used to configure a component.

Some important points:

2.2 Generic Map Example

entity Counter is
  generic (
    WIDTH: positive := 8;
    ENABLED: boolean := true
  );
  port (
    clk: in std_logic;
    reset: in std_logic;
    count: out std_logic_vector(WIDTH-1 downto 0)
  );
end entity;

architecture RTL of MyDesign is
  signal my_counter_output: std_logic_vector(7 downto 0);
begin
  my_counter_inst: Counter
    generic map (
      WIDTH => 8,
      ENABLED => true
    )
    port map (
      clk => system_clock,
      reset => reset_signal,
      count => my_counter_output
    );
end architecture;
Module 5 - Structural Programming

3. VHDL Modularity

3.1 VHDL Modularity Explanation

Example: 4-bit Ripple Carry Adder using 4 Full Adders. A Ripple Carry Adder adds binary numbers with a chained carry.

3.1.1 Stage 1: Full Adder

entity Full_Adder is
  port (
    A, B, Cin: in std_logic;
    Sum, Cout: out std_logic
  );
end entity Full_Adder;

architecture RTL of Full_Adder is
begin
  Sum <= (A xor B) xor Cin;
  Cout <= (A and B) or ((A xor B) and Cin);
end architecture;

3.1.2 Stage 2: 4-bit Ripple Carry Adder

entity Four_Bit_RCA is
  port (
    A, B: in std_logic_vector(3 downto 0);
    Sum: out std_logic_vector(3 downto 0);
    Cout: out std_logic
  );
end entity Four_Bit_RCA;

architecture RTL of Four_Bit_RCA is
  signal Carry: std_logic_vector(3 downto 0);
begin
  FA0: Full_Adder port map (A(0), B(0), '0', Sum(0), Carry(0));
  FA1: Full_Adder port map (A(1), B(1), Carry(0), Sum(1), Carry(1));
  FA2: Full_Adder port map (A(2), B(2), Carry(1), Sum(2), Carry(2));
  FA3: Full_Adder port map (A(3), B(3), Carry(2), Sum(3), Cout);
end architecture;

3.1.3 Stage 3: Testbench

entity RCA_tb is
end entity RCA_tb;

architecture RTL of RCA_tb is
  signal A, B, Sum: std_logic_vector(3 downto 0);
  signal Cout: std_logic;
  signal Clock: std_logic := '0';
  constant Clock_Period: time := 10 ns;

  component Four_Bit_RCA
    port (
      A, B: in std_logic_vector(3 downto 0);
      Sum: out std_logic_vector(3 downto 0);
      Cout: out std_logic
    );
  end component;
begin
  Clock_Process: process
  begin
    while now < 1000 ns loop
      Clock <= not Clock;
      wait for Clock_Period / 2;
    end loop;
    wait;
  end process;

  A <= "1101";
  B <= "0011";

  RCA: Four_Bit_RCA port map (A, B, Sum, Cout);

  process
  begin
    wait until rising_edge(Clock);
    if Cout = '1' then
      report "The addition result is overflowing";
    end if;
    report "Addition result: " & to_string(Sum);
    wait;
  end process;
end architecture RTL; -- Corrected from Main_Architecture to RTL
Module 5 - Structural Programming

4. Array and Type

4.1 Array and Type in VHDL

4.1.1 Array

An array is a collection of elements of the same data type. It can be one-dimensional or multi-dimensional.

4.1.2 Type

A type is a new data definition. It can be a built-in type (std_logic, integer) or a user-derived type.

4.2 Type and Array Example

type RegisterArray is array (0 to 7) of std_logic_vector(7 downto 0);
signal registers: RegisterArray := (others => (others => '0'));

Module 6 - Looping Construct

loop

Module 6 - Looping Construct

Introduction to Looping Constructs

Introduction to Looping Constructs

Looping constructs are VHDL instructions that allow a program to repeat the same block of code, a process known as iteration (similar to C).

Loops in VHDL are divided into two main categories based on how they function and what they create.


1. Sequential Loops (Inside a process)

These loops are similar to those in traditional programming languages. They describe an algorithm or a sequence of operations that execute step-by-step.

There are two main types of sequential loops:


2. Concurrent Loops (Outside a process)

This type of loop is unique to hardware description languages. It doesn't describe an algorithm but instead describes the replication of hardware structures. It acts like copy and pasting ICs on a breadboard.

image

(well, that's the gist of it)

Module 6 - Looping Construct

For Loop

For Loop

The for loop is the most common type of sequential loop in VHDL. It is used to repeat a block of code a specific, pre-determined number of times. This makes it ideal for tasks where you know exactly how many iterations you need, such as processing the bits of a vector.


Syntax

The basic structure of a for loop is:

loop_label: for <index_variable> in <range> loop
    -- code code code...
end loop loop_label;

Example:

testloop: for i in 0 to 2 loop
    -- code cedo deco ... .. . .
end loop

Notes

When using a for loop, there are a few important rules to remember:


Example: Init Memory

The code below shows the for loop iterating through every address of the memory to write a '0' value to it, like setting a RAM's content to 0.

type t_memory is array(0 to 63) of std_logic_vector(7 downto 0);

-- Inside your architecture...
p_Memory_Reset: process (i_Clock, i_Reset)
    variable v_ram : t_memory;
begin
    if (i_Reset = '1') then
        -- Initialize all 64 locations of the RAM to zero.
        -- We use a label "RAM_INIT_LOOP" for clarity.
        RAM_INIT_LOOP: for i in v_ram'range loop
        -- RAM_INIT_LOOP: for i in 0 to 63 loop <- would work too. variable`range is more flexible
            v_ram(i) := (others => '0');
        end loop RAM_INIT_LOOP;

    elsif rising_edge(i_Clock) then
        -- ... normal memory operation code ...
    end if;
end process p_Memory_Reset;

This loop is much cleaner than writing 64 individual lines of code. It uses v_ram'range (variable'range) to automatically loop through the entire size of the array.

Module 6 - Looping Construct

While Loop

While Loop

The while loop is used where the number of repetitions is not known from the start. A while loop continues to execute as long as a specified condition is true.


Syntax

The basic structure of a while loop is:

loop_label: while <condition> loop
    -- Going through...
    -- IMPORTANT: Must include logic to eventually make the condition false!
end loop loop_label;

Warning: Infinite Loops

A while loop can create an infinite loop. This occurs if the loop's condition never becomes false. Unlike in C, Java, Python etc. where infinite loops are harmless, VHDL shouldn't have these.

To avoid this, ensure that the logic inside the loop will eventually cause the condition to become false.


Example: Finding the First '1'

In this example, we'll search a vector from left to right (from the most significant bit) to find the position of the first bit that is a '1'.

Notice that unlike a for loop, we must manually declare and update our index variable (i).

-- Inside a process...
p_Find_First_One: process(a_vector)
    -- We must declare our own index variable for a while loop
    variable i : integer := a_vector'left; -- Start at the leftmost bit
    variable i_position : integer := -1; -- -1 if no 1 is found
begin
    -- Reset variables for each run of the process
    i_position := -1;
    i := a_vector'left;

    FIRST_ONE: while (i >= a_vector'right) loop

        if a_vector(i) = '1' then
            i_position := i;
            exit FIRST_ONE; -- Quit if found
        end if;

        -- Manually decrement the index to check the next bit
        i := i - 1;

    end loop SEARCH_LOOP;

    -- Assign the result to a signal
    found_position_signal <= i_position;

end process p_Find_First_One;

In this code, the loop continues as long as i is within the vector's bounds. If a '1' is found, the exit statement terminates the loop early. If no '1' is found, the loop completes naturally when i goes out of bounds.

Module 6 - Looping Construct

Loop Control - Next & Exit

Loop Control - Next & Exit

The two control statements, next and exit, allow you to skip an iteration or terminate the loop entirely, giving you more precise control over your sequential code.

These statements can be used in both for and while loops.


One, The next Statement

The next statement immediately stops the current loop iteration and jumps to the beginning of the next one. Any code that comes after the next statement within the loop body is skipped for that specific iteration (same thing as in C!).

The syntax can be written in two ways:

-- Form 1: Using an if-statement
if <condition> then
    next;
end if;

-- Form 2: Using the 'when' keyword
next when <condition>;

Example: Summing Only Odd Numbers

To skip even numbers, the next statement is perfect!

-- Inside a process...
variable sum : integer := 0;
...
SUM_ODD_LOOP: for i in 1 to 10 loop
    -- If the number is even, skip to the next iteration
    next when (i mod 2 = 0);

    -- This line is only reached for odd numbers
    sum := sum + i;
end loop SUM_ODD_LOOP;
-- At the end, sum will be 1+3+5+7+9 = 25

Two, The exit Statement

The exit statement terminates the loop entirely. As soon as exit is executed, the program moves to the line after end loop.

The syntax is similar to next:

-- Form 1: Using an if-statement
if <condition> then
    exit;
end if;

-- Form 2: Using the 'when' keyword
exit when <condition>;
Example: Finding a Value in Memory

Say, we want to search through a memory to find an address that holds a specific value. Once we find it, there is no reason to continue searching.

-- Inside a process...
constant SEARCH_VALUE : std_logic_vector(7 downto 0) := x"A5";
variable found_addr : integer := -1; -- Default value
...
SEARCH_MEM_LOOP: for i in ram'range loop
    -- If the value at the current address matches,
    -- store the address and exit the loop immediately.
    if ram(i) = SEARCH_VALUE then
        found_addr := i;
        exit SEARCH_MEM_LOOP;
    end if;
end loop SEARCH_MEM_LOOP;

-- The code goes on...

Using exit makes the search efficient. It prevents the loop from doing unnecessary work after the item has been found.

Module 6 - Looping Construct

For-Generate Loop

The Concurrent 'for-generate' Loop

We now switch from sequential loops to a concurrent one.

The for-generate statement is not a loop that executes over time inside a process. Instead, it's a command that tells the synthesizer to create multiple copies of hardware structures.

It's good for repetitive structures like registers and chains of components. A key rule is that for-generate is used outside of a process, in the main architectural body.


Main Differentiators


Syntax

The basic structure of a for-generate statement is:

generate_label: for <identifier> in <range> generate
    -- Concurrent statements to be replicated go here...
    -- (e.g., component instantiations)
end generate generate_label;

Example: 4-Bit Adder

We start with a 1-bit full adder and use for-generate to create and connect four of them in a chain to make a 4-bit adder.

1. Replication Target First, we need the definition of the 1-bit full adder that we want to copy.

component full_adder is
    port (
        A, B, Cin  : in  std_logic;
        S, Cout    : out std_logic
    );
end component;

2. 4-Bit Adder Architecture Next, we use for-generate to create four instances of the full_adder and wire them together.

entity four_bit_adder is
    port (
        A, B    : in  std_logic_vector(3 downto 0);
        Cin     : in  std_logic;
        S       : out std_logic_vector(3 downto 0);
        Cout    : out std_logic
    );
end entity;

architecture structural of four_bit_adder is
    -- Internal signal to wire the carry chain between the adders.
    -- It needs 5 bits to include the first Cin and final Cout.
    signal C : std_logic_vector(4 downto 0);
begin

    -- The first carry wire is connected to the adder's carry-in pin
    C(0) <= Cin;

    -- Generate the chain of 4 full adders
    ADDER_CHAIN: for i in 0 to 3 generate
        -- Create one instance of the full_adder in each "iteration"
        FA_INSTANCE: full_adder
            port map (
                A    => A(i),       -- Connect to the i-th bit of input A
                B    => B(i),       -- Connect to the i-th bit of input B
                Cin  => C(i),       -- The carry-in for this bit
                S    => S(i),       -- The sum output for this bit
                Cout => C(i+1)      -- The carry-out for this bit
            );
    end generate ADDER_CHAIN;

    -- The final carry-out of the chain is the adder's carry-out pin
    Cout <= C(4);

end architecture structural;

Physically (RTL wise), this is the exact same as copy-pasting the 4 adders manually. So, this approach is much cleaner and more organized.

Module 6 - Looping Construct

When & Which

When & Which?

Comparison

Feature for Loop while Loop for-generate Statement
Execution Sequential Sequential Concurrent
Usage Location Inside a process Inside a process Outside a process
Iteration Fixed number of times Repeats while a condition is true Creates N physical copies
Purpose Algorithmic tasks Searching or Polling Hardware Replication

Quick Guide

Remember, all loops have the same syntax rules, so you just need to remember the structure. Good luck! :+1:

Module 7 - Procedure, Function, and Impure Function

Module 7 - Procedure, Function, and Impure Function

Procedure

In VHDL, a procedure is a type of language construct used to group several statements and specific tasks into a single block of code. Procedures help in organizing and simplifying the understanding of complex VHDL designs.

A procedure in VHDL is similar to a void function in languages like C. It performs a specific task but does not return a value. Instead, it may modify signals or variables passed to it as parameters, allowing designers to reuse code and improve readability.

Procedure Declaration

A procedure is defined using a procedure declaration. This declaration specifies the procedure name, any required parameters (if any), and the data type that is returned (if applicable). Below is an example of a procedure declaration in VHDL:

procedure Nama_Procedure(parameter1: tipe_data; parameter2: tipe_data) return tipe_data
  is
  begin
  -- Blok kode procedure
end Nama_Procedure;

Parameters in Procedure
A procedure can accept parameters as arguments. These parameters are used to pass data into the procedure so that it can be processed. The required parameters are defined in the procedure declaration and can be of different modes such as in, out, or inout, depending on whether the data is being read, written, or both.

Procedure Body (Code Block)
The body of a procedure is the section where the tasks to be performed by the procedure are written. Within this block, you can write statements to perform various operations such as calculations, condition checks, data manipulation, and more. This is where the logic of the procedure is implemented, similar to the body of a function in other programming languages.

Procedure Call

To use a procedure, it can be called from the main part of the design or from within another procedure. A procedure is invoked by providing arguments that match the parameters defined in its declaration. Below is an example of how a procedure is used in VHDL.

variable1 := Nama_Procedure(nilai_parameter1, nilai_parameter2);

Example Code

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

entity Procedure_Example is
  port(
    clk : in std_logic;         -- Clock input
    result_out : out integer    -- Output to observe the result
  );
end Procedure_Example;

architecture Behavioral of Procedure_Example is

  -- Signal declarations
  signal sigA : integer := 5;
  signal sigB : integer := 7;
  signal adder_result : integer;

  procedure adder(
    A, B : in integer;       -- Input parameters
    Hasil : out integer      -- Output parameter (will write to adder_result)
  ) is
  begin
    Hasil := A + B;
  end procedure;

begin

  process(clk)
  begin
    if rising_edge(clk) then
      adder(sigA, sigB, adder_result);  -- Call the procedure
    end if;
  end process;

  -- Output assignment
  result_out <= adder_result;

end Behavioral;
Module 7 - Procedure, Function, and Impure Function

Function

In VHDL, a function is a subprogram used to perform calculations or data processing that returns a single value as a result. Functions in VHDL are similar to functions in traditional programming languages such as C or Java, where the main purpose is to compute a value based on the input arguments. Unlike procedures, functions must return exactly one value and cannot modify signals or variables outside the function directly. They are typically used for operations like arithmetic, logical computations, or data conversion.

Functions improve code readability, reusability, and modularity in complex VHDL designs.

Function Declaration

A function is defined using a function declaration. This declaration specifies:

Below is an example of a function declaration in VHDL:

function Function_Name(parameter1: data_type; parameter2: data_type) return return_type is
begin
  -- Function code block
  return value;
end Function_Name;

Parameters in Function

A function can accept input parameters only. These parameters are used to pass data into the function to be processed. Unlike procedures, functions cannot have out or inout parameters. All data returned from a function must be provided through the return statement.

Function Body (Code Block)

The body of a function contains the logic used to compute and return a value. This may include arithmetic operations, conditions, or other expressions. The function must include a return statement that provides the final result.

Function Call

A function is called by using its name and passing the required arguments. The returned value can be directly assigned to a signal or variable.

variable1 := Function_Name(input_value1, input_value2);

Example Code

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

entity Function_Example is
  port(
    clk : in std_logic;         -- Clock input
    result_out : out integer    -- Output to observe the result
  );
end Function_Example;

architecture Behavioral of Function_Example is

  -- Signal declarations
  signal sigA : integer := 10;
  signal sigB : integer := 20;
  signal function_result : integer;

  -- Function Declaration
  function adder(
    A: integer;
    B: integer
  ) return integer is
  begin
    return A + B;
  end function;

begin

  process(clk)
  begin
    if rising_edge(clk) then
      function_result <= adder(sigA, sigB);  -- Call the function
    end if;
  end process;

  -- Output assignment
  result_out <= function_result;

end Behavioral;
Module 7 - Procedure, Function, and Impure Function

Impure Function

In VHDL, an impure function is a special type of function that is allowed to read or modify signals, variables, or states outside its local scope. Unlike a pure function, which always produces the same output for the same input (no side effects), an impure function can interact with external data and may produce different results each time it is called.

Impure functions are useful when you need to:

To define an impure function, you must use the keyword impure.


Impure Function Declaration

impure function Function_Name(parameter1: data_type) return return_type is
begin
  -- Function code block with external side effects
  return value;
end Function_Name

Code Example

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

entity Impure_Function_Example is
  port(
    clk : in std_logic;
    result_out : out integer
  );
end Impure_Function_Example;

architecture Behavioral of Impure_Function_Example is

  -- Signal declaration
  signal counter : integer := 0;
  signal function_result : integer;

  -- Impure Function Declaration
  impure function read_and_increment return integer is
  begin
    counter <= counter + 1;          -- Modifies an external signal
    return counter;                  -- Returns updated value
  end function;

begin

  process(clk)
  begin
    if rising_edge(clk) then
      function_result <= read_and_increment;  -- Call the impure function
    end if;
  end process;

  -- Output assignment
  result_out <= function_result;

end Behavioral;

When to Use Impure or Pure Functions

Code 1

function multiply(a, b: integer) return integer is
begin
  return a * b;
end function;

This is pure because it depends only on the inputs a and b.

Code 2

signal counter : integer := 0;

impure function increment_counter return integer is
begin
  counter <= counter + 1;  -- Modifies external signal
  return counter;
end function;

This function must be impure because it changes an external signal (counter).

Code 3

signal total_sum : integer := 0;

impure function add_and_accumulate(a, b : integer) return integer is
begin
  total_sum <= total_sum + (a + b); -- Modify external signal
  return total_sum;                 -- Return updated accumulated value
end function;

This function must be impure because it changes an external signal (total_sum).

Code 4

signal current_status : integer := 3;

impure function read_status return integer is
begin
  return current_status; -- Reads external signal, so must be impure
end function;

This function mus be impure because it reads external signal that may change over time.

Code 5

impure function random_generator return integer is
  variable seed : integer := 1; -- Static variable retains value between calls
begin
  seed := (seed * 1103515245 + 12345) mod 256; -- Simple random algorithm
  return seed;
end function;

This function must be impure because it maintains internal state (seed) that changes on each call.

Module 7 - Procedure, Function, and Impure Function

Procedure, Function and Impure Function Synthesis

In VHDL, both "function" and "procedure" can be used in hardware descriptions, but it should be understood that hardware synthesis is usually more suitable for implementations based on deterministic and synchronous behavior. Therefore, there are several limitations on the use of functions and procedures in the context of synthesis:

So, while functions and procedures can be used in hardware descriptions and can be synthesized if they meet certain requirements, impure functions are usually not suitable for VHDL synthesis.

Module 7 - Procedure, Function, and Impure Function

Difference between Procedure, Function and Impure Function

Criteria Procedure Function Impure Function
Purpose Perform tasks without returning a value. Return a value from a calculation. Produce an unpredictable value or one that depends on external factors.
Arguments Can have input and/or output arguments. Can have input arguments only. Can have input arguments only.
Return Value Does not return a value (void). Returns a value from a calculation. Returns a value from a calculation.
Usage Used to organize tasks or operations. Used for calculations or data processing. Used when the function's result depends on external factors.
Example vhdl procedure SetFlag(flag: out boolean); vhdl function Add(a, b: integer) return integer; vhdl function RandomNumber return integer;
Synthesis Can be synthesized if it is deterministic and synchronous. Can be synthesized if it is deterministic and synchronous. Not suitable for synthesis because the result is unpredictable or depends on external factors.

Module 9 - Microprogramming

Module 9 - Microprogramming

1. Introduction: The Role of the Control Unit

Module 9 - Microprogramming

2. The Control Unit Dilemma: Hardwired vs. Microprogrammed

The fundamental problem of generating control signals, introduced in Section 1.0, is solved by two distinct design philosophies. This choice between a "hardwired" and a "microprogrammed" control unit represents a classic engineering trade-off between speed and flexibility.

Feature Hardwired Control (FSM) Microprogrammed Control
Implementation Sequential logic circuit (gates, flip-flops) Control Store (ROM) & Sequencer
Speed Very Fast (low propagation delay) Slower (extra memory access)
Flexibility Very Low. Difficult to modify. Very High. Can be updated (firmware).
Design Complexity High, error-prone, and complex to manage. Systematic, orderly, and easier to debug.
Best For RISC (Reduced Instruction Set Computers) CISC (Complex Instruction Set Computers)
Module 9 - Microprogramming

3. Principles of Microprogrammed Control

This section details the core theory of the microprogrammed control unit, the flexible alternative to the hardwired FSM. This approach fundamentally changes the design from a complex, fixed logic circuit to a simple, programmable one.

Module 9 - Microprogramming

4. The Micro-instruction

If the Control Store is the "recipe book" for the CPU, then a micro-instruction is a single "line" in that recipe. It is the most fundamental unit of control in a microprogrammed CPU, defining the exact set of hardware operations that will occur in a single clock cycle.

Module 9 - Microprogramming

5. Execution Flow and Sequencing

This section connects all the previous concepts to illustrate how the microprogrammed control unit runs a program. The core of this operation is the mapping of high-level assembly instructions to low-level micro-routines, all managed by the micro-sequencer.

Phase Micro-operation Keterangan
Fetch uAddr 0: PC_out, MAR_in (Fetch CC1) Send PC to memory.
uAddr 1: RAM_out, IR_in (Fetch CC2) Get instruction 00010011 into IR.
Decode uAddr 1: (Sequencer Logic) Sequencer sees 0001, maps it to uAddr 16.
Execute uAddr 16: IR_addr_out, MAR_in (Execute CC1) Get address 0011 from IR.
uAddr 17: RAM_out, R0_in, PC_inc (Execute CC2) Get data from RAM[0x30] into R0. Increment PC.
uAddr 17: (Sequencer Logic) Sequencer sees SEQ_FETCH, sets next_uPC = 0.

The following VHDL code is a complete, behavioral model of a hypothetical microprogrammed control unit. This example is simplified to clearly demonstrate the core concepts of sequencing, mapping, and conditional branching.

This code directly illustrates the concepts from sections 5.1, 5.2, and 5.3.

library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;

entity Hypothetical_Micro_CU is
    port (
        CLK         : in  std_logic;
        RST         : in  std_logic;
        
        -- Inputs from Datapath
        OPCODE_IN   : in  std_logic_vector(3 downto 0);
        Z_FLAG      : in  std_logic; -- (for conditional jumps)
        
        -- Outputs to a hypothetical 10-signal datapath
        PC_OUT      : out std_logic := '0';
        PC_INC      : out std_logic := '0';
        MAR_IN      : out std_logic := '0';
        RAM_OUT     : out std_logic := '0';
        RAM_IN      : out std_logic := '0';
        IR_IN       : out std_logic := '0';
        ACC_IN      : out std_logic := '0';
        ACC_OUT     : out std_logic := '0';
        TEMP_IN     : out std_logic := '0';
        ALU_OUT     : out std_logic := '0'
    );
end entity;

architecture Behavioral of Hypothetical_Micro_CU is

    -- 3.2.2 Micro-Sequencer
    -- This is the "micro-Program Counter" (uPC)
    signal uPC      : unsigned(7 downto 0) := (others => '0');
    signal next_uPC : unsigned(7 downto 0);
    
    -- 3.2.3 Micro-instruction Register (uIR)
    -- 10 control bits + 2 sequencing bits
    signal uIR : std_logic_vector(11 downto 0); 
    
    -- 4.2.2 Sequencing Field Constants
    constant SEQ_NEXT   : std_logic_vector(1 downto 0) := "00";
    constant SEQ_DECODE : std_logic_vector(1 downto 0) := "01";
    constant SEQ_FETCH  : std_logic_vector(1 downto 0) := "10";
    constant SEQ_HLT    : std_logic_vector(1 downto 0) := "11";
    
    -- 4.2 Format and Fields
    constant C_SEQ_BITS  : integer := 2;
    constant C_CTRL_BITS : integer := 10;
    constant C_UCODE_WIDTH : integer := C_SEQ_BITS + C_CTRL_BITS; -- 12 bits
    
    -- 3.2.1 Control Store (ROM)
    type t_control_store is array(0 to 255) of std_logic_vector(C_UCODE_WIDTH - 1 downto 0);

    -- Helper function to build a micro-instruction
    function to_ucode(
        ctrl : std_logic_vector(C_CTRL_BITS - 1 downto 0);
        seq  : std_logic_vector(C_SEQ_BITS - 1 downto 0)
    ) return std_logic_vector is
    begin
        return seq & ctrl; -- [Seq(1:0), Ctrl(9:0)]
    end function;

    -- Initialize the Control Store (ROM) with micro-routines
    function init_rom return t_control_store is
        variable rom : t_control_store := (others => (others => '0'));
        
        -- Control Field constants (10 bits)
        -- [PC_OUT, PC_INC, MAR_IN, RAM_OUT, RAM_IN, IR_IN, ACC_IN, ACC_OUT, TEMP_IN, ALU_OUT]
        constant C_FETCH1 : std_logic_vector(9 downto 0) := "1010000000"; -- PC_OUT, MAR_IN
        constant C_FETCH2 : std_logic_vector(9 downto 0) := "0001010000"; -- RAM_OUT, IR_IN
        constant C_LDA1   : std_logic_vector(9 downto 0) := "0010000000"; -- MAR_IN (assume from IR)
        constant C_LDA2   : std_logic_vector(9 downto 0) := "0101001000"; -- PC_INC, RAM_OUT, ACC_IN
        constant C_JUMP1  : std_logic_vector(9 downto 0) := "0000000000"; -- (Assume PCI/IRO logic)
        constant C_NOP1   : std_logic_vector(9 downto 0) := "0100000000"; -- PC_INC

    begin
        -- 5.2 Fetch Cycle
        rom(0) := to_ucode(C_FETCH1, SEQ_NEXT);   -- uAddr 0
        rom(1) := to_ucode(C_FETCH2, SEQ_DECODE); -- uAddr 1

        -- 5.1 Mapping Assembly to Micro-routines
        -- These are the "Execute" routines
        
        -- Map Opcode "0001" (LOAD_ACC) to uAddr 20
        rom(20) := to_ucode(C_LDA1, SEQ_NEXT);
        rom(21) := to_ucode(C_LDA2, SEQ_FETCH);

        -- Define targets for conditional branches
        rom(16) := to_ucode(C_NOP1, SEQ_FETCH);  -- uAddr 16 (NOP routine)
        rom(30) := to_ucode(C_JUMP1, SEQ_FETCH); -- uAddr 30 (JUMP routine)

        -- ... the rest of the ROM for other opcodes (ADD, STA, etc.) ...
        
        return rom;
    end function;

    -- Create the constant ROM
    constant Control_Store : t_control_store := init_rom;
    
    -- Define the jump targets for conditional branches
    constant uAddr_NOP_ROUTINE : unsigned(7 downto 0) := to_unsigned(16, 8);
    constant uAddr_JMP_ROUTINE : unsigned(7 downto 0) := to_unsigned(30, 8);

begin

    -- 1. Micro-Program Counter (uPC) Register (The "State")
    process(CLK)
    begin
        if rising_edge(CLK) then
            if RST = '1' then
                uPC <= (others => '0'); -- On reset, go to Fetch (uAddr 0)
            else
                uPC <= next_uPC;
            end if;
        end if;
    end process;

    -- 2. ROM Read (Concurrent)
    uIR <= Control_Store(to_integer(uPC));

    -- 3. Micro-Sequencer (Next uPC Logic)
    -- This process is the "brain" of the Control Unit
    process(uPC, uIR, OPCODE_IN, Z_FLAG)
        variable seq_control : std_logic_vector(1 downto 0);
    begin
        seq_control := uIR(C_UCODE_WIDTH - 1 downto C_CTRL_BITS);
        
        -- Default: always increment
        next_uPC <= uPC + 1;

        case seq_control is
            when SEQ_NEXT =>
                next_uPC <= uPC + 1;
                
            when SEQ_FETCH =>
                next_uPC <= (others => '0'); -- Go back to Fetch (uAddr 0)

            when SEQ_HLT =>
                next_uPC <= uPC; -- Halt by looping on this address

            when SEQ_DECODE =>
                -- This is the 5.2 Decode step
                -- It maps the opcode to a micro-routine address
                case OPCODE_IN is
                    when "0001" => -- LOAD_ACC
                        next_uPC <= to_unsigned(20, 8);
                    
                    -- ... other non-branching opcodes (ADD, STA, etc.) ...
                    
                    -- 5.3 Conditional Branching Logic
                    when "1010" => -- JUMP_IF_ZERO
                        if Z_FLAG = '1' then
                            next_uPC <= uAddr_JMP_ROUTINE;
                        else
                            next_uPC <= uAddr_NOP_ROUTINE;
                        end if;
                        
                    -- ... other conditional branches (JNE, JLT, etc.) ...

                    when others => 
                        next_uPC <= uAddr_NOP_ROUTINE; -- Invalid opcode
                end case;

            when others =>
                next_uPC <= (others => '0'); -- Safe state
        end case;
    end process;

    -- 4. Output Logic (Concurrent)
    -- Map the 10 control bits from the uIR to the output ports
    PC_OUT  <= uIR(9);
    PC_INC  <= uIR(8);
    MAR_IN  <= uIR(7);
    RAM_OUT <= uIR(6);
    RAM_IN  <= uIR(5);
    IR_IN   <= uIR(4);
    ACC_IN  <= uIR(3);
    ACC_OUT <= uIR(2);
    TEMP_IN <= uIR(1);
    ALU_OUT <= uIR(0);

end Behavioral;

Module 10 - Final Project

Module 10 - Final Project

Final Project Guide

Congratulations! 🥳

In this final module, you are given the opportunity to create a project with your group members according to the following provisions:

Final Project Timeline


Final Project Criteria


Final Project Grading Weight


Files and Submission Location


Example Final Project Ideas*

*Only use these three examples as a reference or for idea inspiration. Plagiarism is prohibited.