Files
rt-4d/docs/RE-Report.md
T
viktor 5f4d207aa5 Мост к DMR-модулю, разбор спектра REFV DualTachyon, карта запчастей прошивки
Баузбенд FM100B:
- найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём
- расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации
- инструмент tools/fm100b.py (ping/send/raw/scan)
- cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом)
- разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд
- cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14
- исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md)

Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md):
- вызывается как функция горячей клавиши №22 Analog Spectrum
- вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318
- спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC

Декомпозиция (docs/firmware-parts.md):
- два процессора + внешний SPI = три канала внедрения
- карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode
- найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки)
- дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке
- аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля

Прочее:
- везде исправлен режим прошивки: тангента PTT вместо клавиши "*"
- устаревший Ru-4D_Flasher.exe удалён из репозитория
- добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
2026-07-22 21:17:20 +09:00

71 KiB
Исходник Ответственный История

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`
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 0x08004C000x08028900, 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 0x0800BAE30x0800BB38.
  • 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
  • 0x0800C7670x0800C7CC 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 0x0802724A0x0802833E) — 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 (0x0802219A0x080240D8) — 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^241, 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,0x900x9A,0x9C0xA5 WriteSPI region (4 KB erase + 1 KB program) sum seed 0 0x06; 0x4A=capacity partly new (0x9C0xA5 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 32bit 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/ARM9class, ARMv4/v5), not CortexM/A/R. Evidence:

  • 8entry ARM exception table with separate IRQ and FIQ vectors (CortexM uses a wordpointer NVIC table with SP@0; this uses branch/LDRpc instructions — definitively classic ARM).
  • The reset trampoline at 0x40 performs the textbook classicARM bankedmode 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 classicARM construct absent on CortexM.
  • Zero CP15 coprocessor accesses in the entire 1.46 MiB image (mcr/mrc p15 count = 0): no MMU/cache setup ⇒ not a CortexA/ARM11 application core; a small MMUless 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 — softwareversion 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 (voicecoder) interleaver enable + noisethreshold control

DMR CSBK / signalling

  • armcsbkSendReq — CSBK transmit request
  • SPCclSpInBandDataInd, SPSendInBandDataReq, …banddata_handleReq — inband signalling data path
  • ATDigCallSetupCnf, ATDigCalledStartCnf, CallsetupCnf, ATC_CallProcessReq, SPContactProcessHandleReq

Alarm / emergency

  • SPEMG_StopAlarmReq, ATRecvEmgCallInd, ATAlarmStatusCnf, ATAlarmStatus_s, ATC_EMGtype, SPATCRecvEmgD[i], EMGLIST — DMR emergencyalarm 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 ATCommand channel (MCU→module commands & module→MCU confirms) and SP… = the module's internal serviceprocessor 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 / inband

  • 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 / signalquality / 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 inband data pipe. The SPKirisunEffectCnf symbol hints the baseband stack derives from a Kirisun DMR reference design.

5.5 Size / layout / RTOS / positiondependence

  • Size/layout: 1,527,808 bytes single flat ARM image. Vector table @0, startup trampoline @0x40, literalpool constants and code following; exception handlers relocated into a ~0x03800000 runtime region.
  • RTOS: a preemptive multitasking RTOS is present (POSIXflavored, 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 pthreadstyle thread/mutex/sem naming point to a POSIXAPI RTOS (RTThread/ThreadXclass) rather than FreeRTOS/µCOS (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).
  • Positiondependence: the code is positiondependent (absoluteaddressed). Pointer scan of the image: 29,939 32bit 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 PCrelative 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 060666), 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.

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.