- Полный 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>
20 KiB
RT-4D Firmware — Main Loop, UI State Machine & UI-Rewrite Hook Points
Key: main-loop · Target: stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin (ARM Cortex-M4F Thumb, load/vaddr base 0x08002800).
All disassembly via capstone CS_ARCH_ARM + CS_MODE_THUMB.
This is the hook-point document for a full UI rewrite. It nails down (1) the boot→superloop path, (2) the single UI screen-state variable and the two mirror-image dispatch tables (draw + input) keyed on it, (3) how screens redraw and transition, and (4) the exact vaddrs and strategy to redirect rendering + navigation into custom code while keeping all lower-level radio/codeplug/serial APIs intact.
0. TL;DR — the hook
- Screen-state variable:
g_screen = *(uint8_t*)0x200008B3(0..11). A shadow*(uint8_t*)0x200008B2holds the previous screen.0x200008B3is referenced by 55 literal-pool words across the image — it is the central UI selector. - Draw dispatcher:
ui_draw_dispatch @0x0801E1BC—tbb-jumps ong_screen(12 entries) to the current screen's incremental redraw routine. Called every UI tick from the scheduler at0x080207E6. - Input dispatcher:
ui_process_key @0x0801E6EC— pulls a keycode/keystate from the key struct at0x20000B57, thentbb-jumps ong_screen(12 entries, table@0x0801E780) to the current screen's key handlerhandler(u8 keycode, u8 keystate). Called every UI tick from the scheduler at0x08020816. - Hook strategy (recommended): patch the two
blcall sites in the scheduler —0x080207E6(bl ui_draw_dispatch) and0x08020816(bl ui_process_key) — to call your own router. Your router owns rendering + navigation and calls the stock lower-level APIs (draw_string, keys, RF, DMR, codeplug). This is a 2-instruction redirect and leaves the serial/CPS path and codeplug format completely untouched. See §5.
1. Boot → application superloop
1.1 Reset / CRT startup (library glue, not app logic)
Reset 0x08002AC0: SystemInit(0x0801DA2D) ; then bx __main(0x080029E1)
__main 0x080029E0: bl 0x080029E8 (__scatterload / RW+ZI init, RLE decompressor @0x08002A1E)
bl 0x08002AA0 (__rt_entry)
__rt_entry 0x08002AA0: sequence of ARM C-lib inits, then:
bl 0x0802136C <-- the real application main / superloop
The 0x08002AA0..0x08002D18 cluster is ARM compiler runtime (heap/stack setup, bkpt 0xAB semihosting stubs, the ADC ISR tail). The application proper is app_main @0x0802136C.
1.2 app_main @0x0802136C — the top-level superloop ★ HARD ENTRY POINT
0x0802136C movs r4,#0
0x0802136E bl 0x0801DAD0 ; low-level HW/clock/periph bring-up
0x08021372 ldr r0,=0x08002800 ; ldr r1,=0xE000ED08 ; str r0,[r1] ; VTOR = app vector base
0x08021378 bl 0x08021024 ; init B
0x0802137C movs r0,#0xC8 ; bl 0x08007946 ; init C (display/boot?)
0x08021382 bl 0x080127D4 ; init D
0x08021386 bl 0x08003060 ; init E (codeplug/settings load)
0x0802138A b 0x08021414 ; -> jump to loop top
--- LOOP TOP ---
0x0802138C if (*(u8*)0x20000C54) bl 0x0801A38C ; reboot flag -> NVIC_SystemReset
0x08021396 modeA = *(u8*)0x20000B66 ; serial/session mode gate
modeB = *(u8*)0x20000C12
spiMode = *(u8*)0x20000C57
if (modeA==0 && modeB!=2 && modeB!=4 && spiMode==0)
0x080213B2 bl 0x080207DC ; ★ NORMAL UI TICK (radio running)
else ... ; PC-programming / SPI-write session paths:
0x080213C2 modeB==2 -> bl 0x08019A7C ; CPS mode handler
0x080213E2 modeB==4 -> bl 0x0801F84C ; serial framer
0x080213EE modeA!=0 -> bl 0x0801F84C ; bl 0x0801F540 ; drain FM100B rx (0x200082EF, 0x08006CB8)
0x0802140C bl 0x0801F854 ; serial poll (every iteration)
0x08021410 bl 0x0801FE50 ; housekeeping (every iteration)
0x08021414 b 0x0802138C ; loop
Interpretation. The superloop first honours a reboot request, then branches on serial-session state: when the radio is not in a PC-programming / SPI-write session (0x20000B66==0, 0x20000C12∉{2,4}, 0x20000C57==0), it runs the normal UI tick 0x080207DC. Otherwise it services the CPS/serial paths. 0x0801F854 (serial poll) and 0x0801FE50 (housekeeping) run unconditionally every pass.
Boundary note (respect the CPS/codeplug contract). The serial session vars
0x20000B66 / 0x20000C12 / 0x20000C57 / 0x20000C54and the handlers0x08019A7C / 0x0801F84C / 0x0801F540 / 0x0801F854are the CPS protocol + SPI region-write engine (documented in RT-4D_RE_Report §4). A UI rewrite must leave this entireelsebranch and the two unconditional serial calls exactly as-is — that is the serial/CPS boundary. Only replace what happens inside the normal UI tick0x080207DC.
| vaddr | name | signature | notes |
|---|---|---|---|
| 0x0802136C | app_main |
void app_main(void) __attribute__((noreturn)) |
The application superloop. Sets VTOR, runs inits, then loops. Do not relocate — reset path branches here. |
| 0x0801DAD0 | hw_init |
void hw_init(void) |
clock/peripheral bring-up (called first) |
| 0x08003060 | codeplug_load_init |
void(void) |
last init; loads settings/channels from SPI (candidate) |
| 0x0801A38C | nvic_system_reset |
void(void) noreturn |
writes AIRCR 0x05FA0004 (reboot). Gated by 0x20000C54. |
| 0x080207DC | ui_tick_normal |
void(void) |
the normal-mode UI scheduler (see §2) |
| 0x0801F854 | serial_poll |
void(void) |
runs every loop; part of CPS path — keep |
| 0x0801FE50 | housekeeping |
void(void) |
runs every loop (battery/timers) |
2. ui_tick_normal @0x080207DC — the cooperative UI scheduler
This is not the state machine itself; it is a fixed list of periodic subsystems, several gated by down-counters so they run at different rates. The screen draw + key dispatch are two of its calls.
0x080207DE bl 0x0801F84C ; serial framer (shared)
0x080207E2 bl 0x0801F9C0 ; ? (reads 0x20000C8A menu-key state, 0x200029BB struct)
0x080207E6 bl 0x0801E1BC ; ★ ui_draw_dispatch (SCREEN DRAW — hook here)
0x080207EA bl 0x0801E050 ; ui_sidekey_dispatch (side/long-press hotkeys — see §3.3)
0x080207EE bl 0x0801E3F8 ; build display line buffer (reads 0x200008B3)
0x080207F2 bl 0x0801FAC8 ; ...
0x080207F6 bl 0x0801F504
0x080207FA bl 0x0801FDA4
0x080207FE bl 0x0801FDDC
0x08020802 bl 0x0801FE0C
0x08020806 bl 0x0801DFD0
--- period-gated tasks: counter at 0x20000BF1.. reloads to a period when it hits 0 ---
0x0802080A if(--tick@0x20000BF1==0){reload 1; bl 0x0801E6EC(key?) ...} see note
0x08020816 bl 0x0801E6EC ; ★ ui_process_key (SCREEN INPUT — hook here)
0x0802081A bl 0x08020288
... 0x20000BF2 (reload 4), 0x20000BF3 (reload 0xA), 0x20000BF5 (0xC8),
0x20000BF6 (0x1F4), 0x20000BF8 (0x3E8): slower periodic tasks (RSSI, battery, scan, etc.)
0x080208DC return
Concretely, the two dispatch calls you care about are both unconditional every UI pass:
0x080207E6 bl 0x0801E1BC→ screen draw dispatch0x08020816 bl 0x0801E6EC→ screen key dispatch
(Down-counter reloads observed: 0x20000BF1→1, 0x20000BF2→4, 0x20000BF3→0xA, 0x20000BF5→0xC8, 0x20000BF6→0x1F4, 0x20000BF8→0x3E8 — these throttle the slower periodic tasks; the two dispatchers themselves are not throttled.)
3. The UI state machine
3.1 The state variable
g_screen = *(uint8_t*)0x200008B3 ; current screen id, 0..11 (0x0B)
g_screen_prev = *(uint8_t*)0x200008B2 ; previous screen (used for restore/back)
Both dispatchers guard with cmp g_screen,#0x0C ; bhs <default> before the tbb, so valid ids are 0..11.
3.2 The two mirror dispatch tables (draw + input)
Both are tbb [pc,r0] byte-offset jump tables indexed by g_screen. They are positionally parallel: index i in the draw table and index i in the input table are the same screen.
DRAW dispatcher ui_draw_dispatch @0x0801E1BC:
0x0801E1BE bl 0x080136E4 ; clear per-field dirty flags (row @0x20002512)
0x0801E1C2 r0 = *(u8*)0x200008B3
0x0801E1C6 cmp r0,#0x0C ; bhs 0x0801E228 (default/no-op)
0x0801E1CA tbb [pc,r0] ; table @0x0801E1CE
INPUT dispatcher ui_process_key @0x0801E6EC (tbb inside at 0x0801E780):
0x0801E6EE bl 0x08005A14 ; keypad scan/debounce
0x0801E6F4 if(*(u8*)0x20000B5A==0) return ; keystate (0x20000B57+3) == no-key -> bail
0x0801E6FC if(*(u8*)0x20000B59==0xFF) return ; keycode (0x20000B57+2) == none -> bail
... global lock / special-mode guards ...
0x0801E778 r0 = *(u8*)0x200008B3
0x0801E77C cmp r0,#0x0C ; bhs 0x0801E804 (default)
0x0801E780 tbb [pc,r0] ; table @0x0801E784
each case: r2=0x20000B57; r1=[r2,#3](keystate); r0=[r2,#2](keycode); bl <handler>
0x0801E806 after dispatch: *(u8*)(0x20000B57+2) = 0xFF ; consume keycode
The key event struct is at 0x20000B57: byte +2 = keycode, byte +3 = keystate/edge (1 = press/repeat). Handlers receive (r0=keycode, r1=keystate).
3.3 Screen table — id → {draw handler, input handler}
Verified by decoding both tbb tables (bytes are half-word offsets from the table base):
| id | draw handler | input handler | inferred screen | evidence |
|---|---|---|---|---|
| 0 | 0x08013BD8 |
0x08017F60 |
Home / VFO-A main (default landing) | most strb g_screen,#0 "return home" sites; 0x08013BD8 reads big status struct 0x200009C3+0x42 |
| 1 | (default no-op 0x0801E228 / input 0x0801E804) |
— | blank / transient | table byte 0x2D→no-op; input 0x40→no-op |
| 2 | 0x08013980 |
0x08007B10 |
VFO/standby key screen | 0x08007B10 handles MENU key (10) → sets g_screen=0xA (menu entry, §3.4); EXIT/side keys |
| 3 | 0x08013A44 |
0x0800FE3C |
screen w/ icon @0x0802505C (freq-input / dial) |
draws bitmap via 0x08008D62; input checks 0x20000855 |
| 4 | 0x08014978 |
0x0802074C |
screen w/ icon @0x08025115 |
bitmap blit; input digit 0xC handling |
| 5 | 0x08013940 |
0x08007784 |
numeric entry A (0–F) | input: cmp r4,#0xF; bl 0x08012EF8 (hex digit) |
| 6 | 0x080139EC |
0x08007CD0 |
numeric entry B (0–9) | input: cmp r4,#9; bl 0x08012F78 (dec digit) |
| 7 | 0x08013804 |
0x08007E68→0x08017F60 |
alt of screen 0 | input forwards to id-0 handler 0x08017F60 |
| 8 | 0x08013B6C |
0x08007D…/via 0x0801E7E4→0x0800F930 |
list/scroll screen | draw calls 0x080085D6; several handlers special-case g_screen==8 (0x0800F930) |
| 9 | 0x08013F7C |
(input tbl byte 0x33→no-op region 0x0801E7EA) |
status/info screen | draw reads 0x20000A3D+0x39, 0x20000A7B |
| 10 (0xA) | 0x080142C0 |
0x08017294 |
MENU system ★ | draw = the menu render loop (facts §Display); input = full menu key handler (largest, sub sp,#0x64) |
| 11 (0xB) | 0x08014104 |
0x0801D3B0 |
SMS / editor (text) | draw blits chars at fixed cols; input handles 0–9 (cmp r4,#9) |
Notes:
- Standby "home" screen (id 0/7) is the radio's idle/operating screen (frequency, channel, RX/TX, DMR-rx overlay). The full home rendering also runs through the period-gated tasks and
0x0801E3F8(line-buffer builder) — the dispatched draw handler0x08013BD8does the incremental field redraw. - id 1 is a genuine no-op slot (both tables route it to the shared "do nothing" tail). Treat as "transition/blank".
- All input handlers share the uniform prototype
void screen_key(uint8_t keycode, uint8_t keystate).
3.4 How transitions happen (state writes)
Screen changes are plain byte stores strb rN, [=0x200008B3]. Enumerated immediate-write sites (subset; movs rN,#imm immediately before the strb):
| new id | example write sites | meaning |
|---|---|---|
| 0 | 0x08003490, 0x080094AA, 0x0800AA70, 0x0800AE4C, 0x0800B200, 0x0801E120, 0x0801F06C, 0x0801F81E, … (many) |
return to Home / EXIT |
| 1 | 0x080094AE, 0x080095DC, 0x08009760, 0x0801E5DA, 0x0801F698, … |
enter transient/blank |
| 2 | 0x0801A2BE |
enter VFO/standby key screen |
| 3 | 0x0800B112, 0x08018344, 0x0801A2DE |
enter freq-input/dial |
| 4 | 0x0800BC58, 0x0801E15A |
enter screen 4 |
| 5 | 0x08009842 |
enter numeric-entry A |
| 7 | 0x0801A66C |
enter alt-home |
| 8 | 0x0800AF0A |
enter list/scroll |
| 9 | 0x080094D2 |
enter status/info |
| 10 (0xA) | 0x0800B92A (from Home MENU key, §below) |
enter MENU |
| 11 (0xB) | 0x0800B85C |
enter SMS/editor |
| 0x11 | 0x080094A4 |
(id 17 — value out of 0..11 range; likely a sub-mode byte, not a screen; used by a specialized editor) |
| 0x2A | 0x08009A9C |
(id 42 — same: sub-mode marker, not a screen dispatch id) |
Canonical transition example — Home → Menu (in the id-2 standby key handler 0x08007B10, MENU key path lands in 0x0800B900):
0x0800B91C bl 0x0801AD9C ; menu-open side effects (build top-level list)
0x0800B926 movs r0,#0xA
0x0800B928 ldr r1,=0x200008B3
0x0800B92A strb r0,[r1] ; g_screen = 10 (MENU)
So navigation = write the target id to 0x200008B3 (optionally saving the old value to 0x200008B2 for "back"), plus per-screen enter side-effects. A custom router replicates exactly this.
4. How a screen is (re)drawn each loop
The draw model is incremental / dirty-flag driven, not full-frame:
ui_draw_dispatch @0x0801E1BCfirst calls0x080136E4, which walks a dirty-flag row at0x20002512(loop of 8+) and force-marks fields dirty on screen entry.- It then
tbb-dispatches to the current screen's redraw routine (§3.3). Each routine reads its per-field "changed?" bytes (e.g.0x200008F5,0x20000914,0x20000A84) and only when set calls the text/glyph primitives:draw_string @0x08008A50—void draw_string(u8 y_page /*r0*/, u8 x /*r1*/, const char* s /*r2*/, u8 len /*r3*/, u8 mode /*[sp+0x28]*/); mode 0=normal,1=inverse,2=outline. (Confirmed head:mov sl,r0; mov r7,r1; mov r5,r2; mov fp,r3.)0x08008D62— bitmap/icon blitter (draw_bitmap(mode,x,y,const u8* bmp))0x080089AC,0x080085D6,0x08008798— box/line/clear helpers.
- The framebuffer is flushed to the LCD over SPI2 by the period-gated tasks; the ASCII glyph blitter
0x08007FB8and wide/GBK blitter0x08008454pull font bitmaps from SPI flash (per facts).
Consequence for a rewrite: because draw is gated behind dirty flags and a tbb on g_screen, replacing the dispatched routine (or the dispatcher call) cleanly takes over rendering for that screen without fighting the stock partial-redraw logic — as long as your code marks the whole area dirty / clears+redraws each frame itself.
5. Recommended HOOK POINTS for a custom UI router
Goal: our code owns rendering + navigation + which key does what, while calling stock lower-level APIs (draw_string 0x08008A50, keypad 0x08005A14, RF/DMR/codeplug helpers, and — untouched — the serial/CPS engine). Three options, most-preferred first.
Option A (recommended): redirect the two scheduler dispatch calls
Patch the two bl instructions in ui_tick_normal:
| patch site | stock instr | change to |
|---|---|---|
0x080207E6 |
bl 0x0801E1BC (ui_draw_dispatch) |
bl my_draw_router |
0x08020816 |
bl 0x0801E6EC (ui_process_key) |
bl my_key_router |
- Your
my_key_routerreads the same key struct at0x20000B57(+2 keycode, +3 keystate) — call stock0x08005A14first if you want the stock debounce, or read raw. After handling, write0xFFto0x20000B57+2to consume, exactly as stock does at0x0801E806. - Your
my_draw_routerrenders viadraw_string @0x08008A50and the blit/clear helpers, keyed on your own screen model. You may keep or ignoreg_screen@0x200008B3. - Everything else in the superloop and scheduler is preserved, so RF, DMR RX, scan, battery, and the entire serial/CPS + SPI region-write path (
0x080213B8else-branch,0x0801F854,0x0801FE50) keep working unchanged. This is the minimal, cleanest cut: 2 instructions. - Keep
0x080207EA bl 0x0801E050(side-key/long-press handler) if you still want stock side-key semantics, or repoint it too for full control.
Option B: replace the two tbb jump tables (per-screen, incremental)
Repoint individual entries in the draw table (@0x0801E1CE, 12 bytes) and input table (@0x0801E784, 12 bytes) to your own handlers, one screen at a time. Because entries are 1-byte half-word offsets from the table base, a target must be within +0..+0x1FE of the base; to jump far, keep a stock case as a 2-instruction trampoline (b.w my_handler) inside range. This lets you migrate screens gradually while stock screens still work. More fiddly than Option A.
Option C: own the screen id + provide new handlers
Keep the dispatchers, but treat 0x200008B3 as your state var and point all 12 draw/input slots at your dispatch trampolines. Effectively Option B for all 12 at once; no advantage over Option A.
Lower-level APIs to reuse (stable call targets)
| purpose | vaddr | prototype |
|---|---|---|
| draw text | 0x08008A50 |
draw_string(u8 y_page, u8 x, const char* s, u8 len, u8 mode@sp+0x28) |
| draw icon/bitmap | 0x08008D62 |
draw_bitmap(u8 mode, u8 x, u8 y, const u8* bmp) |
| ascii glyph blit | 0x08007FB8 |
(per facts; SPI font @0x19C000) |
| wide/GBK glyph blit | 0x08008454 |
(per facts; SPI font @0x19E000) |
| keypad scan/debounce | 0x08005A14 |
void keypad_scan(void) (fills 0x20000B57) |
| SPI codeplug read | 0x08021828 |
spi_read(dst, byteaddr, len) — keep format |
| menu-open helper | 0x0801AD9C |
builds top-level menu list (call if reusing stock menu data) |
| reboot | 0x0801A38C |
nvic_system_reset() |
Do NOT touch (serial/CPS + codeplug boundary — hard constraint): the superloop else-branch 0x080213B8..0x0802140A, serial_poll 0x0801F854, the framer/dispatcher 0x0801F84C / 0x08019790 / 0x080188D4, and the SPI region-write engine. These implement the stock CPS protocol and the on-flash codeplug format; leaving them byte-identical is what keeps the stock Radtel CPS working.
6. Key RAM state variables (UI)
| addr | width | name | role |
|---|---|---|---|
0x200008B3 |
u8 | g_screen |
current UI screen id (0..11) — the state machine selector |
0x200008B2 |
u8 | g_screen_prev |
previous screen (back/restore) |
0x20000B57 |
struct | g_key |
key event: +2 keycode, +3 keystate(1=press) |
0x20000C3D |
u8 | g_sidekey |
side/long-press keycode consumed by 0x0801E050 |
0x20000BF1..BF8 |
u8/u16 | scheduler down-counters | throttle slow periodic tasks in ui_tick_normal |
0x20002512 |
u8[8+] | dirty-flag row | per-field "needs redraw" flags (cleared by 0x080136E4) |
0x20000C54 |
u8 | reboot request | superloop → nvic_system_reset |
0x20000B66 / 0x20000C12 / 0x20000C57 |
u8 | serial-session mode gates | select UI vs CPS/SPI-write path (do not repurpose) |
7. Confidence
- HIGH — superloop (
0x0802136C), normal-UI scheduler (0x080207DC), the single screen-state var0x200008B3, and the two mirrortbbdispatchers (draw0x0801E1BC, input0x0801E6EC) with their 12-entry tables. All directly disassembled and cross-checked (55 xrefs to the state var; both tbb tables decoded; the Home→Menu transition traced end-to-end). - HIGH — the recommended hook (patch
blat0x080207E6and0x08020816); both call sites verified in the scheduler disassembly, and the serial/CPS boundary is cleanly separated in the superloop. - MEDIUM — the English names assigned to screen ids 3/4/5/6/8/9/11: the dispatch structure and handler vaddrs are certain, but exact screen semantics are inferred from handler behaviour (digit ranges, bitmap vs text, forwarding) rather than from a label string on each. ids 0/7 (Home), 2 (standby-key), 10 (Menu), 11 (SMS/editor) are well-anchored.