Files
Sprinter-SDCC/docs/im2_isr_design.md
T
snark13 5086c47f0f libc: IM2 Phase 2b — звук CBL/COVOX через callback fill(), без кольца libc
CBL уже имеет аппаратный буфер 256 Б (2×128, двойная буферизация на
стороне железа) — держать поверх него ещё одно кольцо в libc было бы
лишней копией. cbl_open(freq, fmt, pump_mode, underrun_mode, fill)
регистрирует callback, вызываемый из ISR за очередным блоком; он сам
пропихивает данные приложения (откуда угодно) через cbl_push_otir()/
cbl_push_accel() — без промежуточного буфера.

- два насоса: OTIR (порт 0x4F) и ACCEL (акселератор, спец-страница
  EMM 0xFD@0xC000); OTIR+16-бит запрещён (EINVAL) — по исходнику MAME
  порт данных физически не может собрать 16-бит сэмпл из пары байт;
- форматы CBL_FMT_MONO8/16/STEREO8/16, частоты 7.8..109к;
- CBL_UNDERRUN_APP (по умолчанию, недолив не наша забота) /
  CBL_UNDERRUN_SILENCE (буфер тишины malloc'ится только в этом режиме);
- tests/cbltest: матрица 64 комбинации (2 насоса × 8 форматов × 4
  частоты); tests/cblwav: banked-стрим речи с дискеты (физстраницы
  кэшированы заранее — mem_get_page нельзя звать из fill()/ISR);
  tests/cblstream: единственный случай с собственным кольцом уровня
  приложения (диск нельзя читать из fill()).

Verified в MAME 2026-07-07 — все три теста работают.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:54 +03:00

432 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IM2 Interrupt Handlers — Design Document
**Status: Phase 1 (2026-07-06), Phase 2a CTC (2026-07-07), Phase 2b CBL
(2026-07-07, редизайн на callback без кольца — см. ниже) РЕАЛИЗОВАНЫ**
(libc/irq: irq_install/irq_remove + трамплин, irq_ctc_install/remove;
libc/cbl: cbl_open/close/push_otir/push_accel + fill-callback; тесты
tests/irqtest, tests/cbltest, tests/cblwav, tests/cblstream).
Callback-редизайн **verified в MAME 2026-07-07**: все три CBL-теста
(cbltest — матрица 64 комбинации; cblwav — banked-стрим, голос слышен;
cblstream — собственное кольцо приложения) работают.
## Результаты 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 audio (РЕАЛИЗОВАН 2026-07-07, редизайн без кольца)
`libc/include/cbl.h`: `cbl_open(freq_code, fmt, pump_mode,
underrun_mode, fill)` / `cbl_close()` / `cbl_push_otir(src,n)` /
`cbl_push_accel(src,n)` / `cbl_requests()` / `cbl_underruns()`. По
`docs/samples/Пример для CBL.asm`, разделу «Звук через COVOX-Blaster»
в `docs/converted/Forum.txt`, официальной документации "5.3
COVOX-Blaster" (see below) и `docs/converted/accel_r.txt`:
**Архитектурный редизайн (2026-07-07)**: у CBL УЖЕ ЕСТЬ собственный
аппаратный буфер 256 Б, разбитый на ДВЕ половины по 128 Б (double
buffering целиком на стороне железа — см. официальную доку: "Блок ОЗУ
256 байт условно разбит на две банки по 128 байт, и бит 7 порта #FE
указывает какая из банок ОЗУ выводится в ЦАП... используется программой
вывода для определения, нужно ли подгружать следующие 128 байт").
Держать ЕЩЁ ОДНО кольцо в libc поверх этого — лишний второй буфер и
лишняя копия. Библиотека теперь НЕ хранит кольца: при cbl_open()
регистрируется callback `fill(n)`, который ISR вызывает НАПРЯМУЮ, а
callback сам пропихивает n байт ИЗ ДАННЫХ ПРИЛОЖЕНИЯ (статический
массив, банковая EMM-страница, файл — что угодно) прямо в CBL через
`cbl_push_otir()`/`cbl_push_accel()` — без промежуточной копии в libc.
Пример из офиц. документации делает ровно это: `OUTI` читает прямо из
HL, указывающего в банковую страницу с WAV-данными, без всякого
стейджинга.
`fill(n)` вызывается ИЗ ISR — ОБЯЗАН быть быстрым: никаких
ESTEX/BIOS/gfx-вызовов (тот же констрейнт, что у `irq_install()`-
хендлера). В частности, диск (read() — ESTEX) читать из fill() НЕЛЬЗЯ
— см. tests/cblstream ниже, где под это заведено собственное кольцо
уровня приложения. Возвращает ненулевое при успехе, 0 — недолив.
- **Control-порт 0x004E — 16-битный** (`ld bc,#0x004E` / `out (c),a`,
не 8-битный `out (n),a`). Биты: 7 = CBL on, 6 = stereo, 5 = 16-bit,
4 = interrupt enable, 3..0 = код частоты (8=7.8125 кГц…F=109.375 кГц,
0/1 — legacy-режим без сэмплирования). `cbl_open` шлёт
`0x90|fmt|freq` (on + int + формат + частота) под DI, `cbl_close`
шлёт `0`.
- **Формат — CBL_FMT_MONO8/MONO16/STEREO8/STEREO16** (2026-07-07):
биты 5(16-бит)/6(stereo) напрямую соответствуют константам, можно
OR'ить с freq. Блок запроса — 128 Б для 8-бит, 256 Б для 16-бит
(НЕ зависит от моно/стерео — см. Forum.txt: «для каждых 128 байт
(256 в режиме 16 бит)»); тишина — 0x80 (8-бит unsigned) или 0x0000
(16-бит signed). 8-бит сэмплы центр 0x80, 16-бит центр 0x0000,
stereo — чередование L/R. Runtime-размер блока — `_cbl_block`
(128 или 256), заполняется в `cbl_open` по fmt.
- **Два насоса, выбор — CBL_PUMP_OTIR/CBL_PUMP_ACCEL** (2026-07-07):
- **OTIR** (`_cbl_pump_otir`) — блок в порт 0x4F через `otir`; B=младший
байт `_cbl_block` (128 остаётся 128, 256 идёт как 0 — Z80 OTIR:
B=0 значит 256 итераций). Проверено в MAME (звук слышен, 0
underrun).
- **ACCEL** (`_cbl_pump_accel`) — запись через акселератор в
спец-страницу EMM 0xFD, замапленную в окно W3 на 0xC000 (Forum.txt:
«запись данных в COVOX-Blaster... 128/256 байт с адреса 0xC000»).
Последовательность (по accel_r.txt + рабочему прецеденту
libc/gfx/_gfx_hfill256.c): `LD D,D` (режим размера блока) →
immediate `LD A,n` (SMC-патч, 0 значит 256 — тот же трюк, что и в
OTIR) → `LD L,L` (режим "копирование блока") → `LD A,(HL)` /
`LD (DE),A` (блочное чтение источника → блочная запись в
0xC000@стр.0xFD) → `LD B,B` (выкл). Акселератор НЕ продвигает
HL/DE сам — advance после пересылки делается вручную (`add hl,
(block)`). W3 сохраняется/восстанавливается вокруг переключения
(как `bank_read`/`bank_write`); доп. DI/EI не нужны — весь насос
целиком уже внутри ISR (прерывания замаскированы до EI/RETI
трамплина). **Verified в MAME 2026-07-07** — tests/cbltest, все
32 accel-комбинации матрицы прошли без ошибок/underrun.
- Общий каприз с CBL-примером из docs/samples: там для установки
размера блока акселератора используется `LD C,128` СРАЗУ ЗА
`LD D,D` — это противоречит и accel_r.txt («далее следует команда
типа LD A,dat»), и нашему же подтверждённому на gfx констрейнту
(CLAUDE.md: «block-size ОБЯЗАН быть immediate операндом LD A,n»).
Мы взяли ВЕРИФИЦИРОВАННЫЙ вариант (LD A,n), а не пример — вероятно,
у автора размер уже был установлен раньше (заметка в accel_r.txt:
«если размер блока был установлен ранее, его можно не
устанавливать»), и `LD C,128` в примере готовит BC для последующего
`ADD HL,BC`, а не для акселератора.
- **Data-порт 0x4F** (для OTIR-насоса), блок 128/256 байт.
- **Признак запроса блока — бит 7 порта 0xFE — валиден ТОЛЬКО когда
CBL реально активен.** При выключенном CBL MAME (`kbd_fe_r`)
подтягивает этот бит к 1 всегда (`data |= 0xE0`) — поэтому трамплин
проверяет бит 0xFE.7 не напрямую, а через индирекцию
`_irq_cbl_hook`: пока `cbl_open` не установил хук, кадровые
прерывания даже не читают порт 0xFE (см. Phase 1 — по этой же
причине первая версия фильтра была убрана). Официальная документация
описывает тот же бит как "старший бит счётчика" адреса аппаратного
буфера — по нему же программа определяет, какую половину доливать.
- **Насос (`_cbl_pump_otir`/`_cbl_pump_accel`) теперь тривиален**:
`_cbl_reqs++; ok = _cbl_fill ? _cbl_fill(_cbl_block) : 0; if (!ok) {
_cbl_undr++; ...}`. Обычные (не `__naked`) Си-функции — можно, т.к.
трамплин уже сохраняет ВЕСЬ контекст (оба регистровых набора + IX/IY)
вокруг вызова хука, что бы функция ни наделала с регистрами.
- **Поведение при недоливе — CBL_UNDERRUN_APP/SILENCE** (4-й параметр
cbl_open, 2026-07-07): по умолчанию (`APP`, 0) недолив — не забота
библиотеки, буфер тишины НЕ аллоцируется вовсе, в CBL доигрывает то,
что уже лежало в его аппаратном буфере. `SILENCE` (1) — насос сам
пропихивает тишину (`_cbl_silence`, malloc'ится В cbl_open() ТОЛЬКО
в этом режиме, размером `_cbl_block`, залит 0x80/0x0000 по формату) —
тот же приём, что раньше был жёстко вшит в насос, теперь опционален.
`cbl_underruns()` считает недоливы в ОБОИХ режимах — это только
диагностика.
- **CBL-путь НЕ чейнится к DSS** — личное прерывание CBL, полный сейв
контекста (основной набор + теневой AF/BC/DE/HL + IX/IY, т.к. `fill()`
может клобберить что угодно) → `EI`/`RETI` напрямую, без 0x0038.
- `cbl_open` держит те же анти-повторные гарантии, что и irq/ctc:
занятый хук → EBUSY, `atexit(cbl_close)` регистрируется один раз,
`_irq_table_ref()`/`_irq_table_unref()` для общей IM2-таблицы.
- **tests/cbltest**: полная матрица (2 насоса × 8 форматов × 4 частоты
= 64 комбинации) пилообразного тона, ~1 с каждая; `fill_tone()`
всегда возвращает 1 (period-64 тон никогда не "кончается") —
CBL_UNDERRUN_APP без буфера тишины достаточно. Main не поллит
ничего — просто ждёт halt()'ом. OTIR+16-бит (16/64) пропускаются
заранее (cbl_open вернул бы EINVAL).
- **tests/cblwav**: потоковая речь (78 КБ) с ДИСКЕТЫ через banked EMM
(диск слишком медленный для realtime — клип предзагружается в RAM
ДО cbl_open). `fill_speech()` делает `bank_read()` из уже загруженной
страницы в стейджинг и `cbl_push_otir()` — физические номера страниц
кэшированы в массиве ЗАРАНЕЕ (на этапе загрузки, в main-контексте):
`mem_get_page()` — BIOS-вызов, сам управляет EI/DI, и звать его ИЗ
fill() (то есть из ISR) нельзя — его `ei` при возврате может
преждевременно снять маску прерываний, пока мы ещё внутри ISR.
- **tests/cblstream**: без banked-предзагрузки — чтение с диска
ОДНОВРЕМЕННО с воспроизведением; рассчитан на быстрый носитель (HDD).
Единственный из трёх тестов, где приложению НУЖНО собственное кольцо:
`read()` — ESTEX-вызов, а `fill()` зовётся из ISR, где ESTEX/BIOS под
запретом — поэтому диск читается ТОЛЬКО в main (в кольцо уровня
приложения), а `fill_stream()` лишь копирует уже готовые байты и
пропихивает `cbl_push_otir()`. НЕ входит в общую сборку (`make`/
`make floppy`) — только `cd tests/cblstream && make run`, свой образ
диска.
ISA-вектора, цепочки нескольких хендлеров на одном векторе — не
реализовано (см. «Phase 2 (когда понадобится)» ниже).
**Щелчок перед первым звуком за сессию (2026-07-07, A/B/C-стенд)**:
на РЕАЛЬНОМ файле (tests/cblwav, потоковая речь) перед началом
воспроизведения был слышен щелчок/призвук. Диагностика через
tests/cblwav (banked-стрим с пилой, затем с чистой тишиной вместо
речи, в одном запуске) показала: щелчок слышен ТОЛЬКО на первом
запуске программы после старта MAME и НЕ зависит от содержимого потока
(тон / тишина / речь — одинаково). Проверенное на macOS (`afplay`)
воспроизведение исходного speech.pcm — чистое, артефактов в самом
файле нет. Разбивка `cbl_open()` на две записи в порт 0x004E (сначала
код частоты, потом enable, с паузой) не повлияла — отменена. Вывод:
это одноразовый прогрев звуковой подсистемы MAME при первой активации
канала CBL за сессию эмулятора, не баг протокола/драйвера; на реальном
железе, скорее всего, отсутствует (см. docs/TODO.md).
Этот документ собирает всё, что мы знаем о прерываниях 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~~ — реализован, см. «Phase 2b» выше
- 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