Files
rt-4d/docs/Russification.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

151 строка
16 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 Menu Russification — Feasibility & Plan
**Verdict: YES (in-place) — every Russian letter already has a shipped glyph and the stock GBK double-byte render path draws it with zero firmware modification; the only real work is rewriting labels in place and abbreviating any that exceed 7 Cyrillic chars.**
## Why (the three gating facts)
1. **Cyrillic glyphs present? YES.** All 66 modern-Russian letters (33+33, incl. Ё/ё) exist in the shipped SPI font ROM, addressed as GB2312 row A7 (`0xA7A1..0xA7F1`). Confirmed two ways: the Unicode codepoint index at SPI `0x3F0000` lists `U+0410..U+044F` + `U+0401/U+0451` contiguously (slots 9825–9905), and feeding the raw GBK bytes through the firmware's own glyph-address math lands on populated bitmaps (`А`=`A7A7``0x1D00D8`, 20 nonzero bytes).
2. **Render supports double-byte? YES.** `draw_string @0x08008A50` classifies any byte `≥0x80` as a GBK lead byte, consumes the trail byte, forms `(lead<<8)|trail`, and blits a 14-px cell — this is the same classic GBK path the stock Chinese mode uses. No modification needed.
3. **Fields fit? PARTIALLY — but workably.** Records are a hard 16 bytes with the last 2 reserved for the item id, leaving **14 usable bytes = 7 Cyrillic chars** (2 bytes each). Short labels (`Зоны`, `Меню`, `Каналы`, `Имя`) fit natively; labels needing ≥8 Cyrillic chars must be abbreviated (`Настр.`, `Основ.`) — standard practice for Russian ham-radio UIs and not a blocker.
The single most important constraint: records are reached by `base + 16·index` and via 80 hard literal-pool pointers, so **every patch must stay exactly 16 bytes — no resize, no reorder.**
---
## A. Menu / UI string tables & the language system
**The `0x080253ED` table is the entire on-screen UI text pool**, a contiguous array of fixed 16-byte records spanning `0x080253ED → 0x0802723D` (7,760 bytes, 480 records). It starts cleanly after code/literal-pool at `0x080252CD..0x080253EC` and ends where the country/MCC table begins (`0x0802724A`). Verified head:
```
0x80253ed 42 61 73 69 63 20 53 65 74 20 20 20 20 20 30 31 |Basic Set 01|
0x80253fd 4b 65 79 20 44 65 66 69 6e 65 20 20 20 20 30 32 |Key Define 02|
0x802544d 4d 65 73 73 61 67 65 20 20 20 20 20 20 20 30 37 |Message 07|
0x802545d 52 61 64 69 6f 20 4e 61 6d 65 20 20 20 20 30 31 |Radio Name 01| <- submenu restarts at 01
```
It interleaves two 16-byte record kinds, both translatable:
| Kind | Count | Unique | Examples | Suffix |
|---|---|---|---|---|
| Menu items (label + `NN` ordinal) | 181 | 157 | `Basic Set␠␠␠␠␠01`, `Scan Direction15`, `Personal ID␠␠␠01` | 2 ASCII digits |
| Option / enum-value / field labels | 299 | 282 | `Off`, `High`, `Slot 1`, `FM`/`AM`/`SSB`, `Show Caller Info`, `Callsign :`, `Unicode`/`GBK`/`None` | none |
**String model — pointer-indexed, not a packed blob.** A menu-descriptor table at `0x08015214`+ holds 80 literal-pool pointers, each targeting a 16-byte record at `base + 16·index` (all 16-aligned): `lit@0x0801521c → 0x080253ED` (rec #0), `lit@0x08015238 → 0x0802545D` (rec #7), etc. Each descriptor is `[RAM state word 0x2000xxxx][pointer to fixed-16 title record][NUL-terminated inline ASCII copy of the parent label]`. Consequences:
- A record's text = its 16 bytes; its identity = its index `(vaddr 0x080253ED)/16`. **Records must stay exactly 16 bytes** or every pointer/index breaks.
- The trailing `NN` is a parsed ordinal (display ordering/bounds, restarts per submenu) — **not drawn** (renderer emits only the 14-col label) and **must be preserved byte-exact**.
- A full translation must patch **both** the fixed-16 record **and** the inline descriptor copy, or the breadcrumb parent-title stays English.
**No runtime language switch, no parallel Chinese table.** The image contains zero occurrences of `Language`/`English`/`Chinese`/`中文`/`语言` (ASCII or GBK). A full-app scan for coherent GBK CJK runs found exactly one — `0x08019920 = "正在进行数字调试"` (a service/debug status line), not a menu. The SPI dump below the font region has only a sequential codepoint index at `0x14C000`, not menu text. `language=Chinese` lives in the **PC upgrade tool's** `data.ini`, configuring that Windows app, not the radio. **Conclusion: Chinese mode renders CJK by pulling glyphs from the SPI font by codepoint/GBK index at draw time — there is no Chinese table to overwrite. Russification = rewriting the English strings in place.**
Excluded from russification (not UI language): country/MCC table `0x0802724A..0x0802833E` (~256 entries), pinyin IME syllable table `0x0802219A..0x080240D8`.
**Scope:** ~157 menu labels + ~282 option/field labels (both in the fixed-16 table) + ~80 inline descriptor copies (mostly duplicates) + ~150–250 NUL-terminated rodata prompt/status strings (`Please Wait...` @`0x08014964`, `Update DMR Chip` @`0x08014950`, `Unread SMS :` @`0x0801FB44`, `FM RX Standby` @`0x080159F4`, `PC Programing`, `Draft Full!`, `Calibration OK!`, …). **Total ~600–700 distinct strings**, all edited in place at ≤ original byte width.
## B. Font glyph coverage — Cyrillic exists (make-or-break: GO)
**All 66 Russian letters are present in the shipped font ROM.** The glyphs are served from external SPI data-flash, not MCU flash — the app image contains no standalone ASCII font table, and the firmware references SPI font-band literals (`0x00200000` @`0x0801469E`).
**Wide-font Unicode index at SPI `0x3F0000`:** a sorted table of 32,256 big-endian `u16` codepoints (`4E 02 4E 04 …`), `0xFFFF`-terminated at `0x3FFC00`. Slots 0–8288 are CJK; slots 8289+ are the GB2312 A1–A9 symbol/letter rows in Unicode order (Greek `U+0391` at slot 9569, then Cyrillic). ASCII proper returns no slot (served by the separate narrow bank).
**The key test — Cyrillic in the index (all 66, GB2312 A7-row order):**
```
slot 9825:0410(А) 9826:0411(Б) ... 9831:0401(Ё) ... 9857:042f(Я) <- 33 upper incl. Ё
slot 9873:0430(а) 9874:0431(б) ... 9879:0451(ё) ... 9905:044f(я) <- 33 lower incl. ё
```
Note Ё=U+0401 inserted right after Е and ё right after е — GB2312 collation, confirming this is the A7 row (`А=0xA7A1 … Я=0xA7C1, а=0xA7D1 … я=0xA7F1, Ё=0xA7A7, ё=0xA7D7`).
**Bitmap ROM format confirmed:** glyphs live in the dense SPI band `0x150000–0x240000`. Cell format is 16×16, 2 bytes/row, little-endian, bit 0 = leftmost pixel (32 B/glyph for the wide bank). Proof — rendering `U+4E00 一` (single horizontal stroke) yields a clean 14-px bar (`FF 3F` = 8+6 bits, LSB-first), nailing bit order and cell size. Cyrillic-block windows (near `0x1B9880`) show recognizable "А" and "Ж" letterforms.
**We do NOT need to add glyphs.** Russification requires **no font editing** — labels just need to be authored as GB2312 A7-row double-byte codes, a path the stock Chinese mode already exercises. One honest caveat: the exact slot→byte-address arithmetic across the *full* 32k table is banked/sparse (not a clean single-base `FONT_BASE + slot·32`), but this is irrelevant to russification because Section C proves the *menu* path uses a direct GBK plane computation with a known base, and that lands on populated Cyrillic cells.
## C. String rendering / draw path — GBK double-byte, zero firmware change
**Primitives.** `SPI_flash_read(dst,src,len)` @`0x08021826` (thunk `0x08021828`, ~120 callers) issues opcode `0x03`, clocks the address big-endian, streams bytes. Two blitters compute `font_base + glyph_index × cell_size`:
- **ASCII blitter `0x08007FB8`** — 7 px wide, 14-byte cell, base **`0x19C000`**: `index = char 0x20`, `addr = base + index*14`.
- **CJK/GBK blitter `0x08008454`** — 14 px wide, 28-byte cell (`0x1C`), base **`0x19E000`**:
```
row = lead 0x81 ; col = trail 0x40
index = col + row*190 ( 1 when trail > 0x7F, skipping the 0x7F gap)
addr = 0x19E000 + index*28
```
**The decisive routine — `draw_string(x,y,str,len) @0x08008A50`** (~110 callers, incl. menu renderers `0x08013818–0x08014680`) iterates byte-by-byte and dispatches on a lead-byte test:
- byte `0x01–0x7F` → ASCII blitter, cursor +7 px.
- byte `0x80–0xFE`**lead byte**: consumes the next byte, forms `(lead<<8)|trail`, calls CJK blitter, cursor +14 px, `i += 2`.
- `0xFF`/`0x00` → whitespace/terminator.
This is a **classic GBK renderer**: any high-bit byte is a double-byte lead. A sibling width-measurer at `0x0800553C` uses the identical `0x80` classification, confirming the convention project-wide.
**Crucially, the menu path uses direct GBK plane math, NOT the `0x3F0000` Unicode index.** The Unicode table is a separate path (SMS/contact rendering that stores UTF-16). Menu labels reach glyphs by their raw GBK bytes. Feeding GB2312 A7 Cyrillic bytes through the exact firmware math lands on populated cells:
| Char | GBK bytes | computed SPI offset | nonzero bytes in cell |
|---|---|---|---|
| `А` | `A7 A7` | `0x1D00D8` | 20 — legible Cyrillic-A |
| `Я` | `A7 C0` | `0x1D0394` | 21 |
| `а` | `A7 D1` | `0x1D0570` | 14 |
**Exact recipe: to draw `А`, the string must contain raw bytes `0xA7 0xA7`** (`0xA7 0xC0` for `Я`, `0xA7 0xD1` for `а`, …). `draw_string` sees `0xA7 ≥ 0x80`, takes the double-byte branch, and blitter `0x08008454` fetches the already-present Cyrillic cell — **zero firmware modification**. Cost: 2 bytes / 14 px per Cyrillic char.
(Note: the A7-row byte values differ slightly between the two analyses — Section B/D derive `А=0xA7A7` from the Unicode-order slot position, while Section D also cites `А=0xA7A1` from a direct `gb18030` round-trip. This exact byte mapping must be pinned on-target during the PoC — see the PoC verify step — but both agree Cyrillic lives in row A7 and the cells are populated.)
## D. Practical encoding, field-width constraints & effort
**Field-width math.** 16-byte records, last 2 bytes = item id ⇒ **14 usable bytes = max 7 Cyrillic chars** at 2 bytes each. Mixing 1-byte ASCII punctuation (`.`) is legal GBK and saves a byte.
| English | Russian | GBK bytes | Fits 14? |
|---|---|---|---|
| Zone Set | Зоны | 8 | Yes |
| Menu | Меню | 8 | Yes |
| Channel Set | Каналы | 12 | Yes |
| Radio Name | Имя | 6 | Yes |
| Basic Set | Настр. | 11 | Yes |
| Basic Set | Настройки | 18 | **No → abbreviate** |
| Basic Set | Основные | 16 | **No → Основ. (11B)** |
Every label ≥8 Cyrillic chars must be abbreviated (Настр., Каналы, Зоны, Сообщ., Скан, Аналог, Цифра) — normal for Russian ham UIs.
**Approach comparison.** (a) Overwrite English table in place, keep `language=English` — smallest change, firmware-only, ids preserved. (b) Find and overwrite a Chinese GBK table, set `language=Chinese` — but per Section A **no such table exists**, so this is not available. (c) Font-mod: overwrite ASCII cells `0x19C000` with 1-byte Cyrillic — gives 14 chars/label but touches font flash and sacrifices Latin. **Winner: (a).** Section C proves the menu `draw_string` is codepage-agnostic (dispatches on the `0x80` bit regardless of `language` setting) and already resolves A7 Cyrillic to populated glyphs — so overwriting the English table in place with GBK A7 bytes works directly, no config change, no font flash.
## Recommended approach
**Strategy (a): overwrite the English fixed-16 UI table in place with GB2312 A7-row (GBK double-byte) Russian, no config or font-flash changes.** Justification: the menu renderer `draw_string @0x08008A50` classifies bytes purely by the high bit and routes `0x80+` through the GBK blitter `0x08008454`, which computes a direct plane address landing on the Cyrillic cells that already ship in the SPI font ROM. This is a **single-artifact change** (patch the app `.bin`, reflash MCU app) — no dependency on a nonexistent Chinese table, no SPI font rewrite, no `data.ini` edit. Length-preserving patches mean no pointer/index/relocation fix-ups.
## Proof-of-concept — one label
Target the shortest high-value item: **"Zone Set" → "Зоны"** at vaddr `0x0802543D` (file offset `0x0802543D 0x08002800 = 0x22C3D`).
Original 16 bytes:
```
5A 6F 6E 65 20 53 65 74 20 20 20 20 20 20 30 36 "Zone Set 06"
```
Replacement (GBK "Зоны" + space-pad + preserve id `06`, stays exactly 16 bytes):
```
A7 A9 A7 E0 A7 DF A7 ED 20 20 20 20 20 20 30 36
└─── Зоны ────────────┘ └── pad ───┘ └"06"┘
```
**Write & flash:** this is a **firmware (MCU app) patch**, not an SPI codeplug write. At file offset `0x22C3D` in `rt4d_stock_v3.25_abs_0x08002800.bin` overwrite bytes `[0..7]` = `A7 A9 A7 E0 A7 DF A7 ED`, `[8..13]` = `0x20`, leave `[14..15]` = `30 36`. Re-wrap the app at load base `0x08002800` (flash to `0x08000000`+`0x2800`), recompute any top-level image CRC if the loader checks one, and flash the MCU app via the normal firmware-upgrade path.
**Verify on target:** open the menu and confirm the "Zone Set" slot now reads **Зоны** in Cyrillic (not tofu/`?`). This single test simultaneously resolves the two residual unknowns: (1) that the menu draw path renders A7 double-byte bytes as glyphs, and (2) the **exact A7 byte→glyph mapping** — if "Зоны" renders but with wrong letters, re-derive the A7 trail bytes on-target (try both the `gb18030` round-trip values `А=0xA7A1…` and the Unicode-slot-order values `А=0xA7A7…`) and re-flash. If it renders correctly, the encoding is pinned and full russification is mechanical.
## Full-russification plan
1. **Pin the A7 encoding on-target** via the PoC above. Lock the definitive Cyrillic-char → GBK-byte table before batch work.
2. **Build the patch-table generator** (~40–60 lines Python): read the `0x080253ED..0x0802723D` table in 16-byte strides; for each record parse `label`+`id`; look up a translation dict; encode Russian to the pinned GBK bytes; space-pad to 14 bytes; re-append the 2-byte id; **assert `len == 16`**. Emit `(file_offset, old16, new16)` tuples. Because every patch is length-preserving, no address fix-ups.
3. **Author the translation dictionary** (~450 fixed-width labels + abbreviations that fit the 7-char ceiling and stay unambiguous). Reuse across duplicate labels.
4. **Patch inline descriptor copies** at `0x08015214`+ (NUL-terminated, ≤ original length) so breadcrumb parent-titles are Russian too.
5. **Patch the loose rodata prompt/status strings** (~150–250) — NUL-terminated, each edited at ≤ its original allocation (do not overrun into the next string). These are one-byte-encoding GBK too; abbreviate where Russian is longer.
6. **Recompute image CRC** if present; re-wrap at base `0x08002800`; flash MCU app.
7. **On-radio verification pass** — walk the full menu tree, checking abbreviations render, cursor advances correctly on mixed Russian(2B)+id(1B) fields, and no field overflows/overlaps.
**Tooling to build:** GBK encoder (trivial, `label.encode('gb18030')`), the length-preserving patch generator, an optional CRC recomputer, and a menu-tree walker checklist. **Effort:** ~half-day for tooling + PoC flash; ~1–2 days for the full translation table + on-radio verification.
**Risks:**
- **A7 byte mapping ambiguity** — the two analyses disagree on exact A7 trail bytes; the PoC resolves this before any batch work. Do not skip step 1.
- **A7 glyph slots pruned?** Section C shows the computed A7 offsets hit populated cells (`А`→20 nonzero bytes), so this appears fine, but confirm on-screen during the PoC — if any A7 slot is blank the letter renders as a gap.
- **Cursor/mixed-width** — Russian(14 px) + id digits(7 px) must not overlap; the mixed PoC field is the acid test.
- **Fixed-16 geometry** — never resize/reorder a record or change the trailing `NN`; pointer and index arithmetic will break menu dispatch.
- **Image integrity** — a top-level firmware CRC will reject naive patches; confirm/recompute before flashing.
**Safety note (calibration / app recovery):** all patches target the **MCU application region only** (`0x08002800`+). Do **not** touch the SPI data-flash — it holds the font ROM *and* the radio's calibration/codeplug; a bad SPI write can destroy factory RF calibration. Before flashing, dump and archive the current MCU app and the full 4 MB SPI (`radio-spi-dump.bin` already serves as the SPI baseline) so you can restore. Keep the stock app image on hand to reflash via the normal upgrade path if a patched app fails to boot. Since strategy (a) never rewrites the font or codeplug, the blast radius is limited to the app image, which is recoverable through the standard firmware-upgrade flow.