TYT MD-380 hardware (MD-390, Retevis RT3, and the "G" GPS models)
The TYT MD-380, the MD-390 (the same board in an IP67 case) and the Retevis RT3 are one board family: an STM32F405VGT6 MCU, an HR_C5000 DMR baseband and codec chip, no RF transceiver chip but a Skyworks SKY72310 fractional-N synthesizer, separate receive and transmit VCOs, a GT3136 FM IF receiver, a discrete PA with an analog power loop, a 160 x 128 color LCD on the FSMC 8-bit bus, a SPI NOR flash, a 3-row keypad matrix sharing its columns with the LCD data bus, a 16-position channel switch, an analog volume knob with the power switch, and USB full speed on the MCU's OTG_FS core. Each radio is single band, UHF or VHF. The "G" models (MD-380G, MD-390G, RT3 GPS) add a GPS module on a UART.
Conventions: register values are hex. HR_C5000 registers are written as reg = value on its configuration page
(page 0; the driver keeps the HR_C6000's page numbers and maps them, section 4.3). SKY72310 registers are its 4-bit
addresses. A value is marked (unverified) where no datasheet and no measurement on a radio confirms it.
md380-bringup.md is the order in which a radio confirms or corrects the open facts,
and section 17 lists every open question.
1. MCU, clocks, memory
1.1 Part
STM32F405VGT6, LQFP100: Cortex-M4F, 1 MB flash, 192 KB RAM (128 KB SRAM, 64 KB CCM). Read-out protection level 1 on production radios (unverified).
1.2 Clocks
8 MHz crystal on HSE (unverified). RadioFoundry runs the DM-1701's clock plan: PLL M 4, N 72, P 2 (72 MHz SYSCLK), Q 3 (48 MHz
USB), AHB 72 MHz, APB1 36 MHz, APB2 72 MHz; If HSE does not start as a crystal, board_early_init retries it in bypass mode (the DM-1701
needed bypass) before staying on HSI. LSE: a 32.768 kHz crystal is assumed (unverified).
1.3 Internal flash layout
| Sectors | Addresses | Use |
|---|---|---|
| 0-2 (3 x 16 KB) | 0x08000000-0x0800BFFF | TYT bootloader, never written (unverified) |
| 3-7 | 0x0800C000-0x0807FFFF | RadioFoundry application, 464 KB in one piece (boards/md380/memory.ld) |
| 8, 10, 11 | see codec-md380.md | The D002.032 codec as the merge places it: the table windows at their original addresses (sectors 8, 10, 11), the code moved to 0x080E0000 (sector 11), the descriptor at 0x080FFF80 |
| 9 | 0x080A0000-0x080BFFFF | Holds nothing |
The application starts at 0x0800C000 with its vector table there (VECT_TAB_OFFSET 0xC000) (unverified).
boards/md380/memory.ld asserts that the image ends before 0x08080000. The default UHF image takes 320,236 bytes with link time
optimization, on by default here: feature-matrix.md. The manufacturer's firmware keeps
the codec code at 0x08050C44 (sectors 6 and 7), so the merge moves it.
1.4 RAM
128 KB SRAM for data and every DMA buffer (minus 4 bytes at the top, as the DM-1701 keeps them for the boot request
word); 64 KB CCM for CPU-only state (RFOUNDRY_CPU_RAM: the task stacks and the slot engine, as on the DM-1701, a76da0d;
main RAM use went from 89 % to 68 % of the default build with that move).
2. Pin assignments
The pin tables come from public schematic notes (from the RT3 / MD-380 service schematic) that we cannot name. Nothing independent backs them, so every pin is (unverified) unless another basis is cited. The notes warn that the
schematic "is known to be not correct" in places. Where the notes are unclear the pin is marked (unverified). boards/md380/pins.h holds the same map.
2.1 Port A
| Pin | Function | Notes |
|---|---|---|
| PA0 | ADC1 IN0: volume knob position | (unverified) |
| PA1 | ADC1 IN1: battery voltage | (unverified) |
| PA2 | ADC1 IN2: QT_DQT_IN, sub-audio tone input | (unverified); RadioFoundry decodes CTCSS / DCS in software, sampling it with CONFIG_SUBTONE (section 6.4) |
| PA3 | ADC1 IN3: VOX, mic level | (unverified) |
| PA4 | DAC1: APC / TV (PA power reference in TX, front-end tuning in RX) | (unverified); the double use (unverified) |
| PA5 | DAC2: MOD2_BIAS (reference oscillator bias) | (unverified); value from 11.1 |
| PA6 | Keypad row K1 | (unverified) |
| PA7 | POW_C, power latch | (unverified) |
| PA8 | SAVE: enable of the 5C rail (U404), high = on (5.7) | schematic; output, driven high |
| PA9 | VCOVCC_SW: RX or TX VCO supply, high = receive VCO, low = transmit VCO | bench |
| PA10 | DMR_SW: receiver IF to the HR_C5000 | (unverified); low in TX, high in RX 2.6 |
| PA11 / PA12 | USB OTG_FS D- / D+ | (unverified) |
| PA13 | W/N_SW, IF bandwidth; also SWDIO | (unverified); polarity (unverified) |
| PA14 | MICPWR_SW, microphone bias; also SWCLK | (unverified) |
| PA15 | I2S3 WS | (unverified) |
2.2 Port B
| Pin | Function | Notes |
|---|---|---|
| PB0 | ADC1 IN8: RSSI | (unverified) |
| PB1 | ADC1 IN9: BUSY, the receiver's noise squelch level | (unverified); polarity (unverified) |
| PB2 | FM_SW, FM receive path select | high in FM receive (measured on RadioFoundry) |
| PB3 / PB4 / PB5 | SPI1 SCK / MISO / MOSI, SPI flash (AF5) | (unverified) |
| PB6 / PB7 | I2C1 SCL / SDA | (unverified); nothing on it is known; unused |
| PB8 | SPK_C, speaker mute | high = muted (measured on RadioFoundry) |
| PB9 | AFCO, audio amplifier enable | high = on (measured on RadioFoundry) |
| PB10 / PB11 | Channel switch ECN2 / ECN3 | (unverified) |
| PB12 | HR_C5000 voice port chip select | 2.6 |
| PB13 / PB14 / PB15 | SPI2 SCK / MISO / MOSI, HR_C5000 voice port (AF5) | 2.6 |
2.3 Port C
| Pin | Function | Notes |
|---|---|---|
| PC0 | HR_C5000 timeslot interrupt (EXTI0) | (unverified) |
| PC1 | HR_C5000 system interrupt (EXTI1) | (unverified) |
| PC2 | HR_C5000 RF TX interrupt (EXTI2) | (unverified) |
| PC3 | ADC1 IN13: 2T/5T (FM discriminator audio) | (unverified); unused |
| PC4 | RF_APC_SW, PA power loop on | (unverified) |
| PC5 | 5TC, transmitter supply | (unverified) |
| PC6 | LAMP, LCD backlight (TIM8 CH1 PWM, AF3) | (unverified); PWM is our choice |
| PC7 | CTC/DCS_OUT, sub-audio tone output (PWM) | (unverified); TIM3 CH2 (AF2) with CONFIG_SUBTONE (section 6.4), otherwise a low output |
| PC8 | BEEP (TIM8 CH3) | (unverified); unused |
| PC9 | 5RC, receiver supply | (unverified) |
| PC13 | HR_C5000 configuration clock, idles low (section 4.2) | the bus pin (not PE3) |
| PC10 / PC11 / PC12 | I2S3 CK / ext SD / SD | (unverified); direction of the two data lines (unverified) |
| PC14 / PC15 | LSE crystal | (unverified) |
2.4 Port D
| Pin | Function | Notes |
|---|---|---|
| PD0 / PD1 | FSMC D2 / D3 (LCD data, keypad columns) | (unverified) |
| PD2 | Keypad row K2 | (unverified) |
| PD3 | Keypad row K3 | (unverified); carries the side keys (unverified) |
| PD4 / PD5 | FSMC NOE / NWE | (unverified) |
| PD6 | LCD chip select | (unverified) |
| PD7 | SPI flash chip select | (unverified) |
| PD8 | GPS module power on the G models; listed as a second flash chip select on others | 13.7 |
| PD9 | USART3 RX (AF7): GPS NMEA in on the G models; listed as a flash chip select on others | (unverified) |
| PD10 | SKY72310 PS, out-of-lock, open collector: pulled up, high = locked | (unverified); pull-up |
| PD11 | SKY72310 chip select | (unverified) |
| PD12 | FSMC A17, LCD RS (data / command) | (unverified) |
| PD13 | LCD reset | (unverified) |
| PD14 / PD15 | FSMC D0 / D1 | (unverified) |
2.5 Port E
| Pin | Function | Notes |
|---|---|---|
| PE0 | Green LED (RX) | (unverified) |
| PE1 | Red LED (TX) | (unverified) |
| PE2 | HR_C5000 configuration port chip select | (unverified) |
| PE3 | HR_C5000 SDO -> MCU (input, pull-up) | bench |
| PE4 | SKY72310 clock (idles high) | bench |
| PE5 | MCU -> HR_C5000 SDI, also the SKY72310 data | bench |
| PE6 | DMR_SLEEP, HR_C5000 power down (high = down) | (unverified); polarity as the HR_C6000 (unverified) |
| PE7-PE10 | FSMC D4-D7 | (unverified) |
| PE11 | PTT, active low | (unverified) |
| PE12 | External PTT (accessory jack), active low | (unverified) |
| PE13 | FM_MUTE | high = FM audio through, low = muted (measured on RadioFoundry) |
| PE14 / PE15 | Channel switch ECN0 / ECN1 | (unverified) |
2.6 Reset-time levels and the SWD pins
board_early_init sets, before anything slow: the power latch (PA7) high; the HR_C5000 chip select and power
down high and its clock low; the SKY72310 clock high; the SKY72310 chip select high; the transmitter supply, APC loop and receiver supply low and VCO select high (the receive VCO);
DMR_SW high; speaker muted, amplifier off, FM muted; LEDs off; keypad rows low; the flash chip select high. PD8 and
PD9 are left as inputs (section 13). These are safe choices (unverified).
Transmitter safe state (board_rf_safe, crash-log.md 8), at every fault, reset and stub entry and first
in main(): APC_EN (PC4) and 5TC (PC5) low, VCO_SEL (PA9) high (the receive VCO), the APC DAC (PA4, DAC1 channel 1) 0
when its clock is on. The SKY72310 and HR_C5000 keep their state until the next start.
PA13 and PA14 are the SWD pins. The radio needs them as the IF bandwidth switch and the microphone supply, so
radio_md380.c takes them over at radio init; a debugger then has to connect under reset. Build with
MD380_DEFS=-DMD380_KEEP_SWD to leave them alone: the microphone is then
unpowered and the bandwidth switch stays as the hardware pulls it.
3. Interrupts, DMA, timers
Our allocation, following the DM-1701 (boards/dm1701), since the same MCU pins carry the same HR_C5000 lines:
| Source | Use | Priority |
|---|---|---|
| EXTI0 / EXTI1 / EXTI2 (PC0-PC2) | HR_C5000 timeslot / system / RF TX interrupts (falling edge) | 5 |
| TIM7 | 30 ms one-shot after the RF TX interrupt: the burst's PA off (section 5.6) | 5 |
| DMA1 Stream0 ch0 / Stream5 ch2 | I2S3 receive / I2S3ext transmit (PCM, section 6.2) | 6 |
| DMA2 Stream4 ch0 | ADC1 continuous scan (circular; with CONFIG_SUBTONE half and full transfer interrupts feed the sub-audio detector, section 6.4) |
7 with CONFIG_SUBTONE, else none |
| USART3 | GPS NMEA receive (RXNE interrupt into a ring buffer) | 7 |
| TIM8 | LCD backlight PWM, CH1 on PC6 | - |
| TIM3 | sub-audio tone PWM, CH2 on PC7, only with CONFIG_SUBTONE and only while a tone is sent (section 6.4) |
6 |
| OTG_FS | USB (platform) | platform |
Priority 5 is FreeRTOS's configMAX_SYSCALL_INTERRUPT_PRIORITY (platform/stm32f4/FreeRTOSConfig.h): the modem ISRs
notify the DMR task, and a masked section at that level makes every configuration-port and synthesizer transaction
atomic.
4. HR_C5000
4.1 Overview
The HR_C5000 is the HR_C6000's predecessor in the same family: DMR layer 1/2 baseband, two-point modulation outputs,
an IF input from the receiver, and an audio codec with I2S. RadioFoundry has a driver of its own for it,
drivers/modem/hrc5000, which shares only the chip-independent code (drivers/modem/hrc_common: the bus accessors and
the DMR slot engine) with the HR_C6000 driver. The shared engine reaches the chip through a description struct
(hrc_chip_t); the MD-380 build links no HR_C6000 code. The HR_C5000 user manual (HR_C5000 用户手册 V2.0, Chinese,
35 pages, datasheets/C5000/hrc5000.pdf) is the source for the register names and reset values in 4.6. It has no
register map for 0x25, 0x26, 0x37, 0x39, 0x3D, 0x41, 0x45, 0x47, 0x48 or 0xB9-0xBB; those values are (unverified) until a radio
confirms them.
4.2 Configuration port and the synthesizer bus
Bit-banged SPI on PE2 (CS), PC13 (clock), PE5 (MCU -> chip) and PE3 (chip -> MCU) bench. Unlike the HR_C6000 on the DM-1701, the clock idles LOW. Per bit: clock high, set MOSI, clock low (the chip samples MOSI on this falling edge), waits, then samples MISO. The chip drives the next MISO bit only after the falling edge: sampled at once, every read comes one bit late (0x10 read back 0x40 after 0x80 was written, and the LC 00 00 00 00 00 63 30 5A 5E read 80 00 00 00 00 31 98 2D 2F) on air 2026-10-18. Register reads in logs taken before that fix (issue 3, issue 66, up to 2026-10-18) are shifted right by one bit. On any other pin choice every read returned 0xFF. A transaction is 3 bytes for a register write (page, register, value) and 2 bytes plus the data for a read (0x80 | page, register), with CS low around the whole frame. A few cycles per half bit is enough; speed is not a constraint.
The SKY72310 has its own clock (PE4) and chip select (PD11); only the data line (PE5) is shared with the HR_C5000
bench. Its clock idles high: the MCU sets the data bit while the clock is high, drops the clock, raises it again,
and the chip latches the bit on the rising edge. Because PE5 is shared, a transaction on either chip is one masked
section (md380_rfbus_lock), so a modem ISR's register access never lands inside a synthesizer word or the reverse.
4.3 Differences from the HR_C6000
The chip description in hrc5000.c carries these; the HR_C6000 description in hrc6000.c carries the other column.
| Item | HR_C6000 | HR_C5000 | Status |
|---|---|---|---|
| Configuration page | 4 | 0 | bench |
| Reg 0x10 in DMR | 0x6E | 0x6B simplex, 0x4F on a repeater channel; RepeaterSlot (b0) stays 1 and the engine picks the slot from the TC (next row) | manual: b2 ISRepeater = repeated operation, b0 RepeaterSlot. On air through an MMDVM duplex hotspot (issue 108): 0x4F with the engine's slot choice lands every call on the channel's slot, both slots, 2026-10-21. 0x6E / 0x6F (timeslot mode, RepeaterSlot 0 / 1) did not steer the slot: 6 key-ups each went out 2,1,1,1,1,1 and 1,1,1,1,2,1 (2026-10-20) |
| Own DMR ID 0x14-0x16 | big endian | 0x14 holds the low byte | manual (LocalSreAddress L, M, H) |
| TX start / stop | access policy and vocoder control written | tx_begin / tx_end hooks absent |
(unverified) the vocoder runs on the MCU, so 0x22 (EncodeStart) and 0x60 are not used for voice |
| Reg 0x5F (sync / slot report) | read at every burst | absent: DMO / RMO come from the channel | (unverified) not in the manual's table |
| Embedded LC LCSS | chip | MCU writes it in 0x50 for every voice burst: A-F = 0x08 0x19 0x2B 0x3B 0x4A 0x58 | (unverified) |
| CRC_MCU_Control (0x40 bit 3) | present | absent: for CSBK, MBC header and data header write 10 bytes and the chip appends the CRC | on air 2026-10-20: CSBKs and data from the MD-380 decode with good CRC on a DM-32UV and an MMDVM hotspot |
| CACH TC report | 0x52 bit 2, refreshed in received slots | 0x52 bit 2 too (the manual calls bits 3-0 reserved), but it changes only in slots opened for receive, and 1 means TS2 is starting | per-tick probe of a hotspot downlink, 2026-10-21: with receive open in both slots the bit alternates every tick (0x55555555 over 32 ticks); with one slot closed it stops changing. That froze the engine's lock (it closed the other slot once locked) and every repeater key-up was rejected or landed on either slot. The chip flag tc_last_rx keeps both slots open and reads the bit only after a received slot. Polarity from the calls: read as the slot that had ended, every call landed on the other slot |
| Mic gain | 0xE4 = 0x20 + gain | 0x0F = raw value (C5000_MIC_GAIN in radio_md380.c) |
(unverified) |
DMR set-up (hrc5000_set_mode_dmr) writes 0x06 before the reset-release block (0x00 = 0xFF) and FM after it (unverified);
it runs once per mode change; per-burst switching (hrc5000_dmr_path) touches only the
RF side:
- TX: 0x0D = 0x8C, 0x0E = 0x44, mic gain, 0x37 = 0xC2, 0x25 = 0x0E, 0x26 = 0xFE (RX IF and audio off), 0x45 / 0x46 = the calibrated DMR modulation pair, 0x35 = 0x1E, 0x3F = 0x04.
- RX: 0x0E = 0x44, mic gain, 0x37 = 0xC2, 0x25 = 0x0E, 0x26 = 0xFD.
VHF radios swap I and Q with 0x01 = 0xF0 after the init. The manual gives 0x01 as the TX and RX IQ/QI bits; which value the receiver needs is still to be confirmed on a VHF radio (unverified).
FM (hrc5000_set_mode_fm_rx, hrc5000_set_mode_fm_tx) uses its own tables. The TX deviation register 0x35 takes 0x30
(narrow) or 0x3C (wide) in FM and 0x1E in DMR; the narrow / wide mapping is inferred (unverified). CTCSS and DTMF are not
implemented for this chip yet.
4.4 Voice port
Hardware SPI2 (PB13-PB15, AF5) with chip select PB12, commands 0x03 (write) and 0x83 (read) followed by 0x00 and 27 bytes (one voice burst's payload) bench. RadioFoundry uses SPI mode 2 at 1.125 MHz on air 2026-10-18. At 4.5 MHz transmit voice decodes on another radio, but received voice reads back with the first bits of every byte wrong: the C0 words check only two bits off their place and C1 never does. At 1.125 MHz the three frames are in place and C1 checks too; the first data byte after the command still has a few bit errors that the Golay codes correct (unverified).
The voice RAM is 32 bytes: a longer read wraps. A received burst is in it as the three 72-bit AMBE+2 air frames in a
row (the HR_C6000 layout, core/dmr/ambe/ambeFec.h), but only while the decoder runs (4.8).
4.5 Reference oscillator
The calibration's reference tune byte t (section 11) sets regs 0x47 = 0x04 = 0x7F - t, 0x48 = 0x00 if t < 0x80
else 0x03, and DAC2 = t x 4 + 0x600 bench. hrc5000_set_ref() and hrc5000_ref_dac(); the stored tune is
re-applied by every full mode set-up.
4.6 Power and start-up
PE6 low wakes the chip; RadioFoundry waits 12 ms before the first access. hrc5000_power_on then programs the clock
(0x0A, 0x0B, 0x0C, 0xB9-0xBB) and enters FM receive. After init the driver reads 0x10 back for the diagnostics page
(bring-up stage 4).
4.7 Checked against the HR_C5000 user manual
The manual's register table (section 2, "参数配置") runs from 0x00 to 0x90 with reset values. What it says about the registers this driver writes:
| Register | Manual | Driver value | Status |
|---|---|---|---|
| 0x0A | CLKOutDiv, the output clock divider on CKOut (reset 0x02) | 0x80, then 0x00, in the clock step | Not a clock source switch; the effect is unknown (unverified) |
| 0x0B | PLLM (reset 0x0A) | 0x28 | : with XTAL 29.4912 MHz, M = 40 |
| 0x0C | PLLBP bit 7 (0 = PLL, reset 0x82 = bypass), PLL SLEEP bit 6, PLLDO bits 5-4, PLLN bits 3-0 | 0x33 | : N = 3, DO = 3 (divide by 8), PLL on. 29.4912 x 40 / 3 / 8 = 49.152 MHz, the manual's system clock |
| 0xB9-0xBB | not in the register table | 0x32, 0x22, 0x11 | Not confirmed. The manual gives the system clocks as fixed from 49.152 MHz: DMR 9.8304 MHz, FM 8.192 MHz, CodeC 12.288 MHz, external CodeC 24.576 MHz (unverified) |
| 0x18 / 0x19 | section 6 says the system clock is set here; the table defines them as group address registers | not written | Contradiction in the manual; unresolved (unverified) |
| 0x00 | active-low resets: bit 7 DMR protocol, bit 6 PHY, bit 5 codec, bit 4 FM, bit 3 vocoder, bit 2 UDC/DDC, bit 1 I2S, bit 0 CodeC (0 = reset) | 0x28 | DMR frames go out with DMR protocol and PHY at 0, so 0 is a running block here and the manual's polarity looks inverted (unverified) |
| 0x01 | bit 7 RFTransIQMode, bit 6 RFRecvIQMode (0 = IQ, 1 = QI) | 0xB0 (UHF), 0xF0 (VHF) | UHF: TX QI, RX IQ. VHF: both QI (unverified) |
| 0x10 | ModulatorMode bit 7 (0 DMR, 1 FM), ISRepeater bit 2 | 0x6B simplex, 0x4F repeater, 0x80 FM | |
| 0x11 | LocalChanMode bit 7 (1 = valid) | 0x80 | |
| 0x14-0x16 | LocalSreAddress L, M, H (low byte first) | DMR ID, low byte first | |
| 0x1F | LocalEMB, colour code in bits 7-4 | 0x10 (CC 1) | |
| 0x06 | Vocoder638 bit 7 (0 = CMX638 interface off); bit 0 LocalVocoderControl (1 = the CPU controls the vocoder switch) | 0x21 | Consistent; the other bits are not described (unverified) |
| 0x0D | Codec Ctrl, 0xC8 reset: bit 7 MCU controls the codec signals | 0x8C | Bit 7 set; the other bits unconfirmed (unverified) |
| 0x0E | HPoutEn, bit 7 (0 = off, reset 0x40) | 0x44 | Headphone output off; the value stays until a test shows otherwise (unverified) |
| 0x0F | ADLinVol (reset 0xB8) | 0xC8 | In range; the function is not confirmed (unverified) |
| 0x1D, 0x1E | LocalUnaddress and LocalBroadCast, receive addresses in bits 7-4 | 0xFF and 0xF1 | Reads as address 0xF; confirm on air (unverified) |
| 0x21 / 0x22 | access policy; EncodeStart bit 7, EncodeStop bit 6 | not written by default | See 4.3 (unverified) |
| 0x83, 0x87 | InterClear (write 1 to clear), SendStopMask | 0xFF, 0x00 | for the clear |
| 0x30-0x33 | BCLK / LRCK counts for the external codec I2S: Mclk / BCLK and Mclk / LRCK (reset 0) | 0x00 0x17 0x02 0xFF, from the 0x11 block | 24 and 768 counts, so BCLK 512 kHz and LRCK 16 kHz from a 12.288 MHz Mclk, or 256 kHz and 8 kHz from 6.144 MHz. Which Mclk the board gives is not known (unverified) |
| 0x36 | not in the manual. The HR_C6000's manual has bit 7 = external codec BCLK / LRCK off | 0xF0 from the 0x11 block; hrc5000_i2s_clock() writes 0x70 (bit 7 clear) while a PCM stream runs |
See 4.8 (unverified) |
Not in the manual: 0x25, 0x26, 0x36, 0x37, 0x39, 0x3D, 0x41, 0x45, 0x47, 0x48 and 0xB9-0xBB. Their values stay unconfirmed (unverified) until they are checked on a radio.
4.8 I2S clocks and the receive interrupts
Tester logs: issue 3.
| Fact | Basis |
|---|---|
Before any RadioFoundry write the chip's I2S pins toggle (the reset state has the clocks running); after the set-ups they are driven but do not move (ck 0 ws 0) |
tester logs |
| Every set-up's 0x11 block writes 0x36 = 0xF0. On the HR_C6000 bit 7 of 0x36 turns the external-codec BCLK / LRCK off; the C5000 has the same pins (17-20, BCLK / LRCK / ADCDAT / DACDAT) | HR_C6000 manual |
| RadioFoundry never wrote 0x36 after the block, so with the same bit meaning the clocks stay off. The reset register tries (0x28, 0x2A, 0x3F, 0xFF) changed nothing | tester logs |
The driver writes 0x36 = 0x00 while a PCM stream runs and puts 0xF0 back when it stops. Clearing bit 7 alone (0x70)
left the clocks stopped; with 0x00 they run and DMR transmit voice decodes on another radio (issue 66, 2026-10-17).
On receive the PCM doesn't reach the speaker yet: with every value from 0x00 to 0x60 the speaker plays only a faint
TDMA buzz, which sounds like the receiver's audio passed through (4.9)
(hrc5000_i2s_clock(), called from audio_md380.c); a full set-up re-applies the state, since its block rewrites 0x36.
The value is a hypothesis (unverified): the bit is not in the C5000 manual, and bits 6-4 are unknown. -DHRC5000_I2S_CTL_ON=<v>
tries another value (0x00, 0x30). With the clocks running, the diagnostics line i2s ck N ws N (MD380_I2S_PROBE)
gives the rates. The Mclk arithmetic in 4.7 says 16 kHz or 8 kHz for WS; the STM32 side assumes 8 kHz with 32-bit
channels, so a 16 kHz WS means the sample rate or the channel length needs changing (unverified).
Interrupts (manual section 7, with the sub-status registers):
| Item | Fact |
|---|---|
| 0x82 flags | bit 7 send request rejected, bit 6 send start (sub-status 0x84, mask 0x85), bit 5 send end (0x86, mask 0x87), bit 4 late access, bit 3 data received (0x51 gives type and CRC), bit 2 information received (sub-status 0x90: 0 = the whole message passed its check, 1 = failed), bit 1 abnormal exit, bit 0 physical-layer receive. Only the system interrupt needs the write-1 clear in 0x83 |
| 0x51 | bits 7-4 data type, bit 3 PI, bit 2 CRC (0 = good), bits 1-0 sync class (0 none, 1 voice, 2 data, 3 RC) |
| 0x52 | bits 7-4 received color code, bits 3-0 reserved; the slot (timecode) is read from bit 2 |
| Pins | TIME_SLOT_INTER every 30 ms in layer 2, used by the CPU to decide whether and what to send in the next slot; RF_RX_INTER / RF_TX_INTER the receive / transmit switch; SYS_INTER the system interrupt |
| Voice | The CPU reads and writes the voice port (SPI2 on this board) at times it derives from the captured start and end of a call; voice is not announced per burst by the system interrupt |
The tester's logs fit this: flag 0x04 (information) carries the sync-class reports, 0x08 (data) the others, and 0x52 reads 0x08 or 0x18 because the color code is only valid in a report that decoded a burst with one (a voice sync burst has none). Whether the engine can take a received voice call from these reports alone, or must run its voice reads from the 30 ms tick as the manual describes, is open (unverified).
4.9 DMR receive
The HR_C5000 doesn't report each received burst: there is no voice class report per burst as on the HR_C6000. With the reads fixed (4.2), a call shows as on air 2026-10-18, against an AnyTone AT-D878UV:
| Event | What the chip gives |
|---|---|
| Voice LC header, terminator | System interrupt 0x08 with 0x51 = 0x12 / 0x22 (type, class 2) and the LC in page 2 from 0x00 |
| Voice | Nothing per burst. The voice RAM changes at every tick, the other slot's ticks included |
| Late entry, information | 0x10 and 0x04: the LC of the superframe |
The manual's receive flow: with the CPU in charge of the vocoder (0x06 bit 0, which RadioFoundry sets), the CPU starts the decoder after the voice LC header or late entry interrupt by writing 0x22 bit 5 (DecodeStart), and stops it at the terminator or loss of sync (bit 4, DecodeStop). Without DecodeStart the voice RAM never holds received voice (0 of 324 frames checked at any bit offset); with it, the read at the tick after the call's slot holds that slot's burst.
The slot engine does this for chips with rx_voice_read (hrc_chip_t): a header, late entry or information report
starts the decoder (0x22 = 0x27, bits 2-0 keeping the 0x11 block's 0x07), the voice port is read at every tick, the
first read whose frames check (two of three C0 words with at most one error) locks the slot, and that slot's reads
go to the stack as bursts A to F. A read that hasn't changed, or has no frame that checks, is a lost burst; six in a
row end the reception (0x22 = 0x17). The chip's voice class reports are ignored, and its 25 ms report delay doesn't
count as out of step.
On air with this (2026-10-18): calls are taken with the caller and talk group, and 393 frames reached the codec with
2 bit errors in all. The decoded audio is speech (peak 3521 of 32767) and streams to the chip (aud blk climbing),
but the speaker plays only a faint buzz (4.8). Leads, not checked: 0x0E (HPoutEn, bit 7; RadioFoundry writes 0x44, which
leaves it off), 0x37 (DAC gain), the I2S data line directions and sample rate (unknown 17).
4.10 Codec and audio routing
DMR receive audio plays with the LINEOUT values below (on air, 2026-10-19: our test, not authoritative). This section keeps the manual's facts apart from what our tests showed and what we only suspect.
From the user manual:
| Item | Fact |
|---|---|
| Audio pins | 2 HPOUT, 10 LINEOUT, 5-8 MIC2 / MIC1 differential inputs, 11 MICBIAS |
| External-codec I2S | 17 ADCDAT (in), 18 BCLK (out), 19 LRCK (out), 20 DACDAT (out): for an external codec |
| SPI ports | SPI0 = pins 53-57 (the configuration port; the chip is the slave). SPI1 = pins 33-36, "generic vocoder interface" (the voice port; slave). SPI2 = pins 37-43, "codec and vocoder interface" (C_CS and C_SCLK are outputs: the chip drives it) |
| SPI0 command, bits 2-0 | 000 configuration registers, 001 auxiliary table, 010 information data, 011 tone data, 100 CMX638, 101 AMBE3000; bit 7 = read |
| Tone data | the CPU writes the codec over SPI0 with command 0x03, address 0x00 and two bytes; the required write interval is missing from the manual's text |
| 0x06 | bits 7-6 vocoder interface (AMBE3000 / CMX638), 5 DMRFrom (1 = from SPI1), 4 VocoderFrom, 3 SPIFrom, 2 CodeCMode (0 internal, 1 external), 1 OpenMusic, 0 CPU vocoder control |
| MUX4 (codec source) | 0x06 bit 1 = 1: SPI >> codec (highest priority); else 0x10 bit 7 = 1 (FM): codec <> FM; else (DMR): codec <> vocoder |
| 0x0D | bit 7 CPU controls the codec, 6 suspend, 5 sleep, 4 Switch2 (K2), 3 anti-pop; reset 0xC8 |
| 0x0E | bit 7 HPoutEn, 6 HPMute, 5-4 HP volume (0 / 2 / 4 / 6 dB), 3 LineOutEn, 2 Mic1En, 1 Mic2En, 0 Switch1; reset 0x40 |
| 0x0F | bits 7-3 ADLinVol (+12 to -34.5 dB in 1.5 dB steps), 2-1 MicVol (0 / 6 / 12 / 20 dB) |
The driver writes 0x0E = 0x44 (headphone muted, line out off, mic 1 on) and 0x06 = 0x21 in DMR.
What the tests showed (on air, 2026-10-19; not authoritative). The full log is in the feature/md380_follow_up
pull request. Each test started from a power-on, and register reads confirmed the writes took effect.
- Tone data works in FM: a 1 kHz square wave (16-bit, MSB first, one sample every 125 us) played cleanly with 0x0E = 0xBD, which needs bit 2 (Mic1En) and bit 0 (Switch1) set.
- In DMR, no HPOUT-side setting played the MCU's samples, by I2S or as tone data. With 0x0E bit 2 set, DMR mode feeds the codec's input to the speaker (acoustic feedback). FM_SW and FM_MUTE both high plays the receiver's 4FSK as a buzz.
- 0x06 bit 2 = 1 (external codec) during a call stops the voice decoding until the next set-up.
- During a received call the chip sends the MCU a slow ramp near 0x55F8 over I2S (the mic converter's level), never the samples the MCU sent.
- A handheld transmitting a few feet away puts a TDMA buzz into the audio amplifier: faint pulsing in a test can be that, not a routing result.
The fix (our test, on air 2026-10-19; not authoritative). The speaker amplifier is fed from the codec's LINEOUT
(pin 10, 0x0E bit 3), not HPOUT. The tests above never had 0x0D bit 4 (Switch2) and 0x0E bit 3 (LineOutEn) set together
while a call played. hrc5000_speaker_voice() now writes, when a DMR PCM stream opens the route: 0x0D = 0x9C, 0x0E = 0x4C
(HPMute stays set, HPoutEn stays off, Mic1En on) and 0x37 = 0xC2, with 0x36 = 0x00 already written; it writes 0x0D = 0x8C and
0x0E = 0x44 back when the route closes, which should also end the buzz after a call. -DHRC5000_SPK_VOICE=0 leaves it out.
On an MD-380 (UHF, D002.032 codec) against an AT-D878UV on 446.075 TS1, with the build at 17d89001 plus the codec
move: received speech plays clear from the speaker and transmit is clear (one 10 s call: 498 frames, 10 bit errors, 3
concealed, 0 lost). Why both bits are needed together isn't in the manual (unverified). Not yet checked with it: the buzz after
a call or after our own transmit, the key beep in DMR and FM, and FM receive.
5. RF
5.1 Bands and power
One band per radio: UHF 400-480 MHz or VHF 136-174 MHz, 1 W and 5 W (unverified). RadioFoundry builds one image per band
(make BOARD=md380 BAND=uhf|vhf, default UHF); the image's band table refuses the other band. Which band a
radio is can't be read from the hardware by any method we know (the calibration's frequency list, 11.2, is a
candidate for a run-time check).
5.2 SKY72310 synthesizer
Skyworks SKY72310-11 (unverified as the part fitted to this radio). From the data sheet:
- Serial interface: 16-bit words, MSB first: 4 address bits then 12 data bits, latched when CS goes high. Write only.
- Registers: 0 main divider (N_reg = floor(N) - 32, 9 bits, 6..505 in fractional mode); 1 dividend MSB (10 bits); 2 dividend LSB (8 bits); 5 reference divider (R - 1, 5 bits); 6 phase detector gain (5 bits, 0.125-1 mA/rad); 7 power down and Mux_out (bit 0 full power down, 1 synthesizer power down, 2 integer-N, 3 10-bit modulator, 8:6 Mux_out source, 9 tristate); 8 modulation control; 9 modulation data; 14 delta-sigma (0x080, bit 5 signed mode).
- N = f_vco / f_pd with f_pd = f_osc / R <= 50 MHz; the 18-bit dividend is round(2^18 x frac(N)) in unsigned mode.
- The driver uses the signed mode (register 14 bit 5): N_reg = round(N) - 32 and a two's complement dividend for a fraction of -0.5..+0.5. Sent unsigned, every channel whose fraction was over one half came out one phase detector step (8.4 MHz) low with lock detect high: at 443.7, 444.0-444.5, 435 and 450 MHz the radio received nothing, at 440, 445 and 446.075 it did (DM-32UV as the sender, 2026-10-20, issue 108). Either the unsigned-mode write doesn't take or the part runs signed; signed encoding is right in both cases on air.
- A new ratio takes effect on the dividend MSB write: order divider, LSB, MSB.
- Lock detect high = locked (on the Mux_out / LD pin, wired to PD10 (unverified)). The pin is the PS output, open collector (unverified): the MCU's pull-up gives the high (5.7).
drivers/rf/sky72310 implements this and tests/host/sky72310Test.c checks it against a model of the chip. The
board picks the smallest reference divider that keeps N in the fractional range (2 or 3 at 16.8 MHz), the phase
detector gain at its maximum (the loop filter was designed for a value that is unknown) and Mux_out = the
oscillator. The lock detect output's configuration isn't in our copy of the data sheet (unverified).
TX carrier offset (issue 108). One MD-380 UHF measured about +1.9 kHz high at 448.700 MHz (an MMDVM hotspot's
DMR BER test decoded it only from 448.70175 to 448.70200 MHz, BER 0.14-0.28 %; a DM-32UV measured the same way sat
within 50 Hz of 448.700). Hotspot modems (ADF7021) decode only about +-250 Hz around the carrier, so that radio never
woke a duplex hotspot. MD380_TX_OFFSET_HZ (radio_md380.c, a build define) corrects it; -1900 put it within 250 Hz.
Whether the offset is the unit's own error, which the calibration's reference tune (11) should take out, or the way
RadioFoundry applies that tune, is open (unverified): measure a second radio before setting a default.
5.3 Frequency plan
Assumed from the MD-9600, which has the same TCXO and synthesizer and whose notes give the IF [radios.md MD-9600 section]:
| Item | Value | Tag |
|---|---|---|
| Reference | 16.8 MHz TCXO | , from the MD-9600 |
| First IF | 49.95 MHz | , from the MD-9600 |
| UHF receive | VCO = f - 49.95 MHz (LO below) | (unverified) |
| VHF receive | VCO = 2 x (f + 49.95 MHz), divided by 2 (LO above) | (unverified) |
| UHF transmit | VCO = f | (unverified) |
| VHF transmit | VCO = 2 x f | (unverified) |
radio_md380.c keeps the plan in MD380_TCXO_HZ, MD380_IF_HZ, MD380_RX_LO_ABOVE and MD380_VCO_MULT. A wrong
plan shows at once on the bench: no lock (diagnostics "pll UNLK") or a carrier off by a fixed amount (bring-up stage 4).
5.4 Receive chain and switching
Receive: VCOVCC_SW selects the RX VCO, the synthesizer is tuned, 5RC (PC9) powers the receiver, DAC1 sets the front-end tuning voltage from the calibration (11.2), the GT3136 IF goes to the HR_C5000 with DMR_SW (PA10) high, and FM_SW (PB2) is high in FM receive (6.1). W/N_SW (PA13) sets the IF filter for 12.5 or 25 kHz (unverified).
Transmit: 5RC off, DMR_SW low, VCOVCC_SW to the TX VCO, the synthesizer to the TX frequency and locked, then 5TC (PC5), DAC1 = APC drive, RF_APC_SW (PC4) on (unverified for the pins, order ours). FM TX waits for lock detect (5 ms at most) and refuses to key without it.
5.5 RSSI, noise, squelch
RSSI is an analog level on PB0 and the receiver's noise level ("BUSY") on PB1 (unverified). Neither curve is known (unverified). RadioFoundry reports rssi_dbm = -130 + raw x 90 / 4095 and noise = raw / 16 as placeholders, and shows both raw values on the diagnostics page so a bench session with a signal generator can fit them.
Measured on RadioFoundry (UHF, 446.000 MHz, raw ADC, a second radio nearby; 2026-10-14):
| RSSI (IN8) | BUSY (IN9) | |
|---|---|---|
| no signal | 690-760 | 1340-1620 |
| FM carrier, shown as -71 dBm | 2720-2732 | 39-81 |
BUSY falls with a signal (larger = more noise). With the USB programming cable in the accessory jack the receiver doesn't detect a signal at all (RSSI stays near the no-signal level): read these values with the cable out (a log line read back afterwards), or by ear.
Roaming reads this RSSI (the IF chain's level on an ADC) as the site signal (dmr-roaming.md section 3).
5.6 PA and power control
DAC1 is the APC reference, scaled from the calibration's power rows (11.2). Two levels, 1 W and 5 W. TX is refused without an accepted calibration table (no safe uncalibrated drive is known).
DMR: the PA follows the bursts. The RF TX interrupt keys it (APC_EN); a TIM7 one-shot drops it 30 ms later 3.4, and the slot engine's PA-off at the next timeslot interrupt stays as the fallback.
The radio has one synthesizer, so it can't receive while it is set up to transmit. The switch to TX (receiver off, the HR_C5000's transmit set, the TX VCO locked, TX_5V up) happens before the first burst, never at it: done in the RF TX interrupt, the first burst went out unlocked and a one-burst wake-up never reached a repeater. When:
| Start | Switch to TX |
|---|---|
| Own timing (simplex, the repeater wake-up) | at the modem's tx_begin, or at radio tx() if that comes later |
| Aligned to a repeater's slots | at the engine's tx_slot_found, two ticks before the first burst. Until then the receiver stays on: the engine needs the downlink's TC to find the slot (4, the CACH TC row) |
On a split channel the synthesizer returns to RX at the engine's TX done, so the radio hears the repeater's answer to
its wake-up; simplex stays on TX until tx_off().
5.7 The 5C rail (SAVE)
PA8 ("SAVE") is the enable of U404 (XC6204B502, CE active high), the regulator for the 5C rail; 5C feeds the
SKY72310's 3.3 V regulator U602 (XC6204B332, CE tied to its input) and part of the receive chain [schematic,
page 4]. PA8 is an output, driven high. board.c drives it high at start-up.
Measured on a UHF MD-380 with no signal: the raw RSSI reading (ADC1 IN8) went from 246 with PA8 left as an input to 640-720 with it high, and the RSSI follows a received carrier. PD10 (PS, pulled up) reads 1 with SAVE low (the synthesizer unpowered, PS floating) and 0 with SAVE high (powered, not yet programmed: out of lock).
6. Audio
6.1 Paths
FM receive audio and DMR voice both reach the amplifier through the HR_C5000 in RadioFoundry's model: FM through its feed-through (0x36) behind FM_MUTE (PE13), DMR through its codec DAC; SPK_C (PB8) mutes the speaker, AFCO (PB9) turns the amplifier on (the routing (unverified)). Measured on RadioFoundry with a pin scan (the 16 levels of FM_MUTE, FM_SW, SPK_C and AFCO, monitor held on an FM channel): noise only with FM_MUTE high, FM_SW high, SPK_C low and AFCO high. With those levels a received FM call plays clearly. The GT3136's discriminator audio also reaches the MCU on PC3, which RadioFoundry doesn't use. Our tests (2026-10-19, not authoritative) found the HR_C5000's codec output reaching the speaker with FM_SW and FM_MUTE low; DMR voice through it doesn't work yet (4.10).
The volume knob sets the level in the analog path; the MCU only sees its position on PA0. The board therefore has no
CAP_VOLUME_POT and hal_power_t.volume() returns -1. There is no hal_radio_t.volume either: the MCU has no way to set
the speaker level (the HR_C5000's feed-through gain isn't used for it, and the knob acts after it), so the debug link's
volume operation (RADIO op 30) answers "unavailable" (radio-features.md 4.2).
6.2 I2S PCM
I2S3 full duplex, slave to the HR_C5000's codec: WS PA15, CK PC10, SD PC12 (chip -> MCU) and ext SD PC11 (MCU ->
chip), Philips, 16-bit samples in 32-bit channels, 8 kHz (unverified for the pins; format and directions copied from the
DM-1701 (unverified)). audio_md380.c is the DM-1701's driver. Which HR_C5000 port these lines reach (the external-codec
pins or SPI2, the codec / vocoder port) is open (4.10).
6.3 Microphone
Bias from MICPWR_SW (PA14), on whenever the radio runs (section 2.6). Mic level on PA3 for VOX (unverified).
6.4 Sub-audio
CTCSS / DCS are decoded in software from QT_DQT_IN (PA2) and generated as PWM on
CTC/DCS_OUT (PC7) (unverified). RadioFoundry does the same behind CONFIG_SUBTONE (on by default, 3.3 KB of
flash). CTCSS 100.0 Hz works on air both ways (tester, 2026-10-19); DCS and the deviation aren't checked yet. Without
the switch, channels with a tone open on carrier and send no tone.
The FM TX tones (1750 Hz, roger beep) and APRS AFSK are not written (CONFIG_FM_TX_TONES, CONFIG_APRS_RX in
config.mk).
The DSP is chip independent (drivers/rf/subaudio, host-tested on synthetic signals); boards/md380/subtone_md380.c
and power_md380.c are the glue.
Receive. PA2 is one more rank of the ADC1 scan that power_md380.c already runs (IN2 after the temperature
sensor, 480-cycle sampling, ADC clock PCLK2 / 8). Seven ranks give a pass every 383 us, so the sub-audio is sampled at
PCLK2 / 8 / (7 x 492) = 2613 Hz (computed at run time). The DMA stream's half and full transfer interrupts (DMA2
Stream4, priority 7) hand each new sample to the detector; the ring grows from 8 to 16 scans, so the averaged RSSI
and battery readings cover 6 ms instead of 3 ms. The detector (subaudio_rx.h):
| CTCSS | DCS | |
|---|---|---|
| Method | Goertzel at the tone and 3 Hz either side, over overlapping 0.2 s windows, a result every 0.1 s | slicer with peak-tracked threshold, clock recovery on the edges, 23-bit register against the Golay word |
| Opens | tone holds 30 % of the AC power, beats both guard bins by 1 dB, amplitude at least 8 counts; two results in a row | word matches with at most 2 bit errors, twice 23 bits (+-1) apart |
| Closes | three bad results in a row (0.3 s); a 150 ms phase reversal at the end of a transmission is ridden out | no match for three words (0.5 s) |
| Tested | all 50 standard tones, neighbors rejected, +-1 Hz sender error, noise only for 60 s | all 104 codes both polarities, wrong code and polarity rejected, +-1 % bit rate |
status().tone_ok is the detector's verdict (true for a channel without an RX tone); the core debounces it and gates
the squelch (radio-features.md). An inverted code is the same bits inverted on the wire; if the analog path inverts
the waveform, MD380_SUBTONE_RX_INVERT flips it back.
Transmit. TIM3 CH2 on PC7 (AF2) is a 10-bit PWM at 17.6 kHz (the timer clock divided by 4 x 1024). Its update
interrupt (priority 6, running only while a tone is sent) computes a new duty every fourth period, 4.4 kHz: a sine
from a 65-point table for CTCSS, the 23-bit word at 134.4 baud with a light smoothing for DCS. The level ramps up over
20 ms; MD380_SUBTONE_PERCENT (default 30) sets the swing as a share of the duty range. tx() starts it after the PA is
on, tx_off() stops it, and the pin returns to its idle low output. TIM3 and DMA2 Stream4's interrupt are the only
resources added (section 3). The DCS turn-off tail (radio-features.md 3.3a) switches the generator to a 134.4 Hz
sine at the same swing (subaudio_tx_tail, with the interrupt held off for the switch); not yet checked on a radio.
Unverified, check on a radio (docs/testing/md380-bringup.md 5 and 6):
- That PC7 reaches the modulator and where (summing into the HR_C5000's modulation input or the VCO line), and the deviation 30 % gives. CTCSS wants 0.5 to 0.75 kHz of deviation.
- The PWM's own filter: the carrier is 17.6 kHz and the tone at most 254.1 Hz, so any RC corner under about 1 kHz works.
- The signal on PA2: its level (
MD380_SUBTONE_MIN_AMP, default 8 counts of 4096) and its polarity for DCS. - That the ADC rate really is 2613 Hz (the 480-cycle sampling is the existing scan's) and that PA2 is not loaded by the longer sampling.
- A DCS transmission ends without the 134.4 Hz turn-off burst (not written); the receiving squelch tail may be longer than with a hardware decoder.
7. Display
160 x 128 color TFT on the FSMC 8-bit bus (NE1), command at 0x60000000 and data at 0x60040000 (A17 = RS on PD12),
chip select PD6, reset PD13: the DM-1701's wiring (unverified). The controller type is unconfirmed (an HX8302A or
an ILI9481 are candidates). RadioFoundry drives it with drivers/display/st7735 (assuming the ST7735 command set) with
safe-area insets of 1 px at the top and 2 px on the left (the case edge crowds them; right and bottom are close but usable; seen on the radio, 2026-10-16) and two build-time knobs for the bench: MD380_PANEL_TYPE (the st7735 driver's panel
type, default 0) and MD380_PANEL_MADCTL_XOR (orientation bits flipped, default 0). Backlight: LAMP (PC6), TIM8 CH1
PWM at 1 kHz (unverified for the pin; PWM ours).
8. LEDs
Green (RX) on PE0, red (TX) on PE1, active high (unverified).
9. Keypad, side keys, channel switch, PTT
9.1 Matrix
Rows K1 PA6, K2 PD2, K3 PD3, driven high one at a time; columns are the LCD data lines D0-D7, read under the LCD bus lock as on the DM-1701 (unverified for the pins). The key positions are assumed to be the MD-UV380's, the nearest documented relative (unverified):
| Row | D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 |
|---|---|---|---|---|---|---|---|---|
| K1 | 1 | 2 | 3 | 4 | 5 | 6 | 0 | * |
| K2 | Green (menu) | Up | Down | 7 | 8 | 9 | # | Red (back) |
| K3 | - | - | - | - | - | - | SK2 | SK1 |
The diagnostics page shows the raw row bits ("keys rr rr rr"), so one press of each key on a radio fills in this table.
9.2 Channel switch
A 16-position switch with four code lines ECN0-ECN3 on PE14, PE15, PB10, PB11 (unverified), not a quadrature encoder.
input_md380.c reads the code at every 10 ms scan and reports position changes as encoder steps (the shorter way
round across the stop). A code counts after three equal scans (30 ms): the contacts bounce. Pulled up, closed contacts
reading low (MD380_CH_ACTIVE_HIGH flips it). The code is a Gray code in its own line order. Read on RadioFoundry's diagnostics
page ("ch x", after the polarity), one detent at a time clockwise (bench, 2026-10-16):
| Position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Code | 0 | 8 | A | 2 | 3 | B | 9 | 1 | 5 | D | F | 7 | 6 | E | C | 4 |
Each step changes one line, so a bouncing contact reads as the next or previous position, never a jump.
9.3 PTT
PTT on PE11 and the accessory jack's PTT on PE12, both active low with pull-ups (unverified).
10. Power and analog inputs
10.1 Power switch and latch
The volume knob carries the power switch. It doesn't switch the supply: POW_C (PA7) is a latch the firmware holds (unverified),
and the switch breaks the battery sense line (and the volume pot's supply). Measured on a UHF MD-380 with
the radio on USB: the raw battery reading (ADC1 IN1) is 3341-3351 (8.3 V) with the knob on and 3-4 with it off, at once;
the volume reading falls from about 130-155 to 2-4. The radio stays on with the knob off, and with switch_on() always
on it only showed the battery running down. switch_on() now reads the knob from the battery line (radio-features.md
5.5, core/radio/power_switch.c, as on the DM-1701), and power-off drops the latch.
10.2 Battery
7.4 V 2S Li-ion pack (unverified). ADC1 IN1 through a divider: RadioFoundry assumes 40.3 counts per 0.1 V
(MD380_BATT_COUNTS_X10) (unverified). Low 6.2 V and critical 6.0 V are RadioFoundry's choices, as on the DM-1701.
10.3 ADC scan
ADC1 scans IN0 (volume), IN1 (battery), IN3 (mic), IN8 (RSSI), IN9 (noise) and IN16 (temperature sensor) continuously at
480 cycles each into a DMA ring of 8 scans; readings are averages over the ring (power_md380.c).
11. Calibration
11.1 Location
512 bytes in the SPI flash's security registers 1 and 2 (flash addresses 0x1000 and 0x2000, 256 bytes each) 11.1. A third 256-byte block must not be read into a 512-byte buffer 11.1; RadioFoundry reads exactly two. Read only: RadioFoundry never writes them.
11.2 Layout
Assumed to follow the MD-UV380 family's first rows (the same factory tool), single band, 9 points per row (unverified):
| Offset | Content |
|---|---|
| 0x009 | Reference oscillator tune t (section 4.5) |
| 0x010-0x018 | TX power, high (5 W), per point |
| 0x020-0x028 | TX power, low (1 W), per point |
| 0x030-0x038 | Receiver front-end tuning, per point |
| 0x130-0x138 / 0x140-0x148 | DMR modulation pair (0x45 / 0x46) |
| 0x170-0x178 / 0x180-0x188 | FM modulation pair |
| 0x1B0-0x1D3 | The 9 calibration frequencies, 4-byte BCD, least significant byte first, 10 Hz units |
Byte values are taken x 16 to the 12-bit DACs (unverified). calibration_md380.c accepts the table when the power rows are
neither erased nor zero and low < high at every point, and no tuning or modulation row is erased. If the frequency list
is not BCD, ascending and in band, it uses an even grid over the band instead and says so ("factory/grid").
11.3 Interpolation
Linear between the two neighboring points, clamped at the ends.
12. SPI flash
12.1 Device and bus
Winbond W25Q128FV, 16 MB, on SPI1 (PB3-PB5, AF5) with chip select PD7, mode 0, 4.5 MHz (unverified). Some
early VHF radios have a 1 MB part (unverified). flash_md380.c accepts any W25Q (JEDEC type 0x40) and probes the
size.
12.2 Map
On a 16 MB part RadioFoundry uses the DM-1701's map (settings 0x011000, journal 0x019000, open-firmware codeplug 0x000000 and 0x020000-0xE00000, native area 0xE00000, TFTP staging 0xF00000), so the open-firmware CPS works the same. The TYT codeplug at 0 is overwritten by the first open-firmware write: read it out with the manufacturer's CPS first if it matters (md380-migration.md does that, converts it with qdmr and writes it back). There is no calibration window in the main array (11.1). On a smaller part the core gets no areas: settings live in RAM and nothing is written.
13. GPS
13.1 Module
The G models carry a GPS module whose NMEA output goes to the MCU's USART3 (unverified). 9600 baud is assumed (unverified).
13.2 Detection
The same board ships with and without the module, and PD8 (the module's power switch on G models) is listed as a
second flash chip select on others 13.7. So nothing drives PD8 until the module has been heard:
gps_md380.c enables USART3 RX on PD9 at start-up and listens for 5 s. One checksum-valid sentence (any talker, up to
82 characters plus CR LF; drivers/gps/nmea_line) makes the module present: PD8 is then held at the level it already
had, and every sentence goes to the APRS position source (aprs_nmea, CONFIG_APRS). Nothing heard: the UART is
switched off and board.gps_present() stays false, so board_has_gps() hides the GPS features. UART errors are
counted and cleared in the interrupt, so one framing error never stops reception ( 13.7).
14. Bootloader, firmware images, codec
14.1 Firmware file
TYT ships the application as an encrypted "OutSecurityBin" container, the same family as the DM-1701's. The supported file:
| Item | Value |
|---|---|
| File | MD-380-D2.32(AD).bin (TYT "Firmware 2.32") |
| SHA-256 | e1a1a11a84d9afd2d2ceed61be8395e32a8034785abd0be0236478f7d8609f43 |
| Size | 0xF2E00 bytes |
| Model string | "JST51" |
| Segment count word (0x7C) | 0xFFFFFFFF |
| Segment table | one entry: 0x0800C000, 0xF2C00 |
| Payload | 0x100 + 0xF2C00, then 0x100 trailing bytes |
rfoundry/stockfw.parse_container reads this single-segment form (count 0xFFFFFFFF = one segment in the first
table entry).
14.2 Encryption and key
The payload is XOR-encrypted with a 1024-byte key held in the bootloader (unverified). The key's SHA-256 is
5254139c6b596d9413d106e6b7c0685ec245d69555cb28df98f061d6b879c4cd and its first bytes 2E DF 40 B5 (tools/stm32_dfu.py md380 model). It can be derived from the D2.32 firmware file and checked by that hash: the plain column vote is wrong in a third of the columns, and a byte-pair refinement learned from the file
fixes them (codec-md380.md, "Key recovery"; rfoundry/stockfw.derive_key). A key file still works
(--key).
14.3 Bootloader and DFU
Entered by holding PTT and the upper side key while switching on (unverified). USB DFU 0483:DF11 with TYT's
non-standard commands; the same erase-by-sector and block-write protocol as the DM-1701's loader (unverified). Flash a RadioFoundry
image with tools/stm32_dfu.py --model md380 --key KEY rfoundry_md380.bin. The image is plain, starts at 0x0800C000, and the
tool encrypts it for the loader. rfoundry knows the board (boards/md380/board.toml: release stable, an update writes
sectors 3-7 and keeps the codec).
14.4 The codec
The AMBE+2 codec is inside the manufacturer's application: the same library as the
DM-1701's, at other addresses. In the D002.032 file: code window 0x08050C44+0x10884, table window 0x0808FC64+0xAFF8 and
four small table windows in sectors 10 and 11. The merge moves the code to 0x080E0000, keeps the table windows at
their original addresses and writes a descriptor at 0x080FFF80; the codec runs in place from there. Entry points,
calling convention, state sizes, the placement and the extraction recipe are in codec-md380.md.
stockfw.PROFILES has the profile, the codec map (rfoundry/codec_maps/md380/md380-d2032.json) the rewrites,
boards/md380/memory.ld keeps the application out of sectors 8-11, vocoder_md380.c checks and hosts the codec, and
make BOARD=md380 merges it when the firmware file is in codecs/. The debug link reports the codec as
md380-d2032-s11; a build from before the move reports md380-d2032. Without the codec merged the radio is FM only.
15. USB
OTG_FS device on PA11 / PA12 (unverified). RadioFoundry enumerates with the STM32F4 radios' IDs (platform/stm32f4/platform.h) and the product
name "MD-380" (usb_md380.c); the bootloader's DFU device is separate (14.3).
The open-firmware CPS shim reports radio type 7 and serves the MCU ROM from 0x08000000, 1 MB (boards/md380/board.c,
cps-protocol.md 2.2).
16. Variants
| Radio | Difference |
|---|---|
| MD-380 | the base board |
| MD-390 | the same electronics in an IP67 case |
| Retevis RT3 | a rebadged MD-380 |
| MD-380G / MD-390G / RT3 GPS | GPS module on USART3, power on PD8 (section 13) |
| UHF / VHF | separate boards: different RF parts and VCOs, one RadioFoundry image each |
| 1 MB flash | some early VHF radios; RadioFoundry stores nothing on them |
(unverified) for all rows.
17. Unknowns that need a radio
Each with how to settle it; the stage numbers are md380-bringup.md's.
| # | Unknown | How to settle it on a radio |
|---|---|---|
| 1 | HSE crystal or external clock | Stage 1: the radio boots on HSE (USB enumerates); if only bypass works, record it |
| 2 | Display controller, orientation, colors | Stage 2: try MD380_PANEL_TYPE / MD380_PANEL_MADCTL_XOR until the boot screen is right side up and in color |
| 3 | Key matrix positions, side keys on K3 | Stage 3: press each key, read "keys" on the diagnostics page, correct section 9.1 and input_md380.c |
| 4 | Channel switch code polarity and order | Settled 2026-10-16: a Gray code, table in 9.2 |
| 5 | Volume knob on PA0 vs the TX LED | Stage 3: "vol" on the diagnostics page follows the knob, and the red LED lights on PE1 |
| 6 | Battery divider | Stage 3: compare "bat raw" against a meter; set MD380_BATT_COUNTS_X10 |
| 7 | Power switch wiring (direct or sensed) | Stage 3: switch off with the knob: does the radio die at once? |
| 8 | Flash size and JEDEC ID | Stage 3: "flash" line |
| 9 | Calibration layout and scaling | Stage 4: "cal" must say factory; read both security registers over the open-firmware CPS protocol (area 10, which md380_cps_security_read serves) and compare the rows with what the manufacturer's CPS shows |
| 10 | Frequency plan (TCXO, IF, LO side, VCO multiplier per band) | Stage 4: "pll lock" on every channel; an SDR on a 25 kHz-spaced pair of channels shows the receive LO leak and TX carrier |
| 11 | SKY72310 bus timing and lock detect polarity | Stage 4: "pll lock" with a correct carrier; a scope on PD10 while retuning |
| 12 | W/N_SW and DMR_SW polarities (FM_SW, FM_MUTE, SPK_C and AFCO measured, 6.1) | Stage 4/5: receive a known FM signal; invert one pin's level at a time in radio_md380.c (one rebuild each, batched) until IF, audio and bandwidth are right |
| 13 | RSSI and noise curves | Stage 5: signal generator steps, record "rssi" and "busy", fit status() |
| 14 | HR_C5000 configuration port timing, page mapping, reg 0x10 read-back | Stage 4: "C5000 10=" shows a sane value after init |
| 15 | FM TX deviation and modulation balance on the C5000 | Stage 6: TX into a dummy load and an SDR: deviation and frequency against a reference |
| 16 | APC scaling (table x 16) and the two power levels | Stage 6: power meter on 1 W and 5 W at band edges and center |
| 17 | I2S data line directions and format | Stage 7: DMR receive with the codec merged (after 14.4); a silent or noisy speaker means a direction or channel problem |
| 18 | RF TX interrupt lead time and the 30 ms PA hold | Stage 8: scope the PA supply (PC5) against the burst on an SDR |
| 19 | VHF I / Q swap (0x01 = 0xF0) | Stage 8 on a VHF radio: DMR receive and transmit against a known radio |
| 20 | GPS UART pin, baud rate, power pin polarity | Stage 9: on a G model "gps yes" with counts rising; on a non-G model "gps no" and no flash errors |
| 21 | Codec windows in D002.032 | Located and verified on a host (14.4); stage 7 checks voice on the radio |
| 22 | Whether the bootloader key can be derived from a firmware file | Done (14.2): derived from D2.32 and checked by its hash |
18. Datasheets
Skyworks SKY72310-11 data sheet 203639L (https://www.skyworksinc.com/Products/PLLs-Synthesizers-VCOs/SKY72310-11), STMicroelectronics STM32F405xx data sheet and RM0090, Hangzhou Hongrui HR_C5000 user manual (Chinese: pins, SPI ports, configuration registers, audio routing), and gotchas.md 2.6, 3.3, 3.4, 5.2, 11.1, 13.7.
19. Sizes
Flash: the table in feature-matrix.md and the per-feature figures in feature-matrix.md. Measured 2026-10-19 with LTO: default UHF image 320,236 B of 475,136 B (464 KB), 107,120 B of 131,068 B SRAM, 59,640 B of 64 KB CCM; maximal 327,064 B flash.
20. Facts moved from the shared specs
- Settings and storage: on a 16 MB part the core's map 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). There is no calibration window.
- Roaming reads the IF chain level on an ADC as the site signal (dmr-roaming.md 3).
- 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_safepins: APC_EN and TX_5V (5TC) low, VCO_SEL high (receive VCO), APC DAC (DAC1 channel 1) 0.- The G models use USART3 RX on PD9, so there is no UART for the fault trace. Store and restart cause of the crash log are unverified on the radio.
- The volume knob's switch only breaks the battery sense line: raw ADC reading 3345 at 8.3 V, 3 with the knob off.
- The DCS turn-off tail is the generator's 134.4 Hz sine (
subaudio_tx_tail), not yet measured. There is no FM TX tone path yet, so AFSK TX is not written (boards/md380/config.mk). Without a calibration the board refuses to transmit. - Release:
[status] release = "stable", both band images (rfoundry-md380-<version>.binUHF,rfoundry-md380-vhf-<version>.bin). Application sectors 3-7 (464 KB), codec sectors 8, 10, 11, sector 9 unused, so an update keeps the codec. Profilemd380-d2032-s11(a build from before the move reportsmd380-d2032).tools/md380_migrate.pyflashes it and keeps the codeplug.