Using Linux to Control the Outside World

by David Wagg
Using Linux to Control the Outside World

Developing real-time control applications using Linux.

David Wagg and Eduardo Gómez

One of the key problems faced by control engineers is how to make a computer interact with the system they are trying to control. If, for example, we are trying to spin a motor at a constant speed, we need to retrieve the data from the tachometer (usually a voltage signal), calculate a driving signal in the computer and then send this signal back to the motor (i.e., convert the numbers back into voltage levels). One of the simplest and most cost-effective ways of achieving this is to use a data acquisition card. The card forms a link between the computer hardware and the analogue input/output signals required to control the external system. This system could be anything driven by an electrical motor--your garage door for example.

In DOS times, average programmers could buy any card they wished, develop the code to drive it in the way they wanted, and use this code on the computer. However, the arrival of more sophisticated hardware introduced the concept of device drivers (which had been used previously in UNIX and other large operating systems) to the world of personal computing. The device driver is the piece of code that controls the hardware and contains the instructions specific to the card. When installed on the PC, they become part of the operating system and, as such, can rarely be manipulated or developed by the end user. After finding a suitable card and installing the accompanying software (including the device drivers supplied by the manufacturer), it's not unusual to find that it does not do what you want. Unless you've made a bad choice of board, the apparent limitations of the system are nearly always software-related. Particularly limiting are the device drivers, as the user has no opportunity to alter their behavior.

Some commercial operating systems make it difficult for an experienced programmer to develop a device driver themselves. The development kit for creating device drivers is usually not shipped with the compiler, and the documentation can be obscure. Some card manufacturers add to the difficulty by removing vital register-level information from their user manuals. When questioned about this, they will refer you to their ``already optimized'' device driver, which will normally reduce the performance of your system.

Luckily, Linux has many contributors around the world who have provided very good documentation for device driver development. Linux compilers, of course, let you create anything you want without having to buy a special license. Card manufacturers can (usually) be won over, once you tell them plainly that you will not buy their card unless register information is supplied.

In this article we describe some of our experiences in overcoming these limitations while developing a real-time control application. In this case we were limited by financial constraints and had only a relatively old ISA card and a 486 PC. So, in order to obtain the required performance (sampling rate), we needed to write efficient code, including the device drivers. We had no need for a GUI and used only command line-driven programs.

The drivers for the data acquisition card were combined with the Linux real-time clock, so that user controller programs could be written to achieve a crude form of real-time control. Despite the basic nature of both the hardware and the software, the subsequent experimental results were good. In fact, they were equal to other results taken using commercial card/software packages but had obvious cost savings combined with the convenience and flexibility of source code accessibility.

Character Device Drivers

For control engineering applications, a Data Acquisition (DAQ) card (or board) forms the link between computer software and a physical system. The term DAQ usually includes Digital-to-Analogue Conversion (DAC), which occurs when the control signal is sent, from OS to hardware.

In the Linux OS a device driver appears as a file in the /dev/ directory. We are only concerned with character device drivers, which read a stream of bytes, and are appropriate for devices such as data acquisition cards.

A device driver file can be created with the mknod command, which requires specification of the type, and the MAJOR and MINOR numbers for the device. The MAJOR number enables the kernel to identify the hardware device from all the registered hardware devices, so it's important that there's no conflict with the pre-existing MAJOR allocation (see Linux Allocated Devices, by H. Peter Anvin, http://www.linux.locus.halcyon.com/pub/linux/docs/device-list/devices.txt, last version 3/23/00). For a single-character device we can set the MINOR to 0.

For example, we wanted to create a driver for the Amplicon PC30AT data acquisition card. To create a character device driver called pc30at with MAJOR 125 (unallocated as yet) and MINOR 0 we give the command:

mknod /dev/pc30at c 125 0

The appropriate file permissions, depending on which users should have access to the driver, can be set using the chmod command. This file can now be linked with the driver code via a loadable kernel module.

Using Loadable Kernel Modules

The simplest way to integrate driver code into the kernel is to insert it as a kernel module that can be loaded at runtime. Kernel modules are inserted and removed using the commands insmod and rmmod that reside in the /sbin/ directory. Modules are inserted in the form of object files, and the MAJOR number is registered with the kernel. To insert a module we give the command:

/sbin/insmod pc30at.o

To obtain an object file we need to compile some driver code, and this can be done using a standard Makefile approach. In summary, the driver code is compiled into an object file, usually via a Makefile, then the object file is inserted into the kernel as a module, using the insmod command.

Writing a Device Driver for Control Applications

A device driver is represented by the operating system as a file, although it is not a conventional one. The same operations that one would normally perform on a file can be carried out on a device driver. It is therefore possible to open and close the driver (equivalent to initializing and stopping the card) as well as to write and read to and from the driver (equivalent to transferring data to and from the card). Other operations on the card can be carried out using the input/output control ioctl command. This adds much greater versatility to the driver, but for our case, we did without one.

One of the best ways to start driver programming is to have a look at some driver code. We found that the Linux Kernel Programming Guide by Ori Pomerantz is an excellent source of information and includes driver ``template'' code. Effectively, you can take this template and add in the hardware specific functions you require. See also the book Linux Device Drivers by Alessandro Rubini.

The first step toward designing a device driver is to fill in the structure that will define the driver's operations. This structure is named fops. For our PC30AT driver, the operations structure is shown in Listing 1.

Table 1. Operations Structure

So, everything is set to NULL, except the read, write, open and close functions. These functions correspond to the read, write, open and close functions that can be called from the user program. As far as the user is concerned, the driver can be treated exactly as a file.

Register Operations

In this section we discuss the key part of writing a device driver, i.e., writing the assembly code that interacts with the hardware. For ISA data acquisition cards this interaction will usually be carried out using the instructions ``in'' and ``out'', which read and write to specific ports of the PC. These ports are associated with specific registers on the card. Having a good technical manual for the card helps a lot with this stage--something worth considering when you buy your card.

The first consideration is to establish the base address of the card, which is the lowest I/O port in the PC used by this particular card. Port 0x300 is usually a good choice. The address of all the card registers are denoted by this number plus some offset, usually listed in the card manual, along with a description of the function of each register. If, for instance, the card has 16 registers, there will be 16 ports associated with it (in our example, those ranging from 0x300 to 0x316). To avoid a code that depends on the base address, the registers are usually accessed as 0x300+1, 0x300+2, etc. However, it is typical to create a set of names in order to simplify reading the code; instead of writing to 0x300+2, we can write to Base_address + PORT_C30at. In our case, Listing 2 shows which names correspond to the different PC30AT registers.

Table 2. PC30AT Register Names

This information can be put into a header file including the MAJOR number for the driver and the base address for the card. This file should be included in the driver code along with linux/kernel.h, linux/module.h, linux/fs.h and asm/io.h.

Most general-purpose boards will have registers for analogue input and output, digital input and output and accessing the on-board counters. These counters can be used for a variety of purposes, ranging from timing to counting how many samples have been acquired. In addition, some registers will be dedicated to initializing and selecting options for the conversion process. Registers are usually either 8-, 12-, 16- or 32-bit, and data can be written to and read from 8-bit registers using the outb and in functions. The 12-bit registers are usually formed using two 8-bit registers, or zero-padding a 16-bit register. The 32- and 16-bit registers can be accessed as two or four bytes respectively. The outw function can be used to write a 16-32-bit word to a register.

The first function to consider making hardware specific is the init_module. When the function is called, the driver is loaded into the kernel, so it is a good idea to set the card to its default settings and include a hardware check. For this check we used an analogue-to-digital conversion, which is initialized, and then we poll until the conversion is finished. If there is no end to conversion within a certain number of iterations, an error is returned (see Listing 3).

Table 3. Conversion Error

Reading and Writing Data

The PC30AT is a relatively old card and is not fitted with FIFO memory. Although it supports interruption and DMA transference, neither technique lends itself readily to real-time operations. Interrupts add an expensive overhead for every sample acquired. DMA works well with large data transference but has to be initiated too often in a real-time implementation. Therefore, the code relies on polling with wait loops. With newer cards, the samples can be held in the FIFO on-board memory of the card and then transferred together to the memory of the computer, increasing performance and avoiding wait loops. The polling loop is optimized in order to allow the card's register to settle before starting the conversion. Otherwise, the wrong channel to convert is selected by the card.

For both the reading and writing functions we require temporary buffers to hold data for register operations. These buffers are defined as the union of a character array and unsigned short integer (see Listing 4). These union buffers are used for transferring data between the hardware and the kernel memory. A separate buffer is defined in the user code for transferring data between kernel memory and user memory. Note that the reading buffer array has a size 16 corresponding to the number of input channels for the board. Similarly the output buffer has size 2, corresponding to the number of output channels.

Table 4. Temporary Buffers

Driver Read and Write Functions

The read function is designed to read the number of channels set by num_in_chan on each iteration. The user program has to provide a character buffer array, which must be of size 2*num_in_chan, so that short (2 byte) integers can be put into it. To read the input channel data, we trigger the AD conversion with a downward edge, for the init_module function. After each conversion we put the data into the character buffers (see Listing 5).

Table 5. Main Reading Loop

After the reading loop has finished reading all input channels, the data is transfered into the user buffer:

# Put the values from the current array into the<\n>
# user character buffer
for(i=0;i<num_in_chan;i++){
  put_fs_byte(current_adc[i].byte[0],p++);
  put_fs_byte(current_adc[i].byte[1],p++);
}

The write function transfers data from a user buffer and writes it to the DA registers of the card. There are only two output channels on this card: zero and one. The DA registers should be initialized by writing zero to both output channels. In some ways, the write function can be thought of as the reverse of the reading process. First we get values from the user character buffer into the current array:

for(i=0;i<num_out_chan;i++){<\n>
 current_dac[i].byte[0]=get_fs_byte(buf+(2*i));
 current_dac[i].byte[1]=get_fs_byte(buf+(2*i+1));
 count++;
}
Then we send the values to the DA registers. For this card, the output is in a (slightly obscure) 12-bit format, hence the shifting. For example, to write data to channel 0, we use the code:
outb((unsigned char)(current_dac[0].integer>>4),<\n>
          PC30AT_BASE+DAC1H_30AT);
outb((unsigned char)(current_dac[0].integer<<4
          & 0xF0),
        PC30AT_BASE+DAC1L_30AT);
return count
In summary, any of the driver functions can be customized to suit a wide range of cards and applications. The structure of the functions can be kept and the hardware-specific code replaced, or modified, to suit the new application. The example we have shown here is specific to the PC30AT card but demonstrates a general approach for writing custom drivers.
Timing

Now that we have a device driver for the data acquisition card, the next step is to consider the user program that will implement the driver functions. One of the key features of a user program is timing the calls to driver functions. For control engineering applications, we need to work in real-time or at least have timings that are very close to this. Time-critical applications are usually designed using hard real-time systems, where the maximum delay in the timing is guaranteed. However, this requires specially tailored operating systems such as RTLinux (http://www.rtlinux.org/). We did not require this level of precision. In the next section we consider how the timing can be done using the real-time clock already available in the Linux operating system.

Using the Linux Real-Time Clock

The real-time clock is available as the device driver /dev/rtc. This can be opened, read and closed in a similar way to the acquisition card driver. Note: unlike the DAQ card drivers, this driver is read only. The use of this driver is described in the documentation file /usr/src/linux-2.2.14/Documentation/rtc.txt by Paul Gortmaker. There is an example program that demonstrates how interval timing can be achieved. This code can be used to time when calls are made to the acquisition driver. Note that code using sampling rates of greater than 64Hz have to be executed as root. Even so, this provides an efficient way to control calls to drivers at a required sampling rate.

The User Program

The final stage in creating a real-time control application is to write the control code in the form of a user program. However, before launching into full-blown control code, it is sensible to write a simple test program that only reads and writes a signal to the card. Usually an oscilloscope can be used to check if the correct analogue signal is being output by the card. To open the driver we need to make the call:

fd=open("dev/PC30AT",O_RDWR);

Providing fd is returned as greater than zero, we can then start reading and writing to the card. We first read into a character buffer (buff) of size 2*(ADCHANS):

tmp=read(fd,buff,sizeof(buff));
where tmp is the return value from the read function. Then, using the same approach as in the driver code, the data from the character buffer can be read into a union variable current_adc (this one lives in user space), which, as before, is either two bytes or a short integer. Reading in data from the character buffer can be done like this:
current_adc.byte[0]=buff[2*i];<\n>
current_adc.byte[1]=buff[2*i+1];
Finally, the value can be assigned to a pointer and converted from integer to double:
in_ptr[i]=((double)(current_adc.integer)-2047)*10.0/2047;
The conversion maps the 4096 possible levels of the card, in the +/-10V range 2047 corresponds to zero, hence the shift.

Writing output to the card is the reverse process. Taking a value assigned to an output point, we can convert it to a short integer and assign it to a union variable using:

current_dac.integer=(short int)(CONVERT);

Then transfer the data into a character buffer:

buff[2*i]=current_dac.byte[0];<\n>
buff[2*i+1]=current_dac.byte[1];
Finally, we call the driver write function and note the integer return tmp:
tmp=write(fd,buff,sizeof(buff));
These reading and writing processes can be grouped together in functions for convenience. In our case, Read_30ATchans and Write30ATchans.

A simple test is to write a sine wave to the card connected to an oscilloscope. To test the read function, generate a sine function and try to read using the simple test program. The end of the user program should include a close(fd) call.

Putting It All Together: A Real-Time Controller

Using the DAQ card device drivers and the rtc device driver, we can write simple but effective control engineering code. In fact, this process is not too different from the test program. All we need to do is add some control code in the main loop. The example we use here is the well-known PID controller.

This done, we incorporate the timer set-up code from Paul Gortmaker's rtctest.c and start the main control loop. The standard control problem is making the physical system emulate a required ``reference signal'' as closely as possible. Monitoring the error signal--difference between the reference and the measured response--gives an indication of the controller performance. The PID control signal is of the form K(1+(dt/T_{i})+T_{d}/dt), where dt is the time step, and T_i and T_d are the integral action time and derivative action time, respectively. The control code (without timing) is shown in Listing 6.

Table 6. The Control Table

In this code we have set kp=K, ki=K*dt/T_{i} and kd=K*T_{d}/dt, (see Real-Time Computer Control by S. Bennett, 1988, Prentice-Hall, for more detail). Note also that we are only controlling a single channel, channel 0, in this case.

The timing code also needs to be included to obtain a real-time controller. The reference signal we are using in this example is a sine wave generated in the control loop using a time variable based on increments of the sampling interval. However, if desired, it would be possible to adapt this code to generate different reference signals, or read data from a file for use as the reference signal.

Conclusion

We have shown how to implement a simple real-time control engineering application using Linux. This includes writing custom device drivers for data acquisition cards that are loaded into the Linux kernel as modules. Timing the calls to driver functions has been achieved using the real-time clock driver available with the Linux OS. In the future, we would be interested in using a real-time Linux kernel and are watching the developments of these systems with interest.

The main advantage offered by Linux is accessibility to the source code. This is vital for programmers and engineers wishing to experiment and try new ideas. Also, it can be run on inexpensive PC hardware and is distributed either for free or at a very low cost. As a control engineer, writing your own device drivers not only gives insight into the DAQ process, it allows you total control over the actions of your computer-plant interface.

Acknowledgements

David Wagg (David.Wagg@bristol.ac.uk) is a lecturer in Dynamics at the University of Bristol, UK. His research interests include real-time control of engineering systems, particularly systems with nonlinearity. Apart from work, David is a keen climber and also likes to cycle and ski whenever possible. Eduardo Gómez obtained his PhD in Mechanical Engineering in 1999 from the University of Bristol. He has been involved in two major research programmers that applied adaptive control to a dynamic simulator, commonly used in earthquake engineering, known as a shaking table. He is now moving to the European Space Operation Centre (Darmstadt, Germany) to join the European Space Agency workforce.

Load Disqus comments