Reverse-engineered reference for the Retevis MateTalk P64 / P4 DMR radio codeplug, as decoded by p64tool. It documents the on-the-wire region framing, the storage model, and the field↔offset map for every record type.
Authoritative source. The offsets here come from the decompiled vendor CPS (a C# WinForms program), cross-checked against live hardware dumps. Where the CPS and a dump disagreed, the dump won. The firmware image was analysed too but is not a source of offsets (see Firmware notes).
Scope: this describes the P64 V1.1 / firmware 1.0.0.0 codeplug validated by p64tool. The vendor treats codeplug layout as fixed per model (P64 and P4 share the same byte offsets); it is not known to vary across firmware versions, but p64tool's map is empirical, so treat other models/versions as unverified.
The radio stores the codeplug as ~13 regions, each read/written as one framed serial reply:
offset bytes field
0 2 magic 0x5F 0x5F
2 2 frame length u16 LE = (total frame length) - 6
4 6 fixed header 00 26 00 23 02 00
10 2 marker 0x55 0x11
12 2 payload length u16 LE (= N)
14 N payload the region body
14+N 4 trailer 0xFF 0xFF 0x55 0xAA
p64tool stores each region as its full frame and exposes payload() = bytes
[14 .. 14+N]. All multi-byte scalars in the payload are little-endian.
Names are UTF-16LE, NUL-terminated, 0xFF-padded.
Records within a region are fixed-size and begin at region offset 16 (there is
a 16-byte region header). "Size" is the payload length N.
| Region | Purpose | Size | Record layout |
|---|---|---|---|
r01 |
Device identity (model label, DMR ID, fw/date strings) | 275 | scalar strings |
r02 |
General/menu settings + encryption-key table (30×68 @144) | 2187 | scalar + table |
r03 |
DMR network/service, radio DMR ID, advanced timing | 51 | scalar |
r04 |
Contacts (200×40 @16) + RX-group lists (32×72 @8016) | 10323 | two tables |
r05 |
Emergency / digital-alarm systems (16×48 @16) + work-alone scalar tail | 791 | table + scalar |
r06 |
Scan lists (32×90 @16) | 2899 | table |
r07 |
Zones (16×68 @16) | 1107 | table |
r08 |
Channel table (256×72 @16) | 18451 | table |
r0A |
Alerts / man-down settings | 53 | scalar |
r32 |
Unknown — empty/erased on a default codeplug | 51 | — |
rFF |
Unknown (likely factory/calibration) — erased on default | 619 | — |
rKL |
Unknown — empty/erased on a default codeplug | 43 | — |
rML |
Quick-text messages (32×516 @16) | 16531 | table |
The "E-number" mnemonic in the CPS record IDs is a list-type id independent of the region number: E0=zones(r07), E1=scan(r06), E4=contacts(r04), E5=RX-groups(r04), E7=encryption(r02), EM=emergency(r05), CH=channels(r08).
Two coordinate systems appear in the RE work; keep them straight:
-
CPS record-relative offset (0-based from the start of a record). This is the canonical format used throughout this document, and matches the vendor's own
Adim.*[rec, N]copy arrays. -
p64tool record offset — what
src/config.rsindexes asrec[N]. p64tool frames each record slightly differently, so a translation applies:Record family p64tool base Translation (p64tool ← CPS) Channel table ( r08)payload l*72p64tool = CPS + 1Tables with base_ww = 16(contacts, zones, scan, messages, emergency)payload 1 + i*stridep64tool = CPS(shift 0)RX-groups ( base_ww = 8016), enc keys (base_ww = 144)— p64tool = CPS(shift 0)The channel
+1and the table shift-0 were both verified against a live dump.
The CPS also uses a "WW index" for scalar regions: WWxx[i] addresses payload byte
i - 15. So e.g. general-setting WW02[92] is r02 payload byte 77.
Every record round-trips byte-for-byte regardless of decode: the vendor (and p64tool) copy the whole record and only bind a subset of bytes to controls. A RESERVED byte below is one that no handler reads — preserved, never decoded.
Offsets are CPS record-relative (p64tool = these + 1). Many bytes mean different
things for Digital (type==0) vs Analog (type==1) channels — a decoder
must branch on byte 32 before interpreting the type-specific bytes.
| Off | Field |
|---|---|
| 0–31 | Name (UTF-16LE, ≤16 chars, NUL-term, FF-pad) |
| 32 | Channel type: 0=digital, 1=analog |
| 33 | &0x03 power (0=low, 2=high); &0x30>>4 TX-admit criteria; &0x40 RX-only. A also: &0x0C bandwidth/spacing |
| 34 | D: &0x80 SMS delivery-confirm, &0x03 SMS format. A: RESERVED |
| 35 | RESERVED |
| 36–39 | RX frequency, u32 LE, Hz |
| 40–43 | TX frequency, u32 LE, Hz |
| 44 | TX-timeout timer (TOT). DP4-class models: UI index = value − 3 |
| 45 | TOT pre-alert time |
| 46 | TOT re-key time |
| 47 | A: signalling reset time. D: RESERVED |
| 48–49 | D: TX contact index, u16 LE (into contacts). A: RESERVED |
| 50–51 | D: RX-group list index, u16 LE. A: RESERVED |
| 52 | D: &0x0F color code, &0x10 private-call-confirm, &0x20 time-slot (0=TS1,1=TS2). A: emergency-alarm-system index |
| 53 | RESERVED (constant 1) |
| 54 | D: &0x03 timed-preamble preference. A: scan-list index |
| 55 | A: &0x01 auto-start-scan, &0x02 off-network (脱网), &0x10 whisper (耳语). D: RESERVED |
| 56 | D: emergency-alarm-system index. A: &0x06>>1 sub-audio tail-elim kind, &0x10 tail-elim enable, &0x01 tail-HZ |
| 57 | D: &0x01 emergency alarm-indication, &0x02 alarm-reply, &0x04 call-indication. A: RESERVED |
| 58–59 | D: 58 = scan-list index; 59 flags &0x01 auto-scan, &0x02 off-network, &0x08 solo/lone-worker. A: sub-audio DECODE tone (CTCSS/DCS), u16 LE |
| 60–61 | D: 60 enc flags &0x01 enable, &0x02 random-key, &0x04 multi-key-decrypt; 61 = enc-key index. A: sub-audio ENCODE tone, u16 LE |
| 62 | D: &0x08 direct dual-slot (直通双时隙). A: &0x01 RX-squelch mode |
| 63–65 | RESERVED |
| 66–67 | BOOKKEEPING: per-type ordering/group index, u16 LE |
| 68–69 | RESERVED |
| 70–71 | BOOKKEEPING: record number (1-based), u16 LE |
Reserved set (never bound): universal 35, 53, 63, 64, 65, 68, 69; digital
also 47, 55; analog also 34, 48, 49, 50, 51, 57.
A tone is two bytes (lo, hi):
0xFF 0xFF→ off.- CTCSS:
value = (hi & 0x0F)*256 + lois the frequency ×10 (e.g. 1148 = 114.8 Hz), whenhi & 0xC0 == 0x00. - DCS:
code = (hi & 0x0F)*256 + lo(octal on the radio);hi & 0xC0 == 0x80normal,0xC0inverted.
Offsets are CPS record-relative. Only the bound/bookkeeping bytes are listed; everything else in the record is RESERVED.
- 0–31 name; 32–35 DMR ID (u32 LE, ID ≤ 0xFFFFFF so byte 35 is a flag/high byte); 36 call type (0=private, 1=group, 2=all); 38–39 record number (bookkeeping).
- RESERVED: 37.
- 0–31 name; 32–63 member channel numbers (up to 16 × u16 LE); 64 member count; 65 record number (both bookkeeping).
- RESERVED: 66–71.
- 0–31 name; 34–35 member count (u16 LE); 36–67 member channel numbers (16 × u16 LE). A zone/record number and a sort byte also live low in the record.
- 0–31 name; 32 priority-presence flags (
&0x03gate PCH1,&0x0Cgate PCH2); 33 reply-channel mode (0..2; 1 = use designated channel); 34–35 designated / revert TX channel (u16 LE, 0xFFFF = none); 36–37 priority channel 1 (u16 LE); 38–39 priority channel 2 (u16 LE); 40 probe/sample time; 42 signalling hold time; 44 behaviour flags (&0x02talk-back,&0x20nuisance-delete,&0x80scan LED); 56–57 member count (u16 LE); 58–89 member channel numbers (16 × u16 LE). - RESERVED: 41, 43, 45, 46–55.
- Note: priority-channel-1 is at offset 36, not 34 — offset 34 is the separate designated/revert-TX channel. (Two earlier analyses conflated them.)
- 0–31 name; 34 alarm mode/type bitfield; 35 impolite-TX count; 36 polite-TX count; 38 hot-mic duration; 39 RX duration; 45 record number (bookkeeping).
- RESERVED: 32–33, 37, 40–44, 46–47.
- 0 record number (bookkeeping); 1 key length (in hex chars); 2 key type (1=ARC4, 2=AES128, 3=AES256); 4–35 name (UTF-16LE); 36–67 key material (32 bytes).
- RESERVED: 3.
- 0 record number; 2–3 name/text length (chars); 4… message text (UTF-16LE).
Bound byte offsets (payload-relative). Everything else is reserved. Multi-byte IDs are BCD, little-endian byte order.
r02general settings — boundWW02indices (payload byte = index − 15):17, 18, 19, 24, 76, 77, 84, 85, 86, 87, 92, 93, 94, 95, 100, 101, 104, 106, 107, 108, 109, 110, 111, 140, 141. Notables:100/101talk-permit-tone & all-tones-off (WW02[100]==0xCDmutes everything);104key long-press time;106–111programmable-key assignments;140master voice-encryption enable ('C'/67 on,'B'/66 off);141encryption type. The 30×68 encryption-key table lives atWW02byte ≥144.r03DMR service — boundWW03indices16–19(this radio's DMR ID, 4-byte BCD LE),35, 37, 38, 40, 41(hang times / preamble / decode toggles),42(remote-kill decode enable, 0xF9/0xF8),43–47(remote-kill source ID, 4-byte BCD LE).r0Aalerts / man-down — boundWW0Aindices18(man-down enable) through42(the audible-alert toggle block).r05scalar tail — beyond the emergency table, only784–786(work-alone enable/tone/times).
r32 (51 B), rKL (43 B) and rFF (619 B) carry no interpretable data on a
default codeplug: r32 and rKL payloads are all-zero; rFF is ~95 % 0xFF
(erased flash) with only a small 00 01 00 02 00 header. rFF is most likely
factory/calibration storage. These are not a decode mystery — there is simply
nothing there on a stock radio. p64tool preserves them verbatim. Establishing what
(if anything) writes to them would require differential dumps (enable a feature,
re-read, diff).
The firmware (dmr_dm5_1_0_0_0_..._Iap.bin) is an Artery AT32 Cortex-M image
in an IAP container: a 4096-byte header then the raw image. Header offset 0 is
the payload length (u32 LE); the image loads at flash address 0x08000000
(so file_offset = flash_addr − 0x08000000 + 0x1000). It is unencrypted and
shares kernel code with model md440; components include LittleFS, EasyLogger, the
full DMR stack, and platform/db.c (the codeplug/database logic).
The firmware is not a shortcut to codeplug offsets. It embeds no default
codeplug template, so offsets are not recoverable from strings; extracting them
would require disassembling db.c (Cortex-M/thumb). Its value is confirming field
semantics (e.g. the analog/digital config log formats, encryption algorithms) and
cross-checking, not the byte map. The decompiled CPS remains authoritative.
- Decompiled vendor CPS — the field↔offset map. Trust the form code and the
File.cs/File_Save.csload/save paths; theC_DataLoad.csvalidators are stale and disagree with the live paths (e.g. channel type at 0 vs the correct 32). - Live hardware dumps (
p64tool read) — ground truth for region sizes, the offset translation, and per-field value sanity. - Verification without differential dumps: p64tool's
roundtripself-test proves decode→apply is byte-faithful, and decoded values were cross-checked against raw bytes. Differential dumps (change one setting, re-read, diff) can resolve the remaining unknown bytes and the empty regions if ever needed.