Hacker News new | ask | show | jobs
by j16sdiz 842 days ago
I never really understand how HDL works.

I meant, in software programming, we usually program by either specifying the sequence or dependency.

In hardware, nothing runs sequentially, and signals propagate _with delay_. Everything happens at the same time, yet nothing run at the exact same moment. How could we express these chaos in for-loops and procedure look alikes?

4 comments

HDL when operating as an HDL doesn't convert things into for loops or procedures, it's specifying a configuration of gates or FPGA lookup tables. Crucially it doesn't contain any layout information, so additional phases of work are needed.

There is some confusion because the hardware can also be simulated, in a fairly straightforward way - you just need to model all those delays and run an event based simulator. Then the existing HDLs like (System)Verilog contain actual programming language constructs that are just there for use in simulation and can't actually be replicated in hardware.

For loops are one of those things that just don't really exist in hardware.

The way I like to think of hardware is as a machine that takes a giant blob of state, applies a pure function to it, and when the clock goes from low to high it replaces the original state with the new one and the process starts again.

> For loops are one of those things that just don't really exist in hardware.

Hardware for loops are just fine if you think of a for loop as existing in space rather than time--effectively it is always fully unrolled.

> The way I like to think of hardware is as a machine that takes a giant blob of state, applies a pure function to it, and when the clock goes from low to high it replaces the original state with the new one and the process starts again.

That's the way hardware guys think of it, too. :)

https://en.wikipedia.org/wiki/Mealy_machine

Preface: My experience is in verilog so I'll mostly be coming from there.

In most HDLs you have two kinds of code, procedural and continuous code.

Your continuous code is wires which propagate signals and continuous logic (basically pure functions). Since there aren't any registers in this logic, that means you generally need to do timing analysis to make sure it never gets too long between any two given registers (or latches if you are clock stealing). In verilog you mostly do this via assign statements and module instantiations.

Your procedural code is register or latch based code. In verilog these are your always blocks. Here the code represents logic that occurs on every occurance of a given clock edge. The type of always block you use and which edge you specify changes exactly what happens but generally it means "this code runs during this interval every time the clock does a specific transition". Your inputs are generally going to be registers or latches and your outputs will be as well. Anything that happens must do so fast enough for the logic in between to reach a steady state and long enough for it to catch the edge of the register, edge of the edge triggered latch, or level of the level triggered latch and set the register or latch. If that sounds complicated it kinda is but generally you just let your timing analyzer figure it out and complain if stuff is too slow.

Now outside your normal procedural code there are some other ones you see occasionally. There are also initial blocks which only run once and repeat blocks which only run a set number of times but with both you can pause or wait for a condition before proceeding. And there are also forever blocks. In all three of these blocks you can specify timings and delays for things. The most common use for forever blocks for example is to declare a clock that runs at a specified frequency. And initial blocks are used to do setup (such as start the clock loop, set initial values, etc).

Now as to how you do loops and procedural logic? You use registers to break it up. That's what those always blocks are for. Your always block does a little bit of work each time and you use a state machine to codify where in the loop or procedural logic you are. In general FSMs (finite state machines) become your friend in HDL work very quickly.

> continuous code

> continuous logic

This is terminology I'm used to. Generally this is called "combinational logic" in my experience. Is this a language or regional difference?

Oh nah I just wrote it at like 2am.
The semantics of HDLs are generally kind of funky: they have a sequential interpretation, which us what simulators will use. That interpretation does involve a bunch of blocks which will operate in parallel, but within those blocks it's just a sequential series of operations not unlike aby other programming language. But HDLs generally can express a lot more than what can be automatically synthesised into hardware (this is useful because you often have simulation-only test blocks and harnesses which don't need to exist in hardware).

The way you then write hardware is you define a series of blocks which are in the form "when this clock edge happens, update these variables in this way based on the previous value of variables in scope". It doesn't matter so much how you express this change, so long as the synthesis engine can map any given set of input variables to an output, which it will turn into digital logic. It is why verilog has odd constructs like "assign this value to this variable after the next time step". It's not unlike writing event-driven non-blocking code (without async or green threads).

Note at this point that the HDL written is saying nothing about delays: by running it in simulation, everything will complete in time for the next clock cycle. And the synthesiser will happily produce a circuit which the place and router can't actually make fast enough for the next cycle. That's where two other parts come in: firstly there's timing analysis which takes the low-level results of the place-and-route and basically tries to work out the fastest and slowest a signal can pass from one register output to another register input, and how that compares to the clocks to those registers (usually it'll be the same clock). That is generally what will tell you if what you've written won't work in some situation or another. And secondly, the tool can also take that low-level compiler outout, and give you a kind of "decompiled" version in your HDL, which doesn't bear much resemblance to what you put in but does include all the estimated delays, so you can simulate (much more slowly) what the timings will actually look like.

The upshot of which us that writing an HDL generally involves thinking about what the hardware would look like, writing some code that simulates what that hardware would be doing, running it through a slow build cycle, looking at what hardware was actually generated and working put why it's failing timing, thinking about how to change the hardware and then changing the code to get the sythesis tool to spit out the right version of that. You don't tend to get super high levels of abstraction in a given execution path: more the abstraction is in how you connect those parallel blocks together.