The bitmap display and keyboard registers

Framebuffer pixels, keyboard input and console output all use ordinary loads and stores. A program stores colour words in a reserved memory area, and the Screen watches that area as a framebuffer. At four special addresses, loads and stores talk to keyboard and console device registers instead. The special-register arrangement is called memory-mapped I/O. The syscalls you already know still have a place: service 10 exits, and service 32 gives a moving or polling program a controlled pause.

One word is one pixel

The Screen's dimensions come from fixed choices:

  • Unit width and unit height are each one of 1, 2, 4, 8, 16 or 32 display pixels. They say how large the Screen draws one framebuffer word.
  • Display width and display height are each one of 64, 128, 256, 512 or 1024 pixels.

The word grid has display width / unit width columns and display height / unit height rows. For example, a 256 by 256 display with 16 by 16 units holds a 16 by 16 grid, or 256 words.

The low 24 bits of each word hold a colour in RRGGBB order: red in bits 23–16, green in bits 15–8 and blue in bits 7–0. Write the full word as 0x00RRGGBB. Thus 0x00FF0000 is red, 0x0000FF00 is green, 0x000000FF is blue and 0x00FFFFFF is white.

This first program reserves a 16 by 16 framebuffer and changes four of its words.

# @screen unit=16 width=256 height=256 base=display
.data
display: .space 1024        # 16 * 16 words * 4 bytes

.text
.globl main
main:
    la $t0, display
    li $t1, 0x00FF0000      # red: top left
    sw $t1, 0($t0)
    li $t1, 0x0000FF00      # green: one pixel to the right
    sw $t1, 4($t0)
    li $t1, 0x000000FF      # blue: one row below the first pixel
    sw $t1, 64($t0)
    li $t1, 0x00FFFFFF      # white: bottom right
    sw $t1, 1020($t0)
    li $v0, 10
    syscall

The words run left to right, then continue on the next row. Neighbouring words are four bytes apart, and one row here occupies 16 * 4 = 64 bytes. The Screen observes these ordinary memory words; they are different from the device registers used for the keyboard and console below.

Build-time Screen configuration

The first @screen comment in the entry file configures the Screen on every Build, before the first instruction runs:

# @screen unit=16 width=256 height=256 base=display

unit sets both unit dimensions; unitWidth and unitHeight can set them separately. width and height use the fixed display-size choices above. base can name a label defined by the program or give an address such as 0x10010000. A label is convenient because the assembler chooses its address.

Settings left out keep their current Screen values. With no directive, the current configuration also remains; a new MIPS project starts with 1 by 1 units, a 512 by 256 display and base address 0x10010000. An unsupported size produces a warning and is replaced by the nearest allowed choice. A malformed value or an unknown base label also produces a warning and leaves that setting as it was. These warnings do not stop the Build. The assembler itself sees the line only as a comment.

From a row and column to an address

Suppose x is the column and y is the row. First skip y complete rows, with columns words in each row. Then move x words into that row. Finally convert the word count to bytes:

base + (y * columns + x) * 4

When the column count is a power of two, shifts perform both multiplications. This loop draws one horizontal row in the 16-column framebuffer. It recomputes the formula for each value of x so the connection between coordinates and addresses stays visible.

# @screen unit=16 width=256 height=256 base=display
.eqv COLUMNS 16
.data
display: .space 1024

.text
.globl main
main:
    la $s0, display
    li $s1, 6                   # y: draw row 6
    li $s2, 0                   # x: start at column 0
draw:
    sll $t0, $s1, 4             # y * 16 columns
    add $t0, $t0, $s2           # y * columns + x
    sll $t0, $t0, 2             # four bytes per word
    add $t0, $t0, $s0           # add the framebuffer base
    sll $t1, $s2, 4             # increasing blue value
    sw $t1, 0($t0)
    addi $s2, $s2, 1
    blt $s2, COLUMNS, draw
    li $v0, 10
    syscall

The keyboard and console registers

The addresses from 0xffff0000 through 0xffff000c are special memory-mapped I/O registers. Each register is a word, but the character itself is in the low byte.

addressregisteroperation and effect
0xffff0000receiver controlLoad it and test bit 0. Ready is 1 when receiver data holds a character.
0xffff0004receiver dataLoad the waiting character. That read consumes it.
0xffff0008transmitter controlBit 0 is Ready and remains 1 because the console can accept a character.
0xffff000ctransmitter dataStore a character code to append its low byte to the console.

Here is console output through the transmitter. Service 10 still ends the program; no output syscall is involved.

.eqv TRANSMITTER_DATA 0xffff000c

.text
.globl main
main:
    li $s0, TRANSMITTER_DATA
    li $t0, 'H'
    sw $t0, 0($s0)
    li $t0, 'i'
    sw $t0, 0($s0)
    li $t0, '\n'
    sw $t0, 0($s0)
    li $v0, 10
    syscall

Polling the keyboard

A key can arrive at any time, so a program repeatedly loads receiver control and checks Ready bit 0. When Ready is 1, it loads receiver data. If another character is already queued, that next character moves into receiver data and Ready stays 1; otherwise Ready becomes 0. The queue preserves the order in which the keys were typed.

Click the Screen before typing. A visible focus ring shows that it owns the keyboard; keys typed while another part of the page has focus do not enter this receiver queue.

.eqv RECEIVER_CONTROL 0xffff0000
.eqv RECEIVER_DATA    0xffff0004
.eqv TRANSMITTER_DATA 0xffff000c
.data
banner: .asciiz "Click the Screen, then type. q exits.\n"

.text
.globl main
main:
    li $v0, 4
    la $a0, banner
    syscall

poll:
    lw $t0, RECEIVER_CONTROL
    andi $t0, $t0, 1            # keep only Ready bit 0
    bnez $t0, receive

    li $v0, 32                  # avoid a busy-wait while the queue is empty
    li $a0, 10                  # let 10 ms of program time pass
    syscall
    j poll

receive:
    lw $t1, RECEIVER_DATA       # consuming read
    andi $t1, $t1, 0xFF
    sw $t1, TRANSMITTER_DATA    # echo through MMIO
    li $t2, 'q'
    bne $t1, $t2, poll
    li $v0, 10
    syscall

This is polling: repeatedly asking the device whether work is ready. Service 32 keeps an empty poll from turning into a tight busy-wait. The loads, branches, li instructions and syscall instruction still count toward the running instruction budget. The elapsed pause itself adds no running instructions, so the program can wait without spending that budget merely to count time.

Keep bit 1 of both control registers clear in this lesson. It requests interrupt-driven I/O, which the Playground does not support; the run stops if a program tries to set it. Poll Ready bit 0 instead.

Automated testcases cannot type into the Screen. A runFor testcase such as the one above only runs up to that instruction budget and checks that no runtime error occurred. It does not verify the keyboard interaction or the rendered Screen image, so try those parts interactively.

Optional application: a moving dot

The same address calculation and service 32 are enough for a small animation. This program draws a dot, waits, erases that word and advances x.

# @screen unit=16 width=256 height=256 base=display
.eqv COLUMNS 16
.eqv DOT 0x00FFCC33
.data
display: .space 1024

.text
.globl main
main:
    la $s0, display
    li $s1, 0                   # x
    li $s2, 1                   # direction
frame:
    sll $t0, $s1, 2             # x * 4
    addi $t0, $t0, 512          # row 8: 8 * 16 * 4
    add $t0, $t0, $s0
    li $t1, DOT
    sw $t1, 0($t0)

    li $v0, 32
    li $a0, 50
    syscall

    sw $zero, 0($t0)            # erase the old dot
    add $s1, $s1, $s2
    bltz $s1, reverse
    blt $s1, COLUMNS, frame
reverse:
    sub $s2, $zero, $s2
    add $s1, $s1, $s2
    add $s1, $s1, $s2
    j frame

Updating only the old and new dot positions is efficient for this animation because only one small object changes. Partial redraw is not a universal rule: when most of an image changes, redrawing the whole framebuffer can be the simpler approach.

Your turn

The first exercise uses the 16 by 16 framebuffer below. Starting with x = 5 and y = 3, derive the pixel address from the coordinates and store white there. Do not replace the coordinates with a precomputed word index, byte offset or absolute address. Leave the check after your code in place; it loads through the address you calculated and prints the stored colour as a decimal number. That output checks the store-and-load pair; the Screen shows whether you chose the requested pixel.

# @screen unit=16 width=256 height=256 base=display
.eqv COLUMNS 16
.data
display: .space 1024

.text
.globl main
main:
    li $t0, 5                   # x
    li $t1, 3                   # y
    # Set $t2 to the pixel address and store 0x00FFFFFF there.
    # your code here

    lw $a0, 0($t2)              # exercise check
    li $v0, 1
    syscall
    li $v0, 10
    syscall
Show solution
# @screen unit=16 width=256 height=256 base=display
.eqv COLUMNS 16
.data
display: .space 1024

.text
.globl main
main:
    li $t0, 5                   # x
    li $t1, 3                   # y
    sll $t2, $t1, 4             # y * 16 columns
    add $t2, $t2, $t0           # y * columns + x
    sll $t2, $t2, 2             # four bytes per word
    la $t3, display
    add $t2, $t2, $t3           # framebuffer base + byte offset
    li $t4, 0x00FFFFFF
    sw $t4, 0($t2)

    lw $a0, 0($t2)              # exercise check
    li $v0, 1
    syscall
    li $v0, 10
    syscall

For the second exercise, print exactly Hi followed by a newline by storing each character in the MMIO transmitter data register. Do not use syscall service 4 or 11. The automated check can compare the exact console text, but output alone cannot prove which route produced it; checking that the stores use 0xffff000c is part of the exercise.

.text
.globl main
main:
    # your code here
Show solution
.eqv TRANSMITTER_DATA 0xffff000c

.text
.globl main
main:
    li $s0, TRANSMITTER_DATA
    li $t0, 'H'
    sw $t0, 0($s0)
    li $t0, 'i'
    sw $t0, 0($s0)
    li $t0, '\n'
    sw $t0, 0($s0)
    li $v0, 10
    syscall