Controlling GPIB instruments from C with GPIBEE and Code::Blocks

Overview

Most of the examples on this site drive instruments from Python. This one is for the other case: a C program on Windows that opens an instrument through GPIBEE, sends SCPI commands and reads the answers back. No Python, no runtime to ship, just an .exe.

You do not have to write it from scratch. A complete Code::Blocks project is ready to download below. It builds out of the box, queries *IDN? and prints the reply. From there you change one line to point at your own instrument and start adding commands.

The example talks to the instrument through a small helper layer called XyGPIB, which comes with it. That is example code rather than a library you have to adopt: it exists so a first program does not have to meet the full VISA API head-on. Calling VISA directly works just as well, and the section near the end explains the trade.

By the end of this page you will have a working C program talking to an instrument, and you will know the handful of functions needed to write your own.

Why Code::Blocks

Code::Blocks is a small, self-contained C and C++ IDE that runs on Windows, Linux and macOS. It has been around a long time, it is free and open source, and it does not want to be anything more than an IDE: no account, no telemetry, no plugin marketplace to work through before you can compile something.

What makes it a good fit for a tutorial like this one is the Windows installer that bundles its own compiler. On most other toolchains, "install the IDE" and "install a working C compiler" are two separate projects, and the second one is where people give up. Here they are one download.

It is also genuinely light. The whole thing installs in under a minute, starts instantly, and runs comfortably on a laptop that would struggle with a modern IDE. If your normal environment is Visual Studio or VS Code, the sources in the project are plain C11 and will build there just as well. Code::Blocks is the path of least resistance, not a requirement.

Prerequisites

  • A GPIBEE on the network, with an instrument on the GPIB bus.
  • Windows. The example project targets 64-bit Windows.
  • A VISA implementation installed on the PC. See the next section.
  • Code::Blocks with a C compiler. See further below if you do not have it.

Install a VISA

VISA is the standard API that sits between your program and the instrument. Your code says "open this resource, write this string, read the answer", and VISA takes care of the transport underneath, whether that is LAN, USB or a GPIB card. GPIBEE presents itself as a VXI-11 instrument on the network, so any VISA can talk to it.

VISA is not part of Windows, and it is not bundled with Code::Blocks. You have to install one, and the example needs it only to run, not to build.

Recommended:R&S VISA from Rohde & Schwarz. It is free, the download does not require a customer account, and it installs a useful GUI tool alongside it that you will want in a moment to find your instrument's resource name. Our Installing & Using a VISA tutorial walks through the install screen by screen and then through the R&S VISA Tester.

NI-VISA and the Keysight IO Libraries Suite work equally well. If one of them is already on the machine, use it and skip ahead.

Install Code::Blocks

Skip this if you already have Code::Blocks with a working compiler. Otherwise, download it from the official binaries page:

The page offers several files, and the choice matters. Take codeblocks-25.03mingw-setup.exe from the "Microsoft Windows (64 bit, default)" section:

The Code::Blocks Windows download list with codeblocks-25.03mingw-setup.exe highlighted
The file to pick. The others install the IDE without a compiler.

The mingw in the name is the important part: this build ships with the GCC compiler and the GDB debugger included, taken from the WinLibs project. Install it and you are done. There is nothing else to fetch, no toolchain to configure, no environment variable to set. Code::Blocks finds the bundled compiler on first start and selects it automatically.

The plain codeblocks-25.03-setup.exe gives you the editor with no compiler behind it. It is the right choice if you already run your own toolchain, and a frustrating one if you do not.

Either mirror is fine:the page offers each file from two hosts. They serve the same installer.

Download the example

CodeBlocksGpibeeExample.zip
Complete Code::Blocks project in plain C. Builds with no VISA SDK, headers or import library installed.
⤓ Download the project

Unpack it somewhere convenient. Inside you will find:

FileWhat it is
main.cThe example program. Loads VISA, opens the instrument, queries *IDN?, prints the answer, closes. This is the file you edit.
XyGPIB.h / .cThe helper API described on this page. The only header your own code needs to include.
visa_interface.h / .cThe VISA runtime loader underneath XyGPIB. Used internally only. You should not need to open it.
CodeBlocksGpibeeExample.cbpThe Code::Blocks project, with Debug and Release targets.
README.mdThe same ground as this page, plus notes on how the code was verified.
No VISA SDK needed to build:visa_interface.h declares the few VISA types it uses itself instead of including the IVI Foundation headers, and the VISA DLL is loaded at run time rather than linked. So the project compiles on a machine with no VISA installed at all. VISA only becomes a requirement when you run the program.

Finding your way around Code::Blocks

Skip this section if you have used an IDE before. If you have not, it is four things.

Open the project, not the source file

The file to open is CodeBlocksGpibeeExample.cbp. The .cbp extension means Code::Blocks Project, and it is the file that ties the five sources together and carries the compiler settings. Double-click it, or use File → Open inside Code::Blocks.

Opening main.c on its own also works, but then Code::Blocks has a lone file rather than a project, and the Build commands have nothing to build. If the title bar does not say [CodeBlocksGpibeeExample], that is what happened.

The Management panel on the left

Once the project is open, the Management panel lists everything in it, split into Sources and Headers. Double-click any entry to open it in the editor. main.c is the one you want.

Code::Blocks with the project open, the Management panel listing Sources and Headers, and the green Run arrow in the toolbar
The project open, with the Management panel on the left and the green Run arrow in the toolbar.

The Run button

The green arrow in the toolbar, marked Run in the screenshot above, starts the program that was last built. It opens a console window, runs the executable and holds the window open at the end so you can read the output before it disappears.

Run on its own does not compile anything. If you have just edited main.c, it will happily run the previous build and leave you wondering why your change did nothing. Use Build and run instead, which is the next item along.

The Build menu

Everything to do with compiling lives under Build:

The Code::Blocks Build menu, showing Build, Compile current file, Run, Build and run, Rebuild and Clean with their keyboard shortcuts
The Build menu. Build and run (F9) is the one you will use most.
CommandShortcutWhen to use it
Build and runF9Compiles anything that changed, then runs it. This is the normal one.
BuildCtrl-F9Compile only, without running. Useful for checking that an edit compiles.
RunCtrl-F10Runs the last build without compiling.
RebuildCtrl-F11Throws away all previous output and builds everything from scratch. Reach for it when the build behaves oddly after changing settings.
CleanDeletes the build output and stops. The next build then starts fresh.
Compile current fileCtrl-Shift-F9Compiles just the open file. Handy in a large project, unnecessary in this one.

Results appear in the Build log at the bottom of the window. Warnings are normal and are printed in the log; the line that matters is the final one, which reports the number of errors and warnings.

Set your instrument address

Open main.c. Near the top there is one line you have to change:

/* Adjust to the instrument on your network. */
#define INSTR_RESOURCE   "TCPIP::192.168.3.2::inst0,4::INSTR"

That string is a VISA resource name. Read from the left it says: reach the device over TCP/IP, at address 192.168.3.2, and talk to GPIB address 4 behind it. The 192.168.3.2 is the GPIBEE itself, and the 4 is the instrument sitting on the GPIB bus. Both parts will almost certainly differ on your bench.

You do not have to construct that string by hand. The R&S VISA Tester, installed alongside R&S VISA, will find it for you: it searches the network, lists what it finds, and hands you the exact resource name to paste. The Installing & Using a VISA tutorial covers that search step by step, including the GPIB scan that shows which addresses are occupied.

Example, instrument at GPIB address 22 on a GPIBee at 192.168.1.50
TCPIP::192.168.1.50::inst0,22::INSTR

The timeout on the next line is in milliseconds and applies to every read and write on the session. Three seconds suits most instruments. Raise it if you are triggering long measurements, since a sweep that takes ten seconds to complete needs a timeout longer than the sweep.

#define IO_TIMEOUT_MS    3000

Build and run

Press F9 (Build and run). The project has Debug and Release targets; either is fine to start with.

A console window opens, and a successful run looks like this:

Console output showing VISA loaded from visa64.dll, the resource being opened, and an IDN response from a HEWLETT-PACKARD 34401A
Four lines, and the last one is the instrument answering for itself.

Reading down: VISA was found and loaded, the resource opened, and the instrument replied to *IDN? with its manufacturer, model, serial number and firmware revision. Process returned 0 is Code::Blocks reporting the exit code, and the prompt to press a key is what keeps the window from vanishing before you can read it.

If you get that far, the whole chain is working: your compiler, VISA, the network, the GPIBEE, the GPIB bus and the instrument. Everything after this is just sending different commands.

When it does not work

If the GPIBEE cannot be reached, or there is no instrument at the GPIB address you asked for, the open fails and you see this instead:

Console output showing viOpen failed with VISA status 0xBFFF0011, and Failed to open the resource
A failed open. The middle line is the VISA error, printed because the example switches debug output on.

Two lines are worth reading carefully:

  • viOpen failed: VISA status 0xBFFF0011 (Insufficient location information or the device or resource is not present in the system.) comes from XyGPIB_SetDebug(true). That is VISA's own wording, decoded for you. Without debug output switched on you would only get the line below it.
  • Failed to open "TCPIP::192.168.3.2::inst0,4::INSTR" is the example's own message, printed because XyGPIB_Open() returned NULL.

Despite its wording, 0xBFFF0011 nearly always means one of two ordinary things: the GPIBEE is not reachable at that IP address, or it is reachable but nothing answers at that GPIB address. Note the execution time of about three seconds in the screenshot, which is the IO_TIMEOUT_MS value elapsing while VISA waits.

Check them in that order. Open the GPIBEE web interface in a browser to confirm the IP, then use its GPIB scan to see which addresses actually have an instrument on them. A device switched off, set to a different address, or with no GPIB cable seated properly all produce this same error.

When no VISA is installed

The program checks for VISA before it does anything else. If none is installed it says so in plain language and tells you where to get one, rather than dying with a missing-DLL box:

Could not load a working VISA runtime: visa64.dll / visa32.dll not found

No usable VISA implementation was found on this PC.
Please install R&S VISA (free) from Rohde & Schwarz:
  https://www.rohde-schwarz.com/appnote/RSVisa
A note on 64-bit:the project forces -m64, so it always produces a 64-bit executable. The compiler bundled with the mingw installer handles both, so this needs no attention from you. It matters because a 32-bit program cannot load a 64-bit visa64.dll, and getting that combination wrong produces a confusing "not found" for a file that is plainly there.

Termination and timeout

Everything about how the session behaves is decided in a single call:

instr = XyGPIB_Open(INSTR_RESOURCE, XyGPIB_TERM_LF, XyGPIB_TERM_LF, IO_TIMEOUT_MS);

The two middle arguments are the write termination and the read termination, in that order, and they are set independently. This matters more than it looks like it should. Termination is how both ends know a message has ended, and it is the single most common reason a first attempt at instrument control hangs until it times out.

Write termination is what gets appended to every string you send. Most SCPI instruments expect a line feed and will sit silently waiting if they do not get one.

Read termination is how the read knows to stop. It is applied to the session itself, so the read returns the moment the terminator arrives rather than waiting out the full timeout. XyGPIB_Read() then strips the trailing CR, LF or CRLF from what it hands back, so you never have to trim the string yourself.

ValueOn writeOn read
XyGPIB_TERM_LFappends \nstops at \n. The usual choice, and what the example uses.
XyGPIB_TERM_CRappends \rstops at \r. Some older instruments want this.
XyGPIB_TERM_CRLFappends \r\nstops at \n, since VISA matches a single byte and CRLF always ends in LF.
XyGPIB_TERM_EOIappends nothingno terminator byte. The read ends on the EOI signal alone, and nothing is stripped.

If you do not know what your instrument wants, start with XyGPIB_TERM_LF for both. If reads time out while writes clearly work, the read termination is the first thing to change.

Write, read and query

Three functions cover almost everything you will do with a SCPI instrument. All three return bool: true means it worked.

Write: send a command

Use it for commands that produce no answer. The termination you chose at open time is appended for you, so do not add \n yourself.

XyGPIB_Write(instr, "*RST");                 /* reset the instrument      */
XyGPIB_Write(instr, "CONF:VOLT:DC 10,0.001"); /* 10 V range, 1 mV res.     */

Read: fetch an answer

You pass a buffer and its full size. The result is always NUL-terminated on success, and the trailing CR/LF is already removed, so it is an ordinary C string you can pass straight to printf or atof.

char response[256];

if (XyGPIB_Read(instr, response, sizeof(response))) {
    printf("got: %s\n", response);
}
Size it generously:the read is capped at bufSize - 1 bytes to leave room for the NUL. If the instrument sends more than fits, you get a truncated answer and the rest is still queued for the next read, which then returns a confusing fragment. 256 bytes is plenty for a reading; identification strings and error lists want more.

Query: write then read, in one call

A query is simply a write followed by a read, and it is what you want for anything ending in ?. This is the workhorse.

char response[256];

if (!XyGPIB_Query(instr, "*IDN?", response, sizeof(response))) {
    fprintf(stderr, "*IDN? query failed.\n");
} else {
    printf("instrument: %s\n", response);
}

Putting those together, a complete measurement is short:

char buf[64];
double volts;

XyGPIB_Write(instr, "CONF:VOLT:DC");

if (XyGPIB_Query(instr, "READ?", buf, sizeof(buf))) {
    volts = atof(buf);
    printf("%.6f V\n", volts);
}

When the text functions are the wrong tool

Screenshots, waveform dumps and anything else that is not text will contain zero bytes, and a zero byte ends a C string. For those, use XyGPIB_WriteRaw() and XyGPIB_ReadRaw(), which add nothing, strip nothing and report exactly how many bytes arrived.

unsigned char image[65536];
size_t got;

XyGPIB_Write(instr, "HCOP:DATA?");

if (XyGPIB_ReadRaw(instr, image, sizeof(image), &got)) {
    printf("%zu bytes\n", got);
}

XyGPIB function reference

The complete API. Everything that can fail returns bool, so a raw ViStatus never reaches your code.

FunctionWhat it does
Startup
bool XyGPIB_LoadVisa(char *loadedPath, size_t size) Loads the system VISA runtime, trying visa64.dll then visa32.dll. Call once before opening anything. Writes the path of the DLL it found into loadedPath.
const char *XyGPIB_GetVisaLoadErrorDetail(void) Why the previous XyGPIB_LoadVisa() failed, in readable text.
Open and close
XyGPIB_Instr *XyGPIB_Open(const char *resource, XyGPIB_Term writeTerm, XyGPIB_Term readTerm, unsigned long timeoutMs) Opens a resource and returns a handle, or NULL on failure. Sets the timeout and both termination modes.
bool XyGPIB_Close(XyGPIB_Instr *instr) Closes the session and frees the handle. Safe to call with NULL. The handle is freed whatever it returns, so never use it again afterwards.
Text transfers
bool XyGPIB_Write(XyGPIB_Instr *instr, const char *str) Sends str with the write termination appended.
bool XyGPIB_Read(XyGPIB_Instr *instr, char *buf, size_t bufSize) Reads a reply into buf, strips the trailing CR/LF, NUL-terminates. Reads at most bufSize - 1 bytes.
bool XyGPIB_Query(XyGPIB_Instr *instr, const char *cmd, char *buf, size_t bufSize) Write then read, in one call.
Binary transfers
bool XyGPIB_WriteRaw(XyGPIB_Instr *instr, const void *data, size_t length) Sends exactly length bytes, unmodified.
bool XyGPIB_ReadRaw(XyGPIB_Instr *instr, void *buf, size_t maxLength, size_t *actualLength) Reads up to maxLength bytes with no stripping and no NUL. actualLength receives the count, and is set to 0 on failure.
Diagnostics
void XyGPIB_SetDebug(bool enable) When on, every VISA error is printed to stderr as it happens, with the status code and its description. Off by default. Turn it on while developing.
bool XyGPIB_GetDebug(void) Whether debug printing is currently on.
ViStatus XyGPIB_GetLastVisaError(void) The VISA status of the most recent call that failed. One remembered value, not a queue, and a later success does not clear it.

What XyGPIB is, and is not

XyGPIB is example code, not a product. It is a few hundred lines of C that ship inside the project you just downloaded. There is no library to install, no version to track, no release notes and no support commitment behind it. It is yours to read, change, rename, cut down or delete outright.

It is meant to be taken and used. That is the point of it being here: drop XyGPIB.h and XyGPIB.c into your own project and you have GPIBEE talking to an instrument on the first afternoon rather than the third. Loading the VISA runtime, opening and closing sessions cleanly, getting the termination attributes right and turning status codes into something readable are all solved already, and those are exactly the parts that cost a day each when you start from nothing. Treat it as a running start for your own integration, not as a finished component to be admired from a distance.

The other half of its job is keeping a first program readable. VISA is a capable and fairly large API, and meeting it head-on in your first hour means a resource manager session, an instrument session, attribute codes for the timeout and termination, and a ViStatus to decode after every call, all before you can ask an instrument its name. XyGPIB puts a thin layer over that so the example fits on one screen.

Working with VISA directly is a perfectly good choice, and past the basics it is the better one. Everything XyGPIB does, VISA does; XyGPIB simply exposes fewer functions with an easier interface. When you outgrow it, you are not stuck: include a VISA header, or visa_interface.h from the project, and call VISA yourself. The two mix freely in the same program.

While it is useful, it takes three things off your hands:

  • No status codes to decode. VISA returns a ViStatus from every call, which you are expected to test against a list of constants. XyGPIB returns bool, and keeps the underlying status available through XyGPIB_GetLastVisaError() for the times you want it.
  • No session or attribute bookkeeping. Opening an instrument through VISA means a resource manager session, an instrument session, and attribute writes for the timeout and termination. XyGPIB_Open() is one call with four arguments.
  • No termination handling in your code. Appending terminators on write and trimming them on read is the sort of detail that quietly produces strings with an invisible \r on the end. It is handled once, in one place.

What is deliberately missing

XyGPIB covers opening a session and moving bytes, and stops there. Several GPIB primitives you may well need are simply not in it:

  • Device clear, to reset an instrument's interface and empty its buffers (viClear in VISA).
  • Triggering, the Group Execute Trigger that starts a measurement on one or several instruments at once (viAssertTrigger).
  • Reading the status byte by serial poll, the usual way to ask an instrument whether it is finished or has an error waiting (viReadSTB).
  • Service requests and events, so an instrument can interrupt you rather than being polled.
  • Locking, and the full VISA attribute set.

None of these are missing by oversight, and none are waiting on a future version. This is example code: adding them would make it a library, which is not what it is for. VISA offers all of them, and since XyGPIB is a thin layer over VISA you can call them directly alongside it in the same program.

Stuck adding one of these?If you need device clear, triggering, serial poll or anything else in that list and cannot get it working, write to support@gpibee.com. Tell us what you are trying to do and we will help you get there.

Troubleshooting

ProblemSolution
"Could not load a working VISA runtime" No VISA is installed, or a 32-bit build is looking for a 64-bit DLL. Install R&S VISA and confirm the Release/Debug target is building 64-bit.
Code::Blocks reports no compiler found You installed the plain setup rather than the mingw one. Either install codeblocks-25.03mingw-setup.exe over it, or point Settings → Compiler → Toolchain executables at a toolchain you already have.
XyGPIB_Open() fails with 0xBFFF0011 The GPIBEE is not reachable at that IP, or no instrument answers at that GPIB address. Confirm the IP in a browser, then use the GPIB scan in the web interface, or the R&S VISA Tester, to see which addresses are occupied.
Writes work, reads always time out Read termination. The instrument is not sending the terminator you configured. Try XyGPIB_TERM_EOI, or the other CR/LF variants.
The answer has a stray character at the end You are in XyGPIB_TERM_EOI mode, which strips nothing by design. Switch to CR, LF or CRLF, or trim it yourself.
Long measurements fail but short ones work The timeout is shorter than the measurement. Raise IO_TIMEOUT_MS past the longest operation you trigger.
Answers arrive one command late A previous read left data queued, usually after a truncated read. Enlarge the buffer, and make sure every query is read exactly once.
Turn the debug output on:XyGPIB_SetDebug(true) is already in the example's main(). It prints the VISA status and its description for every failure, which usually names the problem outright and saves a round of guessing.