Academic Company Events NI Developer Zone Support Solutions Products & Services Contact NI MyNI

Document Type: Tutorial
NI Supported: Yes
Publish Date: Sep 11, 2006


Feedback


Yes No

Related Categories

Related Links - Developer Zone

Related Links - Products and Services

Minimizing Code Growth Within an IVI Instrument Driver

2 ratings | 4.00 out of 5
Print

Overview

By reading this document, you should:

    • Gain knowledge of some common causes of code growth
    • Learn how to recognize similarities within instrument driver code
    • Learn how to reduce redundancy in instrument driver code through shared range tables, shared callback functions, and shared utility routines


Introduction

IVI (Interchangeable Virtual Instruments) drivers are powerful, high-performance software modules that are capable of configuring and controlling the most common test and measurement instruments of today. However, when developed for certain instruments, the code for IVI drivers can grow to undesired lengths despite their high performance and flexibility. Code growth can make the driver more complex and harder to manage. The purpose of this document is to provide you with some helpful tips on how to reuse code within your drivers to help protect against code growth.

Causes of Code Growth


Some common causes of code growth are redundancy in:
  • Attribute callbacks
  • Attribute range tables
  • Utility functions

Depending on their complexity, some instrument drivers may require a large number of attributes. A large number of attributes can be necessary when you want to construct a more accurate software model of the instrument or when you want to provide as much instrument-specific functionality as possible from the driver.

A large number of attributes typically means that you will write a large number of attribute callbacks, which can generate a substantial amount of code. However, some of these callback functions may contain similar routines or identical code. While having a large number of attributes is often unavoidable, they can often be implemented with a smaller amount of code.

Like attribute callbacks, a large number of attribute range tables may be generated by the development of a more complex instrument driver. In some cases, several attributes might support the same range of values. Again, code growth can occur with the creation of many identical range tables.

Even redundant high-level functions can cause unnecessary code growth. Redundancy can occur when related functions duplicate an implementation.

Developing Cleaner Code


You can recognize potential problems in your driver by identifying and exploiting patterns of structure and behavior in the instrument and driver. Thus, you can generate cleaner, reusable code that is easier to manage. The following sections contain examples that explain how you can go about accomplishing cleaner code.

Sharing Attribute Callbacks


Callback functions are very important in IVI drivers in that they are the mechanism by which you can customize validation, coercion, and I/O routines for a specific attribute. Often, an instrument vendor builds an instrument in such a way that I/O strings for setting and querying attributes are fairly similar in format. Additionally, related attributes almost always possess similar or identical I/O strings.

For example, the following table contains eight attributes taken from the Tekronix 754 instrument driver along with their associated I/O command strings. The purpose of these attributes is to configure the glitch and runt triggers of the instrument. The notation <v> represents the value to which the user wishes to set the associated attribute.


Attribute
Set I/O String
Query I/O String
GLITCH_WIDTH ":TRIG:":TRIG:MAI:PUL:GLI:WID?"
GLITCH_POLARITY":TRIG:":TRIG:MAI:PUL:GLI:POL?"
GLITCH_CONDITION":TRIG:":TRIG:MAI:PUL:GLI:FILT?"
RUNT_HI_THRESHOLD ":TRIG:":TRIG:MAI:PUL:RUNT:THR:HIGH?"
RUNT_LO_THRESHOLD ":TRIG:":TRIG:MAI:PUL:RUNT:THR:LOW?"
RUNT_POLARITY":TRIG:":TRIG:MAI:PUL:RUNT:POL?"
RUNT_WHEN ":TRIG:":TRIG:MAI:PUL:RUNT:WHE?"
RUNT_WIDTH  ":TRIG:":TRIG:MAI:PUL:RUNT:WID?"

Given this set of I/O strings, the read and write callbacks for these attributes can be easily implemented. The example code below demonstrates how the read and write callbacks for GLITCH_POLARITY, an enumerated attribute, could be written:


static ViStatus GlitchPolarity_ReadCallback

(ViSession vi,


ViSession io,


ViConstString channelName,


ViAttr attributeId,


ViInt32 *value)
{
ViStatus error = VI_SUCCESS;

viCheckErr( viQueryf (io, ":TRIG:MAI:PUL:GLI:POL?", "%Ld", value));

Error:
Return error;
}

static ViStatus GlitchPolarity_ReadCallback (ViSession vi,
ViSession io,
ViConstString channelName,
ViAttr attributeId,
ViInt32 *value)
{
ViStatus error = VI_SUCCESS;

viCheckErr( viQueryf (io, ":TRIG:MAI:PUL:GLI:POL?", "%Ld", value));

Error:
Return error;
}

static ViStatus GlitchWidth_WriteCallback (ViSession vi,
ViSession io,
ViConstString channelName,
ViAttr attributeId,
ViInt32 value)
{
ViStatus error = VI_SUCCESS;

viCheckErr( viPrintf (io, ":TRIG:MAI:PUL:GLI:WID %Ld;", value));

Error:
Return error;
}

The read and write callbacks for the other seven attributes can be implemented in a similar manner. Unfortunately, this approach results in 16 callbacks for performing I/O, as well as additional callbacks for checking and coercing values.

Examine the I/O strings for the attributes and you will notice that they all follow the same format. All of the I/O strings contain the header :TRIG:MAI:PUL: followed by a trigger type and then the attribute associated with that type. One approach might be to implement one general read callback and one general write callback for the attributes of the same trigger type; however, doing this would prove to be extremely difficult. Although attributes of the same subsystem have similar I/O strings, they are almost always of different type. Furthermore, the callbacks could then only be shared by the attributes in that particular subsystem.

Another approach is to generalize attribute callbacks not by the similarities in the I/O strings but by the similarity of how I/O is performed for an attribute as well as its type. Based on this, you can regroup the attributes as follows.


ViInt32 (Enumerated)
Set I/O String
Query I/O String
GLITCH_POLARITY ":TRIG:":TRIG:MAI:PUL:GLI:POL?"
GLITCH_CONDITION  ":TRIG:":TRIG:MAI:PUL:GLI:FILT?"
RUNT_POLARITY":TRIG:":TRIG:MAI:PUL:RUNT:POL?"
RUNT_WHEN ":TRIG:":TRIG:MAI:PUL:RUNT:WHE?"

ViReal64
Set I/O String
Query I/O String
GLITCH_WIDTH  ":TRIG:":TRIG:MAI:PUL:GLI:WID?"
RUNT_HI_THRESHOLD":TRIG:":TRIG:MAI:PUL:RUNT:THR:HIGH?"
RUNT_LO_THRESHOLD":TRIG:":TRIG:MAI:PUL:RUNT:THR:LOW?"
RUNT_WIDTH":TRIG:":TRIG:MAI:PUL:RUNT:WID?"

For each attribute, the set and query I/O strings are identical, except that a question mark is added to the end when querying the attribute, and a value is appended when setting it. You can create a string-value table that associates a command string with an attribute ID:

IviStringValueTable CommandTable =
{
{GLITCH_WIDTH, "TRIG:MAI:PUL:GLI:WID"},
{GLITCH_POLARITY, "TRIG:MAI:PUL:GLI:POL"},
{GLITCH_CONDITION, "TRIG:MAI:PUL:GLI:FILT"},
{RUNT_HIGH_THRESHOLD, "TRIG:MAI:PUL:RUNT:THR:HIGH"},
{RUNT_LOW_THRESHOLD, "TRIG:MAI:PUL:RUNT:THR:LOW"},
{RUNT_POLARITY, "TRIG:MAI:PUL:RUNT:POL"},
{RUNT_WHEN, "TRIG:MAI:PUL:RUNT:WHE"},
{RUNT_WIDTH, "TRIG:MAI:PUL:RUNT:WID"},
{VI_NULL, VI_NULL},
};

An IviStringValueTable is an array of structures that contain a value (in this case, an attribute ID) and a string (in this case, a command string). String values in an IviStringValueTable can be retrieved by calling the Ivi_GetStringFromTable function.

With the command table in place, you can now create read and write callbacks for the enumerated attributes:

static ViStatus enum_ReadCallback (ViSession vi,
ViSession io,
ViConstString channelName,
ViAttr attributeId,
ViInt32 *value)
{
ViStatus error = VI_SUCCESS;
ViString cmd;

viCheckErr( Ivi_GetStringValueFromTable (CommandTable, attributeId, &cmd));
viCheckErr( viQueryf (io, ":%s?", “%Ld”,cmd, value));

Error:
Return error;
}

static ViStatus enum_WriteCallback (ViSession vi,
ViSession io,
ViConstString channelName,
ViAttr attributeId,
ViInt32 value)
{
ViStatus error = VI_SUCCESS;
ViString cmd;
ViString cmdStr;
IviRangeTablePtr rt;

viCheckErr( Ivi_GetStringValueFromTable (Commands, attributeId, &cmd));
checkErr( Ivi_GetAttrRangeTable (vi, channelName, attributeId, &rt));
viCheckErr( Ivi_GetViInt32EntryFromValue (value, rt, VI_NULL, VI_NULL,
VI_NULL, VI_NULL, &cmdStr,
VI_NULL));

viCheckErr( viPrintf (io, ":%s %s;", cmd, cmdStr));
Error:
Return error;
}

Similarly, you can create the read and write callbacks for the ViReal64 attributes:

static ViStatus real_ReadCallback (ViSession vi,
ViSession io,
ViConstString channelName,
ViAttr attributeId,
ViReal64 *value)
{
ViStatus error = VI_SUCCESS;
ViString cmd;

viCheckErr( Ivi_GetStringValueFromTable (Commands, attributeId, &cmd));
viCheckErr( viQueryf (io, ":%s?", “%Lf,” cmd, value));

Error:
Return error;
}

static ViStatus real_WriteCallback (ViSession vi,
ViSession io,
ViConstString channelName,
ViAttr attributeId,
ViReal64 value)
{
ViStatus error = VI_SUCCESS;
ViString cmd;
ViString cmdStr;
IviRangeTablePtr rt;

viCheckErr( Ivi_GetStringValueFromTable (Commands, attributeId, &cmd));
viCheckErr( viPrintf (io, ":%s %Lf;", cmd, value));

Error:
Return error;
}

The original number of callbacks has been reduced from 16 to 4. Notice that the structure of the callbacks remains relatively the same. The only difference is, that instead of hardcoding command strings into the callback functions, the command strings are now retrieved from the IviStringValue table and built into the I/O strings accordingly. Hardcoding string commands into a callback function allows that function to be used by only one specific attribute. This new approach allows you to generalize callbacks and to add new attributes more easily. To add a new attribute, simply add its ID and command string to the command table. Like attribute range tables, you can associate callbacks with an attribute either by its Ivi_AddAttribute<type> function or by the advanced section of the Attribute Editor in LabWindows/CVI:

Ivi_AddAttributeViReal64 (vi, TKTDS754_ATTR_GLITCH_WIDTH,
"TKTDS754_ATTR_GLITCH_WIDTH",
2.0e-9, 0, real_ReadCallback,
real_WriteCallback,
&triggerTimeRangeTable, 0);


Sharing Attribute Range Tables

Range tables are powerful software tools that define the values to which an attribute may be set. This ability allows you to verify an attribute value before you perform any instrument I/O, and it can greatly minimize the occurrence of I/O errors. As stated earlier, code growth can occur when different range tables are created for attributes with similar ranges.

Consider the following example. The attributes amDepth and dutyCycle are both expressed as percentages and can range from 0.0 to 100.0. At first, you might quickly implement two separate range tables for these attributes as follows:

static IviRangeTableEntry amDepth_RangeTableEntries[] =
{
{0.0, 100.0, 0, "", 0},
{IVI_RANGE_TABLE_LAST_ENTRY}
};
static IviRangeTable amDepth_RangeTable =
{
IVI_VAL_RANGED,
VI_TRUE,
VI_TRUE,
VI_NULL,
amDepth_RangeTableEntries,
};

static IviRangeTableEntry dutyCycle_RangeTableEntries[] =
{
{0.0, 100.0, 0, "", 0},
{IVI_RANGE_TABLE_LAST_ENTRY}
};
static IviRangeTable dutyCycle_RangeTable =
{
IVI_VAL_RANGED,
VI_TRUE,
VI_TRUE,
VI_NULL,
dutyCycle_RangeTableEntries,
};

This approach is perfectly valid. It would compile and run without any problems. However, this code is repetitious. The two attributes have identical ranges and are of the same type: VIReal64. In fact, the two range tables are identical in every respect except for their names.

When you have only two attributes with identical ranges, the cost of an extra range table is not particularly noticeable. However, if you have 10 or even more attributes that are given as percentages, the repetition of code can become quite costly. A better approach is to create a percentage range table, as shown below. This range table is more generalized and can be used by any attribute with the same range:

static IviRangeTableEntry percentageRangeTableEntries[] =
{
{0.0, 100.0, 0, "", 0},
{IVI_RANGE_TABLE_LAST_ENTRY}
};
static IviRangeTable percentageRangeTable =
{
IVI_VAL_RANGED,
VI_TRUE,
VI_TRUE,
VI_NULL,
percentageRangeTableEntries,
};

After creating one range table which can be shared by multiple attributes, you also need to configure each attribute to use it. One way to configure the attributes is by changing the range table parameter in the Ivi_AddAttribute<type> function of each attribute. You pass the address of the range table to the function:

Ivi_AddAttributeViReal64 (instrument_handle, ATTRIBUTE_1,
"ATTRIBUTE_1", default_value,
attibute1_flags, attribute1_ReadCallback,
attribute1_WriteCallback,
&percentageRangeTable);

Another way to configure the attributes is by using the Attribute Editor in LabWindows/CVI. Double-click the desired attribute and then change the range table parameter as shown below.


Sharing Utility Routines

High-level functions often contain similar routines that you can localize into shared utility routines. Although this technique is recognized as a good, general programming practice, shown below is an example to demonstrate this technique while developing IVI drivers.

The following two functions are taken from the hp34401a driver. These two functions, init and InitWithOptions, can also be found in every other IVI-compliant instrument driver:

ViStatus _VI_FUNC hp34401a_init (ViRsrc resourceName, ViBoolean IDQuery,
ViBoolean resetDevice, ViSession *newVi)
{
ViStatus error = VI_SUCCESS;

if (newVi == VI_NULL)
{
Ivi_SetErrorInfo (VI_NULL, VI_FALSE, IVI_ERROR_INVALID_PARAMETER,
VI_ERROR_PARAMETER4, "Null address for Instrument
Handle");
checkErr( IVI_ERROR_INVALID_PARAMETER);
}

checkErr( hp34401a_InitWithOptions (resourceName, IDQuery, resetDevice,
"", newVi));
Error:
return error;
}

ViStatus _VI_FUNC hp34401a_InitWithOptions (ViRsrc resourceName,
ViBoolean IDQuery,
ViBoolean resetDevice,
ViString optionString,
ViSession *newVi)
{
ViStatus error = VI_SUCCESS;
ViSession vi = VI_NULL;
ViChar newResourceName[IVI_MAX_MESSAGE_BUF_SIZE];
ViChar newOptionString[IVI_MAX_MESSAGE_BUF_SIZE];
ViBoolean isLogicalName;

if (newVi == VI_NULL)
{
Ivi_SetErrorInfo (VI_NULL, VI_FALSE, IVI_ERROR_INVALID_PARAMETER,
VI_ERROR_PARAMETER5, "Null address for Instrument
Handle");
checkErr( IVI_ERROR_INVALID_PARAMETER);
}

*newVi = VI_NULL;
checkErr( Ivi_GetInfoFromResourceName (resourceName, optionString,
newResourceName, newOptionString,
&isLogicalName));

checkErr( Ivi_SpecificDriverNew ("hp34401a", newOptionString, &vi));
checkErr( hp34401a_IviInit (newResourceName, IDQuery, resetDevice, vi));

if (isLogicalName)
checkErr( Ivi_ApplyDefaultSetup (vi));

*newVi = vi;

Error:
if (error < VI_SUCCESS)
Ivi_Dispose (vi);
return error;
}

Notice that the init function calls the InitWithOptions function with an empty option string. Furthermore, the only operation performed by init before it calls InitWithOptions is parameter checking, which is also done by InitWithOptions. The parameter checking is redundant. The check must be performed in the InitWithOptions function because, like init, this function is exported to the user. The check must also be done in the init function because, although the parameter newVi being checked is the same, the parameter numbers are different between the functions. Thus, different VISA error codes must be passed to the Ivi_SetErrorInfo function.

You can remove the redundant checking by restructuring the code so that the init function does not call the InitWithOptions function at all. The two functions should behave as separate entities, each doing its own specific error checking and then performing similar initialization routines.

You can write a utility function called initialize that actually performs the initialization:

static ViStatus initialize (ViRsrc resourceName,
ViBoolean IDQuery,
ViBoolean resetDevice,
ViString optionString,
ViSession *newVi)
{
ViSession vi = VI_NULL;
ViChar newResourceName[IVI_MAX_MESSAGE_BUF_SIZE];
ViChar newOptionString[IVI_MAX_MESSAGE_BUF_SIZE];
ViBoolean isLogicalName;

*newVi = VI_NULL;
checkErr( Ivi_GetInfoFromResourceName (resourceName, optionString,
newResourceName, newOptionString,
&isLogicalName));

checkErr( Ivi_SpecificDriverNew ("hp34401a", newOptionString, &vi));

checkErr( hp34401a_IviInit (newResourceName, IDQuery, resetDevice, vi));
if (isLogicalName)
checkErr( Ivi_ApplyDefaultSetup (vi));

*newVi = vi;

Error:
if (error < VI_SUCCESS)
Ivi_Dispose (vi);
return error;
}

Now, the init and InitWithOptions functions should call initialize:

ViStatus _VI_FUNC hp34401a_init (ViRsrc resourceName, ViBoolean IDQuery,
ViBoolean resetDevice, ViSession *newVi)
{
ViStatus error = VI_SUCCESS;

if (newVi == VI_NULL)
{
Ivi_SetErrorInfo (VI_NULL, VI_FALSE, IVI_ERROR_INVALID_PARAMETER,
VI_ERROR_PARAMETER4, "Null address for Instrument
Handle");
checkErr( IVI_ERROR_INVALID_PARAMETER);
}

checkErr( initialize (resourceName, IDQuery, resetDevice, newVi));

Error:
return error;
}


ViStatus _VI_FUNC hp34401a_InitWithOptions (ViRsrc resourceName,
ViBoolean IDQuery,
ViBoolean resetDevice,
ViString optionString,
ViSession *newVi)
{
ViStatus error = VI_SUCCESS;

if (newVi == VI_NULL)
{
Ivi_SetErrorInfo (VI_NULL, VI_FALSE, IVI_ERROR_INVALID_PARAMETER,
VI_ERROR_PARAMETER5, "Null address for Instrument"
"Handle");
checkErr( IVI_ERROR_INVALID_PARAMETER);
}

checkErr( initialize (resourceName, IDQuery, resetDevice, newVi));

Error:
return error;
}

Conclusion


As test and measurement instruments increase in their functionality, instrument drivers must increasingly expose more functionality and instrument features. However, the complexity of the instrument does not have to be carried over into the corresponding IVI driver. By identifying common range tables, callbacks, and utility functions, an IVI driver can maintain efficiency and flexibility while reducing the amount of code, thus being easier to modify and extend.
2 ratings | 4.00 out of 5
Print

Reader Comments | Submit a comment »

 

Legal
This tutorial (this "tutorial") was developed by National Instruments ("NI"). Although technical support of this tutorial may be made available by National Instruments, the content in this tutorial may not be completely tested and verified, and NI does not guarantee its quality in any way or that NI will continue to support this content with each new revision of related products and drivers. THIS TUTORIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND AND SUBJECT TO CERTAIN RESTRICTIONS AS MORE SPECIFICALLY SET FORTH IN NI.COM'S TERMS OF USE (http://ni.com/legal/termsofuse/unitedstates/us/).