Files
rt-4d/docs/subsystems/radio.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

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

RT-4D RF Control API (radio key)

Reverse-engineering of the RF transceiver control path in the RT-4D stock application firmware (rt4d_stock_v3.25_abs_0x08002800.bin, ARM Cortex-M4F Thumb, vaddr base 0x08002800). All addresses are absolute vaddr. This document is an API reference for rewriting the UI while reusing the stock RF/DMR functions, and it respects the hard boundary: it does not touch the SPI codeplug format or the serial/CPS protocol.


0. TL;DR — the single most important architectural fact

There is NO discrete RF transceiver chip driven by the MCU. The RT-4D has no AT1846S / RDA1846 / SA828-class analog transceiver on an MCU-side I²C/SPI bus. Confirmed:

  • Zero I²C hardware — no I2C1/2/3 base (0x40005400/5800/5C00) literal anywhere in the image; no bit-banged AT1846S register-write helper (reg = (addr<<... ), 3-byte I²C write) exists.
  • SPI2 (0x40003800) is the external data-flash bus only — its byte-transfer helper spi_xfer_byte @0x08021538 drives codeplug/calibration/font reads (opcode 0x03, CS on GPIOB); it never talks to an RF PLL.
  • The entire radio transceiver — synthesiser, RX demod (FM/AM/SSB), TX modulator, AMBE vocoder, RSSI, CTCSS/DCS, squelch — lives inside the FM100B baseband SoC. The MCU controls all of it by sending a small binary "ATC" request protocol over USART3 (0x40004800) and blocking for the confirm.

Therefore the "RF control API we must reuse" is:

  1. the ATC message layer (atc_send / atc_send_pl + ~25 typed wrappers), and
  2. a handful of MCU-local helpers for things physically wired to the MCU: battery ADC, audio DAC/codec enable, band-select GPIO, PA/CS GPIO, and the FM100B reset/hard-reset line.

The UI rewrite should call the high-level composite functions (radio_apply_channel, ptt_tx_start, battery_read) and the FM100B is reprogrammed transparently. Frequencies flow from the codeplug (unchanged format) through a RAM mirror into these functions — you never re-tune calibration.


1. RF chip identity & the transport bus

Item Finding Evidence
RF transceiver Integrated in FM100B baseband SoC (Kirisun-derived DMR chip; ARM7/9-class, WebRTC DSP + AMBE). Not an MCU-side chip. No I²C base; RF config only appears as USART3 ATC payloads; FM100B strings ATC_ChFreqSetReq/ATC_SetRfPowerLevelReq/ATC_RssiReadReq (RE report §5.4).
Bus MCU↔FM100B USART3 @ 0x40004800, byte-oriented, IRQ-driven RX (ISR 0x0802061C? — actually 0x080205B0), polled TX. usart3_tx_byte @0x08006CB8 loads 0x40004800; ISR pushes to ring 0x200082EF.
Bus MCU↔SPI-flash SPI2 0x40003800 (codeplug/cal/fonts) — not RF. spi_xfer_byte @0x08021538.
PC/CPS link USART6 0x40011400 — untouched, keep as-is. RE report §4.

1.1 USART3 low-level primitives (raw byte I/O to FM100B)

vaddr signature what it does
0x08021EA8 u16 usart_read_dr(u32 port) returns port->DR ([port+4])
0x08021EB0 void usart_write_dr(u32 port, u8 b) port->DR = b & 0x1FF
0x08021EC2 bool usart_flag(u32 port, u32 mask) (port->SR & mask) != 0 (RXNE=0x20, TXE=0x80)
0x08006CB8 void usart3_tx_byte(u8 b) send 1 byte to FM100B (buffers to ring 0x200092EF when flag 0x20000B67 set, else polls TXE and writes DR)
0x08006C9C void usart3_tx_buf(u8 *buf, u16 len) send len bytes (loops usart3_tx_byte)

You will not call these directly for RF; they are the substrate under the ATC layer.


2. The ATC message layer — the core RF/DMR command API

2.1 Frame format (built in RAM buffer 0x2000706E)

off  field
 0   0x68                 frame start / sync
 1   msg_id               (see §3 table)
 2   arg1                 (byte)
 3   arg2                 (byte)
 4   hdr_checksum (BE16)  computed by chk @0x08002EA8, byte-swapped @0x0800BD2C
 6   payload_len (BE16)   0 for the no-payload variant
 8   arg3 / payload[0..]  (payload copied here by memcpy @0x080062EC)
 8+len  0x10              trailer subtype marker
 ...   checksum

Then usart3_tx_buf(&frame, 8+len+…) is called and the sender blocks on the confirm.

2.2 The two core senders (CALLABLE, but you normally call the wrappers)

vaddr signature notes
0x0801B044 void atc_send(u8 msg_id, u8 a1, u8 a2, u8 a3, u32 timeout) no-payload request. Writes frame, sends 0xA bytes, then spin-waits on cnf_flags[msg_id] @0x20007476[msg_id] becoming ≠0xFF, decrementing a timeout counter at 0x20000C52; calls scheduler 0x08003050 while waiting.
0x0801B0C4 void atc_send_pl(u8 msg_id, u8 a1, u8 a2, u8 a3, u8 *payload, u16 len, u32 timeout) payload variant (extra args on stack: [sp+0x20]=payload, [sp+0x24]=len, [sp+0x28]=timeout). Same blocking confirm-wait.
  • Confirm table: 0x20007476 is a per-msg_id array of confirm flags; before send, [msg_id]←0xFF; the USART3 RX handler (atc_rx @0x08006D00 region) writes the Cnf back and the sender unblocks. The confirm often carries the return value (e.g. RSSI, version) into the RX-decoded RAM structs.
  • timeout arg is a loop count (typ. 0x64=100, 0xBB8=3000, 0x3E8=1000).
  • Helpers: chk @0x08002EA8 (frame checksum), htons @0x0800BD2C (byte-swap16), memcpy @0x080062EC, memset @0x08006038 / 0x08002BEE / 0x08002C52.

2.3 Typed wrappers (the practical entry points)

Every wrapper is atc_send(msg_id, 1, 1, param, 0x64) unless noted (the 1,1 are fixed sub-fields). Each takes its single byte/word parameter in r0.

vaddr msg_id inferred signature inferred meaning (FM100B ATC symbol)
0x08006E6C 0x06 void atc_call_process(u8 a, u8 call_type, u32 target_id, u8 r3) Start call / key DMR TX (ATC_CallProcessReq). call_type 1=Private,2=Group,4=AllCall. Payload: type + BCD DMR-ID (via id2bcd @0x0800786C) + freq(0x20007DA9[5]) + 16-byte block.
0x08007084 0x07 void atc_w07(...) channel/slot-related set (payload built from RAM 0x2000A5FD-0x38, freq×; timeout 3000)
0x08006FD8 0x0A void atc_w0A(...) payload set (RX-related)
0x0800760A 0x02 void atc_set_call_spk_vol(u8 v) DMR called speaker volume (from settings [0x188]%25)
0x08007530 0x0B void atc_set_call_mic_gain(u8 v) DMR call MIC gain (settings [0x187]%25)
0x08007548/0x08007598 0x0C void atc_set_color_code(u8 cc) / template variant DMR color code / off-CTCSS (from [0x63])
0x080075F4 0x4D void atc_set_dig_squelch(u8 v) DMR squelch level (settings [0x193]%17)
0x080075DC 0x55 void atc_set_sms_mode(u8 v) SMS/monitor flag (ATC_SmsmodeSetReq, settings [0x196]&1)
0x08006DFC 0x25 (indirect) void atc_w_sms2(u8 v) second SMS/monitor flag (settings [0x195]&1); sends a fixed 0x1F-byte template
0x08006C4C 0x49 void atc_set_denoise(u8 tx, u8 rx) TX/RX denoise (settings [0x185],[0x186])
0x0800719C 0x49 void atc_w49b(u8 v) init-time variant (payload from an ADR const)
0x08006C88 0x25 void atc_w25(void) init handshake (atc_send(0x25,1,1,1))
0x08006C74 0x05 void atc_w05(void) atc_send(0x05,1,1,2) — init/enable
0x080071CC 0x45 void atc_w45(u8 v) init default (called with 2)
0x080075C6 0x48 void atc_w48(u8 v) init default (called with 0xF) — likely AGC/EQ default
0x08007670/0x08007620 0x4C void atc_w4C(u8 v) / template variant RX enable / mute (called with 1)
0x08007688/0x080076CC 0x09 void atc_query09(void) / atc_w09(u8) fixed 0x18-byte query/keepalive (sets 0x20000C3F busy flag)
0x080074FA 0x2A void atc_set_radio_id(u32 dmr_id) set radio's own DMR ID (ATC_RadioIDSetReq); 4-byte LE payload
0x080074D2 0x57 void atc_w57(u8 v) 2-byte set (init-time, called with 0)
0x080071E2 0x62 void atc_ch_enable(u8 rx_en, u8 tx_en) channel RX/TX wait/enable (ATC_CurChannelWaitSetReq); 2-byte payload
0x0800720C 0x82 void atc_channel_set(chan_cfg *cfg) ★ SET RX FREQ + TX FREQ + MODE + BW + CC + call-type ★ (ATC_ChannelSetReq) — see §4
0x0800736C 0x81 void atc_set_mute_code(u16 code) analog mute code / DCS value ([cfg+0x14])
0x08007404 0x84 void atc_set_rxgroup(u8 gl_index) RX group list upload (reads groups 0xC6000 stride 0x50, contacts 0x5E000 stride 0x15) = ATC_DigChGroupSetReq
0x080071E2 0x62 (see above)

Naming confidence: the msg_ids and calling conventions are certain (decoded directly). The English names are inferred from (a) the caller context in radio_apply_channel (which settings byte feeds each), (b) the payload shape, and (c) the FM100B ATC_* symbol list. Treat the ★ ones (0x82 freq/mode, 0x06 call, 0x2A radio-id, 0x62 enable) as high-confidence; the audio/denoise/squelch ones as medium-high.


3. ★ atc_channel_set @0x0800720C — the RX/TX frequency + mode setter

Signature: void atc_channel_set(chan_cfg *cfg) (msg_id 0x82, 0x14-byte payload).

cfg is a channel-parameter block (the RAM staging struct, e.g. 0x20002E7A, 0x20007DA9, or a copy of a 48-byte codeplug channel record). Field layout used by this function:

cfg off field used how
+0x00 flags byte bit1→bandwidth(+1), bit2→a flag, bits6-7→RX/TX permission (checked by caller)
+0x01 flags2 high nibble → modulation (FM/AM/SSB)
+0x05 RX freq (u32 LE, 10 Hz units = MHz×100000) rx_hz = rxfreq × 10 → 4 bytes big-endian into payload
+0x09 TX freq (u32 LE, 10 Hz units) tx_hz = txfreq × 10 → 4 bytes big-endian
+0x11 contact index (u16) reads contact rec at 0x5E000 + idx*0x15 → call type (0→1 Priv, 1→2 Grp, 2→4 All); target ID or 0xAAAAAAAA for all-call
+0x13 CTCSS/DCS select (handled by caller via 0x0C/0x84)
+0x14 mute code / DCS (u16) (caller → 0x81)

Key disassembly:

0800720c push {r4,r5,r6,lr}; r4 = cfg
08007220 ldr r0,[r4,#5]          ; RX freq (10Hz)
08007224 ldr r1,=0x16e3600       ; 24000000 = 240.00000 MHz band threshold
08007226 cmp r0,r1               ; >=240MHz -> band flag 0x20000C34 = 1 (UHF) else 0 (VHF)
0800725a add r0,r0,r0,lsl#2 ; lsls#1 → r0*10   ; convert 10Hz→Hz
...store BE at payload+0xf (RX), +0x13 (TX)...
08007340 ldrb r0,[r4,#0x14]      ; extra param
08007354 movs r0,#0x82 ; bl atc_send_pl   ; send ChannelSet

Frequency units — DEFINITIVE: codeplug stores MHz × 100000 (i.e. 10 Hz units, matches FREQ_MULTIPLIER=100000). This function multiplies by ×10 to hand the FM100B plain Hz (big-endian u32). So: payload_hz = codeplug_value × 10. Example: 43880000 (10Hz) → 438800000 Hz.

Callers (reuse these, or call atc_channel_set directly): 0x0801AEE8 (inside radio_apply_channel), 0x0801F590, 0x0801F5C0 (dual-watch/scan band re-tune).


4. ★ radio_apply_channel @0x0801AE9C — the composite "tune the radio" entry point

This is the function the new UI should call to make the radio adopt a channel. It takes the channel-config block and pushes everything (freq, mode, power/enable, color code, squelch, gains, radio-ID, CTCSS/DCS, denoise) to the FM100B in one shot, reading auxiliary values from the RAM settings mirror 0x200029BB.

Signature: void radio_apply_channel(chan_cfg *cfg) (cfg in r0).

Sequence (evidence = disassembly 0x0801AE9C0x0801B016):

if (cfg->flags>>6 == 0)  // normal RX/TX channel
    atc_query09()                     // 0x8007688  quiet/prep
    delay(0x14)                       // 0x8007946
    atc_channel_set(cfg)              // 0x800720C  ★ RX/TX freq + mode + BW
    atc_ch_enable(cfg&1, cfg&1)       // 0x80071E2  msg 0x62
    if (dmr) {
        atc_set_call_spk_vol(settings[0x188]%25)  // 0x800760A msg 0x02
        atc_set_call_mic_gain(settings[0x187]%25) // 0x8007530 msg 0x0B
        atc_set_dig_squelch(settings[0x193]%17)   // 0x80075F4 msg 0x4D
        atc_set_sms_mode(settings[0x196]&1)       // 0x80075DC msg 0x55
        atc_w_sms2(settings[0x195]&1)             // 0x8006DFC
        atc_set_color_code(cfg[0x63-region])      // 0x8007598 msg 0x0C
    } else {  // analog
        atc_set_denoise(settings[0x185], settings[0x186]) // 0x8006C4C msg 0x49
    }
    // radio ID: channel-custom (cfg+0x16) if cfg bit3 set, else settings[0x180]
    atc_set_radio_id(...)             // 0x80074FA msg 0x2A
    // CTCSS/DCS:
    if (cfg[0x13]==0) atc_set_color_code_off() // 0x8007548 msg 0x0C
    else              atc_set_rxgroup(cfg[0x13]-1) // 0x8007404 msg 0x84
    atc_set_mute_code(cfg[0x14])      // 0x800736C msg 0x81
else if (cfg->flags>>6 == 1)  // special/FM-broadcast/monitor branch
    atc_query09(); atc_w4C_tpl();     // 0x8007688, 0x8007620
    delay(0x14)
    GPIOA->BSRR = 0x4000              // band/PA GPIO bit14 set
    ... reset several RAM state bytes, call 0x801D938 (RX open) ...
    apply_backlight(settings[0x10D])  // 0x80049E4

Callers: 0x08002FE0, 0x08009F08, 0x0800B7B8, 0x0800BBD4, 0x0801F490 (channel change, VFO set, zone switch, scan). In the rewrite, call radio_apply_channel(cfg) after you populate cfg (a 48-byte codeplug channel record, or a synthesized VFO record) — the codeplug format is untouched.

RAM boundary object: 0x200029BB = RAM mirror of main_settings (SPI 0x002000). The UI reads/writes this struct (offsets match rt4d_codeplug RadioSettings, e.g. [0x188]=call spk vol, [0x193]=digital squelch, [0x180]=radio DMR-ID, [0x10D]=backlight); the RF apply reads from it. Persisting it back to SPI keeps the codeplug format intact.


5. ★ ptt_tx_start @0x08007E78 — PTT on / start TX

Signature: void ptt_tx_start(u8 mode) (mode in r0: distinguishes DMR vs analog / call-type).

Disassembly 0x08007E780x08007EFE:

08007e78 push {r4,lr}; r4=mode
         ... call-start-beep if settings[0x18d] (0x801B684) ...
08007e90 if (band_flag 0x20000C34 == 1)  GPIOA->BSRR = (1<<10)   // set  band/PA bit10  (UHF)
08007ea4 else                            GPIOA->BSRR = (1<<10)<<16 // reset band bit10  (VHF)
   switch(mode):
     0: dmr_tx(0xFF, cur_contact_id 0x20000B3C[..0x11])  // bl 0x8006D00  (DMR key)
     1: atc_call_process(1, call_type 0x20000C9E, target 0x20000CA8, 0)   // Private
     2: atc_call_process(1, 0x20000C13, 0x20000C14, 1)                    // ...
     3: atc_call_process(1, 1, 0x20000CA8, 2)                             // AllCall
08007ef8 tx_state 0x20000B6E = 3   // "transmitting"
  • DMR TX goes through dmr_tx @0x08006D00 (the USART3 DMR-record/AMBE path).
  • Analog/DMR-call TX goes through atc_call_process @0x08006E6C (msg 0x06).
  • The band-select / PA-enable GPIO is GPIOA pin 10 (0x40020000, BSRR +0x18/+0x28), driven by band flag 0x20000C34 (set in atc_channel_set when RXfreq ≥ 240 MHz).

Callers (PTT key handlers): 0x0801EC34, 0x0801ED86, 0x0801ED94, 0x0801ED9C.

PTT off / stop TX: the reverse path returns to RX by re-running the RX-open (0x0801D938) and clearing tx_state 0x20000B6E; the analog carrier key is released via atc_ch_enable/atc_w4C. For a rewrite, calling radio_apply_channel(cfg) (which re-opens RX) after dropping PTT restores RX cleanly. (A dedicated atc_call_release exists in the 0x06/0x62 family; the tx_state byte 0x20000B6E and 0x20000B73 gate it.)


6. TX power, squelch, bandwidth, CTCSS/DCS, mode — where each lives

RF parameter How it is set Function / evidence
RX frequency cfg[+5] (10 Hz) → ×10 → Hz atc_channel_set @0x0800720C (msg 0x82)
TX frequency cfg[+9] (10 Hz) → ×10 → Hz same
Mode FM/AM/SSB cfg[+1] high nibble → payload same (0x82). Values 0=FM,1=AM,2=SSB per codeplug AnalogModulation.
Bandwidth W/N cfg[+0] bit1 → payload (bit+1) same (0x82). 0=Wide/25k, 1=Narrow/12.5k.
TX power Hi/Lo carried in atc_channel_set payload flags (from codeplug byte); FM100B applies power DAC from its own NV cal via ATC_SetRfPowerLevelReq. The MCU does not compute a power DAC value — it sends the Hi/Lo level and the FM100B uses its NV calibration. msg 0x82 payload + FM100B SPCali_PowerOpt
Squelch (DMR) settings[0x193]atc_set_dig_squelch 0x080075F4 (msg 0x4D)
Squelch (analog) settings[0x102] region + atc analog SQ path analog SQ is an FM100B cal (SPCali_AnaSQthOpt); level pushed via the analog-set family
Color code atc_set_color_code 0x08007548/0x08007598 (msg 0x0C)
CTCSS/DCS cfg[+0x13] select → atc_set_rxgroup/off; cfg[+0x14] value → atc_set_mute_code 0x08007404 (0x84), 0x0800736C (0x81)
Radio DMR ID settings[0x180] or cfg[+0x16] atc_set_radio_id @0x080074FA (msg 0x2A)
MIC gain / SPK vol (DMR) settings[0x187],[0x188] 0x08007530 (0x0B), 0x0800760A (0x02)
TX/RX denoise (analog) settings[0x185],[0x186] atc_set_denoise @0x08006C4C (0x49)

Power note: because Hi/Lo maps to an FM100B-internal calibrated DAC, the UI must only pass the codeplug power byte through radio_apply_channel; it must never try to write a raw power value — that would require the per-unit calibration and risk PA damage.


7. MCU-local RF-adjacent helpers (not FM100B)

7.1 Battery voltage (ADC1)

  • battery_read @0x0801094Cvoid battery_read(void). Software-starts ADC1 (adc_sw_start @0x08020B4C, CR2.SWSTART bit30), waits up to 20 samples, then batt = (adc_raw << 2) / 0x42 → stored at 0x200008B0 (accumulator raw 0x2000089C). Divisor 0x42(66) ⇒ result is in 0.1 V units (feeds the BATT:x.xV string). Callers: 0x08012816 (boot/about), 0x0801E288 (periodic/low-batt check).
  • ADC helpers: adc_sw_start @0x08020B4C(port,en), ADC ISR @0x08002D1C accumulates into 0x2000089C.
  • RSSI is not an MCU ADC read — RSSI/signal-quality is read back from the FM100B via ATC query (ATC_RssiReadReq/ATRssiQueryCnf, msg-id in the 0x09/query family) and lands in an RX-decoded RAM struct.

7.2 Audio (DAC / codec)

  • DAC control at 0x40007400: dac_enable_chX @0x08020F90 / 0x08020FA4 / 0x08020FE0 toggle DAC CR enable/trigger bitfields (bit0/bit16/bit1/bit17). Used to gate the audio path.
  • Speaker volume for voice is largely an FM100B setting (atc_set_call_spk_vol msg 0x02, SPMicVoiceCnf); the MCU DAC is the tone/beep/analog-audio out. Amp-enable is a GPIO (see below).

7.3 Key GPIOs (for the rewrite)

GPIO purpose evidence
GPIOA (0x40020000) BSRR, bit10 (0x400) band-select / PA enable (VHF vs UHF; set on TX) 0x8007E90, 0x801F574, 0x801F5A4, 0x801AFD8 (bit14 0x4000 in special branch)
GPIOB (0x40020400) BSRR +0x28 SPI-flash CS and FM100B reset toggles spi_flash_read @0x08021828 (0x40020428), 0x8007F60 (FM100B reset, bit set/reset via 0x40020418)
delay @0x08007946(ms) busy delay used around FM100B commands/reset pervasive

7.4 SPI flash / calibration read (used by RF setup, keep format)

  • spi_flash_read @0x08021828void spi_flash_read(void *dst, u32 addr, u32 len). Opcode 0x03, CS on GPIOB; handles 3-byte vs 4-byte addressing (chip-id 0x18/0x19 at 0x20000C1C). This reads the calibration block at SPI 0x000000, channels, contacts (0x5E000), groups (0xC6000), and fonts.
  • How calibration feeds RF: the MCU does not apply RF calibration itself. The 4 KB cal block at SPI 0x000000 is per-unit factory data that the FM100B consumes (its SPCaliFreqSetCnf / SPCali_*Opt NV items) to trim VCO/PLL, TX power DAC, and squelch/RSSI thresholds. The MCU only reads cal for display/backup. The UI must reuse the stock apply path (which sends frequency + Hi/Lo level and lets the FM100B self-calibrate); it must not re-tune.

8. The reusable "set radio to F/mode/power then PTT" call sequence

For the rewritten UI, the clean, minimal sequence (all stock functions, codeplug + CPS untouched):

// 1. Build/obtain a channel-config block `cfg` (a 48-byte codeplug channel record, or a VFO
//    record you synthesize in the SAME on-flash format — do NOT change the format).
//    Set:  cfg[+5]=rx_freq_10Hz  cfg[+9]=tx_freq_10Hz
//          cfg[+0]: bit1=narrow, bits6-7=rx/tx-perm, power bit as in codeplug
//          cfg[+1]: high nibble = modulation (0 FM,1 AM,2 SSB)
//          cfg[+0x13]/[+0x14]=CTCSS-DCS select/value,  cfg[+0x11]=contact index
//    (rx/tx freq in codeplug 10 Hz units = MHz*100000)

// 2. Make sure the RAM settings mirror 0x200029BB holds the desired
//    color-code / squelch / gains / radio-ID (offsets = rt4d RadioSettings).

// 3. Push the whole channel to the FM100B (freq, mode, BW, power, CC, SQ, ID, CTCSS):
radio_apply_channel(cfg);          // 0x0801AE9C

// 4. To transmit:
ptt_tx_start(mode);                // 0x08007E78   (mode 0 = DMR, 1/2/3 = analog/call variants)
//    -> sets band GPIO (GPIOA bit10) and keys TX via atc_call_process/dmr_tx

// 5. To stop TX / return to RX:
//    clear tx_state 0x20000B6E and re-open RX; simplest robust way is:
radio_apply_channel(cfg);          // re-runs the RX-open path

If you need finer control instead of the composite, call the wrappers directly: atc_channel_set(cfg) (freq/mode/BW), atc_ch_enable(rx,tx), atc_set_color_code(cc), atc_set_dig_squelch(sq), atc_set_radio_id(id), atc_call_process(a,type,id,r3).


9. Master callable-entry-point table

vaddr name signature confidence
0x0801AE9C radio_apply_channel void(chan_cfg*) high — verified 5 callers, full body
0x08007E78 ptt_tx_start void(u8 mode) high — 4 PTT callers
0x0800720C atc_channel_set (RX/TX freq+mode+BW) void(chan_cfg*) msg 0x82 high
0x08006E6C atc_call_process (key TX / start call) void(u8 a,u8 type,u32 id,u8 r3) msg 0x06 high
0x08006D00 dmr_tx (DMR record/AMBE TX) void(u8 a, u16 contact) med-high
0x080074FA atc_set_radio_id void(u32 dmr_id) msg 0x2A high
0x080071E2 atc_ch_enable (RX/TX wait) void(u8 rx,u8 tx) msg 0x62 high
0x080075F4 atc_set_dig_squelch void(u8) msg 0x4D med-high
0x08007548/0x08007598 atc_set_color_code void(u8) msg 0x0C med-high
0x0800736C atc_set_mute_code (DCS val) void(u16) msg 0x81 med
0x08007404 atc_set_rxgroup (CTCSS/DCS/RX-group) void(u8 idx) msg 0x84 med
0x08007530 atc_set_call_mic_gain void(u8) msg 0x0B med
0x0800760A atc_set_call_spk_vol void(u8) msg 0x02 med
0x08006C4C atc_set_denoise void(u8 tx,u8 rx) msg 0x49 med
0x0801B044 atc_send void(u8 id,u8,u8,u8,u32 to) high (core)
0x0801B0C4 atc_send_pl void(u8 id,u8,u8,u8,u8*pl,u16 len,u32 to) high (core)
0x08006C9C usart3_tx_buf void(u8*,u16) high
0x08006CB8 usart3_tx_byte void(u8) high
0x0801094C battery_read void(void)0x200008B0 (0.1 V) high
0x08020B4C adc_sw_start void(u32 port,u8 en) high
0x08021828 spi_flash_read void(void*,u32 addr,u32 len) high
0x08021538 spi_xfer_byte u8(u8) on SPI2 high
0x08007946 delay_ms void(u32) high
0x08002EA8 atc_checksum u16(u8*,u16) med
0x0800786C dmr_id_to_bcd u32(u32) med

Key RAM state (the UI/RF boundary)

addr meaning
0x200029BB RAM mirror of main_settings (SPI 0x2000) — offsets = RadioSettings
0x20007DA9 ATC call/freq staging struct ([0]=type,[1..4]=id,[5..8]=freq)
0x20002E7A / 0x20002120 channel-config staging blocks (used by scan/dual-watch)
0x20007476[msg_id] ATC confirm-flag array (0xFF=pending)
0x2000706E ATC TX frame build buffer
0x20000C34 band flag (0=VHF <240 MHz, 1=UHF) → GPIOA band bit
0x20000B6E TX/call state (3 = transmitting)
0x200008B0 battery voltage (0.1 V units)
0x200008B00x2000089C battery ADC raw accumulator

10. Boundary compliance (codeplug + CPS unchanged)

  • The RF API operates on a channel-config block in the stock 48-byte codeplug format and on the RAM settings mirror 0x200029BB whose layout equals rt4d_codeplug.RadioSettings. Reusing these keeps the SPI codeplug format identical, so the stock CPS round-trips.
  • All RF programming is USART3 ATC traffic to the FM100B — completely separate from the USART6 CPS/serial protocol (0x34/0x52/region-id framing). Rewriting the UI and calling these functions changes nothing the CPS observes.
  • Calibration (SPI 0x000000) is consumed by the FM100B, not recomputed by the MCU. The rewrite reuses the stock freq/power/mode apply path, so the per-unit factory tuning is honored and never overwritten.