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

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

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

Прочее:
- везде исправлен режим прошивки: тангента PTT вместо клавиши "*"
- устаревший Ru-4D_Flasher.exe удалён из репозитория
- добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
2026-07-22 21:17:20 +09:00

350 строки
23 KiB
Markdown
Исходник Постоянная ссылка Ответственный История

Этот файл содержит неоднозначные символы Юникода
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.