- Полный 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>
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 at0x080207E6) —tbb-jumps on the single screen-state byteg_screen @0x200008B3(0..11) to the current screen's incremental draw routine. - Screen input:
ui_process_key @0x0801E6EC(called at0x08020816) — first callskeypad_process @0x08005A14(scan+debounce), thentbb-jumps on the sameg_screento the current screen's key handlerhandler(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 scheduler — 0x080207E6 (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 |
0x08007E68→0x08017F60 |
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 @0x080207DCis called from the superloop (0x080213B2) and callsui_process_key @0x0801E6ECat0x08020816;ui_process_keyitself callskeypad_process @0x08005A14. Thisg_screen/0x200008B3path (12 screens) is the LIVE normal-mode UI.ui_input_service @0x08005E54has exactly one caller,0x08010D08, which sits behind a blockingdelay_ms(0xBB8)— a special sub-mode / config context, not the normal tick. Its routerscreen_dispatch @0x08018DF4(also called from0x0800A8D8,0x080172CEinside 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.
4. Callable API reference — the "SDK" a new UI links against
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 0x2000→0x20002EBE → 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 fromg_dispand paint them in your own layout. - B (max control): ignore
g_disp; read the channel cache0x20002DEA+g_calldirectly, 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)
- SPI codeplug region bases & strides: calibration
0x000000(4 KB, per-unit, never write); settings0x002000bank0 +0x003000shadow; channels0x004000/48; zones0x01C000/48; contacts0x05C000(read0x5E000)/27; group-lists runtime0x0C6000/80; enc-key-names0x0D0000/48; msgs0x094000; fm0x0D6000; dtmf-names0x0C7000; addressbook0x126000/32; fonts0x19C000/0x19E000; unicode index0x3F0000. - Record field layouts: channel freq = u32 LE
MHz×100000@+0x05; contact type@+0 / id@+1 LE; 16-B0xFF-padded names; enums. - Settings dual-bank +
0xABCDmagic @ offset0x0C(bank00x2000/ shadow0x3000). Keep the magic value and offset. - USART6 CPS protocol: ISR
0x0802061C, framer0x0801F864, top dispatcher0x08019790, region R/W handler0x080188D4; buffers0x20002EBE/0x200092EF/0x20000C5C..64; opcodes0x34(/0x10/0x54/0x58/0xEE),0x52(read 1 KB),0x40+0x90..0xA5region writes,0xA4addressbook; 8-bit-sum-seed-0 checksum. - Superloop CPS branch: the
else-branch0x080213B8..0x0802140A(0x08019A7C,0x0801F84C,0x0801F540) and the two unconditional serial callsserial_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/band0x20002DBB(+1=active band), live channel cache0x20002DEA(+band*48), incoming DMR call0x20007DC2, DMR RX overlay0x2000A6C5, key struct0x20000B57.
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. Recommended UI-rewrite architecture
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=(T−P); 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
- Phase 0 — scaffolding & recovery. Build the injected blob; add
my_draw_router/my_key_routerthat tail-call the stock dispatchers unchanged. Flash; confirm the radio behaves identically (proves the hook + relocation are correct, zero behavior change). Keep the stock.binfor recovery. - 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). Inmy_key_router, own id-0 keys (channel up/down viaread_channel+radio_apply_channel, MENU→setg_screen=10, PTT viaptt_tx_start). Everything else stock. - Phase 2 — replace the menu (id 10). Own
g_screen==10draw+key; drive a custom menu model; read/write settings via0x20002014+settings_save @0x08004CB0. Reuse stock menu-open helper0x0801AD9Conly if convenient. - 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 secondaryscreen_dispatchsub-mode (§3.4) only if a replaced screen enters it. - Phase 4 — full ownership (optional). Once all screens are custom, drop the fallbacks; optionally repoint the side-key handler
0x0801E050and line-buffer builder0x0801E3F8too.
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 bootloader0x39/0x57path) — 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)
- msg 0x49 semantics (
0x08006C4C): denoise vs radio-ID set — the two subsystem docs disagree. Preferatc_set_radio_id @0x080074FA(msg 0x2A) for ID; trace 0x49's caller (channel-settings menu) on-device. (LOW) main_settingshandle0x20002014vs0x200029BB— confirm which is the 4 KB working copy vs a sub-pointer, and thatsettings_savestages the right one before commit. (MEDIUM)- 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) - Secondary
screen_dispatch @0x08018DF4(5-entry,ctx @0x20002121) reachability — enumerate exactly which stock screens/sub-modes route into it so migration covers them. (MEDIUM) - Group-lists
0xC6000vs CPS0x07C000, enc-key-names0xD0000vs0x082000, zones 48-B vs 512-B — firmware andconstants.pydisagree; round-trip with stock CPS before writing these regions; reuse firmware wrappers to stay stock-correct. (MEDIUM — codeplug boundary) - DMR TG/Color-Code standby overlay render sequence (
g_call @0x2000A6C5, near0x08006754) — only partially traced; confirm when redesigning the RX overlay. (MEDIUM) - 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)