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