Channels ▼
RSS

.NET

The VISA I/O API & .NET

Source Code Accompanies This Article. Download It Now.


November, 2004: The VISA I/O API & .NET

David is an applications engineer with Agilent Technologies. He can be contacted at author_dgladfelter@agilent.com.


Accessing Unmanaged DLL Functions From .NET Languages

Variable Argument Lists in .NET and Unmanaged Interoperability


The Virtual Instrument Software Architecture (VISA) is a vendor interchangeable Standard for instrument communication developed by the VXIplug&play Systems Alliance (http://www.vxipnp.org/). VISA identifies a common I/O API for communicating over a variety of bus interfaces, including GPIB, VXI, and serial interfaces.

The VISA Standards body (http://www.ivifoundation.org/) has specifications for API versions in C, COM, and other programming technologies—but currently not .NET. Consequently, Agilent Technologies (the company I work for) has developed header files, help, and tutorials for using VISA in C# and Visual Basic .NET. Anyone familiar with the C version of the VISA API should be comfortable using the .NET header files.

I/O APIs and VISA

An I/O API is a set of functions that provide a logical pipe to a device attached to the computer. I/O APIs permit PC/device communication via an arbitrary, typically string-based language defined by the device. The characteristic functions in an I/O API are Read and Write, usually taking string parameters. Most I/O APIs are session based, meaning that an Open method is called, creating a logical exclusive pipe to the device, and at the end of communication a Close method is called ending the session and freeing any allocated resources. Some of the best-known I/O APIs are the Windows Sockets API, which allows network communication over the TCP/IP protocol and the C printf/scanf communication with files and the console.

Well-understood, common devices eventually have driver software developed where each driver function corresponds to an operation on the device. I/O APIs are used in devices where it is not economical or convenient to put the logic to control the device in the PC. Such devices typically have smaller sales volume, a wide array of features, and are used in a variety of environments. All of these characteristics make it difficult to justify providing a more powerful driver on the PC. Instruments such as voltmeters and network analyzers are often devices with these characteristics, and I/O APIs are a common way of controlling them.

VISA is an I/O API targeted at communication with instruments or other test-and-measurement devices. The VISA standard describes communication with devices using test- and measurement-specific I/O hardware as well as better-known communication mechanisms, such as TCP/IP, USB, and RS-232, either directly or over test-measurement-specific protocols on top of those mechanisms. VISA has standard API definitions over two of the most common PC software reuse mechanisms—C DLLs and Microsoft's COM.

Listing One is a VISA I/O session to an instrument that can be accessed via a TCP/IP connection. The Resource Manager is a class factory that knows how to find, enumerate, and open resources. The viOpen call opens an I/O session to the instrument. The viRead and viWrite calls send/receive data to/from the instrument, respectively. The viClear resets the I/O state of the session and viClose ends communication. Using these VISA methods (defined in visa.h) requires that a Visa32.dll DLL be on the computer, findable in the DLL search path, and configured to access the resources with which the program attempts to communicate.

.NET and VISA

Languages such as C# and VB.NET (among others) have a different software reuse and function-calling mechanism than C/C++. .NET methods are still called through call stacks, but a new metadata format is used to describe the method signature, rather than the header files and .lib files used in C/C++. Microsoft recognized that the ability to call C DLLs from .NET (and vice versa) would remain essential for some time, so it built-in the ability to call between unmanaged functions and .NET code (see the accompanying text box entitled "Accessing Unmanaged DLL Functions from .NET Languages").

There is also interoperability between COM objects and .NET, with the ability to use COM objects from .NET code and the ability to easily expose .NET objects as COM objects. The VISA I/O API has both C and COM versions. The COM version can be automatically imported into .NET either through the tools provided in the .NET SDK or in Microsoft Visual Studio .NET. There is no such ability for C DLLs and header files. This discrepancy exists because .h and .lib files describing a C API do not typically have enough metadata for a caller to reliably build call stacks to the C functions they describe. However, the metadata of COM type libraries (.tlb files) do provide enough metadata to call the COM interfaces they describe.

Agilent has developed redistributable .NET header files that import the VISA-C DLL's API into .NET via a package called "Agilent VISA Header Files and Examples for Visual Basic, Visual Basic .NET, and C#" (available at http://www.agilent.com/find/adn-iolib-drivers/; free registration is required to download). This process lets you call the VISA-C functions from .NET code. This is usually preferable because it avoids the overhead of the VISA COM implementation and provides the functions in a way that's familiar to long-time users of the VISA-C API.

Agilent's .NET Header Files for VISA

After creating a .NET project using either C# or VB.NET, the first step is to add the header files to the project. To do this, you have two options:

  • Add them, which creates a copy of the files and places them in the project directory.
  • Link to them, which references them in the directory where they live rather than creating a new copy. The default directory the two header files live in after installation is c:\program files\visa\winnt\agvisa\include.

Linking to files requires that you right click the project in the Solution Explorer window and click the Add Existing Item... option under the Add submenu. The file-open dialog box that opens has a down-arrow inside the Open button, which gives the Link File option; see Figure 1, the Link File option of the Add Existing Item dialog box.

Clicking the Link File option creates a link to the file wherever it exists without copying it. After linking, you can identify the file as linked by observing that a small arrow appears in the lower-left corner of the file's icon in the Solution Explorer window. Linking to a file is the best option if you do not plan on changing it, or if you want any changes to be shared across a number of projects.

Once the file is added, you can begin writing VISA I/O code. The functions do not exist in a namespace so calling them does not require any import statements. Listing Two, a VB.NET program that uses the Agilent VISA-C .NET header file, grabs the screen image of an oscilloscope and downloads it to the PC, saving it as a GIF file.

The viStatusDesc function used in the CheckStatus method returns a string description of the last error on the current thread if such a description is available. The data is put into a buffer provided by the caller. The System.Text.StringBuilder object with a Capacity property value as big as the largest possible message best represents such a parameter, so that is what the method expects.

The viSetAttribute function and its reciprocal viGetAttribute function are used to set and get properties of the I/O session. The visa.h C header file for VISA-C declares the two functions:

ViStatus _VI_FUNC viSetAttribute (ViObject vi,
ViAttr attrName, ViAttrState attrValue);
ViStatus _VI_FUNC viGetAttribute (ViOb ject vi,
ViAttr attrName, void _VI_PTR attrValue);

The ViAttrState type is a typedef of an unsigned 32-bit integer. Agilent chose to represent these functions as several overloads in the .NET header files for the VISA-C library. The viSetAttribute function is used to send signed and unsigned 8-, 16-, and 32-bit integers for various VISA properties. The viGetAttribute function is used to retrieve the types set by viSetAttribute and also to retrieve strings into a preallocated buffer whose address is provided by the caller in the attrValue parameter. Based on the type conversions from managed to unmanaged code, the overloads in Listing Three were chosen for the .NET header files.

The correct version of these functions is available for any VISA attribute type, but if you use the wrong variable type for a particular VISA attribute type, the .NET runtime will not detect this logic error. There is little risk for the viSetAttribute function since all the parameters are integral and the VISA-C library will not overwrite any memory. Unfortunately, for viGetAttribute, some overloaded functions use pointer parameters that the VISA-C library will dereference and write into the dereferenced memory space, meaning that undefined behavior could result from the misuse of this function.

viPrintf and the corresponding viScanf are the main formatted I/O functions in VISA. They correspond to the C runtime library's printf and scanf functions, and take variable argument lists (see the accompanying text box entitled "Variable Argument Lists in .NET and Unmanaged Interoperability"). They extend the supported data types of the C runtime functions to include serialized data formats commonly seen in instrument communication, such as framed binary data structures called "IEEE 488.2 Definite-Length Binary Data blocks." Finally, they take a VISA session parameter instead of a file handle. There are also versions of the functions, viSPrintf and viSScanf, that write out to a string buffer parameter rather than to the VISA session.

Because there are several possible C primitive data types that can be passed, such as individual integral values (for in data) and pointers to individual integral values (for out or in-out data), the possible overload list grows geometrically with the number of data parameters in the variable argument list. Agilent decided to provide one-variable versions of all the most common data parameters. To support this, there are a total of 60 overloads per file in both the C# and VB.NET versions of the header files. If there had been overloads for every combination of two parameters, there would have been hundreds of overloads per file.

There are several reasons we decided this was good enough. The majority of customer use of formatted I/O requires only one parameter per call. Users have the ability to add new overloaded formatted I/O functions based on the ones already presently targeted at common data combinations they use in their applications. Alternatively, they can read and write their data piecemeal, calling the formatted I/O functions repeatedly to read or write a complete message part by part.

The same caution that applied to the viGetAttribute and viSetAttribute functions applies to the formatted I/O functions: Because the .NET runtime cannot compare the format string parameter to the actual argument(s) passed on the call stack, calling these methods incorrectly is unsafe and can result in memory access violations.

Conclusion

The VISA-C in .NET header files provide a fast and simple method of accessing instrument I/O in .NET languages. Moreover, they provide an I/O API that will be very familiar to C/C++ or Visual Basic 6 programmers who move to a .NET language. The design decisions made for these header files illustrate some of the tradeoffs of calling older, unmanaged code from .NET environments.

Because these header files are simple, you must take the same care in calling these unmanaged functions from .NET as they were calling them from an unmanaged language such as C++. They must make sure for the overloaded and variable argument list functions that the parameter describing what the subsequent parameters will be, such as the format string, is consistent with those parameters.

Calling unmanaged code from .NET takes a programmer off the beaten path. There can be a number of gotchas and more than one way of doing the same thing, requiring careful consideration of the tradeoffs of each option. The greatest danger is probably being too clever, such as using undocumented .NET keywords and APIs. The VISA-C in .NET header files aim for the most direct translation of the C API into .NET rather than adding overhead or unnecessary complexity.

DDJ



Listing One

#include <stdio.h>
#include <stdlib.h>
#include <visa.h>
#include <string.h>

void programExit(char *s)
{
    printf("%s\n",s);
    exit(-1);
}
int main(int argc, char *argv[])
{
    ViSession defaultRM;
    ViStatus status;
    ViSession sess;
    ViUInt32 retCount;
    ViString idnQuery = "*IDN?\n";
    char idnResponse[100];

    // Open Resource Manager that knows how to find, parse, and open resources
    status = viOpenDefaultRM(&defaultRM);
    if ( status != VI_SUCCESS ) { programExit("viOpenDefaultRM() failed");}
    // Open a session to the device, located on the network with hostname 
    // "arbGen.agilent.com" and having a device name of "inst0"
    status = viOpen(defaultRM,"TCPIP::arbGen.agilent.com::inst0::INSTR",
                                        VI_EXCLUSIVE_LOCK,VI_NULL,&sess);
    if ( status != VI_SUCCESS ) { programExit("viOpen() failed");}
    // Perform a clear to put the device I/O in a known state.
    status = viClear(sess);
    if ( status != VI_SUCCESS ) { programExit("viClear() failed");}
    // Ask the instrument to identify itself
    status = viWrite(sess,idnQuery,(ViUInt32)strlen(idnQuery),&retCount);
    if ( status != VI_SUCCESS ) { programExit("viWrite() failed");}
    if ( retCount != strlen(idnQuery)) { programExit("viWrite() failed\n"); }
    // read back the identification string
    status = viRead(sess,idnResponse,sizeof(idnResponse),&retCount);
    if ( status != VI_SUCCESS ) { programExit("viRead() failed\n");}
    // null-terminate the response so it can be printed
    idnResponse[retCount] = 0;
    printf("%s = %s\n","TCPIP::arbGen.agilent.com::inst0::INSTR",idnResponse);
    // Close the I/O resources  
    viClose(sess);
    viClose(defaultRM);
    return 0;
}
Back to article


Listing Two
Module Module1
    Sub Main()
        ' Run sample program with an instrument address of "GPIB::12::INSTR"
        RunExample("GPIB0::12::INSTR")
    End Sub
    Private Sub RunExample(ByVal instrAddress As String)
        ' Declare Variables used in the program
        Dim status As Integer      'VISA function status return code
        Dim defrm As Integer = 0   'Session to Default Resource Manager
        Dim vi As Integer = 0      'Session to instrument
        Dim x As Integer        'Loop Variable
        Dim ResultsArray(50000) As Byte 'results array, holds a GIF
        Dim length As Integer      'Number of bytes returned from instrument
        Dim headerlength As Integer 'length of header
        ' the file to write the picture
        Dim fs As System.IO.FileStream = Nothing
        'Set the default number of bytes that will be contained in the
        'ResultsArray to 50,000 (50kB)
        length = 50000
        Try
            If System.IO.File.Exists("picture.gif") Then
                System.IO.File.Delete("picture.gif")
            End If
            ' Open the default resource manager session
            status = visa32.viOpenDefaultRM(defrm)
            ' Open the session.  For GPIB, the address string looks like: 
            '       GPIB0::18::INSTR
            ' For PSA, to use LAN, change the string to
            ' "TCPIP0::xxx.xxx.xxx.xxx::inst0::INSTR" where 
            ' xxxxx is the IP address
            status = visa32.viOpen(defrm, "instrAddress", 0, 0, vi)
            CheckStatus(defrm, status)
            ' Set the I/O timeout to fifteen seconds
            status = visa32.viSetAttribute(vi,visa32.VI_ATTR_TMO_VALUE,15000)
            CheckStatus(vi, status)
            'Store the current screen image on flash as C:PICTURE.GIF
            status = visa32.viPrintf(vi,":MMEM:STOR:SCR 'C:PICTURE.GIF'" & vbLf)
            CheckStatus(vi, status)
            'Grab the screen image file from the instrument
            status = visa32.viPrintf(vi, ":MMEM:DATA? 'C:PICTURE.GIF'" & vbLf)
            CheckStatus(vi, status)
            ' We're reading this as raw binary, although it is a IEEE 488.2
            ' binary block containing byte data.  We could've used
            ' "%#b" format string, and the byte array would not contain the
            ' IEEE binary block header.
            status = visa32.viScanf(vi, "%#y", length, ResultsArray)
            CheckStatus(vi, status)
            'Delete the tempory file on the flash named C:PICTURE.GIF
            status = visa32.viPrintf(vi, ":MMEM:DEL 'C:PICTURE.GIF'" & vbLf)
            CheckStatus(vi, status)
            'Close the vi session and the resource manager session
            Call visa32.viClose(vi)
            vi = 0
            Call visa32.viClose(defrm)
            defrm = 0
            'Store the results in a text file
            fs = _
                        New System.IO.FileStream("picture.gif", _
                        IO.FileMode.OpenOrCreate)
            Dim zeroVal() As Char = {"0"}
            Dim zeroValByte() As Byte
            zeroValByte = System.Text.Encoding.ASCII.GetBytes(zeroVal)
            headerlength = ResultsArray(1) - zeroValByte(0) + 2
            fs.Write(ResultsArray, headerlength, length - 2 - headerlength)
        Catch err As System.ApplicationException
            MsgBox("*** Error : " & err.Message, vbExclamation, _
                                        "VISA Error Message")
            Exit Sub
        Catch err As System.SystemException
            MsgBox("*** Error : " & err.Message, vbExclamation, _
                                        "System Error Message")
            Exit Sub
        Catch err As System.Exception
            Debug.Fail("Unexpected Error")
            MsgBox("*** Error : " & err.Message, vbExclamation, _
                                        "Unexpected Error")
            Exit Sub
        Finally
            If Not fs Is Nothing Then fs.Close()
            If vi <> 0 Then
                Call visa32.viClose(vi)
            End If
            If defrm <> 0 Then
                Call visa32.viClose(defrm)
            End If
        End Try
    End Sub
    Private Sub CheckStatus(ByVal vi As Integer, ByVal status As Integer)
        If (status < visa32.VI_SUCCESS) Then
            Dim err As System.Text.StringBuilder = New _
                            System.Text.StringBuilder(256)
            visa32.viStatusDesc(vi, status, err)
            Throw New ApplicationException(err.ToString())
        End If
    End Sub
End Module
Back to article


Listing Three
#Region "viGetAttribute Overloads"
    <DllImportAttribute("VISA32.DLL", EntryPoint:="#133", _
                        ExactSpelling:=True, CharSet:=CharSet.Ansi, _
                        SetLastError:=True, _
                        CallingConvention:=CallingConvention.Winapi)> _
    Public Function viGetAttribute(ByVal vi As Integer, _
                                    ByVal attrName As Integer, _
                                    ByRef attrValue As Byte) As Integer
    End Function
    <DllImportAttribute("VISA32.DLL", EntryPoint:="#133", _
                        ExactSpelling:=True, CharSet:=CharSet.Ansi, _
                        SetLastError:=True, _
                        CallingConvention:=CallingConvention.Winapi)> _
    Public Function viGetAttribute(ByVal vi As Integer, _
                                    ByVal attrName As Integer, _
                                    ByRef attrValue As Short) As Integer
    End Function
    <DllImportAttribute("VISA32.DLL", EntryPoint:="#133", _
                        ExactSpelling:=True, CharSet:=CharSet.Ansi, _
                        SetLastError:=True, _
                        CallingConvention:=CallingConvention.Winapi)> _
    Public Function viGetAttribute(ByVal vi As Integer, _
                                    ByVal attrName As Integer, _
                                    ByRef attrValue As Integer) As Integer
    End Function
    <DllImportAttribute("VISA32.DLL", EntryPoint:="#133", _
                    ExactSpelling:=True, CharSet:=CharSet.Ansi, _
                    SetLastError:=True, _
                    CallingConvention:=CallingConvention.Winapi)> _
    Public Function viGetAttribute(ByVal vi As Integer, _
                    ByVal attrName As Integer, _
                    ByVal attrValue As System.Text.StringBuilder) As Integer
    End Function
#End Region

#Region "viSetAttribute Overloads"
    <DllImportAttribute("VISA32.DLL", EntryPoint:="#134", _
                    ExactSpelling:=True, CharSet:=CharSet.Ansi, _
                    SetLastError:=True, _
                    CallingConvention:=CallingConvention.Winapi)> _
    Public Function viSetAttribute(ByVal vi As Integer, _
                                    ByVal attrName As Integer, _
                                    ByVal attrValue As Byte) As Integer
    End Function
    <DllImportAttribute("VISA32.DLL", EntryPoint:="#134", _
                    ExactSpelling:=True, CharSet:=CharSet.Ansi, _
                    SetLastError:=True, _
                    CallingConvention:=CallingConvention.Winapi)> _
    Public Function viSetAttribute(ByVal vi As Integer, _
                                    ByVal attrName As Integer, _
                                    ByVal attrValue As Short) As Integer
    End Function
    <DllImportAttribute("VISA32.DLL", EntryPoint:="#134", _
                        ExactSpelling:=True, CharSet:=CharSet.Ansi, _
                        SetLastError:=True, _
                        CallingConvention:=CallingConvention.Winapi)> _
    Public Function viSetAttribute(ByVal vi As Integer, _
                                    ByVal attrName As Integer, _
                                    ByVal attrValue As Integer) As Integer
    End Function
#End Region
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

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

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

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

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

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

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

Video