There are a lot of programming languages for creating software, but for designing digital logic the field is much smaller. This isn't entirely surprising. Pretty much everyone has a computer capable of running software, but very few people have wafer fabs, and even FPGAs are relatively uncommon. (Read: expensive - the ones we use retail for about $8000.)
The two major players are VHDL and Verilog, which are register-transfer languages. This means that their basic abstractions are registers (things that hold a value) and combinational logic elements (things that take zero or more values and produce one or more new values). The simplest of these are wires, which take a value and deliver the same value.
In terms of abstraction level, it corresponds to something like C in the software world. You don't have to think about the placement of logic elements, but you do have to think about what every single one is, when things happen in terms of clock lines, and the amount of type checking that you have is on the level of ensuring that the real and expected inputs have the same number of bits.
There are some very high-level tools available for producing Verilog. One example is the Synopsis Processor Designer, which uses pipelines as the fundamental abstraction in a language called LISA 2. If you're designing something like a traditional CPU (including superscalar and GPU-style SIMT cores), it provides an easy way of specifying what happens in each stage.
Bluespec SystemVerilog (BSV) is a language that began life as a Haskell domain-specific language (although it now has very little of the Haskell syntax remaining). It was created by Arvind at MIT to design high-performance network cards. It has since been developed by a spin-out company that specializes in tools for digital logic, originally ASICs and increasingly FPGAs.
Thinking in Parallel
Coming from a software background, the first thing that you have to get used to in the hardware world is that everything happens at once. In software, even with multithreading, there is a point or a small set of points where your control flow is at any given time. You can allocate and deallocate resources on demand. In hardware, resources are "allocated" when you create the chip and then are either idle or working. In something like a pipelined CPU, you'll have multiple instructions in-flight at any given time, and each pipeline stage will be processing its inputs, independently of any concept of global state.
In Verilog, you typically keep things in sync using clock lines that are explicitly defined and used to trigger events. In BSV, this is inferred. A BSV module contains rules (actions) that are triggered whenever all their preconditions are met. Typically, these are implicit conditions.
For example, if you have pipeline stages connected by a FIFO, the rules that read from the FIFO will not fire unless the FIFO has one or more values in it. In the generated Verilog, there will be logic for asserting a line to indicate that there is data ready and using that to trigger the action, but (as should be the case in a high-level language) the programmer isn't responsible for creating it.
BSV modules are very close to classes in an object-oriented language. You instantiate them (statically, obviously, because instantiating a module means creating some hardware) and then you communicate with them either explicitly via methods or implicitly via FIFOs or similar. When you invoke a method from another module directly, that method invocation must happen in the same clock cycle as the caller, which means it imposes some synchronization overhead during synthesis and may end up lowering the maximum clock speed that the design can support.
How you communicate with modules depends a little bit on their complexity. Some are intended to be small and embeddable (for example, the register and FIFO implementations in the BSV standard library are, themselves, modules), and some are instantiated as part of larger modules. Larger ones, such as an instruction decoder, take a reasonable amount of die space and so will typically communicate via FIFOs or similar - something that allows them to work the next clock cycle from when the data was provided.
Don't Miss These Related Articles Also by David Chisnall
- The Benefits of Learning Multiple Programming Languages
- What Language I Use for... Building Scalable Servers: Erlang
- What Language I Use for... Creating Reusable Libraries: Objective-C
When I'm writing software, I generally like languages with late binding and duck typing because they make it easier to refactor the code incrementally and without having to recompile all dependent libraries. In hardware, this is not an issue. You will never upgrade a shared library in a CPU. At most, you may upgrade some components on an SoC independently, and ensuring that the interfaces remain compatible is very important.
BSV provides a richer type system. In terms of basic data types, it's fairly close to Verilog. You can define integer types of arbitrary widths, tagged unions (where the type of the current value of the union is either propagated statically or via some extra wires), structures, and so on. It allows any of these to have notions of equality and mapping to bits derived automatically. The type system (as one might expect from a Haskell-derived language) goes a lot further than that and into the module system.
For example, when you create a register in BSV, you'll write something like this:
Reg#(UInt#(64)) counter <- mkReg(0);
This defines a variable called counter. The type of this is parameterized. Reg is an interface that defines the methods available on registers. UInt is the unsigned integer type. This, then, is a register containing a 64-bit integer. It is defined by instantiating the mkReg module (concrete modules are typically prefixed by mk in BSD, short for make), which infers its type from the type of variable and has an initial value of 0.
This is where some of the real power of BSV starts to become apparent. There is a single mkReg module, which implements registers of any type. They are not limited to being just integers; they can hold any type that can be converted to the Bit type (another parameterized type, meaning a fixed-size group of bits). This isn't limited to the built-in types. It's possible to create, for example, floating-point operation modules that are parameterized on the number of bits in the mantissa and exponent and instantiate the same logic for 32-bit, 64-bit, or any other arbitrary sized values that you decide to support (I had a final-year undergraduate student implement an FPU in this way last year).
Simulation Speed and Verification
There are several advantages to a higher-level representation. The obvious one is that it's easier to check the code. The type checking is just the start here. There's a lot of work on converting BSV to representations that allow for formal verification and proving various properties of a system. The availability of more high-level semantic information in the language means that there's more information to check.
The higher-level representation also makes simulation a lot (2-3 orders of magnitude) faster. One of the problems with hardware design is that optimizing the code for synthesis means doing more in parallel, which makes simulation slower.
A cycle-accurate Verilog simulation needs to work at a much finer granularity than a cycle-accurate BSV simulation. The BSV simulator is still around 4 orders of magnitude than an FPGA synthesis, but that's a speed where it's (just about) possible to boot a real OS on a simulated CPU and run some simple tests. It's not something I'd recommend, but for simpler tests it's very fast.
I am a very lazy person and I hate solving the same problem more than once. As software has become more complex, code reuse has gone from being a nice ideal to a fundamental necessity, and the tools have evolved along with this. In the hardware world, it's still a lot less common. Talking to some of the major CPU and GPU vendors, their workflow still largely involves starting from scratch each new generation. They'll have some reusable building blocks, but the overall chip design is new each generation. The idea of incrementally improving it over time just doesn't happen (there are exceptions, but they're fairly rare).
With Bluespec SystemVerilog, it feels like this doesn't have to be the case. It's possible to reuse code, not just between projects but also within the same one. Instantiating different versions of a module just by changing some types in the declaration is amazingly powerful.
The one downside of BSV is that it's a proprietary language with a single vendor behind it. The proprietary part isn't really avoidable because most ASIC and FPGA development toolchains are proprietary. The single-vendor part is somewhat more unfortunate: There are lots of people willing to sell you Verilog toolchains and, although it might be painful, moving between them is possible.