Baofeng DM-1701 hardware (and its STM32 siblings)

The Baofeng DM-1701, the Retevis RT84 and the TYT MD-UV380 / MD-UV390 (5 W and "Plus 10 W") are one board family: an STM32F405VGT6 (LQFP100) MCU, an HR_C6000 DMR baseband/codec chip, one AT1846S RF transceiver covering both VHF and UHF, a 160 x 128 color LCD on an 8-bit 8080-style bus, a 16 MB SPI NOR flash, a 3-row keypad matrix that shares its column lines with the LCD data bus, a rotary channel encoder, a volume potentiometer with a built-in power switch, and USB full speed device on the MCU's OTG_FS core. This file records what the hardware needs. The DM-1701 is the primary target; differences for the siblings are collected in section 17.

Conventions:

Item Convention
Register values Hex
AT1846S registers 16 bit, written as reg = value
HR_C6000 registers Written as page:reg = value
"Unverified" The fact is inferred. No datasheet or measurement confirms it

Register-level HR_C6000 behavior is in modem-hrc6000.md and is not repeated here.


1. MCU, clocks, memory

1.1 Part

Item Value
MCU STM32F405VGTx, LQFP100, Cortex-M4F, 1 MB flash, 128 KB SRAM (0x20000000), 64 KB CCM RAM (0x10000000), 4 KB backup SRAM (unused)
FPU single precision, hard float ABI (fpv4-sp-d16)
NVIC priority bits 4 (16 levels); priority grouping 4 (all preemption)
RTOS tick 1 kHz (SysTick); a separate 1 kHz HAL time base runs on TIM14 at priority 3

1.2 Clock tree (normal run mode)

Item Value
HSE 8 MHz external clock in bypass mode (no crystal on OSC_IN/OSC_OUT; a clock signal is fed in). 8 MHz is implied by USB working at 48 MHz with the PLL values below
LSE 32.768 kHz crystal on PC14/PC15, clocks the RTC
PLL source HSE, M = 4 (VCO in 2 MHz), N = 72 (VCO 144 MHz), P = 2, Q = 3
SYSCLK / HCLK 72 MHz
APB1 (PCLK1) 36 MHz (/2); APB1 timer clock 72 MHz
APB2 (PCLK2) 72 MHz (/1); APB2 timer clock 72 MHz
48 MHz domain (USB) 144 / 3 = 48 MHz
Flash wait states 2 at boot; 3 is used when the clock is re-applied at run time
Voltage scale 1
PLLI2S N = 192, R = 4 -> 96 MHz (unused in practice: I2S3 runs as slave, see 4.4)
RTC LSE, async prescaler 127, sync prescaler 255 -> 1 Hz

Boot gotcha: the bootloader leaves the system running from HSE/PLL. The HSE configuration cannot be changed while it is the system clock, so the application first switches SYSCLK to HSI, then reprograms HSE/PLL, then switches back to the PLL.

1.3 Reduced-power ("eco") clock

Item Value
PLL P 6 (SYSCLK 24 MHz)
APB1 /1 (24 MHz)
Flash wait states 0
Backlight timer period changed with the clock (TIM1, section 9.3)
Rule Not entered while USB is in the configured state. The 48 MHz domain stays at 48 MHz, but the bus clocks drop too far for the USB stack. On a USB reset the full speed clock is restored first

1.4 Internal flash layout

STM32F405, 1 MB: sectors 0-3 are 16 KB, sector 4 is 64 KB, sectors 5-11 are 128 KB. The V02.06 CSV codec runs in place from sectors 10 and 11, where the merge puts it (codec-placement.md, "STM32 radios"; codec-dm1701.md): tables at their original address 0x080D19F0..0x080E3A72, code moved from its original address 0x08074714 to 0x080E3A74..0x080F3C90, and the descriptor at 0x080FFF80.

Sector Address Size Holds
0-2 0x08000000..0x0800BFFF 48 KB bootloader: never erased or written
3 0x0800C000..0x0800FFFF 16 KB RadioFoundry (vector table first)
4 0x08010000..0x0801FFFF 64 KB RadioFoundry
5-9 0x08020000..0x080BFFFF 5 x 128 KB RadioFoundry
10 0x080C0000..0x080DFFFF 128 KB codec tables (from 0x080D19F0)
11 0x080E0000..0x080FFFFF 128 KB codec tables (to 0x080E3A71), codec code (0x080E3A74..0x080F3C8F), descriptor (0x080FFF80..0x080FFFFF)

A flash sector can only be erased whole, so boards/dm1701/memory.ld gives the application sectors 3-9 (0x0800C000..0x080BFFFF, 720 KB in one piece) and asserts that everything it loads into flash ends before 0x080C0000. The image is one .bin from 0x0800C000, and an update erases and writes only the sectors it covers. The bytes around the codec in sectors 10 and 11 are 0xFF after a merge and are never executed. The DM-1701's settings and codeplug live in the external SPI flash, so nothing else writes internal flash.

The manufacturer's firmware, and RadioFoundry builds from before the codec moved, keep the codec code in sectors 7 and 8, which the application now covers. A release or manifest with two ranges (the application and a sector 9 data part) is from before the move and is refused. Moving a radio to this layout writes the codec once, from the manufacturer's firmware file or from a copy the tools read from the radio earlier (release.md, "Codec handling").

rfoundry's keep and merge modes follow from this map (release.md, "Codec handling").

The codec's entry points, as offsets from the code start: encode +0x130, encode FEC +0x4E8, decode +0x5D8. Call with the Thumb bit set when calling through a pointer.

Codec calling convention (the interface to the third-party blob; unverified beyond "it works"):

Call r0 r1 r2 r3 stack words [0], [1], [2], [3]
decode (called twice per 20 ms frame) PCM out (80 samples per call) 80 49 input bits, one bit per 16-bit word 0 0, half index (0 then 1), pointer to decoder state (0x7EC bytes)
encode (called twice per 20 ms frame) 72-word bit buffer out 0 PCM in (80 samples per call) 80 flags (0x1840 first half, 0x0800 second half), half index (0/1), 0x2000, pointer to encoder state (0x2000 bytes)
encode FEC bit buffer same bit buffer 0 pointer to FEC state (0x100 bytes) none

The 72 encoded bits come back one bit per 16-bit word (LSB) and are packed MSB first into 9 bytes. On decode, the 9-byte (72-bit) AMBE+2 frame must first be de-interleaved and FEC-decoded to 49 bits by the caller (Golay(24,12) on C0, Golay(23,12) on C1 after removing the pseudo-random whitening seeded from C0's 12 data bits with x' = 173x + 13849 mod 65536, the standard AMBE+2 3600x2450 FEC; see the codec spec). The state buffers must be initialized by the codec's own init calls (codec-dm1701.md C). Gotcha: a call site that builds the stack arguments must not be optimized in a way that moves the stack pointer (with -Os around the call, DMR TX crashed); calling through a small assembly shim with an explicit frame avoids this.

1.5 RAM

Region Address Size Notes
SRAM1+SRAM2 0x20000000 128 KB general data, DMA buffers. Initial stack pointer must be below the top of RAM: the bootloader rejects SP = 0x20020000, so use 0x2001FFFC (4-byte aligned)
CCM 0x10000000 64 KB CPU (D-bus) only: not reachable by any DMA controller. Not zeroed and not initialized by the startup code: anything placed there starts with garbage and initializers are ignored unless the startup code copies them
Backup SRAM 0x40024000 4 KB battery backed only if VBAT is supplied (unverified on this board); unused

Main RAM and CCM are both nearly full in a full build (frame buffers, flash sector buffer, calibration copy, USB buffers, data-service buffers); plan buffers explicitly.


2. Pin assignments

All 100 pins that matter, by port. "In" pins have the pull noted. Active levels are what the board needs.

2.1 Port A

Pin Dir Function Notes
PA0 analog ADC1_IN0: volume potentiometer wiper log-taper pot, see 11.2
PA1 analog ADC1_IN1: battery voltage divider see 11.1; also senses the power switch (11.3)
PA2 out UHF receiver 5 V switch high = UHF RX chain powered
PA3 analog ADC1_IN3: microphone level (VOX detector)
PA4 analog DAC_OUT1: PA power control reference (APC) 12-bit, buffered; see 7
PA5 out VHF receiver 5 V switch high = VHF RX chain powered
PA6 out keypad row 0 drive push-pull high when scanning this row, floating otherwise
PA7 out power latch high keeps the radio powered after the switch; low lets it turn off
PA8 AF4 open drain I2C3_SCL (AT1846S) 400 kHz
PA9 out GPS module power enable high = on; the pin has a USART1_TX function but is used as a GPIO (no TX to the GPS)
PA10 AF7 USART1_RX from GPS 9600 8N1, RX only
PA11 AF10 USB_OTG_FS_DM
PA12 AF10 USB_OTG_FS_DP
PA13 out microphone bias/power switch high = mic powered. This is SWDIO: once driven as GPIO, SWD only works with connect-under-reset
PA14 SWCLK debug clock left as SWCLK
PA15 AF6 I2S3_WS (from HR_C6000)

2.2 Port B

Pin Dir Function Notes
PB3 AF5 SPI1_SCK (SPI flash)
PB4 AF5 SPI1_MISO (SPI flash DO)
PB5 AF5 SPI1_MOSI (SPI flash DI)
PB8 out speaker mute high = muted (reset state high)
PB9 out audio power amplifier enable high = on (reset state high)
PB11 in, pull-up, EXTI11 both edges rotary encoder B
PB12 out HR_C6000 voice SPI chip select (V_CS) idle high
PB13 AF5 SPI2_SCK (HR_C6000 voice port)
PB14 AF5 SPI2_MISO
PB15 AF5 SPI2_MOSI

2.3 Port C

Pin Dir Function Notes
PC0 in, EXTI0 falling HR_C6000 TIME_SLOT_INTER
PC1 in, EXTI1 falling HR_C6000 SYS_INTER
PC2 in, EXTI2 falling HR_C6000 RF_TX_INTER the chip's RF_RX_EN / RF_TX_EN outputs are not routed to the MCU
PC4 out PA enable 2: power control (APC) loop enable high = on
PC5 out PA enable 1: TX supply high = on
PC6 out PA band select (DM-1701, RT84, MD-UV380 5 W) low = VHF PA, high = UHF PA. On the MD-UV390 Plus 10 W this pin is the LCD backlight instead
PC7 out, open drain CTCSS/DCS PWM output idles high; not used (the AT1846S generates sub-audio tones)
PC8 out beep PWM output idles low; not used (beeps go out as PCM via the HR_C6000)
PC9 AF4 open drain I2C3_SDA (AT1846S)
PC10 AF6 I2S3_CK (from HR_C6000)
PC11 AF5 I2S3ext_SD: MCU -> HR_C6000 PCM
PC12 AF6 I2S3_SD: HR_C6000 -> MCU PCM
PC14/PC15 LSE 32.768 kHz crystal

2.4 Port D

Pin Dir Function Notes
PD0 AF12 / in pull-down LCD D2 (FSMC_D2) / keypad column 2 shared, section 9 and 10
PD1 AF12 / in pull-down LCD D3 (FSMC_D3) / keypad column 3
PD2 out keypad row 1 drive
PD3 out keypad row 2 drive (also the side-key row)
PD4 AF12 LCD RD (FSMC_NOE)
PD5 AF12 LCD WR (FSMC_NWE)
PD6 out LCD chip select GPIO, active low (not an FSMC NE pin)
PD7 out SPI flash chip select idle high. (This pin is FSMC_NE1 but is not used as such)
PD8 out LCD backlight (DM-1701, RT84, MD-UV380 5 W) high = on, PWM by DMA (9.3). On the Plus 10 W this pin is the PA band select instead
PD9 out RX audio source multiplexer default high (HR_C6000 line out to the speaker amp); driven low to route the AT1846S audio output directly (used while sending AFSK audibly). Polarity meaning unverified
PD12 AF12 LCD RS / D-C (FSMC_A17) 0 = command, 1 = data
PD13 out LCD reset active low
PD14 AF12 / in pull-down LCD D0 (FSMC_D0) / keypad column 0
PD15 AF12 / in pull-down LCD D1 (FSMC_D1) / keypad column 1

2.5 Port E

Pin Dir Function Notes
PE0 out green LED high = on
PE1 out red LED high = on
PE2 out HR_C6000 config SPI chip select (U_CS) idle high, bit-banged port
PE3 out HR_C6000 config SPI clock bit-banged, idles high
PE4 out HR_C6000 config SPI MOSI
PE5 in HR_C6000 config SPI MISO
PE6 out HR_C6000 PWD (power down) high = powered down, low = running. Reset state high
PE7 AF12 / in pull-down LCD D4 (FSMC_D4) / keypad column 4
PE8 AF12 / in pull-down LCD D5 / keypad column 5
PE9 AF12 / in pull-down LCD D6 / keypad column 6
PE10 AF12 / in pull-down LCD D7 / keypad column 7
PE11 in, pull-up PTT (side) active low
PE12 in, pull-up external PTT (accessory jack) active low
PE14 in, pull-up, EXTI14 both edges rotary encoder A sampled with B on every edge of either (10.3)

2.6 Port H

Pin Function
PH0 / PH1 HSE external clock input (bypass)

2.7 Reset-time output levels

Set before configuring as outputs: C6000 CS and PWD high; C6000 CLK/MOSI low; LEDs off; both RX 5 V switches, all keypad rows, power latch, mic power low; PA enables, PA band select and beep low; voice CS, speaker mute and amp enable high; backlight low; RX audio mux and flash CS high; CTCSS PWM high. The application drives the power latch high as its first action, otherwise the radio turns off when the user releases nothing more than the power switch transient.

Transmitter safe state (board_rf_safe, crash-log.md 8), at every fault, reset and stub entry and first in main(): PA_EN1 (PC5) and PA_EN2 (PC4) low, PA band select (PC6) low, the APC DAC (PA4, DAC1 channel 1) 0 when its clock is on. The AT1846S and HR_C6000 keep their state until the next start.


3. Interrupts, DMA, timers

3.1 Interrupt priorities

RTOS kernel at 7 (lowest); RTOS "max syscall" priority 5: ISRs that call RTOS APIs must be at 5..15.

IRQ Priority Source
TIM8_TRG_COM_TIM14 3 1 kHz HAL/application time base (TIM14)
EXTI0 5 HR_C6000 timeslot interrupt (PC0)
EXTI1 5 HR_C6000 system interrupt (PC1)
EXTI2 5 HR_C6000 RF TX interrupt (PC2)
TIM6_DAC 5 AFSK bit clock (TIM6), DAC underrun
EXTI15_10 6 rotary encoder A (PE14, EXTI14) and B (PB11, EXTI11)
DMA1_Stream0 6 I2S3 RX
DMA1_Stream5 6 I2S3ext TX
USART1 6 GPS
OTG_FS 6 USB
DMA2_Stream1 7 TIM1 CC1 backlight pattern
DMA2_Stream2 7 USART1 RX
DMA2_Stream4 7 ADC1
PendSV 7 RTOS

The three HR_C6000 interrupts only matter in DMR mode; in FM they can be ignored.

3.2 DMA

Controller / stream / channel Peripheral Direction Width Mode Priority
DMA1 S0 ch0 SPI3_RX (I2S3 RX) P->M 16 bit circular low
DMA1 S5 ch2 I2S3_EXT_TX M->P 16 bit circular low
DMA2 S0 ch0 memory-to-memory (frame buffer -> LCD data address) M->M 8 bit, FIFO on normal low
DMA2 S1 ch6 TIM1_CH1 (backlight pattern -> GPIO BSRR) M->P 32 bit circular high
DMA2 S2 ch4 USART1_RX P->M 8 bit circular low
DMA2 S4 ch0 ADC1 P->M 16 bit circular low

Memory-to-memory transfers on the F4 are only possible on DMA2. The frame buffer source must be in main SRAM (not CCM).

3.3 Timers

Timer Clock Setting Use
TIM1 72 MHz prescaler 0, period 7100 (4000 in eco), CC1 timing mode, DMA request on CC1 backlight software PWM: about 10.1 kHz DMA rate, 100 steps -> about 101 Hz PWM
TIM2 72 MHz prescaler 1024, period 1024 configured, no use found (unverified)
TIM3 72 MHz prescaler 1023, period 1000, TRGO on update ADC1 trigger, about 70.2 Hz scan rate
TIM4 72 MHz prescaler 7200, period 1000 configured, no use found (unverified)
TIM6 72 MHz prescaler 1, period 30000 -> 1200 Hz; prescaler 7 -> 300 Hz AFSK (APRS) bit clock
TIM14 72 MHz 1 MHz count, period 1000 1 kHz time base

4. HR_C6000 connection

4.1 Overview

Function MCU side Notes
Configuration port (U_SPI) bit-banged on PE2 (CS), PE3 (CLK), PE4 (MOSI), PE5 (MISO) 8-bit transfers, MSB first
Voice port (V_SPI) SPI2 hardware on PB13/14/15, CS on PB12 (GPIO) used only for the 27-byte vocoder frame buffer (page 3)
Audio I2S3 full duplex, MCU is slave 8 kHz PCM both ways
Interrupts PC0 timeslot, PC1 system, PC2 RF TX, falling edge
Power PE6 = PWD, high = sleep
Clock chip XTALI from a board oscillator the PLL values used (0x0B = 0x28, 0x0C = 0x33) are the datasheet's row for a 29.4912 MHz XTALI giving 49.152 MHz. Oscillator frequency on the board unverified

4.2 Configuration port timing

Mode 3 equivalent: clock idles high; for each bit MOSI is set, CLK driven low, MISO sampled, CLK driven high (the chip latches on the rising edge). No added delays were needed at 72 MHz with GPIO writes (about 1-2 MHz effective). CS low for the whole transaction.

Framing (same as the GD-77 family, see modem-hrc6000.md 1.1):

Transaction Bytes sent
write one register page, register, value
read one register page OR 0x80, register, dummy; data comes back in the third byte
write extended (11-bit) address page OR 0x40, address bits 7:0, address bits 10:8, value
block write page, start register, n data bytes (up to about 130 used)
block read page OR 0x80, start register, n dummy bytes (up to 0x60 used)

The port is shared by task code and ISRs: transactions must be atomic with respect to the HR_C6000 ISRs (mask or lock around each transaction).

4.3 Voice port

SPI2 master, 8 bit, MSB first, CPOL = 1, CPHA = first edge (mode 2), prescaler 8 -> 4.5 MHz, software CS on PB12. Framing: write = page 0x03, register 0x00, then 27 AMBE bytes; read = 0x83, 0x00, then 27 bytes clocked in (the data follows the two header bytes). Up to 32 data bytes per transfer.

4.4 I2S audio

Item Value
Peripheral SPI3 in I2S mode, slave, Philips standard, 16-bit data in a 32-bit frame (16B extended), CPOL high, MCLK not output
Clock and WS driven by the HR_C6000 (its codec runs at 12.288 MHz)
Rate 8 kHz
RX (PC12, I2S3_SD) PCM from the HR_C6000 codec ADC (microphone)
TX (PC11, I2S3ext_SD) PCM to the HR_C6000 codec DAC (decoded voice, beeps, voice prompts)
Buffers double buffered blocks of 160 samples (20 ms), half and full transfer interrupts
Start start the DMA right after a WS rising edge (wait for WS low, then high, then low, then high) so the channels line up

The HR_C6000 codec register 0xE0 value 0xC9 says "I2S slave" in its documentation while the STM32 is also slave; in practice the chip drives CK and WS on this board (unverified which register bit makes it master).

4.5 Power and start-up

  1. PWD low (PE6), wait at least 10 ms.
  2. Program the PLL while running from XTALI, switch to the PLL. The datasheet asks for more than 500 us between writing 0x0B/0x0C and switching 0x0A back to the PLL; a driver should wait.
  3. Continue with the init sequence in modem-hrc6000.md section 3.

Power down: PWD high. The microphone power (PA13) is usually switched with the receiver; some external speaker-mics generate false PTT events when their bias is removed, so keep PA13 high during receive power saving.


5. AT1846S RF transceiver

5.1 Bus and part

Item Value
Count one AT1846S covers both bands on the DM-1701 / RT84 / MD-UV380 (the MD-2017 has two, see 17)
Bus I2C3 hardware (PA8 SCL, PC9 SDA), 400 kHz, fast mode duty 2
7-bit address 0x2E (the datasheet address with SEN high; 0x71 if SEN were low). Shifted 8-bit form 0x5C
Write address+W, register, data high byte, data low byte
Read address+W, register, then repeated transaction address+R, 2 bytes (high first). A stop between the two works
Retries the bus occasionally NAKs; retry up to 3 times with 1 ms between
Reference 26 MHz assumed (reg 0x04 bit0 = 0 and reg 0x30 bit14 = 1 select 25.6/26 MHz and 26/13 MHz, i.e. 26 MHz). Unverified on the board
Paging reg 0x7F selects the register page (0 or 1); page 1 holds the AGC table

Caching the last value written per register and page avoids redundant I2C traffic (writes that do not change a value can be skipped; read-modify-write can use the cache). I2C3 is used from task code and, for burst-by-burst RF switching, from ISRs: an ISR must not start a transfer while the task owns the bus (check the peripheral state first and skip the action rather than block).

5.2 Init sequence (data)

"wait N ms" rows are delays, not register writes.

Step Register = value Purpose
1 0x30 = 0x0001 soft reset
2 wait 50 ms
3 0x30 = 0x0004 power on (pdn_reg)
4 0x04 = 0x0FD0 clock mode 25.6/26 MHz
5 0x0A = 0x7C20 default value
6 0x13 = 0xA100 undocumented
7 0x1F = 0x1001 GPIO6 = squelch out, GPIO1 = CSS out
8 0x31 = 0x0031 undocumented (vendor value)
9 0x33 = 0x44A5 AGC number
10 0x34 = 0x2B89 RX digital gain
11 0x41 = 0x4122 digital voice gain (bits 6:0); upper bits as found
12 0x42 = 0x1052 VOX shut threshold (per the RDA1846 manual)
13 0x43 = 0x0100 FM deviation
14 0x44 = 0x07FF RX / TX gain controls
15 0x3A = 0x00C3 squelch / voice channel config
16 0x59 = 0x0B90 deviation settings
17 0x47 = 0x7F2F undocumented (same as other AT1846S radios)
18 0x4F = 0x2C62 undocumented
19 0x53 = 0x0094 undocumented (vendor value)
20 0x54 = 0x2A3C undocumented (vendor value)
21 0x55 = 0x0081 undocumented (vendor value)
22 0x56 = 0x0B02 squelch detection time
23 0x57 = 0x1C00 bypass RSSI low-pass filter
24 0x58 = 0x9CDD filter settings
25 0x5A = 0x06DB unknown
26 0x63 = 0x16AD pre-emphasis bypass threshold
27 0x0F = 0x8A24 unknown
28 0x05 = 0x8763 unknown (also written before every frequency change)
29 0x30 = 0x40A4 prepare calibration
30 0x30 = 0x40A6 calibration enable
31 wait 100 ms
32 0x30 = 0x4006 calibration done
33 wait 10 ms
34 12.5 kHz table (5.4) default bandwidth
35 wait 200 ms
36 (after the HR_C6000 init) 0x15 = 0x1100 IF tuning bits 12:9

DTMF tone registers (0x67-0x76) are not needed: DTMF is generated by the HR_C6000.

5.3 Register 0x30 values used

Bit meanings (datasheet): 14 xtal_mode (1 = 26/13 MHz), 13 filter_band_sel (1 = 25 kHz analog filter), 12 band_mode (1 = 25 kHz DSP), 7 mute, 6 tx_on, 5 rx_on, 3 sq_on, 2 pdn_reg, 1 chip_cal_en, 0 soft_reset.

State 12.5 kHz 25 kHz
idle (RX and TX off), before a frequency write 0x6006 0x7006
receiver off at power-down 0x4006 0x7006
RX on 0x6026 0x7026
FM TX 0x6046 0x7046
DMR TX 0x60C6 0x60C6 (same value for both)

The 12.5 kHz setting keeps filter_band_sel = 1 (the 25 kHz analog filter) with the 12.5 kHz DSP mode, which is what works on these radios. Bit 1 (chip_cal_en in the datasheet) stays set in normal operation, as found.

5.4 Bandwidth tables (data)

Send the whole table, then update reg 0x30: keep the other bits, set bits 13:12 to 11 (25 kHz) or 10 (12.5 kHz), clear tx_on/rx_on, then set rx_on.

Register 12.5 kHz 25 kHz Purpose
0x3A 0x44CB 0x40CB bandwidth related squelch config
0x15 0x1100 0x1F00 IF tuning bits 12:9
0x32 0x4495 0x7564 AGC target power
0x3A 0x00C3 0x00C3 modu_det_sel (rewritten)
0x59 0x0B90 0x0B90 deviation; bits 5:0 are the AT1846S CTCSS/DCS deviation. Not doubled for 25 kHz: the 25 kHz mode already doubles it (0x0BA0 measured 4.0× the 12.5 kHz tone deviation on air, 0x0B90 gives 2.0×)
0x3F 0x29D1 0x29D1 rssi3_th
0x3C 0x1B34 0x1B34 pk_det_th
0x48 0x19B1 0x1E38 noise1_th
0x60 0x0F17 0x0F17 noise2_th
0x62 0x1425 0x3767 modu_det_th
0x65 0x2494 0x248A th_sif for squelch RSSI detect
0x66 0xEB2E 0xFF2E rssi_comp and AFC range
0x7F 0x0001 0x0001 page 1
p1:0x06 0x0014 0x0024 AGC table
p1:0x07 0x020C 0x0214 AGC table
p1:0x08 0x0214 0x0224 AGC table
p1:0x09 0x030C 0x0314 AGC table
p1:0x0A 0x0314 0x0324 AGC table
p1:0x0B 0x0324 0x0344 AGC table
p1:0x0C 0x0344 0x0384 AGC table
p1:0x0D 0x1344 0x1384 AGC table
p1:0x0E 0x1B44 0x1B84 AGC table
p1:0x0F 0x3F44 0x3F84 AGC table
p1:0x12 0xE0EB 0xE0EB AGC table
0x7F 0x0000 0x0000 back to page 0

5.5 Mode tables (data)

FM (analog) mode:

Register = value Purpose
0x33 = 0x44A5 AGC number
0x41 = 0x4431 digital voice gain
0x42 = 0x10F0 VOX shut threshold
0x43 = 0x00A9 FM deviation
0x58 = 0xBC85 high/low pass filters on, de-emphasis off (de-emphasis is done in the HR_C6000)
0x44 = 0x06CC internal volume about 80 %
0x3A = 0x00C3 modu_det_sel
0x40 = 0x0030 undocumented; bit 0 = pass low-frequency audio. Cleared for FM

DMR mode (the HR_C6000 needs the raw 4FSK baseband):

Register = value Purpose
0x40 = 0x0031 undocumented; bit 0 set passes low-frequency audio (needed for 4FSK)
0x15 = 0x1100 IF tuning
0x32 = 0x4495 AGC target
0x3A = 0x00C3 no mic input to the AT1846S modulator
0x3C = 0x1B34 pk_det_th
0x3F = 0x29D1 rssi3_th
0x41 = 0x4122 digital voice gain
0x42 = 0x1052 VOX shut threshold
0x43 = 0x0100 deviation
0x48 = 0x19B1 noise1_th
0x58 = 0x9CDD all audio filters off
0x44 = 0x07FF AF gain 100 %. Lower values (e.g. 0x00CF-ish) give better BER on strong signals but fail to decode some hotspots; the DM-1701 needs more AF gain than the MD-UV380

5.6 Frequency setting

The synthesizer word is in units of 1/16 kHz (62.5 Hz): word = f(Hz) / 62.5 = f(MHz) x 16000. Registers: 0x29 holds word bits 29:16 (in bits 13:0), 0x2A holds bits 15:0. Example from the datasheet: 409.75 MHz -> 0x640960 -> 0x29 = 0x0064, 0x2A = 0x0960.

Write sequence (RX or TX frequency):

Step Action
1 0x30 = idle value (5.3)
2 0x05 = 0x8763 ("normal" frequency mode)
3 0x29 = high word
4 0x2A = low word
5 0x49 = 0x0C15 (squelch open/shut thresholds; see 5.9)
6 0x30 = RX on value
7 set the PA DAC: the TX drive value when this was the TX frequency, 0 when RX

TX frequency offset (empirical): with the normal TX sequence (RX frequency written, then TX frequency written with RX on, then TX on) the carrier is about 3 kHz low; RadioFoundry adds +3.00 kHz (12.5 kHz) or +6.15 kHz (25 kHz FM) to the TX word. With a different sequence (frequency written with the receiver off, then straight to TX) there is no offset and adding it puts the carrier +2.93 kHz high (measured on air). The cause is unknown; a new driver should measure the actual offset of its own sequence.

Band selection for the receive front end: VHF band -> PA5 high, PA2 low; UHF -> PA2 high, PA5 low (5.8, section 6).

5.7 Sub-audio (CTCSS / DCS)

Register Field Use
0x4A ctcss1_freq = Hz x 100 TX CTCSS tone; set to 13440 (134.4 Hz, the DCS bit rate) for DCS
0x4D ctcss2_freq = Hz x 100 RX CTCSS tone to detect
0x4B / 0x4C cdcss code bits 23:16 / 15:0 the 23-bit Golay word of the DCS code, both RX and TX
0x4E bits 10:9 ctcss/cdcss_sel for TX 11 = CTCSS, 10 = DCS, 00 = none. Bits 8:6 = cdcss_sel: bit 8 = inverted, bit 7 = 1, bit 6 = 0 (23-bit)
0x3A bits 4:0 RX detect select bit 3 = CTCSS2, bit 1 = normal DCS, bit 2 = inverted DCS (bit 0 CTCSS1, bit 4 phase shift unused)
0x5B ctcss match / unmatch thresholds (high / low byte) both bytes = (25000 - tone_reg) / 1000, or 1 if tone_reg > 24000 (tone_reg = Hz x 100)
0x1C flags bit 9 ctcss1_cmp, bit 8 ctcss2_cmp, bit 7 cdcss positive match, bit 6 cdcss negative match, bit 0 sq_cmp

Recipes (as values):

Action Writes
TX CTCSS on 0x4A = tone; 0x4D = 0; 0x4B = 0; 0x4C = 0; 0x4E high byte: keep bits under mask 0xF9, set 0x06
TX sub-audio off 0x4A = 0; 0x4D = 0; 0x4E high byte: keep mask 0xF9, set 0x00
TX DCS 0x4A = 13440; 0x4D = 0; 0x4B = Golay bits 23:16; 0x4C = bits 15:0; 0x4E high byte: keep mask 0x38, set 0x04 (normal) or 0x05 (inverted)
RX CTCSS 0x4A = 0; 0x4B = 0; 0x4C = 0; 0x4D = tone; 0x5B = threshold in both bytes; 0x3A low byte: keep mask 0xE0, set 0x08
RX DCS 0x4A = 13440; 0x4D = 0; 0x4B/0x4C = Golay word; 0x4E high byte as TX DCS; 0x3A low byte: keep mask 0xE0, set 0x02 (normal) or 0x04 (inverted)
RX off 0x4A = 0; 0x4D = 0; 0x4E high byte keep mask 0xF9, set 0x00
check read 0x1C: require bit 0 (sq_cmp); for DCS also bit 7 (normal) or bit 6 (inverted); for CTCSS also bit 8 (high byte bit 0)

DCS word: the 9-bit code (octal digits as 3-bit groups, e.g. 023 -> 0x013), with bit 11 set (the fixed "100" marker: word bits 11:9 = 100), and the 11 Golay(23,12) parity bits of the 12-bit value (marker + code) in bits 22:12. Generator polynomial x^11 + x^10 + x^6 + x^5 + x^4 + x^2 + 1 (0xC75). Compute it; no table is needed.

The chip sends the word MSB first (bit 22 first) and compares received bits in the same order, while DCS sends code bit 0 first. So 0x4B/0x4C take the word bit-reversed: 023 -> 0x763813 -> reversed 0x640E37 -> 0x4B = 0x0064, 0x4C = 0x0E37. The unreversed word (0x0076 / 0x3813, as the AT1846S programming guide shows) went out as D023 read backwards on the SDR bench, and the radio accepted no other radio's DCS; with the reversed word it sends and receives D023N.

DCS turn-off tail (radio-features.md 3.3a): 0x4E bits 10:9 from 10 (DCS) to 11 (CTCSS) for the tail. 0x4A already holds 13440, so the chip sends 134.4 Hz as a CTCSS tone; back to 10 (and the polarity in bits 8:6, kept) on PTT again. The chip's own tail elimination (0x30 bit 11, 0x4E bits 15:14 phase shift) is a CTCSS feature and stays off. Not yet measured on the SDR bench.

5.8 Tones, voice channel and TX audio source

Register Use
0x3A bits 14:12 voice_sel: 000 nothing (the mode tables' 0x00C3), 001 tone1, 010 tone2, 011 tone1 + tone2, 100 microphone (AT1846S programming guide, TX voice channel select). Bits 6:4 are squelch / sub-audio detect enables (gotchas 3.10)
0x7A bit 14 single tone enable (bit 15 DTMF enable)
0x35 tone1 frequency = Hz x 10
0x36 tone2 frequency = Hz x 10
0x79 = 0xC000 single tone select
0x57 bit 0 audio feedback (TX audio looped to the speaker)
0x59 bits 15:6 tone deviation; 0x1C used for tones (about 3.1 kHz peak; 0x40 measured 7.2 kHz on air), restore the saved value after
0x41 bits 6:0 voice TX gain; save before tones, restore after

On these radios FM microphone audio is processed and modulated by the HR_C6000 (two-point modulation), not by the AT1846S. The AT1846S tone generator is still used for the 1750 Hz burst and for AFSK (with the HR_C6000 microphone input muted, HR_C6000 0xE0 bit 6 cleared): voice_sel = tone1 makes the AT1846S modulate it, as it does its CTCSS. With voice_sel left at 000 the carrier went out unmodulated (gotchas 3.10); the fixed path is verified on air (HIL 2026-10-10: the burst, the roger beep and APRS AFSK, docs/testing/on-air-coverage.md).

Morse keying for the hotspot's CW ID (tx_tone_key, boards/dm1701/radio_dm1701.c): with the tone set up by tx_tone, key up writes the 0x59 tone deviation bits 15:6 to 0 and key down back to the tone value (0x1C) (bits 5:0 kept from the saved value), so the carrier stays up and the tone switches without a retune. On air: test_hotspot_cwid checks the Morse tone (docs/testing/on-air-coverage.md).

RadioFoundry's tx_tone(hz) (boards/dm1701/radio_dm1701.c, the roger beep and the 1750 Hz burst of radio-features.md 3.3) uses tone1 with 0x79 = 0xC000 and tone deviation 0x1C, and mutes the HR_C6000 microphone path (page 4 0xE0 bit 6). tx_tone(0) and every return to receive restore the saved registers.

DTMF (tx_dtmf, radio-features.md 3.7) is the same path with tone2 as well: 0x35 = the low tone, 0x36 = the high tone, voice_sel = 011 (tone1 + tone2), single tone select and enable as above (0x7A bit 15, the chip's own DTMF mode, times each digit itself and isn't used). The deviation is DTMF_DEV in 0x59 bits 15:6, 0x1C like the single tone until measured for the pair. A new digit only rewrites 0x35 and 0x36.

5.9 RSSI, noise, squelch

Register Field
0x1B high byte rssi_db (1 dB units)
0x1B low byte noise_db (1 dB units)
0x1A low byte mic_db (0.5 dB units), not used (mic level comes from the MCU ADC on these radios)
0x49 = 0x0C15 th_h_sq = 0x18, th_l_sq = 0x15 (hardware squelch thresholds, 1 dB units)

Conversion to dBm (measured): UHF dBm = rssi - 151; VHF dBm = -164 + rssi x 32 / 27. The same formulas apply to noise_db. A software squelch on noise_db works well: carrier present when noise_db is below a threshold (RadioFoundry: 70 minus 3 per squelch step, 3 dB hysteresis), sampled every 25 ms.

Roaming reads this RSSI as the site signal (dmr-roaming.md section 3). A survey visit relies on the AT1846S RSSI and the HR_C6000's sync acquisition needing well under 100 ms on a continuous BS outbound (a burst every 30 ms); four 25 ms readings follow.

5.10 APRS AFSK transmit

RadioFoundry's hal_afsk_t (boards/dm1701/afsk_dm1701.c, aprs.md 5) uses the AT1846S tone generator as the voice source, as for the roger beep. TIM6 (APB1 timer clock 72 MHz, prescaler 2, ARR = clk / 2 / 1200 - 1, interrupt priority 5, no RTOS calls) interrupts at every symbol boundary. It writes the next tone to register 0x35 when the tone changes: one I2C write of about 0.1 ms in an 833 us symbol. The radio core keeps the I2C bus free meanwhile. At the end it force-writes 0x35, because the interrupt bypassed the driver's cache. The level (0-100, 50 default) scales the tone deviation register 0x59 around the driver's value.

The tone generator is the voice source only with reg 0x3A bits 14:12 (voice_sel) = 001 and 0x7A bit 14 (single tone) set. An earlier build wrote bits 6:4 and keyed a silent carrier (gotchas.md 3.10, fixed in 694249b). tests/hil/test_aprs.py measured the result on air (aprs.md 13).

5.11 Receiver sleep

RadioFoundry's rx_sleep(true) (receiver battery save, radio-features.md 5.4) puts the AT1846S in idle and switches the receive supply off. rx_sleep(false) retunes.


6. RF path control (PA, LNA, band)

Signal Pin RX TX VHF TX UHF Off
UHF RX 5 V PA2 high if UHF low low low
VHF RX 5 V PA5 high if VHF low low low
TX supply (PA enable 1) PC5 low high high low
APC loop enable (PA enable 2) PC4 low high high low
PA band select PC6 (PD8 on Plus 10 W) don't care low high low
APC reference DAC1 / PA4 0 drive value drive value 0
HR_C6000 PWD PE6 low low low high

Ordering:

  • Enter TX: both RX supplies off; configure the HR_C6000 for FM TX or DMR; AT1846S reg 0x30 to the TX value; then PA enable 1 high, PA band select, then PA enable 2 high. The DAC drive is set when the TX frequency is written.
  • Enter RX: PA enable 1 and 2 low first; RX supply for the band; AT1846S reg 0x30 to the RX value; HR_C6000 to FM RX or DMR.
  • Burst-by-burst DMR TX (the PA follows the bursts): to TX, reg 0x30 receiver off, the TX frequency, reg 0x30 DMR TX, both RX supplies off, then the PA; back to RX, the PA enables low, reg 0x30 receiver off, the RX frequency if it differs, the RX supply for the band, reg 0x30 RX on. The receiver must work between bursts (an SFR's inbound slot, sfr.md 3). The "transmitting" state of the radio must therefore be taken from the HR_C6000 slot state, not from the PA pins: the last burst of a call (the terminator) is still in flight after the PA logic has gone back to RX once.
  • Power-down: PA enables low, both RX supplies low, mic power optionally low, HR_C6000 PWD high, AT1846S reg 0x30 = receiver off value.

Powering both receivers together and powering only the band in use both work; RadioFoundry powers only the band in use.


7. TX power control (DAC)

DAC channel 1 on PA4, 12-bit right aligned, output buffer on, no trigger: 0..4095. The DAC value sets the APC loop reference: higher = more power. The calibration table gives 8-bit points scaled x16 to 12 bits (section 12).

Power levels from four calibrated curves (very low, low, mid, high), each interpolated against frequency (12.3):

Level Nominal (5 W radios) DAC value
0 50 mW very low - 160, or 0 if very low <= 160
1 250 mW very low
2 500 mW very low + (low - very low) x f0
3 750 mW very low + (low - very low) x f1
4 1 W low
5 2 W mid
6 3 W mid + (high - mid) x f2
7 4 W mid + (high - mid) x f3
8 5 W high
9 user user-set DAC value

Interpolation fractions (empirical):

Radio Band f0 f1 f2 f3
DM-1701 / RT84 VHF 0.45 0.75 0.25 0.53
DM-1701 / RT84 220 MHz 0.45 0.75 0.25 0.53 (estimate, untested)
DM-1701 / RT84 UHF 0.46 0.73 0.14 0.36
MD-UV380 5 W VHF / 220 / UHF 0.35 / 0.38 / 0.40 0.70 0.34 / 0.30 / 0.25 0.61 / 0.59 / 0.55
MD-UV390 Plus 10 W VHF / 220 / UHF 0.58 / 0.58 / 0.55 0.83 / 0.83 / 0.75 0.21 / 0.21 / 0.17 0.45 / 0.45 / 0.43

On the 10 W radio levels 5-8 are 2 W, 3 W, 5 W, 10 W: level 5 = low + (mid - low) x f2, level 6 = low + (mid - low) x f3, level 7 = mid, level 8 = high. Clamp to 4095.

Without a loaded calibration RadioFoundry drives a fixed DAC value of 300 (DM1701_DAC_UNCALIBRATED in boards/dm1701/radio_dm1701.c), a very low drive.


8. Audio paths

Path Route
FM RX antenna -> AT1846S demodulator -> AT1846S audio out -> HR_C6000 line in -> HR_C6000 FM feed-through (page 4 reg 0x36 = 0x02) with de-emphasis (0x34) -> HR_C6000 DAC line out -> RX audio mux (PD9 high) -> speaker amp
DMR RX AT1846S baseband (filters off, reg 0x40 bit 0 set) -> HR_C6000 4FSK demodulator -> 27-byte AMBE frames over V_SPI -> MCU codec -> PCM over I2S -> HR_C6000 DAC -> amp
FM TX mic (bias on PA13) -> HR_C6000 mic input (codec ADC, mic gain 0xE4) -> HR_C6000 FM processing -> two-point modulation outputs (MOD1 to the reference oscillator, MOD2 to the VCO)
DMR TX mic -> HR_C6000 codec ADC -> PCM over I2S -> MCU codec -> AMBE frames over V_SPI -> HR_C6000 framing and two-point 4FSK modulation
Beeps / prompts MCU-generated PCM (8 kHz sine synthesis, prompts decoded by the codec) -> I2S -> HR_C6000 DAC -> amp
Mic level / VOX separate analog tap on PA3 (ADC)
Speaker amp enable PB9 high and mute PB8 low to hear audio; amp off and mute high otherwise

The RX audio mux on PD9 is driven low when the AT1846S output (e.g. its tone generator during audible AFSK) should go straight to the amp (unverified).

APRS receive takes FM receive audio from the I2S stream (squelch gated, 8 kHz): no ADC pin sees the discriminator, but the HR_C6000 puts its FM receive audio on I2S (21). The audio tap says so (AUDIO_TAP_FM_RX), and the core reads it through the shared stream (aprs.md 5.3). The stream is gated by the FM feed-through (4:0x36), so it stays on at volume zero (11.2). The HR_C6000's RX RAM carries the same audio ungated, for a later receive path of the board's own.


9. Display

9.1 Panel and bus

Item Value
Panel 160 x 128 color TFT, RGB565, 8-bit 8080 interface
DM-1701 visible area 160 x 120: the panel's first 8 rows are hidden; RadioFoundry offsets all row addresses by 8 and clears rows 0-7 once
Controllers seen an ST7735-class / Himax HX8353-E-class controller (types 0 and 1) and a different controller that needs the 0xFE/0xEF "inter command" unlock (types 2 and 3; GalaxyCore GC9106-like, unverified)
Interface FSMC bank 1, sub-bank 1 (address 0x60000000), SRAM type, 16-bit width configured, byte accesses used on D0-D7
Command address 0x60000000 (A17 = 0 -> RS low)
Data address 0x60040000 (with a 16-bit bank, HADDR bit 18 drives FSMC_A17 = PD12 = RS high)
Chip select PD6, GPIO, active low, asserted around each burst
Reset PD13, active low: hold low 20 ms, then high
FSMC BCR1 MBKEN, MTYP = NOR (bit 3), MWID = 16 bit, FACCEN, WREN (value 0x10D9; bit 7 undocumented, unverified)
FSMC BTR1 MBKEN-like value 0x00100517; the register bits written are MBKEN, MUXEN, MTYP_0, MWID_0, BURSTEN, WRAPMOD, CBURSTRW (meaning unclear, unverified)
Timing a very short delay after each command/data byte was needed with byte-by-byte writes
Full refresh 160 x 128 x 2 bytes = 40,960 bytes by DMA2 memory-to-memory to the data address

After each burst RadioFoundry writes 0x00 to the data address so the shared data lines rest low before the keypad is scanned.

The case's lens hides about 5 px at the left and right edges. RadioFoundry's safe-area insets default to 5 px left and right, none top and bottom (CONFIG_DISPLAY_INSET* in boards/dm1701/config.mk, ui.md 1).

9.2 Controller type and orientation

The panel type is stored by the factory in SPI flash security register 3, byte 0x1D (read with command 0x48 at address 0x00301D), low two bits:

Value DM-1701 meaning MD-UV380 meaning
0 normal rotated 180 degrees
1 rotated 180 degrees normal
2 vertically flipped; treated as type 3 with RGB order horizontally flipped
3 normal, BGR order rotated 180 degrees

MD-UV380 panels are always treated as RGB order. MADCTL (0x36) written at the end of init: 0x60 for type 1, 0xE0 for type 2, 0xA0 for types 0 and 3. Color: RGB565 big-endian on the bus (high byte first); in BGR panels red and blue fields are swapped.

On the MD-UV380, security register 3 byte 0x1C = 0xF4 identifies a Plus 10 W radio (seen, not used).

9.3 Init sequences (data)

Common: reset pulse (CS high, RST low 20 ms, RST high), CS low, then:

Types 2 and 3 (controller with inter-command unlock), command followed by parameter bytes:

Command Parameters
0xFE (none)
0xEF (none)
0xB4 0x00
0xFF 0x16
0xFD 0x4F (type 2) or 0x40 (type 3)
0xA4 0x70
0xE7 0x94, 0x88
0xEA 0x3A
0xED 0x11
0xE4 0xC5
0xE2 0x80
0xA3 0x12
0xE3 0x07
0xE5 0x10
0xF0 0x00
0xF1 0x55
0xF2 0x05
0xF3 0x53
0xF4 0x00
0xF5 0x00
0xF7 0x27
0xF8 0x22
0xF9 0x77
0xFA 0x35
0xFB 0x00
0xFC 0x00
0xFE (none)
0xEF (none)
0xE9 0x00
wait 20 ms

Types 0 and 1 (ST7735-class):

Command Parameters Meaning (ST7735 naming)
0x11 sleep out
wait 120 ms
0xB1 0x05, 0x3C, 0x3C frame rate, normal mode
0xB2 0x05, 0x3C, 0x3C frame rate, idle
0xB3 0x05, 0x3C, 0x3C, 0x05, 0x3C, 0x3C frame rate, partial
0xB4 0x03 inversion control
0xC0 0x28, 0x08, 0x04 power control 1
0xC1 0xC0 power control 2
0xC2 0x0D, 0x00 power control 3
0xC3 0x8D, 0x2A power control 4
0xC4 0x8D, 0xEE power control 5
0xC5 0x1A VCOM
0x36 0x08 MADCTL (overwritten below)
0xE0 0x04, 0x0C, 0x07, 0x0A, 0x2E, 0x30, 0x25, 0x2A, 0x28, 0x26, 0x2E, 0x3A, 0x00, 0x01, 0x03, 0x13 positive gamma
0xE1 0x04, 0x16, 0x06, 0x0D, 0x2D, 0x26, 0x23, 0x27, 0x27, 0x25, 0x2D, 0x3B, 0x00, 0x01, 0x04, 0x13 negative gamma

Then for all types:

Command Parameters
0x36 MADCTL per 9.2
0x2A CASET 0x00, 0x00, 0x00, 160
0x2B RASET 0x00, 0x00, 0x00, 128 (DM-1701: 120 + 8)
0x3A COLMOD 0x05 (16 bit/pixel)
0x11 sleep out
0x29 display on

Then CS high. Writing a band of rows: RASET start/end (DM-1701: add the 8-row offset), 0x2C RAMWR, pixel bytes. Inversion: 0x21 / 0x20.

9.4 Backlight

PD8 (PC6 on Plus 10 W), active high. Dimming: a 101-entry table of 32-bit words is sent by DMA2 Stream1 (TIM1 CC1 request, circular, 100 transfers) to the port's BSRR register: entry 0 sets the pin, entry N resets it, all others are 0, so the duty is N %. 0 % and 100 % stop the DMA and drive the pin directly.


10. Keypad, side keys, encoder, PTT

10.1 Matrix

Rows are driven high one at a time (push-pull high for the row being read, input/floating for the others). Columns are the eight LCD data lines read as inputs with pull-downs; a pressed key reads high. Sequence per scan:

  1. Switch the eight data lines from FSMC to GPIO: first drive them low as outputs for a moment (about 10 loop iterations) to discharge them, then make them inputs with pull-down. Without the discharge step phantom key presses appear on some radios (a glitch when leaving FSMC mode).
  2. For each row: drive it high, wait about 100 loop iterations to settle, read the columns, return the row to input.
  3. The first key found wins (no multi-key support on the front keys).

DM-1701 / RT84 matrix:

Row (pin) D0 (PD14) D1 (PD15) D2 (PD0) D3 (PD1) D4 (PE7) D5 (PE8) D6 (PE9) D7 (PE10)
Row 0 (PA6) 1 4 7 * up arrow right function key left function key -
Row 1 (PD2) 2 5 8 0 down arrow red (back) green (menu) -
Row 2 (PD3) 3 6 9 # - top (orange) key side key 2 side key 1

The physical labels of the DM-1701's two "function" keys (right and left in the table, likely the P1/P2 or A/B keys) are unverified. Side key 1 is the one just above the PTT (it is the one held with PTT to enter the bootloader); side key 2 is the lower one (unverified ordering on the RT84).

MD-UV380 matrix (for comparison):

Row D0 D1 D2 D3 D4 D5 D6 D7
Row 0 1 2 3 4 5 6 0 *
Row 1 green up down 7 8 9 # red
Row 2 - - - - - - side key 2 side key 1

The MD-UV380 has no orange key in the matrix.

Side keys are read in the same way (row 2 high, read D5/D6/D7) and can be held together with each other and with PTT.

10.2 PTT

PE11 (internal PTT) and PE12 (external PTT on the accessory jack): inputs with pull-ups, active low; either one means PTT.

10.3 Rotary channel encoder

Quadrature, A on PE14, B on PB11 (both pull-up), with detents. The driver interrupts on both edges of B only and counts one step per B edge (-1 if A equals B after the edge, else +1). One B edge per detent means two quadrature steps per detent, resting where A equals B (00 and 11). Measured on the bench (2026-10-10, a76da0d): 10 slow and 10 fast detents each gave exactly 10 detents and 20 transitions, 11 counterclockwise gave -11 / 22, a wobble without a detent gave 0 detents (14 transitions, 12 reversals), never an illegal state.

RadioFoundry interrupts on both edges of both lines (EXTI14 for A, EXTI11 for B, one handler) and feeds every sample of the two levels to the table decoder in drivers/input/quadrature: each sample against the previous one is no change, one step either way, or both lines changed (a missed sample, ignored). A detent counts only when the lines reach a rest state with two steps the same way since the last one. The interrupt clears its pending flags before it reads the pins, so an edge during the read interrupts again.

Why counting B edges went wrong (seen as missed steps and steps backward, 2026-10-10):

  • Going one way, B is the first line to change when the knob leaves a detent. A light touch or a knob that settles back counts a step and then a step back: the focus jumps a row and returns.
  • Contact bounce on B gives several edges. EXTI has one pending flag per line, so edges close together (or while the interrupt is masked or preempted) merge, and the handler reads the level after all of them: a rise-fall-rise reads "high" twice and counts two steps for one, never in step with the detent.
  • Bounce on A was ignored, but A's level was sampled at B edges only, so a sample taken while A was still settling (a fast turn) gives the wrong direction.

The decoder makes all three harmless: bounce is a step and the same step back (net zero), merged edges are a repeated level (no change), and only whole detents count. Clockwise is positive (forward): the next channel on Home, focus down in lists (confirmed on the radio).

10.4 Power switch

The volume knob's switch interrupts the battery sense line. The MCU holds its own supply with the latch (PA7 high). When the battery reading drops below 5.5 V (11.1) the switch is treated as off: the firmware finishes its shutdown and drives PA7 low, which removes power. This also means a deeply discharged battery looks like "switch off" (unverified whether there is a second sense path).


11. Analog inputs

ADC1, 12-bit, PCLK2/8 = 9 MHz, scan of 4 ranks, 480-cycle sampling, triggered by TIM3 TRGO (about 70 Hz), DMA2 Stream4 circular into a 4-word array:

Rank Channel Signal
1 IN0 (PA0) volume pot
2 IN1 (PA1) battery
3 IN3 (PA3) mic level
4 IN16 internal temperature sensor

11.1 Battery

Voltage in 0.1 V units = raw / 37.5 on the DM-1701 / RT84 (raw / 40.3 on the MD-UV380), plus a user trim of -5..+10 (0.1 V steps). Thresholds (0.1 V): full 8.2 V; low-battery hysteresis 6.2 / 6.4 V; "power switch off" below 5.5 V. Ignore readings for 1.5 s after power-up or wake (they settle); average with a long window (1000 samples) for display, use the latest sample for decisions.

RadioFoundry's thresholds: board.battery_low_mv 6200, battery_critical_mv 6000 (boards/dm1701/board.c, radio-features.md 5.1).

11.2 Volume pot

Log-taper pot read on PA0. Raw ranges (measured): DM-1701 min about 29, max about 2075, breakpoint about 318; MD-UV380 22..2070, breakpoint 309; MD-UV390 Plus 10 W 19..2054, breakpoint 308; RT3S 23..2020, breakpoint 317; MD-2017 32..2048, breakpoint 321. A two-segment linear approximation around the breakpoint maps it to -31..+31 dB, with a "minimum = mute" zone at the bottom (with 5 counts of hysteresis).

11.3 Mic level

PA3 gives the microphone level; convert the raw value to dB (20 log10) for VOX.

RadioFoundry's mic_level (boards/dm1701/power_dm1701.c, VOX in radio-features.md 3.4) gives 0-100: 20 log10 of the raw reading over the ADC's 72 dB, from the shared scale in drivers/audio/miclevel/miclevel.h. Not verified on the bench.

11.4 Temperature

Internal sensor: 943 counts at 25 C, 3.1 counts per C (datasheet nominal 0.76 V and 2.5 mV/C at 3.3 V reference). Uncalibrated, coarse.


12. Calibration data

12.1 Location and life cycle

Copy Location Size
Factory SPI flash security registers 1 and 2 (addresses 0x001000 and 0x002000, 256 bytes each, read with command 0x48 + 3 address bytes + 1 dummy byte) 512 bytes
Working copy SPI flash main array 0x010000 512 bytes

At start-up: read the working copy; if bytes 0x1B0-0x1B7 are not the marker 00 25 00 40 00 45 01 40, read the factory table, overwrite those 8 bytes with the marker, derive the very-low power row and apply the power conversion below, then write the result to 0x010000. Never write the security registers.

Power conversion when deriving the working copy (empirical): very low = low x k0 (always), and if converting: low x= k1, mid x= k2, high x= k3.

Radio Band k0 k1 k2 k3
DM-1701 / RT84 VHF 0.61 1.00 0.85 1.30
DM-1701 / RT84 UHF 0.65 1.00 0.86 1.70
MD-UV380 5 W VHF / UHF 0.63 / 0.72 1.00 1.00 / 1.05 1.10 / 1.25
MD-UV390 Plus 10 W VHF / UHF 0.35 / 0.40 0.65 / 0.70 1.25 / 1.20 1.40 / 1.35

12.2 Table layout (512 bytes)

UHF rows have 9 points (400-480 MHz), VHF rows 5 points. "Used" says whether the open firmware applies it.

Offset Size Meaning Used / how
0x000 1 VOX level 1 no
0x001 1 VOX level 10 no
0x002 1 RX low voltage (meaning unknown) no
0x003 1 RX high voltage (meaning unknown) no
0x004 1 RSSI reading at -120 dBm read, not used (fixed formulas in 5.9)
0x005 1 RSSI reading at -70 dBm read, not used
0x006 2 unknown
0x008 1 unknown
0x009 1 UHF reference oscillator tune yes: signed offset = value - 128, see 12.4
0x00A 2 unknown
0x00C 1 VHF reference oscillator tune yes, as UHF
0x00D 3 unknown
0x010 9 UHF high power yes, DAC/16
0x019 5 VHF high power yes
0x01E 2 unknown
0x020 9 UHF low power yes
0x029 5 VHF low power yes
0x02E 2 unknown
0x030 9 UHF RX front-end tuning no (no tuning DAC path used on this radio)
0x039 5 VHF RX front-end tuning no
0x03E 2 unknown
0x040 9 UHF squelch level 9 open no
0x049 7 unknown
0x050 9 UHF squelch level 9 close no
0x059 7 unknown
0x060 9 UHF squelch level 1 open no
0x069 7 unknown
0x070 9 UHF squelch level 1 close no
0x079 7 unknown
0x080 16 unknown
0x090 9 UHF CTCSS deviation, 67 Hz no (fixed HR_C6000 values, see 12.5)
0x099 2 unknown
0x09B 5 VHF CTCSS deviation, 67 Hz no
0x0A0 9 UHF CTCSS deviation, 151.4 Hz no
0x0A9 2 unknown
0x0AB 5 VHF CTCSS deviation, 151.4 Hz no
0x0B0 9 UHF CTCSS deviation, 254.1 Hz no
0x0B9 2 unknown
0x0BB 5 VHF CTCSS deviation, 254.1 Hz no
0x0C0 16 unknown
0x0D0 9 UHF DCS deviation no
0x0D9 2 unknown
0x0DB 5 VHF DCS deviation no
0x0E0 5 VHF squelch 9 open no
0x0E5 5 VHF squelch 9 close no
0x0EA 5 VHF squelch 1 open no
0x0EF 5 VHF squelch 1 close no
0x0F4 12 unknown
0x100 40 VHF calibration frequencies: 10 x 4 bytes BCD (5 RX/TX pairs) no
0x128 8 unknown
0x130 9 UHF DMR I gain yes, HR_C6000 4:0x45 in DMR
0x139 5 VHF DMR I gain yes
0x13E 2 unknown
0x140 9 UHF DMR Q gain yes, HR_C6000 4:0x46 in DMR
0x149 5 VHF DMR Q gain yes
0x14E 2 unknown
0x150 32 unknown
0x170 9 UHF FM I gain yes, HR_C6000 4:0x45 in FM TX
0x179 5 VHF FM I gain yes
0x17E 2 unknown
0x180 9 UHF FM Q gain yes, HR_C6000 4:0x46 in FM TX
0x189 5 VHF FM Q gain yes
0x18E 2 unknown
0x190 9 UHF mid power yes
0x199 5 VHF mid power yes
0x19E 2 unknown
0x1A0 9 UHF very low power (derived, 12.1) yes
0x1A9 5 VHF very low power (derived) yes
0x1AE 2 unknown
0x1B0 72 UHF calibration frequencies: 18 x 4 bytes BCD (9 RX/TX pairs); the first 8 bytes double as the working-copy marker no
0x1F8 8 unknown

BCD frequency format: 4 bytes little-endian BCD, 10 Hz units (00 25 00 40 = 400.02500 MHz).

12.3 Interpolation against frequency

Frequencies in 10 Hz units; above 300 MHz use the UHF row.

Row kind Band First point Step Points
power UHF 400 MHz 10 MHz 9
power VHF 135 MHz 10 MHz 5
I/Q gains, RX tuning UHF 400 MHz 10 MHz 9
I/Q gains, RX tuning VHF 136 MHz 9.5 MHz 5

index = (f - first) / step, clamped to the row; linear interpolation between point[index] and point[index+1]; above the last point, extrapolate with the slope of the last segment. Power and RX-tuning points are scaled x16 (8-bit to 12-bit DAC), gains stay 8-bit (clamp 0..255). Power uses the TX frequency; gains use the TX frequency too.

12.4 Reference oscillator offset

offset = (VHF or UHF tune byte) - 128, signed, for the band in use (TX band while transmitting, RX band otherwise). Written to HR_C6000 4:0x47 = offset, 4:0x48 = 0x03 if offset < 0 else 0x00, 4:0x04 = offset. In FM TX, 4:0x48 = 0x00 and 4:0x04 = offset.

12.5 FM deviation (fixed, not from the table)

HR_C6000 FM TX deviation coefficients used instead of the table rows:

Band / bandwidth 4:0x35 voice 4:0xA0 CTCSS 4:0xA0 DCS
UHF 25 kHz 0x24 0x09 0x02
UHF 12.5 kHz 0x12 0x06 0x01
VHF 25 kHz 0x73 (0x68 noted as the start of distortion) 0x18 0x06
VHF 12.5 kHz 0x37 0x10 0x04

13. SPI flash

13.1 Device and bus

Item Value
Part Winbond W25Q128 class, 16 MB; JEDEC 0x9F returns manufacturer 0xEF (Winbond; others may appear) and device 0x4018
Bus SPI1 master, mode 0 (CPOL 0, CPHA first edge), MSB first, 8 bit, APB2/16 = 4.5 MHz; CS = PD7 (GPIO)
Commands used 0x03 read, 0x02 page program (256 B), 0x20 4 KB sector erase, 0x06 write enable, 0x05/0x35/0x15 status 1/2/3, 0x9F JEDEC ID, 0x48 read security register (3 address bytes + 1 dummy)
Busy wait poll status 1 bit 0 with 1 ms sleeps
Init check treat "device ID != 0x4018" as a flash failure (safe mode)

13.2 Map

The flash keeps the GD-77 codeplug layout so that CPS tools interoperate: the first 64 KB emulate the GD-77's 64 KB EEPROM one-to-one, and the GD-77's 1 MB "flash" image starts at 0x020000 (offset +128 KB). Details of the codeplug records are in open-firmware-cps.md.

Address (SPI flash) GD-77-relative address Size Content
0x000000 EEPROM 0x0000 64 KB emulated EEPROM: general settings at 0x00E0 (callsign 0x00E0, DMR ID 0x00E8), DTMF contacts 0x2F88, signaling/DTMF 0x1400, APRS configs 0x1588, first 128 channels (header 0x3780, data 0x3790), last used channel per zone 0x6000, radio settings block at 0x604B, boot screen/password at 0x7518-0x755F, quick keys 0x7524, VFO A/B at 0x7590, zones 0x8000+
0x010000 (local) 512 B calibration working copy (12.1). CPS reads/writes it through a 0x10000 window
0x011000 (ours) 32 KB RadioFoundry settings ring (settings-store.md 2); no open-firmware tool uses it, the CPS shim refuses writes
0x019000 (ours) 8 KB RadioFoundry sector journal (settings-store.md 4)
0x020000 flash 0x00000 4 KB custom data: boot image and boot melody, themes (one 4 KB sector; erased when the user clears custom data)
0x050000 flash 0x30000 256 KB DMR ID database part 1 (header "Id" + type byte + record length + entry count; see the CPS spec)
0x09B1B0 flash 0x7B1B0 channels 129-1024 (bank headers 16 B before each bank, data from 0x9B1C0)
0x0A7620 flash 0x87620 contacts
0x0AD620 flash 0x8D620 RX group list lengths (76 lists), lists from 0x0AD6A0
0x0AF400 flash 0x8F400 voice prompts (header magic 'VP' = 0x5056); with the long ID-database format the ID database may overwrite this area
0x0D8000 flash 0xB8000 to 0x120000 DMR ID database part 2
0x100000 flash 0xE0000 old voice prompt location (read for compatibility)
0x120000 - 0xDFFFFF about 12.9 MB unused by the codeplug layout; GPS logs use the ID database areas when enabled
0xE00000 - 0xEFFFFF (ours) 1 MB reserved for the native codeplug (codeplug-native.md 4); the CPS shim refuses writes from here up
0xF00000 - 0xF3FFFF (ours) 256 KB staging area for a codeplug written over TFTP (tftp.md 4): the commit record, then the staged image
0xF40000 - 0xFFFFFF 768 KB unused
security register 1-2 512 B factory calibration (read only)
security register 3 256 B factory flags: 0x1C (0xF4 = MD-UV390 Plus 10 W), 0x1D (LCD type)

14. Other peripherals

Item Details
GPS (where fitted) USART1 RX on PA10, 9600 8N1, DMA2 Stream2 circular, half/full callbacks; power on PA9 (high = on)
RTC LSE 32.768 kHz, 24 h format; keeps time while the MCU has power (no separate backup battery confirmed, unverified)
LEDs PE0 green, PE1 red, active high
Backup SRAM unused
Watchdog RadioFoundry starts the IWDG and feeds it from the idle task (CONFIG_WATCHDOG, watchdog.md)

15. Bootloader and firmware images

15.1 Entering the bootloader

Hold PTT + side key 1 (the key just above PTT) while turning the power switch on. The radio enumerates as a USB DFU device, VID 0x0483 PID 0xDF11 (ST's DFU IDs, but the TYT/AnyRoad bootloader, not ST's ROM loader). The device string at index 1 reads "AnyRoad Technology". The screen stays dark. The interface string describes the memory layout in the usual DfuSe "@name/0xADDR/NN*SSKg,..." form.

15.2 Flashing protocol (DfuSe-style, as the loader drives it)

Step Request (bmRequestType 0x21 unless noted)
1 wait for idle: GETSTATUS (0xA1, request 3, length 6); while state != dfuIDLE (2) send CLRSTATUS (4) and repeat
2 vendor command DNLOAD (request 1, wValue 0, 2 data bytes) 0x91 0x01; GETSTATUS, 100 ms, GETSTATUS, wait for idle
3 vendor command 0x91 0x31, same handling (meaning of both commands unknown; required)
4 erase: for each address 0x0800C000, 0x08010000, 0x08020000, 0x08040000, 0x08060000, 0x08080000, 0x080A0000, 0x080C0000, 0x080E0000 (the loader also lists 0x08100000), DNLOAD wValue 0 with 5 bytes 0x41 + address (little endian); GETSTATUS must report dnBUSY (4) then dnIDLE (5)
5 per region (same list; sizes 0x4000, 0x10000, then 0x20000 each): set address = DNLOAD 0x21 + address; then DNLOAD data blocks of 1024 bytes with wValue = block number starting at 2 and incrementing (region 0x0800C000 uses blocks 2..0x11, 0x08010000 blocks 2..0x41, the 128 KB regions 2..0x81); after each block GETSTATUS must be dnBUSY then dnIDLE. The last block is padded with 0xFF to 1024 bytes
6 exit: set address 0x08000000, then DNLOAD with zero length; the device resets (it may not answer the final GETSTATUS)

The image is written as one contiguous stream starting at 0x0800C000: byte n of the file goes to 0x0800C000 + n.

Every listed region has the same rule: blocks 2..N, the key applied from the image start, which is block aligned at every sector start (for example 0x080A0000 - 0x0800C000 = 0x94000, a multiple of 1024). rfoundry erases and writes a region only when it has data, so an update leaves sectors 10 and 11 (the codec) out. The loader accepts a sector written on its own, with sectors left out on both sides: a RadioFoundry image that wrote only sectors 3-6 and 9 flashed through rfoundry, booted and passed the radio-only HIL group (2026-10-14).

15.3 Image encryption

The bootloader decrypts what it receives. The host XORs every byte of the plain application image with a 1024-byte per-model key: encrypted[n] = plain[n] XOR key[n mod 1024], n counted from the start of the image (address 0x0800C000). No other transformation, header or checksum. Keys (1024 bytes each, values not reproduced here; identify a key by its SHA-256):

Model Key SHA-256 First 4 bytes
DM-1701 (also used for the RT84, unverified) d983ce95fbc19fc78b913d9afc0381e3ba4ffedb480798865292d05a2fb3deff 3A 04 74 AD
MD-UV380 / MD-UV390 / MD-2017 739ce35cc8db6e72f95598effac1a01ae71dbe2a5792c1a69acaee5dbf634991 00 AA 89 89
MD-380 5254139c6b596d9413d106e6b7c0685ec245d69555cb28df98f061d6b879c4cd 2E DF 40 B5
MD-9600 c3d665b297651b52310ec098e87440b147136241359c1154d474e517b048c9ae A2 FA BB 4B

The key tables are identified by hash only. A key is derived on the user's machine from the manufacturer's firmware file, or read from the radio's bootloader (below).

Derived from a firmware file, DM-1701. The key falls out of any DM-1701 firmware file from the manufacturer. The payload is plain XOR key[i mod 1024], and in a firmware image 0x00 is the most common plain byte in every key column, so the most common cipher byte of column j is key[j] for all but a few columns; those are settled by trying each close runner-up byte until the SHA-256 of the result matches the table above. On every firmware file in codec-dm1701.md ("Firmware files": V02.01, V02.02, V02.03 with and without CSV, V02.06 CSV and noCSV) this gives the key with hash d983ce95... in under 0.1 s, needing at most a few hundred hash checks. tools/rfoundry/rfoundry/stockfw.py (derive_key) does this on the user's machine; only the hash is in the repository. The same method should work for the other models' files (unverified: no MD-UV380, MD-380 or MD-9600 file was tried).

In the bootloader, DM-1701. The bootloader (sectors 0-2) holds the key as one plain 1024-byte block: in the bootloader exactly one 1024-byte window has the hash above. RadioFoundry can read those sectors (read protection level 1 doesn't stop code running from user flash, debuglink.md 3.10), so the web flasher (bootkey.js) and rfoundry (bootkey.py) read them over READ_MEM and take the block whose SHA-256 matches. Updating a radio that runs RadioFoundry therefore needs no firmware file. The search tries every offset (4-byte aligned first) and assumes no layout, so a bootloader version that keeps the key elsewhere works too. Unverified for the other models' bootloaders; when no block matches, both clients fall back to the firmware file.

Other loader facts:

  • A manufacturer's firmware file is recognized by its first 16 bytes "OutSecurityBin" followed by two zero bytes; it must never be flashed as an application image.

15.4 Software entry to a bootloader

Goal: re-flash over USB without someone holding PTT + side key 1 at power-on (15.1). There are two possible routes, both reached from the running firmware through the debug link (docs/spec/debuglink.md 3.9-3.11).

Read protection. rfoundry_debug.py chip on a DM-1701 (2026-10-10) reads idcode 0x0009A413 (STM32F405/407), 1024 KB flash, FLASH_OPTCR 0x0FFF55ED: RDP level 1 (byte 0x55), no sector write protection, BOR off, user bits all 1. Assume every DM-1701 is at RDP level 1 until one reads otherwise. RDP level 1 does not stop the application from reading flash (only a debugger, RAM boot and system-memory boot are locked out), so the bootloader (sectors 0-2) can be read with rfoundry_debug.py dump. It is proprietary and is never committed.

Route 1: ST's ROM bootloader (USB DfuSe in system memory). Refused on this radio. The firmware has it: REBOOT mode 1 stores the flag 0xB007DF11 in RTC_BKP19R and resets. A backup register is used rather than RAM because the bootloader runs in RAM before the application. First thing in main(), stm32_sysboot_check() (platform/stm32f4/sysboot.c) finds the flag and then:

  1. clears the flag and checks that it is clear, so a failed entry boots normally next time (and boots normally anyway if the flag won't clear);
  2. sets the power-up pin levels (board_sysboot_prepare: power latch high, transmitter off, HR_C6000 powered down);
  3. turns interrupts off, stops SysTick and clears every NVIC enable and pending bit;
  4. returns RCC to HSI with HSE and the PLLs off at their reset values, sets FLASH_ACR to 0 and resets every peripheral except the GPIO ports;
  5. checks that 0x1FFF0000 holds a plausible vector table, maps system memory at 0 (SYSCFG_MEMRMP), sets VTOR to 0, loads MSP from 0x1FFF0000 and jumps to the vector at 0x1FFF0004.

The firmware and rfoundry_debug.py both refuse this mode unless RDP is level 0. At level 1 the ROM bootloader refuses every flash access, and its only way out, the read-unprotect command, sets RDP to 0 by mass-erasing all of flash, the bootloader included. After that the radio could be reloaded only over SWD. At level 2 the ROM bootloader is disabled. On the bench unit (RDP 1) this route is closed. The code stays in the firmware because it is gated and does nothing unless the flag is set. If a unit ever reads RDP 0, the procedure would be: rfoundry_debug.py chip -> rfoundry_debug.py reboot --rom-dfu -> a DfuSe host that erases and writes only sectors 3-11 (0x0800C000 and up). No such host tool exists in tools/: stm32_dfu.py has no ROM mode.

Untested risks of route 1, found while writing it (they matter only on an RDP 0 unit):

  • Power latch. The volume knob only breaks the battery-sense line (10.4); PA7 holds the supply. While the ROM loader runs, PA7 stays high, so the knob cannot turn the radio off. Getting out of a stuck ROM loader takes a DfuSe leave command or removing the battery. Because the flag is already cleared, the next start is normal.
  • HSE clock. The board feeds an external clock in bypass mode (1.2), not a crystal. ST's ROM needs HSE for USB DFU on the F405 and may set it up in crystal mode. If that doesn't start, USB DFU never enumerates.
  • Interface probing. The ROM also listens on USART1 (PA9 / PA10, wired to GPS power and GPS RX on GPS-fitted siblings) and on USART3 / CAN2. Activity on those pins can capture the loader before USB. The DM-1701 has no GPS fitted, so PA10 is idle on it.
  • Leaving. Where the ROM's DfuSe leave command jumps, and with which memory mapping, is unverified. A battery pull always returns to the normal start (bootloader, then the application).

Route 2: a software entry into the bootloader's own DFU mode. This route would leave RDP alone and keep the XOR-keyed DfuSe path that tools/stm32_dfu.py already speaks (15.2, 15.3). It works only if the bootloader checks something besides the PTT + side key 1 combination that firmware can set before a reset: a RAM word it reads before clearing RAM, an RTC backup register, or a backup SRAM location.

There is no such flag. The bootloader decides once, after its own startup has cleared and initialized main SRAM, and there is no timeout. It stays in DFU when either of these holds; otherwise it loads MSP from 0x0800C000 and jumps to the word at 0x0800C004:

  • GPIOE IDR bits 8 and 11 (PE8, keypad column 5 / side key 1; PE11, PTT) both read low, the power-on key combination.
  • The application's initial stack pointer at 0x0800C000 fails (SP & 0x2FFE0000) == 0x20000000, so an erased sector 3 (0xFFFFFFFF) keeps it in DFU. The reset vector is not range checked.

It reads no RAM word, no RTC backup register or backup SRAM, and no reset-cause flag (RCC_CSR), and it doesn't set VTOR (the application sets its own). So nothing firmware can set before a reset makes it stay in DFU, except making the application look invalid. The least damaging way is a single flash word program that clears bit 29 of the word at 0x0800C000 (0x2001FFFC to 0x0001FFFC; programming can only clear bits, so no erase is needed). After that the radio boots into DFU every time until a full image is written again, which tools/stm32_dfu.py does. Sectors 0-2 are never touched, so the bootloader always stays reachable. This is not implemented; it is a candidate REBOOT mode 2 with the same gate plus an explicit confirmation in the tool.

Route 3: no bootloader at all, for updates. A build with CONFIG_STUB_ENTRY=y loads a RAM flash stub over the debug link and the stub writes the application sectors itself (stub-stm32f4.md). It uses the DFU check above as its safety net: it erases sector 3 first and programs the word at 0x0800C000 last, so an interrupted update leaves the radio in DFU at the next power-on with no keys held.


16. USB

Item Value
Core OTG_FS, device only, embedded PHY on PA11/PA12, VBUS sensing off, SOF off, no dedicated EP1 interrupt
FIFO RAM 1.25 KB (0x140 words): RX 0x80 words, TX0 0x30, TX1 0x80, TX2 0x10
Endpoints the F405 OTG_FS has EP0 plus 3 IN and 3 OUT endpoints
Serial mode CDC-ACM: interrupt IN 0x82 (8 bytes), bulk IN 0x81 / OUT 0x01 (64 bytes)
Network mode CDC-NCM 1.0, NTB16 only: notification IN 0x82 (16 bytes, 16 ms interval), bulk IN 0x81 / OUT 0x01 (64 bytes); interfaces 0 (communication) and 1 (data, alternate setting 1 active), grouped by an IAD
Why a switch ACM + NCM together need 4 IN endpoints (2 + 2) and there are only 3, so the radio is either a serial port or a network adapter; the choice is a stored setting applied at USB start (switching re-enumerates)
IDs used VID 0x1FC9; PID 0x0094 serial, 0x0096 network (a separate PID so Windows binds a different driver)
Windows 10 NCM binding Microsoft OS 1.0 descriptors: string index 0xEE, vendor code 0x47, extended compat ID "WINNCM"; Windows 11, Linux and macOS bind NCM by class
NTB sizes IN NTBs small (one datagram each); accept OUT NTBs up to 2048 bytes (Linux needs at least one full Ethernet frame plus headers); NTB parameters: NdpIn/Out divisor 4, alignment 4, no datagram limit
Reported speed 12 Mbit/s

USB gotchas found on the DM-1701:

  • macOS aggregates back-to-back datagrams into one NTB; the NTB parser must walk every datagram pointer (not stop after a fixed count) and also bound the number of NDPs to avoid loops.
  • macOS only brings the link up after a NetworkConnection notification is sent once the device is configured. On a cold boot in network mode macOS sometimes never activates the link; re-enumerating (soft disconnect/connect) about 5 s after boot when the link is still down fixes it.
  • Reassemble OUT NTBs from 64-byte packets using the NTB block length rather than relying on a zero-length packet, and re-arm the OUT endpoint only after the NTB has been consumed (natural flow control).
  • The reduced-power clock must not be entered while USB is configured (1.3).

17. Variants

Build variants: DM1701, RT84, MDUV380, MDUV380_10W_PLUS. The MD-2017 is not built: its pins are not defined.

Aspect DM-1701 RT84 MD-UV380 / UV390 5 W MD-UV390 Plus 10 W MD-2017 (partial code only)
Visible LCD rows 120 (8-row offset) 128 128 128 128
LCD type source sec. reg 3 byte 0x1D, DM-1701 table (9.2) as DM-1701 (unverified) sec. reg 3 byte 0x1D, MD-UV380 table, always RGB as MD-UV380 fixed: type 3, RGB
Keypad matrix 10.1 DM-1701 table same as DM-1701 10.1 MD-UV380 table as MD-UV380 different (P1/P2 on row 1, up/down on row 2)
Side keys on row 2 D5 top orange, D6 side 2, D7 side 1 same D7 side 1, D6 side 2, no orange same D5 trackball press, D7 side 2, extra orange button on its own GPIO (active low)
Navigation rotary encoder rotary encoder rotary encoder rotary encoder trackball (4 direction pulse inputs)
PA band select / backlight PC6 / PD8 PC6 / PD8 PC6 / PD8 PD8 / PC6 (swapped) as MD-UV380 (unverified)
Battery ADC divisor 37.5 37.5 40.3 40.3 37.5 (unverified)
Volume pot range 29..2075, break 318 as DM-1701 22..2070 / 309 (RT3S 23..2020 / 317) 19..2054 / 308 32..2048 / 321
Max power 5 W 5 W 5 W (a 10 W radio can be forced by setting) 10 W
Power fractions 7, 12.1 DM-1701 rows same 5 W rows 10 W rows
AT1846S count 1 1 1 1 2 (two independent receivers)
DFU key DM-1701 key DM-1701 key (unverified) MD-UV380 key MD-UV380 key MD-UV380 key
10 W identification n/a n/a sec. reg 3 byte 0x1C = 0xF4 means Plus 10 W
Soft volume (volume shown on screen) yes yes yes yes no

Everything else (MCU pins for RF, HR_C6000, AT1846S, SPI flash, USB, clocks, flash layout, codec address) is the same across the family.


18. Gotchas summary

  1. The bootloader rejects an initial stack pointer equal to the top of RAM; use 0x2001FFFC.
  2. Set VTOR to 0x0800C000; the vector table is not at 0x08000000.
  3. The bootloader leaves HSE/PLL running: switch to HSI before reprogramming HSE/PLL.
  4. CCM RAM is not zeroed or initialized by default startup code and is invisible to DMA. RadioFoundry puts the RTOS heap (every task stack), the slot engine, the radio state machine and the big CPU-only buffers there; .ccmbss (RFOUNDRY_CPU_RAM) is zeroed before main by a .preinit_array entry in board.c, .ccmram (RFOUNDRY_CPU_RAM_NOINIT) is not. DMA buffers stay static in main SRAM, and each stream's set-up checks that (DMA_RAM_OK).
  5. The codec must be at exactly 0x0807537C; the code budget is the space below it. Keep large constant tables above the codec end if the code grows.
  6. PA13 is SWDIO; driving it as mic power breaks SWD (use connect-under-reset).
  7. LCD data lines double as keypad columns: discharge them before reading keys, and leave them low after LCD writes.
  8. HR_C6000 config SPI and AT1846S I2C are touched from ISRs and tasks: make transactions atomic, and never block on I2C in an ISR.
  9. The PA is switched per burst in DMR; "transmitting" must come from the modem's slot state, not from the PA pins.
  10. The HR_C6000 RF_RX_EN / RF_TX_EN pins are not wired to the MCU; RF switching is done by the MCU (I2C + GPIO), about 0.5 ms per switch.
  11. The AT1846S TX frequency offset depends on the write sequence (about -3 kHz with the normal sequence, none with a direct switch); measure, do not copy.
  12. DMR RX needs AT1846S AF gain at maximum (0x44 = 0x07FF) and reg 0x40 bit 0 set; FM needs bit 0 clear.
  13. A volume-knob power-off looks like the battery dropping below 5.5 V; the firmware must then release the PA7 latch.

19. Datasheets

Public documents this page relies on: the AT1846S datasheet 0.3 and programming guide 1.4, the HR_C6000 datasheets, the W25Q80BV datasheet, ETSI TS 102 361-1 v2.2.1.

Not available for this radio: the STM32F405 datasheet and RM0090 reference manual; the LCD controller datasheets (ST7735S / HX8353-E, and the controller of panel types 2/3); the W25Q128 datasheet (the W25Q80BV covers the command set); schematics or a service manual; audio power amplifier, RF PA and TCXO part data; GPS module data.

20. Bench findings (RadioFoundry on a DM-1701, 2026-10-09)

Verified on one radio with the new firmware; they override the unverified notes above where they differ.

  • Keypad: the 10.1 matrix is right. The two "function" keys are labeled P1 (row 0, D6) and P2 (row 0, D5). There is no orange key: the three black side keys are, top to bottom, row 2 D5 (the "top key" position), row 2 D7, row 2 D6; PTT is separate. A row released while still high stays charged and makes every key also read as the key below it: drive the row low before releasing it and keep idle rows pulled down.
  • Encoder: clockwise counts up with the 10.3 rule, one count per detent.
  • Panel: this radio reports type 2 (security register 3, byte 0x1D). Driving it with the type 2 MADCTL (0xE0) shows the picture mirrored top to bottom; treating it as type 3 with RGB order (as 9.2 says) is correct. The 8 hidden rows are panel rows 0-7 either way.
  • Volume pot: raw 26 at the bottom, 2063 at the top.
  • Battery: raw / 37.5 gives a plausible 7.1 V (not yet checked against a meter).
  • Calibration: the working copy at 0x010000 carries the marker. The FM I gain rows (0x170 UHF, 0x179 VHF) are all 0x00 on both bands while the other gain rows hold values: an all-zero gain row is valid data.
  • Soft volume: HR_C6000 4:0x37 (RX gain) scales FM feed-through audio as well as DMR audio; -31 dB is still audible, so the bottom of the knob must also mute the speaker path. It mutes the amp only: the feed-through (0x36) and the gain (unity at the bottom) stay as for any FM receive, because the I2S copy of the FM audio that APRS receive reads is gated and scaled by both. Before this, a knob at zero turned the feed-through off and APRS heard nothing.
  • Volume override: because the MCU reads the knob and sets this gain itself, the debug link can set the level instead (RADIO op 30, radio-features.md 4.2): a step 0..16 maps to the same 0..100 % scale as the knob, in force until it is cleared (RAM only). The knob has no analog path around the gain, so the override fully takes its place (HW, not yet run on a radio).
  • HR_C6000: 4:0x10 reads back 0x80 after FM RX set-up (the config port works with the 4.2 timing).
  • FM TX (VFO 461.8625 MHz, 12.5 kHz, 1 W, measured with an RTL-SDR, 2026-10-09): with the TX sequence of radio_dm1701.c (frequency written with the receiver off, straight to TX) the carrier is within about 50 Hz of the channel, so no TX offset is added. Unmodulated carrier residual FM 41 Hz RMS, no hum or tone. Voice at normal level: 2.5 kHz peak deviation (99.5th percentile), 636 Hz RMS, about 7 kHz occupied at -26 dB. The FM I calibration row of 0 (Mod2 amplitude) does not stop voice modulation.
  • DMR TX (Ch 1 467.375 MHz, cc 2, 1 W, SDR bench + an MMDVM_HS receiver, 2026-10-10): switching per burst from RX on straight to TX on the already-tuned frequency put the carrier 3.07 kHz low (an ADF7021-based receiver decoded nothing; a wide-AFC SDR decoder still did). Retuning with the receiver off before every burst (as FM TX does) brings it to about +65 Hz, and the MMDVM_HS receiver decodes headers, all superframes with the embedded LC and the terminator. Burst spacing 60.000 ms (sd 0.002 ms), 31 ms PA per burst.

21. Audio paths for USB audio (bench study)

Which audio paths reach the MCU, for the USB audio design (usb-audio.md). Measured with probe firmware: CONFIG_AUDIO_PROBE=y adds debug link command 0x10 (debuglink.md 3.14), which reads and writes HR_C6000 registers, runs the PCM stream with a test tone and reports both I2S channels' levels. tests/hil/test_audio_probe.py runs the experiments.

Question Answer
DMR voice as PCM, both ways Yes, today. The I2S link to the HR_C6000 already carries 8 kHz 16-bit PCM both ways, and the MCU runs the AMBE codec. Decoded calls can go to the host and host audio can be encoded and sent, at no new CPU cost (21.2)
FM receive audio to the MCU Yes, two ways (bench, 2026-10-12, 21.3.1). The I2S stream carries it with the default settings, squelch gated. The HR_C6000's RX RAM carries it ungated, 8 kHz big endian 16-bit, one 256-sample half per 32 ms, with de-emphasis switchable (0x34 bit 5): the voice and flat modes of usb-audio.md 5.5, flat from about 300 Hz up
MCU audio into the FM modulator Yes: PWM on PC8 into the mic input (candidate E, to be built), or the modulator's offset register (bench, 2026-10-12, 21.3.1): the firmware writes 0x04 (two-point MOD1 offset) about 37,000 times a second, about 57-75 Hz deviation a step, so about 6 bits of resolution at normal deviation, and none of the chip's FM processing (pre-emphasis, limiter, CTCSS: the MCU does them). No path makes the chip's own FM chain take MCU samples: the TX RAM is a tap the codec writes, external-codec mode reads pins 16-20, which don't reach the MCU. The chip's selcall tone generator takes phase steps from the MCU: clean AFSK
An MCU ADC on the RX audio line, or a free DAC on the mic line No. ADC1 sees only the volume pot, the battery and a mic level tap; DAC1 drives the PA power, the DAC2 pin is a GPIO (21.1)

21.1 What the board has

From sections 2, 4.4, 8 and 11 and the HR_C6000 user manual (the vendor datasheet; register table and appendix A in its Chinese section):

Path Route MCU access
FM RX antenna, AT1846S, its audio output, into the HR_C6000, FM receive processing there (de-emphasis, sub-audio detection, the audio filters, page 4 0x34), the HR_C6000 codec DAC, line out, the RX audio mux (PD9 high), the speaker amp none documented: the audio never leaves the HR_C6000 digitally
FM TX microphone (bias PA13), HR_C6000 LINEIN1, its codec ADC, FM transmit processing (compressor, pre-emphasis, limiter, channel filter), two-point modulation outputs none documented
DMR RX AT1846S baseband, HR_C6000 4FSK demodulator, AMBE frames over V_SPI, MCU codec, PCM over I2S, HR_C6000 DAC, amp PCM, 8 kHz, MCU to codec
DMR TX microphone, HR_C6000 codec ADC, PCM over I2S, MCU codec, AMBE frames over V_SPI PCM, 8 kHz, codec to MCU
Beeps, prompts MCU PCM over I2S to the codec DAC PCM out
Mic level (VOX) an analog tap on PA3 (ADC1 IN3) a level, not audio (sampled at about 70 Hz)

The I2S link (SPI3 as I2S slave on PA15/PC10/PC11/PC12, the HR_C6000 driving the clocks): Philips, 16-bit data in 32-bit frames, both channels, 8 kHz, circular DMA on DMA1 streams 0 and 5 in 20 ms halves (platform/stm32f4/hrc/i2s_pcm.c, hal/hal_audio.h). This is the only audio data path between the MCU and the radio.

No other path exists on this board: ADC1's four inputs are the volume pot (PA0), battery (PA1), mic level (PA3) and the temperature sensor; DAC channel 1 (PA4) sets the PA power (APC); the DAC channel 2 pin (PA5) is the VHF receiver 5 V switch. The RX audio mux (PD9) chooses between the HR_C6000 line out and the AT1846S audio output toward the speaker amp only. A hardware modification (a wire from the AT1846S audio output to a free ADC pin) would work for receive but is not something users should need, and there is no equivalent for transmit.

21.2 DMR

The DMR voice path already moves PCM between the MCU and the codec in both directions (core/dmr/ambe/ambeVoice.c): decoded frames are played as 20 ms blocks, and microphone blocks are encoded while transmitting. For a USB voice endpoint the audio router (usb-audio.md 5.1) takes the decoded block for the host as well as (or instead of) the speaker, and feeds host PCM to the encoder instead of the microphone. CPU, codec stack and RAM are what a normal DMR call uses (the AMBE codec runs for one direction at a time; a DMR radio is half duplex). DMR on this board was verified on air in both directions (section 20).

21.3 FM

What the HR_C6000 manual says:

  • The built-in codec's input mux takes MIC_P / MIC_N (differential) or LINEIN1 / LINEIN2 (single ended), then a gain stage, the ADC and a digital filter; its output path is a gain stage (page 4 0x37), the DAC and LINEOUT1 / LINEOUT2 (manual 4.5.1, figure 4.12).
  • Register 0xE0 (manual register table, Chinese section): bit 7 MCU controls the codec, bit 6 LINEIN1 enable, bit 5 LINEIN2 enable, bit 4 LINEOUT1, bit 3 LINEOUT2, bit 2 MIC_N, bit 1 MIC_P, bit 0 I2S slave. Our 0xC9 (DMR, FM TX) is MCU control + LINEIN1 + LINEOUT2 + I2S; 0x89 (FM RX) the same without LINEIN1. So the microphone is wired to LINEIN1, and modem-hrc6000.md 2 calls bit 6 "mic input enable", which is right in effect. On a DM-1701 board photo, pin 8 (LINEIN2) has no trace and pins 6 (MIC_N) and 7 (LINEIN1) share a via; the bench agrees (21.3.1, LINEIN2 loop). FM receive audio plays with 0xE0 = 0x89, both line inputs off, and the HR_C6000 detects the sub-audio tones, so the receive audio reaches its FM chain through the baseband ADC (the path DMR uses), not the codec inputs: section 8's "line in" for FM RX is probably that. Pin 8 is the free analog input for a mod (candidate D).
  • 0xE3 bit 0: "codec internal DAC and ADC pass-through enable, normally 0". The direction is not stated.
  • The FM transmit chain takes the codec ADC's samples; the FM receive chain ends at the codec DAC (manual 6, figure 6.1). The I2S "vocoder interface" carries the codec ADC's samples to an external vocoder and the vocoder's PCM to the DAC (manual pin table, C_SDI / C_SDO; 0x22 bits 2-1 choose I2S and its master mode). Nothing says the FM chains can read from or write to the I2S interface.
  • The chip has "FM data" RAM: two 512-byte buffers in the TX RAM ("can be written externally for FM send; voice data can also come internally from the codec") and two in the RX RAM (manual 5.3 tables), and FM-mode interrupts in 0x82: bit 7 "FM: sending data request", bit 1 "FM: receiving data". Register 0xD7 bit 1 (written 0xDB here before) selects "send FM voice" over MSK signaling. This looks like the MCU can exchange FM audio samples with the chip through its RAM pages, but the format, rate and control are not documented. Our FM RX set-up writes 0x81 = 0x19, which masks both FM data flags.
  • The FM chain can use an external codec instead of the built-in one (0x06 bit 2, appendix A2), over a second I2S port on pins 17-20 with the chip as master. The MCU sits on the other one, the vocoder port (pins 34-37), so this only helps if pins 17-20 reach the MCU, which nothing we have shows. The block diagram's "I2S" next to the FM transmit chain is this port.
  • A "prompt tone" path (0x06 bit 1, appendix A1) lets the MCU write DAC samples over U_SPI without I2S: speaker only, but with 0xE3 bit 0 it is another way into the ADC and so the FM transmit chain if the I2S clock stops in FM. modem-hrc6000.md 8.1 has the details of all of these.

21.3.1 Bench results (2026-10-12)

Probe firmware, tests/hil/test_audio_probe.py, NOAA WX3 (162.475 MHz) for receive, 467.375 MHz into the SDR bench for transmit.

Question Result
I2S clock in FM runs: 50 blocks a second
FM RX on I2S yes, with the default settings (0xE0 = 0x89): RMS about 2000 on NOAA, both channels the same, exactly 0 with the squelch closed (the feed-through, 0x36, gates the digital stream too). LINEIN1, LINEIN2 and 0xE3 bit 0 only lower it
FM RX in the RX RAM yes, ungated. 0x82 bit 1 comes 31.0 times a second once 0x81 unmasks it (0x19 masks it, apart from about 1 a second): one 512-byte half per 32 ms, so 256 samples at 8 kHz. Samples are 16-bit big endian. The chip fills a half progressively: read both halves twice and take the one that didn't change (98-100 % of the audio captured at about 47 reads a second over the debug link). NOAA speech: 50 ms envelope CV 0.82; a quiet channel: steady noise, CV 0.06, louder than the signal (noise quieting)
What the RX RAM tap sits after 0x34's processing: with de-emphasis (bit 5) the noise falls 12 dB from 500 Hz to 2.5 kHz, without it it is flat (-1 dB). Bit 7 adds the 300-3400 Hz band pass, bit 2 the wider RX filter. Below 300 Hz stays about 20 dB down in every setting: a fixed sub-audio high pass
FM TX from I2S (A, B) no. 0xE3 bit 0 changes nothing. LINEIN2 on (0xE0 bit 5) puts a steady 2.49 kHz tone on air at about 1.7 kHz RMS deviation whether the probe sends nothing, 1 kHz or 600 Hz: something on that input, not MCU audio. Don't enable LINEIN2
FM TX from the TX RAM (C) no: the TX RAM is a tap. Keyed, 0x82 bit 7 ("sending data request") comes 31.0 times a second once unmasked. Read back through page 6 (U_SPI OPMode 110, manual 5.2), the FM halves hold the codec's samples, big endian 16-bit, one half per 32 ms, rewritten whether or not the microphone is on: only 0x60 bit 7 (the analog TX chain off) stops the writes (304 bit flips tried). The static-RAM tests (the settings below, the sweeps) were moot for that reason; the firmware's RAM feed (probe op 11) rewrites each half within 4 ms of the codec moving on, and with it a 1 kHz tone never reaches the air: the default settings, every FM value of 0x10, each bit of 0x01, 0x06, 0x0D, 0x0E, 0x11, 0x21, 0x22, 0x25, 0x26, 0x30-0x37, 0x40, 0x41, 0x50-0x52, 0x60, 0xA1, 0xD0-0xE5 and page 1 0x50 / 0x51 (525 settings), 0xD7 bit 1 (the manual's "send FM voice", alone and with MSK mode), and each FM data request answered with 0x41, 0x22, 0x60 or 0x50 writes. 0x01 bit 4, 0x25 bits 0 and 1 drop the carrier. Earlier: 0xDB bit 1 (0xDB isn't in the manual; the bit is 0xD7 bit 1), 0x60 = 0x40 or 0xC0, 0x06 bits 0, 2, 3, 4, 0xE0 bit 7 off, 0xE2 bit 4
FM TX through the MOD1 offset (0x04) yes. Stepped while keyed, 0x04 moves the carrier linearly, about 75 Hz a step as a signed byte (0xCF as set; +8 steps +595 Hz, +32 +2395 Hz, -32 -2395 Hz, bit 7 flipped +9.5 kHz). Manual 4.7.2: 0x04 is the MOD1 (reference crystal) offset, 0x02 the MOD2 (VCO) one; 0x02, 0x03, 0x05, 0x45-0x48 moved nothing. Probe op 10 writes 0x04 = as set + a sine back to back (37,500 writes a second): 300, 1000, 2000 and 3000 Hz tones on air, about 57 Hz deviation a step at 1 kHz (16 steps: 650 Hz rms), flat 1-2 kHz. 0x46 (MOD1 amplitude) doesn't scale it. The first SINAD figures (about 4 dB) came from phase jitter in the probe's millisecond-clock timing, not the path; the microsecond-clock build is not measured yet
FM TX from the chip's tone generator yes, AFSK-class. 2-tone mode (0xA1 = 0x10, 0xA0 = 0x39), "long A", retriggered by 0x60 0x00 -> 0x80 while keyed: tone A comes from its 16-bit phase step at aux 0x11A (f / 32000 x 2^16, written through page 1 with an extended address). Rewritten every 100 ms from the host it switches cleanly between 1200 and 2200 Hz at about 2.2 kHz rms deviation: a phase-continuous AFSK generator inside the FM chain
FM TX source switch 0x06 bit 2 (external codec) moves the FM chain's input off the built-in codec: LINEIN2's tone stops the moment it is set. The external port (pins 16-20) isn't wired to the MCU, and the MCU's I2S tone doesn't reach the air in that mode (with 0x36 bit 7 and 0x22 bits 1-2 too). A wire from the MCU's PCM output (PC11) to ADCDAT (pin 19) would make it a full-quality path (candidate D)
PC8 into MIC_P / MIC_N yes, no mod. A 1 kHz square wave on PC8 (the beep PWM, TIM8 CH3, probe op 12) goes on air as a 1 kHz tone at 3.0 kHz rms deviation (SINAD 8.1 dB: a square wave's harmonics) once 0xE0 bits 1-2 (MIC_P, MIC_N) are on; nothing with only LINEIN1 or LINEIN2 on. The beep net reaches the differential mic input, so PWM at a high carrier with the duty set by each sample feeds the chip's own FM chain (pre-emphasis, limiter, CTCSS) through the codec ADC
PWM audio on PC8 the best path so far. A sine by PWM (probe op 14: TIM8 CH3, the duty rewritten each carrier period by DMA) into MIC_P / MIC_N: 1 kHz at 1 kHz rms deviation (4 % of the PWM range) 14.1 dB SINAD; at 2.6 kHz rms (10 %, mic first stage -12 dB) 15.6 dB; more drives the limiter. The carrier (17.6-140 kHz) makes no difference: the board and the codec filter it. The floor with MIC_P / MIC_N on and no audio is 115 Hz rms in the voice band (50 Hz with them off). Flat (0x34 bits 7-5 off) the response is 300 Hz 0 dB, 1 kHz 0 dB, 2 kHz -3.5 dB, 2.5 kHz -5.3 dB, 3 kHz -7 dB, 3.5 kHz -14 dB, nothing at 3.8 kHz: the codec's 8 kHz sampling. Fine for voice and 1200 / 4800 baud (equalized); not 9600
0x04 response flat to 6 kHz: 16 steps give 689, 661, 650, 633 and 599 Hz rms deviation at 1, 3, 3.8, 4.8 and 6 kHz. It skips the codec, so it is the only no-mod transmit path wide enough for 9600 baud FSK (flat to 4.8 kHz; a 25 kHz channel, 0x34 bit 4). Its resolution (about 6 bits) and the CPU it takes (32,700 bus writes a second) remain; receiving 9600 needs the AT1846S audio output on an MCU ADC (a mod), the RX RAM tap being 8 kHz
SINAD reference not settled: the tone generator's "long A" plays in bursts of about 0.5 s, so its 10.6 dB SINAD (2.2 kHz rms) mixes tone and gaps. The bench's own ceiling still needs a steady clean source (a signal generator, or the bench's own modem board in FM)
Pins 16-20 wiring not wired to the MCU. External-codec mode (0x06 bit 2) stops the I2S lines the MCU sees (PA15 WS, PC10 CK, PC12 SD) and starts none; pins 16-20 as the chip's timed GPIO outputs (0xC7-0xD0), keyed, move no MCU pin (probe op 13 scans)
0x04 quality (microsecond timing) 1 kHz, 16 steps: SINAD 12.7 dB at 720 Hz rms deviation; 32 steps 10.9 dB; noise shaping makes it 1.5 dB worse. About 32,500 writes a second
LINEIN2 loop no loop: an open input. LINEIN2's tone stays with LINEOUT2 off (and with LINEOUT1 on instead) and stops when the input gain drops (0xE4 = 0x20); the 1 kHz I2S tone never reaches the air. A floating high-gain input oscillates; nothing on the board drives pin 8
MSK mode 0xA1 bit 6 with 0xD5 = 0x48 keys the chip's MSK modem: a steady 1800 Hz at 2.7 kHz rms deviation, not the RAM

| FM TX from the prompt tone FIFO | no. The FIFO (page 3 address 0, 32 samples when 0x88 bit 0 reads 0) drains at exactly 8000 samples a second whether 0x06 bit 1 is set or not, and a 1400 Hz tone through it never reaches the air: path open, with 0xE3 bit 0, either byte order | | Bit sweep while keyed | no. Each bit of 0x25, 0x26, 0xE1, 0xE2, 0xE3, 0xE5, 0x0D, 0x0E, 0x0F and 0x38 flipped for 0.5 s (80 flips, 10 key-ups) with a 600 Hz tone on I2S and a 1 kHz tone in the TX RAM: neither tone on air during any flip. LINEIN2's tone, used as the time marker, came at 2.59-2.61 kHz this time (2.49 kHz before): an oscillation on that input |

So the chip's FM chain takes audio only from its codec ADC (microphone, LINEIN1, LINEIN2) or the external codec port; nothing the MCU writes over U_SPI or I2S reaches it. MCU audio goes on air through the modulator's MOD1 offset (0x04) instead, with the MCU doing pre-emphasis, limiting and CTCSS, at about 6 bits; AFSK can use the chip's tone generator. Full quality needs candidate D (the PC11 to pin 19 wire). The register reference with the manual's details is hrc6000-registers.md.

Candidates, in order of preference:

Receive (FM audio to the MCU) Transmit (MCU audio to the modulator) Cost if it works
A 0xE0 bit 5: LINEIN2 into the codec ADC, if the AT1846S audio (or the speaker line) is wired to it; I2S then carries it as it carries the microphone in DMR (none) nothing: same I2S stream
B 0xE3 bit 0 with FM feed-through on: the DAC's signal (FM receive audio) looped into the ADC and out on I2S 0xE3 bit 0 with feed-through off: I2S PCM to the DAC, looped into the ADC, which feeds the FM transmit chain nothing: same I2S stream
C FM data RAM (page 2, 0x030-0x42F) read on the "receiving data" flag FM data RAM written on the "sending data request" flag 16 KB/s each way over the bit-banged config port (about 1-2 Mbit/s, so roughly 8-16 % of the bus per direction, in 64-byte transactions); FM only, when the modem ISRs don't run. Format, rate and the TX source bit to be found on the bench
E PWM on PC8 (TIM8 CH3) with 0xE0 bits 1-2 on: the beep net reaches MIC_P / MIC_N (none) TIM8 and one DMA stream; no bus traffic
D a hardware modification a wire from PC11 (I2S3ext_SD) to ADCDAT (pin 19) with 0x06 bit 2; or MCU PWM through an RC filter and a 10 µF capacitor into LINEIN2 (pin 8) with 0xE0 bit 5 (PC8, the old beep PWM, is a candidate) not for users

A, B and C don't work (21.3.1). Receive uses the I2S stream or the RX RAM tap. Transmit uses E, or the MOD1 offset for content above the codec's band; AFSK can also use the chip's tone generator.

The probe experiments, in the order they were run:

  1. Does the I2S clock run on an FM channel? (If not, no FM PCM at all without C. Beeps on FM depend on it too: beep.no_stream in radio-features.md 4 asks the same question.)
  2. Receive: NOAA weather radio in the VFO with the squelch open, then a quiet frequency; for the register settings as set, A, LINEIN1 and B, the RMS of both I2S channels. A setting whose RMS rises clearly with the signal carries receive audio. It also tells which I2S channel the chip fills (audio_dm1701.c assumes the left one, unverified).
  3. Transmit (opt-in RF, SDR bench): key FM with the microphone muted (0xE0 bit 6 off) while the probe sends a 1 kHz tone into I2S; for "no tone", "tone", B and A, the voice band deviation the bench measures.
  4. Candidate C, receive: on NOAA and on a quiet frequency, the rate of the 0x82 FM flags with 0x81 as set and with bits 7 and 1 unmasked, whether the RX RAM buffers change, and their level read as 16-bit (both byte orders) and 8-bit samples.
  5. Candidate C, transmit (opt-in RF): both TX RAM buffers hold a 1 kHz tone, a whole number of cycles in 256 samples, so a chip that replays them sends a steady tone without refills. Keyed with the microphone muted, for the likely source settings (FM flags unmasked, 0xDB bit 1 (meant: 0xD7 bit 1), 0x60 = 0x40 or 0xC0, 0x06 bit 0, big endian samples): the FM flag rate and the voice band deviation against a silent-RAM baseline.

21.4 Running the probe

With the AMBE codec merged as usual:

make BOARD=dm1701 CONFIG_AUDIO_PROBE=y BUILD=build/dm1701-probe   # flash build/dm1701-probe/rfoundry_dm1701.bin
python3 -m pytest tests/hil/test_audio_probe.py -v -s -rs          # stream in FM, the receive variants (NOAA), RX RAM
python3 -m pytest tests/hil/test_audio_probe.py -v -s -rs --allow-rf --allow-service-stop -k tx   # I2S and RAM TX variants

By hand (an FM channel or the VFO):

tools/rfoundry_debug.py aprobe start            # stream with silence out; then:
tools/rfoundry_debug.py aprobe status           # blocks counting up = the I2S clock runs in FM
tools/rfoundry_debug.py aprobe reg 4 0xE0       # 0x89 expected in FM receive
tools/rfoundry_debug.py aprobe wreg 4 0xE0 0xA9 # LINEIN2 on (candidate A); status / samples with and without a signal
tools/rfoundry_debug.py aprobe wreg 4 0xE0 0x89
tools/rfoundry_debug.py aprobe wreg 4 0xE3 0x53 # candidate B (0xE3 is 0x52 at init: bit 0 set); watch status
tools/rfoundry_debug.py aprobe wreg 4 0xE3 0x52
tools/rfoundry_debug.py aprobe samples -o /tmp/blk.raw   # one block of both channels, tone estimate printed
tools/rfoundry_debug.py aprobe stop
tools/rfoundry_debug.py aprobe wreg 4 0x81 0x9B # unmask the FM data flags (0x19 | 0x82)
tools/rfoundry_debug.py aprobe watch 1000 0x82 0x030   # FM flag rates, RX RAM word changes
tools/rfoundry_debug.py aprobe ram 2 0x030 64   # the start of the RX FM buffer

Expected readings: in FM receive with the defaults, both channels near silence (the microphone is off). A candidate works for receive if, with NOAA audible and the squelch open, one channel's RMS rises well above (4x or more) its value on a quiet frequency. For transmit, the SDR bench's voice band RMS deviation with the tone and the candidate setting is well above the "mic muted, no tone" row.

22. Facts moved from the shared specs

  • Settings and storage: the core's map for the 16 MB SPI flash is settings 0x011000 (32 KB, 8 sectors), journal 0x019000, open-firmware EEPROM 0x000000, open-firmware flash 0x020000 up to 0xE00000, native area 0xE00000 (1 MB), TFTP staging 0xF00000 (256 KB), calibration window 0x010000 (512 B). The range 0x011000-0x01AFFF has never been checked on a used radio, for the foreign-region guard of settings-store.md 5.
  • The board's hal_radio has tx_tone, so the MMDVM task keys the 1750 Hz burst (mmdvm-protocol.md).
  • USB: the STM32F405 OTG_FS has three IN endpoints besides EP0, so serial and network are alternatives, not a composite (usb-network.md).
  • board_rf_safe pins: PA_EN1 (TX supply) and PA_EN2 (APC loop) low, PA_BAND low, APC DAC (DAC1 channel 1) 0.
  • GPS: USART1 RX on PA10, with PA9 as the GPS power switch.
  • Crash log (crash-log.md): the backup SRAM kept the record through the bootloader. A trap test gave HardFault with CFSR UNDEFINSTR in the task host; a hang test ("CRASH TEST") was recorded at 3750 ms. The snapshots named radio_task, dmr_task, input_task, the UI task in input_next / ui_task and the USB task in tud_task_ext (2026-10-17/23, test_crash.py). The restart cause after a watchdog reset is watchdog.
  • Roaming (dmr-roaming.md 3): a survey visit relies on the AT1846S RSSI plus HR_C6000 sync acquisition needing well under 100 ms on a continuous outbound (a burst every 30 ms), then four 25 ms readings.
  • On air since 2026-10-10: receiver sleep (test_battery_save*: cycles when idle, monitor and a call wake it), the 1750 Hz burst, the roger beep and APRS AFSK (HIL). VOX was checked in silence only (test_vox_quiet).
  • AFSK TX retunes the AT1846S tone generator per symbol. The AFSK RX audio comes from the HR_C6000 FM audio on I2S (squelch gated, 8 kHz, de-emphasized). DMR LRRP and UDT passed on air at 467.375 MHz, CC 2, 50 mW. An AFSK tone level of 0x1C gives about 3.1 kHz peak deviation (0x40 gave 7.2 kHz).
  • REBOOT mode 1 enters the ROM DFU with a flag in an RTC backup register (15.4). The spare beep PWM pin is PC8 (TIM8 CH3, 72 MHz carrier, DMA2 stream 2), used by AUDIO_PROBE PIN_TONE / PIN_PWM at about 37,000 MOD writes a second.
  • Release and flashing: application sectors 3-9 (720 KB), codec sectors 10 and 11 (1.4), keep supported, profile dm1701-v0206-csv-s11. Releases before the move linked read-only data into sector 9 as a second range, profile dm1701-v0206-csv. An interrupted update writes sector 3 (the vector table) last, so a cut before it leaves the bootloader in DFU; a later half application may crash but never bricks. Enter the bootloader by hand (PTT and side key 1 at power-on) and flash again.

View the source of this page