hardware descriptions

Object oriented design in synthesizable VHDL

Here I talk about how to create reusable VHDL code through object oriented design principles. Object oriented VHDL design is actually just a set of design patterns which we can use to divide our code into small self contained and reusable objects that provide some meaningful functionality. The best part of using these design patterns is that you don’t need to figure out new tools or languages that might be tool or vendor specific. We can accomplish object orientation just with standard VHDL but with slight adjustment in our way of thinking about the code. Since the important language features were already part of VHDL93 standard, even some legacy tools like Xilinx ISE supports them.

In object oriented languages, an object is commonly implemented in a data type that has both data and functions under one named construct called class. Object is then the instantiation of this class. What this class actually is, is just a form of abstract data type of composite elements and functions that act on this composite data type. In VHDL this abstract data type of composite elements is a record and by extension our VHDL object is a signal of this record type with the functionality implemented in a procedure that takes this object as argument.

The ideas presented here are explained with an example of a simple led blink. For more substantial design with object oriented VHDL, see any of the other solutions I have written about. Here we also only concentrate on synthesizable subset of VHDL.

Object oriented logic design

An object in this context is a piece of code delivered in one or more packages. This package contains a record hodling the which when  a collection of related registers, a procedure that creates some functionality using this record and a collection of functions to access this created functionality. There are many benefits in making our code into distinct objects. Just by referring a block of code as an object makes its purpose and scope easier to understand. A multiplier object multiplies, divider object divides, sincos object performs sine and cosine calculations and an UART provides access to simple communications link.

Objects in code should have easily distinguishable and bounded context. We should have one object for one thing to make it easier identify code that can be reused. For an example, a multiplication is needed in a divider so it is obvious that divider should have access to one and all of the functionality needed from the multiplier object should be delivered by the multiplier object.

Building larger objects from smaller ones lets us design incrementally with small steps. This is undeniably the most important design principle as it allows us to build new solutions from the ones we have already created. By dividing our code into objects it is easy to make meaningful progress in small parts that could be used outside some big feature to which they are developed to.

Developing code into small objects is a great way to increase the level of abstraction of the code. When we use the the interface of an object we can think of requesting actions and receiving messages instead of thinking about bits, shifts or registers that we are actually implementing these messages and requests with. This greatly increases the efficiency and descriptiveness of our code and its readability all of which promote reuse.

Developing Objects in VHDL

There are two main types of constructs in VHDL that can be developed as distinct objects. The first is the component instantiation of an entity with signals of record types on its port. The second is a signal from a record with a procedure that holds the logic that uses the registers defined in this record. These are outlined next.

Object from component instantiation of an entity

As indicated by the declaration of an entity as a component in the application code, the entity is an entire functionally complete design. Most entities have their own clocked processes inside them and many instantiate other components from entities. The port of an entity is very strong form of encapsulation as the functionality behind the port is only accessible is the through the signals in the port. Thus entities are used always with the port signals as interface.

Declaring a component from an entity at the minimum requires the component instantiation, naming the instance, adding a clock and the signals that are going into and out from the port as well as instantiating the signals and setting clock signals to them. A minimal component instantiation from of an uart is shown in the code snippet below. The init_uart procedure initializes the uart object to be used in the process named “test”.

architecture minimal_object_from_entity of example is

    signal uart_clocks   : uart_clock_group       ;
    signal uart_FPGA_in  : uart_FPGA_input_group  ;
    signal uart_FPGA_out : uart_FPGA_output_group ;
    signal uart_data_in  : uart_data_input_group  ;
    signal uart_data_out : uart_data_output_group ;
    
begin
------------------------------------------------------------------------
    test : process(clock)
    begin
        if rising_edge(clock) then
            init_uart(uart_data_in);
        end if;
    end process;
------------------------------------------------------------------------
    uart_clocks <= (clock => clock);

    u_uart : uart
    port map( uart_clocks ,
    	  uart_FPGA_in    ,
    	  uart_FPGA_out   ,
    	  uart_data_in    ,
    	  uart_data_out);
------------------------------------------------------------------------
Object from a record-procedure pair

The other way to do objects in VHDL code is by using a record for defining the registers and then using a procedure to create the logic that creates the object using these registers. Instantiating an object from a record requires only two lines of code. One for the signal of module record type in the declarative region of architecture and another for call to the create_<module> procedure in the process. This can be seen in the snippet below.

architecture minimal_object_from_register of example is

    signal multiplier_object : multiplier_record := init_multiplier;
    
begin
------------------------------------------------------------------------
    test : process(clock)
    begin
        if rising_edge(clock) then
            create_multiplier(multiplier_object);
        end if;
    end process;
Access type of the entity and record objects

The main difference in these two methods is with the access to internal registers. With component from an entity, we have an interface structure in the port where the design in the architecture is used to implement this interface. The accessible part outside the objects architecture is only the port. The implementation of the architecture is not accessible outside of the port.

With record-procedure pair as an object, the registers defined by the record are possible to be interfaced with different functions and procedures. Thus the object from record-procedure structure behaves like a object with ‘public’ access to the objects internal registers and the component from an entity behaves like an object with ‘private’ registers defined in the architecture of the entity.

In order to promote reuse of code the internal structure of an architecture should be built from the record objects. This way parts of the architectural implementation can be used outside of the architecture to which they are developed. Since the big idea is to make arbitrarily small objects, this time we concentrate on the objects from record-procedure pair.

Implementation of a record-procedure pair as object in VHDL

The basic VHDL object has three main parts, a procedure that creates the functionality of the object, the record type signal that defines the registers for this procedure and an interface.  The interface is a set of functions and procedures with which the object is used. This structure is shown in Figure 1.

Figure 1. structure of basic object in vhdl, the record type holds the registers and logic that modifies the registers contents is defined in procedure

 

The object_record has all of the registers required to perform the objects action. Typically this includes the states of a state machine, typically just a counter, the input and output registers and possibly some stored intermediate values that are needed internally by the object. I use a naming convention that the record is <object>_record and the procedure is create_<object>. For ease of instantiation I always include an init_<object> constant for initializing the record.

The object is delivered in a <object>_pkg.vhd source file in which the record and the interface functions are defined. The package body can be in the same source file or in a separate package_body .vhd file. The VHDL package-package_body pair corresponds to .hpp and an associated .cpp files in C++.

The big idea here is that once we have designed some object, we can the use this object in functions and procedures. Since VHDL allows nesting function and procedure calls we can use the object through its interface in another procedure. The same applies to records also, thus we can use records in records and procedures in procedures. This is what allows building objects from other objects.

Design procedure for reuse through object oriented design

The code is developed with first creating a test bench, then adding a hacky quickfix version of the required functionality to it. This hack is then refactored into objects and its methods. Then these objects are used as parts of new hacky pieces of code that are then again refactored. In the realm of test-driven development this is referred with red-green-refactor mantra.

Since we always create a test bench first when a new object is designed, this process leaves us with a trail of breadcrumbs in the form of test benches for all of the intermediate objects also. These test benches are in effect an always up-to-date documentation on how the code is used. Since the code is designed to be reused from the ground up, by having the functionality in the objects package that is developed alongside the test bench for it we have only a limited part of the code actually without any simulation. Also we can add further tests when we find new ways in which the code is used. Adding tests helps to prevent the application from breaking when we eventually do change a specific part of the code.

The design procedure is highlighted next in a walk through of a led blinker design

Example : Led blinker design

The led blinker is the hello world of embedded systems and is very likely the first thing that is going to be tested whenever a new hardware is brought up for the first time.

I also made a pair of videos showcasing the object oriented counter design with the GHDL, Gtkwave vunit combination. These can be found in the video section

We start with simple down counter for simplest possible example on object oriented design with VHDL. The logic is just to count down if value is larger than zero.

if counter > 0 then
    counter <= counter - 1;
end if;

We can start this counter by assigning any value to the counter register. We will arbitrarily set this to 10 if counter is not running.

-- start if counter has stopped
if counter = 0 then
    counter <= 10;
end if;
    

The first thing that we need  is a signal that tells when the countdown is ready. For this we will add a boolean to check when counting_has_completed. This value is set when the counter is at 1 thus it will be true only for one clock cycle when counter is 0. We use this signal instead of just checking for counter = 0 as this signal is true for only when the counter is ready. Checking for counter = 0 indicates that the counter is not running.

if counter > 0 then
    counter <= counter - 1;
end if;

if counter = 1 then
    counting_has_completed <= true;
else
    counting_has_completed <= false;
end if;
    

Next we will create an object from this counter. In order to do that we require two things, the counter_object_record which holds the registers and a procedure for creating functionality of a counter from the registers in the record. We also should always add a constant to initialize the signal of this record type.


package counter_pkg is
------------------------------------------------------------------------
    type counter_object_record is record
        counter : natural range 0 to 2**16-1;
        counting_has_completed : boolean;
    end record;
    
    constant init_counter : counter_object_record :=(
        counter => 0, 
        counting_has_completed => false);
        
    procedure create_counter (
        signal counter_object : inout counter_object_record);

The functionality is then moved into a procedure called create_counter

procedure create_counter 
    (
        signal counter_object : inout counter_object_record
    )
    is 
    begin
        if counter > 0 then
            counter <= counter - 1;
        end if;
        
        if counter = 1 then
            counting_has_completed <= true;
        else
            counting_has_completed <= false;
        end if;
end create_counter;

The minimal interface is completed with a procedure for requesting the counter and a function to check if counter_is_ready. We will also add a function to check if counter_is_not_running. These two functions and the request_counter procedure are the interface to this object.

procedure request_delay 
    (
        signal counter_object : inout counter_object_record;
        counter_value : in natural 1 to 2**16-1
    )
    is 
    begin
        counter_object.counter <= counter_value;
end request_delay;

function counter_is_ready
( 
    counter_object : counter_object_record
)
return boolean
is
    return  counter_object.counting_has_completed;
end request_delay;
function counter_is_not_running
( 
    counter_object : counter_object_record
)
return boolean
is
    return  counter_object.counter = 0;
end counter_is_not_running;

The object can now be created with instantiating a signal from the record and creating the functionality with call to the create_counter procedure as seen below. The code snippet creates a 10 to 0 counter that returns true once every 11 clock cycles.

-- in architecture    
    signal counter : counter_object_record := init_counter;
begin

    test_counter : process(clock) is
    begin
        if rising_edge(clock) then
            create_counter(counter);
            
            if counter_is_not_running(counter) then
                request_counter(counter, 10);
            end if;
            
        end if;
    end process;

This type of logic is commonly used for creating a time base for periodically performing some action. We could always copy this type of logic every time we need a time base, but we can also create another method for this.

 Since VHDL allows nesting procedure calls we can create this new method of interfacing our counter object using the interface of the counter object. In practice we simply move the code from the process shown above into its own procedure. Since we are creating a timebase from a counter object, we will name this procedure create_timebase_from and the value from which we count zero from is called count_to_zero_from.

procedure create_timebase_from 
    (
        signal counter_object : inout counter_object_record;
        count_to_zero_from : natural 1 to 2**16-1
    )
    is 
    begin
        create_counter(counter);
            
        if counter_is_not_running(counter) then
            request_counter(counter, count_to_zero_from);
        end if;
end create_timebase_from;

The idea behind these funny names is that reading the code out loud tells what it is doing. It is also very easy to figure out descriptive names for these since the names read out like a sentence that describes its use. So here we create_timebase_from a counter object which counts down from count_to_zero_from which set to value 10. You shouldn’t need a comment to explain what this is doing.

-- in architecture    
    signal counter : counter_object_record := init_counter;
begin

    test_counter : process(clock) is
    begin
        if rising_edge(clock) then
            create_timebase_from(
                counter_object => counter,
                count_to_zero_from => 10);
        end if;
    end process;

It might seem wasteful to create a new method just to replace one if statement, but it only takes less than 20 lines of code to do so and it saves 2 to 3 lines from the application code, which is the test bench in this case. Since we are striving for reusable code, we only ever need to use this 6 to 10 times before we have actually saved in total number of written lines and it is significantly more understandable. Writing the procedure with these extra lines of code also needs to prevent one bug once in ever for it to save time easily 100 fold or more compared to how long it took to write it!

Led blinker synthesis to FPGA

I use two evaluation kits for this the Efinix Trion evaluation kit as well as an Intel Cyclone 10lp evaluation kit. Both kits have 4 user leds so we create 4 led blinkers with different blink rates using the cascaded counters.

We also commonly need much longer delays than is reasonable to create with a simple counter. For example if we wish to blink a led at the rate of one second, at 120Mhz clock we would be counting to 120 million, or if we wanted a 4 minute timer, we would need to count to 28 billion.  Simple solution to this we use cascaded counters.  The cascaded counter functions such that another counter is decremented as the previous counter is ready. This is shown here

-- in architecture    
    signal counter : counter_object_record := init_counter;
    signal slow_counter : counter_object_record := init_counter;
begin

    test_counter : process(clock) is
    begin
        if rising_edge(clock) then
            create_timebase(counter, 10);
            if counter_is_ready(counter) then
                create_timebase(slow_counter, 10);
            end if;
        end if;
    end process;

We first create the led blinker object. This is going to be called led_blinker. The led blinker has two counters and led state and a create led blinker procedure.

    type led_blinker_record is record
        fast_counter : counter_object_record;
        slow_counter : counter_object_record;
        led_state    : std_logic;
    end record;
    
    constant init_led_blinker : led_blinker_record := (init_counter, init_counter, '0');

    procedure create_led_blinker (
        signal led_blinker_object : inout led_blinker_record;
        signal led_out : out std_logic;
        slow_counter_value : in natural range 1000 to 15e3);

Additionally this package defines two array types for the led blinker object since we are creating a several of these.



    type led_array is array (integer range <>) of led_blinker_record;
    type int_array is array (integer range <>) of integer;

The led blinker is placed in it’s own component called led blinker main. This component creates 4 led blinkers all with their own counter values defined in the counter_values constant.

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

library work;
    use work.led_blinker_pkg.all;
    use work.led_blinker_main_pkg.all;

entity led_blinker_main is
    port (
        clk_120MHz : in std_logic;
        led_blinker_main_FPGA_out : out led_blinker_main_FPGA_output_group 
    );
end entity led_blinker_main;

architecture rtl of led_blinker_main is

    alias leds is led_blinker_main_FPGA_out.leds;

    signal led_blinker_array : led_array(leds'range) := (init_led_blinker, init_led_blinker, init_led_blinker, init_led_blinker);
    constant counter_values : int_array(leds'range) := (4000, 6000, 8000, 2000);

begin

    led_blinker : process(clk_120MHz)
    begin
        if rising_edge(clk_120MHz) then

            create_led_blinker(led_blinker_array(0), leds(0), counter_values(0));
            create_led_blinker(led_blinker_array(1), leds(1), counter_values(1));
            create_led_blinker(led_blinker_array(2), leds(2), counter_values(2));
            create_led_blinker(led_blinker_array(3), leds(3), counter_values(3));

        end if; --rising_edge
    end process led_blinker;	

end rtl;

I have also used the ‘range attribute to define the array ranges. This is done to allow for simple syntax checking to catch mismatch between the number of leds present in the board and the number of led blinkers we have created for blinking them. Due to a quirk of VHDL we cannot put the create_led_blinkers in a loop as arguments to procedure calls need to have static names.

This led blinker module is then routed out of the fpga design. The top module needs to be separately created for cyclone and for the efinix trion as the pll clock enters the design through the port of the top module in efinix instead of being a separate ip that is instantiated. Since the blinker is its own module, only the top module needs to be separate and both boards use the same led_blinker_main entity

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

library work;
    use work.led_blinker_main_pkg.all;

entity efinix_top is
    port (
        clk_120Mhz : in std_logic;
        uart_rx    : in std_logic;
        uart_tx    : out std_logic;
        leds       : out std_logic_vector(3 downto 0)
    );
end entity efinix_top;

architecture rtl of top is

    signal led_blinker_main_FPGA_in : led_blinker_main_FPGA_input_group;
    signal led_blinker_main_FPGA_out : led_blinker_main_FPGA_output_group;

begin

    leds <= led_blinker_main_FPGA_out.leds;
    led_blinker_main_FPGA_in <=(uart_FPGA_in=>(uart_transreceiver_FPGA_in =>(uart_rx_FPGA_in =>(uart_rx => uart_rx))));
    uart_tx <= led_blinker_main_FPGA_out.uart_FPGA_out.uart_transreceiver_FPGA_out.uart_tx_FPGA_out.uart_tx;

    u_led_blinker_main : led_blinker_main
    port map( clk_120MHz, led_blinker_main_FPGA_in, led_blinker_main_FPGA_out);

end rtl;

The Intel top module has the led_blinker_main_FPGA_input/output records routed to the port of the top module. This is greatly beneficial since it allows the io signals to be defined in the lower parts of the architecture. This is slightly more convenient as it prevents the need to first route the io from the top module port to io pins and additionally from the design into the top module port signals.

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

library work;
    use work.led_blinker_main_pkg.all;

entity cyclone_top is
    port (
        clk50mhz : in std_logic;
        led_blinker_main_FPGA_in : in led_blinker_main_FPGA_input_group;
        led_blinker_main_FPGA_out : out led_blinker_main_FPGA_output_group
    );
end entity cyclone_top;

architecture rtl of top is

    component main_pll IS
        PORT
        (
            inclk0 : IN STD_LOGIC  := '0';
            c0     : OUT STD_LOGIC
        );
    END component;

    signal clk_120MHz : std_logic; 

begin

    u_main_pll : main_pll
    port map(clk50mhz, clk_120MHz);

    u_led_blinker_main : led_blinker_main
    port map( clk_120MHz, led_blinker_main_FPGA_in, led_blinker_main_FPGA_out);

end rtl;

The video below shows the rather erradic looking led blinkers as every led blinks at different rate. Since blink rates are an integer multiple of each other, the blinking does have a period of around 4 seconds.

Notes

The design presented here was a simple led blinker but developed incrementally. Although the design here is a bit contrived, it does highlight the best design method that I know of. We start small and add more required functions little by little. While doing this we leave behind usable pieces of code and its associated tests. Even if these tests do nothing else but just run the code, they are still incredibly useful when parts of the code is reused again in some other future solution that we are developing.

We should also have a script for running all of the projects tests as we move forward since this allows us spot a mistake hopefully as soon as one pops up. I highly recommend Vunit with GHDL for running tests and GTKWave for visualizing the traces as this combination allows running many tests in parallel and GHDL is blindingly fast. Vunit is painful to figure out but is very extremely much worth the effort! I will eventually update the software setup tutorial to include Vunit also.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top