Isle Chapter 5 Software

Published 22 Jan 2026 (DRAFT)

This page describes the chapter 5 software. This software is written in bare metal RISC-V assembler without a software library. The chapter 5 hardware is simpler than the main Isle design, so this software is not compatible with later chapters or the main Isle computer.

All the RISC-V instructions featured in these designs are covered in the RISC-V Assembler Guide.

If you're new to the project, read Isle FPGA Computer for an introduction. See Isle Index for more pages.

Hello

Our first example, hello.s, display "Hello!" using text mode hardware.

.equ TRAM_BASE, 0x4000  # text mode ram (tram) base address

.section .text
.global _start

_start:
    li sp, 0xC000  # stack grows down from here
    li s1, TRAM_BASE

    li t0, 0x0C000048  # colour 0xC - U+0048 - Latin Capital letter H
    sw t0, 0(s1)
    li t0, 0x0C000065  # colour 0xC - U+0065 - Latin Small letter E
    sw t0, 4(s1)
    li t0, 0x0C00006C  # colour 0xC - U+006C - Latin Small letter L
    sw t0, 8(s1)
    li t0, 0x0C00006C  # colour 0xC - U+006C - Latin Small letter L
    sw t0, 12(s1)
    li t0, 0x0C00006F  # colour 0xC - U+006F - Latin Small letter O
    sw t0, 16(s1)
    li t0, 0x03000021  # colour 0x3 - U+0021 - Exclamation mark
    sw t0, 20(s1)

.exit:
    j .exit

We use .equ to define a constant, TRAM_BASE, the starting address for text mode ram. Avoiding magic numbers in asm is as essential as anywhere else.

We then define a text section with the global symbol _start, which define the entry point for our program, where it'll start executing.

We set the stack pointer to one word beyond the top of memory with li sp, 0xC000. This example doesn't use the stack, but I always initialise the stack pointer as good practice.

With li s1, TRAM_BASE we load the tram base address into a saved register (survives function calls).

We're ready to write characters, which is straightforward once we know the tram data structure:

|31  28|27  24|23  21|20                  0|
|------|------|------|---------------------|
| BG   | FG   |  -   |        UCP          |
|------|------|------|---------------------|

Thus load immediate instruction li t0, 0x0C000048 is for code point 0x0048, which is capital H (the same as ASCII), foreground colour 0xC and background colour 0x0. The colours come from palette table; for the hello example we use the default palette loaded at design time, but you'll see how to control the palette in the fourth example, below.

sw t0, 0(s1) writes the word in register t0 to the address in s1 (which we previously set to TRAM_BASE) offset by 0. This updates the first character in text mode, at the top left of the display.

We repeat this process for each character, increasing the address offset by 4 each time because our CPU is byte addressed and each tram entry is 4 bytes (one word) long.

Finally, we have an infinite loop with the .exit label, as there's no operating system to return to.

You can see which characters are supported with chapter 5 hardware by looking at unifont-rom.mem.

You're no doubt thinking this is not an elegant way to write strings! You're right. In the next chapter we'll introduce a print function that handles this for us.

Screenshot of Verilator/SDL window showing 'Hello!' in Isle text mode.
RISC-V CPU saying "Hello!" using Isle text mode.

Framecount

The framecount.s example displays a hexadecimal frame counter at the top left of the display. We opt for hex in this example because it's simpler to calculate than decimal, especially as the chapter 5 CPU doesn't have multiply and divide instructions.

.equ TRAM_BASE,      0x4000  # text mode ram (tram) base address
.equ HWREG_BASE,     0xC000  # hardware reg base address
.equ FRAME_FLAG,     0x0110  # signals start of new frame (offset to HWREG_BASE)
.equ FRAME_FLAG_CLR, 0x0114  # clear frame flag (offset to HWREG_BASE)

.equ TEXT_COLR, 0x0D000000  # 0xXY000000 Y=foreground colour and X=background

.section .text
.global _start

_start:
    li sp, 0xC000  # stack grows down from here
    li s11, TEXT_COLR

.L_cnt_loop:
    li s1, TRAM_BASE  # needs to be in the loop to reset it after L_str_loop
    # load integer counter from memory and convert to hex string
    la a0, cnt_str  # load address of string to hold result
    la t5, cnt  # load address of counter in memory
    lw a1, 0(t5)  # load integer value of counter
    call int_strx  # convert integer to string; returns string address in a0

    # print hex number
.L_str_loop:
    lbu t2, 0(a0)  # load char
    beqz t2, .L_next_frame  # check for string end
    or t0, s11, t2  # combine text colour with code point
    sw t0, 0(s1)  # store char in tram
    addi a0, a0, 1  # next char
    addi s1, s1, 4  # next tram address
    j .L_str_loop

.L_next_frame:
    # increment counter in memory
    la t5, cnt
    lw t0, 0(t5)
    addi t0, t0, 1
    sw t0, 0(t5)

    # wait for next frame
    li a0, 1
    call frame_waitn
    j .L_cnt_loop


# frame_waitn - wait for n frame start signals
#   a0: frame count (starts) to wait for
#   return: none
#
#   returns immediately when 'a0' is zero
#
frame_waitn:
    beqz a0, 1f  # don't wait if 'a0' is zero
    li   t6, HWREG_BASE  # hwreg base addr
0:
    lw   t0, FRAME_FLAG(t6)  # load frame flag
    beqz t0, 0b  # loop if flag not set
    sw   zero, FRAME_FLAG_CLR(t6)  # clear frame flag (strobe)

    addi a0, a0, -1  # decrement remaining frame count
    bnez a0, 0b  # loop if frames remain
1:
    ret


# int_strx - integer to hexadecimal string
#   a0: address to hold decoded string (8 bytes + null termination)
#   a1: integer
#   return: address of string start
#
int_strx:
    li   t5, 0x3A      # threshold for converting to A-F
    addi t6, a0, 8     # start with least significant digit and work back
    sb   zero, 0(t6)   # store null-termination

.L_nib_loop:
    addi t6, t6, -1    # decrement string address
    andi t0, a1, 0xF   # AND out first nibble
    addi t0, t0, 0x30  # add U+0030 offset
    blt  t0, t5, 0f    # jump to loop end if number
    addi t0, t0, 7     # add 0x7 for A-F
0:
    sb   t0, 0(t6)     # write Unicode code point to string
    srli a1, a1, 4     # shift across next nibble
    bne  t6, a0, .L_nib_loop
    ret


.section .data

.balign 4
cnt:
    .word 0
cnt_str:  # 9 bytes
    .byte 0, 0, 0, 0, 0, 0, 0, 0, 0

The frame counter example uses data held in memory, which we define in the data section near the bottom of the source. We have a 32-bit counter at label cnt and a 9 byte string at cnt_str.

The section of code starting at .L_cnt_loop, loads the addresses of the string and counter in the data section (using the la pseudoinstruction) then loads the counter's integer value, before calling the int_strx function.

Further explanation to follow shortly.

Jump

jump.s is a two frame jumping figure animation.

.equ TRAM_BASE,      0x4000  # text mode ram (tram) base address
.equ HWREG_BASE,     0xC000  # hardware reg base address
.equ FRAME_FLAG,     0x0110  # signals start of new frame (offset to HWREG_BASE)
.equ FRAME_FLAG_CLR, 0x0114  # clear frame flag (offset to HWREG_BASE)

.equ TEXT_COLR, 0x0C000000  # 0xXY000000 Y=foreground colour and X=background
.equ ANIM_RATE, 60  # frames to wait between animation
.equ TEXT_LINE, 84  # number of characters in a line
.equ TRAM_DEPTH, 84*24  # number of characters in tram

.section .text
.global _start

_start:
    li sp, 0xC000  # stack grows down from here

.L_down:
    li s1, TRAM_BASE + (4*TEXT_LINE+4)*4

    li t0, TEXT_COLR | 0x006F  # U+006F - Latin Small letter O
    sw t0, 4(s1)
    addi s1, s1, 4*TEXT_LINE  # next line (4 bytes per char)
    li t0, TEXT_COLR | 0x003C  # U+003C - Less-than sign
    sw t0, 0(s1)
    li t0, TEXT_COLR | 0x004F  # U+004F - Latin Capital letter O
    sw t0, 4(s1)
    li t0, TEXT_COLR | 0x003E  # U+003E - Greater-than sign
    sw t0, 8(s1)
    addi s1, s1, 4*TEXT_LINE
    li t0, TEXT_COLR | 0x002F  # U+002F - Solidus or Slash
    sw t0, 0(s1)
    li t0, TEXT_COLR | 0x005C  # U+005C - Backslash
    sw t0, 8(s1)

    li a0, ANIM_RATE
    call frame_waitn
    call clr_text

.L_up:
    li s1, TRAM_BASE + (2*TEXT_LINE+4)*4

    li t0, TEXT_COLR | 0x005C  # U+005C - Backslash
    sw t0, 0(s1)
    li t0, TEXT_COLR | 0x006F  # U+006F - Latin Small letter O
    sw t0, 4(s1)
    li t0, TEXT_COLR | 0x002F  # U+002F - Solidus or Slash
    sw t0, 8(s1)

    addi s1, s1, 4*TEXT_LINE  # next line
    li t0, TEXT_COLR | 0x004F  # U+004F - Latin Capital letter O
    sw t0, 4(s1)
    addi s1, s1, 4*TEXT_LINE
    li t0, TEXT_COLR | 0x002F  # U+002F - Solidus or Slash
    sw t0, 0(s1)
    li t0, TEXT_COLR | 0x005C  # U+005C - Backslash
    sw t0, 8(s1)

    li a0, ANIM_RATE
    call frame_waitn
    call clr_text
    j .L_down


# frame_waitn - wait for n frame start signals
#   a0: frame count (starts) to wait for
#   return: none
#
#   returns immediately when 'a0' is zero
#
frame_waitn:
    beqz a0, 1f  # don't wait if 'a0' is zero
    li   t6, HWREG_BASE  # hwreg base addr
0:
    lw   t0, FRAME_FLAG(t6)  # load frame flag
    beqz t0, 0b  # loop if flag not set
    sw   zero, FRAME_FLAG_CLR(t6)  # clear frame flag (strobe)

    addi a0, a0, -1  # decrement remaining frame count
    bnez a0, 0b  # loop if frames remain
1:
    ret


# clr_text - clear textmode (tram)
#   no arguments, no return
#
clr_text:
    li t1, TRAM_BASE
    li t2, TRAM_DEPTH
    li t3, 0x20  # clear with space and default colours
    li t6, 0

0:  # loop over tram clearing locations
    sw   t3, 0(t1)   # clear location
    addi t6, t6, 1   # increment counter
    addi t1, t1, 4   # increment address (word-based)
    blt  t6, t2, 0b
    ret

The clr_text function is convenient, but not very efficient, because it clears every location in tram.

Further explanation to follow shortly.

Palette

The palette loading example, palette.s, shows how you can load a palette at runtime from the data section.

.equ CLUT_BASE, 0x0000  # colour lookup table base address
.equ TRAM_BASE, 0x4000  # text mode ram (tram) base address

.equ PAL_NAME, pal_go  # name of palette in data section
.equ PAL_SIZE, 16  # number of colours in palette (must match PAL_NAME)

.equ CHAR, 0x2588  # U+2588 - Full block
# .equ CHAR, 0x3F  # U+003F - Question mark

.section .text
.global _start

_start:
    li sp, 0xC000  # stack grows down from here
    li s1, TRAM_BASE
    li s2, CLUT_BASE
    li s11, CHAR

    # load palette config
    li t4, PAL_SIZE  # load palette size
    la t6, PAL_NAME  # load address of palette in data section

    # load palette into colour lookup table
.L_load_pal:
    lhu t3, 0(t6)  # load palette entry (unsigned half word)
    sw t3, 0(s2)  # save palette to CLUT
    addi s2, s2, 4  # next CLUT address
    addi t6, t6, 2  # next data half word
    addi t4, t4, -1  # count down
    bnez t4, .L_load_pal

    # write characters in all palette colours
    li t4, PAL_SIZE  # number of colours in palette
    li t5, 0  # palette index
.L_disp_pal:
    slli t0, t5, 24  # shift colour index into position
    or t0, s11, t0  # combine char code point with colour
    sw t0, 0(s1)  # store to tram
    addi t5, t5, 1  # next palette entry
    addi s1, s1, 4  # next tram address (+1 word)
    blt t5, t4, .L_disp_pal

.exit:
    j .exit


.section .data
.balign 2

pal_mono:  # palette: Mono-2
    .hword 0x10A5  # 0x0 - (04, 05, 05)
    .hword 0x7BFE  # 0x1 - (30, 31, 30)

pal_aqua:  # palette: Aqua-4
    .hword 0x00AB  # 0x0 - (00, 05, 11)
    .hword 0x0171  # 0x1 - (00, 11, 17)
    .hword 0x02F7  # 0x2 - (00, 23, 23)
    .hword 0x4FDC  # 0x3 - (19, 30, 28)

pal_go:  # palette Go-16
    .hword 0x0C43  # 0x0 - (03, 02, 03)
    .hword 0x0CAD  # 0x1 - (03, 05, 13)
    .hword 0x096F  # 0x2 - (02, 11, 15)
    .hword 0x0A76  # 0x3 - (02, 19, 22)
    .hword 0x058A  # 0x4 - (01, 12, 10)
    .hword 0x02CD  # 0x5 - (00, 22, 13)
    .hword 0x34F2  # 0x6 - (13, 07, 18)
    .hword 0x5DB5  # 0x7 - (23, 13, 21)
    .hword 0x44C5  # 0x8 - (17, 06, 05)
    .hword 0x458E  # 0x9 - (17, 12, 14)
    .hword 0x7189  # 0xA - (28, 12, 09)
    .hword 0x7ACF  # 0xB - (30, 22, 15)
    .hword 0x65C2  # 0xC - (25, 14, 02)
    .hword 0x7EC0  # 0xD - (31, 22, 00)
    .hword 0x5AF1  # 0xE - (22, 23, 17)
    .hword 0x7399  # 0xF - (28, 28, 25)

You can define your own palette with its own label, just bear in mind it's in RGB555 format.

Further explanation to follow shortly.

Next step: RISC-V CPU or Isle Index