16-bit computer made using FPGA
16-bit computer on a FPGA
There are follow-ups of this topic:
- 32-bit FPGA computer,
- hardware sprites,
- the first game,
- PS/2 keyboard support,
- UART Loader,
- text mode,
- graphics mode,
- assembler,
- byte-oriented instructions.
I have made a 16-bit computer using my DE0-NANO FPGA board. The computer has 16-bit CPU, 64K 16-bit words, UART (115200 bps), and VGA (640x480, text-based frame buffer, 80x60 characters).
The Verilog code is on the github.
- 32-bit FPGA computer,
- hardware sprites,
- the first game,
- PS/2 keyboard support,
- UART Loader,
- text mode,
- graphics mode,
- assembler,
- byte-oriented instructions.
I have made a 16-bit computer using my DE0-NANO FPGA board. The computer has 16-bit CPU, 64K 16-bit words, UART (115200 bps), and VGA (640x480, text-based frame buffer, 80x60 characters).
The Verilog code is on the github.
At first I used a breadboard
The schematics of the current version is above
The 16-bit CPU has 8 general-purpose registers (r0 – r7), pc (program
counter), sp (stack pointer), ir (instruction register), mbr (memory buffer register), h (higher word
when multiplying, or remainder when dividing).
The address bus is 16 bits wide, addressing 65536 memory locations
(words). Data bus is also 16 bits wide, having each memory location 16 bits wide. This gives 65536
16-bits words, or 128KB.
Video output is VGA, 640x480. Text mode has 80x60 characters, each
character being 8x8 pixels in dimensions. Video framebuffer in text mode has 4800 16-bit
words (80x60 characters). The lower byte has the ASCII character, while the upper byte has the
attributes (3 bits for the background color, 3 bits for the foreground color, inverted, and the last two
bits unused).
It has two interrupts: IRQ0 and IRQ1. IRQ0 is connected to the KEY2 of the
DE0-NANO, while IRQ1 is connected to the UART. Whenever a byte comes to the UART, it generates an IRQ1.
Interrupt causes CPU to push flags to the stack, then to push PC to the stack and then to jump to the
location designated for the CPU:
- for the IRQ0, it is 0x0004, and
- for the IRQ1, it is 0x0008.
It is up to the programmer to put the code in those locations. Usually, it is
a JUMP instruction. To return from the interrupt routine, it is necessary to put the IRET instruction. It
pops the return address, and then pops the flags register, and then goes back into the interrupted program.
KEY1 of the DE0-NANO is used as the reset key. When pressed, it forces CPU
to go to the 0x0000 address. Usually there is a JUMP instruction to go to the main program.
VGA text mode
Text mode is 80x60 characters, occupying 4800 words. Lower byte is the
ASCII code of a character, while the upper byte is the attributes:
7
|
6
|
5
|
4
|
3
|
2
|
1
|
0
|
|
|
Foreground color,
inverted
|
Background color
|
||||
|
|
r
|
g
|
b
|
r
|
g
|
b
|
The foreground color is inverted so zero values (default) would mean white
color. That way, you don't need to set the foreground color to white, and by default (0, 0, 0), it is
white. The default background color is black (0, 0, 0). This means that if the upper (Attribute) byte is
zero (0x00), the background color is black, and the foreground color is white.
Attributes provide 8 foreground and 8
background colors
VGA female connector is connected via resistors to the GPIO-0 expansion
header of the DE0-NANO board:
- GPIO_R (pin 2, GPIO_00, PIN_A3) -> 68Ohm -> VGA_R,
- GPIO_G (pin 4, GPIO_01, PIN_C3) -> 68Ohm -> VGA_G,
- GPIO_B (pin 6, GPIO_03, PIN_D3) -> 68Ohm -> VGA_B,
- GPIO_HS (pin 8, GPIO_05, PIN_B4) -> 470Ohm -> VGA_HORIZONTAL_SYNC,
- GPIO_VS (pin 10, GPIO_07, PIN_B5) -> 470Ohm -> VGA_VERTICAL_SYNC.
UART interface
UART interface provides TTL serial communication on 115200kbps. It uses
one start bit, one stop bit, and eight data bits, no parity, no handshake.
UART is connected to the GPIO-0 expansion header of the DE0-NANO board:
- TX (pin 32, GPIO_025, PIN_D9) should be connected to the RX pin of the PC,
- RX (pin 34, GPIO_027, PIN_E10) should be connected to the TX pin of the PC.
UART is used within the CPU via IN, and OUT instructions. RX triggers the
IRQ1, which means that whenever a byte is received via UART, the IRQ1 will be triggered, forcing CPU to
jump to the 0x0008 address. There you should place the JUMP instruction to your UART interrupt routine.
Inside the UART interrupt routine, you can get the received byte by using
the IN instruction:
in r1, [64]; r1 holds now received byte from the
UART
To send a byte, first you need to check if the UART TX is free. You can do
it by using the in instruction:
loop:
in r5, [65] ; tx busy in r5
cmp r5, 0
jz not_busy ; if not busy, send back the received character
j loop
not_busy:
out [66], r1 ; send the character to the UART
Addresses used by the UART are in the following table:
Address
|
Description
|
64
|
Received byte from the
RX part of the UART (use the IN instruction).
|
65
|
0 if the TX part of the
UART is free to send a byte, 1 if TX part is busy.
|
66
|
Byte to be sent must be
placed here using the OUT instruction.
|
Assembler
The assembler for the CPU is again a fork of the customasm. This is a universal
assembler, which can be used to generate machine code for any CPU. All you need is the instructions
definition file. That file contains the list of instructions and the resulting machine code. For
example, here are several instructions for my CPU:
#cpudef
{
#bits 16
#tokendef reg
{
r0 = 0
r1 = 1
r2 = 2
r3 = 3
r4 = 4
r5 = 5
r6 = 6
r7 = 7
sp = 8
h = 9
}
nop -> 16'0x0000
halt -> 16'0xfff0
mov {dest: reg}, {src: reg} -> src[3:0] @ dest[3:0] @ 4'0x0 @ 4'0x1
mov {dest: reg}, {value} -> 4'0x0 @ dest[3:0] @ 4'0x1 @ 4'0x1 @ value[15:0]
...
}
The assembler is on the github.
UART loader
I have hardcoded the UART loader in the RAM memory. This means that
whenever I start the computer, RAM contains the loader and after the reset, the loader is started.
The loader initially sends the identification sequence to the PC, and the PC then sends the program
to be executed. All this is done via serial port (UART). On my PC, I use the USB-to-serial (TTL)
dongle. This way, I can develop programs on my PC and then load them on the board after the reset.
The protocol for the loader is a modification of the Raspbootin protocol, which I have used for my
bare metal programming.
Java client for uploading images is on the github.
Java client for uploading images is on the github.
Emulator
I have made an emulator for this computer. It is written in Java and
it supports full-speed execution, break points, and step-by-step execution (both step-into and
step-over). It has a separate window to display characters in the VGA framebuffer, and on that
window, when you press a key, it will generate an IRQ1, emulating the UART reception of a byte
("received" byte is a pressed key).
Java-based emulator is on the github.
Java-based emulator is on the github.
Conclusion
FPGA programming is fun, but also can be painful. Especially when you
need to wait couple of minutes for the design to be compiled and placed on your board. Also, as I
said earlier, the learning curve is very steep. You can run into a lot of problems at the beginning.
First of all, you need to figure that all the lines in the Verilog are "executed" in parallel. You
actually need to imagine the resulting hardware (or at least to have an idea what will be created)
when programming in Verilog.
Next, you need to read warnings! Some of those warnings will actually
tell you what you have done wrong in your design. I have also run into problems with the operator
precedence in Verilog. I had to place brackets to make an explicit order of evaluation, because it
was different from my usual experience (in other programming languages).
Also, I have used the Icarus Verilog to make the simulations. It is
way faster than using the Altera ModelSim tool, and much easier to work with. Unfortunately, it
cannot detect timing problems, which can occur, and which can make your design fail. If that is the
case, read those warnings again!
I will expand this text and write more texts about this computer and
my experience with the FPGA programming.
Comments
Comments powered by Disqus