If you’ve got some decent test equipment, chances are that you’re going to be automating it sooner or later. While I covered most of the basics of sending and receiving commands in my first tutorial, there comes a time when you need to delve a little deeper.
For example – ever wanted to know if your power supply was running in constant voltage or constant current mode? You’ll find there isn’t a direct command to query this, but instead, you get greeted by a rather complicated-looking SCPI Status Model diagram like this …
Confused? I was. When I first saw it, I decided I’d just infer what the supply is doing by reading the voltage and current because it seemed easier … but the proper way isn’t so difficult if you just spend a little time to learn about how the SCPI Status Model works.
This tutorial will try to explain how to make use of the SCPI Status Registers by covering some of the basics behind registers and bitwise operations, while demonstrating it in action on a Rohde & Schwarz NGM202 with some practical code examples. It will end by showing some key differences between instruments that users should be aware of. By the end of this, you should hopefully understand a lot more about how the SCPI Status Model works and be able to implement your own code to extract operational information about your instrument through pyvisa.
Disclaimer: All code provided is without warranty and is provided in good faith for the illustration of SCPI Status Model concepts. It may be freely copied, modified and used, however, I cannot be held responsible for any damage which may be incurred from use or inability to use the code regardless of how such damages are incurred. Use at your own risk.
What is a Register? A Primer on Bitwise Operations
This section should be rather self-explanatory to those with some basic programming knowledge, however, is provided as a primer for completeness. A register can be considered a memory location. The SCPI Status Model has a number of registers which indicate instrument status, operations and events. These registers can be queried through a number of commands and return a single number, usually representative of an 8-bit unsigned or 16-bit unsigned integer.
The registers values are bit-mapped, which means that each bit represents a particular state. For example, the table below shows the bit-map of the NGM202’s STATus:QUEStionable:INSTrument:ISUMmary<x>:COND registers:
This means that the returned value needs to be treated as binary and decomposed into its bits to determine the state of each of these parameters. This can be achieved through bitwise operations which include the AND, OR operators and techniques such as bitmasking.
Consider the hypothetical situation we read the STAT:QUES:INST:ISUM1:COND register and are returned a value of 2. What does this mean?
2 (decimal) = BIT 15 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0] BIT 0 (binary) 2^15 2^0 (bit value) 32768 1 (bit value evaluated)
For illustration, it would be easiest to convert the value into binary and examine the bits. Each bit position has a value of 2 to the power of the bit position, thus the first bit has a place value of 1, followed by 2, 4, 8, 16, 32, 64, 128, etc.
This is a very simple example – what it means is we have just one bit set at the 1st bit position (notice we count from 0 upward). This indicates the current bit is set, so the instrument is operating in constant voltage mode. This may seem strange but this is indicating there is a “fault” reaching the programmed current, so it is limited by voltage.
For reference, here is the bit-map for the STATus:OPERation:COND registers which can be decomposed in much the same way:
In this case, what if the response to a query of the STAT:OPER:INST:ISUM:COND registers and we are given a value of 5121?
2 (decimal) = BIT 15 [0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 1] BIT 0 (binary) 2^15 2^0 (bit value) 32768 1 (bit value evaluated)
This means we get a bit in the positions 0, 10 and 12, which means that the unit is in FastLog, is also logging and is also calibrating. It’s not a realistic condition to expect, but it illustrates how multiple conditions are conveyed in the single value we receive from a register query.
While this logic works fine for manually decomposing the register values, how can we do it programmatically in Python? This is where the bitwise operators and bit manipulations come into play. It will also be important to keep in mind that a zero value is interpreted as False, while a non-zero value is interpreted as True.
Lets first look at how we can identify if a given bit is set in a register value:
# A quick Python example to check if Bit 2 is set # by Gough Lui (goughlui.com) def testBit (value,position) : if value & (1<<position) : print("Bit "+str(position)+" is set for "+str(value)) else : print("Bit "+str(position)+" is not set for "+str(value)) for i in range (0,16) : testBit(i,2)
This simple program checks if bit 2 is set for values from 0 to 16 and prints the result to the console.
Bit 2 is not set for 0 Bit 2 is not set for 1 Bit 2 is not set for 2 Bit 2 is not set for 3 Bit 2 is set for 4 Bit 2 is set for 5 Bit 2 is set for 6 Bit 2 is set for 7 Bit 2 is not set for 8 Bit 2 is not set for 9 Bit 2 is not set for 10 Bit 2 is not set for 11 Bit 2 is set for 12 Bit 2 is set for 13 Bit 2 is set for 14 Bit 2 is set for 15
The function testBit accepts the value to be tested and the bit position to be tested. The “magic” is in the line
if value & (1<<position) :
which uses the magic of bit masking to perform the test. The expression (1<<position) generates the binary number corresponding to a given bit position by taking a 1 and shifting it left by position number of steps. In our case, 1<<2 will result in binary 100. The second part which is the bit masking exploits the AND operator which operates on a per-bit-position basis and returns a 1 if both inputs are true and 0 otherwise. Because we are AND-ing the value of value with binary 100, the output will be either all zero (if there is a 0 in the bit 2 position), or 100 (if there is a 1 in the bit 2 position). As we said earlier, a non-zero result is considered True, while a zero result is considered False, so removing the need to put an explicit comparison.
You could also easily change this to report all the set bits in a given value – for example
# A quick Python example to check which bits are set # by Gough Lui (goughlui.com) def findBits (value) : pos=0 while value >= (1<<pos) : if value & (1<<pos) : print("Bit "+str(pos)+" is set for "+str(value)) pos=pos+1 value=5121 findBits(value)
This simply goes through each bit position in ascending order until the value of the bit position is greater than the value put into the function, in which case it stops. The assumption is that the values are provided as unsigned integers. Using our example earlier, it correctly determines the bits set in a value of 5121:
Bit 0 is set for 5121 Bit 10 is set for 5121 Bit 12 is set for 5121
In general, there is no need to make such functions – you would just code the tests you would like to do in your code directly, but if desired, they could be used and modified to add the ability to return a value or list of values to your other Python code.
The SCPI Status Model – A Hierarchy of Events
With some basic binary arithmetic under our belts, we can now start to decipher the SCPI Status Model. In most cases, it is drawn as some sort of tree which can be quite complex to understand at a glance. As a result, I’ve tried to break it down a little and explain what each register means in plain English and illustrate it in a (vaguely) funnel-shaped diagram.
First, we consider the SCPI Status Model for a single-channel instrument, such as the Keithley 2450 SMU:
The diagram illustrates the status from the more detailed near the top and less detailed towards the bottom. Three different status types are grouped – the QUEStionable type, the OPERation type and the standard event type. These three types of status (along with some other inputs) are fed into the status byte (*STB) which is ANDed with the service request enable (*SRE) byte and the resulting bits are ORed together to generate the service request (SRQ) flag which will be discussed later. What is important to understand is that all of the status bits are essentially “filtered” through multiple steps to arrive at a few status bits in the status byte which eventually drives the service request flag.
Focusing first on the STATus:QUEStionable subsystem, this is concerned with reporting the status of the instrument channel. Within this subsystem there are three registers – the CONDition, the ENABle and the EVENt. The COND register reflects the current state of the channel – for example, is it currently CC, CV, etc. The ENAB register serves as a bit-mask which decides which events should be monitored for changes (usually, when a bit changes from 0 to 1). The EVEN register latches these changes since the last read based on which events are allowed by the ENAB register and is cleared whenever it is read. This means it forms a memory of if any changes have occurred since the last read. The bits of the EVEN register are OR-ed together to form bit 3 of the status byte – thus forming a summary of whether there have been any changes in all enabled parameters regarding the instrument channel. It should be noted that the EVENt keyword is often omitted entirely as it is in square brackets, thus STAT:OPER? is the same as STAT:OPER:EVEN? and not STAT:OPER:COND? as one might incorrectly assume.
The same structure is replicated with the STATus:OPERation subsystem, but this is concerned with instrument operations rather than channel status. This might include operations such as logging, calibration, etc. The logic of the three registers remains the same, except the summary bit is in bit 7 of the status byte.
Aside from these two detailed forms of status, there is also the standard event status byte (*ESR). This reports some of the common statuses, such as operation complete. This is masked by event status enable byte (*ESE) and this forms the event summary bit in bit 5 of the status byte. The standard event status is probably not all that useful in most cases (as there are dedicated queries, such as *OPC? to determine operation complete with most devices) but is perhaps good to be aware of.
Hopefully, based on that explanation, the structure and operation of the SCPI Status Model is now understood, because the model becomes more complicated when an instrument with multiple channels (e.g. the Rohde & Schwarz NGM202) is being considered.
In fact, it has become so big that I couldn’t fit it on one diagram, so we instead have two diagrams – one for the QUES status type and the other for the OPER status type.
In this model, now the status of the channel is pushed out into the STAT:QUES:INST:ISUM<x> and STAT:OPER:INST:ISUM<x> subsystems, where ISUM stands for instrument summary. The resulting statuses can be aggregated in two different ways because of this – for an aggregate summary of events on the whole channel, the STAT:QUES:INST and STAT:OPER:INST subsystems can be queried. This is summarised into one bit 13 of the STAT:QUES and STAT:OPER subsystems. If you want to aggregate statuses based on event type across all channels, then you need to use the STAT:QUES and STAT:OPER subsystems which still drive the status bit 3 in the status byte. The hierarchy is hence a little more complex in this case, but it still works in a very similar way.
Why is the Status Model Like This?
Now that the status model has been introduced and explained, perhaps it is worth pondering why it is designed this way. While I don’t know for sure, it is likely to be related to the GPIB bus and the design of interrupt-enabled computer peripherals as a whole.
Remember when I mentioned that the whole model eventually summarises through multiple layers of bit-masks and ORs into a single bit – the service request (SRQ) flag. The GPIB bus itself has a service request line which is a physical line used by any instrument on the GPIB bus to tell the host that they need attention. When a device’s status model results in asserting the service request line, it acts like an interrupt to the host, grabbing its attention. Most hosts (when enabled) perform a procedure known as serial poll which instructs all devices to prepare their status bytes for transmission onto the bus once addressed so the controller can figure out which instrument(s) requested service.
The process of handling such a service request begins to look very similar (logically) speaking to handling an interrupt request on an embedded microprocessor. Usually you get an interrupt, then you need to figure out who caused the interrupt, what in that device caused the interrupt and handle it accordingly. In this case, you can identify which instrument requested attention, so you need to read through the event registers going “up” to the more detailed status to narrow down which operation type or condition type and which channel was responsible. With some instruments, the conditions can be very flexible which can provide a lot of interesting possibilities to improve communications between the host and instrument.
This is why most programs you might encounter in pseudo-code in manufacturer’s manuals typically set up the whole tree and will test the status byte, then test the various event registers, right down to the ISUM level before handling the condition. For some simple applications, doing all of this may not be necessary at all (and ironically, could be less efficient), but handling the condition in this way is most flexible in case you need to handle other types of events in the future.
This type of service-request handling is perhaps most applicable to GPIB-based systems but should still work across interfaces which can pass such bus status messages (e.g. USB-488, VXI-11, HiSLIP) but with the limitation that some of these interfaces do not support true asynchronous interrupt operation. It does not work on interfaces which only pass data messages such as RS-232/serial or TCP sockets. It is still possible to determine the SRQ status by querying the status byte registers on those devices (e.g. by using *STB?) which is treated as a regular common command, but on some instruments, this could have undesirable side effects such as overwriting the output buffer and would not perform as well as an interrupt-style event would.
Example 1: Interpret Channel Status
Perhaps a good example to begin with is just to be able to read and interpret the condition of the two channels on the NGM202. In this code example, it configures both outputs to 1V/0.05A and switches them on. Every 250ms or so, it queries the condition bits for the questionable register and decodes the output to human-readable text by finding the set bits (findBits) and iterating through the set of bits through decodeQues.
# A quick Python example to check questionable condition bits # by Gough Lui (goughlui.com) import pyvisa import time resource_manager = pyvisa.ResourceManager() # You can change the variable name and resource name ins_ngm202 = resource_manager.open_resource("TCPIP0::192.168.80.18::inst0::INSTR") def findBits (value) : pos=0 setbits=[] while value >= (1<<pos) : if value & (1<<pos) : setbits.append(pos) pos=pos+1 return setbits def decodeQues (bit) : if bit == 0 : return "Voltage" elif bit == 1 : return "Current" elif bit == 4 : return "Temperature Overrange" elif bit == 9 : return "OVP Tripped" elif bit == 10 : return "Fuse Tripped" else : return "Unknown State" # Setup both channels for 1V, 0.05A, Outputs On print("Setting up NGM202 - 1V/0.05A/ON CH1 & CH2") ins_ngm202.write("INST:NSEL 1") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.write("INST:NSEL 2") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.query("*OPC?") # Query Status Approximately Every 250ms while True : ch1Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM1:COND?")[0]) ch2Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM2:COND?")[0]) print("At "+str(time.time())+" status values were "+str(ch1Ques)+","+str(ch2Ques)) print("CH1 Status: ",end="") for bit in findBits(ch1Ques) : print(decodeQues(bit),end=" ") print("CH2 Status: ",end="") for bit in findBits(ch2Ques) : print(decodeQues(bit),end=" ") print("") time.sleep(0.25)
Running this code with both channels shorted, the channel 2 being open made open by disconnecting the jumper wire, the output would look as follows:
At 1621045876.3472614 status values were 1,1 CH1 Status: Voltage CH2 Status: Voltage At 1621045876.6108997 status values were 1,1 CH1 Status: Voltage CH2 Status: Voltage At 1621045876.8745422 status values were 1,2 CH1 Status: Voltage CH2 Status: Current At 1621045877.1388226 status values were 1,2 CH1 Status: Voltage CH2 Status: Current
To ensure it is actually capable of decoding everything, I short the output of both outputs again but enable the fuse feature on channel 2 with a five second delay and enable logging. If you wish to automatically configure the fuse, you will need to add the following SCPI commands to the initialisation:
CURR:PROT:DEL:INIT 5
CURR:PROT:DEL 5
CURR:PROT 1
The output looks as follows:
At 1621061978.7802002 status values were 1,1 CH1 Status: Voltage CH2 Status: Voltage At 1621061979.0427673 status values were 1,1 CH1 Status: Voltage CH2 Status: Voltage At 1621061979.3059003 status values were 1,1026 CH1 Status: Voltage CH2 Status: Current Fuse Tripped At 1621061979.570686 status values were 1,1026 CH1 Status: Voltage CH2 Status: Current Fuse Tripped
At this point in time, a question you might be thinking of is why are there two bits – one for voltage and one for current? If the supply is not operating in constant current (i.e. voltage bit set), it would be operating in constant voltage (i.e. current bit set), so we could theoretically save a bit entirely and just look at one or the other. While this might be true in the majority of cases, there is an edge case that can occur when the load is perfectly matched to the voltage and current setting on a power supply where neither voltage/current fault exists. This state does exist even if it is transient – I’ve seen it with my own eyes while reviewing the Keysight E36103A power supply where the screen displays “Unreg” for the mode, as the supply really isn’t doing any regulation at all.
Example 2: Catch Individual Channel Events
A naive question may be why we would bother with the EVENt registers when you can read the condition bits directly from the CONDition register – surely if you keep reading the CONDition registers, you will know everything you need to know, right?
Well, the problem is that some events can be very transient and communication between the host and instrument can be quite slow. For example, an SMU may go into a constant current condition for a period of a few tens of microseconds and then return to constant voltage. If you were to poll only the CONDition bits, you would probably miss it. The EVENt register would have latched this event though!
This code example instead tries to log any changes in channel state when they occur based on reading the EVENt register. It can only do this because it sets up the ENABle register beforehand to catch the kind of events it would want to catch.
# A quick Python example to catch individual channel events # by Gough Lui (goughlui.com) import pyvisa import time resource_manager = pyvisa.ResourceManager() # You can change the variable name and resource name ins_ngm202 = resource_manager.open_resource("TCPIP0::192.168.80.18::inst0::INSTR") def findBits (value) : pos=0 setbits=[] while value >= (1<<pos) : if value & (1<<pos) : setbits.append(pos) pos=pos+1 return setbits def decodeQues (bit) : if bit == 0 : return "Voltage" elif bit == 1 : return "Current" elif bit == 4 : return "Temperature Overrange" elif bit == 9 : return "OVP Tripped" elif bit == 10 : return "Fuse Tripped" else : return "Unknown State" # Setup both channels for 1V, 0.05A, Outputs On print("Setting up NGM202 - 1V/0.05A/ON CH1 & CH2") ins_ngm202.write("INST:NSEL 1") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.write("INST:NSEL 2") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.query("*OPC?") # Configure the ENABle register based on all possible events ins_ngm202.write("STAT:QUES:INST:ISUM1:ENAB "+str(int((1<<10)|(1<<9)|(1<<4)|(1<<1)|(1<<0)))) ins_ngm202.write("STAT:QUES:INST:ISUM2:ENAB "+str(int((1<<10)|(1<<9)|(1<<4)|(1<<1)|(1<<0)))) # Clear the events in both registers ins_ngm202.query("STAT:QUES:INST:ISUM1?") ins_ngm202.query("STAT:QUES:INST:ISUM2?") # Query Events Continuously - Print when Event Happens while True : ch1Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM1?")[0]) ch2Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM2?")[0]) if ch1Ques | ch2Ques : print("Event Captured at "+str(time.time())+" which was a change in ",end="") if ch1Ques : print("CH1 ",end="") for bit in findBits(ch1Ques) : print(decodeQues(bit),end=" ") ch1Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM1:COND?")[0]) print("New Status: ",end="") for bit in findBits(ch1Ques) : print(decodeQues(bit),end=" ") if ch2Ques : print("CH2 ",end="") for bit in findBits(ch2Ques) : print(decodeQues(bit),end=" ") ch2Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM2:COND?")[0]) print("New Status: ",end="") for bit in findBits(ch2Ques) : print(decodeQues(bit),end=" ") print("")
This code polls the two EVENt registers continuously, only executing the rest of the code when the registers indicate any changes in status. When any change is detected, the time is printed along with which bits changed and the new status at that point. To make sure it operates correctly, however, one must be sure to read both registers after setting up the ENABle register otherwise there may be “old” events in the register.
Running this code should produce output which looks as follows (noting I have configured manually a fuse trip on Channel 2 and am pulling the jumper cable on Channel 1):
Event Captured at 1621067745.6039796 which was a change in CH1 Current New Status: Current Event Captured at 1621067747.301403 which was a change in CH1 Voltage New Status: Voltage Event Captured at 1621067747.3381453 which was a change in CH2 Voltage Current Fuse Tripped New Status: Voltage Fuse Tripped Event Captured at 1621067747.3629065 which was a change in CH2 Current New Status: Current Fuse Tripped
This demonstrates the power of the event register as it seems things can happen fast during output transitions. The event where I unplugged the jumper and plugged it back in on Channel 1 can be seen. The fuse trip event happened almost immediately after and initially resulted in changes to both voltage and current bits even though reading it seems to show that only the voltage bit was set. This indicates that in that time between polls, the current bit would have toggled state from zero to one and back to zero. The last line shows the current bit returning to one while the voltage bit changes to zero.
This also illustrates a key point – when setting the ENABle bits, this only causes events to be generated when the corresponding condition bit goes from zero to one. But what if we are monitoring status like a temperature overrange (bit 4) and we want to know when the channel recovers to normal temperature? We could poll the CONDition register continuously to see if it’s back at zero again and this may be necessary for some devices.
But the Rohde & Schwarz NGM202 has another trick up its sleeve – the ability to filter for positive, negative or both transitions. Setting the ENABle register is equivalent to setting the PTRansition filter, but if you want to register the opposite transition, you could use the NTRansition filter. Code that sets up the filter for the temperature overrange on both edges would look like this:
ins_ngm202.write("STAT:QUES:INST:ISUM1:ENAB "+str(int(1<<4))) ins_ngm202.write("STAT:QUES:INST:ISUM1:PTR "+str(int(1<<4))) ins_ngm202.write("STAT:QUES:INST:ISUM1:NTR "+str(int(1<<4))) ins_ngm202.write("STAT:QUES:INST:ISUM2:ENAB "+str(int(1<<4))) ins_ngm202.write("STAT:QUES:INST:ISUM2:PTR "+str(int(1<<4))) ins_ngm202.write("STAT:QUES:INST:ISUM2:NTR "+str(int(1<<4)))
Note that setting the PTR register bits may be redundant as they are set by the ENAB command that precedes it, but it may be good practice to be explicit anyway if intending to use the filters.
Example 3: Catch Multiple Channel Events
While the code in the previous sections may have worked, it may have been noticed that it seems to be doing things a little repetitively – it’s checking the EVENt register for each channel individually. Wouldn’t it be great if we could avoid this altogether? Yes – and we can do this thanks to the magic of the hierarchical nature of the SCPI Status Model.
In this example, I go one layer above to the STATus:QUEStionable:INSTrument level so as to get an overview of which channels have events since the last check, so I can know directly which channel had an event. This can reduce overheads and improve performance.
# A quick Python example to catch multiple channel events # by Gough Lui (goughlui.com) import pyvisa import time resource_manager = pyvisa.ResourceManager() # You can change the variable name and resource name ins_ngm202 = resource_manager.open_resource("TCPIP0::192.168.80.18::inst0::INSTR") def findBits (value) : pos=0 setbits=[] while value >= (1<<pos) : if value & (1<<pos) : setbits.append(pos) pos=pos+1 return setbits def decodeQues (bit) : if bit == 0 : return "Voltage" elif bit == 1 : return "Current" elif bit == 4 : return "Temperature Overrange" elif bit == 9 : return "OVP Tripped" elif bit == 10 : return "Fuse Tripped" else : return "Unknown State" # Setup both channels for 1V, 0.05A, Outputs On print("Setting up NGM202 - 1V/0.05A/ON CH1 & CH2") ins_ngm202.write("INST:NSEL 1") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.write("INST:NSEL 2") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.query("*OPC?") # Configure the STAT:QUES:INST:ISUM registers based on all possible events ins_ngm202.write("STAT:QUES:INST:ISUM1:ENAB "+str(int((1<<10)|(1<<9)|(1<<4)|(1<<1)|(1<<0)))) ins_ngm202.write("STAT:QUES:INST:ISUM2:ENAB "+str(int((1<<10)|(1<<9)|(1<<4)|(1<<1)|(1<<0)))) # Configure the STAT:QUES:INST register - Note Ch1 is bit 1, Ch2 is bit 2 # as bit 0 is used for "extension" (Ch 15+) ins_ngm202.write("STAT:QUES:INST:ENAB "+str(int((1<<1)|(1<<2)))) # Clear the events in all EVENt registers ins_ngm202.query("STAT:QUES:INST:ISUM1?") ins_ngm202.query("STAT:QUES:INST:ISUM2?") ins_ngm202.query("STAT:QUES:INST?") # Query Events Continuously - Print when Event Happens while True : chXQues=int(ins_ngm202.query_ascii_values("STAT:QUES:INST?")[0]) if chXQues & ((1<<1)|(1<<2)) : # If either or both channels have events print("Event Captured at "+str(time.time())+" which was a change in ",end="") if chXQues & (1<<1) : # Channel 1 has an event ch1Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM1?")[0]) print("CH1 ",end="") for bit in findBits(ch1Ques) : print(decodeQues(bit),end=" ") ch1Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM1:COND?")[0]) print("New Status: ",end="") for bit in findBits(ch1Ques) : print(decodeQues(bit),end=" ") if chXQues & (1<<2) : # Channel 2 has an event ch2Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM2?")[0]) print("CH2 ",end="") for bit in findBits(ch2Ques) : print(decodeQues(bit),end=" ") ch2Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM2:COND?")[0]) print("New Status: ",end="") for bit in findBits(ch2Ques) : print(decodeQues(bit),end=" ") print("")
The logic changes slightly to make use of the additional information – it first checks if any channels need attention. Only if one or more channels need attention, then the channel in question is checked and acted upon. A key trap is that the instrument bits are bit 1 for channel 1 and bit 2 for channel 2, as bit 0 is used as an extension bit for channel 15 and above.
The result of registering the fuse trip and disconnection/re-connection of the jumper cable is identical, although the way it polls the instrument for events is more efficient.
Event Captured at 1621069811.4688902 which was a change in CH1 Current New Status: Current Event Captured at 1621069812.626724 which was a change in CH1 Voltage New Status: Voltage Event Captured at 1621069814.0642164 which was a change in CH2 Fuse Tripped New Status: Voltage Fuse Tripped Event Captured at 1621069814.0749745 which was a change in CH2 Voltage Current New Status: Voltage Fuse Tripped Event Captured at 1621069814.0862088 which was a change in CH2 Current New Status: Current Fuse Tripped
Example 4: Propagate Events to the Status Byte
If I haven’t already lost you in a sea of confusion, then congratulations because we’ve almost reached the “pot of gold”. The goal of navigating the errors through the model is often to propagate them to the status byte which we can get an “aggregate” look at if the instrument needs any attention, and use this to drive the service request (SRQ) line to grab the attention of the host.
To push the error all the way up, we will need to navigate another layer or two up the hierarchy – to the STATus:QUEStionable level, then to the *STB/*SRE level. This is rather straightforward to do.
# A quick Python example illustrating status byte serial poll # by Gough Lui (goughlui.com) import pyvisa import time resource_manager = pyvisa.ResourceManager() # You can change the variable name and resource name ins_ngm202 = resource_manager.open_resource("TCPIP0::192.168.80.18::inst0::INSTR") def findBits (value) : pos=0 setbits=[] while value >= (1<<pos) : if value & (1<<pos) : setbits.append(pos) pos=pos+1 return setbits def decodeQues (bit) : if bit == 0 : return "Voltage" elif bit == 1 : return "Current" elif bit == 4 : return "Temperature Overrange" elif bit == 9 : return "OVP Tripped" elif bit == 10 : return "Fuse Tripped" else : return "Unknown State" # Setup both channels for 1V, 0.05A, Outputs On print("Setting up NGM202 - 1V/0.05A/ON CH1 & CH2") ins_ngm202.write("INST:NSEL 1") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.write("INST:NSEL 2") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.query("*OPC?") # Configure the STAT:QUES:INST:ISUM registers based on all possible events ins_ngm202.write("STAT:QUES:INST:ISUM1:ENAB "+str(int((1<<10)|(1<<9)|(1<<4)|(1<<1)|(1<<0)))) ins_ngm202.write("STAT:QUES:INST:ISUM2:ENAB "+str(int((1<<10)|(1<<9)|(1<<4)|(1<<1)|(1<<0)))) # Configure the STAT:QUES:INST register - Note Ch1 is bit 1, Ch2 is bit 2 # as bit 0 is used for "extension" (Ch 15+) ins_ngm202.write("STAT:QUES:INST:ENAB "+str(int((1<<1)|(1<<2)))) # Configure the STAT:QUES register - Note Bit 13 is used for summary of STAT:QUES:INST ins_ngm202.write("STAT:QUES:ENAB "+str(int(1<<13))) # Configure generation of SRQ - bit 3 is used for STAT:QUES ins_ngm202.write("*SRE "+str(int(1<<3))) # Clear the events in all EVENt registers ins_ngm202.query("STAT:QUES:INST:ISUM1?") ins_ngm202.query("STAT:QUES:INST:ISUM2?") ins_ngm202.query("STAT:QUES:INST?") ins_ngm202.query("STAT:QUES?") # Query Events Continuously - Print when Event Happens while True : stByte=ins_ngm202.stb # By Serial Polling # stByte=int(ins_ngm202.query_ascii_values("*STB?")[0]) # By Polling the Status Byte if stByte & (1<<3) : chXQues=int(ins_ngm202.query_ascii_values("STAT:QUES:INST?")[0]) if chXQues & ((1<<1)|(1<<2)) : # If either or both channels have events print("Event Captured at "+str(time.time())+" which was a change in ",end="") if chXQues & (1<<1) : # Channel 1 has an event ch1Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM1?")[0]) print("CH1 ",end="") for bit in findBits(ch1Ques) : print(decodeQues(bit),end=" ") ch1Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM1:COND?")[0]) print("New Status: ",end="") for bit in findBits(ch1Ques) : print(decodeQues(bit),end=" ") if chXQues & (1<<2) : # Channel 2 has an event ch2Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM2?")[0]) print("CH2 ",end="") for bit in findBits(ch2Ques) : print(decodeQues(bit),end=" ") ch2Ques=int(ins_ngm202.query_ascii_values("STAT:QUES:INST:ISUM2:COND?")[0]) print("New Status: ",end="") for bit in findBits(ch2Ques) : print(decodeQues(bit),end=" ") print("")
The code configures the propagation of the error up the STAT:QUES level (bit 13), into the status byte (bit 3) which is then configured to generate SRQ. The code uses the serial polling method to check if the status byte indicates an event has occurred – this is the instrument’s way of grabbing the host’s attention. A commented line shows an alternative “manual” polling of the status byte which is probably less efficient.
Of course, the code still does the same thing, so the code result is essentially identical:
Event Captured at 1621070890.7111063 which was a change in CH1 Current New Status: Current Event Captured at 1621070891.6278923 which was a change in CH1 Voltage New Status: Voltage Event Captured at 1621070891.669559 which was a change in CH1 Current New Status: Current Event Captured at 1621070891.6848423 which was a change in CH1 Voltage New Status: Voltage Event Captured at 1621070893.2781715 which was a change in CH2 Voltage Current Fuse Tripped New Status: Voltage Fuse Tripped Event Captured at 1621070893.300663 which was a change in CH2 Current New Status: Current Fuse Tripped
Example 5: Operation Status
While a lot has been said about the channel condition (questionable) status, not much has been said about the operation status registers. From the manual which lists commands, it seems that the structure of the registers is essentially the same, but there is no information about any of the STAT:QUES:INST or STAT:QUES:INST:ISUM levels. While they do exist, their meanings are not known and cannot be easily inferred – but we don’t need them as the key information can be found in the STATus:OPERation level.
# A quick Python example to check operation condition bits # by Gough Lui (goughlui.com) import pyvisa import time resource_manager = pyvisa.ResourceManager() # You can change the variable name and resource name ins_ngm202 = resource_manager.open_resource("TCPIP0::192.168.80.18::inst0::INSTR") def findBits (value) : pos=0 setbits=[] while value >= (1<<pos) : if value & (1<<pos) : setbits.append(pos) pos=pos+1 return setbits def decodeOper (bit) : if bit == 0 : return "Calibrating" elif bit == 10 : return "Logging" elif bit == 12 : return "FastLog" else : return "Unknown State" # Setup both channels for 1V, 0.05A, Outputs On print("Setting up NGM202 - 1V/0.05A/ON CH1 & CH2") ins_ngm202.write("INST:NSEL 1") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.write("INST:NSEL 2") ins_ngm202.write("SOUR:VOLT 1") ins_ngm202.write("SOUR:CURR 0.05") ins_ngm202.write("OUTP 1") ins_ngm202.query("*OPC?") # Query Status Approximately Every 250ms while True : chOper=int(ins_ngm202.query_ascii_values("STAT:OPER:COND?")[0]) print("At "+str(time.time())+" status values were "+str(chOper)) print("Status: ",end="") for bit in findBits(chOper) : print(decodeOper(bit),end=" ") print("") time.sleep(0.25)
This example is very similar to the first example, just that instead of looking at the questionable registers, we are looking at the operation registers and decoding them as such. To test this, I enable logging on the front panel and can see the resulting decoded output as follows:
At 1621071454.6124895 status values were 0 Status: At 1621071454.8669236 status values were 0 Status: At 1621071455.120687 status values were 0 Status: At 1621071455.3768692 status values were 1024 Status: Logging At 1621071455.6319375 status values were 1024 Status: Logging At 1621071455.88841 status values were 1024 Status: Logging
Propagating any operational statuses to the status byte is very similar, just that it controls bit 7 of the status byte instead.
What About The Other Status Byte?
In my earlier status model diagrams, I had illustrated in purple, another type of status byte – the standard event status byte (*ESR) and its corresponding enable (*ESE). This particular register does not seem to be specifically documented for the NGM202. This is likely because this is a standard register which is part of the SCPI standard – indeed it is rarely detailed by instrument datasheets as a whole.
But referring to this reference from Keysight, the bits of the *ESR correspond to:
- Bit 0 – Operation Complete
- Bit 1 – Request Control
- Bit 2 – Query Error
- Bit 3 – Device Dependent Error
- Bit 4 – Execution Error
- Bit 5 – Command Error
- Bit 6 – User Request Key (Local)
- Bit 7 – Power On
But before placing too much weight on this information, one must remember a caveat of all SCPI instruments …
Caveat Emptor: Instruments ARE different –
Read the Manual, Trust But Verify!
The SCPI standard has done a lot in unifying the command syntax and standard commands across different types of instruments from almost all leading vendors, but programmers venturing into instrument automation would be mistaken to think that things should just work.
Different instruments are likely to implement things in different ways. For example, some instruments may not implement all of the error types in the standard event status byte – testing for them will never give any result. As noted in the earlier section on the status model, differences between single and multiple-channel instruments result in additional status model complexities. In the case of this instrument, the NGM202, while some of the lower hierarchy levels in the STAT:OPER:INST exist, their meanings are not documented. As a result, sometimes it will take some reading of the manual to fully understand how to use a given device’s status model and what it is capable of. But that’s not to say that the manual is infallible – the are occasionally minor mistakes in labelling which can lead to confusion as well. This means it’s inevitable that users are going to have to experiment to work around quirks and get the most from their instruments.
Something of note is the Keithley 2450 SMU’s SCPI Status Model diagram:
It’s quite well drawn and relatively easy to understand, however, there is a key difference between this model and the NGM202’s and that’s not just because this is a single channel device.
The Keithley 2450 SMU is such a complex device that the number of conceivable errors and statuses have outgrown what can be reasonably allocated to the fifteen usable condition status bits – so they have taken a different approach. Internally, the SMU generates events by an event number. Certain event numbers can be mapped to a condition status bit to either set or clear the corresponding bit, thus creating a flexible status model where the definition of what each bit means can be customised to meet the users’ needs. This is achieved through the STAT:OPER:MAP and STAT:QUES:MAP commands which accept the bit number, set event and clear event as parameters.
By contrast, the status model for a Keithley 2110 digital multimeter is quite a bit simpler – there is no operation register at all, instead, everything is in the questionable register. Some of the simplest devices may have no status model at all, while some like the Tektronix PA1000 seem to deviate from the norm with their own DSR/DSE registers:
Regardless of what the registers are called, the way they operate conceptually is still the same.
Conclusion
Getting started with automating SCPI instruments is not difficult, but sometimes simple questions such as “is my channel operating in constant current or constant voltage?” result in the need to understand the intimidating-looking SCPI Status Model.
Regardless of how it is depicted, it is a hierarchy of bit-mapped registers which represent a condition, bit-mask and latched events which are then used to drive the next stage of the hierarchy until everything is summarised into a single status request bit. Understanding how these can be used to detect states and transitions in states efficiently is not so difficult once you understand the basics.
This post is my attempt to condense my journey in learning about and implementing code that uses the SCPI Status Model in a way that is (hopefully) easier to understand and more practical than the limited guidance given in manuals. The provided Python code examples using pyvisa should serve as good starting points to illustrate the relevant registers and values necessary to achieve certain aims, even if they are not entirely realistic for production environments.
I hope this has gone some way to demystifying the SCPI Status Model and status registers – best of luck with your test equipment automation journey.