tuning.cpp: import full PRALINE RX/TX tuning tables with quarter-shift offsets

This commit is contained in:
gullradriel
2026-08-08 21:33:46 +02:00
parent b1df55a26e
commit 419fc5d48a
+332 -99
View File
@@ -27,8 +27,8 @@ namespace tuning {
namespace config {
// Forward declarations
Config low_band(const rf::Frequency target_frequency);
Config mid_band(const rf::Frequency target_frequency);
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
Config high_band(const rf::Frequency target_frequency);
#ifdef PRALINE
@@ -36,133 +36,366 @@ Config high_band(const rf::Frequency target_frequency);
* PRALINE Tuning Configuration
* ============================
*
* Reference: hackrf_usb/common/tune_config.h praline_tune_config_rx[]
* These tables are copied verbatim from the reference firmware,
* hackrf/firmware/common/tune_config.h (praline_tune_config_rx /
* praline_tune_config_tx), and the selection and offset maths below reproduce
* hackrf/firmware/common/radio.c radio_update_frequency() /
* analog_from_digital_rf() / compute_offset().
*
* The hackrf_usb firmware uses a table-driven approach where each entry
* specifies:
* - rf_range_end_mhz: Upper frequency limit for this config
* - if_mhz: IF frequency (what MAX2831 tunes to)
* - high_lo: true = high-side injection, false = low-side
* - shift: FPGA quarter-shift mode (not implemented in Mayhem yet)
* Each entry gives, for target frequencies up to rf_range_end_mhz:
* if_mhz the IF the MAX2831 tunes to (0 = mixer bypassed, IF = RF)
* high_lo true -> LO = IF + analogue RF (mixer inverts the spectrum)
* false -> LO = IF - analogue RF (no inversion)
* shift the FPGA quarter-rate shift mode used for this entry
*
* Key insight: The IF frequency varies to keep the RFFC5072 VCO in a
* safe operating range (ideally 3500-5000 MHz, avoiding extremes).
* The quarter-rate shift is the part Mayhem was previously missing. RX entries
* deliberately place the analogue passband a quarter of the ADC rate away from
* the requested frequency (+8 MHz at the usual 32 Msps AFE rate) so that the
* wanted signal never sits on the DC offset / LO leakage, and then ask the FPGA
* to rotate it back down to DC. Both halves have to be programmed together:
* - tuning to target + offset without asking the FPGA to rotate leaves the
* signal 8 MHz out and the decimation filter deletes it;
* - tuning to target with no offset (what Mayhem did) parks the signal on DC,
* under the LO leakage and the gateware's adaptive DC block.
*
* RFFC5072 VCO calculation:
* High-side injection: LO = IF + RF, VCO = LO × lodiv
* Low-side injection: LO = IF - RF, VCO = LO × lodiv
* Where lodiv = 2 for frequencies where VCO > 2700 MHz
*
* From hackrf_usb tune_config_rx (simplified):
* 0-2100 MHz: IF=2375, high_lo=true → VCO = (2375+RF)×2
* 2105-2115: IF=2375, high_lo=false → VCO = (2375-RF)×2
* 2115-2130: IF=2425, high_lo=false → VCO = (2425-RF)×2
* ... (more entries for fine-grained control)
* 2320-2580: IF=0 (bypass mode, no mixer)
* 2580+: High-pass mode
* "up" and "down" are named for the direction the FPGA rotates, so
* FPGA_QUARTER_SHIFT_MODE_UP means the analogue centre is placed ABOVE the
* requested frequency, and DOWN below it (radio.c analog_from_digital_rf()).
*/
// Simplified tune_config lookup for Mayhem
// Returns the IF frequency in Hz for a given target frequency
constexpr rf::Frequency praline_get_if_frequency(const rf::Frequency target_frequency) {
const uint32_t freq_mhz = target_frequency / 1'000'000;
namespace {
// Based on hackrf_usb tune_config_rx table
if (freq_mhz < 2100) {
// Most low-band frequencies: use 2375 MHz IF
// This keeps VCO around 4750-4950 MHz for FM band
return 2375'000'000;
} else if (freq_mhz < 2320) {
// Transition zone: use varying IF to avoid VCO edges
// These frequencies are tricky - near MAX2831 minimum
// Use 2425 MHz to give some margin
return 2425'000'000;
} else {
// Bypass mode or high-band - IF not used for mixer
return 0;
struct PralineTuneConfig {
uint16_t rf_range_end_mhz;
uint16_t if_mhz;
bool high_lo;
uint8_t shift; /* 0b00 none, 0b11 up, 0b01 down */
};
/* The tables below are kept column-aligned to match the reference source, so
* they are exempt from reformatting. */
// clang-format off
/* tuning table optimized for RX */
constexpr PralineTuneConfig praline_tune_config_rx[] = {
{ 0, 2360, true, 0b00},
{ 50, 2320, true, 0b11},
{ 100, 2320, true, 0b01},
{ 140, 2320, true, 0b11},
{ 406, 2560, true, 0b11},
{ 511, 2380, true, 0b11},
{ 578, 2560, true, 0b01},
{ 741, 2340, true, 0b11},
{ 861, 2560, true, 0b01},
{ 921, 2560, true, 0b11},
{ 1049, 2340, true, 0b01},
{ 1169, 2380, true, 0b11},
{ 1360, 2340, true, 0b11},
{ 1544, 2560, true, 0b01},
{ 1675, 2560, true, 0b11},
{ 1992, 2380, true, 0b01},
{ 2070, 2340, true, 0b01},
{ 2150, 2360, true, 0b01},
{ 2168, 2560, false, 0b11},
{ 2185, 2580, false, 0b11},
{ 2202, 2580, false, 0b01},
{ 2205, 2520, false, 0b11},
{ 2216, 2560, false, 0b11},
{ 2223, 2540, false, 0b11},
{ 2234, 2580, false, 0b11},
{ 2240, 2560, false, 0b11},
{ 2251, 2580, false, 0b01},
{ 2258, 2580, false, 0b11},
{ 2265, 2540, false, 0b01},
{ 2271, 2580, false, 0b11},
{ 2273, 2560, false, 0b11},
{ 2275, 2580, false, 0b01},
{ 2280, 2500, false, 0b01},
{ 2284, 2540, false, 0b11},
{ 2289, 2580, false, 0b01},
{ 2293, 2540, false, 0b01},
{ 2298, 2520, false, 0b01},
{ 2300, 2580, false, 0b11},
{ 2302, 2540, false, 0b01},
{ 2309, 2560, false, 0b01},
{ 2311, 2580, false, 0b01},
{ 2314, 2540, false, 0b11},
{ 2315, 2540, false, 0b01},
{ 2320, 2580, false, 0b11},
{ 2380, 0, false, 0b11},
{ 2440, 0, false, 0b01},
{ 2500, 0, false, 0b11},
{ 2580, 0, false, 0b01},
{ 2583, 2360, false, 0b11},
{ 2584, 2380, false, 0b11},
{ 2587, 2340, false, 0b11},
{ 2593, 2340, false, 0b01},
{ 2607, 2340, false, 0b11},
{ 2609, 2360, false, 0b11},
{ 2615, 2360, false, 0b01},
{ 2627, 2340, false, 0b01},
{ 2629, 2360, false, 0b01},
{ 2631, 2380, false, 0b11},
{ 2644, 2340, false, 0b11},
{ 2649, 2380, false, 0b11},
{ 2651, 2380, false, 0b01},
{ 2654, 2500, false, 0b11},
{ 2665, 2360, false, 0b11},
{ 2669, 2380, false, 0b01},
{ 2672, 2360, false, 0b01},
{ 2682, 2340, false, 0b11},
{ 2687, 2380, false, 0b11},
{ 2692, 2340, false, 0b11},
{ 2695, 2500, false, 0b11},
{ 2705, 2360, false, 0b11},
{ 2707, 2380, false, 0b01},
{ 2712, 2340, false, 0b01},
{ 2717, 2520, false, 0b11},
{ 2728, 2380, false, 0b11},
{ 2730, 2560, false, 0b11},
{ 2734, 2500, false, 0b11},
{ 2758, 2340, false, 0b11},
{ 2780, 2360, false, 0b11},
{ 2787, 2520, false, 0b11},
{ 2802, 2380, false, 0b11},
{ 2809, 2540, false, 0b11},
{ 2822, 2380, false, 0b01},
{ 2831, 2560, false, 0b11},
{ 2854, 2340, false, 0b11},
{ 2875, 2360, false, 0b11},
{ 2898, 2380, false, 0b11},
{ 2918, 2380, false, 0b01},
{ 2936, 2520, false, 0b01},
{ 2944, 2380, false, 0b01},
{ 2959, 2560, false, 0b11},
{ 2976, 2340, false, 0b11},
{ 2985, 2500, false, 0b01},
{ 3003, 2340, false, 0b11},
{ 3009, 2540, false, 0b11},
{ 3027, 2380, false, 0b11},
{ 3034, 2560, false, 0b11},
{ 3050, 2380, false, 0b01},
{ 3069, 2500, false, 0b11},
{ 3094, 2520, false, 0b11},
{ 3119, 2540, false, 0b11},
{ 3144, 2560, false, 0b11},
{ 3169, 2560, false, 0b01},
{ 3180, 2500, false, 0b11},
{ 3204, 2340, false, 0b11},
{ 3232, 2360, false, 0b11},
{ 3292, 2340, false, 0b01},
{ 3340, 2380, false, 0b01},
{ 3369, 2340, false, 0b11},
{ 3399, 2360, false, 0b11},
{ 3429, 2380, false, 0b11},
{ 3464, 2500, false, 0b11},
{ 3489, 2520, false, 0b11},
{ 3512, 2540, false, 0b11},
{ 3551, 2500, false, 0b01},
{ 3582, 2540, false, 0b11},
{ 3611, 2560, false, 0b11},
{ 3639, 2520, false, 0b11},
{ 3729, 2340, false, 0b11},
{ 3817, 2380, false, 0b01},
{ 3942, 2360, false, 0b01},
{ 4049, 2540, false, 0b11},
{ 4134, 2500, false, 0b01},
{ 4194, 2560, false, 0b11},
{ 4353, 2520, false, 0b11},
{ 4449, 2360, false, 0b01},
{ 4562, 2500, false, 0b11},
{ 4672, 2560, false, 0b11},
{ 4769, 2540, false, 0b11},
{ 4849, 2560, false, 0b01},
{ 4889, 2560, false, 0b11},
{ 4929, 2560, false, 0b11},
{ 4969, 2560, false, 0b11},
{ 5009, 2560, false, 0b11},
{ 5049, 2560, false, 0b11},
{ 5092, 2360, false, 0b11},
{ 5209, 2340, false, 0b01},
{ 5298, 2380, false, 0b01},
{ 5468, 2340, false, 0b01},
{ 5582, 2520, false, 0b11},
{ 5702, 2340, false, 0b11},
{ 5888, 2520, false, 0b01},
{ 6092, 2340, false, 0b01},
{ 6240, 2560, false, 0b11},
{ 6609, 2340, false, 0b11},
{ 6752, 2380, false, 0b01},
{ 6930, 2520, false, 0b01},
{ 7000, 2560, false, 0b11},
{ 7070, 2560, false, 0b01},
{ 7251, 2580, false, 0b01},
{ 0, 0, false, 0b00},
};
/* tuning table optimized for TX */
constexpr PralineTuneConfig praline_tune_config_tx[] = {
{ 2100, 2375, true, 0b00},
{ 2105, 2375, false, 0b00},
{ 2115, 2425, false, 0b00},
{ 2130, 2375, false, 0b00},
{ 2150, 2425, false, 0b00},
{ 2160, 2475, false, 0b00},
{ 2175, 2425, false, 0b00},
{ 2190, 2475, false, 0b00},
{ 2195, 2425, false, 0b00},
{ 2210, 2375, false, 0b00},
{ 2248, 2425, false, 0b00},
{ 2265, 2525, false, 0b00},
{ 2300, 2425, false, 0b00},
{ 2320, 2525, false, 0b00},
{ 2580, 0, false, 0b00},
{ 3000, 2325, false, 0b00},
{ 3140, 2375, false, 0b00},
{ 3200, 2425, false, 0b00},
{ 3280, 2375, false, 0b00},
{ 3340, 2425, false, 0b00},
{ 3420, 2475, false, 0b00},
{ 3480, 2525, false, 0b00},
{ 3500, 2475, false, 0b00},
{ 3595, 2425, false, 0b00},
{ 3625, 2375, false, 0b00},
{ 3670, 2475, false, 0b00},
{ 3710, 2425, false, 0b00},
{ 3760, 2525, false, 0b00},
{ 3790, 2475, false, 0b00},
{ 3860, 2425, false, 0b00},
{ 3915, 2375, false, 0b00},
{ 4000, 2425, false, 0b00},
{ 4055, 2375, false, 0b00},
{ 4125, 2425, false, 0b00},
{ 4700, 2375, false, 0b00},
{ 4800, 2425, false, 0b00},
{ 5000, 2375, false, 0b00},
{ 5260, 2475, false, 0b00},
{ 5465, 2525, false, 0b00},
{ 5560, 2375, false, 0b00},
{ 5720, 2425, false, 0b00},
{ 5860, 2475, false, 0b00},
{ 5970, 2575, false, 0b00},
{ 6000, 2375, false, 0b00},
{ 6500, 2325, false, 0b00},
{ 6750, 2375, false, 0b00},
{ 6850, 2425, false, 0b00},
{ 6950, 2475, false, 0b00},
{ 7000, 2525, false, 0b00},
{ 7251, 2575, false, 0b00},
{ 0, 0, false, 0b00},
};
// clang-format on
/* radio.c select_tune_config(): first entry whose range end is above the
* requested frequency. The list is terminated by an all-zero entry, which is
* also what a frequency past the end of the table lands on. */
const PralineTuneConfig* select_tune_config(const rf::Frequency target_frequency, const bool transmit) {
const PralineTuneConfig* entry = transmit ? praline_tune_config_tx : praline_tune_config_rx;
const uint32_t freq_mhz = static_cast<uint32_t>(target_frequency / 1'000'000);
while ((entry->rf_range_end_mhz != 0) || (entry->if_mhz != 0)) {
if ((target_frequency == 0) || (entry->rf_range_end_mhz > freq_mhz))
break;
entry++;
}
return entry;
}
// Returns true for high-side injection, false for low-side
constexpr bool praline_use_high_side_injection(const rf::Frequency target_frequency) {
const uint32_t freq_mhz = target_frequency / 1'000'000;
// Based on hackrf_usb tune_config_rx table
if (freq_mhz < 2100) {
// Standard low-band: high-side injection
// LO = IF + RF, mixer inverts spectrum
return true;
} else if (freq_mhz < 2105) {
// Narrow transition: still high-side
return true;
} else if (freq_mhz < 2320) {
// Near MAX2831 minimum: use low-side injection
// LO = IF - RF, no spectrum inversion
return false;
} else {
// Bypass/high-band - doesn't matter, mixer bypassed
return false;
}
/* radio.c compute_offset(): a quarter of the AFE (ADC) sample rate, or zero if
* no shift is in use or the AFE rate isn't known yet. */
constexpr uint32_t quarter_shift_offset(const uint8_t shift, const uint32_t afe_rate) {
return (shift == 0) ? 0 : (afe_rate / 4);
}
/* radio.c analog_from_digital_rf(). */
rf::Frequency analog_from_digital_rf(const rf::Frequency target_frequency, const uint8_t shift, const uint32_t afe_rate) {
const rf::Frequency offset = quarter_shift_offset(shift, afe_rate);
if (shift == 0b11)
return target_frequency + offset;
if (shift == 0b01)
return (offset > target_frequency) ? (offset - target_frequency)
: (target_frequency - offset);
return target_frequency;
}
} // namespace
#endif // PRALINE
// Low band <2170 Mhz (HackRF One) or <2320 MHz (PRALINE):
constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_frequency) {
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
#ifdef PRALINE
// Use the tune_config lookup for PRALINE
return praline_get_if_frequency(target_frequency);
#else
return 2650'000'000 - (target_frequency / 7);
#endif
}
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
Config low_band(const rf::Frequency target_frequency) {
const rf::Frequency second_lo_frequency = low_band_second_lo_frequency(target_frequency);
/* Past the end of the table: no usable configuration. */
if ((entry->rf_range_end_mhz == 0) && (entry->if_mhz == 0))
return {};
#ifdef PRALINE
/* afe_rate == 0 means the caller doesn't know the ADC rate, so no LO offset
* is applied. The FPGA rotation has to be dropped with it: rotating without
* the matching offset moves the wanted signal off DC by afe_rate / 4. */
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
/* if_mhz == 0 means the mixer is bypassed and the transceiver tunes the RF
* directly; there is no first LO in that case. */
const rf::Frequency second_lo_frequency =
(entry->if_mhz == 0) ? analog_rf : (static_cast<rf::Frequency>(entry->if_mhz) * 1'000'000);
if (entry->if_mhz == 0)
return {0, second_lo_frequency, rf::path::Band::Low, false, shift};
/* The low band always runs through the low-pass image-reject filter, so
* the spectrum is inverted exactly when the first LO ends up above the IF,
* i.e. for high-side injection. This is hackrf_usb.c radio_changed():
* invert = (img_reject == RF_PATH_FILTER_LOW_PASS) && (freq_lo > freq_if)
*/
rf::Frequency first_lo_frequency;
bool mixer_invert;
if (praline_use_high_side_injection(target_frequency)) {
// High-side injection: LO = IF + RF
first_lo_frequency = second_lo_frequency + target_frequency;
if (entry->high_lo) {
first_lo_frequency = second_lo_frequency + analog_rf;
mixer_invert = true;
} else {
// Low-side injection: LO = IF - RF
first_lo_frequency = second_lo_frequency - target_frequency;
first_lo_frequency = second_lo_frequency - analog_rf;
mixer_invert = false;
}
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert, shift};
#else
(void)afe_rate;
(void)transmit;
const rf::Frequency second_lo_frequency = 2650'000'000 - (target_frequency / 7);
const rf::Frequency first_lo_frequency = target_frequency + second_lo_frequency;
const bool mixer_invert = true;
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
#endif
}
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2580 MHz (PRALINE):
Config mid_band(const rf::Frequency target_frequency) {
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2740 MHz (PRALINE):
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
#ifdef PRALINE
// For Praline with MAX2831 (2.3-2.6 GHz range)
// Frequencies 2170-2300 MHz need upconversion since they're below MAX2831 minimum
if (target_frequency < 2300'000'000) {
// Treat as low band - need mixer
return low_band(target_frequency);
}
// Frequencies 2300-2600 MHz can go direct (no RFFC5072)
else if (target_frequency <= 2600'000'000) {
const rf::Frequency second_lo_frequency = target_frequency;
const rf::Frequency first_lo_frequency = 0;
const bool mixer_invert = false;
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert};
}
// Frequencies 2600-2740 MHz need downconversion since they're above MAX2831 maximum
else {
// Treat as high band
return high_band(target_frequency);
/* radio.c select_img_reject() / tuning.c: on PRALINE the MAX2831 tunes
* direct (mixer bypassed) from 2320 to 2580 MHz. band_mid starts at
* TRANSITION = 2320 MHz, so everything below that already went to
* low_band(). */
if (target_frequency <= 2580'000'000) {
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
/* Mixer bypassed: no first LO, the MAX2831 tunes the (offset)
* analogue RF directly and the FPGA rotates it back. */
return {0, analog_rf, rf::path::Band::Mid, false, shift};
}
/* 2580-2740 MHz: above the bypass window, downconvert. */
return high_band(target_frequency);
#else
(void)afe_rate;
(void)transmit;
const rf::Frequency second_lo_frequency = target_frequency;
const rf::Frequency first_lo_frequency = 0;
const bool mixer_invert = false;
@@ -203,12 +436,12 @@ Config high_band(const rf::Frequency target_frequency) {
return {first_lo_frequency, second_lo_frequency, rf::path::Band::High, mixer_invert};
}
Config create(const rf::Frequency target_frequency) {
Config create(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
/* TODO: This is some lame code. */
if (rf::path::band_low.contains(target_frequency)) {
return low_band(target_frequency);
return low_band(target_frequency, afe_rate, transmit);
} else if (rf::path::band_mid.contains(target_frequency)) {
return mid_band(target_frequency);
return mid_band(target_frequency, afe_rate, transmit);
} else if (rf::path::band_high.contains(target_frequency)) {
return high_band(target_frequency);
} else {