libc/irq: цепочка кадровых обработчиков + all-modes W1-remap
_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>
This commit is contained in:
+26
-18
@@ -5,26 +5,34 @@
|
|||||||
|
|
||||||
## Ближайшее
|
## Ближайшее
|
||||||
|
|
||||||
- [ ] **FPS-делитель (frame pacing)** — gfx_set_fps_div(n): логический
|
Порядок реализации связки: **сначала цепочка irq, потом FPS-делитель
|
||||||
|
поверх неё** (делитель — просто один слот цепи; так снимается EBUSY
|
||||||
|
на сосуществование со своим тиком приложения).
|
||||||
|
|
||||||
|
- [x] **(1) irq: цепочка кадровых обработчиков** — СДЕЛАНО 2026-07-15.
|
||||||
|
Массив `isr_t _irq_chain[4]` + `_irq_chain_n` в W2-data (заменили
|
||||||
|
`_irq_user`); трамплин `tr_frame` проходит слоты (один тяжёлый сейв
|
||||||
|
на всю цепь, пустой слот пропускается); API `irq_chain_add`
|
||||||
|
(0/-1+ENOMEM), `irq_chain_remove(h)`; `irq_install` → обёртка
|
||||||
|
chain_add (EBUSY исчез), `irq_remove()` рвёт всю цепь; refcount
|
||||||
|
таблицы на первом/последнем слоте; мутации под IRQ_DISABLE.
|
||||||
|
All-modes W1-remap: трамплин копируется в W2 (`_irq_tramp_w2buf`,
|
||||||
|
copy-safe — только jr/djnz + литерал jp 0x0038), вокруг вызова
|
||||||
|
восстанавливает базовую W1-страницу (`_irq_app_w1_page` = IN 0xA2
|
||||||
|
при install). ПРОВЕРЕНО в MAME (tests/irqtest, 2 хендлера):
|
||||||
|
tiny/big/huge — chain 97=97 → remove h2 → 48/0; **huge = код в W1,
|
||||||
|
remap работает** (цепочка тикает синхронно). Дизайн:
|
||||||
|
docs/im2_isr_design.md.
|
||||||
|
FOLLOW-UP: (а) CTC-трамплин пока требует код в W2 → в small/huge
|
||||||
|
`irq_ctc_install` = EINVAL (нужна такая же W2-копия _irq_ctc_tramp);
|
||||||
|
(б) small для мелких программ (BSS в W1) → irq_install EINVAL
|
||||||
|
(safe; снять резервом area _IM2 в верхней W2).
|
||||||
|
- [ ] **(2) FPS-делитель (frame pacing)** — gfx_set_fps_div(n): логический
|
||||||
кадр = ровно n кадровых интервалов (50/25/16.7 fps), при
|
кадр = ровно n кадровых интервалов (50/25/16.7 fps), при
|
||||||
переполнении слота — выравнивание на ближайший фронт; счётчик
|
переполнении слота — выравнивание на ближайший фронт; счётчик
|
||||||
кадров через irq_install (только tiny/big в первой версии).
|
кадров через irq_chain_add(_gfx_frame_isr) (только tiny/big в
|
||||||
ПОЛНЫЙ ПЛАН: docs/sprite-api-design.md §9е (обсуждён 2026-07-14).
|
первой версии). ПОЛНЫЙ ПЛАН: docs/sprite-api-design.md §9е
|
||||||
- [ ] **irq: цепочка обработчиков** (дизайн 2026-07-14, реализация по
|
(обсуждён 2026-07-14; ограничение EBUSY снято цепочкой).
|
||||||
потребности — понадобится, когда в программе сойдутся fps-делитель
|
|
||||||
+ свой тик/звук). Вместо одного слота (EBUSY) — фиксированный
|
|
||||||
массив `isr_t _irq_chain[IRQ_CHAIN_MAX /* 4 */]` в W2-data:
|
|
||||||
- трамплин вместо SMC-вызова одного handler'а обходит слоты
|
|
||||||
(load/or a/skip-NULL/call — пустой слот ~40Т, копейки на кадре);
|
|
||||||
- API: `irq_chain_add(h)` (0 / -1+ENOMEM когда слоты кончились),
|
|
||||||
`irq_chain_remove(h)` (по указателю; слот → NULL);
|
|
||||||
- совместимость: `irq_install(h)` становится обёрткой chain_add
|
|
||||||
(EBUSY исчезает), `irq_remove()` БЕЗ аргумента снимает ВСЮ цепь
|
|
||||||
и возвращает IM 1 — семантика exit/atexit сохранена;
|
|
||||||
- порядок вызова = порядок слотов; взаимный порядок хендлеров НЕ
|
|
||||||
гарантируется после remove/add (первая дыра) — хендлеры обязаны
|
|
||||||
быть независимы; дисциплина ISR прежняя (irq.h);
|
|
||||||
- CTC-цепочка (irq_ctc_*) — тем же паттерном, если понадобится.
|
|
||||||
- [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio,
|
- [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio,
|
||||||
ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous —
|
ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous —
|
||||||
трогался asm акселератора); затем прогон на реальном Sprinter
|
трогался asm акселератора); затем прогон на реальном Sprinter
|
||||||
|
|||||||
@@ -193,3 +193,97 @@ _intWaitVsyncSys
|
|||||||
Если будем добавлять поддержку — делать это отдельным opt-in механизмом
|
Если будем добавлять поддержку — делать это отдельным opt-in механизмом
|
||||||
(по аналогии с banked-режимами), с обязательной обёрткой off/restore вокруг всех
|
(по аналогии с banked-режимами), с обязательной обёрткой off/restore вокруг всех
|
||||||
точек входа в систему.
|
точек входа в систему.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Разбор двух предложений по спрайтовому движку (обсуждение 2026-07-14)
|
||||||
|
|
||||||
|
Итог двух обсуждений (эмуляция FastRAM в dev-MAME подтверждена
|
||||||
|
разработчиками MAME как полная).
|
||||||
|
|
||||||
|
### 8.1. Предложение «W0/FastRAM как код-банк `__banked`»
|
||||||
|
|
||||||
|
Идея: выделить 1-2 FastRAM-страницы в crt0, научиться размещать в них
|
||||||
|
`__banked`-код с целью W0 и контрактом «такой код не зовёт DSS/BIOS»;
|
||||||
|
критичным функциям указывать размещение в W0/FastRAM.
|
||||||
|
|
||||||
|
**Framing правильный** (переиспользовать трамплин `__banked` +
|
||||||
|
контракт), но W0-банк — НЕ «ещё один bank id», а тяжелее по трём осям:
|
||||||
|
|
||||||
|
1. **Своп W0 сносит всё окно 0 с RST-векторами** (ESTEX RST10, BIOS
|
||||||
|
RST8, IM1 RST38, RST0). Пока FastRAM в W0: сисколлы невозможны (это
|
||||||
|
и есть контракт — ок), но прерывание фатально (`#0038` → мусор
|
||||||
|
FastRAM) → обязателен `DI` на весь интервал. `DI` бьёт ISR-счётчик
|
||||||
|
кадров (см. FPS-делитель, [[sprite-engine-perf]]).
|
||||||
|
2. **Загрузку нельзя сделать как в `crt0_banked`** (там `ESTEX READ`
|
||||||
|
прямо в окно, строки 165-173): сам READ вектрится через W0. Нужен
|
||||||
|
2-шаг — READ в DRAM-буфер, затем `DI` + FastRAM в W0 + **`LD`-копия**
|
||||||
|
(акселератор с FastRAM не работает!) + выкл. Плюс FastRAM затирается
|
||||||
|
запуском дочернего DSS-процесса — нужна перезагрузка.
|
||||||
|
3. **16 КБ окно против 64 КБ**: кросс-страничные вызовы внутри FastRAM
|
||||||
|
тянут вложенный W0-своп. Вызовы в W1/W2/W3 — свободны (те окна
|
||||||
|
замаплены).
|
||||||
|
|
||||||
|
Плюс **пер-вызовная такса трамплина** (~40-60Т: DI/toggle/EI/restore)
|
||||||
|
съедает выигрыш именно на горячих inner-листьях, которые от FastRAM
|
||||||
|
выигрывают больше всего. Per-function `__banked` — неправильная
|
||||||
|
гранулярность.
|
||||||
|
|
||||||
|
**Выгода** мала: FastRAM ускоряет только выборку инструкций. Из бюджета
|
||||||
|
спрайта 19.5К (blit 9.7 + heal 6.4 + тик 3.0) blit и heal —
|
||||||
|
акселератор/видео-ОЗУ, не выигрывают; выигрывает только тик+сорт по
|
||||||
|
fetch: ~15-20К/кадр ≈ +1 спрайт (3-4% бюджета в самом узком месте).
|
||||||
|
|
||||||
|
**Рекомендация:** если вообще трогать — модель «горячий остров», а не
|
||||||
|
«функция-в-банке»: одна 16-КБ FastRAM-страница с самодостаточным
|
||||||
|
кластером (тик + сортировщик + их листья), вход один раз за кадр через
|
||||||
|
единственную обёртку `DI / своп-в / вызов / своп-обратно / EI`, внутри
|
||||||
|
острова обычные `call`, никаких RST. Убирает таксу, сводит связку с
|
||||||
|
прерываниями к одному DI-интервалу на кадр (мы и так HALT-ждём между
|
||||||
|
кадрами; ISR-счётчик кадров тикает во время HALT-ожидания ПОСЛЕ EI, а
|
||||||
|
не во время острова → совместимо с FPS-делителем, пока остров < 1
|
||||||
|
кадра).
|
||||||
|
|
||||||
|
### 8.2. Данные в FastRAM вместо кода?
|
||||||
|
|
||||||
|
Вывод неожиданный: **для нашей нагрузки данные — более СЛАБЫЙ рычаг,
|
||||||
|
чем код.** Wait-state вставляется на КАЖДЫЙ доступ к DRAM, а на Z80
|
||||||
|
большинство обращений к памяти — выборка инструкций, не операндов. Тик
|
||||||
|
= IX-индексный код (`ld a,(ix+d)` — DD-префикс: 3 байта fetch + 1 байт
|
||||||
|
данных) → ¾ штрафа на fetch, ¼ на операнде. Значит `sprite_t` в FastRAM
|
||||||
|
убирает лишь ¼, а код тика — ¾.
|
||||||
|
|
||||||
|
По данным движка:
|
||||||
|
- **Пиксели/атласы — жёсткое НЕТ** (их читает акселератор; с FastRAM не
|
||||||
|
работает → блит откатился бы на LD-циклы).
|
||||||
|
- **Массив `sprite_t` — не стоит:** тик его трогает, но выигрыш мал (см.
|
||||||
|
выше), а хуже того — его читают и heal/blit под акселератор, которым
|
||||||
|
нужна нормальная карта памяти. `sprite_t` в W0-FastRAM → весь
|
||||||
|
`sprite_update` под `DI` + непроверенный вопрос «работает ли
|
||||||
|
акселератор при W0=FastRAM». Не кандидат, пока это не подтверждено
|
||||||
|
артефактом в MAME.
|
||||||
|
- **Таблица Y-сорта + чисто-CPU скретч — да, но не отдельное решение:**
|
||||||
|
живут в той же 16-КБ странице «горячего острова» с кодом сортировщика
|
||||||
|
(сорт не трогает ни акселератор, ни сисколлы). Отдельного рычага
|
||||||
|
«данные» тут нет.
|
||||||
|
|
||||||
|
**Единственный сильный независимый кейс для данных-в-FastRAM:**
|
||||||
|
CPU-bound алгоритм с большим LUT случайного доступа (fetch внутреннего
|
||||||
|
цикла крошечный, чтений таблицы — тьма). У движка такого нет (тяжёлая
|
||||||
|
графика = акселератор). Но если добавим CPU-side эффект (программный
|
||||||
|
per-pixel шейдинг, палитровый remap, софт-скейлер) — его LUT станет
|
||||||
|
главным кандидатом на FastRAM-данные. На будущее.
|
||||||
|
|
||||||
|
**Ограничения именно для FastRAM-данных:** лежат в окне 0
|
||||||
|
(#0000-#3FFF) → буфер под BIOS/ESTEX туда нельзя (нужно #4000-#BFFF);
|
||||||
|
стек остаётся в W2; не переживают дочерний DSS-процесс; заполняются
|
||||||
|
только `LD`-копией (акселератор мимо).
|
||||||
|
|
||||||
|
### 8.3. Общий вердикт
|
||||||
|
|
||||||
|
Оба варианта реализуемы, но бьют по 3-4% бюджета в самом узком месте
|
||||||
|
(тик+сорт fetch), мимо доминирующих blit/heal. Остаётся **последним
|
||||||
|
резервом** ([[fast_ram]] в памяти). Перед любой реализацией — дешёвый
|
||||||
|
бенч в dev-MAME: один и тот же CPU-loop в DRAM vs FastRAM по
|
||||||
|
`totalcycles`, чтобы измерить реальный коэффициент wait-state, который
|
||||||
|
эмулятор моделирует, и подтвердить, что овчинка стоит выделки.
|
||||||
|
|||||||
+211
-1
@@ -397,7 +397,217 @@ User's ISR НЕ должен:
|
|||||||
|
|
||||||
- ~~CBL/COVOX prerequisite handler~~ — реализован, см. «Phase 2b» выше
|
- ~~CBL/COVOX prerequisite handler~~ — реализован, см. «Phase 2b» выше
|
||||||
- ISA interrupt handler (для ZX-Bus карт)
|
- ISA interrupt handler (для ZX-Bus карт)
|
||||||
- Multiple user handler chain (e.g. tick + sound)
|
- 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"
|
## Альтернатива: отдельный memory mode "im2"
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ gfx_demo 4103
|
|||||||
gfxbanks 6044
|
gfxbanks 6044
|
||||||
hello 4167
|
hello 4167
|
||||||
hello2 4244
|
hello2 4244
|
||||||
irqtest 5850
|
irqtest 6527
|
||||||
ls 4835
|
ls 4835
|
||||||
malloc 4461
|
malloc 4461
|
||||||
mdview 18878
|
mdview 18878
|
||||||
@@ -41,7 +41,7 @@ mouse 4382
|
|||||||
openenv 6124
|
openenv 6124
|
||||||
palfile 5406
|
palfile 5406
|
||||||
ptime 5744
|
ptime 5744
|
||||||
rpgprof 11852
|
rpgprof 12082
|
||||||
rpgwalk 9501
|
rpgwalk 9501
|
||||||
rt_test 4892
|
rt_test 4892
|
||||||
seek 4188
|
seek 4188
|
||||||
|
|||||||
|
+21
-13
@@ -900,19 +900,27 @@ Call-sites НЕ меняются: gfx_wait_vsync() один; n<=1 — СТАРЫ
|
|||||||
(данные; div=0/1 → старый путь; НЕ инициализировать).
|
(данные; div=0/1 → старый путь; НЕ инициализировать).
|
||||||
- common/_gfx_frame_isr.c — ISR: `_gfx_frame_tick++` (правила irq.h:
|
- common/_gfx_frame_isr.c — ISR: `_gfx_frame_tick++` (правила irq.h:
|
||||||
без ESTEX/gfx/банков — здесь чистый инкремент).
|
без ESTEX/gfx/банков — здесь чистый инкремент).
|
||||||
- common/gfx_set_fps_div.c — сеттер: n>=2 → irq_install(_gfx_frame_isr)
|
- common/gfx_set_fps_div.c — сеттер: n>=2 → irq_chain_add(_gfx_frame_isr)
|
||||||
(однократно; повторные вызовы только меняют div), n<=1 →
|
(ТАРГЕТНО свой слот; однократно, повторные вызовы только меняют div),
|
||||||
irq_remove (если ставили) + div=1. Возврат 0 / -1+errno
|
n<=1 → irq_chain_remove(_gfx_frame_isr) (если ставили) + div=1.
|
||||||
(EINVAL/EBUSY от irq_install). ТОЛЬКО этот модуль ссылается на
|
ВАЖНО: chain_remove, НЕ irq_remove — выключение делителя не должно
|
||||||
irq-механику и ISR — DCE: программа без сеттера не тянет ничего
|
снести собственный хендлер приложения (см. цепочку в
|
||||||
(wait ссылается лишь на data-модуль).
|
docs/im2_isr_design.md). Возврат 0 / -1+errno (EINVAL — не tiny/big;
|
||||||
|
ENOMEM — 4 слота заняты). ТОЛЬКО этот модуль ссылается на irq-механику
|
||||||
|
и ISR — DCE: программа без сеттера не тянет ничего (wait ссылается
|
||||||
|
лишь на data-модуль).
|
||||||
- gfx_wait_vsync.c — ветка `if (_gfx_fps_div >= 2)` перед текущим
|
- gfx_wait_vsync.c — ветка `if (_gfx_fps_div >= 2)` перед текущим
|
||||||
поллингом.
|
поллингом.
|
||||||
|
|
||||||
### Ограничения (задокументировать в gfx.h)
|
### Ограничения (задокументировать в gfx.h)
|
||||||
|
|
||||||
1. irq_install работает ТОЛЬКО в tiny/big; в small/huge вернёт EINVAL
|
1. irq_install работает ТОЛЬКО в tiny/big; в small/huge вернёт EINVAL
|
||||||
→ gfx_set_fps_div(2+) там недоступен (rpgprof — small!). ПРИЧИНА
|
→ gfx_set_fps_div(2+) там недоступен (rpgprof — small!). СНИМАЕТСЯ
|
||||||
|
вариантом «все режимы» (docs/im2_isr_design.md «Вариант работает во
|
||||||
|
всех режимах памяти», 2026-07-14): трамплин → W2-резидентный, хендлер
|
||||||
|
остаётся в W1, трамплин восстанавливает статическую базовую W1-страницу
|
||||||
|
приложения вокруг вызова (паттерн __banked). Тогда rpgprof (small)
|
||||||
|
тоже получит делитель. ПРИЧИНА исходного ограничения
|
||||||
(см. docs/im2_isr_design.md): IM2-таблица/трамплин/handler обязаны
|
(см. docs/im2_isr_design.md): IM2-таблица/трамплин/handler обязаны
|
||||||
лежать по адресам, валидным в ЛЮБОЙ момент прихода прерывания —
|
лежать по адресам, валидным в ЛЮБОЙ момент прихода прерывания —
|
||||||
стабильно только W2 (стек обязан быть там по требованию DSS, окно
|
стабильно только W2 (стек обязан быть там по требованию DSS, окно
|
||||||
@@ -929,12 +937,12 @@ Call-sites НЕ меняются: gfx_wait_vsync() один; n<=1 — СТАРЫ
|
|||||||
период кадра 896×320, дрейфа нет; фаза не привязана к лучу — разовая
|
период кадра 896×320, дрейфа нет; фаза не привязана к лучу — разовая
|
||||||
синхронизация поллингом фронта при установке). Делать по
|
синхронизация поллингом фронта при установке). Делать по
|
||||||
потребности.
|
потребности.
|
||||||
2. Один слот user-ISR: приложение со СВОИМ irq_install получит EBUSY
|
2. ~~Один слот user-ISR (EBUSY)~~ — СНЯТО цепочкой обработчиков: сеттер
|
||||||
от сеттера. Обход v1: приложение инкрементирует _gfx_frame_tick из
|
зовёт irq_chain_add, приложение со своим хендлером — тоже слот,
|
||||||
своего обработчика и выставляет _gfx_fps_div напрямую (внутренние
|
оба тикают. Дизайн цепочки: docs/im2_isr_design.md «Цепочка
|
||||||
символы задокументировать); правильное решение — ЦЕПОЧКА
|
кадровых обработчиков». Порядок реализации: цепочка → потом
|
||||||
обработчиков в libc/irq (дизайн записан в docs/TODO.md 2026-07-14,
|
делитель поверх неё. (Осталось лишь: при 4 занятых слотах сеттер
|
||||||
реализация по потребности).
|
вернёт ENOMEM.)
|
||||||
3. W0-атласы + IM2: прерывание при замапленном атласе идёт через
|
3. W0-атласы + IM2: прерывание при замапленном атласе идёт через
|
||||||
IM2-таблицу приложения (W2) → чейн на 0x0038 = ISR-стаб страницы
|
IM2-таблицу приложения (W2) → чейн на 0x0038 = ISR-стаб страницы
|
||||||
атласа → DSS. Путь совместим по построению, но ПРОВЕРИТЬ в MAME
|
атласа → DSS. Путь совместим по построению, но ПРОВЕРИТЬ в MAME
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ int main(void)
|
|||||||
if (gfx_pal_fload(0, "bard.pal") < 0)
|
if (gfx_pal_fload(0, "bard.pal") < 0)
|
||||||
gfx_pal_fload(0, "a:\\bard.pal");
|
gfx_pal_fload(0, "a:\\bard.pal");
|
||||||
gfx_pal_sync();
|
gfx_pal_sync();
|
||||||
|
gfx_sprite_ysort(YSORT_Y);
|
||||||
|
|
||||||
getdatetime(&dt);
|
getdatetime(&dt);
|
||||||
srand((unsigned)(dt.second * 77u + dt.minute));
|
srand((unsigned)(dt.second * 77u + dt.minute));
|
||||||
|
|||||||
+35
-16
@@ -1,19 +1,26 @@
|
|||||||
/*
|
/*
|
||||||
* irq.h — пользовательский обработчик кадрового прерывания (IM 2).
|
* irq.h — пользовательские обработчики кадрового прерывания (IM 2).
|
||||||
*
|
*
|
||||||
* Sprinter шлёт вектор 0xFF от трёх источников: кадр (50 Гц),
|
* Sprinter шлёт вектор 0xFF от трёх источников: кадр (50 Гц),
|
||||||
* клавиатура (бит 0 порта 0x19), CBL/COVOX. Штатно DSS работает в
|
* клавиатура (бит 0 порта 0x19), CBL/COVOX. Штатно DSS работает в
|
||||||
* IM 1 (обработчик 0x0038). irq_install() переключает CPU в IM 2 со
|
* IM 1 (обработчик 0x0038). Установка обработчика переключает CPU в
|
||||||
* своей таблицей и зовёт handler ТОЛЬКО на кадровых прерываниях;
|
* IM 2 со своей таблицей и зовёт handler'ы ТОЛЬКО на кадровых
|
||||||
* после него (и на всех прочих источниках) управление всегда
|
* прерываниях; после них (и на всех прочих источниках) управление
|
||||||
* передаётся штатному обработчику DSS на 0x0038 — клавиатура,
|
* всегда передаётся штатному обработчику DSS на 0x0038 — клавиатура,
|
||||||
* SYSTIME и курсор мыши продолжают работать. irq_remove()
|
* SYSTIME и курсор мыши продолжают работать. irq_remove() возвращает
|
||||||
* возвращает IM 1/I как было (и вызывается автоматически из exit).
|
* IM 1/I как было (и вызывается автоматически из exit).
|
||||||
*
|
*
|
||||||
* ТРЕБОВАНИЕ ПАМЯТИ: код и данные программы должны быть в W2
|
* ЦЕПОЧКА: можно поставить до IRQ_CHAIN_MAX независимых обработчиков
|
||||||
* (0x8000-0xBFFF) — режимы tiny и big. В small/huge (CODE в W1)
|
* (напр. свой тик + FPS-делитель gfx_set_fps_div). Порядок вызова =
|
||||||
* irq_install вернёт -1/EINVAL: во время прерывания W1 может быть
|
* порядок слотов; после remove/add взаимный порядок НЕ гарантируется
|
||||||
* перемаплен. Дизайн: docs/im2_isr_design.md.
|
* (переиспользуется первая дыра) — обработчики обязаны быть независимы.
|
||||||
|
*
|
||||||
|
* ПАМЯТЬ: работает во ВСЕХ режимах (tiny/small/big/huge) — трамплин
|
||||||
|
* при коде в W1 копируется в W2 и восстанавливает базовую W1-страницу
|
||||||
|
* вокруг вызова (docs/im2_isr_design.md). Единственное исключение:
|
||||||
|
* в small для очень мелких программ данные могут ещё лежать в W1 —
|
||||||
|
* тогда установка вернёт -1/EINVAL (перейти на huge или увеличить
|
||||||
|
* размер). Обработчик может быть обычной C-функцией где угодно.
|
||||||
*
|
*
|
||||||
* Правила для handler'а (вызывается ~50 раз/с, все регистры
|
* Правила для handler'а (вызывается ~50 раз/с, все регистры
|
||||||
* сохраняются снаружи):
|
* сохраняются снаружи):
|
||||||
@@ -29,13 +36,25 @@
|
|||||||
|
|
||||||
typedef void (*isr_t)(void);
|
typedef void (*isr_t)(void);
|
||||||
|
|
||||||
/* Установить обработчик кадрового прерывания.
|
/* Максимум одновременных кадровых обработчиков в цепочке. */
|
||||||
* 0 — OK; -1 + errno: EBUSY (уже установлен), EINVAL (код/данные
|
#define IRQ_CHAIN_MAX 4
|
||||||
* не в W2 — неподходящий memory mode). */
|
|
||||||
|
/* Добавить обработчик кадрового прерывания в цепочку.
|
||||||
|
* 0 — OK; -1 + errno: EINVAL (handler==NULL или данные не в W2),
|
||||||
|
* ENOMEM (все IRQ_CHAIN_MAX слотов заняты). */
|
||||||
|
int irq_chain_add(isr_t handler);
|
||||||
|
|
||||||
|
/* Снять ОДИН обработчик из цепочки (по указателю). Идемпотентно;
|
||||||
|
* освобождение последнего слота возвращает IM 1/I штатному DSS. */
|
||||||
|
void irq_chain_remove(isr_t handler);
|
||||||
|
|
||||||
|
/* Совместимость: irq_install == irq_chain_add (EBUSY больше не бывает —
|
||||||
|
* несколько обработчиков легальны; при заполнении слотов — ENOMEM). */
|
||||||
int irq_install(isr_t handler);
|
int irq_install(isr_t handler);
|
||||||
|
|
||||||
/* Снять обработчик и вернуть IM2-таблицу DSS. Безопасно звать
|
/* Снять ВСЮ цепочку и вернуть IM2-таблицу DSS. Безопасно звать
|
||||||
* повторно; вызывается автоматически при exit(). */
|
* повторно; вызывается автоматически при exit(). Для точечного
|
||||||
|
* снятия — irq_chain_remove. */
|
||||||
void irq_remove(void);
|
void irq_remove(void);
|
||||||
|
|
||||||
/* ---- CTC-таймер (Z84C015, вектор 0x06 — отдельный от кадрового) ---
|
/* ---- CTC-таймер (Z84C015, вектор 0x06 — отдельный от кадрового) ---
|
||||||
|
|||||||
+23
-1
@@ -29,12 +29,29 @@
|
|||||||
/* Буфер под таблицу: 256 (выравнивание) + 257 (таблица). */
|
/* Буфер под таблицу: 256 (выравнивание) + 257 (таблица). */
|
||||||
#define _IRQ_VEC_BUF_SIZE (256 + 257)
|
#define _IRQ_VEC_BUF_SIZE (256 + 257)
|
||||||
|
|
||||||
|
/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в
|
||||||
|
* W1). С запасом над реальным размером трамплина (~150 Б). */
|
||||||
|
#define _IRQ_TRAMP_BUF_SIZE 224
|
||||||
|
|
||||||
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
|
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
|
||||||
extern isr_t _irq_user; /* кадровый handler; NULL = нет */
|
|
||||||
|
/* Цепочка кадровых обработчиков (см. _irq_state.c, docs/im2_isr_design). */
|
||||||
|
extern isr_t _irq_chain[IRQ_CHAIN_MAX];
|
||||||
|
extern uint8_t _irq_chain_n; /* число занятых слотов */
|
||||||
|
|
||||||
extern isr_t _irq_ctc_user; /* CTC-handler; NULL = нет */
|
extern isr_t _irq_ctc_user; /* CTC-handler; NULL = нет */
|
||||||
extern uint8_t _irq_old_i; /* I до захвата таблицы */
|
extern uint8_t _irq_old_i; /* I до захвата таблицы */
|
||||||
extern uint8_t _irq_refs; /* счётчик пользователей таблицы */
|
extern uint8_t _irq_refs; /* счётчик пользователей таблицы */
|
||||||
|
|
||||||
|
/* W1-remap (all-modes): базовая W1-страница приложения + сохранённая
|
||||||
|
* страница на входе в прерывание; W2-копия трамплина. */
|
||||||
|
extern uint8_t _irq_app_w1_page;
|
||||||
|
extern uint8_t _irq_saved_w1;
|
||||||
|
extern uint8_t _irq_tramp_w2buf[_IRQ_TRAMP_BUF_SIZE];
|
||||||
|
|
||||||
|
/* Порт-регистр окна W1 (страница). */
|
||||||
|
#define _IRQ_W1_PORT 0xA2
|
||||||
|
|
||||||
/* Хук CBL-насоса (libc/cbl регистрирует _cbl_pump). Косвенность
|
/* Хук CBL-насоса (libc/cbl регистрирует _cbl_pump). Косвенность
|
||||||
* сознательная: прямая ссылка трамплина на cbl-модуль притянула бы
|
* сознательная: прямая ссылка трамплина на cbl-модуль притянула бы
|
||||||
* его (и 2 КБ кольца) в каждую irq-программу. NULL = CBL выключен,
|
* его (и 2 КБ кольца) в каждую irq-программу. NULL = CBL выключен,
|
||||||
@@ -61,6 +78,11 @@ uint8_t *_irq_table(void);
|
|||||||
void _irq_tramp(void);
|
void _irq_tramp(void);
|
||||||
void _irq_ctc_tramp(void);
|
void _irq_ctc_tramp(void);
|
||||||
|
|
||||||
|
/* Метка конца кадрового трамплина — для вычисления его размера при
|
||||||
|
* копировании в W2 (_irq_table_ref). Объявлена как массив, чтобы
|
||||||
|
* можно было взять адрес без вызова. */
|
||||||
|
extern uint8_t _irq_tramp_end[];
|
||||||
|
|
||||||
/* SMC-операнд `jp` на обработчик DSS (метка в _irq_tramp.c). */
|
/* SMC-операнд `jp` на обработчик DSS (метка в _irq_tramp.c). */
|
||||||
extern uint16_t _irq_dss_target;
|
extern uint16_t _irq_dss_target;
|
||||||
|
|
||||||
|
|||||||
+37
-8
@@ -1,14 +1,43 @@
|
|||||||
/*
|
/*
|
||||||
* _irq_state — данные IM2-модуля: буфер вектор-таблицы, указатели на
|
* _irq_state — данные IM2-модуля. Только данные.
|
||||||
* user-handler'ы (кадровый и CTC), сохранённый регистр I, счётчик
|
*
|
||||||
* ссылок таблицы. Только данные.
|
* Кадровый обработчик теперь ЦЕПОЧКА (_irq_chain[]) вместо одного слота
|
||||||
|
* (см. docs/im2_isr_design.md «Цепочка кадровых обработчиков»).
|
||||||
|
*
|
||||||
|
* Всё, к чему трамплин обращается ВО ВРЕМЯ прерывания, обязано жить в
|
||||||
|
* W2 (0x8000-0xBFFF) — единственное окно, которое DSS не перемапливает
|
||||||
|
* (там стек). В tiny/big/huge вся BSS садится в W2 (code-loc W2 либо
|
||||||
|
* data-loc 0x8000); в small для маленьких программ BSS может оказаться
|
||||||
|
* в W1 — тогда _irq_table_ref вернёт EINVAL (safe). Полное снятие
|
||||||
|
* этого ограничения (резерв area _IM2) — отдельный follow-up.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "_irq.h"
|
#include "_irq.h"
|
||||||
|
|
||||||
uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
|
uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
|
||||||
isr_t _irq_user;
|
|
||||||
isr_t _irq_ctc_user;
|
/* Цепочка кадровых обработчиков. Пустой слот = NULL. Порядок вызова =
|
||||||
isr_t _irq_cbl_hook;
|
* порядок индексов; после remove/add взаимный порядок НЕ гарантирован
|
||||||
uint8_t _irq_old_i;
|
* (переиспользуется первая дыра) — хендлеры обязаны быть независимы. */
|
||||||
uint8_t _irq_refs;
|
isr_t _irq_chain[IRQ_CHAIN_MAX];
|
||||||
|
uint8_t _irq_chain_n; /* число занятых слотов (быстрый выход) */
|
||||||
|
|
||||||
|
isr_t _irq_ctc_user; /* CTC-handler; NULL = нет */
|
||||||
|
isr_t _irq_cbl_hook; /* хук CBL-насоса; NULL = нет */
|
||||||
|
uint8_t _irq_old_i; /* I до захвата таблицы */
|
||||||
|
uint8_t _irq_refs; /* счётчик пользователей таблицы */
|
||||||
|
|
||||||
|
/* --- W1-remap для all-modes (см. трамплин) ---------------------------
|
||||||
|
* В small/huge базовый код приложения в W1 (статическая страница).
|
||||||
|
* Трамплин (в W2) вокруг вызова хендлеров восстанавливает эту страницу
|
||||||
|
* в W1, чтобы хендлер, лежащий где угодно в плоском 0x4000-0xBFFF, был
|
||||||
|
* адресуем; на выходе возвращает прочитанное на входе (что было —
|
||||||
|
* возможно DSS-страница, если прервали сисколл). */
|
||||||
|
uint8_t _irq_app_w1_page; /* базовая W1-страница (IN 0xA2 при install) */
|
||||||
|
uint8_t _irq_saved_w1; /* W1 на входе в прерывание (для restore) */
|
||||||
|
|
||||||
|
/* W2-копия трамплина: в small/huge сам _irq_tramp лежит в W1-коде и в
|
||||||
|
* момент прерывания может быть перемаплен — _irq_table_ref копирует его
|
||||||
|
* байты сюда (BSS=W2) и направляет вектор на копию. В tiny/big копия не
|
||||||
|
* нужна (трамплин уже в W2), буфер не используется. */
|
||||||
|
uint8_t _irq_tramp_w2buf[_IRQ_TRAMP_BUF_SIZE];
|
||||||
|
|||||||
+42
-11
@@ -1,10 +1,19 @@
|
|||||||
/*
|
/*
|
||||||
* _irq_table — жизненный цикл общей IM2-таблицы (счётчик ссылок).
|
* _irq_table — жизненный цикл общей IM2-таблицы (счётчик ссылок).
|
||||||
*
|
*
|
||||||
* Первый ref: проверка W2-адресов, построение таблицы (заливка H +
|
* Первый ref: проверка W2-адреса БУФЕРА (данные обязаны быть в W2),
|
||||||
* jp _irq_tramp по смещению H), I := H, IM 2, atexit-уборка.
|
* запись базовой W1-страницы приложения, W2-размещение кадрового
|
||||||
|
* трамплина (прямо или копией — см. ниже), построение таблицы (заливка
|
||||||
|
* H + jp <W2-трамплин> по смещению H), I := H, IM 2, atexit-уборка.
|
||||||
* Последний unref: I := старое, IM 1 (штатный режим DSS — его
|
* Последний unref: I := старое, IM 1 (штатный режим DSS — его
|
||||||
* обработчик на 0x0038; см. docs/samples/sprinterIntLib.asm).
|
* обработчик на 0x0038; см. docs/samples/sprinterIntLib.asm).
|
||||||
|
*
|
||||||
|
* All-modes (docs/im2_isr_design.md): вектор-таблица и данные цепи
|
||||||
|
* должны быть в W2 (это BSS — W2 в tiny/big/huge; в small для мелких
|
||||||
|
* программ может уехать в W1 → EINVAL, safe). Код трамплина в small/
|
||||||
|
* huge лежит в W1 и в момент прерывания может быть перемаплен — если
|
||||||
|
* &_irq_tramp < 0x8000, копируем его байты в _irq_tramp_w2buf (BSS=W2)
|
||||||
|
* и направляем вектор туда. Трамплин написан copy-safe (см. его шапку).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -21,6 +30,18 @@ static uint8_t irq_get_i(void) __naked
|
|||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Прочитать порт-регистр окна W1 (0xA2) — текущая физическая страница.
|
||||||
|
* В main-контексте (install) это статическая базовая страница
|
||||||
|
* приложения (small/huge); в tiny/big значение не используется по
|
||||||
|
* назначению, но remap безвреден. */
|
||||||
|
static uint8_t irq_read_w1_page(void) __naked
|
||||||
|
{
|
||||||
|
__asm
|
||||||
|
in a, (#_IRQ_W1_PORT)
|
||||||
|
ret
|
||||||
|
__endasm;
|
||||||
|
}
|
||||||
|
|
||||||
/* I := A, режим IM 2. */
|
/* I := A, режим IM 2. */
|
||||||
static void irq_set_i_im2(uint8_t i_val) __naked
|
static void irq_set_i_im2(uint8_t i_val) __naked
|
||||||
{
|
{
|
||||||
@@ -65,17 +86,28 @@ int _irq_table_ref(void)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Трамплины и таблица обязаны жить в W2 (0x8000-0xBFFF): во время
|
/* Вектор-таблица и данные цепи обязаны жить в W2 (0x8000-0xBFFF):
|
||||||
* прерывания W1/W3 могут быть перемаплены. CODE вне W2 =
|
* во время прерывания W1/W3 могут быть перемаплены. Это BSS — W2
|
||||||
* неподходящий memory mode (small/huge). */
|
* во всех режимах, КРОМЕ small для мелких программ (данные ещё в
|
||||||
uint16_t tramp = (uint16_t)&_irq_tramp;
|
* W1) → EINVAL, safe. Код трамплина в W1 обрабатывается копией. */
|
||||||
uint16_t buf = (uint16_t)_irq_vec_buf;
|
uint16_t buf = (uint16_t)_irq_vec_buf;
|
||||||
if (tramp < 0x8000 || tramp >= 0xC000 ||
|
if (buf < 0x8000 || buf + _IRQ_VEC_BUF_SIZE > 0xC000) {
|
||||||
buf < 0x8000 || buf + _IRQ_VEC_BUF_SIZE > 0xC000) {
|
|
||||||
errno = EINVAL;
|
errno = EINVAL;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Базовая W1-страница приложения — для W1-remap в трамплине. */
|
||||||
|
_irq_app_w1_page = irq_read_w1_page();
|
||||||
|
|
||||||
|
/* W2-адрес кадрового трамплина: если оригинал уже в W2 (tiny/big) —
|
||||||
|
* прямо; иначе (small/huge, код в W1) — копия в W2-буфер. */
|
||||||
|
uint16_t tramp = (uint16_t)&_irq_tramp;
|
||||||
|
if (tramp < 0x8000) {
|
||||||
|
uint16_t n = (uint16_t)_irq_tramp_end - tramp;
|
||||||
|
memcpy(_irq_tramp_w2buf, (const void *)tramp, n);
|
||||||
|
tramp = (uint16_t)_irq_tramp_w2buf;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t *table = _irq_table();
|
uint8_t *table = _irq_table();
|
||||||
uint8_t h = (uint8_t)((uint16_t)table >> 8);
|
uint8_t h = (uint8_t)((uint16_t)table >> 8);
|
||||||
|
|
||||||
@@ -84,11 +116,10 @@ int _irq_table_ref(void)
|
|||||||
/* Старое I — только чтобы вернуть регистр при unref: DSS работает
|
/* Старое I — только чтобы вернуть регистр при unref: DSS работает
|
||||||
* в IM 1 (I=0x3F — наследие Spectrum ROM, НЕ таблица). */
|
* в IM 1 (I=0x3F — наследие Spectrum ROM, НЕ таблица). */
|
||||||
_irq_old_i = irq_get_i();
|
_irq_old_i = irq_get_i();
|
||||||
_irq_dss_target = 0x0038;
|
|
||||||
|
|
||||||
memset(table, h, 257);
|
memset(table, h, 257);
|
||||||
uint8_t *stub = table + h; /* h < 0xC0 — с [0xFF] не пересекается */
|
uint8_t *stub = table + h; /* h < 0xC0 — с [0xFF] не пересекается */
|
||||||
stub[0] = 0xC3; /* jp _irq_tramp */
|
stub[0] = 0xC3; /* jp <W2-трамплин> */
|
||||||
stub[1] = (uint8_t)(tramp & 0xFF);
|
stub[1] = (uint8_t)(tramp & 0xFF);
|
||||||
stub[2] = (uint8_t)(tramp >> 8);
|
stub[2] = (uint8_t)(tramp >> 8);
|
||||||
|
|
||||||
|
|||||||
+85
-45
@@ -1,23 +1,31 @@
|
|||||||
/*
|
/*
|
||||||
* _irq_tramp — вход IM2-прерывания (вектор 0xFF: кадр/клавиатура/CBL).
|
* _irq_tramp — вход IM2-прерывания (вектор 0xFF: кадр/клавиатура/CBL),
|
||||||
|
* версия с ЦЕПОЧКОЙ кадровых обработчиков и W1-remap для all-modes.
|
||||||
*
|
*
|
||||||
* До выяснения источника трогаем только AF:
|
* Разбор источника (трогаем только AF, пока не выяснили):
|
||||||
* - клавиатура (бит 0 порта 0x19 = SIO-A RR0 «Rx Character
|
* - клавиатура (бит 0 порта 0x19 = SIO-A RR0) → сразу к DSS, БЕЗ
|
||||||
* Available») уходит напрямую в DSS;
|
* remap (W1 не трогаем — прерванный контекст сохранён как есть);
|
||||||
* - CBL (бит 7 порта 0xFE) проверяется ТОЛЬКО при зарегистрированном
|
* - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
|
||||||
* хуке _irq_cbl_hook: при выключенном CBL бит подтянут к 1 (MAME
|
* _irq_cbl_hook (иначе бит подтянут к 1); личное прерывание, RETI
|
||||||
* kbd_fe_r: data |= 0xE0) и проверка ложно срабатывала бы на
|
* без чейна к DSS;
|
||||||
* каждом кадре. CBL-путь: полный сейв → насос → restore →
|
* - кадровое: проход по _irq_chain[] — все ненулевые слоты по порядку.
|
||||||
* EI/RETI, к DSS НЕ чейнится (личное прерывание);
|
|
||||||
* - кадровое: полный сейв → user-handler → restore → чейн.
|
|
||||||
*
|
*
|
||||||
* Чейн — ВСЕГДА jp на обработчик DSS 0x0038 (DSS работает в IM 1;
|
* W1-remap (docs/im2_isr_design.md «Вариант работает во всех режимах»):
|
||||||
* jp с interrupted-PC на стеке имитирует RST 38) — он делает свой
|
* в small/huge базовый код приложения (и хендлеры) в W1, а сам трамплин
|
||||||
* save/restore и завершает EI/RETI: клавиатурный FIFO, SYSTIME и
|
* во время прерывания исполняется из W2-КОПИИ (_irq_table_ref копирует
|
||||||
* курсор мыши живут.
|
* этот код в _irq_tramp_w2buf, т.к. оригинал в W1 может быть перемаплен
|
||||||
|
* DSS). Перед вызовом хендлеров копия сохраняет текущую W1-страницу и
|
||||||
|
* ставит базовую страницу приложения (_irq_app_w1_page), после —
|
||||||
|
* возвращает сохранённую (перед chain к DSS / перед RETI). Единый
|
||||||
|
* «restore W1=base» покрывает хендлер в W1, в W2 и на стыке 0x8000.
|
||||||
|
* В tiny/big remap безвреден (хендлеры в W2, W1 всё равно восстановлен).
|
||||||
*
|
*
|
||||||
* Операнд финального jp (_irq_dss_target) — SMC, патчится из
|
* COPY-SAFE: все внутренние переходы — jr/djnz (относительные);
|
||||||
* _irq_table_ref; модуль в _CODE (RAM на Sprinter, W2 в tiny/big).
|
* единственный call — к _irq_call_hl (исполняется при W1=base, значит
|
||||||
|
* оригинал в W1 достижим); оба jp — литерал 0x0038 (W0, всегда
|
||||||
|
* замаплен). Никаких абсолютных само-ссылок → байты копируются как
|
||||||
|
* есть, без релокации. ВНИМАНИЕ: не добавлять `jp <метка>` внутрь
|
||||||
|
* трамплина — сломает копию.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "_irq.h"
|
#include "_irq.h"
|
||||||
@@ -25,25 +33,35 @@
|
|||||||
void _irq_tramp(void) __naked
|
void _irq_tramp(void) __naked
|
||||||
{
|
{
|
||||||
__asm
|
__asm
|
||||||
push af
|
push af ; сохранить прерванный AF
|
||||||
in a, (#0x19)
|
in a, (#_IRQ_SIO_A)
|
||||||
rrca ; бит 0 — клавиатурный байт принят?
|
rrca ; бит 0 — клавиатурный байт принят?
|
||||||
jr C, _irq_chain
|
jr NC, tr_notkbd
|
||||||
|
|
||||||
;; --- CBL? --- старший байт хука ненулевой у любого
|
;; --- клавиатура: прямо к DSS, без remap ---
|
||||||
;; W2-адреса — для проверки «хук есть» хватает одного байта.
|
pop af
|
||||||
|
.db 0xC3 ; jp nn
|
||||||
|
__irq_dss_target::
|
||||||
|
.dw 0x0038 ; SMC-операбельно, но по умолчанию 0x0038
|
||||||
|
|
||||||
|
tr_notkbd:
|
||||||
|
;; --- CBL? --- старший байт хука ненулевой у любого W2-адреса.
|
||||||
ld a, (__irq_cbl_hook + 1)
|
ld a, (__irq_cbl_hook + 1)
|
||||||
or a, a
|
or a, a
|
||||||
jr Z, _irq_frame
|
jr Z, tr_frame
|
||||||
in a, (#0xFE)
|
in a, (#0xFE)
|
||||||
rlca ; бит 7 — запрос блока CBL?
|
rlca ; бит 7 — запрос блока CBL?
|
||||||
jr NC, _irq_frame
|
jr NC, tr_frame
|
||||||
|
|
||||||
;; --- CBL: сейв, насос, restore, EI/RETI (без DSS) ---
|
;; --- CBL: remap-скобка + хук + приватный RETI ---
|
||||||
|
in a, (#_IRQ_W1_PORT) ; сохранить прерванную W1
|
||||||
|
ld (__irq_saved_w1), a
|
||||||
|
ld a, (__irq_app_w1_page)
|
||||||
|
out (#_IRQ_W1_PORT), a ; W1 = базовая страница приложения
|
||||||
push bc
|
push bc
|
||||||
push de
|
push de
|
||||||
push hl
|
push hl
|
||||||
.db 0x08 ; ex af,af (теневой AF)
|
.db 0x08 ; ex af,af (теневой AF)
|
||||||
push af
|
push af
|
||||||
exx
|
exx
|
||||||
push bc
|
push bc
|
||||||
@@ -60,26 +78,31 @@ void _irq_tramp(void) __naked
|
|||||||
pop bc
|
pop bc
|
||||||
exx
|
exx
|
||||||
pop af
|
pop af
|
||||||
.db 0x08 ; ex af,af (теневой AF)
|
.db 0x08 ; ex af,af
|
||||||
pop hl
|
pop hl
|
||||||
pop de
|
pop de
|
||||||
pop bc
|
pop bc
|
||||||
|
ld a, (__irq_saved_w1) ; вернуть прерванную W1
|
||||||
|
out (#_IRQ_W1_PORT), a
|
||||||
pop af
|
pop af
|
||||||
ei
|
ei
|
||||||
reti
|
reti
|
||||||
|
|
||||||
_irq_frame:
|
tr_frame:
|
||||||
;; --- кадровое прерывание ---
|
;; --- кадровое: цепочка обработчиков ---
|
||||||
|
ld a, (__irq_chain_n)
|
||||||
|
or a, a
|
||||||
|
jr Z, tr_frame_exit ; нет хендлеров → к DSS без remap/сейва
|
||||||
|
|
||||||
|
in a, (#_IRQ_W1_PORT) ; remap-скобка: сохранить + база
|
||||||
|
ld (__irq_saved_w1), a
|
||||||
|
ld a, (__irq_app_w1_page)
|
||||||
|
out (#_IRQ_W1_PORT), a
|
||||||
|
;; полный сейв обоих наборов + индексные (хендлеры клобберят всё)
|
||||||
push bc
|
push bc
|
||||||
push de
|
push de
|
||||||
push hl
|
push hl
|
||||||
ld hl, (__irq_user)
|
.db 0x08
|
||||||
ld a, h
|
|
||||||
or a, l
|
|
||||||
jr Z, _irq_pop3 ; handler не установлен
|
|
||||||
|
|
||||||
;; Сохранить теневой набор и индексные — user-код клобберит всё.
|
|
||||||
.db 0x08 ; ex af,af (теневой AF)
|
|
||||||
push af
|
push af
|
||||||
exx
|
exx
|
||||||
push bc
|
push bc
|
||||||
@@ -88,8 +111,25 @@ void _irq_tramp(void) __naked
|
|||||||
push ix
|
push ix
|
||||||
push iy
|
push iy
|
||||||
|
|
||||||
ld hl, (__irq_user)
|
;; проход по слотам: HL идёт по _irq_chain, B = счётчик слотов
|
||||||
|
ld hl, #__irq_chain
|
||||||
|
ld b, #IRQ_CHAIN_MAX
|
||||||
|
tr_loop:
|
||||||
|
ld e, (hl)
|
||||||
|
inc hl
|
||||||
|
ld d, (hl)
|
||||||
|
inc hl
|
||||||
|
ld a, d
|
||||||
|
or a, e
|
||||||
|
jr Z, tr_skip ; пустой слот
|
||||||
|
push bc ; сохранить счётчик + указатель цепи
|
||||||
|
push hl
|
||||||
|
ex de, hl ; HL = адрес хендлера
|
||||||
call _irq_call_hl
|
call _irq_call_hl
|
||||||
|
pop hl
|
||||||
|
pop bc
|
||||||
|
tr_skip:
|
||||||
|
djnz tr_loop
|
||||||
|
|
||||||
pop iy
|
pop iy
|
||||||
pop ix
|
pop ix
|
||||||
@@ -98,19 +138,19 @@ void _irq_tramp(void) __naked
|
|||||||
pop bc
|
pop bc
|
||||||
exx
|
exx
|
||||||
pop af
|
pop af
|
||||||
.db 0x08 ; ex af,af (теневой AF)
|
.db 0x08
|
||||||
_irq_pop3:
|
|
||||||
pop hl
|
pop hl
|
||||||
pop de
|
pop de
|
||||||
pop bc
|
pop bc
|
||||||
_irq_chain:
|
ld a, (__irq_saved_w1) ; вернуть прерванную W1 перед chain
|
||||||
|
out (#_IRQ_W1_PORT), a
|
||||||
|
tr_frame_exit:
|
||||||
pop af
|
pop af
|
||||||
;; Всегда к DSS (IM 1, обработчик 0x0038) — EI/RETI за ним.
|
.db 0xC3 ; jp 0x0038 (литерал — copy-safe)
|
||||||
.db 0xC3 ; jp nn
|
.dw 0x0038
|
||||||
__irq_dss_target::
|
|
||||||
.dw 0x0038 ; SMC: подтверждается в _irq_table_ref
|
|
||||||
|
|
||||||
_irq_call_hl:
|
_irq_call_hl:
|
||||||
jp (hl)
|
jp (hl)
|
||||||
|
__irq_tramp_end::
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* irq_chain_add — добавить обработчик кадрового прерывания в цепочку.
|
||||||
|
* Возврат 0 / -1+errno: EINVAL (handler==NULL, или данные не в W2 —
|
||||||
|
* из _irq_table_ref), ENOMEM (все IRQ_CHAIN_MAX слотов заняты).
|
||||||
|
*
|
||||||
|
* Первый слот захватывает IM2-таблицу (_irq_table_ref). Порядок вызова
|
||||||
|
* хендлеров = порядок индексов; после remove/add взаимный порядок НЕ
|
||||||
|
* гарантирован — хендлеры обязаны быть независимы (см. irq.h).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include "_irq.h"
|
||||||
|
|
||||||
|
int irq_chain_add(isr_t handler)
|
||||||
|
{
|
||||||
|
if (!handler) { errno = EINVAL; return -1; }
|
||||||
|
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < IRQ_CHAIN_MAX; i++)
|
||||||
|
if (!_irq_chain[i]) break;
|
||||||
|
if (i == IRQ_CHAIN_MAX) { errno = ENOMEM; return -1; }
|
||||||
|
|
||||||
|
/* Первый обработчик — захватить таблицу и включить IM2. Делаем ДО
|
||||||
|
* записи слота: при неудаче (EINVAL в small) состояние не тронуто.
|
||||||
|
* Пока слот не выставлен, chain_n==0 → трамплин на кадре просто
|
||||||
|
* чейнит к DSS. */
|
||||||
|
if (_irq_chain_n == 0)
|
||||||
|
if (_irq_table_ref() != 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
IRQ_DISABLE();
|
||||||
|
_irq_chain[i] = handler;
|
||||||
|
_irq_chain_n++;
|
||||||
|
IRQ_ENABLE();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* irq_chain_remove — снять ОДИН обработчик кадрового прерывания из
|
||||||
|
* цепочки (по указателю). Освобождение последнего слота отпускает
|
||||||
|
* IM2-таблицу (_irq_table_unref → возврат IM 1, если больше никто не
|
||||||
|
* держит). Идемпотентно: неизвестный/NULL handler игнорируется.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "_irq.h"
|
||||||
|
|
||||||
|
void irq_chain_remove(isr_t handler)
|
||||||
|
{
|
||||||
|
if (!handler) return;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < IRQ_CHAIN_MAX; i++) {
|
||||||
|
if (_irq_chain[i] == handler) {
|
||||||
|
IRQ_DISABLE();
|
||||||
|
_irq_chain[i] = 0;
|
||||||
|
_irq_chain_n--;
|
||||||
|
IRQ_ENABLE();
|
||||||
|
if (_irq_chain_n == 0)
|
||||||
|
_irq_table_unref();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,10 @@ int irq_ctc_install(isr_t handler, uint8_t div2, uint8_t div3)
|
|||||||
{
|
{
|
||||||
if (!handler) { errno = EINVAL; return -1; }
|
if (!handler) { errno = EINVAL; return -1; }
|
||||||
if (_irq_ctc_user) { errno = EBUSY; return -1; }
|
if (_irq_ctc_user) { errno = EBUSY; return -1; }
|
||||||
|
/* CTC-трамплин вектрится напрямую (table[0x06] = &_irq_ctc_tramp) в
|
||||||
|
* обход W2-копии кадрового пути — поэтому CTC пока требует код в W2
|
||||||
|
* (tiny/big). All-modes для CTC (копия _irq_ctc_tramp) — follow-up. */
|
||||||
|
if ((uint16_t)&_irq_ctc_tramp < 0x8000) { errno = EINVAL; return -1; }
|
||||||
if (_irq_table_ref() != 0) return -1;
|
if (_irq_table_ref() != 0) return -1;
|
||||||
|
|
||||||
uint16_t tramp = (uint16_t)&_irq_ctc_tramp;
|
uint16_t tramp = (uint16_t)&_irq_ctc_tramp;
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* irq_install — установить user-handler кадрового прерывания.
|
* irq_install — обёртка над цепочкой (обратная совместимость).
|
||||||
* Захватывает общую IM2-таблицу (_irq_table_ref) и включает кадровый
|
* Теперь просто добавляет обработчик в цепь: EBUSY больше не бывает
|
||||||
* путь трамплина. Детали и результаты отладки: docs/im2_isr_design.md.
|
* (несколько хендлеров легальны), при 4 занятых слотах — ENOMEM.
|
||||||
|
* Возврат/errno — как у irq_chain_add.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <errno.h>
|
|
||||||
#include "_irq.h"
|
#include "_irq.h"
|
||||||
|
|
||||||
int irq_install(isr_t handler)
|
int irq_install(isr_t handler)
|
||||||
{
|
{
|
||||||
if (!handler) { errno = EINVAL; return -1; }
|
return irq_chain_add(handler);
|
||||||
if (_irq_user) { errno = EBUSY; return -1; }
|
|
||||||
if (_irq_table_ref() != 0) return -1;
|
|
||||||
_irq_user = handler;
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -1,16 +1,24 @@
|
|||||||
/*
|
/*
|
||||||
* irq_remove — снять кадровый user-handler и отпустить IM2-таблицу
|
* irq_remove — снять ВСЮ кадровую цепочку и отпустить IM2-таблицу
|
||||||
* (последний пользователь возвращает I/IM 1 — штатный режим DSS).
|
* (последний пользователь возвращает I/IM 1 — штатный режим DSS).
|
||||||
* Идемпотентно; вызывается и из atexit-уборки.
|
* Идемпотентно; вызывается и из atexit-уборки, поэтому обязано снимать
|
||||||
|
* всё разом (exit не знает про отдельные слоты). Для точечного снятия
|
||||||
|
* одного обработчика — irq_chain_remove.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "_irq.h"
|
#include "_irq.h"
|
||||||
|
|
||||||
void irq_remove(void)
|
void irq_remove(void)
|
||||||
{
|
{
|
||||||
if (!_irq_user) return;
|
if (_irq_chain_n == 0) return;
|
||||||
|
|
||||||
IRQ_DISABLE();
|
IRQ_DISABLE();
|
||||||
_irq_user = 0;
|
for (uint8_t i = 0; i < IRQ_CHAIN_MAX; i++)
|
||||||
|
_irq_chain[i] = 0;
|
||||||
|
_irq_chain_n = 0;
|
||||||
IRQ_ENABLE();
|
IRQ_ENABLE();
|
||||||
|
|
||||||
|
/* Цепь держала РОВНО одну ссылку на таблицу (взята при первом
|
||||||
|
* слоте) — отпускаем её один раз. */
|
||||||
_irq_table_unref();
|
_irq_table_unref();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
*
|
*
|
||||||
* Проверяет:
|
* Проверяет:
|
||||||
* 1. кадровый handler тикает ~50 Гц (сверка с time() за 3 секунды);
|
* 1. кадровый handler тикает ~50 Гц (сверка с time() за 3 секунды);
|
||||||
|
* 1b. цепочка: второй обработчик (irq_chain_add) тикает синхронно с
|
||||||
|
* первым; irq_chain_remove снимает только его, первый жив;
|
||||||
* 2. клавиатура/SYSTIME живы при установленном handler'е (чейн к DSS);
|
* 2. клавиатура/SYSTIME живы при установленном handler'е (чейн к DSS);
|
||||||
* 3. CTC-таймер (вектор 0x06): пресет vsync ~49 Гц ОДНОВРЕМЕННО с
|
* 3. CTC-таймер (вектор 0x06): пресет vsync ~49 Гц ОДНОВРЕМЕННО с
|
||||||
* кадровым; затем быстрый режим 875 кГц/(50*50) = 350 Гц;
|
* кадровым; затем быстрый режим 875 кГц/(50*50) = 350 Гц;
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
#include <irq.h>
|
#include <irq.h>
|
||||||
|
|
||||||
static volatile uint16_t ticks;
|
static volatile uint16_t ticks;
|
||||||
|
static volatile uint16_t ticks2;
|
||||||
static volatile uint16_t ctc_ticks;
|
static volatile uint16_t ctc_ticks;
|
||||||
|
|
||||||
static void on_tick(void)
|
static void on_tick(void)
|
||||||
@@ -24,6 +27,11 @@ static void on_tick(void)
|
|||||||
ticks++;
|
ticks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void on_tick2(void)
|
||||||
|
{
|
||||||
|
ticks2++;
|
||||||
|
}
|
||||||
|
|
||||||
static void on_ctc(void)
|
static void on_ctc(void)
|
||||||
{
|
{
|
||||||
ctc_ticks++;
|
ctc_ticks++;
|
||||||
@@ -86,6 +94,39 @@ int main(void)
|
|||||||
|
|
||||||
printf("ticks: %u in 3 s (~%u Hz, expect ~50)\n", got, got / 3);
|
printf("ticks: %u in 3 s (~%u Hz, expect ~50)\n", got, got / 3);
|
||||||
|
|
||||||
|
/* --- Phase 1b: цепочка — второй кадровый обработчик --- */
|
||||||
|
if (irq_chain_add(on_tick2) != 0) {
|
||||||
|
printf("chain_add failed: errno=%d\n", errno);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
puts("chain: 2nd handler added");
|
||||||
|
{
|
||||||
|
IRQ_DISABLE();
|
||||||
|
uint16_t a0 = ticks, b0 = ticks2;
|
||||||
|
IRQ_ENABLE();
|
||||||
|
time_t tc = time(0);
|
||||||
|
while (time(0) < tc + 2) ;
|
||||||
|
IRQ_DISABLE();
|
||||||
|
uint16_t da = ticks - a0, db = ticks2 - b0;
|
||||||
|
IRQ_ENABLE();
|
||||||
|
printf("chain both: h1=%u h2=%u in 2 s (both ~100, must match)\n",
|
||||||
|
da, db);
|
||||||
|
}
|
||||||
|
/* Снять ТОЛЬКО второй — первый обязан продолжать тикать. */
|
||||||
|
irq_chain_remove(on_tick2);
|
||||||
|
{
|
||||||
|
IRQ_DISABLE();
|
||||||
|
uint16_t a0 = ticks, b0 = ticks2;
|
||||||
|
IRQ_ENABLE();
|
||||||
|
time_t tc = time(0);
|
||||||
|
while (time(0) < tc + 1) ;
|
||||||
|
IRQ_DISABLE();
|
||||||
|
uint16_t da = ticks - a0, db = ticks2 - b0;
|
||||||
|
IRQ_ENABLE();
|
||||||
|
printf("chain after remove h2: h1=%u (>0) h2=%u (must be 0)\n",
|
||||||
|
da, db);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Phase 2: CTC-таймер одновременно с кадровым --- */
|
/* --- Phase 2: CTC-таймер одновременно с кадровым --- */
|
||||||
if (irq_ctc_install(on_ctc, IRQ_CTC_VSYNC_DIV2, IRQ_CTC_VSYNC_DIV3)) {
|
if (irq_ctc_install(on_ctc, IRQ_CTC_VSYNC_DIV2, IRQ_CTC_VSYNC_DIV3)) {
|
||||||
printf("irq_ctc_install failed: errno=%d\n", errno);
|
printf("irq_ctc_install failed: errno=%d\n", errno);
|
||||||
|
|||||||
Reference in New Issue
Block a user