Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

Real-time Signal Analysis & Real-Time Linux: Part 1


Jul03: Real-Time Signal Analysis & Real-Time Linux: Part I

Matt is a programmer at FSMLabs in New Mexico. He can be contacted at [email protected].


In many cases, real-time control systems need to acquire, process, or transform signals, then either act on the results or push back through some analog/digital (A/D) device. Hard real-time systems must do this every time—without fail, and under the worst possible conditions.

Real-time applications that contend with management and nonreal-time applications for resources are complex systems with conflicting demands. RTLinux from FSMLabs (the company I work for) addresses these conflicts by enforcing a strict boundary between real-time components and those with no timing demands. This is done by using a small, verifiable, and deterministic hard real-time operating system running a nonreal-time system as an application server. Consequently, real-time code runs in an RTOS, and nonreal-time code runs in a general-purpose operating system (such as Linux or NetBSD).

In the first installment of this two-part article, I show how you do hard real-time signal acquisition and control it from a normal Java GUI application. To do this, I simulate an incoming analog signal, then sample it with a digital component. In next month's installment, I'll simplify both the real-time and Java code, and add a digital filter. And finally, I'll show how you can use MATLAB to directly interoperate with the real-time system.

Digital Acquisition

Many systems include a piece of hardware that provides an A/D bridge to make a continuous signal discrete. Depending on its capabilities, this device can safely oversample a signal and keep a predetermined amount of backlogged data in storage until the operating system has time to log the samples.

This may be sufficient for a general-purpose OS that's only interested in logging data, as long as the buffer is sufficient to handle the worst-case delays as determined by the operating system (which may be unbounded). However, aberrations in power signals might mean that recovery and other operations must occur immediately, rather than sometime after the logging process catches up on the backlog. If analysis and reaction must happen deterministically and provably without fail, you need a hard real-time system.

Various patches have improved aspects of Linux performance (such as improving interrupt-response times), but offer no further RTOS facilities. Users must either pass the data down to Linux for later processing (and go back to the broken logging example), or do all of the real-time processing in the context of the interrupt handler. This is out of the question for all but the simplest of applications.

The example I present here creates the signal in a high-frequency thread with sufficient granularity to generate a precise waveform made up of several frequency components. This thread is sampled by a lower frequency thread, acting as an A/D acquisition mechanism. By varying the parameters via the control system, you can change the incoming wave characteristics and the sampling parameters.

Data Processing

To process the data, the system needs four communication channels—one for userspace to see the raw signal, one for the sampled version, and two so that you can shuttle commands between RTLinux and Java. The names are user-defined (instead of having to use /dev/rtf* for all devices). Rather than having to remember that /dev/rtf16 is sampled data and /dev/rtf23 is the pure signal, you can just call mkfifo() with /sampled_signal and /pure_signal, respectively. RTLinux/Pro (http://www.fsmlabs.com/products/) makes sure that the nodes are created appropriately in the Linux filesystem based on the permission bits.

In Listing One, the initialization code for the system sigaction() is used for the two command FIFOs. For the signal FIFOs, you can dump the data and continue on in real time. I recommend sigaction() to receive commands effectively. The alternative is to constantly poll the device for possible input, which is inefficient. With sigaction(), you tell RTLinux what events you're interested in, and the handlers are only executed when those events happen.

In addition to mkfifo(), sigaction(), and other POSIX calls (open() and read(), for instance), RTLinux/Pro uses ftruncate() to size the device (instead of the custom ioctl() calls used in the past). In this example, I create the signal FIFOs with room for a 10-sample backlog, and the command FIFOs with a structure size that I haven't looked at yet. This pushes commands between the real-time code and Java programs.

There are four FIFOs at the end of this initialization, two of which are hooked to event handlers, and two that are spawned threads—one for the pure signal, and one that samples it. I let the threads use the FPU with the pthread_attr_setfp_np() call. Except for this, everything is POSIX based.

Listing Two is the signal-generating thread that's been spawned. As with any normal thread, this function starts, enters a loop, and generates a core signal with a noise component. The loop executes and sleeps on a 5-ms (200 Hz) interval, stepping the wave components at each point. After each loop iteration, it writes the output to the FIFO previously set up. During initialization, I allocated space for 10 samples, but it could easily be 100 if the userspace application can't keep up with the output. For low throughput, 10 samples are enough.

Why cast the sample to an int, multiplying by 10,000, and call htonl() before writing to the FIFO? The answer is straightforward: This example assumes it is being run on an x86 machine, which runs Little-endian. Java has always taken the network-centric approach and run Big-endian. For simplicity, I'm reversing the byteorder so it doesn't have to be done in the Java code. The data type used is a float, so you scale it up to be useful as an integer approximation. A more exact method could be used, but this keeps the example simple.

As with the initializing code, this code is POSIX based. clock_gettime() is important, although commonly misused. It is important to sample the clock outside of the execution loop, and only add time delays and sleep within the loop. Calling clock_gettime() as part of the loop occurs in the same context as the rest of the code, and the sampling overhead may induce low-order jitter over long periods. Making the call once outside of the main loop and always adding to that value ensures you avoid drift.

You can now sample the signal in real time. A looping thread starts that operates at a given hertz rate. At each wakeup, you get the latest value that the signal-generating function has left. If this were a real A/D situation, you wouldn't have the other thread, but would get the sample value directly from the hardware device.

The only remaining real-time code is that responsible for sending/receiving control commands to/from Java. Values you might send/receive include the hertz rate of the core signal, hertz rate of the noise component, and sampling rate of the slower sampling thread. With these calls, the Java control program can direct the activities of the real-time code. Since the devices and events were set up during initialization, you only need to deal with the handlers. Listing Three presents two functions: control_from_rtl_handler() and control_to_rtl_handler(). As their names imply, the first writes information to a FIFO, and the second reads in commands. If you refer back to the initialization, sigaction() hooks these calls to their inverse activities; this can be confusing if you're new to sigaction().

In the case of control_from_rtl_handler(), it responds to read actions on the other end of the FIFO, so it performs a write() to service that request. Likewise, control_to_rtl_handler() is connected to write() events from the userspace, which means that someone has submitted data to be read.

There's little work involved in each of these calls. If you need to do a write, set up a structure that defines the data to be sent. Fill it out, then send it down the FIFO via write() as you would with any other file. Again, I do byte reordering to simplify the Java example. For read() activities, you handle the case where not all of the data is written before the userspace is preempted, so you fill the read data into a structure, then pull the command data out and apply it.

The real-time code runs infinitely unless you shut it down cleanly. Listing Four has two threads that must be safely stopped; this is done by canceling the thread and joining its resources. For the FIFO devices created here, RTLinux/Pro supports the unlink() call, which also takes care of removing the node from RTLinux and the host filesystem. So, for each FIFO, you close() it normally, unlink() it, and you're done. unlink() takes care of removing the Linux filesystem entry if needed.

Java Control Software

As with most GUI programs, much of the code manages the visual components. In the interest of brevity, I only examine the Java code that directly interacts with the RTLinux FIFOs. Listing Five is the object code that sends/receives commands. The class contains all of the local variables needed to fill out the command structure, and the constructor opens up the FIFO as directed by the caller. It opens the devices as data streams so that writing primitives such as ints is simple.

The calls receiveCommand() and sendCommand() write integers out or read them in from the correct device to exchange data with the real-time system. Since endian conversion is handled on the other end of the FIFO, no other work needs to be done. This object plugs into the graphical application; see Figure 1. The GUI code (available electronically; see "Resource Center," page 5) is no different than any other Java GUI. For instance, in the main code, you instantiate this object with:

SignalCommand sc = new SignalCom-

mand("/control_to_rtl","/control_from_rtl");

From there, you operate on the object just like any other. As you can see in Figure 1, this is a straightforward GUI—you can get information from the real-time side, or set/send values for the core signal, noise, and sampling rate.

Still, this doesn't do much good unless you can see what the real-time side is doing with its data. Listing Six is the class that reads the signal data from the real-time FIFO. You instantiate this code twice—once on the device that provides the pure signal, and once on the sampled output. Again, byte ordering is done on the other end of the FIFO, so you don't have to worry about that here—you just read the sample points and plot them into an XYSeries object, which is a component in the visualization tool. Visualization is done with JFreeChart (http://www.object-refinery.com/jfreechart/), which provides many means of visualizing data. The XYSeries object places the points in a collection such that they can be viewed in a simple chart.

For the default values in the system, the output is in Figure 2, with the pure signal on the top and the sampled output on the bottom. As you can see, the system is short of the Nyquist rate of the highest frequency component and there is signal aliasing in the sampled data. The pure signal output is "stretched"—this signal is being incremented at 200 Hz, so it fills the chart every second, having the effect of zooming in on the signal. The sampled graph is only 10 samples per second, so it doesn't fill the graph quickly and offers a zoomed out perspective. In Figure 2, the data points are filled at 10 per second, and you can see that most of the signal energy is the 1-Hz wave, advancing slowly across the 200-point graph.

In Figure 3, the sampling rate is higher, so you capture all of the signal information in the sampled output; again, the pure signal is on the top and the sampled output on the bottom. You can see that, because sampling is at a higher rate, the view zooms in and you capture all of the information in the signal. The result is a 1-Hz wave with a 14-Hz component captured cleanly in the sampled output. These visualizations are arbitrary and are only shown here as an example—you can easily plug this data into any visualization tool.

Next Month

In the next installment of this article, I will simplify both the real-time and Java code, add a digital filter, and show how to use MATLAB to directly interoperate with the real-time system.

DDJ

Listing One

struct command { 
   int signal_hz;
   int noise_hz;
   int sample_hz;
   } ;
static pthread_t signal_gen_t;
static pthread_t sampler_t;
static int sampled_fd; 
static int signal_fd;
static int control_to_rtl_fd;
static int control_from_rtl_fd;

int init_module(void) {
   pthread_attr_t attr;
   struct rtl_sigaction sigact_r;
   struct rtl_sigaction sigact_w;

   mkfifo("/pure_signal",0777);
   signal_fd = open("/pure_signal", O_RDWR|O_NONBLOCK);
   ftruncate(signal_fd, sizeof(int)*10);

   mkfifo("/sampled_signal",0777);
   sampled_fd = open("/sampled_signal", O_RDWR|O_NONBLOCK);
   ftruncate(sampled_fd, sizeof(int)*10);

   mkfifo("/control_to_rtl", 0777);
   control_to_rtl_fd = open("/control_to_rtl", O_RDWR|O_NONBLOCK); 
   ftruncate(control_to_rtl_fd, sizeof(struct command));
   sigact_r.sa_sigaction = control_to_rtl_handler;
   sigact_r.sa_fd = control_to_rtl_fd;
   sigact_r.sa_flags = RTL_SA_WRONLY|RTL_SA_SIGINFO;
   rtl_sigaction(RTL_SIGPOLL, &sigact_r, NULL);

   mkfifo("/control_from_rtl", 0777);
   control_from_rtl_fd = open("/control_from_rtl", O_RDWR|O_NONBLOCK);
   ftruncate(control_from_rtl_fd, sizeof(struct command));
   sigact_w.sa_sigaction = control_from_rtl_handler; 
   sigact_w.sa_fd = control_from_rtl_fd;
   sigact_w.sa_flags = RTL_SA_RDONLY|RTL_SA_SIGINFO;
   rtl_sigaction(RTL_SIGPOLL, &sigact_w, NULL);

   pthread_attr_init(&attr);
   pthread_attr_setfp_np(&attr,1);
   pthread_create(&signal gen_t, &attr, signal_gen, 0);
   pthread_create(&sampler_t, &attr, sampler, 0);
   return 0;

Back to Article

Listing Two

void *signal_gen(void *arg) { 
#define THREAD_PERIOD 5000000
#define SAMPLE_FRACTION 1000000000/THREAD_PERIOD
struct timespec next;
   float x1 = 0;
   float x2 = 1.5;
   float incr1;
   float incr2;
   float cur1;
   float cur2; 
   int icur;
   clock_gettime(CLOCK_REALTIME, &next);
      while (1) { 
         timespec_add_ns(&next, THREAD_PERIOD);
         clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &next, NULL);
         incr1 = 2*PI / (SAMPLE_FRACTION / signal_hz);
         x1 += incr1; 
         cur1 = (float)sin(x1);
         incr2 = 2*PI / (SAMPLE_FRACTION / noise_hz);
         x2 += incr2;
         cur2 = (float)sin(x2);
         cur signal = cur1 + (0.5*cur2);
         icur = (int)(cur_signal * 10000);
         icur = htonl(icur);
         write(signal_fd,&icur,sizeof(int)); 
      } 
   return 0;
}

Back to Article

Listing Three

void control_from_rtl_handler(int_sig, rtl_siginfo_t *siginfo, void *v) {
      int_ret;
      struct command com;
      com.signal_hz = htonl(signal_hz);
      com.noise_hz = htonl(noise_hz);
      com.sample_hz = htonl(sample_hz);
      ret = write(siginfo ->rtl_si_fd, &com, sizeof(struct command));
}
static int count = 0;
static unsigned char readdata[sizeof(struct command)];
static char *ptr = readdata;
void control_to_rtl_handler(int_sig, rtl_siginfo_t *siginfo, void *v) {
      int ret;
      struct command com;
      ret = read(siginfo ->rtl_si_fd, ptr, sizeof(unsigned char)); 
      count += ret;
      ptr += ret;
         if (count >= sizeof(struct command)) { 
            memcpy(&com, readdata, sizeof(struct command));
               signal_hz = ntohl(com.signal_hz);
               noise_hz = ntohl(com.noise_hz);
               sample_hz = ntohl(com.sample_hz);
               count = 0; ptr = readdata;
   } 
}

Back to Article

Listing Four

void cleanup_module(void) { 
   pthread_cancel(signal_gen_t);
   pthread_join(signal_gen_t, NULL);
   pthread_cancel(sampler_t);
   pthread_join(sampler_t, NULL);
   close(signal_fd);
   unlink("/pure_signal");
   close(sampled_fd);
   unlink("/sampled_signal");
   close(control_to_rtl_fd);
   unlink("/control_to_rtl");
   close(control_from_rtl_fd);
   unlink("/control_from_rtl");
}

Back to Article

Listing Five

class SignalCommand { 
   private int signal_hz;
   private int noise_hz;
   private int sample_hz;
   private DataInputStream dis;
   private DataOutputStream dos;
   private String write_fifo, read_fifo;
   public SignalCommand(String write_fifo, String read_fifo) { 
      this.write fifo = write_fifo;
      this.read fifo = read_fifo;
      try { 
         dis = new DataInputStream(
               new FileInputStream(read_fifo));
        dos = new DataOutputStream(
              new FileOutputStream(write_fifo));
      } catch (Exception e) { 
               System.out.println("Could not open FIFO: " + e);
      } 
   }
   public void setSignalHz(int signal_hz) { 
      this.signal_hz = signal_hz;
   } 
   public void setNoiseHz(int noise_hz) { 
      this.noise_hz = noise_hz;
   } 
   public void setSampleHz(int sample_hz) { 
      this.sample_hz = sample_hz;
   }
   public int getSignalHz() { return this.signal_hz; } 
   public int getNoiseHz() { return this.noise_hz; } 
   public int getSampleHz() { return this.sample_hz; }
   public void receiveCommand() {
      try { 
        signal_hz = dis.readInt();
        noise_hz = dis.readInt();
        sample_hz = dis.readInt(); 
      } catch (Exception e) { 
         System.out.println("Error reading commands: " + e);
      } 
   }
   public void sendCommand() { 
      Process proc;
      try { 
         dos.writeInt(signal_hz);
         dos.writeInt(noise_hz); 50
         dos.writeInt(sample_hz);
      } catch (Exception e) { 
         System.out.println("Error writing commands: " + e);
      } 
   }
}

Back to Article

Listing Six

class Reader extends Thread
{ 
   DataInputStream dis;
   XYSeries series;
   JLabel lab;
   public Reader(String device, XYSeries series, JLabel lab) { 
      this.series = series;
      this.lab = lab;
      try {
         dis = new DataInputStream(
               new FileInputStream(device));
      } catch (Exception e) { 
         System.out.println("Failed open: " + e);
      } 
   }
final static int area_len = 200;
public void run() { int i = 0; 
   int count = 0;
   int newval = 0;
   int zerval = 0;
   for (i = 0; i < area_len; i++) { 
      series.add(i,0);
   } 
   while (true) { 
      try { 
         i = dis.readInt()/100; 
      } catch (Exception e) { 
         System.out.println("Failed: " + e);
      } 
      try { 
         count++;
         newval = count % area_len;
         zerval = newval - (int)(area len * .95);
         if (zerval < 0)
            zerval = area len + zerval;
         series.update(newval, new Integer(i)); 
         series.update(zerval, new Integer(0));
         lab.setText((new Date()).toString());
     } catch (Exception ee) { 
     } 
   } 
 } 
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.