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

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

Этот файл содержит неоднозначные символы Юникода
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `0x0801AE9C``0x0801B016`):
```
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 `0x08007E78``0x08007EFE`:
```
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 @0x0801094C``void 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 @0x08021828``void 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):
```c
// 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) |
| `0x200008B0``0x2000089C` | 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.