RENEW Project Documentation

Version 1.0

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

Basic Tutorial

We present several code snippets showing some of the basic commands required to configure the Iris boards from the Python design flow.

Instantiate device

The SDR object for a particular Iris can be retrieved by using the board’s serial number:

serialtx = "RF3C000025"
sdr = SoapySDR.Device(dict(driver='iris', serial=serialtx))

Write/Read Registers

There are multiple operations that require reading and writing from/to the Iris FPGA registers. The writeRegister command with the parameter “IRIS30” allows the user to write the value tx_gain_ctrl_en to register number TX_GAIN_CTRL. In this specific case, we are disabling TX gain control by writing a zero to register 88:

# Disable TX gain control
TX_GAIN_CTRL = 88  # Register 88
tx_gain_ctrl_en = 0
sdr.writeRegister("IRIS30", TX_GAIN_CTRL, tx_gain_ctrl_en)

Similarly, we can read a given value from a register by specifying that register’s number. In this specific case, we are reading the measured RSSI from register 284:

# Disable TX gain control
rssi = sdr.readRegister("IRIS30", FPGA_IRIS030_RD_MEASURED_RSSI)

Define RF parameters:

The following commands allow users to set different RF parameters such as gains (at the LMS7 and RF frontend), sampling rate, carrier frequency, and enable/disable DC offset correction:

# Some default sample rates
freq = 2e9	# Tx Frequency (Hz)
rate = 5e6	# Tx Sample Rate
info = sdr.getHardwareInfo() 	# Collect information on RF frontend
for ch in [0, 1]:
    # RF chains 0 and 1
    sdr.setSampleRate(SOAPY_SDR_TX, ch, rate)
    sdr.setSampleRate(SOAPY_SDR_RX, ch, rate)
    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)
    if "CBRS" in info["frontend"]:
        sdr.setGain(SOAPY_SDR_TX, ch, 'ATTN', 0)  # [-18,0] by 3
        sdr.setGain(SOAPY_SDR_TX, ch, 'PA1', 15)  # [0|15]
        sdr.setGain(SOAPY_SDR_TX, ch, 'PA2', 0)   # [0|15]
        sdr.setGain(SOAPY_SDR_TX, ch, 'PA3', 30)  # [0|30]
    sdr.setGain(SOAPY_SDR_TX, ch, 'IAMP', 12)     # [0,12]
    sdr.setGain(SOAPY_SDR_TX, ch, 'PAD', -20)     # [-52,0]

    if "CBRS" in info["frontend"]:
        sdr.setGain(SOAPY_SDR_RX, ch, 'ATTN', 0)   # [-18,0]
        sdr.setGain(SOAPY_SDR_RX, ch, 'LNA1', 30)  # [0,33]
        sdr.setGain(SOAPY_SDR_RX, ch, 'LNA2', 17)  # [0,17]
    sdr.setGain(SOAPY_SDR_RX, ch, 'LNA', rxgain)   # [0,30]
    sdr.setGain(SOAPY_SDR_RX, ch, 'TIA', 0)        # [0,12]
    sdr.setGain(SOAPY_SDR_RX, ch, 'PGA', 0)        # [-12,19]
    sdr.setAntenna(SOAPY_SDR_RX, ch, "TRX")
    sdr.setDCOffsetMode(SOAPY_SDR_RX, ch, True)

Transmission Modes

Two transmission modes have been made available to users:

  • Streaming Mode: In this mode, the user generates a signal in software and streams it from the host to the Iris board before every single transmission.

  • Block RAM Transmission Mode: In this mode, the user generates a signal in software and loads it into Block RAM so that every time a transmission is triggered, the signal is simply sent directly from RAM to the LMS7 IC without having to continuously re-send (stream) from the host.

a) Streaming

Streaming (both TX/RX) is comprised of three different steps: a) setup stream, b) activate stream, and c) write/read stream:

  • setupStream requires us to specify the direction, format of the stream, and channels or RF chains we are using:

    • Direction: Specified via a constant indicating TX or RX, i.e., SOAPY_SDR_TX or SOAPY_SDR_RX.
    • Format: Depending on how the user wants to process the samples for their specific application, they can select 32-bit complex floats (SOAPY_SDR_CF32) or 16-bit complex short integers (SOAPY_SDR_CS16). Notice the FPGA expects a short integer format and any conversion to floating point is done by the SoapySDR driver.
    • RF chains: 0 for channel A and 1 for channel B, [0, 1] for both.
  • activateStream works differently for TX and RX:

    • TX: It only requires us to pass the stream object.
    • RX: In the case of receiving a stream, we not only pass the stream object but also a “flags” parameter, a time offset for scheduling the RX event (timeNs), and the number of samples we are reading. See below for more detailed information on the flags and event time parameters.
  • In writeStream we specify the stream object, the signal we are transmitting, the number of samples, flags, and TX event time (timeNs). Notice that this is opposite to what we do for the RX stream. That is, we provide these parameters using the activateStream command. See below for more detailed information on the flags and event time parameters.

  • In readStream we specify the RX stream object, as well as the variables where we will store the received samples, and the number of samples we are reading.

Flags and Event Time Parameters

For both TX and RX, we specify one or more flags to indicate characteristics such as whether we are transmitting/receiving a burst or continuous stream, whether the event will be triggered manually or after some time offset, etc. On the TX side, the user specifies the flags at each burst. This allows the user to operate differently on each burst, if desired. In contrast, on the RX side when the user activates the stream, a flag is specified for the entire stream until terminated.

The following are the primary flags available:

  • SOAPY_SDR_END_BURST - Indicate end of burst for transmit or receive. For write, end of burst if set by the caller. For read, end of burst is set by the driver. In the abscence of this flag, TX/RX will be continuous. If RX is set to continuous, the software is required to continuously read the buffer.

  • SOAPY_SDR_HAS_TIME - Indicates that the time stamp is valid. For write, the caller must set has time when timeNs is provided. For read, the driver sets has time when timeNs is provided. With this flag we are essentially specifying (via timeNs) a time in the future where once the FPGA counter reaches it, a TX/RX event is triggered.

  • SOAPY_SDR_WAIT_TRIGGER - The event (either TX or RX) will happen after a trigger. This trigger can come from user specified trigger in software (via the sdr.writeSetting(“TRIGGER_GEN”, “”) command) or from another board, if boards are chained. Notice that the event happens once the trigger arrives at the hardware after a non-constant delay.

  • SOAPY_SDR_ONE_PACKET - Indicates transmit or receive of only a single packet. Applicable when the driver fragments samples into packets. For write, the user sets this flag to only send a single packet. For read, the user sets this flag to only receive a single packet.

TX Streaming Example

txStream = sdr.setupStream(SOAPY_SDR_TX, SOAPY_SDR_CF32, [0, 1])
sr = sdr.readStream(rxStream, [waveRxA, waveRxB], numSamps) # this will provide the current timestamp
if sr.ret > 0: 	# If valid
    txTime = sr.timeNs & 0xFFFFFFFF00000000
    txTime += (0x000000000 + (startSymbol << 16))
for j in range(txSymNum):
    txTimeNs = txTime  # Specify a sufficiently large time offset in the future
    st = sdr.writeStream(txStream, [waveTxA, waveTxB], numSamps, flags, timeNs=txTimeNs)

RX Streaming Example

# Setup RX stream
rxStream = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CF32, [0, 1])
# Read samples into this buffer
num_samps = 2**14 # 16384 samples
sampsRx = [numpy.zeros(num_samps, numpy.complex64), numpy.zeros(num_samps, numpy.complex64)]
buff0 = sampsRx[0]  # RF Chain A
buff1 = sampsRx[1]  # RF Chain B
sdr.activateStream(rxStream,	# stream object
        SOAPY_SDR_END_BURST,    # flags
        0,                      # timeNs (don't care unless using SOAPY_SDR_HAS_TIME)
        buff0.size)             # numElems - this is the burst size
sr = sdr.readStream(rxStream, [buff0, buff1], buff0.size)

Deactivating and Closing Streams

The following code is used to terminate and close stream “rxStream” for sdr Iris board


b) Block RAM Transmission Mode:

One of the supported transmission modes allows users to generate a signal, write it to Block RAM in the FPGA, and simply trigger one or more transmissions without having to continuously stream the signals from the host to the Iris board. The following code snippet shows that we can use the writeRegisters command to write to either “TX_RAM_A” for RF chain A or “TX_RAM_B” for RF chain B, the values specified in the third argument. More specifically, in this case we first use the cfloat2uint32() function to convert pilot1 and pilot2 from a floating point value, to the type expected by the FPGA code, and write these values to the Block RAM specified in the first argument.

replay_addr specifies an offset within the memory block in case the user wants to write different types of signals into different segments of the same memory space. We also specify the order of the samples, IQ vs. QI (i.e., IQ when the In-phase component comes first, and the Quadrature component second, and vice versa). IQ is the default mode.

Notice the writeRegisters command is in plural here, compared to the writeRegister above (singular). These are two different functions. In order to write to Block RAM, the former command should be always used.

replay_addr = 0
sdr.writeRegisters("TX_RAM_A", replay_addr, cfloat2uint32(pilot1, order='IQ').tolist())
sdr.writeRegisters("TX_RAM_B", replay_addr, cfloat2uint32(pilot2, order='IQ').tolist())

To continuously transmit the signal that has been loaded into RAM, we can simply write the “TX_REPLAY” setting, and pass a string specifying the number of samples:

sdr.writeSetting("TX_REPLAY", str(number_samples))

TDD Framer

We have designed a Time-Division-Duplex-based framer. This framer provides a simple interface for users to specify a TX/RX schedule. Here we provide an example on how to use this functionality. In the figure below we present a schedule where an eNB (base station) initially transmits a pilot (P) to a user equipment (UE), then the UE replies with another pilot after a guard interval (GI) where there is no TX/RX activity, and finally the eNB transmits three consecutive sinusoids with guard intervals in between transmissions.

Pilots P consist of signals that have been pre-loaded into block RAM. On the other hand, the sinusoid we are transmitting during symbol T is streamed from the host. Notice the TDD framer does not restrict the user to any particular transmission mode.

The code snipped below shows how the schedule is specified for the eNB (bsched) and for the UE (msched). Then, we build a python dictionary structure that provides some configuration parameters for both the TX and RX:

  • tdd_enabled - Boolean to specify whether TDD is enabled or disabled.

  • frame_mode - String that indicates one of three frame counting modes:

    • free_running : immediately starts the next frame after current
    • triggered : waits for a trigger after current frame
    • continuous_resync : immediately starts but resyncs if triggered
  • symbol_size - Size of each symbol in the frame in terms of number of samples

  • dual_pilot - Indicates whether the frame includes two pilots, that is, whether we are sending one pilot per RF channel.

  • frames - Each board’s corresponding schedule.

  • max_frame - Number of frames to transmit (Repetitions of a given schedule).

We then write this configuration via the writeSetting command using JSON encoding. Finally, we need to initiate the TDD frame transmission by generating a trigger via the writeSetting command. Notice that this software command triggers the first Iris in a chain, which consequently triggers the rest, one after the other with a short delay in between each. Unless a max_frame value is specified, this will trigger a continuous replay transmission that can be manually stopped by the user via Ctrl+C.

symSamp = numSamps + prefix_pad + postfix_pad
# Send only one frame (set max_frame to 1)
bconf = {"tdd_enabled": True, "trigger_out": False, "symbol_size": symSamp, "frames": [bsched], "max_frame": 1}
mconf = {"tdd_enabled": True, "trigger_out": False, "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))

bsdr.writeSetting("TRIGGER_GEN", "")

Last updated on 16 May 2019 / Published on 12 Feb 2019