Amarisoft

TRX API

 

The purpose of this tutorial is to explain about the architecture and details of TRX API. TRX API is a set of specification defined by Amarisoft which is designed for implementing software libraries connecting various RF hardware (e.g, sdr card, cpri card, USRP etc) to Amarisoft software (e.g, Amarisoft Callbox). We are sharing the specification of TRX API for other users to implement their own software libraries to connect user's hardware to Amarisoft application.

NOTE : For the detailed document on TRX API specification, refer to this.

Table of Contents

 

 

Underlying Structure

The underlying structure and functionality of TRX API are illustrate as below. As shown here, the component labeled as TRX Driver has a set of APIs (i.e, TRX API) which are mapped to various trx operation functions of Amarisoft Application and controls/communicate with RF hardware indirectly (i.e, via RF Driver(another layer of driver stack) or directly.

 

Implementation Example in Amarisoft Product

Following is some of example architectures of TRX driver being used in Amarisoft product. As illustrated here, TRX API has widely and long been used in Amarisoft software stack implying that the architecture has well been verified.

NOTE : the f() in the API name represents init(),start(),read(),write(),end() collectively.

NOTE : The mapping between Amarisoft Application software and the TRX driver for a specific hardware is mapped in rf_driver section in the enb configuration file.

As a concrete example, the architecture of Amarisoft sdr driver is illustrate below. For Amarisoft SDR card, the TRX driver named trx_sdr.so is implemented according to TRX API specification. In case of Amarisoft sdr, trx_sdr (TRX Driver) is mapped to Amarisoft Callbox or UEsim software and communicating with RF driver (libsdr.so and sdr.ko) which communicate/control the physical SDR card.

NOTE :  The TRX driver should be placed in /root/enb and the name of the driver (derived from the trx driver file name) should be set to rf_driver name in enb configuration file.

 

 

TRX_Dummy : The Template

As a starter of the TRX driver implementation, we provide a simple template of TRX driver named trx_example.c. You may use this template as a starting point, change the name of the function and adding your own code for controlling/connecting to RF hardware.

 

Overall Structure

Overall structure of trx_example.c is as shown below. It has several core APIs as specified in TRX API specification, named trx_driver_init, trx_dummy_start, trx_dummy_read, trx_dummy_write, trx_dummy_end respectively. Each of these functions are mapped to trx functions in Amarisoft Application as specified in the specification. One major difference between this template and real implementation is that the API implemented this function does not have any real connection to lower layer drivers which are connected to any hardware.

 

Where to get ?

The TRX API template is included in installation package but does not get installed on the system during the installation procedure. You need to unpack (untar) this component manually from the installation package.

Once you unpack (untar) the trx_example component, you will get the trx_example.c source code, trx_driver header file and Makefile to compile it.

 

Code Review

Now let's look into the source file and see what it mean. What you can do with this part is to try to understand overall code structure and figure out where to put your own routine to control/communicate with your own RF hardware.

 

trx_driver_init

As you see in the diagram of TRX API lifetime , trx_driver_init would be the first function being called to establish connection between Amarisoft Application software and TRX driver. As the name indicate, it performs various steps for initialization of TRX driver and make mapping between TRX API interface functions (e.g, trx_read_func2, trx_write_func2 etc) and user defined functions(e.g, trx_dummy_read, trx_dummy_write etc). Usually this functions does various parameter initialization but would not perform any of RF hardware configuration.

/*

 * Initializes the TRX driver with a given TRXState.

 * This function checks for ABI compatibility between the LTEENB and the TRX driver. If there's a mismatch, it logs an error message and returns -1 to indicate failure.

 * On success, it allocates and initializes a TRXDummyState structure, setting its `dump_max` field based on a parameter.

 * It then updates the TRXState with pointers to dummy functions for various operations (end, write, read, start, get_sample_rate),  effectively setting up the TRX driver to use these dummy

 * implementations.

 * Returns 0 on successful initialization.

 * The critical part of this function is to map the user defined function to TRX API template function pointer

 * - `s1->opaque` is assigned to `s`, linking the TRX driver state with a user-defined structure for internal use.

 * - The next lines assign dummy functions to the TRX driver's operational callbacks:

 *   - trx_dummy_end maps to `trx_end_func` for ending a TRX session,

 *   - trx_dummy_write maps to `trx_write_func2` for writing data,

 *   - trx_dummy_read maps to `trx_read_func2` for reading data,

 *   - trx_dummy_start maps to `trx_start_func2` for starting a TRX session,

 *   - trx_dummy_get_sample_rate_func maps to `trx_get_sample_rate_func` for getting the sample rate.

 * These assignments ensure that the TRX driver operates with predefined dummy behaviors for critical operations.

 */

int trx_driver_init(TRXState *s1)

{

    TRXDummyState *s;

    double val;

 

    if (s1->trx_api_version != TRX_API_VERSION) {

        fprintf(stderr, "ABI compatibility mismatch between LTEENB and TRX driver (LTEENB ABI version=%d, TRX driver ABI version=%d)\n",

                s1->trx_api_version, TRX_API_VERSION);

        return -1;

    }

 

    s = malloc(sizeof(TRXDummyState));

    memset(s, 0, sizeof(*s));

    s->dump_max = 0;

 

    /* option to dump the maximum sample value */

    if (trx_get_param_double(s1, &val, "dump_max") >= 0)

        s->dump_max = (val != 0);

    

    s1->opaque = s;

    s1->trx_end_func = trx_dummy_end;

    s1->trx_write_func2 = trx_dummy_write;

    s1->trx_read_func2 = trx_dummy_read;

    s1->trx_start_func2 = trx_dummy_start;

    s1->trx_get_sample_rate_func = trx_dummy_get_sample_rate;

    return 0;

}

 

trx_dummy_start

As you see in the diagram of TRX API lifetime , this function corresponds to 'Start' step of the diagram. This is the good place where you start RF hardware and initialize the time stamp. You may add your own routine for starting your RF hardware (e.g, function call for your hardware specific API) in this function.

/*

 * This function initializes the dummy start process for a TRX session.

 * It first retrieves the internal state from the opaque pointer in the TRXState structure. The function checks if the number of RF ports is exactly one, as only one TX port is supported.

 * If not, it returns -1 to indicate an error.

 * It then sets the sample rate by dividing the numerator by the denominator from the provided parameters. The TX and RX channel counts are also set based on the input parameters.

 * A timestamp is generated using `gettimeofday` to compute the first RX timestamp in sample rate units. This involves converting the current time in seconds to sample rate units and adding the

 *  microseconds part, also converted to sample rate units.Finally, it records the last disp time using `get_time_us()` and returns 0 to indicate success.

 */

static int trx_dummy_start(TRXState *s1, const TRXDriverParams2 *p)

{

    TRXDummyState *s = s1->opaque;

    struct timeval tv;

 

    if (p->rf_port_count != 1)

        return -1; /* only one TX port is supported */

 

    s->sample_rate = p->sample_rate[0].num / p->sample_rate[0].den;

    s->tx_channel_count = p->tx_channel_count;

    s->rx_channel_count = p->rx_channel_count;

 

    gettimeofday(&tv, NULL);

    /* compute first RX timetamp in sample rate units */

    s->rx_timestamp = (int64_t)tv.tv_sec * s->sample_rate +

        ((int64_t)tv.tv_usec * s->sample_rate / 1000000);

 

    s->last_disp_time = get_time_us();

    return 0;

}

 

trx_dummy_read

trx_dummy_read is the function where you put the routine to read(recieve) IQ data from RF hardware. You have to implement a routine in this function that waits until IQ data from hardware is available and read a chunk of data when available. And then put the time stamp to the read chunk. (NOTE : for futher details of this function, refer to this)

/*

 * This function simulates reading samples for a TRX session.

 * It updates the timestamp for received samples and increments the received sample count. The function then calculates the end time for the current batch of samples based on the updated timestamp.

 * A loop is used to simulate the delay until the end time, using the PC's real-time clock. If the calculated delay is more than 10 milliseconds, it's capped at 10 milliseconds to avoid long sleeps.

 * Finally, for each RX channel, it fills the provided sample buffer with zeros to simulate receiving blank samples.

 * The function returns the count of samples supposedly read, which matches the requested count.

 *

 * Parameters:

 * - s1: Pointer to the TRXState structure, which includes the opaque pointer to the TRXDummyState.

 * - timestamp: The timestamp for the samples being read.

 * - samples: An array of pointers to the sample buffers for each TX channel.

 * - count: The number of samples per channel.

 * - rf_port_index: The index of the RF port being written to.

 * - md: Metadata associated with the write operation, including flags.

 */

static int trx_dummy_read(TRXState *s1, trx_timestamp_t *ptimestamp, void **psamples, int count, int rf_port_index, TRXReadMetadata *md)

{

    TRXDummyState *s = s1->opaque;

    int64_t end_time, d;

    TRXComplex *samples;

    int j;

 

    *ptimestamp = s->rx_timestamp;

    s->rx_timestamp += count;

    s->rx_count += count;

    end_time = ts_to_time(s, s->rx_timestamp);

    /* Since we don't have a real sample source, we just return zero samples and use the PC real time clock as time source */

    for(;;) {

        d = end_time - get_time_us();

        if (d <= 0)

            break;

        if (d > 10000)

            d = 10000;

        usleep(d);

    }

    

    for(j = 0; j < s->rx_channel_count; j++) {

        samples = psamples[j];

        memset(samples, 0, count * sizeof(TRXComplex));

    }

    return count;

}

 

trx_dummy_write

trx_dummy_write is the function where you put the routine to write(transmit) IQ data to RF hardware. You have to implement a routine in this function that send a chunk of IQ data to RF hardware. (NOTE : for futher details of this function, refer to this)

/*

 * This function handles the writing of samples in the dummy TRX implementation. It processes the samples provided to it, updating the maximum sample value and saturation count.

 *

 * Parameters:

 * - s1: Pointer to the TRXState structure, which includes the opaque pointer to the TRXDummyState.

 * - timestamp: The timestamp for the samples being written.

 * - samples: An array of pointers to the sample buffers for each TX channel.

 * - count: The number of samples per channel.

 * - rf_port_index: The index of the RF port being written to.

 * - md: Metadata associated with the write operation, including flags.

 *

 * The function first checks if the write operation is not just padding and if the dump_max flag is set.

 * It then iterates over each TX channel and each sample within the channel:

 * - Computes the absolute value of each sample.

 * - Updates the saturation count if the sample value is at or above the maximum representable value (1.0).

 * - Tracks the maximum sample value encountered.

 *

 * If more than 2 seconds have passed since the last display, it prints the maximum sample value and saturation count, then resets these metrics and updates the last display time.

 * Finally, it increments the total count of transmitted samples.

 */

 

static void trx_dummy_write(TRXState *s1, trx_timestamp_t timestamp, const void **samples, int count, int rf_port_index, TRXWriteMetadata *md)

{

    TRXDummyState *s = s1->opaque;

 

    if (!(md->flags & TRX_WRITE_FLAG_PADDING) && s->dump_max) {

        const float *tab;

        int i, j;

        float v_max, v;

 

        v_max = s->max_sample;

        for(j = 0; j < s->tx_channel_count; j++) {

            tab = (const float *)samples[j];

            for(i = 0; i < count * 2; i++) {

                v = fabsf(tab[i]);

                /* Note: 1.0 corresponds to the maximum value */

                if (v >= 1.0)

                    s->sat_count++;

                if (v > v_max) {

                    v_max = v;

                }

            }

        }

        s->max_sample = v_max;

        if ((get_time_us() - s->last_disp_time) >= 2000000) {

            printf("max_sample=%0.3f sat=%d\n", s->max_sample, s->sat_count);

            s->max_sample = 0;

            s->sat_count = 0;

            s->last_disp_time = get_time_us();

        }

    }

 

    s->tx_count += count;

}