##2544 bladeRF supports up to 122 MHz sample rate. Adds new 2048 and 4096 half band decimation filters. (#2545)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-11-01 04:24:22 -05:00
committed by GitHub
parent 276f4a316a
commit 866aadc8c3
11 changed files with 660 additions and 31 deletions

View File

@@ -23,7 +23,14 @@ import java.util.ArrayList;
import java.util.List;
/**
* Sample rates offered for bladeRF tuners. Each is a multiple of the 25 kHz polyphase channel spacing.
* Sample rates offered for bladeRF tuners, up to the 122.88 MHz maximum of the bladeRF 2.0. Use getRates() to get the
* rates supported by a specific device, since the bladeRF 1.0, USB 2.0 connections and older FPGA images have lower
* maximums.
*
* Rates above 61.44 MHz use the AD9361 oversample feature, which requires 8-bit samples.
*
* Each rate is a multiple of the 25 kHz polyphase channel spacing, except for the 61.44 and 122.88 MHz rates. The
* channelizer uses slightly wider channels at those rates.
*/
public enum BladeRfSampleRate
{
@@ -36,7 +43,21 @@ public enum BladeRfSampleRate
RATE_15_0(15_000_000, "15.00 MHz"),
RATE_20_0(20_000_000, "20.00 MHz"),
RATE_25_0(25_000_000, "25.00 MHz"),
RATE_30_0(30_000_000, "30.00 MHz");
RATE_30_0(30_000_000, "30.00 MHz"),
RATE_35_0(35_000_000, "35.00 MHz"),
RATE_40_0(40_000_000, "40.00 MHz"),
RATE_45_0(45_000_000, "45.00 MHz"),
RATE_50_0(50_000_000, "50.00 MHz"),
RATE_55_0(55_000_000, "55.00 MHz"),
RATE_60_0(60_000_000, "60.00 MHz"),
RATE_61_44(61_440_000, "61.44 MHz"),
RATE_70_0(70_000_000, "70.00 MHz (8-bit)"),
RATE_80_0(80_000_000, "80.00 MHz (8-bit)"),
RATE_90_0(90_000_000, "90.00 MHz (8-bit)"),
RATE_100_0(100_000_000, "100.00 MHz (8-bit)"),
RATE_110_0(110_000_000, "110.00 MHz (8-bit)"),
RATE_120_0(120_000_000, "120.00 MHz (8-bit)"),
RATE_122_88(122_880_000, "122.88 MHz (8-bit)");
public static final BladeRfSampleRate DEFAULT = RATE_10_0;
private final int mRate;
@@ -56,6 +77,14 @@ public enum BladeRfSampleRate
return mRate;
}
/**
* Indicates if this rate requires the oversample feature and 8-bit samples.
*/
public boolean isOversampled()
{
return mRate > RATE_61_44.mRate;
}
@Override
public String toString()
{
@@ -63,7 +92,7 @@ public enum BladeRfSampleRate
}
/**
* Sample rates within the device maximum, which is the lower of the hardware limit and what the USB link sustains.
* Sample rates within the device maximum, which is the lower of what the hardware, FPGA image and USB link support.
* @param maximumRate in Hertz
* @return rates, lowest to highest
*/

View File

@@ -30,6 +30,8 @@ import io.github.dsheirer.source.tuner.TunerClass;
public class BladeRfTuner extends Tuner
{
private static final int BITS_PER_SAMPLE = 32;
private static final double SAMPLE_SIZE_12_BIT = 12.0;
private static final double SAMPLE_SIZE_8_BIT = 8.0;
/**
* Constructs an instance
@@ -72,12 +74,13 @@ public class BladeRfTuner extends Tuner
@Override
public double getSampleSize()
{
return 12.0;
BladeRfSampleRate rate = getController().getBladeRfSampleRate();
return rate != null && rate.isOversampled() ? SAMPLE_SIZE_8_BIT : SAMPLE_SIZE_12_BIT;
}
@Override
public int getMaximumUSBBitsPerSecond()
{
return BladeRfSampleRate.RATE_30_0.getRate() * BITS_PER_SAMPLE;
return BladeRfSampleRate.RATE_61_44.getRate() * BITS_PER_SAMPLE;
}
}

View File

@@ -28,14 +28,17 @@ import io.github.dsheirer.bladerf.Channel;
import io.github.dsheirer.bladerf.ChannelLayout;
import io.github.dsheirer.bladerf.DeviceInfo;
import io.github.dsheirer.bladerf.DeviceSpeed;
import io.github.dsheirer.bladerf.Feature;
import io.github.dsheirer.bladerf.GainMode;
import io.github.dsheirer.bladerf.GainModeInfo;
import io.github.dsheirer.bladerf.Range;
import io.github.dsheirer.bladerf.SampleBuffer;
import io.github.dsheirer.bladerf.SampleFormat;
import io.github.dsheirer.bladerf.StreamConfig;
import io.github.dsheirer.bladerf.Version;
import io.github.dsheirer.buffer.FloatNativeBuffer;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.SignedByteNativeBuffer;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.ITunerErrorListener;
@@ -62,6 +65,12 @@ import org.usb4java.LibUsb;
*
* Only the first receive channel (RX1 connector) is used. The bladeRF 2.0 has a second receive channel, but it shares
* the local oscillator and sample rate with the first, so it couldn't be tuned as an independent tuner.
*
* Sample rates above 61.44 MHz use the AD9361 oversample feature with 8-bit samples. These are only offered with FPGA
* version 0.15.0 or later, because starting an 8-bit sample stream on an older FPGA crashes in libbladeRF.
*
* The device is reopened when changing between oversampled and normal sample rates. Disabling the oversample feature
* doesn't fully restore the RFIC. The device reports the correct settings, but streams at the wrong rate or times out.
*/
public class BladeRfTunerController extends TunerController
{
@@ -75,10 +84,16 @@ public class BladeRfTunerController extends TunerController
private static final Channel CHANNEL = Channel.RX0;
private static final float SAMPLE_SCALE = 1.0f / 2048.0f;
private static final SampleFormat SAMPLE_FORMAT = SampleFormat.SC16_Q11;
private static final SampleFormat OVERSAMPLE_SAMPLE_FORMAT = SampleFormat.SC8_Q7;
private static final int OVERSAMPLE_MINIMUM_FPGA_MAJOR = 0;
private static final int OVERSAMPLE_MINIMUM_FPGA_MINOR = 15;
private static final int BUFFER_SAMPLE_COUNT = 65536;
private static final StreamConfig STREAM_CONFIG = new StreamConfig(32, BUFFER_SAMPLE_COUNT, 16);
private static final StreamConfig OVERSAMPLE_STREAM_CONFIG = new StreamConfig(64, BUFFER_SAMPLE_COUNT, 32);
private static final long USB2_MAXIMUM_BYTES_PER_SECOND = 32_000_000L;
private static final double MAXIMUM_USABLE_BANDWIDTH_PERCENT = 0.90;
//Measured with a carrier swept across the band at 122.88 MHz. The analog filter is 3 dB down at about +/- 33 MHz.
private static final double OVERSAMPLE_USABLE_BANDWIDTH_HZ = 66_000_000.0;
private static final long STREAM_CHECK_INTERVAL_SECONDS = 1;
private final int mBus;
@@ -89,6 +104,7 @@ public class BladeRfTunerController extends TunerController
private ScheduledFuture<?> mStreamCheck;
private List<BladeRfSampleRate> mSampleRates = List.of();
private BladeRfSampleRate mSampleRate;
private boolean mOversampled;
private List<GainModeInfo> mGainModes = List.of();
private Range mGainRange;
private long mMinimumTunableFrequency = MINIMUM_TUNABLE_FREQUENCY_HZ;
@@ -136,6 +152,8 @@ public class BladeRfTunerController extends TunerController
try
{
mDevice = BladeRf.open(findDevice());
mSampleRate = null;
mOversampled = false;
if(!mDevice.isFpgaConfigured())
{
@@ -270,12 +288,69 @@ public class BladeRfTunerController extends TunerController
if(device.speed() != DeviceSpeed.SUPER)
{
maximum = Math.min(maximum, USB2_MAXIMUM_BYTES_PER_SECOND / SAMPLE_FORMAT.bytesPerSample());
return Math.min(maximum, USB2_MAXIMUM_BYTES_PER_SECOND / SAMPLE_FORMAT.bytesPerSample());
}
if(isOversampleSupported(device.fpgaVersion()))
{
maximum = Math.max(maximum, getOversampleMaximumSampleRate(device));
}
else
{
mLog.info("bladeRF sample rates limited to [" + maximum + "] Hz - FPGA version [" + device.fpgaVersion() +
"] doesn't support 8-bit samples. Upgrade to FPGA version 0.15.0 or later for higher sample rates.");
}
return maximum;
}
/**
* Indicates if the FPGA version supports the 8-bit samples that are required for the oversample feature.
* @param fpga version
* @return true if supported
*/
static boolean isOversampleSupported(Version fpga)
{
return fpga.major() > OVERSAMPLE_MINIMUM_FPGA_MAJOR ||
(fpga.major() == OVERSAMPLE_MINIMUM_FPGA_MAJOR && fpga.minor() >= OVERSAMPLE_MINIMUM_FPGA_MINOR);
}
/**
* Usable portion of an oversampled rate, which is limited by the analog filter that stays in the signal path.
* @param rate that is oversampled
* @return usable bandwidth percentage
*/
static double getOversampleUsableBandwidthPercentage(BladeRfSampleRate rate)
{
return Math.min(MAXIMUM_USABLE_BANDWIDTH_PERCENT, OVERSAMPLE_USABLE_BANDWIDTH_HZ / rate.getRate());
}
/**
* Highest sample rate that the device reports while the oversample feature is enabled.
* @return maximum rate, or zero if the device doesn't have the oversample feature, such as the bladeRF 1.0.
*/
private static long getOversampleMaximumSampleRate(BladeRfDevice device) throws BladeRfException
{
try
{
device.enableFeature(Feature.OVERSAMPLE, true);
}
catch(BladeRfException e)
{
mLog.info("bladeRF oversample feature isn't available - " + e.getMessage());
return 0;
}
try
{
return device.sampleRateRange(CHANNEL).max();
}
finally
{
device.enableFeature(Feature.DEFAULT, true);
}
}
/**
* Finds the libbladeRF description of the device at this controller's USB bus and port.
*/
@@ -450,11 +525,33 @@ public class BladeRfTunerController extends TunerController
try
{
if(rate.isOversampled() != mOversampled)
{
reopenDevice();
if(rate.isOversampled())
{
//Bandwidth has to be set first since it can't be reliably set with oversample enabled.
mDevice.setBandwidth(CHANNEL, mDevice.bandwidthRange(CHANNEL).max());
mDevice.enableFeature(Feature.OVERSAMPLE, true);
mOversampled = true;
}
}
mDevice.setSampleRate(CHANNEL, rate.getRate());
long requested = (long)(rate.getRate() * MAXIMUM_USABLE_BANDWIDTH_PERCENT);
long bandwidth = mDevice.setBandwidth(CHANNEL, requested);
setUsableBandwidthPercentage(Math.min(MAXIMUM_USABLE_BANDWIDTH_PERCENT,
(double)bandwidth / rate.getRate()));
if(rate.isOversampled())
{
setUsableBandwidthPercentage(getOversampleUsableBandwidthPercentage(rate));
}
else
{
long requested = (long)(rate.getRate() * MAXIMUM_USABLE_BANDWIDTH_PERCENT);
long bandwidth = mDevice.setBandwidth(CHANNEL, requested);
setUsableBandwidthPercentage(Math.min(MAXIMUM_USABLE_BANDWIDTH_PERCENT,
(double)bandwidth / rate.getRate()));
}
mSampleRate = rate;
mSamplesPerMillisecond = rate.getRate() / 1000.0f;
mFrequencyController.setSampleRate(rate.getRate());
@@ -465,7 +562,7 @@ public class BladeRfTunerController extends TunerController
}
finally
{
if(streaming)
if(streaming && mDevice != null)
{
startStream();
}
@@ -478,6 +575,33 @@ public class BladeRfTunerController extends TunerController
}
}
/**
* Closes and reopens the device and restores the settings that were reset, except for the sample rate and bandwidth
* which the caller sets. The sample stream must be stopped.
*/
private void reopenDevice() throws SourceException, BladeRfException
{
long frequency = mFrequencyController.getTunedFrequency();
BladeRfDevice device = mDevice;
mDevice = null;
mOversampled = false;
closeDevice(device, false);
mDevice = BladeRf.open(findDevice());
mDevice.setFrequency(CHANNEL, frequency);
mDevice.setGainMode(CHANNEL, mGainMode);
if(mGainMode == GainMode.MANUAL)
{
mDevice.setGain(CHANNEL, mGain);
}
if(mBiasT)
{
mDevice.setBiasTee(CHANNEL, true);
}
}
/**
* Gain modes supported by the device.
*/
@@ -679,7 +803,9 @@ public class BladeRfTunerController extends TunerController
{
try
{
mStream = mDevice.startRx(ChannelLayout.RX_X1, SAMPLE_FORMAT, STREAM_CONFIG, this::receive);
mStream = mOversampled ?
mDevice.startRx(ChannelLayout.RX_X1, OVERSAMPLE_SAMPLE_FORMAT, OVERSAMPLE_STREAM_CONFIG, this::receive) :
mDevice.startRx(ChannelLayout.RX_X1, SAMPLE_FORMAT, STREAM_CONFIG, this::receive);
mStreamCheck = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::checkStream, STREAM_CHECK_INTERVAL_SECONDS,
STREAM_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
@@ -746,7 +872,16 @@ public class BladeRfTunerController extends TunerController
{
try
{
broadcast(new FloatNativeBuffer(scale(buffer.copyShorts()), System.currentTimeMillis(), mSamplesPerMillisecond));
long timestamp = System.currentTimeMillis();
if(buffer.format() == OVERSAMPLE_SAMPLE_FORMAT)
{
broadcast(new SignedByteNativeBuffer(buffer.copyBytes(), timestamp, 0.0f, 0.0f, mSamplesPerMillisecond));
}
else
{
broadcast(new FloatNativeBuffer(scale(buffer.copyShorts()), timestamp, mSamplesPerMillisecond));
}
}
catch(RuntimeException e)
{

View File

@@ -90,7 +90,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", "[][][][][][][][][][grow]"));
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", "[][][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
add(getTunerIdLabel());
@@ -101,7 +101,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
add(getButtonPanel(), "span,align left");
add(new JSeparator(), "span,growx,push");
add(new JSeparator(), "span,growx");
add(new JLabel("Frequency (MHz):"));
add(getFrequencyPanel(), "wrap");
@@ -112,7 +112,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
add(new JLabel());
add(getBiasTCheckBox(), "wrap");
add(new JSeparator(), "span,growx,push");
add(new JSeparator(), "span,growx");
add(new JLabel("Gain Control"), "wrap");
add(new JLabel("Mode:"));
@@ -121,6 +121,9 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
//Empty row that takes all extra vertical space so that the controls stay stacked at the top of the editor.
add(new JLabel(), "span,pushy");
}
@Override

View File

@@ -46,6 +46,16 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
//Maximum number of filled buffers for the blocking queue
private static final int BUFFER_MAX_CAPACITY = 600;
//Decimation rate where a 2048 sample fragment produces too few samples for the consumer, so the decimated samples
//are assembled into longer buffers. Decoders apply additional decimation that requires a buffer length that's a
//multiple of their decimation.
private static final int ASSEMBLED_OUTPUT_DECIMATION = 1024;
private static final int ASSEMBLED_OUTPUT_LENGTH = 64;
//Decimation rate where sample fragments can be too short for the filter, which requires a buffer length that's a
//multiple of the decimation rate, so the mixed samples are assembled before decimation.
private static final int ASSEMBLED_INPUT_DECIMATION = 2048;
private final Dispatcher<T> mBufferDispatcher;
private final ComplexMixer mFrequencyCorrectionMixer;
private final IRealDecimationFilter mIDecimationFilter;
@@ -59,6 +69,15 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
private final long mFrequencyOffset;
private volatile long mFrequencyCorrection;
private final AtomicBoolean mPendingMixerUpdate = new AtomicBoolean();
private final boolean mAssembleOutput;
private final float[] mInputI;
private final float[] mInputQ;
private int mInputPointer;
private long mInputTimestamp;
private float[] mOutputI;
private float[] mOutputQ;
private int mOutputPointer;
private long mOutputTimestamp;
/**
* Constructs a frequency translating and CIC decimating channel source.
@@ -87,6 +106,9 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
//The decimation filters copy the mixed samples, unless there's no decimation and the mixed samples are the output
mReuseMixerOutput = decimation > 1;
mAssembleOutput = decimation >= ASSEMBLED_OUTPUT_DECIMATION;
mInputI = decimation >= ASSEMBLED_INPUT_DECIMATION ? new float[decimation] : null;
mInputQ = decimation >= ASSEMBLED_INPUT_DECIMATION ? new float[decimation] : null;
//Set dispatcher to process 1/10 of estimated sample arrival rate, 20 times per second (up to 200% per interval)
mBufferDispatcher = new Dispatcher(threadName, 50, getHeartbeatManager(), BUFFER_MAX_CAPACITY);
@@ -242,23 +264,113 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
}
ComplexSamples basebanded = mix(iterator.next());
float[] i = mIDecimationFilter.decimateReal(basebanded.i());
float[] q = mQDecimationFilter.decimateReal(basebanded.q());
try
if(mInputI != null)
{
mSamplesListener.receive(new ComplexSamples(i, q, basebanded.timestamp()));
assembleInput(basebanded);
}
catch(Throwable t)
else
{
//The listener can be made null and cause the error - only log if we have a non-null listener
if(mSamplesListener != null)
{
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener + "]");
}
decimate(basebanded.i(), basebanded.q(), basebanded.timestamp());
}
}
}
}
/**
* Assembles the mixed samples into buffers of the decimation rate length and decimates each full buffer.
* @param samples that were mixed, which are copied since the mixer output arrays can be reused.
*/
private void assembleInput(ComplexSamples samples)
{
int pointer = 0;
while(pointer < samples.i().length)
{
if(mInputPointer == 0)
{
mInputTimestamp = samples.timestamp();
}
int length = Math.min(samples.i().length - pointer, mInputI.length - mInputPointer);
System.arraycopy(samples.i(), pointer, mInputI, mInputPointer, length);
System.arraycopy(samples.q(), pointer, mInputQ, mInputPointer, length);
pointer += length;
mInputPointer += length;
if(mInputPointer == mInputI.length)
{
mInputPointer = 0;
decimate(mInputI, mInputQ, mInputTimestamp);
}
}
}
/**
* Decimates the samples and sends them to the listener.
*/
private void decimate(float[] i, float[] q, long timestamp)
{
output(mIDecimationFilter.decimateReal(i), mQDecimationFilter.decimateReal(q), timestamp);
}
/**
* Sends the decimated samples to the listener, assembling them into larger buffers at the maximum decimation.
* @param i decimated samples
* @param q decimated samples
* @param timestamp of the first sample
*/
private void output(float[] i, float[] q, long timestamp)
{
if(!mAssembleOutput)
{
dispatch(new ComplexSamples(i, q, timestamp));
return;
}
int pointer = 0;
while(pointer < i.length)
{
if(mOutputPointer == 0)
{
//New arrays each time since the assembled arrays are passed to the consumer
mOutputI = new float[ASSEMBLED_OUTPUT_LENGTH];
mOutputQ = new float[ASSEMBLED_OUTPUT_LENGTH];
mOutputTimestamp = timestamp;
}
int length = Math.min(i.length - pointer, ASSEMBLED_OUTPUT_LENGTH - mOutputPointer);
System.arraycopy(i, pointer, mOutputI, mOutputPointer, length);
System.arraycopy(q, pointer, mOutputQ, mOutputPointer, length);
pointer += length;
mOutputPointer += length;
if(mOutputPointer == ASSEMBLED_OUTPUT_LENGTH)
{
mOutputPointer = 0;
dispatch(new ComplexSamples(mOutputI, mOutputQ, mOutputTimestamp));
}
}
}
/**
* Sends the samples to the listener.
*/
private void dispatch(ComplexSamples samples)
{
try
{
mSamplesListener.receive(samples);
}
catch(Throwable t)
{
//The listener can be made null and cause the error - only log if we have a non-null listener
if(mSamplesListener != null)
{
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener + "]");
}
}
}
}
}