diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6af5353d..eaa1d982 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -159,7 +159,7 @@ jobs: strategy: matrix: os: ['macos', 'ubuntu', 'windows'] - board: ['HACKRF_ONE', 'JAWBREAKER', 'RAD1O'] + board: ['HACKRF_ONE', 'JAWBREAKER', 'RAD1O', 'PRALINE'] cmake: ['3.10.0', 'latest'] exclude: - os: 'windows' diff --git a/firmware/blinky/blinky.c b/firmware/blinky/blinky.c index e26d5f83..95b1173e 100644 --- a/firmware/blinky/blinky.c +++ b/firmware/blinky/blinky.c @@ -27,8 +27,13 @@ int main(void) detect_hardware_platform(); pin_setup(); +#ifndef PRALINE /* enable 1V8 power supply so that the 1V8 LED lights up */ enable_1v8_power(); +#else + /* enable 1V2 power supply so that the 3V3FPGA LED lights up */ + enable_1v2_power(); +#endif /* Blink LED1/2/3 on the board. */ while (1) diff --git a/firmware/common/LPC4320_M4_memory.ld b/firmware/common/LPC4320_M4_memory.ld index 976e7855..a5997f32 100644 --- a/firmware/common/LPC4320_M4_memory.ld +++ b/firmware/common/LPC4320_M4_memory.ld @@ -25,7 +25,7 @@ MEMORY { /* rom is really the shadow region that points to SPI flash or elsewhere */ - rom (rx) : ORIGIN = 0x00000000, LENGTH = 96K + rom (rx) : ORIGIN = 0x00000000, LENGTH = 1M ram_local1 (rwx) : ORIGIN = 0x10000000, LENGTH = 96K ram_local2 (rwx) : ORIGIN = 0x10080000, LENGTH = 32K ram_sleep (rwx) : ORIGIN = 0x10088000, LENGTH = 8K diff --git a/firmware/common/LPC43xx_M4_memory_rom_only.ld b/firmware/common/LPC43xx_M4_memory_rom_only.ld new file mode 100644 index 00000000..c0213a7a --- /dev/null +++ b/firmware/common/LPC43xx_M4_memory_rom_only.ld @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2025 Great Scott Gadgets + * Copyright 2012 Jared Boone + * + * This file is part of HackRF + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +SECTIONS +{ + /* ROM-only section */ + .rom_only : { + . = ALIGN(4); + KEEP(*(.rom_only)) + . = ALIGN(4); + } > rom +} \ No newline at end of file diff --git a/firmware/common/cpld_jtag.c b/firmware/common/cpld_jtag.c index d80a99d4..1641499b 100644 --- a/firmware/common/cpld_jtag.c +++ b/firmware/common/cpld_jtag.c @@ -25,9 +25,11 @@ #include #include +#ifndef PRALINE static refill_buffer_cb refill_buffer; static uint32_t xsvf_buffer_len, xsvf_pos; static unsigned char* xsvf_buffer; +#endif void cpld_jtag_take(jtag_t* const jtag) { @@ -36,22 +38,26 @@ void cpld_jtag_take(jtag_t* const jtag) /* Set initial GPIO state to the voltages of the internal or external pull-ups/downs, * to avoid any glitches. */ -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) gpio_set(gpio->gpio_pp_tms); #endif + gpio_clear(gpio->gpio_tck); +#ifndef PRALINE gpio_set(gpio->gpio_tms); gpio_set(gpio->gpio_tdi); - gpio_clear(gpio->gpio_tck); +#endif -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) /* Do not drive PortaPack-specific TMS pin initially, just to be cautious. */ gpio_input(gpio->gpio_pp_tms); gpio_input(gpio->gpio_pp_tdo); #endif + gpio_output(gpio->gpio_tck); +#ifndef PRALINE gpio_output(gpio->gpio_tms); gpio_output(gpio->gpio_tdi); - gpio_output(gpio->gpio_tck); gpio_input(gpio->gpio_tdo); +#endif } void cpld_jtag_release(jtag_t* const jtag) @@ -61,17 +67,20 @@ void cpld_jtag_release(jtag_t* const jtag) /* Make all pins inputs when JTAG interface not active. * Let the pull-ups/downs do the work. */ -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) /* Do not drive PortaPack-specific pins, initially, just to be cautious. */ gpio_input(gpio->gpio_pp_tms); gpio_input(gpio->gpio_pp_tdo); #endif + gpio_input(gpio->gpio_tck); +#ifndef PRALINE gpio_input(gpio->gpio_tms); gpio_input(gpio->gpio_tdi); - gpio_input(gpio->gpio_tck); gpio_input(gpio->gpio_tdo); +#endif } +#ifndef PRALINE /* return 0 if success else return error code see xsvfExecute() */ int cpld_jtag_program( jtag_t* const jtag, @@ -102,3 +111,4 @@ unsigned char cpld_jtag_get_next_byte(void) xsvf_pos++; return byte; } +#endif \ No newline at end of file diff --git a/firmware/common/cpld_jtag.h b/firmware/common/cpld_jtag.h index 39168ccc..9219dbcb 100644 --- a/firmware/common/cpld_jtag.h +++ b/firmware/common/cpld_jtag.h @@ -27,11 +27,13 @@ #include "gpio.h" typedef struct jtag_gpio_t { - gpio_t gpio_tms; gpio_t gpio_tck; +#ifndef PRALINE + gpio_t gpio_tms; gpio_t gpio_tdi; gpio_t gpio_tdo; -#ifdef HACKRF_ONE +#endif +#if (defined HACKRF_ONE || defined PRALINE) gpio_t gpio_pp_tms; gpio_t gpio_pp_tdo; #endif diff --git a/firmware/common/firmware_info.c b/firmware/common/firmware_info.c index b64894b1..2b3d25ff 100644 --- a/firmware/common/firmware_info.c +++ b/firmware/common/firmware_info.c @@ -33,6 +33,8 @@ #define SUPPORTED_PLATFORM (PLATFORM_HACKRF1_OG | PLATFORM_HACKRF1_R9) #elif RAD1O #define SUPPORTED_PLATFORM PLATFORM_RAD1O +#elif PRALINE + #define SUPPORTED_PLATFORM PLATFORM_PRALINE #else #define SUPPORTED_PLATFORM 0 #endif diff --git a/firmware/common/fpga.c b/firmware/common/fpga.c new file mode 100644 index 00000000..3cd91f12 --- /dev/null +++ b/firmware/common/fpga.c @@ -0,0 +1,227 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include +#include "hackrf_core.h" +#include "ice40_spi.h" +#include "lz4_blk.h" +#include "m0_state.h" +#include "streaming.h" +#include "rf_path.h" +#include "selftest.h" + +// FPGA bitstreams blob. +extern uint32_t _binary_fpga_bin_start; +extern uint32_t _binary_fpga_bin_end; +extern uint32_t _binary_fpga_bin_size; + +// USB buffer used during selftests. +#define USB_BULK_BUFFER_SIZE 0x8000 +extern uint8_t usb_bulk_buffer[USB_BULK_BUFFER_SIZE]; + +struct fpga_image_read_ctx { + uint32_t addr; + size_t next_block_sz; + uint8_t init_flag; + uint8_t buffer[4096 + 2]; +}; + +static size_t fpga_image_read_block_cb(void* _ctx, uint8_t* out_buffer) +{ + // Assume out_buffer is 4KB + struct fpga_image_read_ctx* ctx = _ctx; + size_t block_sz = ctx->next_block_sz; + + // first iteration: read first block size + if (ctx->init_flag == 0) { + w25q80bv_read(&spi_flash, ctx->addr, 2, ctx->buffer); + block_sz = ctx->buffer[0] | (ctx->buffer[1] << 8); + ctx->addr += 2; + ctx->init_flag = 1; + } + + // finish at end marker + if (block_sz == 0) + return 0; + + // Read compressed block (and the next block size) from flash. + w25q80bv_read(&spi_flash, ctx->addr, block_sz + 2, ctx->buffer); + ctx->addr += block_sz + 2; + ctx->next_block_sz = ctx->buffer[block_sz] | (ctx->buffer[block_sz + 1] << 8); + + // Decompress block. + return lz4_blk_decompress(ctx->buffer, out_buffer, block_sz); +} + +bool fpga_image_load(unsigned int index) +{ +#if defined(DFU_MODE) || defined(RAM_MODE) + return false; +#endif + + // TODO: do SPI setup and read number of bitstreams once! + + // Prepare for SPI flash access. + spi_bus_start(spi_flash.bus, &ssp_config_w25q80bv); + w25q80bv_setup(&spi_flash); + + // Read number of bitstreams from flash. + // Check the bitstream exists, and extract its offset. + uint32_t addr = (uint32_t) &_binary_fpga_bin_start; + uint32_t num_bitstreams, bitstream_offset; + w25q80bv_read(&spi_flash, addr, 4, (uint8_t*) &num_bitstreams); + if (index >= num_bitstreams) + return false; + w25q80bv_read(&spi_flash, addr + 4 * (index + 1), 4, (uint8_t*) &bitstream_offset); + + // A callback function is used by the FPGA programmer + // to obtain consecutive gateware chunks. + ssp1_set_mode_ice40(); + ice40_spi_target_init(&ice40); + struct fpga_image_read_ctx fpga_image_ctx = { + .addr = (uint32_t) &_binary_fpga_bin_start + bitstream_offset, + }; + const bool success = ice40_spi_syscfg_program( + &ice40, + fpga_image_read_block_cb, + &fpga_image_ctx); + ssp1_set_mode_max283x(); + + return success; +} + +static uint8_t lfsr_advance(uint8_t v) +{ + const uint8_t feedback = ((v >> 3) ^ (v >> 4) ^ (v >> 5) ^ (v >> 7)) & 1; + return (v << 1) | feedback; +} + +bool fpga_sgpio_selftest() +{ +#if defined(DFU_MODE) || defined(RAM_MODE) + return true; +#endif + + // Enable PRBS mode. + ssp1_set_mode_ice40(); + ice40_spi_write(&ice40, 0x01, 0x40); + ssp1_set_mode_max283x(); + + // Stream 512 samples from the FPGA. + sgpio_configure(&sgpio_config, SGPIO_DIRECTION_RX); + m0_set_mode(M0_MODE_RX); + m0_state.shortfall_limit = 0; + baseband_streaming_enable(&sgpio_config); + while (m0_state.m0_count < 512) + ; + baseband_streaming_disable(&sgpio_config); + m0_set_mode(M0_MODE_IDLE); + + // Disable PRBS mode. + ssp1_set_mode_ice40(); + ice40_spi_write(&ice40, 0x01, 0); + ssp1_set_mode_max283x(); + + // Generate sequence from first value and compare. + bool seq_in_sync = true; + uint8_t seq = lfsr_advance(usb_bulk_buffer[0]); + for (int i = 1; i < 512; ++i) { + if (usb_bulk_buffer[i] != seq) { + seq_in_sync = false; + break; + } + seq = lfsr_advance(seq); + } + + // Update selftest result. + selftest.sgpio_rx_ok = seq_in_sync; + if (!selftest.sgpio_rx_ok) { + selftest.report.pass = false; + } + + return selftest.sgpio_rx_ok; +} + +bool fpga_if_xcvr_selftest() +{ +#if defined(DFU_MODE) || defined(RAM_MODE) + return true; +#endif + + const size_t num_samples = USB_BULK_BUFFER_SIZE / 2; + + // Set gateware features for the test. + ssp1_set_mode_ice40(); + ice40_spi_write(&ice40, 0x01, 0x1); // RX DC block + ice40_spi_write(&ice40, 0x05, 128); // NCO phase increment + ice40_spi_write(&ice40, 0x03, 1); // NCO TX enable + ssp1_set_mode_max283x(); + + // Configure RX calibration path and settle for 1ms. + rf_path_set_direction(&rf_path, RF_PATH_DIRECTION_RX_CALIBRATION); + delay_us_at_mhz(1000, 204); + + // Stream samples from the FPGA. + m0_set_mode(M0_MODE_RX); + m0_state.shortfall_limit = 0; + baseband_streaming_enable(&sgpio_config); + while (m0_state.m0_count < num_samples) + ; + baseband_streaming_disable(&sgpio_config); + m0_set_mode(M0_MODE_IDLE); + + rf_path_set_direction(&rf_path, RF_PATH_DIRECTION_OFF); + + // Gateware default settings. + ssp1_set_mode_ice40(); + ice40_spi_write(&ice40, 0x01, 0); + ice40_spi_write(&ice40, 0x03, 0); + ssp1_set_mode_max283x(); + + // Count zero crossings in the received samples. + // N/2 samples/channel * 2 zcs/cycle / 8 samples/cycle = N/8 zcs/channel + unsigned int expected_zcs = num_samples / 8; + + unsigned int zcs_i = 0; + unsigned int zcs_q = 0; + uint8_t last_sign_i = 0; + uint8_t last_sign_q = 0; + for (size_t i = 0; i < num_samples; i += 2) { + uint8_t sign_i = (usb_bulk_buffer[i] & 0x80) ? 1 : 0; + uint8_t sign_q = (usb_bulk_buffer[i + 1] & 0x80) ? 1 : 0; + zcs_i += sign_i ^ last_sign_i; + zcs_q += sign_q ^ last_sign_q; + last_sign_i = sign_i; + last_sign_q = sign_q; + } + + // Allow a zero crossings counting error of +-5%. + bool i_in_range = (zcs_i > expected_zcs * 0.95) && (zcs_i < expected_zcs * 1.05); + bool q_in_range = (zcs_q > expected_zcs * 0.95) && (zcs_q < expected_zcs * 1.05); + + // Update selftest result. + selftest.xcvr_loopback_ok = i_in_range && q_in_range; + if (!selftest.xcvr_loopback_ok) { + selftest.report.pass = false; + } + + return selftest.xcvr_loopback_ok; +} diff --git a/firmware/common/fpga.h b/firmware/common/fpga.h new file mode 100644 index 00000000..b28c062b --- /dev/null +++ b/firmware/common/fpga.h @@ -0,0 +1,31 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __FPGA_H +#define __FPGA_H + +#include + +bool fpga_image_load(unsigned int index); +bool fpga_sgpio_selftest(); +bool fpga_if_xcvr_selftest(); + +#endif // __FPGA_H diff --git a/firmware/common/hackrf_core.c b/firmware/common/hackrf_core.c index cedff0f3..58f3b17d 100644 --- a/firmware/common/hackrf_core.c +++ b/firmware/common/hackrf_core.c @@ -26,6 +26,8 @@ #include "sgpio.h" #include "si5351c.h" #include "spi_ssp.h" +#include "max2831.h" +#include "max2831_target.h" #include "max283x.h" #include "max5864.h" #include "max5864_target.h" @@ -34,14 +36,17 @@ #include "i2c_bus.h" #include "i2c_lpc.h" #include "cpld_jtag.h" +#include "ice40_spi.h" #include "platform_detect.h" #include "clkin.h" +#include "selftest.h" #include #include #include #include +#include -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) #include "portapack.h" #endif @@ -55,16 +60,32 @@ static struct gpio_t gpio_led[] = { #ifdef RAD1O GPIO(5, 26), #endif +#ifdef PRALINE + GPIO(4, 6), +#endif }; // clang-format off +#ifndef PRALINE static struct gpio_t gpio_1v8_enable = GPIO(3, 6); +#else +static struct gpio_t gpio_1v2_enable = GPIO(4, 7); +static struct gpio_t gpio_3v3aux_enable_n = GPIO(5, 15); +#endif -/* MAX283x GPIO (XCVR_CTL) PinMux */ +/* MAX283x GPIO (XCVR_CTL / CS_XCVR) PinMux */ +#ifdef PRALINE +static struct gpio_t gpio_max283x_select = GPIO(6, 28); +#else static struct gpio_t gpio_max283x_select = GPIO(0, 15); +#endif -/* MAX5864 SPI chip select (AD_CS) GPIO PinMux */ +/* MAX5864 SPI chip select (AD_CS / CS_AD) GPIO PinMux */ +#ifdef PRALINE +static struct gpio_t gpio_max5864_select = GPIO(6, 30); +#else static struct gpio_t gpio_max5864_select = GPIO(2, 7); +#endif /* RFFC5071 GPIO serial interface PinMux */ // #ifdef RAD1O @@ -78,6 +99,9 @@ static struct gpio_t gpio_max5864_select = GPIO(2, 7); #ifdef HACKRF_ONE static struct gpio_t gpio_vaa_disable = GPIO(2, 9); #endif +#ifdef PRALINE +static struct gpio_t gpio_vaa_disable = GPIO(4, 1); +#endif #ifdef RAD1O static struct gpio_t gpio_vaa_enable = GPIO(2, 9); #endif @@ -115,10 +139,23 @@ static struct gpio_t gpio_low_high_filt_n = GPIO(2, 12); static struct gpio_t gpio_tx_amp = GPIO(2, 15); static struct gpio_t gpio_rx_lna = GPIO(5, 15); #endif +#ifdef PRALINE +static struct gpio_t gpio_tx_en = GPIO(3, 4); +static struct gpio_t gpio_mix_en_n = GPIO(3, 2); +static struct gpio_t gpio_mix_en_n_r1_0 = GPIO(5, 6); +static struct gpio_t gpio_lpf_en = GPIO(4, 8); +static struct gpio_t gpio_rf_amp_en = GPIO(4, 9); +static struct gpio_t gpio_ant_bias_en_n = GPIO(1, 12); +#endif -/* CPLD JTAG interface GPIO pins */ -static struct gpio_t gpio_cpld_tdo = GPIO(5, 18); +/* CPLD JTAG interface GPIO pins, FPGA config pins in Praline */ static struct gpio_t gpio_cpld_tck = GPIO(3, 0); +#ifdef PRALINE +static struct gpio_t gpio_fpga_cfg_creset = GPIO(2, 11); +static struct gpio_t gpio_fpga_cfg_cdone = GPIO(5, 14); +static struct gpio_t gpio_fpga_cfg_spi_cs = GPIO(2, 10); +#else +static struct gpio_t gpio_cpld_tdo = GPIO(5, 18); #if (defined HACKRF_ONE || defined RAD1O) static struct gpio_t gpio_cpld_tms = GPIO(3, 4); static struct gpio_t gpio_cpld_tdi = GPIO(3, 1); @@ -126,14 +163,17 @@ static struct gpio_t gpio_cpld_tdi = GPIO(3, 1); static struct gpio_t gpio_cpld_tms = GPIO(3, 1); static struct gpio_t gpio_cpld_tdi = GPIO(3, 4); #endif +#endif -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) static struct gpio_t gpio_cpld_pp_tms = GPIO(1, 1); static struct gpio_t gpio_cpld_pp_tdo = GPIO(1, 8); #endif /* other CPLD interface GPIO pins */ +#ifndef PRALINE static struct gpio_t gpio_hw_sync_enable = GPIO(5, 12); +#endif static struct gpio_t gpio_q_invert = GPIO(0, 13); /* HackRF One r9 */ @@ -143,6 +183,19 @@ static struct gpio_t gpio_h1r9_1v8_enable = GPIO(2, 9); static struct gpio_t gpio_h1r9_vaa_disable = GPIO(3, 6); static struct gpio_t gpio_h1r9_hw_sync_enable = GPIO(5, 5); #endif + +#ifdef PRALINE +static struct gpio_t gpio_p2_ctrl0 = GPIO(7, 3); +static struct gpio_t gpio_p2_ctrl1 = GPIO(7, 4); +static struct gpio_t gpio_p1_ctrl0 = GPIO(0, 14); +static struct gpio_t gpio_p1_ctrl1 = GPIO(5, 16); +static struct gpio_t gpio_p1_ctrl2 = GPIO(3, 5); +static struct gpio_t gpio_clkin_ctrl = GPIO(0, 15); +static struct gpio_t gpio_aa_en = GPIO(1, 7); +static struct gpio_t gpio_trigger_in = GPIO(6, 26); +static struct gpio_t gpio_trigger_out = GPIO(5, 6); +static struct gpio_t gpio_pps_out = GPIO(5, 5); +#endif // clang-format on i2c_bus_t i2c0 = { @@ -172,6 +225,45 @@ si5351c_driver_t clock_gen = { .i2c_address = 0x60, }; +spi_bus_t spi_bus_ssp1 = { + .obj = (void*) SSP1_BASE, + .config = &ssp_config_max5864, + .start = spi_ssp_start, + .stop = spi_ssp_stop, + .transfer = spi_ssp_transfer, + .transfer_gather = spi_ssp_transfer_gather, +}; + +#ifdef PRALINE +const ssp_config_t ssp_config_max283x = { + /* FIXME speed up once everything is working reliably */ + /* + // Freq About 0.0498MHz / 49.8KHz => Freq = PCLK / (CPSDVSR * [SCR+1]) with PCLK=PLL1=204MHz + const uint8_t serial_clock_rate = 32; + const uint8_t clock_prescale_rate = 128; + */ + // Freq About 4.857MHz => Freq = PCLK / (CPSDVSR * [SCR+1]) with PCLK=PLL1=204MHz + .data_bits = SSP_DATA_9BITS, // send 2 words + .serial_clock_rate = 21, + .clock_prescale_rate = 2, + .gpio_select = &gpio_max283x_select, +}; + +static struct gpio_t gpio_max2831_enable = GPIO(7, 1); +static struct gpio_t gpio_max2831_rx_enable = GPIO(7, 2); +static struct gpio_t gpio_max2831_rxhp = GPIO(6, 29); +static struct gpio_t gpio_max2831_ld = GPIO(4, 11); + +max2831_driver_t max283x = { + .bus = &spi_bus_ssp1, + .gpio_enable = &gpio_max2831_enable, + .gpio_rxtx = &gpio_max2831_rx_enable, + .gpio_rxhp = &gpio_max2831_rxhp, + .gpio_ld = &gpio_max2831_ld, + .target_init = max2831_target_init, + .set_mode = max2831_target_set_mode, +}; +#else const ssp_config_t ssp_config_max283x = { /* FIXME speed up once everything is working reliably */ /* @@ -186,6 +278,9 @@ const ssp_config_t ssp_config_max283x = { .gpio_select = &gpio_max283x_select, }; +max283x_driver_t max283x = {}; +#endif + const ssp_config_t ssp_config_max5864 = { /* FIXME speed up once everything is working reliably */ /* @@ -200,17 +295,6 @@ const ssp_config_t ssp_config_max5864 = { .gpio_select = &gpio_max5864_select, }; -spi_bus_t spi_bus_ssp1 = { - .obj = (void*) SSP1_BASE, - .config = &ssp_config_max5864, - .start = spi_ssp_start, - .stop = spi_ssp_stop, - .transfer = spi_ssp_transfer, - .transfer_gather = spi_ssp_transfer_gather, -}; - -max283x_driver_t max283x = {}; - max5864_driver_t max5864 = { .bus = &spi_bus_ssp1, .target_init = max5864_target_init, @@ -241,10 +325,60 @@ w25q80bv_driver_t spi_flash = { sgpio_config_t sgpio_config = { .gpio_q_invert = &gpio_q_invert, +#ifndef PRALINE .gpio_hw_sync_enable = &gpio_hw_sync_enable, +#endif .slice_mode_multislice = true, }; +#ifdef PRALINE +const ssp_config_t ssp_config_ice40_fpga = { + .data_bits = SSP_DATA_8BITS, + .spi_mode = SSP_CPOL_1_CPHA_1, + .serial_clock_rate = 21, + .clock_prescale_rate = 2, + .gpio_select = &gpio_fpga_cfg_spi_cs, +}; + +ice40_spi_driver_t ice40 = { + .bus = &spi_bus_ssp1, + .gpio_select = &gpio_fpga_cfg_spi_cs, + .gpio_creset = &gpio_fpga_cfg_creset, + .gpio_cdone = &gpio_fpga_cfg_cdone, +}; +#endif + +radio_t radio = { + .channel[RADIO_CHANNEL0] = + { + .id = RADIO_CHANNEL0, + .config = + { + .sample_rate[RADIO_SAMPLE_RATE_CLOCKGEN] = + {.hz = 0}, + .filter[RADIO_FILTER_BASEBAND] = {.hz = 0}, + .frequency[RADIO_FREQUENCY_RF] = + { + .hz = 0, + .if_hz = 0, + .lo_hz = 0, + .path = 0, + }, + .gain[RADIO_GAIN_RF_AMP] = {.enable = 0}, + .gain[RADIO_GAIN_RX_LNA] = {.db = 0}, + .gain[RADIO_GAIN_RX_VGA] = {.db = 0}, + .gain[RADIO_GAIN_TX_VGA] = {.db = 0}, + .antenna[RADIO_ANTENNA_BIAS_TEE] = + {.enable = false}, + .mode = TRANSCEIVER_MODE_OFF, + .clock[RADIO_CLOCK_CLKIN] = {.enable = false}, + .clock[RADIO_CLOCK_CLKOUT] = {.enable = false}, + .trigger_mode = HW_SYNC_MODE_OFF, + }, + .clock_source = CLOCK_SOURCE_HACKRF, + }, +}; + rf_path_t rf_path = { .switchctrl = 0, #ifdef HACKRF_ONE @@ -275,14 +409,23 @@ rf_path_t rf_path = { .gpio_tx_amp = &gpio_tx_amp, .gpio_rx_lna = &gpio_rx_lna, #endif +#ifdef PRALINE + .gpio_tx_en = &gpio_tx_en, + .gpio_mix_en_n = &gpio_mix_en_n, + .gpio_lpf_en = &gpio_lpf_en, + .gpio_rf_amp_en = &gpio_rf_amp_en, + .gpio_ant_bias_en_n = &gpio_ant_bias_en_n, +#endif }; jtag_gpio_t jtag_gpio_cpld = { - .gpio_tms = &gpio_cpld_tms, .gpio_tck = &gpio_cpld_tck, +#ifndef PRALINE + .gpio_tms = &gpio_cpld_tms, .gpio_tdi = &gpio_cpld_tdi, .gpio_tdo = &gpio_cpld_tdo, -#ifdef HACKRF_ONE +#endif +#if (defined HACKRF_ONE || defined PRALINE) .gpio_pp_tms = &gpio_cpld_pp_tms, .gpio_pp_tdo = &gpio_cpld_pp_tdo, #endif @@ -406,6 +549,7 @@ bool sample_rate_frac_set(uint32_t rate_num, uint32_t rate_denom) MSx_P2 = (128 * b) % c; MSx_P3 = c; +#ifndef PRALINE if (detected_platform() == BOARD_ID_HACKRF1_R9) { /* * On HackRF One r9 all sample clocks are externally derived @@ -426,6 +570,10 @@ bool sample_rate_frac_set(uint32_t rate_num, uint32_t rate_denom) /* MS0/CLK2 is the source for SGPIO (CODEC_X2_CLK) */ si5351c_configure_multisynth(&clock_gen, 2, 0, 0, 0, 0); //p1 doesn't matter } +#else + /* MS0/CLK0 is the source for the MAX5864/FPGA (AFE_CLK). */ + si5351c_configure_multisynth(&clock_gen, 0, MSx_P1, MSx_P2, MSx_P3, 1); +#endif if (streaming) { sgpio_cpld_stream_enable(&sgpio_config); @@ -486,6 +634,7 @@ bool sample_rate_set(const uint32_t sample_rate_hz) return false; } +#ifndef PRALINE if (detected_platform() == BOARD_ID_HACKRF1_R9) { /* * On HackRF One r9 all sample clocks are externally derived @@ -518,22 +667,17 @@ bool sample_rate_set(const uint32_t sample_rate_hz) 1, 0); //p1 doesn't matter } +#else + /* MS0/CLK0 is the source for the MAX5864/FPGA (AFE_CLK). */ + si5351c_configure_multisynth(&clock_gen, 0, p1, p2, p3, 1); + + /* MS0/CLK1 is the source for SCT_CLK (CODEC_X2_CLK). */ + si5351c_configure_multisynth(&clock_gen, 1, p1, p2, p3, 0); +#endif return true; } -bool baseband_filter_bandwidth_set(const uint32_t bandwidth_hz) -{ - uint32_t bandwidth_hz_real; - bandwidth_hz_real = max283x_set_lpf_bandwidth(&max283x, bandwidth_hz); - - if (bandwidth_hz_real) { - hackrf_ui()->set_filter_bw(bandwidth_hz_real); - } - - return bandwidth_hz_real != 0; -} - /* Configure PLL1 (Main MCU Clock) to max speed (204MHz). Note: PLL1 clock is used by M4/M0 core, Peripheral, APB1. @@ -639,6 +783,16 @@ void cpu_clock_init(void) * CLK5 -> MAX2837 (MAX2871 on rad1o) * CLK6 -> none * CLK7 -> LPC43xx (uses a 12MHz crystal by default) + * + * Clocks on Praline: + * CLK0 -> AFE_CLK (MAX5864/FPGA) + * CLK1 -> SCT_CLK + * CLK2 -> MCU_CLK (uses a 12MHz crystal by default) + * CLK3 -> External Clock Output (power down at boot) + * CLK4 -> XCVR_CLK (MAX2837) + * CLK5 -> MIX_CLK (RFFC5072) + * CLK6 -> AUX_CLK1 + * CLK7 -> AUX_CLK2 */ if (detected_platform() == BOARD_ID_HACKRF1_R9) { @@ -744,7 +898,30 @@ void cpu_clock_init(void) CGU_BASE_SSP1_CLK = CGU_BASE_SSP1_CLK_AUTOBLOCK(1) | CGU_BASE_SSP1_CLK_CLK_SEL(CGU_SRC_PLL1); -#if (defined JAWBREAKER || defined HACKRF_ONE) +#ifndef RAD1O + /* Enable 32kHz oscillator */ + CREG_CREG0 &= ~(CREG_CREG0_PD32KHZ | CREG_CREG0_RESET32KHZ); + CREG_CREG0 |= CREG_CREG0_EN32KHZ; + + /* Allow 1ms to start up. */ + delay_us_at_mhz(1000, 204); + + /* Use frequency monitor to check 32kHz oscillator is running. */ + CGU_FREQ_MON = CGU_FREQ_MON_RCNT(511) | CGU_FREQ_MON_CLK_SEL(CGU_SRC_32K); + CGU_FREQ_MON |= CGU_FREQ_MON_MEAS_MASK; + while (CGU_FREQ_MON & CGU_FREQ_MON_MEAS_MASK) + ; + uint32_t count = + (CGU_FREQ_MON & CGU_FREQ_MON_FCNT_MASK) >> CGU_FREQ_MON_FCNT_SHIFT; + // We should see a single count, because 511 cycles of the 12MHz internal + // RC oscillator corresponds to 1.39 cycles of the 32768Hz clock. + selftest.rtc_osc_ok = (count == 1); + if (!selftest.rtc_osc_ok) { + selftest.report.pass = false; + } +#endif + +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) /* Disable unused clocks */ /* Start with PLLs */ CGU_PLL0AUDIO_CTRL = CGU_PLL0AUDIO_CTRL_PD(1); @@ -815,7 +992,7 @@ void cpu_clock_init(void) clock_source_t activate_best_clock_source(void) { -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) /* Ensure PortaPack reference oscillator is off while checking for external clock input. */ if (portapack_reference_oscillator && portapack()) { portapack_reference_oscillator(false); @@ -828,7 +1005,7 @@ clock_source_t activate_best_clock_source(void) if (si5351c_clkin_signal_valid(&clock_gen)) { source = CLOCK_SOURCE_EXTERNAL; } else { -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) /* Enable PortaPack reference oscillator (if present), and check for valid clock. */ if (portapack_reference_oscillator && portapack()) { portapack_reference_oscillator(true); @@ -847,6 +1024,8 @@ clock_source_t activate_best_clock_source(void) &clock_gen, (source == CLOCK_SOURCE_HACKRF) ? PLL_SOURCE_XTAL : PLL_SOURCE_CLKIN); hackrf_ui()->set_clock_source(source); + radio.channel[RADIO_CHANNEL0].clock_source = source; + return source; } @@ -860,6 +1039,13 @@ void ssp1_set_mode_max5864(void) spi_bus_start(max5864.bus, &ssp_config_max5864); } +#ifdef PRALINE +void ssp1_set_mode_ice40(void) +{ + spi_bus_start(&spi_bus_ssp1, &ssp_config_ice40_fpga); +} +#endif + void pin_setup(void) { /* Configure all GPIO as Input (safe state) */ @@ -879,14 +1065,16 @@ void pin_setup(void) * * LPC43xx pull-up and pull-down resistors are approximately 53K. */ -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) scu_pinmux(SCU_PINMUX_PP_TMS, SCU_GPIO_PUP | SCU_CONF_FUNCTION0); scu_pinmux(SCU_PINMUX_PP_TDO, SCU_GPIO_PDN | SCU_CONF_FUNCTION0); #endif + scu_pinmux(SCU_PINMUX_CPLD_TCK, SCU_GPIO_PDN | SCU_CONF_FUNCTION0); +#ifndef PRALINE scu_pinmux(SCU_PINMUX_CPLD_TMS, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); scu_pinmux(SCU_PINMUX_CPLD_TDI, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); scu_pinmux(SCU_PINMUX_CPLD_TDO, SCU_GPIO_PDN | SCU_CONF_FUNCTION4); - scu_pinmux(SCU_PINMUX_CPLD_TCK, SCU_GPIO_PDN | SCU_CONF_FUNCTION0); +#endif /* Configure SCU Pin Mux as GPIO */ scu_pinmux(SCU_PINMUX_LED1, SCU_GPIO_NOPULL); @@ -895,6 +1083,9 @@ void pin_setup(void) #ifdef RAD1O scu_pinmux(SCU_PINMUX_LED4, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION4); #endif +#ifdef PRALINE + scu_pinmux(SCU_PINMUX_LED4, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); +#endif /* Configure USB indicators */ #ifdef JAWBREAKER @@ -902,31 +1093,52 @@ void pin_setup(void) scu_pinmux(SCU_PINMUX_USB_LED1, SCU_CONF_FUNCTION3); #endif + led_off(0); + led_off(1); + led_off(2); +#ifdef RAD1O + led_off(3); +#endif + gpio_output(&gpio_led[0]); gpio_output(&gpio_led[1]); gpio_output(&gpio_led[2]); #ifdef RAD1O gpio_output(&gpio_led[3]); #endif +#ifdef PRALINE + gpio_output(&gpio_led[3]); +#endif +#ifdef PRALINE + disable_1v2_power(); + disable_3v3aux_power(); + gpio_output(&gpio_1v2_enable); + gpio_output(&gpio_3v3aux_enable_n); + scu_pinmux(SCU_PINMUX_EN1V2, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); + scu_pinmux(SCU_PINMUX_EN3V3_AUX_N, SCU_GPIO_FAST | SCU_CONF_FUNCTION4); +#else disable_1v8_power(); if (detected_platform() == BOARD_ID_HACKRF1_R9) { -#ifdef HACKRF_ONE + #ifdef HACKRF_ONE gpio_output(&gpio_h1r9_1v8_enable); scu_pinmux(SCU_H1R9_EN1V8, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); -#endif + #endif } else { gpio_output(&gpio_1v8_enable); scu_pinmux(SCU_PINMUX_EN1V8, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); } +#endif -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) /* Safe state: start with VAA turned off: */ disable_rf_power(); /* Configure RF power supply (VAA) switch control signal as output */ if (detected_platform() == BOARD_ID_HACKRF1_R9) { + #ifdef HACKRF_ONE gpio_output(&gpio_h1r9_vaa_disable); + #endif } else { gpio_output(&gpio_vaa_disable); } @@ -948,10 +1160,39 @@ void pin_setup(void) #endif +#ifdef PRALINE + scu_pinmux(SCU_P2_CTRL0, SCU_P2_CTRL0_PINCFG); + scu_pinmux(SCU_P2_CTRL1, SCU_P2_CTRL1_PINCFG); + scu_pinmux(SCU_P1_CTRL0, SCU_P1_CTRL0_PINCFG); + scu_pinmux(SCU_P1_CTRL1, SCU_P1_CTRL1_PINCFG); + scu_pinmux(SCU_P1_CTRL2, SCU_P1_CTRL2_PINCFG); + scu_pinmux(SCU_CLKIN_CTRL, SCU_CLKIN_CTRL_PINCFG); + scu_pinmux(SCU_AA_EN, SCU_AA_EN_PINCFG); + scu_pinmux(SCU_TRIGGER_IN, SCU_TRIGGER_IN_PINCFG); + scu_pinmux(SCU_TRIGGER_OUT, SCU_TRIGGER_OUT_PINCFG); + scu_pinmux(SCU_PPS_OUT, SCU_PPS_OUT_PINCFG); + + p2_ctrl_set(P2_SIGNAL_CLK3); + p1_ctrl_set(P1_SIGNAL_CLKIN); + narrowband_filter_set(0); + clkin_ctrl_set(CLKIN_SIGNAL_P22); + + gpio_output(&gpio_p2_ctrl0); + gpio_output(&gpio_p2_ctrl1); + gpio_output(&gpio_p1_ctrl0); + gpio_output(&gpio_p1_ctrl1); + gpio_output(&gpio_p1_ctrl2); + gpio_output(&gpio_clkin_ctrl); + gpio_output(&gpio_pps_out); + gpio_output(&gpio_aa_en); + gpio_input(&gpio_trigger_in); + gpio_input(&gpio_trigger_out); +#endif + /* enable input on SCL and SDA pins */ SCU_SFSI2C0 = SCU_I2C0_NOMINAL; - spi_bus_start(&spi_bus_ssp1, &ssp_config_max283x); + ssp1_set_mode_max283x(); mixer_bus_setup(&mixer); @@ -961,6 +1202,14 @@ void pin_setup(void) sgpio_config.gpio_hw_sync_enable = &gpio_h1r9_hw_sync_enable; } #endif + +#ifdef PRALINE + board_rev_t rev = detected_revision(); + if ((rev == BOARD_REV_PRALINE_R1_0) || (rev == BOARD_REV_GSG_PRALINE_R1_0)) { + rf_path.gpio_mix_en_n = &gpio_mix_en_n_r1_0; + } +#endif + rf_path_pin_setup(&rf_path); /* Configure external clock in */ @@ -969,12 +1218,33 @@ void pin_setup(void) sgpio_configure_pin_functions(&sgpio_config); } +#ifdef PRALINE +void enable_1v2_power(void) +{ + gpio_set(&gpio_1v2_enable); +} + +void disable_1v2_power(void) +{ + gpio_clear(&gpio_1v2_enable); +} + +void enable_3v3aux_power(void) +{ + gpio_clear(&gpio_3v3aux_enable_n); +} + +void disable_3v3aux_power(void) +{ + gpio_set(&gpio_3v3aux_enable_n); +} +#else void enable_1v8_power(void) { if (detected_platform() == BOARD_ID_HACKRF1_R9) { -#ifdef HACKRF_ONE + #ifdef HACKRF_ONE gpio_set(&gpio_h1r9_1v8_enable); -#endif + #endif } else { gpio_set(&gpio_1v8_enable); } @@ -983,13 +1253,14 @@ void enable_1v8_power(void) void disable_1v8_power(void) { if (detected_platform() == BOARD_ID_HACKRF1_R9) { -#ifdef HACKRF_ONE + #ifdef HACKRF_ONE gpio_clear(&gpio_h1r9_1v8_enable); -#endif + #endif } else { gpio_clear(&gpio_1v8_enable); } } +#endif #ifdef HACKRF_ONE void enable_rf_power(void) @@ -1018,6 +1289,21 @@ void disable_rf_power(void) } #endif +#ifdef PRALINE +void enable_rf_power(void) +{ + gpio_clear(&gpio_vaa_disable); + + /* Let the voltage stabilize */ + delay(1000000); +} + +void disable_rf_power(void) +{ + gpio_set(&gpio_vaa_disable); +} +#endif + #ifdef RAD1O void enable_rf_power(void) { @@ -1033,6 +1319,17 @@ void disable_rf_power(void) } #endif +#ifdef PRALINE +void led_on(const led_t led) +{ + gpio_clear(&gpio_led[led]); +} + +void led_off(const led_t led) +{ + gpio_set(&gpio_led[led]); +} +#else void led_on(const led_t led) { gpio_set(&gpio_led[led]); @@ -1042,6 +1339,7 @@ void led_off(const led_t led) { gpio_clear(&gpio_led[led]); } +#endif void led_toggle(const led_t led) { @@ -1051,17 +1349,28 @@ void led_toggle(const led_t led) void set_leds(const uint8_t state) { int num_leds = 3; -#ifdef RAD1O +#if (defined RAD1O || defined PRALINE) num_leds = 4; #endif for (int i = 0; i < num_leds; i++) { +#ifdef PRALINE + gpio_write(&gpio_led[i], ((state >> i) & 1) == 0); +#else gpio_write(&gpio_led[i], ((state >> i) & 1) == 1); +#endif } } void hw_sync_enable(const hw_sync_mode_t hw_sync_mode) { +#ifndef PRALINE gpio_write(sgpio_config.gpio_hw_sync_enable, hw_sync_mode == 1); +#else + ssp1_set_mode_ice40(); + uint8_t prev = ice40_spi_read(&ice40, 0x01); + ice40_spi_write(&ice40, 0x01, (prev & 0x7F) | ((hw_sync_mode == 1) << 7)); + ssp1_set_mode_max283x(); +#endif } void halt_and_flash(const uint32_t duration) @@ -1078,3 +1387,33 @@ void halt_and_flash(const uint32_t duration) delay(duration); } } + +#ifdef PRALINE +void p1_ctrl_set(const p1_ctrl_signal_t signal) +{ + gpio_write(&gpio_p1_ctrl0, signal & 1); + gpio_write(&gpio_p1_ctrl1, (signal >> 1) & 1); + gpio_write(&gpio_p1_ctrl2, (signal >> 2) & 1); +} + +void p2_ctrl_set(const p2_ctrl_signal_t signal) +{ + gpio_write(&gpio_p2_ctrl0, signal & 1); + gpio_write(&gpio_p2_ctrl1, (signal >> 1) & 1); +} + +void clkin_ctrl_set(const clkin_signal_t signal) +{ + gpio_write(&gpio_clkin_ctrl, signal & 1); +} + +void pps_out_set(const uint8_t value) +{ + gpio_write(&gpio_pps_out, value & 1); +} + +void narrowband_filter_set(const uint8_t value) +{ + gpio_write(&gpio_aa_en, value & 1); +} +#endif diff --git a/firmware/common/hackrf_core.h b/firmware/common/hackrf_core.h index d6211b37..87656788 100644 --- a/firmware/common/hackrf_core.h +++ b/firmware/common/hackrf_core.h @@ -34,13 +34,16 @@ extern "C" { #include "si5351c.h" #include "spi_ssp.h" +#include "max2831.h" #include "max283x.h" #include "max5864.h" #include "mixer.h" #include "w25q80bv.h" #include "sgpio.h" +#include "radio.h" #include "rf_path.h" #include "cpld_jtag.h" +#include "ice40_spi.h" /* * SCU PinMux @@ -53,8 +56,16 @@ extern "C" { #ifdef RAD1O #define SCU_PINMUX_LED4 (PB_6) /* GPIO5[26] on PB_6 */ #endif +#ifdef PRALINE + #define SCU_PINMUX_LED4 (P8_6) /* GPIO4[6] on P8_6 */ +#endif #define SCU_PINMUX_EN1V8 (P6_10) /* GPIO3[6] on P6_10 */ +#define SCU_PINMUX_EN1V2 (P8_7) /* GPIO4[7] on P8_7 */ +#ifdef PRALINE + #define SCU_PINMUX_EN3V3_AUX_N (P6_7) /* GPIO5[15] on P6_7 */ + #define SCU_PINMUX_EN3V3_OC_N (P6_11) /* GPIO3[7] on P6_11 */ +#endif /* GPIO Input PinMux */ #define SCU_PINMUX_BOOT0 (P1_1) /* GPIO0[8] on P1_1 */ @@ -82,9 +93,14 @@ extern "C" { #define SCU_SSP1_CS (P1_20) /* P1_20 */ /* CPLD JTAG interface */ +#ifdef PRALINE + #define SCU_PINMUX_FPGA_CRESET (P5_2) /* GPIO2[11] on P5_2 */ + #define SCU_PINMUX_FPGA_CDONE (P4_10) /* GPIO5[14] */ + #define SCU_PINMUX_FPGA_SPI_CS (P5_1) /* GPIO2[10] */ +#endif #define SCU_PINMUX_CPLD_TDO (P9_5) /* GPIO5[18] */ #define SCU_PINMUX_CPLD_TCK (P6_1) /* GPIO3[ 0] */ -#if (defined HACKRF_ONE || defined RAD1O) +#if (defined HACKRF_ONE || defined RAD1O || defined PRALINE) #define SCU_PINMUX_CPLD_TMS (P6_5) /* GPIO3[ 4] */ #define SCU_PINMUX_CPLD_TDI (P6_2) /* GPIO3[ 1] */ #else @@ -93,24 +109,76 @@ extern "C" { #endif /* CPLD SGPIO interface */ -#define SCU_PINMUX_SGPIO0 (P0_0) -#define SCU_PINMUX_SGPIO1 (P0_1) -#define SCU_PINMUX_SGPIO2 (P1_15) -#define SCU_PINMUX_SGPIO3 (P1_16) -#define SCU_PINMUX_SGPIO4 (P6_3) -#define SCU_PINMUX_SGPIO5 (P6_6) -#define SCU_PINMUX_SGPIO6 (P2_2) -#define SCU_PINMUX_SGPIO7 (P1_0) -#if (defined JAWBREAKER || defined HACKRF_ONE || defined RAD1O) - #define SCU_PINMUX_SGPIO8 (P9_6) +#ifdef PRALINE + #define SCU_PINMUX_SGPIO0 (P0_0) + #define SCU_PINMUX_SGPIO1 (P0_1) + #define SCU_PINMUX_SGPIO2 (P1_15) + #define SCU_PINMUX_SGPIO3 (P1_16) + #define SCU_PINMUX_SGPIO4 (P9_4) + #define SCU_PINMUX_SGPIO5 (P6_6) + #define SCU_PINMUX_SGPIO6 (P2_2) + #define SCU_PINMUX_SGPIO7 (P1_0) + #define SCU_PINMUX_SGPIO8 (P8_0) + #define SCU_PINMUX_SGPIO9 (P9_3) + #define SCU_PINMUX_SGPIO10 (P8_2) + #define SCU_PINMUX_SGPIO11 (P1_17) + #define SCU_PINMUX_SGPIO12 (P1_18) + #define SCU_PINMUX_SGPIO14 (P1_18) + #define SCU_PINMUX_SGPIO15 (P1_18) + + #define SCU_PINMUX_SGPIO0_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION3) + #define SCU_PINMUX_SGPIO1_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION3) + #define SCU_PINMUX_SGPIO2_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION2) + #define SCU_PINMUX_SGPIO3_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION2) + #define SCU_PINMUX_SGPIO4_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION6) + #define SCU_PINMUX_SGPIO5_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION2) + #define SCU_PINMUX_SGPIO6_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_PINMUX_SGPIO7_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION6) + #define SCU_PINMUX_SGPIO8_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_PINMUX_SGPIO9_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION6) + #define SCU_PINMUX_SGPIO10_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_PINMUX_SGPIO11_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION6) + #define SCU_PINMUX_SGPIO12_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_PINMUX_SGPIO14_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_PINMUX_SGPIO15_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + +#else + #define SCU_PINMUX_SGPIO0 (P0_0) + #define SCU_PINMUX_SGPIO1 (P0_1) + #define SCU_PINMUX_SGPIO2 (P1_15) + #define SCU_PINMUX_SGPIO3 (P1_16) + #define SCU_PINMUX_SGPIO4 (P6_3) + #define SCU_PINMUX_SGPIO5 (P6_6) + #define SCU_PINMUX_SGPIO6 (P2_2) + #define SCU_PINMUX_SGPIO7 (P1_0) + #if (defined JAWBREAKER || defined HACKRF_ONE || defined RAD1O) + #define SCU_PINMUX_SGPIO8 (P9_6) + #endif + #define SCU_PINMUX_SGPIO9 (P4_3) + #define SCU_PINMUX_SGPIO10 (P1_14) + #define SCU_PINMUX_SGPIO11 (P1_17) + #define SCU_PINMUX_SGPIO12 (P1_18) + #define SCU_PINMUX_SGPIO14 (P4_9) + #define SCU_PINMUX_SGPIO15 (P4_10) + + #define SCU_PINMUX_SGPIO0_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION3) + #define SCU_PINMUX_SGPIO1_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION3) + #define SCU_PINMUX_SGPIO2_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION2) + #define SCU_PINMUX_SGPIO3_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION2) + #define SCU_PINMUX_SGPIO4_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION2) + #define SCU_PINMUX_SGPIO5_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION2) + #define SCU_PINMUX_SGPIO6_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_PINMUX_SGPIO7_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION6) + #define SCU_PINMUX_SGPIO8_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION6) + #define SCU_PINMUX_SGPIO9_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION7) + #define SCU_PINMUX_SGPIO10_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION6) + #define SCU_PINMUX_SGPIO11_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION6) + #define SCU_PINMUX_SGPIO12_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_PINMUX_SGPIO14_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_PINMUX_SGPIO15_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #endif -#define SCU_PINMUX_SGPIO9 (P4_3) -#define SCU_PINMUX_SGPIO10 (P1_14) -#define SCU_PINMUX_SGPIO11 (P1_17) -#define SCU_PINMUX_SGPIO12 (P1_18) -#define SCU_PINMUX_SGPIO14 (P4_9) -#define SCU_PINMUX_SGPIO15 (P4_10) -#define SCU_HW_SYNC_EN (P4_8) /* GPIO5[12] on P4_8 */ +#define SCU_HW_SYNC_EN (P4_8) /* GPIO5[12] on P4_8 */ /* MAX2837 GPIO (XCVR_CTL) PinMux */ #ifdef RAD1O @@ -119,13 +187,40 @@ extern "C" { #define SCU_XCVR_B7 (P9_3) /* GPIO[] on P8_3 */ #endif -#define SCU_XCVR_ENABLE (P4_6) /* GPIO2[6] on P4_6 */ -#define SCU_XCVR_RXENABLE (P4_5) /* GPIO2[5] on P4_5 */ -#define SCU_XCVR_TXENABLE (P4_4) /* GPIO2[4] on P4_4 */ -#define SCU_XCVR_CS (P1_20) /* GPIO0[15] on P1_20 */ +#ifdef PRALINE + #define SCU_XCVR_ENABLE (PE_1) /* GPIO7[1] on PE_1 */ + #define SCU_XCVR_RXENABLE (PE_2) /* GPIO7[2] on PE_2 */ + #define SCU_XCVR_CS (PD_14) /* GPIO6[28] on PD_14 */ + #define SCU_XCVR_RXHP (PD_15) /* GPIO6[29] on PD_15 */ + #define SCU_XCVR_LD (P9_6) /* GPIO4[11] on P9_6 */ + + #define SCU_XCVR_ENABLE_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_XCVR_RXENABLE_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_XCVR_CS_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_XCVR_RXHP_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_XCVR_LD_PINCFG \ + (SCU_GPIO_FAST | SCU_CONF_FUNCTION0 | SCU_CONF_EPD_EN_PULLDOWN | \ + SCU_CONF_EPUN_DIS_PULLUP) +#else + #define SCU_XCVR_ENABLE (P4_6) /* GPIO2[6] on P4_6 */ + #define SCU_XCVR_RXENABLE (P4_5) /* GPIO2[5] on P4_5 */ + #define SCU_XCVR_TXENABLE (P4_4) /* GPIO2[4] on P4_4 */ + #define SCU_XCVR_CS (P1_20) /* GPIO0[15] on P1_20 */ + + #define SCU_XCVR_ENABLE_PINCFG (SCU_GPIO_FAST) + #define SCU_XCVR_RXENABLE_PINCFG (SCU_GPIO_FAST) + #define SCU_XCVR_TXENABLE_PINCFG (SCU_GPIO_FAST) + #define SCU_XCVR_CS_PINCFG (SCU_GPIO_FAST) +#endif /* MAX5864 SPI chip select (AD_CS) GPIO PinMux */ -#define SCU_AD_CS (P5_7) /* GPIO2[7] on P5_7 */ +#ifdef PRALINE + #define SCU_AD_CS (PD_16) /* GPIO6[30] on PD_16 */ + #define SCU_AD_CS_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) +#else + #define SCU_AD_CS (P5_7) /* GPIO2[7] on P5_7 */ + #define SCU_AD_CS_PINCFG (SCU_GPIO_FAST) +#endif /* RFFC5071 GPIO serial interface PinMux */ #if (defined JAWBREAKER || defined HACKRF_ONE) @@ -133,6 +228,20 @@ extern "C" { #define SCU_MIXER_SCLK (P2_6) /* GPIO5[6] on P2_6 */ #define SCU_MIXER_SDATA (P6_4) /* GPIO3[3] on P6_4 */ #define SCU_MIXER_RESETX (P5_5) /* GPIO2[14] on P5_5 */ + + #define SCU_MIXER_SCLK_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_MIXER_SDATA_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) +#endif +#ifdef PRALINE + #define SCU_MIXER_ENX (P5_4) /* GPIO2[13] on P5_4 */ + #define SCU_MIXER_SCLK (P9_5) /* GPIO5[18] on P9_5 */ + #define SCU_MIXER_SDATA (P9_2) /* GPIO4[14] on P9_2 */ + #define SCU_MIXER_RESETX (P5_5) /* GPIO2[14] on P5_5 */ + #define SCU_MIXER_LD (PD_11) /* GPIO6[25] on PD_11 */ + + #define SCU_MIXER_SCLK_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_MIXER_SDATA_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_MIXER_LD_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) #endif #ifdef RAD1O #define SCU_VCO_CE (P5_4) /* GPIO2[13] on P5_4 */ @@ -153,6 +262,9 @@ extern "C" { #ifdef HACKRF_ONE #define SCU_NO_VAA_ENABLE (P5_0) /* GPIO2[9] on P5_0 */ #endif +#ifdef PRALINE + #define SCU_NO_VAA_ENABLE (P8_1) /* GPIO4[1] on P8_1 */ +#endif #ifdef RAD1O #define SCU_VAA_ENABLE (P5_0) /* GPIO2[9] on P5_0 */ #endif @@ -193,6 +305,40 @@ extern "C" { #define SCU_TX_AMP (P5_6) /* GPIO2[15] on P5_6 */ #define SCU_RX_LNA (P6_7) /* GPIO5[15] on P6_7 */ #endif +#ifdef PRALINE + #define SCU_TX_EN (P6_5) /* GPIO3[4] on P6_5 */ + #define SCU_MIX_EN_N (P6_3) /* GPIO3[2] on P6_3 */ + #define SCU_MIX_EN_N_R1_0 (P2_6) /* GPIO5[6] on P2_6 */ + #define SCU_LPF_EN (PA_1) /* GPIO4[8] on PA_1 */ + #define SCU_RF_AMP_EN (PA_2) /* GPIO4[9] on PA_2 */ + #define SCU_ANT_BIAS_EN_N (P2_12) /* GPIO1[12] on P2_12 */ + #define SCU_ANT_BIAS_OC_N (P2_11) /* GPIO1[11] on P2_11 */ +#endif + +#ifdef PRALINE + #define SCU_P2_CTRL0 (PE_3) /* GPIO7[3] on PE_3 */ + #define SCU_P2_CTRL1 (PE_4) /* GPIO7[4] on PE_4 */ + #define SCU_P1_CTRL0 (P2_10) /* GPIO0[14] on P2_10 */ + #define SCU_P1_CTRL1 (P6_8) /* GPIO5[16] on P6_8 */ + #define SCU_P1_CTRL2 (P6_9) /* GPIO3[5] on P6_9 */ + #define SCU_CLKIN_CTRL (P1_20) /* GPIO0[15] on P1_20 */ + #define SCU_AA_EN (P1_14) /* GPIO1[7] on P1_14 */ + #define SCU_TRIGGER_IN (PD_12) /* GPIO6[26] on PD_12 */ + #define SCU_TRIGGER_OUT (P2_6) /* GPIO5[6] on P2_6 */ + #define SCU_PPS_OUT (P2_5) /* GPIO5[5] on P2_5 */ + + #define SCU_P2_CTRL0_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_P2_CTRL1_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_P1_CTRL0_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_P1_CTRL1_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_P1_CTRL2_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_CLKIN_CTRL_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_AA_EN_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION0) + #define SCU_TRIGGER_IN_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_TRIGGER_OUT_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + #define SCU_PPS_OUT_PINCFG (SCU_GPIO_FAST | SCU_CONF_FUNCTION4) + +#endif #define SCU_PINMUX_PP_D0 (P7_0) /* GPIO3[8] */ #define SCU_PINMUX_PP_D1 (P7_1) /* GPIO3[9] */ @@ -242,26 +388,6 @@ extern "C" { #define SCU_H1R9_NO_VAA_EN (P6_10) /* GPIO3[6] on P6_10 */ #define SCU_H1R9_HW_SYNC_EN (P2_5) /* GPIO5[5] on P2_5 */ -typedef enum { - TRANSCEIVER_MODE_OFF = 0, - TRANSCEIVER_MODE_RX = 1, - TRANSCEIVER_MODE_TX = 2, - TRANSCEIVER_MODE_SS = 3, - TRANSCEIVER_MODE_CPLD_UPDATE = 4, - TRANSCEIVER_MODE_RX_SWEEP = 5, -} transceiver_mode_t; - -typedef enum { - HW_SYNC_MODE_OFF = 0, - HW_SYNC_MODE_ON = 1, -} hw_sync_mode_t; - -typedef enum { - CLOCK_SOURCE_HACKRF = 0, - CLOCK_SOURCE_EXTERNAL = 1, - CLOCK_SOURCE_PORTAPACK = 2, -} clock_source_t; - void delay(uint32_t duration); void delay_us_at_mhz(uint32_t us, uint32_t mhz); @@ -271,11 +397,18 @@ extern const ssp_config_t ssp_config_w25q80bv; extern const ssp_config_t ssp_config_max283x; extern const ssp_config_t ssp_config_max5864; +#ifndef PRALINE extern max283x_driver_t max283x; +#else +extern max2831_driver_t max283x; +extern ice40_spi_driver_t ice40; + +#endif extern max5864_driver_t max5864; extern mixer_driver_t mixer; extern w25q80bv_driver_t spi_flash; extern sgpio_config_t sgpio_config; +extern radio_t radio; extern rf_path_t rf_path; extern jtag_t jtag_cpld; extern i2c_bus_t i2c0; @@ -283,19 +416,29 @@ extern i2c_bus_t i2c0; void cpu_clock_init(void); void ssp1_set_mode_max283x(void); void ssp1_set_mode_max5864(void); +#ifdef PRALINE +void ssp1_set_mode_max2831(void); +void ssp1_set_mode_ice40(void); +#endif void pin_setup(void); +#ifdef PRALINE +void enable_1v2_power(void); +void disable_1v2_power(void); +void enable_3v3aux_power(void); +void disable_3v3aux_power(void); +#else void enable_1v8_power(void); void disable_1v8_power(void); +#endif bool sample_rate_frac_set(uint32_t rate_num, uint32_t rate_denom); bool sample_rate_set(const uint32_t sampling_rate_hz); -bool baseband_filter_bandwidth_set(const uint32_t bandwidth_hz); clock_source_t activate_best_clock_source(void); -#if (defined HACKRF_ONE || defined RAD1O) +#if (defined HACKRF_ONE || defined RAD1O || defined PRALINE) void enable_rf_power(void); void disable_rf_power(void); #endif @@ -316,6 +459,36 @@ void hw_sync_enable(const hw_sync_mode_t hw_sync_mode); void halt_and_flash(const uint32_t duration); +#ifdef PRALINE +typedef enum { + P1_SIGNAL_TRIGGER_IN = 0, + P1_SIGNAL_AUX_CLK1 = 1, + P1_SIGNAL_CLKIN = 2, + P1_SIGNAL_TRIGGER_OUT = 3, + P1_SIGNAL_P22_CLKIN = 4, + P1_SIGNAL_P2_5 = 5, + P1_SIGNAL_NC = 6, + P1_SIGNAL_AUX_CLK2 = 7, +} p1_ctrl_signal_t; + +typedef enum { + P2_SIGNAL_CLK3 = 0, + P2_SIGNAL_TRIGGER_IN = 2, + P2_SIGNAL_TRIGGER_OUT = 3, +} p2_ctrl_signal_t; + +typedef enum { + CLKIN_SIGNAL_P1 = 0, + CLKIN_SIGNAL_P22 = 1, +} clkin_signal_t; + +void p1_ctrl_set(const p1_ctrl_signal_t signal); +void p2_ctrl_set(const p2_ctrl_signal_t signal); +void narrowband_filter_set(const uint8_t value); +void clkin_ctrl_set(const clkin_signal_t value); +void pps_out_set(const uint8_t value); +#endif + #ifdef __cplusplus } #endif diff --git a/firmware/common/hackrf_ui.c b/firmware/common/hackrf_ui.c index df6db4fa..ac7f1c18 100644 --- a/firmware/common/hackrf_ui.c +++ b/firmware/common/hackrf_ui.c @@ -78,7 +78,7 @@ const hackrf_ui_t* hackrf_ui(void) { /* Detect on first use. If no UI hardware is detected, use a stub function table. */ if (ui == NULL && ui_enabled) { -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) if (portapack_hackrf_ui_init) { ui = portapack_hackrf_ui_init(); } diff --git a/firmware/common/ice40_spi.c b/firmware/common/ice40_spi.c new file mode 100644 index 00000000..d3ac713c --- /dev/null +++ b/firmware/common/ice40_spi.c @@ -0,0 +1,124 @@ +/* + * Copyright 2024 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ice40_spi.h" + +#include +#include "hackrf_core.h" + +void ice40_spi_target_init(ice40_spi_driver_t* const drv) +{ + /* Configure SSP1 Peripheral and relevant FPGA pins. */ + scu_pinmux(SCU_SSP1_CIPO, (SCU_SSP_IO | SCU_CONF_FUNCTION5)); + scu_pinmux(SCU_SSP1_COPI, (SCU_SSP_IO | SCU_CONF_FUNCTION5)); + scu_pinmux(SCU_SSP1_SCK, (SCU_SSP_IO | SCU_CONF_FUNCTION1)); + scu_pinmux(SCU_PINMUX_FPGA_CRESET, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); + scu_pinmux(SCU_PINMUX_FPGA_CDONE, SCU_GPIO_PUP | SCU_CONF_FUNCTION4); + scu_pinmux(SCU_PINMUX_FPGA_SPI_CS, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); + + /* Configure GPIOs as inputs or outputs as needed. */ + gpio_clear(drv->gpio_creset); + gpio_output(drv->gpio_creset); + gpio_input(drv->gpio_cdone); + // select is configured in SSP code +} + +uint8_t ice40_spi_read(ice40_spi_driver_t* const drv, uint8_t r) +{ + uint8_t value[3] = {r & 0x7F, 0, 0}; + spi_bus_transfer(drv->bus, value, 3); + return value[2]; +} + +void ice40_spi_write(ice40_spi_driver_t* const drv, uint8_t r, uint16_t v) +{ + uint8_t value[3] = {(r & 0x7F) | 0x80, v, 0}; + spi_bus_transfer(drv->bus, value, 3); +} + +static void spi_ssp1_wait_for_tx_fifo_not_full() +{ + while ((SSP_SR(SSP1_BASE) & SSP_SR_TNF) == 0) {} +} + +static void spi_ssp1_wait_for_rx_fifo_not_empty() +{ + while ((SSP_SR(SSP1_BASE) & SSP_SR_RNE) == 0) {} +} + +static void spi_ssp1_wait_for_not_busy() +{ + while (SSP_SR(SSP1_BASE) & SSP_SR_BSY) {} +} + +static uint32_t spi_ssp1_transfer_word(const uint32_t data) +{ + spi_ssp1_wait_for_tx_fifo_not_full(); + SSP_DR(SSP1_BASE) = data; + spi_ssp1_wait_for_not_busy(); + spi_ssp1_wait_for_rx_fifo_not_empty(); + return SSP_DR(SSP1_BASE); +} + +bool ice40_spi_syscfg_program( + ice40_spi_driver_t* const drv, + size_t (*read_block_cb)(void* ctx, uint8_t* buffer), + void* read_ctx) +{ + // Drive CRESET_B = 0, SPI_SS = 0, SPI_SCK = 1. + gpio_clear(drv->gpio_creset); + gpio_clear(drv->gpio_select); + + // Wait a minimum of 200 ns. + delay_us_at_mhz(1, 204 / 4); // 250 ns. + + // Release CRESET_B or drive CRESET_B = 1. + gpio_set(drv->gpio_creset); + + // Wait a minimum of 1200 μs to clear internal configuration memory. + // Testing showed us that we need to wait longer. Let's wait 1800 μs. + delay_us_at_mhz(1800, 204); + + // Set SPI_SS = 1, Send 8 dummy clocks. + gpio_set(drv->gpio_select); + spi_ssp1_transfer_word(0); + + // Send configuration image serially on SPI_SI to iCE40, most-significant bit + // first, on falling edge of SPI_SCK. + uint8_t out_buffer[4096] = {0}; + gpio_clear(drv->gpio_select); + for (;;) { + size_t read_sz = read_block_cb(read_ctx, out_buffer); + if (read_sz == 0) + break; + for (size_t j = 0; j < read_sz; j++) { + spi_ssp1_transfer_word(out_buffer[j]); + } + } + + // Wait for 100 clocks cycles for CDONE to go high. + gpio_set(drv->gpio_select); + for (size_t j = 0; j < 13; j++) { + spi_ssp1_transfer_word(0); + } + + return gpio_read(drv->gpio_cdone); +} diff --git a/firmware/common/ice40_spi.h b/firmware/common/ice40_spi.h new file mode 100644 index 00000000..93c5adc6 --- /dev/null +++ b/firmware/common/ice40_spi.h @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __ICE40_SPI_H +#define __ICE40_SPI_H + +#include +#include +#include "gpio.h" +#include "spi_bus.h" + +struct ice40_spi_driver_t; +typedef struct ice40_spi_driver_t ice40_spi_driver_t; + +struct ice40_spi_driver_t { + spi_bus_t* const bus; + gpio_t gpio_select; + gpio_t gpio_creset; + gpio_t gpio_cdone; +}; + +void ice40_spi_target_init(ice40_spi_driver_t* const drv); +uint8_t ice40_spi_read(ice40_spi_driver_t* const drv, uint8_t r); +void ice40_spi_write(ice40_spi_driver_t* const drv, uint8_t r, uint16_t v); +bool ice40_spi_syscfg_program( + ice40_spi_driver_t* const drv, + size_t (*read_block_cb)(void* ctx, uint8_t* buffer), + void* read_ctx); + +#endif // __ICE40_SPI_H diff --git a/firmware/common/lz4_blk.c b/firmware/common/lz4_blk.c new file mode 100644 index 00000000..5ffcf43e --- /dev/null +++ b/firmware/common/lz4_blk.c @@ -0,0 +1,86 @@ +/* + * Copyright 2024 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include +#include +#include + +#include "lz4_blk.h" + +// Decompress raw LZ4 block. +int lz4_blk_decompress(const uint8_t* src, uint8_t* dst, size_t length) +{ + const uint8_t* src_end = src + length; // Point to the end of the current block. + const uint8_t* dst_0 = dst; // Store original dst pointer to compute output size. + + while (src < src_end) { + uint8_t token = *src++; + + // Get the literal length from the high nibble of the token. + uint32_t literal_length = token >> 4; + + // If literal length is 15 or more, we need to read additional length bytes. + if (literal_length == 0x0F) { + uint8_t len; + while ((len = *src++) == 0xFF) { + literal_length += 0xFF; + } + literal_length += len; + } + + // Copy the literals, if any. + if (literal_length > 0) { + memcpy(dst, src, literal_length); + src += literal_length; + dst += literal_length; + } + + // If we're at the end, break (no match data to process). + if (src >= src_end) { + break; + } + + // Get the match offset (2 bytes). + uint16_t offset = src[0] | (src[1] << 8); + src += 2; + + // Match length (low nibble of token + 4). + uint32_t match_length = (token & 0x0F) + 4; + + // If match length is 19 or more, we need to read additional length bytes. + if ((token & 0x0F) == 0x0F) { + uint8_t len; + while ((len = *src++) == 0xFF) { + match_length += 0xFF; + } + match_length += len; + } + + // Copy the match data. + const uint8_t* match_ptr = dst - offset; + for (uint32_t i = 0; i < match_length; i++) { + *dst++ = *match_ptr++; + } + } + + // Return the size of the output. + return dst - dst_0; +} \ No newline at end of file diff --git a/firmware/common/lz4_blk.h b/firmware/common/lz4_blk.h new file mode 100644 index 00000000..19c8207e --- /dev/null +++ b/firmware/common/lz4_blk.h @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __LZ4_BLK_H +#define __LZ4_BLK_H + +#include +#include + +int lz4_blk_decompress(const uint8_t* src, uint8_t* dst, size_t length); + +#endif \ No newline at end of file diff --git a/firmware/common/m0_state.c b/firmware/common/m0_state.c new file mode 100644 index 00000000..83af28d0 --- /dev/null +++ b/firmware/common/m0_state.c @@ -0,0 +1,39 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "m0_state.h" +#include +#include + +void m0_set_mode(enum m0_mode mode) +{ + // Set requested mode and flag bit. + m0_state.requested_mode = M0_REQUEST_FLAG | mode; + + // The M0 may be blocked waiting for the next SGPIO interrupt. + // In order to ensure that it sees our request, we need to set + // the interrupt flag here. The M0 will clear the flag again + // before acknowledging our request. + SGPIO_SET_STATUS_1 = (1 << SGPIO_SLICE_A); + + // Wait for M0 to acknowledge by clearing the flag. + while (m0_state.requested_mode & M0_REQUEST_FLAG) {} +} diff --git a/firmware/common/m0_state.h b/firmware/common/m0_state.h new file mode 100644 index 00000000..cd0391fc --- /dev/null +++ b/firmware/common/m0_state.h @@ -0,0 +1,64 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __M0_STATE_H__ +#define __M0_STATE_H__ + +#include + +#define M0_REQUEST_FLAG (1 << 16) + +struct m0_state { + uint32_t requested_mode; + uint32_t active_mode; + uint32_t m0_count; + uint32_t m4_count; + uint32_t num_shortfalls; + uint32_t longest_shortfall; + uint32_t shortfall_limit; + uint32_t threshold; + uint32_t next_mode; + uint32_t error; +}; + +enum m0_mode { + M0_MODE_IDLE = 0, + M0_MODE_WAIT = 1, + M0_MODE_RX = 2, + M0_MODE_TX_START = 3, + M0_MODE_TX_RUN = 4, +}; + +enum m0_error { + M0_ERROR_NONE = 0, + M0_ERROR_RX_TIMEOUT = 1, + M0_ERROR_TX_TIMEOUT = 2, +}; + +/* Address of m0_state is set in ldscripts. If you change the name of this + * variable, it won't be where it needs to be in the processor's address space, + * unless you also adjust the ldscripts. + */ +extern volatile struct m0_state m0_state; + +void m0_set_mode(enum m0_mode mode); + +#endif /*__M0_STATE_H__*/ diff --git a/firmware/common/max2831.c b/firmware/common/max2831.c new file mode 100644 index 00000000..d7bbbfc3 --- /dev/null +++ b/firmware/common/max2831.c @@ -0,0 +1,385 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +/* + * 'gcc -DTEST -DDEBUG -O2 -o test max2831.c' prints out what test + * program would do if it had a real spi library + * + * 'gcc -DTEST -DBUS_PIRATE -O2 -o test max2831.c' prints out bus + * pirate commands to do the same thing. + */ + +#include +#include +#include "max2831.h" +#include "max2831_regs.def" // private register def macros +#include "selftest.h" + +#define MIN(x, y) ((x) < (y) ? (x) : (y)) + +/* Default register values. */ +static const uint16_t max2831_regs_default[MAX2831_NUM_REGS] = { + 0x1740, /* 0: enable fractional mode (Table 16 recommends 0x0740, clearing unknown bit) */ + 0x119a, /* 1 */ + 0x1003, /* 2 */ + 0x0079, /* 3: PLL divider settings for 2437 MHz */ + 0x3666, /* 4: PLL divider settings for 2437 MHz */ + 0x00a4, /* 5: divide reference frequency by 2 */ + 0x0060, /* 6: enable TX power detector */ + 0x1022, /* 7: 110% TX LPF bandwidth */ + 0x2021, /* 8: pin control of RX gain, 11 MHz LPF bandwidth */ + 0x03b5, /* 9: pin control of TX gain */ + 0x1d80, /* 10: 3.5 us PA enable delay, zero PA bias */ + 0x0074, /* 11: LNA high gain, RX VGA moderate gain (Table 27 recommends 0x007f, maximum gain) */ + 0x0140, /* 12: TX VGA minimum */ + 0x0e92, /* 13 */ + 0x0100, /* 14: reference clock output disabled */ + 0x0145, /* 15: RX IQ common mode 1.1 V */ +}; + +/* Set up all registers according to defaults specified in docs. */ +static void max2831_init(max2831_driver_t* const drv) +{ + drv->target_init(drv); + max2831_set_mode(drv, MAX2831_MODE_SHUTDOWN); + + memcpy(drv->regs, max2831_regs_default, sizeof(drv->regs)); + drv->regs_dirty = 0xffff; + + /* Write default register values to chip. */ + max2831_regs_commit(drv); + + /* Disable lock detect output. */ + set_MAX2831_LOCK_DETECT_OUTPUT_EN(drv, false); + max2831_regs_commit(drv); + + // Read state of lock detect pin. + bool initial = gpio_read(drv->gpio_ld); + + // Enable lock detect output. + set_MAX2831_LOCK_DETECT_OUTPUT_EN(drv, true); + max2831_regs_commit(drv); + + // Read new state of lock detect pin. + bool new = gpio_read(drv->gpio_ld); + + // If the pin state changed, we know our writes are working. + selftest.max2831_ld_test_ok = initial != new; + if (!selftest.max2831_ld_test_ok) { + selftest.report.pass = false; + } +} + +/* + * Set up pins for GPIO and SPI control, configure SSP peripheral for SPI, and + * set our own default register configuration. + */ +void max2831_setup(max2831_driver_t* const drv) +{ + max2831_init(drv); + + /* Use SPI control instead of B1-B7 pins for gain settings. */ + set_MAX2831_RXVGA_GAIN_SPI_EN(drv, 1); + set_MAX2831_TXVGA_GAIN_SPI_EN(drv, 1); + + //set_MAX2831_TXVGA_GAIN(0x3f); /* maximum gain */ + set_MAX2831_TXVGA_GAIN(drv, 0x00); /* minimum gain */ + //set_MAX2831_RX_HPF_SEL(drv, MAX2831_RX_HPF_100_HZ); + set_MAX2831_LNA_GAIN(drv, MAX2831_LNA_GAIN_MAX); /* maximum gain */ + set_MAX2831_RXVGA_GAIN(drv, 0x18); + + /* maximum rx output common-mode voltage */ + //set_MAX2831_RXIQ_VCM(drv, MAX2831_RXIQ_VCM_1_2); + + /* configure baseband filter for 8 MHz TX */ + set_MAX2831_LPF_COARSE(drv, MAX2831_RX_LPF_7_5M); + set_MAX2831_RX_LPF_FINE_ADJ(drv, MAX2831_RX_LPF_FINE_100); + set_MAX2831_TX_LPF_FINE_ADJ(drv, MAX2831_TX_LPF_FINE_100); + + /* clock output disable */ + set_MAX2831_CLKOUT_PIN_EN(drv, 0); + + max2831_regs_commit(drv); +} + +static void max2831_write(max2831_driver_t* const drv, uint8_t r, uint16_t v) +{ + uint32_t word = (((uint32_t) v & 0x3fff) << 4) | (r & 0xf); + uint16_t values[2] = {word >> 9, word & 0x1ff}; + spi_bus_transfer(drv->bus, values, 2); +} + +uint16_t max2831_reg_read(max2831_driver_t* const drv, uint8_t r) +{ + return drv->regs[r]; +} + +void max2831_reg_write(max2831_driver_t* const drv, uint8_t r, uint16_t v) +{ + drv->regs[r] = v; + max2831_write(drv, r, v); + MAX2831_REG_SET_CLEAN(drv, r); +} + +static inline void max2831_reg_commit(max2831_driver_t* const drv, uint8_t r) +{ + max2831_reg_write(drv, r, drv->regs[r]); +} + +void max2831_regs_commit(max2831_driver_t* const drv) +{ + int r; + for (r = 0; r < MAX2831_NUM_REGS; r++) { + if ((drv->regs_dirty >> r) & 0x1) { + max2831_reg_commit(drv, r); + } + } +} + +void max2831_set_mode(max2831_driver_t* const drv, const max2831_mode_t new_mode) +{ + // Only change calibration bits if necessary to reduce SPI activity. + bool tx_cal = (new_mode == MAX2831_MODE_TX_CALIBRATION); + bool rx_cal = (new_mode == MAX2831_MODE_RX_CALIBRATION); + if (get_MAX2831_TX_CAL_MODE_EN(drv) != tx_cal) { + set_MAX2831_TX_CAL_MODE_EN(drv, tx_cal); + max2831_regs_commit(drv); + } + if (get_MAX2831_RX_CAL_MODE_EN(drv) != rx_cal) { + set_MAX2831_RX_CAL_MODE_EN(drv, rx_cal); + max2831_regs_commit(drv); + } + + drv->set_mode(drv, new_mode); + max2831_set_lpf_bandwidth(drv, drv->desired_lpf_bw); +} + +max2831_mode_t max2831_mode(max2831_driver_t* const drv) +{ + return drv->mode; +} + +void max2831_start(max2831_driver_t* const drv) +{ + max2831_regs_commit(drv); + max2831_set_mode(drv, MAX2831_MODE_STANDBY); +} + +void max2831_tx(max2831_driver_t* const drv) +{ + max2831_regs_commit(drv); + max2831_set_mode(drv, MAX2831_MODE_TX); +} + +void max2831_rx(max2831_driver_t* const drv) +{ + max2831_regs_commit(drv); + max2831_set_mode(drv, MAX2831_MODE_RX); +} + +void max2831_tx_calibration(max2831_driver_t* const drv) +{ + max2831_regs_commit(drv); + max2831_set_mode(drv, MAX2831_MODE_TX_CALIBRATION); +} + +void max2831_rx_calibration(max2831_driver_t* const drv) +{ + max2831_regs_commit(drv); + max2831_set_mode(drv, MAX2831_MODE_RX_CALIBRATION); +} + +void max2831_stop(max2831_driver_t* const drv) +{ + max2831_regs_commit(drv); + max2831_set_mode(drv, MAX2831_MODE_SHUTDOWN); +} + +void max2831_set_frequency(max2831_driver_t* const drv, uint32_t freq) +{ + uint32_t div_frac; + uint32_t div_int; + uint32_t div_rem; + uint32_t div_cmp; + int i; + + /* ASSUME 40MHz PLL. Ratio = F*R/40,000,000. */ + /* TODO: fixed to R=2. Check if it's worth exploring R=1. */ + freq += (20000000 >> 21); /* round to nearest frequency */ + div_int = freq / 20000000; + div_rem = freq % 20000000; + div_frac = 0; + div_cmp = 20000000; + for (i = 0; i < 20; i++) { + div_frac <<= 1; + div_rem <<= 1; + if (div_rem >= div_cmp) { + div_frac |= 0x1; + div_rem -= div_cmp; + } + } + + /* Write order matters? */ + //set_MAX2831_SYN_REF_DIV(drv, MAX2831_SYN_REF_DIV_2); + set_MAX2831_SYN_INT(drv, div_int); + set_MAX2831_SYN_FRAC_HI(drv, (div_frac >> 6) & 0x3fff); + set_MAX2831_SYN_FRAC_LO(drv, div_frac & 0x3f); + max2831_regs_commit(drv); +} + +typedef struct { + uint32_t bandwidth_hz; + uint8_t ft; +} max2831_ft_t; + +typedef struct { + uint8_t percent; + uint8_t ft_fine; +} max2831_ft_fine_t; + +// clang-format off +/* measured -0.5 dB complex baseband bandwidth for each register setting */ +static const max2831_ft_t max2831_rx_ft[] = { + { 11600000, MAX2831_RX_LPF_7_5M }, + { 15100000, MAX2831_RX_LPF_8_5M }, + { 22600000, MAX2831_RX_LPF_15M }, + { 28300000, MAX2831_RX_LPF_18M }, + { 0, 0 }, +}; + +static const max2831_ft_fine_t max2831_rx_ft_fine[] = { + { 90, MAX2831_RX_LPF_FINE_90 }, + { 95, MAX2831_RX_LPF_FINE_95 }, + { 100, MAX2831_RX_LPF_FINE_100 }, + { 105, MAX2831_RX_LPF_FINE_105 }, + { 110, MAX2831_RX_LPF_FINE_110 }, + { 0, 0 }, +}; + +static const max2831_ft_t max2831_tx_ft[] = { + { 16000000, MAX2831_TX_LPF_8M }, + { 22000000, MAX2831_TX_LPF_11M }, + { 33000000, MAX2831_TX_LPF_16_5M }, + { 45000000, MAX2831_TX_LPF_22_5M }, + { 0, 0 }, +}; + +static const max2831_ft_fine_t max2831_tx_ft_fine[] = { + { 90, MAX2831_TX_LPF_FINE_90 }, + { 95, MAX2831_TX_LPF_FINE_95 }, + { 100, MAX2831_TX_LPF_FINE_100 }, + { 105, MAX2831_TX_LPF_FINE_105 }, + { 110, MAX2831_TX_LPF_FINE_110 }, + { 115, MAX2831_TX_LPF_FINE_115 }, + { 0, 0 }, +}; +//clang-format on + + +uint32_t max2831_set_lpf_bandwidth(max2831_driver_t* const drv, const uint32_t bandwidth_hz) { + const max2831_ft_t* coarse; + const max2831_ft_fine_t* fine; + + drv->desired_lpf_bw = bandwidth_hz; + + if (drv->mode == MAX2831_MODE_RX) { + coarse = max2831_rx_ft; + fine = max2831_rx_ft_fine; + } else { + coarse = max2831_tx_ft; + fine = max2831_tx_ft_fine; + } + + /* Find coarse and fine settings for LPF. */ + bool found = false; + const max2831_ft_fine_t* f = fine; + for (; coarse->bandwidth_hz != 0; coarse++) { + uint32_t coarse_aux = coarse->bandwidth_hz / 100; + for (f = fine; f->percent != 0; f++) { + if ((coarse_aux * f->percent) >= drv->desired_lpf_bw) { + found = true; + break; + } + } + if (found) break; + } + + /* + * Use the widest setting if a wider bandwidth than our maximum is + * requested. + */ + if (!found) { + coarse--; + f--; + } + + /* Program found settings. */ + set_MAX2831_LPF_COARSE(drv, coarse->ft); + if (drv->mode == MAX2831_MODE_RX) { + set_MAX2831_RX_LPF_FINE_ADJ(drv, f->ft_fine); + } else { + set_MAX2831_TX_LPF_FINE_ADJ(drv, f->ft_fine); + } + max2831_regs_commit(drv); + + return coarse->bandwidth_hz * f->percent / 100; +} + +bool max2831_set_lna_gain(max2831_driver_t* const drv, const uint32_t gain_db) { + uint16_t val; + switch(gain_db){ + case 40: // MAX2837 compatibility + case 33: + case 32: // MAX2837 compatibility + val = MAX2831_LNA_GAIN_MAX; + break; + case 24: // MAX2837 compatibility + case 16: + val = MAX2831_LNA_GAIN_M16; + break; + case 8: // MAX2837 compatibility + case 0: + val = MAX2831_LNA_GAIN_M33; + break; + default: + return false; + } + set_MAX2831_LNA_GAIN(drv, val); + max2831_reg_commit(drv, 11); + return true; +} + +bool max2831_set_vga_gain(max2831_driver_t* const drv, const uint32_t gain_db) { + if( (gain_db & 0x1) || gain_db > 62) {/* 0b11111*2 */ + return false; +} + + set_MAX2831_RXVGA_GAIN(drv, (gain_db >> 1) ); + max2831_reg_commit(drv, 11); + return true; +} + +bool max2831_set_txvga_gain(max2831_driver_t* const drv, const uint32_t gain_db) { + uint16_t value = MIN((gain_db << 1) | 1, 0x3f); + set_MAX2831_TXVGA_GAIN(drv, value); + max2831_reg_commit(drv, 12); + return true; +} diff --git a/firmware/common/max2831.h b/firmware/common/max2831.h new file mode 100644 index 00000000..4279cbc8 --- /dev/null +++ b/firmware/common/max2831.h @@ -0,0 +1,99 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __MAX2831_H +#define __MAX2831_H + +#include +#include + +#include "gpio.h" +#include "spi_bus.h" + +/* 16 registers, each containing 14 bits of data. */ +#define MAX2831_NUM_REGS 16 +#define MAX2831_DATA_REGS_MAX_VALUE 16384 + +typedef enum { + MAX2831_MODE_SHUTDOWN, + MAX2831_MODE_STANDBY, + MAX2831_MODE_TX, + MAX2831_MODE_RX, + MAX2831_MODE_TX_CALIBRATION, + MAX2831_MODE_RX_CALIBRATION, +} max2831_mode_t; + +struct max2831_driver_t; +typedef struct max2831_driver_t max2831_driver_t; + +struct max2831_driver_t { + spi_bus_t* bus; + gpio_t gpio_enable; + gpio_t gpio_rxtx; + gpio_t gpio_rxhp; + gpio_t gpio_ld; + void (*target_init)(max2831_driver_t* const drv); + void (*set_mode)(max2831_driver_t* const drv, const max2831_mode_t new_mode); + max2831_mode_t mode; + uint16_t regs[MAX2831_NUM_REGS]; + uint16_t regs_dirty; + uint32_t desired_lpf_bw; +}; + +/* Initialize chip. */ +extern void max2831_setup(max2831_driver_t* const drv); + +/* Read a register via SPI. Save a copy to memory and return + * value. Mark clean. */ +extern uint16_t max2831_reg_read(max2831_driver_t* const drv, uint8_t r); + +/* Write value to register via SPI and save a copy to memory. Mark + * clean. */ +extern void max2831_reg_write(max2831_driver_t* const drv, uint8_t r, uint16_t v); + +/* Write all dirty registers via SPI from memory. Mark all clean. Some + * operations require registers to be written in a certain order. Use + * provided routines for those operations. */ +extern void max2831_regs_commit(max2831_driver_t* const drv); + +max2831_mode_t max2831_mode(max2831_driver_t* const drv); +void max2831_set_mode(max2831_driver_t* const drv, const max2831_mode_t new_mode); + +/* Turn on/off all chip functions. Does not control oscillator and CLKOUT */ +extern void max2831_start(max2831_driver_t* const drv); +extern void max2831_stop(max2831_driver_t* const drv); + +/* Set frequency in Hz. Frequency setting is a multi-step function + * where order of register writes matters. */ +extern void max2831_set_frequency(max2831_driver_t* const drv, uint32_t freq); +uint32_t max2831_set_lpf_bandwidth( + max2831_driver_t* const drv, + const uint32_t bandwidth_hz); +bool max2831_set_lna_gain(max2831_driver_t* const drv, const uint32_t gain_db); +bool max2831_set_vga_gain(max2831_driver_t* const drv, const uint32_t gain_db); +bool max2831_set_txvga_gain(max2831_driver_t* const drv, const uint32_t gain_db); + +extern void max2831_tx(max2831_driver_t* const drv); +extern void max2831_rx(max2831_driver_t* const drv); +extern void max2831_tx_calibration(max2831_driver_t* const drv); +extern void max2831_rx_calibration(max2831_driver_t* const drv); + +#endif // __MAX2831_H diff --git a/firmware/common/max2831_regs.def b/firmware/common/max2831_regs.def new file mode 100644 index 00000000..fb8fa8a9 --- /dev/null +++ b/firmware/common/max2831_regs.def @@ -0,0 +1,132 @@ +/* -*- mode: c -*- */ + +#ifndef __MAX2831_REGS_DEF +#define __MAX2831_REGS_DEF + +/* Generate static inline accessors that operate on the global + * regs. Done this way to (1) allow defs to be scraped out and used + * elsewhere, e.g. in scripts, (2) to avoid dealing with endian + * (structs). This may be used in firmware, or on host predefined + * register loads. */ + +#define MAX2831_REG_SET_CLEAN(_d, _r) (_d->regs_dirty &= ~(1UL<<_r)) +#define MAX2831_REG_SET_DIRTY(_d, _r) (_d->regs_dirty |= (1UL<<_r)) + +/* On set_, register is always set dirty, even if nothing + * changed. This makes sure that write that have side effects, + * e.g. frequency setting, are not skipped. */ + +/* n=name, r=regnum, o=offset (bits from LSB), l=length (bits) */ +#define __MREG__(n,r,o,l) \ +static inline uint16_t get_##n(max2831_driver_t* const _d) { \ + return (_d->regs[r] >> o) & ((1<regs[r] &= ~(((1<regs[r] |= ((v&((1< + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "max2831_target.h" + +#include +#include "hackrf_core.h" + +void max2831_target_init(max2831_driver_t* const drv) +{ + /* Configure SSP1 Peripheral (to be moved later in SSP driver) */ + scu_pinmux(SCU_SSP1_COPI, (SCU_SSP_IO | SCU_CONF_FUNCTION5)); + scu_pinmux(SCU_SSP1_SCK, (SCU_SSP_IO | SCU_CONF_FUNCTION1)); + + scu_pinmux(SCU_XCVR_CS, SCU_XCVR_CS_PINCFG); + + /* + * Configure XCVR_CTL GPIO pins. + * + * The RXTX pin is also known as RXENABLE because of its use on the + * MAX2837 which had a separate TXENABLE. On MAX2831 a single RXTX pin + * switches between RX (high) and TX (low) modes. + */ + scu_pinmux(SCU_XCVR_ENABLE, SCU_XCVR_ENABLE_PINCFG); + scu_pinmux(SCU_XCVR_RXENABLE, SCU_XCVR_RXENABLE_PINCFG); + scu_pinmux(SCU_XCVR_RXHP, SCU_XCVR_RXHP_PINCFG); + scu_pinmux(SCU_XCVR_LD, SCU_XCVR_LD_PINCFG); + + /* Set GPIO pins as outputs. */ + gpio_output(drv->gpio_enable); + gpio_output(drv->gpio_rxtx); + gpio_output(drv->gpio_rxhp); + gpio_input(drv->gpio_ld); +} + +void max2831_target_set_mode(max2831_driver_t* const drv, const max2831_mode_t new_mode) +{ + /* MAX2831_MODE_SHUTDOWN: + * All circuit blocks are powered down, except the 4-wire serial bus + * and its internal programmable registers. + * + * MAX2831_MODE_STANDBY: + * Used to enable the frequency synthesizer block while the rest of the + * device is powered down. In this mode, PLL, VCO, and LO generator + * are on, so that Tx or Rx modes can be quickly enabled from this mode. + * These and other blocks can be selectively enabled in this mode. + * + * MAX2831_MODE_TX: + * All Tx circuit blocks are powered on. The external PA is powered on + * after a programmable delay using the on-chip PA bias DAC. The slow- + * charging Rx circuits are in a precharged “idle-off” state for fast + * Tx-to-Rx turnaround time. + * + * MAX2831_MODE_RX: + * All Rx circuit blocks are powered on and active. Antenna signal is + * applied; RF is downconverted, filtered, and buffered at Rx BB I and Q + * outputs. The slow- charging Tx circuits are in a precharged “idle-off” + * state for fast Rx-to-Tx turnaround time. + */ + + switch (new_mode) { + default: + case MAX2831_MODE_SHUTDOWN: + gpio_clear(drv->gpio_rxtx); + gpio_clear(drv->gpio_enable); + break; + case MAX2831_MODE_STANDBY: + gpio_set(drv->gpio_rxtx); + gpio_clear(drv->gpio_enable); + break; + case MAX2831_MODE_TX: + case MAX2831_MODE_TX_CALIBRATION: + gpio_set(drv->gpio_rxtx); + gpio_set(drv->gpio_enable); + break; + case MAX2831_MODE_RX: + case MAX2831_MODE_RX_CALIBRATION: + gpio_clear(drv->gpio_rxtx); + gpio_set(drv->gpio_enable); + break; + } + drv->mode = new_mode; +} diff --git a/firmware/common/max2831_target.h b/firmware/common/max2831_target.h new file mode 100644 index 00000000..11671b73 --- /dev/null +++ b/firmware/common/max2831_target.h @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __MAX2831_TARGET_H +#define __MAX2831_TARGET_H + +#include "max2831.h" + +void max2831_target_init(max2831_driver_t* const drv); +void max2831_target_set_mode(max2831_driver_t* const drv, const max2831_mode_t new_mode); + +#endif // __MAX2831_TARGET_H diff --git a/firmware/common/max2837.c b/firmware/common/max2837.c index 46da787d..a3e91498 100644 --- a/firmware/common/max2837.c +++ b/firmware/common/max2837.c @@ -33,11 +33,12 @@ #include #include "max2837.h" #include "max2837_regs.def" // private register def macros +#include "selftest.h" /* Default register values. */ static const uint16_t max2837_regs_default[MAX2837_NUM_REGS] = { 0x150, /* 0 */ - 0x002, /* 1 */ + 0x002, /* 1: data sheet says 0x002 but read 0x1c2 */ 0x1f4, /* 2 */ 0x1b9, /* 3 */ 0x00a, /* 4 */ @@ -66,13 +67,17 @@ static const uint16_t max2837_regs_default[MAX2837_NUM_REGS] = { 0x1a9, /* 22 */ 0x24f, /* 23 */ 0x180, /* 24 */ - 0x100, /* 25 */ + 0x100, /* 25: data sheet says 0x100 but read 0x10a */ 0x3ca, /* 26 */ - 0x3e3, /* 27 */ + 0x3e3, /* 27: data sheet says 0x100 but read 0x3f3 */ 0x0c0, /* 28 */ 0x3f0, /* 29 */ - 0x080, /* 30 */ - 0x000}; /* 31 */ + 0x080, /* 30: data sheet says 0x080 but read 0x092 */ + 0x000}; /* 31: data sheet says 0x000 but read 0x1ae */ + +static const uint8_t max2837_regs_skip_verify[] = {1, 25, 27, 30, 31}; + +static uint16_t max2837_read(max2837_driver_t* const drv, uint8_t r); /* Set up all registers according to defaults specified in docs. */ static void max2837_init(max2837_driver_t* const drv) @@ -85,6 +90,28 @@ static void max2837_init(max2837_driver_t* const drv) /* Write default register values to chip. */ max2837_regs_commit(drv); + + /* Read back registers to verify. */ + selftest.max283x_readback_total_registers = MAX2837_NUM_REGS; + for (int r = 0; r < MAX2837_NUM_REGS; r++) { + for (unsigned int i = 0; i < sizeof(max2837_regs_skip_verify); i++) { + if (max2837_regs_skip_verify[i] == r) { + goto next; + } + } + uint16_t value = max2837_read(drv, r); + if (value != drv->regs[r]) { + selftest.max283x_readback_bad_value = value; + selftest.max283x_readback_expected_value = drv->regs[r]; + break; + } +next: + selftest.max283x_readback_register_count = r + 1; + } + + if (selftest.max283x_readback_register_count < MAX2837_NUM_REGS) { + selftest.report.pass = false; + } } /* diff --git a/firmware/common/max2837_target.c b/firmware/common/max2837_target.c index 29f692da..dc8f69c1 100644 --- a/firmware/common/max2837_target.c +++ b/firmware/common/max2837_target.c @@ -33,12 +33,12 @@ void max2837_target_init(max2837_driver_t* const drv) scu_pinmux(SCU_SSP1_COPI, (SCU_SSP_IO | SCU_CONF_FUNCTION5)); scu_pinmux(SCU_SSP1_SCK, (SCU_SSP_IO | SCU_CONF_FUNCTION1)); - scu_pinmux(SCU_XCVR_CS, SCU_GPIO_FAST); + scu_pinmux(SCU_XCVR_CS, SCU_XCVR_CS_PINCFG); /* Configure XCVR_CTL GPIO pins. */ - scu_pinmux(SCU_XCVR_ENABLE, SCU_GPIO_FAST); - scu_pinmux(SCU_XCVR_RXENABLE, SCU_GPIO_FAST); - scu_pinmux(SCU_XCVR_TXENABLE, SCU_GPIO_FAST); + scu_pinmux(SCU_XCVR_ENABLE, SCU_XCVR_ENABLE_PINCFG); + scu_pinmux(SCU_XCVR_RXENABLE, SCU_XCVR_RXENABLE_PINCFG); + scu_pinmux(SCU_XCVR_TXENABLE, SCU_XCVR_TXENABLE_PINCFG); /* Set GPIO pins as outputs. */ gpio_output(drv->gpio_enable); diff --git a/firmware/common/max2839.c b/firmware/common/max2839.c index bd49b924..62c5561f 100644 --- a/firmware/common/max2839.c +++ b/firmware/common/max2839.c @@ -33,6 +33,7 @@ #include #include "max2839.h" #include "max2839_regs.def" // private register def macros +#include "selftest.h" static uint8_t requested_lna_gain = 0; static uint8_t requested_vga_gain = 0; @@ -40,9 +41,9 @@ static uint8_t requested_vga_gain = 0; /* Default register values. */ static const uint16_t max2839_regs_default[MAX2839_NUM_REGS] = { 0x000, /* 0 */ - 0x00c, /* 1: data sheet says 0x00c but read 0x22c */ + 0x00c, /* 1: data sheet says 0x00c but read 0x20c or 0x22c*/ 0x080, /* 2 */ - 0x1b9, /* 3: data sheet says 0x1b9 but read 0x1b0 */ + 0x1b0, /* 3: data sheet says 0x1b9 but read 0x1b0 or 0x1b9 */ 0x3e6, /* 4 */ 0x100, /* 5 */ 0x000, /* 6 */ @@ -64,12 +65,12 @@ static const uint16_t max2839_regs_default[MAX2839_NUM_REGS] = { 0x1a9, /* 22 */ 0x24f, /* 23 */ 0x180, /* 24 */ - 0x000, /* 25: data sheet says 0x000 but read 0x00a */ + 0x00a, /* 25: data sheet says 0x000 but read 0x00a */ 0x3c0, /* 26 */ - 0x200, /* 27: data sheet says 0x200 but read 0x22a */ + 0x200, /* 27: data sheet says 0x200 but read 0x22a or 0x22f */ 0x0c0, /* 28 */ - 0x03f, /* 29: data sheet says 0x03f but read 0x07f */ - 0x300, /* 30: data sheet says 0x300 but read 0x398 */ + 0x03f, /* 29: data sheet says 0x03f but read 0x07f or 0x17f */ + 0x300, /* 30: data sheet says 0x300 but read 0x398 or 0x31a */ 0x340}; /* 31: data sheet says 0x340 but read 0x359 */ /* @@ -79,6 +80,10 @@ static const uint16_t max2839_regs_default[MAX2839_NUM_REGS] = { * different settings. */ +static const uint8_t max2839_regs_skip_verify[] = {1, 3, 8, 11, 21, 25, 27, 29, 30, 31}; + +static uint16_t max2839_read(max2839_driver_t* const drv, uint8_t r); + /* Set up all registers according to defaults specified in docs. */ static void max2839_init(max2839_driver_t* const drv) { @@ -90,6 +95,28 @@ static void max2839_init(max2839_driver_t* const drv) /* Write default register values to chip. */ max2839_regs_commit(drv); + + /* Read back registers to verify. */ + selftest.max283x_readback_total_registers = MAX2839_NUM_REGS; + for (int r = 0; r < MAX2839_NUM_REGS; r++) { + for (unsigned int i = 0; i < sizeof(max2839_regs_skip_verify); i++) { + if (max2839_regs_skip_verify[i] == r) { + goto next; + } + } + uint16_t value = max2839_read(drv, r); + if (value != drv->regs[r]) { + selftest.max283x_readback_bad_value = value; + selftest.max283x_readback_expected_value = drv->regs[r]; + break; + } +next: + selftest.max283x_readback_register_count = r + 1; + } + + if (selftest.max283x_readback_register_count < MAX2839_NUM_REGS) { + selftest.report.pass = false; + } } /* diff --git a/firmware/common/max283x.c b/firmware/common/max283x.c index 62f9829d..5d291804 100644 --- a/firmware/common/max283x.c +++ b/firmware/common/max283x.c @@ -32,9 +32,15 @@ #include "spi_bus.h" extern spi_bus_t spi_bus_ssp1; +#ifdef PRALINE +static struct gpio_t gpio_max2837_enable = GPIO(6, 29); +static struct gpio_t gpio_max2837_rx_enable = GPIO(3, 3); +static struct gpio_t gpio_max2837_tx_enable = GPIO(3, 2); +#else static struct gpio_t gpio_max2837_enable = GPIO(2, 6); static struct gpio_t gpio_max2837_rx_enable = GPIO(2, 5); static struct gpio_t gpio_max2837_tx_enable = GPIO(2, 4); +#endif max2837_driver_t max2837 = { .bus = &spi_bus_ssp1, diff --git a/firmware/common/max2871.c b/firmware/common/max2871.c index 11a0e0be..598599cc 100644 --- a/firmware/common/max2871.c +++ b/firmware/common/max2871.c @@ -21,6 +21,7 @@ #include "max2871.h" #include "max2871_regs.h" +#include "selftest.h" #if (defined DEBUG) #include @@ -35,6 +36,7 @@ #include #include +static uint32_t max2871_spi_read(max2871_driver_t* const drv); static void max2871_spi_write(max2871_driver_t* const drv, uint8_t r, uint32_t v); static void max2871_write_registers(max2871_driver_t* const drv); static void delay_ms(int ms); @@ -67,6 +69,11 @@ void max2871_setup(max2871_driver_t* const drv) gpio_set(drv->gpio_vco_le); /* active low */ gpio_set(drv->gpio_synt_rfout_en); /* active high */ + selftest.mixer_id = max2871_spi_read(drv) >> MAX2871_DIE_SHIFT; + if (selftest.mixer_id != 7) { + selftest.report.pass = false; + } + max2871_regs_init(); int i; for (i = 5; i >= 0; i--) { diff --git a/firmware/common/max2871_regs.h b/firmware/common/max2871_regs.h index 70ce8d99..a18f6217 100644 --- a/firmware/common/max2871_regs.h +++ b/firmware/common/max2871_regs.h @@ -23,7 +23,8 @@ #define MAX2871_REGS_H #include -#define MAX2871_VASA (1 << 9) +#define MAX2871_VASA (1 << 9) +#define MAX2871_DIE_SHIFT 28 void max2871_regs_init(void); uint32_t max2871_get_register(int reg); diff --git a/firmware/common/max5864_target.c b/firmware/common/max5864_target.c index d020ca6e..ed433415 100644 --- a/firmware/common/max5864_target.c +++ b/firmware/common/max5864_target.c @@ -38,5 +38,5 @@ void max5864_target_init(max5864_driver_t* const drv) * Configure CS_AD pin to keep the MAX5864 SPI disabled while we use the * SPI bus for the MAX2837. FIXME: this should probably be somewhere else. */ - scu_pinmux(SCU_AD_CS, SCU_GPIO_FAST); + scu_pinmux(SCU_AD_CS, SCU_AD_CS_PINCFG); } diff --git a/firmware/common/mixer.c b/firmware/common/mixer.c index e2747317..e9c512fe 100644 --- a/firmware/common/mixer.c +++ b/firmware/common/mixer.c @@ -41,9 +41,16 @@ static struct gpio_t gpio_vco_le = GPIO(2, 14); static struct gpio_t gpio_vco_mux = GPIO(5, 25); static struct gpio_t gpio_synt_rfout_en = GPIO(3, 5); #endif +#ifdef PRALINE +static struct gpio_t gpio_rffc5072_select = GPIO(2, 13); +static struct gpio_t gpio_rffc5072_clock = GPIO(5, 18); +static struct gpio_t gpio_rffc5072_data = GPIO(4, 14); +static struct gpio_t gpio_rffc5072_reset = GPIO(2, 14); +static struct gpio_t gpio_rffc5072_ld = GPIO(6, 25); +#endif // clang-format on -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) const rffc5071_spi_config_t rffc5071_spi_config = { .gpio_select = &gpio_rffc5072_select, .gpio_clock = &gpio_rffc5072_clock, @@ -61,6 +68,9 @@ spi_bus_t spi_bus_rffc5071 = { mixer_driver_t mixer = { .bus = &spi_bus_rffc5071, .gpio_reset = &gpio_rffc5072_reset, + #ifdef PRALINE + .gpio_ld = &gpio_rffc5072_ld, + #endif }; #endif #ifdef RAD1O @@ -76,7 +86,7 @@ mixer_driver_t mixer = { void mixer_bus_setup(mixer_driver_t* const mixer) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) (void) mixer; spi_bus_start(&spi_bus_rffc5071, &rffc5071_spi_config); #endif @@ -87,7 +97,7 @@ void mixer_bus_setup(mixer_driver_t* const mixer) void mixer_setup(mixer_driver_t* const mixer) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) rffc5071_setup(mixer); #endif #ifdef RAD1O @@ -97,7 +107,7 @@ void mixer_setup(mixer_driver_t* const mixer) uint64_t mixer_set_frequency(mixer_driver_t* const mixer, uint64_t hz) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) return rffc5071_set_frequency(mixer, hz); #endif #ifdef RAD1O @@ -107,7 +117,7 @@ uint64_t mixer_set_frequency(mixer_driver_t* const mixer, uint64_t hz) void mixer_tx(mixer_driver_t* const mixer) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) rffc5071_tx(mixer); #endif #ifdef RAD1O @@ -117,7 +127,7 @@ void mixer_tx(mixer_driver_t* const mixer) void mixer_rx(mixer_driver_t* const mixer) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) rffc5071_rx(mixer); #endif #ifdef RAD1O @@ -127,7 +137,7 @@ void mixer_rx(mixer_driver_t* const mixer) void mixer_rxtx(mixer_driver_t* const mixer) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) rffc5071_rxtx(mixer); #endif #ifdef RAD1O @@ -137,7 +147,7 @@ void mixer_rxtx(mixer_driver_t* const mixer) void mixer_enable(mixer_driver_t* const mixer) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) rffc5071_enable(mixer); #endif #ifdef RAD1O @@ -147,7 +157,7 @@ void mixer_enable(mixer_driver_t* const mixer) void mixer_disable(mixer_driver_t* const mixer) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) rffc5071_disable(mixer); #endif #ifdef RAD1O @@ -157,7 +167,7 @@ void mixer_disable(mixer_driver_t* const mixer) void mixer_set_gpo(mixer_driver_t* const mixer, uint8_t gpo) { -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) rffc5071_set_gpo(mixer, gpo); #endif #ifdef RAD1O diff --git a/firmware/common/mixer.h b/firmware/common/mixer.h index c882e898..fc732275 100644 --- a/firmware/common/mixer.h +++ b/firmware/common/mixer.h @@ -23,7 +23,7 @@ #ifndef __MIXER_H #define __MIXER_H -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) #include "rffc5071.h" typedef rffc5071_driver_t mixer_driver_t; #endif diff --git a/firmware/common/operacake_sctimer.c b/firmware/common/operacake_sctimer.c index 7d8af774..fca5d03b 100644 --- a/firmware/common/operacake_sctimer.c +++ b/firmware/common/operacake_sctimer.c @@ -51,6 +51,9 @@ static uint32_t default_output = 0; * * To trigger the antenna switching synchronously with the sample clock, the * SGPIO is configured to output its clock (f=2 * sample clock) to the SCTimer. + * + * On Praline, instead, MS0/CLK1 (SCT_CLK) is configured to output its + * clock (f=2 * sample clock) to the SCTimer. */ void operacake_sctimer_init() { @@ -89,6 +92,7 @@ void operacake_sctimer_init() P7_0, SCU_CONF_EPUN_DIS_PULLUP | SCU_CONF_EHS_FAST | SCU_CONF_FUNCTION1); +#ifndef PRALINE // Configure the SGPIO to output the clock (f=2 * sample clock) on pin 12 SGPIO_OUT_MUX_CFG12 = SGPIO_OUT_MUX_CFG_P_OUT_CFG(0x08) | // clkout output mode SGPIO_OUT_MUX_CFG_P_OE_CFG(0); // gpio_oe @@ -97,9 +101,20 @@ void operacake_sctimer_init() // Use the GIMA to connect the SGPIO clock to the SCTimer GIMA_CTIN_1_IN = 0x2 << 4; // Route SGPIO12 to SCTIN1 + uint8_t sct_clock_input = SCT_CONFIG_CKSEL_RISING_EDGES_ON_INPUT_1; +#else + // Configure pin P6_4 as SCT_IN_6 + scu_pinmux(P6_4, SCU_CLK_IN | SCU_CONF_FUNCTION1); + + // Use the GIMA to connect MS0/CLK1 (SCT_CLK) on pin P6_4 to the SCTimer + GIMA_CTIN_6_IN = 0x0 << 4; + + uint8_t sct_clock_input = SCT_CONFIG_CKSEL_RISING_EDGES_ON_INPUT_6; +#endif + // We configure this register first, because the user manual says to SCT_CONFIG |= SCT_CONFIG_UNIFY_32_BIT | SCT_CONFIG_CLKMODE_PRESCALED_BUS_CLOCK | - SCT_CONFIG_CKSEL_RISING_EDGES_ON_INPUT_1; + sct_clock_input; // Halt the SCTimer to enable it to be configured SCT_CTRL = SCT_CTRL_HALT_L(1); diff --git a/firmware/common/platform_detect.c b/firmware/common/platform_detect.c index 85113b08..73656479 100644 --- a/firmware/common/platform_detect.c +++ b/firmware/common/platform_detect.c @@ -32,28 +32,32 @@ static board_rev_t revision = BOARD_REV_UNDETECTED; static struct gpio_t gpio2_9_on_P5_0 = GPIO(2, 9); static struct gpio_t gpio3_6_on_P6_10 = GPIO(3, 6); +static struct gpio_t gpio3_4_on_P6_5 = GPIO(3, 4); +static struct gpio_t gpio2_6_on_P4_6 = GPIO(2, 6); #define P5_0_PUP (1 << 0) #define P5_0_PDN (1 << 1) #define P6_10_PUP (1 << 2) #define P6_10_PDN (1 << 3) +#define P6_5_PDN (1 << 4) /* * Jawbreaker has a pull-down on P6_10 and nothing on P5_0. * rad1o has a pull-down on P6_10 and a pull-down on P5_0. * HackRF One OG has a pull-down on P6_10 and a pull-up on P5_0. * HackRF One r9 has a pull-up on P6_10 and a pull-down on P5_0. - */ + * Praline has a pull-down on P6_5. */ #define JAWBREAKER_RESISTORS (P6_10_PDN) #define RAD1O_RESISTORS (P6_10_PDN | P5_0_PDN) #define HACKRF1_OG_RESISTORS (P6_10_PDN | P5_0_PUP) #define HACKRF1_R9_RESISTORS (P6_10_PUP | P5_0_PDN) +#define PRALINE_RESISTORS (P6_5_PDN) /* * LEDs are configured so that they flash if the detected hardware platform is * not supported by the firmware binary. Only two LEDs are flashed for a - * hardware detection failure, but three LEDs are flashed if CPLD configuration + * hardware detection failure, but three LEDs are flashed if CPLD/FPGA configuration * fails. */ static struct gpio_t gpio_led1 = GPIO(2, 1); @@ -109,7 +113,7 @@ uint32_t check_pin_strap(uint8_t pin) * scheme with ADC0_3 tied to VCC. */ // clang-format off -static const uint8_t revision_from_adc[32] = { +static const uint8_t hackrf_revision_from_adc[32] = { BOARD_REV_UNRECOGNIZED, BOARD_REV_UNRECOGNIZED, BOARD_REV_UNRECOGNIZED, @@ -144,6 +148,47 @@ static const uint8_t revision_from_adc[32] = { BOARD_REV_HACKRF1_R8 }; +/* + * Starting with r0.1, Praline also uses a voltage on ADC0_3 to set an + * analog voltage that indicates the hardware revision. The high five + * bits of the ADC result are mapped to 32 revisions. Note that, + * unlike HackRF One, Praline revisions are mapped in ascending order. + */ +static const uint8_t praline_revision_from_adc[32] = { + BOARD_REV_PRALINE_R0_1, + BOARD_REV_PRALINE_R0_2, + BOARD_REV_PRALINE_R0_3, + BOARD_REV_PRALINE_R1_0, + BOARD_REV_PRALINE_R1_1, + BOARD_REV_PRALINE_R1_2, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, + BOARD_REV_UNRECOGNIZED, +}; + // clang-format on void detect_hardware_platform(void) @@ -158,6 +203,7 @@ void detect_hardware_platform(void) gpio_input(&gpio2_9_on_P5_0); gpio_input(&gpio3_6_on_P6_10); + gpio_input(&gpio3_4_on_P6_5); /* activate internal pull-down */ scu_pinmux(P5_0, SCU_GPIO_PDN | SCU_CONF_FUNCTION0); @@ -174,14 +220,17 @@ void detect_hardware_platform(void) /* activate internal pull-up */ scu_pinmux(P5_0, SCU_GPIO_PUP | SCU_CONF_FUNCTION0); scu_pinmux(P6_10, SCU_GPIO_PUP | SCU_CONF_FUNCTION0); + scu_pinmux(P6_5, SCU_GPIO_PUP | SCU_CONF_FUNCTION0); delay_us_at_mhz(4, 96); /* tri-state for a moment before testing input */ scu_pinmux(P5_0, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); scu_pinmux(P6_10, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); + scu_pinmux(P6_5, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); delay_us_at_mhz(4, 96); /* if input fell quickly, there must be an external pull-down */ detected_resistors |= (gpio_read(&gpio2_9_on_P5_0)) ? 0 : P5_0_PDN; detected_resistors |= (gpio_read(&gpio3_6_on_P6_10)) ? 0 : P6_10_PDN; + detected_resistors |= (gpio_read(&gpio3_4_on_P6_5)) ? 0 : P6_5_PDN; switch (detected_resistors) { case JAWBREAKER_RESISTORS: @@ -208,6 +257,12 @@ void detect_hardware_platform(void) } platform = BOARD_ID_HACKRF1_R9; break; + case PRALINE_RESISTORS: + if (!(supported_platform() & PLATFORM_PRALINE)) { + halt_and_flash(3000000); + } + platform = BOARD_ID_PRALINE; + break; default: platform = BOARD_ID_UNRECOGNIZED; halt_and_flash(1000000); @@ -225,7 +280,7 @@ void detect_hardware_platform(void) } else if (LOW(adc0_3) && HIGH(adc0_4)) { revision = BOARD_REV_HACKRF1_R7; } else if (LOW(adc0_4)) { - revision = revision_from_adc[adc0_3 >> 5]; + revision = hackrf_revision_from_adc[adc0_3 >> 5]; } else { revision = BOARD_REV_UNRECOGNIZED; } @@ -235,10 +290,27 @@ void detect_hardware_platform(void) } else { revision = BOARD_REV_UNRECOGNIZED; } + } else if (platform == BOARD_ID_PRALINE) { + revision = praline_revision_from_adc[adc0_3 >> 5]; } - if ((revision > BOARD_REV_HACKRF1_OLD) && LOW(adc0_7)) { - revision |= BOARD_REV_GSG; + switch (platform) { + case BOARD_ID_HACKRF1_OG: + case BOARD_ID_HACKRF1_R9: + if ((revision > BOARD_REV_HACKRF1_OLD) && LOW(adc0_7)) { + revision |= BOARD_REV_GSG; + } + break; + case BOARD_ID_PRALINE: + scu_pinmux(P4_6, SCU_GPIO_PDN | SCU_CONF_FUNCTION0); + gpio_input(&gpio2_6_on_P4_6); + if (gpio_read(&gpio2_6_on_P4_6)) { + revision |= BOARD_REV_GSG; + } + scu_pinmux(P4_6, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0); + break; + default: + break; } } diff --git a/firmware/common/platform_detect.h b/firmware/common/platform_detect.h index 49a2c4ff..f02e733b 100644 --- a/firmware/common/platform_detect.h +++ b/firmware/common/platform_detect.h @@ -30,6 +30,7 @@ #define PLATFORM_HACKRF1_OG (1 << 1) #define PLATFORM_RAD1O (1 << 2) #define PLATFORM_HACKRF1_R9 (1 << 3) +#define PLATFORM_PRALINE (1 << 4) typedef enum { BOARD_ID_JELLYBEAN = 0, @@ -37,6 +38,7 @@ typedef enum { BOARD_ID_HACKRF1_OG = 2, /* HackRF One prior to r9 */ BOARD_ID_RAD1O = 3, BOARD_ID_HACKRF1_R9 = 4, + BOARD_ID_PRALINE = 5, BOARD_ID_UNRECOGNIZED = 0xFE, /* tried detection but did not recognize board */ BOARD_ID_UNDETECTED = 0xFF, /* detection not yet attempted */ } board_id_t; @@ -48,11 +50,23 @@ typedef enum { BOARD_REV_HACKRF1_R8 = 3, BOARD_REV_HACKRF1_R9 = 4, BOARD_REV_HACKRF1_R10 = 5, + BOARD_REV_PRALINE_R0_1 = 6, + BOARD_REV_PRALINE_R0_2 = 7, + BOARD_REV_PRALINE_R0_3 = 8, + BOARD_REV_PRALINE_R1_0 = 9, + BOARD_REV_PRALINE_R1_1 = 10, + BOARD_REV_PRALINE_R1_2 = 11, BOARD_REV_GSG_HACKRF1_R6 = 0x81, BOARD_REV_GSG_HACKRF1_R7 = 0x82, BOARD_REV_GSG_HACKRF1_R8 = 0x83, BOARD_REV_GSG_HACKRF1_R9 = 0x84, BOARD_REV_GSG_HACKRF1_R10 = 0x85, + BOARD_REV_GSG_PRALINE_R0_1 = 0x86, + BOARD_REV_GSG_PRALINE_R0_2 = 0x87, + BOARD_REV_GSG_PRALINE_R0_3 = 0x88, + BOARD_REV_GSG_PRALINE_R1_0 = 0x89, + BOARD_REV_GSG_PRALINE_R1_1 = 0x8a, + BOARD_REV_GSG_PRALINE_R1_2 = 0x8b, BOARD_REV_UNRECOGNIZED = 0xFE, /* tried detection but did not recognize revision */ BOARD_REV_UNDETECTED = 0xFF, /* detection not yet attempted */ diff --git a/firmware/common/radio.c b/firmware/common/radio.c new file mode 100644 index 00000000..30db6872 --- /dev/null +++ b/firmware/common/radio.c @@ -0,0 +1,460 @@ +/* + * Copyright 2012-2025 Great Scott Gadgets + * Copyright 2012 Jared Boone + * Copyright 2013 Benjamin Vernoux + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "hackrf_core.h" +#include "tuning.h" + +#include "radio.h" + +radio_error_t radio_set_sample_rate( + radio_t* radio, + radio_chan_id chan_id, + radio_sample_rate_id element, + radio_sample_rate_t sample_rate) +{ + // we only support the clock generator at the moment + if (element != RADIO_SAMPLE_RATE_CLOCKGEN) { + return RADIO_ERR_INVALID_ELEMENT; + } + + radio_config_t* config = &radio->channel[chan_id].config; + + // TODO get the actual tuned frequency from sample_rate_frac_set + sample_rate.hz = (double) sample_rate.num / (double) sample_rate.div; + + if (config->mode == TRANSCEIVER_MODE_OFF) { + config->sample_rate[element] = sample_rate; + return RADIO_OK; + } + + bool ok = sample_rate_frac_set(sample_rate.num, sample_rate.div); + if (!ok) { + return RADIO_ERR_INVALID_PARAM; + } + + config->sample_rate[element] = sample_rate; + return RADIO_OK; +} + +radio_sample_rate_t radio_get_sample_rate( + radio_t* radio, + radio_chan_id chan_id, + radio_sample_rate_id element) +{ + return radio->channel[chan_id].config.sample_rate[element]; +} + +radio_error_t radio_set_filter( + radio_t* radio, + radio_chan_id chan_id, + radio_filter_id element, + radio_filter_t filter) +{ + // we only support the baseband filter at the moment + if (element != RADIO_FILTER_BASEBAND) { + return RADIO_ERR_INVALID_ELEMENT; + } + + radio_config_t* config = &radio->channel[chan_id].config; + + if (config->mode == TRANSCEIVER_MODE_OFF) { + config->filter[element] = filter; + return RADIO_OK; + } + + uint32_t real_hz; +#ifndef PRALINE + real_hz = max283x_set_lpf_bandwidth(&max283x, filter.hz); +#else + real_hz = max2831_set_lpf_bandwidth(&max283x, filter.hz); +#endif + if (real_hz == 0) { + return RADIO_ERR_INVALID_PARAM; + } + + config->filter[element] = (radio_filter_t){.hz = real_hz}; + return RADIO_OK; +} + +radio_filter_t radio_get_filter( + radio_t* radio, + radio_chan_id chan_id, + radio_filter_id element) +{ + return radio->channel[chan_id].config.filter[element]; +} + +radio_error_t radio_set_gain( + radio_t* radio, + radio_chan_id chan_id, + radio_gain_id element, + radio_gain_t gain) +{ + if (element > RADIO_GAIN_COUNT) { + return RADIO_ERR_INVALID_ELEMENT; + } + + radio_config_t* config = &radio->channel[chan_id].config; + + if (config->mode == TRANSCEIVER_MODE_OFF) { + config->gain[element] = gain; + return RADIO_OK; + } + + uint8_t real_db; + switch (element) { + case RADIO_GAIN_RF_AMP: + rf_path_set_lna(&rf_path, gain.enable); + break; + case RADIO_GAIN_RX_LNA: +#ifndef PRALINE + real_db = max283x_set_lna_gain(&max283x, gain.db); +#else + real_db = max2831_set_lna_gain(&max283x, gain.db); +#endif + if (real_db == 0) { + return RADIO_ERR_INVALID_PARAM; + } + break; + case RADIO_GAIN_RX_VGA: +#ifndef PRALINE + real_db = max283x_set_vga_gain(&max283x, gain.db); +#else + real_db = max2831_set_vga_gain(&max283x, gain.db); +#endif + if (real_db == 0) { + return RADIO_ERR_INVALID_PARAM; + } + break; + case RADIO_GAIN_TX_VGA: +#ifndef PRALINE + real_db = max283x_set_txvga_gain(&max283x, gain.db); +#else + real_db = max2831_set_txvga_gain(&max283x, gain.db); +#endif + if (real_db == 0) { + return RADIO_ERR_INVALID_PARAM; + } + break; + } + + config->gain[element] = gain; + return RADIO_OK; +} + +radio_gain_t radio_get_gain(radio_t* radio, radio_chan_id chan_id, radio_gain_id element) +{ + return radio->channel[chan_id].config.gain[element]; +} + +radio_error_t radio_set_frequency( + radio_t* radio, + radio_chan_id chan_id, + radio_frequency_id element, + radio_frequency_t frequency) +{ + // we only support setting the final rf frequency at the moment + if (element != RADIO_FREQUENCY_RF) { + return RADIO_ERR_INVALID_ELEMENT; + } + + radio_config_t* config = &radio->channel[chan_id].config; + + if (config->mode == TRANSCEIVER_MODE_OFF) { + config->frequency[element] = frequency; + return RADIO_OK; + } + + // explicit + if (frequency.if_hz || frequency.lo_hz) { + bool ok = set_freq_explicit( + frequency.if_hz, + frequency.lo_hz, + frequency.path); + if (!ok) { + return RADIO_ERR_INVALID_PARAM; + } + + config->frequency[element] = frequency; + return RADIO_OK; + } + + // auto-tune + uint64_t real_hz; +#ifndef PRALINE + switch (config->mode) { + case TRANSCEIVER_MODE_RX: + case TRANSCEIVER_MODE_RX_SWEEP: + case TRANSCEIVER_MODE_TX: + // TODO return if, of components so we can support them in the getter + real_hz = tuning_set_frequency(max283x_tune_config, frequency.hz); + break; + default: + return RADIO_ERR_INVALID_CONFIG; + } +#else + switch (config->mode) { + case TRANSCEIVER_MODE_RX: + real_hz = tuning_set_frequency(max2831_tune_config_rx, frequency.hz); + break; + case TRANSCEIVER_MODE_RX_SWEEP: + real_hz = + tuning_set_frequency(max2831_tune_config_rx_sweep, frequency.hz); + break; + case TRANSCEIVER_MODE_TX: + real_hz = tuning_set_frequency(max2831_tune_config_tx, frequency.hz); + break; + default: + return RADIO_ERR_INVALID_CONFIG; + } +#endif + if (real_hz == 0) { + return RADIO_ERR_INVALID_PARAM; + } + + frequency.hz = real_hz; + config->frequency[element] = frequency; + return RADIO_OK; +} + +radio_frequency_t radio_get_frequency( + radio_t* radio, + radio_chan_id chan_id, + radio_frequency_id element) +{ + return radio->channel[chan_id].config.frequency[element]; +} + +radio_error_t radio_set_antenna( + radio_t* radio, + radio_chan_id chan_id, + radio_antenna_id element, + radio_antenna_t value) +{ + if (element > RADIO_ANTENNA_COUNT) { + return RADIO_ERR_INVALID_ELEMENT; + } + + radio_config_t* config = &radio->channel[chan_id].config; + + if (config->mode == TRANSCEIVER_MODE_OFF) { + config->antenna[element] = value; + return RADIO_OK; + } + + switch (element) { + case RADIO_ANTENNA_BIAS_TEE: + rf_path_set_antenna( + &rf_path, + config->antenna[RADIO_ANTENNA_BIAS_TEE].enable); + break; + } + + config->antenna[element] = value; + return RADIO_OK; +} + +radio_antenna_t radio_get_antenna( + radio_t* radio, + radio_chan_id chan_id, + radio_antenna_id element) +{ + return radio->channel[chan_id].config.antenna[element]; +} + +radio_error_t radio_set_clock( + radio_t* radio, + radio_chan_id chan_id, + radio_clock_id element, + radio_clock_t value) +{ + radio_config_t* config = &radio->channel[chan_id].config; + + if (element > RADIO_CLOCK_COUNT) { + return RADIO_ERR_INVALID_ELEMENT; + } + + // CLKIN is not supported as it is automatically detected from hardware state + if (element == RADIO_CLOCK_CLKIN) { + return RADIO_ERR_UNSUPPORTED_OPERATION; + } + + si5351c_clkout_enable(&clock_gen, value.enable); + + config->clock[element] = value; + return RADIO_OK; +} + +radio_clock_t radio_get_clock( + radio_t* radio, + radio_chan_id chan_id, + radio_clock_id element) +{ + if (element == RADIO_CLOCK_CLKIN) { + return (radio_clock_t){ + .enable = si5351c_clkin_signal_valid(&clock_gen), + }; + } + + return radio->channel[chan_id].config.clock[element]; +} + +radio_error_t radio_set_trigger_mode( + radio_t* radio, + radio_chan_id chan_id, + hw_sync_mode_t mode) +{ + radio_config_t* config = &radio->channel[chan_id].config; + + config->trigger_mode = mode; + return RADIO_OK; +} + +hw_sync_mode_t radio_get_trigger_mode(radio_t* radio, radio_chan_id chan_id) +{ + return radio->channel[chan_id].config.trigger_mode; +} + +transceiver_mode_t radio_get_mode(radio_t* radio, radio_chan_id chan_id) +{ + return radio->channel[chan_id].config.mode; +} + +rf_path_direction_t radio_get_direction(radio_t* radio, radio_chan_id chan_id) +{ + radio_config_t* config = &radio->channel[chan_id].config; + + switch (config->mode) { + case TRANSCEIVER_MODE_RX: + case TRANSCEIVER_MODE_RX_SWEEP: + return RF_PATH_DIRECTION_RX; + case TRANSCEIVER_MODE_TX: + return RF_PATH_DIRECTION_TX; + default: + return RF_PATH_DIRECTION_OFF; + } +} + +clock_source_t radio_get_clock_source(radio_t* radio, radio_chan_id chan_id) +{ + return radio->channel[chan_id].clock_source; +} + +radio_error_t radio_switch_mode( + radio_t* radio, + radio_chan_id chan_id, + transceiver_mode_t mode) +{ + radio_error_t result; + radio_channel_t* channel = &radio->channel[chan_id]; + radio_config_t* config = &channel->config; + + // configure firmware direction from mode (but don't configure the hardware yet!) + rf_path_direction_t direction; + switch (mode) { + case TRANSCEIVER_MODE_RX: + case TRANSCEIVER_MODE_RX_SWEEP: + direction = RF_PATH_DIRECTION_RX; + break; + case TRANSCEIVER_MODE_TX: + direction = RF_PATH_DIRECTION_TX; + break; + default: + rf_path_set_direction(&rf_path, RF_PATH_DIRECTION_OFF); + config->mode = mode; + return RADIO_OK; + } + config->mode = mode; + + // sample rate + radio_sample_rate_t sample_rate = + radio_get_sample_rate(radio, channel->id, RADIO_SAMPLE_RATE_CLOCKGEN); + result = radio_set_sample_rate( + radio, + channel->id, + RADIO_SAMPLE_RATE_CLOCKGEN, + sample_rate); + if (result != RADIO_OK) { + return result; + } + + // baseband filter bandwidth + radio_filter_t filter = + radio_get_filter(radio, channel->id, RADIO_FILTER_BASEBAND); + result = radio_set_filter(radio, channel->id, RADIO_FILTER_BASEBAND, filter); + if (result != RADIO_OK) { + return result; + } + + // rf_amp enable + radio_gain_t enable = radio_get_gain(radio, channel->id, RADIO_GAIN_RF_AMP); + result = radio_set_gain(radio, channel->id, RADIO_GAIN_RF_AMP, enable); + if (result != RADIO_OK) { + return result; + } + + // gain + radio_gain_t gain; + if (config->mode == TRANSCEIVER_MODE_RX || + config->mode == TRANSCEIVER_MODE_RX_SWEEP) { + gain = radio_get_gain(radio, channel->id, RADIO_GAIN_RX_LNA); + result = radio_set_gain(radio, channel->id, RADIO_GAIN_RX_LNA, gain); + if (result != RADIO_OK) { + return result; + } + + gain = radio_get_gain(radio, channel->id, RADIO_GAIN_RX_VGA); + result = radio_set_gain(radio, channel->id, RADIO_GAIN_RX_VGA, gain); + if (result != RADIO_OK) { + return result; + } + + } else if (config->mode == TRANSCEIVER_MODE_TX) { + gain = radio_get_gain(radio, channel->id, RADIO_GAIN_TX_VGA); + result = radio_set_gain(radio, channel->id, RADIO_GAIN_TX_VGA, gain); + if (result != RADIO_OK) { + return result; + } + } + + // antenna + radio_antenna_t bias_tee = + radio_get_antenna(radio, channel->id, RADIO_ANTENNA_BIAS_TEE); + result = radio_set_antenna(radio, channel->id, RADIO_ANTENNA_BIAS_TEE, bias_tee); + if (result != RADIO_OK) { + return result; + } + + // tuning frequency + radio_frequency_t frequency = + radio_get_frequency(radio, channel->id, RADIO_FREQUENCY_RF); + result = radio_set_frequency(radio, channel->id, RADIO_FREQUENCY_RF, frequency); + if (result != RADIO_OK) { + return result; + } + + // finally, set the rf path direction + rf_path_set_direction(&rf_path, direction); + + return RADIO_OK; +} diff --git a/firmware/common/radio.h b/firmware/common/radio.h new file mode 100644 index 00000000..caeca0b3 --- /dev/null +++ b/firmware/common/radio.h @@ -0,0 +1,264 @@ +/* + * Copyright 2012-2025 Great Scott Gadgets + * Copyright 2012 Jared Boone + * Copyright 2013 Benjamin Vernoux + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __RF_CONFIG_H__ +#define __RF_CONFIG_H__ + +#include +#include + +#include "rf_path.h" + +typedef enum { + RADIO_OK = 1, + RADIO_ERR_INVALID_PARAM = -2, + RADIO_ERR_INVALID_CONFIG = -3, + RADIO_ERR_INVALID_CHANNEL = -4, + RADIO_ERR_INVALID_ELEMENT = -5, + RADIO_ERR_UNSUPPORTED_OPERATION = -10, + RADIO_ERR_UNIMPLEMENTED = -19, + RADIO_ERR_OTHER = -9999, +} radio_error_t; + +typedef enum { + RADIO_CHANNEL0 = 0x00, +} radio_chan_id; + +typedef enum { + RADIO_FILTER_BASEBAND = 0x00, +} radio_filter_id; + +typedef enum { + RADIO_SAMPLE_RATE_CLOCKGEN = 0x00, +} radio_sample_rate_id; + +typedef enum { + RADIO_FREQUENCY_RF = 0x00, + RADIO_FREQUENCY_IF = 0x01, + RADIO_FREQUENCY_OF = 0x02, +} radio_frequency_id; + +typedef enum { + RADIO_GAIN_RF_AMP = 0x00, + RADIO_GAIN_RX_LNA = 0x01, + RADIO_GAIN_RX_VGA = 0x02, + RADIO_GAIN_TX_VGA = 0x03, +} radio_gain_id; + +typedef enum { + RADIO_ANTENNA_BIAS_TEE = 0x00, +} radio_antenna_id; + +typedef enum { + RADIO_CLOCK_CLKIN = 0x00, + RADIO_CLOCK_CLKOUT = 0x01, +} radio_clock_id; + +typedef struct { + uint32_t num; + uint32_t div; + double hz; +} radio_sample_rate_t; + +typedef struct { + uint64_t hz; +} radio_filter_t; + +typedef struct { + union { + bool enable; + uint8_t db; + }; +} radio_gain_t; + +typedef struct { + uint64_t hz; // desired frequency + uint64_t if_hz; // intermediate frequency + uint64_t lo_hz; // front-end local oscillator frequency + uint8_t path; // image rejection filter path +} radio_frequency_t; + +typedef struct { + bool enable; +} radio_antenna_t; + +typedef struct { + bool enable; +} radio_clock_t; + +// legacy type, moved from hackrf_core +typedef enum { + HW_SYNC_MODE_OFF = 0, + HW_SYNC_MODE_ON = 1, +} hw_sync_mode_t; + +// legacy type, moved from hackrf_core +typedef enum { + CLOCK_SOURCE_HACKRF = 0, + CLOCK_SOURCE_EXTERNAL = 1, + CLOCK_SOURCE_PORTAPACK = 2, +} clock_source_t; + +// legacy type, moved from usb_api_transceiver +typedef enum { + TRANSCEIVER_MODE_OFF = 0, + TRANSCEIVER_MODE_RX = 1, + TRANSCEIVER_MODE_TX = 2, + TRANSCEIVER_MODE_SS = 3, + TRANSCEIVER_MODE_CPLD_UPDATE = 4, + TRANSCEIVER_MODE_RX_SWEEP = 5, +} transceiver_mode_t; + +#define RADIO_CHANNEL_COUNT 1 +#define RADIO_SAMPLE_RATE_COUNT 1 +#define RADIO_FILTER_COUNT 1 +#define RADIO_FREQUENCY_COUNT 4 +#define RADIO_GAIN_COUNT 4 +#define RADIO_ANTENNA_COUNT 1 +#define RADIO_CLOCK_COUNT 2 +#define RADIO_MODE_COUNT 6 + +typedef struct { + // sample rate elements + radio_sample_rate_t sample_rate[RADIO_SAMPLE_RATE_COUNT]; + + // filter elements + radio_filter_t filter[RADIO_FILTER_COUNT]; + + // gain elements + radio_gain_t gain[RADIO_GAIN_COUNT]; + + // frequency elements + radio_frequency_t frequency[RADIO_FREQUENCY_COUNT]; + + // antenna elements + radio_antenna_t antenna[RADIO_ANTENNA_COUNT]; + + // clock elements + radio_clock_t clock[RADIO_CLOCK_COUNT]; + + // trigger elements + hw_sync_mode_t trigger_mode; + + // currently active transceiver mode + transceiver_mode_t mode; +} radio_config_t; + +typedef struct radio_channel_t { + radio_chan_id id; + radio_config_t config; + radio_config_t mode[RADIO_MODE_COUNT]; + clock_source_t clock_source; +} radio_channel_t; + +typedef struct radio_t { + radio_channel_t channel[RADIO_CHANNEL_COUNT]; +} radio_t; + +/** + * API Notes + * + * - All radio_set_*() functions return a radio_error_t + * - radio_set_*() functions work as follows: + * - if the channel mode is TRANSCEIVER_MODE_OFF only the configuration will + * be updated, the hardware state will remain unaffected. + * - if the channel is something other than TRANSCEIVER_MODE_OFF both the + * configuration and hardware state will be updated. + * - this makes it possible to maintain multiple channel configurations and + * switch between them with a single call to radio_switch_mode() + */ + +radio_error_t radio_set_sample_rate( + radio_t* radio, + radio_chan_id chan_id, + radio_sample_rate_id element, + radio_sample_rate_t sample_rate); +radio_sample_rate_t radio_get_sample_rate( + radio_t* radio, + radio_chan_id chan_id, + radio_sample_rate_id element); + +radio_error_t radio_set_filter( + radio_t* radio, + radio_chan_id chan_id, + radio_filter_id element, + radio_filter_t filter); +radio_filter_t radio_get_filter( + radio_t* radio, + radio_chan_id chan_id, + radio_filter_id element); + +radio_error_t radio_set_frequency( + radio_t* radio, + radio_chan_id chan_id, + radio_frequency_id element, + radio_frequency_t frequency); +radio_frequency_t radio_get_frequency( + radio_t* radio, + radio_chan_id chan_id, + radio_frequency_id element); + +radio_error_t radio_set_gain( + radio_t* radio, + radio_chan_id chan_id, + radio_gain_id element, + radio_gain_t gain); +radio_gain_t radio_get_gain(radio_t* radio, radio_chan_id chan_id, radio_gain_id element); + +radio_error_t radio_set_antenna( + radio_t* radio, + radio_chan_id chan_id, + radio_antenna_id element, + radio_antenna_t value); +radio_antenna_t radio_get_antenna( + radio_t* radio, + radio_chan_id chan_id, + radio_antenna_id element); + +radio_error_t radio_set_clock( + radio_t* radio, + radio_chan_id chan_id, + radio_clock_id element, + radio_clock_t value); +radio_clock_t radio_get_clock( + radio_t* radio, + radio_chan_id chan_id, + radio_clock_id element); + +radio_error_t radio_set_trigger_mode( + radio_t* radio, + radio_chan_id chan_id, + hw_sync_mode_t mode); +hw_sync_mode_t radio_get_trigger_mode(radio_t* radio, radio_chan_id chan_id); + +transceiver_mode_t radio_get_mode(radio_t* radio, radio_chan_id chan_id); +rf_path_direction_t radio_get_direction(radio_t* radio, radio_chan_id chan_id); +clock_source_t radio_get_clock_source(radio_t* radio, radio_chan_id chan_id); + +// apply the current channel configuration and switch to the given transceiver mode +radio_error_t radio_switch_mode( + radio_t* radio, + radio_chan_id chan_id, + transceiver_mode_t mode); + +#endif /*__RF_CONFIG_H__*/ diff --git a/firmware/common/rf_path.c b/firmware/common/rf_path.c index 0a82ea7f..5f41ec0c 100644 --- a/firmware/common/rf_path.c +++ b/firmware/common/rf_path.c @@ -31,12 +31,13 @@ #include "gpio_lpc.h" #include "platform_detect.h" #include "mixer.h" +#include "max2831.h" #include "max283x.h" #include "max5864.h" #include "sgpio.h" #include "user_config.h" -#if (defined JAWBREAKER || defined HACKRF_ONE || defined RAD1O) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined RAD1O || defined PRALINE) /* * RF switches on Jawbreaker are controlled by General Purpose Outputs (GPO) on * the RFFC5072. @@ -48,6 +49,9 @@ * * The rad1o also uses GPIO pins to control the different switches. The amplifiers * are also connected to the LPC. + * + * On Praline, a subset of control signals is managed by GPIO pins on the LPC, while + * the remaining signals are generated through combinatorial logic in hardware. */ #define SWITCHCTRL_NO_TX_AMP_PWR (1 << 0) /* GPO1 turn off TX amp power */ #define SWITCHCTRL_AMP_BYPASS (1 << 1) /* GPO2 bypass amp section */ @@ -91,16 +95,13 @@ #define SWITCHCTRL_ANT_PWR (1 << 6) /* turn on antenna port power */ +#ifdef HACKRF_ONE /* - * Starting with HackRF One r9 this control signal has been moved to the - * microcontroller. + * In HackRF One r9 this control signal has been moved to the microcontroller. */ -#ifdef HACKRF_ONE static struct gpio_t gpio_h1r9_no_ant_pwr = GPIO(2, 4); -#endif -#ifdef HACKRF_ONE static void switchctrl_set_hackrf_one(rf_path_t* const rf_path, uint8_t ctrl) { if (ctrl & SWITCHCTRL_TX) { @@ -192,6 +193,47 @@ static void switchctrl_set_hackrf_one(rf_path_t* const rf_path, uint8_t ctrl) } #endif +#ifdef PRALINE +static void switchctrl_set_praline(rf_path_t* const rf_path, uint8_t ctrl) +{ + if (ctrl & SWITCHCTRL_TX) { + gpio_set(rf_path->gpio_tx_en); + if (ctrl & SWITCHCTRL_NO_TX_AMP_PWR) { + ctrl |= SWITCHCTRL_AMP_BYPASS; + } + } else { + gpio_clear(rf_path->gpio_tx_en); + if (ctrl & SWITCHCTRL_NO_RX_AMP_PWR) { + ctrl |= SWITCHCTRL_AMP_BYPASS; + } + } + + if (ctrl & SWITCHCTRL_MIX_BYPASS) { + gpio_set(rf_path->gpio_mix_en_n); + } else { + gpio_clear(rf_path->gpio_mix_en_n); + } + + if (ctrl & SWITCHCTRL_HP) { + gpio_clear(rf_path->gpio_lpf_en); + } else { + gpio_set(rf_path->gpio_lpf_en); + } + + if (ctrl & SWITCHCTRL_AMP_BYPASS) { + gpio_clear(rf_path->gpio_rf_amp_en); + } else { + gpio_set(rf_path->gpio_rf_amp_en); + } + + if (ctrl & SWITCHCTRL_ANT_PWR) { + gpio_clear(rf_path->gpio_ant_bias_en_n); + } else { + gpio_set(rf_path->gpio_ant_bias_en_n); + } +} +#endif + #ifdef RAD1O static void switchctrl_set_rad1o(rf_path_t* const rf_path, uint8_t ctrl) { @@ -264,6 +306,8 @@ static void switchctrl_set(rf_path_t* const rf_path, const uint8_t gpo) mixer_set_gpo(&mixer, gpo); #elif HACKRF_ONE switchctrl_set_hackrf_one(rf_path, gpo); +#elif PRALINE + switchctrl_set_praline(rf_path, gpo); #elif RAD1O switchctrl_set_rad1o(rf_path, gpo); #else @@ -357,6 +401,36 @@ void rf_path_pin_setup(rf_path_t* const rf_path) gpio_output(rf_path->gpio_low_high_filt_n); gpio_output(rf_path->gpio_tx_amp); gpio_output(rf_path->gpio_rx_lna); +#elif PRALINE + /* Configure RF switch control signals */ + scu_pinmux(SCU_TX_EN, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); + board_rev_t rev = detected_revision(); + if ((rev == BOARD_REV_PRALINE_R1_0) || (rev == BOARD_REV_GSG_PRALINE_R1_0)) { + scu_pinmux(SCU_MIX_EN_N_R1_0, SCU_GPIO_FAST | SCU_CONF_FUNCTION4); + } else { + scu_pinmux(SCU_MIX_EN_N, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); + } + scu_pinmux(SCU_LPF_EN, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); + scu_pinmux(SCU_RF_AMP_EN, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); + + /* Configure antenna port power control signal */ + scu_pinmux(SCU_ANT_BIAS_EN_N, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); + + /* Configure RF power supply (VAA) switch */ + scu_pinmux(SCU_NO_VAA_ENABLE, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); + + /* + * Safe (initial) switch settings turn off both amplifiers and antenna port + * power and enable both amp bypass and mixer bypass. + */ + switchctrl_set(rf_path, SWITCHCTRL_SAFE); + + /* Configure RF switch control signals as outputs */ + gpio_output(rf_path->gpio_ant_bias_en_n); + gpio_output(rf_path->gpio_tx_en); + gpio_output(rf_path->gpio_mix_en_n); + gpio_output(rf_path->gpio_lpf_en); + gpio_output(rf_path->gpio_rf_amp_en); #else (void) rf_path; /* silence unused param warning */ #endif @@ -369,12 +443,17 @@ void rf_path_init(rf_path_t* const rf_path) max5864_shutdown(&max5864); ssp1_set_mode_max283x(); +#ifdef PRALINE + max2831_setup(&max283x); + max2831_start(&max283x); +#else if (detected_platform() == BOARD_ID_HACKRF1_R9) { max283x_setup(&max283x, MAX2839_VARIANT); } else { max283x_setup(&max283x, MAX2837_VARIANT); } max283x_start(&max283x); +#endif // On HackRF One, the mixer is now set up earlier in boot. #ifndef HACKRF_ONE @@ -407,7 +486,11 @@ void rf_path_set_direction(rf_path_t* const rf_path, const rf_path_direction_t d ssp1_set_mode_max5864(); max5864_tx(&max5864); ssp1_set_mode_max283x(); +#ifdef PRALINE + max2831_tx(&max283x); +#else max283x_tx(&max283x); +#endif sgpio_configure(&sgpio_config, SGPIO_DIRECTION_TX); break; @@ -426,10 +509,31 @@ void rf_path_set_direction(rf_path_t* const rf_path, const rf_path_direction_t d ssp1_set_mode_max5864(); max5864_rx(&max5864); ssp1_set_mode_max283x(); +#ifdef PRALINE + max2831_rx(&max283x); +#else max283x_rx(&max283x); +#endif sgpio_configure(&sgpio_config, SGPIO_DIRECTION_RX); break; +#ifdef PRALINE + case RF_PATH_DIRECTION_TX_CALIBRATION: + case RF_PATH_DIRECTION_RX_CALIBRATION: + rf_path->switchctrl &= ~SWITCHCTRL_TX; + mixer_disable(&mixer); + ssp1_set_mode_max5864(); + max5864_xcvr(&max5864); + ssp1_set_mode_max283x(); + if (direction == RF_PATH_DIRECTION_TX_CALIBRATION) { + max2831_tx_calibration(&max283x); + } else { + max2831_rx_calibration(&max283x); + } + sgpio_configure(&sgpio_config, SGPIO_DIRECTION_RX); + break; +#endif + case RF_PATH_DIRECTION_OFF: default: rf_path_set_lna(rf_path, 0); @@ -439,7 +543,11 @@ void rf_path_set_direction(rf_path_t* const rf_path, const rf_path_direction_t d ssp1_set_mode_max5864(); max5864_standby(&max5864); ssp1_set_mode_max283x(); +#ifdef PRALINE + max2831_set_mode(&max283x, MAX2831_MODE_STANDBY); +#else max283x_set_mode(&max283x, MAX283x_MODE_STANDBY); +#endif sgpio_configure(&sgpio_config, SGPIO_DIRECTION_RX); break; } diff --git a/firmware/common/rf_path.h b/firmware/common/rf_path.h index 1f88efa3..0ebc75f2 100644 --- a/firmware/common/rf_path.h +++ b/firmware/common/rf_path.h @@ -32,6 +32,10 @@ typedef enum { RF_PATH_DIRECTION_OFF, RF_PATH_DIRECTION_RX, RF_PATH_DIRECTION_TX, +#ifdef PRALINE + RF_PATH_DIRECTION_TX_CALIBRATION, + RF_PATH_DIRECTION_RX_CALIBRATION, +#endif } rf_path_direction_t; typedef enum { @@ -70,6 +74,13 @@ typedef struct rf_path_t { gpio_t gpio_tx_amp; gpio_t gpio_rx_lna; #endif +#ifdef PRALINE + gpio_t gpio_tx_en; + gpio_t gpio_mix_en_n; + gpio_t gpio_lpf_en; + gpio_t gpio_rf_amp_en; + gpio_t gpio_ant_bias_en_n; +#endif } rf_path_t; void rf_path_pin_setup(rf_path_t* const rf_path); diff --git a/firmware/common/rffc5071.c b/firmware/common/rffc5071.c index ec69eed8..b755db64 100644 --- a/firmware/common/rffc5071.c +++ b/firmware/common/rffc5071.c @@ -35,7 +35,9 @@ #include #include "rffc5071.h" #include "rffc5071_regs.def" // private register def macros +#include "selftest.h" +#include #include "hackrf_core.h" /* Default register values. */ @@ -51,7 +53,7 @@ static const uint16_t rffc5071_regs_default[RFFC5071_NUM_REGS] = { 0xff00, /* 08 */ 0x8220, /* 09 */ 0x0202, /* 0A */ - 0x4800, /* 0B */ + 0x0400, /* 0B */ 0x1a94, /* 0C */ 0xd89d, /* 0D */ 0x8900, /* 0E */ @@ -79,6 +81,11 @@ void rffc5071_init(rffc5071_driver_t* const drv) memcpy(drv->regs, rffc5071_regs_default, sizeof(drv->regs)); drv->regs_dirty = 0x7fffffff; + selftest.mixer_id = rffc5071_reg_read(drv, RFFC5071_READBACK_REG); + if ((selftest.mixer_id >> 3) != 2031) { + selftest.report.pass = false; + } + /* Write default register values to chip. */ rffc5071_regs_commit(drv); } @@ -92,6 +99,12 @@ void rffc5071_setup(rffc5071_driver_t* const drv) gpio_set(drv->gpio_reset); gpio_output(drv->gpio_reset); +#ifdef PRALINE + /* Configure mixer PLL lock detect pin */ + scu_pinmux(SCU_MIXER_LD, SCU_MIXER_LD_PINCFG); + gpio_input(drv->gpio_ld); +#endif + rffc5071_init(drv); /* initial setup */ @@ -109,6 +122,9 @@ void rffc5071_setup(rffc5071_driver_t* const drv) /* GPOs are active at all times */ set_RFFC5071_GATE(drv, 1); + /* Enable GPO Lock output signal */ + set_RFFC5071_LOCK(drv, 1); + rffc5071_regs_commit(drv); } @@ -292,3 +308,113 @@ void rffc5071_set_gpo(rffc5071_driver_t* const drv, uint8_t gpo) rffc5071_regs_commit(drv); } + +#ifdef PRALINE +bool rffc5071_poll_ld(rffc5071_driver_t* const drv, uint8_t* prelock_state) +{ + // The RFFC5072 can be configured to output PLL lock status on + // GPO4. The lock detect signal is produced by a window detector + // on the VCO tuning voltage. It goes high to show PLL lock when + // the VCO tuning voltage is within the specified range, typically + // 0.30V to 1.25V. + // + // During the tuning process the lock signal will often go high, + // only to drop lock briefly before returning to the locked state. + // + // Therefore, to reliably detect lock it is necessary to also + // track the state of the FSM that controls the tuning process. + // + // Before re-tuning begins, and after final lock has been + // established, the FSM can be considered to be in STATE_LOCKED. + // + // The very first state change only occurs around 150us _after_ a + // new frequency has been set and registers updated: + // + // L 123456L (STATE) + // ----___------_--- (LD) + // + // This means we need to track the state(s) that occur before + // STATE_LOCKED to be able to reliably identify lock. + // + // At the time of writing 15 different states have been spotted in + // the wild. + // + // The first six states occur at some point during most tuning + // operations with the others occuring less frequently. + // + // Of the first six, two states have been identified as + // STATE_PRELOCKn which, once entered, indicate that no further + // changes will occur to the locked state. + enum state { + STATE_LOCKED = 0x17, // 0b10111 + STATE_00010 = 0x02, + STATE_00100 = 0x04, + STATE_01011 = 0x0b, + STATE_PRELOCK1 = 0x10, // 0b10000 + STATE_PRELOCK2 = 0x1e, // 0b11110 + STATE_00000 = 0x00, + STATE_00001 = 0x01, // mixer bypassed + STATE_00011 = 0x03, + STATE_00101 = 0x05, + STATE_00110 = 0x06, + STATE_00111 = 0x07, + STATE_01010 = 0x0a, + STATE_10110 = 0x16, + STATE_11110 = 0x1e, // ? + STATE_11111 = 0x1f, + STATE_NONE = 0xff, + }; + + // Select which fields will be made available in the readback + // register - we only need to do this the first time. + if (*prelock_state == STATE_NONE) { + set_RFFC5071_READSEL(drv, 0b0011); + rffc5071_regs_commit(drv); + } + + // read fsm state + uint16_t rb = rffc5071_reg_read(drv, RFFC5071_READBACK_REG); + uint8_t rsm_state = (rb >> 11) & 0b11111; + + // get gpo4 lock detect signal + bool gpo4_ld = gpio_read(drv->gpio_ld); + + // parse state + switch (rsm_state) { + case STATE_LOCKED: // 'normal operation' + if (gpo4_ld && + ((*prelock_state == STATE_PRELOCK1) || + (*prelock_state == STATE_PRELOCK2))) { + return true; + } + break; + case STATE_00010: + case STATE_00100: + case STATE_01011: + break; + case STATE_PRELOCK1: + *prelock_state = rsm_state; + break; + case STATE_PRELOCK2: + *prelock_state = rsm_state; + break; + // other states + case STATE_00000: + case STATE_00001: + case STATE_00011: + case STATE_00101: + case STATE_00110: + case STATE_00111: + case STATE_01010: + case STATE_10110: + case STATE_11111: + case STATE_NONE: + break; + default: + // unknown state + break; + } + + return false; +} +#endif diff --git a/firmware/common/rffc5071.h b/firmware/common/rffc5071.h index 94904245..1586e285 100644 --- a/firmware/common/rffc5071.h +++ b/firmware/common/rffc5071.h @@ -34,6 +34,9 @@ typedef struct { spi_bus_t* const bus; gpio_t gpio_reset; +#ifdef PRALINE + gpio_t gpio_ld; +#endif uint16_t regs[RFFC5071_NUM_REGS]; uint32_t regs_dirty; } rffc5071_driver_t; @@ -67,5 +70,8 @@ extern void rffc5071_enable(rffc5071_driver_t* const drv); extern void rffc5071_disable(rffc5071_driver_t* const drv); extern void rffc5071_set_gpo(rffc5071_driver_t* const drv, uint8_t); +#ifdef PRALINE +extern bool rffc5071_poll_ld(rffc5071_driver_t* const drv, uint8_t* prelock_state); +#endif #endif // __RFFC5071_H diff --git a/firmware/common/rffc5071_regs.def b/firmware/common/rffc5071_regs.def index ae45a677..022010c0 100644 --- a/firmware/common/rffc5071_regs.def +++ b/firmware/common/rffc5071_regs.def @@ -27,6 +27,7 @@ #define RFFC5071_REG_SET_DIRTY(_d, _r) (_d->regs_dirty |= (1UL<<_r)) #define RFFC5071_READBACK_REG 31 +#define RFFC5071_DEV_ID_MASK 0xFFFC /* Generate static inline accessors that operate on the global * regs. Done this way to (1) allow defs to be scraped out and used diff --git a/firmware/common/rffc5071_spi.c b/firmware/common/rffc5071_spi.c index 87871b68..3254ebdb 100644 --- a/firmware/common/rffc5071_spi.c +++ b/firmware/common/rffc5071_spi.c @@ -65,8 +65,8 @@ static void rffc5071_spi_bus_init(spi_bus_t* const bus) { const rffc5071_spi_config_t* const config = bus->config; - scu_pinmux(SCU_MIXER_SCLK, SCU_GPIO_FAST | SCU_CONF_FUNCTION4); - scu_pinmux(SCU_MIXER_SDATA, SCU_GPIO_FAST); + scu_pinmux(SCU_MIXER_SCLK, SCU_MIXER_SCLK_PINCFG); + scu_pinmux(SCU_MIXER_SDATA, SCU_MIXER_SDATA_PINCFG); gpio_output(config->gpio_clock); rffc5071_spi_direction_out(bus); diff --git a/firmware/common/selftest.c b/firmware/common/selftest.c new file mode 100644 index 00000000..e1f6ca7d --- /dev/null +++ b/firmware/common/selftest.c @@ -0,0 +1,23 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "selftest.h" +selftest_t selftest; diff --git a/firmware/common/selftest.h b/firmware/common/selftest.h new file mode 100644 index 00000000..0731801b --- /dev/null +++ b/firmware/common/selftest.h @@ -0,0 +1,55 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SELFTEST_H +#define __SELFTEST_H + +#include +#include + +typedef struct { + uint16_t mixer_id; +#ifdef PRALINE + bool max2831_ld_test_ok; +#else + uint16_t max283x_readback_bad_value; + uint16_t max283x_readback_expected_value; + uint8_t max283x_readback_register_count; + uint8_t max283x_readback_total_registers; +#endif + uint8_t si5351_rev_id; + bool si5351_readback_ok; +#ifndef RAD1O + bool rtc_osc_ok; +#endif +#ifdef PRALINE + bool sgpio_rx_ok; + bool xcvr_loopback_ok; +#endif + struct { + bool pass; + char msg[511]; + } report; +} selftest_t; + +extern selftest_t selftest; + +#endif // __SELFTEST_H diff --git a/firmware/common/sgpio.c b/firmware/common/sgpio.c index ccf7e162..73f17e0c 100644 --- a/firmware/common/sgpio.c +++ b/firmware/common/sgpio.c @@ -34,21 +34,21 @@ static void update_q_invert(sgpio_config_t* const config); void sgpio_configure_pin_functions(sgpio_config_t* const config) { - scu_pinmux(SCU_PINMUX_SGPIO0, SCU_GPIO_FAST | SCU_CONF_FUNCTION3); - scu_pinmux(SCU_PINMUX_SGPIO1, SCU_GPIO_FAST | SCU_CONF_FUNCTION3); - scu_pinmux(SCU_PINMUX_SGPIO2, SCU_GPIO_FAST | SCU_CONF_FUNCTION2); - scu_pinmux(SCU_PINMUX_SGPIO3, SCU_GPIO_FAST | SCU_CONF_FUNCTION2); - scu_pinmux(SCU_PINMUX_SGPIO4, SCU_GPIO_FAST | SCU_CONF_FUNCTION2); - scu_pinmux(SCU_PINMUX_SGPIO5, SCU_GPIO_FAST | SCU_CONF_FUNCTION2); - scu_pinmux(SCU_PINMUX_SGPIO6, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); - scu_pinmux(SCU_PINMUX_SGPIO7, SCU_GPIO_FAST | SCU_CONF_FUNCTION6); - scu_pinmux(SCU_PINMUX_SGPIO8, SCU_GPIO_FAST | SCU_CONF_FUNCTION6); - scu_pinmux(SCU_PINMUX_SGPIO9, SCU_GPIO_FAST | SCU_CONF_FUNCTION7); - scu_pinmux(SCU_PINMUX_SGPIO10, SCU_GPIO_FAST | SCU_CONF_FUNCTION6); - scu_pinmux(SCU_PINMUX_SGPIO11, SCU_GPIO_FAST | SCU_CONF_FUNCTION6); - scu_pinmux(SCU_PINMUX_SGPIO12, SCU_GPIO_FAST | SCU_CONF_FUNCTION0); /* GPIO0[13] */ - scu_pinmux(SCU_PINMUX_SGPIO14, SCU_GPIO_FAST | SCU_CONF_FUNCTION4); /* GPIO5[13] */ - scu_pinmux(SCU_PINMUX_SGPIO15, SCU_GPIO_FAST | SCU_CONF_FUNCTION4); /* GPIO5[14] */ + scu_pinmux(SCU_PINMUX_SGPIO0, SCU_PINMUX_SGPIO0_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO1, SCU_PINMUX_SGPIO1_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO2, SCU_PINMUX_SGPIO2_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO3, SCU_PINMUX_SGPIO3_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO4, SCU_PINMUX_SGPIO4_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO5, SCU_PINMUX_SGPIO5_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO6, SCU_PINMUX_SGPIO6_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO7, SCU_PINMUX_SGPIO7_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO8, SCU_PINMUX_SGPIO8_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO9, SCU_PINMUX_SGPIO9_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO10, SCU_PINMUX_SGPIO10_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO11, SCU_PINMUX_SGPIO11_PINCFG); + scu_pinmux(SCU_PINMUX_SGPIO12, SCU_PINMUX_SGPIO12_PINCFG); /* GPIO0[13] */ + scu_pinmux(SCU_PINMUX_SGPIO14, SCU_PINMUX_SGPIO14_PINCFG); /* GPIO5[13] */ + scu_pinmux(SCU_PINMUX_SGPIO15, SCU_PINMUX_SGPIO15_PINCFG); /* GPIO5[14] */ if (detected_platform() == BOARD_ID_HACKRF1_R9) { scu_pinmux( @@ -64,7 +64,9 @@ void sgpio_configure_pin_functions(sgpio_config_t* const config) hw_sync_enable(0); gpio_output(config->gpio_q_invert); +#ifndef PRALINE gpio_output(config->gpio_hw_sync_enable); +#endif } void sgpio_set_slice_mode(sgpio_config_t* const config, const bool multi_slice) diff --git a/firmware/common/sgpio.h b/firmware/common/sgpio.h index e41982c5..6b2773da 100644 --- a/firmware/common/sgpio.h +++ b/firmware/common/sgpio.h @@ -37,7 +37,9 @@ typedef enum { typedef struct sgpio_config_t { gpio_t gpio_q_invert; +#ifndef PRALINE gpio_t gpio_hw_sync_enable; +#endif bool slice_mode_multislice; } sgpio_config_t; diff --git a/firmware/common/si5351c.c b/firmware/common/si5351c.c index af858875..d21506d4 100644 --- a/firmware/common/si5351c.c +++ b/firmware/common/si5351c.c @@ -25,6 +25,7 @@ #include "platform_detect.h" #include "gpio_lpc.h" #include "hackrf_core.h" +#include "selftest.h" #include /* HackRF One r9 clock control */ @@ -199,7 +200,7 @@ void si5351c_configure_clock_control( pll = SI5351C_CLK_PLL_SRC_A; #endif -#if (defined JAWBREAKER || defined HACKRF_ONE) +#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE) if (source == PLL_SOURCE_CLKIN) { /* PLLB on CLKIN */ pll = SI5351C_CLK_PLL_SRC_B; @@ -264,6 +265,15 @@ void si5351c_configure_clock_control( data[5] = SI5351C_CLK_POWERDOWN; data[6] = SI5351C_CLK_POWERDOWN; } +#ifdef PRALINE + data[1] = SI5351C_CLK_FRAC_MODE | SI5351C_CLK_PLL_SRC(pll) | + SI5351C_CLK_SRC(SI5351C_CLK_SRC_MULTISYNTH_SELF) | + SI5351C_CLK_IDRV(SI5351C_CLK_IDRV_4MA); + data[3] = clkout_ctrl; + data[5] = SI5351C_CLK_INT_MODE | SI5351C_CLK_PLL_SRC(pll) | + SI5351C_CLK_SRC(SI5351C_CLK_SRC_MULTISYNTH_SELF) | + SI5351C_CLK_IDRV(SI5351C_CLK_IDRV_4MA) | SI5351C_CLK_INV; +#endif si5351c_write(drv, data, sizeof(data)); } @@ -274,11 +284,19 @@ void si5351c_configure_clock_control( void si5351c_enable_clock_outputs(si5351c_driver_t* const drv) { /* Enable CLK outputs 0, 1, 2, 4, 5 only. */ + /* Praline: enable 0, 4, 5 only. */ /* 7: Clock to CPU is deactivated as it is not used and creates noise */ /* 3: External clock output is deactivated by default */ + +#ifndef PRALINE uint8_t value = SI5351C_CLK_ENABLE(0) | SI5351C_CLK_ENABLE(1) | SI5351C_CLK_ENABLE(2) | SI5351C_CLK_ENABLE(4) | SI5351C_CLK_ENABLE(5) | SI5351C_CLK_DISABLE(6) | SI5351C_CLK_DISABLE(7); +#else + uint8_t value = SI5351C_CLK_ENABLE(0) | SI5351C_CLK_ENABLE(1) | + SI5351C_CLK_DISABLE(2) | SI5351C_CLK_ENABLE(4) | SI5351C_CLK_ENABLE(5) | + SI5351C_CLK_DISABLE(6) | SI5351C_CLK_DISABLE(7); +#endif uint8_t clkout = 3; /* HackRF One r9 has only three clock generator outputs. */ @@ -356,8 +374,8 @@ void si5351c_clkout_enable(si5351c_driver_t* const drv, uint8_t enable) { clkout_enabled = (enable > 0); - //FIXME this should be somewhere else uint8_t clkout = 3; + /* HackRF One r9 has only three clock generator outputs. */ if (detected_platform() == BOARD_ID_HACKRF1_R9) { clkout = 2; @@ -371,6 +389,18 @@ void si5351c_clkout_enable(si5351c_driver_t* const drv, uint8_t enable) void si5351c_init(si5351c_driver_t* const drv) { + /* Read revision ID */ + selftest.si5351_rev_id = si5351c_read_single(drv, 0) & SI5351C_REVID; + + /* Read back interrupt status mask register, flip the mask bits and verify. */ + uint8_t int_mask = si5351c_read_single(drv, 2); + int_mask ^= 0xF8; + si5351c_write_single(drv, 2, int_mask); + selftest.si5351_readback_ok = (si5351c_read_single(drv, 2) == int_mask); + if (!selftest.si5351_readback_ok) { + selftest.report.pass = false; + } + if (detected_platform() == BOARD_ID_HACKRF1_R9) { /* CLKIN_EN */ scu_pinmux(SCU_H1R9_CLKIN_EN, SCU_GPIO_FAST | SCU_CONF_FUNCTION4); diff --git a/firmware/common/si5351c.h b/firmware/common/si5351c.h index 493cc73b..4c91bfba 100644 --- a/firmware/common/si5351c.h +++ b/firmware/common/si5351c.h @@ -56,7 +56,8 @@ extern "C" { #define SI5351C_CLK_IDRV_6MA 2 #define SI5351C_CLK_IDRV_8MA 3 -#define SI5351C_LOS (1 << 4) +#define SI5351C_LOS (1 << 4) +#define SI5351C_REVID 0x03 enum pll_sources { PLL_SOURCE_UNINITIALIZED = -1, diff --git a/firmware/common/spi_ssp.c b/firmware/common/spi_ssp.c index 91e5bcc6..4e25aa4a 100644 --- a/firmware/common/spi_ssp.c +++ b/firmware/common/spi_ssp.c @@ -39,7 +39,7 @@ void spi_ssp_start(spi_bus_t* const bus, const void* const _config) SSP_CR1(bus->obj) = 0; SSP_CPSR(bus->obj) = config->clock_prescale_rate; - SSP_CR0(bus->obj) = (config->serial_clock_rate << 8) | SSP_CPOL_0_CPHA_0 | + SSP_CR0(bus->obj) = (config->serial_clock_rate << 8) | config->spi_mode | SSP_FRAME_SPI | config->data_bits; SSP_CR1(bus->obj) = SSP_SLAVE_OUT_ENABLE | SSP_MASTER | SSP_ENABLE | SSP_MODE_NORMAL; diff --git a/firmware/common/spi_ssp.h b/firmware/common/spi_ssp.h index 02286317..769277dd 100644 --- a/firmware/common/spi_ssp.h +++ b/firmware/common/spi_ssp.h @@ -34,6 +34,7 @@ typedef struct ssp_config_t { ssp_datasize_t data_bits; + ssp_cpol_cpha_t spi_mode; uint8_t serial_clock_rate; uint8_t clock_prescale_rate; gpio_t gpio_select; diff --git a/firmware/common/tune_config.h b/firmware/common/tune_config.h new file mode 100644 index 00000000..cbbbe431 --- /dev/null +++ b/firmware/common/tune_config.h @@ -0,0 +1,220 @@ +/* + * Copyright 2024-2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __TUNE_CONFIG_H__ +#define __TUNE_CONFIG_H__ + +typedef struct { + uint16_t rf_range_end_mhz; + uint16_t if_mhz; + bool high_lo; +} tune_config_t; + +#ifndef PRALINE +// TODO maybe one day? +static const tune_config_t max283x_tune_config[] = {}; +#else +// clang-format off +/* tuning table optimized for TX */ +static const tune_config_t max2831_tune_config_tx[] = { + { 2100, 2375, true }, + { 2175, 2325, false }, + { 2320, 2525, false }, + { 2580, 0, false }, + { 3000, 2325, false }, + { 3100, 2375, false }, + { 3200, 2425, false }, + { 3350, 2375, false }, + { 3500, 2475, false }, + { 3550, 2425, false }, + { 3650, 2325, false }, + { 3700, 2375, false }, + { 3850, 2425, false }, + { 3925, 2375, false }, + { 4600, 2325, false }, + { 4700, 2375, false }, + { 4800, 2425, false }, + { 5100, 2375, false }, + { 5850, 2525, false }, + { 6500, 2325, false }, + { 6750, 2375, false }, + { 6850, 2425, false }, + { 6950, 2475, false }, + { 7000, 2525, false }, + { 7251, 2575, false }, + { 0, 0, false }, +}; + +/* tuning table optimized for 20 Msps interleaved RX sweep mode */ +static const tune_config_t max2831_tune_config_rx_sweep[] = { + { 140, 2330, false }, + { 424, 2570, true }, + { 557, 2520, true }, + { 593, 2380, true }, + { 776, 2360, true }, + { 846, 2570, true }, + { 926, 2500, true }, + { 1055, 2380, true }, + { 1175, 2360, true }, + { 1391, 2340, true }, + { 1529, 2570, true }, + { 1671, 2520, true }, + { 1979, 2380, true }, + { 2150, 2330, true }, + { 2160, 2550, false }, + { 2170, 2560, false }, + { 2179, 2570, false }, + { 2184, 2520, false }, + { 2187, 2560, false }, + { 2194, 2530, false }, + { 2203, 2540, false }, + { 2212, 2550, false }, + { 2222, 2560, false }, + { 2231, 2570, false }, + { 2233, 2530, false }, + { 2237, 2520, false }, + { 2241, 2550, false }, + { 2245, 2570, false }, + { 2250, 2560, false }, + { 2252, 2550, false }, + { 2258, 2570, false }, + { 2261, 2560, false }, + { 2266, 2540, false }, + { 2271, 2570, false }, + { 2275, 2550, false }, + { 2280, 2500, false }, + { 2284, 2560, false }, + { 2285, 2530, false }, + { 2289, 2510, false }, + { 2293, 2570, false }, + { 2294, 2540, false }, + { 2298, 2520, false }, + { 2301, 2570, false }, + { 2302, 2550, false }, + { 2307, 2530, false }, + { 2308, 2560, false }, + { 2312, 2560, false }, + { 2316, 2540, false }, + { 2317, 2570, false }, + { 2320, 2570, false }, + { 2580, 0, false }, + { 2585, 2360, false }, + { 2588, 2340, false }, + { 2594, 2350, false }, + { 2606, 2330, false }, + { 2617, 2340, false }, + { 2627, 2350, false }, + { 2638, 2360, false }, + { 2649, 2370, false }, + { 2659, 2380, false }, + { 2664, 2350, false }, + { 2675, 2360, false }, + { 2686, 2370, false }, + { 2697, 2380, false }, + { 2705, 2350, false }, + { 2716, 2360, false }, + { 2728, 2370, false }, + { 2739, 2380, false }, + { 2757, 2330, false }, + { 2779, 2350, false }, + { 2790, 2360, false }, + { 2801, 2370, false }, + { 2812, 2380, false }, + { 2821, 2570, false }, + { 2831, 2520, false }, + { 2852, 2330, false }, + { 2874, 2350, false }, + { 2897, 2370, false }, + { 2913, 2510, false }, + { 2925, 2570, false }, + { 2937, 2530, false }, + { 2948, 2540, false }, + { 2960, 2550, false }, + { 2975, 2330, false }, + { 2988, 2340, false }, + { 3002, 2330, false }, + { 3014, 2360, false }, + { 3027, 2370, false }, + { 3041, 2500, false }, + { 3052, 2510, false }, + { 3064, 2520, false }, + { 3082, 2500, false }, + { 3107, 2520, false }, + { 3132, 2540, false }, + { 3157, 2560, false }, + { 3170, 2570, false }, + { 3192, 2500, false }, + { 3216, 2340, false }, + { 3270, 2330, false }, + { 3319, 2370, false }, + { 3341, 2340, false }, + { 3370, 2330, false }, + { 3400, 2350, false }, + { 3430, 2370, false }, + { 3464, 2520, false }, + { 3491, 2540, false }, + { 3519, 2560, false }, + { 3553, 2510, false }, + { 3595, 2540, false }, + { 3638, 2570, false }, + { 3665, 2540, false }, + { 3685, 2560, false }, + { 3726, 2330, false }, + { 3790, 2370, false }, + { 3910, 2350, false }, + { 4014, 2510, false }, + { 4123, 2380, false }, + { 4191, 2550, false }, + { 4349, 2510, false }, + { 4452, 2570, false }, + { 4579, 2500, false }, + { 4707, 2570, false }, + { 4831, 2560, false }, + { 4851, 2570, false }, + { 4871, 2560, false }, + { 4891, 2570, false }, + { 4911, 2540, false }, + { 4931, 2550, false }, + { 4951, 2560, false }, + { 5044, 2330, false }, + { 5065, 2340, false }, + { 5174, 2330, false }, + { 5285, 2380, false }, + { 5449, 2340, false }, + { 5574, 2510, false }, + { 5717, 2340, false }, + { 5892, 2530, false }, + { 6096, 2350, false }, + { 6254, 2560, false }, + { 6625, 2340, false }, + { 6764, 2540, false }, + { 6930, 2530, false }, + { 7251, 2570, false }, + { 0, 0, false }, +}; + +// TODO these are just copies of max2831_tune_config_rx_sweep for now +#define max2831_tune_config_rx max2831_tune_config_rx_sweep + // clang-format on + +#endif + +#endif /*__TUNE_CONFIG_H__*/ diff --git a/firmware/common/tuning.c b/firmware/common/tuning.c index 08432ad9..7e65af6f 100644 --- a/firmware/common/tuning.c +++ b/firmware/common/tuning.c @@ -25,6 +25,7 @@ #include "hackrf_ui.h" #include "hackrf_core.h" #include "mixer.h" +#include "max2831.h" #include "max2837.h" #include "max2839.h" #include "sgpio.h" @@ -33,21 +34,41 @@ #define FREQ_ONE_MHZ (1000ULL * 1000) -#define MIN_LP_FREQ_MHZ (0) -#define MAX_LP_FREQ_MHZ (2170ULL) +#ifndef PRALINE -#define ABS_MIN_BYPASS_FREQ_MHZ (2000ULL) -#define MIN_BYPASS_FREQ_MHZ (MAX_LP_FREQ_MHZ) -#define MAX_BYPASS_FREQ_MHZ (2740ULL) -#define ABS_MAX_BYPASS_FREQ_MHZ (3000ULL) + #define MIN_LP_FREQ_MHZ (0) + #define MAX_LP_FREQ_MHZ (2170ULL) -#define MIN_HP_FREQ_MHZ (MAX_BYPASS_FREQ_MHZ) -#define MID1_HP_FREQ_MHZ (3600ULL) -#define MID2_HP_FREQ_MHZ (5100ULL) -#define MAX_HP_FREQ_MHZ (7250ULL) + #define ABS_MIN_BYPASS_FREQ_MHZ (2000ULL) + #define MIN_BYPASS_FREQ_MHZ (MAX_LP_FREQ_MHZ) + #define MAX_BYPASS_FREQ_MHZ (2740ULL) + #define ABS_MAX_BYPASS_FREQ_MHZ (3000ULL) -#define MIN_LO_FREQ_HZ (84375000ULL) -#define MAX_LO_FREQ_HZ (5400000000ULL) + #define MIN_HP_FREQ_MHZ (MAX_BYPASS_FREQ_MHZ) + #define MID1_HP_FREQ_MHZ (3600ULL) + #define MID2_HP_FREQ_MHZ (5100ULL) + #define MAX_HP_FREQ_MHZ (7250ULL) + + #define MIN_LO_FREQ_HZ (84375000ULL) + #define MAX_LO_FREQ_HZ (5400000000ULL) + +#else + + #define MIN_LP_FREQ_MHZ (0) + #define MAX_LP_FREQ_MHZ (2320ULL) + + #define ABS_MIN_BYPASS_FREQ_MHZ (2000ULL) + #define MIN_BYPASS_FREQ_MHZ (MAX_LP_FREQ_MHZ) + #define MAX_BYPASS_FREQ_MHZ (2580ULL) + #define ABS_MAX_BYPASS_FREQ_MHZ (3000ULL) + + #define MIN_HP_FREQ_MHZ (MAX_BYPASS_FREQ_MHZ) + #define MAX_HP_FREQ_MHZ (7250ULL) + + #define MIN_LO_FREQ_HZ (84375000ULL) + #define MAX_LO_FREQ_HZ (5400000000ULL) + +#endif static uint32_t max2837_freq_nominal_hz = 2560000000; @@ -58,6 +79,7 @@ uint64_t freq_cache = 100000000; * hz between 0 to 999999 Hz (not checked) * return false on error or true if success. */ +#ifndef PRALINE bool set_freq(const uint64_t freq) { bool success; @@ -72,12 +94,12 @@ bool set_freq(const uint64_t freq) max283x_set_mode(&max283x, MAX283x_MODE_STANDBY); if (freq_mhz < MAX_LP_FREQ_MHZ) { rf_path_set_filter(&rf_path, RF_PATH_FILTER_LOW_PASS); -#ifdef RAD1O + #ifdef RAD1O max2837_freq_nominal_hz = 2300 * FREQ_ONE_MHZ; -#else + #else /* IF is graduated from 2650 MHz to 2340 MHz */ max2837_freq_nominal_hz = (2650 * FREQ_ONE_MHZ) - (freq / 7); -#endif + #endif mixer_freq_hz = max2837_freq_nominal_hz + freq; /* Set Freq and read real freq */ real_mixer_freq_hz = mixer_set_frequency(&mixer, mixer_freq_hz); @@ -117,13 +139,90 @@ bool set_freq(const uint64_t freq) if (success) { freq_cache = freq; hackrf_ui()->set_frequency(freq); -#ifdef HACKRF_ONE + #ifdef HACKRF_ONE operacake_set_range(freq_mhz); -#endif + #endif } return success; } +uint64_t tuning_set_frequency(const tune_config_t* config, const uint64_t frequency_hz) +{ + (void) config; + + bool result = set_freq(frequency_hz); + if (!result) { + return 0; + } + + return frequency_hz; +} +#else +bool set_freq(const uint64_t freq) +{ + return tuning_set_frequency(max2831_tune_config_rx_sweep, freq) != 0; +} + +uint64_t tuning_set_frequency(const tune_config_t* cfg, const uint64_t freq) +{ + bool found; + uint64_t mixer_freq_hz; + uint64_t real_mixer_freq_hz; + + if (freq > (MAX_HP_FREQ_MHZ * FREQ_ONE_MHZ)) { + return false; + } + + const uint16_t freq_mhz = freq / FREQ_ONE_MHZ; + + found = false; + for (; cfg->rf_range_end_mhz != 0; cfg++) { + if (cfg->rf_range_end_mhz > freq_mhz) { + found = true; + break; + } + } + + if (!found) { + return false; + } + + max2831_mode_t prior_max2831_mode = max2831_mode(&max283x); + max2831_set_mode(&max283x, MAX2831_MODE_STANDBY); + + if (cfg->if_mhz == 0) { + rf_path_set_filter(&rf_path, RF_PATH_FILTER_BYPASS); + max2831_set_frequency(&max283x, freq); + sgpio_cpld_set_mixer_invert(&sgpio_config, 0); + } else if (cfg->if_mhz > freq_mhz) { + rf_path_set_filter(&rf_path, RF_PATH_FILTER_LOW_PASS); + if (cfg->high_lo) { + mixer_freq_hz = FREQ_ONE_MHZ * cfg->if_mhz + freq; + real_mixer_freq_hz = mixer_set_frequency(&mixer, mixer_freq_hz); + max2831_set_frequency(&max283x, real_mixer_freq_hz - freq); + sgpio_cpld_set_mixer_invert(&sgpio_config, 1); + } else { + mixer_freq_hz = FREQ_ONE_MHZ * cfg->if_mhz - freq; + real_mixer_freq_hz = mixer_set_frequency(&mixer, mixer_freq_hz); + max2831_set_frequency(&max283x, real_mixer_freq_hz + freq); + sgpio_cpld_set_mixer_invert(&sgpio_config, 0); + } + } else { + rf_path_set_filter(&rf_path, RF_PATH_FILTER_HIGH_PASS); + mixer_freq_hz = freq - FREQ_ONE_MHZ * cfg->if_mhz; + real_mixer_freq_hz = mixer_set_frequency(&mixer, mixer_freq_hz); + max2831_set_frequency(&max283x, freq - real_mixer_freq_hz); + sgpio_cpld_set_mixer_invert(&sgpio_config, 0); + } + + max2831_set_mode(&max283x, prior_max2831_mode); + freq_cache = freq; + hackrf_ui()->set_frequency(freq); + operacake_set_range(freq_mhz); + return true; +} +#endif + bool set_freq_explicit( const uint64_t if_freq_hz, const uint64_t lo_freq_hz, @@ -144,7 +243,11 @@ bool set_freq_explicit( } rf_path_set_filter(&rf_path, path); +#ifdef PRALINE + max2831_set_frequency(&max283x, if_freq_hz); +#else max283x_set_frequency(&max283x, if_freq_hz); +#endif if (lo_freq_hz > if_freq_hz) { sgpio_cpld_set_mixer_invert(&sgpio_config, 1); } else { diff --git a/firmware/common/tuning.h b/firmware/common/tuning.h index d199bb8e..76bbb5b3 100644 --- a/firmware/common/tuning.h +++ b/firmware/common/tuning.h @@ -25,14 +25,17 @@ #define __TUNING_H__ #include "rf_path.h" +#include "tune_config.h" #include #include -bool set_freq(const uint64_t freq); +bool set_freq(const uint64_t freq); // TODO deprecate bool set_freq_explicit( const uint64_t if_freq_hz, const uint64_t lo_freq_hz, const rf_path_filter_t path); +uint64_t tuning_set_frequency(const tune_config_t* config, const uint64_t frequency_hz); + #endif /*__TUNING_H__*/ diff --git a/firmware/common/w25q80bv.c b/firmware/common/w25q80bv.c index b29fa84a..fe09a2e8 100644 --- a/firmware/common/w25q80bv.c +++ b/firmware/common/w25q80bv.c @@ -67,7 +67,8 @@ void w25q80bv_setup(w25q80bv_driver_t* const drv) do { device_id = w25q80bv_get_device_id(drv); } while (device_id != W25Q80BV_DEVICE_ID_RES && - device_id != W25Q16DV_DEVICE_ID_RES); + device_id != W25Q16DV_DEVICE_ID_RES && + device_id != W25Q32JV_DEVICE_ID_RES); } uint8_t w25q80bv_get_status(w25q80bv_driver_t* const drv) @@ -129,7 +130,8 @@ void w25q80bv_chip_erase(w25q80bv_driver_t* const drv) do { device_id = w25q80bv_get_device_id(drv); } while (device_id != W25Q80BV_DEVICE_ID_RES && - device_id != W25Q16DV_DEVICE_ID_RES); + device_id != W25Q16DV_DEVICE_ID_RES && + device_id != W25Q32JV_DEVICE_ID_RES); w25q80bv_wait_while_busy(drv); w25q80bv_write_enable(drv); @@ -182,7 +184,8 @@ void w25q80bv_program( do { device_id = w25q80bv_get_device_id(drv); } while (device_id != W25Q80BV_DEVICE_ID_RES && - device_id != W25Q16DV_DEVICE_ID_RES); + device_id != W25Q16DV_DEVICE_ID_RES && + device_id != W25Q32JV_DEVICE_ID_RES); /* do nothing if we would overflow the flash */ if ((len > drv->num_bytes) || (addr > drv->num_bytes) || diff --git a/firmware/common/w25q80bv.h b/firmware/common/w25q80bv.h index c0d7cf10..55fefaad 100644 --- a/firmware/common/w25q80bv.h +++ b/firmware/common/w25q80bv.h @@ -29,6 +29,7 @@ #define W25Q80BV_DEVICE_ID_RES 0x13 /* Expected device_id for W25Q80BV */ #define W25Q16DV_DEVICE_ID_RES 0x14 /* Expected device_id for W25Q16DV */ +#define W25Q32JV_DEVICE_ID_RES 0x15 /* Expected device_id for W25Q32JV */ #include "spi_bus.h" #include "gpio.h" diff --git a/firmware/fpga/amaranth_future/fixed.py b/firmware/fpga/amaranth_future/fixed.py new file mode 100644 index 00000000..81503f1f --- /dev/null +++ b/firmware/fpga/amaranth_future/fixed.py @@ -0,0 +1,357 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# Copyright (c) 2024 S. Holzapfel +# SPDX-License-Identifier: BSD-3-Clause +# +# from https://github.com/apfaudio/tiliqua/blob/main/gateware/src/amaranth_future/fixed.py +# which is also derived from https://github.com/amaranth-lang/amaranth/pull/1005 +# slightly modified + +from amaranth import hdl +from amaranth.utils import bits_for + +from dsp.round import convergent_round + +__all__ = ["Shape", "SQ", "UQ", "Value", "Const"] + +class Shape(hdl.ShapeCastable): + def __init__(self, i_or_f_width, f_width = None, /, *, signed): + if f_width is None: + self.i_width, self.f_width = 0, i_or_f_width + else: + self.i_width, self.f_width = i_or_f_width, f_width + + self.signed = bool(signed) + + @staticmethod + def cast(shape, f_width=0): + if not isinstance(shape, hdl.Shape): + raise TypeError(f"Object {shape!r} cannot be converted to a fixed.Shape") + + # i_width is what's left after subtracting f_width and sign bit, but can't be negative. + i_width = max(0, shape.width - shape.signed - f_width) + + return Shape(i_width, f_width, signed = shape.signed) + + def as_shape(self): + return hdl.Shape(self.signed + self.i_width + self.f_width, self.signed) + + def __call__(self, target): + return Value(self, target) + + def const(self, value): + if value is None: + value = 0 + return Const(value, self)._target + + def from_bits(self, raw): + c = Const(0, self) + c._value = raw + return c + + def max(self): + c = Const(0, self) + c._value = c._max_value() + return c + + def min(self): + c = Const(0, self) + c._value = c._min_value() + return c + + def __repr__(self): + return f"fixed.Shape({self.i_width}, {self.f_width}, signed={self.signed})" + + +class SQ(Shape): + def __init__(self, *args): + super().__init__(*args, signed = True) + + +class UQ(Shape): + def __init__(self, *args): + super().__init__(*args, signed = False) + + +class Value(hdl.ValueCastable): + def __init__(self, shape, target): + self._shape = shape + self._target = target + + @staticmethod + def cast(value, f_width=0): + return Shape.cast(value.shape(), f_width)(value) + + def round(self, f_width=0): + # If we're increasing precision, extend with more fractional bits. + if f_width > self.f_width: + return Shape(self.i_width, f_width, signed = self.signed)(hdl.Cat(hdl.Const(0, f_width - self.f_width), self.raw())) + + # If we're reducing precision, perform convergent rounding (round to even). + elif f_width < self.f_width: + return Shape(self.i_width, f_width, signed = self.signed)( convergent_round(self.raw(), self.f_width - f_width) ) + + return self + + def truncate(self, f_width=0): + return Shape(self.i_width, f_width, signed = self.signed)(self.raw()[self.f_width - f_width:]) + + @property + def i_width(self): + return self._shape.i_width + + @property + def f_width(self): + return self._shape.f_width + + @property + def signed(self): + return self._shape.signed + + def as_value(self): + return self._target + + def raw(self): + """ + Adding an `s ( )` signedness wrapper in `as_value` when needed + breaks lib.wiring for some reason. In the future, raw() and + `as_value()` should be combined. + """ + if self.signed: + return self._target.as_signed() + return self._target + + def shape(self): + return self._shape + + def eq(self, other): + # Regular values are assigned directly to the underlying value. + if isinstance(other, hdl.Value): + return self.raw().eq(other) + + # int and float are cast to fixed.Const. + elif isinstance(other, int) or isinstance(other, float): + other = Const(other, self.shape()) + + # Other value types are unsupported. + elif not isinstance(other, Value): + raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value") + + # Match precision. + other = other.round(self.f_width) + + return self.raw().eq(other.raw()) + + def __mul__(self, other): + # Regular values are cast to fixed.Value + if isinstance(other, hdl.Value): + other = Value.cast(other) + + # int are cast to fixed.Const + elif isinstance(other, int): + other = Const(other) + + # Other value types are unsupported. + elif not isinstance(other, Value): + raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value") + + return Value.cast(self.raw() * other.raw(), self.f_width + other.f_width) + + def __rmul__(self, other): + return self.__mul__(other) + + def __add__(self, other): + # Regular values are cast to fixed.Value + if isinstance(other, hdl.Value): + other = Value.cast(other) + + # int are cast to fixed.Const + elif isinstance(other, int): + other = Const(other) + + # Other value types are unsupported. + elif not isinstance(other, Value): + raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value") + + f_width = max(self.f_width, other.f_width) + + return Value.cast(self.round(f_width).raw() + other.round(f_width).raw(), f_width) + + def __radd__(self, other): + return self.__add__(other) + + def __sub__(self, other): + # Regular values are cast to fixed.Value + if isinstance(other, hdl.Value): + other = Value.cast(other) + + # int are cast to fixed.Const + elif isinstance(other, int): + other = Const(other) + + # Other value types are unsupported. + elif not isinstance(other, Value): + raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value") + + f_width = max(self.f_width, other.f_width) + + return Value.cast(self.round(f_width).raw() - other.round(f_width).raw(), f_width) + + def __rsub__(self, other): + return -self.__sub__(other) + + def __pos__(self): + return self + + def __neg__(self): + return Value.cast(-self.raw(), self.f_width) + + def __abs__(self): + return Value.cast(abs(self.raw()), self.f_width) + + def __lshift__(self, other): + if isinstance(other, int): + if other < 0: + raise ValueError("Shift amount cannot be negative") + + if other > self.f_width: + return Value.cast(hdl.Cat(hdl.Const(0, other - self.f_width), self.raw())) + else: + return Value.cast(self.raw(), self.f_width - other) + + elif not isinstance(other, hdl.Value): + raise TypeError("Shift amount must be an integer value") + + if other.signed: + raise TypeError("Shift amount must be unsigned") + + return Value.cast(self.raw() << other, self.f_width) + + def __rshift__(self, other): + if isinstance(other, int): + if other < 0: + raise ValueError("Shift amount cannot be negative") + + return Value.cast(self.raw(), self.f_width + other) + + elif not isinstance(other, hdl.Value): + raise TypeError("Shift amount must be an integer value") + + if other.signed: + raise TypeError("Shift amount must be unsigned") + + # Extend f_width by maximal shift amount. + f_width = self.f_width + 2**other.width - 1 + + return Value.cast(self.round(f_width).raw() >> other, f_width) + + def __lt__(self, other): + if isinstance(other, hdl.Value): + other = Value.cast(other) + elif isinstance(other, int): + other = Const(other) + elif not isinstance(other, Value): + raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value") + f_width = max(self.f_width, other.f_width) + return self.round(f_width).raw() < other.round(f_width).raw() + + def __ge__(self, other): + return ~self.__lt__(other) + + def __eq__(self, other): + if isinstance(other, hdl.Value): + other = Value.cast(other) + elif isinstance(other, int): + other = Const(other) + elif not isinstance(other, Value): + raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value") + f_width = max(self.f_width, other.f_width) + return self.round(f_width).raw() == other.round(f_width).raw() + + def __repr__(self): + return f"(fixedpoint {'SQ' if self.signed else 'UQ'}{self.i_width}.{self.f_width} {self._target!r})" + + +class Const(Value): + def __init__(self, value, shape=None): + + if isinstance(value, float) or isinstance(value, int): + num, den = value.as_integer_ratio() + elif isinstance(value, Const): + # FIXME: Memory inits seem to construct a fixed.Const with fixed.Const + self._shape = value._shape + self._value = value._value + return + else: + raise TypeError(f"Object {value!r} cannot be converted to a fixed.Const") + + # Determine smallest possible shape if not already selected. + if shape is None: + f_width = bits_for(den) - 1 + i_width = max(0, bits_for(abs(num)) - f_width) + shape = Shape(i_width, f_width, signed = num < 0) + + # Scale value to given precision. + if 2**shape.f_width > den: + num *= 2**shape.f_width // den + elif 2**shape.f_width < den: + num = round(num / (den // 2**shape.f_width)) + value = num + + self._shape = shape + + if value > self._max_value(): + print("WARN fixed.Const: clamp", value, "to", self._max_value()) + value = self._max_value() + if value < self._min_value(): + print("WARN fixed.Const: clamp", value, "to", self._min_value()) + value = self._min_value() + + self._value = value + + def _max_value(self): + return 2**(self._shape.i_width + + self._shape.f_width) - 1 + + def _min_value(self): + if self._shape.signed: + return -1 * 2**(self._shape.i_width + + self._shape.f_width) + else: + return 0 + + @property + def _target(self): + return hdl.Const(self._value, self._shape.as_shape()) + + def as_integer_ratio(self): + return self._value, 2**self.f_width + + def as_float(self): + if self._value > self._max_value(): + v = self._min_value() + self._value - self._max_value() - 1 + else: + v = self._value + return v / 2**self.f_width + + # TODO: Operators + + def __mul__(self, other): + # Regular values are cast to fixed.Value + if isinstance(other, hdl.Value): + other = Value.cast(other) + + # int are cast to fixed.Const + elif isinstance(other, int): + other = Const(other) + + # Other value types are unsupported. + elif not isinstance(other, Value): + raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value") + + return Value.cast(self.raw() * other.raw(), self.f_width + other.f_width) + + def __rmul__(self, other): + return self.__mul__(other) diff --git a/firmware/fpga/board.py b/firmware/fpga/board.py new file mode 100644 index 00000000..9185f872 --- /dev/null +++ b/firmware/fpga/board.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# +# This file is part of HackRF. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Elaboratable, Signal, Instance, Module, ClockDomain +from amaranth.build import Resource, Pins, Clock, Attrs +from amaranth.vendor import LatticeICE40Platform +from amaranth_boards.resources import SPIResource + +__all__ = ["PralinePlatform", "ClockDomainGenerator"] + + +class PralinePlatform(LatticeICE40Platform): + device = "iCE40UP5K" + package = "SG48" + + default_clk = "SB_HFOSC" # 48 MHz internal oscillator + hfosc_div = 0 # Do not divide + + resources = [ + Resource("fpga_clk", 0, Pins("39", dir="i"), + Attrs(GLOBAL=True, IO_STANDARD="SB_LVCMOS")), + + # ADC/DAC interfaces. + Resource("afe_clk", 0, Pins("35", dir="i"), + Clock(40e6), Attrs(GLOBAL=True, IO_STANDARD="SB_LVCMOS")), + Resource("dd", 0, Pins("38 37 36 34 32 31 28 27 26 25", dir="o"), + Attrs(IO_STANDARD="SB_LVCMOS")), + Resource("da", 0, Pins("46 45 44 43 42 41 40 39", dir="i"), + Attrs(IO_STANDARD="SB_LVCMOS")), + + # SGPIO interface. + Resource("host_clk", 0, Pins("20", dir="o"), + Attrs(IO_STANDARD="SB_LVCMOS")), + Resource("host_data", 0, Pins("21 19 6 13 10 3 4 18", dir="io"), + Attrs(IO_STANDARD="SB_LVCMOS")), + Resource("q_invert", 0, Pins("9", dir="i"), + Attrs(IO_STANDARD="SB_LVCMOS")), + Resource("direction", 0, Pins("12", dir="i"), + Attrs(IO_STANDARD="SB_LVCMOS")), + Resource("disable", 0, Pins("23", dir="i"), + Attrs(IO_STANDARD="SB_LVCMOS")), + Resource("capture_en", 0, Pins("11", dir="o"), + Attrs(IO_STANDARD="SB_LVCMOS")), + Resource("trigger_in", 0, Pins("48", dir="i"), + Attrs(IO_STANDARD="SB_LVCMOS")), + Resource("trigger_out", 0, Pins("2", dir="o"), + Attrs(IO_STANDARD="SB_LVCMOS")), + + # SPI can be used after configuration. + SPIResource("spi", 0, role="peripheral", + cs_n="16", clk="15", copi="17", cipo="14", + attrs=Attrs(IO_STANDARD="SB_LVCMOS"), + ), + ] + + connectors = [] + + +class ClockDomainGenerator(Elaboratable): + + @staticmethod + def lut_delay(m, signal, *, depth): + signal_out = signal + for i in range(depth): + signal_in = signal_out + signal_out = Signal() + m.submodules += Instance("SB_LUT4", + p_LUT_INIT=0xAAAA, # Buffer configuration + i_I0=signal_in, + o_O=signal_out, + ) + return signal_out + + def elaborate(self, platform): + m = Module() + + # Define clock domains. + m.domains.gck1 = cd_gck1 = ClockDomain(name="gck1", reset_less=True) # analog front-end clock. + + # We need to delay `gck1` clock by at least 8ns, not possible with the PLL alone. + # Each LUT introduces a minimum propagation delay of 9ns (best case). + delayed_gck1 = self.lut_delay(m, platform.request("afe_clk").i, depth=2) + m.d.comb += cd_gck1.clk.eq(delayed_gck1) + platform.add_clock_constraint(delayed_gck1, 40e6) + + return m diff --git a/firmware/fpga/build.py b/firmware/fpga/build.py new file mode 100644 index 00000000..e1bd6aa3 --- /dev/null +++ b/firmware/fpga/build.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +import lz4.block +import struct +import os + +from board import PralinePlatform +from top.standard import Top as standard +from top.half_precision import Top as half_precision +from top.ext_precision_rx import Top as ext_precision_rx +from top.ext_precision_tx import Top as ext_precision_tx + + +BLOCK_SIZE = 4096 # 4 KiB blocks +OUTPUT_FILE = "praline_fpga.bin" + +def compress_blockwise(input_stream, output_stream): + # For every block... + while block := f_in.read(BLOCK_SIZE): + + # Compress the block using raw LZ4 block compression. + compressed_block = lz4.block.compress(block, store_size=False, + mode="high_compression", compression=12) + + # Write the compressed block length and its contents to the output file. + f_out.write(struct.pack(" +# SPDX-License-Identifier: BSD-3-Clause + +from math import floor, log2, ceil, comb + +from amaranth import Module, Signal, Const, signed, ResetInserter, DomainRenamer, C +from amaranth.utils import bits_for + +from amaranth.lib import wiring, stream, data +from amaranth.lib.wiring import In, Out, connect + +from dsp.round import convergent_round + + +class CICInterpolator(wiring.Component): + def __init__(self, M, stages, rates, width_in, width_out=None, num_channels=1, always_ready=False, domain="sync"): + self.M = M + self.stages = stages + self.rates = rates + self.width_in = width_in + self.num_channels = num_channels + if width_out is None: + width_out = width_in + self.bit_growths()[-1] + self.width_out = width_out + self._domain = domain + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(signed(width_in), num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(signed(width_out), num_channels), + always_ready=always_ready + )), + "factor": In(range(bits_for(max(rates)))), + }) + + def bit_growths(self): + bit_growths = cic_growth(N=self.stages, M=self.M, R=max(self.rates)) + return bit_growths + + def elaborate(self, platform): + m = Module() + + always_ready = self.output.signature.always_ready + + # Detect interpolation factor changes to provide an internal reset signal. + factor_last = Signal.like(self.factor) + factor_change = Signal() + m.d.sync += factor_last.eq(self.factor) + m.d.sync += factor_change.eq(factor_last != self.factor) + factor_reset = ResetInserter(factor_change) + + # Calculated bit growths only used below for integrator stages. + bit_growths = iter(self.bit_growths()) + + stages = [] + + # When M=1, we can replace the inner CIC stage with an equivalent zero-order hold integrator. + inner_zoh = self.M == 1 + + # Comb stages. + width = self.width_in + for i in range(self.stages - int(inner_zoh)): + next_width = self.width_in + next(bit_growths) + stage = factor_reset(CombStage(self.M, width, width_out=next_width, num_channels=self.num_channels, always_ready=always_ready)) + m.submodules[f"comb{i}"] = stage + stages += [ stage ] + width = next_width + + # Upsampling. + if list(self.rates) != [1]: + if inner_zoh: + _ = next(bit_growths), next(bit_growths) # drop comb and integrator growths + stage = factor_reset(Upsampler(self.num_channels * width, max(self.rate), zero_order_hold=inner_zoh, variable=True, always_ready=always_ready)) + m.submodules["upsampler"] = stage + m.d.sync += stage.factor.eq(1 << self.factor) + stages += [ stage ] + + # Integrator stages. + for i in range(self.stages - int(inner_zoh)): + width_out = self.width_in + next(bit_growths) + stage = SignExtend(width, width_out, num_channels=self.num_channels, always_ready=always_ready) + m.submodules[f"signextend{i}"] = stage + stages += [ stage ] + stage = factor_reset(IntegratorStage(width_out, width_out, num_channels=self.num_channels, always_ready=always_ready)) + m.submodules[f"integrator{i}"] = stage + stages += [ stage ] + width = width_out + + # Variable gain stage. + min_shift = self.width_in + cic_growth(N=self.stages, M=self.M, R=min(self.rates))[-1] - self.width_out # at min rate + shift_per_rate = { int(log2(rate)): min_shift + (self.stages-1)*i for i, rate in enumerate(self.rates) } + stage = factor_reset(ProgrammableShift(width, width_out=self.width_out, num_channels=self.num_channels, shift_map=shift_per_rate, always_ready=always_ready)) + m.submodules["gain"] = stage + if len(self.rates) > 1: + m.d.sync += stage.shift.eq(self.factor) + stages += [ stage ] + width = self.width_out + + # Connect all stages to build the final filter. + # For the upsampling CIC, we can only drop bits at the last stage. + last = wiring.flipped(self.input) + for stage in stages: + connect(m, last, stage.input) + last = stage.output + connect(m, last, wiring.flipped(self.output)) + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class CICDecimator(wiring.Component): + def __init__(self, M, stages, rates, width_in, width_out=None, num_channels=1, always_ready=False, domain="sync"): + self.M = M + self.stages = stages + self.rates = rates + self.width_in = width_in + self.num_channels = num_channels + self._domain = domain + if width_out is None: + width_out = width_in + ceil(stages * log2(max(rates) * M)) + self.width_out = width_out + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(signed(width_in), num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(signed(width_out), num_channels), + always_ready=always_ready + )), + "factor": In(range(bits_for(max(rates)))), + }) + + def truncation_summary(self): + rates = min(self.rates) + return cic_truncation(N=self.stages, R=rates, M=self.M, + Bin=self.width_in, Bout=self.width_out) + + def elaborate(self, platform): + m = Module() + + stages = [] + + always_ready = self.output.signature.always_ready + + full_width = self.width_in + ceil(self.stages * log2(max(self.rates) * self.M)) + stage_widths = ( full_width - n for n in self.truncation_summary() ) + + # Sign extend stage + last_width = self.width_in + stage_width = next(stage_widths) + stage = SignExtend(last_width, stage_width, num_channels=self.num_channels, always_ready=always_ready) + m.submodules["signextend"] = stage + stages += [ stage ] + last_width = stage_width + + # Integrator stages + for i in range(self.stages): + stage_width = next(stage_widths) + stage = IntegratorStage(last_width, stage_width, num_channels=self.num_channels, always_ready=always_ready) + m.submodules[f"integrator{i}"] = stage + stages += [ stage ] + last_width = stage_width + + # Downsampling + if list(self.rates) != [1]: + stage = Downsampler(self.num_channels * last_width, max(self.rates), variable=True, always_ready=always_ready) + m.submodules["downsampler"] = stage + m.d.sync += stage.factor.eq(1 << self.factor) + stages += [ stage ] + + # Comb stages + for i in range(self.stages): + stage_width = next(stage_widths) + stage = CombStage(self.M, last_width, stage_width, num_channels=self.num_channels, always_ready=always_ready) + m.submodules[f"comb{i}"] = stage + stages += [ stage ] + last_width = stage_width + + # Gain stage + + # Ensure filter gain is at least the gain from width conversion. + min_growth = ceil(self.stages * log2(min(self.rates) * self.M)) + if min_growth < self.width_out - self.width_in: + growth = self.width_out - self.width_in - min_growth + stage = WidthConverter(last_width, last_width+growth, num_channels=self.num_channels, always_ready=always_ready) + m.submodules["gain0"] = stage + stages += [ stage ] + last_width = last_width + growth + + if len(self.rates) > 1: + shift_per_rate = { int(log2(rate)): self.stages*i for i, rate in enumerate(self.rates) } + stage = ProgrammableShift(last_width, width_out=self.width_out, num_channels=self.num_channels, shift_map=shift_per_rate, always_ready=always_ready) + m.submodules["gain"] = stage + m.d.sync += stage.shift.eq(self.factor) + stages += [stage] + last_width = self.width_out + + # Connect stages, rounding/truncating where needed + last = wiring.flipped(self.input) + for stage in stages: + connect(m, last, stage.input) + last = stage.output + connect(m, last, wiring.flipped(self.output)) + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class ProgrammableShift(wiring.Component): + def __init__(self, width_in, shift_map, width_out=None, num_channels=1, always_ready=False): + self.num_channels = num_channels + self.width_in = width_in + self.width_out = width_out or width_in + self.shift_map = shift_map + if len(self.shift_map) == 1: + self.shift = C(list(self.shift_map.keys())[0]) + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(signed(self.width_in), num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(signed(self.width_out), num_channels), + always_ready=always_ready + )), + } | ({"shift": In(range(max(shift_map.keys())+1))} if len(shift_map)>1 else {})) + + def elaborate(self, platform): + m = Module() + + # Implement the map itself (should it be done outside?) + max_shift = max(self.shift_map.values()) + + value_scaled = [ Signal(signed(self.width_in + max_shift)) for _ in range(self.num_channels) ] + scaled_valid = Signal() + scaled_ready = Signal() + + with m.If(~scaled_valid | scaled_ready): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += scaled_valid.eq(self.input.valid) + with m.If(self.input.valid): + for c in range(self.num_channels): + with m.Switch(self.shift): + for k, v in self.shift_map.items(): + with m.Case(k): + m.d.sync += value_scaled[c].eq(self.input.payload[c] << (max_shift - v)) + + with m.If(~self.output.valid | self.output.ready): + m.d.comb += scaled_ready.eq(1) + m.d.sync += self.output.valid.eq(scaled_valid) + with m.If(scaled_valid): + for c in range(self.num_channels): + if max_shift > 0: + # Convergent rounding / round to even. + m.d.sync += self.output.payload[c].eq(convergent_round(value_scaled[c], max_shift)) + # Truncation. + #m.d.sync += self.output.payload[c].eq(value_scaled[c][max_shift:]) + else: + m.d.sync += self.output.payload[c].eq(value_scaled[c]) + + return m + + +class SignExtend(wiring.Component): + def __init__(self, width_in, width_out, num_channels=1, always_ready=False): + self.num_channels = num_channels + self.always_ready = always_ready + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(signed(width_in), num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(signed(width_out), num_channels), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + m = Module() + for c in range(self.num_channels): + m.d.comb += self.output.p[c].eq(self.input.p[c]) + m.d.comb += self.output.valid.eq(self.input.valid) + if not self.always_ready: + m.d.comb += self.input.ready.eq(self.output.ready) + return m + + +class WidthConverter(wiring.Component): + def __init__(self, width_in, width_out, num_channels=1, always_ready=False): + self.width_in = width_in + self.width_out = width_out + self.num_channels = num_channels + self.always_ready = always_ready + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(signed(width_in), num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(signed(width_out), num_channels), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + m = Module() + + shift = self.width_out - self.width_in + + for c in range(self.num_channels): + m.d.comb += self.output.p[c].eq(self.input.p[c] << shift) + m.d.comb += self.output.valid.eq(self.input.valid) + if not self.always_ready: + m.d.comb += self.input.ready.eq(self.output.ready) + return m + + +class CombStage(wiring.Component): + def __init__(self, M, width_in, width_out=None, num_channels=1, always_ready=False): + assert M in (1,2) + self.M = M + self.width_in = width_in + self.width_out = width_out or width_in + 1 + self.num_channels = num_channels + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(signed(self.width_in), num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(signed(self.width_out), num_channels), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + m = Module() + + shift = max(self.width_in - self.width_out, 0) + delay = [ Signal.like(self.input.p) for _ in range(self.M) ] + + with m.If(~self.output.valid | self.output.ready): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += self.output.valid.eq(self.input.valid) + with m.If(self.input.valid): + m.d.sync += delay[0].eq(self.input.p) + m.d.sync += [ delay[i].eq(delay[i-1]) for i in range(1, self.M) ] + for c in range(self.num_channels): + diff = self.input.p[c] - delay[-1][c] + m.d.sync += self.output.p[c].eq(diff if shift == 0 else (diff >> shift)) + + return m + + +class IntegratorStage(wiring.Component): + def __init__(self, width_in, width_out, num_channels=1, always_ready=False): + self.width_in = width_in + self.width_out = width_out + self.num_channels = num_channels + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(signed(width_in), num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(signed(width_out), num_channels), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + m = Module() + + shift = max(self.width_in - self.width_out, 0) + + accumulator = Signal.like(self.input.p) + for c in range(self.num_channels): + m.d.comb += self.output.payload[c].eq(accumulator[c] if shift == 0 else (accumulator[c] >> shift)) + + with m.If(~self.output.valid | self.output.ready): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += self.output.valid.eq(self.input.valid) + with m.If(self.input.valid): + for c in range(self.num_channels): + m.d.sync += accumulator[c].eq(accumulator[c] + self.input.payload[c]) + + return m + + +class Upsampler(wiring.Component): + def __init__(self, width, factor, zero_order_hold=False, variable=False, always_ready=False): + self.width = width + self.zoh = zero_order_hold + signature = { + "input": In(stream.Signature(width, always_ready=always_ready)), + "output": Out(stream.Signature(width, always_ready=always_ready)), + } + if variable: + signature.update({"factor": In(range(factor + 1))}) + else: + self.factor = Const(factor) + super().__init__(signature) + + def elaborate(self, platform): + m = Module() + + counter = Signal.like(self.factor) + ready_stb = Signal(init=1) + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(ready_stb) + + with m.If(~self.output.valid | self.output.ready): + with m.If(counter == 0): + m.d.sync += self.output.payload.eq(self.input.payload) + m.d.sync += self.output.valid.eq(self.input.valid) + with m.If(self.input.valid): + m.d.sync += counter.eq(self.factor - 1) + m.d.sync += ready_stb.eq(self.factor < 2) + with m.Else(): + if not self.zoh: + m.d.sync += self.output.payload.eq(0) + m.d.sync += self.output.valid.eq(1) + m.d.sync += counter.eq(counter - 1) + with m.If(counter == 1): + m.d.sync += ready_stb.eq(1) + + return m + + +class Downsampler(wiring.Component): + def __init__(self, width, factor, variable=False, always_ready=False): + signature = { + "input": In(stream.Signature(width, always_ready=always_ready)), + "output": Out(stream.Signature(width, always_ready=always_ready)), + } + if variable: + # TODO: optimize bit + signature.update({"factor": In(range(factor + 1))}) + else: + self.factor = Const(factor) + super().__init__(signature) + + def elaborate(self, platform): + m = Module() + + counter = Signal.like(self.factor) + + with m.If(self.input.ready & self.input.valid): + with m.If(counter == 0): + m.d.sync += counter.eq(self.factor - 1) + with m.Else(): + m.d.sync += counter.eq(counter - 1) + + with m.If(self.output.ready | ~self.output.valid): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += self.output.valid.eq(self.input.valid & (counter == 0)) + with m.If(self.input.valid & (counter == 0)): + m.d.sync += self.output.payload.eq(self.input.payload) + + return m + + +# Refs: +# [1]: Eugene Hogenauer, "An Economical Class of Digital Filters For Decimation and Interpolation," +# IEEE Trans. Acoust. Speech and Signal Proc., Vol. ASSP-29, April 1981, pp. 155-162. +# [2]: Rick Lyons, "Computing CIC filter register pruning using MATLAB" +# https://www.dsprelated.com/showcode/269.php +# [3]: Peter Thorwartl, "Implementation of Filters", Part 3, lecture notes. +# https://www.so-logic.net/documents/trainings/03_so_implementation_of_filters.pdf + + +# CIC downsamplers / decimators +# How much can we prune / truncate every stage output given a desired output width ? +# Calculate how many bits we can discard after each intermediate stage such that the quantization +# error introduced is not greater than the one introduced by truncating/rounding at the filter +# output. + +def F_sq(N, R, M, i): + assert i <= 2*N + 1 + if i == 2*N + 1: return 1 # eq. (16b) from [1] + # h(k) and L (range of k), eq. (9b) from [1] + if i <= N: + # integrator stage + L = N * (R * M - 1) + i - 1 + def h(k): + return sum((-1)**l * comb(N, l) * comb(N-i+k-R*M*l, k-R*M*l) + for l in range(k//(R*M)+1)) + else: + # comb stage + L = 2*N + 1 - i + def h(k): + return (-1)**k * comb(2*N+1-i, k) + # Compute standard deviation error gain from stage i to output + F_i_sq = sum(h(k)**2 for k in range(L+1)) + return F_i_sq + +def cic_truncation(N, R, M, Bin, Bout=None): + full_width = Bin + ceil(N * log2(R * M)) # maximum width at output + Bout = Bout or full_width # allow to specify full width + B_last = full_width - Bout # number of bits discarded at last stage + t = log2(2**(2*B_last)/12) + log2(6 / N) # Last two terms of (21) from [1] + truncation = [] + for stage in range(2*N): + ou = F_sq(N, R, M, stage+1) + B_i = floor(0.5 * (-log2(ou) + t)) # Eq. (21) from [1] + truncation.append(max(0, B_i)) + truncation.append(max(0, B_last)) + truncation[0] = 0 # [2]: fix case where input is truncated prior to any filtering + return truncation + +# CIC upsamplers / interpolators +# How much bit growth there is per intermediate stage? +# In the interpolator case, we cannot discard bits in intermediate stages: small errors in the +# interpolator stages causes the variance of the error to grow without bound resulting in an +# unstable filter. + +def cic_growth(N, R, M): + growths = [] + for i in range(1, 2*N+1): + if i <= N: + G_i = 2**i # comb stage + # special case from [1] when differential delay is 1 + if M == 1 and i == N: + G_i = 2**(N - 1) + else: + G_i = (2**(2*N-i) * (R*M)**(i-N)) / R # integration stage + growths.append(ceil(log2(G_i))) + return growths + + + + + +# +# Tests +# + +import unittest +import numpy as np +from amaranth.sim import Simulator +from collections import namedtuple + +class _TestFilter(unittest.TestCase): + + def _generate_samples(self, count, width, f_width=0): + # Generate `count` random samples. + rng = np.random.default_rng(0) + samples = rng.normal(0, 1, count) + + # Convert to integer. + samples = np.round(samples / max(abs(samples)) * (2**(width-1) - 1)).astype(int) + assert max(samples) < 2**(width-1) and min(samples) >= -2**(width-1) # sanity check + + if f_width > 0: + return samples / (1 << f_width) + return samples + + def _filter(self, dut, samples, count, oob=[], outfile=None): + + async def input_process(ctx): + if hasattr(dut, "enable"): + ctx.set(dut.enable, 1) + for name, value in oob.items(): + ctx.set(getattr(dut, name), value) + await ctx.tick() + await ctx.tick() + + for sample in samples: + ctx.set(dut.input.payload, [sample.item()]) + ctx.set(dut.input.valid, 1) + await ctx.tick().until(dut.input.ready) + ctx.set(dut.input.valid, 0) + + filtered = [] + async def output_process(ctx): + if not dut.output.signature.always_ready: + ctx.set(dut.output.ready, 1) + async for clk, rst, valid, payload in ctx.tick().sample(dut.output.valid, dut.output.payload): + if valid: + filtered.append(payload[0]) + if len(filtered) == count: + break + + sim = Simulator(dut) + sim.add_clock(1/100e6) + sim.add_testbench(input_process) + sim.add_testbench(output_process) + if outfile is not None: + with sim.write_vcd(outfile): + sim.run() + else: + sim.run() + + return filtered + + +class TestCICDecimator(_TestFilter): + + def test_filter(self): + num_samples = 1024 + input_width = 8 + input_samples = self._generate_samples(num_samples, input_width) + + test = namedtuple('CICDecimatorTest', ['M', 'order', 'rates', 'factor_log', 'width_in', 'width_out', 'outfile'], defaults=(None,)*7) + cic_tests = [] + + # for different CIC orders... + for o in [1,2,3,4]: + # test signal with no rate change + cic_tests.append(test(M=1, order=o, rates=(1,), factor_log=0, width_in=8, width_out=8)) + cic_tests.append(test(M=2, order=o, rates=(1,), factor_log=0, width_in=8, width_out=8)) + cic_tests.append(test(M=2, order=o, rates=(1,), factor_log=0, width_in=8, width_out=12)) + + # test decimation by 4 with different M values and minimum decimation factors + cic_tests.append(test(M=1, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + cic_tests.append(test(M=2, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + cic_tests.append(test(M=1, order=o, rates=(2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + cic_tests.append(test(M=2, order=o, rates=(2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + cic_tests.append(test(M=1, order=o, rates=(4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + + # different bit widths + cic_tests.append(test(M=1, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=9)) + cic_tests.append(test(M=1, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=10)) + cic_tests.append(test(M=1, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=0, width_in=8, width_out=12)) + cic_tests.append(test(M=1, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=1, width_in=8, width_out=12)) + cic_tests.append(test(M=1, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=12)) + + # test fixed decimation by 32 + cic_tests.append(test(M=1, order=o, rates=(32,), factor_log=5, width_in=8, width_out=8)) + + + for t in cic_tests: + with self.subTest(t): + factor_log = t.factor_log + factor = 1 << factor_log + cic_order = t.order + M = t.M + + # Build taps by convolving boxcar filter repeatedly. + taps0 = [1 for _ in range(factor*M)] + taps = [1] + for i in range(cic_order): + taps = np.convolve(taps, taps0) + + # Compute the expected result. + cic_gain = (factor*M)**cic_order + width_gain = 2**(t.width_out - t.width_in) + filtered_np = np.convolve(input_samples, taps) + filtered_np = filtered_np[::factor] # decimate + filtered_np = np.round(filtered_np * width_gain // cic_gain) # scale + filtered_np = filtered_np.astype(np .int32).tolist() # convert to python list + + # Simulate DUT + dut = CICDecimator(M, cic_order, t.rates, t.width_in, t.width_out, always_ready=True) + filtered = self._filter(dut, input_samples, len(input_samples)//factor, oob={"factor":factor_log}, outfile=t.outfile) + + # As we have some rounding error, we expect some samples to differ at most by 1 + max_diff = np.max(np.abs(np.array(filtered) - np.array(filtered_np[:len(filtered)]))) + + self.assertLessEqual(max_diff, 1) + #self.assertListEqual(filtered_np[:len(filtered)], filtered) + + +class TestCICInterpolator(_TestFilter): + + def test_filter(self): + num_samples = 1024 + + test = namedtuple('CICInterpolatorTest', ['M', 'order', 'rates', 'factor_log', 'width_in', 'width_out', 'outfile'], defaults=(None,)*7) + cic_tests = [] + + # for different CIC orders... + for o in [1,2,3,4]: + # test signal bypass + cic_tests.append(test(M=1, order=o, rates=(1,), factor_log=0, width_in=8, width_out=8)) + cic_tests.append(test(M=1, order=o, rates=(1,), factor_log=0, width_in=12, width_out=8)) + + # test interpolation by 4 with different M values and minimum interpolation factors + cic_tests.append(test(M=1, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + cic_tests.append(test(M=2, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + cic_tests.append(test(M=1, order=o, rates=(2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + cic_tests.append(test(M=2, order=o, rates=(2, 4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + cic_tests.append(test(M=1, order=o, rates=(4, 8, 16, 32), factor_log=2, width_in=8, width_out=8)) + + # different bit widths + cic_tests.append(test(M=1, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=12, width_out=8)) + cic_tests.append(test(M=2, order=o, rates=(1, 2, 4, 8, 16, 32), factor_log=2, width_in=12, width_out=8)) + cic_tests.append(test(M=1, order=o, rates=(2, 4, 8, 16, 32), factor_log=2, width_in=12, width_out=8)) + + # test fixed interpolation by 32 + cic_tests.append(test(M=1, order=o, rates=(32,), factor_log=5, width_in=8, width_out=8)) + + cic_tests.append(test(M=1, order=o, rates=(32,), factor_log=5, width_in=12, width_out=8)) + + for t in cic_tests: + with self.subTest(t): + + input_samples = self._generate_samples(num_samples, t.width_in) + + factor_log = t.factor_log + factor = 1 << factor_log + cic_order = t.order + M = t.M + + # Build taps by convolving boxcar filter repeatedly. + taps0 = [1 for _ in range(factor*M)] + taps = [1] + for i in range(cic_order): + taps = np.convolve(taps, taps0) + + # Compute the expected result + cic_gain = (factor*M)**cic_order // factor + width_gain = 2**(t.width_out - t.width_in) + filtered_np = np.zeros(factor * num_samples) + filtered_np[::factor] = input_samples + filtered_np = np.convolve(filtered_np, taps) + filtered_np = np.round(filtered_np * width_gain / cic_gain) # scale + filtered_np = filtered_np.astype(np.int32).tolist() # convert to python list + + # Simulate DUT + dut = CICInterpolator(M, cic_order, t.rates, t.width_in, t.width_out, always_ready=False) + filtered = self._filter(dut, input_samples, len(input_samples)//factor, oob={"factor":factor_log}, outfile=t.outfile) + + self.assertListEqual(filtered_np[:len(filtered)], filtered) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/firmware/fpga/dsp/dc_block.py b/firmware/fpga/dsp/dc_block.py new file mode 100644 index 00000000..8e9a2e28 --- /dev/null +++ b/firmware/fpga/dsp/dc_block.py @@ -0,0 +1,141 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Module, Signal, Mux, signed, DomainRenamer, Cat, EnableInserter +from amaranth.lib import wiring, stream, data +from amaranth.lib.wiring import In, Out + + +class DCBlock(wiring.Component): + """ + DC blocking filter with dithering + + Removes DC offset using a leaky integrator: + y[n] = x[n] - avg[n-1] + avg[n] = alpha * y[n] + avg[n-1] + where alpha is the leakage coefficient (2**-ratio). + """ + def __init__(self, width, ratio=12, num_channels=1, always_ready=True, domain="sync"): + self.ratio = ratio + self.width = width + self.num_channels = num_channels + self.domain = domain + + sig = stream.Signature( + data.ArrayLayout(signed(width), num_channels), + always_ready=always_ready + ) + super().__init__({ + "input": In(sig), + "output": Out(sig), + "enable": In(1), + }) + + def elaborate(self, platform): + m = Module() + + # Resync control signaling. + enable = Signal() + m.d.sync += [ + enable .eq(self.enable), + ] + + # Fixed-point configuration. + ratio = self.ratio + input_shape = signed(self.width) + ext_precision = signed(self.width + ratio) + + # Shared PRNG for all channels. + prng_en = Signal() + m.submodules.prng = prng = EnableInserter(prng_en)(Xoroshiro64AOX()) + prng_bits = prng.output + + # Common signaling. + with m.If(~self.output.valid | self.output.ready): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += self.output.valid.eq(self.input.valid) + + with m.If(self.input.ready & self.input.valid): + m.d.comb += prng_en.eq(1) + + # Per-channel processing. + for c in range(self.num_channels): + + # Signal declarations. + sample_in = self.input.p[c] + y = Signal(input_shape) # current output + y_q = Signal(input_shape) # last output + avg = Signal(ext_precision) # current average + avg_q = Signal(ext_precision) # last average + qavg = Signal(input_shape) # quantized avg + qavg_q = Signal(input_shape) # last quantized avg + error = Signal(ratio) + dither = Signal(ratio) # dither pattern + + # Generate unique dither pattern for each channel. + m.d.sync += dither.eq(prng_bits.word_select(c, ratio)) + + def saturating_sub(a, b): + r = a - b + r_sat = Cat((~r[-1]).replicate(self.width-1), r[-1]) + overflow = r[-1] ^ r[-2] # sign bit of the result different from carry (top 2 bits) + return Mux(overflow, r_sat, r) + + with m.If(self.input.valid & self.input.ready): + + m.d.sync += [ + y_q .eq(y), + avg_q .eq(avg), + qavg_q .eq(qavg), + ] + + m.d.comb += [ + y .eq(saturating_sub(sample_in, qavg_q)), + avg .eq(avg_q + y_q), # update avg + + # Truncate with dithering, discard quantization error. + Cat(error, qavg).eq(avg + dither), + ] + + m.d.sync += self.output.p[c].eq(Mux(enable, y, self.input.p[c])) + + if self.domain != "sync": + m = DomainRenamer(self.domain)(m) + + return m + + +class Xoroshiro64AOX(wiring.Component): + """ Variant of xoroshiro64 for faster hardware implementation """ + """ AOX mod from 'A Fast Hardware Pseudorandom Number Generator Based on xoroshiro128' """ + output: Out(32) + + def __init__(self, s0=1, s1=0): + self.s0 = s0 + self.s1 = s1 + super().__init__() + + def elaborate(self, platform): + m = Module() + + s0 = Signal(32, init=self.s0) + s1 = Signal(32, init=self.s1) + + a = 26 + b = 9 + c = 13 + + sx = Signal(32) + sa = Signal(32) + m.d.comb += sx.eq(s0 ^ s1) + m.d.comb += sa.eq(s0 & s1) + + m.d.sync += s0.eq(s0.rotate_left(a) ^ sx ^ (sx << b)) + m.d.sync += s1.eq(sx.rotate_left(c)) + m.d.sync += self.output.eq(sx ^ (sa.rotate_left(1) | sa.rotate_left(2))) + + return m diff --git a/firmware/fpga/dsp/fir.py b/firmware/fpga/dsp/fir.py new file mode 100644 index 00000000..0faeda8b --- /dev/null +++ b/firmware/fpga/dsp/fir.py @@ -0,0 +1,604 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from math import ceil, log2 + +from amaranth import Module, Signal, Mux, DomainRenamer +from amaranth.lib import wiring, stream, data, memory +from amaranth.lib.wiring import In, Out +from amaranth.utils import bits_for + +from amaranth_future import fixed + +from dsp.mcm import ShiftAddMCM + + +class HalfBandDecimator(wiring.Component): + def __init__(self, taps, data_shape, shape_out=None, always_ready=False, domain="sync"): + midtap = taps[len(taps)//2] + assert taps[1::2] == [0]*(len(taps)//4) + [midtap] + [0]*(len(taps)//4) + assert midtap == 0.5 + self.taps = taps + self.data_shape = data_shape + if shape_out is None: + shape_out = data_shape + self.shape_out = shape_out + self.always_ready = always_ready + self._domain = domain + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(data_shape, 2), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(shape_out, 2), + always_ready=always_ready + )), + "enable": In(1), + }) + + @staticmethod + def interleave_with_zeros(seq, factor): + out = [] + for n in seq: + out.append(n) + out.extend([0]*factor) + return out[:-factor] + + def elaborate(self, platform): + m = Module() + + always_ready = self.always_ready + taps = [ 2 * tap for tap in self.taps ] # scale by 0.5 at the output + fir_taps = self.interleave_with_zeros(taps[0::2], 1) + + # Arms + m.submodules.fir = fir = FIRFilter(fir_taps, shape=self.data_shape, always_ready=always_ready, + num_channels=1, add_tap=len(fir_taps)//2+1) + + with m.FSM(): + + with m.State("BYPASS"): + + with m.If(~self.output.valid | self.output.ready): + m.d.sync += self.output.valid.eq(self.input.valid) + with m.If(self.input.valid): + m.d.sync += self.output.payload.eq(self.input.payload) + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + + with m.If(self.enable): + m.next = "DECIMATE" + + with m.State("DECIMATE"): + + # Input switching. + odd = Signal() + input_idx = Signal() + even_valid = Signal() + even_buffer = Signal.like(self.input.p) + q_inputs = Signal.like(self.input.p) + + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq((~odd & ~even_valid) | fir.input.ready) + + # Even samples are buffered and used as a secondary + # carry addition for the FIR filter. + # I and Q channels are muxed in time, demuxed later in the output stage. + with m.If(self.input.valid & self.input.ready): + m.d.sync += odd.eq(~odd) + with m.If(~odd): + with m.If(~even_valid | fir.input.ready): + m.d.sync += even_valid.eq(self.input.valid) + with m.If(self.input.valid): + m.d.sync += even_buffer.eq(self.input.p) + + # Process two I samples and two Q samples in sequence. + with m.If(fir.input.ready & fir.input.valid): + m.d.sync += input_idx.eq(input_idx ^ 1) + + with m.If(input_idx == 0): + m.d.comb += [ + fir.add_input .eq(even_buffer[0]), + fir.input.p .eq(self.input.p[0]), + fir.input.valid .eq(self.input.valid & even_valid), + ] + with m.If(fir.input.ready & fir.input.valid): + m.d.sync += [ + q_inputs[0].eq(even_buffer[1]), + q_inputs[1].eq(self.input.p[1]), + ] + with m.Else(): + m.d.comb += [ + fir.add_input .eq(q_inputs[0]), + fir.input.p .eq(q_inputs[1]), + fir.input.valid .eq(1), + ] + + # Output sum and demux. + output_idx = Signal() + + with m.If(~self.output.valid | self.output.ready): + if not fir.output.signature.always_ready: + m.d.comb += fir.output.ready.eq(1) + m.d.sync += self.output.valid.eq(fir.output.valid & output_idx) + with m.If(fir.output.valid): + m.d.sync += self.output.p[0].eq(self.output.p[1]) + m.d.sync += self.output.p[1].eq(fir.output.p[0] * fixed.Const(0.5)) + m.d.sync += output_idx.eq(output_idx ^ 1) + + # Mode switch logic. + with m.If(~self.enable): + m.d.sync += input_idx.eq(0) + m.d.sync += output_idx.eq(0) + m.d.sync += odd.eq(0) + m.d.sync += even_valid.eq(0) + m.next = "BYPASS" + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class HalfBandInterpolator(wiring.Component): + def __init__(self, taps, data_shape, shape_out=None, always_ready=False, num_channels=1, domain="sync"): + midtap = taps[len(taps)//2] + assert taps[1::2] == [0]*(len(taps)//4) + [midtap] + [0]*(len(taps)//4) + assert midtap == 0.5 + self.taps = taps + self.data_shape = data_shape + if shape_out is None: + shape_out = data_shape + self.shape_out = shape_out + self.always_ready = always_ready + self._domain = domain + self.num_channels = num_channels + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(data_shape, num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(shape_out, num_channels), + always_ready=always_ready + )), + "enable": In(1), + }) + + def elaborate(self, platform): + m = Module() + + always_ready = self.always_ready + + taps = [ 2 * tap for tap in self.taps ] + arm0_taps = taps[0::2] + arm1_taps = taps[1::2] + delay = arm1_taps.index(1) + + # Arms + m.submodules.fir0 = fir0 = FIRFilter(arm0_taps, shape=self.data_shape, shape_out=self.shape_out, always_ready=always_ready, num_channels=self.num_channels) + m.submodules.fir1 = fir1 = Delay(delay, shape=self.data_shape, always_ready=always_ready, num_channels=self.num_channels) + arms = [fir0, fir1] + + with m.FSM(): + + with m.State("BYPASS"): + + with m.If(~self.output.valid | self.output.ready): + m.d.sync += self.output.valid.eq(self.input.valid) + with m.If(self.input.valid): + m.d.sync += self.output.payload.eq(self.input.payload) + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + + with m.If(self.enable): + m.next = "INTERPOLATE" + + with m.State("INTERPOLATE"): + + # Mode switch logic. + with m.If(~self.enable): + m.next = "BYPASS" + + # Input + + for i, arm in enumerate(arms): + m.d.comb += arm.input.payload.eq(self.input.payload) + m.d.comb += arm.input.valid.eq(self.input.valid & arms[i^1].input.ready) + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(arms[0].input.ready & arms[1].input.ready) + + # Output + + # Arm index selection: switch after every delivered sample + arm_index = Signal() + + # Output buffers for each arm. + arm_outputs = [arm.output for arm in arms] + if self.output.signature.always_ready: + buffers = [stream.Signature(arm.payload.shape()).create() for arm in arm_outputs] + for arm, buf in zip(arm_outputs, buffers): + with m.If(~buf.valid | buf.ready): + if not arm.signature.always_ready: + m.d.comb += arm.ready.eq(1) + m.d.sync += buf.valid.eq(arm.valid) + with m.If(arm.valid): + m.d.sync += buf.payload.eq(arm.payload) + arm_outputs = buffers + + with m.If(~self.output.valid | self.output.ready): + with m.Switch(arm_index): + for i, arm in enumerate(arm_outputs): + with m.Case(i): + for c in range(self.num_channels): + m.d.sync += self.output.payload[c].eq(arm.payload[c]) + m.d.sync += self.output.valid.eq(arm.valid) + if not arm.signature.always_ready: + m.d.comb += arm.ready.eq(1) + with m.If(arm.valid): + m.d.sync += arm_index.eq(arm_index ^ 1) + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class FIRFilter(wiring.Component): + + def __init__(self, taps, shape, shape_out=None, always_ready=False, num_channels=1, add_tap=None): + self.taps = list(taps) + self.add_tap = add_tap + self.shape = shape + if shape_out is None: + shape_out = self.compute_output_shape() + self.shape_out = shape_out + self.num_channels = num_channels + self.always_ready = always_ready + + sig = { + "input": In(stream.Signature( + data.ArrayLayout(shape, num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(shape_out, num_channels), + always_ready=always_ready + )) + } + if add_tap is not None: + sig |= {"add_input": In(data.ArrayLayout(shape, num_channels))} + + super().__init__(sig) + + def taps_shape(self): + taps_as_ratios = [tap.as_integer_ratio() for tap in self.taps] + f_width = bits_for(max(tap[1] for tap in taps_as_ratios)) - 1 + i_width = max(0, bits_for(max(abs(tap[0]) for tap in taps_as_ratios)) - f_width) + return fixed.Shape(i_width, f_width, signed=any(tap < 0 for tap in self.taps)) + + def compute_output_shape(self): + taps_shape = self.taps_shape() + signed = self.shape.signed | taps_shape.signed + f_width = self.shape.f_width + taps_shape.f_width + filter_gain = ceil(log2(sum(self.taps) + (1 if self.add_tap is not None else 0))) + i_width = max(0, self.shape.as_shape().width + taps_shape.as_shape().width - signed - f_width + filter_gain) + return fixed.Shape(i_width, f_width, signed=signed) + + def elaborate(self, platform): + m = Module() + + # Implement transposed-form FIR because it needs a smaller number of registers. + + # Helper function to create smaller size registers for fixed point ops. + def fixed_reg(value, *args, **kwargs): + reg = Signal.like(value.raw(), *args, **kwargs) + return fixed.Value(value.shape(), reg) + + # Implement constant multipliers. + terms = [] + for i, tap in enumerate(self.taps): + tap_fixed = fixed.Const(tap) + terms.append(tap_fixed._value) + + m.submodules.mcm = mcm = ShiftAddMCM(self.shape.as_shape().width, terms, num_channels=self.num_channels, always_ready=self.always_ready) + wiring.connect(m, wiring.flipped(self.input), mcm.input) + + # Cast outputs to fixed point values. + muls = [] + for i, tap in enumerate(self.taps): + tap_fixed = fixed.Const(tap) + muls.append([ fixed.Value.cast(mcm.output.p[c][f"{i}"], tap_fixed.f_width + self.shape.f_width) for c in range(self.num_channels) ]) + + # Implement adder line. + with m.If(~self.output.valid | self.output.ready): + if not self.always_ready: + m.d.comb += mcm.output.ready.eq(1) + m.d.sync += self.output.valid.eq(mcm.output.valid) + + # Carry sum + if self.add_tap is not None: + add_input_q = Signal.like(self.add_input) + m.d.sync += add_input_q.eq(self.add_input) + + for c in range(self.num_channels): + + accum = None + for i, tap in enumerate(self.taps[::-1]): + + match (accum, tap): + case (None, 0): continue + case (None, _): value = muls[::-1][i][c] + case (_, 0): value = accum + case (_, _): value = muls[::-1][i][c] + accum + + if self.add_tap is not None: + if i == self.add_tap: + value += add_input_q[c] + + accum = fixed_reg(value, name=f"add_{c}_{i}") + + with m.If(mcm.output.valid & mcm.output.ready): + m.d.sync += accum.eq(value) + + m.d.comb += self.output.payload[c].eq(accum) + + return m + + +class Delay(wiring.Component): + def __init__(self, delay, shape, always_ready=False, num_channels=1): + self.delay = delay + self.shape = shape + self.num_channels = num_channels + + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(shape, num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(shape, num_channels), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + if self.delay < 3: + return self.elaborate_regs() + return self.elaborate_memory() + + def elaborate_regs(self): + m = Module() + + last = self.input.payload + for i in range(self.delay + 1): + reg = Signal.like(last, name=f"reg_{i}") + with m.If(self.input.valid & self.input.ready): + m.d.sync += reg.eq(last) + last = reg + m.d.comb += self.output.payload.eq(last) + + with m.If(self.output.ready | ~self.output.valid): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += self.output.valid.eq(self.input.valid) + + return m + + def elaborate_memory(self): + m = Module() + + m.submodules.mem = mem = memory.Memory( + shape=self.input.payload.shape(), + depth=self.delay, + init=(tuple(0 for _ in range(self.num_channels)) for _ in range(self.delay)) + ) + mem_wr = mem.write_port(domain="sync") + mem_rd = mem.read_port(domain="sync") + + addr = Signal.like(mem_wr.addr) + with m.If(self.input.valid & self.input.ready): + m.d.sync += addr.eq(Mux(addr == self.delay-1, 0, addr + 1)) + + m.d.comb += [ + mem_wr.addr .eq(addr), + mem_rd.addr .eq(addr), + mem_wr.data .eq(self.input.payload), + mem_wr.en .eq(self.input.valid & self.input.ready), + mem_rd.en .eq(self.input.valid & self.input.ready), + self.output.payload .eq(mem_rd.data), + ] + + with m.If(self.output.ready | ~self.output.valid): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += self.output.valid.eq(self.input.valid) + + return m + + +# +# Tests +# + +import unittest +import numpy as np +from amaranth.sim import Simulator + +class _TestFilter(unittest.TestCase): + + rng = np.random.default_rng(0) + + def _generate_samples(self, count, width, f_width=0): + # Generate `count` random samples. + samples = self.rng.normal(0, 1, count) + + # Convert to integer. + samples = np.round(samples / max(abs(samples)) * (2**(width-1) - 1)).astype(int) + assert max(samples) < 2**(width-1) and min(samples) >= -2**(width-1) # sanity check + + if f_width > 0: + return samples / (1 << f_width) + return samples + + def _filter(self, dut, samples, count, num_channels=1, outfile=None, empty_cycles=0): + + async def input_process(ctx): + if hasattr(dut, "enable"): + ctx.set(dut.enable, 1) + await ctx.tick() + ctx.set(dut.input.valid, 1) + for sample in samples: + if num_channels > 1: + ctx.set(dut.input.payload, [s.item() for s in sample]) + else: + ctx.set(dut.input.payload, [sample.item()]) + await ctx.tick().until(dut.input.ready) + if empty_cycles > 0: + ctx.set(dut.input.valid, 0) + await ctx.tick().repeat(empty_cycles) + ctx.set(dut.input.valid, 1) + ctx.set(dut.input.valid, 0) + + filtered = [] + async def output_process(ctx): + if not dut.output.signature.always_ready: + ctx.set(dut.output.ready, 1) + while len(filtered) < count: + payload, = await ctx.tick().sample(dut.output.payload).until(dut.output.valid) + if num_channels > 1: + filtered.append([v.as_float() for v in payload]) + else: + filtered.append(payload[0].as_float()) + if not dut.output.signature.always_ready: + ctx.set(dut.output.ready, 0) + + sim = Simulator(dut) + sim.add_clock(1/100e6) + sim.add_testbench(input_process) + sim.add_testbench(output_process) + if outfile: + with sim.write_vcd(outfile): + sim.run() + else: + sim.run() + + return filtered + + +class TestFIRFilter(_TestFilter): + + def test_filter(self): + taps = [-1, 0, 9, 16, 9, 0, -1] + taps = [ tap / 32 for tap in taps ] + + num_samples = 1024 + input_width = 8 + input_samples = self._generate_samples(num_samples, input_width) + + # Compute the expected result + filtered_np = np.convolve(input_samples, taps).tolist() + + # Simulate DUT + dut = FIRFilter(taps, fixed.SQ(15, 0), always_ready=True) + filtered = self._filter(dut, input_samples, len(input_samples)) + + self.assertListEqual(filtered_np[:len(filtered)], filtered) + + +class TestHalfBandDecimator(_TestFilter): + + def test_filter_no_backpressure(self): + taps = [-1, 0, 9, 16, 9, 0, -1] + taps = [ tap / 32 for tap in taps ] + + num_samples = 1024 + input_width = 8 + samples_i_in = self._generate_samples(num_samples, input_width, f_width=7) + samples_q_in = self._generate_samples(num_samples, input_width, f_width=7) + + # Compute the expected result + filtered_i_np = np.convolve(samples_i_in, taps)[1::2].tolist() + filtered_q_np = np.convolve(samples_q_in, taps)[1::2].tolist() + + # Simulate DUT + dut = HalfBandDecimator(taps, data_shape=fixed.SQ(7), shape_out=fixed.SQ(0,16), always_ready=True) + filtered = self._filter(dut, zip(samples_i_in, samples_q_in), len(samples_i_in) // 2, num_channels=2) + filtered_i = [ x[0] for x in filtered ] + filtered_q = [ x[1] for x in filtered ] + + self.assertListEqual(filtered_i_np[:len(filtered_i)], filtered_i) + self.assertListEqual(filtered_q_np[:len(filtered_q)], filtered_q) + + def test_filter_with_spare_cycles(self): + taps = [-1, 0, 9, 16, 9, 0, -1] + taps = [ tap / 32 for tap in taps ] + + num_samples = 1024 + input_width = 8 + samples_i_in = self._generate_samples(num_samples, input_width, f_width=7) + samples_q_in = self._generate_samples(num_samples, input_width, f_width=7) + + # Compute the expected result + filtered_i_np = np.convolve(samples_i_in, taps)[1::2].tolist() + filtered_q_np = np.convolve(samples_q_in, taps)[1::2].tolist() + + # Simulate DUT + dut = HalfBandDecimator(taps, data_shape=fixed.SQ(7), shape_out=fixed.SQ(0,16), always_ready=True) + filtered = self._filter(dut, zip(samples_i_in, samples_q_in), len(samples_i_in) // 2, num_channels=2, empty_cycles=3) + filtered_i = [ x[0] for x in filtered ] + filtered_q = [ x[1] for x in filtered ] + + self.assertListEqual(filtered_i_np[:len(filtered_i)], filtered_i) + self.assertListEqual(filtered_q_np[:len(filtered_q)], filtered_q) + + def test_filter_with_backpressure(self): + taps = [-1, 0, 9, 16, 9, 0, -1] + taps = [ tap / 32 for tap in taps ] + + num_samples = 1024 + input_width = 8 + samples_i_in = self._generate_samples(num_samples, input_width, f_width=7) + samples_q_in = self._generate_samples(num_samples, input_width, f_width=7) + + # Compute the expected result + filtered_i_np = np.convolve(samples_i_in, taps)[1::2].tolist() + filtered_q_np = np.convolve(samples_q_in, taps)[1::2].tolist() + + # Simulate DUT + dut = HalfBandDecimator(taps, data_shape=fixed.SQ(7), shape_out=fixed.SQ(0,16), always_ready=False) + filtered = self._filter(dut, zip(samples_i_in, samples_q_in), len(samples_i_in) // 2, num_channels=2) + filtered_i = [ x[0] for x in filtered ] + filtered_q = [ x[1] for x in filtered ] + + self.assertListEqual(filtered_i_np[:len(filtered_i)], filtered_i) + self.assertListEqual(filtered_q_np[:len(filtered_q)], filtered_q) + +class TestHalfBandInterpolator(_TestFilter): + + def test_filter(self): + taps = [-1, 0, 9, 16, 9, 0, -1] + taps = [ tap / 32 for tap in taps ] + num_samples = 1024 + input_width = 8 + input_samples = self._generate_samples(num_samples, input_width, f_width=7) + + # Compute the expected result + input_samples_pad = np.zeros(2*len(input_samples)) + input_samples_pad[0::2] = 2*input_samples # pad with zeros, adjust gain + filtered_np = np.convolve(input_samples_pad, taps).tolist() + + # Simulate DUT + dut = HalfBandInterpolator(taps, data_shape=fixed.SQ(0, 7), shape_out=fixed.SQ(0,16), always_ready=False) + filtered = self._filter(dut, input_samples, len(input_samples) * 2) + + self.assertListEqual(filtered_np[:len(filtered)], filtered) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/firmware/fpga/dsp/fir_mac16.py b/firmware/fpga/dsp/fir_mac16.py new file mode 100644 index 00000000..fea4824a --- /dev/null +++ b/firmware/fpga/dsp/fir_mac16.py @@ -0,0 +1,829 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from math import ceil, log2 + +from amaranth import Module, Signal, Mux, DomainRenamer, ClockSignal, signed +from amaranth.lib import wiring, stream, data, memory +from amaranth.lib.wiring import In, Out +from amaranth.utils import bits_for + +from amaranth_future import fixed + +from dsp.sb_mac16 import SB_MAC16 +from dsp.fir import Delay + + +class HalfBandDecimatorMAC16(wiring.Component): + def __init__(self, taps, data_shape, overclock_rate=4, shape_out=None, always_ready=False, domain="sync"): + midtap = taps[len(taps)//2] + assert taps[1::2] == [0]*(len(taps)//4) + [midtap] + [0]*(len(taps)//4) + self.taps = taps + self.data_shape = data_shape + if shape_out is None: + shape_out = data_shape + self.shape_out = shape_out + self.always_ready = always_ready + self.overclock_rate = overclock_rate + self._domain = domain + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(data_shape, 2), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(shape_out, 2), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + m = Module() + + always_ready = self.always_ready + taps = [ 2 * tap for tap in self.taps ] # scale by 0.5 at the output + fir_taps = taps[0::2] + dly_taps = taps[1::2] + delay = dly_taps.index(1) - 1 + + # Arms + m.submodules.fir = fir = FIRFilterMAC16(fir_taps, shape=self.data_shape, overclock_rate=2*self.overclock_rate, always_ready=always_ready, num_channels=2, carry=self.data_shape) + m.submodules.dly = dly = Delay(delay, shape=self.data_shape, always_ready=always_ready, num_channels=2) + + # Input switching. + odd = Signal() + + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(~odd | fir.input.ready) + m.d.comb += dly.output.ready.eq(1) + + m.d.comb += [ + dly.input.p.eq(self.input.p), + dly.input.valid.eq(self.input.valid & ~odd), + ] + + # Even samples are buffered and used as a secondary + # carry addition for the FIR filter. + with m.If(self.input.valid & self.input.ready): + m.d.sync += odd.eq(~odd) + + # + for c in range(2): + m.d.comb += [ + fir.sum_carry[c] .eq(dly.output.p[c]), # TODO: optimize shape? + fir.input.p[c] .eq(self.input.p[c]), + ] + m.d.comb += fir.input.valid .eq(self.input.valid & odd) + + # Output. + + with m.If(~self.output.valid | self.output.ready): + if not fir.output.signature.always_ready: + m.d.comb += fir.output.ready.eq(1) + m.d.sync += self.output.valid.eq(fir.output.valid) + with m.If(fir.output.valid): + m.d.sync += self.output.p[0].eq(fir.output.p[0] * fixed.Const(0.5)) + m.d.sync += self.output.p[1].eq(fir.output.p[1] * fixed.Const(0.5)) + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class HalfBandInterpolatorMAC16(wiring.Component): + def __init__(self, taps, data_shape, shape_out=None, overclock_rate=4, always_ready=False, num_channels=1, domain="sync"): + midtap = taps[len(taps)//2] + assert taps[1::2] == [0]*(len(taps)//4) + [midtap] + [0]*(len(taps)//4) + assert midtap == 0.5 + self.taps = taps + self.data_shape = data_shape + if shape_out is None: + shape_out = data_shape + self.shape_out = shape_out + self.always_ready = always_ready + self._domain = domain + self.num_channels = num_channels + self.overclock_rate = overclock_rate + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(data_shape, num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(shape_out, num_channels), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + m = Module() + + always_ready = self.always_ready + + taps = [ 2 * tap for tap in self.taps ] + arm0_taps = taps[0::2] + + # Arms + m.submodules.fir = fir = FIRFilterMAC16(arm0_taps, shape=self.data_shape, shape_out=self.shape_out, overclock_rate=self.overclock_rate, always_ready=always_ready, num_channels=self.num_channels, delayed_port=True) + + busy = Signal() + with m.If(fir.input.valid & fir.input.ready): + m.d.sync += busy.eq(1) + + # Input + m.d.comb += fir.input.payload.eq(self.input.payload) + m.d.comb += fir.input.valid.eq(self.input.valid & ~busy) + + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(fir.input.ready & ~busy) + + # Output + + # Arm index selection: switch after every delivered sample + arm_index = Signal() + + delayed = Signal.like(fir.input_delayed) + with m.If(fir.output.valid & fir.output.ready): + m.d.sync += delayed.eq(fir.input_delayed) + + + with m.If(~self.output.valid | self.output.ready): + with m.Switch(arm_index): + with m.Case(0): + for c in range(self.num_channels): + m.d.sync += self.output.payload[c].eq(fir.output.payload[c]) + m.d.sync += self.output.valid.eq(fir.output.valid) + if not fir.output.signature.always_ready: + m.d.comb += fir.output.ready.eq(1) + with m.If(fir.output.valid): + m.d.sync += arm_index.eq(1) + with m.Case(1): + for c in range(self.num_channels): + m.d.sync += self.output.payload[c].eq(delayed[c]) + m.d.sync += self.output.valid.eq(1) + m.d.sync += arm_index.eq(0) + m.d.sync += busy.eq(0) + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class FIRFilterMAC16(wiring.Component): + + def __init__(self, taps, shape, shape_out=None, always_ready=False, overclock_rate=8, num_channels=1, carry=None, delayed_port=False): + self.carry = carry + self.taps = list(taps) + self.shape = shape + if shape_out is None: + shape_out = self.compute_output_shape() + self.shape_out = shape_out + self.num_channels = num_channels + self.always_ready = always_ready + self.overclock_rate = overclock_rate + self.delayed_port = delayed_port + + signature = { + "input": In(stream.Signature( + data.ArrayLayout(shape, num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(shape_out, num_channels), + always_ready=always_ready + )), + } + if carry is not None: + signature.update({ + "sum_carry": In(data.ArrayLayout(carry, num_channels)) + }) + if delayed_port: + signature.update({ + "input_delayed": Out(data.ArrayLayout(shape, num_channels)) + }) + super().__init__(signature) + + def taps_shape(self): + taps_as_ratios = [tap.as_integer_ratio() for tap in self.taps] + f_width = bits_for(max(tap[1] for tap in taps_as_ratios)) - 1 + i_width = max(0, bits_for(max(abs(tap[0]) for tap in taps_as_ratios)) - f_width) + return fixed.Shape(i_width, f_width, signed=any(tap < 0 for tap in self.taps)) + + def compute_output_shape(self): + taps_shape = self.taps_shape() + signed = self.shape.signed | taps_shape.signed + f_width = self.shape.f_width + taps_shape.f_width + filter_gain = ceil(log2(sum(self.taps))) + i_width = max(0, self.shape.as_shape().width + taps_shape.as_shape().width - signed - f_width + filter_gain) + if self.carry is not None: + f_width = max(f_width, self.carry.f_width) + i_width = max(i_width, self.carry.i_width) + 1 + shape_out = fixed.Shape(i_width, f_width, signed=signed) + return shape_out + + def elaborate(self, platform): + m = Module() + + # Build filter out of FIRFilterSerialMAC16 blocks. + overclock_factor = self.overclock_rate + + # Symmetric coefficients special case. + symmetric = (self.taps == self.taps[::-1]) + + # Even-symmetric case. (N=2*K) + # Odd-symmetric case. (N=2*K+1) + if symmetric: + taps = self.taps[:ceil(len(self.taps)/2)] + odd_symmetric = ((len(self.taps) % 2) == 1) + else: + taps = self.taps + + dsp_block_count = ceil(len(taps) / overclock_factor) + + + def pipe(signal, length): + name = signal.name if hasattr(signal, "name") else "signal" + pipe = [ signal ] + [ Signal.like(signal, name=f"{name}_q{i}") for i in range(length) ] + for i in range(length): + m.d.sync += pipe[i+1].eq(pipe[i]) + return pipe + + + if self.carry is not None: + sum_carry_q = Signal.like(self.sum_carry) + with m.If(self.input.valid & self.input.ready): + m.d.sync += sum_carry_q.eq(self.sum_carry) + + for c in range(self.num_channels): + + last = self.input + dsp_blocks = [] + + for i in range(dsp_block_count): + taps_slice = taps[i*overclock_factor:(i+1)*overclock_factor] + input_delayed = len(taps_slice) + carry = last.output.p.shape() if i > 0 else self.carry + + if (i == dsp_block_count-1) and symmetric and odd_symmetric: + taps_slice[-1] /= 2 + input_delayed -= 1 + + dsp = FIRFilterSerialMAC16(taps=taps_slice, shape=self.shape, taps_shape=self.taps_shape(), carry=carry, symmetry=symmetric, + input_delayed_cycles=input_delayed, always_ready=self.always_ready) + dsp_blocks.append(dsp) + + if i == 0: + m.d.comb += [ + dsp.input.p .eq(self.input.p[c]), + dsp.input.valid .eq(self.input.valid & self.input.ready), + ] + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(dsp.input.ready) + if self.carry is not None: + m.d.comb += dsp.sum_carry.eq(sum_carry_q[c]) + else: + m.d.comb += [ + dsp.input.p .eq(pipe(last.input_delayed, last.delay())[-1]), + dsp.input.valid .eq(last.output.valid), + dsp.sum_carry .eq(last.output.p), + ] + if not last.output.signature.always_ready: + m.d.comb += last.output.ready.eq(dsp.input.ready) + + last = dsp + + if self.delayed_port: + m.d.comb += self.input_delayed[c].eq(last.input_delayed) + + if symmetric: + + for i in reversed(range(dsp_block_count)): + end_block = (i == dsp_block_count-1) + m.d.comb += [ + dsp_blocks[i].rev_input .eq(dsp_blocks[i+1].rev_delayed if not end_block else dsp_blocks[i].input_delayed), + ] + + m.submodules += dsp_blocks + + m.d.comb += [ + self.output.payload[c] .eq(last.output.p), + self.output.valid .eq(last.output.valid), + ] + if not last.output.signature.always_ready: + m.d.comb += last.output.ready.eq(self.output.ready) + + return m + + +class FIRFilterSerialMAC16(wiring.Component): + + def __init__(self, taps, shape, shape_out=None, taps_shape=None, carry=None, symmetry=False, input_delayed_cycles=None, always_ready=False): + assert shape.as_shape().width <= 16, "DSP slice inputs have a maximum width of 16 bit." + + self.carry = carry + self.taps = list(taps) + self.shape = shape + self.taps_shape = taps_shape or self.taps_shape() + if shape_out is None: + shape_out = self.compute_output_shape() + self.shape_out = shape_out + self.always_ready = always_ready + self.symmetry = symmetry + if input_delayed_cycles is None: + self.input_delayed_cycles = len(self.taps) + else: + self.input_delayed_cycles = input_delayed_cycles + + signature = { + "input": In(stream.Signature(shape, always_ready=always_ready)), + "input_delayed": Out(shape), + "output": Out(stream.Signature(shape_out, always_ready=always_ready)), + } + if carry is not None: + signature.update({ + "sum_carry": In(carry) + }) + else: + self.sum_carry = 0 + if symmetry: + signature.update({ + "rev_input": In(shape), + "rev_delayed": Out(shape), + }) + super().__init__(signature) + + def taps_shape(self): + taps_as_ratios = [tap.as_integer_ratio() for tap in self.taps] + f_width = bits_for(max(tap[1] for tap in taps_as_ratios)) - 1 + i_width = max(0, bits_for(max(abs(tap[0]) for tap in taps_as_ratios)) - f_width) + return fixed.Shape(i_width, f_width, signed=any(tap < 0 for tap in self.taps)) + + def compute_output_shape(self): + taps_shape = self.taps_shape + signed = self.shape.signed | taps_shape.signed + f_width = self.shape.f_width + taps_shape.f_width + filter_gain = ceil(log2(max(1, sum(self.taps)))) + i_width = max(0, self.shape.as_shape().width + taps_shape.as_shape().width - signed - f_width + filter_gain) + if self.carry is not None: + f_width = max(f_width, self.carry.f_width) + i_width = max(i_width, self.carry.i_width) + 1 + shape_out = fixed.Shape(i_width, f_width, signed=signed) + return shape_out + + def delay(self): + return 1 + 1 + 3 + len(self.taps) - 1 + + def elaborate(self, platform): + m = Module() + + depth = len(self.taps) + counter_in = Signal(range(depth)) + counter_mult = Signal(range(depth)) + counter_out = Signal(range(depth)) + dsp_ready = ~self.output.valid | self.output.ready + + window_valid = Signal() + window_ready = dsp_ready + multin_valid = Signal() + + + input_ready = Signal() + # Ready to process a sample either when the DSP slice is ready and the samples window is: + # - Not valid yet. + # - Only valid for 1 more cycle. + m.d.comb += input_ready.eq(~window_valid | ((counter_in == depth-1) & window_ready)) + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(input_ready) + + window = [ Signal.like(self.input.p, name=f"window_{i}") for i in range(max(depth, self.input_delayed_cycles)) ] + + # Sample window. + with m.If(input_ready): + m.d.sync += window_valid.eq(self.input.valid) + with m.If(self.input.valid): + m.d.sync += window[0].eq(self.input.p) + for i in range(1, len(window)): + m.d.sync += window[i].eq(window[i-1]) + + m.d.sync += multin_valid.eq(window_valid) + + dsp_a = Signal.like(self.input.p) + with m.Switch(counter_in): + for i in range(depth): + with m.Case(i): + m.d.sync += dsp_a.eq(window[i]) + + m.d.comb += self.input_delayed.eq(window[self.input_delayed_cycles-1]) + + # Sample counter. + with m.If(window_ready & window_valid): + m.d.sync += counter_in.eq(_incr(counter_in, depth)) + + # Symmetry handling. + if self.symmetry: + + window_rev = [ Signal.like(self.input.p, name=f"window_rev_{i}") for i in range(depth) ] + + with m.If(input_ready & self.input.valid): + m.d.sync += window_rev[0].eq(self.rev_input) + m.d.sync += [ window_rev[i].eq(window_rev[i-1]) for i in range(1, len(window_rev)) ] + + m.d.comb += self.rev_delayed.eq(window_rev[-1]) + + dsp_a_rev = Signal.like(self.input.p) + with m.Switch(counter_in): + for i in range(depth): + with m.Case(i): + m.d.sync += dsp_a_rev.eq(window_rev[depth-1-i]) + + + # Coefficient ROM. + taps_shape = self.taps_shape + assert taps_shape.as_shape().width <= 16, "DSP slice inputs have a maximum width of 16 bit." + coeff_data = memory.MemoryData( + shape=taps_shape, + depth=depth, # +200 to force BRAM + init=(fixed.Const(tap, shape=taps_shape) for tap in self.taps), + ) + m.submodules.coeff_rom = coeff_rom = memory.Memory(data=coeff_data) + coeff_rd = coeff_rom.read_port(domain="sync") + m.d.comb += coeff_rd.addr.eq(counter_in) + + shape_out = self.compute_output_shape() + + if self.carry: + sum_carry_q = Signal.like(self.sum_carry) + with m.If(self.input.ready & self.input.valid): + m.d.sync += sum_carry_q.eq(self.sum_carry) + + m.submodules.dsp = dsp = iCE40Multiplier() + if self.symmetry: + m.d.comb += dsp.a.eq(dsp_a + dsp_a_rev) + else: + m.d.comb += dsp.a.eq(dsp_a) + m.d.comb += [ + dsp.b .eq(coeff_rd.data), + shape_out(dsp.p) .eq(sum_carry_q if self.carry is not None else 0), + dsp.valid_in .eq(multin_valid & window_ready), + dsp.p_load .eq(counter_mult == 0), + self.output.p .eq(shape_out(dsp.o)), + self.output.valid .eq(dsp.valid_out & (counter_out == depth-1)), + ] + + # Multiplier input and output counters. + with m.If(dsp.valid_in): + m.d.sync += counter_mult.eq(_incr(counter_mult, depth)) + with m.If(dsp.valid_out): + m.d.sync += counter_out.eq(_incr(counter_out, depth)) + + return m + + + +class iCE40Multiplier(wiring.Component): + + a: In(signed(16)) + b: In(signed(16)) + valid_in: In(1) + + p: In(signed(32)) + p_load: In(1) + + o: Out(signed(32)) + valid_out: Out(1) + + def elaborate(self, platform): + m = Module() + + def pipe(signal, length): + pipe = [ signal ] + [ Signal.like(signal, name=f"{signal.name}_q{i}") for i in range(length) ] + for i in range(length): + m.d.sync += pipe[i+1].eq(pipe[i]) + return pipe + + p_load_v = Signal() + + dsp_delay = 3 + valid_pipe = pipe(self.valid_in, dsp_delay) + m.d.comb += p_load_v.eq(self.p_load & self.valid_in) + p_pipe = pipe(self.p, dsp_delay-1) + p_load_pipe = pipe(p_load_v, dsp_delay - 1) + m.d.comb += self.valid_out.eq(valid_pipe[dsp_delay]) + + m.submodules.sb_mac16 = mac = SB_MAC16( + C_REG=0, + A_REG=1, + B_REG=1, + D_REG=0, + TOP_8x8_MULT_REG=0, + BOT_8x8_MULT_REG=0, + PIPELINE_16x16_MULT_REG1=0, + PIPELINE_16x16_MULT_REG2=1, + TOPOUTPUT_SELECT=1, + TOPADDSUB_LOWERINPUT=2, + TOPADDSUB_UPPERINPUT=1, + TOPADDSUB_CARRYSELECT=3, + BOTOUTPUT_SELECT=1, + BOTADDSUB_LOWERINPUT=2, + BOTADDSUB_UPPERINPUT=1, + BOTADDSUB_CARRYSELECT=0, + MODE_8x8=0, + A_SIGNED=1, + B_SIGNED=1, + ) + + m.d.comb += [ + # Inputs. + mac.CLK .eq(ClockSignal("sync")), + mac.CE .eq(1), + mac.C .eq(Mux(p_load_pipe[2], p_pipe[2][16:], self.o[16:])), + mac.A .eq(self.a), + mac.B .eq(self.b), + mac.D .eq(Mux(p_load_pipe[2], p_pipe[2][:16], self.o[:16])), + mac.AHOLD .eq(~valid_pipe[0]), # 0: load + mac.BHOLD .eq(~valid_pipe[0]), + mac.CHOLD .eq(0), + mac.DHOLD .eq(0), + mac.OHOLDTOP .eq(~valid_pipe[2]), + mac.OHOLDBOT .eq(~valid_pipe[2]), + mac.ADDSUBTOP .eq(0), + mac.ADDSUBBOT .eq(0), + mac.OLOADTOP .eq(0), + mac.OLOADBOT .eq(0), + + # Outputs. + self.o .eq(mac.O), + ] + + return m + + +def _incr(signal, modulo): + if modulo == 2 ** len(signal): + return signal + 1 + else: + return Mux(signal == modulo - 1, 0, signal + 1) + +# +# Tests +# + +import unittest +import numpy as np +from amaranth.sim import Simulator + +class _TestFilter(unittest.TestCase): + + rng = np.random.default_rng(0) + + def _generate_samples(self, count, width, f_width=0): + # Generate `count` random samples. + samples = self.rng.normal(0, 1, count) + + # Convert to integer. + samples = np.round(samples / max(abs(samples)) * (2**(width-1) - 1)).astype(int) + assert max(samples) < 2**(width-1) and min(samples) >= -2**(width-1) # sanity check + + if f_width > 0: + return samples / (1 << f_width) + return samples + + def _filter(self, dut, samples, count, num_channels=1, outfile=None, empty_cycles=0): + + async def input_process(ctx): + if hasattr(dut, "enable"): + ctx.set(dut.enable, 1) + await ctx.tick() + + for i, sample in enumerate(samples): + if num_channels > 1: + ctx.set(dut.input.payload, [s.item() for s in sample]) + else: + if isinstance(dut.input.payload.shape(), data.ArrayLayout): + ctx.set(dut.input.payload, [sample.item()]) + else: + ctx.set(dut.input.payload, sample.item()) + ctx.set(dut.input.valid, 1) + await ctx.tick().until(dut.input.ready) + ctx.set(dut.input.valid, 0) + if empty_cycles > 0: + await ctx.tick().repeat(empty_cycles) + + filtered = [] + async def output_process(ctx): + if not dut.output.signature.always_ready: + ctx.set(dut.output.ready, 1) + while len(filtered) < count: + payload, = await ctx.tick().sample(dut.output.payload).until(dut.output.valid) + if num_channels > 1: + filtered.append([v.as_float() for v in payload]) + else: + if isinstance(payload.shape(), data.ArrayLayout): + filtered.append(payload[0].as_float()) + else: + filtered.append(payload.as_float()) + if not dut.output.signature.always_ready: + ctx.set(dut.output.ready, 0) + + sim = Simulator(dut) + sim.add_clock(1/100e6) + sim.add_testbench(input_process) + sim.add_testbench(output_process) + if outfile: + with sim.write_vcd(outfile): + sim.run() + else: + sim.run() + + return filtered + + +class TestFIRFilterMAC16(_TestFilter): + + def test_filter_serial(self): + taps = [-1, 0, 9, 16, 9, 0, -1] + taps = [ tap / 32 for tap in taps ] + + num_samples = 1024 + input_width = 8 + input_samples = self._generate_samples(num_samples, input_width) + + # Compute the expected result + filtered_np = np.convolve(input_samples, taps).tolist() + + # Simulate DUT + dut = FIRFilterSerialMAC16(taps, fixed.SQ(15, 0), always_ready=False) + filtered = self._filter(dut, input_samples, len(input_samples)) + + self.assertListEqual(filtered_np[:len(filtered)], filtered) + + def test_filter(self): + taps = [-1, 0, 9, 16, 9, 0, -1] + taps = [ tap / 32 for tap in taps ] + + num_samples = 1024 + input_width = 8 + input_samples = self._generate_samples(num_samples, input_width) + + # Compute the expected result + filtered_np = np.convolve(input_samples, taps).tolist() + + # Simulate DUT + dut = FIRFilterMAC16(taps, fixed.SQ(15, 0), always_ready=False) + filtered = self._filter(dut, input_samples, len(input_samples)) + + self.assertListEqual(filtered_np[:len(filtered)], filtered) + + +class TestHalfBandDecimatorMAC16(_TestFilter): + + def test_filter(self): + + common_dut_options = dict( + data_shape=fixed.SQ(7), + shape_out=fixed.SQ(0,31), + overclock_rate=4, + ) + + taps0 = (np.array([-1, 0, 9, 16, 9, 0, -1]) / 32).tolist() + taps1 = (np.array([-2, 0, 7, 0, -18, 0, 41, 0, -92, 0, 320, 512, 320, 0, -92, 0, 41, 0, -18, 0, 7, 0, -2]) / 1024).tolist() + + + inputs = { + + "test_filter_with_backpressure": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, always_ready=False, taps=taps0), + "sim_opts": dict(empty_cycles=0), + }, + + "test_filter_with_backpressure_and_empty_cycles": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, always_ready=False, taps=taps0), + "sim_opts": dict(empty_cycles=3), + }, + + "test_filter_with_backpressure_taps1": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, always_ready=False, taps=taps1), + "sim_opts": dict(empty_cycles=0), + }, + + "test_filter_no_backpressure_and_empty_cycles_taps1": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, always_ready=True, taps=taps0), + "sim_opts": dict(empty_cycles=3), + }, + + "test_filter_no_backpressure": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, always_ready=True, taps=taps1), + "sim_opts": dict(empty_cycles=3), + }, + } + + for name, scenario in inputs.items(): + + with self.subTest(name): + taps = scenario["dut_options"]["taps"] + num_samples = scenario["num_samples"] + + input_width = 8 + samples_i_in = self._generate_samples(num_samples, input_width, f_width=7) + samples_q_in = self._generate_samples(num_samples, input_width, f_width=7) + + # Compute the expected result + filtered_i_np = np.convolve(samples_i_in, taps)[1::2].tolist() + filtered_q_np = np.convolve(samples_q_in, taps)[1::2].tolist() + + # Simulate DUT + dut = HalfBandDecimatorMAC16(**scenario["dut_options"]) + filtered = self._filter(dut, zip(samples_i_in, samples_q_in), len(samples_i_in) // 2, num_channels=2, **scenario["sim_opts"]) + filtered_i = [ x[0] for x in filtered ] + filtered_q = [ x[1] for x in filtered ] + + self.assertListEqual(filtered_i_np[:len(filtered_i)], filtered_i) + self.assertListEqual(filtered_q_np[:len(filtered_q)], filtered_q) + + +class TestHalfBandInterpolatorMAC16(_TestFilter): + + def test_filter(self): + + common_dut_options = dict( + data_shape=fixed.SQ(7), + shape_out=fixed.SQ(1,16), + overclock_rate=4, + ) + + taps0 = (np.array([-1, 0, 9, 16, 9, 0, -1]) / 32).tolist() + taps1 = (np.array([-2, 0, 7, 0, -18, 0, 41, 0, -92, 0, 320, 512, 320, 0, -92, 0, 41, 0, -18, 0, 7, 0, -2]) / 1024).tolist() + + inputs = { + + "test_filter_with_backpressure": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, always_ready=False, num_channels=2, taps=taps0), + "sim_opts": dict(empty_cycles=0), + }, + + "test_filter_with_backpressure_and_empty_cycles": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, num_channels=2, always_ready=False, taps=taps0), + "sim_opts": dict(empty_cycles=3), + }, + + "test_filter_with_backpressure_taps1": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, num_channels=2, always_ready=False, taps=taps1), + "sim_opts": dict(empty_cycles=0), + }, + + "test_filter_no_backpressure_and_empty_cycles_taps1": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, num_channels=2, always_ready=True, taps=taps0), + "sim_opts": dict(empty_cycles=8), + }, + + "test_filter_no_backpressure": { + "num_samples": 1024, + "dut_options": dict(**common_dut_options, num_channels=2, always_ready=True, taps=taps1), + "sim_opts": dict(empty_cycles=16), + }, + + } + + + for name, scenario in inputs.items(): + with self.subTest(name): + taps = scenario["dut_options"]["taps"] + num_samples = scenario["num_samples"] + + input_width = 8 + samples_i_in = self._generate_samples(num_samples, input_width, f_width=7) + samples_q_in = self._generate_samples(num_samples, input_width, f_width=7) + + # Compute the expected result + input_samples_pad = np.zeros(2*len(samples_i_in)) + input_samples_pad[0::2] = 2*samples_i_in # pad with zeros, adjust gain + filtered_i_np = np.convolve(input_samples_pad, taps).tolist() + input_samples_pad = np.zeros(2*len(samples_q_in)) + input_samples_pad[0::2] = 2*samples_q_in # pad with zeros, adjust gain + filtered_q_np = np.convolve(input_samples_pad, taps).tolist() + + # Simulate DUT + dut = HalfBandInterpolatorMAC16(**scenario["dut_options"]) + filtered = self._filter(dut, zip(samples_i_in, samples_q_in), len(samples_i_in) * 2, num_channels=2, **scenario["sim_opts"]) + filtered_i = [ x[0] for x in filtered ] + filtered_q = [ x[1] for x in filtered ] + + self.assertListEqual(filtered_i_np[:len(filtered_i)], filtered_i) + self.assertListEqual(filtered_q_np[:len(filtered_q)], filtered_q) + +if __name__ == "__main__": + unittest.main() diff --git a/firmware/fpga/dsp/mcm.py b/firmware/fpga/dsp/mcm.py new file mode 100644 index 00000000..ce9bd978 --- /dev/null +++ b/firmware/fpga/dsp/mcm.py @@ -0,0 +1,156 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from collections import defaultdict + +from amaranth import Module, Signal, signed +from amaranth.lib import wiring, stream, data +from amaranth.lib.wiring import In, Out +from amaranth.utils import bits_for + + +class ShiftAddMCM(wiring.Component): + def __init__(self, width, terms, num_channels=1, always_ready=False): + self.terms = terms + self.width = width + self.num_channels = num_channels + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(signed(width), num_channels), + always_ready=always_ready)), + "output": Out(stream.Signature( + data.ArrayLayout( + data.StructLayout({ + f"{i}": signed(width + bits_for(term)) for i, term in enumerate(terms) + }), num_channels), always_ready=always_ready)), + }) + + def elaborate(self, platform): + m = Module() + + # Get unique, odd terms. + terms = self.terms + unique_terms = defaultdict(list) + for i, term in enumerate(terms): + if term == 0: + continue + term_odd, shift = make_odd(term) + unique_terms[term_odd] += [(i, shift)] + + # Negated inputs for CSD. + input_neg = Signal.like(self.input.p) + for c in range(self.num_channels): + m.d.comb += input_neg[c].eq(-self.input.p[c]) + + with m.If(~self.output.valid | self.output.ready): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += self.output.valid.eq(self.input.valid) + + for term, outputs in unique_terms.items(): + + term_csd = to_csd(term) + + for c in range(self.num_channels): + + n = self.input.p[c] + n_neg = input_neg[c] + + result = None + for s, t in enumerate(term_csd): + if t == 0: + continue + n_base = n if t == 1 else n_neg + shifted_n = n_base if s == 0 else (n_base << s) + if result is None: + result = shifted_n + else: + result += shifted_n + + # A single register can feed multiple outputs. + result_q = Signal(signed(self.width+bits_for(term-1)), name=f"mul_{term}_{c}") + with m.If(self.input.ready & self.input.valid): + m.d.sync += result_q.eq(result) + + for out_index, shift in outputs: + m.d.comb += self.output.p[c][f"{out_index}"].eq(result_q if shift == 0 else (result_q << shift)) + + return m + + +def make_odd(n): + """Convert number to odd fundamental by right-shifting. Returns (odd_part, shift_amount)""" + if n == 0: + return 0, 0 + + shift = 0 + while n % 2 == 0: + n = n >> 1 + shift += 1 + + return n, shift + + +def multiply(n, k): + if k == 0: + return 0 + + csd_k = to_csd(k) + + result = None + for i, c in enumerate(csd_k): + if c == 0: + continue + shifted_n = n if i == 0 else (n << i) + if result is None: + if c == 1: + result = shifted_n + elif c == -1: + result = -shifted_n + else: + if c == 1: + result += shifted_n + elif c == -1: + result -= shifted_n + + return result[:bits_for(k-1)+len(n)].as_signed() + + +def to_csd(n): + """ Convert integer to Canonical Signed Digit representation (LSB first). """ + if n == 0: + return [0] + + sign = n < 0 + n = abs(n) + binary = [ int(b) for b in f"{n:b}" ][::-1] + + # Apply CSD conversion algorithm. + binary_padded = binary + [0] + carry = 0 + csd = [] + for i, bit in enumerate(binary_padded): + nextbit = binary_padded[i+1] if i+1 < len(binary_padded) else 0 + d = bit ^ carry + ys = nextbit & d # sign bit + yd = ~nextbit & d # data bit + csd.append(yd - ys) + carry = (bit & nextbit) | ((bit|nextbit)&carry) + if sign: + csd = [-1*c for c in csd] + + # Remove trailing zeros. + while len(csd) > 1 and csd[-1] == 0: + csd.pop() + + # Regular binary representation is preferred if the number + # of additions was not improved. + if sum(binary) <= sum(abs(d) for d in csd) - sign: + if sign: + return [ -d for d in binary ] + return binary + + return csd diff --git a/firmware/fpga/dsp/nco.py b/firmware/fpga/dsp/nco.py new file mode 100755 index 00000000..e464399a --- /dev/null +++ b/firmware/fpga/dsp/nco.py @@ -0,0 +1,103 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from math import pi, sin, cos + +from amaranth import Module, Signal, Mux, Cat +from amaranth.lib import wiring, memory +from amaranth.lib.wiring import In, Out + +from util import IQSample + + +class NCO(wiring.Component): + """ + Retrieve cos(x), sin(x) using a look-up table. + Latency is 2 cycles. + + We only precompute 1/8 of the (cos,sin) cycle, and the top 3 bits of the + phase are used to reconstruct the final values with symmetric properties. + + Parameters + ---------- + phase_width : int + Bit width of the phase accumulator. + output_width : int + Bit width of the output cos/sin words. + + Signals + ------- + phase : Signal(phase_width), in + Input phase. + en : Signal(1), in + Enable strobe. + output : IQSample(output_width), out + Returned result for cos(phase), sin(phase). + """ + + def __init__(self, phase_width=24, output_width=10): + self.phase_width = phase_width + self.output_width = output_width + super().__init__({ + "phase": In(phase_width), + "en": In(1), + "output": Out(IQSample(output_width)), + }) + + def elaborate(self, platform): + m = Module() + + # Create internal table with precomputed entries. + addr_width = (self.output_width + 1) - 3 + lut_depth = 1 << addr_width + lut_scale = (1 << (self.output_width-1)) - 1 + lut_phases = [ i * pi / 4 / lut_depth for i in range(lut_depth) ] + lut_data = memory.MemoryData( + shape=IQSample(self.output_width), + depth=lut_depth, + init=({"i": round(lut_scale * cos(x)), "q": round(lut_scale * sin(x))} for x in lut_phases) + ) + m.submodules.table = table = memory.Memory(data=lut_data) + table_rd = table.read_port(domain="sync") + + # 3 MSBs of the phase word: sign, quadrant, octant. + o, q, s = self.phase[-3:] + rev_addr = o + swap = Signal() + neg_cos = Signal() + neg_sin = Signal() + with m.If(self.en): + m.d.sync += [ + swap .eq(q ^ o), + neg_cos .eq(s ^ q), + neg_sin .eq(s), + ] + + # Map phase to the [0,pi/4) range. + octant_phase = Signal(addr_width) + octant_mask = rev_addr.replicate(len(octant_phase)) # reverse mask + m.d.comb += octant_phase.eq(octant_mask ^ self.phase[-addr_width-3:-3]) + + # Retrieve precomputed (cos, sin) values from the reduced range. + e_s0 = Signal(IQSample(self.output_width)) + m.d.comb += [ + table_rd.addr.eq(octant_phase), + table_rd.en .eq(self.en), + e_s0 .eq(table_rd.data), + ] + + # Unmap the phase to its original octant. + e_s1 = Signal.like(e_s0) + e_s2 = Signal.like(e_s1) + + m.d.comb += [ + Cat(e_s1.i, e_s1.q) .eq(Mux(swap, Cat(e_s0.q, e_s0.i), e_s0)), + e_s2.i .eq(Mux(neg_cos, -e_s1.i, e_s1.i)), + e_s2.q .eq(Mux(neg_sin, -e_s1.q, e_s1.q)), + ] + m.d.sync += self.output.eq(e_s2) + + return m diff --git a/firmware/fpga/dsp/quarter_shift.py b/firmware/fpga/dsp/quarter_shift.py new file mode 100644 index 00000000..aad8f98d --- /dev/null +++ b/firmware/fpga/dsp/quarter_shift.py @@ -0,0 +1,55 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Module, Signal, Mux +from amaranth.lib import wiring, stream +from amaranth.lib.wiring import In, Out + +from util import IQSample + +class QuarterShift(wiring.Component): + input: In(stream.Signature(IQSample(8), always_ready=True)) + output: Out(stream.Signature(IQSample(8), always_ready=True)) + enable: In(1) + up: In(1) + + def elaborate(self, platform): + m = Module() + + index = Signal(range(4)) + + with m.If(~self.output.valid | self.output.ready): + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + m.d.sync += self.output.valid.eq(self.input.valid) + with m.If(self.input.valid): + # Select direction of shift with the `up` signal. + with m.If(self.up): + m.d.sync += index.eq(index - 1) + with m.Else(): + m.d.sync += index.eq(index + 1) + + # Generate control signals derived from `index`. + swap = index[0] + inv_q = index[0] ^ index[1] + inv_i = index[1] + + # First stage: swap. + i = Mux(swap, self.input.p.q, self.input.p.i) + q = Mux(swap, self.input.p.i, self.input.p.q) + + # Second stage: sign inversion. + i = Mux(inv_i, -i, i) + q = Mux(inv_q, -q, q) + + with m.If(self.enable): + m.d.sync += self.output.p.i.eq(i) + m.d.sync += self.output.p.q.eq(q) + with m.Else(): + m.d.sync += self.output.p.i.eq(self.input.p.i) + m.d.sync += self.output.p.q.eq(self.input.p.q) + + return m \ No newline at end of file diff --git a/firmware/fpga/dsp/round.py b/firmware/fpga/dsp/round.py new file mode 100644 index 00000000..bdd199a1 --- /dev/null +++ b/firmware/fpga/dsp/round.py @@ -0,0 +1,17 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +def convergent_round(value, discarded_bits): + retained = value[discarded_bits:] + discarded = value[:discarded_bits] + msb_discarded = discarded[-1] + rest_discarded = discarded[:-1] + lsb_retained = retained[0] + # Round up: + # - If discarded > 0.5 + # - If discarded == 0.5 and retained is odd + round_up = msb_discarded & (rest_discarded.any() | lsb_retained) + return retained + round_up diff --git a/firmware/fpga/dsp/sb_mac16.py b/firmware/fpga/dsp/sb_mac16.py new file mode 100644 index 00000000..8325f007 --- /dev/null +++ b/firmware/fpga/dsp/sb_mac16.py @@ -0,0 +1,343 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Module, Instance, Signal, Mux, Cat +from amaranth.lib import wiring +from amaranth.lib.wiring import In, Out +from amaranth.vendor import SiliconBluePlatform + +class SB_MAC16(wiring.Component): + + # Input ports + CLK: In(1) + CE: In(1) + C: In(16) + A: In(16) + B: In(16) + D: In(16) + AHOLD: In(1) + BHOLD: In(1) + CHOLD: In(1) + DHOLD: In(1) + IRSTTOP: In(1) + IRSTBOT: In(1) + ORSTTOP: In(1) + ORSTBOT: In(1) + OLOADTOP: In(1) + OLOADBOT: In(1) + ADDSUBTOP: In(1) + ADDSUBBOT: In(1) + OHOLDTOP: In(1) + OHOLDBOT: In(1) + CI: In(1) + ACCUMCI: In(1) + SIGNEXTIN: In(1) + + # Output ports + O: Out(32) + CO: Out(1) + ACCUMCO: Out(1) + SIGNEXTOUT: Out(1) + + + def __init__(self, + NEG_TRIGGER=0, + C_REG=0, + A_REG=0, + B_REG=0, + D_REG=0, + TOP_8x8_MULT_REG=0, + BOT_8x8_MULT_REG=0, + PIPELINE_16x16_MULT_REG1=0, + PIPELINE_16x16_MULT_REG2=0, + TOPOUTPUT_SELECT=0, + TOPADDSUB_LOWERINPUT=0, + TOPADDSUB_UPPERINPUT=0, + TOPADDSUB_CARRYSELECT=0, + BOTOUTPUT_SELECT=0, + BOTADDSUB_LOWERINPUT=0, + BOTADDSUB_UPPERINPUT=0, + BOTADDSUB_CARRYSELECT=0, + MODE_8x8=0, + A_SIGNED=0, + B_SIGNED=0): + + super().__init__() + + # Parameters + self.parameters = dict( + NEG_TRIGGER=NEG_TRIGGER, + C_REG=C_REG, + A_REG=A_REG, + B_REG=B_REG, + D_REG=D_REG, + TOP_8x8_MULT_REG=TOP_8x8_MULT_REG, + BOT_8x8_MULT_REG=BOT_8x8_MULT_REG, + PIPELINE_16x16_MULT_REG1=PIPELINE_16x16_MULT_REG1, + PIPELINE_16x16_MULT_REG2=PIPELINE_16x16_MULT_REG2, + TOPOUTPUT_SELECT=TOPOUTPUT_SELECT, + TOPADDSUB_LOWERINPUT=TOPADDSUB_LOWERINPUT, + TOPADDSUB_UPPERINPUT=TOPADDSUB_UPPERINPUT, + TOPADDSUB_CARRYSELECT=TOPADDSUB_CARRYSELECT, + BOTOUTPUT_SELECT=BOTOUTPUT_SELECT, + BOTADDSUB_LOWERINPUT=BOTADDSUB_LOWERINPUT, + BOTADDSUB_UPPERINPUT=BOTADDSUB_UPPERINPUT, + BOTADDSUB_CARRYSELECT=BOTADDSUB_CARRYSELECT, + MODE_8x8=MODE_8x8, + A_SIGNED=A_SIGNED, + B_SIGNED=B_SIGNED, + ) + + + def elaborate(self, platform): + if isinstance(platform, SiliconBluePlatform): + return self.elaborate_hard_macro() + else: + return self.elaborate_simulation() + + + def elaborate_hard_macro(self): + m = Module() + m.submodules.sb_mac16 = Instance("SB_MAC16", + # Parameters. + **{ f"p_{k}": v for k, v in self.parameters.items() }, + + # Inputs. + i_CLK=self.CLK, + i_CE=self.CE, + i_C=self.C, + i_A=self.A, + i_B=self.B, + i_D=self.D, + i_IRSTTOP=self.IRSTTOP, + i_IRSTBOT=self.IRSTBOT, + i_ORSTTOP=self.ORSTTOP, + i_ORSTBOT=self.ORSTBOT, + i_AHOLD=self.AHOLD, + i_BHOLD=self.BHOLD, + i_CHOLD=self.CHOLD, + i_DHOLD=self.DHOLD, + i_OHOLDTOP=self.OHOLDTOP, + i_OHOLDBOT=self.OHOLDBOT, + i_ADDSUBTOP=self.ADDSUBTOP, + i_ADDSUBBOT=self.ADDSUBBOT, + i_OLOADTOP=self.OLOADTOP, + i_OLOADBOT=self.OLOADBOT, + i_CI=self.CI, + i_ACCUMCI=self.ACCUMCI, + i_SIGNEXTIN=self.SIGNEXTIN, + + # Outputs. + o_O=self.O, + o_CO=self.CO, + o_ACCUMCO=self.ACCUMCO, + o_SIGNEXTOUT=self.SIGNEXTOUT, + ) + return m + + + def elaborate_simulation(self): + m = Module() + + p = self.parameters + + assert p["NEG_TRIGGER"] == 0, "Falling edge input clock polarity not supported in simulation." + + # Internal wire, compare Figure on page 133 of ICE Technology Library 3.0 and Fig 2 on page 2 of Lattice TN1295-DSP + # http://www.latticesemi.com/~/media/LatticeSemi/Documents/TechnicalBriefs/SBTICETechnologyLibrary201608.pdf + # https://www.latticesemi.com/-/media/LatticeSemi/Documents/ApplicationNotes/AD/DSPFunctionUsageGuideforICE40Devices.ashx + iA = Signal(16) + iB = Signal(16) + iC = Signal(16) + iD = Signal(16) + iF = Signal(16) + iJ = Signal(16) + iK = Signal(16) + iG = Signal(16) + iL = Signal(32) + iH = Signal(32) + iW = Signal(16) + iX = Signal(16) + iP = Signal(16) + iQ = Signal(16) + iY = Signal(16) + iZ = Signal(16) + iR = Signal(16) + iS = Signal(16) + HCI = Signal() + LCI = Signal() + LCO = Signal() + + # Registers + rC = Signal(16) + rA = Signal(16) + rB = Signal(16) + rD = Signal(16) + rF = Signal(16) + rJ = Signal(16) + rK = Signal(16) + rG = Signal(16) + rH = Signal(32) + rQ = Signal(16) + rS = Signal(16) + + # Regs C and A + with m.If(self.IRSTTOP): + m.d.sync += [ + rC.eq(0), + rA.eq(0), + ] + with m.Elif(self.CE): + with m.If(~self.CHOLD): + m.d.sync += rC.eq(self.C) + with m.If(~self.AHOLD): + m.d.sync += rA.eq(self.A) + + m.d.comb += [ + iC.eq(rC if p["C_REG"] else self.C), + iA.eq(rA if p["A_REG"] else self.A), + ] + + # Regs B and D + with m.If(self.IRSTBOT): + m.d.sync += [ + rB.eq(0), + rD.eq(0) + ] + with m.Elif(self.CE): + with m.If(~self.BHOLD): + m.d.sync += rB.eq(self.B) + with m.If(~self.DHOLD): + m.d.sync += rD.eq(self.D) + + m.d.comb += [ + iB.eq(rB if p["B_REG"] else self.B), + iD.eq(rD if p["D_REG"] else self.D), + ] + + # Multiplier Stage + p_Ah_Bh = Signal(16) + p_Al_Bh = Signal(16) + p_Ah_Bl = Signal(16) + p_Al_Bl = Signal(16) + Ah = Signal(16) + Al = Signal(16) + Bh = Signal(16) + Bl = Signal(16) + + m.d.comb += [ + Ah.eq(Cat(iA[8:16], Mux(p["A_SIGNED"], iA[15].replicate(8), 0))), + Al.eq(Cat(iA[0:8], Mux(p["A_SIGNED"] & p["MODE_8x8"], iA[7].replicate(8), 0))), + Bh.eq(Cat(iB[8:16], Mux(p["B_SIGNED"], iB[15].replicate(8), 0))), + Bl.eq(Cat(iB[0:8], Mux(p["B_SIGNED"] & p["MODE_8x8"], iB[7].replicate(8), 0))), + p_Ah_Bh.eq(Ah * Bh), # F + p_Al_Bh.eq(Al[0:8] * Bh), # J + p_Ah_Bl.eq(Ah * Bl[0:8]), # K + p_Al_Bl.eq(Al * Bl), # G + ] + + # Regs F and J + with m.If(self.IRSTTOP): + m.d.sync += [ + rF.eq(0), + rJ.eq(0) + ] + with m.Elif(self.CE): + m.d.sync += rF.eq(p_Ah_Bh) + if not p["MODE_8x8"]: + m.d.sync += rJ.eq(p_Al_Bh) + + m.d.comb += [ + iF.eq(rF if p["TOP_8x8_MULT_REG"] else p_Ah_Bh), + iJ.eq(rJ if p["PIPELINE_16x16_MULT_REG1"] else p_Al_Bh), + ] + + # Regs K and G + with m.If(self.IRSTBOT): + m.d.sync += [ + rK.eq(0), + rG.eq(0) + ] + with m.Elif(self.CE): + with m.If(~p["MODE_8x8"]): + m.d.sync += rK.eq(p_Ah_Bl) + m.d.sync += rG.eq(p_Al_Bl) + + m.d.comb += [ + iK.eq(rK if p["PIPELINE_16x16_MULT_REG1"] else p_Ah_Bl), + iG.eq(rG if p["BOT_8x8_MULT_REG"] else p_Al_Bl), + ] + + # Adder Stage + iK_e = Signal(24) + iJ_e = Signal(24) + m.d.comb += [ + iK_e.eq(Cat(iK, Mux(p["A_SIGNED"], iK[15].replicate(8), 0))), + iJ_e.eq(Cat(iJ, Mux(p["B_SIGNED"], iJ[15].replicate(8), 0))), + iL.eq(iG + (iK_e << 8) + (iJ_e << 8) + (iF << 16)), + ] + + # Reg H + with m.If(self.IRSTBOT): + m.d.sync += rH.eq(0) + with m.Elif(self.CE): + if not p["MODE_8x8"]: + m.d.sync += rH.eq(iL) + + m.d.comb += iH.eq(rH if p["PIPELINE_16x16_MULT_REG2"] else iL) + + # Hi Output Stage + XW = Signal(17) + Oh = Signal(16) + + m.d.comb += [ + iW.eq([iQ, iC][p["TOPADDSUB_UPPERINPUT"]]), + iX.eq([iA, iF, iH[16:32], iZ[15].replicate(16)][p["TOPADDSUB_LOWERINPUT"]]), + XW.eq(iX + (iW ^ self.ADDSUBTOP.replicate(16)) + HCI), + self.ACCUMCO.eq(XW[16]), + self.CO.eq(self.ACCUMCO ^ self.ADDSUBTOP), + iP.eq(Mux(self.OLOADTOP, iC, XW[0:16] ^ self.ADDSUBTOP.replicate(16))), + ] + + with m.If(self.ORSTTOP): + m.d.sync += rQ.eq(0) + with m.Elif(self.CE): + with m.If(~self.OHOLDTOP): + m.d.sync += rQ.eq(iP) + + m.d.comb += [ + iQ.eq(rQ), + Oh.eq([iP, iQ, iF, iH[16:32]][p["TOPOUTPUT_SELECT"]]), + HCI.eq([0, 1, LCO, LCO ^ self.ADDSUBBOT][p["TOPADDSUB_CARRYSELECT"]]), + self.SIGNEXTOUT.eq(iX[15]), + ] + + # Lo Output Stage + YZ = Signal(17) + Ol = Signal(16) + + m.d.comb += [ + iY.eq([iS, iD][p["BOTADDSUB_UPPERINPUT"]]), + iZ.eq([iB, iG, iH[0:16], self.SIGNEXTIN.replicate(16)][p["BOTADDSUB_LOWERINPUT"]]), + YZ.eq(iZ + (iY ^ self.ADDSUBBOT.replicate(16)) + LCI), + LCO.eq(YZ[16]), + iR.eq(Mux(self.OLOADBOT, iD, YZ[0:16] ^ self.ADDSUBBOT.replicate(16))), + ] + + with m.If(self.ORSTBOT): + m.d.sync += rS.eq(0) + with m.Elif(self.CE): + with m.If(~self.OHOLDBOT): + m.d.sync += rS.eq(iR) + + m.d.comb += [ + iS.eq(rS), + Ol.eq([iR, iS, iG, iH[0:16]][p["BOTOUTPUT_SELECT"]]), + LCI.eq([0, 1, self.ACCUMCI, self.CI][p["BOTADDSUB_CARRYSELECT"]]), + self.O.eq(Cat(Ol, Oh)), + ] + + return m diff --git a/firmware/fpga/interface/__init__.py b/firmware/fpga/interface/__init__.py new file mode 100644 index 00000000..a19e3fc2 --- /dev/null +++ b/firmware/fpga/interface/__init__.py @@ -0,0 +1 @@ +from .max586x import MAX586xInterface \ No newline at end of file diff --git a/firmware/fpga/interface/max586x.py b/firmware/fpga/interface/max586x.py new file mode 100644 index 00000000..b94d2152 --- /dev/null +++ b/firmware/fpga/interface/max586x.py @@ -0,0 +1,66 @@ +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Module, Signal, C, Cat +from amaranth.lib import io, stream, wiring +from amaranth.lib.wiring import Out, In + +from util import IQSample + +class MAX586xInterface(wiring.Component): + adc_stream: Out(stream.Signature(IQSample(8), always_ready=True)) + dac_stream: In(stream.Signature(IQSample(8), always_ready=True)) + + adc_capture: In(1) + dac_capture: In(1) + q_invert: In(1) + + def __init__(self, bb_domain): + super().__init__() + self._bb_domain = bb_domain + + def elaborate(self, platform): + m = Module() + adc_stream = self.adc_stream + dac_stream = self.dac_stream + + # Generate masks for inverting the Q component based on the q_invert signal. + q_invert = Signal() + rx_q_mask = Signal(8) + tx_q_mask = Signal(10) + m.d[self._bb_domain] += q_invert.eq(self.q_invert) + with m.If(q_invert): + m.d.comb += [ + rx_q_mask.eq(0x80), + tx_q_mask.eq(0x1FF), + ] + with m.Else(): + m.d.comb += [ + rx_q_mask.eq(0x7F), + tx_q_mask.eq(0x200), + ] + + # Capture the ADC signals using a DDR input buffer. + m.submodules.adc_in = adc_in = io.DDRBuffer("i", platform.request("da", dir="-"), i_domain=self._bb_domain) + m.d.comb += [ + adc_stream.p.i .eq(adc_in.i[0] ^ 0x80), # I: non-inverted between MAX2837 and MAX5864. + adc_stream.p.q .eq(adc_in.i[1] ^ rx_q_mask), # Q: inverted between MAX2837 and MAX5864. + adc_stream.valid .eq(self.adc_capture), + ] + + # Output the transformed data to the DAC using a DDR output buffer. + m.submodules.dac_out = dac_out = io.DDRBuffer("o", platform.request("dd", dir="-"), o_domain=self._bb_domain) + with m.If(dac_stream.valid): + m.d.comb += [ + dac_out.o[0] .eq(Cat(C(0, 2), dac_stream.p.i) ^ 0x200), + dac_out.o[1] .eq(Cat(C(0, 2), dac_stream.p.q) ^ tx_q_mask), + ] + with m.Else(): + m.d.comb += [ + dac_out.o[0] .eq(0x200), + dac_out.o[1] .eq(0x200), + ] + + return m \ No newline at end of file diff --git a/firmware/fpga/interface/spi.py b/firmware/fpga/interface/spi.py new file mode 100644 index 00000000..e7a622c5 --- /dev/null +++ b/firmware/fpga/interface/spi.py @@ -0,0 +1,424 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Elaboratable, Module, Instance, Signal, ClockSignal + +# References: +# [1] LATTICE ICE™ Technology Library, Version 3.0, August, 2016 +# [2] iCE40™ LP/HX/LM Family Handbook, HB1011 Version 01.2, November 2013 + +class SPIDeviceInterface(Elaboratable): + + def __init__(self, port): + # I/O port. + self.port = port + + # Data I/O. + self.word_in = Signal(8) + self.word_out = Signal(8) + self.word_in_stb = Signal() + + # Status flags. + self.busy = Signal() + + + def elaborate(self, platform): + m = Module() + + spi_adr = Signal(8, init=0b1000) # address + spi_dati = Signal(8) # data input + spi_dato = Signal(8) # data output + spi_rw = Signal() # selects between read or write (high = write) + spi_stb = Signal() # strobe must be asserted to start a read/write + spi_ack = Signal() # ack that the transfer is done (read valid, write ack) + + # SB_SPI interface is documented in [1]. + sb_spi_params = { + # SPI port connections. + "o_SO": self.port.cipo.o, + "o_SOE": self.port.cipo.oe, + "i_SI": self.port.copi.i, + "i_SCKI": self.port.clk.i, + "i_SCSNI": ~self.port.cs.i, # chip select is inverted due to PinsN + # Internal signaling. + "i_SBCLKI": ClockSignal("sync"), + "i_SBSTBI": spi_stb, + "i_SBRWI": spi_rw, + "o_SBACKO": spi_ack, + } + sb_spi_params |= { f"i_SBADRI{i}": spi_adr[i] for i in range(8) } + sb_spi_params |= { f"i_SBDATI{i}": spi_dati[i] for i in range(8) } + sb_spi_params |= { f"o_SBDATO{i}": spi_dato[i] for i in range(8) } + + m.submodules.sb_spi = sb_spi = Instance("SB_SPI", **sb_spi_params) + + # Register addresses (from [2]). + SPI_ADDR_SPICR0 = 0b1000 # SPI Control Register 0 + SPI_ADDR_SPICR1 = 0b1001 # SPI Control Register 1 + SPI_ADDR_SPICR2 = 0b1010 # SPI Control Register 2 + SPI_ADDR_SPIBR = 0b1011 # SPI Clock Prescale + SPI_ADDR_SPISR = 0b1100 # SPI Status Register + SPI_ADDR_SPITXDR = 0b1101 # SPI Transmit Data Register + SPI_ADDR_SPIRXDR = 0b1110 # SPI Receive Data Register + SPI_ADDR_SPICSR = 0b1111 # SPI Master Chip Select Register + + # Initial values for programming registers ([2]). + registers_init = { + SPI_ADDR_SPICR2: 0b00000110, # CPOL=1 CPHA=1 mode, MSB first + SPI_ADDR_SPICR1: 0b10000000, # Enable SPI + } + + # De-assert strobe signals unless explicitly asserted. + m.d.sync += spi_stb.eq(0) + m.d.sync += self.word_in_stb.eq(0) + + with m.FSM(): + + # Register initialization. + for i, (address, value) in enumerate(registers_init.items()): + with m.State(f"INIT{i}"): + m.d.sync += [ + spi_adr .eq(address), + spi_dati .eq(value), + spi_stb .eq(1), + spi_rw .eq(1), + ] + with m.If(spi_ack): + m.d.sync += spi_stb.eq(0) + if i+1 < len(registers_init): + m.next = f"INIT{i+1}" + else: + m.next = "WAIT" + + with m.State("WAIT"): + m.d.sync += [ + spi_adr .eq(SPI_ADDR_SPISR), + spi_stb .eq(1), + spi_rw .eq(0), + ] + with m.If(spi_ack): + m.d.sync += spi_stb.eq(0) + # bit 3 = RRDY, data is available to read + # bit 4 = TRDY, transmit data is empty + # bit 6 = BUSY, chip select is asserted (low) + # bit 7 = TIP, transfer in progress + m.d.sync += self.busy.eq(spi_dato[6]) + with m.If(spi_dato[7] & spi_dato[4]): + m.next = "SPI_TRANSMIT" + with m.Elif(spi_dato[3]): + m.next = "SPI_READ" + + with m.State("SPI_READ"): + m.d.sync += [ + spi_adr .eq(SPI_ADDR_SPIRXDR), + spi_stb .eq(1), + spi_rw .eq(0), + ] + with m.If(spi_ack): + m.d.sync += [ + spi_stb .eq(0), + self.word_in .eq(spi_dato), + self.word_in_stb .eq(1), + ] + m.next = "WAIT" + + with m.State("SPI_TRANSMIT"): + m.d.sync += [ + spi_adr .eq(SPI_ADDR_SPITXDR), + spi_dati .eq(self.word_out), + spi_stb .eq(1), + spi_rw .eq(1), + ] + with m.If(spi_ack): + m.d.sync += spi_stb.eq(0) + m.next = "WAIT" + + return m + + +class SPICommandInterface(Elaboratable): + """ Wrapper of SPIDeviceInterface that splits data sequences into phases. + + I/O signals: + O: command -- the command read from the SPI bus + O: command_ready -- a new command is ready + + O: word_received -- the most recent word received + O: word_complete -- strobe indicating a new word is present on word_in + I: word_to_send -- the word to be loaded; latched in on next word_complete and while cs is low + """ + + def __init__(self, port): + + # I/O port. + self.interface = SPIDeviceInterface(port) + + # Command I/O. + self.command = Signal(8) + self.command_ready = Signal() + + # Data I/O + self.word_received = Signal(8) + self.word_to_send = Signal.like(self.word_received) + self.word_complete = Signal() + + + def elaborate(self, platform): + + m = Module() + + # Attach our SPI interface. + m.submodules.interface = interface = self.interface + + # De-assert our control signals unless explicitly asserted. + m.d.sync += [ + self.command_ready.eq(0), + self.word_complete.eq(0) + ] + + m.d.comb += interface.word_out.eq(self.word_to_send) + + with m.FSM(): + + with m.State("COMMAND_PHASE"): + with m.If(interface.word_in_stb): + m.d.sync += [ + self.command .eq(interface.word_in), + self.command_ready .eq(1), + ] + m.next = "DATA_PHASE" + + # Do not advance if chip select is deasserted. + with m.If(~interface.busy): + m.next = "COMMAND_PHASE" + + with m.State("DATA_PHASE"): + with m.If(interface.word_in_stb): + m.d.sync += self.word_received.eq(interface.word_in) + m.d.sync += self.word_complete.eq(1) + m.next = "DUMMY_PHASE" + + # Do not advance if chip select is deasserted. + with m.If(~interface.busy): + m.next = "COMMAND_PHASE" + + # The SB_SPI block always returns 0xFF for the second byte, so at least one + # dummy byte must be added to retrieve valid data. This behavior is shown in + # Figure 22-16, "Minimally Specified SPI Transaction Example," from [2]. + with m.State("DUMMY_PHASE"): + with m.If(~interface.busy): + m.next = "COMMAND_PHASE" + + return m + + +class SPIRegisterInterface(Elaboratable): + """ SPI device interface that allows for register reads and writes via SPI. + The SPI transaction format matches: + + in: WAAAAAAA[...] VVVVVVVV[...] DDDDDDDD[...] + out: XXXXXXXX[...] XXXXXXXX[...] RRRRRRRR[...] + + Where: + W = write bit; a '1' indicates that the provided value is a write request + A = all bits of the address + V = value to be written into the register, if W is set + R = value to be read from the register + + Other I/O ports are added dynamically with add_register(). + """ + + def __init__(self, port): + """ + Parameters: + address_size -- the size of an address, in bits; recommended to be one bit + less than a binary number, as the write command is formed by adding a one-bit + write flag to the start of every address + register_size -- The size of any given register, in bits. + """ + + self.address_size = 7 + self.register_size = 8 + + # + # Internal details. + # + + # Instantiate an SPI command transciever submodule. + self.interface = SPICommandInterface(port) + + # Create a new, empty dictionary mapping registers to their signals. + self.registers = {} + + # Create signals for each of our register control signals. + self._is_write = Signal() + self._address = Signal(self.address_size) + + + def _ensure_register_is_unused(self, address): + """ Checks to make sure a register address isn't in use before issuing it. """ + + if address in self.registers: + raise ValueError("can't add more than one register with address 0x{:x}!".format(address)) + + + def add_sfr(self, address, *, read=None, write_signal=None, write_strobe=None, read_strobe=None): + """ Adds a special function register to the given command interface. + + Parameters: + address -- the register's address, as a big-endian integer + read -- a Signal or integer constant representing the + value to be read at the given address; if not provided, the default + value will be read + read_strobe -- a Signal that is asserted when a read is completed; if not provided, + the relevant strobe will be left unconnected + write_signal -- a Signal set to the value to be written when a write is requested; + if not provided, writes will be ignored + write_strobe -- a Signal that goes high when a value is available for a write request + """ + + assert address < (2 ** self.address_size) + self._ensure_register_is_unused(address) + + # Add the register to our collection. + self.registers[address] = { + 'read': read, + 'write_signal': write_signal, + 'write_strobe': write_strobe, + 'read_strobe': read_strobe, + 'elaborate': None, + } + + + def add_read_only_register(self, address, *, read, read_strobe=None): + """ Adds a read-only register. + + Parameters: + address -- the register's address, as a big-endian integer + read -- a Signal or integer constant representing the + value to be read at the given address; if not provided, the default + value will be read + read_strobe -- a Signal that is asserted when a read is completed; if not provided, + the relevant strobe will be left unconnected + """ + self.add_sfr(address, read=read, read_strobe=read_strobe) + + + + def add_register(self, address, *, value_signal=None, size=None, name=None, read_strobe=None, + write_strobe=None, init=0): + """ Adds a standard, memory-backed register. + + Parameters: + address -- the register's address, as a big-endian integer + value_signal -- the signal that will store the register's value; if omitted + a storage register will be created automatically + size -- if value_signal isn't provided, this sets the size of the created register + init -- if value_signal isn't provided, this sets the reset value of the created register + read_strobe -- a Signal to be asserted when the register is read; ignored if not provided + write_strobe -- a Signal to be asserted when the register is written; ignored if not provided + + Returns: + value_signal -- a signal that stores the register's value; which may be the value_signal arg, + or may be a signal created during execution + """ + self._ensure_register_is_unused(address) + + # Generate a name for the register, if we don't already have one. + name = name if name else "register_{:x}".format(address) + + # Generate a backing store for the register, if we don't already have one. + if value_signal is None: + size = self.register_size if (size is None) else size + value_signal = Signal(size, name=name, init=init) + + # If we don't have a write strobe signal, create an internal one. + if write_strobe is None: + write_strobe = Signal(name=name + "_write_strobe") + + # Create our register-value-input and our write strobe. + write_value = Signal.like(value_signal, name=name + "_write_value") + + # Create a generator for a the fragments that will manage the register's memory. + def _elaborate_memory_register(m): + with m.If(write_strobe): + m.d.sync += value_signal.eq(write_value) + + # Add the register to our collection. + self.registers[address] = { + 'read': value_signal, + 'write_signal': write_value, + 'write_strobe': write_strobe, + 'read_strobe': read_strobe, + 'elaborate': _elaborate_memory_register, + } + + return value_signal + + + def _elaborate_register(self, m, register_address, connections): + """ Generates the hardware connections that handle a given register. """ + + # + # Elaborate our register hardware. + # + + # Create a signal that goes high iff the given register is selected. + register_selected = Signal(name="register_address_matches_{:x}".format(register_address)) + m.d.comb += register_selected.eq(self._address == register_address) + + # Our write signal is always connected to word_received; but it's only meaningful + # when write_strobe is asserted. + if connections['write_signal'] is not None: + m.d.comb += connections['write_signal'].eq(self.interface.word_received) + + # If we have a write strobe, assert it iff: + # - this register is selected + # - the relevant command is a write command + # - we've just finished receiving the command's argument + if connections['write_strobe'] is not None: + m.d.comb += [ + connections['write_strobe'].eq(self._is_write & self.interface.word_complete & register_selected) + ] + + # Create essentially the same connection with the read strobe. + if connections['read_strobe'] is not None: + m.d.comb += [ + connections['read_strobe'].eq(~self._is_write & self.interface.word_complete & register_selected) + ] + + # If we have any additional code that assists in elaborating this register, run it. + if connections['elaborate']: + connections['elaborate'](m) + + + def elaborate(self, platform): + m = Module() + + # Attach our SPI interface. + m.submodules.interface = self.interface + + # Split the command into our "write" and "address" signals. + m.d.comb += [ + self._is_write.eq(self.interface.command[-1]), + self._address .eq(self.interface.command[:-1]) + ] + + # Create the control/write logic for each of our registers. + for address, connections in self.registers.items(): + self._elaborate_register(m, address, connections) + + # Build the logic to select the 'to_send' value, which is selected + # from all of our registers according to the selected register address. + with m.Switch(self._address): + for address, connections in self.registers.items(): + if connections['read'] is not None: + with m.Case(address): + # Hook up the word-to-send signal to the read value for the relevant + # register. + m.d.comb += self.interface.word_to_send.eq(connections['read']) + + return m diff --git a/firmware/fpga/requirements.txt b/firmware/fpga/requirements.txt new file mode 100644 index 00000000..4b676b22 --- /dev/null +++ b/firmware/fpga/requirements.txt @@ -0,0 +1,3 @@ +amaranth==v0.5.8 +amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git@23c66d6 +lz4 diff --git a/firmware/fpga/top/ext_precision_rx.py b/firmware/fpga/top/ext_precision_rx.py new file mode 100644 index 00000000..6eb3f138 --- /dev/null +++ b/firmware/fpga/top/ext_precision_rx.py @@ -0,0 +1,215 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Elaboratable, Module, Signal, Mux, Instance, Cat, ClockSignal, DomainRenamer +from amaranth.lib import io, fifo, stream, wiring +from amaranth.lib.wiring import Out, In, connect + +from amaranth_future import fixed + +from board import PralinePlatform, ClockDomainGenerator +from interface import MAX586xInterface +from interface.spi import SPIRegisterInterface +from dsp.fir import FIRFilter +from dsp.fir_mac16 import HalfBandDecimatorMAC16 +from dsp.cic import CICDecimator +from dsp.dc_block import DCBlock +from dsp.quarter_shift import QuarterShift +from util import ClockConverter, IQSample + + +class MCUInterface(wiring.Component): + adc_stream: In(stream.Signature(IQSample(12), always_ready=True)) + direction: In(1) + enable: In(1) + + def __init__(self, domain="sync"): + self._domain = domain + super().__init__() + + def elaborate(self, platform): + m = Module() + + adc_stream = self.adc_stream + + # Determine data transfer direction. + direction = Signal() + enable = Signal() + m.d.sync += enable.eq(self.enable) + m.d.sync += direction.eq(self.direction) + transfer_from_adc = (direction == 0) + + # SGPIO clock and data lines. + m.submodules.clk_out = clk_out = io.DDRBuffer("o", platform.request("host_clk", dir="-"), o_domain=self._domain) + m.submodules.host_io = host_io = io.DDRBuffer('io', platform.request("host_data", dir="-"), i_domain=self._domain, o_domain=self._domain) + + # State machine to control SGPIO clock and data lines. + rx_clk_en = Signal() + m.d.sync += clk_out.o[1].eq(rx_clk_en) + m.d.sync += host_io.oe.eq(transfer_from_adc) + + data_to_host = Signal.like(adc_stream.p) + rx_data_buffer = Signal(8) + m.d.comb += host_io.o[0].eq(rx_data_buffer) + m.d.comb += host_io.o[1].eq(rx_data_buffer) + + with m.FSM(): + with m.State("IDLE"): + m.d.comb += rx_clk_en.eq(enable & transfer_from_adc & adc_stream.valid) + + with m.If(rx_clk_en): + m.d.sync += rx_data_buffer.eq(adc_stream.p.i >> 8) + m.d.sync += data_to_host.eq(adc_stream.p) + m.next = "RX_I1" + + with m.State("RX_I1"): + m.d.comb += rx_clk_en.eq(1) + m.d.sync += rx_data_buffer.eq(data_to_host.i) + m.next = "RX_Q0" + + with m.State("RX_Q0"): + m.d.comb += rx_clk_en.eq(1) + m.d.sync += rx_data_buffer.eq(data_to_host.q >> 8) + m.next = "RX_Q1" + + with m.State("RX_Q1"): + m.d.comb += rx_clk_en.eq(1) + m.d.sync += rx_data_buffer.eq(data_to_host.q) + m.next = "IDLE" + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class FlowAndTriggerControl(wiring.Component): + trigger_en: In(1) + direction: Out(1) # async + enable: Out(1) # async + adc_capture: Out(1) + dac_capture: Out(1) + + def __init__(self, domain): + super().__init__() + self._domain = domain + + def elaborate(self, platform): + m = Module() + + # + # Signal synchronization and trigger logic. + # + trigger_enable = self.trigger_en + trigger_in = platform.request("trigger_in").i + trigger_out = platform.request("trigger_out").o + host_data_enable = ~platform.request("disable").i + m.d.comb += trigger_out.eq(host_data_enable) + + # Create a latch for the trigger input signal using a special FPGA primitive. + trigger_in_latched = Signal() + trigger_in_reg = Instance("SB_DFFES", + i_D = 0, + i_S = trigger_in, # async set + i_E = ~host_data_enable, + i_C = ClockSignal(self._domain), + o_Q = trigger_in_latched + ) + m.submodules.trigger_in_reg = trigger_in_reg + + # Export signals for direction control and capture gating. + m.d.comb += self.direction.eq(platform.request("direction").i) + m.d.comb += self.enable.eq(host_data_enable) + + with m.If(host_data_enable): + m.d[self._domain] += self.adc_capture.eq((trigger_in_latched | ~trigger_enable) & (self.direction == 0)) + m.d[self._domain] += self.dac_capture.eq((trigger_in_latched | ~trigger_enable) & (self.direction == 1)) + with m.Else(): + m.d[self._domain] += self.adc_capture.eq(0) + m.d[self._domain] += self.dac_capture.eq(0) + + return m + + +class Top(Elaboratable): + + def elaborate(self, platform): + m = Module() + + m.submodules.clkgen = ClockDomainGenerator() + + # Submodules. + m.submodules.flow_ctl = flow_ctl = FlowAndTriggerControl(domain="gck1") + m.submodules.adcdac_intf = adcdac_intf = MAX586xInterface(bb_domain="gck1") + m.submodules.mcu_intf = mcu_intf = MCUInterface(domain="sync") + + m.d.comb += adcdac_intf.adc_capture.eq(flow_ctl.adc_capture) + m.d.comb += adcdac_intf.dac_capture.eq(flow_ctl.dac_capture) + m.d.comb += adcdac_intf.q_invert.eq(platform.request("q_invert").i) + m.d.comb += mcu_intf.direction.eq(flow_ctl.direction) + m.d.comb += mcu_intf.enable.eq(flow_ctl.enable) + + # Half-band filter taps. + taps_hb1 = [-2, 0, 5, 0, -10, 0,18, 0, -30, 0,53, 0,-101, 0, 323, 512, 323, 0,-101, 0, 53, 0, -30, 0,18, 0, -10, 0, 5, 0,-2] + taps_hb1 = [ tap/1024 for tap in taps_hb1 ] + + taps_hb2 = [ -6, 0, 19, 0, -44, 0, 89, 0, -163, 0, 278, 0, -452, 0, 711, 0, -1113, 0, 1800, 0, -3298, 0, 10370, 16384, 10370, 0, -3298, 0, 1800, 0, -1113, 0, 711, 0, -452, 0, 278, 0, -163, 0, 89, 0, -44, 0, 19, 0, -6] + taps_hb2 = [ tap/16384/2 for tap in taps_hb2 ] + + rx_chain = { + # DC block and quarter shift. + "dc_block": DCBlock(width=8, num_channels=2, domain="gck1"), + "quarter_shift": DomainRenamer("gck1")(QuarterShift()), + + # CIC mandatory first stage with compensator. + "cic": CICDecimator(2, 4, (4,8,16,32), width_in=8, width_out=12, num_channels=2, always_ready=True, domain="gck1"), + "cic_comp": DomainRenamer("gck1")(FIRFilter([-0.125, 0, 0.75, 0, -0.125], shape=fixed.SQ(11), shape_out=fixed.SQ(11), always_ready=True, num_channels=2)), + + # Final half-band decimator stages. + "hbfir1": HalfBandDecimatorMAC16(taps_hb1, data_shape=fixed.SQ(11), overclock_rate=4, always_ready=True, domain="gck1"), + "hbfir2": HalfBandDecimatorMAC16(taps_hb2, data_shape=fixed.SQ(11), overclock_rate=8, always_ready=True, domain="gck1"), + + # Clock domain conversion. + "clkconv": ClockConverter(IQSample(12), 4, "gck1", "sync", always_ready=True), + } + for k,v in rx_chain.items(): + m.submodules[f"rx_{k}"] = v + + # Connect receiver chain. + last = adcdac_intf.adc_stream + for block in rx_chain.values(): + connect(m, last, block.input) + last = block.output + connect(m, last, mcu_intf.adc_stream) + + # SPI register interface. + spi_port = platform.request("spi") + m.submodules.spi_regs = spi_regs = SPIRegisterInterface(spi_port) + + # Add control registers. + ctrl = spi_regs.add_register(0x01, init=0) + rx_decim = spi_regs.add_register(0x02, init=0, size=3) + #tx_intrp = spi_regs.add_register(0x04, init=0, size=3) + + m.d.comb += [ + # Trigger enable. + flow_ctl.trigger_en .eq(ctrl[7]), + + # RX settings. + rx_chain["dc_block"].enable .eq(ctrl[0]), + rx_chain["quarter_shift"].enable .eq(ctrl[1]), + rx_chain["quarter_shift"].up .eq(ctrl[2]), + + # RX decimation rate. + rx_chain["cic"].factor .eq(rx_decim+2), + ] + + return m + + +if __name__ == "__main__": + plat = PralinePlatform() + plat.build(Top()) diff --git a/firmware/fpga/top/ext_precision_tx.py b/firmware/fpga/top/ext_precision_tx.py new file mode 100644 index 00000000..4268606d --- /dev/null +++ b/firmware/fpga/top/ext_precision_tx.py @@ -0,0 +1,215 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Elaboratable, Module, Signal, Instance, Cat, ClockSignal, DomainRenamer +from amaranth.lib import io, fifo, stream, wiring +from amaranth.lib.wiring import Out, In, connect + +from amaranth_future import fixed + +from board import PralinePlatform, ClockDomainGenerator +from interface import MAX586xInterface +from interface.spi import SPIRegisterInterface +from dsp.fir import FIRFilter +from dsp.fir_mac16 import HalfBandInterpolatorMAC16 +from dsp.cic import CICInterpolator +from util import ClockConverter, IQSample, StreamSkidBuffer + + +class MCUInterface(wiring.Component): + dac_stream: Out(stream.Signature(IQSample(12))) + direction: In(1) + enable: In(1) + + def __init__(self, domain="sync"): + self._domain = domain + super().__init__() + + def elaborate(self, platform): + m = Module() + + dac_stream = self.dac_stream + + # Determine data transfer direction. + direction = Signal() + enable = Signal() + m.d.sync += enable.eq(self.enable) + m.d.sync += direction.eq(self.direction) + transfer_to_dac = (direction == 1) + + # SGPIO clock and data lines. + m.submodules.clk_out = clk_out = io.DDRBuffer("o", platform.request("host_clk", dir="-"), o_domain=self._domain) + m.submodules.host_io = host_io = io.DDRBuffer('io', platform.request("host_data", dir="-"), i_domain=self._domain, o_domain=self._domain) + + # State machine to control SGPIO clock and data lines. + tx_clk_en = Signal() + m.d.sync += clk_out.o[0].eq(tx_clk_en) + + tx_dly_write = Signal(4) + tx_in_sample = Signal(4*8) + m.d.sync += tx_dly_write.eq(tx_dly_write << 1) + m.d.sync += tx_in_sample.eq(Cat(host_io.i[1], tx_in_sample)) + + # Small TX FIFO to avoid overflows from the write delay. + m.submodules.tx_fifo = tx_fifo = fifo.SyncFIFOBuffered(width=24, depth=4) + m.d.comb += [ + tx_fifo.w_data.word_select(0, 12) .eq(tx_in_sample[20:32]), + tx_fifo.w_data.word_select(1, 12) .eq(tx_in_sample[4:16]), + tx_fifo.w_en .eq(tx_dly_write[-1]), + dac_stream.p .eq(tx_fifo.r_data), + dac_stream.valid .eq(tx_fifo.r_rdy), + tx_fifo.r_en .eq(dac_stream.ready), + ] + + with m.FSM(): + with m.State("IDLE"): + m.d.comb += tx_clk_en.eq(enable & transfer_to_dac & dac_stream.ready) + + with m.If(tx_clk_en): + m.next = "TX_I1" + + with m.State("TX_I1"): + m.d.comb += tx_clk_en.eq(1) + m.next = "TX_Q0" + + with m.State("TX_Q0"): + m.d.comb += tx_clk_en.eq(1) + m.next = "TX_Q1" + + with m.State("TX_Q1"): + m.d.comb += tx_clk_en.eq(1) + m.d.sync += tx_dly_write[0].eq(1) # delayed write + m.next = "IDLE" + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class FlowAndTriggerControl(wiring.Component): + trigger_en: In(1) + direction: Out(1) # async + enable: Out(1) # async + adc_capture: Out(1) + dac_capture: Out(1) + + def __init__(self, domain): + super().__init__() + self._domain = domain + + def elaborate(self, platform): + m = Module() + + # + # Signal synchronization and trigger logic. + # + trigger_enable = self.trigger_en + trigger_in = platform.request("trigger_in").i + trigger_out = platform.request("trigger_out").o + host_data_enable = ~platform.request("disable").i + m.d.comb += trigger_out.eq(host_data_enable) + + # Create a latch for the trigger input signal using a special FPGA primitive. + trigger_in_latched = Signal() + trigger_in_reg = Instance("SB_DFFES", + i_D = 0, + i_S = trigger_in, # async set + i_E = ~host_data_enable, + i_C = ClockSignal(self._domain), + o_Q = trigger_in_latched + ) + m.submodules.trigger_in_reg = trigger_in_reg + + # Export signals for direction control and capture gating. + m.d.comb += self.direction.eq(platform.request("direction").i) + m.d.comb += self.enable.eq(host_data_enable) + + with m.If(host_data_enable): + m.d[self._domain] += self.adc_capture.eq((trigger_in_latched | ~trigger_enable) & (self.direction == 0)) + m.d[self._domain] += self.dac_capture.eq((trigger_in_latched | ~trigger_enable) & (self.direction == 1)) + with m.Else(): + m.d[self._domain] += self.adc_capture.eq(0) + m.d[self._domain] += self.dac_capture.eq(0) + + return m + + +class Top(Elaboratable): + + def elaborate(self, platform): + m = Module() + + m.submodules.clkgen = ClockDomainGenerator() + + # Submodules. + m.submodules.flow_ctl = flow_ctl = FlowAndTriggerControl(domain="gck1") + m.submodules.adcdac_intf = adcdac_intf = MAX586xInterface(bb_domain="gck1") + m.submodules.mcu_intf = mcu_intf = MCUInterface(domain="sync") + + m.d.comb += adcdac_intf.dac_capture.eq(flow_ctl.dac_capture) + m.d.comb += adcdac_intf.q_invert.eq(platform.request("q_invert").i) + m.d.comb += mcu_intf.direction.eq(flow_ctl.direction) + m.d.comb += mcu_intf.enable.eq(flow_ctl.enable) + + # Half-band filter taps. + taps_hb1 = [-2, 0, 5, 0, -10, 0,18, 0, -30, 0,53, 0,-101, 0, 323, 512, 323, 0,-101, 0, 53, 0, -30, 0,18, 0, -10, 0, 5, 0,-2] + taps_hb1 = [ tap/1024 for tap in taps_hb1 ] + + taps_hb2 = [3, 0, -16, 0, 77, 128, 77, 0, -16, 0, 3] + taps_hb2 = [ tap/256 for tap in taps_hb2 ] + + tx_chain = { + # Clock domain conversion. + "clkconv": ClockConverter(IQSample(12), 4, "sync", "gck1", always_ready=False), + + # Half-band interpolation stages (+ skid buffers for timing closure). + "hbfir1": HalfBandInterpolatorMAC16(taps_hb1, data_shape=fixed.SQ(11), + overclock_rate=8, num_channels=2, always_ready=False, domain="gck1"), + "skid1": DomainRenamer("gck1")(StreamSkidBuffer(IQSample(12), always_ready=False)), + "hbfir2": HalfBandInterpolatorMAC16(taps_hb2, data_shape=fixed.SQ(11), + overclock_rate=4, num_channels=2, always_ready=False, domain="gck1"), + "skid2": DomainRenamer("gck1")(StreamSkidBuffer(IQSample(12), always_ready=False)), + + # CIC interpolation stage. + "cic_comp": DomainRenamer("gck1")(FIRFilter([-0.125, 0, 0.75, 0, -0.125], shape=fixed.SQ(11), shape_out=fixed.SQ(11), always_ready=False, num_channels=2)), + + "cic_interpolator": CICInterpolator(2, 4, (4, 8, 16, 32), 12, 8, num_channels=2, + always_ready=False, domain="gck1"), + } + for k,v in tx_chain.items(): + m.submodules[f"tx_{k}"] = v + + # Connect transmitter chain. + last = mcu_intf.dac_stream + for block in tx_chain.values(): + connect(m, last, block.input) + last = block.output + connect(m, last, adcdac_intf.dac_stream) + + + # SPI register interface. + spi_port = platform.request("spi") + m.submodules.spi_regs = spi_regs = SPIRegisterInterface(spi_port) + + # Add control registers. + ctrl = spi_regs.add_register(0x01, init=0) + tx_intrp = spi_regs.add_register(0x02, init=0, size=3) + + m.d.comb += [ + # Trigger enable. + flow_ctl.trigger_en .eq(ctrl[7]), + + # TX interpolation rate. + tx_chain["cic_interpolator"].factor .eq(tx_intrp + 2), + ] + + return m + + +if __name__ == "__main__": + plat = PralinePlatform() + plat.build(Top()) diff --git a/firmware/fpga/top/half_precision.py b/firmware/fpga/top/half_precision.py new file mode 100644 index 00000000..4cc0e20b --- /dev/null +++ b/firmware/fpga/top/half_precision.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# +# This file is part of HackRF. +# +# Copyright (c) 2024 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Elaboratable, Module, Signal, C, Mux, Instance, Cat, ClockSignal, DomainRenamer, signed +from amaranth.lib import io, stream, wiring, cdc, data, fifo +from amaranth.lib.wiring import Out, In, connect + +from board import PralinePlatform, ClockDomainGenerator +from interface import MAX586xInterface +from interface.spi import SPIRegisterInterface +from dsp.dc_block import DCBlock +from dsp.round import convergent_round +from util import IQSample, ClockConverter + + +class MCUInterface(wiring.Component): + adc_stream: In(stream.Signature(IQSample(4), always_ready=True)) + dac_stream: Out(stream.Signature(IQSample(4))) + direction: In(1) + enable: In(1) + + def __init__(self, domain="sync"): + self._domain = domain + super().__init__() + + def elaborate(self, platform): + m = Module() + + adc_stream = self.adc_stream + dac_stream = self.dac_stream + + # Determine data transfer direction. + direction = Signal() + enable = Signal() + m.d.sync += enable.eq(self.enable) + m.d.sync += direction.eq(self.direction) + transfer_from_adc = (direction == 0) + transfer_to_dac = (direction == 1) + + # SGPIO clock and data lines. + m.submodules.clk_out = clk_out = io.DDRBuffer("o", platform.request("host_clk", dir="-"), o_domain=self._domain) + m.submodules.host_io = host_io = io.DDRBuffer('io', platform.request("host_data", dir="-"), i_domain=self._domain, o_domain=self._domain) + + # State machine to control SGPIO clock and data lines. + m.d.sync += clk_out.o[0].eq(0) + m.d.sync += clk_out.o[1].eq(0) + m.d.sync += host_io.oe.eq(transfer_from_adc) + + data_to_host = Signal.like(Cat(adc_stream.p.i, adc_stream.p.q)) + assert len(data_to_host) == 8 + m.d.comb += host_io.o[0].eq(data_to_host) + m.d.comb += host_io.o[1].eq(data_to_host) + + tx_dly_write = Signal(2) + m.d.sync += tx_dly_write.eq(tx_dly_write << 1) + m.d.comb += dac_stream.payload.eq(host_io.i[1]) + m.d.comb += dac_stream.valid.eq(tx_dly_write[-1]) + + with m.FSM(): + with m.State("IDLE"): + with m.If(enable): + with m.If(transfer_from_adc & adc_stream.valid): + m.d.sync += data_to_host.eq(Cat(adc_stream.p.i, adc_stream.p.q)) + m.d.sync += clk_out.o[1].eq(1) + + with m.Elif(transfer_to_dac & dac_stream.ready): + m.d.sync += clk_out.o[0].eq(1) + m.d.sync += tx_dly_write[0].eq(1) # delayed write + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class FlowAndTriggerControl(wiring.Component): + trigger_en: In(1) + direction: Out(1) # async + enable: Out(1) # async + adc_capture: Out(1) + dac_capture: Out(1) + + def __init__(self, domain): + super().__init__() + self._domain = domain + + def elaborate(self, platform): + m = Module() + + # + # Signal synchronization and trigger logic. + # + trigger_enable = self.trigger_en + trigger_in = platform.request("trigger_in").i + trigger_out = platform.request("trigger_out").o + host_data_enable = ~platform.request("disable").i + m.d.comb += trigger_out.eq(host_data_enable) + + # Create a latch for the trigger input signal using a FPGA primitive. + trigger_in_latched = Signal() + trigger_in_reg = Instance("SB_DFFES", + i_D = 0, + i_S = trigger_in, # async set + i_E = ~host_data_enable, + i_C = ClockSignal(self._domain), + o_Q = trigger_in_latched + ) + m.submodules.trigger_in_reg = trigger_in_reg + + # Export signals for direction control and gating captures. + m.d.comb += self.direction.eq(platform.request("direction").i) + m.d.comb += self.enable.eq(host_data_enable) + + with m.If(host_data_enable): + m.d[self._domain] += self.adc_capture.eq((trigger_in_latched | ~trigger_enable) & (self.direction == 0)) + m.d[self._domain] += self.dac_capture.eq((trigger_in_latched | ~trigger_enable) & (self.direction == 1)) + with m.Else(): + m.d[self._domain] += self.adc_capture.eq(0) + m.d[self._domain] += self.dac_capture.eq(0) + + return m + + + + +class IQHalfPrecisionConverter(wiring.Component): + input: In(stream.Signature(IQSample(8), always_ready=True)) + output: Out(stream.Signature(IQSample(4), always_ready=True)) + + def elaborate(self, platform): + m = Module() + + m.d.comb += [ + self.output.p.i .eq(convergent_round(self.input.p.i, 4)), + self.output.p.q .eq(convergent_round(self.input.p.q, 4)), + self.output.valid .eq(self.input.valid), + ] + + return m + +class IQHalfPrecisionConverterInv(wiring.Component): + input: In(stream.Signature(IQSample(4))) + output: Out(stream.Signature(IQSample(8))) + + def elaborate(self, platform): + m = Module() + + m.d.comb += [ + self.output.p.i .eq(self.input.p.i << 4), + self.output.p.q .eq(self.input.p.q << 4), + self.output.valid .eq(self.input.valid), + self.input.ready .eq(self.output.ready), + ] + + return m + + +class Top(Elaboratable): + + def elaborate(self, platform): + m = Module() + + m.submodules.clkgen = ClockDomainGenerator() + + # Submodules. + m.submodules.flow_ctl = flow_ctl = FlowAndTriggerControl(domain="gck1") + m.submodules.adcdac_intf = adcdac_intf = MAX586xInterface(bb_domain="gck1") + m.submodules.mcu_intf = mcu_intf = MCUInterface(domain="sync") + + m.d.comb += adcdac_intf.adc_capture.eq(flow_ctl.adc_capture) + m.d.comb += adcdac_intf.dac_capture.eq(flow_ctl.dac_capture) + m.d.comb += adcdac_intf.q_invert.eq(platform.request("q_invert").i) + m.d.comb += mcu_intf.direction.eq(flow_ctl.direction) + m.d.comb += mcu_intf.enable.eq(flow_ctl.enable) + + rx_chain = { + "dc_block": DCBlock(width=8, num_channels=2, domain="gck1"), + "half_prec": DomainRenamer("gck1")(IQHalfPrecisionConverter()), + "clkconv": ClockConverter(IQSample(4), 4, "gck1", "sync"), + } + m.submodules += rx_chain.values() + + # Connect receiver chain. + last = adcdac_intf.adc_stream + for block in rx_chain.values(): + connect(m, last, block.input) + last = block.output + connect(m, last, mcu_intf.adc_stream) + + + tx_chain = { + "clkconv": ClockConverter(IQSample(4), 4, "sync", "gck1", always_ready=False), + "half_prec": DomainRenamer("gck1")(IQHalfPrecisionConverterInv()), + } + m.submodules += tx_chain.values() + + # Connect transmitter chain. + last = mcu_intf.dac_stream + for block in tx_chain.values(): + connect(m, last, block.input) + last = block.output + connect(m, last, adcdac_intf.dac_stream) + + # SPI register interface. + spi_port = platform.request("spi") + m.submodules.spi_regs = spi_regs = SPIRegisterInterface(spi_port) + + # Add control registers. + ctrl = spi_regs.add_register(0x01, init=0) + m.d.comb += [ + # Trigger enable. + flow_ctl.trigger_en .eq(ctrl[7]), + + # RX settings. + rx_chain["dc_block"].enable .eq(ctrl[0]), + ] + + return m + + +if __name__ == "__main__": + plat = PralinePlatform() + plat.build(Top_HP()) diff --git a/firmware/fpga/top/standard.py b/firmware/fpga/top/standard.py new file mode 100644 index 00000000..50c73df8 --- /dev/null +++ b/firmware/fpga/top/standard.py @@ -0,0 +1,320 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Elaboratable, Module, Signal, Mux, Instance, Cat, ClockSignal, DomainRenamer, EnableInserter +from amaranth.lib import io, fifo, stream, wiring, cdc +from amaranth.lib.wiring import Out, In, connect + +from amaranth_future import fixed + +from board import PralinePlatform, ClockDomainGenerator +from interface import MAX586xInterface +from interface.spi import SPIRegisterInterface +from dsp.fir import HalfBandDecimator, HalfBandInterpolator +from dsp.cic import CICDecimator, CICInterpolator +from dsp.dc_block import DCBlock +from dsp.quarter_shift import QuarterShift +from dsp.nco import NCO +from util import ClockConverter, IQSample, StreamSkidBuffer, LinearFeedbackShiftRegister + + +class MCUInterface(wiring.Component): + adc_stream: In(stream.Signature(IQSample(8), always_ready=True)) + dac_stream: Out(stream.Signature(IQSample(8))) + direction: In(1) + enable: In(1) + prbs: In(1) + + def __init__(self, domain="sync"): + self._domain = domain + super().__init__() + + def elaborate(self, platform): + m = Module() + + adc_stream = self.adc_stream + dac_stream = self.dac_stream + + # Determine data transfer direction. + direction = Signal() + enable = Signal() + m.submodules.enable_cdc = cdc.FFSynchronizer(self.enable, enable, o_domain=self._domain) + m.submodules.direction_cdc = cdc.FFSynchronizer(self.direction, direction, o_domain=self._domain) + transfer_from_adc = (direction == 0) + transfer_to_dac = (direction == 1) + + # SGPIO clock and data lines. + m.submodules.clk_out = clk_out = io.DDRBuffer("o", platform.request("host_clk", dir="-"), o_domain=self._domain) + m.submodules.host_io = host_io = io.DDRBuffer('io', platform.request("host_data", dir="-"), i_domain=self._domain, o_domain=self._domain) + + # State machine to control SGPIO clock and data lines. + tx_clk_en = Signal() + rx_clk_en = Signal() + m.d.sync += clk_out.o[0].eq(tx_clk_en) + m.d.sync += clk_out.o[1].eq(rx_clk_en) + m.d.sync += host_io.oe.eq(transfer_from_adc) + + data_to_host = Signal.like(adc_stream.p) + m.d.comb += host_io.o[0].eq(data_to_host) + m.d.comb += host_io.o[1].eq(data_to_host) + + tx_dly_write = Signal(3) + host_io_prev_data = Signal(8) + m.d.sync += tx_dly_write.eq(tx_dly_write << 1) + m.d.sync += host_io_prev_data.eq(host_io.i[1]) + + # Small TX FIFO to avoid overflows from the write delay. + m.submodules.tx_fifo = tx_fifo = fifo.SyncFIFOBuffered(width=16, depth=8) + m.d.comb += [ + tx_fifo.w_data .eq(Cat(host_io_prev_data, host_io.i[1])), + tx_fifo.w_en .eq(tx_dly_write[-1]), + dac_stream.p .eq(tx_fifo.r_data), + dac_stream.valid .eq(tx_fifo.r_rdy), + tx_fifo.r_en .eq(dac_stream.ready), + ] + + # Pseudo-random binary sequence generator. + prbs_advance = Signal() + prbs_count = Signal(2) + m.submodules.prbs = prbs = EnableInserter(prbs_advance)( + LinearFeedbackShiftRegister(degree=8, taps=[8,6,5,4], init=0b10110001)) + + with m.FSM(): + with m.State("IDLE"): + m.d.comb += tx_clk_en.eq(enable & transfer_to_dac & dac_stream.ready) + m.d.comb += rx_clk_en.eq(enable & transfer_from_adc & adc_stream.valid) + + with m.If(self.prbs): + m.next = "PRBS" + with m.Elif(rx_clk_en): + m.d.sync += data_to_host.eq(adc_stream.p) + m.next = "RX_Q" + with m.Elif(tx_clk_en): + m.next = "TX_Q" + + with m.State("RX_Q"): + m.d.comb += rx_clk_en.eq(1) + m.d.sync += data_to_host.i.eq(data_to_host.q) + m.next = "IDLE" + + with m.State("TX_Q"): + m.d.comb += tx_clk_en.eq(1) + m.d.sync += tx_dly_write[0].eq(1) # delayed write + m.next = "IDLE" + + with m.State("PRBS"): + m.d.sync += host_io.oe.eq(1) + m.d.sync += data_to_host.eq(prbs.value) + m.d.comb += rx_clk_en.eq(prbs_count == 0) + m.d.comb += prbs_advance.eq(prbs_count == 0) + m.d.sync += prbs_count.eq(prbs_count + 1) + with m.If(~self.prbs): + m.next = "IDLE" + + if self._domain != "sync": + m = DomainRenamer(self._domain)(m) + + return m + + +class FlowAndTriggerControl(wiring.Component): + trigger_en: In(1) + direction: Out(1) # async + enable: Out(1) # async + adc_capture: Out(1) + dac_capture: Out(1) + + def __init__(self, domain): + super().__init__() + self._domain = domain + + def elaborate(self, platform): + m = Module() + + # + # Signal synchronization and trigger logic. + # + trigger_enable = self.trigger_en + trigger_in = platform.request("trigger_in").i + trigger_out = platform.request("trigger_out").o + host_data_enable = ~platform.request("disable").i + m.d.comb += trigger_out.eq(host_data_enable) + + # Create a latch for the trigger input signal using a special FPGA primitive. + trigger_in_latched = Signal() + trigger_in_reg = Instance("SB_DFFES", + i_D = 0, + i_S = trigger_in, # async set + i_E = ~host_data_enable, + i_C = ClockSignal(self._domain), + o_Q = trigger_in_latched + ) + m.submodules.trigger_in_reg = trigger_in_reg + + # Export signals for direction control and capture gating. + m.d.comb += self.direction.eq(platform.request("direction").i) + m.d.comb += self.enable.eq(host_data_enable) + + with m.If(host_data_enable): + m.d[self._domain] += self.adc_capture.eq((trigger_in_latched | ~trigger_enable) & (self.direction == 0)) + m.d[self._domain] += self.dac_capture.eq((trigger_in_latched | ~trigger_enable) & (self.direction == 1)) + with m.Else(): + m.d[self._domain] += self.adc_capture.eq(0) + m.d[self._domain] += self.dac_capture.eq(0) + + return m + + +class Top(Elaboratable): + + def elaborate(self, platform): + m = Module() + + m.submodules.clkgen = ClockDomainGenerator() + + # Submodules. + m.submodules.flow_ctl = flow_ctl = FlowAndTriggerControl(domain="gck1") + m.submodules.adcdac_intf = adcdac_intf = MAX586xInterface(bb_domain="gck1") + m.submodules.mcu_intf = mcu_intf = MCUInterface(domain="sync") + + m.d.comb += adcdac_intf.adc_capture.eq(flow_ctl.adc_capture) + m.d.comb += adcdac_intf.dac_capture.eq(flow_ctl.dac_capture) + m.d.comb += adcdac_intf.q_invert.eq(platform.request("q_invert").i) + m.d.comb += mcu_intf.direction.eq(flow_ctl.direction) + m.d.comb += mcu_intf.enable.eq(flow_ctl.enable) + + # Half-band filter taps. + taps = [-2, 0, 7, 0, -18, 0, 41, 0, -92, 0, 320, 512, 320, 0, -92, 0, 41, 0, -18, 0, 7, 0, -2] + taps = [ tap/1024 for tap in taps ] + + taps2 = [3, 0, -16, 0, 77, 128, 77, 0, -16, 0, 3] + taps2 = [ tap/256 for tap in taps2 ] + + taps3 = [-9, 0, 73, 128, 73, 0, -9] + taps3 = [ tap/256 for tap in taps3 ] + + taps4 = [-8, 0, 72, 128, 72, 0, -8] + taps4 = [ tap/256 for tap in taps4 ] + + taps5 = [-1, 0, 9, 16, 9, 0, -1] + taps5 = [ tap/32 for tap in taps5 ] + + common_rx_filter_opts = dict( + data_shape=fixed.SQ(7), + always_ready=True, + domain="gck1", + ) + + rx_chain = { + # DC block and quarter shift. + "dc_block": DCBlock(width=8, num_channels=2, domain="gck1"), + "quarter_shift": DomainRenamer("gck1")(QuarterShift()), + + # Half-band decimation stages. + "hbfir5": HalfBandDecimator(taps5, **common_rx_filter_opts), + "hbfir4": HalfBandDecimator(taps4, **common_rx_filter_opts), + "hbfir3": HalfBandDecimator(taps3, **common_rx_filter_opts), + "hbfir2": HalfBandDecimator(taps2, **common_rx_filter_opts), + "hbfir1": HalfBandDecimator(taps, **common_rx_filter_opts), + + # Clock domain conversion. + "clkconv": ClockConverter(IQSample(8), 4, "gck1", "sync"), + } + for k,v in rx_chain.items(): + m.submodules[f"rx_{k}"] = v + + # Connect receiver chain. + last = adcdac_intf.adc_stream + for block in rx_chain.values(): + connect(m, last, block.input) + last = block.output + connect(m, last, mcu_intf.adc_stream) + + tx_chain = { + # Clock domain conversion. + "clkconv": ClockConverter(IQSample(8), 4, "sync", "gck1", always_ready=False), + + # Half-band interpolation stages (+ skid buffers for timing closure). + "hbfir1": HalfBandInterpolator(taps, data_shape=fixed.SQ(7), + num_channels=2, always_ready=False, domain="gck1"), + "skid2": DomainRenamer("gck1")(StreamSkidBuffer(IQSample(8), always_ready=False)), + "hbfir2": HalfBandInterpolator(taps2, data_shape=fixed.SQ(7), + num_channels=2, always_ready=False, domain="gck1"), + "skid3": DomainRenamer("gck1")(StreamSkidBuffer(IQSample(8), always_ready=False)), + + # CIC interpolation stage. + "cic_interpolator": CICInterpolator(1, 3, (1, 2, 4, 8), 8, 8, num_channels=2, + always_ready=False, domain="gck1"), + } + for k,v in tx_chain.items(): + m.submodules[f"tx_{k}"] = v + + # Connect transmitter chain. + last = mcu_intf.dac_stream + for block in tx_chain.values(): + connect(m, last, block.input) + last = block.output + # DAC can also be driven with an internal NCO. + m.submodules.nco = nco = DomainRenamer("gck1")(NCO(phase_width=16, output_width=8)) + with m.If(nco.en): + m.d.comb += [ + adcdac_intf.dac_stream.p.eq(nco.output), + adcdac_intf.dac_stream.valid.eq(1), + tx_chain["cic_interpolator"].output.ready.eq(1), + ] + with m.Else(): + connect(m, last, adcdac_intf.dac_stream) + + # SPI register interface. + spi_port = platform.request("spi") + m.submodules.spi_regs = spi_regs = SPIRegisterInterface(spi_port) + + # Add control registers. + ctrl = spi_regs.add_register(0x01, init=0) + rx_decim = spi_regs.add_register(0x02, init=0, size=3) + tx_ctrl = spi_regs.add_register(0x03, init=0, size=1) + tx_intrp = spi_regs.add_register(0x04, init=0, size=3) + tx_pstep = spi_regs.add_register(0x05, init=0) + + m.d.sync += [ + # Trigger enable. + flow_ctl.trigger_en .eq(ctrl[7]), + + # PRBS enable. + mcu_intf.prbs .eq(ctrl[6]), + + # RX settings. + rx_chain["dc_block"].enable .eq(ctrl[0]), + rx_chain["quarter_shift"].enable .eq(ctrl[1]), + rx_chain["quarter_shift"].up .eq(ctrl[2]), + + # RX decimation rate. + rx_chain["hbfir5"].enable .eq(rx_decim > 4), + rx_chain["hbfir4"].enable .eq(rx_decim > 3), + rx_chain["hbfir3"].enable .eq(rx_decim > 2), + rx_chain["hbfir2"].enable .eq(rx_decim > 1), + rx_chain["hbfir1"].enable .eq(rx_decim > 0), + + # TX interpolation rate. + tx_chain["cic_interpolator"].factor .eq(Mux(tx_intrp > 2, tx_intrp - 2, 0)), + tx_chain["hbfir1"].enable .eq(tx_intrp > 0), + tx_chain["hbfir2"].enable .eq(tx_intrp > 1), + ] + + # TX NCO control. + tx_pstep_gck1 = Signal(8) + m.submodules.nco_phase_cdc = cdc.FFSynchronizer(tx_pstep, tx_pstep_gck1, o_domain="gck1") + m.d.gck1 += [ + nco.en .eq(tx_ctrl[0]), + nco.phase .eq(nco.phase + (tx_pstep_gck1 << 6)), + ] + + return m + + +if __name__ == "__main__": + plat = PralinePlatform() + plat.build(Top()) diff --git a/firmware/fpga/util/__init__.py b/firmware/fpga/util/__init__.py new file mode 100644 index 00000000..75334121 --- /dev/null +++ b/firmware/fpga/util/__init__.py @@ -0,0 +1,57 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from amaranth import Module, signed, Shape +from amaranth.lib import wiring, stream, data, fifo +from amaranth.lib.wiring import In, Out + +from ._stream import StreamSkidBuffer, StreamMux, StreamDemux +from .lfsr import LinearFeedbackShiftRegister + + +class IQSample(data.StructLayout): + def __init__(self, width=8): + super().__init__({ + "i": signed(width), + "q": signed(width), + }) + + +class ClockConverter(wiring.Component): + + def __init__(self, shape, depth, input_domain, output_domain, always_ready=True): + super().__init__({ + "input": In(stream.Signature(shape, always_ready=always_ready)), + "output": Out(stream.Signature(shape, always_ready=always_ready)), + }) + self.shape = shape + self.depth = depth + self._input_domain = input_domain + self._output_domain = output_domain + + def elaborate(self, platform): + m = Module() + + m.submodules.mem = mem = fifo.AsyncFIFO( + width=Shape.cast(self.shape).width, + depth=self.depth, + r_domain=self._output_domain, + w_domain=self._input_domain) + + m.d.comb += [ + # write. + mem.w_data .eq(self.input.p), + mem.w_en .eq(self.input.valid), + # read. + self.output.p .eq(mem.r_data), + self.output.valid .eq(mem.r_rdy), + mem.r_en .eq(self.output.ready), + ] + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(mem.w_rdy) + + return m + diff --git a/firmware/fpga/util/_stream.py b/firmware/fpga/util/_stream.py new file mode 100644 index 00000000..ad6c28e3 --- /dev/null +++ b/firmware/fpga/util/_stream.py @@ -0,0 +1,189 @@ +# +# This file is part of HackRF. +# +# Copyright (c) 2025 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +import unittest + +from amaranth import Module, Mux, Signal, Cat +from amaranth.lib import wiring, stream, data +from amaranth.lib.wiring import In, Out +from amaranth.sim import Simulator + + +class StreamSkidBuffer(wiring.Component): + + def __init__(self, shape, always_ready=False): + super().__init__({ + "input": In(stream.Signature(shape, always_ready=always_ready)), + "output": Out(stream.Signature(shape, always_ready=always_ready)), + }) + + def elaborate(self, platform): + m = Module() + + # To provide for the "elasticity" needed due to a registered "ready" signal, we need + # two registers for the payload. When the consumer is not ready, there's a cycle + # where the data from the producer is stored in r_payload. + # Read https://www.itdev.co.uk/blog/pipelining-axi-buses-registered-ready-signals + + r_payload = Signal.like(self.input.payload, reset_less=True) + r_valid = Signal() + + with m.If(self.input.ready): + m.d.sync += r_valid.eq(self.input.valid) + m.d.sync += r_payload.eq(self.input.payload) + + # r_valid can only be asserted when there is incoming data but the consumer is not ready. + with m.If(self.output.ready): + m.d.sync += r_valid.eq(0) + + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(~r_valid) + m.d.comb += self.output.valid.eq(self.input.valid | r_valid) + m.d.comb += self.output.p.eq(Mux(r_valid, r_payload, self.input.p)) + + return m + + +class StreamMux(wiring.Component): + + def __init__(self, data_shape, num_channels, always_ready=False): + self.num_channels = num_channels + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(data_shape, num_channels), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(data_shape, 1), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + m = Module() + + ratio = self.num_channels + counter = Signal(range(ratio)) + + sreg = Signal.like(self.input.p) + m.d.comb += self.output.payload.eq(sreg[0]) + + with m.If(self.output.ready & self.output.valid): + m.d.sync += counter.eq(counter + 1) + for i in range(ratio-1): + m.d.sync += sreg[i].eq(sreg[i+1]) + + with m.If(~self.output.valid | (self.output.ready & (counter == ratio-1))): + m.d.sync += self.output.valid.eq(self.input.valid) + m.d.sync += sreg.eq(self.input.payload) + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + + return m + + +class StreamDemux(wiring.Component): + + def __init__(self, data_shape, num_channels, always_ready=False): + self.num_channels = num_channels + super().__init__({ + "input": In(stream.Signature( + data.ArrayLayout(data_shape, 1), + always_ready=always_ready + )), + "output": Out(stream.Signature( + data.ArrayLayout(data_shape, num_channels), + always_ready=always_ready + )), + }) + + def elaborate(self, platform): + m = Module() + + ratio = self.num_channels + counter = Signal(range(ratio)) + + with m.If(~self.output.valid | self.output.ready): + m.d.sync += self.output.valid.eq(self.input.valid & (counter == ratio-1)) + if not self.input.signature.always_ready: + m.d.comb += self.input.ready.eq(1) + with m.If(self.input.valid): + + m.d.sync += self.output.p[ratio-1].eq(self.input.p[0]) + for i in range(ratio-1): + m.d.sync += self.output.p[i].eq(self.output.p[i+1]) + + # TODO: if I remove the following line timing is much worse. Study why. + m.d.sync += self.output.p.eq(Cat(self.output.p[len(self.input.p):], self.input.p)) + m.d.sync += counter.eq(counter + 1) + + return m + + + + +class TestStreamMux(unittest.TestCase): + + def test_mux(self): + dut = StreamMux(data_shape=8, num_channels=2) + input_stream = [[0xAA, 0xBB], [0xCC, 0xDD]] + output_stream = [] + output_len = 4 + + async def stream_input(ctx): + for sample in input_stream: + ctx.set(dut.input.payload, sample) + ctx.set(dut.input.valid, 1) + await ctx.tick().until(dut.input.ready) + ctx.set(dut.input.valid, 0) + + async def stream_output(ctx): + ctx.set(dut.output.ready, 1) + while len(output_stream) < output_len: + await ctx.tick() + if ctx.get(dut.output.valid): + output_stream.append(ctx.get(dut.output.payload)) + + sim = Simulator(dut) + sim.add_clock(1e-6) + sim.add_testbench(stream_input) + sim.add_testbench(stream_output) + sim.run() + + self.assertListEqual(output_stream, [[0xAA], [0xBB], [0xCC], [0xDD]]) + + + def test_demux(self): + dut = StreamDemux(data_shape=8, num_channels=2) + input_stream = [[0xAA], [0xBB], [0xCC], [0xDD]] + output_stream = [] + output_len = 2 + + async def stream_input(ctx): + for sample in input_stream: + ctx.set(dut.input.payload, sample) + ctx.set(dut.input.valid, 1) + await ctx.tick().until(dut.input.ready) + ctx.set(dut.input.valid, 0) + + async def stream_output(ctx): + ctx.set(dut.output.ready, 1) + while len(output_stream) < output_len: + await ctx.tick() + if ctx.get(dut.output.valid): + output_stream.append(ctx.get(dut.output.payload)) + + sim = Simulator(dut) + sim.add_clock(1e-6) + sim.add_testbench(stream_input) + sim.add_testbench(stream_output) + sim.run() + + self.assertListEqual(output_stream, [[0xAA, 0xBB], [0xCC, 0xDD]]) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/firmware/fpga/util/lfsr.py b/firmware/fpga/util/lfsr.py new file mode 100644 index 00000000..740d80ac --- /dev/null +++ b/firmware/fpga/util/lfsr.py @@ -0,0 +1,59 @@ +# +# This file is part of Glasgow Interface Explorer. +# +# Copyright (c) 2025 Glasgow Interface Explorer contributors. +# SPDX-License-Identifier: BSD-0-Clause + +from amaranth import * + + +__all__ = ["LinearFeedbackShiftRegister"] + + +class LinearFeedbackShiftRegister(Elaboratable): + """A linear feedback shift register. Useful for generating long pseudorandom sequences with + a minimal amount of logic. + + Use ``CEInserter`` and ``ResetInserter`` transformers to control the LFSR. + + :param degree: + Width of register, in bits. + :type degree: int + :param taps: + Feedback taps, with bits numbered starting at 1 (i.e. polynomial degrees). + :type taps: list of int + :param reset: + Initial value loaded into the register. Must be non-zero, or only zeroes will be + generated. + :type reset: int + """ + + def __init__(self, degree, taps, init=1): + assert init != 0 + + self.degree = degree + self.taps = taps + self.init = init + + self.value = Signal(degree, init=init) + + def elaborate(self, platform): + m = Module() + feedback = 0 + for tap in self.taps: + feedback ^= (self.value >> (tap - 1)) & 1 + m.d.sync += self.value.eq((self.value << 1) | feedback) + return m + + def generate(self): + """Generate every distinct value the LFSR will take.""" + value = self.init + mask = (1 << self.degree) - 1 + while True: + yield value + feedback = 0 + for tap in self.taps: + feedback ^= (value >> (tap - 1)) & 1 + value = ((value << 1) & mask) | feedback + if value == self.init: + break \ No newline at end of file diff --git a/firmware/hackrf-common.cmake b/firmware/hackrf-common.cmake index 1fd3df11..06cb7bec 100644 --- a/firmware/hackrf-common.cmake +++ b/firmware/hackrf-common.cmake @@ -40,6 +40,8 @@ SET(LIBOPENCM3 ${PATH_HACKRF_FIRMWARE}/libopencm3) SET(PATH_DFU_PY ${PATH_HACKRF_FIRMWARE}/dfu.py) SET(PATH_CPLD_BITSTREAM_TOOL ${PATH_HACKRF_FIRMWARE}/tools/cpld_bitstream.py) set(PATH_HACKRF_CPLD_DATA_C ${CMAKE_CURRENT_BINARY_DIR}/hackrf_cpld_data.c) +SET(PATH_PRALINE_FPGA_BIN ${PATH_HACKRF_FIRMWARE}/fpga/build/praline_fpga.bin) +SET(PATH_PRALINE_FPGA_OBJ ${CMAKE_CURRENT_BINARY_DIR}/fpga.o) include(${PATH_HACKRF_FIRMWARE}/dfu-util.cmake) @@ -72,7 +74,7 @@ if(NOT DEFINED BOARD) set(BOARD HACKRF_ONE) endif() -if(BOARD STREQUAL "HACKRF_ONE") +if(BOARD STREQUAL "HACKRF_ONE" OR BOARD STREQUAL "PRALINE") set(MCU_PARTNO LPC4320) else() set(MCU_PARTNO LPC4330) @@ -84,7 +86,7 @@ endif() SET(HACKRF_OPTS "-D${BOARD} -DLPC43XX -D${MCU_PARTNO} -DTX_ENABLE -D'VERSION_STRING=\"${VERSION}\"'") -SET(LDSCRIPT_M4 "-T${PATH_HACKRF_FIRMWARE_COMMON}/${MCU_PARTNO}_M4_memory.ld -Tlibopencm3_lpc43xx_rom_to_ram.ld -T${PATH_HACKRF_FIRMWARE_COMMON}/LPC43xx_M4_M0_image_from_text.ld") +SET(LDSCRIPT_M4 "-T${PATH_HACKRF_FIRMWARE_COMMON}/${MCU_PARTNO}_M4_memory.ld -Tlibopencm3_lpc43xx_rom_to_ram.ld -T${PATH_HACKRF_FIRMWARE_COMMON}/LPC43xx_M4_M0_image_from_text.ld -T${PATH_HACKRF_FIRMWARE_COMMON}/LPC43xx_M4_memory_rom_only.ld") SET(LDSCRIPT_M4_RAM "-T${PATH_HACKRF_FIRMWARE_COMMON}/${MCU_PARTNO}_M4_memory.ld -Tlibopencm3_lpc43xx.ld -T${PATH_HACKRF_FIRMWARE_COMMON}/LPC43xx_M4_M0_image_from_text.ld") @@ -143,7 +145,7 @@ macro(DeclareTarget project_name variant_suffix cflags ldflags) add_library(${project_name}${variant_suffix}_objects OBJECT ${SRC_M4} ${project_name}${variant_suffix}_m0_bin.s) set_target_properties(${project_name}${variant_suffix}_objects PROPERTIES COMPILE_FLAGS "${cflags}") - add_executable(${project_name}${variant_suffix}.elf $) + add_executable(${project_name}${variant_suffix}.elf $ ${OBJ_M4}) add_dependencies(${project_name}${variant_suffix}.elf libopencm3_${project_name}) target_link_libraries( @@ -170,11 +172,6 @@ macro(DeclareTargets) ${PATH_HACKRF_FIRMWARE_COMMON}/sgpio.c ${PATH_HACKRF_FIRMWARE_COMMON}/rf_path.c ${PATH_HACKRF_FIRMWARE_COMMON}/si5351c.c - ${PATH_HACKRF_FIRMWARE_COMMON}/max283x.c - ${PATH_HACKRF_FIRMWARE_COMMON}/max2837.c - ${PATH_HACKRF_FIRMWARE_COMMON}/max2837_target.c - ${PATH_HACKRF_FIRMWARE_COMMON}/max2839.c - ${PATH_HACKRF_FIRMWARE_COMMON}/max2839_target.c ${PATH_HACKRF_FIRMWARE_COMMON}/max5864.c ${PATH_HACKRF_FIRMWARE_COMMON}/max5864_target.c ${PATH_HACKRF_FIRMWARE_COMMON}/mixer.c @@ -191,6 +188,9 @@ macro(DeclareTargets) ${PATH_HACKRF_FIRMWARE_COMMON}/clkin.c ${PATH_HACKRF_FIRMWARE_COMMON}/gpdma.c ${PATH_HACKRF_FIRMWARE_COMMON}/user_config.c + ${PATH_HACKRF_FIRMWARE_COMMON}/radio.c + ${PATH_HACKRF_FIRMWARE_COMMON}/selftest.c + ${PATH_HACKRF_FIRMWARE_COMMON}/m0_state.c ) if(BOARD STREQUAL "RAD1O") @@ -207,6 +207,25 @@ macro(DeclareTargets) ) endif() + if(BOARD STREQUAL "PRALINE") + SET(SRC_M4 + ${SRC_M4} + ${PATH_HACKRF_FIRMWARE_COMMON}/fpga.c + ${PATH_HACKRF_FIRMWARE_COMMON}/ice40_spi.c + ${PATH_HACKRF_FIRMWARE_COMMON}/max2831.c + ${PATH_HACKRF_FIRMWARE_COMMON}/max2831_target.c + ) + else() + SET(SRC_M4 + ${SRC_M4} + ${PATH_HACKRF_FIRMWARE_COMMON}/max283x.c + ${PATH_HACKRF_FIRMWARE_COMMON}/max2837.c + ${PATH_HACKRF_FIRMWARE_COMMON}/max2837_target.c + ${PATH_HACKRF_FIRMWARE_COMMON}/max2839.c + ${PATH_HACKRF_FIRMWARE_COMMON}/max2839_target.c + ) + endif() + link_directories( "${PATH_HACKRF_FIRMWARE_COMMON}" "${LIBOPENCM3}/lib" @@ -228,7 +247,7 @@ macro(DeclareTargets) set_target_properties(${PROJECT_NAME}_m0.elf PROPERTIES LINK_FLAGS "${LDFLAGS_M0}") DeclareTarget("${PROJECT_NAME}" "" "${CFLAGS_M4}" "${LDFLAGS_M4}") - DeclareTarget("${PROJECT_NAME}" "_ram" "${CFLAGS_M4_RAM}" "${LDFLAGS_M4_RAM}") + DeclareTarget("${PROJECT_NAME}" "_ram" "${CFLAGS_M4_RAM} -DRAM_MODE" "${LDFLAGS_M4_RAM}") DeclareTarget("${PROJECT_NAME}" "_dfu" "${CFLAGS_M4_RAM} -DDFU_MODE" "${LDFLAGS_M4_RAM}") add_custom_target( diff --git a/firmware/hackrf_usb/CMakeLists.txt b/firmware/hackrf_usb/CMakeLists.txt index 311c18ff..f4b06d8e 100644 --- a/firmware/hackrf_usb/CMakeLists.txt +++ b/firmware/hackrf_usb/CMakeLists.txt @@ -31,6 +31,18 @@ add_custom_command( DEPENDS ${PATH_CPLD_BITSTREAM_TOOL} ${PATH_HACKRF_CPLD_XSVF} ) +add_custom_command( + OUTPUT ${PATH_PRALINE_FPGA_OBJ} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${PATH_PRALINE_FPGA_BIN} "fpga.bin" + COMMAND ${CMAKE_OBJCOPY} + -I binary + -O elf32-littlearm + --rename-section .data=.rom_only + "fpga.bin" ${PATH_PRALINE_FPGA_OBJ} + DEPENDS ${PATH_PRALINE_FPGA_BIN} +) + set(SRC_M4 hackrf_usb.c "${PATH_HACKRF_FIRMWARE_COMMON}/tuning.c" @@ -42,22 +54,17 @@ set(SRC_M4 usb_device.c usb_endpoint.c usb_api_board_info.c - usb_api_cpld.c usb_api_m0_state.c usb_api_register.c usb_api_spiflash.c usb_api_transceiver.c usb_api_operacake.c usb_api_sweep.c + usb_api_selftest.c usb_api_ui.c "${PATH_HACKRF_FIRMWARE_COMMON}/usb_queue.c" "${PATH_HACKRF_FIRMWARE_COMMON}/fault_handler.c" "${PATH_HACKRF_FIRMWARE_COMMON}/cpld_jtag.c" - "${PATH_HACKRF_FIRMWARE_COMMON}/cpld_xc2c.c" - "${PATH_HACKRF_CPLD_DATA_C}" - "${PATH_HACKRF_FIRMWARE_COMMON}/xapp058/lenval.c" - "${PATH_HACKRF_FIRMWARE_COMMON}/xapp058/micro.c" - "${PATH_HACKRF_FIRMWARE_COMMON}/xapp058/ports.c" "${PATH_HACKRF_FIRMWARE_COMMON}/crc.c" "${PATH_HACKRF_FIRMWARE_COMMON}/rom_iap.c" "${PATH_HACKRF_FIRMWARE_COMMON}/operacake.c" @@ -66,7 +73,28 @@ set(SRC_M4 set(SRC_M0 sgpio_m0.s) -if(BOARD STREQUAL "HACKRF_ONE") +if(BOARD STREQUAL "PRALINE") + SET(SRC_M4 + ${SRC_M4} + "${PATH_HACKRF_FIRMWARE_COMMON}/lz4_blk.c" + usb_api_praline.c + ) + SET(OBJ_M4 + ${PATH_PRALINE_FPGA_OBJ} + ) +else() + SET(SRC_M4 + ${SRC_M4} + usb_api_cpld.c + "${PATH_HACKRF_FIRMWARE_COMMON}/cpld_xc2c.c" + "${PATH_HACKRF_CPLD_DATA_C}" + "${PATH_HACKRF_FIRMWARE_COMMON}/xapp058/lenval.c" + "${PATH_HACKRF_FIRMWARE_COMMON}/xapp058/micro.c" + "${PATH_HACKRF_FIRMWARE_COMMON}/xapp058/ports.c" + ) +endif() + +if(BOARD STREQUAL "HACKRF_ONE" OR BOARD STREQUAL "PRALINE") SET(SRC_M4 ${SRC_M4} "${PATH_HACKRF_FIRMWARE_COMMON}/portapack.c" diff --git a/firmware/hackrf_usb/hackrf_usb.c b/firmware/hackrf_usb/hackrf_usb.c index e6536795..d9717864 100644 --- a/firmware/hackrf_usb/hackrf_usb.c +++ b/firmware/hackrf_usb/hackrf_usb.c @@ -46,6 +46,8 @@ #include "usb_api_register.h" #include "usb_api_spiflash.h" #include "usb_api_operacake.h" +#include "usb_api_praline.h" +#include "usb_api_selftest.h" #include "operacake.h" #include "usb_api_sweep.h" #include "usb_api_transceiver.h" @@ -57,6 +59,8 @@ #include "hackrf_ui.h" #include "platform_detect.h" #include "clkin.h" +#include "fpga.h" +#include "selftest.h" extern uint32_t __m0_start__; extern uint32_t __m0_end__; @@ -92,7 +96,7 @@ static usb_request_handler_fn vendor_request_handler[] = { usb_vendor_request_set_vga_gain, usb_vendor_request_set_txvga_gain, NULL, // was set_if_freq -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) usb_vendor_request_set_antenna_enable, #else NULL, @@ -126,6 +130,24 @@ static usb_request_handler_fn vendor_request_handler[] = { usb_vendor_request_read_supported_platform, usb_vendor_request_set_leds, usb_vendor_request_user_config_set_bias_t_opts, +#ifdef PRALINE + usb_vendor_request_spi_write_fpga, + usb_vendor_request_spi_read_fpga, + usb_vendor_request_p2_ctrl, + usb_vendor_request_p1_ctrl, + usb_vendor_request_set_narrowband_filter, + usb_vendor_request_set_fpga_bitstream, + usb_vendor_request_clkin_ctrl, +#else + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, +#endif + usb_vendor_request_read_selftest, }; static const uint32_t vendor_request_handler_count = @@ -200,6 +222,7 @@ void usb_set_descriptor_by_serial_number(void) } } +#ifndef PRALINE static bool cpld_jtag_sram_load(jtag_t* const jtag) { cpld_jtag_take(jtag); @@ -211,6 +234,7 @@ static bool cpld_jtag_sram_load(jtag_t* const jtag) cpld_jtag_release(jtag); return success; } +#endif static void m0_rom_to_ram() { @@ -231,15 +255,23 @@ int main(void) // Copy M0 image from ROM before SPIFI is disabled m0_rom_to_ram(); + // This will be cleared if any self-test check fails. + selftest.report.pass = true; + detect_hardware_platform(); pin_setup(); +#ifndef PRALINE enable_1v8_power(); +#else + enable_3v3aux_power(); + enable_1v2_power(); +#endif #ifdef HACKRF_ONE // Set up mixer before enabling RF power, because its // GPO is used to control the antenna bias tee. mixer_setup(&mixer); #endif -#if (defined HACKRF_ONE || defined RAD1O) +#if (defined HACKRF_ONE || defined RAD1O || defined PRALINE) enable_rf_power(); #endif cpu_clock_init(); @@ -248,11 +280,21 @@ int main(void) ipc_halt_m0(); ipc_start_m0((uint32_t) &__ram_m0_start__); +#ifndef PRALINE if (!cpld_jtag_sram_load(&jtag_cpld)) { halt_and_flash(6000000); } +#else + #if !defined(DFU_MODE) && !defined(RAM_MODE) + if (!fpga_image_load(0)) { + halt_and_flash(6000000); + } + delay_us_at_mhz(100, 204); + fpga_sgpio_selftest(); + #endif +#endif -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) portapack_init(); #endif @@ -281,6 +323,10 @@ int main(void) rf_path_init(&rf_path); +#ifdef PRALINE + fpga_if_xcvr_selftest(); +#endif + bool operacake_allow_gpio; if (hackrf_ui()->operacake_gpio_compatible()) { operacake_allow_gpio = true; @@ -321,9 +367,11 @@ int main(void) case TRANSCEIVER_MODE_RX_SWEEP: sweep_mode(request.seq); break; +#ifndef PRALINE case TRANSCEIVER_MODE_CPLD_UPDATE: cpld_update(); break; +#endif default: break; } diff --git a/firmware/hackrf_usb/usb_api_m0_state.c b/firmware/hackrf_usb/usb_api_m0_state.c index 8c77bb73..678d8dee 100644 --- a/firmware/hackrf_usb/usb_api_m0_state.c +++ b/firmware/hackrf_usb/usb_api_m0_state.c @@ -26,21 +26,6 @@ #include #include -void m0_set_mode(enum m0_mode mode) -{ - // Set requested mode and flag bit. - m0_state.requested_mode = M0_REQUEST_FLAG | mode; - - // The M0 may be blocked waiting for the next SGPIO interrupt. - // In order to ensure that it sees our request, we need to set - // the interrupt flag here. The M0 will clear the flag again - // before acknowledging our request. - SGPIO_SET_STATUS_1 = (1 << SGPIO_SLICE_A); - - // Wait for M0 to acknowledge by clearing the flag. - while (m0_state.requested_mode & M0_REQUEST_FLAG) {} -} - usb_request_status_t usb_vendor_request_get_m0_state( usb_endpoint_t* const endpoint, const usb_transfer_stage_t stage) diff --git a/firmware/hackrf_usb/usb_api_m0_state.h b/firmware/hackrf_usb/usb_api_m0_state.h index 021ac65b..32f40fcf 100644 --- a/firmware/hackrf_usb/usb_api_m0_state.h +++ b/firmware/hackrf_usb/usb_api_m0_state.h @@ -19,51 +19,14 @@ * Boston, MA 02110-1301, USA. */ -#ifndef __M0_STATE_H__ -#define __M0_STATE_H__ +#ifndef __M0_STATE_USB_H__ +#define __M0_STATE_USB_H__ -#include #include - -#define M0_REQUEST_FLAG (1 << 16) - -struct m0_state { - uint32_t requested_mode; - uint32_t active_mode; - uint32_t m0_count; - uint32_t m4_count; - uint32_t num_shortfalls; - uint32_t longest_shortfall; - uint32_t shortfall_limit; - uint32_t threshold; - uint32_t next_mode; - uint32_t error; -}; - -enum m0_mode { - M0_MODE_IDLE = 0, - M0_MODE_WAIT = 1, - M0_MODE_RX = 2, - M0_MODE_TX_START = 3, - M0_MODE_TX_RUN = 4, -}; - -enum m0_error { - M0_ERROR_NONE = 0, - M0_ERROR_RX_TIMEOUT = 1, - M0_ERROR_TX_TIMEOUT = 2, -}; - -/* Address of m0_state is set in ldscripts. If you change the name of this - * variable, it won't be where it needs to be in the processor's address space, - * unless you also adjust the ldscripts. - */ -extern volatile struct m0_state m0_state; - -void m0_set_mode(enum m0_mode mode); +#include "m0_state.h" usb_request_status_t usb_vendor_request_get_m0_state( usb_endpoint_t* const endpoint, const usb_transfer_stage_t stage); -#endif /*__M0_STATE_H__*/ +#endif /*__M0_STATE_USB_H__*/ diff --git a/firmware/hackrf_usb/usb_api_praline.c b/firmware/hackrf_usb/usb_api_praline.c new file mode 100644 index 00000000..9fcde4b1 --- /dev/null +++ b/firmware/hackrf_usb/usb_api_praline.c @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2022 Great Scott Gadgets + * Copyright 2012 Jared Boone + * Copyright 2013 Benjamin Vernoux + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "usb_api_praline.h" +#include "usb_queue.h" +#include + +#include + +usb_request_status_t usb_vendor_request_p1_ctrl( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + p1_ctrl_set(endpoint->setup.value); + usb_transfer_schedule_ack(endpoint->in); + } + return USB_REQUEST_STATUS_OK; +} + +usb_request_status_t usb_vendor_request_p2_ctrl( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + p2_ctrl_set(endpoint->setup.value); + usb_transfer_schedule_ack(endpoint->in); + } + return USB_REQUEST_STATUS_OK; +} + +usb_request_status_t usb_vendor_request_clkin_ctrl( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + clkin_ctrl_set(endpoint->setup.value & 1); + usb_transfer_schedule_ack(endpoint->in); + } + return USB_REQUEST_STATUS_OK; +} + +usb_request_status_t usb_vendor_request_set_narrowband_filter( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + narrowband_filter_set(endpoint->setup.value); + usb_transfer_schedule_ack(endpoint->in); + } + return USB_REQUEST_STATUS_OK; +} + +bool fpga_image_load(unsigned int index); + +usb_request_status_t usb_vendor_request_set_fpga_bitstream( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + if (!fpga_image_load(endpoint->setup.value)) { + return USB_REQUEST_STATUS_STALL; + } + usb_transfer_schedule_ack(endpoint->in); + } + return USB_REQUEST_STATUS_OK; +} \ No newline at end of file diff --git a/firmware/hackrf_usb/usb_api_praline.h b/firmware/hackrf_usb/usb_api_praline.h new file mode 100644 index 00000000..2fb2bd48 --- /dev/null +++ b/firmware/hackrf_usb/usb_api_praline.h @@ -0,0 +1,48 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __USB_API_PRALINE_H__ +#define __USB_API_PRALINE_H__ + +#include +#include + +usb_request_status_t usb_vendor_request_p2_ctrl( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage); + +usb_request_status_t usb_vendor_request_p1_ctrl( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage); + +usb_request_status_t usb_vendor_request_clkin_ctrl( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage); + +usb_request_status_t usb_vendor_request_set_narrowband_filter( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage); + +usb_request_status_t usb_vendor_request_set_fpga_bitstream( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage); + +#endif /* end of include guard: __USB_API_PRALINE_H__ */ diff --git a/firmware/hackrf_usb/usb_api_register.c b/firmware/hackrf_usb/usb_api_register.c index 8058fb5e..7e043750 100644 --- a/firmware/hackrf_usb/usb_api_register.c +++ b/firmware/hackrf_usb/usb_api_register.c @@ -25,8 +25,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -38,6 +40,7 @@ usb_request_status_t usb_vendor_request_write_max283x( const usb_transfer_stage_t stage) { if (stage == USB_TRANSFER_STAGE_SETUP) { +#ifndef PRALINE if (endpoint->setup.index < MAX2837_NUM_REGS) { if (endpoint->setup.value < MAX2837_DATA_REGS_MAX_VALUE) { max283x_reg_write( @@ -48,6 +51,18 @@ usb_request_status_t usb_vendor_request_write_max283x( return USB_REQUEST_STATUS_OK; } } +#else + if (endpoint->setup.index < MAX2831_NUM_REGS) { + if (endpoint->setup.value < MAX2831_DATA_REGS_MAX_VALUE) { + max2831_reg_write( + &max283x, + endpoint->setup.index, + endpoint->setup.value); + usb_transfer_schedule_ack(endpoint->in); + return USB_REQUEST_STATUS_OK; + } + } +#endif return USB_REQUEST_STATUS_STALL; } else { return USB_REQUEST_STATUS_OK; @@ -59,6 +74,7 @@ usb_request_status_t usb_vendor_request_read_max283x( const usb_transfer_stage_t stage) { if (stage == USB_TRANSFER_STAGE_SETUP) { +#ifndef PRALINE if (endpoint->setup.index < MAX2837_NUM_REGS) { const uint16_t value = max283x_reg_read(&max283x, endpoint->setup.index); @@ -73,6 +89,22 @@ usb_request_status_t usb_vendor_request_read_max283x( usb_transfer_schedule_ack(endpoint->out); return USB_REQUEST_STATUS_OK; } +#else + if (endpoint->setup.index < MAX2831_NUM_REGS) { + const uint16_t value = + max2831_reg_read(&max283x, endpoint->setup.index); + endpoint->buffer[0] = value & 0xff; + endpoint->buffer[1] = value >> 8; + usb_transfer_schedule_block( + endpoint->in, + &endpoint->buffer, + 2, + NULL, + NULL); + usb_transfer_schedule_ack(endpoint->out); + return USB_REQUEST_STATUS_OK; + } +#endif return USB_REQUEST_STATUS_STALL; } else { return USB_REQUEST_STATUS_OK; @@ -219,3 +251,39 @@ usb_request_status_t usb_vendor_request_user_config_set_bias_t_opts( } return USB_REQUEST_STATUS_OK; } + +#ifdef PRALINE +usb_request_status_t usb_vendor_request_spi_write_fpga( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + ssp1_set_mode_ice40(); + ice40_spi_write(&ice40, endpoint->setup.index, endpoint->setup.value); + ssp1_set_mode_max283x(); + usb_transfer_schedule_ack(endpoint->in); + return USB_REQUEST_STATUS_OK; + } + return USB_REQUEST_STATUS_OK; +} + +usb_request_status_t usb_vendor_request_spi_read_fpga( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + ssp1_set_mode_ice40(); + const uint8_t value = ice40_spi_read(&ice40, endpoint->setup.index); + ssp1_set_mode_max283x(); + endpoint->buffer[0] = value; + usb_transfer_schedule_block( + endpoint->in, + &endpoint->buffer, + 1, + NULL, + NULL); + usb_transfer_schedule_ack(endpoint->out); + } + return USB_REQUEST_STATUS_OK; +} +#endif diff --git a/firmware/hackrf_usb/usb_api_register.h b/firmware/hackrf_usb/usb_api_register.h index cb3a2f77..36ec34b7 100644 --- a/firmware/hackrf_usb/usb_api_register.h +++ b/firmware/hackrf_usb/usb_api_register.h @@ -57,5 +57,11 @@ usb_request_status_t usb_vendor_request_set_leds( usb_request_status_t usb_vendor_request_user_config_set_bias_t_opts( usb_endpoint_t* const endpoint, const usb_transfer_stage_t stage); +usb_request_status_t usb_vendor_request_spi_write_fpga( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage); +usb_request_status_t usb_vendor_request_spi_read_fpga( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage); #endif /* end of include guard: __USB_API_REGISTER_H__ */ diff --git a/firmware/hackrf_usb/usb_api_selftest.c b/firmware/hackrf_usb/usb_api_selftest.c new file mode 100644 index 00000000..71cdb415 --- /dev/null +++ b/firmware/hackrf_usb/usb_api_selftest.c @@ -0,0 +1,130 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include "usb_api_selftest.h" +#include "selftest.h" +#include "platform_detect.h" + +static char* itoa(int val, int base) +{ + static char buf[32] = {0}; + int i = 30; + if (val == 0) { + buf[0] = '0'; + buf[1] = '\0'; + return &buf[0]; + } + for (; val && i; --i, val /= base) + buf[i] = "0123456789abcdef"[val % base]; + return &buf[i + 1]; +} + +void append(char** dest, size_t* capacity, const char* str) +{ + for (int i = 0;; i++) { + if (capacity == 0 || str[i] == '\0') { + return; + } + *((*dest)++) = str[i]; + *capacity -= 1; + } +} + +void generate_selftest_report(void) +{ + char* s = &selftest.report.msg[0]; + size_t c = sizeof(selftest.report.msg); +#ifdef RAD1O + append(&s, &c, "Mixer: MAX2871, ID: "); + append(&s, &c, itoa(selftest.mixer_id, 10)); + append(&s, &c, "\n"); +#else + append(&s, &c, "Mixer: RFFC5072, ID: "); + append(&s, &c, itoa(selftest.mixer_id >> 3, 10)); + append(&s, &c, ", Rev: "); + append(&s, &c, itoa(selftest.mixer_id & 0x7, 10)); + append(&s, &c, "\n"); +#endif + append(&s, &c, "Clock: Si5351"); + append(&s, &c, ", Rev: "); + append(&s, &c, itoa(selftest.si5351_rev_id, 10)); + append(&s, &c, ", readback: "); + append(&s, &c, selftest.si5351_readback_ok ? "OK" : "FAIL"); + append(&s, &c, "\n"); +#ifdef PRALINE + append(&s, &c, "Transceiver: MAX2831, LD pin test: "); + append(&s, &c, selftest.max2831_ld_test_ok ? "PASS" : "FAIL"); + append(&s, &c, "\n"); +#else + append(&s, &c, "Transceiver: "); + append(&s, + &c, + (detected_platform() == BOARD_ID_HACKRF1_R9 ? "MAX2839" : "MAX2837")); + append(&s, &c, ", readback success: "); + append(&s, &c, itoa(selftest.max283x_readback_register_count, 10)); + append(&s, &c, "/"); + append(&s, &c, itoa(selftest.max283x_readback_total_registers, 10)); + if (selftest.max283x_readback_register_count < + selftest.max283x_readback_total_registers) { + append(&s, &c, ", bad value: 0x"); + append(&s, &c, itoa(selftest.max283x_readback_bad_value, 10)); + append(&s, &c, ", expected: 0x"); + append(&s, &c, itoa(selftest.max283x_readback_expected_value, 10)); + } + append(&s, &c, "\n"); +#endif +#ifndef RAD1O + append(&s, &c, "32kHz oscillator: "); + append(&s, &c, selftest.rtc_osc_ok ? "PASS" : "FAIL"); + append(&s, &c, "\n"); +#endif +#ifdef PRALINE + append(&s, &c, "SGPIO RX test: "); + append(&s, &c, selftest.sgpio_rx_ok ? "PASS" : "FAIL"); + append(&s, &c, "\n"); + append(&s, &c, "Loopback test: "); + append(&s, &c, selftest.xcvr_loopback_ok ? "PASS" : "FAIL"); + append(&s, &c, "\n"); +#endif +} + +usb_request_status_t usb_vendor_request_read_selftest( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + generate_selftest_report(); + usb_transfer_schedule_block( + endpoint->in, + &selftest.report, + sizeof(selftest.report), + NULL, + NULL); + usb_transfer_schedule_ack(endpoint->out); + return USB_REQUEST_STATUS_OK; + } else { + return USB_REQUEST_STATUS_OK; + } +} diff --git a/firmware/hackrf_usb/usb_api_selftest.h b/firmware/hackrf_usb/usb_api_selftest.h new file mode 100644 index 00000000..a2f38816 --- /dev/null +++ b/firmware/hackrf_usb/usb_api_selftest.h @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Great Scott Gadgets + * + * This file is part of HackRF. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __USB_API_SELFTEST_H +#define __USB_API_SELFTEST_H + +#include +#include + +usb_request_status_t usb_vendor_request_read_selftest( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage); + +#endif // __USB_API_SELFTEST_H diff --git a/firmware/hackrf_usb/usb_api_sweep.c b/firmware/hackrf_usb/usb_api_sweep.c index af58a85e..ae4e74b4 100644 --- a/firmware/hackrf_usb/usb_api_sweep.c +++ b/firmware/hackrf_usb/usb_api_sweep.c @@ -33,11 +33,15 @@ #include -#define MIN(x, y) ((x) < (y) ? (x) : (y)) -#define MAX(x, y) ((x) > (y) ? (x) : (y)) -#define FREQ_GRANULARITY 1000000 -#define MAX_RANGES 10 -#define THROWAWAY_BUFFERS 2 +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#define FREQ_GRANULARITY 1000000 +#define MAX_RANGES 10 +#ifndef PRALINE + #define THROWAWAY_BUFFERS 2 +#else + #define THROWAWAY_BUFFERS 1 +#endif static uint64_t sweep_freq; static uint16_t frequencies[MAX_RANGES * 2]; @@ -88,7 +92,11 @@ usb_request_status_t usb_vendor_request_init_sweep( ((uint16_t) (data[10 + i * 2]) << 8) + data[9 + i * 2]; } sweep_freq = (uint64_t) frequencies[0] * FREQ_GRANULARITY; - set_freq(sweep_freq + offset); + radio_set_frequency( + &radio, + RADIO_CHANNEL0, + RADIO_FREQUENCY_RF, + (radio_frequency_t){.hz = sweep_freq + offset}); usb_transfer_schedule_ack(endpoint->in); } return USB_REQUEST_STATUS_OK; @@ -99,9 +107,9 @@ void sweep_bulk_transfer_complete(void* user_data, unsigned int bytes_transferre (void) user_data; (void) bytes_transferred; - // For each buffer transferred, we need to bump the count by three buffers - // worth of data, to allow for the discarded buffers. - m0_state.m4_count += 3 * 0x4000; + // For each buffer transferred, we need to bump the count to allow + // for the buffer(s) that are to be discarded. + m0_state.m4_count += (THROWAWAY_BUFFERS + 1) * 0x4000; } void sweep_mode(uint32_t seq) @@ -121,8 +129,8 @@ void sweep_mode(uint32_t seq) // 4. M4 adds the sweep metadata at the start of the block and // schedules a bulk transfer for the block. // - // 5. M4 retunes - this takes about 760us worst-case, so should be - // complete before the M0 goes back to RX. + // 5. M4 retunes - this takes about 760us worst-case (300us on praline), + // so should be complete before the M0 goes back to RX. // // 6. M4 spins until the M0 mode changes to RX, then advances the // m0_count limit by 16K and sets the next mode to WAIT. @@ -152,8 +160,9 @@ void sweep_mode(uint32_t seq) } } - // Set M0 to switch back to RX after two more buffers. - m0_state.threshold += 0x8000; + // Set M0 to switch back to RX after we have received our + // discard buffers. + m0_state.threshold += (0x4000 * THROWAWAY_BUFFERS); m0_state.next_mode = M0_MODE_RX; // Write metadata to buffer. @@ -178,7 +187,7 @@ void sweep_mode(uint32_t seq) NULL); // Use other buffer next time. - phase = (phase + 1) % 2; + phase = (phase + 1) % THROWAWAY_BUFFERS; if (++blocks_queued == dwell_blocks) { // Calculate next sweep frequency. @@ -211,7 +220,11 @@ void sweep_mode(uint32_t seq) } // Retune to new frequency. nvic_disable_irq(NVIC_USB0_IRQ); - set_freq(sweep_freq + offset); + radio_set_frequency( + &radio, + RADIO_CHANNEL0, + RADIO_FREQUENCY_RF, + (radio_frequency_t){.hz = sweep_freq + offset}); nvic_enable_irq(NVIC_USB0_IRQ); blocks_queued = 0; } diff --git a/firmware/hackrf_usb/usb_api_transceiver.c b/firmware/hackrf_usb/usb_api_transceiver.c index c639a11c..0858a6ff 100644 --- a/firmware/hackrf_usb/usb_api_transceiver.c +++ b/firmware/hackrf_usb/usb_api_transceiver.c @@ -78,7 +78,17 @@ usb_request_status_t usb_vendor_request_set_baseband_filter_bandwidth( if (stage == USB_TRANSFER_STAGE_SETUP) { const uint32_t bandwidth = (endpoint->setup.index << 16) | endpoint->setup.value; - if (baseband_filter_bandwidth_set(bandwidth)) { + radio_error_t result = radio_set_filter( + &radio, + RADIO_CHANNEL0, + RADIO_FILTER_BASEBAND, + (radio_filter_t){.hz = bandwidth}); + if (result == RADIO_OK) { + radio_filter_t real = radio_get_filter( + &radio, + RADIO_CHANNEL0, + RADIO_FILTER_BASEBAND); + hackrf_ui()->set_filter_bw(real.hz); usb_transfer_schedule_ack(endpoint->in); return USB_REQUEST_STATUS_OK; } @@ -103,7 +113,43 @@ usb_request_status_t usb_vendor_request_set_freq( } else if (stage == USB_TRANSFER_STAGE_DATA) { const uint64_t freq = set_freq_params.freq_mhz * 1000000ULL + set_freq_params.freq_hz; - if (set_freq(freq)) { + radio_error_t result = radio_set_frequency( + &radio, + RADIO_CHANNEL0, + RADIO_FREQUENCY_RF, + (radio_frequency_t){.hz = freq}); + if (result == RADIO_OK) { + usb_transfer_schedule_ack(endpoint->in); + return USB_REQUEST_STATUS_OK; + } + return USB_REQUEST_STATUS_STALL; + } else { + return USB_REQUEST_STATUS_OK; + } +} + +usb_request_status_t usb_vendor_request_set_freq_explicit( + usb_endpoint_t* const endpoint, + const usb_transfer_stage_t stage) +{ + if (stage == USB_TRANSFER_STAGE_SETUP) { + usb_transfer_schedule_block( + endpoint->out, + &explicit_params, + sizeof(struct set_freq_explicit_params), + NULL, + NULL); + return USB_REQUEST_STATUS_OK; + } else if (stage == USB_TRANSFER_STAGE_DATA) { + radio_error_t result = radio_set_frequency( + &radio, + RADIO_CHANNEL0, + RADIO_FREQUENCY_RF, + (radio_frequency_t){ + .if_hz = explicit_params.if_freq_hz, + .lo_hz = explicit_params.lo_freq_hz, + .path = explicit_params.path}); + if (result == RADIO_OK) { usb_transfer_schedule_ack(endpoint->in); return USB_REQUEST_STATUS_OK; } @@ -126,9 +172,15 @@ usb_request_status_t usb_vendor_request_set_sample_rate_frac( NULL); return USB_REQUEST_STATUS_OK; } else if (stage == USB_TRANSFER_STAGE_DATA) { - if (sample_rate_frac_set( - set_sample_r_params.freq_hz * 2, - set_sample_r_params.divider)) { + radio_error_t result = radio_set_sample_rate( + &radio, + RADIO_CHANNEL0, + RADIO_SAMPLE_RATE_CLOCKGEN, + (radio_sample_rate_t){ + .num = set_sample_r_params.freq_hz * 2, + .div = set_sample_r_params.divider, + }); + if (result == RADIO_OK) { usb_transfer_schedule_ack(endpoint->in); return USB_REQUEST_STATUS_OK; } @@ -142,14 +194,17 @@ usb_request_status_t usb_vendor_request_set_amp_enable( usb_endpoint_t* const endpoint, const usb_transfer_stage_t stage) { + radio_gain_t off = {.enable = false}; + radio_gain_t on = {.enable = true}; + if (stage == USB_TRANSFER_STAGE_SETUP) { switch (endpoint->setup.value) { case 0: - rf_path_set_lna(&rf_path, 0); + radio_set_gain(&radio, RADIO_CHANNEL0, RADIO_GAIN_RF_AMP, off); usb_transfer_schedule_ack(endpoint->in); return USB_REQUEST_STATUS_OK; case 1: - rf_path_set_lna(&rf_path, 1); + radio_set_gain(&radio, RADIO_CHANNEL0, RADIO_GAIN_RF_AMP, on); usb_transfer_schedule_ack(endpoint->in); return USB_REQUEST_STATUS_OK; default: @@ -165,8 +220,9 @@ usb_request_status_t usb_vendor_request_set_lna_gain( const usb_transfer_stage_t stage) { if (stage == USB_TRANSFER_STAGE_SETUP) { - uint8_t value; - value = max283x_set_lna_gain(&max283x, endpoint->setup.index); + radio_gain_t gain = {.db = endpoint->setup.index}; + uint8_t value = + radio_set_gain(&radio, RADIO_CHANNEL0, RADIO_GAIN_RX_LNA, gain); endpoint->buffer[0] = value; if (value) { hackrf_ui()->set_bb_lna_gain(endpoint->setup.index); @@ -188,8 +244,9 @@ usb_request_status_t usb_vendor_request_set_vga_gain( const usb_transfer_stage_t stage) { if (stage == USB_TRANSFER_STAGE_SETUP) { - uint8_t value; - value = max283x_set_vga_gain(&max283x, endpoint->setup.index); + radio_gain_t gain = {.db = endpoint->setup.index}; + uint8_t value = + radio_set_gain(&radio, RADIO_CHANNEL0, RADIO_GAIN_RX_VGA, gain); endpoint->buffer[0] = value; if (value) { hackrf_ui()->set_bb_vga_gain(endpoint->setup.index); @@ -211,8 +268,9 @@ usb_request_status_t usb_vendor_request_set_txvga_gain( const usb_transfer_stage_t stage) { if (stage == USB_TRANSFER_STAGE_SETUP) { - uint8_t value; - value = max283x_set_txvga_gain(&max283x, endpoint->setup.index); + radio_gain_t gain = {.db = endpoint->setup.index}; + uint8_t value = + radio_set_gain(&radio, RADIO_CHANNEL0, RADIO_GAIN_TX_VGA, gain); endpoint->buffer[0] = value; if (value) { hackrf_ui()->set_bb_tx_vga_gain(endpoint->setup.index); @@ -233,14 +291,25 @@ usb_request_status_t usb_vendor_request_set_antenna_enable( usb_endpoint_t* const endpoint, const usb_transfer_stage_t stage) { + radio_antenna_t off = {.enable = false}; + radio_antenna_t on = {.enable = true}; + if (stage == USB_TRANSFER_STAGE_SETUP) { switch (endpoint->setup.value) { case 0: - rf_path_set_antenna(&rf_path, 0); + radio_set_antenna( + &radio, + RADIO_CHANNEL0, + RADIO_ANTENNA_BIAS_TEE, + off); usb_transfer_schedule_ack(endpoint->in); return USB_REQUEST_STATUS_OK; case 1: - rf_path_set_antenna(&rf_path, 1); + radio_set_antenna( + &radio, + RADIO_CHANNEL0, + RADIO_ANTENNA_BIAS_TEE, + on); usb_transfer_schedule_ack(endpoint->in); return USB_REQUEST_STATUS_OK; default: @@ -251,41 +320,9 @@ usb_request_status_t usb_vendor_request_set_antenna_enable( } } -usb_request_status_t usb_vendor_request_set_freq_explicit( - usb_endpoint_t* const endpoint, - const usb_transfer_stage_t stage) -{ - if (stage == USB_TRANSFER_STAGE_SETUP) { - usb_transfer_schedule_block( - endpoint->out, - &explicit_params, - sizeof(struct set_freq_explicit_params), - NULL, - NULL); - return USB_REQUEST_STATUS_OK; - } else if (stage == USB_TRANSFER_STAGE_DATA) { - if (set_freq_explicit( - explicit_params.if_freq_hz, - explicit_params.lo_freq_hz, - explicit_params.path)) { - usb_transfer_schedule_ack(endpoint->in); - return USB_REQUEST_STATUS_OK; - } - return USB_REQUEST_STATUS_STALL; - } else { - return USB_REQUEST_STATUS_OK; - } -} - -static volatile hw_sync_mode_t _hw_sync_mode = HW_SYNC_MODE_OFF; static volatile uint32_t _tx_underrun_limit; static volatile uint32_t _rx_overrun_limit; -void set_hw_sync_mode(const hw_sync_mode_t new_hw_sync_mode) -{ - _hw_sync_mode = new_hw_sync_mode; -} - volatile transceiver_request_t transceiver_request = { .mode = TRANSCEIVER_MODE_OFF, .seq = 0, @@ -311,12 +348,13 @@ void transceiver_shutdown(void) led_off(LED2); led_off(LED3); - rf_path_set_direction(&rf_path, RF_PATH_DIRECTION_OFF); + radio_switch_mode(&radio, RADIO_CHANNEL0, TRANSCEIVER_MODE_OFF); m0_set_mode(M0_MODE_IDLE); } void transceiver_startup(const transceiver_mode_t mode) { + radio_switch_mode(&radio, RADIO_CHANNEL0, mode); hackrf_ui()->set_transceiver_mode(mode); switch (mode) { @@ -324,14 +362,12 @@ void transceiver_startup(const transceiver_mode_t mode) case TRANSCEIVER_MODE_RX: led_off(LED3); led_on(LED2); - rf_path_set_direction(&rf_path, RF_PATH_DIRECTION_RX); m0_set_mode(M0_MODE_RX); m0_state.shortfall_limit = _rx_overrun_limit; break; case TRANSCEIVER_MODE_TX: led_off(LED2); led_on(LED3); - rf_path_set_direction(&rf_path, RF_PATH_DIRECTION_TX); m0_set_mode(M0_MODE_TX_START); m0_state.shortfall_limit = _tx_underrun_limit; break; @@ -340,7 +376,8 @@ void transceiver_startup(const transceiver_mode_t mode) } activate_best_clock_source(); - hw_sync_enable(_hw_sync_mode); + hw_sync_mode_t trigger_mode = radio_get_trigger_mode(&radio, RADIO_CHANNEL0); + hw_sync_enable(trigger_mode); } usb_request_status_t usb_vendor_request_set_transceiver_mode( @@ -370,9 +407,15 @@ usb_request_status_t usb_vendor_request_set_hw_sync_mode( const usb_transfer_stage_t stage) { if (stage == USB_TRANSFER_STAGE_SETUP) { - set_hw_sync_mode(endpoint->setup.value); - usb_transfer_schedule_ack(endpoint->in); - return USB_REQUEST_STATUS_OK; + radio_error_t result = radio_set_trigger_mode( + &radio, + RADIO_CHANNEL0, + endpoint->setup.value); + if (result == RADIO_OK) { + usb_transfer_schedule_ack(endpoint->in); + return USB_REQUEST_STATUS_OK; + } + return USB_REQUEST_STATUS_STALL; } else { return USB_REQUEST_STATUS_OK; } diff --git a/firmware/hackrf_usb/usb_api_transceiver.h b/firmware/hackrf_usb/usb_api_transceiver.h index 3c9db650..9fb54c2e 100644 --- a/firmware/hackrf_usb/usb_api_transceiver.h +++ b/firmware/hackrf_usb/usb_api_transceiver.h @@ -79,7 +79,6 @@ usb_request_status_t usb_vendor_request_set_rx_overrun_limit( void request_transceiver_mode(transceiver_mode_t mode); void transceiver_startup(transceiver_mode_t mode); void transceiver_shutdown(void); -void start_streaming_on_hw_sync(); void rx_mode(uint32_t seq); void tx_mode(uint32_t seq); void off_mode(uint32_t seq); diff --git a/firmware/hackrf_usb/usb_descriptor.c b/firmware/hackrf_usb/usb_descriptor.c index ed388859..7f260b82 100644 --- a/firmware/hackrf_usb/usb_descriptor.c +++ b/firmware/hackrf_usb/usb_descriptor.c @@ -27,7 +27,7 @@ #define USB_VENDOR_ID (0x1D50) -#ifdef HACKRF_ONE +#if (defined HACKRF_ONE || defined PRALINE) #define USB_PRODUCT_ID (0x6089) #elif JAWBREAKER #define USB_PRODUCT_ID (0x604B) @@ -37,7 +37,7 @@ #define USB_PRODUCT_ID (0xFFFF) #endif -#define USB_API_VERSION (0x0108) +#define USB_API_VERSION (0x0109) #define USB_WORD(x) (x & 0xFF), ((x >> 8) & 0xFF) @@ -226,6 +226,16 @@ uint8_t usb_descriptor_string_product[] = { 'd', 0x00, '1', 0x00, 'o', 0x00, +#elif PRALINE + 16, // bLength + USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType + 'P', 0x00, + 'r', 0x00, + 'a', 0x00, + 'l', 0x00, + 'i', 0x00, + 'n', 0x00, + 'e', 0x00, #else 14, // bLength USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType diff --git a/host/hackrf-tools/src/hackrf_clock.c b/host/hackrf-tools/src/hackrf_clock.c index 9d293af4..82d0d25d 100644 --- a/host/hackrf-tools/src/hackrf_clock.c +++ b/host/hackrf-tools/src/hackrf_clock.c @@ -59,6 +59,46 @@ int parse_int(char* s, uint8_t* const value) } } +int parse_p1_ctrl_signal(char* s, enum p1_ctrl_signal* const signal) +{ + if (strcasecmp("trigger_in", s) == 0) { + *signal = P1_SIGNAL_TRIGGER_IN; + } else if (strcasecmp("aux_clk1", s) == 0) { + *signal = P1_SIGNAL_AUX_CLK1; + } else if (strcasecmp("clkin", s) == 0) { + *signal = P1_SIGNAL_CLKIN; + } else if (strcasecmp("trigger_out", s) == 0) { + *signal = P1_SIGNAL_TRIGGER_OUT; + } else if (strcasecmp("p22_clkin", s) == 0) { + *signal = P1_SIGNAL_P22_CLKIN; + } else if (strcasecmp("pps_out", s) == 0) { + *signal = P1_SIGNAL_P2_5; + } else if (strcasecmp("off", s) == 0) { + *signal = P1_SIGNAL_NC; + } else if (strcasecmp("aux_clk2", s) == 0) { + *signal = P1_SIGNAL_AUX_CLK2; + } else { + fprintf(stderr, "Invalid signal '%s'\n", s); + return HACKRF_ERROR_INVALID_PARAM; + } + return HACKRF_SUCCESS; +} + +int parse_p2_ctrl_signal(char* s, enum p2_ctrl_signal* const signal) +{ + if (strcasecmp("clkout", s) == 0) { + *signal = P2_SIGNAL_CLK3; + } else if (strcasecmp("trigger_in", s) == 0) { + *signal = P2_SIGNAL_TRIGGER_IN; + } else if (strcasecmp("trigger_out", s) == 0) { + *signal = P2_SIGNAL_TRIGGER_OUT; + } else { + fprintf(stderr, "Invalid signal '%s'\n", s); + return HACKRF_ERROR_INVALID_PARAM; + } + return HACKRF_SUCCESS; +} + int si5351c_read_register(hackrf_device* device, const uint16_t register_number) { uint16_t register_value; @@ -247,6 +287,10 @@ static void usage() printf("\t-a, --all: read settings for all clocks\n"); printf("\t-i, --clkin: get CLKIN status\n"); printf("\t-o, --clkout : enable/disable CLKOUT\n"); + printf("\t-1, --p1 : select the HackRF Pro P1 SMA connector signal (default: clkin)\n"); + printf("\tone of: clkin, trigger_in, trigger_out, p22_clkin, pps_out, aux_clk1, aux_clk2, off\n"); + printf("\t-2, --p2 : select the signal for the HackRF Pro P2 SMA connector (default: clkout)\n"); + printf("\tone of: clkout, trigger_in, trigger_out\n"); printf("\t-d, --device : Serial number of desired HackRF.\n"); printf("\nExamples:\n"); printf("\thackrf_clock -r 3 : prints settings for CLKOUT\n"); @@ -258,6 +302,8 @@ static struct option long_options[] = { {"all", no_argument, 0, 'a'}, {"clkin", required_argument, 0, 'i'}, {"clkout", required_argument, 0, 'o'}, + {"p1", required_argument, 0, '1'}, + {"p2", required_argument, 0, '2'}, {"device", required_argument, 0, 'd'}, {0, 0, 0, 0}, }; @@ -272,6 +318,10 @@ int main(int argc, char** argv) bool clkin = false; uint8_t clkout_enable; uint8_t clkin_status; + bool p1_ctrl = false; + bool p2_ctrl = false; + enum p1_ctrl_signal p1_signal = P1_SIGNAL_CLKIN; + enum p2_ctrl_signal p2_signal = P2_SIGNAL_CLK3; const char* serial_number = NULL; int result = hackrf_init(); @@ -282,8 +332,12 @@ int main(int argc, char** argv) return EXIT_FAILURE; } - while ((opt = getopt_long(argc, argv, "r:aio:d:h?", long_options, &option_index)) != - EOF) { + while ((opt = getopt_long( + argc, + argv, + "r:aio:1:2:d:h?", + long_options, + &option_index)) != EOF) { switch (opt) { case 'r': read = true; @@ -303,6 +357,16 @@ int main(int argc, char** argv) result = parse_int(optarg, &clkout_enable); break; + case '1': + p1_ctrl = true; + result = parse_p1_ctrl_signal(optarg, &p1_signal); + break; + + case '2': + p2_ctrl = true; + result = parse_p2_ctrl_signal(optarg, &p2_signal); + break; + case 'd': serial_number = optarg; break; @@ -326,7 +390,7 @@ int main(int argc, char** argv) } } - if (!clkin && !clkout && !read) { + if (!clkin && !clkout && !read && !p1_ctrl && !p2_ctrl) { fprintf(stderr, "An operation must be specified.\n"); usage(); return EXIT_FAILURE; @@ -372,6 +436,26 @@ int main(int argc, char** argv) } } + if (p1_ctrl) { + result = hackrf_set_p1_ctrl(device, p1_signal); + if (result) { + printf("hackrf_set_p1_ctrl() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + } + + if (p2_ctrl) { + result = hackrf_set_p2_ctrl(device, p2_signal); + if (result) { + printf("hackrf_set_p2_ctrl() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + } + result = hackrf_close(device); if (result) { printf("hackrf_close() failed: %s (%d)\n", diff --git a/host/hackrf-tools/src/hackrf_debug.c b/host/hackrf-tools/src/hackrf_debug.c index 73f98090..b95b2478 100644 --- a/host/hackrf-tools/src/hackrf_debug.c +++ b/host/hackrf-tools/src/hackrf_debug.c @@ -32,6 +32,15 @@ #define REGISTER_INVALID 32767 +enum parts { + PART_NONE = 0, + PART_MAX2837 = 1, + PART_SI5351C = 2, + PART_RFFC5072 = 3, + PART_MAX2831 = 4, + PART_GATEWARE = 5, +}; + int parse_int(char* s, uint32_t* const value) { uint_fast8_t base = 10; @@ -60,11 +69,30 @@ int parse_int(char* s, uint32_t* const value) } } -int max2837_read_register(hackrf_device* device, const uint16_t register_number) +int max283x_read_register( + hackrf_device* device, + const uint16_t register_number, + uint8_t part) { uint16_t register_value; - int result = - hackrf_max2837_read(device, (uint8_t) register_number, ®ister_value); + int result = HACKRF_SUCCESS; + + switch (part) { + case PART_MAX2837: + result = hackrf_max2837_read( + device, + (uint8_t) register_number, + ®ister_value); + break; + case PART_MAX2831: + result = hackrf_max2831_read( + device, + (uint8_t) register_number, + ®ister_value); + break; + default: + return HACKRF_ERROR_INVALID_PARAM; + } if (result == HACKRF_SUCCESS) { printf("[%2d] -> 0x%03x\n", register_number, register_value); @@ -76,13 +104,25 @@ int max2837_read_register(hackrf_device* device, const uint16_t register_number) return result; } -int max2837_read_registers(hackrf_device* device) +int max283x_read_registers(hackrf_device* device, uint8_t part) { uint16_t register_number; + uint16_t register_count; int result = HACKRF_SUCCESS; - for (register_number = 0; register_number < 32; register_number++) { - result = max2837_read_register(device, register_number); + switch (part) { + case PART_MAX2837: + register_count = 32; + break; + case PART_MAX2831: + register_count = 16; + break; + default: + return HACKRF_ERROR_INVALID_PARAM; + } + + for (register_number = 0; register_number < register_count; register_number++) { + result = max283x_read_register(device, register_number, part); if (result != HACKRF_SUCCESS) { break; } @@ -90,13 +130,30 @@ int max2837_read_registers(hackrf_device* device) return result; } -int max2837_write_register( +int max283x_write_register( hackrf_device* device, const uint16_t register_number, - const uint16_t register_value) + const uint16_t register_value, + uint8_t part) { int result = HACKRF_SUCCESS; - result = hackrf_max2837_write(device, (uint8_t) register_number, register_value); + + switch (part) { + case PART_MAX2837: + result = hackrf_max2837_write( + device, + (uint8_t) register_number, + register_value); + break; + case PART_MAX2831: + result = hackrf_max2831_write( + device, + (uint8_t) register_number, + register_value); + break; + default: + return HACKRF_ERROR_INVALID_PARAM; + } if (result == HACKRF_SUCCESS) { printf("0x%03x -> [%2d]\n", register_value, register_number); @@ -150,7 +207,7 @@ int si5351c_write_register( if (result == HACKRF_SUCCESS) { printf("0x%2x -> [%3d]\n", register_value, register_number); } else { - printf("hackrf_max2837_write() failed: %s (%d)\n", + printf("hackrf_si5351c_write() failed: %s (%d)\n", hackrf_error_name(result), result); } @@ -360,22 +417,54 @@ int rffc5072_write_register( return result; } -enum parts { - PART_NONE = 0, - PART_MAX2837 = 1, - PART_SI5351C = 2, - PART_RFFC5072 = 3, -}; +int fpga_spi_read_register(hackrf_device* device, const uint16_t register_number) +{ + uint8_t register_value; + int result = + hackrf_fpga_spi_read(device, (uint8_t) register_number, ®ister_value); + + if (result == HACKRF_SUCCESS) { + printf("[%2d] -> 0x%02x\n", register_number, register_value); + } else { + printf("hackrf_fpga_spi_read() failed: %s (%d)\n", + hackrf_error_name(result), + result); + } + + return result; +} + +int fpga_spi_write_register( + hackrf_device* device, + const uint16_t register_number, + const uint16_t register_value) +{ + int result = HACKRF_SUCCESS; + result = hackrf_fpga_spi_write(device, (uint8_t) register_number, register_value); + + if (result == HACKRF_SUCCESS) { + printf("0x%02x -> [%2d]\n", register_value, register_number); + } else { + printf("hackrf_fpga_spi_write() failed: %s (%d)\n", + hackrf_error_name(result), + result); + } + + return result; +} int read_register(hackrf_device* device, uint8_t part, const uint16_t register_number) { switch (part) { case PART_MAX2837: - return max2837_read_register(device, register_number); + case PART_MAX2831: + return max283x_read_register(device, register_number, part); case PART_SI5351C: return si5351c_read_register(device, register_number); case PART_RFFC5072: return rffc5072_read_register(device, register_number); + case PART_GATEWARE: + return fpga_spi_read_register(device, register_number); } return HACKRF_ERROR_INVALID_PARAM; } @@ -384,7 +473,8 @@ int read_registers(hackrf_device* device, uint8_t part) { switch (part) { case PART_MAX2837: - return max2837_read_registers(device); + case PART_MAX2831: + return max283x_read_registers(device, part); case PART_SI5351C: return si5351c_read_registers(device); case PART_RFFC5072: @@ -401,11 +491,18 @@ int write_register( { switch (part) { case PART_MAX2837: - return max2837_write_register(device, register_number, register_value); + case PART_MAX2831: + return max283x_write_register( + device, + register_number, + register_value, + part); case PART_SI5351C: return si5351c_write_register(device, register_number, register_value); case PART_RFFC5072: return rffc5072_write_register(device, register_number, register_value); + case PART_GATEWARE: + return fpga_spi_write_register(device, register_number, register_value); } return HACKRF_ERROR_INVALID_PARAM; } @@ -465,19 +562,26 @@ static void usage() printf("\t-w, --write : write register specified by last -n argument with value \n"); printf("\t-c, --config: print SI5351C multisynth configuration information\n"); printf("\t-d, --device : specify a particular device by serial number\n"); - printf("\t-m, --max2837: target MAX2837\n"); + printf("\t-m, --max283x: target MAX283x\n"); printf("\t-s, --si5351c: target SI5351C\n"); printf("\t-f, --rffc5072: target RFFC5072\n"); + printf("\t-g, --gateware: target gateware registers\n"); + printf("\t-P, --fpga : load the n-th bitstream to the FPGA\n"); + printf("\t-1, --p1 : P1 control\n"); + printf("\t-2, --p2 : P2 control\n"); + printf("\t-C, --clkin <0/1>: CLKIN control (0 for P1_CLKIN, 1 for P22_CLKIN)\n"); + printf("\t-N, --narrowband <0/1>: narrowband filter disable/enable\n"); printf("\t-S, --state: display M0 state\n"); printf("\t-T, --tx-underrun-limit : set TX underrun limit in bytes (0 for no limit)\n"); printf("\t-R, --rx-overrun-limit : set RX overrun limit in bytes (0 for no limit)\n"); printf("\t-u, --ui <1/0>: enable/disable UI\n"); printf("\t-l, --leds : configure LED state (0 for all off, 1 for default)\n"); + printf("\t-t, --selftest: read self-test report\n"); printf("\nExamples:\n"); printf("\thackrf_debug --si5351c -n 0 -r # reads from si5351c register 0\n"); printf("\thackrf_debug --si5351c -c # displays si5351c multisynth configuration\n"); printf("\thackrf_debug --rffc5072 -r # reads all rffc5072 registers\n"); - printf("\thackrf_debug --max2837 -n 10 -w 22 # writes max2837 register 10 with 22 decimal\n"); + printf("\thackrf_debug --max283x -n 10 -w 22 # writes max283x register 10 with 22 decimal\n"); printf("\thackrf_debug --state # displays M0 state\n"); } @@ -489,19 +593,28 @@ static struct option long_options[] = { {"device", required_argument, 0, 'd'}, {"help", no_argument, 0, 'h'}, {"max2837", no_argument, 0, 'm'}, + {"max283x", no_argument, 0, 'm'}, {"si5351c", no_argument, 0, 's'}, {"rffc5072", no_argument, 0, 'f'}, + {"gateware", no_argument, 0, 'g'}, + {"fpga", required_argument, 0, 'P'}, + {"p1", required_argument, 0, '1'}, + {"p2", required_argument, 0, '2'}, + {"clkin", required_argument, 0, 'C'}, + {"narrowband", required_argument, 0, 'N'}, {"state", no_argument, 0, 'S'}, {"tx-underrun-limit", required_argument, 0, 'T'}, {"rx-overrun-limit", required_argument, 0, 'R'}, {"ui", required_argument, 0, 'u'}, {"leds", required_argument, 0, 'l'}, + {"selftest", no_argument, 0, 't'}, {0, 0, 0, 0}, }; int main(int argc, char** argv) { int opt; + uint8_t board_id = BOARD_ID_UNDETECTED; uint32_t register_number = REGISTER_INVALID; uint32_t register_value; hackrf_device* device = NULL; @@ -518,8 +631,19 @@ int main(int argc, char** argv) uint32_t led_state; uint32_t tx_limit; uint32_t rx_limit; + uint32_t p1_state; + uint32_t p2_state; + uint32_t clkin_state; + uint32_t narrowband_state; + uint32_t bitstream_index; bool set_tx_limit = false; bool set_rx_limit = false; + bool set_p1 = false; + bool set_p2 = false; + bool set_clkin = false; + bool set_narrowband = false; + bool set_fpga_bitstream = false; + bool read_selftest = false; int result = hackrf_init(); if (result) { @@ -532,7 +656,7 @@ int main(int argc, char** argv) while ((opt = getopt_long( argc, argv, - "n:rw:d:cmsfST:R:h?u:l:", + "n:rw:d:cmsfg1:2:C:N:P:ST:R:h?u:l:t", long_options, &option_index)) != EOF) { switch (opt) { @@ -594,6 +718,39 @@ int main(int argc, char** argv) part = PART_RFFC5072; break; + case 'g': + if (part != PART_NONE) { + fprintf(stderr, "Only one part can be specified.'\n"); + return EXIT_FAILURE; + } + part = PART_GATEWARE; + break; + + case '1': + set_p1 = true; + result = parse_int(optarg, &p1_state); + break; + + case '2': + set_p2 = true; + result = parse_int(optarg, &p2_state); + break; + + case 'C': + set_clkin = true; + result = parse_int(optarg, &clkin_state); + break; + + case 'N': + set_narrowband = true; + result = parse_int(optarg, &narrowband_state); + break; + + case 'P': + set_fpga_bitstream = true; + result = parse_int(optarg, &bitstream_index); + break; + case 'u': set_ui = true; result = parse_int(optarg, &ui_enable); @@ -603,6 +760,9 @@ int main(int argc, char** argv) set_leds = true; result = parse_int(optarg, &led_state); break; + case 't': + read_selftest = true; + break; case 'h': case '?': @@ -642,14 +802,16 @@ int main(int argc, char** argv) } if (!(write || read || dump_config || dump_state || set_tx_limit || - set_rx_limit || set_ui || set_leds)) { + set_rx_limit || set_ui || set_leds || set_p1 || set_p2 || set_clkin || + set_narrowband || set_fpga_bitstream || read_selftest)) { fprintf(stderr, "Specify read, write, or config option.\n"); usage(); return EXIT_FAILURE; } if (part == PART_NONE && !set_ui && !dump_state && !set_tx_limit && - !set_rx_limit && !set_leds) { + !set_rx_limit && !set_leds && !set_p1 && !set_p2 && !set_clkin && + !set_narrowband && !set_fpga_bitstream && !read_selftest) { fprintf(stderr, "Specify a part to read, write, or print config from.\n"); usage(); return EXIT_FAILURE; @@ -663,6 +825,20 @@ int main(int argc, char** argv) return EXIT_FAILURE; } + if (part == PART_MAX2837) { + result = hackrf_board_id_read(device, &board_id); + if (result != HACKRF_SUCCESS) { + fprintf(stderr, + "hackrf_board_id_read() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + if (board_id == BOARD_ID_PRALINE) { + part = PART_MAX2831; + } + } + if (write) { result = write_register(device, part, register_number, register_value); } @@ -699,6 +875,56 @@ int main(int argc, char** argv) } } + if (set_p1) { + result = hackrf_set_p1_ctrl(device, p1_state); + if (result != HACKRF_SUCCESS) { + printf("hackrf_set_p1_ctrl() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + } + + if (set_p2) { + result = hackrf_set_p2_ctrl(device, p2_state); + if (result != HACKRF_SUCCESS) { + printf("hackrf_set_p2_ctrl() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + } + + if (set_clkin) { + result = hackrf_set_clkin_ctrl(device, clkin_state); + if (result != HACKRF_SUCCESS) { + printf("hackrf_set_clkin_ctrl() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + } + + if (set_narrowband) { + result = hackrf_set_narrowband_filter(device, narrowband_state); + if (result != HACKRF_SUCCESS) { + printf("hackrf_set_narrowband_filter() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + } + + if (set_fpga_bitstream) { + result = hackrf_set_fpga_bitstream(device, bitstream_index); + if (result != HACKRF_SUCCESS) { + printf("hackrf_set_fpga_bitstream() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + } + if (dump_state) { hackrf_m0_state state; result = hackrf_get_m0_state(device, &state); @@ -725,6 +951,19 @@ int main(int argc, char** argv) result = hackrf_set_leds(device, led_state); } + if (read_selftest) { + hackrf_selftest selftest; + result = hackrf_read_selftest(device, &selftest); + if (result != HACKRF_SUCCESS) { + printf("hackrf_read_selftest() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + printf("Self-test result: %s\n", selftest.pass ? "PASS" : "FAIL"); + printf("%s", selftest.msg); + } + result = hackrf_close(device); if (result) { printf("hackrf_close() failed: %s (%d)\n", diff --git a/host/hackrf-tools/src/hackrf_info.c b/host/hackrf-tools/src/hackrf_info.c index 68c95938..c8834326 100644 --- a/host/hackrf-tools/src/hackrf_info.c +++ b/host/hackrf-tools/src/hackrf_info.c @@ -46,7 +46,7 @@ void print_board_rev(uint8_t board_rev) } } -void print_supported_platform(uint32_t platform, uint8_t board_id) +void print_supported_platform(uint32_t platform, uint8_t board_id, uint8_t board_rev) { printf("Hardware supported by installed firmware:\n"); if (platform & HACKRF_PLATFORM_JAWBREAKER) { @@ -59,6 +59,13 @@ void print_supported_platform(uint32_t platform, uint8_t board_id) (platform & HACKRF_PLATFORM_HACKRF1_R9)) { printf(" HackRF One\n"); } + if (platform & HACKRF_PLATFORM_PRALINE) { + if (board_rev & HACKRF_BOARD_REV_GSG) { + printf(" HackRF Pro\n"); + } else { + printf(" Praline\n"); + } + } switch (board_id) { case BOARD_ID_HACKRF1_OG: if (!(platform & HACKRF_PLATFORM_HACKRF1_OG)) { @@ -79,6 +86,11 @@ void print_supported_platform(uint32_t platform, uint8_t board_id) break; } printf("Error: Firmware does not support hardware platform.\n"); + case BOARD_ID_PRALINE: + if (platform & HACKRF_PLATFORM_PRALINE) { + break; + } + printf("Error: Firmware does not support hardware platform.\n"); } } @@ -188,7 +200,8 @@ int main(void) read_partid_serialno.part_id[0], read_partid_serialno.part_id[1]); - if ((usb_version >= 0x0106) && ((board_id == 2) || (board_id == 4))) { + if ((usb_version >= 0x0106) && + ((board_id == 2) || (board_id == 4) || (board_id == 5))) { result = hackrf_board_rev_read(device, &board_rev); if (result != HACKRF_SUCCESS) { fprintf(stderr, @@ -210,7 +223,7 @@ int main(void) result); return EXIT_FAILURE; } - print_supported_platform(supported_platform, board_id); + print_supported_platform(supported_platform, board_id, board_rev); } result = hackrf_get_operacake_boards(device, &operacakes[0]); @@ -247,6 +260,21 @@ int main(void) } #endif /* HACKRF_ISSUE_609_IS_FIXED */ + if (usb_version >= 0x0109) { + hackrf_selftest selftest; + result = hackrf_read_selftest(device, &selftest); + if (result != HACKRF_SUCCESS) { + printf("hackrf_read_selftest() failed: %s (%d)\n", + hackrf_error_name(result), + result); + return EXIT_FAILURE; + } + if (!selftest.pass) { + printf("Self-test FAIL:\n"); + printf("%s", selftest.msg); + } + } + result = hackrf_close(device); if (result != HACKRF_SUCCESS) { fprintf(stderr, diff --git a/host/hackrf-tools/src/hackrf_sweep.c b/host/hackrf-tools/src/hackrf_sweep.c index 4e0e5ec1..38efa223 100644 --- a/host/hackrf-tools/src/hackrf_sweep.c +++ b/host/hackrf-tools/src/hackrf_sweep.c @@ -96,7 +96,6 @@ int gettimeofday(struct timeval* tv, void* ignored) #define OFFSET 7500000 #define BLOCKS_PER_TRANSFER 16 -#define THROWAWAY_BLOCKS 2 #if defined _WIN32 #define m_sleep(a) Sleep((a)) diff --git a/host/libhackrf/src/hackrf.c b/host/libhackrf/src/hackrf.c index ae19ef19..eab9b0e0 100644 --- a/host/libhackrf/src/hackrf.c +++ b/host/libhackrf/src/hackrf.c @@ -36,6 +36,7 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSI /* Avoid redefinition of timespec from time.h (included by libusb.h) */ #define HAVE_STRUCT_TIMESPEC 1 #define strdup _strdup + #define strcasecmp _stricmp #endif #include @@ -55,8 +56,8 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSI // the same values. typedef enum { HACKRF_VENDOR_REQUEST_SET_TRANSCEIVER_MODE = 1, - HACKRF_VENDOR_REQUEST_MAX2837_WRITE = 2, - HACKRF_VENDOR_REQUEST_MAX2837_READ = 3, + HACKRF_VENDOR_REQUEST_MAX283X_WRITE = 2, + HACKRF_VENDOR_REQUEST_MAX283X_READ = 3, HACKRF_VENDOR_REQUEST_SI5351C_WRITE = 4, HACKRF_VENDOR_REQUEST_SI5351C_READ = 5, HACKRF_VENDOR_REQUEST_SAMPLE_RATE_SET = 6, @@ -100,6 +101,14 @@ typedef enum { HACKRF_VENDOR_REQUEST_SUPPORTED_PLATFORM_READ = 46, HACKRF_VENDOR_REQUEST_SET_LEDS = 47, HACKRF_VENDOR_REQUEST_SET_USER_BIAS_T_OPTS = 48, + HACKRF_VENDOR_REQUEST_FPGA_SPI_WRITE = 49, + HACKRF_VENDOR_REQUEST_FPGA_SPI_READ = 50, + HACKRF_VENDOR_REQUEST_P2_CTRL = 51, + HACKRF_VENDOR_REQUEST_P1_CTRL = 52, + HACKRF_VENDOR_REQUEST_SET_NARROWBAND_FILTER = 53, + HACKRF_VENDOR_REQUEST_SET_FPGA_BITSTREAM = 54, + HACKRF_VENDOR_REQUEST_CLKIN_CTRL = 55, + HACKRF_VENDOR_REQUEST_READ_SELFTEST = 56, } hackrf_vendor_request; #define USB_CONFIG_STANDARD 0x1 @@ -900,7 +909,36 @@ int ADDCALL hackrf_max2837_read( result = libusb_control_transfer( device->usb_device, LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, - HACKRF_VENDOR_REQUEST_MAX2837_READ, + HACKRF_VENDOR_REQUEST_MAX283X_READ, + 0, + register_number, + (unsigned char*) value, + 2, + 0); + + if (result < 2) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + +int ADDCALL hackrf_max2831_read( + hackrf_device* device, + uint8_t register_number, + uint16_t* value) +{ + int result; + + if (register_number >= 16) { + return HACKRF_ERROR_INVALID_PARAM; + } + + result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_MAX283X_READ, 0, register_number, (unsigned char*) value, @@ -933,7 +971,40 @@ int ADDCALL hackrf_max2837_write( device->usb_device, LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, - HACKRF_VENDOR_REQUEST_MAX2837_WRITE, + HACKRF_VENDOR_REQUEST_MAX283X_WRITE, + value, + register_number, + NULL, + 0, + 0); + + if (result != 0) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + +int ADDCALL hackrf_max2831_write( + hackrf_device* device, + uint8_t register_number, + uint16_t value) +{ + int result; + + if (register_number >= 16) { + return HACKRF_ERROR_INVALID_PARAM; + } + if (value >= 0x4000) { + return HACKRF_ERROR_INVALID_PARAM; + } + + result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_MAX283X_WRITE, value, register_number, NULL, @@ -1096,6 +1167,83 @@ int ADDCALL hackrf_rffc5071_write( } } +int ADDCALL hackrf_fpga_spi_read( + hackrf_device* device, + uint8_t register_number, + uint8_t* value) +{ + USB_API_REQUIRED(device, 0x0109); + int result; + + result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_FPGA_SPI_READ, + 0, + register_number, + (unsigned char*) value, + 1, + 0); + + if (result < 1) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + +int ADDCALL hackrf_fpga_spi_write( + hackrf_device* device, + uint8_t register_number, + uint8_t value) +{ + USB_API_REQUIRED(device, 0x0109); + int result; + + result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_FPGA_SPI_WRITE, + value, + register_number, + NULL, + 0, + 0); + + if (result != 0) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + +int ADDCALL hackrf_read_selftest(hackrf_device* device, hackrf_selftest* selftest) +{ + USB_API_REQUIRED(device, 0x0109); + + int result; + + result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_READ_SELFTEST, + 0, + 0, + (unsigned char*) selftest, + sizeof(hackrf_selftest), + 0); + + if (result < sizeof(hackrf_selftest)) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + int ADDCALL hackrf_get_m0_state(hackrf_device* device, hackrf_m0_state* state) { USB_API_REQUIRED(device, 0x0106) @@ -2211,6 +2359,9 @@ const char* ADDCALL hackrf_board_id_name(enum hackrf_board_id board_id) case BOARD_ID_HACKRF1_R9: return "HackRF One"; + case BOARD_ID_PRALINE: + return "HackRF Pro"; + case BOARD_ID_UNRECOGNIZED: return "unrecognized"; @@ -2237,6 +2388,9 @@ extern ADDAPI uint32_t ADDCALL hackrf_board_id_platform(enum hackrf_board_id boa case BOARD_ID_HACKRF1_R9: return HACKRF_PLATFORM_HACKRF1_R9; + case BOARD_ID_PRALINE: + return HACKRF_PLATFORM_PRALINE; + default: return 0; } @@ -2919,6 +3073,30 @@ extern ADDAPI const char* ADDCALL hackrf_board_rev_name(enum hackrf_board_rev bo case BOARD_REV_GSG_HACKRF1_R10: return "r10"; + case BOARD_REV_PRALINE_R0_1: + case BOARD_REV_GSG_PRALINE_R0_1: + return "r0.1"; + + case BOARD_REV_PRALINE_R0_2: + case BOARD_REV_GSG_PRALINE_R0_2: + return "r0.2"; + + case BOARD_REV_PRALINE_R0_3: + case BOARD_REV_GSG_PRALINE_R0_3: + return "r0.3"; + + case BOARD_REV_PRALINE_R1_0: + case BOARD_REV_GSG_PRALINE_R1_0: + return "r1.0"; + + case BOARD_REV_PRALINE_R1_1: + case BOARD_REV_GSG_PRALINE_R1_1: + return "r1.1"; + + case BOARD_REV_PRALINE_R1_2: + case BOARD_REV_GSG_PRALINE_R1_2: + return "r1.2"; + case BOARD_ID_UNRECOGNIZED: return "unrecognized"; @@ -3022,6 +3200,123 @@ int ADDCALL hackrf_set_user_bias_t_opts( } } +int ADDCALL hackrf_set_p1_ctrl(hackrf_device* device, const enum p1_ctrl_signal signal) +{ + USB_API_REQUIRED(device, 0x0109); + + int result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_P1_CTRL, + signal, + 0, + NULL, + 0, + 0); + + if (result != 0) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + +int ADDCALL hackrf_set_p2_ctrl(hackrf_device* device, const enum p2_ctrl_signal signal) +{ + USB_API_REQUIRED(device, 0x0109); + + int result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_P2_CTRL, + signal, + 0, + NULL, + 0, + 0); + + if (result != 0) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + +int ADDCALL hackrf_set_clkin_ctrl( + hackrf_device* device, + const enum clkin_ctrl_signal signal) +{ + USB_API_REQUIRED(device, 0x0109); + + int result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_CLKIN_CTRL, + signal, + 0, + NULL, + 0, + 0); + + if (result != 0) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + +int ADDCALL hackrf_set_narrowband_filter(hackrf_device* device, const uint8_t value) +{ + USB_API_REQUIRED(device, 0x0109); + + int result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_SET_NARROWBAND_FILTER, + value, + 0, + NULL, + 0, + 0); + + if (result != 0) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + +int ADDCALL hackrf_set_fpga_bitstream(hackrf_device* device, const uint8_t index) +{ + USB_API_REQUIRED(device, 0x0109); + + int result = libusb_control_transfer( + device->usb_device, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + HACKRF_VENDOR_REQUEST_SET_FPGA_BITSTREAM, + index, + 0, + NULL, + 0, + 0); + + if (result != 0) { + last_libusb_error = result; + return HACKRF_ERROR_LIBUSB; + } else { + return HACKRF_SUCCESS; + } +} + #ifdef __cplusplus } // __cplusplus defined. #endif diff --git a/host/libhackrf/src/hackrf.h b/host/libhackrf/src/hackrf.h index faa3d402..0ffa048f 100644 --- a/host/libhackrf/src/hackrf.h +++ b/host/libhackrf/src/hackrf.h @@ -638,6 +638,11 @@ enum hackrf_error { * @ingroup device */ #define HACKRF_PLATFORM_HACKRF1_R9 (1 << 3) +/** + * HACKRF Praline platform bit in result of @ref hackrf_supported_platform_read + * @ingroup device + */ +#define HACKRF_PLATFORM_PRALINE (1 << 4) /** * HACKRF board id enum @@ -666,6 +671,10 @@ enum hackrf_board_id { * HackRF One (rev. 9 & later. 1-6000MHz, 20MSPS, bias-tee) */ BOARD_ID_HACKRF1_R9 = 4, + /** + * Praline + */ + BOARD_ID_PRALINE = 5, /** * Unknown board (failed detection) */ @@ -719,6 +728,31 @@ enum hackrf_board_rev { */ BOARD_REV_HACKRF1_R10 = 5, + /** + * praline board revision 0.1, generic + */ + BOARD_REV_PRALINE_R0_1 = 6, + /** + * praline board revision 0.2, generic + */ + BOARD_REV_PRALINE_R0_2 = 7, + /** + * praline board revision 0.1, generic + */ + BOARD_REV_PRALINE_R0_3 = 8, + /** + * praline board revision 1.0, generic + */ + BOARD_REV_PRALINE_R1_0 = 9, + /** + * praline board revision 1.1, generic + */ + BOARD_REV_PRALINE_R1_1 = 10, + /** + * praline board revision 1.2, generic + */ + BOARD_REV_PRALINE_R1_2 = 11, + /** * board revision 6, made by GSG */ @@ -740,6 +774,31 @@ enum hackrf_board_rev { */ BOARD_REV_GSG_HACKRF1_R10 = 0x85, + /** + * praline board revision 0.1, made by GSG + */ + BOARD_REV_GSG_PRALINE_R0_1 = 0x86, + /** + * praline board revision 0.2, made by GSG + */ + BOARD_REV_GSG_PRALINE_R0_2 = 0x87, + /** + * praline board revision 0.1, made by GSG + */ + BOARD_REV_GSG_PRALINE_R0_3 = 0x88, + /** + * praline board revision 1.0, made by GSG + */ + BOARD_REV_GSG_PRALINE_R1_0 = 0x89, + /** + * praline board revision 1.1, made by GSG + */ + BOARD_REV_GSG_PRALINE_R1_1 = 0x8a, + /** + * praline board revision 1.2, made by GSG + */ + BOARD_REV_GSG_PRALINE_R1_2 = 0x8b, + /** * unknown board revision (detection failed) */ @@ -851,6 +910,43 @@ enum sweep_style { INTERLEAVED = 1, }; +/** + * P1 SMA connector signal. + * + * Used by @ref hackrf_set_p1_ctrl, to select the signal for the P1 SMA connector. + */ +enum p1_ctrl_signal { + P1_SIGNAL_TRIGGER_IN = 0, + P1_SIGNAL_AUX_CLK1 = 1, + P1_SIGNAL_CLKIN = 2, + P1_SIGNAL_TRIGGER_OUT = 3, + P1_SIGNAL_P22_CLKIN = 4, + P1_SIGNAL_P2_5 = 5, + P1_SIGNAL_NC = 6, + P1_SIGNAL_AUX_CLK2 = 7, +}; + +/** + * P2 SMA connector signal. + * + * Used by @ref hackrf_set_p2_ctrl, to select the signal for the P2 SMA connector. + */ +enum p2_ctrl_signal { + P2_SIGNAL_CLK3 = 0, + P2_SIGNAL_TRIGGER_IN = 2, + P2_SIGNAL_TRIGGER_OUT = 3, +}; + +/** + * CLKIN (clock input) signal. + * + * Used by @ref hackrf_set_clkin_ctrl, to select the clock input signal CLKIN. + */ +enum clkin_ctrl_signal { + CLKIN_SIGNAL_P1 = 0, + CLKIN_SIGNAL_P22 = 1, +}; + /** * Opaque struct for hackrf device info. Object can be created via @ref hackrf_open, @ref hackrf_device_list_open or @ref hackrf_open_by_serial and be destroyed via @ref hackrf_close * @ingroup device @@ -979,6 +1075,15 @@ typedef struct { uint32_t error; } hackrf_m0_state; +/** + * Self-test results. + * @ingroup debug + */ +typedef struct { + bool pass; + char msg[511]; +} hackrf_selftest; + /** * List of connected HackRF devices * @@ -1237,6 +1342,18 @@ extern ADDAPI int ADDCALL hackrf_get_m0_state( hackrf_device* device, hackrf_m0_state* value); +/** + * Get the results of the device self-test + * + * @param[in] device device to query + * @param[out] self-test results + * @return @ref HACKRF_SUCCESS on success or @ref hackrf_error variant + * @ingroup debug + */ +extern ADDAPI int ADDCALL hackrf_read_selftest( + hackrf_device* device, + hackrf_selftest* value); + /** * Set transmit underrun limit * @@ -1292,6 +1409,22 @@ extern ADDAPI int ADDCALL hackrf_max2837_read( uint8_t register_number, uint16_t* value); +/** + * Directly read the registers of the MAX2831 transceiver IC + * + * Intended for debugging purposes only! + * + * @param[in] device device to query + * @param[in] register_number register number to read + * @param[out] value value of the specified register + * @return @ref HACKRF_SUCCESS on success or @ref hackrf_error variant + * @ingroup debug + */ +extern ADDAPI int ADDCALL hackrf_max2831_read( + hackrf_device* device, + uint8_t register_number, + uint16_t* value); + /** * Directly write the registers of the MAX2837 transceiver IC * @@ -1308,6 +1441,22 @@ extern ADDAPI int ADDCALL hackrf_max2837_write( uint8_t register_number, uint16_t value); +/** + * Directly write the registers of the MAX2831 transceiver IC + * + * Intended for debugging purposes only! + * + * @param device device to write + * @param register_number register number to write + * @param value value to write in the specified register + * @return @ref HACKRF_SUCCESS on success or @ref hackrf_error variant + * @ingroup debug + */ +extern ADDAPI int ADDCALL hackrf_max2831_write( + hackrf_device* device, + uint8_t register_number, + uint16_t value); + /** * Directly read the registers of the Si5351C clock generator IC * @@ -1389,6 +1538,40 @@ extern ADDAPI int ADDCALL hackrf_rffc5071_write( uint8_t register_number, uint16_t value); +/** + * Directly read the registers of the current gateware through the FPGA SPI interface + * (HackRF Pro) + * + * Intended for debugging purposes only! + * + * @param[in] device device to query + * @param[in] register_number register number to read + * @param[out] value value of the specified register + * @return @ref HACKRF_SUCCESS on success or @ref hackrf_error variant + * @ingroup debug + */ +extern ADDAPI int ADDCALL hackrf_fpga_spi_read( + hackrf_device* device, + uint8_t register_number, + uint8_t* value); + +/** + * Directly write the registers of the current gateware through the FPGA SPI interface + * (HackRF Pro) + * + * Intended for debugging purposes only! + * + * @param[in] device device to write + * @param[in] register_number register number to write + * @param[out] value value to write in the specified register + * @return @ref HACKRF_SUCCESS on success or @ref hackrf_error variant + * @ingroup debug + */ +extern ADDAPI int ADDCALL hackrf_fpga_spi_write( + hackrf_device* device, + uint8_t register_number, + uint8_t value); + /** * Erase firmware image on the SPI flash * @@ -2100,6 +2283,41 @@ extern ADDAPI int ADDCALL hackrf_set_user_bias_t_opts( hackrf_device* device, hackrf_bias_t_user_settting_req* req); +/** + * Select signal for HackRF Pro SMA connector P1. + */ +extern ADDAPI int ADDCALL hackrf_set_p1_ctrl( + hackrf_device* device, + const enum p1_ctrl_signal signal); + +/** + * Select signal for HackRF Pro SMA connector P2. + */ +extern ADDAPI int ADDCALL hackrf_set_p2_ctrl( + hackrf_device* device, + const enum p2_ctrl_signal signal); + +/** + * Select signal for HackRF Pro clock input CLKIN. + */ +extern ADDAPI int ADDCALL hackrf_set_clkin_ctrl( + hackrf_device* device, + const enum clkin_ctrl_signal signal); + +/** + * Enable/disable narrowband filter in HackRF Pro. + */ +extern ADDAPI int ADDCALL hackrf_set_narrowband_filter( + hackrf_device* device, + const uint8_t value); + +/** + * Program the selected FPGA bitstream in HackRF Pro. + */ +extern ADDAPI int ADDCALL hackrf_set_fpga_bitstream( + hackrf_device* device, + const uint8_t index); + #ifdef __cplusplus } // __cplusplus defined. #endif