# 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 Принцип 1. Установить свой обработчик IM2 (общий вектор `#FF`). 2. В обработчике читать байты с SIO-A (порт `#18`), декодировать PS/2 Scan Code Set 2, обновлять битовую карту held-состояний. 3. Основной код по кадровому прерыванию (или раз в кадр) читает карту опросом — без ожидания. 4. 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` — один байт протокола. Размер сообщения: | Последовательность | Значение | |:------------------|:---------| | `` | Make — клавиша нажата (код < 0xF0) | | `0xF0 ` | Break — клавиша отпущена | | `0xE0 ` | Extended — расширенная клавиша (стрелки, RAlt, RCtrl) | | `0xE0 0xF0 ` | 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`). Простая реализация: ```asm ; 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 нарушена, возможны залипшие клавиши. **Алгоритм восстановления:** 1. Обнаружить overrun (читать SIO RR1, бит 5). 2. Сбросить held-карту **всех клавиш, кроме**: | Код PS/2 | Клавиша | Почему | |:--------:|:--------|:-------| | `0x12` | Left Shift | PS/2 автоповтор — только последняя | | `0x59` | Right Shift | нажатая клавиша. Если сбросить | | `0x14` | Left Ctrl | модификатор, он не восстановится до | | `0x11` | Left Alt | физического отпускания. Игра «теряет» | | `0xE014` | Right Ctrl | шифт/контрол/альт при каждом overrun. | | `0xE011` | Right Alt | | 3. Взвести флаг «был overrun» — основной код может проигнорировать held-состояние на этот кадр. ### 9.3.6 Координация с DSS Пока обработчик забирает байты с SIO-A, FIFO пуст — DSS не видит клавиатуру. Для вызова DSS-функций (консоль, диалоговые окна): 1. Переключить флаг «raw active = 0» в IM2-обработчике. 2. Обработчик начинает пропускать байты в DSS (JP `0038h` или через штатный трамплин). 3. После завершения ввода — переключить флаг обратно. **Внимание:** если обработчик просто перестаёт читать SIO-A, но не возвращает управление DSS, порт SIO-A перестанет генерировать прерывания (FIFO полон, Rx готов = 0). Для корректной передачи управления нужно разрешить DSS-обработчику читать SIO-A. ### 9.3.7 Пример: минимальный raw-обработчик ```asm ; Флаги: 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:** ```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 по обеим осям. ```asm 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), инициализация обязательна: ```asm 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`: ```asm 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):** ```asm 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: ``` **Чтение байта с мыши (опрос, без прерываний):** ```asm 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` — правая. **Декодирование:** ```asm ; 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`