- Полный 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>
21 KiB
RT-4D Standby / Main (Home) Screen — Render Map & Rewrite Recipe
Key: main-screen · 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 document reverse‑engineers the standby / operating (VFO or channel) home screen — the first UI redesign target. It ties into the three prior docs: geometry & draw primitives in display.md, the state machine & hook points in main-loop.md, and keys in input.md. Read those for the low‑level draw API and the superloop hook; this doc covers what the home screen paints, from which data, at which coordinates, and how to replace it.
0. TL;DR
- The home screen is screen id 0 (
g_screen @0x200008B3 == 0). Its draw handler ishome_draw @0x08013BD8(dispatched fromui_draw_dispatch @0x0801E1BC'stbbtable). An alternate home / dual‑display variant is id 7 →0x08013804. - Home render is two‑stage and data‑driven, not hard‑coded:
format_area_display(area, hi, shift) @0x08011414reads the live channel cache at0x20002DEA(48‑byte records) + the per‑area mode table at0x20002DCF, formats frequency / channel‑name / mode‑tag ASCII into the display structg_disp @0x200009C3, and sets dirty flags ing_dirty @0x200024EB.home_draw @0x08013BD8consumesg_disp+g_dirtyand calls thedisplay.mdprimitives (draw_string,draw_number_row,draw_str_spaced,draw_marker5) to paint each changed element.
- Redraw is incremental / dirty‑flag driven. An element repaints only when its byte in
g_dirty @0x200024EBis non‑zero; setting a flag (e.g. after a channel/area change, RX event, or key) triggers repaint on the next UI tick. No full‑frame clear. - The single struct you must understand is
g_disp @0x200009C3. Every on‑screen string/flag is a field of it (see §3). To replace the home screen, either (a) keepformat_area_displayand re‑lay‑out in your ownhome_draw, or (b) ignore both and render straight from the cache0x20002DEA+ radio getters. Both keep the SPI codeplug format and CPS serial protocol untouched (they only read cache RAM and call display primitives).
1. Where the home screen is dispatched from
Chain (from main-loop.md, confirmed): superloop app_main @0x0802136C → normal UI tick ui_tick_normal @0x080207DC → bl ui_draw_dispatch @0x0801E1BC at call‑site 0x080207E6. ui_draw_dispatch tbb‑jumps on g_screen @0x200008B3; index 0 → home_draw @0x08013BD8, index 7 → 0x08013804 (alt/dual home). The mirror input tbb (ui_process_key @0x0801E6EC) routes id 0 → key handler 0x08017F60.
| screen id | draw handler | content struct | dirty‑flag array | role |
|---|---|---|---|---|
| 0 | home_draw 0x08013BD8 |
g_disp 0x200009C3 |
g_dirty 0x200024EB |
primary standby (VFO/channel) home |
| 7 | 0x08013804 |
0x2000096A |
0x200009BD |
alt home / dual‑display variant |
| 2 | 0x08013980 |
0x200008D0 |
0x200008F5 |
standby‑key / list overlay |
Hook (recommended, from
main-loop.mdOption A): redirect theblat0x080207E6tomy_draw_router; render your own home there. Everything else (RF, DMR RX, scan, battery, and the entire serial/CPS + SPI region‑write path) is preserved. 2‑instruction cut.
2. The render pipeline (data → struct → pixels)
SPI codeplug channels (0x004000, 48B recs) ← format on flash, DO NOT CHANGE
│ loaded/cached by channel-load code (writes 0x20002DEA)
▼
g_chcache @0x20002DEA : live channel record cache, 48-byte (0x30) stride, [area]
g_chmode @0x20002DCF : per-area display-mode bytes (= g_chcache − 0x1B)
│
▼ format_area_display(area, hi, shift) @0x08011414 (4 call sites)
│ reads freq/tones/power/mode from cache, formats ASCII
▼
g_disp @0x200009C3 : DISPLAY STRUCT (freq/name strings, mode flags, TG, markers)
│ + sets element dirty flags in g_dirty @0x200024EB
▼ home_draw @0x08013BD8 (dispatched every UI tick for screen 0)
│ paints only elements whose g_dirty byte != 0
▼
draw_string / draw_number_row / draw_str_spaced / draw_marker5 (display.md prims)
▼ lcd_set_pos + lcd_write_col → direct-to-GDDRAM (no framebuffer)
2.1 format_area_display(u8 area, u8 hi, u8 shift) @0x08011414 — the formatter
Prototype (AAPCS): void format_area_display(u8 area /*r0*/, u8 hi /*r1→r5*/, u8 shift /*r2→r8*/).
area(r4) = which VFO/area (0 = A, 1 = B) — indexes the cache0x20002DEA + area*0x30.hi(r5) = highlight/selection state (0,1,2,3) → stored tog_disp+0x1D;2selects an alt copy path.shift(r8) = TX‑shift display mode:0=simplex/RX shown,1=+shift,2=−shift/reverse (Talkaround/Reverse). Selects which of RX(+5)/TX(+9) freq words is shown.
Reads (per 48‑byte cache record at 0x20002DEA + area*0x30):
| cache off | field | used for |
|---|---|---|
[+0] |
flags; >>6 = mode (1 ⇒ DMR/digital, else analog) |
"DMR"/"ANA" tag, "A-"/"D-" prefix |
[+5] |
u32 RX frequency (MHz×100000, FREQ_MULTIPLIER) |
main freq digits |
[+9] |
u32 TX frequency | shown when shift≠0 / reverse |
[+0xD] |
u12 (ubfx #0,#0xC) RX tone (CTCSS/DCS) |
tone field |
[+0xE]>>4 |
nibble | power/flag |
[+0xF] |
u12 TX tone | tone field |
[+0x10]>>4 |
nibble | flag |
[+0x20] |
16‑byte channel name (ASCII) | name display (mode==2) |
Also reads globals: g_power @0x20000B76 → g_disp+0x1C (the "HD"/status flag byte); g_chmode @0x20002DCF + area → g_disp+0x1E (element mode), [+2+area] → g_disp+0x1F (sub‑mode).
Writes into g_disp @0x200009C3 (see §3) and builds ASCII via helpers:
num_to_ascii(uint val, int ndigits) @0x08018530— right‑alignsndigitsdecimal ASCII into scratch0x200024D0. Used to render the frequency (freq, 8 digits) and channel number.str_insert_char(buf, ch, pos, len) @0x080135A8— insertschatpos(shifting right). Used to splice the "." decimal point into the frequency string (ch=0x2Eat pos 3) →"438.80000".memcpy_off(dst, src, dstoff, len) @0x080062EC—dst[dstoff+i]=src[i]. The workhorse for copying templates / name / formatted number intog_dispfields.
Inline ASCII templates embedded in the formatter (decoded): "CH MODE", "VFO MODE", "VFO MODE-A", "VFO MODE-B", "CH-", "A-" (analog), "D-" (digital), "ANA", "DMR". These are the literal strings the stock home screen shows in the mode/status line.
Called from 4 sites (0x080033D2, 0x08004F60, 0x08008F28, 0x0800951C) — always after a state change (area switch, channel change, mode toggle). Example call: 0x0800951C → format_area_display(area=[0x20000B5F], hi=2, shift=[0x200029BB+0x67]).
2.2 home_draw @0x08013BD8 — the painter
Consumes g_disp @0x200009C3 and per‑element dirty flags g_dirty @0x200024EB. For each element: if (g_dirty[k]) draw…. Ends pop {…pc} at 0x08013E50. Row constants used: r4=1 (top status page pair), r5=2 (Area A page pair), r6=5 (Area B page pair) — but these are re‑derived from g_disp+0x42 (active area) at entry so the selected area gets the larger/highlighted rows. Uses mode arg to draw_string = normal(0)/inverse(1) for highlight.
3. g_disp @0x200009C3 — the display struct (field map)
Two parallel area sub‑records of stride 0x43 (Area A at +0x00, Area B at +0x43), plus shared/status fields. Offsets below are from 0x200009C3 (Area A); add 0x43 for Area B. All confirmed from format_area_display writes + home_draw reads.
| off | type | name | meaning / source | drawn by (element) |
|---|---|---|---|---|
+0x00 |
char[16] | area_line0 |
Area A top text (name/tag line) | draw_str_spaced (flag+0) |
+0x15 |
char[7] | line_prefix |
"CH-"+num or "A-"/"D-" mode tag |
draw_str_spaced, x=0x68 (flag+6) |
+0x1C |
u8 | status_flag |
← g_power @0x20000B76; drives "HD"/" " top‑right indicator |
draw_str_spaced x=0x74, template "HD"/" " (flag+2) |
+0x1D |
u8 | hiliteA |
highlight state (r5) for Area A frame | draw_frame_corners (0x08008224) |
+0x1E |
u8 | modeA |
element mode: 0 = blank, 1 = frequency, 2 = channel name | selects freq vs name path |
+0x1F |
u8 | submodeA |
sub‑mode: 0/1 name width, 2 = big‑digit freq | picks big_char vs string |
+0x20 |
char[16] | contentA |
the main Area A string (formatted freq "438.80000" OR channel name OR "CH MODE"/"VFO MODE") |
draw_number_row(big digits) / draw_string on row r5 (flag+7) |
+0x30 |
u8 | modeB |
Area B element mode (0/1/2) | (flag+5) |
+0x31 |
u8 | submodeB |
Area B sub‑mode | |
+0x32 |
char[16] | contentB |
Area B main string | draw_string row r6 (flag+5) |
+0x42 |
u8 | active_area |
0 = A selected, else B — chooses which rows get big/highlight | branches at handler entry |
+0x43… |
— | Area B record | mirror of +0x00… at stride 0x43 |
(rendered by sibling fn 0x08013E64) |
+0x63 |
u8 | ab_marker |
0/1/2 A/B right‑edge marker state ← g_active_area 0x20000B5F |
draw_marker5 pages 2 & 5 (flag+7 preamble) |
+0x64 |
u8 | batt_marker |
0/1 → battery/dual marker layout | draw_marker5 pages 1/3/6 (flag+8) |
Additional companion buffer g_disp2 @0x20000A28 holds the mode‑tag / secondary line ("ANA"/"DMR", "A-"/"D-") built alongside contentA — drawn on the status row.
draw_marker5(page, on) @0x0800870C(aka the "batt/arrow icon" indisplay.md): draws a 5‑column glyph at columns 123–127 (0x7B+i). Pattern bytes30 78 FC 30 30= a right‑pointing arrowhead → the A/B active‑area selector arrows on the far right edge.draw_icon_batt @0x08008874is the structurally‑identical sibling used for the battery/dual markers.
4. On‑screen element → data source → draw call (the master table)
Coordinates: page = 8‑px row band (0=top…7), x = pixel column (0…127). Rows: top status ≈ page 1, Area A ≈ pages 2–3, Area B ≈ pages 5–6 (exact page passed in r4/r5/r6, re‑ordered by active_area).
| # | Element (what you see) | Data source | Draw call (page,x) | Dirty flag |
|---|---|---|---|---|
| 1 | Area A main line — frequency "438.80000" (big digits) or channel name or "CH MODE"/"VFO MODE" |
g_disp+0x20 ← format_area_display from cache 0x20002DEA[area] (+5 freq / +0x20 name) & modeA=g_disp+0x1E |
`draw_number_row(r5,0x14,…,8 | 7)big digits **or**draw_string(r5,0x64,…,2)` |
| 2 | Area B main line (dual‑watch second area) | g_disp+0x32 ← same formatter, area=1 |
draw_string(r6,0x0A/0x27/0x1C,…) per modeB=g_disp+0x30 |
g_dirty[5] |
| 3 | Top name/tag line (radio name / area label) | g_disp+0x00 |
draw_str_spaced(r4,0x00,…,16) |
g_dirty[0] |
| 4 | Channel # / A‑/D‑ prefix ("CH-001", "A-", "D-") |
g_disp+0x15 ← templates "CH-"/"A-"/"D-" + num_to_ascii |
draw_str_spaced(r4,0x68,…,4) |
g_dirty[6] |
| 5 | Mode tag "ANA" / "DMR" (FM/AM/SSB vs digital) |
g_disp2 0x20000A28 ← cache[+0]>>6 (mode bits) |
memcpy_off+draw_string on status row |
(with #4) |
| 6 | HD / status indicator (top‑right) | g_disp+0x1C ← g_power @0x20000B76; template "HD"/" " |
draw_str_spaced(r4,0x74,…,2) |
g_dirty[2] |
| 7 | A/B selector arrows (right edge, col 123) | g_disp+0x63 ← g_active_area 0x20000B5F; g_disp+0x64 |
draw_marker5(2,on) & draw_marker5(5,on); batt markers pages 1/3/6 |
g_dirty[7],[8] |
| 8 | Area A highlight frame | g_disp+0x1D (hilite) & +0x42 |
draw_frame_corners(page,x) 0x08008224 |
g_dirty[3] |
| 9 | CTCSS/DCS tone, power nibble | cache [+0xD]/[+0xF] (12‑bit tone), [+0xE]/[+0x10] (nibble) |
folded into contentA/g_disp2 via formatter |
(with #1) |
| 10 | DMR TG / Contact / Color‑Code (RX) | DMR call ctx g_call @0x2000A6C5 (+1=call type, +2=ID, +0x38=name), fed from cache & FM100B |
built by the call‑info path near 0x08006754; painted on Area line as name/ID |
RX‑event driven |
| 11 | Battery / signal icons | see display.md §draw_icon_batt/signal (cols 21 & 123); battery volts from ADC1 0x08010960 |
draw_icon_batt/draw_icon_signal |
period‑gated task |
Notes:
- FM / AM / SSB vs DMR: the coarse analog/digital split is
cache[+0]>>6→"ANA"/"DMR". The fine analog demod (FM/AM/SSB) is a per‑channel field (RX Demod (A)menu) stored in the channel record and shown via the same status‑line path; it is not a separate draw primitive. - RX/TX arrow: TX vs RX display is driven by the
shiftarg (Talkaround/Reverse) selecting cache[+5](RX) vs[+9](TX); the on‑air TX indicator is a status‑flag element, not a distinct routine. - Dual‑watch A/B:
active_area (g_disp+0x42)andab_marker (+0x63)decide which area is highlighted and where the arrows point;g_active_area @0x20000B5Fis the master.
5. Key RAM state variables (home screen)
| addr | width | name | role |
|---|---|---|---|
0x200009C3 |
struct | g_disp |
home display struct — all home strings/flags (§3). Area A +0, Area B +0x43. |
0x200024EB |
u8[9+] | g_dirty |
per‑element dirty flags; element k paints iff g_dirty[k]!=0. Indices used: 0,2,3,4,5,6,7,8. |
0x20002DEA |
rec[N] | g_chcache |
live channel cache, 48‑byte (0x30) stride; [+5]RXfreq [+9]TXfreq [+0xD/0xF]tones [+0x20]name. Mirrors codeplug channel record — read‑only for UI. |
0x20002DCF |
u8[] | g_chmode |
per‑area display mode/sub‑mode bytes (= g_chcache − 0x1B). |
0x20000B5F |
u8 | g_active_area |
selected VFO/area index (0=A). |
0x20000B76 |
u8 | g_power/status |
drives g_disp+0x1C (HD indicator). |
0x200024D0 |
u8[16] | g_numbuf |
scratch for num_to_ascii (freq/number ASCII build). |
0x2000A6C5 |
struct | g_call |
DMR RX call ctx: +1 call type, +2 ID (BCD‑decoded via 0x080112B8), +0x38 name. |
0x200029BB |
struct | g_uictx2 |
secondary UI context (+0x63,+0x67,+0x197 read by formatter callers). |
0x200008B3 |
u8 | g_screen |
screen id; 0 = this home screen. |
6. Redraw cadence & triggers
- Cadence:
home_drawis called every UI tick (unthrottled) fromui_tick_normal @0x080207DCviaui_draw_dispatch @0x0801E1BCat0x080207E6, but each element is gated by itsg_dirtybyte, so a steady screen costs ~nothing. - Triggers (what sets
g_dirty):- Key / channel / area change — the standby‑key handler path (e.g.
0x080094F8) callsformat_area_display, then setsg_dirty[7]=1(0x080095DA) andg_disp+0x63 = g_active_area, forcing Area A repaint next tick. Other setters at0x08005DA0,0x0800975E. - Screen entry —
ui_draw_dispatchpreambleclear_dirty_flags @0x080136E4walks a companion dirty row0x20002512and force‑marks fields dirty on entry (full repaint on screen switch). - RX / DMR event — updates
g_call @0x2000A6C5and re‑runs the formatter → dirties the affected area. - Periodic — battery/signal icons and RSSI are repainted by the slower period‑gated tasks in
ui_tick_normal(counters0x20000BF5/BF6/BF8), independent ofg_dirty.
- Key / channel / area change — the standby‑key handler path (e.g.
- Model: direct‑to‑GDDRAM (no framebuffer,
display.md§1). Partial repaint is safe because elements occupy fixed, non‑overlapping regions.
7. Recipe to REPLACE the home screen
7.1 Hook point
Redirect the draw dispatch (main-loop.md Option A): patch bl 0x0801E1BC at 0x080207E6 → bl my_draw_router. In my_draw_router, if (g_screen==0) my_home(); else <call stock 0x0801E1BC for other screens>. (Symmetrically patch key dispatch 0x08020816 if you want custom home navigation — see input.md.)
7.2 Two build strategies
Strategy A — reuse stock formatter, re‑lay‑out only (least work).
Keep calling format_area_display(area, hi, shift) @0x08011414 (it does the codeplug→ASCII math for you), then read the ready‑made strings from g_disp @0x200009C3 and paint them wherever you like with draw_string. You get frequency/name/mode formatting for free and never touch the cache or codeplug.
// stock entry points (thumb; set bit0 when taking a function pointer)
void format_area_display(u8 area,u8 hi,u8 shift); // 0x08011414 — fills g_disp
void draw_string(u8 pg,u8 x,const char*s,u8 n,u8 mode); // 0x08008A50
void draw_number_row(u8 pg,u8 x,const u8*d,u16 n,u8 mode);// 0x08008B90 (big freq digits)
#define G_DISP ((volatile u8*)0x200009C3)
void my_home(void){
format_area_display(g_active_area,2,0); // stock builds strings into g_disp
for(u8 pg=0;pg<8;pg++) blit_cols(pg,128,0,0); // clear (display.md §2.1)
draw_string(0, 2, (char*)&G_DISP[0x00], 16, 0); // top line
draw_string(2, 4, (char*)&G_DISP[0x20], 16, 0); // Area A main (freq/name)
draw_string(5, 4, (char*)&G_DISP[0x32], 16, 0); // Area B main
// add your own layout, icons, TG/CC from g_call@0x2000A6C5, battery, etc.
}
Strategy B — full custom, read straight from the cache (max control).
Skip g_disp entirely. Read the live channel cache and radio getters yourself:
#define CH(area) ((volatile u8*)(0x20002DEA + (area)*0x30))
u32 rx = *(u32*)(CH(area)+5); // RX freq, MHz*100000
u32 tx = *(u32*)(CH(area)+9); // TX freq
u16 rtone = *(u16*)(CH(area)+0xD) & 0x0FFF; // RX CTCSS/DCS
u8 isDMR = CH(area)[0] >> 6; // 1 => digital
char*name = (char*)(CH(area)+0x20); // 16-byte channel name
// DMR RX overlay:
#define CALL ((volatile u8*)0x2000A6C5) // +1 type, +2 id, +0x38 name
// battery volts: call 0x08010960 (raw*4/0x42 -> tenths of a volt)
Format freq with num_to_ascii(rx,8) @0x08018530 (→ 0x200024D0) then str_insert_char(buf,'.',3,len) @0x080135A8, or roll your own. Paint with the display.md primitives.
7.3 Data getters + draw primitives your home screen will call
| purpose | vaddr | prototype |
|---|---|---|
| build all home strings (opt.) | 0x08011414 |
format_area_display(u8 area,u8 hi,u8 shift) |
| decimal ASCII | 0x08018530 |
num_to_ascii(uint val,int ndigits) → 0x200024D0 |
| insert char (decimal pt) | 0x080135A8 |
str_insert_char(char*buf,char ch,int pos,int len) |
| byte copy w/ offset | 0x080062EC |
memcpy_off(void*dst,const void*src,int dstoff,int len) |
| text | 0x08008A50 |
draw_string(pg,x,s,n,mode) |
| big freq digits | 0x08008B90 |
draw_number_row(pg,x,digits,n,mode) |
| spaced small text | 0x08008BC6 |
draw_str_spaced(pg,x,s,n,mode) |
| A/B arrow / batt marker | 0x0800870C / 0x08008874 |
draw_marker5(page,on) (col 123) |
| highlight frame corners | 0x08008224 |
draw_frame_corners(x,page) |
| battery/signal icons | 0x08008874/0x08008A04 |
(see display.md) |
| battery volts (ADC) | 0x08010960 |
u8 batt_read(void) (tenths V) |
| clear region | 0x08008D24 |
blit_cols(pg,count,NULL,0) |
7.4 Boundary compliance (hard constraint)
Everything above reads RAM caches (0x20002DEA, 0x2000A6C5, 0x200009C3) and calls display + ADC primitives. None of it writes SPI codeplug regions or touches the USART6 CPS framer / SPI region‑write engine (main-loop.md §5). The channel cache 0x20002DEA mirrors the on‑flash 48‑byte channel record but is a separate RAM copy — reading it cannot change the codeplug format. CPS + codeplug compatibility is unaffected.
8. Confidence
- HIGH — home screen = id 0, draw handler
0x08013BD8; the formatterformat_area_display @0x08011414; the display structg_disp @0x200009C3field map; the channel cache0x20002DEA(48‑byte stride, RX@+5/TX@+9/tones@+0xD,0xF/name@+0x20); helpersnum_to_ascii 0x08018530,str_insert_char 0x080135A8,memcpy_off 0x080062EC; the dirty‑flag model (g_dirty 0x200024EB) and the0x080095DAtrigger. All directly disassembled, literal pools resolved, inline templates decoded ("CH MODE"/"VFO MODE"/"CH-"/"A-"/"D-"/"ANA"/"DMR"). - MEDIUM‑HIGH — exact page (row) numbers per element: the
r4/r5/r6row registers are re‑ordered byactive_areaat handler entry, so the absolute page of Area A vs B swaps with selection; the column x‑values are exact (from the draw calls). Battery/signal icon columns perdisplay.md. - MEDIUM — the DMR TG/Color‑Code overlay path (
g_call 0x2000A6C5, near0x08006754) is identified and its fields typed, but the full CC/slot rendering sequence on the standby line is only partially traced (it interleaves with FM100B RX events); the analog FM/AM/SSB sub‑tag is inferred from theRX Demod (A)channel field rather than a distinct draw routine. Confirm on‑device when redesigning the DMR overlay.