Skip to content

RISC-V Assembly Guidelines For The Turbo V

Rando notes for me

Using .S files:

section_kernelvars cursor_column .space 1 cursor_row .space 1 textcolor .space 1 alignment .space 1 long_var .space 4

vs .s:

.section .data

banner_color: .byte MSG_COLOUR_RED

(but sections can't be arbitrarily named? Confused)

Folks can write assembly however they like, ultimately. That is part of the fun of a bare-metal computer! To make things easier for folks, these are some suggestions for how to make the experience more consistent.

Using the ABI

The system kernel abides by the RISC-V ABI and it is suggested that applications do the same.

Raw Name ABI Meaning Preserved
x0 zero Zero Immutable
x1 ra Return Address No
x2 sp Stack Pointer Yes
x3 up (gp) User "Zero" Page Immutable
x4 kp (tp) Kernel "Zero" Page Immutable
x5 - x7 t0 - t2 Temporary Registers No
x8 - x9 s0 - s1 Callee-Saved Registers Yes
x10 - x17 a0 - a7 Argument Registers No
x18 - x27 s2 - s11 Callee-saved Registers Yes
x28 - x31 t3 - t6 Temporary Registers No

Note s0 can be an alias to fp (Frame Pointer) which C programs may use.

zp and kp

zp and kp are set as part of the Turbo V boot sequence and point to the User Variables and Kernel Variables memory bases respectively.

On the Turbo V, the zp (gp in the official RISCV abi) register has a more special meaning. It stores the base memory address for the User Vars ("Zero Page" in 6502 terms) area. This allows constantly fast access to important user variables, much like how the zero page works on 6502. The user can have up to 2KB of data here, though at present 1KB is allocated (though this may change).

Likewise, kp (tp) is the same thing, only for the Kernel Vars. Unlike the 6502, on the Turbo V the kernel has its own "zero page". This makes interrupts less expensive (at least those which use kernel vars).

The original use of the tp is a thread pointer which is mostly for multi-threaded systems, of which Turbo V is not. While future versions might be, that is a big enough change where it might be a different system entirely, though one where the function of tp would still be similar to how it will be used today.

Why do all this? The user and kernel variable spaces use fast DPRAM available and run at CPU speed. So even if a program is running in System RAM (Bus RAM), variables fetches will be fast. Second, this can result in fewer instructions as the variables can be fetched quickly using zp or kp as memory offsets. It's all very similar to the 6502 zeropage.

Using tv.inc

tv.inc contains helper macros and constants to using the system. Things like names of the kernel calls, the kcall macro (for calling kernel routines), colors, character names, etc.

This keeps applications a bit more consistent as they use well known common names and macros.

Local Variables

Folks coming from 6502 may be used to overloading variables from the Zeropage. Things like zp_TMP0 might get renamed in a local routine to FARTS to make it easier to follow. In RISCV, there are many more registers where it may be beneficial to rename a register to a local variable. The recommended way to do that is via .equ and .undef. For example:

cool_routine:
    # Define local aliases
    .equ    cursor_col, t2
    .equ    cursor_row, t3

    lbu  cursor_col, 0(t1)
    lbu  cursor_row, 1(t1)
    ...

    # Clean up aliases to avoid conflicts
    .undef    cursor_col
    .undef    cursor_row

Scopes

In gcc/gas scopes as they exist in ca65 are not a thing. They can be somewhat emulated by using a few things:

Local Labels

Using local labels keeps those labels local to the scope of the file. This is done with the .L prefix (e.g. .Lloop1).

.local

Using .local keeps the defined routine or variable local to the file.