M²Lab Instrumentation note · 2026·05·05
Instrumentation note

Controlling the NF BP4610 over USB on Windows 11

A working setup for the BP4610 Bipolar DC Power Supply with firmware 1.40, which uses the JEMIMA USB488 protocol rather than USBTMC. The vendor's official position is that this firmware revision is not supported on Windows 8 or later. These notes record what we did to get it talking to Python on Windows 11.

Device
NF BP4610
Firmware
v1.40
USB ID
0D4A : 0006
Protocol
JEMIMA USB488
Host OS
Windows 11
Stack
libusb · pyusb
01 — Summary

What you need to know

If you only read one section, read this one. Each point below was a separate failure mode we had to work through.

01
BP4610 firmware 1.40 speaks JEMIMA USB488, a vendor-specific protocol (USB class FF) distinct from USBTMC. NI-VISA, Keysight IO Libraries, and similar stacks do not recognize it.
02
On Windows 11, bind libusb-win32 via Zadig and talk to the device with pyusb. The vendor's NFUSBTMC.inf is signed only for Windows 7 and earlier.
03
Enter REMOTE via a USB control transfer: bmRequestType=0x20, bRequest=0x03, wValue=0x0001. The front panel does not display an "RMT" indicator on this model — verify with Get Remote/Local instead.
04
Send ASCII commands as raw bytes terminated only by a zero-length packet. Appending \n or \r\n sets ESR bit 5 (Command Error), which then cascades and looks like every subsequent query is timing out.
05
Send *CLS in its binary common-command form (0x00 0x01) to keep ESR clean.
06
IOUT is not the DC current setting. It controls the initial output state at power-on (0 or 1). The DC current setpoint command is CURR, with 0.001 A resolution over ±10 A.
07
CURR can be changed while the output is on. This makes continuous current sweeps practical without toggling OUTP between points.
02 — Background

Why USBTMC drivers don't work

The BP4610 enumerates as a vendor-specific USB device (class 0xFF). Standard test-and-measurement stacks assume USBTMC framing on the bulk endpoints, and that framing is not what this firmware uses. Symptoms before figuring this out:

  • Yellow exclamation mark in Device Manager after the vendor's driver fails to install on Windows 11.
  • Manually binding the device to "USB Test and Measurement Device (IVI)" succeeds, but *IDN? times out (BFFF0015) in NI-VISA Test Panel.
  • Generating an .inf with the NI-VISA Driver Wizard and using a USB::RAW resource lets writes succeed but reads time out — the framing on the IN endpoint is not what NI-VISA expects.

The protocol the device actually uses is the JEMIMA USB488 stack defined around 1999–2000 under a NEDO project in Japan. It predates and is independent of the later USB-IF USBTMC standard. NF firmware 1.50 and later add USBTMC as well, but 1.40 is USB488-only. The full protocol specification is shipped, in English, inside NF's free USB driver package as Usb488ps.txt; the JEMIMA license permits use with attribution.

03 — Setup

Driver and Python setup

Driver binding

Run Zadig, enable Options → List All Devices, select the BP4610 (VID 0x0D4A, PID 0x0006), choose libusb-win32 as the target driver, and click Replace Driver. This replaces any previously installed NF driver, so if you also need NF's stock control software you'll have to swap back.

Python environment

pip install pyusb numpy

# libusb-1.0.dll must be reachable. Zadig usually places it; otherwise
# grab a Windows release of libusb and put the DLL on PATH.

Endpoint discovery

import usb.core

dev = usb.core.find(idVendor=0x0D4A, idProduct=0x0006)
dev.set_configuration()
for ep in dev.get_active_configuration()[(0, 0)]:
    print(hex(ep.bEndpointAddress), ep.wMaxPacketSize)

# 0x01  64    OUT, bulk — host to device, commands
# 0x82  64    IN,  bulk — device to host, responses
# 0x83   8    IN,  interrupt — status (unused here)
04 — Protocol

Protocol notes

REMOTE entry

USB488 maps the IEEE-488.1 interface messages (REMOTE, LOCAL, GTL, SDC, etc.) to USB control transfers. The most important one is Set Remote/Local:

# bmRequestType  bRequest    wValue    wIndex   wLength
#     0x20         0x03      0x0001     0x00     0x00
#                            ^^^^^^
#                            0x0000  LOCAL
#                            0x0001  REMOTE
#                            0x0002  LOCAL LOCKOUT

dev.ctrl_transfer(0x20, 0x03, 0x0001, 0, 0, timeout=2000)
Note The BP4610 does not display an "RMT" indicator on its front panel even when in REMOTE. Don't use the panel as a check; instead read the state back with Get Remote/Local (bmRequestType=0xA0, bRequest=0x04).

Bulk message format

After REMOTE entry, commands go over the bulk OUT endpoint. The first byte of the first packet identifies the message type:

Byte 0MeaningExample
0x00IEEE-488.2 common command (binary form)0x00 0x01 = *CLS
0x01Binary data (e.g. arbitrary waveform)
0x20–0x7FASCII data (vendor command)VOLT 5.0, MODE?

Every message ends with a zero-length packet (ZLP) on the bulk OUT endpoint. This is standard USB bulk practice and the BP4610 relies on it to mark end-of-message.

Terminator behavior

The spec describes ASCII data as "0x20–0x7F, 0x0D, and 0x0A only", which reads as if line-feed terminators are fine. In practice on this firmware they are accepted but flagged:

PayloadExecuted?ESR
LMCP 8.0yes0   clean
LMCP 8.0;yes0   clean
LMCP 8.0\nyes32   cmd error
LMCP 8.0\r\nyes32   cmd error
*CLS\nno32   stays set
*CLSyes0   clean
\x00\x01canonical0   clean

When ESR bit 5 stays set, subsequent queries can hang or return stale data, which is exactly the cascade-timeout symptom that initially looks like the set commands aren't working at all. The fix is to not append a terminator, and to use the binary form for common commands.

05 — Pitfalls

Pitfalls we hit

Things that cost us time and might cost yours, in roughly the order they came up.

× NI-VISA USBTMC binding
Reads time out. The IN endpoint framing isn't USBTMC.
× NI-VISA USB::RAW resource
Writes go through, reads time out. Same underlying reason.
× Keysight IO Libraries Suite
Same — these stacks all assume USBTMC.
Found NF's free protocol spec
Usb488ps.txt in their USB driver zip is the full USB488 protocol stack reference.
pyusb + libusb-win32 worked for queries
*IDN? returned the device identification string.
× Set commands appeared to do nothing
They were actually executing; ESR bit 5 was sticking, which made follow-up queries return zero or hang.
Identified the terminator issue
Sending payloads with no \n and ZLP-only termination keeps ESR at 0.
× Assumed IOUT was DC current
IOUT 0.5 got clamped to 0; IOUT 2.0 to 1. It's a 0/1 boolean for power-on initial output state.
Found CURR
The actual CC-mode DC current setpoint command. 0.001 A resolution.
Verified CURR changes with output ON
Bipolar setpoints update immediately, ESR stays clean — a continuous sweep is feasible.
06 — Reference

Command reference

Commands we have personally verified on firmware 1.40. The BP4610 manual lists more; treat anything below as known-good and anything outside as needing a quick check.

Identification & status

CommandFormDescriptionExample response
*IDN?ASCIIDevice identificationNF Corporation,BP4610,…,1.40
*CLSbinary preferredClear status registers
*ESR?ASCIIEvent status register0
*STB?ASCIIStatus byte0
*OPC?ASCIIOperation complete1

Mode and output

CommandParameterDescriptionNotes
MODE0–5Operation mode0=CV-INT, 3=CC-INT, etc.
OUTP0 / 1Output ON/OFF
IOUT0 / 1Initial output at power-onNot a current setpoint

Setpoints

CommandRangeDescriptionChange with output ON?
VOLT−115.00 to +115.00 VDC voltage (CV mode)No — output must be OFF
CURR−10.000 to +10.000 ADC current (CC mode)Yes
ACVL0 to 120 Vp-pSuperimposed AC voltage
FREQ1 Hz to 100 kHzAC frequency
WAVE0–17Waveform select

Limiters

CommandRangeDescription
LMVP / LMVM+7.0…+117.0 / −117.0…−7.0 V± voltage limiter
LMCP / LMCM+1.00…+26.00 / −26.00…−1.00 A± current limiter

Measurement

CommandDescriptionResolution
MVLT?DC voltage measurement0.1 V
MACV?AC voltage measurement (Vp-p)1 Vp-p
MCUR?DC current measurement0.01 A
MACC?AC current measurement (Ap-p)0.1 Ap-p
07 — Code

Python driver

A minimal driver class implementing the protocol decisions above. Only dependency is pyusb.

import usb.core
import usb.util
import time


class BP4610:
    """NF BP4610 bipolar DC power supply, JEMIMA USB488.

    Tested on Windows 11 with firmware 1.40.

    Implementation notes:
      - ASCII commands carry no \\n / \\r\\n terminator; ZLP only.
      - *CLS uses the binary common-command form (0x00 0x01).
      - REMOTE entry via control transfer (0x20, 0x03, wValue=1).
    """

    VID = 0x0D4A
    PID = 0x0006
    EP_OUT = 0x01
    EP_IN  = 0x82

    MODE_CV_INT, MODE_CV_EXT, MODE_CV_ADD = 0, 1, 2
    MODE_CC_INT, MODE_CC_EXT, MODE_CC_ADD = 3, 4, 5

    def __init__(self, timeout_ms=2000):
        self.dev = usb.core.find(idVendor=self.VID, idProduct=self.PID)
        if self.dev is None:
            raise RuntimeError('BP4610 not found')
        try:
            self.dev.set_configuration()
        except usb.core.USBError:
            pass
        self.timeout = timeout_ms
        self._drain_input()
        self.set_remote(True)
        self.cls()

    def _drain_input(self):
        try:
            while True:
                self.dev.read(self.EP_IN, 64, timeout=50)
        except usb.core.USBError:
            pass

    def set_remote(self, remote=True):
        ren = 0x0001 if remote else 0x0000
        self.dev.ctrl_transfer(0x20, 0x03, ren, 0, 0, timeout=self.timeout)

    def cls(self):
        self.dev.write(self.EP_OUT, b'\x00\x01', timeout=self.timeout)
        self.dev.write(self.EP_OUT, b'', timeout=self.timeout)

    def write(self, command):
        if isinstance(command, str):
            command = command.encode('ascii')
        command = command.rstrip(b'\r\n')
        self.dev.write(self.EP_OUT, command, timeout=self.timeout)
        self.dev.write(self.EP_OUT, b'', timeout=self.timeout)  # ZLP

    def read(self, max_bytes=1024):
        chunks = []
        try:
            while True:
                data = self.dev.read(self.EP_IN, 64, timeout=self.timeout)
                if len(data) == 0:
                    break
                chunks.append(bytes(data))
                if len(data) < 64:
                    break
                if sum(len(c) for c in chunks) >= max_bytes:
                    break
        except usb.core.USBError as e:
            if 'timeout' not in str(e).lower() or not chunks:
                raise
        full = b''.join(chunks)
        if len(full) > 0 and 0x20 <= full[0] <= 0x7F:
            return full.decode('ascii', errors='replace').strip().strip('"')
        return full

    def query(self, command):
        self.write(command)
        return self.read()

    def close(self):
        try:
            self.write('CURR 0'); time.sleep(0.1)
            self.write('OUTP 0'); time.sleep(0.2)
            self.set_remote(False)
        except: pass
        usb.util.dispose_resources(self.dev)

    # ----- high-level helpers -----
    def set_mode(self, m):  self.write(f'MODE {m}')
    def output(self, on=True): self.write(f'OUTP {1 if on else 0}')
    def set_voltage(self, v): self.write(f'VOLT {v}')   # OFF only
    def set_current(self, i): self.write(f'CURR {i}')   # ON ok

    def set_v_limits(self, vmax, vmin):
        self.write(f'LMVP {vmax}'); self.write(f'LMVM {vmin}')

    def set_i_limits(self, imax, imin):
        self.write(f'LMCP {imax}'); self.write(f'LMCM {imin}')

    def measure(self):
        return {
            'V_dc': float(self.query('MVLT?')),
            'V_ac': float(self.query('MACV?')),
            'I_dc': float(self.query('MCUR?')),
            'I_ac': float(self.query('MACC?')),
        }

Basic usage

from bp4610 import BP4610

bp = BP4610()
print(bp.query('*IDN?'))

bp.set_i_limits(5.0, -5.0)
bp.set_v_limits(30.0, -30.0)
bp.set_mode(BP4610.MODE_CC_INT)

bp.set_current(0)
bp.output(True)
bp.set_current(2.5)              # changes immediately

print(bp.measure())
bp.close()
08 — Application

FMR field sweep example

Using the BP4610 as the current source for an electromagnet and a lock-in amplifier (here, an SR844) for detection. Because CURR updates while the output is on, the loop is straightforward.

import numpy as np, time, pyvisa
from bp4610 import BP4610


def fmr_sweep(bp, lockin, i_start, i_stop, n,
              dwell_s=0.5, settle_s=0.1):
    if max(abs(i_start), abs(i_stop)) > 10:
        raise ValueError("|I| exceeds 10 A spec")

    currents = np.linspace(i_start, i_stop, n)
    I_meas = np.zeros(n); X = np.zeros(n); Y = np.zeros(n)

    try:
        bp.set_mode(BP4610.MODE_CC_INT); time.sleep(0.2); bp.cls()
        bp.set_current(0); bp.output(True); time.sleep(0.3)

        # soft ramp to start to avoid back-EMF kicks on inductive loads
        for i_pre in np.linspace(0, i_start, 20):
            bp.set_current(i_pre); time.sleep(0.05)
        time.sleep(0.5)

        for k, i_t in enumerate(currents):
            bp.set_current(i_t)
            time.sleep(settle_s + dwell_s)
            I_meas[k] = float(bp.query('MCUR?'))
            X[k] = float(lockin.query('OUTP? 1'))
            Y[k] = float(lockin.query('OUTP? 2'))
    finally:
        last = float(bp.query('CURR?'))
        for i_post in np.linspace(last, 0, 20):
            bp.set_current(i_post); time.sleep(0.05)
        bp.output(False)

    return currents, I_meas, X, Y


if __name__ == '__main__':
    rm = pyvisa.ResourceManager()
    sr844 = rm.open_resource('GPIB0::8::INSTR')

    bp = BP4610()
    bp.set_i_limits(6.0, -6.0)
    bp.set_v_limits(40.0, -40.0)

    I, I_m, X, Y = fmr_sweep(bp, sr844,
                             i_start=-3.0, i_stop=+3.0,
                             n=301, dwell_s=0.5)
    bp.close()

    np.savetxt('fmr_sweep.txt',
               np.column_stack([I, I_m, X, Y]),
               header='I_set[A] I_meas[A] X[V] Y[V]')
Safety Always soft-ramp to the start point and back to zero. A sudden current change into an inductive load produces a back-EMF spike that can hit the voltage limiter or trip protection.