# Radtel RT-4D — Firmware Reverse-Engineering Report

## Executive summary

- **What it is.** The Radtel RT-4D is a dual-band DMR handheld radio running application firmware version **RT-4D V3.25** (build date `DATE:2026-02-05`). It supports DMR digital voice/data (Tier II, time-slots, color codes, talkgroups, SMS, encryption) plus wideband analog RX including **FM / AM / SSB** demodulation.
- **Two processors.** (A) A **main MCU** — ARM Cortex-M4F (Thumb, STM32F4-compatible register map), FPU enabled, flash base `0x08000000`, bootloader `0x08000000..0x08002800`, application at `0x08002800`. (B) A dedicated **FM100B DMR baseband SoC** — a classic **ARM (ARM-mode, ARMv4/v5, ARM7/9-class)** processor with an 8-entry IRQ/FIQ vector table, running a POSIX-style RTOS with an embedded WebRTC DSP + AMBE vocoder. The two talk over an internal UART.
- **Likely chips.** The MCU is definitively **not a genuine ST part** (it writes RCC registers ST leaves Reserved); it is an **STM32F407-class Cortex-M4F clone, most likely Artery AT32F407/AT32F403A** (ranked candidates below). The FM100B is an MMU-less baseband ARM core (no CP15 anywhere in 1.46 MB).
- **What we extracted.** Full MCU memory map + peripheral census; complete interrupt/vector map with per-ISR peripheral identification; the entire on-device menu tree (from a fixed-width blob at `0x080253ED`); the PC serial/flashing protocol (opcodes `0x34`/`0x52`/region-writes, plus the bootloader `0x39` protocol); the FM100B Req/Cnf/Ind message interface (~129 symbols); and a fully annotated 4 MB SPI data-flash map from a live radio dump.
- **Key opportunities.** Well-defined SPI region write protocol for codeplug modding; a documented "Update DMR Chip" path to reflash the FM100B baseband; rich DMR remote-command (stun/kill/wake/monitor) and encryption code reachable from named string anchors; extended SPI write opcodes (`0x9C..0xA5`) and an internal `0xABCD` magic-gated handler that the public CPS tools do not touch.
- **Key risks.** The **4 KB calibration block at SPI `0x000000` is per-unit factory data and irreplaceable** — any errant region write or full erase destroys it. The MCU's true silicon vendor is inferred from the register map, not read from an IDCODE, so an SVD/debug setup must be validated on-target. The bootloader image is not in the app binary, so internal-flash reflashing is only understood at the protocol level.

## Provenance

The MCU images were obtained from the **official RT-4D firmware upgrade package** (release `20260205`): the vendor RAR was unpacked to a ZIP containing a .NET updater ("Ido_Update"); inside that updater the application firmware is carried as an **Intel-HEX string stored in the managed (`#US`) string heap**, which was decoded into an absolute-addressed binary. This yields `rt4d_stock_v3.25_abs_0x08002800.bin` (155,740 bytes, load base `0x08002800`) and the equivalent absolute `rt4d_stock_v3.25.ihex` (base `0x08000000`). The **FM100B DMR baseband** image (`FM100B_V1.2.0.32_20260130.bin`, 1,527,808 bytes, ARM base `0x00000000`) ships in the separate "DMR Upgrade Tool 260204" package. Independently, a **live 4 MB SPI data-flash dump** (`radio-spi-dump.bin`) was read directly off a physical radio; it is data (calibration + codeplug + font/DSP asset ROMs), not code, and is cross-referenced throughout against the community CPS constants in `rt4d-cps/rt4d_codeplug/constants.py`.

## 1. MCU Identification & Memory Map

### 1.1 Reset & startup path (from vaddr 0x08002AC0)

The application vector table (`0x08002800`) begins `SP=0x2000AE48`, `Reset=0x08002AC1`. The reset handler is a minimal CMSIS-style stub:

```
0x08002ac0  ldr r0,[pc,#0x24]   ; r0 = 0x0801DA2D  (SystemInit, thumb)
0x08002ac2  blx r0
0x08002ac4  ldr r0,[pc,#0x24]   ; r0 = 0x080029E1  (__main / app entry)
0x08002ac6  bx  r0
```
Literals resolved: `lit@0x08002AE8 = 0x0801DA2D` (SystemInit), `lit@0x08002AEC = 0x080029E1` (main).

**SystemInit @ 0x0801DA2C** — fully decoded from its literal pool (`0x0801DAC0..0x0801DACC`):

| Insn | Target | Op | Meaning |
|---|---|---|---|
| 0x0801DA2E | **CPACR 0xE000ED88** | `r|=0x00F00000` | Enable FPU CP10/CP11 full access → **Cortex-M4F confirmed** |
| 0x0801DA40 | **RCC_CR 0x40023800+0x00** | set bit0 | HSION |
| 0x0801DA50 | RCC_CR | wait `(CR>>1)&1` | wait HSIRDY |
| 0x0801DA5C | **RCC_CFGR +0x08** | `&=~3` | SW=HSI |
| 0x0801DA6E | RCC_CFGR | wait `(CFGR>>2)&3==0` | wait SWS=HSI |
| 0x0801DA7C | RCC_CR | `&=0xFEF2FFFF` | clear HSEON(16), HSEBYP(18), CSSON(19), PLLON(24) |
| 0x0801DA88 | **RCC_CFGR +0x08** | `=0x40000000` | reset CFGR (MCO2=SYSCLK) |
| 0x0801DA92 | **RCC_PLLCFGR +0x04** | `=0x00033002` | PLL reset value |
| 0x0801DA98 | **RCC +0xA0 (0x400238A0)** | `=0x000F0000` | *non-ST extended RCC register* |
| 0x0801DAA2 | **RCC_CIR +0x0C** | `=0x009F0000` | clear all clock IRQ flags |
| 0x0801DAB6 | **SCB_VTOR 0xE000ED08** | `=0x08000000` | vector table base |

The actual PLL/HSE bring-up lives in a separate HAL-style driver (`~0x0801DB20`): it enables HSE, programs PLL via helpers, spins on lock, sets AHB/APB prescalers through **RCC_CFGR bitfield helpers** at `0x08020B90` (`bfi …,#0,#0xC` = HPRE/PPRE fields), and switches `SW=PLL` (`r0=2`) waiting `SWS==2`. **FLASH_ACR 0x40023C00** is referenced once (`lit@0x0801DB70`) inside this driver for wait-state latency — canonical STM32F4 FLASH interface base.

### 1.2 Peripheral literal census (0x40000000–0x5009FFFF, 0xE0000000–0xE00FFFFF)

Word-aligned literal-pool constants matching the STM32F4 base grid (counts are literal occurrences):

| Base | Peripheral | Notes |
|---|---|---|
| 0x40003800 | SPI2/I2S2 | canonical |
| 0x40004400 / 0x40004800 | USART2 / USART3 | canonical |
| 0x40007000 | PWR | canonical |
| 0x40007400 (+0x10) | DAC | ch1/ch2 DHR — canonical |
| 0x40010000 | USART1 (×9) | canonical |
| 0x40011400 | USART6 (×6) | canonical (used for PC programming link, §4) |
| 0x40012000 | ADC1 | canonical |
| 0x40014000 | TIM9 | canonical |
| 0x40020000 | **GPIOA (×42)** | canonical |
| 0x40020400 / 0x40020800 / 0x40021400 | GPIOB / GPIOC / GPIOF | canonical AHB1 GPIO stride 0x400 |
| 0x40023000 | CRC | canonical |
| **0x40023800** | **RCC** (×14) | see §1.3 |
| 0x40023C00 | FLASH interface | canonical |
| 0x40026000 / 0x40026400 | DMA1 / DMA2 | canonical |
| 0xE000ED88 | CPACR (FPU) | Cortex-M4F |
| 0xE000ED08 | SCB_VTOR | ARMv7-M |
| **0xE0042000** | **DBGMCU** (×3 literals) | see §1.4 |
| 0xE0000004 / 0xE0001101 | ITM / DWT | ARMv7-M debug |

**Absent (significant):** no USB-OTG (0x50000000 / 0x40040000), no RNG (0x50060000), no Ethernet, no I2C base literals — the radio uses UARTs, SPI2, GPIO, ADC1, DAC, CRC, and DMA only.

### 1.3 Register-map fingerprint — the decisive evidence

Genuine STM32F407 RCC registers end at **PLLI2SCFGR = 0x40023884**. The firmware repeatedly reads/writes three RCC offsets that **do not exist on a genuine STM32F407**:

- **RCC+0xA0 (0x400238A0)** — written `0x000F0000` in SystemInit; loaded at 4 further sites (0x08020BAC, 0x08020DA0, 0x08020E7C, 0x08021BC4).
- **RCC+0xA4 (0x400238A4)** — 0x08020C20.
- **RCC+0x68 (0x40023868)** — 0x08020F70 (`str r1,[r0]` writer helper).

These extended clock-tree registers in the 0x68/0xA0/0xA4 window, combined with a fully STM32F4-identical GPIO/USART/SPI/DMA/ADC/CRC/RCC-core layout, are the classic signature of an **STM32F407-compatible clone**, most consistent with **Artery AT32F403A/AT32F407** (Artery's "CRM" block places additional MISC/PLL registers in exactly this 0x90–0xB0 range, while keeping RCC core offsets 0x00/0x04/0x08/0x0C/0x40/0x44 bit-identical to ST). The core clock helpers at 0x08020B90–0x08020C0E manipulate CFGR HPRE (bits 4-7), PPRE1 (10-12), PPRE2 (13-15) exactly as ST — so the clone is register-compatible on the documented registers and merely *adds* vendor registers.

### 1.4 Device-ID / UID / signature checks

- **DBGMCU 0xE0042000** appears as 3 literal-pool words (0x08012A9C/AB0/AC4). This is the STM32-family DBGMCU base (present on genuine ST *and* all F407 clones). It is used for debug-freeze/config, **not** as an IDCODE dispatch — there is **no** compare of an IDCODE value against a device table near a UID read.
- **No unique-ID base is referenced anywhere:** neither STM32 UID `0x1FFF7A10` / flash-size `0x1FFF7A22`, nor Artery/APM32/F1-style `0x1FFFF7E8`. A full aligned scan of the 0x1FFF0000–0x1FFFFFFF system-memory range returned **zero** literals. The firmware never reads a chip UID, so software does not self-identify the die.
- **No vendor ASCII strings** (`artery`, `at32`, `geehy`, `apm32`, `gd32`, `stm32`) exist in the image; the only version string is `VER :RT-4D V3.25` @ 0x0800BA88.

### 1.5 Ranked candidate list (adversarial)

| Rank | Candidate | Confidence | Evidence for | Evidence against |
|---|---|---|---|---|
| 1 | **Artery AT32F407 / AT32F403A** | **~55%** | Extended RCC/CRM registers at +0x68/+0xA0/+0xA4 that genuine STM32F407 lacks; ST-identical core RCC/GPIO/USART/DMA offsets; M4F; these radios are widely known to use Artery clones. | Cannot see a UID/IDCODE self-check to prove the die; the extended registers are inferred-as-Artery from map position, not read back from a datasheet-matched value. |
| 2 | **GigaDevice GD32F407 / GD32F303** | ~18% | Also STM32F4-map-compatible clone with extra RCC bits; M4-class; common in Chinese radios. | GD32's extra RCC registers cluster differently (e.g. ADDCTL at 0xCC/0xC8), not cleanly at 0xA0/0xA4. |
| 3 | **Geehy APM32F407** | ~12% | STM32F407 drop-in clone, M4F, same peripheral map. | APM32 tracks ST's RCC map closely and does not add registers exactly at +0xA0/+0xA4; less likely. |
| 4 | **Genuine STM32F407** | ~10% | Every documented register offset matches ST exactly; SP/SRAM/flash all in ST-legal ranges. | **Writes to RCC+0xA0/+0xA4/+0x68 which are Reserved on genuine STM32F407** — a stock ST part would ignore these; their deliberate, repeated use argues the silicon actually implements them → argues *against* genuine ST. |
| 5 | F405/F103-class | <5% | — | Ruled out: FPU/CP10-11 enabled (M4F, not M3 → not F103); 0x40011400/DAC/GPIOF present and 128KB SRAM span → F407-class not F405-minimal. |

**Adversarial note on the top pick:** the AT32 call is a map-position inference, not a hard read. The only *proven* facts are (a) M4F, (b) STM32F4 register-map compatible on all standard peripherals, and (c) it drives three RCC registers that genuine STM32F407 does not define. Any of the three named clones would satisfy (a)–(c); AT32 is favored on prior-art (Radtel/other Chinese DMR radios shipping Artery parts) plus the specific 0xA0/0xA4 placement, but the die cannot be excluded (GD32/APM32) without a live IDCODE read from the chip. The safest defensible statement: **"STM32F407-class ARM Cortex-M4F clone, most likely Artery AT32F407, definitively not a genuine ST part given the extended RCC writes."**

### 1.6 Memory map

| Region | Range | Size | Evidence |
|---|---|---|---|
| Bootloader (flash) | 0x08000000 – 0x080027FF | 10 KB | Given; app vaddr base 0x08002800; SystemInit sets VTOR=0x08000000 |
| Application (flash) | 0x08002800 – 0x08028A9C | 155,740 B (~152 KB) | `rt4d_stock_v3.25_abs_0x08002800.bin` size; flash literals reach 0x08021xxx |
| Flash device total | 0x08000000 – 0x0803FFFF (min) | ≥256 KB (F407 class) | Highest densely-used 64K page 0x08020000 (×147); sparse hits to 0x080C0000 are data-table/false, not code |
| Main SRAM | 0x20000000 – 0x2001FFFF | **128 KB contiguous** | SRAM literals span up to 0x2001Fxxx; initial `SP=0x2000AE48` (top-of-stack ~43 KB into SRAM) |
| CCM SRAM (0x10000000) | — | not confirmed | `0x1000exxx` literals are misaligned/odd → Thumb immediates, not data pointers; no confirmed CCM data use |
| DBGMCU | 0xE0042000 | — | debug config |
| Cortex-M4 SCS | 0xE000E000 (VTOR 0xED08, CPACR 0xED88), ITM 0xE0000000, DWT 0xE0001000 | — | ARMv7-M private peripheral bus |

**Peripheral bases in use:** RCC 0x40023800 (+extended 0x68/0xA0/0xA4), FLASH-IF 0x40023C00, PWR 0x40007000, CRC 0x40023000, GPIOA/B/C/F (0x40020000/0400/0800, 0x40021400), USART1 0x40010000, USART2 0x40004400, USART3 0x40004800, USART6 0x40011400, SPI2 0x40003800, ADC1 0x40012000, DAC 0x40007400, TIM9 0x40014000, DMA1 0x40026000, DMA2 0x40026400.

**Clock summary:** M4F @ FPU-enabled; HSI → HSE → PLL bring-up (`PLLCFGR` staged from reset `0x00033002`), SYSCLK switched to PLL (`SW=2`) with FLASH wait-states set via `0x40023C00`; bus dividers programmed through RCC_CFGR HPRE/PPRE1/PPRE2 bitfields.

## 2. Vector Table & Interrupt Map

The application vector table sits at the app base **0x08002800** (the reset vector reprograms `SCB->VTOR` to this after the bootloader hands off). Word0 = initial SP **0x2000AE48**, word1 = Reset **0x08002AC1**. Every entry is an odd (Thumb) address, confirming a Cortex-M image.

### 2.1 Shared stub handlers

Two "do-nothing" targets dominate the table, and both are tight infinite loops (`B .`):

```
0x08002AD8:  b  #0x08002AD8      ; NMI / HardFault / SysTick vector target (word 0x08002AD9)
0x08002ADA:  b  #0x08002ADA      ; generic default IRQ target        (word 0x08002ADB)
```

- **0x08002AD9** is the target of the NMI, HardFault, and **SysTick** slots (exceptions #2, #3, #15). SysTick is therefore *not* used — there is no tick ISR; timing is handled by a TIM (see below).
- **0x08002ADB** is the shared `Default_Handler` wired into every unused external IRQ slot.

Slots that are literally `0x00000000` (words 7–10, 13, and the reserved Cortex-M slots) are the architecturally-reserved gaps and unused vendor IRQs.

### 2.2 System exception vectors (#0–#15)

| # | Exception | Handler | Used? |
|---|-----------|---------|-------|
| 0 | Initial SP | 0x2000AE48 | — |
| 1 | Reset | 0x08002AC1 | unique |
| 2 | NMI | 0x08002AD9 | stub loop |
| 3 | HardFault | 0x08002AD9 | stub loop |
| 4 | MemManage | 0x08002AC1* | (word 0x08002AC1 pattern reused) |
| 5–6 | Bus/UsageFault | 0x080127D1 / 0x08014E1D | unique |
| 7–10 | Reserved | 0x00000000 | — |
| 11 | SVCall | 0x0801A845 | unique |
| 12 | DebugMon | 0x08007869 | unique |
| 13 | Reserved | 0x00000000 | — |
| 14 | PendSV | 0x08018C1D | unique |
| 15 | SysTick | 0x08002AD9 | **stub loop (unused)** |

SVCall/PendSV being unique but SysTick being a stub is the classic signature of a bare-metal cooperative loop with hardware-timer scheduling, or an RTOS that drives the scheduler from a TIM rather than SysTick.

### 2.3 External IRQ map (#16 = table word 16, off 0x40)

Non-default (unique) handlers, with the peripheral identified from the base address loaded in each ISR's literal pool:

| IRQ# | STM32F4 name | Handler | Used? | Evidence / notes |
|------|--------------|---------|-------|------------------|
| 0 | WWDG | 0x08002AD9 | — | (word16 = 0x08002AD9 stub) |
| 11 | EXTI / DMA1_Stream0-ish region | 0x080076E5 | ✅ unique | small flag-setter |
| 18 | **ADC** | 0x08002D1D | ✅ unique | literal `0x40012000` = ADC1 base; reads conversion, increments a counter |
| 25 | **TIM1_UP / TIM10** | 0x0801DBB1 | ✅ unique | literal `0x40010000`; large handler, drives display/keypad scan bytes at 0x2000BF1.. |
| 28 | **TIM2** | 0x0801DDCD | ✅ unique | literal `0x40007410` region + SRAM state; timing/tick replacement for SysTick |
| 39 | **USART3** | 0x080205B1 | ✅ unique | literal `0x40004800` = USART3 base — RX ISR, ring-buffer push (internal FM100B link, §4) |
| 71 | **USART6** | 0x0802061D | ✅ unique | literal `0x40011400` = USART6 base — RX ISR, ring-buffer push (PC programming link, §4) |

All other external slots hold the shared default stub **0x08002ADB** or `0x00000000`, i.e. their peripherals' interrupts are disabled.

> **Index caveat:** exact IRQ numbering (e.g. TIM1 vs the precise EXTI line) is inferred from the peripheral base each ISR touches rather than from position alone, because the vendor may be an F407 clone (AT32/APM32/GD32) with a slightly reordered NVIC table. The **peripheral identity from the literal pool is the hard evidence**; the STM32F4 IRQ# column is the best-fit standard name.

### 2.4 Disassembly of clearly-used ISRs

**USART3 RX ISR @0x080205B0 (IRQ 39)** — the two UART ISRs are structurally identical; both call the same pair of helpers `0x8021EC2` (check-flag) and `0x8021EA8` (read-byte), then push into a SRAM ring buffer:

```
0x080205B0: push {r4,lr}
0x080205B4: movs r1,#0x20         ; flag mask 0x20 = RXNE (USART_SR bit5)
0x080205B6: ldr  r0,[pc,#0x48]    ; ->0x40004800  USART3 base
0x080205B8: bl   #0x8021EC2       ; if(SR & RXNE)
0x080205BC: cbz  r0,#0x80205FC
0x080205BE: ldr  r0,[pc,#0x40]    ; USART3
0x080205C0: bl   #0x8021EA8       ; r0 = USART3->DR  (read byte)
0x080205C4: uxtb r4,r0
0x080205C6: ldr  r0,[pc,#0x3c]    ; ->0x20000C2C  rx ring struct
0x080205C8: ldrh r1,[r0]          ; head index
0x080205CA: ldr  r0,[r0]
0x080205CE: str  r0,[r2]          ; advance write pointer
```
Buffer sits at SRAM `0x20000C2C`/`0x20007575`.

**USART6 RX ISR @0x0802061C (IRQ 71)** — same shape, base `0x40011400` (USART6), buffer at `0x20000C60`/`0x200082EF`.

> **Serial-link assignment (cross-referenced with §4):** USART6 (`0x40011400`) is the **external PC programming link**; USART3 (`0x40004800`) is the **internal MCU↔FM100B DMR-baseband UART** (§4.5 shows the DMR-record parser at `0x08006D00` referencing USART3). The FM100B link uses the larger SRAM buffer (`0x200082EF`).

**ADC ISR @0x08002D1C (IRQ 18)**:
```
0x08002D1C: push {r4,lr}
0x08002D1E: movs r1,#0x20         ; ADC_SR EOC-class flag
0x08002D20: ldr  r0,[pc,#0x34]    ; ->0x40012000  ADC1 base
0x08002D22: bl   #0x80209D4       ; test flag
0x08002D2C: bl   #0x80209CE       ; clear flag
0x08002D30..38: ldr/adds/str      ; ++conversion counter at 0x20000FB8-region
```
Services **ADC1** — battery-voltage / RSSI / (possibly volume-knob) sampling.

**TIM ISR @0x0801DBB0 (IRQ 25, TIM1_UP/TIM10) and @0x0801DDCC (IRQ 28, TIM2)** both open with the update-flag idiom:
```
0x0801DBB2: movs r1,#1            ; TIM_SR UIF (bit0)
0x0801DBB4: ldr  r0,[pc,#0x180]   ; ->0x40010000  TIM1
0x0801DBB6: bl   #0x8021C5C       ; if(SR & UIF)
0x0801DBC2: bl   #0x8021C56       ; clear UIF
```
The TIM1 handler then writes a large block of GPIO/state bytes at `0x20000BF1..0x20000BFA` (display column / keypad-matrix scan). The TIM2 handler (base region `0x40007410`) is the periodic software-tick that stands in for the disabled SysTick.

### 2.5 Active peripherals (from non-default ISRs)

Based purely on which vectors are unique (not the stub), the firmware actively drives:

- **USART3 + USART6** — the two RX-interrupt-driven serial links (internal FM100B DMR link and external CPS host link).
- **ADC1** — analog sampling (battery / RSSI).
- **TIM1 (or TIM10)** — display/keypad matrix scanning.
- **TIM2** — periodic system tick (replacing the unused SysTick).
- **SVCall / PendSV** — context/service switching (RTOS-style), while **SysTick is deliberately stubbed**.

Everything else — WWDG, PVD, RTC, all DMA streams, SPI, I2C, USB_OTG_FS, the remaining TIMs and USARTs — points at the shared `Default_Handler` (0x08002ADB) or is null, i.e. those peripherals are either polled or unused at the NVIC level. Notably **no DMA stream ISR is active**, so the UART links are handled byte-by-byte in interrupt context (consistent with the ring-buffer push seen in the USART ISRs), and **USB_OTG_FS has no ISR** (programming is over USART, not native USB).

## 3. Strings, Menu Tree & Feature Inventory

All addresses are absolute in the MCU application image (load base `0x08002800`). 4,744 raw ASCII runs (len ≥ 3) were extracted; ~970 are "wordy" strings. The bulk of the useful UI text sits in a contiguous string/label region roughly `0x08004C00`–`0x08028900`, with a single dense fixed-width **master menu blob at `0x080253ED`** that decodes the entire menu hierarchy exactly.

### 3.1 String Classification

**Version / build markers**
- `0x0800BA88` `VER :RT-4D V3.25` — firmware banner (the anchor named in the hard facts).
- `0x0800BA9F` `DATE:2026-02-05` — build date.
- `0x0800BAB4` `DMR :` — placeholder for the FM100B baseband version read back at boot.
- `0x0800BACB` `IC  :` — SPI-flash chip-ID label, followed by the JEDEC decode table `25Q80 1MB` / `25Q16 2MB` / … / `25Q512 64MB` / `Unknown` at `0x0800BAE3`–`0x0800BB38`.
- `0x0800BB44` `BATT:0.0V` — battery voltage readout.

**Menu items** — see the reconstructed tree (§3.2). The canonical source is the fixed-width blob at `0x080253ED` (14-char label + 2-digit index records).

**DMR / digital features**
- `0x0800CFCB` `DMR Time Slot: 1` / `0x0800CFE0` `DMR Time Slot: 2`
- `0x0800D007` `DMR Encrypt: Off`
- `0x08016BDF` `Encryption   (D)`, `0x080165D0` `Encryption Set`
- `0x08016BF8` `TX Politely  (D)`, `0x08016C10` `Promiscuous  (D)`, `0x08016C24` `Channel ID (D)`, `0x08016C3C` `ID Select    (D)`
- `0x080167B8` `Color Code   (D)`, `0x080167CC` `Contacts     (D)`
- `0x0800CCE0` `Digital Mode`, `0x0800CD28` `Promiscuous: On/Off`, `0x0800CD50` `Dual Slot : Off/On`
- Call-type labels: `Individual` / `Group Call` / `All Call` (multiple copies, e.g. `0x0800A35F`, `0x0800A374`, `0x08012A71`) with `SID:` / `GID:` / `AID:` prefixes at `0x0800A36C`/`A380`/`A3A8`.

**Remote-control / kill / alarm (high value — see §3.4)**
- `0x08003457` ` DTMF Remote Kill`, `0x0800346C` `DMR Remote Kill`
- `0x08006B30` `DMR Remote Stun`, `0x0801E858`/`0x0801ECB8` `DMR/DRM Remote Stun`, `0x0801E844`/`0x0801ECA4`/`0x08019CB8` `DTMF Remote Stun`
- `0x0800C767`–`0x0800C7CC` `Remote Stun:` / `Remote Kill:` / `Wake Up:` (edit fields)
- `0x08006AEC` `Emergency Alarm`, `0x08006B03` `Being Searched`, `0x08006B1B` `Radio Wake Up`, `0x08019CCC` `DTMF Wake up`
- `0x0800A5B0` `Being Monitored`, `0x0801D72B` `Monitor Mode`
- `0x0801ED28` `Prohibit TX` (result of a stun), `0x0800C7E0`/`C7F4`/`C808` `Radio Online` / `Radio Offline` / `Check Failed` (radio-check / presence).

**Analog / signalling features**
- `0x08009A67` `RX CTC/DCS (A)`, `0x08009A7C` `TX CTC/DCS (A)`, `0x080166FC` `DCS Encrypt (A)`, `0x0800A023` `Remote CTC/DCS`, `0x0801F2AB` `NO CTC/DCS`
- `0x08016744` `Scrambler (A)`, `0x0800AFE7` `Mute Code (A)`, `0x08016714` `Band Width (A)`, `0x0801675C` `Busy Lock (A)`, `0x0801672C` `Tail Tone (A)`
- DTMF suite: `Send Single Tone` (`0x0800C743`), `Area A DTMF:` / `Area B DTMF:` (`0x0801B26C`/`B280`), plus DTMF Delay/Interval/Duration/Select/Display/TX Gain/Control menu labels.
- `0x0800C288` `TX End Tone: MDC` — MDC-1200 signalling.

**Modes / RX**
- `0x0800CC7B`/`CC90`/`CCA4` `RX Mode : AM / SSB / FM` — the receiver supports AM & SSB, not just FM.
- `0x0800B86B`/`B873`/`B878` ` FM ` / ` AM ` / `SSB` mode tags.
- `0x0800CCFF` `Analog VOX : On/Off`, `0x0800C1E3` `Dual Standby:Off/On`.

**Calibration / service**
- `0x08019C87` `Calibration OK!`
- `0x08014950` `Update DMR Chip` + `0x08014964` `Please Wait...` — the FM100B baseband firmware-flash path invoked from the MCU.
- `0x08019842`/`0x08019714`-area & `0x080198F0` `PC Programing` — CPS/serial programming mode.
- Backup/restore: `0x08004C8B` `Backing up...`, `0x08012B5C`/`0x0801A597` `Recovering...`, `0x0800EDA0`/`0x080178F4` `Saving Data.....`, `0x0800F0A3` `Clearing Data...`.

**Error & status messages**
- `0x08005803` `ERROR`, `0x08007070` `Contact Error!`, `0x0800ED8C` `Type Error!`, `0x08006DE4` `Call type error,`
- `0x0800DCC0`/`0x08017AA4`/`0x08017B90` `ID Out Of Range`, `0x08017A8B` `ID Conflict`, `0x0801EE9B` `Invalid ID`, `0x08017733` `Non-existent ID`, `0x08028815`/`0x080284F0` `Unknown station`
- `0x0800ED77` `Contacts Full!`, `0x0801AB64` `Members Full`, `0x08017AE8`/`0x0801CEE8` `Send Failed`, `0x08017AB8` `Sending`
- `0x0800D460` `is out of range`, `0x0800D474` `Resulting freq`, `0x0800D86F` `Cannot be set!`
- `0x0801E3B8` `Please Charge!`, `0x0801E193` `repeater failed`, `0x0801E1A8` `Connect to`.

**Country / region list** (~256 entries; base `0x0802724A`–`0x0802833E`) — MCC/ITU country-code table. Examples: `Falkland Islands`, `Venezuela`, `Argentina Republic`, `South Africa`, `Papua New Guinea`, `Korea Republic o[f]`, `Saudi Arabia`, `Kazakhstan`, `Czech Republic`, `Switzerland`, `Netherlands`. `United States` appears 26× and `United Kingdom` 4× (consecutive MCC blocks). `0x0801BD1C` `Unknown Country` is the fallback. This is a DMR **home-country / MCC lookup**, not a UI language selector.

**Pinyin input table** (`0x0802219A`–`0x080240D8`) — a full CJK pinyin syllable list (`bang`,`beng`,`bian`,…,`zhuo`,`zong`,`zuan`) used for Chinese character entry; confirms a Chinese IME in the SMS/contacts editor. Latin-input mode strings live at `0x080086B4` `ABC`/`abc`/`123`/`PY1`/`PY2`.

**Developer / debug leftovers**
- Misspellings shipped in production: `Copy(Recive)` (`0x080034A3`), `Receving` (`0x080034B8`), `Cantacts List` (`0x08016CC8`), `DRM Remote Stun` (`0x0801ECB8`), `Swasiland` (`0x08027402`), `PC Programing` (`0x080198F0`). These are useful low-entropy grep anchors.
- No printf/format-string or file-path debug strings survive; the image is otherwise release-stripped.

### 3.2 Menu Tree (from the fixed-width blob at `0x080253ED`)

The blob is a flat sequence of `label(14)+index(2)` records concatenated per submenu; the seven top-level items each own the following index-01… run. Reconstructed hierarchy:

```
Main Menu
├─ 01 Basic Set
│   ├─ 01 Radio Name        ├─ 02 Voice Prompt     ├─ 03 Key Beep
│   ├─ 04 Lock Timer        ├─ 05 Backlight        ├─ 06 Light Timer
│   ├─ 07 Brightness        ├─ 08 Menu Exit        ├─ 09 Dual Standby
│   ├─ 10 TX Priority       ├─ 11 Freq Step        ├─ 12 Talkaround
│   ├─ 13 Save Mode         ├─ 14 Scan Mode        ├─ 15 Scan Direction
│   ├─ 16 Scan Dwell        ├─ 17 Scan Interval    ├─ 18 Scan Return
│   ├─ 19 Scan Start        ├─ 20 Scan End         ├─ 21 Alarm Type
│   ├─ 22 Main PTT TX       ├─ 23 Area A Mode      ├─ 24 Area A Show
│   ├─ 25 Area A Zone       ├─ 26 Area B Mode      ├─ 27 Area B Show
│   ├─ 28 Area B Zone       ├─ 29 Save CH          ├─ 30 Delete CH
│   ├─ 31 LCD Contrast      ├─ 32 Freq Input       ├─ 33 Reverse CH Dir
│   ├─ 34 Carrier LED       ├─ 35 RSSI Refresh     ├─ 36 APO
│   ├─ 37 APO Timer         ├─ 38 FM RX Standby    ├─ 39 Initialization
│   ├─ 40 Instruction       └─ 41 Version
├─ 02 Key Define
│   ├─ 01 Second PTT        ├─ 02 Side Key 1 S     ├─ 03 Side Key 1 L
│   ├─ 04 Side Key 2 S      ├─ 05 Side Key 2 L     ├─ 06..15 "0..9 Press Long"
│   └─ (16) SQ Level        # trails the Key Define run as its own idx-01 of Analog Set
├─ 03 Analog Set
│   ├─ 01 SQ Level          ├─ 02 TX Start Tone    ├─ 03 TX End Tone
│   ├─ 04 Single Tone       ├─ 05 Tone Timer       ├─ 06 MIC Gain
│   ├─ 07 SPK Gain          ├─ 08 Glitch TH        ├─ 09 Detect Range
│   ├─ 10 Repeater Delay    ├─ 11 DTMF Delay       ├─ 12 DTMF Interval
│   ├─ 13 DTMF Duration     ├─ 14 DTMF Mode        ├─ 15 DTMF Select
│   ├─ 16 DTMF Display      ├─ 17 DTMF TX Gain     ├─ 18 DTMF RX TH
│   ├─ 19 DTMF Control      ├─ 21 VOX              ├─ 22 VOX Delay
│   ├─ 23 VOX TH            └─ 24 Short Tail        # note: index 20 is skipped
├─ 04 Digital Set
│   ├─ 01 Personal ID       ├─ 02 Call Tone        ├─ 03 Call End Tone
│   ├─ 04 Group Hold        ├─ 05 Single Hold      ├─ 06 SQ Level
│   ├─ 07 MIC Gain          ├─ 08 SPK Gain         ├─ 09 TX Denoise
│   ├─ 10 RX Denoise        ├─ 11 Contacts Set     ├─ 12 TG List Set
│   ├─ 13 Encryption Set    ├─ 14 Called Show      ├─ 15 Send DTMF
│   ├─ 16 Caller Keep       ├─ 17 Call Log         ├─ 18 Clear All Log
│   └─ 19 Address Book
├─ 05 Channel Set
│   ├─ 01 DMR Or Analog     ├─ 02 RX/TX Limit      ├─ 03 CH Alias
│   ├─ 04 TX Power          ├─ 05 Scan Add         ├─ 06 TOT
│   ├─ 07 Offset Freq       ├─ 08 Set TX Freq
│   ├─ 09 CTC/DCS    (A)    ├─ 10 RX CTC/DCS (A)   ├─ 11 TX CTC/DCS (A)
│   ├─ 12 DCS Encrypt(A)    ├─ 13 Mute Code  (A)   ├─ 14 Band Width (A)
│   ├─ 15 Tail Tone  (A)    ├─ 16 Scrambler  (A)   ├─ 17 Busy Lock  (A)
│   ├─ 18 RX Demod   (A)
│   ├─ 19 DMR Mode   (D)    ├─ 20 DMR Slot   (D)   ├─ 21 Color Code (D)
│   ├─ 22 Contacts   (D)    ├─ 23 TG List    (D)   ├─ 24 Encryption (D)
│   ├─ 25 TX Politely(D)    ├─ 26 Promiscuous(D)   ├─ 27 Channel ID (D)
│   └─ 28 ID Select  (D)
├─ 06 Zone Set                 # (top-level slot; label sourced from 0x080152.. block)
├─ 07 Message
│   ├─ 01 New SMS           ├─ 02 Inbox            ├─ 03 Outbox
│   ├─ 04 Drafts            ├─ 05 Default SMS      ├─ 06 Clear All SMS
│   ├─ 07 SMS Format        ├─ 08 SMS Font         └─ 09 SMS Prompt
└─ (Contacts submenus, referenced by Digital Set → Contacts Set / Address Book)
    ├─ Contacts List: 01 Contacts List  02 Add Contact
    └─ Contact edit:  01 Edit Name       02 Select CH
```

`(A)` = analog-only parameter, `(D)` = DMR-only parameter — the firmware tags each channel parameter by mode. Index 20 is skipped in Analog Set and index gaps confirm conditionally-hidden items (e.g. VOX shown only when enabled).

### 3.3 Feature Inventory

- **Dual-processor DMR**: MCU drives an FM100B DMR chip; `Update DMR Chip` (`0x08014950`) confirms the MCU can reflash the baseband over the internal UART.
- **DMR digital voice/data**: time-slot select (TS1/TS2 `0x0800CFCB`), Color Code, Talk Groups (`TG List Set`), Individual/Group/All-Call, SMS over DMR.
- **DMR encryption**: `Encryption Set` / `Encryption (D)` with an On/Off state (`DMR Encrypt: Off` `0x0800D007`). `DCS Encrypt (A)` is a separate analog feature. (Encryption *type* enumeration lives in code/data, not in plain strings — an RE follow-up target.)
- **Remote command suite** (DMR **and** DTMF variants): Remote **Stun**, Remote **Kill**, **Wake Up**, **Radio Check** (`Radio Online/Offline`/`Check Failed`), **Monitor / Being Monitored**, **Emergency Alarm / Being Searched**. Stun result = `Prohibit TX`.
- **Caller/called ID display**: `Show Caller Info` / `Show Called Info` / `Called Show` (`0x0800C823`/`C84C`/`0x080165EC`), `Caller Keep`.
- **Scanner**: Scan Mode/Direction/Dwell/Interval/Return/Start/End, `Scanning` status, `Scan Add` per channel.
- **Dual watch / dual display**: `Dual Standby`, `Dual Slot`, `Area A/B Mode/Show/Zone`, `Dual Display` / `Single Display` (`0x080184F8`/`0x0801850C`).
- **Multi-mode RX**: FM / **AM** / **SSB** demodulation (`RX Mode` strings) + `RX Demod (A)` per-channel — broadband/airband RX capability, not just ham FM.
- **FM broadcast radio**: `FM RX Standby` menu item (`0x080159F4`).
- **Analog signalling**: CTCSS/DCS (RX+TX split), DCS "encrypt", Scrambler, Mute Code, MDC end-tone, full DTMF encode/decode with per-area DTMF IDs, Busy Lock/TX-Politely.
- **SMS**: Inbox / Outbox / Drafts / Default SMS / Clear-All, `SMS Format`, `SMS Font`, `SMS Prompt`; `Unread SMS :` counter (`0x0801FB44`); draft/limit errors (`Draft Full!`). Chinese pinyin IME + ABC/123/PY input modes.
- **Contacts / Addressbook**: `Contacts List`, `Add Contact`, `Edit Member`, `Members Full` (talk-group cap), `Contacts Full!` (contact cap). Contact type = Individual/Group/All Call. `16777215` (`0x08007E07`) = 2^24−1, the max 24-bit DMR ID (confirms IDs are 24-bit).
- **Station/repeater**: `Station Name`, `Offset Set` / `Offset Freq` / `Set TX Freq`, `Talkaround`, `Reverse Freq`, `Repeater Delay`, `Connect to … repeater failed`.
- **Power/UI**: High/Low TX power, TOT (`0x08015806` area), APO + APO Timer, backlight/light-timer/brightness/LCD-contrast, Carrier LED, RSSI Refresh, Lock Timer, Voice Prompt.
- **Service**: on-device calibration (`Calibration OK!`), backup/restore of the SPI data flash, factory `Initialization`, `PC Programing` (CPS).
- **Country/MCC table**: ~256 entries for DMR home-country selection.

### 3.4 Highest-value RE anchors (address → why)

| Address | String | Why it's an anchor |
|---|---|---|
| `0x0800BA88` | `VER :RT-4D V3.25` | Version banner; xref finds the boot/about screen builder and DMR/IC version readback code. |
| `0x080253ED` | master menu blob | Single table driving the whole menu; its xref locates the menu-dispatch state machine and per-item index handlers. |
| `0x08014950` | `Update DMR Chip` | Only anchor for the FM100B baseband-flash routine (internal-UART XMODEM/bootloader trigger). |
| `0x0800346C` / `0x08006B30` / `0x0800C790` | `DMR Remote Kill` / `DMR Remote Stun` / `Remote Kill:` | Locate the remote-command TX/RX handlers — the security-critical CSBK stun/kill/wake path. |
| `0x0800D007` / `0x080165D0` | `DMR Encrypt: Off` / `Encryption Set` | Entry to the encryption enable + key-select code; leads to the (unstringed) cipher/type table. |
| `0x08019C87` | `Calibration OK!` | Anchors the calibration write routine → maps which SPI-dump offsets hold RF calibration. |
| `0x08007E07` `16777215` / `0x0800A36C` `SID:`/`GID:`/`AID:` | DMR ID constants/labels | Confirm 24-bit ID handling; xref finds ID validation (`ID Out Of Range`/`ID Conflict`) and call-type routing. |
| `0x080198F0` `PC Programing` / `0x08004C8B` `Backing up...` | CPS + backup | Anchor the serial-protocol / SPI-flash read-write engine used by the CPS. |
| Misspellings `Copy(Recive)` `0x080034A3`, `Cantacts List` `0x08016CC8`, `DRM Remote Stun` `0x0801ECB8` | typos | Unique low-collision grep hooks for cross-referencing duplicated handler code. |

## 4. Serial / Flashing Protocol

The RT-4D main MCU exposes two logically distinct serial links: an external **PC programming link** (RS485-style half-duplex, **USART6 @ `0x40011400`**, with a GPIO direction/DE line toggled through the bit-set/clear helper at `0x8021c6e`) and an internal link to the FM100B DMR baseband (**USART3 @ `0x40004800`**, §4.5). This section documents the PC-link command set as implemented in the application (`VER :RT-4D V3.25`), plus how bootloader/flash mode is reached.

### 4.1 Frame reception & command validation

Incoming bytes land in a SRAM ring buffer (`data @ 0x20007ddb`, head/tail at `0x20000c5c`/`0x20000c60`). The pre-dispatch **framer** lives at `0x0801f864`. It peeks the first byte of a candidate frame and only accepts it if the opcode is a known first-byte; otherwise it advances the tail by one and resyncs:

```
0801f880 cmp r0,#0x34   beq accept     ; Notify / mode / Close
0801f88e cmp r0,#0x40   beq accept     ; WriteSPI region 0x40 (calibration)
0801f89c cmp r0,#0x90 ; blt reject
0801f8aa cmp r0,#0xa5 ; ble accept      ; WriteSPI regions 0x90..0xA5  (incl. 0xA4 addr-book)
0801f8ba cmp r0,#0x52   beq accept     ; ReadSPI
```

It then computes the **expected frame length** by opcode and re-checks it against the number of buffered bytes (`0x0801f8ca`):

| First byte | Frame length | Meaning |
|---|---|---|
| `0x34` | **5** (`movs r4,#5`) | Notify / mode-select / Close |
| `0x52` | **4** (`movs r4,#4`) | ReadSPI (1 opcode + 2 block + 1 cksum) |
| anything else (`0x40`,`0x90..0xA5`) | **0x404 = 1028** (`movw r4,#0x404`) | WriteSPI / addr-book (opcode+2 hdr + 1024 data + 1 cksum) |

**Checksum (normal mode):** simple 8-bit sum of all bytes except the last, seed **0**, compared against the trailing byte. This is verified inline at `0x0801f914` via the sum helper `0x80109de` (`checksum(buf, len-1)`), and a mismatch discards the frame. This matches the CLI's `_checksum` (`sum(command[:-1]) & 0xFF`). Validated frames are copied into the assembly buffer at `0x200092ef` and handed to the dispatcher `0x8019790`.

Note the CLI's `command_write_spi` uses opcode byte `region_id` (e.g. `0x91`) directly as the first byte — consistent with the framer accepting any byte in `0x90..0xA5` as a 1028-byte write frame. The `0x57` byte named in some CLI shorthand is **not** literally compared as a first byte in the app; the real first byte of an SPI write is the **region id**, and `0x52` is the read.

### 4.2 Top-level dispatcher `0x08019790`

```
08019796 ldrb r0,[r4]        ; frame[0]
08019798 cmp  r0,#0x34  bne 0x8019898   ; -> SPI/addrbook handler 0x80188d4
0801979c ldrb r0,[r4,#3]     ; sub-command = frame[3]
0801979e cmp  r0,#0x10  ->  NOTIFY
080197e2 cmp  r0,#0x54 / 0x58 -> ENTER-MODE
08019854 cmp  r0,#0xee  ->  CLOSE (reboot)
```

**`0x34` — multiplexed control command** (frame `[0x34, a, b, sub, cksum]`):

- **`sub = 0x10` → Notify / enter session.** Clears the 8-byte work area (`memset` via `0x8013738`), emits a status/banner string, replies **`0x06` (ACK)** into the TX buffer, sets session-active flag (`0x20000c59`←1). Confirms CLI `command_notify` = `[0x34,0x00,0x00,0x10,cksum] → 0x06`.
- **`sub = 0x54` or `0x58` → enter SPI-access mode.** Sets mode flags (`0x20000c57`/related) and pre-initializes flash context. `0x54` sets mode=1, `0x58` sets mode=2. These select the flash-write personality used by subsequent region writes (single-bank vs. dual-bank / large-flash path) and reply `0x06`. **Not present in the open CLIs.**
- **`sub = 0xEE` → Close.** Clears the session flag, and depending on the active mode calls a region-finalize routine (`0x8004cb0` or `0x8004ab0`, each a `0x1000`-byte SPI region rewrite/commit), then calls **`0x801a38c` which performs an `NVIC_SystemReset`**:
  ```
  0801a396 ldr r0,[AIRCR]  ; 0xE000ED0C
  0801a39e orr r0, #0x05FA0000
  0801a3a2 adds r0,#4       ; VECTRESET|SYSRESETREQ
  0801a3a6 str r0,[AIRCR]   ; reboot
  ```
  So the CLI's fixed Close frame `[0x34,0x52,0x05,0xEE,0x79]` reboots the radio (returning it to normal firmware, exiting the programming session).

There is also a guarded branch at `0x8019888` comparing a stored word against **`0xABCD`** which, when matched, invokes a secondary handler (`0x801ad9c`/`0x801a5ec`/`0x801ae2c`) — an internal magic-gated path, not used by the public CLIs.

**`0xA4` — Address-book (global contacts) write** (`0x80198ae`): accepts a 1028-byte frame `[0xA4, blkHi, blkLo, 1024×data, cksum]`. It is rejected (`0x18`/`0x19`/`0x17` chip-state checks) if the external flash is too small, matching the CLI's handling of `0x4A` ("capacity limit") and `0xA4` ("capacity mismatch") error replies. On success it writes the block to the large contacts area and ACKs `0x06`.

### 4.3 ReadSPI (`0x52`) and WriteSPI (region id) — handler `0x080188d4`

Both live in `0x80188d4`. `r4 = (frame[1]<<8)|frame[2]` = **KB block index**.

**`0x52` ReadSPI** (`0x080188f4`): echoes the 3-byte header back, computes the byte address `addr = block << 10` (`lsls r4,#0xa`), calls SPI read `0x8021828(dst, addr, 0x400)` for a 1024-byte block, appends a sum checksum over 1027 bytes (`0x80109de`, len `0x403`), and streams `header(3) + data(1024) + cksum(1) = 1028` bytes back. This is exactly the CLI's `command_read_spi` (reads 1028, strips 3-byte header, verifies sum). A leading `0xFF` in byte[0] signals "bootloader active / not readable", which `is_bootloader_mode` uses to detect flash mode.

**Region-write** (`0x0801898e` onward): a `switch(frame[0])` maps each region id to a base **KB offset (`r7`)** and **size in KB (`r8`)**, then erases the covered sectors and programs the 1024-byte payload:

| Opcode | r7 (KB base) | r8 (KB size) | Region (matches `SPI_REGIONS`) |
|---|---|---|---|
| `0x40` | 0 | 1 | calibration (`0x000000`, 4 KB span, 1 KB write) |
| `0x90` | 2 | 1 | main_settings (`0x002000`) |
| `0x91` | 4 | 0x0C | channels (`0x004000`, 48 KB) |
| `0x92` | 0x1C | 0x20 | zones (`0x01C000`, 128 KB) |
| `0x93` | 0x5C | 0x34 | contacts (`0x05C000`) |
| `0x94` | 0x7C | 5 | groups (`0x07C000`) |
| `0x95` | 0xC6 | 5 | dmr_keys (`0x082000` per constants; see §6 note) |
| `0x96` | 0xD0 | 3 | call_log (`0x088000`) |
| `0x97` | 0xD6 | 1 | default_sms (`0x094000`) |
| `0x98` | 0xF0 | 1 | fm_settings-adjacent |
| `0x9A` | 0x100 | 1 | schedules-class |
| `0x9C..0xA5` | via `tbb` jump table at `0x8018a2e` (offsets `0x14C`,`0x164`,`0x188`,`0x198`,`0x19C`,`0x19E`,`0x352`,`0x3F0`,`0x400`/size `0xC00`, …) | | extended data regions not all exposed by the CLI |

**The `0x9C..0xA5` range (via the `tbb` table) is broader than the CLI's published region list** — several of these opcodes (e.g. the `0x400`-KB-base / `0xC00`-KB-size entry) target large data areas the community tools do not currently write.

Write mechanics (`0x8018b76`): for a normal region, before programming it **erases `r8` sectors** by calling the erase primitive `0x8021924` once per KB-sector index `r7+i` (`0x8018b7c`). Then it programs the 1024-byte page with `0x8021a70(addr, payload, 0x400)`, and replies **`0x06`**. This matches CLI `command_write_spi` (region byte + block + 1024 data + sum → `0x06`).

**Flash erase granularity:** the erase primitive `0x8021924` shifts the index left by 12 (`lsls r4,r4,#0xc` → ×4096) and issues SPI opcode **`0x20`** (`movs r0,#0x20` at `0x802193a`) — i.e. a **4 KB sector erase**. Read uses SPI opcode `0x03`; page programming respects 256-byte page boundaries (`rsb r5,#0x100` at `0x8021a7c`). The `cmp #0x18 / #0x19` chip-ID checks select 3-byte vs 4-byte addressing for larger flash parts.

### 4.4 Bootloader / flash mode and the `0x39` firmware protocol

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 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

The MCU uses **USART3 (`0x40004800`, referenced at `0x8006cfc`)** for the internal link to the FM100B DMR baseband module (the PC programming link is on USART6). The DMR-side receive/parse routine at `0x08006d00` reads structured records — note the indexing `r6*0x1B + 0x5E000` (`0x8006d14`: `rsb`/`add` producing a 27-byte-stride record base into the `0x5E000` SPI contacts area) and the `0x15`-byte reads via `0x8021828` — i.e. the MCU pulls DMR contact/alias records and hands them across USART3 to the vocoder module. This framing is **binary and record-oriented, entirely separate from the PC-link `0x34/0x52/region-id` framing**; it carries AMBE/CSBK signaling payloads rather than the checksummed programming frames. The PC-link framer explicitly ignores any byte not in `{0x34,0x40,0x52,0x90..0xA5}`, so DMR traffic and PC traffic cannot be confused even if physically bridged.

### 4.6 Summary of confirmed opcodes

| Opcode (frame[0]) | Sub (frame[3]) | Direction | Checksum | Response | Confirmed |
|---|---|---|---|---|---|
| `0x34` | `0x10` | Notify/open | sum seed 0 | `0x06` | yes (CLI) |
| `0x34` | `0x54` | Enter SPI mode 1 | sum seed 0 | `0x06` | **new** |
| `0x34` | `0x58` | Enter SPI mode 2 | sum seed 0 | `0x06` | **new** |
| `0x34` | `0xEE` | Close → `NVIC_SystemReset` | sum seed 0 | (reboots) | yes (CLI) |
| `0x52` | — | ReadSPI 1 KB block | sum seed 0 | `hdr+1024+cksum`; `0xFF`=bootloader | yes (CLI) |
| `0x40`,`0x90`–`0x9A`,`0x9C`–`0xA5` | — | WriteSPI region (4 KB erase + 1 KB program) | sum seed 0 | `0x06`; `0x4A`=capacity | partly new (`0x9C`–`0xA5` extended) |
| `0xA4` | — | Address-book block write | sum seed 0 | `0x06` / `0x4A` / `0xA4` | yes (CLI) |
| internal `0xABCD` magic gate | — | secondary handler `0x801ad9c` | — | — | **new, unexplored** |
| `0x39`-class (`0x10`/`0x55`), `0x57` write, `0xFF` probe | — | **bootloader** flash protocol | **sum seed 0x48** | `0x06`/`0xFF` | in bootloader (not in app image) |

## 5. FM100B DMR Baseband Firmware

**File:** `FM100B_V1.2.0.32_20260130.bin` — 1,527,808 bytes (≈1.46 MiB), raw ARM32 (ARM mode), load base `0x00000000`.

### 5.1 ARM32 Vector Table

The first 0x20 bytes are the classic 32‑bit ARM exception vector table: a `B` for reset followed by seven `LDR pc,[pc,#0x14]` instructions that pull their targets from a literal pool at 0x20–0x3c.

```
00000000  b   #0x40            ; Reset  -> 0x40 (startup trampoline)
00000004  ldr pc, [pc, #0x14]  ; Undef      lit@0x20
00000008  ldr pc, [pc, #0x14]  ; SWI        lit@0x24
0000000c  ldr pc, [pc, #0x14]  ; Prefetch   lit@0x28
00000010  ldr pc, [pc, #0x14]  ; Data Abort lit@0x2c
00000014  ldr pc, [pc, #0x14]  ; Reserved   lit@0x30
00000018  ldr pc, [pc, #0x14]  ; IRQ        lit@0x34
0000001c  ldr pc, [pc, #0x14]  ; FIQ        lit@0x38
```

Literal pool (0x20–0x3c) resolves the handler addresses:

| Exception | Literal @ | Handler target |
|-----------|-----------|----------------|
| Undef     | 0x20 | `0x0360f41e` |
| SWI       | 0x24 | `0x03e00001` |
| Prefetch  | 0x28 | `0x03800001` |
| Data Abort| 0x2c | `0x03a00001` |
| Reserved  | 0x30 | `0x03c00001` |
| IRQ       | 0x34 | `0x04000001` |
| FIQ       | 0x38 | `0x04200001` |

**Implied reset entry:** the reset vector branches to the startup trampoline at **0x40** (the code executes in place from flash, then handlers live in a copied/remapped region). The handler targets all land in the `0x0380_0000–0x0420_0000` window, showing the runtime image is relocated into an external RAM/XIP region at ~`0x03800000+`. (Word 0x3c = `0xbeef0001` is a padding/magic marker, not a vector.)

### 5.2 Core / SoC class

This is a **bare classic ARM core in ARM state (ARM7/ARM9‑class, ARMv4/v5), not Cortex‑M/A/R.** Evidence:

- 8‑entry ARM exception table with separate **IRQ and FIQ** vectors (Cortex‑M uses a word‑pointer NVIC table with SP@0; this uses branch/LDR‑pc instructions — definitively classic ARM).
- The reset trampoline at 0x40 performs the textbook classic‑ARM banked‑mode startup: mask interrupts and switch processor mode via CPSR, e.g.
  ```
  00000044  mrs r0, apsr
  00000048  orr r0, r0, #0xc0     ; set I+F bits -> disable IRQ & FIQ
  0000004c  msr cpsr_c, r0
  00000050  mrs r0, apsr
  00000054  bic r0, r0, #0x1f
  00000058  orr r0, r0, #0x1f     ; -> System mode (0x1F)
  0000005c  msr cpsr_c, r0
  ```
  Banked CPSR mode/interrupt bits (`0xC0`, mode `0x1F`) are a classic‑ARM construct absent on Cortex‑M.
- **Zero CP15 coprocessor accesses** in the entire 1.46 MiB image (`mcr/mrc p15` count = 0): no MMU/cache setup ⇒ not a Cortex‑A/ARM11 application core; a small MMU‑less DSP/baseband ARM.

This matches a **dedicated DMR/vocoder baseband SoC** rather than a general MCU — the firmware itself carries a `WebRTC` audio DSP stack (see §5.3) doing AMBE vocoding + noise suppression/AGC.

### 5.3 Extracted & categorized strings

**Version / identity**
- `TSwVerQueryCnf`, `SPSwVerQuery` — software‑version query interface
- `VocoderVersion\WebRTC\source\dig…`, `…rcom\VocoderVersion\WebRTC\source…` — build path revealing an embedded **WebRTC** vocoder/DSP source tree
- `FM100`, `VERSION:%08X`, `W]pF_VERSION`

**AMBE / vocoder**
- `ATCRecvAmbe6sdataCnf` — receive AMBE 6s data confirm (AMBE frame delivery)
- `vocoder mutex`, `WebRtcNsx_ProcessCore`, `WebRtcNsx_CalcParam`, `WebRtcAgc_CalculateGa[in]` — WebRTC **NSx** (noise suppression) + **AGC** blocks feeding the vocoder
- `ATC_VCDInterleavEnReq`, `ATVCDInterleavQueryCnf`, `SPGetVcdNoiseTHReq`, `SPVcdInterleavQuery`, `ATC_SendVcdNoi…seqTHSetReq` — VCD (voice‑coder) interleaver enable + noise‑threshold control

**DMR CSBK / signalling**
- `armcsbkSendReq` — CSBK transmit request
- `SPCclSpInBandDataInd`, `SPSendInBandDataReq`, `…banddata_handleReq` — in‑band signalling data path
- `ATDigCallSetupCnf`, `ATDigCalledStartCnf`, `CallsetupCnf`, `ATC_CallProcessReq`, `SPContactProcessHandleReq`

**Alarm / emergency**
- `SPEMG_StopAlarmReq`, `ATRecvEmgCallInd`, `ATAlarmStatusCnf`, `ATAlarmStatus_s`, `ATC_EMGtype`, `SPATCRecvEmgD[i]`, `EMGLIST` — DMR emergency‑alarm subsystem

**Calibration / NV**
- `SPCaliFreqSetCnf`, `SPCali_ChannelParamOpt`, `SPCali_PowerOpt`, `SPCali_DigMod1Opt`, `SPCali_AnaSQthOpt`, `SPCali_AnaVccnOpt`, `SPCali_HeadGQParamOpt`, `SPCali_SQRXFreqOpt`, `PCali_Dig_Fastopenclose_timeOpt`
- `ATC_ClearNVdataReq`, `INCM_NV_WriteItem`, ENV/NV item store with error strings (`Error: The ENV (@0x%…)`, `ENV size is too big`)

**RTOS / tasks / mutex** — see §5.5.

### 5.4 MCU ⇄ FM100B message interface (Req/Cnf/Ind protocol)

The MCU and FM100B exchange a structured **Request / Confirm / Indicate** message protocol over the internal UART (`uart_task`, `atc_queue`, `sp_queue`). Names carry two prefix families: **`ATC_`/`AT…`** = the AT‑Command channel (MCU→module commands & module→MCU confirms) and **`SP…`** = the module's internal service‑processor side. ~129 distinct `*Req`/`*Cnf`/`*Ind` symbols were recovered; grouped by subsystem below (garbled fragments from the extraction omitted):

**Channel / RF configuration (MCU→module `Req`)**
- `ATC_ChFreqSetReq`, `ATC_ChSlotSetReq`, `ATC_ChannelSetCnf`, `ATC_CurChannelWaitSetReq`, `ATC_SetRfPowerLevelReq`, `ATCAgcthSetReq`, `ATCEQLevelSetReq`, `ATC_AnaChGroupSetReq`, `ATC_AnaSignalNumSetReq`, `ATC_DigChGroupSetReq`, `ATC_UVFreqGpio_SE[t]`

**Call setup / processing**
- `ATC_CallProcessReq`, `ATDigCallSetupCnf`, `ATDigCalledStartCnf`, `ATAnaCalledStartCnf`, `CallsetupCnf`, `SPAnaCallsetupCnf`, `PTTStatusCnf`, `ATC_SendCallPromptReq`, `SPContactProcessHandleReq`, `SPBreakCnf`, `SPBSActTimeoverCnf`

**Identity / contacts**
- `ATC_RadioIDSetReq`, `ATRadioIDQueryCnf`, `ATC_CalledContactINfoQuery`, `ATC_CurChKeySetReq`, `ATC_CurChDigdataSetReq`

**Voice / vocoder / record**
- `ATCRecvAmbe6sdataCnf`, `ATVoiceDecCnf`, `ATC_VCDInterleavEnReq`, `ATVCDInterleavQueryCnf`, `SPGetVcdNoiseTHReq`, `SPMicVoiceCnf`, `ATC_RecordEnReq`, `ATC_RecordDataPlayReq`, `ATC_LocalRecordPlayReq`, `ATC_PlaySingleToneReq`, `ATC_DTMFToneSetReq`, `SYStoneSetReq`

**Signalling / SMS / in‑band**
- `armcsbkSendReq`, `SPSendInBandDataReq`, `SPCclSpInBandDataInd`, `ATC_SmsmodeSetReq`, `ATUploadRxSmsCnf`, `ATC_MonitorTxtimeSetReq`, `ATC_DigMonitorEnSetReq`

**Scan / roam**
- `ATScanStatusQueryCnf`, `ATScanSwitchCnf`, `SPSCAN_ScanInd`, `ATCurChScanlistQueryCnf`, `ATC_RoamlistSetReq`, `SPRoamList_S[e]tReq`

**Emergency / alarm**
- `SPEMG_StopAlarmReq`, `ATRecvEmgCallInd`, `ATAlarmStatusCnf`

**RSSI / signal‑quality / measurement**
- `ATC_RssiReadReq`, `ATRssiQueryCnf`, `SPRssilev…QueryCnf`, `SPRssi_glitchQueryCnf`, `SPRssi_noiselevQueryCnf`, `SPAT[C]Sql_glitchQueryCnf`, `ATNoiselevSetCnf`, `ATBtlLevelQueryCnf`

**Calibration / NV**
- `SPCaliFreqSetCnf`, `ATC_ClearNVdataReq`, `SPCali_*Opt` set (Power/Channel/DigMod/AnaSQth/AnaVccn/HeadGQ/SQRXFreq)

**System / power / lifecycle**
- `SPATSysReadyInd`, `SPATWkInd`, `TSwVerQueryCnf`, `ATCmdSetCnf`, `ATModuleStatusQueryCnf`, `ATC_Se[t]DeepSleepReq`, `ATSPSendSleepReq`, `SPNullMsgSendReq`, `SPMmiSetupCnf`, `DrvMmiKeyStateInd`, `SPKirisunEffectCnf`

Semantics: **`*Req`** = command initiated by one side, **`*Cnf`** = confirmation/response to a Req, **`*Ind`** = unsolicited asynchronous indication (e.g. `SPATSysReadyInd`, `ATRecvEmgCallInd`, `SPSCAN_ScanInd`, `DrvMmiKeyStateInd`). `SPCclSpInBandDataInd`/`SPSendInBandDataReq` show the bidirectional CSBK in‑band data pipe. The `SPKirisunEffectCnf` symbol hints the baseband stack derives from a **Kirisun** DMR reference design.

### 5.5 Size / layout / RTOS / position‑dependence

- **Size/layout:** 1,527,808 bytes single flat ARM image. Vector table @0, startup trampoline @0x40, literal‑pool constants and code following; exception handlers relocated into a `~0x03800000` runtime region.
- **RTOS:** a preemptive multitasking RTOS is present (POSIX‑flavored, newlib C runtime). Recovered task/thread IDs and synchronization objects:
  - Threads/tasks: `TASKID_APP`, `TASKID_ATC`, `TASKID_SP`, `TASKID_KEY`, plus `frame_rx_task`, `uart_task`
  - Queues: `atc_queue`, `sp_queue`, `key_queue`, `czapp_queue`, `temp_det_queue`, `frame_rx` queue (`ceate queue failed`)
  - Mutex/sem: `vocoder mutex`, `intercom time mutex`, `psem`, `rtos_sem`, generic `mutex`
  - Diagnostics: `create thread failed …`, `thread - %s stack:`, `warning: %s stack is …`, `close to end of stack address.`, `thread:%s abort!`, `assertion "%s" failed: file "%s"` (newlib assert). The `POSIX` string plus pthread‑style thread/mutex/sem naming point to a POSIX‑API RTOS (RT‑Thread/ThreadX‑class) rather than FreeRTOS/µC‑OS (no FreeRTOS/uCOS signatures found).
- **DSP payload:** WebRTC audio engine embedded — `WebRtcNsx_ProcessCore`/`WebRtcNsx_CalcParam` (noise suppression) and `WebRtcAgc_CalculateGain` (AGC), staged before the AMBE vocoder (`vocoder mutex`, `ATCRecvAmbe6sdataCnf`).
- **Position‑dependence:** the code is **position‑dependent (absolute‑addressed)**. Pointer scan of the image: **29,939** 32‑bit words fall inside the image range (0–1.46 MiB) and **7,698** words point into the fixed `0x0380_0000–0x0420_0000` relocation window — dense absolute pointer tables (literal pools, vector handlers, jump tables) with no PC‑relative PIC/GOT indirection. The image must be loaded at its fixed base and its handlers copied to the fixed high region; it is not relocatable.

## 6. SPI Data Flash / Codeplug Layout (from live radio dump)

The file `radio-spi-dump.bin` is a full read of the RT-4D's external SPI data flash (a 4 MB / 32 Mbit part). It contains **no executable code** — it is the calibration block, the user codeplug (channels/zones/contacts/keys), plus large read-only font/graphics/DSP tables that the firmware streams from flash. Everything below is cross-referenced against `rt4d-cps/rt4d_codeplug/constants.py`.

### 6.1 Dump validation and coarse map

- **Size:** 4,194,304 bytes = `0x400000` (exactly 4 MB). Confirmed.
- **Fill ratio:** `0xFF` (erased) = 2,469,353 bytes (**58.9 %**); `0x00` = 340,767 (8.1 %); other = 1,384,184 (33.0 %). Consistent with a mostly-empty codeplug in the low megabyte and dense read-only asset tables in the upper half.

64 KB block occupancy map (`#` = has data, `.` = all-`0xFF`):

```
0x000000: ##.#.#...#..##..   0x100000: #.#.############
0x200000: ######..........   0x300000: ..#..#########.#
```

Two clearly distinct zones: **low flash `0x000000–0x0DFFFF`** = user/config data (sparse), and **`0x100000–0x3FFFFF`** = large contiguous asset ROMs (fonts, CJK index tables, DSP/waveform data — see §6.6).

### 6.2 Region cross-reference against `constants.py`

`constants.py` `SPI_REGIONS` predicts the low-flash layout. Findings per region (first bytes + verdict):

| Region (id) | Addr | Size | State in this dump | Decoded |
|---|---|---|---|---|
| **calibration** (0x40) | `0x000000` | `0x1000` | **Full, 0 % FF** — critical | See §6.3 |
| main_settings (0x90) | `0x002000` | `0x1000` | 85 % FF, 614 data bytes | Config present; magic `CD AB` at `0x00200C` |
| channels (0x91) | `0x004000` | `0xC000` | 99.8 % FF | **2** channels programmed |
| zones (0x92) | `0x01C000` | `0x20000` | 99.8 % FF | 3 records; zone name `"DMRhub"` at `0x01E004` |
| contacts (0x93) | `0x05C000` | `0x10000` | ~100 % FF | 3 contacts (see below) |
| groups (0x94) | `0x07C000` | `0x3000` | all FF | empty |
| dmr_keys (0x95) | `0x082000` | `0x3000` | all FF | empty **at this address** — see note |
| call_log (0x96) | `0x088000` | `0xC000` | ~100 % FF | 1 stale entry |
| default_sms (0x97) | `0x094000` | `0x1000` | all FF | empty |
| schedules (0x98) | `0x0C6000` | `0x8000` | ~100 % FF | tiny header `36 .. 01 00` at `0x0C6000` |
| fm_settings (0x99) | `0x0D6000` | `0x1000` | all FF | no FM presets stored |
| dtmf_names (0x80) | `0x0C7000` | `0x100` | all FF | empty |

**Discrepancies / corrections to `constants.py`:**
- The **encryption-key name table is at `0x0D0000`, not `0x082000`.** The dump has a dense table of 256 entries `"Key 1"…"Key 256"` on a **48-byte stride** starting `0x0D0002` (block `0x0D0000` is 10.6 % full, 6,948 data bytes). The `dmr_keys` region `0x082000` is entirely `0xFF`. So `constants.py`'s `dmr_keys` addr looks stale/wrong for V3.25 — the real key store lives in the `0x0D0000` bank. (Note the §4.3 write-opcode table follows `constants.py` and lists `0x95 → 0x082000`; that is the *protocol* region id, but this live dump shows the actual populated key names sit at `0x0D0000` — reconcile before writing keys.)
- The region `constants.py` labels **`zones` @`0x01C000`** actually contains **48-byte channel-format records** (same header/frequency layout as the channels region), not the 512-byte `ZONE_SIZE` structures it defines. The 512-byte zone-record assumption does not match this firmware's on-flash layout at `0x01C000`.

### 6.3 Calibration block structure (`0x000000`, region 0x40) — CRITICAL, DO NOT LOSE

The block is **100 % populated** (non-`0xFF` bytes extend all the way to `0x000FFF`). Its structure is a series of **16-byte tables of monotonically-ramping single-byte values** — the classic layout of per-band, per-frequency-point tuning tables (VCO/PLL trim, TX power DAC, RX squelch/RSSI thresholds). Header + first tables:

```
000000  9A 00 37 A0 38 6B 40 AB 05 00 05 00 0A 00 05 00   header / band-edge params
000010  3A 3C 3F 41 44 47 4A 4B 4C 4D 4E 4F 50 51 52 80   16-pt ramp (rising) — per-freq cal curve
000020  0A 80 12 E2 34 40 2A 4F B3 12 E2 34 40 2A 4F B3   sub-block marker 0x80 + repeated 7-byte tuple
000030  /--2357>77777777                                  16-pt ramp then flat  → power table
000040  1E×8 19×8   0050 2D×8 28×8   → paired hi/lo tables (e.g. TX power hi/lo per band)
000060  4B 4C 4D 4E 4E×4  50×8       → rising-then-clamped curve (power ramp)
000090  11 80 12 E2 34 40 2A F3 ...   → second band sub-block (same 0x80 + tuple signature as 0x20)
0000A0  37 38 39 3A 3B 3C 3D 3E 3E 3D...  → VCO/PLL trim curve
0000C0  2D×16 ; 48 49 4A 4B..46  → squelch + another power curve
```

**Hypothesised field layout:**
- `0x0000–0x000F`: global header — band-edge / reference constants (`9A 00 37 A0 38 6B 40 AB`), plus small counts (`05 00 05 00 0A 00 05 00` look like table lengths = 5,5,10,5).
- Repeating **`0x80`-tagged sub-blocks** (`0x000020`, `0x000090`, …) delimit per-band groups; each carries an identical 7-byte tuple `12 E2 34 40 2A 4F B3` that reads as a shared PLL/reference constant.
- **16-entry ramp tables** = calibration curve vs. frequency point (16 points across the band). The paired equal-length runs (`1E×8` then `19×8`, `2D×8` then `28×8`) are almost certainly **High/Low power DAC pairs**; the rising-then-clamped curves (`4B 4C 4D 4E …`) are **TX power vs. frequency**; the plateau `2D×16` blocks are **squelch/RSSI thresholds**.

This 4 KB block is per-unit factory data and is **not recoverable if erased** — it must be preserved in any backup and never overwritten by a CPS write that only touches codeplug regions.

### 6.4 User codeplug decode

**Frequency encoding (confirmed):** in each 48-byte channel record, a flag byte at offset `+4`, then a **32-bit little-endian** frequency at offset `+5`, value = `MHz × 100000` (matches `FREQ_MULTIPLIER`). Verified:

```
CH0 @0x4000: 03 10 00 01 | 00 | 40 8E 9D 02(=0x029D8E40=43880000) → 438.80000 MHz RX=TX  name "Simplex"
CH1 @0x4030: 07 10 00 01 | 00 | ...                              → 430.80000 / 440.80000 MHz  name "Duplex"
```

**Channels:** only **2 of 1024** slots programmed (`"Simplex"` @`0x004020`, `"Duplex"` @`0x004050`). Names are ASCII, `0xFF`-padded, 16-byte field at record offset `+0x20`.

**Contacts** (`0x05C000`, 32-byte records at `0x05E000`): 3 entries —

```
05E000  02 AA AA AA AA "All Call"           → type 0x02 = All-Call, ID 0xAAAAAAAA (broadcast)
05E015  01 06 00 00 00 "TG6"                → type 0x01 = Group, TG 6   (ID as BCD nibbles)
05E02A  01 66 06 00 00 "TG666"              → type 0x01 = Group, TG 666 (BCD 66 06 → 0666)
```

Talkgroup IDs are stored **BCD, little-endian** (`66 06` → `0666`), confirming the group-contact ID format.

**Zones:** 3 records at `0x01C000`; a human zone name `"DMRhub"` sits at `0x01E004`.

**Radio's own DMR ID / callsign:** the `main_settings` block (`0x002000`) is largely erased (only `0x002010–0x00201B` carry config bytes `01 00 00 01 01 00 00 03 00 28 …` and the `CD AB` magic). **No callsign string and no distinct radio DMR-ID field is populated** in this dump — the owner had not set (or had cleared) their personal ID/callsign, so nothing personally-identifying is present in the settings bank.

### 6.5 Human-readable strings (belong to the user — summary only)

~13,300 unique ASCII strings ≥4 chars across the dump, but the overwhelming majority are **font/asset artifacts** (see §6.6), not user data. The genuine **user-authored** strings are few and all in low flash:
- Channel names: `"Simplex"`, `"Duplex"` (2).
- Zone name: `"DMRhub"` (1).
- Contact names: `"All Call"`, `"TG6"`, `"TG666"` (3).
- Encryption-key labels: `"Key 1"…"Key 256"` at `0x0D0000` — these are the firmware's **default** key-slot names, not user text.
- No personal callsign, name, or DMR ID string found anywhere in the dump.

### 6.6 Upper flash `0x100000–0x3FFFFF` — read-only asset ROMs (not codeplug)

These dense blocks are firmware assets, not user data, and should be treated as read-only:
- `0x100000` onward and `0x150000–0x24FFFF`: glyph bitmap / font data (byte-ramp grayscale patterns).
- `0x164000`: a **pinyin romanization table** (`"kao shang xia … jiu ho yin hu …"`) — Chinese input-method / font index.
- `0x350000–0x3DFFFF`: `0x80`-filled and low-amplitude byte-ramp tables → DSP / audio-waveform / additional glyph data.
- `0x3F0000`: a **big-endian Unicode CJK index table** (`4E 02 4E 04 4E 05 … U+4E02, U+4E04…`) mapping codepoints into the font ROM.

### 6.7 Annotated SPI flash offset map

| Offset | End | Size | Contents | Populated |
|---|---|---|---|---|
| `0x000000` | `0x000FFF` | 4 KB | **Calibration** (VCO/power/squelch tables) — CRITICAL | 100 % |
| `0x001000` | `0x001FFF` | 4 KB | reserved / erased | 0 % |
| `0x002000` | `0x002FFF` | 4 KB | main_settings bank0 (magic `CD AB` @`0x200C`) | 15 % |
| `0x003000` | `0x003FFF` | 4 KB | settings bank1 (beta) | 0 % |
| `0x004000` | `0x00FFFF` | 48 KB | **Channels** (48-byte recs) — 2 programmed | <1 % |
| `0x01C000` | `0x03BFFF` | 128 KB | **Zones** (48-byte chan-format recs, name "DMRhub") | <1 % |
| `0x05C000` | `0x06BFFF` | 64 KB | **Contacts** (32-byte recs) — 3 programmed | <1 % |
| `0x07C000` | `0x07EFFF` | 12 KB | Groups / RX group lists | empty |
| `0x082000` | `0x084FFF` | 12 KB | dmr_keys (per constants.py) — **empty here** | empty |
| `0x088000` | `0x093FFF` | 48 KB | Call log | ~empty |
| `0x094000` | `0x0C5FFF` | — | SMS presets/drafts/inbox/outbox area | empty |
| `0x0C6000` | `0x0CDFFF` | 32 KB | Schedules (small header only) | <1 % |
| `0x0C7000` | `0x0C70FF` | 256 B | DTMF names | empty |
| `0x0D0000` | `0x0D2FFF` | ~12 KB | **Encryption-key name table** ("Key 1…256", 48-B stride) | 11 % |
| `0x0D6000` | `0x0D6FFF` | 4 KB | FM broadcast presets | empty |
| `0x100000` | `0x24FFFF` | ~1.3 MB | Font / glyph bitmap ROM + pinyin table (`0x164000`) | dense |
| `0x250000` | `0x31FFFF` | — | mostly erased | ~0 % |
| `0x350000` | `0x3DFFFF` | ~0.5 MB | DSP/waveform + glyph asset tables (`0x80`-filled) | dense |
| `0x3F0000` | `0x3FFFFF` | 64 KB | Unicode CJK codepoint index (big-endian, U+4E00…) | dense |

**Bottom line for backup/restore:** the irreplaceable per-unit data is the **4 KB calibration block at `0x000000`**. User codeplug lives entirely in `0x002000–0x0D6FFF` (settings, channels @`0x004000`, zones @`0x01C000`, contacts @`0x05C000`, key names @`0x0D0000`). Everything at `0x100000+` is stock firmware assets identical across radios and safe to regenerate from the vendor image.

## Recommended RE toolchain & setup

### MCU application (Ghidra / IDA)

- **Language / processor:** `ARM Cortex` variant, **little-endian, Thumb** (`ARM:LE:32:Cortex` in Ghidra). The image is pure Thumb (every vector is an odd address).
- **Two equivalent import routes:**
  1. **Load the Intel-HEX** `rt4d_stock_v3.25.ihex` — it carries absolute addresses (base `0x08000000`), so Ghidra/IDA places the app at `0x08002800` automatically. Preferred.
  2. **Load the raw bin** `rt4d_stock_v3.25_abs_0x08002800.bin` with **load/image base = `0x08002800`** (not `0x08000000` — the bootloader is not in this file).
- **Memory blocks to define manually** (the bin/ihex only covers flash): create RAM `0x20000000` size `0x20000` (128 KB SRAM, RW); map the SCS/peripheral ranges as needed for the SVD.
- **SVD:** load an **STM32F407** SVD as the baseline (register-map-compatible) — it correctly labels RCC `0x40023800`, GPIO `0x40020000`, USART1/2/3/6, SPI2, ADC1, DAC, TIM, DMA, FLASH-IF, PWR, CRC. Then **manually annotate the three non-ST extended RCC registers** (`RCC+0x68 = 0x40023868`, `RCC+0xA0 = 0x400238A0`, `RCC+0xA4 = 0x400238A4`), which the F407 SVD marks Reserved. If you can confirm the die is **Artery AT32F407/AT32F403A** on-target (recommended — read IDCODE over SWD), switch to the Artery **CRM** SVD, which names those registers natively.
- **Entry points / vector table:**
  - Vector table at **`0x08002800`**: word0 = initial SP `0x2000AE48`, word1 = Reset `0x08002AC1`.
  - Force-disassemble the reset handler at **`0x08002AC0`** (Thumb, clear bit 0), then follow `SystemInit @ 0x0801DA2C` and `main @ 0x080029E0`.
  - Define the exception/IRQ table entries from §2 (SVCall `0x0801A845`, PendSV `0x08018C1D`, and the active ISRs: ADC `0x08002D1D`, TIM1 `0x0801DBB1`, TIM2 `0x0801DDCD`, USART3 `0x080205B1`, USART6 `0x0802061D`). Set VTOR = `0x08000000` mentally, but note the *app* table is used post-boot.
- **Bootloader/app split:** the bootloader (`0x08000000–0x080027FF`) is **not** in these files. Treat `0x08002800` as the app entry; the `0x39`/`0x57`/`0xFF` bootloader flash protocol (checksum seed `0x48`) lives only in that missing image — dump it separately over SWD if you need it.
- **High-value starting xrefs:** the string anchors in §3.4 (menu blob `0x080253ED`, `Update DMR Chip` `0x08014950`, remote kill/stun strings, `Calibration OK!` `0x08019C87`) and the serial dispatcher `0x08019790` / framer `0x0801F864`.

### FM100B baseband

- **Language / processor:** **ARM little-endian, ARM mode** (`ARM:LE:32:v5t` or `v4t` — classic ARM7/9-class, *not* Cortex). Load `FM100B_V1.2.0.32_20260130.bin` at **base `0x00000000`**.
- **Vector table** at `0x0` (B reset + 7× `LDR pc,[pc,#0x14]`); reset trampoline at **`0x40`**. Define the literal-pool handler pointers at `0x20–0x38`.
- **Relocation region:** create a second memory block at **`0x03800000`** (the handler/relocation window `0x03800000–0x04200000`) so the ~7,700 absolute pointers resolve. The image is position-dependent; do not rebase.
- **No SVD** applies (custom baseband SoC); reverse peripherals from the driver code. Anchor on the `ATC_/SP…` Req/Cnf/Ind symbol strings (§5.4) to name the UART message handlers.

### SPI data flash

- Not code — open `radio-spi-dump.bin` in a hex editor / the CPS. Use the §6.7 offset map and `rt4d-cps/rt4d_codeplug/constants.py` (with the two corrections in §6.2) to parse regions.

## Prioritized next steps

1. **Back up the radio first (see safety note).** Read all of SPI flash — especially the **4 KB calibration block at `0x000000`** — before touching anything.
2. **Confirm the MCU die on-target.** Connect SWD, read the DBGMCU/IDCODE and the UID region; this resolves the AT32-vs-GD32-vs-APM32 ambiguity from §1.5 and lets you pick the correct SVD. Also dump the **bootloader** `0x08000000–0x08002800` while you have SWD.
3. **Map the serial engine.** Xref the framer `0x0801F864` and dispatcher `0x08019790`; fully enumerate the region-write `tbb` table at `0x8018A2E` to document the undocumented `0x9C..0xA5` write opcodes and the `0xABCD` magic-gated handler (`0x801AD9C`) — these are unexplored by the community CPS.
4. **Trace the `0x34/0x54/0x58` mode-select flags** (`0x20000c57`) to understand the single-bank vs dual-bank flash-write personalities before writing any region from a custom tool.
5. **Decode the calibration block** (§6.3): correlate the `Calibration OK!` writer (`0x08019C87`) with the 16-byte ramp tables to label each per-band curve (VCO/PLL trim, TX power hi/lo DAC, squelch/RSSI). This is the highest-value RF-modding target.
6. **Reverse the DMR remote-command path** (stun/kill/wake/monitor) from the string anchors in §3.4 → find the CSBK RX handler and the `Prohibit TX` enforcement; assess whether stun/kill can be disabled or spoofed.
7. **Reverse the encryption implementation** from `Encryption Set` `0x080165D0` → locate the cipher/type table (not stringed) and the key store; reconcile the key-name table location discrepancy (`0x0D0000` live vs `0x082000` in constants).
8. **Map the FM100B message interface** (§5.4): pair each `ATC_*Req` the MCU sends over USART3 with its `*Cnf`; this documents the full MCU↔baseband API and is the path to custom DMR features and to understanding the `Update DMR Chip` (`0x08014950`) reflash.
9. **Fix/extend the CPS constants** (§6.2 corrections: zone record size, key-name table address) so community tooling round-trips correctly against V3.25.

## Safety note — calibration backup before any flashing

The **4 KB calibration block at SPI offset `0x000000`** is **per-unit factory RF data** (VCO/PLL trim, TX-power DAC curves, squelch/RSSI thresholds) and is **NOT recoverable if erased or overwritten** — there is no copy in the firmware image, and a wrong value will mis-tune the transmitter (out-of-spec power/deviation, potential PA damage or spurious emissions). Before any write/flash operation:

1. **Read and archive the full 4 MB SPI dump** (`radio-spi-dump.bin` is one such capture) and separately verify the first `0x1000` bytes are non-`0xFF` (a valid calibration block is 100% populated per §6.3).
2. **Never issue a full-chip erase** or a bulk write that spans `0x000000`. The `0x40` region write erases a 4 KB sector at offset 0 — treat it as off-limits unless you are deliberately restoring a verified backup.
3. When modding the **codeplug only**, restrict writes to `0x002000–0x0D6FFF` (settings/channels/zones/contacts/keys). Everything at `0x100000+` is stock, regenerable firmware assets.
4. For **MCU or FM100B firmware** flashing, keep the stock vendor images (`rt4d_stock_v3.25*`, `FM100B_V1.2.0.32_20260130.bin`) on hand for rollback, and confirm you can reach the bootloader (**power on holding PTT**, `0xFF` probe echoes `0xFF`) *before* erasing, so a failed flash is recoverable.