- Полный 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>
18 KiB
RT-4D — Codeplug & CPS-Compatibility Boundary (key: codeplug)
Scope. This file defines the compatibility boundary for a UI rewrite: the on-SPI codeplug
format and the PC/CPS serial protocol that MUST stay byte-identical, and the firmware functions a
rewritten UI MUST reuse so it never re-implements (and never diverges from) the stock format. All
vaddrs are absolute in the MCU application image (Thumb, load base 0x08002800,
rt4d_stock_v3.25_abs_0x08002800.bin). Cross-checked against rt4d-cps/rt4d_codeplug/constants.py
models.py+parser.py, and against the liveradio-spi-dump.bin.
TL;DR for the rewrite: do not touch the SPI region layout, the record structs, the settings dual-bank/
0xABCDmagic, or the USART60x34/0x52/region-idserial dispatcher. Do call the firmware's SPI primitives and the per-record read/save wrappers listed in §5. If you keep those two invariants, the stock Radtel CPS keeps working unchanged.
1. Low-level SPI flash primitives (the mandatory bottom layer — REUSE)
These four are the only functions that actually touch the external SPI data-flash. Everything the UI or CPS does goes through them. A rewritten UI should call the record wrappers (§5) rather than these directly, but they are documented because the wrappers and the CPS dispatcher both depend on them and they define the on-flash addressing.
| vaddr | signature | what it does | evidence |
|---|---|---|---|
0x08021828 |
void spi_flash_read(void *dst, uint32 addr, uint32 len) |
CS low; sends opcode 0x03; sends 24-bit addr (32-bit if chip-id byte==0x18/0x19); streams len bytes to dst; CS high. |
opcode movs r0,#3@0x08021838; addr-width branch on #0x18/#0x19; byte-loop via 0x8021538/0x8021580. |
0x08021924 |
void spi_flash_erase4k(uint32 sector_idx) |
addr = sector_idx << 12; WREN; opcode 0x20 (4 KB sector erase); waits WIP. |
lsls r4,#0xc@0x08021928; movs r0,#0x20@0x0802193a; poll 0x8021984. |
0x08021a70 |
void spi_flash_program(uint32 addr, const void *src, uint32 len) |
Page-program respecting 256-byte page boundaries; loops calling spi_page_program(0x080217b8) per page. |
rsb r5,r0,#0x100 page-split@0x08021a7c; page loop. |
0x080217b8 |
void spi_page_program(uint32 addr, const void*src, uint16 len) |
WREN + opcode 0x02 single page write (≤256 B). |
called only from 0x08021a70. |
Supporting helpers used alongside them (reuse verbatim):
| vaddr | signature | role |
|---|---|---|
0x080109de |
uint8 checksum(const void *buf, uint32 len) |
8-bit sum, seed 0. Used by CPS read/write frames and codeplug. |
0x08010540 |
void flash_write_guard_enter(void) |
Sets "flash busy" RAM flag, quiesces competing access before a region rewrite. Call before any erase/program batch. |
0x0801058c |
void flash_write_guard_exit(void) |
Clears the busy flag. Call after. |
0x08013738 |
void memset8(void*dst,int val,uint len, ...) |
zero/fill helper used to clear work areas before assembling a record. |
0x080060ac |
bool memcmp_eq(const void*a,const void*b,uint16 len) |
equality compare (used for magic/marker checks). |
Chip-ID gate. A RAM byte holds the SPI JEDEC id; ==0x18(16 MB)/0x19(32 MB) switches to
4-byte addressing and enables the 0xA4 addressbook / large regions. A UI rewrite must not assume a
fixed flash size — always go through spi_flash_read/erase/program, which handle this.
2. SPI codeplug region map — DO NOT CHANGE (format is CPS-visible)
Region base addresses and record sizes as they appear in the firmware's own read/write code
(the authoritative on-flash layout). The CPS region_id column is the byte the CPS sends as
frame[0] for a WriteSPI; the firmware's serial dispatcher (§4) maps it to the same base.
| Region | SPI base | record | stride | count | CPS region_id | firmware access site |
|---|---|---|---|---|---|---|
| calibration (per-unit, NEVER write) | 0x000000 |
4 KB blob | — | 1 | 0x40 |
read 0x08004cbe |
| main_settings bank0 | 0x002000 |
struct (4 KB page) | — | 1 | 0x90 |
save 0x08004cb0; magic 0xABCD @+0x0C |
| settings bank1 (shadow) | 0x003000 |
struct (4 KB page) | — | 1 | (via 0x90) |
commit 0x08004b0e |
| channels | 0x004000 |
Channel | 48 (0x30) |
1024 | 0x91 |
read 0x08004f2a/0x080055a0; addr = 0x4000 + idx*48 |
| zones | 0x01C000 |
Zone (48-B chan-format recs here, see note) | 48 | 256 | 0x92 |
batch 0x08004b6a |
| contacts | 0x05C000 |
Contact | 27 (0x1B) core, 0x15 read |
~2048 | 0x93 |
find 0x08005810; addr = 0x5E000 + idx*27 |
| groups (CPS view) | 0x07C000 |
— | — | — | 0x94 |
empty in dump; runtime uses 0xC6000, see note |
| dmr_keys (CPS view) | 0x082000 |
— | — | — | 0x95 |
empty in dump; key names live at 0x0D0000, see note |
| call_log | 0x088000 |
— | — | — | 0x96 |
— |
| default_sms/msg | 0x094000 |
Message | 256 | — | 0x97 |
text read 0x08009498 |
| RX group lists (runtime) | 0x0C6000 |
GroupList | 80 (0x50) |
— | (see note) | read 0x08007426; addr = 0xC6000 + idx*80 |
| enc-key names (runtime) | 0x0D0000 |
key-slot name | 48 | 256 | — | dump-confirmed |
| global addressbook | 0x126000 |
contact | 32 (0x20) |
— | 0xA4 |
read 0x08009ba2; addr = 0x126000 + slot*32 |
| fm_settings | 0x0D6000 |
FMSettings | 4 KB | 1 | 0x99 |
batch 0x08004c48 |
| dtmf_names | 0x0C7000 |
16×16 | 16 | 16 | 0x80 |
— |
Record structs (must be emitted byte-identically — from models.py/parser.py, confirmed by firmware access)
- Channel (48 B @
0x004000 + idx*48):+0x00common flags (rx_tx bits4-5);+0x03busy-lock;+0x04mode/modulation/bandwidth bits (bit? = DIGITAL 0x00 / ANALOG 0x01);+0x05u32 LE rx_freq (MHz×100000); tx_freq follows;+0x0Epromiscuous;+0x12tot/ctdcs_select;+0x14mute_code;+0x2016-byte ASCII name (0xFF-padded). Firmware reads exactly0x30bytes. Selector:read_channel(idx)copies to the live band struct at RAM0x20002DEA + band*48(§3). - Contact (27-B stride @
0x05E000 + idx*27):+0x00type (0=Private,1=Group,2=AllCall);+0x01u32 LE DMR id (group IDs stored BCD-LE);+0x05name. Firmware reads0x15bytes. The lookup at0x08005810matches on[+1..4]==id && [+0]==type. - GroupList (80-B @
0x0C6000 + idx*80): header + up to 32 member u16 contact indices; member value>= 0x2710(10000) = empty. Members resolve into the 27-B contact table. - Global addressbook contact (32-B @
0x126000 + slot*32): name(16)+id, used for caller-alias resolution of incoming DMR calls that aren't in the small contacts table.
NOTE — three layout discrepancies vs
constants.py(reconcile before writing these regions):
- RX group lists: the running firmware reads them at
0x0C6000, 80-byte stride (0x08007426), not0x07C000.0x07C000is0xFFin the live dump.constants.pygroups@0x07C000(region0x94) is the CPS write region id; the firmware's live group-list store is0xC6000. If the rewrite manages RX groups, use the firmware wrapper (§5) rather than a hard-coded address.- Encryption key names live at
0x0D0000(48-B stride, "Key 1".."Key 256"), not0x082000.- Zones @
0x01C000hold 48-byte channel-format records in this firmware, not the 512-byteZONE_SIZEstructsconstants.pyassumes. Match the firmware's on-flash layout.These are firmware-vs-CPS-constant mismatches that already exist in stock; preserve stock behaviour — a rewrite must reproduce whatever the stock firmware reads/writes, so the CPS round-trips unchanged.
3. In-RAM state the UI reads/writes, and the save-to-SPI path
The UI never re-reads SPI on every draw; it keeps a small set of "live" structs in SRAM and lazily commits them. A rewritten UI must use these same RAM structures so the save path stays correct.
| RAM addr | contents | notes |
|---|---|---|
0x20002014 |
main_settings working copy (the RadioSettings struct, ~4 KB image of 0x002000) |
Field offsets = models.py RadioSettings offsets (e.g. +0x60 LED on/off, +0x85 display mode A, +0x184 remote_control). Magic 0xABCD sits at +0x0C when valid. |
0x20002120 |
freq-lock / channel-limit sub-block of settings | referenced with 0x20002014 at boot (0x08005f1e). |
0x20002DBB |
VFO/UI state — byte +0x01 = active band A/B (0/1), drives which live channel struct is used |
read at every channel access (0x08004f30, 0x080055a4). |
0x20002DEA |
live Channel struct, band A (48 B); band B at 0x20002E1A (+band*48) |
filled by read_channel(idx); the standby screen renders from here. |
0x200029bb / 0x200029d7 |
additional settings mirror fields (channel name/id area) | e.g. 0x08004f52 reads +0x67. |
0x20000ce8 |
contacts/messaging cursor block (record count +0x04, type +0x01, band +0x06) |
used by contact/SMS browsers. |
0x20002ebe |
4 KB SPI staging buffer — every region read/program passes through here | shared by codeplug save (0x08004cb0), batch copy (0x08004ab0), contacts compact (0x080061e0) and the CPS ReadSPI/WriteSPI dispatcher (0x080188d4). |
0x2000a5c5 / 0x2000a7c5 / 0x20006f86 |
per-op scratch (channel-edit, addressbook, SMS text) | transient. |
0x200092ef |
CPS TX assembly buffer; 0x20000c64 = its write index |
do not repurpose. |
Save-to-SPI functions (REUSE — these keep the format + dual-bank correct)
| vaddr | signature | what it commits |
|---|---|---|
0x08004cb0 |
void settings_save(void) |
guard-enter → read 0x2000 into 0x20002ebe → erase sector 1 → program 0x2000 (4 KB) → guard-exit. Commits main_settings. (A prior copy from 0x20002014→0x20002ebe is done by the caller.) |
0x08004ab0 |
void codeplug_backup_to_shadow(void) |
Reads each live region (settings 0x2000, channels 0x4000+, zones 0x1C000+, contacts 0x5E000+, schedules 0xC6000+, fm 0xD6000+, …) and re-programs it to a +offset shadow copy (settings→0x3000, channels→0x10000+, zones→0x1D000/0x3E000+, contacts→0x92000+). This is the "Backing up…" path. |
0x080061e0 |
void contacts_compact(uint idx) |
Deletes contact idx by reading 0x1015 bytes, shifting records down 21 B (27-B stride preserved), erasing + reprogramming the affected 4 KB pages of the 0x5E000 region. Format-preserving contact delete. |
0x08006000 |
void <region>_erase8(void) |
erases 8 sectors from base (idx+0x126) — bulk clear used before a full-region rewrite. |
Dual-bank / 0xABCD magic. main_settings uses bank0 0x2000 (live) with a shadow at 0x3000.
Validity is gated by the 0xABCD magic word at settings offset 0x0C (byte pattern CD AB @
0x00200C, matches the live dump). constants.py additionally references a DTCN
marker at +0xFFC for beta41+ A/B-bank selection; no DTCN string exists in v3.25 firmware code
— that marker is a CPS-side/beta convention. For v3.25 the rewrite must (a) keep writing 0xABCD at
+0x0C, (b) keep the bank0/0x2000 primary + 0x3000 shadow arrangement, and (c) go through
settings_save/codeplug_backup_to_shadow rather than hand-rolling the bank logic. Do not change
the magic value or its offset or the CPS/stock loader will treat settings as invalid.
4. CPS serial protocol handler (USART6) — DO NOT CHANGE, MUST STAY REACHABLE
The stock Radtel CPS talks to main's USART6 link. A UI rewrite must leave this entire path intact
and reachable (i.e. keep entering "PC Programming" mode and keep the ISR + framer + dispatcher wired).
| stage | vaddr | role |
|---|---|---|
| USART6 RX ISR | 0x0802061c |
pushes bytes into ring buffer (data 0x20007ddb, head/tail 0x20000c5c/0x20000c60). |
| Frame framer | 0x0801f864 |
accepts first byte only if in {0x34, 0x40, 0x52, 0x90..0xA5}; computes length (0x34→5, 0x52→4, writes→0x404=1028); verifies trailing sum checksum (0x80109de, seed 0); copies validated frame to 0x200092ef; hands to dispatcher. |
| Top dispatcher | 0x08019790 |
on 0x34: sub frame[3] = 0x10 Notify(→0x06) / 0x54,0x58 enter-SPI-mode / 0xEE Close→NVIC_SystemReset (0x801a38c). 0x34 guard compares a word vs 0xABCD (0x08019888) → secondary handler 0x801ad9c. else → region handler. |
| Region R/W handler | 0x080188d4 |
0x52 ReadSPI: `block=(f[1]<<8) |
Confirmed opcodes (must remain byte-for-byte): 0x34/0x10 notify→0x06; 0x34/0x54,0x34/0x58
enter SPI mode; 0x34/0xEE close→reboot; 0x52 read 1 KB; 0x40,0x90–0x9A,0x9C–0xA5 region
writes (4 KB erase + 1 KB program); 0xA4 addressbook. Checksum = 8-bit sum seed 0 over all-but-last
byte. The rewrite must not alter 0x080188d4, 0x0801f864, 0x08019790, the USART6 ISR, or the
buffers 0x20002ebe/0x200092ef/0x20000c5c..64. Since the CPS reads/writes raw SPI blocks, as
long as the on-flash format (§2) is preserved, the CPS is automatically compatible.
5. Per-record READ/SAVE wrappers a rewritten UI should call (REUSE list)
These are the format-safe entry points. Signatures are inferred from register usage; RAM destinations are where stock leaves the decoded record for the UI to render/edit.
| vaddr | inferred signature | behaviour |
|---|---|---|
0x08004f20 (in-fn) / 0x080055a0 (in-fn) |
void read_channel(uint16 idx) |
spi_flash_read(&liveChan[band], 0x4000 + idx*48, 48); band = *(u8*)0x20002DBC. Dest 0x20002DEA + band*48. |
0x08005810 |
bool find_contact_by_id(uint32 id, uint8 type, char *out_name16) |
scans 0x5E000 (27-B stride); on [+1..4]==id && [+0]==type copies 16-B name to out. Returns found. |
0x08009b90 (in-fn) |
void read_addressbook_contact(uint slot, ...) |
spi_flash_read(0x2000a7c5, 0x126000 + slot*32, 32). Global addressbook (32-B). |
0x08007404 |
void read_group_list(uint16 idx) |
spi_flash_read(sp+0xA4, 0xC6000 + idx*80, 80); then resolves each u16 member (<0x2710) into a contact via 0x5E000 stride. |
0x08009480 (in-fn) |
void read_message(...) |
reads 0xC8(200) bytes of message text into 0x20006f86. |
0x08004cb0 |
void settings_save(void) |
commit main_settings (see §3). |
0x080061e0 |
void contacts_compact(uint idx) |
format-safe contact delete (see §3). |
0x08004ab0 |
void codeplug_backup_to_shadow(void) |
full backup path (see §3). |
0x08003254 (in-fn) |
channel-edit read: spi_flash_read(0x2000a5c5+0x39, 0x4000+idx*48, 0x91) |
used by the channel editor; note it reads 0x91 (>48) into a wider edit scratch. |
(“in-fn” = the read/save is an inline sequence inside a larger UI handler; the wrapper boundary is
the containing function’s entry. When rewriting, either call the containing handler or replicate the
exact spi_flash_* calls with the addresses/strides above.)
6. DO-NOT-CHANGE vs REUSE — the CPS-safe contract
DO NOT CHANGE (format & protocol — CPS-visible)
- SPI region bases & record strides in §2 (channels
0x4000/48, contacts0x5E000/27, zones0x1C000/48, settings0x2000, group-lists0xC6000/80, addressbook0x126000/32, msgs0x94000, fm0xD6000, dtmf-names0xC7000, enc-key-names0xD0000). - Record field layouts (freq = u32 LE
MHz×100000@ channel+0x05; contact type@+0, id@+1LE; 16-B0xFF-padded names; contact-type enum 0/1/2; power/mode/scan enums). - Settings dual-bank +
0xABCDmagic @ offset0x0C(bank00x2000/ shadow0x3000). - Calibration block
0x000000(4 KB) — read-only, per-unit, never erase/write. - USART6 CPS protocol: framer
0x0801f864, dispatcher0x08019790, region handler0x080188d4, ISR0x0802061c; opcodes0x34(/0x10/0x54/0x58/0xEE),0x52,0x40/0x90..0xA5,0xA4; sum-seed-0 checksum; buffers0x20002ebe/0x200092ef/0x20000c5c..64.
REUSE (call these; don't re-implement)
- SPI primitives:
spi_flash_read 0x08021828,spi_flash_erase4k 0x08021924,spi_flash_program 0x08021a70,spi_page_program 0x080217b8,checksum 0x080109de,flash_write_guard_enter 0x08010540/_exit 0x0801058c. - Record wrappers (§5):
read_channel,find_contact_by_id 0x08005810,read_group_list 0x08007404,read_addressbook_contact,read_message,settings_save 0x08004cb0,contacts_compact 0x080061e0,codeplug_backup_to_shadow 0x08004ab0. - Live RAM structs (§3):
settings 0x20002014,vfo/band 0x20002DBB,live channel 0x20002DEA(+band*48).
Contract: if the rewritten UI (a) mutates only the §3 RAM structs and calls the §5 save wrappers, and (b) leaves the §4 serial path untouched, then the on-SPI bytes remain exactly what stock produces, and the stock Radtel CPS reads/writes the radio identically. That is the CPS-safe boundary.
7. Confidence / open items
- High confidence: SPI primitives (§1) — fully disassembled, opcodes/addr-width proven.
Channel (48@
0x4000), contact (27@0x5E000) strides — proven from live read sites and dump. CPS dispatcher0x080188d40x52/0xA4paths — fully decoded and matchconstants.py. Settings staging/save via0x20002ebe→0x2000— proven. - Medium confidence: exact
settings_savecaller that copies0x20002014→0x20002ebe(the copy is in the menu handler, not shown here);codeplug_backup_to_shadowfull region list (decoded through contacts; tail regions inferred from the loop bases). - To verify before writing group-lists / keys: the
0xC6000group-list vs0x07C000CPS region and0xD0000key-names vs0x082000— stock firmware andconstants.pydisagree; a round-trip test with the stock CPS is the safest confirmation. Reuse the firmware wrappers to stay stock-correct.