Input and serial fixes.
This commit is contained in:
parent
89631006f2
commit
91629dc5eb
12 changed files with 408 additions and 49 deletions
232
LLVM816-ASKS.md
232
LLVM816-ASKS.md
|
|
@ -213,3 +213,235 @@ and #$ff`. ABI verified from a compiled probe: pointer lo16 in A, bank in X,
|
|||
return in A, $e0-$e3 imaginary-register scratch. Any OTHER volatile I/O read
|
||||
through a variable pointer (input/video HALs, future code) is still exposed
|
||||
until the codegen fix lands.
|
||||
|
||||
## 8. Peeled-first-iteration address negation is one's complement, not two's (2026-08-20) -- FIXED IN TOOLCHAIN same day (see end of item)
|
||||
|
||||
A loop that indexes a local array at a base computed by SUBTRACTING a
|
||||
uint8_t parameter (`k = 8u - nb;` then `t[(uint8_t)(k + i)]` inside a
|
||||
counted loop) is compiled with the FIRST iteration peeled, and that
|
||||
peeled iteration's address is computed as
|
||||
|
||||
base + sign_extend(~nb) ; one byte LOW: ~nb = -nb - 1
|
||||
|
||||
where two's complement (`-nb`) was needed. Every later iteration
|
||||
increments correctly, so exactly the first element is read one byte
|
||||
low and the remaining seven are right -- a silent wrong-VALUES class
|
||||
with no crash, in the same derived-induction-variable family as the
|
||||
modulo-advance item.
|
||||
|
||||
The emitted idiom (RetroNet src/crypto/rnSipHash.c sipRot, w65816
|
||||
-O2 -ffreestanding -ffunction-sections, disassembly of the peeled
|
||||
first read's address):
|
||||
|
||||
lda 0xb6,s ; nb
|
||||
eor #0x1aff ; low byte: nb ^ 0xff = ~nb (high byte junk)
|
||||
and #0xff
|
||||
eor #0x80 ; sign-extend byte
|
||||
clc
|
||||
adc #0xff80 ; (x ^ 0x80) - 0x80
|
||||
...
|
||||
tsc
|
||||
clc
|
||||
adc #0xa3 ; t[] frame base (t + 8 region)
|
||||
clc
|
||||
adc 0x1f,s ; += sext(~nb) --> t + 8 - nb - 1 = t[k-1]
|
||||
|
||||
Blast radius measured before the workaround: EVERY SipHash-2-4 tag the
|
||||
IIgs client computed was wrong (the rotate primitive feeds every round),
|
||||
so every sealed uplink failed server-side authentication. Nothing
|
||||
client-side ever caught it because the protocol's selective encryption
|
||||
leaves downlink frames plaintext -- silent until a peer verifies.
|
||||
Root-caused via on-target known-answer tests + a per-primitive state
|
||||
probe (SIPBLOCK) diffed against the host build of the same source; the
|
||||
"first iteration reads index k-1" model reproduces both on-target wrong
|
||||
tags bit-for-bit end to end.
|
||||
|
||||
WORKAROUND LANDED IN RETRONET: sipRot now takes k (the precomputed read
|
||||
base) instead of nb, removing the 8-nb negation from the function; the
|
||||
emitted first-iteration address becomes a plain base + k and all KATs
|
||||
pass on target. Any other loop whose peeled first iteration derives
|
||||
its address from a subtracted narrow parameter is still exposed.
|
||||
|
||||
RESOLUTION (2026-08-20): root cause was NOT the address lowering -- the
|
||||
IR and ISel were correct (a true NEGA8 pseudo). The AsmPrinter expanded
|
||||
NEGA8 as an UNWRAPPED `EOR #$FF ; INA` on a false "already 8-bit M"
|
||||
assumption: in the real 16-bit-M context the CPU decoded the 2-byte
|
||||
EOR #imm8 as 3-byte EOR #imm16 and swallowed the INA opcode ($1A) as the
|
||||
immediate's high byte -- the disassembly tell is `eor #$1AFF`. FIXED in
|
||||
W65816AsmPrinter.cpp: NEGA8 now emits emitSepM/emitRepM around the pair,
|
||||
exactly like LDA8absX/STA8absX (safe: SepRepCleanup's isMNeutral never
|
||||
extends an M=8 window across the pseudo). Audited: NEGA8 was the only
|
||||
unwrapped Imm8 expansion. Blast radius before the fix: 6 sites in the
|
||||
RetroNet IIgs client -- the SipHash rotate (every sealed uplink failed to
|
||||
authenticate) and 5 rnWidget functions (index corruption; wild-jump BRK
|
||||
during the download offer frame; likely the phantom-FORM garbage too).
|
||||
|
||||
Original ask (kept for the repro): compile the pre-workaround sipRot
|
||||
shape below at -O2 for w65816 and inspect the first read's address chain.
|
||||
(Verbatim pre-workaround function; gSipV is a 40-byte global.)
|
||||
|
||||
```c
|
||||
static void sipRot(uint8_t a, uint8_t nb, uint8_t mode) {
|
||||
uint8_t t[16];
|
||||
uint8_t i;
|
||||
uint8_t k;
|
||||
uint8_t x;
|
||||
|
||||
for (i = 0u; i < 8u; i++) {
|
||||
x = gSipV[(uint8_t)(a + i)];
|
||||
t[i] = x;
|
||||
t[(uint8_t)(i + 8u)] = x;
|
||||
}
|
||||
k = (uint8_t)(8u - nb);
|
||||
for (i = 0u; i < 8u; i++) {
|
||||
x = t[(uint8_t)(k + i)];
|
||||
if (mode == 3u) {
|
||||
x = (uint8_t)((x >> 3) | (uint8_t)(t[(uint8_t)(k + i + 1u)] << 5));
|
||||
} else if (mode == 1u) {
|
||||
x = (uint8_t)((x << 1) | (uint8_t)(t[(uint8_t)(k + i - 1u)] >> 7));
|
||||
}
|
||||
gSipV[(uint8_t)(a + i)] = x;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. i8 store to a 1-byte stack slot is a 16-bit STA d,s that overflows the return address (2026-08-20) -- FIXED IN TOOLCHAIN same day
|
||||
|
||||
M=16 is the default mode, so `sta d,s` writes TWO bytes. When the target
|
||||
frame object is only 1 byte (an i8 spill / i8 arg-save) at the TOP slot of a
|
||||
frame, the second byte lands on the caller's saved RETURN ADDRESS low byte and
|
||||
the callee's RTL jumps mid-instruction into a different function. Live proof:
|
||||
RetroNet's rnClientOnFileRecord saving its uint8_t `op` clobbered its own
|
||||
return 0x80->0x01, so its RTL landed inside a different rasterizer switch case,
|
||||
skipped the caller's arg cleanup, and left the DISPLAY record-walk's stack
|
||||
pointer 4 low -> $F6 (big-frame pointer) wrong -> the walk byte-crawled forever
|
||||
on the offer frame (the IIgs download "offer-silence": FILE_OFFER never
|
||||
processed, client never drained the serial).
|
||||
|
||||
Minimal repro: `void fwd(uint8_t op, const uint8_t *r){ sink(op, r); }` at -O2
|
||||
for w65816 emits `sta 0x2, s` (16-bit) to save op adjacent to the return.
|
||||
|
||||
FIX (W65816RegisterInfo.cpp eliminateFrameIndex, STAfi case): when
|
||||
`MFI.getObjectSize(FI) == 1`, wrap the STA_StackRel in SEP #$20 / REP #$20 so a
|
||||
1-byte slot gets a genuine 8-bit store. Only the STORE is a hazard; a 16-bit
|
||||
LOAD of the extra byte is a harmless read (the high byte of an i8 value is
|
||||
ignored), so LDAfi is left 16-bit. Verified: fwd now emits
|
||||
`sep #$20 ; sta 0x1,s ; rep #$20`. Same defect family as ASKS #8 (NEGA8) and
|
||||
the $F8 far-frame bank byte -- "an 8-bit operation emitted without the mode
|
||||
wrap in default-16-bit-M code."
|
||||
|
||||
## 10. Pointer-deref to a GLOBAL hardcodes bank 0 (STZ $E2) under the GS/OS Loader (2026-08-20) -- FIXED IN TOOLCHAIN 2026-08-21 (provenance-based; see end)
|
||||
|
||||
The LDAptr / STAptr / STBptr custom inserter (W65816ISelLowering.cpp) lowers a
|
||||
[dp],Y indirect-long deref with the BANK byte forced to 0 via `STZ $E2` (unless
|
||||
the LoaderBankDeref path, which uses $BE, is taken). Bank-0 is correct for a
|
||||
pointer to a STACK local (the 65816 stack is always bank 0) but WRONG for a
|
||||
pointer to a GLOBAL: under the GS/OS Loader the program's BSS/globals are placed
|
||||
in the program bank ($BE, e.g. $0A), not bank 0. So `&someGlobal` dereferenced
|
||||
through this path reads bank-0 garbage, while the SAME global accessed directly
|
||||
via `sta/lda abs` (DBR-relative, DBR=$BE) is correct -- a silent, layout-
|
||||
dependent miscompile. The inserter's own comment already flags this tension
|
||||
(gmtime's __gmtimeBuf hit it).
|
||||
|
||||
Live proof: RetroNet's inlined rnXferRequest materialised `&gX.id` (a static
|
||||
global) and read it through LDAptr; the FILE_REQUEST then carried xferId=0xAD
|
||||
(bank-0 garbage) instead of 1, and the server rejected the download. 0xAD is
|
||||
not any valid gX field -- the tell of a wrong-bank read. Disasm:
|
||||
`lda #<gX.id off> ; sta $e0 ; stz $e2 ; lda [$e0],y`.
|
||||
|
||||
WORKAROUND in app: `noinline` on the function so the global is read in the
|
||||
caller through DBR-relative abs16 and passed by the ABI (same shape as the
|
||||
existing rnXferState workaround).
|
||||
|
||||
Ask: the deref bank cannot be a blanket 0 or blanket $BE when stack pointers
|
||||
(bank 0) and global pointers ($BE) are both derefed through one pseudo. The
|
||||
principled fix is a 24-bit pointer representation (carry the bank in the pointer
|
||||
value) or provenance tracking so a global-derived pointer derefs with $BE and a
|
||||
stack-derived pointer with 0.
|
||||
|
||||
Fixes EVALUATED and rejected from the app side (2026-08-20):
|
||||
1. -w65816-loader-bank-deref (existing flag): whole-program, flips EVERY
|
||||
LDAptr to $BE. Fixes global derefs but breaks stack-pointer derefs (the
|
||||
reason LDAptrBank0 exists for va_arg). Also the runtime *.o are shared
|
||||
across Loader AND non-Loader (GNO/smoke) builds, so it can't be enabled
|
||||
for just the S16/Loader target without a per-mode runtime split.
|
||||
2. Fold (load <materialized global addr>) -> abs16+DBR at selection -- the
|
||||
clean targeted fix, but NOT minimally reproducible: every small repro
|
||||
(inlined forwarder, barrier-forced re-read, struct field) correctly
|
||||
selects abs16. The mis-selection only appears inside a very large inlined
|
||||
function (RetroNet rnFileRecord, ~0x916 bytes, three transfer routines
|
||||
inlined), so it is a scheduling/CSE artifact of address materialization
|
||||
under register pressure, not a stable pattern -- hard to fix safely
|
||||
without provenance.
|
||||
RESOLUTION (2026-08-21, W65816ISelLowering.cpp LDAptr/STAptr/STBptr custom
|
||||
inserter): instead of a blanket bank policy, use PROVENANCE. When the pointer
|
||||
being derefed traces (through COPYs) to an LDAi16imm of a global / external
|
||||
symbol, the deref is exactly a DBR-relative absolute access to that global --
|
||||
the same thing a direct `global` reference compiles to -- so emit LDAabs /
|
||||
STAabs / STA8abs with the global operand and skip the [dp],Y deref entirely.
|
||||
That lands in DBR's bank (correct for a global) with no bank guess, and only
|
||||
GLOBAL-address derefs are touched -- stack pointers (bank 0) and heap/other
|
||||
pointers keep the existing deref path, so no stack-out-param regression. NB the
|
||||
bank canNOT be sourced from $BE: $BE is the code segment's PBR and in a
|
||||
multi-segment program the data (BSS) segment is a different bank; only DBR (via
|
||||
abs) is right. Single-segment builds are unaffected (DBR==0==old STZ bank).
|
||||
Verified: RetroNet's rnXferRequest/rnXferState no longer need `noinline` (both
|
||||
removed) and the inlined rnFileRecord emits zero bank-0 pointer-derefs; the IIgs
|
||||
download DISK-VERIFIES from the pure-clean build. Also fixes the runtime gmtime
|
||||
__gmtimeBuf case this item's parent note describes. What is still NOT covered:
|
||||
a global address that flows through pointer ARITHMETIC the trace can't follow
|
||||
(e.g. `p = &g; p += i; *p`) -- there the provenance is lost and the deref falls
|
||||
back to bank 0; those remain candidates for the 24-bit-pointer rework.
|
||||
|
||||
## 11. ADDframe (stack-slot LEA) scheduled between a CMP and its branch clobbers the flags (2026-08-24) -- FIXED IN TOOLCHAIN same day
|
||||
|
||||
`ADDframe` - the LEA-equivalent that puts the address of a stack slot in A -
|
||||
expands at PEI (W65816RegisterInfo.cpp eliminateFrameIndex) to `TSC; CLC;
|
||||
ADC #disp`. That sequence rewrites N/Z (TSC, ADC), C (CLC, ADC) and V (ADC).
|
||||
The pseudo deliberately carries no `Defs = [P]` (the same reasoning as the
|
||||
LDA pseudos, see the NOTE above LDAi16imm in W65816InstrInfo.td) and is
|
||||
`isReMaterializable`, so a post-RA pass may legally place it between a CMP
|
||||
and the Bxx that consumes the CMP's flags. Machine Copy Propagation did
|
||||
exactly that in RetroNet's src/platform/rnClient.c (main, the widget action
|
||||
dispatch):
|
||||
|
||||
lda 0xf,s ; act.kind
|
||||
cmp #0x3 ; == RN_WA_SUBMIT ?
|
||||
tsc ; \
|
||||
clc ; > ADDframe: &act, hoisted here for the SUBMIT arm
|
||||
adc #0xc1 ; /
|
||||
bne .LBB3_61 ; tests (SP+0xC1) != 0 - ALWAYS taken
|
||||
|
||||
so `act.kind == RN_WA_SUBMIT` could never be true: the LOGIN button
|
||||
activated, rnWidgetActivate wrote kind=3, and rnClientOnSubmit was never
|
||||
reached. On the IIgs the login form was never submitted; the harness's
|
||||
Tab/Return retry loop then produced exactly nine no-widget Return uplinks,
|
||||
which is what pinned it. A scan of every TU in the IIgs client found this
|
||||
ONE instance (`cmp` immediately followed by `tsc` before the branch), which
|
||||
is why only the submit was affected.
|
||||
|
||||
The existing mitigation - the PHP/PLP wrap in W65816StackSlotCleanup.cpp
|
||||
Pass -2.5 - did not catch it for two reasons: (1) ADDframe was not in the
|
||||
pass's corrupting set (isLdaLike), so the backward walk from the branch hit
|
||||
it, took isFlagDefining's default ("anything else defines flags") and called
|
||||
it the TEST, then declined to wrap; (2) the pass only collects BEQ/BNE/BMI/
|
||||
BPL, on the (true) grounds that LDA-like ops leave C alone - but ADDframe
|
||||
also clobbers C and V, so a `cmp; ADDframe; bcc` would have been miscompiled
|
||||
with no protection at all.
|
||||
|
||||
RESOLUTION (2026-08-24, W65816StackSlotCleanup.cpp Pass -2.5):
|
||||
* ADDframe is now in isLdaLike (N/Z-corrupting), so the walk continues
|
||||
past it to the real CMP and wraps it: `cmp #3; php; tsc; clc; adc #disp+1;
|
||||
plp; bne`. It was already in isStackRel and the bump list, so its
|
||||
ImmOffset gets the +1 that compensates PHP's S decrement; the FP-relative
|
||||
exemption in the bump is skipped for ADDframe because it has no FP-
|
||||
relative expansion (always TSC+ADC, i.e. always SP-relative).
|
||||
* BCC/BCS/BVC/BVS are collected as well, with a separate
|
||||
isCarryCorrupting predicate (ADDframe only): under a C/V branch an
|
||||
N/Z-only corrupter is walked past without a wrap, so the size of every
|
||||
existing carry-branch sequence is unchanged.
|
||||
* Pinned by llvm/test/CodeGen/W65816/addframe-flag-wrap.mir (nz, carry,
|
||||
and the lda-under-carry no-wrap case).
|
||||
Verified: the rnClient.c TU rescans clean (zero cmp/tsc/branch sequences in
|
||||
all 20 IIgs client TUs) and the IIgs login submits live - see the RetroNet
|
||||
e2e matrix (`joey-iigs session`).
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ typedef enum {
|
|||
#define JL_CHAR_TAB 0x09
|
||||
#define JL_CHAR_RETURN 0x0D
|
||||
#define JL_CHAR_ESCAPE 0x1B
|
||||
// The FORWARD delete (the key labelled Del/Delete on a PC, ST or Amiga keyboard - not backspace). A text
|
||||
// editor needs both: backspace removes the character BEFORE the caret, this one removes the character
|
||||
// UNDER it. 0x7F is ASCII DEL, which is what every one of these keyboards already reports.
|
||||
#define JL_CHAR_DELETE 0x7F
|
||||
|
||||
// Typed-character queue capacity (ring buffer; one slot stays empty,
|
||||
// so JL_CHAR_QUEUE_SIZE - 1 characters can be pending). Must be a
|
||||
|
|
@ -114,7 +118,7 @@ void jlInputPoll(void);
|
|||
|
||||
// Pop the next typed character as a 7-bit code, or -1 if the queue
|
||||
// is empty. Codes: printable ASCII 0x20..0x7E, plus JL_CHAR_BACKSPACE,
|
||||
// JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE. Shift, caps lock, and
|
||||
// JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE, JL_CHAR_DELETE. Shift, caps lock, and
|
||||
// the keyboard layout are already applied by the backend, so
|
||||
// punctuation and shifted symbols arrive correctly on every port.
|
||||
// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS
|
||||
|
|
|
|||
|
|
@ -167,6 +167,8 @@
|
|||
#define JL_HAS_SERIAL_WRITE
|
||||
#define JL_HAS_SERIAL_AVAILABLE
|
||||
#define JL_HAS_SERIAL_FLUSH
|
||||
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||
#define JL_HAS_SERIAL_SET_RTS
|
||||
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create
|
||||
// $2001, storageType 13), disk-free (GS/OS Volume $2008 via prefix 0).
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
|
|
@ -353,6 +355,8 @@
|
|||
#define JL_HAS_SERIAL_WRITE
|
||||
#define JL_HAS_SERIAL_AVAILABLE
|
||||
#define JL_HAS_SERIAL_FLUSH
|
||||
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||
#define JL_HAS_SERIAL_SET_RTS
|
||||
// save files + disk space (POSIX mkdir/remove + int21h/36h disk-free)
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
|
|
|
|||
|
|
@ -101,4 +101,16 @@ int16_t jlSerialReadByte(void);
|
|||
// Discard any buffered received bytes.
|
||||
void jlSerialFlush(void);
|
||||
|
||||
// ASSERT OR DROP RTS, for a caller that does its own receive-side flow control.
|
||||
//
|
||||
// JL_SERIAL_FLOW_RTSCTS asks the PORT to handshake; this is the other half - the application saying
|
||||
// "my buffer is nearly full, stop sending" and later "go ahead", which is what a client with a small
|
||||
// ring and a peer that can be told to pause actually needs. RetroNet's 6551 clients (C64, Apple II)
|
||||
// have done exactly this for a long time and the 16-bit tier could not, so a JoeyLib target's only
|
||||
// defence against an overrun was the packet layer's retransmit.
|
||||
//
|
||||
// `on` = true asserts RTS (ready to receive). A port with no controllable RTS line no-ops, and a
|
||||
// closed port no-ops; neither is an error, so a caller can drive it unconditionally.
|
||||
void jlSerialSetRts(bool on);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ void jlInputCharPush(uint8_t ch) {
|
|||
uint8_t head;
|
||||
uint8_t next;
|
||||
|
||||
if (ch > 0x7E) {
|
||||
// 0x7F is the FORWARD delete, not junk: it is the one code above the printable range the queue
|
||||
// carries, because a text editor needs a key that removes the character under the caret.
|
||||
if (ch > 0x7E && ch != JL_CHAR_DELETE) {
|
||||
return;
|
||||
}
|
||||
if (ch < 0x20 &&
|
||||
|
|
|
|||
|
|
@ -1011,6 +1011,16 @@ void jlpGenericSerialFlush(void);
|
|||
#define jlpSerialFlush() jlpGenericSerialFlush()
|
||||
#endif
|
||||
#endif
|
||||
// Application-driven RTS (jlSerialSetRts). The generic stub does nothing, which is the honest answer
|
||||
// for a port whose RTS line is not controllable - a caller drives this unconditionally.
|
||||
void jlpGenericSerialSetRts(bool on);
|
||||
#if !defined(jlpSerialSetRts)
|
||||
#if defined(JL_HAS_SERIAL_SET_RTS)
|
||||
void jlpSerialSetRts(bool on);
|
||||
#else
|
||||
#define jlpSerialSetRts(_o) jlpGenericSerialSetRts((_o))
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
// --- Save files + disk space (platform-only; blank-port stub generic + JL_HAS override) ---
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@ int16_t jlSerialReadByte(void) {
|
|||
}
|
||||
|
||||
|
||||
void jlSerialSetRts(bool on) {
|
||||
if (!gSerialOpen) {
|
||||
return;
|
||||
}
|
||||
jlpSerialSetRts(on);
|
||||
}
|
||||
|
||||
|
||||
uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len) {
|
||||
if (!gSerialOpen || buf == NULL || len == 0u) {
|
||||
return 0u;
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@
|
|||
// (0xE0 0x1C) and keypad '/' (0xE0 0x35).
|
||||
#define SCAN_ENTER 0x1C
|
||||
#define SCAN_SLASH 0x35
|
||||
#define SCAN_DELETE 0x53 // shared by keypad '.' and the E0-prefixed Delete key
|
||||
|
||||
// Private ISR->poll ring for translated characters. Power of two so
|
||||
// the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes.
|
||||
|
|
@ -351,10 +352,14 @@ static void keyboardIsr(void) {
|
|||
// commands, not text, so they produce no character.
|
||||
ch = 0;
|
||||
if (extended) {
|
||||
// Of the 0xE0 pairs only keypad Enter and keypad '/'
|
||||
// type; the rest (arrows, Home/End, ...) do not.
|
||||
// Of the 0xE0 pairs only keypad Enter, keypad '/' and
|
||||
// DELETE type; the rest (arrows, Home/End, ...) do not.
|
||||
// Delete is the FORWARD delete a text editor needs -
|
||||
// backspace already types 0x08 from the main block.
|
||||
if (code == SCAN_ENTER || code == SCAN_SLASH) {
|
||||
ch = gScanAsciiNormal[code];
|
||||
} else if (code == SCAN_DELETE) {
|
||||
ch = JL_CHAR_DELETE;
|
||||
}
|
||||
} else if (code >= SCAN_KEYPAD_FIRST && code <= SCAN_KEYPAD_LAST &&
|
||||
gIsrNumLock && gKeypadAscii[code - SCAN_KEYPAD_FIRST] != 0) {
|
||||
|
|
|
|||
|
|
@ -278,6 +278,23 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
|
|||
}
|
||||
|
||||
|
||||
// RTS on demand (jlSerialSetRts): MCR bit 1. DTR and OUT2 are preserved - OUT2 physically gates this
|
||||
// UART's interrupt to the PIC, so clearing it while dropping RTS would silence the receiver entirely,
|
||||
// which is the opposite of what a flow-control pause is for.
|
||||
void jlpSerialSetRts(bool on) {
|
||||
uint8_t mcr;
|
||||
|
||||
if (gComBase == 0u) {
|
||||
return;
|
||||
}
|
||||
mcr = (uint8_t)(MCR_DTR | MCR_OUT2);
|
||||
if (on) {
|
||||
mcr = (uint8_t)(mcr | MCR_RTS);
|
||||
}
|
||||
outportb((int)(gComBase + UART_MCR), mcr);
|
||||
}
|
||||
|
||||
|
||||
void jlpSerialPoll(void) {
|
||||
// The RX IRQ fills the ring in the background; nothing to pump.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ void jlpGenericSerialPoll(void) {
|
|||
}
|
||||
|
||||
|
||||
// A port with no controllable RTS line: doing nothing is the honest answer, and it lets a caller drive
|
||||
// jlSerialSetRts unconditionally instead of guarding every call site with a platform test.
|
||||
void jlpGenericSerialSetRts(bool on) {
|
||||
(void)on;
|
||||
}
|
||||
|
||||
|
||||
uint16_t jlpGenericSerialRead(uint8_t *buf, uint16_t max) {
|
||||
(void)buf;
|
||||
(void)max;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,10 @@ static uint16_t gRxTail;
|
|||
static bool gOpen;
|
||||
static bool gIsScc; // true: 8530 SCC; false: 6551 ACIA
|
||||
static volatile uint8_t *gStatus; // SCC command reg (RR0) / 6551 status reg
|
||||
static uint8_t gWr5; // the SCC's live WR5 (Tx bits + DTR + RTS), so jlpSerialSetRts
|
||||
// can flip ONE bit without guessing the rest back: WR5 is
|
||||
// write-only on this chip, and rebuilding it from scratch would
|
||||
// silently drop the data-bit and Tx-enable settings
|
||||
static volatile uint8_t *gData; // data register (both)
|
||||
|
||||
|
||||
|
|
@ -219,7 +223,8 @@ static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_
|
|||
sccWrite(ctrl, 13u, (uint8_t)((tc >> 8) & 0xFFu)); // time constant high
|
||||
sccWrite(ctrl, 14u, 0x03u); // BRG enable, source = PCLK
|
||||
sccWrite(ctrl, 3u, (uint8_t)(wr3 | 0x01u)); // Rx enable
|
||||
sccWrite(ctrl, 5u, (uint8_t)(wr5 | 0x8Au)); // Tx enable + DTR + RTS
|
||||
gWr5 = (uint8_t)(wr5 | 0x8Au);
|
||||
sccWrite(ctrl, 5u, gWr5); // Tx enable + DTR + RTS
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -352,6 +357,23 @@ void iigsSerialPump(void) {
|
|||
}
|
||||
|
||||
|
||||
// RTS on demand (jlSerialSetRts). SCC: WR5 bit 1, applied to the retained copy so the Tx enable, DTR and
|
||||
// data-bit fields survive. 6551: the command register's bits 3:2 - %10 asserts RTS with the receiver
|
||||
// interrupt disabled (this HAL polls), %00 deasserts it AND gates the transmitter off, which is why the
|
||||
// deassert is only ever a pause.
|
||||
void jlpSerialSetRts(bool on) {
|
||||
if (!gOpen) {
|
||||
return;
|
||||
}
|
||||
if (gIsScc) {
|
||||
gWr5 = on ? (uint8_t)(gWr5 | 0x02u) : (uint8_t)(gWr5 & (uint8_t)~0x02u);
|
||||
sccWrite(gStatus, 5u, gWr5);
|
||||
return;
|
||||
}
|
||||
gStatus[1] = on ? 0x09u : 0x01u; // 6551 command: DTR on, RTS asserted / deasserted
|
||||
}
|
||||
|
||||
|
||||
void jlpSerialPoll(void) {
|
||||
uint16_t tail;
|
||||
uint16_t guard;
|
||||
|
|
|
|||
124
tools/xdftool.py
124
tools/xdftool.py
|
|
@ -101,8 +101,12 @@ class Xdf:
|
|||
continue
|
||||
name = e[0:8].decode("ascii", "replace").rstrip()
|
||||
ext = e[8:11].decode("ascii", "replace").rstrip()
|
||||
# Human68k EXTENDED names: chars 9-18 of the stem live in bytes 12-21 (see encode_name_ext).
|
||||
# Fold them back in so a listing shows the full name a guest wrote (e.g. the 9-char blob keys).
|
||||
more = e[12:22].split(b"\x00", 1)[0].decode("ascii", "replace").rstrip()
|
||||
stem = name + more
|
||||
out.append({
|
||||
"name": f"{name}.{ext}" if ext else name,
|
||||
"name": f"{stem}.{ext}" if ext else stem,
|
||||
"cluster": struct.unpack("<H", e[26:28])[0],
|
||||
"size": struct.unpack("<I", e[28:32])[0],
|
||||
"attr": e[11],
|
||||
|
|
@ -214,47 +218,81 @@ class Xdf:
|
|||
self.data[off] = DELETED_MARKER
|
||||
return True
|
||||
|
||||
def add_file(self, name, payload):
|
||||
self.delete(name) # overwrite semantics
|
||||
need = (len(payload) + self.cluster_bytes - 1) // self.cluster_bytes
|
||||
def _alloc_write_payload(self, payload):
|
||||
# Allocate a FAT chain for `payload`, write it (zero-padding the final cluster so stale bytes never
|
||||
# leak), and return the head cluster (0 for an empty payload). Shared by add_file and
|
||||
# add_file_in_subdir so the cluster-writing logic lives in exactly one place.
|
||||
cb = self.cluster_bytes
|
||||
need = (len(payload) + cb - 1) // cb
|
||||
if need == 0:
|
||||
return 0
|
||||
free = self.free_clusters()
|
||||
if len(free) < need:
|
||||
raise OSError(f"{self.path}: need {need} clusters, {len(free)} free "
|
||||
f"({len(free) * self.cluster_bytes} bytes)")
|
||||
f"({len(free) * cb} bytes)")
|
||||
chain = free[:need]
|
||||
for idx, cluster in enumerate(chain):
|
||||
off = self.cluster_offset(cluster)
|
||||
chunk = payload[idx * self.cluster_bytes:(idx + 1) * self.cluster_bytes]
|
||||
chunk = payload[idx * cb:(idx + 1) * cb]
|
||||
self.data[off:off + len(chunk)] = chunk
|
||||
# Zero the tail of the final cluster so stale bytes never leak.
|
||||
if len(chunk) < self.cluster_bytes:
|
||||
self.data[off + len(chunk):off + self.cluster_bytes] = \
|
||||
bytes(self.cluster_bytes - len(chunk))
|
||||
if len(chunk) < cb:
|
||||
self.data[off + len(chunk):off + cb] = bytes(cb - len(chunk))
|
||||
self.fat_set(cluster, 0xFFF if idx == need - 1 else chain[idx + 1])
|
||||
return chain[0]
|
||||
|
||||
def _put_entry_in_dir(self, first_cluster, entry):
|
||||
# Write a 32-byte directory `entry` into the first FREE/DELETED slot of the directory whose data
|
||||
# begins at first_cluster, extending the cluster chain by one when every slot is already used.
|
||||
per = self.cluster_bytes // DIR_ENTRY_SIZE
|
||||
cluster = first_cluster
|
||||
last = cluster
|
||||
guard = 0
|
||||
while 2 <= cluster < EOC_MIN:
|
||||
base = self.cluster_offset(cluster)
|
||||
for i in range(per):
|
||||
slot = base + i * DIR_ENTRY_SIZE
|
||||
if self.data[slot] in (FREE_MARKER, DELETED_MARKER):
|
||||
self.data[slot:slot + DIR_ENTRY_SIZE] = entry
|
||||
return
|
||||
last = cluster
|
||||
cluster = self.fat_get(cluster)
|
||||
guard += 1
|
||||
if guard > self.max_cluster:
|
||||
raise OSError(f"{self.path}: directory cluster chain loops")
|
||||
# Every slot is used: append a fresh, all-free cluster and write into its first slot.
|
||||
free = self.free_clusters()
|
||||
if not free:
|
||||
raise OSError(f"{self.path}: no free cluster to extend directory")
|
||||
newc = free[0]
|
||||
self.fat_set(last, newc)
|
||||
self.fat_set(newc, 0xFFF)
|
||||
base = self.cluster_offset(newc)
|
||||
self.data[base:base + self.cluster_bytes] = bytes(self.cluster_bytes)
|
||||
self.data[base:base + DIR_ENTRY_SIZE] = entry
|
||||
|
||||
def add_file(self, name, payload):
|
||||
self.delete(name) # overwrite semantics
|
||||
# encode_name_ext writes the Human68k EXTENDED name (a strict superset of 8.3: ext10 is all-NUL
|
||||
# for a plain 8.3 name), so names up to 18 chars - like RetroNet's 9-char blob keys - land in a
|
||||
# form the guest and xdftool's own find()/read_file() both resolve.
|
||||
main11, ext10 = self.encode_name_ext(name)
|
||||
head = self._alloc_write_payload(payload)
|
||||
slot = None
|
||||
for i, off, e in self._entries():
|
||||
for _, off, e in self._entries():
|
||||
if e[0] in (FREE_MARKER, DELETED_MARKER):
|
||||
slot = off
|
||||
break
|
||||
if slot is None:
|
||||
raise OSError(f"{self.path}: root directory full ({self.root_entries} entries)")
|
||||
self.data[slot:slot + DIR_ENTRY_SIZE] = self._dir_entry(
|
||||
main11, ATTR_ARCHIVE, head, len(payload), ext10)
|
||||
|
||||
entry = bytearray(DIR_ENTRY_SIZE)
|
||||
entry[0:11] = self.encode_name(name)
|
||||
entry[11] = ATTR_ARCHIVE
|
||||
# Fixed timestamp: reproducible images matter more than real mtimes,
|
||||
# since these get byte-compared across gate runs.
|
||||
struct.pack_into("<H", entry, 22, (12 << 11)) # 12:00:00
|
||||
struct.pack_into("<H", entry, 24, ((2026 - 1980) << 9) | (1 << 5) | 1)
|
||||
struct.pack_into("<H", entry, 26, chain[0] if need else 0)
|
||||
struct.pack_into("<I", entry, 28, len(payload))
|
||||
self.data[slot:slot + DIR_ENTRY_SIZE] = entry
|
||||
|
||||
def _dir_entry(self, name11, attr, cluster, size):
|
||||
def _dir_entry(self, name11, attr, cluster, size, ext10=None):
|
||||
entry = bytearray(DIR_ENTRY_SIZE)
|
||||
entry[0:11] = name11
|
||||
entry[11] = attr
|
||||
if ext10 is not None: # Human68k extended name
|
||||
entry[12:22] = ext10
|
||||
struct.pack_into("<H", entry, 22, (12 << 11)) # 12:00:00
|
||||
struct.pack_into("<H", entry, 24, ((2026 - 1980) << 9) | (1 << 5) | 1) # 2026-01-01
|
||||
struct.pack_into("<H", entry, 26, cluster)
|
||||
|
|
@ -262,36 +300,34 @@ class Xdf:
|
|||
return entry
|
||||
|
||||
def add_file_in_subdir(self, dirname, name, payload):
|
||||
# Create a one-cluster subdirectory in root (with . and .. entries) and write `name` into it, so a
|
||||
# guest that reads e.g. SAVES/RN.CFG finds it. Fresh-template semantics: the subdir must not exist
|
||||
# yet (the callers copy a clean image per run). Human68k FAT12 is otherwise ordinary FAT12.
|
||||
# Write `name` into subdirectory `dirname` (creating SUB/. and SUB/.. when the subdir is new), so a
|
||||
# guest reading e.g. SAVES/RN.CFG or SAVES/<blob> finds it. If the subdir ALREADY exists (a second
|
||||
# file staged into the same SAVES/), add INTO it rather than creating a duplicate root entry the
|
||||
# guest's SUB lookup would never resolve. Human68k FAT12 is otherwise ordinary FAT12.
|
||||
dstem = dirname.upper()
|
||||
if len(dstem) > 8:
|
||||
raise ValueError(f"subdir '{dirname}' does not fit 8.3")
|
||||
cb = self.cluster_bytes
|
||||
need = max(1, (len(payload) + cb - 1) // cb)
|
||||
main11, ext10 = self.encode_name_ext(name)
|
||||
_, _, de = self.find(dirname)
|
||||
if de is not None and (de[11] & ATTR_DIR):
|
||||
head = self._alloc_write_payload(payload)
|
||||
entry = self._dir_entry(main11, ATTR_ARCHIVE, head, len(payload), ext10)
|
||||
self._put_entry_in_dir(struct.unpack("<H", de[26:28])[0], entry)
|
||||
return
|
||||
# Fresh subdirectory: reserve its cluster first so the payload allocation cannot reuse it, then
|
||||
# lay down '.', '..', and the file entry.
|
||||
free = self.free_clusters()
|
||||
if len(free) < need + 1:
|
||||
raise OSError(f"{self.path}: need {need + 1} clusters, {len(free)} free")
|
||||
if not free:
|
||||
raise OSError(f"{self.path}: no free cluster for subdirectory")
|
||||
dir_cluster = free[0]
|
||||
data_chain = free[1:1 + need]
|
||||
# File data clusters.
|
||||
for idx, cluster in enumerate(data_chain):
|
||||
off = self.cluster_offset(cluster)
|
||||
chunk = payload[idx * cb:(idx + 1) * cb]
|
||||
self.data[off:off + len(chunk)] = chunk
|
||||
if len(chunk) < cb:
|
||||
self.data[off + len(chunk):off + cb] = bytes(cb - len(chunk))
|
||||
self.fat_set(cluster, 0xFFF if idx == need - 1 else data_chain[idx + 1])
|
||||
# The subdirectory's own cluster: '.', '..', then the file entry, rest free.
|
||||
self.fat_set(dir_cluster, 0xFFF)
|
||||
head = self._alloc_write_payload(payload)
|
||||
cb = self.cluster_bytes
|
||||
doff = self.cluster_offset(dir_cluster)
|
||||
self.data[doff:doff + cb] = bytes(cb)
|
||||
self.data[doff:doff + 32] = self._dir_entry(b". ", ATTR_DIR, dir_cluster, 0)
|
||||
self.data[doff + 32:doff + 64] = self._dir_entry(b".. ", ATTR_DIR, 0, 0)
|
||||
self.data[doff + 64:doff + 96] = self._dir_entry(
|
||||
self.encode_name(name), ATTR_ARCHIVE, data_chain[0], len(payload))
|
||||
self.fat_set(dir_cluster, 0xFFF)
|
||||
# Root entry pointing at the subdirectory.
|
||||
self.data[doff + 64:doff + 96] = self._dir_entry(main11, ATTR_ARCHIVE, head, len(payload), ext10)
|
||||
slot = None
|
||||
for _, off, e in self._entries():
|
||||
if e[0] in (FREE_MARKER, DELETED_MARKER):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue