Files
rt-4d/docs/UI-Architecture.md
T
viktorиClaude Opus 4.8 ae36c3b729 RT-4D: реверс прошивки, русификация, кастомный UI, флешеры
- Полный 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>
2026-07-08 15:47:22 +09:00

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

RT-4D Stock Firmware — UI Architecture & API Reference (for a UI rewrite)

Target image: stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin — ARM Cortex-M4F (STM32F407-class Artery AT32F407 clone), Thumb, load/vaddr base 0x08002800. App flash spans 0x08002800 .. 0x0802885C (~152 KB). All disassembly via capstone CS_ARCH_ARM + CS_MODE_THUMB.

This is the master blueprint for rewriting the RT-4D's entire UI (standby screen + menu system + all screens + navigation) while reusing the stock firmware's display/keys/RF/DMR/codeplug functions as an SDK, and while keeping the SPI codeplug format and the CPS serial protocol byte-identical so the stock Radtel CPS keeps working. It synthesizes seven subsystem RE docs (re/main-loop.md, re/display.md, re/input.md, re/radio.md, re/dmr.md, re/codeplug.md, re/main-screen.md), resolves their contradictions, and preserves every concrete vaddr/signature.


1. Executive summary

How the UI is structured. Boot glue hands off to the application superloop app_main @0x0802136C. Each pass, the superloop honours a reboot flag, then branches on serial-session state: if the radio is not in a PC-programming / SPI-write session it runs the normal UI tick ui_tick_normal @0x080207DC; otherwise it services the CPS/serial paths. The normal UI tick is a cooperative scheduler that, among other periodic tasks, calls exactly two dispatchers:

  • Screen draw: ui_draw_dispatch @0x0801E1BC (called at 0x080207E6) — tbb-jumps on the single screen-state byte g_screen @0x200008B3 (0..11) to the current screen's incremental draw routine.
  • Screen input: ui_process_key @0x0801E6EC (called at 0x08020816) — first calls keypad_process @0x08005A14 (scan+debounce), then tbb-jumps on the same g_screen to the current screen's key handler handler(u8 keycode, u8 keystate).

Both dispatchers are keyed on the same state byte and their 12-entry tbb tables are positionally parallel (index i = same screen in both). Drawing is incremental / dirty-flag driven into a page-addressed 128×64 mono LCD with no framebuffer (pixels stream straight to GDDRAM).

Single best hook strategy. Patch the two bl call sites in the scheduler0x080207E6 (draw) and 0x08020816 (key) — to call your own router (my_draw_router / my_key_router) living in free app flash. Your router owns rendering + navigation and calls the stock lower-level APIs for everything else. This is a 2-instruction redirect that leaves the superloop, the RF/DMR RX pipeline, battery/scan tasks, and the entire serial/CPS + SPI region-write path untouched. (See §4 for exact patch encoding and a phased plan.)

CPS-compatibility boundary. Two things must stay byte-identical: (a) the on-SPI codeplug format (region bases, record strides/fields, settings dual-bank + 0xABCD magic, per-unit calibration at 0x000000), and (b) the USART6 CPS protocol (ISR 0x0802061C, framer 0x0801F864, dispatcher 0x08019790, region R/W handler 0x080188D4, its buffers, and all opcodes). The UI never touches these directly — it mutates the RAM working copies of settings/channels and calls the stock save wrappers, which preserve the format. Because the CPS transfers raw SPI blocks, preserving the on-flash format automatically preserves CPS compatibility. A third internal bus (USART3 ↔ FM100B DMR baseband, "ATC" 0x68…0x10 framing) carries all RF/DMR programming and is independent of both boundaries — the UI reuses it via the RF/DMR wrappers.


2. Memory & architecture map

Region Range / value Notes
Bootloader flash 0x08000000 .. 0x08002800 separate image; CPS bootloader opcodes 0x39/0x57. Do not touch.
App flash 0x08002800 .. ~0x0802885C this image (~152 KB). Vector base set into VTOR by app_main.
Free app flash 0x08029000+ (and gaps at 0x08028860/0x08028900, already used by russification cave + font) inject the custom UI layer here (§4.4).
SRAM 0x20000000 .. 0x20020000 (128 KB), initial SP 0x2000AE48 UI state vars live in 0x2000xxxx.
External SPI data-flash 4 MB (dumped in radio-spi-dump.bin) codeplug + calibration + fonts. Read via spi_flash_read @0x08021828 (opcode 0x03). Codeplug format frozen.
MCU peripherals RCC 0x40023800; USART1 0x40010000, USART2 0x40004400, USART3 0x40004800 (FM100B DMR), USART6 0x40011400 (PC/CPS); SPI2 0x40003800; ADC1 0x40012000 (batt/RSSI); DAC 0x40007400 (audio); TIM1/2/9/10; GPIOA–F TIM2 ISR @0x0801DDCD = tick; SysTick unused.
Display mono 128×64, 8 pages × 128 cols, bit-banged GPIO (RS 0x40010000, CLK/DATA GPIOB 0x40020400, CS GPIOA 0x40020000), no framebuffer fonts fetched from SPI 0x19C000 (ASCII 14 B/glyph) / 0x19E000 (GBK 28 B/glyph).

Three independent buses (keep them straight): USART6 = CPS (frozen), USART3 = FM100B/DMR (reuse via ATC wrappers), SPI2/GPIO SPI = codeplug+fonts (format frozen; reads OK). The UI rewrite touches only the LCD GPIO and reads.


3. The main loop & screen state machine — exact hook points

3.1 Boot → superloop (verified)

Reset 0x08002AC0 → SystemInit(0x0801DA2D) → __main 0x080029E0 → __rt_entry 0x08002AA0
   → bl 0x0802136C            ; app_main — the application superloop
app_main 0x0802136C:
   hw_init(0x0801DAD0); VTOR=0x08002800; init B/C/D/E; then LOOP:
   0x0802138C  if (*(u8*)0x20000C54) nvic_system_reset(0x0801A38C)   ; reboot flag
   0x08021396  if (mode 0x20000B66==0 && 0x20000C12∉{2,4} && 0x20000C57==0)
   0x080213B2      bl 0x080207DC        ; ★ NORMAL UI TICK  (ui_tick_normal)
               else  … CPS / SPI-write session handlers …            ; ← DO NOT TOUCH
   0x0802140C  bl 0x0801F854 (serial_poll)   ; every pass — CPS path — keep
   0x08021410  bl 0x0801FE50 (housekeeping)  ; every pass — keep
   0x08021414  loop

3.2 ui_tick_normal @0x080207DC — the two dispatch calls to hook (verified by disasm)

0x080207E6  bl 0x0801E1BC   ; ★ ui_draw_dispatch   (SCREEN DRAW — hook here)
0x08020816  bl 0x0801E6EC   ; ★ ui_process_key     (SCREEN INPUT — hook here)

Both are unconditional every UI pass. Other scheduler calls (0x0801E050 side-key handler, 0x0801E3F8 line-buffer builder, and period-gated RSSI/battery/scan tasks throttled by counters 0x20000BF1..BF8) can be left as-is or repointed for full control.

3.3 The screen state machine (single selector + two mirror tbb tables)

g_screen      = *(u8*)0x200008B3   ; current screen id, 0..11 — 55 literal-pool xrefs
g_screen_prev = *(u8*)0x200008B2   ; previous screen (back/restore)

Both dispatchers guard cmp g_screen,#0x0C; bhs default before the tbb. Draw-table base @0x0801E1CE, input-table base @0x0801E784 (each 12 one-byte half-word offsets). Navigation = write the target id to 0x200008B3 (+ per-screen enter side-effects); optionally save old id to 0x200008B2.

id draw handler input handler screen
0 0x08013BD8 0x08017F60 Home / standby (VFO/channel) — primary
1 default no-op default no-op blank / transient
2 0x08013980 0x08007B10 VFO/standby-key screen (MENU key → id 10)
3 0x08013A44 0x0800FE3C freq-input / dial (icon 0x0802505C)
4 0x08014978 0x0802074C screen 4 (icon 0x08025115)
5 0x08013940 0x08007784 numeric entry A (hex 0–F)
6 0x080139EC 0x08007CD0 numeric entry B (dec 0–9)
7 0x08013804 0x08007E680x08017F60 alt/dual home (variant of 0)
8 0x08013B6C via 0x0800F930 list/scroll screen
9 0x08013F7C no-op status/info screen
10 (0xA) 0x080142C0 0x08017294 MENU system
11 (0xB) 0x08014104 0x0801D3B0 SMS / text editor

Canonical transition (Home→Menu, in id-2 handler): bl 0x0801AD9C (build top-level list) → movs r0,#0xA; strb r0,[=0x200008B3].

3.4 Contradiction resolved — there are TWO dispatch systems; only one is the live UI

re/input.md documents a second router, screen_dispatch @0x08018DF4, keyed on ctx[+1] @0x20002121 (0..5), reached from ui_input_service @0x08005E54. Verified xrefs settle which is authoritative:

  • ui_tick_normal @0x080207DC is called from the superloop (0x080213B2) and calls ui_process_key @0x0801E6EC at 0x08020816; ui_process_key itself calls keypad_process @0x08005A14. This g_screen/0x200008B3 path (12 screens) is the LIVE normal-mode UI.
  • ui_input_service @0x08005E54 has exactly one caller, 0x08010D08, which sits behind a blocking delay_ms(0xBB8) — a special sub-mode / config context, not the normal tick. Its router screen_dispatch @0x08018DF4 (also called from 0x0800A8D8, 0x080172CE inside menu code) drives a smaller 5-entry secondary state machine used by that sub-mode and certain menu-internal editors.

Conclusion for the rewrite: hook the primary path (0x080207E6 / 0x08020816, g_screen @0x200008B3). The secondary screen_dispatch/ctx @0x20002121 machine is real but subordinate; if a screen you replace routes into it, override that screen's handler too. Both use the same key struct 0x20000B57 (byte +2 = keycode, +3 = keystate), so key reads are uniform.


All addresses are absolute Thumb vaddrs; when taking a function pointer, set bit0. AAPCS: r0..r3 = args, extra args on stack.

4.1 Display (reuse verbatim — the graphics toolkit)

Panel: 128×64 mono, 8 pages × 128 cols, 1 bpp, no framebuffer (immediate). "Row" = 2 pages (16 px). mode: 0 normal / 1 inverse (selection highlight) / 2 outline.

vaddr name C signature notes
0x08008A50 draw_string void draw_string(u8 page, u8 x, const char *s, u16 len, u8 mode /*[sp+0x28]*/) primary text API. ASCII (+7px) & GBK/CJK/Cyrillic (lead≥0x80, +14px) auto-routed; wraps X~121.
0x08007FB8 ascii_blit void(u8 page,u8 x,u8 ch,u8 mode) one 7×16 ASCII glyph; SPI font 0x19C000+(ch-0x20)*14.
0x08008454 gbk_blit void(u8 page,u8 x,u16 gbk,u8 mode) one 14×16 double-byte glyph; SPI font 0x19E000.
0x08008530 gbk_char_at void(u8 page,u8 x,u16 gbk) standalone wide-char draw.
0x080089AC small_char void(u8 page,u8 x,char ch) compact 5×8 font (1 page).
0x08008100 big_char void(u8 page,u8 x,u8 ch,u8 mode) large freq digits (33 B/glyph).
0x08008B90 draw_number_row void(u8 page,u8 x,const u8*digits,u16 n,u8 mode) big-digit row, 12px stride (freq).
0x08008BC6 draw_str_spaced void(u8 page,u8 x,const u8*s,u16 n,u8 mode) small-font row, 6px stride.
0x08008D24 blit_cols void(u8 page,u8 count,const u8*cols,u8 src) 1-page raw blit; src=0 ⇒ clear (0x00).
0x08008D62 blit_rect void(u8 page,u8 x,u8 pages,u8 width,const u8*bmp /*[sp]*/) multi-page bitmap/icon blit (use to flush an SRAM shadow).
0x08008DAA draw_box void(u8 page_origin) rounded-rect popup/menu border.
0x08008CF0 draw_hline_seg void(u8 col,bool on) 2-px separator/underline.
0x08008224 draw_frame_corners void(u8 x,u8 page) selection frame corners.
0x0800870C / 0x08008874 draw_marker5 / draw_icon_batt void(u8 page,bool on) A/B arrow (col 123) / battery marker.
0x08008A04 draw_icon_signal void(u8 page,bool on) signal icon (col 21).
0x08014A7C lcd_set_pos void(u8 page,u16 col) cursor; col_cmd = 0xB7 x.
0x08014B28 lcd_write_col void(u8 column_bits) stream 8 vertical px (LSB=top), auto-advance.
0x08014C34 lcd_set_brightness void(u8 level) level 0..4 → PWM duty {0,5,0x1E,0x64,0xFF}.
0x08021828 spi_flash_read void(void*dst,u32 addr,u32 len) font/codeplug read (opcode 0x03) — reads only.
0x08007946 delay_ms void(u32 ms) busy delay.

Clear full screen: loop blit_cols(pg,128,0,0) for pg=0..7. Selection highlight: draw_string(..., mode=1).

4.2 Input (reuse — keypad/PTT)

Polled GPIO 4×4 matrix (not ADC ladder, no rotary encoder). PTT = GPIOA pin12. Side keys = codes 0x11/0x12.

vaddr name C signature notes
0x0801130C keypad_decode u8(void) raw matrix word 0x20000B7C → key code; 0xFF=none. No debounce.
0x08005A14 keypad_process void(void) scan+debounce+long/repeat; fills key struct KeyEv @0x20000B57. Called by ui_process_key.
0x08012C24 key_event_clear void(void) ack/consume pending key (sets +2 to 0xFF).
0x0801B294 keypad_scan_col void(int col) scan one column into 0x20000B7C.
0x0801B398 keypad_task void(void) column-drive state machine (call each ~1ms if you own the loop).
0x08021316 gpio_read_pin int(void*port,u32 mask) 1 if all masked IDR bits set.
0x08021C6E gpio_write_pin void(void*port,u32 mask,int state) ODR set/clear.

Key struct KeyEv @0x20000B57: +2 = delivered keycode (0xFF=none), +3 = keystate (1=press/short/long, 2=repeat), +5 = u16 hold-ticks (long/repeat threshold 0x2BC=700), +0x1E (0x20000B75) = PTT flag. Stock consumes by writing 0xFF to +2 after dispatch (0x0801E806).

Key-code enum: 0x00..0x09=digits 0–9, 0x0B=MENU/M, 0x0C=UP, 0x0D=DOWN, 0x0E=*, 0x0F=# (also menu-enter sentinel), 0x10=EXIT, 0x11=SIDE1, 0x12=SIDE2, 0xFF=none. (Non-digit label assignment is medium-high; confirm silk-screen on-device.)

4.3 Radio / RF (reuse — FM100B ATC layer). There is NO MCU-side RF chip; all RF/DMR lives in the FM100B, driven over USART3.

vaddr name C signature notes
0x0801AE9C radio_apply_channel void(chan_cfg *cfg) ★ composite "tune the radio": pushes freq/mode/BW/power/CC/SQ/gains/ID/CTCSS to FM100B in one shot. Call this after populating cfg.
0x0800720C atc_channel_set void(chan_cfg *cfg) (msg 0x82) ★ RX+TX freq + mode + bandwidth. Freq = codeplug 10 Hz units ×10 → Hz BE. cfg[+5]=RXfreq, [+9]=TXfreq, [+1]hi-nibble=modulation, [+0]bit1=narrow.
0x08007E78 ptt_tx_start void(u8 mode) ★ PTT on / start TX. mode 0=DMR, 1/2/3=analog/private/allcall. Sets band GPIO (GPIOA pin10).
0x08006E6C atc_call_process void(u8 a,u8 type,u32 id,u8 r3) (msg 0x06) start call / key DMR TX (type 1=Priv,2=Grp,4=All).
0x080071E2 atc_ch_enable void(u8 rx,u8 tx) (msg 0x62) channel RX/TX enable.
0x080074FA atc_set_radio_id void(u32 dmr_id) (msg 0x2A) set our DMR ID.
0x080075F4 atc_set_dig_squelch void(u8) (msg 0x4D) DMR squelch.
0x08007548/0x08007598 atc_set_color_code void(u8) (msg 0x0C) color code / off.
0x0800736C atc_set_mute_code void(u16) (msg 0x81) analog DCS/mute value.
0x08007404 atc_set_rxgroup / read_group_list void(u8 idx) (msg 0x84) RX-group/CTCSS upload; also reads group list 0xC6000+idx*80.
0x08007530/0x0800760A atc_set_call_mic_gain / spk_vol void(u8) (msg 0x0B / 0x02) DMR mic gain / spk vol.
0x08006C4C atc_set_denoise (a.k.a. dmr_set_radio_id in dmr.md) void(u8 tx,u8 rx) (msg 0x49) analog denoise / ID set — see §note.
0x0801B044 atc_send void(u8 id,u8 a1,u8 a2,u8 a3,u32 to) core no-payload sender; blocks on confirm.
0x0801B0C4 atc_send_pl void(u8 id,u8 a1,u8 a2,u8 a3,u8*pl,u16 len,u32 to) core payload sender; blocks on confirm.
0x0801094C battery_read void(void)0x200008B0 (0.1 V) ADC1 battery. (0x08010960 returns the value.)
0x08020B4C adc_sw_start void(u32 port,u8 en) ADC software start.

RAM boundary object main_settings mirror: re/radio.md uses 0x200029BB, re/codeplug.md uses 0x20002014. These are two named handles into the settings working area (offsets = rt4d_codeplug.RadioSettings); the RF apply reads settings fields from this mirror. Treat 0x20002014 as the canonical 4 KB working copy of SPI 0x002000, and 0x200029BB as a settings sub-region pointer used by the RF path; when in doubt, read/modify via the codeplug save wrappers (§4.5) so the on-flash format stays correct.

msg 0x49 name conflict: re/radio.md calls 0x08006C4C atc_set_denoise(tx,rx); re/dmr.md calls it dmr_set_radio_id(idHi,idLo). Both agree it's a 4-byte payload cmd 0x49 sender; the semantics are unresolved (LOW confidence). For an ID set, prefer the dedicated atc_set_radio_id @0x080074FA (msg 0x2A). Verify 0x49 on-target before relying on either name.

4.4 DMR (reuse — FM100B protocol over USART3, 0x68…0x10 framing; independent of CPS)

vaddr name C signature notes
0x08003050 poll_serial void(void) pump: run FM100B RX parse + CPS framer + RX drain once. Call in any wait loop.
0x08018CB0 fm100b_rx_parse int(void) consume one framed FM100B message, verify checksum, dispatch; returns 1 if consumed.
0x08006348 fm100b_on_frame void(u8*frame) master *Cnf/*Ind dispatch (sets resp[cmd], jump-tables Inds).
0x08006CFC dmr_call_start_from_contact void(u8 dummy,u16 contact_idx) originate call to stored contact (maps type, latches curcall).
0x08006FD8 dmr_call_resend void(void) re-send current-call setup (PTT continue).
0x0800736C dmr_sms_send void(u16 target) send SMS (msg 0x82 header + 0x81 payload).
0x08006D00 dmr_contact_read void(u8 dummy,u16 idx,out u8 rec[21]) read 21-B contact record idx*27 + 0x5E000 (name/ID/type).
0x080074FA atc_set_radio_id void(u32 dmr_id) (msg 0x2A) set own DMR ID (canonical).
0x0801A38C nvic_system_reset noreturn void(void) reboot (remote-kill enforcement).

Incoming-call state (read to render RX overlay): 0x20007DC2+0 type(0=Grp,1=Priv,2=All), +1 u32 dest/TG, +5 u32 caller ID. Status bytes 0x20000C3C/3D. Resolve caller name by ID via find_contact_by_id @0x08005810 (fallback: decimal ID). resp[cmd] array @0x20007476 (0xFF=pending). Convert on-wire BE IDs with be32_to_u32 @0x080112B8.

4.5 Codeplug / Settings (reuse — format-safe read/save; DO NOT re-implement)

Low-level SPI (reuse; addressing/opcodes baked in):

vaddr name C signature
0x08021828 spi_flash_read void(void*dst,u32 addr,u32 len) (opcode 0x03)
0x08021924 spi_flash_erase4k void(u32 sector_idx) (opcode 0x20)
0x08021A70 spi_flash_program void(u32 addr,const void*src,u32 len) (page-split)
0x080217B8 spi_page_program void(u32 addr,const void*src,u16 len) (opcode 0x02, ≤256 B)
0x080109DE checksum u8(const void*buf,u32 len) (8-bit sum, seed 0)
0x08010540 / 0x0801058C flash_write_guard_enter / _exit void(void) (bracket erase/program batches)

Per-record read/save wrappers (call these — they keep format + dual-bank correct):

vaddr name C signature commits/reads
0x08004F20/0x080055A0 read_channel void(u16 idx) spi_flash_read(&liveChan[band], 0x4000+idx*48, 48); dest 0x20002DEA + band*48.
0x08005810 find_contact_by_id bool(u32 id,u8 type,char*out_name16) scan contacts 0x5E000 (27-B stride).
0x08007404 read_group_list void(u16 idx) 0xC6000 + idx*80; resolves members.
0x08009B90 read_addressbook_contact void(uint slot, out) 0x126000 + slot*32 (32-B).
0x08009480 read_message void(…) 200 B text → 0x20006F86.
0x08004CB0 settings_save void(void) commit main_settings: guard → stage 0x20000x20002EBE → erase sector 1 → program 4 KB → guard-exit.
0x080061E0 contacts_compact void(uint idx) format-safe contact delete.
0x08004AB0 codeplug_backup_to_shadow void(void) full "Backing up…" region backup.

5. The main / standby screen — data sources + draw + replacement

Screen id 0, draw handler home_draw @0x08013BD8, input handler 0x08017F60. (Alt/dual variant = id 7 → 0x08013804.)

Render pipeline (data-driven, two-stage):

SPI channels (0x004000, 48B)  → cached into  g_chcache @0x20002DEA  (48B stride, [area])
        │
        ▼  format_area_display(u8 area,u8 hi,u8 shift)  @0x08011414
        │     reads cache freq/tones/mode/name → formats ASCII into g_disp
        ▼
g_disp @0x200009C3  (display struct; Area A +0, Area B +0x43, stride 0x43)
        │     + sets element dirty flags in g_dirty @0x200024EB
        ▼  home_draw @0x08013BD8   (every UI tick; paints element k iff g_dirty[k]!=0)
        ▼  draw_string / draw_number_row / draw_str_spaced / draw_marker5  → GDDRAM

Key g_disp @0x200009C3 fields: +0x00 top name/tag line; +0x15 "CH-nnn"/"A-"/"D-" prefix; +0x1C status flag ("HD"); +0x1E element mode (0 blank/1 freq/2 name); +0x20 main Area-A string ("438.80000"/name/"CH MODE"/"VFO MODE"); +0x32 Area-B string; +0x42 active area; +0x63 A/B arrow marker. Companion g_disp2 @0x20000A28 holds the "ANA"/"DMR" tag. Channel cache fields (0x20002DEA+area*0x30): [+0]>>6=digital, [+5]=RXfreq (MHz×100000), [+9]=TXfreq, [+0xD]&0xFFF=RX tone, [+0x20]=16-B name. DMR RX overlay ctx g_call @0x2000A6C5 (+1 type, +2 ID, +0x38 name).

Helpers for formatting your own home screen: num_to_ascii(val,ndigits) @0x08018530 (→ scratch 0x200024D0), str_insert_char(buf,ch,pos,len) @0x080135A8 (splice the .), memcpy_off(dst,src,dstoff,len) @0x080062EC.

Two replacement strategies:

  • A (least work): keep calling format_area_display(area,hi,shift) @0x08011414 (does codeplug→ASCII math), then read the ready strings from g_disp and paint them in your own layout.
  • B (max control): ignore g_disp; read the channel cache 0x20002DEA + g_call directly, format with the helpers, paint with §4.1 primitives.

Both only read RAM caches and call display/ADC primitives — the codeplug format and CPS protocol are untouched.


6. DO-NOT-CHANGE list vs REUSE list — the compatibility contract

DO NOT CHANGE (format & protocol — CPS-visible)

  1. SPI codeplug region bases & strides: calibration 0x000000 (4 KB, per-unit, never write); settings 0x002000 bank0 + 0x003000 shadow; channels 0x004000/48; zones 0x01C000/48; contacts 0x05C000(read 0x5E000)/27; group-lists runtime 0x0C6000/80; enc-key-names 0x0D0000/48; msgs 0x094000; fm 0x0D6000; dtmf-names 0x0C7000; addressbook 0x126000/32; fonts 0x19C000/0x19E000; unicode index 0x3F0000.
  2. Record field layouts: channel freq = u32 LE MHz×100000 @ +0x05; contact type@+0 / id@+1 LE; 16-B 0xFF-padded names; enums.
  3. Settings dual-bank + 0xABCD magic @ offset 0x0C (bank0 0x2000 / shadow 0x3000). Keep the magic value and offset.
  4. USART6 CPS protocol: ISR 0x0802061C, framer 0x0801F864, top dispatcher 0x08019790, region R/W handler 0x080188D4; buffers 0x20002EBE / 0x200092EF / 0x20000C5C..64; opcodes 0x34(/0x10/0x54/0x58/0xEE), 0x52 (read 1 KB), 0x40+0x90..0xA5 region writes, 0xA4 addressbook; 8-bit-sum-seed-0 checksum.
  5. Superloop CPS branch: the else-branch 0x080213B8..0x0802140A (0x08019A7C, 0x0801F84C, 0x0801F540) and the two unconditional serial calls serial_poll 0x0801F854 + housekeeping 0x0801FE50. Keep "PC Programming" mode reachable.

REUSE (call these; don't re-implement)

  • Display §4.1, Input §4.2, Radio §4.3, DMR §4.4, Codeplug §4.5 tables above.
  • Live RAM structs: settings working copy 0x20002014, VFO/band 0x20002DBB (+1=active band), live channel cache 0x20002DEA(+band*48), incoming DMR call 0x20007DC2, DMR RX overlay 0x2000A6C5, key struct 0x20000B57.

Contract: if the rewritten UI (a) mutates only the RAM working structs and commits via the §4.5 save wrappers, and (b) leaves the §6.4/6.5 serial path byte-identical, then the on-SPI bytes remain exactly what stock produces and the stock Radtel CPS round-trips unchanged.


7.1 Custom UI layer, injected into free flash

Place a custom UI layer (screen router + custom screens + a small shadow-framebuffer if desired) in free app flash 0x08029000+ (avoid the russification cave/font at 0x08028860/0x08028900). It hooks the main-loop screen/key dispatch and calls the stock SDK (§4).

      superloop 0x0802136C
        └─ ui_tick_normal 0x080207DC
             ├─ 0x080207E6  bl ►  my_draw_router   (was bl 0x0801E1BC)
             └─ 0x08020816  bl ►  my_key_router    (was bl 0x0801E6EC)

my_draw_router(): render current custom screen via draw_string/blit_cols/... (§4.1).
                  For not-yet-migrated screens, tail-call stock 0x0801E1BC.
my_key_router():  call keypad_process 0x08005A14 (or read KeyEv 0x20000B57 directly),
                  dispatch to custom screen; consume by writing 0xFF to 0x20000B59.
                  For not-yet-migrated screens, tail-call stock 0x0801E6EC.

7.2 The patch (2 instructions)

Each site is a Thumb BL (4 bytes). Recompute the BL immediate for the new target:

site stock new
0x080207E6 bl 0x0801E1BC bl my_draw_router
0x08020816 bl 0x0801E6EC bl my_key_router

Encode BL (T1) for target T from PC P=site+4: off=(TP); S=off>>24&1; imm10=(off>>12)&0x3FF; imm11=(off>>1)&0x7FF; J1=~(off>>23)&1 ^ ... — use a small assembler/capstone-keystone or the standard bl encoder; verify the round-trip disassembles to the intended target before flashing. (Both sites already contain a BL, so only the 4-byte immediate changes.)

7.3 Phased plan

  1. Phase 0 — scaffolding & recovery. Build the injected blob; add my_draw_router/my_key_router that tail-call the stock dispatchers unchanged. Flash; confirm the radio behaves identically (proves the hook + relocation are correct, zero behavior change). Keep the stock .bin for recovery.
  2. Phase 1 — replace the standby screen (id 0). In my_draw_router, if (g_screen==0) my_home(); else stock_draw(); (Strategy A from §5). In my_key_router, own id-0 keys (channel up/down via read_channel + radio_apply_channel, MENU→set g_screen=10, PTT via ptt_tx_start). Everything else stock.
  3. Phase 2 — replace the menu (id 10). Own g_screen==10 draw+key; drive a custom menu model; read/write settings via 0x20002014 + settings_save @0x08004CB0. Reuse stock menu-open helper 0x0801AD9C only if convenient.
  4. Phase 3 — per-screen migration. Replace ids 2/3/4/5/6/8/9/11 one at a time; each stays behind an if (g_screen==k) guard, falling back to stock for the rest. Migrate the secondary screen_dispatch sub-mode (§3.4) only if a replaced screen enters it.
  5. Phase 4 — full ownership (optional). Once all screens are custom, drop the fallbacks; optionally repoint the side-key handler 0x0801E050 and line-buffer builder 0x0801E3F8 too.

7.4 Keep it flashable / recoverable

  • The custom layer lives above the stock image; the two 4-byte patches are the only edits to stock code — trivially revertible.
  • Do not move app_main, the vector table, or the CPS branch. Keep PC-Programming mode reachable so a bad UI can still be re-flashed by the stock CPS (or the bootloader 0x39/0x57 path) — this is the recovery guarantee.
  • Because the codeplug format is untouched, a recovery re-flash of stock firmware finds a valid codeplug and boots normally.
  • Test each phase against the stock CPS (read-back + write) to confirm the on-flash format is still byte-identical.

8. Open questions / lowest-confidence items (resolve on-target)

  1. msg 0x49 semantics (0x08006C4C): denoise vs radio-ID set — the two subsystem docs disagree. Prefer atc_set_radio_id @0x080074FA (msg 0x2A) for ID; trace 0x49's caller (channel-settings menu) on-device. (LOW)
  2. main_settings handle 0x20002014 vs 0x200029BB — confirm which is the 4 KB working copy vs a sub-pointer, and that settings_save stages the right one before commit. (MEDIUM)
  3. Non-digit key labels 0x0B/0x0C/0x0D/0x10 (MENU/UP/DOWN/EXIT) and side-key combo origin (0x11/0x12) — confirm silk-screen mapping on hardware. (MEDIUM-HIGH)
  4. Secondary screen_dispatch @0x08018DF4 (5-entry, ctx @0x20002121) reachability — enumerate exactly which stock screens/sub-modes route into it so migration covers them. (MEDIUM)
  5. Group-lists 0xC6000 vs CPS 0x07C000, enc-key-names 0xD0000 vs 0x082000, zones 48-B vs 512-B — firmware and constants.py disagree; round-trip with stock CPS before writing these regions; reuse firmware wrappers to stay stock-correct. (MEDIUM — codeplug boundary)
  6. DMR TG/Color-Code standby overlay render sequence (g_call @0x2000A6C5, near 0x08006754) — only partially traced; confirm when redesigning the RX overlay. (MEDIUM)
  7. Free-flash extent & alignment — verify 0x08029000+ is erased/available on the actual part and pick a flash-page-aligned base for the injected blob. (LOW — mechanical)