From 409acbc3c9b4a11d58090b0fc2c2caf925d8a749 Mon Sep 17 00:00:00 2001 From: Michael Ossmann Date: Mon, 24 Nov 2025 20:53:41 -0500 Subject: [PATCH] Add support for HackRF Pro (code name: Praline) Co-authored-by: mndza Co-authored-by: Martin Ling Co-authored-by: Antoine van Gelder --- .github/workflows/build.yml | 2 +- firmware/blinky/blinky.c | 5 + firmware/common/LPC4320_M4_memory.ld | 2 +- firmware/common/LPC43xx_M4_memory_rom_only.ld | 31 + firmware/common/cpld_jtag.c | 22 +- firmware/common/cpld_jtag.h | 6 +- firmware/common/firmware_info.c | 2 + firmware/common/fpga.c | 227 +++++ firmware/common/fpga.h | 31 + firmware/common/hackrf_core.c | 429 ++++++++- firmware/common/hackrf_core.h | 263 +++++- firmware/common/hackrf_ui.c | 2 +- firmware/common/ice40_spi.c | 124 +++ firmware/common/ice40_spi.h | 48 + firmware/common/lz4_blk.c | 86 ++ firmware/common/lz4_blk.h | 30 + firmware/common/m0_state.c | 39 + firmware/common/m0_state.h | 64 ++ firmware/common/max2831.c | 385 ++++++++ firmware/common/max2831.h | 99 +++ firmware/common/max2831_regs.def | 132 +++ firmware/common/max2831_target.c | 101 +++ firmware/common/max2831_target.h | 30 + firmware/common/max2837.c | 37 +- firmware/common/max2837_target.c | 8 +- firmware/common/max2839.c | 39 +- firmware/common/max283x.c | 6 + firmware/common/max2871.c | 7 + firmware/common/max2871_regs.h | 3 +- firmware/common/max5864_target.c | 2 +- firmware/common/mixer.c | 30 +- firmware/common/mixer.h | 2 +- firmware/common/operacake_sctimer.c | 17 +- firmware/common/platform_detect.c | 84 +- firmware/common/platform_detect.h | 14 + firmware/common/radio.c | 460 ++++++++++ firmware/common/radio.h | 264 ++++++ firmware/common/rf_path.c | 120 ++- firmware/common/rf_path.h | 11 + firmware/common/rffc5071.c | 128 ++- firmware/common/rffc5071.h | 6 + firmware/common/rffc5071_regs.def | 1 + firmware/common/rffc5071_spi.c | 4 +- firmware/common/selftest.c | 23 + firmware/common/selftest.h | 55 ++ firmware/common/sgpio.c | 32 +- firmware/common/sgpio.h | 2 + firmware/common/si5351c.c | 34 +- firmware/common/si5351c.h | 3 +- firmware/common/spi_ssp.c | 2 +- firmware/common/spi_ssp.h | 1 + firmware/common/tune_config.h | 220 +++++ firmware/common/tuning.c | 137 ++- firmware/common/tuning.h | 5 +- firmware/common/w25q80bv.c | 9 +- firmware/common/w25q80bv.h | 1 + firmware/fpga/amaranth_future/fixed.py | 357 ++++++++ firmware/fpga/board.py | 90 ++ firmware/fpga/build.py | 68 ++ firmware/fpga/build/praline_fpga.bin | Bin 0 -> 187734 bytes firmware/fpga/dsp/__init__.py | 0 firmware/fpga/dsp/cic.py | 743 ++++++++++++++++ firmware/fpga/dsp/dc_block.py | 141 +++ firmware/fpga/dsp/fir.py | 604 +++++++++++++ firmware/fpga/dsp/fir_mac16.py | 829 ++++++++++++++++++ firmware/fpga/dsp/mcm.py | 156 ++++ firmware/fpga/dsp/nco.py | 103 +++ firmware/fpga/dsp/quarter_shift.py | 55 ++ firmware/fpga/dsp/round.py | 17 + firmware/fpga/dsp/sb_mac16.py | 343 ++++++++ firmware/fpga/interface/__init__.py | 1 + firmware/fpga/interface/max586x.py | 66 ++ firmware/fpga/interface/spi.py | 424 +++++++++ firmware/fpga/requirements.txt | 3 + firmware/fpga/top/ext_precision_rx.py | 215 +++++ firmware/fpga/top/ext_precision_tx.py | 215 +++++ firmware/fpga/top/half_precision.py | 227 +++++ firmware/fpga/top/standard.py | 320 +++++++ firmware/fpga/util/__init__.py | 57 ++ firmware/fpga/util/_stream.py | 189 ++++ firmware/fpga/util/lfsr.py | 59 ++ firmware/hackrf-common.cmake | 37 +- firmware/hackrf_usb/CMakeLists.txt | 42 +- firmware/hackrf_usb/hackrf_usb.c | 54 +- firmware/hackrf_usb/usb_api_m0_state.c | 15 - firmware/hackrf_usb/usb_api_m0_state.h | 45 +- firmware/hackrf_usb/usb_api_praline.c | 87 ++ firmware/hackrf_usb/usb_api_praline.h | 48 + firmware/hackrf_usb/usb_api_register.c | 68 ++ firmware/hackrf_usb/usb_api_register.h | 6 + firmware/hackrf_usb/usb_api_selftest.c | 130 +++ firmware/hackrf_usb/usb_api_selftest.h | 32 + firmware/hackrf_usb/usb_api_sweep.c | 43 +- firmware/hackrf_usb/usb_api_transceiver.c | 151 ++-- firmware/hackrf_usb/usb_api_transceiver.h | 1 - firmware/hackrf_usb/usb_descriptor.c | 14 +- host/hackrf-tools/src/hackrf_clock.c | 90 +- host/hackrf-tools/src/hackrf_debug.c | 287 +++++- host/hackrf-tools/src/hackrf_info.c | 34 +- host/hackrf-tools/src/hackrf_sweep.c | 1 - host/libhackrf/src/hackrf.c | 303 ++++++- host/libhackrf/src/hackrf.h | 218 +++++ 102 files changed, 10548 insertions(+), 367 deletions(-) create mode 100644 firmware/common/LPC43xx_M4_memory_rom_only.ld create mode 100644 firmware/common/fpga.c create mode 100644 firmware/common/fpga.h create mode 100644 firmware/common/ice40_spi.c create mode 100644 firmware/common/ice40_spi.h create mode 100644 firmware/common/lz4_blk.c create mode 100644 firmware/common/lz4_blk.h create mode 100644 firmware/common/m0_state.c create mode 100644 firmware/common/m0_state.h create mode 100644 firmware/common/max2831.c create mode 100644 firmware/common/max2831.h create mode 100644 firmware/common/max2831_regs.def create mode 100644 firmware/common/max2831_target.c create mode 100644 firmware/common/max2831_target.h create mode 100644 firmware/common/radio.c create mode 100644 firmware/common/radio.h create mode 100644 firmware/common/selftest.c create mode 100644 firmware/common/selftest.h create mode 100644 firmware/common/tune_config.h create mode 100644 firmware/fpga/amaranth_future/fixed.py create mode 100644 firmware/fpga/board.py create mode 100644 firmware/fpga/build.py create mode 100644 firmware/fpga/build/praline_fpga.bin create mode 100644 firmware/fpga/dsp/__init__.py create mode 100644 firmware/fpga/dsp/cic.py create mode 100644 firmware/fpga/dsp/dc_block.py create mode 100644 firmware/fpga/dsp/fir.py create mode 100644 firmware/fpga/dsp/fir_mac16.py create mode 100644 firmware/fpga/dsp/mcm.py create mode 100755 firmware/fpga/dsp/nco.py create mode 100644 firmware/fpga/dsp/quarter_shift.py create mode 100644 firmware/fpga/dsp/round.py create mode 100644 firmware/fpga/dsp/sb_mac16.py create mode 100644 firmware/fpga/interface/__init__.py create mode 100644 firmware/fpga/interface/max586x.py create mode 100644 firmware/fpga/interface/spi.py create mode 100644 firmware/fpga/requirements.txt create mode 100644 firmware/fpga/top/ext_precision_rx.py create mode 100644 firmware/fpga/top/ext_precision_tx.py create mode 100644 firmware/fpga/top/half_precision.py create mode 100644 firmware/fpga/top/standard.py create mode 100644 firmware/fpga/util/__init__.py create mode 100644 firmware/fpga/util/_stream.py create mode 100644 firmware/fpga/util/lfsr.py create mode 100644 firmware/hackrf_usb/usb_api_praline.c create mode 100644 firmware/hackrf_usb/usb_api_praline.h create mode 100644 firmware/hackrf_usb/usb_api_selftest.c create mode 100644 firmware/hackrf_usb/usb_api_selftest.h 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("J_CV-Eihh9+MMZ_M%0r?;>_oo6(1?@4CGdHVI~5VP4MPUa z#)x9IRh%An)vIEHt&-1(Q4!UH0q*yhP^(4w`2h1OA;PF@11^-ODSI|*WDjIY+P?J? zBHkY{3gP2&g5hxpi^h8Jb7AG`m@pm<`yumZW8kbtgn^gU)hWOcZX9w*c!wBX5ol2y z*RTSS#>E;Ol|-ZY=#~6?o?G) zU=>h~Cl(?p7vS?InVP^!4brSotMcI1fd3Nv5RV=jh@2k%2fUM&jD>-7hyzMrbb30MDvXG1s>Ha!}H=O4Mgy@#$+YA|Vgx z;wDvC&V@DPs3m9&aC#S@G|*+ukz9IPPv48!1Is+|vK;kZ^~Dl%#lXh7LVq(5wfjCp zvAY>jD|fDtU!f?!KQJMD(B3B1{2b5FENeVs;^`*44x?5#>8hyv0jZseIEPgIq!1rq zK>0RV&{&1Ss@Fv21aArk_gxo=IDZP1dVNN;I@d%&ex)415k-yiaVliXTn;-&$_2s- z-HSqN5=zTkR7uj1^03g)2o&qbP|-I?GbWF4CRgHo^10|UhZtyIwvZPUj z;@Z%O-dxdD1$DPaSNXjdGH0#`IAuoHX2^?Wf!3l*12y`XVH)zZOKh0`gk30%2aVH$ zeb?!#(ru{4Fh?%e3l`ZJ8%M^FRl>VVM#EN2k0xU`iQ3MlPlIB)ij7aY^*0&aH8Dtu z^1`MEv~jAzZBaNN2Q=jdi}J!4-ltt+s3EwHp7ferx$_mcMab!E!?K|OBF3q`V`v%%6M zWgoz?f4iIW{p^}`!a&uofFFXXDOy@JH5mH zK~4M-(Q9*?0t@dV>VS7UrZ6_6EqQ4gu8;BJat#t^b$o=n8go#Ua7vu7H^coHT5td= zF1klon{lt@x$qF0;(lj{X5-9id2v;!&P$|zXVgu^NgCdHYC^+^zUzZ(w2M-MR_Fz+ z?oTmYF1RJ_4hqytkCNLG((ulYHT;*I$f-g~#JxVB z*OX~R=nXrk_VNFl`4hi78}TMC!~p!K&^8VJvYUSb|Nr6n-~5^XNarD7f`J9ruQ9zG z3+w$5+65ro!0M(_U^wbL;m1LnOY!oW>^yhYUV-UBl$Uf3!c*EnAIOEB4V&F^zn@+C zHW|hi`W3H5KJ282Las!6lwKEb5UN9uTUvws`LI)3K1$lBAtFA_zU#PZ%la0C*B0aZ zlI5$L?5sh1VS5moLOF)6nX^j$KBNRc7y5H$C}3235J z+*vyKvafh?{BfZ*Anw`NZOFw0E5P8T1KLK(2$8a~wD+)}`}VeoSpNIpcQuHBK%qHB zV2-#(`m%2a=#EyyW{6*4n3b%{dfz;56eH1n0)uc8FSR}(LL7oMTvHybZ891X8XAw{$e>Y(4ApV8m+_2eeyMF<+}q7KQ3X6_x!ooPLR7gIj&VLkcH`t zv|%-4fL5y-Gb69nRd4?Sd+5e$!+M;*u6u%?oP!L4iEzd`;KQ#^19OZI z;1|gXXr3j!fL72b28ivNgYJe)iHDOq5Y5@ziE-(lNG5wl|Fbl%o?Sa%_FJ@WlOPZ% zhpnBVXT5lh9NLdo{YZgYabw}m010A+gBr9M_8vP$Yu=$rFUJeiBRNK-m)BtKv`h#U zg6>_@hW~KxDadwSwA!QTbcaHQ{7%l%k2l%eRk4eBW8~yqqig4fPGGq6G^P)B()G?| zV(3c$O)c7RWe(0*gxY&_a)$3hx;THf7Z#(UE=d-ks`tP*fp+b`f@G;XI69mOWBk(H zt@;mv$QJJ3+uNjPRd;5oHffv3xRVJuHA}v}b&cywp9?B%Af6VK@1LGy4sNWBoQ;GQMtij;V=)&d^$wT`pVFer4*(*YcjdaItqzqthRa=qZ> zGlrN&u#^OTOFRIpw??B0yG4nLwqNRLrWrDZ=7EPc>_HHpf)6b6XoxM?Jl z_g!t+aEk+uCJ3^1(CmBkg5K>~0e#ELJSqFzMcPJ6Lc{V<{YE%Vu6dzo$yCmFg*8DP z6((Ngy%yw3pV#DT?<*~0jfwS2!nQTc)SgCx z^{VKoSj3TwTeSii#n}BrNu6fTEI<|QQVy5N}nJe5Takwcc zChD+xL9d1F9EjZ6@r-}IqOIFD2an^L&^t*!7GDDorW@LN>=B5sWpG$d z_nA(&bOSrr`4>sc`mL%gZ>$R0{a!RMMz!yw$!>I6>p`+%1%TVKhXnyW+#mXd?6N{v8(T8D!#C+jBKQ4F3^4{0?U1T; zYwi!IKho*buR0#oM1P%+nncKwv^9AxlQi#}pwX(QMeNR>uJ^QXMXQUXa}tr@rvcll zelmm+n(cb6dTM#zjl>0Ei?>?!Ua^T5;zxXD8~+TEVoiqlpVlApWdC2rw89r-bxZw6 z^Evn_s9~b_T(4MR6odl8$@HG~M9A6I!Kjjv+5aJMA4c?<7KF!CA0 z_ZjU+UUYFbV#dYUUf;!fQP&ocFdms3&M2MvN8vbf2fw`Yqj04AOufXq+HiwxdeF{+UQ28tR8U-BGpM@Y}YcPn)1T+%WcQ5ft)U1 zd+9Obs8A9`fuvS{b8>ph>ZW`25k*`eKYgtS zT`le$KTsW-~eh&&8TJE5hxtOtmkHCs zfzl;Px~SzC-E8pYN7CbuwlJfb@;=PLQ8P92H)pvns1b4|cP5eXNkTJ-NdLQ?&7i(!kkys~5OPRa{%c zwV3d%Yd%?BmaS27HNG?ak;_!(OGlAB6w+8*Ur}iKfOit_MRr!xF{)P5p zhIEmo<{XiWB9R&pN^qsH&+?{1mpISfw}t$eO@atygeFP}3NNZFU4b8!(oZfzEQR~yp8 zHc9rgh4J&oX(xzTzfG11&)~3tw3ZSt`;AfHtK5a;`Y=N^eo}?MK}u{$w?sj?S;u)L`Te=t}0hI zF@0eKb5X|WBgXB&7srEEyS)c;1_HD}JApFd({01Fve)MdL1z6QkrBoJ;~FF18tHKi zO{4m5m6N>&;91Pu%FJ#P+EnLS#q~I+56`RT2~mh2ymo&I)JJ-5>*B2yi+-|`OJt8m zXP6SEQpc6#NP2`hjx%;4(wcCVRqvSrtLPimi(ml5(^T$51gkb^FUX-W32F4PPBR(r_o3r6Ci#McLs+2%) zBt>qLxcOmJwA|Y7R^z$FTt2n4f*4bC zkye0rWJY+LDA%XPBvddzpdu#>NteEk^B11*H)99QJ@^ME9-mKn@2Pc6Y|lk(n0snp z6va*SDU&?4JxjUkJarA@$u{Zs;|V7s<#mDy+&mkh^FQ+bjqjDqxjd20R>Yg-S*_H{ z*&^x;bMzDy!8an+y^L!>sRBJMD)u0aU@@w*9C>ZU+SXGBDS%pgk)2qQ$g<{DrCl`^hEi}Z>gEC{#iC4;ufO6fF;UgMd4X$xZz+KL8g zkLvqOL{qijL6!ups#aY#iAlk5sQ%ZASY@B)L;{n>d}NcoAJ5bCvU!I@<5jZ={L}wJ ze(EzBSly&hQ?@hUDCmbphH~NctwOqW)Til~yCVyHR4daI1>uZlFJRn|EEgnF_8$Fv zLv`p0-d0AkJdK8(Llr}QB6p1x4a5shCiOJU zlrf0pZ>A~kyWhJ4=M!tI_wY^DWDM1;9EJbrk<_(1UEAKU=YQICCio0Z+H89x5GoEF zMUD08hL`x1NepgXunjp;vq$v3plg$`0p-dkSs?0K5i zvQ!(bx9Nw}=<_<8F4d|nmt>?kpkKj4MrzMLB~A+0b+Ncr59}A#t|$)VHjx&9;fu0V z`|jH)ay13}?m)!8NihkEyhxr-wDsr$pGYU}n`c1eX_Qe&Vw?-sj=L4^*fU(pyVe1E zG4XL^pu~Gsx5~*T@?^v+4peT~(}p*zVIq&4Z3AtLtcI|DIzm5||DaPZN6gBp=lq+t zYhx*K#RT81e%?&V16EE1XNv63A1$U}A4WisjJE@~Z`dPfc6pRb!Dvb@=w6~O zStqCULmU>dNc)C8uN&fJ_4{(>2R8B+rCXG57a&;Ml=6U6-~T)-#wE}tlYLA=^-`%i z;RH{2#%d9<5fQ6hOsu9p^&!hw7b=>}qEhM;U6%C`Ra6LkQN8CZY`mBRqsGLH4puSb zwfZqjxb@3{Gj`e3b33vE0_k6S84*xGPiJGcBTBIWxE5lF>MmtTi_23Tz-S;v(KK^xIr{}#R8YK;vCelI`t26!FeeE|&9_(&pJ@WrtAKJ9T03PKMBaJ4Z- zo=6cNEVY6X%KXhJ3NO$FD_93Q^~Rm>`a|7bRssay8Pku+V{~N~>OMNV$bG27Wx7KMWy7rFhR(TSc* zo>pj;Yqr3)eL2NnnG?c7M16BJmcfQ^W7wyL)V`M1f>$)$4~Gf)V$)vT^kk^vlQ_pO z_lLF=MO*84lRTsleBB6i|4^cEMvpJ^!v~5Bh0%PxV9_TP7M5= z;qrz4d11RX_O7%_m_?V0b(r}qcY5wzQ6=SA*b}L}_Xa&0xr^~ja{C@LC-3b%S=}t0 zp0)TIiw-m3A*5ZIjZ$x#*5~AX*d#qY#WqTeo;Ga47dM64OX*|L zNsS9>C1Pm4Z3QS*Hsj9F{2urEih80#3oY);Io^MT<=vcPeLM)!vF=-DorPhP@eiq} zXB|P?%f2T#<! z4D_n)Vcyc)k8I_EgeB~^qChJ_L7qW%cpWCXB2B05w+S+2X0VHpuAk0)%j1|~^lio^ zgG$3`h&jaMF3ZM-1NCo7h>aS5V3)qa%CeD$E8VOjw3ojMZa4WR*|Fb03^4_dw~pxJ zixa6ThogcslpXg>pf1Ym}A;4(v+(@`N3Q+QM4p^vrAGDv8+;K z)NvAS4eNY912W6^ib&7Ma2H{C*2$c;vft;xR{BER9wLlX%7Xj~JVyqpcVIsXeJ);S9~Wkx7MJB#KD% z98>v^!3%US`>a7|Hr!BA&B$%iqNfyEXQA$W*#8z*U%R+upkfS5{aQLmZ8hZi6OY5w zw#k?+?na$DW>kMw?<)?9Yol2P6HY!4a~U*qWJ{ODRI-sOnbldn$CFVvun!=SCQCxioa#_E405Rr6|o43C&(u?{%m}gjM>mUo9i+=87=^?H44I z0##p~7^jLbRwXo?)`ve!V;hSyp4Ffc@?C?%;t&+p41#^8Q;RW=X9G!r@hRG{lxr{P zKa8^u=e>^vc-mZ1IGMZHMzO>bjG~a1(T?dtwU3_GEQXykYpy6V??Ew9`2!b?d>_U! zv^Zp2tctTa=2vk$bnr zk4-D6$lnUXQ&d~CQf&(F6+sMEuUlm^vgDws#;xc55i2cLnemQwzDt_*Z+)9|#~+~f zgtTWptfGnZwfd^~e3CNc=hH{s(_9mu3IA1Q3|Fuu^?`+<+Qn z?TgjTN6MWJFY&AvW&Bl{z+^$sV>yE!#tfm+NKt4o)8e&`ZiL900vvXbYhDz1{MfOV z33r-Y=dV|F6Q?x=nJE=Av-OWDT&Y_e_;*>VYztQrBe<=-Pdgq|+&}EM>PHJ~lc7Ts zek0A#(i=-5#BO;>SGJKTfy{{JYz0O`Xf!v0TjHYGS>p+k_jOvbT2VPKm@6iw(Ogdw zQHNg(4AG7UWgndy;Wl=SxWj654%!Jq*y&-UG^cyGQ?B~SE=6QGvn$wWIBN6)(|f7< zvxP*Kn*{`jDA+l0Bw)*6y-UF;=e9LAaInNI(Qy6

)#<9TxbDY?sY^ld$((YKlV& zI|F*HC}fgV5L~JkRJSOnzSsYoXqkG56E}a!LObxL7EjSE9yB{xKL$%M_CoO{LjANA zx>MX^thBNu)wM;spQVSeStp~9Y?jqV*ETsT=iwRCGW}xuONHE#@nSRy^lmJOQe&`^ z+chiIFRNPIVY-!w1`?3(>@(js1H!Cx{Pii#@@FG<`#q|jX>KmalLhlep*26PF@~@U zqX2(1oHjI`BixBEhn<}Ymj|OEya*TcflG-+8-J@(%HGoswmKoufP%%lF(6qSkTJb8 zg=!+3Tvj3KZaKGUfHutG^G84Jh&QCA-H%B9i&^x+`FaCYlb3>gB^f>&7l2<9l1fho zFA%$W9!S>e;%3au$^!P^b%)sdIerjskH%FFji!{hIVEHDLHBs$5lvC$i1YrUfutl+}M0hL>7Zp@_E(1@RcGFpc-g$z$OrE*Dx!plZD{Kw$=o zox!l$+w@^hWF)(@^hF+RVcLnZZgouaMtW`k{ALYOR`Q~l135N;GBP*W{Wqg3g-gZ86HO9$q2`!1NDA_TLWd4Or&G zh@`h`b*@?d6w}g|G`&BBQw=dyD+oUr#4$`QchC8lL@f<#>jN@niy!&CS;WuFVK?P0 z3RTzTFA&W9;yJf_i_gsYUYdNTDV}`t1f)1S_vM-Rh1A{^h3MjJy$`UOVu*WyTWda= zTf!;=8rWx*t#0)Ak4cmNY*udQi_Q($5pd8D!44plb*=oJ0G z)UBecP*k)tkP$bs_#L(FAZ6<_aMv+L=W+J81bUnk5+lPnSC48eqC07KT6s3A(~E{> z#f;RE^j0qBlasLjYon+1xY`L(EiOEGOV$wzHW^aiDBD~QL)mC@lMyF*GNyLK4|DCkGnm=DHK#d7A@>dzQz)B9_8*NDpoyyBPTSGstD=gXN6D zXw>3VoG8fZ`^aCM-CCUSRlQ5*Gf1vsJuBYxZ4sDxvj#wAok3r7G0Q`#SUV6iY|#j- z3K8;?e@Avv5j>Kf;B+y*$gsb>CTB%q+fLHk*wll3s(Aq|yb;^t*Zv$^Lo*(COHr?( zcQ%GnHb#$yC0RzMY2peT(eB)E6T93qw zsP3l`lVr&F%f887$dZn+SBCoDM$DM_m3#ucvOkJMXk7S2Ig0T214HFjDn?}d(%qq{ zp|DbC;JacoUyS?0V@`JJ)NF$gbP`SO-))^uopg)amRc49b$s0}f{y4t&4Eug@pnso z#l}8pu2%C4V1?A5ll__WRb0wwmLm!7%y$OYPP)NtdN5EwGueSuW%y_M?Se2g8Qr9L zl<=6Cl;OD|nqcB=FDv8B!FLfolyH3V;`vkScMIE6lu>CPMcImw@{BDru9VDiQlt%) zpkkDeeh8Tf zEM~;Pnwn|ko;vb3-o!=TwT5}5&_%jDo9MGFw3}w5lT6`Ke8?mLM#XJQx`B&H-&|1& zCNCLw*F4NOk<+~T3I7VXx5yGP3spkiCAL)KY$&Ay!EJQu%5}LX5FS<)y(Db@d08vmO4hRN7~qCDY=OjyIm_HB=qJ! z$Z%w#Xi3GwnD9P-!fWT~!fXSy_aO=j_qLbJwhFw(A#`{Hs!4_AToFV0bDKuV8DTC@ zAPX$LsqNegXU!eStulIj>TRY`Vqo=iA|rQ<-~%OFTYS?h42QPLYGsYsjG2+!J3o+b zYBbYSR)Xis1}ov&|u1UR5ikW*VACwtBJ$G(vO}@RHHb? z6=A?QeKtH0={}!p;vG8%Y(ji0J3<K* zSX_312+t506y(vb+&>@Z5BdJAITn+WiUEqyyO3>RV(X1}dGE+CO^OxEooQ)cSj(q- zTU18wMW^G?ARhkI<_v<$V+FeQ#CTz&s>AzQEAkcv9wR$%vf$b^5LI14!lR35DMc;S|X)`Wq+ob z?sf-!-q^9+CZ9-$FkTyckYYrj?f+^n5?t`wKEA^V&Q48AqG6L9QyMg~qUu`RZA#KD zI$rh(066*^`c<6$eP)-i)R)@idF7ie4vF&Z-2JYG^Du<{eGr}h(mYoSpui>V&p&-B zdjQz>Q$s%PQuqxJ`uVo!pH7P7a-P@O*a?-D7^os?{>GhwBZYOANVmKO*Tz|oFs>NZ zYKuYz)U;CDX>|JQyS?j-q!n@UC;FRlJn~G~R~E|mD<5cZ!+q$rK{SLktKRrBsj-`@ zPE@o^)$MN=8aSri@q_1CykLI;qX>-Fek;I?=l4qX!KGx2wRN=Sg$C-$<>-f1d6~4k zu_n-aOSiP?+o3*F**bA&w(NM&Vd`T)a#I-Y$gA>kNKy5?jXP!NV~j;}w!^xmRgc}L zg{%({J*8_x&|ckWfTHH?JY2PrzU}CnH#`V?LVlRiWKb^%hxN(*#)O)4%(l#-F2Isc zAz|D2@OY{5gJdUy=`mTa;?(85Df$csy%+j?q%i+5Jqhezu;fXFC&j$nZhH<^&W4va zdZUL)Ha&B?HT}rtb1K0F_!G}L%oV4LU$Xe>^c-SxP`B&v*7s_tkfZg<-JMUYD+w&d zD|5hDx<|x@y4EZb}B!@VakV|z_0 z@`?lV%3mRh(GSV>C_-PZs5+q(2uw547d_{?FRDh$JgMRFv&yh-A&AE z@0j?N`U88&I{hHp((n))lK(YvOt^hf?z9@V)@H@$9j3t6_wOM-OdMz$1KAgJj-qS0 z8kRl4!jjAzrKS1XJ)2fC^!7{%rd*P2cjpRQ>}9HrBGho#Y78n^mTTl|U1Grsh zcG`WTO-w1DFlT%&3R2}56ZlsfD}I!z{va_ce7<<+`J3!k{Swct=-A@7>Mh12R9izg zn@5z(=c+fS4%J29>)}0d`x~&mNA0ER)2O3W2Wmj+?F$%XqJQ=Dx4b<-uogF z{u2tk_|)&g+BTX&zim=-YTMWwc!3!EuE6 zOUOS*y4Cxg?D3-IDgXFflm> zGLf$QC7Zom@6MxqFV22DI8UoFA6l}m%_!7vk_AOChTK2C|L+s8$Rpa&jcJw?Jh{|z z3p6Zwol}}CR3s0#;Y9G$jHl@Xe@7l`u@8YyoZOpphUtmMJUKyR^l;9D$DZ|IjaL`{ z$Pecjze{%f#;iY?AR{UWf3DS8+LE(4YH{cW%~P~H02Bkgc{x1{FBSY3y;I;Qmjgn^ z;4T?5nmXBS*~|D}{Pg4;R!j`H63?**iPn+gCt@r*SQ2~Dl9c?>;#!>}kF!;NqV%)X zEUR}ojs#aMPRFsYT9O7M-ln(DtP~>tcz&i5?25Lz3CDxouu6XW;{iTZytCNzBBb5f zeo(t7d+dp6ky8SB2foj7PptnB2Ptvsizygvon6GVSi@CkyFd5cHIekvH14**g*vuD zR^mGPh8wf68&S6{jH88W*GYl|H};x-BrPp^@vYgl`nIa3lGfVvik&U;7n-M|GLLJK z^f<*3e}iI7_m=jv$s9*>*}F|+A{WobEQqHs@oWhH+`ptfu+5%nX+{2LY}lWPVf}nY zk{zD`5Z`Et2FIp^t$*@nvZX)z4?DNETA?5M^Tyas4Y}_FTe?T_y?jj z7=QyQBW=@PJ&49L`mJ8ExZCh9TH`az3>`vk!w~6(l}B<@se=5E!-ttHz6=Zy1Qv6 z{}ycw#l;D0MLFMsAss!g275`Z{6KF%cJ(T|*^>F4X>}({n{D*s>RcrIp(KW!!=6KL zqHjJfD&w0N1M&$_e_UGQ`O(+Pxd-;pa`kU6V^i$JtLVmwBa+NzXxQn==HNqJFBhwK zXdZYl05r+O?O^Xp2v&t5SHCpq#@|y%n|R+eyzF+<6s5oMXNFtY{k;r8Rhrl%2#Z$J zt16-{S6xQvgOMI;#y^Jj4NUJ-$h$4MyhRmwQPA+9oUX(6R1~Y4 zGc;r`r^CIQj%Ro$&vKbh%hq(M@3!?W6RjfN&I#se`u|WF^X4Ox?t#2e6(+W85xNvP z1c_yixcy)|_fzWkqDV8%)7j)`jm7?dB;7nsOl7mmH;}wnmdT-U>rKg=q0Y0!aQruq zxgV^H#(S9Vp7mRx<3s{A-fJS_;%=8_ZQDG~aWwE6a;s0!gjG)$^$rvDe`YpyiXIh2 zj%+It_cmrF1zSF{l_G1bU>B9&>C$nsN&NWJ{M}gni%$O2f%?EziDvY`ZWQwfp~&}! z=n-!>r#+dPZLSFzsBCKT)9o)6dydDVpS*BCh5vorXIwY_3WEe1y&U0Gq-EGv+=%EiA0q1 zG~6Ojz6bZy$JTh`SsY|bV)sNbntj;+l#%W#6jXj0U{}$6;QCFap_ne_p(kY-hf{PP zbMW>05y6K@T3zqb{u_CGLM*sPPqxO0ph>p=c+YD$SOFK8W5H&`a@7Z7pTd8YdQ&YM z@tMn_Ut8)`+TxMm3)6gWaR(6K#3|f2vOrn zv27+4spmDh1j$>A0{&~-4Do%)&c%#jF2c1`nSw`2FkSBPfGO3f;y}Uq&j$+gW??jI zyHVp>HWXQ<37f|h`c2z)TlIMwN>d2_5B1erO%#)hMxlMK7$!|72WvpR_kflUi4swpxprl>{oDoK|OuzjlHxv$)Wd!=CQ z=4n;ex?|U0aG)lpu|Pj1Oj9t+8~-S)7AEJY0lV}-LlL~I6^~Zin!QcVII8WC-QNca z`iG;X{1`_d`nA2cl~+z{=lrsTI`7Z2+NC#~sbfx#!e3G6rZV$UUBbhq;wN?xLJRE- zL>fovP8#9C`YFcpsj6VtBYL<+eclW^E8&~nLAgc`y`&TiE3wlM@ifi$syAq7ctKpB zZH*f~YsIt5>U~y^2ohS2gv2X5-puIjbgEe1lH3E6NIkV?92MignPQ>po^hKJ7E&r! zJxS!gW#V?VNNc0E*1c4!hpheXkt_Xnfp{~O56X@=z?iJ|-Fmr8xfu4-agkw5mk z6dR9L8?~BsA%psu_o|E0>CGuwYaPI2c`rOkvu#*1*k7%;#DICsNX(jq!$=z@E^OSL01r#~wtjn$I%X*7BVyggk8eh9rjG-PE}hp|JG^?tVb$8&o1 zn4I>H%CCgwrFrP;y+ihuIn_6xynyFqmfrsiukDG)pW&nO^K!0f)xQ(u&(MRtTOG+9 z+B@u12XWEH{xGe6cb9jSN~R!Gr2YjHK+Vh@L8Tg(k&%&0Au^&(b;QF}Xcim#ZjZ8?F?jv@$ipcG_;jMNXSe172 z>u^ceQjuZZK&xSimf8|qqu*|yXm)2#o7k@&@^nx=zM zEp|dZ8@?0%N}un{SJ&_-9^nxn0k@4^*(R&tE4itepJn;nhZ^~Il#l*+YglgG>DP> z&@#1)w1dihEPG(`!^1P#G9|oKI2u;2;jN{+hVAT0U1(l0U4(^hGEmB%{p6HdD;J1* zxXwR7==UElroFhjN6`YT%@YN z4u%BsEEbbQeQTGu4@V$NIrC~JSPwFjhD%@0&smZ^ztr8#P6>W_K=qyhSTwV+5hIKt zam>sU3+s2&%r6q+Yt)tOwPO%Rv6tuL84t7E03jyyi6fa)oY*z&_wVyr%FlKU^Rp(l z&(gfo9>Gn;IYu7u=2?kK4|#2d%os%@-|F&sb($WWLK+7&_MNr=li83sS}Z-oqoagY z4$Jm%@G39k4SPT0hB7F zt1%3*0LF!-rh8FiPTwfG`f7(O#{)+@_^T)%X~;#B#8+LO;|$D z3%)Bc2ws_Rj|+~6_ZDU`MM`%yIT0$#!f8!Dg0C5Bd$KLt^X#I%w!lV~KA)23I}oXc zZSqJ?*66An;in|cn>xHnTnXo0w8ZRtNL<(HXf8toSDUJA<3-Fy-s0o_Ek~C_yH}~n z>WFmny}98GrWIYzrU3zueT`A$wz;B(8EP6{rQ{hLt6`*w#(&(#)6Ixvt2(=+{*XFC zu5sqiXGPYn^EFfO2yR`|O*WPNbf<%UIc(*Q7Ud1P@#pe4tDIK0V|bb%Yt;aEx1u3= zOh4oizUc(usT~`$g0f1z8>h@QE&%7oaJp8EZLUwZC?}n%C1y|e@g5;?K@4tO4V~)lEZ#%w?Y3??jdCru&g6+L{vGREQk6!$*ekW z-#s`>7V>ES1+-2rYZAJbZ3~*n-Ip;`T5(%hbfZF9A)UqH%mJAyvtnRLft2>0{%9 zyR-dMrEOe9%5>EF0|xAdQn$KUAlpQqju~E1=t(4Ovu(6+rASX zP3bK9>a3l9KRC7Yl*3Gps6iMZw@B_LESPJ9qvr74vWsrm-6h;^H|6n~dXXuxSRz<> z)#5odcnLMk0@T4}D`s|3CA_`Z4YB9Q9U|OtqqU3QO+)QC!8D`YPAX?e$?|+{m#q=F z+t2_B?|fr#R*aOc_{av^;ZD;ss$3*zSwJR7ZX%FP0^B=@w;bLgL0e!t#Yzsup+#-j z2_rQ?pP@gzFKyl&5Q73oBbMe)lH~{BWIIEJ?OXHi3(XK_vAKxA2QHf{Tbdb0U8_a8-KRU6FQOS|o~G7I?M<@~BLbOFMxUV3zwGMIcXWdt z_CAm*LC#*YQbgsZGuRk3Tc*?E8OOGN{b05nF5JxuSBWu4r=cWxiMg-GMU08B(Zv{K zSMG?rkRNe|T6)UlA1ARf7E4OHhoY*TdsgSwBt|RulDMC`On1#s7B^ofo^wr`UY`FH z+U2E)(2=;aWA9${t>KeLQ$2AS|1nUuKWi?kgmSgl?E6)uZ)boVSz?*awy3(GIPn*f z87-7m|GEc_;QNOK(wL$<;*~j+H5d$Y*7i^EZzLPM5zN~~Zy-j=?KOUQaIi}Ov@;8mh z7P{AP6ARa`E7)Z24fUE8*&H&3C4=u6YqXxtgWEKJcC z-Ouz0Rivc3f5bVCcu${i%DZ43y(4IsUK`9=tVR6r8{OWQW*fY?YF=zM$%*6V%TS}V z3B6J^ie;{+@qzr-7}-f_rbd((_Fe$DCcoXgHNz02~Fh*mb_|k<9+kYxZ zGL?Jj$v7gJ&z0@192v%+Lo#SYnolTkp4_lNPSQ*pH$3WHhB`548V_8H%Z054r&7^N zbGhu|2j_%OSi;tmR~rPt%2gYC^upQ)gy<1&-M4?X5v<;P`~r+#k{$dS=)EH@}-n%Ne;jC|MRvgQRJd%Yt zHgoZW8NTWdweg#*V({5#Lk)gbXdF*C+R&%=hwPd;T~e|Ru*0xXpM$N9zr3AJ-TUx! zC%qRHUM8{h_o;+W4I}h4a5opzzT8;6-Vu{9(PQc&T2#aygQQ2eL-U!rtE%540ABLp z>nWjzqI961J=Oj^7w!+H>!n&_b9l>3MqREb_=g#{+1X8IvPsq{*pF(c)Se4^s-95U zaC!Qv{!n4zn{q}Zk$INKyXMhTbZCF)cVhHi8j3#Q{5Kdq>_4NmAFiWh0ol1!vCy9)pzOZb*}4Kk(5I|2ng-U|QxDiNEKKhLyZk-@77(oR34SqLbm%TMKHLK~ByQ zOLC(b9MG*};>OU$Ar)scjFknub7ua$yB zfIBFMap#!kSt`W=NVWN8hXt1vH{R~=2-vypo7oIiu_VYAZo5Tw0|VD+!V8tLIoYjg zZc#}{z|O2Svdr#?nPH=??tx1|H-0;x&wC)}KPSC%WOPxQAVHyPRpX=&o^)WDg6e+l zdkbtLHxrw7LJdEnAO+Fq@R!_Bix01L;qrwsYA;T=wxvFg(^B=n*XDf0^xJII6td$5wj4 z8>2pPS@t$B_{fXFiF)kwYssr%+nVc>ehLo-tQ@4W)qok{40+>1=3SLP3#sj`0XkE5 zBXpd93YZ9qcTQz2d_eth!h3*U(co?8+Oet>_NNIuAwR%RxoRONVwwMsqw9c=s>8LU@x*AjE(#1xO^oOA%3#9V9?xEdwG3bu|>-sOa7l1jRCRRCEUgMHCH! zE3Rc$&fHa#n0#-8p5yRLx;rw$7_c;mr(7H!uz-qP&gmb z{wp6;2zn;ITAUY1ryLYshNS*W!$R^i=o)y0a)NCb0`r*r0W0t{50?6^C%G{le7?0y zb|2+|Hj1Y;2v{sR%Wh{`p>}!NUWi7EA2Hbe=vg{9e`cIK#mw$u)_G3>smZoWq#0K` z;FE2q=^iYzIg$+#r^r9lG%}=N9=k%G&TO;%Q{*Nm*u4-j*DA}kd}I^_h$XBS@}{v8 zAZ|1!{WXwh623fv3UfHRO}|*00t-x-c(ISixV6LcBF4^)s@v<#V$&l<;CDs&+=M1d4lqZGiT ztL;(uBR$=xWsSa(vnm=>u(}DDYCj((H3n(u+@GW&I(UT_2q`t|pKMh1m+^K!W}eH0 zk^wHH&jkGOs4*`}YPLHt1(5f2<+Ej5|3+f1`!^R&vy2rm0WK%yyA9D^Yz? z87y}3@I@bFC|jQ0N4Cj3pEW$sN|!Q6&vW2449dgeo6OmYzWTI>$$pnYtvAW%c+B@y zYjSSrNm~8qZ{Q=cm2Zxp)G+sFVuI9e%-6}xw_$in&FQCjRiJP2XPzLOzW-gB@0RM@ z=*1?5gBk z_#$fCs#E5!sJB+C+@cTP$gX{s$b5IR#>M3lG%edK-|8FIvL&YE(|%)Q@KnvPtUWNvTK+a&ln z(Q|7vkv{LrH>q0B{iIb%*I)^hqFC${Sr7SB*O3|9yPvE$u^<~}Vn&hWo%V-EDIC6T zG+L=gY}oD@)OV#+KaF&D6-QnkRTw--mb$X{V*`ZRbZ#$^hDD=TN+nNAgPD%aD5%DN zHV~wOvM*5e-IB#EX^9_MzdDoZ&GcE5ce3zL$I;kwfG&_7`8kA#Jg`A>Out+-X+9Ah zeYn0^V!q3zBA*;%;eXrz(v5!%PqXKUpJH8{?~ul@Z!F+LgLn3BB+Gt(?l`iSZHv<@ zE~71K7>HV)XW`%ID-||=Vzu0|?rl2X!2;ns3Z*{u)Q8H)t(z!^S@ZpTPy(%jiZRNc zk$wTSJqIg`6qUsKGM2U3&P$E@LYU?Hz~YWL!FUOv!&IbTo}LyjS8Bq?eo9=G^N&dV4$ zrlrzo^Li7{R=Z3LNLK91@5XR*#rR$PlI+rdn~P=SLJ(i)&xTbxZA+#20hEKvAz^SVsB~G7 zTbaEd^GE`Sm75osha>n!feV>ncXRMXTe4%>F(zpg9f}Qw&ES*Z>Ju2gf#zUXe~+-a zhEZS=ka&P6GEw_a>*mrX4e$_tLqRcu4F8`o8pN3Pr45HP{JT1nFc9^;xi%bX2ZgD) zOtg@#L28iwjf%}0MJqA&!G_!;@&#dM(OXn>QFl3gAKJ^oiaVfepaiq!g6d9@mQR;0 z_mYW_-o@RtvZscFNLNY%y2}#m_1E21P{esI72j;d74iKp7f2(u{A*a=P2S2`Hu1>Z zOX@=ORBIAX1_sn@ym%(Ysepv80($9548zk%G3i-2The_42Z6oYpmm)N*sfp6rLxNq z$!SykkbKHlC+|T#LiRj;DwR37)Y-HMY^=8hsDoO_{&XB4YRQQJ@C5SbMj@vplHM0Y zg5I33V`tLZf1yRhs@N#?w3tFB5A3HMrs;YYLmIW2XVaxNzMMM?R_rfei-N5k@7RFN ziJ1rI9;B;Zy9*b~wx~+FY0`86rR;b=!;L~DBPOEGE#Or+!+^&0Za&gQLti)vcLknG zR)b53v(I37;d^Q2yRjRuzj_E#;PZ3ALGiA{<+o7j?4d1k)wE{1^r}4yZIHeB=a# zH~3Za^~5ZEU7^)(bY@l${?sON2hpHsg!Q4B8F50r!IrNNP{wt~C^Ov%bvxAG`!ga ziX>{FMk~&O71_c5^Y|o)Sz-{$f8!bT|BwVQVC|u^t@c>Xj*-1!OXA21sElM)2K`Hgv3t+jwU$~DuobQa*LE_15Dx`%Q5%t2 z)9{u)jUP2=!yqXQvgIf*&8=b`Sq%`pR)63L7FFj$-k%~<0Q){0Y~orIj0TOMe{Ll? z-K0PjjMAxI)Bzk{L&vKaagdglx}IC*%(;;2qZFaDwC<@mDk5$_7y7sCp>nW%zEr=V zkBsx#S^5UgANYNk0gW8_%`dIWX#+4MbMsf_JLD{SK9KB-h%nw5RzLut#01)1yf1;3 z-UQ@b>p+g@{gBR(12su)a*KA->(xCD`o|@KCR-y>E=`($2eX`Vu*>; z5;JkyPvRM6HcuFli+IJ@;r5lX0iJ1`bssTFYFKkGh!x0JO@0zqnG}dP zm1lVP<^Tqd@=o+Ho5OQImCe2ULP~9W z3O9|hjr*_#81ox7U>j$B$)`!kLD~1XP+*KKda3XHubW{s*az!}Q{}>sx9pLYVx|;V zRVKF6kL36I%EWE_Q~+3RGpATXC^=S1)vFcwDiUO5G-@8KmQc*9@T;Qdc88R?E+~-%}9J_k#m<%@X$m zCLlxPKfBv-J#)dgD`*Sri`A3F>CS#cL2|_BeOa^7yME4;31r*ksoj7L5Rz8y^C24# zrma$s*Sy%~tG$Wgm9Oj&!-_uNlj1#O0;ARdf=KnA@a^ z^v|5Hd8jI;>|lYn$2tfW%Ub^NqMPMb6z%zN7zN`?q|xX6--ENIp6K@D?L@EvZ(HY* z1z43UX5SW*Mr}$QzDct_8P$$qgLj_lU#G<-_+ALf!77ynw=R*+cT6<~UMYAr=RM$H zb^?uGM={D3!>k#|=cH+D>BD^BcHPz`k{_qG(dWMZD>Q$FbXp5L6hndy^X@}LgZ}g3 z*KZ*ky{W8Y{SB?uKqmq(%>^LW!JqP)CMW0z;q@R+9e9_0?e6?=F zkHtNRLY4eSZJwR==`PuXm8CUh%V7b|`MC;3|4-^bMY) z_{n>B7k{F4e1j#@a=9~F8D=J06+so#%>>0lDQ}E^@9yjOEIk1$yI@D;Fh&A_Q15*D z*XTCm_Gjjp_+1m9-PdVY$+U%`&*d2R^7%UY`Vn&xzpRt8BctvACAf{` zg&`xGB}D=w(&gi5BCL_E7OFOrh&NzG_Noh#4u}jC)#T}B*@KpYxucr$U=anI#zC>* zdwbN=Dy2!0$5PP#jw9EJZpZ*;gUj+5y$?jZ?AH0H?Z(&%zGfITP0Wk`-lOfKMHfI% zNlMNmVG{wjv~6Tpn<#q?6rS|;Kce526WS8sf1dtIt!EQO5`;DsW3G|;A^1}m4bS}c zRTTED`COd}7|Jjwvu0cPis3Z+S&MP4O$=@wy+ktL5F}UGH@D7WCi@CB3{<^R;wG(M zOTz$nJV$Zte*nOADz>29ZXMi|(*K6>_;@U)pAM}+vBZb?)TS*Xd6ZoZBFed_VU$Xq z(bDw~YW#RGUVTXMQEWUkN4@~m;R?d?tz|{Bg6Jqmcuv9s+g#vbBIHHx2eTqy2nlHs zN|``-GN)ij{~f-dm@Uy2Y4ocv9N!E{MNtQEAS>3*iu9nrget4ayHvNHbi1oTS=+L3 zs7)riZst+SOOrGjA{no@FpK52Cy{1p7k2-jzgYIXC4165-_Q(!rO}^LHuk|A+ z#Y74SsrxgMdnZyJgpR(?^;_`TE>kR*HJQeA++jm}D}m#u?&>x1X(9aqU%{K6N_!=H zxlDorZUfryGeT@xrVky!aLc7B6fL#3!Tr#wETlA09JYbkK70DosB1@}wh2?-Hosdy zP5NAfjt259nLO7Y!+>L=)#D*Zg}__=ct^+(s#ZL5sE0TjK#sDmDaRxx^63dLT7TREfZHu;62=7gf!?pw*yfgez3|ZBYFz= zPmrs6kUdHx7mp@BJQ{$=uqE_1QiAFZ(JPCV*<#bJ6 zK>QwxcB2JEw(*)&gzH`FqSbc6b22WP8>P=ExERe7VpEr2Vs;10Hg}SRBqu-u%Y{X& zMKt(AN*Rtw1S3(pFxPD=u3y&q02x)2S(tX`lQsI3snLbi!|{CR3><#)2@?FW#7+x& zrAc8Oq?4lI$&efVm`jWDSw)O}mdoNTwux&>t=+e?THhjPI?^Xyrn&1-T1fNVhMSwj z?3uI+7?+G703&;l=dCpK_a^K?`?s}$xwNIEtV=VF;zG7?)2c)%#E0a|5@W1?VbziN z4yW7^?mA`(EF#(U9e8m~Z|TO>5xvjiEpWoPBW4KHpfeGn3`Eci7Ys*NI*dkRIwEWc zSLn56_?bM?0mlz=lfd|(n2e(d9+^T}IAX&HtfI)&-1wtNq)vxk@*j6l$>!4#%{Pg+(v(Azb+CtRSq4Gf2N8V>nqt6qH zL#u1J&dRNIWrQR}yD{y-))6(!P@1Kr{tDyW}H6;m>6-Uh}Bq(UQ< z$V9Ey#7?Y{FwFur0mFBhFon$R^@{cC&6{tf@DoR0AW#xQUJV2r=>V?VkUB$lC-ac` z8XL9Cl!r;x+?^tpvgrLcmTr(`K2MN*Guqg%+3*?*f{5tZ#m!hJU_eb(_UvKDr&t`x`U1t-(~{v}gMWI<9Uqq4FZd(}@3F+4PDV+C4#qkL$m3 z12&+m`d2I)1$d>L1+q_65oxvHA6Somy?LQ*{`b-AdH9@G905|rq;lHinX?u&;F$Ji z^=hIC+NGmg1E1T2iN8Z&SUnb?PcH6+@K|VE3)^ty!QKMB#Ngk7=cwn;vkj_?jriOY zklx{F31~=PW6F<9VI;|#0`cv5@`d&~Iqo(Gkm8fNuOcySQn!GIOPDI|-lN>LR`zn~ z-Pb14hjew!Av1e6^yQ#la%RsXx&hE~<vkUl#TfyL0xy8mEh&P^|(VO}tztwBGwel1tbcrme5dK^(1E8vhZ)}>^oMvyt@ zzNM;lMY8!H^x_N-Pd}08AGPf=o)f{^>|vGFfzpa{yyGg$csx#xLj&J~^jyJL_y)K=nn$NE=T@R&Yn1|$FO)j? zA$=bJm^mbUm)*qp4HG`4D7~M6vhonsot@(`qD%{6?h>l1mguA9YM&FnXEPKx_s`oP zAsct`X&HhgYSO`V36gQxZpP@3=m^cu zE;YCL*cp~8r*7g-L9%i{_cwM9oJ- zmD65nV)D(+To`x|*jID-NvJu|R?Iw7zSoYvudd}=75fHE5b3-x9Uno0gMj)41-2^T z@I>A!sP3kKbZ-*x*?45|T8fX_KqT$y{@ffcSI^43$0G&3^Q1(1sdI)pFlwhRThEAK z>{}7#pv{pPv@nS{wxMLp)9Ps#n{RAhXh#H6owUxiUs1R%aWnl$v8Fd=GA}_CgJ+x= zi+hGqdb&J{-sHdOFb8%7Z#UmxDI~g9m9FAl0<9r2NKL11luv@R^Iwg5e(hrkE;6I$ zpsucUX&V?3Fd8iU2jr>LlJ;0fJ!NzR-A#W(uDkfC4Z9DlmVWYbw8BU>f7V0YO4Otn zMZRRgqe04OsNKF*s}xUyM}rpQ-j=LwiobN0GjO*4RZUCT4R3RXwJm5)XaXk zc4hZINm?hZw3jS7Q;%rN_<)b$+0OxzOC!1q6K-hR2{KLMwtuBCw5XYWtZezKp3JoO zb;Eqblj(c{<48>f{4%htu>exD7CtU-V04%!*YV?bwrY0py0x9JQ#I(MNJ$FY$(shV zQ|52sWIh%9rytke;Hk8A{R?K84{KTRf8$h#X9zUyTRvR=76wz4>iy+-^*#zIn{^^)m(^thE~c22S)GbvPoFs=^e~aXZtSg6bMsh z($LI(C_EH?PV8zPtQeJEEH!;2&Aw$fs=#6=4Si_w*XbGZvvM>C9Hr<>?#Td|b%ju# zz1YBQ-WX;y)Z_Wl3lkCeiY6OGRLVH49wn}~u}_2P>e}9~*LLAUVLcxm*{qIzwSN*G zbO(OqC9(_2^jj(wrFc4evJngtOB*?t?g>#fLUurRmHm4LQHKOO`S&mF&~U;MpH_Qt zBZu`xJ~BM`TbtVeAF^+z4)?*L2mlZuYfe9Yc8HTpuvbS(`G!Wza!9;+5AUI*aBpZmzYi!gJW>80|h;IM)J4HQdluCCL0j2R? zRM=L3Vi-O7yWXl=;#tI=FxN#lIM;!@L2yy1~iua$@cJy~5aRwPi z8tZuF!8kY+_e8&=>gv4X+c7_cSysLI6fGcGAp8LEYq4Kuy^C#O^AEY`&81t5@BTd$ z9R%2KCUvOJ7Mn_cH0A?>duKbzV{j;nQojU?-f)-Fk5`2c(2L35Im0$C1;v3Bh0x); z12f`~oFxPbwVgCJe=nawr)5bj(y-&)0WS*AB z@KL4?Q;})aOFc1dg^gfayq)7*L#y@I@zO1N-R@d~RS@Rk!_>6?Of@nPTeZAKr&QUs z2>Y5HfjEQJl##cP$6t(h5^o53zrPq0^2yuQEEV&C=a~F&NY=B%IA!RNIm1R?YRB~_Q1=}xT9GnON&1_V{>Oz2*M6F?TZauG6%sVCg} zs_9^9_iPojSf|%LMKJyE4=x7mg~H9wnZT)a7K)FevsytnEvGab#Y5l>pvMM5tlbK{ z@B1`|A{k9YOnfpNqQIn+i3I-hw$e)Uc@ZIUW&;w!6ru6h<$VY{TWYXrCqcksxnL^) zBxO_IEXcZN)95vXbZyNq@$ZQKjZAAh7}|fBg~{3VI!eLU(SnQ@9o)6}acNjV8!`=! ze)TPg1{I%M#HUazqW@s~ZzQFV!JCIQy1G1jZZ~0uu^D5B<*t<{Mzx!`z(EE~&_WGR z$J;~@YqX+smK%_8lV&^RudIB8S)Ux!ZwEzIIqnXKl!?=XQHD{|KL*8K>GSHB`+eAh zOCPeq3N1DMz$%cQzbvQl7W^)6l#$ZGyYVbAQVrxgasJhEpF5)(u&|xwq9;R9{;}%ZBxp7F&g@~Ja=SLhWz$lcW z=OJ0>&<*%d!l-$`tw;A&p&9b(&mZoGRC+TeQXQAJDWFI9a;+KY7^Sfxt*HZe0EtWK zniUj>pO)E|6gF5J^%e}hdSep1nLeM5>Sr zs;Y_h)7;{Qj$Rq7h+tZloc-J!ffB}>iU7VvTIhS5^l5TKo!_HZ%YP4P?4l9Y7n50AZ|f&{H;c!;IYt^TPV3skEBRa`P-LNIWzeTJY%W z@(+^At^<&8UYk~qy!d#-y@~y z6oYa>)hLjiS;T#8IU+9B_+eLFmI_-{ABm(T-_+Mt!@(vlnEWs2N!QXR+==~g1$Ce5 z+1!W}l2tVHE+ja<6b5G&mX{{8R515e@Ms(hurrWALzLC3e!jN!Q~qh=R>?8TvL@WUpR9(#sDdbZaxyaSkmL=!%E z6E|;5Bhx0S@#D2h%!4y#gt1z7vwPga2F>5r3XHa2$v)@JtMtuMq_v*2`l(A3NBmmR zGtIY4>^7KyvAX@hNXE8MWmg@PFVIm#-j^*$hHFD<)ev-6_av}M-EI<{cF&!=giP6s2jQ4Sk3_>g z{lb?~%LUg8`7?tJ24J!l$(4z!E;shOMr{Lh>UQ$rYs|l8?*V#w;g_nd1YG z>2G^03i+Z36mjrDU10r7pP{0?Vud&S_L&`0R{3`NF+G=R9l0Nuj7{u9LQVO1fy;x+ z`PWgq6cvP2?MhVS6Pk02x0#DPxi1cd2HC+b#q7*tbS~$jle#{>TXN2$pI8z$d@4PA zFUhQ-ac5M2o=?-f-T|Z>ESpiDr%1K0_V1Dgun6wq60)iBpS`tv|Gvf~HgDZP4dM;& zohRP{!GMm8itU!I)UwUzFlmHeN_i8d1sJ7{8XDGv2d~7bc%VSNG0((3xONHr6m?xcv^p%iQXIRc1 zfQfq;Ti6$J5Q$a44ppaOs zlE%QG0E6OxXL-@TlE0nmi)HHhFYDAHQY78|$3ZNs<{bu`gV4E9h49T|p>`jgQe=up zbmkDT2Y%S%I$C(mjqI|5jvjJw9xf7t>T-<+w~d#w#LZGYT(faV+H~qNy@!@FsxxO| zcMwbQpG%M`lIjmKDa{);3e{++i6J6 z8zhGlvPq5CwMLzNOnyv4yqNEKl%2y8f0OxOE+g7Kz^##AB;KJ{SF|@_we_&=*1B{3 z`=l~V0H%CrA#ackj+cEDm@mnTcA4;>@@GuFP9EAB+QicW^3((Yv3`-N*1g;Nf7roU zOv?e-lV@VG&mbksSR*$|tv|gtgcst{+OL#WqvS!hLskaUZgsIQwWw5VWJ7?BN@a$$ zT*V0^f0uJ`_$JgUC)ELb5!6(iY?1zsbcWYCw|};W&2`87?+&czK3tIpjEJMGXSyl1u0@|F6w)KIIJ&sf^uWGDdp9zzn(~GI& zoEAR}!6PQ%*o1^N)mh+ffs}qf8%RTHhM_NLF6u2s$r3HYp9u;{$#tTQu-^TYDHc;S zk5h*LVSM^8Z}6XZr^`jlp90`HQb0I&cqYtMwaJ`!+stgRCCOTw8DWVhV_X=|hw}GRL zZQ!Zi@tu9}aP-@iyp^_-*^h4tyVOnUih6C|93YS$O0^{K^?BD?%@6GLxH6TJ2QVdnq z&U2LS*#~cZXbR$M{6>dOO2T{>ehn)4q&uY|15#^2VL?E86#W9pbwkkykzDn&vWe$K z6VS{T%C*^2JV7?`2J|fdEa%|uiWtSza0tnK@N^nkNyk$}+2gKP?Zzs*=}t1!Qp5TA zFAs8B-}N?3%Bk2lkrxJT@mMIH!Nwh;bpwi1*n_)eRGF7fnBnAJU}4^}clc?lU-g3l>}z)R%n_+#lZ`h(pcVL zqs3O$;@_tEkWqgz{@v?&QK5~7+;R4W{M`Sgp;O7ggt71 z{9~PqWR*^t`m{~@iyc@CJIg+Z>%g<)Pkf9~zg*W_sS4LFywQQ+Cb?=Oou323Y!h+d zw-DWcHN`EtrLF#U55D7hmlcBDc!DRevU<1=f1sKDLa;HZu9^Bv(4bNkF9$G2hoky& z2bPLA#9&#-=fF~oowy(C@hzeQR7iF2a%t1@^)%oqWCEn)UJUF-rKkjp z+8`w}h`JY`-Om0`O2LQCiX!4*MiR|Y@4^ydB7ksmci1&s)0|&d6&<3 z7AlXz+9xxq<}x-18>VfH!L&K30lmf#NMz1_as?g(4OY+@7yOwGt}(^aU!bCpJ&YFh z!By5a*d>^r;D*4~7j9m->bE7cQNY}I5i4d!gFkBvC8v zbqwAAO9SVBU@>p(Q0m8?v*

eZqK`bgWaCOtIW>j6mz@R3U>)5u<%fnefRX6H68i zR8DycGY~S-GA1vIU{??rYLG6Z`(8ILyimeYy~6A4Bhe$=mTDXK;dy=y(3Ut6?s(XV z_FztVEd>Cb@dU->fyeKCgKG2vNkje{E?Cw;)%R6(e@7XxuZ38{MOF=_VLEMts94AMGpe@XMDt_x zHoNXk>M>C%*mJcbesdR(B_$8s8yc(1_amG=os-!uu*LQAzjJ!_t&9OC@axMF9>jXGW>Kw4YhwqZhfYsT2{bzH0VgG zhs+;kJghkkj$$;+1(?0sgXgjJf7Hm|p{xVmZ@|j`r(9p>)H<^zbE&vO8rrq7RY_!b zD>gi0AsO$&vfM5G!mmg%DEkxR=>|Na)k=wU5ZpfWJV-D!@u446Q8Kkz2{p!;!=Kj$Lg#Nk5k81r*Kvps#K@-jugV#gEgE=D%a}5N<7?2vhm@3EG((Qy3 zlK{PqPLlDt93A;48kk!Z%3xEmKUC#=;ZJb8xXr$7wvWrKB#F`c9kq+HfhP#f?+!3a_Eg?D+2V0}me8%GmP(@TWH%LG6 z1r(yRZ>wUk{GQ*G<+hj&%M!3Ao#I&JMc0BnrU-8aO!4|PsK!3OYBM}lz`C3g7(L&Y zqByY*PW-t`o!AXG^RdD(Uq&wQ`CN)tSt0VYDodjYQMfiN|2t{ z*x%UfxJeXk%lBO2XI?h|FkMq_-FCv=r0pI2ziiZ+Qg5rW!^9{TSVRMtnvDt-PaMBT zBEEl`hgn~25~SA|5VZ@ zr683aguEFou!gN)ItdK9qAc(E$()TQ;I|yX?;D1)VP|9o>Q(0?pM!trD+>Es;q?Q5 z#MSN7mG@ zDP9JXIVyW^op+X2<-<+D(M9m|6+F4>7+qRmP<`qe!E(iYrwW; zoSDUP*1WC7Blajc1)M58l7WQi9BtB7OZ*BgQgFrMDMT-0mb-E;~F$}R*UgZ zKkBz8hAvP^QUyUa1IQpNnk}JX2L$jy@x)U4S*i;PM0$FA2nx$(UrPc925r*)NkZ^q zR`on3Z$0GnLq?>rWY>D(knC?MEJbS4p7KIQoF^pm+cNB^rRNf0A;Ftlo=-Ghc7gR5X7O(yA7IF||x$xoC z&&sd~_Mbg`lpH*U)U4{*wdom7zsHCymL}(QD*>EDB5S3A@q+8uqn6vS?hYnjY8` zAnt%+`i%$hY~T3OHe$e;j*65XT)dm6nAh~(^Km-sPD3Wb%|(&9E*e*=uwr+rdr*wz z)@}1*Yj{Vl*W`Ry&%}Hwt`q)2hb=Afx;?*!U%|`)AN3r9Od>w1A_*W@33DR!LkPMm z!*E20UW4D+0b>UY_pAS~@jr<9NH!7CPD29=7)6M$R6%&_1NbG>)oe#7(nAPq3J_M| z#SMI-?jQ_d6lI9C2Z+=Qj1ua8^gtUdi{x*H??BkgchmDnzyktwHkLIyH&Z=BgbR89 z7ETEoN~IMfoG9+3@7RSZfai^TH;fzx2#xGT${T-0F=+MQ&J)qT@|w#q>Kctm=5~7C#T5+PxXB=eRdf(`KI0vzV~SH8n;7kz zNo)d6>!Cp*?9w`xVMeHaQDiG73y0U$4Wc~>0l+R@Zopkz;MGixcFJid^6|LyBmv#~ z{d4Fh8aE6`LoFrjplFoO40|-n zS6T;DBboEc4e)bj4TaP3C7J_HVBJBBn{U74_b6I^hr|5q4m=5KeYy_oBA9Pk-&VtbiG%7Qhx@Fhc>WX*aQ6udCS8DcLyP&R!)AqYfUAq=)|I+G|41Zy5;h)#JS z?L#bV#u(xWduqbn<(Z6uFr6l@l`WZ)<7jl8;}Fj|Fi}O01iOVrFSPj>GW+x*GMcti zjpH6z62$V4>T6F7kU3EDfHXSW#_a83$($_SP}duq=S4TuU_zkWX)nPGY#h|)KCS0T z=9!Fe85Zg%uHGs&A+^`%u?48)rnub}pnrajD%J}%+=jiJ8;5{*NZFpCGN%-$ACx$Q zC>!t9xPDEk+2e)_I4^Dv+k_G@A1@i6W(ooPsKRji#}KFb?+5OeNv>m`}H&91?~Q4^;~o72U@k0QR*zvpX2OVCDq2Wl>A#{7Ygvw za%t4ev2ToZho@y4gWVqEn14UrZ-cM9X#Qv*>1Gs3t!X}@PBraKf8H~W)@z^!`4znY z);QCCm=MqRa{=#n(R+pq%g*o973v(WXJl_@kSQc)H&BaKaRW+9O5!@ik%jE-5DaEE zJfm**T?Vuy3t`KiLyDI=sA@4Bh`$G!8ipD;Sg*0Qp0L07KXk$Wq;70`1-x9H2L3@7 z^rWJ4%6mALR;q~EoI(3pT$ek)-KVG02A#7cV=BI-TYyx|V z66~Lz>=?q})nWlIWk0Ay9+yv3p6zh>Oq2&O(;^u_8gmw%U*)j0s3`Y`AO&@M)Q)ka zIVAg!O<+~iy-m+^&uqW%Caq1zOH2Wij~g(=6@s1U_D1-|f2tNwuDglV1Ye)Tz4B#L zY|}Mfq~5cQdQ_T>8NLN+Hat?Zt8<1vNy{+5M{6XGE5lEl3hqG3Sy|&T%6GD$tjIo8 zf31t!sG{y*`yTZRST<&}WapYXQ&*QCP%Egh{{|>PMCaNbTLeE^9)MlB?~{Tn=;DIj zc05~`4u#SSY*X@E$yEFZS!3$2UhB{9zfI4a(42PMDz5{-bV%w{KMmJE%B04kear4E zz&Z8Cc62hQ~JF(Bt;- z8TWOFW^O-CW=y|EDt2>3qvIYh6=nagcsxN>8`SXO=n~dlOGEoeHraQ)4MWm5V`kCK z>@iVG@}*lx^n9*k*3qTfnEn7lkA(%~W`(m*Jh9$m><-`C_W)xotdUIh0M$?W833eo z^0uhgc9cX(A4x#m2cvN4ePfr|W=TmqY6v0SEHyOBezeJE^n9Ftio-$!)UA+^r|VV^ z>Ee2y1rF06JbgO-kK@?0(u@(;N11&k-}}JXL3FMLfQKtun8UT*%;2u95OFDvslwfu<2{>MiisBL3e2q<+GEr8?~ZchWYnn zfCgluKY$5*$y5ve-dNFSBPO)yy%NCiu;u4Yjt?PD-hg8h&v>Pl`kj3GryFh{{xxhD zi$$L`vH*?xjk<$X4e)cn@0JUF3YqAF4hGUHq4*seO9Lu%1YOipslo!3zMi)MPX^nz zk;(+*vcmL#1oWR(?JB$tH#i)1oAw(=*>_>sruo|5Y9WjN~e}oSl=xlW?0{2PzH3#8Pr_+Nw>PhR;|j z1U|hYhHzXx?luja8chDr5y4<}+6rn+JSUi5vRvlt~ZXYp2B9Ii~n~5$Kmzi zib}ve+2-pe)Tbw(Bn31`oI%=)g$ku;QF*X_$)zVhs!{A`GeSS7!V;n*Mg7*?={I0DF;t^p|DaUju_<&BD)M za4IF?R#*k9#K^w{QFKTGz^#kq0q%Q=^-ERN1$9`U!#1#1JR3<4$}IVERGQ;Zk;LO66l{IJ*q72C_Jb==X|Mjt zMwCjCfB*}oKTizjuKj^R3)n(!X%S$}uI0OzGZcT@O>0UodHj&?HS++qw#l<6<;F;n zu8>z2czJ_rwvzRNJAS#d!fr4DQXRtDovtGV^I zrt7Q7jL>s30&BLIn-3kkh{mjPB@v*?Y{^P$=~$e_S(I^jA)@Vi#nO>}%BgXUO#~hg z=0<=ZJVl;rl5Km&Nq-BzgeMZJygdy8Q-9t%^E?Pj<&+>SXWhZF3rrLU<)ionOXipe z@TV*<+-nzFpoCPCgc z^Jh!uSo;=fuK!z%I_7&+8z_f!gu6p1tnojx#R@&mE)@jM;b{2>HB!X+yn}k2XlZ{8 zk7$>5_9Gs$Ru|N`?2OA2%}fWqGuC4}}v05o7p}vPZ3NrqTt=6%5}B*8WS0jbQy`G>+uQjhNdqM_3+d zE??@?dLh5wL>*V6j0(PVoA*wsnW7D|hbt%F+J{K8caG}785Dkvk37Can@O{4><-zm;fhMH%aZcuZ#TN3W6<>4dtw``7w zHBpp4ClR2D2Y{E2Z(v77)tZsjL`AVbQMW#k?wS;Z5ep0f=N8O_sfcW@%!RaBAml4M zy6&M6$P{Z}S+cbXCD}HTHV^c@*Td|~dwJh8x|PfAzi4w}Tpi+mKIk$71`v7qzktLP zQPnPNt4v3tEnE523H1$e8bTF_d}Ys*6I5jg-nlB3{wpZDY!lUt{>9m&CRWiBH=;pN z6o=9=xF;?TD0d?@bg^NaCEUYVGILEUp>5-ap_*0D%Wq_a<5n*@23VUgEp5m^U{ zpOB+nP@dCqO`a|7ITEdT3zSWyDvq?0>_$t9{Y5u7V^Cp5RotJapOLP>haFN#+9wGe z?Ce)jr>R(vi6j8nZt-N49@MQbh)tJ@HU*vnc9dHq<=`o=LnM?_(og>bYFWs_*x#Y_x1R@o>%3o2167C>1Rv6S0Lhh~MGs zmjP2Jh(jqSUDks>uHFSPsn{Fd;=P%bM4+!@T9S_0&H{Ilk!aPv)Yjq zq7;j%p)r6JqUzYI{(6+=-cNZAsR#@vf6S07s0dO8GwVN9~kXV9fUAWt^n1T3jxXcdY1ZGO06Lzk*k3 z8vzw)U^_&(?eFaoRZR4hlaNI+buMpkp4_(g9ZY32DA3<7hin?(8Mp_RnD!P`j|Wq; zJ1vL-!NYpefeEcIsM)Nt-nI@ISY>7HU zlAz@Pe;^H}9*)6-VCsccET-XLClCQE)=>M3fTj1icI`hhyCrQ=Ao`4Z*yfEqYRW2x z7Wn!-7U3Te68IxV680B3A(||m929bqw#bAgC^W+XuQ?clzb8Rka|cmXI^cQmyY3V- zN};qrrjs55rGR4i;P1~x+!pn;2@Ya6!x>V30uz4}NigDu2k;iM-0Cizcpk;)VUZ%u z^t?{K5AZOco9zmqrK$ikRZQw`B)s!AGJPW4_+;oh+!Z_1)-Z#rfY=VA3?+}Mkf9V) zXGffH|8^QiDRM~FjY9ZPzV^LGT|amJ80Z^t`l?;-umB+Zlt4d-E&V@TZvq`fmBx>L z_f}O`b*Gb5cRC@2kW^=Zun3)WfB*q*H(?POXb=ITLW77=8I6D#mC>67Fu1meh>F@# zqNt+=&A1Gx6@mg9C5||wjymlMjyluqj>_SccS&$Y*9^HO&df0=431YHfk|j3q86QYb>@8)$w1 z-F8NR3@0tNnk=e*gXSBA3N8GAeuZi)^TWGe+l1A}e7NhFA9?O}Uqe3Hl(FXUS}O7C zbsi_)fiwH&j@2w3XyZz6A-RNvo~x;G zg5W&diPcyeqENSkQjlT}#=1T7s7hw+Z_*0}6zf+D^GANjbfI3v3!{ z+RkSTO!UX8Eni43D8S$MW8@%LwWfFz(Tv>}r?CzBb+5j4h$RrV;)gqPs=wrsZJxw- zXhewu3~C!u33K+T8xf)v?z)Y-58C%|lWdKw9IFlRT~j7^)M3N(mT&GRL4ylxYhump z1ii14@qxXk4aZ%u@gXSkH0A^3;O5{M%o;152&Caj9y+eJJ;WKKyjUj-#$&$%mo6+? zI+)@KOUHh>j>T(boE7gM<_xe70a%E)`85^6qJ?NM3%}QwIG=IwSM5`R6&xM!+NFrE zRRkmIeT>fK^8pQXKxt|6cMW$Of*GI#k+lb^Mk5)lKD8~vqlGnb>yLDPGxE`(t2&io zTku(9DB+MM5iT0}gH!|zo3n0^hzu$txQkzP5V(a7dEtLn#a^X^TB0i_*&KCP1=qnI znKjVpA+tiGQ;NV|w6V&2ghGR+Jo=y$tXs+9POaRj8YZqdY{y`UpL$?1uUqvN0bo{T z(%L!dQiz1{GN>1h{Cgu5lUik!AfK@Nr(LRlETW3uE3^>$XMWH^?~1V(u~2rCwSssQ zJ3?6xf1Pe*vD?v)7|*4zL$<-_cYA~h=Qy%)ml~6ZLi28+2z(kKM&FVQwYIY03Kyy7~`I zC-H8~0C9a)wbGF5iuvGJi43;#iPL5Uc2*C_s;?ZvU+Ui#wGnB*g>(=qpqreyKQx0S zafSvY4$IHwkv*@!v|r1)BU22HNeE36I3$PI41ULCdVBOD!p5f%&7 zs*{8nK5U5s(>2dK{<{RrNyVJUp{FEE8g6Xul`m0j0J3zsF2Qi=8dt=- zmnCq9Ldb1nO%e;$dV>?JsLXPik=t~W?$@dY@s>ZkV&HmTg&=9{pt_gwoDxqx-9ZnS z4a+EeT-*N@1;sFS<;%#{b-1?X};8@<CMViyZ?H-Q=601o0o*(U`9fyyDU}DUR0a6V%S@6 ztE`}Fbog`7iZ20LLVd))SZ1k`RM8EFBDd7vF{D#Db|GSrMu zq}S-J2V+}RaOI3Y>qe&Ll$snbhzs9A9bt>woE~0Dp=#sFs8kRFbhNRVQ&9D}n{rb= z>N;MwgW--~7ls_7V|+y7YG~nnD^4l0^k>Sxyr2992aNb?BARmL-R4qD?De}7}2y%^}`0o56!AKW{91iA#l53sOg5Hba)j3GQz)4!BXW`{T~ z*Du+SIhg*_za@HWinoy>;`ydUo|BDG7FIHzW*&S^mlpN_?j^ALThFr2zX0$~aZybLOsYOrC; zk|-V~@*1$(a>Pf=ID*e(Ta7d~JtsT$y;5%M^*K)8H1l?1Io^I1Xt=fJ^T}viwjdj^ zJStPd*YdP4N$j(UwwWY%Zqo7np7+vlz%H)oWoZkQ{T+jePP1VH#3_n`!ab=QK8a9+ z0h!m^Z;LvB;sD%zS)4rSY2W=0u{aq6JN7lAwh*QJI_APq5r6HU=QH(jG%!(4;!_SI zVjA0FvE28M67PhAdn@q=!c~MP*I?;cxr5Yc4 z6C*xa)Cm7$G#_c7whD zdYs^ijJ};tcxF}Ms|MeVjHG3)PbWD(fj&2QuFK1iV#%T}2GzzXOBPjgPd!gI7E+LX zc_(tI%ZFjJU9#&z819Y_Zf0sOQY_#$2kTE_58GxC(Knnq z*OST!W_rL+L=v9z|NiXoObv8Q1h@)Q8n06X#o?NC5=)6qQkvqSl?UT^k!4s*}&R05E?6a z{t-AFq>n+P>^Zb)89qR@iy&A4e?u-Yh(ir;S8QA#eJwPz{rNpoVsO!|-V&xJbvc+K zD;rH2rRSA436CX7TmTbLR)%9svUm;}VLViv$bir;moyaHyWc`mtve|cB{_upAac}v z8Xb;^0o+k4QXu~f+#{e;K$!90pi3syBGzY-``&ZvpiRt2QuG*j{jeZzvhaG7Au^Qh zTu2(d7qMGC%WGUlRVWKUh>P%?v+vnNiS?(*)kbvxEFU{-7~_A9N-3@Q#%5f+J8uKN zh4DVP>pZ!PN)>wmohh~m+-6Rvg^_X9WJnhZ|BzH4t8OxVrYu+v!dS41em2-?n@8 zGL!*cXD0&aqZ8CZ8SFNznWlYj8$Dv!9)!85=EKqgn>V>JN~?@k$`zeZ1`y79D*G`i zGm-i4ZB4#L{vGh-tOnSYw|NYU@YK2nC9 zTLFMHIHn;M%dY${3y-#=vB0Y4k{TMUSp z@v1bE2+kD=E}O!SrfMsK>rZ`594Q{HKrt0 zMOw7c+V^mhcvMdjyj94pM1R=4b+w&*_&)wE5R9xxBGT+kswB$kC~m^r!380acj|Y> zzYI%zYEnDIc{HJ22;tH^WsbT2nb82tByy4nr(NA(y?22_!g{4 zUO>aMQFN%!=uVN@jr&1;;9MF zO1QW)ZzP6R4cCr++8`tJiL<6!`zyGz_4FMtHxp6&nr&8y0~^xBpyt6|@ScjXgAP73 ziaSKs(>|aCta9q~AaiXJYnWste^5(~E?ce?A;zVZ+_VR3m<7&qSiZ4-djEd$@K9KoPn`vY)r-ldw&b^ZNz&Lr zdo1PmzncS5H;T)!wF~iNe)nKC1gvQuRUWqx^yKEHv~In(g=(S(ZR=(UD2GC&#Qol# z+^Vsf`0s!Qm2mLOFuX6^m8a8&UaVi6M+q0xy{LunMY_fH-ZQkSx`I4n#esK~JN95v z`fqb`v`0N?e}py`wFj<^nod{M4zS7~TCW_IdQhr34Vuf)rxqQT9t35urNdKWU=Gg_m7}vQnFor zl+mk{l1)Us+TNPLejn3xk;Ziw`x3PsW4MiKj9R6K!hK#30$e-v#S*2Ade-XJakqCXoX8c*=qIbEITS8yWJNKlmI?owA5LfZOGR8^LCb!@Pf$$1bpmee9te1$;V zAupvs;O(s0xjzxGFG@@eS92sDOmPmIH+eko5%(j@HKC?Q&|$c-NO31JHi?Q0m@ng1 zMTut>3AD=gDrwEG?Y=1`I10LdlE^XshHE+CIaO*AEDg~We@`|L+yln20z*$jJS;Xg zm5UFZ)FDKXyf)AeHoRPzJC1o0E)Rt_0|EyD%%uMaIT?Nj&7Dv7bW-1cN)Un$N)Msa zS90 zH$F1XWn&|&ngn&)B_SJI51SEZuoAT&PzYx^NTp*$Aw|%LF(s4y#${AjA6{1RE1X7( z3oK|jZ<&KK)mBlgw%b}RO!7g7%J03al!DsAIW+J4EAu_(EE+sXoy6vH)q8^t&L;Le z5{Yme!vVgG!(YwrbAY)eB_c|~63JvXJI#tuq$nmG53d%Ydo+$Bur=ii0u2&~80Vs< zOrW2DGY7+?b3Z(W(+CczV}NuF>*h@IA${BN0yclJbd4;iquLCIWJsv=BSfjhC4ORv z8s{magy2DnrK@=z6*Ibo-TJcuqe62>JwWx=Bs&df%T-7gRlPzTEdfNU5TpCot)Zjd z+w-SlrIkkt76t;O{rC!QlY*b(t#SS?T}__5Be8ZkN|`EZq3<_Z%n{5?xwT- z)IYpZf?GGcQkh9Kl5Y|e_p=cPh-?e07Ee_Y_?MG(Q31P~^SUmHit)Ye0+xDPKZFl4 zuJ>a9#NfNyv345{YTT8}E2p$KZ&jWbEAa3kgW15V@Dpml^9ia<#83Cas`!zNw$1N& z>v(4|G~zb13dKlDp*Dm36%0B#nd-y8#nnj`sqGG?ga-Oa-t5@WB$xvRurmdyLL=X_ zkl(!|zd#ek9j{I$O*Su(@N5AdBOTM5t7bHrj&BPZySngdUUM~{XDSmW@4Hkvn$bEw z5`~El97^4R%boP#{#2)sn39Ha*DIETrpiRV4V^2U!NW;KvP%vYsjzgFDxiTR^xnKJ z!?flt=jJELUVC7Z`isI&+4*SeAc`Y{fTB6GVd9*v%x}|r0w(r$AMnCG)1rWmkJ1pq zHPd`4$&e`ZR^X{W_}P~fHltyCb|}L8+1CU=r|b){OMS6ob$>}MkmA>CD-=)w14;ws z&F@>xQYx)I@Tr<&V{Pef7wXM$_8l22I)?sh7)kRp3}2t|yT&w>=mg#aQ}O-;Hgz4g zLC<{!B;_Ic8B=aEX2j6Np7B{V_K!dxoL2xyU@U~1zt;Xp37y^n;7obmW+3@da_o#9Wa*Pfthx5qV?i=jw-0EotlbS#5C=RgG=niN& zf$OeZqErV@=<{@thyfsLI=l2`r83-Ac%ItvcjF5@-dG_ls1hWStjpzRS|Y603X9mt z-g7Zx(g~v{VI~L;gO)@F|HV9+W?cf-7R}I1u--iN0ZMMZ5ibXqt8m4r@{%{V;$0Rs zA!$Ezss`hxJPB=1%CFh9*Ne3EK7guyGV2hj(9> zM_s$~AU0knOtO%2&NlCK9aU6sJ_P8pIU&e78zRn9@C)r0-i#LPnb2lR%xi@8Pj$ZB z2{x$>UJ}qG5FBDw-8dt9KVKlrF_y$SPS#)j%;q2Ut`D(2nWh;HVlwuoD4{;Z;% z)$NcnCSv2P0|s@N&CEJ9-0iY@W6S7|#x0RcZw2DyT_xAm9z&?rKaHX=Ez@>NhRX!l z&*PJ@&}8$*P1Hyi`(4i~$GmWnOP5Q>MYa*F((;yxNao_H8;$M;El-#(i;N2H>wKN~ zTy|-8?4`*PZ~NEWQ4v8j*z9_hremMQYAKGUSP{d8He3~5N{Ayq#P@ubpClC8t&mD& zFd7+~uJ2u{=NR&Ka_*4~k z+)KW`dbXY0usA+kUX-!4iRR)xk|_8VFldwr9|}}3qIX3Izp5KjhOId9j2j%vNa1-@ z!y;E)BPK6GR~)qGBfR-px7E94D2wnd1?DI~>>3$6?-+EQD=dXdwSH zh{K6z1=>-1ZWjq!7fzQk+{?Rt2^LB(x6f0nzk&MGF6W#zin86DEpkO#0tW2?sLRNp zPdzKQ4}o!fb-hAvwiE>eflf=- z8Nx!>A*bp3SMpv8hC8q4|K;xq;}t>P%IYW#77*JIJ~l1GU$%n7amn9LOKtXk>ORZ*MOxPZb+w1S?=we6w7;3&l*N|J2O2M#vs}Oddq{o4)epLvWw-ozA}bW z=v8C3fKH!Jd48jsjV>i~N#;Zzq8D6&wtqQ8G6R8{-%-Y5ZxOXRIgWn(eodPK6^%c9 zkuj543g@0JkxzoVa>m$ADn3ErE%6W1ECT;b1iux{_6l9jr+%R0Tuh8^@581B#p=*jZM;RJ{zdlnXKK78^gY+rlu}LR`L+ZY>Jj7yypDA`Fl)(izoVq}7UhHdv8@6Seno$EP1!thogNeoLifVWcr z{9MX@TYB$nqc=NS_%xgItM>Af_vtjUy`Hf6j+M%iz3t>n~ zOP+l-Dv0|tDLK0*EhxsgOk|PM8h4(r(TOm#2D8T+aT}gkoT&6PRT=|Udo*7+d?^ZU-B z#rItZ*@pj9L$!Dl6&E~Kh=@Uf*L!L|vm~EULu?LZHHtyXKUf|PnANBxxC*`uP@KT{{nqJ-0al3hmo28Bb)p@8Z74UXtvg+ zLy78K{HJPW98x-+2GeuNGHiLu+aRGqkv}iZ9^S(S@PJd;&26AQll0>DJpx-+b`GZZ zUi0I{crA$}CU7dQK}zYu^he5I_AR_pkMh{2b-5Y w#?SFxB4BOF|MH#eWXnLz;5 zfbzW>yu+QNAe@imZGDv^(I2!qlR=59kGItIX)^GrBOv*Iy(>J>2A zh3XYK!)3Q$?O3wRfn8i!JxZSETy=5oRmg^Csmjt`B*rNQY-3svIEsC=bl^sjE}!&sFTzpTLBy)&b+ z+a=2XaH$;1|BF#+&bwFnGDvz@fIWa6VAJ6D%Po5OzKIG;8UxC>jjQ}OX_Dx7(V(V& zpe-)?7{4k{#t%y%oU_muO(}M3wuzq*|BgbF*lKh`5{eqoj>X;d9>a zBB&$VJX%5*yS5y0JV;Aq%t!N9C6$g=zpW2 z2hrvPx)Q!1yM;kGPS9N|8l9Mil8~L9M-N}Y?)lw)Ns3u;4vO{B3l~XEue_z*s$vs1 zw6}pvN}8`wFAL%Z;3>5#b=QrHicBpS)=QM&LD6M2?_0Ic#<{BhqxG)%K*ARIJFkI5 zgt96-2&I;TuGsBXsu|bQ&(<8qZ4U2V=9=#0Og?AxYQv(DhuzlL1DBvi*{|00$_CA{ z`WDb}K8D_sq+*>7cqQ4#B{(UNQcIj9e{Yq0bhsfpj;s|JHZqaD(|3(t*^-5)tn`mw zeT91+OCBQn;+5q$y5Rxe2+rZ-+7&eK@t-Bd##8Wy1ApbS3U5v2BZ^o+b>l^e7^aUl z!kugNd&6CUaS|CVG0aoi)7I_&oG`nv3!6weGK4{9f(}pwQ!X2NJKp#r`F7-k-p)MEMvH)AeJEa2xlZsG5nW{v?tJRVN29kOZaX_o# z)Lq2dZo8RD+w7_(50ZxvITDhlYjRW zhp}UT62RPHdi=&kYx(cFKLLmABV~mIKHAA2_>4-nbga+?iGO?Wq@h-~Kq~~O7A&)| zi(hc?ipe%oC{`?x>$=2PVK6-89stywh!lQ)%$UoUA?f-pO@T{O(x5$dT)X{2M547X z(%qV5NhVAKL~mOCmV1Gq210~2nCn@jbyAC%s?wjdx{5H`)8H_N&7wyr@h3$`egxKhsN(0k8nm8}5Va_}jGu z2!~A*oFgFtm?0_jGh<8V{#u4JEj^c{a})CsFQ9H^#3oRQo43>C?knOAg;cfnK*@aa zwc%a%%XKp5g_qGg!!9@x$MqgCtJ<+5=6?cdVvP&@zCs0_RUy*U&w!y*W4s z)KC+v)OJ`=n)MtI3H2;aDl%cnwN(j+SZ-98!xR-dR9LIBTj4?pC(55dua|Yz#v4gl z@yW-@diDf@0l>NQ0~~v{bg63qMJzO;;5hj3^=xp$n#5k4zEa0MzF|MU6-OL(XX1x$ zxJfo%%jmr+Oyo1d31IOAN%VCEz{VK5nI%$)jzZBCNHpnJ?ja97@B;4IqL#nscM?ES z$I!UtWZ4Ln0ZVxV8r|7V<4~{wjAZSBam$6G%7rusQtN;H9LDe_kx@eqYwDTJ44c$1 zT^MK1_MnP}M(?W5T9(D)OosbvYrs2N!I!m$A}O-Q(XO&h5^zi9zJqg8^$e{b$FLIu zTo4i~KqT2%eFU20V6hg^F%6=!TxfWCKIzD>KZ{tbfPNILO`D3}zcl}JA(|XGww1^x z7WeT@)Q$IPgiUo55o9M+TZ=Iop!yO$Xz0qgk-rhsMFV!2@k0|N6x#p^!D^3U!V*>6 zRpSTTtVn9NeNi}rn#cV8{ z9RHpI52(+)`|rKNak#x{vih^5hKdq5bcBiT+&sp{(#BW3O~AiFnZCo6MR-m$=q0(= zQat-5#ig;}%wgHmNK1yEzFLueLy&N8Q~DYVhZB({Ez)*WL}}s16Q;vG!8V}}!Y23z z*aQM))rl>MJ6llOI=i_7BNr$Nd0oll`KqGzF<_1CuX(1(xFJ>f& zH7f%5I2w1TeE3C809V?$X1NVV5l-_2>(V2VLi6MRJhu`1MqwkkX z#dll)Yi8oV7Z-JA&Ls-J2w>c0YU-7_T;MH_#LG<*-_NR~G$ttTRGwV?PLRi+i!>7=Fm<`Wq`~adO_$09@n7w6F4Of-6?igX5ufg3E3CZc{cd#3 z)5VV2XHsSF08PQJXsa+WDN(>f-SN@JxpB-;<=tF3u z%O`D@i6~*ykBcR)i(b=b=_`P(Jm;lSB%5ChqzVqZV$Q^pfJ^Bsil@_u@Mt}Cajm=JJ~@q-#i6r|G54BAx>7HVu#_W@dmW6Fc`4P%~?lq#2C z=@8_1yZY6XvQg|?c|=`lG`Un&8sE}8npW$wV;~E$$wEhsmto`fp&=5oEKo4|0SF~z zo6x62)^il8-Q=x-cMy_Az;>u8cxhhCyPvKO@vPY_*`OUY4l|gEbPe1Pbk)vc39@Uu zYsmVUWE(}^dBSBY7<91pI~V1Wot#caIzZA|-+Z`NySTR_o4n|fvM?MlIMId`&Ti23 z`FisFht}0lmMJpm6k^pHu?L0N0rd9z9LO_#g#P3~LX9>%>|1@kIamdBL(Y&8a=dY_ z!GoGHp%o(DEFn2;J(a}32=x!ooF7~eZyIP*1Xi3?qL|xmY(YPJ_5`X3qXGAxlUWzn z;<}1>jR`mFvG3OUrJ#E&(EOC^vXY*N=F1tURaVW&ce}#l;fKsS>>Hh~RcCEGNbvsw^tMsiga{F+BKz|~10@GgJiM=t+soT5*7qZkWxt1xA_*F$ z61S-?rMY6Pnb80UU2-8~8TxUZ8?C*Yz!PR6tK8piM6&8iGFx`3d**aepp`}hOh8d? zr*?SC!Us{dT7RU)e8a0-zsyp36=<+v$G%x(EG-OosUYq)V{^7*_!w9~=nw>NA_~~$Ubl&c9e+iG_7h4^wR7u2A87mqPunOfjI{@@T5j8o=xSp<6G_;VCwwH3&9Jy64`9;;L#k5vXod+adD(KY%`3i|NYgUNJc3Tj`b9-g@X*Fo{xFg6eOE z;Drzr0mvag*O0gfPHmN^ z?!306G{wF4{q0DB_9}5rj)oR|%5#izsA(O<_u^*hlzJ-G83|lae{rxK(^e3$ErBt# zX*jHSFcmY|T6Tj*L$3QK%R=uOlwZZa=-G@9!;_avhnPYKM*!YDJ(d-^g=(KxkD?0+ zyUcU`A7E&wd+j?P06X9j8vp3cu?B69pr>Z_d9wTibiN8qTEuGYYjk>unF|mjugl!Y zZ$$G2=n+E~u_m|KG=R=~>g;QBU8ZR*W9K&$X~`9QGm&V?Kg(^N%uF;tv<5HPzoTH5 z2bg1VFPq}+bsKMGUE!N&eWbQsqno98A@6vD_53Er-e#+ko>+}Oh_6$v#jlRwokJG% zAwp46eBg&@)IupApn19TJBVTUQ@(kx9uyf%craEm#obt|6G`LQ`i#kI>}BNiB-Q+* zWjLOd3&J~BMg`~sVA`>@N%s6$2@9`yF*4JN2gomV^3}WO1=#&G9EkF#(n>zH+oZhH zc^C)7Sr4*agQotLh^5U&;m2F?>yYhpu>79M1LGqdDlH*yB|LpT89%+Sy}FFz12#FX z%Y}qmps9rJp$o|SD>LWQr9l4lIe8%fl0w1CZ&0@d(uF7vADT&nMrN)}O%Bp2Psw%6 zh8AjZ(E!|cFCu@GltK%cTtGwXz*NGnqCsXFd4l_?6;Juo%}~33_#)G`Iqr2#;39Z( zK+F4m%Gs4H=f)bR5;jfdHK$5Z0}$x1r)x4++q$)y`u0=VbtNgNHg&|C5}qc~(}OO? z*Jt+cT92HOg%FBWl5*fF>dHo3kpzL*3!-Kqg`T*7i%zmPbLV!bn+d(3o$mK7Aw(zP&V4yWO(jGf1W z=9z~Xz>a+e-mG!}cc4Jgnbc@}^5`RitP-^Rz!N$FS+;&d=MX6&2YEI5P-cQ&;&(s? zO^8V+6_qv=SQ~;RgWJs-&^ku4{F#ogOb<9rLH_Dy;sQQJdm5YhaFaKcuiF}^RNyMrxoHMuR0; z>*Y;U5X@35IohB>w;CIASlRTU6JOGzNqh12HfXKdvL|^+WEcx58IFEsTh<-MTh;fXPvUXr1>^_Ij#*=Ozr5Oe^ZNPXdI7kIODv2-At0y?uxx-(;HR%{&w* z&pT9P;AD?Ke5osr`g3Se{&;or_|C)Y_wkUi%Qa974H5T+wMsN(kKm_hCM5|^%l2Uf zD_luj)_ndK+Ej(QRT_fH2WVe-&u9-qU^5<+sMx?|O?0e*424J$Z)3U_!c4>^@G786 zylR+7@r_c)f*<#LP99C+qvK_5=s1P9=2+?C(9EVJT=C@( z-uttNY8>hiY1qTNSF|bP3mh=*weR&26dG?n<5sr~z@wMglm)Y1~l zF0!u=g7&NXb|N)=qVQ=g1C4Kh7@}~CYd@p+phG)GEKW~;_oZmnWa$^z?Spoa`_vOI z#M$>JZ%jDbSH`)_csR4*F6`!km?Y9Cd* zVW-|~Fd^N+MckdEqOj$T_iH=Z}dQpzin~#rt7MqI;zb>-F|BQ9<>NzjaW_M-j zztq_a#89m5J| z_D6aQHZTvH_YE^E0%eZeQUL&+1#rr8g2?grxNP zyUIY5KXU|6uI;wa_e09&{A~nr!%&dOW>04##(eRo)yK0D537G)`BsEKQ_tq26}LQN^rArKb(;M3ogYjt(+nm9xw& zgk3=g6JWi9YS{Hq=+ta|2(ahd?$I{puTW^1ZL4gMsZ7f_k#&~W;S0X4`)gE4gTrnk zU^+4qc-`UjTY6z9D93AyvbVm*O4i0P=|&yILY~-igJMILJewcfr6!8XB+agxwY5d( z!y14NQoR%-1ggmf?WALxI1Ld=FvGzFmX%Pb<8}Fx`&9Ps!q`rj^$AfM(Vh1swIO*B z?kht_M5#@1Mp3fnpg2P?{1r08#3b2_?T%B-=4t)h21Q!ba_8Z#PJVmS>lPSxsxCh5 z*E%F~RL z0k$qJ1i0!R+u=JKhphGlB+S-j>Q&x3wUnLp#||nV?i%Z&kF(;+&^R6@*X09b(sJW9 zg=1MLH31l!m-^gWrF5q;uKn^N@ZgK(`7wZcs4t2R3jW%SIKXWA@ph5k}P7x+J!laqpA zG7l*Q^g%5cv=n;?bQv7-5x{Z6Py$@EB1v_`@ii3%%KKJ~P2zxoIF^EXpH6WYUdk>i zpvhp)ms=^0FDiTY1CS4=_<0J7%%wQVaG_F-Lv}&lo@2J9l>meMw2qB)RR8&0O4=56 zOY*~noJ0!_m(BC$BSiR^+9aeCDO1PX`{ZIOwSc`wabS9WMUZ~!XL%M>xbWB;Ey?g4 zfGJTcAR5!*Ptf#yY)ry_$0>Yl8khi~LX&@VEvFaPb@s=%S%IuJODU{;gzT`HFqpL? z`W}ty)iUd7da!}+0VaeVqfSZfZA{SfIcn`edfd%6d&#=)8!^=xdq0-qpHT-!o)DNa z>!oiSP0edgqEcSvdT?!x6AUQ#P-NJir?R}PY`6vfBz3|(ECeaHyqCt-Ly-8Tle9gC z(88wk$J-Xdl;U^{xp~^gDxdvl^kekL8~NDoRw45{;do`6>#tWgC+ zmhgBcK7#hgf;VIveRNdNT9+LK*pVTQn!#5E8%%eKy6aimUsll&$G!rc2NEUJ5&jl` zdmd@1Ns;aSh3%6i7xBhJki^Qk2qC}m^4%z#bSO3)07xcw6=;|qTu04YTvE40S&o$RKmeb_o@bZ@?o+OXf9eyh$U%JD3r zXjEEG+k-YBB;ya^ z#Bl3N1&u<7jk01!BUUMMS9ugWeC(-?yX0ZTUkg;mdd6Cid7{^q)*fZ4gq`orQvCs) za=;KcrWo=X__P|n16(@8N}*Yl-@ulh#f;1 z4lz>wTWspl53~h zpd-4ne?+3NmDY;#54 z&6VM8{Becq z0!q}(My<)I(45PI-VXR&B}g86igpzW+9Z8@D8D$ zysZ-uJ#x$YYeoUAg9R55dO@0$q-Hdq4Ob&x){88Qso$g2+1ZaL|EUlH4435c2Ns8g z?^gtlrJt3B%`HeLLrZ5T&8SEX1R0EtSw9XAPbZqb!I!~snu zrupAPEd2pUaj{E(LB1aKo+|$Dl_Z^%FD<1<8Q_+5*7@^3j?jN{D@^_v&4r8a&e zLOD?DG|3|E2O1GVv1UFgKfyumN0S7LL-Ckm0qLh`QlVP;G#eMF(Rr~og(?OK^CT;^ z9ZbANMcxy9q>d}DqF znaukfE0)`-e_`!Xz@(O|#r9k_bfEB34m!cI-O31^5nZBWpP}q)y&(%5LV^oO`^VXs zI?Bg>8cM^pEI@hGyfV#fe+eF@qBX*{Xy_MwNPS*e@y1{9pq>wAZ$nSKSiy>n*BpxR zvY`AA5Nvj{SojZGqUDoFh&nRm`Bbvi)t>_Sc@B(QFQdezG~%dS>_T;{Iz(j|RFwH8 z2ib*dG=KCxO$eYWhRHWviXU1?TgLqE-Mkp!Kpr)zFHyyO1N}59`;kKu-2|bRct2~9 za*Y(sHY`Z{1#kj}^?k2SYoeye5GvZ$56HnkvBhqDJB8tomm&Gppn7^KU!AAT&Q3~^ z!If*c07wHV6OCTrD}TNp?@?9nAoUHFd7+Xce`Vz}7Y|m80Zws8+0kFEX-3wQ9PQ1c zn0EXIr5J!39oyEMlfXz#MSg6c|Iv+KxEFVp5rkRgMUB@PAQ8WruQNeKp@!8lip;-U zEmm|b50)fYvga9f0CvzMXsp0zt8>X1`T5;ce&24%Y@sTGHU?`uXPxlVIr2t>jENCYWSU>B*X7fdL*;4P_ z6@+nXlZvz_fK3#bGiqPN&gqq3YV@3*XS@oJO6=f+yV8H6#a2BO1qeuNgIsabg?ZR3 zk^%+3ipX-dzAP7uI{%LJVx>-BX9D8gT$57WfHlICao_83ODD|xhLB1uuCAj%EKA*z zL6w3?P7Xb$j209PDx{cWXe|$x59C z^S`dFuu!1^(A09NK`vfViX%&ujLvlSj#`Tp;nk8`__FiQ?gyIHqny(ks;1)99l8rP zcZyM@bk?~7W|LR>k23B?Z@P!S;=)A)AzX+@N|fPlcrGDdaupER5j9Y8p^6JdV361| zyuaf>yKCYvJJH`|;Ux%R2OZbxca#%YjL)Eq^SnY@$KB3J*_4cbzo*DFl#AF0e-H^*3g=4={V>=JkeCCU7w zrLhDX}I|_6O)D6>%GT=eyP1wI1pE2pIAGvmCpU9$W?oAH|wuLR6nIg1E-+)ud zeCWa7QJE|5p}EjPU?QM){k|Ss!dp=6I`YNG&?q|`x`W!~%h&}hI;u64q=9TZl@GF! zXElRR4}1r?2$dv0-Jm(QN(N8ua-uR^9&hO<9Vg@76z;cyagk3FZxq7w3|#;yL)JR* zulqP{j0mDjdqe^^ku42oAcjLGB8VT8;m}wzQTO$Ui&-uTX$D|BS{GI`Rf^j*$y9}l z&&-{E6oU&QWRolM704{Xyt~IOTf8>3){+zn9}R=%VUvdw7m)Bf;l(gPC3pO#6Ws{v z$E?yC5Ed+e>j-XU1JhL^2)HKY9m?3r%0#Em0cZ|RoVbgukLs%crGs%7ar)2lx0%zb z5^DhIg2SYh%f4sICSDg{j`4z3G>^PTEt#*i;}sgp9W-tF6^_GZ@@Z@-B#FiOcq%u! zhnH)#i1Kg-KkKHXYvvIoawzkw?rgGVrzWtv2U@%c^dkAxKjEfxmW!tuoUweg>Q@pYb~nzdDbIzD;f7?Xap4YvSiAs&bC% zo=+Hs@jf2p8Du<&?aAC6`VP_7ois6Q8+W4r@itoeD>lAPqtq-$3NnEe3z6)D70C+m zzUKAKTk(#j#epR=vU#p~KQmGD^rH|ko5{uqya8Zg_(w`w;cBNz*iigJiG0>lM>o@y z>a<0>)gb1lP=7JQ8O6+?w|}6ohax2arKcR30A~ZK#VB?r8d|N4yh=?3uJk+|;Gq|Z z&E`c#i$Q_HD_LWDt|ICH2*E?gYO^V)Hb{BZCwW4CZR{as={MTvs0N#sU+N6Fw^+q67GT3z zC=)zolvDN-8ewmklvUZsYhX8dpCj?n%s>wi;VNMV6e6!}=rL3o} zW3EotZ$YS7r2P96hKCI-_q;hBiX=6g*pLDg@0Sp>sifOO?;H9|JAm6XFR((|4xZzA zp1%iyFwi2t+ zg8I4ziuN;)p#f~h>V7UWbsRomuL@43TvK-nJ98trH}hOtC!|K6oNq-5^_w1Ir6TR6 zx#iEAhI)VpeZs@@#R3Lfsx3np!3AOT)i2%7ET&wBDeoF4=Y^a(Dbvt05<%PFU?hkf z?;&^LGTPk9rHWujv7e>U!a+l-BY8vaT^6X6PLVQzMnk*cQB}&akR6v!d2Les|fFd_gU1d-F73c=!2V)`+ zfb+YYiQxQlXbV&>@R}9X^qTYx$*34>gEWtQrc6c?iqWaKH__EIe#FxO?1heBzhyFi z^9qffo2XI|xj*QB8KwL(kc&A0F6~L(838@GhCZTchKH|Ps9PfO zCM}DEAxGfv<*Qu2rj67e+1LDZui}@VtN!Zyc)!lG02b4_(xq~D4^?PrsrJ<3X~#&P ztZ*SssSnalyF>xnKvHQIr^DO&C#2MI&aKNlr+ETWF=To;Y48}l9_XoCJQQ?c^D7LU z{#a4{uDpZ(jQd2^15hV@on^&jHWovfmQJ<3_WOEm)$?MsI>bUoU7ZiOS33*!AHLg5 ztBOBJgHS6=<6&av9TcfxOeVxR_(5fDQkTTAm}FsRm#>X@AKG?D0&poNbeuU99cGCB^2-}`Yqa^h=(ly1-k1vXJH&zgme@qcmm=HXFP+5hmlx0bG6(p}wI z=q#kGlO`k(NQJ}@Fp^3qARt16fDj$*AYgEfpoo4|Dj^^;j<#%qI@-vNs6jxS!5NFh z4MhnqBaS-Lf;#Fr-)TS?7c}pu;=IrEzJLAx|1r;)q?f8&=bn4^^EtsxC~deI1EHA4 zi6W(nD8@^U{7x$ov&#>Ond%x^SNBFQijF)AvyqT3f+`xAf_W{^L&#zj*niu=6)Aj` zw)iaV7 z845E>?eO?<$|zGh81V-l#=~a6 z@b2fRI#HnyB|jhvi&73?(5jbF^*^J*Fl8$=Q%w9GKgqwuYVNg^JXQ@bjhV&^#`=JneaRz`TdDA{9z^AFg!W@>iY3) z=yH7o@kzWeW4DdFm7Bkeq4^&K!fG@*j&+&v7}v8A!V0qpUjF23R-;M(N?Z76CGUf4 zorHh4KjTJ@vD;GM zRr595y6d{1aLAM!y8@`&vYPL1Cj%b6tYxQ;)}HJO{<&o&xqSPFRwjuzs8FxeLp&N3SgsA!e(q42$^i8y|W zG{9CMNb{FrfC{%&3`%XPNtc93_zC z$^NrYR%WCHr2Zl)O_G-}4Lni>WPF!y5P!(gHN1582AtC4_kB70aW*!zd#gB9a{mG? z@d2+$9!1w*0IijM!j>;BJTke-yUm1fCE8t0QowRhsOpgD%2L(~4dI(-7*nOdrZFrO z0a$-#&I>%Wyjyk;V!<{1VQCu9hFjRQN^$tki5^F{975cL6{+lbp-*75un3x+ta}^0 zn`&6A(-z59QQLs~`*pO?gN(u-;^I9@zsnQRyj8al0wrpw6jPw-a;ZyWpY=45Nnf`t z{Rw*u4kJq>$kaTk!I+^Lv3N;~XkM=3jyHL0YkMG8EEvQg$fM#?bT8cxI%|&oW1N zt9)toHg^%BOld#@Sc|9sO&Y*D$^MtC*b#2Wc?OLMKvX1rhzz=V2Fs(1Z}WXuPDWvy zoiwsJ_l($s&r-4!-viG{)9}-mOT#a=(9>(G5X#sVSP~GP6dVQ*wdTg)hVl3iqJr4% z*H;EIXl%Y|8@Q=9((pN8oe8@=+(AkDZ21LzzH$BwSO(Brz9hYDd~^cU?6M`B zjh0=>Yu05_MGSoBvJlVs`y(cJ*>kWZ56igi9GFBj zkH&VFypkdrS97jU5F?jiFDbedXcnR*Lg_VrK8Hr~(lSaN?2O&PcKGYvyAU|{DV zK#hwp{b>&V8yw)^F~Ra|93RGL_I+0f>MTs`$>FV?=6YMbWC$BEK=Wh@A6@PCprL_H z4s7Ek*XmGgU06Ifs`pk^8bO; z3|Zf8cyKTIs&M1k2chNWzT)76>?tk!qf5c86&$;jm1bLpZN(>w4xiN(xackeox7zs z1%{Ef6OoRJ1)Cv8463P9MJW=86NA}m`0@8&J z(kY603jA}v;l_Xnvq{=6X31*TpU{RDIjtEFNg;%r-Cb;TU!BSkP5;}Rfi`?yjLYR4 z$FHHPIlE%hiLyESnbae#QvF9s+``kvoCqJG^5E4qsZ8>_Z=rC*4b80fhmM^sh#(U? zBo3E=&4~iKS6wk%PEuROQA0l@_SS)El?-bB79m+PmyJN_Q8Qcib*HI|v%6qG`DYD8 z<@==;GQ4nQtJw251yn{$yN0|%t~jquQ}vxpiyWWFD_n>vWcY(WN@KI6>(h3NnS;*| zR&%pEDxnIqA&G)fY^wi|m=X3%pTPL}l+t(Gu^-(t>_&LrAEBPPoCc-#pj9GUh6j}q z4yIK}t3-zJkFy-fW5j`xzG{40UYF61xUYe@OAClwpaei7)Ho%vp+>=NHwB1qk#aPz zs8ykDp>peSZ8%K{K0?zQzn9Ev(lCJbHf~?GJyvJRU=D2pIvWZU> zwLBFNl6>CY~hCS+$x4GVj4fRxznF19)zMDWhfj9)6I3F1eVU`Jfl1)1nIa-;LM@ zVrz91auK0^7y|I{r@^z;@Oz)ACd3 zijiq~=%SkaCTsvT2bYvv3w#9OxVhiPkkp>xG?f@w-f<#TXOBD*{_HGmDMS zBZX#p=@kumjMWh9!fHEbcb;ds@+PG?<*@iRe4@4sDRzW;9C+Hn?;*_DP%XsHZKlX` z$eA~c>Kdx|u3vX;>>o{6I6S_?Y0Blo*DO zvW-%v8;0;aMe+kk45(5NAPw40%|qCfkE{i+LdSrji?U!wVaGEltOMn#I5`+L0#HCu3GxQ1PAT6=YIrHK%GU-;U3Z~O6dRo7i!lJ$G_s4+M(KKa|mPdTPm@~ow zoJIWc?o{qTf*+1O?GZ=%(n>xAj`AQ4eop^@BUJR{z$a9&ZO|^W$;1fPIWkp#(J>zv zfqgy=$*a5tOlK+JG}Pyw`T*t$o;mDY6Qv^em$z^~!g@J)Vm0C5FAD~E+a#t;&w|yN zue}Y-&=I>poc+9nZFF$M29F1NA8Y9eQH2Q;8=f^<+J=}G%$B+=dB7p$Aa>{xp|Wsw z3?kr2w(JJagiSVA*e}Py1c;1ullU5~^;mXfg82ALY)>f<-Tz#V9NV;$U&QM5CG^Do z%aMFxaWX{-K8KSxw{uyh%_A8wtynL z=Vj8BZAi7DKM~g-01i9HC6j5m?YOVn1R}a)*R)0K76LXK2&!N?OwI{v4#fQss9u7KAE z9bn^r|9cHrYQJ*GlROZ-4vC@o#|HGU7?!o*y9xn68)(~8a#pFZLc(k>(+j^5C$o!$ zAJksn0B=OS-45u4!!iB{*Q_5W3gd)jacq93`g>=+NiY$Mwnwiu594qu!I|jeCRH}}#X0y9Wc`S#KnVI@r$0SRuJ{G^cS%FMfW)8Jpb{)Z4Z8SLE#9)t=;0wq+K`Ir zleeR?dqsGo_Bx~I#TMQq{~@7Zeim}>p=^pc3)k!sVEmUz(_Mu+bBSx6A%f>fJEfru zqWNqJOaWYZDE-+rd>)kvA%uko!KE4+>Y0HdJI~M~0o*88eB2{ddHFFXa1?xl$?3$< zEw~`QB2oZG+y5FX%$jGD-d|!P0f)3_Xrz57cL<*g#|V!MpijsUxM27PK@C-?>gdWd zGH4W(dp@C!D%4X1QQm*OEard@u4_f)y@b3I>`VV9fD77QVZHq`IFrWUXGJM)5Vda= z1?+*JaxqXbWY{mkq9_f4vJ*naG)%>95iSeO@`}+455CY4f;HL{f|VH!!5Bz}lC4r5 z(0e*Vh=XT`v{q?_jqn_;V5Qk|BN$q$5KTycLaTuJvgL}9H5TO?xQ#MAi3Ry?XPI_& z46D#$1d`9;q)kR0{d&h19~o z9A-ZqpJJ;hFmR}60FdUznKTriyvlyWzl6}T_ly0#yFv(N*}wfzQLqPOE-?H!PrsG% zO2~fl;)=p=VasOX;iC5AHB{J`8jTg~D14hx z-G1I!ZWvx9s_@YIK;XvN+o;z^*|+<7J+cd^mTG48^_=a&BqQ6l8zrn~{y3N}0<<~b zB8p6js6FbRrH0T0uwv^SodGY?WCW^GUa>ZW)~|*N4b@W(lQ}(52~PU6P!WR37%c?Z zT+dO^c-cOzdB4)-u70}I-h!s7e#(rgH`JbSqz@DW6oR~Js31ItCzO~PcZ3NY~k>?wpU8s&^Sw@U|ujSbFl29zoh7_yf zDS@xF6LkTU2UK|2bt3c4WIkUB@j^x5K4340%<-auk*N6Ac;8QAamaU*sQMhcD8zu#LK&<*v;S~@8G@15R~>>lidHyaMfWvPn_Mq< zVzZ2TiLgfvA@eqe!)C^a?|Fp}$}o=Gx=9SV1~`U-3BL@P_E~{b@elpxSA9)o~F_%LI*5WW>iQ8G184^I;A*jxivskU;S1E$@FIDt#Alv=-#M>Xg(Z2Qg5ImYZoY(;&z#d2;6mXW= zR_xay&HfX1WmyPuAUTBGb6?-z@x43n;bp%rct!t$<$8~f33ztXC&Bh(9G)&Kgu zxMJUSlnI2A{Z9{sC?w*w%znu+Of>}x1-j+Fz6rlR!I6+rP5JdI*^;B;Fo;#2uU+bZ zTpblcQ0$jbwMHKMAw$@M^)Z`Lw6ULO$NYC0Q|yU)!l{SYW2?KyUd+G=0f!S4u3fNiQMsBIA%?faJd>KV!kQKSZ9 z3l(--(;ry7&l37|)RbRl~HKT$xz*O(9gG+z~=(Wb87#;F2@8 z$mzIGCz?cYvbNi5SI5EG(rAhOaq*57Ch14kcGBZr3gf)LcTr&uuEWzMmMgE&~z29tO>6mC9wmyz#v=5+hvRCh3h7OOy@-}h|gO3M3&`6eO1O}EOw-?VMe?VZ_@tu^} zHW&t8GI%4qISiZEo(1DwNm7#XoWq1+nn*z8L6s@InsG1)5)6+u#ZeHSyOJE&!~tyq z6bTgO$>Km|F9X=W;1OKI%fQxnhCs-~0lB3Xq_)wg28Pr-$Y%@gyve9L=J&Cn`7(w1 zy3cL%DUX_&_fO=^V)Z$u+05MA!zLxMPuwt}3YFkgU{wP979Vx^#g(*7MdC{_okhgM z9utX6el_EmBZB*ms1Q7tQ*8)19bW2|+*8C*)@&Fce--Td3Ev_q;5{IUk5m0&ig-m? z8A*<2h=e7APg5ZGLs-2xq$C|4R{=d_7g*r<8;A|KJ`^*9)$(N5&WY42`A|t)F{A_L zf1w@Qsb`t7Y@Tc73Q`ptsyL6Xskf5(MysS{vVTGOq;QO3 zu$6_J)6s{hDGcTM0jZRQvKkc#IT*TrtyTJ6&NOLrlG*OVF= z@8ILjY+#8=&nnK_u?%Wzl?E4z_lj6mzAa+23=EpQSimtey#y6hKk&zk{6k z2BNq_A0H${!HS0{02Iz?`&_B;SO|c1-=@M%29yT!A?T*bEN+#KaS|Sn>j+{Qv1E%d z@Ll))&stzHlvbN;()BE$9pisCaoCEiD0!>{*sea=Guv*)S-RCdz{sbfg4&B@I04>0 z4$mFDOVkMeU{+$jns|+h@hdp7sM*!+(>LE+jiBv+h3s9`|E)B#93dvJ#eX)V0*O14fk|MK^!sYx$ z$Vl6neWv2tQPW03m?7hD+B-rJ7)b%*! zVZ7f;Y@;$!unh)pq4&1(y3l@L2YP$h_YMTxME7j#2v!1gPR$7 z>1cvPj>uOg7*rFI-PGs&J$94!oH2+r!-?b`B*4YQDcT|oDxzA4Fu=S+)Iy=h#qe@d zV4)y*U9;_wV5(hgcisxZJHCd6oQ?J5B0v_@eiFgT?O?cIhd~*IcB#VD%*xl%7=pH% zQM5+D%+s%q&Q5B0fC=QCXIE#mIR0HqrO69I2WJUZ%WIKaQnf64(x^4byMhSO1{NxU zK!Le763|p_q!+BS@@ecMb!O>r`>Ba{g34e(7#@k^b$9murl^fo9|B^VV-e#o293D# zupH2a$pIWw78EMVM#Bt`IwtHySfFSX_lfxW7)`Ul%1E5BUj&3Pcfi-s45*9-FmD{f7xRN^_>ix@)k7DwbMk-8uX=){Sfi{u8K3M?Pu z?0>i80+F%dR4uxuiaI=^>&cFrHB>-MvT&aWZQw!YrwR;K?RdSP)tA1~)hZRjnH2lD z!vuexsM-Jr*v;NxCggQ-#$bw@x}9grr)#R-H+@KWhH|~A5RwN|IEq~9GPKjo7>_`J z7{N6wP(W9B89(tTq6|ncs)@O%N1|mwa^w8rt6QZ|RmN{kyoZ&e2rk{s z9vWr<*i3#P)hdL{LIjr8nZ+laT|m)WTE-`u-I8UU?n7$T949^M31t zg1R_H;46By-(;B?8|FLyjqn$SU+P+vK zgp<&3ytef_p6Nm*DZsGr`vpsVA2RE8TTAx!P5Je95BJ}W{a3^r{lv0w*B1#%fO8C) zwub;p4G$?8qi7r4Z--2;*tgXMt2++!dsGOiz`hwE&$A)6mX=kRSoi;iRQr42pNYAA ztP<3)^S=fpB>X6X-2%1=R@gbzzU8*exW-rM?c4I}lJ@j3Y1IFJN#Onf_mI#+0S)mE zQ?`pf3xNL(W=0^TMDP?*MUpNdw{HTu@QSS=@P81-b~pH{XJ{iHKHq+AKFIMX7F=UL zSfd?Y*p4UOX?y&As=e90`15)@bJTY!emiByVc^}}fZevk1+7v)d4K+OwJ-IrHvj*# zU!!S|&whH`sjS~o!hIjLNqT7r;q2epG~8z2tI_77Y&%xc*H>h}-`DrOP4r&-ehbku z0bCN>9o_!Wug7xEuk;KQF3RD|@l*2lv zfE)XUK3?5x2?#^xQ-*_{u=_^D$gN6_0075*7J~N-lkR01dxdCAY8<|0XXCI#8WfZEOFXRTa38=E0&y_zh zw`p6nmxlBMEN{w>ZZmQVW>ntF`nf)L@}Sc%5ta-Z4dY+HGLtWBU~+{46C0(^bPGHh zN^WT6vc0Jh*JQyP?6(0tvgCS645ctVZ`?V-)Gvq?qi!37ct{d~(pujZ6Af@e6Js2F zP+HhK(Ed`HZ;t4wV5$AQ)H@U5DO8N+2y{%reF1Q`bNiiVbuy5CK6mDbJf{FZTAjOD6IoAdiz;_OoeX2Kgf(LGzQQFyKOOhk1b~V`bOGP=}>^X;c7Z4sC{lr0em%3 z>mWFTP(v926o4w9KrFX)q&U!Q10(#FT)0FC2OICgIsnF^f_5Xj z{w$e7rTF}>hxvy7Cn`i!r!9RO@MJT{z>SC7TfPbRyt2Y}>4w|KwEqI*Y#XmEq~syk z)y$6mmqIwS2l1^>L&;|LZ+xR)`hpWcavwxh3Kg@TYF1?U9*zq8x5Yz33SZl5~- zkKL{V@==WZ8#qA3OCVQ3*NS0+iilJ?O8G%YM!(_+Rc|6)H#PATu^N^Y%chV%jmiRg zjIJ>7Rbv7zv+&bPp_x?XN2Np!A{bj3Y~XrLh+j!%>=A}T__9FRZmY z7ispkj(H66m4{9z1v0!Vvtu5SS{izAW&)U5Z-RQQ9FycNzw!^R2Z2lf>x$9IQOfP4hg#>+8`27 zc}NsDBb0TUATQ#9)V*l5L>BA=VbB7Img277iz&w(DP-pSAcP!KVW)1Sptn^BOudID z2%Vb}s9Z{{gyB9xjyfonz8U3Iex^V1PwO$E289qc%IBiF9Z-EJ+Xk6{kVvq~2G&gz zX9EG+$9klgNpJnB4jozSMY_MYIcx<_Bmp-@&(P(L5NRnPA)8W?sQv!fl@v7lXrxx4 z9u@TPH_0z0zvCit25VE!F$Uoyae6RVeN#+td zXBU&gbW|Dqh6}SD4Q#=KQl^+WOJpiwG9c1a^`xRU3=_lEzILY9WXGn77+z7M_nJ}e zk7bDTKZIft8+dR2nYGT8UZrnia$)at^P$lUa?!fSf*JBtI>ZpZ$0-^v86Oi2kq z-Y;-(f;w;O<+-#+c-WW7Z!%SPr0yHn3*VpGe7Aw4YoS4hYu=B$B--RS&ymIY=s;(j z49<~a|;NPDxJxnVFpQFTWlQ7L`Ws&te z_7_cZHCqU&X4T=8&9FlNm8{iM&-}gj@$$y!uTN?Y_uvW3vk9H!J{- z$~Zw-?TkvR&-aS10F1q?(qDw+iyxk@gNQMB?@WHyvnulye7OK)g^A2q*O++-bIj!{ zx;>_vRNF9B&@szr0Eg2M(4OIm!jCoR3hkzrW_I4mKHiQUVut5H zib@$yv`L0c-rWwqRlUD$QadeQU|DxNG9gR`nD2;{5=jG@9yP6OQ6D%>Jjl9+oWLMV zik%%Y&uo=Sq! zp=@QYNkGg#u>oG3;$CWiwDRGrNO*&VJw_LssHRNyX1OaIHIv{#x0=~`9n(ktvmQxB zY52UbSR?F;V~$+Gl`c$UKEm~E)(Q}_+t#P{3cNvb{LUGkiKEfgma!f8WXAM2lR32U zn)M)hJbrk=5v7e8CWH~6NyR=z=8Y%d8|-il$Wq*XIGb^D(p8=2yP~G;E}`wc?bAt| zXweIC0H4r%HI@x2U#J@32B*JsHUO>P5sUY|vCC1U6pnBt7k)-2Np1fs3EYI~rx$&Z zLiMZp`~Z`{h!E0P&dr`P_@f8^SA~<0BheoRK@%~KL_Bc|g^i8rk#%B_{B1I&0~+UP z!Uvr8X}~Yn4}`RiNhmyr+cJEgWe&oaM5KUKUx+9Hwpt1%f!t3*y%8DV4_=^p3X?hm zOoOoyB8-I72q!7b>Rvts*r;_s8bYxxBSMIrtnQ?;!B2Xo|`XcwC1O%0X0)FgBRsQs)%5JwQ9LX`ew>KnDd|FV7 zm+!%X$sE!6CVoWUK~fn((-Bz&FA2ZCo5ZUb^lm5kn#zlqW};oTCHgJenZT6(;u?0^ zi`-%sTZlOyFp{v<7NWHW_Txmin^+P`wM(xwbt-1|V@lft6EH-kb6X5cDF{K;f>uAy zz~ce#JUxqA3__!?mCcVsejvDb^$r}h*z%hgdz$>pRcHjUxakGv<=I#ASn_TaeXh7r z!}MENBUA(=N0Fv}fo@u@_Khl&Sc2}xYO+r~1PV0Ry!j)=rV>pF#z{Slkd3)xd*D-c zj@sMN*L9QYn(P=3z74~1+QkqdCk=tlSLgvt6n2@8qgdMZg(q}AB2|ugxHK0@IS-_I zcP~ifRzadaAyriwycO1(iQ!3ah(H>i2JAhT`8-Q@ zwBxOsHfL3;(eA2N;YWVpZg{BhFBUA1c+0HsT+eC7jYT0;lMT&1mDG`J( zU-w7hVFmkM+JLhNKFvS_rOGc70d=#BBnsA$pe5tO5wb#PZv4uE+6)pX;G& z%{oH7AQz_zYfb#M)0{^9ui^nqKZ;c4DUb~kFu{Z>%bq-=OBgJqz}tDVF;)LETqFI^>!&3++)L_NEUpU!Y)(Op&M5TbwJ z`I9D?KElnZCgp6@ycvw4^fbdI0nnq$PBuLQU7sYdz1RT`7AC!mSUWKE?5GCo5f%y2 z38K_Dv^rKx~S1m881sQm>8fc94zfu|r<$~4;Rgecb zJaq(n0Y)sahcEyR#hb#V?7V8wogjoR(Pr>KHqJYurS8hv)RJ|0{LPe!hAD3eq zjwPm6#DIRm%@kY866to*wjIJW6I9G`kHxsD-qmD!jOx-Ne(>Ss|IT6-Eohgf0?7wEL1(IRJ?UNJ3oG7`G?SkMEmYc`mMN-)66X|6@|I*Y$RIjNV0 zO2k7RRGx~IY9bkyPIY5A`hakEO!*NPuhHBO}j<*tYZg3TaL1#Z0yPZYh#F3~i6S`E2 z8oLQ;MJFFGH3FM2hO}2$94#dL2E&sWhT1KMQA3Gx2GSF4>bs(ji=vYd4m0ea3yNey ze09geufmdD>PccAd~#n&*SiZ$tz7C1j2WM zVC+`t`os#pGtA~TUcD=!bjt+c{T`4x|E>fDs{E)_#9v+}qYm9NTSPXOU1V<2U84TjFBg{Z z4)J6ZUH%GhW2$wfXgL2MT)2T{_bhQztAr^I52Rgo%(9M9&4<(~>V?qi*H*%h}O9nt8KvL>+wODkWD+ZK>)#5M8InGjj8tSVB;R2X|n`=j4 z){LOIrh)ow3H$z>0evcMJ@y~A?=ZH7a;JH|xBVyG@o?fGRiAhnF&kPcnk66}-L-z= z0=mec@k^XZKP`vAb2e^}-euEQ3uoaX$W3j%BlRJkUh>@>F<{`af2K*@C;{=4-WKQt zX>58H&jxw`T_K;Ao3RAUfN<|M$Y|fD9fPXm7)&-lxM)(Ea`3o$@Scoj4Mw1Ix6t$i ziNSojREu1;0r0`5pd;+jnDL*Xyp4o?ur1a>^bC|6tMK|E@if9iK(WwZMI<3v6mIes zkBYw9?J$50MvcpVh$DQHdl%$>H8&b&v8~V0e);fWpjMQv8a*kEInyt)CVop2EQG5- zgO=l9S7CTX*(d1+9105+3hPA8U9~|_OW)1t!1>;XTPJbB;~5ZVV^#w=M)Jiuo`o6G+oKAH?ZKz0Uw&6*PZun$d=WS^=zLp=0#XwV+rW~t_jejh?kZcz7= z>kT^ONWMsn8+0U&bU2XNuS-QWz!DwYdW_sc9QrEDlSbYY4+`3=1WhiPkUS)+GF~IF z9QK+Rs#To{Ce8qYrG<~(j-3Obfxo8+;J9E+Y+!W?<=W^et0E(oh3`Y+J%_>yOr0m- zL?E~{t$@zT+#(#Xa_1%xBu2GIIXm?lVw9&9_$c7PMc@o75H9}-f6U$s(iBi<1m4%i zX_L~^$jM;j21m`nR1>yE_t9;Ibpb5z=ArOOBue%QIv${?o6U8*d(|kr5^aLhMKGb5ri1{x zuHoYXAmAaR&v*hH0YV@XDUJ&VQj9M5!u<1~o#nFyxHetP7n#d#v4Kj|?=eH@EHwdL z$N*jz1yj%IhKP+KeUFVPO9NCa6@_DEpJGM_Y_syG*+u+?IJ(?3*=kS)8`cWIAd(U* zwjMKyUu&kYSL{Jso+PJHme)z=9CyDBYRb=^OX0fiUfbp~hkTQ6W^J`_e{{6VK`Qa% z!aghs3c!a?-P)hP;tAmai^O&8#^Lv|@N@cyY%B_A;FE2}X~9+GqPL8H@z~a5otPrQ z3{p)Gx|2Nc>n`fpj~MG1b2sZ}x= zb=ZjxUbRIxv8Tt-`v+S%3$tKJ^6K@LVSp;TgS0PUszh6QOmHmF>ENOkn(KtU+`vnN6>!q-N!16KOTMs+q(RMhJD?$DPf{|Q=v5$xZ2tU0GTZiP>HzlnTTqhc66Dr zp)^7%fNDJr&NIja1|$Mw++b&qBT)DSI4B2ry)Ce7zEWs4n;OM^S3N`BVlBK6&Qx=- zZe*}Qbn9;V%)leMT$@Z{SpXs%D6F`57FNNxeF^4SCB||R*mVSnxk>F1)9Xl0?ia_S zk>-2Ebx^mUkk$E-wX}g;{j}Y6%E-C=9x&Gl=tIu0Agb z2St9psCKpUvqBw>hutRip51u?n`*G-`hGsOXqAIvCDOadr9uX@Z;rrj0L&B?c}NVH zf|Q9Y(>Vwev($_!@;plRq^`4r78mcYEU6lU4gT`K~&w)dl1ez{%z^I z1?PM*6Qnrn3V`lLR>_Z8f*5m@Um!)X=zLs+P>-`Gp!`Hj=xD}aWIMEDAt@FSkjvT6 ziypEylcV!qI^(N#zGfoKh`1hEoa)!7z=Ny*y3!OW=dRz(spe?iqb_#VWs&eXOar{@ zI*alFRB$+N;Tk zV*vP+P=~PG3ImGfjiiaWLL@phBtIsTXbR*~qtqmi`hGWv zP+%LJai;|!5hMfCqrlBA@Muf2V+WR26|eM*A6zL5FL}@3r4{e0m!yf7ZSUZKz5;}B?g){Z`c4) zmdDqN*Lrys#SFVP6zWtB(&9o_8V6nZ(DAUon`mb*70o-wbB=V&MvS%Pglz6RD~hwS zlaM|_+)zuvEBEJ3O7CPN5UBc;r5Sbs!as563{ZGM#b?RUw7#p}0!PO4&>fK$0x0Tf zHOMlm2}f<1%f0wVIDVcP!hkBoL(DOP`oPbk`sfh__(yl&Rga8B#t~trV6c}g_|L&t zS*0xVdj1`XHldM-EwI!G(r-_W&X$iWDB>3^+Z|V4GXqYQq4Fx36Tj7teq1D_7PLy) z!Ig;b2&(i9y%2*6@)30qGY^S~VHhjKSkculCCrb+cE&ZzYItK)0q}x0ZV?7;k7MB0 ztc!@gqLcwsz--?FvFFN?A3q=k6Wy}rmqQ>V>gTE~VAs>^m{rz>J%^DGP|fj2X3&6Km>3 zX`-OU;GLqxzLK$ny{@f-iAN`2d!DBk<8!I)brGGtMiq!1Otv;47R7U^M9;Zu% z%kBDKt-ZCIo&w9M0KMS**z<0BiU##=>2(FvMx_y^r*#tA^dAnoo&470>(Q^{V}dKpa3lZtm5?#?zY`j0CqO|at&Z39kaqloL1KvR~`VW;>C zf!1(2Lr)c=-u_ps{qL+4Xw3P%v)!s6$$ z-h^TFi6toNDup4Y!HxV4OeJ-5bSLcBp1Ys!C0DU4(T2qU0wUG?y%KQT;#kVw(S@GCVfvSZ9aPQ)x}C3rthypQK=eL3@CR?WR-0mlpOMB z#jGtYLJ`p~o@A78TN0dtu~{-2jBPZJ9%0i#>`PZ3JJTYB$@mPj-(8uVPbr!K+vAED z{pjhvLD6im2wb+M)i&_iZy6*Zvl`4a6|=_Yo150kND;HB3VYi_Snlij`xDvLA?7M zOd-oAJWIhAh5<_LS5kG`r1U#<-f1qfeF}O05nD#k#+J*WZOQRo?VZ$QYk278Qi7^R z!U=&Kfkjiy)%xd~XQ?L{U+ zZ#kxG7Th7Dv=e_hDammou>q3}S9E152IKOkVVRbuhN@%WN4{3!kl zWKNC#W+%22VO)p+i=lySf3UsScMTSOACl2fa^Zg8iD`jI24OX@bs0#bBWctILU6tk zLZ;n(S4oV(dM#viu?%1~lMK`2kT z=jcod7g@tO;8|5?kYR}HcZ!8fY&+P4VX1M-v_Pay(FrkZ3B`!{`{QVzN5V1~7&_Q7mR3+6!`3>L*g@HKVc zOcSfadCW{ksI(WSG;Ab(!|5c2HzApbfcFffyx3j)c}&S~p7j0gq%Oow*uljXpb60Y za9bhbBcRbUg8dBDAMiE(+8gn5*zTIE?lMC2`A0%S44&S7Lc%^bl52mC33f(kIXCiv z0GqEt$)%k0=l~PKD}r!H|3=ecHVLMKcOgI6FQ__J#-E|jq)}U*dIjg(QLwz0ih(`G zxQwr>JNPvX-!G@btyfW>sJ?bTN4O9ea{Md&277wQaF&$-qht=e$#OWbw_&djUD zvC*`%*cRXlxFzR zAJ2=(f6zXepG*+e1{NBGnTqbCZxM|_ef|$NhMa1ir!Fo0md^y1&fcYCU?&)Yb+shn z7lir#Kq`t6=sscP!XydCq~-c3dE79=cK21_30SO&rPCU=0w&aaDt02zAgzU!Z)^Xf zmhp(NuWUSetTg9QiGp`fQ6oPrL))H~V0qZyz7~OW8zO!Le~>5t#&6|G+cG)x3J-HL zTR!K=i^6)sCm{N6KTc2Sjty_dI`9snJerzB;+wzLv8Njj7rD*~T=ibZ#&13+u1NRgRXcM!_dr|;^^e66?iY^WVvy7-~-%>^P zeQ~Ljci>f4BB=;w-_ANrmOR$}h1i%odQ#dDwSgPqgK9E6q8RNTCVwx=BSe0 zv7iMn$*jJS%m)qw)|vRe3^W4%oki07;5l*hLtTHTF^oy4=wZas_VZIBWE$$j{0RDJo;dtE1dK#(JwqR`UAZ4#3Z^YEfCg}!)o8q{ znXQELaj9)2bD7}IBApgaqv;4`PR z^ox#LzXdxCw9zE10PugcBzMa|DA0~tFEiaIcek zYdu+1ciP0xhLP5FX{z9LLbx<@5g)Ce(jYO1y(m!Na-+=CwKC+$1q;f=Q z;!pXXggEA=6XAuli`$JXX$g`^Q0gqCcANxl5vhKIt_BU&b}>8 zB^1)JURQ(zv{0@38*4xs&Dtr#3R&EiRFGosN;+f+6x0QoGew;iF5KlZX8E;aWp35m zDoxAX*z7s;s8or39QLZ@9#nN*uACR&0sW(Ye_$q%qoBzXV56nrA3rD9?>8xHqMumH zVSKpvz3Z7LlFr*ebiZDz6q8k;q)D=8^TE2P9{nmx7EXN10yq;{!4@H5dyB*-YYYD& zoQLfQz$lbtd8H)5dYyKQBLFgM@zz7)D{!>l0eWxrfd_Q>igODj2oO7_HHX4R-AR}E zKsPaT(kz}Q>Njf5quroEHc}p4ngTNwhg@|#Pm0kdljfWf1L!0M&@jD~p*sW%Rl?(| zSeJh@1c(b@GEx#^VulbfmN-iuy{i|IxL%fg=RdsC`&rj+pJ%^$PIi**fsj4J()BM>ik8G}r!Iw@7^1^I~N_qqmsYoTc{7F^MpMUhzETuNRfcVIH!+RU_B zpV>n0HAfr~F+>l>a@J$(l7KnHvzG)7wC2&4)zwD`Qnbs7Luvo(A)4&TBu1Mx{*qZPJA>%-{zA6W4+XG8@;F9wsXi= zvKK`C_?lw!3JJ}d8z?~$J00C|i#DEdbgjI_= zEDEL;C#V=)!=mDTCJBNHG6*WxZ6Z){`x?Y5ZuLS$v5h8LY^_T>pom-Ru(;IP@_R1- ze?L_NnYqh3?|GN!ectySh1|fp2NA{K??r39BoqKvp)2uGcE*!XVrcbKswCxuS1}*+ zumK`yjGVFH4}JnV2@e0Uf%F^#G(_R(2JZ6tl#AGjr#f7Y>x~b41n&FXoqhmNp7@>= zBK%bA0h&Ue_>~xO{52Ye#g@K(1?E_0`ouEiSFpUPiFh)N zz!ceV6~da3jYPumR$QZ&>~1hfpsRoJW)h@oC&T-T>uD;IGia@oIk9*x{zk0Ta|6+{ zF>!`;h@Vu>l=$24EnY!&-+B_^c2T(spog>yQYlzKUbXf&0D!1uHppie1bpj0mwt@; zjx1g+$nS3XwizmO>AKTDpbVL*E(G^^^mEJ!qH~!qI?*TqB0_%OqG5nRHK2M_P)JkS zU=yD5yUygXOl7H_fDW;Xi$PSY@on^`XPlXUKqLp2ie}VUu;|GqckE2xSHIwKs7;TO zk}6v;rW*-g0Y^Lse`MH#D(TXM)cAe}9)Y1};FYR5u`nxLiPRpv{NX$$A!)syiGw}| z-oF!E&Wq3lsL)1IqrekAd+uq&L*4b!Y6bmKjXjl7dSd%b?h&Rw0Kumm7BDk42Gal> zDS|VMkSIUP=2}A#=xD4+8OY~c+Irqho|J`R5EPM!-;{ic3DJ_s+92!ggfnSwZRbwE3V3b9!JF~R*=lK^!>c<>^buStu> zej_#Fnb-0-0;nHEhw|D_L*<-I(<6n+Cf(hKD)iGFxvwp>lq|JFDYXGQ>F zK}rZ|#TcwASN*VRDQu@k+QY028yQy<&VK$aan!6kfCKFrtR1*h!Ea*@ptx$*Xz`Ms z$s)&u2JD zJ?Ie6^LrSQ`-FnbS!fQM*db^wTGXx3W95CDG0(4kQwjR9|EUz4SvDx_{;(hCU(}gxp@P`&9xN05RLitceyY%A<)bPVh0q zgaj$s$geRYYop8#JAZepY+{$#sV|}2DM`~vnTr5&Lj|z zCTa-W^c^_|A3^(J^zJnIh_WPD$Bb{nt*7!HcMG2yMD}!qUP$j-XrdJ{*VAlseRrw) zH>z7Gwi&*eewScnR%br%@ZfWf9IhY3hT2e*MIPiPiqz;RI%?&GCITPCVVAUIH&-A0 zyd7yVx^NZWhJ|3jX23jvhYyq4__&A^&)~HI;l;FAuyH#d)&N(P-DKv3@Hok}5Ls=c z0xUAl(T0<{t8yj>i^)5t7Xd*?`Lza=J(>GSrK{tVudUECFcwiaBzB-xrjJuHNmp*5 zAFXYt-HW|wN*#dipx~Sq2P;$2deJMIlaY^_X&EMkj)r(0y$Sbg4x*#(Am%kBXfpO< zJ5B0mBdv?i&p|p$ucg8#N{ks}Kh41-1^pE%wYWc)W+ZiDA*322z-vPaa$?Mo`M7_K z-0jH~yYytszoz04Lae6hrv?lB0_Y2vO(V4`Qcr5gwrS#kz>s?<^Z2`lEax-@6v6qS zcHDEs*&mK4=c(;)Tc_dipx~m%1T`3u#*~iSXQ!iMo0%^@an_{@;ccxv<($V+kjw9d zkz0hI_&hYD+he#(er_rbBE9FFpfAJ*4*)F^ZuwaAga8c7mhckix`30Z>ZA=?$*%r( zpj->C(`!W%71R-w_MbW==OFeoQ}U~FnNWw{vL8Z^Zq6n+Wi3UD$w|V3QTW2-A!D<_(9F#JJ*3f z9_{5si99HVir{d9mr8q#3=TX?lEfzeM43#?o2qBHB8a8WJ^Li6aQi_`PRqXa`cbya zNAhusG#>zD>g85F>Y)KrMvKVfh`OLUIKVbAzO%>}8z<5E?{F4zB*F39iCAY;?*_xx z%Ph><^YLi>bId@;EUQql96FwRJc*TOC>wm+qFb%DhoAxwu7cZHsDA2j`mUt(tc?vg z(svJvwd$qte+%8|qT3^pf=-7m(5@CerDL%2Zgm?y?&1~g2%K22GcOJjQu?f*3^$JI zg;C0L`-3)rlN)~x+fX-iK&rCk3H@`jFw`W2Y|2FsEDPsmS~e1fGNaf`=!acMs9~`V zmu+l#S(8WGFo~F(Z(H#;!8e_f$6pE6BKgqIBLySFMQCY^K(@%L&36i5(h|XOGTe)N z3)>!k+lr*8`!5yK(0;(PDFzwml5I$}oF)=KIm4dSuguT9_vBVd7&A_{A8?4d}sJ!ZpK|`L;!pHjwza93PND#Mv@;k_Cld z9*xK}(0fOKr1*({O9Ced?CQqR{yO!;yx^=#la35PWCA9l53}0HU*aE$hXvUPML)0X z$F`OEL@^i3<>St=@mFDC2d>5whJxAY$Ylai#2UccV9+rUK;;o@BS_>x3b+Te7G@v) z*P&J)M?|`NHGEQf#pODl>LHH|1qvN-#^LVRUbFytx!)HBnH%K`l7pl(gx?3>Jv?4( z91C^uP4A~7n&K1G@cO%_)0EZKLYMygGCBkm`8i*n|%y47@ zxmW8^O2#Qu^ww|EKk>&B43sE@Js6ywVkflN&ozu%&9L2waHyM{h{dc`1MdphMtxm?S1Qo=VT&NBq`3Y;{o4_NQ=9$-iDR#a{j zQ|hEUiS4k|$!O$iY$(2QU&-EdSSfeQ&auVWI1_7y+_QRxbv3L)1(_6wJ` zh@ENxB1^_$*3;|xFgV>l#5G&ma6&IZ`JA3nzAqs5p!~@l-RpqlrA~Jl`h2JHU+hqm z!gO!P0#ZMxjQwe`Rl&4w-`HnrYzC|gm=Qmn@Cx_>E6$c~oTSwxfMFu;$P7?8_dijg zvjm=wM*}jiXfKOr@6E!6EKB%wCrJxPUE(LW;CS?*;od2pS4*st;g=~i~Lk@cknoRT!QmdjMB04oP2^G55S$o*Q`?IJGrC4;Gb&47*PP@NndxlpYVGK z;GbB2CS7n!Jt+@d$YO-V7G_Gg4(`cZ{H&83o9*!+ArwB^iX&5V{Q)Kl0YXc2}6IE4UsGjsAQq}2d#F+Gh3 zFQ33C-6;Ec)gCKH0=XP`?{MZqj!NRq@X%wl1f#pPZkAjB2F4`bX zWRE*|bA|si4{+WqD9(iz&|^$Bh)w%9PQs}gcw_h)(^2?VB-YF>22lXs$nzR~&iwgQ zk8#x1M&JecJ(g?0A3_0Wlt}b?E#WXo-z0L{ z;)4*^Y=m93E)Rfa2G_q1F~{513M@D*n6-iGqoT8uL!mm#eh9Fzo>@WyYIQ`3G0*lY zT*7ZMr#}+FEfL~s7a8T8?lxpAa<8{pzQMDu3)s$O)=>k3x|y)RmVNL(+_D;zC}-vF zv|JH?1m~iBF-~(Q>07~WSfpLVYD=f9gM_Zqb#}VZ67^d=>w5lst)23FV^Bs^0m(~b zc4B+Gnx5x#YzO{Do(WI*^w6$fu0eJ?!4gl3A}7iU(csQCHcl$yCCDl!CWPxpI)vv_{5a{b255 z8t~e$D5B2fr_#BD%B&D0`>xN;=OtL|(B=(4(NkCrZr`=NOTz=ua&0(Y-eQ#7c};9e z=@!3oq})&3$l@B-cpK0D_nLnygaUVOL@2>haIu5s!bY|^WwbzMSy&+|5QnkM(o{Ej zud)F(m{jmQ?;cJ&J-84LFS|wFK=ecq*mzOlu#jrY?R0S};LA!UM#28zp-J+>!C2yi z{u9)N`7`0E6qJ647i5d?)Q+%`v@){jdzIl7#vxWn21OJg2yQVP;-I`8bv@n)R0TTF zCyPGm?$BvCUks4vR|^pcF%5arUg6n!`PbhjiE0`Gy>Vl14$@^E6>V98hp{nf0cOGqA)!7>t94X%ORZ zPVgXZ^}u_Z*^i7(+4~qYkIpWtCasBD`-5~DC0hw=!tcSI1E)NCy65wXg1t^*CuLrD zyBWMfr^>DJQ-MEPc0p)44h^4tE^G&p;)1Og=;Oew!;X#BiN`WacQ{wfKAN z;|v@)3&hrEwZdQ%b|r|lkYUI7>rr7;QrR~i5%1pPS{;~%?@{TQEuzFOfqKOZ!Qe8q zCZZZSviBbX>7h%s9F2AH@^Kp(hJE4k{%1edSL1+)p4B^#f`7aK_Rm4GQ%_Ud72F0h zt^*#!jk7)^l@W6ap;GTg<^-^Prtbh8zf2z6B4!L}``f}60k?vkBhb=25;z*#6sCe+ z0)E|%NvAw*Q-=$yhsc;xfbj{a$L4SZo9AtYlY>7xV^jT5<|oI>>uXdA9SuQCNJ0Yr z#n*tjQP@{=1&=o4oNJBn>hs~X1BJNCk0@-m7d{RI+r0lcoEc`1z!&q`pPrSYPNoqs z5rLdA9Sv%vpvLGq|JYReMjlYw9pC=q@bkf6#h&1noIm6h_;5AoJ@F+loRr>BPRlC( z{;wl=m}d}oKv1wqK=m1yM2Tl^dkw$HE`$XprcB|Ths2(ZMzC-S+4xJps+Vx0R%hO! zAMxr&G5#dhyJGW?AN0uBILM};<-jSR_ao)SJv5i$w8XpLfPnLs*Y<$Ne zjo4Sm9X#!u^qvRKmyaLOkR`ZlGs~n=#%AFtQ2$Hhq%S&~_7pda>aE0$U=p0CX$K@o zxL5>U+W!X@`SSpB%g?NNnPT12hStww%a4>Gs!)19d@tY>0DqHk0J2#3Oex>L_6?je zv_kMuFNdEHI_g0+e9Z3Gd@w_FTm>(IuH2hFh3;!WV*{-6&kKEEiJo<@WbAg@q1t?A zac6C|-YKNIM#AKJT%-+g)mvBZ^e(0JD}TZDDYj(rAllOHJJ77@(e^C5tJxJ;89?{N zc&4BYMKkc94XqF{{)beCQN`V(InBX>vYZvpRZ{-a{~q$uxkEo`_)L~k8nWh~?}Tab zuq!b7%B9(v7hSleX6g(&hC0AW>phJZPx;4W_(~rF-jv-yRJ$Sf%Cl+oL&O}i|Eh2( z`L*}rZpav(~f z0j4LY>F;X+YNwZ*x`}Y>Gr+5H$WoKj=>=dMVj@ShOG~g)B-l5%PI2 z(T~N~8w%;Rt50;{QFr}h=#Ygh96L?K$qxvS(!!9PT4d;M$7XhuilheD_jwnHHs6U} z)ejRIDMlvH+Hm;hUvN043|R;5zREh5#%04!(9sGvkJYAoc^lqu5gV8v(kLpYsqCl_ zR}uwFsq#pMEI)Rz>0{L6o7S%-9z|Z;cL@La(7rJhM<~lM3#e{@T0m7_pZbb+)Xr4E z40%*c%8&kko5dHNW*<%VLufd0m@C;G4Vcx~0?ioyZJcV?q>Jal{+v&HZE@u;JR0xW znZS{CER4;dr?A0n220{hHBC{?Y%VqyoLTM$%ErGyU6szi*GuAH6OPx^und;ZqC|>S zkEw)bpFnWK9@gHMRBaCAP&P75)?93~;{-9o>!o8)w!p8RD!#O2?+>KoG42ddodC1F{9m$h)7?o!ECDbDRwH z9^4>|wXQ=ZD~#)PVBJG{ZRTW+EOT^)xzZ9WHr1$iv2&JTfi+YdR6)}gXN@M<0As!( z5lZyj%cf+<%(E!}4_rb2UI>zPU7VS^qp0h?I8PUU%f1Yd-4Oi_COV)EUB|x7fPSkXPYuNI*XCP<_7d#?m#~w29%Ii+YEw7C_H1WIx6kozXTca|fMEb_P9A zF`cRK%frIMJMlkS5!xDxSC&W1e&oZ6 zEyZ9%kz}VPJw*CDut@-U{ESq`u7J3m2)WSF_OlwRJPY&xoVPfW7 zERLBNksJ3I6%*vs2Cd`ei^GqB1o%APy*kw<5v>;AoUQk+*O&GYC!@1yr4J}1oZ^NH zZu?rVZM1tXrMB1MHYj5cZj~3B8j3g=_Tfp&Afn2&Qdd)ZuSb~E5;|6=RgCF{+pkG` zO=aGrIZ7f`&|6!sqCd;nr zgoR|BWIf*!ilZGGhQ)$h+7fM}*WGvtdjll2JRHF{NSjxSH?o~GP{Y*Fo!f$_6t%f) z@i?Xp?jq49BJe@yD!%(oZ;8l0?Z)t=b+h0ml&wHZ0&q>xC4}~%-c!$DRm%{Gd}SP= zS%4kJgU0B&MBALcf#hy%5Gx4Nc*}>}f`bX?Cj@1heZ^&-SOm+23O4E=A!kXAoaQOv z;=KMK3f9TWR=MO2@aSm|;Z88Pd;w4P!p$I+XE=J(LW{NrB5V7D5(m$DQ`pVxxSLrS zFA!V&z6dt-jNCjrF9+0e4vcBi(Pq|rgk}*a0%nW7jSa(DLeYe?aGlx)yC=(>XWjSN z9MmfMuDZxWad zH*XvZqt26m6&9^mK>jsKf*|9tNA}Ykl}z5WaHNm<=gdI1-u3ZhN(19j3TsCp3GK4T?uVz~AX}po zZE0rNS3P_P#6p4&JZTy>u@l1gT!(e_W8we*c{sRvEaDAdrwO8W{T{USkvEXH5%yg4 zK$U~}&~X^Tguf1sHPs-}>sDe+jNdgHvw|+5{KBwFgWWq(l;dGG>EN+-Xg7=5IK|%Y zxu^qfK=#c#o#BWiX5%1Ti^&tB5At&w(-yKkj7VzccJOy_xH$CrP?ck`a$9=<6sD7N zaw6zjI@t-cJITNURE=fcBpNZ;!JL%5e{*-}kaZZq#InCZG3Bq(kybq;#t#4mc<6!+ zY-FL0IXRe@V;y6K`or9BNIQ1u%HeDv>Zd^7pS(t3bUEwfYp`2T2Yb1;nU_mz292@l z4_dk93_&+Xq6HZFemx}-Qu3tw7maLXhdH*9i~uWM*6(#X4H1$NBz{B<8-td0|sFt zOZ83*GhSlpwn(6;p_qy)pFcdYk7@@MD*Ii?83J=gZ8uY3Ee*t-M_sUQGAm9W zVRRWQ&TXgfP|_c7$9>oOIHYxjj?p3pI)uKfm)PHtovUM|;t-bw+%tZ#VtKvjsu zDm8Y57ZyLKwPM048r(b} z65O2Cfk6v%8xTLLH+fOwQFCAz%p$8iRF9@ITYeC)8Fn@-;d=5z=;YgM~_w6l=Dy zOU~sMT@-;B?xD zR6U0^-0|DreT|enT5zWjc}?VeykAvP3xD}l%!`OK*I~-z37!PVyVO7o;__m;IeeiV zjqrOxxy?Jy*iaEIn9i@)Gq{3n4HHFLhL!d{&VJxr+g|~X(vFr$)FQzl!?LEZpol|` zUaP0!VBtw;MC;%8D-N8_p5W|nfGVDEVH-dzIaqWAQVXu>6gXFZ9g zg)^W7SwAS~{A94cSD9iD@| z&~OlbC=obf{Q#5+V?26+0|4K}kNzw~h0#U@Nc{SrJ6hbsu|?j>Mn`hd3C|05M#8*YBz8Jbn(|tvD#Hhxw4+YQ)f=LK3 zVP6nEB7l(9cqXLlFZ1~~Kd~xmp~DL|HnuMV43tJk7;$ zrGg@uV2RcQZ1~Ffd?0Z?o^m+92-^g*4Q{4inF&#C&UyDAG|Yd8=bVrLcY?GE&l>9Y zvYpzaI=o5*h{Qd4JGEFiFqB_PYP~NW8fhukgI00Jup|oS00d3&THqh$@C8af!j`59 zKoe_3QBZ5VgKA4WdY19up+Jd{tiuN}m;EuCV?U5~-u(of()|v+dDeY2z=6_tFyRyd zCqIRf3A9uWi4s;^9AT+cD@YiXW5Vd&$af;6(&T8i!V57{>4tbK@x9D0!zvQEobiFm z4`q&o75AO20HY@nWbqnUnk1m8t41VvGVIzA{0lP8+=j0Lih^c;-p8dw71A0`X=OVkn)ZC_ zrI?LxOdl7QNYd30R1FXKFkZyP2}g4x_<5?eai>slrIboj?l2c)cx)w|$-xWp@EKM4 z?g12~L&YHb*JNl;@M&UIB4$88)mgAhzO*}`V`ou!4LF~26&+#o)EYlwy-6G+M^W`j zno%*u6w5=qu5~;!5JwBrIy&3@GPWMej6XE#o+9dS-bwj6lq6G(k4*3MS8HnnRle(> z)Xv)tr7PgM#Py?~{ucNV${`4r%<}e~E*Lj{b{)$G#E9_L%c=Mv`mfn^l3gf?1LTIK z3yPh518Q(k1mpfq+W^l3(Vv&=m@0At=qu@8^(NtABg^kJMY4NYk{yOWDpS<2317vX zEH>581Zeyrl!6qPHB~OWl^7O-D%bv+naugtt0LB+%d!T?2B5PXZ^$3O)Mp>^2ty*^ zFmI&D&X0YkqOf|CW@nDGSdPyYxr(%(wnoaK6?z%(lM&hZ9@bCb2{Lf6p(kLVk(~(| z`7OQ^KN2yXo!Q1fl)}erGf;&(6&UP0{fN=)&4lvl=J;2m-}G0{=g0JyjYxBhQozeB*%urLW1+Ggk?}#t0B? zYio@*?moZEN_Ic)dcy3e`G71;R>#8OK1^lPO!@nIfa zPCngBFUCGr`DXbj*<}t}g2j=4dv^814BfQg|HgIXq_3cu4d*=t@LR_XzeRu{dsqG615a;xKK*LeQcybui;5V zdDg4I9EDrllx+i)QV$>;tuq}iK^DRKm{ZiZ9#!i0^(!QdQ6)+dJ-;80%@3pL5&2AR zjQydvfui^?U_Rq1X=6Y5bOH6?gJucRTldrb=`U})19$Y>ZZ9K{vXoPM8RAOG{*9BQ zn?0!5w={q9pKS*WSpuyB^HBr21)on<;7PWddmzBN%HPW`wQ1r$3LvXP08L4^rZc!k zwsjG?wW_h6)lMC_@mesV%p*X2BdE~|8Q7&t+!UHRB3n{SUeKM4SNV~`_&r8#J`WdY zRbg6a#b|A%;pBXpy6HF&m^|>@Y}HPV=D@M;MscZG&L0AC=(t?3gWrYRW9{R$>S-3K z{M>Ll?ZpB6%ZZ{qiWN+n9+BGEBI=4~gpY{~N0L#NwnC{o=KZfs1Jcr@2t*b)bc{a{ zw4Jzazn8B?z?wmH%bExU9R`#sm>Fevn)bFytUFWXYI}-7*zv?R>vj(59)FFr`hy&4%$s^;NN;0(1vdQfsw`{h&+ySGaAj9j|Axs@d`MAPvg~W%K~L2Q;l7R@MXA( z_83T@5}k|vCBL|KWP72tAyZ7h@v8Z*qE_9Z^MPd{s-4@BMGEX@$S0Cx==^~}0pPAK zqR?{0#W!fRFoL(^gC7564|GW|&54T2R@#3wT|CL}4M%QU9dI&g4ZLd~UHb?n0$#bO z`cJ3HQ+ODq0bIEPkSOo)NHqeMp zQrKJXXU?NF;}{cp5gQNy`=mwWtcJr-78n|H#y9&lo#n$3q{1OFh)PaJZA*@(!B^T1 zA^qxerC2o%&Jk8a%+Fd!6%?tn%v<>%>nJNUR7!&@>EHn^Ee}wITYbf$iOliTPZX;_ zZ8Qst1Kmm_2(a6)S5GBdK5o>??rbkp+PH^YDpB-$NZQaD-(%D`-HN@RX&ymFBzg9; z$2EAah~;RKYAKO&{9ghLsffk_?c#tTp05M7Eu^o0|4U348!{ODKz{ zGIxjlYRA%<&%3?j8OVj1-m z7n&*UA&vV0xDe}d(ArP&Nw519z9Hj@6U{PUC*dO%%c9@iY_>mDv;8z>3TVFy<{`@FWtaI}UvYT1{v)zCqL zM6Zf3Y53|}EMf3*qq7p{2GW=Q*>ne_Hb2Dp58SEnT7-K6i6??P#x(xx7ud%BZq~X! zv}Xn6;Oz*|>A{Ey3dvVc`y%e_FNWW7`OPP`2C!Mpc$z4!s_a8@ zf6=^W_pi>+4J=(l(^c!2_1So(qP92!j-J0jUv4gNTct)c5AWwZt`wcf%bkxq=i^LC z!X$7P|03g}$A7?o%0(EF8Rend8XD+b3DnJ2N6c6KMphCaWdKwBa4G!1!A&^{^}_l$ z;JDl>Es8*BU>Uuh>9a^N+aa+W-8^#F*^qfEyWz?n7y=uF&6LO9dQ;CZ#|r-VrqYx% z0Xm=ry2Oo>hqe;Ro)k56e)jq@54sGkkfMIbR5p?*X{U&scZlE_?ZzXB&&MDlYM{G( zel&4H0gU9`2bSQp;E&J>vLFB12RHZ*Osxv+qTIzPJ_XO}!<^VL;&L3dQ?(=w^jB*o^yrdz{1i4uKMyUP^D*XIrbu+)_)K#|{tKgDAL$v6KXMU;bsj#q-Vg_0`> z9n;BqNV*WEwRQF*%yzF}$i%|N9P|`6i}LYa3T*^YfoV13DJ8rKC>07Foc8r`5?JU0 zAho8?B*Ldr2?_=?&tJpyq9vabbdwK#+6ii#Pw8TpaoL3L<61#BJ$^t_go6^U#TWBJ z^$4FZPN4A$?EVPU16oy0y`IGbiq~_EuAU-^X3+g+(_&@^Z$PhQ?3Ti!7aPFbqC#!= zF=AQ9{?)UG+9iTx<99W6%z&lpH&ypO- zMgYiDPG_g4kaEI%5we^bF+89P9~x&Fe^nv3oeuyPE|um?foEVzx+A#L!jg;Z5!&#= zxo=QdBHQpFhX+xML;X8yFEDE)u`>A_IX8UL%yui^RzR|(o}ml4ht^xpQplq=a>8fO zH{9Z*_uB`B#{^OC^%eJ2zj6|Bsd9R!7SKV@Wy_Eaos}Bs^s&~ z(oA@X5!6@>i3Zq!i##@=w}&%Wd8h9-l9liQV5fr-avIKr)O8-|LBODqc+?~QVW7M9 zAL1TI&)Cbzx=}vLc5=xdAoHdVke^$~V}7W11RnP>qL_sIFg!X>}Qoc~4%;?6aQdPBenCA}}=4VoE`>^Mi1zi%u1|StmpnLlqSo(<7ntrgu-CKEThvVneo{vjH_efVfbG8f1KAV>^Qiei6J15 zF5o%Cr6Y%uP=b~Ry@Fs9-BGRZgOu#Rk>fkR5l0Fn71(w|4kwsq;^DId))<|3T>6-h zXPGQF9tLD7r_Z2PBw?2;LG};KxgEMKN$`|hTIxFplEiAywwHQNpF@06O6B}+#R|xi z1O~|Em*Pm?Wp|)ex&aBmf?>=%&@ecu^%`X)1yZaHo!6mgnoNF#@WoJl5G#j0rBMN3 zKWzNfIPV^{!GlnEjSClvME=})9$kVSbO5Ps(!TBC)u6V(_+|hnanuZsCukW^tdjb3&Mm@)?#qZ4(G&8`>ebOWsXMgBEfKn`7jYTWJi^rj_$y7gZb%@t~`ks7Ij|6@#VqMw|niH;F=ywltobdCS6Zr;U>^r)hn`6}B z317ty;w++ZvBPBYa*BFnb(-;GO}+o@q*#Ivqu3RLO^V!HV$`N8`wr2?b|ry#3Jnk} zxX>rZLOg9Bazn_PK5%${VUvxAKBy(2w?YRI^pJ;;818`nL6)Emk>5>3Qixu%? z8BhnjL{S(tUi*-4tAFXW`ksXQw?czdTq z)fBQhjM_1(z3_%swBFl+f-&tbL?SR1dq;@;kz*OU3M!xAv6djgE(LB+dKadf_4v8{ch zF)^oMqth3Y-+VFs)2+DoBPSC-0Mt^lmc6$_F_M3=*nZPanOp{7TkkGlsiW(pI~e#UpWTr-w)0juif*lFNdD?9S$*lN(isb z=5nwR${T$lP%jEXWEH5~PK$rDD$8Lx;W`Y09qHXGtBIX!#xqFkn7m(n0q4tMgM-3T zv2z3H7$0k(bv`<=JYa>61?<>Pb`-Q>PMI;M%EY*H5!6lR+^N4WW#5{IC^b1!X>(7CPsxTR6U2rGj+a2W{VRwW?wygFa z{}wsYM*>XGNXi0`S#NjOh}m+=Rfp+w!|pj4D%v)mBAH1V+9x^jsSst2Ucy8I$(V{d zdD3e7&11tTlTV3u++pQV@gupP@q5Pv;*+8_TGF!XFjwvW^zJ5<48u#e*%EZuf+S$n zc^b%Z>K&1(m?hT3OB})y`?W6j9muLZ;Egix3))a;E2u3DvdGZ58+!@|gZtl_lpYZ9 z1ih}1#ibbfbG@_|_V*L!)NF=n6B?!y_9b8y*!(l3^d>c&c$(zEoPki@UN{O|v9Mit zy6Fgcr%tzq$~pU<@3fmZNkF^kMp|W32`Ma&n$&pt;d*w2>3i@Hp=>2AuBFx+v#zup z`TbYSS-Ak*@@N{~&9C!kr~==>JV(><4}B* z#82?`#bZw>{5$h3l9D7~F4nYB&Tpo=AP>YR+lVZvn(8geM_-H^uhH=-b~QT;r&zK@>}6NjW~%)q(o&NC zu2bEeI2CCqv!>O*&yvokj)$wxGM0u5A$CRcDgOjXye%%?)sK)!3gO|FM~E*c3{9yy zaS+eEi^dhv(UJQhxFpsr_l0d74cY>{o+iMvtJcVFh*O3TcX{n<6v3~=@H&3sLg;(_ z@$96z2+T%ikksg7Ta#qmShdf>|pT!kS@qQF@6P0fWe6w>Z zWT&*#pnF30WKUxK>>B(M8f;ZcRmbyicexvv5X!}}8#Ixc|HoV?usw`0DT)(3Qlcy{ zXi;79=bI1l`G>XtEfHhWa%C} zsLp=AAwizJbT>RH7qoMdCbD%gKR0QkZEXuwQE|21M@|-DkDUJKYj8#o6x-ndZz;sa z?4*k!y`(;KBfeXEhjsZtEM5NZXC=Ri*J_&9?I+0- ziXQfcoT{N;(9n-125wC)CyJBaV6J}1zmN7G2J7a6E;boG(qrB>cxlTim0UD*Z4UdP z^FlR~*9JxXWjuqgGv4s{^;7W;*3UC{>@S&0Y@E1eFlH+t?I2SK%kIF+NIipMTWSNO zNM$2hzmqEo9WB{!klb(vY3Lf0v@S|*{4#MEX9vSKc`91;nqoj2&rcI$R)3zhzwsmYD3s3F3&ZikWgAFf}$S`o2 z|It*45HbBDPDLnx!bQ(sq$jsy1A|k4rtb0tLz6=~Fr9UQ-_8EF4{*Rai<=3hB^#1) zth<>yfcb_f4riQQL*frw1V-e2O^U-Gyw;88-#~SrM%b%SVmVdH;%itEEsKyxMfaQ< z-{D231LdqDw(z}YDed_Qezl6&W=<2qsDFXpw>33x& zhD>3*vhyl&?2z6D#f24En#C$=lA~z0gc<_inag{Ok`C;=G9Q6s8?=hGD^Db=IoVi& z@NPX9#m{>7T_}LCjLdv&2T=72UYXbWGWux|YRy6hg3k>s-TaaGDM7 zAR8^}rsU_)JNFiU_2^P2WBlU3Kl)4^z&_Bv=mZnRECYpOFBpVnf(Kg6{|38dRD6(ys7m2>kB}P~4+h){1V7EIAAZfW9o2gr`7{fJirrpq%+DlSLWD?HH?!8kCxvVg|UWc!tkkM67v0QPVDAFnxNU&{ zHwcFx`-%oMp0B%31>WAAz>|^nXj8)eYC0o(r}0`m$miUCwJa!uFTd~xd@tA-$1i5% zZBWp>&P3Z;x+OFRius1*JfiCY{6!(zflS}6yz3m0ADv+el<$e6tzN++9n8|H_nY}iNt ze~SadAQU(tVYJ`&y)Y_rPN6CXKaBMk#Tcvb@6(zg&aTU;+TI!1%)>0#GdB`ygbi1V?RB`pyE{ zIA2vF@9?M>_z&u^*>WV}p%c7&s6@TTnxLp{SRaY+=P*lUG`|M{0Q`f3)73bxo#5D2 zBjrF6Qi+8M@TIKql80Gs+Oe9T z5scr^@UQ*^0vV-lhYVyAtZuFYj z|BOMKf=rQfX+}s%>ZwoOPC1-^D#`=gM3MO9B%VS+e}kty{eGI(TE_4`q7JHZ^6q~S zQRg;Z>>v_&)kVj57OfttkCSleMY`$5HyUh$_P>pZA9z)13T(4)m}~ueWr%(?Y|C?t z)(G^bI5^V!ebSW}~yf?irigA|;v6gADm|%UXFtbM0nv1_~3erX7w$PWCLq z9%aubSEx&_rqi>jEWUm^a(;MR?S%P7W1Cj^s@t78U!OUXre%8c=?+pQztXic#2`=( z3NaBB+g(1_sid7I!gh{MCmMH3blaFp;$5+{V0SJF1NRJ@tvLL-r8K-uJ*7xmU1`vZOflWNOIxZ4W9n1Ia|R9&oFK87-bfFYiJhEly-RsmBBb~z%3yH4i&%1 zJM{6CB!|?8!UdZ99SzR};gMPPeatP~okMnPl`5yX?WL3-zLr@lFqhoQ^4VI|ip3_H zFB}xk-`_lJOzCKy4akayldT>!nt0jwp(Y`k*_dwnRS)^w%GN9`nkH#Hz^I@})sj-Qzis){FUM=B)W&1?F%+ z+weO+wmttp7PB>U&r5D$wdei(#rb6K-l;!+<0%`1@J&_O|L5Mf7~Iiop%==Mn@I1>yhA{NbatS>C(vF6VsD zIp1>*7C^LPBLypV!D<;3fjjOFcMc5g+@#9{M$dxDev3QT9W{JqPk8OlWJd7*^`%eI zkd@BzBI{QI+ZoHpP46ez6r#0<=Kp@_8rKC})uUtAjML1iv+SR*=H)F}mSvSMTv;99 zy`n83hIrf6V(PQj9r4=le5w_5TbCniez)J`gRlBld_d!@lDg?|Fq>KWIM%UPQIg#T zwmKd`{cbE?jSaIKr4&$ry68cKUMG{d8S@?eBqzl1XS9I1`Lyr)`s?KC&cM|aZv`XpMTSQhpB!; zp&bzIE69I<3b`{L8#MBw>$pw3KcPknyGxR>PRQP8&rTBg6-L6| zlO(iq6e-cggynWz^S`coa9_jitoNxm7H5j*s*aKjy!*O!f`Vu6Xx`1m(bIE5KWuS! zn5gfbLTAyuMiOI1l6|(+={Izd>q~v*waISe1y|GDoXH-f2-y7pcXWF;wOw91ee6}x zxXPn&;}V1S^TLN5(uT89SEK`V(o$Fb=|Bqcz~698fhk;X5WCli#X?G>fn7eH)0=;swS{6F=P5NZ`CFVupqEOIdgF8irj%a5R5*~JA4n+~n;#_`TR z$%-QY74GZ5^Nj-~ z!+vaoz*1wpDQ&^F8Y~N%caKXT4T*-2QCYMIE;JxZ2fv>J+ZE3CDTD$=BU>Z*WLy|3 zN&MmykKjaY==SY}bSnSNPd-h+^R#8g5eJv0E$YgZCBb~1YEbzd6;rQ^HO)(A#I zY*;(=+T^OFgznG$1XtwvbYo;PLUJ?ydph+fKrW zT=?Vjbcwy*A$V#8Gy}Sgl-^B0z=C5jH*vmf{Diqc{AtI#KxtsaT;KSw!s+ev&D{{er_ljZ^8kkw81m$+usOH=%Qw*W&ljedH|$xflc6 zR}g>W*k$bfab_A86hFLTIW8QaiEfTXhg(lQ3p@m`^S`p2_tB#jNlIYKzfuQKN-rew z(UuKfOt8n(f$hlNzb@+J*>E(-K5TFmL4`<|-!-qW6fZdqvmMK;a4qUP@ec^0Dop^k zj>A<@0E6McmOqySR`e}>oytdiREoG|IBh(szE+n(JCaO%=<-wBvOB3}I`K{3(-1zr z>UMm`O9cwZST`moAy|-NQ;j_(TCqKh5_D#y zS-UHWq8!QaKrFxdiXiM{sl|34h0-oy3Ii4+CiI7_0l3TqeV^^*1YBegUUt zAxmJ_UNws(Lfainxt_alrXB(F&6<+urWHRNqXf8pT{g|tPcbu&5EWlA_hsCgYgYnq zPI8Z?ryY3YCu&7i&$A`{+KA}>*Q>`dTkR*=WAOcOYl@1OO^O$sV@U*x&NLt3Rbn1c ztN>O(Ek6VNTJLresIOGxB89on%-|TLsW(_t82?5>YLD-8rEG8!l8 zAvoGvIT(D`qfpT-_*j{7FW#Cpv}QJnHwUlo6qn;rDD@*O+-qQ*QiTrAh16)C^%%3Q zom8F}ydy+twFUZiP)mE)eGM0CooXI#CQJvB4ky_K{dP56gq3~FtE$*lBvoI3X zogSU?=L8sJFlRva3u=G$d`IX_6+h3LzMTI4Xr9DD=lzdMb~ z-v8VzBmwrazzYhb$w5KHrWe!Nn@puZ8(F&yweonAnb*<>3Sax854;~6Zz2n~X34CB zv_Vb4jo6H@VhJ4AmMgdqm@krpZKU_lX3^&UQyf9H8vwp=pcs;J3QCSgNNj(q4}=n` z@soq|sEL<1U zYmrVdC2ZHRx4x!<90Ec6n)!?mjIZIJ^qhJ&sf*Uz{4waY0Kpj&A@=bbL{Ldx`PXS2 zWl5CK*;EK^JU}1Jj@<$SXgc|-@25X_6=g|y$TN6N*b_V;t+p5Ri2~L2BMks{6tBh5 zjd1w*s7G8B)2n;rx9>0ySEr!`UzrW8B1^##slUzGxta=_wVo6P61NM0r!A;@>Duz# z4rSb|x1$gNHFcK_df3fBA`?uDuvy;w+SG5ZxowEVWXXVE9D?S=Gy`i#Tv70 zxv@2aF-aNAf4~DYYiR+IS=^`x`0~#%I7h^Y#%mDR$*vT_K-ZY4$(DnI0?C37BA~5x z`RVT}M0{hy0+dXb6oqAH)E=XK#GOMUxq?9_KtH&@u#=(&FB)Ef3 zxB;O$lDDZG8kc+7dlt4b+yXUNv>LI%@TdUBM$JYFzS|aKN23y@USnF_vTevsnjtE}iz>`;Tz4esjL>c&j8Rxd8hH`Q#i6ZC4bEf=P%Yjrj1 zF~%0oL{@>CNu$>FPcw^h__?&;=mK@6T_l#hXpzTql|->)4rJUFGTqL@MttZBCYk={q z1u_9aXqb5^vL8nG16c2{h?e>d0~ff3+ht`Q+f@k+a)2UrBmT4(mA_33<+mg%9f3m$ z>Jj3XXF~8y`6}`=4e7Uk)#LismI4=ZD{6Y}k+Oc?088LZo+07lmrzCS>JL#kQ930{?zr5`pO zK+prU3{T+UA@qv%a=oA**@R8V%Q(izA^kSftkZr09(Ui1QDu@Xq;KB}eu z0icn7>SH)rwK)-7Bme3~bgPYO>OAp}z4-~ZD>!9n@*%TlmYk<2C53ebcJO6Hp^7gm zDDi)HkHf%$?CFJ!PTU*f9{(pO0tm!^ncHULm93CQ;D}J2gAg9A$ZiME5wHA=_BA%Y z4%IIpbnIUtRNQMLczs||=Te!rJOcEqZ47m0Vq#Xhp;$@abW0*+k3oY`({Cy=k;W-q zAF}6W_&?JohR8t4tuV|Jq6CH&Ow%(h(Lc@fpG1k2&?G96uWqv!rz2yf<4C*Uzw0UF zy5xBbEQ-%=Olk^k6H-aQRn380G$@I+Qsu%OvM)hN1-XSVbGv8xp-vm=>v>i840zL=qRe4_?>f(-$xRhjdmivfmRIcS@i^&;9?oI?2jo z$~A-!yGP~ez@t)8&4ch%7kZ#q;+m{FC{W&2|6zoAJ&)+JnTg z8G=Jo8A~-k`d6kid4stj$PPR2Ezsp~(LxvqLCf#>lB5JhaGJ5UaoeIP=czF#IE7(@*w8D%a7AHJE&0^4AfkN*p-ZMyB^!nXD zi~$~43aHqf#G>RpR=a&cp16x8{)bj#o2Q3Raj+2c*b_N4nhnGlAWEjgyj7pS)i|Fj z&fb|EqxoY;Z7nC_GgKO5vPVuSXW#a#qRg zJc9{oy-lxVd^oVK4rL@)wzW=z?BkjhKA>mdD>=6pkNP4z}F zsnf4!3Q3$7{Tsou`i+D)?E8oY8RX}^?}1MAWtt>M_Wv&aI>qlCRn71fr|laGPD|qW z4b4g-q7pFEM{PTViYHuUCSV{GzF_1=Cz3R!>J1&9UtgF+irM$F53v=dobp6z!!K0j zO*>8UF?Z|@9(=4oUQS_|jkrlMbs4otvt=t)sARXBn6K=Dbj87n{cESApM2V(oYUd5 zD!RS#?w@eCf&(d_#d&VW!lGHG*r550Qa8T=+DzTYa+3dLL8;T1rpv{pcemn2G(Gk# zVjOZE(L((WoC1)1#18IjZr1b`G>vJ2d!Ta=I-#NQIQ4d?r@T~Kq9OYI@Sj~BWXbK` z6p;pTQ(1{23ZK3-|Fd)# z`I>fu5J61|SK65F=u%?9H91l>=CBksL1U^x-vqwL2Z~?Xd?WgXe)SaAi}}*n<_9as zgLD%rzuo|inJU{LQ-bvF^Ooq;##3%?o*8!?8-?}lJG6ZbwY~Z4133IV*-@APi8RWc z^{lrGi;WHi_aS@QV>Pp2|8_LX^SyYLQPZU(uE90M*=LWbJnEHb_?l2ssNmslGkNc6 zRRIq7y`Ofj)TI5STj3%a`jPoN+yN>a+QahoA4Dr|KMc1co#%_P^luL#WJgQ$f1BkH zfj}$TmhpOEap|E&F*}$%7%_)$i#LdWKoGu|}&`TpPmOuzuY?=Kq z8WYbv!2<6h4;_`&Q@k}H#z58fZdNu-+t^7M{=SuK(tS&E`92-d&}A%i>hqsr_1J2< zb_Xn1OkfC2+tE*z&DONRX&(!jY7dR?=%R!X(FB)6bg+w}Hm>V>HW~m!w}`EhNU6@N zr->P#c1@tHc+YV37|XRDrERS^Cf5*17CPErOh=bBNKp{FxZLo41ta*XtwZz`mu&=V1ZW~_A5~<% zmzc`l1C_-nUcJ=z_uh?2yM|HBNK{`Qm5!tlW+`oPsC;!7cA`uwAS?QMB(i zhwN56E7gNW-EolkPzwa{32b|BDH=2Ul~u3nPje>)W~p`&=@)Q%5IMhO*K3DUbPY~{ zUBkFHteq>Shb~<&uxTv@@(YbNP^LTWXbbcBC(L2BHTxf6AGUZ60QTGtyO?+wzV$}s zrRb|kCX4p>+)b>XH#8}auESYJB3VANpB^&Qzovt1)%W^Or)N}FhHE}#<5If4uzZ2Z zh=)jH&KulTeJHxJe;J&Es0$LR4rRhL9JSdhf?f?Sd+UJ@crtVo9KnZc+WR{AimIa) z3+n)d9bA%iJgd(~y_;gf&8h1TaK*IGmy%j3D@9U4{2b339k@B13kYt#q+X)rfQ^*Q zfxfj#*uj{%~o#uLSDauy# zzrgBzqcIBhEsG|J)wHL{Lp^6DFl9b09dYiHwM5PBC@QwKY!6@{tp1@3 zYDxjEV-A-JvXoflR>dY*MST$34l{eW1HR)TdN8RA;wwF$u17%0rUWiVa8omzn2quE zgQmRI4mbABJoQch4TQQ0rNC=JliC$1XtKv+vRtI%$?n> zR#N3j?0Ce?oll6+Hiu*{svPnG6mCRdwu{Z~6!#}7$uXibKR`eHzC+c#hHLq8*GznY zyq|D*rS?~7ieh5;Hy)ZpEwdD{WACOz4s)?ol)ar^24x;CEY5H{wT}FQdMze+P(^>f z08C`&CY`7<4B3|z<-Y){9?z}eaiqItoySAu1;1GMzzA$NZqDTB5Z1wz# zjwmfbu{l^a`TY|lF=KldVhhu{DH3gssPAEX+uz(?nUostWk1SB9dSAOR(4{<1~YFq zT-%NBMCl+lcY~C?m3nW+-gb55B+7Ky7K|yFu3j6HER6E)+!06-OVT2&80A#~yGV=v z5A$A9^STZYfdRQ*|b-JlJll--nzu4#tEG!GxkWq zjF(e!D27UH9!kJvv<6Vx_a?|fU+|-iET&o@z;w>}180ARB~^YV8G)*G(AW_B71e2W zcaFl5w*H1#|4l6NH6-E7hqvkD7SCF&$^gpKd6(r@d;(`lNWro_E;lyBy3T=vQATeM z$Ntt(6YJ^AHrE6sG0?m|(xQ5Q3F~TFck+*^e;*wR*;a|=dH0vcAVz4ItBv}$`*!7F zR$7y34=pQ(XBZaIbRD^$G3~7=BP-B>b4q=_+q^)JEc!DC6OMkSg<1NqF7av-DVWrs za)o4}!^HxT(AW4zl*!VsAUJSOm_?K||E3+bkUidPBS`GG-`^1c(>dtQPE0eX7culw z^1hoaWU@snQB7Q1-NTMKWW74wMUc+zdhd1T;eCi{y>)=PzpiV{g759yZ;5XQL`k@8 zs@Gy&Ax?6=_VL4vK@8hZF8|CWmYr@l%Y!mA>6$QDLD{S%QMRMR3SRk@x{VdQZ1VQ@ zTEZ~{cB3SqY1Quvh(GB0{^zgQLw}p%AG26MS=?+47KA#t$3e@&QZHc z3S#};jzl%1)Bl& zA7T~yN%*p_r8;rTo87${E2Nw@*zVZ&dBHD}#a`sap(1g{o{9RpUdLzieCVno174v> z&J9pvAfr(vD%jC|g>B>vI1;&oCEw2aDVtf4q+&X3hdl-cYm1d!Tg8Jl|4wD{^@%Je z6ecLYPZp5RendweRbpUCqJ+HqcjwgtZR_t1^S`zfj;e&49Ig)e6;3XCAOYM3cl=zv z#UolbAiLo!H%!f5SuCN8xhjLn84aC@O$aLlXx>`~o?Me4{w?Qlm!R}b8O0V>FQ=eq zPzv6e<~6?w+9n}X9FQJK2r!A3J+6mGW^Q?mc3Sjr&uH!7r{yz|&5_Q$6ub83QNn^N ze=)@5ry%i$?`c4wa|@o4hnD?Tq%USjYqFw611d1f#nnkT-LB!aZ?9at`fHtL7`2v= zX*WYKbpzGQd>bn7eYRrNA;}?LWrom(yeWGgX64$EO%G-G@1e74YJK?VQ~wQH+nhYptNDnTx1t;qj?JxJNJ^+GEY#z@ITp zvb2nNt~dAx!XI8&Tzf%w+45JKF)}M>_B-ws*sB<2nI?DX>}Fm+#txGhbt_fC&Ap#Q z{B$>(0j^P0;ZUjrd!G_GV#qlv?|$@`q$_?^65c=T{McX`yydpY0gpubw!X%rKF0~5 zL6fUv?6!iTYgeT`f6T{+YHc&Twv~jVhuDq}GQ|1LBARf$<_I;ORjKg$@n*R&5+mV) z7Q>IjX4g|L2RG18-01G7`>IPR9R)_)u|uz@$K7&7b2ImWFHr4y$fC$tN!h}fMu7kb z1V}_jn`ax=J3I+e#DEd%%{HSD+$02K5Ld0&MAqpnRzB?HnAhEEg@S(so`MfEYEL+) zc|z|w3V;eZE_S=cQT`1Z)D}PSpcPbmGVNzCU~=Gq1RTlnkCr^ut$BwAaZ(GnxuUwqukBhMYkN0;?ic0Jf& zoZ`XLDM8uC-~1Z;XJpn|i=NSg(!(yLq5pK`p)Au%ZxSor<#eUh**3{5w$ALU2hDeS zz^}gdK&+jR&n5=A+Ptf5<W|p&0aVS)sSEPPW@M{g*;qW%EaEqQEsWTpYp2@bnI0st*jml8>RB)%amB7aS z-=9!xL1wBr{i6*bM3-y~VU6w9Bwp|ZKRl8Z0ulAxLF7HsMbKY>qwP2>FSZnOUrWLiAhUg z*SfZJJS~o|r`f`@IzbuA$V+1tfoEjLJ|XxCN^vKbpyJIhqE+$QN+tFo`rK6^YU~vt zm65CV%{>krdi38yd~)7x6Vm65RfKkipTmGrentv}XGXNatN}6x{TczSCd6V!X-#8j zA8?5s{8$ww5Sn>mF>o;XuB}C4;>Ae}BTSehSX>BRUIX<^n!1QeMKYH~v1%g1R?i`b z#Ai`LyE7J+jnQFPZd|dfkgt66{ZSMlDbdS;DzQH=WqF~OK>f_R39)6* z-~M3bU?BAb*30}5vtS2zE9fR_`t=}g!RG&RHJ-C~gU)fkmaL&jXxi}42lTJ&C>a6` z2evs;rnJAcR;SJSFn_8Ob|YWg8+HW5F(qj#J7FT#CFB0{Gx0DP8}BE~(PJ+gGlww630=pP4l&!0t)$6*3Lx}~S1DMh!lkHg43@{%gB>r5pc#QSAg~> zW~p4!H}Vob%XhbB(fI4R4>5@-;It4F@u3IMl0UlQvc?p%2|dXfeE4aI!W_KzZ2#$> z2x2$CN%Pq_84mK9GwBBJTES&_5}^(=q!595QHx;(U7Yr?K;rrTlGus@%jJ8PJ(XlB z7e$nA^B;+If}A#+28HR_`VIHGcs(Y?prTo<0wwOD1Q+q1rPhmBF@}ditbygYhXI#) z?LQn*#7`PN0X6z~Lcp+emcpBr%CT%qxI#`y%EIZaI2UZe9v(QC6 ziFwR0Il5+oaEd(`7G+sBg}~=abF*^3fpcuhYP7aRO@ZJD8{rw(zTl|LdP#L~*BM#k zLD2XdnQC78eBsKnE&_z0D5gEq%B1x12tn{g)~3oImI^<#GS2pp(qbWB)V zXcetF*x#&xxnZ@D3Tru+BAn^m|EBEi%zaRc(q(}0LogPfw+H!LTUmHyuVf~`s@#YZ zijql@Mc@r1G|9SzZ)rM&S3M6O3-4xF(VUi@TA=B~HXbR8 zVJFo=AC65gXUe^i4eAsUQYoBwGiJMaWarj>SnX=)A*2^!w}q1~s8*vk zV@Wjyz#FlOASvJ3o4po#$P|4)$O;C2T!l1onHd?y%U?~#x9BH`Cvdomx+^5auPYp* zKbx+!3;9=7$XJbO09FaNGno#{Q6eg7(5H%8?qh3?Q_?(u2NIAdkWS>DWsVy?xLQN1 zw4H(KC*1K;t;}reglCvr5)M>FJ*iQ=z<2+e#(7i|8`G`;#H?T31m*e_bEVu2BeG zU&U8;Q&2TIa(;^!?SpMxkIwTpi&46fXi*}r{Y3EL&;ZTN-cU{@8%MIbUS22GZ zSduwn0NN^E&3zl-oOB>3Y+9ql2+NoCHx`Vk=|z^W=m8QZJ~F{sbJ0BGq&r*;>E#)E z7#~F_OmIaCo6%!^YeB4aTZJ+;(zP|Scb&qfe8b4dvtj8VrW+RAZ*1yGIniIqp7Av$ z*2I^Swlrp!%CT&`!;*>>Yd-rq&48)m=|+LpmB!jBPy3`k;MtHGy$LC~4Th7~uO&Q5 z^++IFs`?Gouk@tT)tM}NTY=uA+Ycw;GC=iYox<6{pE2_?T@|E3r&j&~M3y|n8?bM+ zV$_5B8;ypb`fw4lgod4bCqrU!UvsK(G5#m&QJ5mgmr$Xz-W?1n{ZBfq7eZP%Jp1-B zGM7{rR~Xm1uFQ7HLV1~a5l(AFT)L*HF_BIVA(OaJl#g&6N4e?oB0DG>vlweY3kxAG zWo@GUH8<%ondx?L|IAi? zgZEof*s+)893O8G;lUTw*m`NI-lEG|E)Wfg#-KvBxsA>_(H9yCa<8b!ZGFkk{ z!`)VM;ZlXh4M7RZ3W~H`1x$pz-=Hd!!V}>FYN0x8R~2NC#9*I;b{Tj#dy$KNrYKLs zK|^e?ju=OQ1wx;Fp{&-NA3kM^1d)GJgyN@MvKVGmS99XhH1X|GEokL`sendGiA5|- z5BU^!rc|ZfDgW1)K`s)RHS;OaBd^nL!P-o7ZlkEoU@+a;4>Vl?JyK~AGK6H89S-px zd@aj_rGpU)q~(MZWqVsa7*80j(gT^wELWCTO#V@78?WVRv0RIM6Iey@hVV|pd28~mH&VE`&KXtRo0R1pzNZ?;fCyv=V> z!8eMdLZCt^3GR9L&sQ-Ox`;?pswGcDJ*+~#+N#tBE8#$^!^58-Dy0mrCS@R|W_!95 zoprATx3Mo#a+CJXk1XC#J!Ek$F9aip1(*;lU}|kzgnOZZAQ{4as%0~9nGMLruGedg zmI9>SqZGl+?2TE5%xPee!q~S&GbfsN3QWXWgb^P0NDwhrE_BYGwz&eDZ;Iq9ZL5dXnGwF1?VpmK1!U zRM2~uC!X6eZ86@t-nop|dKP5qFL~@gz&YECYB*@FzVb?9KWUxGv|o2bIAfF0`&1sV z#+Q9bg<~sRTf#{a01ynL*$7T*+|(g%gklwIgwyl$|1r7G6Yx>MQ|xqS7BP)rhJL{Dz;0NL|NW zAhIbHgu`-EF;{3(>Okvq^;tD9z*p1TfXaScj*l0>RT}}fWG@g+4ru}}xj9Y2C2sw$ ztd1USf%r<`9oJ+^^#tY&vOHnl09%z<86!1CK?#FiZt({?Q&3A9Ol^!qxRXRs)y@zS zIwCZc=RbGGKniuIT$u?H01PoZc<7hM0;oywF$^s#^viTXB0M1eB9YV`_?=pM^UMf&nU)B#uByCJX?0!Gp(1 zh^*WC?wzivUo44OGBlU|J^Pgl3oD{$Y;+)KM*h($2$^1T5V!e29uL88mvnFN=x zGt;J=@&)&WNAoD$8eOq&xJIm}-N2pXrJA{J%8^Efhy6GbzpU`3KR{c{PK zAHCB~B?vuRpdBx>QUXXh9D@f zBQ}j~_JCF%?Oz2%gqoA(T&hVFMRIizm0(MOLFs1u37*PJF^7=zMs+eBsY|Qb@CXQE zGGX^%WUk)41<9My4eY@=5zKSJz(>~r&(3bve2w**ETmM`1x)ZQyHE$FOq(8hVW!~L zx7rTIj7AK}7Cr+&H5$fhF7Y6KF{h@6k(N6BFVbFScBw)2dBG14#P~lbH2`P_g@#l7 zVF^x^iwxG1l@a@22+G8XE7(f}YWrt0f=JWRYXMI{djHW>1{J@(sYiDTc28nuw|W(v zML36@N!dVbxn9neO}P(jtt+&|lsLYW4oj0pVTW9oN4wB*&%i!OJU7joCTsBa2V=J? zK>vlCL-PNhs^dLAa-hcH0usTZp_S;+MyIu*r6yDN_t-Kt9>OHLlbtpv760d2W^~} z3u_HooV^?=`_U>LAgH}~EU!=^u$RR<;cR&}U`nJDbY@|_o8DTFPgwpWsy;wQ%)y4w zSn_<85%rQcPg%mJV;tg-&dF3C`bNTFImRn83E0N&0qk|=dE`IYPA=H0qmNE^Yqs5o z!NXsC5jY=;Rl`4Dcy$a9Pb{Kg`wV$cy{Sh`0`+D4=iF}?2$HHkv|haoCx1CK;7s(o2%5zi|<2JjBq zAD+sD!9tYt;y%iW>@Z_ACxtz6e@%%NL+-Rxv_3BeVPc~$CE2s@AR^4UAhbkm9Oo=sVTB4)G(Qa`d^Euq_5qb8NH_=UPn!wIo_wWE@qgjr6 z1P=EH<;zCE@r+MXDW$Nj`)+lWy3{Sk4UmiLlxSpa`T|q|hR|#@M?s^mU4@y5slGI` z;3yOdf63*)1@S;}Q*yW!%wAQ9FhMu0W5vm`!g6j6!6)i7QCMw5gp(?4eqNw;_hkPl9*JcE^qGaU%PVhWgD8-oCF0l}4vb z-J{z<*kQHjuG&`GTZ>GA#3==DG>QmclISv<`}*Gw%QcDSFy+<{_EQ$^X2F387&>w# zml7e_@}O3=^ZB0RJe60TICapC!$w^ZQ^=Db1u?g7t2fQIG?CI5{9=h(CvljjqaKnP zwv>a2Q+e@TWBsiTB_Ki8nEGT&G&P+a2sK8YM(mh{xLWNABS~+&ZmVj5WIgsXTHi$n zxF8lplA$6kJIOjn)PzZBw{*6_$$N3MZW4`i)qVF({A=Bxg|@g-5J?ySdBZ{JGlE8n zIz940vRIaAzQyx(;jFqz5AJUOiKfEXjC_YcDb3>Jsp;-L0&)XYvc!$pgvU{RG)r-Q z?zRPxbA=EZwgu1f{k$Th^~VPpCL3vVR1N0b>_XM-=5O$s5(~I?XD-Jw*u0y5!u^ip zh6u?1t7(QJrlWc$lkh1!#h+QWilp&E?KS%wU9V`tV=)#qt0)4bTJ_c9nRn>4lj_Zd z0*msCIr`j-t0Rr>C^pHQN}07 zq00^@id*=bOW)Oen76_298pW$Vg8U4OF#k#NM7moJ6*4I=b4}Dj(O&@WP~`Yzqt^; z6gHT)p3wJ0ms8WoW?+(FN*@cRt(973+IEI3`5hT3_co!bT0$+~+(d&zB;W7`v!8qO zMKkYm}J01}rt$Gv*uZ>3H4Fg{r|wlxnL1XaRwdlOK8RG9>$ z+8N#=Aa`1J&!(Z|zx@c#(8Fs*Z5q)aJK6+*=s=RWZ0!Xt4mSzMQNqUch#(HY@jPHo)gR;-;rxH3 zG7mFACrasm2oQWToMUt`@TwUA<|pwg052A0plb!{Y*1yiD5Nf-2g863j0W$(6~_ZY z$}!hds_cJYjTSGo zAtMCeHH-vC)0^HOh1;gfhq>1MvPaUQ7fYW^S@BtHz{16pP8ogTWEu~5R#$JYe$nXZgh&@ zOIs_omJmK6A+@@oF2?}~oEYP7NIG1p=a~ogQ<8-#d&~105;VXQh4)f@q@XEG%SOdf zR{wbot>|++qWueEU-V{mP1lRa9SFifQ6qcpXQU|K^WzaII>2s=P;F#idZPGIVqJM~ zWPZX+^385eV^6Z>X7GmZZ;&*Za`C`;o^fZdYHrXyO)`IeThv?*k zFc)}~luoo@+9fnrOZ^gUV=$NUT)1c!g{JCWb`wkc_A{1ksdH8{CkxXYJh&Gyl*@m^ z(Zzs%V!ghbRfjKt8a>g3r@TRfNHrsEEKo>qQ?D7Sm{p!@ns9)_mEv>X#Uv4k_vzw& z;qx!=5x^+bI7Sh~O*oSHJ-2gqH5;9M{_V)^EZ4R)F=a$ttGjef;U_1#FFt@~0!>rX zD{X8CEEgNDXVL-Do}pfm5GrtVm@98*fh1xY$O!bm(|iM!KDqNZPKWpss6JF(8~yw$ z`0?slIqZroD$Oe`q-m%(QtJTy07s%H7iflO8z~QO?cqIm6P05;E?e zx0#-9&jh{2I{(r7vI$5DX@hk&T-)hDSmd`s79dbXEt>z;FFwQmJ*4$~e$(x&mq0(% z-`L`rxX;mF9>e)@ZS0*669)^nmAd%g2vZ0NBF(%PY>Z!w2SI?1pqj(G;VW40QdEMW zJ{0UxwPc(=dix9Cmd#@!Z5ZvGNnNtwanFIN&@ctrR=&9&7Vs|3(&5dOlSp5erfg5T z^_SAi|z9%mF$d99X#9iE5Tb)o3)le|D(PD8+Ya`>3@<0kMgdm9TDycZwHdcGzb*f>N3wP79_+G9Wh$I9C7n$G^O z;ju)X2P7l2tZVEHOIHg#WRz#LM{HKP?)3DjY&kpoVLCB|u`h(kv?=}spTYUv zO{#h#jcSpUcL^E0c=_pd|si0W6{7k(?%2iiEci)kZ&rW*zh2Aak$N8COq_%Bz|5yWOBH7XeDSnRSP zy00-qo2Ph=70vT+G|^P$@LzQa-io>)^taG_$7+JM-#A2x*p7=4Ne-X(89A+ zc2~yGx;keNA-RIQ_0sM~+qH`%brIAff5}qxDHA1V)G|NT2BEx!1A|fVTMo3hd9Gwk z<2rP#u&SSQ=c;-5;Bj;?rn-)YbfGZ=Yuo}4X^B>m)mnHzug3;roi}deW6qRIT7_s7 z+3Pcm`3Abz*vN7B*M5D6dkAjg;%&$ygc8yYP4WD%UYWXyJ(JIH$1{(Q5;k(bsu^2* z>#ILXn7!C1RGAwj8d&KJcvIImMIOINh-wg6E zQE8a6Pi_6i*-Rct!b7y^vqE4m*SdkiV*AUB(E0)ypLMZ2cWkPoaZ5&*_EeKNeVWgp{OW~iM#BhO32k#g9vy_X1BdOR$InGQP4o)@h2q;7%PaGpQnX&`K zMmn0BM8(=Al;cHZOdDS65xz=kmK<`>cgDkd*x(@^fpj%lSw=*we zeq%7Vn{f-c-hApPm1KX)c(%s6R)KT3l*NK+*JL9(9?IOtau#(@7eUSbh&*MiGmA=k z3@28C(&XLQMie5g=OdT{#XoW;sekisHWW>l_?xW6`h>A;!24qQmxh0NN$^@E+@+!v z95^KY3m<&GSRLXOXPaiZ$+x|p2A7g?hL5&}=rwoJh_pwMg4u)ZnyG>o; z%I4E=jfMe_%4L!t?YptKZ*- zDM2(AaZ)(0%50qISsBaPMv;d440+v1obQ&c2ibdo(N?e&LyEqJ5gps(50_z{|NhMu@81f2V)#y_StV(53{-$ir zjutV0TzrfW$y76O2V{`7&sGwGWoABI)>p=hxA98WVB@PFaN!{Nsc7U^X=J~n6Tr50 zLmK+q&-?YNPLhMn4EWw3QQu8Mj`wD^WSAm0=$rM)p5%|<8@sV4)HDql3}E2}I39O6 zOJhyZ;a+4)PPLee*EsXS-Z{I98l$gkPro+}9V{s0o;!E^sZ}f`?~PiIAhcSQ@w@v) zU4z-iNfU&qt8ikSfOY%TnDHb+m=Ll2UIa9Ziti&Z+x*g(A|;lzAec)nAdVq^l)Mlo zOIXU$#zi%8DC^L?-+9?tZB7Di(c1Zu_2fI%SFfCVuxybAL0OC3)uPd4Zy0CAp%!mW z8@_~^+D3pPuFd^J%J_urRkJB_XdsAd)&{l+ezRpqVfZc~ARD{v2_CRfq%g}UCSxTb5S$9ExSD-iRA0n6PIw(CV(@R z2L}@QFR3-A3#2$oW29k=snj_dJ^~lwWHjZ^gY%H%Y{5K|4Tzt8XKB|K?1)*H`_9M-~~h_ONk5O$1DS5&>5E_REPB^F>zb;=Kfl zW9TF=($pt8ZlY?9T+Ewj2-R77k9XaE)Vi)8Kth(59$eB=p1H$ z@c+^ECeTq-TiED6r^fDdlIlzZ2%$2N1VpS1K*RtiAxw(W$P`hrLBxQl0l5l@csc|` z1#J)!MWvY>L4zowqE-khDry9;;@rSh@LsNVA}S)n`-=PCzt&rCy}QYP3A zZ|_~=OHqO~m+ERUJq3#uCI~PqZSV^VbErA1+iz=Qkb#qhdJ%{d1vf>PgbyXHoIQ+a z6B}l+A*j}_WY}w%;1y~(ydedr(82t?iJ*r*y`m0!D7B?I5)BJQjJl0dC+fk%~MX(ok`_|<`Si83S=xR7KoC4MJ` zl6HbB-}F>s->E2?rP+0Jh(q2t5+T@nEZY;R)k;TisFeb0tE6fiRz+93{><64`4I$8 z@PKxmvjgv2m@|~x+8gUfuTKabc{_DQrl_0>W^SW4hKHKClzT9TO{nm=r({^QjW9Es zn?+cHmhLkJQC%5oV~G8yPGgR}M1Dk22(j2;7bYD0^m5#-5oL(dhFLTh9sj<;vuPbl z3f572)ktIz=dr5pyJ3mG#FPFz)x`lk;3Ah8oVJr?&qu##LS$S{fI|9=STzFZUX=0~ zEJ^#@EubucSZ6awm)BtD*bPB}0T#mS%BD$%+ zO=K*eCRUKN&@Uv8T3Qz=+$C;j1V=?1JL15{KIJJz8W1m0#v{!dUc$*PpePw@J-y0P zs^H)MdsY-@c+bUgY>{L?AkKd#cPF;@5JfIIii-z#db>Z%RQh`1hEIE8mqki5Ua^`g zU0QDi8YFvfB`07J`n-#c+-mdDxLuvxO4I|{A>6R+$fVL@nyrwh zpI6MG0+p*AEljG7XLq4Hj-C0~MQB8{G)0}mo`VCksbV^zO$J@c+tl2h_uhxt9ITUB z5UIhoeUzU!tOuo|aC8dCxsVzGc8t*Ij7}Y;_^zkCboQrV`G| ziqVzWr@TStO4YJzXj8uKN*ocB8zEk0Hd~1+x$Zg=&~q zP<%lytH@`P24_TxgVXE*%ZjNW0p_uQ`nBBcst6yBrko3al+?=jcr&zPt*ah;z@o}yFYw% zDIp8Y()D0;8pTkkX3xUYkI(E%p<92sYq;D;(E3iUv})-}^Z+^dB~;_Q^{QP+4>M7& zTr&`@sK=`^r4o|hz%}rJ!%4vYN%8La#M1z`56AV@g)kgesZQ>iI9kAM(Op6!adC+q zXt|xjF5rPn8pGK4+|?}+egtmrXOw4$zt%_IznhCmj9;Ol!k>+CDGr&*Lu3ywlp|R| zdBqV-MAXCejPS^@ye74^1Q9*!zOGbYp|xgq90OW@l&DhX?}AalZ`WT{WK76;e-1%m z+x))IG0yM4OkE5TbKz{Hg=%Oio`49A5_v-%qjiYhe2=~=2EuX#;__biXS2M4_G%%y zt1P1=KvGy6$;D$I)V@T=-)w-`&s|wlBGpVi3deTFbU6f#^^IS(K-&IvN!;;)^CZcf z7UU)BwFEPsE`;BNS5HR6OsZRdPWzL=N?VyQ_g!?fCy0U?b%2GQjoaLu4t zsx=|(#O?Bv{eIS{1;B!-tjuyYW&T5DHuE_f zLAT9QEva_PiCP?4qj3bQ7r4T9sw1VNZY|lA`saq}A^9x;@O0HMe-q*K=MQXS^L%3J4#u2|7;y@Hu7xjv))LC5AZlHp%`1L z;zMKT?jMPuyTsYF%~h4m$$4!VkRj83Y!buDt6M0SpF_@c6$!WV%k~H(olo{d7j}k8 z#1Rl)If-rfYuk`oiAEG;eyisRzSSRtKxFBG6D(mLQ-Vgd7D|2$QsH$VY)vySUe1Q* zykC!rlclh}VV!twF!i%f_L~bi9n41FUgc|&UZQ1`PpZ>mL~|FPAAZF@4DE(SLaO`; z)iWl{atop-n18DQS$jlnjy`8QLt{mINVcqlrFw~?Z8w@qV5OXX&9&>bkoDrJY@TE0 z(Ud*YNoj%3 z0ov}f8yBKSigCHab5!e6GysAnn?Rv|uLwdD$4Mo3^lx|Vc})xFXcFc{(4fotOUWUB zc6EB-gL#RJIlMhkKzeZ-!(SKNS*xCpQgn`+$h|hu&=ttDokfS4wgQ&>)E{f5$*=CW zvw;jA_LFN<{VlpBW2mttr}gRNZ>e;EVDQNfwO!51*bu4fi>(Mc%M7Z7K&_dZPLS6! zA}BT1(&{c(f`8uNw|^J^X0eV%4S2cq*s zBMFHC9T9xu>}l4T7hxO=_(Bn8GX@1#zM7JkFnjTg94+}B6%C$jh^qr&jw#@K;R zNp$tkCbSRy_3MUAedVQIC1G*A;m4b(r^YTA%{(RZz$o@4E zgRTC=3x=HedLh4`((P5$U|GzyQ$yF&1Tm=5>qt-vo<-tqL?)+EKey|J$ph(%_wOIdeU9r=oe0I1qdWP8+?vqGqG1fz>oSw7X zMxTiG|4EFkQks3i2V?L<)1MIz9QC`JW0ht&$@yQb(!#Qao%2l$}@N zi+GVIS9Ij^E`^ag{Mj=GjtE^1AiOX_f_SXTqF=IchBe_Z@qMUeH6eSFtSp3d`n@}Y zK?}~XXavF$Azs}r>zC+XtQbs%4%P460@o`;Tg zch|i04>Y5FQzE}D(CESnv48(^u?uI0>^N!H@{>K5NCYmhh53Ux!=Y9cQTXWl!Ddpk zl&}TPq1XyG=FEDRoP}h?41RQ8#D#_O+pWW-rzd(UD1khtG{qaR0%ObZAHit$J*&CM zUA`>$E*h&7SIAIoZN+R-ZscvuX$9pp((r{jr1s|x48SB*9qy!u`kgr^vFR%+cPX=l zpj}^_v14jOnsPd4 z@E04p<_Y@XA+ZwO7?jZT&ubPnqUUHZivbZO`c!P7WlGv3Y3OAON5h2h8P(v2BlF7Q zIESIGULuNIP(K6Y1a4ry=x0=yB8yG08s4sUs1ttpm!YgIQ6l=WP0MhG*MUhgkeS@v zaRNHd5~-&ASXJhoC+@soFu;m84++eg?LuD6EB_JT!rckXKT!OGC73E3(-JKRi!moD z+pipuwz>Tt*Ax*{wz1Jr#7S^h zu%2RDchFIJXcbQ+jeTP$A>VOkftf&Y0!`Be{*R2S+{!YG&}>HgFHRxpCXFcPR-{K^!(evIxKsN{aNpKuaX`$y(>> z;kjSoKrAJ3sG-G$4YVKMo2b!lL-fp&cx~Vn^a0XII(-*y5VWvmc59ShgkA_HdGpyV z#9Q9l;?IC^@Ao+JRqQ|4MJ_iw*z*;-g~(~Y4GcWtw4lXGDIo)I{H8&!VbgRxW$@2O zVK1uX)aMoc)cvb-z&6UcZ^qnhAS41y@)?SX66{WV5jDeLzhxeaF{obtt6#F?cvYPK z1BTk9gy!>!n6-FURB(G}d4aq)`tJA1g{fazJ&~#PCkltx-AT zHVXE;OqH&_`{xCQ2tml* zXV}f3!C#&+4^FzhSn?2!_A$nf91>x?MZv-`G0RugvcW);&RETzn_4Aw#*-_yn=SjZ z1@v9BAajyEqN~l&jcU2NRqwy9 z$)4;BB51%z=&+LRRmI+5hR^m?BoD$k%t%9NzRMk@1*>G5L zZ)T2f%%IMs=yqQCb(WWE+)UMPKM?g5_!*kByfewykfvT{HGB3D^;B0E5<&<7;8p#q z^i$udyV_S{C4Fgk(wNPv@1L7NH~51NI^7(f4V`;kLi9p(1KMYba+35CY#YaBNPA%m zKlUhsS=ZW=Nx9l*XD`hl47Yok#ExD+Hg#nziQz~9#SZ#9*7 z!JJ4{^Z)Bhe*>wrUnbisro2?dlp%5Rd~JC6%>OERYRS~3F}cfIve`{ZpNGBt3*f9K zh_$|%4cUTMsM2dNCTA_saVA>_xP?AVwE)p;_qpU_g4{Z<1y}Xyvu!W|j3Li(7eS*3 z-%ypY0DWp@v4A?xxF-sd)o62JK2MM4@SugSoIwY~fFZBsrR$IF+oOlOh-nZI_1bud zhZcJPmEWh$9B06ak*E9BHsDXT;Q^3?#=i4YI9$WOmR?>Zm|ud19I&7i8s1`f?`wuxz5jb*mFe*LD$ zHmkBB154#=y8M&IEo^BxDq~z6JbF}Ku2~yOmC|EeOi-dM@&VCem*R`HvNbWJ;-=() z0^d1PhDoBg*2PR0^|7cacBV1Bz*wxFoH`nE_8Ts1SU-9Hz4HQW0h?V2Ge**{?4(eOycc1FKe?4!!+-@Zt2xP0ad?7Nr{=x7_iIk+nlPt- z@mVikDhZx7I%oo{u%FubpLPn6)`c^tw$uJG2+qVzDfn8 z@Srv{g2>-22*d#8m{1J%;zooYOK*H874XC_SbjncC-n%%hH0g22xJUs%lB@Iqj2$) zUiMzQH#u&#VS64j%jKm=GYn81+w2Og!NXd9=FBsCaY=YI6~;N{v~VGzY+%}UbV@pX znT&y)sWObcj(WZZPS-t;!1cgDu=x|05~HIybk)lI)GHX?by{Z9Q1CN8M>Z0%IuQeR5(Jd3s7*O8Nf4D_$~$$!%04@Q|)( z)IVYGEb0Wl6|`+@cm)ilM<26$# z(|E7<0XFh?zpc{6VIl!hh9f4*kKwGP6)T?dr(d1MgoRLQ80v@e| z+Ye^YRWAo{Y`l+#iNGpn&DZ9no~ z7LlYDQ~-`aUChxmCL69!MG~esBg=m8DTofIN8F!APoobZc0#1Gn86Rw5eeP01TAg_ zMfS2wZ81MOkr+H8fRnXHuV9C}rjP;f=K?RkT!^h2DSMi0@=#6!ljgH zUCMwSNpRK%KZB6FL#T@Jp4Cf2^v}5+wKn)1jxeO;Hu}SxcnOR_E;bA?lCgUcBVosX zMuy%kBt$usm(ZgGhQTWdXnz8|GqTao#_Q?+a-l9br~&yjxQcm^aTh8R%I4Eyc|DNt z0hWr!_`!HSr3sgUD}Eun5QkOdpN%v*fYdW!Q>r}U4^R06MGS+&cZ%*1_C44FZ47V< z7E36w3lz|($Mo}?y}HBvu5<(ZYLk#zN&odx#kS&Cvphw7qoi-y&$`$<0B3EZGGoZD z{b*epVfQ351St3D%qdE~YOmlFE6?_EoAB#Rp;jyabCL(*$DdMi9N zZj9vR>tz>%j|CN^WS7B5$GVcnm*_uH=|C3(tcSGM0WSX!HfH7bP_iRDYzvSyW4@m7 z8HIYPNG8*xEbpid#l&;X&UnFHpCgtA*Rz|q`$b_m?(Alzu;1DbRsJTmbrE#^zUexc zN3y4v@}7>1f04Bep3voQjG*elMO!dbx*(?#9UTD)i>d9b#59@9hK6?IhSTlA=vY_Q zU)X4&hcIq|hRRd>IDX{a7S>c}UN+KI$V>`!dPjQ69;IO}_H*-ePX09Z-62S?rslXn zpgsrh76HZ0eM{O?Zz4jm!_}vTqT9|`rSF;CYkc~}mg2t7EWSpwGCHAUubx(t&+P;- z&2$xOHa5hQ5?-_URT;UK>y5p#TadX#Jh|O z)rLLJSe#*GKwb>)Lebu;je0r?eA+;&fawcUOfOLqTWH7!2Y2?)q0H5o=^AMq25%sa zj`+eUoGRba_y)MCIEjjg9G0UcRH$zRXSv^E-w|jdFm{O;$t@tUz_7@yuJRwOff=SG z&aGBf_SPFAmR2uWi=lXdlVml2pbl}Q#cou>y3JLJU#h>eC{3+Yh{vxca$n}FhE~Mw z5U}`iDG>NCbD>jPd;<0CC0pt7Y2p=98)z-iB?_Z4pUPRh11lZK}AM2UA}~RCQ5;#-VZhg30xl zQd%5Wp~|iKwi`)9O8`I(GIC720TD$p=DuLH!3(8^Q?$`4@;3`kD(q~CZV@tnY9ZwJ zE;{le@m{GgjK=FsCFy)=hpkZ{sdY5fzF%IKW;zfB?hnW&6B=g<3~a(yF7e1$3h_XpmG=l*yw z$j)CnI^aL|;Qy}a`~O^X`Ts5%{eLd$creKS_n?60U5(k1xcBYK>j*J%dr5iZ~1)JZEr*8pK!9&8u1R0(KFzhfroN!iD@1md3l2 zIf-2qfaTw_yLyrZy0dM(yflfD@hRn8Mt%CuBXu{uD8aH&NO8senvB#IR)h@ z4E6wZ4~=A{orKN1h_H8#eY!iGrDrtwp!5Qc4O#l#TTz@j{~&cag&rjG9p(oYawLNRa<&>NFO5yJ*3zF&F(}HX?RfRkYdf`|y zBA+8(R~hh+Vto%!sUFI5WM?fifDn{LXiMQ#d<1Q_5iNjnsAwJ}A7mU59q|GDW5pc^ zpasr&{rJ5YqVDJE?i8M%Z2n9&7vAzM@__%qf@^7qYw+#a-Pc|f#r{n>5vpO9x|A~F zrq(Mfs^Qtnnba|~;ORT&5g2-=+9M8+Vho@C!`ASNKulm?dJvZxTI#2{?nJXAV=gT1-Afkb1AsO6>~|v}>cE zoVPd7TlYzJ5Ol8u78v|u&fNuCW(@K?S1W0)2>RjE< zKKnZ_$>;^G)Q}JmxXKXuqW_{0P~E<1c~jp?jadWzqN2Az-qi6WcDrhWf7zEtsz|AzZ zlXD@Kl*KP6G!VDqwqdC-uanN~+R7^W9)PA&NAR=^R5r8Lg`Lz<#*?J(Ff6!i)mb;s zTmTUIgPNFbllp{aS5mS%%$PSBB)#$&1{x6+$ix&HT>gm*oI=>;^3|9%4dF>t3&DUM zsM7?hXV*XcX`8x@TB4wra2E??-Z{2DIoJ5cAM`({j?Z8pTn9t@Ptt`|1lb~U-<1T5 z6Yw9hCF5KX$^Hg@iIFSrN=}Ojqt?)3VB9i9t3P4$>%k4ykiGD1aUSTZ&~WWndGtVY zLp7YLINsR#tG}CV-c}7durZNdv0K#P$<{oTRmSuaa6+l-1{5Lv{neE)qWtRTiXWHw zHthPHZu|WS@DTYCfwuNxjIlP}+EdXH-$vShnf>3%F1OOro+=n$O1_=kkzED@5jFF_ zuM=V97A9p|Q^_haIKPgHwfH2K-uxJda%21h%M%HzzBB6gKfGN zY9F040@wpcXd<^nr_)MJ#*sLFd7zo&C&G~sn{m2|raoIQ$*f5RoC2ncSA2+iFeY&D z8NdO)xw6eiv4E2&wG`e;`X97`L^ec8!*3Ms1{EsDLKAUWLV8m+m{1jf^5fbvnYXck zVk(y`qNsoh4yAHI0l_K=2+&ecT z1;V?=H7QJW5EQtE&+^3QZ>a^833&-FB{j$LnDLc9p3;Y<0EgRnks?UI&>KZ=;5Et< zsQ9YJ9gU5I9V|Nmp47nx9PPLqIM^P$5@&1J6fGGFHn5TblR6Q>W(%R=*TMc$aTAOw zSQj8Hl%cq&W_6--_TEWTAZ`w$Ap~3O0vy?)R06IT$S{CFDjX423Xhyx{yi`-EGI{wmY~mQ#bEI zDBP8J_ebL<`u7&x}0pe@#*`aoa95cMlyk7g;r=;56( zyNU&`acr~vs_g<#X;d#k@ zepI?ijYZ|)yIs6lI9^QAI+|BaXwbL+8@98A{ocI|4Fr^tT~gHa?T;yWwS=1EuSh+} z0t}A!%X58HT@90LD{{qD(#&>*HWZ)1G{zzwj6^x(e^XlY zc{J&hpl-{WO*EQ@M!;hOC{KILPvdfmM|kg8=1>AU-K44MwsobeUa2T)YLVCOCb*>^ zr+=W69yrTxob_HDTp*Pl1>{tHJ$g7zObD*3d(@T0 zTWh}AgFbd`+7LwlQsSK5p;q*Nu%ljUiG>(%vUEfp?|SfQHpc!K@v$1p#VhUDWF7(`=!T@Nyt9^?{O{g)x&zx?uWdl~iU};=5L@Y04XY3+o5qkAf(l(O{NIRi-T0PD;_YW5~ zU%15wGlMpF^&uXb` zg0e^3wu``7^6?f@n@o{L4y{vVXwyw$VliF>gJX}LSBNWPmacvi!-igb45H~R&CZH- z_9%}QU>s_ew6*XIY=fw~)!43aVuy9)L$axcW*1+v#U@LFufH&6dyZI5#jz4kD&+;Q zImhQ#M@go6H<}Zv7cAzLY^5np8o%AE^|aqpuFTa!0R8ZgsytD?`etg(tBweaVNg(p zd~Garor-@TuHO7P+4cy88zRR$j1X)IQYWfW>1p5xcz8=E1+u%H`P>$P71XAD+KAnf z@YAN*wq}!f&&MVkj!!ugpoGbHV>2`>#64%c#Ksp#Sd8qOdHJ9p!w7nrd8#6H)l$KG zsS4E1{P$uX^Xiy@^lk%J-vb;ZC>nlTeK{K55+})qL|RnDs6*;M$g;ywKzM&bOMm19 zUcY`B{nxpuJ_01$?WX*K3L=`n8MKOsXEg&@}Gxj@$WRvp-6S=C$ zzFBds!jGS&swY2CXX#GeFzZ((`V-EBg*N52?=qYx?`@t>dHM_J@N>eWb{VjutRAJhA}aa?|_Xy5n~TxqhpP_7EXD8Uxoaf)X0so%$0 zRHI1J20kS+f^w>g8d_)iY)m@TPifu!A*AV(sT|>FQo`eHy9Bt`wB}d*F@DBPgBN(< z5n-E!KfQpW3RPZTv;XG?^hVUjoxpCO$WlD(FJ)6|_IioBQudyFmSyH9Y5B^MOTEUK z?P(6l^b6Y|F^uGG2?ftsG@n$;=ymuQc6{?RYCVQx!vMp)kET2*#xit``Bt6efI2D6 zTo>i*`&-biJ2h#2F<@8q=WYSFP^>%(EYtLFHX~o!$-M$gv5hBb;T7RNg66_#y`CR`P zNc)%a_bu~J_f>&yn+NN(J3vjkIO6VZ38?m~zhqEF|N0m1GZLm{7N&fM4v@@E-Rjt? zFcorzRrtOchkvmT(hbU(3^LkBF1Yl%f>V>Rub84 zR<}(8y%Wf4wIRJ}niu;xDkW@2^1T$U1#=aDb%_0<(C2Epi2>2-<|w`)vI4H9YB`TR z5B3XuuBJ6FQv~1riZf3MWiEH@-U%PS*7p(@k)f3SnxLetGaOgvQD;Pc6?ab@4|9ZleP;`Oo`oUGtrR5S2OKj?42Cd4g@mD3T-AT@run8>>jX2 z5*OXpg-A`M9T%aJwdT9GQAwAK{>#kG-~FCFUu`#@z#bLaIf{TNwdZwFa0bESNNx=X zzc389(J0%v4bP=Hli+}e&fA^eh&3f!ML+2Y-dt1%C3 zul_&VMIQ_^vsy{cqtuEpWALYNjSAD3;ncEvxZ`o&{Sb`Q5#E82t0ZE%@qdOU+@DC_CxA5Fb2^B3wzD^gOsu@HU9Q0nC<8 z&qz4C_&d}syt2dP5&|#OTJDWZ$RM&g#}6|6o%Z5ClKeIeHSw>DDJXT6gt#q)sdGgY z_BeoV$=>Q_vvz;-e#eG6h^P++I`o`W?v~J@%t$?ji*dijSFkih>uhNn2$2D4M4d;& zOkEp;#TWaW!de7NZfJts=tDkFVOPP~0{c22RfXwF_E5lu)=D+A8W+(t8@W9Ik0{OB zP7R{>p#=W)5sX}n@loH0=442_$gS=&mWDwc3C)Oj{sDSfxn&yLrfb+t_h&NV@WSXf zpEHtF5(~de6%H((dK|5(NUKm;LWmH;>-yc-YD0g0FG^H_OCEQXv>yIB^IglFz|-hq zI*3J5ED9nu?$#2RRyT_TVK}p`hHJb)|4B3X1dhfe8Cr%t?YXbqC6et69^OcWJaR8m zF?1My57t6C<It)gaS$S zT+0qeQWA1AI7>|BrW$I-B&icP4kPEzjO*b);8w$NYr7X(ST~_`b7N0PG>8!E-#P_(T#XeE`!!LqOV3x}#y!d4` zrIx94DY(2!nf3eNJw%25y^?K2Qis>{*ctDk!qLo>OUT~u_9DC630Pn`hU*?=kKie! zH5}XV&nu_`v9=^6eQMA?FKfoE>w2)t$RG0=hg*sAi7HNdn-rnZt9E{XCv&UW<>^*{rKNV`6$3|(?GD>XZQTPZ z5}TN-ZgujX3DF>!2qa+3^c<~*!HUck$!fXDve!@=zZ`8XL}!mg?HIp;)UMP5x`x$c zP9}Ii#cDwdE)|e&ex$VDWv~YNN@@Lzp|^Pk@1}^VfdZ*wD~ilI8&WexiS5z0mf0og zQW^D-dLRsz2MzjHgjn_(5$5n07{r>frF^!YFlB*8mEw_)eV) zy)FuNLcFRFQ2%JRldevKU^<}06bTkd(&8B>GWE3X-(uKr&k>-K^Lfu0v3HAD<137H z5vrCSM};f~+z2-EVevAb<3Ji`C*nuwZ`O!=u&2!s2k+@La}jccYgkgDn&#e$&WT9% z^!Z>VY~RfNFcN6jQVJDm$IHblRCRNM`T|$p)%QFP1xrkTws~bbMUVB)Z2(m<`7bII zP8+vdQS3Vv`jJyjkv5R7yy+KBc~Q%+UKoX6hb|r#clf@Vi41)zHX=1Jj-oXxxVgk2 z(zRuV>6qhS^^pF>}%1?%nWxnv=QTc}5E!`kv+Go#{$ton|iokkSetM>hWnDppp9 zNt0Bd(g(_zSXhrATc=|Lo`WFAI=$LdD-JwJi~YyPAjwK@Sytx4<6j3I#<67RK&>oM@c@(5nRcu9GJoTA6md})Z)i2g1CBoE38dJ zHk9j!a|WImK%R>YB|}!&Kd*$kSTOKkrm!Cm;SGY>PW3G6mrLqCJ@2k5@0+Fb?A8M; z?PbaP?uc1GQo3^~^_12WWfOI3b(`sN(o)h`4;#D?I(&>yw4mh{O93y*5Df?<{6nE= zjy%Ofnc=w>8!s)6&n)Qm=?!SS22o1EL zP}DP<3J4uZMkNr*3IA8!g1#NqBxV|)KM3T~M6~_RRrW*Y#cqyMI0S?#G5X+ZUaeZm zvO&qS;Ze5aKx6_MVbVp8x}UQ%+(NtLx^Z`gVpexw93!!5o=cZz)HG7f5~&DA0kIuW z8r~F|&O)*k?&=VJ`10jYK%taLfdq)3-w3xoDNjWJwjcXKPUay5W)=5-%G(cLL@DrJ z3B#+`Jq2V}Ridn7P2f{7WEo5Ba8yFM%@)qDQi2&FqpRUl?J98&&C0K&3*;sPK>Cd0Z6ZF{92x}b)6 zp}Qms@!zR$KiB+J70B~##brYtuh&kgA-Zx@!a72R*h0wruHdT7^JiMG>UQ18{@QGw zvVc)9c$ULoqLl9+HS%WfYlBxNU>HU?B`$GDb@`z8qjgIEvfJG;{tAf(37UvjMs19W zcq3$KV^|gc@P*$~lv@>hc?*zVr72-;JvLla8By%Hr1aPBQDo!*Du5o8`nINtb7Hz&Za2u`UlyNPEZu#vKZnM= z;xcbIPY`;iLuazQ)4p1h)ct2s97aqsqVN^eOf=~!ml5Vel$X&T=~bSwTo$4i9sIpU ze>Y4RIDPPU7lf5px7BpeM~4zUqEx+bgGXTT%Y z`S?4L$FL8;!gfg#$+hzZE(uR;X~t`$I9c0??;shq;p)!2PFS?1vKs#so`awOY9|_@!-s&-dVJ3u zcufH(KNggr5kIRf=?pKNS9V?eIK`HdAnsY@r z3tmfX5ub*540JvcYhUfTgE^J;zbGuhLT}*BCPCP(Ez`Z3?lp%V23tj;m+>l%qCTtj?ZU^yW?URDU_-O5u!{oucte0LXKr8*ns``A&$x)n)KRS@<{PXa5)A<& zamcJD^3P_~k_vp={0ghhbAbLa9baZmKnc=EUf-H%i_AX6mY_Z?6}Pm`!gPSHj@by!og$I-Wq#$(+rpX3$xK+Of}Hr% zU=yU&0#JDjfD*3W(;=;X(%~`p`-)x8cE^8}6r>KhCOj8Iy}7fNm7q7WTce$CEVXt&a;5^8(fVI#0dfjvR^ z;2lfV0993mw^(U3FSYn_4R#o-P}wxHs~^he79S22Mvmzu?@E*#LHA~gPDk|8W^Js? ztwvZZ81bBda<+UkITEN>-p2~FR*#|#`*~*n{e47(c4Eb_FR!6QTBj#(9~iO(-7u*f z>V!|TTWyGnw*C$RNqp|T;b<0Wy+8r7iU~j7xjo(@*m}6(&Hpe;li!VJaf@AR+T6WP zbIjB|KWV~f3Ap>zW^H+y&4Q_uU|L+K+lBTvT3PQB@i2>@3@AP3w~gV7?u(}2*$#_2 z)$dW-!(_^ZwW=2W!Ehgei!u7biK=kuuV5BUJ3r^Q}p)sjr> zI-08E&NfYA&YiUarKOm@BK^9u9t2I=eQB^XOgMj*7^&n<5f5uiUl5>H(+a(-U*B)u zTDnxdkput(>SiNhScPDD^uBJRSeBZCAlemMVe5Qs&W;TRJzwws3q^M(Lc`VUibHhJ z*TWPOX~-|j$h^=f*G%AP@^9?{gd5lB1@8xJCP{8sn=O7H^X+Y@yvHO_M5rfYiG+Y;kzgDX;7YvM` zs3wPwK9fOiqafF&Q(6TRm}!|{=_PHdo<(drVl!+Ru_Nek7g22h-EV)Sjij`*uLG4d zBIZ>^Q=fU4pMsB-l*njE{F)0<{K6E7G}_Ne#-W{InP(NFJPG6UBf zYnsgM)F;;}?Y>yQTmOqqvS^44^M-b{z3K$X66_9O$az?j?AP$(Wn?A4s9=wr{*D-8 zTKg~8=2)kHcge3`V%mTHx0pI|j?|3%K4B_KJbA0LF=jGQORqe;4*^e5BB{ztP||cG z&Sk3H>DBoslzX~O3dBha(th$%5}hB6%*atr_Nb1id?kyNFN3LFwD8WakY_(1(y^(z zt-t|{sP!6%4miikZJ?jROFqgbd#BDOqA9qqZ81oTw+b&kVNloCTud`=v#D(bjJ+tE zI`r+tTgezOhsq zlxV*gLQ<_<&S0g9f#{{&fFc!QI`W+D>fDi&iDl&oyVb!-HecYV&`+#`Rbx4JS`vN2 z3l=SZj(*cLB6u1c+Ugq@8A_|FlS?CfcAMDZk51px7$-lcYf{uPhUd)GlFdbvHpl)% zHC3b#S($zrQitp_I((e;ido0YBaE)Pm(8dk`>?u*%gGb5IcnwU6xHrsfsht}CfJ)H zXr{_mCYj}&JS@g@yx+MJ^wYB!!|mFD$SvXUJAQY+*O&$a&=->%F_QI8$c>`x#t^|K#f&#mbxRvx?rA4T6U zmy#lX&7X0zKicQPI8msJq$Q@?`xj3b^#<3Q;@|#+qQSIUjWZ^O{}ZY_%1v5rH=G6u zN8|Xg{Q@LAaazz9uv763|EWO||5m=g7C##h6eE>tp7y* zAyzj=&x{cbqcoUI?VY6A1-pcF>J01>MItwz!Y{lQw}J8LQ4UpHaOvF_P@C22W7+Vi zajz-vVA@+ke)R6Oi3)ys)P*62es>mm%tpL%tD8dhs^@`ItRUccfm90_KmPCwR@Jcc z1QtasliZ?Q#A3oiZ3C|z07%K9v4Slf2$>%mN<0vsi8R4+nCBFFrpG|sHr#Jp2HlpE z`vpFdy#S90x5C(`d$ZXd7Ke=m7tgPh`z~WJ=4!rBj=Q}n%uTw$0aPiv-wiQ}etx}M zLv7dojJvfx`pjHphLki)WIc-$JK=16R(dEwzn+6Fq?>@Eu|Huk&7~F<(8I^BBwC8G z2V83YIf1xequW%xIcP?U7%pDx~txh9U19VUArHTQcd4wXvC7;Z4>4?{$e zN~JC_7Q(sPpWtM}Y=!j#>p&c-#CU*@ilg~^-oA1?b@n^|dK2%Wpm2LJx-=jK1lp?f zs+kbtcR)`S$s`K4X>1&O7rZDQAo}14u$U8Zh&;hBa>#A@7Z$b{0@44W8>qqG$vGt} zYk8hd1GpUi-u|)}v9)tJwpYz|I?=C+ujpbJf7h?r7zc?_|8`-0%i8q^UazPlvY8iN zvDiP`+wlm!2e*@RM)EyEVw*>R@aEdBD_O#$I#Y@4@B(-A&@22be>k@f@e(AnGMZs3 z{ukI$;)1ril9K%?-RClX7t5QcEoH5WMEV9YO5IvoIEi_dT44^Y+(1l4E15WbsEnD1 zyT>j;56!^ZFm}+sx&Dw+9a6MEJILAqaCCN((veKcXh?G%xVBa#EeYq$vDspy0Lx8K z#O^#{_M@pTctnj4QIJS~{&7KvYaqGM`XQ<8C-FGrc`VH+=R3N$CIAB$-C-!)t_YUH z+#GsE`sPRtB%QTCgdS)E6z7iFp~38tKi3h_>=jF96!E_2voz7i>Acx!-9MT_*|zEr zLE%ORNsET}c?Yp&luJ{zAOqtq`I>tYi`0wXc+Krj>T-2MisGvK9S2=5-(i?EbPZk= zovl^2bSkmNIGAcHjj??9-uCBowvutY*P(8*{V7NQ*2p3cc^WO{S}H|=7|@#3Y}UZR zRc47jiQR^rEq%}p`3?Sr>Si~{9Z_3#fv59w`Uh_ne{hydVg+>BHwv#*L%BdAh|Wp} zN74y@D5M%oBFdwZ32^#7CFrhvl- zH%9al5&$Oaf%K4T4npb%Wh%AyLmJIMPX&MZ7kCQ{y*AB&QoQg?39an39D!iUQTG3b zU^7cDVDE~ z_F{}Hm%?;G)@>rGc~=L%=0mwMx*O*~z7KFJkc$hzTFx|N)ns8 zfv=1}l_4UA@=r7<`GfaZjfJyjdd1CnHNl9XA`Ag-{W^tC8IH1MzriCuqE%$uYL59Y z>){wgm2zy&r`S)U#xP#zY2o(~6v;9BaYIfdr9!E$RV(c`RJ*Mrjq>*J273MhejAAw za>amdsB>z#MIlC8uDk&Is4AF9i^537CFQ{I6CiZf<^N*oOTeQlulLWl++~(b?j)0B z0wK9c1`-xwLIMPhNE5EXS0s;H=gfZ{@lib{*hB~d|* z64Y8-tsSwVwk{Dwt)l+l>;KoMkD6reoqNCUd}n#jd(Nqc*T$Sr1)xQo{pO@&#r{FX z>De4Ahkr#C(w9divG;kMtOw?bhDWw?H=-G+|JXRZXzllF4GuVZ+68|Mv7x^A@z45CFS|jT_H>7Fd{;7h#MUWUIe}ak;EtPvzVEb72K?(y&auSck z4p#8Ox{P1@4>x8R!GTmPxB9G}>){vz?Fh#N>&m&$e~dZI;ckmf1G2ZVG6t4hVfqWB z!bFuNzI25T0VmRP|I?uT3visatK_0QM^D4e6)u{lH)9w_hB=sy>i-9;I7_) z#uiD8{fM}WcwXsBUWj?}4*`;Ad;__JK9sNC?#D?W*4bl5w@+(fHQIskDdQ* zDuWe9R-;R&1Du{!Dms{iL+G%An@*+E6g2srUg@y5kS20K*C18RgYbM0{o2PI!v7)k zq$|1$k=wdSr@V$zo>QsBo<{zKa8!X7;o17pNEoLIYE*X)z*LcoTr;+M<-r`<5)$>u ztx(YhJh+ns03XOwTLSiOKE?B0Kr>^!1QnPk@G;`-8;rFkD9DZz-4I#AX~vCccrUw- zDjnw}%KzbiB#<~ra03j$hAOJ7jjLihR>+QAiF~L1wgTGY`_=RU_Pvj_ktb6hbPFH?q87dik-rzy-Wl3NV3fjAhq!v$6%LgLfw6CnL)4x_0L+~; z0W!#BuICseNF@{JwhK^LD4lVKV|TV(OkR-fshiqgj;9B z{nC?&cH9?#%Sb_N$4a@GTWbN6mx*OGkDqJzQj>=nL0pf}Sh0`3rqMVWd%X-ugx=-f z7EE~pBs(f8+W@y(+Lu6P96FBm(6B+B^$}^PI7|TbiM!HxN`hO&qM`?BCv-3Y(&SjE zvi`@;A(YGf#*?zE-Z^5&` zn&-dkbpaj#Q}C7 z<9X=yQ|_%g-N0M543jx1AL8@rsIR4@0n6hwt?=1NTY~^Sf!)f(%xxHA}Xs7oW zmQ_ScQ;4`>w+y~{D%RN9M(|^>2@wwPXPE)=3}G@oTZ&Ue(0P00%9YqDkJf;-#OOR^ zO}Zl0GiqIjQtMdTUqw7ZdMTk3Nq;~B{Ba?Jtlfk4;_PGyj?C}gEAW)UkF=(97NtIeK8)^No-S<_?M*Gq>fqBiXaPBA3L zbRyN%YsY@Td?~HVAREik)A51Z;qpIny!{5p@>c7cyk?{~p|V{4iFi5uPX)~}9h4|9 zS)e*`o?LVf78xzCaFR!M?*a=R`sX@qVf)Rjs~=Z3r&3aQ-<*(Sp9(Hjf26?+xVoY& zfUi9upkrJHk}lldAgZXS7lsNm9xLgkZkd7>ashUH(Wcqr-~cxBay(XikIXXHZsxr% zq_wB$iP3_U=WqBob$wQwB{}|ONHO%lP@F)@%e_#{j3B}7J}oTY;1gBp1wyfyYdrmH_KB!@Xe?rp`chuxO4O_D#8gi z2^oEBj&Ge=>i}IZ*n03X-6r{@7!xm48Vk>Tmf7}aJ21flq`wlWZv&GgdemAZp1vU+=e1gF zGFDo+q@CSl#Ld_n)pDkS0;L0FJCFSV05nuQ#*eA%FF_O)%@RvP6@vlm+l1#C(u$?r z#W6P}&M8a0Ch*p>60;y@3snZ;zLzWOe&6V|uGuQI9rRW>17#sukGshwG}Y~w)h6Xf zux>iu-{CNOE1DLNlZ0~mgCRE=^bgTb5JjhiT~h19f*>2PMo_*^=aY3ttFY;F>0!-F z%7IcrMP0zGE{&{PHM%<_(f@;N3w<&i>sG#@j5pB}-TWUW>Yc$jhbNL~6)hNa94_Nqr9FKi-6G`*i=7hS$kDxmA=$ zlDK~`-rjS+Sde)dd+Ixf@_kzX)-<$JUH`>#Fe6N~E9^%9qrCPqGcmGlL@)oCAJw0Z zr%6`2y{@yFJ~h^k9+EL>!0rBK2P@u8cV^)1duW>Wm4*&Vb{s={X|kwkBebRNFJMzv zuW#VE0$E8v4JpS#HmwPuW8o;qvB-$Toaugvp^CiMTxccwN5xZZ8j|C0?>WCb?GI>rt;l!P#1wefX7e1*&=08K=Y z!OaeiR=i$8DY&F>TK`VK4xNnPfKHm$!ZP)Enl1{bN5v~V2LfmOcLAD84~@XS)DUHM z#^2`UH3;U?U|hfm-bBFg34L9L_tO0=`AD2Oqz8OYh3Vs!Utqs9Gp`YR3lY&yGsE^6 znm*v<;17$n3N!=N!qG#b$#|)=0muFfgQthKc?8tb2K#TpL_+P2%jsl%6P$<)lliFM zKO3}(1M9ahA8-=d=DF4886SL z*l4OSTA}c&$pVWl77O$uRkkI?@+aBKT9RsWu~=( zN&qjO2mA>Cjsj!xpY9EnSz-a707C)vbFs0dnJjk$YZ|dA+Y@C?kHn?A-3uDOr@o<% ziAD4E;$Eozf$?aBa)$$p2Q$S_l!!P8VKO}ObTd&0eNzYlt;FKvg=1v5{|1$X({PPl z%F50L2K~J>_=8vUHXDtap&qIcnqbDpXV@Dkhz*?$0uJlh!5=W0K}^ND^8$k|CL}C;ornVsEPy1Q+`VPebyZ9v47?&Tw zr3eNZT85c>Qfu`1ncTnM8 zEX7F@O#tlS2x1542$Z?Df5JCh%xNVi@ytf32AO@6+oR%Wb@IWL%+5~QBOhYE4(L6y zDFnp>l`|!@n(A)4CGiuZ)lvbva3bwyZXREPoC{w1a24exFn+<-+#R$9ff)qQ+IdR+ zF|e(_W%h{PB6e;U-p(7#c!bJh2sh$p7X}SFi15wVnf-M96IRoK`LsIBS-(GLP2Qu9 z4GRTnv@AukpB&cwKxadb%m?9aDYFXp ze^$m&9@?api#wXM^+S72>~Eif19P^5`m%O%@w86wlii%Hf={6maP zr?RMaaXuzzSg&+0;}cQ!6WPUwDC;^VbXF6mmTSeB;qkvyyCL8BBt1(k8M^G-CuURffWo``@9aYIx&-!7;^1Y=TDh7w8T^AFy1l^d=34 z!b{d0kc`V^Vgw}H4)Of{8w~mrZ?~Zsk2LCTCeleCqqXVw`^B$dDBnxd!FEx)6@_ZF)Cr(Wt zW>rD@Z5mu$Th3tvW_8vzP;72tnNJ)hybMkAS?ofVvb79MHx0nI)nCI0_pq@vOSnC8 z`38^Aif3WHy3W>d-Xf?RsA%WI$l8`mb#V5N%YYYGylT=i^bci~O`8W(B>f0YQ*+7M zP5NbMRgH`Q#^%iy&@r-!^RG1R_av4uty|BhD@c3AkC>%+6khHspS+gUBwOz}Qx~6O zl8c*w->F-{-!uBe=$L^QRXVjikG9H7sLBj|@4pJ`3X(eJGmd>V9l2PueGNIhPVfyc zF&ZH0b(+4?pHK+DEJ%euh>ckrFE1mNWZZ%5X5imZ614w5{|KYARdDogj?QO!aMyH* zLytL_E@{(d>EcV|7lKpKonyovou9|zao^u`Xp5qW7y=2ZIqeO6 z8w^9cj9lMON#?TN_>fE#l5YELoqpN*15e{TlmM_(=DSiuEBHRYyvxw93OXv6EycVF z9i8T&T z8l?Pp=w$dAt#lWIeWFm^WnTkGUl14>S-k<#-7a3-zf+3uLW9&$uz7eZ;{WHfl;ev( zQDf)$hBQ>)r!UhKAQ4I)V`Ekns@+H0Idn>( zN?}7e<5)f*Fwhu8sW#8@j_Suh%yq+I(pmG~?raD%-kDgG1YR$4;qcU^n#x@0lp)U zgfRv#U%Qj=V3vI*RW#Up$#>iZ)Sg1rgo(e5F?@5m?NSMwAgG~` zp%dDOJN}KO;4Vv_L-kQ-2@4JM8l+W=7g?D#kPQ5{SsXl-2D@}I&+`YV?9{87#BYQ2 z6Kg0Tv|t2neuc(Hx#AkZa$jb-c`?Il-B!fMp*bv-`u6kO7H$>u8|1kthK zLIX{DXW)uTAEPo5f=3Q=9x}9nMyb8ez^@BRc^>gaycj8`vP=uBZ;Twmi5@SXqY1_3 z&Px)XDJ=^Qrf?Zl&G@p+F1|W<9QWhbO=WnfiTL7s@xwC->4Xkc6lZ>|gw`5!JmdtZ@cJ z55%RaDJJlqYnkyXHTHPLvsHD-U!Tc_aaG2mP_39-v7!DjlO^1I zjIahH&w-~7D2dP5NcByag*ra2M*In~bsdV_{!HG2g=TyGOw+k!oW*8%>~+5o=-)>< zt#OLG3cgZY%ZMP)cQ2$n1Ux6<60&XO2?0N5uEG04s73L5zGgh|yM^dR8nvP!B&(xN zNRAORKn^ZT|C0sEoE@ao2|fk+Sii%H;8~Tin|TPy#IpSKY#L>wU7Q_`>Qq`doXW5M zXb#QLS8AD0xgW`oJh>0G$v0EGcTF zd;{q((VQhKs7bhk9)D*CrEvHiPaQ#1AL9>sYD)U7c&)iy&Xh(dPWf5gWvl%E#u@IZo=<%p0fDOh@I%4hHGue3!=RuRGC!r+lec z_?6Nu;=rUA1e*3jLid1$xW%(<>`k|0e;ts-aG++{(j=Ul6Ht@QP%h>_37Ow8 zkhfbogK=}=!busab^4IRfEI@`iTRh6(et?$m=}QHfyUpbzo^lNcz868`U+ken*xC= zRp$Y`sbvf4yhrSVDns#Yv4j8PC7D+7I@;EJbS1n+KKb^35Qf4SqhwXZF)rHvQwB_dyEs%xj^@H&UXFE9O)I^H%^i`Dfm6LI1HT z@C3{?nuf^5@ITi~r!-yq3syT*CyyC$F*HgPD!*ssfZh>HfezQEJ*_;>S%!3%Vs#jE zqdni}`$FLtpBC(S$KaJC>^z@c-QPi(ClK9{22QRvpJ+P2G#8-?1sSjADoH1e_^?fr zFYwVJngPLX`Gn?w8aoKf0HU#m^SU{?l||GM#-Hcm%sZ^^mEL~fcw8c$#pJq9bbxKs zmaHNha_%;1%|_aP>?hH|WmT?!8UAiY;0@*7<5eaDI&C;x(B_(ibj{Fc>&Hae{M0 zd|42gLFAWCW^pisqBL^I>3d|p{&$3e#zHHCjEu9H#CK>GwMU-Mvg{){S*uW5r&cxj zAxK6Dr^2>nfWsej7;?NgPH=-U?KP4MgTX$>glCjGM<|B!NQ3^Ṽd?TpKdDcOA zT`1sZYVK}hr3a3Yql_JBPvDS&AuN)D<`cX^=b!6aZ$cVI`;TrsiL+2(z3M|aTEj9b zXl~1dljSX=gTFuT3NlRp zGSh}&lR%UasI8(ay;NIVv2*CKBP=RI0zOEQ_$8l4XmpP^>9ODE5+$)wWR^fFODd$# zQ4dG->1sNrdyj!bi7c+|;8R+LA)vYqgaoWjo+<{Fl$`F5U;1?y){mpb7YZJexWpzj zu<%qfo@yX)Iy@BKC_&l)e{Cx{*dNNLZ;jz`rAVkOK`WWiMHuzE_|lQM)i56&4FD5! zupUY%SVo-JtC3bf^!x~(g^m?h)73rpK|6yyycaika5qqQya&gjCXZ5+;YY@Ok_8o_ zf-T{MWe}^rro&j8X5je9tp{pNwTi9Jy*E&cb1v)Sc%oVW{T`0^6dWZJ>YK4i!>>oP z4Q&ps%i3B7pkD|y(r(b@U4k0ej%+`c|DOHHgV$4e)8;VMpi1?3?0fpO+96eh$FTlm zT=my1&c09^STk&s-Apej83X^6!~XFZuS4g;(xq4&C|V0r_N4W5jJpX1j$t^m96tkA zBa2N?FzpVkcfQL!Y)xXXLQ8<1!#MzwG8C5WWkh&zs_8h$alVa!T_x^6)y58PM3xE^ zJ^iW|zlGtrkyplIkHY?3z#QkdEvf^&7-I`_s1Q0Y1VT`zDvsMW2Qid|yhC}2s#aW! zmq@8k@K<9W&bCkk!R>?emcv;(Ed_M>TDCNtYQrA5IrM`$5D!3p%&qzqLX-O2R;WR` z_=>+G4FT`v1clvt7WQ4p;x_*PXS9&Sxin0Jz+0z40-OqdWiCX*q9JHCjcX9TC57#A zlrjoWo-A1cG0hjT_J-j!K+X0X0*%pf1Nsh5Wu@28%kCeli_H#F6wNCWG@?JB<$dum z8bgY(dIw|xjEi1^@C*|xzyTOCw_py&{@pi@{Ub>P)OFl?H`(h)#Jp%swvINh^`ScYubZ!?e4 z`JK;qJ18|s6bznynBQ;<{CyJ3<%P#{wM9Kv~6 z_CX%KjH(hz>UG<+RerDSR^*xw44z+o6G+F9h0X>Y5IIzjUrxBX4Ywe~2YK+-txQB5 z$^a%{2-$(Cu>)0ff4)a(=X~3f2JfmPvsXjGbss9lxr0oV{W3z#2;Vgj#Ph+fF{B2qzq7K*iXhg-D5HjgQzMLvl*LDU;;x)X6;Zr zbOfquey}Ndz)M|iTx?t{g>zk(2h&+-6Bvu-^nMJ+?dWfrnre*WlvAt2Gcj2!`hhk( zz121)GDxi`*3%6wKyz@^u?B^e|3Q+DZiAmW2`QDc);B;651c#7kxFM-ct<}_K@&Do zs49`-&;N#wVp@+inB`<*0PW2ThOJB6g`O!f;eKk%^M)Y;;znyGHjw& ze$i(m`NpCQlnGz-^YH>b-3Nj372@8T(KU!~m?5r;z24TZ+mS;* zfU_3Q*u7tAV<2O{>XFDURuyjosOW=N5*f(>GFRBFL4R04k)X1D@!nm1emjUX=#xPj z)(ewL=~UZsdSnZZwha}*=>pac-`gJ5Yi~yl1!T&xIPZl}G8}Gda3XZ!pjPpHP60Yr zx*}V$zQMq5tteb0Ly>xhj1Oa<0=;Ln`2gsP>yh;FlGguYbkQ_e7ER%JBMU>fZ&XfIO zQs)F+vDCo$bOhNF{IlxoLMB#0k7hlaT3BW6+aqgTZ6MOSkkXcjm42n0p&QqaTUJ%knrr^v zm$TjaM6cfKx|{{oY*nSRZ>3pLZPn`h3dt&1Q>f0)1^X(~raf8EqwrGf4xG<-c;59- z2EcKw(K2r~dZ0^Fu3m3m=O4oz+8W|_=;0ME56?a<^X`MZX)}WldoH;N%FhF{;KPTg z_Fge0*zo&6txf%7e1jEuWN5082N^9_&;2b3nZp5s$B3XC+!E@67P-vqXY{crCZsz+ z15kK{gVA#q&Y1&o(O4UPgjv&}ko0+-nA7}?$2=hR8|e#?=x!|8Z_Fizw^ zY)QgY!~L|ht}jph0gAXe^|i|?mBQV^Mx2g8V4a*^y9VF;TwJ;M{3aLvfOmjLWN5^{ zM$mWR9uJ8{+gW%LWNR5gqf%Ov{j`3Fee-TdBE$2Yxdf(*#u(l zN__Z)%N=I=n`u&{`orVM{Ub(D;mn&VgC(?LScZ*vZRZ2XzK52=6Gv>Po}h1|EK1c4 zpra&%zn$|a77pD#7z}7Nx?}$4u2a>6h$HA0`sO#9aHQVBttpL>2ZTf~7KW;=7ujH& zbi&dpWR*!F^>hk3&9v5?Lbj-Frm@4$^bP~S`Zu%8QeS&)dxhgZ4A8iPHMCH3qI39~ z&Z39_-#FeL{NXl4qb?kJ87&|fvqIgCMqnDT;2s7g?xZlXU6Jg))Z7Pe3Z4jlJDtNF zXCB_1es&rjbl2j~oG>kk59r}=x3dga4Zz+vd6Bsfu^iotJhQ{_60w+f)gSBI@RWR8 z8c0r(na?eE*kgo|aL!y-d!PfLU#B}8C}rUq^nh7wVQD>AozW|{h`ybRW$EB^6>@>8 z-NSEQO6v-?;a4{+@%+iaLZDN`*~o56vy_F+dF(u)HcM+Zz02aM_bCavb_}lxXvV){ zf+FJf<%c$)NBcJ{kjuhdE^7ef^*fd@a_=7R^s*t---qp+JMi!X<|MD!xa)j2SkYRl zQuG9CW*dn;OC@p%%=HfoF2O?_-tgaA?He#9{86G-fm^(v>eJS@!IgF#p5>< ziKOiGsHL>t5aAVjDW9$4sFxI)7!(6{?(u$Vriz@dwc0U>U>n(mxqJ=s53oEqX#Gdg zrR%l)N)h|eY36X%;C{&9q;u#HLj&1Hhk{SI6|Y{GN0tjZZX)**6jZY#FM$sJE$u61 zy_>5YxKGdb0Pf-?I>Rig$TM$d%a!HdmE%@lCmv9K6t4+W!3T!8`iDhPUdQ%%K z3h5Q`JO=&&)C8Xty+`;V&RJmDG1oMoul{M!i|AvL?LjV+idH5mL&;fNKv~_w;7HSC zE7?YSxixGbx7e8&^7tYdqx6@M4O{R6K6?1qzh(`dFv<7*RYSB)H7o&X*5`b~Q9fnYQmv+O&h@YDAm3=TcDHmi9`r@O2(XKB;yX)jXGKy9rU~E7aJ}u?>Z%jTrY|1b#D><;%cF z6Iof_*!>r9RvnIe=E6{)!?PD0%3ybXWYzxKQW|s7340`)6hPU)TxtyR86$O>m-8g6 z)!ScR!mXY$G)~+4=3t7Sec=JYmU@0IvCaz_+aqUKvR|bbLeZbKbH$X&F1})hmg9f= zKCaPx14vcT*w%h@IyjwuAA2((v^9hUMW(Z8>cz3Iw_fE_Xc9~yEgVCEnlf{DaliYJPp$3}4tR5LVw| zULr4M<^bs-dv{y40Jiz~>l_^Y&HsKA>kc2~ON#iLH1kX?6qfoxx=Gk?P=-0uQ8dg@|o69%q8@smq8 z{_9)Fk~d9>v%^g0g>Pln)LWUzr{~YWNyQC;@QPe5=Q=c&`#TdAJib8`xf#I^uxRD! zlsjDTvNeY_J$Dn+%G9MSuUKB97Y!)(=O>di#89^cEWq^>hEVCb6u40vv|^aZL#{4f z0dRwcd`7krerFYOd3L|%yOrc8@>C_ z(ku#8*k(SeXV$P{elL|(T&=M(-e73@Y8oumYF6qdcp00=*#JlIuzMM>_O&vBS0$RI z4Tb|x539ImB;mW^2iy&lU2OcT7^bCo#7ZNI1>ND%0;Ne4O0=8pe)o!dNi9uBQY+Hip1Rn zS2s{d*p6Z~hvjEZW@Y9mGJ;Q1?sCd4Gxsu9$nRjGZC(y^2x95+R@}Xw$}kZ&CtOlR zx%Y6r#w^bku;O3=l1aByc%+hVbEL<_X>6d4aHs-FjUPGu%DgD*hLWdF7pSOz zp&aD|F{!20zFqg-C1-#ZtPHTOEdC19@^i+>GSCdFMs!4WQU!O4c@6p1B4e<0{z{G9 z!?g-RvyFV8=fb{2#&h}*P74V6#1Kq{#?Gg~iOiQ;P3d!MM)LWFN+N=v%5#7s`H`TD z{f=~+jy2(~(|z+V4=33?uS1P69==aU$)!=3hz&U{vnRML2ni?HGH+P#f)A;^Nq4Rn#1~Z8uu%On@WU z!a}=5=mjAv$WE!-ytUX~f`V-^5H}6UGRp`4j%h|?R|zy4Kxad==_soMsb?TvC~{s6 zFz}T2$i?+8nYLSE4mRFETK7RS3fd9G&HoD@RPwYx%t>p;OBWt4kskt3)!P38;C zs@{(ITgWIeShVUA=VviM9`WU~DJ*fWP{gW(F>4Ljv|r|w^)=@b3=Y=l`5_wh_A zSIOx*6=(41C-f}o7}?i12#)_iO{Z|I}KjoY5XAm|&~CZYkGDXi;n00F%D{mWy{6H$9XHBi7Zj)l5N$H=r1;>>3e4N+2<1jgsoaKB zI?DUAj4z)Bj>!low-}P+r<5=ATfQD681j(Z0@Pq2Nz(!z=t&HCsc|w|suem2?oE(E zDVaNSJDj0m(2ID-7PyvIvI$9+{*3^YA4h?PJ&2`q6Q2ne4*^D5(mZbBX}8jHvybfQ ziMojl;*nF3K`D#9ogEhlVi4Mq(%xv(9ZscxO20#kZD@~n=&>BAe9Z!0_>?-okLq#9 zzRTLrCEN9{2B`z+VVT`PU|ZS`ZbWjOxSd3oixB-dFUkH29$|};5ej{ylK6rS9V_c_ zXDi85JgmM9TpiwS@^3Fl_|3C<#Uwiqv;Y)E4|m~tvXQxzQSx7->xKbH1HhyqDM@gO zjEuL}JMI8D9No?flC2_t1x9BWOvSJuYcw`)WjN(Y_Gq=762;V*1sRA1R?NhvuvHEc zf&ah|TOFO7H_{>ToY#xR39K4nCzwjj!G3X znM(qC?=w%r9kep2!fxTb{kxt%Z-?)^-KA^06>z*}2O+}_>O=4Lem(-brL1&%nV3Km z{{tNar+=hwZ0uqNqq8%kA4mdez+gVFfp3?h17!~7IjSUi0i1_D(oeVX@=MIKSV2RC z7TiLe&C33aY7qLnG)Ym{TOWu?41hfB*+cDu0fqb%%S-Q&qsdU0fQG2GyYtErKBCAz+j&0}>2nH3M?VB`eRJO_+ICMRHtdfUR~(s#x4&$r{Jv(dqFFpFKZ+5y zIcX`PhqJ7CGYwA8ZHI?6MV2F0;dZAAiJ7Gju2XyvRWK7%f|E$(V5SZ7Y3R`taA>2Q z;W%RhDX;nxX)Ah9kMLq1=ZD{G1{7+Dvh)rz0w92`p<;6oLo)?LCLKvq)DbiXBE{4U zs|A5tNSI|Yo*N2ZZ=R3lrxK6fY$sl`Z!x0S2_)c}?Nmn=Z30WoF+ZL&3$DuU1+3z5 zYbQI(X$sz8@5Ark3XL`QA4l4?X9Q?M`rrxmVdFH7mN-8c_Gu>tY0RKlw#s;#Dt|uz z>I|~VJx^?=Q*)RDGDCfnhST$@1OMqGibuKg7>gurG@t{O1f~fJ!WBw%fO`4-{hLtc zb28PpX;W8#1cQY)FJw--bh&ekh1;(OJ0I#r3qc&dVb?($RQ@u(D?^TPW~*!F$;R6~ z+s`Xv=Bbim%vu|gbxE>Ol{~2$m-PEAa$K7vAX?h{+~4o>5r&#Wz~kLUBXeAU@HB(y zICdJECbB@wYX(I6E31fCylT)9(ParI33dI;54-!{j1HU7*tGOJ9WU`T&U>@~_;lQ5Rqwo$1ZToGwTLQSgz zzxA$#v{@YOeKKoP?(3IS(I^h2R0AkkzlaYh!s%}MqWN7} z2K)=PmK)^ONKQYFBJH-40F^r&Wg~$bOrwxt^g>G9_0LZ>mQJG4$$E2|=avZ6|4A(_ zKJ#x6)ETFBVFW2cUV^-Y(FScZ0bw`AuQO(5fdf0bLUzQXQ>;@qzBozMNQvNB$!`6q z&-QUj9l+yke!}4y+WX+JFsPP?vC%L&QCM1W2kRJYw{V932(!1QyMeO1qWA7NU0-*^|ERYxI}PF-wN%ceem z8FdEbd41Wgbo8Czxu<1bn6I7>R=lulfnIJyVUJRLWsa}hBcyz+vxl43r4){hDy`4- z`A>Ut&h71ee)#zv|J1e?j@%uEpNvjd|0N8SE~1NqDF)x@vcJ#WaKaanE9L0Z%Mjh= zWRBP9mB zpab>@{tU*4VGY+E#Not`Hd#(SeLo1S?l$2<*9VW2qsZd0xp2}f)R7W|(ZHY3e~zFc z;_f+AZrY91Z4*xt*&9y4kw84lC{@Bc2Bn>P4kl?A%65*E=P;GN&_@n5L!9PMqbh;! zyA>P|^ph6<1ppggdpV_xNM1wj)%=q#2~`a$P8HHCz4&JS@tbI5L%Kl&fXXg3qjna5 zr8ax@J7J$)#`;*tIj7uCn&;{<^%9jQbw7(A1zrZbaP%EfYyfEc@hd}lshc>GKEQ^_3k@%TNhc4?KO+zTXw}zubK#qCUG%mJ`|)USFxW$EVmcyN(%a`(44E3oUjC!>z9g{2}cmgb1( z86YU4AM<7rVN4-C#J}-F!`N`!712i*X_Z+BZ0Sw?x$x3@>4A6vEe;g&n9w?F4f-U` zetHwCTxs}+5Z4LIzCxsNsz}I$DWQ-L%_YEDR_#?}2fxq=E;Bkh-Ntt)_NmG^+#&mY zQB)d2^qKQFRyFs z{Xx<0dWYI4dli5~%=-7a-BCK07UI^VskMBh2(G4uO4D6xe}OkoIWr^LdA@rs`uy)e z750ak)pdiw&S1o184d-%*c{%6Q0{UuZZc^%@?fqg?|nmi(DoIt@UqUYPT@~9|FA$G;zN3qXV!m(dUcFc@vUFyaCt5U=xs(;qM=)x<^ zXvXT+O_=8V{+ET}6JuOdrBS%Anx#K~4|=)$rQBU`f5*P8jsGnWi0Icdb`~p47Gt7$ z(zKHivtJ^DO-_PK_23RaCZMCXC&@HwQ#6Uni)iGn0J3v>4Sl&}=Q4W1uzVYg;~FwmzO67FwD<03O7ZB_=~llh zaVCAnbWu?8h)zX1j6W8(dEV>v@Wc}z%-o!)@1C)IwdXAr^KF!uL?I6B&4a3qBB__D z3(?CFNL1m-3?lc@^JY~ePYZ38F|@V*F@b!At~=Fc*xBqq)Bxc>`k!@Kf}4^&EX!iR zplnbA&3WfGW@LPzmsMG><6e8`{d_y!=AR+yP7}3F1@?b^_vS2t!}eMt)~zvpC~idO zRv!I)Ze~24%(3nlz{KIHMw5bijyzVP;>P;Y!hn8QX z#uL1mtLLUAm2rqdB-_G*hiSw@^E6^PBrc+Yc3{$5t^?Bb)ct?Il~OBXoi3@bJc{Qa z`3o;g7Yd(-LTSDm{D*c5zcsb1kH))!8=Y>w3>9X+e4h26yZW9^=W;Lu^i}9w)WrY> z4Ho;-l%o)e{L=Cc-Y2JQK&!~vl0ek!KL_Mq`Z`G0_?CdkK~RMeFz`?pYH@ciVa@)x z-8S;5497fZWqV}h`5Rw13#XP~Lc>R#)ZZYfgwY>@* z?AWWwaNucn<$^`nJMdXa2|1UA+BE-tf*$jqvx<^a)L+c_t*f3`A(I8oH>aaA$PngP z?@;RIl@5w)oAsRB+&;h1Px!X0x9?{ry|*z^zn2%v=R{J@S3;@9MiPoNFN1`+bynIt zAm(%Io?b|-8*17f8_2>vKZss!itfl{W2Kmwyikyo zbXyK>YA44tmkvHV|S2+<>2dU#5s=){(F2Jw$;`(DkK%+&BPOV+&e^oW<`d zO8_8-((j_tt6p|)C-Eb}uG56hL$s29p*{klarnQ9t_WJe*`+kYcj|tadS3#4>^89* zscCoLS#5cgpSct`wn-&(lVq<_o9SFO0aeDW)=`{VO8JnGAWcVr1 zmqUx-6VdQD2GNB~lrq@pz+c7|!l^;O%J1#!VyX+YUTSdWW40NcNi}EBOD=%}3kSfz zTdaFCjL+qynwlr@lumr?C=I?E?DAk{J81()EMvpH8ugKdr+wM+UJB>}kFH++7|VDg z1<~>HKOfdlC3^>YkUUws;Q;=+GEwNzKsKrmjn@qNknZFz7MvL$x3;jPgpZcF9IeDc zJ2xYZx_h9)I`NMdr!kgY(ZAQygb~^rL=YIQ6-NP6!7-TPxig7hhKPUe#XN3~??=$F zVlPW8T7|u`2*pWg4fC7Bk#FlPKd^ue>9Jqt7Ao#*komyJWAD(N$}&4>{AroxLq(a^ zD$MUV2B1>m?VnIi6#5tC7s)})rsO%~JOl`6ChLP1g{ksTZVW*KXv+?%lw-j0WV3R= zrrGe$VuTi-3z@xRU=B+p@E$k`O2W}XGXQ5G&3FVq0jE1EunqzNhEQ-dT682yW2*4?+Tj+ke!%U=BD2rgYWO>k;MqA)HyS_i7^A;b#zBA6N zwsFVd(#;J9!6~hS1Iqf|i)$CQnpG^v;OO`NDK>o+4L`XL29KOmZ%kx~X{)wn56jvT z8uX65(I&RwJqw9&Ef2kN9u87gkvMNmx4oE!yC+HOn^|J?iGQg&9-o;pFA zp3=AHb9f+;xS%Wxz%U?H@wCW`Rid)^T{)YtY|-uOz0`w5l9~3%Gn62XbT|SZlScPb z!(nlwq;f@9p_)5TX|LL&S*AvlTF!CXs`O`&MZq}~3-*z!5x?@%XP#69hyIjzB|*tH z0Ri?|$yTu<(Jx6$tibtvK%~b9l?AZY;_K;G*?jm-P~b6oxsKcLCLivi5+Le{kuvJX zOAnSw1pe)@XPEQjAVLg!XFY&W&%2C{qs48AO$o835jKyqtNXqP<6BenM7Z;NjEhib zYE8?J5xR84^)GNg)uE3h+o{C|jdG`IlUnAyBnMD@c^?!>&85*5e`9f(bgpCV8HQ)O z=7D{Dao%@OR@hZk|3Aogy_G}~2QSgiS6!srtGW#M-Dj>^kM=DG$W~ye#BVzD`lZxZ zwky3y8WzKH!pGXh2P?aXM)!7%{R~CSQoLm-vP=J@K*5o7%T8qz7$N4?H=6V%Uj=TA zjjp8bVQ$wt&lz2CmZ2P^1v6BjXWCdFY|g%RuI>{f|B5JwVWVY z8x>r4zL;)@6n*wjQn)gb>!XgiO)fYB>S2Oyt7-IVnI`^hqOUiPi*HU~(%fh{Z)D`J z1cF**ZnrM5aXs*+s3y;}8fjvU>+$(oZQL+_<3UlAWcC3neF&tIE6tOAUD=dDm!SzT zF%f&oEVsl*T7{E~Da2!)em-G8Iam37SNZd@mjQo?g(Uwwt1O*+i}X5B*?q-=?G!3W z#9A+pV*5}l=d-|MLU+o_%FZetb>i?fDOpcc{`S;v&1nX9R-iZLDznlbpw%L<+Gu|4 zY#pVE51YS;cnQX?y04UD`!oIy0cWfz#?9rsZDn%c|~I>~km z^kzOh(zV#=@{%%MZ^o2c{-}o*HRtwM^M6X}42<19$L-W6PHAd)e&-)$DIj}K8s%LC zc|eK#|EufE!>TH~_Sf_5@yr`IOfqdIK}Fag5|+sZ$E@TyMx=Q}Qz}a=Orp|#aO|y= z%FN2wj%8&tmZps!)LyMjvHafV-cwmxS{}nvljOU1eb;wg-}SqG{DF9eJv@6oYq;0F z)}jWtU%!n?0e$SJ;cQV{_%|ng3nq&Ct;lYt+J+W8<0I;~v^+0wjJJ?}wg}0+e@Vh9 zV83kaprQ`-Qa5dbYIe1XRZ<64o9K;!)CEF`+}*&$jwL&D>laQCgJ|tVOU4z@=I*LNyJ8U=<^BsYSV8ObYC^-DsIDosbS%tEwx22zOYG`8JY67nB z6F@&A;7mbZ2b6qG&}o~OD%P;VgawJ-<&p=$n_;tP5$rH|?tvQyZ%zxnA!8R-r-xN} zvFbM-az6l9PC9Lh65dFExVi3$Bb zyDbXs?CRq5cGToDf51UoJj*Q7v}JmcO=^oTPo}2yw#5C*EwKuHF|MFKdRJB?dh*9&stJ_m#Dlsp1GP1T2j6n+Wl z>7EH%wOA*S%^Ppd7YPP7)J^Oz+jC zPG!|e_eIiWbtF)_;CCn=L>-^{RI`M1Sc2h9$Q6~BiBdO_|##jUV5V+}|G9l>X(|G&X@*57}6OQ&h zbU_du$!G0!?f!)2K@4QC_-5Tf?#Oq{5a94pN}_8VM(%XSCDiMf{V_I&KiQE6+Itd60hYKN9&JN=-fB%vok#rlulqGM-c0r+pHs^xJ65 zyDVD>Hi$)z5b~Z1WmvN)zUfkwYg!iQ%ARFr0Zqt|EreGbHm|tOsuFFvG@6>-4faW4t z5f_xZ(R*T37|C2+e20y1J4vekje8zhN3^%^29`4x8w6_B9QxXkqi`?nac_dBr=gd>G?gJR8ULx~*nR2@~ z*TEI|>F_$by#=KEfpVY@1iyVyL6V7K@A8*i^ zs<&W-$g84M$T70P9KJrS<$iGQ=$0->=$;ksQOMYP|cLd5dvKM^8* zK|AUHW9`)pC!i#~kl>}53LQk^jru%d*_It1f-_w-36Y#%(kLy^OcRiiC&KgpFJ!D& zlg)Ua8G^l6E1}H_NR9%YCwdC~=fUiSJ!S(0cMdr(rIhwG!tE-v(9D4DM7Mz2JTUH{ zM}}K#Lt)q#9dSjCUh>@$Bntu05I@XN-o)<>;%u&q*`x2L5%N3Ly;ID>Su{Z-U2jN3 zi;*9?@V4hC%Bm?Kp=_>U&8MyF(Kiu zc{(3>)}}M`A++M~5#-%@GtYS!mB%x{GoXuG7n^akdua-JKMf&W{Gpx&!!nTRLLT&7kjPmjQ&v}040SuFe|rJK8@z>i~)ew;$~d<0hUO*n-X zbBlx|6QQO%2mT+1sxu>_Z@5`_SQ9J*@-w&VZdQv&f=_60$NJ1lKu1P_>6P+AW_T^O zNY0&L&a=4)ar6}{M#~K@e#i`!ThryyOp5JzfT3|59>IWgEJGaskO!ntZdkgLJPWQHrd%n<>l}>R@x>?t@OBTL&RhL`BN9fDWT)Iq$Ggg zWC2c1ri$k6hi_*)ISr2#Li?Y#W7W8;I3X+5C~a@CgWkn0iKx%)=fFFwGXa)fo@^rA zh}U*0XaqS0Lt^yes%=h7tm8@AYH5{VF)DldPaNJe7R?z3F)7IP)}3z3PUQj<&XaVQ zY5K3~bpIcLFplX?GelmhjEp`GcWa84{4^&JqoLC7(w)8638kq{-okI9Nc z3^a3$(l;r10w{2h_zu^Ns!qfkzYuDjZ{1u6vylkN=?gcPX_ZcpE??qTshW@%#KX}Q zDjD7mu1j`X`a@LS`k$c}6TeAd*SWU=y-=RtY7+2}p>kXksLG$BHrDGCM=O_?2Y&?L z@;Wfz5pdti`?6fq{SsQs;DS&e?eMF=NCmCG_L11t(!bfUMK%(mQ{&8t_M@zM6<)jE z6@s_p;+4`U*NQM8Dz?;9TC6-+$S)VEkrykq+P2@|!??f12r3A5s$ssq!H+4e7MB{YHhZIEGTO%pkkQ$e^C(; zzUb&SNt1n@3x=D1FxQv~A0-3OgCC#Ol2QqjRV{;pq5v!f3bn*Q36cD2=F^=a=!m-R z!-e435x=nTX6z26wASHkd5KhJVa^4u2EIf%8H&zbyZCn(Viq8#y$eQJ@h~yIKZ}A} z0fJCG=z%$&SQf3;@p1PF0{G;DHJa3Rq5k)|33Ohld&49h5u;g&i_@1-P%o$HN}0I> z5t;i*ZwF!S?QbvE;IM;y?-n`H(1OgT8LVloyT^4er0FU43{EXyDBF;SjPFOIhGBV* zzu!&%74I^eT2(S1n!{xP8s10~&fhiM3S7cW-}zt;m_LKVIp9U3lHvpw1j4%w{1AqF zZ(VyQ4rX?ToAmRv+^*!5mgLA^v(o$460|OQ9(foo?ZRA(+Gq_T5VNykuXUPh)-$}K zXTzTTyj1-Pr%s1-WEk~KUy0uMgEHR|Wr3&K##gf*q4Jl*@=Wg6(KyPub_kRcJfcap zXTslF<~1uH8PS-S~}$TX>v8c@b%^(0vn(-SkLWed#0{K2TgQH-%m?6h8z|A63~Er2CmE?y82)t}}>MKC?(F1tjq z=Z-an;&xH^kmk8NiA)h&i%!raPziKijdm5SRk9A3jM8xuYu=AB^94rxNLz*C@v<^h zW8k^#EK){(6P@Bf#lDfAW;g_fE^o*zcL`L8j#lJ8%-~h;GKc3+yj0 zhs#f@>U`UGUy1vXmK>2I2RW{%YQ4J-l~Q!2Fb41n>M(Qhi3B)X;4XPl7gC$kp{- za_(YvIqiC*G&wB_;d&C&Xkkv- z@j9E}0eTxK)wHpBFEY_vew{Mc3iu4PfsbpHiaGcS;$!ks&2-(K+tq8M89>oJs zx)?w~xmONqeBivZHY(vevx~r4+XHool%|#B1HVO;F^eGq`^^0OOX3|YWOXdH;7ysm ztXEJ%bT2>i`CBbim}QK!G@F5_J|UcH)tnIuQ9&y>P!6*<_q)h;%Gu%_DJSS(uv6r> ze5L0`?|~eC@AFlUftE=(>$qT6+`S4nnj-%x^AcxD^>somt{uQU83^HKZS} zzr$Ihudw8!q-7jrdY{AjNE?o|@adiMEHqx}jD1_f4CS&D3XLzWh>~@S5MSOReiwyq zl?oan0p@;JLBIY!=1UUjir80`L^T!GT(L-{d366zH#cz;-V^pyc9AugJ=g&&Pg!O4 zJKhwtIPguKmcHN~(!;Hn)N0De3!}{c>#vjtWDI(Szc_kmm$Mdy7?xLbWrtZa6$bD8 zkSJd?ovaJ+3zf*FYk$`ku(ClvK*Y0vx_VYp2nw5wQJ)GA0)_xsmXcyMI}N_!a!g}z zEx9lb)MCmR0fX$FNHkL1%xX`r8-Wl-+4P1KSNNt#t68bOq-~|3-y4pp^WR=^ppo>f zyIekm>U5Nq`n9HOq`a2s4K@NP-r{8*AYB3IxIWj>1H$f^yStd>{t70_ZsjO2q;4pY z)_JtZez80(&=Ch(Af&wMqT2-2?qD8VyyF{IQ9x;fyu)}CKAgGkRwsz>&iJ9pif-|@ znR*+~2nIyoBuc7QNr;Rjp%;4|KYS#=f}pryy)1}4qvUV*+Z&4ngQEDy?Wf`)c)7}J z;&)2C_hE|9@)EyJQggPKArL^t&u%XpkH ziyFy-DKt=Y7E+bU6p3ZC36v1@01C_@men#2!}dy9d~71qE#Sq)c5!ZClJr1osF^&Jq)d4^z{oqN6g9h+$m0I^S;N*jHkd ze)o^eZrfOrhBCBVeZ*s^0YJw|=WI%s-xln~g!+tf95t=!pH!S}d}Id{c}-E1D6}l> zqG2J+++in-Gw69KchM`)F~@eP3T8y%{i_Cc7+**n$;*=Da#hS^ml8Ni`I)9y7Oq9W zIYa25fSKR3)Qn0v^o;kF*Webn^6dD@3WfYUPd1^uK9*KPbKTSwCCMxHgNr0OYU90O zeJd~Kc(M+slAjQ~NbzpvSe+q0NV9ji0~h%SF3OpuR^ibi+PM|~v%6CE_P$Wn+sE4E ziOWN)8M#En&1zNizCRJB_`Hcr=PM;T%)p2Mf_kD`|H24Tvw#Z4!E3`%KznFd3FA6R zIemk$d|^V1PzY-#zmBk&nx<32>=v@ghq#*j3O5B7+At9QlQ&(T1s|fGrDx^2nCzDv zlr2i{v3cb5$xnj6zt=k<4ZVPkJJQs->H0<7_TEJx4>tI zKheC{s zY?Fk4g#t>@3otrKr&Ue5Vp-Ik_gC=mh$~N{u|4$`_;QUx>@e>=^l}kg8SO(zZ#lkO%4U8r zj+Me2MmP*m?#ebVrQ8G9)pj1)Ry*o-r8*xW$rQ$Y^g$eyK`AyEEIq&sErx+sf+xFu0-J%Qpz?Y-OB+Pe&HHIbOhuTXHlF6&*|ce{~+~E(OVDC?#)P}Gz)PDHFXY9r6ff31%zV+qNW`>)=C#kD_R<*HVF|) z1o=TP(M2`5aCuqVVm zO`nqXLp@uqZ|(zKXY59~Lw_6XQ}>FwB;HKFVq>}^mc{XqQDP6Va9x3junw$z;#4@q z!r(kme@YP@J;RJAh>sO|;hwsT>J-u~RcIbR~^g}M9oAg19c+BIvpm%9l-9Id?-gm@)Q6VzR+Kd97gkyETi6nyq8Qq7KDecCq28qjdWx8Bt_{bPSBaJJMS_o z^=bTdra-RlK&9@X+F3R~CJ#>Po-~@oE}=?i4eFEJpWd%z`@#w}z`9)c2&N+Y5j@DZE;y$$H;!Ix)nHa;PK;3u*c^v93z1O+8EcT7-K($*$qMO)G z=jFDQ58PNh<_jarVTRZZr$i73+;y)<2f^P5(xHi`l(b+vkEl!b z!FF;KlPpX3(|NCq03pw3)DH>6^?&wvyUv6Ses+W97LCYxZ0M_;s8cAG+ozKPmRvJx zl-yLkWht3A(*;;F#HemU@2?3(mFP;k|9T3em#pW`*ZmUR!B)ay?FG@p>~fYV&ldsK zQ86v*w9j^Bv%nedV&=ee&wU3S_VzCiKLXaWu^Z!3NfsNx)1j*3f%h+SIUj%t$3Exo zpM9iVbT96D-WWBC=0Sg))c~Nq=Ke<`ME=-*3(Xk&=czAATJSiJFKd!|`N*LPzPbJ1 zD;_00_)g#Brx?=<;p$?}W?ra#8_kI!$_O3%TIm(9Sy+>&`2$pv62l$17sX;C$U2~H z(?RnYKUvFRsa#C=~6mcis+(fL0%lOpT2pRM2<;p-Lnb zH8uqNf*{Wpq-F~BRE&*JQ;_v`twS`tudu!%BcomIEX7xDv02+xoHtn+J^GX zu{6xtyKje08Ukc5PND`AXmkWs-SN<**ua{6mXOh^My6jyEvK0ThzIKi-I#di=qqi5M{4KJ3mYPya|_X3}Ladrb~wDXtnKALXq5mG3+`-oo)cUt3!FWO9I^d^JISYE0qP2x@w> zFj=%uA%&}F&cS`Ls-Iy>FN+YF&oI~Ze3II8i3Ko4m)ryK;A1}pHnE9(Ccy1dIw$bV zxp2hT$2VvzsWZWIqSEP4E!p*}dON`@dA}}drjv%&$^-4Rw zxYIztuZI>jg;ngUI!nto(TVrFY}1YdrO>xQ69C1qn-CPGfw#&x}G#{>%_tbyHUu9p^mf zpJQ0XM8`&w@`RxB?5Uc)FsB;oF)Ke=A1q_~QS%}e+Gat(jNQsn6kk%_E0o~cWJ^eg zJCJa49p0<`vv3Ms(5mD!=)*+a6;GRHe4wo>7PWWT{!kR$N2w85#Ij%r!lP5aHbK`{ z^yR0LEk4W<3PayMmTvFxV6PP=tK`pqcqv1@EbKl zr!h?I2w;G8&DP6#w7mnHeA$~dMEcO)gC_*@OrIYR`Wtt~zC~lcn|~2U?Y2mMjtMug z`_qYyK|y=J=+8`3eayO;cj#o%vA!rXZGCM-kwLC6Z%DzBN~YZs97ZFv-ecyQVrF7>_#8?@!uMJ<+z&9J``GG9E}va0~l zW+QRVVQx+ogXsHC&3K>}rY987K_4i_4}X#7r-R{9mIUHu3M z-6q9E)w*^K8VG8x7$l$h0-NMWh=o40ppbfc_k`&TJCCA<&WY|{Dj{6lY*8?`{tdS4 zM`&6=f+0&bC-}rzs+eCPn^0D7N7HE~{{#32MDF;1S_oS_bm8b|0Z{N>R9!%6YH?_s z#z$1;|G8?0h`w$ywdYz3vZpOM2`AoM-F*asxmEFD*DZ8GS^LLif}n3b+>SPwA4{rGhyoOyso> zjg?A`T^)5iy?&nbE}SN&s8cO!KELM$C0iHYVAnQ#4( z0TwsH26`sMhtV^-Msvppz$=EDy*=fOwQ|l#bn)xR*=m^FQ$sOB&?zK$# zlDiyfk8mwv9$lN==%Lsrsv88W@uo`~P5ht`kaxC=;!PAR;u@Cdkg`Sad})PA7j=S| z4rovJaJ#okAY7Q_u;8xB@-*!H&&EeYX8k}ldOy>GL5?ZA=n-nO$*_4As3ZGOfufHM zR9?d-S9NxfTNYqMQ_A}?-82V_#zo%%#EB^v?E>aR`!jY}8f@p()OEOoCo2z8wiccx z#?^mC7dh9Yri;)F)op$WU{YKdT)3&HW(cR0=bL9kwq6y_r6LqDc6l}sYMh0Q&MHSl z3pt$GJ>3vPA@O6af18Ac=)hSS+%S{v(Aeij4jy8 zK{{Dq|vJa`Y@+bL{peI|CYD*6z&z`O!UqDqUY+|45fe1AogO zZJJiM7*6T$4hrNaE1XN$p^2N?6#ki}R(&7VkotYw=x&VlcI<2k#{b zXFg^}Y9b9`*)?6;h0*N+3d0%mSl25I)3w1MUge>Giyi^7B!#|bSPS;BH8JKyBudR3Rh_jt1({LD*X&re53Lt^rdp%7S$ z%$C3@m@K6hau)aHF|n_?;d^(y9-y|YpAA?(W#CF?9AFTxkEe5ZJ9Mzii2199^%VEi zv&v|`+)C$cMbO67#e8Ecj|nX@6x$OAG9HDqaSK8V$d(*)kUNN5wn`Sv_}3Ut-T9Ct z6pcr)giog=;2ko9s;eW!TTlCUXI{$y7P~X zfcqXi@qmpXto=+u)l=2H*R2O#>ib@{|wa4l4EAOaUN=wt?& zW)}h(Fj}rgk0iJ=6X_M`UEBdS(0@5Knu#Z?`prywAgEHPJO4?`#trH~2{q8Z%4DEn zW+Zj1)(#pZ4sDwbkA6^$Q;>u|FQd(XT<8c1Dah-%R^h69vfeRsRXPyS+F)GAOH$B8 z>0V;`6#9t$c$pA{`bahF__`aFm_en>e<%Z|+E0o?ayRt-d` z=2;u&rJpEw$Jzy$7HFch*H6$V2_O87>Ka}B>IY_=?xF&E^0lctq6Vxv+={XAw+csa zv4fW2n9B1_1VY2_$?5M^DmdR;<1{QWzi+y2=$z~K!A2u_`^;WbgN(0Ct!EFXEXGqJt5QY~OM< z&&Bk4q~BhoC%{iOb((L2QALNQb~Xx@M43BdF7gkcm}mviM-NhJs>1CNlAzZ%CliWU zYa}ifP7@L>k(C*8*GZm7><-#ENV91L+`9D88Wx9CrCTKl`2+TmN$ zQ%Dw#0u04OZt|ZPEZ;~SRY@<>H`J)nJFAd)2LYc)z>vp6jee(wjBe@KrGhvEUOpG* zFgaVKWYHMZJoGKf?&?h>{TkG=8?yLGsrauQ8;%44QXUA9Wg`Cv4E}rm(D)EUYnere zTQ4LxDFo?W^%RZ3p8o@~N~}?}pxcys8lcpHPKiK zm_DTBUgL3*wXA*PN<29yZ{u%@(@mqxMA#_N0;&8gqsJv6UfG6nm?fkv;oznJD%5=5 zWJe)aS#wrkBjr8I!hqObMWY8z?-m^?%oudoJ%@~su3c9rWFKtAP|~}iURw6d#1Rl& zI@l-;le!2wf~JU_ozu(U@9Ou3mh#fN%%w@xeqsn-FX`PH1pi;EunW6@vKFAsl56R| zQ;uMM!8J6$G?R|{ydK)QaFUL0EBnn33mt|X(o`8Ww}H+aC2ThHI3MmfOFAt%Zmyf( z4gqJc%$|&HvH8bRQ5&vmg9|mxh zxCHbjpbDx44r3My?byaRoU!<|N$;>;e(a{(EusOR6u@4y?z}Jvr%2aSWnL;Gl4@S# zzaD$ryqD;8p)~9F@EYmhPU?q!b0E-uiaL>9%X9rYk;-8aA!#kb%J(RrS^eZwlJoSm zlrWx+uR1ilvn z*#y0QpnJzC1(|lnIPW$`o{9E}gfXM5wj=I)8HpyscEDyt#ald=gaz2Oqn|Z`3P?Np z(H$VlB($R?ig55;46cpi<=I#znAb!)c?%leQX5eNJ +# 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