SDR API
The purpose of this tutorial is to explain about the architecture and details of sdr_example.c which is located in /root/trx_sdr/api and provide a very simple example of how you can extend the sample code to your own application. The example provided with the installation package would open the possibility of using Amarisoft SDR card as a part of your own software stack.
Table of Contents
Introduction
Software Defined Radio (SDR) represents a transformative approach to radio communications, enabling radio functionalities to be implemented and modified through software rather than traditional hardware components. At the core of SDR technology is the flexibility to process and generate radio signals entirely in the digital domain, allowing for rapid prototyping, easy updates, and support for multiple communication standards on a single hardware platform. The Amarisoft SDR solution is a prominent example in this field, offering a powerful SDR card and an associated software stack that caters to a wide array of wireless communication applications. The sdr_example.c file, located in the /root/trx_sdr/api directory, serves as a reference implementation and starting point for developers aiming to interact with the SDR hardware through a well-defined API. This tutorial delves into the architectural aspects of sdr_example.c, elucidating its structure, initialization routines, interaction patterns with the SDR hardware, and the modular design that allows for extensibility. By understanding the example code and its underlying architecture, developers can efficiently integrate Amarisoft SDR capabilities into their own applications, leveraging the hardware's real-time signal processing features, high throughput capabilities, and seamless software-hardware interfacing. The tutorial further demonstrates a minimal extension, miniGen, to showcase how the foundational concepts in sdr_example.c can be adapted for custom development, underlining SDR's significance as a cornerstone in modern wireless system design and prototyping.
-
Context and Background
- SDR technology abstracts radio signal processing into software, enhancing flexibility and enabling rapid deployment of new protocols.
- Amarisoft SDR cards are widely used in research, prototyping, and production deployments for cellular and wireless communication systems.
- The sdr_example.c file exemplifies direct API interactions with the SDR hardware, providing a template for user-defined radio applications.
-
Relevance and Importance of the Tutorial
- Understanding sdr_example.c is essential for developers seeking to harness the full potential of the Amarisoft SDR platform in custom applications.
- The tutorial bridges the gap between basic SDR card usage and advanced application development tailored to specific wireless needs.
- It highlights best practices for SDR integration, initialization, data flow management, and modular extension within user software stacks.
-
Learning Outcomes
- Comprehend the architectural layout and workflow of sdr_example.c and its role in the SDR software stack.
- Gain insights into SDR API initialization, data handling routines, and event processing mechanisms.
- Understand how to extend the example code to implement bespoke signal generation or processing features, as illustrated with miniGen.
- Develop the capability to design and prototype custom SDR applications using Amarisoft hardware and API interfaces.
-
Prerequisite Knowledge and Skills
- Familiarity with C programming and software development in a Linux environment.
- Basic understanding of digital signal processing (DSP) concepts and radio communication fundamentals.
- Knowledge of how hardware APIs interact with user-space applications for real-time data transfer and control.
- Prior experience with build systems, compilation, and debugging of C code is advantageous for following code modifications and extensions.
Summary of the Tutorial
This tutorial provides a comprehensive overview of using the sdr_example.c application, which demonstrates how to interact with an SDR (Software Defined Radio) device through the libsdr.so library and related APIs. The tutorial details the underlying structure, essential APIs, and a step-by-step methodology for extending the example into a user-specific application, illustrated through the implementation of a simple signal generator called miniGen.
-
Test Setup and Methodology
- The test environment requires an Amarisoft Callbox or UEsim system with at least one SDR card. In the tutorial, two systems are used: the code is executed on the Callbox while the UEsim acts as a spectrum analyzer to validate the transmitted signals.
- The directory structure for the custom application (miniGen) is established under /root/trx_sdr/api. Proper symbolic links are created to reference the necessary library files (libc_wrapper_sdr.so and libsdr.so).
-
General Test Procedure
- Initialize various parameters using default values or user-defined values.
- Start the SDR card using the appropriate API call.
- Set the transmit gain either to a default or user-specified value.
- Create and execute RX and TX threads. These threads typically run infinite loops that continuously read from (RX) and write to (TX) the SDR card.
- On completion of both RX and TX threads, close and release the SDR resources.
-
Application-Specific Procedure: miniGen Signal Generator
-
Code Extension and Structure:
- The sdr_example.c template is copied and modified to create miniGen.c.
- Core functions such as rx_thread_func() and main() are revised.
- New functions are added:
- process_runtime_input()
- process_runtime_user_input()
- reset_sdr()
- GenerateQAM()
-
Runtime Operation:
- Upon execution, the application prints basic information and begins transmitting a signal stream.
- Users can interactively modify signal parameters in real time via command prompts:
- tx_gain: Change or query the current transmit gain.
- tx_freq: Change or query the transmit frequency (Hz).
- tx_bw: Change or query the transmit bandwidth (Hz).
- Parameter changes are reflected immediately in the transmitted signal, which can be validated using a connected spectrum analyzer.
-
Signal Generation Methodology:
- The GenerateQAM() function generates a sequence of IQ data, populating a buffer with complex values representative of a QAM (Quadrature Amplitude Modulation) signal. This buffer is then passed to the transmit buffer of the SDR port for transmission.
- Parameters to GenerateQAM() include:
- Pointer to the buffer (buf) for complex numbers
- Buffer length (len)
- QAM modulation order (M)
- The function iterates over the buffer, generating random x and y coordinates for each symbol, ensuring placement on a QAM constellation grid.
-
Code Extension and Structure:
-
Core APIs Used in Procedures
- Opening and initializing the SDR device: msdr_open()
- Setting and releasing start parameters: msdr_set_default_start_params(), msdr_release_start_params()
- Starting and stopping the SDR: msdr_start(), msdr_stop()
- Gain configuration and querying: msdr_set_tx_gain(), msdr_get_tx_gain(), msdr_set_rx_gain(), msdr_get_rx_gain()
-
Key Notes on Application Development
- Modification and extension of rx_thread_func() and tx_thread_func() are crucial for tailoring the application to specific use cases.
- The RX thread is essential for timing and synchronization, even for TX-only applications.
In summary, the tutorial outlines a clear methodology for developing and testing custom SDR applications using libsdr.so, detailing both the setup and operational steps, while emphasizing the importance of proper application structure and runtime parameter control.
Where to Get ?
The sdr_example.c is provided with installation package and you can find the code and required libraries at /root/trx_sdr/api.

Underlying Structure
sdr_example.c is running on top of sdr device driver and libsdr.so library. The overall structure of sdr_example.c and underlying components are illustrated below. libsdr.so is communicating with sdr device driver and provide various API for user program (e.g, sdr_example.c in this case).
You can check out the entire list of the API functions provided by libsdr.so from libsdr.h file located in /root/trx_sdr/api. Some of the API functions that are most commonly used by user program are those functions starting with msdr :
- msdr_open()
- msdr_set_default_start_params()
- msdr_release_start_params()
- msdr_start()
- msdr_stop()
- msdr_set_tx_gain()
- msdr_set_rx_gain()
- msdr_get_tx_gain()
- msdr_get_rx_gain()
The functions of user program would vary widely depending on the purpose (application) of the program, but the template code (sdr_example.c) implements several important functions as below. Whatever application you would create, it is highly likely that these functions will be used in your own application as well.
- thread_configure()
- rx_thread_func()
- rx_thread_func()
- msdr_tx_gain_adjust()

Overall flow of sdr_example.c is illustrated below. I think you would follow this basic sequence in your own application as well.
- i) Initialize various parameters with internal default and the initial values set by your own code
- ii) Start sdr card
- iii) Set the tx_gain of sdr_card with default value or the value set by your own code
- iv) create and execute rx_thread and tx_thread. This part is the core of the user application. Usually there is infinate loop within these function that are continuously reading and writing data from/to sdr card.
- v) When the both rx_thread and tx_thread finished, close sdr

The most important component of sdr_example are the two functions : rx_thread_func and tx_thread_func. There are some dependancies between these two function as illustrated below. As show in this illustration, the rx_thread is essential because it gives the timing information from the hardware. Even for a pure TX application (e.g. signal generator) you would need to have this thread to trigger the tx thread as needed..

Basic APIs
There are a long list of APIs supported by libsdr.so (declared in /root/trx_sdr/api/libsdr.h). But the list of the most important APIs (especially for this tutorial) can be listed as below.
|
MultiSDRState *msdr_open(const char *args); void msdr_set_default_start_params(MultiSDRState *s, SDRStartParams *p, size_t p_size,int tx_count, int rx_count, int port_count); void msdr_release_start_params(MultiSDRState *p, size_t p_size); int msdr_set_start_params(MultiSDRState *s, const SDRStartParams *p, size_t p_size); int msdr_start(MultiSDRState *s); int msdr_stop(MultiSDRState *s);
int msdr_set_tx_gain(MultiSDRState *s, int channel, double gain); int msdr_set_rx_gain(MultiSDRState *s, int channel, double gain); double msdr_get_tx_gain(MultiSDRState *s, int channel); double msdr_get_rx_gain(MultiSDRState *s, int channel); |
Example 1 : Simple Signal Generator
Just as an example of showing how to modify/extend sdr_example.c for user specific application, I will write a code for very simple signal generator. It just generate continuous stream of QAM signal and transmit it to TX port of SDR card. For the simplicity of the description, I will call this application as miniGen.
Test Setup
To test the code, the only requirement would be to have Amarisoft Callbox or UEsim with at least one SDR card. In this tutorial, I used two systems (Callbox and UEsim). I am running my code on callbox and using UEsim as a spectrum analyzer to validate the output of my application.

Code Structure
The way I extend the sdr_example.c to my own application (miniGen.c) is illustrated as below. Basically I just copied sdr_example.c to miniGen.c and revise the existing functions and add a couple of new functions.
- Functions revised from the sdr_example.c
- rx_thread_func()
- rx_thread_func()
- main()
- Functions newly added
- process_runtime_input()
- process_runtime_user_input()
- reset_sdr()
- GenerateQAM()

Directory and Files
For this tutorial, I created a directory named miniGen under /root/trx_sdr/api. (

Since I created a new directory, I changed the symbolic links to properly points to the location of the library file (libc_wrapper_sdr.so and libsdr.so)

How it works
This is how my sample code (miniGen.c) works.
When you run the code, you will see a bunch of basic informations printed out. At this point, a stream of the signal start being transmitted as shown in the spectrum (

Now you can change the parameters of the signal generator by typing in command at the prompt. The parameters you can change in this example code are
- tx_gain : change TX gain or retrieve the current TX gain
- tx_freq : change TX frequency (in Hz) or retrieve the current TX frequency (in Hz)
- tx_bw : change TX Bandwidth (in Hz) or retrieve the current TX Bandwidth (in Hz)
First, let's change tx_gain. If you just type in 'tx_gain' and hit enter without specifying any specific value, it prints out the tx_gain which is currently set.

Now let's try setting a specific value for tx_gain by typing in the command and value. Then the specified value is applied and you can confirm that result of the execution on spectrum analyzer.

In the same way, you can use the tx_freq command. Just running tx_freq(TX Center Frequency) without a specified value to get the current setting and with a specified value to change tx_freq value.

In the same way, you can use the tx_bw command. Just running tx_bw (TX bandwidth) without a specified value to get the current setting and with a specified value to change tx_bw value.

Code Details
Now let's look a little bit further into the source of the application. You can get the entire code here. Here I will just take a look at some important highlights only and will not go through the entire code line by line.
GenerateQAM
The functionality of this function is simple. It generate a sequence of IQ data and store the sequence into a specified buffer. In this example application, the generated sequence will be stored to a specific TX buffer of sdr RF port(e.g, rf_ports[0].tx_buf[0]) which will eventually passed to msdr_write.
|
int GenerateQAM(Complex *buf, int len, int M) { int i; float x, y; int sqrtM = sqrt(M); // Assuming M is a perfect square for (i = 0; i < len; i++) { x = ((rand() % sqrtM) * 2 - sqrtM + 1); // Random number between -sqrt(M) and sqrt(M) y = ((rand() % sqrtM) * 2 - sqrtM + 1); // Random number between -sqrt(M) and sqrt(M) buf[i].re = x; buf[i].im = y; } return 0; } |
process_runtime_user_input
As easily guessed from the name of the function, this function is to process the input command from user. In this example application, a few threads (tx thread, rx thread) are always running in the background. So this function should be also run as a thread to process the customer input while the signal transmission is ongoing in the background.
|
/** * This function continuously processes user input from the command line for runtime configuration of the application. * It prompts the user for configuration settings, processes commands, and allows the user to quit the program. * * The function operates in a loop, reading input from the standard input. It supports two special commands: * - 'Q' or 'q': Quits the program by breaking the loop and setting a flag to stop further processing. * - 'h': Typically would display help information, though the handling for 'h' is expected to be within the `process_runtime_input` function. * * Any other input is passed to the `process_runtime_input` function for further processing, assuming the input is not just a newline. */ void *process_runtime_user_input() { char input[256];
while (keep_running) { printf("Type in configuration setting (Q to quit program, h for help) \n> "); fgets(input, sizeof(input), stdin);
// Remove trailing newline input[strcspn(input, "\n")] = 0; // Check if the user entered 'q' or 'Q' if (strcmp(input, "q") == 0 || strcmp(input, "Q") == 0) { keep_running = 0; break; } else { if (input[0]) process_runtime_input(input); }
} return NULL; }
/** * Parses the input string to extract the command and its optional argument. * Supports commands for displaying help, setting transmission frequency, gain, * bandwidth, and other operational parameters. * * Commands: * - h or help: Displays available commands and their usage. * - tx_freq <frequency>: Sets the transmit frequency in Hz. Displays current frequency if no value is provided. * - tx_gain <gain>: Sets the transmit gain. Displays current gain if no value is provided. * - tx_bw <bandwidth>: Sets the transmit bandwidth in Hz. Displays current bandwidth, sample rate, and buffer length if no value is provided. * @param input The raw input string containing a command and optionally its value. */ void process_runtime_input(char* input) { char* command = strtok(input, " "); char* optarg = strtok(NULL, " ");
if (strcmp(command, "tx_freq") == 0) { if (optarg != NULL) { // Set tx_freq } else { // Print current tx_freq } } else if (strcmp(command, "tx_gain") == 0) { if (optarg != NULL) { // Set tx_gain } else { // Print current tx_gain } } else if (strcmp(command, "tx_bw") == 0) { if (optarg != NULL) { // Set tx_bw } else { // Print current tx_bw } } else { // Handle unknown command } } |
reset_sdr
This functions is to reset sdr card. In current implementation of sdr library, the only parameter/configuration that can be changed while sdr is running is tx/rx gain change. All other configurations (e.g, frequency, bandwidth, sample rate etc) requires resetting the sdr. That's why I wrote this function to allow users to change frequency, bandwidth on the fly.
|
/** * Resets the software-defined radio (SDR) to a known state. * * This function performs a series of operations to safely reset the SDR. It ensures that the SDR is stopped, * reconfigured with new parameters, and restarted. The process involves: * * 1. Pausing any ongoing operations by setting a flag. * 2. Waiting for a brief period to ensure all operations have been halted. * 3. Stopping the SDR to ensure it's in a known state before reconfiguration. * 4. Attempting to set new start parameters for the SDR. If this fails, an error is reported and the program exits. * 5. Restarting the SDR with the new parameters. If this fails, an error is reported and the program exits. * 6. Resuming operations by clearing the pause flag, indicating the SDR is ready for use. * */ void reset_sdr() { paused = 1; usleep(100000); msdr_stop(s); /* Start */ ret = msdr_set_start_params(s, params, sizeof(*params)); if (ret < 0) { fprintf(stderr, "msdr_set_start_params: invalid SDR parameters\n"); exit(1); }
ret = msdr_start(s); //msdr_release_start_params(params, sizeof(*params)); <-- Commented out this because the same parameter structure is used again. if (ret < 0) { fprintf(stderr, "msdr_start: error\n"); exit(1); } paused = 0; } |
tx_thread_func
This function is the most important part of this example application (i.e, signal generator) but there are almost no change from the sdr_example.c. I just put a bunch of printf() to print out some basic information. However it is important to clearly understand what this function does. This function will be run as a thread in main()
|
/** * The transmission thread function for a sdr. * * This function is designed to run in a separate thread and handles the transmission of data through an SDR. It takes a pointer to an RFPort structure as its argument, which contains all the necessary * information and buffers for transmission. * * The function performs the following operations: * - Initializes the transmission thread and prints initial transmission parameters. * - Enters a loop that continues as long as a global `keep_running` flag is set. * Inside the loop, it: * - Locks the associated mutex to safely access shared resources. * - Waits for the condition variable if the sample count is less than the buffer length, indicating that there isn't enough data to transmit. * - Adjusts the sample count and transmission timestamp after waking up. * - Unlocks the mutex. * - Checks if the `keep_running` flag is still set; if not, exits the loop. * - Calls `msdr_write` to transmit data. If successful, adjusts the transmission timestamp based on the number of samples written. * * The loop exits either when `keep_running` is cleared(keep_running is set to 0 when you press Ctrl+C or 'q'/'Q'. */ static void* tx_thread_func(void *opaque) { ... thread_configure("TX%d", rfp->port_index);
// Print basic information
while (keep_running) { pthread_mutex_lock(&rfp->mutex); while (rfp->sample_count < rfp->buf_len && keep_running) { pthread_cond_wait(&rfp->cond, &rfp->mutex); } ... pthread_mutex_unlock(&rfp->mutex);
if (!keep_running) break;
// Write the data stored in rfp->tx_buf to sdr card to transmit. the data of rfp->tx_buf is prepared in GenerateQAM function. ret = msdr_write(rfp->msdr_state, rfp->tx_timestamp, (const void**)rfp->tx_buf, rfp->buf_len, rfp->port_index, &mdw); ... } return NULL; } |
main
What you need to note and understand is how to implement the flow. You should be familiar with this flow as much as possible since this flow would apply almost every sdr application software.

|
/** * This program configures and operates a sdr for both transmission (TX) and reception (RX). * It starts by setting default parameters for the SDR operation, including frequency, sample rate, gain, and channel * counts. These parameters can be overridden by command-line arguments provided by the user. * * The main steps of the program are as follows: * 1. Open the SDR device using the provided arguments or defaults. * 2. Set default start parameters for the SDR, including channel counts and operational modes. * 3. Adjust the transmission gain as specified. * 4. Initialize signal handling for graceful shutdown. * 5. Generate initial QAM-modulated signal data for transmission. * 6. Create and start separate threads for handling RX and TX operations for each RF port. This includes initializing synchronization primitives (mutexes and condition variables) * and starting the threads. * 7. Introduce a brief delay to ensure threads are running. * 8. Start a user input processing thread to handle runtime commands. * 9. Wait for the user input thread to finish, indicating the user has requested to stop the program. * 10. Signal all RX and TX threads to stop by setting a global flag and joining the threads to ensure they have completed. * 11. Finally, stop and close the SDR device to clean up resources. * * Throughout its execution, the program prints status messages to inform the user of its progress and any errors. * It also handles user interrupts (e.g., Ctrl+C) to ensure the SDR device is properly closed before exiting. */ int main(int argc, char **argv) {
// Set default paramters for variables/parameters // Reset the parameters if user run the program with command line options
s = msdr_open(args); ... msdr_set_default_start_params(s, params, sizeof(*params), tx_channel_count, rx_channel_count, rf_port_count);
...
/* Start */ ret = msdr_set_start_params(s, params, sizeof(*params)); ret = msdr_start(s);
/* Set the tx_gain */ .. msdr_tx_gain_adjust(s, &tx_gain); ...
/* flag setting for tx, rx thread execution */ keep_running = 1; signal(SIGINT, intHandler);
/* generate signal data */ GenerateQAM(rf_ports[0].tx_buf[0], rf_ports[0].buf_len, 256);
/* create threads */ for (p = 0; p < rf_port_count; p++) { RFPort *rfp = &rf_ports[p];
pthread_mutex_init(&rfp->mutex, NULL); pthread_cond_init(&rfp->cond, NULL); printf("[Procedure] Starting RX/TX threads\n"); pthread_create(&rfp->rx_thread_id, NULL, rx_thread_func, rfp); pthread_create(&rfp->tx_thread_id, NULL, tx_thread_func, rfp); } // Delay for 0.1 seconds usleep(100000);
// Create the user input processing thread pthread_t user_input_thread_id; pthread_create(&user_input_thread_id, NULL, process_runtime_user_input, NULL);
// Wait for end of input processing thread pthread_join(user_input_thread_id, NULL);
paused = 1; keep_running = 0;
for (p = 0; p < rf_port_count; p++) { RFPort *rfp = &rf_ports[p]; printf("[Procedure] Joining RX/TX threads\n"); pthread_join(rfp->rx_thread_id, NULL); /* Signal TX thread */ pthread_mutex_lock(&rfp->mutex); pthread_cond_signal(&rfp->cond); pthread_mutex_unlock(&rfp->mutex); pthread_join(rfp->tx_thread_id, NULL); }
/* stop and close sdr */ msdr_release_start_params(params, sizeof(*params)); msdr_stop(s); msdr_close(s); return 0; } |
/**
* Generates a Quadrature Amplitude Modulation (QAM) signal.
*
* This function fills a buffer with complex numbers representing a QAM signal.
*
* Parameters:
* - buf: A pointer to the first element of an array of Complex numbers where the QAM signal will be stored.
* - len: The length of the buffer, indicating how many Complex numbers should be generated.
* - M: The modulation order of the QAM signal. This function assumes M is a perfect square.
*
* The function iterates over the buffer, generating random x and y coordinates for each complex number. These coordinates are calculated to lie
* within the range of -sqrt(M) to sqrt(M), effectively placing them on a QAM constellation grid.
*
* Returns:
* - 1 on successful execution.
*/