From 8a952b99eb7cd4967287717c0dc1c942dc01e8fa Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 7 Jul 2026 09:15:04 +0300 Subject: [PATCH] =?UTF-8?q?irq:=20Phase=202a=20=E2=80=94=20CTC-=D1=82?= =?UTF-8?q?=D0=B0=D0=B9=D0=BC=D0=B5=D1=80=20=D0=BD=D0=B0=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=BC=20=D0=B2=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B5=200x06=20(Z84C015)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - irq_ctc_install(handler, div2, div3) / irq_ctc_remove: канал 2 CTC делит видеотакт 875 кГц (1 тик = 1 знакоместо), канал 3 считает от него и прерывает; f = 875000/(div2*div3), div 0 = 256. Пресет IRQ_CTC_VSYNC_DIV2/3 = 112*160 — точное начало кадра ~48.8 Гц БЕЗ примеси клавиатуры (вектор 0x06 отделён от общего 0xFF) - CTC-трамплин: полный сейв -> handler -> EI/RETI; RETI обязателен (daisy chain Z84C015 снимает IUS только по опкоду RETI); к DSS не чейнится — личное прерывание - общая IM2-таблица под счётчиком ссылок (_irq_table.c): кадровый и CTC-хендлеры независимы, последний unref возвращает I/IM 1; atexit-уборка глушит CTC обязательно (иначе кГц-прерывания душат шелл после выхода) - порты/слова по docs/samples: CH0=0x10/CH2=0x12/CH3=0x13, 0x57/0xD7/вектор в CH0, стоп 0x03 - irqtest: CTC-vsync параллельно с кадровым + произвольная частота; MAME: frame 48 Гц, ctc(vsync) 49 Гц (parallel frame жив), ctc(50x50) 350 Гц точно по формуле, remove/выход чистые Co-Authored-By: Claude Fable 5 --- docs/im2_isr_design.md | 29 +++++++++- docs/libc-reference.md | 2 + docs/size_baseline.tsv | 2 +- libc/include/irq.h | 20 +++++++ libc/irq/_irq.h | 52 ++++++++++++----- libc/irq/_irq_ctc_tramp.c | 55 ++++++++++++++++++ libc/irq/_irq_state.c | 7 ++- libc/irq/_irq_table.c | 115 +++++++++++++++++++++++++++++++++++++ libc/irq/irq_ctc_install.c | 60 +++++++++++++++++++ libc/irq/irq_ctc_remove.c | 34 +++++++++++ libc/irq/irq_install.c | 87 ++-------------------------- libc/irq/irq_remove.c | 22 ++----- tests/irqtest/irqtest.c | 57 ++++++++++++++++-- 13 files changed, 418 insertions(+), 124 deletions(-) create mode 100644 libc/irq/_irq_ctc_tramp.c create mode 100644 libc/irq/_irq_table.c create mode 100644 libc/irq/irq_ctc_install.c create mode 100644 libc/irq/irq_ctc_remove.c diff --git a/docs/im2_isr_design.md b/docs/im2_isr_design.md index 3ab9dec..6031916 100644 --- a/docs/im2_isr_design.md +++ b/docs/im2_isr_design.md @@ -53,8 +53,33 @@ - irq_remove вешается на atexit (выход без снятия = IM 2/I указывают в память умершего процесса = крах шелла); восстановление — всегда IM 1. -Phase 2 (CBL/COVOX handler, ISA, цепочка нескольких хендлеров) — ниже -по плану, не реализовано. +## 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 handler — вернуть фильтр 0xFE.7 при активном CBL, +sample-буфер; фундамент Audio API), ISA-вектора, цепочки нескольких +хендлеров — не реализовано. Этот документ собирает всё, что мы знаем о прерываниях Sprinter и план реализации user-задаваемых ISR через Z80 IM 2 mode. Когда возьмёмся за реализацию — читать этот файл, чтобы не повторять research. diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 581739c..8a9ba56 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -161,6 +161,8 @@ font[row*256+char]). |---|---| | `int irq_install(isr_t h)` | h зовётся ~50 Гц на кадровых прерываниях; DSS-обработчик чейнится всегда (клавиатура/SYSTIME/мышь живы). 0 / -1+errno (EBUSY повтор, EINVAL — код не в W2: только tiny/big) | | `void irq_remove(void)` | вернуть таблицу DSS; идемпотентно; висит на atexit | +| `int irq_ctc_install(h, div2, div3)` | периодический таймер CTC (вектор 0x06): f = 875000/(div2×div3), div 0=256; пресет кадра IRQ_CTC_VSYNC_DIV2/3 (112×160, ~48.8 Гц); независим от кадрового; трамплин завершает RETI | +| `void irq_ctc_remove(void)` | глушит CTC (обязательно; atexit подстрахует) | | `IRQ_DISABLE()/IRQ_ENABLE()` | di/ei — скобки для чтения shared-переменных из main | Handler'у нельзя: ESTEX/BIOS-вызовы, gfx_*/своп окон, акселератор, diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 2580948..a003b1a 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -23,7 +23,7 @@ gfx_mous 5542 gfx_text 2560 hello 4176 hello2 4251 -irqtest 4818 +irqtest 5834 ls 4919 malloc 4462 mdview 19122 diff --git a/libc/include/irq.h b/libc/include/irq.h index 89aa06b..d63b654 100644 --- a/libc/include/irq.h +++ b/libc/include/irq.h @@ -25,6 +25,8 @@ #ifndef IRQ_H #define IRQ_H +#include + typedef void (*isr_t)(void); /* Установить обработчик кадрового прерывания. @@ -36,6 +38,24 @@ int irq_install(isr_t handler); * повторно; вызывается автоматически при exit(). */ void irq_remove(void); +/* ---- CTC-таймер (Z84C015, вектор 0x06 — отдельный от кадрового) --- + * + * Канал 2 тактируется 875 кГц (1 тик = 1 знакоместо видеотракта) и + * делит; канал 3 считает от канала 2 и прерывает. Частота handler'а: + * f = 875000 / (div2 * div3), div 0 = 256 + * (диапазон ~13 Гц .. десятки кГц; на частотах выше пары кГц handler + * должен быть ОЧЕНЬ коротким). Ставится независимо от irq_install — + * можно иметь оба. Ограничения для handler'а те же, что выше. + * Обработчик завершается RETI (в трамплине) — daisy chain Z84C015. */ + +int irq_ctc_install(isr_t handler, uint8_t div2, uint8_t div3); +void irq_ctc_remove(void); /* глушит CTC; тоже вызывается из exit() */ + +/* Пресет точного кадра: 112 тактов (2 пикс. линии) * 160 = 320 линий, + * ~48.8 Гц, начало кадра — «vsync без примеси клавиатуры». */ +#define IRQ_CTC_VSYNC_DIV2 112 +#define IRQ_CTC_VSYNC_DIV3 160 + /* Скобки для доступа main-кода к данным, разделяемым с ISR. */ #define IRQ_DISABLE() __asm__("di") #define IRQ_ENABLE() __asm__("ei") diff --git a/libc/irq/_irq.h b/libc/irq/_irq.h index fef4823..27ee411 100644 --- a/libc/irq/_irq.h +++ b/libc/irq/_irq.h @@ -4,15 +4,21 @@ * Механика (docs/im2_isr_design.md): * - таблица векторов: 257 байт с выравниванием 256 внутри статического * буфера 513 байт (_irq_state.c), заполняется байтом H = старший - * байт её же адреса; CPU по вектору V читает адрес (H<<8|H) — - * трёхбайтовый `jp _irq_tramp` лежит ВНУТРИ таблицы по смещению H - * (безопасно: внешний вектор — 0xFF, а H < 0xC0; заливка H - * перехватывает и вектора SIO/CTC, если их кто-то включит); - * - трамплин (_irq_tramp.c): AF-пробник порта 0x19 (SIO-A RR0); - * кадровое прерывание → полный save → user handler → restore; - * затем ВСЕГДА jp на обработчик DSS 0x0038 (DSS работает в IM 1; - * SMC-операнд патчится при install) — EI/RETI делает DSS; - * - install: I := H, IM 2; remove: I := старое, IM 1 (штатный режим DSS). + * байт её же адреса; CPU по внешнему вектору 0xFF читает адрес + * (H<<8|H) — трёхбайтовый `jp _irq_tramp` лежит ВНУТРИ таблицы по + * смещению H (H < 0xC0 — с [0xFF]/[0x100] не пересекается; заливка + * H перехватывает и вектора SIO 0x10..0x1E, если их кто-то включит); + * - таблица общая для кадрового и CTC-хендлеров — жизнью управляет + * счётчик ссылок (_irq_table_ref/_irq_table_unref в _irq_table.c); + * unref последнего возвращает I и IM 1 (штатный режим DSS); + * - кадровый трамплин (_irq_tramp.c): AF-пробник порта 0x19 (SIO-A + * RR0, бит 0 = клавиатурный байт); кадр → полный save → user + * handler → restore; затем ВСЕГДА jp на обработчик DSS 0x0038 + * (DSS работает в IM 1) — EI/RETI делает DSS; + * - CTC-трамплин (_irq_ctc_tramp.c): вектор 0x06 (Z84C015, каналы + * 2+3 в связке, база 875 кГц); полный save → handler → restore → + * EI/RETI — RETI ОБЯЗАТЕЛЕН (daisy chain: без него CTC не пошлёт + * следующее прерывание); к DSS НЕ чейнится (наше личное прерывание). */ #ifndef _IRQ_INTERNAL_H #define _IRQ_INTERNAL_H @@ -24,14 +30,32 @@ #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 */ +extern isr_t _irq_user; /* кадровый handler; NULL = нет */ +extern isr_t _irq_ctc_user; /* CTC-handler; NULL = нет */ +extern uint8_t _irq_old_i; /* I до захвата таблицы */ +extern uint8_t _irq_refs; /* счётчик пользователей таблицы */ -/* Вход трамплина (asm, _irq_tramp.c). */ +/* Порты встроенной периферии Z84C015 (docs/samples/SIO_CTC_KEY.asm). */ +#define _IRQ_CTC_CH0 0x10 +#define _IRQ_CTC_CH2 0x12 +#define _IRQ_CTC_CH3 0x13 +#define _IRQ_SIO_A 0x19 /* SIO-A RR0: бит 0 = Rx available */ + +/* Захватить/отпустить IM2-таблицу (счётчик ссылок). ref: при первом + * захвате строит таблицу, ставит I/IM2, взводит atexit-уборку; + * 0 или -1+errno (EINVAL — код/буфер не в W2). unref: при последнем + * отпускании возвращает I и IM 1. */ +int _irq_table_ref(void); +void _irq_table_unref(void); + +/* Адрес выровненной таблицы (валиден при _irq_refs > 0). */ +uint8_t *_irq_table(void); + +/* Входы трамплинов (asm). */ void _irq_tramp(void); +void _irq_ctc_tramp(void); -/* SMC-операнд `jp` на старый DSS-обработчик — метка внутри трамплина, - * пишется из irq_install (16-битный адрес). */ +/* SMC-операнд `jp` на обработчик DSS (метка в _irq_tramp.c). */ extern uint16_t _irq_dss_target; #endif diff --git a/libc/irq/_irq_ctc_tramp.c b/libc/irq/_irq_ctc_tramp.c new file mode 100644 index 0000000..c148648 --- /dev/null +++ b/libc/irq/_irq_ctc_tramp.c @@ -0,0 +1,55 @@ +/* + * _irq_ctc_tramp — вход прерывания CTC-канала 3 (вектор 0x06, IM 2). + * + * Полный сейв обоих наборов + IX/IY, вызов _irq_ctc_user, restore, + * EI/RETI. RETI ОБЯЗАТЕЛЕН: периферия Z84C015 в daisy chain снимает + * своё «interrupt under service» только увидев опкод RETI на шине — + * с обычным RET следующее прерывание от CTC не придёт (см. + * docs/samples/sprinterIntLib.asm). К DSS НЕ чейнимся — это наше + * личное прерывание, не системное. + */ + +#include "_irq.h" + +void _irq_ctc_tramp(void) __naked +{ + __asm + push af + push bc + push de + push hl + .db 0x08 ; ex af,af (теневой AF) + push af + exx + push bc + push de + push hl + push ix + push iy + + ld hl, (__irq_ctc_user) + ld a, h + or a, l + jr Z, _ictc_skip + call _ictc_call_hl + _ictc_skip: + + pop iy + pop ix + pop hl + pop de + pop bc + exx + pop af + .db 0x08 ; ex af,af (теневой AF) + pop hl + pop de + pop bc + pop af + ei + reti + + _ictc_call_hl: + jp (hl) + __endasm; +} diff --git a/libc/irq/_irq_state.c b/libc/irq/_irq_state.c index 5531960..32aa1c5 100644 --- a/libc/irq/_irq_state.c +++ b/libc/irq/_irq_state.c @@ -1,10 +1,13 @@ /* - * _irq_state — данные IM2-модуля: буфер вектор-таблицы, указатель на - * user-handler, сохранённый регистр I. Только данные. + * _irq_state — данные IM2-модуля: буфер вектор-таблицы, указатели на + * user-handler'ы (кадровый и CTC), сохранённый регистр I, счётчик + * ссылок таблицы. Только данные. */ #include "_irq.h" uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE]; isr_t _irq_user; +isr_t _irq_ctc_user; uint8_t _irq_old_i; +uint8_t _irq_refs; diff --git a/libc/irq/_irq_table.c b/libc/irq/_irq_table.c new file mode 100644 index 0000000..2f13be1 --- /dev/null +++ b/libc/irq/_irq_table.c @@ -0,0 +1,115 @@ +/* + * _irq_table — жизненный цикл общей IM2-таблицы (счётчик ссылок). + * + * Первый ref: проверка W2-адресов, построение таблицы (заливка H + + * jp _irq_tramp по смещению H), I := H, IM 2, atexit-уборка. + * Последний unref: I := старое, IM 1 (штатный режим DSS — его + * обработчик на 0x0038; см. docs/samples/sprinterIntLib.asm). + */ + +#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; +} + +/* I := A, штатный IM 1 (DSS). */ +static void irq_set_i_im1(uint8_t i_val) __naked +{ + (void)i_val; + __asm + ld i, a + im 1 + ret + __endasm; +} + +/* Уборка на exit: снять оба хендлера (CTC глушится обязательно — + * иначе после выхода его прерывания задушат шелл). */ +static void irq_atexit(void) +{ + irq_ctc_remove(); + irq_remove(); +} + +uint8_t *_irq_table(void) +{ + return (uint8_t *)(((uint16_t)((uint16_t)_irq_vec_buf + 255u)) + & 0xFF00u); +} + +int _irq_table_ref(void) +{ + if (_irq_refs) { + _irq_refs++; + return 0; + } + + /* Трамплины и таблица обязаны жить в 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 = _irq_table(); + uint8_t h = (uint8_t)((uint16_t)table >> 8); + + IRQ_DISABLE(); + + /* Старое I — только чтобы вернуть регистр при unref: DSS работает + * в IM 1 (I=0x3F — наследие Spectrum ROM, НЕ таблица). */ + _irq_old_i = irq_get_i(); + _irq_dss_target = 0x0038; + + 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_set_i_im2(h); + IRQ_ENABLE(); + + _irq_refs = 1; + + static uint8_t atexit_armed; + if (!atexit_armed) { + atexit_armed = 1; + atexit(irq_atexit); + } + return 0; +} + +void _irq_table_unref(void) +{ + if (!_irq_refs) return; + if (--_irq_refs) return; + IRQ_DISABLE(); + irq_set_i_im1(_irq_old_i); + IRQ_ENABLE(); +} diff --git a/libc/irq/irq_ctc_install.c b/libc/irq/irq_ctc_install.c new file mode 100644 index 0000000..1d375eb --- /dev/null +++ b/libc/irq/irq_ctc_install.c @@ -0,0 +1,60 @@ +/* + * irq_ctc_install — периодический user-таймер на CTC Z84C015 + * (каналы 2+3 в связке, вектор 0x06 — ОТДЕЛЬНЫЙ от кадрового 0xFF). + * + * Канал 2 тактируется видеотактом 875 кГц (1 тик = 1 знакоместо) и + * работает делителем; канал 3 считает от канала 2 и генерирует + * прерывание. Частота = 875000 / (div2 * div3), div 0 = 256. + * Пресет кадра: 112 * 160 (2 пиксельные линии * 160 = 320 линий) — + * точное начало кадра ~48.8 Гц без примеси клавиатуры. + * + * Программирование (docs/samples/sprinterIntLib.asm, SIO_CTC_KEY.asm; + * значения control-слов «надо читать доку по z84c15»): + * 0x57 -> CH2, div2 — счётчик, без прерывания + * 0xD7 -> CH3, div3 — счётчик, прерывание разрешено + * 0x00 -> CH0 — базовый вектор блока CTC = 0 (ch3 -> 0x06) + */ + +#include +#include "_irq.h" + +static uint8_t ctc_div2; +static uint8_t ctc_div3; + +static void ctc_program(void) __naked +{ + __asm + ld a, #0x57 ; CH2: counter, int off, TC следом + out (#_IRQ_CTC_CH2), a + ld a, (_ctc_div2) + out (#_IRQ_CTC_CH2), a + ld a, #0xD7 ; CH3: counter, int on, TC следом + out (#_IRQ_CTC_CH3), a + ld a, (_ctc_div3) + out (#_IRQ_CTC_CH3), a + xor a, a ; базовый вектор CTC = 0 + out (#_IRQ_CTC_CH0), a + ret + __endasm; +} + +int irq_ctc_install(isr_t handler, uint8_t div2, uint8_t div3) +{ + if (!handler) { errno = EINVAL; return -1; } + if (_irq_ctc_user) { errno = EBUSY; return -1; } + if (_irq_table_ref() != 0) return -1; + + uint16_t tramp = (uint16_t)&_irq_ctc_tramp; + uint8_t *table = _irq_table(); + + IRQ_DISABLE(); + _irq_ctc_user = handler; + /* Вектор 0x06 — обычная (не same-byte) запись в таблицу. */ + table[0x06] = (uint8_t)(tramp & 0xFF); + table[0x07] = (uint8_t)(tramp >> 8); + ctc_div2 = div2; + ctc_div3 = div3; + ctc_program(); + IRQ_ENABLE(); + return 0; +} diff --git a/libc/irq/irq_ctc_remove.c b/libc/irq/irq_ctc_remove.c new file mode 100644 index 0000000..444c46c --- /dev/null +++ b/libc/irq/irq_ctc_remove.c @@ -0,0 +1,34 @@ +/* + * irq_ctc_remove — остановить CTC-таймер (мягкий reset канала 3 — + * control-слово 0x03: reset без TC) и отпустить IM2-таблицу. + * Обязателен перед выходом (atexit-уборка зовёт сама): без остановки + * CTC продолжит дёргать INT после возврата в шелл. Идемпотентно. + */ + +#include "_irq.h" + +static void ctc_stop(void) __naked +{ + __asm + ld a, #0x03 ; CH3: soft reset, прерывание выкл + out (#_IRQ_CTC_CH3), a + ld a, #0x03 ; CH2: остановить и делитель + out (#_IRQ_CTC_CH2), a + ret + __endasm; +} + +void irq_ctc_remove(void) +{ + if (!_irq_ctc_user) return; + IRQ_DISABLE(); + ctc_stop(); + _irq_ctc_user = 0; + /* Слоты вектора 0x06 — назад в заливку H (чейн на 0x0038). */ + uint8_t *table = _irq_table(); + uint8_t h = (uint8_t)((uint16_t)table >> 8); + table[0x06] = h; + table[0x07] = h; + IRQ_ENABLE(); + _irq_table_unref(); +} diff --git a/libc/irq/irq_install.c b/libc/irq/irq_install.c index 2dfbee4..f9dc491 100644 --- a/libc/irq/irq_install.c +++ b/libc/irq/irq_install.c @@ -1,96 +1,17 @@ /* - * irq_install — установить user-handler кадрового прерывания (IM 2). - * - * Строит 257-байтную вектор-таблицу (выравнивание 256 в рантайме, - * заполнение байтом H = старший байт адреса таблицы; `jp _irq_tramp` - * лежит внутри таблицы по смещению H — читаются лишь байты - * [0xFF]/[0x100] для внешнего вектора 0xFF; вектора встроенной - * периферии Z84C015 (SIO 0x10..0x1E, CTC 0x06) штатно выключены, но - * заливка H перехватывает и их — чейн на 0x0038 валиден для всех). - * - * ВАЖНО (проверено по docs/samples/sprinterIntLib.asm и - * SIO_CTC_KEY.asm): DSS работает в IM 1 — его обработчик живёт на - * 0x0038, а I=0x3F — наследие Spectrum ROM, НЕ признак IM 2-таблицы. - * Поэтому чейн из трамплина ВСЕГДА идёт на 0x0038 (jp 0x0038 с - * interrupted-PC на стеке в точности имитирует вход IM 1 / RST 38), - * а старое I сохраняется только чтобы вернуть регистр при remove. - * - * irq_remove вешается на atexit — выход без снятия оставил бы I/IM2 - * на памяти умершего процесса. + * irq_install — установить user-handler кадрового прерывания. + * Захватывает общую IM2-таблицу (_irq_table_ref) и включает кадровый + * путь трамплина. Детали и результаты отладки: docs/im2_isr_design.md. */ -#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(); - - /* Старое I — только для восстановления регистра в irq_remove. */ - _irq_old_i = irq_get_i(); - - /* DSS работает в IM 1 — чейн всегда на его обработчик 0x0038. - * НЕ читать «таблицу по I»: I=0x3F — мусорное наследие Spectrum - * ROM, чтение [0x3FFF]/[0x4000] даёт случайный адрес (0x00BF) и - * зависание на первом же прерывании. */ - _irq_dss_target = 0x0038; - - 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); - + if (_irq_table_ref() != 0) return -1; _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 index 410e9fe..034a6ac 100644 --- a/libc/irq/irq_remove.c +++ b/libc/irq/irq_remove.c @@ -1,30 +1,16 @@ /* - * irq_remove — снять user-handler и вернуть систему в штатный режим - * DSS: I := сохранённое значение (наследие Spectrum ROM, обычно 0x3F) - * и ВСЕГДА IM 1 — DSS работает в IM 1, его обработчик на 0x0038 - * (так же делают сэмплы сообщества: sprinterIntLib.asm - * intRestoreDefaultInterrupt, SIO_CTC_KEY.asm). Идемпотентно; - * вызывается и из atexit. + * irq_remove — снять кадровый user-handler и отпустить IM2-таблицу + * (последний пользователь возвращает I/IM 1 — штатный режим DSS). + * Идемпотентно; вызывается и из atexit-уборки. */ #include "_irq.h" -static void irq_restore(uint8_t i_val) __naked -{ - (void)i_val; - __asm - ;; A = старое I (только регистр; режим DSS — всегда IM 1). - ld i, a - im 1 - ret - __endasm; -} - void irq_remove(void) { if (!_irq_user) return; IRQ_DISABLE(); _irq_user = 0; - irq_restore(_irq_old_i); IRQ_ENABLE(); + _irq_table_unref(); } diff --git a/tests/irqtest/irqtest.c b/tests/irqtest/irqtest.c index 296a25e..aa3e237 100644 --- a/tests/irqtest/irqtest.c +++ b/tests/irqtest/irqtest.c @@ -1,11 +1,13 @@ /* - * irqtest — smoke-тест IM2-модуля (Phase 1, docs/im2_isr_design.md). + * irqtest — smoke-тест IM2-модуля (Phase 1+2, docs/im2_isr_design.md). * * Проверяет: - * 1. тикает ~50 Гц (сверка с time() за 3 секунды); + * 1. кадровый handler тикает ~50 Гц (сверка с time() за 3 секунды); * 2. клавиатура/SYSTIME живы при установленном handler'е (чейн к DSS); - * 3. irq_remove останавливает тики; - * 4. чистый выход в шелл (I восстановлен). + * 3. CTC-таймер (вектор 0x06): пресет vsync ~49 Гц ОДНОВРЕМЕННО с + * кадровым; затем быстрый режим 875 кГц/(50*50) = 350 Гц; + * 4. irq_remove/irq_ctc_remove останавливают тики; + * 5. чистый выход в шелл (I/IM восстановлены, CTC заглушен). */ #include @@ -15,12 +17,18 @@ #include static volatile uint16_t ticks; +static volatile uint16_t ctc_ticks; static void on_tick(void) { ticks++; } +static void on_ctc(void) +{ + ctc_ticks++; +} + /* Диагностика: регистр I до установки (что за режим у DSS?). */ static uint8_t get_i(void) __naked { @@ -78,6 +86,47 @@ int main(void) printf("ticks: %u in 3 s (~%u Hz, expect ~50)\n", got, got / 3); + /* --- Phase 2: CTC-таймер одновременно с кадровым --- */ + if (irq_ctc_install(on_ctc, IRQ_CTC_VSYNC_DIV2, IRQ_CTC_VSYNC_DIV3)) { + printf("irq_ctc_install failed: errno=%d\n", errno); + return 1; + } + { + time_t t3 = time(0); + while (time(0) == t3) ; + IRQ_DISABLE(); + uint16_t c0 = ctc_ticks, f0 = ticks; + IRQ_ENABLE(); + time_t t4 = time(0); + while (time(0) < t4 + 2) ; + IRQ_DISABLE(); + uint16_t dc = ctc_ticks - c0, df = ticks - f0; + IRQ_ENABLE(); + printf("ctc(vsync): %u in 2 s (~%u Hz), frame in parallel: %u\n", + dc, dc / 2, df); + } + irq_ctc_remove(); + + /* CTC на произвольной частоте: 875000/(50*50) = 350 Гц. */ + if (irq_ctc_install(on_ctc, 50, 50)) { + printf("ctc fast install failed: errno=%d\n", errno); + return 1; + } + { + time_t t5 = time(0); + while (time(0) == t5) ; + IRQ_DISABLE(); + uint16_t c0 = ctc_ticks; + IRQ_ENABLE(); + time_t t6 = time(0); + while (time(0) < t6 + 2) ; + IRQ_DISABLE(); + uint16_t dc = ctc_ticks - c0; + IRQ_ENABLE(); + printf("ctc(50x50): %u in 2 s (~%u Hz, expect ~350)\n", dc, dc / 2); + } + irq_ctc_remove(); + puts("press any key (keyboard must work)..."); getch();