PIC18 public-core vertical slice — progress
Branch: agent/pic18-core
Base revision: 8e3b5ee4d
Status: standard instruction set complete; first part onboarded at L1.
The PIC18F4520 (configs/chips/pic18f4520.yaml, examples/pic18f4520) boots
real SDCC 4.4.0 C firmware with EUSART TX and the PORTB latch. No other PIC18
part is claimed, no private chip data is copied, and no Proteus parity is
claimed.
Landed
- Public
labwired_config::Arch::Pic18with serde spellingspic18andpic18f; existing architecture variants and aliases are unchanged. - Public
labwired_core::Arch::Pic18. MachineFamily::Pic18andmachine_family()mapping.elf_arch(204)recognisesEM_MCHP_PIC, the registered ELF machine value for Microchip's 8-bit PIC family (PIC18 is the only 8-bit PIC core modelled here).cpu/pic18.rs: reset, byte-addressed program fetch, 21-bit byte PC with bit 0 always clear, WREG/STATUS/BSR, banked data memory and Access Bank resolution, SFR window, PCL/PCLATH/PCLATU, FSR0-2 (including the indirect aliases), TBLPTR/TABLAT/PROD, and a hardware return stack.Cputrait integration, runtime snapshot variant, trace observer events, register names/IDs, builder and node hooks, loader entry-point alignment check, and WASM/loader/CLI match arms (WASM execution is explicitly unavailable for PIC18 at this milestone).
Implemented instruction semantics
Encodings follow the PIC18 standard instruction set. Any word outside this set
returns SimulationError::DecodeError(pc); no opcode is guessed.
| Group | Instructions | Notes |
|---|---|---|
| Control | NOP, SLEEP, CLRWDT, RESET, DAW, PUSH, POP |
0x0000 and the 1111 xxxx xxxx xxxx NOP form; SLEEP sets RCON.TO, clears PD and idles until an enabled flag; CLRWDT sets TO/PD (no WDT); RESET restores the reset SFR state and keeps program memory |
| Literal | MOVLW, MOVLB, RETLW, ADDLW, SUBLW, ANDLW, IORLW, XORLW, MULLW |
MOVLB selects BSR<3:0>; SUBLW is k - W; MULLW writes PRODH:PRODL |
| Byte move | MOVWF, MOVF, MOVFF |
MOVFF is two words and uses a 12-bit absolute source/destination, ignoring banking; either side may be an indirect alias |
| Byte ALU | ADDWF, ADDWFC, SUBWF, SUBWFB, SUBFWB, ANDWF, IORWF, XORWF, COMF, NEGF, CLRF, SETF, INCF, DECF, SWAPF, MULWF |
d and a bits honored; borrow forms subtract !C |
| Rotate | RLCF, RLNCF, RRCF, RRNCF |
the C forms rotate through carry |
| Skip | CPFSEQ, CPFSGT, CPFSLT, TSTFSZ, DECFSZ, DCFSNZ, INCFSZ, INFSNZ |
unsigned compares; no flags; a skipped two-word instruction is skipped as a unit |
| Table | TBLRD*, *+, *-, +*; TBLWT*, *+, *-, +* |
reads flash, user ID and CONFIG bytes (unimplemented memory reads 0); TBLWT only moves TBLPTR, since no EECON1 write sequence is modelled |
| Bit | BCF, BSF, BTG, BTFSC, BTFSS |
skip instructions skip a following two-word instruction as a unit |
| Relative | BRA, RCALL, BZ, BNZ, BC, BNC, BOV, BNOV, BN, BNN |
signed offsets in instruction words |
| Absolute | two-word GOTO, two-word CALL |
20-bit literal is a word address (k -> PC<20:1>) |
| Stack | RETURN/RETURN FAST |
CALL FAST saves WREG/STATUS/BSR for the matching fast return |
| Interrupts | RETFIE/RETFIE FAST |
vectors at 0x0008/0x0018, INTCON/INTCON2/INTCON3, PIR1/PIR2/PIE1/PIE2/IPR1/IPR2, RCON.IPEN, GIE/GIEH/GIEL/PEIE; entry saves and FAST restores the fast register stack |
| FSR literal | LFSR |
12-bit literal loaded into FSR0/FSR1/FSR2 |
| Indirect | INDF0/1/2, POSTINC0/1/2, POSTDEC0/1/2, PREINC0/1/2, PLUSW0/1/2 |
Virtual Access Bank registers used by byte/bit file operations; full 12-bit FSR address, independent of BSR and a |
Flags
STATUS models the classic PIC18 flag bits: C=0, DC=1, Z=2, OV=3,
N=4; upper bits read 0. Exact rules:
ADDWF,INCF: carry out of bit 7, digit carry out of bit 3, signed overflow from same-sign operands, zero and sign of the result.DECF: borrow polarity follows PIC18 subtraction (C/DCset when no borrow), signed overflow from opposite-sign inputs, zero and sign.ADDWFC,ADDLW: asADDWF, with carry-in forADDWFC.SUBWF,SUBLW,SUBWFB,SUBFWB,NEGF: subtraction polarity asDECF; the borrow forms subtract!C;NEGFis0 - f.ANDWF,IORWF,XORWF,MOVF,COMF,ANDLW,IORLW,XORLW,RLNCF,RRNCF:ZandNonly.RLCF,RRCF:C,Z,N.DAW:Cset when the adjust carries out (never cleared).CLRF:Zonly.MOVWF,MOVFF,SETF,SWAPF,MULWF,MULLW, the skip forms,BCF,BSF,BTG: no flags.- Instructions that affect flags do not write their result into
STATUS; they update only the flag bits, matching the datasheet destination rule.
Interrupts
The core samples enabled interrupt sources between instructions (after the current instruction retires) and pushes the next instruction as the return address. Sticky flags live in the SFR file and are cleared by the ISR.
RCON.IPEN = 0: every source vectors to0x0008;INTCON.GIE(bit 7) is the global enable and is cleared on entry. ThePIR1/PIR2peripheral sources additionally needINTCON.PEIE(bit 6); theINTCON/INTCON3sources need onlyGIE.RETFIEsetsGIEagain.RCON.IPEN = 1: high-priority sources vector to0x0008throughGIEH(bit 7), low-priority sources to0x0018throughGIEHandGIEL(bit 6) —GIEH = 0disables all interrupts (DS39631 Register 9-1). Entry clears only the matching global bit, so a high-priority source may nest inside a low-priority ISR.RETFIEsetsGIEHif it is clear (leaving a high ISR) andGIELotherwise, so a high ISR nested in a low one returns withGIELstill clear.- Sources:
INTCONTMR0/INT0/RB,INTCON3INT1/INT2, and the eight bits ofPIR1/PIE1/IPR1andPIR2/PIE2/IPR2. INT0 is always high priority whenIPEN = 1; the other source priorities come fromINTCON2,INTCON3or the matching IPR bit.INTCON3follows the PIC18F4520 layout (DS39631 Register 9-3): INT2IP=7, INT1IP=6, INT2IE=4, INT1IE=3, INT2IF=1, INT1IF=0. - Interrupt entry loads WREG/STATUS/BSR into the one-level fast register
stack (the same shadow
CALL FAST/RETURN FASTuse), andRETFIE FASTrestores them. A high-priority entry nested in a low-priority ISR overwrites that save, as on silicon. Per-device vector remapping is not modelled. -
No peripheral raises a flag yet.
interrupt_requestreads the CPU's internal SFR array only, and no bus peripheral writesPIRx/INTCONx, so today the only way to pend a source is firmware (or a test) writing the flag bit.Cpu::set_exception_pendingis recorded but not consumed. -
Default model: 16 banks x 256 bytes (
0x000..0xFFF) and a 64 KiB flash. A per-device geometry can be supplied withPic18::with_geometry;Pic18::for_chip_namelooks it up in thePIC18_DEVICEStable. a = 1:BSR<3:0>:f.a = 0:fbelow the device's Access Bank split addresses access RAM atf;fat or above it addresses the SFR window at0xF00 + f(so0xE8is WREG at0xFE8). The split is0x80for parts whose SFRs start at0xF80(PIC18F4520) and0x60for the K-series and unlisted names.- Modeled SFRs: WREG, STATUS, BSR, FSR0L/H, FSR1L/H, FSR2L/H, PRODL/H, TABLAT, TBLPTRL/H/U, PCL, PCLATH, PCLATU, STKPTR, TOSL/H/U. Other SFR addresses are plain bytes in the file for now.
- Indirect aliases are the classic Access Bank virtual registers in three
groups:
0xFDB..0xFDF(FSR2),0xFE3..0xFE7(FSR1) and0xFEB..0xFEF(FSR0), each orderedPLUSW,PREINC,POSTDEC,POSTINC,INDF. This matches DS41303F §5.4.3.1/§5.4.3.3, including the INDF1 example at0xFE7. Byte-oriented and bit-oriented file operations resolve through the same helper. INDFnreads/writesFSRnwithout changing it.POSTINCn/POSTDECnaccess the current address and then move the pointer by one;PREINCnmoves before the access. A read-modify-write instruction (ADDWF,INCF,BCF, ...) resolves the operand once, writes the original location, then applies the pointer update once.PLUSWnadds the signed 8-bit WREG offset and changes neither WREG norFSRn.- If an indirect access resolves to any of the 15 virtual operand addresses
above, the virtual register is not physically implemented: an indirect read
returns
0x00, and an indirect write is aNOP(DS41303F §5.4.3.3). A rejected write also drops the selected operand's post-action, because no access is committed in this model. - If an indirect write resolves to an FSR pair (
FSR0L/H,FSR1L/H,FSR2L/H), the byte is stored in the FSR, but the selected operand'sPOSTINC/POSTDECis not applied.PREINChas already run before the target is known. This is the documented self-target interaction, e.g.POSTDEC2targetingFSR2Lstores the value without decrementing. - FSR addresses and updates wrap in the 12-bit data space. The direct SFR read/write helpers still expose the raw file bytes; indirect expansion and virtual-target filtering are instruction-operand features.
MOVFFtakes 12-bit absolute operands (no BSR) and expands the virtualFSRnaliases on either side: the source is read and its FSR updated, then the destination is resolved and written. The evidence is compiled code: SDCC 4.4.0's pic16 port builds its software stack fromMOVFF r, POSTDEC1,MOVFF PREINC1, randMOVFF PLUSW2, r, and the PIC18F4520 smoke (examples/pic18f4520) only prints its banner if those moves are indirect.TOSL/H/Uremain direct hardware-stack mirrors; indirect-target semantics for those stack registers are not claimed. The virtual-operand rule above is limited to the documented 15 INDF/POST/PRE/PLUS addresses.
Program counter and return stack
- The PC is byte addressed;
set_pc/snapshot restore clear bit 0. - Single-word instructions advance by 2;
CALL,GOTO,MOVFF,LFSRadvance by 4. - A write to
PCLcombines PCLATU/PCLATH/PCL and aligns the target. ADDWF PCL,fis a computed GOTO from the instruction's own address: the low-byte addition does not propagate into the upper PC bytes, and readingPCLlatches the current PCLATH/PCLATU.MOVFF ... ,PCLuses the latches.- Return stack defaults to 31 levels (classic parts). Push past full sets
STKFULand overwrites the top entry; pop from empty setsSTKUNFand returns 0. The pointer never wraps circularly.STVRENreset behavior is not modelled yet.
Tests and results
Commands run for the first vertical slice (d6402f376). This table is a
record of that commit; it predates the PIC18F4520 firmware smoke, and the
unit test count has grown since (cargo test -p labwired-core --lib cpu::pic18):
| Command | Result |
|---|---|
cargo test -p labwired-core pic18 |
PASS: 37 PIC18 unit tests; other test binaries filtered to 0 |
cargo test -p labwired-core --lib cpu::pic18 |
PASS: 37 passed; 0 failed |
cargo fmt --all -- --check |
PASS |
XC8 local check (command -v xc8-cc / xc8, repository fixture search) |
No XC8 compiler on PATH and no PIC18 ELF/HEX fixture in the worktree; no real firmware smoke is claimed |
cargo test -p labwired-core one_arch_policy |
PASS: 8 passed; 0 failed |
cargo test -p labwired-core --test session_catalog |
PASS: 1 passed; 0 failed |
cargo test -p labwired-core --test register_compliance |
PASS: 1 passed; 0 failed |
cargo test -p labwired-core --test register_coverage |
PASS: 1 passed; 0 failed |
cargo test -p labwired-config --lib --tests |
PASS: 265 passed across 9 binaries; 0 failed |
cargo test -p labwired-config |
PASS: 265 passed (including 0 doctests); 0 failed |
cargo test -p labwired-loader |
PASS: 57 passed across 4 binaries; 0 failed |
cargo check -p labwired-cli -p labwired-wasm -p labwired-loader |
PASS |
cargo check --workspace --all-targets |
Blocked by a pre-existing unrelated failure: firmware-semihost-smoke uses host-invalid inline assembly registers r0/r1 |
The PIC18 tests cover reset and PC alignment, Access Bank vs BSR addressing,
INDF0/1/2 through their FSRs with a conflicting BSR, FSR1/FSR2 post-increment,
POSTINC read/write and read-modify-write pointer movement, POSTDEC/PREINC
ordering, PLUSW positive and negative signed offsets without pointer/WREG
changes, 12-bit pointer wrap, indirect bit operations, reads/writes of all
15 virtual operand aliases (0x00 read, NOP write), the FSR-pair
write/post-action suppression (including self-targeting POSTDEC2), exact
C/DC/Z/OV/N cases, logic/CLRF/MOVF flag rules, MOVFF absolute
addressing, two-word GOTO/CALL/RETURN, LFSR, conditional branches,
BTG/BTFSS, RCALL/RETLW, skip over a two-word instruction, return-stack
overflow/underflow, PCL latch writes, MOVFF to PCL, ADDWF PCL computed
jumps with and without low-byte carry, unknown-opcode DecodeError, snapshot
round-trip, program image loading, and a hand-assembled add/store/GOTO program.
The interrupt tests cover GIE masking, PEIE gating of peripheral sources
with IPEN = 0, the high/low vectors and GIEH/GIEL gating (including
GIEH = 0 masking low-priority sources), INTCON TMR0/INT0 sources, the
INTCON3 INT1/INT2 bits pinned to raw datasheet values, PIR1/PIE1/IPR1
priority, RETFIE re-enabling, and RETFIE FAST restoring the context
saved on interrupt entry (no hand-seeded shadow). cpu_trace_conformance
covers Pic18 like every other core. At that commit no catalogue part was
onboarded; the PIC18F4520 L1 slice (examples/pic18f4520) now boots a real
SDCC-built firmware image.
Commit: d6402f376 (feat(core): add PIC18 vertical slice).
Follow-ups: 75f859ce9 (fix(pic18): let MOVFF to PCL update the PC),
7f81f318b (progress note), c718f35a8
(test(pic18): cover PCL jumps and remaining bit/return ops), and
d56049d17 (feat(pic18): implement FSR indirect addressing), plus
3fb236eec (fix(pic18): honor virtual indirect target rules).
Next missing pieces
- Loader / firmware toolchain. SDCC 4.4.0 + gputils HEX output is
wrapped byte for byte in an
EM_MCHP_PICELF (examples/pic18f4520/ firmware/hex2elf.py) and loads through thePT_LOADbyte-address path, CONFIG words included. A native XC8 ELF still needs verifying against a pinned XC8 release. - Interrupts. The basic controller is in: vectors
0x0008/0x0018,INTCON/INTCON2/INTCON3,PIR1/PIR2/PIE1/PIE2/IPR1/IPR2,RCON.IPEN,GIE/GIEH/GIEL/PEIE,RETFIE/RETFIE FAST, and the fast register stack saved on entry. Still missing: peripherals that raisePIRxflags, per-device vector remapping, interrupt latency/cycle accounting, and theRCONreset-source/STVRENbehavior tied to the vector path. - Peripherals. In: EUSART TX (
TXREGto the console, read-onlyTRMT/TXIF),PORTA..E/LATx/TRISxwith reset directions (input pins read 0),SLEEPwake, andRCON.TO/PD. Missing: pad masks and external pin drive, analog/PPS, oscillator andCONFIGdecoding, TMR0/1/2/3, CCP/ECCP, MSSP, EUSART RX and baud timing, ADC, comparators, WDT, EEPROM, and reset sources. - Full ISA. The standard set is complete. Still missing: the
XINSTextended set and indexed-literal addressing, and per-instruction cycle counts for skips over two-word instructions (counted as 2, not 3). - Data-memory completeness. Expanded (>4 KiB) data memory and wider FSR
pointers, direct
FSRnHfield masking (indirect paths already wrap at 12 bits), unmodeled SFR reset values, indirect-target behavior forTOSL/H/U, andMOVFFL. The per-device Access Bank split and flash size come from thePIC18_DEVICEStable (Pic18::for_chip_name;0x60and 64 KiB for an unlisted name). - Device geometry and integration. The PIC18F4520 has a chip YAML,
geometry, console wiring (
labwired testand the interactive run) and a real end-to-end firmware run.PIC18_DEVICESadds the geometry of 157 more parts whose SFR map is identical to the PIC18F4520's for every register and bit the model implements (checked per part against the SDCC 4.4.0 device headers; flash from the gputils linker scripts, split from SDCCpic16devices.txt;scripts/pic18/check_sfr_map.py --check-tablere-verifies every row given a local SDCC 4.4.0 and gputils 1.4.0; neither is vendored, so CI does not run it). A table row is geometry only: a part still needs its own descriptor and a firmware run before it counts as supported, and support must not be inferred from a family label. J11/J13/J50/J53, K40/Q10, 13K50/14K50 and 1220/1230/1320/1330 are left out because their EUSART, IOC or port map differs. RAM/bank-count limits, pins absent from smaller packages, andSTVRENremain unmodelled.