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