Files
rt-4d/research/re/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

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/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)

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 0x08005A140x08005B54):

  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() 0x08012C24void 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 onlyadc_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

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:

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