DragonWins Home Page

BBC Home Page

BBC Software Applications Page



BBC Software Defined Radio (SDR) Engine

(Last Mod: 27 November 2010 21:37:20 )


Bookmarks on this page

Links to other pages



Overview

Drawing upon some of the structural concepts used by GNU Radio (GR), a new BBC real-time engine, call the SDR Engine, was developed for creating simple Software Defined Radio (SDR) simulators. This should permit users to readily implement working simulators and to develop and incorporate their own processing blocks. In fact, it is a completely generic framework suitable for building up processing systems for many kinds of systems, not just BBC.

Like the other engines, it is completely ANSI-C (C99) compliant.

The basic idea behind the SDR Engine is quite simple: A system, called a "radio", even though much more general processing engines can be constructed, is composed of a collection of SDRB's (SDR Blocks) that are connected together with FIFOs. It is roughly analogous to how one might built up a component stereo system: First you place all of the individual components, such as a CD player, a graphic equalizer, and a power amplifier, into a chassis (or perhaps just stack them together on a shelf) and then you connect them together with cables that carry information from one component to another. In the case of an SDR Engine, the SDR structure is the chassis, the SDRB's are the components, and FIFO's are the cables.

By connecting each block with a FIFO, the developer of the block is relieved from having to construct an interface that is specific to the blocks it will be interacting with --instead blocks simply pull the data they need from input FIFOs and push the data they produce into output FIFOs. Certainly each block still has to ensure that it knows how to interpret the data it pulls and how to format the data it pushes, but they don't have to worry about the mechanics of moving data in and out of the block.

Once the blocks are instantiated and connected together, the radio can be launched and allowed to start processing the data. The main SDR structure consists of an array of these blocks; when the radio is launched a scheduler runs that repeatedly cycles through the block, invoking each one in turn. In essence, what results in a cooperative multitasking system in which each block is executed and operates with the understanding that it is to perform only a small task before returning control to the scheduler. For instance, let's assume that a particular block computes the energy in a signal by simply squaring it. Then on each invocation it would remove one data value from its input FIFO, square that value, and push the result into its output FIFO. If there doesn't happen to be any data available in its input FIFO, then the block is expected to take an appropriate action and return control to the scheduler promptly; an appropriate action may be to do nothing or perhaps to push a zero into the output FIFO (the latter might make sense if a true real-time system were being carefully simulated), what would not be appropriate would be to wait until a piece of data became available. Similarly, if the output FIFO lacks sufficient space to accept the data that would be generated, the block must again take appropriate action that does not involve stalling the radio.

The engine itself is really nothing more than a framework that performs the overhead tasks of passing data from one user-defined function to another and then calling those functions to act on that data. As such, all of the "magic" is contained in the user-defined functions which, of course, have to be written in a pretty specific way in order to interact successfully with the engine. In general, each block needs requires a function that performs tasks in each of four phases: initialization, reset, runtime, and finalization. Of these, the runtime section is the most critical and is responsible for, on most invocations, consuming a small amount of data from its input ports and sending a small amount of data to its output ports. Much more these functions and how to develop them can be found by reading further.


A Simple Radio Example

The following is the main() function for a very simple radio that was used when developing the SDR Engine code:

int main(void)
{
	SDR *radio;

	// Instantiate the radio chassis
	radio = SDR_new(NULL, NULL);

	// Define Blocks
	SDR_new_block(radio, "Source", Source);
	SDR_new_block(radio, "Sink",   Sink);

	// Connect Blocks
	SDR_connect(radio, "Source:0", "Sink:0", 8, 16);

	// Tell the radio which block to monitor for end-of-run
	SDR_add_monitor(radio, "Sink");
	
	// Reset the radio
	SDR_reset(radio);

	// Run the radio if no errors logged during construction
	if (!SDR_errors(radio)
		SDR_run(radio);

	// Delete the entire radio
	SDR_delete(radio);

	return EXIT_SUCCESS;
}

Creating a New Radio

The SDR structure is the top level chassis, or "black box", that contains links to all of the blocks and FIFOs in the radio as well as other data needed to run the radio. From a high-level viewpoint, it is simply a container that must be passed to every function that is called so that the function has access to the radio's pieces. The programmer must call SDR_new() and store the pointer that is returned so that it can be passed, as the first argument, to all of the SDR_*() functions.

Each SDR contains a void pointer that is available for the programmer to use to point to a memory block that they want all of the blocks to be able to see and access. If the programmer wishes to attach such a block of memory at this time, they may pass a pointer to that block as the first parameter in the SDR_new() call. If they do not wish to add such a block, they simply pass a NULL pointer.

A radio normally goes through four distinct phases: Initialization, Reset, Run, and Deletion. The programmer may optionally pass the SDR_new() function pointers to functions that are to be executed during each of these phases, using NULL pointers for any that do not exist. These are a more advanced radio feature and most programmers can simply pass the SDR_new() function five NULL pointers. 

Instantiating SDR Processing Blocks

To add a block to the radio, the programmer uses the SDR_new_block() function. Each call creates one block and adds it to the radio; each block must be given a unique, case-sensitive name that will be used to refer to the block in the future. The behavior of the block is determined by a programmer defined function, a pointer to which is passed as the final argument

Connecting Blocks Together

Once blocks have been added to the radio, they can be connected together using the SDR_connect() function. The first argument (following the pointer to the SDR radio itself) is the block name that is the source of data for the connection. Following that is the block name of the of the sink for that connection's data. Next is the width, in bytes, of the connection followed by the desired depth of the connecting FIFO. Note that the length of the FIFO must be an integer power of two, however the FIFO creation function will handle this detail and increase the depth to the next larger power or two if necessary. Since blocks may have multiple ports, the port number can be appended to the block name using the format "blockname:portnumber". If no colon is found in the block name, then the function assumes that Port 0 is to be used.

Monitoring the Status of Selected Blocks

The next line of code in the example above calls the SDR_add_monitor() function. This function is used to inform the radio which block(s) to monitor so as to detect when all of the processing has completed and that the radio should stop. The function can be called multiple times and, each time it is called, the indicated block is added to a list; all of the blocks in the list have to stop running in order for the radio to halt. How this mechanism works is that each block maintains a status flag that indicates that it is still running. Each block also has access to these status flags for each of the blocks that feed its input FIFOs. One of the responsibilities of the runtime function is to use this information, along with the status of the input FIFOs and its own internal state, to determine when it will no longer be pushing data to any of its output FIFOs and to set its own status flag accordingly. Assuming that the blocks that serve as the data sources terminate, for instance by reaching the end of a data file, this should eventually trickle down the processing chain to the sink blocks. Eventually all of the blocks that are being monitors will stop running and, when that is detected, the radio as a whole will halt.

Resetting the Radio

Before the radio is launched, it is good practice to reset it to a known state. This function does two things: first, it calls the reset functions in each of the blocks that make up the radio and, second, it flushes the contents of all of the interconnecting FIFOs.

Testing the Radio for Errors

As the radio is built, initialized, connected, and reset, all of the functions have the ability to report an error to the radio. The SDR_errors() function merely returns the total number of errors that were reported, or, more accurately, the accumulated total of all the values returned during radio construction.

Running the SDR Radio

At this point, the next line of code, which is where all the action takes place, is actually a bit boring. The SDR_run() function simply checks all of the monitored blocks and, if the radio is still running, call the scheduler which makes one pass through the blocks invoking each one's runtime function. This continues until all of the monitored blocks have stopped running.

Deleting the SDR Radio

The final (non-trivial) line of code is the call to SDR_delete() which, as described above, goes through block-by-block and invokes that block's finalization function, deallocates its data structure (if not already done so), and deallocates the block itself. It also deallocates all of the connecting FIFOs. Finally, it deallocates itself.


The Anatomy of an SDR Processing Block

The Status Flag

Each block contains a variable that serves as a status flag to tell the other blocks, and the radio as a whole, whether it is still active. Before the radio is run, the user registers certain blocks with the radio for monitoring using the SDR_add_monitor() function. Normally, all of the sink blocks (those that do not output any data through a block port) should be monitored so that the radio will halt only after all data has made its way through all of the paths and the sink blocks have taken all action on them.

When the radio is run, each pass begins with a check to see if any of the blocks being monitored are active. If any of them are, or if no blocks are being monitored at all, then another pass through the radio takes place in which the following happens, block-by-block: If the block is not a source block, it's active status is set to active if any of its input ports are active. A input port is considered active if either the FIFO attached to it has any data or if the block feeding that FIFO is active. If none of its ports are active, then the block is marked as inactive. Keep in mind that source blocks, by definition, do not have any input ports, they are not subjected to this inspection and marking process; since a source block must get its data from some source other than a data path that the radio can monitor, the radio has no reasonable means of estimating whether it is still active or not. Hence only a source block's run function can update its active status and therefore every source block's run function needs to handle this housekeeping task.

If none of the blocks, other than the source blocks, update the block's status flag then the following sequence of events occurs: As the source block reaches the end of its data it goes inactive. The next block down the chain will be marked inactive once it has pulled the last of the data from FIFO connecting it to the source block. The block's run function will be invoked on any pass in which the radio changes its status from active to inactive. This is to permit the block the opportunity to override the radio and remark itself as active. This will cascade down the radio until, finally, all of the blocks feeding the sink block are marked inactive. At that point the sink block will run one more time and, unless it remarks itself as active before exiting, the radio will halt before starting the next pass. Thus, other than the source blocks, which must deal with the status flag, most radios will run properly if the status flag is completely ignored and the radio is allowed to mark the blocks automatically.

There are two exceptions to the "let the radio worry about its own status" rule. The obvious one is when a sink block is unable to complete its processing of the data in the same pass that it pulls the last data from the last active input port. Most of the time this should not be a problem since the block knows that it now has all of the data it is going to get and, if it is the only sink in the radio, there is no particular need to exit this pass quickly. However, if the sink block can't finish processing in the present pass, then all it needs to do is mark itself as still active before it returns. Note that it will need to continue marking itself active in each subsequent pass, as well. The other exception is for intermediate blocks that also cannot finish the processing of the last data they receive in the same pass that they receive it, they should also mark themselves as active until they have completed processing the data.

To get a deeper understanding of how the status flag works, and how it can be overridden, let's consider some of the details. All blocks are marked active when they are created with the SDR_add_block()function. This same function then calls the block's initialization function could, in theory, change its status to inactive. Similarly, if the SDR_reset() function is called prior to launching the radio, then it, too, marks all of the blocks as active (regardless of what a particular block's initialization function may have done). However, once marking the block as active, it calls the block's reset function which could change its status. It is difficult to image a situation when this would be a reasonable thing to do, but the capability is there, if needed. A more subtle detail is that block's will actually be allowed to run one time after they have pulled the last of their data. The reason is that because the radio does the status check and marking before calling the block's run function, thus the pass in which the block actually empties the last of the data from its input ports will not be the final time the block is called. The next pass is when the radio will detect that none of its ports are active and mark it inactive, but the radio will always call a block on any pass in which the radio changes it's status in order to give the block's run function an opportunity to override it. It's important to note once a block is marked inactive and that is allowed to stand by the block's run function on the pass in which it occurs, there is no simple mechanism to reactivate it until the run is complete and the radio is reset.

The Ports

When a block is instantiated it starts off with no ports of any kind. When blocks are connected together using the SDR_connect() function, an output port is instantiated in the "from" block and an input port is instantiated in the "to" block. Furthermore, a FIFO is instantiated and connected between the two blocks. The width of both ports and the FIFO is set to the requested width, which is given in bytes. The depth of the FIFO is the smallest integer power of two that is equal to or greater than the requested depth.

Each port is associated with a port number, which is indicated by appending ":number" to the block name. If no port number is indicated, then it will be assumed that Port 0 is being referred to.

Ports are instantiated in the blocks as connections are made. Any (non-negative integer) port number less than SDRB_PORTS may be specified. This parameter, found in the SRDB.h file, is 16 in the original version of the SDR Engine, but can be modified. Internally, each block maintains two arrays of pointers, each holding SDRB_PORTS entries, therefore the memory overhead for uninstantiated ports is not high, but can be an issue in memory-starved embedded systems.

The Data

Each block contains a void pointer that the programmer can use for their own purposes. Normally they would define a structure and store a pointer to that structure in this member, however there is nothing to prevent them for using it as a pointer to a single data element if that is all they need. While it is technically possible to use it to directly store data, this is not particularly easy and is highly discouraged.

The Radio will assign a NULL value to this pointer when the block is created and will never do anything else with it until the block is deleted. As one of the final housekeeping tasks during the deletion process and, specifically, after the programmer provided deletion function has returned, the SDR engine will attempt to free the memory pointed to. Thus it is important that the deletion function ensure that it is either NULL or that it points to a currently allocated block of memory that is safe to deallocate. Either approach is acceptable, but it is probably cleaner if the deletion process takes care of its own housekeeping and garbage collection and sets this pointer to NULL before returning.

Note that there is no means to pass parameters to any of the blocks functions. In simple blocks it is likely that this will suffice and all necessary information can simply be hardcoded into the functions or included in a data structure that gets attached. However, while this is adequate for very simple blocks or blocks that are in the early stage of development, sooner or later the programmer is going to want to be able to develop blocks that are flexible and which can be configured without having to modify the block's code. There are several ways to get around the inability to directly pass parameters to the initialization, as well as the other two functions, in order to achieve this. The first is to simply place the necessary parameters in a data structure that gets attached to the radio before the blocks are added so that the initialization function will have access to those parameters when it runs. A second method is to write block functions that can be called in order to control the block's configuration after it has been created. Yet another, though not generally recommended, method is to have the initialization function request input directly from the user; this method is probably reasonable to use when developing or testing code, but should be avoided in more polished blocks.

The Function

Each block contains a function pointer that the programmer assigns to the function that defines the behavior of that block. This is accomplished by passing the name of the function as the final parameter to the SDR_new_block() function. The function accepts two arguments, a pointer to the block itself and an integer, named when, telling it why the function is being called. A typical program will pass through four distinct phases: Initialization, Reset, Running, and Finalization. The function will be invoked during each of these phases and the value of when lets the function know what phase it is in so that it may act accordingly. The function returns an integer which is nominally the number of errors that were detected during execution of the function, however as long as it returns zero if there were no errors and a strictly positive value if there was, the system can respond appropriately.


The Radio and Block Data Pointers

The data block structure attached to the SDR structure (the radio-level data block) is a pre-defined structure that can be used to store parameters and make them available to the radio's processing blocks. The data blocks that are attached to the individual processing blocks, on the other hand, have no definition at all; instead, it is merely a pointer that is available to the developer of the processing block and they are free to use it in any way they desire.


Processing Block Functions

Each block contains a function pointer that the programmer assigns to the function that defines the behavior of that block. The function accepts two arguments, a pointer to the block itself and an integer, named when, telling it why the function is being called. A typical program will pass through four distinct phases: Initialization, Reset, Running, and Finalization. The function will be invoked during each of these phases and the value of when lets the function know what phase it is in so that it may act accordingly. The function returns an integer which is nominally the number of errors that were detected during execution of the function, however as long as it returns zero if there were no errors and a strictly positive value if there was, the system can respond appropriately.

The following is a skeleton template for a block function.

int template(SDRB *p, int when)
{
	int retvalue;
	IN_DATATYPE in_data;
	OUT_DATATYPE out_data;

	retvalue = 0; // Set retvalue to 1 to indicate errors.
	switch (when)
	{
		case SDRB_INI: // Initialation Behavior
			break;

		case SDRB_RST: // Reset Behavior
			break;

		case SDRB_RUN: // Runtime Behavior
			if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
			{
				//SDRB_pop(p, 0, &in_data);
				// Do something to generate out_data from in_data.
				//SDRB_push(p, 0, &out_data);
			}
			break;

		case SDRB_DEL: // Deletion Behavior
			break;

		default: // Should never reach this point.
			retvalue = 1;
	}

	return retvalue;
}

Where IN_DATATYPE and OUT_DATATYPE are merely types that are compatible with the data being exchanged with the ports.

The Initialization Phase

The function is invoked with when = SDRB_INI before the SDR_new_block() function returns. This section should perform any tasks that should be performed exactly once before the radio can be used. This is in contract to the Reset Phase, which is normally invoked prior to each run of the radio. For programs that only run the radio once, the distinction is largely moot.

The Reset Phase

The function is invoked with when = SDRB_INI whenever the SDR_reset() function is called. This section should perform any tasks that should be performed prior to each funning of the radio. This is in contract to the Initialization Phase, which only occurs once during the life of the radio. For programs that only run the radio once, the distinction is largely moot.

The Runtime Phase

The function is invoked with when = SDRB_RUN during each pass of the SDR_run() function. The SDR runtime scheduler, at the present time, is extremely simple: each block's function is invoked in the order that the blocks were added to the radio. For this reason, significant performance gains can sometimes be achieved by adding the blocks in order starting with the source and ending with the sink. While this approach works well with single-path radios, if a radio has multiple paths then getting it to run optimally, given the simplistic nature of the scheduler, is trickier. Of course, there is nothing to prevent the programmer from getting under the hood and writing their own scheduler, but for most applications the provided scheduler will likely be good enough.

The Runtime section of the block function is the most critical to obtaining proper radio function and performance. The skeleton code in the template function gives a feel for how this section should be written. Before doing anything, the block should determine whether there is sufficient data in the input ports and sufficient room in the output ports for it to do anything. In the case where the block consumes a single element of data and produces a single element of data, this is easily accomplished with full and empty checks. Note that SDRB_is_empty() is only capable of checking if an input FIFO is empty while SDRB_is_full() is only capable of checking if an output FIFO is full. If the necessary port conditions aren't satisfied, then the block function should normally return without taking any further action. If there is sufficient room, then it should pop the necessary data from the input ports, perform whatever processing is needed, and push the resulting data to the output ports.

The Finalization Phase

The function is invoked with when = SDRB_DEL whenever the SDR_delete() function to destroy the entire radio. Before deleting a block, that block's functions is given the opportunity to take care of any housekeeping tasks such as deallocating memory and closing files. One point to keep in mind is that the data structure that is attached to the block will be freed by the SDR_delete() function after the block's function completes; however it cannot free any memory associated with pointers that might be contained in that structure -- that is the block function's responsibility. The block function can also free the overall data structure if it desires, but if it does, then it is important that it also set the block's data pointer to NULL to prevent the structure from being freed a second time; the function SDRB_set_data() can be used for this purpose.


Radio Parameters

The original version of the radio permitted programmers to use the same block multiple times in the same radio, however there were significant limitations that had to be observed for it to work properly. First, the block function could not use static variables (at least not without considerable care) because different blocks would call the same function. That restriction still exists. To be sure, since each block function receives a pointer to the instance of the block that called it, a block function could be crafted that used static variables and dealt with this issue properly; however, the simplest solution is just to place all instance-specific data in a data structure that is attached to that instance of the block. This is still the case.

A more important issue is that the original version also allowed for a radio-level data structure that all of the blocks could access. The idea was that the developer of the radio would define a block structure for that radio and place it were the block functions could see it, with the obvious place being to place it in the source code file for that radio's function blocks. This worked well for the early trial radios, but two significant flaws to the approach became readily apparent as soon as a radio was built that reused blocks from a previous radio and, furthermore, used more than one instance of the same block. The problem with the block reuse was that the previous radio had a particular structure defined for the radio-level data block and the processing blocks were written so as to interact with that data structure. However, the new radio required that an additional parameter be stored in the radio-level data block which, of course, meant changing the block definition. This, in turn, meant that the functions from the other radio could no longer reliably access information in the radio-level block. An obvious way to deal with this problem is to use a pre-defined name for the radio-level data block and to place it's definition in a header file that gets included in all of the source code files for any block functions. Then the programmer simply needs to be sure that any and all elements required by any block functions that are included in the project (even if they aren't being used, since they must still compile) are present in the radio-level data block. This is probably a workable solution if there is only a small number of processing blocks that must be supported, but it is not very scalable. Worse, it does not solve the problem of multiple instances of the same processing block trying to access information from the radio-level data block.

Consider the case of a radio that has two instances of a block called 'delay' that begins processing by outputting N zero samples and, after that, simply buffers the data presented to it. The block needs to be told what N is and you want to have that be a parameter that gets stored, at least initially, in the radio-level data block. During initialization, the 'delay' block retrieves the value of N from the radio and stores it locally in the block's data structure. This works fine if you only have one instance of the 'delay' block in the radio, but it runs into serious problems if there are multiple instances and each instance needs a different value for that parameter. How does each block instance know how to get the parameter value that is meant for it? Remember, each instance is running the exact same block function.

The solution to this problem that has been implemented in the second version of the radio is to use a pre-defined radio-level data structure but to make it's contents dynamic. This is done by also defining a data structure for parameters that not only contains the parameter value, but also the name of the parameter (as a text string) and the identifying information for the block that the data is targeted, such as the block name (as a text string). Storing and accessing information this way is very flexible, but also very inefficient; however, the radio-level block structure is not meant to be accessed while the radio is running -- blocks should fetch any information they need during initialization, and perhaps during reset, and copy it into their own block's data structure.

While this provides a means of providing information that is targeted for a particular block, it does not address information that is intended for all of the blocks. Nor does it deal with the case of information that is targeted for some set of blocks, but not all blocks.