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/docs/images/block-diagram-pro.png b/docs/images/block-diagram-pro.png
new file mode 100644
index 00000000..5fc77983
Binary files /dev/null and b/docs/images/block-diagram-pro.png differ
diff --git a/docs/images/block-diagram-pro.svg b/docs/images/block-diagram-pro.svg
new file mode 100644
index 00000000..000186fe
--- /dev/null
+++ b/docs/images/block-diagram-pro.svg
@@ -0,0 +1,5348 @@
+
+
diff --git a/docs/images/hackrf-pro-preliminary-photo.jpg b/docs/images/hackrf-pro-preliminary-photo.jpg
new file mode 100644
index 00000000..97629bbc
Binary files /dev/null and b/docs/images/hackrf-pro-preliminary-photo.jpg differ
diff --git a/docs/images/rad1o_8.jpg b/docs/images/rad1o_8.jpg
new file mode 100644
index 00000000..f64c265d
Binary files /dev/null and b/docs/images/rad1o_8.jpg differ
diff --git a/docs/source/enclosure_options.rst b/docs/source/enclosure_options.rst
index 51d3c5ff..788c085e 100644
--- a/docs/source/enclosure_options.rst
+++ b/docs/source/enclosure_options.rst
@@ -1,8 +1,8 @@
Enclosure Options
~~~~~~~~~~~~~~~~~
-The commercial version of HackRF One from Great Scott Gadgets ships with an injection molded plastic enclosure, but it is designed to fit two optional enclosures:
+Commercial versions of both HackRF Pro and HackRF One from Great Scott Gadgets ship with an injection molded plastic enclosure, but are also designed to fit two optional enclosures:
- * Hammond 1455J1201: HackRF One fits this extruded aluminum enclosure and other similar models from Hammond Manufacturing. In order to use the enclosure's end plates, you will have to drill them. An end plate template can be found in the HackRF One KiCad layout.
+ * Hammond 1455J1201: Both HackRF Pro and HackRF One fit this extruded aluminum enclosure and other similar models from Hammond Manufacturing. In order to use the enclosure's end plates, you will have to drill them. An end plate template can be found in the relevant HackRF KiCad layout.
- * Acrylic sandwich: You can also use a laser cut acrylic enclosure with HackRF One. This is a good option for access to the expansion headers. A design can be found in the HackRF One hardware directory. Use any laser cutting service or purchase from a `reseller `__.
\ No newline at end of file
+ * Acrylic sandwich: You can also use a laser cut acrylic enclosure with either HackRF Pro or HackRF One. This is a good option for access to the expansion headers. A design can be found in the HackRF hardware directory. Use any laser cutting service or purchase from a `reseller `__.
diff --git a/docs/source/expansion_interface.rst b/docs/source/expansion_interface.rst
index a87b8bf5..80002cdb 100644
--- a/docs/source/expansion_interface.rst
+++ b/docs/source/expansion_interface.rst
@@ -1,55 +1,7 @@
Expansion Interface
~~~~~~~~~~~~~~~~~~~
-The HackRF One expansion interface consists of headers P9, P20, P22, and P28. These four headers are installed on the commercial HackRF One from Great Scott Gadgets.
-
-
-
-P9 Baseband
-^^^^^^^^^^^
-
-A direct analog interface to the high speed dual ADC and dual DAC.
-
-.. list-table ::
- :header-rows: 1
- :widths: 1 1
-
- * - Pin
- - Function
- * - 1
- - GND
- * - 2
- - GND
- * - 3
- - GND
- * - 4
- - RXBBQ-
- * - 5
- - RXBBI-
- * - 6
- - RXBBQ+
- * - 7
- - RXBBI+
- * - 8
- - GND
- * - 9
- - GND
- * - 10
- - TXBBI-
- * - 11
- - TXBBQ+
- * - 12
- - TXBBI+
- * - 13
- - TXBBQ-
- * - 14
- - GND
- * - 15
- - GND
- * - 16
- - GND
-
-
+The common HackRF expansion interface consists of headers P20, P22, and P28. These headers are present on both HackRF Pro and HackRF One, and support hardware add-ons including PortaPack and Opera Cake.
P20 GPIO
^^^^^^^^
@@ -65,15 +17,15 @@ Providing access to GPIO, ADC, RTC, and power.
* - 1
- VBAT
* - 2
- - RTC_ALARM
+ - RTC_ALARM (One) / PB_5 (Pro)
* - 3
- - VCC
+ - VCC (One) / 3V3AUX (Pro)
* - 4
- WAKEUP
* - 5
- GPIO3_8
* - 6
- - GPIO3_0
+ - GPIO3_0 (One) / GPIO3_9 (Pro)
* - 7
- GPIO3_10
* - 8
@@ -121,7 +73,7 @@ I2S, SPI, I2C, UART, GPIO, and clocks.
* - Pin
- Function
* - 1
- - CLKOUT
+ - CLKOUT (One) / P2 (Pro)
* - 2
- CLKIN
* - 3
@@ -133,21 +85,21 @@ I2S, SPI, I2C, UART, GPIO, and clocks.
* - 6
- I2C1_SDA
* - 7
- - SPIFI_MISO
+ - SPIFI_MISO (One) / PB_1 (Pro)
* - 8
- - SPIFI_SCK
+ - SPIFI_SCK (One) / PB_3 (Pro)
* - 9
- - SPIFI_MOSI
+ - SPIFI_MOSI (One) / PA_4 (Pro)
* - 10
- GND
* - 11
- - VCC
+ - VCC (One) / 3V3AUX (Pro)
* - 12
- - I2S0_RX_SCK
+ - I2S0_RX_SCK (One) / PA_3 (Pro)
* - 13
- - I2S_RX_SDA
+ - I2S0_RX_SDA (One) / I2S0_TX_SDA (Pro)
* - 14
- - I2S0_RX_MCLK
+ - I2S0_RX_MCLK (One) / PB_0 (Pro)
* - 15
- I2S0_RX_WS
* - 16
@@ -169,7 +121,7 @@ I2S, SPI, I2C, UART, GPIO, and clocks.
* - 24
- SDA
* - 25
- - CLK6
+ - CLK6 (One) / AUX_CLK2 (Pro)
* - 26
- SCL
@@ -187,7 +139,7 @@ SDIO, GPIO, clocks, and CPLD.
* - Pin
- Function
* - 1
- - VCC
+ - VCC (One) / 3V3AUX (Pro)
* - 2
- GND
* - 3
@@ -211,25 +163,69 @@ SDIO, GPIO, clocks, and CPLD.
* - 12
- GND
* - 13
- - GCK2
+ - GCK2 (One) / P5_6 (Pro)
* - 14
- - GCK1
+ - GCK1 (One) / P5_7 (Pro)
* - 15
- - B1AUX14 (trigger output)
+ - Trigger out: B1AUX14 (One) / TRIGGER.OUT (Pro)
* - 16
- - B1AUX13 (trigger input)
+ - Trigger in: B1AUX13 (One) / TRIGGER.IN (Pro)
* - 17
- CPLD_TCK
* - 18
- - BANK2F3M2
+ - BANK2F3M2 (One) / PE_0 (Pro)
* - 19
- - CPLD_TDI
+ - CPLD_TDI (One) / I2S0_RX_SDA (Pro)
* - 20
- - BANK2F3M6
+ - BANK2F3M6 (One) / P9_1 (Pro)
* - 21
- - BANK2F3M12
+ - BANK2F3M12 (One) / P5_3 (Pro)
* - 22
- - BANK2F3M4
+ - BANK2F3M4 (One) / P1_7 (Pro)
+
+P9 Baseband (HackRF One)
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+A direct analog interface to the high speed dual ADC and dual DAC.
+
+.. list-table ::
+ :header-rows: 1
+ :widths: 1 1
+
+ * - Pin
+ - Function
+ * - 1
+ - GND
+ * - 2
+ - GND
+ * - 3
+ - GND
+ * - 4
+ - RXBBQ-
+ * - 5
+ - RXBBI-
+ * - 6
+ - RXBBQ+
+ * - 7
+ - RXBBI+
+ * - 8
+ - GND
+ * - 9
+ - GND
+ * - 10
+ - TXBBI-
+ * - 11
+ - TXBBQ+
+ * - 12
+ - TXBBI+
+ * - 13
+ - TXBBQ-
+ * - 14
+ - GND
+ * - 15
+ - GND
+ * - 16
+ - GND
Additional unpopulated headers and test points are available for test and development, but they may be incompatible with some enclosure or expansion options.
diff --git a/docs/source/external_clock_interface.rst b/docs/source/external_clock_interface.rst
index 4f9de698..fc3f3b1d 100644
--- a/docs/source/external_clock_interface.rst
+++ b/docs/source/external_clock_interface.rst
@@ -1,9 +1,17 @@
-===========================================
-External Clock Interface (CLKIN and CLKOUT)
-===========================================
+========================
+External Clock Interface
+========================
.. _external_clock_interface:
+HackRF Pro
+~~~~~~~~~~
+
+HackRF Pro has two configurable SMA ports, P1 and P2. By default, P1 is configured as CLKIN and P2 as CLKOUT. The default behaviour of these signals is as described for HackRF One below.
+
+HackRF One
+~~~~~~~~~~
+
HackRF One produces a 10 MHz clock signal on CLKOUT. The signal is a 3.3 V, 10 MHz square wave intended for a high impedance load.
The CLKIN port on HackRF One is a high impedance input that expects 3.3 V square wave at 10 MHz. Do not exceed 3.3 V or drop below 0 V on this input. Do not connect a clock signal at a frequency other than 10 MHz (unless you modify the firmware to support this). You may directly connect the CLKOUT port of one HackRF One to the CLKIN port of another HackRF One.
diff --git a/docs/source/hackrf_connectors.rst b/docs/source/hackrf_connectors.rst
index afd710a2..4d5fc66a 100644
--- a/docs/source/hackrf_connectors.rst
+++ b/docs/source/hackrf_connectors.rst
@@ -2,6 +2,6 @@
Connectors
==========
-The connectors on HackRF One are SMA.
+The connectors on both HackRF Pro and HackRF One are SMA.
-**Note:** SMA connectors and RP-SMA connectors are visually very similar. If you connect an RP-SMA antenna to HackRF One, it will seem to connect snugly but won't function at all because neither the male nor female side has a center pin. RP-SMA connectors are most common on 2.4 GHz antennas and are popular on Wi-Fi equipment. Adapters are available.
+**Note:** SMA connectors and RP-SMA connectors are visually very similar. If you connect an RP-SMA antenna to a HackRF, it will seem to connect snugly but won't function at all because neither the male nor female side has a center pin. RP-SMA connectors are most common on 2.4 GHz antennas and are popular on Wi-Fi equipment. Adapters are available.
diff --git a/docs/source/hackrf_one.rst b/docs/source/hackrf_one.rst
index e46e2877..13fac6bb 100644
--- a/docs/source/hackrf_one.rst
+++ b/docs/source/hackrf_one.rst
@@ -2,10 +2,12 @@
HackRF One
================================================
+.. _hackrf_one:
+
.. image:: ../images/HackRF-One-fd0-0009.jpeg
:alt: HackRF One
-HackRF One is the current hardware platform for the HackRF project. It is a Software Defined Radio peripheral capable of transmission or reception of radio signals from 1 MHz to 6 GHz. Designed to enable test and development of modern and next generation radio technologies, HackRF One is an open source hardware platform that can be used as a USB peripheral or programmed for stand-alone operation.
+HackRF One was the first production hardware platform for the HackRF project. It is a Software Defined Radio peripheral capable of transmission or reception of radio signals from 1 MHz to 6 GHz. Designed to enable test and development of modern and next generation radio technologies, HackRF One is an open source hardware platform that can be used as a USB peripheral or programmed for stand-alone operation.
| `Product page `_
| `Where to buy `_
diff --git a/docs/source/hackrf_pro.rst b/docs/source/hackrf_pro.rst
new file mode 100644
index 00000000..18e4a151
--- /dev/null
+++ b/docs/source/hackrf_pro.rst
@@ -0,0 +1,50 @@
+================================================
+HackRF Pro
+================================================
+
+.. image:: ../images/hackrf-pro-preliminary-photo.jpg
+ :alt: HackRF Pro
+
+HackRF Pro is the current hardware platform for the HackRF project. It is a Software Defined Radio peripheral capable of transmission or reception of radio signals from 100 kHz to 6 GHz. HackRF Pro is designed to be backwards compatible with software and hardware developed for use with
+:ref:`HackRF One `,
+whilst introducing many new features and improvements.
+
+| `Product page `_
+| `Where to buy `_
+
+Features
+~~~~~~~~
+
+ * 100 kHz to 6 GHz operating frequency
+ * Tunable from 0 Hz to 7.1 GHz
+ * Half-duplex transceiver
+ * Up to 20 million samples per second
+ * 8-bit quadrature samples (8-bit I and 8-bit Q)
+ * Compatible with GNU Radio, SDR#, and more
+ * Software-configurable RX and TX gain and baseband filter
+ * Software-controlled RF port power (50 mA at 3.3 V)
+ * SMA RF connector
+ * SMA clock input and output for synchronization and triggering
+ * Convenient buttons for programming
+ * Internal pin headers for expansion
+ * High-Speed USB 2.0 with Type-C connector
+ * USB-powered
+ * Open source hardware
+
+Compared to HackRF One, HackRF Pro introduces a host of new and updated features, including:
+
+ * Wider operating frequency range
+ * Improved RF performance with flatter frequency response
+ * Modern USB Type-C connector
+ * Built-in TCXO crystal oscillator for superior timing stability
+ * Logic upgrade from a CPLD to a power-efficient FPGA
+ * Elimination of the DC spike
+ * Extended-precision mode with 16-bit samples for low sample rates (typical ENOB: 9-11)
+ * Half-precision mode with 4-bit samples at up to 40 Msps
+ * More RAM and flash memory for custom firmware
+ * Installed shielding around the radio section
+ * Trigger input and output accessible through clock connectors
+ * Cutout in the PCB provides space for future add-ons
+ * Improved power management
+ * Enhanced RF port protection
+ * Facility to hardware-disable transmit mode
diff --git a/docs/source/hackrfs_buttons.rst b/docs/source/hackrfs_buttons.rst
index d2a6a690..4282ba92 100644
--- a/docs/source/hackrfs_buttons.rst
+++ b/docs/source/hackrfs_buttons.rst
@@ -2,10 +2,12 @@
Buttons
=======
+This information is applicable to both HackRF Pro and HackRF One.
+
The **RESET button** resets the microcontroller. This is a reboot that should result in a USB re-enumeration.
-The **DFU button** invokes a USB DFU bootloader located in the microcontroller's ROM. This bootloader makes it possible to unbrick a HackRF One with damaged firmware because the ROM cannot be overwritten.
+The **DFU button** invokes a USB DFU bootloader located in the microcontroller's ROM. This bootloader makes it possible to unbrick a HackRF with damaged firmware because the ROM cannot be overwritten.
The DFU button only invokes the bootloader during reset. This means that it can be used for other functions by custom firmware.
-To invoke DFU mode: Press and hold the DFU button. While holding the DFU button, reset the HackRF One either by pressing and releasing the RESET button or by powering on the HackRF One. Release the DFU button.
+To invoke DFU mode: Press and hold the DFU button. While holding the DFU button, reset the HackRF either by pressing and releasing the RESET button or by powering on the HackRF. Release the DFU button.
diff --git a/docs/source/hardware_components.rst b/docs/source/hardware_components.rst
index d960c5e2..f2085bba 100644
--- a/docs/source/hardware_components.rst
+++ b/docs/source/hardware_components.rst
@@ -2,20 +2,50 @@
Hardware Components
================================================
-Major parts used in HackRF One:
+Block Diagrams
+~~~~~~~~~~~~~~
+
+HackRF Pro Block Diagram
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. image:: ../images/block-diagram-pro.png
+ :align: center
+
+HackRF One r1-r8 Block Diagram
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. image:: ../images/block-diagram.png
+ :align: center
+
+HackRF One r9 Block Diagram
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. image:: ../images/block-diagram-r9.png
+ :align: center
+
+Key Components
+~~~~~~~~~~~~~~
+
+Major parts used in HackRF:
+
+* `MAX2831 2.3 to 2.6 GHz transceiver `__
+ * Used on HackRF Pro.
+ * `Datasheet `__
* `MAX2837 2.3 to 2.7 GHz transceiver `__
+ * Used on HackRF One (except revision r9), Jawbreaker and rad1o.
* `Datasheet `__
* `MAX2839 2.3 to 2.7 GHz transceiver `__
+ * Substitution for MAX2837, used on HackRF One revision r9.
* `Datasheet `__
- * substitution for MAX2837.
* `MAX5864 ADC/DAC `__
* `Datasheet `__
* `Si5351 clock generator `__
* `AN619: Manually Generating an Si5351 Register Map `__
* `Datasheet `__ - see AN619 for the complete register map.
* `Other Documentation `__ - includes application notes, user guides, and white papers.
-* CoolRunner-II CPLD
+* `ice40 UltraPlus FPGA `__ (HackRF Pro)
+* CoolRunner-II CPLD (all other platforms)
* `LPC43xx ARM Cortex-M4 microcontroller `__
* `User Manual `__
* `Datasheet `__
@@ -25,22 +55,5 @@ Major parts used in HackRF One:
* `RFFC5072 mixer/synthesizer `__
* `Datasheet `__
* `Other Documentation `__ ; click "Technical Documents" - includes programming guides and application notes.
-* `W25Q80BV 8M-bit Flash `__
-
-
-Block Diagrams
-~~~~~~~~~~~~~~
-
-HackRF One r1-r8 Block Diagram
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. image:: ../images/block-diagram.png
- :align: center
-
-|
-
-HackRF One r9 Block Diagram
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. image:: ../images/block-diagram-r9.png
- :align: center
+* `W25Q32 32M-bit Flash `__ (HackRF Pro)
+* `W25Q80BV 8M-bit Flash `__ (all other platforms)
diff --git a/docs/source/hardware_triggering.rst b/docs/source/hardware_triggering.rst
index a6b3c7f2..6411fb35 100644
--- a/docs/source/hardware_triggering.rst
+++ b/docs/source/hardware_triggering.rst
@@ -4,26 +4,52 @@
Hardware Triggering
===================
-HackRF One transmit and receive operations can be synchronized with another HackRF One or with other external equipment by using the trigger input and output on pin header P28. Triggering provides time synchronization with error of less than one sample period.
+HackRF transmit and receive operations can be synchronized with another HackRF or with other external equipment by using the trigger input and output. Triggering provides time synchronization with error of less than one sample period.
+HackRF Pro has two configurable SMA ports, P1 and P2, which can be set up to provide both clock synchronization and triggering.
+
+HackRF One has CLKIN and CLKOUT ports for clock synchronization, but hardware triggering requires opening the case to access the P28 header.
Clock Synchronization
~~~~~~~~~~~~~~~~~~~~~
-When triggering one HackRF One from another, it is often desirable to first ensure that the two devices share a common frequency reference. This has an added benefit of grounding the HackRFs to each other, eliminating one of the wires required for triggering. See :ref:`External Clock Interface ` for instructions.
+When triggering one HackRF from another, it is often desirable to first ensure that the two devices share a common frequency reference. This has an added benefit of grounding the HackRFs to each other, eliminating one of the wires required for triggering. See :ref:`External Clock Interface ` for instructions.
-Either HackRF One may serve as the clock source for the other regardless of which is providing the trigger output.
+Either HackRF may serve as the clock source for the other regardless of which is providing the trigger output.
+
+Usage
+~~~~~
+
+Use ``hackrf_info`` to discover the serial numbers of both HackRFs. Using the serial number of the HackRF to be triggered, use ``hackrf_transfer -H`` to set up a triggered operation. For example:
+
+* ``hackrf_transfer -H -d -a 0 -l 32 -g 32 -r rx1.cs8``
+
+The command will print "Waiting for trigger..." until a trigger signal is detected on the device's trigger input.
+
+In another terminal, use the serial number of the triggering HackRF One to initiate an operation to take place at the same time as the triggered operation. For example:
+
+* ``hackrf_transfer -d -a 0 -l 32 -g 32 -r rx2.cs8``
+
+Note that no special argument is required to activate the trigger output.
+
+Both ``hackrf_transfer`` commands will start sampling RF signals at the same time, accurate to less than one sample period.
-Requirements
-~~~~~~~~~~~~
+Additional Devices
+~~~~~~~~~~~~~~~~~~
+
+Multiple HackRFs may be triggered by a single HackRF. Ensure that all the devices share a common ground and then connect one device's trigger output to the trigger inputs of the other devices (with jumpers connected via a breadboard, for example).
+
+Equipment other than a HackRF may be connected to a HackRF's trigger input or output. The trigger signal is a 3.3 V pulse that triggers on the rising edge.
+
+HackRF One Triggering Requirements
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To connect two HackRF Ones for triggering you will need:
* a male-to-male jumper wire for 0.1" pin headers
* an SMA cable for clock synchronization or a second jumper wire
-
.. _open_your_hackrf_one:
Open Your HackRF One
@@ -49,32 +75,6 @@ First ensure that the two devices share a common ground. This may be accomplishe
Next use a jumper wire to connect P28 pin 15 (trigger output) on one HackRF One to P28 pin 16 (trigger input) on the other HackRF One.
-Usage
-~~~~~
-
-Use ``hackrf_info`` to discover the serial numbers of both HackRF Ones. Using the serial number of the HackRF One to be triggered, use ``hackrf_transfer -H`` to set up a triggered operation. For example:
-
-* ``hackrf_transfer -H -d -a 0 -l 32 -g 32 -r rx1.cs8``
-
-The command will print "Waiting for trigger..." until a trigger signal is detected on the device's trigger input.
-
-In another terminal, use the serial number of the triggering HackRF One to initiate an operation to take place at the same time as the triggered operation. For example:
-
-* ``hackrf_transfer -d -a 0 -l 32 -g 32 -r rx2.cs8``
-
-Note that no special argument is required to activate the trigger output.
-
-Both ``hackrf_transfer`` commands will start sampling RF signals at the same time, accurate to less than one sample period.
-
-
-Additional Devices
-~~~~~~~~~~~~~~~~~~
-
-Multiple HackRF Ones may be triggered by a single HackRF One. Ensure that all the devices share a common ground and then connect one device's trigger output to the trigger inputs of the other devices (with jumpers connected via a breadboard, for example).
-
-Equipment other than a HackRF One may be connected to a HackRF One's trigger input or output. The trigger signal is a 3.3 V pulse that triggers on the rising edge.
-
-
References
~~~~~~~~~~
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 25379195..ce845e66 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -13,9 +13,17 @@ Welcome to HackRF's documentation!
.. toctree::
:maxdepth: 2
- :caption: HackRF One Hardware
+ :caption: Hardware Platforms
+
+ hackrf_pro
+ hackrf_one
+ rad1o
+ jawbreaker
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Hardware Information
- hackrf_one
hackrf_minimum_requirements
list_of_hardware_revisions
hardware_components
@@ -29,13 +37,6 @@ Welcome to HackRF's documentation!
usb_cables
rf_shield_installation
-.. toctree::
- :maxdepth: 2
- :caption: Jawbreaker Hardware
-
- jawbreaker
-
-
.. toctree::
:maxdepth: 2
:caption: Firmware
diff --git a/docs/source/leds.rst b/docs/source/leds.rst
index fa598d41..ce2b89ce 100644
--- a/docs/source/leds.rst
+++ b/docs/source/leds.rst
@@ -2,8 +2,19 @@
LEDs
====
+HackRF Pro
+~~~~~~~~~~
+
+When HackRF Pro is plugged in to a USB host, four LEDs should turn on: MCU, FPGA, RF, and USB. The MCU LED indicates that the primary internal power supply is working properly. The FPGA and RF LEDs indicate that firmware is running and has switched on additional internal power supplies. The USB LED indicates that the HackRF Pro is communicating with the host over USB.
+
+HackRF One
+~~~~~~~~~~
+
When HackRF One is plugged in to a USB host, four LEDs should turn on: 3V3, 1V8, RF, and USB. The 3V3 LED indicates that the primary internal power supply is working properly. The 1V8 and RF LEDs indicate that firmware is running and has switched on additional internal power supplies. The USB LED indicates that the HackRF One is communicating with the host over USB.
+Both versions
+~~~~~~~~~~~~~
+
The RX and TX LEDs indicate that a receive or transmit operation is currently in progress.
-Each LED is a single color. There are no multi-colored LEDs on HackRF One. Adjacent LEDs are different colors in order to make them easier to distinguish from one another. The colors do not mean anything.
+Each LED is a single color. There are no multi-colored LEDs on either HackRF One or HackRF Pro. Adjacent LEDs are different colors in order to make them easier to distinguish from one another. The colors do not mean anything.
diff --git a/docs/source/list_of_hardware_revisions.rst b/docs/source/list_of_hardware_revisions.rst
index 134c41eb..9a7ab235 100644
--- a/docs/source/list_of_hardware_revisions.rst
+++ b/docs/source/list_of_hardware_revisions.rst
@@ -2,11 +2,15 @@
Hardware Revisions
================================================
-HackRF One hardware revisions exist mainly to deal with changes in component availability. Each of the revisions meet the same performance specifications that are measured in the factory.
+Hardware revisions exist mainly to deal with changes in component availability. Each revision of a product meets the same performance specifications that are measured in the factory.
+HackRF Pro
+~~~~~~~~~~
-List of Hardware Revisions
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+The initial production revision of HackRF Pro is r1.2.1.
+
+HackRF One
+~~~~~~~~~~
HackRF One r1–r4
^^^^^^^^^^^^^^^^
diff --git a/docs/source/rad1o.rst b/docs/source/rad1o.rst
new file mode 100644
index 00000000..0a6e52d1
--- /dev/null
+++ b/docs/source/rad1o.rst
@@ -0,0 +1,16 @@
+=====
+rad1o
+=====
+
+rad1o is the badge from the 2015 Chaos Communication Camp.
+
+The rad1o badge contains a full-featured SDR (software defined radio) half-duplex transceiver, operating in a frequency range of about 50 MHz - 4000 MHz, and is software compatible to the HackRF.
+
+.. image:: ../images/rad1o_8.jpg
+ :alt: rad1o
+
+(rad1o picture provided by Christoph Krichenbauer with Creative Commons License CC-BY-NC_SA)
+
+More information can be found at the `rad1o badge wiki `__
+
+Compared to HackRF One, the rad1o badge uses a different mixer (MAX2871) with a reduced frequency range.
diff --git a/docs/source/rf_shield_installation.rst b/docs/source/rf_shield_installation.rst
index 63a10304..56a5b84c 100644
--- a/docs/source/rf_shield_installation.rst
+++ b/docs/source/rf_shield_installation.rst
@@ -2,6 +2,8 @@
RF Shield Installation Instructions
===============================================
+HackRF Pro ships with an RF shield as standard.
+
Official Great Scott Gadgets HackRF Ones do not come from the factory with an RF shield installed around the radio section of the PCB. They do, however, have pads in place so that one may be installed if a user has a reason and an inclination to do so. The reason that they do not come preinstalled is that early testing revealed that the RF shield did little to improve the performance of the HackRF One. The recommended RF shield is the BMI-S-230-F-R (frame) with the BMI-S-230-C (shield). A two part RF shield is recommended because the shield section can be removed to allow access to the RF section of the HackRF One. This can be important if it becomes necessary to probe any part of the RF section, or to replace any parts of the RF section. However, even with a two part RF shield, it can be difficult to access the RF section of the HackRF One in certain situations. The following steps are a basic set of instructions for installing a RF shield on a HackRF One.
**CAUTION: Soldering a RF shield onto a HackRF One comes with a certain amount of risk. Beyond the inherent risks of soldering itself, this process may damage the HackRF One and no warranty is available to cover damage incurred from this process. If you do choose to install a RF shield on your HackRF One please proceed with caution.**
diff --git a/docs/source/usb_cables.rst b/docs/source/usb_cables.rst
index c947ce88..0f003cf5 100644
--- a/docs/source/usb_cables.rst
+++ b/docs/source/usb_cables.rst
@@ -21,7 +21,7 @@ The before and after images were both taken with the preamp on and the LNA and V
-Why isn't my HackRF One detectable after I plug it into my computer?
+Why isn't my HackRF detectable after I plug it into my computer?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-If your HackRF One isn't immediately detectable it is very possible that your Micro USB cable is not meeting HackRF One's requirements. HackRF One requires quite a bit of supply current and solid USB 2.0 high speed communications to operate. It is common for HackRF One to reveal cables with deficiencies such as carrying power but not data, carrying data but not enough power, etc. Please try multiple cables to resolve this issue. More than once people have gotten their HackRF One to work after trying their fifth cable.
\ No newline at end of file
+If your HackRF isn't immediately detectable it is very possible that your USB cable is not meeting HackRF's requirements. HackRF requires quite a bit of supply current and solid USB 2.0 high speed communications to operate. It is common for HackRF to reveal cables with deficiencies such as carrying power but not data, carrying data but not enough power, etc. Please try multiple cables to resolve this issue. More than once people have gotten their HackRF to work after trying their fifth cable.
diff --git a/firmware/README b/firmware/README
index 8235e816..50a9e45e 100644
--- a/firmware/README
+++ b/firmware/README
@@ -28,6 +28,7 @@ $ cmake ..
$ make
$ hackrf_spiflash -w hackrf_usb.bin
+If you have a HackRF Pro, add -DBOARD=PRALINE to the cmake command.
If you have a Jawbreaker, add -DBOARD=JAWBREAKER to the cmake command.
If you have a rad1o, use -DBOARD=RAD1O instead.
@@ -48,9 +49,9 @@ For loading firmware into RAM with DFU you will need:
http://dfu-util.sourceforge.net/
-To start up HackRF One in DFU mode, hold down the DFU button while powering it
-on or while pressing and releasing the RESET button. Release the DFU button
-after the 3V3 LED illuminates.
+To start up HackRF One or HackRF Pro in DFU mode, hold down the DFU button
+while powering it on or while pressing and releasing the RESET button. Release
+the DFU button after the 3V3 LED illuminates.
A .dfu file is built by default when building firmware. Alternatively you can
use a known good .dfu file from a release package. Load the firmware into RAM
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/adc.c b/firmware/common/adc.c
new file mode 100644
index 00000000..5d1ddaca
--- /dev/null
+++ b/firmware/common/adc.c
@@ -0,0 +1,45 @@
+/*
+ * 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 "adc.h"
+#include
+#include
+
+uint16_t adc_read(uint8_t pin)
+{
+ bool alt_pin = (pin & 0x80);
+ pin &= ~0x80;
+ uint8_t pin_mask = (1 << pin);
+ if (alt_pin) {
+ SCU_ENAIO0 |= pin_mask;
+ } else {
+ SCU_ENAIO0 &= ~pin_mask;
+ }
+ ADC0_CR = ADC_CR_SEL(pin_mask) | ADC_CR_CLKDIV(45) | ADC_CR_PDN | ADC_CR_START(1);
+ while (!(ADC0_GDR & ADC_DR_DONE) || (((ADC0_GDR >> 24) & 0x7) != pin))
+ ;
+ return (ADC0_GDR >> 6) & 0x03FF;
+}
+
+void adc_off(void)
+{
+ ADC0_CR = 0;
+}
diff --git a/firmware/common/adc.h b/firmware/common/adc.h
new file mode 100644
index 00000000..94481678
--- /dev/null
+++ b/firmware/common/adc.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 __ADC_H__
+#define __ADC_H__
+
+#include
+
+uint16_t adc_read(uint8_t pin);
+void adc_off(void);
+
+#endif // __ADC_H__
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..069d1a09
--- /dev/null
+++ b/firmware/common/fpga.c
@@ -0,0 +1,138 @@
+/*
+ * 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 "fpga.h"
+#include "fpga_regs.def"
+
+/* Set up all registers according to the loaded bitstream's defaults. */
+void fpga_init(fpga_driver_t* const drv)
+{
+ // Standard bitstream default register values.
+ set_FPGA_STANDARD_CTRL_DC_BLOCK(drv, true);
+ set_FPGA_STANDARD_CTRL_QUARTER_SHIFT_EN(drv, false);
+ set_FPGA_STANDARD_CTRL_QUARTER_SHIFT_UP(drv, false);
+ set_FPGA_STANDARD_CTRL_PRBS(drv, false);
+ set_FPGA_STANDARD_CTRL_TRIGGER_EN(drv, false);
+ set_FPGA_STANDARD_TX_CTRL(drv, 0);
+
+ // TODO support the other bitstreams
+
+ fpga_regs_commit(drv);
+}
+
+void fpga_setup(fpga_driver_t* const drv)
+{
+ // TODO implement fpga_target.c
+
+ fpga_init(drv);
+}
+
+uint8_t fpga_reg_read(fpga_driver_t* const drv, uint8_t r)
+{
+ uint8_t v;
+ ssp1_set_mode_ice40();
+ v = ice40_spi_read(drv->bus, r);
+ ssp1_set_mode_max283x();
+ drv->regs[r] = v;
+ return v;
+}
+
+void fpga_reg_write(fpga_driver_t* const drv, uint8_t r, uint8_t v)
+{
+ drv->regs[r] = v;
+ ssp1_set_mode_ice40();
+ ice40_spi_write(drv->bus, r, v);
+ ssp1_set_mode_max283x();
+ FPGA_REG_SET_CLEAN(drv, r);
+}
+
+static inline void fpga_reg_commit(fpga_driver_t* const drv, uint8_t r)
+{
+ fpga_reg_write(drv, r, drv->regs[r]);
+}
+
+void fpga_regs_commit(fpga_driver_t* const drv)
+{
+ int r;
+ for (r = 1; r < FPGA_NUM_REGS; r++) {
+ if ((drv->regs_dirty >> r) & 0x1) {
+ fpga_reg_commit(drv, r);
+ }
+ }
+}
+
+void fpga_set_trigger_enable(fpga_driver_t* const drv, const bool enable)
+{
+ fpga_reg_read(drv, FPGA_STANDARD_CTRL);
+ set_FPGA_STANDARD_CTRL_TRIGGER_EN(drv, enable & 0b1);
+ fpga_regs_commit(drv);
+}
+
+void fpga_set_rx_dc_block_enable(fpga_driver_t* const drv, const bool enable)
+{
+ set_FPGA_STANDARD_CTRL_DC_BLOCK(drv, enable);
+ fpga_regs_commit(drv);
+}
+
+void fpga_set_rx_decimation_ratio(fpga_driver_t* const drv, const uint8_t value)
+{
+ set_FPGA_STANDARD_RX_DECIM(drv, value & 0b111);
+ fpga_regs_commit(drv);
+}
+
+void fpga_set_rx_quarter_shift_mode(
+ fpga_driver_t* const drv,
+ const fpga_quarter_shift_mode_t mode)
+{
+ set_FPGA_STANDARD_CTRL_QUARTER_SHIFT_EN(drv, (mode >> 0) & 0b1);
+ set_FPGA_STANDARD_CTRL_QUARTER_SHIFT_UP(drv, (mode >> 1) & 0b1);
+ fpga_regs_commit(drv);
+}
+
+void fpga_set_tx_interpolation_ratio(fpga_driver_t* const drv, const uint8_t value)
+{
+ set_FPGA_STANDARD_TX_INTRP(drv, value & 0b111);
+ fpga_regs_commit(drv);
+}
+
+void fpga_set_prbs_enable(fpga_driver_t* const drv, const bool enable)
+{
+ set_FPGA_STANDARD_CTRL(drv, 0);
+ if (enable) {
+ set_FPGA_STANDARD_CTRL_PRBS(drv, enable);
+ }
+ fpga_regs_commit(drv);
+}
+
+void fpga_set_tx_nco_enable(fpga_driver_t* const drv, const bool enable)
+{
+ set_FPGA_STANDARD_TX_CTRL_NCO_EN(drv, enable);
+ fpga_regs_commit(drv);
+}
+
+void fpga_set_tx_nco_pstep(fpga_driver_t* const drv, const uint8_t phase_increment)
+{
+ set_FPGA_STANDARD_TX_PSTEP(drv, phase_increment);
+ fpga_regs_commit(drv);
+}
diff --git a/firmware/common/fpga.h b/firmware/common/fpga.h
new file mode 100644
index 00000000..fb4270a8
--- /dev/null
+++ b/firmware/common/fpga.h
@@ -0,0 +1,83 @@
+/*
+ * 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
+#include "ice40_spi.h"
+
+/* Up to 6 registers, each containing up to 8 bits of data */
+#define FPGA_NUM_REGS 6
+#define FPGA_DATA_REGS_MAX_VALUE 255
+
+typedef enum {
+ FPGA_QUARTER_SHIFT_MODE_NONE = 0b00,
+ FPGA_QUARTER_SHIFT_MODE_UP = 0b11,
+ FPGA_QUARTER_SHIFT_MODE_DOWN = 0b01,
+} fpga_quarter_shift_mode_t;
+
+struct fpga_driver_t;
+typedef struct fpga_driver_t fpga_driver_t;
+
+struct fpga_driver_t {
+ ice40_spi_driver_t* bus;
+ uint8_t regs[FPGA_NUM_REGS];
+ uint8_t regs_dirty;
+};
+
+/* Initialize the loaded bitstream's registers to their default values. */
+extern void fpga_init(fpga_driver_t* const drv);
+
+/* Initialize fpga and gateware. */
+extern void fpga_setup(fpga_driver_t* const drv);
+
+/* Read a register via SPI. Save a copy to memory and return
+ * value. Mark clean. */
+extern uint8_t fpga_reg_read(fpga_driver_t* const drv, uint8_t r);
+
+/* Write value to register via SPI and save a copy to memory. Mark
+ * clean. */
+extern void fpga_reg_write(fpga_driver_t* const drv, uint8_t r, uint8_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 fpga_regs_commit(fpga_driver_t* const drv);
+
+void fpga_set_trigger_enable(fpga_driver_t* const drv, const bool enable);
+void fpga_set_rx_dc_block_enable(fpga_driver_t* const drv, const bool enable);
+void fpga_set_rx_decimation_ratio(fpga_driver_t* const drv, const uint8_t value);
+void fpga_set_rx_quarter_shift_mode(
+ fpga_driver_t* const drv,
+ const fpga_quarter_shift_mode_t mode);
+void fpga_set_tx_interpolation_ratio(fpga_driver_t* const drv, const uint8_t value);
+
+void fpga_set_prbs_enable(fpga_driver_t* const drv, const bool enable);
+void fpga_set_tx_nco_enable(fpga_driver_t* const drv, const bool enable);
+void fpga_set_tx_nco_pstep(fpga_driver_t* const drv, const uint8_t phase_increment);
+
+bool fpga_image_load(unsigned int index);
+bool fpga_spi_selftest();
+bool fpga_sgpio_selftest();
+bool fpga_if_xcvr_selftest();
+
+#endif // __FPGA_H
diff --git a/firmware/common/fpga_image.c b/firmware/common/fpga_image.c
new file mode 100644
index 00000000..ddac8595
--- /dev/null
+++ b/firmware/common/fpga_image.c
@@ -0,0 +1,108 @@
+/*
+ * 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 "hackrf_core.h"
+#include "lz4_blk.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;
+
+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)
+ selftest.fpga_image_load = SKIPPED;
+ selftest.report.pass = false;
+ 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.
+ ice40_spi_target_init(&ice40);
+ ssp1_set_mode_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();
+
+ // Update selftest result.
+ selftest.fpga_image_load = success ? PASSED : FAILED;
+ if (selftest.fpga_image_load != PASSED) {
+ selftest.report.pass = false;
+ }
+
+ return success;
+}
diff --git a/firmware/common/fpga_regs.def b/firmware/common/fpga_regs.def
new file mode 100644
index 00000000..c575c493
--- /dev/null
+++ b/firmware/common/fpga_regs.def
@@ -0,0 +1,74 @@
+/* -*- mode: c -*-
+ *
+ * Copyright 2012 Michael Ossmann
+ * 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_REGS_DEF
+#define __FPGA_REGS_DEF
+
+#define FPGA_REG_SET_CLEAN(_d, _r) (_d->regs_dirty &= ~(1UL<<_r))
+#define FPGA_REG_SET_DIRTY(_d, _r) (_d->regs_dirty |= (1UL<<_r))
+
+/* 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. */
+
+/* On set_, register is always set dirty, even if nothing
+ * changed. This makes sure that writes that have side effects,
+ * e.g. frequency setting, are not skipped. */
+
+/* n=name, r=regnum, o=offset (bits from LSB) of LSB of field,
+ * l=length (bits) */
+#define __MREG__(n,r,o,l) \
+static inline uint8_t get_##n(fpga_driver_t* const _d) { \
+ return (_d->regs[r] >> o) & ((1L<regs[r] &= (uint8_t)(~(((1L<regs[r] |= (uint8_t)(((v&((1L<
+ *
+ * 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 "m0_state.h"
+#include "streaming.h"
+#include "selftest.h"
+#include "fpga.h"
+
+// USB buffer used during selftests.
+#define USB_BULK_BUFFER_SIZE 0x8000
+extern uint8_t usb_bulk_buffer[USB_BULK_BUFFER_SIZE];
+
+static int rx_samples(const unsigned int num_samples, uint32_t max_cycles)
+{
+ uint32_t cycle_count = 0;
+ int rc = 0;
+
+ m0_set_mode(M0_MODE_RX);
+ m0_state.shortfall_limit = 0;
+ baseband_streaming_enable(&sgpio_config);
+ while (m0_state.m0_count < num_samples) {
+ cycle_count++;
+ if ((max_cycles > 0) && (cycle_count >= max_cycles)) {
+ rc = -1;
+ break;
+ }
+ }
+ baseband_streaming_disable(&sgpio_config);
+ m0_set_mode(M0_MODE_IDLE);
+
+ return rc;
+}
+
+bool fpga_spi_selftest()
+{
+ // Skip if FPGA configuration failed.
+ if (selftest.fpga_image_load != PASSED) {
+ selftest.fpga_spi = SKIPPED;
+ return false;
+ }
+
+ // Test writing a register and reading it back.
+ uint8_t reg = 5;
+ uint8_t write_value = 0xA5;
+ ssp1_set_mode_ice40();
+ ice40_spi_write(&ice40, reg, write_value);
+ uint8_t read_value = ice40_spi_read(&ice40, reg);
+ ssp1_set_mode_max283x();
+
+ // Update selftest result.
+ selftest.fpga_spi = (read_value == write_value) ? PASSED : FAILED;
+ if (selftest.fpga_spi != PASSED) {
+ selftest.report.pass = false;
+ }
+
+ return selftest.fpga_spi == PASSED;
+}
+
+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()
+{
+ bool timeout = false;
+
+ // Skip if FPGA configuration failed or its SPI bus is not working.
+ if ((selftest.fpga_image_load != PASSED) || (selftest.fpga_spi != PASSED)) {
+ selftest.sgpio_rx = SKIPPED;
+ return false;
+ }
+
+ // Enable PRBS mode.
+ fpga_set_prbs_enable(&fpga, true);
+
+ // Stream 512 samples from the FPGA.
+ sgpio_configure(&sgpio_config, SGPIO_DIRECTION_RX);
+ if (rx_samples(512, 10000) == -1) {
+ timeout = true;
+ }
+
+ // Disable PRBS mode.
+ fpga_set_prbs_enable(&fpga, false);
+
+ // Generate sequence from first value and compare.
+ bool seq_in_sync = (usb_bulk_buffer[0] != 0);
+ 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.
+ if (seq_in_sync) {
+ selftest.sgpio_rx = PASSED;
+ } else if (timeout) {
+ selftest.sgpio_rx = TIMEOUT;
+ } else {
+ selftest.sgpio_rx = FAILED;
+ }
+ if (selftest.sgpio_rx != PASSED) {
+ selftest.report.pass = false;
+ }
+
+ return selftest.sgpio_rx == PASSED;
+}
+
+static void measure_tone(int8_t* samples, size_t len, struct xcvr_measurements* results)
+{
+ results->zcs_i = 0;
+ results->zcs_q = 0;
+ results->max_mag_i = 0;
+ results->max_mag_q = 0;
+ results->avg_mag_sq_i = 0;
+ results->avg_mag_sq_q = 0;
+ uint8_t last_sign_i = 0;
+ uint8_t last_sign_q = 0;
+ for (size_t i = 0; i < len; i += 2) {
+ int8_t sample_i = samples[i];
+ int8_t sample_q = samples[i + 1];
+ uint8_t sign_i = sample_i < 0 ? 1 : 0;
+ uint8_t sign_q = sample_q < 0 ? 1 : 0;
+ results->zcs_i += sign_i ^ last_sign_i;
+ results->zcs_q += sign_q ^ last_sign_q;
+ last_sign_i = sign_i;
+ last_sign_q = sign_q;
+ uint8_t mag_i = sign_i ? -sample_i : sample_i;
+ uint8_t mag_q = sign_q ? -sample_q : sample_q;
+ if (mag_i > results->max_mag_i)
+ results->max_mag_i = mag_i;
+ if (mag_q > results->max_mag_q)
+ results->max_mag_q = mag_q;
+ results->avg_mag_sq_i += mag_i * mag_i;
+ results->avg_mag_sq_q += mag_q * mag_q;
+ }
+ results->avg_mag_sq_i /= (len / 2);
+ results->avg_mag_sq_q /= (len / 2);
+}
+
+static bool in_range(int value, int expected, int error)
+{
+ int max = expected * (100 + error) / 100;
+ int min = expected * (100 - error) / 100;
+ return (value > min) && (value < max);
+}
+
+bool fpga_if_xcvr_selftest()
+{
+ bool timeout = false;
+
+ // Skip if FPGA configuration failed or its SPI bus is not working.
+ if ((selftest.fpga_image_load != PASSED) || (selftest.fpga_spi != PASSED)) {
+ selftest.xcvr_loopback = SKIPPED;
+ return false;
+ }
+
+ const size_t num_samples = USB_BULK_BUFFER_SIZE / 2;
+
+ // Set common RX path and gateware settings for the measurements.
+ fpga_set_tx_nco_pstep(&fpga, 64); // NCO phase increment
+ fpga_set_tx_nco_enable(&fpga, true); // TX enable
+ rf_path_set_direction(&rf_path, RF_PATH_DIRECTION_RX_CALIBRATION);
+ max2831_set_lna_gain(&max283x, 16);
+ max2831_set_vga_gain(&max283x, 36);
+ max2831_set_frequency(&max283x, 2500000000);
+
+ // Capture 1: 4 Msps, tone at 0.5 MHz, narrowband filter OFF
+ sample_rate_frac_set(4000000 * 2, 1);
+ delay_us_at_mhz(1000, 204);
+ if (rx_samples(num_samples, 2000000) == -1) {
+ timeout = true;
+ }
+ measure_tone(
+ (int8_t*) usb_bulk_buffer,
+ num_samples,
+ &selftest.xcvr_measurements[0]);
+
+ // Capture 2: 4 Msps, tone at 0.5 MHz, narrowband filter ON
+ narrowband_filter_set(1);
+ delay_us_at_mhz(1000, 204);
+ if (rx_samples(num_samples, 2000000) == -1) {
+ timeout = true;
+ }
+ measure_tone(
+ (int8_t*) usb_bulk_buffer,
+ num_samples,
+ &selftest.xcvr_measurements[1]);
+
+ // Capture 3: 20 Msps, tone at 5 MHz, narrowband filter OFF
+ fpga_set_tx_nco_pstep(&fpga, 255);
+ sample_rate_frac_set(20000000 * 2, 1);
+ narrowband_filter_set(0);
+ delay_us_at_mhz(1000, 204);
+ if (rx_samples(num_samples, 2000000) == -1) {
+ timeout = true;
+ }
+ measure_tone(
+ (int8_t*) usb_bulk_buffer,
+ num_samples,
+ &selftest.xcvr_measurements[2]);
+
+ // Capture 4: 20 Msps, tone at 5 MHz, narrowband filter ON
+ narrowband_filter_set(1);
+ delay_us_at_mhz(1000, 204);
+ if (rx_samples(num_samples, 2000000) == -1) {
+ timeout = true;
+ }
+ measure_tone(
+ (int8_t*) usb_bulk_buffer,
+ num_samples,
+ &selftest.xcvr_measurements[3]);
+
+ // Restore default settings.
+ sample_rate_set(10000000);
+ rf_path_set_direction(&rf_path, RF_PATH_DIRECTION_OFF);
+ narrowband_filter_set(0);
+ fpga_init(&fpga);
+
+ if (timeout) {
+ selftest.xcvr_loopback = TIMEOUT;
+ selftest.report.pass = false;
+ return false;
+ }
+
+ unsigned int expected_zcs;
+ bool i_in_range;
+ bool q_in_range;
+ bool i_energy_in_range;
+ bool q_energy_in_range;
+
+ // Capture 0:
+ // Count zero crossings.
+ // N/2 samples/channel * 2 zcs/cycle / 16 samples/cycle = N/16 zcs/channel
+ expected_zcs = num_samples / 16;
+ i_in_range = in_range(selftest.xcvr_measurements[0].zcs_i, expected_zcs, 5);
+ q_in_range = in_range(selftest.xcvr_measurements[0].zcs_q, expected_zcs, 5);
+ i_energy_in_range = (selftest.xcvr_measurements[0].avg_mag_sq_i > 500) &&
+ (selftest.xcvr_measurements[0].avg_mag_sq_i < 2500);
+ q_energy_in_range = (selftest.xcvr_measurements[0].avg_mag_sq_q > 500) &&
+ (selftest.xcvr_measurements[0].avg_mag_sq_q < 2500);
+ bool capture_0_test =
+ i_in_range && q_in_range && i_energy_in_range && q_energy_in_range;
+
+ // Capture 1:
+ // Count zero crossings.
+ expected_zcs = num_samples / 16;
+ i_in_range = in_range(selftest.xcvr_measurements[1].zcs_i, expected_zcs, 5);
+ q_in_range = in_range(selftest.xcvr_measurements[1].zcs_q, expected_zcs, 5);
+ i_energy_in_range = (selftest.xcvr_measurements[1].avg_mag_sq_i > 500) &&
+ (selftest.xcvr_measurements[1].avg_mag_sq_i < 2500);
+ q_energy_in_range = (selftest.xcvr_measurements[1].avg_mag_sq_q > 500) &&
+ (selftest.xcvr_measurements[1].avg_mag_sq_q < 2500);
+ bool capture_1_test =
+ i_in_range && q_in_range && i_energy_in_range && q_energy_in_range;
+
+ // Capture 2:
+ // Count zero crossings.
+ expected_zcs = num_samples / 4;
+ i_in_range = in_range(selftest.xcvr_measurements[2].zcs_i, expected_zcs, 5);
+ q_in_range = in_range(selftest.xcvr_measurements[2].zcs_q, expected_zcs, 5);
+ i_energy_in_range = (selftest.xcvr_measurements[2].avg_mag_sq_i > 400) &&
+ (selftest.xcvr_measurements[2].avg_mag_sq_i < 2000);
+ q_energy_in_range = (selftest.xcvr_measurements[2].avg_mag_sq_q > 400) &&
+ (selftest.xcvr_measurements[2].avg_mag_sq_q < 2000);
+ bool capture_2_test =
+ i_in_range && q_in_range && i_energy_in_range && q_energy_in_range;
+
+ // Capture 3:
+ i_energy_in_range = (selftest.xcvr_measurements[3].avg_mag_sq_i < 100);
+ q_energy_in_range = (selftest.xcvr_measurements[3].avg_mag_sq_q < 100);
+ bool capture_3_test = i_energy_in_range && q_energy_in_range;
+
+ // Update selftest result.
+ selftest.xcvr_loopback =
+ (capture_0_test && capture_1_test && capture_2_test && capture_3_test) ?
+ PASSED :
+ FAILED;
+ if (selftest.xcvr_loopback != PASSED) {
+ selftest.report.pass = false;
+ }
+
+ return selftest.xcvr_loopback == PASSED;
+}
diff --git a/firmware/common/hackrf_core.c b/firmware/common/hackrf_core.c
index cedff0f3..5d59da00 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,6 +36,7 @@
#include "i2c_bus.h"
#include "i2c_lpc.h"
#include "cpld_jtag.h"
+#include "ice40_spi.h"
#include "platform_detect.h"
#include "clkin.h"
#include
@@ -41,7 +44,7 @@
#include
#include
-#ifdef HACKRF_ONE
+#if (defined HACKRF_ONE || defined PRALINE)
#include "portapack.h"
#endif
@@ -55,16 +58,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 +97,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 +137,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 +161,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 */
-static struct gpio_t gpio_hw_sync_enable = GPIO(5, 12);
+#ifndef PRALINE
+static struct gpio_t gpio_trigger_enable = GPIO(5, 12);
+#endif
static struct gpio_t gpio_q_invert = GPIO(0, 13);
/* HackRF One r9 */
@@ -141,7 +179,20 @@ static struct gpio_t gpio_q_invert = GPIO(0, 13);
static struct gpio_t gpio_h1r9_rx = GPIO(0, 7);
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);
+static struct gpio_t gpio_h1r9_trigger_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
@@ -172,6 +223,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 +276,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 +293,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 +323,68 @@ w25q80bv_driver_t spi_flash = {
sgpio_config_t sgpio_config = {
.gpio_q_invert = &gpio_q_invert,
- .gpio_hw_sync_enable = &gpio_hw_sync_enable,
+#ifndef PRALINE
+ .gpio_trigger_enable = &gpio_trigger_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,
+};
+
+fpga_driver_t fpga = {
+ .bus = &ice40,
+};
+#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_enable = false,
+#ifdef PRALINE
+ .resampling_n = 0,
+ .shift = FPGA_QUARTER_SHIFT_MODE_NONE,
+#endif
+ },
+ .clock_source = CLOCK_SOURCE_HACKRF,
+ },
+};
+
rf_path_t rf_path = {
.switchctrl = 0,
#ifdef HACKRF_ONE
@@ -275,14 +415,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 +555,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 +576,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 +640,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 +673,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.
@@ -613,73 +763,6 @@ void cpu_clock_init(void)
/* use IRC as clock source for APB3 */
CGU_BASE_APB3_CLK = CGU_BASE_APB3_CLK_CLK_SEL(CGU_SRC_IRC);
- i2c_bus_start(clock_gen.bus, &i2c_config_si5351c_fast_clock);
-
- si5351c_init(&clock_gen);
- si5351c_disable_all_outputs(&clock_gen);
- si5351c_disable_oeb_pin_control(&clock_gen);
- si5351c_power_down_all_clocks(&clock_gen);
- si5351c_set_crystal_configuration(&clock_gen);
- si5351c_enable_xo_and_ms_fanout(&clock_gen);
- si5351c_configure_pll_sources(&clock_gen);
- si5351c_configure_pll_multisynth(&clock_gen);
-
- /*
- * Clocks on HackRF One r9:
- * CLK0 -> MAX5864/CPLD/SGPIO (sample clocks)
- * CLK1 -> RFFC5072/MAX2839
- * CLK2 -> External Clock Output/LPC43xx (power down at boot)
- *
- * Clocks on other platforms:
- * CLK0 -> MAX5864/CPLD
- * CLK1 -> CPLD
- * CLK2 -> SGPIO
- * CLK3 -> External Clock Output (power down at boot)
- * CLK4 -> RFFC5072 (MAX2837 on rad1o)
- * CLK5 -> MAX2837 (MAX2871 on rad1o)
- * CLK6 -> none
- * CLK7 -> LPC43xx (uses a 12MHz crystal by default)
- */
-
- if (detected_platform() == BOARD_ID_HACKRF1_R9) {
- /* MS0/CLK0 is the reference for both RFFC5071 and MAX2839. */
- si5351c_configure_multisynth(
- &clock_gen,
- 0,
- 20 * 128 - 512,
- 0,
- 1,
- 0); /* 800/20 = 40MHz */
- } else {
- /* MS4/CLK4 is the source for the RFFC5071 mixer (MAX2837 on rad1o). */
- si5351c_configure_multisynth(
- &clock_gen,
- 4,
- 20 * 128 - 512,
- 0,
- 1,
- 0); /* 800/20 = 40MHz */
- /* MS5/CLK5 is the source for the MAX2837 clock input (MAX2871 on rad1o). */
- si5351c_configure_multisynth(
- &clock_gen,
- 5,
- 20 * 128 - 512,
- 0,
- 1,
- 0); /* 800/20 = 40MHz */
- }
-
- /* MS6/CLK6 is unused. */
- /* MS7/CLK7 is unused. */
-
- /* Set to 10 MHz, the common rate between Jawbreaker and HackRF One. */
- sample_rate_set(10000000);
-
- si5351c_set_clock_source(&clock_gen, PLL_SOURCE_XTAL);
- // soft reset
- si5351c_reset_pll(&clock_gen);
- si5351c_enable_clock_outputs(&clock_gen);
-
//FIXME disable I2C
/* Kick I2C0 down to 400kHz when we switch over to APB1 clock = 204MHz */
i2c_bus_start(clock_gen.bus, &i2c_config_si5351c_fast_clock);
@@ -744,7 +827,7 @@ 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)
+#if (defined JAWBREAKER || defined HACKRF_ONE || defined PRALINE)
/* Disable unused clocks */
/* Start with PLLs */
CGU_PLL0AUDIO_CTRL = CGU_PLL0AUDIO_CTRL_PD(1);
@@ -777,7 +860,7 @@ void cpu_clock_init(void)
CCU1_CLK_APB1_CAN1_CFG = 0;
CCU1_CLK_APB1_I2S_CFG = 0;
CCU1_CLK_APB1_MOTOCONPWM_CFG = 0;
- CCU1_CLK_APB3_ADC0_CFG = 0;
+ //CCU1_CLK_APB3_ADC0_CFG = 0;
CCU1_CLK_APB3_ADC1_CFG = 0;
CCU1_CLK_APB3_CAN0_CFG = 0;
CCU1_CLK_APB3_DAC_CFG = 0;
@@ -813,9 +896,97 @@ void cpu_clock_init(void)
#endif
}
+void clock_gen_init(void)
+{
+ i2c_bus_start(clock_gen.bus, &i2c_config_si5351c_fast_clock);
+
+ si5351c_init(&clock_gen);
+ si5351c_disable_all_outputs(&clock_gen);
+ si5351c_disable_oeb_pin_control(&clock_gen);
+ si5351c_power_down_all_clocks(&clock_gen);
+ si5351c_set_crystal_configuration(&clock_gen);
+ si5351c_enable_xo_and_ms_fanout(&clock_gen);
+ si5351c_configure_pll_sources(&clock_gen);
+ si5351c_configure_pll_multisynth(&clock_gen);
+
+ /*
+ * Clocks on HackRF One r9:
+ * CLK0 -> MAX5864/CPLD/SGPIO (sample clocks)
+ * CLK1 -> RFFC5072/MAX2839
+ * CLK2 -> External Clock Output/LPC43xx (power down at boot)
+ *
+ * Clocks on other platforms:
+ * CLK0 -> MAX5864/CPLD
+ * CLK1 -> CPLD
+ * CLK2 -> SGPIO
+ * CLK3 -> External Clock Output (power down at boot)
+ * CLK4 -> RFFC5072 (MAX2837 on rad1o)
+ * 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) {
+ /* MS0/CLK0 is the reference for both RFFC5071 and MAX2839. */
+ si5351c_configure_multisynth(
+ &clock_gen,
+ 0,
+ 20 * 128 - 512,
+ 0,
+ 1,
+ 0); /* 800/20 = 40MHz */
+ } else {
+ /* MS4/CLK4 is the source for the RFFC5071 mixer (MAX2837 on rad1o). */
+ si5351c_configure_multisynth(
+ &clock_gen,
+ 4,
+ 20 * 128 - 512,
+ 0,
+ 1,
+ 0); /* 800/20 = 40MHz */
+ /* MS5/CLK5 is the source for the MAX2837 clock input (MAX2871 on rad1o). */
+ si5351c_configure_multisynth(
+ &clock_gen,
+ 5,
+ 20 * 128 - 512,
+ 0,
+ 1,
+ 0); /* 800/20 = 40MHz */
+ }
+
+ /* MS6/CLK6 is unused. */
+ /* MS7/CLK7 is unused. */
+
+ /* Set to 10 MHz, the common rate between Jawbreaker and HackRF One. */
+ sample_rate_set(10000000);
+
+ si5351c_set_clock_source(&clock_gen, PLL_SOURCE_XTAL);
+ // soft reset
+ si5351c_reset_pll(&clock_gen);
+ si5351c_enable_clock_outputs(&clock_gen);
+}
+
+void clock_gen_shutdown(void)
+{
+ i2c_bus_start(clock_gen.bus, &i2c_config_si5351c_fast_clock);
+ si5351c_disable_all_outputs(&clock_gen);
+ si5351c_disable_oeb_pin_control(&clock_gen);
+ si5351c_power_down_all_clocks(&clock_gen);
+}
+
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 +999,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 +1018,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,7 +1033,14 @@ void ssp1_set_mode_max5864(void)
spi_bus_start(max5864.bus, &ssp_config_max5864);
}
-void pin_setup(void)
+#ifdef PRALINE
+void ssp1_set_mode_ice40(void)
+{
+ spi_bus_start(&spi_bus_ssp1, &ssp_config_ice40_fpga);
+}
+#endif
+
+void pin_shutdown(void)
{
/* Configure all GPIO as Input (safe state) */
gpio_init();
@@ -879,14 +1059,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 +1077,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 +1087,35 @@ void pin_setup(void)
scu_pinmux(SCU_PINMUX_USB_LED1, SCU_CONF_FUNCTION3);
#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
+ 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);
}
@@ -945,22 +1134,84 @@ void pin_setup(void)
scu_pinmux(SCU_PINMUX_GPIO3_10, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
scu_pinmux(SCU_PINMUX_GPIO3_11, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
+#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);
+ 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);
+
+ 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);
+ gpio_clear(&gpio_fpga_cfg_spi_cs);
+ gpio_output(&gpio_fpga_cfg_spi_cs);
+ gpio_clear(&gpio_fpga_cfg_creset);
+ gpio_output(&gpio_fpga_cfg_creset);
+ gpio_input(&gpio_fpga_cfg_cdone);
#endif
/* enable input on SCL and SDA pins */
SCU_SFSI2C0 = SCU_I2C0_NOMINAL;
+}
- spi_bus_start(&spi_bus_ssp1, &ssp_config_max283x);
+/* Run after pin_shutdown() and prior to enabling power supplies. */
+void pin_setup(void)
+{
+ 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]);
+#if (defined RAD1O || defined PRALINE)
+ gpio_output(&gpio_led[3]);
+#endif
+
+ ssp1_set_mode_max283x();
mixer_bus_setup(&mixer);
#ifdef HACKRF_ONE
if (detected_platform() == BOARD_ID_HACKRF1_R9) {
rf_path.gpio_rx = &gpio_h1r9_rx;
- sgpio_config.gpio_hw_sync_enable = &gpio_h1r9_hw_sync_enable;
+ sgpio_config.gpio_trigger_enable = &gpio_h1r9_trigger_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 +1220,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 +1255,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 +1291,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 +1321,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 +1341,7 @@ void led_off(const led_t led)
{
gpio_clear(&gpio_led[led]);
}
+#endif
void led_toggle(const led_t led)
{
@@ -1051,17 +1351,25 @@ 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)
+void trigger_enable(const bool enable)
{
- gpio_write(sgpio_config.gpio_hw_sync_enable, hw_sync_mode == 1);
+#ifndef PRALINE
+ gpio_write(sgpio_config.gpio_trigger_enable, enable);
+#else
+ fpga_set_trigger_enable(&fpga, enable);
+#endif
}
void halt_and_flash(const uint32_t duration)
@@ -1078,3 +1386,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..301cb7aa 100644
--- a/firmware/common/hackrf_core.h
+++ b/firmware/common/hackrf_core.h
@@ -34,13 +34,17 @@ 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"
+#include "fpga.h"
/*
* SCU PinMux
@@ -53,8 +57,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 +94,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 +110,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_TRIGGER_EN (P4_8) /* GPIO5[12] on P4_8 */
/* MAX2837 GPIO (XCVR_CTL) PinMux */
#ifdef RAD1O
@@ -119,13 +188,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 +229,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 +263,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 +306,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] */
@@ -240,27 +387,7 @@ extern "C" {
#define SCU_H1R9_NO_ANT_PWR (P4_4) /* GPIO2[4] on P4_4 */
#define SCU_H1R9_EN1V8 (P5_0) /* GPIO2[9] on P5_0 */
#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;
+#define SCU_H1R9_TRIGGER_EN (P2_5) /* GPIO5[5] on P2_5 */
void delay(uint32_t duration);
void delay_us_at_mhz(uint32_t us, uint32_t mhz);
@@ -271,31 +398,52 @@ 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;
+extern fpga_driver_t fpga;
+
+#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;
void cpu_clock_init(void);
+void clock_gen_init(void);
+void clock_gen_shutdown(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_shutdown(void);
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
@@ -312,10 +460,40 @@ void led_off(const led_t led);
void led_toggle(const led_t led);
void set_leds(const uint8_t state);
-void hw_sync_enable(const hw_sync_mode_t hw_sync_mode);
+void trigger_enable(const bool enable);
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..6f641f54
--- /dev/null
+++ b/firmware/common/max2831.c
@@ -0,0 +1,419 @@
+/*
+ * 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"
+#include "adc.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);
+}
+
+/*
+ * 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_30_KHZ);
+ 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);
+
+ /* Read RSSI with ADC. */
+ uint16_t rssi_1 = selftest.max2831_mux_rssi_1 = adc_read(1);
+
+ /* Switch to temperature sensor. */
+ set_MAX2831_RSSI_MUX(drv, MAX2831_RSSI_MUX_TEMP);
+ max2831_regs_commit(drv);
+
+ /* Read temperature. */
+ uint16_t temp = selftest.max2831_mux_temp = adc_read(1);
+
+ /* Switch back to RSSI. */
+ set_MAX2831_RSSI_MUX(drv, MAX2831_RSSI_MUX_RSSI);
+ max2831_regs_commit(drv);
+
+ /* Read RSSI again. */
+ uint16_t rssi_2 = selftest.max2831_mux_rssi_2 = adc_read(1);
+
+ /* If the ADC results are as expected, we know our writes are working. */
+ bool rssi_1_good = (rssi_1 < 10);
+ bool rssi_2_good = (rssi_2 < 10);
+ bool temp_good = (temp > 100) && (temp < 500); // -40 to +85C
+
+ selftest.max2831_mux_test_ok = rssi_1_good & rssi_2_good & temp_good;
+
+ if (!selftest.max2831_mux_test_ok) {
+ selftest.report.pass = false;
+ }
+}
+
+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 },
+};
+
+/* measured -0.5 dB complex baseband bandwidth for each register setting */
+static const max2831_ft_t max2831_tx_ft[] = {
+ { 11900000, MAX2831_TX_LPF_8M },
+ { 15800000, MAX2831_TX_LPF_11M },
+ { 23600000, MAX2831_TX_LPF_16_5M },
+ { 31300000, 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;
+}
+
+void max2831_set_rx_hpf_frequency(max2831_driver_t* const drv, const max2831_rx_hpf_freq_t freq)
+{
+ /**
+ * Frequency RXHP RX_HPF_SEL (D13:D12)
+ *
+ * 100 Hz low 00
+ * 4 kHz low x1
+ * 30 kHz low 10
+ * 600 kHz high xx
+ */
+ switch (freq) {
+ case MAX2831_RX_HPF_100_HZ:
+ case MAX2831_RX_HPF_4_KHZ:
+ case MAX2831_RX_HPF_30_KHZ:
+ set_MAX2831_RX_HPF_SEL(drv, freq);
+ max2831_reg_commit(drv, 7);
+ gpio_clear(drv->gpio_rxhp);
+ break;
+ case MAX2831_RX_HPF_600_KHZ:
+ gpio_set(drv->gpio_rxhp);
+ break;
+ }
+}
diff --git a/firmware/common/max2831.h b/firmware/common/max2831.h
new file mode 100644
index 00000000..033b590e
--- /dev/null
+++ b/firmware/common/max2831.h
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+typedef enum {
+ MAX2831_RX_HPF_100_HZ = 0,
+ MAX2831_RX_HPF_4_KHZ = 1,
+ MAX2831_RX_HPF_30_KHZ = 2,
+ MAX2831_RX_HPF_600_KHZ = 3,
+} max2831_rx_hpf_freq_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);
+
+/* Set receiver high-pass filter corner frequency in Hz */
+extern void max2831_set_rx_hpf_frequency(
+ max2831_driver_t* const drv,
+ const max2831_rx_hpf_freq_t freq);
+
+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..ec00a3b8 100644
--- a/firmware/common/platform_detect.c
+++ b/firmware/common/platform_detect.c
@@ -23,60 +23,47 @@
#include "firmware_info.h"
#include "gpio_lpc.h"
#include "hackrf_core.h"
+#include "adc.h"
#include
-#include
static board_id_t platform = BOARD_ID_UNDETECTED;
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);
static struct gpio_t gpio_led2 = GPIO(2, 2);
static struct gpio_t gpio_led3 = GPIO(2, 8);
-/*
- * Return 10-bit ADC result.
- */
-uint16_t adc_read(uint8_t pin)
-{
- pin &= 0x7;
- uint8_t pin_mask = (1 << pin);
- ADC0_CR = ADC_CR_SEL(pin_mask) | ADC_CR_CLKDIV(45) | ADC_CR_PDN | ADC_CR_START(1);
- while (!(ADC0_GDR & ADC_DR_DONE) || (((ADC0_GDR >> 24) & 0x7) != pin)) {}
- return (ADC0_GDR >> 6) & 0x03FF;
-}
-
-void adc_off(void)
-{
- ADC0_CR = 0;
-}
-
/*
* Starting with r6, HackRF One has pin straps on ADC pins that indicate
* hardware revision. Those pins were unconnected prior to r6, so we test for
@@ -109,7 +96,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 +131,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 +186,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 +203,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 +240,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 +263,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 +273,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..d1441847
--- /dev/null
+++ b/firmware/common/radio.c
@@ -0,0 +1,547 @@
+/*
+ * 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;
+
+ /*
+ * Store the sample rate rounded to the nearest Hz for convenience.
+ */
+ if (sample_rate.div == 0) {
+ return RADIO_ERR_INVALID_PARAM;
+ }
+ sample_rate.hz = (sample_rate.num + (sample_rate.div >> 1)) / sample_rate.div;
+
+ if (config->mode == TRANSCEIVER_MODE_OFF) {
+ config->sample_rate[element] = sample_rate;
+ return RADIO_OK;
+ }
+
+#ifdef PRALINE
+ #define MAX_AFE_RATE 40000000
+ #define MAX_N 5
+ uint8_t n = 0; // resampling ratio is 2**n
+ if ((config->mode == TRANSCEIVER_MODE_RX) ||
+ (config->mode == TRANSCEIVER_MODE_RX_SWEEP)) {
+ n = 1;
+ uint32_t afe_rate_x2 = 2 * sample_rate.hz;
+ while ((afe_rate_x2 <= MAX_AFE_RATE) && (n < MAX_N)) {
+ afe_rate_x2 <<= 1;
+ n++;
+ }
+ fpga_set_rx_decimation_ratio(&fpga, n);
+ }
+ config->resampling_n = n;
+ bool ok = sample_rate_frac_set(sample_rate.num << n, sample_rate.div);
+ if (ok) {
+ config->sample_rate[element] = sample_rate;
+ radio_channel_t* channel = &radio->channel[chan_id];
+ radio_frequency_t frequency =
+ radio_get_frequency(radio, channel->id, RADIO_FREQUENCY_RF);
+ ok = radio_set_frequency(
+ radio,
+ channel->id,
+ RADIO_FREQUENCY_RF,
+ frequency);
+ }
+#else
+ bool ok = sample_rate_frac_set(sample_rate.num, sample_rate.div);
+#endif
+ 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;
+ }
+
+#ifndef PRALINE
+ max283x_set_lpf_bandwidth(&max283x, filter.hz);
+#else
+ uint32_t lpf_bandwidth =
+ (config->sample_rate[RADIO_SAMPLE_RATE_CLOCKGEN].hz * 3) / 8;
+ uint32_t offset = 0;
+ if (config->shift != FPGA_QUARTER_SHIFT_MODE_NONE) {
+ offset = (config->sample_rate[RADIO_SAMPLE_RATE_CLOCKGEN].hz
+ << config->resampling_n) /
+ 8;
+ }
+ lpf_bandwidth += offset * 2;
+ max2831_set_lpf_bandwidth(&max283x, lpf_bandwidth);
+#endif
+
+ config->filter[element] = filter;
+ 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);
+#ifdef PRALINE
+ if (ok) {
+ fpga_set_rx_quarter_shift_mode(
+ &fpga,
+ FPGA_QUARTER_SHIFT_MODE_NONE);
+ config->shift = FPGA_QUARTER_SHIFT_MODE_NONE;
+ radio_channel_t* channel = &radio->channel[chan_id];
+ radio_filter_t filter = radio_get_filter(
+ radio,
+ channel->id,
+ RADIO_FILTER_BASEBAND);
+ ok = radio_set_filter(
+ radio,
+ channel->id,
+ RADIO_FILTER_BASEBAND,
+ filter);
+ }
+#endif
+ if (!ok) {
+ return RADIO_ERR_INVALID_PARAM;
+ }
+
+ config->frequency[element] = frequency;
+ return RADIO_OK;
+ }
+
+ // auto-tune
+ bool ok;
+#ifndef PRALINE
+ switch (config->mode) {
+ case TRANSCEIVER_MODE_RX:
+ case TRANSCEIVER_MODE_RX_SWEEP:
+ case TRANSCEIVER_MODE_TX:
+ ok = set_freq(frequency.hz);
+ break;
+ default:
+ return RADIO_ERR_INVALID_CONFIG;
+ }
+#else
+ const tune_config_t* tune_config;
+ switch (config->mode) {
+ case TRANSCEIVER_MODE_RX:
+ tune_config = praline_tune_config_rx;
+ break;
+ case TRANSCEIVER_MODE_RX_SWEEP:
+ tune_config = praline_tune_config_rx_sweep;
+ break;
+ case TRANSCEIVER_MODE_TX:
+ tune_config = praline_tune_config_tx;
+ break;
+ default:
+ return RADIO_ERR_INVALID_CONFIG;
+ }
+ bool found = false;
+ for (; (tune_config->rf_range_end_mhz != 0) || (tune_config->if_mhz != 0);
+ tune_config++) {
+ if ((frequency.hz == 0) ||
+ (tune_config->rf_range_end_mhz > (frequency.hz / FREQ_ONE_MHZ))) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return RADIO_ERR_INVALID_PARAM;
+ }
+
+ fpga_set_rx_quarter_shift_mode(&fpga, tune_config->shift);
+ config->shift = tune_config->shift;
+ uint32_t offset = (config->sample_rate[RADIO_SAMPLE_RATE_CLOCKGEN].hz
+ << config->resampling_n) /
+ 8;
+ ok = tuning_set_frequency(tune_config, frequency.hz, offset);
+ if (ok) {
+ radio_channel_t* channel = &radio->channel[chan_id];
+ radio_filter_t filter =
+ radio_get_filter(radio, channel->id, RADIO_FILTER_BASEBAND);
+ ok = radio_set_filter(radio, channel->id, RADIO_FILTER_BASEBAND, filter);
+ }
+#endif
+ if (!ok) {
+ return RADIO_ERR_INVALID_PARAM;
+ }
+
+ 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_enable(radio_t* radio, radio_chan_id chan_id, bool enable)
+{
+ radio_config_t* config = &radio->channel[chan_id].config;
+
+ config->trigger_enable = enable;
+ return RADIO_OK;
+}
+
+bool radio_get_trigger_enable(radio_t* radio, radio_chan_id chan_id)
+{
+ return radio->channel[chan_id].config.trigger_enable;
+}
+
+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;
+ }
+
+ /*
+ * Because of offset tuning on Praline, the sample rate can affect the
+ * tuning configuration, so radio_set_sample_rate() calls
+ * radio_set_frequency(). Also because of offset tuning, the tuning
+ * configuration can affect the baseband filter bandwidth (in addition
+ * to the filter bandwidth being automatically based on the sample
+ * rate), so radio_set_frequency() calls radio_set_filter().
+ */
+#ifndef PRALINE
+ // 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;
+ }
+
+ // 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;
+ }
+#endif
+
+ // 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;
+ }
+
+ // 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..60e2cc4d
--- /dev/null
+++ b/firmware/common/radio.h
@@ -0,0 +1,265 @@
+/*
+ * 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"
+#include "fpga.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;
+ uint32_t 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 {
+ 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
+ bool trigger_enable;
+
+ // currently active transceiver mode
+ transceiver_mode_t mode;
+
+#ifdef PRALINE
+ // resampling ratio is 2**n
+ uint8_t resampling_n;
+
+ // quarter-rate shift configuration for offset tuning
+ fpga_quarter_shift_mode_t shift;
+#endif
+
+} 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_enable(radio_t* radio, radio_chan_id chan_id, bool enable);
+bool radio_get_trigger_enable(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..161e74ec 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 */
@@ -69,7 +71,7 @@ static const uint16_t rffc5071_regs_default[RFFC5071_NUM_REGS] = {
0x0000, /* 1A */
0x0000, /* 1B */
0xc840, /* 1C */
- 0x1000, /* 1D */
+ 0x0000, /* 1D, readsel = 0b0000 */
0x0005,
/* 1E */};
@@ -81,6 +83,11 @@ void rffc5071_init(rffc5071_driver_t* const drv)
/* Write default register values to chip. */
rffc5071_regs_commit(drv);
+
+ selftest.mixer_id = rffc5071_reg_read(drv, RFFC5071_READBACK_REG);
+ if ((selftest.mixer_id >> 3) != 4416) {
+ selftest.report.pass = false;
+ }
}
/*
@@ -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,11 @@ void rffc5071_setup(rffc5071_driver_t* const drv)
/* GPOs are active at all times */
set_RFFC5071_GATE(drv, 1);
+#ifdef PRALINE
+ /* Enable GPO Lock output signal */
+ set_RFFC5071_LOCK(drv, 1);
+#endif
+
rffc5071_regs_commit(drv);
}
@@ -231,19 +249,22 @@ uint64_t rffc5071_config_synth(rffc5071_driver_t* const drv, uint64_t lo)
fvco = lo << n_lo;
- /* higher divider and charge pump current required above
- * 3.2GHz. Programming guide says these values (fbkdiv, n,
- * maybe pump?) can be changed back after enable in order to
- * improve phase noise, since the VCO will already be stable
- * and will be unaffected. */
+ /*
+ * Higher charge pump leakage setting is required above 3.2 GHz.
+ */
if (fvco > (3200 * FREQ_ONE_MHZ)) {
- fbkdivlog = 2;
set_RFFC5071_PLLCPL(drv, 3);
} else {
- fbkdivlog = 1;
set_RFFC5071_PLLCPL(drv, 2);
}
+ /*
+ * Supposedly fbkdivlog can be set to 1 when VCO is below 3.2 GHz, but
+ * this has resulted in tuning instability on some boards, most evident
+ * in RX sweep mode.
+ */
+ fbkdivlog = 2;
+
uint64_t tmp_n = (fvco << (24ULL - fbkdivlog)) / REF_FREQ;
/* Round to nearest step = ref_MHz / 2**s. For s=6, step=625000 Hz */
@@ -292,3 +313,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..0e57d626
--- /dev/null
+++ b/firmware/common/selftest.h
@@ -0,0 +1,75 @@
+/*
+ * 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
+
+enum {
+ FAILED = 0,
+ PASSED = 1,
+ SKIPPED = 2,
+ TIMEOUT = 3,
+};
+
+typedef uint8_t test_result_t;
+
+typedef struct {
+ uint16_t mixer_id;
+#ifdef PRALINE
+ uint16_t max2831_mux_rssi_1;
+ uint16_t max2831_mux_temp;
+ uint16_t max2831_mux_rssi_2;
+ bool max2831_mux_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;
+#ifdef PRALINE
+ test_result_t fpga_image_load;
+ test_result_t fpga_spi;
+ test_result_t sgpio_rx;
+ test_result_t xcvr_loopback;
+
+ struct xcvr_measurements {
+ uint32_t zcs_i;
+ uint32_t zcs_q;
+ uint8_t max_mag_i;
+ uint8_t max_mag_q;
+ uint32_t avg_mag_sq_i;
+ uint32_t avg_mag_sq_q;
+ } xcvr_measurements[4];
+#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..50b98d77 100644
--- a/firmware/common/sgpio.c
+++ b/firmware/common/sgpio.c
@@ -34,37 +34,39 @@ 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(
- SCU_H1R9_HW_SYNC_EN,
+ SCU_H1R9_TRIGGER_EN,
SCU_GPIO_FAST | SCU_CONF_FUNCTION4); /* GPIO5[5] */
} else {
scu_pinmux(
- SCU_HW_SYNC_EN,
+ SCU_TRIGGER_EN,
SCU_GPIO_FAST | SCU_CONF_FUNCTION4); /* GPIO5[12] */
}
sgpio_cpld_set_mixer_invert(config, 0);
- hw_sync_enable(0);
-
gpio_output(config->gpio_q_invert);
- gpio_output(config->gpio_hw_sync_enable);
+
+#ifndef PRALINE
+ trigger_enable(false);
+ gpio_output(config->gpio_trigger_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..0d8e9f0f 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;
- gpio_t gpio_hw_sync_enable;
+#ifndef PRALINE
+ gpio_t gpio_trigger_enable;
+#endif
bool slice_mode_multislice;
} sgpio_config_t;
diff --git a/firmware/common/si5351c.c b/firmware/common/si5351c.c
index af858875..d9686ea1 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,26 @@ 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;
+ }
+
+ /* Do the same with them flipped back. */
+ 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..dd21d028
--- /dev/null
+++ b/firmware/common/tune_config.h
@@ -0,0 +1,395 @@
+/*
+ * 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__
+
+#ifdef PRALINE
+ #include "fpga.h"
+
+typedef struct {
+ uint16_t rf_range_end_mhz;
+ uint16_t if_mhz;
+ bool high_lo;
+ fpga_quarter_shift_mode_t shift;
+} tune_config_t;
+
+// clang-format off
+/* tuning table optimized for TX */
+static const tune_config_t praline_tune_config_tx[] = {
+ { 2100, 2375, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2105, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2115, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2130, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2150, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2160, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2175, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2190, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2195, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2210, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2248, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2265, 2525, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2300, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2320, 2525, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2580, 0, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3000, 2325, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3140, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3200, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3280, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3340, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3420, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3480, 2525, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3500, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3595, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3625, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3670, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3710, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3760, 2525, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3790, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3860, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3915, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4000, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4055, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4125, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4700, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4800, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5000, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5260, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5465, 2525, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5560, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5720, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5860, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5970, 2575, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6000, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6500, 2325, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6750, 2375, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6850, 2425, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6950, 2475, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 7000, 2525, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 7251, 2575, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 0, 0, false, 0 },
+};
+
+/* tuning table optimized for 20 Msps interleaved RX sweep mode */
+static const tune_config_t praline_tune_config_rx_sweep[] = {
+ { 140, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 424, 2570, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 557, 2520, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 593, 2380, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 776, 2360, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 846, 2570, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 926, 2500, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 1055, 2380, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 1175, 2360, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 1391, 2340, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 1529, 2570, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 1671, 2520, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 1979, 2380, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2150, 2330, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2160, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2170, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2179, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2184, 2520, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2187, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2194, 2530, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2203, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2212, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2222, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2231, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2233, 2530, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2237, 2520, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2241, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2245, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2250, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2252, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2258, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2261, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2266, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2271, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2275, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2280, 2500, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2284, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2285, 2530, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2289, 2510, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2293, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2294, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2298, 2520, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2301, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2302, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2307, 2530, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2308, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2312, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2316, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2317, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2320, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2580, 0, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2585, 2360, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2588, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2594, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2606, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2617, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2627, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2638, 2360, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2649, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2659, 2380, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2664, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2675, 2360, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2686, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2697, 2380, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2705, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2716, 2360, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2728, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2739, 2380, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2757, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2779, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2790, 2360, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2801, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2812, 2380, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2821, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2831, 2520, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2852, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2874, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2897, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2913, 2510, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2925, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2937, 2530, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2948, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2960, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2975, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 2988, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3002, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3014, 2360, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3027, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3041, 2500, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3052, 2510, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3064, 2520, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3082, 2500, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3107, 2520, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3132, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3157, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3170, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3192, 2500, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3216, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3270, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3319, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3341, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3370, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3400, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3430, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3464, 2520, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3491, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3519, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3553, 2510, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3595, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3638, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3665, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3685, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3726, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3790, 2370, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 3910, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4014, 2510, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4123, 2380, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4191, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4349, 2510, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4452, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4579, 2500, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4707, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4831, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4851, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4871, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4891, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4911, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4931, 2550, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 4951, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5044, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5065, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5174, 2330, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5285, 2380, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5449, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5574, 2510, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5717, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 5892, 2530, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6096, 2350, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6254, 2560, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6625, 2340, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6764, 2540, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 6930, 2530, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 7251, 2570, false, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 0, 0, false, 0 },
+};
+
+/* tuning table optimized for RX */
+static const tune_config_t praline_tune_config_rx[] = {
+ { 0, 2360, true, FPGA_QUARTER_SHIFT_MODE_NONE },
+ { 50, 2320, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 100, 2320, true, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 140, 2320, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 406, 2560, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 511, 2380, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 578, 2560, true, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 741, 2340, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 861, 2560, true, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 921, 2560, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 1049, 2340, true, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 1169, 2380, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 1360, 2340, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 1544, 2560, true, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 1675, 2560, true, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 1992, 2380, true, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2070, 2340, true, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2150, 2360, true, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2168, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2185, 2580, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2202, 2580, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2205, 2520, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2216, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2223, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2234, 2580, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2240, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2251, 2580, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2258, 2580, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2265, 2540, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2271, 2580, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2273, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2275, 2580, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2280, 2500, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2284, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2289, 2580, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2293, 2540, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2298, 2520, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2300, 2580, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2302, 2540, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2309, 2560, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2311, 2580, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2314, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2315, 2540, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2320, 2580, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2380, 0, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2440, 0, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2500, 0, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2580, 0, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2583, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2584, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2587, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2593, 2340, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2607, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2609, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2615, 2360, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2627, 2340, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2629, 2360, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2631, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2644, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2649, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2651, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2654, 2500, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2665, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2669, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2672, 2360, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2682, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2687, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2692, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2695, 2500, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2705, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2707, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2712, 2340, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2717, 2520, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2728, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2730, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2734, 2500, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2758, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2780, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2787, 2520, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2802, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2809, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2822, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2831, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2854, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2875, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2898, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2918, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2936, 2520, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2944, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 2959, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2976, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 2985, 2500, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 3003, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3009, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3027, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3034, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3050, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 3069, 2500, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3094, 2520, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3119, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3144, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3169, 2560, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 3180, 2500, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3204, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3232, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3292, 2340, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 3340, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 3369, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3399, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3429, 2380, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3464, 2500, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3489, 2520, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3512, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3551, 2500, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 3582, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3611, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3639, 2520, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3729, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 3817, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 3942, 2360, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 4049, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 4134, 2500, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 4194, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 4353, 2520, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 4449, 2360, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 4562, 2500, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 4672, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 4769, 2540, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 4849, 2560, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 4889, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 4929, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 4969, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 5009, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 5049, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 5092, 2360, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 5209, 2340, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 5298, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 5468, 2340, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 5582, 2520, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 5702, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 5888, 2520, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 6092, 2340, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 6240, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 6609, 2340, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 6752, 2380, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 6930, 2520, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 7000, 2560, false, FPGA_QUARTER_SHIFT_MODE_UP },
+ { 7070, 2560, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 7251, 2580, false, FPGA_QUARTER_SHIFT_MODE_DOWN },
+ { 0, 0, false, 0 },
+};
+// clang-format on
+#endif
+
+#endif /*__TUNE_CONFIG_H__*/
diff --git a/firmware/common/tuning.c b/firmware/common/tuning.c
index 08432ad9..e1a1d951 100644
--- a/firmware/common/tuning.c
+++ b/firmware/common/tuning.c
@@ -25,34 +25,52 @@
#include "hackrf_ui.h"
#include "hackrf_core.h"
#include "mixer.h"
+#include "max2831.h"
#include "max2837.h"
#include "max2839.h"
#include "sgpio.h"
#include "operacake.h"
#include "platform_detect.h"
-#define FREQ_ONE_MHZ (1000ULL * 1000)
+#ifndef PRALINE
-#define MIN_LP_FREQ_MHZ (0)
-#define MAX_LP_FREQ_MHZ (2170ULL)
+ #define MIN_LP_FREQ_MHZ (0)
+ #define MAX_LP_FREQ_MHZ (2170ULL)
-#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 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_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_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)
+ #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
+
+#ifndef PRALINE
static uint32_t max2837_freq_nominal_hz = 2560000000;
-uint64_t freq_cache = 100000000;
-
/*
* Set freq/tuning between 0MHz to 7250 MHz (less than 16bits really used)
* hz between 0 to 999999 Hz (not checked)
@@ -72,12 +90,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);
@@ -115,15 +133,76 @@ bool set_freq(const uint64_t freq)
}
max283x_set_mode(&max283x, prior_max283x_mode);
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;
}
+#else
+
+bool tuning_set_frequency(
+ const tune_config_t* cfg,
+ const uint64_t freq,
+ const uint32_t offset)
+{
+ 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;
+
+ uint64_t rf = freq;
+ if (cfg->shift == FPGA_QUARTER_SHIFT_MODE_DOWN) {
+ if (offset > rf) {
+ rf = offset - rf;
+ } else {
+ rf = rf - offset;
+ }
+ } else if (cfg->shift == FPGA_QUARTER_SHIFT_MODE_UP) {
+ rf = rf + offset;
+ }
+
+ 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, rf);
+ 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 + rf;
+ real_mixer_freq_hz = mixer_set_frequency(&mixer, mixer_freq_hz);
+ max2831_set_frequency(&max283x, real_mixer_freq_hz - rf);
+ sgpio_cpld_set_mixer_invert(&sgpio_config, 1);
+ } else {
+ mixer_freq_hz = FREQ_ONE_MHZ * cfg->if_mhz - rf;
+ real_mixer_freq_hz = mixer_set_frequency(&mixer, mixer_freq_hz);
+ max2831_set_frequency(&max283x, real_mixer_freq_hz + rf);
+ sgpio_cpld_set_mixer_invert(&sgpio_config, 0);
+ }
+ } else {
+ rf_path_set_filter(&rf_path, RF_PATH_FILTER_HIGH_PASS);
+ mixer_freq_hz = rf - FREQ_ONE_MHZ * cfg->if_mhz;
+ real_mixer_freq_hz = mixer_set_frequency(&mixer, mixer_freq_hz);
+ max2831_set_frequency(&max283x, rf - real_mixer_freq_hz);
+ sgpio_cpld_set_mixer_invert(&sgpio_config, 0);
+ }
+
+ max2831_set_mode(&max283x, prior_max2831_mode);
+ 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 +223,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..863fe6a0 100644
--- a/firmware/common/tuning.h
+++ b/firmware/common/tuning.h
@@ -25,14 +25,24 @@
#define __TUNING_H__
#include "rf_path.h"
+#include "tune_config.h"
#include
#include
+#define FREQ_ONE_MHZ (1000ULL * 1000)
+
bool set_freq(const uint64_t freq);
bool set_freq_explicit(
const uint64_t if_freq_hz,
const uint64_t lo_freq_hz,
const rf_path_filter_t path);
+#ifdef PRALINE
+bool tuning_set_frequency(
+ const tune_config_t* cfg,
+ const uint64_t freq,
+ const uint32_t offset);
+#endif
+
#endif /*__TUNING_H__*/
diff --git a/firmware/common/usb.c b/firmware/common/usb.c
index 3b8c8b46..9188da73 100644
--- a/firmware/common/usb.c
+++ b/firmware/common/usb.c
@@ -520,7 +520,7 @@ static void copy_setup(usb_setup_t* const dst, const volatile uint8_t* const src
dst->length_h = src[7];
}
-void usb_endpoint_init(const usb_endpoint_t* const endpoint)
+void usb_endpoint_init(const usb_endpoint_t* const endpoint, const bool enable_zlp)
{
usb_endpoint_flush(endpoint);
@@ -537,10 +537,15 @@ void usb_endpoint_init(const usb_endpoint_t* const endpoint)
// TODO: There are more capabilities to adjust based on the endpoint
// descriptor.
usb_queue_head_t* const qh = usb_queue_head(endpoint->address);
- qh->capabilities = USB_QH_CAPABILITIES_MULT(0) | USB_QH_CAPABILITIES_ZLT |
+ qh->capabilities = USB_QH_CAPABILITIES_MULT(0) |
USB_QH_CAPABILITIES_MPL(max_packet_size) |
((transfer_type == USB_TRANSFER_TYPE_CONTROL) ? USB_QH_CAPABILITIES_IOS :
0);
+ if (enable_zlp) {
+ qh->capabilities &= ~USB_QH_CAPABILITIES_ZLT;
+ } else {
+ qh->capabilities |= USB_QH_CAPABILITIES_ZLT;
+ }
qh->current_dtd_pointer = 0;
qh->next_dtd_pointer = USB_TD_NEXT_DTD_POINTER_TERMINATE;
qh->total_bytes = USB_TD_DTD_TOKEN_TOTAL_BYTES(0) | USB_TD_DTD_TOKEN_MULTO(0);
diff --git a/firmware/common/usb.h b/firmware/common/usb.h
index 64ea355d..3d089816 100644
--- a/firmware/common/usb.h
+++ b/firmware/common/usb.h
@@ -50,7 +50,7 @@ void usb_set_address_deferred(
usb_endpoint_t* usb_endpoint_from_address(const uint_fast8_t endpoint_address);
-void usb_endpoint_init(const usb_endpoint_t* const endpoint);
+void usb_endpoint_init(const usb_endpoint_t* const endpoint, const bool enable_zlp);
void usb_endpoint_stall(const usb_endpoint_t* const endpoint);
diff --git a/firmware/common/w25q80bv.c b/firmware/common/w25q80bv.c
index b29fa84a..fe09a2e8 100644
--- a/firmware/common/w25q80bv.c
+++ b/firmware/common/w25q80bv.c
@@ -67,7 +67,8 @@ void w25q80bv_setup(w25q80bv_driver_t* const drv)
do {
device_id = w25q80bv_get_device_id(drv);
} while (device_id != W25Q80BV_DEVICE_ID_RES &&
- device_id != W25Q16DV_DEVICE_ID_RES);
+ device_id != W25Q16DV_DEVICE_ID_RES &&
+ device_id != W25Q32JV_DEVICE_ID_RES);
}
uint8_t w25q80bv_get_status(w25q80bv_driver_t* const drv)
@@ -129,7 +130,8 @@ void w25q80bv_chip_erase(w25q80bv_driver_t* const drv)
do {
device_id = w25q80bv_get_device_id(drv);
} while (device_id != W25Q80BV_DEVICE_ID_RES &&
- device_id != W25Q16DV_DEVICE_ID_RES);
+ device_id != W25Q16DV_DEVICE_ID_RES &&
+ device_id != W25Q32JV_DEVICE_ID_RES);
w25q80bv_wait_while_busy(drv);
w25q80bv_write_enable(drv);
@@ -182,7 +184,8 @@ void w25q80bv_program(
do {
device_id = w25q80bv_get_device_id(drv);
} while (device_id != W25Q80BV_DEVICE_ID_RES &&
- device_id != W25Q16DV_DEVICE_ID_RES);
+ device_id != W25Q16DV_DEVICE_ID_RES &&
+ device_id != W25Q32JV_DEVICE_ID_RES);
/* do nothing if we would overflow the flash */
if ((len > drv->num_bytes) || (addr > drv->num_bytes) ||
diff --git a/firmware/common/w25q80bv.h b/firmware/common/w25q80bv.h
index c0d7cf10..55fefaad 100644
--- a/firmware/common/w25q80bv.h
+++ b/firmware/common/w25q80bv.h
@@ -29,6 +29,7 @@
#define W25Q80BV_DEVICE_ID_RES 0x13 /* Expected device_id for W25Q80BV */
#define W25Q16DV_DEVICE_ID_RES 0x14 /* Expected device_id for W25Q16DV */
+#define W25Q32JV_DEVICE_ID_RES 0x15 /* Expected device_id for W25Q32JV */
#include "spi_bus.h"
#include "gpio.h"
diff --git a/firmware/fpga/amaranth_future/fixed.py b/firmware/fpga/amaranth_future/fixed.py
new file mode 100644
index 00000000..81503f1f
--- /dev/null
+++ b/firmware/fpga/amaranth_future/fixed.py
@@ -0,0 +1,357 @@
+#
+# This file is part of HackRF.
+#
+# Copyright (c) 2025 Great Scott Gadgets
+# Copyright (c) 2024 S. Holzapfel
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# from https://github.com/apfaudio/tiliqua/blob/main/gateware/src/amaranth_future/fixed.py
+# which is also derived from https://github.com/amaranth-lang/amaranth/pull/1005
+# slightly modified
+
+from amaranth import hdl
+from amaranth.utils import bits_for
+
+from dsp.round import convergent_round
+
+__all__ = ["Shape", "SQ", "UQ", "Value", "Const"]
+
+class Shape(hdl.ShapeCastable):
+ def __init__(self, i_or_f_width, f_width = None, /, *, signed):
+ if f_width is None:
+ self.i_width, self.f_width = 0, i_or_f_width
+ else:
+ self.i_width, self.f_width = i_or_f_width, f_width
+
+ self.signed = bool(signed)
+
+ @staticmethod
+ def cast(shape, f_width=0):
+ if not isinstance(shape, hdl.Shape):
+ raise TypeError(f"Object {shape!r} cannot be converted to a fixed.Shape")
+
+ # i_width is what's left after subtracting f_width and sign bit, but can't be negative.
+ i_width = max(0, shape.width - shape.signed - f_width)
+
+ return Shape(i_width, f_width, signed = shape.signed)
+
+ def as_shape(self):
+ return hdl.Shape(self.signed + self.i_width + self.f_width, self.signed)
+
+ def __call__(self, target):
+ return Value(self, target)
+
+ def const(self, value):
+ if value is None:
+ value = 0
+ return Const(value, self)._target
+
+ def from_bits(self, raw):
+ c = Const(0, self)
+ c._value = raw
+ return c
+
+ def max(self):
+ c = Const(0, self)
+ c._value = c._max_value()
+ return c
+
+ def min(self):
+ c = Const(0, self)
+ c._value = c._min_value()
+ return c
+
+ def __repr__(self):
+ return f"fixed.Shape({self.i_width}, {self.f_width}, signed={self.signed})"
+
+
+class SQ(Shape):
+ def __init__(self, *args):
+ super().__init__(*args, signed = True)
+
+
+class UQ(Shape):
+ def __init__(self, *args):
+ super().__init__(*args, signed = False)
+
+
+class Value(hdl.ValueCastable):
+ def __init__(self, shape, target):
+ self._shape = shape
+ self._target = target
+
+ @staticmethod
+ def cast(value, f_width=0):
+ return Shape.cast(value.shape(), f_width)(value)
+
+ def round(self, f_width=0):
+ # If we're increasing precision, extend with more fractional bits.
+ if f_width > self.f_width:
+ return Shape(self.i_width, f_width, signed = self.signed)(hdl.Cat(hdl.Const(0, f_width - self.f_width), self.raw()))
+
+ # If we're reducing precision, perform convergent rounding (round to even).
+ elif f_width < self.f_width:
+ return Shape(self.i_width, f_width, signed = self.signed)( convergent_round(self.raw(), self.f_width - f_width) )
+
+ return self
+
+ def truncate(self, f_width=0):
+ return Shape(self.i_width, f_width, signed = self.signed)(self.raw()[self.f_width - f_width:])
+
+ @property
+ def i_width(self):
+ return self._shape.i_width
+
+ @property
+ def f_width(self):
+ return self._shape.f_width
+
+ @property
+ def signed(self):
+ return self._shape.signed
+
+ def as_value(self):
+ return self._target
+
+ def raw(self):
+ """
+ Adding an `s ( )` signedness wrapper in `as_value` when needed
+ breaks lib.wiring for some reason. In the future, raw() and
+ `as_value()` should be combined.
+ """
+ if self.signed:
+ return self._target.as_signed()
+ return self._target
+
+ def shape(self):
+ return self._shape
+
+ def eq(self, other):
+ # Regular values are assigned directly to the underlying value.
+ if isinstance(other, hdl.Value):
+ return self.raw().eq(other)
+
+ # int and float are cast to fixed.Const.
+ elif isinstance(other, int) or isinstance(other, float):
+ other = Const(other, self.shape())
+
+ # Other value types are unsupported.
+ elif not isinstance(other, Value):
+ raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
+
+ # Match precision.
+ other = other.round(self.f_width)
+
+ return self.raw().eq(other.raw())
+
+ def __mul__(self, other):
+ # Regular values are cast to fixed.Value
+ if isinstance(other, hdl.Value):
+ other = Value.cast(other)
+
+ # int are cast to fixed.Const
+ elif isinstance(other, int):
+ other = Const(other)
+
+ # Other value types are unsupported.
+ elif not isinstance(other, Value):
+ raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
+
+ return Value.cast(self.raw() * other.raw(), self.f_width + other.f_width)
+
+ def __rmul__(self, other):
+ return self.__mul__(other)
+
+ def __add__(self, other):
+ # Regular values are cast to fixed.Value
+ if isinstance(other, hdl.Value):
+ other = Value.cast(other)
+
+ # int are cast to fixed.Const
+ elif isinstance(other, int):
+ other = Const(other)
+
+ # Other value types are unsupported.
+ elif not isinstance(other, Value):
+ raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
+
+ f_width = max(self.f_width, other.f_width)
+
+ return Value.cast(self.round(f_width).raw() + other.round(f_width).raw(), f_width)
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ def __sub__(self, other):
+ # Regular values are cast to fixed.Value
+ if isinstance(other, hdl.Value):
+ other = Value.cast(other)
+
+ # int are cast to fixed.Const
+ elif isinstance(other, int):
+ other = Const(other)
+
+ # Other value types are unsupported.
+ elif not isinstance(other, Value):
+ raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
+
+ f_width = max(self.f_width, other.f_width)
+
+ return Value.cast(self.round(f_width).raw() - other.round(f_width).raw(), f_width)
+
+ def __rsub__(self, other):
+ return -self.__sub__(other)
+
+ def __pos__(self):
+ return self
+
+ def __neg__(self):
+ return Value.cast(-self.raw(), self.f_width)
+
+ def __abs__(self):
+ return Value.cast(abs(self.raw()), self.f_width)
+
+ def __lshift__(self, other):
+ if isinstance(other, int):
+ if other < 0:
+ raise ValueError("Shift amount cannot be negative")
+
+ if other > self.f_width:
+ return Value.cast(hdl.Cat(hdl.Const(0, other - self.f_width), self.raw()))
+ else:
+ return Value.cast(self.raw(), self.f_width - other)
+
+ elif not isinstance(other, hdl.Value):
+ raise TypeError("Shift amount must be an integer value")
+
+ if other.signed:
+ raise TypeError("Shift amount must be unsigned")
+
+ return Value.cast(self.raw() << other, self.f_width)
+
+ def __rshift__(self, other):
+ if isinstance(other, int):
+ if other < 0:
+ raise ValueError("Shift amount cannot be negative")
+
+ return Value.cast(self.raw(), self.f_width + other)
+
+ elif not isinstance(other, hdl.Value):
+ raise TypeError("Shift amount must be an integer value")
+
+ if other.signed:
+ raise TypeError("Shift amount must be unsigned")
+
+ # Extend f_width by maximal shift amount.
+ f_width = self.f_width + 2**other.width - 1
+
+ return Value.cast(self.round(f_width).raw() >> other, f_width)
+
+ def __lt__(self, other):
+ if isinstance(other, hdl.Value):
+ other = Value.cast(other)
+ elif isinstance(other, int):
+ other = Const(other)
+ elif not isinstance(other, Value):
+ raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
+ f_width = max(self.f_width, other.f_width)
+ return self.round(f_width).raw() < other.round(f_width).raw()
+
+ def __ge__(self, other):
+ return ~self.__lt__(other)
+
+ def __eq__(self, other):
+ if isinstance(other, hdl.Value):
+ other = Value.cast(other)
+ elif isinstance(other, int):
+ other = Const(other)
+ elif not isinstance(other, Value):
+ raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
+ f_width = max(self.f_width, other.f_width)
+ return self.round(f_width).raw() == other.round(f_width).raw()
+
+ def __repr__(self):
+ return f"(fixedpoint {'SQ' if self.signed else 'UQ'}{self.i_width}.{self.f_width} {self._target!r})"
+
+
+class Const(Value):
+ def __init__(self, value, shape=None):
+
+ if isinstance(value, float) or isinstance(value, int):
+ num, den = value.as_integer_ratio()
+ elif isinstance(value, Const):
+ # FIXME: Memory inits seem to construct a fixed.Const with fixed.Const
+ self._shape = value._shape
+ self._value = value._value
+ return
+ else:
+ raise TypeError(f"Object {value!r} cannot be converted to a fixed.Const")
+
+ # Determine smallest possible shape if not already selected.
+ if shape is None:
+ f_width = bits_for(den) - 1
+ i_width = max(0, bits_for(abs(num)) - f_width)
+ shape = Shape(i_width, f_width, signed = num < 0)
+
+ # Scale value to given precision.
+ if 2**shape.f_width > den:
+ num *= 2**shape.f_width // den
+ elif 2**shape.f_width < den:
+ num = round(num / (den // 2**shape.f_width))
+ value = num
+
+ self._shape = shape
+
+ if value > self._max_value():
+ print("WARN fixed.Const: clamp", value, "to", self._max_value())
+ value = self._max_value()
+ if value < self._min_value():
+ print("WARN fixed.Const: clamp", value, "to", self._min_value())
+ value = self._min_value()
+
+ self._value = value
+
+ def _max_value(self):
+ return 2**(self._shape.i_width +
+ self._shape.f_width) - 1
+
+ def _min_value(self):
+ if self._shape.signed:
+ return -1 * 2**(self._shape.i_width +
+ self._shape.f_width)
+ else:
+ return 0
+
+ @property
+ def _target(self):
+ return hdl.Const(self._value, self._shape.as_shape())
+
+ def as_integer_ratio(self):
+ return self._value, 2**self.f_width
+
+ def as_float(self):
+ if self._value > self._max_value():
+ v = self._min_value() + self._value - self._max_value() - 1
+ else:
+ v = self._value
+ return v / 2**self.f_width
+
+ # TODO: Operators
+
+ def __mul__(self, other):
+ # Regular values are cast to fixed.Value
+ if isinstance(other, hdl.Value):
+ other = Value.cast(other)
+
+ # int are cast to fixed.Const
+ elif isinstance(other, int):
+ other = Const(other)
+
+ # Other value types are unsupported.
+ elif not isinstance(other, Value):
+ raise TypeError(f"Object {other!r} cannot be converted to a fixed.Value")
+
+ return Value.cast(self.raw() * other.raw(), self.f_width + other.f_width)
+
+ def __rmul__(self, other):
+ return self.__mul__(other)
diff --git a/firmware/fpga/board.py b/firmware/fpga/board.py
new file mode 100644
index 00000000..9185f872
--- /dev/null
+++ b/firmware/fpga/board.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+#
+# This file is part of HackRF.
+#
+# Copyright (c) 2024 Great Scott Gadgets
+# SPDX-License-Identifier: BSD-3-Clause
+
+from amaranth import Elaboratable, Signal, Instance, Module, ClockDomain
+from amaranth.build import Resource, Pins, Clock, Attrs
+from amaranth.vendor import LatticeICE40Platform
+from amaranth_boards.resources import SPIResource
+
+__all__ = ["PralinePlatform", "ClockDomainGenerator"]
+
+
+class PralinePlatform(LatticeICE40Platform):
+ device = "iCE40UP5K"
+ package = "SG48"
+
+ default_clk = "SB_HFOSC" # 48 MHz internal oscillator
+ hfosc_div = 0 # Do not divide
+
+ resources = [
+ Resource("fpga_clk", 0, Pins("39", dir="i"),
+ Attrs(GLOBAL=True, IO_STANDARD="SB_LVCMOS")),
+
+ # ADC/DAC interfaces.
+ Resource("afe_clk", 0, Pins("35", dir="i"),
+ Clock(40e6), Attrs(GLOBAL=True, IO_STANDARD="SB_LVCMOS")),
+ Resource("dd", 0, Pins("38 37 36 34 32 31 28 27 26 25", dir="o"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+ Resource("da", 0, Pins("46 45 44 43 42 41 40 39", dir="i"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+
+ # SGPIO interface.
+ Resource("host_clk", 0, Pins("20", dir="o"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+ Resource("host_data", 0, Pins("21 19 6 13 10 3 4 18", dir="io"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+ Resource("q_invert", 0, Pins("9", dir="i"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+ Resource("direction", 0, Pins("12", dir="i"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+ Resource("disable", 0, Pins("23", dir="i"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+ Resource("capture_en", 0, Pins("11", dir="o"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+ Resource("trigger_in", 0, Pins("48", dir="i"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+ Resource("trigger_out", 0, Pins("2", dir="o"),
+ Attrs(IO_STANDARD="SB_LVCMOS")),
+
+ # SPI can be used after configuration.
+ SPIResource("spi", 0, role="peripheral",
+ cs_n="16", clk="15", copi="17", cipo="14",
+ attrs=Attrs(IO_STANDARD="SB_LVCMOS"),
+ ),
+ ]
+
+ connectors = []
+
+
+class ClockDomainGenerator(Elaboratable):
+
+ @staticmethod
+ def lut_delay(m, signal, *, depth):
+ signal_out = signal
+ for i in range(depth):
+ signal_in = signal_out
+ signal_out = Signal()
+ m.submodules += Instance("SB_LUT4",
+ p_LUT_INIT=0xAAAA, # Buffer configuration
+ i_I0=signal_in,
+ o_O=signal_out,
+ )
+ return signal_out
+
+ def elaborate(self, platform):
+ m = Module()
+
+ # Define clock domains.
+ m.domains.gck1 = cd_gck1 = ClockDomain(name="gck1", reset_less=True) # analog front-end clock.
+
+ # We need to delay `gck1` clock by at least 8ns, not possible with the PLL alone.
+ # Each LUT introduces a minimum propagation delay of 9ns (best case).
+ delayed_gck1 = self.lut_delay(m, platform.request("afe_clk").i, depth=2)
+ m.d.comb += cd_gck1.clk.eq(delayed_gck1)
+ platform.add_clock_constraint(delayed_gck1, 40e6)
+
+ return m
diff --git a/firmware/fpga/build.py b/firmware/fpga/build.py
new file mode 100644
index 00000000..e1bd6aa3
--- /dev/null
+++ b/firmware/fpga/build.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# This file is part of HackRF.
+#
+# Copyright (c) 2025 Great Scott Gadgets
+# SPDX-License-Identifier: BSD-3-Clause
+
+import lz4.block
+import struct
+import os
+
+from board import PralinePlatform
+from top.standard import Top as standard
+from top.half_precision import Top as half_precision
+from top.ext_precision_rx import Top as ext_precision_rx
+from top.ext_precision_tx import Top as ext_precision_tx
+
+
+BLOCK_SIZE = 4096 # 4 KiB blocks
+OUTPUT_FILE = "praline_fpga.bin"
+
+def compress_blockwise(input_stream, output_stream):
+ # For every block...
+ while block := f_in.read(BLOCK_SIZE):
+
+ # Compress the block using raw LZ4 block compression.
+ compressed_block = lz4.block.compress(block, store_size=False,
+ mode="high_compression", compression=12)
+
+ # Write the compressed block length and its contents to the output file.
+ f_out.write(struct.pack("
+# SPDX-License-Identifier: BSD-3-Clause
+
+from math import floor, log2, ceil, comb
+
+from amaranth import Module, Signal, Const, signed, ResetInserter, DomainRenamer, C
+from amaranth.utils import bits_for
+
+from amaranth.lib import wiring, stream, data
+from amaranth.lib.wiring import In, Out, connect
+
+from dsp.round import convergent_round
+
+
+class CICInterpolator(wiring.Component):
+ def __init__(self, M, stages, rates, width_in, width_out=None, num_channels=1, always_ready=False, domain="sync"):
+ self.M = M
+ self.stages = stages
+ self.rates = rates
+ self.width_in = width_in
+ self.num_channels = num_channels
+ if width_out is None:
+ width_out = width_in + self.bit_growths()[-1]
+ self.width_out = width_out
+ self._domain = domain
+ super().__init__({
+ "input": In(stream.Signature(
+ data.ArrayLayout(signed(width_in), num_channels),
+ always_ready=always_ready
+ )),
+ "output": Out(stream.Signature(
+ data.ArrayLayout(signed(width_out), num_channels),
+ always_ready=always_ready
+ )),
+ "factor": In(range(bits_for(max(rates)))),
+ })
+
+ def bit_growths(self):
+ bit_growths = cic_growth(N=self.stages, M=self.M, R=max(self.rates))
+ return bit_growths
+
+ def elaborate(self, platform):
+ m = Module()
+
+ always_ready = self.output.signature.always_ready
+
+ # Detect interpolation factor changes to provide an internal reset signal.
+ factor_last = Signal.like(self.factor)
+ factor_change = Signal()
+ m.d.sync += factor_last.eq(self.factor)
+ m.d.sync += factor_change.eq(factor_last != self.factor)
+ factor_reset = ResetInserter(factor_change)
+
+ # Calculated bit growths only used below for integrator stages.
+ bit_growths = iter(self.bit_growths())
+
+ stages = []
+
+ # When M=1, we can replace the inner CIC stage with an equivalent zero-order hold integrator.
+ inner_zoh = self.M == 1
+
+ # Comb stages.
+ width = self.width_in
+ for i in range(self.stages - int(inner_zoh)):
+ next_width = self.width_in + next(bit_growths)
+ stage = factor_reset(CombStage(self.M, width, width_out=next_width, num_channels=self.num_channels, always_ready=always_ready))
+ m.submodules[f"comb{i}"] = stage
+ stages += [ stage ]
+ width = next_width
+
+ # Upsampling.
+ if list(self.rates) != [1]:
+ if inner_zoh:
+ _ = next(bit_growths), next(bit_growths) # drop comb and integrator growths
+ stage = factor_reset(Upsampler(self.num_channels * width, max(self.rates), 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..a620310b 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,10 @@ 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
+ ${PATH_HACKRF_FIRMWARE_COMMON}/adc.c
)
if(BOARD STREQUAL "RAD1O")
@@ -207,6 +208,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 +248,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..2b797339 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,18 @@ 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
+ usb_api_adc.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 +74,30 @@ 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"
+ "${PATH_HACKRF_FIRMWARE_COMMON}/fpga_image.c"
+ "${PATH_HACKRF_FIRMWARE_COMMON}/fpga_selftest.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..860ff18f 100644
--- a/firmware/hackrf_usb/hackrf_usb.c
+++ b/firmware/hackrf_usb/hackrf_usb.c
@@ -46,6 +46,9 @@
#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 "usb_api_adc.h"
#include "operacake.h"
#include "usb_api_sweep.h"
#include "usb_api_transceiver.h"
@@ -57,6 +60,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 +97,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 +131,26 @@ 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_write_fpga_reg,
+ usb_vendor_request_read_fpga_reg,
+ 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,
+ usb_vendor_request_adc_read,
+ usb_vendor_request_test_rtc_osc,
};
static const uint32_t vendor_request_handler_count =
@@ -166,8 +191,8 @@ void usb_configuration_changed(usb_device_t* const device)
/* Configuration number equal 0 means usb bus reset. */
led_off(LED1);
}
- usb_endpoint_init(&usb_endpoint_bulk_in);
- usb_endpoint_init(&usb_endpoint_bulk_out);
+ usb_endpoint_init(&usb_endpoint_bulk_in, false);
+ usb_endpoint_init(&usb_endpoint_bulk_out, false);
}
void usb_set_descriptor_by_serial_number(void)
@@ -200,6 +225,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 +237,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,9 +258,25 @@ 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_shutdown();
+ clock_gen_shutdown();
+ delay_us_at_mhz(10000, 96);
pin_setup();
+#ifndef PRALINE
enable_1v8_power();
+ clock_gen_init();
+#else
+ enable_3v3aux_power();
+ #if !defined(DFU_MODE) && !defined(RAM_MODE)
+ enable_1v2_power();
+ enable_rf_power();
+ clock_gen_init();
+ #endif
+#endif
#ifdef HACKRF_ONE
// Set up mixer before enabling RF power, because its
// GPO is used to control the antenna bias tee.
@@ -248,11 +291,18 @@ 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
+ fpga_image_load(0);
+ delay_us_at_mhz(100, 204);
+ fpga_spi_selftest();
+ fpga_sgpio_selftest();
+#endif
-#ifdef HACKRF_ONE
+#if (defined HACKRF_ONE || defined PRALINE)
portapack_init();
#endif
@@ -270,8 +320,8 @@ int main(void)
usb_queue_init(&usb_endpoint_bulk_out_queue);
usb_queue_init(&usb_endpoint_bulk_in_queue);
- usb_endpoint_init(&usb_endpoint_control_out);
- usb_endpoint_init(&usb_endpoint_control_in);
+ usb_endpoint_init(&usb_endpoint_control_out, false);
+ usb_endpoint_init(&usb_endpoint_control_in, true);
nvic_set_priority(NVIC_USB0_IRQ, 255);
@@ -281,6 +331,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 +375,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_adc.c b/firmware/hackrf_usb/usb_api_adc.c
new file mode 100644
index 00000000..ee1bc98e
--- /dev/null
+++ b/firmware/hackrf_usb/usb_api_adc.c
@@ -0,0 +1,49 @@
+/*
+ * 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 "adc.h"
+#include "usb_api_adc.h"
+
+usb_request_status_t usb_vendor_request_adc_read(
+ usb_endpoint_t* const endpoint,
+ const usb_transfer_stage_t stage)
+{
+ if (stage == USB_TRANSFER_STAGE_SETUP) {
+ if ((endpoint->setup.index & ~0x80) > 7) {
+ return USB_REQUEST_STATUS_STALL;
+ }
+ uint16_t value = adc_read(endpoint->setup.index);
+ adc_off();
+ 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;
+ }
+ return USB_REQUEST_STATUS_OK;
+}
diff --git a/firmware/hackrf_usb/usb_api_adc.h b/firmware/hackrf_usb/usb_api_adc.h
new file mode 100644
index 00000000..8f4d2568
--- /dev/null
+++ b/firmware/hackrf_usb/usb_api_adc.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_ADC_H__
+#define __USB_API_ADC_H__
+
+#include
+#include
+
+usb_request_status_t usb_vendor_request_adc_read(
+ usb_endpoint_t* const endpoint,
+ const usb_transfer_stage_t stage);
+
+#endif // __USB_API_ADC_H__
diff --git a/firmware/hackrf_usb/usb_api_board_info.c b/firmware/hackrf_usb/usb_api_board_info.c
index 97f92a99..aeff83b8 100644
--- a/firmware/hackrf_usb/usb_api_board_info.c
+++ b/firmware/hackrf_usb/usb_api_board_info.c
@@ -130,6 +130,8 @@ usb_request_status_t usb_vendor_request_reset(
const usb_transfer_stage_t stage)
{
if (stage == USB_TRANSFER_STAGE_SETUP) {
+ pin_shutdown();
+ clock_gen_shutdown();
#ifdef HACKRF_ONE
/*
* Set boot pins as inputs so that the bootloader reads them
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..2cd57cb9 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,35 @@ 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_write_fpga_reg(
+ usb_endpoint_t* const endpoint,
+ const usb_transfer_stage_t stage)
+{
+ if (stage == USB_TRANSFER_STAGE_SETUP) {
+ fpga_reg_write(&fpga, endpoint->setup.index, endpoint->setup.value);
+ usb_transfer_schedule_ack(endpoint->in);
+ return USB_REQUEST_STATUS_OK;
+ }
+ return USB_REQUEST_STATUS_OK;
+}
+
+usb_request_status_t usb_vendor_request_read_fpga_reg(
+ usb_endpoint_t* const endpoint,
+ const usb_transfer_stage_t stage)
+{
+ if (stage == USB_TRANSFER_STAGE_SETUP) {
+ const uint8_t value = fpga_reg_read(&fpga, endpoint->setup.index);
+ 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..8fc62c2c 100644
--- a/firmware/hackrf_usb/usb_api_register.h
+++ b/firmware/hackrf_usb/usb_api_register.h
@@ -57,5 +57,13 @@ 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);
+#ifdef PRALINE
+usb_request_status_t usb_vendor_request_write_fpga_reg(
+ usb_endpoint_t* const endpoint,
+ const usb_transfer_stage_t stage);
+usb_request_status_t usb_vendor_request_read_fpga_reg(
+ usb_endpoint_t* const endpoint,
+ const usb_transfer_stage_t stage);
+#endif
#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..1bafbe00
--- /dev/null
+++ b/firmware/hackrf_usb/usb_api_selftest.c
@@ -0,0 +1,226 @@
+/*
+ * 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
+#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;
+ }
+}
+
+#ifdef PRALINE
+static const char* test_result_to_str(test_result_t result)
+{
+ switch (result) {
+ case FAILED:
+ return "FAIL";
+ case PASSED:
+ return "PASS";
+ case SKIPPED:
+ return "SKIP";
+ case TIMEOUT:
+ return "TIMEOUT";
+ }
+ return "????";
+}
+#endif
+
+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, RSSI mux test: ");
+ append(&s, &c, selftest.max2831_mux_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
+#ifdef PRALINE
+ append(&s, &c, "FPGA configuration: ");
+ append(&s, &c, test_result_to_str(selftest.fpga_image_load));
+ append(&s, &c, "\n");
+ append(&s, &c, "FPGA SPI: ");
+ append(&s, &c, test_result_to_str(selftest.fpga_spi));
+ append(&s, &c, "\n");
+ append(&s, &c, "SGPIO RX test: ");
+ append(&s, &c, test_result_to_str(selftest.sgpio_rx));
+ append(&s, &c, "\n");
+ append(&s, &c, "Loopback test: ");
+ append(&s, &c, test_result_to_str(selftest.xcvr_loopback));
+ append(&s, &c, "\n");
+ // Dump transceiver loopback measurements.
+ for (int i = 0; i < 4; ++i) {
+ struct xcvr_measurements* m = &selftest.xcvr_measurements[i];
+ append(&s, &c, " ");
+ append(&s, &c, itoa(i, 10));
+ append(&s, &c, ":");
+ append(&s, &c, itoa(m->zcs_i, 10));
+ append(&s, &c, ",");
+ append(&s, &c, itoa(m->zcs_q, 10));
+ append(&s, &c, ",");
+ append(&s, &c, itoa(m->max_mag_i, 10));
+ append(&s, &c, ",");
+ append(&s, &c, itoa(m->max_mag_q, 10));
+ append(&s, &c, ",");
+ append(&s, &c, itoa(m->avg_mag_sq_i, 10));
+ append(&s, &c, ",");
+ append(&s, &c, itoa(m->avg_mag_sq_q, 10));
+ 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;
+ }
+}
+
+usb_request_status_t usb_vendor_request_test_rtc_osc(
+ usb_endpoint_t* const endpoint,
+ const usb_transfer_stage_t stage)
+{
+ uint16_t count;
+ if (stage == USB_TRANSFER_STAGE_SETUP) {
+ enum {
+ START_32KHZ_OSCILLATOR,
+ START_FREQ_MONITOR,
+ READ_FREQ_MONITOR,
+ STOP_32KHZ_OSCILLATOR,
+ } step = endpoint->setup.index;
+
+ switch (step) {
+ case START_32KHZ_OSCILLATOR:
+ // Enable 32kHz oscillator
+ CREG_CREG0 &= ~(CREG_CREG0_PD32KHZ | CREG_CREG0_RESET32KHZ);
+ CREG_CREG0 |= CREG_CREG0_EN32KHZ;
+ usb_transfer_schedule_ack(endpoint->in);
+ return USB_REQUEST_STATUS_OK;
+ case START_FREQ_MONITOR:
+ // Start counting cycles with frequency monitor
+ CGU_FREQ_MON = CGU_FREQ_MON_RCNT(511) |
+ CGU_FREQ_MON_CLK_SEL(CGU_SRC_32K);
+ CGU_FREQ_MON |= CGU_FREQ_MON_MEAS_MASK;
+ usb_transfer_schedule_ack(endpoint->in);
+ return USB_REQUEST_STATUS_OK;
+ case READ_FREQ_MONITOR:
+ if (~(CGU_FREQ_MON & CGU_FREQ_MON_MEAS_MASK)) {
+ // Measurement completed.
+ count = (CGU_FREQ_MON & CGU_FREQ_MON_FCNT_MASK) >>
+ CGU_FREQ_MON_FCNT_SHIFT;
+ } else {
+ // Measurement failed to complete.
+ count = 0;
+ }
+ usb_transfer_schedule_block(
+ endpoint->in,
+ &count,
+ sizeof(count),
+ NULL,
+ NULL);
+ usb_transfer_schedule_ack(endpoint->out);
+ return USB_REQUEST_STATUS_OK;
+ case STOP_32KHZ_OSCILLATOR:
+ // Disable 32kHz oscillator
+ CREG_CREG0 &= ~CREG_CREG0_EN32KHZ;
+ CREG_CREG0 |= (CREG_CREG0_PD32KHZ | CREG_CREG0_RESET32KHZ);
+ usb_transfer_schedule_ack(endpoint->in);
+ return USB_REQUEST_STATUS_OK;
+ default:
+ return USB_REQUEST_STATUS_STALL;
+ }
+ } 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..453d0362
--- /dev/null
+++ b/firmware/hackrf_usb/usb_api_selftest.h
@@ -0,0 +1,36 @@
+/*
+ * 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);
+
+usb_request_status_t usb_vendor_request_test_rtc_osc(
+ 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..cbc8e293 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,7 @@ void transceiver_startup(const transceiver_mode_t mode)
}
activate_best_clock_source();
- hw_sync_enable(_hw_sync_mode);
+ trigger_enable(radio_get_trigger_enable(&radio, RADIO_CHANNEL0));
}
usb_request_status_t usb_vendor_request_set_transceiver_mode(
@@ -370,9 +406,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_enable(
+ &radio,
+ RADIO_CHANNEL0,
+ endpoint->setup.value != 0);
+ 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..e06d6c16 100644
--- a/firmware/hackrf_usb/usb_api_transceiver.h
+++ b/firmware/hackrf_usb/usb_api_transceiver.h
@@ -35,7 +35,6 @@ typedef struct {
extern volatile transceiver_request_t transceiver_request;
-void set_hw_sync_mode(const hw_sync_mode_t new_hw_sync_mode);
usb_request_status_t usb_vendor_request_set_transceiver_mode(
usb_endpoint_t* const endpoint,
const usb_transfer_stage_t stage);
@@ -79,7 +78,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..70c4daf1 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,19 @@ uint8_t usb_descriptor_string_product[] = {
'd', 0x00,
'1', 0x00,
'o', 0x00,
+#elif PRALINE
+ 22, // bLength
+ USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
+ 'H', 0x00,
+ 'a', 0x00,
+ 'c', 0x00,
+ 'k', 0x00,
+ 'R', 0x00,
+ 'F', 0x00,
+ ' ', 0x00,
+ 'P', 0x00,
+ 'r', 0x00,
+ 'o', 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..36f066c1 100644
--- a/host/hackrf-tools/src/hackrf_clock.c
+++ b/host/hackrf-tools/src/hackrf_clock.c
@@ -28,6 +28,10 @@
#include
#include
+#ifdef _MSC_VER
+ #define strcasecmp _stricmp
+#endif
+
#define CLOCK_UNDEFINED 0xFF
#define REGISTER_INVALID 32767
@@ -59,6 +63,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 +291,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 +306,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 +322,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 +336,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 +361,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 +394,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 +440,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..b3045b18 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,74 @@ int rffc5072_write_register(
return result;
}
-enum parts {
- PART_NONE = 0,
- PART_MAX2837 = 1,
- PART_SI5351C = 2,
- PART_RFFC5072 = 3,
-};
+int fpga_read_register(hackrf_device* device, const uint16_t register_number)
+{
+ uint8_t register_value;
+ int result = hackrf_fpga_read_register(
+ device,
+ (uint8_t) register_number,
+ ®ister_value);
+
+ if (result == HACKRF_SUCCESS) {
+ printf("[%2d] -> 0x%02x\n", register_number, register_value);
+ } else {
+ printf("hackrf_fpga_read_register() failed: %s (%d)\n",
+ hackrf_error_name(result),
+ result);
+ }
+
+ return result;
+}
+
+int fpga_read_registers(hackrf_device* device)
+{
+ uint16_t register_number;
+ int result = HACKRF_SUCCESS;
+
+ for (register_number = 1; register_number <= 5; register_number++) {
+ result = fpga_read_register(device, register_number);
+ if (result != HACKRF_SUCCESS) {
+ break;
+ }
+ }
+
+ return result;
+}
+
+int fpga_write_register(
+ hackrf_device* device,
+ const uint16_t register_number,
+ const uint16_t register_value)
+{
+ int result = HACKRF_SUCCESS;
+ result = hackrf_fpga_write_register(
+ device,
+ (uint8_t) register_number,
+ register_value);
+
+ if (result == HACKRF_SUCCESS) {
+ printf("0x%02x -> [%2d]\n", register_value, register_number);
+ } else {
+ printf("hackrf_fpga_write_register() 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_read_register(device, register_number);
}
return HACKRF_ERROR_INVALID_PARAM;
}
@@ -384,11 +493,14 @@ 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:
return rffc5072_read_registers(device);
+ case PART_GATEWARE:
+ return fpga_read_registers(device);
}
return HACKRF_ERROR_INVALID_PARAM;
}
@@ -401,11 +513,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_write_register(device, register_number, register_value);
}
return HACKRF_ERROR_INVALID_PARAM;
}
@@ -465,19 +584,28 @@ 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("\t-o, --rtc-osc: test 32.768kHz RTC oscillator\n");
+ printf("\t-a, --adc : read value from an ADC channel. Add 0x80 for alternate pin\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 +617,30 @@ 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'},
+ {"rtc-osc", no_argument, 0, 'o'},
+ {"adc", required_argument, 0, 'a'},
{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 +657,22 @@ 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;
+ uint32_t adc_channel;
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;
+ bool test_rtc_osc = false;
+ bool read_adc = false;
int result = hackrf_init();
if (result) {
@@ -532,7 +685,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:ta:o",
long_options,
&option_index)) != EOF) {
switch (opt) {
@@ -594,6 +747,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 +789,17 @@ int main(int argc, char** argv)
set_leds = true;
result = parse_int(optarg, &led_state);
break;
+ case 't':
+ read_selftest = true;
+ break;
+ case 'o':
+ test_rtc_osc = true;
+ break;
+
+ case 'a':
+ read_adc = true;
+ result = parse_int(optarg, &adc_channel);
+ break;
case 'h':
case '?':
@@ -642,14 +839,18 @@ 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 || test_rtc_osc ||
+ read_adc)) {
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 && !test_rtc_osc &&
+ !read_adc) {
fprintf(stderr, "Specify a part to read, write, or print config from.\n");
usage();
return EXIT_FAILURE;
@@ -663,6 +864,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 +914,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 +990,46 @@ 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);
+ }
+
+ if (test_rtc_osc) {
+ bool pass;
+ result = hackrf_test_rtc_osc(device, &pass);
+ if (result != HACKRF_SUCCESS) {
+ printf("hackrf_test_rtc_osc() failed: %s (%d)\n",
+ hackrf_error_name(result),
+ result);
+ return EXIT_FAILURE;
+ }
+ printf("RTC test result: %s\n", pass ? "PASS" : "FAIL");
+ }
+
+ if (read_adc) {
+ uint16_t value;
+ result = hackrf_read_adc(device, adc_channel, &value);
+ if (result != HACKRF_SUCCESS) {
+ printf("hackrf_read_adc() failed: %s (%d)\n",
+ hackrf_error_name(result),
+ result);
+ return EXIT_FAILURE;
+ }
+ printf("ADC0_%d (%s pin): %d\n",
+ adc_channel & 0x7,
+ adc_channel & 0x80 ? "alternate" : "dedicated",
+ value);
+ }
+
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 1a57ec66..e214ec92 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 d3b143b2..5aab1991 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,16 @@ 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_WRITE_REG = 49,
+ HACKRF_VENDOR_REQUEST_FPGA_READ_REG = 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_READ_ADC = 57,
+ HACKRF_VENDOR_REQUEST_TEST_RTC_OSC = 58,
} hackrf_vendor_request;
#define USB_CONFIG_STANDARD 0x1
@@ -924,7 +935,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,
@@ -957,7 +997,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,
@@ -1120,6 +1193,216 @@ int ADDCALL hackrf_rffc5071_write(
}
}
+int ADDCALL hackrf_fpga_read_register(
+ 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_READ_REG,
+ 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_write_register(
+ 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_WRITE_REG,
+ 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 < 2) {
+ last_libusb_error = result;
+ return HACKRF_ERROR_LIBUSB;
+ } else {
+ return HACKRF_SUCCESS;
+ }
+}
+
+int ADDCALL hackrf_test_rtc_osc(hackrf_device* device, bool* pass)
+{
+ USB_API_REQUIRED(device, 0x0109);
+
+ int result;
+
+ enum {
+ START_32KHZ_OSCILLATOR,
+ START_FREQ_MONITOR,
+ READ_FREQ_MONITOR,
+ STOP_32KHZ_OSCILLATOR,
+ } step;
+
+ // Enable 32kHz oscillator
+ result = libusb_control_transfer(
+ device->usb_device,
+ LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR |
+ LIBUSB_RECIPIENT_DEVICE,
+ HACKRF_VENDOR_REQUEST_TEST_RTC_OSC,
+ 0,
+ START_32KHZ_OSCILLATOR,
+ NULL,
+ 0,
+ 1000);
+
+ if (result < 0) {
+ last_libusb_error = result;
+ return HACKRF_ERROR_LIBUSB;
+ }
+
+// Wait 1s for oscillator startup
+#ifdef _WIN32
+ Sleep(1000);
+#else
+ usleep(1000000);
+#endif
+
+ // Start frequency monitor
+ result = libusb_control_transfer(
+ device->usb_device,
+ LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR |
+ LIBUSB_RECIPIENT_DEVICE,
+ HACKRF_VENDOR_REQUEST_TEST_RTC_OSC,
+ 0,
+ START_FREQ_MONITOR,
+ NULL,
+ 0,
+ 1000);
+
+ if (result < 0) {
+ last_libusb_error = result;
+ return HACKRF_ERROR_LIBUSB;
+ }
+
+// Wait for frequency monitor result
+#ifdef _WIN32
+ Sleep(1);
+#else
+ usleep(1000);
+#endif
+
+ // Read frequency monitor result
+ uint16_t count = 0;
+ result = libusb_control_transfer(
+ device->usb_device,
+ LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE,
+ HACKRF_VENDOR_REQUEST_TEST_RTC_OSC,
+ 0,
+ READ_FREQ_MONITOR,
+ (unsigned char*) &count,
+ sizeof(count),
+ 1000);
+
+ if (result < sizeof(count)) {
+ last_libusb_error = result;
+ return HACKRF_ERROR_LIBUSB;
+ }
+
+ if (count == 1) {
+ // Disable 32kHz oscillator
+ result = libusb_control_transfer(
+ device->usb_device,
+ LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR |
+ LIBUSB_RECIPIENT_DEVICE,
+ HACKRF_VENDOR_REQUEST_TEST_RTC_OSC,
+ 0,
+ STOP_32KHZ_OSCILLATOR,
+ NULL,
+ 0,
+ 1000);
+
+ if (result < 0) {
+ last_libusb_error = result;
+ return HACKRF_ERROR_LIBUSB;
+ }
+
+ *pass = true;
+ } else {
+ *pass = false;
+ }
+
+ return HACKRF_SUCCESS;
+}
+
+int ADDCALL hackrf_read_adc(hackrf_device* device, uint8_t adc_channel, uint16_t* value)
+{
+ USB_API_REQUIRED(device, 0x0109);
+
+ int result;
+
+ if ((adc_channel & ~0x80) > 7) {
+ 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_READ_ADC,
+ 0,
+ adc_channel,
+ (unsigned char*) value,
+ 2,
+ 0);
+
+ if (result < 2) {
+ last_libusb_error = result;
+ return HACKRF_ERROR_LIBUSB;
+ } else {
+ *value = FROM_LE16(*value);
+ return HACKRF_SUCCESS;
+ }
+}
+
int ADDCALL hackrf_get_m0_state(hackrf_device* device, hackrf_m0_state* state)
{
USB_API_REQUIRED(device, 0x0106)
@@ -2235,6 +2518,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";
@@ -2261,6 +2547,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;
}
@@ -2943,6 +3232,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";
@@ -3046,6 +3359,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 98fc4dd3..0247e6ca 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[4095];
+} hackrf_selftest;
+
/**
* List of connected HackRF devices
*
@@ -1249,6 +1354,42 @@ 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);
+
+/**
+ * Test the RTC oscillator on the device
+ *
+ * @param[in] device device to query
+ * @param[out] pass RTC oscillator test result
+ * @return @ref HACKRF_SUCCESS on success or @ref hackrf_error variant
+ * @ingroup debug
+ */
+extern ADDAPI int ADDCALL hackrf_test_rtc_osc(hackrf_device* device, bool* pass);
+
+/**
+ * Read a value from an ADC channel
+ *
+ * @param[in] device device to query
+ * @param[in] adc_channel ADC channel, e.g. 0 for ADC0_0. Add 0x80 to use an alternate pin.
+ * @param[out] value Value read from ADC.
+ * @return @ref HACKRF_SUCCESS on success or @ref hackrf_error variant
+ * @ingroup debug
+ */
+extern ADDAPI int ADDCALL hackrf_read_adc(
+ hackrf_device* device,
+ uint8_t adc_channel,
+ uint16_t* value);
+
/**
* Set transmit underrun limit
*
@@ -1304,6 +1445,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
*
@@ -1320,6 +1477,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
*
@@ -1401,6 +1574,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_read_register(
+ 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_write_register(
+ hackrf_device* device,
+ uint8_t register_number,
+ uint8_t value);
+
/**
* Erase firmware image on the SPI flash
*
@@ -2112,6 +2319,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