USRP Hardware Driver and USRP Manual  Version:
UHD and USRP Manual
ZBX Daughterboard


The ZBX daughterboard is a two-channel superheterodyne transceiver with a focus of telecommunication applications in the frequency range below 8 GHz. It supports analog bandwidths of up to 400 MHz.

The ZBX daughterboard is the daughterboard for the Ettus USRP X410.

Feature list:

  • Frequency range (Tx and Rx): 1 MHz - 7.2 GHz (Note: Tune range extends to 8 GHz)
  • Maximum analog bandwidth: 400 MHz
  • Gain range: 0-60 dB.
    • Note: Rx gain range is reduced to 0-38 dB for frequencies below 500 MHz.
  • On-board CPLD for high flexibility
  • Maximum output power: up to 23 dBm (depending on frequency, see TX Maximum Output Power in specifications)
  • Maximum input power: 0 dBm (operational, see specifications for damage levels)
  • Timed tuning is not supported by the X410, even though it is possible to tune the ZBX LOs at a given time, because timed tuning of the NCO (within the RFSoC) is not supported.

See the RF section in the Ettus USRP X410 Specifications for a comprehensive ZBX daughterboard specifications list.

Theory of Operation

The ZBX daughterboard has two transceiver chains. The following simplified block diagram shows their structure:

ZBX Block Diagram

It is a superheterodyne transceiver with up to two IF stages. The second IF stage is only used for center frequencies below 3 GHz. Above that frequency, the desired center frequency becomes the first intermediate frequency (IF1). The second LO stage is always enabled, and moves the IF to a value between 1 and 2 GHz. The USRP ADC/DAC (running at a sampling rate of approx. 3 GHz) will sample the IF directly, and downconvert to or from DC digitally.

The TX and RX paths are almost symmetric, with slight variations on the various frequency bands. The various gain stages are spread out along the TX and RX paths (see also Gain Control; note that the TX path includes selectable amplifiers as well as DSAs). All LO synthesizers are identical (LMX2572).

Digital Control

For digital controls, the ZBX includes a CPLD (its source code is part of the UHD repository, and can be found under fpga/usrp3/top/x400/dboards/zbx/cpld/). The CPLD is controlled via registers. Its register space is exposed as a subset of the Radio RFNoC block register space (starting at address 0x80000). The CPLD is used to control all switches, DSAs, amplifiers, LEDs, LO synthesizers and power rails. The CPLD also controls state-dependent behaviour of the ZBX (i.e., behaviour depending on the RX/TX state). For this purpose, ATR signals from the FPGA are routed to the CPLD. Parts of the CPLD feature set are also described in Gain Control.

LO Control

The normal operation of the ZBX daughterboard is to simply tune it to a desired center frequency, and UHD will internally calculate frequencies for the individual LOs as well as the NCO. UHD uses a few rules when calculating LO and NCO frequencies:

  • To simplify the algorithms, LO frequencies are quantized to multiples of the LO reference frequency (which itself depends on the master clock rate).
  • To reduce LO spurs, the LO synthesizer outputs are filtered with an analog bandpass filter with a minimum frequency of 3.2 GHz.
  • To avoid injection locking (an effect where nearby synthesizers influence each other), UHD is programmed to choose different frequencies for the various synthesizers. When programming the two channels to the same frequency, the LOs will thus intentionally run at different frequencies. The combination of the various LOs and the NCO frequency still results in the same center frequency.

The state of individual LOs can be queried and configured independently. Use the following API calls to do so:

Note that manually modifying LOs is considered advanced behaviour, and may result in a bad state of the device. To undo manual changes, use the regular API calls to set a center frequency.

Antenna Ports

The ZBX has two SMA ports per channel, called "TX/RX0" and "RX1". In addition, the antenna values can be set to "CAL_LOOPBACK" to loop back the Tx path into the Rx path (this is sometimes required for calibration purposes). The Rx antenna value can also be set to "TERMINATION" to terminate the Rx path.

Use the uhd::usrp::multi_usrp::get_rx_antennas() or uhd::usrp::multi_usrp::get_tx_antennas() API calls to enumerate the valid antenna names. When using RFNoC API, use the uhd::rfnoc::radio_control::get_rx_antennas() and uhd::rfnoc::radio_control::get_tx_antennas() calls, respectively.

Status LEDs

The ZBX daughterboard is equipped with two LEDs per channel, one for "TX/RX0" and one for "RX1". These LEDs behave as follows:

LED State TX/RX0 RX1
Off Port is inactive Port is inactive
Green Port is receiving Port is receiving
Red Port is transmitting N/A

Power Calibration

The ZBX supports the UHD power API (see also Power Level Controls). UHD ships with nominal calibration data which will allow setting the reference power levels without previously manually calibrating the device.


Every channel has three "locked" sensors for the LO stages (lo1_locked, lo2_locked, and nco_locked). A "virtual" sensor called lo_locked confirms that all LOs that are currently engaged are locked. The "NCO lock" sensor is a special case: The NCO is not on the daughterboard (it is part of the RFSoC FPGA), but to simplify the API it was placed together with the LO lock sensors. Unlike the (analog) synthesizers on the daughterboard, the NCO "unlock" is not used to signify a loss of reference lock, but to signal that the NCO is still in reset.

Additionally, the ZBX has a temperature sensor: temperature. While the UHD API allows addressing a sensor based on direction (RX/TX) and channel (0/1), there is only one physical temperature sensor, and it will return the same value regardless of which channel or direction is selected.

The following API calls can be used to enumerate available sensors, and query their values:

Gain Control

The ZBX has a sophisticated gain control, capable of controlling either an overall gain, or manually controlling its individual gain stages. Furthermore, the onboard CPLD can store gain tables as well, which can be accessed from other RFNoC blocks via RFNoC commands.

The TX path has three gain-related components: Two DSAs, as well an amplifier path. The former have an individual gain range of 31 dB. The amplifier path allows selecting one of two amplifiers, one with a nominal gain of 14 dB for lower frequencies, and one with a nominal gain of 21 dB for higher frequencies. Their actual amplification values depend on the specific frequency, and may also vary from device to device. The amplifiers can be bypassed.

The RX path consists of four DSAs, each with a gain range of 15 dB.

Different gain behaviours of the ZBX daughterboard are controlled by gain profiles, which may be set independently for TX and RX. Different gain profiles have different API behaviours as explained in the rest of this section.

To switch between gain profiles, use the uhd::usrp::multi_usrp::set_tx_gain_profile() or uhd::usrp::multi_usrp::set_rx_gain_profile() API calls. When using the RFNoC API, use the uhd::rfnoc::radio_control::set_tx_gain_profile() or uhd::rfnoc::radio_control::set_rx_gain_profile() API calls. The main difference between these APIs is that the multi_usrp API calls allow a default channel value, which the RFNoC API calls do not.

As with all other devices, the following API calls set or query gain values:

These API calls come in different flavours, with an optional 'gain name' argument. Note the multi_usrp API calls default to setting the overall gain value, which is not allowed in all gain profiles. All API calls have a corresponding API call to query the allowable gain range.

Note: The RX gain range is not consistent on ZBX. Below 500 MHz, the gain range is reduced to 0-38 dB. It is recommended to use get_rx_gain_range() to query the currently valid gain range.

Note: Some gain profiles require changing on both TX and RX. For example, changing from default to table_noatr must happen on TX and RX at the same time. UHD will automatically change the gain profile accordingly. It is therefore recommended to call get_rx_gain_profile() or get_tx_gain_profile() to verify the correct gain profile when in doubt.

Default Gain Profile

This gain profile is active by default. It allows setting a single, scalar gain value. UHD will internally use a gain table to linearize the overall gain (meaning that a 1 dB gain increase will also increase the transmit or receive power by 1 dB). However, the UHD-internal gain table is not calibrated per-device, nor does it take into account temperature or other changes.

In this gain mode, it is not possible to set the individual gain stages directly, but it is possible to read them back. This may be helpful when trying to fine-tune gain settings in software.

// Assumption: 'usrp' is a multi_usrp object
usrp->set_tx_gain_profile("default"); // Only necessary if the gain profile was set to something else before
usrp->set_tx_gain(30); // Will set the gain to 30 dB on all associated daughterboards
std::cout << usrp->get_tx_gain() << std::endl; // Should print "30"
usrp->set_tx_gain(0, "DSA1"); // Will cause an exception
// Individual DSAs may still be queried. Note that even though this is an
// attenuator, the return value is a gain (higher values mean more TX power):
auto dsa1_gain = usrp->get_tx_gain("DSA1");

ATR Behaviour: The gains will apply to their respective ATR state, i.e., RX gains will be applied to both the RX and full-duplex state, and TX gains will be applied to the TX and full-duplex state. In the idle state, gains are set to minimum gain.

RFNoC Commands: All DSA values are on one register for a given ATR state, and the TX amplifier shares a register with the antenna controls. That means that changing the DSA values in this gain profile will cause two register writes (one for RX/TX, and one for full-duplex). Changing the TX amplifier gain value will incur another two writes.

Manual Gain Profile

When more control is desired, the manual gain profile can be applied. Here, it is no longer possible to request an overall gain value. However, it is now possible to set the DSA and amplifier values directly.

usrp->set_tx_gain(30); // ERROR: Now, we have to specify a name
usrp->set_tx_gain(5, "DSA1"); // Set DSA1 to 5 dB gain (equals 26 dB attenuation)
// The following line will return an undefined value and print a warning, but
// will not throw. That's because calling the overall gain is a common API call
// done by many utilities, and this behaviour is considered most backward compatible.
std::cout << usrp->get_tx_gain() << std::endl;
std::cout << usrp->get_tx_gain("DSA1") << std::endl; // Should print '5'

CPLD-Table Gain Profile with ATR control

In this profile, UHD exposes access to the gain table stored on the CPLD. By default, the CPLD is initialized with the same gain table as UHD uses internally, i.e., there is no difference in behaviour when using this profile, unless the gain table is modified.

Setting DSA values directly from UHD is not possible in this profile, nor is setting an overall gain value. However, it is now possible to load an entry from the CPLD gain table and apply it to the current DSA settings. In this profile, the ATR behaviour for the DSAs is the same as in the 'default' or 'manual' profiles. That means loading a DSA table entry requires two writes to the CPLD, one for TX/RX, and one for full-duplex.

The gain "values" are no longer interpreted as dB values, but refer to DSA table indices.

An important use case of this gain profile is when testing CPLD gain tables, but not changing other aspects of the software control. Often, this is an intermediate debugging step while developing applications that use RFNoC commands to control the gain.

Another use case of this profile is when running RFNoC applications, where the gain is controlled from another RFNoC block, but the ATR behaviour is left in its default state.

usrp->set_tx_gain(30); // ERROR: Now, we have to specify a name
usrp->get_radio_control().set_tx_gain(5); // This works, though. The radio_control
// object is smart enough to infer that
// this is the only action left.
// The previous line and the following have the same effect:
usrp->set_tx_gain(5, "TABLE");
// The CPLD DSA table entry at index 5 is now loaded and applied to the TX and
// full-duplex ATR modes. In other words, the register values TX0_TABLE_DSA*
// are copied to TX0_DSA*.
// The following line will return an undefined value and print a warning, but
// will not throw. That's because calling the overall gain is a common API call
// done by many utilities, and this behaviour is considered most backward compatible.
std::cout << usrp->get_tx_gain() << std::endl;
// The following line will print the actual value of DSA1. Note, however, that
// UHD needs to read back the value from the CPLD, since it can't know what's
// stored on the CPLD. That means the following call will require a read from
// the CPLD, which is not possible if there are timed commands queued up.
std::cout << usrp->get_tx_gain("DSA1") << std::endl; // Should print whatever
// was originally stored
// in TX0_TABLE_DSA1, which
// was copied to TX0_DSA1

CPLD-Table Gain Profile without ATR control

When running applications where the FPGA has full control over the gain, and the ATR behaviour should also be replaced by register writes, this profile may be used.

The main difference to the previous profile is that when the radio switches between RX, TX, full duplex, and idle states, there is no automatic update of the gain values. Instead, the current gain values are selected by writing to the SW_RF0_DSA_CONFIG and SW_RF1_DSA_CONFIG registers. Changing these registers will select an entry from the RX/TX DSA tables. Unlike the gain tables, these are not prepopulated.

usrp->set_tx_gain(30); // ERROR: Now, we have to specify a name
usrp->get_radio_control().set_tx_gain(5); // This works, though. The radio_control
// object is smart enough to infer that
// this is the only action left.
// The previous line and the following have the same effect (but it's a different
// effect than when using gain profile 'table'):
usrp->set_tx_gain(5, "TABLE");
// The CPLD SW_RF0_DSA_CONFIG register is now set to 5. That means the DSA values
// stored in TX0_DSA1[5] and TX0_DSA2[5] are now being used, assuming no other
// entity is sending commands to the CPLD via RFNoC.
// Copying CPLD gain table entries into the TX0_DSA* registers is not possible
// from software in this profile. Rather, we are hands-off and let the FPGA take
// control.
// Let's assume that an RFNoC block has sent register writes to the radio block
// in order to load new gain table entries, and apply them. We can still read
// back the current DSA values (the ones being currently used on the RF chain)
// by reading back from the CPLD.
// This is not possible if there are timed commands queued up.
std::cout << usrp->get_tx_gain("DSA1") << std::endl; // Should print whatever
// is currently in TX0_DSA1[i],
// where i is the value of

Auto-Transmit-Receive Registers (ATR)

Like other USRPs, the X410 provides GPIOs to the daughterboards that communicate the RX/TX state. The ZBX is, by default, configured to switch settings based on the current state (RX, TX, full duplex, idle). For example, the TX/RX antenna is switched between the TX and RX channels depending on the state. A total of 4 GPIOs between the motherboard FPGA and the daughterboard CPLD are used for this purpose, two pins per channel.

ZBX uses these pins to select between different RF control values (e.g., the aforementioned TX/RX antenna switch position; this also includes the front-panel LEDs) as well as DSA controls (e.g., when doing RX only, the TX gain stages can be set to zero to minimize leakage). Internally, this works by converting the ATR pins into a control word. This control word is used to index tables that contain different values for RF control/LEDs as well as DSAs.

Example: Assume the device is transmitting, but not receiving, on channel 0. The FPGA will set the ATR pins for channel 0 to a binary value of 0b10, which equals a decimal value of 2. The transmit gain is controlled by DSA values that are stored in tables called TX0_DSA1 and TX0_DSA2, respectively. For the duration of the transmission, the DSA values at table position TX0_DSA1[2] and TX0_DSA2[2] will thus be used. Similarly, tables for RF path control and LEDs are used to configure those. This mode of using the ATR pins is called the "classic ATR" mode and is the default behaviour.

The ZBX daughterboard provides two additional modes of utilizing those pins:

  • "Software Defined": In this mode, the ATR pins are ignored. The control word is derived from another register. In this case, changing the table index requires another register write as opposed to the almost instantaneous tracking of the ATR state in the other modes.
  • "FPGA controlled": This is similar to the classic ATR mode, but it combines the pins from channel 0 and 1. The downside is that channel 0 and 1 are no longer independent, but it allows using 16 entries from the tables instead of four as in the classic ATR mode.

The ATR pin mode can be set independently for channel 0 and 1, and also for DSA tables vs. RF control/LED tables. Note that combining the "FPGA controlled" mode on one channel with the "classic" mode on the other channel would yield a possibly conflicting configuration.

Usage of these modes is considered highly advanced usage of ZBX. The "FPGA controlled" mode is not supported by UHD without custom modifications (it is possible, however, to manually write to the appropriate registers to use this mode). Using this mode would also require modifications of the FPGA image to add custom controls to the ATR GPIO pins.

The "software defined" mode can be enabled for the DSA tables by using the table_noatr gain profile (see the previous section).

Updating the ZBX CPLD

Note: You may need to update the ZBX CPLD after updating filesystems. If an update is required, MPM will fail to initialize, and provide a corresponding error message.

If you need to update the ZBX CPLD, you can do so by running the following command on the device:


By default, this command will update the the CPLDs on both daughterboards with the image from the default path. To specify the image or daughterboards to program, use the following options:

  • --dboards=0,1
  • --file=<path_to_cpld_image>

By default, the cpld-zbx.rpd file will be provided at /lib/firmware/ni/cpld-zbx.rpd. Note that after downloading the ZBX CPLD, you will need to completely shut down and power-cycle the device.

If you are updating the image during a filesystem update, i.e., after installing the new filesystem with Mender, but before rebooting, you will need to mount the new filesystem first and copy the image over:

mkdir /mnt/other
mount /dev/mmcblk0p3 /mnt/other
cp /mnt/other/lib/firmware/ni/cpld-zbx.rpd ~
umount /mnt/other

Note that the other filesystem may be either /dev/mmcblk0p2 or /dev/mmcblk0p3. You can now update to the new CPLD image:

zbx_update_cpld --file=~/cpld-zbx.rpd

CPLD Programming: Details

Read this section if you want to create your own ZBX CPLD image, or require more information on how the CPLD image is updated.

The source code for the ZBX CPLD is provided within the UHD code repository, at fpga/usrp3/top/x400/dboards/zbx/cpld. Read the Makefile for more instructions on how to build images.

The build process will produce two CPLD bitfiles: A .rpd file and a .svf file. Both are required for different programming methods. There are two programming methods:

  • Flash mode: This requires the .rpd file. It uses the motherboard CPLD as a programming device. This is the default mode.
  • Legacy mode: This directly calls into openocd to flash the ZBX CPLD. It requires the .svf file.

Before running the updater, ensure that MPM is installed on your device as some resources from MPM code are used.

You should also ensure that the power rails to the daughterboard are up and that MPM can successfully communicate with the daughterboard when running (running uhd_usrp_probe successfully is sufficient).

MPM does not need to be running when the updater script is executed, but you should start it once to ensure a valid FPGA image is loaded and communication to the daughterboard is working before attempting this.

To specify a programming mode, use the --updater argument. For example, to force the legacy mode, use the following command:

zbx_update_cpld --updater=legacy --file=/lib/firmware/ni/cpld-zbx.svf

This will program the image from the default location onto the CPLD using the 'legacy' method.

Flash Memory and EEPROM

Every ZBX daughterboard has 2 MB of non-volatile flash memory which can be used to store data such as daughterboard-specific information. When logged into the X410 Linux system, the flash memory is mounted into the filesystem under /mnt/db0_flash and /mnt/db1_flash, respectively. The daughterboard also uses a separate EEPROM to store revision, serial, and product ID of the daughterboard.