P32W programmer's guide¶
How to write programs for the P32W: the registers, the memory map, the instructions, the devices on the I/O card, exceptions and interrupts, virtual memory, the boot ROM — and the tools that check your program before any hardware exists. The authorities behind this guide: isa-v0.md (instructions and encodings), isa-system.md (traps, interrupts, the MMU), instruction-reference.md (generated from the ISA table), and the decision log decisions.md. How the machine does it in hardware: architecture.md (the cards and the address map) and microarch.md (the microcode).
1. The machine in one page¶
- 32-bit, load/store, 16 general registers, fixed 32-bit instructions, byte addressing, little-endian, aligned accesses only (a misaligned word or halfword traps).
- No condition flags: branches compare two registers (
BEQ,BLT, …). - Two modes: supervisor (reset, the kernel) and user. User programs run in a virtual address space of 2 GiB, translated by a software-managed TLB; supervisor code sees physical memory directly.
- Memory: 1 MiB boot ROM at address 0, RAM above it on one to four 16 MiB memory cards (up to 64 MiB), the I/O page at the top of the address space (D52).
- Speed: a 3 MHz microcycle, about 6–7 microcycles per instruction — roughly 0.5 MIPS. Every instruction, including multiply and divide, takes a fixed, small number of cycles.
2. Registers¶
| Register | Use |
|---|---|
r0 (zero) |
always reads 0; writes are discarded |
r1–r14 |
general purpose |
r15 (sp) |
stack pointer — banked: the supervisor SP in supervisor mode, the user SP in user mode; switching mode switches the bank |
PC |
instruction address (a multiple of 4) |
Control registers — read with MFCR rd, cr, written with MTCR cr, rs,
supervisor mode only:
| # | Name | Contents |
|---|---|---|
| 0 | STATUS |
bit 0 S (supervisor), 1 IE (interrupts enabled), 2 PS (previous S), 3 PIE (previous IE) |
| 1 | EPC |
the PC of the instruction that trapped (or the next one, for an interrupt) |
| 2 | CAUSE |
exception code; bit 31 set for an interrupt (then bits 2:0 = the IRQ line) |
| 3 | BADADDR |
the faulting address (misaligned, bus error, privilege, TLB, protection); 0 otherwise |
| 4 | VBR |
address of the one exception handler (multiple of 4) |
| 5 | USP |
the user-mode r15, as seen from supervisor mode |
| 6 | IPEND |
read-only: bit n = interrupt line n asserted |
| 7 | IMASK |
bit n = 1 enables interrupt line n |
| 8 | ASID |
the current address-space ID (8 bits) |
| 9 | TLBHI |
a TLB entry's tag: VPN (31:12), I (8: instruction side), ASID (7:0) |
| 10 | TLBLO |
a TLB entry's data: PFN (31:12), X (2), W (1), V (0) |
Reset state: supervisor mode, interrupts disabled (IE = 0, IMASK = 0),
VBR = 0, PC = 0. No other register is initialised — set sp first.
3. Memory map (physical)¶
| Range | Size | What |
|---|---|---|
0x0000_0000–0x000F_FFFF |
1 MiB | boot ROM (flash on the memory card); writing it is a bus error |
0x0010_0000–0x00FF_FFFF |
15 MiB | RAM on the first memory card (window 0) |
0x0100_0000–0x03FF_FFFF |
up to 48 MiB | RAM on more memory cards, 16 MiB each (windows 1–3, by jumpers; D52) |
0xFE00_0000–0xFE07_FFFF |
64 KiB a card | kept for video memory, at 0xFE00_0000 + n × 64 KiB; the video card (revision 2, §6.6) has none on the bus, so nothing answers |
0xFFFF_8000–0xFFFF_FFFF |
32 KiB | I/O page (§6): eight 4 KiB card windows |
| everything else | — | nothing answers: bus error |
A memory card that is not there does not answer either, so a program that
sizes memory must be ready for a bus error (the kernel's probe is
kernel/vm.c).
The I/O page is the top 32 KiB so that a sign-extended 16-bit offset from
r0 reaches it: lbu r2, 0xFFFFFF05(r0) is one instruction. The assembler
accepts the full address as the offset.
4. Instructions¶
The full table, generated from the ISA source, is instruction-reference.md. In outline:
| Group | Instructions |
|---|---|
| Arithmetic, logic (register) | ADD SUB AND OR XOR NOR SLL SRL SRA SLT SLTU |
| … with an immediate | ADDI SLTI SLTIU (sign-extended), ANDI ORI XORI (zero-extended), SLLI SRLI SRAI, LUI |
| Multiply, divide | MUL MULH MULHU DIV DIVU REM REMU — division by zero does not trap: DIV/DIVU give all ones, REM/REMU the dividend |
| Load, store | LB LBU LH LHU LW, SB SH SW — rd, offset(rs) |
| Branch | BEQ BNE BLT BGE BLTU BGEU ra, rb, label (±128 KiB) |
| Jump, call | JMP label, CALL label (±32 MiB), JR rb, CALLR rb, RET |
| System | SYSCALL BREAK RTE MFCR MTCR WAIT TLBW HALT |
Assembler pseudo-instructions (assembler/p32asm.py): NOP, MOV rd,rs,
NEG, NOT, LI rd,value (1 or 2 instructions), LA rd,address (always 2),
PUSH rs, POP rd, BEQZ/BNEZ ra,label, and BGT BLE BGTU BLEU (the
operands swapped).
Calls. CALL pushes the return address and jumps; RET pops it. The stack
is full-descending: sp points at the last word pushed, and PUSH
decrements before storing. Keep sp a multiple of 4.
A calling convention (used by the library and the examples; the ISA does
not impose one): arguments in r2–r5, the result in r1; a routine may
clobber r10–r14; it preserves r1–r9 except the result. System calls
(the test kernels): number in r1, arguments r2–r5, result r1.
Large constants and addresses: LI r1, 0x12345678 becomes
LUI r1, 0x1234 + ORI r1, r1, 0x5678; LA always uses two words so a
label's address may change between assembler passes.
Example: print a string¶
li sp, 0x01000000 ; top of the first memory card
la r2, msg
call uart_puts
halt
msg: .asciz "Hello, P32W\n"
.include "../software/lib/p32io.inc"
5. Programs, the boot ROM and reset¶
At reset the CPU fetches from address 0 in supervisor mode: the boot ROM
is the first code that runs. A ROM program must:
1. set sp (e.g. li sp, 0x01000000, the top of the first memory card);
2. initialise the console UART (uart_init, §6.1) — the emulators work
without it, the hardware does not;
3. set VBR before anything can trap, if it wants to handle exceptions.
The assembler's output is a flat image starting at address 0: that is the
ROM image, for the emulators (p32emu image.bin) and for the flash
programmer alike.
The boot ROM monitor (software/monitor) is the
standard ROM program: a console prompt to dump and modify memory, call a
program, load sectors from the CompactFlash card, and boot a program from
it (sector 0: "P32B", sector count, load address, entry point). Its
exception handler reports CAUSE, EPC and BADADDR and returns to the
prompt.
Programming the ROM on the real machine: the four flash chips (SST39SF0x0) on the memory card hold byte lanes 0–3 of each word. Program them in a TL866-class programmer (split the image into lanes). (Writing them in place through the debug card, with a PROG jumper, was planned in D50 and dropped in D101: the memory card ties the chips' write line high.)
6. The devices¶
All I/O registers are in the top 32 KiB, in card windows of 4 KiB
(window n at 0xFFFF_8000 + n × 0x1000, chosen by jumpers on the card;
D52). A card's registers are the top 256 bytes of its window, and at
+FF0 / +FF1 / +FF4 every card answers with its card ID (byte:
1 I/O, 2 disk, 3 video; the debug card answers no bus address, D101), its
revision (byte) and its
interrupt status (word, a bit per device). The default jumpers: the
I/O card in window 7 (FFFF_F000), the disk card in window 6
(FFFF_E000), the video card in window 5 (FFFF_D000).
Byte registers (UARTs, ports, RTC, USB, network; the CF data register
is a halfword)
must be accessed with LB/LBU/SB; timer registers with LW/SW.
Any other size, or an address no device uses, is a bus error — and the
I/O page traps in user mode, so only the kernel touches devices.
| Address | Device | IRQ |
|---|---|---|
FFFF_FF00–FF07 |
UART 0 — the console | 2 |
FFFF_FF08–FF0F |
UART 1 | 3 |
FFFF_FF30–FF37 |
UART 2 | 2 |
FFFF_FF38–FF3F |
UART 3 | 3 |
FFFF_FF10 |
TCOUNT — timer count |
0 |
FFFF_FF14 |
TRELOAD — timer period |
|
FFFF_FF18 |
TCTRL — bit 0 enable, bit 1 pending (write 1 to clear) |
|
FFFF_FF20 |
input port (8 bits) | |
FFFF_FF21 |
output port (8 bits, reads back) | |
FFFF_FF40 / FF41 |
real-time clock (DS12C887): register number / register | 5 |
FFFF_FF50 / FF51 |
USB 0 (CH376S): data / command, status (§6.8) | 6 |
FFFF_FF52 / FF53 |
network coprocessor: data / status, control (§6.9) | 7 |
FFFF_FF54 / FF55 |
USB 1, as FF50 / FF51 |
6 |
FFFF_FFF0 / FFF1 / FFF4 |
the I/O card's ID (1), revision, interrupt status (bit 0 timer, 1 UART 0, 2 UART 1, 3 UART 2, 4 UART 3, 6 USB 0, 7 USB 1, 8 network) |
The disk card (window 6): four IDE ports of 16 bytes, port p at
FFFF_EF00 + 16 p (0 CF 0, 1 CF 1, 2 IDE A, 3 IDE B; §6.4).
| Address | Device | IRQ |
|---|---|---|
FFFF_EF00–EF07 |
port 0, the first CompactFlash card: the True-IDE task file | 1 |
FFFF_EF08–EF0F |
port 0's control block (EF0E alternate status) |
|
FFFF_EF10–EF3F |
ports 1–3, the same layout | 1 |
FFFF_EF40 / EF44 / EF48 |
the DMA engine: DMAADR, DMACNT, DMACTL (words; §6.7) | 1 |
FFFF_EFF0 / EFF1 / EFF4 |
the disk card's ID (2), revision, interrupt status (bit p = port p) |
The interrupt levels are jumpers on the cards; the defaults are in the IRQ columns above and in §7, and §6.10 lists every source with its jumpers and its vector.
software/lib/p32io.inc has tested routines for everything below; its
symbols (U0, ULSR, CFCMD, …) are used here.
6.1 The UARTs (TL16C550)¶
Each UART has the PC-standard 16550 registers at consecutive bytes:
| Offset | Read | Write | With LCR bit 7 (DLAB) set |
|---|---|---|---|
| 0 | RBR: received byte | THR: byte to send | DLL: divisor low |
| 1 | IER: interrupt enable | IER | DLM: divisor high |
| 2 | IIR: interrupt identification | FCR: FIFO control | |
| 3 | LCR: line control | LCR | |
| 4 | MCR: modem control | MCR | |
| 5 | LSR: bit 0 = data ready, bit 5 = transmitter ready | — | |
| 6 | MSR: modem status | — | |
| 7 | SCR: scratch | SCR |
Baud rate = 1.8432 MHz / (16 × divisor): 12 → 9600, 6 → 19200,
1 → 115200. Set-up (uart_init): LCR = 0x80, DLL/DLM = divisor,
LCR = 0x03 (8 data bits, no parity, 1 stop), FCR = 0x07 (FIFOs on and
cleared), IER = 0.
Polled I/O:
putc: lbu r10, 0xFFFFFF05(r0) ; LSR
andi r10, r10, 0x20 ; transmitter ready?
beqz r10, putc
sb r2, 0xFFFFFF00(r0) ; THR
ret
getc: lbu r10, 0xFFFFFF05(r0)
andi r10, r10, 0x01 ; data ready?
beqz r10, getc
lbu r1, 0xFFFFFF00(r0) ; RBR
ret
Interrupts: IER bit 0 interrupts when a byte arrives, bit 1 when the
transmitter is empty. Read IIR to find out which (bits 3:0: 0x4 data
received, 0x2 transmitter empty, 0x1 nothing pending); reading IIR clears
a transmitter-empty interrupt, reading RBR a receive interrupt. Then enable
the line in IMASK (bit 2 for UARTs 0 and 2, bit 3 for UARTs 1 and 3) and set STATUS.IE.
On the I/O card each UART has a DB9 connector at the front edge.
6.2 The timer¶
A 32-bit down-counter: TCOUNT counts down once per tick while
TCTRL bit 0 is set; when it reaches 0 it reloads from TRELOAD and sets
pending (TCTRL bit 1), which raises IRQ0 until acknowledged by writing
TCTRL with bit 1 set (keep bit 0 set to keep counting).
li r2, 10000 ; 10 ms at 1 MHz
sw r2, 0xFFFFFF14(r0) ; TRELOAD
sw r2, 0xFFFFFF10(r0) ; TCOUNT
li r2, 1
sw r2, 0xFFFFFF18(r0) ; enable
; in the IRQ0 handler:
li r2, 3
sw r2, 0xFFFFFF18(r0) ; acknowledge, keep counting
The tick: 1 MHz on the hardware, divided from the bus clock, so it stops
when the CPU is halted from the front panel. In the emulators one tick
per instruction, so interrupt timing is exactly repeatable in tests. On the
hardware TCOUNT may read 0 for one microcycle at expiry, just before the
reload; in the emulators it never reads 0 while counting.
6.3 The ports¶
FFFF_FF20 reads the 8-bit input port; FFFF_FF21 is the 8-bit output port
(reads back what was written). Both go to a connector on the I/O card; with
nothing connected the input port reads 0xFF (pull-ups). In the emulators
-p N sets the input value and -t traces writes to the output port.
6.4 The CompactFlash disk¶
The disk card's port 0: a CF card in a socket at the card's front edge, in True-IDE mode; the data register is 16 bits wide (D26); 512-byte sectors addressed by a 28-bit LBA.
| Address | Read | Write |
|---|---|---|
EF00 |
data: a halfword (lhu / sh), the first byte in bits 7:0 |
data |
EF01 |
error | features |
EF02 |
sector count | sector count |
EF03–EF05 |
LBA 7:0, 15:8, 23:16 | same |
EF06 |
drive/head | 0xE0 | LBA 27:24 (LBA mode, drive 0) |
EF07 |
status: 7 BSY, 6 DRDY, 3 DRQ, 0 ERR | command |
Cards start in 16-bit mode; cf_init sends features = 0x81, command
0xEF (SET FEATURES: no 8-bit transfers) in case earlier software chose
8-bit mode, and ignores a refusal. Read a sector (cf_read): count = 1,
LBA, head, command 0x20; wait for DRQ; read the data register as a
halfword 256 times (into a halfword-aligned buffer). Write (cf_write):
command 0x30, wait for DRQ, write 256 halfwords, wait for BSY clear. A
count of n moves n sectors with one command (wait for DRQ before each).
Byte accesses to EF00 reach only DD0–7: they work after SET FEATURES
0x01 on a card that offers 8-bit mode (optional in the CF standard). 0xEC (IDENTIFY) returns 512 bytes of
device information, word 0 = 0x848A for a CF card. Reading the status
register clears the disk interrupt (IRQ1), raised when a sector is ready
and when a write or command completes. An empty port reads status 0x00
(not ready). The other ports work the same way, 16 bytes further on each.
In the emulators -d image.img gives a disk backed by a file of raw
sectors (dd if=/dev/zero of=disk.img bs=512 count=2048 for 1 MiB); -d1,
-d2, -d3 fill the other ports.
Ports 1–3. The four ports are the same circuit (D66), so everything
above holds for each of them at its own base, EF00 + 16 p:
| Port | Registers | What is on it | P32W Unix |
|---|---|---|---|
| 0 | FFFF_EF00–EF0F |
the boot CF card, in a CF-to-IDE adapter at the front edge | /dev/disk |
| 1 | FFFF_EF10–EF1F |
a second CF card, the same way | /dev/disk1 |
| 2 | FFFF_EF20–EF2F |
IDE channel A: a 40-pin cable, a master and a slave | /dev/hda, hdb |
| 3 | FFFF_EF30–EF3F |
IDE channel B, the same | /dev/hdc, hdd |
On an IDE channel bit 4 of the drive/head register picks the drive:
0xE0 | LBA 27:24 for the master, as above, and 0xF0 | LBA 27:24 for the
slave. A port's offsets +8 to +15 are its control block: +14 is the
alternate status (read) and device control (write). Each port's INTRQ is
bit p of the card's interrupt status (EFF4), all four on the card's one
level (IRQ1 by default, vectors 0x60–0x63; §6.10). In the emulators
-d2s and -d3s give the channels' slaves.
6.5 The real-time clock (D23)¶
A DS12C887 (the PC's MC146818 registers) with its own crystal and battery.
Write the register number to FF40, then read or write it at FF41:
| Reg | Reg | ||
|---|---|---|---|
00 |
seconds | 07 |
day of the month |
02 |
minutes | 08 |
month |
04 |
hours | 09 |
year (00–99) |
06 |
day of the week (1 = Sunday) | 32 |
century |
0A |
A: bit 7 UIP (update in progress — wait while set); bits 6:4 = 010 runs the oscillator |
0B |
B: bit 7 SET (stop to set), bit 2 binary (else BCD), bit 1 24-hour |
0C |
C: interrupt flags (reading clears them) | 0D |
D: bit 7 = the battery is good |
0E–7F |
battery-backed RAM (32 is the century) |
P32W Unix sets B = 0x06 (binary, 24-hour, no interrupts) and keeps UTC
(kernel/rtc.c). The chip ships with its oscillator off: write A = 0x20
once. Its interrupt (alarm, periodic, update) is IRQ5. In the emulators the
clock follows the host's clock, or -R SECONDS (a fixed start).
6.6 The video card and PS/2 (D53, D103, D104)¶
VGA, 640 × 480, one bit a dot, drawn by an HD63484 ACRTC, and two PS/2 ports (keyboard and mouse). The card is video-card-acrtc.md; Hitachi's user's manual has the chip's registers and its 38 commands.
The picture is in the chip's own memory: none of it is on the bus. A
program sends the chip commands (clear, copy, line, rectangle, fill, a
word read or written), 16-bit words through its write FIFO, and the chip
draws. The 64 KiB at 0xFE05_0000, where the earlier revisions had the
picture, does not answer.
Registers (window 5). The chip's two are halfwords (LHU / SH);
the PS/2 section's are bytes. Any other size, or anything else in the page,
is a bus error.
| Address | Read | Write |
|---|---|---|
FFFF_DF00 |
the chip's status, in bits 7–0 (mask the rest): bit 0 WFE (the write FIFO is empty), 1 WFR (it has room), 2 RFR (the read FIFO holds a word), 3 RFF (it is full), 5 CED (the chip is idle), 6 ARD (area detect), 7 CER (command error) | the chip's address register: which register DF04 reaches (0: the FIFOs) |
FFFF_DF04 |
the register addressed, or the next word of the read FIFO | the register addressed, or a word to the write FIFO |
FFFF_DF08 |
card status: bit 0 FULL (a PS/2 byte waits), 1 SRC (0 the keyboard's, 1 the mouse's), 2 BUSY (a command not yet taken), 3 TEXT (text method A's parts are fitted), 4 the chip's IRQ | a PS/2 command: 01 interrupts on, 02 off, 03 acknowledge (no interrupt until the next byte), 04 mouse on, 05 off, 06 flush, 07 keyboard reset, 10–17 the keyboard's LEDs, FF start again |
FFFF_DF0C |
the PS/2 byte; reading it clears FULL. Read it only after FULL = 1 | — (a bus error) |
FFFF_DFF0 / DFF1 |
card ID (3), revision (2) | — |
FFFF_DFF4 |
interrupt status, a word: bit 0 the PS/2 section (FULL with its interrupts on), bit 1 the chip (its IRQ) | — |
The chip. Registers r02–rFF are words at even addresses; from r80
up the address register steps by 2 after each access, so a table loads
with one address write. A reset leaves the chip stopped with ABT set (its
command control register r02 reads 8000): the FIFOs are shut until a
program clears it. The write FIFO holds eight words; a word written when
it is full is lost, so write only while WFR is 1, or eight words after
WFE. The status register's high byte is not defined.
The chip's interrupt is whatever its command control register enables,
a bit in its low byte for each status flag: 0001 is WEE, an interrupt
when the write FIFO is empty. It is a level: it stays until the flag falls
or the enable is cleared.
Time. A command's work takes the chip from microseconds to tens of milliseconds: a character copied about 150 to 300 µs, a screen scrolled about 30 ms. CED says when it is done.
The keyboard arrives as ASCII: the card's PS/2 section turns the scan
codes into characters (Shift, Caps Lock, Ctrl-letter, Enter as CR,
Backspace 08, Delete 7F) and the arrows, Home, End, Page Up and Page
Down into VT100 sequences. The mouse's 3-byte packets come whole, with SRC
set. One queue of 63 bytes for both.
The console (the kernel's video.c) keeps 25 rows of 80 characters of
8 × 16 dots on lines 40–439. The font is a picture in the chip's memory
under the screen (from the card's font EPROM); a character is one graphic
copy from it. Seven more pictures, made by the chip at start, give bold
(for bright), underline, inverse and their mixes. The cursor is lines 14
and 15 of its cell in XOR.
At reset the chip is stopped: a black screen.
6.7 The disk card's DMA engine (D75)¶
The disk card's revision 1 (EFF1 reads 1) adds a DMA engine: given a
port, a memory address and a count, it does the drive's PIO itself — two
accesses to the port's data register for each word, as the CPU's — and
moves each word to or from memory in one bus cycle as a bus master,
while the CPU runs on. So every drive and CF card works with it. P32W Unix
does not use it yet: its driver moves the data itself.
Its registers are words (LW / SW; any other size is a bus error),
and they never wait:
| Address | Read | Write |
|---|---|---|
FFFF_EF40 DMAADR |
the physical address of the next word | the first word's physical address (bits 1:0 ignored) |
FFFF_EF44 DMACNT |
the words left (bits 15:0; 31:16 read 0) | the words to move, 0–65535 (bits 15:0) |
FFFF_EF48 DMACTL |
the bits below (31:8 read 0) | the bits below; any write clears DONE and ERR |
| DMACTL bit | Read | Write |
|---|---|---|
| 0 | BUSY: running, with words left | RUN: 1 starts; 0 stops after the word in hand |
| 1 | DIR | DIR: 0 the drive to memory, 1 memory to the drive |
| 3:2 | the port | the port, 0–3 |
| 4 | IE | IE: interrupt on DONE or ERR |
| 5 | MODE | MODE (multiword DMA): reserved, write 0 |
| 6 | DONE: the count ran out | — |
| 7 | ERR: a memory cycle no card answered; DMAADR is its address, and that word was not counted | — |
At reset DMACTL is 0 (stopped); DMAADR and DMACNT are not initialised.
A sector at a time (D75): issue the ATA command as for PIO (§6.4) and
wait for DRQ; then write DMAADR, DMACNT = 128 (one sector) and DMACTL. The
first halfword of each pair is the word's low one, so the sector lands in
memory just as cf_read's halfword loop would put it. A command for n
sectors takes n transfers, each started after its own DRQ; DMAADR has run
on past the sector, so the next one needs only DMACNT and DMACTL. A write
is command 0x30, DRQ, the engine with DIR = 1, then a wait for BSY clear,
as cf_write does.
; port 2 (IDE A) has a sector ready (DRQ): move it into buf
la r2, buf ; in RAM: physical, a multiple of 4
sw r2, 0xFFFFEF40(r0) ; DMAADR
li r2, 128 ; one sector, in words
sw r2, 0xFFFFEF44(r0) ; DMACNT
li r2, 0x09 ; RUN, DIR 0 (to memory), port 2
sw r2, 0xFFFFEF48(r0) ; DMACTL
dmaw: lw r2, 0xFFFFEF48(r0) ; (or set IE and take the interrupt)
andi r3, r2, 1 ; BUSY?
bnez r3, dmaw
andi r3, r2, 0x80 ; ERR?
bnez r3, error ; (else DMACTL reads 0x48: DONE, port 2)
The interrupt: with IE, DONE or ERR is the card's fifth source, status
bit 4 of EFF4, on the card's level (IRQ1 by default), vector
window × 16 + 4 (0x64); writing DMACTL (with 0, say) acknowledges it.
Both emulators model the engine; emulator/test/diskdma_test.asm uses
all of it.
6.8 USB (D43)¶
Two USB-A ports for storage — a thumb drive as a removable disk — each a CH376S module in sockets on the I/O card, its USB socket at the front edge. The CH376S runs USB and the drive's protocol itself; P32W sees two byte registers a port:
| Address | Read | Write |
|---|---|---|
FFFF_FF50 |
USB 0: data | data |
FFFF_FF51 |
USB 0: status — bit 7 low while an interrupt is pending, bit 4 busy | a command |
FFFF_FF54 / FF55 |
USB 1, as FF50 / FF51 |
the same |
A command is a byte written to FF51; its data, if any, follows in or
out through FF50. The chip needs 1.5 µs after a command byte and 0.6 µs
between data bytes. It reports each event — a step done, a drive plugged
in or pulled out — as an interrupt: its INT# goes low, so status bit 7
reads 0, the I/O card's interrupt status bit 6 (USB 0) or 7 (USB 1) is
set, and its level is raised (IRQ6 by default). The event is the chip's
interrupt status code
(USB_INT_SUCCESS, USB_INT_CONNECT, USB_INT_DISK_READ, …).
The chip's sector mode, in outline:
- RESET_ALL first, always: the card cannot reset the chip (the module
resets itself only at power-on), so a reboot finds it as it was left.
- CHECK_EXIST with 0x55: the data register reads back 0xAA if the
controller is there. SET_USB_MODE makes it a USB host.
- A drive plugged in (USB_INT_CONNECT): DISK_INIT (some drives need
a few tries, about 5), then DISK_CAPACITY.
- Read: DISK_READ with the LBA (4 bytes) and a sector count; each
sector then comes in eight 64-byte chunks, each announced by
USB_INT_DISK_READ, fetched with RD_USB_DATA0 and followed by
DISK_RD_GO; USB_INT_SUCCESS ends it. Write: the same, with
DISK_WRITE, WR_HOST_DATA and DISK_WR_GO.
- Every wait needs a time limit, so that a drive pulled out in the
middle of a transfer is an error, not a hang.
The command and status codes are WCH's (the CH376 datasheet and its
CH376INC.H); kernel/usb.c is a working driver. Expect about
150–250 KB/s (an estimate). P32W Unix has the drive as /dev/usb, a block
device (mkfs, mount), and /dev/rusb, its raw sectors for the FAT
tools (dos(1)). In the emulators -U usb.img plugs a drive holding
usb.img into USB 0 (-U1 into USB 1), and -Uout N pulls it out after
N sectors.
6.9 The network coprocessor (D45)¶
A Raspberry Pi Pico 2 W and a W5500 Ethernet controller (a WIZ850io module, its RJ45 at the front edge) on the I/O card do the networking — TCP, UDP, TLS, DNS — and P32W asks them for socket operations through a mailbox: a request latch it writes and a reply latch it reads, each with a full / empty flag. The protocol is net-mailbox.md (version 1).
| Address | Read | Write |
|---|---|---|
FFFF_FF52 NDATA |
the next reply byte (reading takes it) | the next request byte |
FFFF_FF53 NSTAT / NCTRL |
status: bit 0 RXF a reply byte is waiting, 1 TXE the request latch is empty, 2 ATTN the coprocessor has events, 3 IEN as written, 7 READY its firmware is running; bits 4–6 read 0 | control: bit 3 IEN (interrupt while ATTN), bit 7 RESET (restart the coprocessor, empty both latches) |
Every write to NCTRL sets IEN from its bit 3, so a RESET written with
bit 3 clear also turns the interrupt off. After a RESET, READY is 0 until
the coprocessor has started again; the machine's reset restarts it too,
and leaves IEN 0 and both latches empty. Check TXE before each byte you
write and RXF before each byte you read, and depend on those flags, never
on timing (the Pico answers within a microsecond, the emulators' model at
once):
nput: lbu r10, 0xFFFFFF53(r0) ; NSTAT
andi r10, r10, 0x02 ; TXE: the request latch empty?
beqz r10, nput
sb r2, 0xFFFFFF52(r0) ; NDATA
ret
nget: lbu r10, 0xFFFFFF53(r0)
andi r10, r10, 0x01 ; RXF: a reply byte waiting?
beqz r10, nget
lbu r1, 0xFFFFFF52(r0) ; NDATA: reading takes it
ret
Messages. P32W sends a request, then reads its one reply before it
sends the next; nothing else ever arrives in NDATA. Both begin with the
same 4-byte header:
| Byte | Request | Reply |
|---|---|---|
| 0 | the command | the status: 0 OK, 1 AGAIN, … |
| 1 | the socket, 0–7 (0 if none) | the socket (the new one, for SOCKET and ACCEPT) |
| 2, 3 | the payload's length, low byte first | the same |
| 4 … | the payload, at most 1024 bytes | the same |
Addresses and ports inside payloads are in network order (big-endian).
The commands: 01 HELLO, 02 IFINFO, 03 IFCONFIG, 04 EVENTS,
10 SOCKET (TCP, UDP, TLS, a DNS lookup or ICMP), 11 CONNECT, 12
BIND, 13 LISTEN, 14 ACCEPT, 15 SEND, 16 RECV, 17
SENDTO, 18 RECVFROM, 19 SHUTDOWN, 1A CLOSE, 1B STATE;
their payloads, the socket states and the status codes are in
net-mailbox.md. HELLO is 01 00 00 00; it closes every socket, and its
reply is 00 00 08 00 and the text P32NET 1.
Events. CONNECT only starts a connection. The coprocessor sets ATTN
whenever something a program may be waiting for changes on a socket:
connected or failed, a DNS answer, data in, room to send, a connection to
accept, the peer closing. EVENTS replies with a bit per changed socket
and clears ATTN. With IEN set, ATTN interrupts: the I/O card's status
bit 8, IRQ7 by default, vector 0x78; without, poll ATTN.
Starting: wait for READY, then send HELLO. A driver that loses its
place writes RESET, waits for READY and starts again with HELLO. Without
the I/O card these addresses are a bus error; with it but no running
coprocessor, READY is 0. In the emulators -N puts the coprocessor on
the Mac's network (P32W is 10.0.2.15, the Mac 10.0.2.2; listening ports
below 1024 move up by 8000, which -Nport N changes; -Nca FILE gives
TLS other certificates to trust); without -N, READY is 0.
6.10 Interrupt sources¶
Every source of an interrupt, with its default level and its vector at the default windows (the I/O card 7, the disk card 6, the video card 5):
| Vector | Source | Status bit | Level (jumpers) | The handler clears it by |
|---|---|---|---|---|
0x70 |
timer | I/O 0 | 0 (L0_*) |
writing TCTRL with bit 1 set (§6.2) |
0x71 |
UART 0 | I/O 1 | 2 (L1_*) |
reading RBR or IIR (§6.1) |
0x72 |
UART 1 | I/O 2 | 3 (L2_*) |
the same |
0x73 |
UART 2 | I/O 3 | 2 (L3_*) |
the same |
0x74 |
UART 3 | I/O 4 | 3 (L4_*) |
the same |
0x75 |
real-time clock | I/O 5 | 5 (L5_*) |
reading register C (§6.5); the emulators do not model this interrupt |
0x76 |
USB 0 | I/O 6 | 6 (L6_*) |
the CH376S's own protocol (§6.8) |
0x77 |
USB 1 | I/O 7 | 6 (L7_*) |
the same |
0x78 |
network coprocessor | I/O 8 | 7 (L8_*) |
EVENTS, which clears ATTN, or writing IEN = 0 (§6.9) |
0x60–0x63 |
disk ports 0–3 | disk 0–3 | 1 (L2 L1 L0) |
reading the port's status register (§6.4) |
0x64 |
the disk card's DMA engine | disk 4 | 1 (the same) | writing DMACTL (§6.7) |
0x50 |
PS/2 keyboard and mouse | video 0 | 4 (its own three jumpers) | reading DF0C, which takes the byte, until FULL is 0 (§6.6) |
0x51 |
the video chip (the HD63484's IRQ) | video 1 | 4 (its own three) | making the flag fall, or clearing its enable in the chip's r02 (§6.6) |
The jumpers are at each card's edge, labelled: a fitted jumper is 0, an
open one 1, and a level is three jumpers read as a binary number. On the
I/O card every device has its own three, Ld_2 Ld_1 Ld_0 for status
bit d: USB 0's level 6 is L6_2 and L6_1 open, L6_0 fitted. The disk
card has one set, L2 L1 L0, for all its sources (level 1: L0 open). The
video card has a set for each of its two (both level 4: the high jumper
open). The window is
WIN2 WIN1 WIN0, read the same way (7: all open; 6: WIN0 fitted; 5:
WIN1 fitted).
The vector. When the CPU takes line n, the card with a source asking
on level n answers the acknowledge cycle (D57) with its window × 16 + the
lowest asking status bit among its sources on that level, and CAUSE =
0x8000_0000 | vector << 8 | n (§7), so a handler can dispatch on CAUSE
bits 15:8 alone. Sources that share a level are served lowest bit first:
with UART 0 and UART 2 both asking the vector is 0x71, and once UART 0's
request is cleared the line, still held by UART 2, traps again with
0x73. A vector of 0xFF means no card answered: read the cards' status
words (+FF4) instead. Two cards must never have sources on the same
level — both would answer; one card may use several (the I/O card uses
six). A status word shows its card's requests whatever their levels.
6.11 The debug card (D64)¶
A bus tester, not a device: the host — the Mac, over USB through the card's Pico 2 — drives it, and no program does. It sits in a slot of its own, slot 0, and works in one of two modes, chosen by a switch at its front edge, never both:
- LISTEN: a logic analyzer on every signal of the bus and of the CPU's local link, one sample a bus cycle (or a faster timing view). Its bus drivers are held off by wiring, so it cannot disturb a running program.
- DRIVE: it is a bus master. With the CPU and MMU / data cards in, it
borrows the bus by DMA (
-BREQ0, the arbiter's highest priority) one cycle at a time, between the CPU's cycles, and reads and writes memory and device registers with the CPU halted or running: the host loads programs this way. So memory can change under a running program. (It cannot write the boot ROM: the memory card ties the flash chips' write line high, and they are programmed in a chip programmer, D101.) In DRIVE the host can also pull any interrupt line low. With those two cards pulled it makes the clock and reset itself, to test a card alone; no program runs then. It cannot do that against a card that is in: each of the two ties a line to ground that turns the debug card's drivers for its lines off (D102).
In either mode the host can also clock the machine (D76): the CPU card then runs from the debug card's 12 MHz (a 1.5 MHz machine) or a phase at a time, one per STEP command. The timer counts bus clocks (§6.2), so time slows with the machine; the devices with clocks of their own (the UARTs' baud rate, the real-time clock, the USB modules, the coprocessor, the drives, the video card) keep real time, and a UART receiving at full speed overruns.
7. Exceptions and interrupts¶
One handler. Every exception and interrupt enters supervisor mode at the
address in VBR:
1. EPC ← the trapping instruction (an interrupt: the next instruction);
2. CAUSE ← the code, BADADDR ← the faulting address (or 0);
3. PS ← S, PIE ← IE; S ← 1, IE ← 0 (and r15 becomes the supervisor SP);
4. PC ← VBR.
Entry writes no memory and no register, so it cannot fault. The trapping
instruction has had no effect — return with RTE to re-execute it (after
fixing the cause, e.g. a TLB refill), or add 4 to EPC first to skip it
(SYSCALL and BREAK leave EPC pointing at themselves).
| Code | Cause | BADADDR |
|---|---|---|
| 1 | ILLEGAL — undefined opcode or non-zero reserved bits | 0 |
| 2 | PRIVILEGE — privileged instruction in user mode, or a user access to ≥ 0x8000_0000 |
0 / the address |
| 3 | MISALIGNED — a load/store or jump not aligned | the address |
| 4 | BUSERROR — nothing answered (or wrong access size in the I/O page, or a ROM write) | the physical address |
| 5 | SYSCALL | 0 |
| 6 | BREAK | 0 |
| 8 / 9 / 10 | TLB miss on fetch / load / store | the virtual address |
| 11 | PROTECT — store to a read-only page, fetch from a non-executable one | the virtual address |
0x8000_0000 | v << 8 | n |
interrupt on line n; v = the card's vector (its window × 16 + the device's interrupt status bit; 0xFF: none answered, D57) |
0 |
Interrupts (8 levels, set by jumpers; the defaults: 0 timer, 1 disk,
2 UARTs 0 and 2, 3 UARTs 1 and 3, 4 PS/2, 5 real-time clock, 6 USB,
7 network; every source and its vector: §6.10) are level-triggered and taken between
instructions when STATUS.IE = 1 and IPEND & IMASK ≠ 0, lowest line
first. The handler must clear the device's request, or it will trap again
at once. Interrupt shadow: after an MTCR (e.g. setting IE) or an
RTE, one more instruction runs before a pending interrupt is taken — so
RTE always lets the interrupted program make progress.
A handler skeleton:
la r2, handler
mtcr VBR, r2
handler: ; supervisor SP in effect
push r10
push r11
mfcr r10, CAUSE
blt r10, r0, irq ; bit 31 set: an interrupt
... ; an exception: CAUSE, BADADDR, EPC
irq: andi r10, r10, 7 ; the line
... ; acknowledge the device
pop r11
pop r10
rte
A handler that may itself trap, or that re-enables interrupts, must first
save EPC and STATUS (they are overwritten by the next trap). WAIT
idles until an enabled interrupt line is asserted — the kernel's idle loop.
8. User mode and virtual memory¶
User programs run with STATUS.S = 0: privileged instructions and the I/O
page trap, and every address is translated. User addresses
0x0000_0000–0x7FFF_FFFF go through the TLB; anything above traps
(PRIVILEGE). Supervisor addresses are physical.
Pages are 4 KiB. The TLB is split: 64 entries for instruction fetches
(I-TLB), 64 for loads and stores (D-TLB), each direct-mapped — the entry
for virtual page number VPN is entry VPN mod 64 — and tagged with an
ASID, so switching processes is just MTCR ASID (no flush).
The TLB is managed by software. On a miss the hardware has already put the
faulting VPN, the side (I) and the current ASID into TLBHI; the handler
looks the page up in its own page table, writes TLBLO, executes TLBW, and
returns with RTE to retry the access:
tlbmiss: ; CAUSE 8, 9 or 10
mfcr r10, TLBHI ; VPN | I | ASID, loaded by the hardware
srli r10, r10, 12 ; the VPN
slli r10, r10, 2 ; index into a linear table of TLBLO words
la r11, pagetable ; (this process's table)
add r10, r10, r11
lw r10, 0(r10) ; PFN | X W V
beqz r10, segfault ; not mapped: the process dies
mtcr TLBLO, r10
tlbw ; write the entry TLBHI selects
rte ; retry the instruction
TLBLO = PFN << 12 | X << 2 | W << 1 | V. A store needs W, a fetch X; V = 0
entries never match. See emulator/test/vmkernel.inc for a complete kernel
(processes in separate address spaces, preemption by the timer, system
calls that check user pointers).
9. Tools¶
| Tool | |
|---|---|
assembler/p32asm.py src.asm -o img.bin -l listing.lst |
two-pass assembler; flat ROM image at 0 |
emulator/p32emu [-t] [-d disk] [-a aux] [-p n] img.bin |
instruction-level emulator (the reference) |
emulator/p32uemu ... |
microcode-level emulator: runs the real control store, cycle by cycle |
make -C emulator test |
every test: both emulators must agree instruction by instruction |
The console is the emulators' stdin/stdout; UART 1 goes to files (-a, -A);
HALT ends the run with exit status r1 & 0xFF. With VBR = 0 an exception
stops the emulators with a register dump (on the hardware it would jump to
0). -t traces every instruction, trap and interrupt.