From 5184415fc42dca65cfd7e2382a13bfab7c390533 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 7 Jul 2026 09:05:28 +0300 Subject: [PATCH] =?UTF-8?q?irq:=20=D1=84=D0=B8=D0=BA=D1=81=20Phase=201=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B4?= =?UTF-8?q?=D0=BA=D0=B8=20=E2=80=94=20DSS=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=B2=20IM=201,=20=D1=87=D0=B5=D0=B9?= =?UTF-8?q?=D0=BD=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0=20=D0=BD=D0=B0=20?= =?UTF-8?q?0x0038?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Первая версия висла на первом прерывании. Две причины (verified по docs/samples/sprinterIntLib.asm, SIO_CTC_KEY.asm и исходникам MAME): - DSS работает в IM 1 (обработчик 0x0038); I=0x3F — наследие Spectrum ROM, НЕ таблица: чтение [I<<8|0xFF] давало мусор (0x00BF) и прыжок в никуда. Чейн из трамплина теперь ВСЕГДА jp 0x0038 (interrupted-PC на стеке = имитация RST 38); irq_remove безусловно восстанавливает IM 1 - CBL-фильтр по биту 7 порта 0xFE убран: при выключенном CBL бит подтянут к 1 (MAME kbd_fe_r: data |= 0xE0) — каждый кадр ложно уходил в чейн, user-handler не вызывался бы. Вернуть в Phase 2 вместе с поддержкой CBL Попутно подтверждено: порт 0x19 = SIO-A RR0 (Z84C015), бит 0 = Rx Available; вектора встроенной периферии SIO 0x10..0x1E / CTC 0x06 (заливка 257×H ловит любые); внешний вектор 0xFF. irqtest: диагностика I до установки + фаза без ESTEX; прогон в MAME: ~49 Гц, клавиатура жива, remove останавливает тики, чистый выход. Co-Authored-By: Claude Fable 5 --- docs/im2_isr_design.md | 56 +++++++++++++++++++++++++++++++---------- libc/include/irq.h | 11 ++++---- libc/irq/_irq.h | 13 +++++----- libc/irq/_irq_tramp.c | 23 +++++++++-------- libc/irq/irq_install.c | 34 +++++++++++++++---------- libc/irq/irq_remove.c | 16 ++++++------ tests/irqtest/irqtest.c | 30 +++++++++++++++++++++- 7 files changed, 126 insertions(+), 57 deletions(-) diff --git a/docs/im2_isr_design.md b/docs/im2_isr_design.md index 25d3c20..3ab9dec 100644 --- a/docs/im2_isr_design.md +++ b/docs/im2_isr_design.md @@ -1,27 +1,57 @@ # IM2 Interrupt Handlers — Design Document -**Status: Phase 1 РЕАЛИЗОВАН 2026-07-06** (libc/irq: irq_install/ -irq_remove + трамплин; тест tests/irqtest). Отличия реализации от -плана ниже: +**Status: Phase 1 РЕАЛИЗОВАН 2026-07-06, ИСПРАВЛЕН после отладки** +(libc/irq: irq_install/irq_remove + трамплин; тест tests/irqtest). + +## Результаты verification (2026-07-06, по docs/samples и исходникам MAME) + +Первая версия зависала на первом же прерывании после irq_install. +Причины и проверенные факты: + +- **DSS работает в IM 1** (обработчик на 0x0038). I=0x3F — наследие + Spectrum ROM, НЕ признак IM 2-таблицы. Доказательства: + `docs/samples/sprinterIntLib.asm` (intRestoreDefaultInterrupt: `ld i,a` + + `im 1` безусловно, «обязательно перед функциями дос и биос») и + `docs/samples/SIO_CTC_KEY.asm` (выход: `LD I,A` + `IM 1`). Чтение + «таблицы DSS» по [I<<8+0xFF] давало мусор (0x00BF) и было причиной + зависания. Фикс: чейн из трамплина ВСЕГДА на 0x0038 (jp с + interrupted-PC на стеке = имитация RST 38); irq_remove всегда + восстанавливает IM 1 (I — только регистр). +- **CBL-фильтр по биту 7 порта 0xFE убран из трамплина**: в MAME при + выключенном CBL бит 7 всегда = 1 (sprinter.cpp kbd_fe_r: + `data |= 0xE0`), и каждое кадровое прерывание ложно классифицировалось + как CBL — user-handler не вызывался бы никогда. Признак «#fe.bit7=1» + (sprinterIntLib.asm) имеет смысл только при активном CBL — вернуть в + Phase 2 вместе с поддержкой CBL. +- **Порт 0x19 = SIO-A RR0 (Z84C015), бит 0 = «Rx Character Available»** — + семантика клавиатурного пробника верна (подтверждено + sprinterIntLib.asm: `in a,(COM_A); bit 0,a; Z → кадровое`). +- **Вектора встроенной периферии Z84C015**: SIO — 0x10..0x1E, CTC — 0x06 + (базовые вектора задаются записью в WR2 SIO-B / CTC ch0). Штатно их + прерывания выключены (сэмплы включают/выключают их сами); наша + заливка 257×H перехватывает любой вектор на трамплин, а чейн на + 0x0038 безопасен для любого источника. +- Внешний вектор действительно 0xFF (MAME sprinter.cpp: + `set_irq_acknowledge_callback` → 0xff). + +Отличия реализации от плана ниже: - **отдельный `--memory im2` НЕ понадобился**: таблица — статический - буфер 513 Б в BSS с выравниванием в рантайме; т.к. Sprinter шлёт - только вектор 0xFF, значимы лишь байты [0xFF]/[0x100], и 3-байтовый + буфер 513 Б в BSS с выравниванием в рантайме; т.к. внешний вектор — + только 0xFF, значимы лишь байты [0xFF]/[0x100], и 3-байтовый `jp _irq_tramp` лежит ВНУТРИ таблицы по смещению H (H = старший байт её адреса, < 0xC0 — не пересекается). Никаких linker-областей и правок crt0; - работает в tiny/big (код и данные в W2); в small/huge irq_install возвращает EINVAL (проверка адресов трамплина/буфера); -- **чейн к DSS — ВСЕГДА** (и клавиатура, и кадр, и CBL): не нужно - знать, что DSS делает в своём ISR — SYSTIME/клавиатура/мышь живут. - User-handler зовётся только на кадровых (фильтр: бит 0 порта 0x19, - бит 7 порта 0xFE → мимо); финальный jp — SMC-операнд; +- **чейн к DSS — ВСЕГДА и всегда на 0x0038** (и клавиатура, и кадр): + не нужно знать, что DSS делает в своём ISR — SYSTIME/клавиатура/мышь + живут. User-handler зовётся только на кадровых (фильтр: бит 0 + порта 0x19 → мимо); финальный jp — SMC-операнд; - W3-порт в трамплине НЕ сохраняется: gfx держит DI на время свопов, а user-handler'у banking запрещён; DSS свои окна сохраняет сам; -- irq_remove вешается на atexit (выход без снятия = I указывает в - память умершего процесса = крах шелла); old_I==0 → восстановление IM 1; -- открытые вопросы 1 (что делает DSS ISR) и 2 (ld a,i) сняты - дизайном «всегда чейн» и безусловным EI. +- irq_remove вешается на atexit (выход без снятия = IM 2/I указывают в + память умершего процесса = крах шелла); восстановление — всегда IM 1. Phase 2 (CBL/COVOX handler, ISA, цепочка нескольких хендлеров) — ниже по плану, не реализовано. diff --git a/libc/include/irq.h b/libc/include/irq.h index 60a3ddd..89aa06b 100644 --- a/libc/include/irq.h +++ b/libc/include/irq.h @@ -2,12 +2,13 @@ * irq.h — пользовательский обработчик кадрового прерывания (IM 2). * * Sprinter шлёт вектор 0xFF от трёх источников: кадр (50 Гц), - * клавиатура (бит 0 порта 0x19), CBL/COVOX (бит 7 порта 0xFE). - * irq_install() перехватывает IM2-таблицу и зовёт handler ТОЛЬКО на - * кадровых прерываниях; после него (и на всех прочих источниках) - * управление всегда передаётся штатному обработчику DSS — клавиатура, + * клавиатура (бит 0 порта 0x19), CBL/COVOX. Штатно DSS работает в + * IM 1 (обработчик 0x0038). irq_install() переключает CPU в IM 2 со + * своей таблицей и зовёт handler ТОЛЬКО на кадровых прерываниях; + * после него (и на всех прочих источниках) управление всегда + * передаётся штатному обработчику DSS на 0x0038 — клавиатура, * SYSTIME и курсор мыши продолжают работать. irq_remove() - * восстанавливает всё как было (и вызывается атоматически из exit). + * возвращает IM 1/I как было (и вызывается автоматически из exit). * * ТРЕБОВАНИЕ ПАМЯТИ: код и данные программы должны быть в W2 * (0x8000-0xBFFF) — режимы tiny и big. В small/huge (CODE в W1) diff --git a/libc/irq/_irq.h b/libc/irq/_irq.h index e5a8d59..fef4823 100644 --- a/libc/irq/_irq.h +++ b/libc/irq/_irq.h @@ -6,12 +6,13 @@ * буфера 513 байт (_irq_state.c), заполняется байтом H = старший * байт её же адреса; CPU по вектору V читает адрес (H<<8|H) — * трёхбайтовый `jp _irq_tramp` лежит ВНУТРИ таблицы по смещению H - * (безопасно: Sprinter шлёт только V=0xFF, а H < 0xC0); - * - трамплин (_irq_tramp.c): AF-пробник портов 0x19/0xFE; кадровое - * прерывание → полный save → user handler → restore; затем ВСЕГДА - * jp на старый обработчик DSS (SMC-операнд, патчится при install) — - * EI/RETI делает DSS; - * - install: I := H, IM 2; remove: I := старое, IM 2. + * (безопасно: внешний вектор — 0xFF, а H < 0xC0; заливка H + * перехватывает и вектора SIO/CTC, если их кто-то включит); + * - трамплин (_irq_tramp.c): AF-пробник порта 0x19 (SIO-A RR0); + * кадровое прерывание → полный save → user handler → restore; + * затем ВСЕГДА jp на обработчик DSS 0x0038 (DSS работает в IM 1; + * SMC-операнд патчится при install) — EI/RETI делает DSS; + * - install: I := H, IM 2; remove: I := старое, IM 1 (штатный режим DSS). */ #ifndef _IRQ_INTERNAL_H #define _IRQ_INTERNAL_H diff --git a/libc/irq/_irq_tramp.c b/libc/irq/_irq_tramp.c index 53e0753..bdc3d84 100644 --- a/libc/irq/_irq_tramp.c +++ b/libc/irq/_irq_tramp.c @@ -2,11 +2,17 @@ * _irq_tramp — вход IM2-прерывания (вектор 0xFF, все источники). * * До выяснения источника трогаем только AF: клавиатура (бит 0 порта - * 0x19) и CBL (бит 7 порта 0xFE) уходят напрямую в DSS. Кадровое - * прерывание: полный сейв обоих наборов + IX/IY, вызов user-handler'а, - * симметричный restore. В конце ВСЕГДА jp на старый обработчик DSS - * (он делает свой save/restore и завершает EI/RETI) — клавиатурный - * FIFO, SYSTIME и курсор мыши продолжают обслуживаться. + * 0x19 = SIO-A RR0 «Rx Character Available») уходит напрямую в DSS. + * Кадровое прерывание: полный сейв обоих наборов + IX/IY, вызов + * user-handler'а, симметричный restore. В конце ВСЕГДА jp на + * обработчик DSS 0x0038 (DSS работает в IM 1; jp с interrupted-PC на + * стеке имитирует RST 38) — он делает свой save/restore и завершает + * EI/RETI: клавиатурный FIFO, SYSTIME и курсор мыши живут. + * + * CBL-фильтр по биту 7 порта 0xFE УБРАН: при выключенном CBL бит + * подтянут к 1 (MAME kbd_fe_r: data |= 0xE0), и каждое кадровое + * прерывание ложно уходило бы в чейн — user-handler не вызывался бы + * никогда. Вернуть вместе с поддержкой CBL в Phase 2. * * Операнд финального jp (_irq_dss_target) — SMC, патчится из * irq_install; модуль в _CODE (RAM на Sprinter, W2 в tiny/big). @@ -21,9 +27,6 @@ void _irq_tramp(void) __naked in a, (#0x19) rrca ; бит 0 — клавиатурный байт принят? jr C, _irq_chain - in a, (#0xFE) - rlca ; бит 7 — запрос сэмпла CBL? - jr C, _irq_chain ;; --- кадровое прерывание --- push bc @@ -61,10 +64,10 @@ void _irq_tramp(void) __naked pop bc _irq_chain: pop af - ;; Всегда к DSS — EI/RETI за ним. + ;; Всегда к DSS (IM 1, обработчик 0x0038) — EI/RETI за ним. .db 0xC3 ; jp nn __irq_dss_target:: - .dw 0x0038 ; патчится в irq_install + .dw 0x0038 ; SMC: подтверждается в irq_install _irq_call_hl: jp (hl) diff --git a/libc/irq/irq_install.c b/libc/irq/irq_install.c index 930d6c3..2dfbee4 100644 --- a/libc/irq/irq_install.c +++ b/libc/irq/irq_install.c @@ -3,11 +3,20 @@ * * Строит 257-байтную вектор-таблицу (выравнивание 256 в рантайме, * заполнение байтом H = старший байт адреса таблицы; `jp _irq_tramp` - * лежит внутри таблицы по смещению H — Sprinter шлёт только вектор - * 0xFF, читаются лишь байты [0xFF]/[0x100]), запоминает старый - * обработчик DSS из его таблицы (по регистру I), патчит SMC-операнд - * чейна и переключает I/IM2. irq_remove вешается на atexit — выход - * без снятия оставил бы I на памяти умершего процесса. + * лежит внутри таблицы по смещению H — читаются лишь байты + * [0xFF]/[0x100] для внешнего вектора 0xFF; вектора встроенной + * периферии Z84C015 (SIO 0x10..0x1E, CTC 0x06) штатно выключены, но + * заливка H перехватывает и их — чейн на 0x0038 валиден для всех). + * + * ВАЖНО (проверено по docs/samples/sprinterIntLib.asm и + * SIO_CTC_KEY.asm): DSS работает в IM 1 — его обработчик живёт на + * 0x0038, а I=0x3F — наследие Spectrum ROM, НЕ признак IM 2-таблицы. + * Поэтому чейн из трамплина ВСЕГДА идёт на 0x0038 (jp 0x0038 с + * interrupted-PC на стеке в точности имитирует вход IM 1 / RST 38), + * а старое I сохраняется только чтобы вернуть регистр при remove. + * + * irq_remove вешается на atexit — выход без снятия оставил бы I/IM2 + * на памяти умершего процесса. */ #include @@ -57,15 +66,14 @@ int irq_install(isr_t handler) IRQ_DISABLE(); + /* Старое I — только для восстановления регистра в irq_remove. */ _irq_old_i = irq_get_i(); - if (_irq_old_i) { - /* Таблица DSS: адрес обработчика — байты [0xFF]/[0x100]. */ - const uint8_t *old = (const uint8_t *)(((uint16_t)_irq_old_i << 8) - + 0xFF); - _irq_dss_target = (uint16_t)(old[0] | ((uint16_t)old[1] << 8)); - } else { - _irq_dss_target = 0x0038; /* фолбэк: DSS в IM 1 */ - } + + /* DSS работает в IM 1 — чейн всегда на его обработчик 0x0038. + * НЕ читать «таблицу по I»: I=0x3F — мусорное наследие Spectrum + * ROM, чтение [0x3FFF]/[0x4000] даёт случайный адрес (0x00BF) и + * зависание на первом же прерывании. */ + _irq_dss_target = 0x0038; memset(table, h, 257); uint8_t *stub = table + h; /* h < 0xC0 — с [0xFF] не пересекается */ diff --git a/libc/irq/irq_remove.c b/libc/irq/irq_remove.c index 2d5a373..410e9fe 100644 --- a/libc/irq/irq_remove.c +++ b/libc/irq/irq_remove.c @@ -1,7 +1,10 @@ /* - * irq_remove — снять user-handler и вернуть IM2-таблицу DSS - * (I := сохранённое значение). Идемпотентно; вызывается и из atexit. - * Если DSS был в IM 1 (old_i == 0) — восстанавливаем IM 1. + * irq_remove — снять user-handler и вернуть систему в штатный режим + * DSS: I := сохранённое значение (наследие Spectrum ROM, обычно 0x3F) + * и ВСЕГДА IM 1 — DSS работает в IM 1, его обработчик на 0x0038 + * (так же делают сэмплы сообщества: sprinterIntLib.asm + * intRestoreDefaultInterrupt, SIO_CTC_KEY.asm). Идемпотентно; + * вызывается и из atexit. */ #include "_irq.h" @@ -10,13 +13,8 @@ static void irq_restore(uint8_t i_val) __naked { (void)i_val; __asm - ;; A = старое I. 0 → DSS работал в IM 1. + ;; A = старое I (только регистр; режим DSS — всегда IM 1). ld i, a - or a, a - jr Z, _irqr_im1 - im 2 - ret - _irqr_im1: im 1 ret __endasm; diff --git a/tests/irqtest/irqtest.c b/tests/irqtest/irqtest.c index 23e9874..296a25e 100644 --- a/tests/irqtest/irqtest.c +++ b/tests/irqtest/irqtest.c @@ -21,6 +21,21 @@ static void on_tick(void) ticks++; } +/* Диагностика: регистр I до установки (что за режим у DSS?). */ +static uint8_t get_i(void) __naked +{ + __asm + ld a, i + ret + __endasm; +} + +/* Busy-пауза без единого ESTEX-вызова (~симв. полсекунды). */ +static void busy_wait(void) +{ + for (volatile uint16_t i = 0; i < 30000u; i++) ; +} + /* Атомарное чтение 16-битного счётчика, разделяемого с ISR. */ static uint16_t read_ticks(void) { @@ -33,11 +48,24 @@ static uint16_t read_ticks(void) int main(void) { + /* --- диагностика ДО установки --- */ + uint8_t old_i = get_i(); + const uint8_t *vec = (const uint8_t *)(((uint16_t)old_i << 8) + 0xFF); + printf("I=%02x, [I:FF]=%02x%02x (DSS isr cand: %04x)\n", + old_i, vec[1], vec[0], + (unsigned)(vec[0] | ((uint16_t)vec[1] << 8))); + if (irq_install(on_tick) != 0) { printf("irq_install failed: errno=%d\n", errno); return 1; } - puts("irq installed; measuring 3 s..."); + puts("irq installed"); + + /* Фаза 1: busy-пауза БЕЗ ESTEX — жив ли трамплин/чейн вообще. */ + busy_wait(); + printf("phase1: ticks=%u (expect >0)\n", read_ticks()); + + puts("phase2: measuring 3 s vs time()..."); /* Выровняться на границу секунды (time() зовёт ESTEX SYSTIME в * цикле — заодно стресс чейна к DSS). */