libc/kbd: kbd_raw_poll + замер потери нажатий при зажатом Shift (KBD-1)
Симптом: при удерживаемом Shift часть нажатий стрелок не отрабатывает
(~15 % по наблюдению пользователя), без Shift потерь нет.
Переведено в числа: нажимается Home — тоже расширенная клавиша (тот же
E0-префикс и тот же «fake shift»), но игрой игнорируется, поэтому рельеф
комнаты на результат не влияет. Счётчики — брейкпоинты MAME с действием
{ b@ADDR = b@ADDR+1 ; g } на входе клавиатурной ветки трамплина, на чтении
порта 0x18 и на установке make-бита.
Что измерено (10 нажатий Shift+Home, дошло make):
игра идёт, опрос ВКЛ 9/10 игра идёт, опрос ВЫКЛ 9/10
игра ЗАМОРОЖЕНА (блитов нет вообще, длинных DI нет) 8/10
Обе исходные гипотезы отпали:
- длина наших DI-окон ни при чём (в замороженном кадре потерь больше);
- снятие di в accel-ядрах libbgi УРОНИЛО машину — режим «акселератор при
EI» из docs/new/06-accel.md §6.6 в этой прошивке недоступен.
Байт теряется НИЖЕ нашего кода: на 49 прочитанных байт пришлось только 28
входов в клавиатурную ветку, то есть ~44 % импульсов запроса прерывания не
обслуживается и трёхбайтовый FIFO SIO переполняется.
Потолок приёма измерен отдельной программой tests/kbdpoll (ничего, кроме
kbd_raw_poll в цикле): 25 нажатий -> 25 make, 250 байт из 250, ноль потерь.
Значит опрос лечит полностью, вопрос только в плотности: нужно раз в
~0.5 мс, а шесть вызовов за 60-мс кадр давали раз в 10 мс.
Поэтому вызовы из roomtest.c УБРАНЫ (они стояли там, где прерывания и так
разрешены, и дублировали трамплин — 9/10 с ними и без). Сама функция
kbd_raw_poll оставлена в libc: она корректна и нужна как основа плотного
опроса. В заголовке и в libc-reference — честная оговорка, чтобы её не
ставили в игровой цикл «на всякий случай» без замера.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,30 @@ uint8_t kbd_raw_down(uint16_t code);
|
||||
* «отваливался» при каждом overrun (см. kbd_raw_sync.c). */
|
||||
void kbd_raw_sync(void);
|
||||
|
||||
/* Вычерпать приёмный FIFO ОПРОСОМ, не дожидаясь прерывания. Возвращает
|
||||
* 0 (FIFO был пуст, ~40 тактов) или 1 (что-то вычерпано и декодировано в
|
||||
* ту же карту, что ведёт трамплин).
|
||||
*
|
||||
* Зачем: на каждый принятый байт запрос прерывания живёт единицы
|
||||
* микросекунд, и пропущенный импульс не «догоняется» — байт лежит в
|
||||
* 3-байтовом FIFO до следующего прерывания. Пачки в 5 байт (тап стрелки;
|
||||
* при зажатом Shift — ещё и «fake shift» вокруг расширенного кода) при
|
||||
* этом переполняют FIFO: теряется make («нажатие не сработало») или break
|
||||
* (залипание).
|
||||
*
|
||||
* ЧЕСТНАЯ ОГОВОРКА ПО ЭФФЕКТИВНОСТИ (замер 2026-08-01, PoP roomtest,
|
||||
* см. applications/PoP/roomtest/TASKS.md, KBD-1): расстановка «несколько
|
||||
* вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО — потери те же, что
|
||||
* без неё. Такие вызовы попадают в участки, где прерывания и так
|
||||
* разрешены, и лишь дублируют трамплин. Смысл появляется только у
|
||||
* ПЛОТНОГО опроса (в цикле ожидания, десятки-сотни раз за кадр), и это
|
||||
* ещё не подтверждено замером. Не ставить эту функцию в игровой цикл
|
||||
* «на всякий случай» — сначала померить.
|
||||
*
|
||||
* Тело идёт под DI и БЕЗУСЛОВНО делает EI на выходе: рассчитано на вызов
|
||||
* из главного цикла, из ISR звать нельзя. */
|
||||
uint8_t kbd_raw_poll(void);
|
||||
|
||||
#define KBD_EXT 0x0100 /* база кода была расширенной (0xE0-префикс) */
|
||||
|
||||
/* Позиционные коды PS/2 Set 2 — то, что реально нужно платформеру.
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* kbd_raw_poll — вычерпать приёмный FIFO клавиатуры ОПРОСОМ, не дожидаясь
|
||||
* прерывания. Дополняет (не заменяет) декодер в трамплине: тот же разбор
|
||||
* PS/2 Set 2, та же битовая карта _kbdraw_down[], тот же FSM префиксов
|
||||
* _kbdraw_pending — просто вызванный из главного цикла приложения.
|
||||
*
|
||||
* ЗАЧЕМ (разбор 2026-08-01, applications/PoP/roomtest/TASKS.md, KBD-1).
|
||||
* Приёмный FIFO SIO — 3 байта, и рассчитывать на то, что каждый принятый
|
||||
* байт разбудит нас прерыванием, НЕЛЬЗЯ:
|
||||
* - импульс запроса прерывания клавиатуры живёт лишь 32 такта CPU
|
||||
* (dev-MAME: sprinter.cpp on_kbd_data → irq_off_timer), а ядра
|
||||
* акселератора держат DI на весь блит — сотни микросекунд. Импульс,
|
||||
* попавший в такое окно, теряется НАСОВСЕМ: байт остаётся в FIFO до
|
||||
* следующего прерывания (следующий байт либо кадровое, 50 Гц);
|
||||
* - трафик от клавиатуры пачками: тап стрелки — 5 байт, а при зажатом
|
||||
* Shift ещё и «fake shift» (E0 F0 12 / E0 12 вокруг расширенного кода)
|
||||
* — по 5 байт на нажатие И на отпускание. Три ячейки FIFO такую пачку
|
||||
* не держат: потерянный make = «нажатие не сработало», потерянный
|
||||
* break = залипшая клавиша (её потом лечит kbd_raw_sync, ценой сброса
|
||||
* всех немодификаторных клавиш).
|
||||
* Опрос из главного цикла снимает зависимость от пойманного импульса: чем
|
||||
* чаще позвали, тем меньше шанс, что в FIFO успеет накопиться четвёртый
|
||||
* байт. Звать между фазами кадра (после тяжёлых блитов), НЕ раз в кадр.
|
||||
*
|
||||
* ABI: __sdcccall(1), без аргументов, результат uint8 в A — 0 если FIFO
|
||||
* был пуст (быстрый выход, ~40 тактов), 1 если что-то вычерпано. IX не
|
||||
* трогаем. Тело идёт под DI: чтение порта 0x18 деструктивно, и гонка с
|
||||
* трамплином за read-modify-write ячейки _kbdraw_down[] стоила бы
|
||||
* потерянного бита. DI/EI — БЕЗУСЛОВНЫЕ (как IRQ_DISABLE/IRQ_ENABLE в
|
||||
* kbd_raw_open): функция рассчитана на вызов из главного цикла, где
|
||||
* прерывания разрешены; звать её из ISR нельзя.
|
||||
*/
|
||||
|
||||
#include <kbd_raw.h>
|
||||
#include "_kbdraw.h"
|
||||
#include "../irq/_irq.h"
|
||||
|
||||
uint8_t kbd_raw_poll(void) __naked
|
||||
{
|
||||
__asm
|
||||
ld a, (__kbdraw_active)
|
||||
or a, a
|
||||
jr Z, kp_off ; raw-канал закрыт — не наше дело
|
||||
;; DI ДО пробы «есть ли байт»: иначе между пробой и DI успело бы
|
||||
;; влезть прерывание, трамплин вычерпал бы FIFO, а мы всё равно
|
||||
;; пошли бы читать порт 0x18 — и получили бы из пустого приёмника
|
||||
;; мусорный «скан-код» (взведённый бит клавиши, которую не нажимали).
|
||||
di
|
||||
in a, (#_IRQ_SIO_A) ; SIO-A RR0
|
||||
rrca ; бит 0 — байт принят?
|
||||
jr C, kp_work
|
||||
ei
|
||||
kp_off:
|
||||
xor a, a
|
||||
ret
|
||||
|
||||
kp_work:
|
||||
push hl
|
||||
push de
|
||||
push bc
|
||||
kp_drain:
|
||||
in a, (#_IRQ_SIO_DATA) ; чтение деструктивно = дренаж байта
|
||||
cp #0xF0
|
||||
jr Z, kp_setbrk
|
||||
cp #0xE0
|
||||
jr Z, kp_setext
|
||||
|
||||
;; --- обычный байт кода: БИТ code&7 в _kbdraw_down[code>>3].
|
||||
;; Маска сдвигом, а не таблицей — ровно как в трамплине, чтобы
|
||||
;; расхождения декодеров были невозможны by construction. ---
|
||||
ld e, a ; E = код
|
||||
and #7
|
||||
ld b, a
|
||||
inc b ; B = (код&7)+1 для djnz
|
||||
ld a, #1
|
||||
jr kp_mskdec
|
||||
kp_mskloop:
|
||||
add a, a
|
||||
kp_mskdec:
|
||||
djnz kp_mskloop ; A = 1 << (код&7)
|
||||
ld c, a ; C = маска бита
|
||||
ld a, e
|
||||
rrca
|
||||
rrca
|
||||
rrca
|
||||
and #0x1F ; A = код>>3 (код <= 255)
|
||||
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, kp_noext
|
||||
ld de, #32 ; +256 кодов = +32 байта карты
|
||||
add hl, de
|
||||
kp_noext:
|
||||
ld a, (__kbdraw_pending)
|
||||
bit 0, a ; _KBDRAW_PEND_BREAK?
|
||||
jr Z, kp_mkdown
|
||||
ld a, c
|
||||
cpl ; break → сбросить бит
|
||||
and a, (hl)
|
||||
jr kp_wr
|
||||
kp_mkdown:
|
||||
ld a, c ; make → взвести бит
|
||||
or a, (hl)
|
||||
kp_wr:
|
||||
ld (hl), a
|
||||
xor a, a
|
||||
ld (__kbdraw_pending), a ; префиксы потреблены
|
||||
jr kp_next
|
||||
|
||||
kp_setbrk:
|
||||
ld hl, #__kbdraw_pending
|
||||
set 0, (hl)
|
||||
jr kp_next
|
||||
kp_setext:
|
||||
ld hl, #__kbdraw_pending
|
||||
set 1, (hl)
|
||||
kp_next:
|
||||
in a, (#_IRQ_SIO_A)
|
||||
rrca ; ещё байт в FIFO?
|
||||
jr C, kp_drain
|
||||
|
||||
;; --- FIFO пуст: тот же разбор Rx-overrun, что и в трамплине.
|
||||
;; RR1 бит5 = Rx Overrun; без Error Reset (WR0 = 0x30) бит залипнет
|
||||
;; и следующий overrun будет неотличим. ---
|
||||
ld a, #1
|
||||
out (#_IRQ_SIO_A), a ; указатель на RR1
|
||||
in a, (#_IRQ_SIO_A) ; читаем RR1 (указатель авто-сброс в 0)
|
||||
bit 5, a
|
||||
jr Z, kp_noovr
|
||||
ld a, #0x30
|
||||
out (#_IRQ_SIO_A), a ; WR0 Error Reset
|
||||
ld a, #1
|
||||
ld (__kbdraw_overrun), a ; снимет kbd_raw_sync()
|
||||
kp_noovr:
|
||||
pop bc
|
||||
pop de
|
||||
pop hl
|
||||
ei
|
||||
ld a, #1
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
Reference in New Issue
Block a user