# 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 @0x08006CB8`** `void(u8 b)`: optionally mirrors the byte into the RX ring when a loopback flag (`0x20000B67`) is set, then `usart_write_DR(0x40004800,b)` and spins on TX-complete (SR bit `0x80`). - **`usart3_tx_buf @0x08006C9C`** `void(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 at `0x200092EF`, index mask `0xFFF`. - (There is also a 1 KB ring at `0x20000C2C`/`0x20007575`, mask `0x3FF`, filled in parallel — a secondary/debug capture.) Draining/parsing happens in the main loop, **not** in the ISR: - **`fm100b_rx_ring_drain @0x0801FE50`** `void(void)`: while `tail < head`, pull one byte and feed the **byte accumulator**… actually it calls `fm100b_rx_parse` per available byte via `0x08018BFC`? — the concrete flow is: `poll_serial` calls `fm100b_rx_parse` directly. - **`fm100b_rx_parse @0x08018CB0`** `int(void)`: 1. Search the ring for a `0x68` byte (advance tail past junk). 2. Read `len = (ring[p+6]<<8)|ring[p+7]` (BE). Reject if `len >= 0x200`. 3. Require `head-tail >= len+9` bytes buffered, and `ring[p+8+len] == 0x10` (EOF). 4. Copy the whole `len+9` frame out of the ring into a linear work buffer. 5. `sum16(frame,len+9)` must equal the stored checksum at `+4`; else drop. 6. On success advance the tail past the frame and call **`fm100b_on_frame(frame)`** (`0x08006348`); return 1. - **`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): 1. **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. 2. **By-ID reverse lookup** — the RX screen turns a numeric `source_id`/`dest_id` into a name via the search wrapper at `0x08007E68` → `0x08017F60` (walks the contacts region comparing the 24-bit ID), with an alternate at `0x08006E6C`. On a hit it renders the stored name; on a miss it renders the raw decimal ID (24-bit, max `16777215` per string `0x08007E07`). 3. **SMS-target / key-name table** — `0x0D0000`, 48-byte stride (per the live SPI dump), used by `dmr_sms_send` to 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: " → 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 `0x20007DC2` and `resp[]` `0x20007476` are your UI inputs. Poll `poll_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 0x05` and the `0x0B/0x0C/0x42/0x48/0x4D` family — 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 the `0x0B/0x0C/0x42/0x48/0x4D/0x55/0x57/0x62` param 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 `0x84` `ATC_CalledContactINfoQuery` return 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.