The development of the RENEW project takes place at three different layers as shown in the figure below.
This layer is where most users will spent most of their time.
The application layer provides a user interface through a set of APIs.
Users can generate, manipulate, transmit and receive RF signals on the hardware by calling these APIs.
The two major projects within RENEW are RENEWLab and Agora:
The middleware layer is built around the SoapySRD software which consists of an open-source API written in C/C++ and a runtime library for interfacing with SDR devices like the Faros base station and Iris SDRs.
The FPGA layer allows us to interact with the LMS7 transceiver IC (Integrated Circuit) as well as to create different TX/RX functionalities and advanced DSP functions with the goal of simplifying the application layer.
The FPGA fabric rarely needs to be modified. In case of FPGA modifications, the FPGA code is released here.
This rest of this page provides a detailed description of the Application layer, i.e., both RENEWLab and Agora.
RENEWLab Uplink Channel Sounder provides a framework to configure RENEW clients to transmit uplink pilots and user-defined data signals to the base station and to record the received signals from all base station antennas to HDF5 datasets in real-time. The pilot signals are transmitted in a time-orthogonal fashion from each configured client and uplink signals are sent at common subframes by all clients. These pilot and data signals are recorded as multi-dimensional matrices in separate datasets in the HDF5 file. Dataset analysis tools are developed in Python3 that enables plotting of signals at various dimensions and calculation of various statistics of the massive MIMO channel as well as offline demultiplexing of the uplink signals.
In this document, we provide an extensive tutorial on the configuration and usage of the Uplink Channel Sounder framework.
The Sounder relies on a Time-Division-Duplex-based (TDD) framer to specify each device operation at every point in time. At the most basic level, the Sounder is a continuous repetition of a user-defined schedule that specifies when and what will be transmitted/received by each device.
We define several different types of transmissions and actions:
A schedule or frame is declared as a string, constructed using different combinations of the letters described above. Consider the following schedule for a one Base Station (BS), two User Equipment (UE) or client setup:
{
"BaseStations" : {
"frame_schedule" : [
"BGGGGGPPPPUGGGGGGGGG"
],
...
},
"Clients" : {
"frame_schedule" : [
"GGGGGGPPGGUGGGGGGGGG",
"GGGGGGGGPPUGGGGGGGGG"
],
...
}
}
Since we are using two UEs, we declare the frame for each UE as a separate string within a string array. The frame length of the Base Station and UEs must be the same. In the first time slot, a (B)eacon is transmitted by the Base Station to synchronize the UEs. The next action corresponds to a couple of (P)ilots being sent from the two antennas in the first UE. A ‘P’ in an even location (if we start counting from zero) means a pilot will be sent from antenna A in the UE, whereas a ‘P’ in an odd slot means a pilot will be transmitted from antenna B. Next, the second UE repeats this process. The ‘P’s in the Base Station simply mean the BS is expecting a pilot at that time. Also, notice there are no overlapping ‘P’s among clients. Finally, (U)plink data is transmitted from both UEs at the exact same time. Similarly to the Base Station ‘P’, a ‘U’ in the BS schedule simply means the BS is expecting an uplink data transmission.
We define three frame modes to specify how UEs will trigger their transmissions:
free_running
: The UE does not expect a beacon to begin transmitting. In this mode, the UE relies on an internal trigger that is set as soon as we run the code. This makes the system asynchronous in the sense that the time slots from the UE and BS schedules cannot be guaranteed to be aligned. This mode is useful when we simply want to continuously transmit data from a UE, with no gaps in time, and collect data without caring for the start/end of each frame.triggered
: The UE waits for an initial beacon before it starts transmitting. If a beacon is detected, then the UE triggers its transmission(s) according to the schedule, and the BS collects pilots/data until the specified maximum number of frames is reached. If no beacon is detected, no uplink transmissions take place. In this mode it is common to see different start delays, of a few frames, among different UEs.continuous_resync
: The UE waits for an initial beacon before it starts transmitting. However, unlike the triggered
mode, the UE re-attempts to detect a beacon every 1000th frame to compensate for any clock drift due to the clock mismatch between transmitter and receiver.Notice that the schedule, along with other parameters used for channel measurement are defined in a JSON-formatted file and passed as an input argument to the sounder executable. Every JSON configuration file has the following format and must include both BaseStations
and Clients
objects:
{
"BaseStations" : {
...
},
"Clients" : {
...
}
}
Base stations and clients have some common parameters, such as center frequency and sample rate that must have the same value for meaningful operation (unless some new experiment scenarios are intended). Some parameters are common but have different values such as TX and RX gain values. Lastly, either of the BaseStations
and Clients
have their own specific parameters. A full list of common and specific parameters and their descriptions are listed below.
The following code shows the contents of a basic configuration file for one base station and two UEs:
{
"serial_file" : "files/topology.json",
"frequency" : 3.6e9,
"sample_rate" : 5e6,
"channel" : "AB",
"rx_gain_a" : 65,
"tx_gain_a" : 81,
"rx_gain_b" : 65,
"tx_gain_b" : 81,
"frame_schedule" : [
"BGGGGGPPPPUGGGGGGGGG"
],
"max_frame" : 4000,
"ofdm_symbol_per_slot" : 10,
"fft_size" : 64,
"cp_size" : 16,
"ofdm_tx_zero_prefix" : 160,
"ofdm_tx_zero_postfix" : 160,
"beamsweep" : true,
"ue_channel" : "AB",
"ue_rx_gain_a" : [65, 65],
"ue_tx_gain_a" : [81, 81],
"ue_rx_gain_b" : [65, 65],
"ue_tx_gain_b" : [81, 81],
"ue_frame_schedule" : [
"GGGGGGPPGGUGGGGGGGGG",
"GGGGGGGGPPUGGGGGGGGG"
],
"ue_modulation" : "16QAM",
"tx_advance" : [135, 135]
}
Channel Sounder code is maintained as part of the RENEWLab repository and can be found here. Before building the code, make sure all the dependencies described in Software Overview are installed. An additional dependency for the Sounder code is the muFFT library which is included as a git submodule and needs to be statically built and linked with the sounder code. To do so, navigate to the Sounder directory and do as follows:
$ cd mufft
$ git submodule update --init
$ cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON ./ && make -j
$ cd ..
Now, we are ready to compile the sounder code:
$ mkdir build
$ cd build
$ cmake .. -DCMAKE_BUILD_TYPE=Release -DLOG_LEVEL=info && make -j
$ cd ../
The build process generates an executable called “sounder”. Now let’s start looking at creating or modifying existing JSON configuration files. Some sample configuration files are provided in files.
Once a JSON file is configured, the Sounder experiment is ready to run. If only pilot slots (and noise) are configured in the schedule, the following command can be used:
./sounder -conf /path/to/json
The sounder experiments start to run and an hdf5 file will be generated. The sounder will store the time-domain received samples in the uplink pilot slots into the file. The name of the file will be auto-generated (you can find it in the sounder logs) and has the format: “trace-year-month-day-hour-minute-second_AxBxC_D_E.hdf5”, where
A= Number of cells
B= Number of antennas
C= Number of users
D= first BS antenna index (default= 0)
E= last BS antenna index (default= Number of antennas minus 1)
The hdf5 file will include a Pilot_Samples dataset in which 5-dimensional data of the pilots will be saved. At the end of the experiment, the dimensions are as the following:
[max_frame, number of cells, number of pilot slots, number of BS antennas, samples in each slot]
The default location of the dataset will be in the logs/ folder. You can specify a path to store the data using the -store_path command-line option:
./sounder -conf path/to/json -store_path path/to/hdf5
If uplink data slots are also included, the binary data to be sent must first be generated as the following:
./sounder -conf /path/to/json -gen_ul_bits
Or if you intend to save them in a specific path (which you will use to store hdf5 as well):
./sounder -conf /path/to/json -store_path /path/to/hdf5 -gen_ul_bits
This command creates a separate random binary file for each user. It also generates two more files for each random binary file corresponding to modulated data and mapped to OFDM symbol (frequency-domain data) and the corresponding time-domain data. These three files have the following naming format:
ul_data_b_A_B_C_D_E_F_X_G.bin
ul_data_f_A_B_C_D_E_F_X_G .bin
ul_data_t_A_B_C_D_E_F_AB_G.bin
A= Modulation (QPSK, 16QAM, 64QAM)
B= Number of non-zero subcarriers per OFDM symbol
C= Number of subcarriers in OFDM symbol
D= Number of OFDM symbols per slot
E= Number of uplink data slots
F= Number of frames
X= Channels: A, B, AB
G= Index of the Client
Example:
$ ./build/sounder -conf myfiles/conf-uplink-bs-one-client.json --gen_data_bits
71:810724 INFOR:
Input config:
BaseStations: {"beacon_antenna":0,"beamsweep":false,"cells":1,"channel":"AB","cp_size":16,"fft_size":64,"frame_schedule":["BGPPGGUGGGGGGGGGGGGG"],"frequency":3600000000.0,"hub_id":"myfiles/hub-serials.txt","max_frame":4000,"ofdm_symbol_per_slot":10,"postfix":160,"prefix":160,"rate":5000000.0,"rxgainA":65,"rxgainB":65,"sdr_id":["myfiles/iris-serials.txt"],"txgainA":75,"txgainB":75}
Clients: {"channel":"AB","cp_size":16,"fft_size":64,"frame_mode":"continuous_resync","frame_schedule":["GGPPGGUGGGGGGGGGGGGG"],"frequency":3600000000.0,"hw_framer":false,"ofdm_symbol_per_slot":10,"postfix":160,"prefix":160,"rate":5000000.0,"rxgainA":[65],"rxgainB":[65],"sdr_id":["RF3E000392"],"tx_advance":135,"txgainA":[75],"txgainB":[75]}
RF3E000347
RF3E000564
RF3E000569
RF3E000639
RF3E000605
RF3E000600
RF3E000611
RF3E000627
Number of valid devices loaded from myfiles/iris-serials.txt: 8
Number of valid devices loaded from myfiles/hub-serials.txt: 0
71:811568 INFOR: Cores found 36 ...
71:811583 INFOR: Allocating 4 cores to receive threads ...
71:811588 INFOR: Allocating 1 cores to record threads ...
71:811593 INFOR: Allocating 1 cores to client threads ...
71:811598 INFOR: Configuration file was successfully parsed!
Saving UL data bits for radio 0 to logs/ul_data_b_QPSK_52_64_10_1_1_AB_0.bin
Saving UL frequency-domain data for radio 0 to logs/ul_data_f_QPSK_52_64_10_1_1_AB_0.bin
Saving UL time-domain data for radio 0 to logs/ul_data_t_QPSK_52_64_10_1_1_AB_0.bin
Once the data is created, you can run the sounder as follows:
./sounder -conf /path/to/json -store_path /path/to/hdf5
The sounder will store the time-domain received samples in the uplink data slots into the file. The name of the file will be auto-generated (you can find it in the sounder logs) and has a similar format to when no uplink data is used, except it starts with “trace-uplink”.
The hdf5 file will include a “Uplink_Data” dataset in which 5-dimensional data of the uplink slots will be saved. At the end of the experiment, the dimensions are as the following:
[max_frame, number of cells, number of uplink data (‘U’) slots, number of BS antennas, samples in each slot]
Lastly, the sounder can also be run in internal measurement mode, where uplink and downlink pilots can be sent from a reference antenna to all base station antennas and vice versa. The reference antenna is assumed to be synchronized with the rest of the array. In the case of Faros, it could either be within the same daisy chain or in a separate chain connected to the Faros hub. For this type of measurement, no Clients object is required in the JSON. As mentioned in the “BaseStations-only parameters” section, for this mode “internal_measurement” parameter is set to true. Also, the last identifier in the sdr_id file is considered to be the reference node. The sounder is run similar to when pilot data is collected. The data will be saved in “Pilot_Samples” dataset which is 5-dimensional as below:
[max_frame, number of cells, 2, number of BS antennas, samples in each slot]
Note the middle dimension is 2 which indicates index 0 for data sent from BS antennas to the reference, and index 1 for data sent from the reference node to the BS antennas.
If Sounder is intended to be run two or more machines, where BaseStations is configured on one and clients are run on the other(s), the following command can be used.
For the BaseStations,
`./build/sounder -conf /path/to/json -bs_only`
For the clients,
`./build/sounder -conf /path/to/json -client_only`
Use plot_hdf5.py and MMIMO_RECEIVER.py to analyze datasets collected from Sounder.
Usage format:
python3 plot_hdf5.py <hdf5_file_path_filename> --option(s)
Available options:
Users can use “python3 plot_hdf5.py –help” in the terminal to get a list of short option descriptions.
Option | Description | Default | Available Range |
---|---|---|---|
–deep-inspect | Performs a deep data analysis. It performs a channel analysis on the referenced frame, BS antenna and clients, validates all pilots for all clients, plots a frame map for good and bad frames, and frames’ starting indices per antenna. This option will take a minute or so to finish the five plots. | Disabled | Use this option to enable it. |
–ref-frame=NUM | Specify the index of the frame to be plotted. This frame will also be used as the reference frame in the auto correlation. | 0 | [0, n-frames] |
Make sure to use the index within the range specified by n-frames. | |||
If the user specifies it outside n-frames, the script shall throw a warning. | |||
–ref-ant=NUM | Specify the index of the reference BS antenna to be plotted. | 0 | [0, the number of BS antennas used in the experiment] |
Note: Each BS Iris module can have two polarized antennas. | |||
If the user specifies an antenna index outside the number of BS antennas used, the script shall abort. | |||
–ref-user=NUM | Specify the index of the reference client to be plotted. | 0 | |
If the user specifies a client index outside the total number of clients available, the script shall abort. | |||
Note: Each Iris can act as two clients if both channels are used. The ref-user corresponds to each listed client’s channel. This is important to note when both channels on clients are enabled. | |||
For example, ref-user 0 is the first client’s channel A; ref-user 1 is its channel B; ref-user 2 is the second client’s channel A; ref-user 3 is its channel B. | |||
–n-frames=NUM | Specify the number of frames to analyze. | 2000 | [1, the number of collected frames - frame-start] |
If 0 is used, the script shall throw a warning and process the whole dataset, instead. | |||
If the user specifies a number outside the total number of frames collected, the script shall analyze up to the maximum available number of frames. | |||
–sub-sample=NUM | Analyze frames by downsampling by sub-sample. For example, if n-frame is 1000 and sub-sample is 10, a total of 100 frames shall be analyzed. | 1 | A user can specify a number higher than 1 to speed up the data processing. However, it will miss fine-grained information. |
–thresh=NUM | Specify the amplitude threshold to detect a valid pilot for a frame. | 0.001 | If no valid pilot frame is detected, the script shall throw a no valid frames found error and abort. |
–frame-start=NUM | Specify the index of the frame from where we will start analyzing the number of frames defined by n-frames. | 0 | [0, the number of frames collected in the experiment] |
Make sure that [frame-start, frame-start + n-frames] is within the range of the maximum number of frames collected. | |||
–verify-trace | Perform a quick inspection of the HDF5 file. It produces several charts in one plot to show the real and imaginary numbers of the selected frame and all frames specified in n-frames option. | Enabled | This option is always enabled by default. The user does not need to use it. |
–analyze-trace | Calculate and plot achievable bit rates for multi-user and single-user beamforming with zero-forcing and conjugate schemes. | Disabled | Use this option to enable it. |
Example usage of these tools is shown below.
A quick analysis plot: Navigate to the renew-software/PYTHON/IrisUtils directory and run
python3 plot_hdf5.py /path/to/hdf5/file
Analyze certain BS antenna performance: For example, BS Antenna 6, Client 1, and a particular frame 140.
python3 plot_hdf5.py /path/to/hdf5/file --ref-ant 6 --ref-user 1 --ref-frame 140
Analyze the achievable bit rate of the system:
python3 plot_hdf5.py /path/to/hdf5/file --analyze-trace
Deep analysis: You can find out the number of good and bad frames output on the terminal.
python3 plot_hdf5.py /path/to/hdf5/file --deep-inspect
Use the RENEWLab Python demo tool, MMIMO_RECEIVER.py: Navigate to the renew-software/PYTHON/DEMO directory.
a. Check the TX samples in the AWGN simulation mode. Below two commands do the same thing.
python3 MMIMO_RECEIVER.py --file=/path/to/hdf5/file
python3 MMIMO_RECEIVER.py --file=/path/to/hdf5/file/with/uplink --mode=AWGN
b. Analyze both TX and RX samples in REPLAY mode.
python3 MMIMO_RECEIVER.py --file=/path/to/hdf5/file/with/uplink --mode=REPLAY
c. Analyze 5 frames from Frame 1000 to Frame 2000.
python3 MMIMO_RECEIVER.py --file=/path/to/hdf5/file/with/uplink --mode=REPLAY --frame=1000:2000
RENEW supports a Python-based development flow for rapid physical-layer and signal processing prototyping. This framework allows users to generate a signal on Python, manipulate it, stream it to one or multiple Iris boards (SDR hardware), trigger an over-the-air transmission, and then receive it on another board.
In this page, we provide the following:
We present several code snippets showing some of the basic commands required to configure the Iris boards from the Python design flow.
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))
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
FPGA_IRIS030_RD_MEASURED_RSSI = 284
rssi = sdr.readRegister("IRIS30", FPGA_IRIS030_RD_MEASURED_RSSI)
The following commands allow users to set different RF parameters such as gains, 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)
sdr.setGain(SOAPY_SDR_TX, ch, 0)
sdr.setGain(SOAPY_SDR_RX, ch, 0)
sdr.setAntenna(SOAPY_SDR_RX, ch, "TRX")
sdr.setDCOffsetMode(SOAPY_SDR_RX, ch, True)
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.
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:
activateStream works differently for TX and RX:
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.
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.
txStream = sdr.setupStream(SOAPY_SDR_TX, SOAPY_SDR_CF32, [0, 1])
sdr.activateStream(txStream)
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))
flags = SOAPY_SDR_HAS_TIME | SOAPY_SDR_END_BURST
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)
# 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)
The following code is used to terminate and close stream “rxStream” for sdr Iris board
sdr.deactivateStream(rxStream)
sdr.closeStream(rxStream)
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). QI 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='QI').tolist())
sdr.writeRegisters("TX_RAM_B", replay_addr, cfloat2uint32(pilot2, order='QI').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))
We use the same TDD Framer presented above in the Sounder section. The code below presents an example showing how to define the framer schedule and the different parameters, and how to write this configuration via the writeSetting command using JSON encoding.
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:
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).
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.
bsched = "PGRGGGGGTGTGTGGG"
msched = "RGPGGGGGRGRGRGGG"
symSamp = numSamps + prefix_pad + postfix_pad
# Send only one frame (set max_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))
bsdr.writeSetting("TRIGGER_GEN", "")
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:
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"
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 the tutorial above.
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()
We have developed several demo projects to provide users a quick start into designing their own experiments:
SISO_OFDM.py - Generates, transmits, and receives and OFDM signal. The user can select one of the following modulation schemes BPSK/QPSK/16QAM/64QAM It requires two Iris boards (chained or unchained). The TX board will transmit the signal from RF chain A and the RX board will receive it at RF chain A as well (script can be extended to support both chains). The script can be run in two modes:
SISO_TX.py - Simple transmitter. Generate signal and transmit it from one of the antennas in the Iris Board. Supported signal types: LTE, WiFi LTS, WiFi STS, and Sine. Relies on the Block RAM pre-loaded-signal transmission mode.
SISO_RX.py - Simple receiver. Receive signal and plot it continuously. Signal can be recorded and stored in HDF5 format. Alternatively, RX samples can be recorded into a binary file using the “write_to_file” function. AGC can be enabled by the user, if desired.
Run both the SISO_TX.py script and SISO_RX.py script at the same time.
SISO_TXRX_TDD.py - This script is useful for testing the TDD operation. It programs two Irises in TDD mode with the following framing schedule:
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).
SOUNDER_TXRX.py - Basic channel sounding test. It program two Irises in TDD mode (one as a base station node, and the other as a UE/client node) to transmit and receive according to a specific schedule/frame where the BS is required to first send a beacon signal (initial “P”). This beacon consists of a signal that has been pre-loaded to the BS FPGA buffer. Similarly, the conjugate of the beacon signal has been pre-loaded to the UE’s buffer in order to enable any correlation operation on the UE side.
MMIMO_RECEIVER.py - Simple “Offline” Massive MIMO receiver. Used to post-process data obtained from the Sounder. Please see more details here.
MMIMO_DOWNLINK.py - Script to test the effects of reciprocity calibration on downlink signal.
BEACON_SWEEP.py - Generate beacon and precode signal in order to create a beamsweep.
NB_CAL_DEMO.py - Sample script showing a basic reciprocity calibration procedure that runs at the Base Station (eNodeB).
WB_CAL_DEMO.py - Wideband calibration demo. It implements the Argos calibration method among anntennas within a base station, and plots the magnitude and phase of the calibration vector on 4 select subcarriers for all antennas.
AGC_SIM_DEMO.py - Non-realtime script designed to demonstrate the operation of the AGC State machine. This AGC runs on the host machine and therefore it implements a VERY coarse version of the AGC (non-real time). That is, only one gain adjustment occurs at each buffer read. It requires two Iris boards. One TX and one RX. The TX is continuously transmitting and we use the digital RSSI measurements obtained from the LMS7 in order to adapt the RX amplifiers. As of this moment, there is no way of synchronizing a received frame to the reading of the RSSI. Therefore, we need to keep the TX continuously sending a signal (e.g., by running SISO_TX.py on one of the boards).
RENEW offers a Matlab-based development flow for physical-layer and signal processing analysis. This framework allows users to generate various types of signals on Matlab, manipulate, write it to one or multiple Iris boards (SDR hardware), trigger an over-the-air transmission, and then receive it on another board. After the reception, the user may analyze and employ channel and carrier offset estimation, and plot and visualize the received samples. In this section, we provide the following:
Before looking into individual files and tutorials, it is good to have a big-picture understanding of the design flow. Iris nodes can be set up and configured directly through a framework called SoapySDR. This framework offers handles to its functions in either C++ or Python. Therefore, to use Matlab on Iris nodes we need a layer facilitating the communication between a Matlab experiment script and the hardware. We went with Python and gathered various hardware configuration and operation functions in a single file. This is what we call the Python Driver.
This driver follows a standard object-oriented logic. Instances of Iris nodes are created and configured via various methods/functions. To accommodate for maximum flexibility and ease-of-use we intersected another layer between the experiment scripts and the Python driver. This way, creating and handling Python objects and calling their methods is done in a different collection of Matlab functions we call Matlab Driver. Thus, Matlab experiment scripts remain clean of anything besides the familiar Matlab function calling format and communication with Python objects is done separately.
We provide the code to both drivers and the user is welcome to extend their functionalities as needed. The logic of this design is shown in the figure below.
In this tutorial, we go over a simple Matlab MIMO script that implements a single-shot transmission from multiple clients (also known as User Equipment or UE) to multiple base station radios. We define two different modes: a) OTA (Over-the-air) and b) SIM_MOD (simulation).
If in simulation mode we simply use a Rayleigh channel whereas the OTA mode relies on the Iris hardware for transmission and reception. In both cases the clients transmit an OFDM signal that resembles a typical 802.11 WLAN waveform. If the transmission is OTA, then the user specifies a TDD schedule that tells all clients when to transmit their frame. The base station initiates the schedule by sending a beacon signal that synchronizes clients. After that, all clients will transmit simultaneously. We implement a frame structure that allows the base station to capture clean (non-overlaping/staggered) training sequences for equalization and demultiplexing of the concurrent data streams. In the figure below we observe the structure of the transmit signals for each client. Notice this is for a four-client topology. You will notice that the training sequences of the multiple clients are staggered. This allows the base station to estimate the channel from each of its antennas to each of the clients in a clean manner. After all the training sequences, all clients send their data frames simultaneously. On the receiver side, the base station will perform channel estimation, equalization, and use zero-forcing or conjugate beamforming to demultiplex the different data streams.
clear
close all;
% For python bindings
[version, executable, isloaded] = pyversion;
if ~isloaded
pyversion /usr/bin/python
py.print() %weird bug where py isn't loaded in an external script
end
If simulation mode, specify Rayleigh channel. If over-the-air, specify serial numbers of base station nodes and client nodes. Here we also configure the frequency, transmit and receive gains, and sampling rate.
% Params:
N_BS_NODE = 4;
N_UE = 2;
WRITE_PNG_FILES = 0; % Enable writing plots to PNG
SIM_MOD = 1;
DEBUG = 0;
PLOT = 1;
if SIM_MOD
chan_type = "rayleigh"; % Will use only Rayleigh for simulation
sim_SNR_db = 15;
TX_SCALE = 1; % Scale for Tx waveform ([0:1])
bs_ids = ones(1, N_BS_NODE);
ue_ids = ones(1, N_UE);
else
%Iris params:
TX_SCALE = 0.5; % Scale for Tx waveform ([0:1])
chan_type = "iris";
USE_HUB = 0;
TX_FRQ = 2.5e9;
RX_FRQ = TX_FRQ;
TX_GN = 42;
TX_GN_ue = 42;
RX_GN = 20;
SMPL_RT = 5e6;
N_FRM = 10;
bs_ids = string.empty();
ue_ids = string.empty();
ue_scheds = string.empty();
if USE_HUB
% Using chains of different size requires some internal
% calibration on the BS. This functionality will be added later.
% For now, we use only the 4-node chains:
bs_ids = ["RF3E000134", "RF3E000191", "RF3E000171", "RF3E000105",...
"RF3E000053", "RF3E000177", "RF3E000192", "RF3E000117",...
"RF3E000183", "RF3E000152", "RF3E000123", "RF3E000178", "RF3E000113", "RF3E000176", "RF3E000132", "RF3E000108", ...
"RF3E000143", "RF3E000160", "RF3E000025", "RF3E000034",...
"RF3E000189", "RF3E000024", "RF3E000139", "RF3E000032", "RF3E000154", "RF3E000182", "RF3E000038", "RF3E000137", ...
"RF3E000103", "RF3E000180", "RF3E000181", "RF3E000188"];
hub_id = "FH4A000001";
else
bs_ids = ["RF3E000189", "RF3E000024", "RF3E000139", "RF3E000032", "RF3E000154", "RF3E000182", "RF3E000038", "RF3E000137"];
end
ue_ids= ["RF3E000060", "RF3E000157"];
N_BS_NODE = length(bs_ids); % Number of nodes/antennas at the BS
N_UE = length(ue_ids); % Number of UE nodes
end
First, select the receiver beamforming algorithm to be used. Currently we support both Zero-Forcing and Conjugate Beamforming. In addition, we specify all the necessary OFDM parameters such as number of subcarriers, number of OFDM symbols, modulation order, and index of pilot and data subcarriers. Notice we follow the basic parameters from a 20 MHz 802.11a waveform.
MIMO_ALG = 'ZF'; % MIMO ALGORITHM: ZF or Conjugate
fprintf("MIMO algorithm: %s \n",MIMO_ALG);
% Waveform params
N_OFDM_SYM = 44; % Number of OFDM symbols for burst, it needs to be less than 47
MOD_ORDER = 16; % Modulation order (2/4/16/64 = BSPK/QPSK/16-QAM/64-QAM)
% OFDM params
SC_IND_PILOTS = [8 22 44 58]; % Pilot subcarrier indices
SC_IND_DATA = [2:7 9:21 23:27 39:43 45:57 59:64]; % Data subcarrier indices
SC_IND_DATA_PILOT = [2:27 39:64]';
N_SC = 64; % Number of subcarriers
CP_LEN = 16; % Cyclic prefix length
N_DATA_SYMS = N_OFDM_SYM * length(SC_IND_DATA); % Number of data symbols (one per data-bearing subcarrier per OFDM symbol) per UE
N_LTS_SYM = 2; % Number of
N_SYM_SAMP = N_SC + CP_LEN; % Number of samples that will go over the air
N_ZPAD_PRE = 90; % Zero-padding prefix for Iris
N_ZPAD_POST = N_ZPAD_PRE -14; % Zero-padding postfix for Iris
% Rx processing params
FFT_OFFSET = 16; % Number of CP samples to use in FFT (on average)
DO_APPLY_PHASE_ERR_CORRECTION = 1; % Enable Residual CFO estimation/correction
We follow the 802.11 convention and define the preamble as frequency domain long-term training sequences (LTS or LTF) and convert them to time domain samples through an IFFT. Then, we generate the staggered (time-orthogonal) preambles for the different clients. These preambles are the ones that will allow the receiver to cleanly estimate the channel to each client in order to perform channel equalization.
% LTS for fine CFO and channel estimation
lts_f = [0 1 -1 -1 1 1 -1 1 -1 1 -1 -1 -1 -1 -1 1 1 -1 -1 1 -1 1 -1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 ...
1 1 -1 -1 1 1 -1 1 -1 1 1 1 1 1 1 -1 -1 1 1 -1 1 -1 1 1 1 1].';
lts_t = ifft(lts_f, N_SC); %time domain
% Arrange time-orthogonal pilots
preamble_common = [lts_t(33:64); repmat(lts_t,N_LTS_SYM,1)];
l_pre = length(preamble_common);
pre_z = zeros(size(preamble_common));
preamble = zeros(N_UE * l_pre, N_UE);
for jp = 1:N_UE
preamble((jp-1)*l_pre + 1: (jp-1)*l_pre+l_pre,jp) = preamble_common;
end
We generate multiple random sequences of symbols, one for each client. Then we build an array that will contain all the frequency-domain samples across subcarriers and OFDM symbols. This array combines both the pilots and data. At the end we simply construct the full time-domain OFDM waveform to be transmitted (one vector per client).
%% Generate a payload of random integers
tx_data = randi(MOD_ORDER, N_DATA_SYMS, N_UE) - 1;
tx_syms = mod_sym(tx_data, MOD_ORDER);
% Reshape the symbol vector to a matrix with one column per OFDM symbol
tx_syms_mat = reshape(tx_syms, length(SC_IND_DATA), N_OFDM_SYM, N_UE);
% Define the pilot tone values as BPSK symbols
pilots = [1 1 -1 1].';
% Repeat the pilots across all OFDM symbols
pilots_mat = repmat(pilots, 1, N_OFDM_SYM, N_UE);
%% IFFT
% Construct the IFFT input matrix
ifft_in_mat = zeros(N_SC, N_OFDM_SYM, N_UE);
% Insert the data and pilot values; other subcarriers will remain at 0
ifft_in_mat(SC_IND_DATA, :, :) = tx_syms_mat;
ifft_in_mat(SC_IND_PILOTS, :, :) = pilots_mat;
%Perform the IFFT
tx_payload_mat = ifft(ifft_in_mat, N_SC, 1);
% Insert the cyclic prefix
if(CP_LEN > 0)
tx_cp = tx_payload_mat((end-CP_LEN+1 : end), :, :);
tx_payload_mat = [tx_cp; tx_payload_mat];
end
% Reshape to a vector
tx_payload_vecs = reshape(tx_payload_mat, ceil(numel(tx_payload_mat)/N_UE), N_UE);
% Construct the full time-domain OFDM waveform
tx_vecs = [zeros(N_ZPAD_PRE, N_UE); preamble; tx_payload_vecs; zeros(N_ZPAD_POST, N_UE)];
% Leftover from zero padding:
tx_vecs_iris = tx_vecs;
% Scale the Tx vector to +/- 1
tx_vecs_iris = TX_SCALE .* tx_vecs_iris ./ max(abs(tx_vecs_iris));
% Create BS Hub and UE objects. Note: BS object is a collection of Iris
% nodes.
bs_sched = ["BGGGGGRG"]; % BS schedule
ue_sched = ["GGGGGGPG"]; % UE schedule
%number of samples in a frame
n_samp = length(tx_vecs_iris);
% Iris nodes' parameters
bs_sdr_params = struct(...
'id', bs_ids, ...
'n_sdrs', N_BS_NODE, ... % number of nodes chained together
'txfreq', TX_FRQ, ...
'rxfreq', RX_FRQ, ...
'txgain', TX_GN, ...
'rxgain', RX_GN, ...
'sample_rate', SMPL_RT, ...
'n_samp', n_samp, ... % number of samples per frame time.
'n_frame', N_FRM, ...
'tdd_sched', bs_sched, ... % number of zero-paddes samples
'n_zpad_samp', N_ZPAD_PRE ...
);
ue_sdr_params = bs_sdr_params;
ue_sdr_params.id = ue_ids;
ue_sdr_params.n_sdrs = N_UE;
ue_sdr_params.txgain = TX_GN_ue;
ue_sdr_params.tdd_sched = ue_sched;
If we are operating in simulation mode, we simply take the transmit vector and pass it through a Rayleigh channel. However, if we are actually dealing with hardware and transmitting signals over the air, the process is a bit more involved.
This happens inside the getRxVec()
function. Below we will go over the actions taking place inside the function.
if (SIM_MOD)
rx_vec_iris = getRxVec(tx_vecs_iris, N_BS_NODE, N_UE, chan_type, sim_SNR_db, bs_sdr_params, ue_sdr_params, []);
rx_vec_iris = rx_vec_iris.'; % just to agree with what the hardware spits out.
else
if USE_HUB
rx_vec_iris = getRxVec(tx_vecs_iris, N_BS_NODE, N_UE, chan_type, [], bs_sdr_params, ue_sdr_params, hub_id);
else
rx_vec_iris = getRxVec(tx_vecs_iris, N_BS_NODE, N_UE, chan_type, [], bs_sdr_params, ue_sdr_params, []);
end
end
We first initialize both the BS nodes and UE nodes:
n_samp = bs_param.n_samp;
if ~isempty(hub_id)
node_bs = iris_py(bs_param,hub_id);
else
node_bs = iris_py(bs_param,[]); % initialize BS
end
node_ue = iris_py(ue_param,[]); % initialize UE
Then we set up the transmission and reception by setting up streams and synchronizing base station radios. The following lines (i) reset the correlator on the UE, hence, preparing it to synchronize based on the beacon it will have to detect from the base station, (ii) set up reading streams on both nodes before the transmission starts, and (iii) configure the schedules on the BS and the UE, respectively:
node_ue.sdr_configgainctrl();
node_bs.sdrsync(); % synchronize delays only for BS
node_ue.sdrrxsetup(); % set up reading stream
node_bs.sdrrxsetup();
tdd_sched_index = 1; % for uplink only one frame schedule is sufficient
node_bs.set_tddconfig(1, bs_param.tdd_sched(tdd_sched_index)); % configure the BS: schedule, etc.
node_ue.set_tddconfig(0, ue_param.tdd_sched(tdd_sched_index)); % configure the UE: schedule, etc.
Next, we write the data to be transmitted from both the BS and UE. The BS will initiate the writing of the predefined beacon on the FPGA block ram for transmission during the ‘P’ slots of the BS schedule. The beacon sequence is hardcoded in the Python driver and the Matlab user should not be concerned with it. The UE in turn also writes its pilots onto the block ram so that they will be transmitted during the ‘P’ slots of its schedule:
node_bs.sdr_setupbeacon(); % Burn beacon to the BS(1) RAM
for i=1:n_ue
node_ue.sdrtx_single(tx_data(:,i), i); % Burn data to the UE RAM
end
The next step is to set up reception by activating the RX stream on the base station and enabling the correlator on the UE:
node_bs.sdr_activate_rx(); % activate reading stream
node_ue.sdr_setcorr() % activate correlator
The BS then reads and stores the received data from the receiver stream that we had set up and activated earlier:
[y, data0_len] = node_bs.sdrrx(n_samp); % read data
Finally we end the streams on both nodes:
node_bs.sdr_close();
node_ue.sdr_close();
Upon reception, each base station node performs a cross-correlation in an attempt to find the LTS sent by each client. Since we know the structure of each frame, once we find the LTS we can determine the start of the payload (data).
l_rx_dec=length(rx_vec_iris);
%% Correlate for LTS
% Complex cross correlation of Rx waveform with time-domain LTS
a = 1;
unos = ones(size(preamble_common'));
lts_corr = zeros(N_BS_NODE, length(rx_vec_iris));
data_len = (N_OFDM_SYM)*(N_SC +CP_LEN);
rx_lts_mat = double.empty();
payload_ind = int32.empty();
payload_rx = zeros(N_BS_NODE, data_len);
lts_peaks = zeros(N_BS_NODE, N_UE);
for ibs =1:N_BS_NODE
% Correlation through filtering
v0 = filter(fliplr(preamble_common'),a,rx_vec_iris(ibs,:));
v1 = filter(unos,a,abs(rx_vec_iris(ibs,:)).^2);
lts_corr(ibs,:) = (abs(v0).^2)./v1; % normalized correlation
% Sort the correlation values
sort_corr = sort(lts_corr(ibs,:), 'descend');
% Take the N_UE largest values
rho_max = sort_corr(1:N_UE);
% Get the indices of N_UE largest corr. values
lts_peaks(ibs,:) = find(lts_corr(ibs,:) >= min(rho_max));
% position of the last peak
max_idx = max(lts_peaks(ibs,:));
% In case of bad correlatons:
if (max_idx + data_len) > length(rx_vec_iris) || (max_idx < 0) || (max_idx - length(preamble) < 0)
fprintf('Bad correlation at antenna %d max_idx = %d \n', ibs, max_idx);
% Real value doesn't matter since we have corrrupt data:
max_idx = length(rx_vec_iris)-data_len -1;
end
payload_ind(ibs) = max_idx +1;
pream_ind_ibs = payload_ind(ibs) - length(preamble);
pl_idx = payload_ind(ibs) : payload_ind(ibs) + data_len;
rx_lts_mat(ibs,:) = rx_vec_iris(ibs, pream_ind_ibs: pream_ind_ibs + length(preamble) -1 );
payload_rx(ibs,1:length(pl_idx) -1) = rx_vec_iris(ibs, payload_ind(ibs) : payload_ind(ibs) + length(pl_idx) -2);
end
From the received preambles we can estimate the channel matrix between all TX and RX nodes. We convert both preambles and data to frequency domain and use our knowledge of the TX training sequence and the RX signal to do channel estimation, averaging over two LTS symbols.
% Construct a matrix from the received pilots
n_plt_samp = floor(length(preamble)/ N_UE); % number of samples in a per-UE pilot
Y_lts = zeros(N_BS_NODE,N_UE, n_plt_samp);
for iue = 1:N_UE
plt_j_ix = (iue-1) * n_plt_samp +1:iue * n_plt_samp;
Y_lts(:,iue,:) = rx_lts_mat(:,plt_j_ix);
end
% Take N_SC samples from each LTS
rx_lts_idx = CP_LEN +1 : N_LTS_SYM * N_SC +CP_LEN;
Y_lts = Y_lts(:,:,rx_lts_idx);
% Reshape the matix to have each lts pilot in a different dimension:
% N_BS_NODE x N_UE x 64 x 2
Y_lts = reshape(Y_lts, N_BS_NODE, N_UE, [], N_LTS_SYM);
% Take FFT:
Y_lts_f = fft(Y_lts, N_SC,3);
% Construct known pilot matrix to use i next step:
lts_f_mat = repmat(lts_f, N_BS_NODE *N_UE * N_LTS_SYM,1);
lts_f_mat = reshape(lts_f_mat, [], N_LTS_SYM, N_UE, N_BS_NODE);
lts_f_mat = permute(lts_f_mat, [4 3 1 2]);
% Get the channel by dividing by the pilots
G_lts = Y_lts_f ./ lts_f_mat;
% Estimate the channel by averaging over the two LTS symbols:
H_hat = mean(G_lts, 4);
% Reshape the payload and take subcarriers without the CP
payload_rx = reshape(payload_rx,N_BS_NODE, (N_SC + CP_LEN),[]);
payload_noCP = payload_rx(:,CP_LEN-FFT_OFFSET+(1:N_SC),:);
% Take FFT
Y_data = fft(payload_noCP, N_SC, 2);
Compute beamforming weights (zero-forcing and conjugate supported), and apply them to separate data streams. We also compute the channel condition number.
% Apply ZF by multiplying the pseudo inverse of H_hat[N_BS_NODE x NUE] for each suubcarrier:
nz_sc = find(lts_f ~= 0); % non-zero subcarriers
syms_eq = zeros(N_UE,N_SC,N_OFDM_SYM);
channel_condition = double.empty();
channel_condition_db = double.empty();
for j=1:length(nz_sc)
if(strcmp(MIMO_ALG,'ZF'))
% Pseudo-inverse:(H'*H)^(-1)*H':
HH_inv = inv((squeeze(H_hat(:,:, nz_sc(j) ) )' * squeeze(H_hat(:,:, nz_sc(j) ) ) ) ) * squeeze(H_hat(:,:, nz_sc(j) ) )';
x = HH_inv*squeeze(Y_data(:,nz_sc(j),:));
else
% Do yourselves: Conj BF:
% Normalization coeff:
H_pow = diag(abs (H_hat(:,:, nz_sc(j) )' * H_hat(:,:, nz_sc(j) ) ));
% Apply BF:
x = (H_hat(:,:, nz_sc(j) )') * squeeze(Y_data(:,nz_sc(j),:))./ repmat(H_pow, 1, N_OFDM_SYM);
end
syms_eq(:,nz_sc(j),:) = x;
channel_condition(nz_sc(j)) = cond(H_hat(:,:,nz_sc(j) ) );
channel_condition_db(nz_sc(j)) = 10*log10(channel_condition(nz_sc(j)) );
end
Phase error is obtained from pilots across symbols. Once we measure the error, we apply it to entire frame.
if DO_APPLY_PHASE_ERR_CORRECTION
% Extract the pilots and calculate per-symbol phase error
pilots_f_mat = syms_eq(:,SC_IND_PILOTS,:);
pilots_f_mat_comp = pilots_f_mat.* permute(pilots_mat, [3 1 2]);
pilot_phase_err = angle(mean(pilots_f_mat_comp,2));
else
% Define an empty phase correction vector (used by plotting code below)
pilot_phase_err = zeros(N_UE, 1, N_OFDM_SYM);
end
pilot_phase_err_corr = repmat(pilot_phase_err, 1, N_SC, 1);
pilot_phase_corr = exp(-1i*(pilot_phase_err_corr));
% Apply the pilot phase correction per symbol
syms_eq_pc = syms_eq.* pilot_phase_corr;
syms_eq_pc = syms_eq_pc(:,SC_IND_DATA,:);
% Reshape the 3-D matrix to 2-D:
syms_eq_pc = reshape(syms_eq_pc, N_UE, [] );
syms_eq_pc = syms_eq_pc.';
rx_data = demod_sym(syms_eq_pc ,MOD_ORDER);
Finally, we plot a variety of results that can be used to understand the performance of the system:
Start with plotting setup
cf = 0;
fst_clr = [0, 0.4470, 0.7410];
sec_clr = [0.8500, 0.3250, 0.0980];
% Rx signal
cf = cf + 1;
figure(cf); clf;
for sp = 1:N_BS_NODE
subplot(N_BS_NODE,2,2*(sp -1) + 1 );
plot(real(rx_vec_iris(sp,:)));
axis([0 length(rx_vec_iris(sp,:)) -TX_SCALE TX_SCALE])
grid on;
title(sprintf('BS antenna %d Rx Waveform (I)', sp));
subplot(N_BS_NODE,2,2*sp);
plot(imag(rx_vec_iris(sp,:)), 'color' , sec_clr);
axis([0 length(rx_vec_iris(sp,:)) -TX_SCALE TX_SCALE]);
grid on;
title(sprintf('BS antenna %d Rx Waveform (Q)', sp));
end
%% Constellations
cf = cf+ 1;
figure(cf); clf;
if N_BS_NODE >=4
sp_rows = ceil(N_BS_NODE/4)+1;
else
sp_rows = ceil(N_BS_NODE/2)+1;
end
sp_cols = ceil(N_BS_NODE/(sp_rows -1));
for sp=1:N_UE
subplot(sp_rows,sp_cols, sp);
plot(syms_eq_pc(:,sp),'o','MarkerSize',1, 'color', sec_clr);
axis square; axis(1.5*[-1 1 -1 1]);
grid on;
hold on;
plot(tx_syms(:, sp),'*', 'MarkerSize',10, 'LineWidth',2, 'color', fst_clr);
title(sprintf('Equalized Uplink Tx (blue) and Rx (red) symbols for stream %d', sp));
% legend({'Rx','Tx'},'Location','EastOutside', 'fontsize', 12);
end
for sp=1:N_BS_NODE
subplot(sp_rows,sp_cols, sp_cols+sp);
plot(squeeze(Y_data(sp,:,:)),'co','MarkerSize',1);
axis square; axis(max(max(max( abs( Y_data)) ))*[-1 1 -1 1]);
title(sprintf('Unequalized received symbols at BS ant. %d', sp));
grid on;
hold on;
end
cond()
function.%% Channel Estimates
cf = cf + 1;
cond_clr = [0.8500, 0.250, 0.1980];
bw_span = (20/N_SC) * (-(N_SC/2):(N_SC/2 - 1)).';
figure(cf); clf;
sp = 0;
for ibs = 1:N_BS_NODE
for iue = 1:N_UE
sp = sp+1;
subplot(N_BS_NODE,N_UE,sp);
bar(bw_span, fftshift(abs( squeeze(H_hat(ibs, iue, : ) ) ) ),1,'LineWidth', 1);
axis([min(bw_span) max(bw_span) 0 1.1*max(abs( squeeze(H_hat(ibs, iue, :) ) ) )])
grid on;
title(sprintf('UE %d -> BS ant. %d Channel Estimates (Magnitude)', iue, ibs))
xlabel('Baseband Frequency (MHz)')
end
end
subplot(N_BS_NODE+1,1,N_BS_NODE+1);
bh = bar(bw_span, fftshift(channel_condition_db) ,1, 'LineWidth', 1);
set(bh,'FaceColor',cond_clr);
axis([min(bw_span) max(bw_span) 0 max(channel_condition_db)+1])
grid on;
title('Channel Condition (dB)')
xlabel('Baseband Frequency (MHz)')
%% EVM & SNR
sym_errs = sum(tx_data(:) ~= rx_data(:));
bit_errs = length(find(dec2bin(bitxor(tx_data(:), rx_data(:)),8) == '1'));
evm_mat = double.empty();
aevms = zeros(N_UE,1);
snr_mat = zeros(N_UE,1);
cf = cf + 1;
figure(cf); clf;
for sp = 1:N_UE
tx_vec = tx_syms_mat(:,:,sp);
evm_mat(:,sp) = abs(tx_vec(:) - syms_eq_pc(:,sp) ).^2;
aevms(sp) = mean(evm_mat(:,sp));
snr_mat(sp) = -10*log10(aevms (sp));
subplot(2,N_UE,sp)
plot(100*evm_mat(:,sp),'o','MarkerSize',1)
axis tight
hold on
plot([1 length(evm_mat(:,sp) )], 100*[aevms(sp), aevms(sp)],'color', sec_clr,'LineWidth',4)
hold off
xlabel('Data Symbol Index')
ylabel('EVM (%)');
legend('Per-Symbol EVM','Average EVM','Location','NorthWest');
h = text(round(.05*length(evm_mat(:,sp))), 100*aevms(sp), sprintf('Effective SINR: %.1f dB', snr_mat(sp)));
set(h,'Color',[1 0 0])
set(h,'FontWeight','bold')
set(h,'FontSize',10)
set(h,'EdgeColor',[1 0 0])
set(h,'BackgroundColor',[1 1 1])
title(sprintf('Stream from UE %d', sp));
grid on
end
for sp=1:N_UE
subplot(2,N_UE,N_UE+sp);
imagesc(1:N_OFDM_SYM, (SC_IND_DATA - N_SC/2), 100*fftshift( reshape(evm_mat(:,sp), [], N_OFDM_SYM), 1));
hold on;
h = line([1,N_OFDM_SYM],[0,0]);
set(h,'Color',[1 0 0]);
set(h,'LineStyle',':');
hold off;
grid on
xlabel('OFDM Symbol Index');
ylabel('Subcarrier Index');
title(sprintf('Stream from UE %d', sp));
h = colorbar;
set(get(h,'title'),'string','EVM (%)');
end
fprintf('\n MIMO Results:\n');
fprintf('Num Bytes: %d\n', N_UE*N_DATA_SYMS * log2(MOD_ORDER) / 8);
fprintf('Sym Errors: %d (of %d total symbols)\n', sym_errs, N_UE * N_DATA_SYMS);
fprintf('Bit Errors: %d (of %d total bits)\n', bit_errs, N_UE*N_DATA_SYMS * log2(MOD_ORDER));
fprintf("SNRs (dB): \n");
fprintf("%.2f\t", snr_mat);
fprintf("\n");
This driver is written for TDD SISO communication between two Iris boards. It is assumed that the communication between the boards is done through block ram transmission mode where a whole block of data (packet) is transmitted and received at slots allocated for these two functionalities. Note that block ram transmission corresponds to a P (pilot transmission) in the TDD scheduling map.
Click here for the complete code.
We begin by defining the iris_py class which will be the interface between the Matlab script that calls the Matlab driver and the underlying Python driver and object. The list of properties include parameters such as sample rate, transmitter gain, and frequency, receiver gain and frequency, number of samples of the packet. A variable sdr_params will be used to receive the experiment’s values for the aforementioned parameters and py_obj will contain the Python object created through the Python Driver’s object class corresponding to an Iris node.
Note that we must zero-pad our data to account for the analog path and over-the-air delays. Example values for zero-padding are given in the tutorial scripts that follow. Also, it is important that the number of samples to be transmitted is below 4096 including zero-padding. This is due to the size of the Iris board ram that we use for bursty transmission.
classdef iris_py < handle
properties
sdr_params;
py_obj; %pyhton object
% Parameters to feed python (pun very intended!)
serial_id;
sample_rate;
tx_freq;
rx_freq;
bw;
tx_gain;
rx_gain;
n_samp;
tdd_sched;
n_zpad_samp; %number of zero-padding samples
end
The constructor initializes the experiment’s parameters received from the caller Matlab script(s), creates a python object and assigns it to the variable py_obj, and passes the parameters to that Python object.
function obj = iris_py(sdr_params)
if nargin > 0
obj.sdr_params = sdr_params;
obj.serial_id = sdr_params.id;
obj.sample_rate = sdr_params.sample_rate;
obj.tx_freq = sdr_params.txfreq;
obj.rx_freq = sdr_params.rxfreq;
obj.tx_gain = sdr_params.txgain;
obj.rx_gain = sdr_params.rxgain;
obj.n_samp = sdr_params.n_samp;
obj.tdd_sched = sdr_params.tdd_sched;
obj.n_zpad_samp = sdr_params.n_zpad_samp;
obj.py_obj = py.iris_py.Iris_py( pyargs('serial_id',obj.serial_id,...
'tx_freq', obj.tx_freq, 'rx_freq', obj.rx_freq,...
'tx_gain',obj.tx_gain,'rx_gain',obj.rx_gain,...
'sample_rate',obj.sample_rate, 'n_samp',obj.n_samp,'n_zpad_samp',obj.n_zpad_samp) );
end
end
end
The functions below are a bridge between the Matlab experiment scripts and the underlying Python Driver and objects. They translate calls from the scripts to calls on the Python objects.
Generate a trigger signal to start the frame count. Used in the chained mode where the boards are chained together and exchange control commands directly and off the air.
function sdrtrigger(obj, trig)
obj.py_obj.set_trigger(pyargs('trig',trig));
end
In the chained mode, used by the node assumed to be the base station to synchronize the delays between itself and the UE node. In the unchained mode, it resets the correlator on the UE node to start trying to find the beginning of the frame by correlating with a known sequence, i.e., the beacon.
function sdrsync(obj, is_bs)
obj.py_obj.sync_delays(pyargs('is_bs', is_bs));
end
Called to setup the correlator on the UE node.
function sdr_setcorr(obj)
obj.py_obj.set_corr();
end
This function will turn on the gain control on the transmitter side.
function sdr_txgainctrl(obj)
obj.py_obj.tx_gain_ctrl();
end
In the unchained mode, by calling the function below, the base station node will burn the beacon sequence onto the base station' ram to be transmitted at each pilot slot in the scheduling frame.
function sdr_txbeacon(obj, prefix_len)
obj.py_obj.burn_beacon( pyargs('prefix_len', prefix_len) );
end
Configure the scheduling frame on an Iris node. If in chained mode, the same functionality is expected by both the UE and the base station nodes. Otherwise, the UE needs to configure the trigger_out parameter.
function set_config(obj, chained_mode, is_bs, trigger_out)
if chained_mode
obj.py_obj.config_sdr_tdd_chained(pyargs('tdd_sched', obj.tdd_sched));
else
obj.py_obj.config_sdr_tdd( pyargs('tdd_sched', obj.tdd_sched, 'is_bs', is_bs, 'trigger_out', trigger_out));
end
end
These functions set the receiver streams up and activate them.
function sdrrxsetup(obj)
obj.py_obj.setup_stream_rx();
end
function sdr_activate_rx(obj)
obj.py_obj.activate_stream_rx();
end
The data packet received from the caller Matlab script are burnt onto the node’s ram memory.
function sdrtx(obj, data)
re = real(data);
im = imag(data);
obj.py_obj.burn_data( pyargs('data_r', re, 'data_i', im) );
end
This function reads the recieved data sent from the Python driver, turns them into complex floats and sends them back to the caller Matlab script.
function [data, len] = sdrrx(obj)
rcv_data = obj.py_obj.recv_stream_tdd();
data = double( py.array.array( 'd',py.numpy.nditer( py.numpy.real(rcv_data) ) ) ) + ...
1i*double( py.array.array( 'd',py.numpy.nditer( py.numpy.imag(rcv_data) ) ) );
len = length(data);
end
Finally, using the function bellow, the opened streams are closed and the Iris boards are reset.
function sdr_close(obj)
obj.py_obj.close();
delete(obj.py_obj);
end
This piece of software serves as an intermediate between Matlab and the Iris nodes. It has the same functionalities described in the Python Basic Tutorial with the only difference being the repackaging of these functionalities in a class -properties - methods software scheme. Further explanations on each function are provided in the Matlab Driver where we comment on these functions' Matlab equivalents.
In writing this driver we made the following assumptions:
Transmission and reception follow a TDD pattern. This is set by defining a TDD schedule on the nodes used in the experiment. This schedule describes a slotted frame and defines the action in each slot. These actions, for now, are ‘P’, i.e., transmit a “pilot”, or better, transmit what is on the FPGA ram, ‘R’, read, and ‘G’ guard band. Further details on TDD framing may be found in Python Basic Tutorial.
The transmission of data is done in bursts through the ram memory on each node. Data to be transmitted is first burnt on the ram and is transmitted at every ‘P’ pilot slot in accordance with the predefined TDD schedule.
It is assumed that the boards may be chained and hence share control information directly without wireless transmission, or not. In the first case, a trigger signal initiated by the node assumed to be the base station will signal the start of the frame. In the second case, at each pilot slot of the frame, the base station will send a beacon sequence known to and pre-installed on the UE. The UE will correlate the received signal with the local replica and when this correlation successfully passes a threshold, the UE assumes the frame has started and will read or transmit data according to the predefined TDD schedule.
Agora is a high-performance system for massive-MIMO baseband processing. Agora uses a queue-based master-worker model. A master thread is responsible for scheduling tasks and a pool of worker threads are responsible for executing tasks. The communication between the master thread worker threads are achieved through single-producer multi-consumer or multi-producer single-consumer shared memory FIFO queues (concurrentqueue).
As shown in the figure below, Agora has a socket interface to communicate with remote radio unit (RRU) and a MAC interface to communicate with upper layer. Baseband processing blocks are implemented as Doer functions.
DoFFT does FFT of an OFDM symbol to convert data from time domain to frequency domain.
(OFDM_CA_NUM
: number of subcarriers in an OFDM symbol, BS_ANT_NUM
: number of base station antennas, fft_block_size
: task granularity of FFT, values are given in config.cpp)
BS_ANT_NUM
per symbolfft_block_size
OFDM_CA_NUM
, data type: 2-bytes integer (2 bytes for I and 2 bytes for Q)socket_buffer
OFDM_CA_NUM
, data type: complex float (4 bytes for I and 4 bytes for Q)data_buffer
data_buffer
calib_buffer
DoRecip calculates the reciprocal calibration matrix.
(BS_ANT_NUM
: number of base station antennas, OFDM_DATA_NUM
: number of data subcarriers in an OFDM symbol, recip_block_size
: task granularity of Recip, values are given in config.cpp)
OFDM_DATA_NUM
per reciprocity calibration symbolrecip_block_size
(processing recip_block_size
subcarriers in on function call)BS_ANT_NUM
, data type: complex floatscalib_buffer
BS_ANT_NUM
, data type: complex floatrecip_buffer
DoZF does precoder calculation to get precoder matrix from CSI matrix with Zeroforcing method.
(BS_ANT_NUM
: number of base station antennas, UE_NUM
: number of users, OFDM_DATA_NUM
: number of data subcarriers in an OFDM symbol, zf_block_size
: task granularity of ZF, values are given in config.cpp)
OFDM_DATA_NUM
per pilot symbolzf_block_size
(processing zf_block_size
subcarriers in on function call)csi_buffer
: data size BS_ANT_NUM x UE_NUM
(stored with partial transposed layout)recip_buffer
: data size BS_ANT_NUM
, used for reciprocal calibration to get downlink precoder matrixUE_NUM x BS_ANT_NUM
, data type: complex floatul_zf_buffer
dl_zf_buffer
csi_gather_buffer
for CSI matrix with continuous layoutDoDemul does equalization and demodulation to get log-likelihood ratios (LLRs), which are the input to soft-decision decoding.
(BS_ANT_NUM
: number of base station antennas, UE_NUM
: number of users, OFDM_DATA_NUM
: number of data subcarriers in an OFDM symbol, demul_block_size
: task granularity of Demul, values are given in config.cc)
OFDM_DATA_NUM
per uplink data symboldemul_block_size
(processing demul_block_size
subcarriers in on function call)data_buffer
: data vector with data size: BS_ANT_NUM x 1
(stored with partial transposed layout)ul_zf_buffer
: precoder matrix with data size UE_NUM x BS_ANT_NUM
int8_t
demod_soft_buffer
: LLRs vector with data size: UE_NUM x 1
smp_buffer
with continuous layoutDoDecode does LDPC decoding.
(UE_NUM
: number of users, LDPC_config.cbCodewLen
: number of encoded bits in a code block, LDPC_config.cbLen
: number of information bits in a code block, LDPC_config.nblocksInSymbol
: number of code blocks in a symbol, values are given in config.cpp)
UE_NUM * LDPC_config.nblocksInSymbol
in a symbolint8_t
demod_soft_buffer
: LLR vector with LDPC_config.cbCodewLen
bits (saved as int8_t bytes)uint8_t
decoded_buffer
: decoded information bits with size LDPC_config.cbLen