# 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`, обязательно вызывая оригинал.
