# IM2 Interrupt Handlers — Design Document **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 с выравниванием в рантайме; т.к. внешний вектор — только 0xFF, значимы лишь байты [0xFF]/[0x100], и 3-байтовый `jp _irq_tramp` лежит ВНУТРИ таблицы по смещению H (H = старший байт её адреса, < 0xC0 — не пересекается). Никаких linker-областей и правок crt0; - работает в tiny/big (код и данные в W2); в small/huge irq_install возвращает EINVAL (проверка адресов трамплина/буфера); - **чейн к DSS — ВСЕГДА и всегда на 0x0038** (и клавиатура, и кадр): не нужно знать, что DSS делает в своём ISR — SYSTIME/клавиатура/мышь живут. User-handler зовётся только на кадровых (фильтр: бит 0 порта 0x19 → мимо); финальный jp — SMC-операнд; - W3-порт в трамплине НЕ сохраняется: gfx держит DI на время свопов, а user-handler'у banking запрещён; DSS свои окна сохраняет сам; - irq_remove вешается на atexit (выход без снятия = IM 2/I указывают в память умершего процесса = крах шелла); восстановление — всегда IM 1. ## Phase 2a — CTC-таймер (РЕАЛИЗОВАН 2026-07-07) `irq_ctc_install(handler, div2, div3)` / `irq_ctc_remove()` — вектор 0x06, ОТДЕЛЬНЫЙ от кадрового 0xFF (решает проблему «кадр и клавиатура неразличимы»). Механика (по docs/samples/sprinterIntLib.asm и SIO_CTC_KEY.asm): - CTC Z84C015: канал 2 тактируется видеотактом **875 кГц (1 тик = 1 знакоместо)** и работает делителем; канал 3 считает от канала 2 и прерывает. f = 875000/(div2*div3), div 0 = 256; - пресет IRQ_CTC_VSYNC_DIV2/3 = 112×160 (2 пикс. линии × 160 = 320 линий) — точное начало кадра ~48.8 Гц; - порты: CH0=0x10, CH2=0x12, CH3=0x13; control-слова 0x57 (ch2: counter, int off) / 0xD7 (ch3: counter, int on); базовый вектор блока пишется в CH0 (0 → вектор ch3 = 0x06); стоп = 0x03 (reset); - **RETI обязателен** в CTC-трамплине: daisy chain Z84C015 снимает IUS только по опкоду RETI — с RET следующее прерывание не придёт; - CTC-путь НЕ чейнится к DSS (личное прерывание); кадровые/клавиатурные 0xFF идут своим путём параллельно; - общая IM2-таблица под счётчиком ссылок (_irq_table.c): кадровый и CTC-хендлеры ставятся/снимаются независимо, последний возвращает I/IM 1; atexit-уборка глушит CTC ОБЯЗАТЕЛЬНО (иначе после выхода прерывания кГц-частоты душат шелл). Phase 2b (CBL/COVOX handler — вернуть фильтр 0xFE.7 при активном CBL, sample-буфер; фундамент Audio API), ISA-вектора, цепочки нескольких хендлеров — не реализовано. Этот документ собирает всё, что мы знаем о прерываниях Sprinter и план реализации user-задаваемых ISR через Z80 IM 2 mode. Когда возьмёмся за реализацию — читать этот файл, чтобы не повторять research. ## Зачем нужны прерывания - **Timer ISR (50/60 Hz)** — счётчик кадров, плавная анимация без busy-loop, тайминги - **Mouse / keyboard async-обработка** — без polling - **Music playback** — AY-3-8910, COVOX через прерывания - **Real-time games** — input + game logic + render в interrupt-driven архитектуре ## Hardware-факты (из docs/converted) ### Sources of vector 0xFF | Источник | Detect bit | Частота | |---|---|---| | Frame (screen refresh) | (default if none of below) | 50/60 Hz | | Keyboard | port `0x19` (COM_A) bit 0 | event-driven | | CBL/COVOX (sound) | port `0xFE` bit 7 (sample request) | sample-rate-dependent | | Mouse | — (hardware interrupt not wired) | — | | ISA | другой vector (configurable) | depends | Источники: - `docs/converted/Forum.txt:956` — кадровые и клавиатурные прерывания приходят с vector 0xFF; различаются по bit 0 порта 0x19. От мыши прерываний нет - `docs/converted/Forum.txt:758-764` — CBL также vector 0xFF, отличить по bit 7 порта 0xFE - `docs/converted/IvanMak.txt:1086` — `READ_KBD`: IN(0x19), bit 0 = "байт принят"; затем IN(0x18) = data byte; нужно drain FIFO (до 3 байт) - `docs/converted/IvanMak.txt:1471` — ВАЖНОЕ ОГРАНИЧЕНИЕ: vector table + ISR + stack ОБЯЗАНЫ быть в области `0x8000..0xBFFF` (window 2). Иначе BIOS будет отключать прерывания на каждой вызове функции ### Что DSS делает в своём ISR (предположения, требует verification) DSS shell имеет свой IM 2 handler: - Drain'ит keyboard FIFO в свой буфер (читается через ESTEX WAITKEY/SCANKEY) - Возможно обновляет ESTEX SYSTIME ($21) tick counter - Возможно poll'ит mouse (хотя hardware-IRQ от mouse нет — может быть software polling) - Refresh курсора мыши (он же видимый и движется в shell) Без chain'инга к DSS: - Сломается клавиатура (ESTEX kbd functions не получат байты) - Может сломаться SYSTIME counter - Может перестать обновляться mouse cursor ### IM2-трюк Стандартная схема для одиночного ISR address: 1. Аллоцировать **257-байтный** буфер заполненный одинаковым байтом `H` 2. Загрузить `I = H` (например `H=0xA3` → table at `0xA300`, обращения `0xA300..0xA400`) 3. При прерывании CPU читает байт по `(I<<8)|v` и следующий 4. Если оба байта = `H` → ISR address = `(H<<8)|H` = `HHHH` 5. По адресу `HHHH` положить `jp real_isr` Поскольку для нас интересен только vector 0xFF: read bytes at `(0xA3FF)` and `(0xA400)`. Если table заполнена H=0xA3 — оба байта читаются как 0xA3. ISR_ADDR = `0xA3A3`. По адресу 0xA3A3 кладём 3-байтовый `jp _trampoline`. ## Предлагаемый дизайн ### Public API (libc/include/irq.h) ```c typedef void (*isr_t)(void); int irq_install(isr_t handler); /* 0 OK, -1 error (already installed) */ void irq_remove(void); /* Convenience macros — wrap DI/EI when modifying volatile globals * shared between main and ISR. */ #define IRQ_DISABLE() __asm di __endasm #define IRQ_ENABLE() __asm ei __endasm ``` Пример использования: ```c volatile uint16_t ticks = 0; void on_tick(void) { ticks++; } int main(void) { irq_install(on_tick); uint16_t start = ticks; while (ticks - start < 50) { /* wait 1s */ } irq_remove(); } ``` ### Внутренности **Аллокация vector page:** - Static buffer 513 байт в `_BSS` (sprinter.lib). - Размер 513 = 256 (выравнивание) + 257 (сама table) — в худшем случае выравнивание тратит 256 байт. - Внутри буфера ищем 256-byte aligned адрес. SDCC может не поддерживать `__attribute__((aligned(256)))` — придётся через ассемблер с `.area _BSS_ALIGNED` и линкер-флаг для выравнивания, или через runtime поиск aligned position. - **Alternative:** заранее линкуем vector page по фиксированному адресу через linker flag `-Wl-b_VECTORS=0xA300` (как у банков). Стабильнее. **Trampoline в W2:** - Маленький asm-блок (~50 байт) который: 1. `ex af,af'; exx; push ix; push iy` — сохранить ВСЕ регистры 2. `in a, (0xE2); push af` — сохранить current W3 page byte 3. `in a, (0x19); bit 0, a; jr z, _not_kbd` — keyboard? - keyboard path: chain to DSS old ISR (jp/call to saved address) 4. `in a, (0xFE); bit 7, a; jr z, _not_cbl` — CBL? (Phase 2) 5. Frame path: `ld hl, (user_handler); ld a, h; or l; jr z, _no_user; call hl_indirect` 6. `pop af; out (0xE2), a` — restore W3 7. `pop iy; pop ix; exx; ex af,af'; ei; reti` **Где живёт trampoline:** - Для `tiny` mode: `_CODE` = W2 → естественно - Для `big` mode: `_CODE` = W2 → естественно - Для `small`/`huge`: `_CODE` = W1, **но trampoline ДОЛЖЕН быть в W2** (W1 может swap'нуться) - **Решение:** новая linker area `_TRAMP_W2` с absolute address в W2 (например 0xBE00). sprinter-cc размещает её через `-Wl-b_TRAMP_W2=0xBE00`. trampoline.s помечает себя `.area _TRAMP_W2`. **Chain to DSS:** - В `irq_install`: ```asm ld a, i ; A = current vector page high byte ld (old_I), a ld h, a ld l, #0xFF ld a, (hl) ; A = vector_high (= old_I по trick'у) ld d, a ld e, a ; DE = address of DSS's IM2 jp ld hl, (de) ; HL = DSS's old jp target ld (dss_old_isr), hl ``` - В trampoline keyboard-path: ```asm ld hl, (dss_old_isr) push hl ret ; jumps to DSS ISR which ends with EI; RETI ``` - **Опасность:** DSS's ISR может предполагать что регистры свежие (как только что от CPU) → возможно нужно НЕ saving некоторые регистры до chain'а **`irq_remove`:** - DI - Restore I to old value - Restore IM mode (обычно был IM 2 → IM 2; редко IM 1 if shell upgraded) - Free vector page if dynamically allocated - EI ## Ограничения user handler'а User's ISR может: - Читать/писать volatile globals - Делать дешёвые арифметические операции - Менять `g_text_attr` (но не вызывать putch/cputs) User's ISR НЕ должен: - Вызывать `printf` / `puts` / `malloc` / любые ESTEX/BIOS функции — они могут не быть re-entrant - Использовать `gfx_*` — они swap'ят W3, наш trampoline уже сохраняет порт но если внутри ISR будет повторный swap то trampoline не сможет восстановить - Запускать accelerator (LD D,D и т.д.) — accel меняет систему команд CPU - Долго работать — ISR должен быть быстрым (< 1ms), иначе пропустим следующий ## Открытые вопросы 1. **Что именно DSS делает в своём ISR** — disassemble DSS или вызвать его с инструментировкой 2. **`ld a, i` semantics** на Sprinter — на Z80 P/V flag отражает IFF2; нужно для save/restore 3. **Alignment vector page** — найти SDCC-совместимый способ: либо linker absolute area, либо runtime align внутри 513-байтного буфера 4. **Re-entrancy ESTEX из main во время ISR**: - Если main вызывает ESTEX и в это время приходит interrupt → DSS chain'инг должен работать корректно (DSS уже спроектирован под IM 2) - Если main вызывает BIOS (RST 8) — это отключает прерывания на время вызова, OK 4. **Memory budget** — vector page 513 байт в BSS уменьшит heap. В tiny mode с heap ~10KB это ~5%. OK. ## Phase 1 acceptance - `examples/irq_test/` — счётчик тиков растёт с 50 Hz - Клавиатура продолжает работать через DSS chain (можно прервать тест клавишей) - Корректный exit — DSS shell получает управление обратно без crash - Работает во всех memory modes (tiny, small, big, huge) - Memory note `memory/sprinter_im2_isr.md` с описанием ABI и ограничений ## Phase 2 (когда понадобится) - CBL/COVOX prerequisite handler (для audio playback) - ISA interrupt handler (для ZX-Bus карт) - Multiple user handler chain (e.g. tick + sound) ## Альтернатива: отдельный memory mode "im2" Идея: вместо того чтобы крутить trampoline location во всех существующих режимах, сделать **отдельный `--memory im2`** который: - Forces CODE в W2 (как tiny) - Reserves определённый адрес в W2 под vector page и trampoline (например 0xBE00..0xBFFF) - crt0_im2.s ставит IM 2 в начале (заменяет DSS handler с chain) - crt0_im2.s восстанавливает на exit **Плюсы:** - Меньше matrix-сложности (irq работает только в одном mode) - Можно агрессивно reserved'ить W2-память - Тестируется как единое целое **Минусы:** - Программам приходится явно выбирать `--memory im2` для использования прерываний - Дублирование crt0 и runtime Текущее предложение — пойти этим путём (отдельный mode) для v2, не лезть в существующие crt0. ## Внешние ссылки - `docs/converted/IvanMak.txt:1040-1054` — секция 9.3 "Прерывания от ISA" + 9.4 "AT-Клавиатура" - `docs/converted/IvanMak.txt:1469-1473` — IM 2 ограничения (table/stack/ISR в W2) - `docs/converted/Forum.txt:758-764` — CBL interrupt discrimination - `docs/converted/Forum.txt:956` + `:1049` — vector 0xFF disambiguation - `docs/converted/Parinov.txt:601` — IM 1 alternative (handler по адресу 0x0038, не наш путь) ## История - 2026-06-01 — research собран в этот документ, реализация отложена до v2