Skip to content
Eliott65wiki
Contents

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.

A system call, step by stepThe program puts its parameters in a block of its own memory, writes the block address at $FFF8–$FFF9 and waits by reading $FFF9 again. The kernel, on core 0, reads the block, does the work, writes the results and sets $FFF9 back to zero. The program resumes. About 5 microseconds for a simple call.6502 programKernel (RP2040 core 0)① prepares a parameter block② writes the block address at $FFF8–$FFF9③ waits, reading $FFF9 againLDA $FFF9 / BNEreads the block, does the work,writes the results into the block④ sets $FFF9 back to 0⑤ resumes with its results≈ 5 µs for a simple call, about 36 6502 cycles
A system call seen from both sides. The 6502 keeps running during the call: it loops on $FFF9.

The block always has the same shape:

ByteContents
+0service number (SYSTEME, ECRAN, FICHIERS…)
+1function number within that service
+2error code, written by the kernel (0 = success)
+3 onwardsthe 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 loop2.77 µs
one complete system call4.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 $FFF8 and 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 in PHP / 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 EXECUTER does not return on success: the new program has taken its place.

The services

No.ServiceWhat it is for
0SYSTEMEtime, information, hibernation, processes (launch, yield, sleep, exit, kill)
1ECRANtest pattern, plain text, colour text, graphics
2CLAVIERkey queue in the program’s memory, reading, layout
3STOCKAGEread and write 512-byte blocks on the USB stick
4FICHIERSlist, read, write, delete, execute
5GRAPHIQUEvideo memory, palette, layers, sprites, raster
6SONnotes 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.

Sources in the repository : docs/adr/ADR-0002-noyau-hors-bande.md, docs/adr/ADR-0011-api-source-unique.md, docs/api/API.md, docs/journal/03-bus-demarrage.md, docs/journal/04-performances.md.