Files
Sprinter-SDCC/libc/include/kbd_raw.h
T
snark13 6dabe9b4b1 libc: пока raw-клавиатура открыта, не звать обработчик DSS — он крал наши скан-коды
Корень двух багов SprPoP (KBD-STUCK-WAIT, частично GRAB-KBD-TIMING) нашёлся
в исходниках DSS (docs/sources/Estex-DSS): обработчик прерывания DSS живёт в
IM1 по 0x0038 и ПЕРВЫМ ДЕЛОМ делает `CALL KEYSCAN`, а тот вычерпывает FIFO
SIO досуха.  Наш трамплин проверял «есть ли клавиатурный байт» один раз, на
входе в прерывание, а хвост кадрового пути уходил в DSS — значит скан-код,
прилетевший позже, доставался DSS и уезжал в его буфер.  Rx-overrun при этом
НЕ взводится (байт не потерян железом, а прочитан не тем владельцем) — отсюда
и загадка исходного диагноза: бит залип при `_kbdraw_overrun == 0`.

Измерено в MAME (брейки + totalcycles): окно 738 тактов (~34 мкс) на каждом
кадровом прерывании, из них 481 такт — пролог самого DSS.  Поэтому проверка
FIFO перед chain'ом снимает лишь треть и не годится (пробовали, кражи
продолжались); кадровый путь при открытом raw теперь заканчивается приватным
RETI, окно = 0.  Цена: на это время у DSS замирает опрос мыши и мигание
текстового курсора — зафиксировано в <kbd_raw.h>.

Пойманный случай (старая сборка): DSS прочитал 0x74 (make стрелки «вправо»)
при _kbdraw_pending = EXT, то есть посылку E0 74 разорвало пополам между
двумя владельцами канала.

Проверка: брейк на входе KEYSCAN с условием «страница точно DSS + raw открыт»
до правки срабатывал мгновенно (50/с), после — молчит; положительный контроль
на нашем RETI срабатывает сразу.

Трамплин 300 -> 310 Б (буфер W2-копии поднят 336 -> 384, запас 74 Б);
размерный эталон обновлён: +10 Б у программ, линкующих IRQ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:40:39 +03:00

147 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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.
*
* КАДРОВОЕ ОБСЛУЖИВАНИЕ DSS НА ЭТО ВРЕМЯ ОСТАНОВЛЕНО. Пока raw открыт,
* трамплин НЕ вызывает обработчик прерывания DSS вообще: тот первым делом
* зовёт свой KEYSCAN, который вычерпывает FIFO SIO и уводит наши байты
* (потерянный make = несработавшее нажатие, потерянный break = залипшая
* клавиша; поймано в MAME 2026-08-28). Вместе с KEYSCAN замирают и
* остальные его кадровые дела — опрос мыши (Dss.Mouse.GetPackets) и
* мигание текстового курсора. ЗНАЧИТ: приложению, которому нужна мышь
* через DSS, raw-канал открывать нельзя (мышь встанет); клавиатурные
* события всё равно берутся из kbd_raw_down().
*
* КОДЫ КЛАВИШ: значения ниже — общеизвестный стандарт 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);
/* Считать ГОЛЫЕ (без префикса 0xE0) коды нумпада ТЕМИ ЖЕ клавишами, что и
* расширенные: 0x69..0x7D, кроме ESC 0x76 и F11 0x78. По умолчанию
* выключено — нумпад остаётся отдельным набором клавиш.
*
* В PS/2 это и есть одни и те же физические клавиши: навигационный блок
* шлёт код с префиксом 0xE0, нумпад — тот же код голым (KP4 = Left,
* KP8 = Up, KP7 = Home, KP9 = PgUp и т.д.). Флаг восстанавливает это
* равенство: kbd_raw_sync() переносит биты голых кодов в EXT-половину
* карты и гасит их в PLAIN.
*
* ЗАЧЕМ ЭТО НУЖНО (а не только «нумпад тоже работает»). При переполнении
* 3-байтового FIFO SIO теряется префикс, и make стрелки садится в PLAIN
* как код нумпада — а break придёт уже с префиксом и снимет бит в ДРУГОЙ
* половине. Клавиша остаётся зажатой НАВСЕГДА: kbd_raw_any_down()
* отвечает «да», и любое ожидание «пока ничего не нажато» виснет насмерть.
* После перекладки make и break работают с одним и тем же битом.
*
* Чего флаг НЕ лечит: потерю префикса у BREAK (`E0 F0 6B` -> `F0 6B`) —
* тогда бит стоит уже в EXT-половине и выглядит как реально зажатая
* клавиша; снимается перенажатием или typematic-повтором.
*
* Перекладку делает kbd_raw_sync(), то есть приложение обязано звать его
* раз в кадр (оно и так обязано — см. ниже). */
void kbd_raw_keypad_as_ext(uint8_t on);
/* Есть ли хотя бы одна зажатая клавиша (0/1). Полезно для экранов с
* семантикой «продолжить любой клавишей»; один вызов читает 64-байтную
* bitmap, а не перебирает все 512 scan-кодов через kbd_raw_down(). */
uint8_t kbd_raw_any_down(void);
/* Восстановление после Rx-overrun SIO: звать РАЗ В КАДР (до чтения
* kbd_raw_down). Аппаратный FIFO SIO 3 байта; при длинных DI-окнах пачка
* скан-кодов (напр. быстрый тап стрелки: make+break = 5 байт) переполняет
* его → потерян break → залипшая клавиша. Трамплин ловит overrun и
* взводит флаг; kbd_raw_sync по флагу сбрасывает held-состояние всех
* клавиш КРОМЕ модификаторов (Shift/Ctrl/Alt): PS/2 автоповторяет только
* последнюю нажатую клавишу, поэтому обычные зажатые перечитаются
* typematic'ом, а сброшенный модификатор восстановить нечем — он бы
* «отваливался» при каждом overrun (см. kbd_raw_sync.c). */
void kbd_raw_sync(void);
/* Вычерпать приёмный FIFO ОПРОСОМ, не дожидаясь прерывания. Возвращает
* 0 (FIFO был пуст, ~40 тактов) или 1 (что-то вычерпано и декодировано в
* ту же карту, что ведёт трамплин).
*
* Зачем: на каждый принятый байт запрос прерывания живёт единицы
* микросекунд, и пропущенный импульс не «догоняется» — байт лежит в
* 3-байтовом FIFO до следующего прерывания. Пачки (тап стрелки — 5 байт;
* одновременное отпускание нескольких клавиш — больше) при этом
* переполняют FIFO: теряется make («нажатие не сработало») или break
* (залипание).
*
* КАК ЕЁ ЗВАТЬ (замеры 2026-08-01, PoP roomtest — см.
* applications/PoP/roomtest/TASKS_CLOSED.md, KBD-1):
* - «несколько вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО —
* потери те же, что без них: такие вызовы попадают в участки, где
* прерывания и так разрешены, и лишь дублируют трамплин;
* - работает только ПЛОТНЫЙ опрос, порядка раза в 0.5 мс. Столько
* времени есть даром в ожидании кадра, поэтому штатный способ —
* повесить эту функцию idle-хуком графики:
* gfx_set_idle_hook(my_poll_wrapper); // <gfx.h>
* Проверено: 35 нажатий стрелки с зажатым Shift → 35 дошедших make
* против 9 из 10 без хука.
* Не ставить в игровой цикл «на всякий случай» — сначала померить.
*
* Тело идёт под DI и БЕЗУСЛОВНО делает EI на выходе: рассчитано на вызов
* из главного цикла, из ISR звать нельзя. */
uint8_t kbd_raw_poll(void);
#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
#define KBD_LCTRL 0x14
#define KBD_LALT 0x11
#define KBD_RCTRL (KBD_EXT | 0x14)
#define KBD_RALT (KBD_EXT | 0x11)
#endif