# BIOS Programmer's Reference

The system firmware contains a library of procedures
which aims to isolate software from the specific details
of the underlying hardware.
They allow programs to conduct input and output to two types of devices:
the user's console, and an external storage medium.

It should be remembered that the B in BIOS stands for *basic*;
the BIOS is not a replacement for an operating system.
Applications which invoke BIOS services directly
will still have dependencies on the overall nature of Kestrel-2DX hardware resources.
Thus, software using BIOS will only be portable between
similar revisions of the Kestrel-2DX hardware.

## Locating the BiosInterface Table.

Programs which are launched by the BIOS
may appear *anywhere* in the Kestrel-2DX IPL memory space.
BIOS itself, however, exists at a fixed location, starting at address 0.
Thus, programs that are loaded from external media must be position independent somehow,
and must find out for themselves how to access system BIOS.

The current process for locating the system BIOS is to scan memory for
a well-defined, 64-bit numeric constant starting at address $10000,
and ending at whatever address your program was loaded at.
The following assembly language routine will perform this task
for current revisions of the Kestrel-2DX hardware:

                    include "biosdata.i"

                    ; The first thing we need to do is find the BIOS entry points.
                    ; We don't actually know where it is placed in memory, except
                    ; that it will reside at an address less than _start.

    _start:         auipc   s0,0                    ; S0 -> _start
                    addi    sp,sp,-8
                    sd      ra,0(sp)

                    lui     a0,$10000               ; Start of ROM+RAM in Kestrel-2DX
                    ld      a2,bs_match-_start(s0)
    seek_bios:      ld      a3,bi_matchword(a0)     ; Find it yet?
                    beq     a3,a2,found_bios
                    addi    a0,a0,8                 ; Otherwise, try next dword in RAM
                    blt     a0,s0,seek_bios

                    ; If we're here, we did not find the BIOS entry point.  Wedge
                    ; hard, as there's nothing else we can do.  Your code might somehow
                    ; show a more helpful message on the screen, etc.

    wedge:          lui     a0,$10000
                    lh      a1,0(a0)
                    addi    a1,a1,1
                    sh      a1,0(a0)
                    jal     x0,wedge

                    ; If we've found the BIOS, then let's remember where we found it,
                    ; then kick off the rest of our program.

    found_bios:     sd      a0,bs_bi-_start(s0)
                    jal     x0,start_program

                    align   8
    bs_match:       dword   BI_MATCHWORD
    bs_bi:          dword   0

The value of `BI_MATCHWORD` is `$0BADC0DEB105DA7A`.  This value is chosen to appear, probabilistically, virtually never without explicit intent within the relatively tight range of memory being scanned.

## Calling BIOS Functions

Once you have a reference to the `BiosInterface` table,
you can invoke BIOS functions using a consistent procedure:

    First, load parameters into registers A0..A7.
    Invoke the desired procedure through the stored pointer.

If necessary, return values will appear in either `a0` and/or in `a1`.
Alternatively, some BIOS procedures take pointers to variables,
which will receive results upon successful completion of a procedure.

Here's an example where we desire to change the cursor location on the screen:

    L1:         auipc   a1,0                        ; A1 -> L1
                addi    a0,a1,cursor_x - L1         ; A0 -> cursor_x
                addi    a1,a1,cursor_y - L1         ; A1 -> cursor_y
                ld      t0,bs_bi - _start(s0)       ; T0 -> BiosInterface table
                ld      t0,bi_term_cursor_swap(t0)  ; T0 -> term_cursor_swap entry point
                jalr    ra,0(t0)                    ; Call BIOS procedure

                ; ...

    cursor_x:   byte    0
    cursor_y:   byte    0


## BIOS Function Reference

This section contains a brief summary of each of the BIOS functions currently implemented,
as of ROM version 0.1.0.  Each entry is formatted like so:

    bios_function_name (bios_interface_offset)

    results = bios_function_name(param1, param2, ...)
    A0                           A0      A1      ...

    Description of procedure taken when calling this function,
    including results returned.

### Storage Procedure Error Codes

Many of the storage-related BIOS procedures returns an error code.
These procedures share the same error code space, which can be summarized as follows:

* `E_OK` (0) &mdash; No errors were discovered; the SD card is safe to conduct I/O with.
* `E_UNIT` (-1) &mdash; The selected device is not recognized as a valid peripheral.
* `E_CARD` (-2) &mdash; The SD card has failed basic protocol checks and/or is missing.
* `E_TIMEOUT` (-3) &mdash; The SD card performed valid protocol sequences up until this point, but now is taking much longer than expected to complete some step in the protocol.  This can also happen because the SD card is now missing (perhaps because the user pulled it out in the middle of an I/O operation).
* `E_NOT_ACCEPTED` (-4) &mdash; The SD card seems to be operating OK, but for some reason a write operation was rejected by the SD card (e.g., perhaps due to incorrect CRC or insufficient privileges).

### Terminal Output: Cursor Management

#### `term_cursor_on` (8)

    term_cursor_on()

This procedure decrements an internal counter.
If the counter remains non-zero,
no further action is taken.
Otherwise, the cursor will be rendered again.
See also `term_cursor_off`.

**Note.**
After booting into software loaded from external storage,
the cursor will be off by default.
Your software must make a call to `term_cursor_on` to render the cursor again.

#### `term_cursor_off` (16)

    term_cursor_off()

This procedure turns the cursor off if it's visible,
and increments a counter.
As long as the counter is non-zero,
the cursor will remain off.

For every call to `term_cursor_off`,
there must be a corresponding call to `term_cursor_on`
for the cursor to be visible again.

#### `term_cursor_swap` (24)

    term_cursor_swap(uint8_t *px, uint8_t *py)
                     A0           A1

This procedure *exchanges* the requested cursor position
with the current cursor position.

On entry,
`px` is a *pointer* to a *byte* holding the desired X coordinate, and
`py` is a *pointer* to a *byte* holding the desired Y coordinate,
respectively,
of the new cursor position.
If either `*px` or `*py` are beyond the valid dimensions of the screen,
they will be clipped to the largest value possible.

On return,
`*px` will contain the previous cursor X coordinate, and
`*py` will contain the previous cursor Y coordinate.

This function can be used in three ways.

To recover the dimensions of the screen,
you can attempt to move the cursor to the largest expressible coordinate:

    uint8_t x = 255, y = 255;
    term_cursor_swap(&x, &y);
    term_cursor_swap(&x, &y);  Restores previous position
    printf(
        "The screen has %d rows and %d columns\n",
        y + 1,
        x + 1
    );

To move the cursor to a new position, simply ignore the results:

    uint8_t x = 40, y = 12;
    term_cursor_swap(&x, &y);

To query the cursor's current position, you must restore the cursor's previous position.
However, make sure you capture the results of the first call to `term_cursor_swap`:

    uint8_t x, y;
    term_cursor_swap(&x, &y);  Grabs current position.
    where.x = x;
    where.y = y;
    term_cursor_swap(&x, &y);  Restores previous position.

### Terminal Output: Displaying Text

#### `term_out_clear` (32)

    term_out_clear()

Clears the screen, and homes the cursor to the upper, left-hand corner.

**Note.**  You must turn the cursor off prior to
clearing the screen.
Clearing the screen will erase the cursor without updating BIOS cursor state.
The next time the cursor is moved, turned off, or a character is printed,
it will end up corrupting the display
by leaving a stray inverse-video character cell
(assuming the BIOS implements the cursor as a solid block).

The proper sequence for emitting a character is:

    term_cursor_off();
    term_out_clear();
    term_cursor_on();

#### `term_out_chr` (40)

    term_out_chr(char ch)
                 A0

Prints the provided character `ch` to the screen.

Some ASCII control values have certain interpretations
which do not result in a graphic character being printed.

|Character Code|Result|
|:-:|:--|
|$07|The *BEL* character; since the Kestrel-2DX has no audio facilities, it is ignored.|
|$08|Moves cursor to the left one character.  Does *not* erase the character underneath the cursor, nor does it shift characters to the right.|
|$09|Advance cursor to the next tab-stop.|
|$0A|Without altering the cursor's horizontal position, advance to the next line on the screen.  Scroll if necessary.|
|$0B|Vertical tab; ignored.|
|$0C|Clears the screen.|
|$0D|Without advancing the cursor to the next line, return the cursor to the far left-hand edge of the screen.|

Tab-stops are located every eight characters on the screen.

All other characters are treated as graphic characters for printing.
**Note.**  Printing characters in the set {0..6, 14..31} is not officially supported,
for these characters are intended for terminal control purposes.
Future versions of the Kestrel-2DX BIOS may interpret more control characters in the future.

**Note.**  You must turn the cursor off prior to
printing any characters.
The first character thus output will overwrite the cell containing the cursor.
If/when the cursor is moved,
or subsequently turned off for any other reason,
it will end up corrupting the display
by leaving a stray inverse-video character
(assuming the BIOS implements the cursor as a solid block).

The proper sequence for emitting a character is:

    term_cursor_off();
    term_out_chr(ch);
    term_cursor_on();

#### `term_out_buf` (48)

    term_out_buf(char *buf, size_t length)
                 A0         A1

Outputs a fixed-sized buffer to the screen.
Bytes within the buffer are interpreted as
7-bit ASCII with an extended character set
for character codes {128..255}.

Character codes {7..13} are treated as control
characters.  See `term_out_chr` for more details.

The buffer pointed to by `buf` *may* contain NUL characters.
These characters are rendered like any other character.
No more than `length` bytes will be printed.

**Note.**  You must turn the cursor off prior to
printing any characters.
The first character thus output will overwrite the cell containing the cursor.
If/when the cursor is moved,
or subsequently turned off for any other reason,
it will end up corrupting the display
by leaving a stray inverse-video character
(assuming the BIOS implements the cursor as a solid block).

The proper sequence for emitting a character is:

    buf = "Text goes here...";
    ...
    term_cursor_off();
    term_out_buf(buf, strlen(buf));
    term_cursor_on();

#### `term_out_str` (56)

    term_out_str(char *buf)
                 A0

Outputs a NUL-terminated string pointed to by `buf` to the screen.
Character codes {6..13} are treated as control
characters.  See `term_out_chr` for more details.
Character code 0 is considered NUL.

**Note.**  You must turn the cursor off prior to
printing any characters.
The first character thus output will overwrite the cell containing the cursor.
If/when the cursor is moved,
or subsequently turned off for any other reason,
it will end up corrupting the display
by leaving a stray inverse-video character
(assuming the BIOS implements the cursor as a solid block).

The proper sequence for emitting a character is:

    buf = "Text goes here...";
    ...
    term_cursor_off();
    term_out_str(buf);
    term_cursor_on();

### Terminal Input: Keyboard

#### `term_in_pollraw` (64)

    term_in_pollraw(uint16_t *praw, int *valid)
                    A0              A1

This procedure polls the keyboard hardware for a raw scancode.
If a scancode is available,
it is assigned to `*praw` and `*valid` is set to a non-zero value.
Otherwise, if no data is available yet,
`*valid` is set to zero, and the contents of `*praw` is undefined.

`praw` is a *pointer* to a 16-bit half-word,
which will hold the scancode if data are available.
`valid` is a *pointer* to a 64-bit dword
which will indicate if the value in `praw` is safe to use.

Scancodes are basically PS/2 scan codes,
but slightly modified, as follows:

|  15 | 14 .. 9 |  8  |  7 .. 0  |
|:-:|:-:|:-:|:-:|
| `R` | `0` | `X` | scan code |

where `R` is set if the scancode is a *key release* event.
That is, when *pressing* a key,
the scancode generated will have `R` clear;
meanwhile, when *releasing* that same key,
`R` will be set.

`X` is set if the key is an *extended* key code.
One of the characteristics of the PS/2 keyboard encoding
is that some keys share the same scancode,
for backward compatibility with older 16-bit DOS applications
running on IBM PC/XT or PC/AT computers.
For example, with `NUMLOCK` turned off,
pressing `4` on the numeric keypad generates the same scancode
as pressing the cursor-left key, since on earlier keyboards,
the numeric keypad *doubled* as the cursor movement keypad,
depending on the state of the `NUMLOCK` key.
Thus, the `X` bit allows interested applications
the opportunity to distinguish between "traditional" and "extended"
keys serving the same intended purpose.

The scan code, of course, corresponds to the PS/2 scan code
assigned to the particular key in question.

#### `term_in_rawtoascii` (72)

    term_in_rawtoascii(uint16_t raw, char *pascii, int *pvalid)
                       A0            A1            A2

This procedure attempts to convert the raw keyboard scancode
as returned by `term_in_pollraw`, into a corresponding ASCII codepoint.
Unfortunately, not all scancodes correspond to ASCII codepoints.
Thus, this procedure may fail.

`raw` is the raw scancode as returned by `term_in_pollraw`.
`pascii` is a *pointer* to a *byte*
which will hold the resulting character if the translation is successful.
`pvalid` is a *pointer* to a *dword*
which will indicate if the translation succeeded or not.

If `*pvalid` is non-zero,
the translation is successful, and
`*pascii` will contain a valid ASCII codepoint.
Otherwise, `*pascii` will be *undefined*, and its value must remain untrusted.

**Note.**  Currently, `*pascii` is set to zero on entry to this function.
However, do not depend on this behavior;
this is simply an artifact of how it's currently written,
and is not intended to be a formal part of its behavior.

#### `term_in_getmeta` (80)

    uint8_t term_in_getmeta()
    A0

Returns the current state of the
CTRL and SHIFT flags.
See also `term_in_setshift` and
`term_in_setctrl` for details on how these flags work.

The resulting byte is a bitmask:

| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| `0` | `0` | `0` | `0` | `0` | `0` | `C` | `S` |

where `S` represents the current state of the SHIFT flag,
and `C` represents the current state of the CTRL flag.

All other bits are explicitly reserved for future use,
and **must** be ignored by the caller for upward compatibility.

#### `term_in_setshift` (88)

    term_in_setshift(int state)
                     A0

If `state` is non-zero,
assert the SHIFT flag.
This causes all future ASCII graphic characters
submitted to `term_in_rawtoascii`
to be treated as *uppercase* or,
in the case of numerals,
*punctuation*
characters instead.

If `state` is zero,
restore normal, lowercase graphic character
handling semantics.

#### `term_in_setctrl` (96)

    term_in_setctrl(int state)
                    A0

If `state` is non-zero,
assert the CTRL flag.
This causes all future ASCII graphic characters
submitted to `term_in_rawtoascii`
to be treated as *control* characters instead.

If `state` is zero,
restore normal graphic character handling semantics.

**NOTE.** The CTRL flag overrules the SHIFT flag.
Thus, there is no difference between CTRL-A and ** CTRL-SHIFT-A;
both will result in a character code of $01.

### External Storage

#### `sdmmc_writeblock` (104)

    err = sdcard_writeblock(block, buffer)
    A0                      A0     A1

Copies a 512-byte block of data from the buffer provided in `buffer`
to the storage sector named in `block`.

#### `sdmmc_readblock` (112)

    err = sdcard_readblock(block, buffer)
    A0                     A0     A1

Copies a 512-byte block of data from the sector number provided in `block`,
to the buffer supplied in `buffer`.
The caller is responsible for ensuring that `buffer` is big enough to hold at least 512 bytes.

#### `sdmmc_idle` (120)

    err = sdmmc_idle()
    A0

Performs an SD protocol initialization and idling sequence for the card,
if any card exists in the slot.
If, for any reason, a problem were to occur during this procedure,
an appropriate error result is returned.
**It is vitally important that no I/O to the SD card be performed if an error is returned from this procedure.**
Doing so may result in abnormal system behavior and/or data loss,
both in memory *and on the SD card itself.*

#### `sdmmc_is_present` (128)

    flag = sdmmc_is_present()
     A0

Returns non-zero if an SD card is inserted into the slot; 0 otherwise.

#### `spi_select` (136)

    err = spi_select(device)
    A0                 A0

Addresses a specific SPI device for I/O.
Currently, only device 0 is supported (the SD card slot).
Devices 1..3 are accepted, but do not address any specific hardware in the stock Kestrel-2DX configuration.

Returns `E_OK` if given a valid device ID; otherwise, `E_UNIT` is returned.

#### `spi_deselect` (144)

    spi_deselect()

Deselects all SPI devices.

## Rationale

### Why Scan for `BiosInterface`?

The RISC-V JAL instruction only covers a 2MB range of addresses relative to the `JAL` instruction itself.  A future Kestrel design may well place RAM resources further than 2MB away from where the BIOS ROM sits.  Thus, software cannot reliably use the `JAL` instruction to invoke BIOS services directly.  It can, however, load a base address into a temporary register, typically `t0`, and invoke services through that using `JALR`.

### Why Not Use `ECALL`?

When it was first written, the BIOS consisted of two parts: a raw assembly bootstrap routine, and the bulk of the functionality written in C.  No means currently exists of informing the assembler where the C component's `_start` address resides.  Further, I have had, and continue to have, great difficulty convincing GNU `ld` to place the `.text` segment where I desire it.  If I had control over this, this whole problem could have been avoided.  Alas, it *refuses* to place `.text` ahead of `.data`, and so here we are.  Thus, just like application software loaded from external storage, it must scan memory to find this entry point.  Thus, one must wonder what value to set the `mtvec` register to handle machine-mode traps with?  While this is a solvable problem, **at the time**, I considered this approach easier to get working sooner.

One more note on this: using `ECALL` necessarily means that `mtvec` would need very careful handling if a loaded operating system wanted to use `ECALL` itself to handle system calls.  It would, in effect, circumvent BIOS all-together, leaving the BIOS with no clear way of being invoked, except through a process of chaining.  Careful coordination between BIOS and the operating system would be needed to ensure BIOS and the OS could evolve independently of each other.

### Why Start Scan at Address $10000?

The BIOS code necessarily incorporates this constant as a value within its ROM.
If scanning starting at address 0,
then application programs will find the instance in ROM, not in RAM.
The ROM-resident table of pointers to BIOS functions have not been properly relocated,
and so calling them directly will only crash your software.

Starting the scan at address $10000 ensures
that scanning starts at the beginning of RAM,
where the BIOS will have copied and properly relocated all the pointers within the `BiosInterface` table.