System I/O Reference

The RISC-V processor that you have developed over the semester will be inserted into a top-level design that includes other logic that will be provided for you. This logic will allow your processor to interact with a default memory configuration as well as the various input/output devices on the FPGA board such as the LEDs and switches. Your processor will be able to control and interact with these devices. A high-level overview of your processor system is shown below:

This page provides an overview of the system resources available within the given system project and a programming reference for the system. The base system includes the following resources:

Memory Organization

The system that has been created for you has several different address regions, each with a specific and important purpose. The purpose of these memory regions is summarized below:

Address Range Size (bytes) Purpose
0x00000000 - 0x00001fff 8192 Instruction Memory (.text)
0x00002000 - 0x00003fff 8192 Data Memory (.data)
0x00007f00 - 0x00007fff 256 Memory Mapped I/O
0x00008000 - 0x0000bfff 16384 VGA Space

Instruction Memory

The instruction is the region that contains the instructions of the program that is being executed. This memory is read only and cannot be changed during run-time. The instruction memory is fixed at 8192 bytes (or 2048 instruction words) and is mapped to adddresses 0x00000000 - 0x00001fff. All programs wil begin at address 0x00000000.

The program is initialized inside of the bitstream of your full system. You can set the initial program that is compiled into your bitstream by setting the top-level TEXT_MEMORY_FILENAME generic before synthesizing and implementing your design.

The program loaded into this memory can be changed in the bitstream after implementation by running the load_mem.tcl script as described here.

Data Memory

This is the region that contains the global data memory. There are 8192 bytes or 2048 words. The top of this region is used for the stack and the bottom of this region is used for global data memory.

The data segment is initialized inside of the bitstream of your full system. You can set the initial program that is compiled into your bitstream by setting the top-level DATA_MEMORY_FILENAME generic before synthesizing and implementing your design.

The data segment loaded into this memory can be changed in the bitstream after implementation by running the load_mem.tcl script as described here.

Memory Mapped I/O (mmio)

This region is allocated for specific I/O devices such as the LEDs. The I/O devices that have been added to this system can be controlled and monitored by writing data to and reading data from reserved memory locations that are dedicated to I/O devices in the system. This approach for controlling I/O is called “Memory Mapped I/O”. Memory Mapped I/O includes dedicated decoding logic within that monitors the address and control signals coming out of the RISC-V processor. When this decoding logic observes a write operation (caused by the ‘sw’ instruction) at the specific location of a I/O device, it intercepts this write and sends the data to the appropriate I/O device. The I/O devices act just like a memory in that they accept data from the ‘sw’ instruction and provide data back to the processor from the ‘lw’ instruction.

256 bytes of address space has been reserved for the I/O devices in this system. The I/O space is located at 0x0000_7f00 (i.e., 0x0000_7f00 - 0x0000_7fff). Each individual I/O device in this system has a specific address reserved within this range and is represented as an 8-bit offset. For example, the I/O device with an offset of 0x18 is located at 0x000_7f18. The I/O space reserved for the system you are using is summarized in the table below:

I/O Offset Read Write
0x00 (0) Read value of LEDs Write LED values
0x04 (4) Read Switch status N/A
0x08 (8) TX Status Write value to UART TX FIFO
0x10 (16) RX data N/A
0x14 (20) RX status N/A
0x18 (24) Seven Segment Display Value Seven Segment Display Value
0x24 (36) Read Buttons status N/A
0x30 (48) Timer (ms) New Timer value
0x34 (52) Character Color Character Color

LEDs

Your RISC-V processor is able to control the 16 LEDs that are provided on the Basys 3 board. You can turn these LEDs on by writing data to address 0x0000_7f00 (0x0000_7f00 + 0). Writing to this memory location using a store word (‘sw’) instruction will set the value of the 16 LEDs. Bit 0 of the word corresponds to LED 0, bit 1 corresponds to LED 1, and so on. Note that since there are 16 LEDs and 32 bits in a word, the upper 16 bits written to this address location will be ignored. The bit value ‘1’ corresponds with turning ‘on’ the corresponding LED.

The following code example demonstrates how to write the value 0xa5 to the LED registers.

    # Assumes that x3 contains the I/O address space
    .eqv LED_OFFSET 0x0
    addi x6, x0, 0xa5
    sw x6, LED_OFFSET(x3)

This example will turn ON the following LEDs: LED7, LED5, LED2, and LED0. All other LEDs will be turned off.

Note that you can also read from this memory location using the load word (‘lw’) instruction to determine which LEDs are currently turned on (as dictated by a previous ‘sw’ instruction to the LEDs). The following example demonstrates how to read the values being displayed on the LEDs.

    lw t0, LED_OFFSET(x3)

Switches

The memory mapped address 0x0000_7f04 (0x0000_7f00 + 0x04) corresponds to the slide switches. Reading from this memory location will indicate which switches are in the “on” (up) position. Bit 0 of the resulting word corresponds to the status of SW0 (right-most switch), bit 1 corresponds to SW1, and so on. Since there are only 16 switches, the top 16 bits of this word will always be zero. Writing a value to this memory location will not do anything. The following code example demonstrates how to read the switches:

    # Read the switches from 0x00007f04 by providing an offset of 4 to t0
    # (t0 + 4 = 0x00007f04)
    .eqv SWITCH_OFFSET 0x4
    lw t1, SWITCH_OFFSET(x3)
    # The value of the switches is now in t1

Buttons

The address 0x0000_7f24 (0x0000_7f00 + 0x24) corresponds to the push buttons. Reading from this memory location will indicate buttons which are pressed. When a button is pressed, the bit corresponding to the button will have the value ‘1’. The relationship between bit location and button is summarized in the table below:

4 3 2 1 0
UP RIGHT DOWN LEFT CENTER
BTNU BTNR BTND BTNL BTNC

The following example demonstrates how to read the buttons, branch back when the ‘down’ button (BTND) NOT pressed, and then fall through when the ‘down’ button IS pressed:

    # It is better to use constants when programming I/O
    .eqv BUTTON_OFFSET 0x24
    .eqv BUTTON_D_MASK 0x04
    # Assume x3 has the I/O base address
wait_for_btnd:
    # Read the buttons 
    lw t1, BUTTON_OFFSET(x3)
    # Mask the buttons for button D
    andi t1, t1, BUTTON_D_MASK
    # If button d is not pressed, branch back
    beq t1, x0, wait_for_btnd:
button_d_pressed:
    # fall through after button d is pressed

Seven Segment Display

Address 0x0000_7f18 (0x0000_7f00 + 0x18) corresponds to the seven-segment display. Writing to this memory location will cause the lower 16-bits of the word being written to at this address to be displayed in hex on the four digits of the seven segment display. Like the LEDs, you can read the value of this memory location to see what value is currently being displayed on the seven-segment display. The following code segment demonstrates a loop that continuously updates the value of the seven segment display:

    .eqv SEVEN_SEGMENT_DISPLAY_OFFSET 0x18
    addi t1, x0, 0
loop:
    sw t1, SEVEN_SEGMENT_DISPLAY(x3)
    addi t1, t1, 1
    beq x0, x0, loop

Timer

A simple timer has been added to the system to provide a time stamp to facilitate timing accurate interactions. When the timer is read (0x0000_7f30), it will return the number of milliseconds since the processor was first turned on or since it was last reset. The timer can be loaded with any value by writing to the same address location (this is a convenient way to reset the timer). The following code example demonstrates how to wait for 10 ms:

    .eqv TIMER 0x30
    sw x0, TIMER(x3)      # Clear timer to zero
    addi t1, x0, 10       # Set t1 to 10 (10 milliseconds)
wait:
    lw t2, TIMER(x3)
    beq t2, t1, after_wait
    beq x0, x0, wait
after_wait:

UART

The circuit included with your processor also contains a “Universal Asynchronous Receiver/Transceiver” (UART). You should be familiar with a UART and what it does from the ECEN 220 course. This module will allow you to send bytes of data to a computer and receive bytes from the computer when typed in a terminal program. This will allow you to interact with your FPGA board from a computer terminal. You can send bytes over the UART and receive bytes from the computer by reading and writing to memory locations dedicated to the UART. The UART included with this system has a baudrate of 115200 and uses even parity.

To send a byte over the UART transmitter, first check to see if the transmitter is busy by reading the TX status register at address (0x0000_7f08). The first bit of this status register indicates whether the TX module is busy or not. If it is busy, wait until the status register indicates that the TX module is no longer busy - if you try to write a byte over TX while it is busy, the request will be ignored. Once the TX module is no longer busy, you can issue a transmission over the UART by writing to address 0x0000_7f08. Writing this register will cause the TX module to transmit the lower 8 bits of the word you write over the UART.

Bits Function
TX Status Register (read) - 0x0000_7f08  
0 TX Busy
TX Control Register (write) - 0x0000_7f08  
[7:0] Write the 8 bit byte over the UART

The following example demonstrates a transfer over UART:

    .eqv UART_TX 0x08
    .eqv UART_TX_BUSY 0x1
wait_for_tx_not_busy:
    lw t0, UART_TX(x3)
    andi t0, t0, UART_TX_BUSY
    bne x0, t0, wait_for_tx_not_busy
    # send word
    sw t1, UART_TX(x3)

To receive a byte from the UART receiver, first read the RX status register at address 0x0000_7f14 to see if there is a new byte available. If there is a new byte available, read the actual byte by reading the RX data register at address 0x0000_7f10. When this register is read, the “new byte” flag will be reset to zero. The RX status register has three flags indicating whether there is a new byte available, RX busy, and whether there was an error during the last RX transfer.

Bit Function
RX Status Register - read (0x0000_7f14)  
0 New data available from RX
1 The RX unit is busy
2 The previous RX transmission had an error
RX Data Register - read (0x0000_7f10)  
[7:0] Read received data

The following example demonstrates how to receive a word from the UART:

    .eqv UART_RX_STATUS 0x14
    .eqv UART_RX_DATA 0x10
    .eqv UART_RX_DATA_READY 0x1
wait_for_rx_data:
    lw t0, UART_RX_STATUS(x3)
    andi t0, t0, UART_RX_DATA_READY
    beq x0, t0, wait_for_rx_data
    # ge word
    lw t0, UART_RX_DATA(x3)

VGA

A VGA display controller is included in the I/O devices and can be programmed by the processor. This controller organizes the 640x480 display into a text character display that can display 80x30 characters (with each character having size 8x16). This controller allows you to place ASCII text characters on the screen at any of these 80x30 locations. Each character on the VGA screen is mapped to a different address location and control of the VGA display involves writing data to specific locations within this memory. The base location of the VGA display is 0x0000_8000 and the display consumes 16384 bytes (0x0000_8000 through 0x0000_bfff).

An ASCII character can be written to a specific location on the screen by writing the 8-bit ASCII value to the appropriate address. The address of each of the 80x30 character locations is determined by the following table:

Base Address Row Address Column Address
addr[31:14] addr[13:9] addr[8:2]

For example, the address of the location 24,13 (0x18,0xd) is 0x0000_8000 + 0xd << 9 + 0x18 << 2 = 0x0000_8000 + 0x1a00 + 0x60 = 0x0000_9A60. Writing the value 0x21 to this value will place the character “!” at the location (column,row) = 24,13 as shown in the following code:

    li t0, 0x00009A60
    addi t1, x0, 0x21
    sw t1, 0(t0)

Note: The li pseudo instruction involves the use of both the lui and the addi instruction. We currently do not have the ability to execute the lui instruction so we will use other instruction sequences to do this for the lab.

The color of the characters on the VGA can also be controlled by the assembly instructions. All colors are specified by a “RGB” 12-bit value. The top four bits [11:8] specify the “Red” component of the color, bits [7:4] specify the “Green” component of the color, and the lower four bits [3:0] specify the “Blue” component of the color. The four bits associated with each primary color indicates the intensity of the given primary color. A value of “0000” for any of the primary colors will provide none of the given color while a value of “1111” for a primary color indicates provide this primary color at its maximum intensity. The table below demonstrates eight different colors that can be generated with this 12-bit color signal:

Red [11:8] Green [7:4] Blue [3:0]  
0000 0000 0000 Black
0000 0000 1111 Blue
0000 1111 0000 Green
0000 1111 1111 Cyan
1111 0000 0000 Red
1111 0000 1111 Magenta
1111 1111 0000 Yellow
1111 1111 1111 White

With 12 bits, you can create up to 4096 different colors. Each printed character appears on the display as two colors: the foreground (the color of the pixels that make up the character shape) and the background (the color where the character pixels do not exist). By default, all characters share the same foreground as background colors (this can be changed if necessary). The color of the foreground and background can be set by the VGA color register located at address 0x000_7f34. Bits [11:0] in this register determine the RGB color of the foreground and bits [23:12] determine the RGB color of the background as shown in the following table:

Reserved Background Color Foreground Color
[31:24] [23:12] [11:0]
00000000 RRRRGGGGBBBB RRRRGGGGBBBB

In addition to setting a default foreground/background color, it is also possible to set a custom foreground and background color for each character on the display. This allows you to create interesting colored characters with differing backgrounds that move around on the display.

To set a custom foreground and background color for a particular location on the VGA display, write a value to the character location with a non-zero color set in the upper 25 bits of the 32-bit word. If any bits [31:8] are non-zero, the VGA memory will store the store bits [19:8] into the foreground color for the character and store bits [31:20] into the background color for the character. The 32-bits used by the VGA character memory are defined as follows:

Background Color Foreground Color Unused Character Value
data[31:20] data[19:8] data[7] data[6:0]

For example, to write the character “!” (ASCII 0x21) with a red foreground color (0xf00) and a blue background color (0x00f), you would write the following value to the VGA display:

0x21 | (0xf00 << 8) |  (0x00f << 20)  = 0x00ff0021

To write the character “$” (ASCII 0x24) to the display using the default background and foreground, then write the character value 0x24 to the memory (bit 7 is zero for 7-bit ASCII characters).


Last Modified: 2024-07-01 00:08:02 +0000