Files
rt-4d/research/re/main-screen.md
T
viktor 5f4d207aa5 Мост к DMR-модулю, разбор спектра REFV DualTachyon, карта запчастей прошивки
Баузбенд FM100B:
- найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём
- расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации
- инструмент tools/fm100b.py (ping/send/raw/scan)
- cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом)
- разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд
- cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14
- исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md)

Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md):
- вызывается как функция горячей клавиши №22 Analog Spectrum
- вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318
- спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC

Декомпозиция (docs/firmware-parts.md):
- два процессора + внешний SPI = три канала внедрения
- карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode
- найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки)
- дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке
- аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля

Прочее:
- везде исправлен режим прошивки: тангента PTT вместо клавиши "*"
- устаревший Ru-4D_Flasher.exe удалён из репозитория
- добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
2026-07-22 21:17:20 +09:00

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 CortexM4F Thumb, load/vaddr base 0x08002800). All disassembly via capstone CS_ARCH_ARM + CS_MODE_THUMB.

This document reverseengineers 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 lowlevel 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 is home_draw @0x08013BD8 (dispatched from ui_draw_dispatch @0x0801E1BC's tbb table). An alternate home / dualdisplay variant is id 7 → 0x08013804.
  • Home render is twostage and datadriven, not hardcoded:
    1. format_area_display(area, hi, shift) @0x08011414 reads the live channel cache at 0x20002DEA (48byte records) + the perarea mode table at 0x20002DCF, formats frequency / channelname / modetag ASCII into the display struct g_disp @0x200009C3, and sets dirty flags in g_dirty @0x200024EB.
    2. home_draw @0x08013BD8 consumes g_disp + g_dirty and calls the display.md primitives (draw_string, draw_number_row, draw_str_spaced, draw_marker5) to paint each changed element.
  • Redraw is incremental / dirtyflag driven. An element repaints only when its byte in g_dirty @0x200024EB is nonzero; setting a flag (e.g. after a channel/area change, RX event, or key) triggers repaint on the next UI tick. No fullframe clear.
  • The single struct you must understand is g_disp @0x200009C3. Every onscreen string/flag is a field of it (see §3). To replace the home screen, either (a) keep format_area_display and relayout in your own home_draw, or (b) ignore both and render straight from the cache 0x20002DEA + 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 @0x080207DCbl ui_draw_dispatch @0x0801E1BC at callsite 0x080207E6. ui_draw_dispatch tbbjumps 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 dirtyflag array role
0 home_draw 0x08013BD8 g_disp 0x200009C3 g_dirty 0x200024EB primary standby (VFO/channel) home
7 0x08013804 0x2000096A 0x200009BD alt home / dualdisplay variant
2 0x08013980 0x200008D0 0x200008F5 standbykey / list overlay

Hook (recommended, from main-loop.md Option A): redirect the bl at 0x080207E6 to my_draw_router; render your own home there. Everything else (RF, DMR RX, scan, battery, and the entire serial/CPS + SPI regionwrite path) is preserved. 2instruction 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 cache 0x20002DEA + area*0x30.
  • hi (r5) = highlight/selection state (0,1,2,3) → stored to g_disp+0x1D; 2 selects an alt copy path.
  • shift (r8) = TXshift 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 48byte 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] 16byte channel name (ASCII) name display (mode==2)

Also reads globals: g_power @0x20000B76g_disp+0x1C (the "HD"/status flag byte); g_chmode @0x20002DCF + areag_disp+0x1E (element mode), [+2+area]g_disp+0x1F (submode).

Writes into g_disp @0x200009C3 (see §3) and builds ASCII via helpers:

  • num_to_ascii(uint val, int ndigits) @0x08018530 — rightaligns ndigits decimal ASCII into scratch 0x200024D0. Used to render the frequency (freq, 8 digits) and channel number.
  • str_insert_char(buf, ch, pos, len) @0x080135A8 — inserts ch at pos (shifting right). Used to splice the "." decimal point into the frequency string (ch=0x2E at pos 3) → "438.80000".
  • memcpy_off(dst, src, dstoff, len) @0x080062ECdst[dstoff+i]=src[i]. The workhorse for copying templates / name / formatted number into g_disp fields.

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: 0x0800951Cformat_area_display(area=[0x20000B5F], hi=2, shift=[0x200029BB+0x67]).

2.2 home_draw @0x08013BD8 — the painter

Consumes g_disp @0x200009C3 and perelement 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 rederived 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 subrecords 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"/" " topright 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 submode: 0/1 name width, 2 = bigdigit 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 submode
+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 rightedge 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 modetag / secondary line ("ANA"/"DMR", "A-"/"D-") built alongside contentA — drawn on the status row.

draw_marker5(page, on) @0x0800870C (aka the "batt/arrow icon" in display.md): draws a 5column glyph at columns 123–127 (0x7B+i). Pattern bytes 30 78 FC 30 30 = a rightpointing arrowhead → the A/B activearea selector arrows on the far right edge. draw_icon_batt @0x08008874 is the structurallyidentical sibling used for the battery/dual markers.


4. Onscreen element → data source → draw call (the master table)

Coordinates: page = 8px 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, reordered 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+0x20format_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 (dualwatch 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 0x20000A28cache[+0]>>6 (mode bits) memcpy_off+draw_string on status row (with #4)
6 HD / status indicator (topright) g_disp+0x1Cg_power @0x20000B76; template "HD"/" " draw_str_spaced(r4,0x74,…,2) g_dirty[2]
7 A/B selector arrows (right edge, col 123) g_disp+0x63g_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] (12bit tone), [+0xE]/[+0x10] (nibble) folded into contentA/g_disp2 via formatter (with #1)
10 DMR TG / Contact / ColorCode (RX) DMR call ctx g_call @0x2000A6C5 (+1=call type, +2=ID, +0x38=name), fed from cache & FM100B built by the callinfo path near 0x08006754; painted on Area line as name/ID RXevent 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 periodgated 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 perchannel field (RX Demod (A) menu) stored in the channel record and shown via the same statusline path; it is not a separate draw primitive.
  • RX/TX arrow: TX vs RX display is driven by the shift arg (Talkaround/Reverse) selecting cache [+5] (RX) vs [+9] (TX); the onair TX indicator is a statusflag element, not a distinct routine.
  • Dualwatch A/B: active_area (g_disp+0x42) and ab_marker (+0x63) decide which area is highlighted and where the arrows point; g_active_area @0x20000B5F is 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 perelement 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, 48byte (0x30) stride; [+5]RXfreq [+9]TXfreq [+0xD/0xF]tones [+0x20]name. Mirrors codeplug channel record — readonly for UI.
0x20002DCF u8[] g_chmode perarea display mode/submode 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 (BCDdecoded 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_draw is called every UI tick (unthrottled) from ui_tick_normal @0x080207DC via ui_draw_dispatch @0x0801E1BC at 0x080207E6, but each element is gated by its g_dirty byte, so a steady screen costs ~nothing.
  • Triggers (what sets g_dirty):
    • Key / channel / area change — the standbykey handler path (e.g. 0x080094F8) calls format_area_display, then sets g_dirty[7]=1 (0x080095DA) and g_disp+0x63 = g_active_area, forcing Area A repaint next tick. Other setters at 0x08005DA0, 0x0800975E.
    • Screen entryui_draw_dispatch preamble clear_dirty_flags @0x080136E4 walks a companion dirty row 0x20002512 and forcemarks fields dirty on entry (full repaint on screen switch).
    • RX / DMR event — updates g_call @0x2000A6C5 and reruns the formatter → dirties the affected area.
    • Periodic — battery/signal icons and RSSI are repainted by the slower periodgated tasks in ui_tick_normal (counters 0x20000BF5/BF6/BF8), independent of g_dirty.
  • Model: directtoGDDRAM (no framebuffer, display.md §1). Partial repaint is safe because elements occupy fixed, nonoverlapping 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 0x080207E6bl 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, relayout only (least work). Keep calling format_area_display(area, hi, shift) @0x08011414 (it does the codeplug→ASCII math for you), then read the readymade 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 regionwrite engine (main-loop.md §5). The channel cache 0x20002DEA mirrors the onflash 48byte 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 formatter format_area_display @0x08011414; the display struct g_disp @0x200009C3 field map; the channel cache 0x20002DEA (48byte stride, RX@+5/TX@+9/tones@+0xD,0xF/name@+0x20); helpers num_to_ascii 0x08018530, str_insert_char 0x080135A8, memcpy_off 0x080062EC; the dirtyflag model (g_dirty 0x200024EB) and the 0x080095DA trigger. All directly disassembled, literal pools resolved, inline templates decoded ("CH MODE"/"VFO MODE"/"CH-"/"A-"/"D-"/"ANA"/"DMR").
  • MEDIUMHIGH — exact page (row) numbers per element: the r4/r5/r6 row registers are reordered by active_area at handler entry, so the absolute page of Area A vs B swaps with selection; the column xvalues are exact (from the draw calls). Battery/signal icon columns per display.md.
  • MEDIUM — the DMR TG/ColorCode overlay path (g_call 0x2000A6C5, near 0x08006754) 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 subtag is inferred from the RX Demod (A) channel field rather than a distinct draw routine. Confirm ondevice when redesigning the DMR overlay.