SCPI over Serial
Send SCPI commands over RS-232 serial with PySerial. Connect to bench instruments, query measurements, and handle errors.
Many bench instruments (multimeters, power supplies, oscilloscopes, signal generators) accept SCPI commands over RS-232. You send an ASCII string like *IDN? and read back the response.
Serial Settings for SCPI Instruments
SCPI over RS-232 varies by manufacturer. Check your instrument's manual for exact settings. These are the most common:
| Parameter | Typical value | Notes |
|---|---|---|
| Baud rate | 9600 or 19200 | Some instruments support 115200 |
| Data bits | 8 | Always 8 for SCPI |
| Parity | None | Some HP/Agilent instruments use odd parity |
| Stop bits | 1 | Rarely 2 |
| Flow control | None or RTS/CTS | Use RTS/CTS for instruments that support it |
| Termination | \n (LF) | Some use \r\n (CRLF) or \r (CR) |
Connect and Identify
import serial
ser = serial.Serial(
port="/dev/ttyUSB0",
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=2,
)
ser.write(b"*IDN?\n")
identity = ser.readline().decode().strip()
print(identity) # e.g. "Keysight Technologies,34461A,MY12345678,A.03.01"
ser.close()The *IDN? query is standard SCPI. Every compliant instrument responds with manufacturer, model, serial number, and firmware version.
Log instrument measurements automatically
TofuPilot records test results from your PySerial scripts, tracks pass/fail rates, and generates compliance reports. Free to start.
Send and Query Pattern
SCPI has two types of messages: commands (no response) and queries (end with ?, expect a response).
import serial
def scpi_command(ser, cmd):
"""Send a SCPI command (no response expected)."""
ser.write(f"{cmd}\n".encode())
def scpi_query(ser, cmd):
"""Send a SCPI query and return the response string."""
ser.write(f"{cmd}\n".encode())
return ser.readline().decode().strip()
ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=2)
# Configure a DC voltage measurement
scpi_command(ser, "CONF:VOLT:DC 10") # 10V range
scpi_command(ser, "TRIG:SOUR IMM") # Immediate trigger
# Take a measurement
voltage = scpi_query(ser, "MEAS:VOLT:DC?")
print(f"DC Voltage: {voltage} V")
# Read current
current = scpi_query(ser, "MEAS:CURR:DC?")
print(f"DC Current: {current} A")
ser.close()Error Checking
SCPI instruments maintain an error queue. After a sequence of commands, query SYST:ERR? to check for problems.
import serial
def check_errors(ser):
"""Read all errors from the instrument's error queue."""
errors = []
while True:
ser.write(b"SYST:ERR?\n")
response = ser.readline().decode().strip()
# Format: error_code,"error message"
# No error returns: +0,"No error"
if response.startswith("+0") or response.startswith("0"):
break
errors.append(response)
return errors
ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=2)
# Send some commands
ser.write(b"CONF:VOLT:DC 10\n")
ser.write(b"BOGUS:COMMAND\n") # This will cause an error
# Check for errors
errors = check_errors(ser)
if errors:
for err in errors:
print(f"Instrument error: {err}")
else:
print("No errors")
ser.close()Reset and Self-Test
Standard SCPI commands every instrument supports:
import serial
import time
ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=10)
# Reset to factory defaults
ser.write(b"*RST\n")
time.sleep(2) # Give the instrument time to reset
# Run self-test (0 = pass)
ser.write(b"*TST?\n")
result = ser.readline().decode().strip()
print(f"Self-test: {'PASS' if result == '0' else 'FAIL'}")
# Wait for all pending operations to complete
ser.write(b"*OPC?\n")
opc = ser.readline().decode().strip()
print(f"Operations complete: {opc}")
ser.close()Multiple Measurements
Read a batch of measurements in a loop. Add *OPC? synchronization if your instrument processes commands asynchronously.
import serial
import time
ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=5)
# Configure for DC voltage
ser.write(b"CONF:VOLT:DC AUTO\n")
readings = []
for i in range(10):
ser.write(b"READ?\n")
value = ser.readline().decode().strip()
try:
readings.append(float(value))
print(f"Reading {i+1}: {value} V")
except ValueError:
print(f"Reading {i+1}: invalid response '{value}'")
time.sleep(0.5)
if readings:
avg = sum(readings) / len(readings)
print(f"\nAverage: {avg:.6f} V")
print(f"Min: {min(readings):.6f} V")
print(f"Max: {max(readings):.6f} V")
ser.close()Termination Characters
The most common source of SCPI-over-serial problems is wrong line termination. If readline() hangs, the instrument isn't sending the character PySerial expects.
| Instrument family | Typical termination |
|---|---|
| Keysight/Agilent | \n (LF) |
| Tektronix | \n (LF) |
| Rohde & Schwarz | \n (LF) |
| Keithley | \r\n (CRLF) |
| older HP | \r (CR) or \r\n |
If readline() times out, try reading raw bytes to see what the instrument actually sends:
import serial
ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=3)
ser.write(b"*IDN?\n")
raw = ser.read(200)
print(repr(raw)) # Shows exact bytes including \r, \n
ser.close()PyVISA vs Raw PySerial for SCPI
Both work. Here's when to pick which:
| PySerial | PyVISA | |
|---|---|---|
| Install | pip install pyserial | pip install pyvisa pyvisa-py (or NI-VISA) |
| VISA resource string | N/A | ASRL/dev/ttyUSB0::INSTR |
| Termination handling | Manual (\n in your strings) | Automatic (read_termination property) |
| GPIB/USB-TMC/LAN | No | Yes |
| Multiple transport backends | Serial only | Serial, GPIB, USB, Ethernet, all through one API |
| Overhead | Minimal | Slightly more (VISA layer) |
Use PySerial when RS-232 is your only interface and you want minimal dependencies.
Use PyVISA when you also need GPIB, USB-TMC, or LAN instruments, or you want read_termination/write_termination to handle line endings for you. PyVISA uses PySerial under the hood for ASRL resources (when using the pyvisa-py backend).
import pyvisa
rm = pyvisa.ResourceManager("@py") # pyvisa-py backend (uses PySerial)
inst = rm.open_resource("ASRL/dev/ttyUSB0::INSTR")
inst.baud_rate = 9600
inst.read_termination = "\n"
inst.write_termination = "\n"
print(inst.query("*IDN?"))
inst.close()
rm.close()For dedicated PyVISA documentation, see the PyVISA docs.