Monitoring a Real-Time Digital Signal in C and C++ on Linux
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 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 multithreaded software design to achieve the level of throughput needed to process the signal.
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. 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 less-than
c;i++)
{
res+=::read(fd,&ch,1);
*it=(ch*0.01);
it++;
}
return res;
}
Notice, the
'less-than' 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. For instance, tracking signals for
guiding 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;
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;
};
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;
}
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;
floatstd_dev[SIGSZ],upper_bound=0,lower_bound=0;
int dex=0;
SignalDevice* 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;
}
}
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;
SignalDevice* 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. 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. More information on this topic is explained in my article on Digital Signal Detection on Linux using C and C++ . There are many things to consider when writing software for signal processing. This program demonstrates only a small portion of the task.
This
Web Page Created with PageBreeze Free
Website Builder