Баузбенд 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
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/3base (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 helperspi_xfer_byte @0x08021538drives codeplug/calibration/font reads (opcode0x03, 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:
- the ATC message layer (
atc_send/atc_send_pl+ ~25 typed wrappers), and - 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:
0x20007476is a per-msg_idarray of confirm flags; before send,[msg_id]←0xFF; the USART3 RX handler (atc_rx @0x08006D00region) 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 FM100BATC_*symbol list. Treat the ★ ones (0x82freq/mode,0x06call,0x2Aradio-id,0x62enable) 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(msg0x06). - The band-select / PA-enable GPIO is GPIOA pin 10 (
0x40020000, BSRR+0x18/+0x28), driven by band flag0x20000C34(set inatc_channel_setwhen 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, thenbatt = (adc_raw << 2) / 0x42→ stored at0x200008B0(accumulator raw0x2000089C). Divisor0x42(66) ⇒ result is in 0.1 V units (feeds theBATT:x.xVstring). Callers:0x08012816(boot/about),0x0801E288(periodic/low-batt check).- ADC helpers:
adc_sw_start @0x08020B4C(port,en), ADC ISR@0x08002D1Caccumulates into0x2000089C. - 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 the0x09/query family) and lands in an RX-decoded RAM struct.
7.2 Audio (DAC / codec)
- DAC control at
0x40007400:dac_enable_chX @0x08020F90 / 0x08020FA4 / 0x08020FE0toggle 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_volmsg 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). Opcode0x03, CS on GPIOB; handles 3-byte vs 4-byte addressing (chip-id0x18/0x19at0x20000C1C). This reads the calibration block at SPI0x000000, 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
0x000000is per-unit factory data that the FM100B consumes (itsSPCaliFreqSetCnf/SPCali_*OptNV 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) |
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
0x200029BBwhose layout equalsrt4d_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-idframing). 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.