diff --git a/docs/kernel-notes.md b/docs/kernel-notes.md index fae5df7..b2ed73f 100644 --- a/docs/kernel-notes.md +++ b/docs/kernel-notes.md @@ -15,6 +15,7 @@ the code in `kernel/` is built on. | GPIO access (init/exit) | gpiod descriptor API; descriptors resolved by `(chip, hwnum)` via `gpio_device_find_by_label("pinctrl-bcm2835")` + `gpio_device_get_desc()`, **not** `gpio_to_desc()` (see "GPIO descriptor lookup" below) | `iec_init`/`iec_exit` | | Kernel↔userspace | Character device `/dev/iec0` + `kfifo` + wait queue (IEC ≤ 1000 B/s; relayfs not justified) | `iec_read`, `emit_record` | | `udelay` vs. poll | Poll-with-timeout for CLK transitions; `udelay` only for fixed delays (EOI ack 80 µs, EOI detect 250 µs) | `iec_timing.h`, `wait_clk` | +| Sense-line reads | Debounced (glitch-filtered): a level change is believed only after it holds `IEC_DEBOUNCE_US` (5 µs); shorter pulses are rejected as noise. Mirrors a confirmed-working reference listener. Fast path is a single register read, so tight CLK polls stay cheap. ATN ISR + self-test stay **raw** | `iec_read_stable`, `db_*_asserted` | | isolcpus / nohz_full | **Not** in the Phase-1 baseline. Add `isolcpus=3 nohz_full=3 rcu_nocbs=3 irqaffinity=0-2` only if Phase-2 bit-error rate > 1% | (boot cmdline) | | PREEMPT_RT | Stock kernel sufficient; RT is a last resort | — | | Module signing | Not required on stock RPi OS Bookworm | — | @@ -222,8 +223,10 @@ The host needs qemu binfmt for arm64; the script registers it once via ## Starting timing constants See `kernel/iec_timing.h`. Tune in Phase 2 against a real C64 / logic analyser. -The likely first knobs: `IEC_EOI_DETECT_US` (EOI false positives/negatives) and -`IEC_CLK_TIMEOUT_US` (frame errors under load). +The likely first knobs: `IEC_EOI_DETECT_US` (EOI false positives/negatives), +`IEC_CLK_TIMEOUT_US` (frame errors under load), and `IEC_DEBOUNCE_US` (the +sense-line glitch-filter window — raise it if a noisy bus still yields bit +errors, lower it if it smears fast edges). ## Open items to verify on hardware diff --git a/kernel/iec_listener.c b/kernel/iec_listener.c index 2314d33..3d73d4b 100644 --- a/kernel/iec_listener.c +++ b/kernel/iec_listener.c @@ -81,6 +81,54 @@ static DECLARE_WAIT_QUEUE_HEAD(iec_work_wq); /* listener addressing state (decoded in-kernel, just enough to participate) */ static bool addressed_listener; +/* + * --- debounced sense lines (glitch filter) ------------------------------- + * + * Mirrors the 5 us glitch filter from a confirmed-working reference listener. + * The raw iec_*_asserted() helpers in iec_lines.h read + * the GPIO register once; on a level-shifted 5 V<->3.3 V bus a single sample + * can land on noise and yield a wrong bit, with no chance to retry since the + * bit loop runs with IRQs off. These wrappers only believe a level change once + * it has held for IEC_DEBOUNCE_US; a shorter pulse is rejected as a glitch and + * the previous stable level is kept. + * + * Fast path (no change since last read) is a single register read, so the tight + * CLK polls in wait_clk() stay cheap; the debounce cost is paid only when an + * edge actually appears. udelay() is safe with IRQs disabled. + * + * Concurrency: the entire IEC receive path runs single-threaded in the worker + * kthread, so the per-line stable state needs no locking. The ATN ISR must NOT + * use these (it needs an instantaneous read and must not touch this state) -- + * it keeps using the raw iec_atn_asserted() from iec_lines.h. + */ +static int db_clk = 1; /* last stable level, 1 = released (high) = idle bus */ +static int db_data = 1; +static int db_atn = 1; +static int db_reset = 1; + +static int iec_read_stable(unsigned int pin, int *last_stable) +{ + int raw = iec_gpio_read(pin); + unsigned int held; + + if (raw == *last_stable) + return *last_stable; /* fast path: no change */ + + /* a transition appeared: require it to hold IEC_DEBOUNCE_US */ + for (held = 0; held < IEC_DEBOUNCE_US; held++) { + udelay(1); + if (iec_gpio_read(pin) != raw) + return *last_stable; /* bounced back -> glitch, ignore */ + } + *last_stable = raw; /* held steady -> accept */ + return raw; +} + +static inline bool db_clk_asserted(void) { return iec_read_stable(IEC_GPIO_CLK, &db_clk) == 0; } +static inline bool db_atn_asserted(void) { return iec_read_stable(IEC_GPIO_ATN, &db_atn) == 0; } +static inline bool db_reset_asserted(void) { return iec_read_stable(IEC_GPIO_RESET, &db_reset) == 0; } +static inline bool db_data_asserted(void) { return iec_read_stable(IEC_GPIO_DATA, &db_data) == 0; } + /* receive_byte() outcomes */ enum iec_rx { IEC_RX_OK = 0, /* byte received */ @@ -126,10 +174,10 @@ static enum iec_rx wait_clk(bool want_asserted, unsigned int timeout_us, { unsigned int waited = 0; - while (iec_clk_asserted() != want_asserted) { - if (iec_atn_asserted() == data_phase) + while (db_clk_asserted() != want_asserted) { + if (db_atn_asserted() == data_phase) return IEC_RX_ATN; - if (iec_reset_asserted()) + if (db_reset_asserted()) return IEC_RX_RESET; if (waited++ >= timeout_us) return IEC_RX_TIMEOUT; @@ -164,9 +212,9 @@ static enum iec_rx receive_byte(u8 *out, bool *eoi, bool data_phase) if (data_phase) { unsigned int waited = 0; - while (!iec_clk_asserted()) { - if (iec_atn_asserted()) { rc = IEC_RX_ATN; goto out; } - if (iec_reset_asserted()) { rc = IEC_RX_RESET; goto out; } + while (!db_clk_asserted()) { + if (db_atn_asserted()) { rc = IEC_RX_ATN; goto out; } + if (db_reset_asserted()) { rc = IEC_RX_RESET; goto out; } if (waited++ >= IEC_EOI_DETECT_US) { /* ack EOI: pull DATA low for Tei, then release */ iec_data_assert(); @@ -190,7 +238,7 @@ static enum iec_rx receive_byte(u8 *out, bool *eoi, bool data_phase) if (rc != IEC_RX_OK) goto out; /* released(high) = bit 1, asserted(low) = bit 0 */ - if (!iec_data_asserted()) + if (!db_data_asserted()) value |= (1u << i); } @@ -229,10 +277,10 @@ static void run_state_machine(void) bool eoi; enum iec_rx rc; - if (iec_reset_asserted()) + if (db_reset_asserted()) goto reset; - if (iec_atn_asserted()) { + if (db_atn_asserted()) { /* RECEIVE_COMMAND: ATN held low */ rc = receive_byte(&b, &eoi, false); if (rc == IEC_RX_RESET) @@ -256,9 +304,9 @@ static void run_state_machine(void) /* LISTENER: data phase */ emit_record(IEC_KIND_EVENT, IEC_EV_ATN_RELEASED, IEC_FLAG_ADDRESSED); for (;;) { - if (iec_reset_asserted()) + if (db_reset_asserted()) goto reset; - if (iec_atn_asserted()) { + if (db_atn_asserted()) { /* C64 interrupts with a new command */ emit_record(IEC_KIND_EVENT, IEC_EV_ATN_COMMAND, 0); break; diff --git a/kernel/iec_timing.h b/kernel/iec_timing.h index 10ffb39..10632ab 100644 --- a/kernel/iec_timing.h +++ b/kernel/iec_timing.h @@ -25,8 +25,12 @@ * 80 us gives margin. */ #define IEC_EOI_ACK_HOLD_US 80 -/* Debounce between consecutive bus reads (mirrors sd2iec / 1571 ROM). */ -#define IEC_DEBOUNCE_US 2 +/* Glitch filter window for sense-line reads. A level change is only believed + * once it has held this long; a shorter pulse is treated as noise. Matches a + * confirmed-working reference listener (5 us). sd2iec / the 1571 ROM + * use a comparable few-microsecond debounce. Far shorter than a real IEC bit + * (tens of us), so it never smears valid data. */ +#define IEC_DEBOUNCE_US 5 /* Between-bytes minimum hold before the talker releases CLK for the next byte * (Tbb). We simply wait for the next CLK transition; this is documentary. */