The P32W emulators: user guide and theory of operation¶
P32W has two emulators, both in emulator/:
p32emu |
p32uemu |
|
|---|---|---|
| Source | emulator/p32emu.c (D7) |
emulator/p32uemu.c (D14) |
| Level | instruction: decodes each instruction and performs its effect in C | microcycle: runs the real control store, one microword per cycle, over a model of the CPU card and the MMU / data card |
| Knows the instruction set? | yes — it is the definition (the golden model) | no — every instruction's behaviour comes from the microcode |
| Counts | instructions | instructions and microcycles |
| Speed on an M3 Pro (measured below) | ~125 M instructions/s on plain code | ~70 M microcycles/s ≈ 22 M instructions/s |
Both include the same devices, emulator/p32io.h (D50): the I/O card
(four UARTs, the timer, the input and output ports, the real-time clock
(D23), two USB ports (D43) and the network coprocessor (D45)), the disk card
(two CompactFlash sockets and two IDE channels) and the video card (its
HD63484 chip, modelled in emulator/p32acrtc.h, and its PS/2 section). The memory cards (-M) are modelled in
each emulator. The two programs run the same images and take the same
device options, and make test checks that they agree at every
instruction.
This document has two parts. Part 1 covers using the emulators: building
them, the options, interactive use, disks, bare-metal programs, debugging and
scripted tests. Part 2 explains how each emulator works inside and how
the two are kept in agreement. Everything here was checked against the code
(p32emu.c, p32uemu.c, p32io.h and test/run_tests.sh as of
2026-10-24). The examples were run on an Apple M3 Pro, on scratch copies of
the images, on 2026-10-14, and the p32uemu runs and the measurements on
2026-10-24; instruction and cycle counts in them can differ slightly from
today's builds.
Part 1 — User guide¶
1. Which emulator, when¶
Use p32emu by default. It is the reference: it is fast, it has an
instruction trace (-t), and when it reports an error it shows the
instruction that caused it. Use it for running P32W Unix, for developing
software, and for tests.
Use p32uemu when the question is about the hardware. It runs the 53-bit
microwords that will go into the seven control-store flash chips. It reports
how many microcycles a program takes (at 3 MHz that is the real machine's
run time, D17). It can model the I/O card's timer cycle for cycle (-C),
bus wait states (-W) and a DMA master competing for the bus (-D), and
it shows whether the microcode — not the C code — implements the ISA. A
program that works on p32emu but not on p32uemu points to a microcode
bug, or to a gap in the model of the CPU card or the MMU / data card.
Measured speed¶
All runs used the same images on an Apple M3 Pro, with both emulators built
-O2 by Apple clang 21. The C loop and the boot were each run two or three
times and varied by under 3%; session1 ran once.
| Workload | Instructions | p32emu |
p32uemu |
Microcycles | p32uemu slower by |
|---|---|---|---|---|---|
Stand-alone C loop (--sa, an array sum, no I/O) |
107,666,722 | 0.87 s = 124 M instr/s | 4.80 s = 68 M cycles/s (22.4 M instr/s) | 328,143,482 (3.05 per instruction) | 5.5× |
Boot P32W Unix, log in as root, halt (-w script) |
7,490,575 | 0.33 s (23 M instr/s) | 0.65 s (41 M cycles/s, 11.5 M instr/s) | 26,738,361 (3.57 per instruction) | 2.0× |
software/tests/session1 (boot, log in, 30 shell commands, halt; -w) |
25,586,345 | 1.30 s (20 M instr/s) | 2.60 s (35 M cycles/s, 9.8 M instr/s) | 91,699,408 (3.58 per instruction) | 2.0× |
On plain computation p32uemu is about 5.5 times slower than p32emu.
Under P32W Unix the gap is only 2 times, and the reason is in p32emu:
with interrupts enabled, both emulators ask the I/O card for pending lines
once per instruction (io_ipend()). Under -w that call runs the prompt
matcher on every UART poll. A sample profile of p32emu running
session1 shows prompted(), memcmp and strlen as the top entries. So
scripted P32W Unix sessions measure the I/O model more than the CPU model.
For comparison, the real machine runs a 3 MHz microcycle (D17). At that rate the boot-and-halt run's 26.7 M microcycles take about 9 s, session1 about 31 s, and the C loop about 109 s.
2. Building¶
make -C emulator # both emulators (and the generated files they need)
The emulator/Makefile does three things:
| Target | Made by | From |
|---|---|---|
p32opc.h — opcodes, formats, must-be-zero masks, the privileged set, control-register numbers and names, exception causes and names |
generators/gen_p32opc.py |
microcode/p32isa.py (the ISA's single source of truth) |
p32uc.h — the microword's width (UC_BITS) and field layout (UF_*), the codes of every field (ASRC_*, BSRC_*, RSRC_*, RDST_*, RSEL_*, ALU_*, IMM_*, MEM_*, SEQ_*, COND_*, MODE_*), internal-sequence numbers, the physical register-file addresses (PR_*), the control-register decode table |
microcode/p32ucode.py --header |
p32isa.py + the generator |
microcode/p32ucode.bin — the control store, 16,384 words × 8 bytes (little-endian; 53 bits used). The same run writes u0.bin–u6.bin (one 16 KiB image per flash chip, byte n of each word) and dec_op.bin / dec_cr.bin (the CPU card's decode flash) |
microcode/p32ucode.py -o ../microcode |
the same |
p32emu |
cc -O2 -std=c99 p32emu.c |
p32emu.c, p32opc.h, p32io.h |
p32uemu |
cc -O2 -std=c99 p32uemu.c |
p32uemu.c, p32opc.h, p32uc.h, p32io.h |
Never edit p32opc.h or p32uc.h. Change microcode/p32isa.py (the
ISA) or microcode/p32ucode.py (the microcode) and rebuild. The microcode
generator refuses to write images that break its static checks (§13.5).
python3 microcode/p32ucode.py --stats prints the cycle count of every
instruction.
make -C emulator test runs the whole regression suite (§9).
make -C emulator clean removes the emulators, the generated headers, the
microcode images and the test outputs.
3. Running: the command line¶
p32emu [-t] [-m] [-k FILE] [-M N] [-s FILE] [-l N] [device options] image.bin
p32uemu [-u UCODE] [-s FILE] [-l N] [-c | -C] [-G [-q FILE]] [-W N] [-D ADDR:PERIOD] [-M N] [device options] image.bin
image.bin is a boot ROM image. It is loaded at physical address 0
(1 MiB at most) and the CPU starts there in supervisor mode, with interrupts
off and VBR = 0. Options and the image can appear in any order. Numbers
for -l, -p and -R are read with C's base-0 rules: 0x1F is hex, 010
is octal, and anything else is decimal.
Options of both emulators¶
| Option | Meaning |
|---|---|
-s FILE |
Write the architectural state at the start of every instruction to FILE, one line each, in the same format for both emulators (§8.2). This is the basis of the cross-check. |
-l N |
Stop after N instructions with a register dump and status 255. Default: 100,000,000. When the console is an interactive terminal (stdin is a tty and -w is not given) the default is no limit, so a session runs until halt or ^]. -l 0 means no limit. Instructions are counted; trap and interrupt entry are not. |
-M N |
Memory cards fitted: 1 (default) to 4, 16 MiB each, in the memory windows 0 … N−1 (D52). RAM runs from 1 MiB up to N × 16 MiB; an address above it gets no acknowledge, a BUSERROR (the kernel's memory probe relies on this). -M 16,0,6: each window's MiB instead — a card with only some of its 2 MiB banks fitted (D60), 0 for no card; window 0 needs one. An address in a bank that is not fitted is a BUSERROR, so memory may have holes. |
-d IMG |
The disk card's first CompactFlash socket (port 0): a raw image of 512-byte sectors, opened read/write and written in place (§6). The file must exist; its size sets the card's capacity (whole sectors). Without -d the socket is empty. |
-d1 IMG |
The second CompactFlash socket (port 1). |
-d2 IMG, -d2s IMG |
IDE channel A (port 2): the master and the slave drive. |
-d3 IMG, -d3s IMG |
IDE channel B (port 3): the master and the slave drive. |
-a OUT |
UART 1's output goes to the file OUT. -a2, -a3: the same for UARTs 2 and 3. |
-A IN |
UART 1's input comes from the file IN. -A2, -A3: UARTs 2 and 3. |
-T PORT |
UART 1 is a TCP server on localhost:PORT (loopback only, one connection at a time): a second terminal (§4.3). -T2, -T3: UARTs 2 and 3. |
-p N |
The value the input port (FFFF_FF20) reads (8 bits; default 0). |
-U IMG |
A USB drive in the I/O card's first USB port (CH376S, D43), holding IMG (read/write). -Uout N pulls it out after N sectors have moved. |
-U1 IMG |
A USB drive in the second USB port. |
-N |
The network coprocessor (D45), carried out with the Mac's sockets (10.0.2.2 is the Mac; net-mailbox.md). -Nport N moves listening ports below 1024 up by N (default 8000); -Nca FILE names the certificates TLS trusts. Without -N the coprocessor is present but not running. |
-V FILE |
Fits the video card and writes its screen at exit: the picture's 640 × 480 dots to FILE.pbm (once the chip is started), and to FILE the console's 25 rows of 80 characters, read from the dots: each 8 × 16 cell on lines 40–439 is matched against the font's glyphs, plain, bold, underlined or inverse, with the cursor or without (? where nothing matches). |
-K FILE |
Fits the video card and has its PS/2 keyboard type FILE's text. The keys go as set-2 scan codes into the model of the card's PS/2 section, which turns them into ASCII as the ATmega328P's firmware does. What is typed waits until the section's queue (63 bytes) has room, so a long file loses nothing. |
-Km B0,B1,B2 |
Fits the video card; its PS/2 mouse sends this 3-byte packet (hex), after the keys. Without -V, -K, -Km or -dbg there is no video card: its window does not answer. |
-w |
Scripted input: each input line is held back until the output ends with a prompt (§10.1). This also turns off raw/interactive mode. |
-P STR |
Adds a prompt for -w. The first -P replaces the default list. At most 12 prompts. |
-R SECS |
The real-time clock starts at SECS (seconds since 1970, UTC) and then advances with emulated time. Without it, the clock starts at the host's current time (§4.6). |
p32emu only¶
| Option | Meaning |
|---|---|
-t |
Trace every instruction to stderr (address, mode S/U, the instruction word and its disassembly), every trap and interrupt, every TLBW, and every write to the output port. It also flushes UART output after every byte, so program output and trace lines appear in order. |
-m |
TLB statistics at exit, on stderr: for the I-TLB and the D-TLB, the user-mode accesses, the misses (cold: the slot was empty; conflict: another page held it), the miss rate, protection faults, TLBWs and valid entries replaced. Then, for the same accesses, the misses of four "what if" TLBs that assume every miss is refilled: 128 entries direct-mapped, 64 entries 2-way, 128 entries 2-way (LRU), and 64 entries indexed by VPN ^ VPN >> 6. In scripts: EMUFLAGS=-m software/tests/run_session.sh NAME prints the report after the transcript. |
-k FILE |
A kernel profile at exit, to FILE: how many instructions ran at each supervisor-mode address in the kernel's MiB (0010_0000–001F_FFFF), plus totals for user mode and for other supervisor code (the ROM). tools/kprof.py totals it by function. |
p32uemu only¶
| Option | Meaning |
|---|---|
-u FILE |
The control-store image. Default ../microcode/p32ucode.bin, relative to the current directory, which is right only when you run from emulator/. From the repository root, pass -u microcode/p32ucode.bin. |
-c |
The timer ticks once per microcycle, instead of once per instruction. |
-C |
The I/O card's timer exactly as the hardware does it: a 1 MHz tick every third 3 MHz microcycle (with -F, every fourth 4 MHz one: the CPU card's clock jumper, D74), reload one cycle after reaching 0, and WAIT holds its microcycle instead of skipping time (§13.6); and the disk card's two wait states on every port access (D66), the video card's three on every access to its chip and one on a PS/2 command written (D104, D106). The gate-level simulation of the device cards compares against this. If -C is given, -c has no effect. |
-G |
The measuring gate (implies -C; §4.7, D85): while bit 7 of the output port is up it counts the machine's cycles, and reports when the bit falls. |
-q FILE |
With -G, a profile of what runs while the gate is up, written at exit: the cycles at each address, in user mode and in the kernel (§4.7, D88, D92). |
-W N |
N wait states on every bus cycle (a fetch, load or store): the bus's -WAIT (D50). The sequencer holds the microcycle, so they show only in the cycle count and in timer ticks (with -c or -C). |
-D ADDR:PERIOD |
A DMA master on the bus, to test the bus lock (D54): every PERIOD microcycles it asks for the bus; the arbiter grants it at the next microcycle boundary unless -LOCK is held, and it adds 1 to the RAM word at physical ADDR (a read and a write: two bus clocks, which a CPU bus cycle in that microcycle waits for). At HALT it reports how many it added and the word's value (§13.5). -Dnolock ADDR:PERIOD: the arbiter ignores -LOCK, so a SWAP can lose an addition. |
p32uemu has no -t. Both emulators reject an unknown option, or a second
image name, by printing the usage message and exiting with status 255.
Exit status and the final line¶
| How the run ends | stderr | Exit status |
|---|---|---|
HALT |
HALT at PC, r1=XXXXXXXX (N instructions[, M idle ticks]) (p32emu) / (N instructions, M cycles) (p32uemu) |
r1 & 0xFF |
An exception or interrupt while VBR = 0 (no handler) |
EXCEPTION: … and a register dump |
255 |
| The instruction limit | STOP: instruction limit … and a dump |
255 |
WAIT that can never wake up |
STOP: WAIT with no possible wake-up and a dump |
255 |
| ^] typed on the console (interactive) | [p32emu: ^] -- stopped] |
0 |
Usage error, missing or oversized image, a file that cannot be opened, -T cannot bind, a control store of the wrong size |
a message | 255 |
p32uemu only: a microword with an undefined field code |
microcode: undefined … and a dump |
255 |
HALT is privileged, so a user-mode program cannot stop the emulator; it
gets a PRIVILEGE trap instead. In p32emu, "idle ticks" are timer ticks
that WAIT skipped (§12.6), and they are printed only when there were any.
p32uemu's HALT line does not report idle ticks.
$ ./p32emu hello.bin; echo "status=$?"
P32W lives! instruction-level reference emulator
HALT at 00000014, r1=00000000 (254 instructions)
status=0
$ ./p32uemu -u p32ucode.bin hello.bin; echo "status=$?"
P32W lives! instruction-level reference emulator
HALT at 00000014, r1=00000000 (254 instructions, 868 cycles)
status=0
4. Interactive use¶
4.1 The console¶
UART 0 is the console, connected to the emulator's stdin and stdout. When
stdin is a terminal and -w is not given, the emulator starts in
interactive mode and prints this to stderr:
[p32emu: interactive; ^] stops the emulator]
(p32uemu prints the same text, with "p32emu" in it, because the message
comes from the shared p32io.h.) In interactive mode:
- The Mac terminal is put in raw mode: no echo, no line editing, no
signals, no CR→NL translation, no XON/XOFF. Every key goes straight to
UART 0, so ^C, ^Z, the arrow keys and
vireach P32W, and P32W does its own echo. Return arrives as CR. - stdout is unbuffered.
- ^] (Ctrl-]) stops the emulator at once, with status 0. The terminal is restored on exit. ^] is checked only on the console; on the TCP terminal it is passed to P32W.
- There is no instruction limit unless you give
-l. - Idle time is real time (§4.6).
The terminal is read on every 256th poll of the UART (a few hundred instructions at most), so the delay is not noticeable.
4.2 The boot monitor and booting P32W Unix¶
The ROM image you normally run is the boot monitor,
software/monitor/monitor.asm. It sets VBR to its own exception handler,
sets UART 0 to 9600 baud (the emulator ignores the divisor), and prompts
with >. Commands, with numbers in hex:
| Command | Effect |
|---|---|
d ADDR [LEN] |
dump memory as words (16 bytes by default) |
m ADDR VALUE |
store a word |
g ADDR |
call ADDR; the program may RET to the monitor, which then shows r1 |
l LBA COUNT ADDR |
load COUNT sectors from the CompactFlash card to ADDR |
b |
boot from the card: sector 0 is a header ("P32B", sector count, load address, entry point); sectors 1.. are loaded and the entry point is called |
? |
help |
q |
HALT with r1 = 0 (the emulator exits with status 0) |
If a command causes an exception (a bad address, for example), the monitor
prints CAUSE, EPC and BADADDR and goes back to the prompt. Its line buffer
and stack are at 0x001F_FC00, the top of the first memory card's first
2 MiB bank (every machine has it, D60).
To boot P32W Unix (see also unix.md):
make -C emulator
make -C software disk # software/build/disk.img
python3 assembler/p32asm.py software/monitor/monitor.asm -o monitor.bin
emulator/p32emu -d software/build/disk.img monitor.bin
Type b at >. The kernel loads and the system goes multi-user. Log in on
the console as root, ken or guest (no password); the passwords are
listed in unix.md, "Logging in".
The same session, scripted (not interactive, so the transcript can be shown; the image is a scratch copy):
$ printf '?\nl 0 1 100000\nd 100000 10\nq\n' | ./p32emu -d m.img monitor.bin | tr -d '\r'
HALT at 000000BC, r1=00000000 (5406 instructions)
P32W monitor -- ? for help
> ?
d ADDR [LEN] dump memory
m ADDR VALUE store a word
g ADDR call a program
l LBA COUNT ADDR load sectors from the disk
b boot from the disk
q halt
> l 0 1 100000
loaded 00000001 sectors at 00100000
> d 100000 10
00100000: 42323350 00000083 00100000 00100000
> q
The dump shows the boot header: the magic "P32B" (0x42323350), 0x83
kernel sectors, load address 0x0010_0000, entry 0x0010_0000. The HALT
line comes first only because stdout went through tr while stderr went
straight to the terminal.
Halting cleanly. As root, use halt, reboot or shutdown [-r]. These
write every buffer to the disk, mark the filesystem clean, and then execute
HALT, so the emulator exits with status 0. reboot and shutdown -r
restart into the monitor instead, and you type b again. ^] is pulling
the plug. The disk image keeps whatever the kernel had already written,
the superblock's clean flag stays 0, and the next boot reports that the
filesystem was not unmounted cleanly. For example, after a boot that was
stopped with ^] at the login: prompt:
superblock word 6 ("clean"), read with a few lines of Python at sector 2048:
disk.img (the original) clean=1
a.img (a run ended by halt) clean=1
t.img (a run ended by ^]) clean=0
4.3 The second terminal (-T)¶
-T PORT makes UART 1 a TCP server on localhost:PORT. The emulator
prints [UART 1: nc localhost PORT] and then [UART 1: connected] /
[UART 1: disconnected] as clients come and go. Only one client can be
connected at a time. When it hangs up, the port waits for the next one.
P32W Unix runs a getty on /dev/tty1, so this gives a second user:
emulator/p32emu -T 2323 -d software/build/disk.img monitor.bin
python3 tools/p32term.py # in another window; default port 2323
tools/p32term.py [port]puts the Mac terminal in raw mode and sends every key at once, as the console does, sovi,top'sq,moreand ^C all work. ^] leaves p32term; the emulator keeps running. If the emulator is not running yet, p32term retries every 2 s.nc localhost 2323also works, but the Mac's line discipline holds each line until Return, and ^C endsncinstead of reaching P32W.- Output sent while nobody is connected is lost. It is not buffered.
If you connect after the system has booted, the
login:prompt has already gone. Press Return andgettyprints it again:
tty1 before Return: b''
tty1 after Return: b'\r\n\r\nP32W Unix (tty1)\r\n\r\nlogin: '
Alternatively, start p32term.py first, since it waits for the emulator.
- The TCP terminal is also the path for Kermit file transfer:
C-Kermit's set host /raw-socket localhost PORT, or
tools/pykermit.py --tcp PORT send|get|dir|finish (unix.md, "Moving
files with Kermit").
A two-user run, driven through a pseudo-terminal (console) and a socket (tty1), with ken logged in on tty1 and root on the console:
# who
root tty0 Oct 14 17:19
ken tty1 Oct 14 17:19
# sleep 2; date
Wed Oct 14 17:19:25 2026
# halt
P32W Unix: halted
HALT at 0010025C, r1=00000000 (13441216 instructions, 17115522 idle ticks)
sleep 2 took 2.6 s of wall-clock time: idle time was real time (§4.6).
The 17 million idle ticks are the emulated microseconds the CPU spent in
WAIT.
4.4 Files as the second UART (-A, -a)¶
Without -T, UART 1 can read from a file (-A IN) and write to one
(-a OUT). A file input is "typed" as fast as P32W reads it (or, with -w,
a line per prompt, using UART 1's own output to detect the prompts). At end
of file, UART 1 simply never has data again. The two-user session test
uses -A tests/session2.tty1 -a tty1.out.
4.5 What happens to the disk image¶
-d IMG opens the image read/write. Every sector the program writes goes
to the file at once (fwrite and fflush per sector), so the image
always holds what the emulated card holds. There is no copy-on-write and no
undo. Keep a copy if you need the old state.
Never point two emulators at the same image, and never change an image
with p32fs.py while an emulator is using it. Neither the emulators nor
p32fs.py lock the file. The kernel caches blocks and the superblock in
memory, so the two writers would overwrite each other and corrupt the
filesystem. The test scripts always build or copy their own image
(run_session.sh builds one in $TMPDIR; make -C software testdisk
makes build/test.img). Use a copy in the same way for experiments.
software/build/disk.img is the image Ken boots.
4.6 Time: the timer, idle waiting and the real-time clock¶
The timer counts one tick per instruction (§12.5). P32W Unix programs it
for 50 Hz: TRELOAD = 20,000 ticks (kernel/param.h: HZ 50, TICK
20000). So in the emulators one instruction counts as one microsecond of
emulated time. The real machine runs P32W Unix at about 0.94 MIPS (3.18 microcycles per
instruction at 3 MHz, §13.2), so emulated time runs a little fast.
Idle. When the kernel has nothing to run it executes WAIT. The
emulator handles this in one of two ways:
- Interactive (the console is a terminal, or
-Tis in use): it sleeps inselect()on the terminal (and the TCP socket) for as many microseconds as the timer has left, and wakes early if a key arrives. Idle emulated time therefore passes at real speed, andsleep 10takes ten seconds. - Not interactive (piped or scripted input, no
-T): it jumps straight to the timer's expiry. Asleep 60in a scripted session costs almost no host time. This is what keeps the tests fast and repeatable.
p32uemu -C does neither: it holds the WAIT microcycle until a line comes
up, simulating every cycle at full host speed.
The real-time clock (D23, a DS12C887 model at FFFF_FF40/41):
- By default it reads the host's clock (UTC) at the moment P32W first reads it, and from then on advances with emulated time: instructions plus idle ticks, one microsecond each. During interactive idle that matches real time. During heavy computation the emulator runs faster than 1 MIPS, so P32W's clock gains on the Mac's. P32W Unix reads the clock only at boot and counts its own ticks after that, so the system time behaves the same way.
-R SECSstarts it at a fixed time (-R 1791936000= 2026-10-14 00:00 UTC, as all the tests use). Together with non-interactive input, this makes every run exactly repeatable.- Setting the clock (
date 2610141230as root) changes it for the rest of this run only. Nothing is saved, and the 114 bytes of battery-backed RAM are also lost when the emulator exits. - Under
p32uemu -Cthe clock does not advance at all (§15).
4.7 Measuring a run: p32uemu -G and cycles¶
To find how long something takes on the real machine, boot under p32uemu
-G and run it through cycles (D85). Never point this at disk.img while
you need what is on it: a build writes to the disk. Use a copy.
$ cp software/build/disk.img m.img
$ emulator/p32uemu -u microcode/p32ucode.bin -G -d m.img monitor.bin
...
# cycles make
cycles command raises bit 7 of the output port, runs the command, lowers
the bit, and prints the command's real, user and system time. For several
steps use cycles sh -c '...', or cycles start, the steps and cycles
stop. With -G the emulator counts while the bit is up, and when it falls
it prints on its standard error:
[cycles #1: 32,107,476 cycles, 0:00:10.7 at 3 MHz (10.702 s)
9,772,072 instructions, 0.91 MIPS busy; idle 0 cycles (0.0%)
disk 152 sectors read, 0 written, 18 commands; console 6,143 bytes]
MIPS busy leaves the idle cycles out. A count still open when the machine
stops is reported then. The counter is 64 bits: 32 would wrap every 23.9
minutes at 3 MHz, and 64 last 195,000 years. Under -G the emulator does
about 30 million cycles a second on P32W Unix on an M3 Pro, some 10 times
the real machine's speed.
What it covers. The processor's work exactly: the count is p32uemu -C's,
which the gate-level simulations match cycle for cycle, wait states included.
It counts the whole machine, not one process, so anything else running is in
the total. Devices take no time (§15): to the count add the drive's access
time for each disk command, and the console's bytes at its baud rate.
Where the time goes. Add -q FILE and the emulator also keeps the cycles
at each address while the gate is up, in user mode (D88) and in the kernel
(D92). tools/uprof.py FILE MAP
totals them by function, the static ones too, from the program's link map
(p32ld -M) and the objects it names, run from where the map's paths start:
$ cd software
$ python3 ../assembler/p32ld.py build/crt0.o build/cc/*.o build/libc.a -M cc.map -o /tmp/cc.x
$ python3 ../tools/uprof.py prof cc.map
Addresses are a process's, so profile one program at a time: cycles cc -S
x.c is cc alone, but cycles cc -c x.c runs as too, and the two would
add up at the same addresses.
The kernel has one set of addresses, so -k totals a whole job, every process
in it, in cycles (p32emu -k and kprof.py count instructions, from boot):
$ cd kernel
$ python3 ../tools/uprof.py prof unix.map -k
It is for the emulator only, not a hardware feature. cycles tells the
emulator when to count by writing the output port, so on the real machine it
would only set that port and clear it. There, use time.
5. P32W Unix in scripts and on the second emulator¶
Everything in §4 works the same with p32uemu (add
-u microcode/p32ucode.bin when running from the repository root). It
boots P32W Unix, runs the shell, and writes the same disk image as p32emu
does. Two scratch copies of the image, booted with the same script and the
same -R, came out byte-identical after halt:
$ ./p32emu -w -R 1791936000 -l 0 -d a.img monitor.bin < boot.in > a.out
HALT at 00100270, r1=00000000 (7490575 instructions)
$ ./p32uemu -u p32ucode.bin -w -R 1791936000 -l 0 -d b.img monitor.bin < boot.in > b.out
HALT at 00100270, r1=00000000 (7490575 instructions, 26738361 cycles)
$ cmp a.out b.out && cmp a.img b.img && echo identical
identical
(boot.in is b, root, root, halt, one per line.)
6. Disk images¶
6.1 Building one¶
make -C software disk # -> software/build/disk.img (512 MB, D40), from software/root.manifest
This runs tools/p32fs.py build IMAGE 512 root.manifest --kernel
../kernel/unix.bin --inodes 16384. p32fs.py lays out the disk as follows:
| Sectors | Contents |
|---|---|
| 0 | boot header: "P32B", kernel sector count, load address, entry, first filesystem sector |
| 1 … 2047 | the kernel image (1 MiB boot area) |
| 2048 … | the filesystem: 1 KiB blocks, superblock, inodes (64 bytes), free-block bitmap, data |
P32FS_TIME=SECS fixes the timestamps for reproducible images (the
session tests use it). p32fs.py mkfs IMAGE SIZE_MB [--inodes N] [--kernel
FILE] makes an empty filesystem.
6.2 Looking at and changing an image¶
p32fs.py ls IMAGE [PATH] p32fs.py cat IMAGE PATH
p32fs.py put IMAGE HOSTFILE PATH [--mode OCT] [--uid N] [--gid N]
p32fs.py mkdir IMAGE PATH [--mode OCT] [--uid N] [--gid N]
p32fs.py mknod IMAGE PATH c|b MAJOR MINOR [--mode OCT]
p32fs.py rm IMAGE PATH p32fs.py ln IMAGE OLD NEW
p32fs.py chmod IMAGE OCT PATH p32fs.py chown IMAGE UID GID PATH
p32fs.py fsck IMAGE
On a scratch copy (ken is uid 100, gid 10):
$ p32fs.py cat fs.img /etc/motd
Welcome to P32W Unix.
$ p32fs.py put fs.img note.txt /usr/ken/note.txt --uid 100 --gid 10
$ p32fs.py ls fs.img /usr/ken
drwxr-xr-x 2 100 10 192 .
drwxr-xr-x 10 0 0 320 ..
-rw-r--r-- 1 100 10 235 hello.c
-rw------- 1 100 10 279 .sh_history
-rwxr-xr-x 1 100 10 37332 a.out
-rw-r--r-- 1 100 10 20 note.txt
$ p32fs.py fsck fs.img
fs.img: 540 inodes, 8822 data blocks, clean
$ p32fs.py rm fs.img /usr/ken/note.txt
$ p32fs.py fsck fs.img
fs.img: 539 inodes, 8821 data blocks, clean
fsck checks the bitmap against the blocks in use, link counts, sizes, .
and .., and that every block is owned once. Its "clean" means no
problems found. It does not report the superblock's unmounted-cleanly
flag; the kernel checks that flag at boot.
6.3 Updating files on a system you are running¶
There are two safe ways:
- Stop, edit, boot.
haltP32W Unix (so the image is written out and clean), runp32fs.py put …on the image, then boot it again. Do not edit the image while the emulator is still running. - Copy the file in while the system runs, over the second terminal
with Kermit (
-T PORT, thenkermit -xon P32W and C-Kermit ortools/pykermit.py --tcp PORT send FILEon the Mac). The kernel writes the file, so the image stays consistent.
7. Bare-metal programs¶
Anything that is not P32W Unix runs as a ROM image: a flat little-endian binary placed at physical address 0 and started there in supervisor mode.
7.1 In assembly¶
python3 assembler/p32asm.py prog.asm -o prog.bin [-l prog.lst]
emulator/p32emu prog.bin
When the program starts: PC = 0, supervisor mode (untranslated
addresses), interrupts off, VBR = 0, all registers 0, RAM all zeros (on
the real machine RAM will hold garbage at power-on, so do not rely on
this). A minimal program, emulator/test/hello.asm:
CON = 0xFFFFFF00 ; UART 0 data register
li sp, 0x00500000 ; top of RAM; full-descending stack (D6)
la r10, msg
call puts
li r1, 0
halt ; exit status = r1 & 0xFF
puts: lbu r11, 0(r10)
beqz r11, puts_done
sb r11, CON(r0)
addi r10, r10, 1
jmp puts
puts_done:
ret
msg: .asciz "P32W lives! instruction-level reference emulator\n"
In the emulators the UART needs no setup (there is no baud rate) and its
transmitter is always ready. For portable code, set up the UART and poll
the line-status register as the programmer's guide shows
(software/lib/p32io.inc has the routines). The program's HALT sets the
exit status, which makes self-checking tests simple: isa_test.asm halts
with the number of the failing case.
7.2 In C¶
python3 cc/p32cc.py --sa prog.c -o prog.bin
emulator/p32emu prog.bin < /dev/null
--sa links software/sa/crt0_sa.s. The C runtime copies .data to RAM,
clears .bss, sets the stack at the top of RAM, and installs a small trap
handler that serves exit, write, read and sbrk through the real
SYSCALL instruction. main's result becomes the exit status:
$ cat sq.c
#include <stdio.h>
int main(void)
{
int i;
for (i = 1; i <= 5; i++)
printf("%d squared is %d\n", i, i * i);
return 42;
}
$ python3 cc/p32cc.py --sa sq.c -o sq.bin
$ ./p32emu sq.bin </dev/null; echo "status=$?"
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25
HALT at 000000DC, r1=0000002A (19977 instructions)
status=42
$ ./p32uemu -u p32ucode.bin sq.bin </dev/null; echo "status=$?"
1 squared is 1
...
5 squared is 25
HALT at 000000DC, r1=0000002A (19977 instructions, 71312 cycles)
status=42
7.3 The memory map and the I/O page¶
The emulators follow the address map of D52 (architecture.md §4):
| Physical address | What | Notes |
|---|---|---|
0000_0000–000F_FFFF |
ROM, 1 MiB (the image), on the memory card in window 0 | a write is BUSERROR |
0010_0000 up to N × 16 MiB |
RAM: -M N memory cards of 16 MiB in windows 0 … N−1 (1 to 4, default 1), or -M S0,S1,… MiB per window (D60) |
|
above it, to FDFF_FFFF |
nothing | any access is BUSERROR (an empty memory window too) |
FE05_0000–FE05_FFFF |
nothing: the video card (revision 2, D103) has no memory on the bus | BUSERROR |
FFFF_8000–FFFF_FFFF |
the device windows: eight of 4 KiB, window n at FFFF_8000 + n × 1000, a card's registers in the window's top 256 bytes. The I/O card is in window 7, the disk card in window 6, the video card in window 5 |
only the registers below answer; anything else is BUSERROR |
| I/O address | Device | Access | IRQ |
|---|---|---|---|
FFFF_FF00–07 |
UART 0 (TL16C550 registers 0–7): the console, stdin/stdout | bytes | 2 |
FFFF_FF08–0F |
UART 1: -A/-a files or -T TCP |
bytes | 3 |
FFFF_FF30–37 |
UART 2: -A2/-a2 or -T2 |
bytes | 2 (shared with UART 0) |
FFFF_FF38–3F |
UART 3: -A3/-a3 or -T3 |
bytes | 3 (shared with UART 1) |
FFFF_FF10 TCOUNT, FF14 TRELOAD, FF18 TCTRL (bit 0 enable, bit 1 pending; write 1 to bit 1 to acknowledge) |
timer | aligned words only | 0 |
FFFF_FF20 |
input port (reads -p; writes ignored) |
byte | |
FFFF_FF21 |
output port (reads back; -t traces writes) |
byte | |
FFFF_FF40 / FF41 |
real-time clock: register number (write) / register (read/write) | bytes | (5, not generated) |
FFFF_FF50 / FF51 |
USB port 0 (CH376S): data / command and status (-U) |
bytes | 6 |
FFFF_FF52 / FF53 |
the network coprocessor: data / status and control (-N) |
bytes | 7 |
FFFF_FF54 / FF55 |
USB port 1, as FF50/FF51 (-U1) |
bytes | 6 |
FFFF_EF00 + 16 p |
the disk card's port p (0–3): +0–7 the task file (+0 data, +7 status / command), +8–15 the control block (+14 alternate status). Port 0 is CF socket 0 (-d), port 1 CF socket 1 (-d1), ports 2 and 3 the IDE channels A and B, master and slave (-d2 -d2s, -d3 -d3s; the DEV bit of +6 picks the drive). A halfword access to +0 is the 16-bit data register (D26). A port with nothing on it reads 00 in every register, as the card's pull-downs make it (D66) |
bytes; halfword at +0 | 1 (every port) |
FFFF_DF00 / DF04 |
the video card's chip, an HD63484: its status and address register; the register addressed or the FIFOs (programmer's guide §6.6; only with the card fitted) | halfwords | 4 |
FFFF_DF08 / DF0C |
the video card's status and PS/2 command; the PS/2 byte | bytes | 4 |
FFFF_xFF0 / xFF1 / xFF4 in windows 7, 6 and 5 |
card ID (1 I/O, 2 disk, 3 video), revision (bytes), interrupt status (word, a bit per device, 1 = asking) | as listed |
The rule is the card's: byte registers answer byte accesses only, the timer
answers aligned word accesses only, and anything else gets no acknowledge,
which the MMU / data card turns into BUSERROR. emulator/test/io_test.asm
exercises the I/O card's devices and CF socket 0 (card_test.asm the card
registers and UARTs 2 and 3, video_test.asm the video card):
$ ./p32emu -p 0x5A -a aux.out -d io.img io_test.bin </dev/null; echo "status=$?"
io ok
HALT at 0000025C, r1=00000000 (10498 instructions)
status=0
$ cat aux.out
aux port
7.4 Going to the hardware¶
The same ROM image is what the memory card's boot flash holds:
python3 assembler/romsplit.py IMAGE.bin [PREFIX] splits it into the four
byte-lane files, one per flash chip.
8. Debugging¶
8.1 The instruction trace (p32emu -t)¶
$ ./p32emu -t hello.bin 2>&1 >/dev/null | head -8
00000000 S 2FF00050 LUI r15,0x50
00000004 S 2FA00000 LUI r10,0x0
00000008 S 23AA0030 ORI r10,r10,0x30
0000000C S 61000002 CALL 00000018
00000018 S 44BA0000 LBU r11,0(r10)
0000001C S 50B00003 BEQ r11,r0,0000002C
00000020 S 48B0FF00 SB r11,-256(r0)
00000024 S 20AA0001 ADDI r10,r10,1
Each line shows the address, the mode (S or U), the instruction word and
the disassembly. Pseudo-instructions appear as what they expand to (li →
LUI/ORI, beqz → BEQ … r0), and branch and jump targets are shown
as absolute addresses. Events appear as indented lines:
TRAP TLBMISS_X (8) addr=00008014 -> 00000168 (vm_demo)
TLBW I[8] VPN=00008 ASID=1 -> PFN=00008 V-X (vm_demo: valid, executable)
TLBW D[0] VPN=00400 ASID=1 -> PFN=00200 VW- (vm_demo: valid, writable)
INTERRUPT IRQ0 at 000000F4 -> 000001F8 (irq_test)
OUTPUT PORT <- 3C (io_test)
The TRAP line gives the cause name and number, BADADDR, and the handler
address (VBR). The INTERRUPT line gives the line, the EPC (the next
instruction) and the handler. A trap line follows the instruction that faulted, except for
a fault on the fetch itself: then there is no instruction line, only
the TRAP line, because the instruction was never read. This happens for
example after an RTE to a user page that is not in the TLB yet.
The trace can be large: P32W Unix runs about 9 million instructions to
boot. Combine -t with -l or a small program, and filter it with
grep.
8.2 State traces (-s FILE, both emulators)¶
One line per instruction, written before the instruction runs:
PC MODE r1 … r14 r15 o=OTHER-r15 st=STATUS epc=EPC cause=CAUSE bad=BADADDR
00000004 S 00000000 … 00000000 00500000 o=00000000 st=1 epc=00000000 cause=00000000 bad=00000000
r15 is the current mode's stack pointer and o= the other bank (the USP
in supervisor mode, the SSP in user mode). The trace does not include
r0, VBR, IMASK, ASID, TLBHI/TLBLO, the TLB, memory or devices.
Differences in those show up a few instructions later, when they affect
registers.
The two emulators write identical traces for any program they agree on, so
cmp finds the first instruction at which they disagree. The first 300,000
instructions of a Unix boot (a 61 MB trace each):
$ printf 'b\n' | ./p32emu -l 300000 -R 1791936000 -s r.trace -d a.img monitor.bin >/dev/null
$ printf 'b\n' | ./p32uemu -u p32ucode.bin -l 300000 -R 1791936000 -s u.trace -d b.img monitor.bin >/dev/null
$ cmp r.trace u.trace && echo "300000 states identical"
300000 states identical
And a deliberate disagreement: -c changes the timer, so the interrupt
test diverges as soon as timing matters:
$ ./p32emu -s r.trace irq_test.bin; ./p32uemu -c -u p32ucode.bin -s u.trace irq_test.bin
$ cmp r.trace u.trace
r.trace u.trace differ: char 4087, line 21
Line 21 is the 21st instruction. diff r.trace u.trace | head shows both
states there, and p32emu -t -l 21 shows what led up to it.
8.3 The stop dump¶
When an emulator stops on an unhandled exception, the instruction limit or
a hopeless WAIT, it prints the machine's state. The two dumps differ
because the two machines differ:
$ ./p32emu mis.bin (li r2, 0x00100002 ; lw r3, 0(r2))
EXCEPTION: misaligned load at address 00100002
pc=00000008 ins=42320000 LW r3,0(r2) (3 instructions)
mode=S status=1 epc=00000000 cause=00000000 badaddr=00000000 vbr=00000000 usp=00000000
imask=00 timer: count=0 reload=0 enable=0 pending=0 asid=0 tlbhi=00000000 tlblo=00000000
r0 =00000000 r1 =00000000 r2 =00100002 r3 =00000000
...
$ ./p32uemu -u p32ucode.bin mis.bin
EXCEPTION: cause 00000003 at address 00100002 (no handler, VBR = 0)
pc=0000000C ir=42320000 uaddr=1:01:0:0 mode=S status=1 (3 instructions, 11 cycles)
rf0 =00000000 rf1 =00000000 rf2 =00100002 rf3 =00000000
...
rf28=00000000 rf29=00000000 rf30=00000000 rf31=00000000
p32emushows the faulting instruction: its address, word and disassembly, the control registers, the timer and MMU registers, and r0–r15. For a limit stop,pc/insare the last instruction executed.p32uemushows the hardware state:pcis the PC register, which has already advanced past the fetch (0000000Chere, for the instruction at00000008);iris the instruction register;uaddris the microaddressSEQ:OPCODE:STEP:COND(here1:01:0:0= the TRAP internal sequence, step 0); and the dump lists all 32 physical register-file words: rf15 = USP, rf16–19 = EPC/CAUSE/BADADDR/VBR, rf20–27 = the microcode scratch registers T0–T7, rf28 = the r0 write sink, rf31 = SSP. The cause is a number (3 = MISALIGNED, as inp32isa.py).
8.4 The instruction limit¶
The default limit of 100 M instructions stops a program that has gone into
a loop. Raise it for long scripted runs: the session tests use -l
4000000000, and the native-compiler tests use up to 2000000000000.
$ ./p32emu -l 1000 primes.bin >/dev/null
STOP: instruction limit (1000) reached
pc=0000001C ins=20220001 ADDI r2,r2,1 (1000 instructions)
...
$ ./p32uemu -u p32ucode.bin -l 1000 primes.bin >/dev/null
STOP: instruction limit reached
pc=00000020 ir=20220001 uaddr=0:20:0:0 mode=S status=1 (1000 instructions, 3497 cycles)
Both emulators stop after exactly N instructions, at the same point.
8.5 Debugging programs on P32W itself¶
Inside P32W Unix, db program [args] is a machine-level debugger with
breakpoints, single-stepping, bt, registers, memory and disassembly
(unix.md, "Debugging"). For user programs this is usually more
convenient than an emulator trace, which mixes in the kernel. To see what
the kernel does, use p32emu -t with a limit, or look up addresses in
kernel/unix.map.
9. Tests¶
make -C emulator test (emulator/test/run_tests.sh) covers the whole
project. The parts that involve the emulators are:
- Programs on p32emu (
emulator/test/*.asm): exact encodings,hello,isa_test(every instruction),echo,fibandprimes(compared with Python references),trap_test, the stage-A kernel programs,irq_test,wait_nowake,mt_demo,vm_test,vm_demo, and theexc_*programs (each must stop with status 255 and the rightEXCEPTION:message). - The devices on both emulators, instruction by instruction:
io_test(UARTs, ports, the CF disk; identical disk images afterwards),card_test(the card ID registers, UARTs 2 and 3, the shared interrupt levels) andvideo_test(the video card: its chip's registers and FIFOs, the font, characters, a line, interrupts, the keyboard and the mouse; identical-Vscreen dumps).acrtc_test.cchecks the chip's model by itself.vidcard_testis the video card's short program for the gate-level machine:generators/sim_sys.py --videoagainstp32uemu -C, the same trace and cycles, both interrupts taken (D107). - The bus lock (D54):
swap_dmarunsSWAPonp32uemuagainst the DMA master (-D) at several periods and must lose no addition; with-Dnolockit must lose some, so the test can tell. - The cross-check (§14.1): 20 programs run on both emulators. Exit
status, console output and the
-strace must be identical.lib_testand the scripted monitor session also compare disk images. - P32W Unix sessions (§10.2), on p32emu, with session1 also on p32uemu.
10. Scripted sessions¶
10.1 -w and prompts¶
An interrupt-driven program such as P32W Unix reads input when it is ready.
-w makes piped input arrive the way a person would type it, after the
program has asked for it:
- Input is delivered one line at a time. A line is released only when the output ends with a prompt. After a line is released, the output must show a prompt again before the next one.
- A prompt counts only at the start of a line (just after a
\n), unless it is written with a leading*, which allows it anywhere. - The defaults are
#,$,>,login:,Password:,Ok\r\n(BASIC) and*?(BASIC'sINPUT, anywhere in a line). -P STRadds a prompt; the first-Preplaces the defaults (up to 8 in all). Example fromdb_test.sh:-w -P '# ' -P 'db> ' -P 'login: ' -P 'Password: ' -P '> '.-wapplies to UART 0 and, with-A, to UART 1, each watching its own output.-wturns off interactive mode (no raw terminal). The default instruction limit of 100 M therefore applies: pass-l.- When the input runs out, nothing more is typed. End every script with
halt. Otherwise the system keeps idling (skipping time) until the instruction limit stops it with status 255.
10.2 The session tests¶
software/tests/run_session.sh NAME [uemu] builds a fresh image with fixed
timestamps in $TMPDIR, assembles the monitor, and then runs:
p32emu -w -R 1791936000 -l 4000000000 -d $T/os.img [-A NAME.tty1 -a tty1.out] monitor.bin < NAME.in
It removes the lines that depend on binary sizes or speed (kernel sector
count, free memory, seconds in date/uptime/time), prints the
transcript (and tty1's, if there is one), and runs p32fs.py fsck on the
image the session leaves behind. make test compares the output with
NAME.expect. session1 also runs with uemu, on p32uemu and the real
control store. Other tests use the same pattern: toolchain_test.sh,
cc_test.sh, the tar and zip tests, db_test.sh (with -P), and
kermit_test.py / vi_test.py (which use the TCP terminal, -T).
Part 2 — Theory of operation¶
11. The shared devices (p32io.h)¶
Both emulators #include "p32io.h". It holds the only copy of every device
(D50): the I/O card, the disk card and the video card, so the two cannot
disagree about devices. The CPU side calls six functions:
| Function | Use |
|---|---|
io_read(pa, size, &v) / io_write(pa, size, v) |
an access to a device window, the value right-aligned. They return 1 if nothing answers (no -ACK), which the caller turns into BUSERROR. |
io_ipend() |
the IRQ lines now asserted, as IPEND shows them |
io_tick() |
one timer tick (per instruction, or per microcycle with -c) |
io_wait(imask) |
WAIT with nothing pending: idle until something can wake the CPU, or report that nothing can |
io_hwedge() |
p32uemu -C: the I/O card's clock edge |
The video card's chip is a model of its own, p32acrtc.h (from
YACC1-D's, moved to the chip's 16-bit bus), which p32io.h includes: the
registers, the two FIFOs, the commands a driver needs, the chip's busy
time. Its time is the machine's: the timer's microseconds, or the
microcycles with p32uemu -C, where each access to the chip takes three
wait states and a PS/2 command written to the card takes one (D106). With
-C the CPU's interrupt logic has the chip's IRQ as the microcycle began:
a request that a register write causes is seen in the next microcycle, as
on the card, where the write lands in the middle of the cycle's last clock,
the moment the CPU card samples the lines (D107). io_wait knows that the chip finishes its work by itself and
may then interrupt, as it knows the timer. The option parsing for the
device options (io_option: -d -d1 -d2 -d2s -d3 -d3s -a -A -T and their
UART 2 and 3 forms, -p -w -P -R -U -U1 -Uout -N -Nport -Nca -V -K -Km)
and the terminal setup (io_init) are shared as well.
Devices respond instantly. There is no baud-rate delay, no disk seek, and no BSY phase. Software must depend only on status bits, as on the hardware. Concretely:
- UARTs 0–3 (TL16C550 subset): RBR/THR, IER, IIR/FCR, LCR (DLAB),
MCR, LSR, MSR, SCR, DLL/DLM. The divisor and FCR are stored and otherwise
ignored. A byte written to THR is sent at once, so LSR always shows THRE
and TEMT. LSR.DR shows whether input is available, read ahead one byte.
MSR reads
B0(DCD, DSR, CTS). Interrupts: receive-data-available while IER bit 0 is set and input is waiting; THRE after a byte is sent or when IER bit 1 is newly enabled, cleared by reading IIR (16550 rules). There is no FIFO model and no line errors. - Timer:
TCOUNT,TRELOAD,TCTRL(§12.5, §13.6). - The disk card's drives (True-IDE task file, four ports with a master
and a slave each; the DEV bit selects the drive, and register writes 1–6
go to both drives as on a shared cable): READ SECTOR(S) 20h/21h and WRITE
SECTOR(S) 30h/31h (LBA mode only, count 0 = 256), IDENTIFY ECh (model
"P32W EMULATED CF CARD" on the CF sockets, "P32W EMULATED IDE DISK" on
the IDE channels), SET FEATURES EFh (01h = 8-bit, 81h = 16-bit
transfers), the power/cache/parameter no-ops (E0 E1 E5 E7 91). Anything
else aborts with ABRT. Data moves through a 512-byte buffer: byte reads
and writes at the port's +0, or halfwords there (D26). INTRQ is set when
a sector is ready or written and cleared by reading Status; every port's
INTRQ drives IRQ1, and the disk card's interrupt status shows which.
Reading past the end of an image gives IDNF. A drive that is not fitted
reads Status
00. - Ports:
-pvalue in, latch out. - RTC: MC146818 registers 0, 2, 4, 6–9,
0x32(century), A–D, and RAM0x0E–0x7F. Binary or BCD and 12/24-hour follow register B. SET (B bit 7) freezes the time for setting and commits it when cleared. UIP is never set and register C reads 0 (no alarm or periodic interrupts). - USB (two CH376S in sector mode, D43; usb-design.md):
a drive whose contents are an image file (
-U,-U1), or no drive. - The network coprocessor (D45): the two byte registers and the
request / reply protocol of net-mailbox.md, carried out
with the Mac's own sockets; TLS goes through
tools/p32tls.py. - The video card (only with
-V,-K,-Kmor-dbg), revision 2 (D103, D104): the HD63484 through its two addresses, and the PS/2 section as its ATmega328P's firmware shows it (ASCII from the keyboard, the mouse's packets whole, the nine commands). No memory on the bus. It draws no picture;-Vwrites the screen at exit. What the chip's model lacks (circles, arcs, PAINT, character screens, the cursors) is listed at the head ofp32acrtc.h: such a command sets the chip's error bit and is named on stderr. - Card registers: each card answers at
+FF0of its window its card ID, at+FF1its revision (the I/O card 0, the disk card 1, the video card 2) and at+FF4its interrupt status. - IRQ lines asserted: 0 timer, 1 disk, 2 UART 0 or 2, 3 UART 1 or 3, 4 the video card (its PS/2 section, its chip), 6 USB, 7 network. IRQ5 (RTC) is defined but never asserted.
12. p32emu: the instruction-level reference¶
12.1 Machine state¶
uint32_t r[16]; // r15 = the CURRENT mode's SP
uint32_t sp_other; // the inactive r15 bank (USP in S mode, SSP in U mode)
uint32_t pc, status; // status: S=1 IE=2 PS=4 PIE=8; reset = S
uint32_t epc, cause, badaddr, vbr, imask, asid, tlbhi, tlblo;
struct { uint32_t hi, lo; } tlb[2][64]; // [0] D-TLB, [1] I-TLB
uint8_t *rom /* 1 MiB */, *ram /* from 1 MiB to -M N x 16 MiB */;
The r15 bank (D6) is implemented by swapping values: set_mode()
exchanges r[15] and sp_other whenever S changes. MFCR USP (which is
privileged, so it is always used in S mode) reads sp_other.
12.2 The loop¶
run() repeats the following for every instruction:
- Limit: if the count has reached
-l, stop. - Interrupt check (between instructions, B2): if
IEandio_ipend() & IMASK, enter the interrupt (§12.4). After anMTCRor anRTE, the check uses theIEandIMASKfrom before that instruction (the interrupt shadow, D20). - State trace (
-s), then count the instruction andio_tick(). - Fetch through
load_acc(pc, 4, ACC_FETCH), which translates the address and checks the bus. - Decode with the tables in
p32opc.h:p32_fmt[op] == F_ILLEGALor any set must-be-zero bit (p32_zmask[op]) → ILLEGAL;p32_priv[op]in user mode → PRIVILEGE. Field extraction: A = bits 23:20, B = 19:16, C = 15:12, 16-bit immediate (sign- or zero-extended), 24-bit jump offset. - Execute in a
switchon the opcode. The C code is the definition of each instruction, for example DIV by zero gives0xFFFFFFFF, and branches compare A with B (D5). - Commit:
pc = next,r[0] = 0.
p32opc.h is generated from p32isa.py, so the opcode numbers, formats
and the illegal/privileged decisions are the same ones the assembler and
the microcode generator use.
12.3 Exceptions: precise by construction¶
trap(code, addr, what) is called from any check. It sets
EPC = cur_pc (the faulting instruction), CAUSE, and BADADDR = addr.
Then it updates STATUS (PS←S, PIE←IE, S←1 with the r15 swap, IE←0),
sets pc = VBR & ~3, and longjmps back to the top of the loop,
abandoning the instruction. This is precise because every check happens
before any state is written. A load writes rd only after the access
succeeds. CALL stores the return address and then updates SP. RET
loads the return address, checks its alignment, and only then pops. So
RTE can simply run the instruction again (D10).
The check order for an access is (C5): alignment → user access to the upper
2 GiB (PRIVILEGE) → TLB miss → protection → physical bus (ROM write,
unmapped address, an I/O register that does not answer or answers another
size). BADADDR is the virtual address for the first four and the
physical address for a bus error. Traps raised by the control logic
(ILLEGAL, PRIVILEGE for an opcode, SYSCALL, BREAK) have BADADDR = 0. No
memory is written by trap entry (D10).
While VBR = 0 no handler is installed, so an exception or interrupt
stops the emulator with the dump (§8.3) and status 255. This is how bare
test programs report a fault.
12.4 Privilege and interrupts¶
User mode can run neither the privileged opcodes (HALT, RTE, MFCR,
MTCR, WAIT and TLBW, the set PRIVILEGED in
p32isa.py) nor access
0x8000_0000 and above. RTE returns to EPC & ~3 and restores S←PS,
IE←PIE.
interrupt() picks the lowest asserted, enabled line (fixed priority)
and enters the trap with CAUSE = 0x8000_0000 | v << 8 | n (v the vector
io_iack(n) gives: the acknowledge cycle, D57), BADADDR = 0 and
EPC = the next, unexecuted instruction. Entering clears IE, so the next
check cannot interrupt the handler's first instruction. The interrupt
shadow (D20) copies the hardware: the sequencer decides "interrupt" in an
instruction's last microcycle, while that cycle's own write to STATUS or
IMASK lands only at its closing edge. p32emu therefore saves IE and
IMASK before each instruction (sh_ie, sh_im) and uses them for the
check after an MTCR or an RTE. As a result one more instruction always
runs after RTE before a pending interrupt is taken, which guarantees
progress.
12.5 The timer: one tick per instruction (D11)¶
io_tick() runs once per instruction, before the fetch: if enabled,
TCOUNT counts down; at 0 it sets pending (IRQ0) and reloads from
TRELOAD. The hardware timer runs at 1 MHz from the clock divider, so the
real interrupt point depends on the exact cycle counts of the
instructions. Tying the tick to the instruction count instead makes
interrupt timing exactly repeatable and independent of the
microarchitecture. A program sees the same interrupt at the same
instruction every run. That is what lets irq_test check "the interrupt
arrives at exactly this instruction". It also means the instruction-level
model never needs cycle counts, which keeps it the simple, golden reference
(D11 point 4). The cost is that p32emu's timing is not the hardware's
(§15). The RTC's emulated time (rtc_us) advances with the same ticks.
12.6 WAIT and idle skipping¶
WAIT (privileged) calls io_wait(IMASK):
- If an enabled line is already up, it returns at once, and the interrupt is
taken at the next check if
IEis set. - If the timer is off or IRQ0 is masked, nothing can ever wake the CPU,
because the timer is the only device that changes by itself. The emulator
stops with
STOP: WAIT with no possible wake-upand status 255 (wait_nowake.asmtests this). This also applies when only a UART interrupt is enabled: the model never considers input that might arrive later. - Otherwise, if interactive (a terminal console or
-T), itselect()s for up toTCOUNTmicroseconds. If input arrives first, the timer advances by the time that actually passed and the UART is polled at once. Otherwise the timer expires. - Otherwise it jumps to the expiry: the remaining
TCOUNTis added to "idle ticks" and to the RTC's emulated time, pending is set, andTCOUNTreloads. That costs no host time.
Either way WAIT itself is one instruction. The skipped ticks are not
instructions.
12.7 The TLB (stage C, D12/D13)¶
There are two direct-mapped tables of 64 entries: the I-TLB for
fetches and the D-TLB for loads and stores. Each entry is hi = VPN<<12 |
ASID and lo = PFN<<12 | X W V. Translation applies to user mode
only; supervisor mode is untranslated. For a user access to address va
below 2 GiB:
index = (va >> 12) & 63 side = fetch ? I : D
hit = lo.V && hi.VPN == va.VPN && hi.ASID == ASID
miss -> TLBMISS_X / _R / _W, TLBHI = va.VPN | I-bit(0x100) | ASID
store to W=0 or fetch from X=0 -> PROTECT, TLBHI set the same way
pa = lo.PFN | (va & 0xFFF)
Refill is done by software: the kernel's miss handler finds the
faulting page in TLBHI, writes TLBLO, and runs TLBW, which writes
entry [TLBHI.I][TLBHI.VPN & 63]. Then RTE retries the access. There is
no hardware page-table walker and no global bit. ASIDs are 8 bits, so the
kernel flushes by writing invalid entries. The writable bits are
TLBHI & 0xFFFFF1FF and TLBLO & 0xFFFFF007.
12.8 Memory and the I/O page¶
load_acc and store check alignment, translate, and then either call
io_read/io_write (physical ≥ 0xFFFF_8000) or map() the address into
the ROM or the RAM of the fitted memory cards (-M), little-endian,
size bytes at a time. A ROM write or
an address in none of them is BUSERROR.
13. p32uemu: the microcode-level model¶
13.1 What it models¶
p32uemu is the three-bus datapath of microarch.md,
clocked one microcycle at a time. Bus A carries the first operand
(register-file read port A, or the PC), bus B the second (read port B,
or the immediate unit), and bus R the result, which goes to one
destination a cycle. It models the two cards that make up the processor,
the CPU card and the MMU / data card
(architecture.md §3), joined by the local link on J3
(bus.md), and the rest of the machine as the bus sees it:
| Card, part | Model state |
|---|---|
| CPU card: sequencer, control store, instruction register and decode | microaddress {useq, uop, ustep, ucond}, IR, STATUS, IMASK, the cause latch, the fault flags (FFAULT: the fault was in the fetch; MMUF: the trap came from the MMU / data card), the decode trap, the CR decode table (uc_crkind, uc_craddr from p32uc.h), the immediate unit |
| CPU card: register file and PC | rf[32], the 32-word register SRAM, with two read ports (A, B) and one write port (from R); pc (bits 31:2: loads mask the low bits, PCINC adds 4) |
| CPU card: MDR | mdr, the store data, loaded from B (MDRB) or from R, and put on R replicated (MDRS) |
| CPU card: the ALU module | latches alua, alub and the function aluop, loaded together by ALUGO; the result on R (RSRC = ALU) from the next cycle until the next ALUGO; compare outputs EQ, LT, LTU of the latched operands |
| MMU / data card: address side | MAR, ASID, TLBHI, TLBLO, the split TLB, the access checks, the BADADDR latch (mmu_badaddr, updated by MMU faults only, D17) |
| MMU / data card: data side | load steering: in a LOAD cycle the addressed byte or halfword, brought down to bit 0 and extended, onto R |
| MMU / data card: the bus master interface | wait states (-W), the bus lock (-LOCK, held from a locked LOADW to its STOREW, D54) and, with -D, the arbiter and a DMA master |
| memory cards | ROM and RAM arrays (-M) |
| I/O, disk and video cards | p32io.h |
13.2 The control store and the microword¶
p32ucode.bin holds 16,384 words, one per microaddress:
microaddress = SEQ (bit 13) | OPCODE (12:5) | STEP (4:1) | COND (bit 0)
SEQ = 0 runs the instruction in IR (OPCODE = IR[31:24]). SEQ = 1
runs an internal sequence: 0 RESET, 1 TRAP, 2 INTR. STEP counts cycles
within a sequence (at most 16). Every step is a pair of words: COND
picks the twin, using the condition named by the previous word and
evaluated on the state after that word's clock edge (the P8X scheme). The
56-bit microword (p32uc.h's UF_* macros; seven flash chips, u0.bin–
u6.bin) has these fields, from bit 0 up:
| Field | Bits | Values |
|---|---|---|
ASRC |
2 | bus A's source: NONE, REG (read port A), PC |
BSRC |
2 | bus B's source: NONE, REG (read port B), IMM (the immediate unit) |
RSRC |
4 | bus R's source: NONE, ALU, MEM (a read cycle's data, steered for LOAD), IMM, MAR, MDRS (MDR replicated, the store data), CAUSE (the cause latch), BADADDR (the MMU / data card's latch), CRHW (a hardware CR), AGU (the address adder: A + the AOFF offset, D59) |
RDST |
3 | R's destination: NONE, REG (the write port), PC, MAR, PCMAR (PC and MAR together), IR, MDR, CRHW |
MDRB |
1 | MDR ← B at the edge |
MARPC |
1 | MAR ← PC at the edge (the next fetch's address) |
PCINC |
1 | PC += 4 at the edge |
ALUGO |
1 | latch A, B and ALU into the ALU module at the edge |
ALU |
5 | ADD SUB AND OR XOR NOR SLL SRL SRA SLT SLTU MUL MULH MULHU DIV DIVU REM REMU PASSA |
RSA, RSB, RSW |
3 + 3 + 3 | the register address of port A, port B and the write port: IR field A, B or C (with the r15 bank rule), CONST, CRMAP (the CR table), SP (15 with the bank rule) |
RCONST |
5 | the physical register address for any port that names CONST (r0 0, EPC 16, CAUSE 17, BADADDR 18, VBR 19, T0–T7 20–27) |
IMM |
3 | SEXT16, ZEXT16, LUI, BR (sext16·4), J (sext24·4), FOUR, ZERO, INC8 (IR 7:0 sign-extended, D59); on B or on R |
MEM |
3 | NONE, FETCH, LOAD, STORE (size from IR), LOADW, STOREW (word), CHECKW (alignment check only), IACK (the interrupt acknowledge cycle, D57) |
LOCK |
1 | on a LOADW: LOCKREQ on the local link, so -LOCK is held from this read to the end of the following STOREW (D54; SWAP, D51). The generator rejects it on any other word, and a locked read without its STOREW |
SEQ |
2 | NEXT (step + 1), END (instruction done: interrupt or fetch), TRAP (raise RAISE) |
COND |
3 | ZERO, EQ, LT, LTU (of the ALU's latched operands), CRILL (reserved CR), CRHW (hardware CR), FFAULT, MMUF |
MODE |
3 | NONE, TRAPENTER, RTE, TLBW, HALT, WAIT |
RAISE |
4 | the cause for SEQ = TRAP (SYSCALL, BREAK, ILLEGAL) |
AOFF |
2 | the address adder's offset: SEXT16 (IR 15:0), OFF8 (IR 15:8), INDEX (B << IR 1:0) (D59) |
MARINC |
1 | MAR += 4 at the edge (LI32, D59) |
Step 0 of every opcode's block is the shared fetch. Every instruction
starts with MAR = PC, and its last word sets MAR to the next PC (MARPC,
or RDST = PCMAR), so the fetch needs no address cycle of its own. For
example ADD rd, rs1, rs2:
| Step | Microword |
|---|---|
| 0 | RSRC=MEM MEM=FETCH RDST=IR PCINC (MAR already holds the PC; the opcode now selects the block) |
| 1 | ASRC=REG(RSA=B) BSRC=REG(RSB=C) ALUGO ALU=ADD |
| 2 | RSRC=ALU RDST=REG(RSW=A) MARPC SEQ=END |
The ALU is pipelined: step 1 latches both operands and the function, and step 2 puts the result on R while the PC goes to MAR for the next fetch.
Cycle counts, including the fetch (p32ucode.py --stats): ALU and
immediate operations 3, LUI 2, LI32 3, loads and stores 3 (indexed
stores and post-increment 4; the address adder, D59), SWAP 5, branches 3
(not taken) or 4 (taken), JMP 3, JR 4, CALL 7, CALLR 10, RET 6,
RTE 3, MFCR/MTCR 3–5, HALT/SYSCALL/BREAK/WAIT/TLBW 2. An
illegal or privileged-in-user-mode opcode costs one suppressed cycle after
the fetch (§13.3, step 4). Trap entry takes 6 cycles and interrupt entry 6.
Measured: P32W Unix booted, root logged in, halt (7,049,876
instructions) takes 22,427,463 cycles, 3.18 per instruction: 0.94 MIPS
at 3 MHz. For comparison, before the address adder and the D59
instructions the same run took 26,738,361 cycles (7,490,575 instructions,
3.57 per instruction), and a one-bus datapath 47,030,356 (6.28).
13.3 One microcycle (cycle_body)¶
- Instruction boundary (
SEQ= 0, step 0): check the limit, write the state trace, count the instruction, andio_tick()(default timing). - Record
IEandIMASKas the cycle begins, for the interrupt decision. - Read the microword at the microaddress.
- Decode trap (D18): in step 1, the first after the fetch, the CPU card's decode table flags an illegal opcode (format, must-be-zero bits) or a privileged one in user mode. The cycle does nothing and the TRAP sequence follows.
- Buses A and B, from the state as the cycle began.
ASRCputs a register-file word (address fromRSA) or the PC on A;BSRCputs a register-file word (address fromRSB) or the immediate unit on B. - Bus R: exactly one source.
RSRCselects one value: the ALU's result from the operands and function an earlierALUGOlatched, a memory read cycle (a fetch orLOADWgives the whole word; aLOADgives the addressed byte or halfword brought down and extended), the immediate unit, MAR, the MDR replicated (MDRS: a byte or halfword on all four lanes, by the size fromIR), the cause latch, the BADADDR latch, or a hardware control register. IfMEMnames a cycle butRSRCis not MEM (a store, or aCHECKW), the memory cycle runs with R as the store data. - A fault aborts the cycle (§13.5).
- The clock edge.
RDSTlatches R into one destination: a register-file word (address fromRSW),PC(low two bits dropped),MAR,PCandMARtogether,IR,MDR, or a hardware CR. At the same edgeMDRBloads MDR from B,MARPCloads MAR from the PC,ALUGOlatches A, B and theALUfunction, andPCINCadds 4 to the PC. Then theMODEaction: TRAPENTER (PS←S, PIE←IE, S←1, IE←0), RTE (S←PS, IE←PIE), TLBW (write the entryTLBHIselects), HALT (print and exit), WAIT (under-C, with nothing pending, the microcycle is held and runs again). - Next microaddress. NEXT: step + 1,
ucond= the condition on the post-edge state, so EQ, LT and LTU compare the operands this word latched; leaving step 0,uopis loaded fromIR[31:24]. END: if the start-of-cycleIEis set and a line in the start-of-cycleIMASKis pending — and this is not a TRAPENTER cycle (D20) — go to INTR with the cause0x8000_0000 | n. Otherwise go to step 0, the next fetch, with MAR already at its address. TRAP: go to the TRAP sequence withRAISEas the cause.
This model has the same restrictions as the hardware's datapath. Each bus
has one driver a cycle; R has one destination, beside which only MDR (from
B), MAR (from the PC), the ALU's latches and the PC's increment change at
the edge; the register file reads two words and writes one. Adding two
registers therefore takes two cycles after the fetch: both operands into
the ALU, then the result back to the register file. The generator checks
every word: R with a destination has a source, a read's data is on R, a
store's data is MDRS, MAR and MDR are not loaded twice, the PC is not
both loaded and incremented, and the immediate unit is not on B and R at
once.
13.4 Registers, the r15 bank and r0¶
The register file is 32 physical words:
| Physical | Contents |
|---|---|
| 0 | r0 (written 0 once, by the RESET sequence) |
| 1–14 | r1–r14 |
| 15 | r15 in user mode = USP |
| 16–19 | EPC, CAUSE, BADADDR, VBR |
| 20–27 | T0–T7, microcode scratch |
| 28 | write sink |
| 31 | r15 in supervisor mode = SSP |
The three ports each form their own address (RSA, RSB, RSW) and
share the word's one RCONST. The bank rule is applied to the
address: an IR-field or SP reference to register 15 means physical 31
when S = 1. No value is copied on a mode change (compare p32emu's swap).
r0 has no special storage. When a write's address comes from an IR
field equal to 0, the CPU card redirects it to physical 28, so physical 0
keeps the 0 that RESET wrote. MFCR/MTCR reach EPC, CAUSE, BADADDR, VBR
and USP through the CR decode table as ordinary SRAM words (RSA or
RSW = CRMAP). STATUS, IPEND, IMASK, ASID, TLBHI and TLBLO are
hardware registers (RSRC / RDST = CRHW). A reserved CR number makes the
CRILL condition true, and the microcode raises ILLEGAL.
13.5 Memory cycles, byte lanes and faults¶
mem_cycle() models the MMU / data card and the memory cards. It checks
alignment (MAR against the size from IR, or 4 for the W kinds), then
translation (the same TLB rules as §12.7, with TLBHI set on a miss or
protection fault), then the physical bus. Reads return the whole aligned
word with all four lanes driven, as a memory card does; for a LOAD the
MMU / data card's steering brings the addressed byte or halfword down and
extends it onto R in the same cycle. Stores write only the lanes the
byte enables select, from the replicated data on R (MDRS). For the
I/O page the model shifts the value to and from the lane, as the MMU /
data card's steering does.
A fault (mem_cycle returns 1):
- Suppresses the whole cycle: no R destination, no
MDRB,MARPC,ALUGOorPCINC, noMODE. This is the CPU card's abort gating (D17). - Latches the cause and
mmu_badaddr= the faulting address (virtual for the MMU checks, physical for BUSERROR, as the hardware's PA lines give, D50), sets MMUF, and sets FFAULT if the fault is in the fetch cycle (step 0). - Jumps to the TRAP sequence, which is ordinary microcode:
| Step | Microword |
|---|---|
| 0 | CAUSE ← cause latch, condition FFAULT |
| 1 | A = PC, B = 4 (fault after the fetch: PC already incremented) or B = 0 (fault in the fetch), ALU ← SUB, condition MMUF |
| 2 | BADADDR ← 0 (raised by the CPU card) or ← BADADDR latch (MMU fault) |
| 3 | EPC ← ALU |
| 4 | A = VBR, ALU ← PASSA |
| 5 | PC, MAR ← ALU, MODE=TRAPENTER, END |
INTR is similar, in 6 cycles: MAR ← cause latch (0x8000_0000 | n) with
A = PC, ALU ← PASSA; EPC ← ALU (the next instruction); the
acknowledge cycle, CAUSE ← MEM (MEM = IACK: MAR with bits 15:8 = the
vector, D57); BADADDR ← 0;
A = VBR, ALU ← PASSA; then PC, MAR ← ALU with TRAPENTER. Both end
with MAR at the handler, ready for its fetch. Entering either sequence with
VBR = 0 stops the emulator (as p32emu does).
The generator's commit-after-access check (D14) makes this precise.
On every path through every instruction, no programmer-visible state (an
architectural register, PC, a store, STATUS, the TLB) is written before the
instruction's last cycle that can fault. Earlier results go to T0–T7 or
wait in the ALU. For example CALL computes SP - 4 in the ALU, stores
the return address there, and the ALU still holds SP - 4 when the next
step writes it to SP; SWAP keeps the word it read in T0 until its store has
succeeded. A faulting instruction therefore leaves nothing behind, and
RTE re-runs it, exactly as in p32emu. The generator also checks that
step 1 never starts a memory cycle, because a decode trap must be able to
suppress that step (D18), and that every END leaves MAR at the next PC.
Wait states, the lock and DMA (cycle()). A microcycle whose MEM
field names a bus cycle (a fetch, load or store) is held for -W N more
clocks, as the MMU / data card holds the CPU card through STALL while a
slave asserts -WAIT (D50). Only the cycle count and, with -c or -C,
the timer see the difference. A locked LOADW (the LOCK bit) sets the
bus lock, and its STOREW, or a fault, clears it (D54). With -D, the DMA
master asks for the bus every PERIOD microcycles; the arbiter grants it at
the next microcycle boundary unless the lock is held (-Dnolock: in any
case). Its read and write of the word come first, and a CPU bus cycle in
that microcycle waits two clocks for them. This is how swap_dma shows
that SWAP loses nothing to a DMA master under -LOCK (§9).
13.6 Timer timing: default, -c and -C¶
| Mode | Tick | Matches | Use |
|---|---|---|---|
| default | once per instruction, at the instruction boundary (not in trap or interrupt entry), like p32emu |
p32emu, instruction by instruction |
the cross-check; running software |
-c |
once per microcycle, including trap and interrupt entry | nothing exactly: a quick approximation of hardware timing | experiments |
-C |
io_hwedge() at the end of every microcycle, with the I/O card's logic: a tick every third cycle while enabled (1 MHz from the 3 MHz microcycle), the enable taking effect one edge late, a TCOUNT write in that cycle winning over the count, and at 0 (enabled) the next edge reloading and setting pending, so TCOUNT reads 0 for one cycle. WAIT with nothing pending holds its microcycle (the sequencer's hold) instead of skipping |
the I/O card's timer logic, cycle for cycle | checking hardware timing |
With -c or -C, interrupts arrive at different instructions than in
p32emu, so self-checking programs that test exact interrupt timing fail
on purpose. irq_test halts with r1 = 3 under -c and r1 = 8 under -C:
$ ./p32emu irq_test.bin HALT at 000001F0, r1=00000000 (169 instructions, 98 idle ticks)
$ ./p32uemu irq_test.bin HALT at 000001F0, r1=00000000 (169 instructions, 582 cycles)
$ ./p32uemu -c irq_test.bin HALT at 000001F4, r1=00000003 (23 instructions, 77 cycles)
$ ./p32uemu -C irq_test.bin HALT at 000001F4, r1=00000008 (92 instructions, 319 cycles)
(-u p32ucode.bin omitted.) Programs that depend only on correct
behaviour, not exact timing, pass in every mode: mt_demo exits 0 under
all three, after 8,215, 104,978 and 9,553 instructions.
13.7 What p32uemu deliberately does not know¶
- Instruction semantics. There is no
ADD, branch orCALLin the C code. It only knows how to move a value from a source to a destination, how the ALU computes each function, the immediate formats, and the memory card rules. WhatADDmeans is defined by the sequence of microwords. A wrong microword gives a wrong result, which the cross-check catches. - What it does take from the ISA tables is what the hardware also has in
tables: the decode trap uses
p32_fmt,p32_zmaskandp32_priv(the CPU card's decode flash,dec_op.bin, holds the same data); load and store size and signedness come from the opcode (the same flash's SIZE/SIGNED bits); the CR table comes fromp32uc.h. It does not read thedec_*.binimages themselves. - The ALU is behavioural: its result is on R from the cycle after
ALUGO, as the module's pipeline gives it. The ALU module's FPGA is verified against the same functions separately (§14.3).
14. Keeping the two honest¶
14.1 The state-trace cross-check¶
For every one of 20 test programs (hello, isa_test, echo, fib,
primes, trap_test, kernel_hello, kernel_fault, irq_test,
wait_nowake, mt_demo, vm_test, vm_demo, and the seven exc_*),
run_tests.sh runs both emulators with the same input and requires the
following to be identical:
- the exit status,
- the console output, and
- the
-sstate trace, line by line: PC, mode, r1–r15, the other r15, STATUS, EPC, CAUSE and BADADDR at the start of every instruction.
If they differ, the test reports the first differing line. io_test,
lib_test and the scripted monitor session also require identical disk
images afterwards. session1 runs the whole P32W Unix session on
p32uemu against the same expected transcript. Both emulators include the
same p32io.h and tick the timer at the same points, so any difference
comes from the CPU: either the microcode or the C model of a card.
The trace runs at instruction boundaries, so the comparison is exact at the architectural level but does not cover the cycle count.
14.2 Shared sources¶
p32opc.h and p32uc.h are generated from p32isa.py and p32ucode.py,
and the assembler imports p32isa.py directly. The emulators, the
assembler, the microcode and the decode flash therefore cannot disagree
about an encoding. make test also checks that the generated instruction
reference (docs/instruction-reference.md) and the bus definition
(docs/bus.md, from generators/p32bus.py, checked against the microcode)
are up to date.
14.3 The other checks against the emulators' functions¶
The FPGA ALU's Verilog is run against the emulator's ALU functions, all 19,
with the results sampled when the bus needs them (skipped without
iverilog). Simulation of the CPU card at gate level, against p32uemu's
microcycle counts, comes with the CPU card's netlist.
15. Limits and differences from the hardware¶
Timing
p32emuhas no cycles. Its timer ticks per instruction (≈ 1 MIPS of emulated time; the hardware runs ≈ 0.94 MIPS under P32W Unix), so interrupts land at different instructions than on the hardware.p32uemu's microcycle counts are those of the microcode, but only-Cmakes the timer cycle-exact. The run time on the real machine is cycles ÷ 3 MHz. Wait states appear only with-W, the same number on every bus cycle; flash timing and the ALU's internal timing are not modelled, and every other microcycle is one clock.- Devices take no time: no baud rate, no disk access time, no BSY
(
-Gcounts disk commands and console bytes, for adding them on, §4.7). Software that works only because a device is fast will fail on the hardware. Software that follows the status bits will work. - Interactive idle runs in real time; non-interactive idle skips ahead.
Under
-Cidle spins at full host speed.
Not modelled
- IRQ5 (RTC) is never asserted, and the front panel raises no interrupt. The RTC's alarm, periodic and update interrupts, UIP, and register C flags are absent.
- UART FIFOs, line errors, break, modem-control effects; THR is always empty.
- The disk card's drives: only the commands in §11 and LBA addressing; the alternate status reads Status without touching INTRQ, and the device-control register is not modelled. The disk card's DMA engine is not modelled.
- The video card draws no picture (
-Vwrites the screen at exit; the debugger's page shows it live). The mouse moves only by-Km. Its chip's model stops short of the chip (the head ofp32acrtc.h). - The front panel, the debug card (its host link and logic analyzer;
-Dstands in for a DMA master), in-place programming of the boot flash; reset other than power-on (software restarts by jumping to the monitor). - Power-on contents: RAM and the RTC's RAM start as zeros, the clock as host
time or
-R. p32uemu -C: the RTC does not advance, because onlyio_tickandio_waitmove emulated time, and-Cuses neither.WAITwith only a UART interrupt enabled (timer off) stops the emulator instead of waiting for input.
Sources: emulator/p32emu.c, emulator/p32uemu.c, emulator/p32io.h,
emulator/Makefile, emulator/test/run_tests.sh, microcode/p32ucode.py,
microcode/p32isa.py, tools/p32fs.py,
tools/p32term.py, software/monitor/monitor.asm,
software/tests/run_session.sh; decisions D6, D7, D10, D11, D13, D14,
D17, D18, D20, D23, D26, D43, D45, D50, D52, D54.