- Полный 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>
14 KiB
RT-4D Menu Selection → Inverse-Video Highlight — RE Findings & Patch Spec
Firmware: rt4d_stock_v3.25_abs_0x08002800.bin (ARM Cortex-M4F Thumb, load base 0x08002800, size 155740 = 0x2605C).
All addresses are virtual (vaddr). File offset = vaddr − 0x08002800.
1. Render pipeline (as reverse-engineered)
1.1 Text / glyph layer (confirmed, trusted)
draw_string@0x08008A50. Signature:(r0=y_page, r1=x_pixel, r2=char*, r3=len, [sp,#0x28]=mode). Themodeword at[sp,#0x28]is loaded (ldr r3,[sp,#0x28]) and passed asr3/r7to the glyph blitters. Advance is +7px per ASCII char (adds r0,r6,#7).- ASCII glyph blitter @0x08007FB8, CJK blitter @0x08008454. Both copy a 14-byte
(7 cols × 2 pages, column-major, 8 vertical px/byte) glyph into a stack buffer via
0x08021828, then transform by mode (r7) before blitting:- mode 0 → glyph copied as-is → normal (black text on clear background).
- mode 1 →
mvnsevery byte (full invert) + edge masks: even byte&=0xFE, odd byte&=0x7F. → INVERSE VIDEO: the whole 7×16 cell becomes a white bar with the glyph punched black, leaving a 1px gap top+bottom for clean row separation. Verified by simulation:mode1(empty cell)=FE 7F FE 7F…(solid bar w/ 1px gaps);mode1(solid px)=00(black). This is exactly a modern "highlight bar with readable text". - mode 2 → odd bytes
|=0x80→ sets the bottom pixel of the top page → a thin underline / bottom rule (used for title bars / section headers), NOT a full highlight. - mode ≥3 → falls through to normal (mode 3/4 seen in the home screen = plain text).
- LCD framebuffer primitives (128×64 mono, ST7565/UC1701 class, page-addressed over SPI):
lcd_set_addr(r0=x, r1=y)@0x08014A7C — mapsx → (0xB7−x), sets page/column.lcd_write_col(r0=byte)@0x08014B28 — writes one 8-px vertical column, auto-advances.lcd_flush()@0x08014CB8 — DMA/SPI blit of the composed frame (calls SPI0x08004CE0).- Line geometry: 7px pitch → 18 chars per 128px line; y is a page index 0..7
(menu text is drawn at page/
y=4).
1.2 Menu system architecture (retained-mode, staged buffers)
The settings menu (Basic Set, Key Define, Analog Set, Digital Set, Channel Set,
Zone Set, Message, Device Name, … full descriptor/label table at 0x0801521C–0x08015440,
[submenu/handler ptr][16-byte fixed label] records) is not drawn by a single visible
row loop. Instead it is retained-mode:
- Widget-setup helpers stage a menu descriptor into RAM state struct @0x20000CE8
(fields:
+1=widget type,+4/5=item count,+2=selected index,+6=sel,+7=flags) and label/value buffers @0x20000A83 (+0x15=current-item text,+0x27=next-item text,+0x17=inline-edit buffer,+0x2a=split/cursor position). Setup entry points:0x08009C58(generic selectable list),0x0800B0B4(numeric value),0x0800B100,0x0800B8D8,0x08009D3C. - List refresh
0x0801CB10computesseland(sel+1)%count, copies the selected item into the "current" slot (+0x15) and the following item into the "next" slot (+0x27) — i.e. a 2-line window with the selected item always in the top ("current") slot. - Screen paint / blit happens in the home/menu render dispatcher
0x080142C0, whichtbb-dispatches (@0x08014348, on mode byte[struct-1 +0x16], cases 0–6) to 7 small widget painters, all drawing aty=4:- case 0 →
0x08014074(inline field / list-item painter) — see §2. - cases 1–6 →
0x080143A0 / 0x080143E6 / 0x08014442 / 0x080144DE / 0x08014514 / 0x0801435A(inline value editors: split a value into segments, draw the edited segment with mode 1 and the rest with mode 0).
- case 0 →
- The big per-item value screen
0x08014E20(called from0x0800ABD0/0x0800B054) is the submenu value/edit dispatch; it feeds text through0x08009C58(×8) rather than drawing directly.
Menu-list RENDER routine answer (task item 1): the visible menu row/field is drawn by
0x08014074 (dispatched from the render loop 0x080142C0 via the tbb @0x08014348).
0x08014074 is the only function in the image that both references the menu text buffer
(0x20000A83) and calls a glyph blitter — it is the concrete draw site to patch.
2. How the current selection is drawn (task item 2)
0x08014074 (reads state struct 0x20000A83; L = [+0x2a] = split/caret position 0..0x10;
buf = +0x17 = 17-char item text buffer). All draws at y=4. Decoded:
if [+0x2a] >= 0x11: ; buffer full — no caret
draw_string(y=4, x=1, buf, len=0x11, mode=0) ; whole line, normal
else:
draw_string(y=4, x=1, buf, len=L, mode=0) ; text BEFORE the cursor (normal)
draw_string(y=4, x=L*7+1, buf+L, len=1, mode=1) ; the SELECTED char (INVERSE) ← cursor
draw_string(y=4, x=L*7+8, buf+L+1, len=0x10-L, mode=0) ; text AFTER the cursor (normal)
So today the "cursor / selection indicator" is a single character rendered in mode-1
inverse video (a 1-char-wide highlight caret), positioned at column L. The special
treatment of the selected index is the middle draw_string call with len=1, mode=1
(instruction sequence: movs r0,#1 ; str r0,[sp] sets mode=1; movs r3,#1 sets len=1).
The inline value-editor widgets (cases 1–6 of 0x080142C0) work the same way, inverting the
segment currently being edited.
There is no >/triangle glyph and no separate arrow bitmap — the "arrow/left-cursor"
the UI shows is this inverse caret block. (The only bitmap-cursor-like helper, 0x08008224,
is the battery/RSSI icon drawer, unrelated.) So "remove the arrow" = "stop drawing the
1-char inverse caret and instead inverse the entire selected line".
3. Cleanest way to a full-width inverse highlight (task item 3)
Two mechanisms exist; mode-1 is the right one (mode 2 is only an underline):
- (a) Draw the selected row's full text with mode 1 and pad the string to the full 18-col line width so the highlight bar spans edge-to-edge. Because mode-1 inverts each cell (including the space glyph → solid bar with 1px gaps), a right-padded string already yields a full-width readable highlight bar — no separate rectangle-fill routine is required.
- (b) Fill/invert-rect helper: the image has no general "invert rectangle" routine;
the only rect-ish primitive is the icon column-writer
0x08008300/0x08008224(fixed 14-col templates). Re-purposing it is more invasive than (a). So approach (a) is chosen.
For the settings-list specifically the item text is staged into buffers padded with spaces
already (buffers are memset-filled to 0x10 with 0x20/blanks by 0x080062EC before the
label copy), so a mode-1 draw of the current-slot buffer paints the whole row as a bar.
4. Concrete patch (task item 4)
Approach: minimal, in-place, length-safe — switch the selected row's whole draw to mode 1
The selected line is the top / "current" slot of 0x08014074. Replace the 3-segment
(normal | inverse-caret | normal) draw with one full-width mode-1 draw of the whole buffer.
This makes the entire selected line an inverse highlight bar and eliminates the 1-char caret.
0x08014074 prologue+body bytes (for reference, from offset 0x11874):
0x08014074: 38 b5 22 48 90 f8 2a 00 11 28 09 db 00 20 11 23 ; push; ldr r0,=struct; ldrb r0,[r0,#0x2a]; cmp #0x11; blt; movs r0,#0; movs r3,#0x11
0x08014084: 1e 4a 17 32 01 21 00 90 04 20 f4 f7 df fc 34 e0 ; ldr r2,=struct; adds r2,#0x17; movs r1,#1; str r0,[sp]; movs r0,#4; bl draw_string; b .+
0x08014094: 00 20 00 90 19 48 90 f8 2a 30 00 f1 17 02 01 21 ; movs r0,#0; str r0,[sp](mode=0); ...; movs r1,#1
0x080140a4: 04 20 f4 f7 d3 fc 01 20 00 90 14 48 90 f8 2a 30 ; movs r0,#4; bl draw_string; movs r0,#1; str r0,[sp](mode=1) ← caret
...
The blt at 0x0801407E (11 28 09 db: cmp r0,#0x11 / blt) already selects between the
"full buffer" branch (0x08014080, draws the whole 17-char buffer at x=1,y=4) and the
"3-segment caret" branch (0x08014094). The simplest robust change is: make the whole-buffer
branch use mode 1, and force execution down that branch always (skip the caret path). That
gives a full-line inverse highlight for the item and removes the caret entirely.
Patch — 2 sites, 4 bytes total, no code cave, no length change.
The full-buffer branch at 0x08014080 draws draw_string(y=4, x=1, buf, len=0x11, mode=[sp]).
We (P1) force that branch to always run and (P2) make its mode = 1.
P1 — force the full-buffer / highlight branch. Remove the blt that would otherwise divert
to the 3-segment caret path, so the full-buffer draw at 0x08014080 always executes:
- vaddr
0x0801407E: original09 DB(blt #0x08014094) → new00 BF(nop). (2 bytes)
P2 — make that draw inverse. The branch sets its mode via movs r0,#0 ; str r0,[sp]:
- vaddr
0x08014080: original00 20(movs r0,#0) → new01 20(movs r0,#1). (2 bytes)
Verified patched disassembly:
cmp r0,#0x11 ; nop ; movs r0,#1 ; movs r3,#0x11 ; ldr r2,=buf ; adds r2,#0x17 ; movs r1,#1 ; str r0,[sp] ; movs r0,#4 ; bl draw_string
→ draws the full 17-char (space-padded) buffer at x=1,y=4 in mode 1; the caret path at
0x08014094 is now dead code.
Result: whenever this widget paints, it draws the full 17-char (space-padded to 18-col line)
buffer at x=1, y=4 in mode 1 = full-width inverse-video highlight bar, and the old
single-char inverse caret path (0x08014094…) is never reached → arrow/caret removed.
Final patch list — (vaddr, file_offset, original_bytes, new_bytes):
0x0801407E (off 0x1187E) : 09 DB -> 00 BF ; blt 0x8014094 -> nop
0x08014080 (off 0x11880) : 00 20 -> 01 20 ; movs r0,#0 -> movs r0,#1 (mode 0 -> 1)
Bytes are shown in stored (file) order. Total change: 4 bytes, in place, no length change.
Optional wider fix (cases 1–6 / other menus)
The same 1-char-inverse→full-line-inverse idea applies to the inline value-editor widgets
0x080143A0…0x08014514 and the list refresh 0x0801CB10. Those are out of scope for a
minimal, low-risk patch (each edits distinct value fields where a per-segment caret is
actually desirable). Recommend shipping only the 0x08014074 change first, verify on-radio,
then decide whether the value-editors should also flip.
Alternative (code-cave) approach, if per-row control is wanted
If you later want the highlight on a scrolling multi-row list (rather than the single current-item slot), a code cave is available:
- 0xFF-erased cave:
0x08024AD2, 320 bytes free. - 0x00 cave:
0x08024778, 336 bytes free. A small Thumb helper could loop rows, callingdraw_string(y=row_page, x=1, row_text, 18, mode = (row==sel)?1:0), then hook it in place of thebl 0x08014074. Not needed for the minimal fix above.
5. Residual risks & on-radio verification
Risks
- Buffer padding: the full-line branch draws
len= the buffer count. If the item text isn't space-padded to the full 18 columns in every menu that reaches0x08014074, the highlight bar will only span the text, not the whole line. Mitigation: the setup helpersmemsetthe buffers to blanks (0x20) to width 0x10 before copying the label, so padding is generally present; confirm visually. If a bar is short, extend the draw len to 18 and ensure trailing spaces. - Shared painter:
0x08014074(case 0) may also render non-list inline fields (e.g. a name/DTMF entry field) where the single-char caret was intentional. Forcing full-line inverse there removes the per-char caret — acceptable for a "selected line" look but check text-entry screens remain usable (you lose the char-position caret). If that regresses a text-entry screen, gate the change on the widget-type byte instead of nop-ing theblt. - 1px row gaps: mode-1 edge masks leave 1px clear at top and bottom of the cell — this is desirable (separates rows) and matches modern radios; no action needed.
- Checksum/signature: if the loader validates a firmware CRC/signature, patched bytes must be re-CRC'd. v3.25 is an absolute image at 0x08002800 — verify whether the bootloader checks an appended checksum before flashing.
Verification on-radio
- Flash patched image. Enter Menu. The currently-highlighted item should show as a solid inverse bar (white background, black text) spanning the line width; the old 1-char arrow/inverse caret should be gone.
- Scroll up/down: the highlight bar must follow the selection (top "current" slot) and text stays readable at every position.
- Enter a submenu with a numeric value (e.g. Backlight / Light Timer) — confirm value
screens still render (those go through
0x08014E20, unaffected). - Open a text-entry screen (Device Name / Message) — confirm it's still operable (risk #2). If the editing caret is needed there, switch to the type-gated variant.
- Watch for any garbled top line at boot/home screen (shared render dispatcher
0x080142C0) — the patch only alters case-0 widget, home layout should be unchanged.
Address quick-reference
| what | vaddr |
|---|---|
| draw_string | 0x08008A50 |
| ASCII glyph blitter (mode in r7) | 0x08007FB8 |
| CJK glyph blitter | 0x08008454 |
| lcd_set_addr / lcd_write_col / lcd_flush | 0x08014A7C / 0x08014B28 / 0x08014CB8 |
| menu descriptor/label table | 0x0801521C–0x08015440 |
| list-widget setup (generic) | 0x08009C58 |
| list refresh (current/next slot) | 0x0801CB10 |
| screen render dispatcher (tbb) | 0x080142C0 (tbb @0x08014348) |
| selected-row painter (PATCH SITE) | 0x08014074 |
| menu state struct / text buffers (RAM) | 0x20000CE8 / 0x20000A83 |
| code caves | 0x08024AD2 (320B, 0xFF) / 0x08024778 (336B, 0x00) |