Sweeping a Single-tone Signal

Discussions related to embedded firmware, driver, and user mode application software development
Post Reply
mponce
Posts: 7
Joined: Mon Feb 24, 2014 2:13 pm

Sweeping a Single-tone Signal

Post by mponce »

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
bpadalino
Posts: 303
Joined: Mon Mar 04, 2013 4:53 pm

Re: Sweeping a Single-tone Signal

Post by bpadalino »

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.
mponce
Posts: 7
Joined: Mon Feb 24, 2014 2:13 pm

Re: Sweeping a Single-tone Signal

Post by mponce »

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?
jynik
Posts: 455
Joined: Thu Jun 06, 2013 8:15 pm

Re: Sweeping a Single-tone Signal

Post by jynik »

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.
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?
Well, that'd certainly take that "long" tuning time out of the picture.

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?
mponce
Posts: 7
Joined: Mon Feb 24, 2014 2:13 pm

Re: Sweeping a Single-tone Signal

Post by mponce »

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:

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')
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.
bpadalino
Posts: 303
Joined: Mon Mar 04, 2013 4:53 pm

Re: Sweeping a Single-tone Signal

Post by bpadalino »

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'.

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')
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
mponce
Posts: 7
Joined: Mon Feb 24, 2014 2:13 pm

Re: Sweeping a Single-tone Signal

Post by mponce »

I tried your code and your output definitely looks cleaner than mines! This is the output signal using a 10 MHz samplerate,
single_tone_sine_1mhz_10msr_2.jpg
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)
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.
bpadalino
Posts: 303
Joined: Mon Mar 04, 2013 4:53 pm

Re: Sweeping a Single-tone Signal

Post by bpadalino »

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
mponce
Posts: 7
Joined: Mon Feb 24, 2014 2:13 pm

Re: Sweeping a Single-tone Signal [solved]

Post by mponce »

I was using a 28 MHz tx bandwidth. Lowering it to 10MHz got rid of the extra images.

And yes!
swept_frequencies.jpg
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')
My next step will be limiting the chirp to certain frequencies and shifting it (tampering with bpadalino's code).

Thanks for the help guys :)
Post Reply