Files
rt-4d/docs/subsystems/input.md
T
viktor 5f4d207aa5 Мост к DMR-модулю, разбор спектра REFV DualTachyon, карта запчастей прошивки
Баузбенд FM100B:
- найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём
- расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации
- инструмент tools/fm100b.py (ping/send/raw/scan)
- cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом)
- разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд
- cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14
- исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md)

Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md):
- вызывается как функция горячей клавиши №22 Analog Spectrum
- вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318
- спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC

Декомпозиция (docs/firmware-parts.md):
- два процессора + внешний SPI = три канала внедрения
- карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode
- найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки)
- дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке
- аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля

Прочее:
- везде исправлен режим прошивки: тангента PTT вместо клавиши "*"
- устаревший Ru-4D_Flasher.exe удалён из репозитория
- добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
2026-07-22 21:17:20 +09:00

312 строки
17 KiB
Markdown
Исходник Ответственный История

Этот файл содержит неоднозначные символы Юникода
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.
# RT-4D Input Subsystem — Keypad / PTT / Dispatch API
Reverse-engineered from `rt4d_stock_v3.25_abs_0x08002800.bin` (Cortex-M4F Thumb, load base `0x08002800`).
Scope: keypad matrix scan, debounce, key-code map, PTT/side keys, event delivery, and the callable
API + hook points a rewritten UI would use to read input and route keys to a custom router.
All addresses are absolute MCU flash vaddrs. RAM state vars are `0x2000xxxx`.
---
## 0. TL;DR — the input pipeline
```
TICK (main loop, ~1ms cadence)
keypad_task(col) 0x0801B398 ── drives one column output line (GPIOA/GPIOF ODR), cycles col 0..3
│ calls per column:
keypad_scan_col(col) 0x0801B294 ── reads 4 row inputs (GPIOF1, GPIOA8, GPIOA9, GPIOB11),
│ sets/clears bit (1<<(col*4+row)) in RAW word 0x20000B7C (active-low)
keypad_decode() 0x0801130C ── maps RAW bitmask 0x20000B7C → compact KEY CODE (0x00..0x12, 0xFF=none)
keypad_process() 0x08005A14 ── debounce + short/long/repeat classify; writes KeyEv struct @0x20000B57
│ KeyEv[+2]=delivered key, KeyEv[+3]=event type (1=short/long,2=repeat)
ui_input_service() 0x08005E54 ── main-loop input step: calls keypad_process, reads KeyEv[+2],
│ then calls the screen router, then clears KeyEv[+2]
screen_dispatch(ctx,aux,key) 0x08018DF4 ── reads current-screen id ctx[+1] (ctx=0x20002120),
tbb-dispatches to the active screen's key handler
```
**This is a polled GPIO 4×4 matrix (NOT an ADC ladder).** ADC1 is used only for battery/RSSI, not keys.
**Up/Down are matrix keys, not a rotary encoder.** PTT and the two side keys are separate GPIO reads.
---
## 1. Hardware: GPIO 4×4 key matrix
The keypad is a 4-column × 4-row matrix. Columns are driven low one at a time (outputs), rows are read
(active-low inputs, pulled high). Confirmed from `keypad_scan_col` (`0x0801B294`) which reads, per column,
four row pins via the GPIO bit-read helper and packs them into the 16-bit raw word `0x20000B7C`:
| Row idx | Port/pin (mask passed to gpio_read_pin) | Evidence (in 0x0801B294) |
|---|---|---|
| row 0 | **GPIOF pin1** (`0x40021400`, mask `0x0002`) | `ldr 0x40021400; movs r1,#2; bl 0x8021316` |
| row 1 | **GPIOA pin8** (`0x40020000`, mask `0x0100`) | `ldr 0x40020000; mov r1,#0x100` |
| row 2 | **GPIOA pin9** (`0x40020000`, mask `0x0200`) | `ldr 0x40020000; mov r1,#0x200` |
| row 3 | **GPIOB pin11** (`0x40020400`, mask `0x0800`) | `ldr 0x40020400; mov r1,#0x800` |
For each row: `bit = 1 << (col*4 + row)`; if `gpio_read_pin()==0` (pin low = pressed) → `raw |= bit`,
else `raw &= ~bit`. So `0x20000B7C` is a live pressed-keys bitmap, one bit per matrix cell.
Column drive lives in `keypad_task` (`0x0801B398`): a 4-state machine using `0x20000B7A` as the current
column index; each phase writes the column-select via `gpio_write_pin`/BSRR on GPIOA (`0x40020000`) and
GPIOF (`0x40021400`, BSRR/ODR region seen as `0x40021428`/`0x40020428`), then invokes `keypad_scan_col`
for that column. Columns cycle 0→1→2→3 across successive ticks.
### GPIO helper primitives (callable API)
| vaddr | signature | behavior |
|---|---|---|
| `0x08021316` | `int gpio_read_pin(GPIO_TypeDef* port, uint32_t mask)` | returns 1 if **all** masked IDR (`+0x10`) bits set, else 0 |
| `0x08021C6E` | `void gpio_write_pin(GPIO_TypeDef* port, uint32_t mask, int state)` | state 1 → `ODR(+0x0C)|=mask`; state 0 → `ODR&=~mask` |
| `0x08021C4C` | `void gpio_bsrr(GPIO_TypeDef* port, uint32_t mask)` (bit-set/reset via BSRR) | used by column drive & LCD |
---
## 2. KEY CODE MAP — `keypad_decode()` @ `0x0801130C`
Signature: **`uint8_t keypad_decode(void)`** — reads the raw matrix word `*(uint16_t*)0x20000B7C`
and returns the firmware key code. Returns `0xFF` when nothing/unknown is pressed. Every comparison in
the function reads the same `ldrh [0x20000B7C]`; verified all 19 literal loads resolve to `0x20000B7C`.
### Raw matrix-bit → key code (single-key)
| raw `0x20000B7C` value | KEY CODE (return) | physical key (inferred) |
|---|---|---|
| `0x4000` | `0x00` | **0** |
| `0x0002` | `0x01` | **1** |
| `0x0020` | `0x02` | **2** |
| `0x0200` | `0x03` | **3** |
| `0x0004` | `0x04` | **4** |
| `0x0040` | `0x05` | **5** |
| `0x0400` | `0x06` | **6** |
| `0x0008` | `0x07` | **7** |
| `0x0080` | `0x08` | **8** |
| `0x0800` | `0x09` | **9** |
| `0x0001` | `0x0B` | **Menu / M** (col0,row0) |
| `0x0010` | `0x0C` | **Up** ▲ |
| `0x0100` | `0x0D` | **Down** ▼ |
| `0x1000` | `0x10` | **Exit / Back** |
| `0x2000` | `0x0E` | **\*** (star) |
| `0x8000` | `0x0F` | **#** (hash) — used elsewhere as "menu enter" sentinel |
Note key code `0x0A` is not produced by a single raw bit; it is produced only by the combo pattern below
(it is the "PTT/M combined" boot code). The digit codes `0x00..0x09` map 1:1 to digits 0-9.
### Combo / multi-line patterns (special)
| raw `0x20000B7C` | KEY CODE | meaning |
|---|---|---|
| `0x1111` | `0x0A` | all-column row0 held (boot key-combo / factory) |
| `0x2222` | `0x11` | **Side key 1** family / all-column row1 |
| `0x4444` | `0x12` | **Side key 2** family / all-column row2 |
> Interpretation of `0x11`/`0x12` as the two programmable side keys is corroborated by
> `ui_input_service` (`0x08005E54`), which special-cases `KeyEv[+2]==0x11` and `==0x12` *before* the
> normal screen dispatch (see §4), matching the CPS "Side Key 1/2 (Short/Long)" menu items.
### Key-code enum (for the rewrite)
```c
enum key {
KEY_0=0x00, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, // 0x00..0x09
KEY_COMBO_A=0x0A, // boot combo
KEY_MENU=0x0B, // "M"
KEY_UP=0x0C,
KEY_DOWN=0x0D,
KEY_STAR=0x0E, // '*'
KEY_HASH=0x0F, // '#' (also menu-enter sentinel)
KEY_EXIT=0x10, // Back/Exit
KEY_SIDE1=0x11,
KEY_SIDE2=0x12,
KEY_NONE=0xFF
};
```
---
## 3. Debounce + short/long/repeat — `keypad_process()` @ `0x08005A14`
Signature: **`void keypad_process(void)`**. Called once per input-service pass from `ui_input_service`.
Operates on the **key-event struct `KeyEv` @ `0x20000B57`** (byte fields unless noted):
| field | addr | meaning |
|---|---|---|
| `KeyEv[+1]` | `0x20000B58` | live decoded key this pass (`= keypad_decode()`), `0xFF`=none |
| `KeyEv[+2]` | `0x20000B59` | **delivered event key** consumed by the UI (`0xFF` when no event) |
| `KeyEv[+3]` | `0x20000B5A` | **event type**: `1` = press/release (short or long), `2` = auto-repeat |
| `KeyEv[+4]` | `0x20000B5B` | repeat-active flag |
| `KeyEv[+5]` | `0x20000B5C` | `uint16_t` **hold-duration counter** (ticks; incremented while held) |
Behavior (decoded from `0x08005A14``0x08005B54`):
1. `KeyEv[+1] = keypad_decode()`. Also reads PTT: `gpio_read_pin(GPIOA, 0x1000)` (GPIOA pin12) → PTT state at `0x20000B75` (`KeyEv[+0x1E]`); PTT keycode is `0x11`-adjacent handling.
2. While a key stays held: `KeyEv[+5]++` (bounded).
3. On **release** (`KeyEv[+1]==0xFF`) with `KeyEv[+5] > 10`: publish the just-released key as an event (`KeyEv[+2]=key, KeyEv[+3]=1`); otherwise call `key_event_clear()` (`0x08012C24`).
4. **Long/repeat threshold `0x2BC` (700 ticks)**: if same key held and `KeyEv[+5] > 0x2BC` → publish repeat (`KeyEv[+2]=key, KeyEv[+3]=2, KeyEv[+4]=1`). A secondary `>10` gate distinguishes the short vs long delivery. Beep feedback is emitted via `0x8014CB8` on valid/invalid keys.
5. Screen-change edge (`0x08005B38`): if the current screen id changed vs `KeyEv[+1]`, resets the hold counter — prevents key bleed across screens.
Supporting call: `key_event_clear()` **`0x08012C24`** — `void key_event_clear(void)` sets
`KeyEv[+0..+2]=0xFF`, `KeyEv[+3..+5]=0`. **This is the "consume/ack the pending key" API.**
---
## 4. Delivery to the active screen — `ui_input_service()` @ `0x08005E54`
Signature: **`void ui_input_service(void)`** — the main-loop input step. Sequence:
1. `keypad_process()` (`0x08005A14`) — refresh KeyEv.
2. Service a couple of periodic timers (`0x200201DC`/`0x201F5E4` housekeeping — not input).
3. Read `KeyEv[+2]` (delivered key). Side keys `0x11`/`0x12` are handled specially (their own screen-independent action path) and are **not** forwarded to the generic router. `0xFF` (no key) is skipped.
4. Otherwise call **`screen_dispatch(ctx=0x20002120, aux=0x20002014, key=KeyEv[+2])`** (`0x08018DF4`).
5. After dispatch, clear: `KeyEv[+2]=0xFF` (`0x20000B59`) and `ctx[+0x15]=0xFF` (`0x20002135`).
6. Then paints: `0x080136E4` (status), `0x080142C0` (menu/standby render).
### The router — `screen_dispatch(ctx, aux, key)` @ `0x08018DF4`
Signature: **`void screen_dispatch(void* ctx, void* aux, uint8_t key)`** — `ctx = 0x20002120`.
- If `key == 0x0F` (`#`): enter/menu path → `0x0800F0E4` then `0x08005484` (menu-enter), return.
- Else read **current-screen id = `ctx[+1]` (byte at `0x20002121`)**, range 0..5, and `tbb`-dispatch:
| `ctx[+1]` | screen key handler | notes |
|---|---|---|
| 0 | `0x08018E5A` | **standby / main VFO screen** |
| 1 | `0x08018E5A` | (shares standby handler) |
| 2 | `0x08019018` | secondary screen (dual-area / menu list) |
| 3 | `0x0801926C` | screen 3 |
| 4 | `0x08018F08` | screen 4 |
| (5) | falls through | |
Inner standby handler `0x08018E5A(ctx, aux, key)`: `if key==0xFF return;` sets `ctx[+0x15]=0xFF`;
then `key-0x0A; cmp #7` `tbb` to handle the nav/function keys `0x0A..0x10` (Menu/Up/Down/*/#/Exit),
and the default branch treats numeric keys as ASCII (`key+0x30`) → digit-entry handler `0x080134F8`.
Menu navigation calls `0x08007984`; audible feedback via `0x08014CB8` (beep freq `0x1B8` ok / `0x65C` err).
**`ctx[+1]` (`0x20002121`) is the current-screen selector, and the `tbb` table at `0x08018E16` is the
screen→handler map. This is the exact hook point for a rewritten UI router** (see §7).
---
## 5. PTT, side keys, monitor, and analog inputs
- **PTT**: GPIO input **GPIOA pin12** (`gpio_read_pin(0x40020000, 0x1000)`), read inside `keypad_process`
and also standalone at `0x08005A32` (parent `0x08005A14`). PTT is *not* part of the matrix; it latches
into `KeyEv[+0x1E]` (`0x20000B75`) and gates TX. A separate small helper stores PTT/key state and
compares against key code `0x11`.
- **Side key 1 / Side key 2**: surface as decoded key codes `0x11` / `0x12` (combo patterns `0x2222` /
`0x4444`), intercepted in `ui_input_service` (`0x08005E54`) ahead of the router, matching the CPS
"Side Key 1/2 Short/Long" definitions. Their configured action is looked up from settings there.
- **Monitor / squelch-open**: driven from the same key/side-key path (no dedicated GPIO monitor line found
separate from the side keys); the "Monitor" function is a side-key/long-press action, not a distinct pin.
- **Rotary/channel knob & Up/Down**: **no rotary encoder**. Channel change is the matrix Up (`0x0C`) /
Down (`0x0D`) keys handled by the active screen. No quadrature-decode code exists.
- **Volume/power knob**: analog volume is a hardware potentiometer in the audio path (not MCU-sampled);
no ADC channel is decoded as a knob position. **ADC1 (`0x40012000`) is battery voltage + RSSI only**
`adc_init` @ `0x08002D64`, battery read/convert @ `0x08010960` (`raw*4/0x42` scaling → `BATT:x.xV`),
ADC EOC counters incremented in the ADC ISR `0x08002D1C`. None of this feeds key input.
### Boot-time key-combo handler `0x08003060`
`void boot_key_scan(void)` — at power-on reads `keypad_decode()` directly (not through KeyEv) and matches
combos: `0x11`, `0x0A` (→ enters a special mode: `0x080149D4`/`0x0801492C`, PC-programming/init screen),
`0x0F`, etc. This implements the hold-key-at-power-on entries (flash mode = **PTT held at power-on**). Uses the same key codes as §2.
---
## 6. RAM state variables (input)
| addr | name | type | meaning |
|---|---|---|---|
| `0x20000B7A` | `kp_cur_col` | u8 | current column being driven/scanned (0..3) |
| `0x20000B7C` | `kp_raw` | u16 | live pressed-key bitmap `1<<(col*4+row)`, active-low packed |
| `0x20000B57` | `KeyEv` | struct | key-event struct (base; see §3 for fields +1..+5) |
| `0x20000B58` | `KeyEv.live` | u8 | live decoded key |
| `0x20000B59` | `KeyEv.key` | u8 | **delivered event key** (read by router; `0xFF`=none) |
| `0x20000B5A` | `KeyEv.type` | u8 | 1=short/long press, 2=auto-repeat |
| `0x20000B5B` | `KeyEv.rep` | u8 | repeat-active flag |
| `0x20000B5C` | `KeyEv.dur` | u16 | hold-duration counter (ticks); long/repeat @ `0x2BC`=700 |
| `0x20000B75` | `KeyEv.ptt` | u8 | PTT pressed flag (GPIOA12) |
| `0x20002120` | `ui_ctx` | struct | UI/screen context passed to router |
| `0x20002121` | `ui_ctx.screen` | u8 | **current-screen id** (router `tbb` selector, 0..5) |
| `0x20002135` | `ui_ctx[+0x15]` | u8 | per-frame scratch (cleared after dispatch) |
---
## 7. CALLABLE API for a rewritten UI
### 7a. Read input (poll model — recommended)
The cleanest reuse is to keep the stock scan/decode/debounce and read the published event:
```c
// stock addresses (Thumb; call with bit0 set)
uint8_t keypad_decode(void); // 0x0801130C -> raw key code, 0xFF=none (no debounce)
void keypad_process(void); // 0x08005A14 -> updates KeyEv (debounce+long/repeat)
void key_event_clear(void); // 0x08012C24 -> ack/consume pending key
#define KeyEv_key (*(volatile uint8_t*)0x20000B59) // delivered key, 0xFF=none
#define KeyEv_type (*(volatile uint8_t*)0x20000B5A) // 1=press,2=repeat
#define KeyEv_dur (*(volatile uint16_t*)0x20000B5C) // hold ticks (for your own long-press cutoff)
#define KeyEv_ptt (*(volatile uint8_t*)0x20000B75) // PTT
// Our UI main loop:
for(;;){
keypad_process(); // let stock code scan+debounce
uint8_t k = KeyEv_key;
if(k != 0xFF){
my_router(k, KeyEv_type); // <-- our dispatch
key_event_clear(); // consume
}
// ... our render ...
}
```
You do **not** need to touch the timer ISRs: `keypad_task`/`keypad_scan_col` are invoked from the stock
tick path that also runs `keypad_process` housekeeping; if you replace the main loop you can call
`keypad_task(col)` yourself per tick, or simpler—call `keypad_process()` which reads the already-scanned
`kp_raw`. (If you fully own the loop, drive `keypad_task(0..3)` round-robin each ~1ms so `kp_raw` refreshes.)
Raw / low-level entry points if you want to bypass debounce:
```c
int gpio_read_pin (void* port, uint32_t mask); // 0x08021316
void gpio_write_pin(void* port, uint32_t mask,int);// 0x08021C6E
void keypad_scan_col(int col); // 0x0801B294 scans one column into kp_raw
void keypad_task(void); // 0x0801B398 column-drive state machine (call each tick)
```
### 7b. Hook to route keys to OUR router (drop-in replacement)
The single interception point is **`ui_input_service` (`0x08005E54`)** → it calls
**`screen_dispatch(ctx=0x20002120, aux=0x20002014, key)` at `0x08005F1E`**. Two options:
1. **Replace the router (minimal patch):** repoint the `bl 0x08018DF4` at `0x08005F1E` to our own
`router(ctx, aux, key)`. We then own all per-screen handling while stock keypad scan/decode/debounce,
PTT, side-key pre-handling, and post-dispatch clear remain intact. Our router reads
`ui_ctx.screen` (`0x20002121`) as the active-screen id (or we manage our own screen id) and dispatches.
2. **Replace the whole input+render loop:** call `keypad_process()` ourselves (7a) and never enter
`ui_input_service`; then stock `screen_dispatch` and the render calls (`0x080136E4`, `0x080142C0`) are
bypassed entirely — full UI ownership. Keep calling stock lower-level draw/RF/DMR APIs.
Either way the **serial/CPS protocol and SPI codeplug format are untouched** — the input path has no
contact with the USART6 framer (`0x0801F864`) or the SPI region writers, so replacing the UI router does
not affect CPS compatibility.
---
## 8. Confidence
| Item | Confidence | Basis |
|---|---|---|
| `keypad_decode` @0x0801130C + full key-code map | **High** | 19 loads all resolve to `0x20000B7C`; explicit `cmp`/`movs r0,#code`; single-exit |
| GPIO 4×4 matrix (not ADC ladder); row pins GPIOF1/GPIOA8/9/GPIOB11 | **High** | `keypad_scan_col` masks `2/0x100/0x200/0x800` on distinct ports + `1<<(col*4+row)` packing |
| `keypad_process` debounce + long/repeat (`0x2BC` threshold) + KeyEv layout | **High** | full disasm `0x08005A14`; field offsets confirmed via stores |
| `ui_input_service``screen_dispatch` route; ctx `0x20002120`, screen id `[+1]` | **High** | resolved call-site literals + `tbb` table at `0x08018E16` |
| PTT = GPIOA pin12; ADC1 = battery/RSSI only (no knob) | **High** | `gpio_read_pin(GPIOA,0x1000)` in key path; ADC only in battery/ISR |
| Physical-key labels (which code = Menu/Up/Down/Exit/*/#) | **Medium-High** | inferred from standby handler usage (digit ASCII, nav tbb, `#`=menu-enter, `0x11/0x12`=side keys); exact silk-screen assignment of the four *non-digit* codes 0x0B/0x0C/0x0D/0x10 should be confirmed on-device |
| Side keys = codes `0x11`/`0x12` (combo `0x2222`/`0x4444`) | **Medium** | special-cased before router; matches CPS side-key menu; combo-pattern origin unusual—verify on hardware |