Мост к DMR-модулю, разбор спектра REFV DualTachyon, карта запчастей прошивки

Баузбенд FM100B:
- найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём
- расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации
- инструмент tools/fm100b.py (ping/send/raw/scan)
- cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом)
- разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд
- cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14
- исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md)

Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md):
- вызывается как функция горячей клавиши №22 Analog Spectrum
- вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318
- спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC

Декомпозиция (docs/firmware-parts.md):
- два процессора + внешний SPI = три канала внедрения
- карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode
- найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки)
- дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке
- аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля

Прочее:
- везде исправлен режим прошивки: тангента PTT вместо клавиши "*"
- устаревший Ru-4D_Flasher.exe удалён из репозитория
- добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
Этот коммит содержится в:
2026-07-22 21:17:20 +09:00
родитель ae36c3b729
Коммит 5f4d207aa5
34 изменённых файлов: 21718 добавлений и 17823 удалений
+393
Просмотреть файл
@@ -0,0 +1,393 @@
# 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.