# 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*)0x200008B2` holds the previous screen. `0x200008B3` is referenced by **55** literal-pool words across the image — it is the central UI selector. - **Draw dispatcher:** `ui_draw_dispatch @0x0801E1BC` — `tbb`-jumps on `g_screen` (12 entries) to the current screen's incremental redraw routine. Called every UI tick from the scheduler at `0x080207E6`. - **Input dispatcher:** `ui_process_key @0x0801E6EC` — pulls a keycode/keystate from the key struct at `0x20000B57`, then `tbb`-jumps on `g_screen` (12 entries, table `@0x0801E780`) to the current screen's key handler `handler(u8 keycode, u8 keystate)`. Called every UI tick from the scheduler at `0x08020816`. - **Hook strategy (recommended):** patch the two `bl` call sites in the scheduler — `0x080207E6` (`bl ui_draw_dispatch`) and `0x08020816` (`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 / 0x20000C54` and the handlers `0x08019A7C / 0x0801F84C / 0x0801F540 / 0x0801F854` are the **CPS protocol + SPI region-write engine** (documented in RT-4D_RE_Report §4). A UI rewrite must leave this entire `else` branch and the two unconditional serial calls **exactly as-is** — that is the serial/CPS boundary. Only replace what happens *inside* the normal UI tick `0x080207DC`. | 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** dispatch - `0x08020816 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 ` 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 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 handler `0x08013BD8` does 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: 1. `ui_draw_dispatch @0x0801E1BC` first calls `0x080136E4`, which walks a dirty-flag row at `0x20002512` (loop of 8+) and force-marks fields dirty on screen entry. 2. 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. 3. The framebuffer is flushed to the LCD over SPI2 by the period-gated tasks; the ASCII glyph blitter `0x08007FB8` and wide/GBK blitter `0x08008454` pull 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_router` reads the same key struct at `0x20000B57` (+2 keycode, +3 keystate) — call stock `0x08005A14` first if you want the stock debounce, or read raw. After handling, write `0xFF` to `0x20000B57+2` to consume, exactly as stock does at `0x0801E806`. - Your `my_draw_router` renders via `draw_string @0x08008A50` and the blit/clear helpers, keyed on your own screen model. You may keep or ignore `g_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 (`0x080213B8` else-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 var `0x200008B3`, and the two mirror `tbb` dispatchers (draw `0x0801E1BC`, input `0x0801E6EC`) 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 `bl` at `0x080207E6` and `0x08020816`); 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.