Project 5 Display Controller and Character ROM

Introduction to Advanced Clocking Wizard and Character ROM

3456

Introduction

For this design, you will create a high resolution VGA controler and convert it to an HDMI video connection. Once you have a working digital display controller, you will set up memory used to show characters on your display.

Prelab

vga example

Background

Read through the background materials provided to the right, they’ll help you configure the clocking wizard, HDMI IP core, and the Character ROM. They will also provide background information on VGA and the timings required for high resolutions.

Requirements

1. Display a solid square at a fixed position on the display

After confirming that your VGA controller can output a 640x480 display, change it so it can display a 16x16 solid square. You can place the square at any coordinate on the screen as long as all the pixels within the square are visible. You square needs to be a different color from the background so it is visible. Making the position of your square reconfigurable through either parameters or inputs may make later requirements easier.

2. Display a bitmap within a 16x16 square

Consider the following verilog module:


module char_gen(
	input [3:0] line_addr,
	output [15:0] line_data
);

	reg [15:0] mini_rom[15:0];

	assign line_data = mini_rom[line_addr];

	initial
	begin
		mini_rom[0] =  16'b1111111111111111;
		mini_rom[1] =  16'b1000000000000001;
		mini_rom[2] =  16'b1000110000000001;
		mini_rom[3] =  16'b1000110000000001;
		mini_rom[4] =  16'b1000110011111101;
		mini_rom[5] =  16'b1000110111111001;
		mini_rom[6] =  16'b1000110000000001;
		mini_rom[7] =  16'b1000000000000001;
		mini_rom[8] =  16'b1000000000000001;
		mini_rom[9] =  16'b1001000000001001;
		mini_rom[10] = 16'b1000100000010001;
		mini_rom[11] = 16'b1000010000100001;
		mini_rom[12] = 16'b1000001010000001;
		mini_rom[13] = 16'b1000000100000001;
		mini_rom[14] = 16'b1000000000000001;
		mini_rom[15] = 16'b1111111111111111;
	end

endmodule

The module contains the data for a 16x16 1-bit (black or white) bitmap image. However it is stored in a 16-bit wide and 16-bit deep memory, meaning there are 256 bits of data in the rom (16*16). The width of a memory is how many bits are read of the output at each discrete address, and the depth is how many discrete addresses make up the memory. Each line is a 16-bit binary value, and different lines can be accessed by providing the correct 4-bit address to the module. Once the 16-bit ‘word’ is output from the rom, the desired bit can be selected using a 16-1 mux, which will require a 4-bit select number. Thus to access a desired data bit in this rom you need to provide a 4-bit ‘row’ address as well as a 4-bit ‘column’ address.



    wire [3:0] line_addr;
    wire [3:0] col_addr;
    wire [15:0] line_data;

    wire pix_data;

    //use line_addr as an input to get line_data as an output
    mini_rom(
        .line_addr(line_addr),
        .line_data(line_data)
    );

    //choose which bit based on the value of col_addr;
    assign pix_data = line_data[col_addr];


The square displayed in requirement 1 is composed of 256 individual pixels (16 pixels per row and 16 rows), each sent to the display at exact times. In order to display a 16x16 bitmap in place of the square, you will need to know the coordinate within the square at the time that pixel in the square is sent to the display. Thus, you need to calculate the correct line and column address for the ROM from your display controller’s horizontal and vertical counters.

Hint, if your square is at coordinate x:35, y:22, where is the pixel at ROM x:15, y:6 going to be displayed on the screen? Figure out a correlation between your ROM line and column address and the current display pixel counters, and use this to display the bitmap in place of your 16-bit square. Remember to still perform bounds checking on the square and only enable output from the ROM if it is in bounds.

3. Display an ascii character within a 16x16 square

The bitmap provided for requirement 2 is a small distributed-rom based memory. In this requirement you will interface with a rom that has a full ascii character set, which is stored in a block memory. The small rom in requirement 2 needed 4 bits of address to access every line of character data. The character rom implements 128 characters, meaning 7-bits of address are needed to address just the characters, for each character there are 16 lines of 16-bit wide data which means in addition to the 7-bit charachter address, a 4-bit line address makes up an 11-bit address. This is 32 kilobits (2^11 * 16), meaning it takes up a single block ram device on the zynq (the block rams are actually 36kb but that includes parity bits).

Refer to the background documentation for how to interface with the provided ascii character rom. Modify your system for displaying a 16x16 bitmap to use the data from the character rom instead. For this requirement you can set the ascii value of the character using the slide switches on your board (you can directly wire up to the character rom, you do not need to use the GPIO AXI IP Yet).

4. Control the Character displayed and its location through software

Modify your Display controller so it is a self contained module with the following inputs:

  • Ascii value [6:0]
  • loc_x [9:0]
  • loc_y [9:0]
  • ch_color [23:0] ==
  • bg_color [23:0] ==
  • pix_clk
  • pix_clkx5
  • rst

Have your module output any necessary signals required to output to a VGA or HDMI display. Do not integerate a clocking wizard IP into your module.

Create an axi IP that contains your display controller module and control the input parameters (ascii, location, color) using the output of AXI-connected registers. Package your module as an IP core, with the pixel clock and 5x pixel clock as seperate input.

When connecting your AXI module to the zynq, instead of connecting to the FCLK of the processor, instantiate a clocking wizard IP in your block design to generate your 1x and 5x pixel clocks. Connect the outputs of these to the corresponding inputs, but also use the 5x pixel clock as the bus clock for the AXI bus. Before synthesizing your system configure UART1 and instantiate an AXI GPIO Controller IP core connected to the switches and buttons on the blackboard (these processes are described in the prelab).

After synthesizing your system, write a program in vitis to control your complete display controller.

First write (and test) driver functions for your ip module it is recommended you implement similar functionality to the following functions:

void set_ch_ascii(char al)
void set_ch_loc(uint32_t x, uint32_t y);
void set_ch_color(uint8_t red, uint8_t blue, uint8_t green);
void set_bg_color(uint8_t red, uint8_t blue, uint8_t green);

//note if you prefer, you can concatenate red green and blue into a single 32-bit number (though only 24-bits are used).
// In this format you can use commonly "HEX Color" choosers on the internet to feed you a hex code directly.
//If you use 3 seperate values, you can break up the hex code too.

To aid in writing your program you can also use a struct to hold information for your display, an example of what you might want to create is provided:

typedef struct DispInfo
{
    char ascii;
    uint32_t x;
    uint32_t y;

    uint8_t fg_red;
    uint8_t bg_green;
    uint8_t bg_blue;

    uint8_t bg_red;
    uint8_t bg_green;
    uint8_t bg_blue;

} disp_info_t;

Then you can write a function that reads from your struct and calls the set functions within:

void update_disp( disp_info_t *data);

Remember, when accessing members from a pointer to a struct you can use -> notation:

//this does the same thing as dereferencing the pointer,
//then accessing the requested member
output = data -> x;

//this is an equivalent statement
output = (*data).x;

Also remember when passing pointers to functions you can use the address of operator (&) to pass a pointer:

//this is the actual struct variable declared
disp_info_t screen_data;

//this passes a pointer to the struct to update_disp
update_disp(&screen_dta);

After writing your driver functions, write a program that does the following:

  • Set the character displayed on the screen to the last character recieved over uart from your PC. (ignore newlines/returns so you get a displayable ascii character).

  • Use the buttons to move the character around on the screen

  • Use switches to set the color of the character. Using 12 switches you can set the 4-MSB of each color channel from a 3 groups of 4 switches. If you have 10 switches, use 3-bits per color.

You can set the background color to a constant value, or change it in your program as you please.

Challenges

1. Display more than one character on the screen

Have more than a single character on the screen at once. These characters do not need to be software controlled.