This document specifies the binary layout of the 245,760-byte (0x3C000) native codeplug image used by the Baofeng DM-1702 and DM-1702B radios. All offsets are file offsets (zero-based).
Source: Reverse-engineered from 8 independent OEM USB captures, Ghidra static analysis of the stock CPS executable, and cross-capture byte-level validation. The authoritative in-code reference is
NativeCoverageAudit.cs.Convention: Factory images contain GB2312 Chinese default text (e.g. 电话列表 = "Phone List", 信道 = "Channel") embedded in page headers and name tables. These are the firmware's factory-default labels and are treated as the source of truth for identifying the native purpose of each memory region. Where this project extends beyond what OEM captures demonstrate, the behavior is classified as structurally generated (extrapolated from validated patterns, not confirmed against OEM behavior).
| Property | Value |
|---|---|
| Total size | 245,760 bytes (0x3C000) |
| Sector size | 4,096 bytes (0x1000) |
| Sector count | 60 |
| Read/write block size | 64 bytes |
| Byte order | Little-endian (LE16/LE32) unless noted |
| Offset | Length | Section |
|---|---|---|
| 0x0000 | 0x3000 | Non-CPS data (three-zone pattern: FF-filled, zero-filled, mixed) |
| 0x3000 | 0x0002 | Channel count (LE16: factory=1, Ryan=159) |
| 0x3002 | 0x0010 | Reserved (zero-filled in all captures) |
| 0x3012 | 0x0FC0 | Channel records — linear region (85 × 0x30 bytes, indices 0–84) |
| 0x4000 | 0x0FFC | Channel name table 1 (372 × 11 bytes, indices 0–371) |
| 0x5000 | 0x0300 | Configuration section (DMR ID, radio name, DTMF, keys, etc.) |
| 0x5500 | 0x0100 | GPS settings (16 entries × 16 bytes) |
| 0x6000 | 0x0F00 | Zone data — linear region (14 × 0x112 bytes, indices 0–13) |
| 0x7000 | 0x0D80 | Contact metadata |
| 0x8000 | 0x1600 | RX group / system data (RX groups, emergency, privacy, lone worker) |
| 0xA000 | 0x1000 | Quick text messages (count + stride 0x81 records) |
| 0xB000 | 0x1000 | Scan list data (up to 32 × 0x39-byte records; count at 0xB000) |
| 0x2B000 | var | Zone data — overflow pages (14 zones per 4K page, indices 14+) |
| 0xF000 | 0x1000 | Channel records — overflow page 0 (84 slots, indices 85–168). OEM-validated. Factory header: 电话列表 1 ("Phone List 1") |
| 0x10000 | 0x1000 | Phone/DTMF data — 电话列表 2 ("Phone List 2"). Structurally generated: this project writes channel overflow page 1 here (indices 169–252) |
| 0x11000 | 0x1000 | Phone/DTMF data — 电话列表 3 ("Phone List 3"). Structurally generated: this project writes channel overflow page 2 here (indices 253–255) |
| 0x12000 | 0x8000 | Phone/DTMF system data (电话列表/电话系统, stride 0x30) |
| 0x1B000 | 0x1000 | Unknown (zero-filled in all captures) |
| 0x1C000 | 0x2000 | Channel name table 2 (372+ × 11 bytes, indices 372+; 信道 defaults) |
| 0x1E000 | 0x0800 | Channel–contact map (LE16 entries, 1 per channel) |
| 0x1F000 | 0x4000 | Contact region (paged: 170 records per 4K page, bitmap, sorted index) |
Note: Page 0 at 0xF000 is OEM-validated for channel overflow records (Ryan capture: 74 records at indices 85–158). Pages 0x10000–0x1A000 contain phone/DTMF system data in all OEM captures — factory headers contain GB2312
电话列表("Phone List") and the data area holds phone list entries at stride 0x30. No OEM capture exceeds 169 channels; no Ghidra evidence confirms the OEM CPS writes channel records into pages 0x10000+. This project extrapolates the page 0 pattern into pages 1–2 (0x10000–0x11000) to support up to 256 channels — this is structurally generated behavior that overwrites phone/DTMF data. Pages 0x1C000–0x1DFFF contain a second channel name table (factory default: 信道 = "Channel" at stride 11).
Each channel occupies 48 bytes (stride 0x30). Channels are stored in two regions:
85 records at 0x3010 + index × 0x30. Index 84's record at 0x3FD0 is the last before the name table at 0x4000.
Source of truth: Ghidra
FUN_004191f0confirms formulai * 0x30 + 0x3010. Cross-validated againstryan_whidbey_1.datachannel 0 RX BCD at 0x3010.
Overflow pages at 0xF000, each 4,096 bytes (0x1000). Each page has a 0x32-byte (50-byte) header followed by 84 channel record slots at stride 0x30.
For index i ≥ 85:
pagedIndex = i - 85
page = pagedIndex / 84
slot = pagedIndex % 84
offset = 0xF000 + page × 0x1000 + 0x32 + slot × 0x30
Page header layout (0x32 bytes):
-
Bytes 0x00–0x04:
00-01-02-03-04(identifier, identical across all captures) -
Bytes 0x05–0x10: FF-fill or channel metadata
-
Bytes 0x11–0x1F: zeros
-
Bytes 0x20–0x2B: GB2312 factory text —
电话列表 2("Phone List 2"). This is the firmware's label identifying the page's native purpose: phone/DTMF list data. The OEM CPS preserves this header when writing channel records into page 0 at 0xF000. Pages 1+ (0x10000+) contain phone/DTMF data in all OEM captures. -
Bytes 0x2C–0x31: state/count bytes
-
Overflow pages: Page 0 at 0xF000 (indices 85–168) is OEM-validated (Ryan capture, 74 records). Pages 1–2 at 0x10000/0x11000 (indices 169–255) are structurally generated — this project extrapolates the page 0 record format into pages that natively hold phone/DTMF data (电话列表). No OEM capture has >169 channels; the true OEM maximum is unknown.
-
Record capacity: 85 linear + (3 × 84) paged = 337 record slots, capped at 256 channels (indices 0–255).
-
Name capacity: Table 1 at 0x4000 holds 372 names (indices 0–371). Table 2 at 0x1C000 holds indices 372+. 256 channels fit entirely within Table 1.
-
Channel count: Stored at 0x3000 as LE16 (factory=1, Ryan=159)
| Offset | Size | Field |
|---|---|---|
| +0x00 | 4 | RX frequency (BCD, swapped-pair order: bytes [1,0,3,2]) |
| +0x04 | 4 | TX frequency (BCD, swapped-pair order: bytes [1,0,3,2]) |
| +0x08 | 1 | Mode / channel flags byte 1 |
| +0x09 | 1 | Power level / bandwidth flags |
| +0x0A | 2 | CTCSS/DCS RX tone |
| +0x0C | 2 | CTCSS/DCS TX tone |
| +0x0E | 1 | Color code (digital channels) |
| +0x0F | 1 | Time slot (digital channels) |
| +0x10 | 2 | Contact index (LE16, digital channels) |
| +0x12 | 1 | RX group index |
| +0x13 | 1 | Scan list index |
| +0x14 | 1 | GPS system index |
| +0x15–0x2F | 27 | Reserved / extended flags |
Frequencies are stored as 4-byte BCD with a swapped-pair byte order. To decode:
- Read bytes at offsets [1, 0, 3, 2] (swap pairs).
- Extract each BCD nibble to form an 8-digit decimal string.
- Multiply by 10 to get frequency in Hz.
Example: bytes 62 14 00 25 → reordered 14 62 25 00 → BCD 14622500 → 146,225,000 Hz → 146.2250 MHz.
Channel names are stored in two tables with identical 11-byte stride (ASCII/GB2312, zero-padded):
Table 1 at 0x4000 (indices 0–371, 372 × 11 = 4,092 bytes):
nameOffset = 0x4000 + index × 11 (for index 0–371)
Table 2 at 0x1C000 (indices 372+, contiguous across page boundary into 0x1D000):
nameOffset = 0x1C000 + (index - 372) × 11 (for index ≥ 372)
- Table 1 capacity: 372 names (0x4000–0x4FFB). Factory defaults: entry 0–1 = English (
Channel 1,Channel 2), entries 2–371 = GB2312 (信道 3,信道 4, …). - Table 2 capacity: 372+ names (0x1C000–0x1DFFF, 8,192 bytes ÷ 11 = 744 slots). Factory defaults: entry 0 = null, entries 1–651 = GB2312 (
信道 2). Entry 372 straddles the 0x1CFFC/0x1D000 page boundary, proving the table is contiguous. - Table 2 is unused in Ryan's capture: All entries match factory defaults (0 diffs between factory and Ryan).
- Ghidra formula:
((index - 0x174) % 0x174) × 0xB— the modulo 0x174 (372) maps indices 0–371 to Table 1 offsets and indices 372–743 to Table 2 offsets. - CPS UI split: The CPS shows "Table1 / Table2" at index 50 (0x4226) within Table 1, but this is a UI-only division — the binary layout within each table is strictly linear.
- Our serializer: Currently uses only Table 1 (indices 0–371). Table 2 support would be needed for >372 channel names.
| Index Range | Table | Offset Range |
|---|---|---|
| 0–49 | 1 | 0x4000–0x4225 |
| 50–371 | 1 | 0x4226–0x4FFB |
| 372–743 | 2 | 0x1C000–0x1DFFF |
Contacts use a paged layout starting at offset 0x1F000:
- Page size: 4,096 bytes (0x1000)
- Records per page: 170 (24-byte records packed sequentially)
- Contact record: 24 bytes — 16 bytes UTF-16LE name + 4 bytes call ID (LE32) + 4 bytes type/flags
Additional structures:
- Contact bitmap: zero-indexed, one bit per contact slot
- Sorted index table: LE16 entries providing alphabetical ordering
pageIndex = contactIndex / 170
offsetWithinPage = (contactIndex % 170) * 24
imageOffset = 0x1F000 + (pageIndex * 0x1000) + offsetWithinPage
Source: Ghidra
rebuild_functions_decompiled.cFUN_0041f960 (lines 571–816), binary-confirmed against Ryan and baseline captures.
Zone data starts at offset 0x6000 with stride 0x112 (274 bytes). Maximum 250 zones (Ghidra: uVar2 == 0 || 0xFA < uVar2).
- Zone count: Single byte at image offset 0x6000 (overlaps zone[0] record byte +0x00). Written LAST by the CPS after all records.
- Ryan: 0x15 (21 zones). Factory: varies by capture.
14 zone records at 0x6000 + index × 0x112. Ghidra locator: i * 0x112 + 0x6010 (points to name at +0x10).
| Offset | Size | Field |
|---|---|---|
| +0x00 | 16 | Reserved zeros. Zone[0]+0x00 is overwritten by the global zone count byte. |
| +0x10 | 16 | Zone name (ASCII, zero-padded) |
| +0x20 | 1 | Authoritative member count (firmware reads here: name_base + 0x10) |
| +0x21 | 128 | Member list (64 × LE16, 1-based channel indices) |
| +0xA1 | 1 | Echo member count |
| +0xA2 | 128 | Echo member list (64 × LE16, mirrors +0x21) |
Overflow pages starting at 0x2B000. 14 zones per page (0xE × 0x112 = 0xEFC ≤ 0x1000).
For index i ≥ 14:
j = i - 14
page = j / 14
slot = j % 14
offset = (page + 0x2B) × 0x1000 + slot × 0x112
Ghidra formula: ((i - 0xE) / 0xE + 0x2B) * 0x1000 + ((i - 0xE) % 0xE) * 0x112
Paged zone records omit the 16-byte reserved prefix — fields shift down by 0x10:
| Offset | Size | Field |
|---|---|---|
| +0x00 | 16 | Zone name (ASCII, zero-padded) |
| +0x10 | 1 | Authoritative member count |
| +0x11 | 128 | Member list (64 × LE16, 1-based channel indices) |
| +0x91 | 1 | Echo member count |
| +0x92 | 128 | Echo member list (64 × LE16) |
- Binary-confirmed: Ryan zone[14] "25k Repeater 2/2" at 0x2B000 with name at +0x00.
- Members per zone: Max 64 (Ghidra:
local_18 == 0 || 0x40 < local_18). Ryan zone[5] "GMRS" has 22 members.
Source: Ghidra
rebuild_functions_decompiled.c(lines 1090–1293), binary-confirmed against Ryan and baseline captures.
Scan list data at offset 0xB000, stride 0x39 (57 bytes), maximum 32 scan lists.
- Scan list count: Single byte at image offset 0xB000 (overlaps scanList[0] record byte +0x00). Written LAST by the CPS after all records. Same pattern as zone count.
- Ryan: 0x0E (14 scan lists). Baseline: 0x00 (zero-filled template area).
- Area fill: Ryan's CPS FF-fills the 0x1000-byte area before writing records. Baseline's CPS zero-fills and writes 32 GB2312 template records.
| Offset | Size | Field | Evidence |
|---|---|---|---|
| +0x00 | 1 | 0xFF from area fill (scanList[0]'s +0x00 is overwritten by the global count) | Ryan SL[1]: 0xFF; baseline: 0x00 |
| +0x01 | 11 | Name (10 ASCII chars + pad byte) | Ryan SL[0]: "NOAA WX ", SL[1]: "MURS" |
| +0x0C | 1 | Authoritative member count (Ghidra: puVar13[-7] as loop bound) |
Ryan SL[0]: 0x08, SL[1]: 0x06 |
| +0x0D | 1 | Flags (high nibble gates priority channel ref at +0x10) | Baseline: 0x03, Ryan: 0x10 |
| +0x0E | 1 | Constant 0x06 | All captures |
| +0x0F | 1 | Constant 0x00 | All captures |
| +0x10 | 2 | Priority channel 1 reference (LE16, CPS-maintained) | Ghidra: participates in channel-remap |
| +0x12 | 4 | Priority channel 2 + reserved (CPS-maintained) | Ghidra: channel-remap |
| +0x16 | 1 | Constant 0x0A | All captures |
| +0x17 | 1 | Priority bitmap low (0x00=none, 0x80=has priority) | Ryan SL[0]: 0x80, SL[1]: 0xFF |
| +0x18 | 1 | Priority bitmap high / per-member digital flags | Ryan SL[0]: 0xFF, SL[1]: 0xFF |
| +0x19 | 32 | Member list (16 × LE16, 1-based channel indices) | Ryan SL[0]: ch1,1,2,3… (16 max) |
RX group data at offset 0xC000 uses a split layout:
- Group metadata (name, member count) at the start of the page.
- Member contact index lists at a later offset within the same page.
Identified by GB2312 default text analysis. This region was previously (incorrectly) documented as channel overflow pages.
- 0xE000–0xE1FF: Phone system records (电话系统 = "Phone System", stride 0x50, 4 entries)
- 0xE200–0xEFFF: Phone list names (电话列表 = "Phone List", stride 0x30, 256 entries in factory)
- 0xF000: Channel overflow page 0 (NOT phone data despite 电话列表 text in its header — see above)
- 0x10000–0x10FFF: Phone list data continuation (FF-fill header, stride 0x30 records)
- 0x11000–0x11FFF: Phone list names continuation (电话列表 2 repeating at stride 0x30)
- 0x12000–0x1A000: Additional phone/DTMF data pages (same FF+01 header pattern as 0x10000)
- 0x1B000: Zero-filled in all captures (purpose unknown)
Not serialized: Our codeplug serializer does not write phone/DTMF system data. Under the clean-write architecture (
Dm1702NativeImageBuilder.Build()), these regions are zeroed. If DTMF phone system data needs to be preserved, the user must save and reload from an OEM.datafile.
The configuration section at offset 0x5000 (1,280 bytes) contains all radio parameters. Every field below is OEM-validated via cross-capture byte-level comparison of 8 .data files unless noted otherwise. Offsets are relative to 0x5000.
Source of truth:
Dm1702NativeConfigSerializer.cs(serializer),NativeCoverageAudit.md(per-field evidence).
| Offset | Size | Field | Evidence |
|---|---|---|---|
| +0x00 | 1 | Backlight duration (0=Off, 1=10s, 2=15s, 3=20s, 4=5s, 5=Always) | Cross-capture: baseline=1, eng=5 |
| +0x01 | 1 | Constant 0x32 (all 8 captures) |
OEM constant |
| +0x02 | 1 | Analog squelch level (0–15) | Cross-capture: baseline=5, ryan=12 |
| +0x03 | 1 | Packed bitfield: b0=VOX enable, b2=features modified flag, b5=keypad lock, b6=CTCSS tail revert | Cross-capture: baseline=0x00, eng=0x67, ryan=0x24 |
| +0x04 | 4 | DMR ID (LE32) | Cross-capture: ryan=3176775 |
| +0x07 | 1 | b6=Show channel number | Cross-capture: baseline=0x40, ryan=0x00 |
| +0x0B | 1 | Clock / misc flags (0x48=clock on, 0x47=clock off) | Cross-capture: baseline=0x47, ryan=0x48 |
| +0x0C | 1 | Default power level (0=Low, 1=Medium, 2=High) | Cross-capture validated |
| +0x0D | 1 | Packed: b4=always set, b2+b3=battery saver, b0+b1=lone worker echo | Cross-capture: baseline=0x10, ryan=0x1C |
| +0x0E | 1 | Digital squelch level (0–15, default=5) | Cross-capture: baseline=0x05 |
| +0x10 | 16 | Date stamp (ASCII) | Per-write timestamp |
| +0x20 | 12 | Band limit constants (factory: 37 08 ... 60 ...) |
All 8 captures identical |
| +0x08 | 1 | VOX level (0–10) | Cross-capture: eng=0x03 |
| +0x09 | 1 | Language (0=English, 1=Chinese) | Cross-capture validated |
| +0x0A | 1 | TX timeout (0–15, ×15s) | Cross-capture validated |
| +0x110 | 16 | Radio name (ASCII, zero-padded) | ryan="K0RPW", baseline=zeros |
| +0x120 | 16 | TX preamble duration | Cross-capture validated |
| +0x140 | 1 | Mic gain | Cross-capture validated |
| +0x150 | 14 | Key assignment table (7 keys × 2 bytes: short press, long press) | Cross-capture: 9/14 bytes differ between captures |
| +0x180 | 16 | Radio name (ASCII, duplicate at alternate offset) | ryan="K0RPW" |
| +0x192 | 10 | DTMF PTT ID (ASCII, zero-padded) | Cross-capture: default "2345678" |
| +0x19C | 8 | DTMF Kill Code (ASCII) | Cross-capture validated |
| +0x1A4 | 8 | DTMF Revive Code (ASCII) | Cross-capture validated |
| +0x1C0 | 16 | Startup intro line 1 (ASCII) | ryan="Pacific NW" |
| +0x1D0 | 16 | Startup intro line 2 (ASCII) | ryan=zeros |
The first 0x3010 bytes follow a three-zone pattern observed across all captures:
- FF-filled region — factory default padding
- Zero-filled region — cleared by firmware
- Mixed-content region — radio-internal data not written by CPS
This region is preserved byte-for-byte during codeplug writes.
Each 4K sector ends with a marker byte at offset sectorBase + 0xFFF. These markers are written by Dm1702NativeSectorSerializer using a fixed table of observed OEM values.
The factory OEM CPS populates default entity names using GB2312-encoded Chinese text. Entry #1 of each entity type typically uses an English name; entry #2 and beyond use Chinese. These labels are useful for identifying which image region holds which entity type.
Encoding: GB2312 (code page 936). Each Chinese character is a double-byte pair with high byte 0xA1–0xF7 and low byte ≥ 0xA1. Mixed with ASCII in the same name field (e.g.,
DMR系统 1).
| GB2312 Text | Hex Bytes | English Translation | Image Region | Default Pattern | Count (Factory) |
|---|---|---|---|---|---|
| 信道 | D0 C5 B5 C0 |
Channel | 0x4000 (Channel Name Table) | 信道 3, 信道 4, … (entries 1–2 are English Channel 1, Channel 2) |
1,022 |
| 区域 | C7 F8 D3 F2 |
Zone | 0x6000 (Zone Names) | 区域 2, 区域 3, … (entry 1 is English Zone 1) |
4 |
| 列表 | C1 D0 B1 ED |
List | 0x8000 (RX Group List Names) | 列表 2, 列表 3, … (entry 1 is English List 1) |
28 |
| DMR系统 | 44 4D 52 CF B5 CD B3 |
DMR System | 0x8640 (DMR/Digital Signaling System) | DMR系统 1, DMR系统 2, … |
6 |
| 模拟系统 | C4 A3 C4 E2 CF B5 CD B3 |
Analog System | 0x8700 (Analog Signaling System) | 模拟系统 1, 模拟系统 2, … |
4 |
| 两音系统 | C1 BD D2 F4 CF B5 CD B3 |
Two-Tone System | 0x9000 (2-Tone Signaling System) | 两音系统 1, 两音系统 2, … |
15 |
| 联系人 | C1 AA CF B5 C8 CB |
Contact | 0x9400 (Contact Names) | 联系人 1, 联系人 2, … |
33 |
| 扫描列表 | C9 A8 C3 E8 C1 D0 B1 ED |
Scan List | 0xB000 (Scan List Names) | 扫描列表2, 扫描列表3, … (entry 1 is English List1) |
30 |
| 电话系统 | B5 E7 BB B0 CF B5 CD B3 |
Phone System | 0xE000 (DTMF Phone System) | 电话系统 1, 电话系统 2, … |
4 |
| 电话列表 | B5 E7 BB B0 C1 D0 B1 ED |
Phone List | 0xE200 (Phone/DTMF List Names) | 电话列表 1, 电话列表 2, … |
256 |
| 联系列表 | C1 AA CF B5 C1 D0 B1 ED |
Contact List | 0x26000 (Extended Contact List) | 联系列表 1, 联系列表 2, … |
109 |
| 状态列表 | D7 B4 CC AC C1 D0 B1 ED |
Status List | 0x29000 (Status Message List) | 状态列表 1, 状态列表 2, … |
101 |
| Character | Pinyin | English |
|---|---|---|
| 信 | xìn | message / signal |
| 道 | dào | channel / path |
| 区 | qū | area / zone |
| 域 | yù | region / domain |
| 列 | liè | column / list |
| 表 | biǎo | table / list |
| 系 | xì | system / relate |
| 统 | tǒng | system / unify |
| 模 | mó | model / analog |
| 拟 | nǐ | simulate / imitate |
| 两 | liǎng | two / both |
| 音 | yīn | sound / tone |
| 联 | lián | connect / link |
| 人 | rén | person |
| 扫 | sǎo | sweep / scan |
| 描 | miáo | trace / describe |
| 电 | diàn | electric |
| 话 | huà | speech / phone |
| 状 | zhuàng | state / status |
| 态 | tài | condition / state |
- Entry #1 is English, #2+ is Chinese: Consistent across all entity types in the factory image. The CPS appears to initialize the first slot with an English default name and all subsequent slots with GB2312 Chinese.
- Ryan's capture has fewer GB2312 hits (9 unique strings vs. 12 in factory) because user-programmed names overwrite the Chinese defaults with ASCII text.
傲斜is a false positive: BytesB0 C1 D0 B1appear in Ryan's scan list data region (0x3993) as part of record data fields, not intentional text. The scanner's double-byte heuristic matches these coincidentally.- Default count reveals slot capacity: The repetition count of each Chinese default string in the factory image reflects the total number of pre-initialized slots for that entity type (e.g., 256 phone list entries, 109 contact list entries, 101 status messages).