Мост к 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 удалений
+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`, обязательно вызывая оригинал.