Added graphics mode in the FPGA computer
Added graphics mode to the FPGA computer
This is another follow-up of the original FPGA Computer post.
I have added the graphics mode to the FPGA computer - 320x240 pixels, 8 colors for each pixel. Framebuffer starts at the same address as the text mode one (2400 decimal), but it now displays pixels, instead of characters.
I have added the graphics mode to the FPGA computer - 320x240 pixels, 8 colors for each pixel. Framebuffer starts at the same address as the text mode one (2400 decimal), but it now displays pixels, instead of characters.
Each pixel can have one of eight colors. There are two pixels per byte in the framebuffer:
7 6 5 4 3 2 1 0
x r g b x r g b
For example, if you want to draw two red pixels at the (0, 0) coordinates (top left corner), you need to
put the following byte into the location 2400:
7 6 5 4 3 2 1 0
0 1 0 0 0 1 0 0
Or, 0x44 in hex.
Since the default mode is text mode, I have devised a system to switch video modes:
mov r1, 1
out [128], r1
This code will switch to the graphics mode of 320x240. To switch back to the text mode, you need to execute the following code:
mov r1, 0
out [128], r1
The implementation in Verilog was not complicated compared to the text mode. First of all, I had to decide which resolution to implement. I have chosen 320x240 with 8 colors, because it consumes 38400 bytes, which is the least amount of memory with the decent resolution and number of colors. I could not have more pixels, since that would consume more RAM than the computer has (64KB).
Even this mode consumes more than half of the available memory, so I can always make other modes not so demanding in memory (reduce the number of colors). For example, having the same 320x240 black and white framebuffer would consume 9600 bytes, or approx. 9KB.
Next, the implementation has two pixels per byte of the framebuffer. So, I have recycled the text mode vga module and used the same two variables: x and y to go through all the pixels of the screen. Then I had to fetch in advance the next word (two bytes - remember, memory is organized in 32KW, having data bus 16 bits wide) containing next four pixels. So the algorithm was simple:
if ((x & 7) == 7) begin
// when we are finishing current word,
// containing four pixels,
// we need to fetch in advance
// the next word (x+1, y)
// (at the last pixel of the current character,
// let's fetch next)
rd <= 1'b1;
wr <= 1'b0;
addr <= VIDEO_MEM_ADDR + ((xx >> 2)+(yy * 80) + 1);
mem_read <= 1'b1;
end
end
// when we start the horizontal blanking,
// and we need to go to the next line,
// we need to fetch in advance the first word
// in the next line (0, y+1)
rd <= 1'b1;
wr <= 1'b0;
mem_read <= 1'b1;
if ((y & 1) == 1) begin
addr <= VIDEO_MEM_ADDR + ((yy + 1) * 80);
end
else begin
addr <= VIDEO_MEM_ADDR + ((yy) * 80);
end
end
// when we start the vertical blanking,
// we need to fetch in advance the first word at (0, 0)
rd <= 1'b1;
wr <= 1'b0;
mem_read <= 1'b1;
addr <= VIDEO_MEM_ADDR + 0;
end
When we set the addres bus to the address of the word (containing pixels) to be fetched,
then we receive that word using the following code:
if (mem_read) begin
pixels <= data;
rd <= 1'bz;
wr <= 1'bz;
mem_read <= 1'b0;
end
r <= pixels[12 - ((xx & 3) << 2) + 0] == 1'b1;
g <= pixels[12 - ((xx & 3) << 2) + 1] == 1'b1;
b <= pixels[12 - ((xx & 3) << 2) + 2] == 1'b1;
end
else begin
// blanking -> no pixels
r <= 1'b0;
g <= 1'b0;
b <= 1'b0;
end
Here is the assembler example which draws two pixels, three lines and a circle on the screen:
mov r0, 1
out [128], r0 ; set the video mode to graphics
mov r0, 0 ; x = 0
mov r1, 100 ; y = 100
mov r2, 7 ; white color (0111)
call pixel
inc r0 ; x = 1
mov r2, 4 ; red color (0100)
call pixel
mov r2, 4 ; red color (0100)
mov r0, 50 ; A.x = 50
mov r1, 50 ; A.y = 50
mov r3, 150 ; B.x = 150
mov r4, 150 ; B.y = 150
call line
mov r2, 2 ; green color (0010)
mov r0, 50 ; A.x = 50
mov r1, 50 ; A.y = 50
mov r3, 150 ; B.x = 150
mov r4, 50 ; B.y = 50
call line
mov r2, 1 ; blue color (0001)
mov r0, 150 ; A.x = 150
mov r1, 50 ; A.y = 50
mov r3, 150 ; B.x = 150
mov r4, 150 ; B.y = 150
call line
mov r2, 7 ; white color (0111)
mov r0, 150 ; x = 150
mov r1, 150 ; y = 150
mov r3, 50 ; r = 50
call circle
Or, 0x44 in hex.
Since the default mode is text mode, I have devised a system to switch video modes:
mov r1, 1
out [128], r1
This code will switch to the graphics mode of 320x240. To switch back to the text mode, you need to execute the following code:
mov r1, 0
out [128], r1
So, number 1 at the port 128 sets the video mode to 320x240,
while 0 sets to the text mode.
The implementation in Verilog was not complicated compared to the text mode. First of all, I had to decide which resolution to implement. I have chosen 320x240 with 8 colors, because it consumes 38400 bytes, which is the least amount of memory with the decent resolution and number of colors. I could not have more pixels, since that would consume more RAM than the computer has (64KB).
Even this mode consumes more than half of the available memory, so I can always make other modes not so demanding in memory (reduce the number of colors). For example, having the same 320x240 black and white framebuffer would consume 9600 bytes, or approx. 9KB.
Next, the implementation has two pixels per byte of the framebuffer. So, I have recycled the text mode vga module and used the same two variables: x and y to go through all the pixels of the screen. Then I had to fetch in advance the next word (two bytes - remember, memory is organized in 32KW, having data bus 16 bits wide) containing next four pixels. So the algorithm was simple:
- during the
visible scanline processing, the video module fetches next four pixels when displaying the third
pixel of the current word in a row
else if (x < 640 && !mem_read)
beginif ((x & 7) == 7) begin
// when we are finishing current word,
// containing four pixels,
// we need to fetch in advance
// the next word (x+1, y)
// (at the last pixel of the current character,
// let's fetch next)
rd <= 1'b1;
wr <= 1'b0;
addr <= VIDEO_MEM_ADDR + ((xx >> 2)+(yy * 80) + 1);
mem_read <= 1'b1;
end
end
- during the horizontal blanking, the video module fetches
first four pixels at the beginning of the next row
else if ((x >= 640)
&& (y < 480)) begin// when we start the horizontal blanking,
// and we need to go to the next line,
// we need to fetch in advance the first word
// in the next line (0, y+1)
rd <= 1'b1;
wr <= 1'b0;
mem_read <= 1'b1;
if ((y & 1) == 1) begin
addr <= VIDEO_MEM_ADDR + ((yy + 1) * 80);
end
else begin
addr <= VIDEO_MEM_ADDR + ((yy) * 80);
end
end
- during the vertical blanking, the video module fetches the
first four pixels at the top left corner of the screen (start of the video memory - address 2400
decimal).
if ((x >= 640) && (y >= 480))
begin// when we start the vertical blanking,
// we need to fetch in advance the first word at (0, 0)
rd <= 1'b1;
wr <= 1'b0;
mem_read <= 1'b1;
addr <= VIDEO_MEM_ADDR + 0;
end
if (mem_read) begin
pixels <= data;
rd <= 1'bz;
wr <= 1'bz;
mem_read <= 1'b0;
end
Received pixels are stored in the pixels register.
The actual output of the pixels register to the r, g, and b wires of the vga connector
is then simple:
if (valid) beginr <= pixels[12 - ((xx & 3) << 2) + 0] == 1'b1;
g <= pixels[12 - ((xx & 3) << 2) + 1] == 1'b1;
b <= pixels[12 - ((xx & 3) << 2) + 2] == 1'b1;
end
else begin
// blanking -> no pixels
r <= 1'b0;
g <= 1'b0;
b <= 1'b0;
end
The xx and yy variables contain actual x and y positions divided by two. x and y iterate in the
640x480 range, while xx and yy iterate in the range of 320x200:
assign xx = x >> 1;
assign yy = y >> 1;
Assembler example
Assembler examples can be found here.Here is the assembler example which draws two pixels, three lines and a circle on the screen:
mov r0, 1
out [128], r0 ; set the video mode to graphics
mov r0, 0 ; x = 0
mov r1, 100 ; y = 100
mov r2, 7 ; white color (0111)
call pixel
inc r0 ; x = 1
mov r2, 4 ; red color (0100)
call pixel
mov r2, 4 ; red color (0100)
mov r0, 50 ; A.x = 50
mov r1, 50 ; A.y = 50
mov r3, 150 ; B.x = 150
mov r4, 150 ; B.y = 150
call line
mov r2, 2 ; green color (0010)
mov r0, 50 ; A.x = 50
mov r1, 50 ; A.y = 50
mov r3, 150 ; B.x = 150
mov r4, 50 ; B.y = 50
call line
mov r2, 1 ; blue color (0001)
mov r0, 150 ; A.x = 150
mov r1, 50 ; A.y = 50
mov r3, 150 ; B.x = 150
mov r4, 150 ; B.y = 150
call line
mov r2, 7 ; white color (0111)
mov r0, 150 ; x = 150
mov r1, 150 ; y = 150
mov r3, 50 ; r = 50
call circle
First we switch to the graphics mode
(out instruction). Then we draw two pixels. The
pixel subroutine has three parameters: r0 (x-coordinate), r1(y-coordinate) and r2 (color). The color is
determined by the content that is put in the r2 register. It is 0x7, which means that all three bits of a pixel are set to 1, having the
white color.
Three lines are drawn using the line subroutine. It has five
parameters: r0 (x1-coordinate), r1 (y1-coordinate), r2
(color), r3 (x2-coordinate), and
r4 (y2-coordinate). The circle subroutine has four
parameters: r0 (x-coordinate), r1 (y-coordinate), r2 (color), and r3 (radius).
Here is the snapshot of the emulator:
Conclusion
Adding graphics mode was not that
complicated. I have decided to have the 320x240 resolution having each pixel independent of the other
(no attributes). That approach consumed quite a lot of memory, but this is not important since this
computer will be comparable to the vintage platforms of the 70s and 80s.
The graphics module is on the github.
The graphics module is on the github.
Comments
Comments powered by Disqus