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
- SDR API
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.
*/