From a6fe50e247dca9f3fb2e06e91536dfee1d06f227 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 6 Jul 2026 21:21:16 +0300 Subject: [PATCH] =?UTF-8?q?libc:=20IM2=20user-ISR,=20Phase=201=20=E2=80=94?= =?UTF-8?q?=20libc/irq=20(irq=5Finstall/irq=5Fremove)=20+=20irqtest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проще исходного плана (docs/im2_isr_design.md обновлён): отдельный --memory im2 не понадобился. - : irq_install(handler) — вызов ~50 Гц только на КАДРОВЫХ прерываниях (клавиатура bit0:0x19 и CBL bit7:0xFE отфильтровываются); штатный обработчик DSS чейнится ВСЕГДА (SMC-jp, адрес из старой IM2-таблицы по регистру I) — клавиатура/SYSTIME/мышь живут - вектор-таблица: 513 Б BSS + runtime-выравнивание; Sprinter шлёт только вектор 0xFF, поэтому jp-заглушка лежит внутри самой таблицы по смещению H — без linker-областей и правок crt0 - трамплин: полный сейв обоих наборов+IX/IY вокруг user-handler'а (ex af,af' как .db 0x08 — апостроф ломает препроцессор SDCC) - tiny/big: работает (код в W2); small/huge: EINVAL по проверке адресов; irq_remove идемпотентен и висит на atexit (выход без снятия = I в памяти умершего процесса = крах шелла); old_I==0 → IM1 - tests/irqtest: тики за 3 с против time() (~50 Гц), живая клавиатура под handler'ом, остановка после remove, чистый выход - docs: im2_isr_design (статус+дельты), libc-reference (), TODO Co-Authored-By: Claude Fable 5 --- Makefile | 2 +- docs/TODO.md | 11 +++--- docs/im2_isr_design.md | 26 +++++++++++- docs/libc-reference.md | 11 ++++++ docs/size_baseline.tsv | 1 + libc/include/irq.h | 42 ++++++++++++++++++++ libc/irq/_irq.h | 36 +++++++++++++++++ libc/irq/_irq_state.c | 10 +++++ libc/irq/_irq_tramp.c | 72 +++++++++++++++++++++++++++++++++ libc/irq/irq_install.c | 88 +++++++++++++++++++++++++++++++++++++++++ libc/irq/irq_remove.c | 32 +++++++++++++++ tests/irqtest/Makefile | 5 +++ tests/irqtest/irqtest.c | 65 ++++++++++++++++++++++++++++++ 13 files changed, 394 insertions(+), 7 deletions(-) create mode 100644 libc/include/irq.h create mode 100644 libc/irq/_irq.h create mode 100644 libc/irq/_irq_state.c create mode 100644 libc/irq/_irq_tramp.c create mode 100644 libc/irq/irq_install.c create mode 100644 libc/irq/irq_remove.c create mode 100644 tests/irqtest/Makefile create mode 100644 tests/irqtest/irqtest.c diff --git a/Makefile b/Makefile index 30e4c65..48cc292 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \ malloc mem_test argv errno rt_test openenv ls conio conio2 \ attrprob timedir mouse banklocl stdlib assrtest ptime stattest \ - filetest fdmax fbench solidt dec_test gets stest2 winrest \ + filetest fdmax fbench solidt irqtest dec_test gets stest2 winrest \ bios_text text_palette \ gfx_demo gfx_d16 gfx_text gfx_mous gfx_dbuf diff --git a/docs/TODO.md b/docs/TODO.md index 0db398b..f47b301 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -35,13 +35,14 @@ imagesize/getimage/putimage (COPY/XOR/AND/OR/NOT_PUT), setviewport/ Acceptance: типичная BGI-программа переносится без существенных правок. Референс: Turbo C 2.x BGIDEMO. -### IM2 Interrupt Handlers +### IM2 Interrupt Handlers — Phase 1 ГОТОВ (2026-07-06, libc/irq, tests/irqtest; Phase 2: CBL/ISA/цепочки) User-ISR через IM 2 — timer ticks, музыка (AY/COVOX), real-time -игры, async input. Решение: отдельный memory mode `--memory im2`. -Полный research/design: docs/im2_isr_design.md (vector 0xFF общий, -disambiguation по портам 0x19/0xFE; таблица/ISR/стек в W2; chain к -DSS-хендлеру обязателен). +игры, async input. Phase 1 реализован БЕЗ отдельного memory mode: +libc/irq (: irq_install/irq_remove), таблица в BSS с runtime- +выравниванием, jp-заглушка внутри таблицы, чейн к DSS всегда; +работает в tiny/big, в small/huge — EINVAL. Детали и отличия от +исходного плана: docs/im2_isr_design.md. ### Прочее v2 diff --git a/docs/im2_isr_design.md b/docs/im2_isr_design.md index 14274f0..25d3c20 100644 --- a/docs/im2_isr_design.md +++ b/docs/im2_isr_design.md @@ -1,6 +1,30 @@ # IM2 Interrupt Handlers — Design Document -**Status:** РЕАЛИЗАЦИЯ ОТЛОЖЕНА (до пост-релизной версии). Обязательная фича для v2. +**Status: Phase 1 РЕАЛИЗОВАН 2026-07-06** (libc/irq: irq_install/ +irq_remove + трамплин; тест tests/irqtest). Отличия реализации от +плана ниже: + +- **отдельный `--memory im2` НЕ понадобился**: таблица — статический + буфер 513 Б в BSS с выравниванием в рантайме; т.к. Sprinter шлёт + только вектор 0xFF, значимы лишь байты [0xFF]/[0x100], и 3-байтовый + `jp _irq_tramp` лежит ВНУТРИ таблицы по смещению H (H = старший + байт её адреса, < 0xC0 — не пересекается). Никаких linker-областей + и правок crt0; +- работает в tiny/big (код и данные в W2); в small/huge irq_install + возвращает EINVAL (проверка адресов трамплина/буфера); +- **чейн к DSS — ВСЕГДА** (и клавиатура, и кадр, и CBL): не нужно + знать, что DSS делает в своём ISR — SYSTIME/клавиатура/мышь живут. + User-handler зовётся только на кадровых (фильтр: бит 0 порта 0x19, + бит 7 порта 0xFE → мимо); финальный jp — SMC-операнд; +- W3-порт в трамплине НЕ сохраняется: gfx держит DI на время свопов, + а user-handler'у banking запрещён; DSS свои окна сохраняет сам; +- irq_remove вешается на atexit (выход без снятия = I указывает в + память умершего процесса = крах шелла); old_I==0 → восстановление IM 1; +- открытые вопросы 1 (что делает DSS ISR) и 2 (ld a,i) сняты + дизайном «всегда чейн» и безусловным EI. + +Phase 2 (CBL/COVOX handler, ISA, цепочка нескольких хендлеров) — ниже +по плану, не реализовано. Этот документ собирает всё, что мы знаем о прерываниях Sprinter и план реализации user-задаваемых ISR через Z80 IM 2 mode. Когда возьмёмся за реализацию — читать этот файл, чтобы не повторять research. diff --git a/docs/libc-reference.md b/docs/libc-reference.md index a44332c..581739c 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -155,6 +155,17 @@ rect fill_rect` — через акселератор (hline/vline burst до 25 font[row*256+char]). Палитра: `gfx_pal_load/set/get/get_color/reset` (страницы 0..3). +## — user-ISR кадрового прерывания (IM 2) + +| Сигнатура | Описание | +|---|---| +| `int irq_install(isr_t h)` | h зовётся ~50 Гц на кадровых прерываниях; DSS-обработчик чейнится всегда (клавиатура/SYSTIME/мышь живы). 0 / -1+errno (EBUSY повтор, EINVAL — код не в W2: только tiny/big) | +| `void irq_remove(void)` | вернуть таблицу DSS; идемпотентно; висит на atexit | +| `IRQ_DISABLE()/IRQ_ENABLE()` | di/ei — скобки для чтения shared-переменных из main | + +Handler'у нельзя: ESTEX/BIOS-вызовы, gfx_*/своп окон, акселератор, +banked-функции; только volatile-глобалы и быстрая работа (<1 мс). + ## — низкий уровень (BIOS $A4/$A6) `pal_load pal_get pal_set_color pal_get_color` (страница 0..7, diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index f7fb61a..2580948 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -23,6 +23,7 @@ gfx_mous 5542 gfx_text 2560 hello 4176 hello2 4251 +irqtest 4818 ls 4919 malloc 4462 mdview 19122 diff --git a/libc/include/irq.h b/libc/include/irq.h new file mode 100644 index 0000000..60a3ddd --- /dev/null +++ b/libc/include/irq.h @@ -0,0 +1,42 @@ +/* + * irq.h — пользовательский обработчик кадрового прерывания (IM 2). + * + * Sprinter шлёт вектор 0xFF от трёх источников: кадр (50 Гц), + * клавиатура (бит 0 порта 0x19), CBL/COVOX (бит 7 порта 0xFE). + * irq_install() перехватывает IM2-таблицу и зовёт handler ТОЛЬКО на + * кадровых прерываниях; после него (и на всех прочих источниках) + * управление всегда передаётся штатному обработчику DSS — клавиатура, + * SYSTIME и курсор мыши продолжают работать. irq_remove() + * восстанавливает всё как было (и вызывается атоматически из exit). + * + * ТРЕБОВАНИЕ ПАМЯТИ: код и данные программы должны быть в W2 + * (0x8000-0xBFFF) — режимы tiny и big. В small/huge (CODE в W1) + * irq_install вернёт -1/EINVAL: во время прерывания W1 может быть + * перемаплен. Дизайн: docs/im2_isr_design.md. + * + * Правила для handler'а (вызывается ~50 раз/с, все регистры + * сохраняются снаружи): + * МОЖНО: volatile-глобалы, арифметика, короткая работа (<1 мс); + * НЕЛЬЗЯ: ESTEX/BIOS-вызовы (printf/malloc/файлы), gfx_* и любой + * своп W1/W3, акселератор, banked-функции, longjmp наружу. + */ + +#ifndef IRQ_H +#define IRQ_H + +typedef void (*isr_t)(void); + +/* Установить обработчик кадрового прерывания. + * 0 — OK; -1 + errno: EBUSY (уже установлен), EINVAL (код/данные + * не в W2 — неподходящий memory mode). */ +int irq_install(isr_t handler); + +/* Снять обработчик и вернуть IM2-таблицу DSS. Безопасно звать + * повторно; вызывается автоматически при exit(). */ +void irq_remove(void); + +/* Скобки для доступа main-кода к данным, разделяемым с ISR. */ +#define IRQ_DISABLE() __asm__("di") +#define IRQ_ENABLE() __asm__("ei") + +#endif diff --git a/libc/irq/_irq.h b/libc/irq/_irq.h new file mode 100644 index 0000000..e5a8d59 --- /dev/null +++ b/libc/irq/_irq.h @@ -0,0 +1,36 @@ +/* + * _irq.h — внутренности IM2-модуля (НЕ публичный заголовок). + * + * Механика (docs/im2_isr_design.md): + * - таблица векторов: 257 байт с выравниванием 256 внутри статического + * буфера 513 байт (_irq_state.c), заполняется байтом H = старший + * байт её же адреса; CPU по вектору V читает адрес (H<<8|H) — + * трёхбайтовый `jp _irq_tramp` лежит ВНУТРИ таблицы по смещению H + * (безопасно: Sprinter шлёт только V=0xFF, а H < 0xC0); + * - трамплин (_irq_tramp.c): AF-пробник портов 0x19/0xFE; кадровое + * прерывание → полный save → user handler → restore; затем ВСЕГДА + * jp на старый обработчик DSS (SMC-операнд, патчится при install) — + * EI/RETI делает DSS; + * - install: I := H, IM 2; remove: I := старое, IM 2. + */ +#ifndef _IRQ_INTERNAL_H +#define _IRQ_INTERNAL_H + +#include +#include + +/* Буфер под таблицу: 256 (выравнивание) + 257 (таблица). */ +#define _IRQ_VEC_BUF_SIZE (256 + 257) + +extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE]; +extern isr_t _irq_user; /* NULL = не установлен */ +extern uint8_t _irq_old_i; /* I до install */ + +/* Вход трамплина (asm, _irq_tramp.c). */ +void _irq_tramp(void); + +/* SMC-операнд `jp` на старый DSS-обработчик — метка внутри трамплина, + * пишется из irq_install (16-битный адрес). */ +extern uint16_t _irq_dss_target; + +#endif diff --git a/libc/irq/_irq_state.c b/libc/irq/_irq_state.c new file mode 100644 index 0000000..5531960 --- /dev/null +++ b/libc/irq/_irq_state.c @@ -0,0 +1,10 @@ +/* + * _irq_state — данные IM2-модуля: буфер вектор-таблицы, указатель на + * user-handler, сохранённый регистр I. Только данные. + */ + +#include "_irq.h" + +uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE]; +isr_t _irq_user; +uint8_t _irq_old_i; diff --git a/libc/irq/_irq_tramp.c b/libc/irq/_irq_tramp.c new file mode 100644 index 0000000..53e0753 --- /dev/null +++ b/libc/irq/_irq_tramp.c @@ -0,0 +1,72 @@ +/* + * _irq_tramp — вход IM2-прерывания (вектор 0xFF, все источники). + * + * До выяснения источника трогаем только AF: клавиатура (бит 0 порта + * 0x19) и CBL (бит 7 порта 0xFE) уходят напрямую в DSS. Кадровое + * прерывание: полный сейв обоих наборов + IX/IY, вызов user-handler'а, + * симметричный restore. В конце ВСЕГДА jp на старый обработчик DSS + * (он делает свой save/restore и завершает EI/RETI) — клавиатурный + * FIFO, SYSTIME и курсор мыши продолжают обслуживаться. + * + * Операнд финального jp (_irq_dss_target) — SMC, патчится из + * irq_install; модуль в _CODE (RAM на Sprinter, W2 в tiny/big). + */ + +#include "_irq.h" + +void _irq_tramp(void) __naked +{ + __asm + push af + in a, (#0x19) + rrca ; бит 0 — клавиатурный байт принят? + jr C, _irq_chain + in a, (#0xFE) + rlca ; бит 7 — запрос сэмпла CBL? + jr C, _irq_chain + + ;; --- кадровое прерывание --- + push bc + push de + push hl + ld hl, (__irq_user) + ld a, h + or a, l + jr Z, _irq_pop3 ; handler не установлен + + ;; Сохранить теневой набор и индексные — user-код клобберит всё. + .db 0x08 ; ex af,af (теневой AF) + push af + exx + push bc + push de + push hl + push ix + push iy + + ld hl, (__irq_user) + call _irq_call_hl + + pop iy + pop ix + pop hl + pop de + pop bc + exx + pop af + .db 0x08 ; ex af,af (теневой AF) + _irq_pop3: + pop hl + pop de + pop bc + _irq_chain: + pop af + ;; Всегда к DSS — EI/RETI за ним. + .db 0xC3 ; jp nn + __irq_dss_target:: + .dw 0x0038 ; патчится в irq_install + + _irq_call_hl: + jp (hl) + __endasm; +} diff --git a/libc/irq/irq_install.c b/libc/irq/irq_install.c new file mode 100644 index 0000000..930d6c3 --- /dev/null +++ b/libc/irq/irq_install.c @@ -0,0 +1,88 @@ +/* + * irq_install — установить user-handler кадрового прерывания (IM 2). + * + * Строит 257-байтную вектор-таблицу (выравнивание 256 в рантайме, + * заполнение байтом H = старший байт адреса таблицы; `jp _irq_tramp` + * лежит внутри таблицы по смещению H — Sprinter шлёт только вектор + * 0xFF, читаются лишь байты [0xFF]/[0x100]), запоминает старый + * обработчик DSS из его таблицы (по регистру I), патчит SMC-операнд + * чейна и переключает I/IM2. irq_remove вешается на atexit — выход + * без снятия оставил бы I на памяти умершего процесса. + */ + +#include +#include +#include +#include "_irq.h" + +/* Прочитать регистр I. */ +static uint8_t irq_get_i(void) __naked +{ + __asm + ld a, i + ret + __endasm; +} + +/* I := A, режим IM 2. */ +static void irq_set_i_im2(uint8_t i_val) __naked +{ + (void)i_val; + __asm + ;; __sdcccall(1): uint8_t-аргумент уже в A. + ld i, a + im 2 + ret + __endasm; +} + +int irq_install(isr_t handler) +{ + if (!handler) { errno = EINVAL; return -1; } + if (_irq_user) { errno = EBUSY; return -1; } + + /* Трамплин и таблица обязаны жить в W2 (0x8000-0xBFFF): во время + * прерывания W1/W3 могут быть перемаплены. CODE вне W2 = + * неподходящий memory mode (small/huge). */ + uint16_t tramp = (uint16_t)&_irq_tramp; + uint16_t buf = (uint16_t)_irq_vec_buf; + if (tramp < 0x8000 || tramp >= 0xC000 || + buf < 0x8000 || buf + _IRQ_VEC_BUF_SIZE > 0xC000) { + errno = EINVAL; + return -1; + } + + uint8_t *table = (uint8_t *)(((uint16_t)(buf + 255u)) & 0xFF00u); + uint8_t h = (uint8_t)((uint16_t)table >> 8); + + IRQ_DISABLE(); + + _irq_old_i = irq_get_i(); + if (_irq_old_i) { + /* Таблица DSS: адрес обработчика — байты [0xFF]/[0x100]. */ + const uint8_t *old = (const uint8_t *)(((uint16_t)_irq_old_i << 8) + + 0xFF); + _irq_dss_target = (uint16_t)(old[0] | ((uint16_t)old[1] << 8)); + } else { + _irq_dss_target = 0x0038; /* фолбэк: DSS в IM 1 */ + } + + memset(table, h, 257); + uint8_t *stub = table + h; /* h < 0xC0 — с [0xFF] не пересекается */ + stub[0] = 0xC3; /* jp _irq_tramp */ + stub[1] = (uint8_t)(tramp & 0xFF); + stub[2] = (uint8_t)(tramp >> 8); + + _irq_user = handler; + irq_set_i_im2(h); + + IRQ_ENABLE(); + + /* Выход без снятия = крах шелла (I указывает в память процесса). */ + static uint8_t atexit_armed; + if (!atexit_armed) { + atexit_armed = 1; + atexit(irq_remove); + } + return 0; +} diff --git a/libc/irq/irq_remove.c b/libc/irq/irq_remove.c new file mode 100644 index 0000000..2d5a373 --- /dev/null +++ b/libc/irq/irq_remove.c @@ -0,0 +1,32 @@ +/* + * irq_remove — снять user-handler и вернуть IM2-таблицу DSS + * (I := сохранённое значение). Идемпотентно; вызывается и из atexit. + * Если DSS был в IM 1 (old_i == 0) — восстанавливаем IM 1. + */ + +#include "_irq.h" + +static void irq_restore(uint8_t i_val) __naked +{ + (void)i_val; + __asm + ;; A = старое I. 0 → DSS работал в IM 1. + ld i, a + or a, a + jr Z, _irqr_im1 + im 2 + ret + _irqr_im1: + im 1 + ret + __endasm; +} + +void irq_remove(void) +{ + if (!_irq_user) return; + IRQ_DISABLE(); + _irq_user = 0; + irq_restore(_irq_old_i); + IRQ_ENABLE(); +} diff --git a/tests/irqtest/Makefile b/tests/irqtest/Makefile new file mode 100644 index 0000000..5b4da35 --- /dev/null +++ b/tests/irqtest/Makefile @@ -0,0 +1,5 @@ +# Build irqtest.exe — smoke IM2 user-ISR (tiny mode: код/данные в W2). + +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := irqtest +include $(PROJ_ROOT)/app.mk diff --git a/tests/irqtest/irqtest.c b/tests/irqtest/irqtest.c new file mode 100644 index 0000000..23e9874 --- /dev/null +++ b/tests/irqtest/irqtest.c @@ -0,0 +1,65 @@ +/* + * irqtest — smoke-тест IM2-модуля (Phase 1, docs/im2_isr_design.md). + * + * Проверяет: + * 1. тикает ~50 Гц (сверка с time() за 3 секунды); + * 2. клавиатура/SYSTIME живы при установленном handler'е (чейн к DSS); + * 3. irq_remove останавливает тики; + * 4. чистый выход в шелл (I восстановлен). + */ + +#include +#include +#include +#include +#include + +static volatile uint16_t ticks; + +static void on_tick(void) +{ + ticks++; +} + +/* Атомарное чтение 16-битного счётчика, разделяемого с ISR. */ +static uint16_t read_ticks(void) +{ + uint16_t v; + IRQ_DISABLE(); + v = ticks; + IRQ_ENABLE(); + return v; +} + +int main(void) +{ + if (irq_install(on_tick) != 0) { + printf("irq_install failed: errno=%d\n", errno); + return 1; + } + puts("irq installed; measuring 3 s..."); + + /* Выровняться на границу секунды (time() зовёт ESTEX SYSTIME в + * цикле — заодно стресс чейна к DSS). */ + time_t t0 = time(0); + while (time(0) == t0) ; + uint16_t start = read_ticks(); + time_t t1 = time(0); + while (time(0) < t1 + 3) ; + uint16_t got = read_ticks() - start; + + printf("ticks: %u in 3 s (~%u Hz, expect ~50)\n", got, got / 3); + + puts("press any key (keyboard must work)..."); + getch(); + + irq_remove(); + uint16_t frozen = read_ticks(); + time_t t2 = time(0); + while (time(0) < t2 + 1) ; + if (read_ticks() == frozen) puts("remove: ticks frozen, OK"); + else puts("remove: FAIL, still ticking"); + + puts("irqtest done."); + return 0; +}