Баузбенд 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
23 KiB
RT-4D — DMR / FM100B interface (MCU side) — API reference (dmr)
Scope: the MCU-side code that talks to the FM100B DMR baseband over USART3 (0x40004800). All addresses are absolute in the MCU app image (rt4d_stock_v3.25_abs_0x08002800.bin, load base 0x08002800, ARM Thumb). This is the layer a rewritten UI must reuse verbatim to do DMR: originate calls, render incoming calls, set radio ID / TG / color code / slot, SMS, and handle remote stun/kill/wake.
Boundary note: none of this touches the SPI codeplug format or the CPS serial protocol — the FM100B link is a third, internal, binary UART with its own 0x68…0x10 framing. It is completely independent of the CPS 0x34/0x52/region-id framing (USART6). Reusing these functions does not change any CPS-visible format. The only codeplug coupling is read-only: caller-name display reads contact records from SPI 0x5C000/0x5E000 and the key/SMS-target table at 0x0D0000 (same layout the CPS already writes).
0. TL;DR — the callable entry points that matter
| vaddr | name (inferred) | C signature | what it does |
|---|---|---|---|
0x0801B044 |
fm100b_send1 |
void(u8 cmd,u8 b,u8 sub,u8 data,void* respbuf,u16 timeout) |
build+send a 1-data-byte 0x68 frame to FM100B, block until its *Cnf arrives (or timeout) |
0x0801B0C4 |
fm100b_send |
void(u8 cmd,u8 b,u8 sub,u16 len,void* respbuf,const u8* payload,u16 timeout) |
same, with an N-byte payload |
0x08006C9C |
usart3_tx_buf |
void(const u8* buf,u16 len) |
raw byte-blit of a frame out USART3 |
0x08006CB8 |
usart3_tx_byte |
void(u8 b) |
one byte out USART3->DR, spin on TC |
0x08003050 |
poll_serial |
void(void) |
pump: run FM100B RX parse + CPS framer + RX drain once. Call this in any wait loop. |
0x08018CB0 |
fm100b_rx_parse |
int(void) |
scan USART3 RX ring for one 0x68 frame, verify checksum, dispatch it; returns 1 if a frame consumed |
0x08006348 |
fm100b_on_frame |
void(u8* frame) |
master *Cnf/*Ind dispatch: writes resp[cmd]=frame[3] then jump-tables to the per-cmd Ind handler |
0x08006CFC† |
dmr_call_start_from_contact |
void(u8 dummy, u16 contact_idx) |
originate a call to a stored contact: look up record, send cmd6 setup, latch current-call state |
0x08006FD8† |
dmr_call_resend |
void(void) |
re-send cmd6 for the latched current call (PTT continue) |
0x08006C4C |
dmr_set_radio_id |
void(u8 idHi,u8 idLo) |
cmd 0x49 — set our personal DMR ID on the module |
0x0800736C |
dmr_sms_send |
void(u16 target_or_contact) |
cmd 0x82 — send an SMS |
0x08006D00 |
dmr_contact_read |
void(u8 dummy,u16 idx,...) |
read a 21-byte DMR contact record idx*27 + 0x5E000 from SPI (for name/ID display) |
0x0801A38C |
nvic_system_reset |
noreturn void(void) |
reboot (used by remote-kill enforcement) |
† dmr_call_start_from_contact is the function whose body begins at 0x08006CFC/0x08006D00; dmr_call_resend body starts 0x08006FD8. Signatures below.
Confidence: high on the framing, the two send primitives, the RX parser, the fm100b_on_frame dispatch table, the incoming-call state block, and the reset. Medium-high on individual command semantics (cmd numbers are proven from call sites; their meaning is inferred from surrounding code + the FM100B ATC_* symbol list in the prior report).
1. MCU ↔ FM100B wire protocol (USART3 0x40004800)
1.1 Frame format (both directions)
Every message is a framed packet built/parsed at the byte level. Layout (offsets in bytes):
+0 0x68 sync / SOF (constant; parser rejects anything else)
+1 cmd command id (see §2)
+2 b secondary/opcode byte (usually 1 on Req; on Ind = subtype)
+3 sub sub-command / status. On *Cnf this byte is the result code.
+4..+5 cksum 16-bit checksum, big-endian (see §1.2)
+6..+7 len payload length, big-endian (u16)
+8..+8+len-1 payload (len bytes; for send1 it is a single data byte)
+8+len 0x10 EOF / end marker (constant)
Total on-wire size = len + 9. The send1 primitive uses len=1, so its frame is 10 bytes (68 cmd b sub CKh CKl 00 01 data 10).
Evidence — fm100b_send1 @0x0801B044:
0x0801b054 movs r0,#0x68 ; str [buf+0] ; SOF
0x0801b05c strb r4,[buf+1] / r5,[+2] / r6,[+3] ; cmd,b,sub
0x0801b062 movw #0xffff ; strh [buf+4] ; cksum placeholder
0x0801b068 bl 0x800bd2c ; strh r0,[buf+6] ; len = bswap16(1)
0x0801b074 strb r7,[buf+8] ; single data byte
0x0801b078 movs #0x10 ; strb [buf+9] ; EOF marker
0x0801b07c bl 0x8002ea8 (sum16, len=10) ; checksum over 10 bytes
0x0801b086 strh r0,[buf+4] ; store bswap16(cksum) at +4
0x0801b092 ldr r0,=0x2000706e ; bl 0x8006c9c ; usart3_tx_buf(buf,10)
fm100b_send @0x0801B0C4 is identical but len=r3, copies payload ([sp+0x20]) into buf+8 via memcpy 0x80062EC, writes 0x10 at buf+8+len, and sends len+9 bytes.
Helpers:
0x0800BD2C = bswap16(u16)— byte-swap; used to store the BE 16-bit len and cksum.0x08002EA8 = sum16(const u8* buf,u16 len)— sum of big-endian 16-bit words → the checksum.0x08021EB0 = usart_write_DR(base,byte)(str [base+4]),0x08021EA8 = usart_read_DR(base),0x08021EC2 = usart_get_flag(base,mask).
1.2 TX path
usart3_tx_byte @0x08006CB8void(u8 b): optionally mirrors the byte into the RX ring when a loopback flag (0x20000B67) is set, thenusart_write_DR(0x40004800,b)and spins on TX-complete (SR bit0x80).usart3_tx_buf @0x08006C9Cvoid(const u8* buf,u16 len):for i in 0..len: usart3_tx_byte(buf[i]).- Shared TX frame buffer at SRAM
0x2000706E(both send primitives build here; not re-entrant — the send primitives block until*Cnf, so a single global buffer is safe only from the main loop).
1.3 RX path
Per-byte RX is interrupt-driven (USART3 ISR @0x080205B0, IRQ 39). It pushes each byte into a 4 KB ring:
- ring struct head/word at
0x20000C64, data buffer at0x200092EF, index mask0xFFF. - (There is also a 1 KB ring at
0x20000C2C/0x20007575, mask0x3FF, filled in parallel — a secondary/debug capture.)
Draining/parsing happens in the main loop, not in the ISR:
-
fm100b_rx_ring_drain @0x0801FE50void(void): whiletail < head, pull one byte and feed the byte accumulator… actually it callsfm100b_rx_parseper available byte via0x08018BFC? — the concrete flow is:poll_serialcallsfm100b_rx_parsedirectly. -
fm100b_rx_parse @0x08018CB0int(void):- Search the ring for a
0x68byte (advance tail past junk). - Read
len = (ring[p+6]<<8)|ring[p+7](BE). Reject iflen >= 0x200. - Require
head-tail >= len+9bytes buffered, andring[p+8+len] == 0x10(EOF). - Copy the whole
len+9frame out of the ring into a linear work buffer. sum16(frame,len+9)must equal the stored checksum at+4; else drop.- On success advance the tail past the frame and call
fm100b_on_frame(frame)(0x08006348); return 1.
- Search the ring for a
-
poll_serial @0x08003050=fm100b_rx_parse(); cps_framer(0x0801F854); fm100b_rx_ring_drain(0x0801FE50);. This is the cooperative pump. Every blocking send loop (see §1.4) calls this; a rewritten UI's idle/wait loop must call it too.
1.4 Request/Confirm handshake (how blocking works)
Both send primitives implement a synchronous Req→Cnf:
resp[cmd] = 0xFF ; mark pending (resp array @0x20007476, indexed by cmd)
usart3_tx_buf(frame,len) ; send
timeout_ctr = timeout ; @0x20000C52
do { poll_serial(); } while (resp[cmd]==0xFF && timeout_ctr!=0);
fm100b_on_frame (§3) sets resp[cmd] = frame[3] when the matching *Cnf arrives, which breaks the loop. So respbuf/timeout args are: timeout is the last stacked arg (e.g. 0x3E8=1000 for call setup, 0x64=100 for config); the "respbuf" stack arg is a copy of the timeout counter seed. The response/status code for a command after the call returns is resp[cmd] at 0x20007476+cmd.
2. Command set (MCU → FM100B *Req), from call sites
Extracted by decoding (cmd=r0, b=r1, sub=r2, data/len=r3) at every call to the two send primitives. cmd is proven from the immediate; the name maps to the FM100B ATC_*Req symbol families documented in the prior RE report (§5.4).
| cmd | via | b | sub | payload | wrapper vaddr | inferred meaning (ATC_*Req) |
|---|---|---|---|---|---|---|
0x02 |
send1 | 1 | 1 | 1B | 0x0800760A |
misc mode set |
0x05 |
send1 | 1 | 2 | data=2 | 0x08006C74 |
channel/RF config set (ATC_ChannelSetReq-class) |
0x06 |
send | 1 | call_type | 5B [type,ID_be32] |
0x08006CFC |
DMR call setup (ATC_CallProcessReq) |
0x07 |
send | 1 | 1 | var | 0x08007180 |
contact/data set (ATC_CurChDigdataSetReq) |
0x09 |
send1 | 1 | 1 | 1B | 0x080076CC |
misc |
0x0A |
send | 1 | 1 | 5B [type,ID_be32] |
0x08006FD8 |
send/originate call (TX PTT) variant |
0x0B |
send1 | 1 | 1 | 1B | 0x08007530 |
set param |
0x0C |
send1 | 1 | 1 | data=0 | 0x08007598 |
set param |
0x25 |
send1 | 1 | 1 | data=1 | 0x08006C88 |
init/enable |
0x2A |
send | 1 | 1 | 4B | 0x080074FC |
set 32-bit param |
0x42 |
send1 | 1 | 1 | 1B | 0x080075B4 |
set param |
0x48 |
send1 | 1 | 1 | 1B | 0x080075C6 |
set param |
0x49 |
send | 1 | 1 | 4B | 0x08006C4C |
set our radio DMR ID (ATC_RadioIDSetReq) |
0x4D |
send1 | 1 | 1 | 1B | 0x080075F4 |
set param |
0x55 |
send1 | 1 | 1 | data+1 |
0x080075DC |
set param (increment) |
0x4C |
send1 | 1 | 1 | 1B | 0x08007680 |
set param |
0x57 |
send | 1 | 1 | 2B | 0x080074E0 |
set param |
0x62 |
send | 1 | 1 | 2B | 0x080071F0 |
set param |
0x81 |
send | 1 | 1 | var | 0x080073E0 |
SMS payload block (SPSendInBandDataReq) |
0x82 |
send | 1 | 1 | 20B | 0x0800736C |
SMS send (header+target) |
0x84 |
send | r0 | r0 | — | 0x080074BC |
contact info query (ATC_CalledContactINfoQuery) |
0x84 |
rawTX | — | — | 10B fixed | 0x08007548 |
boot/wake handshake — literal frame 68 84 01 01 00 00 00 01 00 10 sent raw via usart3_tx_buf, marks resp [+0x84]. Verified live on hardware: reply 68 84 00 00 87 7B 00 00 10 (status 0 = OK). Earlier listed as 0x64 — that was a misread. |
The single byte b is almost always 1 on a Req; on Ind frames frame[2] is the subtype selector (see §3). sub (frame[3]) is the module's status on the returned *Cnf.
2.1 Selected wrapper decompilations (callable API)
dmr_set_radio_id @0x08006C4C void dmr_set_radio_id(u8 idHi, u8 idLo)
payload[0]=idHi; payload[1]=idLo; payload[2..3]=0;
fm100b_send(cmd=0x49,b=1,sub=1,len=4,payload,timeout=0x64);
Sets the module's own DMR ID. (Only 2 bytes filled here; the personal ID low 16 bits — the caller composes the full 24-bit ID before calling.)
dmr_call_start_from_contact @0x08006CFC void dmr_call_start_from_contact(u8 unused, u16 contact_idx)
rec = dmr_contact_read(0xFF, contact_idx); // 21B record @ contact_idx*27 + 0x5E000
if (rec[0] > 2) { error("Call type error"); return; } // 0800aeb8 = show msg
call_type = (rec[0]==0)?1 : (rec[0]==1)?2 : (rec[0]==2)?4 : ...; // 1=Group,2=Private,4=AllCall
target_id = be32(rec[+1]); // 32/24-bit target
build payload = [call_type, target_id_be32]; // 5 bytes
fm100b_send(cmd=0x06, b=1, sub=call_type, len=5, payload, timeout=0x3E8);
// latch current-call state @0x20007DA9: [0]=call_type, [1..4]=target_id
This is the originate-call entry. It maps the contact record's stored type to the module's call_type (Group→1, Private→2, All→4) and sends the setup, then also fires a follow-on raw frame (0x8006DFC region) that TX-blits a 0x1F-byte packet.
dmr_call_resend @0x08006FD8 void dmr_call_resend(void)
type = curcall[0]; id = be32(curcall[+5]); // curcall @0x20007DA9
payload=[type,id_be32]; fm100b_send(0x06,1,type,5,payload,0x3E8);
Re-issues the setup for the already-latched call (used to keep a group call up / PTT re-key).
dmr_send_call_0a @0x08006FD8-region (0x08007000) void(u8 type, u32 id) — cmd 0x0A, same 5-byte [type,id_be32] payload, timeout=0x3E8. This is the alternate "start voice" path (the two, cmd6 vs cmd0xA, correspond to ATDigCallSetupCnf vs a direct voice-start).
dmr_sms_send @0x0800736C void dmr_sms_send(u16 target)
if (target != 0) { // resolve target contact
rec = SPI_read(0x0D0000 + (target-1)*48, 48);// SMS-target table (0x0D0000, 48B stride)
switch(rec[+1]) { type=1→grp, 4→prv, 5→all } // map record type
}
build 0x22-byte msg: dst = 0xAAAAAAAA if all-call else target;
fm100b_send(cmd=0x82,b=1,sub=1,len=20,payload,timeout=0x64); // header
// followed by cmd 0x81 payload block(s) for the text (0x080073E0)
dmr_contact_read @0x08006D00 void dmr_contact_read(u8 unused, u16 idx, out u8 rec[21])
base = idx*27 + 0x5E000; // 27-byte stride, contacts region
SPI_read(base, 21, rec); // 0x8021828 = spi_read(dst,addr,len)
// rec[0] = contact type (0=Group,1=Private,2=AllCall); rec[+1..]=ID + name
The stride is 27 bytes at 0x5E000 (= codeplug contacts 0x05C000 + 0x2000). This is the routine the UI calls to turn a contact index into a type+ID+name for display and for call setup. (Note the on-flash contact record the CPS writes is 32 bytes at 0x5E000 per the codeplug report; the module-facing read here pulls the first 21 bytes.)
3. Incoming frames (FM100B → MCU *Cnf / *Ind) — the RX side the UI renders
3.1 Master dispatch fm100b_on_frame @0x08006348
void fm100b_on_frame(u8* f) {
resp[f[1]] = f[3]; // 0x20007476[cmd] = status → unblocks the Req wait
if (f[1] >= 0xC1) return;
switch (f[1]) { /* jump table @0x0800636C, cmd*4 half-word offsets */ }
}
Jump-table result (cmds with a real Ind handler; all others fall to the no-op default 0x08006C26 and only update resp[]):
| cmd | handler vaddr | meaning |
|---|---|---|
0x01 |
0x08006670 |
status |
0x02 |
0x08006672 |
status |
0x03–0x04 |
0x08006674/76 |
status |
0x05 |
0x0800668C |
channel/config change Ind (latches new state, sets a "changed" flag) |
0x06 |
0x080066AA |
INCOMING CALL Ind — caller/TG/type → UI (see §3.2) |
0x07 |
0x0800671E |
call/PTT status Ind (call end, TX status) |
0x09 |
0x08006816 |
call-timer/ready Ind (arms a 0x320 timer) |
0x0A |
0x08006870 |
remote-command Ind (stun/kill; see §3.3) |
others (0x0B+, 0x12–0xC0) |
small resp[]-only stubs |
pure *Cnf acknowledgements |
3.2 Incoming-call Ind 0x080066AA — what the standby/RX screen reads
Frame layout for a cmd6 Ind: f[8]=call_type (1=Group, 2=Private, 4=AllCall), f[9..12]=source(caller) ID (BE), f[13..16]=dest/TG ID (BE). Handler:
status = f[3] → 0x20000C3C / 0x20000C3D
call_type: 1→0, 2→1, 4→2 → curcall[0] @0x20007DC2
dest_id = be32(f[+0xD]) → curcall[+1] (u32) (0x80112B8 = be32_to_u32)
src_id = be32(f[+9]) → curcall[+5] (u32) (the CALLER id the UI shows)
if (first-of-call flag) {
slot = curcall[+1]>>4; set_rx_slot_indicator(slot); // 0x8018530
copy state block // 0x80062ec
}
Incoming-call state block 0x20007DC2 (this is what a rewritten RX screen reads to draw "caller / TG / type"):
+0 u8 call_type (0=Group, 1=Private, 2=AllCall)
+1 u32 dest_id / talkgroup (little-endian in RAM)
+5 u32 source_id (the caller's DMR ID)
0x80112B8 = be32_to_u32(const u8* p) converts the on-wire big-endian IDs. Additional call-status bytes: 0x20000C3C (raw status), 0x20000C3D (mirror).
Talker alias / caller name: the frame carries the numeric IDs only. The UI resolves the caller name by looking the source_id up against the contacts table (dmr_contact_read / the by-ID search at 0x08007E68 → 0x08017F60, and 0x08006E6C alt lookup). If no contact matches, the raw ID is shown (Unknown station string at 0x08028815).
3.3 Remote-command Ind 0x08006870 (cmd 0x0A) — stun / kill / wake
sub = f[2]; code = f[3] → 0x20000C?? state
if (code == 0xA1) show_msg(...); // e.g. remote check / stun-related
if (enabled_flag[+0x184]) {
if (code == 0xA2) { // REMOTE KILL
kill_state = 4;
persist_word = 0x4444; store @[+0xC]; // marker written to NV
0x801A900(); // commit to SPI/NV
delay(0x7D0); 0x8007946(0x7D0);
nvic_system_reset(); // 0x801A38C — reboot into killed state
}
}
So the enforcement of a remote kill is a persisted 0x4444 marker + reboot via nvic_system_reset @0x0801A38C. A rewritten UI that wants to ignore remote kill would stub this handler or the 0x184 enable flag; to keep stock behavior, leave fm100b_on_frame's cmd-0x0A path intact. (Prohibit TX string 0x0801ED28 and DMR Remote Kill/Stun anchors 0x0800346C/0x08006B30 live on the UI side that reads these flags.)
3.4 Incoming SMS (module → MCU)
SMS received by the module arrives as an Ind carrying the text block; the MCU stores it into the SMS/inbox codeplug area. The upload confirm corresponds to the FM100B ATUploadRxSmsCnf symbol. The MCU-side receive path shares the same fm100b_on_frame dispatch (one of the resp[]-updating cmds) plus a data-copy into RAM; the inbox commit reuses the standard SPI codeplug writer (unchanged format).
4. Contact / address-book lookup for caller-name display
Two record stores are involved (both are read-only from DMR's perspective; the CPS owns their format):
- Contacts (module-facing) —
dmr_contact_read @0x08006D00:record = SPI[idx*27 + 0x5E000], 21 bytes:[0]=type, [+1..]=ID, name. Used both to originate calls and to name a contact index. - By-ID reverse lookup — the RX screen turns a numeric
source_id/dest_idinto a name via the search wrapper at0x08007E68→0x08017F60(walks the contacts region comparing the 24-bit ID), with an alternate at0x08006E6C. On a hit it renders the stored name; on a miss it renders the raw decimal ID (24-bit, max16777215per string0x08007E07). - SMS-target / key-name table —
0x0D0000, 48-byte stride (per the live SPI dump), used bydmr_sms_sendto resolve an SMS destination.
Group IDs are stored BCD/LE in the contact record (66 06 → TG 666, per the codeplug report); the module wire format uses plain big-endian 24/32-bit — be32_to_u32 @0x80112B8 and the payload-build shifts in the wrappers do the conversion. Keep both conversions if reusing these functions.
5. Call sequences for a rewritten UI
5.1 Boot / attach the module
// stock boot fires: raw 0x84 handshake (0x08007548), then a burst of config Reqs
fm100b_send1(0x05,1,2, data=2, resp, 0x64); // channel/RF config
fm100b_send (0x49,1,1, len=4, [idHi,idLo,0,0], resp, 0x64); // dmr_set_radio_id
// ... other 0x0B/0x0C/0x42/0x48/0x4D param sets as needed
// each call blocks via poll_serial() until resp[cmd] != 0xFF
5.2 Originate a DMR call (private or group)
// UI has a contact index (or build an ad-hoc record):
dmr_call_start_from_contact(0xFF, contact_idx); // 0x08006CFC
// → looks up record, maps type, sends cmd6 [type,id_be32], latches curcall@0x20007DA9
// while PTT held, keep the call up:
while (ptt_down) { dmr_call_resend(); poll_serial(); } // 0x08006FD8, re-sends cmd6/0x0A
// on release: send the corresponding stop/param Req and drop PTT.
For a raw call without a stored contact: build payload=[call_type, target_id_be32] yourself and call fm100b_send(0x06,1,call_type,5,payload,0x3E8) (or cmd 0x0A for the voice-start variant), then set curcall@0x20007DA9.
5.3 Render an incoming call (standby/RX screen)
// In the main loop, keep pumping the link:
poll_serial(); // 0x08003050 — drains USART3, dispatches Inds
// When cmd6 Ind fires, the state block @0x20007DC2 is populated:
u8 type = curcall_rx[0]; // 0=Group,1=Private,2=AllCall
u32 tg = *(u32*)(curcall_rx+1); // talkgroup / dest
u32 src = *(u32*)(curcall_rx+5); // caller DMR ID
// Resolve caller name:
name = contact_name_by_id(src); // 0x08017F60 search; fallback → decimal(src)
draw: "<name or src> → TG <tg>" (type-dependent: SID/GID/AID labels @0x0800A36C)
// status/end: cmd7 Ind updates call-status bytes; cmd9 arms the call timer.
5.4 Send an SMS
// text staged in RAM by the editor; target is a contact index or 0 for the default
dmr_sms_send(target); // 0x0800736C → cmd 0x82 header + cmd 0x81 payload
// wait resp[0x82]/resp[0x81]; ATUpload* / send-fail handled by fm100b_on_frame.
6. RAM state map (DMR)
| addr | size | contents |
|---|---|---|
0x2000706E |
~0x200 | TX frame build buffer (0x68…0x10) |
0x20007476 |
0xC1 | resp[cmd] response/status array (0xFF=pending) |
0x20000C52 |
u16 | Req timeout counter |
0x200092EF |
0x1000 | USART3 RX ring data |
0x20000C64 |
— | USART3 RX ring head/index |
0x20007575 |
0x400 | secondary RX capture ring |
0x20000B67 |
u8 | USART3 TX→RX loopback capture flag |
0x20007DA9 |
5+ | outgoing current-call: [0]=type,[1..4]=id, [+5]=id copy |
0x20007DC2 |
9 | incoming call: [0]=type,[1..4]=dest/TG,[5..8]=caller id |
0x20000C3C/3D |
u8×2 | incoming-call status bytes |
7. Reuse guidance for the UI rewrite
- Keep and call as-is:
fm100b_send1 (0x0801B044),fm100b_send (0x0801B0C4),poll_serial (0x08003050),fm100b_rx_parse (0x08018CB0),fm100b_on_frame (0x08006348),dmr_contact_read (0x08006D00),nvic_system_reset (0x0801A38C), and the wrappers in §2. They contain the whole USART3 protocol and are codeplug/CPS-neutral. - Read, never reframe: the incoming-call block
0x20007DC2andresp[]0x20007476are your UI inputs. Pollpoll_serial()from your event loop; read those to render. - To originate: prefer the wrappers (
dmr_call_start_from_contact,dmr_sms_send,dmr_set_radio_id) so type-mapping and current-call latching stay correct. If you bypass them, replicate the Group→1/Private→2/AllCall→4 mapping and the big-endian ID packing. - Do not re-implement framing/checksums yourself — call the two send primitives; that guarantees the FM100B never sees a malformed frame and keeps the module firmware (unchanged) happy.
- Color code / timeslot are set through the per-channel config Reqs (
cmd 0x05and the0x0B/0x0C/0x42/0x48/0x4Dfamily — set from the channel record fields); these carry no codeplug-format dependency beyond reading the channel record the CPS already writes.
Open items (medium confidence, worth a second pass on-target)
- Exact
sub/field meaning of the0x0B/0x0C/0x42/0x48/0x4D/0x55/0x57/0x62param Reqs (which is color-code vs squelch vs power vs denoise) — the cmd numbers are certain; individual mapping needs tracing each wrapper's caller (channel-settings menu handlers). - The cmd
0x84ATC_CalledContactINfoQueryreturn payload layout (talker-alias source) — its Ind path falls to the default stub here, so alias text likely arrives on a different cmd or is assembled MCU-side from contacts. - Encryption enable/key-select Req (menu
Encryption Set @0x080165D0) — routed through one of the param Reqs above; not yet pinned to a specific cmd byte.