libc+libbgi: raw-клавиатура (held-state) + column-major блит спрайтов

libc/kbd: kbd_raw_open/close/down — сырой PS/2-канал клавиатуры с
held-state (битовая карта _kbdraw_down[512], EXT-клавиши +256).  Пока
raw открыт, кадровый IRQ-трамплин перехватывает байт SIO у DSS и
декодирует make/break (0xF0/0xE0-префиксы) сам.  FIFO вычерпывается В
ЦИКЛЕ (приёмный буфер SIO 3 байта; пачка break-кодов при одновременном
отпускании иначе теряется → залипание клавиши).  Буфер W2-трамплина
поднят 224→288 Б под выросший обработчик.

libc/conio: kbd_mod_state() — live-состояние модификаторов (ESTEX
CTRLKEY $33h), Shift/Ctrl/Alt/Lock прямо сейчас, KBD_MOD_* маска.

libbgi: gfx_blit_cols(x,y,img,flip) + _bgi_blit_cols_raw — блит
column-major спрайта вертикальным accel-проходом, бесплатный
горизонтальный флип (sstride<0), клип по экрану.  Для персонажей.

libbgi/atlas_load: восстанавливать W3 ДО записи a->count (atlas_t в
--bank памяти резолвится через W3; count оставался мусором).

tests/kbdraw — тест raw-клавиатуры; size-baseline +kbdraw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:48:32 +03:00
parent e14b6745f7
commit 484b18d10c
19 changed files with 602 additions and 7 deletions
+56
View File
@@ -163,6 +163,11 @@ libc отказывает сама (EMFILE, предохранитель _fd_gua
Клавиатура: `kbhit getch getche getkey` (+KEY_* коды позиций),
`char *cgets(buf)`.
`uint16_t kbd_mod_state(void)` — live-состояние модификаторов ПРЯМО
СЕЙЧАС (ESTEX CTRLKEY $33h, не событие из буфера — держится, пока
клавиша реально зажата); (mode<<8)|shift, расшифровка KBD_MOD_*.
Покрывает только Shift/Ctrl/Alt/Rus-Lat/Lock — для обычных клавиш
(стрелки и т.п.) live-state у ESTEX нет, см. `<kbd_raw.h>`.
Вывод с атрибутом: `putch cputs cprintf` (~10× медленнее stdio-пути;
'\n' НЕ транслируется — писать "\r\n").
Атрибуты: `textcolor textbackground textattr`, `set/get_text_attr`,
@@ -285,6 +290,57 @@ Cross-lib: libbgi всегда линкуется с libc; _cbl_port_ref/unref
Handler'у нельзя: ESTEX/BIOS-вызовы, gfx_*/своп окон, акселератор,
banked-функции; только volatile-глобалы и быстрая работа (<1 мс).
## <kbd_raw.h> — эксклюзивный raw-канал клавиатуры (вектор 0xFF, свой путь)
Мотивация — applications/PoP/docs/PORT_PLAN.md §2: ESTEX
kbhit/getch/getkey/kbd_mod_state — событийные, без held-state для
обычных (не модификаторных) клавиш. Decode make/break (PS/2 Scan Code
Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс расширенной
клавиши) прямо в трамплине прерывания (libc/irq/_irq_tramp.c), по
прецеденту приватного пути CBL (не чейнится к DSS, пока открыт).
| Сигнатура | Описание |
|---|---|
| `int kbd_raw_open(void)` | включить: с этого момента ВЕСЬ поток клавиатурных байт достаётся нам, DSS его не видит. 0 / -1+errno (EBUSY повтор) |
| `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit |
| `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 |
| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) |
| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно |
**ИСПРАВЛЕНО 2026-07-15 (был неверный вывод, ниже — то, что реально
подтвердилось):** изначально скриншот-тестирование (снимок экрана
после `press_key('up')`) не показывало видимого эффекта на
UP/DOWN/RIGHT — из этого сделан ОШИБОЧНЫЙ вывод «скрипт не может их
нажать». На самом деле причина была в неудачном таймінге снимков
относительно дуги прыжка, а не в отсутствии сигнала. Подтверждено
брейкпоинтом в отладчике MAME (adress уровня приложения на входе в
код `jumping=1` в `applications/PoP/poc/poc.c`): `press_key('up')`
ЧЕСТНО доходит до raw-декодера и триггерит код прыжка — брейкпоинт
сработал ровно один раз за одно удержание клавиши (это же заодно
подтвердило фикс двойного триггера через `up_prev` edge-detect в
`poc.c`). Так что скриптовый `press_key` для UP/DOWN/RIGHT РАБОТАЕТ
корректно так же, как для LEFT — предыдущая запись про «ограничение
именно в скриптовом инжекте» была неверной, оставлена в истории
git как урок: при повторных «нет эффекта» на скриншотах — проверять
брейкпоинтом/watchpoint'ом на конкретный адрес кода, не полагаться
только на визуальный снимок с произвольным таймингом.
**ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/
kbd_mod_state` НЕ получают новых событий (в т.ч. CTRLKEY подряд отдаёт
то же самое, что было на момент открытия — резидентный обработчик DSS,
от которого зависит его live-state, во время raw не выполняется).
ESC для выхода — через `kbd_raw_down(KBD_ESC)`, не через DSS.
Верификация (tests/kbdraw, MAME, 2026-07-15): `press_key` в мосте
MAME дёргает ОБЕ клавиатуры (PC ms_naturl + ZX-матрица `IO_LINE`)
одновременно — наблюдался паразитный незатухающий бит от ZX-пути
(обычный код без EXT-префикса, застревал в DOWN) — не воспроизводится
на реальном сценарии (только PC/AT-клавиатура, без матрицы, см.
applications/PoP/docs/PORT_PLAN.md §2 — пользователь подтвердил, что
матрица на Sprinter давно не используется). Для будущих MAME-тестов
этой функции — бить только по `:kbd:ms_naturl:*`, не через
удобный `press_key` (или перепроверить точную семантику полей моста).
## <cbl.h> — потоковый звук CBL/COVOX (вектор 0xFF, свой ISR)
Без собственного кольца (2026-07-07): у CBL уже есть аппаратный буфер
+1
View File
@@ -33,6 +33,7 @@ gfxbanks 6044
hello 4167
hello2 4244
irqtest 6527
kbdraw 4911
ls 4835
malloc 4461
mdview 18878
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
33 hello
34 hello2
35 irqtest
36 kbdraw
37 ls
38 malloc
39 mdview
+9
View File
@@ -86,6 +86,15 @@ void _bgi_blit_rows_raw(const uint8_t *src, uint8_t *dst,
uint8_t w, uint8_t h,
int sstride, uint8_t y0);
/* КОЛОНОЧНАЯ специализация (column-major src -> экран): каждая колонка —
* вертикальный accel-burst (LD A,A, Port_Y авто-шаг); dst +1/колонку, src
* +sstride/колонку. Даёт бесплатный горизонтальный флип (sstride<0 + src
* с конца). w=колонок, h=высота(=размер блока), y0=Port_Y стартовый.
* bgi256/_bgi_blit_cols_raw.c. */
void _bgi_blit_cols_raw(const uint8_t *src, uint8_t *dst,
uint8_t w, uint8_t h,
int sstride, uint8_t y0);
/* Специализация для heal: src == dst == адрес экрана, оба страйда 0
* (строку выбирает Port_Y) — адресной арифметики в цикле нет, строка
* 77Т + burst'ы. y0 — стартовый Port_Y; w,h: 0 = 256. Без скобки и
+84
View File
@@ -0,0 +1,84 @@
/*
* _bgi_blit_cols_raw — accel block-copy КОЛОНКАМИ (буфер column-major ->
* экран). Парный к _bgi_blit_rows_raw, но вертикальный проход: каждая
* КОЛОНКА копируется одним accel-burst'ом вниз (Port_Y авто-шагает),
* опкод LD A,A (0x7F — «копирование блока для вертикального прохода»,
* см. docs/reference/accel_r.txt). dst двигается на +1 (следующий x) на
* колонку; Port_Y сбрасывается в y0 перед каждой колонкой (accel шагает
* его внутри burst'а); src двигается на sstride на колонку.
*
* ЗАЧЕМ: бесплатный ГОРИЗОНТАЛЬНЫЙ флип персонажей — вызывающий подаёт
* src с конца (src+(w-1)*h) и sstride=-h, тогда правая колонка источника
* ложится в левый x экрана (зеркало). Реверс без CPU и без удвоения
* памяти. Спрайты персонажей хранятся column-major.
*
* ВХОД (__sdcccall(1)): src→HL, dst→DE; стек: w 4(ix), h 5(ix),
* sstride 6/7(ix), y0 8(ix). w — число КОЛОНОК (1..256, 0=256); h —
* высота колонки = размер accel-блока (1..256, 0=256); sstride — шаг
* адреса src между колонками (±); y0 — стартовый Port_Y. Callee-pop 5 Б.
*
* Raw: без клиппинга (вызывающий); W3 замаплен (_bgi_begin/end снаружи);
* банк выставлен вызывающим (0x5C прозрачный / 0x50 обычный). src ВНЕ W3.
* ОДИН DI на весь блит. Колонки > 256 строк режет вызывающий (h ≤ 256).
* Клоббер: AF/BC/DE/HL; IX сохраняется.
*/
#include "../_bgi.h"
void _bgi_blit_cols_raw(const uint8_t *src, uint8_t *dst,
uint8_t w, uint8_t h,
int sstride, uint8_t y0) __naked
{
(void)src; (void)dst; (void)w; (void)h; (void)sstride; (void)y0;
__asm
push ix
ld ix, #0
add ix, sp
;; SMC: размер accel-блока <- h (высота колонки)
ld a, 5 (ix)
ld (bc_len_imm), a
;; SMC: src-страйд между колонками (8-бит adc-цепочка, ± на вызов)
ld a, 6 (ix)
ld (bc_slo_imm), a
ld a, 7 (ix)
ld (bc_shi_imm), a
ld b, 4 (ix) ; B = число колонок (0 => 256: djnz)
ld c, 8 (ix) ; C = y0 (Port_Y, константа)
di ; один DI на весь блит
ld d, d ; 0x52 режим размера блока
ld a, #0
bc_len_imm = . - 1
ld b, b ; 0x40 стоп (B=счётчик не тронут)
bc_col:
ld a, c
out (#0x89), a ; Port_Y = y0 (сброс на верх колонки)
;; ЧТЕНИЕ ГОРИЗОНТАЛЬНОЙ копией (LD L,L): src-колонка лежит в ОЗУ
;; СМЕЖНО, адрес шагает (не port_y). ЗАПИСЬ ВЕРТИКАЛЬНОЙ (LD A,A):
;; port_y шагает вниз по видео-колонке. Режим меняется между read и
;; write (do_accel_block применяет один режим на весь блок).
ld l, l ; 0x6D гориз. копия (для ЧТЕНИЯ смежных)
ld a, (hl) ; read-блок: h смежных байт src -> буфер accel
ld a, a ; 0x7F верт. копия (для ЗАПИСИ колонки)
ld (de), a ; write-блок: буфер -> видео-колонка (port_y++)
ld b, b ; 0x40 стоп
;; src += sstride (следующая колонка источника)
ld a, l
add a, #0
bc_slo_imm = . - 1
ld l, a
ld a, h
adc a, #0
bc_shi_imm = . - 1
ld h, a
inc de ; dst += 1 (следующий x)
djnz bc_col
ei ; закрыть DI-окно
pop ix
;; callee-pop 5 байт (w,h,sstride:2,y0)
pop hl ; ret-адрес
pop af
pop af
inc sp
jp (hl)
__endasm;
}
+11 -2
View File
@@ -23,7 +23,7 @@
int atlas_load(atlas_t *a, const char *path)
{
uint8_t *pg = (uint8_t *)0xC000;
uint8_t saved_w3, blk, ok;
uint8_t saved_w3, blk, ok, count;
uint16_t stub;
int fd, n;
@@ -45,8 +45,9 @@ int atlas_load(atlas_t *a, const char *path)
n = read(fd, pg, 16384);
ok = (n >= (int)_ATL_DATA_OFF &&
pg[0] == 'S' && pg[1] == 'P' && pg[2] == 'A' && pg[3] == '1');
count = 0;
if (ok) {
a->count = pg[4];
count = pg[4];
stub = (uint16_t)&_gfx_w0_isr;
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
pg[0x39] = (uint8_t)(stub & 0xFF);
@@ -55,6 +56,13 @@ int atlas_load(atlas_t *a, const char *path)
pg[0x67] = 0x45;
}
/* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской
* памяти (её адрес тоже резолвится через W3), запись при ещё
* замапленной странице атласа ушла бы не в тот физический банк
* (найдено 2026-07-15 — PoP room.c держит tile_atlas[] в --bank 1,
* a->count оставался мусором, room_draw читал пиксели не с того
* оффсета). a->blk/a->page выше пишутся ДО ремапа — их это не
* касалось, только a->count. */
sprinter_page_w3(saved_w3);
close(fd);
@@ -64,5 +72,6 @@ int atlas_load(atlas_t *a, const char *path)
errno = EINVAL;
return -1;
}
a->count = count;
return 0;
}
+61
View File
@@ -0,0 +1,61 @@
/*
* gfx_blit_cols — блит спрайта column-major (getimage-заголовок uint16 w,h;
* далее пиксели ПО КОЛОНКАМ: колонка 0 сверху вниз, потом колонка 1, ...)
* в (x,y) текущим банком (gfx_set_bank; 0x5C/0x58 прозрачный). Каждая
* колонка — вертикальный accel-burst (_bgi_blit_cols_raw).
*
* flip != 0 — ГОРИЗОНТАЛЬНОЕ зеркало (для направления персонажа): источник
* читается с последней колонки к первой (sstride отрицательный), правая
* колонка ложится в левый x экрана. Без CPU-реверса и без второй копии
* атласа. Зеркало происходит ВНУТРИ футпринта [x, x+w); чтобы держать
* "hot-point" персонажа на месте (как оригинал: xpos -= w), вызывающий
* при флипе подаёт x = anchor - w (см. pop_kid.c kid_draw).
*
* Клип по экрану ЕСТЬ (аналог _gfx_blit_full, но роли осей зеркальны:
* x = номер КОЛОНКИ, y = смещение ВНУТРИ колонки). sstride считается по
* ПОЛНОЙ высоте (колонки в ОЗУ лежат full_h друг от друга), длина accel-
* блока — по обрезанной. Спрайты Kid < 256 по обеим осям → нарезка на
* полосы >256 не нужна (raw принимает uint8_t w,h; 0 => 256). img вне W3.
*/
#include <stdint.h>
#include "../_bgi.h"
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
{
const uint8_t *p = (const uint8_t *)img;
int full_w = p[0] | (p[1] << 8);
int full_h = p[2] | (p[3] << 8);
const uint8_t *px = p + 4;
int w = full_w, h = full_h;
int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */
int sy = 0; /* пропуск строк сверху колонки */
int cx = 0; /* пропуск колонок слева экрана */
const uint8_t *src;
uint8_t *dst;
if (full_w <= 0 || full_h <= 0)
return;
/* Клип по экрану — только если хоть один край вылезает. */
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
if (x < 0) { cx = -x; w += x; x = 0; }
if (y < 0) { sy = -y; h += y; y = 0; }
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
if (w <= 0 || h <= 0) return;
}
/* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка.
* cx экранных колонок слева пропускаем шагом sstride (в флипе он
* отрицателен — уходим к более ранним колонкам источника); sy строк
* сверху — простым +sy внутри колонки (порядок строк флип не меняет). */
src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px;
if (cx)
src += cx * sstride;
src += sy;
_bgi_begin();
dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x);
_bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y);
_bgi_end();
}
+7
View File
@@ -93,6 +93,13 @@ void gfx_blit(int x, int y, const void *img);
void gfx_blit_part(int x, int y, const void *img,
int sx, int sy, int w, int h);
/* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel-
* проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без
* CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot-
* point» подавай x=anchor-w при флипе). Клип по экрану есть.
* Для персонажей (флип); фон — обычный gfx_blit (row-major). */
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip);
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
* оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда
* работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;
+26
View File
@@ -0,0 +1,26 @@
/*
* kbd_mod_state — обёртка ESTEX CTRLKEY ($33h): состояние
* модификаторов клавиатуры НА ДАННЫЙ МОМЕНТ (не из буфера — драйвер
* берёт данные прямо из последнего скана, см. ProgrammerManual.txt
* "CTRLKEY"), поэтому в отличие от kbhit/getch/getkey держится ровно
* столько, сколько клавиша реально зажата.
*
* ABI: C=0x33 на входе — функция; на выходе C=mode, B=shift (тот же
* регистр C переиспользуется под результат, как и во всех ESTEX-
* функциях клавиатуры). Расшифровка битов — KBD_MOD_* в <conio.h>.
*/
#include "_conio.h"
uint16_t kbd_mod_state(void) __naked
{
__asm
push ix
ld c, #0x33 ; ESTEX CTRLKEY
rst #0x10
pop ix
ld d, c ; D = mode (старший байт результата)
ld e, b ; E = shift (младший байт результата)
ret ; __sdcccall(1): DE = uint16_t
__endasm;
}
+23
View File
@@ -75,6 +75,29 @@ uint16_t getkey(void);
#define KEY_INS 0x50 /* numpad 0; not verified */
#define KEY_DEL 0x55 /* numpad 5/.; not verified */
/* kbd_mod_state — live-состояние модификаторов ПРЯМО СЕЙЧАС (ESTEX
* CTRLKEY $33h), а не событие из буфера: держится столько, сколько
* клавиша реально зажата (в отличие от kbhit/getch/getkey). Покрывает
* ТОЛЬКО Shift/Ctrl/Alt/Rus-Lat/Lock-клавиши — обычные клавиши
* (стрелки и т.п.) у ESTEX live-state не имеют, см. <kbd_raw.h>.
*
* Возврат: (mode << 8) | shift — расшифровка через KBD_MOD_* ниже. */
uint16_t kbd_mod_state(void);
#define KBD_MOD_RUSLAT 0x8000
#define KBD_MOD_NUMLOCK 0x0800
#define KBD_MOD_SCRLOCK 0x0400
#define KBD_MOD_INSERT 0x0200
#define KBD_MOD_CAPSLOCK 0x0100
#define KBD_MOD_LSHIFT 0x0080
#define KBD_MOD_RSHIFT 0x0040
#define KBD_MOD_CTRL 0x0020
#define KBD_MOD_ALT 0x0010
#define KBD_MOD_LCTRL 0x0008
#define KBD_MOD_LALT 0x0004
#define KBD_MOD_RCTRL 0x0002
#define KBD_MOD_RALT 0x0001
typedef union {
uint16_t value;
struct {
+63
View File
@@ -0,0 +1,63 @@
/*
* kbd_raw.h — эксклюзивный raw-канал клавиатуры: держит битовую карту
* «клавиша N зажата ПРЯМО СЕЙЧАС», обновляемую напрямую из
* прерывания (декодирует PS/2 Scan Code Set 2 — make/break, тот же
* протокол, что и в docs/samples/sprinterKeybLib.asm).
*
* Прецедент и мотивация — applications/PoP/docs/PORT_PLAN.md §2:
* ESTEX kbhit/getch/getkey/CTRLKEY — событийные (нажатие, без
* release), обычные клавиши (не модификаторы) вообще не имеют
* live-state в ESTEX; трамплин кадрового прерывания
* (libc/irq/_irq_tramp.c) в штатном режиме отдаёт клавиатурные байты
* ПРЯМО в DSS, не читая порт данных — байт из аппаратного приёмного
* регистра можно прочитать только ОДИН раз, поэтому «подсмотреть, не
* мешая DSS» невозможно технически.
*
* ЖИЗНЕННЫЙ ЦИКЛ И ГЛАВНОЕ СЛЕДСТВИЕ: пока kbd_raw_open() активен, ВЕСЬ
* поток клавиатурных байт достаётся ЭТОМУ модулю — DSS их не видит.
* kbhit/getch/getkey/CTRLKEY (в т.ч. kbd_mod_state) НЕ получают новых
* событий, пока raw-канал открыт. Приложение обязано проверять «выход»
* (например ESC — KBD_ESC) через kbd_raw_down() само. Перед экранами,
* которым нужен обычный ESTEX-ввод (диалоги/меню на консольном I/O),
* закрыть канал kbd_raw_close(). Паттерн один в один как у
* cbl_open()/cbl_close() (<cbl.h>) — тот же приватный IM2-хук, не
* чейнящийся к DSS.
*
* КОДЫ КЛАВИШ: значения ниже — общеизвестный стандарт AT/PS-2 Scan
* Code Set 2 (не специфика Sprinter). TODO до использования в PoC:
* подтвердить в MAME, что реальный поток байт с SIO-A клавиатуры
* Sprinter именно этот набор кодов (см. defer_unexplained_quirks).
*/
#ifndef KBD_RAW_H
#define KBD_RAW_H
#include <stdint.h>
/* Включить raw-канал: 0 / -1+errno (EBUSY — уже открыт; прочие коды —
* см. errno.h, как у irq_install/cbl_open — общий IM2-механизм). */
int kbd_raw_open(void);
/* Выключить raw-канал, вернуть клавиатуру DSS. Идемпотентно; висит
* на atexit (страховка, как у cbl_close). */
void kbd_raw_close(void);
/* Зажата ли клавиша code ПРЯМО СЕЙЧАС (0/1). code вне 0..511 — 0
* (защитно). Для расширенных клавиш (стрелки и т.п., префикс 0xE0
* на проводе) прибавить KBD_EXT к базовому коду. */
uint8_t kbd_raw_down(uint16_t code);
#define KBD_EXT 0x0100 /* база кода была расширенной (0xE0-префикс) */
/* Позиционные коды PS/2 Set 2 — то, что реально нужно платформеру.
* TODO: подтвердить в MAME перед PoC (см. предупреждение выше). */
#define KBD_UP (KBD_EXT | 0x75)
#define KBD_DOWN (KBD_EXT | 0x72)
#define KBD_LEFT (KBD_EXT | 0x6B)
#define KBD_RIGHT (KBD_EXT | 0x74)
#define KBD_SPACE 0x29
#define KBD_ENTER 0x5A
#define KBD_ESC 0x76
#define KBD_LSHIFT 0x12
#define KBD_RSHIFT 0x59
#endif
+8 -2
View File
@@ -30,8 +30,10 @@
#define _IRQ_VEC_BUF_SIZE (256 + 257)
/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в
* W1). С запасом над реальным размером трамплина (~150 Б). */
#define _IRQ_TRAMP_BUF_SIZE 224
* W1). С запасом над реальным размером трамплина (225 Б после
* FIFO-drain цикла в raw-клавиатурной ветке, 2026-07-16 — проверять
* __irq_tramp_end при правках, запас держать ≥ 60 Б). */
#define _IRQ_TRAMP_BUF_SIZE 288
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
@@ -63,6 +65,10 @@ extern isr_t _irq_cbl_hook;
#define _IRQ_CTC_CH2 0x12
#define _IRQ_CTC_CH3 0x13
#define _IRQ_SIO_A 0x19 /* SIO-A RR0: бит 0 = Rx available */
#define _IRQ_SIO_DATA 0x18 /* SIO-A data — деструктивное чтение
* (дренирует байт); читает либо DSS
* (штатно), либо raw-канал клавиатуры
* (libc/kbd/_kbdraw.h), но не оба сразу */
/* Захватить/отпустить IM2-таблицу (счётчик ссылок). ref: при первом
* захвате строит таблицу, ставит I/IM2, взводит atexit-уборку;
+76 -3
View File
@@ -3,8 +3,16 @@
* версия с ЦЕПОЧКОЙ кадровых обработчиков и W1-remap для all-modes.
*
* Разбор источника (трогаем только AF, пока не выяснили):
* - клавиатура (бит 0 порта 0x19 = SIO-A RR0) → сразу к DSS, БЕЗ
* remap (W1 не трогаем — прерванный контекст сохранён как есть);
* - клавиатура (бит 0 порта 0x19 = SIO-A RR0) → штатно сразу к DSS,
* БЕЗ remap (W1 не трогаем — прерванный контекст сохранён как
* есть). ИСКЛЮЧЕНИЕ: если raw-канал открыт (`_kbdraw_active`,
* см. libc/kbd/_kbdraw.h, kbd_raw_open) — байт достаётся НАМ
* (порт данных 0x18 читается один раз, деструктивно — либо мы,
* либо DSS, не оба), декодируется make/break (PS/2 Scan Code
* Set 2: 0xF0 — префикс отпускания, 0xE0 — префикс расширенной
* клавиши) в битовую карту `_kbdraw_down[]`, и к DSS НЕ идём
* (private RETI — тот же паттерн, что и у CBL ниже). Пока raw
* открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>).
* - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
* _irq_cbl_hook (иначе бит подтянут к 1); личное прерывание, RETI
* без чейна к DSS;
@@ -29,6 +37,7 @@
*/
#include "_irq.h"
#include "../kbd/_kbdraw.h"
void _irq_tramp(void) __naked
{
@@ -38,7 +47,71 @@ void _irq_tramp(void) __naked
rrca ; бит 0 клавиатурный байт принят?
jr NC, tr_notkbd
;; --- клавиатура: прямо к DSS, без remap ---
;; --- клавиатура ---
ld a, (__kbdraw_active)
or a, a
jr Z, tr_kbd_dss ; raw выключен прежнее поведение
;; --- raw включён: байт наш целиком, DSS его не увидит ---
;; ВЫЧЕРПЫВАЕМ FIFO SIO В ЦИКЛЕ (как эталон keybIntHandler /
;; PS2_Scan в docs/samples): приёмный FIFO Z80 SIO 3 байта
;; глубиной, а за один кадр при одновременном отпускании
;; нескольких клавиш приходит ПАЧКА break-кодов (0xF0 code ...).
;; Читать один байт за прерывание = переполнение FIFO = потеря
;; break-кода = клавиша «залипает» навсегда (найдено 2026-07-16).
push hl
push de
tr_kbd_drain:
in a, (#_IRQ_SIO_DATA) ; читаем тем самым дренируем байт
cp #0xF0
jr Z, tr_kbd_setbrk
cp #0xE0
jr Z, tr_kbd_setext
;; --- обычный байт кода: индекс в _kbdraw_down[код(+256 EXT)] ---
ld hl, #__kbdraw_down
ld d, #0
ld e, a
add hl, de
ld a, (__kbdraw_pending)
bit 1, a ; _KBDRAW_PEND_EXT?
jr Z, tr_kbd_noext
ld de, #256
add hl, de
tr_kbd_noext:
ld a, (__kbdraw_pending)
bit 0, a ; _KBDRAW_PEND_BREAK?
jr Z, tr_kbd_mkdown
xor a, a ; break снять (0)
jr tr_kbd_wr
tr_kbd_mkdown:
ld a, #1 ; make взвести (1)
tr_kbd_wr:
ld (hl), a
xor a, a
ld (__kbdraw_pending), a ; сброс накопленных префиксов
jr tr_kbd_next
tr_kbd_setbrk:
ld hl, #__kbdraw_pending
set 0, (hl)
jr tr_kbd_next
tr_kbd_setext:
ld hl, #__kbdraw_pending
set 1, (hl)
;; fall through в tr_kbd_next
tr_kbd_next:
in a, (#_IRQ_SIO_A) ; ещё байт в FIFO?
rrca ; бит 0 CF
jr C, tr_kbd_drain ; да вычерпать следующий
pop de
pop hl
pop af
ei
reti ; raw владеет каналом целиком к DSS НЕ идём
tr_kbd_dss:
;; --- прежнее поведение: прямо к DSS, без remap ---
pop af
.db 0xC3 ; jp nn
__irq_dss_target::
+26
View File
@@ -0,0 +1,26 @@
/*
* _kbdraw.h — внутренности raw-канала клавиатуры (НЕ публичный
* заголовок; публичный интерфейс — <kbd_raw.h>).
*
* Всё состояние — плоские глобалы (живут в W2/_DATA во ВСЕХ memory-
* режимах, см. sprinter_memory_modes) — трамплин прерывания
* (libc/irq/_irq_tramp.c) обращается к ним БЕЗ W1-remap и без call,
* поэтому раскладка памяти приложения тут ни при чём.
*/
#ifndef _KBDRAW_INTERNAL_H
#define _KBDRAW_INTERNAL_H
#include <stdint.h>
/* Биты _kbdraw_pending — накопленные префиксы PS/2 Scan Code Set 2,
* ждущие завершающего байта кода (см. _irq_tramp.c). */
#define _KBDRAW_PEND_BREAK 0x01
#define _KBDRAW_PEND_EXT 0x02
extern uint8_t _kbdraw_active; /* 0 = raw выключен (дефолт, трамплин
* ведёт себя как раньше — прямо к DSS) */
extern uint8_t _kbdraw_pending; /* FSM декодера make/break, см. выше */
extern uint8_t _kbdraw_down[512]; /* 0/1 по коду; +256 для расширенных
* (префикс 0xE0) — см. kbd_raw_down */
#endif
+10
View File
@@ -0,0 +1,10 @@
/*
* _kbdraw_state.c — состояние raw-канала клавиатуры (см. _kbdraw.h).
* Не инициализировать явно = 0 — crt0 зануляет _DATA/_BSS
* (memory/sdcc_static_storage_gotcha).
*/
#include "_kbdraw.h"
uint8_t _kbdraw_active;
uint8_t _kbdraw_pending;
uint8_t _kbdraw_down[512];
+17
View File
@@ -0,0 +1,17 @@
/*
* kbd_raw_close — выключить raw-канал, вернуть клавиатуру DSS
* (kbhit/getch/getkey/kbd_mod_state снова получают события).
* Идемпотентно; висит на atexit (kbd_raw_open).
*/
#include "_kbdraw.h"
#include "../irq/_irq.h"
void kbd_raw_close(void)
{
if (!_kbdraw_active) return;
IRQ_DISABLE();
_kbdraw_active = 0;
IRQ_ENABLE();
_irq_table_unref();
}
+14
View File
@@ -0,0 +1,14 @@
/*
* kbd_raw_down — зажата ли клавиша code ПРЯМО СЕЙЧАС (0/1).
* Читает битовую карту, которую ведёт трамплин прерывания
* (libc/irq/_irq_tramp.c) — O(1), никаких ESTEX-вызовов.
*/
#include <kbd_raw.h>
#include "_kbdraw.h"
uint8_t kbd_raw_down(uint16_t code)
{
if (code >= 512) return 0;
return _kbdraw_down[code];
}
+40
View File
@@ -0,0 +1,40 @@
/*
* kbd_raw_open — включить эксклюзивный raw-канал клавиатуры: с этого
* момента байты SIO-A клавиатуры декодирует трамплин ПРЯМО в
* прерывании (_kbdraw_down[], см. libc/irq/_irq_tramp.c), DSS их
* больше не видит — kbhit/getch/getkey/kbd_mod_state перестают
* получать новые события (applications/PoP/docs/PORT_PLAN.md §2).
*
* Захватывает IM2-таблицу тем же счётчиком ссылок, что
* irq_chain_add/cbl_open (_irq_table_ref) — можно сочетать с кадровой
* цепочкой и CBL одновременно, они независимы (разные условия ветвления
* в трамплине). atexit подстрахует close.
*/
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sprinter_exit.h>
#include <kbd_raw.h>
#include "_kbdraw.h"
#include "../irq/_irq.h"
int kbd_raw_open(void)
{
if (_kbdraw_active) { errno = EBUSY; return -1; }
if (_irq_table_ref() != 0) return -1;
memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
_kbdraw_pending = 0;
static uint8_t atexit_armed;
if (!atexit_armed) {
atexit_armed = 1;
atexit(kbd_raw_close);
}
IRQ_DISABLE();
_kbdraw_active = 1;
IRQ_ENABLE();
return 0;
}
+5
View File
@@ -0,0 +1,5 @@
# Build kbdraw.exe — raw-канал клавиатуры (<kbd_raw.h>), TINY memory mode.
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := kbdraw
include $(PROJ_ROOT)/app.mk
+65
View File
@@ -0,0 +1,65 @@
/*
* kbdraw — тест raw-канала клавиатуры (<kbd_raw.h>). Держит на
* экране live-состояние нескольких клавиш плюс список ВСЕХ кодов,
* которые движок сейчас считает зажатыми (для калибровки KBD_* —
* см. PORT_PLAN.md §2: значения там взяты по общеизвестному стандарту
* PS/2 Set 2, не подтверждены на этом железе/MAME до этого теста).
* Заодно проверяет, что kbd_mod_state() (CTRLKEY, зависит от
* резидентного обработчика DSS) не обновляется, пока raw открыт —
* ожидаемое следствие §2 ("DSS не видит клавиатуру, пока raw
* активен").
*
* Выход — ESC через raw-канал (kbd_raw_down(KBD_ESC)), НЕ через DSS.
*/
#include <conio.h>
#include <kbd_raw.h>
#include <stdio.h>
#include <unistd.h>
static void show(uint8_t row, const char *name, uint8_t down)
{
gotoxy(0, row);
printf("%-8s %s ", name, down ? "DOWN" : "up ");
}
int main(void)
{
uint16_t c;
clrscr();
puts("kbdraw test: hold arrows/space/shift; ESC to quit");
if (kbd_raw_open() != 0) {
puts("kbd_raw_open failed");
return 1;
}
for (;;) {
show(2, "LEFT", kbd_raw_down(KBD_LEFT));
show(3, "RIGHT", kbd_raw_down(KBD_RIGHT));
show(4, "UP", kbd_raw_down(KBD_UP));
show(5, "DOWN", kbd_raw_down(KBD_DOWN));
show(6, "SPACE", kbd_raw_down(KBD_SPACE));
show(7, "LSHIFT", kbd_raw_down(KBD_LSHIFT));
show(8, "RSHIFT", kbd_raw_down(KBD_RSHIFT));
show(9, "ESC", kbd_raw_down(KBD_ESC));
gotoxy(0, 11);
printf("mod=%04X (expected frozen while raw is open) ",
kbd_mod_state());
gotoxy(0, 13);
printf("active codes: ");
gotoxy(14, 13);
for (c = 0; c < 512; c++)
if (kbd_raw_down(c)) printf("%03X ", c);
if (kbd_raw_down(KBD_ESC)) break;
delayms(60);
}
kbd_raw_close();
clrscr();
puts("raw closed, DSS keyboard restored");
return 0;
}