Questa pagina non è ancora tradotta; è mostrata in inglese.

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-11 see codec-md380.md The codec as the merge places it. From D002.032: the table windows at their original addresses (sectors 8, 10, 11), the code moved to 0x080E0000 (sector 11). From D35.14 / S35.14: the main table window at its original address (from sector 9 into sector 10), the code right after it, the small ones in sector 11. Then the descriptor at 0x080FFF80

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: 40 kHz, far above the FM transmit audio band (#330)
PC7 CTC/DCS_OUT, sub-audio tone output (PWM) (unverified); TIM3 CH2 (AF2) with CONFIG_SUBTONE (section 6.4), otherwise released (open drain, high): it tunes the reference (5.2)
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

FreeRTOS's configMAX_SYSCALL_INTERRUPT_PRIORITY is 4 (platform/stm32f4/FreeRTOSConfig.h; even so that a part with 3 priority bits can hold it, gotchas.md 13.11), and no interrupt uses 4. The modem ISRs at 5 notify the DMR task, and a masked section at the syscall 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; the engine reads the bit only after two received slots in a row (gotchas 1.10), so the first read after our own transmission is skipped. Unconfirmed on this chip: whether that first read is frozen or already right. 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. The user's reference trim adds to DAC2 (5.2, "Carrier offset"). The 2-2.5 kHz error two radios showed was not this mapping but PC7 held low (5.2) (unverified until a radio confirms the fix).

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 since the last tick, or has no frame that checks, is a lost burst; six in a row end the reception (0x22 = 0x17). The port is read at every tick, the other slot's too: a silent talker's bursts are the same bytes every time, so only a port that stopped moving is a lost burst. A read compared with this slot's last burst lost every burst of a silent call after the first (gotchas 1.11). That the port moves at the other slot's tick on a direct channel with nothing on that slot comes from the on-air table above; if it doesn't, the same bytes stay lost bursts. The diagnostics line tr c b same bad counts receptions, bursts and lost reads by cause. The chip's voice class reports are ignored, and its 25 ms report delay doesn't count as out of step.

So on this chip an empty drx trace shows only that no read was taken. It doesn't show that the chip went quiet.

A reception lost mid-call costs more than its gap (drx and log times of a bench run, two gaps alike; gotchas 1.12):

Step Time after the last burst What happens
Six lost reads 360 ms DecodeStop. Nothing is read until the next chip interrupt
13 ticks without a burst 420 ms rs l: the receiver restarts, which drops the chip's sync
Hunting for sync about 660 ms The timeslot interrupts stop for about 240 ms, so the tick count falls behind the air
Late entry report about 1 s The chip has assembled the LC. The first read after it is burst F (the trace's slot counts fit that, and the bursts are numbered from A, so they stay one off until the call ends)

A burst is in the port one tick after its slot's report, so the read at a tick holds the burst of the slot that started two ticks before. The engine takes this as fixed: the read is named, and given its slot counter, from the tick before it. It used to learn the relation from the first read that checked after a header, which with traffic on both slots can be the other call's (issue 279). With traffic on both slots both phases hold bursts that check, so the search for the call's phase is limited:

Rule Detail
A header started the read Lock only the header's phase. A late entry report while searching drops that limit
Repeater channel (rx_lock_slot 1 or 2) Lock only a phase the CACH TC names as the channel's slot, never one with the slot unknown. A header is kept unless the TC names its phase the other slot
A lock whose phase the TC names the other slot Search again, without the header's phase. The burst index goes on counting one per frame, so a lock back on the same phase keeps A-F
The slot unknown while locked (a receiver restart) The lock stays; its bursts carry no slot number until the TC locks again
The call's own bursts skipped while its slot is unknown Counted, so the index stays A-F. After a quiet-period restart that costs the first bursts of a call (two in the host test, about 150 ms until the TC locks)

All of these are host-tested (hrc5000Test test_two_slots: a call on each slot, a restart mid-call, a TC wrong from the start, both slots heard with rx_lock_slot 0). On air (the SDR bench sending repeater bursts with the CACH on both slots): the reads alternate as above (phase 0 TS1's frames, phase 1 TS2's), and the wanted call plays with the other slot already talking (77 of 78 bursts; 0 before the fix).

The embedded LC isn't per slot (page 2, modem-hrc6000.md 5.2). On the same bench run, the other call's LC was in page 2 at the wanted call's F: the core took it for a new call, and the wanted call went quiet with the other call's source shown (23 of 78 bursts when the wanted call started first). The engine keeps each slot's call LC and holds back an embedded LC that is the other slot's call's (the rules are in modem-hrc6000.md 5.2). Host-tested (test_two_slots, the LC, alias and mixed LC cases: page 2 gets a call's LC or alias block the tick after its F). On air (the SDR bench, a call on each slot, 78 bursts sent to the wanted slot, two runs each):

Case Before After
Wanted call first, the other starts beside it 23 78, 78 (both slots)
Other call first, wanted call with its headers 75-77, TS2 once 13-19 73-77 (both slots)
Wanted call joined mid-stream (no header) beside the other, TS1 0 played 56-57
Single call: headers, late entry, no header 77-78, 38, 68 the same

Left as they were: both calls joined at once after a channel switch (0-20 bursts) and TS2 joined mid-stream beside a call (0) play as before.

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 a 70 cm test frequency, 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 MHz 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).

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. A second MD-380 (reference tune 0x72) measured 2-2.5 kHz high on a service monitor, on FM too.

Bench, 2026-10-28, the first radio (reference tune t = 0x51), with test builds that changed one control each. The receive error is the center of the band a DM-32UV's DMR is decoded in as the MD-380's VFO steps 250 Hz at a time; the transmit error is from the hotspot's MMDVMCal BER test:

Build DAC2 0x47 / 0x48 Receive Transmit
as in 4.5 0x744 0x2E / 0x00 +2.0 kHz +2.0 kHz
register sign as the HR_C6000's (t - 128) 0x744 0xD1 / 0x03 -0.75 kHz -1.0 to -1.5 kHz
DAC2 fixed 0x600 0x2E / 0x00 -3.25 kHz no clean decode
DAC2 fixed 0xA00 0x2E / 0x00 none within +-9 kHz (about +11 kHz) no clean decode
  • Receive and transmit are off by the same amount, so the error is in the shared 16.8 MHz reference (about 4.5 ppm), not in the synthesizer's divide ratios.
  • DAC2 moves the reference about 16.2 Hz a count at 448.7 MHz (36.1 ppb), higher = higher. The HR_C5000 register moves it about 31 Hz a step.
  • The mapping in 4.5 lands this radio 2 kHz high, and flipping the register's sign overshoots to about -1 kHz.

Cause: CTC/DCS_OUT (PC7) held low [schematic, page 6]. The 16.8 MHz VCTCXO X601 clocks the SKY72310 and, through the Q605 tripler, the second LO, so receive and transmit share it. Its VC pin is driven by U603 (NJM2904) amplifier B:

U603 B input Signal Path
Inverting (pin 6) MOD2, from the HR_C5000 R645 100K
Inverting (pin 6) CTC/DCS_OUT, PC7 R641 560K
Non-inverting (pin 5) MOD2_BIAS, DAC2 (PA5) R605 15K, R615 22K
Feedback R644 100K, C678 100 pF
Output (pin 7) X601 VC (pin 1) R646 4K7, C640 3.3 nF to ground

Any DC on PC7 moves the reference, at -100K / 560K = -0.18 V per volt at the amplifier's output. RadioFoundry used to drive PC7 push-pull low whenever no tone was sent, which raises the non-inverting gain from 2 to 2.18: the same as DAC2 about 0.18 / 2.18 x 0x744 = 150 counts high, about +2.5 kHz at 448.7 MHz, in receive and transmit alike. That fits both radios (+2.0 and +2-2.5 kHz) in size and sign (this is an estimate: the bias divider past R615 isn't fully traced). PC7 now idles released (open drain, high), so the branch carries no current (unverified on a radio).

While a tone is sent the PWM drives PC7 push-pull around 50 % duty (about 1.65 V mean), so a channel with CTCSS or DCS may still transmit off by some hundreds of Hz (unverified; 6.4).

Before that fix, the user's trim corrected it: hal_radio_t.ref_trim (radio-features.md 6.1) adds ppb / 36.1 counts to DAC2 (rounded), at once and at every rx(). A trim of -2000 Hz at 448.7 MHz (-4.5 ppm) put the first radio's transmit within about 150 Hz (hotspot BER 0.05 %, four cold wake-ups on alternating slots). MD380_TX_OFFSET_HZ (radio_md380.c, a build define, default 0) still shifts the transmit frequency alone; with the trim it isn't needed.

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)

The synthesizer returns to RX at the engine's TX done, on every channel. On a split channel the radio then hears the repeater's answer to its wake-up. On simplex the receive LO is 49.95 MHz from the carrier, and a confirmed data response comes in the next frame, 30 ms after our last burst. Left on TX until tx_off() (the radio task), the radio missed nearly every such response (a receive CRC error instead), and the TX VCO kept leaking the carrier about 34 dB below the burst while TX_5V discharged (about 20 ms). rx() then keeps the synthesizer where TX done put it.

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 receive works on air (D023N, 2026-11-02). DCS transmit went out inverted (D023N decoded as D023I by another radio, 2026-11-02), so MD380_SUBTONE_TX_INVERT is on by default. At MD380_SUBTONE_PERCENT 30 the deviation is 0.60 kHz peak for CTCSS 100.0 Hz and about 0.72 kHz for DCS (SDR, 2026-11-02), inside the usual 0.5 to 0.75 kHz. Without CONFIG_SUBTONE, 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 is released again (open drain, high): PC7 sums into the reference oscillator's tuning, so a low there pulls the carrier (5.2). 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 differs between radios and is detected (below). RadioFoundry drives it with drivers/display/st7735 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). Backlight: LAMP (PC6), TIM8 CH1 PWM at 1 kHz (unverified for the pin; PWM ours).

Panel type: display_md380.c picks the st7735 driver's type (drivers/display/st7735/st7735.h) at start-up, from the controller's own ID and the factory byte (panel_md380.c):

  1. After a hardware reset, before any init, it reads the display ID: command 0x04 returns a dummy byte and three ID bytes, 0xDA, 0xDB and 0xDC a dummy byte and one each. Reads use a slower FSMC strobe than writes (address setup 15, data 60 HCLK cycles).
  2. It reads the factory byte: SPI flash security register 3, byte 0x1D. Many radios carry 0xFF there because the register was wiped, so an erased byte says nothing.
  3. The controller ID chooses the init, the byte (where programmed) only the orientation:
Controller ID Controller Factory byte Type
00 91 xx GalaxyCore GC9xxx (the unlock-variant init) 0xFF, unread, or bit 0 = 1 3 (normal)
bit 0 = 0 2 (flipped)
7C xx xx Sitronix ST7735 class 1 1 (rotated 180 degrees)
anything else 0 (normal)
anything else unknown programmed its low two bits
0xFF or unread 0

Bit 0 gives a GC9xxx's orientation in both stock layouts: stock D3.008 to D13.20 tested only bit 0 (0 = flipped GC9xxx, 1 = normal); D3.32 and later use all four types (2 = flipped, 3 = normal GC9xxx).

Stock sets the same color order for every type, so all are driven in RGB order (force_rgb).

One radio (2026-10-27): ID 00 91 02 from both commands on every start (probably a GC9102), factory byte 0xFF, so type 3; picture right side up with correct colors. Its panel also showed a picture with the ST7735 init (type 0), which is why a picture alone doesn't identify the controller. Untested: an ST7735 panel, and types 1 and 2.

Knob or key What
MD380_PANEL_TYPE (build, MD380_DEFS) overrides the detected type
MD380_PANEL_MADCTL_XOR (build) orientation bits flipped on top of the type's, default 0
display.panel_controller (debug link link section) gc9xxx, st7735 or unknown
display.panel_source (same) what chose the type: id, factory, default or build
display.panel_type (same) the type in use
display.panel_id, display.panel_id_reg (same) the ID bytes in hex from 0x04 and from 0xDA-0xDC
display.panel_raw (same) the factory byte in hex, none when it couldn't be read

boards/md380/tests/md380PanelTest.c checks the rules. boards/md380/hil/test_md380_panel.py checks the radio: both ID reads agree, the controller is known, the type follows the rules. An unknown controller fails it: add its ID to panel_md380.c.


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

Single band, 9 points per row:

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-0x1F7 Frequency list: 9 entries of 8 bytes, two 4-byte BCD frequencies each, least significant byte first, 10 Hz units
0x1B2-0x1B3 Band low end: the upper half of the first frequency, 4 BCD digits in 100 kHz units (0x4010 = 401.0 MHz)
0x1F2-0x1F3 Band high end, the same from the last entry (0x4799 = 479.9 MHz)

Only the even points (0, 2, 4, 6, 8) of a row are used. Each odd point is the mean of its neighbors, rounded down, whatever the flash holds there. That holds for every row from 0x010 to 0x180 except 0x0E0. The rest of the frequency list is not used: on the radios seen, the first frequencies run 401.035, 411.035 ... 471.035, 479.995 MHz, close to the points below but not the same.

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 band ends are not BCD, out of band or not ascending, it uses 400.0-470.0 MHz (UHF) or 136.0-174.0 MHz (VHF) instead and says so ("factory/grid").

11.3 Interpolation

The nine points split the band by halving, in 100 kHz units with integer halves: point 0 and 8 are the band ends, point 4 the middle of those, points 2 and 6 the middles of the halves, and the odd points the middles between. For 401.0-479.9 MHz: 4010, 4108, 4207, 4305, 4404, 4502, 4601, 4700, 4799.

The channel frequency is taken in 100 kHz units, truncated (TX frequency for the power rows, RX frequency for the tuning row). The value is linear between the two neighboring points, truncated toward the lower point's value, and clamped to the first and last point outside them. Power and tuning bytes go to the 12-bit DAC as (b x 255) >> 4 (b x 255 in its left-aligned register).


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. The CPS still reads the chip (area 1, up to its end) and the radio info gives its size, so a backup saves all of it; the Codeplug Editor and the flasher's channel step say that the radio stores no codeplug.


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).

Switched on normally, the original firmware is also a DFU device with the same IDs. The flashers tell it from the bootloader by alt 0's layout and refuse it (hardware-dm1701.md 15.1). The MD-380's bootloader, read on a radio (2026-11-02): manufacturer and string 1 "AnyRoad Technology", product "Digital Radio in USB mode", serial "00000000010C", bcdDevice 0x0200, one DFU interface with two alternate settings:

Alt Layout
0 @Internal Flash /0x0800C000/01*016Kg,01*064Kg,07*128Kg (the application area only)
1 @SPI Flash Memory /0x00000000/16*064Kg

boards/md380/board.toml declares alt 0 ([flash.dfu] layout), so the flashers accept only that device.

The same radio switched on normally, running a patched build of the original firmware (2026-11-02): 0483:DF11, bcdDevice 0x0200, serial "00000000010C", alt 0 @Internal Flash /0x08000000/03*016Kg, alt 1 as in the bootloader. String 1 and the manufacturer read "@00000004 : ffffffff" and the product "Patched MD380", which the patch may have changed; the unpatched firmware's strings are not known (?). Alt 0 is the MD-9600's normal-mode layout, so either rule refuses it.

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. The D35.14 (MD-380) and S35.14 (MD-390) files work too: they carry the MD-UV380 family's codec, placed next to its tables in sectors 9 and 10 (untested on the radio). Entry points, calling convention, state sizes, the placement and the extraction recipe are in codec-md380.md. stockfw.PROFILES has the profiles, the codec maps (rfoundry/codec_maps/md380/, one per build) 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: the type is detected (section 7); if the boot screen is wrong, note the display.panel_* keys and try MD380_PANEL_TYPE / MD380_PANEL_MADCTL_XOR
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.

CONFIG_NET_WEBCTL follows CONFIG_NET_HTTP_GZIP on this radio (boards/md380/config.mk): web control's files, stored uncompressed, need more flash than the default image leaves free. With gzip off the radio serves the plain status page.

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_safe pins: 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>.bin UHF, rfoundry-md380-vhf-<version>.bin). Application sectors 3-7 (464 KB), codec sectors 8-11 (D002.032: 8, 10, 11; 35.14: 9-11), so an update keeps the codec. Profile md380-d2032-s11 (a build from before the move reports md380-d2032). tools/md380_migrate.py flashes it and keeps the codeplug.

Vedi il sorgente di questa pagina