Login

Your Position: Home > Agriculture > UHD and USRP Manual - Device Synchronization

UHD and USRP Manual - Device Synchronization

UHD and USRP Manual - Device Synchronization

The following application notes explain how to synchronize multiple USRP devices with the goal of transmitting or receiving time-aligned samples for MIMO or other applications requiring multiple USRP devices operating synchronously.

If you want to learn more, please visit our website.

Note: The following synchronization notes do not apply to USRP1, which does not support the advanced features available in newer products.

Common Reference Signals

USRP devices take two reference signals in order to synchronize clocks and time:

  • A 10 MHz reference to provide a single frequency reference for all devices (in some devices, other reference frequencies than 10 MHz are also supported).
  • A pulse-per-second (PPS) to synchronize the sample time across devices.

The way these reference signals are provided to the devices varies.

External PPS and 10 MHz reference signals

Most USRPs have SMA connnectors on the front- or back-panel to provide these signals (10 MHz reference and PPS). These signals could be provided by an OctoClock, an external/third-party GPSDO, a measurement device's reference outputs, or some other clock-generating device.

Connect these SMA connectors to the reference sources. In your software, select the external reference inputs as clock and time sources:

usrp->set_clock_source(

"external"

);

usrp->set_time_source(

"external"

);

Note: For users generating their own signals for the external SMA connectors, the PPS should be clocked from the 10 MHz reference. See the application notes for your device for specific signal requirements (e.g., voltage).

Note (N200/N210 and B100 only): Sometimes the delay on the PPS signal will cause it to arrive inside the timing margin of the FPGA sampling clock, causing PPS edges to be separated by less or more than 100 million cycles of the FPGA clock.

If this is the case, you can change the edge reference of the PPS signal with this parameter:

usrp->set_time_source(

"_external_"

);

MIMO cable reference signals (N200-Series, USRP2)

Use the MIMO expansion cable to share reference sources (USRP2 and N200-Series only). The MIMO cable can be used to synchronize one device to another device. Users of the MIMO cable may use Method 1 (explained below) to synchronize multiple pairs of devices.

usrp->set_clock_source(

"mimo"

);

usrp->set_time_source(

"mimo"

);

Synchronizing the Device Time

The purpose of the PPS signal is to synchronously latch a time into the device. You can use the uhd::usrp::multi_usrp::set_time_next_pps() function to either initialize the sample time to 0 or an absolute time, such as GPS time or UTC time. For the purposes of synchronizing devices, it doesn't matter what time you initialize to when using uhd::usrp::multi_usrp::set_time_next_pps().

Method 1 - poll the USRP time registers

One way to initialize the PPS edge is to poll the "last PPS" time from the USRP device. When the last PPS time increments, the user can determine that a PPS has occurred:

Method 2 - query the GPSDO for seconds

Most GPSDOs can be configured to output a NMEA string over the serial port once every PPS. The user can wait for this string to determine the PPS edge, and the user can also parse this string to determine GPS time:

Take a look at the sync_to_gps example for more detail.

Method 3 - MIMO cable

Note: This only applies to USRP2 and N200/N210. This method does not require a separate PPS input to the devices, but it is limited to a total of 2 USRPs.

A USRP2 device can synchronize its time to another USRP device via the MIMO cable. Unlike the other methods, this does not use a real "pulse per second". Rather, the USRP device sends an encoded time message over the MIMO cable. The slave device will automatically synchronize to the time on the master device. See Using the MIMO Cable for more detail.

Synchronizing Channel Phase

Align CORDICs in the DSP

In order to achieve phase alignment between USRP devices, the CORDICS in both devices must be aligned with respect to each other. This is easily achieved by issuing stream commands with a time spec property, which instructs the streaming to begin at a specified time. Since the devices are already synchronized via the 10 MHz and PPS inputs, the streaming will start at exactly the same time on both devices. The CORDICs are reset at each start-of-burst command, so users should ensure that every start-of-burst also has a time spec set.

For receive, a burst is started when the user issues a stream command. This stream command should have a time spec set:

For transmit, a burst is started when the user calls send(). The metadata should have a time spec set: :

Align LOs in the front-end (SBX, UBX)

Using timed commands, multiple frontends can be tuned at a specific time. This timed-tuning ensures that the phase offsets between VCO/PLL chains will remain constant after each re-tune. See notes below:

  • Phase synchronization with the UBX is only supported on the X3x0 Series
  • Phase synchronization with the SBX works on both N2x0 and X3x0 Series
  • There is a random phase offset between any two frontends
  • This phase offset is different for different LO frequencies
  • This phase offset remains constant after retuning
  • This phase offset will drift over time due to thermal and other characteristics
  • Periodic calibration will be necessary for phase-coherent applications

Code snippet example, tuning with timed commands:

Align LOs in the front-end (others)

After tuning the RF front-ends, each local oscillator may have a random phase offset due to the dividers in the VCO/PLL chains. This offset will remain constant after the device has been initialized, and will remain constant until the device is closed or re-tuned. This phase offset is typically removed by the user in MIMO applications, using a training sequence to estimate the offset. It will be necessary to re-align the LOs after each tune command.

USRP in Python — PySDR: A Guide to SDR and DSP ...

In this chapter we learn how to use the UHD Python API to control and receive/transmit signals with a USRP which is a series of SDRs made by Ettus Research (now part of NI). We will discuss transmitting and receiving on the USRP in Python, and dive into USRP-specific topics including stream arguments, subdevices, channels, 10 MHz and PPS synchronization.

If you used the standard from-source install, the following command should benchmark the receive rate of your USRP using the Python API. If using 56e6 caused many dropped samples or overruns, try lowering the number. Dropped samples aren&#;t necessarily going to ruin anything, but it&#;s a good way to test the inefficiencies that might come with using a VM or older computer, for example. If using a B 2X0, a fairly modern computer with a USB 3.0 port running properly should manage to do 56 MHz without dropped samples, especially with num_recv_frames set so high.

For more help see Ettus&#; official Building and Installing UHD from source page. Note that there are also methods of installing the drivers that don&#;t require building from source.

While the Python code provided in this textbook should work under Windows, Mac, and Linux, we will only be providing driver/API install instructions specific to Ubuntu 22 (although the instructions below should work on most Debian-based distributions). We will start by creating an Ubuntu 22 VirtualBox VM; feel free to skip the VM portion if you already have your OS ready to go. Alternatively, if you&#;re on Windows 11, Windows Subsystem for Linux (WSL) using Ubuntu 22 tends to run fairly well and supports graphics out-of-the-box.

Receiving samples off a USRP is extremely easy using the built-in convenience function &#;recv_num_samps()&#;, below is Python code that tunes the USRP to 100 MHz, using a sample rate of 1 MHz, and grabs 10,000 samples off the USRP, using a receive gain of 50 dB:

import

uhd

usrp

=

uhd

.

usrp

.

MultiUSRP

()

samples

=

usrp

.

recv_num_samps

(

,

100e6

,

1e6

,

[

0

],

50

)

# units: N, Hz, Hz, list of channel IDs, dB

print

(

samples

[

0

:

10

])

The [0] is telling the USRP to use its first input port, and only receive one channel worth of samples (for a B210 to receive on two channels at once, for example, you could use [0, 1]).

Here&#;s a tip if you are trying to receive at a high rate but are getting overflows (O&#;s are showing up in your console). Instead of usrp = uhd.usrp.MultiUSRP(), use:

usrp

=

uhd

.

usrp

.

MultiUSRP

(

"num_recv_frames="

)

which makes the receive buffer much larger (the default value is 32), helping to reduce overflows. The actual size of the buffer in bytes depends on the USRP and type of connection, but simply setting num_recv_frames to a value much higher than 32 tends to help.

For more serious applications I recommend not using the convenience function recv_num_samps(), because it hides some of the interesting behavior going on under the hood, and there is some set up that happens each call that we might only want to do once at the beginning, e.g., if we want to receive samples indefinitely. The following code has the same functionality as recv_num_samps(), in fact it&#;s almost exactly what gets called when you use the convenience function, but now we have the option to modify the behavior:

import

uhd

import

numpy

as

np

usrp

=

uhd

.

usrp

.

MultiUSRP

()

num_samps

=

# number of samples received

center_freq

=

100e6

# Hz

sample_rate

=

1e6

# Hz

gain

=

50

# dB

usrp

.

set_rx_rate

(

sample_rate

,

0

)

usrp

.

set_rx_freq

(

uhd

.

libpyuhd

.

types

.

tune_request

(

center_freq

),

0

)

usrp

.

set_rx_gain

(

gain

,

0

)

# Set up the stream and receive buffer

st_args

=

uhd

.

usrp

.

StreamArgs

(

"fc32"

,

"sc16"

)

st_args

.

channels

=

[

Additional resources:
Buy Royals Bamboo Skewers Sticks 8 inch, 100pc | Frying ...

You will get efficient and thoughtful service from Highmesh.

0

]

metadata

=

uhd

.

types

.

RXMetadata

()

streamer

=

usrp

.

get_rx_stream

(

st_args

)

recv_buffer

=

np

.

zeros

((

1

,

),

dtype

=

np

.

complex64

)

# Start Stream

stream_cmd

=

uhd

.

types

.

StreamCMD

(

uhd

.

types

.

StreamMode

.

start_cont

)

stream_cmd

.

stream_now

=

True

streamer

.

issue_stream_cmd

(

stream_cmd

)

# Receive Samples

samples

=

np

.

zeros

(

num_samps

,

dtype

=

np

.

complex64

)

for

i

in

range

(

num_samps

//

):

streamer

.

recv

(

recv_buffer

,

metadata

)

samples

[

i

*

:(

i

+

1

)

*

]

=

recv_buffer

[

0

]

# Stop Stream

stream_cmd

=

uhd

.

types

.

StreamCMD

(

uhd

.

types

.

StreamMode

.

stop_cont

)

streamer

.

issue_stream_cmd

(

stream_cmd

)

print

(

len

(

samples

))

print

(

samples

[

0

:

10

])

With num_samps set to 10,000 and the recv_buffer set to , the for loop will run 10 times, i.e., there will be 10 calls to streamer.recv. Note that we hard-coded recv_buffer to but you can find the maximum allowed value using streamer.get_max_num_samps(), which is often around -something. Also note that recv_buffer must be 2d because the same API is used when receiving multiple channels at once, but in our case we just received one channel, so recv_buffer[0] gave us the 1D array of samples that we wanted. You don&#;t need to understand too much about how the stream starts/stops for now, but know that there are other options besides &#;continuous&#; mode, such as receiving a specific number of samples and having the stream stop automatically. Although we don&#;t process metadata in this example code, it contains any errors that occur, among other things, which you can check by looking at metadata.error_code at each iteration of the loop, if desired (errors tend to also show up in the console itself, as a result of UHD, so don&#;t feel like you have to check for them within your Python code).

Receive Gain¶

The following list shows the gain range of the different USRPs, they all go from 0 dB to the number specified below. Note that this is not dBm, it&#;s essentially dBm combined with some unknown offset because these are not calibrated devices.

  • B200/B210/B200-mini: 76 dB
  • X310/N210 with WBX/SBX/UBX: 31.5 dB
  • X310 with TwinRX: 93 dB
  • E310/E312: 76 dB
  • N320/N321: 60 dB

You can also use the command uhd_usrp_probe in a terminal and in the RX Frontend section it will mention the gain range.

When specifying the gain, you can use the normal set_rx_gain() function which takes in the gain value in dB, but you can also use set_normalized_rx_gain() which takes in a value from 0 to 1 and automatically converts it to the range of the USRP you&#;re using. This is convenient when making an app that supports different models of USRP. The downside of using normalized gain is that you no longer have your units in dB, so if you want to increase your gain by 10 dB, for example, you now have to calculate the amount.

Automatic Gain Control¶

Some USRPs, including the B200 and E310 series, support automatic gain control (AGC) which will automatically adjust the receive gain in response to the received signal level, in an attempt to best &#;fill&#; the ADC&#;s bits. AGC can be turned on using:

usrp

.

set_rx_agc

(

True

,

0

)

# 0 for channel 0, i.e. the first channel of the USRP

If you have a USRP that does not implement an AGC, an exception will be thrown when running the line above. With AGC on, setting the gain won&#;t do anything.

Stream Arguments¶

In the full example above you&#;ll see the line st_args = uhd.usrp.StreamArgs("fc32", "sc16"). The first argument is the CPU data format, which is the data type of the samples once they are on your host computer. UHD supports the following CPU data types when using the Python API:

Stream Arg Numpy Data Type Description fc64 np.complex128 Complex-valued double-precision data fc32 np.complex64 Complex-valued single-precision data

You might see other options in documentation for the UHD C++ API, but these were never implemented within the Python API, at least at the time of this writing.

The second argument is the &#;over-the-wire&#; data format, i.e. the data type as the samples are sent over USB/Ethernet/SFP to the host. For the Python API, the options are: &#;sc16&#;, &#;sc12&#;, and &#;sc8&#;, with the 12 bit option only supported by certain USRPs. This choice is important because the connection between the USRP and host computer is often the bottleneck, so by switching from 16 bits to 8 bits you might achieve a higher rate. Also remember that many USRPs have ADCs limited to 12 or 14 bits, using &#;sc16&#; doesn&#;t mean the ADC is 16 bits.

For the channel portion of the st_args, see the Subdevice and Channels subsection below.

The company is the world’s best usrp radio supplier. We are your one-stop shop for all needs. Our staff are highly-specialized and will help you find the product you need.

1 0

Comments

Join Us