Мост к 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
родитель ae36c3b729
Коммит 5f4d207aa5
34 изменённых файлов: 21718 добавлений и 17823 удалений
+3
Просмотреть файл
@@ -5,3 +5,6 @@ __pycache__/
build/
*.spec
.DS_Store
# сборочный мусор
flasher/*.exe
+19 -6
Просмотреть файл
@@ -12,8 +12,12 @@
-**Полный реверс** главного MCU (ARM Cortex-M4F, клон STM32F407) и DMR-баузбенда FM100B (ARM32): карта памяти, периферия, вектора/ISR, serial-протокол, формат codeplug, UI-архитектура.
-**Русификация:** узкий однобайтный CP1251-рендер (патч диспетчера + code-cave в blitter), **свой шрифт 33 заглавные буквы** (13 переиспользуют байты стокового ASCII-шрифта, 20 нарисованы), перевод **485 пунктов меню + ~300 надписей** на заглавный русский. Ребренд версии → **Ru-4D V3.25**.
-**Блюпринт кастомного UI:** точки перехвата главного цикла, «SDK» из вызываемых функций стока (отрисовка/клавиши/радио/DMR/codeplug), C-тулчейн (clang → thumbv7em) — доказано инъекцией своего экрана.
-**Флешеры:** однофайловый веб-флешер (`.html`, WebSerial) и автономный Windows-`.exe` со вшитой прошивкой — прошивка «в одну кнопку».
-**Флешер:** однофайловый веб-флешер (`.html`, WebSerial) со вшитой прошивкой — «в одну кнопку». Автономный `.exe` — в планах.
- ⏸️ **Полная переделка UI** — приостановлена: замена дежурного экрана ломает обработку клавиш (обход преамбулы стокового диспетчера). Возобновление — через *наложение* поверх стока. См. `docs/UI-Architecture.md`.
-**Открыт канал управления DMR-модулем FM100B.** Найден прозрачный мост ПК↔баузбенд (**включение с зажатой клавишей МЕНЮ**), расшифрована контрольная сумма кадров и **проверена на живой рации**. Инструмент — [`tools/fm100b.py`](tools/fm100b.py). Опрос подтвердил версию модуля **V1.2.0.32**, совпавшую с официальным образом.
-**Прошивка разложена на запчасти:** два процессора + внешний SPI = три независимых канала внедрения. Карта — [`docs/firmware-parts.md`](docs/firmware-parts.md).
-**Найден штатный загрузчик ресурсов** (`Unicode Download.exe`, внутри `FontVoicePicture`) — официальный путь замены **шрифтов, голосовых подсказок и картинок** (в т.ч. загрузочного логотипа) без патча MCU.
-**Разобран спектроанализатор REFV DualTachyon** — та самая реализация, которую хвалят пользователи. См. [`docs/refw-spectrum.md`](docs/refw-spectrum.md).
---
@@ -38,7 +42,7 @@
| [`tools/`](tools/) | Флешер (`flash_rt4d.py`) и сборочные скрипты: `build_ru_full.py` (русификация), `glyphs_ru2.py` (шрифт), `patch_fw.py`, `build_ui_home.py` (инъекция UI). |
| [`ui/`](ui/) | `my_ui.c` — кастомный UI-слой на C (компилируется clang'ом под Cortex-M4). |
| [`translations/`](translations/) | JSON-таблицы переводов (меню + rodata) и рабочие списки строк. |
| [`flasher/`](flasher/) | **Готовые флешеры для раздачи:** `Ru-4D_flasher.html` (WebSerial) и `Ru-4D_Flasher.exe`, + генераторы. |
| [`flasher/`](flasher/) | **Флешер для раздачи:** `Ru-4D_flasher.html` (WebSerial, прошивка внутри) + генераторы (`make_flasher.py`, `build_exe.py`). |
| [`backup/`](backup/) | `radio-spi-dump.bin` — полный дамп SPI рации (**калибровка + codeplug**, страховка). |
| [`research/`](research/) | Черновые скрипты разведки (реверс «в процессе»), для истории. |
@@ -46,12 +50,16 @@
## Как прошить рацию
Прошивается **русифицированная V3.25** (`firmware/rt4d_ru_full.bin`). Рацию нужно ввести в режим прошивки: **выключить → включить с зажатой `*`** → подключить USB-C.
Прошивается **русифицированная V3.25** (`firmware/rt4d_ru_full.bin`). Рацию нужно ввести в режим прошивки: **выключить → включить с зажатой тангентой PTT** → подключить USB-C.
1. **Просто (для любого пользователя):** открыть `flasher/Ru-4D_flasher.html` в Chrome/Edge **или** запустить `flasher/Ru-4D_Flasher.exe` → кнопка «Прошить» → выбрать COM-порт. Прошивка вшита внутрь.
1. **Просто (для любого пользователя):** открыть `flasher/Ru-4D_flasher.html` в Chrome/Edge → кнопка «Прошить» → выбрать COM-порт. Прошивка вшита внутрь файла.
2. **Из консоли:** `python tools/flash_rt4d.py COM7 firmware/rt4d_ru_full.bin` (нужен `pyserial`).
> **Не отключайте кабель во время записи.** Драйвер порта — CH343 (на Win10/11 обычно ставится сам). `.exe` не подписан → SmartScreen может предупредить («Подробнее» → «Выполнить»).
> Готового `.exe` в репозитории **нет** — прежняя сборка устарела (советовала клавишу «*»
> вместо тангенты PTT) и удалена. Нормальный прошивальщик будет собран отдельно.
> Сейчас актуальны `Ru-4D_flasher.html` и `tools/flash_rt4d.py`.
> ⚠️ **Не отключайте кабель во время записи.** Драйвер порта — CH343 (на Win10/11 обычно ставится сам).
## Восстановление / безопасность
@@ -69,7 +77,7 @@ python flasher/build_exe.py # -> пересобрать .exe (PyInsta
```
Шрифт правится в `tools/glyphs_ru2.py` (битмапы 6×9), переводы — в `translations/*.json`.
> ⚠️ Сборочные скрипты — с оригинальной dev-машины и содержат абсолютные пути (`C:/Users/.../rt-4d`); при переносе поправьте `ROOT`/пути под новую раскладку каталогов.
> Генераторы флешеров (`make_flasher.py`, `build_exe.py`) вычисляют `ROOT` от своего расположения — переносимы. Остальные сборочные скрипты могут содержать пути с исходной dev-машины.
---
@@ -79,6 +87,11 @@ python flasher/build_exe.py # -> пересобрать .exe (PyInsta
- **[docs/UI-Architecture.md](docs/UI-Architecture.md)** — как переписать весь UI поверх стока: машина экранов, точки перехвата, SDK вызываемых функций, план по фазам.
- **[docs/Russification.md](docs/Russification.md)** — осуществимость и механика русификации.
- **[docs/subsystems/](docs/subsystems/)** — детальные разборы: `main-loop`, `display`, `input`, `radio`, `dmr`, `codeplug`, `main-screen`.
- **[docs/firmware-parts.md](docs/firmware-parts.md)** — **прошивка по запчастям и точки внедрения**: два процессора, карта SPI-ресурсов, три приёма патчинга, дерево меню (181 пункт), где живут MIC Gain / спектр / RX-TX Limit.
- **[docs/baseband-and-audio.md](docs/baseband-and-audio.md)** — управление DMR-баузбендом и аудио-трактом (PA2 — питание УНЧ, PA14 — гейт звука от модуля).
- **[docs/refw-spectrum.md](docs/refw-spectrum.md)** — разбор спектроанализатора кастомной прошивки REFV DualTachyon: как вызывается, из каких функций состоит, что переносимо.
- **[docs/refw-analysis.md](docs/refw-analysis.md)**, **[docs/refw-strings.md](docs/refw-strings.md)** — анализ образа REFV DualTachyon и полная выгрузка его строк/меню.
- **[research/re/](research/re/)** — рабочие заметки по подсистемам (в т.ч. `dmr.md` — таблицы команд FM100B в обе стороны).
## История извлечения прошивки
+2 -2
Просмотреть файл
@@ -479,7 +479,7 @@ Write mechanics (`0x8018b76`): for a normal region, before programming it **eras
The **`0x39`-based firmware-flash protocol** (handshake `[0x39,0x33,0x05,0x10,00]`, erase-trigger `[0x39,0x33,0x05,0x55,00]`, and `0x57 <offHi><offLo> + 1024B` write) with checksum **seed `0x48`** is **not present anywhere in the application binary** — a scan finds no `cmp #0x39` command comparison in the dispatcher (the only `#0x39` compares are the ASCII hex-digit parser at `0x8002e78`). This confirms the `0x39` flasher lives in the **bootloader at `0x08000000..0x08002800`**, which is a separate image not contained in `rt4d_stock_v3.25_abs_0x08002800.bin`. The bootloader is what the CLI's `probe_bootloader` (spamming `0xFF` until it echoes `0xFF`) and `command_handshake` talk to.
**Entering the bootloader from the app:** the app itself never writes internal MCU flash — it only ever reboots via the `NVIC_SystemReset` in `0x801a38c` (the `0x34..0xEE` Close). On reset, execution returns to the bootloader at `0x08000000`, which decides whether to stay in flash mode (based on a key/GPIO check at power-on — the documented "hold `*` at power-on" path — or a magic word left in RAM/backup register) or to jump to the app at `0x08002800` (`SP=0x2000AE48`, `reset=0x08002AC1`). The two firmware-flash speed modes (115200 default vs 256000 requiring `#` held at boot) are also bootloader behavior. Because the app's Close reboots into that bootloader, the practical "enter flash mode" sequence is: open a normal session (`0x34..0x10`), then reboot with hold-key to land in the `0x39` flasher — or power-cycle holding `*`/`#`.
**Entering the bootloader from the app:** the app itself never writes internal MCU flash — it only ever reboots via the `NVIC_SystemReset` in `0x801a38c` (the `0x34..0xEE` Close). On reset, execution returns to the bootloader at `0x08000000`, which decides whether to stay in flash mode (based on a key/GPIO check at power-on — the flash-mode entry (empirically: **power on while holding PTT**) — or a magic word left in RAM/backup register) or to jump to the app at `0x08002800` (`SP=0x2000AE48`, `reset=0x08002AC1`). The two firmware-flash speed modes (115200 default vs 256000 requiring `#` held at boot) are also bootloader behavior. Because the app's Close reboots into that bootloader, the practical "enter flash mode" sequence is: open a normal session (`0x34..0x10`), then reboot with hold-key to land in the `0x39` flasher — or power-cycle holding **PTT**.
### 4.5 MCU ↔ FM100B (DMR baseband) internal UART
@@ -804,4 +804,4 @@ The **4 KB calibration block at SPI offset `0x000000`** is **per-unit factory RF
1. **Read and archive the full 4 MB SPI dump** (`radio-spi-dump.bin` is one such capture) and separately verify the first `0x1000` bytes are non-`0xFF` (a valid calibration block is 100% populated per §6.3).
2. **Never issue a full-chip erase** or a bulk write that spans `0x000000`. The `0x40` region write erases a 4 KB sector at offset 0 — treat it as off-limits unless you are deliberately restoring a verified backup.
3. When modding the **codeplug only**, restrict writes to `0x002000–0x0D6FFF` (settings/channels/zones/contacts/keys). Everything at `0x100000+` is stock, regenerable firmware assets.
4. For **MCU or FM100B firmware** flashing, keep the stock vendor images (`rt4d_stock_v3.25*`, `FM100B_V1.2.0.32_20260130.bin`) on hand for rollback, and confirm you can reach the bootloader (hold `*` at power-on, `0xFF` probe echoes `0xFF`) *before* erasing, so a failed flash is recoverable.
4. For **MCU or FM100B firmware** flashing, keep the stock vendor images (`rt4d_stock_v3.25*`, `FM100B_V1.2.0.32_20260130.bin`) on hand for rollback, and confirm you can reach the bootloader (**power on holding PTT**, `0xFF` probe echoes `0xFF`) *before* erasing, so a failed flash is recoverable.
+150 -150
Просмотреть файл
@@ -1,151 +1,151 @@
## 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.
## 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.
+228
Просмотреть файл
@@ -0,0 +1,228 @@
# RT-4D — управление DMR-баузбендом (чтение/запись) и аудио (вкл/выкл)
Источники: реверс стоковой прошивки V3.25 (`rt4d_stock_v3.25_abs_0x08002800.bin`, база `0x08002800`,
ARM Thumb) + **принципиальная схема `RT4DDLT01 v2.1`** (Xiamen Radtel, 2024-06-04).
---
## 0. Что даёт схема (ключ к обеим задачам)
MCU — **U500, 48 pin** (распиновка LQFP48, F1-совместимая ⇒ AT32F403A/407-класс; подтверждается раскладкой
регистров GPIO, см. §2).
| Пин | Цепь на схеме | Назначение |
|---:|---|---|
| **12** | **AF MUTE4898** | **вкл/выкл УНЧ** → через `R522 (0R)` на **пин 1 (EN)** усилителя `U502 = LN4898` |
| **16 / 17** | **DIG-RXD / DIG-TXD** | UART к DMR-баузбенду FM100B (= **USART3** `0x40004800`) |
| **19** | **DIG POWER-EN** | питание/включение баузбенда |
| **23** | DIG/ANA AF-SW | выбор источника звука: цифра (FM100B) ↔ аналог |
| 21 | DIG/ANA-SW | переключение тракта модуляции |
| 2 / 3 / 4 | SCK1080 / SDA1080 / FM POWER-EN | I²C и питание FM-вещательного приёмника (BK1080) |
| 25–28 | W25Q CS/SCK/DO/DI | SPI-флеш (codeplug/шрифты/калибровка) |
| 32 / 33 | TXD, RXD/PTT | CPS-UART (USART6) и PTT |
Аудио-тракт: `DIG-AFOUT`(баузбенд) / аналог → ключи **BL1555 (U504/U505)** → потенциометр громкости
**VR500 10K** (громкость **аналоговая**, MCU ею не управляет) → `U502 LN4898` → динамик **SPK 16R/1W**
и гнездо гарнитуры `J502/PJ-208B`. Часть цепей (U701, Q700, Q502/Q503) помечены **NC** — не установлены.
---
## 1. Аудио: включение/выключение
### 1.1 Железо
`AF MUTE4898` = **GPIOA, бит 2 (PA2)**`EN` усилителя LN4898. Активный уровень — **высокий = звук есть**.
### 1.2 Регистры (AT32F43x-раскладка GPIO, подтверждена по коду)
`GPIOA = 0x40020000`; `SCR = +0x18` (установить биты), `CLR = +0x28` (сбросить), `TOGR = +0x2C`.
```c
#define GPIOA_SCR (*(volatile unsigned*)0x40020018)
#define GPIOA_CLR (*(volatile unsigned*)0x40020028)
GPIOA_SCR = 0x4; // усилитель ВКЛ (звук есть)
GPIOA_CLR = 0x4; // усилитель ВЫКЛ (тишина)
```
### 1.3 Штатный API прошивки (ref-counted — лучше использовать его)
Счётчик запросов звука — 16-битное слово флагов **`0x20000C6E`**; бит `0x8000` = «звук активен»,
младшие биты — отдельные источники (бип, тон, приём…).
| vaddr | функция | смысл |
|---|---|---|
| `0x0801B574` | `audio_request(u16 mask)` | `flags \|= mask \| 0x8000` — заявка на звук |
| `0x0801A348` | `audio_release(u16 mask)` | `flags &= ~mask`, затем вызывает ↓ |
| `0x0801A824` | `audio_off_if_idle()` | если `flags == 0x8000``flags = 0; GPIOA_CLR = 4` (**выкл**) |
| — | включение | там, где заявка первая: `if (flags == 0) GPIOA_SCR = 4;` (напр. `0x080100DE`, `0x08014D42`) |
Проверенные места: `0x08014D44` (SCR, тракт тона/бипа), `0x0801A836` (CLR, простой),
`0x080100E0` (SCR), `0x08010840` (CLR, последовательность выключения).
> **Как рулить:** для разовых экспериментов достаточно писать в `SCR/CLR`. Для «правильного» поведения
> (чтобы прошивка сама не выключила звук) — использовать `audio_request()/audio_release()`.
### 1.4 Смежное
- **Громкость** — аналоговый потенциометр `VR500`, программно не регулируется. Громкость DMR-голоса
задаётся в самом FM100B: `atc_set_call_spk_vol` (msg `0x02`) @`0x0800760A`.
- **Тон/бип** генерирует **TIM1** (`0x40010000`, CR1.CEN через `set_bit0()` @`0x08021C4C`);
функция бипа — `0x08014CB8` (частота `0x1B8` ok / `0x65C` ошибка).
- **Выбор источника** (цифра/аналог) — `DIG/ANA AF-SW`, пин 23.
---
## 2. Баузбенд FM100B: запись (команды)
Связь — **USART3 `0x40004800`** (пины 16/17), собственный бинарный протокол, полностью независимый
от CPS-протокола (USART6) и от формата codeplug.
### 2.1 Формат кадра
```
68 | cmd | b | sub | CKh CKl | lenH lenL | <payload …len> | 10
```
Полный размер = `len + 9`.
### 2.2 Примитивы отправки (блокирующие, сами качают приём)
| vaddr | функция |
|---|---|
| `0x0801B044` | `fm100b_send1(u8 cmd,u8 b,u8 sub,u8 data,void*resp,u16 timeout)` — 1 байт данных |
| `0x0801B0C4` | `fm100b_send(u8 cmd,u8 b,u8 sub,u16 len,void*resp,const u8*payload,u16 timeout)` |
| `0x08006C9C` | `usart3_tx_buf(const u8*,u16)` — сырая отправка |
| `0x08006CB8` | `usart3_tx_byte(u8)` |
| `0x08003050` | `poll_serial()`**насос**: разбор FM100B + CPS + слив кольца (звать в любом ожидании) |
Таймауты из стока: `0x3E8` (1000) для установки вызова, `0x64` (100) для конфигурации.
### 2.3 Примеры (из стока)
```c
atc_set_radio_id(id) : fm100b_send(0x49,1,1,4,resp,be32(id),0x64); // 0x080074FA (msg 0x2A вариант)
call setup : fm100b_send(0x06,1,type,5,resp,[type,be32(id)],0x3E8);
radio_apply_channel : 0x0801AE9C композит: частота/режим/полоса/мощность/CC/шумодав/ID/CTCSS
```
Полная таблица команд — в [`subsystems/dmr.md`](subsystems/dmr.md) §2 и [`subsystems/radio.md`](subsystems/radio.md) §9.
---
## 3. Баузбенд FM100B: чтение (ответы)
### 3.1 Путь приёма
```
USART3 ISR @0x080205B0 → кольцо 4 КБ
fm100b_rx_parse @0x08018CB0 — ищет кадр 0x68…0x10, проверяет CRC
fm100b_on_frame @0x08006348 — разбор:
status = f[3]; cmd = f[1];
*(u8*)(0x20007476 + cmd) = status; // ← ТАБЛИЦА СТАТУСОВ (193 записи)
if (cmd < 0xC1) jump_table[cmd](); // таблица @0x0800636C
```
### 3.2 Что читаемо
- **Статус любой команды**: `status = *(u8*)(0x20007476 + cmd)` сразу после возврата из `fm100b_send*`.
- **Данные ответа**: раскладываются персональными обработчиками `*Cnf`/`*Ind` из jump-таблицы
`@0x0800636C` (адрес обработчика = `0x0800636C + word[0x0800636C + cmd*4]`).
**104 из 193 команд имеют свой обработчик** (остальные 89 — заглушка `0x08006C26`).
Наиболее «содержательные» (с реальным телом): `cmd 05 @0x0800668C`, `06 @0x080066AA`,
`07 @0x0800671E`, `09 @0x08006816`, `0A @0x08006870`; далее идут короткие переходники к общим обработчикам.
### 3.3 Как получить «сырое» чтение
Штатный парсер сразу диспатчит кадр. Чтобы видеть **любой** ответ целиком, нужно перехватить
`fm100b_on_frame @0x08006348` (или `fm100b_rx_parse @0x08018CB0`) и скопировать кадр в свой буфер —
это открывает полный read-доступ к баузбенду.
---
## 4. Рекомендуемый способ «рулить» баузбендом
Самый практичный вариант для экспериментов — **мост FM100B ↔ ПК**: инъекция в свободный флеш
(`0x08029000+`, см. `UI-Architecture.md`), которая
1. перехватывает `fm100b_on_frame` и отправляет сырые кадры в CPS-UART (USART6),
2. принимает из USART6 команды и вызывает `fm100b_send*`.
Тогда весь протокол баузбенда доступен из скрипта на ПК — можно перебирать команды, читать ответы
и документировать неизвестные (89 «незанятых» cmd — потенциально интересны).
> ⚠️ Безопасность: не трогать калибровочные NV-элементы FM100B (`SPCali_*`) — это заводская трим-настройка
> VCO/PLL/мощности. И не писать «сырую» мощность в обход `radio_apply_channel` — риск для PA.
---
## 5. Перепроверка
Для независимой сверки доступна **REFW beta42** (`rt-4d-refw-beta42.bin`, 251 904 Б = 0x3D800 — полный
образ приложения) — другая, с нуля написанная реализация того же железа: удобно сверять GPIO и команды.
Открытый CPS: <https://github.com/jcalado/rt4d-cps/releases/tag/v0.5.4>.
---
## 6. 🔑 Мост ПК ↔ FM100B — уже реализован в прошивке
Самое важное открытие (найдено через разбор REFW и подтверждено в стоке): рация умеет работать
**прозрачным serial-мостом между ПК и DMR-баузбендом**. Именно так заводская утилита
«DMR Upgrade Tool» шьёт FM100B — через USB рации.
### 6.1 Как это сделано в РЕФВ (REFW beta42) — эталонный поток
Меню «Update DMR Chip» (`0x08008EAC` → UI `0x08005070`), затем:
```c
GPIOB->CLR = 0x2; // PB1 (DIG POWER-EN, пин 19) = LOW → выключить баузбенд
delay_ms(200);
GPIOB->SCR = 0x2; // PB1 = HIGH → включить (холодный ребут FM100B)
for (;;) { // бесконечный мост
pc_to_dmr(); // 0x0801A0EC
dmr_to_pc(); // 0x0801A11C
}
```
| Функция REFW | Роль | Кольцо |
|---|---|---|
| `0x0801A0EC` | ПК → FM100B | 4 КБ @`0x200078E4` (head `0x200078E0`, tail `0x20004F78`) |
| `0x0801A11C` | FM100B → ПК | 1 КБ @`0x200054B4` (head `0x200054AC`, tail `0x200054B0`) |
| `0x0801A094` | байт → USART3 (ждёт TC) | |
| `0x0801ADA8` | байт → USART6 (ПК) | |
| `0x0801A0D4` | `fm100b_tx_buf(buf,len)` | побайтно |
### 6.2 То же самое в стоке — **мост уже встроен**
**Направление ПК → FM100B реализовано в двух местах, оба под флагами:**
```c
// USART6 RX ISR @0x0802061C (байт с ПК)
b = USART6->DR;
ring[0x200082EF + (head & 0xFFF)] = b; // 4 КБ кольцо, head @0x20000C60
if (*(u8*)0x20000B67) // ← ФЛАГ МОСТА
usart3_tx_byte(b); // сразу в баузбенд (0x0802064A)
head++;
// главный цикл @0x080213E8
if (*(u8*)0x20000B66) { // ← ФЛАГ РЕЖИМА (CPS/мост)
cps_framer(); ...
if (tail < head) usart3_tx_byte(ring[...]); // (0x08021406)
}
```
| Адрес | Смысл |
|---|---|
| **`0x20000B67`** | флаг: пересылать байты ПК→FM100B прямо в ISR (он же loopback-флаг в `usart3_tx_byte`) |
| **`0x20000B66`** | флаг режима сессии (ветка CPS/мост в главном цикле) |
| `0x200082EF` | 4 КБ кольцо приёма с ПК, head `0x20000C60` |
| `0x08006CB8` | `usart3_tx_byte()` |
**Практический вывод:** чтобы «рулить бекеном» с компьютера, не нужно писать свой мост —
достаточно перевести рацию в этот режим (пункт меню «Update DMR Chip» / установка флагов),
после чего с ПК можно слать произвольные кадры `68 …` и читать ответы обычным скриптом.
### 6.3 Новые команды FM100B, найденные в REFW
REFW хранит **готовые шаблоны кадров** в флеше и шлёт их `fm100b_tx_buf(buf,len)`:
| Адрес шаблона | Кадр | Назначение |
|---|---|---|
| `0x08020072` | `68 3E 01 01 [CK CK] 00 0F ×15 0F 10` (24 Б) | **cmd `0x3E`** — «Update DMR Chip» (перевод FM100B в режим обновления) |
| `0x0802008A` | `68 42 01 01 [CK CK] 00 01 FF 10` (10 Б) | **cmd `0x42`** |
Команд `0x3E` и `0x42` **не было** в нашем разборе стока — это новые, полученные из REFW.
Отправщик cmd `0x3E``0x0800509C`, вызывается из 4 мест (`0x080064DA`, `0x08008018`,
`0x08011186`, `0x0801129C`).
### 6.4 Проверка версии прошивки DMR (REFW)
`0x08008DE0`+ : REFW читает версию FM100B в `0x20004F74` и сравнивает с `0x01020027`;
если ниже — показывает «Upgrade DMR FW / to v1.2.0.28 / or higher». То есть версия баузбенда
**читается программно** — это готовый пример «чтения» из FM100B.
+314
Просмотреть файл
@@ -0,0 +1,314 @@
# RT-4D: прошивка по запчастям и точки внедрения своего кода
Цель документа — разложить устройство на независимые «детали», для каждой указать
**где лежит, чем читается, чем пишется и как туда внедрить своё**.
Ключевой факт: рация состоит из **двух процессоров** с раздельными прошивками, плюс
внешний SPI-флеш с ресурсами. Это три независимых канала внедрения.
---
## 0. Карта на одном экране
| # | Деталь | Носитель | Размер | Пишется чем | Статус |
|---|---|---|---|---|---|
| 1 | Загрузчик MCU | внутр. flash `0x08000000` | 10 КБ | **ничем** (неприкосновенен) | страховка от кирпича |
| 2 | Прошивка MCU (UI+логика) | внутр. flash `0x08002800` | ~152 КБ | `flash_rt4d.py` | ✅ наш основной цех |
| 3 | Прошивка DMR-модуля FM100B | своя flash в модуле | 1.5 МБ | `DMR Upgrade Tool.exe` / наш мост | ✅ канал открыт |
| 4 | Ресурсы: шрифты, пиньинь, юникод | внешний SPI 4 МБ | ~1 МБ | `Unicode Download.exe` | ✅ найден штатный загрузчик |
| 5 | Голосовые подсказки | внешний SPI | 569 КБ | тот же загрузчик | найден регион |
| 6 | Кодплаг (каналы/контакты/зоны) | внешний SPI | ~100 КБ | штатная CPS | формат менять нельзя |
---
## 1. MCU — ARM Cortex-M4F (клон STM32F407, реально **Artery AT32**)
Опознан по раскладке GPIO: `SCR +0x18` (установить), `CLR +0x28` (сбросить), `TOGR +0x2C`
это Artery, а не ST.
```
0x08000000 ┌────────────────────────┐
│ загрузчик (10 КБ) │ НЕ ТРОГАЕМ — он всегда поднимет рацию
0x08002800 ├────────────────────────┤
│ приложение ~152 КБ │ ← сюда пишем мы
│ 0x08008A50 draw_string│
│ 0x080207DC ui_tick │
│ 0x08028860 наш кейв │
│ 0x08029000 наш C-код │
0x08028000 └────────────────────────┘
SRAM 128 КБ, g_screen @0x200008B3
```
### Как попасть в режим прошивки
**Выключить → включить, удерживая тангенту PTT.**
Протокол загрузчика (реализован в `flash_rt4d.py`):
```
рукопожатие : слать 0xFF, пока не придёт 0xFF
стирание : [39 33 05 10]+CK, затем [39 33 05 55]+CK → ждать ACK 0x06
запись : [57 offHi offLo] + 1024 байт + CK → ждать ACK 0x06
CK = (0x48 + сумма байтов) & 0xFF
```
### Три проверенных приёма внедрения
**а) Точечный патч байта** — самый дешёвый.
Пример (русификация): в `draw_string` стояло `cmp #0x80` — всё, что ≥0x80, считалось
китайским иероглифом шириной 14 px. Меняем на `cmp #0xFF` по адресу `0x08008A6A` — и
диапазон 0x80–0xFE уходит в узкую 7-пиксельную ветку. Один байт открыл кириллицу.
**б) Кодовый кейв** — свободное место после конца кода, куда кладём свою функцию, и
перенаправляем на неё вызов. У нас кейв на `0x08028860`, таблица глиф на `0x08028900`.
**в) Перехват вызова (hook)** — подменяем цель инструкции `bl`.
Две главные точки в `ui_tick_normal @0x080207DC`:
```
0x080207E6 bl → отрисовка экрана (0x0801E1BC)
0x08020816 bl → обработка клавиш (0x0801E6EC)
```
Обе диспетчеризуются по `g_screen @0x200008B3` (значения 0–11) через две tbb-таблицы.
> **Урок, оплаченный кирпичом:** перехватывать надо **поверх** штатного обработчика, а не
> вместо него. Когда мы подменили отрисовку главного экрана целиком, пропала преамбула
> `ui_draw_dispatch` — и клавиши умерли, осталась только тангента. Правильно: вызвать
> оригинал, потом дорисовать своё.
>
> И второй урок: `BATT_READ @0x08010960` — **не** безопасный листовой геттер, вызов из
> хука вешает рацию намертво (обработчик HardFault — пустой бесконечный цикл).
### Свой C-код в прошивке
```bash
clang --target=thumbv7em-none-eabi -mcpu=cortex-m4 -mthumb -Os \
-ffreestanding -fno-builtin -nostdlib -fno-pic -c my.c
ld.lld -Ttext=0x08029000 -e my_draw_router my.o -o my.elf
llvm-objcopy -O binary --only-section=.text my.elf my.bin
```
---
## 2. FM100B — DMR-баузбенд, **отдельный процессор**
Не Cortex-M. Образ `FM100B_V1.2.0.32_20260130.bin` (1 527 808 Б) начинается классической
таблицей векторов ARM32 (`B` + семь `LDR PC,[PC,#0x14]`), обработчики по адресам
`0x0301xxxx` с взведённым битом 0 → **ARMv5T (ARM926-класс), код в Thumb**.
Внутри крутится RTOS: видны имена устройств `rom / flash0 / flash1 / sd / udisk` и
константы MD5.
### Связь MCU ↔ FM100B — USART3 (`0x40004800`)
```
68 | cmd | b | sub | CKhi CKlo | lenHi lenLo | payload… | 10
```
Контрольная сумма — «интернетная» (one's complement) сумма 16-битных BE-слов по всему
кадру, причём **на время расчёта поле CK = 0xFFFF**, результат кладётся BE в `[4..5]`:
```c
u32 s=0; while(len>1){ s += (b[0]<<8)|b[1]; b+=2; len-=2; }
if(len) s += b[0]<<8;
while(s>>16) s = (s&0xFFFF) + (s>>16);
ck = ~s & 0xFFFF;
```
Проверено на живом ответе рации: кадр `68 84 00 00 87 7b 00 00 10` → расчёт даёт `0x877B`. ✅
Функции MCU: `fm100b_send1 @0x0801B044`, `fm100b_send @0x0801B0C4`,
приём `fm100b_rx_parse @0x08018CB0`, диспетчер `fm100b_on_frame @0x08006348`,
насос `poll_serial @0x08003050` (обязателен в любом цикле ожидания).
### Прозрачный мост ПК ↔ FM100B — **включение с зажатой клавишей МЕНЮ**
Найдено в `boot_mode_check @0x08003060`:
```c
if (key == 0x0A) { // МЕНЮ
GPIOB->CLR = 2; delay(200); GPIOB->SCR = 2; // PB1 = питание модуля, перезапуск
for(;;) { bl 0x08019720; // ПК → FM100B
bl 0x08019758; } // FM100B → ПК
}
if (key == 0x0F) baud = 256000; // клавиша '#'
if (key == 0x11) bridge_flag = 1; // SIDE1
```
На экране при этом «Обновление DMR чипа».
Клавиша `#` = 256000 подтверждается официальным changelog CPS V2.01 — независимое совпадение.
Инструмент: **`tools/fm100b.py`** (`ping` / `send` / `raw` / `scan`).
Под WSL порт открывается через `powershell.exe` — WSL2 не пробрасывает COM напрямую.
### Что модуль отвечает живьём
| cmd | ответ | смысл |
|---|---|---|
| `0x25` | `01 02 00 32` | **версия прошивки модуля = V1.2.0.32** |
| `0x24` | `01 00 40 01` | идентификатор/возможности |
| `0x04` | `03` | параметр |
| `0x27`, `0x28` | `00` | статусы |
| `0x01`, `0x03`, `0x0C`, `0x1A` | status 0 | приняты |
| `0x05` | status `0xFF` | отказ (нужен payload) |
| `0x02` | молчит | команды нет |
Версия `01 02 00 32` совпала с именем официального файла `FM100B_V1.2.0.32` — значит
`0x25` это **подтверждённый запрос версии**. REFV ориентируется на более старую V1.2.0.27.
### Направление «модуль → MCU»
Диспетчер `fm100b_on_frame` — таблица 193 смещений с базой `0x0800636C`,
handler = база + запись. Обрабатывается 104 кода, но 94 из них — двухбайтовые заглушки.
**Реальный код только у десяти:** `05, 06, 07, 09, 0A, 25, 3E, 42, 59, 62`.
Разобран `cmd 0x59` — индикация активности приёма, и она же рулит звуком:
```c
payload[0]==1 GPIOA->SCR = 0x4000; // PA14 включить
payload[0]==0 GPIOA->CLR = 0x4000; // PA14 выключить
```
### Семантика из официального changelog
Числа там **шестнадцатеричные** (проверка: `0x33` в таблице отсутствует, `0x51` есть):
| cmd | назначение |
|---|---|
| `0x66` | девиация передачи, широкая/узкая полоса (аналог) |
| `0x67` | компандер (только аналоговые каналы) |
| `0x51` | вокодер, формат 2 — AGC |
| `0x3E` | **обновление прошивки модуля — НЕ СЛАТЬ вслепую** |
Полный словарь имён команд (`ATC_*Req/Cnf/Ind`, ~180 штук) лежит строками прямо в образе
модуля начиная с `0x0CDAE7`: `ATC_ChFreqSetReq`, `ATC_CallProcessReq`, `ATC_RadioIDSetReq`,
`ATC_SMSProcessReq`, `ATC_MICCodecGainSet`, `ATC_SPKCodecGainSet`, `ATC_RssiReadReq`,
`ATC_CalledContactINfoQuery`, `ATC_ChSlotSetReq` и т.д.
---
## 3. Аудио — двухступенчатое
| Линия | Что делает | Как дёрнуть |
|---|---|---|
| **PA2** | питание УНЧ **LN4898 (U502)**, цепь `AF MUTE4898` = вывод 12 MCU | вкл `*(u32*)0x40020018 = 4` / выкл `*(u32*)0x40020028 = 4` |
| **PA14** | гейт звука от DMR-модуля | вкл `*(u32*)0x40020018 = 0x4000` / выкл `0x40020028` |
PA2 подтверждён трижды: принципиальная схема + сток + REFV.
Штатный API со счётчиком ссылок: `audio_request @0x0801B574`, `audio_release @0x0801A348`,
`audio_off_if_idle @0x0801A824`, флаги `0x20000C6E` (бит15 = активен).
**Громкость — аналоговый потенциометр VR500, программно не регулируется.**
---
## 4. Внешний SPI-флеш 4 МБ — ресурсы
Карта занятых регионов (по нашему дампу `radio-spi-dump.bin`):
| Адрес | Размер | Содержимое |
|---|---|---|
| `0x000000` | 20 КБ | настройки/кодплаг |
| `0x0D0000` | 24 КБ | таблица клавиш (видно `"Key 1"`) |
| `0x126000` | 20 КБ | картинки (кандидат на **загрузочный логотип**) |
| `0x14C000` | 45 КБ | индекс/сортировка иероглифов GB2312 |
| **`0x164000`** | 200 КБ | **таблица пиньиня** (китайский ввод) |
| **`0x198000`** | 680 КБ | **шрифты**: ASCII 7×14 @`0x19C000`, CJK 14×14 @`0x19E000` |
| **`0x352000`** | 569 КБ | **голосовые подсказки** (8-бит PCM, тишина = `0x80`) |
| **`0x3F0000`** | 64 КБ | **таблица Unicode↔GB** (начинается `4E 02 4E 04 4E 05`…) |
Формат глифа: **колоночный**, 7 колонок × 14 строк, 2 байта на колонку little-endian,
бит 0 = верхний пиксель. CJK — 14 колонок, 28 байт.
### Штатный загрузчик ресурсов — найден
`Unicode Download.exe` из пакета RT-4D 20251113 внутри называется **`FontVoicePicture`** —
то есть официальный канал заливки **шрифтов, голоса и картинок**. Именно он пишет регионы
`0x164000`, `0x198000`, `0x352000`, `0x3F0000`.
Из официального readme: *«этот инструмент обязателен после обновления прошивки, иначе текст
SMS будет в кракозябрах»* — потому что SMS кодируется Unicode, а рации нужна таблица
соответствия из `0x3F0000`.
⇒ Это готовый путь для замены **загрузочного логотипа** и **своих шрифтов** без патча MCU.
---
## 5. CPS-протокол — контракт совместимости (USART6 `0x40011400`)
Менять **нельзя**, иначе отвалится штатная CPS:
```
0x34/0x10 уведомление · 0x52 чтение SPI · 0x40 и 0x90–0xA5 запись регионов
0x34/0xEE закрытие · контрольная сумма = простая сумма, seed 0
```
Это третий, независимый UART — с кадрами FM100B (`0x68…0x10`) он никак не пересекается.
---
## 6. Каталог прошивок — база для сравнения версий
`.../DMRhub-sale/firmware-catalog/fw/radtel/rt-4d/` — 32 файла, от V3.10 (ноя-2024) до
фев-2026, все версии CPS, плюс `rt-4d_data_lost_fixing.rar`.
Извлечено **7 версий прошивки DMR-модуля**, все ровно по 1 527 808 Б, значит побайтовое
сравнение тривиально:
```
FM100B_V1.2.0.13_20250430 .16_20250620 .18_20250707 .21_20250819
.22_20250916 .24_20251010 .32_20260130 ← стоит в рации
```
Полезное из changelog V3.18: *«оптимизирована связь между DMR-баузбендом и MCU,
оптимизированы некоторые команды связи»* — то есть протокол между версиями менялся.
---
## 7. Что уже сделано и работает
- ✅ Извлечение стока из официального RAR через 4 слоя вложенности
(RAR → ZIP → .NET `Ido_Update` → Intel-HEX в куче управляемых строк `#US`)
- ✅ Полный цикл патч → сборка → прошивка → восстановление
- ✅ Русификация: 483 записи меню + 335 вхождений в rodata + 11 инлайн-строк, 33 глифа
- ✅ Живой двусторонний обмен с DMR-модулем с ПК
- ✅ Аудио: обе линии найдены и подтверждены
## 9. Где какие функции живут (для правки «фишек»)
Дерево меню целиком извлечено: **181 пункт + 304 набора значений** (`menu_strings.json`,
записи по 20 байт: 16 байт метки + позиция). Структура — 7 подменю:
| Подменю | Что внутри (по позициям) |
|---|---|
| **Basic Set** | 01 Radio Name … 31 LCD Contrast, 32 Freq Input, 33 Reverse CH Dir, 34 Carrier LED, **35 RSSI Refresh**, 36 APO, 39 Initialization, 41 Version |
| **Key Define** | 01 Second PTT, 02–05 боковые клавиши, 06–15 «цифра + долгое нажатие» |
| **Analog Set** | 01 SQ Level … **06 MIC Gain, 07 SPK Gain, 08 Glitch TH**, 09 Detect Range, 11–19 DTMF, 21–23 VOX |
| **Digital Set** | 01 Personal ID … 06 SQ Level, **07 MIC Gain, 08 SPK Gain, 09 TX Denoise, 10 RX Denoise**, 11 Contacts Set, 17 Call Log |
| **Channel Set** | 01 DMR Or Analog, **02 RX/TX Limit**, 03 CH Alias, 04 TX Power, 06 TOT, 09–18 аналоговые `(A)`, 19–28 цифровые `(D)` |
| **Message** | 01 New SMS, 02 Inbox, 03 Outbox, 04 Drafts, **07 SMS Format, 08 SMS Font**, 09 SMS Prompt |
| **Zone Set** | зоны |
**Важно:** `MIC Gain` / `SPK Gain` **не спрятаны** — они штатные пункты меню, причём
раздельные для аналога и цифры. Открывать ничего не нужно, можно сразу менять диапазоны
значений или логику.
`SMS Format` в стоке принимает значения **`Hytera / Motorola / Unicode / GBK`** —
то есть выбор формата уже реализован.
### Спектроанализатор
Пунктом меню **не является** — это функция назначаемой клавиши **`Analog Spectrum`**.
Полный список функций клавиш (оттуда же берутся «фишки»):
```
Monitor(Analog) H/L Power Dual Standby TX Priority Scanning
Backlight On-off Roger Beep FM Radio Talkaround Alarm
Freq Detect CTC/DCS Scan Send Single Tone Status Query Remote Monitor
Remote Stun Remote Kill Remote Wake Up Online Check Called Show
RX AM/FM Switch Analog Spectrum SQ Freq Step DA Switch
NOAA Mode Save CH New SMS Jump To SMS Menu Brightness
Analog CH VOX Zone Select Promiscuous Mode Dual Slot On-off Time Slot Switch
Color Code SW DMR Encrypt Off Jump To RX List Jump To Contact Jump To DTMF Sel
```
Из changelog V3.18: точки скана подняты с 81 до 105, три индикатора слева сверху вниз —
**RSSI, Noise, Glitch**. Значит правка спектра = правка этого экрана в MCU
(экран выбирается через `g_screen @0x200008B3`).
### Таблица диапазонов для `RX/TX Limit`
```
18-64 64-136 136-174 174-240 240-320 320-400
400-480 480-560 560-620 840-920 920-1000 МГц
```
⚠️ Таблицы на `0x0801F4D0` и `0x08011218` — это сегментация ВЧ-тракта (VCO/ФАПЧ),
**их патчить нельзя**, это не проверка лимита.
## 8. Ближайшие возможности
1. **Загрузочный логотип** — регион `0x126000` + штатный `FontVoicePicture`.
2. **Диff версий FM100B** — 7 образов одинакового размера, изменения видны сразу.
3. **Выравнивание словаря команд** — сопоставить ~180 имён `ATC_*` из образа модуля
с числовыми кодами (нужна база загрузки образа, указатели смещены).
4. **Своя логика поверх стока** — хук `0x080207E6`, обязательно вызывая оригинал.
+84
Просмотреть файл
@@ -0,0 +1,84 @@
# Разбор кастомной прошивки REFV beta42 (DualTachyon)
Файл: `refw/rt-4d-refw-beta42.bin` — 251 904 Б (`0x3D800`, ровно размер app-региона).
Собственная, написанная **с нуля** реализация прошивки RT-4D. Используется здесь как
**независимая перепроверка** наших выводов по железу.
## 1. Структура образа
| Параметр | REFV beta42 | Сток V3.25 |
|---|---|---|
| База загрузки | `0x08002800` | `0x08002800` |
| Начальный SP | `0x2000BFF0` | `0x2000AE48` |
| Reset | `0x080029E5` | `0x08002AC1` |
| Объём кода | ~137 КБ (до `0x21AAF`), дальше `0xFF` | ~152 КБ |
| Таблица векторов | минимальная (почти все → `0x080029E1/E3`) | полная, 81 обработчик |
Брендинг: `"RT-4D REFW v0.42"` @`0x0801E83A`, `"Dual Tachyon"` @`0x0801E84B`.
## 2. Перепроверка железа — совпало полностью ✅
Перепись периферии REFV (по literal-пулам) совпадает с нашим разбором стока:
| Периферия | Ссылок в REFV | Назначение (наш вывод по стоку) |
|---|---:|---|
| GPIOA `0x40020000` | 54 | основные линии управления |
| RCC `0x40023800` | 27 | тактирование |
| ADC1 `0x40012000` | 21 | батарея |
| GPIOB / GPIOF / GPIOC | 19 / 14 / 7 | периферийные линии |
| TIM1 `0x40010000` | 14 | генерация тона/бипа |
| SPI2 `0x40003800` | 10 | SPI-флеш |
| **USART6** `0x40011400` | 9 | **CPS-линк** |
| TIM9 `0x40014000` | 9 | таймер |
| DAC `0x40007400` | 6 | аудио-тракт |
| **USART3** `0x40004800` | 6 | **линк к DMR-баузбенду FM100B** |
**Совпадения по конкретным битам GPIO:**
| Бит | REFV | Сток | Смысл |
|---|---|---|---|
| **GPIOA bit2** | `movs r2,#4; str r2,[GPIOA+0x18]` @`0x08003FCC` | SCR@`0x08014D44`, CLR@`0x0801A836` | **усилитель звука (AF MUTE4898 → LN4898 EN)** |
| GPIOA bit10 | CLR@`0x080064F6`, SCR@`0x08006516` | CLR@`0x080047B8`, SCR@`0x080047C8` | band-select / PA |
| GPIOA bit13/bit14 | активно используются (в т.ч. TOGR) | то же | служебные линии |
⇒ Вывод по аудио (**GPIOA бит 2 = вкл/выкл УНЧ**) подтверждён двумя независимыми прошивками
и принципиальной схемой. Регистры AT32: `SCR +0x18` (вкл), `CLR +0x28` (выкл), `TOGR +0x2C`.
## 3. Протокол баузбенда
REFV работает с FM100B через тот же **USART3** и то же обрамление кадра (`0x68 …`):
immediate `#0x68` встречается в 12+ местах построения пакетов.
Реализован аккуратный HAL поверх USART:
| vaddr (REFV) | роль |
|---|---|
| `0x08003D30` | конфигурация UART |
| `0x08003E56` | чтение DR |
| `0x08003E5C` | проверка флага статуса (маски `0x20` RXNE, `0x80` TC) |
| `0x080199EA`, `0x0801A056`, `0x0801A0AE` | обёртки приёма/передачи для FM100B |
## 4. Что интересного есть в REFV (по строкам)
- **Обновление прошивки самого DMR-модуля**: `"Upgrade DMR FW"`, `"Update DMR Chip"`,
`"Initialising DMR"`, `"DMR FW ERROR!"`, `"DMR FW returned "` — т.е. реализован протокол
прошивки FM100B по USART3 (самая глубокая форма «рулить бекеном»).
- **Talker Alias** (`"Talker Alias On/Off"`), `"Channel Alias"`.
- **Зоны**: `"Zone Setup"`, `"Zone selected"`, `"Zone is empty!"`, `"not in zone mode"`, `"Zone- CH-"`.
- **Скан**: `"Scan Start/End"`, `"Scan Duration/Dwell/Return/Continue"`, `"channels to scan"`.
- **Хоткеи**: `"Extra Hotkey"`, `"0 Hotkey"``"9 Hotkey"`.
- **Мониторинг/сервис**: `"Monitor success!/failed!"`, `"Being Monitored"`, `"Check Failed"`,
`"Command Failed"`, `"repeater failed"`, `"Calibration OK!"`, `"Calibration"`.
- **Версии**: `"Main Firmware"`, `"DMR Firmware"`, `"Flash Type"` — отображение версий обеих
прошивок и типа флеш-микросхемы.
- Прочее: `"Dual Standby On/Off"`, `"Dual Display"`, `"DMR Remote Kill/Stun"`,
`"DMR Time Slot: 1/2"`, `"DMR Encrypt: Off"`, `"Not DMR Channel!"`, `"DMR compatible"`.
## 5. Как это использовать
1. **Верификация** — любой наш вывод по железу можно проверить, поискав тот же регистр/бит в REFV.
2. **Протокол FM100B** — у REFV есть код прошивки баузбенда; при разборе команд обновления
можно получить полный список сервисных команд FM100B.
3. **Ориентир по фичам** — список выше показывает, что реально достижимо на этом железе.
> REFV распространяется только бинарём (исходников нет). Здесь он используется исключительно
> для сверки/исследования собственной работы.
+134
Просмотреть файл
@@ -0,0 +1,134 @@
# Спектроанализатор REFV (DualTachyon) — где он лежит
Разбор по образу `refw/rt-4d-refw-beta42.bin`
(251 904 Б, md5 `c6ab8306b5512b21b1f49fda8df2b1f5`, база загрузки `0x08002800`).
Именно эту реализацию хвалят пользователи. Ниже — как до неё добраться и из чего она состоит.
---
## 1. Как спектр вызывается
Спектр **не является пунктом меню**. Это **функция назначаемой клавиши № 22**
`Analog Spectrum` в списке функций горячих клавиш (`0x080213A9`, записи по 16 байт).
> В стоке та же функция есть под номером **21** — REFV сдвинул нумерацию, вставив
> `Bandwidth Toggle` на позицию 15.
Диспетчер функций клавиш: **`TBH @0x080094EC`**, 45 ветвей (`cmp #44` перед таблицей),
таблица с `0x080094F0`. Ветка 22 → **`0x08009CAA`**.
## 2. Точка входа `0x08009CAA`
```c
if (mode == 3 || mode == 4) return; // в этих режимах спектр запрещён
bl 0x08010ADC; // подготовка
// [area*5 + 0x84] — режим области:
// 0=Frequency, 1=Channel, 2=Zone
if (area_mode == 2) bl 0x08015318; // ← спектр ПО ЗОНЕ
else bl 0x080139E4; // ← ОБЫЧНЫЙ спектр
goto 0x08009BC0; // общий выход
```
Это ровно совпадает с официальным описанием REFV: «переключитесь в режим VFO или канала,
если нужен обычный спектр; из зоны он сканирует эту зону».
## 3. Обычный спектр — `0x080139E4` (инициализация)
```
0x080139E4 push {r4-r8,lr}
0x080139E8 bl 0x08014888 ; подготовка подсистемы
... ; сброс флагов состояния
0x08013A2C mla r2, #0x30, chan, r3 ; запись канала = 48 байт
0x08013A30 ldr [r2+5] -> [r4+5] ; копирование частоты RX/TX
0x08013A38 ldr [r2+9] -> [r4+9] ; в рабочий VFO
... ; полоса/режим: bfi в [r4], [r4+4], [r4+0xe]
0x08013A86 bl 0x08011164 ; применить настройки ВЧ-тракта
0x08013AA6 bl 0x080136EC ; подготовка экрана
0x08013ABA bl 0x08013944 ; отрисовка шапки
0x08013AC8 bl 0x08015978 ; печать числа (r1=2 разряда)
0x08013AF8 ite eq / 'W' : 'N' ; метка полосы Wide/Narrow
0x08013B1C bl 0x0801C62C ; запуск
0x08013B22 movs r2,#0x0B
0x08013B24 strb r2,[0x200009B0] ; ← ЭКРАН = 11
0x08013B26 pop
```
**Ключевой вывод: спектр — это экран № 11.** Архитектура та же, что в стоке
(`g_screen`, значения 0…11), только переменная в REFV живёт по адресу **`0x200009B0`**.
## 4. Модуль спектра целиком
| Адрес | Роль |
|---|---|
| `0x08009CAA` | точка входа с горячей клавиши, выбор обычный/зонный |
| **`0x080139E4`** | **инициализация обычного спектра**, ставит `экран = 11` |
| **`0x08013BA4`** | **тик**: автомат на 4 состояния, счётчик `[база+0x3E] & 3` |
| **`0x08013BFC`** | **обработчик клавиш** спектра, `cmp r0,#0x10` + `TBH @0x08013C20` (17 клавиш) |
| `0x08013598` | шаг развёртки (состояние 1) |
| `0x08012FA0` | рабочий обработчик (состояния 2 и 3) |
| `0x0801307C` | ветка при `состояние > 1` |
| `0x080135E8` | помощник (вызывается из `0x0801449C`) |
| `0x080136EC`, `0x08013944`, `0x08015978` | подготовка экрана, шапка, печать чисел |
| `0x08014888` | инициализация подсистемы |
| **`0x08015318`** | **спектр по зоне** — отдельная реализация |
### Автомат тика `0x08013BA4`
```c
state = (state + 1) & 3; // [база+0x3E]
switch (state) {
case 1: flags[1]=flags[2]=0; bl 0x08013598; break; // шаг развёртки
case 2: flag=0; goto 0x08012FA0; // обработка
case 3: flag=1; goto 0x08012FA0;
default: goto 0x0800A57C; // пометить перерисовку
}
```
Развёртка размазана по тикам главного цикла — экран не блокируется, поэтому спектр
и ощущается отзывчивым.
### Спектр по зоне `0x08015318`
Перебирает **200 каналов** зоны (`cmp r6,#0xC8`), запись зоны — **0x1BD байт**:
```c
for (i = 0; i < 200; i++) {
ch = zone[i].channel; // halfword, отбрасывается если >= 0x400
if (!(zone.scanlist_bitmap[i>>3] >> (i&7) & 1)) continue; // битовая карта scan-list
read_channel(sp, 0x30*ch + 0x4000); // запись канала 48 байт из SPI
// разбор полей: частота ubfx(…,8,12), CTCSS, полоса, режим
// укладка в массив с шагом 28 байт
}
```
Отсюда и фича «зонный спектр показывает живую информацию о субтонах».
## 5. Прочие детали
- **Перерисовка**: `0x0800A5FC``TBB` на 12 экранов (`cmp #0x0B`), каждая ветка делает
`memset(флаги_экрана, 1, N)`. Для экрана 11 — 16 элементов по адресу `0x200000A9`.
- **Идиом связки экранов** встречается в 6 местах:
`if (screen == 11 || (prev == 11 && (screen & ~4) == 8))` — экраны 8 и 12
накладываются поверх спектра (меню/подсказки).
## 6. Что даёт REFV сверх стока (полная таблица UI, 338 записей)
Новые функции горячих клавиш: `Bandwidth Toggle`, `Talker Alias`, `RX AM/FM/**SSB** SW`,
`DMR DCDM Switch`, `DMR TS/CC Switch`, `DMR Key Select`, `Next Zone`, `DTMF List`,
`Freq Monitor`, `Last Call Info`, `Address Book`, `Contacts List`.
Новое в меню: верхнеуровневое **`Extra`**, `Extra hotkey` + `0…9 Hotkey`,
`Edit Scan List` в зонах, `SMS Templates`, `Time Calibrate`, `AM/FM/SSB RX` в канале,
`Last Channel`, `Skip Scan`, `Radio Info`, `Flash Type`.
## 7. Как это перенести к нам
Прямой перенос бинарного кода невозможен — REFV написан с нуля, у него своя карта
памяти и свои структуры (запись канала 48 Б, запись зоны 0x1BD Б, экран в `0x200009B0`).
Переносится **архитектура**:
1. Спектр — отдельный экран в общей диспетчеризации, а не модальный цикл.
2. Развёртка разбита на состояния по тикам, поэтому UI не подвисает.
3. Вход — функция горячей клавиши, а не пункт меню.
4. Отрисовка перерисовывает только помеченные элементы (массив флагов).
В нашей прошивке всё это ложится на уже найденные точки: хук `0x080207E6` (отрисовка) и
`0x08020816` (клавиши) в `ui_tick_normal @0x080207DC`, диспетчеризация по
`g_screen @0x200008B3`. Обязательно **вызывая оригинальный обработчик** — см.
`docs/firmware-parts.md`.
+509
Просмотреть файл
@@ -0,0 +1,509 @@
# REFV beta42 — полный дамп таблиц строк
## Главная таблица (меню + значения опций) (461 записей)
- значение @0x08020BF0: `hift Main DM`
- значение @0x08020C00: `R ID Custom`
- значение @0x08020C10: `DMR ID FM`
- значение @0x08020C20: ` AM`
- значение @0x08020C30: ` SSB`
- значение @0x08020C40: ` 6 digit`
- значение @0x08020C50: `s 8 digit`
- значение @0x08020C60: `s One Sta`
- значение @0x08020C70: `tion All Ran`
- значение @0x08020C80: `ge Frequen`
- значение @0x08020C90: `cy Mode Channel`
- значение @0x08020CA0: ` Mode Impolit`
- значение @0x08020CB0: `e Carrier`
- значение @0x08020CC0: ` Match CC Matc`
- значение @0x08020CD0: `h Slot 1`
- значение @0x08020CE0: ` Slot 2`
- значение @0x08020CF0: ` Low`
- значение @0x08020D00: ` High`
- значение @0x08020D10: ` Off`
- значение @0x08020D20: ` Carrier`
- значение @0x08020D30: ` Match CTCSS/D`
- значение @0x08020D40: `CS Match Off`
- значение @0x08020D50: ` 1`
- значение @0x08020D60: ` 2`
- значение @0x08020D70: ` 3`
- значение @0x08020D80: ` 4`
- значение @0x08020D90: ` 5`
- значение @0x08020DA0: ` 6`
- значение @0x08020DB0: ` 7`
- значение @0x08020DC0: ` 8`
- значение @0x08020DD0: ` Wide`
- значение @0x08020DE0: ` Narrow`
- значение @0x08020DF0: ` Standar`
- значение @0x08020E00: `d Encrypt`
- значение @0x08020E10: ` 1 Encrypt`
- значение @0x08020E20: ` 2 Encrypt`
- значение @0x08020E30: ` 3 Mute Co`
- значение @0x08020E40: `de ARC`
**меню @0x08020E50** (2 пунктов):
- `28` AES 1
- `56` AES 2
- значение @0x08020E70: ` Private`
- значение @0x08020E80: ` Group`
- значение @0x08020E90: ` Off`
- значение @0x08020EA0: ` TX Star`
- значение @0x08020EB0: `t TX End`
- значение @0x08020EC0: ` TX Star`
- значение @0x08020ED0: `t & End 30 ms`
- значение @0x08020EE0: ` 40 ms`
- значение @0x08020EF0: ` 50 ms`
- значение @0x08020F00: ` 60 ms`
- значение @0x08020F10: ` 70 ms`
- значение @0x08020F20: ` 80 ms`
- значение @0x08020F30: ` 90 ms`
- значение @0x08020F40: ` 100 ms`
- значение @0x08020F50: ` 110 ms`
- значение @0x08020F60: ` 120 ms`
- значение @0x08020F70: ` 130 ms`
- значение @0x08020F80: ` 140 ms`
- значение @0x08020F90: ` 150 ms`
- значение @0x08020FA0: ` 160 ms`
- значение @0x08020FB0: ` 170 ms`
- значение @0x08020FC0: ` 180 ms`
- значение @0x08020FD0: ` 190 ms`
- значение @0x08020FE0: ` 200 ms`
- значение @0x08020FF0: ` 210 ms`
- значение @0x08021000: ` 220 ms`
- значение @0x08021010: ` 230 ms`
- значение @0x08021020: ` 240 ms`
- значение @0x08021030: ` 250 ms`
- значение @0x08021040: ` 260 ms`
- значение @0x08021050: ` 270 ms`
- значение @0x08021060: ` 280 ms`
- значение @0x08021070: ` 290 ms`
- значение @0x08021080: ` 300 ms`
- значение @0x08021090: ` 310 ms`
- значение @0x080210A0: ` 320 ms`
- значение @0x080210B0: ` 330 ms`
- значение @0x080210C0: ` 340 ms`
- значение @0x080210D0: ` 350 ms`
- значение @0x080210E0: ` 360 ms`
- значение @0x080210F0: ` 370 ms`
- значение @0x08021100: ` 380 ms`
- значение @0x08021110: ` 390 ms`
- значение @0x08021120: ` 400 ms`
- значение @0x08021130: ` 0 ms`
- значение @0x08021140: ` 100 ms`
- значение @0x08021150: ` 200 ms`
- значение @0x08021160: ` 300 ms`
- значение @0x08021170: ` 400 ms`
- значение @0x08021180: ` 500 ms`
- значение @0x08021190: ` 600 ms`
- значение @0x080211A0: ` 700 ms`
- значение @0x080211B0: ` 800 ms`
- значение @0x080211C0: ` 900 ms`
- значение @0x080211D0: ` 1000 ms`
- значение @0x080211E0: ` 1100 ms`
- значение @0x080211F0: ` 1200 ms`
- значение @0x08021200: ` 1300 ms`
- значение @0x08021210: ` 1400 ms`
- значение @0x08021220: ` 1500 ms`
- значение @0x08021230: ` 1600 ms`
- значение @0x08021240: ` 1700 ms`
- значение @0x08021250: ` 1800 ms`
- значение @0x08021260: ` 1900 ms`
- значение @0x08021270: ` 2000 ms`
- значение @0x08021280: ` 18-64`
**меню @0x08021290** (12 пунктов):
- `36` MHz 64-1
- `74` MHz 136-1
- `40` MHz 174-2
- `20` MHz 240-3
- `00` MHz 320-4
- `80` MHz 400-4
- `60` MHz 480-5
- `20` MHz 560-6
- `20` MHz 840-9
- `00` MHz 920-1
- `40` 0MHz <2
- `40` MHz >2
- значение @0x08021350: ` MHz Off`
- значение @0x08021360: ` Roger B`
- значение @0x08021370: `eep 1 Roger B`
- значение @0x08021380: `eep 2 Send Ra`
- значение @0x08021390: `dio Name None`
- значение @0x080213A0: ` Monitor`
- значение @0x080213B0: `(Analog) H/L Pow`
- значение @0x080213C0: `er Dual St`
- значение @0x080213D0: `andby TX Prio`
- значение @0x080213E0: `rity Scannin`
- значение @0x080213F0: `g Backlig`
- значение @0x08021400: `ht SwitchRoger B`
- значение @0x08021410: `eep FM Radi`
- значение @0x08021420: `o Talkaro`
- значение @0x08021430: `und Alarm`
- значение @0x08021440: ` Freq De`
- значение @0x08021450: `tect CTCSS/D`
- значение @0x08021460: `CS Scan Send Si`
- значение @0x08021470: `ngle ToneStatus`
- значение @0x08021480: `Query Remote`
- значение @0x08021490: `Monitor Bandwid`
- значение @0x080214A0: `th ToggleRemote`
- значение @0x080214B0: `Stun Remote`
- значение @0x080214C0: `Kill Remote`
- значение @0x080214D0: `Wake Up Online`
- значение @0x080214E0: `Check Talker`
- значение @0x080214F0: `Alias RX AM/F`
- значение @0x08021500: `M/SSB SW Analog`
- значение @0x08021510: `Spectrum SQ`
- значение @0x08021520: ` Frequen`
- значение @0x08021530: `cy Step DA Swit`
- значение @0x08021540: `ch(VFO) NOAA Mo`
- значение @0x08021550: `de Save Ch`
- значение @0x08021560: `annel New SMS`
- значение @0x08021570: ` Jump To`
- значение @0x08021580: ` SMS MenuBrightn`
- значение @0x08021590: `ess Analog`
- значение @0x080215A0: `VOX Zone Se`
- значение @0x080215B0: `lect Promisc`
- значение @0x080215C0: `uous ModeDMR DCD`
- значение @0x080215D0: `M Switch DMR TS`
- значение @0x080215E0: `Switch DMR CC`
- значение @0x080215F0: `Switch DMR Key`
- значение @0x08021600: ` Select Jump To`
- значение @0x08021610: ` RX List Address`
- значение @0x08021620: ` Book Contact`
- значение @0x08021630: `s List Next Zo`
- значение @0x08021640: `ne DTMF Li`
- значение @0x08021650: `st Freq Mo`
- значение @0x08021660: `nitor Last Ca`
- значение @0x08021670: `ll Info Unicode`
- значение @0x08021680: ` GBK`
- значение @0x08021690: ` Hytera`
- значение @0x080216A0: ` Motorol`
- значение @0x080216B0: `a Cancel`
- значение @0x080216C0: ` Confirm`
- значение @0x080216D0: ` 0.42 Du`
- значение @0x080216E0: `alTachyonShow Ch`
- значение @0x080216F0: `annel No.Show Fr`
- значение @0x08021700: `equency Show Al`
- значение @0x08021710: `ias Frequen`
- значение @0x08021720: `cy Mode Channel`
- значение @0x08021730: ` Mode Zone Mo`
- значение @0x08021740: `de Area A`
- значение @0x08021750: ` Main Ar`
- значение @0x08021760: `ea Local A`
- значение @0x08021770: `larm Remote`
- значение @0x08021780: `Alarm Local +`
- значение @0x08021790: ` Remote Origina`
- значение @0x080217A0: `l ChannelCurrent`
- значение @0x080217B0: ` Channel Last Ch`
- значение @0x080217C0: `annel Off`
- значение @0x080217D0: ` 1 : 1`
- значение @0x080217E0: ` 1 : 2`
- значение @0x080217F0: ` 1 : 3`
- значение @0x08021800: ` Off`
- значение @0x08021810: ` Talkaro`
- значение @0x08021820: `und Invert`
- значение @0x08021830: `FrequencyOff`
- значение @0x08021840: ` 5 s`
- значение @0x08021850: ` 10 s`
- значение @0x08021860: ` 15 s`
- значение @0x08021870: ` 30 s`
- значение @0x08021880: ` 45 s`
- значение @0x08021890: ` 60 s`
- значение @0x080218A0: ` 75 s`
- значение @0x080218B0: ` 90 s`
- значение @0x080218C0: ` 105 s`
- значение @0x080218D0: ` 120 s`
- значение @0x080218E0: ` 135 s`
- значение @0x080218F0: ` 150 s`
- значение @0x08021900: ` 165 s`
- значение @0x08021910: ` 180 s`
- значение @0x08021920: ` 195 s`
- значение @0x08021930: ` 210 s`
- значение @0x08021940: ` 225 s`
- значение @0x08021950: ` 240 s`
- значение @0x08021960: ` 255 s`
- значение @0x08021970: ` 270 s`
- значение @0x08021980: ` 285 s`
- значение @0x08021990: ` 300 s`
- значение @0x080219A0: ` 315 s`
- значение @0x080219B0: ` 330 s`
- значение @0x080219C0: ` 345 s`
- значение @0x080219D0: ` 360 s`
- значение @0x080219E0: ` 375 s`
- значение @0x080219F0: ` 390 s`
- значение @0x08021A00: ` 405 s`
- значение @0x08021A10: ` 420 s`
- значение @0x08021A20: ` 435 s`
- значение @0x08021A30: ` 450 s`
- значение @0x08021A40: ` 465 s`
- значение @0x08021A50: ` 480 s`
- значение @0x08021A60: ` 495 s`
- значение @0x08021A70: ` 510 s`
- значение @0x08021A80: ` 525 s`
- значение @0x08021A90: ` 540 s`
- значение @0x08021AA0: ` 555 s`
- значение @0x08021AB0: ` 570 s`
- значение @0x08021AC0: ` 585 s`
- значение @0x08021AD0: ` 600 s`
- значение @0x08021AE0: ` 0.25 kH`
- значение @0x08021AF0: `z 1.25 kH`
- значение @0x08021B00: `z 2.5 kHz`
- значение @0x08021B10: ` 5 kHz`
- значение @0x08021B20: ` 6.25 kH`
- значение @0x08021B30: `z 10 kHz`
- значение @0x08021B40: ` 12.5 kH`
- значение @0x08021B50: `z 20 kHz`
- значение @0x08021B60: ` 25 kHz`
- значение @0x08021B70: ` 50 kHz`
- значение @0x08021B80: ` 100 kHz`
- значение @0x08021B90: ` 500 kHz`
- значение @0x08021BA0: ` 1 MHz`
- значение @0x08021BB0: ` 5 MHz`
- значение @0x08021BC0: ` Edit`
- значение @0x08021BD0: ` Busy`
- значение @0x08021BE0: ` Off`
- значение @0x08021BF0: ` On`
- значение @0x08021C00: ` Contact`
- значение @0x08021C10: `s 01Dial No`
- значение @0x08021C20: `. 02Send`
- значение @0x08021C30: ` 01Save`
- значение @0x08021C40: ` 02Send`
- значение @0x08021C50: ` 01Save`
- значение @0x08021C60: ` 02Delete`
- значение @0x08021C70: ` 03Resend`
- значение @0x08021C80: ` 01Forward`
- значение @0x08021C90: ` 02Delete`
- значение @0x08021CA0: ` 03Reply`
- значение @0x08021CB0: ` 01Forward`
- значение @0x08021CC0: ` 02Delete`
- значение @0x08021CD0: ` 03Edit Na`
- значение @0x08021CE0: `me 01Edit Ty`
- значение @0x08021CF0: `pe 02Edit ID`
- значение @0x08021D00: ` 03Save`
- значение @0x08021D10: ` 04Delete`
- значение @0x08021D20: ` 05Edit Fr`
- значение @0x08021D30: `eq 01Set As`
- значение @0x08021D40: `Current02Edit Na`
- значение @0x08021D50: `me 01Set As`
- значение @0x08021D60: `Current02Send`
- значение @0x08021D70: ` 01Save`
- значение @0x08021D80: ` 02Edit Na`
- значение @0x08021D90: `me 01Edit Ty`
- значение @0x08021DA0: `pe 02Edit Ke`
- значение @0x08021DB0: `y 03Save`
- значение @0x08021DC0: ` 04Edit Na`
- значение @0x08021DD0: `me 01Edit Me`
- значение @0x08021DE0: `mber 02Save`
- значение @0x08021DF0: ` 03Clear`
- значение @0x08021E00: ` 04Edit Na`
- значение @0x08021E10: `me 01Edit Ty`
- значение @0x08021E20: `pe 02Edit ID`
- значение @0x08021E30: ` 03Save`
- значение @0x08021E40: ` 04Edit Na`
- значение @0x08021E50: `me 01Edit Ch`
- значение @0x08021E60: `annels 02Edit Sc`
- значение @0x08021E70: `an List03Contact`
- значение @0x08021E80: `s List 01New Con`
- значение @0x08021E90: `tact 02Work Mo`
- значение @0x08021EA0: `de 01RX Stan`
- значение @0x08021EB0: `dby 02Scan Mo`
- значение @0x08021EC0: `de 03Area`
- значение @0x08021ED0: ` 04Channel`
- значение @0x08021EE0: ` 05New SMS`
- значение @0x08021EF0: ` 01Inbox`
- значение @0x08021F00: ` 02Outbox`
- значение @0x08021F10: ` 03Drafts`
- значение @0x08021F20: ` 04SMS Tem`
- значение @0x08021F30: `plates 05Clear A`
- значение @0x08021F40: `ll SMS 06SMS For`
- значение @0x08021F50: `mat 07SMS Fon`
- значение @0x08021F60: `t 08SMS Pro`
- значение @0x08021F70: `mpt 09DMR DCD`
- значение @0x08021F80: `M 01DMR Slo`
- значение @0x08021F90: `t 02Color C`
- значение @0x08021FA0: `ode 03Contact`
- значение @0x08021FB0: `s 04RX Grou`
- значение @0x08021FC0: `p 05Encrypt`
- значение @0x08021FD0: `ion 06Busy Lo`
- значение @0x08021FE0: `ck 07TX Powe`
- значение @0x08021FF0: `r 08Scan Li`
- значение @0x08022000: `st 09TOT`
- значение @0x08022010: ` 10Channel`
- значение @0x08022020: ` Alias 11Offset`
- значение @0x08022030: `Freq 12Set RX`
- значение @0x08022040: `Freq 13Set TX`
- значение @0x08022050: `Freq 14Promisc`
- значение @0x08022060: `uous 15Custom`
- значение @0x08022070: `DMR ID 16DMR ID`
- значение @0x08022080: `Select 17RX/TX L`
- значение @0x08022090: `imit 18CTCSS/D`
- значение @0x080220A0: `CS 01RX CTCS`
- значение @0x080220B0: `S/DCS 02TX CTCS`
- значение @0x080220C0: `S/DCS 03Set RX`
- значение @0x080220D0: `Freq 04Set TX`
- значение @0x080220E0: `Freq 05DCS Enc`
- значение @0x080220F0: `rypt 06Mute Co`
- значение @0x08022100: `de 07Band Wi`
- значение @0x08022110: `dth 08Tail To`
- значение @0x08022120: `ne 09Scrambl`
- значение @0x08022130: `er 10Busy Lo`
- значение @0x08022140: `ck 11TX Powe`
- значение @0x08022150: `r 12Skip Sc`
- значение @0x08022160: `an 13TOT`
- значение @0x08022170: ` 14Channel`
- значение @0x08022180: ` Alias 15Offset`
- значение @0x08022190: `Freq 16AM/FM/S`
- значение @0x080221A0: `SB RX 17RX/TX L`
- значение @0x080221B0: `imit 18Radio D`
- значение @0x080221C0: `MR ID 01Start T`
- значение @0x080221D0: `one 02End Ton`
- значение @0x080221E0: `e 03Group H`
- значение @0x080221F0: `old 04Single`
- значение @0x08022200: `Hold 05SQ Leve`
- значение @0x08022210: `l 06MIC Gai`
- значение @0x08022220: `n 07SPK Gai`
- значение @0x08022230: `n 08TX Deno`
- значение @0x08022240: `ise 09RX Deno`
- значение @0x08022250: `ise 10Contact`
- значение @0x08022260: `s Setup11Group L`
- значение @0x08022270: `ist Set12Encrypt`
- значение @0x08022280: `ion Set13Send DT`
- значение @0x08022290: `MF 14Caller`
- значение @0x080222A0: `Keep 15Call Lo`
- значение @0x080222B0: `g 16Clear A`
- значение @0x080222C0: `ll Log 17Address`
- значение @0x080222D0: ` Book 18SQ Leve`
- значение @0x080222E0: `l 01TX Star`
- значение @0x080222F0: `t Tone 02TX End`
- значение @0x08022300: `Tone 03Single`
- значение @0x08022310: `Tone 04Tone Ti`
- значение @0x08022320: `mer 05MIC Gai`
- значение @0x08022330: `n 06SPK Gai`
- значение @0x08022340: `n 07Glitch`
- значение @0x08022350: `TH 08Detect`
- значение @0x08022360: `Range 09Repeate`
- значение @0x08022370: `r Delay10DTMF De`
- значение @0x08022380: `lay 11DTMF In`
- значение @0x08022390: `terval 12DTMF Du`
- значение @0x080223A0: `ration 13DTMF Mo`
- значение @0x080223B0: `de 14DTMF Li`
- значение @0x080223C0: `st 15DTMF Di`
- значение @0x080223D0: `splay 16DTMF TX`
- значение @0x080223E0: ` Gain 17DTMF RX`
- значение @0x080223F0: ` TH 18DTMF Co`
- значение @0x08022400: `ntrol 19Time Ca`
- значение @0x08022410: `librate20RSSI Re`
- значение @0x08022420: `fresh 21VOX`
- значение @0x08022430: ` 22VOX Del`
- значение @0x08022440: `ay 23VOX Thr`
- значение @0x08022450: `eshold 24Short T`
- значение @0x08022460: `ail 25Seconda`
- значение @0x08022470: `ry PTT 01Side Ke`
- значение @0x08022480: `y 1 S 02Side Ke`
- значение @0x08022490: `y 1 L 03Side Ke`
- значение @0x080224A0: `y 2 S 04Side Ke`
- значение @0x080224B0: `y 2 L 050 Press`
- значение @0x080224C0: ` Long 061 Press`
- значение @0x080224D0: ` Long 072 Press`
- значение @0x080224E0: ` Long 083 Press`
- значение @0x080224F0: ` Long 094 Press`
- значение @0x08022500: ` Long 105 Press`
- значение @0x08022510: ` Long 116 Press`
- значение @0x08022520: ` Long 127 Press`
- значение @0x08022530: ` Long 138 Press`
- значение @0x08022540: ` Long 149 Press`
- значение @0x08022550: ` Long 15Green K`
- значение @0x08022560: `ey L 16Extra h`
- значение @0x08022570: `otkey 170 Hotke`
- значение @0x08022580: `y 181 Hotke`
- значение @0x08022590: `y 192 Hotke`
- значение @0x080225A0: `y 203 Hotke`
- значение @0x080225B0: `y 214 Hotke`
- значение @0x080225C0: `y 225 Hotke`
- значение @0x080225D0: `y 236 Hotke`
- значение @0x080225E0: `y 247 Hotke`
- значение @0x080225F0: `y 258 Hotke`
- значение @0x08022600: `y 269 Hotke`
- значение @0x08022610: `y 27Radio N`
- значение @0x08022620: `ame 01Key Bee`
- значение @0x08022630: `p 02Lock Ti`
- значение @0x08022640: `mer 03Backlig`
- значение @0x08022650: `ht 04Light T`
- значение @0x08022660: `imer 05Brightn`
- значение @0x08022670: `ess 06Menu Ex`
- значение @0x08022680: `it 07Dual St`
- значение @0x08022690: `andby 08TX Prio`
- значение @0x080226A0: `rity 09Frequen`
- значение @0x080226B0: `cy Step10Talkaro`
- значение @0x080226C0: `und 11Save Mo`
- значение @0x080226D0: `de 12Scan Du`
- значение @0x080226E0: `ration 13Scan En`
- значение @0x080226F0: `d 14Scan Co`
- значение @0x08022700: `ntinue 15Scan Dw`
- значение @0x08022710: `ell 16Scan Re`
- значение @0x08022720: `turn 17Alarm T`
- значение @0x08022730: `ype 18Main PT`
- значение @0x08022740: `T TX 19Area A`
- значение @0x08022750: `Mode 20Area A`
- значение @0x08022760: `Show 21Area A`
- значение @0x08022770: `Zone 22Area B`
- значение @0x08022780: `Mode 23Area B`
- значение @0x08022790: `Show 24Area B`
- значение @0x080227A0: `Zone 25Save Ch`
- значение @0x080227B0: `annel 26Delete`
- значение @0x080227C0: `Channel27LCD Con`
- значение @0x080227D0: `trast 28Freq. D`
- значение @0x080227E0: `igits 29Reverse`
- значение @0x080227F0: ` CH Dir30Scan St`
- значение @0x08022800: `art 31Scan En`
- значение @0x08022810: `d 32Initial`
- значение @0x08022820: `ization33Radio I`
- значение @0x08022830: `nfo 34Basic S`
- значение @0x08022840: `etup 01Key Def`
- значение @0x08022850: `ine 02Analog`
- значение @0x08022860: `Setup 03Digital`
- значение @0x08022870: ` Setup 04Channel`
- значение @0x08022880: ` Setup 05Zone Se`
- значение @0x08022890: `tup 06Message`
- значение @0x080228A0: `s 07FM Radi`
- значение @0x080228B0: `o 08Extra`
## Дополнительная таблица (36 записей)
- значение @0x08020980: `(((((((((PPPPPPP`
- значение @0x08020990: `PPPPPPPPP#######`
- значение @0x080209A0: `#########KKKKKKK`
- значение @0x080209B0: `KKKKKKKKKOff`
- значение @0x080209C0: ` Side 1`
- значение @0x080209D0: ` Side 2`
- значение @0x080209E0: ` Off`
- значение @0x080209F0: ` VFO B`
- значение @0x08020A00: ` Opposit`
- значение @0x08020A10: `e VFO RX + TX`
- значение @0x08020A20: ` allowed Only RX`
- значение @0x08020A30: ` allowed Only TX`
- значение @0x08020A40: ` allowed Off`
- значение @0x08020A50: ` On whil`
- значение @0x08020A60: `e locked On alwa`
- значение @0x08020A70: `ys 400 ms`
- значение @0x08020A80: ` 600 ms`
- значение @0x08020A90: ` 800 ms`
- значение @0x08020AA0: ` 1000ms`
- значение @0x08020AB0: ` Always`
- значение @0x08020AC0: `on Off aft`
- значение @0x08020AD0: `er 5 s TX Back`
- значение @0x08020AE0: `light 01Voltage`
- значение @0x08020AF0: ` Show 02Live Su`
- значение @0x08020B00: `b Tone 03Sub Ton`
- значение @0x08020B10: `e PTT 04TOT War`
- значение @0x08020B20: `ning 05DMR Sca`
- значение @0x08020B30: `n Speed06PTT Loc`
- значение @0x08020B40: `k 07Show Zo`
- значение @0x08020B50: `ne CH 08Talker`
- значение @0x08020B60: `Alias 09 60 ms`
- значение @0x08020B70: ` 100 ms`
- значение @0x08020B80: ` 150 ms`
- значение @0x08020B90: ` 200 ms`
- значение @0x08020BA0: ` Tail To`
- значение @0x08020BB0: `ne Off 55Hz No`
+1 -1
Просмотреть файл
@@ -213,7 +213,7 @@ screen→handler map. This is the exact hook point for a rewritten UI router** (
`void boot_key_scan(void)` — at power-on reads `keypad_decode()` directly (not through KeyEv) and matches
combos: `0x11`, `0x0A` (→ enters a special mode: `0x080149D4`/`0x0801492C`, PC-programming/init screen),
`0x0F`, etc. This implements "hold `*`/`#`/side at power-on" entries. Uses the same key codes as §2.
`0x0F`, etc. This implements the hold-key-at-power-on entries (flash mode = **PTT held at power-on**). Uses the same key codes as §2.
---
Разница между файлами не показана из-за своего большого размера Загрузить разницу
Двоичные данные
Двоичный файл не отображается.
+2 -2
Просмотреть файл
@@ -28,7 +28,7 @@
<div class="warn" id="nowebserial">Откройте этот файл в <b>Google Chrome</b> или <b>Microsoft Edge</b> (нужен WebSerial).</div>
<ol>
<li><b>Выключите</b> рацию.</li>
<li>Включите её, <b>удерживая клавишу «*»</b> (режим прошивки).</li>
<li>Включите её, <b>удерживая тангенту PTT</b> (режим прошивки).</li>
<li>Подключите рацию к ПК кабелем USB-C.</li>
<li>Нажмите <b>«Прошить»</b> и выберите COM-порт рации.</li>
<li><b>Не отключайте</b> кабель до конца записи.</li>
@@ -69,7 +69,7 @@ async function doFlash(){
log('Порт открыт. Синхронизация с бутлоадером...');
port.flush();let ready=false;const end=Date.now()+7000;
while(Date.now()<end){const b=await port.readByte(150);if(b===null){await port.write([0xFF]);}else if(b===0xFF){ready=true;break;}}
if(!ready)throw new Error('Рация не отвечает. Выключите и включите её с зажатой «*», затем повторите.');
if(!ready)throw new Error('Рация не отвечает. Выключите и включите её с зажатой тангентой PTT, затем повторите.');
log('Бутлоадер найден. Стирание...');
for(const trig of [0x10,0x55]){const p=[0x39,0x33,0x05,trig];p.push(cksum(p));port.flush();await port.write(p);
let ok=false;const e2=Date.now()+4000;while(Date.now()<e2){if(await port.readByte(200)===ACK){ok=true;break;}}if(!ok)throw new Error('Ошибка стирания.');}
+4 -3
Просмотреть файл
@@ -3,7 +3,8 @@
it into a single Windows .exe with PyInstaller."""
import base64, os, subprocess, sys
ROOT = "C:/Users/vikto/Documents/Claude/rt-4d"
# project root = parent of this script's directory (portable)
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FW = f"{ROOT}/stock-fw/rt4d_ru_full.bin"
DIST = f"{ROOT}/dist"
@@ -45,7 +46,7 @@ def do_flash(port_name, log, prog):
if not b: p.write(b"\xff")
elif b == b"\xff": ready = True; break
if not ready:
raise RuntimeError("Рация не отвечает. Выключите и включите её\nс зажатой клавишей * , затем повторите.")
raise RuntimeError("Рация не отвечает. Выключите и включите её\nс зажатой тангентой PTT, затем повторите.")
log("Бутлоадер найден. Стирание...")
for trig in (0x10, 0x55):
pl = bytes([0x39, 0x33, 0x05, trig]); pl += cksum(pl)
@@ -80,7 +81,7 @@ class App:
tk.Label(root, text="Ru-4D - Прошивка рации", font=("Segoe UI", 15, "bold"), **F).pack(pady=(16, 0))
tk.Label(root, text=INFO, font=("Segoe UI", 9), bg="#131a24", fg="#7f8ea3").pack()
steps = ("1. Выключите рацию.\n"
"2. Включите её, удерживая клавишу * (режим прошивки).\n"
"2. Включите её, удерживая тангенту PTT (режим прошивки).\n"
"3. Подключите рацию к ПК кабелем USB-C.\n"
"4. Выберите COM-порт и нажмите Прошить .\n"
"5. Не отключайте кабель до конца записи.")
+4 -3
Просмотреть файл
@@ -3,7 +3,8 @@
embedded. Open in Chrome/Edge, one button flashes the RT-4D."""
import base64, os
ROOT = "C:/Users/vikto/Documents/Claude/rt-4d"
# project root = parent of this script's directory (portable)
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FW = f"{ROOT}/stock-fw/rt4d_ru_full.bin"
OUT = f"{ROOT}/dist/Ru-4D_flasher.html"
@@ -40,7 +41,7 @@ HTML = """<!doctype html><html lang="ru"><head><meta charset="utf-8">
<div class="warn" id="nowebserial">Откройте этот файл в <b>Google Chrome</b> или <b>Microsoft Edge</b> (нужен WebSerial).</div>
<ol>
<li><b>Выключите</b> рацию.</li>
<li>Включите её, <b>удерживая клавишу «*»</b> (режим прошивки).</li>
<li>Включите её, <b>удерживая тангенту PTT</b> (режим прошивки).</li>
<li>Подключите рацию к ПК кабелем USB-C.</li>
<li>Нажмите <b>«Прошить»</b> и выберите COM-порт рации.</li>
<li><b>Не отключайте</b> кабель до конца записи.</li>
@@ -81,7 +82,7 @@ async function doFlash(){
log('Порт открыт. Синхронизация с бутлоадером...');
port.flush();let ready=false;const end=Date.now()+7000;
while(Date.now()<end){const b=await port.readByte(150);if(b===null){await port.write([0xFF]);}else if(b===0xFF){ready=true;break;}}
if(!ready)throw new Error('Рация не отвечает. Выключите и включите её с зажатой «*», затем повторите.');
if(!ready)throw new Error('Рация не отвечает. Выключите и включите её с зажатой тангентой PTT, затем повторите.');
log('Бутлоадер найден. Стирание...');
for(const trig of [0x10,0x55]){const p=[0x39,0x33,0x05,trig];p.push(cksum(p));port.flush();await port.write(p);
let ok=false;const e2=Date.now()+4000;while(Date.now()<e2){if(await port.readByte(200)===ACK){ok=true;break;}}if(!ok)throw new Error('Ошибка стирания.');}
+221
Просмотреть файл
@@ -0,0 +1,221 @@
# RT-4D — Codeplug & CPS-Compatibility Boundary (key: `codeplug`)
**Scope.** This file defines the *compatibility boundary* for a UI rewrite: the on-SPI codeplug
format and the PC/CPS serial protocol that MUST stay byte-identical, and the firmware functions a
rewritten UI MUST reuse so it never re-implements (and never diverges from) the stock format. All
vaddrs are absolute in the MCU application image (Thumb, load base `0x08002800`,
`rt4d_stock_v3.25_abs_0x08002800.bin`). Cross-checked against `rt4d-cps/rt4d_codeplug/constants.py`
+ `models.py` + `parser.py`, and against the live `radio-spi-dump.bin`.
> TL;DR for the rewrite: **do not touch** the SPI region layout, the record structs, the settings
> dual-bank/`0xABCD` magic, or the USART6 `0x34/0x52/region-id` serial dispatcher. **Do call** the
> firmware's SPI primitives and the per-record read/save wrappers listed in §5. If you keep those
> two invariants, the stock Radtel CPS keeps working unchanged.
---
## 1. Low-level SPI flash primitives (the mandatory bottom layer — REUSE)
These four are the only functions that actually touch the external SPI data-flash. Everything the
UI or CPS does goes through them. A rewritten UI should call the *record wrappers* (§5) rather than
these directly, but they are documented because the wrappers and the CPS dispatcher both depend on
them and they define the on-flash addressing.
| vaddr | signature | what it does | evidence |
|---|---|---|---|
| `0x08021828` | `void spi_flash_read(void *dst, uint32 addr, uint32 len)` | CS low; sends opcode `0x03`; sends 24-bit addr (32-bit if chip-id byte==`0x18`/`0x19`); streams `len` bytes to `dst`; CS high. | opcode `movs r0,#3`@`0x08021838`; addr-width branch on `#0x18`/`#0x19`; byte-loop via `0x8021538`/`0x8021580`. |
| `0x08021924` | `void spi_flash_erase4k(uint32 sector_idx)` | `addr = sector_idx << 12`; WREN; opcode `0x20` (4 KB sector erase); waits WIP. | `lsls r4,#0xc`@`0x08021928`; `movs r0,#0x20`@`0x0802193a`; poll `0x8021984`. |
| `0x08021a70` | `void spi_flash_program(uint32 addr, const void *src, uint32 len)` | Page-program respecting 256-byte page boundaries; loops calling `spi_page_program(0x080217b8)` per page. | `rsb r5,r0,#0x100` page-split@`0x08021a7c`; page loop. |
| `0x080217b8` | `void spi_page_program(uint32 addr, const void*src, uint16 len)` | WREN + opcode `0x02` single page write (≤256 B). | called only from `0x08021a70`. |
Supporting helpers used alongside them (reuse verbatim):
| vaddr | signature | role |
|---|---|---|
| `0x080109de` | `uint8 checksum(const void *buf, uint32 len)` | 8-bit sum, seed 0. Used by CPS read/write frames and codeplug. |
| `0x08010540` | `void flash_write_guard_enter(void)` | Sets "flash busy" RAM flag, quiesces competing access before a region rewrite. Call **before** any erase/program batch. |
| `0x0801058c` | `void flash_write_guard_exit(void)` | Clears the busy flag. Call **after**. |
| `0x08013738` | `void memset8(void*dst,int val,uint len, ...)` | zero/fill helper used to clear work areas before assembling a record. |
| `0x080060ac` | `bool memcmp_eq(const void*a,const void*b,uint16 len)` | equality compare (used for magic/marker checks). |
**Chip-ID gate.** A RAM byte holds the SPI JEDEC id; `==0x18`(16 MB)/`0x19`(32 MB) switches to
4-byte addressing and enables the `0xA4` addressbook / large regions. A UI rewrite must not assume a
fixed flash size — always go through `spi_flash_read/erase/program`, which handle this.
---
## 2. SPI codeplug region map — DO NOT CHANGE (format is CPS-visible)
Region base addresses and record sizes as they appear **in the firmware's own read/write code**
(the authoritative on-flash layout). The CPS `region_id` column is the byte the CPS sends as
`frame[0]` for a WriteSPI; the firmware's serial dispatcher (§4) maps it to the same base.
| Region | SPI base | record | stride | count | CPS region_id | firmware access site |
|---|---|---|---|---|---|---|
| **calibration** (per-unit, NEVER write) | `0x000000` | 4 KB blob | — | 1 | `0x40` | read `0x08004cbe` |
| **main_settings** bank0 | `0x002000` | struct (4 KB page) | — | 1 | `0x90` | save `0x08004cb0`; magic `0xABCD` @`+0x0C` |
| settings bank1 (shadow) | `0x003000` | struct (4 KB page) | — | 1 | (via `0x90`) | commit `0x08004b0e` |
| **channels** | `0x004000` | Channel | **48** (`0x30`) | 1024 | `0x91` | read `0x08004f2a`/`0x080055a0`; addr `= 0x4000 + idx*48` |
| **zones** | `0x01C000` | Zone (48-B chan-format recs here, see note) | 48 | 256 | `0x92` | batch `0x08004b6a` |
| **contacts** | `0x05C000` | Contact | **27** (`0x1B`) core, `0x15` read | ~2048 | `0x93` | find `0x08005810`; addr `= 0x5E000 + idx*27` |
| **groups** (CPS view) | `0x07C000` | — | — | — | `0x94` | *empty in dump; runtime uses 0xC6000, see note* |
| **dmr_keys** (CPS view) | `0x082000` | — | — | — | `0x95` | *empty in dump; key names live at 0x0D0000, see note* |
| **call_log** | `0x088000` | — | — | — | `0x96` | — |
| **default_sms/msg** | `0x094000` | Message | 256 | — | `0x97` | text read `0x08009498` |
| **RX group lists (runtime)** | `0x0C6000` | GroupList | **80** (`0x50`) | — | (see note) | read `0x08007426`; addr `= 0xC6000 + idx*80` |
| **enc-key names (runtime)** | `0x0D0000` | key-slot name | 48 | 256 | — | dump-confirmed |
| **global addressbook** | `0x126000` | contact | **32** (`0x20`) | — | `0xA4` | read `0x08009ba2`; addr `= 0x126000 + slot*32` |
| **fm_settings** | `0x0D6000` | FMSettings | 4 KB | 1 | `0x99` | batch `0x08004c48` |
| **dtmf_names** | `0x0C7000` | 16×16 | 16 | 16 | `0x80` | — |
### Record structs (must be emitted byte-identically — from `models.py`/`parser.py`, confirmed by firmware access)
- **Channel (48 B @ `0x004000 + idx*48`)**: `+0x00` common flags (rx_tx bits4-5); `+0x03` busy-lock;
`+0x04` mode/modulation/bandwidth bits (bit? = DIGITAL 0x00 / ANALOG 0x01); `+0x05` **u32 LE rx_freq**
(`MHz×100000`); tx_freq follows; `+0x0E` promiscuous; `+0x12` tot/ctdcs_select; `+0x14` mute_code;
`+0x20` 16-byte ASCII name (`0xFF`-padded). Firmware reads exactly `0x30` bytes.
*Selector*: `read_channel(idx)` copies to the live band struct at RAM `0x20002DEA + band*48` (§3).
- **Contact (27-B stride @ `0x05E000 + idx*27`)**: `+0x00` type (`0=Private,1=Group,2=AllCall`);
`+0x01` **u32 LE DMR id** (group IDs stored BCD-LE); `+0x05` name. Firmware reads `0x15` bytes.
The lookup at `0x08005810` matches on `[+1..4]==id && [+0]==type`.
- **GroupList (80-B @ `0x0C6000 + idx*80`)**: header + up to 32 member **u16 contact indices**;
member value `>= 0x2710`(10000) = empty. Members resolve into the 27-B contact table.
- **Global addressbook contact (32-B @ `0x126000 + slot*32`)**: name(16)+id, used for caller-alias
resolution of *incoming* DMR calls that aren't in the small contacts table.
> **NOTE — three layout discrepancies vs `constants.py` (reconcile before writing these regions):**
> 1. **RX group lists**: the running firmware reads them at **`0x0C6000`, 80-byte stride** (`0x08007426`),
> not `0x07C000`. `0x07C000` is `0xFF` in the live dump. `constants.py` `groups@0x07C000` (region
> `0x94`) is the CPS *write* region id; the firmware's live group-list store is `0xC6000`. If the
> rewrite manages RX groups, use the firmware wrapper (§5) rather than a hard-coded address.
> 2. **Encryption key names** live at **`0x0D0000`** (48-B stride, "Key 1".."Key 256"), not `0x082000`.
> 3. **Zones @`0x01C000`** hold 48-byte channel-format records in this firmware, not the 512-byte
> `ZONE_SIZE` structs `constants.py` assumes. Match the firmware's on-flash layout.
>
> These are firmware-vs-CPS-constant mismatches that already exist in stock; **preserve stock behaviour** —
> a rewrite must reproduce whatever the stock firmware reads/writes, so the CPS round-trips unchanged.
---
## 3. In-RAM state the UI reads/writes, and the save-to-SPI path
The UI never re-reads SPI on every draw; it keeps a small set of "live" structs in SRAM and lazily
commits them. A rewritten UI must use these same RAM structures so the save path stays correct.
| RAM addr | contents | notes |
|---|---|---|
| `0x20002014` | **main_settings working copy** (the RadioSettings struct, ~4 KB image of `0x002000`) | Field offsets = `models.py` `RadioSettings` offsets (e.g. `+0x60` LED on/off, `+0x85` display mode A, `+0x184` remote_control). Magic `0xABCD` sits at `+0x0C` when valid. |
| `0x20002120` | freq-lock / channel-limit sub-block of settings | referenced with `0x20002014` at boot (`0x08005f1e`). |
| `0x20002DBB` | **VFO/UI state** — byte `+0x01` = **active band A/B** (0/1), drives which live channel struct is used | read at every channel access (`0x08004f30`, `0x080055a4`). |
| `0x20002DEA` | **live Channel struct, band A** (48 B); band B at `0x20002E1A` (`+band*48`) | filled by `read_channel(idx)`; the standby screen renders from here. |
| `0x200029bb` / `0x200029d7` | additional settings mirror fields (channel name/id area) | e.g. `0x08004f52` reads `+0x67`. |
| `0x20000ce8` | contacts/messaging cursor block (record count `+0x04`, type `+0x01`, band `+0x06`) | used by contact/SMS browsers. |
| `0x20002ebe` | **4 KB SPI staging buffer** — every region read/program passes through here | shared by codeplug save (`0x08004cb0`), batch copy (`0x08004ab0`), contacts compact (`0x080061e0`) **and** the CPS ReadSPI/WriteSPI dispatcher (`0x080188d4`). |
| `0x2000a5c5` / `0x2000a7c5` / `0x20006f86` | per-op scratch (channel-edit, addressbook, SMS text) | transient. |
| `0x200092ef` | **CPS TX assembly buffer**; `0x20000c64` = its write index | do not repurpose. |
### Save-to-SPI functions (REUSE — these keep the format + dual-bank correct)
| vaddr | signature | what it commits |
|---|---|---|
| `0x08004cb0` | `void settings_save(void)` | guard-enter → read `0x2000` into `0x20002ebe` → erase sector 1 → program `0x2000` (4 KB) → guard-exit. Commits `main_settings`. (A prior copy from `0x20002014``0x20002ebe` is done by the caller.) |
| `0x08004ab0` | `void codeplug_backup_to_shadow(void)` | Reads each live region (settings `0x2000`, channels `0x4000+`, zones `0x1C000+`, contacts `0x5E000+`, schedules `0xC6000+`, fm `0xD6000+`, …) and re-programs it to a **+offset shadow copy** (settings→`0x3000`, channels→`0x10000+`, zones→`0x1D000/0x3E000+`, contacts→`0x92000+`). This is the "Backing up…" path. |
| `0x080061e0` | `void contacts_compact(uint idx)` | Deletes contact `idx` by reading `0x1015` bytes, shifting records down 21 B (27-B stride preserved), erasing + reprogramming the affected 4 KB pages of the `0x5E000` region. Format-preserving contact delete. |
| `0x08006000` | `void <region>_erase8(void)` | erases 8 sectors from base (`idx+0x126`) — bulk clear used before a full-region rewrite. |
**Dual-bank / `0xABCD` magic.** `main_settings` uses bank0 `0x2000` (live) with a shadow at `0x3000`.
Validity is gated by the **`0xABCD` magic word at settings offset `0x0C`** (byte pattern `CD AB` @
`0x00200C`, matches the live dump). `constants.py` additionally references a `DTCN`
marker at `+0xFFC` for beta41+ A/B-bank selection; **no `DTCN` string exists in v3.25 firmware code**
— that marker is a CPS-side/beta convention. For v3.25 the rewrite must (a) keep writing `0xABCD` at
`+0x0C`, (b) keep the bank0/`0x2000` primary + `0x3000` shadow arrangement, and (c) go through
`settings_save`/`codeplug_backup_to_shadow` rather than hand-rolling the bank logic. **Do not change
the magic value or its offset** or the CPS/stock loader will treat settings as invalid.
---
## 4. CPS serial protocol handler (USART6) — DO NOT CHANGE, MUST STAY REACHABLE
The stock Radtel CPS talks to `main`'s USART6 link. A UI rewrite must leave this entire path intact
and reachable (i.e. keep entering "PC Programming" mode and keep the ISR + framer + dispatcher wired).
| stage | vaddr | role |
|---|---|---|
| USART6 RX ISR | `0x0802061c` | pushes bytes into ring buffer (`data 0x20007ddb`, head/tail `0x20000c5c`/`0x20000c60`). |
| Frame framer | `0x0801f864` | accepts first byte only if in `{0x34, 0x40, 0x52, 0x90..0xA5}`; computes length (`0x34`→5, `0x52`→4, writes→`0x404`=1028); verifies trailing sum checksum (`0x80109de`, seed 0); copies validated frame to `0x200092ef`; hands to dispatcher. |
| Top dispatcher | `0x08019790` | on `0x34`: sub `frame[3]` = `0x10` Notify(→`0x06`) / `0x54`,`0x58` enter-SPI-mode / `0xEE` Close→`NVIC_SystemReset` (`0x801a38c`). `0x34` guard compares a word vs **`0xABCD`** (`0x08019888`) → secondary handler `0x801ad9c`. else → region handler. |
| Region R/W handler | `0x080188d4` | **`0x52` ReadSPI**: `block=(f[1]<<8)|f[2]`; `spi_flash_read(0x20002ebe, block<<10, 0x400)`; append `checksum(len 0x403)`; stream `hdr(3)+1024+cksum` from `0x200092ef`. **region-id write** (`0x40/0x90..0xA5`): map id→(KB base,KB size), erase covered sectors, `spi_flash_program`, reply `0x06`. **`0xA4`** addressbook write with `0x4A` capacity reject. |
Confirmed opcodes (must remain byte-for-byte): `0x34/0x10` notify→`0x06`; `0x34/0x54`,`0x34/0x58`
enter SPI mode; `0x34/0xEE` close→reboot; `0x52` read 1 KB; `0x40`,`0x90``0x9A`,`0x9C``0xA5` region
writes (4 KB erase + 1 KB program); `0xA4` addressbook. Checksum = 8-bit sum seed 0 over all-but-last
byte. **The rewrite must not alter `0x080188d4`, `0x0801f864`, `0x08019790`, the USART6 ISR, or the
buffers `0x20002ebe`/`0x200092ef`/`0x20000c5c..64`.** Since the CPS reads/writes raw SPI blocks, as
long as the on-flash *format* (§2) is preserved, the CPS is automatically compatible.
---
## 5. Per-record READ/SAVE wrappers a rewritten UI should call (REUSE list)
These are the format-safe entry points. Signatures are inferred from register usage; RAM
destinations are where stock leaves the decoded record for the UI to render/edit.
| vaddr | inferred signature | behaviour |
|---|---|---|
| `0x08004f20` (in-fn) / `0x080055a0` (in-fn) | `void read_channel(uint16 idx)` | `spi_flash_read(&liveChan[band], 0x4000 + idx*48, 48)`; `band = *(u8*)0x20002DBC`. Dest `0x20002DEA + band*48`. |
| `0x08005810` | `bool find_contact_by_id(uint32 id, uint8 type, char *out_name16)` | scans `0x5E000` (27-B stride); on `[+1..4]==id && [+0]==type` copies 16-B name to `out`. Returns found. |
| `0x08009b90` (in-fn) | `void read_addressbook_contact(uint slot, ...)` | `spi_flash_read(0x2000a7c5, 0x126000 + slot*32, 32)`. Global addressbook (32-B). |
| `0x08007404` | `void read_group_list(uint16 idx)` | `spi_flash_read(sp+0xA4, 0xC6000 + idx*80, 80)`; then resolves each u16 member (<0x2710) into a contact via `0x5E000` stride. |
| `0x08009480` (in-fn) | `void read_message(...)` | reads `0xC8`(200) bytes of message text into `0x20006f86`. |
| `0x08004cb0` | `void settings_save(void)` | commit `main_settings` (see §3). |
| `0x080061e0` | `void contacts_compact(uint idx)` | format-safe contact delete (see §3). |
| `0x08004ab0` | `void codeplug_backup_to_shadow(void)` | full backup path (see §3). |
| `0x08003254` (in-fn) | channel-edit read: `spi_flash_read(0x2000a5c5+0x39, 0x4000+idx*48, 0x91)` | used by the channel editor; note it reads 0x91 (>48) into a wider edit scratch. |
*(“in-fn” = the read/save is an inline sequence inside a larger UI handler; the wrapper boundary is
the containing functions entry. When rewriting, either call the containing handler or replicate the
exact `spi_flash_*` calls with the addresses/strides above.)*
---
## 6. DO-NOT-CHANGE vs REUSE — the CPS-safe contract
### DO NOT CHANGE (format & protocol — CPS-visible)
1. **SPI region bases & record strides** in §2 (channels `0x4000`/48, contacts `0x5E000`/27,
zones `0x1C000`/48, settings `0x2000`, group-lists `0xC6000`/80, addressbook `0x126000`/32,
msgs `0x94000`, fm `0xD6000`, dtmf-names `0xC7000`, enc-key-names `0xD0000`).
2. **Record field layouts** (freq = u32 LE `MHz×100000` @ channel `+0x05`; contact type@`+0`,
id@`+1` LE; 16-B `0xFF`-padded names; contact-type enum 0/1/2; power/mode/scan enums).
3. **Settings dual-bank + `0xABCD` magic @ offset `0x0C`** (bank0 `0x2000` / shadow `0x3000`).
4. **Calibration block `0x000000` (4 KB)** — read-only, per-unit, never erase/write.
5. **USART6 CPS protocol**: framer `0x0801f864`, dispatcher `0x08019790`, region handler
`0x080188d4`, ISR `0x0802061c`; opcodes `0x34(/0x10/0x54/0x58/0xEE)`, `0x52`, `0x40/0x90..0xA5`,
`0xA4`; sum-seed-0 checksum; buffers `0x20002ebe`/`0x200092ef`/`0x20000c5c..64`.
### REUSE (call these; don't re-implement)
- SPI primitives: `spi_flash_read 0x08021828`, `spi_flash_erase4k 0x08021924`,
`spi_flash_program 0x08021a70`, `spi_page_program 0x080217b8`, `checksum 0x080109de`,
`flash_write_guard_enter 0x08010540` / `_exit 0x0801058c`.
- Record wrappers (§5): `read_channel`, `find_contact_by_id 0x08005810`, `read_group_list 0x08007404`,
`read_addressbook_contact`, `read_message`, `settings_save 0x08004cb0`,
`contacts_compact 0x080061e0`, `codeplug_backup_to_shadow 0x08004ab0`.
- Live RAM structs (§3): `settings 0x20002014`, `vfo/band 0x20002DBB`, `live channel 0x20002DEA(+band*48)`.
**Contract:** if the rewritten UI (a) mutates only the §3 RAM structs and calls the §5 save wrappers,
and (b) leaves the §4 serial path untouched, then the on-SPI bytes remain exactly what stock produces,
and the stock Radtel CPS reads/writes the radio identically. That is the CPS-safe boundary.
---
## 7. Confidence / open items
- **High confidence**: SPI primitives (§1) — fully disassembled, opcodes/addr-width proven.
Channel (48@`0x4000`), contact (27@`0x5E000`) strides — proven from live read sites and dump.
CPS dispatcher `0x080188d4` `0x52`/`0xA4` paths — fully decoded and match `constants.py`.
Settings staging/save via `0x20002ebe``0x2000` — proven.
- **Medium confidence**: exact `settings_save` caller that copies `0x20002014``0x20002ebe`
(the copy is in the menu handler, not shown here); `codeplug_backup_to_shadow` full region list
(decoded through contacts; tail regions inferred from the loop bases).
- **To verify before writing group-lists / keys**: the `0xC6000` group-list vs `0x07C000` CPS region
and `0xD0000` key-names vs `0x082000` — stock firmware and `constants.py` disagree; a round-trip
test with the stock CPS is the safest confirmation. Reuse the firmware wrappers to stay stock-correct.
+277
Просмотреть файл
@@ -0,0 +1,277 @@
# RT-4D Display Subsystem — API Reference (`display` key)
Reverse-engineered drawing toolkit for the Radtel RT-4D stock firmware
(`rt4d_stock_v3.25_abs_0x08002800.bin`, ARM Cortex-M4F Thumb, load base `0x08002800`).
This is the low-level graphics API to **reuse** when rewriting the UI. All addresses are
absolute vaddrs. Signatures use ARM Thumb AAPCS (r0..r3 = args, extra args on stack).
> **TL;DR for the UI rewrite:** the LCD is a **monochrome page-addressed panel, 128×64
> px = 8 pages × 128 columns**, driven by **bit-banged serial (GPIO), with NO framebuffer** —
> every draw call clocks pixels *directly into the controller's GDDRAM*. There is nothing to
> "flush." You position a cursor with `lcd_set_pos(page,col)` and stream 8-bit vertical
> pixel-slices with `lcd_write_col(byte)`. Everything above that (chars, strings, icons,
> boxes) is built from those two. Fonts live in **external SPI data-flash**, fetched at draw
> time by `spi_flash_read`. The single most important callable is
> **`draw_string(page,x,str,len,mode) @0x08008A50`**.
---
## 1. Display hardware
### 1.1 Panel type, resolution, color format
| Property | Value | Evidence |
|---|---|---|
| Type | **Monochrome, page-addressed** LCD (ST7565/UC1701/SSD1306-class controller) | `lcd_write_col` streams **8 vertical pixels per byte, LSB=top**; page addressing in `lcd_set_pos` |
| Resolution | **128 (W) × 64 (H) px** | `draw_string` wraps X at 121 and cycles page `& 7` (8 pages × 8 = 64 rows); status icons at cols 0x15/0x7B (21/123) |
| Pages (rows of 8px) | **8** (page 0 = top) | page arg masked `& 7` in the wrap logic (`0x08008A82`) |
| Color format | **1 bpp** (bit set = pixel on). "mode" adds invert/outline (see §4) | `lcd_write_col` shifts one bit/pixel; blitter mode transforms are bit ops |
| Controller column origin | **183** (`0xB7`) — visible window is offset inside a wider GDDRAM | `lcd_set_pos`: `col_cmd = 0xB7 x` (`rsb r1,r5,#0xB7` @`0x08014A82`) |
| Framebuffer | **NONE** — direct-to-GDDRAM bit-bang | `lcd_write_col`/`lcd_set_pos` write GPIO registers directly; no large SRAM buffer touched |
Because there is no framebuffer, drawing is **immediate** and **non-atomic** — a partial redraw
is visible mid-frame. The stock UI mitigates flicker by only repainting changed regions. A
rewrite can adopt the same discipline, or maintain its own SRAM shadow buffer and blit it with
the raw-bitmap primitive (`lcd_blit_cols`, §2).
### 1.2 The bit-bang interface (GPIO, not SPI2)
The LCD is **not** on SPI2 (SPI2 @0x40003800 is used elsewhere; the external **SPI data-flash**
that holds the fonts is itself bit-banged on GPIOB — see §5). The LCD is driven by three GPIO
"registers" resolved from `lcd_set_pos`/`lcd_write_col` literal pools:
| Symbol in this doc | Literal | Role (inferred) | Access pattern |
|---|---|---|---|
| `LCD_RS` | `0x40010000` | **Register-Select / DC** line, driven bit0 via helper `gpio_bit0(base,val) @0x08021C4C` (`bfi [r0],r1,#0,#1`). `0`=command phase, `1`=data phase | set 0 before a byte, 1 after |
| `LCD_PORT` | `0x40020400` (GPIOB) | **CLK + DATA** bit-bang port. Set-bits reg at `+0x18`, clear-bits reg at `+0x28` (Artery-clone GPIO SCR/CLR layout) | per-bit CLK low/high, DATA set/clear |
| `LCD_CS` | `0x40020000` (GPIOA) | **CS/latch** — writes GPIOA `+0x18`/`+0x28` around each byte | strobe per byte |
> **Note on the clone GPIO map.** This MCU is an STM32F407-class **Artery AT32** clone. Its GPIO
> "set" and "clear" strobe registers sit at `base+0x18` and `base+0x28` (used as `str val,[port]`
> and `str val,[port+0x10]` after the code loads `port+0x18`). Bit masks seen: `0x08` = CLK,
> `0x20` = DATA. The exact silicon pin numbers are not load-bearing for the UI rewrite (you call
> the primitives, you don't re-bang pins); they are documented here only to explain the register
> writes. `gpio_bit0 @0x08021C4C`, `gpio_clr @0x08021C56`, `gpio_test @0x08021C5C` are the generic
> helpers.
### 1.3 The two hardware primitives (bottom of the stack)
```c
// @0x08014A7C — position the GDDRAM write cursor.
// Sends 3 command bytes: (0xB7 - x_col), (0x10 | (y>>4)), (y & 0x0F) via lcd_send_cmd.
// NOTE arg order: r0 = page/x-select, r1 = the 16-bit-ish column value. See §3 for the
// exact meaning as used by the blitters: r0 = page index, r1 = column (x pixel).
void lcd_set_pos(uint8_t page, uint16_t col); // @0x08014A7C
// @0x08014AA0 — send ONE command byte (RS=0), MSB-first, 8 clocked bits. Internal to set_pos;
// 0 external callers. Effectively lcd_send_cmd(uint8_t cmd).
static void lcd_send_cmd(uint8_t cmd); // @0x08014AA0
// @0x08014B28 — write ONE data byte = a vertical strip of 8 pixels (LSB = top pixel) at the
// current cursor, then auto-advances the column. RS=1. Bits shifted LSB-first (asrs r5,#1).
void lcd_write_col(uint8_t column_bits); // @0x08014B28
```
`lcd_set_pos` internally calls `lcd_send_cmd` three times (col-high, page, col-low). `lcd_write_col`
clocks 8 data bits then strobes CS. Both use the busy-wait delay `delay_short(n) @0x08007934`
(a `subs/bne` spin — **not** a real timer) between edges.
### 1.4 Panel init / power / backlight
- **Backlight** is a **TIM PWM** on TIM1 (`0x40010000`), programmed via `tim_set_ccr @0x08021C14`
and `tim_set_arr_psc @0x08021C04`.
- `lcd_set_brightness(level) @0x08014C34``level` 0..4 → duty {0, 5, 0x1E, 0x64, 0xFF}. Called
from `0x08009204`.
- `backlight_on/init @0x08014BEC` and `@0x08014C0C` — enable pin + set duty.
- **Panel reset / power pins** are toggled in `lcd_power_seq @0x0801D938` (writes GPIOB `+0x18`/`+0x28`
BSRR, runs a PWM ramp via `pwm_cfg @0x0802129C`, delays via `delay_ms @0x08007946`).
- The panel **command init sequence** (contrast/segment-remap/display-on) is executed once at boot
through the same `lcd_send_cmd` path; it does not need to be re-issued by a UI rewrite that keeps
the stock boot. (Reusing `lcd_set_pos`/`lcd_write_col` after boot is sufficient.)
`delay_ms(ms) @0x08007946`, `delay_short(loops) @0x08007934` — timing helpers usable by UI code.
---
## 2. Drawing primitives (callable API)
All coordinates: **`page`** = vertical row of 8 px (0=top..7), **`x`/`col`** = pixel column
(0=left..127). Text/GBK cells are **2 pages tall (16 px)**; the compact font is 1 page tall.
| vaddr | Suggested name | Signature (AAPCS) | What it does | Evidence |
|---|---|---|---|---|
| **0x08008A50** | **draw_string** | `void draw_string(u8 page, u8 x, const char *s, u16 len, u8 mode /*[sp]*/)` | **Primary text API.** Iterates bytes: `0x01–0x7F`→ASCII glyph (+7px); `0x80–0xFE`→GBK lead byte, consumes trail, `(lead<<8)\|trail`, wide glyph (+14px); `0x00/0xFF`→space/stop. Handles X-wrap at 121 and page advance. 5th arg `mode` is read at `[sp+0x28]` inside (10-reg push). | disasm §confirmed; callers pass `mode` via `str r?,[sp]` before `bl` (e.g. `0x0800318C`, `0x0801408E`) |
| 0x08007FB8 | ascii_blit | `void ascii_blit(u8 page, u8 x, u8 ch, u8 mode)` | Draws one **7px-wide × 16px-tall** ASCII glyph. Reads **14 bytes** from SPI font `0x19C000 + (ch0x20)*14` via `spi_flash_read`. Applies mode (invert/outline). Emits via `lcd_set_pos`+`lcd_write_col` (2 pages × 7 cols). | `mov r2,#0x19c000`; `index=(ch-0x20)*? ; 14B read`; per-col set_pos/write_col loop |
| 0x08008076 | ascii_blit_v2 | `void (u8 page,u8 x,u8 ch)` | Variant of ascii_blit using width-shaper `0x08005368` (proportional spacing table). | `0x19c000`, calls `0x08005368` |
| 0x08008454 | gbk_blit | `void gbk_blit(u8 page, u8 x, u16 gbk, u8 mode)` | Draws one **14px-wide × 16px-tall** double-byte (GB2312/GBK) glyph. `row=lead0x81, col=trail0x40, index=col+row*190 (1 if trail>0x7F)`, reads **28 (0x1C) bytes** from `0x19E000 + index*28`. | `mov r2,#0x19e000`; `movs r1,#0x81`; 0x1C read |
| 0x08008530 | gbk_char_at | `void gbk_char_at(u8 page, u8 x, u16 gbk)` | Standalone directly-callable single wide-char draw (same math as gbk_blit, base `0x19E000`, 28B). | `0x19e000`, `subs #0x40`, `0x81` |
| 0x080089AC | small_char | `void small_char(u8 page, u8 x, char ch, ...)` | **Compact 5px font** renderer (1 page tall). Reads `(ch0x20)*5` from an in-flash 5×8 table; clamps ch to 0x20..0x7F. Used for tiny status text/numbers. | `subs #0x20`, `*5` (`r7*5`), single-page write |
| **0x08008CF0** | draw_hline_seg | `void (u8 col, bool on)` | Draws a 2-px separator at pages 4 & 5 (values 0xFE/0x7F when on, else 0) — a horizontal rule/underline used under menu fields. | set_pos(page4/5), write_col(0xFE/0x7F) |
| **0x0800842C** | fill_col_run | `void fill_col_run(u8 page, u8 x, u8 count)` | Writes `count` columns of constant `0x18` at `page` — a thin horizontal bar (used for gauge ticks). | write_col(0x18) loop `r5<r4` |
| **0x08008DAA** | draw_box | `void draw_box(u8 page_origin)` | Draws a **rounded-rectangle frame** (corners 0xF8/0x1F, edges 0xFF) spanning pages `origin`..`origin+6`. Menu/popup border. | corner bytes 0xF8/0x1F, edges 0xFF |
| 0x08008D24 | blit_cols | `void blit_cols(u8 page, u8 count, const u8 *cols, bool src)` | Streams `count` raw column-bytes from `cols[]` at `page` (or zeros if `src==0`) — **generic 1-page bitmap blit / clear**. | set_pos(page), loop write_col(buf[i]) |
| 0x08008D62 | blit_rect | `void blit_rect(u8 page, u8 x, u8 pages, u8 width, const u8 *bmp /*[sp]*/)` | Streams a `pages`×`width` raw bitmap (row-major `bmp[page*width+col]`) — **multi-page bitmap/icon blit**. Use this to blit a shadow framebuffer. | `mla r0,r4,r5,r6` addressing, nested page/col loops |
| 0x0800836C | draw_logo48 (`0x080086CC`) | `void draw_logo(void)` | Draws the **48px×48px boot logo** (6 pages × 0x30 cols) from an in-flash bitmap table. | `#0x58` col base, `r4<6`, `r5<0x30` |
| 0x08008874 | draw_icon_batt | `void draw_icon(u8 page, bool on)` | Draws the battery/side icon at col `0x7B` (123). Status-bar icon. | col `r4+0x7B`, 8B table |
| 0x08008A04 | draw_icon_signal | `void draw_icon(u8 page, bool on)` | Draws a status icon at col `0x15` (21) — signal/antenna glyph. | col `0x15+r4` |
| 0x08008300 | draw_dots | `void draw_dots(u8 which)` | Draws 1–3 dotted markers (byte 0xBD) — page/step indicator dots. | `0xBD` at sp[4..b], cases 1/2/3 |
| 0x08008224 | draw_frame_corners | `void (u8 x, u8 page)` | Draws corner pixels of a highlight frame (2-px inset box). | `rsb #1`, write_col(0) corner pattern |
| 0x08008100 | big_char | `void big_char(u8 page,u8 x,u8 ch,u8 mode)` | Large-font glyph: reads **0x21 (33) bytes** from `0x?+(ch0x20)*0x20` — a taller/wider font variant (frequency display digits). | `subs #0x20`, `*0x20`, 0x21B read, invert mode |
| 0x080081A0 | set_pixel_run | pixel/segment helper | Builds a partial column mask (`0x80>>bit`) for sub-page pixel plotting; used by line/rect fills. | `0x80 asr`, `orr` mask accumulate |
| 0x08008B90 | draw_number_row | `void (u8 page,u8 x,const u8*digits,u16 n,u8 mode)` | Draws `n` glyphs from `digits[]` with **12px** stride (`(i*3)<<2`) — fixed-pitch number/label row (calls big_char `0x08008100`). | stride `r4*3<<2`, calls `0x08008100` |
| 0x08008BC6 | draw_str_spaced | `void (u8 page,u8 x,const u8*s,u16 n,u8 mode)` | Draws `n` chars with **6px** proportional stride (calls small_char `0x080089AC`). | `+6` stride, calls `0x080089AC` |
| 0x08008BFC | draw_field | menu field value paint | Copies a field record (0x14 bytes) and renders label+value; reads struct offsets `+0x102/+0x104/+0x108/+0x10A`. | `movs r2,#0x14`, `memcpy 0x08002BEE` |
### 2.1 Screen-clear / fill
There is **no single "clear whole screen"** ROM export separate from the loop; the stock code
clears by writing zero columns. **To clear a region**, call `blit_cols(page, count, NULL, 0)`
(`@0x08008D24` with `src==0` → writes `0x00`) per page, or `blit_rect` (`@0x08008D62`) with a
zero bitmap. **To clear the full 128×64:** loop `page = 0..7`, `blit_cols(page, 128, NULL, 0)`.
`draw_string` with a run of `0x20` (space) glyphs also erases a text row.
---
## 3. Coordinate system & text metrics
```
col/x → 0 ........................................ 127 (128 px wide)
page 0 ┌───────────────── status bar (icons) ─────────────┐ y 0..7
page 1 │ │ y 8..15
page 2 │ text/GBK cells are 16 px tall = 2 pages │
... │ │
page 7 └───────────────────────────────────────────────────┘ y 56..63
```
- **Origin:** top-left. `page` grows downward, `x`/`col` grows rightward.
- **`page` (row) unit:** one page = **8 vertical pixels**. Valid 0..7. Passed as `r0` to blitters.
- **`x` (column) unit:** **pixels**, 0..127. Passed as `r1`.
- **`lcd_set_pos(page, col)`** maps `col` to the controller command `0xB7 col` (window offset)
and `page` via `0x10 | (page>>4)` / `page & 0x0F`. The blitters call it once per (page, column)
pair, so you rarely call it directly.
**Text cell metrics:**
| Font | Draw fn | Cell W × H | Advance | Glyph src (SPI) | Bytes/glyph |
|---|---|---|---|---|---|
| ASCII (normal) | `ascii_blit` / via `draw_string` | 7 × 16 px (2 pages) | **+7 px** | `0x19C000 + (ch0x20)*14` | 14 |
| GBK / CJK / Cyrillic | `gbk_blit` / via `draw_string` | 14 × 16 px (2 pages) | **+14 px** | `0x19E000 + index*28` | 28 |
| Compact | `small_char` | 5 × 8 px (1 page) | **+6 px** (with `draw_str_spaced`) | in-flash 5×8 table | 5 |
| Large digits | `big_char` | ~11 × 16 px | +12 px (`draw_number_row`) | in-flash *0x20 table | 33 (0x21) |
- **Row height** = 16 px = 2 pages. A 128×64 screen therefore fits **4 text rows** (pages 0-1,
2-3, 4-5, 6-7). Menus use page-4 for the highlighted line (`draw_string(4, …)` in the menu
field painter `@0x08014074`).
- **Chars per row:** 128/7 ≈ **18 ASCII** or **9 GBK** (2-byte) chars.
- **X-wrap:** `draw_string` auto-wraps to next 2-page cell when `x` passes ~121 (ASCII) / 114 (GBK).
---
## 4. Color / mono handling and the `mode` parameter
The panel is 1-bpp. "Color" = pixel on/off. The **`mode`** argument (5th arg of `draw_string`,
4th of `ascii_blit`/`gbk_blit`, read from stack `[sp+0x28]` in `draw_string`) selects a
per-glyph bit transform applied to the fetched font bytes **before** they are clocked out:
| mode | Name | Transform (in `ascii_blit @0x08007FB8`) | Visual |
|---|---|---|---|
| **0** | Normal | none | black text on white (pixel = font bit) |
| **1** | Inverse | `byte = ~byte`, plus edge-bit clears (`&0xFE`/`&0x7F` on alternating bytes) to keep a 1px gutter | white text in a black bar (selection highlight) |
| **2** | Outline | `byte \|= 0x80` on even bytes (adds a boundary line) | boxed/outlined text |
Evidence: `0x08007FDE cmp r7,#1 → invert loop (mvns)`; `0x0800801C cmp r7,#2 → orr #0x80 loop`.
`draw_string` forwards its 5th arg unchanged as this `mode`. **Selection highlighting in menus =
`mode 1`.** For a UI rewrite, pass `mode=1` to render the currently-selected line inverted.
---
## 5. Font/asset fetch (SPI data-flash)
Glyph bitmaps are **not in MCU flash** — they are read on demand from the external SPI data-flash
(the same 4 MB device dumped in `radio-spi-dump.bin`) by:
```c
// @0x08021828 (entry 0x08021826 sets r0=0 then falls through)
void spi_flash_read(void *dst, uint32_t src_addr, uint32_t len); // @0x08021828
```
- Issues SPI opcode **0x03** (read), clocks a **3-byte** address (or 4-byte if the chip capacity
byte is 0x18/0x19), streams `len` bytes into `dst`. Bit-banged on **GPIOB** (`0x40020400`
region, literals `0x8021820..0x8021AD8`). 34 direct BL callers — the glyph blitters are the
heavy users.
- **Font bases:** ASCII narrow `0x19C000` (14 B/glyph), GBK/wide `0x19E000` (28 B/glyph). Cyrillic
is present in the GBK bank at GB2312 row A7 (see `RT-4D_Russification.md`). A separate Unicode
codepoint index lives at SPI `0x3F0000`.
- **Constraint respected:** these are SPI **reads only** for the UI; they do not touch the codeplug
format or the CPS serial protocol. A UI rewrite reuses `spi_flash_read` as-is for fonts.
Related SPI HW primitives (do **not** reuse for writes in a UI rewrite — codeplug boundary):
`spi_send_byte @0x08021538`, `spi_read_bytes @0x08021580`, `spi_sector_erase @0x08021924`
(opcode 0x20, 4 KB), `spi_page_program @0x08021A70`.
---
## 6. Minimal "draw a screen from scratch" recipe
Using only reused stock entry points (no framebuffer needed):
```c
// ---- reused stock API (thumb addresses, call with bl / function pointers) ----
void draw_string (u8 page, u8 x, const char *s, u16 len, u8 mode); // 0x08008A50
void gbk_char_at (u8 page, u8 x, u16 gbk); // 0x08008530
void blit_cols (u8 page, u8 count, const u8 *cols, u8 src); // 0x08008D24 (src=0 => clear)
void draw_box (u8 page_origin); // 0x08008DAA
void lcd_set_pos (u8 page, u16 col); // 0x08014A7C
void lcd_write_col(u8 column_bits); // 0x08014B28
void lcd_set_brightness(u8 level); // 0x08014C34
void my_screen(void)
{
// 1. Clear the whole 128x64 (8 pages) — write zero columns.
for (u8 pg = 0; pg < 8; pg++)
blit_cols(pg, 128, 0, 0); // src=0 -> 0x00 fill
// 2. Title on the top text row (pages 0-1), normal video.
draw_string(0, 2, "MAIN", 4, 0);
// 3. A selected menu line on pages 4-5, inverse video (highlight).
draw_string(4, 1, "Channel 001", 11, 1); // mode 1 = inverse
// 4. A framed popup border (rounded box) starting at page 1.
draw_box(1);
// 5. Draw a raw 8px-tall icon: position cursor, stream columns.
static const u8 batt[8] = {0x3C,0x24,0x24,0x24,0x24,0x24,0x3C,0x18};
lcd_set_pos(0, 118); // page 0, x=118
for (u8 i = 0; i < 8; i++) lcd_write_col(batt[i]);
// No flush needed — pixels are already in the LCD.
}
```
**Notes for the rewrite:**
- Keep to the (page, x-pixel) model. A "row" is 2 pages (16 px); place text rows at pages 0,2,4,6.
- For flicker-free full-screen redraws, either repaint only changed regions (stock approach) or
keep your own `u8 fb[8][128]` SRAM shadow and blit it with `blit_rect @0x08008D62`.
- Use `mode=1` for the selected item; `mode=0` otherwise; `mode=2` for outlined labels.
- Cyrillic/CJK: put raw GB2312 double-byte codes (lead ≥0x80) in the string; `draw_string`
auto-routes them through `gbk_blit`. No firmware change needed (see Russification report).
- All of this is **display-only** and reuses `spi_flash_read` solely for font fetches — it never
writes the SPI codeplug regions and never touches the CPS serial protocol, satisfying the hard
constraint.
---
## 7. Confidence & open items
- **High confidence:** `draw_string`, `ascii_blit`, `gbk_blit`, `gbk_char_at`, `lcd_set_pos`,
`lcd_write_col`, `spi_flash_read`, `mode` semantics, font bases, char metrics, 128×64/8-page
geometry, no-framebuffer/direct-GDDRAM model, brightness PWM. All verified from disassembly +
literal pools + call sites.
- **Medium confidence:** exact controller part number (ST7565/UC1701/SSD1306-family inferred from
page addressing + `0xB7` column origin, not read from an ID); the precise GPIO pin numbers for
CLK/DATA/RS/CS on the Artery clone (register offsets are certain; silicon pin mapping is not
load-bearing). `big_char`/`draw_number_row` font-base literal not fully resolved (only the
33-byte stride is confirmed).
- **Not needed for the rewrite:** the boot-time panel command-init sequence (reused as-is via the
stock boot); re-deriving it would only matter for a from-scratch panel bring-up.
+349
Просмотреть файл
@@ -0,0 +1,349 @@
# RT-4D — DMR / FM100B interface (MCU side) — API reference (`dmr`)
Scope: the **MCU-side** code that talks to the FM100B DMR baseband over **USART3 (`0x40004800`)**. All addresses are absolute in the MCU app image (`rt4d_stock_v3.25_abs_0x08002800.bin`, load base `0x08002800`, ARM Thumb). This is the layer a rewritten UI must **reuse verbatim** to do DMR: originate calls, render incoming calls, set radio ID / TG / color code / slot, SMS, and handle remote stun/kill/wake.
Boundary note: none of this touches the **SPI codeplug format** or the **CPS serial protocol** — the FM100B link is a *third*, internal, binary UART with its own `0x68…0x10` framing. It is completely independent of the CPS `0x34/0x52/region-id` framing (USART6). Reusing these functions does not change any CPS-visible format. The only codeplug coupling is **read-only**: caller-name display reads contact records from SPI `0x5C000`/`0x5E000` and the key/SMS-target table at `0x0D0000` (same layout the CPS already writes).
---
## 0. TL;DR — the callable entry points that matter
| vaddr | name (inferred) | C signature | what it does |
|---|---|---|---|
| `0x0801B044` | `fm100b_send1` | `void(u8 cmd,u8 b,u8 sub,u8 data,void* respbuf,u16 timeout)` | build+send a 1-data-byte `0x68` frame to FM100B, block until its `*Cnf` arrives (or timeout) |
| `0x0801B0C4` | `fm100b_send` | `void(u8 cmd,u8 b,u8 sub,u16 len,void* respbuf,const u8* payload,u16 timeout)` | same, with an N-byte payload |
| `0x08006C9C` | `usart3_tx_buf` | `void(const u8* buf,u16 len)` | raw byte-blit of a frame out USART3 |
| `0x08006CB8` | `usart3_tx_byte` | `void(u8 b)` | one byte out USART3->DR, spin on TC |
| `0x08003050` | `poll_serial` | `void(void)` | **pump**: run FM100B RX parse + CPS framer + RX drain once. Call this in any wait loop. |
| `0x08018CB0` | `fm100b_rx_parse` | `int(void)` | scan USART3 RX ring for one `0x68` frame, verify checksum, dispatch it; returns 1 if a frame consumed |
| `0x08006348` | `fm100b_on_frame` | `void(u8* frame)` | master `*Cnf`/`*Ind` dispatch: writes `resp[cmd]=frame[3]` then jump-tables to the per-cmd Ind handler |
| `0x08006CFC`†| `dmr_call_start_from_contact` | `void(u8 dummy, u16 contact_idx)` | originate a call to a stored contact: look up record, send cmd6 setup, latch current-call state |
| `0x08006FD8`†| `dmr_call_resend` | `void(void)` | re-send cmd6 for the latched current call (PTT continue) |
| `0x08006C4C` | `dmr_set_radio_id` | `void(u8 idHi,u8 idLo)` | cmd `0x49` — set our personal DMR ID on the module |
| `0x0800736C` | `dmr_sms_send` | `void(u16 target_or_contact)` | cmd `0x82` — send an SMS |
| `0x08006D00` | `dmr_contact_read` | `void(u8 dummy,u16 idx,...)` | read a 21-byte DMR contact record `idx*27 + 0x5E000` from SPI (for name/ID display) |
| `0x0801A38C` | `nvic_system_reset` | `noreturn void(void)` | reboot (used by remote-kill enforcement) |
`dmr_call_start_from_contact` is the function whose body begins at `0x08006CFC`/`0x08006D00`; `dmr_call_resend` body starts `0x08006FD8`. Signatures below.
Confidence: **high** on the framing, the two send primitives, the RX parser, the `fm100b_on_frame` dispatch table, the incoming-call state block, and the reset. **Medium-high** on individual command *semantics* (cmd numbers are proven from call sites; their meaning is inferred from surrounding code + the FM100B `ATC_*` symbol list in the prior report).
---
## 1. MCU ↔ FM100B wire protocol (USART3 `0x40004800`)
### 1.1 Frame format (both directions)
Every message is a framed packet built/parsed at the byte level. Layout (offsets in bytes):
```
+0 0x68 sync / SOF (constant; parser rejects anything else)
+1 cmd command id (see §2)
+2 b secondary/opcode byte (usually 1 on Req; on Ind = subtype)
+3 sub sub-command / status. On *Cnf this byte is the result code.
+4..+5 cksum 16-bit checksum, big-endian (see §1.2)
+6..+7 len payload length, big-endian (u16)
+8..+8+len-1 payload (len bytes; for send1 it is a single data byte)
+8+len 0x10 EOF / end marker (constant)
```
Total on-wire size = `len + 9`. The send1 primitive uses `len=1`, so its frame is 10 bytes (`68 cmd b sub CKh CKl 00 01 data 10`).
Evidence — `fm100b_send1 @0x0801B044`:
```
0x0801b054 movs r0,#0x68 ; str [buf+0] ; SOF
0x0801b05c strb r4,[buf+1] / r5,[+2] / r6,[+3] ; cmd,b,sub
0x0801b062 movw #0xffff ; strh [buf+4] ; cksum placeholder
0x0801b068 bl 0x800bd2c ; strh r0,[buf+6] ; len = bswap16(1)
0x0801b074 strb r7,[buf+8] ; single data byte
0x0801b078 movs #0x10 ; strb [buf+9] ; EOF marker
0x0801b07c bl 0x8002ea8 (sum16, len=10) ; checksum over 10 bytes
0x0801b086 strh r0,[buf+4] ; store bswap16(cksum) at +4
0x0801b092 ldr r0,=0x2000706e ; bl 0x8006c9c ; usart3_tx_buf(buf,10)
```
`fm100b_send @0x0801B0C4` is identical but `len=r3`, copies `payload` (`[sp+0x20]`) into `buf+8` via `memcpy 0x80062EC`, writes `0x10` at `buf+8+len`, and sends `len+9` bytes.
Helpers:
- `0x0800BD2C = bswap16(u16)` — byte-swap; used to store the BE 16-bit len and cksum.
- `0x08002EA8 = sum16(const u8* buf,u16 len)` — sum of big-endian 16-bit words → the checksum.
- `0x08021EB0 = usart_write_DR(base,byte)` (`str [base+4]`), `0x08021EA8 = usart_read_DR(base)`, `0x08021EC2 = usart_get_flag(base,mask)`.
### 1.2 TX path
- **`usart3_tx_byte @0x08006CB8`** `void(u8 b)`: optionally mirrors the byte into the RX ring when a loopback flag (`0x20000B67`) is set, then `usart_write_DR(0x40004800,b)` and spins on TX-complete (SR bit `0x80`).
- **`usart3_tx_buf @0x08006C9C`** `void(const u8* buf,u16 len)`: `for i in 0..len: usart3_tx_byte(buf[i])`.
- Shared **TX frame buffer** at SRAM `0x2000706E` (both send primitives build here; not re-entrant — the send primitives block until `*Cnf`, so a single global buffer is safe only from the main loop).
### 1.3 RX path
Per-byte RX is interrupt-driven (**USART3 ISR @0x080205B0**, IRQ 39). It pushes each byte into a **4 KB ring**:
- ring struct head/word at `0x20000C64`, data buffer at `0x200092EF`, index mask `0xFFF`.
- (There is also a 1 KB ring at `0x20000C2C`/`0x20007575`, mask `0x3FF`, filled in parallel — a secondary/debug capture.)
Draining/parsing happens in the main loop, **not** in the ISR:
- **`fm100b_rx_ring_drain @0x0801FE50`** `void(void)`: while `tail < head`, pull one byte and feed the **byte accumulator**… actually it calls `fm100b_rx_parse` per available byte via `0x08018BFC`? — the concrete flow is: `poll_serial` calls `fm100b_rx_parse` directly.
- **`fm100b_rx_parse @0x08018CB0`** `int(void)`:
1. Search the ring for a `0x68` byte (advance tail past junk).
2. Read `len = (ring[p+6]<<8)|ring[p+7]` (BE). Reject if `len >= 0x200`.
3. Require `head-tail >= len+9` bytes buffered, and `ring[p+8+len] == 0x10` (EOF).
4. Copy the whole `len+9` frame out of the ring into a linear work buffer.
5. `sum16(frame,len+9)` must equal the stored checksum at `+4`; else drop.
6. On success advance the tail past the frame and call **`fm100b_on_frame(frame)`** (`0x08006348`); return 1.
- **`poll_serial @0x08003050`** = `fm100b_rx_parse(); cps_framer(0x0801F854); fm100b_rx_ring_drain(0x0801FE50);`. **This is the cooperative pump.** Every blocking send loop (see §1.4) calls this; a rewritten UI's idle/wait loop must call it too.
### 1.4 Request/Confirm handshake (how blocking works)
Both send primitives implement a synchronous Req→Cnf:
```
resp[cmd] = 0xFF ; mark pending (resp array @0x20007476, indexed by cmd)
usart3_tx_buf(frame,len) ; send
timeout_ctr = timeout ; @0x20000C52
do { poll_serial(); } while (resp[cmd]==0xFF && timeout_ctr!=0);
```
`fm100b_on_frame` (§3) sets `resp[cmd] = frame[3]` when the matching `*Cnf` arrives, which breaks the loop. So `respbuf`/timeout args are: timeout is the last stacked arg (e.g. `0x3E8`=1000 for call setup, `0x64`=100 for config); the "respbuf" stack arg is a copy of the timeout counter seed. The **response/status code** for a command after the call returns is `resp[cmd]` at `0x20007476+cmd`.
---
## 2. Command set (MCU → FM100B `*Req`), from call sites
Extracted by decoding `(cmd=r0, b=r1, sub=r2, data/len=r3)` at every call to the two send primitives. `cmd` is proven from the immediate; the name maps to the FM100B `ATC_*Req` symbol families documented in the prior RE report (§5.4).
| cmd | via | b | sub | payload | wrapper vaddr | inferred meaning (`ATC_*Req`) |
|---|---|---|---|---|---|---|
| `0x02` | send1 | 1 | 1 | 1B | `0x0800760A` | misc mode set |
| `0x05` | send1 | 1 | 2 | data=2 | `0x08006C74` | **channel/RF config set** (`ATC_ChannelSetReq`-class) |
| `0x06` | send | 1 | *call_type* | 5B `[type,ID_be32]` | `0x08006CFC` | **DMR call setup** (`ATC_CallProcessReq`) |
| `0x07` | send | 1 | 1 | var | `0x08007180` | contact/data set (`ATC_CurChDigdataSetReq`) |
| `0x09` | send1 | 1 | 1 | 1B | `0x080076CC` | misc |
| `0x0A` | send | 1 | 1 | 5B `[type,ID_be32]` | `0x08006FD8` | **send/originate call (TX PTT)** variant |
| `0x0B` | send1 | 1 | 1 | 1B | `0x08007530` | set param |
| `0x0C` | send1 | 1 | 1 | data=0 | `0x08007598` | set param |
| `0x25` | send1 | 1 | 1 | data=1 | `0x08006C88` | init/enable |
| `0x2A` | send | 1 | 1 | 4B | `0x080074FC` | set 32-bit param |
| `0x42` | send1 | 1 | 1 | 1B | `0x080075B4` | set param |
| `0x48` | send1 | 1 | 1 | 1B | `0x080075C6` | set param |
| `0x49` | send | 1 | 1 | 4B | `0x08006C4C` | **set our radio DMR ID** (`ATC_RadioIDSetReq`) |
| `0x4D` | send1 | 1 | 1 | 1B | `0x080075F4` | set param |
| `0x55` | send1 | 1 | 1 | `data+1` | `0x080075DC` | set param (increment) |
| `0x4C` | send1 | 1 | 1 | 1B | `0x08007680` | set param |
| `0x57` | send | 1 | 1 | 2B | `0x080074E0` | set param |
| `0x62` | send | 1 | 1 | 2B | `0x080071F0` | set param |
| `0x81` | send | 1 | 1 | var | `0x080073E0` | **SMS payload block** (`SPSendInBandDataReq`) |
| `0x82` | send | 1 | 1 | 20B | `0x0800736C` | **SMS send (header+target)** |
| `0x84` | send | *r0* | *r0* | — | `0x080074BC` | **contact info query** (`ATC_CalledContactINfoQuery`) |
| `0x84` | rawTX | — | — | 10B fixed | `0x08007548` | boot/wake handshake — literal frame `68 84 01 01 00 00 00 01 00 10` sent raw via `usart3_tx_buf`, marks resp `[+0x84]`. **Verified live on hardware**: reply `68 84 00 00 87 7B 00 00 10` (status 0 = OK). Earlier listed as `0x64` — that was a misread. |
The single byte `b` is almost always `1` on a Req; on Ind frames `frame[2]` is the *subtype* selector (see §3). `sub` (`frame[3]`) is the module's status on the returned `*Cnf`.
### 2.1 Selected wrapper decompilations (callable API)
**`dmr_set_radio_id @0x08006C4C`** `void dmr_set_radio_id(u8 idHi, u8 idLo)`
```
payload[0]=idHi; payload[1]=idLo; payload[2..3]=0;
fm100b_send(cmd=0x49,b=1,sub=1,len=4,payload,timeout=0x64);
```
Sets the module's own DMR ID. (Only 2 bytes filled here; the personal ID low 16 bits — the caller composes the full 24-bit ID before calling.)
**`dmr_call_start_from_contact @0x08006CFC`** `void dmr_call_start_from_contact(u8 unused, u16 contact_idx)`
```
rec = dmr_contact_read(0xFF, contact_idx); // 21B record @ contact_idx*27 + 0x5E000
if (rec[0] > 2) { error("Call type error"); return; } // 0800aeb8 = show msg
call_type = (rec[0]==0)?1 : (rec[0]==1)?2 : (rec[0]==2)?4 : ...; // 1=Group,2=Private,4=AllCall
target_id = be32(rec[+1]); // 32/24-bit target
build payload = [call_type, target_id_be32]; // 5 bytes
fm100b_send(cmd=0x06, b=1, sub=call_type, len=5, payload, timeout=0x3E8);
// latch current-call state @0x20007DA9: [0]=call_type, [1..4]=target_id
```
This is the **originate-call** entry. It maps the contact record's stored type to the module's `call_type` (Group→1, Private→2, All→4) and sends the setup, then also fires a follow-on raw frame (`0x8006DFC` region) that TX-blits a 0x1F-byte packet.
**`dmr_call_resend @0x08006FD8`** `void dmr_call_resend(void)`
```
type = curcall[0]; id = be32(curcall[+5]); // curcall @0x20007DA9
payload=[type,id_be32]; fm100b_send(0x06,1,type,5,payload,0x3E8);
```
Re-issues the setup for the already-latched call (used to keep a group call up / PTT re-key).
**`dmr_send_call_0a @0x08006FD8`-region (`0x08007000`)** `void(u8 type, u32 id)` — cmd `0x0A`, same 5-byte `[type,id_be32]` payload, `timeout=0x3E8`. This is the alternate "start voice" path (the two, cmd6 vs cmd0xA, correspond to `ATDigCallSetupCnf` vs a direct voice-start).
**`dmr_sms_send @0x0800736C`** `void dmr_sms_send(u16 target)`
```
if (target != 0) { // resolve target contact
rec = SPI_read(0x0D0000 + (target-1)*48, 48);// SMS-target table (0x0D0000, 48B stride)
switch(rec[+1]) { type=1→grp, 4→prv, 5→all } // map record type
}
build 0x22-byte msg: dst = 0xAAAAAAAA if all-call else target;
fm100b_send(cmd=0x82,b=1,sub=1,len=20,payload,timeout=0x64); // header
// followed by cmd 0x81 payload block(s) for the text (0x080073E0)
```
**`dmr_contact_read @0x08006D00`** `void dmr_contact_read(u8 unused, u16 idx, out u8 rec[21])`
```
base = idx*27 + 0x5E000; // 27-byte stride, contacts region
SPI_read(base, 21, rec); // 0x8021828 = spi_read(dst,addr,len)
// rec[0] = contact type (0=Group,1=Private,2=AllCall); rec[+1..]=ID + name
```
The stride is **27 bytes at `0x5E000`** (= codeplug contacts `0x05C000` + `0x2000`). This is the routine the UI calls to turn a contact index into a type+ID+name for display and for call setup. (Note the on-flash contact record the CPS writes is 32 bytes at `0x5E000` per the codeplug report; the module-facing read here pulls the first 21 bytes.)
---
## 3. Incoming frames (FM100B → MCU `*Cnf` / `*Ind`) — the RX side the UI renders
### 3.1 Master dispatch `fm100b_on_frame @0x08006348`
```
void fm100b_on_frame(u8* f) {
resp[f[1]] = f[3]; // 0x20007476[cmd] = status → unblocks the Req wait
if (f[1] >= 0xC1) return;
switch (f[1]) { /* jump table @0x0800636C, cmd*4 half-word offsets */ }
}
```
Jump-table result (cmds with a *real* Ind handler; all others fall to the no-op default `0x08006C26` and only update `resp[]`):
| cmd | handler vaddr | meaning |
|---|---|---|
| `0x01` | `0x08006670` | status |
| `0x02` | `0x08006672` | status |
| `0x03``0x04` | `0x08006674`/`76` | status |
| `0x05` | `0x0800668C` | channel/config change Ind (latches new state, sets a "changed" flag) |
| **`0x06`** | **`0x080066AA`** | **INCOMING CALL Ind** — caller/TG/type → UI (see §3.2) |
| `0x07` | `0x0800671E` | **call/PTT status Ind** (call end, TX status) |
| `0x09` | `0x08006816` | call-timer/ready Ind (arms a `0x320` timer) |
| `0x0A` | `0x08006870` | **remote-command Ind** (stun/kill; see §3.3) |
| others (`0x0B`+, `0x12``0xC0`) | small `resp[]`-only stubs | pure `*Cnf` acknowledgements |
### 3.2 Incoming-call Ind `0x080066AA` — what the standby/RX screen reads
Frame layout for a cmd6 Ind: `f[8]=call_type` (1=Group, 2=Private, 4=AllCall), `f[9..12]=source(caller) ID` (BE), `f[13..16]=dest/TG ID` (BE). Handler:
```
status = f[3] → 0x20000C3C / 0x20000C3D
call_type: 1→0, 2→1, 4→2 → curcall[0] @0x20007DC2
dest_id = be32(f[+0xD]) → curcall[+1] (u32) (0x80112B8 = be32_to_u32)
src_id = be32(f[+9]) → curcall[+5] (u32) (the CALLER id the UI shows)
if (first-of-call flag) {
slot = curcall[+1]>>4; set_rx_slot_indicator(slot); // 0x8018530
copy state block // 0x80062ec
}
```
**Incoming-call state block `0x20007DC2`** (this is what a rewritten RX screen reads to draw "caller / TG / type"):
```
+0 u8 call_type (0=Group, 1=Private, 2=AllCall)
+1 u32 dest_id / talkgroup (little-endian in RAM)
+5 u32 source_id (the caller's DMR ID)
```
`0x80112B8 = be32_to_u32(const u8* p)` converts the on-wire big-endian IDs. Additional call-status bytes: `0x20000C3C` (raw status), `0x20000C3D` (mirror).
Talker alias / caller *name*: the frame carries the numeric IDs only. The UI resolves the **caller name** by looking the `source_id` up against the contacts table (`dmr_contact_read` / the by-ID search at `0x08007E68 → 0x08017F60`, and `0x08006E6C` alt lookup). If no contact matches, the raw ID is shown (`Unknown station` string at `0x08028815`).
### 3.3 Remote-command Ind `0x08006870` (cmd `0x0A`) — stun / kill / wake
```
sub = f[2]; code = f[3] → 0x20000C?? state
if (code == 0xA1) show_msg(...); // e.g. remote check / stun-related
if (enabled_flag[+0x184]) {
if (code == 0xA2) { // REMOTE KILL
kill_state = 4;
persist_word = 0x4444; store @[+0xC]; // marker written to NV
0x801A900(); // commit to SPI/NV
delay(0x7D0); 0x8007946(0x7D0);
nvic_system_reset(); // 0x801A38C — reboot into killed state
}
}
```
So the enforcement of a remote kill is a **persisted `0x4444` marker + reboot** via `nvic_system_reset @0x0801A38C`. A rewritten UI that wants to *ignore* remote kill would stub this handler or the `0x184` enable flag; to *keep* stock behavior, leave `fm100b_on_frame`'s cmd-`0x0A` path intact. (`Prohibit TX` string `0x0801ED28` and `DMR Remote Kill/Stun` anchors `0x0800346C`/`0x08006B30` live on the UI side that reads these flags.)
### 3.4 Incoming SMS (module → MCU)
SMS received by the module arrives as an Ind carrying the text block; the MCU stores it into the SMS/inbox codeplug area. The upload confirm corresponds to the FM100B `ATUploadRxSmsCnf` symbol. The MCU-side receive path shares the same `fm100b_on_frame` dispatch (one of the `resp[]`-updating cmds) plus a data-copy into RAM; the inbox commit reuses the standard SPI codeplug writer (unchanged format).
---
## 4. Contact / address-book lookup for caller-name display
Two record stores are involved (both are **read-only** from DMR's perspective; the CPS owns their format):
1. **Contacts (module-facing)**`dmr_contact_read @0x08006D00`: `record = SPI[idx*27 + 0x5E000]`, 21 bytes: `[0]=type, [+1..]=ID, name`. Used both to originate calls and to name a contact index.
2. **By-ID reverse lookup** — the RX screen turns a numeric `source_id`/`dest_id` into a name via the search wrapper at `0x08007E68``0x08017F60` (walks the contacts region comparing the 24-bit ID), with an alternate at `0x08006E6C`. On a hit it renders the stored name; on a miss it renders the raw decimal ID (24-bit, max `16777215` per string `0x08007E07`).
3. **SMS-target / key-name table**`0x0D0000`, 48-byte stride (per the live SPI dump), used by `dmr_sms_send` to resolve an SMS destination.
Group IDs are stored BCD/LE in the contact record (`66 06` → TG 666, per the codeplug report); the module wire format uses **plain big-endian 24/32-bit**`be32_to_u32 @0x80112B8` and the payload-build shifts in the wrappers do the conversion. Keep both conversions if reusing these functions.
---
## 5. Call sequences for a rewritten UI
### 5.1 Boot / attach the module
```
// stock boot fires: raw 0x84 handshake (0x08007548), then a burst of config Reqs
fm100b_send1(0x05,1,2, data=2, resp, 0x64); // channel/RF config
fm100b_send (0x49,1,1, len=4, [idHi,idLo,0,0], resp, 0x64); // dmr_set_radio_id
// ... other 0x0B/0x0C/0x42/0x48/0x4D param sets as needed
// each call blocks via poll_serial() until resp[cmd] != 0xFF
```
### 5.2 Originate a DMR call (private or group)
```
// UI has a contact index (or build an ad-hoc record):
dmr_call_start_from_contact(0xFF, contact_idx); // 0x08006CFC
// → looks up record, maps type, sends cmd6 [type,id_be32], latches curcall@0x20007DA9
// while PTT held, keep the call up:
while (ptt_down) { dmr_call_resend(); poll_serial(); } // 0x08006FD8, re-sends cmd6/0x0A
// on release: send the corresponding stop/param Req and drop PTT.
```
For a raw call without a stored contact: build `payload=[call_type, target_id_be32]` yourself and call `fm100b_send(0x06,1,call_type,5,payload,0x3E8)` (or cmd `0x0A` for the voice-start variant), then set `curcall@0x20007DA9`.
### 5.3 Render an incoming call (standby/RX screen)
```
// In the main loop, keep pumping the link:
poll_serial(); // 0x08003050 — drains USART3, dispatches Inds
// When cmd6 Ind fires, the state block @0x20007DC2 is populated:
u8 type = curcall_rx[0]; // 0=Group,1=Private,2=AllCall
u32 tg = *(u32*)(curcall_rx+1); // talkgroup / dest
u32 src = *(u32*)(curcall_rx+5); // caller DMR ID
// Resolve caller name:
name = contact_name_by_id(src); // 0x08017F60 search; fallback → decimal(src)
draw: "<name or src> → TG <tg>" (type-dependent: SID/GID/AID labels @0x0800A36C)
// status/end: cmd7 Ind updates call-status bytes; cmd9 arms the call timer.
```
### 5.4 Send an SMS
```
// text staged in RAM by the editor; target is a contact index or 0 for the default
dmr_sms_send(target); // 0x0800736C → cmd 0x82 header + cmd 0x81 payload
// wait resp[0x82]/resp[0x81]; ATUpload* / send-fail handled by fm100b_on_frame.
```
---
## 6. RAM state map (DMR)
| addr | size | contents |
|---|---|---|
| `0x2000706E` | ~0x200 | TX frame build buffer (`0x68…0x10`) |
| `0x20007476` | 0xC1 | **`resp[cmd]`** response/status array (0xFF=pending) |
| `0x20000C52` | u16 | Req timeout counter |
| `0x200092EF` | 0x1000 | USART3 RX ring data |
| `0x20000C64` | — | USART3 RX ring head/index |
| `0x20007575` | 0x400 | secondary RX capture ring |
| `0x20000B67` | u8 | USART3 TX→RX loopback capture flag |
| `0x20007DA9` | 5+ | **outgoing** current-call: `[0]=type,[1..4]=id`, `[+5]=id copy` |
| `0x20007DC2` | 9 | **incoming** call: `[0]=type,[1..4]=dest/TG,[5..8]=caller id` |
| `0x20000C3C/3D` | u8×2 | incoming-call status bytes |
---
## 7. Reuse guidance for the UI rewrite
- **Keep and call as-is**: `fm100b_send1 (0x0801B044)`, `fm100b_send (0x0801B0C4)`, `poll_serial (0x08003050)`, `fm100b_rx_parse (0x08018CB0)`, `fm100b_on_frame (0x08006348)`, `dmr_contact_read (0x08006D00)`, `nvic_system_reset (0x0801A38C)`, and the wrappers in §2. They contain the whole USART3 protocol and are codeplug/CPS-neutral.
- **Read, never reframe**: the incoming-call block `0x20007DC2` and `resp[]` `0x20007476` are your UI inputs. Poll `poll_serial()` from your event loop; read those to render.
- **To originate**: prefer the wrappers (`dmr_call_start_from_contact`, `dmr_sms_send`, `dmr_set_radio_id`) so type-mapping and current-call latching stay correct. If you bypass them, replicate the Group→1/Private→2/AllCall→4 mapping and the big-endian ID packing.
- **Do not** re-implement framing/checksums yourself — call the two send primitives; that guarantees the FM100B never sees a malformed frame and keeps the module firmware (unchanged) happy.
- **Color code / timeslot** are set through the per-channel config Reqs (`cmd 0x05` and the `0x0B/0x0C/0x42/0x48/0x4D` family — set from the channel record fields); these carry no codeplug-format dependency beyond reading the channel record the CPS already writes.
## Open items (medium confidence, worth a second pass on-target)
- Exact `sub`/field meaning of the `0x0B/0x0C/0x42/0x48/0x4D/0x55/0x57/0x62` param Reqs (which is color-code vs squelch vs power vs denoise) — the cmd numbers are certain; individual mapping needs tracing each wrapper's caller (channel-settings menu handlers).
- The cmd `0x84` `ATC_CalledContactINfoQuery` return payload layout (talker-alias source) — its Ind path falls to the default stub here, so alias text likely arrives on a different cmd or is assembled MCU-side from contacts.
- Encryption enable/key-select Req (menu `Encryption Set @0x080165D0`) — routed through one of the param Reqs above; not yet pinned to a specific cmd byte.
+311
Просмотреть файл
@@ -0,0 +1,311 @@
# RT-4D Input Subsystem — Keypad / PTT / Dispatch API
Reverse-engineered from `rt4d_stock_v3.25_abs_0x08002800.bin` (Cortex-M4F Thumb, load base `0x08002800`).
Scope: keypad matrix scan, debounce, key-code map, PTT/side keys, event delivery, and the callable
API + hook points a rewritten UI would use to read input and route keys to a custom router.
All addresses are absolute MCU flash vaddrs. RAM state vars are `0x2000xxxx`.
---
## 0. TL;DR — the input pipeline
```
TICK (main loop, ~1ms cadence)
keypad_task(col) 0x0801B398 ── drives one column output line (GPIOA/GPIOF ODR), cycles col 0..3
│ calls per column:
keypad_scan_col(col) 0x0801B294 ── reads 4 row inputs (GPIOF1, GPIOA8, GPIOA9, GPIOB11),
│ sets/clears bit (1<<(col*4+row)) in RAW word 0x20000B7C (active-low)
keypad_decode() 0x0801130C ── maps RAW bitmask 0x20000B7C → compact KEY CODE (0x00..0x12, 0xFF=none)
keypad_process() 0x08005A14 ── debounce + short/long/repeat classify; writes KeyEv struct @0x20000B57
│ KeyEv[+2]=delivered key, KeyEv[+3]=event type (1=short/long,2=repeat)
ui_input_service() 0x08005E54 ── main-loop input step: calls keypad_process, reads KeyEv[+2],
│ then calls the screen router, then clears KeyEv[+2]
screen_dispatch(ctx,aux,key) 0x08018DF4 ── reads current-screen id ctx[+1] (ctx=0x20002120),
tbb-dispatches to the active screen's key handler
```
**This is a polled GPIO 4×4 matrix (NOT an ADC ladder).** ADC1 is used only for battery/RSSI, not keys.
**Up/Down are matrix keys, not a rotary encoder.** PTT and the two side keys are separate GPIO reads.
---
## 1. Hardware: GPIO 4×4 key matrix
The keypad is a 4-column × 4-row matrix. Columns are driven low one at a time (outputs), rows are read
(active-low inputs, pulled high). Confirmed from `keypad_scan_col` (`0x0801B294`) which reads, per column,
four row pins via the GPIO bit-read helper and packs them into the 16-bit raw word `0x20000B7C`:
| Row idx | Port/pin (mask passed to gpio_read_pin) | Evidence (in 0x0801B294) |
|---|---|---|
| row 0 | **GPIOF pin1** (`0x40021400`, mask `0x0002`) | `ldr 0x40021400; movs r1,#2; bl 0x8021316` |
| row 1 | **GPIOA pin8** (`0x40020000`, mask `0x0100`) | `ldr 0x40020000; mov r1,#0x100` |
| row 2 | **GPIOA pin9** (`0x40020000`, mask `0x0200`) | `ldr 0x40020000; mov r1,#0x200` |
| row 3 | **GPIOB pin11** (`0x40020400`, mask `0x0800`) | `ldr 0x40020400; mov r1,#0x800` |
For each row: `bit = 1 << (col*4 + row)`; if `gpio_read_pin()==0` (pin low = pressed) → `raw |= bit`,
else `raw &= ~bit`. So `0x20000B7C` is a live pressed-keys bitmap, one bit per matrix cell.
Column drive lives in `keypad_task` (`0x0801B398`): a 4-state machine using `0x20000B7A` as the current
column index; each phase writes the column-select via `gpio_write_pin`/BSRR on GPIOA (`0x40020000`) and
GPIOF (`0x40021400`, BSRR/ODR region seen as `0x40021428`/`0x40020428`), then invokes `keypad_scan_col`
for that column. Columns cycle 0→1→2→3 across successive ticks.
### GPIO helper primitives (callable API)
| vaddr | signature | behavior |
|---|---|---|
| `0x08021316` | `int gpio_read_pin(GPIO_TypeDef* port, uint32_t mask)` | returns 1 if **all** masked IDR (`+0x10`) bits set, else 0 |
| `0x08021C6E` | `void gpio_write_pin(GPIO_TypeDef* port, uint32_t mask, int state)` | state 1 → `ODR(+0x0C)|=mask`; state 0 → `ODR&=~mask` |
| `0x08021C4C` | `void gpio_bsrr(GPIO_TypeDef* port, uint32_t mask)` (bit-set/reset via BSRR) | used by column drive & LCD |
---
## 2. KEY CODE MAP — `keypad_decode()` @ `0x0801130C`
Signature: **`uint8_t keypad_decode(void)`** — reads the raw matrix word `*(uint16_t*)0x20000B7C`
and returns the firmware key code. Returns `0xFF` when nothing/unknown is pressed. Every comparison in
the function reads the same `ldrh [0x20000B7C]`; verified all 19 literal loads resolve to `0x20000B7C`.
### Raw matrix-bit → key code (single-key)
| raw `0x20000B7C` value | KEY CODE (return) | physical key (inferred) |
|---|---|---|
| `0x4000` | `0x00` | **0** |
| `0x0002` | `0x01` | **1** |
| `0x0020` | `0x02` | **2** |
| `0x0200` | `0x03` | **3** |
| `0x0004` | `0x04` | **4** |
| `0x0040` | `0x05` | **5** |
| `0x0400` | `0x06` | **6** |
| `0x0008` | `0x07` | **7** |
| `0x0080` | `0x08` | **8** |
| `0x0800` | `0x09` | **9** |
| `0x0001` | `0x0B` | **Menu / M** (col0,row0) |
| `0x0010` | `0x0C` | **Up** ▲ |
| `0x0100` | `0x0D` | **Down** ▼ |
| `0x1000` | `0x10` | **Exit / Back** |
| `0x2000` | `0x0E` | **\*** (star) |
| `0x8000` | `0x0F` | **#** (hash) — used elsewhere as "menu enter" sentinel |
Note key code `0x0A` is not produced by a single raw bit; it is produced only by the combo pattern below
(it is the "PTT/M combined" boot code). The digit codes `0x00..0x09` map 1:1 to digits 0-9.
### Combo / multi-line patterns (special)
| raw `0x20000B7C` | KEY CODE | meaning |
|---|---|---|
| `0x1111` | `0x0A` | all-column row0 held (boot key-combo / factory) |
| `0x2222` | `0x11` | **Side key 1** family / all-column row1 |
| `0x4444` | `0x12` | **Side key 2** family / all-column row2 |
> Interpretation of `0x11`/`0x12` as the two programmable side keys is corroborated by
> `ui_input_service` (`0x08005E54`), which special-cases `KeyEv[+2]==0x11` and `==0x12` *before* the
> normal screen dispatch (see §4), matching the CPS "Side Key 1/2 (Short/Long)" menu items.
### Key-code enum (for the rewrite)
```c
enum key {
KEY_0=0x00, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, // 0x00..0x09
KEY_COMBO_A=0x0A, // boot combo
KEY_MENU=0x0B, // "M"
KEY_UP=0x0C,
KEY_DOWN=0x0D,
KEY_STAR=0x0E, // '*'
KEY_HASH=0x0F, // '#' (also menu-enter sentinel)
KEY_EXIT=0x10, // Back/Exit
KEY_SIDE1=0x11,
KEY_SIDE2=0x12,
KEY_NONE=0xFF
};
```
---
## 3. Debounce + short/long/repeat — `keypad_process()` @ `0x08005A14`
Signature: **`void keypad_process(void)`**. Called once per input-service pass from `ui_input_service`.
Operates on the **key-event struct `KeyEv` @ `0x20000B57`** (byte fields unless noted):
| field | addr | meaning |
|---|---|---|
| `KeyEv[+1]` | `0x20000B58` | live decoded key this pass (`= keypad_decode()`), `0xFF`=none |
| `KeyEv[+2]` | `0x20000B59` | **delivered event key** consumed by the UI (`0xFF` when no event) |
| `KeyEv[+3]` | `0x20000B5A` | **event type**: `1` = press/release (short or long), `2` = auto-repeat |
| `KeyEv[+4]` | `0x20000B5B` | repeat-active flag |
| `KeyEv[+5]` | `0x20000B5C` | `uint16_t` **hold-duration counter** (ticks; incremented while held) |
Behavior (decoded from `0x08005A14``0x08005B54`):
1. `KeyEv[+1] = keypad_decode()`. Also reads PTT: `gpio_read_pin(GPIOA, 0x1000)` (GPIOA pin12) → PTT state at `0x20000B75` (`KeyEv[+0x1E]`); PTT keycode is `0x11`-adjacent handling.
2. While a key stays held: `KeyEv[+5]++` (bounded).
3. On **release** (`KeyEv[+1]==0xFF`) with `KeyEv[+5] > 10`: publish the just-released key as an event (`KeyEv[+2]=key, KeyEv[+3]=1`); otherwise call `key_event_clear()` (`0x08012C24`).
4. **Long/repeat threshold `0x2BC` (700 ticks)**: if same key held and `KeyEv[+5] > 0x2BC` → publish repeat (`KeyEv[+2]=key, KeyEv[+3]=2, KeyEv[+4]=1`). A secondary `>10` gate distinguishes the short vs long delivery. Beep feedback is emitted via `0x8014CB8` on valid/invalid keys.
5. Screen-change edge (`0x08005B38`): if the current screen id changed vs `KeyEv[+1]`, resets the hold counter — prevents key bleed across screens.
Supporting call: `key_event_clear()` **`0x08012C24`** — `void key_event_clear(void)` sets
`KeyEv[+0..+2]=0xFF`, `KeyEv[+3..+5]=0`. **This is the "consume/ack the pending key" API.**
---
## 4. Delivery to the active screen — `ui_input_service()` @ `0x08005E54`
Signature: **`void ui_input_service(void)`** — the main-loop input step. Sequence:
1. `keypad_process()` (`0x08005A14`) — refresh KeyEv.
2. Service a couple of periodic timers (`0x200201DC`/`0x201F5E4` housekeeping — not input).
3. Read `KeyEv[+2]` (delivered key). Side keys `0x11`/`0x12` are handled specially (their own screen-independent action path) and are **not** forwarded to the generic router. `0xFF` (no key) is skipped.
4. Otherwise call **`screen_dispatch(ctx=0x20002120, aux=0x20002014, key=KeyEv[+2])`** (`0x08018DF4`).
5. After dispatch, clear: `KeyEv[+2]=0xFF` (`0x20000B59`) and `ctx[+0x15]=0xFF` (`0x20002135`).
6. Then paints: `0x080136E4` (status), `0x080142C0` (menu/standby render).
### The router — `screen_dispatch(ctx, aux, key)` @ `0x08018DF4`
Signature: **`void screen_dispatch(void* ctx, void* aux, uint8_t key)`** — `ctx = 0x20002120`.
- If `key == 0x0F` (`#`): enter/menu path → `0x0800F0E4` then `0x08005484` (menu-enter), return.
- Else read **current-screen id = `ctx[+1]` (byte at `0x20002121`)**, range 0..5, and `tbb`-dispatch:
| `ctx[+1]` | screen key handler | notes |
|---|---|---|
| 0 | `0x08018E5A` | **standby / main VFO screen** |
| 1 | `0x08018E5A` | (shares standby handler) |
| 2 | `0x08019018` | secondary screen (dual-area / menu list) |
| 3 | `0x0801926C` | screen 3 |
| 4 | `0x08018F08` | screen 4 |
| (5) | falls through | |
Inner standby handler `0x08018E5A(ctx, aux, key)`: `if key==0xFF return;` sets `ctx[+0x15]=0xFF`;
then `key-0x0A; cmp #7` `tbb` to handle the nav/function keys `0x0A..0x10` (Menu/Up/Down/*/#/Exit),
and the default branch treats numeric keys as ASCII (`key+0x30`) → digit-entry handler `0x080134F8`.
Menu navigation calls `0x08007984`; audible feedback via `0x08014CB8` (beep freq `0x1B8` ok / `0x65C` err).
**`ctx[+1]` (`0x20002121`) is the current-screen selector, and the `tbb` table at `0x08018E16` is the
screen→handler map. This is the exact hook point for a rewritten UI router** (see §7).
---
## 5. PTT, side keys, monitor, and analog inputs
- **PTT**: GPIO input **GPIOA pin12** (`gpio_read_pin(0x40020000, 0x1000)`), read inside `keypad_process`
and also standalone at `0x08005A32` (parent `0x08005A14`). PTT is *not* part of the matrix; it latches
into `KeyEv[+0x1E]` (`0x20000B75`) and gates TX. A separate small helper stores PTT/key state and
compares against key code `0x11`.
- **Side key 1 / Side key 2**: surface as decoded key codes `0x11` / `0x12` (combo patterns `0x2222` /
`0x4444`), intercepted in `ui_input_service` (`0x08005E54`) ahead of the router, matching the CPS
"Side Key 1/2 Short/Long" definitions. Their configured action is looked up from settings there.
- **Monitor / squelch-open**: driven from the same key/side-key path (no dedicated GPIO monitor line found
separate from the side keys); the "Monitor" function is a side-key/long-press action, not a distinct pin.
- **Rotary/channel knob & Up/Down**: **no rotary encoder**. Channel change is the matrix Up (`0x0C`) /
Down (`0x0D`) keys handled by the active screen. No quadrature-decode code exists.
- **Volume/power knob**: analog volume is a hardware potentiometer in the audio path (not MCU-sampled);
no ADC channel is decoded as a knob position. **ADC1 (`0x40012000`) is battery voltage + RSSI only**
`adc_init` @ `0x08002D64`, battery read/convert @ `0x08010960` (`raw*4/0x42` scaling → `BATT:x.xV`),
ADC EOC counters incremented in the ADC ISR `0x08002D1C`. None of this feeds key input.
### Boot-time key-combo handler `0x08003060`
`void boot_key_scan(void)` — at power-on reads `keypad_decode()` directly (not through KeyEv) and matches
combos: `0x11`, `0x0A` (→ enters a special mode: `0x080149D4`/`0x0801492C`, PC-programming/init screen),
`0x0F`, etc. This implements the hold-key-at-power-on entries (flash mode = **PTT held at power-on**). Uses the same key codes as §2.
---
## 6. RAM state variables (input)
| addr | name | type | meaning |
|---|---|---|---|
| `0x20000B7A` | `kp_cur_col` | u8 | current column being driven/scanned (0..3) |
| `0x20000B7C` | `kp_raw` | u16 | live pressed-key bitmap `1<<(col*4+row)`, active-low packed |
| `0x20000B57` | `KeyEv` | struct | key-event struct (base; see §3 for fields +1..+5) |
| `0x20000B58` | `KeyEv.live` | u8 | live decoded key |
| `0x20000B59` | `KeyEv.key` | u8 | **delivered event key** (read by router; `0xFF`=none) |
| `0x20000B5A` | `KeyEv.type` | u8 | 1=short/long press, 2=auto-repeat |
| `0x20000B5B` | `KeyEv.rep` | u8 | repeat-active flag |
| `0x20000B5C` | `KeyEv.dur` | u16 | hold-duration counter (ticks); long/repeat @ `0x2BC`=700 |
| `0x20000B75` | `KeyEv.ptt` | u8 | PTT pressed flag (GPIOA12) |
| `0x20002120` | `ui_ctx` | struct | UI/screen context passed to router |
| `0x20002121` | `ui_ctx.screen` | u8 | **current-screen id** (router `tbb` selector, 0..5) |
| `0x20002135` | `ui_ctx[+0x15]` | u8 | per-frame scratch (cleared after dispatch) |
---
## 7. CALLABLE API for a rewritten UI
### 7a. Read input (poll model — recommended)
The cleanest reuse is to keep the stock scan/decode/debounce and read the published event:
```c
// stock addresses (Thumb; call with bit0 set)
uint8_t keypad_decode(void); // 0x0801130C -> raw key code, 0xFF=none (no debounce)
void keypad_process(void); // 0x08005A14 -> updates KeyEv (debounce+long/repeat)
void key_event_clear(void); // 0x08012C24 -> ack/consume pending key
#define KeyEv_key (*(volatile uint8_t*)0x20000B59) // delivered key, 0xFF=none
#define KeyEv_type (*(volatile uint8_t*)0x20000B5A) // 1=press,2=repeat
#define KeyEv_dur (*(volatile uint16_t*)0x20000B5C) // hold ticks (for your own long-press cutoff)
#define KeyEv_ptt (*(volatile uint8_t*)0x20000B75) // PTT
// Our UI main loop:
for(;;){
keypad_process(); // let stock code scan+debounce
uint8_t k = KeyEv_key;
if(k != 0xFF){
my_router(k, KeyEv_type); // <-- our dispatch
key_event_clear(); // consume
}
// ... our render ...
}
```
You do **not** need to touch the timer ISRs: `keypad_task`/`keypad_scan_col` are invoked from the stock
tick path that also runs `keypad_process` housekeeping; if you replace the main loop you can call
`keypad_task(col)` yourself per tick, or simpler—call `keypad_process()` which reads the already-scanned
`kp_raw`. (If you fully own the loop, drive `keypad_task(0..3)` round-robin each ~1ms so `kp_raw` refreshes.)
Raw / low-level entry points if you want to bypass debounce:
```c
int gpio_read_pin (void* port, uint32_t mask); // 0x08021316
void gpio_write_pin(void* port, uint32_t mask,int);// 0x08021C6E
void keypad_scan_col(int col); // 0x0801B294 scans one column into kp_raw
void keypad_task(void); // 0x0801B398 column-drive state machine (call each tick)
```
### 7b. Hook to route keys to OUR router (drop-in replacement)
The single interception point is **`ui_input_service` (`0x08005E54`)** → it calls
**`screen_dispatch(ctx=0x20002120, aux=0x20002014, key)` at `0x08005F1E`**. Two options:
1. **Replace the router (minimal patch):** repoint the `bl 0x08018DF4` at `0x08005F1E` to our own
`router(ctx, aux, key)`. We then own all per-screen handling while stock keypad scan/decode/debounce,
PTT, side-key pre-handling, and post-dispatch clear remain intact. Our router reads
`ui_ctx.screen` (`0x20002121`) as the active-screen id (or we manage our own screen id) and dispatches.
2. **Replace the whole input+render loop:** call `keypad_process()` ourselves (7a) and never enter
`ui_input_service`; then stock `screen_dispatch` and the render calls (`0x080136E4`, `0x080142C0`) are
bypassed entirely — full UI ownership. Keep calling stock lower-level draw/RF/DMR APIs.
Either way the **serial/CPS protocol and SPI codeplug format are untouched** — the input path has no
contact with the USART6 framer (`0x0801F864`) or the SPI region writers, so replacing the UI router does
not affect CPS compatibility.
---
## 8. Confidence
| Item | Confidence | Basis |
|---|---|---|
| `keypad_decode` @0x0801130C + full key-code map | **High** | 19 loads all resolve to `0x20000B7C`; explicit `cmp`/`movs r0,#code`; single-exit |
| GPIO 4×4 matrix (not ADC ladder); row pins GPIOF1/GPIOA8/9/GPIOB11 | **High** | `keypad_scan_col` masks `2/0x100/0x200/0x800` on distinct ports + `1<<(col*4+row)` packing |
| `keypad_process` debounce + long/repeat (`0x2BC` threshold) + KeyEv layout | **High** | full disasm `0x08005A14`; field offsets confirmed via stores |
| `ui_input_service``screen_dispatch` route; ctx `0x20002120`, screen id `[+1]` | **High** | resolved call-site literals + `tbb` table at `0x08018E16` |
| PTT = GPIOA pin12; ADC1 = battery/RSSI only (no knob) | **High** | `gpio_read_pin(GPIOA,0x1000)` in key path; ADC only in battery/ISR |
| Physical-key labels (which code = Menu/Up/Down/Exit/*/#) | **Medium-High** | inferred from standby handler usage (digit ASCII, nav tbb, `#`=menu-enter, `0x11/0x12`=side keys); exact silk-screen assignment of the four *non-digit* codes 0x0B/0x0C/0x0D/0x10 should be confirmed on-device |
| Side keys = codes `0x11`/`0x12` (combo `0x2222`/`0x4444`) | **Medium** | special-cased before router; matches CPS side-key menu; combo-pattern origin unusual—verify on hardware |
+275
Просмотреть файл
@@ -0,0 +1,275 @@
# RT-4D Firmware — Main Loop, UI State Machine & UI-Rewrite Hook Points
**Key:** `main-loop` · **Target:** `stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin` (ARM Cortex-M4F Thumb, load/vaddr base `0x08002800`).
All disassembly via capstone `CS_ARCH_ARM + CS_MODE_THUMB`.
This is the hook-point document for a full UI rewrite. It nails down (1) the boot→superloop path, (2) the single UI screen-state variable and the **two mirror-image dispatch tables** (draw + input) keyed on it, (3) how screens redraw and transition, and (4) the exact vaddrs and strategy to redirect rendering + navigation into custom code while keeping all lower-level radio/codeplug/serial APIs intact.
---
## 0. TL;DR — the hook
- **Screen-state variable:** `g_screen = *(uint8_t*)0x200008B3` (0..11). A shadow `*(uint8_t*)0x200008B2` holds the previous screen. `0x200008B3` is referenced by **55** literal-pool words across the image — it is the central UI selector.
- **Draw dispatcher:** `ui_draw_dispatch @0x0801E1BC``tbb`-jumps on `g_screen` (12 entries) to the current screen's incremental redraw routine. Called every UI tick from the scheduler at `0x080207E6`.
- **Input dispatcher:** `ui_process_key @0x0801E6EC` — pulls a keycode/keystate from the key struct at `0x20000B57`, then `tbb`-jumps on `g_screen` (12 entries, table `@0x0801E780`) to the current screen's key handler `handler(u8 keycode, u8 keystate)`. Called every UI tick from the scheduler at `0x08020816`.
- **Hook strategy (recommended):** patch the two `bl` call sites in the scheduler — `0x080207E6` (`bl ui_draw_dispatch`) and `0x08020816` (`bl ui_process_key`) — to call your own router. Your router owns rendering + navigation and calls the stock lower-level APIs (draw_string, keys, RF, DMR, codeplug). This is a 2-instruction redirect and leaves the serial/CPS path and codeplug format completely untouched. See §5.
---
## 1. Boot → application superloop
### 1.1 Reset / CRT startup (library glue, not app logic)
```
Reset 0x08002AC0: SystemInit(0x0801DA2D) ; then bx __main(0x080029E1)
__main 0x080029E0: bl 0x080029E8 (__scatterload / RW+ZI init, RLE decompressor @0x08002A1E)
bl 0x08002AA0 (__rt_entry)
__rt_entry 0x08002AA0: sequence of ARM C-lib inits, then:
bl 0x0802136C <-- the real application main / superloop
```
The `0x08002AA0..0x08002D18` cluster is ARM compiler runtime (heap/stack setup, `bkpt 0xAB` semihosting stubs, the ADC ISR tail). The application proper is **`app_main @0x0802136C`**.
### 1.2 `app_main @0x0802136C` — the top-level superloop ★ HARD ENTRY POINT
```
0x0802136C movs r4,#0
0x0802136E bl 0x0801DAD0 ; low-level HW/clock/periph bring-up
0x08021372 ldr r0,=0x08002800 ; ldr r1,=0xE000ED08 ; str r0,[r1] ; VTOR = app vector base
0x08021378 bl 0x08021024 ; init B
0x0802137C movs r0,#0xC8 ; bl 0x08007946 ; init C (display/boot?)
0x08021382 bl 0x080127D4 ; init D
0x08021386 bl 0x08003060 ; init E (codeplug/settings load)
0x0802138A b 0x08021414 ; -> jump to loop top
--- LOOP TOP ---
0x0802138C if (*(u8*)0x20000C54) bl 0x0801A38C ; reboot flag -> NVIC_SystemReset
0x08021396 modeA = *(u8*)0x20000B66 ; serial/session mode gate
modeB = *(u8*)0x20000C12
spiMode = *(u8*)0x20000C57
if (modeA==0 && modeB!=2 && modeB!=4 && spiMode==0)
0x080213B2 bl 0x080207DC ; ★ NORMAL UI TICK (radio running)
else ... ; PC-programming / SPI-write session paths:
0x080213C2 modeB==2 -> bl 0x08019A7C ; CPS mode handler
0x080213E2 modeB==4 -> bl 0x0801F84C ; serial framer
0x080213EE modeA!=0 -> bl 0x0801F84C ; bl 0x0801F540 ; drain FM100B rx (0x200082EF, 0x08006CB8)
0x0802140C bl 0x0801F854 ; serial poll (every iteration)
0x08021410 bl 0x0801FE50 ; housekeeping (every iteration)
0x08021414 b 0x0802138C ; loop
```
**Interpretation.** The superloop first honours a reboot request, then branches on **serial-session state**: when the radio is *not* in a PC-programming / SPI-write session (`0x20000B66==0`, `0x20000C12∉{2,4}`, `0x20000C57==0`), it runs the **normal UI tick `0x080207DC`**. Otherwise it services the CPS/serial paths. `0x0801F854` (serial poll) and `0x0801FE50` (housekeeping) run unconditionally every pass.
> **Boundary note (respect the CPS/codeplug contract).** The serial session vars `0x20000B66 / 0x20000C12 / 0x20000C57 / 0x20000C54` and the handlers `0x08019A7C / 0x0801F84C / 0x0801F540 / 0x0801F854` are the **CPS protocol + SPI region-write engine** (documented in RT-4D_RE_Report §4). A UI rewrite must leave this entire `else` branch and the two unconditional serial calls **exactly as-is** — that is the serial/CPS boundary. Only replace what happens *inside* the normal UI tick `0x080207DC`.
| vaddr | name | signature | notes |
|---|---|---|---|
| 0x0802136C | `app_main` | `void app_main(void) __attribute__((noreturn))` | The application superloop. Sets VTOR, runs inits, then loops. **Do not relocate** — reset path branches here. |
| 0x0801DAD0 | `hw_init` | `void hw_init(void)` | clock/peripheral bring-up (called first) |
| 0x08003060 | `codeplug_load_init` | `void(void)` | last init; loads settings/channels from SPI (candidate) |
| 0x0801A38C | `nvic_system_reset` | `void(void) noreturn` | writes AIRCR `0x05FA0004` (reboot). Gated by `0x20000C54`. |
| 0x080207DC | `ui_tick_normal` | `void(void)` | **the normal-mode UI scheduler** (see §2) |
| 0x0801F854 | `serial_poll` | `void(void)` | runs every loop; part of CPS path — keep |
| 0x0801FE50 | `housekeeping` | `void(void)` | runs every loop (battery/timers) |
---
## 2. `ui_tick_normal @0x080207DC` — the cooperative UI scheduler
This is **not** the state machine itself; it is a fixed list of periodic subsystems, several gated by down-counters so they run at different rates. The screen draw + key dispatch are two of its calls.
```
0x080207DE bl 0x0801F84C ; serial framer (shared)
0x080207E2 bl 0x0801F9C0 ; ? (reads 0x20000C8A menu-key state, 0x200029BB struct)
0x080207E6 bl 0x0801E1BC ; ★ ui_draw_dispatch (SCREEN DRAW — hook here)
0x080207EA bl 0x0801E050 ; ui_sidekey_dispatch (side/long-press hotkeys — see §3.3)
0x080207EE bl 0x0801E3F8 ; build display line buffer (reads 0x200008B3)
0x080207F2 bl 0x0801FAC8 ; ...
0x080207F6 bl 0x0801F504
0x080207FA bl 0x0801FDA4
0x080207FE bl 0x0801FDDC
0x08020802 bl 0x0801FE0C
0x08020806 bl 0x0801DFD0
--- period-gated tasks: counter at 0x20000BF1.. reloads to a period when it hits 0 ---
0x0802080A if(--tick@0x20000BF1==0){reload 1; bl 0x0801E6EC(key?) ...} see note
0x08020816 bl 0x0801E6EC ; ★ ui_process_key (SCREEN INPUT — hook here)
0x0802081A bl 0x08020288
... 0x20000BF2 (reload 4), 0x20000BF3 (reload 0xA), 0x20000BF5 (0xC8),
0x20000BF6 (0x1F4), 0x20000BF8 (0x3E8): slower periodic tasks (RSSI, battery, scan, etc.)
0x080208DC return
```
Concretely, the **two dispatch calls you care about** are both unconditional every UI pass:
- `0x080207E6 bl 0x0801E1BC` → screen **draw** dispatch
- `0x08020816 bl 0x0801E6EC` → screen **key** dispatch
(Down-counter reloads observed: `0x20000BF1`→1, `0x20000BF2`→4, `0x20000BF3`→0xA, `0x20000BF5`→0xC8, `0x20000BF6`→0x1F4, `0x20000BF8`→0x3E8 — these throttle the slower periodic tasks; the two dispatchers themselves are not throttled.)
---
## 3. The UI state machine
### 3.1 The state variable
```
g_screen = *(uint8_t*)0x200008B3 ; current screen id, 0..11 (0x0B)
g_screen_prev = *(uint8_t*)0x200008B2 ; previous screen (used for restore/back)
```
Both dispatchers guard with `cmp g_screen,#0x0C ; bhs <default>` before the `tbb`, so **valid ids are 0..11**.
### 3.2 The two mirror dispatch tables (draw + input)
Both are `tbb [pc,r0]` byte-offset jump tables indexed by `g_screen`. They are **positionally parallel**: index *i* in the draw table and index *i* in the input table are the same screen.
**DRAW dispatcher** `ui_draw_dispatch @0x0801E1BC`:
```
0x0801E1BE bl 0x080136E4 ; clear per-field dirty flags (row @0x20002512)
0x0801E1C2 r0 = *(u8*)0x200008B3
0x0801E1C6 cmp r0,#0x0C ; bhs 0x0801E228 (default/no-op)
0x0801E1CA tbb [pc,r0] ; table @0x0801E1CE
```
**INPUT dispatcher** `ui_process_key @0x0801E6EC` (tbb inside at `0x0801E780`):
```
0x0801E6EE bl 0x08005A14 ; keypad scan/debounce
0x0801E6F4 if(*(u8*)0x20000B5A==0) return ; keystate (0x20000B57+3) == no-key -> bail
0x0801E6FC if(*(u8*)0x20000B59==0xFF) return ; keycode (0x20000B57+2) == none -> bail
... global lock / special-mode guards ...
0x0801E778 r0 = *(u8*)0x200008B3
0x0801E77C cmp r0,#0x0C ; bhs 0x0801E804 (default)
0x0801E780 tbb [pc,r0] ; table @0x0801E784
each case: r2=0x20000B57; r1=[r2,#3](keystate); r0=[r2,#2](keycode); bl <handler>
0x0801E806 after dispatch: *(u8*)(0x20000B57+2) = 0xFF ; consume keycode
```
The **key event struct** is at `0x20000B57`: byte +2 = keycode, byte +3 = keystate/edge (1 = press/repeat). Handlers receive `(r0=keycode, r1=keystate)`.
### 3.3 Screen table — id → {draw handler, input handler}
Verified by decoding both `tbb` tables (bytes are half-word offsets from the table base):
| id | draw handler | input handler | inferred screen | evidence |
|---:|---|---|---|---|
| 0 | `0x08013BD8` | `0x08017F60` | **Home / VFO-A main** (default landing) | most `strb g_screen,#0` "return home" sites; `0x08013BD8` reads big status struct `0x200009C3+0x42` |
| 1 | *(default no-op `0x0801E228` / input `0x0801E804`)* | — | **blank / transient** | table byte 0x2D→no-op; input 0x40→no-op |
| 2 | `0x08013980` | `0x08007B10` | **VFO/standby key screen** | `0x08007B10` handles MENU key (10) → sets g_screen=0xA (menu entry, §3.4); EXIT/side keys |
| 3 | `0x08013A44` | `0x0800FE3C` | **screen w/ icon @`0x0802505C`** (freq-input / dial) | draws bitmap via `0x08008D62`; input checks `0x20000855` |
| 4 | `0x08014978` | `0x0802074C` | **screen w/ icon @`0x08025115`** | bitmap blit; input digit `0xC` handling |
| 5 | `0x08013940` | `0x08007784` | **numeric entry A** (0–F) | input: `cmp r4,#0xF; bl 0x08012EF8` (hex digit) |
| 6 | `0x080139EC` | `0x08007CD0` | **numeric entry B** (0–9) | input: `cmp r4,#9; bl 0x08012F78` (dec digit) |
| 7 | `0x08013804` | `0x08007E68``0x08017F60` | **alt of screen 0** | input forwards to id-0 handler `0x08017F60` |
| 8 | `0x08013B6C` | `0x08007D…`/via `0x0801E7E4→0x0800F930` | **list/scroll screen** | draw calls `0x080085D6`; several handlers special-case `g_screen==8` (`0x0800F930`) |
| 9 | `0x08013F7C` | *(input tbl byte 0x33→no-op region `0x0801E7EA`)* | **status/info screen** | draw reads `0x20000A3D+0x39`, `0x20000A7B` |
| **10 (0xA)** | **`0x080142C0`** | **`0x08017294`** | **MENU system** ★ | draw = the menu render loop (facts §Display); input = full menu key handler (largest, `sub sp,#0x64`) |
| 11 (0xB) | `0x08014104` | `0x0801D3B0` | **SMS / editor** (text) | draw blits chars at fixed cols; input handles 0–9 (`cmp r4,#9`) |
Notes:
- **Standby "home" screen (id 0/7)** is the radio's idle/operating screen (frequency, channel, RX/TX, DMR-rx overlay). The full home rendering also runs through the period-gated tasks and `0x0801E3F8` (line-buffer builder) — the dispatched draw handler `0x08013BD8` does the incremental field redraw.
- **id 1** is a genuine no-op slot (both tables route it to the shared "do nothing" tail). Treat as "transition/blank".
- All input handlers share the uniform prototype **`void screen_key(uint8_t keycode, uint8_t keystate)`**.
### 3.4 How transitions happen (state writes)
Screen changes are plain byte stores `strb rN, [=0x200008B3]`. Enumerated immediate-write sites (subset; `movs rN,#imm` immediately before the `strb`):
| new id | example write sites | meaning |
|---:|---|---|
| 0 | `0x08003490, 0x080094AA, 0x0800AA70, 0x0800AE4C, 0x0800B200, 0x0801E120, 0x0801F06C, 0x0801F81E, …` (many) | return to Home / EXIT |
| 1 | `0x080094AE, 0x080095DC, 0x08009760, 0x0801E5DA, 0x0801F698, …` | enter transient/blank |
| 2 | `0x0801A2BE` | enter VFO/standby key screen |
| 3 | `0x0800B112, 0x08018344, 0x0801A2DE` | enter freq-input/dial |
| 4 | `0x0800BC58, 0x0801E15A` | enter screen 4 |
| 5 | `0x08009842` | enter numeric-entry A |
| 7 | `0x0801A66C` | enter alt-home |
| 8 | `0x0800AF0A` | enter list/scroll |
| 9 | `0x080094D2` | enter status/info |
| **10 (0xA)** | **`0x0800B92A`** (from Home MENU key, §below) | **enter MENU** |
| 11 (0xB) | `0x0800B85C` | enter SMS/editor |
| 0x11 | `0x080094A4` | (id 17 — value out of 0..11 range; likely a sub-mode byte, not a screen; used by a specialized editor) |
| 0x2A | `0x08009A9C` | (id 42 — same: sub-mode marker, not a screen dispatch id) |
**Canonical transition example — Home → Menu** (in the id-2 standby key handler `0x08007B10`, MENU key path lands in `0x0800B900`):
```
0x0800B91C bl 0x0801AD9C ; menu-open side effects (build top-level list)
0x0800B926 movs r0,#0xA
0x0800B928 ldr r1,=0x200008B3
0x0800B92A strb r0,[r1] ; g_screen = 10 (MENU)
```
So **navigation = write the target id to `0x200008B3`** (optionally saving the old value to `0x200008B2` for "back"), plus per-screen enter side-effects. A custom router replicates exactly this.
---
## 4. How a screen is (re)drawn each loop
The draw model is **incremental / dirty-flag driven**, not full-frame:
1. `ui_draw_dispatch @0x0801E1BC` first calls `0x080136E4`, which walks a dirty-flag row at `0x20002512` (loop of 8+) and force-marks fields dirty on screen entry.
2. It then `tbb`-dispatches to the current screen's redraw routine (§3.3). Each routine reads its per-field "changed?" bytes (e.g. `0x200008F5`, `0x20000914`, `0x20000A84`) and only when set calls the text/glyph primitives:
- `draw_string @0x08008A50``void draw_string(u8 y_page /*r0*/, u8 x /*r1*/, const char* s /*r2*/, u8 len /*r3*/, u8 mode /*[sp+0x28]*/)`; mode 0=normal,1=inverse,2=outline. (Confirmed head: `mov sl,r0; mov r7,r1; mov r5,r2; mov fp,r3`.)
- `0x08008D62` — bitmap/icon blitter (`draw_bitmap(mode,x,y,const u8* bmp)`)
- `0x080089AC`, `0x080085D6`, `0x08008798` — box/line/clear helpers.
3. The framebuffer is flushed to the LCD over SPI2 by the period-gated tasks; the ASCII glyph blitter `0x08007FB8` and wide/GBK blitter `0x08008454` pull font bitmaps from SPI flash (per facts).
**Consequence for a rewrite:** because draw is gated behind dirty flags and a `tbb` on `g_screen`, replacing the dispatched routine (or the dispatcher call) cleanly takes over rendering for that screen without fighting the stock partial-redraw logic — as long as your code marks the whole area dirty / clears+redraws each frame itself.
---
## 5. Recommended HOOK POINTS for a custom UI router
Goal: our code owns **rendering + navigation + which key does what**, while calling stock lower-level APIs (draw_string `0x08008A50`, keypad `0x08005A14`, RF/DMR/codeplug helpers, and — untouched — the serial/CPS engine). Three options, most-preferred first.
### Option A (recommended): redirect the two scheduler dispatch calls
Patch the two `bl` instructions in `ui_tick_normal`:
| patch site | stock instr | change to |
|---|---|---|
| **`0x080207E6`** | `bl 0x0801E1BC` (ui_draw_dispatch) | `bl my_draw_router` |
| **`0x08020816`** | `bl 0x0801E6EC` (ui_process_key) | `bl my_key_router` |
- Your `my_key_router` reads the same key struct at `0x20000B57` (+2 keycode, +3 keystate) — call stock `0x08005A14` first if you want the stock debounce, or read raw. After handling, write `0xFF` to `0x20000B57+2` to consume, exactly as stock does at `0x0801E806`.
- Your `my_draw_router` renders via `draw_string @0x08008A50` and the blit/clear helpers, keyed on your own screen model. You may keep or ignore `g_screen@0x200008B3`.
- **Everything else in the superloop and scheduler is preserved**, so RF, DMR RX, scan, battery, and the entire serial/CPS + SPI region-write path (`0x080213B8` else-branch, `0x0801F854`, `0x0801FE50`) keep working unchanged. This is the minimal, cleanest cut: **2 instructions**.
- Keep `0x080207EA bl 0x0801E050` (side-key/long-press handler) if you still want stock side-key semantics, or repoint it too for full control.
### Option B: replace the two `tbb` jump tables (per-screen, incremental)
Repoint individual entries in the draw table (`@0x0801E1CE`, 12 bytes) and input table (`@0x0801E784`, 12 bytes) to your own handlers, one screen at a time. Because entries are **1-byte half-word offsets from the table base**, a target must be within `+0..+0x1FE` of the base; to jump far, keep a stock case as a 2-instruction trampoline (`b.w my_handler`) inside range. This lets you migrate screens gradually while stock screens still work. More fiddly than Option A.
### Option C: own the screen id + provide new handlers
Keep the dispatchers, but treat `0x200008B3` as your state var and point all 12 draw/input slots at your dispatch trampolines. Effectively Option B for all 12 at once; no advantage over Option A.
### Lower-level APIs to reuse (stable call targets)
| purpose | vaddr | prototype |
|---|---|---|
| draw text | `0x08008A50` | `draw_string(u8 y_page, u8 x, const char* s, u8 len, u8 mode@sp+0x28)` |
| draw icon/bitmap | `0x08008D62` | `draw_bitmap(u8 mode, u8 x, u8 y, const u8* bmp)` |
| ascii glyph blit | `0x08007FB8` | (per facts; SPI font @0x19C000) |
| wide/GBK glyph blit | `0x08008454` | (per facts; SPI font @0x19E000) |
| keypad scan/debounce | `0x08005A14` | `void keypad_scan(void)` (fills `0x20000B57`) |
| SPI codeplug read | `0x08021828` | `spi_read(dst, byteaddr, len)`**keep format** |
| menu-open helper | `0x0801AD9C` | builds top-level menu list (call if reusing stock menu data) |
| reboot | `0x0801A38C` | `nvic_system_reset()` |
**Do NOT touch** (serial/CPS + codeplug boundary — hard constraint): the superloop else-branch `0x080213B8..0x0802140A`, `serial_poll 0x0801F854`, the framer/dispatcher `0x0801F84C / 0x08019790 / 0x080188D4`, and the SPI region-write engine. These implement the stock CPS protocol and the on-flash codeplug format; leaving them byte-identical is what keeps the stock Radtel CPS working.
---
## 6. Key RAM state variables (UI)
| addr | width | name | role |
|---|---|---|---|
| `0x200008B3` | u8 | `g_screen` | current UI screen id (0..11) — **the state machine selector** |
| `0x200008B2` | u8 | `g_screen_prev` | previous screen (back/restore) |
| `0x20000B57` | struct | `g_key` | key event: +2 keycode, +3 keystate(1=press) |
| `0x20000C3D` | u8 | `g_sidekey` | side/long-press keycode consumed by `0x0801E050` |
| `0x20000BF1..BF8` | u8/u16 | scheduler down-counters | throttle slow periodic tasks in `ui_tick_normal` |
| `0x20002512` | u8[8+] | dirty-flag row | per-field "needs redraw" flags (cleared by `0x080136E4`) |
| `0x20000C54` | u8 | reboot request | superloop → `nvic_system_reset` |
| `0x20000B66 / 0x20000C12 / 0x20000C57` | u8 | serial-session mode gates | select UI vs CPS/SPI-write path (**do not repurpose**) |
---
## 7. Confidence
- **HIGH** — superloop (`0x0802136C`), normal-UI scheduler (`0x080207DC`), the single screen-state var `0x200008B3`, and the two mirror `tbb` dispatchers (draw `0x0801E1BC`, input `0x0801E6EC`) with their 12-entry tables. All directly disassembled and cross-checked (55 xrefs to the state var; both tbb tables decoded; the Home→Menu transition traced end-to-end).
- **HIGH** — the recommended hook (patch `bl` at `0x080207E6` and `0x08020816`); both call sites verified in the scheduler disassembly, and the serial/CPS boundary is cleanly separated in the superloop.
- **MEDIUM** — the *English names* assigned to screen ids 3/4/5/6/8/9/11: the dispatch structure and handler vaddrs are certain, but exact screen semantics are inferred from handler behaviour (digit ranges, bitmap vs text, forwarding) rather than from a label string on each. ids 0/7 (Home), 2 (standby-key), 10 (Menu), 11 (SMS/editor) are well-anchored.
+242
Просмотреть файл
@@ -0,0 +1,242 @@
# 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.
+393
Просмотреть файл
@@ -0,0 +1,393 @@
# RT-4D RF Control API (`radio` key)
Reverse-engineering of the **RF transceiver control path** in the RT-4D stock application firmware
(`rt4d_stock_v3.25_abs_0x08002800.bin`, ARM Cortex-M4F Thumb, vaddr base `0x08002800`).
All addresses are absolute vaddr. This document is an **API reference for rewriting the UI while reusing the
stock RF/DMR functions**, and it respects the hard boundary: it does **not** touch the SPI codeplug format or
the serial/CPS protocol.
---
## 0. TL;DR — the single most important architectural fact
**There is NO discrete RF transceiver chip driven by the MCU.** The RT-4D has *no* AT1846S / RDA1846 / SA828-class
analog transceiver on an MCU-side I²C/SPI bus. Confirmed:
- **Zero I²C hardware** — no `I2C1/2/3` base (`0x40005400/5800/5C00`) literal anywhere in the image; no bit-banged
AT1846S register-write helper (`reg = (addr<<... )`, 3-byte I²C write) exists.
- **SPI2 (`0x40003800`) is the external data-flash bus only** — its byte-transfer helper `spi_xfer_byte @0x08021538`
drives codeplug/calibration/font reads (opcode `0x03`, CS on GPIOB); it never talks to an RF PLL.
- The **entire radio transceiver — synthesiser, RX demod (FM/AM/SSB), TX modulator, AMBE vocoder, RSSI, CTCSS/DCS,
squelch — lives inside the FM100B baseband SoC.** The MCU controls all of it by sending a small binary
**"ATC" request protocol over USART3** (`0x40004800`) and blocking for the confirm.
Therefore the "RF control API we must reuse" is:
1. the **ATC message layer** (`atc_send` / `atc_send_pl` + ~25 typed wrappers), and
2. a handful of **MCU-local helpers** for things physically wired to the MCU: battery ADC, audio DAC/codec enable,
band-select GPIO, PA/CS GPIO, and the FM100B reset/hard-reset line.
The UI rewrite should call the **high-level composite functions** (`radio_apply_channel`, `ptt_tx_start`,
`battery_read`) and the FM100B is reprogrammed transparently. Frequencies flow from the codeplug (unchanged format)
through a RAM mirror into these functions — you never re-tune calibration.
---
## 1. RF chip identity & the transport bus
| Item | Finding | Evidence |
|---|---|---|
| RF transceiver | **Integrated in FM100B baseband SoC** (Kirisun-derived DMR chip; ARM7/9-class, WebRTC DSP + AMBE). Not an MCU-side chip. | No I²C base; RF config only appears as USART3 ATC payloads; FM100B strings `ATC_ChFreqSetReq/ATC_SetRfPowerLevelReq/ATC_RssiReadReq` (RE report §5.4). |
| Bus MCU↔FM100B | **USART3 @ `0x40004800`**, byte-oriented, IRQ-driven RX (ISR `0x0802061C`? — actually `0x080205B0`), polled TX. | `usart3_tx_byte @0x08006CB8` loads `0x40004800`; ISR pushes to ring `0x200082EF`. |
| Bus MCU↔SPI-flash | SPI2 `0x40003800` (codeplug/cal/fonts) — **not RF**. | `spi_xfer_byte @0x08021538`. |
| PC/CPS link | USART6 `0x40011400` — untouched, keep as-is. | RE report §4. |
### 1.1 USART3 low-level primitives (raw byte I/O to FM100B)
| vaddr | signature | what it does |
|---|---|---|
| `0x08021EA8` | `u16 usart_read_dr(u32 port)` | returns `port->DR` (`[port+4]`) |
| `0x08021EB0` | `void usart_write_dr(u32 port, u8 b)` | `port->DR = b & 0x1FF` |
| `0x08021EC2` | `bool usart_flag(u32 port, u32 mask)` | `(port->SR & mask) != 0` (RXNE=0x20, TXE=0x80) |
| `0x08006CB8` | `void usart3_tx_byte(u8 b)` | send 1 byte to FM100B (buffers to ring `0x200092EF` when flag `0x20000B67` set, else polls TXE and writes DR) |
| `0x08006C9C` | `void usart3_tx_buf(u8 *buf, u16 len)` | send `len` bytes (loops `usart3_tx_byte`) |
You will **not** call these directly for RF; they are the substrate under the ATC layer.
---
## 2. The ATC message layer — the core RF/DMR command API
### 2.1 Frame format (built in RAM buffer `0x2000706E`)
```
off field
0 0x68 frame start / sync
1 msg_id (see §3 table)
2 arg1 (byte)
3 arg2 (byte)
4 hdr_checksum (BE16) computed by chk @0x08002EA8, byte-swapped @0x0800BD2C
6 payload_len (BE16) 0 for the no-payload variant
8 arg3 / payload[0..] (payload copied here by memcpy @0x080062EC)
8+len 0x10 trailer subtype marker
... checksum
```
Then `usart3_tx_buf(&frame, 8+len+…)` is called and the sender **blocks** on the confirm.
### 2.2 The two core senders (CALLABLE, but you normally call the wrappers)
| vaddr | signature | notes |
|---|---|---|
| `0x0801B044` | `void atc_send(u8 msg_id, u8 a1, u8 a2, u8 a3, u32 timeout)` | no-payload request. Writes frame, sends 0xA bytes, then **spin-waits** on `cnf_flags[msg_id] @0x20007476[msg_id]` becoming ≠0xFF, decrementing a timeout counter at `0x20000C52`; calls scheduler `0x08003050` while waiting. |
| `0x0801B0C4` | `void atc_send_pl(u8 msg_id, u8 a1, u8 a2, u8 a3, u8 *payload, u16 len, u32 timeout)` | payload variant (extra args on stack: `[sp+0x20]=payload`, `[sp+0x24]=len`, `[sp+0x28]=timeout`). Same blocking confirm-wait. |
- **Confirm table:** `0x20007476` is a per-`msg_id` array of confirm flags; before send, `[msg_id]←0xFF`; the USART3
RX handler (`atc_rx @0x08006D00` region) writes the Cnf back and the sender unblocks. **The confirm often carries the
return value** (e.g. RSSI, version) into the RX-decoded RAM structs.
- **timeout** arg is a loop count (typ. `0x64`=100, `0xBB8`=3000, `0x3E8`=1000).
- **Helpers:** `chk @0x08002EA8` (frame checksum), `htons @0x0800BD2C` (byte-swap16), `memcpy @0x080062EC`,
`memset @0x08006038 / 0x08002BEE / 0x08002C52`.
### 2.3 Typed wrappers (the practical entry points)
Every wrapper is `atc_send(msg_id, 1, 1, param, 0x64)` unless noted (the `1,1` are fixed sub-fields). Each takes its
single byte/word parameter in `r0`.
| vaddr | msg_id | inferred signature | inferred meaning (FM100B ATC symbol) |
|---|---|---|---|
| `0x08006E6C` | `0x06` | `void atc_call_process(u8 a, u8 call_type, u32 target_id, u8 r3)` | **Start call / key DMR TX** (`ATC_CallProcessReq`). call_type 1=Private,2=Group,4=AllCall. Payload: type + BCD DMR-ID (via `id2bcd @0x0800786C`) + freq(`0x20007DA9[5]`) + 16-byte block. |
| `0x08007084` | `0x07` | `void atc_w07(...)` | channel/slot-related set (payload built from RAM `0x2000A5FD-0x38`, freq×; timeout 3000) |
| `0x08006FD8` | `0x0A` | `void atc_w0A(...)` | payload set (RX-related) |
| `0x0800760A` | `0x02` | `void atc_set_call_spk_vol(u8 v)` | DMR **called speaker volume** (from settings `[0x188]%25`) |
| `0x08007530` | `0x0B` | `void atc_set_call_mic_gain(u8 v)` | DMR **call MIC gain** (settings `[0x187]%25`) |
| `0x08007548`/`0x08007598` | `0x0C` | `void atc_set_color_code(u8 cc)` / template variant | **DMR color code / off-CTCSS** (from `[0x63]`) |
| `0x080075F4` | `0x4D` | `void atc_set_dig_squelch(u8 v)` | **DMR squelch level** (settings `[0x193]%17`) |
| `0x080075DC` | `0x55` | `void atc_set_sms_mode(u8 v)` | SMS/monitor flag (`ATC_SmsmodeSetReq`, settings `[0x196]&1`) |
| `0x08006DFC` | `0x25` (indirect) | `void atc_w_sms2(u8 v)` | second SMS/monitor flag (settings `[0x195]&1`); sends a fixed 0x1F-byte template |
| `0x08006C4C` | `0x49` | `void atc_set_denoise(u8 tx, u8 rx)` | **TX/RX denoise** (settings `[0x185]`,`[0x186]`) |
| `0x0800719C` | `0x49` | `void atc_w49b(u8 v)` | init-time variant (payload from an ADR const) |
| `0x08006C88` | `0x25` | `void atc_w25(void)` | init handshake (`atc_send(0x25,1,1,1)`) |
| `0x08006C74` | `0x05` | `void atc_w05(void)` | `atc_send(0x05,1,1,2)` — init/enable |
| `0x080071CC` | `0x45` | `void atc_w45(u8 v)` | init default (called with 2) |
| `0x080075C6` | `0x48` | `void atc_w48(u8 v)` | init default (called with 0xF) — likely AGC/EQ default |
| `0x08007670`/`0x08007620` | `0x4C` | `void atc_w4C(u8 v)` / template variant | RX enable / mute (called with 1) |
| `0x08007688`/`0x080076CC` | `0x09` | `void atc_query09(void)` / `atc_w09(u8)` | fixed 0x18-byte query/keepalive (sets `0x20000C3F` busy flag) |
| `0x080074FA` | `0x2A` | `void atc_set_radio_id(u32 dmr_id)` | **set radio's own DMR ID** (`ATC_RadioIDSetReq`); 4-byte LE payload |
| `0x080074D2` | `0x57` | `void atc_w57(u8 v)` | 2-byte set (init-time, called with 0) |
| `0x080071E2` | `0x62` | `void atc_ch_enable(u8 rx_en, u8 tx_en)` | **channel RX/TX wait/enable** (`ATC_CurChannelWaitSetReq`); 2-byte payload |
| `0x0800720C` | `0x82` | `void atc_channel_set(chan_cfg *cfg)` | **★ SET RX FREQ + TX FREQ + MODE + BW + CC + call-type ★** (`ATC_ChannelSetReq`) — see §4 |
| `0x0800736C` | `0x81` | `void atc_set_mute_code(u16 code)` | analog **mute code / DCS value** (`[cfg+0x14]`) |
| `0x08007404` | `0x84` | `void atc_set_rxgroup(u8 gl_index)` | **RX group list upload** (reads groups `0xC6000` stride `0x50`, contacts `0x5E000` stride `0x15`) = `ATC_DigChGroupSetReq` |
| `0x080071E2`… | `0x62` | (see above) | |
> **Naming confidence:** the msg_ids and calling conventions are *certain* (decoded directly). The English names are
> inferred from (a) the caller context in `radio_apply_channel` (which settings byte feeds each), (b) the payload shape,
> and (c) the FM100B `ATC_*` symbol list. Treat the ★ ones (`0x82` freq/mode, `0x06` call, `0x2A` radio-id, `0x62`
> enable) as high-confidence; the audio/denoise/squelch ones as medium-high.
---
## 3. ★ `atc_channel_set` @0x0800720C — the RX/TX frequency + mode setter
**Signature:** `void atc_channel_set(chan_cfg *cfg)` (msg_id `0x82`, 0x14-byte payload).
`cfg` is a channel-parameter block (the RAM staging struct, e.g. `0x20002E7A`, `0x20007DA9`, or a copy of a 48-byte
codeplug channel record). Field layout used by this function:
| cfg off | field | used how |
|---|---|---|
| `+0x00` | flags byte | bit1→bandwidth(+1), bit2→a flag, bits6-7→RX/TX permission (checked by caller) |
| `+0x01` | flags2 | high nibble → modulation (FM/AM/SSB) |
| `+0x05` | **RX freq** (u32 LE, 10 Hz units = MHz×100000) | `rx_hz = rxfreq × 10` → 4 bytes **big-endian** into payload |
| `+0x09` | **TX freq** (u32 LE, 10 Hz units) | `tx_hz = txfreq × 10` → 4 bytes big-endian |
| `+0x11` | contact index (u16) | reads contact rec at `0x5E000 + idx*0x15` → call type (0→1 Priv, 1→2 Grp, 2→4 All); target ID or `0xAAAAAAAA` for all-call |
| `+0x13` | CTCSS/DCS select | (handled by caller via `0x0C`/`0x84`) |
| `+0x14` | mute code / DCS (u16) | (caller → `0x81`) |
Key disassembly:
```
0800720c push {r4,r5,r6,lr}; r4 = cfg
08007220 ldr r0,[r4,#5] ; RX freq (10Hz)
08007224 ldr r1,=0x16e3600 ; 24000000 = 240.00000 MHz band threshold
08007226 cmp r0,r1 ; >=240MHz -> band flag 0x20000C34 = 1 (UHF) else 0 (VHF)
0800725a add r0,r0,r0,lsl#2 ; lsls#1 → r0*10 ; convert 10Hz→Hz
...store BE at payload+0xf (RX), +0x13 (TX)...
08007340 ldrb r0,[r4,#0x14] ; extra param
08007354 movs r0,#0x82 ; bl atc_send_pl ; send ChannelSet
```
**Frequency units — DEFINITIVE:** codeplug stores `MHz × 100000` (i.e. **10 Hz units**, matches
`FREQ_MULTIPLIER=100000`). This function multiplies by **×10** to hand the FM100B **plain Hz** (big-endian u32).
So: `payload_hz = codeplug_value × 10`. Example: `43880000 (10Hz) → 438800000 Hz`.
**Callers (reuse these, or call `atc_channel_set` directly):** `0x0801AEE8` (inside `radio_apply_channel`),
`0x0801F590`, `0x0801F5C0` (dual-watch/scan band re-tune).
---
## 4. ★ `radio_apply_channel` @0x0801AE9C — the composite "tune the radio" entry point
**This is the function the new UI should call to make the radio adopt a channel.** It takes the channel-config block
and pushes *everything* (freq, mode, power/enable, color code, squelch, gains, radio-ID, CTCSS/DCS, denoise) to the
FM100B in one shot, reading auxiliary values from the RAM settings mirror `0x200029BB`.
**Signature:** `void radio_apply_channel(chan_cfg *cfg)` (`cfg` in `r0`).
Sequence (evidence = disassembly `0x0801AE9C``0x0801B016`):
```
if (cfg->flags>>6 == 0) // normal RX/TX channel
atc_query09() // 0x8007688 quiet/prep
delay(0x14) // 0x8007946
atc_channel_set(cfg) // 0x800720C ★ RX/TX freq + mode + BW
atc_ch_enable(cfg&1, cfg&1) // 0x80071E2 msg 0x62
if (dmr) {
atc_set_call_spk_vol(settings[0x188]%25) // 0x800760A msg 0x02
atc_set_call_mic_gain(settings[0x187]%25) // 0x8007530 msg 0x0B
atc_set_dig_squelch(settings[0x193]%17) // 0x80075F4 msg 0x4D
atc_set_sms_mode(settings[0x196]&1) // 0x80075DC msg 0x55
atc_w_sms2(settings[0x195]&1) // 0x8006DFC
atc_set_color_code(cfg[0x63-region]) // 0x8007598 msg 0x0C
} else { // analog
atc_set_denoise(settings[0x185], settings[0x186]) // 0x8006C4C msg 0x49
}
// radio ID: channel-custom (cfg+0x16) if cfg bit3 set, else settings[0x180]
atc_set_radio_id(...) // 0x80074FA msg 0x2A
// CTCSS/DCS:
if (cfg[0x13]==0) atc_set_color_code_off() // 0x8007548 msg 0x0C
else atc_set_rxgroup(cfg[0x13]-1) // 0x8007404 msg 0x84
atc_set_mute_code(cfg[0x14]) // 0x800736C msg 0x81
else if (cfg->flags>>6 == 1) // special/FM-broadcast/monitor branch
atc_query09(); atc_w4C_tpl(); // 0x8007688, 0x8007620
delay(0x14)
GPIOA->BSRR = 0x4000 // band/PA GPIO bit14 set
... reset several RAM state bytes, call 0x801D938 (RX open) ...
apply_backlight(settings[0x10D]) // 0x80049E4
```
**Callers:** `0x08002FE0`, `0x08009F08`, `0x0800B7B8`, `0x0800BBD4`, `0x0801F490` (channel change, VFO set, zone
switch, scan). In the rewrite, call `radio_apply_channel(cfg)` after you populate `cfg` (a 48-byte codeplug channel
record, or a synthesized VFO record) — the codeplug format is untouched.
**RAM boundary object:** `0x200029BB` = **RAM mirror of `main_settings` (SPI `0x002000`)**. The UI reads/writes this
struct (offsets match `rt4d_codeplug` `RadioSettings`, e.g. `[0x188]`=call spk vol, `[0x193]`=digital squelch,
`[0x180]`=radio DMR-ID, `[0x10D]`=backlight); the RF apply reads from it. Persisting it back to SPI keeps the codeplug
format intact.
---
## 5. ★ `ptt_tx_start` @0x08007E78 — PTT on / start TX
**Signature:** `void ptt_tx_start(u8 mode)` (`mode` in `r0`: distinguishes DMR vs analog / call-type).
Disassembly `0x08007E78``0x08007EFE`:
```
08007e78 push {r4,lr}; r4=mode
... call-start-beep if settings[0x18d] (0x801B684) ...
08007e90 if (band_flag 0x20000C34 == 1) GPIOA->BSRR = (1<<10) // set band/PA bit10 (UHF)
08007ea4 else GPIOA->BSRR = (1<<10)<<16 // reset band bit10 (VHF)
switch(mode):
0: dmr_tx(0xFF, cur_contact_id 0x20000B3C[..0x11]) // bl 0x8006D00 (DMR key)
1: atc_call_process(1, call_type 0x20000C9E, target 0x20000CA8, 0) // Private
2: atc_call_process(1, 0x20000C13, 0x20000C14, 1) // ...
3: atc_call_process(1, 1, 0x20000CA8, 2) // AllCall
08007ef8 tx_state 0x20000B6E = 3 // "transmitting"
```
- **DMR TX** goes through `dmr_tx @0x08006D00` (the USART3 DMR-record/AMBE path).
- **Analog/DMR-call TX** goes through `atc_call_process @0x08006E6C` (msg `0x06`).
- The **band-select / PA-enable GPIO** is **GPIOA pin 10** (`0x40020000`, BSRR `+0x18`/`+0x28`), driven by band flag
`0x20000C34` (set in `atc_channel_set` when RXfreq ≥ 240 MHz).
**Callers (PTT key handlers):** `0x0801EC34`, `0x0801ED86`, `0x0801ED94`, `0x0801ED9C`.
**PTT off / stop TX:** the reverse path returns to RX by re-running the RX-open (`0x0801D938`) and clearing
`tx_state 0x20000B6E`; the analog carrier key is released via `atc_ch_enable`/`atc_w4C`. For a rewrite, calling
`radio_apply_channel(cfg)` (which re-opens RX) after dropping PTT restores RX cleanly. (A dedicated
`atc_call_release` exists in the `0x06`/`0x62` family; the tx_state byte `0x20000B6E` and `0x20000B73` gate it.)
---
## 6. TX power, squelch, bandwidth, CTCSS/DCS, mode — where each lives
| RF parameter | How it is set | Function / evidence |
|---|---|---|
| **RX frequency** | `cfg[+5]` (10 Hz) → ×10 → Hz | `atc_channel_set @0x0800720C` (msg 0x82) |
| **TX frequency** | `cfg[+9]` (10 Hz) → ×10 → Hz | same |
| **Mode FM/AM/SSB** | `cfg[+1]` high nibble → payload | same (0x82). Values 0=FM,1=AM,2=SSB per codeplug `AnalogModulation`. |
| **Bandwidth W/N** | `cfg[+0]` bit1 → payload (`bit+1`) | same (0x82). 0=Wide/25k, 1=Narrow/12.5k. |
| **TX power Hi/Lo** | carried in `atc_channel_set` payload flags (from codeplug byte); FM100B applies power DAC from its own NV cal via `ATC_SetRfPowerLevelReq`. **The MCU does not compute a power DAC value** — it sends the Hi/Lo level and the FM100B uses its NV calibration. | msg 0x82 payload + FM100B `SPCali_PowerOpt` |
| **Squelch (DMR)** | `settings[0x193]``atc_set_dig_squelch` | `0x080075F4` (msg 0x4D) |
| **Squelch (analog)** | `settings[0x102]` region + `atc` analog SQ path | analog SQ is an FM100B cal (`SPCali_AnaSQthOpt`); level pushed via the analog-set family |
| **Color code** | `atc_set_color_code` | `0x08007548/0x08007598` (msg 0x0C) |
| **CTCSS/DCS** | `cfg[+0x13]` select → `atc_set_rxgroup`/off; `cfg[+0x14]` value → `atc_set_mute_code` | `0x08007404` (0x84), `0x0800736C` (0x81) |
| **Radio DMR ID** | `settings[0x180]` or `cfg[+0x16]` | `atc_set_radio_id @0x080074FA` (msg 0x2A) |
| **MIC gain / SPK vol (DMR)** | `settings[0x187]`,`[0x188]` | `0x08007530` (0x0B), `0x0800760A` (0x02) |
| **TX/RX denoise (analog)** | `settings[0x185]`,`[0x186]` | `atc_set_denoise @0x08006C4C` (0x49) |
**Power note:** because Hi/Lo maps to an FM100B-internal calibrated DAC, the UI must only pass the codeplug power
byte through `radio_apply_channel`; it must **never** try to write a raw power value — that would require the per-unit
calibration and risk PA damage.
---
## 7. MCU-local RF-adjacent helpers (not FM100B)
### 7.1 Battery voltage (ADC1)
- `battery_read @0x0801094C``void battery_read(void)`. Software-starts ADC1 (`adc_sw_start @0x08020B4C`,
CR2.SWSTART bit30), waits up to 20 samples, then `batt = (adc_raw << 2) / 0x42` → stored at **`0x200008B0`**
(accumulator raw `0x2000089C`). Divisor `0x42`(66) ⇒ result is in **0.1 V units** (feeds the `BATT:x.xV` string).
Callers: `0x08012816` (boot/about), `0x0801E288` (periodic/low-batt check).
- ADC helpers: `adc_sw_start @0x08020B4C(port,en)`, ADC ISR `@0x08002D1C` accumulates into `0x2000089C`.
- **RSSI** is **not** an MCU ADC read — RSSI/signal-quality is read back from the FM100B via ATC query
(`ATC_RssiReadReq`/`ATRssiQueryCnf`, msg-id in the `0x09`/query family) and lands in an RX-decoded RAM struct.
### 7.2 Audio (DAC / codec)
- DAC control at `0x40007400`: `dac_enable_chX @0x08020F90 / 0x08020FA4 / 0x08020FE0` toggle DAC CR enable/trigger
bitfields (bit0/bit16/bit1/bit17). Used to gate the audio path.
- **Speaker volume for voice** is largely an FM100B setting (`atc_set_call_spk_vol` msg 0x02, `SPMicVoiceCnf`); the
MCU DAC is the tone/beep/analog-audio out. Amp-enable is a GPIO (see below).
### 7.3 Key GPIOs (for the rewrite)
| GPIO | purpose | evidence |
|---|---|---|
| GPIOA (`0x40020000`) BSRR, **bit10** (`0x400`) | **band-select / PA enable** (VHF vs UHF; set on TX) | `0x8007E90`, `0x801F574`, `0x801F5A4`, `0x801AFD8` (bit14 `0x4000` in special branch) |
| GPIOB (`0x40020400`) BSRR `+0x28` | **SPI-flash CS** and FM100B reset toggles | `spi_flash_read @0x08021828` (`0x40020428`), `0x8007F60` (FM100B reset, bit set/reset via `0x40020418`) |
| `delay @0x08007946(ms)` | busy delay used around FM100B commands/reset | pervasive |
### 7.4 SPI flash / calibration read (used by RF setup, keep format)
- `spi_flash_read @0x08021828``void spi_flash_read(void *dst, u32 addr, u32 len)`. Opcode `0x03`, CS on GPIOB;
handles 3-byte vs 4-byte addressing (chip-id `0x18/0x19` at `0x20000C1C`). This reads the **calibration block at SPI
`0x000000`**, channels, contacts (`0x5E000`), groups (`0xC6000`), and fonts.
- **How calibration feeds RF:** the MCU does **not** apply RF calibration itself. The 4 KB cal block at SPI `0x000000`
is per-unit factory data that the **FM100B** consumes (its `SPCaliFreqSetCnf` / `SPCali_*Opt` NV items) to trim
VCO/PLL, TX power DAC, and squelch/RSSI thresholds. The MCU only reads cal for display/backup. **The UI must reuse
the stock apply path (which sends frequency + Hi/Lo level and lets the FM100B self-calibrate); it must not re-tune.**
---
## 8. The reusable "set radio to F/mode/power then PTT" call sequence
For the rewritten UI, the clean, minimal sequence (all stock functions, codeplug + CPS untouched):
```c
// 1. Build/obtain a channel-config block `cfg` (a 48-byte codeplug channel record, or a VFO
// record you synthesize in the SAME on-flash format — do NOT change the format).
// Set: cfg[+5]=rx_freq_10Hz cfg[+9]=tx_freq_10Hz
// cfg[+0]: bit1=narrow, bits6-7=rx/tx-perm, power bit as in codeplug
// cfg[+1]: high nibble = modulation (0 FM,1 AM,2 SSB)
// cfg[+0x13]/[+0x14]=CTCSS-DCS select/value, cfg[+0x11]=contact index
// (rx/tx freq in codeplug 10 Hz units = MHz*100000)
// 2. Make sure the RAM settings mirror 0x200029BB holds the desired
// color-code / squelch / gains / radio-ID (offsets = rt4d RadioSettings).
// 3. Push the whole channel to the FM100B (freq, mode, BW, power, CC, SQ, ID, CTCSS):
radio_apply_channel(cfg); // 0x0801AE9C
// 4. To transmit:
ptt_tx_start(mode); // 0x08007E78 (mode 0 = DMR, 1/2/3 = analog/call variants)
// -> sets band GPIO (GPIOA bit10) and keys TX via atc_call_process/dmr_tx
// 5. To stop TX / return to RX:
// clear tx_state 0x20000B6E and re-open RX; simplest robust way is:
radio_apply_channel(cfg); // re-runs the RX-open path
```
If you need finer control instead of the composite, call the wrappers directly:
`atc_channel_set(cfg)` (freq/mode/BW), `atc_ch_enable(rx,tx)`, `atc_set_color_code(cc)`,
`atc_set_dig_squelch(sq)`, `atc_set_radio_id(id)`, `atc_call_process(a,type,id,r3)`.
---
## 9. Master callable-entry-point table
| vaddr | name | signature | confidence |
|---|---|---|---|
| `0x0801AE9C` | `radio_apply_channel` | `void(chan_cfg*)` | **high** — verified 5 callers, full body |
| `0x08007E78` | `ptt_tx_start` | `void(u8 mode)` | **high** — 4 PTT callers |
| `0x0800720C` | `atc_channel_set` (RX/TX freq+mode+BW) | `void(chan_cfg*)` msg 0x82 | **high** |
| `0x08006E6C` | `atc_call_process` (key TX / start call) | `void(u8 a,u8 type,u32 id,u8 r3)` msg 0x06 | **high** |
| `0x08006D00` | `dmr_tx` (DMR record/AMBE TX) | `void(u8 a, u16 contact)` | med-high |
| `0x080074FA` | `atc_set_radio_id` | `void(u32 dmr_id)` msg 0x2A | high |
| `0x080071E2` | `atc_ch_enable` (RX/TX wait) | `void(u8 rx,u8 tx)` msg 0x62 | high |
| `0x080075F4` | `atc_set_dig_squelch` | `void(u8)` msg 0x4D | med-high |
| `0x08007548`/`0x08007598` | `atc_set_color_code` | `void(u8)` msg 0x0C | med-high |
| `0x0800736C` | `atc_set_mute_code` (DCS val) | `void(u16)` msg 0x81 | med |
| `0x08007404` | `atc_set_rxgroup` (CTCSS/DCS/RX-group) | `void(u8 idx)` msg 0x84 | med |
| `0x08007530` | `atc_set_call_mic_gain` | `void(u8)` msg 0x0B | med |
| `0x0800760A` | `atc_set_call_spk_vol` | `void(u8)` msg 0x02 | med |
| `0x08006C4C` | `atc_set_denoise` | `void(u8 tx,u8 rx)` msg 0x49 | med |
| `0x0801B044` | `atc_send` | `void(u8 id,u8,u8,u8,u32 to)` | **high** (core) |
| `0x0801B0C4` | `atc_send_pl` | `void(u8 id,u8,u8,u8,u8*pl,u16 len,u32 to)` | **high** (core) |
| `0x08006C9C` | `usart3_tx_buf` | `void(u8*,u16)` | high |
| `0x08006CB8` | `usart3_tx_byte` | `void(u8)` | high |
| `0x0801094C` | `battery_read` | `void(void)``0x200008B0` (0.1 V) | high |
| `0x08020B4C` | `adc_sw_start` | `void(u32 port,u8 en)` | high |
| `0x08021828` | `spi_flash_read` | `void(void*,u32 addr,u32 len)` | **high** |
| `0x08021538` | `spi_xfer_byte` | `u8(u8)` on SPI2 | high |
| `0x08007946` | `delay_ms` | `void(u32)` | high |
| `0x08002EA8` | `atc_checksum` | `u16(u8*,u16)` | med |
| `0x0800786C` | `dmr_id_to_bcd` | `u32(u32)` | med |
### Key RAM state (the UI/RF boundary)
| addr | meaning |
|---|---|
| `0x200029BB` | **RAM mirror of main_settings (SPI 0x2000)** — offsets = `RadioSettings` |
| `0x20007DA9` | ATC call/freq staging struct (`[0]=type,[1..4]=id,[5..8]=freq`) |
| `0x20002E7A` / `0x20002120` | channel-config staging blocks (used by scan/dual-watch) |
| `0x20007476[msg_id]` | ATC confirm-flag array (0xFF=pending) |
| `0x2000706E` | ATC TX frame build buffer |
| `0x20000C34` | band flag (0=VHF <240 MHz, 1=UHF) → GPIOA band bit |
| `0x20000B6E` | TX/call state (3 = transmitting) |
| `0x200008B0` | battery voltage (0.1 V units) |
| `0x200008B0``0x2000089C` | battery ADC raw accumulator |
---
## 10. Boundary compliance (codeplug + CPS unchanged)
- The RF API operates on a **channel-config block in the stock 48-byte codeplug format** and on the **RAM settings
mirror `0x200029BB`** whose layout equals `rt4d_codeplug.RadioSettings`. Reusing these keeps the SPI codeplug format
identical, so the stock CPS round-trips.
- All RF programming is **USART3 ATC traffic to the FM100B** — completely separate from the USART6 CPS/serial protocol
(`0x34/0x52/region-id` framing). Rewriting the UI and calling these functions changes nothing the CPS observes.
- **Calibration (SPI `0x000000`) is consumed by the FM100B, not recomputed by the MCU.** The rewrite reuses the stock
freq/power/mode apply path, so the per-unit factory tuning is honored and never overwritten.
+1 -1
Просмотреть файл
@@ -95,7 +95,7 @@ def main():
print("[i] handshake (0xFF probe)...")
if not handshake(p):
raise SystemExit("[E] no bootloader (radio not in flash mode). "
"Power off, hold * (or the flash-mode key) and power on.")
"Power off, hold PTT and power on.")
print("[i] bootloader ready")
print("[i] erasing application region...")
erase(p)
+160
Просмотреть файл
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""FM100B (DMR baseband) console — talk to the RT-4D's DMR chip from the PC.
Требуется: рация включена с зажатой клавишей МЕНЮ (режим «Обновление DMR чипа»),
в котором прошивка работает прозрачным мостом USB ↔ FM100B (USART3).
Формат кадра: 68 | cmd | b | sub | CKh CKl | lenH lenL | payload… | 10
CRC — «интернетная» one's-complement сумма 16-битных BE-слов по всему кадру,
причём во время расчёта поле CRC = 0xFFFF; результат кладётся BE в [4..5].
python3 fm100b.py ping # штатный handshake (cmd 0x84)
python3 fm100b.py send 84 --sub 1 --payload 00
python3 fm100b.py raw 6884010100000001 0010
python3 fm100b.py scan 01 20 # опрос диапазона команд (только чтение статуса)
Порт задаётся через --port (по умолчанию берётся первый CH343).
Под WSL доступ к COM идёт через powershell.exe (WSL2 не пробрасывает COM напрямую).
"""
import argparse, subprocess, sys, time
DEFAULT_BAUD = 115200
# ---------------- frame helpers ----------------
def cksum(frame: bytes) -> int:
"""one's complement sum of 16-bit big-endian words (CRC field must be 0xFFFF)"""
s = 0
i = 0
while len(frame) - i > 1:
s += (frame[i] << 8) | frame[i + 1]
i += 2
if len(frame) - i:
s += frame[i] << 8
while s >> 16:
s = (s & 0xFFFF) + (s >> 16)
return (~s) & 0xFFFF
def build(cmd: int, b: int = 1, sub: int = 1, payload: bytes = b"") -> bytes:
f = bytearray([0x68, cmd, b, sub, 0xFF, 0xFF,
(len(payload) >> 8) & 0xFF, len(payload) & 0xFF])
f += payload
f.append(0x10)
ck = cksum(bytes(f))
f[4], f[5] = (ck >> 8) & 0xFF, ck & 0xFF
return bytes(f)
def parse(resp: bytes):
"""decode a response frame -> dict (or None)"""
if len(resp) < 9 or resp[0] != 0x68:
return None
cmd, b, status = resp[1], resp[2], resp[3]
ck = (resp[4] << 8) | resp[5]
ln = (resp[6] << 8) | resp[7]
payload = resp[8:8 + ln]
chk = bytearray(resp[:9 + ln])
chk[4] = chk[5] = 0xFF
ok = cksum(bytes(chk)) == ck
return dict(cmd=cmd, b=b, status=status, length=ln,
payload=bytes(payload), crc_ok=ok, raw=resp)
# ---------------- serial via powershell (WSL2) ----------------
PS = r'''
$p = New-Object System.IO.Ports.SerialPort "{port}",{baud},"None",8,"one"
$p.ReadTimeout = 120
try {{ $p.Open() }} catch {{ Write-Output "ERR:$($_.Exception.Message)"; exit }}
$p.DiscardInBuffer()
$tx = [byte[]]@({txlist})
if ($tx.Length -gt 0) {{ $p.Write($tx,0,$tx.Length) }}
$buf = New-Object System.Collections.Generic.List[byte]
$sw = [Diagnostics.Stopwatch]::StartNew()
$last = 0
while ($sw.ElapsedMilliseconds -lt {tmo}) {{
try {{ $buf.Add($p.ReadByte()); $last = $sw.ElapsedMilliseconds }} catch {{}}
if ($buf.Count -gt 0 -and ($sw.ElapsedMilliseconds - $last) -gt {idle}) {{ break }}
}}
$p.Close()
Write-Output ("OK:" + (($buf | ForEach-Object {{ "{{0:x2}}" -f $_ }}) -join ""))
'''
def find_port() -> str:
out = subprocess.run(["powershell.exe", "-NoProfile", "-Command",
"[System.IO.Ports.SerialPort]::GetPortNames() -join ','"],
capture_output=True, text=True).stdout.strip().replace("\r", "")
ports = [p for p in out.split(",") if p]
if not ports:
sys.exit("COM-порт не найден. Подключите рацию.")
return ports[0]
def xfer(port: str, tx: bytes, timeout_ms: int = 1200, idle_ms: int = 250) -> bytes:
txlist = ",".join(str(x) for x in tx) if tx else ""
script = PS.format(port=port, baud=DEFAULT_BAUD, txlist=txlist,
tmo=timeout_ms, idle=idle_ms)
r = subprocess.run(["powershell.exe", "-NoProfile", "-Command", script],
capture_output=True, text=True)
line = [l for l in r.stdout.replace("\r", "").split("\n") if l.startswith(("OK:", "ERR:"))]
if not line:
return b""
if line[0].startswith("ERR:"):
sys.exit(line[0])
return bytes.fromhex(line[0][3:])
def show(tx: bytes, rx: bytes):
print(f" TX: {tx.hex(' ')}")
if not rx:
print(" RX: (нет ответа)")
return None
print(f" RX: {rx.hex(' ')}")
p = parse(rx)
if p:
print(f" cmd=0x{p['cmd']:02X} status=0x{p['status']:02X} "
f"len={p['length']} crc={'ok' if p['crc_ok'] else 'BAD'}"
+ (f" payload={p['payload'].hex(' ')}" if p['payload'] else ""))
return p
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--port")
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("ping")
s = sub.add_parser("send"); s.add_argument("code"); s.add_argument("--b", default="1")
s.add_argument("--sub", default="1"); s.add_argument("--payload", default="")
r = sub.add_parser("raw"); r.add_argument("hex", nargs="+")
sc = sub.add_parser("scan"); sc.add_argument("first"); sc.add_argument("last")
a = ap.parse_args()
port = a.port or find_port()
print(f"[порт {port} @ {DEFAULT_BAUD}]")
if a.cmd == "ping":
tx = bytes([0x68, 0x84, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10])
show(tx, xfer(port, tx))
elif a.cmd == "send":
tx = build(int(a.code, 16), int(a.b, 0), int(a.sub, 0), bytes.fromhex(a.payload))
show(tx, xfer(port, tx))
elif a.cmd == "raw":
tx = bytes.fromhex("".join(a.hex))
show(tx, xfer(port, tx))
elif a.cmd == "scan":
lo, hi = int(a.first, 16), int(a.last, 16)
print(f"опрос команд 0x{lo:02X}..0x{hi:02X} (b=1 sub=1, пустой payload)\n")
for c in range(lo, hi + 1):
tx = build(c)
rx = xfer(port, tx, timeout_ms=600, idle_ms=150)
p = parse(rx) if rx else None
if p:
print(f" cmd 0x{c:02X}: status=0x{p['status']:02X} len={p['length']}"
+ (f" payload={p['payload'].hex(' ')}" if p['payload'] else ""))
elif rx:
print(f" cmd 0x{c:02X}: неразобранный ответ {rx.hex(' ')}")
time.sleep(0.03)
if __name__ == "__main__":
main()
+76
Просмотреть файл
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Map every GPIO write in the RT-4D app: find calls to the low-level GPIO
helpers and back-resolve (port, mask, state) from the preceding instructions."""
import struct, re, capstone, os
BASE = 0x08002800
P = "/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin"
IMG = open(P, "rb").read()
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
PORTS = {0x40020000: "GPIOA", 0x40020400: "GPIOB", 0x40020800: "GPIOC",
0x40020C00: "GPIOD", 0x40021000: "GPIOE", 0x40021400: "GPIOF",
0x40021800: "GPIOG"}
# low-level writers discovered by disassembly
HELPERS = {0x08021C6E: "gpio_set_state(port,mask,state)",
0x08021C56: "gpio_bsrr_write(port,mask)",
0x08021C4C: "set_bit0(reg,val)"}
def word(va):
o = va - BASE
return struct.unpack_from("<I", IMG, o)[0] if 0 <= o <= len(IMG) - 4 else None
def lit_of(ins):
m = re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", ins.op_str)
if m and ins.mnemonic.startswith("ldr"):
return word(((ins.address + 4) & ~3) + int(m.group(1), 16))
return None
# linear sweep, keep a sliding window to resolve register values
def scan():
hits = []
win = []
for ins in md.disasm(IMG, BASE):
win.append(ins)
if len(win) > 14:
win.pop(0)
if ins.mnemonic == "bl" and ins.op_str.startswith("#"):
tgt = int(ins.op_str[1:], 16)
if tgt in HELPERS:
regs = {}
for p in win[:-1]:
mm = re.match(r"(r\d+)", p.op_str)
dst = mm.group(1) if mm else None
if not dst:
continue
v = lit_of(p)
if v is not None:
regs[dst] = v
elif p.mnemonic in ("movs", "mov.w", "mov") and "#" in p.op_str:
try:
regs[dst] = int(p.op_str.split("#")[1], 0)
except Exception:
pass
elif p.mnemonic == "movw" and "#" in p.op_str:
try:
regs[dst] = int(p.op_str.split("#")[1], 0)
except Exception:
pass
hits.append((ins.address, HELPERS[tgt], regs.get("r0"), regs.get("r1"), regs.get("r2")))
return hits
if __name__ == "__main__":
rows = scan()
print(f"{len(rows)} GPIO-helper call sites\n")
print(f"{'site':<12}{'helper':<32}{'r0(port/reg)':<22}{'r1(mask/val)':<14}{'r2'}")
for site, h, r0, r1, r2 in rows:
pn = PORTS.get(r0, f"0x{r0:08X}" if r0 is not None else "?")
mask = f"0x{r1:X}" if r1 is not None else "?"
bit = ""
if r1 and r1 and (r1 & (r1 - 1)) == 0:
bit = f" (bit{r1.bit_length()-1})"
print(f"0x{site:08X} {h:<32}{pn:<22}{mask+bit:<14}{r2 if r2 is not None else ''}")
+50
Просмотреть файл
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Find every GPIO-base literal load in the RT-4D app and show the surrounding
code, so each (port, register-offset, bit-mask) access can be identified."""
import struct, re, sys, capstone
BASE = 0x08002800
IMG = open("/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin", "rb").read()
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
PORTS = {0x40020000: "GPIOA", 0x40020400: "GPIOB", 0x40020800: "GPIOC",
0x40020C00: "GPIOD", 0x40021000: "GPIOE", 0x40021400: "GPIOF",
0x40021800: "GPIOG", 0x40021C00: "GPIOH"}
def word(va):
o = va - BASE
return struct.unpack_from("<I", IMG, o)[0] if 0 <= o <= len(IMG) - 4 else None
def litval(ins):
m = re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", ins.op_str)
if m and ins.mnemonic.startswith("ldr"):
return word(((ins.address + 4) & ~3) + int(m.group(1), 16))
return None
def main(ctx=6, only=None):
ins_list = list(md.disasm(IMG, BASE))
idx = {i.address: n for n, i in enumerate(ins_list)}
hits = []
for n, i in enumerate(ins_list):
v = litval(i)
if v in PORTS and (only is None or v == only):
hits.append((n, i, v))
print(f"{len(hits)} GPIO-base literal loads\n")
for n, i, v in hits:
print(f"=== {PORTS[v]} (0x{v:08X}) referenced at 0x{i.address:08X} ===")
for k in range(n, min(n + ctx, len(ins_list))):
j = ins_list[k]
extra = ""
lv = litval(j)
if lv is not None:
extra = f" ; =0x{lv:08X}"
print(f" 0x{j.address:08X}: {j.mnemonic:8} {j.op_str}{extra}")
print()
if __name__ == "__main__":
only = int(sys.argv[1], 16) if len(sys.argv) > 1 else None
main(only=only)
+114
Просмотреть файл
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Exhaustive reference finder for the RT-4D app image.
Decodes one instruction at EVERY 2-byte alignment (linear sweep desyncs on
Thumb), so nothing is missed.
python refs.py addr <0xADDR> [ctx] who pc-relative-loads this address/constant
python refs.py gpio full GPIO map: (port, register, mask) per site
"""
import sys, struct, re, capstone
BASE = 0x08002800
IMG = open("/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin", "rb").read()
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
PORTS = {0x40020000: "GPIOA", 0x40020400: "GPIOB", 0x40020800: "GPIOC",
0x40020C00: "GPIOD", 0x40021000: "GPIOE", 0x40021400: "GPIOF"}
# AT32F43x GPIO layout
REG = {0x00: "CFGR", 0x10: "IDT", 0x14: "ODT", 0x18: "SCR(set)",
0x1C: "CLR16", 0x28: "CLR(clear)", 0x2C: "TOGR(toggle)"}
def word(va):
o = va - BASE
return struct.unpack_from("<I", IMG, o)[0] if 0 <= o <= len(IMG) - 4 else None
def decode_at(va):
o = va - BASE
for i in md.disasm(IMG[o:o + 4], va):
return i
return None
def litval(i):
if i is None or not i.mnemonic.startswith("ldr"):
return None
m = re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", i.op_str)
if not m:
return None
return word(((i.address + 4) & ~3) + int(m.group(1), 16))
def all_lit_loads():
"""[(va, ins, value)] for every pc-relative ldr at any 2-byte alignment"""
out = []
for off in range(0, len(IMG) - 4, 2):
va = BASE + off
i = decode_at(va)
v = litval(i)
if v is not None:
out.append((va, i, v))
return out
def cmd_addr(target, ctx=8):
hits = [(va, i) for va, i, v in all_lit_loads() if v == target]
print(f"pc-relative loads of 0x{target:08X}: {len(hits)}")
for va, i in hits:
print(f"\n--- 0x{va:08X} ({i.mnemonic} {i.op_str}) ---")
a = va
for _ in range(ctx):
j = decode_at(a)
if j is None:
break
v = litval(j)
e = f" ; =0x{v:08X}" if v is not None else ""
print(f" 0x{j.address:08X}: {j.mnemonic:8} {j.op_str}{e}")
a += j.size
def cmd_gpio():
loads = all_lit_loads()
rows = []
for va, i, v in loads:
if v not in PORTS:
continue
reg = i.op_str.split(",")[0].strip()
a = va + i.size
mask = None
# small forward window: find store to [reg,#off]
for _ in range(6):
j = decode_at(a)
if j is None:
break
m = re.match(r"(r\d+), \[" + reg + r", #(0x[0-9a-fA-F]+|\d+)\]$", j.op_str)
if j.mnemonic.startswith("str") and m:
src, off = m.group(1), int(m.group(2), 0)
# back-window for the immediate loaded into src
b = va
for _ in range(10):
b -= 2
p = decode_at(b)
if p and p.op_str.startswith(src + ", #"):
try:
mask = int(p.op_str.split("#")[1], 0)
except Exception:
pass
break
rows.append((va, PORTS[v], off, mask))
break
a += j.size
print(f"{len(rows)} GPIO store sites\n")
print(f"{'site':<13}{'port':<7}{'register':<14}{'mask':<10}bit")
for va, p, off, m in rows:
bit = f"bit{m.bit_length()-1}" if m and (m & (m - 1)) == 0 else ("?" if m is None else "multi")
print(f"0x{va:08X} {p:<7}{REG.get(off, hex(off)):<14}{(hex(m) if m is not None else '?'):<10}{bit}")
if __name__ == "__main__":
if sys.argv[1] == "addr":
cmd_addr(int(sys.argv[2], 16), int(sys.argv[3]) if len(sys.argv) > 3 else 8)
elif sys.argv[1] == "gpio":
cmd_gpio()
+21
Просмотреть файл
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Render a clipped region of the RT-4D schematic PDF at high DPI.
python schem.py <x0> <y0> <x1> <y1> <dpi> <out.png> (coords in PDF points)
Sheets (approx, points): 1: y 12-582 | 2: y 600-1170 | 3: y 1200-1764 | 4: y 1794-2370
Page is 864 x 2400 pt.
"""
import sys, fitz
PDF = "/mnt/c/Users/vikto/Downloads/Telegram Desktop/RT4DDLT01.pdf"
def render(x0, y0, x1, y1, dpi, out):
doc = fitz.open(PDF)
p = doc[0]
clip = fitz.Rect(x0, y0, x1, y1)
pm = p.get_pixmap(dpi=dpi, clip=clip)
pm.save(out)
print(f"{out}: {pm.width}x{pm.height} clip=({x0},{y0})-({x1},{y1}) dpi={dpi}")
if __name__ == "__main__":
a = sys.argv[1:]
render(float(a[0]), float(a[1]), float(a[2]), float(a[3]), int(a[4]), a[5])
+119
Просмотреть файл
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""RT-4D firmware xref/disasm helper.
python xref.py dis <vaddr> [n] disassemble n bytes at vaddr
python xref.py callers <vaddr> who bl/blx's this address
python xref.py lit <value> where this 32-bit literal appears (pool refs)
python xref.py imm <value> movw/movt pairs building this constant
python xref.py find <hexbytes> raw byte search
"""
import sys, struct, capstone
BASE = 0x08002800
import os
_CANDS = [
"/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin",
"/home/viktor/claude/rt-4d-repo/firmware/rt4d_stock_v3.25_abs_0x08002800.bin",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "firmware",
"rt4d_stock_v3.25_abs_0x08002800.bin"),
]
IMG = next(open(p, "rb").read() for p in _CANDS if os.path.exists(p))
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
md.detail = True
def _word(va):
o = va - BASE
if 0 <= o <= len(IMG) - 4:
return struct.unpack_from("<I", IMG, o)[0]
return None
def dis(va, n=0x60):
"""disassemble, annotating PC-relative literal loads with the resolved value"""
off = va - BASE
import re as _re
for i in md.disasm(IMG[off:off + n], va):
ann = ""
m = _re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", i.op_str)
if m and i.mnemonic.startswith("ldr"):
pcv = ((i.address + 4) & ~3) + int(m.group(1), 16)
w = _word(pcv)
if w is not None:
ann = f" ; @0x{pcv:08X} = 0x{w:08X}"
print(f" 0x{i.address:08X}: {i.bytes.hex():<10} {i.mnemonic:8} {i.op_str}{ann}")
def _all_calls():
"""Linear sweep collecting bl/blx targets -> {target: [sites]}."""
xr = {}
for start in (0, 2): # both alignments, dedupe by site
for i in md.disasm(IMG[start:], BASE + start):
if i.mnemonic in ("bl", "blx") and i.op_str.startswith("#"):
t = int(i.op_str[1:], 16)
xr.setdefault(t, set()).add(i.address)
return {k: sorted(v) for k, v in xr.items()}
def callers(va):
xr = _all_calls()
hits = xr.get(va, []) + xr.get(va | 1, []) + xr.get(va & ~1, [])
hits = sorted(set(hits))
print(f"callers of 0x{va:08X}: {len(hits)}")
for h in hits:
print(f" 0x{h:08X}")
return hits
def lit(val):
hits = []
for off in range(0, len(IMG) - 4, 4):
if struct.unpack_from("<I", IMG, off)[0] == val:
hits.append(BASE + off)
print(f"literal 0x{val:08X}: {len(hits)} hit(s)")
for h in hits[:60]:
print(f" 0x{h:08X}")
return hits
def imm(val):
"""find movw/movt pairs that build `val` (constant loaded into a reg)."""
lo, hi = val & 0xFFFF, (val >> 16) & 0xFFFF
out = []
for i in md.disasm(IMG, BASE):
if i.mnemonic == "movw" and i.op_str.endswith(f"#{hex(lo)}"):
out.append(i.address)
print(f"movw #{hex(lo)} (for 0x{val:08X}): {len(out)} site(s)")
for a in out[:60]:
print(f" 0x{a:08X}")
return out
def find(hexs):
pat = bytes.fromhex(hexs)
off = 0
hits = []
while True:
j = IMG.find(pat, off)
if j < 0:
break
hits.append(BASE + j); off = j + 1
print(f"bytes {hexs}: {len(hits)} hit(s)")
for h in hits[:60]:
print(f" 0x{h:08X}")
return hits
if __name__ == "__main__":
cmd = sys.argv[1]
if cmd == "dis":
dis(int(sys.argv[2], 16), int(sys.argv[3], 0) if len(sys.argv) > 3 else 0x60)
elif cmd == "callers":
callers(int(sys.argv[2], 16))
elif cmd == "lit":
lit(int(sys.argv[2], 16))
elif cmd == "imm":
imm(int(sys.argv[2], 16))
elif cmd == "find":
find(sys.argv[2])
+435 -435
Просмотреть файл
@@ -1,436 +1,436 @@
{
"Basic Set": "ОСН. НАСТР",
"Key Define": "НАЗНАЧ КЛАВ",
"Analog Set": "АНАЛОГ НАСТР",
"Digital Set": "ЦИФР НАСТР",
"Channel Set": "НАСТР КАНАЛА",
"Zone Set": "НАСТР ЗОНЫ",
"Message": "СМС",
"Radio Name": "ИМЯ РАЦИИ",
"Voice Prompt": "ОЗВУЧ",
"Key Beep": "БИП КЛАВ",
"Lock Timer": "ТАЙМЕР БЛОК",
"Backlight": "ПОДСВЕТКА",
"Light Timer": "ТАЙМЕР СВЕТА",
"Brightness": "ЯРКОСТЬ",
"Menu Exit": "ВЫХОД ИЗ МЕНЮ",
"Dual Standby": "ДВОЙН ОЖИД",
"TX Priority": "ПРИОР TX",
"Freq Step": "ШАГ ЧАСТ",
"Talkaround": "ПРЯМОЙ",
"Save Mode": "РЕЖИМ ЭКОН",
"Scan Mode": "РЕЖИМ СКАН",
"Scan Direction": "НАПРАВ СКАН",
"Scan Dwell": "ЗАДЕРЖ СКАН",
"Scan Interval": "ИНТЕРВ СКАН",
"Scan Return": "ВОЗВРАТ СКАН",
"Scan Start": "НАЧАЛО СКАН",
"Scan End": "КОНЕЦ СКАН",
"Alarm Type": "ТИП ТРЕВОГИ",
"Main PTT TX": "ОСН PTT TX",
"Area A Mode": "РЕЖИМ ЗОНЫ A",
"Area A Show": "ПОКАЗ ЗОНЫ A",
"Area A Zone": "ЗОНА A",
"Area B Mode": "РЕЖИМ ЗОНЫ B",
"Area B Show": "ПОКАЗ ЗОНЫ B",
"Area B Zone": "ЗОНА B",
"Save CH": "СОХР КАНАЛ",
"Delete CH": "УДАЛ КАНАЛ",
"LCD Contrast": "КОНТРАСТ LCD",
"Freq Input": "ВВОД ЧАСТ",
"Reverse CH Dir": "РЕВЕРС КАНАЛА",
"Carrier LED": "СВЕТОДИОД НЕС",
"RSSI Refresh": "ОБНОВЛ RSSI",
"APO": "АВТОВЫКЛ",
"APO Timer": "ТАЙМЕР АВТОВЫК",
"FM RX Standby": "FM ПРИЁМ ОЖИД",
"Initialization": "СБРОС",
"Instruction": "ИНСТРУКЦИЯ",
"Version": "ВЕРСИЯ",
"Second PTT": "ВТОРОЙ PTT",
"Side Key 1 S": "БОК КЛАВ 1 КОР",
"Side Key 1 L": "БОК КЛАВ 1 ДЛ",
"Side Key 2 S": "БОК КЛАВ 2 КОР",
"Side Key 2 L": "БОК КЛАВ 2 ДЛ",
"0 Press Long": "0 ДЛ НАЖАТИЕ",
"1 Press Long": "1 ДЛ НАЖАТИЕ",
"2 Press Long": "2 ДЛ НАЖАТИЕ",
"3 Press Long": "3 ДЛ НАЖАТИЕ",
"4 Press Long": "4 ДЛ НАЖАТИЕ",
"5 Press Long": "5 ДЛ НАЖАТИЕ",
"6 Press Long": "6 ДЛ НАЖАТИЕ",
"7 Press Long": "7 ДЛ НАЖАТИЕ",
"8 Press Long": "8 ДЛ НАЖАТИЕ",
"9 Press Long": "9 ДЛ НАЖАТИЕ",
"SQ Level": "УР ШУМОПОД",
"TX Start Tone": "ТОН НАЧ TX",
"TX End Tone": "ТОН КОНЦА TX",
"Single Tone": "ОДИНОЧ ТОН",
"Tone Timer": "ТАЙМЕР ТОНА",
"MIC Gain": "УСИЛ МИКР",
"SPK Gain": "УСИЛ ДИНАМ",
"Glitch TH": "ПОРОГ ПОМЕХ",
"Detect Range": "ДИАПАЗ ДЕТЕКТ",
"Repeater Delay": "ЗАДЕРЖ РЕПИТ",
"DTMF Delay": "ЗАДЕРЖ DTMF",
"DTMF Interval": "ИНТЕРВ DTMF",
"DTMF Duration": "ДЛИТ DTMF",
"DTMF Mode": "РЕЖИМ DTMF",
"DTMF Select": "ВЫБОР DTMF",
"DTMF Display": "ПОКАЗ DTMF",
"DTMF TX Gain": "УСИЛ TX DTMF",
"DTMF RX TH": "ПОРОГ RX DTMF",
"DTMF Control": "УПРАВЛ DTMF",
"VOX": "VOX",
"VOX Delay": "ЗАДЕРЖ VOX",
"VOX TH": "ПОРОГ VOX",
"Short Tail": "КОР ХВОСТ",
"Personal ID": "ЛИЧНЫЙ ID",
"Call Tone": "ТОН ВЫЗОВА",
"Call End Tone": "ТОН КОНЦА ВЫЗ",
"Group Hold": "УДЕРЖ ГРУППЫ",
"Single Hold": "УДЕРЖ ЛИЧН",
"TX Denoise": "ШУМОПОД TX",
"RX Denoise": "ШУМОПОД RX",
"Contacts Set": "НАСТР КОНТАКТ",
"TG List Set": "НАСТР СП ТГ",
"Encryption Set": "НАСТР ШИФР",
"Called Show": "ПОКАЗ ВЫЗЫВ",
"Send DTMF": "ПЕРЕД DTMF",
"Caller Keep": "УДЕРЖ ВЫЗЫВ",
"Call Log": "ЖУРНАЛ ВЫЗ",
"Clear All Log": "ОЧИСТ ЖУРНАЛ",
"Address Book": "АДР КНИГА",
"DMR Or Analog": "DMR ИЛИ АНАЛОГ",
"RX/TX Limit": "ЛИМИТ RX/TX",
"CH Alias": "ИМЯ КАНАЛА",
"TX Power": "МОЩН TX",
"Scan Add": "ДОБАВ В СКАН",
"TOT": "ТАЙМАУТ TX",
"Offset Freq": "СДВИГ ЧАСТ",
"Set TX Freq": "ЗАДАТЬ ЧАСТ TX",
"CTC/DCS (A)": "CTC/DCS (A)",
"RX CTC/DCS (A)": "RX CTC/DCS (A)",
"TX CTC/DCS (A)": "TX CTC/DCS (A)",
"DCS Encrypt(A)": "ШИФР DCS (A)",
"Mute Code (A)": "КОД МУТ (A)",
"Band Width (A)": "ПОЛОСА (A)",
"Tail Tone (A)": "ХВОСТ ТОН (A)",
"Scrambler (A)": "СКРЕМБЛ (A)",
"Busy Lock (A)": "БЛОК ЗАНЯТ (A)",
"RX Demod (A)": "ДЕМОД RX (A)",
"DMR Mode (D)": "РЕЖИМ DMR (D)",
"DMR Slot (D)": "СЛОТ DMR (D)",
"Color Code (D)": "ЦВ.КОД (D)",
"Contacts (D)": "КОНТАКТЫ (D)",
"TG List (D)": "СПИСОК ТГ (D)",
"Encryption (D)": "ШИФР (D)",
"TX Politely(D)": "ВЕЖЛ TX (D)",
"Promiscuous(D)": "ПРОМИСК (D)",
"Channel ID (D)": "ID КАНАЛА (D)",
"ID Select (D)": "ВЫБОР ID (D)",
"New SMS": "НОВОЕ СМС",
"Inbox": "ВХОДЯЩИЕ",
"Outbox": "ИСХОДЯЩИЕ",
"Drafts": "ЧЕРНОВИКИ",
"Default SMS": "ШАБЛОН СМС",
"Clear All SMS": "ОЧИСТ ВСЕ СМС",
"SMS Format": "ФОРМАТ СМС",
"SMS Font": "ШРИФТ СМС",
"SMS Prompt": "ОЗВУЧ СМС",
"Contacts List": "СПИС КОНТАКТ",
"Add Contact": "ДОБАВ КОНТАКТ",
"Edit Name": "ИЗМЕН ИМЯ",
"Select CH": "ВЫБОР КАНАЛА",
"Edit Type": "ИЗМЕН ТИП",
"Edit ID": "ИЗМЕН ID",
"Save": "СОХР",
"Edit Member": "ИЗМЕН ЧЛЕНОВ",
"Clear": "ОЧИСТ",
"Edit Key": "ИЗМЕН КЛЮЧ",
"Send": "ПЕРЕД",
"Time :": "ВРЕМЯ :",
"Type :": "ТИП :",
"ID :": "ID :",
"Name :": "ИМЯ :",
"Text :": "ТЕКСТ :",
"State:": "СТАТУС:",
"Callsign :": "ПОЗЫВНОЙ :",
"First Name :": "ИМЯ :",
"Last Name :": "ФАМИЛИЯ :",
"City :": "ГОРОД :",
"State :": "РЕГИОН :",
"Country :": "СТРАНА :",
"Call In/Out :": "ВХ/ИСХ ВЫЗ :",
"Call Type :": "ТИП ВЫЗОВА :",
"ID :": "ID :",
"Talk time :": "ВРЕМЯ РАЗГ :",
"Call start :": "НАЧ ВЫЗОВА :",
"Delete": "УДАЛИТЬ",
"Contacts": "КОНТАКТЫ",
"Dial No.": "НАБОР НОМЕРА",
"Reply": "ОТВЕТ",
"Forward": "ПЕРЕСЛАТЬ",
"Resend": "ПОВТОР ОТПР",
"Off": "ВЫКЛ",
"On": "ВКЛ",
"Edit": "ИЗМЕН",
"Busy": "ЗАНЯТО",
"0.01K": "0.01K",
"0.02K": "0.02K",
"0.03K": "0.03K",
"0.05K": "0.05K",
"0.10K": "0.10K",
"0.25K": "0.25K",
"0.50K": "0.50K",
"1.25K": "1.25K",
"2.50K": "2.50K",
"5.00K": "5.00K",
"6.25K": "6.25K",
"8.33K": "8.33K",
"10.0K": "10.0K",
"12.5K": "12.5K",
"20.0K": "20.0K",
"25.0K": "25.0K",
"50.0K": "50.0K",
" 100K": " 100K",
" 500K": " 500K",
" 1M": " 1M",
" 5M": " 5M",
" 5 S": " 5 С",
" 10 S": " 10 С",
" 15 S": " 15 С",
" 30 S": " 30 С",
" 45 S": " 45 С",
" 60 S": " 60 С",
" 75 S": " 75 С",
" 90 S": " 90 С",
"105 S": "105 С",
"120 S": "120 С",
"135 S": "135 С",
"150 S": "150 С",
"165 S": "165 С",
"180 S": "180 С",
"195 S": "195 С",
"210 S": "210 С",
"225 S": "225 С",
"240 S": "240 С",
"255 S": "255 С",
"270 S": "270 С",
"285 S": "285 С",
"300 S": "300 С",
"315 S": "315 С",
"330 S": "330 С",
"345 S": "345 С",
"360 S": "360 С",
"375 S": "375 С",
"390 S": "390 С",
"405 S": "405 С",
"420 S": "420 С",
"435 S": "435 С",
"450 S": "450 С",
"465 S": "465 С",
"480 S": "480 С",
"495 S": "495 С",
"510 S": "510 С",
"525 S": "525 С",
"540 S": "540 С",
"555 S": "555 С",
"570 S": "570 С",
"585 S": "585 С",
"600 S": "600 С",
"Reverse Freq": "РЕВЕРС ЧАСТ",
"1 : 1": "1 : 1",
"1 : 2": "1 : 2",
"1 : 3": "1 : 3",
"CO": "CO",
"TO": "TO",
"SE": "SE",
"Up": "ВВЕРХ",
"Down": "ВНИЗ",
"Original CH": "ИСХ КАНАЛ",
"Current CH": "ТЕК КАНАЛ",
"Local Alarm": "ЛОК ТРЕВОГА",
"Remote Alarm": "УДАЛ ТРЕВОГА",
"Local + Remote": "ЛОК + УДАЛ",
"Area A": "ЗОНА A",
"Main Area": "ОСН ЗОНА",
"Freq Mode": "РЕЖИМ ЧАСТ",
"CH Mode": "РЕЖИМ КАНАЛА",
"Zone Mode": "РЕЖИМ ЗОНЫ",
"Show CH No.": "ПОКАЗ НОМ КАН",
"Show Freq": "ПОКАЗ ЧАСТ",
"Show Alias": "ПОКАЗ ИМЯ",
"Ensure": "ПОДТВЕРД",
"Cancel": "ОТМЕНА",
"Hytera": "HYTERA",
"Motorola": "MOTOROLA",
"Unicode": "UNICODE",
"GBK": "GBK",
"None": "НЕТ",
"Monitor(Analog)": "МОНИТОР АНАЛОГ",
"H/L Power": "МОЩН ВЫС/НИЗ",
"Scanning": "СКАНИРОВ",
"Backlight On-off": "ПОДСВ ВКЛ/ВЫКЛ",
"Roger Beep": "БИП ОКОНЧ",
"FM Radio": "FM РАДИО",
"Alarm": "ТРЕВОГА",
"Freq Detect": "ДЕТЕКТ ЧАСТ",
"CTC/DCS Scan": "СКАН CTC/DCS",
"Send Single Tone": "ПЕРЕД ОДИН ТОН",
"Status Query": "ЗАПРОС СТАТУСА",
"Remote Monitor": "УДАЛ МОНИТОР",
"Remote Stun": "УДАЛ БЛОК",
"Remote Kill": "УДАЛ ОТКЛ",
"Remote Wake Up": "УДАЛ ПРОБУЖД",
"Online Check": "ПРОВ НА СВЯЗИ",
"RX AM/FM Switch": "ПЕРЕКЛ AM/FM RX",
"Analog Spectrum": "АНАЛОГ СПЕКТР",
"SQ": "ШУМОПОД",
"DA Switch": "ПЕРЕКЛ ЦИФР/АН",
"NOAA Mode": "РЕЖИМ NOAA",
"Jump To SMS Menu": "ПЕРЕХ В МЕНЮ СМС",
"Analog CH VOX": "VOX АНАЛОГ КАН",
"Zone Select": "ВЫБОР ЗОНЫ",
"Promiscuous Mode": "РЕЖИМ ПРОМИСК",
"Dual Slot On-off": "2 СЛОТА ВКЛ/ВЫК",
"Time Slot Switch": "ПЕРЕКЛ СЛОТА",
"Color Code SW": "ПЕРЕКЛ ЦВ.КОДА",
"DMR Encrypt Off": "ШИФР DMR ВЫКЛ",
"Jump To RX List": "ПЕРЕХ К СП RX",
"Jump To Contact": "ПЕРЕХ К КОНТ",
"Jump To DTMF Sel": "ПЕРЕХ К DTMF",
"Roger Beep 1": "БИП ОКОНЧ 1",
"Roger Beep 2": "БИП ОКОНЧ 2",
"Send Radio Name": "ПЕРЕД ИМЯ РАЦИИ",
"18-64 MHz": "18-64 МГЦ",
"64-136 MHz": "64-136 МГЦ",
"136-174 MHz": "136-174 МГЦ",
"174-240 MHz": "174-240 МГЦ",
"240-320 MHz": "240-320 МГЦ",
"320-400 MHz": "320-400 МГЦ",
"400-480 MHz": "400-480 МГЦ",
"480-560 MHz": "480-560 МГЦ",
"560-620 MHz": "560-620 МГЦ",
"840-920 MHz": "840-920 МГЦ",
"920-1000 MHz": "920-1000 МГЦ",
"0 ms": "0 МС",
"100 ms": "100 МС",
"200 ms": "200 МС",
"300 ms": "300 МС",
"400 ms": "400 МС",
"500 ms": "500 МС",
"600 ms": "600 МС",
"700 ms": "700 МС",
"800 ms": "800 МС",
"900 ms": "900 МС",
"1000 ms": "1000 МС",
"1100 ms": "1100 МС",
"1200 ms": "1200 МС",
"1300 ms": "1300 МС",
"1400 ms": "1400 МС",
"1500 ms": "1500 МС",
"1600 ms": "1600 МС",
"1700 ms": "1700 МС",
"1800 ms": "1800 МС",
"1900 ms": "1900 МС",
"2000 ms": "2000 МС",
"30 ms": "30 МС",
"40 ms": "40 МС",
"50 ms": "50 МС",
"60 ms": "60 МС",
"70 ms": "70 МС",
"80 ms": "80 МС",
"90 ms": "90 МС",
"110 ms": "110 МС",
"120 ms": "120 МС",
"130 ms": "130 МС",
"140 ms": "140 МС",
"150 ms": "150 МС",
"160 ms": "160 МС",
"170 ms": "170 МС",
"180 ms": "180 МС",
"190 ms": "190 МС",
"TX Start": "НАЧАЛО TX",
"TX End": "КОНЕЦ TX",
"TX Start & End": "НАЧ И КОНЕЦ TX",
"DTMF-01": "DTMF-01",
"DTMF-02": "DTMF-02",
"DTMF-03": "DTMF-03",
"DTMF-04": "DTMF-04",
"DTMF-05": "DTMF-05",
"DTMF-06": "DTMF-06",
"DTMF-07": "DTMF-07",
"DTMF-08": "DTMF-08",
"DTMF-09": "DTMF-09",
"DTMF-10": "DTMF-10",
"DTMF-11": "DTMF-11",
"DTMF-12": "DTMF-12",
"DTMF-13": "DTMF-13",
"DTMF-14": "DTMF-14",
"DTMF-15": "DTMF-15",
"DTMF-16": "DTMF-16",
"Individual": "ЛИЧНЫЙ",
"Group Call": "ГРУПП ВЫЗОВ",
"ARC": "ARC",
"AES 128": "AES 128",
"AES 256": "AES 256",
"Standard": "СТАНДАРТ",
"Encrypt 1": "ШИФР 1",
"Encrypt 2": "ШИФР 2",
"Encrypt 3": "ШИФР 3",
"Mute Code": "КОД МУТ",
"Wide": "ШИР",
"Narrow": "УЗК",
" 1": " 1",
" 2": " 2",
" 3": " 3",
" 4": " 4",
" 5": " 5",
" 6": " 6",
" 7": " 7",
" 8": " 8",
"Carrier Match": "ПО НЕСУЩЕЙ",
"CTC/DCS Match": "ПО CTC/DCS",
"Low": "НИЗ",
"High": "ВЫС",
"Add": "ДОБАВ",
"Remove": "УДАЛИТЬ",
"+": "+",
"-": "-",
"Dual Slot Off": "2 СЛОТА ВЫКЛ",
"Dual Slot On": "2 СЛОТА ВКЛ",
"Slot 1": "СЛОТ 1",
"Slot 2": "СЛОТ 2",
"Impolite": "НЕВЕЖЛИВО",
"CC Match": "ПО ЦВ.КОДУ",
"6 digits": "6 ЦИФР",
"8 digits": "8 ЦИФР",
"Show Caller Info": "ПОКАЗ ВЫЗЫВ",
"Show Called Info": "ПОКАЗ ВЫЗЫВАЕМ",
"FM": "FM",
"AM": "AM",
"SSB": "SSB",
"Tail Tone Off": "ХВОСТ ТОН ВЫКЛ",
"55Hz No Shift": "55Hz БЕЗ СДВ",
"120¡ã Shift": "120° СДВИГ",
"180¡ã Shift": "180° СДВИГ",
"240¡ã Shift": "240° СДВИГ",
"Radio ID": "ID РАЦИИ",
"Channel ID": "ID КАНАЛА",
"No": "НЕТ",
"Yes": "ДА",
"Latest Program": "ПОСЛ ПРОШИВКА",
"Init Config": "СБРОС НАСТР",
"Init All": "СБРОС ВСЕГО",
"Latest Tuning": "ПОСЛ КАЛИБР",
"Init Tune Data": "СБРОС КАЛИБР",
"DMR": "DMR",
"Analog": "АНАЛОГ",
"RX + TX": "RX + TX",
"Only RX": "ТОЛЬКО RX",
"Only TX": "ТОЛЬКО TX",
"S0 S1 S2 S3 S4 S": "S0 S1 S2 S3 S4 S",
"5 S6 S7 S8 S9 +1": "5 S6 S7 S8 S9 +1",
"0+20+30+40+50+": "0+20+30+40+50+"
{
"Basic Set": "ОСН. НАСТР",
"Key Define": "НАЗНАЧ КЛАВ",
"Analog Set": "АНАЛОГ НАСТР",
"Digital Set": "ЦИФР НАСТР",
"Channel Set": "НАСТР КАНАЛА",
"Zone Set": "НАСТР ЗОНЫ",
"Message": "СМС",
"Radio Name": "ИМЯ РАЦИИ",
"Voice Prompt": "ОЗВУЧ",
"Key Beep": "БИП КЛАВ",
"Lock Timer": "ТАЙМЕР БЛОК",
"Backlight": "ПОДСВЕТКА",
"Light Timer": "ТАЙМЕР СВЕТА",
"Brightness": "ЯРКОСТЬ",
"Menu Exit": "ВЫХОД ИЗ МЕНЮ",
"Dual Standby": "ДВОЙН ОЖИД",
"TX Priority": "ПРИОР TX",
"Freq Step": "ШАГ ЧАСТ",
"Talkaround": "ПРЯМОЙ",
"Save Mode": "РЕЖИМ ЭКОН",
"Scan Mode": "РЕЖИМ СКАН",
"Scan Direction": "НАПРАВ СКАН",
"Scan Dwell": "ЗАДЕРЖ СКАН",
"Scan Interval": "ИНТЕРВ СКАН",
"Scan Return": "ВОЗВРАТ СКАН",
"Scan Start": "НАЧАЛО СКАН",
"Scan End": "КОНЕЦ СКАН",
"Alarm Type": "ТИП ТРЕВОГИ",
"Main PTT TX": "ОСН PTT TX",
"Area A Mode": "РЕЖИМ ЗОНЫ A",
"Area A Show": "ПОКАЗ ЗОНЫ A",
"Area A Zone": "ЗОНА A",
"Area B Mode": "РЕЖИМ ЗОНЫ B",
"Area B Show": "ПОКАЗ ЗОНЫ B",
"Area B Zone": "ЗОНА B",
"Save CH": "СОХР КАНАЛ",
"Delete CH": "УДАЛ КАНАЛ",
"LCD Contrast": "КОНТРАСТ LCD",
"Freq Input": "ВВОД ЧАСТ",
"Reverse CH Dir": "РЕВЕРС КАНАЛА",
"Carrier LED": "СВЕТОДИОД НЕС",
"RSSI Refresh": "ОБНОВЛ RSSI",
"APO": "АВТОВЫКЛ",
"APO Timer": "ТАЙМЕР АВТОВЫК",
"FM RX Standby": "FM ПРИЁМ ОЖИД",
"Initialization": "СБРОС",
"Instruction": "ИНСТРУКЦИЯ",
"Version": "ВЕРСИЯ",
"Second PTT": "ВТОРОЙ PTT",
"Side Key 1 S": "БОК КЛАВ 1 КОР",
"Side Key 1 L": "БОК КЛАВ 1 ДЛ",
"Side Key 2 S": "БОК КЛАВ 2 КОР",
"Side Key 2 L": "БОК КЛАВ 2 ДЛ",
"0 Press Long": "0 ДЛ НАЖАТИЕ",
"1 Press Long": "1 ДЛ НАЖАТИЕ",
"2 Press Long": "2 ДЛ НАЖАТИЕ",
"3 Press Long": "3 ДЛ НАЖАТИЕ",
"4 Press Long": "4 ДЛ НАЖАТИЕ",
"5 Press Long": "5 ДЛ НАЖАТИЕ",
"6 Press Long": "6 ДЛ НАЖАТИЕ",
"7 Press Long": "7 ДЛ НАЖАТИЕ",
"8 Press Long": "8 ДЛ НАЖАТИЕ",
"9 Press Long": "9 ДЛ НАЖАТИЕ",
"SQ Level": "УР ШУМОПОД",
"TX Start Tone": "ТОН НАЧ TX",
"TX End Tone": "ТОН КОНЦА TX",
"Single Tone": "ОДИНОЧ ТОН",
"Tone Timer": "ТАЙМЕР ТОНА",
"MIC Gain": "УСИЛ МИКР",
"SPK Gain": "УСИЛ ДИНАМ",
"Glitch TH": "ПОРОГ ПОМЕХ",
"Detect Range": "ДИАПАЗ ДЕТЕКТ",
"Repeater Delay": "ЗАДЕРЖ РЕПИТ",
"DTMF Delay": "ЗАДЕРЖ DTMF",
"DTMF Interval": "ИНТЕРВ DTMF",
"DTMF Duration": "ДЛИТ DTMF",
"DTMF Mode": "РЕЖИМ DTMF",
"DTMF Select": "ВЫБОР DTMF",
"DTMF Display": "ПОКАЗ DTMF",
"DTMF TX Gain": "УСИЛ TX DTMF",
"DTMF RX TH": "ПОРОГ RX DTMF",
"DTMF Control": "УПРАВЛ DTMF",
"VOX": "VOX",
"VOX Delay": "ЗАДЕРЖ VOX",
"VOX TH": "ПОРОГ VOX",
"Short Tail": "КОР ХВОСТ",
"Personal ID": "ЛИЧНЫЙ ID",
"Call Tone": "ТОН ВЫЗОВА",
"Call End Tone": "ТОН КОНЦА ВЫЗ",
"Group Hold": "УДЕРЖ ГРУППЫ",
"Single Hold": "УДЕРЖ ЛИЧН",
"TX Denoise": "ШУМОПОД TX",
"RX Denoise": "ШУМОПОД RX",
"Contacts Set": "НАСТР КОНТАКТ",
"TG List Set": "НАСТР СП ТГ",
"Encryption Set": "НАСТР ШИФР",
"Called Show": "ПОКАЗ ВЫЗЫВ",
"Send DTMF": "ПЕРЕД DTMF",
"Caller Keep": "УДЕРЖ ВЫЗЫВ",
"Call Log": "ЖУРНАЛ ВЫЗ",
"Clear All Log": "ОЧИСТ ЖУРНАЛ",
"Address Book": "АДР КНИГА",
"DMR Or Analog": "DMR ИЛИ АНАЛОГ",
"RX/TX Limit": "ЛИМИТ RX/TX",
"CH Alias": "ИМЯ КАНАЛА",
"TX Power": "МОЩН TX",
"Scan Add": "ДОБАВ В СКАН",
"TOT": "ТАЙМАУТ TX",
"Offset Freq": "СДВИГ ЧАСТ",
"Set TX Freq": "ЗАДАТЬ ЧАСТ TX",
"CTC/DCS (A)": "CTC/DCS (A)",
"RX CTC/DCS (A)": "RX CTC/DCS (A)",
"TX CTC/DCS (A)": "TX CTC/DCS (A)",
"DCS Encrypt(A)": "ШИФР DCS (A)",
"Mute Code (A)": "КОД МУТ (A)",
"Band Width (A)": "ПОЛОСА (A)",
"Tail Tone (A)": "ХВОСТ ТОН (A)",
"Scrambler (A)": "СКРЕМБЛ (A)",
"Busy Lock (A)": "БЛОК ЗАНЯТ (A)",
"RX Demod (A)": "ДЕМОД RX (A)",
"DMR Mode (D)": "РЕЖИМ DMR (D)",
"DMR Slot (D)": "СЛОТ DMR (D)",
"Color Code (D)": "ЦВ.КОД (D)",
"Contacts (D)": "КОНТАКТЫ (D)",
"TG List (D)": "СПИСОК ТГ (D)",
"Encryption (D)": "ШИФР (D)",
"TX Politely(D)": "ВЕЖЛ TX (D)",
"Promiscuous(D)": "ПРОМИСК (D)",
"Channel ID (D)": "ID КАНАЛА (D)",
"ID Select (D)": "ВЫБОР ID (D)",
"New SMS": "НОВОЕ СМС",
"Inbox": "ВХОДЯЩИЕ",
"Outbox": "ИСХОДЯЩИЕ",
"Drafts": "ЧЕРНОВИКИ",
"Default SMS": "ШАБЛОН СМС",
"Clear All SMS": "ОЧИСТ ВСЕ СМС",
"SMS Format": "ФОРМАТ СМС",
"SMS Font": "ШРИФТ СМС",
"SMS Prompt": "ОЗВУЧ СМС",
"Contacts List": "СПИС КОНТАКТ",
"Add Contact": "ДОБАВ КОНТАКТ",
"Edit Name": "ИЗМЕН ИМЯ",
"Select CH": "ВЫБОР КАНАЛА",
"Edit Type": "ИЗМЕН ТИП",
"Edit ID": "ИЗМЕН ID",
"Save": "СОХР",
"Edit Member": "ИЗМЕН ЧЛЕНОВ",
"Clear": "ОЧИСТ",
"Edit Key": "ИЗМЕН КЛЮЧ",
"Send": "ПЕРЕД",
"Time :": "ВРЕМЯ :",
"Type :": "ТИП :",
"ID :": "ID :",
"Name :": "ИМЯ :",
"Text :": "ТЕКСТ :",
"State:": "СТАТУС:",
"Callsign :": "ПОЗЫВНОЙ :",
"First Name :": "ИМЯ :",
"Last Name :": "ФАМИЛИЯ :",
"City :": "ГОРОД :",
"State :": "РЕГИОН :",
"Country :": "СТРАНА :",
"Call In/Out :": "ВХ/ИСХ ВЫЗ :",
"Call Type :": "ТИП ВЫЗОВА :",
"ID :": "ID :",
"Talk time :": "ВРЕМЯ РАЗГ :",
"Call start :": "НАЧ ВЫЗОВА :",
"Delete": "УДАЛИТЬ",
"Contacts": "КОНТАКТЫ",
"Dial No.": "НАБОР НОМЕРА",
"Reply": "ОТВЕТ",
"Forward": "ПЕРЕСЛАТЬ",
"Resend": "ПОВТОР ОТПР",
"Off": "ВЫКЛ",
"On": "ВКЛ",
"Edit": "ИЗМЕН",
"Busy": "ЗАНЯТО",
"0.01K": "0.01K",
"0.02K": "0.02K",
"0.03K": "0.03K",
"0.05K": "0.05K",
"0.10K": "0.10K",
"0.25K": "0.25K",
"0.50K": "0.50K",
"1.25K": "1.25K",
"2.50K": "2.50K",
"5.00K": "5.00K",
"6.25K": "6.25K",
"8.33K": "8.33K",
"10.0K": "10.0K",
"12.5K": "12.5K",
"20.0K": "20.0K",
"25.0K": "25.0K",
"50.0K": "50.0K",
" 100K": " 100K",
" 500K": " 500K",
" 1M": " 1M",
" 5M": " 5M",
" 5 S": " 5 С",
" 10 S": " 10 С",
" 15 S": " 15 С",
" 30 S": " 30 С",
" 45 S": " 45 С",
" 60 S": " 60 С",
" 75 S": " 75 С",
" 90 S": " 90 С",
"105 S": "105 С",
"120 S": "120 С",
"135 S": "135 С",
"150 S": "150 С",
"165 S": "165 С",
"180 S": "180 С",
"195 S": "195 С",
"210 S": "210 С",
"225 S": "225 С",
"240 S": "240 С",
"255 S": "255 С",
"270 S": "270 С",
"285 S": "285 С",
"300 S": "300 С",
"315 S": "315 С",
"330 S": "330 С",
"345 S": "345 С",
"360 S": "360 С",
"375 S": "375 С",
"390 S": "390 С",
"405 S": "405 С",
"420 S": "420 С",
"435 S": "435 С",
"450 S": "450 С",
"465 S": "465 С",
"480 S": "480 С",
"495 S": "495 С",
"510 S": "510 С",
"525 S": "525 С",
"540 S": "540 С",
"555 S": "555 С",
"570 S": "570 С",
"585 S": "585 С",
"600 S": "600 С",
"Reverse Freq": "РЕВЕРС ЧАСТ",
"1 : 1": "1 : 1",
"1 : 2": "1 : 2",
"1 : 3": "1 : 3",
"CO": "CO",
"TO": "TO",
"SE": "SE",
"Up": "ВВЕРХ",
"Down": "ВНИЗ",
"Original CH": "ИСХ КАНАЛ",
"Current CH": "ТЕК КАНАЛ",
"Local Alarm": "ЛОК ТРЕВОГА",
"Remote Alarm": "УДАЛ ТРЕВОГА",
"Local + Remote": "ЛОК + УДАЛ",
"Area A": "ЗОНА A",
"Main Area": "ОСН ЗОНА",
"Freq Mode": "РЕЖИМ ЧАСТ",
"CH Mode": "РЕЖИМ КАНАЛА",
"Zone Mode": "РЕЖИМ ЗОНЫ",
"Show CH No.": "ПОКАЗ НОМ КАН",
"Show Freq": "ПОКАЗ ЧАСТ",
"Show Alias": "ПОКАЗ ИМЯ",
"Ensure": "ПОДТВЕРД",
"Cancel": "ОТМЕНА",
"Hytera": "HYTERA",
"Motorola": "MOTOROLA",
"Unicode": "UNICODE",
"GBK": "GBK",
"None": "НЕТ",
"Monitor(Analog)": "МОНИТОР АНАЛОГ",
"H/L Power": "МОЩН ВЫС/НИЗ",
"Scanning": "СКАНИРОВ",
"Backlight On-off": "ПОДСВ ВКЛ/ВЫКЛ",
"Roger Beep": "БИП ОКОНЧ",
"FM Radio": "FM РАДИО",
"Alarm": "ТРЕВОГА",
"Freq Detect": "ДЕТЕКТ ЧАСТ",
"CTC/DCS Scan": "СКАН CTC/DCS",
"Send Single Tone": "ПЕРЕД ОДИН ТОН",
"Status Query": "ЗАПРОС СТАТУСА",
"Remote Monitor": "УДАЛ МОНИТОР",
"Remote Stun": "УДАЛ БЛОК",
"Remote Kill": "УДАЛ ОТКЛ",
"Remote Wake Up": "УДАЛ ПРОБУЖД",
"Online Check": "ПРОВ НА СВЯЗИ",
"RX AM/FM Switch": "ПЕРЕКЛ AM/FM RX",
"Analog Spectrum": "АНАЛОГ СПЕКТР",
"SQ": "ШУМОПОД",
"DA Switch": "ПЕРЕКЛ ЦИФР/АН",
"NOAA Mode": "РЕЖИМ NOAA",
"Jump To SMS Menu": "ПЕРЕХ В МЕНЮ СМС",
"Analog CH VOX": "VOX АНАЛОГ КАН",
"Zone Select": "ВЫБОР ЗОНЫ",
"Promiscuous Mode": "РЕЖИМ ПРОМИСК",
"Dual Slot On-off": "2 СЛОТА ВКЛ/ВЫК",
"Time Slot Switch": "ПЕРЕКЛ СЛОТА",
"Color Code SW": "ПЕРЕКЛ ЦВ.КОДА",
"DMR Encrypt Off": "ШИФР DMR ВЫКЛ",
"Jump To RX List": "ПЕРЕХ К СП RX",
"Jump To Contact": "ПЕРЕХ К КОНТ",
"Jump To DTMF Sel": "ПЕРЕХ К DTMF",
"Roger Beep 1": "БИП ОКОНЧ 1",
"Roger Beep 2": "БИП ОКОНЧ 2",
"Send Radio Name": "ПЕРЕД ИМЯ РАЦИИ",
"18-64 MHz": "18-64 МГЦ",
"64-136 MHz": "64-136 МГЦ",
"136-174 MHz": "136-174 МГЦ",
"174-240 MHz": "174-240 МГЦ",
"240-320 MHz": "240-320 МГЦ",
"320-400 MHz": "320-400 МГЦ",
"400-480 MHz": "400-480 МГЦ",
"480-560 MHz": "480-560 МГЦ",
"560-620 MHz": "560-620 МГЦ",
"840-920 MHz": "840-920 МГЦ",
"920-1000 MHz": "920-1000 МГЦ",
"0 ms": "0 МС",
"100 ms": "100 МС",
"200 ms": "200 МС",
"300 ms": "300 МС",
"400 ms": "400 МС",
"500 ms": "500 МС",
"600 ms": "600 МС",
"700 ms": "700 МС",
"800 ms": "800 МС",
"900 ms": "900 МС",
"1000 ms": "1000 МС",
"1100 ms": "1100 МС",
"1200 ms": "1200 МС",
"1300 ms": "1300 МС",
"1400 ms": "1400 МС",
"1500 ms": "1500 МС",
"1600 ms": "1600 МС",
"1700 ms": "1700 МС",
"1800 ms": "1800 МС",
"1900 ms": "1900 МС",
"2000 ms": "2000 МС",
"30 ms": "30 МС",
"40 ms": "40 МС",
"50 ms": "50 МС",
"60 ms": "60 МС",
"70 ms": "70 МС",
"80 ms": "80 МС",
"90 ms": "90 МС",
"110 ms": "110 МС",
"120 ms": "120 МС",
"130 ms": "130 МС",
"140 ms": "140 МС",
"150 ms": "150 МС",
"160 ms": "160 МС",
"170 ms": "170 МС",
"180 ms": "180 МС",
"190 ms": "190 МС",
"TX Start": "НАЧАЛО TX",
"TX End": "КОНЕЦ TX",
"TX Start & End": "НАЧ И КОНЕЦ TX",
"DTMF-01": "DTMF-01",
"DTMF-02": "DTMF-02",
"DTMF-03": "DTMF-03",
"DTMF-04": "DTMF-04",
"DTMF-05": "DTMF-05",
"DTMF-06": "DTMF-06",
"DTMF-07": "DTMF-07",
"DTMF-08": "DTMF-08",
"DTMF-09": "DTMF-09",
"DTMF-10": "DTMF-10",
"DTMF-11": "DTMF-11",
"DTMF-12": "DTMF-12",
"DTMF-13": "DTMF-13",
"DTMF-14": "DTMF-14",
"DTMF-15": "DTMF-15",
"DTMF-16": "DTMF-16",
"Individual": "ЛИЧНЫЙ",
"Group Call": "ГРУПП ВЫЗОВ",
"ARC": "ARC",
"AES 128": "AES 128",
"AES 256": "AES 256",
"Standard": "СТАНДАРТ",
"Encrypt 1": "ШИФР 1",
"Encrypt 2": "ШИФР 2",
"Encrypt 3": "ШИФР 3",
"Mute Code": "КОД МУТ",
"Wide": "ШИР",
"Narrow": "УЗК",
" 1": " 1",
" 2": " 2",
" 3": " 3",
" 4": " 4",
" 5": " 5",
" 6": " 6",
" 7": " 7",
" 8": " 8",
"Carrier Match": "ПО НЕСУЩЕЙ",
"CTC/DCS Match": "ПО CTC/DCS",
"Low": "НИЗ",
"High": "ВЫС",
"Add": "ДОБАВ",
"Remove": "УДАЛИТЬ",
"+": "+",
"-": "-",
"Dual Slot Off": "2 СЛОТА ВЫКЛ",
"Dual Slot On": "2 СЛОТА ВКЛ",
"Slot 1": "СЛОТ 1",
"Slot 2": "СЛОТ 2",
"Impolite": "НЕВЕЖЛИВО",
"CC Match": "ПО ЦВ.КОДУ",
"6 digits": "6 ЦИФР",
"8 digits": "8 ЦИФР",
"Show Caller Info": "ПОКАЗ ВЫЗЫВ",
"Show Called Info": "ПОКАЗ ВЫЗЫВАЕМ",
"FM": "FM",
"AM": "AM",
"SSB": "SSB",
"Tail Tone Off": "ХВОСТ ТОН ВЫКЛ",
"55Hz No Shift": "55Hz БЕЗ СДВ",
"120¡ã Shift": "120° СДВИГ",
"180¡ã Shift": "180° СДВИГ",
"240¡ã Shift": "240° СДВИГ",
"Radio ID": "ID РАЦИИ",
"Channel ID": "ID КАНАЛА",
"No": "НЕТ",
"Yes": "ДА",
"Latest Program": "ПОСЛ ПРОШИВКА",
"Init Config": "СБРОС НАСТР",
"Init All": "СБРОС ВСЕГО",
"Latest Tuning": "ПОСЛ КАЛИБР",
"Init Tune Data": "СБРОС КАЛИБР",
"DMR": "DMR",
"Analog": "АНАЛОГ",
"RX + TX": "RX + TX",
"Only RX": "ТОЛЬКО RX",
"Only TX": "ТОЛЬКО TX",
"S0 S1 S2 S3 S4 S": "S0 S1 S2 S3 S4 S",
"5 S6 S7 S8 S9 +1": "5 S6 S7 S8 S9 +1",
"0+20+30+40+50+": "0+20+30+40+50+"
}
Разница между файлами не показана из-за своего большого размера Загрузить разницу
+7 -7
Просмотреть файл
@@ -1,8 +1,8 @@
[
"Greece s",
"United States n",
"United States o",
"United States p",
"United States q",
"United States r"
[
"Greece s",
"United States n",
"United States o",
"United States p",
"United States q",
"United States r"
]
+318 -318
Просмотреть файл
@@ -1,319 +1,319 @@
{
"+pGpGpGpGuF": "+pGpGpGpGuF",
" DTMF Remote Kill": " DTMF УДАЛ ОТКЛ",
"DMR Remote Kill ": "DMR УДАЛ ОТКЛ",
" Copy(Transmit) ": " КОПИЯ(ПЕРЕДАЧА)",
" Copy(Recive) ": " КОПИЯ(ПРИЁМ)",
"Receving": "ПРИЁМ",
"@Backing up... ": "@РЕЗ.КОПИЯ...",
"Please wait ": "ПОДОЖДИТЕ",
" ERROR": " ОШИБ",
" -000DBM": " -000DBM",
"Password : ": "ПАРОЛЬ :",
"Emergency Alarm ": "ТРЕВОГА",
" Being Searched ": " ПОИСК",
" Radio Wake Up ": " БУДИЛЬНИК",
"DMR Remote Stun ": "DMR УДАЛ БЛОК",
" call stopped. ": " ВЫЗОВ ОСТАНОВЛЕН",
"Call type error,": "ОШИБКА ТИПА ВЫЗ",
" Dialing ": " ВЫЗОВ",
"Callback ": "ОБР.ВЫЗОВ",
"Addressbook ": "АДР.КНИГА",
"All Call ": "ОБЩ.ВЫЗОВ",
"Contact Error! ": "ОШИБКА КОНТАКТА",
"No Analog CH ": "НЕТ АНАЛОГ КАН",
" Group Call": " ГРУПП.ВЫЗ",
" All Call ": " ОБЩ.ВЫЗОВ",
" Individual": " ЛИЧНЫЙ",
" Address Book ": " АДР.КНИГА",
" CTC/DCS (A)": " CTC/DCS (A)",
" RX CTC/DCS (A)": " RX CTC/DCS (A)",
"TX CTC/DCS (A)": "TX CTC/DCS (A)",
" * Switch Type ": " * СМЕНА ТИПА",
"No Call Records ": "НЕТ ЗАПИСЕЙ ВЫЗ",
" Call Log ": " ЖУРНАЛ",
" Remote CTC/DCS ": " УДАЛ CTC/DCS",
"Group Call": "ГРУПП.ВЫЗ",
"All Call ": "ОБЩ.ВЫЗОВ",
"All Call ": "ОБЩ.ВЫЗОВ",
"Being Monitored ": "ПРОСЛУШКА",
" Drafts ": " ЧЕРНОВИКИ",
" Edit Key ": " ПРАВКА КЛЮЧА",
"Station Name : ": "ИМЯ СТАНЦИИ :",
" Offset Set ": " НАСТР СДВИГА",
"Switch": "СМЕНА",
" Edit Member ": " ПРАВКА УЧАСТНИК",
" Inbox ": " ВХОДЯЩИЕ",
" Mute Code (A)": " КОД МУТ (A)",
" New SMS ": " НОВОЕ СМС",
" Outbox ": " ИСХОДЯЩИЕ",
" Default SMS ": " ШАБЛОН СМС",
"Inbox Empty ": "ВХОДЯЩИЕ ПУСТО",
" Outbox Empty ": " ИСХОДЯЩИЕ ПУСТО",
"Outbox ": "ИСХОДЯЩИЕ",
" Drafts Empty ": " ЧЕРНОВИКИ ПУСТО",
"Drafts ": "ЧЕРНОВИКИ",
" Empty ": " ПУСТО",
"Default SMS ": "ШАБЛОН СМС",
" Dial No. ": " НАБОР НОМЕРА",
"Individual ": "ЛИЧНЫЙ",
"Group Call ": "ГРУПП.ВЫЗОВ",
"VER :RT-4D V3.25": "VER :RT-4D V3.25",
" DATE:2026-02-05 ": " DATE:2026-02-05 ",
"DMR : ": "DMR :",
" IC : ": " IC :",
"Unknown ": "НЕИЗВЕСТНО",
"BATT:0.0V ": "BATT:0.0V",
" Version ": " ВЕРСИЯ",
"Power: High ": "МОЩН: ВЫС",
"Power: Low ": "МОЩН: НИЗ",
" Dual Standby:Off": " ДВОЙН ОЖИД:ВЫКЛ",
"Dual Standby:On ": "ДВОЙН ОЖИД:ВКЛ",
"TX Priority:Edit": "ПРИОР TX:ПРАВКА",
"TX Priority:Busy": "ПРИОР TX:ЗАНЯТО",
" Scanning ": " СКАНИРОВ",
"TX End Tone: Off": "ТОН КОНЦА TX:ВЫК",
"TX End Tone: 01 ": "ТОН КОНЦА TX: 01",
"TX End Tone: 02 ": "ТОН КОНЦА TX: 02",
"TX End Tone: MDC": "ТОН КОНЦА TX:MDC",
" Talkaround : Off": " ПРЯМОЙ : ВЫКЛ",
"Talkaround ": "ПРЯМОЙ",
"Reverse Freq ": "РЕВЕРС ЧАСТ",
" Emergency ": " ТРЕВОГА",
" No Analog CH ": " НЕТ АНАЛОГ КАН",
"@Send Single Tone": "@ПЕРЕД ОДИН ТОН",
"Radio Online ": "РАЦИЯ НА СВЯЗИ",
"Radio Offline ": "РАЦИЯ НЕ НА СВЯЗ",
"Check Failed ": "ПРОВЕРКА СБОЙ",
" Show Caller Info": " ПОКАЗ ВЫЗЫВ",
"Called Show: ": "ПОКАЗ ВЫЗЫВ:",
"Show Called Info": "ПОКАЗ ВЫЗЫВАЕМ",
" RX Mode : AM ": " РЕЖИМ RX : AM",
"RX Mode : SSB ": "РЕЖИМ RX : SSB",
"RX Mode : FM ": "РЕЖИМ RX : FM",
" Analog Mode ": " АНАЛОГ РЕЖИМ",
"Digital Mode ": "ЦИФР РЕЖИМ",
" Analog VOX : On ": " АНАЛОГ VOX : ВКЛ",
"Analog VOX : Off": "АНАЛОГ VOX :ВЫКЛ",
"Promiscuous: On ": "ПРОМИСК: ВКЛ",
"Promiscuous: Off": "ПРОМИСК: ВЫКЛ",
"Dual Slot : Off ": "2 СЛОТА : ВЫКЛ",
"Dual Slot : On ": "2 СЛОТА : ВКЛ",
"g oIHx": "g oIHx",
" DMR Time Slot: 1": " DMR СЛОТ: 1",
"DMR Time Slot: 2": "DMR СЛОТ: 2",
" DMR Encrypt: Off": " ШИФР DMR: ВЫКЛ",
"is out of range ": "ВНЕ ДИАПАЗОНА",
" Resulting freq ": " ИТОГ ЧАСТОТА",
" Cannot be set! ": " НЕЛЬЗЯ ЗАДАТЬ!",
"Single display ": "ОДИН ДИСПЛЕЙ",
"ID Out Of Range ": "ID ВНЕ ДИАПАЗ",
" Clear All SMS ": " ОЧИСТ ВСЕ СМС",
" Contacts Full! ": " КОНТАКТЫ ПОЛНЫ!",
"Type Error! ": "ОШИБКА ТИПА!",
"Saving Data.....": "СОХР ДАННЫХ.....",
" Clearing Data...": " ОЧИСТ ДАННЫХ...",
"g cIHx": "g cIHx",
"Starting up... ": "ЗАПУСК...",
"Please wait ": "ПОДОЖДИТЕ",
"CH MODE ": "РЕЖИМ КАНАЛА",
"VFO MODE ": "VFO РЕЖИМ",
" VFO MODE-A ": " VFO РЕЖИМ-A",
" VFO MODE-B ": " VFO РЕЖИМ-B",
"All Call": "ОБЩ.ВЫЗ",
"Recovering... ": "ВОССТАНОВЛ...",
" CH----- ": " CH-----",
"Update DMR Chip ": "ОБНОВЛ DMR ЧИП",
"Please Wait... ": "ПОДОЖДИТЕ...",
"Menu ": "МЕНЮ",
"Basic Set ": "ОСН. НАСТР",
"Key Define ": "НАЗНАЧ КЛАВ",
"Analog Set ": "АНАЛОГ НАСТР",
"Digital Set ": "ЦИФР НАСТР",
"Channel Set ": "НАСТР КАНАЛА",
" Zone Set ": " НАСТР ЗОНЫ",
"Message ": "СМС",
"Device Name ": "ИМЯ РАЦИИ",
"Voice Prompt ": "ОЗВУЧ",
"Key Beep ": "БИП КЛАВ",
"Lock Timer ": "ТАЙМЕР БЛОК",
"Backlight ": "ПОДСВЕТКА",
"Light Timer ": "ТАЙМЕР СВЕТА",
"Brightness ": "ЯРКОСТЬ",
"Menu Exit ": "ВЫХОД ИЗ МЕНЮ",
"Dual Standby ": "ДВОЙН ОЖИД",
"TX Priority ": "ПРИОР TX",
"Freq Step ": "ШАГ ЧАСТ",
"Save Mode ": "РЕЖИМ ЭКОН",
"Scan Mode ": "РЕЖИМ СКАН",
"Scan Direction ": "НАПРАВ СКАН",
"Scan Dwell ": "ЗАДЕРЖ СКАН",
" Scan Interval ": " ИНТЕРВ СКАН",
"Scan Return ": "ВОЗВРАТ СКАН",
"Scan Start ": "НАЧАЛО СКАН",
"Scan End ": "КОНЕЦ СКАН",
"Alarm Type ": "ТИП ТРЕВОГИ",
"Main PTT TX ": "ОСН PTT TX",
"Area A Mode ": "РЕЖИМ ЗОНЫ A",
"Area A Show ": "ПОКАЗ ЗОНЫ A",
"Area A Zone ": "ЗОНА A",
"Area B Mode ": "РЕЖИМ ЗОНЫ B",
"Area B Show ": "ПОКАЗ ЗОНЫ B",
"Area B Zone ": "ЗОНА B",
"Save CH ": "СОХР КАНАЛ",
"Delete CH ": "УДАЛ КАНАЛ",
"LCD Contrast ": "КОНТРАСТ LCD",
"Freq Input ": "ВВОД ЧАСТ",
"Reverse CH Dir ": "РЕВЕРС КАНАЛА",
"Carrier LED ": "СВЕТОДИОД НЕС",
"RSSI Refresh ": "ОБНОВЛ RSSI",
"APO ": "АВТОВЫКЛ",
"APO Timer ": "ТАЙМЕР АВТОВЫК",
"FM RX Standby ": "FM ПРИЁМ ОЖИД",
"Initialization ": "СБРОС",
" Second PTT ": " ВТОРОЙ PTT",
"Side Key 1 S ": "БОК КЛАВ 1 КОР",
"Side Key 1 L ": "БОК КЛАВ 1 ДЛ",
"Side Key 2 S ": "БОК КЛАВ 2 КОР",
"Side Key 2 L ": "БОК КЛАВ 2 ДЛ",
"0 Press Long ": "0 ДЛ НАЖАТИЕ",
"1 Press Long ": "1 ДЛ НАЖАТИЕ",
"2 Press Long ": "2 ДЛ НАЖАТИЕ",
"3 Press Long ": "3 ДЛ НАЖАТИЕ",
"4 Press Long ": "4 ДЛ НАЖАТИЕ",
"5 Press Long ": "5 ДЛ НАЖАТИЕ",
"6 Press Long ": "6 ДЛ НАЖАТИЕ",
"7 Press Long ": "7 ДЛ НАЖАТИЕ",
"8 Press Long ": "8 ДЛ НАЖАТИЕ",
"9 Press Long ": "9 ДЛ НАЖАТИЕ",
" SQ Level ": " УР ШУМОПОД",
"TX Start Tone ": "ТОН НАЧ TX",
"TX End Tone ": "ТОН КОНЦА TX",
"Single Tone ": "ОДИНОЧ ТОН",
"Tone Timer ": "ТАЙМЕР ТОНА",
"MIC Gain ": "УСИЛ МИКР",
"SPK Gain ": "УСИЛ ДИНАМ",
"Glitch TH ": "ПОРОГ ПОМЕХ",
"Detect Range ": "ДИАПАЗ ДЕТЕКТ",
"Repeater Delay ": "ЗАДЕРЖ РЕПИТ",
"DTMF Delay ": "ЗАДЕРЖ DTMF",
"DTMF Interval ": "ИНТЕРВ DTMF",
"DTMF Duration ": "ДЛИТ DTMF",
"DTMF Mode ": "РЕЖИМ DTMF",
"DTMF Select ": "ВЫБОР DTMF",
"DTMF Display ": "ПОКАЗ DTMF",
"DTMF TX Gain ": "УСИЛ TX DTMF",
"DTMF RX TH ": "ПОРОГ RX DTMF",
"DTMF Control ": "УПРАВЛ DTMF",
"VOX ": "VOX",
"VOX Delay ": "ЗАДЕРЖ VOX",
"VOX TH ": "ПОРОГ VOX",
"Short Tail ": "КОР ХВОСТ",
"Personal ID ": "ЛИЧНЫЙ ID",
"Call Tone ": "ТОН ВЫЗОВА",
"Call End Tone ": "ТОН КОНЦА ВЫЗ",
"Group Hold ": "УДЕРЖ ГРУППЫ",
"Single Hold ": "УДЕРЖ ЛИЧН",
"TX Denoise ": "ШУМОПОД TX",
"RX Denoise ": "ШУМОПОД RX",
"Contacts Set ": "НАСТР КОНТАКТ",
" Talk Group ": " ГРУППА",
"Encryption Set ": "НАСТР ШИФР",
"Called Show ": "ПОКАЗ ВЫЗЫВ",
"Send DTMF ": "ПЕРЕД DTMF",
"Caller Keep ": "УДЕРЖ ВЫЗЫВ",
"Clear All Log? ": "ОЧИСТ ЖУРНАЛ?",
"DMR Or Analog ": "DMR ИЛИ АНАЛОГ",
"RX/TX Limit ": "ЛИМИТ RX/TX",
"CH Alias ": "ИМЯ КАНАЛА",
"TX Power ": "МОЩН TX",
"Scan Add ": "ДОБАВ В СКАН",
"TOT ": "ТАЙМАУТ TX",
"Set TX Freq ": "ЗАДАТЬ ЧАСТ TX",
"DCS Encrypt (A)": "ШИФР DCS (A)",
"Band Width (A)": "ПОЛОСА (A)",
"Tail Tone (A)": "ХВОСТ ТОН (A)",
"Scrambler (A)": "СКРЕМБЛ (A)",
"Busy Lock (A)": "БЛОК ЗАНЯТ (A)",
"RX Demod (A)": "ДЕМОД RX (A)",
"DMR Mode (D)": "РЕЖИМ DMR (D)",
"DMR Slot (D)": "СЛОТ DMR (D)",
"Color Code (D)": "ЦВ.КОД (D)",
"Contacts (D)": "КОНТАКТЫ (D)",
"TG List (D)": "СПИСОК ТГ (D)",
" Encryption (D)": " ШИФР (D)",
"TX Politely (D)": "ВЕЖЛ TX (D)",
"Promiscuous (D)": "ПРОМИСК (D)",
"Channel ID (D) ": "ID КАНАЛА (D)",
"ID Select (D)": "ВЫБОР ID (D)",
"Clear All SMS? ": "ОЧИСТ ВСЕ СМС?",
"SMS Format ": "ФОРМАТ СМС",
"SMS Font ": "ШРИФТ СМС",
"SMS Prompt ": "ОЗВУЧ СМС",
"Cantacts List ": "СПИС КОНТАКТ",
"Add Contact ": "ДОБАВ КОНТАКТ",
"Edit Name ": "ИЗМЕН ИМЯ",
"New SMS ": "НОВОЕ СМС",
"Edit Type ": "ИЗМЕН ТИП",
"Edit ID ": "ИЗМЕН ID",
"Save? ": "СОХРАНИТЬ?",
"Delete? ": "УДАЛИТЬ?",
"Send ": "ПЕРЕД",
"Inbox ": "ВХОДЯЩИЕ",
"Outbox ": "ИСХОДЯЩИЕ",
"Contacts ": "КОНТАКТЫ",
" Reply ": " ОТВЕТ",
"Forward ": "ПЕРЕСЛАТЬ",
"Resend? ": "ПОВТОР ОТПР?",
" Query ID ": " ЗАПРОС ID",
" Non-existent ID ": " ID НЕ СУЩЕСТВУЕТ",
"Draft Full! ": "ЧЕРНОВИКИ ПОЛНЫ!",
"No Digital CH ": "НЕТ ЦИФР КАН",
" ID Conflict ": " КОНФЛИКТ ID",
" Send OK ": " ОТПРАВЛЕНО",
"Send Failed ": "СБОЙ ОТПРАВКИ",
"Dual Display ": "ДВОЙН ДИСПЛЕЙ",
"Single Display ": "ОДИН ДИСПЛЕЙ",
"NJPTLH": "NJPTLH",
"0JPT.H": "0JPT.H",
"PC Programing ": "ПРОГРАММ С ПК",
" Calibration OK! ": " КАЛИБРОВКА OK!",
"DTMF Remote Stun": "DTMF УДАЛ БЛОК",
"DTMF Wake up ": "DTMF БУДИЛЬНИК",
"@Recovering... ": "@ВОССТАНОВЛ...",
"Members Full ": "УЧАСТНИКИ ПОЛНЫ",
"CH Full ": "КАНАЛЫ ПОЛНЫ",
" Area A MDC: ": " ЗОНА A MDC:",
"Area B MDC: ": "ЗОНА B MDC:",
"Area A DTMF: ": "ЗОНА A DTMF:",
"Area B DTMF: ": "ЗОНА B DTMF:",
" ID: ": " ID:",
"Unknown Country ": "СТРАНА НЕИЗВ",
" Off ": " ВЫКЛ",
" CH- ": " CH-",
" Call Out ": " ИСХ ВЫЗОВ",
"Call In ": "ВХ ВЫЗОВ",
" All Call ": " ОБЩ.ВЫЗОВ",
"ycd)BBH": "ycd)BBH",
"FM RX ": "FM RX",
" AM RX ": " AM RX",
"SSB RX ": "SSB RX",
"@Clear All Limit ": "@ОЧИСТ ВСЕ ЛИМИТ",
" Monitor Mode ": " РЕЖИМ МОНИТОР",
"<?lllllllllOKB+cdefgh1H": "<?lllllllllOKB+cdefgh1H",
" repeater failed ": " СБОЙ РЕПИТЕРА",
"Connect to ": "ПОДКЛ К",
" 4IHr4H": " 4IHr4H",
"Please Charge! ": "ЗАРЯДИТЕ!",
"g 6IHx": "g 6IHx",
"DRM Remote Stun ": "DMR УДАЛ БЛОК",
"Prohibit TX ": "ЗАПРЕТ TX",
" Invalid ID ": " НЕВЕРНЫЙ ID",
" NO CTC/DCS ": " НЕТ CTC/DCS",
"CTCSS: ": "CTCSS:",
"DCS:D N ": "DCS:D N",
"qHqBH": "qHqBH",
"tAtpG": "tAtpG",
"United States f": "США f",
"Canada ": "КАНАДА"
{
"+pGpGpGpGuF": "+pGpGpGpGuF",
" DTMF Remote Kill": " DTMF УДАЛ ОТКЛ",
"DMR Remote Kill ": "DMR УДАЛ ОТКЛ",
" Copy(Transmit) ": " КОПИЯ(ПЕРЕДАЧА)",
" Copy(Recive) ": " КОПИЯ(ПРИЁМ)",
"Receving": "ПРИЁМ",
"@Backing up... ": "@РЕЗ.КОПИЯ...",
"Please wait ": "ПОДОЖДИТЕ",
" ERROR": " ОШИБ",
" -000DBM": " -000DBM",
"Password : ": "ПАРОЛЬ :",
"Emergency Alarm ": "ТРЕВОГА",
" Being Searched ": " ПОИСК",
" Radio Wake Up ": " БУДИЛЬНИК",
"DMR Remote Stun ": "DMR УДАЛ БЛОК",
" call stopped. ": " ВЫЗОВ ОСТАНОВЛЕН",
"Call type error,": "ОШИБКА ТИПА ВЫЗ",
" Dialing ": " ВЫЗОВ",
"Callback ": "ОБР.ВЫЗОВ",
"Addressbook ": "АДР.КНИГА",
"All Call ": "ОБЩ.ВЫЗОВ",
"Contact Error! ": "ОШИБКА КОНТАКТА",
"No Analog CH ": "НЕТ АНАЛОГ КАН",
" Group Call": " ГРУПП.ВЫЗ",
" All Call ": " ОБЩ.ВЫЗОВ",
" Individual": " ЛИЧНЫЙ",
" Address Book ": " АДР.КНИГА",
" CTC/DCS (A)": " CTC/DCS (A)",
" RX CTC/DCS (A)": " RX CTC/DCS (A)",
"TX CTC/DCS (A)": "TX CTC/DCS (A)",
" * Switch Type ": " * СМЕНА ТИПА",
"No Call Records ": "НЕТ ЗАПИСЕЙ ВЫЗ",
" Call Log ": " ЖУРНАЛ",
" Remote CTC/DCS ": " УДАЛ CTC/DCS",
"Group Call": "ГРУПП.ВЫЗ",
"All Call ": "ОБЩ.ВЫЗОВ",
"All Call ": "ОБЩ.ВЫЗОВ",
"Being Monitored ": "ПРОСЛУШКА",
" Drafts ": " ЧЕРНОВИКИ",
" Edit Key ": " ПРАВКА КЛЮЧА",
"Station Name : ": "ИМЯ СТАНЦИИ :",
" Offset Set ": " НАСТР СДВИГА",
"Switch": "СМЕНА",
" Edit Member ": " ПРАВКА УЧАСТНИК",
" Inbox ": " ВХОДЯЩИЕ",
" Mute Code (A)": " КОД МУТ (A)",
" New SMS ": " НОВОЕ СМС",
" Outbox ": " ИСХОДЯЩИЕ",
" Default SMS ": " ШАБЛОН СМС",
"Inbox Empty ": "ВХОДЯЩИЕ ПУСТО",
" Outbox Empty ": " ИСХОДЯЩИЕ ПУСТО",
"Outbox ": "ИСХОДЯЩИЕ",
" Drafts Empty ": " ЧЕРНОВИКИ ПУСТО",
"Drafts ": "ЧЕРНОВИКИ",
" Empty ": " ПУСТО",
"Default SMS ": "ШАБЛОН СМС",
" Dial No. ": " НАБОР НОМЕРА",
"Individual ": "ЛИЧНЫЙ",
"Group Call ": "ГРУПП.ВЫЗОВ",
"VER :RT-4D V3.25": "VER :RT-4D V3.25",
" DATE:2026-02-05 ": " DATE:2026-02-05 ",
"DMR : ": "DMR :",
" IC : ": " IC :",
"Unknown ": "НЕИЗВЕСТНО",
"BATT:0.0V ": "BATT:0.0V",
" Version ": " ВЕРСИЯ",
"Power: High ": "МОЩН: ВЫС",
"Power: Low ": "МОЩН: НИЗ",
" Dual Standby:Off": " ДВОЙН ОЖИД:ВЫКЛ",
"Dual Standby:On ": "ДВОЙН ОЖИД:ВКЛ",
"TX Priority:Edit": "ПРИОР TX:ПРАВКА",
"TX Priority:Busy": "ПРИОР TX:ЗАНЯТО",
" Scanning ": " СКАНИРОВ",
"TX End Tone: Off": "ТОН КОНЦА TX:ВЫК",
"TX End Tone: 01 ": "ТОН КОНЦА TX: 01",
"TX End Tone: 02 ": "ТОН КОНЦА TX: 02",
"TX End Tone: MDC": "ТОН КОНЦА TX:MDC",
" Talkaround : Off": " ПРЯМОЙ : ВЫКЛ",
"Talkaround ": "ПРЯМОЙ",
"Reverse Freq ": "РЕВЕРС ЧАСТ",
" Emergency ": " ТРЕВОГА",
" No Analog CH ": " НЕТ АНАЛОГ КАН",
"@Send Single Tone": "@ПЕРЕД ОДИН ТОН",
"Radio Online ": "РАЦИЯ НА СВЯЗИ",
"Radio Offline ": "РАЦИЯ НЕ НА СВЯЗ",
"Check Failed ": "ПРОВЕРКА СБОЙ",
" Show Caller Info": " ПОКАЗ ВЫЗЫВ",
"Called Show: ": "ПОКАЗ ВЫЗЫВ:",
"Show Called Info": "ПОКАЗ ВЫЗЫВАЕМ",
" RX Mode : AM ": " РЕЖИМ RX : AM",
"RX Mode : SSB ": "РЕЖИМ RX : SSB",
"RX Mode : FM ": "РЕЖИМ RX : FM",
" Analog Mode ": " АНАЛОГ РЕЖИМ",
"Digital Mode ": "ЦИФР РЕЖИМ",
" Analog VOX : On ": " АНАЛОГ VOX : ВКЛ",
"Analog VOX : Off": "АНАЛОГ VOX :ВЫКЛ",
"Promiscuous: On ": "ПРОМИСК: ВКЛ",
"Promiscuous: Off": "ПРОМИСК: ВЫКЛ",
"Dual Slot : Off ": "2 СЛОТА : ВЫКЛ",
"Dual Slot : On ": "2 СЛОТА : ВКЛ",
"g oIHx": "g oIHx",
" DMR Time Slot: 1": " DMR СЛОТ: 1",
"DMR Time Slot: 2": "DMR СЛОТ: 2",
" DMR Encrypt: Off": " ШИФР DMR: ВЫКЛ",
"is out of range ": "ВНЕ ДИАПАЗОНА",
" Resulting freq ": " ИТОГ ЧАСТОТА",
" Cannot be set! ": " НЕЛЬЗЯ ЗАДАТЬ!",
"Single display ": "ОДИН ДИСПЛЕЙ",
"ID Out Of Range ": "ID ВНЕ ДИАПАЗ",
" Clear All SMS ": " ОЧИСТ ВСЕ СМС",
" Contacts Full! ": " КОНТАКТЫ ПОЛНЫ!",
"Type Error! ": "ОШИБКА ТИПА!",
"Saving Data.....": "СОХР ДАННЫХ.....",
" Clearing Data...": " ОЧИСТ ДАННЫХ...",
"g cIHx": "g cIHx",
"Starting up... ": "ЗАПУСК...",
"Please wait ": "ПОДОЖДИТЕ",
"CH MODE ": "РЕЖИМ КАНАЛА",
"VFO MODE ": "VFO РЕЖИМ",
" VFO MODE-A ": " VFO РЕЖИМ-A",
" VFO MODE-B ": " VFO РЕЖИМ-B",
"All Call": "ОБЩ.ВЫЗ",
"Recovering... ": "ВОССТАНОВЛ...",
" CH----- ": " CH-----",
"Update DMR Chip ": "ОБНОВЛ DMR ЧИП",
"Please Wait... ": "ПОДОЖДИТЕ...",
"Menu ": "МЕНЮ",
"Basic Set ": "ОСН. НАСТР",
"Key Define ": "НАЗНАЧ КЛАВ",
"Analog Set ": "АНАЛОГ НАСТР",
"Digital Set ": "ЦИФР НАСТР",
"Channel Set ": "НАСТР КАНАЛА",
" Zone Set ": " НАСТР ЗОНЫ",
"Message ": "СМС",
"Device Name ": "ИМЯ РАЦИИ",
"Voice Prompt ": "ОЗВУЧ",
"Key Beep ": "БИП КЛАВ",
"Lock Timer ": "ТАЙМЕР БЛОК",
"Backlight ": "ПОДСВЕТКА",
"Light Timer ": "ТАЙМЕР СВЕТА",
"Brightness ": "ЯРКОСТЬ",
"Menu Exit ": "ВЫХОД ИЗ МЕНЮ",
"Dual Standby ": "ДВОЙН ОЖИД",
"TX Priority ": "ПРИОР TX",
"Freq Step ": "ШАГ ЧАСТ",
"Save Mode ": "РЕЖИМ ЭКОН",
"Scan Mode ": "РЕЖИМ СКАН",
"Scan Direction ": "НАПРАВ СКАН",
"Scan Dwell ": "ЗАДЕРЖ СКАН",
" Scan Interval ": " ИНТЕРВ СКАН",
"Scan Return ": "ВОЗВРАТ СКАН",
"Scan Start ": "НАЧАЛО СКАН",
"Scan End ": "КОНЕЦ СКАН",
"Alarm Type ": "ТИП ТРЕВОГИ",
"Main PTT TX ": "ОСН PTT TX",
"Area A Mode ": "РЕЖИМ ЗОНЫ A",
"Area A Show ": "ПОКАЗ ЗОНЫ A",
"Area A Zone ": "ЗОНА A",
"Area B Mode ": "РЕЖИМ ЗОНЫ B",
"Area B Show ": "ПОКАЗ ЗОНЫ B",
"Area B Zone ": "ЗОНА B",
"Save CH ": "СОХР КАНАЛ",
"Delete CH ": "УДАЛ КАНАЛ",
"LCD Contrast ": "КОНТРАСТ LCD",
"Freq Input ": "ВВОД ЧАСТ",
"Reverse CH Dir ": "РЕВЕРС КАНАЛА",
"Carrier LED ": "СВЕТОДИОД НЕС",
"RSSI Refresh ": "ОБНОВЛ RSSI",
"APO ": "АВТОВЫКЛ",
"APO Timer ": "ТАЙМЕР АВТОВЫК",
"FM RX Standby ": "FM ПРИЁМ ОЖИД",
"Initialization ": "СБРОС",
" Second PTT ": " ВТОРОЙ PTT",
"Side Key 1 S ": "БОК КЛАВ 1 КОР",
"Side Key 1 L ": "БОК КЛАВ 1 ДЛ",
"Side Key 2 S ": "БОК КЛАВ 2 КОР",
"Side Key 2 L ": "БОК КЛАВ 2 ДЛ",
"0 Press Long ": "0 ДЛ НАЖАТИЕ",
"1 Press Long ": "1 ДЛ НАЖАТИЕ",
"2 Press Long ": "2 ДЛ НАЖАТИЕ",
"3 Press Long ": "3 ДЛ НАЖАТИЕ",
"4 Press Long ": "4 ДЛ НАЖАТИЕ",
"5 Press Long ": "5 ДЛ НАЖАТИЕ",
"6 Press Long ": "6 ДЛ НАЖАТИЕ",
"7 Press Long ": "7 ДЛ НАЖАТИЕ",
"8 Press Long ": "8 ДЛ НАЖАТИЕ",
"9 Press Long ": "9 ДЛ НАЖАТИЕ",
" SQ Level ": " УР ШУМОПОД",
"TX Start Tone ": "ТОН НАЧ TX",
"TX End Tone ": "ТОН КОНЦА TX",
"Single Tone ": "ОДИНОЧ ТОН",
"Tone Timer ": "ТАЙМЕР ТОНА",
"MIC Gain ": "УСИЛ МИКР",
"SPK Gain ": "УСИЛ ДИНАМ",
"Glitch TH ": "ПОРОГ ПОМЕХ",
"Detect Range ": "ДИАПАЗ ДЕТЕКТ",
"Repeater Delay ": "ЗАДЕРЖ РЕПИТ",
"DTMF Delay ": "ЗАДЕРЖ DTMF",
"DTMF Interval ": "ИНТЕРВ DTMF",
"DTMF Duration ": "ДЛИТ DTMF",
"DTMF Mode ": "РЕЖИМ DTMF",
"DTMF Select ": "ВЫБОР DTMF",
"DTMF Display ": "ПОКАЗ DTMF",
"DTMF TX Gain ": "УСИЛ TX DTMF",
"DTMF RX TH ": "ПОРОГ RX DTMF",
"DTMF Control ": "УПРАВЛ DTMF",
"VOX ": "VOX",
"VOX Delay ": "ЗАДЕРЖ VOX",
"VOX TH ": "ПОРОГ VOX",
"Short Tail ": "КОР ХВОСТ",
"Personal ID ": "ЛИЧНЫЙ ID",
"Call Tone ": "ТОН ВЫЗОВА",
"Call End Tone ": "ТОН КОНЦА ВЫЗ",
"Group Hold ": "УДЕРЖ ГРУППЫ",
"Single Hold ": "УДЕРЖ ЛИЧН",
"TX Denoise ": "ШУМОПОД TX",
"RX Denoise ": "ШУМОПОД RX",
"Contacts Set ": "НАСТР КОНТАКТ",
" Talk Group ": " ГРУППА",
"Encryption Set ": "НАСТР ШИФР",
"Called Show ": "ПОКАЗ ВЫЗЫВ",
"Send DTMF ": "ПЕРЕД DTMF",
"Caller Keep ": "УДЕРЖ ВЫЗЫВ",
"Clear All Log? ": "ОЧИСТ ЖУРНАЛ?",
"DMR Or Analog ": "DMR ИЛИ АНАЛОГ",
"RX/TX Limit ": "ЛИМИТ RX/TX",
"CH Alias ": "ИМЯ КАНАЛА",
"TX Power ": "МОЩН TX",
"Scan Add ": "ДОБАВ В СКАН",
"TOT ": "ТАЙМАУТ TX",
"Set TX Freq ": "ЗАДАТЬ ЧАСТ TX",
"DCS Encrypt (A)": "ШИФР DCS (A)",
"Band Width (A)": "ПОЛОСА (A)",
"Tail Tone (A)": "ХВОСТ ТОН (A)",
"Scrambler (A)": "СКРЕМБЛ (A)",
"Busy Lock (A)": "БЛОК ЗАНЯТ (A)",
"RX Demod (A)": "ДЕМОД RX (A)",
"DMR Mode (D)": "РЕЖИМ DMR (D)",
"DMR Slot (D)": "СЛОТ DMR (D)",
"Color Code (D)": "ЦВ.КОД (D)",
"Contacts (D)": "КОНТАКТЫ (D)",
"TG List (D)": "СПИСОК ТГ (D)",
" Encryption (D)": " ШИФР (D)",
"TX Politely (D)": "ВЕЖЛ TX (D)",
"Promiscuous (D)": "ПРОМИСК (D)",
"Channel ID (D) ": "ID КАНАЛА (D)",
"ID Select (D)": "ВЫБОР ID (D)",
"Clear All SMS? ": "ОЧИСТ ВСЕ СМС?",
"SMS Format ": "ФОРМАТ СМС",
"SMS Font ": "ШРИФТ СМС",
"SMS Prompt ": "ОЗВУЧ СМС",
"Cantacts List ": "СПИС КОНТАКТ",
"Add Contact ": "ДОБАВ КОНТАКТ",
"Edit Name ": "ИЗМЕН ИМЯ",
"New SMS ": "НОВОЕ СМС",
"Edit Type ": "ИЗМЕН ТИП",
"Edit ID ": "ИЗМЕН ID",
"Save? ": "СОХРАНИТЬ?",
"Delete? ": "УДАЛИТЬ?",
"Send ": "ПЕРЕД",
"Inbox ": "ВХОДЯЩИЕ",
"Outbox ": "ИСХОДЯЩИЕ",
"Contacts ": "КОНТАКТЫ",
" Reply ": " ОТВЕТ",
"Forward ": "ПЕРЕСЛАТЬ",
"Resend? ": "ПОВТОР ОТПР?",
" Query ID ": " ЗАПРОС ID",
" Non-existent ID ": " ID НЕ СУЩЕСТВУЕТ",
"Draft Full! ": "ЧЕРНОВИКИ ПОЛНЫ!",
"No Digital CH ": "НЕТ ЦИФР КАН",
" ID Conflict ": " КОНФЛИКТ ID",
" Send OK ": " ОТПРАВЛЕНО",
"Send Failed ": "СБОЙ ОТПРАВКИ",
"Dual Display ": "ДВОЙН ДИСПЛЕЙ",
"Single Display ": "ОДИН ДИСПЛЕЙ",
"NJPTLH": "NJPTLH",
"0JPT.H": "0JPT.H",
"PC Programing ": "ПРОГРАММ С ПК",
" Calibration OK! ": " КАЛИБРОВКА OK!",
"DTMF Remote Stun": "DTMF УДАЛ БЛОК",
"DTMF Wake up ": "DTMF БУДИЛЬНИК",
"@Recovering... ": "@ВОССТАНОВЛ...",
"Members Full ": "УЧАСТНИКИ ПОЛНЫ",
"CH Full ": "КАНАЛЫ ПОЛНЫ",
" Area A MDC: ": " ЗОНА A MDC:",
"Area B MDC: ": "ЗОНА B MDC:",
"Area A DTMF: ": "ЗОНА A DTMF:",
"Area B DTMF: ": "ЗОНА B DTMF:",
" ID: ": " ID:",
"Unknown Country ": "СТРАНА НЕИЗВ",
" Off ": " ВЫКЛ",
" CH- ": " CH-",
" Call Out ": " ИСХ ВЫЗОВ",
"Call In ": "ВХ ВЫЗОВ",
" All Call ": " ОБЩ.ВЫЗОВ",
"ycd)BBH": "ycd)BBH",
"FM RX ": "FM RX",
" AM RX ": " AM RX",
"SSB RX ": "SSB RX",
"@Clear All Limit ": "@ОЧИСТ ВСЕ ЛИМИТ",
" Monitor Mode ": " РЕЖИМ МОНИТОР",
"<?lllllllllOKB+cdefgh1H": "<?lllllllllOKB+cdefgh1H",
" repeater failed ": " СБОЙ РЕПИТЕРА",
"Connect to ": "ПОДКЛ К",
" 4IHr4H": " 4IHr4H",
"Please Charge! ": "ЗАРЯДИТЕ!",
"g 6IHx": "g 6IHx",
"DRM Remote Stun ": "DMR УДАЛ БЛОК",
"Prohibit TX ": "ЗАПРЕТ TX",
" Invalid ID ": " НЕВЕРНЫЙ ID",
" NO CTC/DCS ": " НЕТ CTC/DCS",
"CTCSS: ": "CTCSS:",
"DCS:D N ": "DCS:D N",
"qHqBH": "qHqBH",
"tAtpG": "tAtpG",
"United States f": "США f",
"Canada ": "КАНАДА"
}
Разница между файлами не показана из-за своего большого размера Загрузить разницу