ForewordIn the previous post I gave a brief introduction about what this series of Blog Posts is about, which is creating a C64 system on an FPGA.
Originally I planned for this post to create a 6502 system for the FPGA that can run Klaus Dormann's 6502 Test Tuite.
However, after some thought, I decided to dedicate this post about the principles around Verilog.
Verilog is the language we will be using to create our C64 FPGA implementation. Verilog is very similar to what you would expect from a computer language.
However, Verilog has some subtle principles that works differently from conventional programming languages. Hence, I thought the reason for this post, especially for newcomers that reads this post that has experience of conventional programming languages.
Since I will be using Arlet Ottens's 6502 core as the starting point for the C64 FPGA implementation, I will be using pieces from this core to explain some of the principles of Verilog.
Unpacking the Arlet Core
Let us start off by having a look at the source code of Arlet Ottons' 6502 core. This will just give newcomers a better idea on FPGA programming.
If you have a look at source code for Arlet's core on github, you see two important files: cpu.v and ALU.v.
The *.v file extension denotes a Verilog file. More of the syntax used in this file in a moment.
Let us open up cpu.v for starters. You will see that it starts with the following lines
module cpu( clk, reset, AB, DI, DO, WE, IRQ, NMI, RDY ); input clk; // CPU clock input reset; // reset signal output reg [15:0] AB; // address bus input [7:0] DI; // data in, read bus output [7:0] DO; // data out, write bus output WE; // write enable input IRQ; // interrupt request input NMI; // non-maskable interrupt request input RDY; // Ready signal. Pauses CPU when RDY=0
In the first line we have a module declaration, followed by a set of parameters it can accept.
In the next set of lines you declare for each parameter whether it is an input or an output.
You might have noticed that some parameters have an have numbers in brackets preceding their name.
Parameter DI is one such example. [7:0] means the input DI consists of 8 wires (or eight bits), been numbered from 7 down to 0.
For declarations that doesn't have the square bracket notation, the size of the relevant parameter is only a single bit.
Let us look further down:
reg [15:0] PC; // Program Counter reg [7:0] ABL; // Address Bus Register LSB reg [7:0] ABH; // Address Bus Register MSB wire [7:0] ADD; // Adder Hold Register (registered in ALU) reg [7:0] DIHOLD; // Hold for Data In reg DIHOLD_valid; // wire [7:0] DIMUX; // reg [7:0] IRHOLD; // Hold for Instruction register reg IRHOLD_valid; // Valid instruction in IRHO
This is almost like variable declarations. You may have noticed that I use the term "almost". A variable store is supposed to store a value. Some of the declarations above, does in fact store a value. However, declarations with the word 'wire' doesn't store a value at all, and is literally a wire connecting two components.
Let us now see how values gets assigned to these components.
Firstly an example of a wire assignment:
assign DIMUX = ~RDY ? DIHOLD : DI;
In this case the DIMUX wire set get assigned to the output of a combinational logic driven by the ready signal.
Registers gets assigned values via always blocks. More on this in the next section.
You might be wondering where the file ALU.v is been utilised. Scrolling down in cpu.v to the following snippet will reveal the answer:
ALU ALU( .clk(clk), .op(alu_op), .right(alu_shift_right), .AI(AI), .BI(BI), .CI(CI), .BCD(adc_bcd & (state == FETCH)), .CO(CO), .OUT(ADD), .V(AV), .Z(AZ), .N(AN), .HC(HC), .RDY(RDY) );
So, what is happening here? The double ALU on the first line is a bit confusing, so let us rewrite it a bit for explanatory purposes:
ALU alu_inst( .clk(clk), .op(alu_op), .right(alu_shift_right), .AI(AI), .BI(BI), .CI(CI), .BCD(adc_bcd & (state == FETCH)), .CO(CO), .OUT(ADD), .V(AV), .Z(AZ), .N(AN), .HC(HC), .RDY(RDY) );
This declaration basically declares an instance called alu_inst of the module ALU defined in ALU.v. This is like Object orientation in action :-)
The identifiers that is preceded by dot is module parameters defined within the ALU module. The identifiers within brackets is the corresponding signal within the containing module to which the corresponding ALU parameter should connect to.
This gives us more or less an idea on how modules gets wired together.
With all this said, you might be wondering where the parameters of the CPU module is linking to. The cpu will be contained within an module called the top module, which we will develop in the next post.
Basically every verilog project will contain a top module and is the only module of which you don't need to create in instance of. In object oriented terms, you can almost think of the top module as a singleton.
Always BlocksIn the previous section I mentioned that registers gets assigned with an always block.
Let us us now look into always blocks in more detail.
Here is an example of an always block:
always @(posedge clk) if( state != PUSH0 && state != PUSH1 && RDY && state != PULL0 && state != PULL1 && state != PULL2 ) begin ABL <= AB[7:0]; ABH <= AB[15:8]; end
You will see that for the assignment operator we use <= instead of just a =. This means that the assignment doesn't happen right away, but rather happens the moment when the clock signal transitions from a low to to a high (as indicated by the always statement).
Under the hood in the FPGA, this always block will translate into a number of D flip-flops (also called Data or delay flip-flop). When using Verilog you are quite shielded into which components your code gets translated into. However, just for better understanding I am going to pause a while at the D Flip Flop.
Firstly, here is the visual representation symbol for a D Type Flip-Flop:
This flop-flop receives its input via the D input. As mentioned earlier, changing the input doesn't change the contents of the flip-flop right away. You first need to apply a clock pulse.
The clock input is donated by the input in the symbol by the small triangle. Each time this input transitions from a low to a high, the contents of the flip-flop will change to whatever value is present on the Data input during the clock transition.
I just want to go back briefly to our always block example. Another thing of importance with this always block is that during the clock transition, ABL and ABH will change its values simultaneously. This is also in contrast with conventional programming languages where you will first execute the first statement, and then the second one.
If my last statement confused you, hopefully the following diagram will clear it up:
Another interesting always block scenario is the following:
always @(posedge clk) begin a <= in; b <= a; c <= b; end
In this always block, the element c will only output the value of in, after three clock cycles. The following diagram illustrate this scenario:
always @* if( state == FETCH || state == REG || state == READ ) alu_shift_right = shift_right; else alu_shift_right = 0;
In this always block we didn't explicitly specified a signal to use for clocking. Instead, using the asterisk we inform verilog to generate the sensitivity list, which will basically trigger an assignment whenever one of the input changes.
The interesting thing is that this always block will synthesise a computational block and alu_shift_right will be synthesised as a wire even though it is declared as a register.
This is a bit of a corner case we should be aware of that a register might not always be synthesised as flip-flop.
In SummaryIn this post we discussed the principles of Verilog by using the source code of Arlet Ottens's core as an example.
From the topics we covered was registers, wires and Always blocks.
In the next post we will be developing the top module to accomplish a workable 6502 system that can execute 6502 Test Suite developed by Klaus Dormann.
Till next time!