University of Leicester
Department of Physics and Astronomy
Third/Fourth Year Computing Workshop
Realtime and Interface Programming with C

Dr. R. Willingale

Sep 17, 2004

Contents

1  Introduction
    1.1  C and QNX
    1.2  Books and reference material
    1.3  The workshop problem
2  Getting started
    2.1  Using QNX
    2.2  Editing text files
    2.3  Running C programs
    2.4  Task 1 - the first C program
    2.5  Debugging a C program
3  Interfacing with the outside world
    3.1  The interface card
    3.2  Device drivers and QNX
4  Programming in C
    4.1  The structure of a C program
    4.2  Variables and types
    4.3  Addresses and values
    4.4  Input and output
5  Talking to the PCI-ADC
    5.1  Obtaining details about the PCI-ADC
    5.2  The #include and #define directives
    5.3  Constants
    5.4  Structures
    5.5  Conditional statement blocks
    5.6  Definite loops
    5.7  Task 2 - the show_adc program
    5.8  The PCI-ADC address map
    5.9  Task 3 - the regs_adc program
6  Analogue input and output
    6.1  Reading input data from the card
    6.2  Task 4 - the read_adc program
    6.3  Task 5 - the write_adc program
    6.4  Using the hardware timer
    6.5  Task 6 - the tick_adc program
    6.6  Using hardware interrupts
    6.7  Task 7 - the inter_adc program
    6.8  Effective use of the FIFO memory
    6.9  Task 8 - the fast_adc program
    6.10  Task 9 - program sigen_adc
7  Writing a QNX device driver for the PCI-ADC
    7.1  A description of the device driver
    7.2  Task 10 - program skel_adc
    7.3  Task 11 - a function to configure the PCI-ADC
    7.4  Messages and buffering in the device driver
        7.4.1  Immediate read or write of one data value
        7.4.2  Request for read of more than one value
        7.4.3  Request for write of more than one value
        7.4.4  Control messages
    7.5  Task 12 - implementation of POSIX read
    7.6  Task 13 - implementation of POSIX write
    7.7  Task 14 - implementation of control
    7.8  Task 15 - testing all functions of driver program
    7.9  The complete drive_adc program
8  The Photon Application Builder
    8.1  Widgets, events and modules
    8.2  Task 16 - creating a new application
    8.3  Task 17 - app_adc application
        8.3.1  Header and Initialisation function
        8.3.2  Window opening callback
        8.3.3  Using the PtNumericFloat widget to set/change the frequency
        8.3.4  The RtTrend widget to display a time series graph
        8.3.5  Setting up the grab button
        8.3.6  Testing the complete application
9  Writing a device handler for PGPLOT
    9.1  Task 18 - the device handler phdriv
10  Experiments using the PCI-ADC
    10.1  Task 19 - bifurcation sequences of a driven nonlinear oscillator
    10.2  Task 20 - demonstration of a flux gate magnetometer
11  Quick C Reference
    11.1  Variables, types and declarations
    11.2  Constants
    11.3  Reserved identifiers
    11.4  Input and output
    11.5  Operators
    11.6  Compiler directives
    11.7  Conditional statement blocks
    11.8  The switch case construct
    11.9  Definite loops
    11.10  Indefinite and infinite loops
    11.11  Functions and header files

1  Introduction

1.1  C and QNX

The Fortran 90 programming that you learnt in the first and second year was concerned with computation. The input and output were confined to setting parameters for the computation and listing or plotting the results. The details of how numbers entered at the keyboard actually got into the programme via a READ or how lines were plotted on a screen or piece of paper using the PGPLOT routines were hidden. The activities of the program itself were presented by the Fortran 90 compiler in a totally abstract way. As a programmer you didn't need to concern yourself about where the variables were stored in memory, how the floating point arithmetic was done and so forth. This was by design. Fortran 90 was primarily developed for this sort of abstract, problem solving, programming.

The main impact of computers on everyday life is not in the realms of numerical analysis and the like but in control and interaction with the physical world. Many cars are now equipped with ignition and fuel systems that are controlled by microprocessors. Modern telecommunications are totally dependent on digital control and digital signal processing. Washing machines, supermarket checkouts, traffic lights, fire alarms... the list is almost endless; they all rely on digital control by the ubiquitous microprocessor. Practical physics itself plays a vital role in the same revolution. All new experiments are controlled by a computer and more often than not the poor experimenter never gets to see the real equipment but spends his or her time staring at a computer screen.

All these computer and microprocessor control applications have to be programmed and this workshop aims to teach you the basics of how this is done. It is implemented using the operating system QNX and the computer language C. QNX is an operating system specifically designed for realtime programming applications. It was originally written to run on 8088 processors in 1980 and the second version QNX 2 was produced in 1982. Around 1990 a new version QNX 4 was introduced with enhanced 32-bit operations and POSIX support. We will meet POSIX later; it is a Portable System Interface consisting of a set of basic functions which can be called from C and other languages using suitable bindings. The common standard used at the moment is IEEE Std 1003.1. On the surface (the shell or command language) QNX is very like UNIX but the internal architecture is very different. The QNX operating system is very compact, fast, flexible and designed for the realtime computer programmer.

C is a strange beast that sits uneasily in the heirarchy of programming languages. It contains a high level of software abstraction like Fortran 90 while retaining all the messy details of the internal architecture associated with hardware implementation. It bridges the gap between assembler level programming, peeking and poking with bytes in memory, and high level programming concerned with mathematical functions, data structures and so on.

C is not a new language. It appeared in 1972 in the wake of BCPL and language B. The ANSI standard or ``ANSI C'' was completed in 1988. Since then it has had object-oriented syntax grafted onto it to form C++. In this workshop we shall restrict ourselves to ANSI C and its use in controlling or interfacing with the physical world. Interface programming is writing software which talks directly to a hardware device.

Realtime programming is about writing software which can meet deadlines. The system may have to read data from an external source fast enough so that nothing is missed or lost. Some form of computation may have to be performed and a result produced in a restricted time frame and finally the result or some other data might have to be output by a certain deadline or in some specified time sequence. In realtime programming the world external to the computer waits for no man (or computer).

1.2  Books and reference material

The definitive book on C is

``The C programming language ANSI C Version'' B.W.Kernighan and D.M. Ritchie, Prentice-Hall Software Series, 1988

Written by the original developers of the language this is an excellent reference but rather condensed and technical for a beginner. If you intend to take C further then this is an excellent buy.

There are many more books which may suit your pocket and/or style but choose carefully. Some older books may not cover the ANSII standard C and most new books will concentrate on C++.

The definitive book on C++ is by the head of the research group at AT&T Bell Laboratories in New Jersey who developed the language.

``The C++ Programming Language'', Bjarne Stroustrup, Addison-Wesley, 2nd Ed. 1991

This book contains an excellent summary of C but is mainly devoted to the intricacies of object-oriented programming with C++.

``Practical C programming'', Steve Oualline, O'Reilly & Associates, Inc., 2nd Ed. 1993

An excellent book giving all the basics and hints on programming style.

``Interfacing with C'', H. Hutchings, Electronics World + Wireless World, 1995, ISBN: 0 7506 2228 8, reprinted, Newnes, 1997

This is a book which I found useful in setting up the workshop although the approach is now rather dated.

The Leicester University Computer Centre have an ``Introduction to C Programming'' available on World Wide Web pages at

http://www.le.ac.uk/cc/tutorials/c/index.html

``Getting Started with QNX 4 - A guide for Realtime Programmers'', Bob Krten, Parse Software Devices, 1st Ed. 1998

A useful introduction to programming with QNX 4. There is a copy of this book available in the 3rd/4th year laboratory.

The following references give further information about the experiments described at the end of the workshop:

``Recent Advances in Fluxgate Magnetometry'' Daniel I. Gordon and Robert E. Brown, IEEE Trans. on Magnetics, Vol. MAG-8, No.1, 76-82, March 1972

``The Fluxgate Mechanism, Part I: The Gating Curves of Parallel and Orthogonal Fluxgates'' Fritz Primdahl, IEEE Trans. on Magnetics, Vol. MAG-6, No.2, 376-383, June 1970

``Evidence for Universal Chaotic Behaviour of a Driven Nonlinear Oscillator'' James Testa, José Pérez and Carson Jeffries, Phys. Rev. Lett. 48, 714-717, 1982

``Period Doubling and Chaotic Behaviour in a Driven Anharmonic Oscillator'' Paul S. Linsay, Phys. Rev. Lett. 47, 1349-1352, 1981

1.3  The workshop problem

This workshop is designed around the sort of mission or programming problem which you might be faced with if you were employed as a scientific systems programmer. You are given a PC running a custom built operating system with a particular ADC card installed and you are asked to write a device driver for the card and then to program the system to perform some specified experiment incorporating a graphical user interface (GUI) and implementing high level graphical presentation of the results. You are given the documentation that was supplied with the card, manuals for the operating system, a C compiler and C library and 5 weeks to complete the job.

The tasks in the workshop fall into 4 phases:

2  Getting started

2.1  Using QNX

Each student doing the workshop is allocated a PC running the operating system QNX 4.25. You will have complete control of the machine including system privelages so that you can do the realtime programming effectively. The boot for each machine is configured so that it initially enters a basic screen mode and issues a login prompt. You should login as user root using the system password provided.

Welcome to QNX 4.25
Copyright (c) QNX Software Systems Ltd. 1982,1998
login: root
password:
Last login: Wed Sep 01 09:56:08 1999 on //1/dev/ttyp0
Fri Sep 03 09:14:34 1999
qnx1 root$ 

This will log you in as superuser in the root directory. You can check this using the pwd (print working directory) and ls (list current directory) commands. The response you get should be something like:

qnx1 root$ pwd
/
qnx1 root$ ls
.                  .licenses.bak      dev                registry
..                 .photon            dos                tmp
.abcfg             .profile           etc                tty
.altboot           .prognet           f2c                tut1
.bitmap            Mail               home               usr
.boot              Virtual_Scope      pgplot             vedit
.inodes            bin                pgplot_source
.kshrc             boot               pipe
.lastlogin         cd                 qnx4
.licenses          cd1.0              readme

Your first job is to create a user name for yourself. This can be accomplished using the command passwd and your IRIX username:

qnx1 root$ passwd ug1
User id # (102) 
Group id # (100) 200
Real name () Under Graduate
Home directory (/home/ug1) 
Login shell (/bin/sh) 
New password:
Retype new password:

You should take the default User id, Home directory and Login shell. Type Group id 200 (reserved for students) and select you own Real name and New password.

Now use the cd command to select your home directory and copy the .profile and .kshrc files from root. The .profile sets up environment variables when you log in and .kshrc will configure the shell prompt and set a few aliases.

qnx1 root$ cd /home/ug1
qnx1 root$ cp /.profile .
qnx1 root$ cp /.kshrc .

You should check all this is correct by logging off from root and logging on again as your new username.

qnx1 root$ exit
Welcome to QNX 4.25
Copyright (c) QNX Software Systems Ltd. 1982,1998
login: ug1
password:
Fri Sep 03 09:36:50 1999
qnx1 ug1$ pwd
/home/ug1
qnx1 ug1$ ls -a
.              .kshrc         .profile
..             .lastlogin
qnx1 ug1$ 

The prompt should now include your machine name and username as indicated above.

You should normally log onto the machine as your username rather than root and keep your programs etc. in your home directory. When you start to run programs which talk directly to the hardware you will need the privity level of superuser. You can change to superuser using the su command and using the root password.

qnx1 ug1$ su
password:
qnx1 ug1$

Use the command exit to drop back to your original user login session.

qnx1 ug1$ exit 
qnx1 ug1$

As you can see from the above the command shell is very UNIX like and most of the common UNIX commands are available. The usual control keys can be used to move the cursor about and recall previous command lines:

ctrl p        go back a line
ctrl n        go forward a line
ctrl b        go back a character
ctrl f        go forward a character

You can start the Photon GUI (Photon is the name of the resident QNX Graphical User Interface or windows system) using the command:

qnx1 ug1$ ph

Finding your way around the Photon desktop is fairly easy; all the basic functions you expect to get are there. To exit from Photon click on QNX in the bottom left hand corner of the screen and choose shutdown.

The World Wide Web browser is called Voyager and has an icon with a sailing ship on it. It is installed in the applications menu for root and it defaults to http://www.qnx.com/ the website of QNX Software System Ltd.

Both ftp and telnet are available to give access to remote machines. Simply issue a shell command:

qnx1 ug1$ ftp hostname or IP address
qnx1 ug1$ telnet hostname or IP address

2.2  Editing text files

Before you start programming you must learn how to use a text editor available in QNX. You have a choice of two editors, vi or vedit. The vi editor is an emulation of the UNIX visual display editor and vedit is the resident full screen text editor. If you learn vi then you will be able to use it on all flavours of UNIX (LINUX etc.) and QNX systems but the syntax and mode of operation is rather different from more conventional screen editors like EVE and EMACS. The vedit is very similar to screen editors from other systems but is only available under QNX. If you are using the Photon GUI file manager then the edit option under the file menu will automatically put you into vedit. I suspect most of you will prefer to use vedit but the choice is yours.

Help on both vi and vedit is available in the Photon help. Click on the QNX 4.25 section and then go to utilities reference section N to Z.

When using vi the escape key is ctrl [. You must use this to get out of insert mode and into command mode. There are several ways of exiting from vi back to the shell command prompt. The easiest is ZZ (uppercase Z twice) when in command mode. If you want to quit WITHOUT saving (overwriting) the file use : (colon) to bring up a prompt at the bottom of the screen and then use the command q! to exit.

When using vedit ctrl [, Esc or Alt-X all allow you to exit from the editor.

You should spend 20 minutes or so deciding on which editor you are going to use and trying out its basic functions. The best way of learning about the more sophisticated features in the editors is to use them for the realtime programming tasks to follow.

2.3  Running C programs

The process of creating a source file, compiling and linking and finally running a C program is very similar to using Fortran 90. The souce code is traditionally entered into a text file with name extension .c and this is compiled by the command

qnx1 ug1$ cc prog.c

By default this produces a binary, executable file called a.out which can be run by evoking the name

qnx1 ug1$ a.out

The name of the executable can be specified using the -o switch

qnx1 ug1$ cc -o prog prog.c
qnx1 ug1$ prog

Alternatively creation of an executable binary can be suppressed altogether (for example if you are creating an object library of compiled functions) using the -c switch

qnx1 ug1$ cc -c source.c

This will create the object file source.o which can then be included in an object archive or library or linked directly with a compiled program.

2.4  Task 1 - the first C program

This first task is a simple exercise to give you some practise with the editor, the C compiler and the QNX system in general. Type the following simple C program into a file first.c and then compile and run it in the way described above.

/* A simple C program to read and write from the terminal screen */
#include <stdio.h>
void main()
{
        int value;
        printf("type an integer value : ");
        scanf("%d",&value);
        printf("value typed %d (decimal)\t %x (hex)\n",value,value);
}

The structure of this program will be described in detail later. The output from the program should be something like:

...
qnx1 ug1$ cc first.c -o first
qnx1 ug1$ first
type an integer value : 456
value typed 456 (decimal)        1c8 (hex)

On completion of Task 1 you should have: logged on to your QNX system, created your user name and home directory, configured the home directory by copying the .profile and .kshrc files, familiarised yourself with the system, edited a C source file and compiled and run the first program.

Task 1 completed on:   demonstrator:

2.5  Debugging a C program

In the following sections you are going to write a considerable amount of C source code and you are bound to make mistakes. If the errors are syntactical then the program simply won't compile and run. However it is easy to write a C program which compiles fine but behaves incorrectly when run - i.e. gives the wrong answer! The best way to check how a program is working is to print out separate variables etc. at well chosen points in the sequence or flow of the program to see if they are as you expect. So if your program is not behaving properly put in some printf statements like the one in the first program above to check what is going on. Make sure ALL the variables have the values you expect. The details of how to specify the format of the text you list using printf are given in section 4.4.

3  Interfacing with the outside world

3.1  The interface card

A computer talks to the outside world using bit patterns. These patterns are handled by some form of ``interface card''. This is a piece of hardware that plugs into the address bus of the computer. The computer ``sees'' this interface card as a series of registers that appear in the address space of the machine. The device is said to be ``mapped into the address space'' of the machine. Usually one register has a control function. That is, if a program writes a bit pattern to the address of the control register this configures the way in which the interface card operates. Reading from the same register will tell the computer how the interface card is configured. The more complicated interface cards are effectively programmable and the control register/registers may be rather sophisticated. The remaining registers constitute input or output ports. For example they may be the digital result from an A-to-D converter that provides digital input of an analogue signal.

The details of the registers, how they operate and where they will appear in the machines address space is hardware specific. There is no universal way in which such devices operate although many adopt similar principles of operation. In order to proceed further you will see that we have to be chip specific and explain the details of how a particular interface card works.

The particular interface card installed in the PCs running QNX are PCI-ADC cards manufactured by Blue Chip Technology Ltd. The Peripheral Component Interconnect (PCI) is a bus used for high bandwidth communication with hardware devices on PCs and the Analogue to Digital Converter (ADC) card provides 8 differential analogue inputs, 12 bit resolution ±5 mV to ±5 V, and 4 bipolar analogue outputs, 12 bit resolution ±10 V or ±20 mA. There are also 3 programmable counter/timers for generating interrupts etc..

3.2  Device drivers and QNX

When the PC is switched on the Basic Input Output System (BIOS) detects the PCI-ADC and will allocate it a set of base addresses and an interrupt request line. These parameters are then used by application software to access and use the card. Within QNX and most other operating systems all devices on the bus communicate with applications software using device drivers. These are programs which are usually installed and configured in the operating system when the system is booted and remain installed while the system is running. Whenever an interrupt is generated by a particular device or some data are to be read from or written to a device the appropriate device driver is called into action.

Device drivers have to be designed and configured to suit both the hardware of the device and the environment of the operating system and are they notoriously difficult programs to write and install. QNX was specifically designed for the development, maintainance and easy installation and de-installation of device drivers. Unlike most operating systems any of the device drivers in QNX can be started, stopped and controlled independently without crashing the rest of the kernel software.

Most operating system kernels are monolithic in nature. They consist of many parts (device drivers included) but these parts are glued together very tightly such that if one piece of the system starts to fail (comes unstuck) then the whole system falls apart. In constrast QNX is very modular and robust. All the modules communicate with one another using a message system. This message system acts as the glue but failure of the glue in one part of the system is usually not fatal to the rest.

Device drivers in QNX are written in C. This language provides us with tools to read and write bit patterns from the interface card registers and to service interrupts. In addition the QNX Watcom C Library also provides all the services required to integrate fully the device into the system such that all the appropriate system software can use the device in a uniform, seamless fashion.

4  Programming in C

This section gives you a short introduction to C, explaining all the elements of the first program above and introducing a few more concepts which you will need to read and understand C programs.

4.1  The structure of a C program

We will start by looking at the simple program used in the first task above. The only form of module in C is the function and all C programs consist of a collection of such functions. The principle function entered from the operating system is called main(). So the function main() is equivalent to the PROGRAM module in Fortran 90. C functions take the place of both SUBROUTINEs and FUNCTIONs in Fortran 90.

/* A simple C program to read and write from the terminal screen */
#include <stdio.h>
void main()
{
        int value;
        printf("type an integer value : "); /* prompt for integer */
        scanf("%d",&value); /* read integer */
        printf("value typed %d (decimal)\t %x (hex)\n",value,value);
}

Any text in the source file enclosed between /* and */ is treated as a comment and ignored by the compiler. Such comments can appear anywhere in the code. You can also use a double slash // to indicate that the rest of a line should be ignored (i.e. treated as a comment).

The declarations of all the standard functions used by C are held on header files which are stored in some predefined directory known to the compiler. In order to use these functions the appropriate header file must be included at the top of the source file using the #include directive. The <> indicates that the enclosed file name is to be found on the predefined system directory. The stdio.h header file contains the declarations of the standard input/output functions which are used in this example program. There is no exact equivalent of the Fortran 90 MODULE although header files perform a similar role.

Functions in C usually have arguments and often have a return value. On some systems it is customary for main() to return an integer value to the system (a status return) however the Watcom compiler on QNX requires no return value for main so it is declared void. It is also possible for main() to have arguments used to pass parameters from the system when the program is started but in this simple example there are no arguments so the declaration appears as

void main()

The body of the function is enclosed in curly brackets {}. This is the way statements are grouped together in C and form what is known as the scope of the function. All the variables declared within this scope are local. They are created when the function is evoked and destroyed when the function returns. Conversely any declarations made outside this scope (somewhere else in the source file but not within some other scope) will be global within the file. So declarations of variables, function names etc. in header files like stdio.h are global.

The layout of the C statements in lines is not important although it is advisable to stick to the sort of format used above so that the structure of the functions is obvious. Note that every statement that is compiled is terminated by a semi-colon. This is most important and it you omit these semi-colons you are likely to get many strange error messages.

4.2  Variables and types

The fundamental data types in C are:

char                a single character (usually 1 byte of 8 bits)
short int           usually 1 byte
int                 an integer usually 2 bytes
long int            a large integer usually 4 bytes
float               single precision floating point usually 4 bytes
double              double precision floating point
long double         high precision floating point

The integer types including char can be qualified by unsigned or signed to determine whether or not a sign bit is included.

unsigned int        usually 2 bytes giving range 0 to 65535
signed int          usually 2 bytes giving range -32768 to 32767
unsigned char       1 byte giving range 0 to 255
signed char         1 byte giving range -128 to 127

When using C for driving an interface board or similar the correct use of the correct data type is most important as you will see.

All variables used in C must be declared and all variables and functions must be declared to have a type. The names must start with a alphabetic character and must be unique for the first 8 characters. There is a set of identifiers reserved for use as keywords in C and C++ and these must not be used otherwise.

asm    continue   float     new        signed    try
auto   default    for       operator   sizeof    typedef
break  delete     friend    private    static    union
case   do         goto      protected  struct    unsigned
catch  double     if        public     switch    virtual
char   else       inline    register   template  void
class  enum       int       return     this      volatile
const  extern     long      short      throw     while

Note that all these keywords are in lower case. C is case sensitive so variables ``value'' and ``Value'' are different and for example, ``Do'' is not a keyword and can be a variable name (although this might not be a very clever idea).

The line in our first program

        int value;

declares an integer variable called value.

4.3  Addresses and values

Variable names in a computer language are associated with the value that is stored and this is what you usually think of when you cite a variable in the code. However there is also a memory address associated with a variable telling the computer where the value is stored. In a language like Fortran 90 the programmer does not have to bother about which of these numbers is required in a given situation. In arithmetic expressions it is the value but in subroutine calls or function arguments it is the address since arguments in Fortran are passed by reference not by value.

In C the distinction is important. Arguments are passed by value. This means that if you want a result to be returned as an argument you must pass the address of the appropriate variable. This is why scanf() in the above program looked like

        scanf("%d",&value);

The prefix operator & gets the address or reference for the variable called value.

The fundamental data types were listed in the previous section. So called ``derived types'' can be created using declaration operators. This is where the syntax of C diverges from the sort of thing you are familiar with in Fortran 90 and it is the aspect of C which makes it so powerful for the task in hand. The declaration operators are:

*              pointer, a prefix operator
&              reference, a prefix operator
[]             array, a postfix operator
()             function, a postfix operator

In C there is a derived data type called a pointer. This provides a reference to a variable of a specified type. Note that a pointer is associated with an address AND a type and is not just an integer address. The unary prefix operator * is used to create a pointer in a declaration

int *p;
float *f;

In the above p is a pointer to an integer and f is a pointer to a floating point number. The * operator is also used as the indirection operator to provide a primitive value from a pointer

int q=*p;
float s=*f+1.1;

Here *p is an integer and *f is a floating point number. Note that the unary indirection operator always acts on the name following it so if we have the declarations

int *p,q;

then p will be a pointer to an integer but q will be just an integer.

Another form of derived type is the array. This is declared as

int ar[10];

The variable ar on its own is then a pointer to the first integer in the array and the operator [] enclosing some integer expression (number or index) will generate the value of an integer member of the array. The index values run from 0 to n-1.

Finally we should mention the process of type conversion. In C this is achieved by ``casting''. The syntax is a little odd. A cast is set by enclosing the target type in () parentheses just before the value (or expression with a particular type).

float f;
int t=33;
f=(float)t;

In the above the cast (float)t makes sure that the righthand side of the assignment is the same type as the lefthand side.

4.4  Input and output

The three executable lines in our first program use the printf() function to write to the terminal and scanf() to read from the terminal. Unlike Fortran 90 where I/O is supported by special operations like READ and OPEN in C everything is supported by functions. These functions are supplied in a runtime library and are declared (made know to the compiler) using header files. The first argument of both functions is a format string. Embedded in the format string are specifiers prefixed with % which tell the computer how to write or read the remaining arguments.

        scanf("%d",&value);

For example %d means read a decimal integer from the input stream. The result is placed at the address of the variable value which is written &value. We will hear a lot more about addresses a little later. Similarly in the following printf() statement there are two specifiers %d and %x corresponding to two integer arguments both of which happen to be value.

        printf("value typed %d (decimal)\t %x (hex)\n",value,value);

The complete set of format specifiers is given below

%d        integer decimal notation
%o        integer unsigned octal notation
%x        integer unsigned hexadecimal notation
%u        integer unsigned decimal
%c        single character
%s        string of characters
%e        floating point (single or double) exponential notation
%f        floating point (single or double) decimal notation
%g        floating point as %e or %f, whichever is shorter

The \t and \n are escape characters, tab (tabulate) and produce a new line respectively. The full list of such escapes that stand for single characters and can appear in character strings is

\n        newline
\t        horizontal tab
\v        vertical tab
\b        backspace
\r        carriage return
\f        form feed
\a        alert or bell
\\        backslash
\?        question mark
\'        single quote
\"        double quote
\0        null
\ooo      octal number
\xhhh     hexadecimal number

The octal or hexadecimal number forms make it possible to specify any character in the machines character set but use of them will make the program nonportable. The string of octal or hexadecimal digits is terminated by the first character that is not such a digit.

5  Talking to the PCI-ADC

5.1  Obtaining details about the PCI-ADC

As has already been mentioned when the PC is switched on the Basic Input Output System (BIOS) detects the PCI-ADC and will allocate it a set of base addresses and an interrupt request line. There is also a QNX utility show_pci which lists information about devices on the PCI bus. If you try this you will find that the PCI-ADC board is not included. This is because the base address configuration of the board is unconventional. However if you use:

qnx1 ug1$ show_pci -v

then the device does appear.

In order to use the device these parameters must be made available to software. You could read the listing produced by show_pci and type the numbers into a C source program but this would be very inconvenient! However the Watcom C library includes functions which give access to the PCI BIOS parameters and it is these functions that are used by the utility show_pci. The following program which we shall call show_adc makes use of 3 of these functions to list the required information at the terminal.

/* Program show_adc */
/* Search for PCI-ADC card and list details */
#include <stdio.h>
#include <sys/pci.h>

#define DEVICE_ID 0x0adc
#define VENDOR_ID 0x13c7

void main()
{
  struct _pci_config_regs  pci_reg;
  unsigned lastbus,version,hardware,bus,device;
  int i,pci_ind=0;
    
  if(_CA_PCI_BIOS_Present(&lastbus,&version,&hardware)!=PCI_SUCCESS)
  {
    printf("Cannot find PCI BIOS\n");
    exit(0);
  }

  if(_CA_PCI_Find_Device(DEVICE_ID,VENDOR_ID,pci_ind,&bus,&device)!=PCI_SUCCESS)
  {
    printf("Cannot find PCI-ADC\n");
    exit(0);
  }

  if(_CA_PCI_Read_Config_DWord(bus,device,0,16,(char *)&pci_reg)!=PCI_SUCCESS)
  {
    printf("Cannot read from configuration space of PCI-ADC\n");
    exit(0);
  }
  
  printf("Found PCI-ADC Card as device %d \n",device>>3);
  printf("Vendor ID                    %xh\n",pci_reg.Vendor_ID);
  printf("Device ID                    %xh\n",pci_reg.Device_ID);
  printf("Cache Line Size              %xh\n",pci_reg.Cache_Line_Size);
  printf("Latency Timer                %xh\n",pci_reg.Latency_Timer);
  printf("Header Type                  %xh\n",pci_reg.Header_Type);
  printf("Built In Self Test           %xh\n",pci_reg.BIST);
  for (i=0;i<6;i++)
    printf("Base Address Number %d:    %xh\n",i,pci_reg.Base_Address_Regs[i]);
  printf("Interrupt Line               %d\n",pci_reg.Interrupt_Line);
  printf("Interrupt Pin                %d\n",pci_reg.Interrupt_Pin);

}

5.2  The #include and #define directives

The definitions associated with the pci functions in the library are included using

#include <sys/pci.h>

PCI cards have a device and vendor identifier coded into them. The particular PCI-ADC card installed in the workshop PCs has device identifier hexadecimal 0ADC and vendor identifier hexadecimal 13C7. In show_adc these constants are set using the #define directive.

#define DEVICE_ID 0x0adc

This sets up a macro (compiler procedure) that is used by the compiler to pre-process the source text file. Every occurance of the name DEVICE_ID which is found in the source file will be replaced by the text 0x0adc.

Both #include and #define are directives which manipulate the source file text in some way. The former brings in text from some specified header file and the latter edits the source file text. Frankly these directives can make C source code very difficult to read and understand but we are stuck with them. For example the macro definition of the name PCI_SUCCESS used in the program above is part of the pci.h header file but this is not obvious to the casual reader. In general you should not use a #define macro definition unless you have to but in this case it is not unreasonable.

5.3  Constants

Integer constants are written as:

9876           assumed type int
987654321L     type long
987654321l     type long
U9876          type unsigned int
u9876          type unsigned int

Integers can be expressed in octal or hexadecimal:

0123           leading zero indicate octal constant
0x134A         leading 0x (zero x) indicates hexadecimal

Floating point constants are written as:

4.321   type double
4.3e-5  type double
3.1f    type float
3.1e-1F type float
3.1l    type long double
3.1e-1L type long double

Character constants appear within single quotes. Special characters are specified using the escape characters already listed under the section on Input/Output above:

's'             the byte representing character s
'\a'            the byte representing bell
'\007'          bell specified using octal
'\x7'           bell specified using hexadecimal

A character string constant is enclosed in double quotes.

"This is a character string constant"

5.4  Structures

A structure is a bunch of variables grouped together and given a single name for convenience. In the show_adc program we have:

struct _pci_config_regs  pci_reg;

This declares a structure of type _pci_config_regs and name pci_reg. It is important to distinguish between the typename and the name of a particular instance of the type. The _pci_config_regs is a type name just like char of float or all other types whereas pci_reg is the name of a particular variable in this program. The definition of the structure of type _pci_config_regs is actually done in the sys/pci.h file:

struct _pci_config_regs
{
     unsigned short          Vendor_ID;
     unsigned short          Device_ID;
     unsigned short          Command;
     unsigned short          Status;
     char                    Revision_ID;
     char                    Class_Code[3];
     char                    Cache_Line_Size;
     char                    Latency_Timer;
     char                    Header_Type;
     char                    BIST;
     unsigned long           Base_Address_Regs[6];
     unsigned long           Reserved1[2];
     unsigned long           ROM_Base_Address;
     unsigned long           Reserved2[2];
     char                    Interrupt_Line;
     char                    Interrupt_Pin;
     char                    Min_Gnt;
     char                    Max_Lat;
     char                    Device_Dependent_Regs[192];
};

The components of the structure are referenced using the variable name of the declared structure (in the above this is pci_reg) followed by a dot and the name of the component within the structure. For example:

  printf("Interrupt Line %d\n",pci_reg.Interrupt_Line);

This prints the value of the variable pci_reg.Interrupt_Line as an integer.

Although not used in the above example it is possible and indeed very common practice to declare a pointer to a structure as a variable. You could have:

struct _pci_config_regs  *point_pci_reg;

In this case the variable point_pci_reg would be a pointer and the components in the structure must be referenced using the pointer * operator or a pointer member selection operator (hyphen followed by >):

(*point_pci_reg).Interrupt_Line
point_pci_reg->Interrupt_Line

Note that *point_pci_reg.Interrupt_Line or *(point_pci_reg.Interrupt_Line) are illegal because the component Interrupt_Line is not a pointer.

5.5  Conditional statement blocks

The basic conditional statement block has the form:

if(expression1)
          statement1;
else if (expression2)
          statement2;
else
          statement;

If the statements require more than one line you must use curly braces to gather together the scope of each conditional:

if(expression1)
{
          statement1a;
          statement1b;
          ...
}
else if (expression2)
{
          statement2a;
          statement2b;
          ...
}
else
{
          statementa;
          statementb;
          ...
}

In either case the statement or statements following the (expression) are executed if the value of the expression is true or non-zero. In our show_adc program above the status returns from functions are used to control the flow through the conditional blocks.

  if(_CA_PCI_BIOS_Present(&lastbus,&version,&hardware)!=PCI_SUCCESS)
  {
    printf("Cannot find PCI BIOS\n");
    exit(0);
  }

In this case the function _CA_PCI_BIOS_Present() returns the value specified by the constant PCI_SUCCESS if the PCI BIOS was found. The expression controlling the block uses the != not equals operator to generate a logical value such that the error message and exit(0) are executed if the PCI BIOS is not found. The exit(0) function causes termination of the program.

5.6  Definite loops

A typical definite loop has the form:

        int a[10];
        int i;
        for(i=0;i<10;i++)
        {
                a[i]=i;
        }

The i=0; defines the initial value of the integer counter i. The i<10; is the test used at the start of each pass. If true then the loop will continue. Finally i++ is the operation on the counter for each pass. The ++ is the incrementing operator (add 1). Used as a postfix operator it is applied after i is used. If it were a prefix then it would be applied before i were used. The body of the loop appears between {}. In this case

        {
                a[i]=i;
        }

which takes the current value of i and puts it into ith element of the declared array.

When the loop consist of just one statement as in this case the {} are redundant and the loop could have been written

        for(i=0;i<10;i++)
                a[i]=i;

Note that the loop runs for value of i 0 to 9. When i=10 the loop is completed and the body of the loop is NOT executed. Note also that the array index runs 0 to 9 NOT 1 to 10. Referencing a[10] in this case would be illegal.

In the show_adc program a definite loop is used to list the base addresses of the 6 registers of the PCI board. Note that the registers are numbered 0 to 5. The counter integer i is declared at the top of the program.

  for (i=0;i<6;i++)
    printf("Base Address Number %d: %xh\n",i,pci_reg.Base_Address_Regs[i]);

5.7  Task 2 - the show_adc program

Look up the functions _CA_PCI_BIOS_Present(), _CA_PCI_Find_Device() and _CA_PCI_Read_Config_DWord() in the QNX Watcom Library - A to M manual to see what they do and how the arguments are used.

Type the source code of show_adc into a file show_adc.c, compile and run the program. You will need to set the privity level of the binary file created using the -T1 compiler option because the program messes with the hardware directly and the normal user privity level is not high enough to allow this under QNX. In order to use the program you must be superuser (see above for details of the su command.

qnx1 ug1$ cc -T1 show_adc.c -o show_adc

Task 2 completed on:   demonstrator:

5.8  The PCI-ADC address map

Before we can use the PCI-ADC we must construct the address map of the card. This is described on page 5 of the PCI-ADC User Manual. The actual addresses depend on the machine and where the card is plugged into the PCI bus. For convenience we will collect together the address map information in a header file called pciadc.h.

/* Details of Blue Chip Technology PCI-ADC card */
#ifndef pciadc_h
#define pciadc_h

#define DEVICE_ID 0x0adc
#define VENDOR_ID 0x13c7

struct ADC_regs
{
  int     PIO_PortA_IO_Reg;
  int     PIO_PortB_IO_Reg;
  int     PIO_PortC_IO_Reg;
  int     PIO_Cntrl_Reg;
  int     Count_Time_0_Cnt_Reg;
  int     Count_Time_1_Cnt_Reg;
  int     Count_Time_2_Cnt_Reg;
  int     Count_Time_Cntrl_Reg;
  int     Counter_Cntrl_Reg;
  int     Inter_Enable_Reg;
  int     Inter_Status_Reg;
  int     Ana_OP_Cntrl_Reg;
  int     Ana_Conv_Cntrl_Reg;
  int     Ana_Inp_Select_Reg;
  int     Ana_Inp_Status_Reg;
  int     Ana_Inp_Sample;
  int     Ana_O_Sample_Ch0;
  int     Ana_O_Sample_Ch1;
  int     Ana_O_Sample_Ch2;
  int     Ana_O_Sample_Ch3;
  int     Interrupt_Line;
};

extern int reg_pci_adc(struct ADC_regs *ADC);

#endif

The #ifndef checks whether or not the macro pciadc_h is defined. If it isn't the next line defines it and the rest of the file is executed. If it is then control jumps to the bottom of the file at #endif. These lines ensure that the header definitions are only set once even if the file is included several times.

This header file includes the device and vendor identification codes, a structure to hold all the addresses and a declaration of a function to get the register addresses into the structure. The extern modifier indicates that the declared function reg_pci_adc is defined outside the current file. The argument of the function *ADC is a pointer to a structure of type ADC_regs. This is because we require the function to return the register addresses in the structure.

Using the information from the manual we need to write the code of the function to calculate all the register address using the base addresses already obtained by the show_pci program. The following definition of the function does this. Note that the definition of function return value, name and arguments agrees with the declaration in the pciadc.h header file.

/* Get PCI-ADC register addresses */
#include <stdio.h>
#include <stdlib.h>
#include <sys/pci.h>
#include "pciadc.h"

int reg_pci_adc(struct ADC_regs *ADC)
{
  unsigned device,bus,lastbus,version,hardware;
  int pci_ind=0;
  struct _pci_config_regs pci_reg;
      
  if(_CA_PCI_BIOS_Present(&lastbus,&version,&hardware)!=PCI_SUCCESS)
  {
    printf("reg_pci_adc: cannot find PCI BIOS\n");
    return EXIT_FAILURE;
  }

  if(_CA_PCI_Find_Device(DEVICE_ID,VENDOR_ID,pci_ind,&bus,&device)!=PCI_SUCCESS)
  {
    printf("reg_pci_adc: cannot find PCI-ADC\n");
    return EXIT_FAILURE;
  }

  if(_CA_PCI_Read_Config_DWord(bus,device,0,16,(char *)&pci_reg)!=PCI_SUCCESS)
  {
    printf("reg_pci_adc: cannot read from configuration space of PCI-ADC\n");
    return EXIT_FAILURE;
  }

  ADC->PIO_PortA_IO_Reg =     (int)pci_reg.Base_Address_Regs[2]-1;
  ADC->PIO_PortB_IO_Reg =     (int)pci_reg.Base_Address_Regs[2];
  ADC->PIO_PortC_IO_Reg =     (int)pci_reg.Base_Address_Regs[2]+1;
  ADC->PIO_Cntrl_Reg =        (int)pci_reg.Base_Address_Regs[2]+2;
  ADC->Count_Time_0_Cnt_Reg = (int)pci_reg.Base_Address_Regs[2]+3;
  ADC->Count_Time_1_Cnt_Reg = (int)pci_reg.Base_Address_Regs[2]+4;
  ADC->Count_Time_2_Cnt_Reg = (int)pci_reg.Base_Address_Regs[2]+5;
  ADC->Count_Time_Cntrl_Reg = (int)pci_reg.Base_Address_Regs[2]+6;
  ADC->Counter_Cntrl_Reg =    (int)pci_reg.Base_Address_Regs[2]+7;
  ADC->Inter_Enable_Reg =     (int)pci_reg.Base_Address_Regs[2]+8;
  ADC->Inter_Status_Reg =     (int)pci_reg.Base_Address_Regs[2]+9;
  ADC->Ana_OP_Cntrl_Reg =     (int)pci_reg.Base_Address_Regs[2]+10;
  ADC->Ana_Conv_Cntrl_Reg =   (int)pci_reg.Base_Address_Regs[2]+11;
  ADC->Ana_Inp_Select_Reg =   (int)pci_reg.Base_Address_Regs[2]+12;
  ADC->Ana_Inp_Status_Reg =   (int)pci_reg.Base_Address_Regs[2]+13;
  ADC->Ana_Inp_Sample =       (int)pci_reg.Base_Address_Regs[3]-1;
  ADC->Ana_O_Sample_Ch0 =     (int)pci_reg.Base_Address_Regs[4]-1;
  ADC->Ana_O_Sample_Ch1 =     (int)pci_reg.Base_Address_Regs[4]+1;
  ADC->Ana_O_Sample_Ch2 =     (int)pci_reg.Base_Address_Regs[4]+3;
  ADC->Ana_O_Sample_Ch3 =     (int)pci_reg.Base_Address_Regs[4]+5;
  ADC->Interrupt_Line =       (int)pci_reg.Interrupt_Line;

  return EXIT_SUCCESS;
}

The #include "pciadc.h" uses quotes because the header file is not a system header file but a local one we have written. The function includes the same code as show_pci. It checks if the PCI-BIOS is present, looks for the device and gets the configuration word. Finally it sets all the addresses in the ADC structure using the base addresses. Because ADC is a pointer the pointer to member operator -> is used.

The return value of the function was defined as an integer. This is set by the return statements. The return EXIT_FAILURE indicates failure while return EXIT_SUCCESS indicates all is OK.

We can test out the above using a simple program

/* Program regs_adc */
/* Search for PCI-ADC card and return register addresses */
#include <stdio.h>
#include <stdlib.h>
#include <sys/pci.h>
#include "pciadc.h"

void main()
{
  struct ADC_regs ADC;
                              
  if(reg_pci_adc(&ADC)!=EXIT_SUCCESS)
  {
    printf("Failed to get PCI-ADC register addresses\n");
    exit(0);
  }

  printf("PIO_PortA_IO_Reg     %xh \n",ADC.PIO_PortA_IO_Reg);
  printf("PIO_PortB_IO_Reg     %xh \n",ADC.PIO_PortB_IO_Reg);
  printf("PIO_PortC_IO_Reg     %xh \n",ADC.PIO_PortC_IO_Reg);
  printf("PIO_Cntrl_Reg        %xh \n",ADC.PIO_Cntrl_Reg);
  printf("Count_Time_0_Cnt_Reg %xh \n",ADC.Count_Time_0_Cnt_Reg);
  printf("Count_Time_1_Cnt_Reg %xh \n",ADC.Count_Time_1_Cnt_Reg);
  printf("Count_Time_2_Cnt_Reg %xh \n",ADC.Count_Time_2_Cnt_Reg);
  printf("Count_Time_Cntrl_Reg %xh \n",ADC.Count_Time_Cntrl_Reg);
  printf("Counter_Cntrl_Reg    %xh \n",ADC.Counter_Cntrl_Reg);
  printf("Inter_Enable_Reg     %xh \n",ADC.Inter_Enable_Reg);
  printf("Inter_Status_Reg     %xh \n",ADC.Inter_Status_Reg);
  printf("Ana_OP_Cntrl_Reg     %xh \n",ADC.Ana_OP_Cntrl_Reg);
  printf("Ana_Conv_Cntrl_Reg   %xh \n",ADC.Ana_Conv_Cntrl_Reg);
  printf("Ana_Inp_Select_Reg   %xh \n",ADC.Ana_Inp_Select_Reg);
  printf("Ana_Inp_Status_Reg   %xh \n",ADC.Ana_Inp_Status_Reg);
  printf("Ana_Inp_Sample       %xh \n",ADC.Ana_Inp_Sample);
  printf("Ana_O_Sample_Ch0     %xh \n",ADC.Ana_O_Sample_Ch0);
  printf("Ana_O_Sample_Ch1     %xh \n",ADC.Ana_O_Sample_Ch1);
  printf("Ana_O_Sample_Ch2     %xh \n",ADC.Ana_O_Sample_Ch2);
  printf("Ana_O_Sample_Ch3     %xh \n",ADC.Ana_O_Sample_Ch3); 
  printf("Interrupt Line       %d  \n",ADC.Interrupt_Line);

}

Because the function reg_pci_adc requires a pointer the argument is written as &ADC. Within this program the variable ADC is not a pointer so components are referenced using a dot member operator ADC.PIO_PortA_IO_Reg etc.. Before listing the addresses the program checks the status returned by the reg_pci_adc() function and lists an error message if it has failed.

5.9  Task 3 - the regs_adc program

Type the header code into the file pciadc.h and the function code into file reg_pci_adc.c. Compile the function using the -c switch so that just an object file and no executable file is produced:

qnx1 ug1$ cc reg_pci_adc.c -c

Type the program into file regs_adc.c and compile that using:

qnx1 ug1$ cc -T1 regs_adc.c reg_pci_adc.o -o regs_adc

Note that the reg_pci_adc.o file must be included to satisfy the external reference to the function in the program. Again the -T1 switch set the privity to the correct level so that the hardware of the card can be accessed directly by the program.

Finally run the program and check that the addresses listed conform to the specification in the PCI-ADC User Manual. Perform the arithmetic by hand using the base addresses listed by show_adc to ensure all is OK before proceeding to the next stage.

Task 3 completed on:   demonstrator:

6  Analogue input and output

Details about the analogue inputs of the PCI-ADC card are given in Chapter 3, page 6 of the PCI-ADC User Manual. Each analogue sample from the PCI data bus is 16 bits. The lower 12 bits are the sample value stored as a two's complement integer and the upper 4 bits represent the channel number, 0-15 single ended or 0-7 differential. We will be reading single ended samples from channels 0-4 using gain 1 which gives a range -5 to 5 V. 12 bits gives a total of 4096 values so the nominal voltage increment is 10/4096 V.

The channel, gain and input mode are set by writing to the Analogue Input Select register. Referring to the table on page 10 bits b1..b0 should be set to 0 (single ended), b3..b2 set to 0 (gain 1) and b7..b4 set to the channel number.

An A-to-D conversion is initiated either by software or a hardware trigger. We must select the the form of interrupt or disable interrupts by writing to the Interrupt Enable register. To disable interrupts we must write 0.

Each 16 bit sample is placed in a First-In, First-Out (FIFO) memory which is 1024 words deep. Before we initiate a conversion the FIFO should be cleared.

We can initiate a conversion from software by writing to the Analogue Conversion Control register. Referring to the table on page 8, bit b0 should be set to 0 (manual selection of a single channel), b1 set to 0 (edge triggered single conversion) and b4..b2 set to 001 (software trigger).

We can check the condition of the FIFO by reading the Analogue Input Status register. Referring to the table on page 11 we see that a value of 0 will indicate something is available. A value of 1 in the bit b0 indicates that conversion is still in progress and a value of 0 in bit b1 indicates the FIFO is not empty.

When samples are available in the FIFO we can read them from the Analogue Input Sample register.

6.1  Reading input data from the card

The addresses for the device registers returned in the structure ADC by the function reg_pci_adc are not within the normal memory address space but refer to a special 80x86 hardware port. The WATCOM C library provides functions to read and write from these addresses, outp(), outpd(), outpw(), inp(), inpd(), inpw().

The following program read_adc uses outp() to write to the PCI_ADC registers and inp() or inpw() to read from the PCI_ADC registers.

/* Program read_adc
   Read digitized analogue values from the PCI-ADC */
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <sys/pci.h>
#include "pciadc.h"

void main()
{
  int i,val,ichan;
  float ans,grad,offset;
  struct ADC_regs ADC;
                              
  grad=2.428e-3;
  offset=-0.2097;

  // get ADC register addresses
  if(reg_pci_adc(&ADC)!=EXIT_SUCCESS)
  {
    printf("Failed to get PCI-ADC registers\n");
    exit(0);
  }

  // Now set the Analogue input select register
  ichan=0;
  outp(ADC.Ana_Inp_Select_Reg,(ichan<<4)); 
  outp(ADC.Inter_Enable_Reg,0);
  // clear FIFO buffer
  while(((inp(ADC.Ana_Inp_Status_Reg) & 2) == 0))
    val = inpw(ADC.Ana_Inp_Sample);
  for (i=0;i<10;i++)
  { 
      // start the conversion
      outp(ADC.Ana_Conv_Cntrl_Reg,4);
      while ((inp(ADC.Ana_Inp_Status_Reg)) & 1)
      {
      // wait until the  ADC becomes 'not' busy
      }
      // print out the value!
      val = inpw(ADC.Ana_Inp_Sample);
        ichan=val>>12;
        val=val&0x0fff; 
      if (val>2047)
            val=val|0xfffff000;
      ans=(float)val*grad+offset;
      printf("channel %d, value = %d , %f volts\n",ichan,val,ans);
  }
}

Note that in the Analogue Input Select register the channel number is required in bits b7..b4. The expression ichan<<4 shifts the bits in ichan to the left 4 places to set these bits correctly.

There are two while loops which take the form:

    while(expression)
    {
          ..scope of while
    }

These are indefinite loops which will cycle through the code in the scope until the expression is false. The first such loop is used to clear the FIFO buffer.

  while(((inp(ADC.Ana_Inp_Status_Reg) & 2) == 0))
    val = inpw(ADC.Ana_Inp_Sample);

The function inp() reads the Analogue Input Status register. The operator & is a bitwise AND operation. We AND with 2 to pick out the bit b1. If this is not set the FIFO is not empty and we must read a value from it. This is done using the inpw() function to get a word (16 bits) from the Analogue Input Sample register.

The while loop within the for loop is used to wait for a conversion to be completed. The rest of the code inside the for loop unpacks the information in the analogue input sample. The channel number is held in the upper 4 bits so we use the right bit shift operator >> to extract them. The hexadecimal mask 0x0fff picks out the lower 12 bits.

        ichan=val>>12;
        val=val&0x0fff; 

Because the lower 12 bits are two's complement we must extend the bit pattern for negatives into a 32 bit integer using the bitwise inclusive OR operator |.

      if (val>2047)
            val=val|0xfffff000;

Finally we generate a floating point voltage value by scaling and offsetting by the calibration grad and offset.

      ans=(float)val*grad+offset;

You can now see why C is so well suited to low level manipulation of bits and bytes.

6.2  Task 4 - the read_adc program

Type the program into file read_adc.c and compile in the same way as for the regs_adc program.

You are now ready to read input voltage values from the PCI-ADC card. Before running the program connect up a DC voltage supply and digital voltmeter to an input channel. Take CARE not to exceed the range -5 to 5 volts. Values in excess of ±17 volts will DAMAGE the device!

The program should list the channel number, integer value and voltage value for each sample. Initially the voltage value will be inaccurate because the calibration values are only nominal.

Change the input voltage over the range -5 to 5 volts and work out the calibration constants by linear regression.

Type the constants into the source code, recompile the program and demonstrate that the calibration is good.

Estimate the final accuracy of the device over the range -5 to 5 volts.

Do this for at least two input channels.

Task 4 completed on:   demonstrator:

6.3  Task 5 - the write_adc program

The details of programming the PCI-ADC card for analogue outputs are given on page 14-15 of the PCI-ADC User Manual. Read these two pages carefully. Write a short program called write_adc that requests a voltage setting from the user and then sets an analogue output channel to a constant voltage setting. To save time copy the read_adc program into a new source file and edit it.

qnx1 ug1$ cp read_adc.c write_adc.c

A safe way of requesting a floating value is as follows:

  float ans;
  char line[100];

  // request analogue volts from user                              
  (void) printf("Enter a voltage between -10.0V and 10.0V\n");
  (void) fgets(line,sizeof(line),stdin);
  (void) sscanf(line,"%f",&ans);

The printf function lists a request prompt and fgets reads a character string from the standard input (stdin) into the character array line. The sizeof() operator returns the number of bytes of its argument. Note this is not necessarily the same as the number of elements in an array although in this case it is since each element is a single character which occupies one byte. The function sscanf decodes the character array using a floating format "%f" and puts the result into the variable float ans.

Using the description on page 15 of the PCI-ADC User Manual you must convert the float ans to an integer value.

Then set the Analogue Output Control register using the function outp() and set the analogue output using outpw() on the appropriate output sample register.

Finally read back the value from the same register using inpw() to check it has been set correctly.

Compile the program and test it by connecting a digital voltmeter to the appropriate output channel. Test out at least two channels and note how accurate the output is in comparison with the requested value.

Task 5 completed on:   demonstrator:

6.4  Using the hardware timer

The PCI-ADC cards has three counter timers which can be used to set interrupts, initiate reads and writes and so on. Full details of 82C54 CHMOS Programmable Interval Timer used are provided on the data sheets for the device. As you will see it is a rather complicated integrated circuit. Counter 0 is permanently clocked by an on board 4 MHz oscillator. Counters 1 and 2 may be clocked from several sources as indicated on page 19 of the PCI-ADC User Manual.

The following snippet of code will set up the combination of counter 0 and counter 1 so that they count down once every second. Note that the counters are loaded by writing to the appropriate counter registers. The format required is descibed in detail on page 5 of the Intel 82C54 data sheets.

  // first of all we need to assign the o/p of timer 0 to the input of timer 1
  outp(ADC.Counter_Cntrl_Reg,0x02);
  // program the two counter 0 and 1 to be divide by 40,000 and 100 respectively
  outp(ADC.Count_Time_Cntrl_Reg,0x34);
  outp(ADC.Count_Time_0_Cnt_Reg,0x40);
  outp(ADC.Count_Time_0_Cnt_Reg,0x9C);      // thats timer 0 set up
  outp(ADC.Count_Time_Cntrl_Reg,0x74);
  outp(ADC.Count_Time_1_Cnt_Reg,0x64);
  outp(ADC.Count_Time_1_Cnt_Reg,0x00);      //should be timer 1 set up

We want to use the counters to trigger an analogue input conversion. The following lines set this up.

  // Now set the Analogue input select register
  outp(ADC.Ana_Inp_Select_Reg,0);  // Ch 0, Gain of 1 and single ended
  // Set the Analogue Conversion Control Register
  outp(ADC.Ana_Conv_Cntrl_Reg,0x14);

6.5  Task 6 - the tick_adc program

Check the hexadecimal values used to load the registers in the previous section. What could the values be to produce a 2 second counter? How would you program the PCI-ADC to read each analogue input in turn using the same 1 second counting cycle?

Write a program tick_adc which produces an analogue input every second using the on board counters. The program should list the channel number and voltage read every second.

Test the program using a DC voltage supply connected to an input channel (0 say). If you slowly change the voltage supplied the program should list the voltage in sympathy with that change.

Task 6 completed on:   demonstrator:

6.6  Using hardware interrupts

Hardware interrupts are a key feature of most realtime applications. They are the mechanism by which the computer can response quickly (or VERY quickly) to events external or internal to the computer.

The program tick_adc is rather inefficient because it must spin on a while loop waiting for the next input sample to be produced. Most of the 1 second cycle time the machine is therefore effectively idle. A much more efficient approach is to use a hardware interrupt. The PCI-ADC can be programmed to generate a hardware interrupt when an input sample is available. During the wait the program that requires an input value is not running (asleep). When the sample becomes available a hardware interrupt is generated and the program is re-activated. The sample is read and listed by the program and then the program goes back to sleep again.

An interrupt service routine (ISR) is a piece of code which you supply that is responsible for clearing the source of the interrupt. Such routines run at the highest priority level and must do the minimum possible to avoid have a serious effect on all the scheduling in the operating system. In general an ISR must do something like the following:

The hardware device responsible for generating the interrupt will keep the interrupt line asserted until the ISR reads or writes to a specific address or register to indicate that the interrupt has been noticed and serviced.

In QNX processes communicate with each other using the QNX message system. For the case of an ISR a proxy (or messenger) is used so that the ISR is not blocked (held up) after the message is sent.

The ISR code runs with a special stack segment that the kernel of the operating system allocates for it rather than using the usual DS (data segment). Therefore the code must either be in its own .c module and compiled with special flags to change the stack settings or a special #pragma directive must be used to modify the stack checking as compilation proceeds. The latter is more convenient when writing the code because the ISR can then be included in the same source file that uses it.

Below are the first few line of the source file for a program which will use interrupts to read input samples.

/* Progam inter_adc 
   Read samples using interrupt set by timer on PCI-ADC */
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <sys/pci.h>
#include <sys/proxy.h>
#include <sys/kernel.h>
#include <sys/irqinfo.h>
#include "pciadc.h"

pid_t proxy;
int Interrupt_Enable;

#pragma off (check_stack)

pid_t far handler()
{
  //clear interrupt      
  outp(Interrupt_Enable,0); 
  //enable interrupt again
  outp(Interrupt_Enable,64);
  return(proxy);
}

#pragma on (check_stack);
  
void main()
{
...

The global variable proxy is a process ID (pid_t a type defined within the QNX operating system). The global variable Interrupt_Enable is the address of the Interrupt Enable register of the PCI-ADC. The #pragma lines switch the check_stack on and off. Notice that the ISR is declared as a "far" call which is standard for all interrupt handlers (and of little use elsewhere?). It also returns a process ID (type pid_t) which is the proxy. The return actually sends a message to say that the interrupt has been serviced. You can see that the interrupt has to be reset by writing decimal 64 to the Interrupt Enable register.

The main program must do three things to set up the ISR. It must initialize the register address, attach a proxy and attach the interrupt handler. The code will look something like:

...
  struct ADC_regs ADC;
...                              

  // get ADC register addresses
  if(reg__pci_adc(&ADC)!=EXIT_SUCCESS)
  {
    printf("Failed to get PCI-ADC registers\n");
    exit(0);
  }
  
  Interrupt_Enable = ADC.Inter_Enable_Reg;

  //Right lets set up an interrupt
  if (( proxy = qnx_proxy_attach(0,0,0,0)) == -1)
  {
    printf("Unable to attach Proxy \n");
    exit(0);
  }
  if ((id = qnx_hint_attach(ADC.Interrupt_Line,&handler,FP_SEG(&id))) == -1)
  {
    printf("Unable to attach interrupt \n");
    exit(0);
  }
...

The main program must also set up the counters to initiate conversions in the same way as the tick_adc program and then enable the interrupt on the PCI-ADC.

...
  //set interrupt
  outp(ADC.Inter_Enable_Reg,0);
  outp(ADC.Inter_Enable_Reg,64);

Finally we need a loop to receive the message from the proxy and respond to the interrupt in the main program.

  for(i=0;i<10;i++)
  {
    Receive(proxy,0,0);
    // print out the value!
    val = inpw(ADC.Ana_Inp_Sample);
    if (val>2047)
      val=val|0xfffff000;
    ans=(float)val*grad+offset;
    printf("%d %f \n",val,ans);
  }
...

6.7  Task 7 - the inter_adc program

Study the program snippets in the previous section carefully.

The key QNX C functions which do the work are qnx_proxy_attach(), qnx_hint_attach() and receive(). Look these up in the WATCOM C Library Manual.

Use the table on page 21 of the PCI-ADC User Manual to work out why the interrupt is enabled by writing decimal 64 to the Interrupt Enable register.

Copy the program tick_adc.c to inter_adc.c and modify the code to read on interrupt.

Compile and test the code in the same way as for the tick_adc program.

Task 7 completed on:   demonstrator:

6.8  Effective use of the FIFO memory

The program inter_adc doesn't make effective use of the FIFO memory. As soon as a value is available then an interrupt is generated. Looking at the table on page 21 of the PCI-ADC User Manual an interrupt can be set when the FIFO is half full. If we use this facility we should be able to read more efficiently and therefore at a faster rate from the PCI-ADC analogue inputs.

6.9  Task 8 - the fast_adc program

Work out the value required to be written to the Interrupt Enable register to set interrupt half full.

We want to run at 20kHz. This can be done using counter 0 alone. Work out the hexadecimal values required to load in the counter to operate at this frequency. You will also have to set the Analogue Conversion Control register to trigger on counter 0 correctly.

Modify the inter_adc program to create a fast_adc program which allows the FIFO to become half full before generating an interrupt. You should set up an array of integer values to accept the data read from the FIFO memory. The main loop in the program will look something like the following code. On each pass we wait for a message from the ISR and then pull values from the FIFO memory.

  int j,k,i,vals[4050];
...
  printf("start\n");
  j=0;
  for(i=0;i<5;i++)
  {
    Receive(proxy,0,0);
    k=j;
    while(((inp(ADC.Ana_Inp_Status_Reg)) & 2) == 0)
      vals[j++]=inpw(ADC.Ana_Inp_Sample);
    printf("grabbed %d \n",j-k);
  }
  printf("total %d \n",j);
...

To test this program properly we must use a time varying signal. If we input a square wave of frequency 1 kHz then we should get 20 samples per cycle. The following code analyses such a square wave read in from the PCI-ADC into the array vals[] above. Study the code to find out what it does and then use it to test your fast_adc program.

  int last,tick,tup,tdown,jump;
  float grad,offset;
...
  last=0;
  tick=0;
  tup=0;
  tdown=0;
  for(k=0;k<j;k++)
  {
      if(vals[k]>2047)
        vals[k]=vals[k]|0xfffff000;
      tick++;
      jump=vals[k]-last;
      if(jump>500)
      {
        printf("up   %d %d %d ",tick-tup,last,vals[k]);
        printf(" %f ",(float)last*grad+offset);
        printf(" %f \n",(float)vals[k]*grad+offset);
        tup=tick;
      }
      if(jump<-500)
      {
        printf("down %d %d %d ",tick-tdown,last,vals[k]);
        printf(" %f ",(float)last*grad+offset);
        printf(" %f \n",(float)vals[k]*grad+offset);
        tdown=tick;
      }
      last=vals[k];
  }

Try increasing the frequency to find out when the program starts to loose samples or fails in some other way.

Task 8 completed on:   demonstrator:

6.10  Task 9 - program sigen_adc

We can also use counter interrupts to drive the analogue output of the PCI-ADC. Hence we can operate the device as a signal or periodic wave form generator. Write a program sigen_adc that generates a square wave output with a frequency of 50 Hz.

Work out the values required for the counter 0 registers to give a transition rate of 100 Hz. Set the output to channel 0 and enable interrupt on counter 0. The main loop must wait on receive() for the interrupt and then alternately set the output high and low. Refer to program write_adc to remind yourself how to set analogue output values.

You can check the program is working by looking at the output channel with an oscilloscope. Is your program generating the correct frequency?

Task 9 completed on:   demonstrator:

7  Writing a QNX device driver for the PCI-ADC

In the previous section we talked to the PCI-ADC directly from a main() program and in each different application we repeated code to get the register addresses, program the mode we wanted the board to operate in and to read or write analogue values including handling interrupts. All this is rather complicated and device specific. It would be much more convenient to the applications programmer if all these details were wrapped up in some other program and a simple standard interface was available to talk to the device. Such a program is called a device driver. It is a program which is both operating system and device specific hiding the unecessary details of both.

In most operating systems device drivers are an integral part of the operating system kernel. They are installed when the system is booted and remain alive until the system is shut down. If any device driver falls over the system usually crashes and therefore developing such software is difficult and tedious. However QNX is specifically designed to make writing, testing and modifying device drivers easy. They can be started and stopped just like any other program and if they malfunction the micro kernel is protected so the system is unlikely to crash.

7.1  A description of the device driver

The PCI-ADC board is a complicated device and writing a device driver to exploit its full capabilities would be a rather large task. We shall implement a simple version which illustrates the basic principles and gives access to the major functions already tested in the previous section. We will define the following modes of operation for the board:

mode 0  read or write 1 data value on request (no interrupts)
mode 1  read on interrupt FIFO not empty, write 1 data value on request
mode 2  read on interrupt FIFO half full, write 1 data value on request
mode 3  read 1 data value on request, write on interrupt counter 0

An application program will communicate with the device driver using just five standard functions (four POSIX and one QNX specific):

open()       to open the device
qnx_ioctl()  to issue special control requests to the driver/device
read()       to read bytes (data values) from the device
write()      to write bytes (data values) to the device
close()      to close the device

These functions use the QNX message system to communicate with the driver. The qnx_ioctl() will send or receive a block of control bytes to or from the device. We need a structure to define how these bytes are to be used.

struct ADC_ctrl
{
        int mode;                // operating mode 0,1,2,3
        float freq;              // sampling frequency Hz
        int timer2;              // ticks at freq
        int rchan;               // input channel
        int wchan;               // output channel
        int insize;              // size of input buffer (data values)          
};

The sampling frequency will determine the input sample rate for modes 1 and 2 and the output sample rate for mode 3. The input or output channel for each mode will be rchan and wchan and the size of the input buffer can be chosen to suit the application speed and/or efficiency.

The device driver program must contain the following elements:

7.2  Task 10 - program skel_adc

We will start by writing a skeleton device driver for the PCI-ADC. Firstly we need to add some new features into the pciadc.h file as follows:

/* Details of Blue Chip Technology PCI-ADC card */
#ifndef pciadc_h
#define pciadc_h

#include <sys/types.h>
#include <sys/io_msg.h>

#define DEVICE_ID 0x0adc
#define VENDOR_ID 0x13c7

struct ADC_regs
{
      int      PIO_PortA_IO_Reg;
      int       PIO_PortB_IO_Reg;
      int      PIO_PortC_IO_Reg;
      int       PIO_Cntrl_Reg;
      int      Count_Time_0_Cnt_Reg;
      int      Count_Time_1_Cnt_Reg;
      int      Count_Time_2_Cnt_Reg;
      int       Count_Time_Cntrl_Reg;
      int      Counter_Cntrl_Reg;
      int      Inter_Enable_Reg;
      int      Inter_Status_Reg;
      int      Ana_OP_Cntrl_Reg;
      int      Ana_Conv_Cntrl_Reg;
      int      Ana_Inp_Select_Reg;
      int      Ana_Inp_Status_Reg;
      int      Ana_Inp_Sample;
      int       Ana_O_Sample_Ch0;
      int      Ana_O_Sample_Ch1;
      int      Ana_O_Sample_Ch2;
      int      Ana_O_Sample_Ch3;
        int     Interrupt_Line;
};

struct ADC_ctrl
{
        int mode;                // operating mode
        float freq;              // sampling frequency
        int timer2;              // ticks at freq
        int rchan;               // input channel
        int wchan;               // output channel
        int insize;              // size of input buffer (data values)          
};

struct ADC_qioctl
{
    msg_t       type;
    short int   fd,
                request,
                nbytes;
    long        zero;
    union
    {
      struct ADC_ctrl par;
      char        data[1];
    };
};

typedef union
{
      msg_t                               type;
      struct _io_open                     open;
      struct _io_close                    close;
      struct _io_read                     read;
      struct _io_write                    write;
      struct ADC_qioctl                   qioctl;
} io_msg;

typedef union
{
      msg_t                               status;
      struct _io_open_reply               open;
      struct _io_close_reply              close;
      struct _io_read_reply               read;
      struct _io_write_reply              write;
      struct _io_qioctl_reply             qioctl;
} io_reply;

#define ADC_SETP 1
#define ADC_GETP 2

extern int reg_pci_adc(struct ADC_regs *ADC);
extern int set_pci_adc(struct ADC_regs *ADC, struct ADC_ctrl *ctrl);

#endif

There are now two new structure declarations ADC_ctrl (as introduced above) and ADC_qioctl. The latter will be used for unpacking the messages sent and received by the function qnx_ioctl(). You will see that we are using a new form of declaration, a union. This is similar to a structure; however it defines a single location (address) which can be referred to using different names and have different associated types. So within the ADC_qioctl structure the location after zero is either par (of type ADC_ctrl) or data (of type char). The unions io_msg and io_reply allow unpacking of all the message types that are required by the PCI-ADC driver.

If a new variable of type io_msg or io_reply is defined then these variables can be used as a different type using the names declared in the structure definition.

...
  io_msg msg;           // message received
  io_reply reply;       // message reply
...
  if((nread=msg.read.nbytes/vsize)>ctrl.insize) // use read structure in msg
...
  reply.write.status=EBUSY; // use the write structure in reply
...

The macro definitions (using #define) for ADC_SETP and ADC_GETP are flag values which will be used in control messages to identify a get information or put information message.

Two functions are declared. We have already defined and used reg_pci_adc(). The other function set_pci_adc() will be dicussed below.

Edit your version of the header file pciadc.h to include the new entries.

The source code for the skeleton driver is given below.

/* Program skel_adc
   A skeleton device driver for the PCI-ADC */
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <errno.h>
#include <sys/pci.h>
#include <sys/kernel.h>
#include <sys/proxy.h>
#include "pciadc.h"

// these global variables are required by the interrupt handler
struct ADC_regs ADC; // PCI-ADC register addresses and interrupt line
pid_t proxy;         // pid of attached proxy for interrupt handling
int hintsrce;        // hardware interrupt source

// Interrupt handler
#pragma off (check_stack);
pid_t far handler(void)
{
  // get source of interrupt here
  if((hintsrce=inp(ADC.Inter_Status_Reg))==0)
    return(0);
  //clear interrupt
  outp(ADC.Inter_Enable_Reg,0);
  //enable interrupt again
  outp(ADC.Inter_Enable_Reg,hintsrce);
  // return to proxy
  return(proxy);
}
#pragma on (check_stack);

void main()
{
  char pathadc[]="/adc";// prefix (pathname) for PCI-ADC device
  int hintid;           // hardware interrupt id
  pid_t from;           // pid returned by Receive
  int devno;            // device number used
  io_msg msg;           // message received
  io_reply reply;       // message reply
  // further local variable declarations to follow here

  // create a child process and exit parent
  if(fork()>0)
    exit(EXIT_SUCCESS);

  // register a path name using qnx_device_attach() and
  // qnx_prefix_attach()

  // Attach proxy
  if (( proxy = qnx_proxy_attach(0,NULL,0,0)) == -1)
  {
    fprintf(stderr,"skel_adc: unable to attach proxy \n");
    exit(EXIT_FAILURE);
  }
  // get ADC register addresses
  if(reg_pci_adc(&ADC)!=EXIT_SUCCESS)
  {
    fprintf(stderr,"skel_adc: failed to get PCI-ADC register addresses\n");
    exit(EXIT_FAILURE);
  }
  // Attach interrupt handler
  if((hintid=qnx_hint_attach(ADC.Interrupt_Line,&handler,FP_SEG(&hintid)))==-1)
  {
    fprintf(stderr,"skel_adc: unable to attach interrupt \n");
    exit(EXIT_FAILURE);
  }

  printf("skel_adc: skeleton device driver for PCI-ADC\n");
  printf("skel_adc: device %d, prefix %s",devno,pathadc);
  printf(", proxy pid %d, interrupt id %d\n",proxy,hintid);

  // configuring ADC card to go here

  // allocation of read buffer to go here

  // loop for receiving/serving messages
  for(;;)
  {
    from=Receive(0,&msg,sizeof(msg));
    if(from==proxy)
    {
      // handle source of interrupt here
      printf("skel_adc: received message from proxy\n");
    } else
    {
      switch(msg.type)
      {
        // deal with other message types
        case _IO_OPEN:
          // no action required, just reply
          reply.status=EOK;
          Reply(from,&reply,sizeof(msg));
          break;
        default:
          // message not implemented
          printf("skel_adc: %s received message from pid %d, type %xh\n",
          pathadc,from,msg.type);
          reply.status=ENOSYS;
          Reply(from,&reply,sizeof(msg));
      }
    }
  }
}

There are a number of new features which are introduced in this program.

The interrupt handler is very similar to the one implemented before but now it checks the source of the interrupt by reading the Interrupt Status register. If this is zero then it immediately returns 0 to indicate there is no PCI-ADC interrupt to service. If it is not zero then it clears the interrupt and enables it again. See page 21 of the PCI-ADC User Manual for details of this. Finally it issues return(proxy) to send a message to the driver.

The first action in the main() program is to use fork(). This is not strictly necessary but a neat way of producing an independent process in the machine. The fork() function creates a new process (child) that is an exact copy of the calling process (parent). Upon successful completion the fork() function returns zero to the child and the ID of the child to the parent. So you can see in the above that the parent will exit while the child will continue.

We must use the functions qnx_device_attach() and qnx_prefix_attach() to get a device number and set up a prefix (name) for the PCI-ADC. The code lines for this are missing. Look up these functions in the WATCOM C Library Manual and implement the registration of a pathname for the device. Check the function returns and exit with a suitable error message on failure.

The code for configuring the device and setting up the buffers is missing. This will be added to the skeleton later.

The loop for receiving messages illustrates how the message system is used. The receive() function returns the process identification (PID) of the process that sent the message. If this is the proxy then we have an interrupt from the device. The code for working on this interrupt will be added later.

If the message is not from the proxy then it must be from some process (probably an application program) requesting action from the device driver. We look at the variable msg.typ in a switch statement. Control is passed to the case label that matches the expression in the switch(). The default picks up any values for which there is no match. Note that at the end of each case clause you must use break to jump out of the scope of the switch structure. If the break is absent then the next case will be executed as well. The expression in the switch must evaluate to an integer, character or enumeration.

The _IO_OPEN message type requires no action for the case of the PCI-ADC so we just send an EOK message using Reply(). You should look this function up in the WATCOM C Library Manual.

Type in the skel_adc program and compile it. If you run the program it should tell you what it is doing. The parent process will terminate but the child will still be running as a skeleton driver waiting for messages from the proxy or some other process.

Check that the process is running using:

qnx1 ug1$ ps | grep adc

Alernatively you can check the process, its proxy and interrupt using the sin (system information) utility from within the Photon desktop:

qnx1 ug1$ ph

Click on utilities and start up sin.

You can try and talk to the driver using system utilities. For example try

qnx1 ug1$ ls /adc

To stop the skeleton driver use the slay command:

qnx1 ug1$ slay skel_adc

Task 10 completed on:   demonstrator:

7.3  Task 11 - a function to configure the PCI-ADC

The function to configure the PCI-ADC was declared in the pciadc.h file as:

extern int set_pci_adc(struct ADC_regs *ADC, struct ADC_ctrl *ctrl);

Your task is to write this function. The register address are available in the ADC_regs structure (as used before) and the mode parameters are available in the structure ADC_ctrl as defined above. Your function must do the following:

Type your code into the file set_pci_adc.c and compile it with the -c option to create an object file.

You can test the completed routine by calling it in the skeleton driver. You will need to declare a variable of type ADC_ctrl and set the various components in this structure to sensible values.

Compile the new version of the skeleton driver with

qnx1 ug1$ cc -T1 skel_adc.c reg_pci_adc.o set_pci_adc.o -o skel_adc

If you set a mode which generates interrupts (i.e. not mode=0) then the proxy message from the interrupt handler should be picked up in the message receive loop. Are the interrupts coming at approximately the correct rate? Is the correct source for the interrupt being returned in the variable hintsrce?

Task 11 completed on:   demonstrator:

7.4  Messages and buffering in the device driver

The setting up stage of the device driver is now complete. The remaining task is to construct a buffering scheme and work out how to do the message handling.

The message loop serves two purposes; servicing proxy messages indicating an interrupt has occured and handling messages from other processes which want to control, read or write. To some degree these conflict and we must be careful that the performance of the device is not compromised.

7.4.1  Immediate read or write of one data value

The handling of immediate (not controlled by counter and/or interrupt) read or write requests with one data value is reasonably simple. For a read we get the value from the appropriate device input, pack it into the message buffer and reply immediately to the process that requested the value. For a write we unpack the data value from the message, write the value to the appropriate output and reply immediately to the process that requested the write. Note that it is not sensible to request a read or write of more than one value without counter or interrupt control.

7.4.2  Request for read of more than one value

Suppose a request comes in for a read of a large number of data values from input and such values are being produced by the device at a low frequency and only a few might be currently available in the buffers. We must service the read request quickly so that further interrupts are not missed or further messages do not pile up. Such a read request must indicate or set a read state and then allow the loop to continue processing.

When the interrupt proxy indicates that input values are available in the input FIFO these values must be buffered as quickly as possible into memory within the driver. After this, if a read state is set and there are sufficient values in the buffer to satisfy the read request then we can transfer the values into the message buffer, reply to the read request and cancel the read state.

7.4.3  Request for write of more than one value

Suppose a request comes in for write of a large number of data values on interrupt (as in program sigen_adc). The values must be transfered into a write buffer in the driver, we must set a write state and we must allow the loop to continue processing.

If the proxy indicates an interrupt which requires an output value and the write buffer is still not empty then we can write a value to the PCI-ADC. If this results in an empty write buffer then we have completed the write request. A message must be sent to the process which requested the write and we can cancel the write state.

7.4.4  Control messages

The control parameters are held in a structure of type ADC_ctrl. We shall implement just two control messages; a request to get the current parameter values in the driver and a request to set new values of the parameters and use them to set a new mode in the PCI-ADC card. Since the control parameters include the size of the input buffer we may have to free memory from the current buffer and allocate new memory of the appropriate size.

7.5  Task 12 - implementation of POSIX read

In an application program a POSIX read will look something like the following:

  int fd,nbreq,nbytes,vsize;
  short int ibuf[1000];
...
  if ((fd = open("/adc", O_RDWR)) == -1)
  {
    printf("test_adc: failed to open device /adc\n");
    exit(EXIT_FAILURE);
  }
  printf("test_adc: device /adc open\n");
...
  vsize=2;
  nwreq=10;
  nbreq=nwreq*vsize;
  nbytes = read(fd, ibuf, nbreq);
...

The open() function returns a file descriptor number (an integer like a channel number in Fortran) in the same way as it would for a file on disc. The access flag O_RDWR provides read and write on the device.

The POSIX read() function requests nbreq bytes to be returned in the buffer ibuf. The actual number of bytes returned is the function return value nbytes. For the PCI_ADC each data value is 2 bytes (vsize) and we are requesting 10 data values. The POSIX read will use the QNX message system to get the required data. The /adc device driver to which the /adc prefix is attached will receive a message of type _IO_READ.

The switch case structure within the driver will pick this request up:

  pid_t from;           // pid returned by Receive
  io_msg msg;           // message received
  io_reply reply;       // message reply
  int nread;            // number of values to be read from ADC
  pid_t toread;         // pid of read request
...

  from=Receive(0,&msg,sizeof(msg));
...
    switch(msg.type)
    {
...
       case _IO_READ:
         if(nread>0)
         {
            // a read is already current
            reply.read.status=EBUSY;
            reply.read.zero=0;
            reply.read.nbytes=0;
            Reply(from, &reply.read, sizeof(reply.read));
            break;
         }
       // check number of data values requested
       if((nread=msg.read.nbytes/vsize)>ctrl.insize)
            nread=ctrl.insize;
...
       toread=from;
...

The integer nread will be used to indicate the current read state. If greater than zero then a read is already in progress therefore we return an EBUSY status to the application. Note how the reply message is constructed by assigning values to the reply.read structure which has type io_reply. Check where this was declared in the header file pciadc.h. Look up the Reply() function in the WATCOM Library Manual.

If a read state is in progress then we must save the PID of the process that requested that read so that when it is complete we can reply to the correct application. This is the purpose of the toread=from assignment.

If nread is zero then we can start a new read. Firstly we check that the input buffer is large enough for the number of bytes requested. If the requested number is too large we limit it to the buffer size. The buffer must be allocated after the PCI_ADC device has been set up and before the start of the message loop. This is done using the ubiquitous malloc() function which you must look up in the WATCOM Library Manual.

  short int* inbuf;     // buffer for read data values
  int inread;           // read buffer index

...
  // allocate read buffer
  vsize=2;
  inbuf =(short int*) malloc(ctrl.insize*vsize);
  if(inbuf==0)
  {
    fprintf(stderr,"drive_adc: unable to allocated input buffer\n");
    exit(EXIT_FAILURE);
   }
   printf("drive_adc: insize %d\n",ctrl.insize);
   // set buffer indices/counters
   nread=0;
   inread=0;
...
   // loop for receiving/serving messages
   for(;;)
   {
...

Copy the skel_adc.c program to a new drive_adc.c file. Modify the new source file to handle the _IO_READ message as indicated above. Note that you should change all the occurences of skel_adc to drive_adc in printf() etc.. Write a program test_adc.c which opens the device and issues a POSIX read. Check that the driver is picking up the request with correct number of bytes. Note that you don't need the -T1 option to compile the test_adc.c program because all the direct contact with the hardware is done by the driver program. Remember, each time you modify the drive_adc program you will have to slay any current version which is running and restart with the new version.

Having established that the read message is getting to the driver all that remains to be done is to perform the transfer of data values from the ADC to the application. The behaviour will depend on the number of values requested and the current mode that the ADC is set to. Here is the sort of code required under case _IO_READ.

          toread=from;
          if((ctrl.mode==0)||(ctrl.mode==3))
          {
            if(nread==1)
            {
            //read is immediate, start conversion and wait for value
            // put value in inbuf[0]
...
              offset=sizeof(reply.read)-sizeof(reply.read.data);
            Writemsg(toread,offset,&inbuf[0],nread*vsize);
            reply.read.status=EOK;
            } else
          {
            // too many data values for read mode set
              // error message and set nread=0
          } 
            reply.read.zero=0;
          reply.read.nbytes=nread*vsize;
          Reply(toread, &reply.read, offset);
          nread=0;
        }
...

For modes 0 and 3 an immediate read of one value is OK. The function Writemsg() is used to enter the data into the message buffer. Note how the position for the data in the buffer is set by offset. Look up Writemsg() in the WATCOM Library Manual.

Modify your code to handle a request for an immediate read of one data value. Check that the correct value is returned to the application test_adc. Don't forget to include an error message if more than one read value is requested in mode 0 or 3 and check that such rogue requests are trapped correctly.

If the mode is 1 or 2 then reads are interrupt driven and more than one value can be requested. In your present program these requests should just set the integer nread to the number of values and toread to the PID of the application making the request. We must now service these reads by modifying the interrupt handling section of the main message loop. The global variable hintsrc will control the behaviour of the interrupt handling:

    from=Receive(0,&msg,sizeof(msg));
    if(from==proxy)
    {
      if(hintsrce>=64)
      {
        // read data values from FIFO
        while(((inp(ADC.Ana_Inp_Status_Reg)) & 2) == 0)
        {
          inbuf[inread++]=inpw(ADC.Ana_Inp_Sample);
          if(inread==ctrl.insize)
          {
             inread=0;
          }
      }
        if(nread>0&&inread>=nread)
        {
        // current read completed
        offset=sizeof(reply.read)-sizeof(reply.read.data);
        Writemsg(toread,offset,&inbuf[0],nread*vsize);
        reply.read.status=EOK;
        reply.read.zero=0;
        reply.read.nbytes=nread*vsize;
        Reply(toread, &reply.read, offset);
        // shift remaining values to front of buffer
        for(i=nread;i<inread;i++)
           inbuf[i-nread]=inbuf[i];
        inread=i-nread;
        nread=0;
      }
      }
...
    }
        

Why do we check for a value >=64? The integer inread controls the position in the input buffer for the next value read from the ADC board. If we flow past the end of the buffer we just start overwriting at the beginning again. This is OK since we have limited the number of values requested in any one read to the size of the buffer.

If a read is in progress (nread>0) and we have sufficient values in the input buffer then we are ready to reply to the read request. Once again we transfer values into the message data buffer and reply to the request.

Finally we shift any remaining values to the front of the buffer so that they can be picked up by a subsequent read request if it is made quickly enough. Note if the delay is too large the buffer will overflow and the inread index will shift back to the beginning again overwriting data values.

Modify your version of the program drive_adc.c to implement the above interrupt handling for read requests. Check it is working for modes 1 and 2 using a modified version of test_adc.c.

Your driver program should now be able to handle all forms of read request from application programs. However at the moment it will only be able to service one mode at a time. If you want to change the mode you must edit, recompile and restart the driver. Test your driver in the same way as you tested the read_adc and fast_adc programs.

Task 12 completed on:   demonstrator:

7.6  Task 13 - implementation of POSIX write

The POSIX write function used in an application program is very similar to the read:

  short int wbuf[1000];
  int vsize,nwreq;
... 
  vsize=2;
  nwreq=10;
  nbreq=nwreq*vsize;
  nbytes = write(fd, wbuf, nbreq);
...

The returned values nbytes is the number of bytes successfully written to the device. Further details can be found in the WATCOM Library Manual. The message type from a POSIX write request received by the driver is _IO_WRITE, as you might have guessed.

In a similar way to read, we will use an integer nwrite to indicate a current write is in progress with a PID towrite to indicate the process that requested the write.

Unlike the read case we only need to allocate the write buffer when a write request is made. Having allocated the buffer we should immediately read the message from the application to extract the data values that must be written to the device. The snippet of code to do this looks like:

  short int* wrbuf;     // buffer for write data values
  int vsize=2;          // number of bytes per data value
  pid_t towrite;        // pid of write request
  int offset;           // offset in read/write client buffer
  int wrsize;           // size of write buffer in bytes
...
     towrite=from;
     // create new write buffer
     if(wrsize>0)
       free(wrbuf);
     wrsize=nwrite*vsize;
     wrbuf =(short int*) malloc(wrsize);
     if(wrbuf==0)
     {
       fprintf(stderr,"drive_adc: unable to allocated write buffer\n");
       exit(EXIT_FAILURE);
     }
     // get data values
     offset=sizeof(msg.write)-sizeof(msg.write.data);
     Readmsg(towrite,offset,&wrbuf[0],wrsize);

We must check that a write buffer has not already been allocated by a previous write and use the function free() to release the allocated memory if it has. In general any malloc() should have an accompanying free() to avoid excessive or unecessay memory allocation. The data is read from the message buffer into the write buffer by Readmsg().

For modes 0,1 or 2 and a single data value we can perform the write request immediately. A separate register is used for each output channel so we must use a switch case structure:

    switch(ctrl.wchan)
    {
      case 0:
        outpw(ADC.Ana_O_Sample_Ch0,wrbuf[0]);
        break;
      case 1:
...
    }
    reply.write.status=EOK;
    // error message if too many data values for mode 0,1 or 2 write
...
    reply.write.nbytes=nwrite*vsize;
    Reply(towrite, &reply.write, sizeof(reply.write));                  

Note we set the number of bytes successfully written before replying to the write request message.

As for the read a write of more than one data value will be handled as an interrupt message. This time we look for the bit pattern corresponding to decimal 12 in hintsrce. Why is this?

      if((hintsrce&12)>0)
        // write data value
        if(nwrite>0)
        {
... 
        // write data value to device
        // check if current write is completed. If it is then reply to
      // application that requested write with the number of bytes
      // written

Modify your drive_adc.c program to implement POSIX write. Test it out for all modes by modifying the test_adc.c program (Keep the tests for the POSIX read requests in the test_adc program to check that both read and write requests are handled correctly for each mode.)

Task 13 completed on:   demonstrator:

7.7  Task 14 - implementation of control

An application will control the setting of the PCI_ADC using the qnx_ioctl() function. A typical snippet of code is:

  struct ADC_ctrl ctrl, rctrl;
...
  ctrl.mode=0;
  ctrl.freq=0.0;
  ctrl.timer2=0;
  ctrl.rchan=0;
  ctrl.wchan=0;
  ctrl.insize=3000;
  iret=qnx_ioctl(fd,ADC_SETP,&ctrl,sizeof(ctrl),&rctrl,sizeof(rctrl));
  iret=qnx_ioctl(fd,ADC_GETP,&ctrl,sizeof(ctrl),&rctrl,sizeof(rctrl));
  printf(
  "test_adc: set, mode %d, freq %f, timer2 %d, rchan %d, wchan %d, insize %d\n",
  rctrl.mode,rctrl.freq,rctrl.timer2,rctrl.rchan,ctrl.wchan,rctrl.insize);

The first call uses the ADC_SETP flag to set the mode using the parameters in the ctrl structure. The second call reads the value of these parameters from the driver into the rctrl structure.

The driver must look at the request flag to decide which case to service:

     case _IO_QIOCTL:
       switch(msg.qioctl.request)
       {
          case ADC_GETP:
            // return current parameter values
            offset=sizeof(reply.qioctl)-sizeof(reply.qioctl.data);
            Writemsg(from,offset,&ctrl,sizeof(ctrl));
            reply.qioctl.status=EOK;
            Reply(from, &reply.qioctl, offset);
            break;
...

For the ADC_SETP case the parameters are extracted from the message using a simple assignment:

           case ADC_SETP:
             // set new parameter values
             ctrl=msg.qioctl.par;

The driver must use the function set_pci_adc() to set the mode of the PCI_ADC. It must reallocate the input buffer using free() on the current buffer and malloc() to allocate a new buffer.

Finally we must set nread, nwrite and inread to zero and reply with reply.quioctl.status=EOK to the application.

Modify the drive_adc.c program to implement the _IO_QIOCTL message handling. Add a further case for the _IO_CLOSE message. This requires no action and should look just like the _IO_OPEN already implemented.

Task 14 completed on:   demonstrator:

7.8  Task 15 - testing all functions of driver program

You should now have a complete driver program called drive_adc. It should handle modes 0,1,2 and 3 for read, write and control requests. Modify the program test_adc so that it tests all the functions in a sequence. You must be confident that the driver is working correctly before you proceed to the next stage.

Task 15 completed on:   demonstrator:

7.9  The complete drive_adc program

The driver program neatly hides all the complications of device handling from the applications programmer. The version you have put together doesn't exploit the full capabilities of the PCI_ADC card but addition of further modes etc. would be reasonably straight forward.

The present program has been written as a main() function without resorting to a further layer of function calls. This main() function is rather long and if further modes etc. were added it would probably be a good idea to break up the code into another layer of function modules. For example you could use separate calls to perform the initial setting up, the proxy message handling and the application message handling. However it is good programming practice not to break down your programs into a very large number of small function modules. This can make the code difficult to understand and difficult to modify.

8  The Photon Application Builder

The Photon Application Builder is a visual development tool which enables you to generate C and/or C++ code to implement a Graphical User Interface for your application programs. All the code which creates and drives the windows, creates widgets like buttons and slide bars and handles events (things like button presses) is generated for you. You can produce a complete prototype interface without writing a single line of C or C++. All you have to do is write the code which performs the work of your application and the Photon Application Builder will complile and link this into the GUI.

8.1  Widgets, events and modules

A widget is a standard component of the user interface like a button, slidebar, label, menu, list etc. Each widget is able to draw itself, respond to events (for example a button press) and repair itself if it is damaged (for example when another covering window component is moved or closed). Some widgets are containers which can hold other widgets and manage their layout.

When you create a widget (a particular instance of a button for example) you can specify its geometry, its colour and its position with respect to a container. Using the Photon Application Builder most of this is done automatically or by selecting options on a menu using the mouse.

To construct a complete application you start with modules. These are the windows which you see in most Photon applications. There are several module types; window, dialog, menu, icon, picture.

Each module and widget has attributes or resources which you can set. The most important resources as far as you application is concerned are the initialisation function and the so-called callbacks. These are C functions which you can write or modify that perform some function associated with the module or widget.

So to build an application you:

8.2  Task 16 - creating a new application

You will find the icon for the application builder under dev. tools on the Photon desktop. Launch the PhAB by clicking on the icon. The PhAB window contains a Menu bar across the top (File Edit etc.), a Widget bar down the left hand side, a Speed bar just below the Menu bar (for rotating, arranging widgets etc.) and a main Work area.

You start a new application using the File option on the Menu bar. This will create a new PtWindow container widget on the left of the Work area and display a Control panel on the right of the Work area.

You give your application a name using the File option on the Menu bar. Choose Save As, click on the Application Name field and type a name. Press enter or click to save the application. Several critical operations require the application to have a name (and associated directories) so it's a good idea to do this at the start.

Use the search option in the Photon help facility to look for "hello". This will locate Tutorial 1 - Hello World. Follow this tutorial to create and run a simple (albeit useless!) application. In anticipation of what is to follow call the application app_adc.

Task 16 completed on:   demonstrator:

8.3  Task 17 - app_adc application

The app_adc will perform the following:

8.3.1  Header and Initialisation function

Set the control panel to point to the main application window. Click on Module Links and then click on the start CODE grey box. This will bring up a dialog box that allows you to set up the initialisation of the application. You need to specify and write two things. A header file which includes the pciadc.h and an initialisation function.

Set the header file name to app_adc.h and the function name to init.c. Click on the apply button and then on the edit symbol to the right of the header file name. This will get you into the editor with a prototype header file already present.

Edit the header file to include pciadc.h.

Now edit the init.c file. Again you will have a prototype function. Declare global variables to hold the file descriptor (int fd) and a character string (char textstring[100] say) to hold text messages. Within the scope of the function open the /adc device and construct a suitable text message using the sprintf function. The function sprintf() is like printf() except it writes to a character array rather than the standard output.

   nc=sprintf(textstring,"app_adc: device /adc open");

The number of characters written is return as nc which should also be declared as a global variable.

You can now exit from the initialisation dialog.

8.3.2  Window opening callback

You should already have a PtLabel widget from the "Hello World" application. Using the control panel change the name of this widget to "messages". This will be used to display the initial open success/failure message and subsequent grab success/failure messages. Also clear the label text.

With the control panel still set to the application window click on Callbacks. Then click on the Window Opening callback. Set the name of the callback to setmess or something similar and then edit the source file. Once again you will find you are given a prototype function to edit. This function will be executed when the window is opened. It should set the text in the "messages" PtLabel widget to the string already initialised in the init() function of the application. In formal terms the function will set the text string resource of the widget "messages".

To set the resources of a widget you must set a structure specifying the resources etc. and then pass this structure to the widget. The code looks like the following:

  PtArg_t arg;       // resource argument structure
...
  PtSetArg(&arg,Pt_ARG_TEXT_STRING,textstring,0);
  PtSetResources(ABW_messages,1,&arg);

In this case the name of the resource is Pt_ARG_TEXT_STRING which has the associated type of character array. Therefore textstring should be the global variable (a pointer) you specified to hold the message. Don't forget to declare the global variable in the source file setmess.c. The ABW_messages is the name of the target widget. The lower case portion must be unique within the application and is what you specified for the name of the PtLabel.

At this point you should generate, compile and run the application to check the initialisation works. Try using the application when the PCI_ADC driver is running and then again when it is not running to verify that the message that appears in the label is correct.

8.3.3  Using the PtNumericFloat widget to set/change the frequency

We will use a PtNumericFloat widget to display and allow setting of the sampling frequency (in mode 2 of the drive_adc program). Select the widget from the Widget bar and place it in the application window. If you can't find the widget then use Options from the Menu bar and select customize widget bar. Use the control panel to; set the name to "freq", the Numeric minimum, the Numeric maximum, the Numeric value and the Numeric increment.

Generate, make and run the application again. You will see that the widget has arrows which enable you to cycle up or down the specified range using the mouse. Alternatively you can click on the text and specify a value for the frequency by typing it in verbatim.

8.3.4  The RtTrend widget to display a time series graph

Select the RtTrend widget from the Widget bar and place it in the application window. Use the mouse to adjust the size of the widget frame so that it is suitable for displaying a time series graph. Use the control panel to change the name of the widget to something like "timegraph". We will be plotting mV as a function of time so set the Min value and Max value to -5000 and 5000.

8.3.5  Setting up the grab button

Select a PtButton widget from the Widget bar and place in in your application window. Use the control panel to change the name and label text to "grab".

Your last job is to program the button so that when pressed (by a mouse click) it will:

The unfamiliar bits of code are:

    extern char textline[100];     // global variable for messages
...
    struc ADC_ctrl;
    PtArg_t arg;
    double *dval;
    short int *rbuf;
    int nvals;
...
    // get frequency
    PtSetArg(&arg,Pt_ARG_NUMERIC_VALUE,0,0);
    PtGetResources(ABW_freq, 1,&arg);
    dval=arg.value;
    ctrl.freq=(float)*dval;
...
    // rbuf[] to contain nvals sample values in mV
...
    // set timegraph
    RtTrendChangeData(ABW_timegraph,rbuf,0,nvals);
...
    // final message
    nc=sprintf(textline,"app_adc: read complete, nsample %d, freq %f",
    nvals,ctrl.freq);
    PtSetArg(&arg,Pt_ARG_TEXT_STRING,textline,0);
    PtSetResources(ABW_messages,1,&arg);

Using the control panel set to the PtButton select Callbacks and click on Activate to create the callback function. Call the new routine grabit or something similar and edit the file so that it performs the above.

8.3.6  Testing the complete application

Generate, make and run the application. Use the signal generator to inject a suitable signal (amplitude, frequency and shape) into input channel 0 (or whichever channel you selected in the application). Select a suitable sampling frequency and then click on the "grab" button. You should get a time series graph plotted in the "timegraph" widget. Try changing the frequency, amplitude or pulse shape and grab again.

Go back to the Application Builder and try using the Speed bar (below the Menu bar to group the "grab" button "freq" widget and "messages" label into a neat arrangement. You may have to use the help facilities to work out how to do this!

Task 17 completed on:   demonstrator:

9  Writing a device handler for PGPLOT

The RtTrend widget provides a quick and easy way of producing graphs but the plotted points must be integers and there is no associated mapping or scaling available. Furthermore you can't add axes with numerical scales and tick marks or labels to the plot.

The Photon microGUI has primitive raw drawing facilities so that you can draw lines, fill areas, plot images etc.. However these facilities are primitive and don't include higher level plotting functions like annotation, logarithmic scaling, contour plotting, etc..

The PGPLOT subroutine library you used in the 2nd year Fortran Workshop provides all these higher level functions. Unfortunately QNX is not one of the operating systems that the normal distribution package of PGPLOT can run on. However QNX is very similar to UNIX/Linux: In particular the directory structure and the make and library utilities are almost identical. We ported a Fortran 77 compiler (called f2c because it actually converts F77 code into C) onto the QNX system and managed to compile the PGPLOT subroutine library. Fortunately PGPLOT also comes with a C function interface that enables the routines to be called directly from C. So the C interface to PGPLOT is available on your QNX system.

PGPLOT uses routines called device handlers to translate the high level plotting functions into device/system specific plotting instructions. Because QNX is not supported by PGPLOT there is no device handler for the Photon microGUI. So the next task is to write a simple PGPLOT device handler for Photon in C.

9.1  Task 18 - the device handler phdriv

A detailed description of the requirements of a PGPLOT device handler are given in the manual provided. As it says in the manual the best way to write a new device handler is to modify an existing one. You will find all the device handlers on directory /pgplot_source/drivers. We have taken one of the handlers written in C and produced a skeleton routine which includes all the non-QNX elements. The file is called phskel.c.

The operation of the handler is controlled by the integer argument ifunc in a switch case contruct. The following cases are already set up: 1,2,3,4,5,6 and 7.

For simplicity we will use a single instance of PtRaw as the plotting device. This will be made available to the handler in the global pointer variable PtWidget_t *pgplot_PtRaw. Note that if this pointer is NULL then the widget has not been assigned and many cases will not work. For example case 6 tests to see if pgplot_PtRaw is assigned. The functions used to perform plotting in a PtRaw widget are described in Chapter 16 of the Photon microGUI Programmer's Guide. A detailed description of each plotting function (prefixed by Pg) can be found in the Photon microGUI Library Reference. You will see we have used a variable PhRect_t raw_canvas. This is a structure defined within Photon which holds the size (in pixels) of the plotting space available in a PtRaw widget. The Photon type PgColor_t has been used to define the palette and current plotting/drawing colour and type PhPoint_t a set of polygon points.

The raw coordinates in PtRaw are pixels running from left to right and top to bottom. The positions of the corners are given in raw_canvas. The raw coordinates used by PGPLOT are also pixels but the origin (0,0) is at the bottom left of the canvas. When you convert from PGPLOT raw pixels (usually given in rbuf) remember to flip the y (vertical) direction.

The cases you are required to write are described in the following sections. Copy phskel.c to phdriv.c in your own directory space. Edit the source to create the complete device handler. You will need to read the section of the PGPLOT Manual provided to find the details about implementation of each case.

Check that the source file phdriv.c compiles (using the -c option of the compiler). When all is OK copy the source file to directory /pgplot_source/drivers. Change directory to /pgplot (note no longer the source directory). Login as superuser (if not already superuser) and type make:

qnx1_ug1$ make

This should compile the new device handler and put it into the object library.

In order to use PGPLOT the environment variable PGPLOT_DIR must be set:

qnx1_ug1$ export PGPLOT_DIR=/pgplot

It is a good idea to include this line in your .profile file (if it is not already there).

To test the phdriv function we need an application that uses PGPLOT. All the function calls must occur in the Draw Function of the PtRaw widget. You can define and edit this function using the Resources option of the Control Panel for PtRaw widget in your application window. Remember that the phdriv function gets the widget in the global variable PtWidget_t *pgplot_PtRaw so this must be declared and assigned in your Draw Function. The first argument of the Draw Function is the required widget and indeed it has the name widget so all we need to assign the global variable is:

pgplot_PtRaw=widget;

The C version of PGPLOT has a header file /pgplot/cpgplot.h. This should be included in the Draw Function source file:

#include "/pgplot/cpgplot.h"

We can steal code from the /pgplot_source/cpg/cpgdemo.c program to test out the device handler. The functions demo1, demo2, demo3 and demo4 exercise all the main plotting features in the device handler. You should only try the demo functions one at a time. The cpgend() call included in the demo should be commented out since this has the effect of clearing the plotting surface.

If you specify device /ph in the function cpgbeg() the plot should appear in the PtRaw widget within your application window. Whenever you partially cover and then re-expose the widget the Draw Function will re-draw the plot. If you specify device /ps then a postscript file pgplot.ps will be produced. However this device does require the cpgend() call. This file can be plotted on a postscript laser printer.

In order to link the application the object libraries containing the cpgplot routines must be searched. Such external libraries can be specified by editing the indOfiles in the "build + run" window within the Applications menu. This file should contain the line:

MYOBJ = -L /pgplot -lcpgplot -lpgplot -lf2c -lclib3r

The C functions of all the PGPLOT routines are prefixed by cpg, otherwise they are the same as the Fortran subroutines. You can examine the demo functions to see how they are used. Text documents describing the routines are /pgplot/pgplot.doc and /pgplot/cpgplot.doc.

On completion of Task 18 you should have a version of PGPLOT on your machine which includes a device handler for Photon.

Task 18 completed on:   demonstrator:

10  Experiments using the PCI-ADC

The two experiments described below exploit the nonlinear characteristics of the hysteresis loop of ferromagnetic materials, figure 1.

hyster.gif
Figure 1: Hysteresis of a ferromagnetic material

For a magnetic material there are two sources of magnetic induction B, currents of free charge and the magnetic dipole moment of the material. The magnetic field intensity H is defined as:


H=  B

m0
-M

where B is the magnetic induction and M is the magnetic dipole moment per unit volume. Thus Rearranging we have:


B=m0(H+M)

It is convenient to define a magnetic susceptibility such that:


M=cmH

Therefore we can rewrite the magnetic induction as:


B=m0(1+cm)H

The relative permeability is given by:


B=m0mrH

and is therefore related to the susceptibility:


mr=1+cm

In a ferromagnetic material the magnetic moment depends on the history of the magnetic field intensity and furthermore if this intensity gets very large the material starts to saturate as shown in figure 1.

The magnetic field intensity at the centre of a long solenoid is proportional to the current I in the coil:


H=N0I

where N0 is the number of turns per unit length. Therefore if the current is large enough and the core is ferromagnetic the magnetic induction is a nonlinear function of current.

If the magnetic induction passes through a loop the magnetic flux f is the integral of B.da over a surface bounded by the loop. Faraday's induction law states that the electromotive force (emf volts) given by the integral of the electric field E around the loop is minus the rate of change of magnetic flux through the loop:


ó
õ
E.dl = -  df

dt

If there are N turns of wire around the loop then the total emf will be:


emf=-N  df

dt

The rate of change of magnetic flux depends on some changing current. If the induced emf is seen in the same loop as the current loop generating the field we have self inductance (or simply inductance) L given by:


Nf = LI

Alternatively if the flux is generated by one loop and linked with another we have mutual inductance M given by:


Nf = MI

In both cases Nf is called the flux linkage. If the current varies then the flux linkage varies and we get an induced emf.


V(t)=-L  dI

dt

For non-magnetic materials the magnetic flux is proportional to the the magnetic field intensity which in turn is proportional to the solenoid current and L or M are constant. However in the case of ferromagnetic materials the magnetic flux is not proportional to solenoid current and therefore L or M are not constant. For an inductor (or transformer) with a ferromagnetic core the inductance depends on the current in some complicated way.

10.1  Task 19 - bifurcation sequences of a driven nonlinear oscillator

The basic circuit diagram is shown in figure 2.

lcr.gif
Figure 2: Driven LCR resonance circuit

If the inductance is constant and I(t)=I0exp(iwt) then the voltage across the inductor is V(t)=LiwI0 exp(iwt) and the impedence of the inductance is given by:


Z=  V(t)

I(t)
=iwL

The voltage is sinusoidal and leads the current by 90 degrees. The complete series LCR circuit has a resonant frequency w0=1/Ö(LC). At this frequency the current amplitude will be a maximum. If the inductance has a ferromagnetic core and the peak current is high then the core will saturate and the inductance will change as a function of time. This makes the response of the circuit much more complicated. The inductance provided has a secondary winding like a transformer. The primary has inductance L1 and resistance R1. The secondary has inductance L2 and the impedance of the secondary can be changed using a resistor R2 in series with this winding. If the secondary is open circuit then it has no effect. If the secondary is shorted by R1 then the impedence seen at the primary is given by:


Z=Z1+  w2M2

Z2

where M is the mutual inductance, Z1=R1+iwL1 and Z2=R2+iwL2. By changing R2 we can alter the inductance.

Set up the circuit using a 100mF capacitor and the inductance provided with the secondary shorted. An external resistance is not required since we are trying to run at maximum possible current. You must use the drive amplifier which takes a maximum input of 20V peak-to-peak from the signal generator and supplies upto 3V peak-to-peak with a high current of order 1 amp. The gain of the amplifier is controlled by a 0 to 10V DC input so that you can change the amplitude using a PCI-ADC output channel.

For low amplitudes the circuit should behave as a normal resonant circuit but as the driving amplitude is increased the waveform can become distorted. At a critical value the response will bifurcate. The amplitude will alternate between a high and low value and the period will be doubled. At a slightly higher driving amplitude bifurcation will occur again. As the driving amplitude is slowly increased a sequence of bifurcations occurs until the waveform becomes chaotic. Search for such behaviour in the frequency range 10-20kHz.

Your task is to study this bifurcation behaviour using the PCI-ADC. Write an application that can sample and analyse the voltage waveform across the LCR circuit as a function of driving amplitude. The application should do the following:

When the amplitude is low there should be just one peak value for that amplitude since the waveform is sinusoidal. After bifurcation two peak values will be found. As the amplitude is increased the number of different peak values will rapidly increase. If the waveform becomes chaotic then the sequence of peak values found will never repeat and the response will be aperiodic.

Task 19 completed on:   demonstrator:

10.2  Task 20 - demonstration of a flux gate magnetometer

Figure 3 shows the basic circuit used in a Vacquier, parallel-gated flux gate magnetometer. The two inductors are wired in series and wound such that the field in one is in the opposite direction to the other. Both inductors have a ferromagetic core and the applied waveform is of sufficiently high amplitude to push the magnetization towards saturation. Flux gate magnetometers are also called saturable-core magnetometers. A variation of the same principle introduced by Aschenbrenner and Goubau uses a continuous ring core in place of the two inductors. In this configuration one half of the ring is magnetized in the opposite direction to the other half.

mag.gif
Figure 3: Flux gate magnetometer

If the inductors are identical and there is no external field the induced emf in the detector coil will be zero because there is no net change in magnetic flux as a function of time. If an external field is introduced the ferromagnetic cores will be slightly biased and because of the nonlinear nature of the hysteresis the changing field in one core will no longer cancel the effect of the changing field in the other. The signal seen in the detector coil will contain harmonics of the fundamental drive frequency. Use the hysteresis loop plotted in figure 1 to sketch the expected induced emf in the detector coil, Vdet, if the drive voltage Vin is sinusoidal. A quatitative estimate is possible if the hysteresis loop is approximated by a trapesium (see Gordon and Brown 1972). Their detailed analysis of the flux gate mechanism indicates that the sensitivity increases linearly with the fundamental frequency w0. You should find an adequate response is achieved using n0 » 1kHz. The second harmonic (w = 2w0) can be extracted by filtering or by Fourier analysis and the amplitude calibrated against the external field.

Write an application to demonstrate the principle of the flux gate magnetometer. The inductor coils can be driven using the same amplifier as used in task 19. The voltage on the detector coil should be sampled using the PCI-ADC. A gain of 1 or 2 will be required because the induced emf is quite small. The application should calculate the amplitude of the 2nd harmonic. This could be done by performing a full Discrete Fourier Transform using a FFT algorithm. However since we only need the amplitude of one frequency this can be calculated directly. The complex amplitude is:


Aw=
å
j 
fj( cos(wtj)+isin(wtj))

where fj are the time series samples and tj=jDt is the time of the jth sample. Remember the w is the angular frequency (radians per second) required. The amplitude is then just Ö(AwAw*).

The application should:

The external magnetic field could be generated using Helmholz coils and a DC supply controlled using the PCI-ADC 0-10V output. Alternatively you could attempt to detect the Earth's magnetic field taking measurements at different rotations.

Task 20 completed on:   demonstrator:

11  Quick C Reference

11.1  Variables, types and declarations

The fundamental data types in C are:

char                a single character (usually 1 byte of 8 bits)
short int           usually 1 byte
int                 an integer usually 2 bytes
long int            a large integer usually 4 bytes
float               single precision floating point usually 4 bytes
double              double precision floating point
long double         high precision floating point

The integer types including char can be qualified by unsigned or signed to determine whether or not a sign bit is included.

unsigned int        usually 2 bytes giving range 0 to 65535
signed int          usually 2 bytes giving range -32768 to 32767
unsigned char       1 byte giving range 0 to 255
signed char         1 byte giving range -128 to 127

Derived types are created using the declaration operators:

*              pointer, a prefix operator
&              reference, a prefix operator
[]             array, a postfix operator
()             function, a postfix operator

Structures and unions:

struc typename
{
...
  contents of structure
  
  type name
  ...
}
typedef union
{
      msg_t                               type;
      struct _io_open                     open;
      struct _io_close                    close;
      struct _io_read                     read;
      struct _io_write                    write;
      struct ADC_qioctl                   qioctl;
} io_msg;

External and far declarations:

extern int reg_pci_adc(struct ADC_regs *ADC);// an external declaration
pid_t far handler() // declare an Interrupt Service Routine

11.2  Constants

Integer constants are written as:

9876           assumed type int
987654321L     type long
987654321l     type long
U9876          type unsigned int
u9876          type unsigned int

Integers can be expressed in octal or hexadecimal:

0123           leading zero indicate octal constant
0x134A         leading 0x (zero x) indicates hexadecimal

Floating point constants are written as:

4.321   type double
4.3e-5  type double
3.1f    type float
3.1e-1F type float
3.1l    type long double
3.1e-1L type long double

Character constants appear within single quotes. Special characters are specified using the escape characters (see Input and Output):

's'             the byte representing character s
'\a'            the byte representing bell
'\007'          bell specified using octal
'\x7'           bell specified using hexadecimal

A character string constant is enclosed in double quotes.

"This is a character string constant"

11.3  Reserved identifiers

There is a set of identifiers reserved for use as keywords in C and C++ and these must not be used otherwise.

asm    continue   float     new        signed    try
auto   default    for       operator   sizeof    typedef
break  delete     friend    private    static    union
case   do         goto      protected  struct    unsigned
catch  double     if        public     switch    virtual
char   else       inline    register   template  void
class  enum       int       return     this      volatile
const  extern     long      short      throw     while

11.4  Input and output

scanf("%d",&value); // scan (read) standard input for value
printf("value typed %d \n",value); // write to standard output
fgets(line,sizeof(line),stdin); // get character string from file stream
sscanf(line,"%f",&ans); // scan string for value
nc=sprintf(textline,"an integer %d",ival); // write to a character string

The complete set of format specifiers is:

%d        integer decimal notation
%o        integer unsigned octal notation
%x        integer unsigned hexadecimal notation
%u        integer unsigned decimal
%c        single character
%s        string of characters
%e        floating point (single or double) exponential notation
%f        floating point (single or double) decimal notation
%g        floating point as %e or %f, whichever is shorter

The full list of escape characters is:

\n        newline
\t        horizontal tab
\v        vertical tab
\b        backspace
\r        carriage return
\f        form feed
\a        alert or bell
\\        backslash
\?        question mark
\'        single quote
\"        double quote
\0        null
\ooo      octal number
\xhhh     hexadecimal number

11.5  Operators

C has a very rich set of operators. Here is a list of common operators in order of precedence.

[]             subscripting                 pointer[expr]
()             function call                expr(expr_list)
++             post/pre increment           lvalue++ or ++lvalue
~              complement                   ~expr
!              not                          !expr
-              unary minus                  -expr
+              unary plus                   +expr
&              address of                   &lvalue
*              indirection (dereference)    *expr
()             cast                         (type)expr
*              multiply                     expr*expr
/              divide                       expr/expr
%              modulo (remainder)           expr%expr
+              add (plus)                   expr+expr
-              subtract (minus)             expr-expr
<              less than                    expr<expr
<=             less than or equal           expr<=expr
>              greater than                 expr>expr
>=             greater than or equal        expr>=expr
==             equal                        expr==expr
!=             not equal                    expr!=expr
&              bitwise AND                  expr&expr
^              bitwise exclusive OR         expr^expr
|              bitwise inclusice OR         expr|expr
<<             left shift bits              expr<<shift
>>             right shift bits             expr>>shift
&&             logical AND                  expr&&expr
||             logical inclusice OR         expr||expr
?:             conditional expression       expr?expr:expr
=              simple assignment            lvalue=expr
,              comma (sequencing)           expr,expr

In this table lvalue is an entity which can appear on the left hand side of an assignment, typically a variable name. This may be a simple variable, an array element or a pointer. You should be careful that an lvalue is what you intend, a pointer or a primitive type. The type of both sides of an assignment should be the same.

If you look at some C and C++ code you will often see composite operators like += which means add and assign. These can be confusing and I suggest to begin with you avoid using these.

C++ has additional operators used for I/O and object-oriented syntax. Again, these can be confusing and I suggest you get used to C first before you try to master the complexities of C++.

11.6  Compiler directives

#include <sys/pci.h>      // include a system header file
#include "pciadc.h"       // include a local header file
#define DEVICE_ID 0x0adc  // define a replacement macro
#pragma off (check_stack) // modify stack handling for ISR
#pragma on (check_stack); // reset normal stack handling
#ifndef pciadc_h          // if macro not defined
#define pciadc_h          // define macro
... body of header file or whatever
#endif                    // jump to here if defined

Commonly used macro definitions in header files:

EXIT_SUCCESS   // function integer return OK
EXIT_FAILURE   // funciton integer return not OK

11.7  Conditional statement blocks

The basic conditional statement block has the form:

if(expression1)
          statement1;
else if (expression2)
          statement2;
else
          statement;

If the statements require more than one line you must use curly braces to gather together the scope of each conditional:

if(expression1)
{
          statement1a;
          statement1b;
          ...
}
else if (expression2)
{
          statement2a;
          statement2b;
          ...
}
else
{
          statementa;
          statementb;
          ...
}

In either case the statement or statements following the (expression) are executed if the value of the expression is true or non-zero.

11.8  The switch case construct

      switch(msg.type)
      {
        case A:
        statements for case A
        break;
      case B:
        statements for case b
        break;

11.9  Definite loops

A typical definite loop has the form:

        int a[10];
        int i;
        for(i=0;i<10;i++)
        {
                a[i]=i;
        }

11.10  Indefinite and infinite loops

Indefinite loops come in two forms:

while(expression)
{
        body of loop
}

do
{
        body of loop
}
while(expression)

In the second variant the body of the loop is executed before the expression is evaluated thus ensuring the body is executed at least once.

An infinite loop can be set up using:

         for(;;)
         {
                 ...
                 if(finish loop expression)
                         break;
                 ...
         }

Such a loop should be terminated using break as shown or return. The break statement can be used to terminate any for(), while() or do structure.

11.11  Functions and header files

The main program function (returns void for WATCOM C compiler). The integer argc is the number of command line arguments which are passed in character string array argv. Note argv[0] is the name of the program as it occurs on the command line:

void main(int argc, char* argv[])

The WATCOM C library provides functions to read and write from 80x86 port addresses. These functions require the program to be linked with privity level 1 (using the -T1 option in cc). In each case the port is the hardware port address. The output functions return the value written to the port. The input functions return the value read from the port.

unsigned int  outp(int port,int value)           // write 1 byte
unsigned int  outpw(int port,unsigned int value) // write 2 bytes
unsigned long outpd(int port,unsigned long value)// write 4 bytes
unsigned int  inp(int port)                      // read 1 byte
unsigned int  inpw(int port)                     // read 2 bytes
unsigned long inpd(int port)                     // read 4 bytes

POSIX input and output functions:

fd = open("/adc", O_RDWR);        //open device, return integer
iret=qnx_ioctl(fd,ADC_SETP,&ctrl,sizeof(ctrl),&rctrl,sizeof(rctrl));
                                  // read/write control buffer
nbytes = read(fd, ibuf, nbreq);   // read bytes from device
nbytes = write(fd, wbuf, nbreq);  // write bytes to device
iret = close(fd);                 // close device

Create a child process:

iret = fork(); // return child PID to parent and zero to child:

The prototype declarations of all functions are held in header files. There are an enormous number of these files and an even larger number of protoytpe function declarations. The system wide header files are usually found at /usr/include on Unix and Unix-like systems. For example the floating point mathematics functions are declared in math.h.

To use any of the functions you must declare them by including the appropriate header file at the top of your source file:

#include <math.h>




File translated from TEX by TTH, version 3.01.
On 17 Sep 2004, 11:57.