Stage 4: Writing the FPGA design in VHDL

So we’ve finally made it to the finale in our series on designing with FPGA’s. Following on from our previous article where we implemented the software functions we need in order to be able to reprogram our FPGA easily, we can now get into the nitty-gritty of programming the FPGA to do what we want.

Firmware with a difference

As mentioned briefly in our first article, FPGA’s are not like your common microcontroller. A microcontroller has a processor core that executes instructions sequentially that are stored in on board memory. They’re really good at logically complex sequential operations but are much harder to constrain time wise. Unless you’re using interrupts/hand-crafted assembler then you can probably argue that they’re also non-deterministic, meaning that you don’t know when exactly something is going to be performed. This is due to the other workload the microcontroller has to work through first.

The FPGA on the other hand is an array of logic which you can specify via an HDL to become the logical function you need. Unlike Microcontrollers, FPGA’s excel at more simplistic, high speed parallel tasks. They’re also much more deterministic and are fantastic at doing tightly constrained real time operations and calculations. FPGA’s are consistently used to implement non-standard protocols, tightly controlled DSP calculations and tasks that require parallelizable functions to be performed. They do however struggle with algorithmically complex functions, requiring a lot of logic resource in order to do so.

Writing the VHDL required to get an FPGA working is a very different beast to writing the C or assembler for a microcontroller. When you write VHDL you constantly have to think about how all the logic you’re defining will work with other parts of your design on a real time, concurrent basis. It’s a very different mindset from writing any other language.  You also want to be as far removed from device primitive instantiations as much as possible so that you can make good use of the modularity VHDL can provide (although sometimes you can’t get around them).

Debugging/Verifying VHDL is also very different. In C you can run simulators or debug on the platform by stepping through your code with a debugging tool. In VHDL you rely on using test benches and bus functional models to simulate and verify the majority of the functionality of your design. Only very rarely would you resort to using the equivalent debugging tool which is an IP block that captures the states of certain pre-specified signals for download via a programmer/debugger tool. A typical industry setup for designing FPGA’s usually has two or three verification engineers to each design engineer, demonstrating how much more verification work goes into an FPGA than that which would happen on a typical microcontroller.

VHDL Simulation Waveforms

VHDL Simulation Waveforms used in Informal Verification

Starting Somewhere

There are a lot of different tasks when it comes to designing an FPGA with VHDL. Some of the most frequent are:

  • Doing the design in VHDL
  • Writing informal test benches for the design in VHDL
  • Making the verification solution (which is different from an informal test bench!) in a scripted language
  • Checking timing/using static timing analysis
  • Checking for clock domain crossing/meta-stability/synchronisation etc…

The list of things to do very much depends on how complex your design is. If you’re making a design which is “low speed” (usually below some tens of megahertz)  then you can usually get away with not doing as many timing checks. If you’re only ever using one clock domain (read as just using one clock to run the whole design) then you can avoid clock domain crossing checks. You can also get away with only doing informal test benches if what you are doing if very simple or is not going into a product that needs to be reliable (if they even exist). The list of things to do is very much tailored to the requirements.

In our case, we just want to do some extremely simple stuff. This means we can actually skip test benches, timing checks and other checks all together. All we plan on doing is writing some really simple VHDL in order to show the basics of FPGA design. At some point in the near future, we will write another series on “high speed” FPGA design and verification because that is a whole topic in itself.

Get the tools

We mentioned in previous articles that there are a few main vendors in the FPGA market, with Xilinx and Altera being the biggest. Each of these vendors provides their own software tools in order to design and synthesise FPGA logic. Because we’re focusing on a Xilinx part, we’re going to be using the Xilinx tool set in order to do our design.

Xilinx has two main tools that you can use to design FPGA’s: ISE and Vivado. Vivado is aimed at their much newer line of FPGA’s, more specifically their “7 Series” and above e.g. Artix 7, Kintex 7 and Virtex 7. ISE is aimed at all FPGA’s historically before this, including our humble Spartan 3A. This means that we need to use Xilinx ISE in order to create our design. At the time of writing this (and for probably most of the future now as ISE is no longer in development), the version of ISE I’m going to use is the 64 bit version of 14.7.

ISE has pretty much most of the functionality you will need in order to develop a basic FPGA. The IDE has an built in text editor, GUI windows for the FPGA workflow and also a way to launch iSim, their basic FPGA simulation tool. If we were to set up this design to be built from an automated platform or other synthesizer tools/simulators were to be used then the tool-chain becomes trickier to set up. As we quickly mentioned before though, we’re all good in this instance. ISE can be downloaded straight from the Xilinx website here.

If you chose to go down a different route of programming your FPGA in your design (e.g. on-board SPI flash) then you would also need some programming hardware. Xilinx provides its Platform Cable to aid in the JTAG programming process you would need to perform. In our instance, we covered in the previous article about programming the FPGA that we are instead using a different method to do this. As a side note: I personally never understand why programming cables cost so much and are proprietary to each vendor. This goes for all FPGA vendors and MCU vendors alike.

What’s the design?

It’s always a good idea to know what sort of design you want to make before you begin doing anything. Here at Circuithinking, we plan our systems to ensure that we go into designs prepared and with a target goal and architecture in mind. Preparation for designs is key to making something that is modular and coherent. Not preparing properly in this way can lead into really nasty uninterpretable code. Although we won’t follow the process in this article, we’ll still use a cut-down plan as the framework for when we start coding.

Because the design has been made with breadboard connectivity in mind, we want something simple we can connect up on a breadboard. We also want something that, for the purposes of exhibition, is visual. For those reasons, we’ll go with an LED design. The idea will be to have LED’s dimming in a sequence using pulse width modulation. We can use 8 LED’s with the same driver stamped out for each LED, but with a time offset built into the design so that it will look as if the LED’s are scrolling through brightness one after another. This is one of the best things about VHDL, FPGA’s and I suppose code in general: modularity. If you’ve done it properly once, you can copy and paste to do it many times again.

In deciding an architecture for the code it’s always best to try and group together repeated functions. In our case, the LED driver that suits one LED will work for all so we need just the one LED driver block. We also need some top-level logic to tie these things together. This should be some sort of counting LED controller that gives each LED it’s value of brightness. This block can then control both the rate of dimming easily and the offsets required to give the scrolling pattern.

So for out simple project, that’s the plan completed! Now we need to get into the nitty-gritty of designing the thing.

Make the project

Now that we have ISE installed and an idea of what we want, we can create a project for our design. In order to do this we just follow the wizard for the new project setup. You must specify a project location and also set some settings for the project. Below is a snapshot of the settings used for the current project. In this instance we’re leaving the majority of settings the same as the defaults. The only part that we are changing is the device, package and speed grade to mach that of our chosen FPGA. After this, we want to whizz through the rest of the menus and get to our fresh project in the main window.

ISE Settings

ISE Project Settings for Spartan 3A

After that we need to add some sources. By using the ‘Project -> New Source’ menu option, we can add two VHDL files: one to control all the LED drivers as our ‘Top-Level’ block and one to describe the LED driver. In our case, I have labelled these as ‘top.vhd’ and ‘led_dimmer_control.vhd’. We also want to make something called a constraints file with the suffix of ‘.ucf’. This will tell ISE what signals we want to go to what pin and also what IO standard they use i.e. are they a 3.3V signal or an LVDS pair. The syntax of the .ucf file can be found here.

Write the VHDL

So now we have a project set up and ready to go and we can write some VHDL to describe our desired functionality. We can start with the LED driver. We want something that takes a clock signal and some sort of brightness value we want the LED to be, and then drive a single IO line to an LED in accordance with the brightness parameter. As a side note, we want the brightness value to have at least 12 bits of resolution because of something called the Weber-Fechner Law. This basically boils down to meaning the human eye has a non-linear interpretation of brightness. To really boil it down, basically if we had 8 bits or 255 steps of brightness then when we increase the brightness it would look “steppy” rather than “smooth”. Making the resolution 12 bits gets us around this issue in most cases.

This information gives us enough to make the entity declaration in VHDL. The entity declaration basically describes what the components inputs/outputs are to the outside world. The code snippet below shows us what we’re after.

entity led_dimmer_control is
    Port ( 
				--Inputs to IP
				CLK_24M : in   STD_LOGIC;
				LED_PWM_VALUE : in STD_LOGIC_VECTOR(11 downto 0);
				--Outputs to LEDs
				LED: out  STD_LOGIC
				);
end led_dimmer_control;

The CLK_24M signal is a 24MHz clock that we receive from the PIC, the LED_PWM_VALUE is the brightness setting for the LED and the LED signal is the output to the LED’s high side.

The function of the driver should count the 24MHz clock up to 12 bits full scale (4095). When it is less than the LED_PWM_VALUE it should turn the LED on and when it’s more it should turn the LED off. When it has reached its max value of 4095 then it should reset and start again counting up from 0. This will imitate the PWM function we require. Below is a code snippet of VHDL that describes just that along with the entity declaration from before. This forms the whole LED driver and we’re ready to implement multiples of these in our main control block.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity led_dimmer_control is
    Port ( 
        --Inputs to IP
        CLK_24M : in   STD_LOGIC;
        LED_PWM_VALUE : in STD_LOGIC_VECTOR(11 downto 0);
        --Outputs to LEDs
        LED: out  STD_LOGIC
        );
end led_dimmer_control;

architecture Behavioral of led_dimmer_control is

begin

  process(CLK_24M)
    variable clock_counter : integer := 0;
  begin
    if rising_edge(CLK_24M) then
      if (clock_counter <= to_integer(unsigned(LED_PWM_VALUE))) then
        LED <= '1'; elsif (clock_counter >= 4095) then
        clock_counter := 0;
        LED <= '0';
      else
        LED <= '0';
      end if;
      
      clock_counter := clock_counter + 1;
      
    end if;
  end process;
  
end Behavioral;

Now we have to define some logic that holds all of this together. We also have to define what the design drives externally at this level as well. Fundamentally, the design takes in a 24MHz clock and drives 8 Red LED’s on our breadboard as well as 2 Yellow LED’s on the FPGA board itself. There would typically be a reset button or signal as well but we don’t need one for this design. Using the same principle as above for our entity we are left with the code snippet below as our top-level entity.

entity top is
    Port ( 
        CLK_24M			: in   STD_LOGIC;
        LED_1 			: out  STD_LOGIC;
        LED_2 			: out  STD_LOGIC;
        RED_LED_ARRAY	: out  STD_LOGIC_VECTOR(7 downto 0)
        );
end top;

We also need to define some registers that we can manipulate for the LED brightness values and define our multiple copies of LED drivers. The first snippet below shows the definition of the registers and the defintion of the LED driver in the controller. The second snippet shows the instantiation of the LED driver definition 8 times for our 8 LED’s.

COMPONENT led_dimmer_control
PORT(
  CLK_24M : IN std_logic;
  LED_PWM_VALUE : IN std_logic_vector(11 downto 0);          
  LED : OUT std_logic
  );
END COMPONENT;

signal led_0_brightness_s : std_logic_vector(11 downto 0);
signal led_1_brightness_s : std_logic_vector(11 downto 0);
signal led_2_brightness_s : std_logic_vector(11 downto 0);
signal led_3_brightness_s : std_logic_vector(11 downto 0);
signal led_4_brightness_s : std_logic_vector(11 downto 0);
signal led_5_brightness_s : std_logic_vector(11 downto 0);
signal led_6_brightness_s : std_logic_vector(11 downto 0);
signal led_7_brightness_s : std_logic_vector(11 downto 0);
Inst_led_0_dimmer_control: led_dimmer_control 
PORT MAP(
  CLK_24M => CLK_24M,
  LED_PWM_VALUE => led_0_brightness_s,
  LED => RED_LED_ARRAY(0)
);

Inst_led_1_dimmer_control: led_dimmer_control 
PORT MAP(
  CLK_24M => CLK_24M,
  LED_PWM_VALUE => led_1_brightness_s,--X"3FE",
  LED => RED_LED_ARRAY(1)
);

Inst_led_2_dimmer_control: led_dimmer_control 
PORT MAP(
  CLK_24M => CLK_24M,
  LED_PWM_VALUE => led_2_brightness_s,--X"5FD",
  LED => RED_LED_ARRAY(2)
);

Inst_led_3_dimmer_control: led_dimmer_control 
PORT MAP(
  CLK_24M => CLK_24M,
  LED_PWM_VALUE => led_3_brightness_s,--X"7FC",
  LED => RED_LED_ARRAY(3)
);

Inst_led_4_dimmer_control: led_dimmer_control 
PORT MAP(
  CLK_24M => CLK_24M,
  LED_PWM_VALUE => led_4_brightness_s,--X"9FB",
  LED => RED_LED_ARRAY(4)
);

Inst_led_5_dimmer_control: led_dimmer_control 
PORT MAP(
  CLK_24M => CLK_24M,
  LED_PWM_VALUE => led_5_brightness_s,--X"BFA",
  LED => RED_LED_ARRAY(5)
);

Inst_led_6_dimmer_control: led_dimmer_control 
PORT MAP(
  CLK_24M => CLK_24M,
  LED_PWM_VALUE => led_6_brightness_s,--X"DF9",
  LED => RED_LED_ARRAY(6)
);

Inst_led_7_dimmer_control: led_dimmer_control 
PORT MAP(
  CLK_24M => CLK_24M,
  LED_PWM_VALUE => led_7_brightness_s,--X"FFE",
  LED => RED_LED_ARRAY(7)
);

The last main bit of code we need is a process that sets the brightness of each LED over time. This process is shown below:

led_test_brightness_process: process(CLK_24M)
  constant PWM_MAX : integer := 4094;
  constant FAST_TICK : integer := 19999;
  variable fast_counter_v : integer := 0;
  variable counter_0_v : integer := 0;
  variable counter_1_v : integer := 512;
  variable counter_2_v : integer := 1023;
  variable counter_3_v : integer := 1535;
  variable counter_4_v : integer := 2047;
  variable counter_5_v : integer := 2559;
  variable counter_6_v : integer := 3071;
  variable counter_7_v : integer := 3583;
begin
  if rising_edge(CLK_24M) then
    if (fast_counter_v >= FAST_TICK) then
    
      if (counter_0_v >= PWM_MAX) then
        counter_0_v := 0;
      else
        counter_0_v := counter_0_v + 1;
      end if;
      
      if (counter_1_v >= PWM_MAX) then
        counter_1_v := 0;
      else
        counter_1_v := counter_1_v + 1;
      end if;
      
      if (counter_2_v >= PWM_MAX) then
        counter_2_v := 0;
      else
        counter_2_v := counter_2_v + 1;
      end if;
      
      if (counter_3_v >= PWM_MAX) then
        counter_3_v := 0;
      else
        counter_3_v := counter_3_v + 1;
      end if;
      
      if (counter_4_v >= PWM_MAX) then
        counter_4_v := 0;
      else
        counter_4_v := counter_4_v + 1;
      end if;
      
      if (counter_5_v >= PWM_MAX) then
        counter_5_v := 0;
      else
        counter_5_v := counter_5_v + 1;
      end if;
      
      if (counter_6_v >= PWM_MAX) then
        counter_6_v := 0;
      else
        counter_6_v := counter_6_v + 1;
      end if;
      
      if (counter_7_v >= PWM_MAX) then
        counter_7_v := 0;
      else
        counter_7_v := counter_7_v + 1;
      end if;
      
      fast_counter_v := 0;
    else
      fast_counter_v := fast_counter_v + 1;
    end if;
 
    led_0_brightness_s <= std_logic_vector(to_unsigned(counter_0_v, 12));
    led_1_brightness_s <= std_logic_vector(to_unsigned(counter_1_v, 12));
    led_2_brightness_s <= std_logic_vector(to_unsigned(counter_2_v, 12));
    led_3_brightness_s <= std_logic_vector(to_unsigned(counter_3_v, 12));
    led_4_brightness_s <= std_logic_vector(to_unsigned(counter_4_v, 12));
    led_5_brightness_s <= std_logic_vector(to_unsigned(counter_5_v, 12));
    led_6_brightness_s <= std_logic_vector(to_unsigned(counter_6_v, 12));
    led_7_brightness_s <= std_logic_vector(to_unsigned(counter_7_v, 12));
    
  end if;
end process;

In this process, there are 8 individual LED brightness counters that all count up when the counter that counts the number of 24MHz clock cycles that have happened reaches the “FAST_TICK” value (19999). When the LED brightness counters reach their max values then they reset the brightness back to 0.  The bottom few statements all map these integer counter values into std_logic_vector type (which basically means bits/binary rather than a decimal integer). In future articles we will discuss types further in VHDL and show the perks and challenges of using this heavily-typed language. It’s also worth mentioning that each of these counter values are set in ascending multiples of 512 when the process begins. This means enables the offset we need in order to get the “strobing” look that we want.

After we add all of this together with some declarations to set the yellow LED’s to just be on, we get the finished code below for our second VHDL file.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity top is
    Port ( 
        CLK_24M			: in   STD_LOGIC;
        LED_1 			: out  STD_LOGIC;
        LED_2 			: out  STD_LOGIC;
        RED_LED_ARRAY	: out  STD_LOGIC_VECTOR(7 downto 0)
        );
end top;

architecture Behavioral of top is

  COMPONENT led_dimmer_control
  PORT(
    CLK_24M : IN std_logic;
    LED_PWM_VALUE : IN std_logic_vector(11 downto 0);          
    LED : OUT std_logic
    );
  END COMPONENT;
  
  signal led_0_brightness_s : std_logic_vector(11 downto 0);
  signal led_1_brightness_s : std_logic_vector(11 downto 0);
  signal led_2_brightness_s : std_logic_vector(11 downto 0);
  signal led_3_brightness_s : std_logic_vector(11 downto 0);
  signal led_4_brightness_s : std_logic_vector(11 downto 0);
  signal led_5_brightness_s : std_logic_vector(11 downto 0);
  signal led_6_brightness_s : std_logic_vector(11 downto 0);
  signal led_7_brightness_s : std_logic_vector(11 downto 0);
begin

---------------------Concurrent Statements---------------
  LED_1 <= '1';
  LED_2 <= '1'; ---------------------Processes--------------------------- led_test_brightness_process: process(CLK_24M) constant PWM_MAX : integer := 4094; constant FAST_TICK : integer := 19999; variable fast_counter_v : integer := 0; variable counter_0_v : integer := 0; variable counter_1_v : integer := 512; variable counter_2_v : integer := 1023; variable counter_3_v : integer := 1535; variable counter_4_v : integer := 2047; variable counter_5_v : integer := 2559; variable counter_6_v : integer := 3071; variable counter_7_v : integer := 3583; begin if rising_edge(CLK_24M) then if (fast_counter_v >= FAST_TICK) then
      
        if (counter_0_v >= PWM_MAX) then
          counter_0_v := 0;
        else
          counter_0_v := counter_0_v + 1;
        end if;
        
        if (counter_1_v >= PWM_MAX) then
          counter_1_v := 0;
        else
          counter_1_v := counter_1_v + 1;
        end if;
        
        if (counter_2_v >= PWM_MAX) then
          counter_2_v := 0;
        else
          counter_2_v := counter_2_v + 1;
        end if;
        
        if (counter_3_v >= PWM_MAX) then
          counter_3_v := 0;
        else
          counter_3_v := counter_3_v + 1;
        end if;
        
        if (counter_4_v >= PWM_MAX) then
          counter_4_v := 0;
        else
          counter_4_v := counter_4_v + 1;
        end if;
        
        if (counter_5_v >= PWM_MAX) then
          counter_5_v := 0;
        else
          counter_5_v := counter_5_v + 1;
        end if;
        
        if (counter_6_v >= PWM_MAX) then
          counter_6_v := 0;
        else
          counter_6_v := counter_6_v + 1;
        end if;
        
        if (counter_7_v >= PWM_MAX) then
          counter_7_v := 0;
        else
          counter_7_v := counter_7_v + 1;
        end if;
        
        fast_counter_v := 0;
      else
        fast_counter_v := fast_counter_v + 1;
      end if;
 
      led_0_brightness_s <= std_logic_vector(to_unsigned(counter_0_v, 12));
      led_1_brightness_s <= std_logic_vector(to_unsigned(counter_1_v, 12));
      led_2_brightness_s <= std_logic_vector(to_unsigned(counter_2_v, 12));
      led_3_brightness_s <= std_logic_vector(to_unsigned(counter_3_v, 12));
      led_4_brightness_s <= std_logic_vector(to_unsigned(counter_4_v, 12));
      led_5_brightness_s <= std_logic_vector(to_unsigned(counter_5_v, 12));
      led_6_brightness_s <= std_logic_vector(to_unsigned(counter_6_v, 12));
      led_7_brightness_s <= std_logic_vector(to_unsigned(counter_7_v, 12)); end if; end process; ---------------------Compoenents------------------------- Inst_led_0_dimmer_control: led_dimmer_control PORT MAP( CLK_24M => CLK_24M,
    LED_PWM_VALUE => led_0_brightness_s,
    LED => RED_LED_ARRAY(0)
  );
  
  Inst_led_1_dimmer_control: led_dimmer_control 
  PORT MAP(
    CLK_24M => CLK_24M,
    LED_PWM_VALUE => led_1_brightness_s,--X"3FE",
    LED => RED_LED_ARRAY(1)
  );
  
  Inst_led_2_dimmer_control: led_dimmer_control 
  PORT MAP(
    CLK_24M => CLK_24M,
    LED_PWM_VALUE => led_2_brightness_s,--X"5FD",
    LED => RED_LED_ARRAY(2)
  );
  
  Inst_led_3_dimmer_control: led_dimmer_control 
  PORT MAP(
    CLK_24M => CLK_24M,
    LED_PWM_VALUE => led_3_brightness_s,--X"7FC",
    LED => RED_LED_ARRAY(3)
  );
  
  Inst_led_4_dimmer_control: led_dimmer_control 
  PORT MAP(
    CLK_24M => CLK_24M,
    LED_PWM_VALUE => led_4_brightness_s,--X"9FB",
    LED => RED_LED_ARRAY(4)
  );
  
  Inst_led_5_dimmer_control: led_dimmer_control 
  PORT MAP(
    CLK_24M => CLK_24M,
    LED_PWM_VALUE => led_5_brightness_s,--X"BFA",
    LED => RED_LED_ARRAY(5)
  );
  
  Inst_led_6_dimmer_control: led_dimmer_control 
  PORT MAP(
    CLK_24M => CLK_24M,
    LED_PWM_VALUE => led_6_brightness_s,--X"DF9",
    LED => RED_LED_ARRAY(6)
  );
  
  Inst_led_7_dimmer_control: led_dimmer_control 
  PORT MAP(
    CLK_24M => CLK_24M,
    LED_PWM_VALUE => led_7_brightness_s,--X"FFE",
    LED => RED_LED_ARRAY(7)
  );
end Behavioral;

And that’s it for this design! At this point in a design usually you’d be very far from completion, needing to not only fix coding errors if you had them but also simulate and fix synthesis errors. In our case, we just hit the Synthesize, Implement Design and Generate Programming Files buttons on the left hand side of ISE in order to get our programming file.

Finished Synthesis

Completed Synthesis in ISE

The moment of truth

So this is the last thing we need to do in order to wrap up – see if it works! We follow the information we mentioned in our  previous article about programming the FPGA and bear witness to the results. In true family fortunes style our survey says…

and…

Fantastic! We can see that our LED’s strobe just as we want to. We know our hardware is working, our PIC is programming properly and our VHDL design works too!

So in conclusion…

Just to wrap things up it’s worth mentioning the omissions of these posts. I want to say this because of the huge amount of work that goes into making robust FPGA designs and how this article can be misleading in that regard. These posts don’t describe anything to do with high speed FPGA designs, simulating FPGA designs and code coverage in FPGA designs just to name a few of the many many more aspects involved. Here at Circuithinking we pride ourselves on being able to rise to any challenge presented by our customers and handle any aspect of FPGA designs. With the industry experience we have, we can anecdotally tell you that it takes many engineers a long time to make some of the best designs out there.

With that aside though, this is a very basic framework for FPGA design. We feel that customers can sometimes benefit from experiencing some of the things we go through in order to make your products the best we can. It can help you understand why we’re doing things the way we do and also why we ask our company question when it comes to design, “Why that way?”.

Get in contact with us today to start your design with us or even get consultation on where FPGA acceleration can help your product. Also feel free to tell us what you would like to see next in our new feed, we’re always happy to talk about any aspect of electronic design!