Баузбенд 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
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)
- 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 SPI0x3F0000listsU+0410..U+044F+U+0401/U+0451contiguously (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). - Render supports double-byte? YES.
draw_string @0x08008A50classifies any byte≥0x80as 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. - 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
NNis 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, base0x19C000:index = char − 0x20,addr = base + index*14. - CJK/GBK blitter
0x08008454— 14 px wide, 28-byte cell (0x1C), base0x19E000:
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
- Pin the A7 encoding on-target via the PoC above. Lock the definitive Cyrillic-char → GBK-byte table before batch work.
- Build the patch-table generator (~40–60 lines Python): read the
0x080253ED..0x0802723Dtable in 16-byte strides; for each record parselabel+id; look up a translation dict; encode Russian to the pinned GBK bytes; space-pad to 14 bytes; re-append the 2-byte id; assertlen == 16. Emit(file_offset, old16, new16)tuples. Because every patch is length-preserving, no address fix-ups. - Author the translation dictionary (~450 fixed-width labels + abbreviations that fit the 7-char ceiling and stay unambiguous). Reuse across duplicate labels.
- Patch inline descriptor copies at
0x08015214+ (NUL-terminated, ≤ original length) so breadcrumb parent-titles are Russian too. - 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.
- Recompute image CRC if present; re-wrap at base
0x08002800; flash MCU app. - 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.