This repository contains my work-in-progress SDR implementation of DECT NR+ (ETSI TS 103 636, Part 1 to 5) with the following features:
-
Extensive: supports all values of
$\mu$ ,$\beta$ and NTX - Extensible: enables custom firmware based on slim interfaces to the PHY and application layer
-
Fast: supports low latencies (<250
$\mu s$ ) and high data rates (>100$Mbps$ ) - Reliable: supports MIMO (transmit diversity, beamforming etc.) for diversity combining and SINR maximization
DECT NR+ is a non-cellular radio standard and part of 5G as defined by ITU-R. Introductions are available at ETSI, DECT Forum and Wikipedia. While commonly referred to as DECT NR+, the standard's official designation is DECT-2020 New Radio (NR). The name of the project in this repository is DECTNRP.
- Core Idea
- Directories
- Installation
- Starting
- Contributing
- Citation
- Limitations of an SDR with Host Processing
- Known Issues
- Future Work
- Troubleshooting
Advanced Topics
- Architecture
- AGC
- TX/RX Delay Calibration
- Resampling
- Synchronization
- Compatibility
- JSON Export
- PPS Export and PTP
- Performance Tuning
- Firmware
- Interesting Links
The core idea of the SDR is to provide a basis to write custom DECT NR+ firmware. In terms of the OSI model, the firmware is located on the MAC layer and the layers above as shown in Architecture. The SDR provides:
- Radio layer (typically part of PHY, here separate layer)
- PHY
- PHY to MAC layer interface
- Application layer interface (e.g. UDP sockets, virtual NIC).
Custom DECT NR+ firmware is implemented by deriving from the class tpoint_t and implementing its virtual functions. The abbreviation tpoint stands for termination point which is DECT terminology and simply refers to a DECT NR+ node. There are multiple firmware examples in the directory upper/ with a brief description of each available under Firmware. For instance, the termination point firmware (tfw) tfw_basic_t provides the most basic firmware possible. It derives from tpoint_t and leaves all virtual functions mostly empty. The full list of virtual functions is:
| Virtual Function | Properties | |
|---|---|---|
| 1 | work_start() | called once immediately before PHY starts processing IQ sample |
| 2 | work_regular() | called regularly (polling) |
| 3 | work_irregular() | called irregularly based on requests of the firmware (event-driven) |
| 4 | work_pcc() | called upon PCC reception with correct CRC (event-driven) |
| 5 | work_pcc_error() | called upon PCC reception with incorrect CRC (event-driven, optional) |
| 6 | work_pdc() | called upon PDC reception with correct CRC (event-driven, asynchronous) |
| 7 | work_pdc_error() | called upon PDC reception with incorrect CRC (event-driven, asynchronous) |
| 8 | work_application() | called upon availability of new data on application layer (event-driven) |
| 9 | work_channel() | called upon a finished channel measurement (event-driven, asynchronous) |
| 10 | work_stop() | called once when the SDR must shut down |
For every firmware, constructors are always called first. When the constructors are called, underlying devices on the radio layer as well as the PHY have already been initialized, and thus hardware properties such as center frequency and gains may be changed. However, the radio devices are not streaming IQ samples yet.
After all constructors have been called, work_start() is called to announce the imminent beginning of IQ streaming. Only then all other work-functions are called. For event-driven functions, calls are only made if and when the associated event occurs. Once the SDR receives a signal triggered by pressing ctrl+c, work_stop() is called and the running firmware must stop execution such that the SDR can shut down.
├─ .devcontainer/ docker container setup in VSCode
├─ .vscode/ VS Code settings
├─ apps/ apps sources
├─ bin/ apps post-compilation binaries
├─ cmake/ CMake modules
├─ configurations/ configuration files required to start the SDR
├─ docs/ documentation (doxygen, graphics etc.)
├─ external/ external libraries
├─ gnuradio/ flow graphs (SDR oscilloscope, USRP calibration etc.)
├─ json/ submodule to analyze exported JSON files in Matlab
├─ lib/ library code used by applications
│ ├─ include/dectnrp/
│ | ├─ application/ application layer interfaces
│ | ├─ apps/ utilities for apps in directory apps/
│ | ├─ common/ common functionality across all layers/directories
│ | ├─ cvg/ convergence layer
│ | ├─ dlc/ DLC layer
│ | ├─ mac/ MAC layer
│ | ├─ phy/ physical layer
│ | ├─ radio/ radio layer
│ | ├─ sections_part2/ sections of ETSI TS 103 636-2
│ | ├─ sections_part3/ sections of ETSI TS 103 636-3
│ | ├─ sections_part4/ sections of ETSI TS 103 636-4
│ | ├─ sections_part5_cvg/ sections of ETSI TS 103 636-5
│ | ├─ sections_part5_dlc/ sections of ETSI TS 103 636-5
│ | ├─ simulation/ wireless simulation
│ | ├─ upper/ upper layers, i.e. layers between PHY and application layer
│ ├─ src/ source code (same directories as in lib/include/dectnrp/)
└─ scripts/ shell scripts
The SDR has been tested on Ubuntu 22.04 and 24.04 (Intel and AMD x86-64) and Raspberry Pi OS (Raspberry Pi 5). It has four dependencies that must be installed:
Installation instructions for these dependencies can be found in install_dependencies.sh. After installing the dependencies, the SDR can be either downloaded and compiled (GCC >=13.3.0, Clang >=14.2.0) with install_sdr.sh, or manually with:
git clone --recurse-submodules https://github.com/maximpenner/DECT-NR-Plus-SDR
cd DECT-NR-Plus-SDR
mkdir build
cd build
cmake ..
make -jThe compiled code is kept local in bin/ and not copied to any operating system directories such as /usr/local/.
To start the SDR, the main executable dectnrp must be invoked. It is located in bin/ after compilation.
cd bin/
sudo ./dectnrp "../configurations/basic_simulator/"The executable dectnrp requires exactly one argument, which is a path to a directory containing three configuration files radio.json, phy.json and upper.json as exemplified in configurations/. Each configuration file configures its respective layer(s). In the case of upper.json, this also implies the name of the firmware to load. The SDR is stopped by pressing ctrl+c.
The configuration files may also contain multiple instances of the SDR. In fact, each configuration file configures its layer(s) by listing one or multiple layer units:
radio.json: list of hardwares hw_t (e.g. USRP or simulator), details in hw_config_tphy.json: list of worker pools worker_pool_t, details in worker_pool_config_tupper.json: list of termination points tpoint_t, details in tpoint_config_t
If radio.json and phy.json contain multiple layer instances, each trio of tpoint_t, worker_pool_t and hw_t is either started independently (center figure), or a single tpoint_t controls multiple duos of worker_pool_t and hw_t (right figure).
Furthermore, a set of configuration files is not bound to a specific firmware. By replacing the firmware name in upper.json, the configuration files can be combined with a different firmware. It is up to the firmware to verify at runtime whether the applied configuration is compatible.
Submit a pull request and keep the same licence.
If you use this repository for any publication, please cite the repository according to CITATION.cff.
All processing of IQ samples and all decisions about the radio device state (gains, frequencies etc.) are made on the host. The host exchanges IQ samples and the radio device state with the radio device over an interface (100/10/1GbE, PCIe, USB etc.) which imposes a certain latency depending on the interface type and properties (Ethernet ideally less than 100
- The time between receiving a packet and sending a response packet is comparatively large and latencies such as for IEEE 802.11 (e.g. SIFS of 16
$\mu s$ or less) are not feasible. - Quasi-instantaneous channel access with prior Listen-Before-Talk (LBT) is not possible. By the time the host has measured the channel as free and the TX packet arrives at the radio device, the channel measurement may already be stale.
- The AGC is slow. By the time the host has made the decision to change a gain setting, the channel conditions may have already changed significantly.
Furthermore, most SDRs are general-purpose devices with limitations regarding typical hardware parameters such as output power, linearity, receiver sensitivity, noise figure, selectivity etc.
- The channel coding requires verification. It is based on srsRAN 4G with multiple changes, for instance, an additional maximum code block size
$Z=2048$ . Furthermore, channel coding with a limited number of soft bits is not implemented yet. - MAC messages and information elements (MMIEs) in the standard are subject to frequent changes. Previously completed MMIEs are currently being revised and will be updated soon.
- For some combinations of operating system, CPU and DPDK, pressing ctrl+c does not stop the SDR. The SDR process must then be stopped manually.
- In an earlier version of the standard, the number of transmit streams was signaled by a cyclic rotation of the STF in frequency domain. This functionality will be kept for the time being. In the current version of the standard, the number of transmit streams in a packet must be tested blindly.
- GPIO support for USRP B-series
- continuous TX mode for USRP B-series
- integrate SoapySDR
- add virtual space breakpoint to stop IQ samples exchange
- add tx_order_id overwrite without buffer_tx_t
- test faster AGC gain changes, Fast Gain Switching on TwinRX USRPs
-
$\mu$ detection - integer CFO
-
increase look-ahead of sync_chunk_t (increases latency) - add parallel queues in job_queue_t for asynchronous jobs
-
make job queue lockable by producers to enqueue multiple jobs (weakens encapsulation as producers must lock/unlock the entire queue) - residual STO based on DRS
- residual CFO based on STF
- residual CFO based on DRS
- AoA estimation based on DRS
- MIMO modes with two or more spatial streams
- 1024-QAM
- Crest factor reduction and predistortion
- integration of retransmissions with HARQ into firmware p2p to finalize interfaces
- reusable firmware procedures (association etc.)
- DLC and Convergence layers
- enhanced application layer interfaces to DECT NR+ stack (ingress/egress filtering, blocking etc.)
- plugin system for out-of-tree firmware
- mutex/spinlock around individual datagrams instead of entire queue
- outsource datagram copying to instances of worker_tx_tx_t instead of copying when the firmware is locked
- If combining large bandwidths (
$\beta$ >= 8), a large number of antennas (NTX >= 4) and resampling, synchronization may not be able to process IQ samples fast enough across all antennas. In that case, the program may stop abruptly once the write pointer of the IQ ring buffer catches up with the read pointer. To prevent this from happening, the number of antennas utilized by synchronization can be reduced in sync_param.hpp by settingRX_SYNC_PARAM_AUTOCORRELATOR_ANTENNA_LIMITto either 1, 2 or 4. - If there are occasional packet losses, oversampling in
phy.jsoncan be increased. This is particularly helpful if resampling is used. Downside is that the nominal bandwidth is increased while the actual signal bandwidth remains the same. - If the SNR is low despite a high receive power, resampling has to be tuned. To rule out any other causes, resampling may also be deactivated for testing purposes.
- DPDK and SDR threads should run on separate cores.
- For the best possible performance in terms of PER, the spectrum the SDR operates in should be interference-free.
- The script rts.sh must be executed to fix this UHD issue.
The figure below illustrates the architecture of the SDR with blocks representing C++ classes, objects and threads. All types (postfix _t) have identical names in the source code.
The key takeaways are:
- The radio layer uses a single RX ring buffer of type buffer_rx_t to distribute IQ samples to all workers. For TX, it uses multiple independent buffer_tx_t instances from a buffer_tx_pool_t. The number of buffers is defined in
radio.json, and their size by the radio device class and the oversampling inphy.json. - The PHY has workers for synchronization (worker_sync_t) and workers for packet encoding/decoding and modulation/demodulation (worker_tx_rx_t). The number of workers, their CPU affinity and priority are set in
phy.json. Both worker types communicate through a MPMC job_queue_t. - When synchronization detects a DECT NR+ packet, it creates a job with a sync_report_t, which is then processed by instances of worker_tx_rx_t. During packet processing, these workers call the firmware through the virtual work_*() functions described in Core Idea. Access to the firmware is thread-safe as each worker has to acquire a token_t. All jobs are processed in the same order as they are inserted into the queue.
- Synchronization also creates regular jobs with a regular_report_t. Each of these jobs contains a time update for the firmware, and the starting time of the last known packet. The rate of regular jobs depends on how processing of buffer_rx_t is split up between instances of worker_sync_t in
phy.json. A possible rate is one job each two slots, equivalent to 1200 jobs per second. - Synchronization also creates irregular jobs with an irregular_report_t containing the same information as a regular_report_t. A firmware must request an irregular_report_t for a future point in time. Once this time has passed for synchronization, the respective job is created and the firmware is called as soon as possible.
- The firmware of the SDR is not executed in an independent thread. Instead, the firmware is equivalent to a thread-safe state machine whose state changes are triggered by calls of the work_*() functions. The type of firmware executed is defined in
upper.json. - Each firmware starts and controls its own application layer interface (application_server_t and application_client_t). To allow immediate action for new application layer data, application_server_t is given access to job_queue_t. The job type created by application_server_t contains an application_report_t with the number and size of datagrams available on the application layer.
- The application layer interface is either a set of UDP ports or a virtual NIC.
An ideal fast AGC receives a packet and adjusts its gain settings within a fraction of the STF (e.g. the first two or three patterns). However, as the SDR performs all processing exclusively on the host computer, only a slow software AGC is feasible, which, for example, adjusts gains 50 times per second.
In general, it is best for the FT to keep both transmit power and sensitivity constant, and only for the PT to adjust its own transmit power and sensitivity. The objective of the PT is to achieve a specific uplink receive power at the FT, such that all PTs are received with similar power levels. Moreover, every AGC that is leveled to the STF of a beacon should leave a margin of approx. 10
With a slow AGC, it is common for two consecutive packets to be received with identical gain settings, which can lead to packet masking. This happens when a first packet is received with high input power, followed immediately by a second packet with low input power, for example in a near–far scenario. Both packets are separated only by a guard interval (GI). Since synchronization is based on correlations of the length of the STF, and the STF for
| GI length in |
STF length in |
|
|---|---|---|
| 1 | 18.52 | 64.81 |
| 2 | 20.83 | 41.67 |
| 4 | 10.42 | 20.83 |
| 8 | 10.42 | 10.42 |
AGC settings must be timed to avoid gain changes during packet transmission or reception, as this can lead to packet errors. Potential timing points for AGC adjustments are the GI at the end of every packet as shown in the figure below, or asynchronously any point in time at which a firmware is neither sending nor receiving. With USRPs, queueing gain changes for multiple antennas may lead to the second packet being sent late, i.e., the time span X not being zero.
If a packet is scheduled for transmission at a specific time in the future, the packet is effectively sent several samples later, as various delay-afflicted processes (upsampling, filtering, etc.) are performed in the radio hardware. At low sample rates, a few samples add up to several microseconds.
The exact delay depends on the sample rate, radio device, software version etc. and must be measured individually. This can be achieved with the firmware txrxdelay. It regularly sends packets which inevitably leak into the RX path and then compares the TX and RX time. The measured delay can then be specified in radio.json as tx_time_advance_samples. Each packet will be transmitted earlier to compensate for the delay.
Exemplary measurements:
| Device | DECT NR+ BW in |
Sample Rate in |
Delay in Samples | Delay in |
|---|---|---|---|---|
| X410 | 13.824 | 15.36 | 68 | 4.43 |
| X410 | 13.824 | 30.72 | 79 | 2.57 |
| X410 | 13.824 | 61.44 | 105 | 1.71 |
| B210 | 1.728 | 1.728 | 47 | 27.20 |
| B210 | 1.728 | 3.456 | 53 | 15.34 |
| B210 | 1.728 | 6.912 | 68 | 9.84 |
Most SDR devices support a limited set of sample rates, which typically do not match the DECT NR+ base rate of
-
$1.728 MSs^{-1}$ without oversampling is resampled to$30.72 MSs^{-1} / 16 = 1.92 MSs^{-1}$ -
$1.728 MSs^{-1}$ with oversampling by 2 is resampled to$30.72 MSs^{-1} / 8 = 3.84 MSs^{-1}$ -
$27.648 MSs^{-1}$ with oversampling by 2 is resampled to$30.72 MSs^{-1} \cdot 2 = 61.44 MSs^{-1}$
Fractional resampling is computationally expensive. To reduce the computational load and enable larger bandwidths, the implicit FIR low-pass filter of the resampler can be made shorter. However, this also leads to more aliasing and thus to a larger EVM at the receiver. The resampler parameters are defined in resampler_param.hpp.
Another option is to disable resampling entirely and generate DECT NR+ packets directly at "enforce_dectnrp_samp_rate_by_resampling": false in phy.json.
Synchronization of packets based on the STF is described in DECT-NR-Plus-Link-Level-Simulation. According to the DECT NR+ standard, the STF cover sequence can be disabled for testing purposes. In the SDR, this is done by changing the following line in stf_param.hpp and recompiling:
// #define SECTIONS_PART_3_STF_COVER_SEQUENCE_ACTIVEFor the SDR, synchronization is more reliable if no cover sequence is applied to the STF. This is due to the coarse metric without a cover sequence being wider, and thus harder to miss.
Packets generated with the SDR are compatible with the MATLAB code in DECT-NR-Plus-Link-Level-Simulation. The SDR also has been tested with commercially available DECT NR+ solutions.
Each worker pool can export JSON files with information about received DECT NR+ packets. The export is activated by changing the value of "json_export_length" in phy.json to 100 or higher, i.e. the worker pool collects information of at least 100 packets before exporting all in a single JSON file. To enable the export without jeopardizing the real-time operation of the SDR, the worker pool must have at least two instances of worker_tx_rx_t. This way, at least one worker can continue processing IQ samples while another worker saves a JSON file.
Exported files can be analyzed with DECT-NR-Plus-SDR-JSON.
With the GPIOs of an USRP, the SDR can create pulses that are synchronized to DECT NR+ events, for instance the beginning of a wirelessly received beacon. The generation of pulses is controlled by each firmware individually. The example firmware p2p contains logic to export one pulse per second (PPS).
A PPS signal itself is a frequently used clock for other systems. For example, a Raspberry Pi can be disciplined with a PPS and at the same time act as a PTP source which itself operates synchronously with the PPS (pi5-pps-ptp). Such a PTP source can then be used to discipline further systems:
- The SDR's host computer can synchronize to PTP such that the operating system time and the USRP sample count run synchronously.
- The PPS is stable enough to allow deriving higher clock speeds, for instance 48kHz for distributed wireless audio applications.
- The SDR can also be synchronized to an existing PTP network by converting PTP to a 10MHz and 1PPS signal, and feeding these to the USRP.
When a complete DECT NR+ network is synchronized as shown above, the first component of TSN, namely time synchronization, is established. The second component, scheduling and traffic shaping, can be implemented under Linux using qdiscs which feed IP packets to the SDR through a VNIC. The third component, selection and reservation of communication paths, is provided by the SDR via firmware as scheduled uplink and downlink radio resources between different addressable SDRs.
The following tuning tips have been tested with Ubuntu and help achieving low-latency real-time performance.
- SDR threads with elevated thread priority through threads_core_prio_config_t
- SDR threads on isolated CPU cores (with disabled interrupts) through threads_core_prio_config_t
- Disabled CPU sleep states and CPU frequency scaling
- Low-latency kernel
- DPDK with UHD
-
Adjusted send_frame_size and recv_frame_size in
radio.json - Increased buffer sizes
- In each
radio.json, the value“turnaround_time_us”defines how soon the SDR can schedule a packet transmission relative to the last known SDR timestamp. For UHD, the SDR time is a 64-bit counter in the FPGA. The smaller the turn around time, the lower the latency. Under optimal conditions, between$80\mu s$ to$150\mu s$ are possible.
The use of spinlocks and/or busy waiting requires proper planning of CPU affinity and priority settings for each thread.
Mutex or Spinlock: Mutexes are typically held for a very short period of time, so switching to a spinlock has little impact on CPU usage but may improve latency.
Condition Variable or Busy Waiting: Switching from a condition variable to busy waiting will lead to one or multiple CPU cores spinning at 100% usage.
- application_client.hpp
- baton.hpp
- job_queue_naive.hpp
- token.hpp
- buffer_rx.hpp
- buffer_tx.hpp
- watch.hpp (only additional busy wait methods)
The following firmware examples each demonstrate different capabilities of the SDR. Most examples have a small code base, only the firmware p2p is more complex demonstrating a full duplex IP packet pipe with PLCF feedback reporting and beamforming.
This is the smallest and simplest firmware possible. All virtual functions are empty except for a few asserts. This firmware uses a simulator on the radio layer, i.e. it does not require real radio hardware to be started. If a new firmware is written from scratch, a renamed copy of this firmware is the recommended starting point.
This firmware starts channel measurements in regular intervals and writes the result to the log file.
This is a firmware family. Each individual firmware is a simulation with a single device looping its TX signal back into its own RX path. It is used to test SDR functionality such as synchronization and packet error rates (PERs) over SNR. The wireless channel model can be switched in radio.json from an AWGN channel to a doubly selective Rayleigh fading channel.
This firmware demonstrates how to receive packets from nRF91 SiPs. It scans ACFN=1657 (see ETSI TS 103 636-2 for channel numbering) and writes data to the log file. The ACFN can be changed in tfw_nrf.cpp. Note that decoding the PDC requires the complete 32-bit network ID for scrambling on the PHY. It must either be hard-coded into the firmware or extracted from the PCC and MAC common header, specifically the beacon header.
The P2P (point-to-point) firmware is started on two separate host computers, each connected to an USRP (in this example an X410). One combination of host and USRP acts as a fixed termination point (FT), while the other is the portable termination point (PT). The FT is connected to the internet and once both FT and PT are started, the PT can access the internet through the wireless DECT NR+ connection acting as pipe for IP packets.
If a different USRP type is used, the value of “usrp_args” in radio.json must be modified accordingly. Furthermore, FT and PT must be tuned to a common center frequency. This is done by opening the following two files
and changing the following line in both files to the desired center frequency in Hz:
hw.set_freq_tc(3890.0e6);The CPU cores used may also require modification as both FT and PT use specific cores for their threads. For instance, in radio.json the thread handling received IQ samples is specified as:
"rx_thread_config": [0, 1]The first number 0 is the priority offset (or niceness), so here the thread is started with the highest priority 99 - 0 = 99. The second number 1 specifies the CPU core. Both numbers can also be negative, in which case the scheduler selects the priority and/or the CPU core. Further thread specifications can be found in all .json configuration files.
cd bin/
sudo ./dectnrp "../configurations/p2p_usrpX410/"Once the SDR is running, a TUN interface is instantiated which can be verified with ifconfig. To enable internet sharing at the FT, the interface to the internet is masqueraded:
cd scripts/
sudo ./masquerade.sh <Interface Name>In the file upper.json, the firmware is changed to:
"firmware_name": "p2p_pt"Then the SDR is started.
cd bin/
sudo ./dectnrp "../configurations/p2p_usrpX410/"Once the SDR is running, a TUN interface is instantiated which can be verified with ifconfig. To access the internet through that TUN interface, is it made the default gateway:
sudo ./defaultgateway_dns.sh -a 172.99.180.100 -i tun_pt_0On the PT, internet access should now happen through the DECT NR+ connection. This can be verified by running a speed test and observing the spectrum with a spectrum analyzer, or by checking the packet count in the log file in bin/.
This firmware tests the achievable round-trip time (RTT) between two instances of the SDR.
- UDP packets are generated by the program rtt in bin/ and send to the first SDR. By default, rtt is configured to send packets to localhost, so the binary must be started on the host computer of the first SDR.
- The first SDR transmits these packets wirelessly to the second SDR.
- The second SDR receives these packets and sends response packets ASAP.
- The first SDR receives the response packets and forwards them to the program rtt which finally measures the RTT.
- In case any packet is not received correctly, rtt has an integrated timeout before sending the next packet.
In the file upper.json of the second SDR, the firmware ID must be changed to:
"firmware_id": 1See AGC.




