Scientific/Numerical


A Class Hierarchy for Data Acquisition

Gualtiero Chiaia and Marco Marcon

Acquiring data and controlling devices has never been simpler, thanks to a wide range of off-the-shelf PC control cards. But it can be made more uniform and device independent.


Introduction

PCs now control practically all the steps in a scientific experiment, not only in the final analysis of the results but also during data acquisition. To acquire data, the PC needs a communication link between the hardware and the outside world. This is commonly achieved by data acquisition boards, which perform actions such as:

and so on.

More generally, data acquisition programs need a wide pool of devices at the same time. A typical configuration might use, for example, ten analog outputs (AO), four analog inputs (AI), six counters (CNT), one clock (CLK), and two oscillators (OSC).

From the programming point of view, this traditionally implies the inclusion of card-control functions from commercial libraries, which are delivered together with the PC cards. The large availability of I/O cards on the market, and the lack of commonly adopted standard for their instruction sets, results in a strong coupling between the hardware and the implementation for a given experiment. In other words, the software application (client) and the commercial libraries are so strongly interlinked that code maintenance becomes very demanding. The software usually outlasts the hardware, even though the change of an I/O card requires deep modifications to the original code.

Given these considerations, our effort has gone into decoupling the interface between the application and the hardware, and making it more portable and user-friendly, in an OOP fashion.

Wrapper Classes

Since the very beginning we had clear in mind that an OO approach had all the answers that we were looking for:

polymorphism + inheritance + information hiding = portability + de-coupling + user-friendliness

Polymorphism allows us to talk to an I/O device in a language that is not hardware dependent. For example, you can tell a certain AO to produce a voltage with no regard for its low level instructions, thus increasing the software portability. Inheritance permits a natural abstraction of the interface between the application code and the real world, enhancing their de-coupling. User-friendliness is further boosted by hiding all the low-level information.

To achieve these goals, we have designed a hierarchy of wrapper classes, structured in three levels of decreasing abstraction. The lowest level wraps the instruction set of the specific I/O cards, providing the hardware interface, while the uppermost level forms the software interface to the client application. We believe that this hierarchy constitutes a useful framework for programming portable data-acquisition codes, working on platforms with different acquisition hardware. Our specific hierarchy is based on Burr-Brown data acquisition cards, and on the device drivers purchased from Intelligent-Instrumentation, which is a subsidiary of Burr-Brown Corporation.

To build this hierarchy, we have tried to outline the differences and similarities of the I/O devices, as shown in Figure 1. At the base of the class tree (topmost level) lies class Block, which encapsulates the information used to identify and address a single device out of a pool of several. Block acts as the base class for six derived abstract classes, which represent the different types of devices: VAO (Virtual AO), VAI (Virtual AI), VCNT (Virtual CNT), VMCNT (Virtual Multi-CNT), VCLK (Virtual CLK), and VOSC (Virtual OSC). These abstract classes supply the interface to the lowest-level derived classes, which correspond to the real devices. For example, class AO_PCI200006M encapsulates the functionality of a single AO port on a PCI200006M Burr-Brown commercial module.

From VAO it is furthermore possible to derive other AO classes for other boards on the market. Each pool of devices, which represents all the I/O ports provided by a commercial board, is handled by a Device Container Class (DCC), derived from the abstract class VCard (Listing 1). For example BB_PCI20000 is the DCC for the Burr-Brown board. VCard hosts all the common instructions for the DCCs, implementing them as pure virtual functions. In this way it contains all the behaviours that characterise the abstraction of an acquisition card, in an OOP style. For example:

virtual void
CreateConfig() = 0; //file vcard.h

virtual long
ReadCounter(int i) = 0;

virtual void
WriteAO(int i, float f) = 0;

These functions are then overridden in each DCC, where they are differently implemented according to the specific actions of a given card. Polymorphic behaviour is achieved by dynamically allocating a VCard object with a DCC type:

VCard *board1,
      *board2; //file demo.cpp
board1 = new BB_PCI20000;
board2 = new Wave_device;

and accessing the functions dynamically:

board1->CreateConfig();//file demo.cpp
board2->CreateConfig();

The first instruction calls the version of CreateConfig that is specific for the Burr-Brown DCC, while the second one calls the Wave_device implementation. An important aspect of polymorphism is the need for a virtual base class destructor. For example, when deleting the instance board1 of the base class VCard, the destructor of BB_PCI20000 is called first.

All DCCs host six STL vectors: one of pointers to VAO devices, one of pointers to VAI devices, and so on. Each pointer to a virtual device is allocated with a hardware-specific derived class:

vector<VAO*>
    AnalogOutput; //file vcard.h
.....
//file burrb.cpp
VAO *aop = new AO_PCI20006M(i,j,c);

AnalogOutput.push_back(aop);

where i, j, and c hold the address of this device.

The class hierarchy supports C++ exception handling, in such a way that every low-level class (e.g., AO_PCI20006M) can throw an exception of type CardError:

class CardError //file vmod.h
{
public:
  CardError(string message);
  string RetMsg();
  private:
  string msg;
};

It is then responsibility of the client software to catch the exception in the correct way. For example:

//file AO_BB.cpp:
void AO_PCI20006M::Write(float val)
{.....
  if ((val>max)||(val<min))
    throw(
      CardError(
        "this value is out of range"));
.....}
.....
try   //file demo.cpp
{
  board1->WriteAO(AODev,volt);
//AODev = index of the device
}
catch(CardError Error)
{
  ....
}

In this situation, an error of type "out of range" is thrown from the Write function of a Burr-Brown AO device when val goes out of range. This error may occur in the try section of the application demo.cpp, when writing a voltage on the device AODev resident on board1. In that case the client software should specify, in the catch section, the actions to be performed.

To better understand the use of this class hierarchy, it is interesting to analyse schematically the steps needed to modify the client software in order to control a new I/O card. Let us suppose that this card is produced by company XX (see Figure 1, green tags), and that it provides four AO, two AI, and one OSC:

And so on.

To give an example, we implemented two oscillators using the stereo output (Left/Right) of a common Sound Card. An oscillator produces a periodic analog signal (e.g., a sinusoidal wave) with a predefined amplitude, frequency, and phase. Besides the most typical applications, like time synchronisation, signal modulation, and calibration of audio instrumentation, these devices offer an easy and low-budget desktop wave generator. Furthermore, with simple AC-DC rectification, they can be employed as two extra AO, available to a large community of PC owners.

To use our class hierarchy to implement two oscillators, we have created the DCC class Wave_device, derived from VCard, and the oscillator class OSC_WV derived from the virtual oscillator VOSC (see red tags on Figure 1). These classes are declared and defined in the files wave.h, wave.cpp, OSC_WV.h and OSC_WV.cpp, available on the CUJ web site (see page 3). You also need to download the file silence.wav containing the information for producing a sine wave, as described in the following section. The client software can now adopt a rather simple syntax to control the Left and Right channels:

//in file demo.cpp
Vcard *board2;            
//allocate it with a Sound Card
board2 = new Wave_device;
//automatically configure it
board2->CreatConfig();    
board2->StartOsc(index, type,
          frequency,phase,amplitude);

where type (set to zero in demo.cpp for a sine wave) could be used to produce non-sinusoidal waves (e.g., square waves), and index is 0 or 1 for the Left and Right channels, respectively.

Implementation Details

We don't provide the MasterLink library from Intelligent Instrumentationc, which is the driver for the Burr-Brown cards, so the demo file (demo.cpp) cannot be compiled. However, looking at the full source code can help you understand the class hierarchy. In particular, the file demo.cpp is a useful example for showing the syntax used in the client software. This hierarchy (Figure 1) is implemented in different files, which are organized according to the following table:

Class Name              Header File

VCard                   vcard.h (Listing 1)
Block, VAO, VAI, VCNT,
VMCNT, VCLK, VOSC       vmod.h  (Listing 2)
BB_PCI20000             burrb.h
Wave_device             wave.h
AO_PCI20006M            AO_BB.h
AI_PCI20002M            AI_BB.h
CNT_PCI20007M,
MCNT_PCI20007M          CNT_BB.h
CLK_PCI20007M           CLK_BB.h
OSC_WV                  OSC_WV.h

We lack the space to show a complete implementation of the hierarchy. Therefore we have chosen two examples. The first implements a wait(time) function that uses the Burr-Brown clocks and counters. The second uses the sound card to implement two oscillators.

For example 1, member function BB_PCI20000::Wait(int clock, int counter, long timems) is shown in Figure 2. It overrides the pure virtual function Wait of the VCard base class. As we mentioned earlier, a BB_PCI20000 class, deriving from VCard, hosts six vectors, filled with I/O devices (AO, AI, CLK, CNT, MCNT, OSC). To produce a delay, you must combine a CNT from a Counter vector with a CLK from a Clock vector. The counter counts the ticks from the clock. The CLK and the CNT are allocated with Burr-Brown specific classes (CNT_PCI20007M and CLK_PCI20007M), deriving from abstract classes (VCNT and VCLK), deriving from Block (Listing 2).

The code checks for the existence of the given clock and counter: on error, it throws an exception. Then, it starts a clock with 1 kHz frequency, by calling the Set and Start functions of a CLK. The waiting time in milliseconds (timems) is divided into an integer number of seconds (seconds), plus a remaining number of milliseconds (remainder). This is necessary to avoid counter overflow (65535 = FFFFh). The for loop iterates seconds+1 times, spending one second for each step. The final step lasts less than one second (remainder). A CNT element is then set and started. Note that the counter has a dead time before it can be used. The first while loop waits until it is correctly set. The second while loop waits until the counter reading is equal to ms (one second or remainder).

Sound cards are built to play music. To use them like wave generators, you have to cheat. Make them play a smart .wav file and you will end up with two oscillators, the Left and the Right channels. In example 2, the .wav file (silence.wav) is a stereo file with a sample frequency of 44.1 kHz, containing data for one second of music. This file is played in an infinite loop mode, reproducing a clean sine wave. The amplitude can be chosen in the range of 0-0.75 Volt. The frequency is lower-limited by the high-pass filter of the card output (typically 20 Hz). It is upper-limited by the Nyqvist frequency (22,050 Hz, which is one half of the sample frequency). It is further possible to control the phases of the Left and Right channels to achieve fixed phase shifts between them.

Figure 3 describes the code for setting and starting an oscillator. Among the headers to be included is mmsystem.h, which is provided with the Borland C++ environment for multi-media functionality.

The wave array stores the data to be played. Function OSC_WV::Start calls the multi-media library function sndPlaySound, which plays the file (wave) contained in memory in an infinite loop. OSC_WV::Set writes to the wave array, after some parameters are checked. RetChn is a function inherited from Block which returns zero for the Left channel and one for the Right. Only the even words (1 word == 2 bytes) of the array are used for the Left channel, starting from byte 44 (word 22), while odd words are used for the Right, starting from byte 46 (word 23). Therefore a wave file is an alternated sequence of Left and Right information. The first 44 bytes contain information on the sound (sample frequency, stereo or mono mode, and so on) which are required from the multi-media functions.

Gualtiero Chiaia has a degree in Nuclear Engineering and a Ph.D. in Physics from Politecnico di Milano. He has done research on electron spectroscopy, electronic structure of materials, and material and surface science. He has been interested in computer programming since 1980. He may be reached at gualtiero.chiaia@polimi.it.

Marco Marcon is an undergraduate student in Electronic Engineering at Politecnico di Milano. He has been programming in C since he was 18, and published a couple of articles in the Italian magazine MC microcomputer. He may be reached at Marco_Marcon@rcm.inet.it.