Project/VHDL
Introduction
Ever wished you could make your own integrated circuits? Is regular logic just not doing the trick for you? This project is all about programmable logic, and how you can define your own digital circuits yourself. Welcome to the wonderful world of CPLD's and FPGA's!
History
There was a time that, if you wanted a new logic function on your custom PCB, you had to have more chips. TTL logic chips that is. With quad NAND gates like the 74HC00, and a whole range of different logic functions, PCB's often became very complex. Often, not all the gates of such integrated circuits were used. Late 1970's, one of the first well-known programmable logic devices hit the streets. The Programmable Array Logic or PAL chips were made by Signetics, and at a later stadium copied by National Semiconductor, Texas Instruments and AMD. With names as "PAL16L8" and "PAL22V10", you could define your own logic in a One Time Programmable chip. It worked with fuses and could be programmed only once. It was very convenient to define your own logic gates, without the need for all those specific logic chips. At a later stadium, Electrically erasable GAL devices were pin-to-pin compatible and the road was set for devices that implemented large quantities of logic gates in a single device.
The PAL and GAL devices had a very limited amount of pins, and Complex Programmable Logic Devices or CPLD's were a great improvement on both pin count and complexity. But it wasn't until the Field Programmable Gate Arrays or FPGA's when the functionality and usability really took off. Of course, all of these devices had to be programmed in a certain way, and most of the time it means really defining what gate and functions had to active. There was a great need for a programming language to define the functionality of digital circuits. The Hardware Description Language or HDL was on its way!
Different flavors
The current market for programmable logic is roughly separated in a few brands:
- Xilinx, the largest manufacturer. Popular chips include the Spartan series, Virtex and Zynq devices.
- Altera (now owned by Intel). Inventors of the MAX CPLD's, Cyclone FPGA's and Stratix series.
- Actel. A smaller brand, but still the Igloo and ProAsic series are pretty well known.
- Lattice has the iCE series, MachXO and ispMACH devices.
There are a number of other smaller manufacturers, but these are the most well known ones. But if you want to program a CPLD or FPGA, you have several options. Most software packages offer the use of schematic diagrams to define the functionality inside, but while your designs get more complex, you will want to learn a HDL or Hardware Description Language. Two are commonly known:
- VHDL or VHSIC Hardware Description Language. This is most commonly used in Europe.
- Verilog is more used in the USA.
Building blocks
A modern FPGA device can hold several items to implement your design:
- Logic Elements or Cells. This is the most basic building block and it is used to implement all of your digital functions.
- Memory. These can help to store larger amounts of data in ROM, RAM, Dual port RAM and large buffers such as a First-In, First-Out (FIFO) buffers or shift registers.
- Multipliers. These help a great deal in signal processing and are therefore also referred as DSP blocks.
- Transceivers. Many well-equipped FPGA's offer very fast serial communication blocks. Almost all modern digital communication protocol implement these: HDMI, USB3.x, SDI, SATA and SAS, PCI express and many others.
- SOC's. Some FPGA's are accompanied by microprocessor blocks. Examples include a ZynQ, Cyclone V and SmartFusion devices.
When you are defining your functions in VHDL, you are using the LE's or Cells inside your FPGA. If you want to use other, device-specific blocks, this can be done in two ways. In the first way a specific specialized block is inferred. Through your definition of a function, the computer who reads your VHDL decides that it can best be implemented with a specialized block. Define a 1024-position buffer, and your compiler shortly decide it can best be implemented with a memory block. The other way is by instantiation: You specifically tell that you want to use a DSP, or transceiver block. You will have to define what settings need to be applied, and what configuration you need. This has also a disadvantage: If you want to use a different device, it might not recognize these instantiations!
VHDL first steps
So lets start with VHDL and defining our very first functionality. First, you will have to know something about the language. Every VHDL design has at least one entity. An entity can best be described as a black box. This black box can have one or more ports. Input ports, output ports, or even bidirectional ports. Also, an entity can have generic declarations. You can use this to make your design flexible. For example, you can define a counter function, with a clock and reset input, and a count output port. It can be an eight bit counter, or 16, or 22, or 47.. You could define the amount of bits in a generic declaration.
Once we have our entity, with all ins and outs, we need to define what's inside our black box! In the example of the counter, we can have a regular binary counter, or one with decoded outputs. Or maybe an alternative way of counting? The functionality inside our black box is defined in the architecture.
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity binary_counter is generic( counter_width : natural := 8 -- we want an 8 bit counter by default. ); port( clock : in std_logic; reset : in std_logic := '0'; count : out std_logic_vector(counter_width-1 downto 0) ); end entity binary_counter; architecture behavioural of binary_counter is signal count_internal : std_logic_vector(counter_width-1 downto 0); begin counter : process (clock) begin if clock'event and clock = '1' then -- this means the rising edge of signal "clock" if reset = '1' then count_internal <= (others => '0'); -- this means all lines are defined to '0' else count_internal <= count_internal + 1; end if; end if; end process counter; count <= count_internal; end architecture;
- "Counter, schmounter! I have an Arduino and it can count very well!"
You are very right. A counter can also be implemented in other ways, but programmable logic has several advantages over a microcontroller or microprocessor. These devices work with a defined program, and have a sequential execution of instructions. An FPGA works in a different way. All processes happen in parallel, and every single clock cycle. You can very easily run different processes with totally unrelated clocks and define a way of communicating with each other. You can define all kinds of functions you like inside an FPGA, including one of a microcontroller. This is referred as a "softcore processor". Examples are the Microblaze, NIOS and LEON processor core. An industry standard processor can be defined inside an FPGA, but also, for example, a 19-bit CPU. Possibilities are endless!
So let's take a look how the compiler actually implemented our design!
This is somewhat different to what we've expected, but it does the job. The basic functionality of our counter is in the adder, Add0. It adds the binary value 0x01h to the output of a D flipflop register, count_internal[7..0]. The output of the adder can be chosen with a 2-input multiplexer, that also has the binary value of 0x00h as an input. The multiplexer input is set by the reset input. So the output count[7..0] is advanced every clock cycle, unless we assert the reset line. Exactly what we wanted!
There is another thing we can have within the architecture, which can be of good use: A component. A component is an existing entity, which we can use in our current design. Instead of defining every single function again, we can have a hierarchic structure. A lot of simple entity functions can add up to a complex design. Instead of defining a process, we can "port map" a component to existing signals within our design. This is also the way to connect instantiations as well.
Instantiating
Now we've built a very usable counter design in VHDL, let's use this to make a more complex design. A PWM output is a single digital output to represent multiple values. Besides 0% on and 100% on, being a low signal level and a high signal level, we can assign a number of different values to this output. 50% on means that we toggle between low and high in an equally amount of time. 25% means we have 3 times as much low signal level then we have a high signal level. How can we make this in VHDL?
At some point in out design, we have to compare one value to another. The output of this comparison is the output of our PWM design. If we hold the desired PWM value in a register, we have to have a mechanism that steps through all possible values. If we compare these two values, we have built ourselves a PWM output!. The simplest way to step through a range of values is a counter. Let's use our previously built counter for this purpose. To do that, out counter design must be a component of our PWM design. Let's look at the code how this is accomplished.
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity pwm_output is port( pwm_clock : in std_logic; pwm_value: in std_logic_vector(7 downto 0); pwm_out : out std_logic ); end entity pwm_output; architecture behavioural of pwm_output is signal counter_value : std_logic_vector(7 downto 0); component counter is generic( counter_width : natural ); port( clock : in std_logic; reset : in std_logic; count : out std_logic_vector(counter_width-1 downto 0) ); end component counter; begin pwm_counter : counter generic map( counter_width => 8 ) port map( clock => pwm_clock, reset => '0', count => counter_value ); pwm_compare : process (pwm_clock) begin if pwm_clock'event and pwm_clock = '1' then if counter_value <= pwm_value then pwm_out <= '0'; else pwm_out <= '1'; end if; end if; end process pwm_compare; end behavioural;