2c6f4e33c3
_irq_user → _irq_chain[4]+_irq_chain_n (W2-BSS); трамплин tr_frame проходит слоты (один тяжёлый сейв на всю цепь, пустой слот пропуск). API irq_chain_add (0/-1+ENOMEM) / irq_chain_remove(h); irq_install → обёртка chain_add (EBUSY исчез), irq_remove() рвёт всю цепь; refcount таблицы на первом/последнем слоте, мутации под IRQ_DISABLE. Лимит 4 кадровых + 1 CTC. All-modes: трамплин copy-safe (только jr/djnz + литерал jp 0x0038), _irq_table_ref копирует его в _irq_tramp_w2buf (W2) когда оригинал в W1 (small/huge), вектор → на копию; вокруг вызова хендлеров восстанавливает базовую W1-страницу (_irq_app_w1_page = IN 0xA2). Хендлер может лежать где угодно в плоском 0x4000-0xBFFF. Проверено в MAME (tests/irqtest, 2 хендлера): tiny/big/huge — chain h1=h2 → remove h2 → h1 жив/h2=0; huge = код в W1, remap работает. Follow-up: CTC в small/huge = EINVAL (нужна W2-копия _irq_ctc_tramp); small для мелких программ (BSS в W1) = irq_install EINVAL, safe. Доки: im2_isr_design (цепочка), sprite-api §9е (делитель поверх цепи), fast_ram §8. rpgprof: gfx_sprite_ysort в профиль (+230 Б baseline). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
642 lines
46 KiB
Markdown
642 lines
46 KiB
Markdown
# 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) — ДИЗАЙН ниже
|
||
|
||
## Цепочка кадровых обработчиков (план, 2026-07-14)
|
||
|
||
Мотив: сейчас кадровый слот один (`_irq_user`), второй `irq_install`
|
||
даёт `EBUSY`. Как только в программе сойдутся ≥2 потребителя кадрового
|
||
прерывания (FPS-делитель `gfx_set_fps_div` + свой тик/звук приложения),
|
||
одного слота мало. Решение — фиксированный массив слотов, по которому
|
||
трамплин проходит на каждом кадре.
|
||
|
||
### Модель
|
||
|
||
- `isr_t _irq_chain[IRQ_CHAIN_MAX]` (`IRQ_CHAIN_MAX = 4`) + счётчик
|
||
занятости `uint8_t _irq_chain_n` — в `_irq_state.c` (W2-data;
|
||
ЗАМЕНЯЮТ нынешний `isr_t _irq_user`).
|
||
- Порядок вызова = порядок индексов 0..3. Взаимный порядок хендлеров
|
||
**НЕ гарантируется** после remove/add (переиспользуется первая дыра)
|
||
→ хендлеры обязаны быть независимы. Наш `_gfx_frame_isr` (инкремент
|
||
байта) независим по построению.
|
||
- Дисциплина ISR прежняя (`irq.h`): без ESTEX/BIOS/gfx/банков/акселератора.
|
||
|
||
### Трамплин (`_irq_tramp.c`, секция `_irq_frame`)
|
||
|
||
Полный сейв теневого+индексного набора делается ОДИН раз (амортизируется
|
||
на всю цепь), затем цикл по слотам с пропуском NULL:
|
||
|
||
```
|
||
_irq_frame:
|
||
push bc/de/hl
|
||
ld a, (__irq_chain_n) ; ранний выход: цепь пуста → без сейва
|
||
or a, a
|
||
jr Z, _irq_pop3
|
||
... сейв shadow + IX/IY ...
|
||
ld hl, #_irq_chain
|
||
ld b, #IRQ_CHAIN_MAX
|
||
_irq_chain_loop:
|
||
ld e,(hl) / inc hl / ld d,(hl) / inc hl ; DE = слот
|
||
ld a,d / or a,e / jr Z, _irq_chain_skip ; NULL → пропуск
|
||
push bc / push hl
|
||
call _irq_call_de ; jp (de)-обёртка (как _irq_call_hl)
|
||
pop hl / pop bc
|
||
_irq_chain_skip:
|
||
djnz _irq_chain_loop
|
||
... restore ...
|
||
_irq_pop3:
|
||
pop hl/de/bc
|
||
_irq_chain: ... jp DSS ... ; как сейчас
|
||
```
|
||
|
||
Пустой проход 4 слотов ≈ 4×(load+or+skip) ≈ 40Т — копейки на кадре.
|
||
Ранний выход по `_irq_chain_n==0` сохраняет текущую оптимизацию «нет
|
||
хендлера → без тяжёлого сейва» (трамплин может быть установлен ради
|
||
CBL/CTC при пустой кадровой цепи).
|
||
|
||
### API (`irq.h`)
|
||
|
||
```c
|
||
#define IRQ_CHAIN_MAX 4
|
||
int irq_chain_add(isr_t h); /* 0 / -1+ENOMEM (слоты кончились) */
|
||
void irq_chain_remove(isr_t h); /* снять ОДИН слот (по указателю) */
|
||
```
|
||
|
||
Совместимость (без изменения существующих call-sites):
|
||
- `irq_install(h)` → тонкая обёртка `return irq_chain_add(h)`. `EBUSY`
|
||
исчезает (двойной install теперь легален); при 4 занятых — `ENOMEM`.
|
||
- `irq_remove()` (без аргумента) — снимает **ВСЮ** кадровую цепь и
|
||
возвращает IM 1. Семантика exit/atexit сохранена (exit рвёт всё перед
|
||
возвратом в DSS).
|
||
- `gfx_set_fps_div` использует `irq_chain_add`/`irq_chain_remove`
|
||
(ТАРГЕТНО свой `_gfx_frame_isr`), НЕ `irq_remove` — чтобы выключение
|
||
делителя не снесло собственный хендлер приложения.
|
||
|
||
### Refcount таблицы
|
||
|
||
`irq_chain_add`: если `_irq_chain_n == 0` (первый слот) → `_irq_table_ref()`
|
||
(ставит IM2). `irq_chain_remove`: если слот был последним (`_irq_chain_n`
|
||
уходит в 0) → `_irq_table_unref()` (возврат IM1). CBL/CTC-ссылки на
|
||
таблицу считаются отдельно, как сейчас.
|
||
|
||
### Гонка чтения слота (ОБЯЗАТЕЛЬНО)
|
||
|
||
Трамплин читает слот двумя байтовыми `ld`; запись указателя в
|
||
add/remove — 16-бит store. Порванное чтение (низкий байт новый, старший
|
||
старый) = прыжок в мусор. Поэтому мутации слота+счётчика в
|
||
`irq_chain_add`/`irq_chain_remove` ОБЯЗАНЫ идти под `IRQ_DISABLE()` /
|
||
`IRQ_ENABLE()`. Дёшево и обязательно.
|
||
|
||
### Файлы (канон 1 модуль = 1 функция)
|
||
|
||
- `_irq_state.c` — `_irq_user` → `_irq_chain[IRQ_CHAIN_MAX]` + `_irq_chain_n`.
|
||
- `_irq_tramp.c` — переписать секцию `_irq_frame` на цикл (выше).
|
||
- `_irq.h` — объявить `_irq_chain`, `_irq_chain_n`, `IRQ_CHAIN_MAX`.
|
||
- `irq_chain_add.c`, `irq_chain_remove.c` — НОВЫЕ.
|
||
- `irq_install.c` — обёртка над chain_add; `irq_remove.c` — teardown-all
|
||
(чистит все слоты + unref до нуля).
|
||
- `include/irq.h` — публичные объявления + документировать «порядок не
|
||
гарантирован, хендлеры независимы; двойной install легален; 4 слота».
|
||
- `tests/irqtest` — добавить кейс двух хендлеров (оба тикают свои
|
||
счётчики; remove одного не глушит другого; exit рвёт оба).
|
||
|
||
### Ограничения (СНИМАЮТСЯ — см. «все режимы» ниже)
|
||
|
||
- ~~Только tiny/big~~ — цель теперь **все режимы**, дизайн ниже.
|
||
- CTC-цепочка (`irq_ctc_*`) — тем же паттерном, ОТДЕЛЬНЫЙ массив
|
||
(вектор 0x06); делать по потребности, не сейчас.
|
||
|
||
## Вариант «работает во всех режимах памяти» (план 2026-07-14)
|
||
|
||
Постановка: IM2-прерывания должны работать и в small/huge, где CODE
|
||
лежит в W1. Нельзя просто «загнать всё в W2» — обработчик пишет
|
||
пользователь как обычный C, и он компилируется в `_CODE` (=W1 в
|
||
small/huge). Задача — разложить *путь прерывания* так, чтобы он был
|
||
корректен при ЛЮБОМ содержимом W1/W3 в момент прихода прерывания.
|
||
|
||
### Что на пути прерывания и что реально требует W2
|
||
|
||
Прерывание фетчит и исполняет по цепочке: **вектор-таблица → трамплин →
|
||
обработчик(и) → chain на DSS 0x0038**. В момент прерывания стабильно
|
||
замаплены только W0 (ROM/DSS) и **W2** (там стек — DSS это требует,
|
||
окно не перемапливается). W1 перемапливается банками кода (big) и самим
|
||
DSS при iff=1 во время сисколлов (подтверждено артефактом). W3 — банки
|
||
(huge).
|
||
|
||
Ревизия по компонентам:
|
||
1. **Вектор-таблица** — УЖЕ в W2 (`_irq_vec_buf`, BSS; BSS садится в W2
|
||
во всех режимах). Ничего менять не надо.
|
||
2. **Трамплин** (`_irq_tramp`) — сейчас в `_CODE` (=W1 в small/huge).
|
||
ЕДИНСТВЕННОЕ, что валит проверку `tramp < 0x8000` в
|
||
`_irq_table_ref()`. Нужно сделать W2-резидентным.
|
||
3. **Обработчик пользователя** — обычный C в W1/W3. НЕ трогаем — решаем
|
||
через remap (см. ниже).
|
||
4. **Данные цепи** (`_irq_chain`, `_irq_chain_n`, …) — BSS → W2. ОК.
|
||
|
||
### Ключевой приём: не двигать обработчик, а подсунуть ему страницу
|
||
|
||
ВАЖНО (уточнение 2026-07-14): в small/huge программа видит **плоские
|
||
32 КБ 0x4000–0xBFFF** — две статические страницы (W1+W2 подряд), их
|
||
номера фиксированы на всё время работы. Обработчик может лежать ГДЕ
|
||
УГОДНО в этом диапазоне: в W1 (0x4000–0x7FFF), в W2 (0x8000–0xBFFF)
|
||
или даже НА СТЫКЕ через 0x8000. Значит не нужно знать, в каком окне
|
||
хендлер — нужно лишь на время его вызова сделать весь плоский регион
|
||
адресуемым.
|
||
|
||
Обработчик оставляем на месте. Трамплин (в W2) ПЕРЕД вызовом каждого
|
||
хендлера восстанавливает базовую W1-страницу приложения — паттерн
|
||
`__banked`-трамплина (bank.s: `in a,(0xA2)` / set / call / restore).
|
||
W2 всегда = страница приложения (там стек, DSS не перемапливает), так
|
||
что достаточно вернуть только W1 — и весь плоский 0x4000–0xBFFF
|
||
корректен. Это работает, потому что:
|
||
- в small/huge **базовая W1-страница статична** (пользователь её не
|
||
переключает — модель памяти); её номер crt0 фиксирует один раз при
|
||
старте (`IN A,(0xA2)`) в W2-переменную `_irq_app_w1_page`;
|
||
- если прерывание пришло вне сисколла — W1 уже = базовая, set/restore
|
||
это no-op; если ВНУТРИ сисколла (DSS подменил W1) — трамплин ставит
|
||
базовую на время хендлера и возвращает DSS-страницу перед chain на
|
||
0x0038, прерванный сисколл продолжается корректно.
|
||
|
||
Единый «restore W1=base» покрывает все положения хендлера БЕЗ детекции:
|
||
- целиком в W1 → W1=base делает его код валидным;
|
||
- целиком в W2 → W1-restore безвреден (хендлер оттуда не фетчит), W2
|
||
и так = приложение;
|
||
- на стыке 0x8000 → нижняя часть по W1=base, верхняя по W2=приложение
|
||
— обе корректны.
|
||
|
||
Итог по режимам:
|
||
- **tiny/big**: базовый код/данные в W2 (всегда замаплен) → remap НЕ
|
||
нужен и ВРЕДЕН (в tiny/big нет осмысленной «базовой W1»; W1 —
|
||
банк-окно в big). Как сейчас.
|
||
- **small/huge**: плоские 32 КБ → трамплин ставит `_irq_app_w1_page`
|
||
в 0xA2 вокруг вызова.
|
||
Флаг «нужен remap W1» = (mode ∈ {small,huge}); crt0/sprinter-cc его
|
||
выставляет, трамплин честит.
|
||
|
||
**Scope v1:** любые хендлеры в плоском образе приложения (не-banked) —
|
||
т.е. обычный C где угодно в 0x4000–0xBFFF; покрывает практически все
|
||
ISR (счётчик/звук-тик). `__banked`-хендлер (в W3 huge / отдельном
|
||
банке big) потребовал бы хранить его страницу per-слот и мапить в его
|
||
окно — отдельное расширение, не сейчас.
|
||
|
||
### Как сделать трамплин W2-резидентным — два пути
|
||
|
||
**(A) Резерв W2-области (проще, рекомендую для v1).** Трамплин
|
||
(и, при желании, таблица) — в отдельной area `_IM2` с абсолютным
|
||
адресом в верхней части W2 (напр. под стеком). Область НЕнулевая только
|
||
если слинкованы irq-модули (DCE) → цена платится лишь при использовании
|
||
IRQ; heap-top при слинкованном IRQ = `s__IM2`. В small/huge W2-часть
|
||
статического образа грузится crt0 как есть — код окажется в W2 без
|
||
рантайм-копирования. Правка `_irq_table_ref()`: снять проверку
|
||
`tramp < 0x8000` (трамплин теперь по построению в W2).
|
||
|
||
**(B) Рантайм-эмит стаба в W2-BSS (без резерва области).** Скопировать
|
||
PIC-шаблон трамплина в BSS-буфер (W2) при первом install, пропатчив
|
||
абсолютные само-ссылки (SMC, как `_irq_dss_target` уже сейчас). Плюс:
|
||
никакой зарезервированной области, чисто DCE. Минус: трамплин надо
|
||
писать позиционно-независимым (внутренний control-flow только `jr`;
|
||
`call (hl)`-идиому инлайнить или патчить адрес хелпера при копии).
|
||
Оптимизация поверх (A), если резерв области жмёт бюджет small.
|
||
|
||
Ключевой приём (remap базовой страницы) от выбора (A)/(B) не зависит.
|
||
|
||
### Изменения к плану цепочки (выше)
|
||
|
||
- `_irq_table_ref()`: убрать EINVAL-проверку `tramp/buf < 0x8000` после
|
||
переезда трамплина в W2 (таблица уже там).
|
||
- crt0 (все варианты): записать `_irq_app_w1_page = IN A,(0xA2)` при
|
||
старте (для small/huge; в tiny/big просто не используется).
|
||
- Трамплин `_irq_frame`: вокруг `call` хендлера — save/set/restore
|
||
0xA2 под флагом remap; перед `jp 0x0038` вернуть прерванную W1.
|
||
- Тест: собрать `tests/irqtest` во ВСЕХ четырёх режимах; критично —
|
||
small/huge прогон с активным сисколлом в main (файловый цикл), чтобы
|
||
словить прерывание при DSS-подменённой W1.
|
||
|
||
## Альтернатива: отдельный 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
|