From de30cbfe1bb53dda813c86b3c5788e73945b3962 Mon Sep 17 00:00:00 2001 From: qwer123 <56971466+bumblebee-2025@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:09:55 +0800 Subject: [PATCH] fix ADSB RX weak signal on pro (#3282) --- firmware/application/apps/ui_adsb_rx.cpp | 12 + firmware/application/clock_manager.cpp | 22 +- firmware/application/radio.cpp | 88 ++-- firmware/application/radio.hpp | 4 + firmware/application/receiver_model.cpp | 68 +-- firmware/application/tuning.cpp | 431 ++++++++++++++---- firmware/application/tuning.hpp | 25 +- .../PORTAPACK_APPLICATION/fpga_bridge.c | 60 ++- .../PORTAPACK_APPLICATION/fpga_bridge.h | 128 +++--- 9 files changed, 571 insertions(+), 267 deletions(-) diff --git a/firmware/application/apps/ui_adsb_rx.cpp b/firmware/application/apps/ui_adsb_rx.cpp index 0adb8410a..c84e8fe40 100644 --- a/firmware/application/apps/ui_adsb_rx.cpp +++ b/firmware/application/apps/ui_adsb_rx.cpp @@ -532,6 +532,18 @@ ADSBRxView::ADSBRxView(NavigationView& nav) { logger = std::make_unique(); logger->append(logs_dir / u"ADSB.TXT"); + /* First run only: start from the configuration that is known to receive + * ADS-B on this hardware -- LNA 32, VGA 32, RF amp ON. The first two are + * already ReceiverModel's defaults; the amp is not, and running without it + * costs about 14 dB, which is the difference between a busy list and an + * empty one. Once the user has saved settings for this app their choice + * wins, so this only sets the starting point. + * Going through the field rather than receiver_model keeps the displayed + * value in step: RFAmpField snapshots rf_amp() in its own constructor, + * which has already run by the time we get here. */ + if (!settings_.loaded()) + field_rf_amp.set_value(1); + receiver_model.enable(); baseband::set_adsb(); diff --git a/firmware/application/clock_manager.cpp b/firmware/application/clock_manager.cpp index a4f9d5ad4..b93777b8c 100644 --- a/firmware/application/clock_manager.cpp +++ b/firmware/application/clock_manager.cpp @@ -907,12 +907,24 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) { // Set FPGA RX decimation register fpga_debug_register_write(FPGA_REG_DECIM, n); - /* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN. - * We shift up by (3 * n) to compensate for CIC bit-growth. + /* No RX digital-gain register is written here. + * + * Register 0x03 used to be programmed with (3 * n + 2) as a "CIC + * bit-growth" renormalisation. The gateware has no such register: the + * RX decimator is a chain of unity-gain half-band FIRs selected by + * rx_decim (fpga/top/standard.py), and 0x03 is rx_pstep, whose top two + * bits are the quarter-rate shift. Writing a gain here silently + * cancelled the shift that set_tuning_frequency() had programmed, which + * left the analogue passband offset with no matching rotation. + * + * The shift depends on the AFE rate we just chose, so re-apply it after + * the rate change. ReceiverModel::update_sampling_rate() calls + * update_tuning_frequency() straight after this, which does exactly + * that; the write below only keeps the register consistent in between. */ - uint8_t ds = (3 * n); - ds += 2; - fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds); + fpga_debug_register_write( + FPGA_REG_RX_PSTEP, + (radio::debug::get_cached_quarter_shift() & 0b11) << FPGA_RX_QUARTER_SHIFT_SHIFT); // Re-enable FPGA processing with clean state === fpga_debug_register_write(1, 0x01); diff --git a/firmware/application/radio.cpp b/firmware/application/radio.cpp index f279c402c..a9877e90e 100644 --- a/firmware/application/radio.cpp +++ b/firmware/application/radio.cpp @@ -144,6 +144,10 @@ static rf::Direction cached_direction = rf::Direction::Receive; static bool cached_rf_amp = false; static int_fast8_t cached_lna_gain = 0; static int_fast8_t cached_vga_gain = 0; +/* FPGA quarter-rate shift mode currently programmed, in gateware encoding + * (0b00 none / 0b11 up / 0b01 down). The baseband filter width depends on it, + * so ReceiverModel reads it back through get_quarter_shift(). */ +static uint8_t cached_quarter_shift = 0; #endif void init() { @@ -182,18 +186,15 @@ void init() { fpga_set_mode(FPGA_MODE_RX); - // These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation. - fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0 - fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim - - // RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift). - fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN); - - /* RX Mode: Initialize DC Block parameters to standard Praline values. - * 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability. - */ - fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH); - fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE); + /* Boot register state, matching fpga_init() in hackrf/firmware/common/fpga.c: + * DC block on, no PRBS, no external trigger, no quarter shift, TX NCO off. + * The decimation ratio and the quarter shift are programmed later by + * ClockManager::set_sampling_frequency() and set_tuning_frequency(). */ + fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); + fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation + fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off + fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00); + cached_quarter_shift = 0; ssp1_arbiter.invalidate(); chThdSleepMilliseconds(10); // Let FPGA registers settle @@ -224,13 +225,13 @@ void set_direction(const rf::Direction new_direction) { fpga_debug_register_write(FPGA_REG_TX_PHASE_STEP, 0x00); } else { fpga_set_mode(FPGA_MODE_RX); - // RX Mode: Ensure NCO is disabled and reset digital gain - fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN); - /* RX Mode: Initialize DC Block parameters to standard Praline values. - * 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability. - */ - fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH); - fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE); + /* RX Mode: DC block on, TX NCO off. The quarter shift is re-applied by + * set_tuning_frequency(); clear it here so a stale TX/RX transition + * cannot leave a rotation programmed with no matching LO offset. */ + fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); + fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00); + fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); + cached_quarter_shift = 0; } #endif @@ -309,7 +310,19 @@ bool set_tuning_frequency(const rf::Frequency frequency) { final_frequency = final_frequency + portapack::persistent_memory::config_freq_rx_correction(); } +#ifdef PRALINE + /* The PRALINE tuning tables offset the analogue passband by a quarter of + * the ADC sample rate and have the FPGA rotate it back to DC, so the + * planner needs to know the AFE rate. See tuning.cpp. */ + const uint32_t afe_rate = portapack::clock_manager.get_sampling_frequency() + << portapack::clock_manager.get_resampling_n(); + const auto tuning_config = tuning::config::create( + final_frequency, + afe_rate, + direction == rf::Direction::Transmit); +#else const auto tuning_config = tuning::config::create(final_frequency); +#endif if (tuning_config.is_valid()) { first_if.disable(); @@ -339,6 +352,22 @@ bool set_tuning_frequency(const rf::Frequency frequency) { LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal) } + /* Program the FPGA's quarter-rate shift to match the offset the tuning + * table just applied to the analogue centre frequency. The gateware + * (hackrf/firmware/fpga/top/standard.py) takes both bits from the top + * of register 0x03 (rx_pstep): + * rx_pstep[6] -> quarter_shift.enable + * rx_pstep[7] -> quarter_shift.up + * which is exactly fpga_set_rx_quarter_shift_mode() in + * hackrf/firmware/common/fpga.c: write (mode & 0b11) << 6. + * + * These two settings MUST be programmed together. Tuning off-centre + * without the rotation puts the signal outside the decimation filter's + * passband and it disappears entirely; rotating without the offset + * moves the wanted signal off DC by the same amount. */ + cached_quarter_shift = tuning_config.quarter_shift; + fpga_debug_register_write(FPGA_REG_RX_PSTEP, (cached_quarter_shift & 0b11) << 6); + ssp1_arbiter.invalidate(); #else baseband_cpld.set_invert(mixer_invert ^ baseband_invert); @@ -469,6 +498,10 @@ int_fast8_t get_cached_lna_gain() { int_fast8_t get_cached_vga_gain() { return cached_vga_gain; } + +uint8_t get_cached_quarter_shift() { + return cached_quarter_shift; +} #endif namespace first_if { @@ -570,18 +603,11 @@ void register_write(const size_t register_number, uint32_t value) { void init() { fpga_set_mode(FPGA_MODE_RX); - // These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation. - fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0 - fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim - - // RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift). - fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN); - - /* RX Mode: Initialize DC Block parameters to standard Praline values. - * 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability. - */ - fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH); - fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE); + /* Same boot state as fpga_init() in hackrf/firmware/common/fpga.c. */ + fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); + fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation + fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off + fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00); ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer } diff --git a/firmware/application/radio.hpp b/firmware/application/radio.hpp index c68bef93d..724af9ba9 100644 --- a/firmware/application/radio.hpp +++ b/firmware/application/radio.hpp @@ -126,6 +126,10 @@ rf::Direction get_cached_direction(); bool get_cached_rf_amp(); int_fast8_t get_cached_lna_gain(); int_fast8_t get_cached_vga_gain(); + +/* FPGA RX quarter-rate shift currently programmed, in gateware encoding: + * 0b00 none, 0b11 up, 0b01 down. */ +uint8_t get_cached_quarter_shift(); #endif namespace sgpio { diff --git a/firmware/application/receiver_model.cpp b/firmware/application/receiver_model.cpp index 716286c16..87142016c 100644 --- a/firmware/application/receiver_model.cpp +++ b/firmware/application/receiver_model.cpp @@ -244,10 +244,24 @@ void ReceiverModel::enable() { radio::set_direction(rf::Direction::Receive); #ifdef PRALINE - /* Anchor the Common Mode Voltage (VCM) to 1.2V. - * This stabilizes the electrical floor of the I/Q signals. - */ - radio::set_rx_buff_vcm(1); + /* MAX2831 RX IQ common-mode voltage (register 15). + * + * 0 = 1.1 V, 1 = 1.2 V, 2 = 1.3 V, 3 = 1.45 V. + * + * The reference firmware leaves this alone: max2831.c's default register + * table has reg 15 = 0x0145 (1.1 V) and the line that would raise it is + * commented out ("maximum rx output common-mode voltage"). Mayhem used to + * force 1.2 V here on the theory that it "stabilises the electrical floor + * of the I/Q signals" -- plausible, but never measured, and it was the + * last remaining RF-path setting where this branch disagreed with the + * configuration that is proven to receive ADS-B on this board. + * + * Set to 0 to match the reference (writing 1.1 V is a no-op against the + * power-on default), or back to 1 to restore the old Mayhem behaviour. + * If reception measurably worsens, put it back to 1 and say so -- neither + * value has been verified on hardware. */ +#define PRALINE_RX_IQ_VCM 0 + radio::set_rx_buff_vcm(PRALINE_RX_IQ_VCM); #endif update_tuning_frequency(); @@ -326,47 +340,39 @@ void ReceiverModel::update_baseband_bandwidth() { if (enabled_) { #ifdef PRALINE /* - * PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c + * PRALINE LPF bandwidth, ported from auto_bandwidth() in + * hackrf/firmware/common/radio.c: * - * The LPF should be set to capture the desired signal bandwidth - * while the FPGA decimation filter handles anti-aliasing. + * bb_bandwidth = sample_rate * 3 / 4 + * lpf_bandwidth = bb_bandwidth + offset_hz * 2 * - * For most modes: LPF = (output_sample_rate * 3) / 8 - * For quarter-shift: add offset for shifted spectrum + * where offset_hz is the quarter-rate shift, i.e. afe_rate / 4 when a + * shift is in use. The doubling is because the wanted signal sits + * offset from the analogue centre, so the analogue filter has to stay + * open out to that offset on the far side too. * - * Note: MAX2831 minimum LPF is 11.6 MHz, so for narrow sample rates - * the hardware limit applies and FPGA filter does the real work. + * The previous version used /8 in both places and read the shift from + * bits 2-3 of FPGA register 1, which do not exist in the gateware, so + * it always took the no-shift branch. At the ADS-B rate that asked for + * 750 kHz, which is below the MAX2831's 1.75 MHz floor and therefore + * also switched in the external narrowband AA filter + * (MAX2831::set_lpf_rf_bandwidth_rx), squeezing the RX path shut. The + * reference asks for 17.5 MHz at the same rate and the AA filter stays + * out of circuit. */ uint32_t sample_rate = sampling_rate(); uint8_t resampling_n = portapack::clock_manager.get_resampling_n(); uint32_t afe_rate = sample_rate << resampling_n; - // Base LPF: enough to capture desired bandwidth - uint32_t lpf_bandwidth = (sample_rate * 3) / 8; - - // Check if quarter-shift is enabled (FPGA register 1, bits 2-3) - uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); - uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03; + uint32_t lpf_bandwidth = (sample_rate * 3) / 4; + const uint8_t quarter_shift = radio::debug::get_cached_quarter_shift(); if (quarter_shift != 0) { - // Quarter-shift moves spectrum by AFE_rate/4, need wider LPF - uint32_t offset = afe_rate / 8; + const uint32_t offset = afe_rate / 4; lpf_bandwidth += offset * 2; } - // For best anti-alias performance, also consider AFE Nyquist - // If our calculated LPF is below MAX2831 minimum, it doesn't matter - // But if we can set LPF to just below AFE Nyquist, that's optimal - uint32_t afe_nyquist = afe_rate / 2; - - // Use the larger of: signal bandwidth requirement OR Nyquist protection - // (but MAX2831 driver will clamp to its available settings anyway) - if (lpf_bandwidth < afe_nyquist) { - // Set LPF close to Nyquist for maximum alias rejection - lpf_bandwidth = (afe_nyquist * 9) / 10; // 90% of Nyquist - } - radio::set_baseband_filter_bandwidth_rx(lpf_bandwidth); #else radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth()); diff --git a/firmware/application/tuning.cpp b/firmware/application/tuning.cpp index e1b2d74ba..27186b2c5 100644 --- a/firmware/application/tuning.cpp +++ b/firmware/application/tuning.cpp @@ -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(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(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 { diff --git a/firmware/application/tuning.hpp b/firmware/application/tuning.hpp index b16b6c5a0..710127662 100644 --- a/firmware/application/tuning.hpp +++ b/firmware/application/tuning.hpp @@ -33,18 +33,21 @@ struct Config { : first_lo_frequency(0), second_lo_frequency(0), rf_path_band(rf::path::Band::Mid), - mixer_invert(false) { + mixer_invert(false), + quarter_shift(0) { } constexpr Config( rf::Frequency first_lo_frequency, rf::Frequency second_lo_frequency, rf::path::Band rf_path_band, - bool mixer_invert) + bool mixer_invert, + uint8_t quarter_shift = 0) : first_lo_frequency(first_lo_frequency), second_lo_frequency(second_lo_frequency), rf_path_band(rf_path_band), - mixer_invert(mixer_invert) { + mixer_invert(mixer_invert), + quarter_shift(quarter_shift) { } bool is_valid() const { @@ -55,9 +58,23 @@ struct Config { const rf::Frequency second_lo_frequency; const rf::path::Band rf_path_band; const bool mixer_invert; + + /* PRALINE only: FPGA RX quarter-rate shift mode, in the encoding the + * gateware expects in the top two bits of register 0x03 (rx_pstep): + * 0b00 = none, 0b11 = up, 0b01 = down. + * Matches fpga_quarter_shift_mode_t in hackrf/firmware/common/fpga.h. + * Always 0 on HackRF One (no FPGA). */ + const uint8_t quarter_shift; }; -Config create(const rf::Frequency target_frequency); +/* afe_rate is the ADC sample rate in Hz (output rate << decimation), needed on + * PRALINE to work out how far off centre the quarter-rate shift places the + * analogue passband. Pass 0 (or leave defaulted) to disable the shift. + * transmit selects the TX tuning table. Both are ignored on HackRF One. */ +Config create( + const rf::Frequency target_frequency, + const uint32_t afe_rate = 0, + const bool transmit = false); } /* namespace config */ } /* namespace tuning */ diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c index 495b9f902..77fa49517 100644 --- a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c @@ -125,10 +125,11 @@ // ============================================================================ // Canonical Default Values - SINGLE SOURCE OF TRUTH // ============================================================================ -/* Define the canonical RX defaults in ONE place */ -#define FPGA_RX_DEFAULT_DC_WIDTH 0x04 /* Typical for 40MHz stability */ -#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08 /* Typical for 40MHz stability */ -#define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00 /* No shift initially */ +/* Define the canonical RX defaults in ONE place. + * Matches fpga_init() in hackrf/firmware/common/fpga.c: the gateware only has + * rx_decim and rx_pstep on the RX side, both starting at zero. */ +#define FPGA_RX_DEFAULT_DECIM 0x00 /* No decimation initially */ +#define FPGA_RX_DEFAULT_PSTEP 0x00 /* No quarter-rate shift initially */ /* Define TX defaults */ #define FPGA_TX_DEFAULT_NCO_CTRL 0x00 /* NCO disabled */ @@ -221,12 +222,13 @@ static bool fpga_cdone_read(void) { // These functions allow reading/writing FPGA internal registers via SPI. // The FPGA bitstream implements a simple SPI register interface. // -// FPGA Register Map: -// Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7) -// Reg 2 (RX_DECIM): Decimation ratio [2:0] -// Reg 3 (RX/TX): RX Digital Shift OR TX NCO Control -// Reg 4 (RX_DC_BLOCK_WIDTH/TX_INTERP) [2:0] -// Reg 5 (RX_DC_ADAPT_RATE/TX_PSTEP) [7:0] +// FPGA Register Map (hackrf/firmware/fpga/top/standard.py): +// Reg 1 (CTRL): DC_BLOCK(b0), PRBS(b6), TRIGGER_EN(b7) +// Reg 2 (RX_DECIM): Decimation ratio, log2 [2:0] +// Reg 3 (RX_PSTEP): QUARTER_SHIFT_EN(b6), QUARTER_SHIFT_UP(b7) +// Reg 4 (TX_CTRL): NCO enable (b0) +// Reg 5 (TX_INTRP): Interpolation ratio [2:0] +// Reg 6 (TX_PSTEP): NCO phase step [7:0] // // SPI Protocol: // Read: Send [reg & 0x7F, 0x00, 0x00] -> value in byte 3 @@ -369,22 +371,16 @@ void fpga_rx_enable_dc_block(bool enable) { } /* RX Functions with mode assertion */ -void fpga_rx_set_digital_gain(uint8_t shift) { + +/* Quarter-rate shift, register 0x03 bits [7:6]. Equivalent to + * fpga_set_rx_quarter_shift_mode() in hackrf/firmware/common/fpga.c. + * mode: 0b00 none, 0b11 up, 0b01 down. */ +void fpga_rx_set_quarter_shift_mode(uint8_t mode) { if (current_mode != FPGA_MODE_RX) { /* Log error or assert - wrong mode! */ return; } - fpga_register_write(FPGA_REG_SHARED_3, shift & FPGA_RX_GAIN_SHIFT_MASK); -} - -void fpga_rx_set_dc_block_width(uint8_t width) { - if (current_mode != FPGA_MODE_RX) return; - fpga_register_write(FPGA_REG_SHARED_4, width & FPGA_RX_DC_WIDTH_MASK); -} - -void fpga_rx_set_dc_adapt_rate(uint8_t rate) { - if (current_mode != FPGA_MODE_RX) return; - fpga_register_write(FPGA_REG_SHARED_5, rate); + fpga_register_write(FPGA_REG_RX_PSTEP, (uint8_t)((mode & 0x03) << FPGA_RX_QUARTER_SHIFT_SHIFT)); } // ============================================================================ @@ -394,12 +390,12 @@ void fpga_rx_set_dc_adapt_rate(uint8_t rate) { /* TX Functions with mode assertion */ void fpga_tx_set_nco_enable(bool enable) { if (current_mode != FPGA_MODE_TX) return; - uint8_t val = fpga_register_read(FPGA_REG3_TX_NCO_CTRL); + uint8_t val = fpga_register_read(FPGA_REG_TX_CONTROL); if (enable) val |= FPGA_TX_NCO_EN; else val &= ~FPGA_TX_NCO_EN; - fpga_register_write(FPGA_REG3_TX_NCO_CTRL, val); + fpga_register_write(FPGA_REG_TX_CONTROL, val); } void fpga_tx_set_interpolation(uint8_t ratio) { @@ -424,17 +420,17 @@ static void fpga_register_init(void) { current_mode = FPGA_MODE_RX; fpga_spi_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); - fpga_spi_write(FPGA_REG_DECIM, 0x00); - fpga_spi_write(FPGA_REG_SHARED_3, FPGA_RX_DEFAULT_DIGITAL_GAIN); - fpga_spi_write(FPGA_REG_SHARED_4, FPGA_RX_DEFAULT_DC_WIDTH); - fpga_spi_write(FPGA_REG_SHARED_5, FPGA_RX_DEFAULT_ADAPT_RATE); + fpga_spi_write(FPGA_REG_DECIM, FPGA_RX_DEFAULT_DECIM); + fpga_spi_write(FPGA_REG_RX_PSTEP, FPGA_RX_DEFAULT_PSTEP); + fpga_spi_write(FPGA_REG_TX_CONTROL, FPGA_TX_DEFAULT_NCO_CTRL); + fpga_spi_write(FPGA_REG_TX_INTERP, FPGA_TX_DEFAULT_INTERP); /* Update cache */ fpga_reg_cache[1] = FPGA_CTRL_DC_BLOCK_EN; - fpga_reg_cache[2] = 0x00; - fpga_reg_cache[3] = FPGA_RX_DEFAULT_DIGITAL_GAIN; - fpga_reg_cache[4] = FPGA_RX_DEFAULT_DC_WIDTH; - fpga_reg_cache[5] = FPGA_RX_DEFAULT_ADAPT_RATE; + fpga_reg_cache[2] = FPGA_RX_DEFAULT_DECIM; + fpga_reg_cache[3] = FPGA_RX_DEFAULT_PSTEP; + fpga_reg_cache[4] = FPGA_TX_DEFAULT_NCO_CTRL; + fpga_reg_cache[5] = FPGA_TX_DEFAULT_INTERP; } // ============================================================================ diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h index 16d0bb661..8835ded02 100644 --- a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h @@ -16,10 +16,23 @@ extern "C" { #ifdef PRALINE -/* RX path (legacy PRALINE software map kept for compatibility) */ -#define FPGA_REG_RX_DIGITAL_GAIN 0x03 /* Digital Shift / scaling (RX Mode) */ -#define FPGA_REG_RX_DC_BLOCK_WIDTH 0x04 /* Notch filter cutoff (RX Mode) */ -#define FPGA_REG_RX_DC_ADAPT_RATE 0x05 /* Settle time/Integration (RX Mode) */ +/* + * RX path. + * + * NOTE: the register map below is the one the loaded bitstream actually + * implements. Ground truth is hackrf/firmware/fpga/top/standard.py plus + * hackrf/firmware/common/fpga_regs.def; Mayhem, hackrf_usb and debug_for_adsb + * all load a byte-identical praline_fpga.bin, so that map applies here too. + * + * An earlier "legacy PRALINE software map" claimed register 0x03 was an RX + * digital gain and 0x04/0x05 were DC-block width / adaptation rate. The + * gateware has none of those: 0x03 is rx_pstep (whose top two bits are the + * quarter-rate shift) and 0x04/0x05 are TX registers. Writing the old "RX DC + * width" value of 0x01 to 0x04 actually set tx_ctrl[0] and switched the TX NCO + * on, and every write of the fictional digital gain to 0x03 cleared the + * quarter shift. + */ +#define FPGA_REG_RX_PSTEP 0x03 /* RX phase step; bits [7:6] = quarter shift */ /* * TX path must match the currently built PRALINE standard gateware in @@ -43,70 +56,55 @@ typedef enum { /* * FPGA Register Addresses - * NOTE: - * The currently loaded PRALINE standard gateware uses: - * 0x01 CTRL - * 0x02 RX_DECIM - * 0x03 RX_DIGITAL_GAIN (RX) / TX_NCO_CTRL (TX) - * 0x04 TX_CTRL - * 0x05 TX_INTRP - * 0x06 TX_PSTEP + * + * From hackrf/firmware/fpga/top/standard.py (spi_regs.add_register): + * 0x01 ctrl 8 bits + * 0x02 rx_decim 3 bits + * 0x03 rx_pstep 8 bits + * 0x04 tx_ctrl 1 bit + * 0x05 tx_intrp 3 bits + * 0x06 tx_pstep 8 bits + * + * There is no RX gain register: rx_decim selects half-band FIR stages + * (hbfir1..hbfir5), which are unity-gain, so there is no CIC bit growth to + * renormalise. */ -#define FPGA_REG_CTRL 0x01 /* Control register */ -#define FPGA_REG_DECIM 0x02 /* RX decimation */ -#define FPGA_REG_SHARED_3 0x03 /* Legacy shared register */ -#define FPGA_REG_SHARED_4 0x04 /* Legacy shared register */ -#define FPGA_REG_SHARED_5 0x05 /* Legacy shared register */ -#define FPGA_REG_SHARED_6 0x06 /* TX phase step */ +#define FPGA_REG_CTRL 0x01 /* Control register */ +#define FPGA_REG_DECIM 0x02 /* RX decimation (log2, bits [2:0]) */ +/* 0x03..0x06 are FPGA_REG_RX_PSTEP / FPGA_REG_TX_CONTROL / + * FPGA_REG_TX_INTERP / FPGA_REG_TX_PHASE_STEP, defined above. */ /* * Register 1 (CTRL) Bit Definitions + * standard.py: ctrl[0] -> dc_block.enable, ctrl[6] -> prbs, ctrl[7] -> trigger_en. + * Nothing else in this register is decoded. */ -#define FPGA_CTRL_DC_BLOCK_EN (1 << 0) /* DC block enable */ -#define FPGA_CTRL_QUARTER_SHIFT_EN (1 << 1) /* Quarter-rate shift enable */ -#define FPGA_CTRL_QUARTER_SHIFT_UP (1 << 2) /* Shift direction: 1=up, 0=down */ -#define FPGA_CTRL_TX_MODE (1 << 5) /* TX mode indicator (if applicable) */ -#define FPGA_CTRL_PRBS_EN (1 << 6) /* PRBS test mode */ -#define FPGA_CTRL_TRIGGER_EN (1 << 7) /* External trigger enable */ +#define FPGA_CTRL_DC_BLOCK_EN (1 << 0) /* DC block enable */ +#define FPGA_CTRL_PRBS_EN (1 << 6) /* PRBS test mode */ +#define FPGA_CTRL_TRIGGER_EN (1 << 7) /* External trigger enable */ /* - * Register 3 Dual-Purpose Definitions + * Register 3 (RX_PSTEP) Bit Definitions + * standard.py: rx_pstep[6] -> quarter_shift.enable, rx_pstep[7] -> quarter_shift.up. + * Same encoding as fpga_quarter_shift_mode_t << 6 in hackrf/firmware/common/fpga.c. */ -/* RX Mode: Digital gain/shift */ -#define FPGA_REG3_RX_DIGITAL_GAIN 0x03 -#define FPGA_RX_GAIN_SHIFT_MASK 0x0F /* Bits [3:0] - shift amount */ +/* Position of that 2-bit field inside rx_pstep, for direct register writes. */ +#define FPGA_RX_QUARTER_SHIFT_SHIFT 6 +#define FPGA_RX_QUARTER_SHIFT_MASK 0xC0 -/* TX Mode: NCO control */ -#define FPGA_REG3_TX_NCO_CTRL 0x03 -#define FPGA_TX_NCO_EN (1 << 0) /* NCO enable */ -#define FPGA_TX_NCO_INVERT (1 << 1) /* Invert spectrum */ +/* Mode values, matching fpga_quarter_shift_mode_t in + * hackrf/firmware/common/fpga.h. Pass these to + * fpga_rx_set_quarter_shift_mode(), which shifts them into place. */ +#define FPGA_QUARTER_SHIFT_MODE_NONE 0b00 +#define FPGA_QUARTER_SHIFT_MODE_UP 0b11 +#define FPGA_QUARTER_SHIFT_MODE_DOWN 0b01 /* - * Register 4 Dual-Purpose Definitions + * Register 4 (TX_CTRL) / 5 (TX_INTRP) / 6 (TX_PSTEP) Bit Definitions */ -/* RX Mode: DC block notch width */ -#define FPGA_REG4_RX_DC_WIDTH 0x04 -#define FPGA_RX_DC_WIDTH_MASK 0x07 /* Bits [2:0] */ - -/* TX Mode: Interpolation ratio */ -#define FPGA_REG4_TX_INTERP 0x05 -#define FPGA_TX_INTERP_MASK 0x07 /* Bits [2:0] */ - -/* - * Register 5 Dual-Purpose Definitions - */ -/* RX Mode: DC block adaptation rate */ -#define FPGA_REG5_RX_DC_RATE 0x05 -#define FPGA_RX_DC_RATE_MASK 0xFF /* Bits [7:0] */ - -/* TX Mode: NCO phase step (frequency) */ -#define FPGA_REG5_TX_PHASE_STEP 0x06 -#define FPGA_TX_PHASE_STEP_MASK 0xFF /* Bits [7:0] */ - -/* Export default values so other methods can use them */ -#define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00 -#define FPGA_RX_DEFAULT_DC_WIDTH 0x04 -#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08 +#define FPGA_TX_NCO_EN (1 << 0) /* tx_ctrl[0]: NCO enable */ +#define FPGA_TX_INTERP_MASK 0x07 /* tx_intrp bits [2:0] */ +#define FPGA_TX_PHASE_STEP_MASK 0xFF /* tx_pstep bits [7:0] */ /* * Core Functions @@ -133,9 +131,8 @@ void fpga_register_write(uint8_t reg, uint8_t value); * RX Mode Functions (only valid when mode == FPGA_MODE_RX) */ void fpga_rx_set_decimation(uint8_t ratio); -void fpga_rx_set_digital_gain(uint8_t shift); -void fpga_rx_set_dc_block_width(uint8_t width); -void fpga_rx_set_dc_adapt_rate(uint8_t rate); +/* mode is the gateware encoding: 0b00 none, 0b11 up, 0b01 down. */ +void fpga_rx_set_quarter_shift_mode(uint8_t mode); void fpga_rx_enable_dc_block(bool enable); /* @@ -154,12 +151,13 @@ void fpga_tx_set_phase_step(uint8_t step); * reg: Register number (1-5) * Returns: Register value, or 0xFF if invalid register * - * FPGA Register Map: - * Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7) - * Reg 2 (RX_DECIM): Decimation ratio [2:0] - * Reg 3 (RX/TX): RX Digital Shift OR TX NCO Control - * Reg 4 (RX_DC_BLOCK_WIDTH/TX_INTERP) [2:0] - * Reg 5 (RX_DC_ADAPT_RATE/TX_PSTEP) [7:0] + * FPGA Register Map (hackrf/firmware/fpga/top/standard.py): + * Reg 1 (CTRL): DC_BLOCK(b0), PRBS(b6), TRIGGER_EN(b7) + * Reg 2 (RX_DECIM): Decimation ratio, log2 [2:0] + * Reg 3 (RX_PSTEP): QUARTER_SHIFT_EN(b6), QUARTER_SHIFT_UP(b7) + * Reg 4 (TX_CTRL): NCO_EN(b0) + * Reg 5 (TX_INTRP): Interpolation ratio, log2 [2:0] + * Reg 6 (TX_PSTEP): NCO phase step [7:0] */ uint8_t fpga_debug_register_read(uint8_t reg);