- Полный RE стока V3.25 (Cortex-M4F) + FM100B: карта памяти, протокол, codeplug, UI-архитектура - Русификация: свой CP1251-шрифт + патч рендера, перевод меню и надписей, ребренд Ru-4D V3.25 - Блюпринт переделки UI + C-тулчейн (clang thumbv7em), доказан инъекцией - Готовые флешеры: WebSerial .html и Windows .exe со вшитой прошивкой - Дамп SPI рации, стоковая прошивка, инструменты сборки Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
17 KiB
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) |
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/0x12as the two programmable side keys is corroborated byui_input_service(0x08005E54), which special-casesKeyEv[+2]==0x11and==0x12before the normal screen dispatch (see §4), matching the CPS "Side Key 1/2 (Short/Long)" menu items.
Key-code enum (for the rewrite)
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):
KeyEv[+1] = keypad_decode(). Also reads PTT:gpio_read_pin(GPIOA, 0x1000)(GPIOA pin12) → PTT state at0x20000B75(KeyEv[+0x1E]); PTT keycode is0x11-adjacent handling.- While a key stays held:
KeyEv[+5]++(bounded). - On release (
KeyEv[+1]==0xFF) withKeyEv[+5] > 10: publish the just-released key as an event (KeyEv[+2]=key, KeyEv[+3]=1); otherwise callkey_event_clear()(0x08012C24). - Long/repeat threshold
0x2BC(700 ticks): if same key held andKeyEv[+5] > 0x2BC→ publish repeat (KeyEv[+2]=key, KeyEv[+3]=2, KeyEv[+4]=1). A secondary>10gate distinguishes the short vs long delivery. Beep feedback is emitted via0x8014CB8on valid/invalid keys. - Screen-change edge (
0x08005B38): if the current screen id changed vsKeyEv[+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:
keypad_process()(0x08005A14) — refresh KeyEv.- Service a couple of periodic timers (
0x200201DC/0x201F5E4housekeeping — not input). - Read
KeyEv[+2](delivered key). Side keys0x11/0x12are handled specially (their own screen-independent action path) and are not forwarded to the generic router.0xFF(no key) is skipped. - Otherwise call
screen_dispatch(ctx=0x20002120, aux=0x20002014, key=KeyEv[+2])(0x08018DF4). - After dispatch, clear:
KeyEv[+2]=0xFF(0x20000B59) andctx[+0x15]=0xFF(0x20002135). - 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 →0x0800F0E4then0x08005484(menu-enter), return. - Else read current-screen id =
ctx[+1](byte at0x20002121), range 0..5, andtbb-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 insidekeypad_processand also standalone at0x08005A32(parent0x08005A14). PTT is not part of the matrix; it latches intoKeyEv[+0x1E](0x20000B75) and gates TX. A separate small helper stores PTT/key state and compares against key code0x11. - Side key 1 / Side key 2: surface as decoded key codes
0x11/0x12(combo patterns0x2222/0x4444), intercepted inui_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/0x42scaling →BATT:x.xV), ADC EOC counters incremented in the ADC ISR0x08002D1C. 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 "hold */#/side at power-on" entries. 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:
// 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:
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:
-
Replace the router (minimal patch): repoint the
bl 0x08018DF4at0x08005F1Eto our ownrouter(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 readsui_ctx.screen(0x20002121) as the active-screen id (or we manage our own screen id) and dispatches. -
Replace the whole input+render loop: call
keypad_process()ourselves (7a) and never enterui_input_service; then stockscreen_dispatchand 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 |