How to Monitor a Real-Time Digital Signal in C and C++ on Linux

By Jim Smith

Rate: (0 Ratings)

Monitoring a real-time signal is a requirement that comes with many Digital Signal Processing applicaitons. There are different definitions for real-time. Here when I say real-time processing is "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 multithreaded software design to achieve the level of throughput needed the process the signal.

Instructions

Difficulty: Moderately Easy

Things You’ll Need:

  • A digital signal source (soundcard,TV card,software radio device)
  • Computer running Linux with software engineering tools installed

Step1
In order to do things right you'll have to create some abstractions in C++. Object-oriented programming provides easy code organization features. Our first abstraction we'll create is the Device abstraction. This abstraction will create an interface for the digital signal device. If this were a production system we would use exception handling and more error checking. The C++ source code is shown next and resembles the interface for Linux device drivers the only difference is that Linux drivers are written in C not C++, the source code form this article will be available on my site at http://www.engineerjames.com.

class Device {
public:
Device(char* device = "");
virtual ~Device();
protected:
virtual int open()=0;
virtual int close()=0;
virtual int read()=0;
virtual int write()=0;
virtual int init()=0;
int fd;
const string device_path;
};

This interface is a pure virtual base class. It allows us to keep a common interface to all devices used by our application.
Step2
Now, we can use the base class created above to build our specific class that will be used to manipulate our signal device.

class SignalDevice : public Device {
public:
SignalDevice(char* device);
virtual ~SignalDevice();
virtual int open();
virtual int close();
virtual int read();
virtual int write();
virtual int init();
protected:
void get_signal();
static void *run_statistics(void*);
static void *acquire(void*);
private:
Buffers& buffer;

};

Other types of signal device C++ classes can be derived from SignalDevice, but we'll implement of SignalDevice for our application. This will not be a complex signal device since we'll use the audio device mapped to /dev/dsp on Linux. The functions will be implemented inline to keep the amount of code to a minimum. The C++ source code is shown below.


class SignalDevice : public Device {
public:
SignalDevice(char* device) : Device(device) { }
~SignalDevice(){ }
protected:
int open()
{
unsigned int arg;
int status,res;

fd = ::open("/dev/dsp", O_RDWR);
if (fd lessthan 0)
{
perror("open of /dev/dsp failed");
return 1;
}
arg = SIZE;
status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);
if (status == -1)
perror("SOUND_PCM_WRITE_BITS ioctl failed");
if (arg != SIZE)
perror("unable to set sample size");
arg = CHANNELS;
status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &arg);
if (status == -1)
perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
if (arg != CHANNELS)
perror("unable to set number of channels");
arg = RATE;
status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
if (status == -1)
perror("SOUND_PCM_WRITE_WRITE ioctl failed");
return res;
}

int close() { return ::close(fd); }

int read(vector float ::iterator &it, long c)
{
int res=0;
unsigned char ch;
for(long i=0;i lessthan c;i++)
{
res+=::read(fd,&ch,1);
*it=(ch*0.01);
it++;
}
return res;
}
Step3
Notice, the 'lessthan' used in the expressions. This is because I could not use the actual signs because, the article would not save.

I initialized the device at /dev/dsp to use 8 bit sample sizes so, we'll use a unsigned char to read each sample, 1 channel, the quality of the sound is not an issue here, the sample rate is how many samples will be read in a second. I set this to 8000. The constants used for these values are listed below. The number of seconds worth of digital data to read is set to 1. SIGSZ is used to hold the calculation of the buffer length we'll need to use when creating the arrays that will hold the digital data.

A DSP application that's monitoring a real-time signal will have to be fast enough to handle the signal frequency of that's the result of the environments activity. For instance, tracking signals on a fast moving object for some abnormal behavior requires more processing speed than processing an audio signal from a PC. The thing to remember is that we are not requiring anything to be really fast, just fast enough to process the signal and give useful results. So, in our case the setting below are fast enough for the real-time signal.

const unsigned long LENGTH = 1; /* how many seconds of speech to store */
const unsigned long RATE = 8000; /* the sampling rate */
const unsigned long SIZE = 8; /* sample size: 8 or 16 bits */
const unsigned long CHANNELS = 1;
const unsigned long SIGSZ = ((LENGTH*RATE*SIZE*CHANNELS/8));

const unsigned long NUM_BUFFERS=64;
Step4
NUM_BUFFERS is the number of buffers used to in the list. The read() function will iterate through the list of buffers using each one and moving to the next. This allows our statistics function to run statistics on each buffer of data while the read() function continuously reads data from the DSP device. The application will work on a one second timing resolution. If a signal change is detected we could begin to analyze the problem using the buffers that were previously process by our statistics function. The statistics function could store the value of the average deviation of the sample from the Mean value of the signal in a variable associated with the buffer. This way we'll know how many seconds to go back to catch the complete signal change. Each buffer holds one second of data. While scanning the buffer list if we recognize an abnormal change in the standard deviation of a buffer we know that is were the signal change began. The structures we'll use to hold the signal data is shown next. Multithreaded programming techniques are use to synchronize the access to the structures used to hold the digital data. The mutex used to protect the critical sections of code is used in the Buffers::operator[]() member function of the Buffers C++ class. When a thread attempts to access this container of vectors it must lock the mutex first or sleep until it is released.

struct buff {
buff() : arr(SIGSZ) { pthread_mutex_init(&mutex,NULL); }
pthread_mutex_t mutex;
vector float arr;
};


class Buffers {
public:
Buffers() : sig_buff(NUM_BUFFERS) { }
~Buffers(){ }
vector buff ::iterator operator[](int dex)
{
/*
Lock the mutex and return the buffer at dex position. If the mutex is locked wait for it.
*/
vector buf ::iterator it = (pthread_mutex_lock(&sig_buff[dex].mutex)?it:sig_buff.begin()+dex);
return it;
}
void release_buffer(int dex) /* Release the mutex */
{
pthread_mutex_unlock(&sig_buff[dex].mutex);
}
private:
vector buff sig_buff;
};
Step5
The buffers themselves hold float values, but the data coming from the DSP device is 8 bits. The reason for this is that DSP processing is intensive and the unsigned chars are sufficient to read the samples but not for computing the attributes of the signal. We handle this problem by scaling the values returned from the device. By multiplying each sample by 0.01 before assigning putting it into the buffer of floats we create a manageable set of data for our signal processing routines. The read function is shown below.

int read(vector float ::iterator &it, long c)
{
int res=0;
unsigned char ch;
for(long i=0;i lessthan c;i++)
{
res+=::read(fd,&ch,1);
*it=(ch*0.01); //scale the value before assigning it to the vector element
it++;
}
return res;
}
Step6
The read function should be started first because we want the statistics function to begin processing the digital data immediately after its thread starts. The function run_statistics() does the following calculations:

1. Calculate the Mean value of the signal
2. Get each samples deviation from the mean
3. Calculate the average deviation

The functions implementation is shown below.


void* SignalDevice::run_statistics(void*arg)
{
float avg_dev=0, sample_sum=0,mean=0;
float std_dev[SIGSZ],upper_bound=0,lower_bound=0;
int dex=0;
Signal* s=(SignalDevice*)arg;
vector float arr;
extra_task();
while(true)
{
vector buff ::iterator it=s->buffer[dex];
arr=it->arr;
s->buffer.release_buffer(dex);
cout << "Run_statistics() dex: " << dex << '\n';
if(dex greater-than-or-equal NUM_BUFFERS-1)
dex=0;
else
dex++;
for(unsigned long i=0;i less-than (SIGSZ-1);i++)
sample_sum+=arr[i];
mean=(sample_sum/(SIGSZ-1));
for(unsigned long i=0;i less-than (SIGSZ-1);i++)
{
std_dev[i]=(arr[i]-mean);
avg_dev+=std_dev[i];
}
avg_dev/=(SIGSZ-1);
upper_bound=mean+(avg_dev+3);
lower_bound=mean-(avg_dev-3);
cout << " Mean: " << mean << '\n';
cout << "Avg Dev: " << avg_dev << '\n';
cout << "Upper Bound " << upper_bound << '\n';
cout << "Lower Bound " << -lower_bound << '\n';
cout << "Sample Sum " << sample_sum << '\n';
sample_sum=0;
avg_dev=0;
}
}
Step7
Now we need the functions that will start the programs thread functions and call our read() member function. The functions are shown below. Notice the static members functions used to interface with the POSIX API pthread_create() function. static member functions don't have a pointer to their object passed to them so, they are enough like C style functions for the POSIX APIs to use them as function pointers. The C++ source code for these functions is shown below.

void SignalDevice::get_signal()
{
open();
pthread_t tid;
pthread_create(&tid,NULL,SignalDevice::acquire,this);
pthread_create(&tid,NULL,Signal::run_statistics,this);
pthread_join(tid,NULL);
close();
}


void *SignalDevice::acquire(void*arg)
{
int dex=0;
Signal* s=(SignalDevice*)arg;
vector buff ::iterator it;
vector float ::iterator float_it;
while(true)
{
it=s->buffer[dex];
float_it=it->arr.begin();
cout << "Acquire(): dex is at " << dex << '\n';
s->read(float_it,SIGSZ);
s->buffer.release_buffer(dex);
if(dex greater-than-or-equal NUM_BUFFERS-1)
dex=0;
else
dex++;
}
}

The functions used to analyze the data buffers when a signal change occurs are not implemented here.

Tips & Warnings

  • This application was moderately easy because, the Standard Template Library ( STL ) was used to implement the data structures. The original application did not use the STL and was a much more complex program.
  • You can add the other DSP processing for the stored signal or use filters in addition to what is done here.
  • The scaling is not what I desired to do because, of the data type size difference between the unsigned char and float.

Resources

Post a Comment

POST A COMMENT

Request a New How-To Article

Looking for more How To information? Chances are there’s an eHow member who knows how to do what you’re looking to do. Submit an article request now!

eHow Article:  How to Monitor a Real-Time Digital Signal in C and C++ on Linux

eHow Member: Jim Smith

Jim Smith

Enthusiast Enthusiast | 400 Points

Category: Computers

Articles: See my other articles

Related Ads