Skip to content
Eliott65wiki
Contents

Several programs at once

A 6502 has only 64 KiB of memory and no protection. Yet Eliott65 runs up to eight programs on it, even those that never hand control back. Here is how, step by step.

On this page

Step 1: two programs, two memories (v0.13)

The PIO + DMA bus computes every address from a base. Switching programs is therefore simply a matter of changing the base: the 6502 finds itself in another 64 KiB block of the RP2040. Nothing is copied, not a single byte is lost. The RP2040’s memory had room for two such blocks.

To hand over control, a program calls e65_ceder: as with hibernation, the routine stores its registers in its own bytes, then the kernel stops the 6502, changes the base and restarts it in the other program.

Only one program has the screen and keyboard: it is said to be in the foreground. The others run in the background.

The ghost letters bug

During testing, two letters, “PR”, kept reappearing on the first program’s screen, in a place where it never wrote. Analysis showed that they were exactly the bytes being written by the other program.

The cause: the W65C02S still executes a few cycles after being asked to reset. At the moment of switching, it was already in the new program’s memory; it read there that the call had finished, left its loop and wrote into the wrong memory. The fix: put the 6502 into RESET and give it about 70 cycles in the old program’s memory, while it is only reading, and only then change memory.

Result: 168,000 switches without a single corrupted byte in either program.

Step 2: interrupting a selfish program (v0.14)

“Cooperative” multitasking assumes that each program hands over control from time to time. What if it never does? It has to be possible to interrupt it: that is pre-emptive multitasking.

The 6502 has a non-maskable interrupt, the NMI. But handling it requires code in the 6502’s memory, and Eliott65 does not want to leave anything there. The trick: the kernel borrows 16 bytes ($FFE8–$FFF7) for the duration of the interrupt. It places a tiny handler there that pushes registers A, X and Y (the 6502 has already pushed PC and P), notes the stack pointer, then gives the bytes back before switching programs. The whole state of the interrupted program is on its own stack.

On the board, first time round: a program checking its registers on every pass went through 6,129 pre-emptions and about 2.5 million checks, without a single error.

Step 3: up to 8 programs (v0.17)

Two 64 KiB blocks means two programs. Going further would have required translating addresses on every cycle (an MMU in the bus): when measured, that approach cost 34% of the speed. It was set aside in favour of another idea: swapping pages at switching time.

Several programs in the same memoryThe 6502 always sees a single 64 KiB window, cut into 16 pages of 4 KiB. At each program switch, the active program's non-zero pages go out to a pool of 31 frames of 4 KiB, and the next program's pages come in. A page that is entirely zero takes no frame.The 6502's window64 KiB = 16 pages of 4 KiB010000000000000Fout: its non-zero pagesin: those of the next oneRP2040 pool31 frames of 4 KiBAAABBBBCCCtableactive program (A)program B, waitingprogram C, waitingfree frameA page left at zero takes no frame: a typical program only fills 3 of them.
The 6502 always sees the same 64 KiB window. Waiting programs keep their pages in a pool in the RP2040; only non-zero pages make the round trip.
  • The 6502’s memory is divided into 16 pages of 4 KiB.
  • Waiting programs keep their pages in a pool of 30 frames of 4 KiB (31 up to v0.54: the last one now holds the shared page of zeros, which gave 4 KiB back to the firmware).
  • At switching time, the non-zero pages of the active program go out, and those of the next one come in.
  • A page left at zero costs nothing: it occupies no frame.

A typical program uses only 3 pages (zero page and screen, code, vectors): that leaves room for many others. The limit is set at 8 processes.

Measurement, 5 programs at onceValue
page swapping at switching time307 µs
6502 speed6.89 MHz (−5% compared with a single program)
register errors after 12,308 switches0

What the user saw

  • A horizontal bar moving down the screen: during the swap, the video was reading memory that was being rewritten. The video now reads the foreground screen through its own page table, switched before the rewrite.
  • Lost keystrokes: they were only passed to the program in a case that never arose for the shell. They are now passed when the program enters the window, and in order.
  • A test program that copied itself “as long as there were not two of it” filled the machine with copies of itself: unintentional proof that eight really do fit.

Step 4: sleeping instead of yielding (v0.18)

A shell waiting for a key has nothing to do, yet it was handing over control hundreds of times a second, and each time cost a page swap. It now sleeps: it is no longer chosen until a key arrives for it or a timeout expires. A key for a sleeping foreground program wakes it immediately.

Measured effect: while waiting, 4 switches in 10 seconds instead of about 130 per second.

The shell

SHELL.E65 starts on its own if it is on the USB stick. It is a scrolling terminal, in 40 × 30 colour or, since v0.57, in 80 × 25 with the Hercules mode’s text:

CommandEffect
NOMruns the program NOM.E65 in the foreground
NOM &runs it in the background
PSlists the processes
FG nbrings process n to the foreground
KILL nterminates process n
DIRlists the USB stick, page by page
MODE 40, MODE 80switches to 40 × 30 colour or to 80 × 25
CLS, HELPclears the screen, displays the help

The commands have been in English since v0.57.1, at the user’s request (HELP, FG and KILL replace AIDE, PP and TUER); the messages remain in French.

Ctrl+Tab, handled by the kernel, passes the screen and keyboard to the next program. A hibernated machine can also be restarted in the background: ETAT.HIB &.

Eliott65 shell: REGISTRE & and MULTI & started in the background, then PS listing REGISTRE.E65, SHELL.E65 in the foreground and MULTI.E65
Three processes: REGISTRE and MULTI in the background, the shell in the foreground. Screenshot from the Eliott65 emulator.

A flaw found later

While measuring the graphics (v0.40), a program that made system calls back to back left the kernel unable to pre-empt it: on every attempt, the kernel found a call in progress, gave up, and tried again. The order of operations was corrected (freeze, check the gate, then count the pages). See Tiles and sprites.

Sources in the repository : docs/journal/11-multitache.md, docs/adr/ADR-0015-multitache.md, docs/adr/ADR-0016-preemption-nmi.md, docs/adr/ADR-0017-shell.md, docs/adr/ADR-0018-pages-a-la-commutation.md, docs/adr/ADR-0019-sommeil.md.