For this tutorial, you will need at least Clash version 1.6.3. We’ll be programming the Terasic DE0-Nano FPGA development board using the functional hardware description language Clash. The end result of this tutorial is demonstrated in the video below:
This tutorial is not a general introduction to Clash, nor to programming FPGAs. It is meant to demonstrate how to use Synthesize
annotations and SynthesisAttributes
to configure your Clash designs for an FPGA, without writing a single line of VHDL or (System)Verilog.
We start with some general information about the DE0-Nano board:
1
).0
when they are pressed, and logic level 1
when they are not pressed.The circuit that we are making will repeatedly do one of two things:
We switch between these two modes when the KEY1
button on the board is pressed and subsequently released. We reset the circuit by pressing the KEY0
button.
The complete description of the circuit is given below, where the description of the core behavior of the circuit only starts at line 95, and everything that comes before it is there to correctly set up the FPGA.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
{-# OPTIONS_GHC -fno-warn-orphans #-} module Blinker where import Clash.Prelude import Clash.Intel.ClockGen import Clash.Annotations.SynthesisAttributes -- Define a synthesis domain with a clock with a period of 20000 /ps/. -- i.e. 50 MHz createDomain vSystem{vName="Input", vPeriod=20000} -- Define a synthesis domain with a clock with a period of 50000 /ps/. -- i.e. 20 MHz createDomain vSystem{vName="Dom20MHz", vPeriod=50000} {-# ANN topEntity (Synthesize { t_name = "blinker" , t_inputs = [ PortName "CLOCK_50" , PortName "KEY0" , PortName "KEY1" ] , t_output = PortName "LED" }) #-} topEntity :: -- | Incoming clock -- -- Annotate with attributes to map the argument to the correct pin, -- with the correct voltage settings, on the DE0-Nano development kit. Clock Input `Annotate` 'StringAttr "chip_pin" "R8" `Annotate` 'StringAttr "altera_attribute" "-name IO_STANDARD \"3.3-V LVTTL\"" -> -- | Reset signal, straight from KEY0 Signal Input Bool `Annotate` 'StringAttr "chip_pin" "J15" `Annotate` 'StringAttr "altera_attribute" "-name IO_STANDARD \"3.3-V LVTTL\"" -> -- | Mode choice, straight from KEY1. See 'LedMode'. Signal Dom20MHz Bit `Annotate` 'StringAttr "chip_pin" "E1" `Annotate` 'StringAttr "altera_attribute" "-name IO_STANDARD \"3.3-V LVTTL\"" -> -- | Output containing 8 bits, corresponding to 8 LEDs -- -- Use comma-seperated list in the "chip_pin" attribute to maps the -- individual bits of the result to the correct pins on the DE0-Nano -- development kit Signal Dom20MHz (BitVector 8) `Annotate` 'StringAttr "chip_pin" "L3, B1, F3, D1, A11, B13, A13, A15" `Annotate` 'StringAttr "altera_attribute" "-name IO_STANDARD \"3.3-V LVTTL\"" topEntity clk50Mhz rstBtn modeBtn = -- Connect our clock, reset, and enable lines to the corresponding -- /implicit/ arguments of the 'mealy' and 'isRising' function. exposeClockResetEnable (mealy blinkerT initialStateBlinkerT . isRising 1) clk20Mhz rstSync en modeBtn where -- Enable line for subcomponents: we'll keep it always running en = enableGen -- Start with the first LED turned on, in rotate mode, with the counter -- on zero initialStateBlinkerT = (1, Rotate, 0) -- Signal coming from the reset button is low when pressed, and high when -- not pressed. We convert this signal to the polarity of our domain with -- 'unsafeFromLowPolarity'. rst = unsafeFromLowPolarity rstBtn -- Instantiate a PLL: this stabilizes the incoming clock signal and -- indicates when the signal is stable. We're also using it to transform -- an incoming clock signal running at 50 MHz to a clock signal running at -- 20 MHz. (clk20Mhz, pllStable) = altpll @Dom20MHz (SSymbol @"altpll20") clk50Mhz rst -- Synchronize reset to clock signal coming from PLL. We want the reset to -- remain asserted while the PLL is NOT stable, hence the conversion with -- 'unsafeFromLowPolarity' rstSync = resetSynchronizer clk20Mhz (unsafeFromLowPolarity pllStable) data LedMode -- | After some period, rotate active led to the left = Rotate -- | After some period, turn on all LEDs that are turned off, and vice versa | Complement deriving (Generic, NFDataX) flipMode :: LedMode -> LedMode flipMode Rotate = Complement flipMode Complement = Rotate -- Finally, the actual behavior of the circuit blinkerT :: (BitVector 8, LedMode, Index 6660000) -> Bool -> ((BitVector 8, LedMode, Index 6660000), BitVector 8) blinkerT (leds, mode, cntr) key1R = ((ledsN, modeN, cntrN), leds) where -- clock frequency = 20e6 (20 MHz) -- led update rate = 333e-3 (every 333ms) cnt_max = maxBound :: Index 6660000 -- 20e6 * 333e-3 cntrN | cntr == cnt_max = 0 | otherwise = cntr + 1 modeN | key1R = flipMode mode | otherwise = mode ledsN | cntr == 0 = case mode of Rotate -> rotateL leds 1 Complement -> complement leds | otherwise = leds |
The two things that we wanted to highlight from the “setup” part of the descriptions are:
Synthesize
ANN pragma, andAnnotate
synthesis attributes.The Synthesize
annotation we see on line 15:
1 2 3 4 5 6 7 8 9 |
{-# ANN topEntity (Synthesize { t_name = "blinker" , t_inputs = [ PortName "CLOCK_50" , PortName "KEY0" , PortName "KEY1" ] , t_output = PortName "LED" }) #-} |
lets you control the port names of the generated entity
(VHDL) or module
(Verilog), and the name of the entity
/module
itself. It also tells the Clash compiler from which function it should start synthesis. So in the above case, the compiler should start from the topEnity
function, and should call the generated entity
corresponding to that function: blinker
. Then the ports corresponding to the arguments clk50MHz
, rstBtn
and modeBtn
should be called: CLOCK_50
, KEY0
and KEY1
. The output port, corresponding to the result of the function should be named: LED
.
Next up are the Annotate
attributes, such as the one on line 30:
1 2 3 4 |
Clock Input `Annotate` 'StringAttr "chip_pin" "R8" `Annotate` 'StringAttr "altera_attribute" "-name IO_STANDARD \"3.3-V LVTTL\"" -> |
where we annotate the type Clock Input
with two string attributes:
"chip_pin" "R8"
, which informs Quartus to connect the port corresponding to this argument to the FPGA pin called R8
."altera_attribute" "-name IO_STANDARD \"3.3-V LVTTL\""
, which informs Quartus about the voltage characteristics of the pin associated with the port associated with this argument.All of this allows us to simply import the generated VHDL later on in our Quartus project without having to do any further FPGA configuration.
Now it’s time to generate some VHDL. Copy the above description into a file called Blinker.hs
. Then from the command line, run the Clash compiler to generate VHDL:
1 |
clash -Wall --vhdl -fclash-hdlsyn Quartus Blinker.hs |
This will create a ./vhdl/Blinker.topEntity
directory with all the generated files. Note we passed in the additional flag -fclash-hdlsyn Quartus
, that is because Quartus is somewhat quirky as to where it expects attributes
for entity ports
: in the architecture
. By passing -fclash-hdlsyn Quartus
, we ensure that Clash generates VHDL that is “compliant” with these quirks.
You can inspect ./vhdl/Blinker.topEntity/blinker.vhdl
and the entity and its ports are named as specified in the Synthesize
ANN pragma.
1 2 3 4 5 6 7 |
entity blinker is port(-- clock CLOCK_50 : in blinker_types.clk_input; KEY0 : in boolean; KEY1 : in std_logic; LED : out std_logic_vector(7 downto 0)); end; |
Further down, we can see that the synthesis attributes have propagated to the generated VHDL:
1 2 3 4 5 6 7 8 9 10 11 |
attribute altera_attribute : string; attribute altera_attribute of LED : signal is "-name IO_STANDARD ""3.3-V LVTTL"""; attribute altera_attribute of KEY1 : signal is "-name IO_STANDARD ""3.3-V LVTTL"""; attribute altera_attribute of KEY0 : signal is "-name IO_STANDARD ""3.3-V LVTTL"""; attribute altera_attribute of CLOCK_50 : signal is "-name IO_STANDARD ""3.3-V LVTTL"""; attribute chip_pin : string; attribute chip_pin of LED : signal is "L3, B1, F3, D1, A11, B13, A13, A15"; attribute chip_pin of KEY1 : signal is "E1"; attribute chip_pin of KEY0 : signal is "J15"; attribute chip_pin of CLOCK_50 : signal is "R8"; |
Next up, we create a Quartus project. We could go through the GUI to set things up, but in this case, we will simply create the project manually. Create the following files, with their given content, and put them in the directory where Clash generated the VHDL: ./vhdl/Blinker.topEntity
.
blinker_ext.sdc
1 2 3 4 5 6 |
read_sdc blinker.sdc derive_pll_clocks derive_clock_uncertainty set_false_path @KO8!DzHW98tcEt&nrX2MVPZkkqPS$-from * -to [get_ports {LED[*]}] set_false_path -from [get_ports {KEY0}] -to * set_false_path -from [get_ports {KEY1}] -to * |
blinker.qsf
1 2 3 4 5 6 7 |
set_global_assignment -name DEVICE EP4CE22F17C6 set_global_assignment -name TOP_LEVEL_ENTITY blinker set_global_assignment -name VHDL_FILE blinker_types.vhdl set_global_assignment -name VHDL_FILE blinker.vhdl set_global_assignment -name VHDL_FILE Blinker_topEntity_resetSynchronizer.vhdl set_global_assignment -name QSYS_FILE blinker_altpll_E588ED61A4853C9C.qsys set_global_assignment -name SDC_FILE blinker_ext.sdc |
blinker.qpf
1 |
PROJECT_REVISION = "blinker" |
You should now have a directory with the following files:
blinker_altpll_E588ED61A4853C9C.qsys
blinker_ext.sdc
blinker.qpf
blinker.qsf
blinker.sdc
Blinker_topEntity_resetSynchronizer.vhdl
blinker_types.vhdl
blinker.vhdl
There will also be a clash-manifest.json
which you can ignore for now.
Now start Quartus Prime; once started, in the menu bar, click File -> Open Project
and open blinker.qpf
. Quartus might tell you IP upgrade required.
This is in fact not required and can be ignored. In the menu bar, now click Processing -> Start Compilation
. This can take up to a minute depending on your machine. If everything worked as it was supposed to the last messages in the logs should be in the spirit of:
1 2 3 4 |
Info (332101): Design is fully constrained for setup requirements Info (332101): Design is fully constrained for hold requirements Info: Quartus Prime Timing Analyzer was successful. 0 errors, 1 warning Info (293000): Quartus Prime Full Compilation was successful. 0 errors, 8 warnings |
After synthesis has finished, it is time to program our FPGA board. Connect the FPGA board to a USB port, and start the programmer from the menu bar: Tools -> Programmer
. Press the Start
button on the left to program your FPGA and wait until the progress bar says 100% (Successful)
.
Your FPGA should now be fully configured with the Clash generated design for you to play with. So yeah, go press those buttons!
An FPGA design house delivering "right the first time" solutions.
Design and realisation by:
Comyoo | creatieve studio
Capitool 10
7521 PL Enschede
+31 (0)85 8000 380
info@qbaylogic.com
CoC: 66440483
VAT: NL856554558B01