Signal processing and real-time detection in C and C++ on Linux
In this article I explain the details behind detecting signal changes and the implementation of these applications in C and C++ on Linux. The DSP articles presented on this website are not intended to be comprehensive in there explanation of DSP. The information found on this site is intended to be a brief introduction to processing a digital signal and things to consider.
Monitoring a real-time signal is a requirement that comes with many Digital Signal Processing applications. There are different definitions for real-time. Here when I say real-time processing I mean "reliable processing of a digital signal within a predictable amount of time". When I say real-time signal, I mean "an unpredictable digital signal that's continually generated by a device" A real-time signal needs reliable processing strategies to be useful. Most of the time when dealing with complex systems you'll need a multi-threaded software design to achieve the level of throughput needed to process the signal.
Before embarking on the development of signal processing application make sure you possess a clear understanding of your architectures data-types and how your development language manipulate them in unusual situations. This will be critical for successful operation of your application. Signal Processing is mathematically intensive. Understanding how the data-types are handled in your development environment is equally as important as understanding the details behind threads, processes and how your operating system handles these elements. Your calculations that detect and recognize signals will not be predictable in design documents, unless you can determine how your data will be handled in every scenario your application will encounter.
In my article on Monitoring a Real-Time Digital Signal on Linux using C and C++ I briefly mentioned the importance of understanding issues involved with data-types used for processing a digital signal. When processing an incoming signal the size of the samples generated by the DSP device must be the size of the data-type used to read the samples. For example, if the sample size is 16-bits you'll need a data-type that is 16-bits wide, not more or less than the size of the types used for reading the data. This is not uncommon, when reading data from any file (device,binary,text) this is always a consideration. But, when your dealing with intensive mathematical calculations you may want to scale your data. This could make it necessary to read the signal using a 8-bit data-type and process it using a 32-bit data-type. Consider how I used floating point data-types in the article on “monitoring a real-time signal”. I used them because the unsigned char data-type was not sufficient to calculate the attributes of the signal (average filter , mean, deviations, histogram) that I was computing in the program thread that runs the statistics on the digital signal. This was my way of avoiding over-flows on the variables while running statistics. A char is 8-bits and float is 32-bits on my architecture so, the conversion is necessary because, the 8-bits of a char cannot hold the values required by the application, but a float can hold these values.
The floating point math used by the processor will not be predictable enough most of the time. You'll notice that if you extend the application in my article on monitoring digital signals to detect and act on a change in the running statistics; the results will be unpredictable and random if you don't use special techniques when evaluating the floating point values. This is not desirable, because you only want to act on predictable signal calculations, not on approximation of the incoming data values. For example, two floating point values, float_a and float_b will not always evaluate as being equal when you would expect them to. So, the code,
if(float_a == float_b) { ... } or if(float_a <= float_b) { ... } or if(float_a >= float_b) { ... }
is not reliable. But, these comparisons are critical when detecting and responding to a signal's activity. In order to handle these problems in C++ you'll need good knowledge of pointer arithmetic and understanding of your data-types. In my application in order to compare the statistics data generated by the thread running statistics I would have to protect the data from race conditions and perform comparisons using the difference between the to values instead of the result of the operation in the if statements above. For example, to get predictable results we use,
long error_tolerance=1000000;
//
Cast the address of the floats into a long integers address
//
and assign the values that result to long integer variables.
long
long_a = *((long*)&float_a);
long long_b =
*((long*)&float_b);
// Handle
negative values so -0 maps to 0 etc.
if (long_a < 0) long_a =
0x80000000 – long_a;
if (long_b < 0) long_b = 0x80000000 –
long_b;
// Do the comparison on the
values
result=(long_a-long_b);
if((labs(result))<=error_tolerance)
result=0;
The above code says that if the distance between float_b and float_b is not greater than error_tolerance, assume that float_a == float_b is true. The size of the long integer data-type on my machine is 32-bits and the size of a float is also 32-bits so, the cast from float to long is perfectly fine. An increment or decrement will take you to the respective float value. This code should not be used in-line, the proper way to use this code is to create a function that return a value indicating result of the operation. Remember to cast the address of the float not the value. I don't cover all the details behind this technique, there are articles that go into detail on the subject of performing calculations with floating point numbers. Using this technique a statistics change could be detected like this,
//
float_compare_values(...) returns zero if the difference
//
between float avg_dev and float stable greater than error_tolerance
if(float_compare_values(avg_dev,stable,error_tolerance)!=0) { ... }
In any case the comparison between integer values will always be faster. Since your DSP device is returning integer data (8-bits or 16-bits in this case) you could use long integer data-types instead of scaling to floating point values. Because a long can hold any value a float can hold (both are 32-bits) we could use long integer and calculate the statistics on them. This is faster and more reliable. When we desire to see statistical data from the signal we could scale the values to make them more understandable at first glance. When using this technique you still must keep in mind the data-types of the integers. For example signed, static, and unsigned integers behave differently in calculations and char, long,short and int data-types have their own sizes. Your calculations will fail at unpredictable times if these are not kept in sync.
The implementation of the run statistics function using 32-bit integers instead of floating point values is below. This implementation is easier to use than the first.
void* Signal::run_statistics(void*arg)
{
Signal*
s=(Signal*)arg;
SIGTYPE std_dev[SIGSZ]; // This array holds the
deviation of of each sample read
sig_atomic_t dex=0; //
Index used to access buffer
unsigned long avg_dev=0; // Used to
hold the average deviation
vector<SIGTYPE> arr; //
Vector of 32-bit integers used to hold the
signal
vector<buff>::iterator it; // Iterator used to access
buffers
while(true)
// Infinite loop
{
it=s->buffer[dex]; // Get the Buffer
arr=it->arr; // Copy the data into local array
SignalStateInfo::pos=dex; // Save current position of dex into
atomic variable
s->buffer.release_buffer(dex); // Release
this buffer's mutex
if(dex>=NUM_BUFFERS-1) // Increment or reset dex
appropriately
dex=0;
else
dex++;
AvgFilter::moving_avg_filter(arr); // Apply Average Mean Filter to digital data
for(unsigned
long i=0;i<(SIGSZ-1);i++) { // This loop creates histogram and
sample summations
SignalStateInfo::sample_sum+=arr[i];
}
SignalStateInfo::mean=(SignalStateInfo::sample_sum/(SIGSZ-1));
// Set Mean value of signal
for(unsigned long i=0;i<(SIGSZ-1);i++)
{
// Get the average deviations of each signal
std_dev[i]=labs(arr[i]-SignalStateInfo::mean); // Absolute value of
the difference
avg_dev+=std_dev[i];
}
avg_dev=(avg_dev/(SIGSZ-1));
// Set the average deviation
SignalStateInfo::avg_dev=avg_dev;
// Set atomic variable to value of the average deviation
avg_dev=0;
// Reset avg_dev to 0 for next
iteration
SignalStateInfo::sample_sum=0; // Reset sample
summation to 0 for next iteration
// Print the statistics of the digital signal
cout
<< " Mean: " << SignalStateInfo::mean <<
'\n';
cout << " Avg Dev: " <<
SignalStateInfo::avg_dev << '\n';
}
}
This implementation is almost the same as the previous. Some of the statistics variables are referenced from a structure called SignalStateInfo.
struct
SignalStateInfo {
private:
friend void
*Signal::run_statistics(void*);
static sig_atomic_t
avg_dev;
static SIGTYPE sample_sum;
static
sig_atomic_t mean;
static sig_atomic_t pos;
};
We call labs() for the absolute value of the difference, instead of fabs() used to get the absolute value of floating point numbers. SIGTYPE is defined as a 32-bit integer. This version of the code allows more understandable code. The if() statements that I said would not work directly with floating point values will work as expected. The code that checks the deviation value between the mean and the average deviation can be coded like this,
if(average_deviation>=error_tolerance) { ... }
The if() statement above checks the absolute value of the average deviation. The sign of the deviation does not matter, we are only concerned about deviation itself. The code that used the floating point technique did the same comparison after performing data conversion from floating to long integer types. You can add the SignalStateInfo structure to the source code and compare this function to the version that uses floating point values.