Questa pagina non è ancora tradotta; è mostrata in inglese.
TYT MD-9600 / Retevis RT90 hardware (mobile)
The TYT MD-9600 and the Retevis RT90 are one dual-band mobile radio: an STM32F405VGT6 MCU, an HR_C6000 DMR baseband and codec chip, two complete RF chains (one VHF, one UHF), each with its own synthesizer, its own RX and TX VCOs, its own IF receiver chip and its own PA supply switch, a 49.95 MHz first IF, a 128 x 64 monochrome LCD (ST7567) on SPI, a 4 x 3 front panel key matrix, a quadrature channel encoder, a 16-key DTMF hand microphone on an RJ-45 jack read as two analog levels, a GPS option, and USB full speed on the MCU's own OTG_FS core (mini-USB on the rear). There is no AT1846S and no RF transceiver chip at all; the HR_C6000 sees a 450 kHz IF from the IF receiver chip.
TYT changed the synthesizer and IF parts during production. The board revision decides how the synthesizers (and on V5 the IF) are programmed, and nothing in the firmware can read the revision, so a build is made per revision (section 16).
Status (2026-10-20): nothing in this file has been checked on a radio. No MD-9600 is in hand, and every value is unverified until one is. RadioFoundry builds for it and drives FM receive on every board revision (section 19). Section 17 lists every open question.
Conventions: register values are hex; HR_C6000 registers are written page:reg = value as in
modem-hrc6000.md. Frequencies are given in Hz or MHz. A value marked (datasheet) is from the
STM32F405 data sheet and RM0090 or the Skyworks SKY72310-11 data sheet; (unverified) is not confirmed on a radio or
in a datasheet; ? means not found or contradictory.
1. MCU, clocks, memory
1.1 Part
STM32F405VGT6, LQFP100, 1 MB flash, 128 KB SRAM + 64 KB CCM, 4 KB backup SRAM. The backup SRAM is used (section 10.2). Read-out protection level 1 assumed, as on every TYT STM32 radio (?).
1.2 Clocks
| Item | Value | Note |
|---|---|---|
| HSE | 8 MHz, a crystal oscillator (not bypass mode) | V1-V3: starts in crystal mode (17 #18) |
| LSE | 32.768 kHz crystal, RTC enabled | |
| SYSCLK, USB | 72 MHz and 48 MHz | unverified |
| AHB / APB1 / APB2 | 72 MHz / 36 MHz (/2) / 72 MHz (/1): the DM-1701's bus clocks | unverified |
| RadioFoundry's plan | HSE 8 MHz crystal, PLL M 4, N 72, P 2, Q 3 (2 MHz PLL input, VCO 144 MHz), AHB /1, APB1 /2, APB2 /1, flash 3 wait states. If the crystal doesn't start, RadioFoundry tries bypass mode, then stays on HSI 16 MHz (no USB) | ours |
RadioFoundry derives every time constant from the clock it set up (SystemCoreClock), so a wrong HSE guess shows as a wrong USB clock and a wrong tick, not as a silent offset.
Standby (section 10.2) drops SYSCLK to HSE / 512 with both APBs / 16 and voltage scale 2 while waiting for the power key.
1.3 Internal flash layout
Same TYT loader and layout as the DM-1701 and MD-380: loader in sectors 0-2 (0x08000000-0x0800BFFF, never written), application from 0x0800C000, VTOR 0xC000. The codec sits at a different address in every vendor version and variant (0x08074B18 to 0x08079630 for the code; 0x0807537C in V26.45; codec-md9600.md).
1.4 RAM
RadioFoundry maps CCM (as on the DM-1701). Backup SRAM: 4 KB at BKPSRAM_BASE, kept while DC is applied, used for the standby flag (10.2) and for non-permanent settings.
2. Pin assignments
2.1 Port A
| Pin | Function | Notes |
|---|---|---|
| PA0 | ADC1 IN0: VHF RSSI | |
| PA1 | ADC1 IN1: supply voltage | 10.3 |
| PA2 | ADC123 IN2: QT_DQT_IN, sub-audio tone input, sampled by ADC3 | ADC3 (unverified) |
| PA3 | ADC1 IN3: VOX / mic level | the only mic level input (no separate "mic" signal). A level tap, not the audio: 3-4 counts with the mic quiet, unmoved by a beep in the speaker (2026-11-03). VOX reads it (power_md9600.c) |
| PA4 | DAC1: TV_U, UHF receiver front-end tuning | |
| PA5 | DAC2: APC_TV_V, shared TX power (APC) reference and VHF receiver front-end tuning | |
| PA6 | ADC1 IN6: hand-mic keypad, row level ("P2") | 9.3 |
| PA7 | ADC1 IN7: hand-mic keypad, column level ("P1") | 9.3 |
| PA8 | C5_U_SW, UHF 5 V supply (receiver side) | high = on |
| PA9 | USART1 TX and GPS + microphone power switch | driven as GPIO high = mic and GPS powered (13.2) |
| PA10 | USART1 RX: GPS NMEA in, 9600 baud | |
| PA11 / PA12 | USB OTG_FS D- / D+ | |
| PA13 | R5_U_SW, UHF receiver 5 V; also SWDIO | |
| PA14 | R5_V_SW, VHF receiver 5 V; also SWCLK | |
| PA15 | I2S3 WS (AF6) |
2.2 Port B
| Pin | Function | Notes |
|---|---|---|
| PB0 | ADC1 IN8: UHF RSSI | |
| PB1 | ADC1 IN9: UHF noise (squelch) level | |
| PB2 | FM_SW, FM audio stages on (high) | |
| PB3 / PB4 / PB5 | SPI1 SCK / MISO / MOSI (AF5): HR_C6000 voice port | |
| PB6 | AF_SW, audio amplifier mute, a plain GPIO output: high = muted | unverified; no timer on this pin |
| PB7 | U_V_MOD_SW: HR_C6000 MOD2 output to the VHF (high) or UHF (low) VCO | |
| PB8 / PB9 | I2C1 SCL / SDA (AF4, open drain) | one device at 7-bit address 0x3D, not identified (?) |
| PB10 / PB11 | Channel encoder A (EXTI, both edges) / B; USART3 TX / RX when the optional remote head is fitted | 9.2, 9.5 |
| PB12 | SPI flash chip select | |
| PB13 / PB14 / PB15 | SPI2 SCK / MISO / MOSI (AF5): SPI flash and LCD | mode 3, 18 MHz (12.1, unverified) |
2.3 Port C
| Pin | Function | Notes |
|---|---|---|
| PC0 / PC1 / PC2 | HR_C6000 timeslot / system / RF TX interrupts (EXTI0-2, falling edge) | see 4.4 for the RF TX line |
| PC3 | ADC2 IN13: RX_2T_5T (discriminator audio), sampled with DMA for sub-audio tone detection | unverified |
| PC4 | RF_APC_SW, PA power loop on | |
| PC5 | ADC1 IN15: VHF noise level | |
| PC6 | Display_Light: LCD and key backlight, TIM8 CH1 PWM (AF3) | unverified, 7.3 |
| PC7 | CTC_DCS_PWM, sub-audio tone output, TIM8 CH2 PWM (AF3) | checked |
| PC8 | BEEP_PWM, TIM8 CH3 PWM (AF3) | unverified |
| PC9 | C5_V_SW, VHF 5 V supply (receiver side) | |
| PC10 / PC11 / PC12 | I2S3 CK (AF6) / I2S3ext SD (AF5) / I2S3 SD (AF6) | |
| PC13 | U_V_AF_SW: receive audio from the VHF IF chip (high) or UHF (low) | |
| PC14 / PC15 | LSE crystal |
2.4 Port D
| Pin | Function | Notes |
|---|---|---|
| PD0 / PD1 | Key matrix rows K2 / K3 (open drain) | |
| PD2 / PD3 / PD4 | Key matrix columns K4 / K5 / K6 (inputs, pull-up) | PD2 low at boot also means "remote head fitted" (9.5) |
| PD5 / PD6 | T5_V_SW / T5_U_SW: VHF / UHF transmitter 5 V | |
| PD7 | HR_C6000 voice port chip select | |
| PD8 / PD9 | PLL_CS_V / PLL_CS_U: VHF / UHF synthesizer chip selects | |
| PD10 / PD11 | PLL_DATA / PLL_CLK, shared by both synthesizers | |
| PD12 / PD13 / PD14 | LCD reset / RS (data-command) / chip select | |
| PD15 | Power_Control: main power to the RF and audio sections (high = on) | 10.1 |
2.5 Port E
| Pin | Function | Notes |
|---|---|---|
| PE0 / PE1 | Key matrix rows K0 / K1 (open drain) | |
| PE2 / PE3 / PE4 / PE5 | HR_C6000 configuration port CS / CLK / MISO / MOSI (bit-banged) | MISO and MOSI are the other way round from the DM-1701 (unverified) |
| PE6 | C6000_PWD, HR_C6000 power down | |
| PE7 | FM_MUTE: FM audio to the volume control, high = unmuted | |
| PE8 / PE9 | VCO_VCC_U_SW / VCO_VCC_V_SW: per band, high = RX VCO, low = TX VCO | |
| PE10 | PTT (hand mic), input with pull-up, active low | |
| PE11 / PE12 | V1-V4: IF_CS_V / IF_CS_U (IF chip selects). V5: VHF / UHF IF bandwidth, high = 20 or 25 kHz | 5.4, as V26.45 drives them |
| PE13 / PE14 | V1-V4: IF_DATA / IF_CLK. V5: connect the VHF / UHF IF to the HR_C6000 (high = connected) | 5.4, as V26.45 drives them |
| PE15 | V1-V4: IF_RST (IF chip reset, active low) | V4A / V5: unused, V26.45 leaves it an input |
2.6 Reset-time levels and SWD
Reset levels: HR_C6000 CS/CLK/MOSI and power-down low, IF chip selects/data/clock high, IF reset
low, both synthesizer chip selects and the voice port CS high, synthesizer data/clock low, LCD lines low, Power_Control
low, all T5/R5/C5 supply switches low, U_V_MOD_SW low, FM_SW and FM_MUTE low, CTC_DCS_PWM high. Then
radioPowerOn sets both T5 low, all C5 and R5 high (both receivers are always powered together, "as the TYT firmware
does"), then Power_Control high.
RadioFoundry's transmitter-off levels (board_rf_safe, on any fault, crash or reset; crash-log.md): APC_EN,
T5_V and T5_U low, VCO_U and VCO_V high (the receive VCOs), the PA reference DAC (DAC1 channel 2) at 0.
PA13 / PA14 are the receiver supplies of both bands: like the MD-380, the radio takes the SWD pins at run time, and a debugger has to connect under reset (D for the pins; the consequence ours).
3. Interrupts, DMA, timers
| Source | Use | Note |
|---|---|---|
| EXTI0/1/2 (PC0-PC2) | HR_C6000 timeslot / system / RF TX, priority 5 | |
| EXTI15_10 (PB10) | Channel encoder A, both edges, priority 5 | |
| DMA1 Stream0 ch0 / Stream5 ch2 | I2S3 RX / I2S3ext TX, circular | as the DM-1701 |
| DMA2 Stream2 ch4 | USART1 RX (GPS), circular | |
| DMA1 Stream1 / Stream3 ch4 | USART3 RX / TX (remote head only) | |
| TIM8 CH1 / CH2 / CH3 | PWM on PC6 (backlight), PC7 (sub-audio tone), PC8 (beep): prescaler 9, period 256, about 28 kHz at 72 MHz. CH4's pin (PC9) is a GPIO | unverified |
| TIM6 | DAC2 trigger at 10 kHz for the PA power ramp | unverified |
| ADC1 | 8-channel scan with DMA (10.3) | unverified |
| ADC2 | IN13 (PC3, discriminator audio), circular DMA, for sub-audio tone detection | unverified |
| ADC3 | IN2 (PA2, QT_DQT_IN) | unverified |
4. HR_C6000
4.1 Hookup
Configuration port bit-banged on PE2-PE5, voice port on SPI1 (PB3-PB5, CS PD7, CPOL high, CPHA first edge, PCLK2 / 64 = about 560 kHz), I2S3 slave full duplex Philips 16-bit (extended) 8 kHz, power down PE6, interrupts PC0-PC2: the DM-1701's wiring except the voice port, which is on SPI1 here instead of the DM-1701's SPI. The register behavior is the DM-1701's (modem-hrc6000.md).
4.2 Differences from the DM-1701 set-up
| Item | MD-9600 |
|---|---|
| Clock / PLL | 4:0x0A=0x80, 0x0B=0x28, 0x0C=0x33, 0x0A=0x00, 0xB9=0x05, 0xBA=0x04, 0xBB=0x02: the same values RadioFoundry already uses |
| IF | 450 kHz (4:0x07-0x09 = 0x0B 0xB8 0x00), from the IF receiver chip into ADC_IVINP (pin 70). 4:0x01 bit 6 (receive I/Q swap) follows the first LO's side (5.2): clear on UHF (LO below, 0xB0 in DMR), set on VHF (LO above, 0xF0), as the radio's own firmware does. The wrong value inverts the spectrum: about one voice burst in six syncs and no call is tracked (first radio, 2026-11-02, both bands) |
| FM receive audio | not through the HR_C6000: 4:0x36 = 0x00 always. FM audio comes from the IF chip's demodulator via U_V_AF_SW, FM_SW, FM_MUTE (6.1). The chip stays in DMR mode in FM receive (4:0x10 bit 7 clear), as the radio's own firmware leaves it: in FM mode its line out plays only its own FM audio, so beeps were silent on FM channels (first radio, 2026-11-03) |
| FM TX, CTCSS, DCS, DTMF, tones | through the HR_C6000 (TX tone generator, RX CTCSS / DCS detector); RadioFoundry sends CTCSS and DCS as PWM on PC7 instead (19.9) |
| MOD2 routing | U_V_MOD_SW (PB7) sends the MOD2 output to the active band's VCO; set with every synthesizer write |
| Reference / MOD2 offset | per band byte t from calibration (11.2); signed c = t - 128; 4:0x47 = c, 4:0x48 = 0x03 if c < 0 else 0x00, 4:0x04 = c. Rewritten at every RX entry (otherwise an RX offset is reported after TX) |
| DMR sync detection | 4:0x5F = 0xF0 (MS, BS, TDMA1, TDMA2) |
| Tone mode | 4:0xA1 = 0x00 after the power-on init (bit 7 is reserved, must be 0). With the init's 0x80 the chip never syncs on DMR, though its RSSI (4:0x43 / 0x44) follows the signal (first radio, 2026-11-02) |
4.3 Voice port and I2S
As the DM-1701 (4.3 / 4.4 of hardware-dm1701.md) apart from the SPI instance. DMR receive AGC maps a peak level to the HR_C6000 DAC gain; not a hardware fact.
4.4 Burst timing
The MD-9600 is reported to lack the hardware for the receive-side interrupt the other STM32 radios use to end a TX burst (unverified); the burst can end from the timeslot interrupt once the PA has been on 20 ms. Whether the RF TX interrupt (PC2) works as on the DM-1701 is not stated (?). gotchas.md 14.6: every TX/RX switch means a new synthesizer word and a different VCO (5.2), so the per-burst relock time is the main DMR timing risk.
5. RF
5.1 Bands, power
Dual band, 136-174 MHz and 400-480 MHz (unverified); 127-178 and 380-564 MHz are reported outside the data-sheet mode (unverified; not a recommendation). Rated 50 W VHF / 45 W UHF (40 W on some retailer pages); one review measured 44 W at 146 MHz and 40 W at 446 MHz (unverified). Band selection is by frequency: below 300 MHz is VHF.
5.2 Frequency plan (all revisions)
| Item | Value |
|---|---|
| Reference | 16.8 MHz TCXO, shared |
| First IF | 49.95 MHz |
| VHF RX | LO = f + 49.95 MHz (high side) |
| UHF RX | LO = f - 49.95 MHz (low side) |
| TX | VCO on the carrier (UHF), x2 on V1/V2 VHF (5.3) |
| RX vs TX VCO | separate VCOs per band, picked by VCO_VCC_x_SW (high = RX) |
One synthesizer per band drives both of that band's VCOs, so RX and TX (49.95 MHz apart) always mean reprogramming.
5.3 Synthesizer per revision
All write-only, chip select low during the word, data MSB first, data set while the clock is low and latched on the rising edge. Fractional rule in V1/V2/V4: if the fraction is above one half, N is rounded up and the fraction used as a negative offset (signed modulator).
V1, V2 (and V3): Skyworks SKY72310, one per band. 16-bit words, 4-bit address in the top bits (as hardware-md380.md 5.2, data sheet):
| Band | R (reg 5 = R - 1) | Comparison frequency | VCO |
|---|---|---|---|
| VHF | 6 | 2.8 MHz | 2 x LO, the VHF output divides by 2 |
| UHF | 4 | 4.2 MHz | LO |
N = VCO / f_pd; reg 0 = N - 32; 18-bit fraction (x 2^18), reg 1 = fraction >> 8, reg 2 = low 8 bits. Fixed: reg 6 = 0x01F, reg 7 = 0x3D0, reg 8 = 0, reg 9 = 0. Write order: 8, 9, 5, 0, 2, 1, 6, 7 ("the order the TYT firmware uses") .
V1-V3 reference divider: R 6 (VHF) and 4 (UHF) is one case. The divider also depends on a per-channel setting and on which side of the signal the LO sits, giving R 5 / 4 / 6 or 3 / 2 / 4. What the setting means is not known (unverified); RadioFoundry uses 6 and 4.
V4: "PLL1590" synthesizer, one per band. TYT's own folder for the V4 firmware (V16.45) is named "firmware for PLL1590", which confirms the part family earlier notes call AK1590; the exact part number is still unconfirmed (?). The V16.45 stock images (all four variants) hold the 24-bit words 0x1FF404 and 0x19F404 as literals, that is reg 4 = 0x1FF40 and 0x19F40 in the format below; the V06.09 images hold neither (from our own reading of the stock images). 24-bit words: 20 data bits then the 4-bit address in the low nibble. N in reg 2 (VHF 0x20000 | N, UHF 0x38000 | N), the 18-bit fraction in reg 1, reg 3 = 0x6000 | R, reg 4 = 0x1FF40 for VHF and for UHF receive, 0x19F40 only for UHF transmit (unverified), reg 5 = reg 6 = 0. R: VHF 12, UHF RX 8, UHF TX 4. No x2 on VHF here (VCO on the LO). Write order 5, 6, 3, 4, 1, 2. The V4 VHF synthesizer must be programmed (145 MHz RX) right after power-on or it never locks.
V5 (and V4A): TI LMX2571, one per band, as the original V26.45 firmware programs it (read from its image, not yet checked on a radio). 24-bit words: 8-bit address then 16 data bits. R: VHF 8, UHF 4; 24-bit fraction, the remainder of VCO / f_pd times 2^24, truncated; reg 0x04 = 0x3000 + N, reg 0x01 = fraction >> 16, reg 0x02 = fraction low 16, reg 0x03 = 0, reg 0x05 = 0x100 + R, reg 0x06 = 0x8581, reg 0x07 = 0x1024, reg 0x08 = 0x0430, reg 0x29 = 0x0230, reg 0x2A = 0x0108, reg 0x00 = 0x003E. Sequence on every retune: 0x00, 0x04, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07, 0x08, 0x29, 0x2A, then 0x00 = 0x003F (start calibration) and 0x00 = 0x003E. The clock is left low after each word. The reset (0x00 = 0x2000) goes to each chip once at power-up, VHF then UHF, followed by a wait of 10 system ticks (nominally 10 ms), and never on a retune. The words are the same for RX and TX; only the frequency and R change.
The R above is for the default of a 2-bit per-channel setting (probably the CPS reference frequency choice; inferred). Its other values give R 5 or 4 (VHF) and 3 or 2 (UHF); RadioFoundry uses only the default. The LO side follows the band's upper edge from calibration (11.2): at or below 200 MHz high side, else low side, so VHF is high side and UHF low side as in 5.2.
From the LMX2571 data sheet [TI]: the word is a R/W bit (0 = write), a 7-bit address and 16 data bits, clocked in on the rising edge and latched when LE rises. In these values R4 is FRAC_ORDER 3 (third-order modulator) and PLL_N (12 bits, at most 1023), R1 holds PLL_DEN[23:16] (0) and PLL_NUM[23:16], R2 PLL_NUM[15:0], R3 PLL_DEN[15:0] (0), R5 PLL_R 1 and the pre-divider R, R6 the multiplier bypassed, R8 EXTVCO_SEL (PLL mode with an external VCO) and FSK_EN. The data sheet asks for DEN = 0 when FSK_EN is set; the fraction is then over 2^24, as V26.45 computes it. R7 turns both of the chip's RF output buffers off (the external VCO is the output). R42's charge pump down current is not equal to R41's up current, though the data sheet asks for equal; that is V26.45's value. The data sheet's power-on sequence also writes R60, R58, R53 and the rest; V26.45 writes no register outside the set above, and neither does RadioFoundry. A reset restores the chip's defaults (its own VCO, the TX output buffer on, the external VCO's charge pump off) until R7 and R8 come again, which is why neither resets on a retune.
No lock detect input is known for any revision (?). V26.45 reads no synthesizer pin and never enables the LMX2571's lock detect output.
5.4 IF receiver per revision
V1-V4: AKM AK2365A, one per band. Bit-banged 16-bit words on IF_DATA (PE13) / IF_CLK (PE14) with chip selects PE11 (VHF) / PE12 (UHF), reset PE15 (pulse low for at least 1 us). Data set while the clock is low, latched on the rising edge. Written each time the receiver is turned on: soft reset 0x08AA, then 0x02F1 (25 kHz) or 0x02E9 (12.5 kHz), 0x041D (AGC), 0x0600 (IF gain 6 dB), 0x1601, 0x1880 (auto AGC). The chip's 450 kHz output feeds the HR_C6000, its demodulated audio the FM path (6.1).
From the AK2365 data sheet [AKM] (the A part taken to be the same, unverified): a frame is 16 clocks, MSB first, latched on the rising edge of SCLK while CSN is low: R/W (0 = write), a 6-bit address, one low bit, 8 data bits. That is the word reg << 9 | data above. SCLK high and low are at least 500 ns. Register 1 = 0xF1 is mode 7 (everything on), the wide demodulator level, the +-6 kHz filter and the 50.4 MHz second LO; 0xE9 the narrow level and the +-3 kHz filter. Register 2 = 0x1D starts the discriminator calibration (1.3 ms). Register 4 = 0xAA is the software reset. After power-up the chip needs a hardware reset (RSTN low at least 1 us).
V5: no programmable IF chip. As V26.45 drives the lines: PE11 / PE12 set the VHF / UHF IF filter, high for a 20 or 25 kHz channel and low for 12.5 kHz (only the receiving band's line is written). PE13 / PE14 connect the VHF / UHF IF to the HR_C6000, high = connected: the receiving band's high and the other low, both high in dual watch, both low before a transmit retune and back on last in the return to receive. PE15 is not used. The V5 IF part is not identified (?); the firmware doesn't name it.
5.5 Receive and transmit switching
RX: Power_Control on, both R5 and both C5 on, T5 off, RF_APC_SW off; synthesizer to the LO with VCO_VCC high for the band; DAC1 (UHF) or DAC2 (VHF) = the calibrated front-end tuning voltage; IF set up for the band and bandwidth; U_V_AF_SW to the band.
TX: both R5 off, synthesizer to the carrier with VCO_VCC low, DAC2 = APC drive (for both bands: PA5 serves the APC in TX), then the band's T5 on and the other off, then RF_APC_SW on. On V4 and V4A / V5 the other band's C5 supply goes off while one band transmits (V4A / V5: as V26.45 does it); V1-V3 leave both on (unverified). Per DMR burst only T5 and RF_APC_SW toggle, with no retune; return to RX drops RF_APC_SW, both T5, then reprograms for RX. There is no lock check.
V26.45 (V4A / V5) timing: the transmit retune drops RF_APC_SW, both R5 and both IF routes, sets the supplies and VCO lines, writes the words, then waits 12 system ticks (nominally 12 ms) before T5 and RF_APC_SW. The return to receive writes the words, turns R5 on, waits 4 ticks, then connects the IF routes. The ordering beyond that is the reference port's.
5.6 Power levels and control
DAC2 (12 bit) is the APC reference: the PA drive, in DAC counts. The factory calibration (11.2) holds four PA rows per frequency point, labeled 5 W, 10 W, 25 W and 40 W. The labels are wrong: the rows put out about 10, 14-21, 24-28 and 40-50 W. Measured on one radio with a wattmeter into a dummy load (FM carrier, procedure in power-calibration.md):
| UHF 446.100 | UHF 440.000 | VHF 151.5125 | VHF 146.535 | |
|---|---|---|---|---|
| Factory rows (drive → W) | 816 → 9.6, 928 → 13.9, 1232 → 24.8, 1760 → 39.6 | 800 → 8.7, 896 → 12.3, 1216 → 23.6, 1776 → 39.2 | 992 → 10.1, 1072 → 20.7, 1136 → 28.5, 1760 → 50.2 | 1008 → 10.1, 1056 → 15.8, 1136 → 24.7, 1680 → 48.4 |
| Zero point (highest drive under 0.02 W) | 440 | 450 | 870 | 870 |
| Cliff (highest drive at about 10 W) | 410 | 420 | 860 | 850 |
| Hot (after 3 × 5 s at 25 W): zero point, cliff | 445, 415 | 870, 860 | ||
| Low curve (drive → W) | 450 → 0.07, 504 → 0.96, 677 → 4.99 | 450 → 0.01, 500 → 0.76, 680 → 4.88 | 880 → 0.22, 890 → 0.88, 950 → 5.65 | 880 → 0.36, 890 → 0.86, 950 → 4.85 |
| Top (drive → W) | 2100 → 45.4, 2250 → 47.1 | 2100 → 44.5, 2200 → 46.1 | 1500 → 46.7 | 1760 → 49.8, 1800 → 50.4 |
- The cliff. Below the zero point by 5-20 counts the power loop loses control and the PA puts out about 10 W. A drive of 0 is below it: the ramp starts and ends at the band's zero point (UHF 440, VHF 870), and the drive goes to 0 only with the loop open. Ramping from 0 gave a carrier at key-up and a splash at key-down.
- The low end follows the absolute drive: the zero point moved 10 counts between the two UHF frequencies and not at all on VHF. The low levels are fixed drives per band. At the very bottom that spread is 2-3 dB: drive 457 gave 0.18 W at 446.100 and 0.10 W at 440.000, so 50 mW can't hold ±2 dB across the band.
- The upper levels move with frequency and follow the factory rows: 25 W is interpolated in watts between the rows around it, the top level is the top row plus a fixed step.
- Lowest level. The margin above the cliff is 20 counts (hot shift 5). UHF: 100 mW (456), the lowest name that fits ±2 dB at both frequencies. VHF: the curve is steeper, and 250 mW (880) is the lowest level that keeps the margin.
| Level | UHF drive | UHF name | VHF drive | VHF name |
|---|---|---|---|---|
| 0 | 456 | 100 mW | 880 | 250 mW |
| 1 | 509 | 1 W | 892 | 1 W |
| 2 | 680 | 5 W | 947 | 5 W |
| 3 | rows 2-3 (UHF) or 1-2 (VHF) at 25 W | 25 W | as UHF | 25 W |
| 4 | top row + 337 | 45 W | top row + 45 | 50 W |
The short check with this table (section 4 of the procedure, FM carrier, watts):
| Level | 446.100 | 440.000 | 151.5125 |
|---|---|---|---|
| 0 | 0.15 (100 mW) | 0.08 (100 mW) | 0.18 (250 mW) |
| 1 W | 1.06 | 0.93 | |
| 5 W | 5.07 | 5.29 | |
| 25 W | 26.1 | 27.0 | |
| Top | 45.9 (45 W) | 50.7 (50 W) | |
| Dark step (cliff + 5) | 0.00 (437) | 0.00 (865) |
On VHF the low levels' output rises toward the bottom of the band at a fixed drive: level 0 (880) read 0.37 W at 144.010, 0.25 W at 146.000 and 147.990, and 0.14 W at 151.5125 (−2.5 dB: outside ±2 dB). Drive 882 read 0.45 W at 144.100 and 0.20 W at 151.5125. One fixed drive can't hold 250 mW from 144 to 152 MHz, so the VHF low levels are calibrated at 146 MHz, the middle of 2 m, and hold ±2 dB across it. The HIL wattmeter check keys at 445 and 146 MHz for that reason.
A radio whose rows give levels that don't rise, or a top level over 2300 counts, transmits on no level. The debug
link's tx_drive view key shows each level's drive, and RADIO op 36 sets one transmission's drive
(debuglink.md). RadioFoundry refuses TX without an accepted table.
5.7 RSSI, noise, squelch
Analog per band: VHF RSSI PA0, UHF RSSI PB0, VHF noise PC5, UHF noise PB1. A fit, not a measurement (?): V1/V2/V4 -135 dBm (VHF) / -145 dBm (UHF) + raw8 / 2; V5 -155 dBm + raw8 / 1.95 (raw8 = the 12-bit average >> 4). V26.45 has no dBm conversion (it compares raw readings with the calibration's squelch rows), so the V5 fit doesn't come from it. Calibration also has RSSI points at -120 and -70 dBm and squelch open / close rows (11.2).
The noise line follows the modulation. Under a -73 dBm NOAA carrier (VHF, 25 kHz), it reads about 100 counts in a pause and
peaks at 900-1500 in speech for tens of ms; an empty channel holds 1700-1900 (first radio, 2026-11-03). The squelch
therefore takes the lowest reading over the last 160 ms (radio_md9600.c), cleared on every retune: one reading a
poll let speech close it. RSSI holds steady through the modulation and falls about 11 dB within one 25 ms poll when the
carrier ends (UHF, -127 dBm), so a fall of 8 dB or more since the last poll starts the window over and the squelch closes
on the next reading (md9600_noise_floor, rfseq_md9600.c). Without it, the noise kept the squelch open about 170 ms
after the carrier dropped.
The calibration's squelch rows suit the 12.5 kHz filter. Through the 25 kHz one the noise line reads about twice as high
under the same weak signal: a -89 dBm VHF carrier reads about 1350 instead of 710, while an empty channel moves only from
about 1750 to 1850 and a full-quieting one stays near 220. A 25 kHz FM channel therefore multiplies both thresholds by 3/2
(md9600_rf_sql_wide); without that, the carrier stayed shut at level 1. 7/4 opened level 1 on empty UHF channels. DMR is
always narrow and keeps them.
6. Audio
6.1 Paths
FM: IF chip discriminator -> U_V_AF_SW (band) -> FM_SW (stages on) -> FM_MUTE (high = to the volume control) -> analog volume knob -> amplifier. DMR and beeps: HR_C6000 line out -> volume control. The amplifier mute is AF_SW (PB6) driven as PWM from TIM4: 0 % = unmuted, 100 % = muted; a partial duty (30 %) can be used for click suppression. The volume knob is analog only (no ADC input): no software volume.
6.2 Microphone
Hand mic on the RJ-45 jack: mic audio to the HR_C6000 line input, PTT on PE10, keypad on PA6 / PA7 (9.3). Mic and GPS share one supply switched by PA9 high; it can stay on whenever the radio runs. VOX / mic level on PA3 only.
7. Display
7.1 Panel and bus
128 x 64 monochrome, ST7567, on SPI2 shared with the SPI flash: SCK PB13, MOSI PB15, CS PD14, RS PD13 (low = command, high = data), reset PD12. The flash and the LCD must not be accessed at the same time.
7.2 Init, orientation, contrast
Reset low 1 ms, then high, wait 5 ms. Commands: 0xE2 (reset), 0x2F (booster, regulator, follower on), 0x81 0x12 (electronic volume = contrast 18), 0xA2 (bias 1/9), 0xA1 (SEG reverse), 0xC0 (COM normal), 0xA4 (normal, not all pixels on) or 0xA7 (inverse), 0xAF (display on). Writes per page: 0xB0 | page, 0x10, 0x04 (column 4 offset), then 128 data bytes. Contrast 0x81 + value; the usable range is about 5-30. Sleep: 0xAE then 0xA5. 0xA6 (non-inverse video) is the reset state (?: RadioFoundry should send it).
7.3 Backlight
PC6, TIM8 CH1, period 256 (unverified). On V2 and later the LEDs are off below about 16 % duty and fully on by 19.3 %, so RadioFoundry maps 1-99 % into counts 41-49 of 256 and 100 % to always on. A bit in the hardware option block (11.1) marks radios whose backlight is only switched on and off (unverified). On V1 the duty is proportional; V1 has different backlight hardware,. The keys share the backlight (?).
8. LEDs
No MCU pin drives an LED (unverified). The front panel's TX / RX indicator may be driven by the HR_C6000 or by the RF hardware; unknown (?).
9. Keys, encoder, mic keypad, PTT
9.1 Front panel matrix
Rows K0 PE0, K1 PE1, K2 PD0, K3 PD1 (open drain, idle high, driven low one at a time); columns K6 PD4, K5 PD3, K4 PD2 (pull-up inputs, low = pressed):
| Row | K6 (PD4) | K5 (PD3) | K4 (PD2) |
|---|---|---|---|
| K0 (PE0) | Power | Alarm (orange) | P1 |
| K1 (PE1) | P2 | P3 | P4 |
| K2 (PD0) | Back (red) | Down | Enter (green) |
| K3 (PD1) | Up | - | - |
9.2 Channel encoder
Quadrature on PB10 (A, interrupt on both edges) and PB11 (B); one step per A edge, direction = (A != B). No pull-ups configured (?: external).
On a V1-V3 radio (2026-11-02): two steps per detent, resting where A and B differ (01 or 10), and B leads A turning clockwise. RadioFoundry samples (B, A) with the rest mask for 01 and 10: 10 clicks each way count 10, a wobble counts none. With the A = B mask, every run lost its first click.
9.3 Hand-mic keypad
20 positions as two analog levels: row = 4 - (PA6 raw / 800), column = PA7 raw / 1000 (12-bit raw); row < 5 and column < 4 means a key is down:
| Row | Col 0 | Col 1 | Col 2 | Col 3 |
|---|---|---|---|---|
| 0 | 1 | 2 | 3 | A |
| 1 | 4 | 5 | 6 | B |
| 2 | 7 | 8 | 9 | C |
| 3 | * | 0 | # | D |
| 4 | (unused) | A/B | Up | Down |
The mic has 16 keys plus its own up / down / A/B (unverified). Level windows in raw 12-bit counts, both levels at or above 3445 with no key down (unverified):
| Level | Window 0 | Window 1 | Window 2 | Window 3 | Window 4 |
|---|---|---|---|---|---|
| Row (PA6) | 2830-3444 | 2215-2705 | 1477-2090 | 862-1352 | 247-737 |
| Column (PA7) | 0-1106 | 1231-1844 | 1969-2705 | 2830-3320 | - |
9.4 PTT
Hand-mic PTT on PE10, pull-up, active low. No separate accessory PTT input is known (?).
9.5 Remote head (not a TYT part)
A community-built remote front panel exists: if PD2 (column K4) reads low at boot, PB10 / PB11 become USART3 at 250000 baud and keys, encoder, mic keys, PTT and GPS characters arrive as 3-byte messages, while display rows go out over the link (unverified). RadioFoundry doesn't support it and must not take a P1, P4 or Enter key held at boot for the panel (whether those keys pull PD2 low is unverified).
10. Power
10.1 Supply and main switch
13.8 V mobile supply. The MCU is powered whenever DC is applied; there is no latch the MCU releases to turn itself off. Power_Control (PD15) switches the RF and audio sections. No ignition-sense input is known (?).
10.2 "Off" is standby
Power key = matrix K0 x K6. The radio can be turned "off" by saving settings, writing a standby flag to backup SRAM word 0, and resetting; at boot the flag is found, the clock drops to HSE / 512, row K0 is driven low and K6 is awaited (power key), then the radio runs again after a release and a second reset. Settings that need not survive a supply cut also live in backup SRAM. Removing DC is the only true off. The flag values are RadioFoundry's choice.
10.3 Supply voltage ADC and scan
ADC1 scans eight channels with DMA, ranks IN9 UHF noise, IN15 VHF noise, IN8 UHF RSSI, IN0 VHF RSSI, IN3 mic level, IN1 supply, IN6 mic keypad row, IN7 mic keypad column (unverified). RadioFoundry keeps that order, 480 cycles each at PCLK2 / 8, in a circular DMA buffer. Supply: tenths of a volt = raw / 21.3 (D, plus a user trim); maximum 15.0 V; cutoff hysteresis 8.0 / 10.0 V.
11. Calibration
11.1 Location
512 bytes in the SPI flash security registers, read as one 512-byte security read from register offset 0 (the same mechanism as the MD-380, hardware-md380.md 11.1). A copy can also be kept at main-array address 0x010000, recognized by the first 8 bytes of the UHF frequency list (00 25 00 40 00 45 01 40, 400.025 / 400.145 MHz). RadioFoundry reads the security registers and never writes them. The vendor CPS backs the table up.
Registers 1 and 2 hold 256 bytes each (unverified). Register 3 holds a hardware option block: band edges at +0x14, +0x16, +0x18, +0x1A (16-bit, BCD MHz in the top bits: 0x1450 = 145.0 MHz) and bit 4 of +0x1D, set when the backlight is only switched on and off (unverified).
11.2 Layout (dual band: UHF 9 points, VHF 5 points)
| Offset | Content |
|---|---|
| 0x00 / 0x01 | VOX level 1 / 10 |
| 0x02 / 0x03 | RX low / high voltage (use unknown) |
| 0x04 / 0x05 | RSSI at -120 / -70 dBm |
| 0x09 | UHF reference (MOD2 offset) tune, offset by 128 |
| 0x0C | VHF reference tune, offset by 128 |
| 0x10 / 0x19 | High power (40 W) UHF x9 / VHF x5 |
| 0x20 / 0x29 | Low power (5 W) UHF / VHF |
| 0x30 / 0x39 | RX front-end tuning UHF / VHF |
| 0x40, 0x50, 0x60, 0x70 | UHF squelch level 9 open / close, level 1 open / close (x9) |
| 0x90 / 0x9B, 0xA0 / 0xAB, 0xB0 / 0xBB | CTCSS deviation 67 / 151.4 / 254.1 Hz, UHF x9 / VHF x5 |
| 0xD0 / 0xDB | DCS deviation UHF / VHF |
| 0xE0-0xF3 | VHF squelch 9 open, 9 close, 1 open, 1 close (x5 each) |
| 0x100-0x127 | VHF calibration frequencies, 10 x 4 bytes BCD (5 RX / TX pairs) |
| 0x130 / 0x139, 0x140 / 0x149 | DMR I / Q gain UHF / VHF |
| 0x170 / 0x179, 0x180 / 0x189 | FM I / Q gain UHF / VHF |
| 0x190 / 0x199 | Mid power (25 W) UHF / VHF |
| 0x1A0 / 0x1A9 | Mid-low power (10 W) UHF / VHF |
| 0x1B0-0x1F7 | UHF calibration frequencies, 18 x 4 bytes BCD (9 pairs) |
Everything else is unknown. DAC values are byte x 16.
11.3 Interpolation
UHF points every 10 MHz from 400 MHz (index 0-8); VHF every 9.5 MHz from 136 MHz (0-4); linear between neighbors, extrapolated with the last slope above the top point, clamped to 0-4095.
12. SPI flash
12.1 Device and bus
W25Q128 class, 16 MB, JEDEC 0x4018, on SPI2 with CS PB12, shared with the LCD (7.1). The bus runs in mode 3 at PCLK1 / 2 = 18 MHz for both the flash and the LCD (unverified). Commands in use: 0x03 read, 0x06 write enable, 0x05 status, 0x02 page program, 0x20 4 KB erase, 0xD8 64 KB erase, 0x44 / 0x42 / 0x48 security register erase / program / read with the register number in address bits 15-12. RadioFoundry reads the JEDEC ID but accepts any part that answers with a size. Security registers hold the calibration (11.1).
12.2 Map
Earlier notes put the codeplug / "EEPROM" image at 0 and the calibration copy at 0x010000. The stock TYT codeplug and the stock user database (CSV builds) also live in this flash: the stock firmware needs its flash content restored to run again, so a full backup before the first RadioFoundry flash is mandatory.
What the stock firmware keeps where (unverified; the meaning of most blocks is open):
| Range | Content | RadioFoundry area over it (19.5) |
|---|---|---|
| security registers 1-2 | factory calibration, 512 bytes (11) | none: never written |
| security register 3 | hardware option block, 64 bytes (11.1) | none: never written |
| 0x002000-0x0021FF | settings and state blocks | open-firmware "EEPROM" image |
| 0x02EF00-0x02F00C | the current channel / VFO records and a state record | open-firmware flash image |
| 0x040000-0x049FFF | lists of 200-byte records | open-firmware flash image |
| 0x060000-0x0B97FF | fonts and bitmaps (the update file's resource segment) | open-firmware flash image |
| 0x100000-0x2077FF | further blocks, some loaded into RAM at start | open-firmware flash image |
The update file rewrites the resource segment at 0x060000, but nothing restores the codeplug, settings or lists except a backup. The security registers are the only factory data that a backup can't put back without the stock CPS, which is why RadioFoundry never writes them (19.5).
13. GPS
13.1 Module
Optional, JS-M710 (unverified); the "MD-9600-5P GPS" FCC class II change. NMEA on USART1 RX (PA10), 9600 baud, 8N1, DMA. Bursts of invalid ('V') frames are reported from time to time (unverified).
13.2 Power and detection
PA9 high powers the GPS and the microphone together, so it stays on. No presence pin: detect by listening for valid NMEA, as on the MD-380 (ours).
14. Bootloader and images
14.1 Entering the loader
Disconnect DC (the power key is not enough), hold P1 and the orange (alarm) key while reconnecting. The screen stays
blank and the backlight blinks at 1 Hz (checked on a V1-V3 radio, 2026-11-02). The loader's DFU interface string for
the internal flash, @Internal Flash /0x0800C000/01*016Kg,01*064Kg,07*128Kg, tells it apart from the original
firmware in normal mode, which shows /0x08000000/03*016Kg under the same USB IDs and strings.
14.2 Protocol and encryption
USB DFU 0483:DF11 with TYT's commands, the DM-1701's protocol (hardware-dm1701.md 15.2). The image is XORed with the MD-9600's 1024-byte key, index from the start of the application image; key SHA-256 c3d665b297651b52310ec098e87440b147136241359c1154d474e517b048c9ae, first bytes A2 FA BB 4B (hardware-dm1701.md 15.3). The key can be derived from an original firmware file (as for the DM-1701 and MD-380): it works on every original firmware file listed in 14.3 (measured).
14.3 Stock firmware files
TYT names the files by feature: P = CSV + GPS, V = CSV, S = recorder + GPS, D = recorder (unverified). TYT's V4
firmware and CPS (for the "new updated PLL", V4 and later boards) are not compatible with V1-V3 radios (unverified).
TYT's download page offers only CPS installers for the MD-9600, no firmware. Retevis's firmware download offers the RT90's V5 firmware (two zips, RT90-Firmware-v5 and RT90-Firmware-GPS-v5). Dealers and community pages mirror the older TYT files.
Files examined):
| Version | Boards | Files | Container |
|---|---|---|---|
| 03.28, 03.29, 04.03 | older, board not known | MD9600-REC-D03.28.bin, -REC-D03.29, -REC-D04.03 |
as 06.09 |
| 06.03 | V1-V3 (SKY72310) | MD9600-CSV-V06.03.bin, -CSV-GPS-P06.03, -REC-D06.03, -GPS-REC-S06.03 |
as 06.09 |
| 06.09, 06.24 | V1-V3 (SKY72310) | MD9600-CSV-V06.09.bin, -CSV-GPS-P06.09, -REC-D06.09, -GPS-REC-S06.09 |
model string "MD-9600" at 0x10 |
| 16.24, 16.45 | V4 ("PLL1590") | MD9600-CSV(V4)-V16.45.bin, -CSV-GPS(V4)-P16.45, -REC(V4)-D16.45, -GPS-REC(V4)-S16.45 (and the same four as 16.24) |
model string blank |
| 26.45 | V5 ("2571") | MD9600-CSV(2571V5)-V26.45.bin, -CSV-GPS(2571V5)-P26.45, -REC(2571V5)-D26.45, -GPS-REC(2571V5)-S26.45 |
as 16.45 |
| 26.38, 26.41, 26.42 | V5 ("2571"), Retevis RT90 | RT90-CSV(2571V5)-V26.41.bin, -CSV-GPS(2571V5)-P26.41, -REC(2571V5)-D26.38, -GPS-REC(2571V5)-S26.42 |
as 16.45 |
| 26.57 | V4A ("2571V4") | MD9600-REC(2571V4)-D26.57.bin |
as 16.45 |
Files named MD9600-CSV(V4)-V26.45.bin and -CSV-GPS(V4)-P26.45 also circulate; they hold the V16.45 V4 ("PLL1590")
application, not LMX2571 code. The D26.57 V4A build programs the synthesizers and the IF lines exactly as the V26.45
builds do.
All of them: two segments, (0x00060000, 0x59800) for the external flash resources and (0x0800C000, 0xB1800..0xB6800) for the application, payload at 0x100, 0x100 trailing bytes. The leading digit of the version number is the board family (0 = V1-V3, 1 = V4, 2 = V4A / V5) (F: inferred from the known files). The V06.09 images report SP 0x20001BA0, the V16.45 ones 0x20001BB0.
14.4 Flashing: stock bootloader and stub
Nothing below has run on an MD-9600 yet: every row is unverified.
| Route | When | How |
|---|---|---|
| TYT bootloader | the first install, a radio on stock firmware, or a radio whose application is not committed | keys at power-up (14.1), then DFU with the MD-9600 key (14.2): tools/stm32_dfu.py --model md9600 |
| Flash stub | a radio already running RadioFoundry built with CONFIG_STUB_ENTRY=y |
stub/stubtool.py PORT --rfoundry-enter --write-app build/md9600/rfoundry_md9600.bin (stub-stm32f4.md) |
The stub is the DM-1701's and MD-380's image. The host sends it this radio's board record: SPI flash on SPI2 (PB13 SCK, PB14 MISO, PB15 MOSI, AF5), chip select PB12, mode 3, at most 4.5 MHz, the LCD's chip select PD14 held high while the stub runs, an 8 MHz crystal, the bootloader ending at 0x0800C000. The stub never touches SPI1 / PD7, the HR_C6000's voice port here.
An update with the stub writes the changed sectors of 3-8 only. It leaves the codec in sectors 9-11 alone unless the
image is merged with the codec (rfoundry_md9600_codec.bin), and never touches the bootloader in 0-2. Sector 3 is erased
first and its first word written last, so an interrupted update leaves the application invalid. Whether the TYT
bootloader then stays in DFU on its own, as the DM-1701's does, is unverified. If it doesn't, the key combination of
14.1 still reaches DFU. The stub also backs up and verifies the whole internal flash and the 16 MB SPI flash
(--progmem, --q128), which covers the backup 12.2 asks for once RadioFoundry runs. Before the first install the
web flasher backs up what the original firmware or the bootloader gives (15.2).
15. USB
OTG_FS on PA11 / PA12, mini-USB on the rear, straight to the MCU (no UART bridge). The bootloader's DFU device is separate (14.2). No VBUS sensing: PA9, the OTG_FS VBUS pin, is the mic and GPS supply switch (unverified), so the core must run with VBUS sensing off. The stock application's device is DFU class with two alternate settings, the internal flash and the SPI flash, self powered. TYT's CPS reads and writes the radio through it; most of the commands inside its transfers are not known (17 #21).
15.1 The original firmware's version over USB
Checked on a radio (V06.09, 2026-11-02). Running normally, the original firmware enumerates as USB 0483:DF11, manufacturer "AnyRoad Technology", product "Digital Radio in USB mode": the same IDs and strings as the bootloader. Its interface's alternate settings are "@Internal Flash /0x08000000/03016Kg" and "@SPI Flash Memory /0x00000000/16064Kg"; wTransferSize is 1024. It starts in dfuIDLE. The bootloader's first interface string is "@Internal Flash /0x0800C000/01016Kg,01064Kg,07*128Kg" (seen on a radio): the strings tell the two apart without a request, and the installers send the requests below only to a device showing the original firmware's string.
It reports its version, and nothing on the radio changes, with these requests on interface 0 (wIndex 0, wValue 0):
| Step | Request | Expect |
|---|---|---|
| 1 | GETSTATUS (IN 0xA1, request 3, 6 bytes) | bState 2. In 10: CLRSTATUS (OUT 0x21, request 4); in any other: ABORT (OUT 0x21, request 6) |
| 2 | DNLOAD (OUT 0x21, request 1, block 0), data A2 02 |
accepted |
| 3 | GETSTATUS, GETSTATUS | bState 4 (the command runs after this reply), then 5 |
| 4 | ABORT | dfuIDLE |
| 5 | UPLOAD (IN 0xA1, request 2, block 0, 1024 bytes) | 4 bytes 56 0M m0 mm: version Mm.mm, decimal digits (56 00 60 09 = 06.09, 56 01 60 45 = 16.45) |
A block 0 command is matched on its first byte and its exact length; an unknown one stalls. The bootloader answers the
same sequence too, with 54 02 10 06 (first byte 0x54, 'T'; from its code, not yet sent to a radio): only a first byte
0x56 ('V') is the original firmware's version. 0x54 means the bootloader; a stall or any other answer, unknown. The other block 0
commands, DNLOAD with block 2 and above, and a zero-length DNLOAD (its manifest step writes the SPI flash and resets the
radio) are never sent by RadioFoundry's tools. The version's leading digit is the board family (16), so the installers
read it to learn the board revision (19.3).
15.2 Backup before the install
The original firmware (running normally) and the bootloader both let a host read the radio without writing anything.
The web flasher backs up through either before an install (board.toml [flash.original_backup],
web-flasher.md). Every block 0 command below is DNLOAD block 0, then
GETSTATUS twice (bState 4, then 5), then ABORT, as in 15.1.
| Read | Original firmware (checked on a radio) | Bootloader (from its code; not yet tried) |
|---|---|---|
Info blocks: A2 ss, then UPLOAD block 0 |
01 and 03: option block 0x00-0x37 (32 + 24 bytes); 04: 8 bytes of settings; 05: the calibration as loaded in RAM (512 bytes, odd points interpolated, not the raw registers); 07: CRC-32 of the chip UID, then the 96-bit UID; 08: the clock; 32: the UID record and the STM32 OTP area (561 bytes); 33: the SPI region table and the radio name; 50: a constant; any other code: the version (15.1) | the same codes; the version answers 54 02 10 06; 01, 03, 04 and 05 may be blank |
| Internal flash | UPLOAD of block 2 or above, below program mode, answers from address 0, which on this chip is the flash from 0x08000000 (bootloader first): the block number is ignored, so one request is one snapshot of up to its wLength (at most 64 KB: the bootloader and the first 16 KB of the application). Linux usbfs refuses control transfers above 4096 bytes | the same |
| Program mode | 91 01 (only below mode 0x30); the radio stops normal operation |
91 01, accepted once |
| SPI flash | 21 + LE32 address 0, then UPLOAD of block 2 + n, 1024 bytes each: the 16 MB from (n x 1024). About 47 KB/s, paced by the radio (346 s for 16 MB); re-reads matched |
the same, for the first 1 MB only; above it the answer is a stale buffer |
| Leave | 91 05 (only in program mode): the radio restarts before it answers the first GETSTATUS, and comes back running normally |
stays in program mode. Its 91 05 needs a calibration mode and is never sent. The flasher's own 91 01 only acts below 0x30 (whether it then stalls or does nothing is open), so a write in the same session starts at 91 31 (mode 0x40, erase and write). A power cycle starts over |
The codeplug areas are within the first 2 MB (0x002000-0x03EFFF and 0x110000-0x197FFF in the map of 12.2); the 11.7 MB above 0x204000 (a contact database) is most of the time a full read takes. The internal flash above 0x08010000 can't be read by either: the media read of the internal flash returns a RAM buffer.
Never sent by the backup: 41 (erase), DNLOAD with block 2 and above (write), a zero-length DNLOAD (the original
firmware's manifest writes the SPI flash at 0x02F000 and restarts), 91 with any second byte but 01 and 05 (05 only
to the original firmware), B2-B5, C4, D5, an UPLOAD of block 1, and an UPLOAD above 1024 bytes in program
mode. The flasher refuses each of them in code before it reaches the radio.
16. Board revisions
| PCB label | Synthesizer | IF | Other differences |
|---|---|---|---|
| V1 (first boards, some labeled V2) | SKY72310 x2 | AK2365A x2 | backlight not dimmable the V2 way (7.3) |
| V2, V3 | SKY72310 x2 | AK2365A x2 | |
| V4 | "PLL1590" x2 (part ?) | AK2365A x2 | VHF synthesizer must be set at power-on |
| V4A, V5 | LMX2571 x2 | none programmable; PE11-PE14 switches | RSSI curve differs |
The original firmware follows the same split: V06.09 for V1-V3, V16.45 for V4, V26.45 for V5 (and D26.57 for V4A). The V4A and V5 images program the radio the same way, and neither reads anything that tells V4A from V5. TYT sometimes changed chips without changing the label; a wrong build boots but neither receives nor transmits, and does no harm (unverified). GPS models have been reported working with the V5 build. Radios needing TYT's V4 firmware are V4 or later.
No part of the radio reports its board revision, and TYT ships one firmware per revision (14.3). The GPS and recording models use the same pins as the others (unverified).
The original firmware does report its version over USB (15.1), and its major version follows the revision: 06.xx V1-V3, 16.xx V4, 26.xx V4A/V5. Any other major says nothing about the revision. A radio someone moved to another revision's firmware would report that firmware's line (unverified whether that boots, 14.3).
Detection: no ID pin or register is known. A run-time probe is possible because the families differ in the bus they need, but every candidate needs a radio: for example, program the V1/V2 SKY72310 words and watch RSSI / noise on a known signal, then the V5 words. The safer first design is one image with the revision as a setting chosen at first boot (or in the bring-up menu), defaulting to V5, with TX blocked until RX works (?: our proposal).
Target first: V5 / V4A. It is the current production board (the codec comes from the "2571V5" file), GPS models report V5, and it has the simplest IF path. V1/V2 come next (SKY72310: the driver exists from the MD-380). V4 last (unidentified synthesizer, needs its power-on quirk).
17. Unknowns that need a radio
| # | Unknown | How to settle it |
|---|---|---|
| 1 | Which revision the test radio is | Settled for the first test radio (2026-11-02): its original firmware reported v06.09, so V1-V3; FM receive works with the V1-V3 words. PCB label not read |
| 2 | V4 synthesizer exact part ("PLL1590"), V5 IF part. V5 use of PE15: none, V26.45 leaves it an input | Photos of the board (and FCC internal photos) |
| 3 | Lock detect pin (none mapped; V26.45 reads none) | Scope / board photos; otherwise lock is inferred from RX |
| 4 | LMX2571 delays after reset and calibration. V26.45 waits 10 ticks after the reset and 12 from the transmit words to the PA; the relock time itself is not measured | Scope the PLL on retune; measure the relock time (also gotchas.md 14.6, SFR / DMR burst switching) |
| 5 | RF TX interrupt usefulness and the 20 ms burst-off workaround | Settled (2026-11-03): keyed from the RF TX interrupt, the burst reaches full power ahead of its payload and every burst decodes; no workaround (19.8) |
| 6 | Power level labels (5/10/25/40 W) and the low-level fractions | Power meter into a dummy load, both bands, edges and center |
| 7 | RSSI / noise curves per revision | Signal generator steps |
| 8 | Mic keypad thresholds | Settled (2026-11-02): with the current thresholds all 19 hand mic keys and the mic PTT read as the right key |
| 9 | Front panel LEDs, key backlight | Look at the radio |
| 10 | Loader key combination (orange vs red + P1) | Settled (2026-11-02): P1 + orange, DC reconnected; the backlight blinks at 1 Hz (14.1) |
| 11 | Ignition sense | Schematic / board photos |
| 12 | I2C1 devices | Bus scan |
| 13 | Calibration unknown bytes, BCD frequency format | Dump the security registers and compare with the vendor CPS test screen |
| 14 | 0xA6 needed at display init | First boot |
| 15 | The I2C1 device at 0x3D | Bus scan, board photos |
| 16 | GPS UART baud rate (9600 in 13.1 is unverified) | Listen on PA10 with a GPS model |
| 17 | DMR slot and burst timing on this RF chain (synthesizer relock per TX/RX switch) | SDR captures of a DMR call |
| 18 | The crystal: 8 MHz crystal or an oscillator | Settled for V1-V3 (2026-11-03): the HSE starts in crystal mode, 72 MHz SYSCLK (board.clock=crystal) |
| 19 | LMX2571 registers beyond the documented set and the DEN = 0 reading: V26.45 writes nothing else and takes the fraction over 2^24, as RadioFoundry does. Still open: lock and spurs on a radio | Lock and spur check on V5 |
| 20 | Calibration record layout (11.2) and the hardware option block beyond the band edges | Dump security registers 1-3 |
| 21 | The CPS command set inside the DFU transfers (the version read is known, 15.1) | USB capture of TYT's CPS |
| 22 | Display orientation and column offset | Settled (2026-11-02): segments reversed, COM normal, column offset 4 (segments normal and COM reversed showed the picture upside down) |
| 23 | Encoder steps per detent and rest state | Settled (2026-11-02): 2 steps, resting where A != B, B leads clockwise (9.2) |
| 24 | The V1-V3 per-channel reference divider setting (5.3) | Retune across a band on V1-V3 |
| 25 | Receiver probe thresholds per revision (section 19.3) | Noise readings per revision with the probe words |
| 26 | Synthesizer settle times, the APC ramp, the PA drive rows and the squelch thresholds as RadioFoundry uses them (19.8) | Scope the synthesizer and PA lines on a retune and a key-up; power meter per level |
18. Sources
| Short name | Source | Kind |
|---|---|---|
| Earlier hardware notes on the MD-9600 (pin map, register writes), outside this repository; facts only | ||
| https://www.opengd77.org/viewtopic.php?t=83, hardware versions | Forum (F) | |
| https://www.opengd77.org/viewtopic.php?p=15, installation | Forum (F) | |
| [passion-radio] | https://www.passion-radio.com/mobile/md9600-505.html | Retailer (F) |
| [TYT] | https://www.tyt888.com/download.html (2026-10-12: CPS 1.27, 1.33, 1.43 and a product photo; no firmware) | Vendor |
| TYT stock firmware V06.09 and V16.45 ("firmware for PLL1590"), kept outside the repository; container headers and literal constants only | Vendor files | |
| [FCC] | FCC ID POD-DMR3, https://fccid.io/POD-DMR3 | |
| [SKY] | Skyworks SKY72310-11 data sheet | |
| [ST] | STM32F405 data sheet, RM0090 | |
| [gotchas] | gotchas.md 13.5, 14.6 | This repo |
| [TI] | Texas Instruments LMX2571 data sheet, SNAS654A | |
| [AKM] | AKM AK2365 data sheet, MS1453-E-02 |
19. RadioFoundry on the MD-9600
Bring-up state (2026-11-02): runs on a V1-V3 radio (original firmware v06.09). Checked: boot, display (orientation
7.2 and 17 #22), keys, the debug link and CPS, the data flash, calibration and option block reads, FM receive on VHF
(a NOAA station through a dummy load). Open: the receive audio level is high at the bottom of the volume knob and
sounds overdriven (the HR_C6000 settings, the FM_SW line and the IF chip words match the original firmware's; whether
the original firmware sounds the same on this radio is not checked); UHF receive, DMR, transmit. boards/md9600/.
19.1 What the image does
| Area | State |
|---|---|
| Clocks | 19.2 |
| Display | ST7567 on SPI2 (mode 3, 18 MHz) with the flash under one lock; TIM8 CH1 backlight, fully on when the option block says it can't be dimmed. The 128x64 layouts; hints name P1 and P2 (ui.md 1) |
| Keys | the 4 x 3 matrix (rows open drain, driven low one at a time), the encoder on EXTI10 / EXTI11 into the shared quadrature decoder, PTT on PE10. The power key goes to the power code, not the key map |
| Power | no battery (CAP_NO_BATTERY). "Off" saves, sets a flag in RTC backup register 18 and resets; at start-up the flag holds the radio until the power key is pressed and released. Supply voltage from ADC1 IN1 |
| Flash | SPI NOR with the DM-1701's map (settings 0x011000, journal 0x019000, open-firmware codeplug at 0 and 0x020000); the board revision record at 0x01F000, appended. Security registers read only: calibration and option block (19.5) |
| Receive | FM on both bands, every revision (19.3). Front-end tuning from calibration rows 0x30 / 0x39 (11.2) when they aren't blank, else mid scale |
| DMR | receive on the shared HR_C6000 glue (configuration port PE2-PE5, voice port SPI1 at 4.5 MHz, interrupts PC0-PC2, I2S3 PCM). Reference offset and DMR modulation pair (11.2 rows 0x130-0x149) rewritten at each retune. The two codec profiles (tags 0x0609 and 0x1645, one id md9600-s11) run the stock codec from flash: code at 0x080E0000, tables in sector 9, descriptor at 0x080FFF80 (codec-md9600.md) |
| Audio | FM from the IF chip (FM_MUTE high), PCM (DMR voice, beeps) from the HR_C6000 line out with FM held back, AF_SW muting otherwise (boards/md9600/speaker_md9600.h). A host audio session (NET_USRP) hears FM from PC3 and DMR from the stream, and transmits FM as PWM on PC8 into the HR_C6000's microphone input (19.9) |
| GPS | USART1 RX (PA10), receive interrupt; 9600 baud first, then 4800, 19200, 38400, 57600, 115200, 3 s each. A silent line ends the search after the first window; a module once heard feeds APRS positions. PA9 stays on (it also powers the mic) |
| Hand mic | the keypad of 9.3 through drivers/input/adckeys, three equal scans per key. Key roles of A-D and A/B are a proposal (unverified). No CAP_KEYPAD: a mic may have no keypad |
| Transmit | FM and DMR on UHF and VHF, checked off air into a dummy load (19.8, 19.9; VHF 19.9.1). On in the build (CONFIG_MD9600_TX, CONFIG_MD9600_DMR_TX); it still needs the run-time arm, a stored revision and plausible PA rows. Sub-audio: CTCSS and DCS from the PWM generator on PC7, with the DCS turn-off tail |
| Not yet | Transmit power per level (a power meter), split duplex and the two-PLL burst switching (17 #17), the CPS's own command set, update packaging |
19.2 Clock plan and memory map
| Item | Value |
|---|---|
| HSE | 8 MHz crystal; bypass mode tried if it doesn't start; HSI 16 MHz (no USB) as the last resort |
| PLL | M 4, N 72, P 2, Q 3: SYSCLK 72 MHz, USB 48 MHz |
| Buses | AHB 72 MHz, APB1 36 MHz (timers 72 MHz), APB2 72 MHz |
| Flash | 0x0800C000-0x0809FFFF application (sectors 3-8, 592 KB); sectors 9-11 kept free for the codec |
| RAM | 128 KB main SRAM (DMA buffers), 64 KB CCM (RTOS heap and task stacks, RFOUNDRY_CPU_RAM) |
19.3 Board revisions in one image
| Revision | Synthesizer | IF | Switch |
|---|---|---|---|
| V1-V3 | SKY72310, 16-bit words, VHF VCO at 2 x LO | AK2365A | CONFIG_MD9600_V1V3 |
| V4 | "PLL1590", 24-bit words, the VHF synthesizer set at power-on | AK2365A | CONFIG_MD9600_V4 |
| V4A / V5 | LMX2571, 24-bit words | PE11-PE14 filter and route lines | CONFIG_MD9600_V5 |
All three are on by default; a build with fewer drives only those boards. The revision in use comes from, in order:
- The stored record (8 bytes at 0x01F000: "M96R", a format byte, the revision and its complement). Only a stored record confirms the revision; transmit will need it.
- The receiver probe, when built with
MD9600_REV_PROBE=1. With the transmitter supplies and the PA loop held off and the RX VCOs selected, it programs the UHF synthesizer with each built revision's words for 446.0 MHz receive and reads the UHF noise level against the unprogrammed baseline. A revision wins when its change is at least the threshold and twice every other one's. It never drives PE15. The thresholds are unmeasured, so it is off by default. - The build default,
MD9600_REV_DEFAULT(v5).
A stored revision this image can't drive leaves the receiver off rather than programming the wrong chip. The record
is set with Settings > Advanced > Hardware > Board revision or the debug link's HWOPT (rfoundry_debug.py hwopt "Board revision" V4A/V5); it applies from the next start-up. The installers set it themselves: before the install they read
the original firmware's version (15.1, 16), and after the first start they store the revision it gives when the
radio holds none (board.toml [flash.original_version], web-flasher.md).
A stored revision that differs is reported and kept. A version read by the installer counts as a person's statement:
the record it stores confirms the revision like one set by hand. The open-firmware CPS has no item for it. Until a record is
stored the radio receives with the probe's or the default's revision and never transmits, and the diagnostics page
asks for one ("Set rev: Adv>Hardware"). Its first three lines show the revision and its source ("unbuilt" for a
stored revision this image doesn't drive), that prompt or "rev confirmed", and the probe ("off" when not built).
The default is one constant, MD9600_REV_DEFAULT in boards/md9600/board.mk. It is v5, the current production
board.
19.4 Pin use in RadioFoundry
| Pins | Direction | Idle level | Function | Status |
|---|---|---|---|---|
| PA0, PA1, PA3, PA6, PA7, PB0, PB1, PC5 | analog in | - | ADC1 scan: RSSI, supply, mic level, mic keypad, noise | unverified |
| PA4, PA5 | analog out | 0 | DAC1 UHF tuning, DAC2 VHF tuning / APC | unverified |
| PA8, PC9, PA13, PA14 | out | low, then high in receive | C5 and R5 supplies (PA13 / PA14 are SWD: connect under reset) | unverified |
| PD5, PD6, PC4 | out | low | T5 supplies, PA loop: high only in an armed CONFIG_MD9600_TX build (19.8) |
unverified |
| PD15 | out | low, high in receive | Power_Control | unverified |
| PE8, PE9 | out | high | RX VCOs | unverified |
| PB7 | out | low | MOD2 routing | unverified |
| PD8-PD11 | out | CS high, clock and data low | synthesizer bus, bit-banged | unverified |
| PE11-PE14 | out | CS high, clock and data low (V5: filter and route levels) | IF bus or V5 lines | unverified |
| PE15 | in, out on V1-V4 | high after the reset pulse | AK2365A reset | unverified |
| PB2, PB6, PC13, PE7 | out | low, PB6 high (muted) | FM stages, amplifier mute, audio band, FM mute | unverified |
| PC6 | TIM8 CH1 | off | backlight | unverified |
| PC7 | open drain, TIM8 CH2 when driven | released (high) | sub-audio tone PWM into the reference oscillator's modulation input (19.9). Driven low it pulls the reference: FM still receives, DMR never syncs (first radio, 2026-11-02) | checked |
| PC8 | out, TIM8 CH3 when driven | low | beep PWM net into the HR_C6000's MIC_P / MIC_N and LINEIN1; host FM transmit audio (19.9) | checked |
| PA9 | out | high | mic and GPS supply | unverified |
| PB12, PD14, PD13, PD12 | out | high, high, low, low then high | flash CS, LCD CS, A0, reset | unverified |
| PB13-PB15 | SPI2 AF5 | - | flash and LCD | unverified |
| PE0, PE1, PD0, PD1 | out, open drain | high | key rows | unverified |
| PD2-PD4 | in, pull-up | - | key columns | unverified |
| PB10, PB11 | in, pull-up, EXTI | - | encoder | unverified |
| PE10 | in, pull-up | - | PTT | unverified |
| PE2-PE6, PD7 | out | CS high, PE6 high (powered down) | HR_C6000 (held in power-down) | unverified |
19.5 Data flash, calibration and the board record
| Area | Range | Use |
|---|---|---|
| Open-firmware "EEPROM" image | 0x000000-0x00FFFF | codeplug part 1 (open-firmware-cps.md 7) |
| Free | 0x010000-0x010FFF | kept free (no calibration copy: the calibration stays in the security registers) |
| Settings ring | 0x011000-0x018FFF | 8 sectors (settings-store.md) |
| Sector journal | 0x019000-0x01AFFF | 2 sectors |
| Board record | 0x01F000-0x01FFFF | the stored board revision (19.3) |
| Open-firmware flash image | 0x020000-0xDFFFFF | codeplug part 2; the CPS may write up to 0xE00000 |
| Native codeplug | 0xE00000-0xEFFFFF | reserved |
| Staging | 0xF00000-0xF3FFFF | TFTP and fast CPS writes (tftp.md 4) |
This is the DM-1701's map; a part smaller than 16 MB gets no map and the radio runs from RAM. The settings ring and the journal sit under the foreign region guard (settings-store.md 5), so stock data found there is left alone until the debug link claims it.
Board record. Records of 8 bytes ("M96R", a format byte, the revision and its complement, a spare byte) are appended to the sector, and the newest valid one counts. A power cut while one is written leaves the one before it. The sector is erased only when its 512 slots are used; a cut during that erase leaves no record, which the radio treats as a new radio (probe or build default, transmit refused).
Security registers. RadioFoundry reads registers 1-3 once at start-up and never erases or programs them, and never writes a
status register (the registers' one-time lock bits are there). The SPI NOR driver has no command for either, and
tests/host/spiNorTest.c checks that no such command reaches the chip. The open-firmware CPS can read them as area 10
and can't write them.
Calibration. boards/md9600/cal_md9600.h is the interface for the radio code, on the generic reader in
drivers/cal/calblock/. Both blocks are kept whole as read, unknown bytes included.
| Block | Read as | State | Values |
|---|---|---|---|
| Calibration | registers 1 and 2, 256 bytes each, one 512-byte block | factory, blank (all 0xFF or 0x00), unreadable (a failed read of either register drops the whole block) |
every row of 11.2 by frequency: front-end tuning, the four PA levels, squelch 9 and 1 open and close, CTCSS and DCS deviation; the reference tune per band (byte - 128); the RSSI points; raw bytes |
| Hardware options | register 3, first 64 bytes | as above | band edges; the backlight on / off bit |
A row whose points are all 0xFF or all 0x00 has no value. Rows interpolate linearly between points (rounded) and continue the last slope past the top point. Front-end tuning without a value is mid scale (2048). The four band edges are sorted by value, so the order of the halfwords in the block doesn't matter: the two below 300 MHz are VHF, the two above UHF, and anything else (a nibble above 9, three edges in one band, two equal) gives none. With the backlight bit set, any brightness above 0 drives the backlight fully on. Transmit stays refused whatever the PA rows hold.
19.6 Debug link keys
The board view section (debuglink.md 3.7) carries these keys, each as board.<key>. The supply
voltage is the generic supply_mv key instead (hal_power_t.supply_mv; the divider's scale is unverified, 10.3).
| Key | Value |
|---|---|
rev, rev.source, rev.usable, rev.confirmed |
the revision in use (V1-V3, V4, V4A/V5, none), where it came from (stored, probe, default, none), whether this image drives it, whether a person set it |
rev.stored |
the board record now (a change takes effect at the next start) |
probe |
off, or the revision the receiver probe picked (none: no verdict) |
clock, sysclk_hz |
crystal, bypass (an external clock on the HSE pin), hsi_pll or hsi; the core clock |
flash.id, flash.size, flash.map |
JEDEC memory type and capacity (0x4018 for a 25Q128), bytes, 1 when the map of 19.5 is in use |
cal, opt |
the calibration and option block states (19.5) |
opt.backlight_switched, opt.vhf_hz, opt.uhf_hz |
the option bit; the band edges as low-high in Hz, 0-0 when there are none |
keys |
the matrix, one hex digit per row K0-K3, bits 0-2 = columns K4-K6, 1 = pressed |
ptt |
1 while the hand-mic PTT is down |
lo_hz, retunes, refused |
the synthesizer's last frequency, retunes and refused retunes |
apc, cal.pa |
the APC DAC value of the last transmission; the calibrated PA drive per power level (5, 10, 25, 40 W labels) at the receive frequency as a/b/c/d, 0 where the calibration refuses it (19.8) |
pin.power, pin.c5, pin.r5, pin.tx |
output levels: Power_Control; C5 UHF and VHF; R5 UHF and VHF; T5 UHF, T5 VHF and the PA loop enable (000 in this build) |
adc.noise_u, adc.noise_v, adc.rssi_u, adc.rssi_v, adc.mic, adc.supply, adc.mic_row, adc.mic_col |
the ADC1 scan, raw 12-bit (10.3) |
c6000, c6000.dmr, c6000.errors, c6000.tx_refused |
the HR_C6000 answered after set-up (with 4:0x10), in DMR, failed transfers, bursts the RF switch refused |
speaker |
bit 0 amplifier on, bit 1 FM audio through, bit 2 codec line out, bit 3 amplifier held warm (speaker_hang_s) |
mic_key |
the hand-mic key decoded on the last scan, 255 for none |
gps, gps.baud, gps.nmea |
module heard; the rate listened at (0 once given up); valid / bad sentences / bytes |
subtone |
the sub-audio decoder (CONFIG_SUBTONE): samples fed / rate; the input's rms and mean over 0.11 s (12-bit counts); the tone kind (0 none, 1 CTCSS, 2 DCS, 3 DCS inverted); match; an ADC or DMA fault |
tests/hil/test_md9600.py checks them on a radio: identity, every key present, the transmitter switches low, the
clock, the flash and its map, both blocks readable, no key or PTT down at rest, the ADC scan in range and a plausible
supply, and the receiver tuned when the revision is driven. It never transmits and skips on any other radio.
19.7 USB, programming and updates
| Path | State |
|---|---|
| USB device | CDC serial with the other RadioFoundry STM32 radios' IDs (1FC9:0094, network personality 0096, not built yet), product "MD-9600", serial number from the chip UID; VBUS sensing off (15) |
| Debug link | on the same port (debuglink.md); the board view section (19.6) |
| Codeplug | the open-firmware CPS protocol as radio type 5 (open-firmware-cps.md) and the fast codeplug protocol, writing through the journal (19.5). Area 10 reads the security registers; the CPS calibration window is empty (no calibration in the main array), so no CPS can write calibration |
| TYT's CPS | not supported: its command set is unknown (17 #21). Nothing of it is implemented |
| ROM DFU | debug link REBOOT mode 1 (debuglink.md 3.11), only at RDP level 0, with the same safe pin levels as at power-up first (board_sysboot_prepare) |
| TYT bootloader | the stock route (14.1, 14.2): keys held at power-up, then DFU with the MD-9600 key |
| Flash stub | CONFIG_STUB_ENTRY (off by default, as on the DM-1701; in the maximal configuration): board_stub_prepare switches the transmitter off first, then the shared STM32F4 stub runs with this radio's board record (14.4). Not tried on this radio |
The rfoundry board entry, the web flasher's radio entry and the release images come from boards/md9600/board.toml.
19.8 RF sequencing
boards/md9600/rfseq_md9600.c holds the order of every RF step for every revision; radio_md9600.c maps its lines to
the pins of 19.4. No synthesizer has a lock detect line (5.3), so each gets a fixed settle time after its words. All
times are unverified on a radio; the V4A / V5 transmit settle and reset wait are V26.45's (5.3, 5.5).
| Step | Receive | Transmit (CONFIG_MD9600_TX, armed) |
|---|---|---|
| 1 | PA loop and both T5 off, APC reference 0 | the same |
| 2 | Power_Control, both R5, both C5 on | both R5 off, FM stages off |
| 3 | both VCO_VCC high (RX VCOs) | the band's C5 on; the other band's C5 off on V4 and V4A / V5, on for V1-V3; U_V_MOD_SW to the band; the band's VCO_VCC low (TX VCO); V4A / V5: both IF routes off |
| 4 | the band's synthesizer words for the LO (5.2), then 2 ms | the words for the carrier, then 2 ms (12 ms on V4A / V5) |
| 5 | front-end tuning: DAC1 (UHF) or DAC2 (VHF) from calibration | the band's T5 on, 1 ms |
| 6 | the IF: AK2365A set-up and 2 ms for its calibration (V1-V4), or the filter and route lines (V4A / V5) | RF_APC_SW on |
| 7 | U_V_AF_SW to the band, FM_SW on in FM, off in DMR; 3 ms | APC reference (DAC2) up to the calibrated level in 8 steps of 250 us |
Return to receive ramps the APC reference down in the same steps, opens the loop, drops both T5, then runs the receive column with the last receive settings. For DMR bursts the PA alone (T5, the loop, the ramp) switches while the synthesizer stays on the carrier. DMR receive uses the 12.5 kHz IF setting.
DMR transmit (CONFIG_MD9600_DMR_TX): the HR_C6000 glue's prepare hook runs steps 1-4 without the waits (TX VCO on the
carrier, receivers off) at the start of a transmission, and the task waits the synthesizer's transmit settle (step 4)
plus 1 ms. Each burst then, 0.7 ms after the RF TX interrupt (a timer, no waits in an interrupt): the band's
T5 on, 500 us,
RF_APC_SW on, the APC reference up in 4 steps of 100 us; at its end (the timeslot interrupt) the same ramp down,
RF_APC_SW off, T5 off. T5 alone leaks the carrier about 13 dB under full power, and that counts as on air: keyed at the
interrupt itself (about 1.3 ms ahead of the slot), every burst was 31.7-31.9 ms long at floor + 10 dB on the SDR bench
(2026-11-06, 461.4625 MHz, lowest power; the bench allows 27-31.5 ms). Delayed, full power still comes about 0.2 ms
after the slot boundary, inside the guard ahead of the payload. The split duplex TX half (a BS outbound) keys once and
has no guard: its slot starts with the CACH. Its key-up is timed from the slot clock instead, full power 6 ms ahead of
a timeslot interrupt, after that slot's sync (docs/spec/modem-hrc6000.md 9.1). The receiver comes back at the last burst's PA
drop when the engine says no burst follows (stm32_hrc_dmr_tx_last_out), else at the slot engine's TX done, on every
channel, with the receive column's synthesizer, front-end and line steps and no waits. Back only at TX done, the
receiver was set up a slot later, as an answer starting at our last burst's end came in. On a split channel a
wake-up then hears the repeater's answer (left on the carrier, every key-up to a repeater that was down ended in
no_repeater). On simplex the reply to a confirmed packet comes in the next frame, 30 ms after the last burst, before
the radio task ends the transmission. That end goes through the modem glue's shared path (docs/spec/dmr-modem.md
6.4): with the receiver back from TX done, the board's tx_end keeps the synthesizer and only sets the front-end tune
again (on VHF it shares the DAC with the PA drive, which the end of the transmission zeroes); rx() is a full set-up. An SFR receives between its bursts
(below).
SFR (CONFIG_SFR; the transmission receives between its bursts, docs/spec/sfr.md 3 item 1, gotchas 14.6): at each
burst's end, after the ramp down, the synthesizer goes back to the receive LO (receivers on, RX VCOs, the words, the
front-end DAC, FM_SW off; on V4A / V5 the IF routes) without waits; the IF chip keeps its set-up. RF_TX_INTER moves to 1.5 ms ahead of the slot
(HR_C6000 0x12 = 15, back to 0 when the transmission ends). There the TX retune runs, then 0.6 ms for the lock, then
the burst's T5 and ramp. The original firmware never retunes between bursts, so it gives no figure for this; the
0.6 ms holds for V1-V3 only, and V4A / V5 needs its own sweep. Swept on air (V1-V3, UHF 446.125 MHz, 5 W, 2026-11-03), the bench's 3-superframe call
repeated by the MMDVM SFR:
| Lead, settle | Voice repeated | Corrected bits by voice frame (1st/2nd/3rd) |
|---|---|---|
| 3.0 ms, 2 ms | 12-18 of 18 | 35-59 / 0-12 / 42-56 |
| 2.0 ms, 1 ms | 18 | 71 / 0 / 36 |
| 1.5 ms, 0.6 ms | 12-18 of 18 | 53-67 / 0-6 / 0-6 |
| 1.5 ms, 0.6 ms, the burst ended 0.6 ms before the timeslot interrupt (TIM5, 2026-11-06) | 27 bursts (4 superframes) | 37 / 48 / 48, 16 of 69 frames lost: worse, reverted |
The call goes up whole (18 voice frames and the terminator). About 15-25% of the inbound AMBE frames fail their C0 check, and the first frame of each burst carries most of the corrected bits: the receiver's recovery after the drop at the slot start. Keeping the receivers' supplies on while transmitting changed nothing measurable. One run in three repeated two superframes of three at any setting (not explained yet).
Status (2026-11-03): SFR on V1-V3 is not fully supported yet. The HIL repeater-quality run (4 superframes, 72 inbound voice frames) repeats 27 bursts and passes the slot, CW ID and user-role checks, but loses 13 of 72 inbound frames (about 18%; the limit is 3), with 64 of the 76 corrected bits in the first frame of each burst. The SKY72310's charge pump is already at its maximum (register 6 gain 0x1F) and it has no fast-lock mode, so the receiver's recovery after each outbound burst is set by the loop filter. Next step: scope the synthesizer's lock detect and the receive AGC after a burst to tell the relock from what follows it. Measured off air (UHF, 5 W, 2026-11-03): T5 left on between bursts leaks the carrier at -13 dB, hence T5 per burst. With it, between bursts the carrier is below the measuring receiver's floor (26 dB down). On air at key-up: a 0.5 ms step to -13 dB (T5), then full power 0.4 ms later; key-down takes 0.4 ms. On-time 30.7 ms, same-slot spacing 60.0 ms, outer symbol deviation 1933 Hz, every burst of a 3 s call decoded (headers, 9 superframes, terminator).
At power-up, with the revision known: receivers on, then on V1-V4 the AK2365A reset (PE15 low for 10 us), and on V4 the VHF synthesizer set for 145 MHz receive. On V4A / V5 the LMX2571 reset goes to both chips, then 10 ms; PE15 stays an input.
Transmit is refused unless all hold: the build has CONFIG_MD9600_TX (on by default); a person armed it for this
power cycle (Settings > Advanced > Hardware > Transmit, or HWOPT; every start is unarmed); the revision comes from a
stored record; the frequency is in a band; the calibration's PA rows are present at every point and rise with the
power level (11.2). A refusal or a bus failure leaves the PA loop and both T5 off and the receiver back on. Disarming
drops the PA at once.
Calibration as the RF code uses it (11.2, all unverified): front-end tuning, PA drive per level (5 W row 0x20, 10 W 0x1A0, 25 W 0x190, 40 W 0x10, VHF 9 bytes later), squelch level 1 and level 9 opening thresholds. DAC values and noise thresholds are the byte x 16. The noise reading maps linearly onto the core's squelch scale through the two thresholds: level 1's maps to 64, level 9's to 40. Without them it is the 12-bit reading / 16. RSSI uses the curves of 5.7. The diagnostics page shows which row groups each band accepts ("calrf V RPS U RPS"), the sequencer state, the arm and the last refusal.
19.9 Other RF-side lines
| Line | Use in RadioFoundry |
|---|---|
| PA2, ADC IN2 (QT_DQT_IN, the sub-audio low-pass output) | the CTCSS / DCS decoder (subtone_md9600.c, CONFIG_SUBTONE): ADC2 converts it continuously (480 cycles, 18.3 kHz) into DMA2 stream 3, and each half-transfer interrupt feeds sums of 8 to drivers/rf/subaudio at 2287 Hz. Mid-supply (about 1950 counts). A 100.0 Hz tone on a narrow channel holds it at about 320 counts of amplitude, talking or not; an empty channel gives 300-600 counts rms of noise. The line is AC coupled: a DCS stream on it is high-passed with a corner near 18 Hz, so a run of equal bits sags to under half within a bit (the decoder slices with hysteresis for this). The diagnostics page's "qt" is one ADC3 conversion of it |
| PC3, ADC IN13 (RX_2T_5T) | The MCU's only copy of FM receive audio (disc_md9600.c). One capture, two readers with their own place on its ring: APRS receive (afsk_md9600.c, CONFIG_APRS_RX, while the core listens) and FM receive audio on the PCM stream's input (c6000_md9600.c, NET_USRP, while the stream runs in FM: taken to 8 kHz by drivers/audio/resample, gated by the core's squelch, silent in transmit, level MD9600_FM_RX_GAIN_Q8, set to the DM-1701 FM tap's level for the same signal (1.5 kHz peak deviation at 1 kHz: about 3500 rms, UHF, 2026-11-06)). While either runs, ADC3 converts it continuously (480 cycles, 18.3 kHz) into DMA2 stream 1, and each half-transfer interrupt queues sums of 2 (9146 Hz) with the DC taken away; the diagnostics page's ADC3 reading of PA2 holds its last value meanwhile. Voice-band audio with sub-audio removed: under a carrier with a 100.0 Hz tone it drops to about 16 counts rms and rises to about 400 with speech; 450 counts of noise on an empty channel |
| PC7, TIM8 CH2 | sub-audio tone PWM (md9600_pwm_out), on the backlight's 28 kHz timebase, into the reference oscillator's modulation input. Without a tone the pin is released (open drain high), as at start-up: driven low it pulls the reference and DMR never syncs. In FM transmit TIM8's update interrupt (priority 6) feeds drivers/rf/subaudio's generator a new duty every 8 periods (about 3.6 kHz). The tone's share of the swing is set per band for about 375 Hz deviation at 12.5 kHz and doubled at 25 kHz (MD9600_SUBTONE_PERCENT_UHF 17, _VHF 24; at 30 % UHF gave 665 Hz and VHF 478 Hz). Measured off air (2026-11-04): CTCSS 100.0 Hz at 368 / 754 Hz (UHF, 12.5 / 25 kHz) and 380 / 758 Hz (VHF); DCS D023N at +-385 Hz (UHF) and +-399 Hz (VHF), with a 0.30 s 134.4 Hz tail. The path inverts: DCS goes out with the generator's polarity flipped (MD9600_SUBTONE_TX_INVERT) (first radio, 2026-11-03) |
| PC8, TIM8 CH3 | The beep PWM net reaches the HR_C6000's MIC_P / MIC_N and LINEIN1: a 1 kHz square wave on it while keyed goes on air as a 1 kHz tone (784 Hz rms deviation with MIC_P / MIC_N on, 442 Hz with LINEIN1, 8 Hz with neither; UHF, 2026-11-05). FM transmit audio from a host (NET_USRP, fm_tx_inject in c6000_md9600.c) is PWM on it (md9600_pwm_audio in pwm_md9600.c): TIM8's 28125 Hz timebase stays the backlight's, DMA2 stream 2 rewrites CCR3 every period, and 225 periods take 64 samples (8 ms) linearly interpolated with the rounding error carried. MIC_P / MIC_N on and LINEIN1 off for the over; the mic gain stays (4:0xE4 is not monotonic: gain 19 gave a quarter of gain 7). Full scale is the whole range. Off air at 1 kHz: half scale 675 Hz rms, quarter 337, eighth 173 (linear), full scale 989 Hz rms (the limiter); 400 Hz and 2.5 kHz follow the pre-emphasis; 15-16 dB SINAD, as on other HR_C6000 radios' PWM path. fm_tx_nominal_peak 26000 (about 1.5 kHz peak). The audio probe's pin_pwm runs a sine through the same path |
| PA temperature, fan | no sensor or fan line is known on any revision (?) |
19.9.1 VHF transmit
Checked off air (V1-V3, 151.5125 MHz, 5 W into a dummy load, 2026-11-03), the bench board's receiver retuned to VHF:
| Check | Result |
|---|---|
| FM, 12.5 kHz, CTCSS 100.0 | carrier -31 Hz; CTCSS 100.02 Hz at 380 Hz deviation (478 Hz before the per-band level) |
| FM, 25 kHz, CTCSS 100.0 | carrier -20 Hz; CTCSS at 758 Hz deviation |
| FM, DCS D023N | decoded, +-399 Hz deviation; 134.4 Hz turn-off tail 0.32 s |
| DMR, CC 1, slot 1, 4 s | voice header, 11 superframes and the terminator: 70 bursts, no sync errors, 60.0 ms apart, 1.83-1.97 kHz deviation, carrier +46 Hz |
| Level per power step (relative, leakage from the dummy load) | 10 W +2.7 dB, 25 W +4.0 dB, 40 W +8.9 dB over 5 W (nominal +3.0 / +7.0 / +9.0); needs a power meter |
19.10 Build switches
The board's own switches, in boards/md9600/board.mk and config.mk (the shared ones:
build-config.md):
| Switch | Values |
|---|---|
MD9600_REV_DEFAULT |
v5 (default), v4 or v1v3: the board revision driven when none is stored and the probe gives no verdict (19.3) |
MD9600_REV_PROBE |
1: the receiver probe at start-up picks the revision when none is stored. Off (0) until its thresholds are measured |
CONFIG_MD9600_V1V3 / _V4 / _V5 |
y (default) or n: the synthesizer driver per board revision: V1-V3 (SKY72310 and the AK2365A IF receivers), V4 (the 24-bit "PLL1590" part and the AK2365A), V4A / V5 (LMX2571). All on, one image runs every revision; fewer make a smaller image for those boards. At least one must be on (19.3) |
CONFIG_MD9600_TX |
y (default) or n: the transmit sequence (boards/md9600/rfseq_md9600.c). Built, transmit is still refused until it is armed for the power cycle (Settings > Advanced > Hardware, or the debug link) on a radio whose board revision is stored, and the PA calibration rows look plausible (19.8) |
CONFIG_MD9600_DMR_TX |
y (default) or n: DMR transmit, the per-burst RF keying of 19.8 from the HR_C6000 glue. Needs CONFIG_MD9600_TX, and keys only while transmit is armed |
The 16.8 MHz TCXO has no control line (5.2), so the reference trim (REF_TRIM) programs each synthesizer for
f x (1 + trim) at every tune and burst, the IF unchanged (rfseq_md9600.c; radio-features.md 6.1).
Feature switches that differ from the platform's defaults (config.mk; the two lists:
build-config.md):
| Off by default | Why |
|---|---|
NET_PPP (maximal build too: MAXIMAL_SKIP) |
the radio has the USB network adapter; PPP is for radios with only a UART |
LANG_ES and what follows it |
English only |
UI_COLOR, FONT_XL, FONT_AA, FONT_BIG, AM_RX, BCAST_FM, VIBRATE, NAND_STORAGE, FAULT_TRACE (maximal build too: BOARD_UNSUPPORTED) |
a 128x64 monochrome screen; no air band or broadcast receiver; no vibration motor; NOR data flash; no UART for a fault trace |