- docs/new/ — сводные справочники (архитектура, BIOS, DSS, память, графика, акселератор, IRQ, порты, ввод, звук, известные баги); - docs/Original/ — первоисточники, из которых они собраны (BIOS, Estex DSS, мануалы, описание акселератора), + Форум.doc/.docx в reference; - libbgi/common/gfx_scroll_h.c — обход бага скролла при ширине >256 (правка автора: шаг банды 255 и продвижение указателей на cw; старый вариант с 256 оставлен закомментированным с TODO); - удалён applications/PoP/roomtest/hang_variants.png — рабочая раскладка из разбора позы виса, в репозитории ей не место. Большие архивы (docs/extra ~568 МБ, docs/sources с вложенными git-репо ~68 МБ) в коммит НЕ включены — см. обсуждение. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
21 KiB
9. Клавиатура и мышь
9.1 Общие сведения
Клавиатура Sprinter — AT-совместимая (101 key, PS/2). В Sprinter-1
дополнительно присутствует ZX-клавиатурная матрица через порт #FE
(скан-код через AT-контроллер преобразуется в ZX-матрицу). Sprinter-2+
использует AT-клавиатуру через внутренний SIO Z84C15 (канал A).
Мышь — Microsoft Serial Mouse (2 кнопки) через RS-232 на SIO-B (канал B Z84C15).
Порты SIO
| Порт | Назначение |
|---|---|
#18 |
SIO-A data — клавиатура |
#19 |
SIO-A control — статус/управление |
#1A |
SIO-B data — мышь |
#1B |
SIO-B control — мышь |
Бит 0 порта #19 — признак прерывания от клавиатуры (используется в IM2
для различения источников с общим вектором #FF).
Прерывания
Клавиатура и мышь работают в IM2, общий вектор #FF (совместно с
кадровым и CBL). Различение источника — по портам SIO и флаговым битам
(07-irq.md §7.2).
9.2 Клавиатура: BIOS и ESTEX
FN_KBD_OUT (BIOS $EA)
Отправляет байт непосредственно на AT-клавиатуру [BIOS_v3.txt:0EAh].
Вход: C = $EA
A = байт команды/данных для AT-клавиатуры
Выход: нет
Используется для низкоуровневой настройки клавиатуры (LED-индикаторы, повтор, задержка).
ESTEX: функции клавиатуры (30h–37h)
Через RST 10h с C=30h–37h [DiskSyscalls.txt:30h–37h].
WAITKEY (30h): ждёт нажатия, возвращает код.
Вход: C = 30h
Выход: A = код символа
D = скан-код
E = ASCII-код (0 для функциональных клавиш)
SCANKEY (31h): опрос без ожидания.
Вход: C = 31h
Выход: A = код символа (0 если нажатий нет)
D = скан-код
E = ASCII-код
ECHOKEY (32h): ждёт + эхо-вывод.
Вход: C = 32h
Выход: A = код символа
D = скан-код
E = ASCII-код
CTRLKEY (33h): состояние модификаторов и режимов. libc: kbd_mod_state().
Вход: C = 33h
Выход: C = mode (биты режимов — см. ниже)
B = shift (биты зажатых модификаторов)
| Бит C | Режим |
|---|---|
| 0 | РУС/ЛАТ (0=ЛАТ, 1=РУС) |
| 1 | NumLock |
| 2 | ScrollLock |
| 3 | CapsLock |
| 4 | Insert |
| Бит B | Модификатор |
|---|---|
| 0 | Left Shift |
| 1 | Right Shift |
| 2 | Ctrl (любой) |
| 3 | Alt (любой) |
| 4 | Left Ctrl |
| 5 | Left Alt |
| 6 | Right Ctrl |
| 7 | Right Alt |
K_CLEAR (35h): сбросить буфер клавиатуры.
Вход: C = 35h
K_SETUP (36h): настройка раскладки. [DiskSyscalls.txt:36h]
Вход: C = 36h
A = 0 — получить текущий номер раскладки
A = 1 — установить раскладку по номеру из B
A = 2 — получить имя текущей раскладки (адрес HL)
Выход: при A=0: A = номер раскладки
при A=2: HL = адрес строки с именем
| Номер | Раскладка |
|---|---|
| 0 | Английская (US) |
| 1 | Русская |
| 2 | Английская (UK) |
| 3 | Украинская |
Переключение раскладки по Ctrl+Space (если не переназначено).
TESTKEY (37h): тест на нажатие (без извлечения из буфера).
Вход: C = 37h
Выход: A = код символа (0 если нет)
D = скан-код
E = ASCII-код
9.3 Клавиатура: необработанный (raw) доступ
Штатный DSS (WAITKEY/SCANKEY/CTRLKEY) даёт событийный интерфейс — нажатия, а не состояния клавиш. Для игр и real-time приложений требуется чтение зажатых клавиш в каждый момент. Это реализуется через перехват SIO-A напрямую в обработчике IM2, минуя DSS.
9.3.1 Принцип
- Установить свой обработчик IM2 (общий вектор
#FF). - В обработчике читать байты с SIO-A (порт
#18), декодировать PS/2 Scan Code Set 2, обновлять битовую карту held-состояний. - Основной код по кадровому прерыванию (или раз в кадр) читает карту опросом — без ожидания.
- DSS не получает байт с клавиатуры, пока обработчик их забирает. Для восстановления нормального ввода — временно отключить перехват.
9.3.2 SIO-A и прерывания
SIO-A (порт данных #18, управление #19) работает в IM2.
Инициализация SIO-A (выполняется BIOS при загрузке, повторно не требуется):
| WR | Значение | Эффект |
|---|---|---|
| WR0 | 0x10 |
Сброс RR1 (ошибка) |
| WR1 | 0x07 |
Прерывания по Rx готовности |
| WR3 | 0xC1 |
Rx 8 бит, без CRC |
| WR4 | 0x44 |
×1 клок, 1 стоп-бит, без паритета |
| WR5 | 0xEA |
Tx 8 бит, DTR+RTS=1 |
Различение в IM2: при входе в обработчик проверить бит 0 порта #19.
Если 0 — прерывание не от SIO-A (кадровое, CBL — пропустить).
9.3.3 PS/2 Scan Code Set 2 — протокол
SIO-A передаёт скан-коды PS/2 Set 2. Каждый байт с порта #18 —
один байт протокола. Размер сообщения:
| Последовательность | Значение |
|---|---|
<code> |
Make — клавиша нажата (код < 0xF0) |
0xF0 <code> |
Break — клавиша отпущена |
0xE0 <code> |
Extended — расширенная клавиша (стрелки, RAlt, RCtrl) |
0xE0 0xF0 <code> |
Extended break |
Make-байт устанавливает бит клавиши в held-карте. Break-байт сбрасывает.
FSM декодирования (2 состояния):
PEND_MAKE:
получить байт B
if B == 0xF0 → переход PEND_BREAK
if B == 0xE0 → переход PEND_EXT, Bзап = B
иначе → make(B)
PEND_BREAK:
получить байт B
переход PEND_MAKE
if (ext_flag) → break_ext(B); ext_flag = 0
иначе → break(B)
PEND_EXT:
получить байт B
if B == 0xF0 → ext_flag = 1, переход PEND_BREAK_EXT
иначе → make_ext(B); переход PEND_MAKE
PEND_BREAK_EXT:
получить байт B
ext_flag = 0
break_ext(B)
переход PEND_MAKE
9.3.4 Битовая карта held-состояний
Массив 128 байт (1024 бита для кодов 0..1023, расширенные — с битом
0x0100).
Простая реализация:
; HL = code (0..0x1FF)
; IX = base адреса карты
LD A, H
AND A, 0x07 ; номер бита 8..10
LD D, A
LD A, L
RRCA ; HL/8 (3 сдвига)
RRCA
RRCA
AND A, 0x1F
LD L, A
LD A, H
AND A, 0x07
ADD A, A ; +8*...
ADD A, A
ADD A, A
ADD A, L
LD L, A
LD H, 0x00
ADD IX, HL ; IX = &bitmap[code / 8]
LD A, D
INC A
LD B, A ; B = номер бита в байте + 1
XOR A
SCF ; carry=1 для make, 0 для break
RL A
DJNZ $-2 ; A = 1 << (code & 7)
; make: OR (IX), A ; break: CPL + AND (IX), A
9.3.5 Overrun recovery
SIO FIFO глубиной 3 байта. Если обработчик прерывания не успевает забрать байты (длинная DI-секция, акселератор), FIFO переполняется — RR1 bit5 = 1. С этого момента цепочка make+break нарушена, возможны залипшие клавиши.
Алгоритм восстановления:
-
Обнаружить overrun (читать SIO RR1, бит 5).
-
Сбросить held-карту всех клавиш, кроме:
Код PS/2 Клавиша Почему 0x12Left Shift PS/2 автоповтор — только последняя 0x59Right Shift нажатая клавиша. Если сбросить 0x14Left Ctrl модификатор, он не восстановится до 0x11Left Alt физического отпускания. Игра «теряет» 0xE014Right Ctrl шифт/контрол/альт при каждом overrun. 0xE011Right Alt -
Взвести флаг «был overrun» — основной код может проигнорировать held-состояние на этот кадр.
9.3.6 Координация с DSS
Пока обработчик забирает байты с SIO-A, FIFO пуст — DSS не видит клавиатуру. Для вызова DSS-функций (консоль, диалоговые окна):
- Переключить флаг «raw active = 0» в IM2-обработчике.
- Обработчик начинает пропускать байты в DSS (JP
0038hили через штатный трамплин). - После завершения ввода — переключить флаг обратно.
Внимание: если обработчик просто перестаёт читать SIO-A, но не возвращает управление DSS, порт SIO-A перестанет генерировать прерывания (FIFO полон, Rx готов = 0). Для корректной передачи управления нужно разрешить DSS-обработчику читать SIO-A.
9.3.7 Пример: минимальный raw-обработчик
; Флаги: raw_active = 1 — перехватывать, 0 — пропускать в DSS
; Карта: kbd_held — 128 байт, занулена при старте
; FSM: kbd_state — 0=PEND_MAKE, 1=PEND_BREAK, 2=PEND_EXT, 3=PEND_BREAK_EXT
; kbd_ext — флаг extended (0/1)
KBD_ISR:
; Проверить, что прерывание от SIO-A
IN A, (#19) ; SIO-A RR0
BIT 0, A ; бит 0 = прерывание от SIO-A?
RET Z ; нет — пропустить
IN A, (#18) ; прочитать байт скан-кода
BIT raw_active ; перехват активен?
JP Z, DSS_IRQ ; нет — пусть DSS обработает
; FSM: текущее состояние в kbd_state
LD HL, kbd_state
LD A, (HL)
AND A, 3
JP Z, .pend_make
DEC A
JP Z, .pend_break
DEC A
JP Z, .pend_ext
; .pend_break_ext
LD (HL), 0 ; → PEND_MAKE
LD A, B ; код клавиши
CALL break_ext
EI
RETI
.pend_make:
LD A, B ; байт с SIO-A (уже в B из IN)
CP 0xF0
JR Z, .set_break
CP 0xE0
JR Z, .set_ext
CALL make ; обычный make
LD (HL), 0 ; остаёмся PEND_MAKE
EI
RETI
.set_break:
LD (HL), 1 ; → PEND_BREAK
EI
RETI
.set_ext:
LD (HL), 2 ; → PEND_EXT
EI
RETI
.pend_break:
LD (HL), 0 ; → PEND_MAKE
CALL break
EI
RETI
.pend_ext:
LD A, B
CP 0xF0
JR Z, .set_ext_break
CALL make_ext
LD (HL), 0
EI
RETI
.set_ext_break:
LD (HL), 3 ; → PEND_BREAK_EXT
EI
RETI
Размер кода — ~60 байт.
9.4 Мышь
Драйвер мыши установлен в системном shell Sprinter и доступен через
SST-вызов RST 30h.
9.4.1 SST-функции мыши
Вызов: RST 30h с A = номер функции. Выход в зависимости от функции
(см. таблицу).
| № | Назначение | Вход | Выход |
|---|---|---|---|
$00 |
Инициализация | A=$00 | — |
$01 |
Показать курсор | A=$01 | — |
$02 |
Спрятать курсор | A=$02 | — |
$03 |
Читать состояние | A=$03 | DE=X, HL=Y, A=buttons (b0=left, b1=right) |
$04 |
Переместить курсор | A=$04, DE=X, HL=Y | — |
$05 |
Установить границы X | A=$05, DE=min, HL=max | — |
$06 |
Установить границы Y | A=$06, DE=min, HL=max | — |
$07 |
Вид курсора (текст) | A=$07, DE=sym_and:sym_xor, HL=attr_and:attr_xor | — |
$09 |
Загрузить курсор (граф.) | A=$09, IX=pointer | — |
$0B |
Прочитать курсор | A=$0B | IX=pointer (копия) |
$0C |
Чувствительность X | A=$0C, D=value (1..255) | — |
$0D |
Чувствительность Y | A=$0D, D=value (1..255) | — |
$0E |
Прочитать чувств. X | A=$0E | D=value |
$0F |
Прочитать чувств. Y | A=$0F | D=value |
$11 |
Уведомить о смене режима | A=$11, D=режим (0=текст, 1=граф-256, 2=граф-16) | — |
Пример: прочитать состояние мыши в asm:
LD A, $03 ; READ
RST 30h
; DE = X, HL = Y, A = buttons
LD (mouse_x), DE
LD (mouse_y), HL
AND A, 3
LD (mouse_btn), A
9.4.2 Чувствительность
Значение — делитель: драйвер считает N raw-шагов мыши, прежде чем сдвинуть курсор на 1 пиксель. Меньше = быстрее (чувствительнее). Рекомендуемое начальное значение: 2 по обеим осям.
LD A, $0C ; SET_SENS_H
LD D, 2
RST 30h
LD A, $0D ; SET_SENS_V
LD D, 2
RST 30h
9.4.3 Порядок инициализации
После загрузки драйвер мыши уже инициализирован shell. Если программа начинает «с чистого листа» (без shell), инициализация обязательна:
LD A, $00 ; INIT — инициализировать драйвер
RST 30h
LD A, $05 ; BOUNDS_X: границы экрана
LD DE, 0
LD HL, 319 ; для 320×256
RST 30h
LD A, $06 ; BOUNDS_Y
LD DE, 0
LD HL, 255
RST 30h
LD A, $01 ; SHOW — показать курсор
RST 30h
9.4.4 Смена видеорежима
После переключения видеорежима (графический → текстовый и обратно)
обязательно вызвать $11:
LD A, $11
LD D, 1 ; 0=текст, 1=граф-256, 2=граф-16
RST 30h
Иначе драйвер применяет старые координаты, и курсор рисуется неправильно.
9.4.5 Прямой доступ к SIO-B (raw mouse)
Мышь подключена через SIO-B (порты #1A/#1B), Microsoft Serial Mouse,
1200 бод 7E1. Если программа хочет читать мышь напрямую, минуя
SST-драйвер (например для нестандартного протокола или экономии на
SST), SIO-B нужно инициализировать самостоятельно:
Инициализация SIO-B (1200 7E1):
LD HL, .sio_init
LD B, 6
LD C, #1B ; SIO-B control
OTIR
JR .done
.sio_init:
DB #10 ; WR0: сброс RR1
DB #04 ; WR1: прерывания выкл
DB #44 ; WR4: ×1 клок, 1 стоп, без паритета
DB #C1 ; WR3: Rx 8 бит
DB #EA ; WR5: Tx 8 бит, DTR+RTS=1
DB #15 ; WR0: сброс ошибки + выбран RR1
.done:
Чтение байта с мыши (опрос, без прерываний):
IN A, (#1B) ; SIO-B RR0
BIT 0, A ; бит 0 = Rx готов?
JR Z, .no_data
IN A, (#1A) ; прочитать байт
; обработать ...
Формат пакета Microsoft Serial Mouse:
Длина: 3 байта.
| Байт | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 | L | R | Y7 | Y6 |
| 1 | 0 | X6 | X5 | X4 | X3 | X2 | X1 | X0 |
| 2 | 0 | Y5 | Y4 | Y3 | Y2 | Y1 | Y0 | X7 |
- Координаты X/Y — относительные (приращения с последнего пакета), знаковые.
X7— старший бит X (знак),X6..X0— младшие.Y7..Y6— старшие биты Y,Y5..Y0— младшие.L— левая кнопка (1 = нажата),R— правая.
Декодирование:
; B = байт 0, C = байт 1, D = байт 2
; X:
LD A, C ; байт 1: X6..X0
LD E, A
LD A, D ; байт 2: X7
RLCA
RLCA
AND A, 0x80
OR A, E ; A = X (7 бит + знак)
; знаковое расширение в HL
LD L, A
RLCA
SBC A, A
LD H, A ; HL = X (знаковое 16-бит)
; Y:
LD A, B ; байт 0: Y7..Y6
RLCA
RLCA ; A[1:0] = Y7..Y6
AND A, 0xC0
LD E, A
LD A, D ; байт 2: Y5..Y0
RLCA
RLCA ; A[7:2] = Y5..Y0
AND A, 0xFC
OR A, E ; A = Y (7 бит + знак)
; знаковое расширение в DE
LD E, A
RLCA
SBC A, A
LD D, A ; DE = Y (знаковое 16-бит)
; Кнопки:
LD A, B
AND A, 0x30 ; биты L (4) и R (5)
RRCA ; A = 0..3
RRCA
RRCA
RRCA
LD (buttons), A
9.5 Особенности игровой клавиатуры
Документ: kbd-games.md. Ниже — ключевые паттерны для реализации.
- Held-state vs edge-detect: DSS — событийный (сообщает только момент нажатия). Raw-канал (§9.3) — state-based (зажата/не зажата прямо сейчас). Играм нужен второй.
- Sync once per frame: раз в кадр (по кадровому прерыванию) проверять held-карту. Между кадрами карта обновляется только в IM2-обработчике.
- Edge-detect: для одноразовых действий (прыжок, выстрел) — запоминать
предыдущее held-состояние и сравнивать:
pressed = held & ~prev_held prev_held = held - Типоматик: игнорировать повторные make, пока не придёт break. Реализация: при make — установить бит только если он ещё не был установлен; при break — сбросить.
- Автоотмена (reject): если за кадр нажато больше N клавиш (например 4) — игнорировать все (защита от баунса и мусора).
- Recovery: при overrun — очистить held-карту всех клавиш, кроме модификаторов (LShift 0x12, RShift 0x59, LCtrl 0x14, LAlt 0x11, RCtrl 0xE014, RAlt 0xE011).
9.6 Кросс-ссылки
- Прерывания (IM2, вектор #FF):
07-irq.md §7.2 - DSS-функции клавиатуры:
03-dss.md §3.6 - BIOS FN_KBD_OUT:
02-bios.md §2.7 - Порт клавиатуры
#FE:11-ports.md §11.3 - Игровая клавиатура:
kbd-games.md - Баги:
12-bugs.md - SIO и порты прерываний:
11-ports.md §11.2