VB.net Example Code for Complex Triggering using ps5000a

Post your .Net discussions here
Post Reply
dfergenson
User
User
Posts: 2
Joined: Fri Mar 04, 2016 8:58 am

VB.net Example Code for Complex Triggering using ps5000a

Post by dfergenson »

Does anyone have any working VB.net example code that shows how to set complex triggers using the ps5000a.dll?

I've read through the API Programmer's Guide sections on:

ps5000aSetTriggerChannelConditionsV2
ps5000aSetTriggerChannelDirectionsV2
ps5000aSetTriggerChannelProperties
ps5000aSetPulseWidthQualifierConditions
ps5000aSetPulseWidthQualifierDirections
ps5000aSetPulseWidthQualifierProperties

To be honest, I'm having trouble getting my head around all of the different functions, how to use them and how they interrelate.

The pack #pragma in VB.net is also an issue that I'm having a hard time with. Is there an equivalent or a workaround?

If nobody has done this before, I'll see if I can get something working and post it.

But something to get me from 0 to 20 (if not 60) would help me tremendously.

Thanks.

-Davio

dfergenson
User
User
Posts: 2
Joined: Fri Mar 04, 2016 8:58 am

Re: VB.net Example Code for Complex Triggering using ps5000a

Post by dfergenson »

I fought my way through some of the functions and wrote this explanation of them. I hope that it helps someone and, if you are able to help, there are some requests for additional work below.

A Treatise on Triggering the PicoScope 5000X Series Oscilloscopes Using the Legacy Complex Triggering Functions
by
DAVIO


Overview:

There are three functions that must be called to set up a complex trigger scenario:
  • ps5000aSetTriggerChannelConditions which instructs the PicoScope as to which channels must be true, false or ignored when triggering.
  • ps5000aSetTriggerChannelDirections which instructs the PicoScope as to whether a particular signal on a particular channel should be rising past a threshold, dropping out of a window, etc.
  • ps5000aSetTriggerChannelProperties which defines the thresholds for the different channels.
Although dividing these parameters in these ways seems a bit counterintuitive, with good example code and a clear explanation, it is possible to rerepresent them with more intuitive code.

Each of the .DLL functions requires a different structure array, as described below, to communicate the triggering parameters. The PS5000A API manual describes all of those structures as being byte aligned using the Pack pragma in C family programming languages. This may be confusing to the uninitiated so here’s a brief description of what it means to pack a structure.

A structure is a special case of a variable which contains a heterogenous collection of other variables (each of the sub-variables is called a “field” of the structure). Consider a structure with two fields, one of which is a 16-bit integer and the other of which is a 32-bit integer. By default, a structure will space all of its fields evenly in memory. So, in this case, in order to maintain the fields at regular intervals, the compiler will add unused space between the two fields. The functions in ps5000a.DLL require them to not have that padding. Structures without padding are referred to as being “packed”.
Screen Shot 2021-03-17 at 4.41.46 PM.png
In fact, every field of the structures that are input to ps5000aSetTriggerChannelConditions and ps5000aSetTriggerChannelDirections have the same number of bytes so, while the API Programmer’s Guide instructs the programmers to pack those structures, and while they are packed in the C versions of the example code, there was, in fact, no padding to remove in the first place. So those functions are executed natively from Visual Basic.

ps5000aSetTriggerChannelProperties, however, uses a structure that does have differently sized fields. Visual Basic doesn’t have a packing command so Pico’s solution for programming languages that don’t support packing is to have the program call an intermediary.DLL, ps5000aWrap.dll, written in C (which supports packing), which assembles a packed structure and then calls ps5000a.dll. Unlike ps5000a.dll, ps5000aWrap.dll is open source and the code is hosted on Pico’s GitHub account so it’s a good resource to see how the different functions are called.

To perform complex triggering of analog signal only and without considering the width of an event being triggered off of, we’ll be calling:
  • ps5000aSetTriggerChannelConditions from ps5000a.dll
  • ps5000aSetTriggerChannelDirections from ps5000a.dll
  • SetTriggerChannelProperties from ps5000aWrap.dll

ps5000aSetTriggerChannelConditions:

If I were naming this function, I would have called it ps5000aSetTriggerCombinations. This is the function that communicates, channel by channel, whether a particular channel must individually pass a test, or not pass a test, to instruct the entire scope to trigger. What constitutes the test for a particular channel is specified in the other two functions.

The function’s formal definition is:

Code: Select all

PicoStatusCode = ps5000aSetTriggerChannelConditions(ByVal handle as int16,  ByRef Conditions as a PS5000aTriggerConditions Structure, ByVal nConditions as int16).
PicoStatusCode is one of the codes from the status code enumeration. Like all enumerations, VB defines it as an Int32. The status codes have changed over the years and different examples have expanded numbers of status codes. The most thorough list I’ve found is in the C example code.

handle is the handle that is generated during ps5000aOpenUnit.

The ps5000aTriggerConditions structure should already be defined in the example code but here’s a description of the structure:
ps5000aTriggerConditions.channelA is an int32 that is read in from the trigger state enumeration. That enumeration has values:
  • Don’t Care = 0
  • Must be true to trigger the scope = 1
  • Must be false to trigger the scope = 2
  • Max = 3 (this isn’t defined and seems to be unused)
As its name implies, ps5000aTriggerConditions.channelA instructs the scope as to whether Channel A’s trigger conditions, defined in the next two .DLL calls, must be true, must be false, or should be ignored in deciding whether or not to trigger the entire scope.
ps5000aTriggerConditions.channelB is the same thing for Channel B.
ps5000aTriggerConditions.channelC is the same thing for Channel C.
ps5000aTriggerConditions.channelD is the same thing for Channel D.
ps5000aTriggerConditions.external is the same thing for the Ext Trig port.
ps5000aTriggerConditions.aux is defined as the same thing but the aux port doesn’t seem to exist as far as I can know. Perhaps it refers to whether the signal generator is, itself, triggering but to my mind it can be safely ignored by always assigning it value 0 (Don’t Care).
ps5000aTriggerConditions.pulseWidthQualifier determines whether or not the pulse width will be considered when triggering. This is beyond the scope of this treatise and unless you are using the pulse width qualifier I’d just assign this as 0 (Don’t Care) as well.

nConditions indicates how may different sets of trigger conditions can lead to the scope triggering. It’s possible to define a structure as an array (like any other variable) and each element of the array will be a different combination. For example, if you want to trigger off of ((channel A and B) or (Ext and not C)) then the trigger conditions structure would be:
conditions(0).channelA = True
conditions(0).channelB = True
conditions(0).channelC = Don’t Care
conditions(0).channelD = Don’t Care
conditions(0).external = Don’t Care
conditions(0).aux = Don’t Care
conditions(0).pulseWidthQualifier = Don’t Care
conditions(1).channelA = Don’t Care
conditions(1).channelB = Don’t Care
conditions(1).channelC = False
conditions(1).channelD = Don’t Care
conditions(1).external = True
conditions(1).aux = Don’t Care
conditions(1).pulseWidthQualifier = Don’t Care

In this case nConditions should be set to 2.


ps5000aSetTriggerChannelDirections:

This function is also confusingly named because some of the “directions” that the name refers to are not directions at all. If I were to name it, I would call it ps5000aSetTheNatureOfEachChannel’sTest. But, for conciseness and consistency, I’ll stick with the word “directions”.

It’s easiest to explain what this function does one channel at a time. Suppose you want to trigger Channel A when the signal crosses a particular threshold while going in a positive direction. The crossing while going up would be specified in this function, but not the threshold itself. Actually, there are two thresholds, and two hysteresis values, defined for each channel in SetTriggerChannelProperties as described below. But this function only addresses the directions themselves and they are selected from the following enumeration:
  • Trigger whenever the signal is above the upper threshold = ABOVE = 0
  • Trigger whenever the signal is between the upper and lower thresholds = INSIDE = 0
  • Trigger whenever the signal is below the upper threshold = BELOW = 1
  • Trigger whenever the signal is either above the upper or below the lower threshold = OUTSIDE = 1
  • Trigger as the signal rises across the upper threshold = RISING = 2
  • Don’t trigger at all = NONE = 2
  • Trigger whenever the signal falls below the upper threshold or rises above the lower threshold and is between the two thresholds = ENTER = 2
  • Trigger as the signal falls below the upper threshold = FALLING = 3
  • Trigger as the signal rises above the upper threshold or falls below the lower threshold when it was originally between the thresholds = EXIT = 3
  • Trigger as the signal crosses a threshold in either direction = RISING_OR_FALLING = 4. The manual doesn’t specify which threshold this applies to and I haven’t had time to figure it out through experimentation. If someone could, I’d appreciate it.
  • Trigger whenever the signal either enters the area between the thresholds or leaves the area between the thresholds = ENTER_OR_EXIT = 4
  • Trigger whenever the signal is above the lower threshold = ABOVE_LOWER = 5
  • Trigger whenever the signal is below the lower threshold = BELOW_LOWER = 6
  • Trigger as the signal rises above the lower threshold = RISING_LOWER = 7
  • Trigger as the signal falls below the lower threshold = FALLING_LOWER = 8
  • If the signal rises past the lower threshold, then falls past the lower threshold without having passed the upper threshold then trigger as it falls past the lower threshold = POSITIVE_RUNT = 9
  • If the signal falls past the upper threshold then rises again past the upper threshold without ever having passed the lower threshold then trigger as it rises past the upper threshold = NEGATIVE_RUNT = 10
Screen Shot 2021-03-17 at 4.45.06 PM.png
As you can see, some of the enumerated directions share values and some of those associations are counterintuitive (RISING = NONE) but, in the end, any trigger conditions can actually be specified as a combination of setting the right direction, condition and threshold. The ability to disambiguate some of these situations is in SetTriggerChannelProperties.

The function is formally defined as:

Code: Select all

PicoStatusCode = ps5000aSetTriggerChannelDirections(ByVal handle as int16, ByVal channelA as DIRECTION, ByVal channelB as DIRECTION, ByVal channelC as DIRECTION, ByVal channelD as DIRECTION, ByVal ext as DIRECTION, ByVal aux as DIRECTION)
The arguments are all pretty self explanatory but I didn’t know what to make of aux. I just leave it as 0 and specify its trigger condition as DONT_CARE.

SetTriggerChannelProperties

Pico’s API Programmer’s Guide actually does a better job of naming this function: it should be called setUpTriggerThresholds.

It specifies the thresholds, channel by channel, for any or all of the channels involved in triggering.

The easiest way to find this function declaration is in the example Microsoft Excel spreadsheets which contain Visual Basic for Applications code that calls PS5000aWrap.dll. But be careful of the type definitions. VBA dates back at least as far as Windows 3.0 and back in the 1990s a default integer was 16-bit. It’s safer to redefine all of the types explicitly and it’s critical to make sure that the declarations of the variables match the declarations for the .DLL function itself. My working .DLL declaration for this function is:

Code: Select all

Declare Function SetTriggerProperties Lib "ps5000aWrap.dll" (ByVal handle As Int16, ByRef triggerChannelPropertiesArray As Int32, ByVal nProperties As Int16, ByVal autoTrig As Int32) As enPicoStatusCode
handle is self explanatory.

triggerChannelPropertiesArray is an array of Int32s that has some multiple of 6 elements. In each group of 6:

The first element (remember that this is referred to as (0) in VB) defines the UPPER threshold. This value has the same signed 16-bit range as the values that are read out of the digitizer and with the same caveat: they need to be scaled in order to be related to a real voltage. Pico makes some functions that do the scaling for you but it’s pretty easy just to divide the voltage of the threshold by the voltage of the range and multiply that by (32766/2). If you change voltage ranges, make sure to adjust these thresholds in the same step.

The second element defines the upper threshold’s hysteresis. Hysteresis is the amount that the signal must overrun a threshold (in the specified direction) to be considered to have crossed it and is given in the same units as the threshold itself. It’s a noise rejection tool. Because you can only overrun a threshold, not underrun it, this value is a UInt16 in disguise. You store its value as an Int32 so that it lines up with the others in the array but don’t put more than 16383 in it. And that should be WAY too high anyway.

The third element defines the lower threshold.

The fourth element defines the lower threshold hysteresis.

The fifth element is a member of the Channel enumeration. Where the previous two functions specified values for all of the channels, in this case, the channel must be specified. The channel enumeration may be referred to as the Source enumeration and, like all enumerations, it’s stored as an Int32. The values are:
CHANNEL_A = 0
CHANNEL_B = 1
CHANNEL_C = 2
CHANNEL_D = 3
EXT = 4
MAX = 4 (I don’t think this is used).
AUX = 5 (I don’t think this is used).
Max_TRIGGER_SOURCES = 6 (I don’t think this is used)

The sixth element is another enumeration that specifies whether to use the thresholds as levels or together as a window. If you recall, there were some odd overlaps in the “directions” enumeration. This is where they are disambiguated. The enumeration THRESHOLD_MODE can either have the value LEVEL = 0 or WINDOW = 1 and the difference is self explanatory.


If you are setting thresholds for more than one channel, then the second channel is defined in the same array as elements (6) through (11), the third is defined in elements (12) through (17)… I don’t know what would happen if you were to set the same channel with different thresholds in different parts of triggerChannelPropertiesArray but I suspect that it would be bad.

nProperties is a 16 bit integer that lets ps5000aWrap.dll know how many groups of 6 values to expect in triggerChannelPropertiesArray

autoTrig is simply the number of milliseconds that the PicoScope will wait before triggering by itself if no trigger has been encountered. Setting the value to 0 will prevent it from ever auto triggering.


Outstanding Issues:

All of these functions are deprecated. They’ve all been replaced by V2 versions of themselves but I haven’t had the time to detangle those versions. The fact is, to get to the point where I have an understanding of these functions and useable code rode the line between programming and hacking. I had to look at four different example code repositories to piece together what did what. If someone with greater skills, knowledge or abilities than I have could go through the V2 versions and do some kind of a writeup on them, that would help me tremendously. If you wanted to base it on this document, private message me and I can provide it as a Word or Pages file.

Also, it should be possible to manually pack a structure into a byte array, declare the byte array in the ps5000a.dll function declaration, and then hand the right number of bytes in, bypassing the ps5000aWrap.dll. While this isn’t strictly necessary, it would certainly be a very nice-to-have. I don’t want to sink the time into doing this for the deprecated versions of these functions.

I’ll try to get around to posting some functioning example code to either this forum or my GitHub but, of course if someone were to beat me to it, that would be greatly appreciated as well.

I hope that this helps other programmers. It’s correct as far as I know but if I have made any errors, please tell me so that I can correct them and, of course, feel free to post corrections to the forum.

-DAVIO

Post Reply