RENEW Project Documentation

Version 1.0

Reconfigurable Ecosystem for Next-generation End-to-end Wireless

SISO TX/RX TDD Example

Here we present a complete SISO TX/RX TDD example and describe every segment of the code step by step. Notice the main function is all the way at the end.

This script is useful for testing the TDD operation. It programs two Irises in TDD mode with the following framing schedule:

  • Iris 1: PGRG
  • Iris 2: RGPG

where P means a pilot or a pre-loaded signal, G means Guard band (no Tx or Rx action), R means Rx, and T means Tx, though not used in this script.

The above determines the operation for each frame and each letter determines one symbol. Although every 16 consecutive frames can be scheduled separately. The pilot signal in this case is a sinusoid which is written into FPGA buffer (TX_RAM_A & TX_RAM_B for channels A & B) before the start trigger.

The script programs the Irises in a one-shot mode, i.e. they run for just one frame. This means that each frame starts with a separate trigger. After the end of the frame, the script plots the two Rx symbols which are supposedly what each of the Iris boards received from each other (as shown in the schedule above).

Usage example:

python3 SISO_TXRX_TDD.py --serial1="RF3C000042" --serial2="RF3C000025"

Code

Import libraries:

import sys
sys.path.append('../IrisUtils/')

import SoapySDR
from SoapySDR import *  # SOAPY_SDR_ constants
from optparse import OptionParser
import numpy as np
import time
import os
import math
import json
import matplotlib.pyplot as plt
from cfloat2uint32 import *
from uint32tocfloat import *

Specify FPGA registers. Do NOT change these:

#########################################
#                Registers              #
#########################################
# TDD Register Set
RF_RST_REG = 48
TDD_CONF_REG = 120
SCH_ADDR_REG = 136
SCH_MODE_REG = 140
TX_GAIN_CTRL = 88

Core function. We start by instantiating both the Base Station and Mobile (UE) Iris devices. Then for both TX and RX, we configure some RF parameters such as: rate, RF/BB frequencies, and gains. Via the writeSetting command we disable transmit gain control, and measure the delays between all Iris boards in the chains so they can be used in future operations.

#########################################
#              Functions                #
#########################################
def siso_tdd_burst(serial1, serial2, rate, freq, txgain, rxgain, numSamps, prefix_pad, postfix_pad):
    bsdr = SoapySDR.Device(dict(driver='iris', serial=serial1))
    msdr = SoapySDR.Device(dict(driver='iris', serial=serial2))

    # Some default sample rates
    for i, sdr in enumerate([bsdr, msdr]):
        info = sdr.getHardwareInfo()
        print("%s settings on device %d" % (info["frontend"], i))
        for ch in [0]:
            sdr.setSampleRate(SOAPY_SDR_TX, ch, rate)
            sdr.setSampleRate(SOAPY_SDR_RX, ch, rate)
            #sdr.setFrequency(SOAPY_SDR_TX, ch, freq)
            #sdr.setFrequency(SOAPY_SDR_RX, ch, freq)
            sdr.setFrequency(SOAPY_SDR_TX, ch, 'RF', freq-.75*rate)
            sdr.setFrequency(SOAPY_SDR_RX, ch, 'RF', freq-.75*rate)
            sdr.setFrequency(SOAPY_SDR_TX, ch, 'BB', .75*rate)
            sdr.setFrequency(SOAPY_SDR_RX, ch, 'BB', .75*rate)

            sdr.setGain(SOAPY_SDR_TX, ch, txgain)
            sdr.setGain(SOAPY_SDR_RX, ch, rxgain)

            sdr.setAntenna(SOAPY_SDR_RX, ch, "TRX")
            sdr.setDCOffsetMode(SOAPY_SDR_RX, ch, True)

    # TX_GAIN_CTRL and SYNC_DELAYS
    msdr.writeRegister("IRIS30", TX_GAIN_CTRL, 0)
    bsdr.writeSetting("SYNC_DELAYS", "")
Compute total number of samples (including any prefix+postfix padding), and generate a 100kHz sinusoid for transmissions (*pilot1*). Since we are only transmitting from one RF chain, we essentially write all zeros to the other RF chain.
    # Packet size
    symSamp = numSamps + prefix_pad + postfix_pad
    print("numSamps = %d" % numSamps)
    print("symSamps = %d" % symSamp)

    # Generate sinusoid to be TX
    Ts = 1 / rate
    s_freq = 1e5
    s_time_vals = np.array(np.arange(0, numSamps)).transpose()*Ts
    pilot = np.exp(s_time_vals*1j*2*np.pi*s_freq).astype(np.complex64)*1
    pad1 = np.array([0]*prefix_pad, np.complex64)
    pad2 = np.array([0]*postfix_pad, np.complex64)
    wbz = np.array([0]*symSamp, np.complex64)
    pilot1 = np.concatenate([pad1, pilot, pad2])
    pilot2 = wbz
We initialize the arrays that will be used to store the received signals on both RF chains of both boards. Then we begin the RX streaming process by setting up the RX streams on both the Base Station and the Mobile. We select a 16-bit complex short integer format and enable both RF chains.
    # Initialize RX arrays
    waveRxA1 = np.array([0]*symSamp, np.uint32)
    waveRxB1 = np.array([0]*symSamp, np.uint32)
    waveRxA2 = np.array([0]*symSamp, np.uint32)
    waveRxB2 = np.array([0]*symSamp, np.uint32)

    # Create RX streams
    # CS16 makes sure the 4-bit lsb are samples are being sent
    rxStreamB = bsdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CS16, [0, 1])
    rxStreamM = msdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CS16, [0, 1])
In this script we are using the TDD framer, therefore, we specify the TDD schedule to be used by both boards. Notice we first send a pilot from the Base Station to the Mobile, we add a guard symbol where there is no TX/RX action, then we send a pilot in the opposite direction followed by another guard interval. After specifying the schedule to be used, we write the TDD configuration via the *writeSetting* command. For more information on the parameters in the configuration, see this [basic tutorial](../tutorial). Due to the differences between the data path and the control path going into the RF frontend, we need to add a fixed delay to the control path (*TX_SW_DELAY* setting) before switching to TX mode. Notice we also need to specify we are using the TDD mode.
    # Set Schedule
    bsched = "PGRG"
    msched = "RGPG"
    print("Node 1 schedule %s " % bsched)
    print("Node 2 schedule %s " % msched)
    # Send one frame (set mamx_frame to 1)
    bconf = {"tdd_enabled": True, "frame_mode": "free_running", "symbol_size": symSamp, "frames": [bsched], "max_frame": 1}
    mconf = {"tdd_enabled": True, "frame_mode": "free_running", "dual_pilot": False, "symbol_size": symSamp, "frames": [msched], "max_frame": 1}
    bsdr.writeSetting("TDD_CONFIG", json.dumps(bconf))
    msdr.writeSetting("TDD_CONFIG", json.dumps(mconf))
    
    # SW Delays
    for sdr in [bsdr, msdr]:
        sdr.writeSetting("TX_SW_DELAY", str(30))

    msdr.writeSetting("TDD_MODE", "true")
    bsdr.writeSetting("TDD_MODE", "true")
For transmission, we are loading our *pilot1* (sinusoid) and *pilot2* (all zeros) signals onto RAM. That is, we write these to RAM A for RF chain A, and RAM B for RF chain B, in both boards. Notice we are first converting the floating point samples to *uint32* and the sample order should be *QI*:
    replay_addr = 0
    for sdr in [bsdr, msdr]:
        sdr.writeRegisters("TX_RAM_A", replay_addr, cfloat2uint32(pilot1, order='QI').tolist())
        sdr.writeRegisters("TX_RAM_B", replay_addr, cfloat2uint32(pilot2, order='QI').tolist())
After setting up the RX streams, we activate these. Then we generate the trigger that will initiate the pilot transmission from the Base Station:
    flags = 0
    r1 = bsdr.activateStream(rxStreamB, flags, 0)
    r2 = msdr.activateStream(rxStreamM, flags, 0)
    if r1<0:
        print("Problem activating stream #1")
    if r2<0:
        print("Problem activating stream #2")

    bsdr.writeSetting("TRIGGER_GEN", "")
Read RX streams on both RF chains (A and B) of both Iris boards (msdr and bsdr):
    r1 = msdr.readStream(rxStreamM, [waveRxA1, waveRxB1], symSamp)
    print("reading stream #1 ({})".format(r1))
    r2 = bsdr.readStream(rxStreamB, [waveRxA2, waveRxB2], symSamp)
    print("reading stream #2 ({})".format(r2))
 
    print("printing number of frames")
    print("UE {}".format(SoapySDR.timeNsToTicks(msdr.getHardwareTime(""), rate)))
    print("NB {}".format(SoapySDR.timeNsToTicks(bsdr.getHardwareTime(""), rate)))
Cleanup... Reset some of the FPGA registers and deactivate/close the RX streams on both boards:
    # ADC_rst, stops the tdd time counters, makes sure next time runs in a clean slate
    tdd_conf = {"tdd_enabled" : False}
    for sdr in [bsdr, msdr]:
        sdr.writeSetting("RESET_DATA_LOGIC", "")
        sdr.writeSetting("TDD_CONFIG", json.dumps(tdd_conf))
        sdr.writeSetting("TDD_MODE", "false")

    msdr.deactivateStream(rxStreamM)
    bsdr.deactivateStream(rxStreamB)
    msdr.closeStream(rxStreamM)
    bsdr.closeStream(rxStreamB)
    msdr = None
    bsdr = None
Create a figure with two subplots. One subplot shows the RX signal captured by the Base Station node and the other subplot shows the RX signal captured by the Mobile node:
    fig = plt.figure(figsize=(20, 8), dpi=120)
    fig.subplots_adjust(hspace=.5, top=.85)
    ax1 = fig.add_subplot(2, 1, 1)
    ax1.grid(True)
    ax1.set_title('Serials: (%s, %s)' % (serial1, serial2))
    ax1.set_ylabel('Signal (units)')
    ax1.set_xlabel('Sample index')
    ax1.plot(range(len(waveRxA1)), np.real(uint32tocfloat(waveRxA1)), label='ChA I Node 1')
    ax1.plot(range(len(waveRxB1)), np.real(uint32tocfloat(waveRxB1)), label='ChB I Node 1')
    ax1.set_ylim(-1, 1)
    ax1.set_xlim(0, symSamp)
    ax1.legend(fontsize=10)
    ax2 = fig.add_subplot(2, 1, 2)
    ax2.grid(True)
    ax2.set_ylabel('Signal (units)')
    ax2.set_xlabel('Sample index')
    ax2.plot(range(len(waveRxA2)), np.real(uint32tocfloat(waveRxA2)), label='ChA I Node 2')
    ax2.plot(range(len(waveRxB2)), np.real(uint32tocfloat(waveRxB2)), label='ChB I Node 2')
    ax2.set_ylim(-1, 1)
    ax2.set_xlim(0, symSamp)
    ax2.legend(fontsize=10)
    plt.show()
Main function. We simply parse the arguments provided when running the script and call the core function *siso_tdd_burst()*:
#########################################
#                  Main                 #
#########################################
def main():
    parser = OptionParser()
    parser.add_option("--serial1", type="string", dest="serial1", help="serial number of the device 1", default="")
    parser.add_option("--serial2", type="string", dest="serial2", help="serial number of the device 2", default="")
    parser.add_option("--rate", type="float", dest="rate", help="Tx sample rate", default=5e6)
    parser.add_option("--txgain", type="float", dest="txgain", help="Optional Tx gain (dB)", default=25.0)  # w/CBRS 3.6GHz [0:105], 2.5GHZ [0:105]
    parser.add_option("--rxgain", type="float", dest="rxgain", help="Optional Tx gain (dB)", default=20.0)  # w/CBRS 3.6GHz [0:105], 2.5GHZ [0:108]
    parser.add_option("--freq", type="float", dest="freq", help="Optional Tx freq (Hz)", default=2.6e9)
    parser.add_option("--numSamps", type="int", dest="numSamps", help="Num samples to receive", default=512)
    parser.add_option("--prefix-pad", type="int", dest="prefix_length", help="prefix padding length for beacon and pilot", default=82)
    parser.add_option("--postfix-pad", type="int", dest="postfix_length", help="postfix padding length for beacon and pilot", default=68)
    (options, args) = parser.parse_args()
    siso_tdd_burst(
        serial1=options.serial1,
        serial2=options.serial2,
        rate=options.rate,
        freq=options.freq,
        txgain=options.txgain,
        rxgain=options.rxgain,
        numSamps=options.numSamps,
        prefix_pad=options.prefix_length,
        postfix_pad=options.postfix_length,
    )


if __name__ == '__main__':
    main()
<hr>
Last updated on 7 Dec 2019 / Published on 12 Feb 2019