RISC-V Screen and Memory-Mapped I/O
RISC-V programs draw and read input through memory, not through system calls: the screen is
a grid of words anywhere in memory, and the keyboard and the console are four words at 0xffff0000. Both are RARS's own tools, the bitmap display and the keyboard and display simulator, with the same parameters and the same
register layout, so a program written for RARS runs here unchanged.
Bitmap display
One word of memory is one pixel. Its low 24 bits are the color, red in bits 23-16, green in 15-8 and blue in 7-0; the top byte is ignored. Words run left to right and then top to bottom, so the pixel below a word is one row of words further on.
The screen panel's Display button configures it, with RARS's own five parameters, and a program can ask for them itself with the @screen comment below. The parameters are saved with the project, and testcases run with them too.
0x10000000 (global data), 0x10008000 ($gp), 0x10010000 (static data), 0x10040000 (heap), 0xffff0000 (memory map). Default 0x10010000 (static data), which
is where .data puts your first label. .data
display:.space 262144 # 256 * 256 words
.text
main:
la t0, display
li t1, 0x00ff8000 # low 24 bits: red 0xff, green 0x80, blue 0x00
sw t1, 0(t0) # the pixel at the top left
sw t1, 1024(t0) # 256 words further on: the one below it
Reserve the memory the grid covers, with .space or a label of your own: the
screen shows whatever those words hold, and a program that writes past what it reserved
is writing over something else. Undo walks the picture back with the code, because the
picture is the memory the emulator rolled back.
Configuring the screen from the program
A comment line naming @screen sets those five parameters at every Build, before the first instruction runs, so opening a program and
building it is all it takes. It is a comment, so the same file still assembles in
RARS, where you set the parameters in the tool's window as usual.
# @screen unit=1 width=256 height=256 base=display
.data
display:.space 262144 # 256 * 256 words
.text
main:
la t0, display
The display width in pixels, one of 64, 128, 256, 512, 1024.
The display height in pixels, one of 64, 128, 256, 512, 1024.
How many pixels wide and high one word is drawn, one of 1, 2, 4, 8, 16, 32. unitWidth and unitHeight set the two separately.
Where the grid starts: a label the program defines, which is the point of it — the program never has to know the address — or an address such as 0x10010000 or 268500992. A label not on a word boundary is rounded down to one.
- Order and spacing do not matter, commas are allowed between settings, and
unitWidth,unit-widthandunitwidthare the same name. - What the directive leaves out keeps the value it had, and a program with no
@screenline changes nothing at all: the configuration stays yours. - A value RARS has no entry for, an unknown setting or a label that does not exist is a warning on the directive's line, never an error: a comment cannot stop a program from assembling. A size off the list is replaced by the nearest one on it, and anything else is left as it was.
- Changing a parameter in the Display popover afterwards wins, until the next Build reads the comment again.
Keyboard and display registers
Four words carry one character each way. Click the screen panel to give the program the
keyboard; the ring around it says the editor's own shortcuts are off while it has focus.
What the program transmits is appended to the console transcript, the same one print services write to, which is also what testcases assert on.
Bit 0 is Ready: a typed character is waiting in the receiver data register. The device sets it and clears it; a program only reads it.
The typed character, in the low byte. Reading it takes that character; the next one, if any, appears at once and Ready stays set until the queue is empty.
Bit 0 is Ready, and here it is always set: the console never makes a program wait to print.
Storing a character in the low byte prints it on the console. ASCII 12, a form feed, clears the console instead.
li s0, 0xffff0000
poll:
lw t0, 0(s0) # receiver control
andi t0, t0, 1 # the Ready bit
bnez t0, take
li a7, 32 # nothing typed yet: a wait costs no instructions
li a0, 10
ecall
j poll
take:
lw t1, 4(s0) # receiver data, one character
sw t1, 12(s0) # transmitter data: print it
The receiver never loses a keystroke: what does not fit in the data register waits in a queue behind it, and Ready (1) stays set until that queue is empty. Bit 1 of either control register, RARS's interrupt-enable bit (2), stops the program with an error: this editor polls, it does not deliver device interrupts.
Program time
Service 30 (a7 before a ecall) answers
with the time in milliseconds, low word in a0 and high word in a1, and service 32 waits for the
milliseconds in a0. Time is counted from the start of the run
rather than from 1970, so a program differences two reads exactly as it did before, and
a wait costs no instructions — a program idling on the keyboard never reaches the
execution limit. In a testcase both run on a virtual clock that starts at zero and only
advances through the program's own waits, so a five second wait finishes at once and
elapsed-time output is the same on every machine.
Differences from RARS
- The display is always there: it is a panel next to the memory view rather than a tool you connect to the program before running it.
- Interrupt-driven I/O is not supported. Setting the interrupt-enable bit of a control register stops the program with an error naming the feature; poll the Ready bit instead.
- Time comes from the start of the run, and a testcase runs on a virtual clock. RARS answers with the host's wall clock.
- There is no mouse: neither simulator's tools have one.
- Programs live in
examples/risc-v/in the repository, one per feature, each naming the display it wants in an@screencomment; RARS has no such directive and ignores the line.