Lab 10: Character Display

The purpose of this laboratory is to create a character generator circuit for displaying ASCII text characters on the VGA display. You will also create a simple circuit that writes characters to the screen using the switches and buttons.

The average time to complete this lab last year was 8 hours; however, changes have been made to shorten the lab time this year, and we will review the preliminary material in class ahead of time.

Learning Outcomes

  • Understand how to use internal BRAM memories
  • Draw characters from a font ROM onto the screen

Preliminary

Character Display Organization

Displaying text is an important function of a video controller. Dedicated circuits are often used to facilitate the display of text characters on a screen. To display text on our VGA display, we will organize the 640x480 display area into larger “tiles” where each tile represents a character location. In this lab, the size of each character is 8 pixels wide and 16 pixels high. Mapped onto a 640x480 display, this font will display 80 text characters in each line (i.e., 640 pixels for each row divided by 8 columns per character) and 30 lines (i.e., 480 lines divided by 16 pixels per character, 480 / 16). The layout of the 80 x 30 character screen is shown below.

When drawing text characters on the screen, we will refer to several coordinate systems. The first coordinate system is the display coordinate system, used for locating individual pixels on the display (i.e., 640x480). The top left pixel of the display is labeled (0,0), and the lower right pixel of the display is (639,479). The pixel_x and pixel_y outputs from the VGA timing controller reference this display coordinate system.

The character coordinate system is used to specify the location of characters on the screen (i.e., the 80x30 character layout shown above). The top left character position is (0,0), and the lower right character is (79,29). Every pixel in the display is mapped to one of these characters. To convert the x coordinate of a pixel into its corresponding character column is done by dividing the x pixel value by 8 without rounding. To convert the y coordinate of a pixel into the corresponding character row is done by dividing the y pixel value by 16 (again without rounding). For example, the pixel (x=35, y=41) in the display coordinate system will map to the character located at character position (4, 2) (i.e., 35/8, 41/16), which contains the ‘T’ in the figure above.

This division can easily be done in digital hardware by taking the top seven bits of pixel_x for the x-character location, and taking the top five bits of pixel_y for the y-character location. The following SystemVerilog code demonstrates how to generate the character positions from the pixel positions:

assign char_x_pos = pixel_x[9:3];
assign char_y_pos = pixel_y[8:4];

There is also a sub-character coordinate system for pixels within an individual character. As described above, each character is 8 pixels wide and 16 pixels high. The position of the top left pixel of a character is (0,0), and the lower right pixel of a character is (7,15) as shown in the figure below.

Every pixel in the display coordinate system maps to a specific character in the character coordinate system and to a pixel within the sub-character coordinate system. The x and y locations in the sub-character coordinate system can be found by performing a modulo 8 and modulo 16 operation on the pixel_x and pixel_y values, respectively. Since these are powers of 2, this can be done easily in hardware by just taking the lower bits of the pixel_x and pixel_y values:

assign char_x_pixel = pixel_x[2:0];
assign char_y_pixel = pixel_y[3:0];

Answer the following questions to demonstrate your understanding of the organization of the characters on the display:

How many characters can be displayed on the 640 x 480 display using this font?

What is the character coordinate associated with the pixel at location (279, 173)

What is the sub-character coordinate associated with the pixel at location (279, 173) (i.e., what position is this pixel within a 8x16 character)?

Font ROM

The appearance of the characters on the screen is determined by a font ROM. The font ROM is a read-only memory that stores the pixel pattern for each character in the character set. This is provided to you as char_gen/font_rom.sv. The font ROM stores one bit for each of the 8x16 pixels for each character, for a total of 128 bits per character. A ‘1’ indicates the pixel should be displayed in the foreground (i.e., white for white text), while a ‘0’ indicates the pixel is in the background. The font ROM is organized as a 1-D array of 8-bit words, where each word corresponds to one row of pixels for a character. The text below demonstrates the contents of the font ROM for the upper-case character ‘A’:

   8'b00000000, // 0
   8'b00000000, // 1
   8'b00010000, // 2    *
   8'b00111000, // 3   ***
   8'b01101100, // 4  ** **
   8'b11000110, // 5 **   **
   8'b11000110, // 6 **   **
   8'b11111110, // 7 *******
   8'b11000110, // 8 **   **
   8'b11000110, // 9 **   **
   8'b11000110, // a **   **
   8'b11000110, // b **   **
   8'b00000000, // c
   8'b00000000, // d
   8'b00000000, // e
   8'b00000000, // f

The font ROM for the character ‘A’ shown above will create the character shown below:

The font_rom.sv contains pixel patterns for all 128 characters in the 7-bit ASCII character set, including upper and lower case letters, numbers, punctuation, and special characters. Since each character takes 16 rows in the ROM, the entire character set takes 128 characters x 16 rows/character = 2048 rows in the ROM. In essence, we are mapping a 2-D array of data (128 characters and 16 rows/character) into a 1-D ROM. Since these are powers of two, we can easily generate the ROM address for a given character and row by taking the character value (7 bits) as the upper bits of the address and the row number (4 bits) as the lower bits of the address.

This ROM fits into the size of a single BRAM and will be mapped to one of the BRAMs on your FPGA board. The font ROM is a synchronous memory, meaning it requires one clock cycle to access the memory contents.

Answer the following questions after reviewing the char_gen/font_rom.sv file:

How wide, in bits, is each word of this ROM?

How many bits are required to define a single character in this ROM?

What is the total size, in bits, of this ROM?

What is the value of the word at address 0x32a in this ROM?

What character is associated with address 0x32a in this ROM?

What is the first address in the font ROM that stores the contents for the character ‘A’?

Assume that pixel_x=279 and pixel_y=170 and you are trying to display the character ‘A’. What index will you read from in the font rom?

Assuming the same situation described in the previous question, what is the corresponding pixel value for the character ‘A’ at the given location?

Refer to the timing diagram below and answer the following questions. For each answer, provide your response as an 8-bit SystemVerilog value (i.e., 8’b00000000).

What is the value of the ‘data’ output between clocks 1 and 2 (i.e., 1.5)?

What is the value of the ‘data’ output between clocks 2 and 3?

What is the value of the ‘data’ output between clocks 3 and 4?

Top-Level Design

The goal of this lab is to maintain a memory (called the character memory) of which character should be displayed at each position on the screen, and to display them on the screen by looking up the appropriate pixel patterns from the font ROM. It’s not very interesting to just have a static display; we also want to be able to change which characters are displayed on the screen. Our top-level design will allow the user to write new characters to the memory using two methods:

  1. Sending a character over UART.
  2. Setting the switches to a character value and pressing a button to write that character to the screen.

In both cases, you will maintain an invisible cursor on the screen that indicates where the next character will be written. This will increment left-to-right and top-to-bottom as new characters are received.

To implement this system, you will create the following top-level design that integrates several of the components you have designed in previous labs, including the VGA timing controller, the UART receiver, and the seven segment display controller.

Note: The above diagram does not show every element of the design, certain items are omitted for clarity (e.g., clocks, resets, and logic for updating the foreground and background colors).

Some notable items in the design include:

  1. The VGA timing controller constantly iterates through all pixel locations on the screen, outputting the current pixel coordinates (pixel_x and pixel_y) and synchronization signals (h_sync and v_sync). The main challenge of this lab, is to determine what color to output for each (x, y) pixel, foreground (i.e., the text color) or background.
  2. The Character Generator module, which is discussed in more detail later, internally contains a memory of which character to display at each position on the screen, and an instance of the font ROM to determine how to draw each character. The character generator takes the current pixel coordinates as input and outputs whether the pixel should be foreground or background based on the character memory and font ROM.
  3. Multiplexing logic uses the pixel_out signal from the character generator to determine whether to output the foreground or background color for each pixel, or to output black for the blanking intervals. The color is fed through registers before being output to the VGA ports to eliminate any output glitching. The h_sync and v_sync signals are fed through three registers to balance the latency of the character generator module.
  4. The Character Generator also has a write port, that allows the top-level design to change which character is stored at each position on the screen. The Char Write Logic, takes outputs from the UART receiver and the switches and btnc to determine when to write a new character to the character memory. Whenever a new character is received from the UART or the btnc is pressed, the char write logic will write a new character to the character memory at the current cursor location, and then update the cursor location to the next position on the screen.
  5. When btnl is pressed, the foreground color is updated to the value of the switches, and when btnr is pressed, the background color is updated to the value of the switches.
  6. For convenience, the 4-digit seven segment display is used to show the decimal value of the switches.

The Character Generator

This module:

  1. Stores which character to display at each position on the screen in a memory called the character memory, and allows writing to this memory to change the characters that are displayed on the screen.
  2. Can be provided with a specific pixel location (pixel_x and pixel_y) and will determine whether the pixel at that location should be on (i.e., foreground) or off (i.e., background) based on the character memory and the font ROM (it has an instance of the font ROM within it), and outputs a 1 or 0 on the pixel_out port.

The following diagram illustrates how the character generator uses the character memory and the font ROM to determine whether a given pixel should be foreground or background. Review these slides for an explanation of this example.

Exercises

Make sure to merge in any changes from the startercode before starting the lab.

The following are the exercises for this lab:

Exercise #1: Character Generator

Begin this exercise by creating a new SystemVerilog file named char_gen/char_gen.sv with the following ports:

Parameter Default Function
FILENAME ”” Specifies the filename of the initial contents of the memory
Module Name: char_gen      
Port Name Direction Width Function
clk Input 1 100MHz System Clock
char_we Input 1 Character write enable
char_addr Input 12 The write address of the character memory
char_value Input 7 The 7-bit value to pad and write into the character memory
pixel_x Input 10 The column address of the current pixel
pixel_y Input 9 The row address of the current pixel
pixel_out Output 1 The value of the character output pixel

Note: The flip-flops in this module do not require resets, so this module does not contain a reset signal.

Character Generator Architecture

Character Memory

First write the RTL for the character memory. This memory stores which character to display at each of the 80x30 positions on the screen. The memory will have two synchronous *ports (operating on the rising edge of the clock signal):

  1. A write port for occasionally changing which character is stored at a given position.
  2. A read port for retrieving the character stored at a given position (this will be used constantly to drive the VGA display with the appropriate character data).

The diagram above shows this memory as a separate module, with write (wr_*) and read (rd_*) ports, and a clk input. You might think we should instance a memory block and wire up to these ports. However, instancing an FPGA memory block directly from our code is somewhat complex and unnecessary. In the previous labs you have written behavioral always_ blocks and the synthesis tools has automatically mapped these to FPGA LUTs and flip-flops. Similarly, we will describe a memory behaviorally using always_ blocks, and the synthesis tools will automatically map this to one of the FPGA BRAMs. As such, the code we write will look a bit different than the diagram above, but the resulting circuit will match the diagram.

You do not need to create a separate module for the character memory, we will describe the character memory behaviorally within the char_gen module, as described in the following steps.

  1. Begin your memory by defining an array of logic values of size 4096 x 8 bits.

     logic [7:0] mem_data[0:4095];
    

    This ROM needs to store the 7-bit ASCII value for each character in the 80x30 character grid. However, since these sizes are not powers of two, we will use a larger memory that is sized with powers of two to simplify our design. We will use an 8-bit memory, with 4096 entries. Like the font ROM, this memory will be storing a 2-D array of data in a 1-D memory, so we will split the address into two parts:

    • The upper five address bits (char_mem_addr[11:7]) will choose the character row (0 through 29)
    • The lower seven address bits (char_mem_addr[6:0]) will choose the character column (0 through 79)
    • There is some wasted memory, but it allows for a simpler design and more efficient address decoding.

    Answer the following questions to demonstrate your understanding of the character memory.

    How many bits are stored in the character memory?

    What address of the character memory holds the value for the character located on the character display at col,row = (65,17)?

    What are the character coordinates of the character stored at address 0xB8F in the character memory?

  2. Create synchronous logic to write values into the character memory (called the write port).

    The write port is used to change which character is stored at each location, and is controlled using the address (char_addr), the value to write (char_value), and the write enable signal (char_we).

    The write port logic is described with an always_ff block that updates the value of a given index in the mem_data array:

     mem_data[char_addr] <= ...;
    

    When you write to mem_data, pad the char_value with a 1'b0 in the most significant bit since the char_value is 7 bits but the mem_data is 8 bits.

  3. Create a synchronous read port for the character memory.

    The read port is used by the character generator circuit to determine which character is supposed to be displayed at a particular pixel_x and pixel_y location. The read port logic is also described with an always_ff block that reads the value from the mem_data array, and stores it in an internal signal called char_read_value:

     char_read_value <= mem_data[char_read_addr];
    

    Note: There is no read enable signal for the read port; the memory is always being read.

    You will need to use combinational logic to generate the 12-bit char_read_addr signal, based on the pixel_x and pixel_y input ports. Look at the example in the preliminary section to see how this is done.

  4. Initialize the memory contents from a file (if specified).

    The character memory array can be initialized with an initial set of characters through an initialization file. Add an initial block to your character generator module to initialize the contents of the memory if the FILENAME parameter is set:

     initial begin
         if (FILENAME != "")
             $readmemh(FILENAME, mem_data, 0);
     end
    

Font ROM

  • Instance the font ROM in your character generator module.
  • Connect up the address input of the font ROM appropriately, based on the character value output from the character memory and the row within the character determined by the pixel_y input. Look at the example in the preliminary section to see how this is done.
  • Note that due to the 1-cycle latency of the character memory, the char_read_value is delayed by one clock cycle from the pixel_y signal. Feed the pixel_y signal through a one-cycle pipeline register to create a delayed version of pixel_y (call this pixel_y_r).
  • Create an 8-bit signal, rom_data, connected to the output of the font ROM.

Pixel Out Multiplexer

Create multiplexing logic to select the appropriate pixel from the font ROM output to drive the pixel_out signal. See the example in the preliminary section to see how the pixel selection is done.

Since the font ROM adds a cycle of latency, the output is now two cycles behind the pixel_x signal. To compensate for this delay, pass the pixel_x signal through two registers before using it in the multiplexing logic.

Character Generator Timing Diagram

Include the file char_gen/char_gen.sv in your repository.

Exercise #2: Character Generator Simulation

After creating your module, create a Tcl simulation script for your module. Your tcl file should include the following:

  1. Run the simulation for 100ns without setting any values
  2. Create a 100 MHz oscillating clock and run for a few clock cycles
  3. Perform a character write to location 0x000 with the value 0x41 (ASCII ‘A’) and run for a few clock cycles
  4. Set the pixel_y value to ‘2’ and set pixel_x to values 0 through 7 to see the character ‘A’ displayed for row 2
  5. Do the same thing as above but for pixel_y=3

Note that when you simulate your module, the memory will not be initialized with any values. This is okay; you will be loading a character into the memory during the simulation. Take a screenshot of the waveform.

Include the file char_gen/sim.tcl in your repository.

Include the file char_gen/sim.png in your repository.

Exercise #3: Character Generator Testbench

After simulating your module and verifying it operates with the Tcl script, you will need to test your receiver with the testbench. You may want to begin by testing the decoder interactively in GUI mode with this testbench to resolve syntax and behavioral problems in your design.

Make sure that the testbench simulates without any errors before proceeding.

Exercise #4: Top-Level Design

Create a new file named char_gen_top/char_gen_top.sv and include the following ports:

Parameter Default Function
FILENAME ”” Specifies the filename of the initial contents of character memory
CLK_FREQUENCY 100_000_000 Specifies the frequency of the clock in Hz
BAUD_RATE 19_200 Determines the baud rate of the receiver
WAIT_TIME_US 5_000 Determines the wait time, in micro seconds, for the debounce circuit
REFRESH_RATE 200 Specifies the display refresh rate in Hz
Module Name: char_gen_top      
Port Name Direction Width Function
clk Input 1 100MHz System Clock
btnd Input 1 Reset
btnc Input 1 Write character
btnl Input 1 Set background color
btnr Input 1 Set foreground color
rx_in Input 1 UART Receiver Input
sw Input 12 Determines character to write or color to display
Hsync Output 1 Horizontal synchronization signal
Vsync Output 1 Vertical synchronization signal
vgaRed Output 4 Red color signal
vgaGreen Output 4 Green color signal
vgaBlue Output 4 Blue color signal
anode Output 4 Anode signals for each of the four display digits
segment Output 8 Cathode signals for seven-segment display

Review the top-level design diagram above and the descriptions of each component before starting this exercise. Implement the top-level design, and be sure to follow these specifications:

  1. The inputs (btnd, btnc, btnl, btnr, and rx_in) should go through two flip-flop synchronizers before being used. The switches (sw) should go through one flip-flop synchronizer.
  2. Use the seven_segment4 module to display the value of the switches. Attach the switches (lower 12) to the input to the display and only display the right three digits, with no decimal points.
  3. Create two 12-bit registers for the foreground and background colors. On reset, the background color should be initialized to 12'h000 (black) and the foreground color to 12'hfff (white).When btnl is pressed, set the background color to the value of the switches. When btnr is pressed, set the foreground color to the value of the switches.
  4. Instance the VGA timing controller from the VGA display lab, and connect as shown in the diagram.
  5. Instance the char_gen from the previous exercise, and connect as shown in the diagram. For now, create internal signals char_we, char_addr, char_value, and pixel_out, that will be assigned later.
  6. Instance your UART receiver. Like the last lab, create an ack signal that connects Receive to ReceiveAck to immediately acknowledge any received UART data.
  7. Instance your debouncer circuit for btnc.
  8. Review your instanced modules, make sure clock and reset are connected properly, and make sure any top-level parameters are passed correctly to the appropriate modules.
  9. Implement the Char Write Logic in the diagram to control when new characters are written to the character memory:
    • Write a new character when btnc is pressed (a one-shot edge detector is needed here) or when a new character is received from the UART receiver (ack signal is high).
    • The character value to write should come from the switches when btnc is pressed, and should come from the UART receiver when a new character is received.
    • The address represents an invisible cursor on the screen that indicates where the next character will be written. Remember that the character memory address is 12 bits, where the upper five bits represent the character row and the lower seven bits represent the character column. Make sure to update the address appropriately after each character is written (i.e., increment left-to-right and top-to-bottom, and wrap around to the top after reaching the end of the screen). I.e,:
      • If the character column is less than 79, increment the column by one.
      • If the character column is 79 and the row is 29, set the column and row to 0.
      • If the character column is 79, set the column to 0 and increment the row by one.
  10. Create the logic for the VGA color signals and timing signals as described below.
    • When blank is high, the RGB signals should be set to 12'h000 (black).
    • Otherwise, when pixel_out is high, set the RGB signals to the foreground color, and when low, the background color.
    • Make sure the VGA signals are registered before being output to the VGA ports to eliminate any glitches.
    • The vgaRed, vgaGreen, and vgaBlue signals will be updated three cycles after the VGA timing controller outputs the pixel_x and pixel_y signals (two cycles of latency from the character generator, and one additional cycle from the registers on the output). Make sure the Hsync and Vsync signals are delayed by the same amount to keep them synchronized with the RGB signals.

Include the file char_gen_top/char_gen_top.sv in your repository.

Exercise #5: Top-Level Simulation

Once you have created your top-level design, you will perform a short simulation to demonstrate the writing of character data is working properly. Your top-level module should have a parameter named FILENAME that specifies the filename containing the initial contents of the character memory. By default, this FILENAME is empty indicating that the memory should not be initialized with any values. For this exercise, you will need to create a memory initialization file that initializes the character memory with Abraham Lincoln’s “Gettysburg Address”. The text for this document is saved in the file gettysburg_address.txt.

A python script has been created for to convert this text file into a memory initialization file:

python3 gen_initial_chars.py gettysburg_address.txt gettysburg_address.mem

Simulate your top-level design with the following two parameters (note that you need the ../.. since the gettysburg_address.mem file is two directories up from the work directory where the simulation is run):

SIM_PARAMS = WAIT_TIME_US=50 FILENAME=../../gettysburg_address.mem

Create a TCL simulation file (sim.tcl) that does the following:

  • Simulate loading the character S into the character memory by pressing btnc (make sure to give the button sufficient wait time to get through the debouncer)
  • Simulate loading the character A into the character memory over the UART receiver (this is the same character sent in your previous lab - you can copy the tcl lines for performing this simulation).

Include the following signal in your simulation: clk, btnc, sw, rx_in, hsync, vsync, VGA signals, char_addr, char_we, and char_write_value.

Take a screenshot of the simulation showing the char_gen writes.

Include the file char_gen_top/sim.tcl in your repository.

Include the file char_gen_top/sim.png in your repository.

Exercise #6: Synthesis and Implementation

  • Create a char_gen_top/basys3.xdc file that contains pin location definitions for each of your I/O ports.

    Include the file char_gen_top/basys3.xdc in your repository.

  • For the synthesis and implementation step, you are required to create your own text message to display on the screen. Create a text file name mymessage.txt that includes inspirational, uplifting, or positive text that fills a good portion of the screen (i.e., the message uses the majority of the lines in the display). Create a Makefile rule named mymessage.mem that generates mymessage.mem file from your text message. Further, ignore the .mem file and clean the .mem file as part of the clean process.

  • Before running synthesis, you will need to use SYNTH_PARAMS to set the FILENAME appropriately.
    SYNTH_PARAMS = FILENAME=../../mymessage.mem
    
  • Run synthesis and carefully review the synthesis log to make sure there are no warnings.

    How many FDCE/FDRE/FDSE cells are used by the design?

    How many total LUT resources are used by the design? (add up all the LUT* resources)

    How many total Block RAMs are used by the design? (RAMB18E1 or RAMB36E1)

    How many total IBUF and OBUF resources do you have in the design?

  • Run implementation.

  • Review the timing.rpt file to answer the following questions.

    What is the Worst Negative Slack or ‘WNS’ of your design?

    How many TNS total Endpoints are there in your design?

Open your implemented design in Vivado, and on the ‘Netlist’ tab to the left of the implementation view, expand the netlist to find the Font ROM component (something like: char_gen->fontRom->Leaf Cells->addr_reg_reg). The cell type of the Font ROM is RAMB18E1. Select this cell and take a screenshot of the memory resource.

Include the file char_gen_top/fpga.png in your repository.

Exercise #7: Download and Test

Once the bitstream has been generated, download your bitstream to the FPGA. Hook up your board to the VGA monitor and power it on to make sure the display is operating correctly. Test the ability to receive characters and display them on the screen by running ‘PuTTY’ as in the previous lab. Review the instructions for connecting your transmitter with PuTTY in the PuTTY tutorial. Set the baud rate to 19,200, 8 data bits, ODD parity, and 1 stop bit and connect to the appropriate COM port.

Final Pass-Off:

Refer to the passoff tutorial for the passoff process.

  • See required_files in the passoff script.
  • Makefile rules that must work correctly and be free from errors and warnings:
    • Makefile: clean, mymessage.mem, gettysburg_address.mem
    • char_gen/Makefile: sim_tb
    • char_gen_top/Makefile: synth, implement

Answer the final two questions in your laboratory report:

How many hours did you work on the lab?

Provide any suggestions for improving this lab in the future