I have been able to compile and run the 'libbladeRF_test_repeater' example on Windows and it works great. Playing with this code, I made the frequency of the Tx signal increase by 1 MHz every 100 ms. Eventually, I would like to sweep this signal at a much faster rate (say, every 1 microsecond).
Is this possible? How may I achieve this with a program in C? A different approach is also welcomed.
I'm using a laptop with 64-bit Windows 7, USB 3.0, and Visual Studio 2012.
Info from my bladeRF:
VCTCXO DAC calibration: 0x9ddb
FPGA size: 115 KLE
FPGA loaded: yes
USB bus: 1
USB address: 2
USB speed: SuperSpeed
Backend: libusb
Instance: 0
bladeRF-cli version: 0.10.3-git
libbladeRF version: 0.12.1-git
Firmware version: 1.6.1-git-b7e6642
FPGA version: 0.0.3
Sweeping a Single-tone Signal
-
- Posts: 303
- Joined: Mon Mar 04, 2013 4:53 pm
Re: Sweeping a Single-tone Signal
Every 100ms seems a bit slow, but I haven't profiled it very recently so it might be that slow.
From the host side, there are two major calls that happen to change frequency - first we must set_frequency() which will actually program the PLL inside the LMS6002D. Then we must change some variable capacitors inside using the tune_vcocap() function. This tries to do a bit of a binary search on 64 different values which may be able to make the PLL be stable.
So for every bladerf_lms_read() or bladerf_lms_write() that happens, a USB transfer sends down a message which is passed to the FPGA via a 4Mbps UART. Each time this message gets passed, there are 16 bytes which are transferred. The FPGA then takes this message and translates it into a SPI action on the LMS6002D bus.
Right now, that loop of writing a VCOCAP value and checking the results if the PLL is happy or not is run on the host. This loop is very long and is probably the limiting factor for you right now. You can alleviate this in two different ways: cache the VCOCAP value for the frequencies you want to run, or push the tuning code down into the FPGA NIOS.
The easiest one is to implement is the caching of the VCOCAP value. Unfortunately, that would require that we change the API to add a flag into the set_frequency() function to take in a cached VCOCAP value and/or take in a boolean to find a new VCOCAP value.
For the absolute fastest tuning time possible, you would want to do all of the caching and calculations in the FPGA and send down a "set frequency" request which would perform all the operations including using a LUT for the VCOCAP values.
You might be hard pressed to move at 1us intervals tho. It's only a 1MHz change (or at least in this case), but I don't think you'll be able to get any better than 20us to 50us for tuning times in the worst case.
I am not sure how you want to go forward, but I think it would be cool to have a "set frequency" command go down and have the FPGA handle it all.
From the host side, there are two major calls that happen to change frequency - first we must set_frequency() which will actually program the PLL inside the LMS6002D. Then we must change some variable capacitors inside using the tune_vcocap() function. This tries to do a bit of a binary search on 64 different values which may be able to make the PLL be stable.
So for every bladerf_lms_read() or bladerf_lms_write() that happens, a USB transfer sends down a message which is passed to the FPGA via a 4Mbps UART. Each time this message gets passed, there are 16 bytes which are transferred. The FPGA then takes this message and translates it into a SPI action on the LMS6002D bus.
Right now, that loop of writing a VCOCAP value and checking the results if the PLL is happy or not is run on the host. This loop is very long and is probably the limiting factor for you right now. You can alleviate this in two different ways: cache the VCOCAP value for the frequencies you want to run, or push the tuning code down into the FPGA NIOS.
The easiest one is to implement is the caching of the VCOCAP value. Unfortunately, that would require that we change the API to add a flag into the set_frequency() function to take in a cached VCOCAP value and/or take in a boolean to find a new VCOCAP value.
For the absolute fastest tuning time possible, you would want to do all of the caching and calculations in the FPGA and send down a "set frequency" request which would perform all the operations including using a LUT for the VCOCAP values.
You might be hard pressed to move at 1us intervals tho. It's only a 1MHz change (or at least in this case), but I don't think you'll be able to get any better than 20us to 50us for tuning times in the worst case.
I am not sure how you want to go forward, but I think it would be cool to have a "set frequency" command go down and have the FPGA handle it all.
-
- Posts: 7
- Joined: Mon Feb 24, 2014 2:13 pm
Re: Sweeping a Single-tone Signal
Your ideas are good. It would make sense for the calculations to occur on the device for faster frequency changes, so I want to go that route. However, this seems above my level for now.
I will try caching the VCO values and see how it turns out.
From what I found, the limitation may also be on my Windows OS being able to call functions only so fast (maybe every 20ms?) --another reason to have the bladeRF do all the work.
From what I see, changing the Tx frequency changes the center frequency from which the device will begin transmitting with a certain bandwidth. Would it make a difference choosing a center frequency and creating a single-tone spike that sweeps the bandwidth every microsecond instead of changing the center frequency to move the spike?
I will try caching the VCO values and see how it turns out.

From what I see, changing the Tx frequency changes the center frequency from which the device will begin transmitting with a certain bandwidth. Would it make a difference choosing a center frequency and creating a single-tone spike that sweeps the bandwidth every microsecond instead of changing the center frequency to move the spike?
-
- Posts: 455
- Joined: Thu Jun 06, 2013 8:15 pm
Re: Sweeping a Single-tone Signal
If you suspect Host --> FX3 latency is a problem (certainly probably contributes some undesirable delay), you could probably hack the VCOCAP initial sweep and caching into the FX3 firmware. Maybe you'll see some improvement without having to dive down into the cockles of the FPGA just yet. Heck, you could even create a task in the FX3 firmware to sweep the VCOCAP value -- just be sure to lock access (read up on the CyU3PMutex and associated FX3 API functions) to the UART and be sure not to starve the host-side accesses of the FX3 <-> FPGA UART bridge too much.
While I personally don't know if that's a "good" route to go or not, you should be able to protoype this crudely and do a little benchmarking; Try generating this sweeping waveform offline in Matlab or Octave, and then transmitting it via the bladeRF-cli. ('tx config file='/dev/shm/my_samples.bin format=bin repeat=0' or 'tx config file='/dev/shm/my_samples.csv format=csv repeat=0' for "infinite" repetitions of the samples file). You may be interested in this wiki page if you haven't used the CLI much.
Maybe bpadalino could elaborate more on that -- I assume there's some things to be careful about as your tone nears the "edges" of the LPF?
Well, that'd certainly take that "long" tuning time out of the picture.mponce wrote:Would it make a difference choosing a center frequency and creating a single-tone spike that sweeps the bandwidth every microsecond instead of changing the center frequency to move the spike?
While I personally don't know if that's a "good" route to go or not, you should be able to protoype this crudely and do a little benchmarking; Try generating this sweeping waveform offline in Matlab or Octave, and then transmitting it via the bladeRF-cli. ('tx config file='/dev/shm/my_samples.bin format=bin repeat=0' or 'tx config file='/dev/shm/my_samples.csv format=csv repeat=0' for "infinite" repetitions of the samples file). You may be interested in this wiki page if you haven't used the CLI much.
Maybe bpadalino could elaborate more on that -- I assume there's some things to be careful about as your tone nears the "edges" of the LPF?
-
- Posts: 7
- Joined: Mon Feb 24, 2014 2:13 pm
Re: Sweeping a Single-tone Signal
Thanks for the information guys, it has been helpful. 
Using a modified version of jynik's code I was able to shift a single tone frequency. However, expecting a single tone, I see a "reflection" of this on the opposite side of the center frequency (see attached file). Is this supposed to occur? I've seen this occur on shifted signals in general.
Perhaps my math is off:
e^(j * theta) * e^(j * frequencyShift * theta) ---> i = cos(theta + frequencyShift * theta), q = sin(theta + frequencyShift * theta)
Below is the Python code I'm using to generate the IQ data:
And below are the setting used to transmit the data:
RX sample rate: 1000000 0/1
TX sample rate: 20000000 0/1
RX Bandwidth: 28000000Hz
TX Bandwidth: 28000000Hz
RX Frequency: 1000000000Hz
TX Frequency: 1000000000Hz
State: Idle
Last error: None
File: bladeRF_samples_from_csv.bin
File format: SC16 Q11, Binary
Repetitions: infinite
Repetition delay: none
# Buffers: 32
# Samples per buffer: 32768
# Transfers: 16
Timeout (ms): 1000
tx config file="C:\Windows\Temp\samples.csv" format=csv repeat=0
tx start
Converted CSV to SC16 Q11 file and switched to converted file.

Using a modified version of jynik's code I was able to shift a single tone frequency. However, expecting a single tone, I see a "reflection" of this on the opposite side of the center frequency (see attached file). Is this supposed to occur? I've seen this occur on shifted signals in general.
Perhaps my math is off:
e^(j * theta) * e^(j * frequencyShift * theta) ---> i = cos(theta + frequencyShift * theta), q = sin(theta + frequencyShift * theta)
Below is the Python code I'm using to generate the IQ data:
Code: Select all
#!env python3
import sys
import math
# Number of samples should be a multiple of 1024 to match bladeRF
# buffer size constraints
n_samples = 1024
# IQ values are in the range [-2048, 2047]. Clamp to 1800 just to
# avoid saturating
scale = 1800
if (len(sys.argv) < 2):
print('Usage: ' + sys.argv[0] + ': <output file> [n_samples]\n')
sys.exit(1)
if (len(sys.argv) > 2):
try:
n_samples = int(sys.argv[2])
except ValueError:
print('Invalid value for n_samples: ' + sys.argv[2] + '\n')
sys.exit(1)
if n_samples < 1024 or n_samples % 1024 != 0:
print('n_samples must be a multiple of 1024\n')
sys.exit(1)
with open(sys.argv[1], 'w') as out_file:
for n in range(0, n_samples):
theta = n * (2 * math.pi) / n_samples
frequencyShift = 500
i = int(scale * math.cos(theta + frequencyShift * theta))
q = int(scale * math.sin(theta + frequencyShift * theta))
out_file.write(str(i) + ', ' + str(q) + '\n')
RX sample rate: 1000000 0/1
TX sample rate: 20000000 0/1
RX Bandwidth: 28000000Hz
TX Bandwidth: 28000000Hz
RX Frequency: 1000000000Hz
TX Frequency: 1000000000Hz
State: Idle
Last error: None
File: bladeRF_samples_from_csv.bin
File format: SC16 Q11, Binary
Repetitions: infinite
Repetition delay: none
# Buffers: 32
# Samples per buffer: 32768
# Transfers: 16
Timeout (ms): 1000
tx config file="C:\Windows\Temp\samples.csv" format=csv repeat=0
tx start
Converted CSV to SC16 Q11 file and switched to converted file.
-
- Posts: 303
- Joined: Mon Mar 04, 2013 4:53 pm
Re: Sweeping a Single-tone Signal
I am not sure I follow your math there. When I look at the sample file, it doesn't look so great.
I re-wrote it in a way that I would have done it and verified the output in octave. You can take a look at my code, but it's for sure not 'production ready'.
Note that we don't need the file length to be a multiple of 1024. I am pretty sure we handle the file reads just fine and loop back around to the beginning and handle the buffer management, which is what makes this code possible. So I changed the argument samples to be the minimum length. I also didn't ask what the samplerate was or what the frequency offset was from the command line but could easily be added if you wanted.
Also note that when verifying in octave that some quantization spurs do show up. Since each angle increment is so exact, and the round() function never adds any noise, those spurs will show up. You can either dither the phase increment or the actual output if you want to see the spurs go away - but they're pretty far down.
Give that a try and let me know how it goes?
Brian
I re-wrote it in a way that I would have done it and verified the output in octave. You can take a look at my code, but it's for sure not 'production ready'.
Code: Select all
#!env python
import sys
import math
import cmath
import fractions
# Number of samples should be a multiple of 1024 to match bladeRF
# buffer size constraints
n_samples = 1024
# IQ values are in the range [-2048, 2047]. Clamp to 1800 just to
# avoid saturating
scale = 1800
if (len(sys.argv) < 2):
print('Usage: ' + sys.argv[0] + ': <output file> [n_samples]\n')
sys.exit(1)
if (len(sys.argv) > 2):
try:
min_samples = int(sys.argv[2])
except ValueError:
print('Invalid value for n_samples: ' + sys.argv[2] + '\n')
sys.exit(1)
if min_samples < 1024:
print('n_samples must be a multiple of 1024\n')
sys.exit(1)
Fs = int(1e6)
frequencyShift = int(Fs/10)
ang = 0
dang = math.pi*2*frequencyShift/Fs
n_samples = (frequencyShift*Fs)/pow(fractions.gcd(frequencyShift,Fs),2)
# Check to make sure we aren't writing too many samples
if n_samples > 400e3:
print('n_samples is just too big: ' + str(n_samples) + '\n')
sys.exit(1)
else:
print('n_samples: ' + str(n_samples) + '\n')
count = 0
with open(sys.argv[1], 'w') as out_file:
while True:
x = scale*cmath.exp(1j*ang);
count = count + 1
out_file.write(str(int(round(x.real))) + ', ' + str(int(round(x.imag))) + '\n')
#out_file.write(str(x.real) + ',' + str(x.imag) + '\n');
# Incrment angle
ang = (ang + dang) % (math.pi*2)
# Count down number of samples
if n_samples > 0:
n_samples = n_samples - 1
# Count down minimum number of samples
if min_samples > 0:
min_samples = min_samples - 1
elif ang == 0:
break
print('samples written: ' + str(count) + '\n')
Also note that when verifying in octave that some quantization spurs do show up. Since each angle increment is so exact, and the round() function never adds any noise, those spurs will show up. You can either dither the phase increment or the actual output if you want to see the spurs go away - but they're pretty far down.
Give that a try and let me know how it goes?
Brian
-
- Posts: 7
- Joined: Mon Feb 24, 2014 2:13 pm
Re: Sweeping a Single-tone Signal
I tried your code and your output definitely looks cleaner than mines! This is the output signal using a 10 MHz samplerate,
As I increase the 'frequencyShift' value everything shifts by the offset and the images that are on the left and right of the center frequency become stronger. Perhaps this has something to do with an IQ imbalance? Or, as you mentioned, a quantization error. I'll see if I can add some dither to the signal and check the IQ balance.
Regarding your code, could you explain what's happening at the n_samples part?
Also, sorry for the math; I skipped a few (a lot) of steps. This is what I was trying to do:
Let I = cos(theta) and Q = sin(theta) such that,
I + jQ = e^(j * theta) = Spike in the frequency domain
To shift the spike in the frequency domain we multiply by e^(j * frequencyShift) in the time domain,
Spike * e^(j * frequencyShift) = e^(j * theta) * e^(j * frequencyShift)
= e^(j * (theta + frequencyShift))
= cos(theta + frequencyShift) + j * sin(theta + frequencyShift)
So our new I and Q are, I = cos(theta + frequencyShift) and Q = sin(theta + frequencyShift)
Looking back at this, I see that it was perhaps unnecessary. A spike is already shifted by whatever angle you set in theta.
As I increase the 'frequencyShift' value everything shifts by the offset and the images that are on the left and right of the center frequency become stronger. Perhaps this has something to do with an IQ imbalance? Or, as you mentioned, a quantization error. I'll see if I can add some dither to the signal and check the IQ balance.
Regarding your code, could you explain what's happening at the n_samples part?
Code: Select all
n_samples = (frequencyShift*Fs)/pow(fractions.gcd(frequencyShift,Fs),2)
Let I = cos(theta) and Q = sin(theta) such that,
I + jQ = e^(j * theta) = Spike in the frequency domain
To shift the spike in the frequency domain we multiply by e^(j * frequencyShift) in the time domain,
Spike * e^(j * frequencyShift) = e^(j * theta) * e^(j * frequencyShift)
= e^(j * (theta + frequencyShift))
= cos(theta + frequencyShift) + j * sin(theta + frequencyShift)
So our new I and Q are, I = cos(theta + frequencyShift) and Q = sin(theta + frequencyShift)
Looking back at this, I see that it was perhaps unnecessary. A spike is already shifted by whatever angle you set in theta.
-
- Posts: 303
- Joined: Mon Mar 04, 2013 4:53 pm
Re: Sweeping a Single-tone Signal
Those other images showing up are most likely reconstruction filter images. How wide are the low pass filters you are setting when you do this test? For a 10MHz sample rate, I would recommend something like 8MHz or 7MHz or so. That should cut out any of the other images.
The code to find n_samples was just a quick and dirty method to find the number of points it would take to get the phase to come back to 0 - aka the minimum number of samples required to complete the cycle around the unit circle and return back to a point that I can repeat. It turns out that value is the lcm(x,y)/gcd(x,y) and the lcm can be defined as x*y/gcd(x,y) - so if you take x*y/(gcd(x,y)^2) - it's the same thing.
If you want, that code can easily be adapted to produce a sweeping tone/chirp that is some signal length.
Is that what you're ultimately after?
Brian
The code to find n_samples was just a quick and dirty method to find the number of points it would take to get the phase to come back to 0 - aka the minimum number of samples required to complete the cycle around the unit circle and return back to a point that I can repeat. It turns out that value is the lcm(x,y)/gcd(x,y) and the lcm can be defined as x*y/gcd(x,y) - so if you take x*y/(gcd(x,y)^2) - it's the same thing.
If you want, that code can easily be adapted to produce a sweeping tone/chirp that is some signal length.
Is that what you're ultimately after?
Brian
-
- Posts: 7
- Joined: Mon Feb 24, 2014 2:13 pm
Re: Sweeping a Single-tone Signal [solved]
I was using a 28 MHz tx bandwidth. Lowering it to 10MHz got rid of the extra images.
And yes!
The chirp signal (+ lowering the bandwidth) was a great idea. I was trying to sweep a single tone fast enough to see a swept signal in my spectrum analyzer, but this works just as fine.
Below is the rather crude code I used to make the chirp
My next step will be limiting the chirp to certain frequencies and shifting it (tampering with bpadalino's code).
Thanks for the help guys
And yes!
The chirp signal (+ lowering the bandwidth) was a great idea. I was trying to sweep a single tone fast enough to see a swept signal in my spectrum analyzer, but this works just as fine.
Below is the rather crude code I used to make the chirp
Code: Select all
#!env python3
import sys
import math
# Number of samples should be a multiple of 1024 to match bladeRF
# buffer size constraints
n_samples = 1024
# IQ values are in the range [-2048, 2047]. Clamp to 1800 just to
# avoid saturating
scale = 1800
if (len(sys.argv) < 2):
print('Usage: ' + sys.argv[0] + ': <output file> [n_samples]\n')
sys.exit(1)
if (len(sys.argv) > 2):
try:
n_samples = int(sys.argv[2])
except ValueError:
print('Invalid value for n_samples: ' + sys.argv[2] + '\n')
sys.exit(1)
if n_samples < 1024:
print('n_samples must greater than 1024\n')
sys.exit(1)
with open(sys.argv[1], 'w') as out_file:
for n in range(0, n_samples):
theta = n * (2 * math.pi) / n_samples
frequencyShift = n
i = int(scale * math.cos(frequencyShift*theta))
q = int(scale * math.sin(frequencyShift*theta))
out_file.write(str(i) + ', ' + str(q) + '\n')
Thanks for the help guys
