PCB Assembly
Within a couple of weeks of placing my order the boards arrive. I am a sucker for the white solder mask. It has a shine not unlike the keys of a piano and I like the way it contrasts with the black silkscreen.
I am not well equipped at home for SMT so the surface mount parts were installed at work where I have access to a microscope and a good soldering station. After sacrificing some of my breaks the SMT assembly was finally complete.
On the home front my hobby grade Radio Shack soldering iron is adequate for through-hole so the final touches were done from the comfort of home. Somehow I managed to reverse the pin-out of the DB-9 connector footprints. To compensate for the error I had to install the joystick port connectors on the reverse side of the board. Still though I think the final results look pretty good.
Background Tiles
The FPGA design supports three graphics layers. The lowest priority layer is the tile backgrounds. Next is the sprite layer. Finally at the the very top we have a layer for displaying score and status. This blog will focus on the background tile layer.
A high level diagram showing how the background tiles are generated can be seen below:
The role each block plays can be summarized as follows:
Name Table: The name table is used to select each tile on the screen. As a secondary function the name table also selects the color palette used for the tile.
Pattern Table: The pattern table contains the pixel patterns for the tiles.
The relationship between the name table and pattern table is illustrated in the figure below. In this example the name table entry for row 1 - column 39 is 0x41. This forms the 8 upper bits of the pattern table address. This selects the letter A as the tile.
Color Memory: This memory stores the 9-bit RGB color for the pixels
Video Gen: The Video Gen block is responsible for VGA timing and produces the H_SYNC and V_SYNC signals.
The block also acts as a priority encoder and decides which pixel to display: background, sprite or score/status layer.
Tile Buffer Pair: This block contains two line buffers. On any given horizontal line one buffer is being stuffed with the RGB pixels.
The other line buffer meanwhile reads out the RGB pixels that were previously stored. Every two horizontal lines the roles of each line buffer switch.
The switch occurs every two lines because rather than using the 640 x 480 VGA full resolution we use 240 lines instead.
In addition to the line buffer the block also contains all the logic required to extract data from the name table, pattern table and color memory.
Generation of Colored Pixels
The diagram that follows shows how color pixels are generated. The current design only supports 16 colors. In the future this can be scaled up but in the meantime I am happy to limit the capabilities to be on par with the NES and early 80s arcade games. The Color Memory's upper two address bits come from the name table. The lower two bits originate in the pattern table. A shift register within the tile buffer block is used to shift out the pattern two bits at a time and in the process produces the two lower bits of the Color Memory address.
The Tile Buffer Pair block shown in the high level diagram that was presented earlier is implemented in the tile_buffer_pair.v Verilog file. Within tile_buffer_pair.v two instances of tile_buffer.v are instantiated. The Verilog code for these two files appears below.
tile_buffer_pair.v
module tile_buffer_pair // As the name implies this block manages data to and from 2x tile buffers #(parameter NAME_TABLE_ADDR_WIDTH = 11, parameter COLOR_SEL_WIDTH = 2, parameter PATTERN_TABLE_DATA_WIDTH = 16, parameter COLOR_WIDTH = 9, parameter VIDEO_PIPELINE = 2 ) ( input clk, input reset_n, input synch, //synchronization pulse that occurs 60Hz input buffer_sel, //This signal is used to put the tile buffers in either insertion or extraction mode. //buffer_sel = 1 tile_buffer_a will be in insertion mode. Outputs of tile_buffer_b will be selected //buffer_sel = 0 tile_buffer_b will be in insertion mode. Outputs of tile_buffer_a will be selected input [PATTERN_TABLE_DATA_WIDTH-1:0] pattern_table_data, input [COLOR_WIDTH-1:0] color, //RGB value received from color_memory input [8:0]x_scroll, input [7:0]y_scroll, output reg [COLOR_SEL_WIDTH-1:0] color_sel, output reg [NAME_TABLE_ADDR_WIDTH-1:0] name_table_addr, //The Name Table address has range from 0 to 1199 (40 columns x 30 rows = 1200 tiles) output reg [7:0] line_number, //line_number[2:0] forms the 3 least significant bits of the Pattern Table Address output reg [COLOR_WIDTH:0] background //When '1' the MSB is used to denote presence of pixel. Remaining bits form RGB color of the tile's pixel. ); //**************************** Signals ************************************ // We have 2x tile buffers // _a and _b suffixes are used to differentiate the signals that originate from the buffers wire [COLOR_SEL_WIDTH-1:0] color_sel_a; wire [COLOR_SEL_WIDTH-1:0] color_sel_b; wire [NAME_TABLE_ADDR_WIDTH-1:0] name_table_addr_a; wire [NAME_TABLE_ADDR_WIDTH-1:0] name_table_addr_b; wire [7:0] line_number_a; wire [7:0] line_number_b; wire [COLOR_WIDTH:0] background_a; wire [COLOR_WIDTH:0] background_b; //The following case statement decides which tile buffer to select for the outputs always @ (posedge clk) begin case (buffer_sel) 1'b0: begin color_sel <= color_sel_b; name_table_addr <= name_table_addr_b; line_number <= line_number_b; background <= background_a; end 1'b1: begin color_sel <= color_sel_a; name_table_addr <= name_table_addr_a; line_number <= line_number_a; background <= background_b; end endcase end //Instantiate tile_buffer_a and tile_buffer_b tile_buffer #(.NAME_TABLE_ADDR_WIDTH (NAME_TABLE_ADDR_WIDTH), .COLOR_SEL_WIDTH (COLOR_SEL_WIDTH), .PATTERN_TABLE_DATA_WIDTH (PATTERN_TABLE_DATA_WIDTH), .COLOR_WIDTH (COLOR_WIDTH),.VIDEO_PIPELINE (VIDEO_PIPELINE)) tile_buffer_a ( clk, reset_n, synch, buffer_sel, pattern_table_data, color, x_scroll, y_scroll, color_sel_a, name_table_addr_a, line_number_a, background_a ); tile_buffer #(.NAME_TABLE_ADDR_WIDTH (NAME_TABLE_ADDR_WIDTH), .COLOR_SEL_WIDTH (COLOR_SEL_WIDTH), .PATTERN_TABLE_DATA_WIDTH (PATTERN_TABLE_DATA_WIDTH), .COLOR_WIDTH (COLOR_WIDTH),.VIDEO_PIPELINE (VIDEO_PIPELINE)) tile_buffer_b ( clk, reset_n, synch, (!buffer_sel), pattern_table_data, color, x_scroll, y_scroll, color_sel_b, name_table_addr_b, line_number_b, background_b ); endmodule
tile_buffer.v
module tile_buffer /* This block generates the address for the name table. Additionally it provides the lower bits for patern table and color memory addresses. The 16-bit pattern_table_data is received. Each pixel is represented by 2x bits. The pattern_table_data is directed into a shift register that shifts 2x bits at a time. These two bits form the least significant bits of the color memory address When insert_mode = '1' data from the color memory is directed into the line buffer When insert_mode = '0' data is extracted out of the line buffer for display on the screen */ #(parameter NAME_TABLE_ADDR_WIDTH = 11, parameter COLOR_SEL_WIDTH = 2, parameter PATTERN_TABLE_DATA_WIDTH = 16, parameter COLOR_WIDTH = 9, parameter VIDEO_PIPELINE = 2 ) ( input clk, input reset_n, input synch, //60Hz synchronization pulse input insert_mode, input [PATTERN_TABLE_DATA_WIDTH-1:0] pattern_table_data, input [COLOR_WIDTH-1:0] color, //RGB data received from color_memory input [8:0]x_scroll, input [7:0]y_scroll, output [COLOR_SEL_WIDTH-1:0] color_sel, //2x LSBs of color_memory address output [NAME_TABLE_ADDR_WIDTH-1:0] name_table_addr, //Range is 0 - 1199 output reg [8:0] line_number, //line_number[2:0] forms the 3 least significant bits of the Pattern Table address output [COLOR_WIDTH:0] background //background tile's RGB color. When the MSB is set to '1' a pixel is present ); // *************** Constants ********************** //Constants associated with horizontal_counter and verical_counter parameter HORIZONTAL_TERMINAL_COUNT = 799; parameter VERTICAL_COUNTER_TERMINAL_COUNT = 524; parameter ASSERT_INSERTION_ENABLE = 1; parameter DEASSERT_INSERTION_ENABLE = 797; parameter FIRST_LINE = 0; parameter LAST_LINE = 479; parameter ASSERT_EXTRACT = 160 - VIDEO_PIPELINE; parameter DEASSERT_EXTRACT = 799 - VIDEO_PIPELINE; // Constants used to decode control_counter count parameter CONTROL_COUNTER_TC = 26; //number where control_counter rolls over parameter LOAD_PATTERN_REGISTER = 8; //This is where we load the shift register parameter ENABLE_SHIFT_COUNT = LOAD_PATTERN_REGISTER; //Starts shifting parameter DISABLE_SHIFT_COUNT = ENABLE_SHIFT_COUNT + 7; //Stops shifting parameter ASSERT_BUFFER_WRITE = ENABLE_SHIFT_COUNT + 2; //Start writing to line buffer parameter DEASSERT_BUFFER_WRITE = ASSERT_BUFFER_WRITE + 8; //Stop writing to line buffer parameter INCREMENT_TILE_COUNT = CONTROL_COUNTER_TC; // When control counter is done we increment tile counter // MISC Constants parameter LAST_TILE = 39; //last tile of row parameter COLOR_SEL_DELAYED = 2; //Register Signals reg [9:0] horizontal_counter; // This counter is reset at the beginning of each horizontal line reg [9:0] vertical_counter; // Counts the vertical lines on the screen reg insertion_enable; reg [4:0]control_counter; // This counter is used to control the processes of the shift register and line buffer reg [5:0] tile_counter; // Tile counter is used in the generation of the Name Table address reg [8:0] write_address; // write address of the line buffer reg [9:0] read_address; // read address of the line buffer reg buffer_write; reg [9:0] start_extract; // used in conjunction with 3 LSBs of x_scroll reg [COLOR_WIDTH-1:0] delayed_color_sel [COLOR_SEL_DELAYED-1:0]; reg [PATTERN_TABLE_DATA_WIDTH-1:0]pattern_register; //This is a shift register reg shift_pattern; reg extract_from_buffer; //While set data is read out of the line buffer //Wire Signals wire horizontal_counter_lsb_dec0; wire insertion_disable; wire its_a_pixel; wire [COLOR_WIDTH:0] write_data; //Data that will be written to the line buffer //******************************************************** // horizontal_counter and vertical_counter are local counters that // track the horizontal and vertical scan lines used in VGA // the synch signal is used to synchronize these counters to the video timing //******************************************************** //horizontal_counter always @ (posedge clk or negedge reset_n) begin if (!reset_n) horizontal_counter <= 0; else begin if (synch) horizontal_counter <= 0; //reset on synch else if (horizontal_counter == HORIZONTAL_TERMINAL_COUNT) horizontal_counter <= 0; //also reset on terminal count else horizontal_counter <= horizontal_counter +1; //otherwise increment end end assign horizontal_counter_lsb_dec0 = (horizontal_counter[0] == 1'b0) ? 1'b1 : 1'b0; //vertical_counter always @ (posedge clk or negedge reset_n) begin if (!reset_n) vertical_counter <=0; else begin if (synch) vertical_counter <=0; //reset on synch else if (horizontal_counter == HORIZONTAL_TERMINAL_COUNT) begin //At end of horizontal line.... if (vertical_counter == VERTICAL_COUNTER_TERMINAL_COUNT) vertical_counter <= 0; //Reset vertical counter if at terminal count else vertical_counter <= vertical_counter + 1; //otherwise increment end end end // insertion_enable //When horizontal_counter reaches ASSERT_INSERTION_ENABLE insertion_enable is set to '1' //When LAST_TILE of row is reached insertion_enable is reset. always @ (posedge clk or negedge reset_n) begin if (!reset_n) insertion_enable <=1'b0; else begin if (synch) insertion_enable <=1'b0; else if ((insert_mode) & (vertical_counter < (LAST_LINE + 1)) ) begin if ((horizontal_counter == ASSERT_INSERTION_ENABLE) & (vertical_counter[0] == 1'b0)) insertion_enable <= 1'b1; else if (tile_counter > LAST_TILE) insertion_enable <= 1'b0; end end end //insertion_disable is 'pulsed' when LAST_TILE is reached assign insertion_disable = ((control_counter == INCREMENT_TILE_COUNT) && (tile_counter== LAST_TILE))? 1'b1 : 1'b0; // ******************** // control_counter // ******************** // Decodes of the control_counter value are used to do such things as load and shift the shift register and write to the line buffer // If insertion_enable is set, the control_counter will continue to count up from 0, automatically resetting when CONTROL_COUNTER_TC is reached always @ (posedge clk or negedge reset_n) begin if (!reset_n) control_counter <= 0; else begin if(synch | !insertion_enable) control_counter <= 0; //hold at 0 if insert_enable is inactive else begin if (insertion_disable) control_counter <=0; else if (control_counter == CONTROL_COUNTER_TC) control_counter <=0; else control_counter <= control_counter + 1; end end end //********************************************** // tile counter goes from 0 to 39 //********************************************** always @ (posedge clk or negedge reset_n) begin if (!reset_n) tile_counter <= 0; else begin if (!insertion_enable) tile_counter <= 0; else if (control_counter == INCREMENT_TILE_COUNT) begin tile_counter <= tile_counter + 1; end end end // When the shift_pattern signal is '1' set the shift register will be shifted always @ (posedge clk or negedge reset_n) begin if (!reset_n) shift_pattern <= 1'b0; else begin if (synch) shift_pattern <= 1'b0; else if (control_counter == ENABLE_SHIFT_COUNT) shift_pattern <= 1'b1; else if (control_counter == DISABLE_SHIFT_COUNT) shift_pattern <= 1'b0; end end //***************************************** //pattern_register (shift register) //***************************************** // pattern_register is loaded when the control counter reaches LOAD_PATTERN_REGISTER // when shift_pattern is set, pattern_register is shifted two places to the left on every clock cycle. always @ (posedge clk or negedge reset_n) begin if (!reset_n) pattern_register <= 0; else begin if (control_counter == LOAD_PATTERN_REGISTER) pattern_register <= pattern_table_data; else if (shift_pattern) begin pattern_register[PATTERN_TABLE_DATA_WIDTH-1:2] <= pattern_register[PATTERN_TABLE_DATA_WIDTH-3:0]; //shift 2 bits to the left pattern_register[1:0] <= 2'b00; //fill most significant bits with 0 end end end assign color_sel = pattern_register[PATTERN_TABLE_DATA_WIDTH-1:PATTERN_TABLE_DATA_WIDTH-2]; //The two left most bits of pattern_register form the two least significant bits of the color memory address //*********************************************************** // write_address (for line buffer) //*********************************************************** always @ (posedge clk or negedge reset_n) begin if (!reset_n) write_address <= 0; else begin if (!insertion_enable) write_address <= 0; //hold at reset if we are not in the process of 'inserting' else if (buffer_write) write_address <= write_address +1; //we increment address when buffer_write is active end end //*********************************************************** // buffer_write //*********************************************************** always @ (posedge clk or negedge reset_n) begin if (!reset_n) buffer_write <= 1'b0; else begin if (control_counter == ASSERT_BUFFER_WRITE) buffer_write <= 1'b1; else if (control_counter == DEASSERT_BUFFER_WRITE) buffer_write <= 1'b0; end end // Line number is derived from vertical_counter and y_scroll. // We want to support 320 lines // In the case of 640 x 480 VGA resolution line_number = verical_counter[9:1] + y_scroll // In the case of 1080 x 960 resolution we line_number = verical_counter[9:2] + y_scroll always @ (posedge clk) begin line_number <= vertical_counter[9:1] + y_scroll; end // x_scroll[2:0] is implemented by delaying the screen itself // Larger x_scroll values are created by smaller delays // For instance for x_scroll[2:0] = 7 start_extract is ASSERT_EXTRACT // while for x_scroll[2:0] = 0 start_extract is ASSERT_EXTRACT +14 (Delayed by 7 pixels in our 320 x 240 downscaled resolution) always @ (posedge clk) begin case (x_scroll[2:0]) 3'b000: start_extract <= ASSERT_EXTRACT + 14; 3'b001: start_extract <= ASSERT_EXTRACT + 12; 3'b010: start_extract <= ASSERT_EXTRACT + 10; 3'b011: start_extract <= ASSERT_EXTRACT + 8; 3'b100: start_extract <= ASSERT_EXTRACT + 6; 3'b101: start_extract <= ASSERT_EXTRACT + 4; 3'b110: start_extract <= ASSERT_EXTRACT + 2; 3'b111: start_extract <= ASSERT_EXTRACT; default: start_extract <= ASSERT_EXTRACT + 14; endcase end // The extract_from_buffer signal is used to increment the line_buffer's read address. always @ (posedge clk or negedge reset_n) begin if (!reset_n) extract_from_buffer <= 1'b0; else if((vertical_counter< (LAST_LINE+1)) & (!insert_mode)) begin if (horizontal_counter == start_extract) extract_from_buffer <= 1'b1; else if (horizontal_counter == DEASSERT_EXTRACT ) extract_from_buffer <= 1'b0; end end //We delay color_sel to align it with the color data produced from color memory. integer p; always @ (posedge clk) begin delayed_color_sel[0] <= color_sel; for (p = 1; p < COLOR_SEL_DELAYED; p = p +1)begin delayed_color_sel[p] <= delayed_color_sel[p-1]; end end // Mapping to name_table_address_converter name_table_addr_conv #(.NAME_TABLE_ADDR_WIDTH(NAME_TABLE_ADDR_WIDTH)) name_table_address_converter( .clk (clk), .reset_n (reset_n), .line_number (line_number), .x_scroll (x_scroll), .tile_number (tile_counter), .name_table_address (name_table_addr) ); // If color_sel_delayed[1:0] is 0 the pixel is considered transparent. If it's not 0 then it's considered a pixel. assign its_a_pixel = (delayed_color_sel[COLOR_SEL_DELAYED-1] == 2'b00) ? 1'b0 : 1'b1; // Logic 0 means background, Logic 1 means valid pixel assign write_data = {its_a_pixel,color}; //Mapping to tile_memory tile_memory #(.ADDRESS_WIDTH (9),.DATA_WIDTH (COLOR_WIDTH+1)) tile_mem(clk, read_address[9:1], write_address,write_data, buffer_write, background); //read_address always @ (posedge clk) begin if (extract_from_buffer) read_address <= read_address +1; else read_address <= 0; end endmodule
Failed First Attempt
Last year while the design was still targeting the EP4C QMTECH board I managed to display a simple brick pattern. In the last few weeks I acquired a certain understanding of the quirks and limitations of the open source FPGA design tools, so I figured there was a reasonable chance that I would succeed in displaying the same brick pattern on the OrangeCrab.
What resulted was interesting enough but certainly wasn't the brick pattern I was expecting.
What was displayed instead was a pink screen with strange dots and blue smudges.
In the course of troubleshooting I momentarily altered the design to map the vertical line counter directly to the Color Memory. This produced the cool rainbow pattern shown below.
Finding the Problem
The problem was eventually traced to the Pattern Table memory. I neglected to guard memory writes with the condition that a write pulse is active. As a consequence the Pattern Table was continually overwritten.
The original code is as follows:
//writing to pattern_table always @ (posedge clk_in) begin ram[address_a] <= data_in; end //reading from pattern_table always @ (posedge clk) q <= ram[address_b];
After adding the write pulse condition the code now looks like this:
//writing to pattern_table always @ (posedge clk_in) begin if (wr) ram[address_a] <= data_in; end //reading from pattern_table always @ (posedge clk) q <= ram[address_b];
Simple Functional Example
Now it was time to put together a simple example to verify functionality. To do this I edited the first 8 words of the Pattern Table memory with a simple cross pattern.
The first 8 entries of pattern_table_init.hex are as follows:
0000 0080 0080 3FFC 0080 0080 0080 0080
When we connect the VGA we do indeed see the cross pattern that we were expecting.
So we're off to a good start but what about displaying something a little more interesting?
Designing a Background
Since the graphics capabilities and data format are similar to that of the NES I was able to use open source NES tools to design the graphics.
The NES Screen Tool by Shiru was particularly helpful. Its github repository can be found here:
https://github.com/BrokeStudio/NES-Screen-Tool
The NES Screen Tool is shown below. The right hand side of the screen is used for designing tile-set patterns while the left hand side is used to select the tiles for the Name Table.
Hats off to all the artists that worked on 1980s games. Designing a good looking game is harder than you think. I had to lift some open source art from the following website:
https://opengameart.org/content/tilesets-5
My tile-set and screen layout were heavily inspired by the Metroidvania assets posted by devurandom.
https://opengameart.org/content/16x16-dark-tech-base-tileset
Address Map
Before I could display the newly designed tiles on the OrangeCrab I had to first map the graphics blocks into the 6502 address space. The video_decode.v file that was developed to accomplish this can seen below.
module video_decode
#(parameter ADDRESS_WIDTH = 16)
(
input clk_in,
input [ADDRESS_WIDTH-1:0]address,
input chip_select,
input wr,
output name_table_wr,
output pattern_wr,
output color_wr,
output back_ground_wr,
output x_scroll_wr,
output y_scroll_wr,
output attribute_wr,
output sprite_pattern_wr,
output sprite_color_wr
);
//Address Map
//background 0x1000
//x_scroll 0x1001
//y_scroll 0x1002
//color_memory 0x1020
//sprite_color_memory 0x1040
// sprite_attributes 0x1080
// pattern_table 0x2000 - 0x2FFF
// score_pattern_table 0x3000 - 0x3FFF
// sprite_pattern_table 0x4000 - 0x5FFF
// name_table 0x6000 - 0x7FFF
// score name_table 0x8000 - 0x9FFF
//Constants
parameter PATTERN_TABLE_DECODE = 4'h2;
parameter SCORE_PATTERN_TABLE_DECODE = 4'h3;
parameter SPRITE_PATTERN_TABLE_DECODE_LO = 4'h4;
parameter SPRITE_PATTERN_TABLE_DECODE_HI = 4'h5;
parameter NAME_TABLE_DECODE_LO = 4'h6;
parameter NAME_TABLE_DECODE_HI = 4'h7;
parameter SCORE_NAME_TABLE_DECODE_LO = 4'h8;
parameter SCORE_NAME_TABLE_DECODE_HI = 4'h9;
parameter COLOR_DECODE = 11'h81;
parameter SPRITE_COLOR_DECODE = 11'h82;
parameter SPRITE_ATTRIBUTE_DECODE = 9'h21;
parameter BACKGROUND_DECODE = 16'h1000;
parameter X_SCROLL_DECODE = 16'h1001;
parameter Y_SCROLL_DECODE = 16'h1002;
//Generating write puleses
assign name_table_wr = (chip_select & wr & (address[15:12] == NAME_TABLE_DECODE_LO | address[15:12] == NAME_TABLE_DECODE_HI)) ? 1'b1 : 1'b0;
assign pattern_wr = (chip_select & wr &(address[15:12] == PATTERN_TABLE_DECODE))? 1'b1 : 1'b0;
assign color_wr = (chip_select & wr & (address[15:5] == COLOR_DECODE))? 1'b1 : 1'b0;
assign back_ground_wr = (chip_select & wr & address[15:0] == BACKGROUND_DECODE)? 1'b1 : 1'b0;
assign x_scroll_wr = (chip_select & wr & address[15:0] == X_SCROLL_DECODE)? 1'b1 : 1'b0;
assign y_scroll_wr = (chip_select & wr & address[15:0] == Y_SCROLL_DECODE)? 1'b1 : 1'b0;
assign attribute_wr = (chip_select & wr & address[15:7] == SPRITE_ATTRIBUTE_DECODE)? 1'b1 : 1'b0;
assign sprite_pattern_wr = (chip_select & wr & (address[15:12] == SPRITE_PATTERN_TABLE_DECODE_LO | address[15:12] == SPRITE_PATTERN_TABLE_DECODE_HI ))? 1'b1 : 1'b0;
assign sprite_color_wr = (chip_select & wr & (address[15:5] == SPRITE_COLOR_DECODE))? 1'b1 : 1'b0;
endmodule
Some of the graphics functions require 16 bits of data but the 6502 only has an 8-bit data bus. It was necessary then to have a holding register that allows the lower and upper 8 bits to be latched separately.
//instantiation of graphics_system graphics_system graphics_sys ( .clk_in (clk), .clk (clk), .reset_n (reset_n), .data_in ({16'h0, holding_register}), .address (address[15:0]), .wr(wr), .red (red), .green (green), .blue (blue), .interrupt(nmi), .h_synch (h_synch), .v_synch (v_synch) ); //Need to latch data into holding_register in two seperate 8-bit writes always @(posedge clk) begin if (wr & address == HOLDING_REG_LO) holding_register[7:0] <= data[7:0]; end always @(posedge clk) begin if (wr & address == HOLDING_REG_HI) holding_register[15:8] <= data[7:0]; end
6502 Code for Displaying Tiles
The final piece of the puzzle was to develop a bit of code to display tile graphics. The ASM6 assembler I am using saves the compiled code as a .BIN file. I used the following website to convert this to a hex format that can be recognized by the open source FPGA tools.
https://tomeko.net/online_tools/file_to_hex.php?lang=en
The converted hex values were saved to the rom_init.hex file.
The 6502 assembly code for loading the Color Memory, Name Table and Pattern Table appears below:
;VARIABLES
TICK_COUNTER EQU $0;
LED_COPY EQU $1;
LED EQU $80;
; ADDRESSES FOR VIDEO GENERATION
BG_COLOR EQU $1000 ;This is the screen's background color
COLOR_MEMORY EQU $1020
PATTERN_TABLE EQU $2000
NAME_TABLE EQU $6000
HOLDING_REGISTER_LO EQU $81 ;Latches the lower 8-bits
HOLDING_REGISTER_HI EQU $82 ;Latches the upper 8-bits
;CONSTANTS
ONE_SECOND EQU $3C;
.org $E000
RESET:
SEI ; disable IRQs
CLD ; disable decimal mode
LDX #$FF ;
TXS ; This sets stack pointer to $1FF ($100 + $FF)
; *** Initialize LED ******
LDA #$1;
STA LED_COPY;
STA LED;
; ****** Set screen's backround color
LDA #$0;
STA HOLDING_REGISTER_LO;
LDA #$0;
STA HOLDING_REGISTER_HI;
STA BG_COLOR; 0x0 corresponds to the color black
;*** Initialize COLOR MEMORY
LDX #$0;
STX HOLDING_REGISTER_HI;
COLOR_LOOP:
LDA COLORS, X;
STA HOLDING_REGISTER_LO;
STA COLOR_MEMORY, X;
INX;
CPX #$10;
BNE COLOR_LOOP;
; **** Initialize Name Table
; In all there are 1200 tiles
; Since the 6502 can only count from 0 to 255
; We divide things into 5 loops
; First 4 loops are repeated 256 times
; 5th loop is repeated 176 times
; 4x256 + 176 = 1200
LDX #$0;
BG_LOOP1:
LDA BACKGROUND, X;
STA HOLDING_REGISTER_LO;
LDA BKGND_PAL_SEL, X;
STA HOLDING_REGISTER_HI;
STA NAME_TABLE, X;
INX;
CPX #$00; After reaching 255 the X Registor rolls around to 0. So effectively we are comparing to 256 here
BNE BG_LOOP1;
LDX #$0;
BG_LOOP2:
LDA BACKGROUND+256, X;
STA HOLDING_REGISTER_LO;
LDA BKGND_PAL_SEL+256, X;
STA HOLDING_REGISTER_HI;
STA NAME_TABLE+256, X;
INX;
CPX #$00;
BNE BG_LOOP2;
LDX #$0;
BG_LOOP3:
LDA BACKGROUND+512, X;
STA HOLDING_REGISTER_LO;
LDA BKGND_PAL_SEL+512, X;
STA HOLDING_REGISTER_HI;
STA NAME_TABLE+512, X;
INX;
CPX #$00;
BNE BG_LOOP3;
LDX #$0;
BG_LOOP4:
LDA BACKGROUND+768, X;
STA HOLDING_REGISTER_LO;
LDA BKGND_PAL_SEL+768, X;
STA HOLDING_REGISTER_HI;
STA NAME_TABLE+768, X;
INX;
CPX #$00;
BNE BG_LOOP4;
LDX #$00;
BG_LOOP5:
LDA BACKGROUND+1024, X;
STA HOLDING_REGISTER_LO;
LDA BKGND_PAL_SEL+1024, X;
STA HOLDING_REGISTER_HI;
STA NAME_TABLE+1024, X;
INX;
CPX #$B0;
BNE BG_LOOP5;
;**** Programming Pattern Table
; Data width of Pattern Table is 16-bits
; Lower 8-bits are transfered from PATTERN_LO to HOLDING_REGISTER_LO
; Upper 8-bits are transfered from PATTERN_HI to HOLDING_REGISTER_HI
LDX #$0;
PATTERN_LOOP:
LDA PATTERN_LO, X;
STA HOLDING_REGISTER_LO;
LDA PATTERN_HI, X;
STA HOLDING_REGISTER_HI;
STA PATTERN_TABLE, X;
INX;
CPX #$00; See if it rolls over to 00
BNE PATTERN_LOOP;
Forever:
;jump back to Forever, infinite loop
JMP Forever;
NMI:
LDX TICK_COUNTER;
INX;
STX TICK_COUNTER;
CPX #ONE_SECOND;
BNE EXIT_INTERRUPT;
LDA #$0;
STA TICK_COUNTER ; Reset TICK_COUNTER
LDA LED_COPY;
EOR #$1 ; Toggle LED
STA LED_COPY;
STA LED;
EXIT_INTERRUPT
RTI;
.org $E200
COLORS .db $0, $49, $52, $7F, $0, $7, $27, $3F
.db $0, $8D, $D7, $0, $0, $0, $0, $0
.db $0, $0, $0, $0, $0, $0, $0, $0
.db $0, $0, $0, $0, $0, $0, $0, $0
.org $E400
INCLUDE BACKGROUND.ASM
.org $EC00
INCLUDE BACKGROUND_PALETTE_SELECT.ASM
.org $F200
INCLUDE PATTERN.ASM
.org $FFFA ;first of the three vectors starts here
.dw NMI ;when an NMI happens (once per frame if enabled) the
;processor will jump to the label NMI:
.dw RESET ;when the processor first turns on or is reset, it will jump
;to the label RESET:
.dw 0 ;external interrupt IRQ is not used in this tutorial
; ****** Website for converting BIN File to HEX **************
; https://tomeko.net/online_tools/file_to_hex.php?lang=en
The Final Result
After compiling the design and programming the board all that's left to do is plug in the VGA cable.
When we do so we are greeted with the following display:
This is very much in the style of the NES, and while it might not be much, after struggling with the open source tools this bit of success was definitely welcome. Is this a good omen and a sign that the next successes will come easier? Probably not, but it does show that sometimes patience and hard work is rewarded.