# 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: "<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 `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.
