Files
rt-4d/docs/subsystems/codeplug.md
T
viktorиClaude Opus 4.8 ae36c3b729 RT-4D: реверс прошивки, русификация, кастомный UI, флешеры
- Полный 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>
2026-07-08 15:47:22 +09:00

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 live radio-spi-dump.bin.

TL;DR for the rewrite: do not touch the SPI region layout, the record structs, the settings dual-bank/0xABCD magic, or the USART6 0x34/0x52/region-id serial dispatcher. Do call the firmware's SPI primitives and the per-record read/save wrappers listed in §5. If you keep those two invariants, the stock Radtel CPS keeps working unchanged.


1. Low-level SPI flash primitives (the mandatory bottom layer — REUSE)

These four are the only functions that actually touch the external SPI data-flash. Everything the UI or CPS does goes through them. A rewritten UI should call the record wrappers (§5) rather than these directly, but they are documented because the wrappers and the CPS dispatcher both depend on them and they define the on-flash addressing.

vaddr signature what it does evidence
0x08021828 void spi_flash_read(void *dst, uint32 addr, uint32 len) CS low; sends opcode 0x03; sends 24-bit addr (32-bit if chip-id byte==0x18/0x19); streams len bytes to dst; CS high. opcode movs r0,#3@0x08021838; addr-width branch on #0x18/#0x19; byte-loop via 0x8021538/0x8021580.
0x08021924 void spi_flash_erase4k(uint32 sector_idx) addr = sector_idx << 12; WREN; opcode 0x20 (4 KB sector erase); waits WIP. lsls r4,#0xc@0x08021928; movs r0,#0x20@0x0802193a; poll 0x8021984.
0x08021a70 void spi_flash_program(uint32 addr, const void *src, uint32 len) Page-program respecting 256-byte page boundaries; loops calling spi_page_program(0x080217b8) per page. rsb r5,r0,#0x100 page-split@0x08021a7c; page loop.
0x080217b8 void spi_page_program(uint32 addr, const void*src, uint16 len) WREN + opcode 0x02 single page write (≤256 B). called only from 0x08021a70.

Supporting helpers used alongside them (reuse verbatim):

vaddr signature role
0x080109de uint8 checksum(const void *buf, uint32 len) 8-bit sum, seed 0. Used by CPS read/write frames and codeplug.
0x08010540 void flash_write_guard_enter(void) Sets "flash busy" RAM flag, quiesces competing access before a region rewrite. Call before any erase/program batch.
0x0801058c void flash_write_guard_exit(void) Clears the busy flag. Call after.
0x08013738 void memset8(void*dst,int val,uint len, ...) zero/fill helper used to clear work areas before assembling a record.
0x080060ac bool memcmp_eq(const void*a,const void*b,uint16 len) equality compare (used for magic/marker checks).

Chip-ID gate. A RAM byte holds the SPI JEDEC id; ==0x18(16 MB)/0x19(32 MB) switches to 4-byte addressing and enables the 0xA4 addressbook / large regions. A UI rewrite must not assume a fixed flash size — always go through spi_flash_read/erase/program, which handle this.


2. SPI codeplug region map — DO NOT CHANGE (format is CPS-visible)

Region base addresses and record sizes as they appear in the firmware's own read/write code (the authoritative on-flash layout). The CPS region_id column is the byte the CPS sends as frame[0] for a WriteSPI; the firmware's serial dispatcher (§4) maps it to the same base.

Region SPI base record stride count CPS region_id firmware access site
calibration (per-unit, NEVER write) 0x000000 4 KB blob 1 0x40 read 0x08004cbe
main_settings bank0 0x002000 struct (4 KB page) 1 0x90 save 0x08004cb0; magic 0xABCD @+0x0C
settings bank1 (shadow) 0x003000 struct (4 KB page) 1 (via 0x90) commit 0x08004b0e
channels 0x004000 Channel 48 (0x30) 1024 0x91 read 0x08004f2a/0x080055a0; addr = 0x4000 + idx*48
zones 0x01C000 Zone (48-B chan-format recs here, see note) 48 256 0x92 batch 0x08004b6a
contacts 0x05C000 Contact 27 (0x1B) core, 0x15 read ~2048 0x93 find 0x08005810; addr = 0x5E000 + idx*27
groups (CPS view) 0x07C000 0x94 empty in dump; runtime uses 0xC6000, see note
dmr_keys (CPS view) 0x082000 0x95 empty in dump; key names live at 0x0D0000, see note
call_log 0x088000 0x96
default_sms/msg 0x094000 Message 256 0x97 text read 0x08009498
RX group lists (runtime) 0x0C6000 GroupList 80 (0x50) (see note) read 0x08007426; addr = 0xC6000 + idx*80
enc-key names (runtime) 0x0D0000 key-slot name 48 256 dump-confirmed
global addressbook 0x126000 contact 32 (0x20) 0xA4 read 0x08009ba2; addr = 0x126000 + slot*32
fm_settings 0x0D6000 FMSettings 4 KB 1 0x99 batch 0x08004c48
dtmf_names 0x0C7000 16×16 16 16 0x80

Record structs (must be emitted byte-identically — from models.py/parser.py, confirmed by firmware access)

  • Channel (48 B @ 0x004000 + idx*48): +0x00 common flags (rx_tx bits4-5); +0x03 busy-lock; +0x04 mode/modulation/bandwidth bits (bit? = DIGITAL 0x00 / ANALOG 0x01); +0x05 u32 LE rx_freq (MHz×100000); tx_freq follows; +0x0E promiscuous; +0x12 tot/ctdcs_select; +0x14 mute_code; +0x20 16-byte ASCII name (0xFF-padded). Firmware reads exactly 0x30 bytes. Selector: read_channel(idx) copies to the live band struct at RAM 0x20002DEA + band*48 (§3).
  • Contact (27-B stride @ 0x05E000 + idx*27): +0x00 type (0=Private,1=Group,2=AllCall); +0x01 u32 LE DMR id (group IDs stored BCD-LE); +0x05 name. Firmware reads 0x15 bytes. The lookup at 0x08005810 matches on [+1..4]==id && [+0]==type.
  • GroupList (80-B @ 0x0C6000 + idx*80): header + up to 32 member u16 contact indices; member value >= 0x2710(10000) = empty. Members resolve into the 27-B contact table.
  • Global addressbook contact (32-B @ 0x126000 + slot*32): name(16)+id, used for caller-alias resolution of incoming DMR calls that aren't in the small contacts table.

NOTE — three layout discrepancies vs constants.py (reconcile before writing these regions):

  1. RX group lists: the running firmware reads them at 0x0C6000, 80-byte stride (0x08007426), not 0x07C000. 0x07C000 is 0xFF in the live dump. constants.py groups@0x07C000 (region 0x94) is the CPS write region id; the firmware's live group-list store is 0xC6000. If the rewrite manages RX groups, use the firmware wrapper (§5) rather than a hard-coded address.
  2. Encryption key names live at 0x0D0000 (48-B stride, "Key 1".."Key 256"), not 0x082000.
  3. Zones @0x01C000 hold 48-byte channel-format records in this firmware, not the 512-byte ZONE_SIZE structs constants.py assumes. Match the firmware's on-flash layout.

These are firmware-vs-CPS-constant mismatches that already exist in stock; preserve stock behaviour — a rewrite must reproduce whatever the stock firmware reads/writes, so the CPS round-trips unchanged.


3. In-RAM state the UI reads/writes, and the save-to-SPI path

The UI never re-reads SPI on every draw; it keeps a small set of "live" structs in SRAM and lazily commits them. A rewritten UI must use these same RAM structures so the save path stays correct.

RAM addr contents notes
0x20002014 main_settings working copy (the RadioSettings struct, ~4 KB image of 0x002000) Field offsets = models.py RadioSettings offsets (e.g. +0x60 LED on/off, +0x85 display mode A, +0x184 remote_control). Magic 0xABCD sits at +0x0C when valid.
0x20002120 freq-lock / channel-limit sub-block of settings referenced with 0x20002014 at boot (0x08005f1e).
0x20002DBB VFO/UI state — byte +0x01 = active band A/B (0/1), drives which live channel struct is used read at every channel access (0x08004f30, 0x080055a4).
0x20002DEA live Channel struct, band A (48 B); band B at 0x20002E1A (+band*48) filled by read_channel(idx); the standby screen renders from here.
0x200029bb / 0x200029d7 additional settings mirror fields (channel name/id area) e.g. 0x08004f52 reads +0x67.
0x20000ce8 contacts/messaging cursor block (record count +0x04, type +0x01, band +0x06) used by contact/SMS browsers.
0x20002ebe 4 KB SPI staging buffer — every region read/program passes through here shared by codeplug save (0x08004cb0), batch copy (0x08004ab0), contacts compact (0x080061e0) and the CPS ReadSPI/WriteSPI dispatcher (0x080188d4).
0x2000a5c5 / 0x2000a7c5 / 0x20006f86 per-op scratch (channel-edit, addressbook, SMS text) transient.
0x200092ef CPS TX assembly buffer; 0x20000c64 = its write index do not repurpose.

Save-to-SPI functions (REUSE — these keep the format + dual-bank correct)

vaddr signature what it commits
0x08004cb0 void settings_save(void) guard-enter → read 0x2000 into 0x20002ebe → erase sector 1 → program 0x2000 (4 KB) → guard-exit. Commits main_settings. (A prior copy from 0x200020140x20002ebe 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,0x900x9A,0x9C0xA5 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 functions entry. When rewriting, either call the containing handler or replicate the exact spi_flash_* calls with the addresses/strides above.)


6. DO-NOT-CHANGE vs REUSE — the CPS-safe contract

DO NOT CHANGE (format & protocol — CPS-visible)

  1. SPI region bases & record strides in §2 (channels 0x4000/48, contacts 0x5E000/27, zones 0x1C000/48, settings 0x2000, group-lists 0xC6000/80, addressbook 0x126000/32, msgs 0x94000, fm 0xD6000, dtmf-names 0xC7000, enc-key-names 0xD0000).
  2. Record field layouts (freq = u32 LE MHz×100000 @ channel +0x05; contact type@+0, id@+1 LE; 16-B 0xFF-padded names; contact-type enum 0/1/2; power/mode/scan enums).
  3. Settings dual-bank + 0xABCD magic @ offset 0x0C (bank0 0x2000 / shadow 0x3000).
  4. Calibration block 0x000000 (4 KB) — read-only, per-unit, never erase/write.
  5. USART6 CPS protocol: framer 0x0801f864, dispatcher 0x08019790, region handler 0x080188d4, ISR 0x0802061c; opcodes 0x34(/0x10/0x54/0x58/0xEE), 0x52, 0x40/0x90..0xA5, 0xA4; sum-seed-0 checksum; buffers 0x20002ebe/0x200092ef/0x20000c5c..64.

REUSE (call these; don't re-implement)

  • SPI primitives: spi_flash_read 0x08021828, spi_flash_erase4k 0x08021924, spi_flash_program 0x08021a70, spi_page_program 0x080217b8, checksum 0x080109de, flash_write_guard_enter 0x08010540 / _exit 0x0801058c.
  • Record wrappers (§5): read_channel, find_contact_by_id 0x08005810, read_group_list 0x08007404, read_addressbook_contact, read_message, settings_save 0x08004cb0, contacts_compact 0x080061e0, codeplug_backup_to_shadow 0x08004ab0.
  • Live RAM structs (§3): settings 0x20002014, vfo/band 0x20002DBB, live channel 0x20002DEA(+band*48).

Contract: if the rewritten UI (a) mutates only the §3 RAM structs and calls the §5 save wrappers, and (b) leaves the §4 serial path untouched, then the on-SPI bytes remain exactly what stock produces, and the stock Radtel CPS reads/writes the radio identically. That is the CPS-safe boundary.


7. Confidence / open items

  • High confidence: SPI primitives (§1) — fully disassembled, opcodes/addr-width proven. Channel (48@0x4000), contact (27@0x5E000) strides — proven from live read sites and dump. CPS dispatcher 0x080188d4 0x52/0xA4 paths — fully decoded and match constants.py. Settings staging/save via 0x20002ebe0x2000 — proven.
  • Medium confidence: exact settings_save caller that copies 0x200020140x20002ebe (the copy is in the menu handler, not shown here); codeplug_backup_to_shadow full region list (decoded through contacts; tail regions inferred from the loop bases).
  • To verify before writing group-lists / keys: the 0xC6000 group-list vs 0x07C000 CPS region and 0xD0000 key-names vs 0x082000 — stock firmware and constants.py disagree; a round-trip test with the stock CPS is the safest confirmation. Reuse the firmware wrappers to stay stock-correct.