DEV Community

Cover image for My Code Just Learned to Count. I'm So Proud!
Ernests
Ernests

Posted on • Originally published at ernestsrudzitis.com on

My Code Just Learned to Count. I'm So Proud!

Welcome Back to the Underground

Round two: FIGHT! ๐ŸฅŠ Last time, I let you in on hardware's dirty little secret - that you can write hardware with actual readable code instead of VHDL's syntax nightmare. Now it's time to get our hands dirty. If you missed part one, go back. Trust me, you'll want the full initiation before we start bending hardware to our will.

Today's mission: We're going from "What is this Haskell witchcraft?" to "Heavens, I just made hardware that counts!" Buckle up.

Haskell: The Quick and Dirty Guide

Look, if you're coming from C++ or Python, Haskell is going to feel like driving on the wrong side of the road. Instead of barking orders at your computer, you're having a philosophical conversation with it.

In imperative languages, you tell the computer HOW to do things, step by step. In Haskell? You just describe WHAT things are. Want Fibonacci numbers? Don't tell it how to calculate each one - just tell it that any Fibonacci number is the sum of the two before it.

Mind. Blown. ๐Ÿคฏ

Fire Up the Playground

Time to launch GHCI - that's our playground. Type this in your terminal:

ghci
Enter fullscreen mode Exit fullscreen mode

If it takes a moment to load, that's normal. Even Haskell needs coffee sometimes...
You'll see something like this (version numbers may vary, we don't judge):Screenshot-2024-08-13-181930.png

Pro tip: Create a file called prog.hs so we're not typing everything directly into the void:

ghci> :load path_to_file/prog.hs
ghci> :reload
Enter fullscreen mode Exit fullscreen mode

Types: Haskell's Superpower

Haskell has a type system so strict it makes your high school math teacher look lenient. But here's the secret - it's actually your best friend. The compiler knows the type of EVERYTHING at compile time. No more "undefined is not a function" at 3 AM.

Let's see what Haskell thinks about the number 1:

ghci> :type 1
1 :: Num a => a
Enter fullscreen mode Exit fullscreen mode

Translation: "1 can be any type 'a', as long as 'a' knows how to act like a number."
The compiler just told us it's flexible but not stupid. Beautiful.

Your First Functions (They're Friendlier Than They Look)

Functions in Haskell are refreshingly simple. No parentheses, no commas, just spaces:

successor :: Num a => a -> a
successor val = val + 1

ghci> successor 5
6
Enter fullscreen mode Exit fullscreen mode

That :: means "has the type of". The arrow -> is just showing data flow: takes a number, returns a number.

But here's where it gets fun - pattern matching. Let's run a sketchy car rental business:

data CarBrand = Ferrari | Bugatti | Toyota | Ford

rentCost :: CarBrand -> Int 
rentCost Ferrari = 150        -- "That'll be $150, sir"
rentCost Bugatti = 800        -- "Hope you've got deep pockets"
rentCost anyOtherCar = 30     -- "Meh, whatever"
Enter fullscreen mode Exit fullscreen mode

The function checks patterns top to bottom. First Ferrari it sees? $150. Toyota? Falls through to the $30 "peasant tier".

Recursion: When Functions Call Themselves (And It's Not Weird)

In Haskell, loops are so last century. We use recursion. Here's summing a list the Haskell way:

sumList :: Num a => [a] -> a
sumList [] = 0                     -- Empty list? Sum is 0. Duh.
sumList (x:xs) = x + sumList xs    -- First element + sum of the rest
Enter fullscreen mode Exit fullscreen mode

But wait, there's a sexier way using higher-order functions:

sumListHigherOrder x = foldl (+) 0 x  -- One line. Boom.
Enter fullscreen mode Exit fullscreen mode

Why should you care? Because Clash HATES recursion but LOVES higher-order functions. Remember that - it'll save your sanity later.

Time to Clash! (The Main Event)

Enough foreplay. Let's make some hardware.
Fire up clashi:

clashi
Enter fullscreen mode Exit fullscreen mode

You'll see this (ignore the warning, it's just being dramatic):
Image of clashi interactive environmentIf it takes forever to load, that's normal. It's thinking about all the hardware it's about to help you create.

Your First Circuit: The Mighty AND Gate

Time to play Dr. Frankenstein. We're building an AND gate - the "hello world" of hardware. Create a file called ANDGate.hs:

module ANDGate where
import Clash.Prelude

andBit :: Bit -> Bit -> Bit
andBit x y = x .&. y   -- That's it. You're a hardware engineer now.
Enter fullscreen mode Exit fullscreen mode

Test this bad boy:

ghci> :load path_to_file/ANDGate.hs

ghci> andBit 1 0
0 -- Returns: 0 (sorry, AND gates are picky)

ghci> andBit 1 1
1 -- Returns: 1 (both inputs true = happy gate)
Enter fullscreen mode Exit fullscreen mode

Anticlimactic? Just wait until we see this in silicon...

Seeing Is Believing (Time to Fire Up the Beast)

Remember that 18GB monster we installed? Vivado? Time to wake it up and watch our code become actual circuit diagrams. This is where things get real.

First, tell Clash what to compile:

module ANDGate where
import Clash.Prelude

andBit :: Bit -> Bit -> Bit
andBit x y = x .&. y

topEntity :: Bit -> Bit -> Bit
topEntity = andBit -- "Hey Clash, compile THIS one!"
Enter fullscreen mode Exit fullscreen mode

Now compile to Verilog (exit clashi first):

clash --verilog path_to_file/ANDGate.hs
Enter fullscreen mode Exit fullscreen mode

Check out the new file verilog/ANDGate/topEntity.v. That's YOUR hardware in Verilog. You just wrote 4 lines of Haskell and got valid HDL. Your VHDL-writing colleagues are crying right now.

The Vivado Dance

Fair warning: Vivado's UI looks like it was designed by someone who hates joy. But seeing your code transform into gates and wires? Worth it.

Creating a project in AMD Vivado Step 1 : Create a new project Proceeding with AMD Vivado project creation Step 2 : Proceed Naming and location of project Step 3 : Give your project a recognizable name, the default location is fine Project type selection step Step 4 : Select the type to be RTL Project Adding source files to our project Step 5 : From the dropdown select 'Add Files...'

Locating and selecting the 'topEntity.v' file Step 6 : Locate the 'topEntity.v' file

vivado_blog_2_project_walkthrough_8-min.png Step 7 : Project summary, proceed Selecting default development board Step 8 : Select the FPGA development board. This tutorial doesn't assume you have a physical board, we will be only simulating our designs. So, in this case the selected board choice does not really matter, but the board we opt for is 'Kria KV260 Vision AI Starter Kit SOM'. Generating RTL design Step 9 : Click on 'Open Elaborated Design' and wait for the process to finish Schematic window with the RTL design Step 10 : New 'Schematic' window appears that contains our design in RTL (Register-transfer level) Optional part of synthesizing our design Step 11 : (Optional) Run Synthesis Optional part of synthesizing our design Step 12 : (Optional) Press 'OK'. After finished, choose 'Schematic' under 'Open Synthesized Design' dropdown Schematic window of synthesized design Step 13 : New 'Schematic' window appears that contains our synthesized design

Plot twist: After synthesis, notice something? NO AND GATES! Remember from part 1 - FPGAs fake everything with lookup tables. Sneaky bastards.

Level 2: Hardware with Memory (Things Get Spicy)

Combinational circuits are like goldfish - no memory. But what if your hardware could actually remember things? Enter sequential design, where circuits get a brain upgrade.

We're building a counter that can count up AND down. Because why settle for one direction?

Create Counter.hs:

module Counter where
import Clash.Prelude

type Val = Unsigned 3       -- 3 bits = counts 0 to 7

incrementer :: Val -> Val
incrementer v = v + 1       -- Advanced mathematics here

decrementer :: Val -> Val 
decrementer v = v - 1       -- PhD-level stuff

counter :: (HiddenClockResetEnable dom) => Signal dom Bool -> Signal dom Val 
counter incr = state
    where 
        state = register 0 (mux incr (incrementer <$> state) (decrementer <$> state))
Enter fullscreen mode Exit fullscreen mode

"Holy smoke, what is all that?" - You, probably.

Don't panic. Here's the decoder ring:

  • HiddenClockResetEnable = Clash handles the boring clock stuff
  • register 0 = Our memory starts at zero (humble beginnings)
  • mux = The bouncer that decides: count up or down?
  • <$> = Applying functions to signals (it's just fmap being fancy)

Take It for a Spin

Add this test to your file:

simulateCounter = simulate @System counter [True, True, True, False, False, False]
Enter fullscreen mode Exit fullscreen mode

Run it:

ghci> :load path_to_file/Counter.hs

ghci> simulateCounter
[0,1,2,3,2,1,0,*** Exception: X: finite list
CallStack (from HasCallStack):
...
Enter fullscreen mode Exit fullscreen mode

IT COUNTS! Up three times, down three times. Your code just learned to count! ๐ŸŽ‰
(Ignore that initial 0 and the exception - Clash is just being dramatic)

Making It Real

For Vivado visualization, add this magic:

topEntity :: Clock System -> Reset System -> Signal System Bool -> Signal System Val
topEntity clk rst incr = withClockResetEnable clk rst enableGen counter incr
Enter fullscreen mode Exit fullscreen mode

Compile it:

clash --verilog path_to_file/Counter.hs
Enter fullscreen mode Exit fullscreen mode

A new folder is created 'verilog/Counter' containing ' topEntity.v' that contains the translated code.

Load it into Vivado (same deal as before):

Removal of existing 'topEntity.v' file
Step 1 : Remove the existing 'topEntity.v' file from sources Adding a new design source Step 2 : Choose option to add new sources Adding files Step 3 : Choose 'Add Files' option Selecting the new 'topEntity.v' file Step 4 : Select the 'topEntity.v' source file for the counter circuit design Schematic window of the counter circuit Step 5 : Click on 'Open Elaborated Design' and wait for the process to finish. New 'Schematic' window appears that contains our design in RTL (Register-transfer level).

And BOOM - you'll see:

  • Clock and reset inputs (the heartbeat and panic button)
  • Your increment control signal
  • A multiplexer (the decision maker)
  • A register (the memory)

You just built stateful hardware. In like 15 lines. While VHDL developers are still writing their entity declarations.

The Uncomfortable Truth

Here's what just happened: You learned enough Haskell to be dangerous, wrote hardware that actually works, and visualized it in industry-standard tools.

Your counter? That's the building block for:

  • Timers that control your coffee machine
  • Frequency dividers in communication systems
  • State machines that run traffic lights
  • The foundations of a CPU

And you did it without touching a single line of traditional HDL.

You're One of Us Now

Stone the crows, you did it! You went from zero to building actual circuits that count, remember things, and respond to inputs. While your colleagues are still fighting with VHDL syntax errors, you're already watching RTL schematics materialize from functional code.

Got an FPGA board? Generate that bitstream and watch your code literally reconfigure silicon. No board? No problem - keep simulating and learning.

The hardware underground is growing. You're part of it now.

Welcome to hardware development that doesn't suck. ๐Ÿš€


Want the deep dive? Check out the full technical analysis on my blog

Top comments (0)