System calls: a two-byte gate
A program that wants to display something, read a file or know the time knocks at a two-byte gate. The kernel, on the other side, does the work. Simple, and without reserving anything in the program’s memory.
On this page
The principle
The program prepares a parameter block in its own memory, wherever it likes (except in page zero). Then it writes the address of this block into the gate:
- the low byte at
$FFF8; - the high byte at
$FFF9, which triggers the call.
It then waits for the kernel to reset $FFF9 to zero. The results and the error code are then in
its block.
$FFF9.The block always has the same shape:
| Byte | Contents |
|---|---|
| +0 | service number (SYSTEME, ECRAN, FICHIERS…) |
| +1 | function number within that service |
| +2 | error code, written by the kernel (0 = success) |
| +3 onwards | the parameters, then the results |
In assembly (64tass), with the supplied libe65 library:
lda #<bloc
ldx #>bloc
jsr e65_porte ; or the macro #E65_APPEL bloc
lda bloc+E65_ERREUR ; 0 = success
bloc: .byte E65_SVC_ECRAN, E65_ECRAN_TEXTE, 0
.word $0400 ; address of the text page
.byte $1F ; white on blue
Why it is different
A common way of talking to a system is a fixed area: a “mailbox” at an agreed address, where the
parameters are dropped off. Morpheus, for example, uses 8 bytes at $FF00. Eliott65 does it
differently:
- no reserved area: the block is wherever the program wants, and it can have several;
- no resident code: no system routine to call in 6502 memory;
- the call is synchronous: when the 6502 leaves its loop, the work is done.
How the kernel sees the gate
With the PIO + DMA bus, no ARM core sees the 6502’s writes go by. The kernel, on core 0, therefore
watches the byte $FFF9 in the 6502’s memory. As soon as it is no longer zero, it reads the
block, handles the call, writes the results, then resets $FFF9 to zero.
That is why the high byte must be non-zero: the block cannot be in page zero.
What it costs
The LATENCE.E65 program measured the cost of a call from the 6502 itself, using the system clock:
1,000 iterations of an empty loop, then 1,000 iterations with a SYSTEME PING call.
| Duration | |
|---|---|
| one iteration of the empty loop | 2.77 µs |
| one complete system call | 4.99 µs, i.e. ≈ 36 cycles |
Of these 36 cycles, around thirty are the 6502’s own instructions (preparing, writing the gate, looping, returning). The kernel adds about 1 µs.
The worst case is not measured: a call that arrives while the USB driver is polling the hub (once per second, for a few milliseconds) waits accordingly.
The rules to know
- No calls inside an interrupt handler. Between the write to
$FFF8and the write to$FFF9, an interrupt making its own call would overwrite the low byte. If it is essential, the main program wraps its calls inPHP / SEI … PLP. - The 6502 is not frozen during the call: its clock keeps running and its interrupts are served, even during a long access to the USB stick.
FICHIERS EXECUTERdoes not return on success: the new program has taken its place.
The services
| No. | Service | What it is for |
|---|---|---|
| 0 | SYSTEME | time, information, hibernation, processes (launch, yield, sleep, exit, kill) |
| 1 | ECRAN | test pattern, plain text, colour text, graphics |
| 2 | CLAVIER | key queue in the program’s memory, reading, layout |
| 3 | STOCKAGE | read and write 512-byte blocks on the USB stick |
| 4 | FICHIERS | list, read, write, delete, execute |
| 5 | GRAPHIQUE | video memory, palette, layers, sprites, raster |
| 6 | SON | notes and melodies on the board’s speaker |
The full list, with every parameter block and every error code, is in the API reference.
A single source for the API
The API is described only once, in a Python file in the repository. Two things are generated
from it: the reference documentation and the e65.inc include file for the assembler. A test checks
that both are up to date. The principle is the same as Morpheus’s, which also generates its dispatch
code and its documentation from a single source.