Bring up a complete 5G Standalone network with the permissively-licensed OCUDU gNB (built on srsRAN Project) and a native Open5GS core on COSMOS, driving a USRP over the air on the sb1 or grid domains, then attach two different UEs to the same cell — a commercial Quectel 5G modem and Amarisoft's soft lteue — register each, open a PDU session, reach the internet, and measure real DL/UL throughput.
This tutorial builds a license-free 5G SA cell using OCUDU — a combined CU/DU/RU built on srsRAN Project (standard srsRAN YAML config, no license server) — paired with a native Open5GS 5G core. The gNB runs on an sb1 (or grid) compute node and drives a USRP (NI N310) over a real n41 (2.6 GHz) air interface.
The point of the tutorial is cross-vendor interoperability: the same OCUDU cell and Open5GS core serve two completely different UEs back-to-back — a commercial off-the-shelf Quectel RM520N-GL modem and Amarisoft's software UE (lteue) running over a second USRP. You will learn the one core-side fix that makes non-srsUE UEs work (IPv4-only PDU sessions), why the testbed PLMN is 00101, and the RF-underflow fix that keeps the radio glitch-free. All UEs get an IP in the standard 192.168.100.0/22 pool with internet access.
For the radio-less software version, see the companion OCUDU soft-only tutorial.
After completing this tutorial you will be able to:
sb1/grid.lteue to the same cell/core.| Difficulty | Advanced |
| Estimated time | 60–90 min |
| Domain / sandbox | sb1 or grid (USRP radios) |
| Topic group | Cellular (4G/5G/O-RAN) |
| Last verified | 2026-09-16 — three-mode parity green on sb1 (gNB srv1-lg1 + N310 sdr1-s1-lg1 + modem sdr1-in3), image ocudu-20260915, no isolcpus |
Background knowledge
ssh/ip/iperf3 on Linux.Account & access
sb1 (or grid) for the gNB node, the USRP(s), and the UE node — see Make a reservation.Devices / nodes
| Resource | Role | Qty | Notes |
|---|---|---|---|
| a compute node with a USRP | OCUDU gNB (gnb) + Open5GS core |
1 | drives the gNB N310 (e.g. 10.39.3.1) |
| a UE node | UE host — Quectel modem or soft lteue |
1 | one UE at a time on the same cell |
| NI N310 | gNB radio | 1 | band n41, 40 MHz, 1T1R |
| NI N310 or X310 | soft-UE radio (for lteue) |
1 | driven by lteue over UHD |
| Quectel RM520N-GL | commercial 5G UE | 1 | USB on the UE node; AT /dev/ttyUSB2, QMI wwan0 |
Disk images
| Image | Load onto | Provides |
|---|---|---|
ocudu.ndz |
gNB node | OCUDU gnb (/opt/ocudu), Open5GS (/opt/open5gs), launch scripts, UHD 4.9, IRQ-affinity service |
lte5gue.ndz |
UE node | Amarisoft lteue, UHD 4.9 + trx_uhd, amari_cmd.py, UE configs (soft-UE path); qmicli (modem path) |
(ocudu.ndz / lte5gue.ndz are symlinks to the latest dated builds.)
Software components
| Component | Version | Source |
|---|---|---|
| OCUDU gNB (srsRAN Project) | preinstalled | /opt/ocudu/build/apps/gnb/gnb — no license needed |
| Open5GS 5GC | build-tree | /opt/open5gs, launcher /root/open5gs/start.sh |
Amarisoft lteue |
2026-06-12 | preinstalled in the UE image (soft-UE path only) |
qmicli (libqmi) |
distro | preinstalled on the UE host (COTS-modem path) |
Spectrum / RF / special
00101 (MCC 001 / MNC 01). Soft UEs (lteue, srsue) can only form a 2-digit MNC, so 00101 is the canonical testbed PLMN. UE pool 192.168.100.0/22, gateway 192.168.100.1, internet-NAT'd.The gNB + core run on an sb1/grid node with a local USRP; a second node hosts the UE (modem on USB, or soft lteue on a second USRP):
gNB node (sb1 / grid) UE node
┌───────────────────────┐ ┌────────────────────────┐
│ OCUDU gnb (srsRAN) │ n41 2.6 GHz │ Quectel RM520N-GL │
│ Open5GS 5GC │◄─────────────►│ —or— │
│ ogstun 192.168.100.1 │ USRP N310 │ lteue → 2nd USRP │
│ AMF 127.0.0.5:38412 │ (OTA) │ │
└───────────────────────┘ └────────────────────────┘
OCUDU needs no license server — you just launch the binary.
ssh <username>@console.<domain>.cosmos-lab.org # e.g. console.sb1... or console.grid...
-r 0):omf load -i ocudu.ndz -t <gnb-node> -r 0 -o 1200
omf load -i lte5gue.ndz -t <ue-node> -r 0 -o 1200
omf tell -a on -t <gnb-node>,<ue-node>
Why PLMN 00101? A soft UE splits its IMSI as MCC(3)+MNC(2)+MSIN(10) — it cannot form a 3-digit MNC. Pointing one at a 3-digit PLMN makes it emit a mismatched SUCI → the AMF returns Registration reject [95]. Only a COTS modem can do a 3-digit MNC. So the testbed standardises on 00101.
ssh root@<gnb-node>
/root/open5gs/start.sh # NFs + ogstun 192.168.100.1/22 + NAT→internet + provisions subscribers
ss -lnp -A sctp | grep 38412
LISTEN 0 0 127.0.0.5:38412 0.0.0.0:* users:(("open5gs-amfd",...))
The launcher provisions subscribers from subscriber_db.csv, sets up ogstun on 192.168.100.1/22 with a MASQUERADE rule (so UEs reach the internet), and pins the NFs to housekeeping cores.
Cross-vendor PDU fix — do this once. The srsRAN/OCUDU gNB supports IPv4 only. Amarisoft
lteueand many COTS modems request a dual-stack IPv4v6 PDU session by default, which the gNB rejects every time ([NGAP] Unsupported PDU Session Type: ipv4v6). Force each subscriber's session to IPv4 (type:1):
shell mongosh open5gs --quiet --eval 'db.subscribers.updateMany({}, {$set: {"slice.0.session.0.type": 1}})'
Also provision the modem's SIM (IMSI 001010000000032) if it is not already present — see the modem path below.
⚠️ Do not use the image's
/root/ocudu/gnb-ocudu-40.yml. It is an old X310 file (type=x300, 184.32 MHz master clock, 46.08 Msps) with PLMN 310014 and TAC 1. Against this image's core (PLMN 00101, TAC 7) the gNB exits withNG Setup … unknown-PLMN-or-SNPN/CU-CP failed to connect to AMF— and fixing only the PLMN is not enough, because the TAC must match too. An N310 cannot run thosedevice_argsat all: it has no 184.32 MHz master clock. Write the config below instead.
Steps 1–3 below (Open5GS core, this config, and gNB launch) were re-verified 2026-09-10 on srv2-lg1.sb1 (a freshly imaged node, ocudu.ndz, N310 sdr1-s1-lg1): NG Setup succeeds reliably. The whole modem path was re-verified end to end on 2026-09-14 by the three-mode parity run (gNB srv1-lg1.sb1 freshly loaded with ocudu.ndz, no isolcpus, N310 sdr1-s1-lg1, modem on sdr1-in3.sb1): 0 RF real-time failures in all three modes — see Same experiment, same result below.
First find your N310's data-plane address, and confirm it is an N310 that nobody holds (type: n3xx, claimed: False). The address is per radio — for example sdr1-s1-lg1 on sb1 is 10.39.2.1:
USRP=10.39.2.1 # <-- your reserved N310
uhd_find_devices --args "addr=$USRP" | grep -E 'serial|claimed|product|type'
[INFO] [UHD] linux; GNU C++ version 13.3.0; Boost_108300; UHD_4.9.0.0+ds1-1~noble2
serial: 3176DF5
claimed: False
product: n310
type: n3xx
Check what the core expects — the gNB's PLMN and TAC must match these:
grep -A4 'tai:' /opt/open5gs/etc/amf.yaml
tai:
- plmn_id:
mcc: 001
mnc: 01
tac: 7
Now write the gNB config — n41, 40 MHz, 30 kHz SCS, 1T1R; an N310 needs the 122.88 MHz master clock (61.44 Msps = 122.88/2) and a frame size of 7900 (8000 silently breaks the uplink on an N3xx):
cat > /root/ocudu/gnb-ocudu-n310.yml <<EOF
cu_cp:
amf:
addrs: 127.0.0.5
port: 38412
bind_addrs: 127.0.0.1
supported_tracking_areas:
- tac: 7
plmn_list:
- plmn: "00101"
tai_slice_support_list:
- sst: 1
ru_sdr:
device_driver: uhd
device_args: type=n3xx,addr=${USRP},master_clock_rate=122.88e6,recv_frame_size=7900,send_frame_size=7900
srate: 61.44
otw_format: sc16
tx_gain: 25
rx_gain: 40
cell_cfg:
dl_arfcn: 519000
band: 41
channel_bandwidth_MHz: 40
common_scs: 30
plmn: "00101"
tac: 7
pci: 1
log:
filename: /tmp/gnb.log
all_level: info
pcap:
mac_enable: false
ngap_enable: false
EOF
grep -E 'tac|plmn|device_args' /root/ocudu/gnb-ocudu-n310.yml
srsRAN writes its own log (log.filename above, /tmp/gnb.log) — that file carries gNB started, NG Setup and any RF failure. Send the process's console output somewhere else, or the two interleave into one unreadable file. Keep stdin open (sleep infinity |): the gNB exits when its stdin closes.
cd /opt/ocudu/build
setsid bash -c 'sleep infinity | ./apps/gnb/gnb -c /root/ocudu/gnb-ocudu-n310.yml' >/tmp/gnb-console.log 2>&1 &
for i in $(seq 1 30); do grep -aq 'gNB started' /tmp/gnb.log 2>/dev/null && break; sleep 3; done
grep -aE 'NG Setup|gNB started' /tmp/gnb.log | tail -3
ss -np -A sctp | grep -E 'ESTAB.*38412'
grep -ac 'Real-time failure in RF' /tmp/gnb.log
2026-09-10T12:48:01.100880 [NGAP ] [I] "NG Setup Procedure" finished successfully
2026-09-10T12:48:01.142060 [GNB ] [I] ==== gNB started ===
ESTAB 0 0 127.0.0.5:38412 127.0.0.1:... users:(("open5gs-amfd",...))
3
gNB started plus an established SCTP association = NG Setup succeeded and the cell is on air. The last number is the count of RF real-time failures: it should stay at or near 0 (see RF underflow below). If NG Setup fails, /tmp/gnb-console.log holds the reason. Stop the gNB later with pkill -x gnb — never pkill -f apps/gnb/gnb, which matches (and kills) your own SSH shell.
RF underflow. If the gNB log shows
[RF] Real-time failure in RF: underflowand UEs see CQI 1–2 / drop mid-registration, the NIC IRQs are landing on the gNB PHY cores.ocudu-irq-affinity.service(check withsystemctl is-enabled ocudu-irq-affinity) pins the DATA-iface IRQs to housekeeping cores andopen5gs/start.shpins the NFs there too; the bundle'socudu-ota_gnbfix.shadditionally moves the USRP-NIC IRQs onto that NIC's own NUMA node and restarts the gNB once on an RF-failure flood. On 2026-09-10srv2-lg1.sb1still flooded with the IRQs pinned; on 2026-09-14srv1-lg1.sb1with the same image, noisolcpusand the bundle's fix-up ran all three modes with 0 RF failures.⚠️ Do not reuse a gNB host that still carries
isolcpusfrom an O-RAN 7.2 run.isolcpusremoves those cores from the default affinity of every process, so this gNB — launched without atasksetover all cores — gets only the non-isolated ones. Measured 2026-09-14 onsrv1-lg1.sb1(9 of 24 cores isolated): the Ansible mode never reached NG Setup within its wait, and the other two modes stalled after the DL test (ULiperf3 … Connection timed out). Re-loadocudu.ndz(the image carries noisolcpus) or restore the GRUB command line and reboot;grep -o 'isolcpus=[^ ]*' /proc/cmdlinemust print nothing.
mongosh open5gs --quiet --eval '
db.subscribers.deleteMany({imsi:"001010000000032"});
db.subscribers.insertOne({imsi:"001010000000032",
slice:[{sst:1,default_indicator:true,session:[{name:"internet",type:1,
ambr:{uplink:{value:1,unit:3},downlink:{value:1,unit:3}},
qos:{index:9,arp:{priority_level:8,pre_emption_capability:1,pre_emption_vulnerability:1}}}]}],
ambr:{uplink:{value:1,unit:3},downlink:{value:1,unit:3}},
security:{k:"9E53736BC519AB701CCAA585BDD529EC",amf:"9001",
op:"11111111111111111111111111111111",opc:null},
subscribed_rau_tau_timer:12, network_access_mode:2, subscriber_status:0,
access_restriction_data:32, schema_version:1, __v:0});'
(Adjust IMSI / K / OP to your own SIM.) Use deleteMany, not deleteOne: the image's SIM catalogue can hold two records for the same IMSI, and Open5GS authenticates against whichever it finds first. op all-ones with this K is the same credential as the catalogue's OPc c71942c8… — either form works.
ssh root@<ue-node>
systemctl stop ModemManager
python3 - <<'PY'
import serial, time
s = serial.Serial("/dev/ttyUSB2", 115200, timeout=3)
at = lambda c, w=1.0: (s.write((c+"\r\n").encode()), time.sleep(w), s.read_all().decode())[2]
at('AT+QNWPREFCFG="mode_pref",NR5G')
at('AT+QNWPREFCFG="nr5g_disable_mode",0') # 0 = SA enabled (1 DISABLES SA on this modem!)
at('AT+QNWPREFCFG="nr5g_band",41:77:78') # this cell is n41; a mask left at 77:78 by a 7.2 (n78) run never sees it
at('AT+CGDCONT=1,"IP","internet"') # IPv4 APN
at('AT+COPS=2',2); at('AT+CFUN=0',3); at('AT+CFUN=1',5); at('AT+COPS=1,2,"00101",12',6) # lock PLMN
time.sleep(8)
print("C5GREG:", at('AT+C5GREG?')); print("QENG:", at('AT+QENG="servingcell"'))
s.close()
PY
ip link set wwan0 down; echo Y > /sys/class/net/wwan0/qmi/raw_ip; ip link set wwan0 up # raw_ip only changes while DOWN
qmicli -d /dev/cdc-wdm0 --wds-start-network="apn=internet,ip-type=4" --client-no-release-cid
IP=$(qmicli -d /dev/cdc-wdm0 --wds-get-current-settings | awk '/IPv4 address/{print $NF}')
ip addr add $IP/22 dev wwan0; ip link set wwan0 mtu 1400 up
ip route replace 192.168.100.0/22 dev wwan0
ping -I wwan0 -c 5 192.168.100.1 # core gateway
ping -I wwan0 -c 5 8.8.8.8 # the internet
+C5GREG: 0,1 # registered, home, on NR5G-SA 001/01
IPv4 address: 192.168.100.2
5 packets transmitted, 5 received, 0% packet loss (8.8.8.8)
Use only a scoped route (
192.168.100.0/22 dev wwan0) — never adefault via wwan0(it would black-hole the node's management SSH). If the first ping batch shows 100% loss, that's the modem leaving RRC-idle — the next batch succeeds.
⚠️ Registered, PDU up, IP on
wwan0— and every ping still lost? Check the host side of the modem before anything else:ip -s link show wwan0 # TX "errors" rising by one per packet, TX "packets" 0 = broken USB data pathMeasured 2026-09-14 on
sdr1-in3.sb1: the modem's USB bulk-OUT endpoint was stuck returning-EPROTO(qmi_wwan … wwan0: tx throttle -71withusbnetdynamic debug on), so nothing ever reached the modem — the gNB saw no scheduling request, no GTP-U reached the core, and it looked like a baseband bug on two different gNB stacks. Aqmi_wwanunbind/bind does not clear it; a USB device reset of the modem does (0 % loss right after). The bundle's modem helper does this for you —ocudu-ota_modem.py configurefirst resets the modem ifwwan0shows any TX errors, andpython3 ocudu-ota_modem.py usbheal wwan0does only that step.The same helper also switches off the modem's active carrier MBN profile (
ROW_Commercialships active): on this firmware (RM520NGLAAR01A08M4G) even a lightAT+CFUN=0/AT+CFUN=1re-activates it, so the helper deactivates it after the last radio cycle and waits for+CPIN: READYbefore the PLMN lock (a lock sent earlier fails with+CME ERROR: 13).
lteue (soft UE, over a second USRP)ssh root@<ue-node>
cd /opt/amarisoft/lteue-linux-2026-06-12
ip link set DATA2 mtu 9000
setsid bash -c "tail -f /dev/null | ./lteue-avx2 config/ue-ocudu-00101.cfg" >/tmp/ue_run.log 2>&1 &
sleep 12
python3 amari_cmd.py 9002 '{"message":"power_on","ue_id":1}' # lteue does NOT auto-attach; retry if it races
sleep 10
ip netns exec ue1 ip -br addr show pdn0
ip netns exec ue1 ping -c 4 192.168.100.1
ip netns exec ue1 ping -c 4 8.8.8.8
The soft UE registers and gets an IP in 192.168.100.0/22 — same cell, same core as the modem. (⚠️ after killing lteue with pkill, wait ~20 s for the USRP MPM claim to release before relaunching, or the next lteue aborts with Someone tried to claim this device again.)
Path A above is Mode 1 (manual CLI). The same experiment ships as an Ansible playbook and a Jupyter notebook that read one parameter file, share one log parser and emit one JSON result contract, so the three can be checked against each other. (Path B, the soft lteue, is manual-only for now.)
On the console of your domain (console.sb1.cosmos-lab.org):
cd ~ && wget -q https://www.cosmos-lab.org/files/cosmos-tutorials-ocudu-ota.zip
unzip -o cosmos-tutorials-ocudu-ota.zip # extracts into ~/tutorials/
cd ~/tutorials && ls ocudu-ota
| File | Purpose |
|---|---|
ocudu-ota_params.yml |
the single parameter source: radio node + USRP address, N3xx device args and sample rate, PLMN/IMSI/K/OP, APN, gains, timeouts |
ocudu-ota_gnb.yml.tmpl |
the gNB config template; ocudu-ota_lib.py render-gnb fills it from the params (the image's gnb-ocudu-40.yml is an X310 / PLMN 310-014 file — do not use it as-is) |
ocudu-ota.yml |
Mode 2 — Ansible: cosmos_preflight, radio power-cycle from the console (omf), Open5GS + SIM provisioning + IPv4-only PDU fix, gNB as a named unit, modem registration, QMI data call, dual-target measurement, contract |
ocudu-ota_manual.sh |
Mode 1 scripted: the exact commands of Path A, node by node |
ocudu-ota_tutorial.ipynb |
Mode 3 — the notebook (kernel on the console, papermill-runnable) |
ocudu-ota_modem.py |
the Quectel AT helper: USB self-heal of the data path (usbheal), SA-only, band mask, APN, carrier-MBN off, PLMN lock after +CPIN: READY, registration wait with one radio kick |
ocudu-ota_gnbfix.sh |
gNB-host fix-up: USRP-NIC IRQs on their own NUMA node, RF real-time-failure flood detection with one restart |
ocudu-ota_lib.py |
shared parser + result contract + the parity comparison spec |
ocudu-ota_parity.sh |
runs all three modes on the same two nodes, asserts they agree, and runs the teardown on every exit (pass, fail or Ctrl-C) |
ocudu-ota_teardown.yml |
clean teardown (data call down, gNB/iperf units and Open5GS down; the radio is left powered) |
OMF_NODESis required and ordered: first node = gNB host (theocudu.ndznode with the USRP), second = UE host (the modem). The radio is not inOMF_NODES— it is named in the params (ocudu_sdr_node,ocudu_usrp_addr) because it is a shared sandbox resource that the playbook power-cycles withomffrom the console: reserve it and coordinate with its other users first.
Two traps the bundle handles for you, both in the shipped config. The image's
gnb-ocudu-40.ymldeclares TAC 1 while its Open5GSamf.yamlserves TAC 7 — that pair fails NG Setup withunknown-PLMN-or-SNPNand the gNB exits ("CU-CP failed to connect to AMF"). It also targets an X310 (type=x300, 46.08 Msps) and PLMN 310-014. The bundle therefore renders its own gNB config fromocudu-ota_params.yml(N3xx device args, 61.44 Msps, PLMN 00101, TAC 7). Second: srsRAN writes==== gNB started ===and anyReal-time failure in RFline to its log file (log.filename), not to stdout — watch that file, not the console output.
cd ~/tutorials
export OMF_NODES="srv1-lg1,sdr1-in3" # gNB host, UE host
ansible-playbook ocudu-ota/ocudu-ota.yml
export OMF_NODES="srv1-lg1,sdr1-in3"
/opt/cosmos-jupyter/venv/bin/papermill ocudu-ota/ocudu-ota_tutorial.ipynb ocudu-ota/ocudu-ota_tutorial-out.ipynb
bash ocudu-ota/ocudu-ota_parity.sh srv1-lg1 sdr1-in3
CPU platform (gNB host): 2x Intel(R) Xeon(R) Gold 6226 CPU @ 2.70GHz, 24 cores / 24 threads
=== ocudu-ota parity ===
mode ue_ipv4 rsrp_dbm sinr_db rf_failures ping_rtt_ms ping_remote_loss_pct dl_mbps ul_mbps
----------------------------------------------------------------------------------------------
ansible 192.168.100.2-104 12 0 25.68 0.0 65.7 59.5
manual 192.168.100.2-105 12 0 36.01 0.0 66.4 59.2
jupyter 192.168.100.2-104 12 0 31.18 0.0 66.0 59.0
dl_mbps spread max/min = 1.01 (tol 1.60)
ul_mbps spread max/min = 1.01 (tol 1.80)
PARITY: PASS (same experiment in all modes; all checks green)
artifacts kept in: /tmp/ocudu-ota-parity-seskar.jsobbk
### releasing the rig (teardown)
rig released
(Real output, 2026-09-14. The table runs the UE address into the next column: 192.168.100.2, RSRP -104.)
Pass criteria: every mode reaches gNB started with NG Setup established, registers (+C5GREG: 0,1) and gets a PDU-session address on the same PCI/band; local ping loss ≤ 20 %; DL in 20–400 and UL in 2–120 Mbit/s and within 60 %/80 % of each other across modes. (run on sb1, 2026-09-14: gNB srv1-lg1.sb1 + N310 sdr1-s1-lg1, modem on sdr1-in3.sb1, image ocudu.ndz, no isolcpus). Green parity is what sets this page's Last verified; re-running it after an image refresh is the regression test.
The harness always ends by running ocudu-ota_teardown.yml (the releasing the rig lines), because the three modes deliberately leave their units running for inspection. It also runs the notebook from the bundle's own directory (papermill --cwd), so a bundle unpacked somewhere other than ~/tutorials no longer picks up an older copy there.
Cell pci=1 … band n41 and an established SCTP association on port 38412.+C5GREG: 0,1 on 001/01; or lteue reaches 5GMM-REGISTERED.192.168.100.0/22; ping 192.168.100.1 and ping 8.8.8.8 = 0% loss.[RF] … underflow (IRQ-affinity service active).Measure air capacity with UDP (TCP is RTT-limited). Run an iperf3 server bound to ogstun and keep a keepalive ping running so the UE never drops to RRC-idle:
# on the gNB/core node:
iperf3 -s -B 192.168.100.1 -D
# on the UE node — Quectel path:
ping -I wwan0 -i 0.3 192.168.100.1 >/dev/null 2>&1 &
iperf3 -c 192.168.100.1 -B $IP -u -b 150M -R -t 12 # DL
iperf3 -c 192.168.100.1 -B $IP -u -b 40M -t 12 # UL
# lteue path: prefix each iperf3/ping with ip netns exec ue1
Measured (band n41, 40 MHz, 30 kHz SCS). Throughput is a property of the gNB host's CPU platform as much as of the radio, so every figure is listed against the platform that produced it:
| Date | gNB host CPU platform | Image | UE | Method | DL | UL |
|---|---|---|---|---|---|---|
| 2026-09-16 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads (no CPU isolation) | ocudu-20260915 |
Quectel RM520N-GL | iperf3 TCP, receiver line, 10 s — the parity bundle, three modes |
58.1–58.7 Mbit/s | 58.8–59.1 Mbit/s |
| 2026-09-14 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads (no CPU isolation) | ocudu.ndz |
Quectel RM520N-GL | iperf3 TCP, receiver line, 10 s — the parity bundle, three modes |
65.7–66.4 Mbit/s | 59.0–59.5 Mbit/s |
| 2026-09-08 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads | ocudu.ndz |
Quectel RM520N-GL | same | 55.5–57.8 Mbit/s | 54.0–54.7 Mbit/s |
| 2026-07 | not recorded | ocudu.ndz |
Quectel RM520N-GL | UDP, as above | ≈120 Mbit/s | ≈20 Mbit/s |
| 2026-07 | not recorded | ocudu.ndz + lte5gue.ndz |
Amarisoft lteue over a second USRP |
UDP, as above | ≈14–50 Mbit/s | ≈20–30 Mbit/s |
UL is bounded by the TDD pattern. The TCP figures are what the three modes measure and compare; the UDP commands above measure raw air capacity instead.
If you used the bundle (Mode 2 or 3), run its teardown — it stops the data call, the ocudu-gnb / ocudu-iperf units and Open5GS, and hands the modem back to ModemManager; the radio is left powered. The parity harness already does this on every exit.
cd ~/tutorials && export OMF_NODES="srv1-lg1,sdr1-in3"
ansible-playbook ocudu-ota/ocudu-ota_teardown.yml
After the manual path:
ssh root@<gnb-node> 'pkill -x gnb; pkill -x iperf3; /root/open5gs/stop.sh'
ssh root@<ue-node> 'pkill -x lteue-avx2; qmicli -d /dev/cdc-wdm0 --wds-stop-network=disable-autoconnect 2>/dev/null'
omf tell -a offh -t <gnb-node>,<ue-node>
| Symptom | Likely cause | Fix |
|---|---|---|
gNB: NG Setup … unknown-PLMN-or-SNPN |
core PLMN ≠ gNB plmn |
align both to 00101; restart the gNB (NG Setup is sent once) |
Registration OK but no PDU; Unsupported PDU Session Type: ipv4v6 |
UE requested dual-stack; gNB is IPv4-only | set subscriber session.type:1 (Step 1), power-cycle the UE |
Soft UE: Registration reject [95] |
3-digit MNC on a soft UE | use PLMN 00101 end-to-end |
| gNB relaunch → SSH exit 255, no output | pkill -f apps/gnb/gnb killed your shell |
use pkill -x gnb |
[RF] … underflow, UE CQI 1–2 |
NIC IRQs on the PHY cores | ensure ocudu-irq-affinity.service is active |
gNB aborts: free_buff_pool::pop … Assertion |
USRP streaming wedged (shared radio switched stacks) | omf tell -a reset the USRP host, then one clean start |
Modem detects the cell but won't camp (SEARCH) |
nr5g_disable_mode=1 disables SA on RM520N-GL |
set nr5g_disable_mode to 0; lock AT+COPS=1,2,"00101",12 |
UE ping 8.8.8.8 fails, gateway pings |
no internet NAT | open5gs/start.sh adds MASQUERADE -s 192.168.100.0/22 ! -o ogstun |
lteue aborts: Someone tried to claim this device again |
previous lteue didn't release the USRP claim |
wait ~20 s after pkill (or use SIGTERM, not -9) before relaunch |
Modem registered, PDU up, IP on wwan0, but 100 % ping loss both ways; the gNB never sees a scheduling request |
the modem's USB data endpoint is stuck (-EPROTO): ip -s link show wwan0 shows TX errors rising by one per packet and TX packets 0 |
USB device reset of the modem (a qmi_wwan rebind is not enough); the bundle's ocudu-ota_modem.py configure does it automatically, ocudu-ota_modem.py usbheal wwan0 on its own |
Ansible stops at the preflight: OMF_NODES selected 1 node(s) … but this tutorial needs exactly 2 |
omf stat reports one node as Unknown (UNREACHABLE) — its BMC is not answering — and cosmos-inventory.py only lists POWERON nodes, so it silently drops it even though it answers ssh |
report the node to the testbed admins; meanwhile run with a static inventory — ANSIBLE_INVENTORY=~/static-inv.yml with a YAML group nodes listing the gNB host first (see below) |
Ansible never reaches NG Setup, or the other modes stall after DL (UL iperf3 … Connection timed out) |
the gNB host still carries isolcpus from an O-RAN 7.2 run, so this gNB gets only the non-isolated cores |
re-load ocudu.ndz or restore the GRUB command line and reboot; /proc/cmdline must show no isolcpus |
| One mode's DL test runs much longer than 10 s at a low rate and its UL test then cannot connect, while the other modes are fine | a real-time slip on the gNB host (Real-time failure in FAPI: Received late DL_TTI.request, Downlink data late in the gNB log) — timing, not the modem (wwan0 TX errors stay 0) |
re-run the parity; keep other load off the gNB host |
A manual AT session right after the bundle gets device reports readiness to read but returned no data |
the teardown hands the modem back to ModemManager, which then holds the AT ports | systemctl stop ModemManager first (the bundle's modes do this themselves) |
Static inventory for the UNREACHABLE case (first host = gNB host, second = UE host):
# ~/static-inv.yml
all:
children:
nodes:
hosts:
srv1-lg1.sb1.cosmos-lab.org:
sdr1-in3.sb1.cosmos-lab.org:
vars:
ansible_user: root
cd ~/tutorials && ANSIBLE_INVENTORY=~/static-inv.yml ./ocudu-ota/ocudu-ota_parity.sh srv1-lg1 sdr1-in3
srsue + Open5GS over a ZMQ virtual radio on one node — no USRP. See the OCUDU soft-only tutorial.gnb-ocudu-40-l3.yml (recv/send_frame_size 7900) and ip route replace 10.39.0.0/16 via <data-gw> dev <data-iface> so the USRP subnet never falls back to the control net.omf-ocudu; srsRAN-Project YAML — skill ocudu-gnb (docs.srsran.com).open5gs-core; Quectel modem — skill quectel-5g-modem.Author(s): COSMOS team · Last verified: 2026-09-16 (three-mode parity on sb1, modem path; first verified 2026-07-15) · Tested image/release: ocudu.ndz (OCUDU srsRAN + Open5GS) + lte5gue.ndz, PLMN 00101, pool 192.168.100.0/22 · Bundle: cosmos-tutorials-ocudu-ota.zip · Tags: ocudu, srsran, open5gs, 5g, sa, sdr, usrp, quectel, lteue, cross-vendor