Мост к 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
Этот коммит содержится в:
@@ -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 в обе стороны).
|
||||||
|
|
||||||
## История извлечения прошивки
|
## История извлечения прошивки
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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`, обязательно вызывая оригинал.
|
||||||
@@ -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 распространяется только бинарём (исходников нет). Здесь он используется исключительно
|
||||||
|
> для сверки/исследования собственной работы.
|
||||||
@@ -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`.
|
||||||
@@ -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`
|
||||||
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Двоичные данные
Двоичный файл не отображается.
@@ -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('Ошибка стирания.');}
|
||||||
|
|||||||
@@ -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. Не отключайте кабель до конца записи.")
|
||||||
|
|||||||
@@ -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('Ошибка стирания.');}
|
||||||
|
|||||||
@@ -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 function’s 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.
|
||||||
@@ -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 + (ch−0x20)*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=lead−0x81, col=trail−0x40, 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 `(ch−0x20)*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?+(ch−0x20)*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 + (ch−0x20)*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.
|
||||||
@@ -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.
|
||||||
@@ -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 |
|
||||||
@@ -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.
|
||||||
@@ -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 Cortex‑M4F Thumb, load/vaddr base `0x08002800`). All disassembly via capstone `CS_ARCH_ARM + CS_MODE_THUMB`.
|
||||||
|
|
||||||
|
This document reverse‑engineers 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 low‑level 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 / dual‑display variant is id 7 → `0x08013804`**.
|
||||||
|
- Home render is **two‑stage and data‑driven**, not hard‑coded:
|
||||||
|
1. **`format_area_display(area, hi, shift) @0x08011414`** reads the live **channel cache** at `0x20002DEA` (48‑byte records) + the per‑area **mode table** at `0x20002DCF`, formats frequency / channel‑name / mode‑tag 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 / dirty‑flag driven.** An element repaints only when its byte in `g_dirty @0x200024EB` is non‑zero; setting a flag (e.g. after a channel/area change, RX event, or key) triggers repaint on the next UI tick. No full‑frame clear.
|
||||||
|
- **The single struct you must understand is `g_disp @0x200009C3`.** Every on‑screen string/flag is a field of it (see §3). To replace the home screen, either (a) keep `format_area_display` and re‑lay‑out 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 call‑site `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 | dirty‑flag array | role |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| **0** | **`home_draw 0x08013BD8`** | **`g_disp 0x200009C3`** | **`g_dirty 0x200024EB`** | **primary standby (VFO/channel) home** |
|
||||||
|
| 7 | `0x08013804` | `0x2000096A` | `0x200009BD` | alt home / dual‑display variant |
|
||||||
|
| 2 | `0x08013980` | `0x200008D0` | `0x200008F5` | standby‑key / 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 region‑write path) is preserved. 2‑instruction 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) = TX‑shift 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 48‑byte 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]` | 16‑byte **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` (sub‑mode).
|
||||||
|
|
||||||
|
Writes into `g_disp @0x200009C3` (see §3) and builds ASCII via helpers:
|
||||||
|
- **`num_to_ascii(uint val, int ndigits) @0x08018530`** — right‑aligns `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 per‑element 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 re‑derived 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 sub‑records 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"/" " top‑right 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` | sub‑mode: 0/1 name width, 2 = big‑digit 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 sub‑mode | |
|
||||||
|
| `+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 right‑edge 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 **mode‑tag / 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 5‑column glyph at columns **123–127** (`0x7B+i`). Pattern bytes `30 78 FC 30 30` = a right‑pointing arrowhead → the **A/B active‑area selector arrows** on the far right edge. `draw_icon_batt @0x08008874` is the structurally‑identical sibling used for the battery/dual markers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. On‑screen element → data source → draw call (the master table)
|
||||||
|
|
||||||
|
Coordinates: `page` = 8‑px 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`, re‑ordered 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** (dual‑watch 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** (top‑right) | `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]` (12‑bit tone), `[+0xE]`/`[+0x10]` (nibble) | folded into `contentA`/`g_disp2` via formatter | (with #1) |
|
||||||
|
| 10 | **DMR TG / Contact / Color‑Code (RX)** | DMR call ctx `g_call @0x2000A6C5` (`+1`=call type, `+2`=ID, `+0x38`=name), fed from cache & FM100B | built by the call‑info path near `0x08006754`; painted on Area line as name/ID | RX‑event 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` | period‑gated 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 per‑channel field (`RX Demod (A)` menu) stored in the channel record and shown via the same status‑line 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 on‑air TX indicator is a status‑flag element, not a distinct routine.
|
||||||
|
- **Dual‑watch 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` | per‑element **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**, 48‑byte (`0x30`) stride; `[+5]`RXfreq `[+9]`TXfreq `[+0xD/0xF]`tones `[+0x20]`name. Mirrors codeplug channel record — **read‑only for UI**. |
|
||||||
|
| `0x20002DCF` | u8[] | `g_chmode` | per‑area display mode/sub‑mode 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 (BCD‑decoded 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 standby‑key 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 force‑marks fields dirty on entry (full repaint on screen switch).
|
||||||
|
- **RX / DMR event** — updates `g_call @0x2000A6C5` and re‑runs the formatter → dirties the affected area.
|
||||||
|
- **Periodic** — battery/signal icons and RSSI are repainted by the slower period‑gated tasks in `ui_tick_normal` (counters `0x20000BF5/BF6/BF8`), independent of `g_dirty`.
|
||||||
|
- **Model:** direct‑to‑GDDRAM (no framebuffer, `display.md` §1). Partial repaint is safe because elements occupy fixed, non‑overlapping 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, re‑lay‑out only (least work).**
|
||||||
|
Keep calling `format_area_display(area, hi, shift) @0x08011414` (it does the codeplug→ASCII math for you), then read the ready‑made 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 region‑write engine (`main-loop.md` §5). The channel cache `0x20002DEA` mirrors the on‑flash 48‑byte 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` (48‑byte stride, RX@+5/TX@+9/tones@+0xD,0xF/name@+0x20); helpers `num_to_ascii 0x08018530`, `str_insert_char 0x080135A8`, `memcpy_off 0x080062EC`; the dirty‑flag 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"`).
|
||||||
|
- **MEDIUM‑HIGH** — exact page (row) numbers per element: the `r4/r5/r6` row registers are re‑ordered by `active_area` at handler entry, so the absolute page of Area A vs B swaps with selection; the column x‑values are exact (from the draw calls). Battery/signal icon columns per `display.md`.
|
||||||
|
- **MEDIUM** — the DMR TG/Color‑Code 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 sub‑tag is inferred from the `RX Demod (A)` channel field rather than a distinct draw routine. Confirm on‑device when redesigning the DMR overlay.
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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 ''}")
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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])
|
||||||
@@ -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])
|
||||||
Ссылка в новой задаче
Block a user