- Полный RE стока V3.25 (Cortex-M4F) + FM100B: карта памяти, протокол, codeplug, UI-архитектура - Русификация: свой CP1251-шрифт + патч рендера, перевод меню и надписей, ребренд Ru-4D V3.25 - Блюпринт переделки UI + C-тулчейн (clang thumbv7em), доказан инъекцией - Готовые флешеры: WebSerial .html и Windows .exe со вшитой прошивкой - Дамп SPI рации, стоковая прошивка, инструменты сборки Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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, bootloader0x08000000..0x08002800, application at0x08002800. (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 (opcodes0x34/0x52/region-writes, plus the bootloader0x39protocol); 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 internal0xABCDmagic-gated handler that the public CPS tools do not touch. - Key risks. The 4 KB calibration block at SPI
0x000000is 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
0x000F0000in 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-size0x1FFF7A22, nor Artery/APM32/F1-style0x1FFFF7E8. 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 isVER :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_Handlerwired 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 at0x08006D00referencing 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
0x0800BA88VER :RT-4D V3.25— firmware banner (the anchor named in the hard facts).0x0800BA9FDATE:2026-02-05— build date.0x0800BAB4DMR :— placeholder for the FM100B baseband version read back at boot.0x0800BACBIC :— SPI-flash chip-ID label, followed by the JEDEC decode table25Q80 1MB/25Q16 2MB/ … /25Q512 64MB/Unknownat0x0800BAE3–0x0800BB38.0x0800BB44BATT: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
0x0800CFCBDMR Time Slot: 1/0x0800CFE0DMR Time Slot: 20x0800D007DMR Encrypt: Off0x08016BDFEncryption (D),0x080165D0Encryption Set0x08016BF8TX Politely (D),0x08016C10Promiscuous (D),0x08016C24Channel ID (D),0x08016C3CID Select (D)0x080167B8Color Code (D),0x080167CCContacts (D)0x0800CCE0Digital Mode,0x0800CD28Promiscuous: On/Off,0x0800CD50Dual Slot : Off/On- Call-type labels:
Individual/Group Call/All Call(multiple copies, e.g.0x0800A35F,0x0800A374,0x08012A71) withSID:/GID:/AID:prefixes at0x0800A36C/A380/A3A8.
Remote-control / kill / alarm (high value — see §3.4)
0x08003457DTMF Remote Kill,0x0800346CDMR Remote Kill0x08006B30DMR Remote Stun,0x0801E858/0x0801ECB8DMR/DRM Remote Stun,0x0801E844/0x0801ECA4/0x08019CB8DTMF Remote Stun0x0800C767–0x0800C7CCRemote Stun:/Remote Kill:/Wake Up:(edit fields)0x08006AECEmergency Alarm,0x08006B03Being Searched,0x08006B1BRadio Wake Up,0x08019CCCDTMF Wake up0x0800A5B0Being Monitored,0x0801D72BMonitor Mode0x0801ED28Prohibit TX(result of a stun),0x0800C7E0/C7F4/C808Radio Online/Radio Offline/Check Failed(radio-check / presence).
Analog / signalling features
0x08009A67RX CTC/DCS (A),0x08009A7CTX CTC/DCS (A),0x080166FCDCS Encrypt (A),0x0800A023Remote CTC/DCS,0x0801F2ABNO CTC/DCS0x08016744Scrambler (A),0x0800AFE7Mute Code (A),0x08016714Band Width (A),0x0801675CBusy Lock (A),0x0801672CTail 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. 0x0800C288TX End Tone: MDC— MDC-1200 signalling.
Modes / RX
0x0800CC7B/CC90/CCA4RX Mode : AM / SSB / FM— the receiver supports AM & SSB, not just FM.0x0800B86B/B873/B878FM/AM/SSBmode tags.0x0800CCFFAnalog VOX : On/Off,0x0800C1E3Dual Standby:Off/On.
Calibration / service
0x08019C87Calibration OK!0x08014950Update DMR Chip+0x08014964Please Wait...— the FM100B baseband firmware-flash path invoked from the MCU.0x08019842/0x08019714-area &0x080198F0PC Programing— CPS/serial programming mode.- Backup/restore:
0x08004C8BBacking up...,0x08012B5C/0x0801A597Recovering...,0x0800EDA0/0x080178F4Saving Data.....,0x0800F0A3Clearing Data....
Error & status messages
0x08005803ERROR,0x08007070Contact Error!,0x0800ED8CType Error!,0x08006DE4Call type error,0x0800DCC0/0x08017AA4/0x08017B90ID Out Of Range,0x08017A8BID Conflict,0x0801EE9BInvalid ID,0x08017733Non-existent ID,0x08028815/0x080284F0Unknown station0x0800ED77Contacts Full!,0x0801AB64Members Full,0x08017AE8/0x0801CEE8Send Failed,0x08017AB8Sending0x0800D460is out of range,0x0800D474Resulting freq,0x0800D86FCannot be set!0x0801E3B8Please Charge!,0x0801E193repeater failed,0x0801E1A8Connect 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: Off0x0800D007).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,
Scanningstatus,Scan Addper 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 Modestrings) +RX Demod (A)per-channel — broadband/airband RX capability, not just ham FM. - FM broadcast radio:
FM RX Standbymenu 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 (
0x08015806area), 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, factoryInitialization,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 (memsetvia0x8013738), emits a status/banner string, replies0x06(ACK) into the TX buffer, sets session-active flag (0x20000c59←1). Confirms CLIcommand_notify=[0x34,0x00,0x00,0x10,cksum] → 0x06.sub = 0x54or0x58→ enter SPI-access mode. Sets mode flags (0x20000c57/related) and pre-initializes flash context.0x54sets mode=1,0x58sets mode=2. These select the flash-write personality used by subsequent region writes (single-bank vs. dual-bank / large-flash path) and reply0x06. Not present in the open CLIs.sub = 0xEE→ Close. Clears the session flag, and depending on the active mode calls a region-finalize routine (0x8004cb0or0x8004ab0, each a0x1000-byte SPI region rewrite/commit), then calls0x801a38cwhich performs anNVIC_SystemReset:So the CLI's fixed Close frame0801a396 ldr r0,[AIRCR] ; 0xE000ED0C 0801a39e orr r0, #0x05FA0000 0801a3a2 adds r0,#4 ; VECTRESET|SYSRESETREQ 0801a3a6 str r0,[AIRCR] ; reboot[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 documented "hold * at power-on" path — or a magic word left in RAM/backup register) or to jump to the app at 0x08002800 (SP=0x2000AE48, reset=0x08002AC1). The two firmware-flash speed modes (115200 default vs 256000 requiring # held at boot) are also bootloader behavior. Because the app's Close reboots into that bootloader, the practical "enter flash mode" sequence is: open a normal session (0x34..0x10), then reboot with hold-key to land in the 0x39 flasher — or power-cycle holding */#.
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.
Banked CPSR mode/interrupt bits (
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, r00xC0, mode0x1F) are a classic‑ARM construct absent on Cortex‑M. - Zero CP15 coprocessor accesses in the entire 1.46 MiB image (
mcr/mrc p15count = 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 interfaceVocoderVersion\WebRTC\source\dig…,…rcom\VocoderVersion\WebRTC\source…— build path revealing an embedded WebRTC vocoder/DSP source treeFM100,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 vocoderATC_VCDInterleavEnReq,ATVCDInterleavQueryCnf,SPGetVcdNoiseTHReq,SPVcdInterleavQuery,ATC_SendVcdNoi…seqTHSetReq— VCD (voice‑coder) interleaver enable + noise‑threshold control
DMR CSBK / signalling
armcsbkSendReq— CSBK transmit requestSPCclSpInBandDataInd,SPSendInBandDataReq,…banddata_handleReq— in‑band signalling data pathATDigCallSetupCnf,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_timeOptATC_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_*Optset (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
~0x03800000runtime 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, plusframe_rx_task,uart_task - Queues:
atc_queue,sp_queue,key_queue,czapp_queue,temp_det_queue,frame_rxqueue (ceate queue failed) - Mutex/sem:
vocoder mutex,intercom time mutex,psem,rtos_sem, genericmutex - 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). ThePOSIXstring 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).
- Threads/tasks:
- DSP payload: WebRTC audio engine embedded —
WebRtcNsx_ProcessCore/WebRtcNsx_CalcParam(noise suppression) andWebRtcAgc_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_0000relocation 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, not0x082000. The dump has a dense table of 256 entries"Key 1"…"Key 256"on a 48-byte stride starting0x0D0002(block0x0D0000is 10.6 % full, 6,948 data bytes). Thedmr_keysregion0x082000is entirely0xFF. Soconstants.py'sdmr_keysaddr looks stale/wrong for V3.25 — the real key store lives in the0x0D0000bank. (Note the §4.3 write-opcode table followsconstants.pyand lists0x95 → 0x082000; that is the protocol region id, but this live dump shows the actual populated key names sit at0x0D0000— reconcile before writing keys.) - The region
constants.pylabelszones@0x01C000actually contains 48-byte channel-format records (same header/frequency layout as the channels region), not the 512-byteZONE_SIZEstructures it defines. The 512-byte zone-record assumption does not match this firmware's on-flash layout at0x01C000.
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 00look like table lengths = 5,5,10,5).- Repeating
0x80-tagged sub-blocks (0x000020,0x000090, …) delimit per-band groups; each carries an identical 7-byte tuple12 E2 34 40 2A 4F B3that 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×8then19×8,2D×8then28×8) are almost certainly High/Low power DAC pairs; the rising-then-clamped curves (4B 4C 4D 4E …) are TX power vs. frequency; the plateau2D×16blocks 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"at0x0D0000— 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:
0x100000onward and0x150000–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 Cortexvariant, little-endian, Thumb (ARM:LE:32:Cortexin Ghidra). The image is pure Thumb (every vector is an odd address). - Two equivalent import routes:
- Load the Intel-HEX
rt4d_stock_v3.25.ihex— it carries absolute addresses (base0x08000000), so Ghidra/IDA places the app at0x08002800automatically. Preferred. - Load the raw bin
rt4d_stock_v3.25_abs_0x08002800.binwith load/image base =0x08002800(not0x08000000— the bootloader is not in this file).
- Load the Intel-HEX
- Memory blocks to define manually (the bin/ihex only covers flash): create RAM
0x20000000size0x20000(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, GPIO0x40020000, 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 SP0x2000AE48, word1 = Reset0x08002AC1. - Force-disassemble the reset handler at
0x08002AC0(Thumb, clear bit 0), then followSystemInit @ 0x0801DA2Candmain @ 0x080029E0. - Define the exception/IRQ table entries from §2 (SVCall
0x0801A845, PendSV0x08018C1D, and the active ISRs: ADC0x08002D1D, TIM10x0801DBB1, TIM20x0801DDCD, USART30x080205B1, USART60x0802061D). Set VTOR =0x08000000mentally, but note the app table is used post-boot.
- Vector table at
- Bootloader/app split: the bootloader (
0x08000000–0x080027FF) is not in these files. Treat0x08002800as the app entry; the0x39/0x57/0xFFbootloader flash protocol (checksum seed0x48) 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 Chip0x08014950, remote kill/stun strings,Calibration OK!0x08019C87) and the serial dispatcher0x08019790/ framer0x0801F864.
FM100B baseband
- Language / processor: ARM little-endian, ARM mode (
ARM:LE:32:v5torv4t— classic ARM7/9-class, not Cortex). LoadFM100B_V1.2.0.32_20260130.binat base0x00000000. - Vector table at
0x0(B reset + 7×LDR pc,[pc,#0x14]); reset trampoline at0x40. Define the literal-pool handler pointers at0x20–0x38. - Relocation region: create a second memory block at
0x03800000(the handler/relocation window0x03800000–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.binin a hex editor / the CPS. Use the §6.7 offset map andrt4d-cps/rt4d_codeplug/constants.py(with the two corrections in §6.2) to parse regions.
Prioritized next steps
- Back up the radio first (see safety note). Read all of SPI flash — especially the 4 KB calibration block at
0x000000— before touching anything. - 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–0x08002800while you have SWD. - Map the serial engine. Xref the framer
0x0801F864and dispatcher0x08019790; fully enumerate the region-writetbbtable at0x8018A2Eto document the undocumented0x9C..0xA5write opcodes and the0xABCDmagic-gated handler (0x801AD9C) — these are unexplored by the community CPS. - Trace the
0x34/0x54/0x58mode-select flags (0x20000c57) to understand the single-bank vs dual-bank flash-write personalities before writing any region from a custom tool. - 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. - 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 TXenforcement; assess whether stun/kill can be disabled or spoofed. - Reverse the encryption implementation from
Encryption Set0x080165D0→ locate the cipher/type table (not stringed) and the key store; reconcile the key-name table location discrepancy (0x0D0000live vs0x082000in constants). - Map the FM100B message interface (§5.4): pair each
ATC_*Reqthe 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 theUpdate DMR Chip(0x08014950) reflash. - 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:
- Read and archive the full 4 MB SPI dump (
radio-spi-dump.binis one such capture) and separately verify the first0x1000bytes are non-0xFF(a valid calibration block is 100% populated per §6.3). - Never issue a full-chip erase or a bulk write that spans
0x000000. The0x40region write erases a 4 KB sector at offset 0 — treat it as off-limits unless you are deliberately restoring a verified backup. - When modding the codeplug only, restrict writes to
0x002000–0x0D6FFF(settings/channels/zones/contacts/keys). Everything at0x100000+is stock, regenerable firmware assets. - For MCU or FM100B firmware flashing, keep the stock vendor images (
rt4d_stock_v3.25*,FM100B_V1.2.0.32_20260130.bin) on hand for rollback, and confirm you can reach the bootloader (hold*at power-on,0xFFprobe echoes0xFF) before erasing, so a failed flash is recoverable.