# 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` —
один байт протокола. Размер сообщения:

| Последовательность | Значение |
|:------------------|:---------|
| `<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`).

Простая реализация:

```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`
