Estimate the Carrier Frequency Offset (CFO) between a pair of USRP N310 radios in the COSMOS sb1 sandbox using GNU Radio OFDM flowgraphs and the Schmidt-Cox algorithm.
This tutorial demonstrates carrier frequency offset estimation for OFDM using GNU Radio on the COSMOS testbed. One server transmits an OFDM waveform through a USRP N310 and an IBM 28 GHz PAAM (Phased Array Antenna Module); the other server receives and processes the signal through a second USRP N310 and PAAM, running the Schmidt-Cox coarse CFO estimation algorithm via a GNU Radio flowgraph.
The experiment uses sb1.cosmos-lab.org, which houses USRP N310s (sdr1-in1, sdr1-in2) cross-connected to IBM 28 GHz PAAM mmWave front-ends (rfdev4-in1, rfdev4-in2). The OFDM signal is configured with 64 subcarriers at 62.5 MSPS on a 3 GHz carrier, giving a maximum detectable CFO of approximately 0.488 MHz.
After completing this tutorial you will be able to:
| Difficulty | Intermediate |
| Estimated time | TODO: verify |
| Domain / sandbox | sb1.cosmos-lab.org |
| Topic group | SDR & GNU Radio |
| Last verified | Not re-tested (migrated 2026-06-20) |
Background knowledge
Account & access
Devices / nodes
| Resource | Role | Qty | Notes |
|---|---|---|---|
| srv1-lg1 | Transmit host (runs TX GNU Radio flowgraph) | 1 | sb1.cosmos-lab.org; SSH with -Y for GUI |
| srv2-lg1 | Receive host (runs RX GNU Radio flowgraph) | 1 | sb1.cosmos-lab.org; SSH with -Y for GUI |
| sdr1-in1 | Transmit USRP N310 | 1 | DATA1 addr 10.38.6.1 / DATA2 addr 10.39.6.1; serial 3176DEB |
| sdr1-in2 | Receive USRP N310 | 1 | DATA2 addr 10.39.6.2; serial 3176DFA |
| rfdev4-in1 | Transmit IBM 28 GHz PAAM | 1 | ctrl IP 10.37.7.1; RF2/RF3 cross-connected to sdr1-in1 |
| rfdev4-in2 | Receive IBM 28 GHz PAAM | 1 | ctrl IP 10.37.7.2; RF2/RF3 cross-connected to sdr1-in2 |
Disk images
| Image | Load onto | Provides |
|---|---|---|
| gnuradio_ofdm.ndz | srv1-lg1, srv2-lg1 | Ubuntu 20.04, UHD 4.4, GNU Radio 3.9, GNURadioOFDMExample/ flowgraphs, eth_config_sb1.sh |
Software components
| Component | Version | Source |
|---|---|---|
| GNU Radio / gnuradio-companion | 3.9 | preinstalled in gnuradio_ofdm.ndz |
| UHD (USRP Hardware Driver) | 4.4 | preinstalled in gnuradio_ofdm.ndz |
GNURadioOFDMExample flowgraphs (TX_OFDM.grc, RX_OFDM_CFO_estimation.grc) |
TODO: verify | preinstalled in gnuradio_ofdm.ndz |
| COSMOS array management REST API | TODO: verify | hosted on am1.cosmos-lab.org:5054 |
Spectrum / RF
B:0 (RF3 port) on both USRPs.The sb1.cosmos-lab.org RF path configuration for this experiment:
sdr1-in1 RF3 TX/RX ↔ rfdev4-in1 all ICs / TX / V-polsdr1-in1 RF3 RX2 ↔ rfdev4-in1 all ICs / RX / V-polsdr1-in2 RF3 TX/RX ↔ rfdev4-in2 all ICs / TX / V-polsdr1-in2 RF2 RX2 ↔ rfdev4-in2 all ICs / RX / V-polFull RF path diagrams are in the sb1 domain architecture page.
srv1-lg1 controls sdr1-in1 (and thereby rfdev4-in1) over the 10G DATA interfaces; srv2-lg1 controls sdr1-in2 and rfdev4-in2 likewise. The PAAM devices are managed through the array management REST API on the console. Both servers are controlled from console.sb1.cosmos-lab.org.



Reserve the resources and log into the console:
ssh <username>@console.sb1.cosmos-lab.org
Make sure all nodes and devices used in the experiment are turned off:
omf tell -a offh -t sdr1-in1,sdr1-in2,rfdev4-in1,rfdev4-in2,srv1-lg1,srv2-lg1
Load the gnuradio_ofdm.ndz image onto the server nodes:
omf load -i gnuradio_ofdm.ndz -t srv1-lg1,srv2-lg1
Turn all required resources on and check their status:
omf tell -a on -t sdr1-in1,sdr1-in2,rfdev4-in1,rfdev4-in2,srv1-lg1,srv2-lg1
omf stat -t all
Open two additional SSH sessions to the servers, with -Y for GUI forwarding:
ssh -Y root@srv1-lg1
ssh -Y root@srv2-lg1
On srv1-lg1, run the interface configuration script and verify the DATA1 and DATA2 interfaces have the expected IP addresses:
root@srv1-lg1:~# ./eth_config_sb1.sh
root@srv1-lg1:~# ip addr
2: DATA1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc mq state UP group default qlen 1000
link/ether 1c:34:da:42:0c:3c brd ff:ff:ff:ff:ff:ff
inet 10.38.1.1/16 scope global DATA1
valid_lft forever preferred_lft forever
inet6 fe80::1e34:daff:fe42:c3c/64 scope link
valid_lft forever preferred_lft forever
3: DATA2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc mq state UP group default qlen 1000
link/ether 1c:34:da:42:0c:3d brd ff:ff:ff:ff:ff:ff
inet 10.39.1.1/16 scope global DATA2
valid_lft forever preferred_lft forever
inet6 fe80::1e34:daff:fe42:c3d/64 scope link
valid_lft forever preferred_lft forever
Repeat the same on srv2-lg1.
For the expected IP address assignments for all sb1 nodes, see the SB1 IP Address Assignment table.
On srv1-lg1, run uhd_find_devices to confirm both USRP N310s are reachable:
root@srv1-lg1:~# uhd_find_devices
[INFO] [UHD] linux; GNU C++ version 9.4.0; Boost_107100; UHD_4.4.0.0-0ubuntu1~focal1
--------------------------------------------------
-- UHD Device 0
--------------------------------------------------
Device Address:
serial: 3176DEB
addr: 10.39.6.1
claimed: False
fpga: XG
mgmt_addr: 10.37.6.1
mgmt_addr: 10.38.6.1
mgmt_addr: 10.39.6.1
name: ni-n3xx-3176DEB
product: n310
type: n3xx
--------------------------------------------------
-- UHD Device 2
--------------------------------------------------
Device Address:
serial: 3176DFA
addr: 10.39.6.2
claimed: False
fpga: HG
mgmt_addr: 10.37.6.2
mgmt_addr: 10.39.6.2
name: ni-n3xx-3176DFA
product: n310
type: n3xx
Both USRPs (sdr1-in1 at 10.39.6.1 and sdr1-in2 at 10.39.6.2) should appear. If either is missing, see Troubleshooting.
Note: The source tutorial shows
und_find_devices— this is a typo; the correct command isuhd_find_devices.
The COSMOS array management service provides a RESTful API for dynamic and static PAAM configuration. This experiment uses static configuration (connect + steer + configure in a single configure call).
Configure PAAM #1 (rfdev4-in1) as TX, H-polarization, broadside (0,0):
Run the following command from the console (console.sb1.cosmos-lab.org):
root@console:~# curl "http://am1.cosmos-lab.org:5054/array_mgmt/configure?dev_name=rfdev4-in1.sb1.cosmos-lab.org&ics=all&num_elements=16&txrx=tx&pol=h&theta=0&phi=0"
Expected response (check that 2v7_0 through 2v7_3 ADC readings are non-zero, confirming all ICs initialized):
<?xml version="1.0" encoding="UTF-8"?>
<response status="OK">
<action service="array_mgmt" name="configure" ipaddr="10.37.7.1">
<step name="open" duration="3.254270"/>
<step name="initializaition" duration="0.072135"/>
<step name="enabling" duration="0.020807"/>
<step name="steering" duration="0.010150"/>
<state PAAM_ID="0x24" LO_switch="PLL" if_sw1="0x0" if_sw2="0x0" if_sw3="0x0" if_sw4="0x0" txrx="tx" polarization="h" />
<adc>
<conv index="0" name="1v2" tADC="107" tVolt="0.261" tCurr="0.131"/>
<conv index="1" name="1v5" tADC="345" tVolt="0.843" tCurr="1.686"/>
<conv index="2" name="1v8" tADC="0" tVolt="0.000" tCurr="0.000"/>
<conv index="3" name="2v7_0" tADC="262" tVolt="0.640" tCurr="1.281"/>
<conv index="4" name="2v7_1" tADC="270" tVolt="0.660" tCurr="1.320"/>
<conv index="5" name="2v7_2" tADC="259" tVolt="0.633" tCurr="1.266"/>
<conv index="6" name="2v7_3" tADC="276" tVolt="0.674" tCurr="1.349"/>
<conv index="7" name="3v3_pll" tADC="46" tVolt="0.112" tCurr="0.056"/>
<conv index="8" name="5v_uzed" tADC="308" tVolt="0.753" tCurr="0.753"/>
<conv index="9" name="12v" tADC="328" tVolt="0.802" tCurr="2.672"/>
<conv index="10" name="0V" tADC="0" tVolt="0.000"/>
<conv index="11" name="1V8" tADC="736" tVolt="1.799"/>
</adc>
<step name="status" duration="0.018327"/>
<step name="close" duration="0.100188"/>
</action>
</response>
Configure PAAM #2 (rfdev4-in2) as RX, H-polarization, broadside (0,0):
root@console:~# curl "http://am1.cosmos-lab.org:5054/array_mgmt/configure?dev_name=rfdev4-in2.sb1.cosmos-lab.org&ics=all&num_elements=16&txrx=rx&pol=h&theta=0&phi=0"
Expected response:
<?xml version="1.0" encoding="UTF-8"?>
<response status="OK">
<action service="array_mgmt" name="configure" ipaddr="10.37.7.2">
<step name="open" duration="3.295830"/>
<step name="initializaition" duration="0.071303"/>
<step name="enabling" duration="0.019037"/>
<step name="steering" duration="0.010038"/>
<state PAAM_ID="0x33" LO_switch="PLL" if_sw1="0x0" if_sw2="0x0" if_sw3="0x0" if_sw4="0x0" txrx="rx" polarization="h" />
<adc>
<conv index="0" name="1v2" tADC="106" tVolt="0.259" tCurr="0.130"/>
<conv index="1" name="1v5" tADC="345" tVolt="0.843" tCurr="1.686"/>
<conv index="2" name="1v8" tADC="0" tVolt="0.000" tCurr="0.000"/>
<conv index="3" name="2v7_0" tADC="209" tVolt="0.511" tCurr="1.022"/>
<conv index="4" name="2v7_1" tADC="171" tVolt="0.418" tCurr="0.836"/>
<conv index="5" name="2v7_2" tADC="219" tVolt="0.535" tCurr="1.070"/>
<conv index="6" name="2v7_3" tADC="217" tVolt="0.530" tCurr="1.061"/>
<conv index="7" name="3v3_pll" tADC="172" tVolt="0.420" tCurr="0.210"/>
<conv index="8" name="5v_uzed" tADC="374" tVolt="0.914" tCurr="0.914"/>
<conv index="9" name="12v" tADC="316" tVolt="0.772" tCurr="2.574"/>
<conv index="10" name="0V" tADC="0" tVolt="0.000"/>
<conv index="11" name="1V8" tADC="738" tVolt="1.804"/>
</adc>
<step name="status" duration="0.017713"/>
<step name="close" duration="0.100175"/>
</action>
</response>
On srv1-lg1, navigate to the example directory and open the TX flowgraph in GNU Radio Companion:
root@srv1-lg1:~# cd GNURadioOFDMExample/
root@srv1-lg1:~/GNURadioOFDMExample# gnuradio-companion TX_OFDM.grc
Before running, verify the USRP Sink (TX) block in TX_OFDM.grc is configured with:
10.39.6.1 (sdr1-in1)3e9 (3 GHz)B:0 (RF3)The maximum detectable CFO with these settings is 62.5 / (2 × 64) = 0.488 MHz.

On srv2-lg1, navigate to the example directory and open the RX flowgraph:
root@srv2-lg1:~# cd GNURadioOFDMExample/
root@srv2-lg1:~/GNURadioOFDMExample# gnuradio-companion RX_OFDM_CFO_estimation.grc
Before running, verify the USRP Source (RX) block in RX_OFDM_CFO_estimation.grc is configured with:
10.39.6.2 (sdr1-in2)3e9 (3 GHz)B:0 (RF3)
Start both flowgraphs (TX first, then RX, or simultaneously). On srv2-lg1 you should see received IQ samples and per-packet estimated CFO values in real time.

uhd_find_devices on srv1-lg1 shows both USRP N310s (10.39.6.1 and 10.39.6.2).configure API calls return <response status="OK"> with non-zero 2v7_0–2v7_3 ADC current readings, confirming all four ICs on each PAAM are initialized.omf tell -a offh -t sdr1-in1,sdr1-in2,rfdev4-in1,rfdev4-in2,srv1-lg1,srv2-lg1
No omf save is needed unless you modified the image.
| Symptom | Likely cause | Fix |
|---|---|---|
uhd_find_devices does not show one or both USRPs |
10G DATA interfaces not configured, or interface script not run | Re-run ./eth_config_sb1.sh on the affected server and verify with ip addr that DATA1/DATA2 have correct IPs; then retry uhd_find_devices |
PAAM configure returns an error or zero 2v7_* readings |
PAAM device not powered on, or wrong dev_name |
Confirm rfdev4-in1/rfdev4-in2 are on (omf stat); check the dev_name FQDN matches the inventory |
| GNU Radio Companion does not open (no display) | -Y X11 forwarding not enabled or broken |
Re-connect with ssh -Y; ensure your local machine has an X server running |
| RX flowgraph shows no signal / all noise | TX not transmitting, PAAM misconfigured, or subdev mismatch | Confirm TX flowgraph is running on srv1-lg1; verify both flowgraphs use carrier 3e9 and subdev B:0; re-run PAAM configure commands |
| CFO estimates are outside ±0.488 MHz | Actual frequency offset exceeds the algorithm's capture range | The maximum detectable CFO is limited to sampling_rate / (2 × num_subcarriers) = 62.5 / 128 ≈ 0.488 MHz; reduce the hardware oscillator offset or increase subcarriers |
Author(s): COSMOS team · Last verified: Not re-tested (migrated 2026-06-20) · Tested image/release: gnuradio_ofdm.ndz (Ubuntu 20.04, UHD 4.4, GNU Radio 3.9) · Tags: sdr, gnuradio, ofdm, cfo, mmwave, usrp, n310, ibm-paam