Programmable Logic/Verilog for Software Programmers
Concepts
[edit | edit source]Verilog is a Hardware Description Language (HDL). While the coding style looks similar to software languages such as C++ or Java, with if-statements, loops, variables and expressions, you can't write Verilog the way you would write software. Good hardware designers have a clear picture in their head of the hardware structures they want to generate. The Verilog language is merely a convenient way to express those hardware structures: multiplexers, registers, random logic, state machines, etc.
Clocks and Reset
[edit | edit source]A software program usually starts by calling a main() routine. Execution proceeds until the code exits. There is no such starting point in Verilog: a digital circuit is running forever, constantly processing its inputs, looking at its internal state and generating outputs. The circuit does its processing at regular intervals, as dictated by a special signal called the clock. You have heard of such a signal before: a 2GHz CPU is a circuit that is clocked by a 2 GHz signal. That means such circuit will perform 2 billion steps each second.
The typical starting point of a digital circuit is marked by another special signal called reset. While reset is active, the circuit will not try to do anything. But once reset is deactivated, one expects the digital circuit to start doing something useful.
You can think of your digital design as a series of steps. Each step executes during one clock cycle. If you want to perform a complex task, it's completely normal to use several steps, sometimes thousands.
Parallel Execution
[edit | edit source]A software program is a sequence of assembly instructions. If you change your code and recompile it, you can generate new instructions and run a different program on the same computer: you don't need to throw away your computer and buy a new one to run a new program.
Unfortunately, hardware doesn't work like that: all the logic you need for your design must be defined and cannot be changed on the fly. If you need to add three numbers during one step, then you will need to have provided for that special adder ahead of time.
However, hardware has one big advantage compared to software, which is why it's so powerful: all the steps execute in parallel, at all times. For a complex design, there will literally be millions of fragments of code, all executing at the same time.
Parallel execution is what makes Verilog so weird at first when coming from software languages. Let's try to see how you would write C code if all the statements were executing in parallel all the time. Consider this tiny C code:
main() {
int a = 5; int b = 7;
if(a < b) {
b = b - a;
}
}
Imagine that you have a magic variable __LINE__ and you have to control which line executes next. But all lines execute forever, and at the same time. The C code below would have the same behavior as the original code:
main() {
__LINE__ = 1;
while(1) {
if(__LINE__ == 1) { int a = 5; int b = 7; __LINE__ = 2}
if(__LINE__ == 2) {if(a < b) { __LINE__ = 3; else __LINE__ = 4;}
if(__LINE__ == 3) { b = b - a; }
}
}
Scary, isn't it? That's how hardware works. A hardware design is a series of steps. At the start of each step, all code executes from the beginning again, so you must have saved somewhere which state you were in at the previous step, so you can continue where you left off. In the example above, __LINE__ is your state.
Hello World
[edit | edit source]How would you implement the classic "Hello World" program in Verilog? There is no way to do a printf in hardware. But we can code something similar that highlights how hardware differs from software. Let's try to design a circuit that has one input: an 8-bit character, and one output: the HelloWorld output. This circuit will read the characters coming in, step after step, and when it detects the sequence "Hi" (the letter H followed by the letter i", it will raise its output to 1, to say HelloWorld, until the circuit is reset. If you have a development board, you can wire that signal to an LED and you'll see the light come on when you input the right sequence.
The declaration of the circuit looks like:
module HelloWorld(input [7:0] char, output HelloWorld, input clk, input reset);
The core of the circuit is a state machine that will first look for the letter 'H', then the letter 'i'.
always @(posedge clk) begin
switch(state) begin
case GOT_NOTHING: begin
if(char=='H') state <= GOT_H;
else state <= GOT_NOTHING;
end
case GOT_H: begin
if(char=='i') state <= GOT_HI;
else state <= GOT_NOTHING;
end
end
Then, we just need to drive the output signal HelloWorld to be true when we are in the state GOT_HI:
assign HelloWorld = (state == GOT_HI);
And we shouldn't forget to declare the state type and constants that we used to encode the state:
parameter GOT_NOTHING = 0;
parameter GOT_H = 1;
parameter GOT_HI = 2;
reg [1:0] state;
Loops
[edit | edit source]Now that you understand that each block of Verilog code gets executed at each step from the beginning, we can discuss more advanced language constructs without confusing them with their software look-alike. Yes, Verilog supports loops and if statements. Within reason.
First of all, a loop must complete within the same step. You can't start a loop in a step and finish it later. If you wanted to do that, you'd have to keep a state somewhere that tells you where in the loop you are, and resume appropriately at each new step.
Second, a loop cannot have a variable number of iterations that depend on other variables. You can only loop over a constant quantity. For instance, you can loop other all 16 bits of a vector. But you cannot loop until two variables become equal.
Pointers and Memory
[edit | edit source]As we saw previously, you can't create new hardware on the fly. That means there is no equivalent in Verilog to a malloc() statement. You can't allocate memory. Either the memory is there or it's not. If you think of it, it's the same thing on your PC: you have 8 GB of RAM, and after that, you run out of memory. It's just that your program tends to use less than the total available memory (usually).
Since we can't allocate memory, pointers don't make sense in Verilog. There are memories though. They are small blocks that allow you to read and write data. Their main constraint is that you can only make a certain number of read and writes per step. Actually, most memories will only allow you to process one read or one write at most per step. In Verilog, a memory looks like an array. The dimensions of the array specify the depth and the width of the memory. For instance, this would be a memory of 256 words of 32-bit each:
reg [31:0] my_memory[255:0];
You can access the content of the memory inside an always block, but pay attention: each time you reference the memory signal will count as one read or write port. You definitely do not want to access a memory signal inside a loop unless you know your memory has at least as many ports as the size of the loop. Something that seems as simple as reading from one address or another needs to be implemented correctly. The code below is bad because it will consume two read ports, which is overkill:
always @(posedge clk) begin
if(select_addr1)
data <= my_memory[addr1];
else
data <= my_memory[addr2];
end
You'd want to code this memory read this way, to make sure there is only one access to the memory array:
always @(posedge clk) begin
if(select_addr1)
addr = addr1;
else
addr = addr2;
data <= my_memory[addr];
end
Swapping values
[edit | edit source]A classic software interview question is how to exchange the content of two variables without using a temporary variable. In case you haven't heard the trick yet, the solution (for integers) is to use the XOR operator:
function swap(int a, b) {
a = a xor b;
b = a xor b;
a = a xor b;
}
Verilog is even more powerful: you can swap two variables without using a temporary variable and without using any operation at all. How is that possible? Remember that when you assign a variable, the assignment takes effect in the next step. Therefore, to swap two variables, all you really want to say is: "the next value of b is the current value of a, and the next value of a is the current value of b." Which in Verilog would be expressed using a non-blocking assignment ("<="):
always @(posedge clk) begin
a <= b;
b <= a;
end
The other kind of assignment in Verilog is called the blocking assignment (what you are used to see as "="). That assignment means that the change to the value happens immediately. It's useful to compute intermediate values within an always block, but whenever you want to assign a signal that survives from one step to the next, a non-blocking assignment is preferred. Therefore, the code below is broken: at the end of the always block, both a and b have the same value, and a's original value has been lost.
always @(posedge clk) begin
a = b; // we just lost a's value
b = a;
end
Debugging
[edit | edit source]Steps are such an important concept in Verilog, it's almost impossible to debug the behavior of a digital circuit without having easy access to the history of all the variables. Since each block executes from scratch at each step, you need to know the state that has been generated at the previous step to understand why the current step is not doing what you intended. That's why hardware designers use waveforms, a special kind of value trace that shows a graph of values over time. Since time is a discrete series of steps, it's very easy to line up in a graph. Hardware designers almost never single-step through Verilog, since the parallel execution makes it impossible to have the notion of a "current line" being executed.
Assembly Language
[edit | edit source]The C program you write is not what really runs on your PC. It gets compiled into assembly instructions which are then executed by the processor inside your PC. What is the equivalent of assembly language for Verilog?
Something as simple as the C code below translates into those assembly instructions:
main() {
int a = 2;
int b = a + 1;
}
0000000000400448 <main>:
400448: 55 push %rbp
400449: 48 89 e5 mov %rsp,%rbp
40044c: c7 45 f8 02 00 00 00 movl $0x2,0xfffffffffffffff8(%rbp)
400453: 8b 45 f8 mov 0xfffffffffffffff8(%rbp),%eax
400456: 83 c0 01 add $0x1,%eax
400459: 89 45 fc mov %eax,0xfffffffffffffffc(%rbp)
40045c: c9 leaveq
40045d: c3 retq
Which Is More Powerful, C or Verilog?
[edit | edit source]After reading all the restrictions about Verilog, you may be wondering why anyone would bother. Isn't software more powerful? Not always. If you need to perform very complex functions that are essentially always the same (think video compression, 3D graphics, etc.), then you can hardcode those in Verilog. In software, it would take many assembly instructions to perform large computations: all that a processor can process in one cycle is basically some simple operation on 64-bit values.
Imagine you need to perform 100 additions of 32-bit numbers at once. You'd need 100 instructions in software. In hardware, as long as you can afford the space, you could code this in as few steps as you want, including just one step:
reg [31:0] sum [99:0];
always @(posedge clk) begin
for(i=0;i<100;i++) begin
sum[i] <= a[i] + b[i];
end
end
The software version of that code would look identical, except it would take 100 cycles to execute instead of 1. Hardware is running 100X faster than software.