element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
FPGA
  • Technologies
  • More
FPGA
Blog OrangeCrab Game Console - Part 3
  • Blog
  • Forum
  • Documents
  • Quiz
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join FPGA to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: dang74
  • Date Created: 6 Jan 2023 10:16 PM Date Created
  • Views 11177 views
  • Likes 14 likes
  • Comments 8 comments
Related
Recommended

OrangeCrab Game Console - Part 3

dang74
dang74
6 Jan 2023

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.

image

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.

image

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.

image

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:

image

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. 

image 

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.

 

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

When we do so we are greeted with the following display:

image

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. 

  • Sign in to reply
  • dang74
    dang74 over 2 years ago in reply to DAB

    Wow, so you have experience in the original era.  A lot of people like myself were 'consumers' of video games back in the 80s and now dabble in retro graphics and homebrew games to reconnect with our childhood dreams to be game designers.  It's cool to see you were doing pixel art back in the 70s. 

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • dang74
    dang74 over 2 years ago in reply to javagoza

    Thank you Javagoza.  I am glad you are enjoying the series of blogs.  I am trying to find that special balance between detail and readability.  That can be tricky at times.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • dang74
    dang74 over 2 years ago in reply to genebren

    Thank you.  Again I must give credit to the open source tile-set.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • dang74
    dang74 over 2 years ago in reply to dougw

    Thanks Doug.  At the beginning of the project there were a lot of uncertainties.  Will the carrier board work?  Will the I/O drive strength be enough for VGA?  Will the display look blurry and jittery?  Will I be defeated by FPGA compiler errors?  So it was such a relief to see the graphics on the screen.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • DAB
    DAB over 2 years ago

    Takes me back to the first pixel level packages I worked on back in the 1970's.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
>
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube