Files
Sprinter-SDCC/docs/im2_isr_design.md
T
snark13 5184415fc4 irq: фикс Phase 1 после отладки — DSS работает в IM 1, чейн всегда на 0x0038
Первая версия висла на первом прерывании. Две причины (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 <noreply@anthropic.com>
2026-07-07 09:05:28 +03:00

259 lines
16 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, ИСПРАВЛЕН после отладки**
(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 2 (CBL/COVOX handler, 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