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

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

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 (А=A7A70x1D00D8, 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–0xFElead 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.

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.