Мост к 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/ build/
*.spec *.spec
.DS_Store .DS_Store
# сборочный мусор
flasher/*.exe
+19 -6
Просмотреть файл
@@ -12,8 +12,12 @@
-**Полный реверс** главного MCU (ARM Cortex-M4F, клон STM32F407) и DMR-баузбенда FM100B (ARM32): карта памяти, периферия, вектора/ISR, serial-протокол, формат codeplug, UI-архитектура. -**Полный реверс** главного 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**. -**Русификация:** узкий однобайтный CP1251-рендер (патч диспетчера + code-cave в blitter), **свой шрифт 33 заглавные буквы** (13 переиспользуют байты стокового ASCII-шрифта, 20 нарисованы), перевод **485 пунктов меню + ~300 надписей** на заглавный русский. Ребренд версии → **Ru-4D V3.25**.
-**Блюпринт кастомного UI:** точки перехвата главного цикла, «SDK» из вызываемых функций стока (отрисовка/клавиши/радио/DMR/codeplug), C-тулчейн (clang → thumbv7em) — доказано инъекцией своего экрана. -**Блюпринт кастомного UI:** точки перехвата главного цикла, «SDK» из вызываемых функций стока (отрисовка/клавиши/радио/DMR/codeplug), C-тулчейн (clang → thumbv7em) — доказано инъекцией своего экрана.
-**Флешеры:** однофайловый веб-флешер (`.html`, WebSerial) и автономный Windows-`.exe` со вшитой прошивкой — прошивка «в одну кнопку». -**Флешер:** однофайловый веб-флешер (`.html`, WebSerial) со вшитой прошивкой — «в одну кнопку». Автономный `.exe` — в планах.
- ⏸️ **Полная переделка UI** — приостановлена: замена дежурного экрана ломает обработку клавиш (обход преамбулы стокового диспетчера). Возобновление — через *наложение* поверх стока. См. `docs/UI-Architecture.md`. - ⏸️ **Полная переделка 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). | | [`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). | | [`ui/`](ui/) | `my_ui.c` — кастомный UI-слой на C (компилируется clang'ом под Cortex-M4). |
| [`translations/`](translations/) | JSON-таблицы переводов (меню + rodata) и рабочие списки строк. | | [`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**, страховка). | | [`backup/`](backup/) | `radio-spi-dump.bin` — полный дамп SPI рации (**калибровка + codeplug**, страховка). |
| [`research/`](research/) | Черновые скрипты разведки (реверс «в процессе»), для истории. | | [`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`). 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`. Шрифт правится в `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/UI-Architecture.md](docs/UI-Architecture.md)** — как переписать весь UI поверх стока: машина экранов, точки перехвата, SDK вызываемых функций, план по фазам.
- **[docs/Russification.md](docs/Russification.md)** — осуществимость и механика русификации. - **[docs/Russification.md](docs/Russification.md)** — осуществимость и механика русификации.
- **[docs/subsystems/](docs/subsystems/)** — детальные разборы: `main-loop`, `display`, `input`, `radio`, `dmr`, `codeplug`, `main-screen`. - **[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. 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 ### 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). 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. 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. 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 ## 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.** **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) ## 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). 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. 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. 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.** 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 ## 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: **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| 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| 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| 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 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: It interleaves two 16-byte record kinds, both translatable:
| Kind | Count | Unique | Examples | Suffix | | Kind | Count | Unique | Examples | Suffix |
|---|---|---|---|---| |---|---|---|---|---|
| Menu items (label + `NN` ordinal) | 181 | 157 | `Basic Set␠␠␠␠␠01`, `Scan Direction15`, `Personal ID␠␠␠01` | 2 ASCII digits | | 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 | | 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: **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. - 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**. - 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. - 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.** **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`. 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. **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) ## 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`). **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). **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):** **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 9825:0410(А) 9826:0411(Б) ... 9831:0401(Ё) ... 9857:042f(Я) <- 33 upper incl. Ё
slot 9873:0430(а) 9874:0431(б) ... 9879:0451(ё) ... 9905:044f(я) <- 33 lower 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`). 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. **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. **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 ## 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`: **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`. - **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`**: - **CJK/GBK blitter `0x08008454`** — 14 px wide, 28-byte cell (`0x1C`), base **`0x19E000`**:
``` ```
row = lead 0x81 ; col = trail 0x40 row = lead 0x81 ; col = trail 0x40
index = col + row*190 ( 1 when trail > 0x7F, skipping the 0x7F gap) index = col + row*190 ( 1 when trail > 0x7F, skipping the 0x7F gap)
addr = 0x19E000 + index*28 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: **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 `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`. - 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. - `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. 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: **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 | | Char | GBK bytes | computed SPI offset | nonzero bytes in cell |
|---|---|---|---| |---|---|---|---|
| `А` | `A7 A7` | `0x1D00D8` | 20 — legible Cyrillic-A | | `А` | `A7 A7` | `0x1D00D8` | 20 — legible Cyrillic-A |
| `Я` | `A7 C0` | `0x1D0394` | 21 | | `Я` | `A7 C0` | `0x1D0394` | 21 |
| `а` | `A7 D1` | `0x1D0570` | 14 | | `а` | `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. **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.) (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 ## 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. **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? | | English | Russian | GBK bytes | Fits 14? |
|---|---|---|---| |---|---|---|---|
| Zone Set | Зоны | 8 | Yes | | Zone Set | Зоны | 8 | Yes |
| Menu | Меню | 8 | Yes | | Menu | Меню | 8 | Yes |
| Channel Set | Каналы | 12 | Yes | | Channel Set | Каналы | 12 | Yes |
| Radio Name | Имя | 6 | Yes | | Radio Name | Имя | 6 | Yes |
| Basic Set | Настр. | 11 | Yes | | Basic Set | Настр. | 11 | Yes |
| Basic Set | Настройки | 18 | **No → abbreviate** | | Basic Set | Настройки | 18 | **No → abbreviate** |
| Basic Set | Основные | 16 | **No → Основ. (11B)** | | Basic Set | Основные | 16 | **No → Основ. (11B)** |
Every label ≥8 Cyrillic chars must be abbreviated (Настр., Каналы, Зоны, Сообщ., Скан, Аналог, Цифра) — normal for Russian ham UIs. 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. **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 ## 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. **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 ## Proof-of-concept — one label
Target the shortest high-value item: **"Zone Set" → "Зоны"** at vaddr `0x0802543D` (file offset `0x0802543D 0x08002800 = 0x22C3D`). Target the shortest high-value item: **"Zone Set" → "Зоны"** at vaddr `0x0802543D` (file offset `0x0802543D 0x08002800 = 0x22C3D`).
Original 16 bytes: Original 16 bytes:
``` ```
5A 6F 6E 65 20 53 65 74 20 20 20 20 20 20 30 36 "Zone Set 06" 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): 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 A7 A9 A7 E0 A7 DF A7 ED 20 20 20 20 20 20 30 36
└─── Зоны ────────────┘ └── pad ───┘ └"06"┘ └─── Зоны ────────────┘ └── 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. **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. **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 ## 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. 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. 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. 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. 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. 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. 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. 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. **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:** **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 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. - **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. - **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. - **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. - **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. **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 `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), 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> <div class="warn" id="nowebserial">Откройте этот файл в <b>Google Chrome</b> или <b>Microsoft Edge</b> (нужен WebSerial).</div>
<ol> <ol>
<li><b>Выключите</b> рацию.</li> <li><b>Выключите</b> рацию.</li>
<li>Включите её, <b>удерживая клавишу «*»</b> (режим прошивки).</li> <li>Включите её, <b>удерживая тангенту PTT</b> (режим прошивки).</li>
<li>Подключите рацию к ПК кабелем USB-C.</li> <li>Подключите рацию к ПК кабелем USB-C.</li>
<li>Нажмите <b>«Прошить»</b> и выберите COM-порт рации.</li> <li>Нажмите <b>«Прошить»</b> и выберите COM-порт рации.</li>
<li><b>Не отключайте</b> кабель до конца записи.</li> <li><b>Не отключайте</b> кабель до конца записи.</li>
@@ -69,7 +69,7 @@ async function doFlash(){
log('Порт открыт. Синхронизация с бутлоадером...'); log('Порт открыт. Синхронизация с бутлоадером...');
port.flush();let ready=false;const end=Date.now()+7000; 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;}} 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('Бутлоадер найден. Стирание...'); log('Бутлоадер найден. Стирание...');
for(const trig of [0x10,0x55]){const p=[0x39,0x33,0x05,trig];p.push(cksum(p));port.flush();await port.write(p); 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('Ошибка стирания.');} 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.""" it into a single Windows .exe with PyInstaller."""
import base64, os, subprocess, sys 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" FW = f"{ROOT}/stock-fw/rt4d_ru_full.bin"
DIST = f"{ROOT}/dist" DIST = f"{ROOT}/dist"
@@ -45,7 +46,7 @@ def do_flash(port_name, log, prog):
if not b: p.write(b"\xff") if not b: p.write(b"\xff")
elif b == b"\xff": ready = True; break elif b == b"\xff": ready = True; break
if not ready: if not ready:
raise RuntimeError("Рация не отвечает. Выключите и включите её\nс зажатой клавишей * , затем повторите.") raise RuntimeError("Рация не отвечает. Выключите и включите её\nс зажатой тангентой PTT, затем повторите.")
log("Бутлоадер найден. Стирание...") log("Бутлоадер найден. Стирание...")
for trig in (0x10, 0x55): for trig in (0x10, 0x55):
pl = bytes([0x39, 0x33, 0x05, trig]); pl += cksum(pl) 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="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() tk.Label(root, text=INFO, font=("Segoe UI", 9), bg="#131a24", fg="#7f8ea3").pack()
steps = ("1. Выключите рацию.\n" steps = ("1. Выключите рацию.\n"
"2. Включите её, удерживая клавишу * (режим прошивки).\n" "2. Включите её, удерживая тангенту PTT (режим прошивки).\n"
"3. Подключите рацию к ПК кабелем USB-C.\n" "3. Подключите рацию к ПК кабелем USB-C.\n"
"4. Выберите COM-порт и нажмите Прошить .\n" "4. Выберите COM-порт и нажмите Прошить .\n"
"5. Не отключайте кабель до конца записи.") "5. Не отключайте кабель до конца записи.")
+4 -3
Просмотреть файл
@@ -3,7 +3,8 @@
embedded. Open in Chrome/Edge, one button flashes the RT-4D.""" embedded. Open in Chrome/Edge, one button flashes the RT-4D."""
import base64, os 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" FW = f"{ROOT}/stock-fw/rt4d_ru_full.bin"
OUT = f"{ROOT}/dist/Ru-4D_flasher.html" 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> <div class="warn" id="nowebserial">Откройте этот файл в <b>Google Chrome</b> или <b>Microsoft Edge</b> (нужен WebSerial).</div>
<ol> <ol>
<li><b>Выключите</b> рацию.</li> <li><b>Выключите</b> рацию.</li>
<li>Включите её, <b>удерживая клавишу «*»</b> (режим прошивки).</li> <li>Включите её, <b>удерживая тангенту PTT</b> (режим прошивки).</li>
<li>Подключите рацию к ПК кабелем USB-C.</li> <li>Подключите рацию к ПК кабелем USB-C.</li>
<li>Нажмите <b>«Прошить»</b> и выберите COM-порт рации.</li> <li>Нажмите <b>«Прошить»</b> и выберите COM-порт рации.</li>
<li><b>Не отключайте</b> кабель до конца записи.</li> <li><b>Не отключайте</b> кабель до конца записи.</li>
@@ -81,7 +82,7 @@ async function doFlash(){
log('Порт открыт. Синхронизация с бутлоадером...'); log('Порт открыт. Синхронизация с бутлоадером...');
port.flush();let ready=false;const end=Date.now()+7000; 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;}} 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('Бутлоадер найден. Стирание...'); log('Бутлоадер найден. Стирание...');
for(const trig of [0x10,0x55]){const p=[0x39,0x33,0x05,trig];p.push(cksum(p));port.flush();await port.write(p); 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('Ошибка стирания.');} 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)...") print("[i] handshake (0xFF probe)...")
if not handshake(p): if not handshake(p):
raise SystemExit("[E] no bootloader (radio not in flash mode). " 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] bootloader ready")
print("[i] erasing application region...") print("[i] erasing application region...")
erase(p) 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": "ОСН. НАСТР", "Basic Set": "ОСН. НАСТР",
"Key Define": "НАЗНАЧ КЛАВ", "Key Define": "НАЗНАЧ КЛАВ",
"Analog Set": "АНАЛОГ НАСТР", "Analog Set": "АНАЛОГ НАСТР",
"Digital Set": "ЦИФР НАСТР", "Digital Set": "ЦИФР НАСТР",
"Channel Set": "НАСТР КАНАЛА", "Channel Set": "НАСТР КАНАЛА",
"Zone Set": "НАСТР ЗОНЫ", "Zone Set": "НАСТР ЗОНЫ",
"Message": "СМС", "Message": "СМС",
"Radio Name": "ИМЯ РАЦИИ", "Radio Name": "ИМЯ РАЦИИ",
"Voice Prompt": "ОЗВУЧ", "Voice Prompt": "ОЗВУЧ",
"Key Beep": "БИП КЛАВ", "Key Beep": "БИП КЛАВ",
"Lock Timer": "ТАЙМЕР БЛОК", "Lock Timer": "ТАЙМЕР БЛОК",
"Backlight": "ПОДСВЕТКА", "Backlight": "ПОДСВЕТКА",
"Light Timer": "ТАЙМЕР СВЕТА", "Light Timer": "ТАЙМЕР СВЕТА",
"Brightness": "ЯРКОСТЬ", "Brightness": "ЯРКОСТЬ",
"Menu Exit": "ВЫХОД ИЗ МЕНЮ", "Menu Exit": "ВЫХОД ИЗ МЕНЮ",
"Dual Standby": "ДВОЙН ОЖИД", "Dual Standby": "ДВОЙН ОЖИД",
"TX Priority": "ПРИОР TX", "TX Priority": "ПРИОР TX",
"Freq Step": "ШАГ ЧАСТ", "Freq Step": "ШАГ ЧАСТ",
"Talkaround": "ПРЯМОЙ", "Talkaround": "ПРЯМОЙ",
"Save Mode": "РЕЖИМ ЭКОН", "Save Mode": "РЕЖИМ ЭКОН",
"Scan Mode": "РЕЖИМ СКАН", "Scan Mode": "РЕЖИМ СКАН",
"Scan Direction": "НАПРАВ СКАН", "Scan Direction": "НАПРАВ СКАН",
"Scan Dwell": "ЗАДЕРЖ СКАН", "Scan Dwell": "ЗАДЕРЖ СКАН",
"Scan Interval": "ИНТЕРВ СКАН", "Scan Interval": "ИНТЕРВ СКАН",
"Scan Return": "ВОЗВРАТ СКАН", "Scan Return": "ВОЗВРАТ СКАН",
"Scan Start": "НАЧАЛО СКАН", "Scan Start": "НАЧАЛО СКАН",
"Scan End": "КОНЕЦ СКАН", "Scan End": "КОНЕЦ СКАН",
"Alarm Type": "ТИП ТРЕВОГИ", "Alarm Type": "ТИП ТРЕВОГИ",
"Main PTT TX": "ОСН PTT TX", "Main PTT TX": "ОСН PTT TX",
"Area A Mode": "РЕЖИМ ЗОНЫ A", "Area A Mode": "РЕЖИМ ЗОНЫ A",
"Area A Show": "ПОКАЗ ЗОНЫ A", "Area A Show": "ПОКАЗ ЗОНЫ A",
"Area A Zone": "ЗОНА A", "Area A Zone": "ЗОНА A",
"Area B Mode": "РЕЖИМ ЗОНЫ B", "Area B Mode": "РЕЖИМ ЗОНЫ B",
"Area B Show": "ПОКАЗ ЗОНЫ B", "Area B Show": "ПОКАЗ ЗОНЫ B",
"Area B Zone": "ЗОНА B", "Area B Zone": "ЗОНА B",
"Save CH": "СОХР КАНАЛ", "Save CH": "СОХР КАНАЛ",
"Delete CH": "УДАЛ КАНАЛ", "Delete CH": "УДАЛ КАНАЛ",
"LCD Contrast": "КОНТРАСТ LCD", "LCD Contrast": "КОНТРАСТ LCD",
"Freq Input": "ВВОД ЧАСТ", "Freq Input": "ВВОД ЧАСТ",
"Reverse CH Dir": "РЕВЕРС КАНАЛА", "Reverse CH Dir": "РЕВЕРС КАНАЛА",
"Carrier LED": "СВЕТОДИОД НЕС", "Carrier LED": "СВЕТОДИОД НЕС",
"RSSI Refresh": "ОБНОВЛ RSSI", "RSSI Refresh": "ОБНОВЛ RSSI",
"APO": "АВТОВЫКЛ", "APO": "АВТОВЫКЛ",
"APO Timer": "ТАЙМЕР АВТОВЫК", "APO Timer": "ТАЙМЕР АВТОВЫК",
"FM RX Standby": "FM ПРИЁМ ОЖИД", "FM RX Standby": "FM ПРИЁМ ОЖИД",
"Initialization": "СБРОС", "Initialization": "СБРОС",
"Instruction": "ИНСТРУКЦИЯ", "Instruction": "ИНСТРУКЦИЯ",
"Version": "ВЕРСИЯ", "Version": "ВЕРСИЯ",
"Second PTT": "ВТОРОЙ PTT", "Second PTT": "ВТОРОЙ PTT",
"Side Key 1 S": "БОК КЛАВ 1 КОР", "Side Key 1 S": "БОК КЛАВ 1 КОР",
"Side Key 1 L": "БОК КЛАВ 1 ДЛ", "Side Key 1 L": "БОК КЛАВ 1 ДЛ",
"Side Key 2 S": "БОК КЛАВ 2 КОР", "Side Key 2 S": "БОК КЛАВ 2 КОР",
"Side Key 2 L": "БОК КЛАВ 2 ДЛ", "Side Key 2 L": "БОК КЛАВ 2 ДЛ",
"0 Press Long": "0 ДЛ НАЖАТИЕ", "0 Press Long": "0 ДЛ НАЖАТИЕ",
"1 Press Long": "1 ДЛ НАЖАТИЕ", "1 Press Long": "1 ДЛ НАЖАТИЕ",
"2 Press Long": "2 ДЛ НАЖАТИЕ", "2 Press Long": "2 ДЛ НАЖАТИЕ",
"3 Press Long": "3 ДЛ НАЖАТИЕ", "3 Press Long": "3 ДЛ НАЖАТИЕ",
"4 Press Long": "4 ДЛ НАЖАТИЕ", "4 Press Long": "4 ДЛ НАЖАТИЕ",
"5 Press Long": "5 ДЛ НАЖАТИЕ", "5 Press Long": "5 ДЛ НАЖАТИЕ",
"6 Press Long": "6 ДЛ НАЖАТИЕ", "6 Press Long": "6 ДЛ НАЖАТИЕ",
"7 Press Long": "7 ДЛ НАЖАТИЕ", "7 Press Long": "7 ДЛ НАЖАТИЕ",
"8 Press Long": "8 ДЛ НАЖАТИЕ", "8 Press Long": "8 ДЛ НАЖАТИЕ",
"9 Press Long": "9 ДЛ НАЖАТИЕ", "9 Press Long": "9 ДЛ НАЖАТИЕ",
"SQ Level": "УР ШУМОПОД", "SQ Level": "УР ШУМОПОД",
"TX Start Tone": "ТОН НАЧ TX", "TX Start Tone": "ТОН НАЧ TX",
"TX End Tone": "ТОН КОНЦА TX", "TX End Tone": "ТОН КОНЦА TX",
"Single Tone": "ОДИНОЧ ТОН", "Single Tone": "ОДИНОЧ ТОН",
"Tone Timer": "ТАЙМЕР ТОНА", "Tone Timer": "ТАЙМЕР ТОНА",
"MIC Gain": "УСИЛ МИКР", "MIC Gain": "УСИЛ МИКР",
"SPK Gain": "УСИЛ ДИНАМ", "SPK Gain": "УСИЛ ДИНАМ",
"Glitch TH": "ПОРОГ ПОМЕХ", "Glitch TH": "ПОРОГ ПОМЕХ",
"Detect Range": "ДИАПАЗ ДЕТЕКТ", "Detect Range": "ДИАПАЗ ДЕТЕКТ",
"Repeater Delay": "ЗАДЕРЖ РЕПИТ", "Repeater Delay": "ЗАДЕРЖ РЕПИТ",
"DTMF Delay": "ЗАДЕРЖ DTMF", "DTMF Delay": "ЗАДЕРЖ DTMF",
"DTMF Interval": "ИНТЕРВ DTMF", "DTMF Interval": "ИНТЕРВ DTMF",
"DTMF Duration": "ДЛИТ DTMF", "DTMF Duration": "ДЛИТ DTMF",
"DTMF Mode": "РЕЖИМ DTMF", "DTMF Mode": "РЕЖИМ DTMF",
"DTMF Select": "ВЫБОР DTMF", "DTMF Select": "ВЫБОР DTMF",
"DTMF Display": "ПОКАЗ DTMF", "DTMF Display": "ПОКАЗ DTMF",
"DTMF TX Gain": "УСИЛ TX DTMF", "DTMF TX Gain": "УСИЛ TX DTMF",
"DTMF RX TH": "ПОРОГ RX DTMF", "DTMF RX TH": "ПОРОГ RX DTMF",
"DTMF Control": "УПРАВЛ DTMF", "DTMF Control": "УПРАВЛ DTMF",
"VOX": "VOX", "VOX": "VOX",
"VOX Delay": "ЗАДЕРЖ VOX", "VOX Delay": "ЗАДЕРЖ VOX",
"VOX TH": "ПОРОГ VOX", "VOX TH": "ПОРОГ VOX",
"Short Tail": "КОР ХВОСТ", "Short Tail": "КОР ХВОСТ",
"Personal ID": "ЛИЧНЫЙ ID", "Personal ID": "ЛИЧНЫЙ ID",
"Call Tone": "ТОН ВЫЗОВА", "Call Tone": "ТОН ВЫЗОВА",
"Call End Tone": "ТОН КОНЦА ВЫЗ", "Call End Tone": "ТОН КОНЦА ВЫЗ",
"Group Hold": "УДЕРЖ ГРУППЫ", "Group Hold": "УДЕРЖ ГРУППЫ",
"Single Hold": "УДЕРЖ ЛИЧН", "Single Hold": "УДЕРЖ ЛИЧН",
"TX Denoise": "ШУМОПОД TX", "TX Denoise": "ШУМОПОД TX",
"RX Denoise": "ШУМОПОД RX", "RX Denoise": "ШУМОПОД RX",
"Contacts Set": "НАСТР КОНТАКТ", "Contacts Set": "НАСТР КОНТАКТ",
"TG List Set": "НАСТР СП ТГ", "TG List Set": "НАСТР СП ТГ",
"Encryption Set": "НАСТР ШИФР", "Encryption Set": "НАСТР ШИФР",
"Called Show": "ПОКАЗ ВЫЗЫВ", "Called Show": "ПОКАЗ ВЫЗЫВ",
"Send DTMF": "ПЕРЕД DTMF", "Send DTMF": "ПЕРЕД DTMF",
"Caller Keep": "УДЕРЖ ВЫЗЫВ", "Caller Keep": "УДЕРЖ ВЫЗЫВ",
"Call Log": "ЖУРНАЛ ВЫЗ", "Call Log": "ЖУРНАЛ ВЫЗ",
"Clear All Log": "ОЧИСТ ЖУРНАЛ", "Clear All Log": "ОЧИСТ ЖУРНАЛ",
"Address Book": "АДР КНИГА", "Address Book": "АДР КНИГА",
"DMR Or Analog": "DMR ИЛИ АНАЛОГ", "DMR Or Analog": "DMR ИЛИ АНАЛОГ",
"RX/TX Limit": "ЛИМИТ RX/TX", "RX/TX Limit": "ЛИМИТ RX/TX",
"CH Alias": "ИМЯ КАНАЛА", "CH Alias": "ИМЯ КАНАЛА",
"TX Power": "МОЩН TX", "TX Power": "МОЩН TX",
"Scan Add": "ДОБАВ В СКАН", "Scan Add": "ДОБАВ В СКАН",
"TOT": "ТАЙМАУТ TX", "TOT": "ТАЙМАУТ TX",
"Offset Freq": "СДВИГ ЧАСТ", "Offset Freq": "СДВИГ ЧАСТ",
"Set TX Freq": "ЗАДАТЬ ЧАСТ TX", "Set TX Freq": "ЗАДАТЬ ЧАСТ TX",
"CTC/DCS (A)": "CTC/DCS (A)", "CTC/DCS (A)": "CTC/DCS (A)",
"RX CTC/DCS (A)": "RX CTC/DCS (A)", "RX CTC/DCS (A)": "RX CTC/DCS (A)",
"TX CTC/DCS (A)": "TX CTC/DCS (A)", "TX CTC/DCS (A)": "TX CTC/DCS (A)",
"DCS Encrypt(A)": "ШИФР DCS (A)", "DCS Encrypt(A)": "ШИФР DCS (A)",
"Mute Code (A)": "КОД МУТ (A)", "Mute Code (A)": "КОД МУТ (A)",
"Band Width (A)": "ПОЛОСА (A)", "Band Width (A)": "ПОЛОСА (A)",
"Tail Tone (A)": "ХВОСТ ТОН (A)", "Tail Tone (A)": "ХВОСТ ТОН (A)",
"Scrambler (A)": "СКРЕМБЛ (A)", "Scrambler (A)": "СКРЕМБЛ (A)",
"Busy Lock (A)": "БЛОК ЗАНЯТ (A)", "Busy Lock (A)": "БЛОК ЗАНЯТ (A)",
"RX Demod (A)": "ДЕМОД RX (A)", "RX Demod (A)": "ДЕМОД RX (A)",
"DMR Mode (D)": "РЕЖИМ DMR (D)", "DMR Mode (D)": "РЕЖИМ DMR (D)",
"DMR Slot (D)": "СЛОТ DMR (D)", "DMR Slot (D)": "СЛОТ DMR (D)",
"Color Code (D)": "ЦВ.КОД (D)", "Color Code (D)": "ЦВ.КОД (D)",
"Contacts (D)": "КОНТАКТЫ (D)", "Contacts (D)": "КОНТАКТЫ (D)",
"TG List (D)": "СПИСОК ТГ (D)", "TG List (D)": "СПИСОК ТГ (D)",
"Encryption (D)": "ШИФР (D)", "Encryption (D)": "ШИФР (D)",
"TX Politely(D)": "ВЕЖЛ TX (D)", "TX Politely(D)": "ВЕЖЛ TX (D)",
"Promiscuous(D)": "ПРОМИСК (D)", "Promiscuous(D)": "ПРОМИСК (D)",
"Channel ID (D)": "ID КАНАЛА (D)", "Channel ID (D)": "ID КАНАЛА (D)",
"ID Select (D)": "ВЫБОР ID (D)", "ID Select (D)": "ВЫБОР ID (D)",
"New SMS": "НОВОЕ СМС", "New SMS": "НОВОЕ СМС",
"Inbox": "ВХОДЯЩИЕ", "Inbox": "ВХОДЯЩИЕ",
"Outbox": "ИСХОДЯЩИЕ", "Outbox": "ИСХОДЯЩИЕ",
"Drafts": "ЧЕРНОВИКИ", "Drafts": "ЧЕРНОВИКИ",
"Default SMS": "ШАБЛОН СМС", "Default SMS": "ШАБЛОН СМС",
"Clear All SMS": "ОЧИСТ ВСЕ СМС", "Clear All SMS": "ОЧИСТ ВСЕ СМС",
"SMS Format": "ФОРМАТ СМС", "SMS Format": "ФОРМАТ СМС",
"SMS Font": "ШРИФТ СМС", "SMS Font": "ШРИФТ СМС",
"SMS Prompt": "ОЗВУЧ СМС", "SMS Prompt": "ОЗВУЧ СМС",
"Contacts List": "СПИС КОНТАКТ", "Contacts List": "СПИС КОНТАКТ",
"Add Contact": "ДОБАВ КОНТАКТ", "Add Contact": "ДОБАВ КОНТАКТ",
"Edit Name": "ИЗМЕН ИМЯ", "Edit Name": "ИЗМЕН ИМЯ",
"Select CH": "ВЫБОР КАНАЛА", "Select CH": "ВЫБОР КАНАЛА",
"Edit Type": "ИЗМЕН ТИП", "Edit Type": "ИЗМЕН ТИП",
"Edit ID": "ИЗМЕН ID", "Edit ID": "ИЗМЕН ID",
"Save": "СОХР", "Save": "СОХР",
"Edit Member": "ИЗМЕН ЧЛЕНОВ", "Edit Member": "ИЗМЕН ЧЛЕНОВ",
"Clear": "ОЧИСТ", "Clear": "ОЧИСТ",
"Edit Key": "ИЗМЕН КЛЮЧ", "Edit Key": "ИЗМЕН КЛЮЧ",
"Send": "ПЕРЕД", "Send": "ПЕРЕД",
"Time :": "ВРЕМЯ :", "Time :": "ВРЕМЯ :",
"Type :": "ТИП :", "Type :": "ТИП :",
"ID :": "ID :", "ID :": "ID :",
"Name :": "ИМЯ :", "Name :": "ИМЯ :",
"Text :": "ТЕКСТ :", "Text :": "ТЕКСТ :",
"State:": "СТАТУС:", "State:": "СТАТУС:",
"Callsign :": "ПОЗЫВНОЙ :", "Callsign :": "ПОЗЫВНОЙ :",
"First Name :": "ИМЯ :", "First Name :": "ИМЯ :",
"Last Name :": "ФАМИЛИЯ :", "Last Name :": "ФАМИЛИЯ :",
"City :": "ГОРОД :", "City :": "ГОРОД :",
"State :": "РЕГИОН :", "State :": "РЕГИОН :",
"Country :": "СТРАНА :", "Country :": "СТРАНА :",
"Call In/Out :": "ВХ/ИСХ ВЫЗ :", "Call In/Out :": "ВХ/ИСХ ВЫЗ :",
"Call Type :": "ТИП ВЫЗОВА :", "Call Type :": "ТИП ВЫЗОВА :",
"ID :": "ID :", "ID :": "ID :",
"Talk time :": "ВРЕМЯ РАЗГ :", "Talk time :": "ВРЕМЯ РАЗГ :",
"Call start :": "НАЧ ВЫЗОВА :", "Call start :": "НАЧ ВЫЗОВА :",
"Delete": "УДАЛИТЬ", "Delete": "УДАЛИТЬ",
"Contacts": "КОНТАКТЫ", "Contacts": "КОНТАКТЫ",
"Dial No.": "НАБОР НОМЕРА", "Dial No.": "НАБОР НОМЕРА",
"Reply": "ОТВЕТ", "Reply": "ОТВЕТ",
"Forward": "ПЕРЕСЛАТЬ", "Forward": "ПЕРЕСЛАТЬ",
"Resend": "ПОВТОР ОТПР", "Resend": "ПОВТОР ОТПР",
"Off": "ВЫКЛ", "Off": "ВЫКЛ",
"On": "ВКЛ", "On": "ВКЛ",
"Edit": "ИЗМЕН", "Edit": "ИЗМЕН",
"Busy": "ЗАНЯТО", "Busy": "ЗАНЯТО",
"0.01K": "0.01K", "0.01K": "0.01K",
"0.02K": "0.02K", "0.02K": "0.02K",
"0.03K": "0.03K", "0.03K": "0.03K",
"0.05K": "0.05K", "0.05K": "0.05K",
"0.10K": "0.10K", "0.10K": "0.10K",
"0.25K": "0.25K", "0.25K": "0.25K",
"0.50K": "0.50K", "0.50K": "0.50K",
"1.25K": "1.25K", "1.25K": "1.25K",
"2.50K": "2.50K", "2.50K": "2.50K",
"5.00K": "5.00K", "5.00K": "5.00K",
"6.25K": "6.25K", "6.25K": "6.25K",
"8.33K": "8.33K", "8.33K": "8.33K",
"10.0K": "10.0K", "10.0K": "10.0K",
"12.5K": "12.5K", "12.5K": "12.5K",
"20.0K": "20.0K", "20.0K": "20.0K",
"25.0K": "25.0K", "25.0K": "25.0K",
"50.0K": "50.0K", "50.0K": "50.0K",
" 100K": " 100K", " 100K": " 100K",
" 500K": " 500K", " 500K": " 500K",
" 1M": " 1M", " 1M": " 1M",
" 5M": " 5M", " 5M": " 5M",
" 5 S": " 5 С", " 5 S": " 5 С",
" 10 S": " 10 С", " 10 S": " 10 С",
" 15 S": " 15 С", " 15 S": " 15 С",
" 30 S": " 30 С", " 30 S": " 30 С",
" 45 S": " 45 С", " 45 S": " 45 С",
" 60 S": " 60 С", " 60 S": " 60 С",
" 75 S": " 75 С", " 75 S": " 75 С",
" 90 S": " 90 С", " 90 S": " 90 С",
"105 S": "105 С", "105 S": "105 С",
"120 S": "120 С", "120 S": "120 С",
"135 S": "135 С", "135 S": "135 С",
"150 S": "150 С", "150 S": "150 С",
"165 S": "165 С", "165 S": "165 С",
"180 S": "180 С", "180 S": "180 С",
"195 S": "195 С", "195 S": "195 С",
"210 S": "210 С", "210 S": "210 С",
"225 S": "225 С", "225 S": "225 С",
"240 S": "240 С", "240 S": "240 С",
"255 S": "255 С", "255 S": "255 С",
"270 S": "270 С", "270 S": "270 С",
"285 S": "285 С", "285 S": "285 С",
"300 S": "300 С", "300 S": "300 С",
"315 S": "315 С", "315 S": "315 С",
"330 S": "330 С", "330 S": "330 С",
"345 S": "345 С", "345 S": "345 С",
"360 S": "360 С", "360 S": "360 С",
"375 S": "375 С", "375 S": "375 С",
"390 S": "390 С", "390 S": "390 С",
"405 S": "405 С", "405 S": "405 С",
"420 S": "420 С", "420 S": "420 С",
"435 S": "435 С", "435 S": "435 С",
"450 S": "450 С", "450 S": "450 С",
"465 S": "465 С", "465 S": "465 С",
"480 S": "480 С", "480 S": "480 С",
"495 S": "495 С", "495 S": "495 С",
"510 S": "510 С", "510 S": "510 С",
"525 S": "525 С", "525 S": "525 С",
"540 S": "540 С", "540 S": "540 С",
"555 S": "555 С", "555 S": "555 С",
"570 S": "570 С", "570 S": "570 С",
"585 S": "585 С", "585 S": "585 С",
"600 S": "600 С", "600 S": "600 С",
"Reverse Freq": "РЕВЕРС ЧАСТ", "Reverse Freq": "РЕВЕРС ЧАСТ",
"1 : 1": "1 : 1", "1 : 1": "1 : 1",
"1 : 2": "1 : 2", "1 : 2": "1 : 2",
"1 : 3": "1 : 3", "1 : 3": "1 : 3",
"CO": "CO", "CO": "CO",
"TO": "TO", "TO": "TO",
"SE": "SE", "SE": "SE",
"Up": "ВВЕРХ", "Up": "ВВЕРХ",
"Down": "ВНИЗ", "Down": "ВНИЗ",
"Original CH": "ИСХ КАНАЛ", "Original CH": "ИСХ КАНАЛ",
"Current CH": "ТЕК КАНАЛ", "Current CH": "ТЕК КАНАЛ",
"Local Alarm": "ЛОК ТРЕВОГА", "Local Alarm": "ЛОК ТРЕВОГА",
"Remote Alarm": "УДАЛ ТРЕВОГА", "Remote Alarm": "УДАЛ ТРЕВОГА",
"Local + Remote": "ЛОК + УДАЛ", "Local + Remote": "ЛОК + УДАЛ",
"Area A": "ЗОНА A", "Area A": "ЗОНА A",
"Main Area": "ОСН ЗОНА", "Main Area": "ОСН ЗОНА",
"Freq Mode": "РЕЖИМ ЧАСТ", "Freq Mode": "РЕЖИМ ЧАСТ",
"CH Mode": "РЕЖИМ КАНАЛА", "CH Mode": "РЕЖИМ КАНАЛА",
"Zone Mode": "РЕЖИМ ЗОНЫ", "Zone Mode": "РЕЖИМ ЗОНЫ",
"Show CH No.": "ПОКАЗ НОМ КАН", "Show CH No.": "ПОКАЗ НОМ КАН",
"Show Freq": "ПОКАЗ ЧАСТ", "Show Freq": "ПОКАЗ ЧАСТ",
"Show Alias": "ПОКАЗ ИМЯ", "Show Alias": "ПОКАЗ ИМЯ",
"Ensure": "ПОДТВЕРД", "Ensure": "ПОДТВЕРД",
"Cancel": "ОТМЕНА", "Cancel": "ОТМЕНА",
"Hytera": "HYTERA", "Hytera": "HYTERA",
"Motorola": "MOTOROLA", "Motorola": "MOTOROLA",
"Unicode": "UNICODE", "Unicode": "UNICODE",
"GBK": "GBK", "GBK": "GBK",
"None": "НЕТ", "None": "НЕТ",
"Monitor(Analog)": "МОНИТОР АНАЛОГ", "Monitor(Analog)": "МОНИТОР АНАЛОГ",
"H/L Power": "МОЩН ВЫС/НИЗ", "H/L Power": "МОЩН ВЫС/НИЗ",
"Scanning": "СКАНИРОВ", "Scanning": "СКАНИРОВ",
"Backlight On-off": "ПОДСВ ВКЛ/ВЫКЛ", "Backlight On-off": "ПОДСВ ВКЛ/ВЫКЛ",
"Roger Beep": "БИП ОКОНЧ", "Roger Beep": "БИП ОКОНЧ",
"FM Radio": "FM РАДИО", "FM Radio": "FM РАДИО",
"Alarm": "ТРЕВОГА", "Alarm": "ТРЕВОГА",
"Freq Detect": "ДЕТЕКТ ЧАСТ", "Freq Detect": "ДЕТЕКТ ЧАСТ",
"CTC/DCS Scan": "СКАН CTC/DCS", "CTC/DCS Scan": "СКАН CTC/DCS",
"Send Single Tone": "ПЕРЕД ОДИН ТОН", "Send Single Tone": "ПЕРЕД ОДИН ТОН",
"Status Query": "ЗАПРОС СТАТУСА", "Status Query": "ЗАПРОС СТАТУСА",
"Remote Monitor": "УДАЛ МОНИТОР", "Remote Monitor": "УДАЛ МОНИТОР",
"Remote Stun": "УДАЛ БЛОК", "Remote Stun": "УДАЛ БЛОК",
"Remote Kill": "УДАЛ ОТКЛ", "Remote Kill": "УДАЛ ОТКЛ",
"Remote Wake Up": "УДАЛ ПРОБУЖД", "Remote Wake Up": "УДАЛ ПРОБУЖД",
"Online Check": "ПРОВ НА СВЯЗИ", "Online Check": "ПРОВ НА СВЯЗИ",
"RX AM/FM Switch": "ПЕРЕКЛ AM/FM RX", "RX AM/FM Switch": "ПЕРЕКЛ AM/FM RX",
"Analog Spectrum": "АНАЛОГ СПЕКТР", "Analog Spectrum": "АНАЛОГ СПЕКТР",
"SQ": "ШУМОПОД", "SQ": "ШУМОПОД",
"DA Switch": "ПЕРЕКЛ ЦИФР/АН", "DA Switch": "ПЕРЕКЛ ЦИФР/АН",
"NOAA Mode": "РЕЖИМ NOAA", "NOAA Mode": "РЕЖИМ NOAA",
"Jump To SMS Menu": "ПЕРЕХ В МЕНЮ СМС", "Jump To SMS Menu": "ПЕРЕХ В МЕНЮ СМС",
"Analog CH VOX": "VOX АНАЛОГ КАН", "Analog CH VOX": "VOX АНАЛОГ КАН",
"Zone Select": "ВЫБОР ЗОНЫ", "Zone Select": "ВЫБОР ЗОНЫ",
"Promiscuous Mode": "РЕЖИМ ПРОМИСК", "Promiscuous Mode": "РЕЖИМ ПРОМИСК",
"Dual Slot On-off": "2 СЛОТА ВКЛ/ВЫК", "Dual Slot On-off": "2 СЛОТА ВКЛ/ВЫК",
"Time Slot Switch": "ПЕРЕКЛ СЛОТА", "Time Slot Switch": "ПЕРЕКЛ СЛОТА",
"Color Code SW": "ПЕРЕКЛ ЦВ.КОДА", "Color Code SW": "ПЕРЕКЛ ЦВ.КОДА",
"DMR Encrypt Off": "ШИФР DMR ВЫКЛ", "DMR Encrypt Off": "ШИФР DMR ВЫКЛ",
"Jump To RX List": "ПЕРЕХ К СП RX", "Jump To RX List": "ПЕРЕХ К СП RX",
"Jump To Contact": "ПЕРЕХ К КОНТ", "Jump To Contact": "ПЕРЕХ К КОНТ",
"Jump To DTMF Sel": "ПЕРЕХ К DTMF", "Jump To DTMF Sel": "ПЕРЕХ К DTMF",
"Roger Beep 1": "БИП ОКОНЧ 1", "Roger Beep 1": "БИП ОКОНЧ 1",
"Roger Beep 2": "БИП ОКОНЧ 2", "Roger Beep 2": "БИП ОКОНЧ 2",
"Send Radio Name": "ПЕРЕД ИМЯ РАЦИИ", "Send Radio Name": "ПЕРЕД ИМЯ РАЦИИ",
"18-64 MHz": "18-64 МГЦ", "18-64 MHz": "18-64 МГЦ",
"64-136 MHz": "64-136 МГЦ", "64-136 MHz": "64-136 МГЦ",
"136-174 MHz": "136-174 МГЦ", "136-174 MHz": "136-174 МГЦ",
"174-240 MHz": "174-240 МГЦ", "174-240 MHz": "174-240 МГЦ",
"240-320 MHz": "240-320 МГЦ", "240-320 MHz": "240-320 МГЦ",
"320-400 MHz": "320-400 МГЦ", "320-400 MHz": "320-400 МГЦ",
"400-480 MHz": "400-480 МГЦ", "400-480 MHz": "400-480 МГЦ",
"480-560 MHz": "480-560 МГЦ", "480-560 MHz": "480-560 МГЦ",
"560-620 MHz": "560-620 МГЦ", "560-620 MHz": "560-620 МГЦ",
"840-920 MHz": "840-920 МГЦ", "840-920 MHz": "840-920 МГЦ",
"920-1000 MHz": "920-1000 МГЦ", "920-1000 MHz": "920-1000 МГЦ",
"0 ms": "0 МС", "0 ms": "0 МС",
"100 ms": "100 МС", "100 ms": "100 МС",
"200 ms": "200 МС", "200 ms": "200 МС",
"300 ms": "300 МС", "300 ms": "300 МС",
"400 ms": "400 МС", "400 ms": "400 МС",
"500 ms": "500 МС", "500 ms": "500 МС",
"600 ms": "600 МС", "600 ms": "600 МС",
"700 ms": "700 МС", "700 ms": "700 МС",
"800 ms": "800 МС", "800 ms": "800 МС",
"900 ms": "900 МС", "900 ms": "900 МС",
"1000 ms": "1000 МС", "1000 ms": "1000 МС",
"1100 ms": "1100 МС", "1100 ms": "1100 МС",
"1200 ms": "1200 МС", "1200 ms": "1200 МС",
"1300 ms": "1300 МС", "1300 ms": "1300 МС",
"1400 ms": "1400 МС", "1400 ms": "1400 МС",
"1500 ms": "1500 МС", "1500 ms": "1500 МС",
"1600 ms": "1600 МС", "1600 ms": "1600 МС",
"1700 ms": "1700 МС", "1700 ms": "1700 МС",
"1800 ms": "1800 МС", "1800 ms": "1800 МС",
"1900 ms": "1900 МС", "1900 ms": "1900 МС",
"2000 ms": "2000 МС", "2000 ms": "2000 МС",
"30 ms": "30 МС", "30 ms": "30 МС",
"40 ms": "40 МС", "40 ms": "40 МС",
"50 ms": "50 МС", "50 ms": "50 МС",
"60 ms": "60 МС", "60 ms": "60 МС",
"70 ms": "70 МС", "70 ms": "70 МС",
"80 ms": "80 МС", "80 ms": "80 МС",
"90 ms": "90 МС", "90 ms": "90 МС",
"110 ms": "110 МС", "110 ms": "110 МС",
"120 ms": "120 МС", "120 ms": "120 МС",
"130 ms": "130 МС", "130 ms": "130 МС",
"140 ms": "140 МС", "140 ms": "140 МС",
"150 ms": "150 МС", "150 ms": "150 МС",
"160 ms": "160 МС", "160 ms": "160 МС",
"170 ms": "170 МС", "170 ms": "170 МС",
"180 ms": "180 МС", "180 ms": "180 МС",
"190 ms": "190 МС", "190 ms": "190 МС",
"TX Start": "НАЧАЛО TX", "TX Start": "НАЧАЛО TX",
"TX End": "КОНЕЦ TX", "TX End": "КОНЕЦ TX",
"TX Start & End": "НАЧ И КОНЕЦ TX", "TX Start & End": "НАЧ И КОНЕЦ TX",
"DTMF-01": "DTMF-01", "DTMF-01": "DTMF-01",
"DTMF-02": "DTMF-02", "DTMF-02": "DTMF-02",
"DTMF-03": "DTMF-03", "DTMF-03": "DTMF-03",
"DTMF-04": "DTMF-04", "DTMF-04": "DTMF-04",
"DTMF-05": "DTMF-05", "DTMF-05": "DTMF-05",
"DTMF-06": "DTMF-06", "DTMF-06": "DTMF-06",
"DTMF-07": "DTMF-07", "DTMF-07": "DTMF-07",
"DTMF-08": "DTMF-08", "DTMF-08": "DTMF-08",
"DTMF-09": "DTMF-09", "DTMF-09": "DTMF-09",
"DTMF-10": "DTMF-10", "DTMF-10": "DTMF-10",
"DTMF-11": "DTMF-11", "DTMF-11": "DTMF-11",
"DTMF-12": "DTMF-12", "DTMF-12": "DTMF-12",
"DTMF-13": "DTMF-13", "DTMF-13": "DTMF-13",
"DTMF-14": "DTMF-14", "DTMF-14": "DTMF-14",
"DTMF-15": "DTMF-15", "DTMF-15": "DTMF-15",
"DTMF-16": "DTMF-16", "DTMF-16": "DTMF-16",
"Individual": "ЛИЧНЫЙ", "Individual": "ЛИЧНЫЙ",
"Group Call": "ГРУПП ВЫЗОВ", "Group Call": "ГРУПП ВЫЗОВ",
"ARC": "ARC", "ARC": "ARC",
"AES 128": "AES 128", "AES 128": "AES 128",
"AES 256": "AES 256", "AES 256": "AES 256",
"Standard": "СТАНДАРТ", "Standard": "СТАНДАРТ",
"Encrypt 1": "ШИФР 1", "Encrypt 1": "ШИФР 1",
"Encrypt 2": "ШИФР 2", "Encrypt 2": "ШИФР 2",
"Encrypt 3": "ШИФР 3", "Encrypt 3": "ШИФР 3",
"Mute Code": "КОД МУТ", "Mute Code": "КОД МУТ",
"Wide": "ШИР", "Wide": "ШИР",
"Narrow": "УЗК", "Narrow": "УЗК",
" 1": " 1", " 1": " 1",
" 2": " 2", " 2": " 2",
" 3": " 3", " 3": " 3",
" 4": " 4", " 4": " 4",
" 5": " 5", " 5": " 5",
" 6": " 6", " 6": " 6",
" 7": " 7", " 7": " 7",
" 8": " 8", " 8": " 8",
"Carrier Match": "ПО НЕСУЩЕЙ", "Carrier Match": "ПО НЕСУЩЕЙ",
"CTC/DCS Match": "ПО CTC/DCS", "CTC/DCS Match": "ПО CTC/DCS",
"Low": "НИЗ", "Low": "НИЗ",
"High": "ВЫС", "High": "ВЫС",
"Add": "ДОБАВ", "Add": "ДОБАВ",
"Remove": "УДАЛИТЬ", "Remove": "УДАЛИТЬ",
"+": "+", "+": "+",
"-": "-", "-": "-",
"Dual Slot Off": "2 СЛОТА ВЫКЛ", "Dual Slot Off": "2 СЛОТА ВЫКЛ",
"Dual Slot On": "2 СЛОТА ВКЛ", "Dual Slot On": "2 СЛОТА ВКЛ",
"Slot 1": "СЛОТ 1", "Slot 1": "СЛОТ 1",
"Slot 2": "СЛОТ 2", "Slot 2": "СЛОТ 2",
"Impolite": "НЕВЕЖЛИВО", "Impolite": "НЕВЕЖЛИВО",
"CC Match": "ПО ЦВ.КОДУ", "CC Match": "ПО ЦВ.КОДУ",
"6 digits": "6 ЦИФР", "6 digits": "6 ЦИФР",
"8 digits": "8 ЦИФР", "8 digits": "8 ЦИФР",
"Show Caller Info": "ПОКАЗ ВЫЗЫВ", "Show Caller Info": "ПОКАЗ ВЫЗЫВ",
"Show Called Info": "ПОКАЗ ВЫЗЫВАЕМ", "Show Called Info": "ПОКАЗ ВЫЗЫВАЕМ",
"FM": "FM", "FM": "FM",
"AM": "AM", "AM": "AM",
"SSB": "SSB", "SSB": "SSB",
"Tail Tone Off": "ХВОСТ ТОН ВЫКЛ", "Tail Tone Off": "ХВОСТ ТОН ВЫКЛ",
"55Hz No Shift": "55Hz БЕЗ СДВ", "55Hz No Shift": "55Hz БЕЗ СДВ",
"120¡ã Shift": "120° СДВИГ", "120¡ã Shift": "120° СДВИГ",
"180¡ã Shift": "180° СДВИГ", "180¡ã Shift": "180° СДВИГ",
"240¡ã Shift": "240° СДВИГ", "240¡ã Shift": "240° СДВИГ",
"Radio ID": "ID РАЦИИ", "Radio ID": "ID РАЦИИ",
"Channel ID": "ID КАНАЛА", "Channel ID": "ID КАНАЛА",
"No": "НЕТ", "No": "НЕТ",
"Yes": "ДА", "Yes": "ДА",
"Latest Program": "ПОСЛ ПРОШИВКА", "Latest Program": "ПОСЛ ПРОШИВКА",
"Init Config": "СБРОС НАСТР", "Init Config": "СБРОС НАСТР",
"Init All": "СБРОС ВСЕГО", "Init All": "СБРОС ВСЕГО",
"Latest Tuning": "ПОСЛ КАЛИБР", "Latest Tuning": "ПОСЛ КАЛИБР",
"Init Tune Data": "СБРОС КАЛИБР", "Init Tune Data": "СБРОС КАЛИБР",
"DMR": "DMR", "DMR": "DMR",
"Analog": "АНАЛОГ", "Analog": "АНАЛОГ",
"RX + TX": "RX + TX", "RX + TX": "RX + TX",
"Only RX": "ТОЛЬКО RX", "Only RX": "ТОЛЬКО RX",
"Only TX": "ТОЛЬКО TX", "Only TX": "ТОЛЬКО TX",
"S0 S1 S2 S3 S4 S": "S0 S1 S2 S3 S4 S", "S0 S1 S2 S3 S4 S": "S0 S1 S2 S3 S4 S",
"5 S6 S7 S8 S9 +1": "5 S6 S7 S8 S9 +1", "5 S6 S7 S8 S9 +1": "5 S6 S7 S8 S9 +1",
"0+20+30+40+50+": "0+20+30+40+50+" "0+20+30+40+50+": "0+20+30+40+50+"
} }
Разница между файлами не показана из-за своего большого размера Загрузить разницу
+7 -7
Просмотреть файл
@@ -1,8 +1,8 @@
[ [
"Greece s", "Greece s",
"United States n", "United States n",
"United States o", "United States o",
"United States p", "United States p",
"United States q", "United States q",
"United States r" "United States r"
] ]
+318 -318
Просмотреть файл
@@ -1,319 +1,319 @@
{ {
"+pGpGpGpGuF": "+pGpGpGpGuF", "+pGpGpGpGuF": "+pGpGpGpGuF",
" DTMF Remote Kill": " DTMF УДАЛ ОТКЛ", " DTMF Remote Kill": " DTMF УДАЛ ОТКЛ",
"DMR Remote Kill ": "DMR УДАЛ ОТКЛ", "DMR Remote Kill ": "DMR УДАЛ ОТКЛ",
" Copy(Transmit) ": " КОПИЯ(ПЕРЕДАЧА)", " Copy(Transmit) ": " КОПИЯ(ПЕРЕДАЧА)",
" Copy(Recive) ": " КОПИЯ(ПРИЁМ)", " Copy(Recive) ": " КОПИЯ(ПРИЁМ)",
"Receving": "ПРИЁМ", "Receving": "ПРИЁМ",
"@Backing up... ": "@РЕЗ.КОПИЯ...", "@Backing up... ": "@РЕЗ.КОПИЯ...",
"Please wait ": "ПОДОЖДИТЕ", "Please wait ": "ПОДОЖДИТЕ",
" ERROR": " ОШИБ", " ERROR": " ОШИБ",
" -000DBM": " -000DBM", " -000DBM": " -000DBM",
"Password : ": "ПАРОЛЬ :", "Password : ": "ПАРОЛЬ :",
"Emergency Alarm ": "ТРЕВОГА", "Emergency Alarm ": "ТРЕВОГА",
" Being Searched ": " ПОИСК", " Being Searched ": " ПОИСК",
" Radio Wake Up ": " БУДИЛЬНИК", " Radio Wake Up ": " БУДИЛЬНИК",
"DMR Remote Stun ": "DMR УДАЛ БЛОК", "DMR Remote Stun ": "DMR УДАЛ БЛОК",
" call stopped. ": " ВЫЗОВ ОСТАНОВЛЕН", " call stopped. ": " ВЫЗОВ ОСТАНОВЛЕН",
"Call type error,": "ОШИБКА ТИПА ВЫЗ", "Call type error,": "ОШИБКА ТИПА ВЫЗ",
" Dialing ": " ВЫЗОВ", " Dialing ": " ВЫЗОВ",
"Callback ": "ОБР.ВЫЗОВ", "Callback ": "ОБР.ВЫЗОВ",
"Addressbook ": "АДР.КНИГА", "Addressbook ": "АДР.КНИГА",
"All Call ": "ОБЩ.ВЫЗОВ", "All Call ": "ОБЩ.ВЫЗОВ",
"Contact Error! ": "ОШИБКА КОНТАКТА", "Contact Error! ": "ОШИБКА КОНТАКТА",
"No Analog CH ": "НЕТ АНАЛОГ КАН", "No Analog CH ": "НЕТ АНАЛОГ КАН",
" Group Call": " ГРУПП.ВЫЗ", " Group Call": " ГРУПП.ВЫЗ",
" All Call ": " ОБЩ.ВЫЗОВ", " All Call ": " ОБЩ.ВЫЗОВ",
" Individual": " ЛИЧНЫЙ", " Individual": " ЛИЧНЫЙ",
" Address Book ": " АДР.КНИГА", " Address Book ": " АДР.КНИГА",
" CTC/DCS (A)": " CTC/DCS (A)", " CTC/DCS (A)": " CTC/DCS (A)",
" RX CTC/DCS (A)": " RX CTC/DCS (A)", " RX CTC/DCS (A)": " RX CTC/DCS (A)",
"TX CTC/DCS (A)": "TX CTC/DCS (A)", "TX CTC/DCS (A)": "TX CTC/DCS (A)",
" * Switch Type ": " * СМЕНА ТИПА", " * Switch Type ": " * СМЕНА ТИПА",
"No Call Records ": "НЕТ ЗАПИСЕЙ ВЫЗ", "No Call Records ": "НЕТ ЗАПИСЕЙ ВЫЗ",
" Call Log ": " ЖУРНАЛ", " Call Log ": " ЖУРНАЛ",
" Remote CTC/DCS ": " УДАЛ CTC/DCS", " Remote CTC/DCS ": " УДАЛ CTC/DCS",
"Group Call": "ГРУПП.ВЫЗ", "Group Call": "ГРУПП.ВЫЗ",
"All Call ": "ОБЩ.ВЫЗОВ", "All Call ": "ОБЩ.ВЫЗОВ",
"All Call ": "ОБЩ.ВЫЗОВ", "All Call ": "ОБЩ.ВЫЗОВ",
"Being Monitored ": "ПРОСЛУШКА", "Being Monitored ": "ПРОСЛУШКА",
" Drafts ": " ЧЕРНОВИКИ", " Drafts ": " ЧЕРНОВИКИ",
" Edit Key ": " ПРАВКА КЛЮЧА", " Edit Key ": " ПРАВКА КЛЮЧА",
"Station Name : ": "ИМЯ СТАНЦИИ :", "Station Name : ": "ИМЯ СТАНЦИИ :",
" Offset Set ": " НАСТР СДВИГА", " Offset Set ": " НАСТР СДВИГА",
"Switch": "СМЕНА", "Switch": "СМЕНА",
" Edit Member ": " ПРАВКА УЧАСТНИК", " Edit Member ": " ПРАВКА УЧАСТНИК",
" Inbox ": " ВХОДЯЩИЕ", " Inbox ": " ВХОДЯЩИЕ",
" Mute Code (A)": " КОД МУТ (A)", " Mute Code (A)": " КОД МУТ (A)",
" New SMS ": " НОВОЕ СМС", " New SMS ": " НОВОЕ СМС",
" Outbox ": " ИСХОДЯЩИЕ", " Outbox ": " ИСХОДЯЩИЕ",
" Default SMS ": " ШАБЛОН СМС", " Default SMS ": " ШАБЛОН СМС",
"Inbox Empty ": "ВХОДЯЩИЕ ПУСТО", "Inbox Empty ": "ВХОДЯЩИЕ ПУСТО",
" Outbox Empty ": " ИСХОДЯЩИЕ ПУСТО", " Outbox Empty ": " ИСХОДЯЩИЕ ПУСТО",
"Outbox ": "ИСХОДЯЩИЕ", "Outbox ": "ИСХОДЯЩИЕ",
" Drafts Empty ": " ЧЕРНОВИКИ ПУСТО", " Drafts Empty ": " ЧЕРНОВИКИ ПУСТО",
"Drafts ": "ЧЕРНОВИКИ", "Drafts ": "ЧЕРНОВИКИ",
" Empty ": " ПУСТО", " Empty ": " ПУСТО",
"Default SMS ": "ШАБЛОН СМС", "Default SMS ": "ШАБЛОН СМС",
" Dial No. ": " НАБОР НОМЕРА", " Dial No. ": " НАБОР НОМЕРА",
"Individual ": "ЛИЧНЫЙ", "Individual ": "ЛИЧНЫЙ",
"Group Call ": "ГРУПП.ВЫЗОВ", "Group Call ": "ГРУПП.ВЫЗОВ",
"VER :RT-4D V3.25": "VER :RT-4D V3.25", "VER :RT-4D V3.25": "VER :RT-4D V3.25",
" DATE:2026-02-05 ": " DATE:2026-02-05 ", " DATE:2026-02-05 ": " DATE:2026-02-05 ",
"DMR : ": "DMR :", "DMR : ": "DMR :",
" IC : ": " IC :", " IC : ": " IC :",
"Unknown ": "НЕИЗВЕСТНО", "Unknown ": "НЕИЗВЕСТНО",
"BATT:0.0V ": "BATT:0.0V", "BATT:0.0V ": "BATT:0.0V",
" Version ": " ВЕРСИЯ", " Version ": " ВЕРСИЯ",
"Power: High ": "МОЩН: ВЫС", "Power: High ": "МОЩН: ВЫС",
"Power: Low ": "МОЩН: НИЗ", "Power: Low ": "МОЩН: НИЗ",
" Dual Standby:Off": " ДВОЙН ОЖИД:ВЫКЛ", " Dual Standby:Off": " ДВОЙН ОЖИД:ВЫКЛ",
"Dual Standby:On ": "ДВОЙН ОЖИД:ВКЛ", "Dual Standby:On ": "ДВОЙН ОЖИД:ВКЛ",
"TX Priority:Edit": "ПРИОР TX:ПРАВКА", "TX Priority:Edit": "ПРИОР TX:ПРАВКА",
"TX Priority:Busy": "ПРИОР TX:ЗАНЯТО", "TX Priority:Busy": "ПРИОР TX:ЗАНЯТО",
" Scanning ": " СКАНИРОВ", " Scanning ": " СКАНИРОВ",
"TX End Tone: Off": "ТОН КОНЦА TX:ВЫК", "TX End Tone: Off": "ТОН КОНЦА TX:ВЫК",
"TX End Tone: 01 ": "ТОН КОНЦА TX: 01", "TX End Tone: 01 ": "ТОН КОНЦА TX: 01",
"TX End Tone: 02 ": "ТОН КОНЦА TX: 02", "TX End Tone: 02 ": "ТОН КОНЦА TX: 02",
"TX End Tone: MDC": "ТОН КОНЦА TX:MDC", "TX End Tone: MDC": "ТОН КОНЦА TX:MDC",
" Talkaround : Off": " ПРЯМОЙ : ВЫКЛ", " Talkaround : Off": " ПРЯМОЙ : ВЫКЛ",
"Talkaround ": "ПРЯМОЙ", "Talkaround ": "ПРЯМОЙ",
"Reverse Freq ": "РЕВЕРС ЧАСТ", "Reverse Freq ": "РЕВЕРС ЧАСТ",
" Emergency ": " ТРЕВОГА", " Emergency ": " ТРЕВОГА",
" No Analog CH ": " НЕТ АНАЛОГ КАН", " No Analog CH ": " НЕТ АНАЛОГ КАН",
"@Send Single Tone": "@ПЕРЕД ОДИН ТОН", "@Send Single Tone": "@ПЕРЕД ОДИН ТОН",
"Radio Online ": "РАЦИЯ НА СВЯЗИ", "Radio Online ": "РАЦИЯ НА СВЯЗИ",
"Radio Offline ": "РАЦИЯ НЕ НА СВЯЗ", "Radio Offline ": "РАЦИЯ НЕ НА СВЯЗ",
"Check Failed ": "ПРОВЕРКА СБОЙ", "Check Failed ": "ПРОВЕРКА СБОЙ",
" Show Caller Info": " ПОКАЗ ВЫЗЫВ", " Show Caller Info": " ПОКАЗ ВЫЗЫВ",
"Called Show: ": "ПОКАЗ ВЫЗЫВ:", "Called Show: ": "ПОКАЗ ВЫЗЫВ:",
"Show Called Info": "ПОКАЗ ВЫЗЫВАЕМ", "Show Called Info": "ПОКАЗ ВЫЗЫВАЕМ",
" RX Mode : AM ": " РЕЖИМ RX : AM", " RX Mode : AM ": " РЕЖИМ RX : AM",
"RX Mode : SSB ": "РЕЖИМ RX : SSB", "RX Mode : SSB ": "РЕЖИМ RX : SSB",
"RX Mode : FM ": "РЕЖИМ RX : FM", "RX Mode : FM ": "РЕЖИМ RX : FM",
" Analog Mode ": " АНАЛОГ РЕЖИМ", " Analog Mode ": " АНАЛОГ РЕЖИМ",
"Digital Mode ": "ЦИФР РЕЖИМ", "Digital Mode ": "ЦИФР РЕЖИМ",
" Analog VOX : On ": " АНАЛОГ VOX : ВКЛ", " Analog VOX : On ": " АНАЛОГ VOX : ВКЛ",
"Analog VOX : Off": "АНАЛОГ VOX :ВЫКЛ", "Analog VOX : Off": "АНАЛОГ VOX :ВЫКЛ",
"Promiscuous: On ": "ПРОМИСК: ВКЛ", "Promiscuous: On ": "ПРОМИСК: ВКЛ",
"Promiscuous: Off": "ПРОМИСК: ВЫКЛ", "Promiscuous: Off": "ПРОМИСК: ВЫКЛ",
"Dual Slot : Off ": "2 СЛОТА : ВЫКЛ", "Dual Slot : Off ": "2 СЛОТА : ВЫКЛ",
"Dual Slot : On ": "2 СЛОТА : ВКЛ", "Dual Slot : On ": "2 СЛОТА : ВКЛ",
"g oIHx": "g oIHx", "g oIHx": "g oIHx",
" DMR Time Slot: 1": " DMR СЛОТ: 1", " DMR Time Slot: 1": " DMR СЛОТ: 1",
"DMR Time Slot: 2": "DMR СЛОТ: 2", "DMR Time Slot: 2": "DMR СЛОТ: 2",
" DMR Encrypt: Off": " ШИФР DMR: ВЫКЛ", " DMR Encrypt: Off": " ШИФР DMR: ВЫКЛ",
"is out of range ": "ВНЕ ДИАПАЗОНА", "is out of range ": "ВНЕ ДИАПАЗОНА",
" Resulting freq ": " ИТОГ ЧАСТОТА", " Resulting freq ": " ИТОГ ЧАСТОТА",
" Cannot be set! ": " НЕЛЬЗЯ ЗАДАТЬ!", " Cannot be set! ": " НЕЛЬЗЯ ЗАДАТЬ!",
"Single display ": "ОДИН ДИСПЛЕЙ", "Single display ": "ОДИН ДИСПЛЕЙ",
"ID Out Of Range ": "ID ВНЕ ДИАПАЗ", "ID Out Of Range ": "ID ВНЕ ДИАПАЗ",
" Clear All SMS ": " ОЧИСТ ВСЕ СМС", " Clear All SMS ": " ОЧИСТ ВСЕ СМС",
" Contacts Full! ": " КОНТАКТЫ ПОЛНЫ!", " Contacts Full! ": " КОНТАКТЫ ПОЛНЫ!",
"Type Error! ": "ОШИБКА ТИПА!", "Type Error! ": "ОШИБКА ТИПА!",
"Saving Data.....": "СОХР ДАННЫХ.....", "Saving Data.....": "СОХР ДАННЫХ.....",
" Clearing Data...": " ОЧИСТ ДАННЫХ...", " Clearing Data...": " ОЧИСТ ДАННЫХ...",
"g cIHx": "g cIHx", "g cIHx": "g cIHx",
"Starting up... ": "ЗАПУСК...", "Starting up... ": "ЗАПУСК...",
"Please wait ": "ПОДОЖДИТЕ", "Please wait ": "ПОДОЖДИТЕ",
"CH MODE ": "РЕЖИМ КАНАЛА", "CH MODE ": "РЕЖИМ КАНАЛА",
"VFO MODE ": "VFO РЕЖИМ", "VFO MODE ": "VFO РЕЖИМ",
" VFO MODE-A ": " VFO РЕЖИМ-A", " VFO MODE-A ": " VFO РЕЖИМ-A",
" VFO MODE-B ": " VFO РЕЖИМ-B", " VFO MODE-B ": " VFO РЕЖИМ-B",
"All Call": "ОБЩ.ВЫЗ", "All Call": "ОБЩ.ВЫЗ",
"Recovering... ": "ВОССТАНОВЛ...", "Recovering... ": "ВОССТАНОВЛ...",
" CH----- ": " CH-----", " CH----- ": " CH-----",
"Update DMR Chip ": "ОБНОВЛ DMR ЧИП", "Update DMR Chip ": "ОБНОВЛ DMR ЧИП",
"Please Wait... ": "ПОДОЖДИТЕ...", "Please Wait... ": "ПОДОЖДИТЕ...",
"Menu ": "МЕНЮ", "Menu ": "МЕНЮ",
"Basic Set ": "ОСН. НАСТР", "Basic Set ": "ОСН. НАСТР",
"Key Define ": "НАЗНАЧ КЛАВ", "Key Define ": "НАЗНАЧ КЛАВ",
"Analog Set ": "АНАЛОГ НАСТР", "Analog Set ": "АНАЛОГ НАСТР",
"Digital Set ": "ЦИФР НАСТР", "Digital Set ": "ЦИФР НАСТР",
"Channel Set ": "НАСТР КАНАЛА", "Channel Set ": "НАСТР КАНАЛА",
" Zone Set ": " НАСТР ЗОНЫ", " Zone Set ": " НАСТР ЗОНЫ",
"Message ": "СМС", "Message ": "СМС",
"Device Name ": "ИМЯ РАЦИИ", "Device Name ": "ИМЯ РАЦИИ",
"Voice Prompt ": "ОЗВУЧ", "Voice Prompt ": "ОЗВУЧ",
"Key Beep ": "БИП КЛАВ", "Key Beep ": "БИП КЛАВ",
"Lock Timer ": "ТАЙМЕР БЛОК", "Lock Timer ": "ТАЙМЕР БЛОК",
"Backlight ": "ПОДСВЕТКА", "Backlight ": "ПОДСВЕТКА",
"Light Timer ": "ТАЙМЕР СВЕТА", "Light Timer ": "ТАЙМЕР СВЕТА",
"Brightness ": "ЯРКОСТЬ", "Brightness ": "ЯРКОСТЬ",
"Menu Exit ": "ВЫХОД ИЗ МЕНЮ", "Menu Exit ": "ВЫХОД ИЗ МЕНЮ",
"Dual Standby ": "ДВОЙН ОЖИД", "Dual Standby ": "ДВОЙН ОЖИД",
"TX Priority ": "ПРИОР TX", "TX Priority ": "ПРИОР TX",
"Freq Step ": "ШАГ ЧАСТ", "Freq Step ": "ШАГ ЧАСТ",
"Save Mode ": "РЕЖИМ ЭКОН", "Save Mode ": "РЕЖИМ ЭКОН",
"Scan Mode ": "РЕЖИМ СКАН", "Scan Mode ": "РЕЖИМ СКАН",
"Scan Direction ": "НАПРАВ СКАН", "Scan Direction ": "НАПРАВ СКАН",
"Scan Dwell ": "ЗАДЕРЖ СКАН", "Scan Dwell ": "ЗАДЕРЖ СКАН",
" Scan Interval ": " ИНТЕРВ СКАН", " Scan Interval ": " ИНТЕРВ СКАН",
"Scan Return ": "ВОЗВРАТ СКАН", "Scan Return ": "ВОЗВРАТ СКАН",
"Scan Start ": "НАЧАЛО СКАН", "Scan Start ": "НАЧАЛО СКАН",
"Scan End ": "КОНЕЦ СКАН", "Scan End ": "КОНЕЦ СКАН",
"Alarm Type ": "ТИП ТРЕВОГИ", "Alarm Type ": "ТИП ТРЕВОГИ",
"Main PTT TX ": "ОСН PTT TX", "Main PTT TX ": "ОСН PTT TX",
"Area A Mode ": "РЕЖИМ ЗОНЫ A", "Area A Mode ": "РЕЖИМ ЗОНЫ A",
"Area A Show ": "ПОКАЗ ЗОНЫ A", "Area A Show ": "ПОКАЗ ЗОНЫ A",
"Area A Zone ": "ЗОНА A", "Area A Zone ": "ЗОНА A",
"Area B Mode ": "РЕЖИМ ЗОНЫ B", "Area B Mode ": "РЕЖИМ ЗОНЫ B",
"Area B Show ": "ПОКАЗ ЗОНЫ B", "Area B Show ": "ПОКАЗ ЗОНЫ B",
"Area B Zone ": "ЗОНА B", "Area B Zone ": "ЗОНА B",
"Save CH ": "СОХР КАНАЛ", "Save CH ": "СОХР КАНАЛ",
"Delete CH ": "УДАЛ КАНАЛ", "Delete CH ": "УДАЛ КАНАЛ",
"LCD Contrast ": "КОНТРАСТ LCD", "LCD Contrast ": "КОНТРАСТ LCD",
"Freq Input ": "ВВОД ЧАСТ", "Freq Input ": "ВВОД ЧАСТ",
"Reverse CH Dir ": "РЕВЕРС КАНАЛА", "Reverse CH Dir ": "РЕВЕРС КАНАЛА",
"Carrier LED ": "СВЕТОДИОД НЕС", "Carrier LED ": "СВЕТОДИОД НЕС",
"RSSI Refresh ": "ОБНОВЛ RSSI", "RSSI Refresh ": "ОБНОВЛ RSSI",
"APO ": "АВТОВЫКЛ", "APO ": "АВТОВЫКЛ",
"APO Timer ": "ТАЙМЕР АВТОВЫК", "APO Timer ": "ТАЙМЕР АВТОВЫК",
"FM RX Standby ": "FM ПРИЁМ ОЖИД", "FM RX Standby ": "FM ПРИЁМ ОЖИД",
"Initialization ": "СБРОС", "Initialization ": "СБРОС",
" Second PTT ": " ВТОРОЙ PTT", " Second PTT ": " ВТОРОЙ PTT",
"Side Key 1 S ": "БОК КЛАВ 1 КОР", "Side Key 1 S ": "БОК КЛАВ 1 КОР",
"Side Key 1 L ": "БОК КЛАВ 1 ДЛ", "Side Key 1 L ": "БОК КЛАВ 1 ДЛ",
"Side Key 2 S ": "БОК КЛАВ 2 КОР", "Side Key 2 S ": "БОК КЛАВ 2 КОР",
"Side Key 2 L ": "БОК КЛАВ 2 ДЛ", "Side Key 2 L ": "БОК КЛАВ 2 ДЛ",
"0 Press Long ": "0 ДЛ НАЖАТИЕ", "0 Press Long ": "0 ДЛ НАЖАТИЕ",
"1 Press Long ": "1 ДЛ НАЖАТИЕ", "1 Press Long ": "1 ДЛ НАЖАТИЕ",
"2 Press Long ": "2 ДЛ НАЖАТИЕ", "2 Press Long ": "2 ДЛ НАЖАТИЕ",
"3 Press Long ": "3 ДЛ НАЖАТИЕ", "3 Press Long ": "3 ДЛ НАЖАТИЕ",
"4 Press Long ": "4 ДЛ НАЖАТИЕ", "4 Press Long ": "4 ДЛ НАЖАТИЕ",
"5 Press Long ": "5 ДЛ НАЖАТИЕ", "5 Press Long ": "5 ДЛ НАЖАТИЕ",
"6 Press Long ": "6 ДЛ НАЖАТИЕ", "6 Press Long ": "6 ДЛ НАЖАТИЕ",
"7 Press Long ": "7 ДЛ НАЖАТИЕ", "7 Press Long ": "7 ДЛ НАЖАТИЕ",
"8 Press Long ": "8 ДЛ НАЖАТИЕ", "8 Press Long ": "8 ДЛ НАЖАТИЕ",
"9 Press Long ": "9 ДЛ НАЖАТИЕ", "9 Press Long ": "9 ДЛ НАЖАТИЕ",
" SQ Level ": " УР ШУМОПОД", " SQ Level ": " УР ШУМОПОД",
"TX Start Tone ": "ТОН НАЧ TX", "TX Start Tone ": "ТОН НАЧ TX",
"TX End Tone ": "ТОН КОНЦА TX", "TX End Tone ": "ТОН КОНЦА TX",
"Single Tone ": "ОДИНОЧ ТОН", "Single Tone ": "ОДИНОЧ ТОН",
"Tone Timer ": "ТАЙМЕР ТОНА", "Tone Timer ": "ТАЙМЕР ТОНА",
"MIC Gain ": "УСИЛ МИКР", "MIC Gain ": "УСИЛ МИКР",
"SPK Gain ": "УСИЛ ДИНАМ", "SPK Gain ": "УСИЛ ДИНАМ",
"Glitch TH ": "ПОРОГ ПОМЕХ", "Glitch TH ": "ПОРОГ ПОМЕХ",
"Detect Range ": "ДИАПАЗ ДЕТЕКТ", "Detect Range ": "ДИАПАЗ ДЕТЕКТ",
"Repeater Delay ": "ЗАДЕРЖ РЕПИТ", "Repeater Delay ": "ЗАДЕРЖ РЕПИТ",
"DTMF Delay ": "ЗАДЕРЖ DTMF", "DTMF Delay ": "ЗАДЕРЖ DTMF",
"DTMF Interval ": "ИНТЕРВ DTMF", "DTMF Interval ": "ИНТЕРВ DTMF",
"DTMF Duration ": "ДЛИТ DTMF", "DTMF Duration ": "ДЛИТ DTMF",
"DTMF Mode ": "РЕЖИМ DTMF", "DTMF Mode ": "РЕЖИМ DTMF",
"DTMF Select ": "ВЫБОР DTMF", "DTMF Select ": "ВЫБОР DTMF",
"DTMF Display ": "ПОКАЗ DTMF", "DTMF Display ": "ПОКАЗ DTMF",
"DTMF TX Gain ": "УСИЛ TX DTMF", "DTMF TX Gain ": "УСИЛ TX DTMF",
"DTMF RX TH ": "ПОРОГ RX DTMF", "DTMF RX TH ": "ПОРОГ RX DTMF",
"DTMF Control ": "УПРАВЛ DTMF", "DTMF Control ": "УПРАВЛ DTMF",
"VOX ": "VOX", "VOX ": "VOX",
"VOX Delay ": "ЗАДЕРЖ VOX", "VOX Delay ": "ЗАДЕРЖ VOX",
"VOX TH ": "ПОРОГ VOX", "VOX TH ": "ПОРОГ VOX",
"Short Tail ": "КОР ХВОСТ", "Short Tail ": "КОР ХВОСТ",
"Personal ID ": "ЛИЧНЫЙ ID", "Personal ID ": "ЛИЧНЫЙ ID",
"Call Tone ": "ТОН ВЫЗОВА", "Call Tone ": "ТОН ВЫЗОВА",
"Call End Tone ": "ТОН КОНЦА ВЫЗ", "Call End Tone ": "ТОН КОНЦА ВЫЗ",
"Group Hold ": "УДЕРЖ ГРУППЫ", "Group Hold ": "УДЕРЖ ГРУППЫ",
"Single Hold ": "УДЕРЖ ЛИЧН", "Single Hold ": "УДЕРЖ ЛИЧН",
"TX Denoise ": "ШУМОПОД TX", "TX Denoise ": "ШУМОПОД TX",
"RX Denoise ": "ШУМОПОД RX", "RX Denoise ": "ШУМОПОД RX",
"Contacts Set ": "НАСТР КОНТАКТ", "Contacts Set ": "НАСТР КОНТАКТ",
" Talk Group ": " ГРУППА", " Talk Group ": " ГРУППА",
"Encryption Set ": "НАСТР ШИФР", "Encryption Set ": "НАСТР ШИФР",
"Called Show ": "ПОКАЗ ВЫЗЫВ", "Called Show ": "ПОКАЗ ВЫЗЫВ",
"Send DTMF ": "ПЕРЕД DTMF", "Send DTMF ": "ПЕРЕД DTMF",
"Caller Keep ": "УДЕРЖ ВЫЗЫВ", "Caller Keep ": "УДЕРЖ ВЫЗЫВ",
"Clear All Log? ": "ОЧИСТ ЖУРНАЛ?", "Clear All Log? ": "ОЧИСТ ЖУРНАЛ?",
"DMR Or Analog ": "DMR ИЛИ АНАЛОГ", "DMR Or Analog ": "DMR ИЛИ АНАЛОГ",
"RX/TX Limit ": "ЛИМИТ RX/TX", "RX/TX Limit ": "ЛИМИТ RX/TX",
"CH Alias ": "ИМЯ КАНАЛА", "CH Alias ": "ИМЯ КАНАЛА",
"TX Power ": "МОЩН TX", "TX Power ": "МОЩН TX",
"Scan Add ": "ДОБАВ В СКАН", "Scan Add ": "ДОБАВ В СКАН",
"TOT ": "ТАЙМАУТ TX", "TOT ": "ТАЙМАУТ TX",
"Set TX Freq ": "ЗАДАТЬ ЧАСТ TX", "Set TX Freq ": "ЗАДАТЬ ЧАСТ TX",
"DCS Encrypt (A)": "ШИФР DCS (A)", "DCS Encrypt (A)": "ШИФР DCS (A)",
"Band Width (A)": "ПОЛОСА (A)", "Band Width (A)": "ПОЛОСА (A)",
"Tail Tone (A)": "ХВОСТ ТОН (A)", "Tail Tone (A)": "ХВОСТ ТОН (A)",
"Scrambler (A)": "СКРЕМБЛ (A)", "Scrambler (A)": "СКРЕМБЛ (A)",
"Busy Lock (A)": "БЛОК ЗАНЯТ (A)", "Busy Lock (A)": "БЛОК ЗАНЯТ (A)",
"RX Demod (A)": "ДЕМОД RX (A)", "RX Demod (A)": "ДЕМОД RX (A)",
"DMR Mode (D)": "РЕЖИМ DMR (D)", "DMR Mode (D)": "РЕЖИМ DMR (D)",
"DMR Slot (D)": "СЛОТ DMR (D)", "DMR Slot (D)": "СЛОТ DMR (D)",
"Color Code (D)": "ЦВ.КОД (D)", "Color Code (D)": "ЦВ.КОД (D)",
"Contacts (D)": "КОНТАКТЫ (D)", "Contacts (D)": "КОНТАКТЫ (D)",
"TG List (D)": "СПИСОК ТГ (D)", "TG List (D)": "СПИСОК ТГ (D)",
" Encryption (D)": " ШИФР (D)", " Encryption (D)": " ШИФР (D)",
"TX Politely (D)": "ВЕЖЛ TX (D)", "TX Politely (D)": "ВЕЖЛ TX (D)",
"Promiscuous (D)": "ПРОМИСК (D)", "Promiscuous (D)": "ПРОМИСК (D)",
"Channel ID (D) ": "ID КАНАЛА (D)", "Channel ID (D) ": "ID КАНАЛА (D)",
"ID Select (D)": "ВЫБОР ID (D)", "ID Select (D)": "ВЫБОР ID (D)",
"Clear All SMS? ": "ОЧИСТ ВСЕ СМС?", "Clear All SMS? ": "ОЧИСТ ВСЕ СМС?",
"SMS Format ": "ФОРМАТ СМС", "SMS Format ": "ФОРМАТ СМС",
"SMS Font ": "ШРИФТ СМС", "SMS Font ": "ШРИФТ СМС",
"SMS Prompt ": "ОЗВУЧ СМС", "SMS Prompt ": "ОЗВУЧ СМС",
"Cantacts List ": "СПИС КОНТАКТ", "Cantacts List ": "СПИС КОНТАКТ",
"Add Contact ": "ДОБАВ КОНТАКТ", "Add Contact ": "ДОБАВ КОНТАКТ",
"Edit Name ": "ИЗМЕН ИМЯ", "Edit Name ": "ИЗМЕН ИМЯ",
"New SMS ": "НОВОЕ СМС", "New SMS ": "НОВОЕ СМС",
"Edit Type ": "ИЗМЕН ТИП", "Edit Type ": "ИЗМЕН ТИП",
"Edit ID ": "ИЗМЕН ID", "Edit ID ": "ИЗМЕН ID",
"Save? ": "СОХРАНИТЬ?", "Save? ": "СОХРАНИТЬ?",
"Delete? ": "УДАЛИТЬ?", "Delete? ": "УДАЛИТЬ?",
"Send ": "ПЕРЕД", "Send ": "ПЕРЕД",
"Inbox ": "ВХОДЯЩИЕ", "Inbox ": "ВХОДЯЩИЕ",
"Outbox ": "ИСХОДЯЩИЕ", "Outbox ": "ИСХОДЯЩИЕ",
"Contacts ": "КОНТАКТЫ", "Contacts ": "КОНТАКТЫ",
" Reply ": " ОТВЕТ", " Reply ": " ОТВЕТ",
"Forward ": "ПЕРЕСЛАТЬ", "Forward ": "ПЕРЕСЛАТЬ",
"Resend? ": "ПОВТОР ОТПР?", "Resend? ": "ПОВТОР ОТПР?",
" Query ID ": " ЗАПРОС ID", " Query ID ": " ЗАПРОС ID",
" Non-existent ID ": " ID НЕ СУЩЕСТВУЕТ", " Non-existent ID ": " ID НЕ СУЩЕСТВУЕТ",
"Draft Full! ": "ЧЕРНОВИКИ ПОЛНЫ!", "Draft Full! ": "ЧЕРНОВИКИ ПОЛНЫ!",
"No Digital CH ": "НЕТ ЦИФР КАН", "No Digital CH ": "НЕТ ЦИФР КАН",
" ID Conflict ": " КОНФЛИКТ ID", " ID Conflict ": " КОНФЛИКТ ID",
" Send OK ": " ОТПРАВЛЕНО", " Send OK ": " ОТПРАВЛЕНО",
"Send Failed ": "СБОЙ ОТПРАВКИ", "Send Failed ": "СБОЙ ОТПРАВКИ",
"Dual Display ": "ДВОЙН ДИСПЛЕЙ", "Dual Display ": "ДВОЙН ДИСПЛЕЙ",
"Single Display ": "ОДИН ДИСПЛЕЙ", "Single Display ": "ОДИН ДИСПЛЕЙ",
"NJPTLH": "NJPTLH", "NJPTLH": "NJPTLH",
"0JPT.H": "0JPT.H", "0JPT.H": "0JPT.H",
"PC Programing ": "ПРОГРАММ С ПК", "PC Programing ": "ПРОГРАММ С ПК",
" Calibration OK! ": " КАЛИБРОВКА OK!", " Calibration OK! ": " КАЛИБРОВКА OK!",
"DTMF Remote Stun": "DTMF УДАЛ БЛОК", "DTMF Remote Stun": "DTMF УДАЛ БЛОК",
"DTMF Wake up ": "DTMF БУДИЛЬНИК", "DTMF Wake up ": "DTMF БУДИЛЬНИК",
"@Recovering... ": "@ВОССТАНОВЛ...", "@Recovering... ": "@ВОССТАНОВЛ...",
"Members Full ": "УЧАСТНИКИ ПОЛНЫ", "Members Full ": "УЧАСТНИКИ ПОЛНЫ",
"CH Full ": "КАНАЛЫ ПОЛНЫ", "CH Full ": "КАНАЛЫ ПОЛНЫ",
" Area A MDC: ": " ЗОНА A MDC:", " Area A MDC: ": " ЗОНА A MDC:",
"Area B MDC: ": "ЗОНА B MDC:", "Area B MDC: ": "ЗОНА B MDC:",
"Area A DTMF: ": "ЗОНА A DTMF:", "Area A DTMF: ": "ЗОНА A DTMF:",
"Area B DTMF: ": "ЗОНА B DTMF:", "Area B DTMF: ": "ЗОНА B DTMF:",
" ID: ": " ID:", " ID: ": " ID:",
"Unknown Country ": "СТРАНА НЕИЗВ", "Unknown Country ": "СТРАНА НЕИЗВ",
" Off ": " ВЫКЛ", " Off ": " ВЫКЛ",
" CH- ": " CH-", " CH- ": " CH-",
" Call Out ": " ИСХ ВЫЗОВ", " Call Out ": " ИСХ ВЫЗОВ",
"Call In ": "ВХ ВЫЗОВ", "Call In ": "ВХ ВЫЗОВ",
" All Call ": " ОБЩ.ВЫЗОВ", " All Call ": " ОБЩ.ВЫЗОВ",
"ycd)BBH": "ycd)BBH", "ycd)BBH": "ycd)BBH",
"FM RX ": "FM RX", "FM RX ": "FM RX",
" AM RX ": " AM RX", " AM RX ": " AM RX",
"SSB RX ": "SSB RX", "SSB RX ": "SSB RX",
"@Clear All Limit ": "@ОЧИСТ ВСЕ ЛИМИТ", "@Clear All Limit ": "@ОЧИСТ ВСЕ ЛИМИТ",
" Monitor Mode ": " РЕЖИМ МОНИТОР", " Monitor Mode ": " РЕЖИМ МОНИТОР",
"<?lllllllllOKB+cdefgh1H": "<?lllllllllOKB+cdefgh1H", "<?lllllllllOKB+cdefgh1H": "<?lllllllllOKB+cdefgh1H",
" repeater failed ": " СБОЙ РЕПИТЕРА", " repeater failed ": " СБОЙ РЕПИТЕРА",
"Connect to ": "ПОДКЛ К", "Connect to ": "ПОДКЛ К",
" 4IHr4H": " 4IHr4H", " 4IHr4H": " 4IHr4H",
"Please Charge! ": "ЗАРЯДИТЕ!", "Please Charge! ": "ЗАРЯДИТЕ!",
"g 6IHx": "g 6IHx", "g 6IHx": "g 6IHx",
"DRM Remote Stun ": "DMR УДАЛ БЛОК", "DRM Remote Stun ": "DMR УДАЛ БЛОК",
"Prohibit TX ": "ЗАПРЕТ TX", "Prohibit TX ": "ЗАПРЕТ TX",
" Invalid ID ": " НЕВЕРНЫЙ ID", " Invalid ID ": " НЕВЕРНЫЙ ID",
" NO CTC/DCS ": " НЕТ CTC/DCS", " NO CTC/DCS ": " НЕТ CTC/DCS",
"CTCSS: ": "CTCSS:", "CTCSS: ": "CTCSS:",
"DCS:D N ": "DCS:D N", "DCS:D N ": "DCS:D N",
"qHqBH": "qHqBH", "qHqBH": "qHqBH",
"tAtpG": "tAtpG", "tAtpG": "tAtpG",
"United States f": "США f", "United States f": "США f",
"Canada ": "КАНАДА" "Canada ": "КАНАДА"
} }
Разница между файлами не показана из-за своего большого размера Загрузить разницу