Files
rt-4d/docs/subsystems/main-screen.md
T
viktorиClaude Opus 4.8 ae36c3b729 RT-4D: реверс прошивки, русификация, кастомный UI, флешеры
- Полный 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>
2026-07-08 15:47:22 +09:00

243 строки
21 KiB
Markdown
Исходник Ответственный История

Этот файл содержит неоднозначные символы Юникода
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 @0x080207DC`**`bl ui_draw_dispatch @0x0801E1BC` at callsite `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 | 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 @0x20000B76``g_disp+0x1C` (the "HD"/status flag byte); `g_chmode @0x20002DCF + area``g_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) @0x080062EC`** — `dst[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: `0x0800951C``format_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+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)` | `g_dirty[7]` |
| 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 0x20000A28``cache[+0]>>6` (mode bits) | `memcpy_off`+`draw_string` on status row | (with #4) |
| 6 | **HD / status indicator** (topright) | `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]` (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 entry** — `ui_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 **`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, 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.
```c
// 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:
```c
#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.