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>
31 KiB
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,aim 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(режим размера блока) → immediateLD 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, а не для акселератора.
- OTIR (
- 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 порта 0xFEdocs/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:
- Аллоцировать 257-байтный буфер заполненный одинаковым байтом
H - Загрузить
I = H(напримерH=0xA3→ table at0xA300, обращения0xA300..0xA400) - При прерывании CPU читает байт по
(I<<8)|vи следующий - Если оба байта =
H→ ISR address =(H<<8)|H=HHHH - По адресу
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)
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
Пример использования:
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 байт) который:
ex af,af'; exx; push ix; push iy— сохранить ВСЕ регистрыin a, (0xE2); push af— сохранить current W3 page bytein a, (0x19); bit 0, a; jr z, _not_kbd— keyboard?- keyboard path: chain to DSS old ISR (jp/call to saved address)
in a, (0xFE); bit 7, a; jr z, _not_cbl— CBL? (Phase 2)- Frame path:
ld hl, (user_handler); ld a, h; or l; jr z, _no_user; call hl_indirect pop af; out (0xE2), a— restore W3pop iy; pop ix; exx; ex af,af'; ei; reti
Где живёт trampoline:
- Для
tinymode:_CODE= W2 → естественно - Для
bigmode:_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: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:
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), иначе пропустим следующий
Открытые вопросы
- Что именно DSS делает в своём ISR — disassemble DSS или вызвать его с инструментировкой
ld a, isemantics на Sprinter — на Z80 P/V flag отражает IFF2; нужно для save/restore- Alignment vector page — найти SDCC-совместимый способ: либо linker absolute area, либо runtime align внутри 513-байтного буфера
- Re-entrancy ESTEX из main во время ISR:
- Если main вызывает ESTEX и в это время приходит interrupt → DSS chain'инг должен работать корректно (DSS уже спроектирован под IM 2)
- Если main вызывает BIOS (RST 8) — это отключает прерывания на время вызова, OK
- 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 discriminationdocs/converted/Forum.txt:956+:1049— vector 0xFF disambiguationdocs/converted/Parinov.txt:601— IM 1 alternative (handler по адресу 0x0038, не наш путь)
История
- 2026-06-01 — research собран в этот документ, реализация отложена до v2