You are on page 1of 132

Developing and managing Automatic Measurement Systems in C#

M. Parvis

This material created with the contribution of the students of the now defuncted II Faculty of Engineering Vercelli R.I.P.
...atque, ubi solitudinem faciunt, pacem appellant Tacitus

September 2010

Contents
1 Legalese 2 Forewords 3 Where can I nd how to ? 4 Getting started: my rst 4.1 Nomenclature- part I . 4.2 Create the program . . 4.3 Nomenclature- part II 4.4 Revision questions . . C# program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 5 8 10 10 11 17 19 20 20 24 25 25 26 27 29 31 33 33 34 35 40 42

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

5 Basic operations in C# useful in data acquisition programs 5.1 Formatting and parsing data . . . . . . . . . . . . . . . . . . . 5.2 Measuring time intervals . . . . . . . . . . . . . . . . . . . . . 5.3 Bulk data management and the Buer class . . . . . . . . . . 5.4 Working with les . . . . . . . . . . . . . . . . . . . . . . . . . 5.4.1 Reading and writing formatted data . . . . . . . . . . 5.4.2 Reading and writing binary data . . . . . . . . . . . . 5.4.3 Reading and writing bulk data . . . . . . . . . . . . . . 5.4.4 Transferring data between C# created les and Scilab or Matlab . . . . . . . . . . . . . . . . . . . . . . . . . 5.5 Synchronous message reporting and data request . . . . . . . . 5.6 Useful operations on arrays . . . . . . . . . . . . . . . . . . . 5.7 Errors and exceptions . . . . . . . . . . . . . . . . . . . . . . . 5.8 Dealing with threads . . . . . . . . . . . . . . . . . . . . . . . 5.9 Extending classes with new and override . . . . . . . . . . . . 5.10 Using delegates to deal with the cross-threading problem . . . 1

5.10.1 Using a delegate in the program . . . . 5.10.2 Using a control that is extended to deal gate approach . . . . . . . . . . . . . . 5.11 Revision questions . . . . . . . . . . . . . . .

. . . with . . . . . .

. . the . . . .

. . . . 43 dele. . . . 44 . . . . 47 49 49 55 61 64 69 70 70 71 79 82 83 83 85 87 89 92 92 95 97 100 101 101 102 103 108 110 112 115

6 Drawing a signal evolution 6.1 General considerations . . . . . . . . . . . . . . . . . . . . . . 6.2 Doing it in C#: the simple way . . . . . . . . . . . . . . . . . 6.3 Doing it in C#: using a double buer . . . . . . . . . . . . . . 6.4 Doing it in C#: handling the form resize and using thick pens 6.5 Revision questions . . . . . . . . . . . . . . . . . . . . . . . . 7 Using the Serial line 7.1 Generalities . . . . . . . . . . . . . . . . . . . . . . . . . 7.2 Using the serial line taking advantage of the IDE toolbox 7.3 Manual serial line addition and cross thread solution . . 7.4 Revision questions . . . . . . . . . . . . . . . . . . . . .

. . . .

. . . .

. . . .

8 Using the National Instrument IEEE488 8.1 Simple data polling . . . . . . . . . . . . . . . . . . . . . . 8.2 Waiting for acquisition end without using the SRQ events . 8.3 Using the SRQ in event mode . . . . . . . . . . . . . . . . 8.4 Revision questions . . . . . . . . . . . . . . . . . . . . . . 9 Using the Hewlett Packard IEEE488 9.1 Simple data polling . . . . . . . . . . . . . . . . . . . . . . 9.2 Waiting for acquisition end without using the SRQ events . 9.3 Using the SRQ in event mode . . . . . . . . . . . . . . . . 9.4 Revision questions . . . . . . . . . . . . . . . . . . . . . . 10 Using the National Instrument DAQ Boards 10.1 Forewords . . . . . . . . . . . . . . . . . . . . 10.2 Generalities . . . . . . . . . . . . . . . . . . . 10.3 Data reading . . . . . . . . . . . . . . . . . . 10.4 Generating voltages . . . . . . . . . . . . . . . 10.5 Using digital inputs and outputs . . . . . . . . 10.6 Performing continuous acquisitions . . . . . . 10.7 Using DAQ boards in trigger mode . . . . . .

. . . .

. . . .

. . . .

. . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

10.8 Manual acquisition ne tuning . . . . . 10.8.1 Manually setting the acquisition 10.8.2 Manually setting the input gain 10.9 Revision questions . . . . . . . . . . .

. . . . timing . . . . . . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

116 117 118 118

11 Interface installation and useful links 120 11.1 National Instrument . . . . . . . . . . . . . . . . . . . . . . . 120 11.2 Agilent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 12 GPL Licence 123 12.1 Preamble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 12.2 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION . . . . . . . . . . . . . . . . . . . . . . . . . 124 12.3 How to Apply These Terms to Your New Programs . . . . . . 130

Chapter 1 Legalese
All the material of this primer is copyrighted by Politecnico di Torino and distributed under the GPL licence The software here described has been developed with freely available tools Visual Studio express edition http://www.microsoft.com, Java and NetBeans http://java.sun.com, Scilab http://www.scilab.org, OpenOce http://www.openoffice.org This document written in Latex and converted into pdf by using the latex distribution MixTex available at http://www.miktex.org.

Chapter 2 Forewords
This is not a course on C#. If you wish to learn to program in C# because you wish to became a programmer you are not in the right place. This primer is designed to allow students of courses of master degree regarding Automatic Measurement Systems at Politecnico di Torino to get acquainted with general purpose methods for data acquisition and data view from instruments and data acquisition boards. Custom automatic measurements systems can basically be arranged in two ways: by using integrated environments such as Labview c , which hide most of the details of the communication with the instruments by writing a program in one of the available languages (C, C++, C#, Java, Visual Basic, Pyton,...) taking care of all the details of the communication and of the instrument set-up The rst approach at rst seems more attractive since you can arrange a complete data acquisition system with little or no eort, however it requires you (or your company) to invest a not negligible amount of money buying the environment and often requires you to pay royalties if you want to distribute your programs to your clients. Learning to arrange acquisition systems by using this approach might be a reasonable solution for a bachelor student who does not have the goal of being capable of developing new and tricky applications. The second approach is more complex, requires you to study a programming language, to understand the instrument communication protocols and 5

several programming issues, but gives you a tremendous exibility and the possibility to arrange sophisticated solutions. In addition you (or your company) do not have to invest large amounts of money in the development software and you are not bothered with royalty issues. This is a reasonable way to learn for a master student whose goal is to be capable of developing complex systems and to provide innovation. Once you learned developing a system in this way, the downgrade to the integrated environment is somehow trivial. At this point the question is: which programming language? The correct answer to this question is... any language: once you understand the instrument protocol and its programming issues, most of the eort is done; the actual coding is a small part of the job! Moreover all the object-oriented languages are similar (from the point of view of using them to arrange a measurement system): once you learned to code in a language, moving to another language is a matter of syntax and few other details. So why C#? The answer is that probably the best solution would be Java, which is fully portable among dierent platforms, however most of data acquisition boards and instrument interfaces lack drivers designed for Java. This is probably the price you have to pay for the Java characteristic to be machine independent, but in our case this makes things a bit more complicated. If we had time to develop a more structured work, we could use a solution which gives quite interesting results i.e. we could develop a simple program probably written in C (or C++) which is capable of interfacing the instrument and which does not have a graphic interface, but opens a network socket to interchange data with other either local o remote programs. Then we could use Java to design the graphic interface and connect via socket to our C daemon program. This type of dual-layer solution is widely used in the measurement system arena, but is complex to arrange. C# instead (at least in the Windows c environment) can directly use the driver provided by the interface manufacturers and has a syntax almost equal to Java so that, if you decided to move your code to Java, you might do this without problems. This tutorial comprises a set of C# solutions MyFirstApplication and MySecondApplication: the starting point DrawExample, DrawExampleBuer, and DrawExampleBuerResize: how to draw graphics 6

ParsingAndFormatting and WorkingWithFiles: basic operation in C# BackgroundThread: advanced operations in C# SerialLineUse.Csharp and SerialLineUseManual: how to use the RS232 line NI488.Csharp, NI488-SrqPolling.CSharp, NI488-SrqEvent: how to use a National Instrument IEEE488 HP-IEEE488.BasicRead.Csharp, HP-IEEE488-SrqPolling.CSharp, HPIEEE488-SrqEvent: how to use an Agilent IEEE488 NIDAQ.Csharp: how to use a National Instrument Data Acquisition Board CallBackAcquisition: how to perform a continuous data acquisition If you wish to learn more on C# there are a lot of resources on the internet you can browse for free. Simply google C# tutorial. Among the courses (as for today) you can go to http://www.csharp-station.com http://www.ssw.uni-linz.ac.at/Teaching/Lectures/CSharp/Tutorial/ http://msdn.microsoft.com/en-us/library/aa288436.aspx http://www.java2s.com/Tutorial/CSharp/CatalogCSharp.htm BEWARE: the examples have been written for a specic C# version (i.e. Visual Studio 2008) and for the instrument and data acquisition drivers available at the time of writing. While the program basics remain the same as the software versions change, the exact syntax might change so be prepared to encounter and manage compiler warnings and errors.

Chapter 3 Where can I nd how to ?


Make a C# program Solve the missing reference error Block copy data from one array to another using a Buer Finding max min mean... of an array Format a number, parsing a string Measuring time intervals Using les to save and load data. Here you can also learn how to open data saved in C# by using Matlab and Scilab Having a message box to appear Manage and trap execution errors Use a background thread (advanced) Extend a class (advanced) Using delegates to deal with cross-threading (advanced) Draw the evolution of a signal Use a double buer to draw a big amount of data Draw the evolution of a signal with a tick pen 8

Create a form you can resize for better view Use the serial (RS232) line to interact with instruments Deal with the damned CrossThreadException Use a National Instrument IEEE488 interface Use an Agilent (formerly Hewlett Packard) IEEE488 interface Use a National Instrument DAQ board Fine tuning a DAQ acquisition Use a National Instrument DAQ board to perform a continuous acquisition(advanced) Install the drivers for NI and Agilent boards Revision questions to have an idea of my capabilities: at the end of each section you can nd some revision questions

Chapter 4 Getting started: my rst C# program


4.1 Nomenclature- part I

Before creating our rst program we need to speak the same meta-language so let us introduce some concepts. C# is an object-based language i.e. most of the things that compose a program are objects. An object is a thing that: belongs to a specic class of objects (in the code it is actually a class). Examples of classes of objects are Forms, Buttons, .... is identied by a unique name has to be created (and sometime destroyed). The creation is obtained with the keyword new; the C# usually takes care of destroying no longer required objects, however sometimes an explicit destruction is required by properly using the keyword dispose. has certain properties which can be altered during the object life. Examples of properties for graphic objects are size, color,.... is capable of doing certain operations. The supported operations depend on the object type. Examples of operations are capability of displaying a text or a geometric shape or an image and so on. The dierent operations are requested by invoking the object methods 10

is capable of reacting to certain events. Examples of events are click or double click with the mouse and so on. The reaction to a specic event depends on the object type, but also on the code you decide to write to handle the events you are interested in. Each object is therefore associated to three elements: properties, methods, events. The access (or invocation) of properties and methods is obtained by writing the property (method) name after the object name separating them with a dot: anObjectName.aPropertyName = newValue; anObjectName.aMethod(parameters); note that the property update makes use of the equals sign to assign a new value, while the method does not use it. The event use is more complex since we need to associate some code we write to the event. This is done with the syntax: anObjectName.aEvent += anObjectThatHandleEvents(aProcedure); where anObjectThatHandleEvents is a special wrapper usually provided by the system with the object itself while aProcedure is the procedure we have to write and that must conform some constraints. We will discuss these details later.

4.2

Create the program

Start the C# IDE so that you get the welcome screen (g. 4.1 Have a look at the IDE options to have it behaving as you like. I prefer using a multiple document interface so I changed this option (g 4.2 Since you likely open and close the IDE several times during out primer, I also suggest you change the start-up behavior asking the IDE to re-open the last project. Now we are ready to create our rst project. Go to le/new/Project and select Windows Form Application. Be sure to change the project name before going on since it is a bit more complicated to change it after the project has been created (g. 4.3. Do not worry about the project location 11

Figure 4.1: Welcome screen for the C# IDE

Figure 4.2: Option form for the C# IDE

since you will be asked later where you want to save your work. At this point you have a new project with its main Form As a rst thing, you are strongly advised to change the form name from Form1 to something dierent which remember you of the project purpose. To do this, go on the right panel, select the line which reads Form1 and right click selecting rename. Change the name and you will get a message saying that changing the name will also change the le name. This is exactly what we want since this way we will be able to easily identify our code (g. 4.4) You may also want to change the form caption (i.e. its text property) to something more interesting. To do this select the form in the main panel then in the right panel click the button properties so that the property window appears, look for the text property and change it (g. 4.4) Now it is time to populate our form: on the left you can see a button Toolbox, click it and a window appears expand the common controls, select 12

Figure 4.3: Create the project

a button and drag it onto the form, then select a a textbox and drag it onto the form (g. 4.5. Then, as usual, change the object names to buttonHello and textBoxHello and button text property to Hello! so that it reects its role. Also, for the text box, change the property Multiline to true so that you are not restricted to a single line box and so that you can change its vertical size At this point we are ready to write the code that when we press the button makes it to appear the word Hello!in the text box. Double click on the button and you will be taken to the code editor within the procedure buttonHello_Click then write the code: ....... private void buttonHello_Click(object sender, EventArgs e) { textBoxHello.Text = "Hello!"; } ....... The program is complete and you can run it: go to Debug and Start Debugging. You will see the message the program is being compiled, then your window 13

Figure 4.4: Renaming the base form and its associated le and accessing the form properties

appears and when you press the button, the word Hello! appears in the text box. The single line of code we have written is the rst use we have made of a property (actually the text property) of the object of type TextBox, however the code manages events (when we click the button..) and makes use of the TextBox we never created! How is it possible? The answer is very simple: the IDE takes care of object creation and link to events for us. The code that do this is in a a le automatically created by the IDE which has the name of the form followed by .Designer.cs. You see such le in the solution explorer and you can open (and edit..) it if you want. Unless you know what you are doing it is better you do not edit such le since you can easily mess it up preventing the graphic editor to work. Nevertheless let us what it contains: namespace MyFirstApplication { partial class FormHelloWorld { 14

Figure 4.5: Populating the form

/// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.IContainer components = null; /// /// /// /// <summary> Clean up any resources being used. </summary> <param name="disposing">true if ///managed resources should be disposed; /// otherwise, false.</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code

private System.Windows.Forms.Button buttonHello; 15

private System.Windows.Forms.TextBox textBoxHello; } }

The rst part simply contains creation and disposing code which for now is of no interest for us. Then you see a line saying #region Window.... we will discuss below and nally you see the global declaration of the object we used: the Button and the TextBox. If we expand the region designer generated code we see the procedure InitializeComponent() which is the procedure that create the objects (see the keyword new), sets a lot of object properties and register the handler for the button click (see the line this.buttonHello.Click += new .... #region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.buttonHello = new System.Windows.Forms.Button(); this.textBoxHello = new System.Windows.Forms.TextBox(); this.SuspendLayout(); // // buttonHello // this.buttonHello.Location = new System.Drawing.Point(31, 23); this.buttonHello.Name = "buttonHello"; this.buttonHello.Size = new System.Drawing.Size(75, 23); this.buttonHello.TabIndex = 0; this.buttonHello.Text = "Hello!"; this.buttonHello.UseVisualStyleBackColor = true; this.buttonHello.Click += new System.EventHandler(this.buttonHello_Click); 16

// // textBoxHello // this.textBoxHello.Location = new System.Drawing.Point(31, 79); this.textBoxHello.Multiline = true; this.textBoxHello.Name = "textBoxHello"; this.textBoxHello.Size = new System.Drawing.Size(205, 46); this.textBoxHello.TabIndex = 1; // // FormHelloWorld // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(292, 178); this.Controls.Add(this.textBoxHello); this.Controls.Add(this.buttonHello); this.Name = "FormHelloWorld"; this.Text = "FormHelloWorld"; this.ResumeLayout(false); this.PerformLayout(); } #endregion private System.Windows.Forms.Button buttonHello; private System.Windows.Forms.TextBox textBoxHello; } }

4.3

Nomenclature- part II

To be able to understand the more complex programs we are going to realize we need to agree about the meaning of other keyword and concept. The most important thing to understand is the concept of thread. A thread is 17

an execution path i.e. a block of code within which only a statement can be executed at a time. The concept seems strange at rst, but in reality is quite simple. Let us start from the bottom making reference for simplicity to a computer with a single processor. Of course the microprocessor can execute one instruction at a time, but since the execution is extremely fast, the operating system is able to appear as if it concurrently executes dierent programs. This is the way you can run at the same time a word processor and listen music with your favorite player. One step more: within the same program, e.g. within the word processor you can write words and at the same time the word processor can print; again two operations: well these two operations can appear to be executed at the same time because they belong to dierent code paths i.e. to dierent threads. The code within the same thread cannot allows for the execution of more than a single operation. As an example, if we put the code that play music in the same thread with the code that print the two operations cannot be executed at the same time: either the music stops when you print or the printing does not start until you nish playing the music. This problem aects our programs because in C# all the operations which manage the graphic interface belongs to the same thread so that is an operation takes a lot to be executed, it blocksall the other operation. To see what this means let us to modify the previous example putting inside the button the following code: ........ textBoxResult.Text = "Starting operation\r\n"; DateTime start = DateTime.Now; long tk = start.Ticks; //ticks are increments of 100ns; DateTime stop=DateTime.Now; //this takes 5 s to be executed while((stop.Ticks-tk)<5e7){ stop=DateTime.Now; } textBoxResult.AppendText("Operation complete"); ....... As you see the code write a message in the text box, then performs a ve second operation and nally writes another message 18

Run the code: push the button and look: no message appears, but after 5s the massages appear together. It is worse than what you think: push the button again and cover the uncover the window with some other window programs: your window is not repainted! The reason of course is that the window painting is performed by code within the same thread that is executing the loop so that until you nish the loop no other operation takes place. Note that for this specic case there is a simple work around which is to add inside the loop the statement Application.DoEvents(); which instructs the program to interrupt the code, let the operating system see if something graphic has to be done; allow it to do graphics operations and eventually restart the code. Unfortunately this work around is not always useful and moreover it is dangerous: try running the code and push the button twice: as you see the behavior is a bit strange... The denite solution to this problem is to perform the long operation in a dierent thread, by explicitly creating a new thread. Using correctly the threading is not an easy task; some of hints will be presented later (see Use a background thread)

4.4

Revision questions

what is the dierence between methods and properties ? what is an event how it is managed? where all the details about the graphic interface are stored? how can be edited? what is the meaning of Application.DoEvents() and when could you need to use it?

19

Chapter 5 Basic operations in C# useful in data acquisition programs


This (long) chapter contains most of the basic things you need to write your data acquisition program. Use this chapter as a cookbook to develop your program.

5.1

Formatting and parsing data


The complete source code of this example is available in the ParsingAndFormatting package

One of the commonest task in writing programs for data acquisition and analysis is to parse values which arrives from the instruments in the form of strings to obtain numbers and to print results of data processing in forms and text boxes. While this should be a trivial task, problems arises due to the international settings of the computers. The common way to approach the parsing of a string is to use the Double.Parse (or Float.parse, ....) in the form: double aNumber=Double.Parse(aString); Unfortunately this solution is quite dangerous since it assumes that the symbol used to separate the integra part from the fractional part is the one dened in the so called Culture (i.e. international setting) of the computer on which your program is running (which can be dierent from the culture 20

of the computer on which you developed the software!). Basically there are two possibilities: the separator can be the dot for the English culture and the comma for the Italian culture. Everything would be right if all programs and instruments used the same convention, but this is not true!. If you write a program which get data from instruments following the IEEE488.2 specications the data comes with the dot as the separator therefore using the wrong convention (...the Italian one...) leads to catastrophic results. The solution is to avoid the simplied syntax shown above and to explicitly set the convention you want to use. To do this you need to import the denitions connected to the globalization by adding at top of your program the line; using System.Globalization; Then each time want to perform a data parsing you have to set a culture object and use it CultureInfo cEn = new CultureInfo("en-US"); double aNumber = Double.Parse(aString, cEn); A similar approach HAS to be followed when formatting data. Such an operation is obtained with the command: CultureInfo cEn = new CultureInfo("en-US"); String aString = String.Format(cEn, "{0:##.##}", tst);

The syntax is quite straightforward since for each parameter you want to format you have to add a format descriptor of type "{0:##.##}" where the number is the ordinal position of the parameter and the # are placeholder for digits. Please observe that the separator between integral and fractional part is the dot, while the separator used in the actual formatting is dened in the culture you are using. The following fragment of code shows some more examples and highlight how a wrong combination of culture and separator can lead to error situations.

21

private void buttonParse_Click(object sender, EventArgs e) { //define objects for English and Italian cultures CultureInfo cIta = new CultureInfo("it-IT"); CultureInfo cEn = new CultureInfo("en-US"); String baseString = "1.2345"; double tst = Double.Parse(baseString, cEn); String ss = String.Format(cEn, "{0:##.####}", tst); textBoxParse.AppendText("parsing "+baseString+ "with EN culture gives:" + ss + "\r\n"); //if you are with italian culture you get a strange result tst = Double.Parse(baseString, cIta); ss = String.Format(cEn, "{0:##.####}", tst); textBoxParse.AppendText("parsing "+baseString+ "with IT culture gives " + ss + "\r\n"); baseString = "1,2345"; try { tst = Double.Parse(baseString, cEn); ss = String.Format(cEn, "{0:##.####}", tst); textBoxParse.AppendText("parsing " + baseString + " with EN culture gives:" + ss + "\r\n"); } catch (FormatException ef) { textBoxParse.AppendText("parsing " + baseString + " with EN culture gives:" + ef.Message+ "\r\n"); } tst = Double.Parse(baseString, cIta); ss = String.Format(cEn, "{0:##.####}", tst); textBoxParse.AppendText("parsing " + baseString + " with IT culture gives " + ss + "\r\n"); } private void buttonFormat_Click(object sender, EventArgs e) 22

{ //define objects for English and Italian cultures CultureInfo cIta = new CultureInfo("it-IT"); CultureInfo cEn = new CultureInfo("en-US"); double tst = 1.0 / 3; String ss = String.Format(cEn, "{0:##.##}", tst); textBoxFormat.AppendText ("Formatting 1/3 with format {0:##.##} in EN culture gives " + ss + "\r\n"); ss = String.Format(cIta, "{0:##.##}", tst); textBoxFormat.AppendText ("Formatting 1/3 with format {0:##.##} in + ss + "\r\n");

IT culture gives "

ss = String.Format(cEn, "{0:#0.##}", tst); textBoxFormat.AppendText ("Formatting 1/3 with format {0:#0.##} in EN culture gives " + ss + "\r\n"); ss = String.Format(cIta, "{0:#0.##}", tst); textBoxFormat.AppendText ("Formatting 1/3 with format {0:#0.##} in + ss + "\r\n");

IT culture gives "

//with the culture you can change the //aspect of date and time representation cEn.DateTimeFormat.ShortDatePattern = "yyyy-mm-dd"; cEn.DateTimeFormat.ShortTimePattern = "HH:mm:ss"; DateTime dd = DateTime.Now; ss = String.Format(cEn, "{0:d}", dd); textBoxFormat.AppendText ("printing Custom date yyyy-mm-dd " + ss + "\r\n"); ss = String.Format(cEn, "{0:t}", dd); textBoxFormat.AppendText 23

("printing Custom time HH:mm:ss " + ss + "\r\n"); cEn.DateTimeFormat.ShortTimePattern = "hh:mm:ss"; ss = String.Format(cEn, "{0:t}", dd); textBoxFormat.AppendText ("printing Custom time hh:mm:ss " + ss + "\r\n");

} } The result of these settings is shown in Fig. 5.1

Figure 5.1: Result of dierent format and parsing options

5.2

Measuring time intervals

The measurement of time intervals, as an example to see how long an operation is, is quite simple and obtained by means of the DateTime object. The DateTime object contains methods to deal with time, dates and time and date intervals, plus the method Ticks that returns the number of hundred of nanoseconds since midnight January 1 year 1 and the method Now that returns the actual time. Using two variables and reporting the dierence is therefore an easy way to measure time intervals: DateTime startTime = DateTime.Now; 24

....... DateTime stopTime = DateTime.Now; TimeSpan elapsedTime = stopTime - startTime; this.textBoxTime.Text = String.Format("{0:000.000 ms}", elapsedTime.Ticks*1e-4);

5.3

Bulk data management and the Buer class

Sometimes you need to change the way a data block is interpreted i.e. you need to cast a value without changing its binary nature. This happens when you receive a raw stream of data (e.g. from a network socket, from an interface, reading a le,....) and you need to interpret the data as either integer or oating point number. The common way to receive the data stream is by means of an array of bytes and of course you can convert the bytes manually regardless of the encoding they have (provided that you know the encoding). However, if the encoding is a standard one (including the byte order i.e. the big-endian, little-endian issue), you can convert all the stream at once by using the Buffer static class. The Buffer can convert one-toone bytes into any other primitive type (int, long, oat double,...) without applying any types of processing. Even thought the class has other methods, the most important one is the BlockCopy that copies one array into another Buffer.BlockCopy( sourceArray, sourceOffset, destArray destOffset, byteCount); Be warned that the destination array must be allocated in advance before copying to it. For an example of Buffer use refer to Reading and writing bulk data in the following section

5.4

Working with les


The complete source code of this example is available in the WorkingWithFiles package 25

5.4.1

Reading and writing formatted data

Another common operation when writing software automatic test equipments is the reading and writing of data into les. Two types of les can be used: ASCII and binary. Ascii les can easily be read by other programs such as notepad, but are in general bigger and slower than binary les. On the contrary binary les a more compact and ecient but cannot be easily read by other programs since you need to know in advance how they have been written. In addition Ascii les can only be opened for writing o reading but not both and can only be read sequentially while such limitations do not apply to the binary les. In both cases you need to import the denitions of input output: using System.IO; Then the le is accessed through a FileStream object FileStream file = new FileStream(aFileName, FileMode.OpenOrCreate, FileAccess.Write); After this for the ascii le you have to use two objects of type StreamReader and StreamWriter while the binary le are accessed through BinaryWriter and BinaryReader This code fragment opens a le in ascii mode writes ten numbers and then read them back. Note that since the le is opened in ascii mode to read the values back it is necessary to close the le and open it again in read mode. private void buttonAsciiFile_Click(object sender, EventArgs e) { //beware: when working with the IDE the startup path is //the either sub dir bin/debug or bin/release String myPath = System.Windows.Forms.Application.StartupPath; //text file //open the file in write mode FileStream file = new FileStream(myPath + "\\test.txt", FileMode.OpenOrCreate, FileAccess.Write); StreamWriter sw = new StreamWriter(file); //create writer CultureInfo cEn = new CultureInfo("en-US"); 26

for (int i=0;i<10;i++){ String s=String.Format("{0:##.###}\r\n",i); sw.Write(s); } sw.Close(); //close writer file.Close(); //close file //re-open the file in read mode file = new FileStream(myPath + "\\test.txt", FileMode.Open, FileAccess.Read); StreamReader sr = new StreamReader(file); //create reader String sRead = sr.ReadToEnd(); //read textBoxRes.Text = "reading from test.txt\r\n"; textBoxRes.AppendText(sRead); sr.Close(); //close reader file.Close(); //close file }

5.4.2

Reading and writing binary data

This code fragment opens a le for contemporaneous write and read in binary mode. This time we do not need to close the le after writing and re-open it for reading, however when we read we must know what to read (integers in the rst part and doubles in the second part) private void buttonWriteBinary_Click(object sender, EventArgs e) { //beware: when working with the IDE the startup path is //the either sub dir bin/debug or bin/release String myPath = System.Windows.Forms.Application.StartupPath; //open the file for both read and write FileStream file = new FileStream(myPath + "\\test.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite); BinaryWriter swB = new BinaryWriter(file); //create writer BinaryReader srB = new BinaryReader(file); //create reader 27

//write 5 integers 32 bits for (int x = 0; x < 5; x++) swB.Write(x); //write 5 doubles for (int x = 0; x < 5; x++) { double d = x; swB.Write(d); } //rewind the file file.Seek(0, SeekOrigin.Begin); CultureInfo cEn = new CultureInfo("en-US"); textBoxRes.Text = "Read from test.bin file\r\n"; //read the 5 integer for (int x = 0; x < 5; x++) { int r = srB.ReadInt32(); textBoxRes.AppendText(String.Format(cEn,"{0:000 }\r\n", r)); } //read the 5 double for (int x = 0; x < 5; x++) { double d = srB.ReadDouble(); textBoxRes.AppendText(String.Format(cEn, "{0:000 }\r\n", d)); } swB.Close(); //close writer srB.Close(); //close reader file.Close(); //close file }

28

5.4.3

Reading and writing bulk data

This code fragment opens a le for contemporaneous write and read in binary mode as in the previous code, but in this case we use the Buffer class to speedup the operation. If you want to check the speed remember to run the program twice, since the rst time the system needs time to allocate the area on disk and this makes the comparison unfair. You may expect a speed improvement of a factor two or more when using the Buffer private void buttonBulk_Click(object sender, EventArgs e) { //bulk write //load data String myPath = System.Windows.Forms.Application.StartupPath; int nData = 10000000; double[] dd = new double[nData]; for (int i = 0; i < dd.Length; i++) { dd[i] = i; } FileStream file = new FileStream(myPath + "\\testBulk.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite); BinaryWriter swB = new BinaryWriter(file); //create writer BinaryReader srB = new BinaryReader(file); //create reader //monitor the time to perform the operation DateTime startTime = DateTime.Now; byte[] bb = new byte[dd.Length * 8]; //copy data into a byte array Buffer.BlockCopy(dd, 0, bb, 0, dd.Length * 8); //write the byte array swB.Write(bb); DateTime stopTime = DateTime.Now; TimeSpan elapsedTime = stopTime - startTime; this.textBoxRes.AppendText( String.Format( "{0:000.000 ms} to write {1:00000} double bulk\r\n", 29

elapsedTime.Ticks * 1e-4, nData)); //now bulk read //rewind file.Seek(0, SeekOrigin.Begin); startTime = DateTime.Now; bb = new byte[dd.Length * 8]; srB.Read(bb, 0, bb.Length); //copy data into double dd = new double[nData]; Buffer.BlockCopy(bb, 0, dd, 0, dd.Length * 8); stopTime = DateTime.Now; elapsedTime = stopTime - startTime; this.textBoxRes.AppendText( String.Format( "{0:000.000 ms} to read {1:00000} double bulk\r\n", elapsedTime.Ticks * 1e-4, nData));

swB.Close(); //close writer srB.Close(); //close reader file.Close(); //close file //now use the conventional way file = new FileStream(myPath + "\\testBulkConv.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite); swB = new BinaryWriter(file); //create writer srB = new BinaryReader(file); //create reader //use a buffer startTime = DateTime.Now; for (int i = 0; i < dd.Length; i++) swB.Write(dd[i]); stopTime = DateTime.Now; elapsedTime = stopTime - startTime; this.textBoxRes.AppendText( String.Format( "{0:000.000 ms} to write {1:00000} double with for\r\n", 30

elapsedTime.Ticks * 1e-4, nData)); //now read file.Seek(0, SeekOrigin.Begin); startTime = DateTime.Now; dd = new double[nData]; for (int i = 0; i < dd.Length; i++) dd[i]=srB.ReadDouble(); stopTime = DateTime.Now; elapsedTime = stopTime - startTime; this.textBoxRes.AppendText( String.Format("{0:000.000 ms} to read {1:00000} double with for\r\ elapsedTime.Ticks * 1e-4, nData)); swB.Close(); //close writer srB.Close(); //close reader file.Close(); //close file } }

5.4.4

Transferring data between C# created les and Scilab or Matlab

Files written in C# can be easily read in Scilab. The following code shows how to read both the binary and ascii les //scilab script to read a file written in c# clear //read the binary file [fd,ee]=mopen("bin/debug/test.bin",rb); intData=mget(5,i,fd); doubleData=mget(5,d,fd); mclose(fd); printf (\nBinary Read integer \n); for i=1:5 printf (%i ,intData(i)); 31

end printf (\nBinary Read double\n"); for i=1:5 printf (%e ,doubleData(i)); end info=fileinfo("bin/debug/test.txt"); filesize=info(1); [fd,ee]=mopen("bin/debug/test.txt",r); str=mgetstr(filesize,fd); mclose(fd); printf (\nAscii Read \n"); printf (%s\n",str);

A similar script can be used by people wishing to spend money and use Matlab %matlaqb script to read a file written in c# clear %read the binary file [fd,ee]=fopen(bin/debug/test.bin,rb); intData=fread(fd,5,int32); doubleData=fread(fd,5,double); fclose(fd); disp(sprintf (\nBinary Read integer \n)); for i=1:5 disp(sprintf (%i ,intData(i))); end disp(sprintf (\nBinary Read double\n)); for i=1:5 disp(sprintf (%e ,doubleData(i))); end [fd,ee]=fopen(bin/debug/test.txt,r); ch=fread(fd); 32

fclose(fd); str=setstr(ch); disp(sprintf (\nAscii Read \n)); disp(sprintf (%s\n,str));

5.5

Synchronous message reporting and data request

Sometimes you need to stop the program execution to communicate something to the user requiring his/her attention. This can be done by means of the so called MessageBoxes There are several variations of this object, but the basic avor is simply: MessageBox.Show("MyMessage"); When this statement is executed the program stops and a window appears until the user press OK. You can customize the box by changing icon, caption, and buttons. You can also use the message to get the user response among dierent buttons DialogResult r = MessageBox.Show("MyMessage", "MyCaption", MessageBoxButtons.AbortRetryIgnore); if (r==DialogResult.Abort) { .... } else if (r == DialogResult.Retry) { ... } Please note that in C# the is not an InputBox to request typed data from the user even though such method exists in VisualBasic.

5.6

Useful operations on arrays

Arrays in C# are objects that support some useful operation you can use to speed up the program development. Such operations operate on the entire 33

array to compute specic values (max, min, ...) or to copy the data to another array in a single operation The most important operations are: vector.Max(); vector.Min(); vector.Average(); vector.Sum(); while the copy operation is vector.CopyTo(newPreAllocatedVector); vector.CopyTo(newPreAllocatedVector,startPointInNewVector); note that both syntaxes require the destination vector to be preallocated with a size large enough to accommodate all the elements of the origin vector; this means that in the second syntax the destination vector must have at least the length of the original vector plus the required oset. The copy operation is allowed only on one-dimensional arrays.

5.7

Errors and exceptions

The exception managing in C# is obtained by using the the try, catch, finally structure. Please note that the best way to deal with abnormal situations is to prevent them by checking in advance for potential problems. However sometimes this is not enough so, each time you wonder an exception may occur while executing a statement, you may wrap the critical statement in a safer environment: //read values from channels try { //critical statement(s) readTask = new Task(); readTask.AIChannels.CreateVoltageChannel("Dev1/ai0:1", "", AITerminalConfiguration.Nrse, -10.0, 10.0, AIVoltageUnits.Volts); ....... } 34

catch (DAQException exception) { MessageBox.Show(exception.Message); } catch (IOException exception) { ...... } finally { readTask.Dispose(); } The code comprises the try{} section in which you put the code at risk; one or more catch{} sections where the execution proceeds in case of problems and, optionally, a finally section which is executed regardless of what happened before. In this last section you can perform operations such as object disposing, le closing,... you want to be executed anyway. The catch clause(s) allow you to take dierent actions depending of the type of problems you encountered. Unless you really know what you are doing, you are strongly advised not to leave an exception silent, at least report it with a message box. Note that try/catch blocks can be nested: if an exception is not caught (i.e. an exception type does not appear in a catch clause), it goes to the outer try/catch level; if no outer level exist, the exception crash your program.

5.8

Dealing with threads


The complete source code of this example is available in the BackgroundThread package

BEWARE When you use a thread and graphics objects (i.e. TextBoxes, Buttons,...) you may have to face the so called crossthreading problem. Refer to the following section to learn about this problem If you have a long operation to perform and you wish to be able to perform other tasks in the meantime with your program, you can use a 35

BackgroundWorker. The background worker is a component available in the toolbar, which provides the capability of spawning a separate thread still having the possibility to monitor it from your main program. Let get this component and call it backgroundWorkerLong. The background worker supports two events plus another special event. The two events are ProgressChanged, you can use to send feedbacks to your main program keeping it informed of the processing advance, and RunWorkerCompleted, which is raised when your long process ends. The special event is DoWork which is like a method called to start the process. It is in the form of event just because it is used to map your long procedure into the background worker space. Now let us to dene a long operation: int longWork(int interval, BackgroundWorker worker, DoWorkEventArgs e){ //perform a long operation..... for (long i = 0; i < Int32.MaxValue; i++) { ; } return 0; } If this operation were performed inside a button code it would block the interface unless we used the Application.DoEvents() command. However in this application we prefer to put the code inside the background worker. To do this we create the worker backgroundWorkerLong.WorkerReportsProgress = true; backgroundWorkerLong.WorkerSupportsCancellation = true; int interval=1000000; backgroundWorkerLong.RunWorkerAsync(interval); The two initial lines tell the worker you want to be able to cancel it and you want a feedback. This makes the thread a bit more heavy so the default values for these properties is false. The method .RunWorkerAsync is the key point which starts the work. Note that here you do not tell the system which work you want to do (no reference to out long procedure) however

36

you can pass a parameter of type object (in this example an int, see below for a more complex use of objects) to the worker. The place you specify which work you want the background to execute is the code of the DoWork event: private void backgroundWorkerLong_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker worker = (BackgroundWorker)sender; // The worker result goes into // DoWorkEventArgs result //perform the long operation..... int interval = (int)e.Argument; for (long i = 0; i < Int32.MaxValue; i++) { if (i % interval == 0) { if (worker.CancellationPending) { e.Cancel = true; e.Result = 0; return; } double percentComplete = ((double)i / (double)Int32.MaxValue * 100000000); worker.ReportProgress((int)percentComplete); } } e.Result = 0; }

as you can see every interval numbers a ReportProgress event is red and a check to see if a cancelation request is pending is made. Is such a case the ag of cancel is set at true and the work is interrupted (i.e. the procedure returns). 37

Please observe that the _DoWork procedure runs in the separate thread this allows the long process not to block the other processes, but as anticipated you CANNOT access graphic controls by this procedure since the controls were created in another thread. Please also observe that the _ProgressChanged and _RunWorkerCompleted procedure CAN access the controls since they are red by the background thread, but run in the main thread. Both the ReportProgress and the end of the procedure re an event: private void backgroundWorkerLong_ProgressChanged(object sender, ProgressChangedEventArgs e) { float percent = e.ProgressPercentage / 1000000; this.textBoxReport.Text = percent.ToString(); } private void backgroundWorkerLong_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { MessageBox.Show(e.Error.Message); } else if (e.Cancelled) { this.textBoxReport.Text = "Canceled"; } else { this.textBoxReport.Text = e.Result.ToString(); } } If you need to start the thread passing several parameters to it you have to create a single object that contains all the parameters. The usual way to do this is to use a class that contains everything .... 38

class multipleParams { public int interval = 0; public String greetings = "Hello"; } ..... ..... .... multipleParams p= new multipleParams(); p.interval = interval; p.greetings = "Go!"; backgroundWorkerLong.RunWorkerAsync(p); Of course inside the background procedure you need to decode the object

private void backgroundWorkerLong_DoWork(object sender, DoWorkEventArgs e) { ..... if (e.Argument is multipleParams) { multipleParams p = (multipleParams)e.Argument; interval = p.interval; } else //deal with other object types;

Eventually please note that as usual the use of the toolbox is only a convenience; should you prefer writing the code by hand you simply has to declare the working thread object and link the required procedures to it. Below the code which is automatically generate by the IDE. private System.ComponentModel.BackgroundWorker backgroundWorkerLong; 39

..... this.backgroundWorkerLong = new System.ComponentModel.BackgroundWorker(); .... this.backgroundWorkerLong.DoWork += new System.ComponentModel.DoWorkEventHandler (this.backgroundWorkerLong_DoWork); this.backgroundWorkerLong.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler (this.backgroundWorkerLong_RunWorkerCompleted); this.backgroundWorkerLong.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler (this.backgroundWorkerLong_ProgressChanged);

5.9

Extending classes with new and override


The complete source code of this example is available in the ClassExtention package

A C# class can be extended (i.e. new method added) and modied (i.e. changing the way existing methods work) by using the keywords override and new virtual in front of method declaration you want to add/change. Extending classes may be a trickly aair so use this option with care. Keep in mind that this section is only a very limited subset of the possible situations and is meant as an introduction to a way to solve the so called cross-threading problem. A derived class can be obtained from the base class at declaration by adding the base type to the class type. As an example, the following code can be used to declare a derived class of type SafeTextBox that extend the base class of type TextBox .... public class TextBoxExtended : TextBox { ...... This means that an object of type TextBoxExtended has all properties, method and events of the base class TextBox (e.g. .Text, .AppendText,....) plus all the method, properties, events you add to the new class. 40

If you type and code a method that has the same name as a method already present you are trying to change the way it behaves in your class. At this point two possibilities arise: if the base method was declared (by the programmer who created the base class) as virtual, the method can be modied by writing your method with the keyword override in front if the base method was not declared (by the programmer who created the base class THIS IS THE DEFAULT) as virtual, the keyword override cannot be used and you have to declare your method as either new or new virtual. In the rst case you are going to prevent other programmer to use override on your new method, while in the second case you are going to allow it. In both cases inside the method you are changing, you can start the original code by calling it on the convention object named base As an example, if the extended class is dened as follows using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; namespace ClassExtention { class TextBoxExtended : TextBox { public override string Text { get { return base.Text; } set { base.Text = ">>>" + value + "<<<"; } } 41

public new virtual void AppendText(string aString) { base.AppendText("++++" + aString); } } } the net results is that setting the Text property with the string Hello turns out in the appearance of the string >>>Hello<<< (not so useful, just to explain....). As you see the example modies a property (Text) and a method (AppendText). The former is declared virtual inside the TextBox so that we can modify it via override while the latter is not declared virtual so that the new virtual approach has to be used. IMPORTANT: if you dene the extended class in a separate class le, you get the new object on the toolbox the rst time you compile the project. This allows you add your new components as if they were the base ones. This is quite useful in designing your interface.

5.10

Using delegates to deal with the crossthreading problem


The complete source code of this example is available in the ThreadSafeClasses package

This section explain how to access a control from a thread the did no created it by means of a delegate either in a procedure coded in the main program or by means of a control that is extended to include the delegate procedures. This example is an extension of the Dealing with threads As told in that section the problem is in the _DoWork procedure. If you try to access a control (e.g. a textbox) from the procedure an error arise telling you are accessing a control from a thread dierent from the one that created it.

private void backgroundWorkerLong_DoWork(object sender, DoWorkEventArgs e) { 42

...... textBoxNOTWORK.Text=String.Format("{0:###.###}", percentComplete / 1000000); ....... To avoid this error you must delegate the access operation to a procedure that runs in the control thread so as a rst operation you need to dene the procedure. The controls have a specic property named InvokeRequired that checks the ID of the calling thread and the ID of the thread that created the control and returns true if they are dierent (and therefore the control cannot be directly accessed)

5.10.1

Using a delegate in the program

The delegate approach is accomplished by two dierent operations. Firstly you have to tell the compiler you want to use a delegate procedure and which are the procedure parameters delegate void SetTextCallback(TextBox ctl, string text); Then you have to dene a procedure with the same parameters, that either performs the operation if possible or Invoke a procedure to which it delegate the work. Here is the code: private void SetATextCrossThread(TextBox ctl, string text) { // InvokeRequired compares the thread ID of the // calling thread to the thread ID of the creating thread. // If these threads are different, it returns true. if (ctl.InvokeRequired) { //create the callback object and invoke it SetTextCallback d = new SetTextCallback(SetATextCrossThread); this.Invoke(d, new object[] { ctl, text }); } else 43

{ //no invoke required simply append the text ctl.Text = text; } }

At this point if you want to access the control you call the procedure in the usual way

private void backgroundWorkerLong_DoWork(object sender, DoWorkEventArgs e) { ..... SetATextCrossThread(textBoxWork, message); ......

5.10.2

Using a control that is extended to deal with the delegate approach

This solution require you to extend the control you want to use and use the extended control instead of the base one. The extension has been described in section Extending classes with new and override Keep in mind that you need to override/renew only the properties/methods you want to use cross-thread and not all the methods. In the example below only the text method is changed. Some notes: 1. as you see from the example below, the delegate is dened directly inside the code GetText getTextDel = delegate() { return base.Text; }; 2. the InvokeRequired here is an unqualied property (i.e. without a dot in front) since we are inside the class 44

3. the base keyword is used to refer to the base class 4. the #pragma warning disable 1911 directive is used to get rid of a warning that comply about the anonymous method use 5. if the code is inside a class le included in the project the new object appears in the toolbox and can used as any other base component Here the complete code #pragma warning disable 1911 ...... { public class SafeTextBox : TextBox { delegate void SetText(string text); delegate string GetText(); public override string Text { get { if (InvokeRequired) // Is Invoke required? { GetText getTextDel = delegate() { return base.Text; }; string text = String.Empty; try { text = (string)base.Invoke(getTextDel, null); } catch { } return text; 45

} else { return base.Text; } } set { if (InvokeRequired) // Is Invoke required? { SetText setTextDel = delegate(string text) { base.Text = text; }; try { base.Invoke(setTextDel, new object[] { value }); } catch { } } else base.Text = value; } } } and here the use of the code which is exactly as a conventional textbox private void backgroundWorkerLong_DoWork(object sender, DoWorkEventArgs e) { ...... safeTextBoxWorks.Text = message; ...... 46

The project that explain this section contains a bunch of extended controls you can use in your projects and that are thread aware.

5.11

Revision questions

How can you format two numbers like 1.333333333 and 5.0 so that they are printed as 1.33 and 0005 ? How can you be sure that regardless of the computer operating system language a fractional number is printed using the dot as the separator between integer and fractional part? How can you be sure that regardless of the computer operating system language a fractional number is correctly interpreted? How can you measure a time interval and what is the nominal resolution? How can you eciently copy the content of an array to another array? How can you open a le and write numerical data to it so that the data can be read with a text editor such as notepad? How can you open a le and write numerical data to it in binary mode? How can you open a le and eciently read/write large blocks of numerical data to/from it in binary mode? How can you have a window message appearing that require a user intervention? How can you easily obtain maximum and minimum of an array? How can you prevent an execution error to crash the program? What is the dierence between catch and finally? What is a background thread, when it is useful? How a background thread can communicate with the calling thread? How a background thread can be interrupted? 47

How can you pass several parameters to a thread? How can you create a background thread without using the toolbox? How can you extend a class and why this is useful? What is a virtual method, when you have to used the keyword override? What is the cross-thread problem? when it arises? How can you get rid of this problem? What are delegates? What does it means the keyword InvokeRequired?

48

Chapter 6 Drawing a signal evolution


6.1 General considerations

This is a very common task in the instrumentation and measurement eld. Each time you have a phenomenon which evolves in time you may have the desire to see how the signal evolves. From a mathematic point of view the problem can be expressed with a time depending function: V = f (t) (6.1)

where f is a generic function and V is the quantity you ar interested in. The solution to this request is of course well known and represented by a cartesian plot where the abscissa is the time and the ordinate is the quantity here denoted by the letter V , as if it were a voltage. To x the ideas let us supposed we are interested in following the the process of a capacitor discharge as highlighted in g 6.1 From the theory it is well known that, after the switch is opened at time t0 , the voltage across the capacitor follows an exponential decay. V = V0 e
tT0 RC

(6.2)

The result for a t0 = 1s, V0 = 1V, and = RC = 1s is shown in g 6.2 However, you do not have such function in the reality; the only thing you can do is to put a voltage sampler across the capacitor and sample the voltage as it decrease. The result of this operation is a series of voltages you can arrange in vector of voltage samples. Of course you have to know the interval between the samples i.e. the sampling interval t. At this point we 49

Figure 6.1: The RC circuit for the discharge example

Figure 6.2: The voltage evolution for the RC discharge

can prepare a cartesian plot by reporting the voltage samples on the paper obtaining something similar to 6.3 where the parameters are the same as before and the sampling interval has been set to 0.5s Of course you can connect the points with lines to obtain a better representation of the discharge. This way you could obtain a plot like 6.4 In the following chapters you will learn how to obtain the plot reported in 6.4 directly in the acquisition program, but it can be interesting to observe how the plots above have been obtained. The rst consideration regards the plot of g 6.2 which shows the function behavior. Of course, since the plot has been obtained electronically, the only way to obtain the trace is to construct it by points and therefore g. 6.2 50

Figure 6.3: The voltage evolution for the RC discharge

Figure 6.4: The voltage evolution for the RC discharge

and g. 6.4 are conceptually the same thing, simply with a dierent number of points: g 6.2 has so many short lines that it appears as a continuous line. All three plots can be easily obtained by using programs for data manipulation such as Matlab c or Scilab c Scilab is an OpenSource equivalent of Matlab which can be freely downloaded at http://www.scilab.org and gives almost the same functionalities at no cost. In the remaining part of this document all the examples requiring data manipulation are carried out using Scilab. The most important section which create the traces is the following deltaT=0.01; //use a small sampling interval to have a smooth line 51

t=[0:deltaT:6]; t_0=1; tau=1; V_0=5; V=V_0*ones(1,size(t,2)); openId=find(t==t_0); discharge=[openId:size(t,2)]; V(discharge)=V_0*exp(-(t(discharge)-t_0)/tau); plot(t,V); //plot as a continous line As you can see the data creation ends with two vectors t and V of identical length which are then plotted on a cartesian space with the command plot(t,V);. A couple of considerations: During a real measurement it is expected that the vector V is produced by real measurements, while the vector t is to be created either tagging each element of V with the actual acquisition time or created a prioriif the acquisition interval is predened. the command plot(t,V); automatically scale the traces so that they ll the plot window. If you do not use programs like Scilab, you have to scale the data yourself The complete code which has been used for generating the gures in this tutorial is reported below1 . After the code has run in Scilab, each gure has been copied to the clipboard and pasted into an OpenOce draw to be exported in jpeg format and imported in Latex. xdel(winsid()); //equivalent to matlab close all //first plot: the continuous line deltaT=0.01; //use a small sampling interval to have a smooth line t=[0:deltaT:6]; t_0=1; tau=1; V_0=5; V=V_0*ones(1,size(t,2));
This code in a le named RCDischarge.sce available within DrawExample.Csharp package
1

52

openId=find(t==t_0); discharge=[openId:size(t,2)]; V(discharge)=V_0*exp(-(t(discharge)-t_0)/tau); plot(t,V); //plot as a continuous line //second plot the sampled points scf(); //equivalent to matlab figure deltaT=1; //use a large sampling interval to emulate the measurement t=[0:deltaT:6]; t_0=1; tau=1; V_0=5; V=V_0*ones(1,size(t,2)); openId=find(t==t_0); discharge=[openId:size(t,2)]; V(discharge)=V_0*exp(-(t(discharge)-t_0)/tau); plot(t,V,*); //plot only the measured points //third plot: the sampled interpolated points scf(); plot(t,V); //plot an interpolation

//the plots have small fonts and thin lines good for making a visual study //but not so nice to be inserted into a document. //We have to adjust the figure properties to obtain nice plots figH=winsid(); //enumerate the figures for i=1:size(figH,2) //for each figure scf(figH(i)); //select the figure m=get("current_figure"); //get the figure handle //a simple figure has one child which is the axis ax=m.children; ax.font_size=6; //increase the font //now change color an thickness of the traces 53

//the axis has a children compound which contains the traces compound=ax.children; polyline=compound.children; //the children of the compound is the polyline polyline.thickness=2; //increase the line width polyline.foreground=0; //use color black for lines polyline.mark_foreground=0; //use color black for points end For people wishing to spend money, here is the equivalent Matlab script2 %matlab script for generating data similar to the ones clear all %first plot: the continuous line deltaT=0.01; %use a small sampling interval to have a smooth line t=[0:deltaT:6]; t_0=1; tau=1; V_0=5; V=V_0*ones(1,size(t,2)); openId=find(t==t_0); discharge=[openId:size(t,2)]; V(discharge)=V_0*exp(-(t(discharge)-t_0)/tau); plot(t,V); %plot as a continous line %second plot the sampled points figure(2); deltaT=1; %use a large sampling interval to emulate the measurement t=[0:deltaT:6]; t_0=1; tau=1; V_0=5; V=V_0*ones(1,size(t,2)); openId=find(t==t_0); discharge=[openId:size(t,2)]; V(discharge)=V_0*exp(-(t(discharge)-t_0)/tau);
2

This code in a le named RCDisc.m available within DrawExample.Csharp pack-

age

54

plot(t,V,*); %plot only the measured points %third plot: the sampled interpolated points figure(3); plot(t,V); %plot an interpolation

6.2

Doing it in C#: the simple way


The complete source code of this example is available in the DrawExample.Csharp package

Let us start from scratch: open the Visual Studio IDE and create a new solution of type Windows Form, in order to be compatible with the example use DrawExample.Csharp as solution name In this example we only need a picture box and a button; the picture box will be the area on which the plot appears, while the button will be used to trigger the trace creation. You are strongly advised to change the default names of the object so that they remember you of their function. In the example we can use buttonDraw and pictureBoxTrace. You can change the names by using the property panel on the right. With the same panel you can also change the button text to Draw! and add a border to the pictureBox so that it appears clearly on the form (Fig. 6.5).

Figure 6.5: The form for the draw example with button and pictureBox At this point we need to write two pieces of code: 55

the code ll the vectors to draw and, since we do not have anyone scaling the data for us, we also have to decide how to scale the data in the pictureBox. the code that actually paint the trace Since the code that paints the traces is in a procedure dierent from the one that ll the trace, we also need to declare the vectors which hold the trace as global. To do this simply double click the form and go at top to declare the vectors: .... public partial class DrawExampleForm : Form { float[] t, V; RectangleF picScale; public DrawExampleForm() { ....... The code for generating the data can be written in the procedure connected to the click event for the buttonDraw object. You can access the procedure by double clicking the button. ....... private void buttonDraw_Click(object sender, EventArgs e) { float tMax=6.0F; //the number of seconds we want to plot float deltaT=1.0F; //use a large sampling //interval to emulate the measurement int nMax=(int)(tMax/deltaT)+1; //the number of samples we need; t=new float[nMax]; //allocate the memory to hold the samples V=new float[nMax]; float t_0=1; float tau=1; //the switch opening time //the time constant 56

float V_0=5; //the initial voltage //generate the t vector for (int i=0;i<nMax;i++) t[i]=i*deltaT; //compute the values for the discharge for (int i=0;i<nMax;i++){ if (t[i]<=t_0) V[i]=V_0; else V[i]=V_0*(float)Math.Exp(-(t[i]-t_0)/tau); } //set a dimension to draw //in this case we want a rectangle starting at x=0 y=0 float xStart = 0; float yStart = 0; //... and with dimension xsize=tMax ysize=V_0 float xSize = tMax; float ySize = V_0; picScale = new RectangleF(xStart, yStart, xSize, ySize); //invalidate the picture to force the refresh pictureBoxTrace.Invalidate();

} ......... Some remarks on the code: t=new float[nMax]; the memory allocation in C# is performed through the new keyword and therefore keywords like malloc and free are not present3 . There is no requirement to deallocate no longer used vectors since the system automatically manages the memory4
Actually it is possible to use these keywords and the pointers only within the unsafeenvironment 4 This is not really true, sometime, with very large vectors, an explicit memory free .i.e an assignment to nullfollowed by an explicit call to the garbage collector GC.Collect();can be useful to reduce the memory allocation
3

57

float tMax=6.0F; the nal F is required since a a number is by default represented as a double so that an explicit downcast is required (float)Math.Exp(-(t[i]-t_0)/tau) All mathematical function in C# are accessed through the Math object i.e. you cannot write x=sin(y) since there is not a sin function in C#; you must write y=Math.Sin(x). The Math object returns a double so also in this case an explicit downcast is required to assign the value to a float variable Now we have to write the code that actually paints the trace. The code must be executed in response to a paint event. To access the procedure connected to the paint event select the pictureBox and in the properties highlight the event by using the specic button. Among the events go on paint and click it. The IDE automatically creates a procedure named pictureBoxTrace_Paint and links it to the paint events (you can see this link by opening the le DrawExampleFormDesigner.cs which is present in the solution explorer on the right. Expanding the section Windows Form Designer generated code you will nd a line saying this.pictureBoxTrace.Paint += new System.Windows.Forms.PaintEventHandler(this.pictureBoxTrace_Paint); Take note of this line since we will use this syntax again the following). At this point you have an empty procedure: private void pictureBoxTrace_Paint(object sender, PaintEventArgs e) { } where sender is the PictureBox whose are you want to paint and e is the parameter which allows you to access the graphic engine. The painting involves two operations: data scaling, to have your trace lling the area dened by the picScale rectangle, and the actual painting. The scaling can be accomplished by using the transform capabilities of the graphic engine asking it to map the physical picture box dimension (in pixels) to your time/voltage values. 58

private void pictureBoxTrace_Paint(object sender, PaintEventArgs e) { //to avoid errors skip painting if the requested scale is silly if (picScale.Width == 0 | picScale.Height == 0) return; RectangleF clipB; //this is the picture box we are working on PictureBox pb = (PictureBox)sender; //this is the graphic object associated to the box Graphics g = e.Graphics; //apply the scale transformations //which are required to draw the vectors as we want g.ResetTransform(); clipB = pb.ClientRectangle; //get the actual pic size //adjust x and y scales remembering //that pixels are counted up/down while //the normal Y positive axis is upward //apply a scale transform g.ScaleTransform(clipB.Width / picScale.Width, -clipB.Height / picScale.Height); //move the origin so that the lower left corner //is where we want g.TranslateTransform(-picScale.X, -picScale.Y - picScale.Height); ......... } }

Eventually we can perform the painting by taking advantage of one of two dierent methods of the graphic engine: DrawLine and DrawLines. Both require you to dene a Pen whose properties are used to paint. The Pen has several attributes, but the most important are the color and the thickness. A zero value for the thickness means a line of one pixel and we will use such value. Pay attention that the thickness value is expressed in user space values and this can generate surprising eects. In addition, using a value dierent from zero can greatly slow down the drawing so that use this opportunity with care. An alternative solution which can be used if you really need a 59

thick pen is described in the section of resizable buered drawing. float penThickness = 0; Pen redPen = new Pen(Color.Red, penThickness); The DrawLine version of the painting is quire straightforward; ....... int iMax = V.Length - 1; for (int i = 0; i < iMax ; i++) { g.DrawLine(redPen, t[i], V[i], t[i + 1], V[i + 1]); } } }

The DrawLine based solution has one advantage and two disadvantages over the DrawLines one. The disadvantages are 1. if two points are close together they are NOT DRAWN at all (bug!) 2. the code is not as ecient as using DrawLines while the advantage is that it does not require additional memory allocation The DrawLines solution requires5 the allocation of a vector of PointF PointF[] pf = new PointF[iMax]; for (int i = 0; i < iMax; i++) pf[i] = new PointF(t[i], V[i]); //draw the polyline g.DrawLines(redPen, pf); The result of our work is in Fig. 6.6
Please note that it would also be possible to use such structure instead of the oat vectors for t and V thus avoiding the double allocation, but this would change the similarity with the Scilab example
5

60

Figure 6.6: The plotted signal

This way to plot data works, but it has a little problem: each time the your window needs to be repainted, maybe because you cover it with another window, the entire drawing process is required even though no change in the data occurs. To highlight the eect of this operation let us to change the line float deltaT=1.0F; to float deltaT=0.000001F; Now with the parameters we have, the number of samples grows to six millions so the plot time becomes not negligible. You can see what happens if you cover your form with another window and then you uncover it. As you can see the windows seems freezing for a certain time. In the next section we will see how to avoid this problem at the expense of a little more complex code.

6.3

Doing it in C#: using a double buer


The complete source code of this example is available in the DrawExampleBuer.Csharp package

As you have experienced in the last test the problem in our code is that the paint event requires a recreation of the entire trace even though it is 61

unchanged. The solution is to use a double buer, write the trace onto the buer, which will hold an image of the trace and then simply render the image each time we need so. Let us start the same way as the previous example, this time naming it DrawExampleBuffer. Now we need to declare the variables for holding the buer, which have to be global since they have to be accessible from dierent procedures. public partial class DrawExampleBuffer : Form { BufferedGraphicsContext context; BufferedGraphics grafx; float[] t, V; RectangleF picScale; .... we need BufferedGraphicsContext which provide the space for holding the buer and a BufferedGraphics which provide the bridge to the graphic engine and the buer Then when we want to plot the image we have to allocate the buer and draw inside it instead that writing on the PictureBox. Both operations can be performed when we press the Draw button which this way will contain the code that originally was in the paint event. private void buttonDraw_Click(object sender, EventArgs e) { ........... //up to here same as before...... //retrieve a context context = BufferedGraphicsManager.Current; //allocate the memory context.MaximumBuffer = new Size(this.pictureBoxTrace.Width + 1, this.pictureBoxTrace.Height + 1); grafx = context.Allocate(this.pictureBoxTrace.CreateGraphics(), new Rectangle(0, 0, this.pictureBoxTrace.Width, this.pictureBoxTrace.Height));

62

Graphics g; g = grafx.Graphics; //now the code that originally was in the paint event g.ResetTransform(); Rectangle clipB = pictureBoxTrace.ClientRectangle; g.FillRectangle(Brushes.Azure, clipB); g.ScaleTransform(clipB.Width / picScale.Width, -clipB.Height / picScale.Height); g.TranslateTransform(-picScale.X, -picScale.Y - picScale.Height); Pen redPen = new Pen(Color.Red, 0); //allocate the PointF[] structure and fill it int nMax = V.Length - 1; PointF[] pf = new PointF[nMax]; for (int i = 0; i < nMax; i++) pf[i] = new PointF(t[i], V[i]); //draw the polyline g.DrawLines(redPen, pf); pictureBoxTrace.Invalidate(); }

Eventually the code in the Paint event becomes a simple request to render the buer private void pictureBoxTrace_Paint(object sender, PaintEventArgs e) { //before we push the draw button //the grafx buffer does not exist! if (grafx!=null) grafx.Render(e.Graphics); } Try the code: the rst time you press the draw button you see the delay required to create the image, but after that no other delays are experienced if you cover/uncover the form. 63

6.4

Doing it in C#: handling the form resize and using thick pens
The complete source code of this example is available in the DrawExampleBuerResize.Csharp package

We are nearly to the end of this example, however plots obtained in Scilab are resizable so that we can adapt the from the our screen. This can be easily done with little eort. Let us start as in previous examples, then selecting the form go to properties and double click the ResizeEnd event so that the procedure for this event appears. You could also select the Resize event, but this would trigger the paint event several times during the resize slowing down the interface. private void FormDrawExampleBufferResize1_ResizeEnd (object sender, EventArgs e) { } Now write the code to resize the PictureBox as an example by leaving the upper left corner xed and changing the lower right corner to be just neat to the form limit. private void FormDrawExampleBufferResize1_ResizeEnd (object sender, EventArgs e) { Rectangle rf = this.ClientRectangle; int xPictureBox = rf.Width - pictureBoxTrace.Left - 10; int yPictureBox = rf.Height - pictureBoxTrace.Top - 10; if (xPictureBox > 0 & yPictureBox > 0) { pictureBoxTrace.Size = new Size(xPictureBox, yPictureBox); } } If you try changing the from size you will se the picturebox size changing as required. 64

Now we can re-use the code already written for creating the data, but this time we have to change the code which allocate the memory in the BufferContext that must change each time the form is resized. To do this we have to manage the Resize event of the PictureBoxTrace private void pictureBoxTrace_Resize(object sender, EventArgs e) { allocateContext(); doRedraw(); } private void allocateContext() { //retrieve a context if (context == null) context = BufferedGraphicsManager.Current; //allocate the memory context.MaximumBuffer = new Size(this.pictureBoxTrace.Width + 1, this.pictureBoxTrace.Height + 1); grafx = context.Allocate(this.pictureBoxTrace.CreateGraphics(), new Rectangle(0, 0, this.pictureBoxTrace.Width, this.pictureBoxTrace.Height)); }

as you can see we have now a procedure called allocateContext which is called each time the picturebox is resized so that the memory allocation follows the picture size. As you see the paint event also call another procedure called doRedraw which performs the redraw each time the picture is resized. Such procedure contains the lines that draw into the buer and that were previously contained inside the draw procedure private void doRedraw() { //allocate the PointF[] structure and fill it DateTime startTime = DateTime.Now; 65

int nMax = V.Length - 1; PointF[] pf = new PointF[nMax]; for (int i = 0; i < nMax; i++) { pf[i] = new PointF(t[i], V[i]); }

//define the pen to be used: be warned that the pen width is in //scaled units! CultureInfo cEn = new CultureInfo("en-US"); float width = (float)Double.Parse(this.textBoxWidth.Text, cEn); Pen redPen = new Pen(Color.Red, width); Graphics g; g = grafx.Graphics; g.ResetTransform(); if (picScale == null | V == null | t == null) return; Rectangle clipB = pictureBoxTrace.ClientRectangle; g.FillRectangle(Brushes.Azure, clipB); //use the transform capabilities of the graphics object g.ScaleTransform(clipB.Width / picScale.Width, -clipB.Height / picScale.Height); g.TranslateTransform(-picScale.X, -picScale.Y - picScale.Height); //draw the polyline g.DrawLines(redPen, pf); DateTime stopTime = DateTime.Now; TimeSpan elapsedTime = stopTime - startTime; this.textBoxTime.Text = String.Format("{0:000.000 ms}", elapsedTime.Ticks * 1e-4); pictureBoxTrace.Invalidate(); } private void buttonDraw_Click(object sender, EventArgs e) 66

{ ..... same as before allocateContext(); doRedraw(); }

As already said, since the pen thickness is dierent from zero the value thickness is interpreted in user coordinates and this can produce surprising results especially when the horizontal and vertical scales are numerically quite dierent. You may experiment what is happening by changing the pen thickness in the form and redrawing the signal. If you need to use pens with a thickness dierent from zero is better to avoid using the transform facility of the graphics object. In this case the scaling of the values to be displayed can be accomplished by using the capabilities of the Affine transform as provided by the Matrix object. The Matrix object has Scale and Translate methods that operate in the same way the ScaleTransform and TranslateTransform for the Graphics object. Once the matrix is initialized, the method .TransformPoints(<point vector>) can be called that operate the data transform in the new coordinate system. Pay attention that the method overwrite the original values contained in the vector. private void doRedrawByAffineTransform() { if (picScale == null | V == null | t == null) return; DateTime startTime = DateTime.Now; //allocate the PointF[] structure and fill it int nMax = V.Length - 1; PointF[] pf = new PointF[nMax]; for (int i = 0; i < nMax; i++) { pf[i] = new PointF(t[i], V[i]); } //allocate a matrix to perform the affine transform Matrix tr = new Matrix(); 67

Rectangle clipB = pictureBoxTrace.ClientRectangle; tr.Scale(clipB.Width / picScale.Width, -clipB.Height / picScale.Height); tr.Translate(-picScale.X, -picScale.Y - picScale.Height); //tranform the points //BEWARE: the transformed points are in the SAME vector! //the original values are lost! tr.TransformPoints(pf); //define the pen to be used: be warned that the pen width is in //scaled units, but this is not a problem in this case CultureInfo cEn = new CultureInfo("en-US"); float width = (float)Double.Parse(this.textBoxWidth.Text, cEn); Pen bluePen = new Pen(Color.Blue, width); Graphics g; g = grafx.Graphics; g.ResetTransform(); g.FillRectangle(Brushes.Azure, clipB); //draw the polyline without using the graphic object scaling g.DrawLines(bluePen, pf); DateTime stopTime = DateTime.Now; TimeSpan elapsedTime = stopTime - startTime; this.textBoxTime.Text = String.Format("{0:000.000 ms}", elapsedTime.Ticks*1e-4); pictureBoxTrace.Invalidate(); }

Also note that this solution is less ecient than using the transform capability of the Graphics object. The penalty varies with the conditions, but you can expect a redraw 7-8 times slower by using the ane transform and a pen thickness of zero. The penalty reduces to 2-3 times when using a pen thickness dierent from zero.

68

6.5

Revision questions

Which object is useful for drawing a signal evolution in C#? What is the Paint event and how you have to deal with it? How can you dene the virtual dimension (i.e. the scale) of a plotted trace? What is the double buffer and what are the advantages of its use? How can you handle the form resizing? What are the problems connected to thick (grater then 1 pixel) traces and how can you deal with this problem?

69

Chapter 7 Using the Serial line


7.1 Generalities

The serial line is one of the cheapest interface which can be used to connect and control an instrument. Unfortunately the serial line also known as RS232 has been invented for a connection between a computer and a modem and not for a direct connection between two computers (the instrument from this point of view can be regarded as a computer). This is not a really big problem, but has signicant consequences on the way the connection cable between instrument and computer has to be arranged. You have to consult the instrument manual to prepare the correct cable and keep in mind that the cable type also aect the code you have to write. Some notes: There are two ways you can follow to add the serial line functionalities to your code. You can either take advantage of the IDE toolbox or write all the serial code yourself starting from scratch. The rst approach is easier to follow, but since the second one will be mandatory for devices not appearing in the toolbox, we will explore both approaches. The nal result is obviously identical but you are strongly advised to avoid mixing the two approaches (this is not forbidden but if you do not know exactly what you are doing can become crazy trying to understand what is happening). regardless of the method to add the serial line, the characters coming from the serial line are managed by a specic thread so that there 70

is a real chance you encounter the cross-threading problem as described in Using delegates to deal with cross-thread. In the following the solution based on the IDE shows the use of the (deprecated!) CheckForIllegalCrossThreadCalls = false; directive; while the manual solution implements the (strongly encouraged!) use of delegates

7.2

Using the serial line taking advantage of the IDE toolbox


The complete source code of this example is available in the SerialLineUse.Csharp package

This approach can be followed for using components which are present in the toolbox bar. Please note that we have been using this approach for all the components we used so far (Buttons, PictureBox, ....). Simply start the IDE, create a new Project and rename the Form as usual. Now go to the Toolbox scroll it down until the component SerialPort appears and drag it onto your Form (g. 7.1. Dierently that with other visual components (e.g. Buttons), the new object is placed at the bottom in a separate strip. You are strongly advised to change the name of the newly created component as usual, after that you can start using it as usual and you can take advantage of the automatic creation of procedures connected to the events (g. 7.2. Now we can proceed as usual adding a Button to open/close the line, a button and a textbox to send a message onto the line and a TextBox to see what is received from the line (g. 7.3. In order to use the features of the serial port we have to import the port denitions and since we have are going to use a communication without handshake (see below) we also have to import the Threading denition in order to be able to pause the program. using System.IO.Ports; using System.Threading;

Before entering the code discussion please observe the code in the load event which says 71

Figure 7.1: Dragging the serial port object onto the form

private void FormSerialLIne_Load(object sender, EventArgs e) { //at startup the line is close so disable the send dutton this.buttonSend.Enabled = false; //this to avoid problems with threading error messages CheckForIllegalCrossThreadCalls = false; } The rst statement is trivial, but the second is quite more tricky. For now do not worry about this, we will discuss the cross-thread problem later. Simply keep in mind that disabling the check as we are doing here is a dangerous shortcut. Now we can discuss the port set-up which is contained in the ButtonOpenClose code 72

Figure 7.2: The automatic event management provided by the IDE

private void buttonOpenClose_Click(object sender, EventArgs e) { if (!serialPortInstrument.IsOpen) { serialPortInstrument.PortName = "COM1"; serialPortInstrument.BaudRate = 9600; serialPortInstrument.Parity = Parity.None; serialPortInstrument.StopBits = StopBits.One; serialPortInstrument.DataBits = 8; serialPortInstrument.DtrEnable = serialPortInstrument.Handshake = //serialPortInstrument.Handshake //serialPortInstrument.Handshake 73 true; Handshake.None; = Handshake.RequestToSend; = Handshake.XOnXOff;

Figure 7.3: Serial port management example

serialPortInstrument.ReceivedBytesThreshold = 1; serialPortInstrument.Open(); this.buttonSend.Enabled = true; this.buttonOpenClose.Text = "Close"; } else { serialPortInstrument.Close(); this.buttonSend.Enabled = false; this.buttonOpenClose.Text = "Open"; } } 74

The rst time the button is pressed the serial port is closed so that we can dene its properties. The rst ve lines dene the format of the data to be interchanged and must be updated according to the instrument requirements. In this case we use eight bits, one stop bit an no parity dening a character of ten bits (including the start bit). This is quite a common set-up; the speed is set to 9600 baud. The following two lines are related to the kind of protocol (and cable!) we are using. The data ow synchronization can be either hardware, software or disabled. The hardware handshake Handshake.RequestToSend uses the RTS (request to send) signal to notify data has to be sent and check for CTS (clear to send) signal before sending the data. On course this solution requires a cable designed to use such signals (i.e. a cable with at least ve wires). The software handshake Handshake.XOnXOff makes use of the special characters Xon Xo and does not require control wires in the cable so that only three wires (ground transmit and receive) has to be present When the handshake is disabled (Handshake.None) both the computer and the instrument have to be able to receive the longest message to be exchanged without suering from buer overow. In addition care must be taken in order to avoid sending messages too fast i.e. sending a message before the previous message has been processed by the receiver. In the past, when the processors had limited power, the non handshake approach was quite dangerous, nowadays this is a less critical issue. Eventually the line serialPortInstrument.ReceivedBytesThreshold = 1; instructs the port driver to generate a DataReceived event when one ore more characters are received. The other lines open the port and enable the send button since the port is now ready. At this point we are ready to send messages to the instrument. Making reference to the instrument we are going to use for our tests, we have to send two messages :SYST:REM and *IDN?. The rst message instructs the instrument to go in the remote state while the second one requires the instrument to send back a string with its identication code. The code that accomplish these operations is

75

private void buttonSend_Click(object sender, EventArgs e) { //put the dmm in remote and ask the IDN textBoxMessage.AppendText("Sending SYST:REM\r\n"); serialPortInstrument.Write(":SYST:REM\r\n"); Application.DoEvents(); Thread.Sleep(1000); textBoxMessage.AppendText("Sending *IDN?\r\n"); serialPortInstrument.Write("*IDN?\r\n"); } A couple of comments: each message is terminated with the sequence carriage return line feed. The line feed is mandatory since the instrument complies with the IEEE488.2 protocol which requires ending each command with the line feed The command which puts the instrument in a remote state requires a not negligible time to be executed. Since we are not using an handshake, if we sent the second message in sequence (i.e. at the computer speed) it would produce an error. the Thread.Sleep(1000); statement instructs the program to sleep for 1000ms before continuing; this is quite a long time but useful to follow the communication sequence, the time can be shortened in real applications. The Application.DoEvents(); statement tells the thread which executes the button statements to yield giving the thread which update the text box the possibility to update the box. In the absence of such a statement the text box would be updated only at the end of the button code i.e. the two lines would appear together. At this point we have to discuss the most complex part of out work i.e. how to receive the data from the serial line. This operation is not so easy due to the following combination of facts: the data arrive asynchronously i.e. we do not know when they arrive; it depends on the instrument speed so that we cannot think of waiting a certain time to be sure all the data have arrived

76

the number of characters we have to receive is generally not know in advance so that we cannot simply instruct the driver to trigger an event when a specied number of characters have arrived the data arrive slowly with respect to the computer speed i.e. at 9600 baud a character is sent in about 1ms while a computer instruction is executed in nanoseconds. In addition the characters can arrive discontinuously so that we cannot think of waiting a certain time to be sure the message is complete The solution to all these constraints is to allocate a global buer and queue the characters as they arrive until the terminator character (i.e. the linefeed for the IEEE499.2 protocol). Only when this happens we can extract the message from the queue and process it. The code that performs these operation requires the global denition public partial class FormSerialLIne : Form { String globalReceivedChars; ..... The data managing is performed by the procedure associated to the DataReceived event (which is triggered each time at least one character has arrived). Please not that, since the computer can react with delay to the trigger event due to the other operation it could be engaged in, there is no guarantee the drive buer contains on a character. private void serialPortInstrument_DataReceived(object sender, SerialDataReceivedEventArgs e) { //manage received data int lfPosition; String oneLine; if (e.EventType == SerialData.Chars) { globalReceivedChars = globalReceivedChars + serialPortInstrument.ReadExisting(); //check is a delimiter i.e. a LF has arrived 77

lfPosition = globalReceivedChars.IndexOf("\n"); if (lfPosition > 0) { //extract the message oneLine = globalReceivedChars.Substring(0, lfPosition); //process the message processThisLine(oneLine); //remove the message from the buffer globalReceivedChars = globalReceivedChars.Substring(lfPosition + 1); } }

} private void processThisLine(String line) { //for now do nothing but show the answer textBoxMessage.Text = textBoxMessage.Text + ">> " + line + "\n"; } } As you can see each time the procedure is called all the characters in the driver buer are read and queued to the global buer, the buer is examined and if the linefeed is present the message is extracted processed and removed from the buer. At this point the processing is simply an update of the text box to see what arrived, but also this simple operation is critical. To see why, try commenting out the line CheckForIllegalCrossThreadCalls = false; and look what happens. The reason for the error is that the event driven procedure runs in a thread dierent from the thread that update the graphic interface so that there is (a not so remote...) possibility that both threads try to access the text box control at the same time with unpredictable results. The solution to this problems is that the event driven thread asks the main thread to perform the update with a specic command we will see in the next paragraph. 78

7.3

Manual serial line addition and cross thread solution


The complete source code of this example is available in the SerialLineUseManual package

In this section we will re-create the serial line software without taking advantage of the toolbox and we will tackle the cross threading problem. Let us start a new project and put Buttons and text boxes as in the previous section. Also import threading and port denitions since we are going to use them as before. At this point, instead of dragging the serial line from the toolbox we explicitly declare the serial port as a global variable public partial class SerialLineUseManual : Form { SerialPort serialPortInstrument; String globalReceivedChars; ........ The object of type SerialPort is what we need and is dened in the System.IO.Ports package. Remember that declaring an object is not creating it and we need to explicitly create the serial port. We can do this in the open/close button: private void buttonOpenClose_Click(object sender, EventArgs e) { if (serialPortInstrument == null) { serialPortInstrument = new SerialPort(); serialPortInstrument.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler (this.serialPortInstrument_DataReceived); } serialPortInstrument.PortName = "COM1"; ...... This code fragment makes two things: creates the object serial port (with a check to avoid multiple creations) and associates a procedure 79

(serialPortInstrument_DataReceived) the the event DataReceived. Such an association in the previous example was automatically performed by the IDE, but now we have to do it by hand. That is all: the remaining part of the code that manages the serial line does not change. We still have the problem of the cross threading which is a bit more complex to tackle. As we recalled the problem is connected to the fact that the Data_Received procedure runs in a thread dierent from the thread that manages the graphic component. As we anticipated the solution is to delegate the actual graphic update to a procedure running within the graphic thread passing the text to update to it. Note that this is a delicate operation since involves a cross-threading parameter passage and is performed by using the keyword Invoke. Al already pointed out in section Using delegates to deal with crossthread, the process is performed in two steps. Firstly we have to declare that we are going to use a procedure to delegate an operation to another thread. To do this we put a global delegate declaration delegate void AppendTextCallback(TextBox ctl, string text); This statement tell the system we will make use of a procedure named AppendTextCallback with the shown parameters and that will run in another thread (for us the thread that manages the graphic interface). Then when we want to run the procedure we invoke it from the proper thread:

private void processThisLine(String line) { //for now do nothing but show the answer AppendATextCrossThread(textBoxDataReceived, ">> " + line + "\n"); } .......... private void AppendATextCrossThread(TextBox ctl, string text) { 80

// // // if {

InvokeRequired compares the thread ID of the calling thread to the thread ID of the creating thread. If these threads are different, it returns true. (ctl.InvokeRequired) //create the callback object and invoke it AppendTextCallback d = new AppendTextCallback(AppendATextCrossThread); this.Invoke(d, new object[] { ctl, text });

} else { ctl.Text = ctl.Text + "\r\n" + text; } }

The procedure looks strange but in fact it is quire simple: the parameter TextBox ctl is the text box we want to update. When the procedure AppendATextCrossThread is called from the procedure processThisLine it runs in the thread created by the serial line manager so that the test ctl.InvokeRequired returns true. In this case a delegate of type AppendTextCallback is created that wraps a fresh copy of the procedure AppendATextCrossThread (i.e. of the same procedure that is running!) then a request to the system is made to invoke such procedure within the graphic thread (i.e. the thread identied by the keyword this) The Invoke syntax looks strange, but is not so complex: this.Invoke(d, new object[] { ctl, text }); an invoke is performed for the delegate d passing it a parameter of type object array that must contain the (ordered) parameters which are the parameters expected by the delegate procedure. The former procedure (the one running in the serial line thread) then exists, but the delegate (the copy running in the GUI thread) still exists and runs. When the delegate procedure starts, it is in the graphic thread so that ctl.InvokeRequired returns false and the text is updated. 81

7.4

Revision questions

How can you use the serial line, what is the dierence between using the toolbox and adding manually the object? What is the serial line handshake and how this is connected to the hardware wiring? What are the serial line specic transmission errors? How the data arrive from the serial line and how can you collect and use them? What is the cross-thread problem when using the serial line and how can you solve it?

82

Chapter 8 Using the National Instrument IEEE488


8.1 Simple data polling

The complete source code of this example is available in the NI488.Csharp package BEWARE: in order to compile this program you must have the National Instrument GPIB488.2 drivers installed on your computer. If you want to run this program you also need an IEEE488 National Instrument Board. The use of the National Instrument IEEE488 interface is made through the driver provided by NI. In order to use the driver two operations have to be done. Firstly we have to add the Dynamic Link Library (dll) which contains the driver to the solution resources. To do this go to Project/Add Reference (g. 8.1, select the Net tab and add the two references named NationalInstruments.NI4882 and NationalInstruments.Common. If you are using a pre-assembled code, you can still encounter a missing reference error if the DLL revision present in your computer is not the same which is expected by the program. If you are using a newer driver release there is a good chance you can still compile the program, simply remove the references present in you code and add them again as explained above. Secondly we have to import the driver denitions by means of the usual using directive. using NationalInstruments.NI4882; 83

Figure 8.1: The form to be used for adding the National Instrument reference

At this point we can interact with the instruments by using the object of type Device NationalInstruments.NI4882.Device gpibDmm; The device needs to be opened by telling the system which NI488 board it has to use and which is the instrument address. private int BOARD = 0; private byte ADDRESS = 9; private byte SEC_ADDRESS = 0; gpibDmm = new NationalInstruments.NI4882.Device( BOARD, ADDRESS, SEC_ADDRESS); Some remarks: the board number depends on the computer conguration, refer to Board installation to nd how to determine such number remember that the GPIB is an asynchronous bus: you cannot know in advance how long a read (or write) operation can require. If it last forever (usually due to a wrong instrument setting), you may experience 84

a system hang. To avoid this problem setup an operation timeout (gpibDmm.IOTimeout = TimeoutValue.T10s;) The national instrument APIs employ an internal buer for reading data which is usually limited to one kilobyte. If you expect to read larger data block you either need to increase the buer size (gpibDmm.DefaultBufferSize = 32000;) or setup a read cycle until all the data are extracted from the bus. You may check the GpibStatusFlags.IOComplete ag to check if the IO operation is complete. Writing commands to the instrument and reading responses are nor easy operations since they are accomplished through the device object: gpibDmm.Write("*IDN?\n"); response = gpibDmm.ReadString(); You are strongly advised to surround all instrument related code in a try-catch environment to be able to debug your application

8.2

Waiting for acquisition end without using the SRQ events

The complete source code of this example is available in the NI488.SRQPolling.CSharp package BEWARE: in order to understand this section you must be acquainted with the IEEE488.2 SRQ and status reporting operations. When you have to perform data acquisition operations that last for a while and you use a blocking read data method (e.g. a .ReadString) your system may hang until the operation completes. This is the reason you are strongly advised to set the timeout to a reasonable value, but anyway dead times of several seconds should be avoided. This can be obtained starting the read operation only when the instrument is ready to send the results (i.e. the actual measurement operation is nished). You can use the SRQ bus facility to be notied of this (see the following section), but a similar result can be obtained avoiding the direct SRQ use (and the problems connected to the interrupt handler management) 85

by taking advantage of the IEEE488.2 specications which dene a common command i.e. *OPC specically designed for this purpose. Using this technique requires six steps 1. Setup the DMM to perform the long operation without starting it; this does not contain novelties with respect to the previous section and will not described 2. Enable the operation complete event 3. Cleanup previous events 4. Start the long operation start and issue an OPC request 5. Poll the Status byte until operation completes 6. Read the data Enabling the generation of the operation complete event requires to set the Standard Event Status Register with the common command *ESE and Service Request Enable register with the common command *SRE. The Standard Event Status Register denes which events will be used to feed the Service Request; for the present example you need to enable the Operation Complete event which is the less signicant bit (i.e. the bit zero) thus the command is: gpibDmm.Write("*ESE 1\n"); Then you have to dene which events will be used to trigger the SRQ request. The bit coming from the standard event register is bit number ve thus the required command is: gpibDmm.Write("*SRE 32\n"); After this setup the instrument is instructed to generate a SRQ when the last operation completes, but you must be sure nothing is in the queue and this can be obtained by manually reading the operation complete bit with the common command *OPC? gpibDmm.Write("*OPC?\n"); String strTemp = gpibDmm.ReadString(); 86

At this point you can start the measurement, usually by means of an INIT command, and issue an OPC (without the question mark) command; gpibDmm.Write("INIT\n"); gpibDmm.Write("*OPC\n"); Now the instrument performs the measurement and when the operation completes (i.e. when the *OPC completes its job) it sets the operation complete bit in the Standard Event is set. You can periodically test the bit by reading the status byte, usually within the _Tick event of a timer. The SQR is signalled by bit number six (i.e. 0x40). National Instrument denes an enumeration type (SerialPollFlags) to make the code self-explaining, but you can use integer variables if you prefer ..... timer code SerialPollFlags statusValue = gpibDmm.SerialPoll(); // Test for bit 6 (the SRQ bit) if ((statusValue & SerialPollFlags.RequestingService) == SerialPollFlags.RequestingService) .............

Do not forget to reset the instrument and disable the SRQ request at program end and to surround the code lines with a try-catch environment.

8.3

Using the SRQ in event mode

The complete source code of this example is available in the NI488.SRQEvent.CSharp package BEWARE: in order to understand this section you must be acquainted with the IEEE488.2 SRQ and status reporting operations. You also have to understand the concept of threads and cross-thread problems which are discussed in the serial line chapter (see discussion in the serial line use chapter) The solution described in the previous section provides a better behavior with respect to the simple wait-for-data approach, however it still waste processing time by polling the status byte. Since the SRQ is an event, the 87

best way to deal with it is to register an event handler and wait for the event to be red. To do so you need to have to dene a procedure that handles the event, to register the procedure within the device driver and to interact with data in the driver thread by means of a Delegate procedure. The rst step is to declare the template of the delegate procedure you will use to access the driver thread so that the C# compiler knows how to deal with it: private delegate void NotifyUpdateStatusDelegate( string readText, string status, string count); private NotifyUpdateStatusDelegate notifyUpdateStatusHandler; Then you have to write the procedure that will deal with the event according to the template: private void gpibDmm_Notify(object sender, NotifyData e){} The parameter NotifyData is the way to interact with the driver and contains details about the event (in our case the SRQ) that triggered the procedure. Note that the procedure name is arbitrary, however here the convention underscore plus name of the event (i.e. _Notify has been used) Eventually you have to register the procedure to manage the SQR event by using the .Notify method: gpibDmm.Notify( GpibStatusFlags.DeviceServiceRequest, new NotifyCallback(gpibDmm_Notify), "Example of callback"); Now you are ready to start your long operation as described in the previous section ..... setup code gpibDmm.Write("*ESE 1\n"); gpibDmm.Write("*SRE 32\n"); gpibDmm.Write("*OPC?\n"); String strTemp = gpibDmm.ReadString(); 88

gpibDmm.Write("INIT\n"); gpibDmm.Write("*OPC\n"); When the measurement procedure ends and an SRQ event is red the gpibDmm_Notify procedure is called. Inside the procedure you may read the results and re-enable the SQR generation (that autodisables by default) for next measurement. private void gpibDmm_Notify(object sender, NotifyData e) { try { gpibDmm.Write("FETCH?\n"); // Ask data String readings = gpibDmm.ReadString(); gpibDmm.SerialPoll(); e.SetReenableMask(GpibStatusFlags.DeviceServiceRequest); AppendATextCrossThread(textBoxMessage,readings); } catch (Exception exp) { MessageBox.Show(exp.Message); } } In the example above, after reading the data, a serial poll is performed to clear the SRQ bit then the signalling mechanism is reenabled. Be warned that the code in the procedure is executed within the thread that manages the IEEE488, which is dierent from the thread that manages the GUI so that you cannot call methods that act on graphics components without raising and illegal cross thread exception and you have to use a delegate, in this example the AppendATextCrossThread, which is described in discussion in the serial line use chapter.

8.4

Revision questions

This section is specic to National Instrument boards thus no specic questions are reported here that are connected to the code. However you must 89

be able to answer generic questions related to the IEEE488 in order to successfully arrange a data acquisition program: What is the IEEE488 device address? What are the dierent roles an IEEE488 device can play? What is the dierence between a system controller and a master system controller? How many devices can you connect on a IEEE488 bus? What is the maximum extension of an IEEE488 bus? How the IEEE488 connector is made? What is the extended addressing? Why the IEEE488 employs 3 handshake lines instead of 2? Why the transfer timeout is so important when dealing with the IEEE488? Why some lines use the open collector circuit and what is the problem connected with this solution? What is the meaning of MLA, MTA? What are the common commands? What is the meaning of commands WAI and OPC? What are the data terminators and how the IEEE488.2 denes them? What are the device capabilities and how are they used by the IEEE488.2? What is the maximum expected IEEE488 transfer speed? What is the SRQ? How can be used? When is it useful? What is the serial poll and what are its advantages? What is the parallel poll and how can be used? What are the separators dened by the IEEE488.2? 90

What is the SCPI? How can you program a data transfer between to devices without involving the system controller? What is the HS488 protocol and what are its advantages?

91

Chapter 9 Using the Hewlett Packard IEEE488


9.1 Simple data polling

The complete source code of this example is available in the HP-IEEE488.BasicRead.CSharp package BEWARE: in order to compile this program you must have the Agilent IO Library Suite version 15 or above installed on your computer. If you want to run this program you also need an IEEE488 HP Board. The use of the Hewlett Packard IEEE488 interface is made through the driver provided by Agilent (formerly Hewlett Packard). The driver up to version 15 does not integrate with the Microsoft help system so than you cannot get context help while developing the code. The help is provided in the compiled help le VISACOM.chm, which is available through the menu Start/Agilent IO Library Suite/Help/VISA COM Help In order to use the driver two operations have to be done. Firstly you have to add the Dynamic Link Library (dll) which contains the driver to the solution resources. To do this go to Project/Add Reference (g. 9.1, select the Com tab and add the references named VISA COM 3.0 Type Library. Should you have to use interfaces from other manufacturers, consult the manufacturer documentation to see which reference have to be added. If you are using a pre-assembled code, you can still encounter a missing reference error if the DLL revision present in your computer is not the same expected by the program. If you are using a newer 92

software release there is a good chance you can still compile the program, simply remove the reference present in you code and add them again as explained above.

Figure 9.1: The form to be used for adding the Agilent reference

Secondly you have to import the driver denitions by means of the usual using directive. using Ivi.Visa.Interop; At this point we can interact with the instruments by using dierent types of objects. The simplest way is to use an Ivi.Visa.Interop.FormattedIO488 object which implements the IFormattedIO interface. Such object contains methods to read and write strings, numbers and binary data. The object also embeds a buer where data can be written until a ush command is sent. private Ivi.Visa.Interop.FormattedIO488 ioDmm; The device needs to be initialized and this is obtained by using another object referred to as ResourceManager. Each instrument is identied by its address and by the identication string of the IEEE488 board the instrument is connected to. Agilent drivers employ a double colon separated string which can contain multiple elds. The simplest form is BOARB_NAME::ADDRRESS thus the string GPIB0::22 refers to an instrument with IEEE488 address 22 93

which is connected to the board identied by the nickname GPIB0. The name GPIB0 is the default name for the rst Agilent IEEE488 board; you can nd (and possibly change) the name actually used on your computer by running the IO Control program provided by the Agilent library suite in the utility section. Please note that this program must be run at least once when a new interface is added to the computer. The code to connect the instrument to the FormattedIO488 object can be summarized as follows: //create the formatted io object ioDmm = new FormattedIO488Class(); //define the address as BOARD::id String instrumentAddress = "GPIB0::12"; //select a reasonable timeout int timeOutValue_ms = 2000; //initialize the IO try { ResourceManager grm = new ResourceManager(); ioDmm.IO = (IMessage)grm.Open(instrumentAddress, AccessMode.NO_LOCK, timeOutValue_ms, ""); } catch (SystemException ex) { MessageBox.Show("Open failed on " + instrumentAddress + " " + ex.Source + " " + ex.Message, "HP488 BasicRead", MessageBoxButtons.OK, MessageBoxIcon.Error); ioDmm.IO = null; return; } Some remarks: remember that the IEEE488 is an asynchronous bus: you cannot know in advance how long a read (or write) operation can last. If it lasts forever (usually due to a wrong instrument setting), you may experience 94

a system hang. To avoid this problem select a reasonable timeout value when you open the instrument with the resource manager. At this point you can write and read data from the instrument. The simplest way is to use the WriteString and ReadString methods. Remember that all writing operations go to the driver buer and are ushed when a FlushWrite command is issued (or when the write method is called with the ushAndEnd parameter set at true; in this case the method is blocking i.e. it does not return until the string is ushed). ioDmm.WriteString("*IDN?", true); //the second parameter true force the buffer flush String response = ioDmm.ReadString(); //read data waiting for a suitable end condition Remember that the received data is stored inside the driver buer. If you expect to have large data block increase the buer size above the maximum expected size to simplify the data management. ioDmm.SetBufferSize(BufferMask.IO_IN_BUF, 30000); When you no longer need the object you have to close it: ioDmm.IO.Close();

9.2

Waiting for acquisition end without using the SRQ events

The complete source code of this example is available in the HP-IEEE488.SRQPolling.CSharp package BEWARE: in order to understand this section you must be acquainted with the IEEE488.2 SRQ and status reporting operations. When you have to perform data acquisition operations that last for a while and you use a blocking read data method (e.g. a .ReadString) your system may hang until the operation completes. This is the reason you are strongly advised to set the timeout to a reasonable value, but anyway dead times of several seconds should be avoided. 95

This can be obtained starting the read operation only when the instrument is ready to send the results (i.e. the actual measurement operation is nished). You can use the SRQ bus facility to be notied of this (see the following section), but a similar result can be obtained avoiding the SRQ use (and the problems connected to the interrupt handler management) by taking advantage of the IEEE488.2 specications which dene a common command i.e. *OPC specically designed for this purpose. Using this technique requires six steps 1. Setup the DMM to perform the long operation without starting it; this does not contain novelties and will not described 2. Enable the generation of the operation complete event 3. Cleanup previous events 4. Start the long operation start and issue an OPC request 5. Poll the Status byte until operation completes 6. Read the data Enabling the generation of the operation complete event requires to set the Standard Event Status Register with the common command *ESE and Service Request Enable register with the common command *SRE. The Standard Event Status Register denes which events will be used to feed the Service Request; for the present example you need to enable the Operation Complete event which is the less signicant bit (i.e. the bit zero) thus ioDmm.WriteString("*ESE 1", true); // Enable operation complete bit //in the standard events Then you have to dene which events will be used to trigger the SRQ request. The bit coming from the standard event is bit number ve thus: ioDmm.WriteString("*SRE 32", true); After this setup the instrument is instructed to generate a SRQ when the last operation completes, but you must be sure nothing is in the queue and this can be obtained by manually reading the operation complete bit with the common command *OPC? 96

ioDmm.WriteString("*OPC?", true); String strTemp = ioDmm.ReadString(); At this point you can start the measurement, usually by means of an INIT command, and setup the OPC command ioDmm.WriteString("INIT", true); ioDmm.WriteString("*OPC", true); Now the instrument performs the measurement and when the operation completes it sets the operation complete bit in the Standard Event is set. You can periodically test the bit by reading the status byte, usually within the _Tick event of a timer. The SQR is signalled by bit number six (i.e. 0x40) int statusValue = ioDmm.IO.ReadSTB(); if ((statusValue & 0x40) == 0x40) { //read here the response Do not forget to reset the instrument and disable the SRQ request at program end.

9.3

Using the SRQ in event mode

The complete source code of this example is available in the HP-IEEE488.SRQEvent.CSharp package BEWARE: in order to understand this section you must be acquainted with the IEEE488.2 SRQ and status reporting operations. You also have to understand the concept of threads and cross-thread problems which are discussed in the serial line chapter (see discussion in the serial line use chapter) The solution described in the previous section provides a better behavior with respect to the simple wait-for-data approach, however it still waste processing time by polling the status byte. Since the SRQ is an event, the best way to deal with it is to register an event handler and wait for the event to be red. To do so you need to have your class implementing the IEventHandler interface and this can be obtained by adding the interface name to the class declaration which is usual of Form type 97

public partial class FormSRQEvent : Form, IEventHandler Declaring that the class implements the IEventHandler interface means that the class has to provide an implementation of the method HandleEvent, which is the method that is called when an event is red: public void HandleEvent(IEventManager vi, IEvent theEvent, int userHandle) Then you need to register the class to receive the events generated by the IEEE488 driver. The rst step is to extract the event manager interface from the instrument: private IEventManager ioEventManager; ioEventManager = (IEventManager)ioDmm.IO; Note that the variable used to represent the event manager must be global to be reused later. The second step is to associate the class to the event by using the .InstallHandler method and to enable the events by using the .EnableEvent method ioEventManager.InstallHandler(EventType.EVENT_SERVICE_REQ, this, 0, 0); ioEventManager.EnableEvent(EventType.EVENT_SERVICE_REQ, EventMechanism.EVENT_HNDLR, 0); The rst line installs this instance of the class (i.e. this) as handler for the service request event (EventType.EVENT_SERVICE_REQ); the second line enables the event and directs them to the handler. At this point when a service request is raised the code contained in the handler event is executed. public void HandleEvent(IEventManager vi, IEvent theEvent, int userHandle) { 98

ioDmm.WriteString("FETCH?", true); String readings = ioDmm.ReadString(); AppendATextCrossThread(this.textBoxMessage, " Received:" + readings + "\r\n"); } Be warned that the code in this procedure is executed within the thread that manages the IEEE488, which is dierent from the thread that manages the GUI so that you cannot call methods that act on graphics components without raising and illegal cross thread exception and you have to use a delegate. In the example the delegate is AppendATextCrossThread, which is described in the serial line chapter (see the discussion in the serial line use). To use the SRQ you have to setup the device to generate it, you have to manage the OPC ans ESR registers and you have to take care of the dierent situations that may arise in case of errors otherwise you might easily get spurious signals. Specically: BEFORE enabling the handler it is advisable to be sure any older pending operation on the device is canceled: ioDmm.IO.Clear(); //send a device clear to be sure the DMM //does not have //pending operations //since we are going to use the SRQ be sure //no pending request is present //the answer should be 0 if 32 we have pending data, //if 64 or 96 we have a pending SRQ int xx = ioDmm.IO.ReadSTB(); //now read the standard register, the answer should be 0 //if it is 1 there were a pending operation complete ioDmm.WriteString("*ESR?", true); //this to clear the //standard event register String esr = ioDmm.ReadString(); //if we read the status now the answer MUST be zero int yy = ioDmm.IO.ReadSTB(); The device must be set-up to generate the SRQ 99

ioDmm.WriteString("*CLS", true); //not really required since we cleared it at open time ioDmm.WriteString("*ESR?", true); //this to clear the //standard event register String esr = ioDmm.ReadString(); ioDmm.WriteString("*ESE 1", true); //activate the OPC bit ioDmm.WriteString("*SRE 32", true); //activate the SRQ enable ioDmm.WriteString("*OPC?", true); //clear any pending operation String strTemp = ioDmm.ReadString(); ...... CONFIGURE the DEVICE ioDmm.WriteString("INIT", true); //start a long operation ioDmm.WriteString("*OPC", true); //put an OPC request in queue Each time you want to restart the the SRQ generation you must reenable it and you must be sure the OPC bit has been cleared, otherwise the SRQ wont be generated ioEventManager.EnableEvent(EventType.EVENT_SERVICE_REQ, EventMechanism.EVENT_HNDLR, 0); ioDmm.WriteString("*ESR?", true); //this to clear the //standard event register //otherwise the event will //not be generated again String strTemp = ioDmm.ReadString();

9.4

Revision questions

This section is specic to Hewlett Packard boards thus no specic questions are reported here that are connected to the code. However you must be able to answer generic questions related to the IEEE488 in order to successfully arrange a data acquisition program. See the questions of previous section.

100

Chapter 10 Using the National Instrument DAQ Boards


The complete source code of this example is available in the NIDAQ.Csharp package BEWARE: in order to compile this program you must have the National Instrument NIDAQ-MX drivers installed on your computer. If you want to run this program you also need a NI-DAQ board. BEWARE: not all NI-DAQ boards support all the feature described in this section

10.1

Forewords

National Instrument Data Acquisition Boards are complex devices whose correct use requires knowledge (though rather basic) of analogue electronics, digital electronic, and signa theory. Since not all users have such a knowledge, NI is going toward solutions that hidethe details of the board operations providing reasonable defaultsfor many board working paraments. While this is correct in most situations, sometime you need to override the defaults to achieve the best results. Unfortunately, the NI documentation is not designed to help you in these operations, however the most important working paraments areexposed by the drivers so that it is only a matter of deeply exploring the documentation to nd out what you are looking for.

101

10.2

Generalities

Before using National Instruments DAQ board two preliminary operations are required that are similar to the National Instrument GPIB preliminary operations: Firstly we have to add the Dynamic Link Library (dll) which contains the driver to the solution resources. To do this go to Project/Add Reference (g. 8.1, select the Net tab and add the two reference named NationalInstruments.DAQmx and NationalInstruments.Common. If you are using a pre-assembled code, you can still encounter a missing reference error if the DLL revision present in your computer is not the same which is expected by the program. If you are using a newer driver release there is a good chance you can still compile the program, simply remove the references present in you code and add them again as explained above. Secondly we have to import the driver denitions by means of the usual using directive. using NationalInstruments.DAQmx; National Instrument Data Acquisition Boards are complex devices that can comprise analog to digital converters, digital to analog converters, digital input/output channels, timers, counters, ..... Using a DAQ board is therefore a complex operations which requires a series of steps before the actual use can take place. The basic component which is used is a Task which is the container for channels, timings and formatters. A task support only one type of channels (i.e. one type of operations such as sampling values or generating values) at a time, so if you want to perform data input and data output at the same time you need two dierent tasks. Exploring the the details of all the operations you can perform with the DAQ boards is beyond the scope of this primer, so that here only the basics of data sampling and data generation will be explored. We will deal with tasks in the following however some general remarks are valid regardless of the types of operation you wish to perform with your task The most important thing to keep in mind is that the task settings (e.g. number of channels, sampling rate,..) are silently passed to the task without performing a check that the hardware is capable of supporting them. Of course such a check must be performed before using the board, but this is performed only when explicitly calling the task Control method 102

Such a method takes a parameter which also allows you to alter the task behavior. The possible values are: TaskAction.Abort Aborts execution of the task. Aborting a task immediately terminates the currently active operation, such as a read or a write. Aborting a task puts the task into an unstable but recoverable state. To recover the task, use Start to restart the task or use Stop to reset the task without starting it. TaskAction.Commit Programs the hardware with all parameters of the task thus speeding up the execution of the subsequent operations. TaskAction.Reserve Marks the hardware resources that are needed for the task as in use. No other tasks can reserve these same resources. TaskAction.Unreserve Releases all previously reserved resources. TaskAction.Start Move the task to the running state, which begins device input or output. TaskAction.Stop Move the task from the running state to the committed state, which ends device input or output. TaskAction.Verify Veries that all task parameters are valid for the hardware.

10.3

Data reading

As said, the access to the hardware is obtained through the Task object. A task is a collection of virtual channels whose properties are dened by adding channels to the task itself. The input channels are referred to as AIChannels and you have to add to the analog input channel collection the channels you want to use by creating them readTask.AIChannels.CreateVoltageChannel...... The driver allows you to create an enormous amount of dierent channel types (thermocouple, resistance,...) suitable for dierent operations, however keep in mind that the board has only input voltage channels so that the other channel types are software-created. Moreover, some of the virtual channel 103

types (e.g. resistance channels) may require more than one physical channel. For this reason I suggest you to avoid the use of complex setting whose details are not clear: with little eort you can create any kind of software processing still having all the details clear and controlled. The CreateVoltageChannel is therefore the basic way of dealing with input channels and we will discuss this section only. The syntax is .... CreateVoltageChannel( string physicalChannelName, string nameToAssignChannel, AITerminalConfiguration terminalConfiguration, double minimumValue, double maximumValue, AIVoltageUnits units ); where the parameters dene the way the channel(s) is(are) created physicalChannelName is the string with the names of one or more physical channels to use to create one or more local virtual channels in the form "Devxxx/ai0:1" where Devxxx is the name assigned (by the conguration program e.g. by Measurement and Automation Explorer) to the DAQ board and ai0 ... aiXX represent the physical channels. A set of consecutive channels can be specied by using the colon character, non consecutive channels may be specied by separating them with colons. nameToAssignChannel One or more names to assign to the created local virtual channels. To use the physical channel name as the local virtual channel name, set this value to Empty. terminalConguration The input terminal conguration. This is a critical choice since the acquisition performance strongly depends on this choice. The types of terminal congurations are listed in AITerminalConfiguration minimumValue The minimum value expected from the measurement, in units. Pay attention to this value since it aects the way the board is congured. More details on this subject can be found in Manual acquisition ne tuning 104

maximumValue The maximum value expected from the measurement, in units. Pay attention to this value since it aects the way the board is congured. units The units to use to return the measurement. Once the channels have been created you can either acquire a single value or use the board pacing to sample your inputs. The pacing is set through the timing method: ......ConfigureSampleClock( string signalSource, double rate, SampleClockActiveEdge activeEdge, SampleQuantityMode sampleMode, int samplesPeChannel ); where signalSource The source terminal for the clock. To use the board internal clock , set this value to the empty string. rate The sampling rate in samples per second. If you use an external source for the sample clock, set this input to the maximum expected rate of that clock. BEWARE this is not the actual ADC sampling frequency, see Manual acquisition ne tuning for more details activeEdge The edges of sample clock pulses on which to acquire or generate samples. If you use the internal clock you can safely use SampleClockActiveEdge.Rising sampleMode The duration of the task. A task is either nite and stops once the specied number of samples have been acquired or generated, or it is continuous and continues to acquire or generate samples until the task is explicitly stopped. samplesPerChannel The number of samples to acquire or generate if sampleMode is set to FiniteSamples. If sample mode is set to ContinuousSamples, NIDAQmx uses this value to determine the buer size.

105

Be warned that both in channels and timing you can set wrong or not possible values without receiving error messages. Moreover, if the driver found a way to satisfy your request, it coerce (i.e. force) your parameters to the ones actually available in the board. All the parameters are validated when the task is checked for correctness with the .Control method. A task cannot be used if it is not veried. readTask.Control(TaskAction.Verify); At this point the task is ready to be used and you can get the data. Although you can use also a raw data reading, the best solution to obtain the data is to use a formatter reader which is able to return a matrix which is automatically reshaped according to the number of channels you prepared the task with. The raw data read produces a vector of bytes whose length is equals to the number of samples times the number of channels time the size of each sample (typically two since most boards gives results with 16 bit resolution). byte[] rawData = readTask.Stream.ReadRaw(nSamplesToReadPerChannel); There are two types of analog reader: scaled and unscaled. The former employs the acquisition board calibration parameters to return a vector doubles which are voltages (since we are using a voltage channel), the latter return the binary data produced by the analog to digital converter so that it lead to a more compact data representation.

//scaled reader reader = new AnalogMultiChannelReader(readTask.Stream); //unscaled reader readerU = new AnalogUnscaledReader(readTask.Stream);

//read scaled double[,] data = reader.ReadMultiSample (nSamplesToReadPerChannel*nChannels); //read unscaled Int16[,] dataU = readerU.ReadInt16 (nSamplesToReadPerChannel*nChannels); 106

..... } Please note that the default conguration of the board is that when you attempt to read data if they are not available the task is moved to the start state to collect the data. For this reason you do not see here an explicit call to: readTask.Control(TaskAction.Start); Also note that if you try reading the stream twice a new acquisition is triggered each time you read.. Eventually, when you have done with your task remember to clean up the system by disposing the task itself. Since tasks are complex objects there is a chance the system garbage collector (which should take care of anything left around and no longer used...) get confused and leave the object consuming memory. Moreover, since there is a not negligible possibility of encountering errors using the board it is a good programming choice to encapsulate all the board related code in a try/catch/nnaly structure: Task readTask=null; try { readTask = new Task(); //create two channels ........ } catch (DaqException exception) { MessageBox.Show(exception.Message); } finally { readTask.Dispose(); }

107

10.4

Generating voltages

Voltage generation using the digital to analog converters is obtained by adding output channels to a task: Task writeTask=null; AnalogMultiChannelWriter writer=null; //create a write task try { writeTask = new Task(); writeTask.AOChannels.CreateVoltageChannel( "Dev1/ao0:1", "", 0, 5, AOVoltageUnits.Volts); writeTask.Control(TaskAction.Verify); writer = new AnalogMultiChannelWriter(writeTask.Stream); double[,] dataOut = new double[2,1]; dataOut[0,0] = 3; dataOut[1, 0] = 1; writer.WriteMultiSample(true, dataOut); } catch (DaqException exception) { MessageBox.Show(exception.Message); } finally { writeTask.Dispose(); }

The output channels also allow the generation of waveforms (at least for some types of boards) by using the continuous mode. The code is similar the example above, but with few noticeable dierences. The rst main dierence is that, since the Task which generates the signal has to run until it is explicitly required to stop, the task declaration has to be global and if must not be disposed at the end of preparation, 108

AnalogSingleChannelWriter writer = null; //create a write task try { globalTask = new Task(); globalTask.AOChannels.CreateVoltageChannel(dev+"/ao0", "", -5, 5, AOVoltageUnits.Volts); int nSamplesInBuffer = 2000; double sampleRatePerChannel = 2000; globalTask.Timing.ConfigureSampleClock("", sampleRatePerChannel, SampleClockActiveEdge.Rising, SampleQuantityMode.ContinuousSamples, nSamplesInBuffer);

globalTask.Control(TaskAction.Verify); writer = new AnalogSingleChannelWriter( globalTask.Stream); double[] dataOut = new double[nSamplesInBuffer]; double delta = 2 * Math.PI / nSamplesInBuffer; for (int i = 0; i < nSamplesInBuffer; i++) dataOut[i] = 5*Math.Sin(i * delta); writer.WriteMultiSample(false, dataOut); globalTask.Start(); } catch (DaqException exception) { MessageBox.Show(exception.Message); globalTask.Dispose(); } finally { //the MUST remain active; } 109

As you can see, the (global) task is prepared by adding an output channel as usual, then the board timing is prepared, selecting the option SampleQuantityMode.ContinuousSamples. ............. int nSamplesInBuffer = 2000; double sampleRatePerChannel = 2000; globalTask.Timing.ConfigureSampleClock("", sampleRatePerChannel, SampleClockActiveEdge.Rising, SampleQuantityMode.ContinuousSamples, nSamplesInBuffer); At this point a writer is created and used to write the samples into the board buer. Eventually the generation started and the procedure ends. This way the task scans the buer with the specied timing and sends the samples to the analog to digital converter actually generating the signal. The task continues generating the waveform until it is explicitly stopped: .... globalTask.Stop(); ....

10.5

Using digital inputs and outputs

Digital data input and output are performed by using respectively DIChannels and DOChannels. Both type of channels allows you to address either single lines or groups of lines. Usually lines are referred to as lineXX so that the code for an input operation typically looks like: Task digitalTaskIn = null; DigitalSingleChannelReader digitalReader = null; //create a write task try { digitalTaskIn = new Task(); digitalTaskIn.DIChannels.CreateChannel( "Dev1/line0:3", "", 110

ChannelLineGrouping.OneChannelForAllLines); digitalTaskIn.Control(TaskAction.Verify); digitalReader = new DigitalSingleChannelReader(digitalTaskIn.Stream); byte b = digitalReader.ReadSingleSamplePortByte(); this.textBoxDigital.AppendText( "Digital read: "+b.ToString()+"\r\n"); } catch (DaqException exception) { MessageBox.Show(exception.Message); } finally { digitalTaskIn.Dispose(); }

while for output operations: Task digitalTaskOut=null; DigitalSingleChannelWriter digitalWriter = null; //create a write task try { digitalTaskOut = new Task(); digitalTaskOut.DOChannels.CreateChannel("Dev1/line4:7", "", ChannelLineGrouping.OneChannelForAllLines); digitalTaskOut.Control(TaskAction.Verify); digitalWriter = new DigitalSingleChannelWriter(digitalTaskOut.Stream); bool[] boolData = new bool[4]; boolData[1] = false; digitalWriter.WriteSingleSampleMultiLine(true, boolData); this.textBoxDigital.AppendText("Written digital data\r\n"); 111

} catch (DaqException exception) { MessageBox.Show(exception.Message); } finally { digitalTaskOut.Dispose(); }

10.6

Performing continuous acquisitions

The complete source code of this example is available in the NIDAQ-CallBackAcquisition package BEWARE: in order to compile this program you must have the National Instrument NIDAQ-MX drivers installed on your computer. If you want to run this program you also need a NI-DAQ board. BEWARE: not all NI-DAQ boards support all the feature described in this section This example shows how to perform a long asynchronous acquisition, i.e. a long acquisition during which you can deal with the samples you have already got without stopping the acquisition itself. An asynchronous acquisition consists of two sections: a command which starts the acquisition (BeginRead.....) and connects the task to a procedure which is activated when the requested number of samples has got a callback procedure which is red when the requested number of samples has present and that must remove the samples from the buer and restart the reading. Note that the procedure of data removal from the buer and acquisition restart is a critical operation since it has to be performed before the next sample of the stream has to be acquired. It is therefore quite dicult to operate in this way if the requested sampling frequency is high. Techniques 112

to obtain a seamless stream of data are available that are beyond the scope of this tutorial. The code here shown is only a simplied version still suitable to learn how to deal with the callback operations. Initially you have to declare and prepare the task as already shown, but with the timing set to ContinuousSamples globalTask.Timing.ConfigureSampleClock("", samplingFreq, SampleClockActiveEdge.Rising, SampleQuantityMode.ContinuousSamples, BLOCK_LEN); Once the task has been veried, you have to set-up the reader (either unscaled or scaled) you want to use to extract the samples analogUnscaledReader = new AnalogUnscaledReader(globalTask.Stream); then, before starting the data acquisition you have to create the procedure which will handle the sample blocks as they arrive. Such a procedure must be declared as delegate (see delegate) since it executes in the main thread by means of the keyword AsyncCallback AsyncCallback analogCallback; ........ analogCallback = new AsyncCallback(AnalogInCallback); ........ private void AnalogInCallback(IAsyncResult ar) { ........

as you see from this code fragment we have a global declaration followed by the object creation which points to the actual procedure code (AnalogInCallback). This procedure has only a single parameter of type IAsyncResult. The code contained inside the delegate procedure has to copy and remove the data from the acquisition buer and store them somewhere they can be 113

processed and has to restart the acquisition in order to receive other notications. Please observe that it is important this procedure executes quickly moving lengthy processes in another place (i.e. in a process which executes in another thread). In the code fragment that follows the procedure simply copy the data by using an Array operation in a way suitable to preserve the multichannel data integrity: private void AnalogInCallback(IAsyncResult ar) { try { { //Read the available data from the channels Int16[,] data = analogUnscaledReader.EndReadInt16(ar); for (int i = 0; i < N_CHANNELS; i++) { int sourceStart = i * BLOCK_LEN; int destPos = actualLenght + i * MAX_LEN; Array.Copy(data, sourceStart, queuedRawData, destPos, BLOCK_LEN); }

actualLenght += BLOCK_LEN; this.textBoxStatus.AppendText("Got "+ actualLenght.ToString()+"\r\n"); if (actualLenght < MAX_LEN & running) { analogUnscaledReader.BeginReadInt16( BLOCK_LEN, analogCallback, null); }else{ 114

this.textBoxStatus.AppendText("Done\r\n"); running = false; } } } catch (DaqException ex) { MessageBox.Show(ex.Message); globalTask.Dispose(); } }

10.7

Using DAQ boards in trigger mode

The complete source code of this example is available in the NIDAQ-Trigger package BEWARE: in order to compile this program you must have the National Instrument NIDAQ-MX drivers installed on your computer. If you want to run this program you also need a NI-DAQ board. BEWARE: not all NI-DAQ boards support all the feature described in this section This example shows how to perform a triggered acquisition, i.e. an acquisition that starts only after a specic (hardware) event occurs. Some DAQ boards (check the board documentation!) support trigger channels of digital and/or analogue types. This means that an acquisition task can be armed and instructed to start the actual acquisition only after a trigger event. Please note this facility requires a dedicated hardware inside the DAQ board and therefore is available only in selected devices. Using the trigger is easy: you simply instruct the task to use the trigger globalTask = new Task(); ... set channels globalTask.Timing.ConfigureSampleClock("", sampleRatePerChannel, SampleClockActiveEdge.Rising, 115

SampleQuantityMode.FiniteSamples, nSamplesToReadPerChannel); select the trigger type (in this example digital, rising, channel zero) globalTask.Triggers.StartTrigger.Type = StartTriggerType.DigitalEdge; globalTask.Triggers.StartTrigger.DigitalEdge.Edge = DigitalEdgeStartTriggerEdge.Rising; globalTask.Triggers.StartTrigger.DigitalEdge.Source = "/Dev1/PFI0"; globalTask.Control(TaskAction.Verify); when you start the task: globalTask.Start(); the board waits for the trigger before acquiring the data. This means that a read operation such as: double[,] trgData = reader.ReadMultiSample(nSamplesToReadPerChannel * nChannels); does not return (and freeze the thread) waiting for available data until the trigger arrives. Of course you can avoid this behavior using an asynchronous reading as described in the Continuous acquisition

10.8

Manual acquisition ne tuning

As said in the previous sections, the NIDAQ driver sometimes coercesthe parameters you may set in order to cope with the acquisition system capabilities and sometimes set the parameters according to strategies that might not be what you need. In these case you may want to override the defaults by manually setting the hardware values. This requires knowing the hardware capabilities of your hardware, but can optimize the system performance. Dealing with all the parameters is beyond the scope of this guide, however two aspects may be important in measurement application and need to be explained: the actual acquisition timing and the input programmable amplier (PGA) setting. 116

10.8.1

Manually setting the acquisition timing

As said before, the acquisition pacing is set through the task ConfigureSampleClock method. This method takes a single rate parameter which is the overall sampling sampling frequency. This means that if you congured your board for multichannel sampling the rate is the frequency of the group of samples, not the actual Analog to Digital Converter (ADC) frequency that is higher than the rate (at least rate times the number of congured channels). By default the driver selects the minimum possible delay between the samples of the same group plus a xed delay of 10s (i.e. uses the ADC at the maximum speed but 10s). This solution minimize the skew between samples in each group, but may increase the errors due to the settling time of the input stages. In this case you may manually set the conversion rate between the samples by using the task Timing.AIConvertRate method. Be warned that the Timing.AIConvertRate cannot exceed the maximum value (i.e. Timing.AIConvertMaximumRate) and that the interval between the group of samples (set by the rate parameter in the Timing.ConfigureSampleClock) must be compatible with the selected date and the number of samples int nSamplesToReadPerChannel = 2000; int nChannels = 2; //we used ai0 and ai1 double sampleRatePerChannel = 2000; readTask.Timing.ConfigureSampleClock("", sampleRatePerChannel, SampleClockActiveEdge.Rising, SampleQuantityMode.FiniteSamples, nSamplesToReadPerChannel); //by configuring the sample clock you define the interval //between GROUPS of samples composed of the convertion of the //selected channels // //the interval between samples within the same group //(i.e. the ConvertRate in NI words) is //set by default at the maximum board capability (sometime // adding a delay of 10us to allow the multiplexer to stabilize //if you need to change this default you have to override //the AIConvertRate value in Hertz readTask.Timing.AIConvertRate=100000; 117

10.8.2

Manually setting the input gain

When you create a voltage channel with the CreateVoltageChannel method: AIChannel chA=readTask.AIChannels.CreateVoltageChannel(dev + "/ai0:1", "0-1", AITerminalConfiguration.Differential, MAXV, MAXV, AIVoltageUnits.Volts); The driver uses MAXV and MAXV to choose the gain of the input stages. The actual selected gain depends on the used bard and on its conguration and can be inspected (after the TaskAction.Verify) by reading the properties chB.RangeHigh; chB.RangeLow; The same parameters can be used to set the values, however remember the driver can coerce the values to cope with the board constraints (e.g. in some boards you cannot set dierent ranges on dierent channels)

10.9

Revision questions

This section is specic to National Instrument boards thus no specic questions are reported here that are connected to the code. However you must be able to answer generic questions related to the IEEE488 in order to successfully arrange a data acquisition program. What is the meaning of dierential, single ended, referenced single ended input? What are the problems you can encounter using the wrong input connection? What are the problems connected to the use of not galvanically coupled signals? What are the problems connected to the nite input impedance and when they become important? What is the problem of the common mode signals?

118

What is the CMRR and what are the problems connected to its nite value? What is the problem of settling time and how can it aect the acquisition of multiple signals? How the sampling frequency can be selected and what is the problem of signal reconstruction? What are the most common interpolators?

119

Chapter 11 Interface installation and useful links


In this chapter you can nd some advice on how to install the hardware which is used in this tutorial. If you are using this tutorial within courses of Politecnico di Torino you do not need to install anything, since the basic software you are going to use has already been installed.

11.1

National Instrument

The drivers required to use the National Instrument boards can be downloaded at http://www.ni.com/support/. Check the licensing rules to see if you are entitled to download and use a specic piece of software. Once the drivers have been installed you can congure the dierent boards and nd the addresses and codes to be used in you code by starting the Measurement \& Automation program. Fig. 11.1 shows an example of screen shot where a GPIB board with nickname GPIB0 and address 0 is displayed.

11.2

Agilent

The drivers required to use the Agilent boards can be downloaded at http://www.home.agilent.com/ in the section Test and measurement software. Check the licensing rules to see if you are entitled to download and use a specic piece of software. 120

Figure 11.1: The Measurement & Automation Screen

Once the drivers have been installed you can congure the dierent boards and nd the addresses and codes to be used in you code by starting the IO Control program. Fig. 11.2 shows an example of screen shot where a GPIB board with nickname GPIB0 is displayed.

121

Figure 11.2: The Agilent IO COnguration Screen

122

Chapter 12 GPL Licence


The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.

12.1

Preamble

The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free softwareto make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundations software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions 123

translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether free or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) oer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each authors protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modied by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reect on the original authors reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in eect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyones free use or not licensed at all. The precise terms and conditions for copying, distribution and modication follow.

12.2

TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The Program, below, refers to any such program or work, and a work based on the Program means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modications and/or translated into another language. (Hereinafter, translation is included without limitation in the term modication.) Each licensee is addressed as you. Activities other than copying, distribution and modication are not 124

covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Programs source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option oer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modied les to carry prominent notices stating that you changed the les and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modied program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modied work as a whole. If identiable sections of that work are not derived from the Program, and 125

can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written oer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the oer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an oer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modications to it. For an executable work, complete source 126

code means all the source code for all modules it contains, plus any associated interface denition les, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by oering access to copy from a designated place, then oering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions 127

are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may dier in detail to address new problems or concerns. 128

Each version is given a distinguishing version number. If the Program species a version number of this License which applies to it and any later version, you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are dierent, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA 129

BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS

12.3

How to Apply These Terms to Your New Programs

If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source le to most eectively convey the exclusion of warranty; and each le should have at least the copyright line and a pointer to where the full notice is found. one line to give the programs name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type show w.

130

This is free software, and you are welcome to redistribute it under certain conditions; type show c for details. The hypothetical commands show w and show c should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than show w and show c; they could even be mouse-clicks or menu itemswhatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a copyright disclaimer for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program Gnomovision (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License.

131

You might also like