How to characterise a lithium-ion cell with a bench supply, an electronic load, and a DMM: CC-CV charge, capacity and discharge curves, the IEC 61960 DC pulse method for internal resistance, hardware and software abort limits, and when a dedicated battery cycler is the better buy.

Battery test automation on a bench means scripting a programmable power supply, an electronic load, and a DMM or SMU to charge a cell in constant current then constant voltage, discharge it at a set current to its cut-off voltage, log the curve, compute capacity and internal resistance, and stop the moment any safety limit is crossed. Three instruments cover a single cell: the supply charges, the load discharges, and a 6½-digit DMM reads the cell voltage at its tabs. You can script that in Python with PyVISA, or describe it in a sentence to an AI agent such as TestFlow (testflowinc.com), which builds the same sequence and writes the report from the logged data.
This guide is for hardware, power, and validation engineers who need capacity, discharge curves, and internal resistance on a handful of cells before a dedicated cycler is justified. You get the bench, the wiring, the abort limits, real SCPI for a Keysight E36313A, EL34143A, and 34465A, the IEC 61960 DC pulse method in code, and an honest answer on when to buy a battery cycler instead.
A single-cell characterisation bench needs four roles, and a Li-ion 18650 sits comfortably inside common bench instrument ratings. The examples below use a Molicel INR-18650-P28A because its datasheet states every number the test needs.
| Datasheet item (Molicel P28A) | Value |
|---|---|
| Capacity | 2,800 mAh typical, 2,600 mAh minimum |
| Nominal voltage | 3.6 V |
| Charge | CC-CV to 4.2 V, 50 mA cut-off at 23 °C, standard 2.8 A, maximum 6.0 A |
| Discharge cut-off | 2.5 V |
| Maximum discharge current | 35 A |
| Internal resistance | AC 1 kHz 20 mΩ maximum, DC (10 A, 1 s) 20 mΩ |
| Temperature | charge 0 to 60 °C, discharge -40 to 60 °C |
The instruments, with list prices from the vendors' sites as of October 2026:
| Role | Model | Why it fits | List price |
|---|---|---|---|
| Charger | Keysight E36313A output 1 | 6 V, 10 A, remote sense | US$2,609 |
| Discharger | Keysight EL34143A | 150 V, 60 A, 350 W, built-in battery test subsystem | US$3,128 |
| Cell voltage | Keysight 34465A | 6½ digits, Kelvin leads at the tabs | US$2,075 |
| Temperature | second 34465A | thermocouple input (34465A and 34470A only) | US$2,075 |
One SMU can replace the supply and load for low currents. The Keithley 2450 sources and sinks in four quadrants up to ±1.05 A at 20 V, and the Keithley 2460 reaches 7.35 A at 7.35 V, at list prices of US$8,630 and US$13,700 on tek.com as of October 2026. For a 2.8 Ah cell, a 1 A ceiling means a capacity run takes hours longer than with a separate load, so the supply-plus-load bench is the usual choice above about 1 A.
Battery measurements are millivolt measurements on milliohm paths, so the wiring decides the result. A 20 mΩ cell carrying 2.6 A drops 52 mV across its own resistance, and a holder or lead with the same resistance doubles the apparent drop.
Four rules for a single-cell fixture:
Voltage leads at the cell tabs, separate from the current leads. IEC 61960-3 specifies exactly this for internal resistance: voltage sensed at the terminals, independent of the current-carrying contacts.
A series diode between the supply and the cell. Keysight's E36300 user's guide warns that if over-voltage protection trips with a battery across the output, the battery's current sinks through the supply's internal circuitry and can damage it, and it says a diode must be connected in series.
Supply remote sense at the cell, after the diode. VOLT:SENS:SOUR EXT, (@1) moves regulation to the cell terminals, so the diode's forward drop sits inside the loop. The E36300 data sheet lists 1 V as the maximum load-lead drop for remote sense, which a Schottky diode at these currents fits within.
Load remote sense at the cell with VOLT:SENS:SOUR EXT, (@1) on the EL34143A, so its cut-off voltage is judged at the tabs rather than after the lead drop.
The supply and the load can stay wired to the cell together. The diode blocks the cell from the supply during discharge, and the EL34143A's input-off impedance of 824 kΩ (per the EL30000 data sheet) draws about 5 µA at 4.2 V while it waits. The VI curve sweep guide covers the same 4-wire principle for a resistive DUT.
A battery test needs limits that hold even if the PC crashes, so set hardware cut-offs in the instruments first and software checks second. The script is a monitor, not the last line of defence.
| Condition | Hardware layer | Software abort (example limit) | Source of the number |
|---|---|---|---|
| Over-voltage on charge | Supply OVP at 5.0 V on output 1 | Cell above 4.25 V on the DMM | Datasheet charge voltage 4.2 V |
| Under-voltage on discharge | BATT:CUTO:VOLT 2.5 on the load | Cell below 2.45 V on the DMM | Datasheet cut-off 2.5 V |
| Over-temperature | none on these instruments | Cell surface above 45 °C | Datasheet maximum 60 °C, example margin |
| Charge runaway time | none | Charge longer than 4 h at 1.4 A | Datasheet standard charge 1.5 h at 2.8 A |
| Discharge runaway time | BATT:CUTO:TIM on the load | Discharge longer than 7 h at 0.52 A | 2.8 Ah at 0.52 A is about 5.4 h |
| Lost communication | OVP and load cut-offs still active | the finally block turns everything off | none |
Every software limit in that table is an example for this cell, not a measured result and not a universal value. Derive yours from the cell's datasheet and your lab's safety rules. Physical protection matters as much as code: a fuse in series with the cell sized for your maximum test current, and a fire-safe container for unattended runs.
The E36313A's OVP on output 1 is programmable from 0.5 V to 6.6 V with VOLT:PROT <v>, (@1), and it is always active rather than switchable. The EL34143A's battery subsystem stops the discharge itself when the cell voltage falls below BATT:CUTO:VOLT, a timer set by BATT:CUTO:TIM expires, or a capacity set by BATT:CUTO:CAP has been drawn, per the EL30000 programming guide. Use the battery subsystem's cut-off rather than the load's VOLT:INH:VON setting: in its LIVE mode Von turns the input back on when the voltage recovers, and a resting cell's voltage always recovers.
A capacity test is a charge, a rest, and a discharge, with the cell voltage, current, and temperature logged throughout. This script runs one cycle on the P28A at a 1.4 A charge and a 0.52 A discharge (0.2 It against the 2,600 mAh minimum capacity), integrates capacity, and writes the discharge curve to CSV.
"""One capacity cycle on a single Li-ion cell.
E36313A output 1 charges through a series diode with remote sense at the
cell; EL34143A discharges with remote sense at the cell; 34465A #1 reads
cell voltage at the tabs; 34465A #2 reads a type K thermocouple on the can.
"""
import csv
import time
import pyvisa
V_CHARGE, I_CHARGE, I_TERM = 4.20, 1.4, 0.050 # datasheet CC-CV, 50 mA cut
I_DISCHARGE, V_CUTOFF = 0.52, 2.50 # 0.2 It, datasheet cut-off
ABORT_V_HIGH, ABORT_V_LOW, ABORT_T = 4.25, 2.45, 45.0 # example limits
CHARGE_MAX_S, DISCHARGE_MAX_S, REST_S = 4 * 3600, 7 * 3600, 3600
LOG_EVERY_S = 10
rm = pyvisa.ResourceManager()
psu = rm.open_resource("TCPIP0::192.168.1.10::inst0::INSTR")
load = rm.open_resource("TCPIP0::192.168.1.11::inst0::INSTR")
dmm_v = rm.open_resource("TCPIP0::192.168.1.12::inst0::INSTR")
dmm_t = rm.open_resource("TCPIP0::192.168.1.13::inst0::INSTR")
for inst in (psu, load, dmm_v, dmm_t):
inst.timeout = 10000
inst.write("*RST")
dmm_v.write("CONF:VOLT:DC 10")
dmm_v.write("VOLT:DC:NPLC 10")
dmm_t.write("CONF:TEMP TC,K")
dmm_t.write("UNIT:TEMP C")
psu.write("VOLT:SENS:SOUR EXT, (@1)")
psu.write("VOLT:PROT 5.0, (@1)")
psu.write(f"CURR {I_CHARGE}, (@1)")
psu.write(f"VOLT {V_CHARGE}, (@1)")
load.write("FUNC CURR, (@1)")
load.write("VOLT:SENS:SOUR EXT, (@1)")
load.write("CURR:RANG 3, (@1)")
load.write(f"CURR {I_DISCHARGE}, (@1)")
load.write(f"BATT:CUTO:VOLT {V_CUTOFF}, (@1)")
load.write("BATT:CUTO:VOLT:STAT ON, (@1)")
load.write(f"BATT:CUTO:TIM {DISCHARGE_MAX_S}, (@1)")
load.write("BATT:CUTO:TIM:STAT ON, (@1)")
def sample():
v = float(dmm_v.query("READ?"))
t = float(dmm_t.query("READ?"))
if v > ABORT_V_HIGH or v < ABORT_V_LOW or t > ABORT_T:
raise RuntimeError(f"abort: cell {v:.3f} V, {t:.1f} C")
return v, t
curve, ah = [], 0.0
try:
# 1. CC-CV charge until the taper current falls below 50 mA.
psu.write("OUTP ON, (@1)")
start = time.monotonic()
while True:
time.sleep(LOG_EVERY_S)
v, t = sample()
i = float(psu.query("MEAS:CURR? (@1)"))
if i < I_TERM:
break
if time.monotonic() - start > CHARGE_MAX_S:
raise RuntimeError("abort: charge timeout")
psu.write("OUTP OFF, (@1)")
# 2. Rest so the cell voltage relaxes before discharge.
time.sleep(REST_S)
# 3. CC discharge; the load's battery cut-off ends it at 2.5 V.
load.write("BATT ON, (@1)")
load.write("INP ON, (@1)")
start = last = time.monotonic()
while True:
time.sleep(LOG_EVERY_S)
now = time.monotonic()
i = float(load.query("MEAS:CURR? (@1)"))
v, t = sample()
ah += i * (now - last) / 3600
last = now
curve.append((round(now - start, 1), v, i, t, round(ah, 4)))
if v <= V_CUTOFF or i < 0.1 * I_DISCHARGE: # cut-off reached
break
load_ah = float(load.query("BATT:MEAS:CAP? (@1)"))
finally:
load.write("INP OFF, (@1)")
psu.write("OUTP OFF, (@1)")
with open("discharge.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["t_s", "cell_V", "load_A", "temp_C", "ah"])
w.writerows(curve)
print(f"capacity: {ah * 1000:.0f} mAh integrated, {load_ah * 1000:.0f} mAh from the load")Two capacity numbers come out, and they should agree within a few milliamp-hours: the script's own integration and the load's BATT:MEAS:CAP?. If they disagree, the logging interval is too coarse or a reading was missed. The charge termination is coarser: output 1 specifies current readback as 0.05% + 5 mA, so the 50 mA cut-off is judged to about ±10%, which shifts the end of charge by minutes, not by meaningful capacity. The datasheet's 2,600 mAh minimum is a reasonable example limit, but check the discharge current and temperature the datasheet rates capacity at before you grade against it, because capacity falls as current rises.
The sample() function runs on every log point in both phases, so a cell that heats or drifts out of its window ends the run within one interval. The multimeter automation guide covers the DMM side, including why CONF must come before any NPLC setting.
DC internal resistance on a bench follows IEC 61960-3, clause 7.7.3: discharge at a constant 0.2 It for 10 s ± 0.1 s and record U1, then immediately step to 1.0 It for 1 s ± 0.1 s and record U2. The resistance is R = (U1 − U2) / (I2 − I1). The wording is in the PS IEC 61960-3 national adoption, and Arbin's IEC 61960 procedure matches it. The standard preconditions the cell with a charge and 1 to 4 h of storage at 20 °C ± 5 °C.
It is the rated capacity divided by one hour. Against the P28A's 2,600 mAh minimum, I1 is 0.52 A and I2 is 2.6 A.
import time
import pyvisa
I1, I2 = 0.52, 2.6 # 0.2 It and 1.0 It for 2.6 Ah
rm = pyvisa.ResourceManager()
load = rm.open_resource("TCPIP0::192.168.1.11::inst0::INSTR")
dmm = rm.open_resource("TCPIP0::192.168.1.12::inst0::INSTR")
dmm.write("*RST")
dmm.write("CONF:VOLT:DC 10")
dmm.write("VOLT:DC:NPLC 1") # 20 ms integration keeps U1/U2 on time
load.write("*RST")
load.write("FUNC CURR, (@1)")
load.write("VOLT:SENS:SOUR EXT, (@1)")
load.write("CURR:RANG 3, (@1)") # one range for both levels
load.write(f"CURR {I1}, (@1)")
try:
load.write("INP ON, (@1)")
t0 = time.monotonic()
time.sleep(max(0.0, t0 + 9.97 - time.monotonic()))
u1 = float(dmm.query("READ?"))
i1 = float(load.query("MEAS:CURR? (@1)"))
load.write(f"CURR {I2}, (@1)") # same range, so the input stays on
t1 = time.monotonic()
time.sleep(max(0.0, t1 + 0.97 - time.monotonic()))
u2 = float(dmm.query("READ?"))
i2 = float(load.query("MEAS:CURR? (@1)"))
finally:
load.write("INP OFF, (@1)")
r_dc = (u1 - u2) / (i2 - i1)
print(f"U1 {u1:.5f} V U2 {u2:.5f} V R_dc {r_dc * 1000:.2f} mOhm")Both current levels sit inside the EL34143A's medium range, which the guide says to choose once, because a range change briefly drops the input and would break the pulse. Software timing with time.sleep holds well inside the standard's ± 0.1 s window on a desktop PC, but it is not deterministic; for tighter timing, use the load's own transient or LIST system.
Do not grade an IEC number against the P28A's "DC (10 A, 1 s) 20 mΩ" line. That datasheet figure uses a 10 A pulse, a different method, so treat it as context, not a limit. The AC method in clause 7.7.2 measures at 1.0 kHz ± 0.1 kHz with a current chosen to keep the peak voltage under 20 mV, and the standard notes that it measures impedance. A dedicated meter such as the Hioki BT3562A, which measures at 1 kHz, is listed at €3,098 excluding VAT on Hioki's EU shop as of October 2026.
Some bench instruments package part of this sequence in firmware, which shortens the script without changing the method.
BATT ON with voltage, capacity, and timer cut-offs, and BATT:MEAS:CAP? and BATT:MEAS:TIM? for results. It handles the discharge and its end condition; charging and limits on temperature stay with you.The options split by scale: bench characterisation of a few cells, or production and long-term cycling of many.
PyVISA is MIT licensed, and version 1.16.2 shipped on February 27, 2026. Every script on this page runs on it. You own the sequencing, the abort logic, the logging, and the report, and you also own the job of making sure a crashed script leaves the cell in a safe state. The PyVISA tutorial covers setup, and the data logger without LabVIEW guide covers long logging runs.
TestFlow (testflowinc.com) is an AI agent for electronics testing: you describe the test, the agent builds the workflow and Python, runs it on your bench, and writes the report from measured results. Free to start, then $39 or $149 a month per seat as of October 2026. Bench runs need the Windows desktop app. Not a production battery cycler, and not BMS HIL.
The capacity cycle above, described in one sentence:
> "Charge the cell on E36313A output 1 at 1.4 A to 4.2 V until the current drops below 50 mA, rest one hour, discharge on the EL34143A at 0.52 A to 2.5 V, log cell voltage and thermocouple temperature from the two 34465As every 10 s, abort above 4.25 V or 45 °C, and report capacity against 2,600 mAh."
The agent builds a reviewable workflow of steps (configure, charge, rest, discharge, log, compute) plus the generated Python with the SCPI for those models, and the Windows desktop app runs it on the real instruments. The report charts the discharge curve with a PASS or FAIL per reading, built from the run's results; the agent writes limits, never measurements, and will not widen a limit to make a cell pass (trust guide). The generated Python exports as a standalone package you own (generated code guide).
TestFlow fits cell and small-pack characterisation on instruments you already have. It does not replace a multi-channel cycler for weeks of unattended cycling, it does not do deterministic real-time timing, and it is not a hardware-in-the-loop system for BMS validation; the HIL testing software comparison covers that category. It drives SCPI bench instruments, not DAQ cards. See pricing and the instrument catalog.

The EL34143A battery subsystem and the Keithley 2281S battery test function, described above, run part of the sequence with no PC at all. You give up the charge-discharge sequencing across instruments, the temperature abort, and the report.
A battery cycler is the right purchase when the test is many cells, many cycles, or pack-level power. The vendors below publish no prices; every one sells by quote as of October 2026.
| Vendor | Example line | Scale (from the vendor's page) |
|---|---|---|
| Arbin | LBTS-Cell, RBT-Cell, RBT-Pack | 4 to 256 channels per cell system, packs to 1,500 V; MITS Pro software |
| Maccor | Series 4000 | 1 to 96 channels, up to 1,400 A and 14,400 W |
| BioLogic | BCS-900 series | 8 channels per module, ±150 mA to ±15 A (BCS-915), EIS option |
| Chroma | 17011 cells, 17020 and 17040 packs | 17020 regenerative, 600 W to 60 kW per channel |
| Neware | CT-4008Q-5V6A-S1 | 8 channels, 5 V, 6 A, 30 W per channel |
Several of these build the methods on this page into the software: BioLogic's BT-Lab task list includes DCIR, and Chroma's 17011 data sheet describes DCIR modes that comply with IEC 61960. Buy one when you need channel count, regenerative discharge, formation and grading, or months of unattended cycle-life data. Keep the bench approach for the first cells, the fixture debug, and the one-off characterisation that does not justify a cycler.
| Need | Bench + Python | TestFlow | Built-in instrument function | Battery cycler |
|---|---|---|---|---|
| One to a few cells | Yes | Yes | Yes | Overkill |
| Dozens of cells in parallel | Hard | Hard | No | Yes |
| Weeks of cycle-life data | Fragile | Fragile | No | Yes |
| Regenerative pack discharge | No | No | No | Yes |
| Custom limits and report | You write it | Built from the run | No | Vendor software |
| Cost as of Oct 2026 | Free software | Free version, then $39 or $149 a month | Included | By quote |
Most wrong battery numbers trace back to setup, not to the cell.
LIVE mode the load turns back on as the cell relaxes, so the discharge never really ends.The automated test report generation guide covers turning the CSV into a report that cites where each number came from.
Bench battery characterisation is slow, repetitive, and unforgiving of a script that stops watching. It is exactly the kind of sequence worth describing once and running the same way every time.
Connect your instruments. Pick the manufacturer and model, paste the VISA address (USB, LAN, GPIB, or serial), and the agent knows what is on your bench. No bench yet? Use a placeholder address, build the full automation, and swap in the real address when you are in the lab.
Tell the agent what to test. For example, "run a VI sweep from 1 to 10 V in 1 V steps at 0.5 A load current," or "suggest the tests for a power-management device."
The agent builds the complete workflow in seconds. Instrument-aware automation appears on the canvas, with the generated scripts visible in a code panel you can inspect and edit.
Run it in your lab. Click Run in the TestFlow desktop app for Windows and the status panel streams results step by step, with measured values inline (VOUT = 3.301 V, asserted 3.2 to 3.4 V, PASS). One click exports a structured PDF report, or the raw results as CSV.


The step-by-step walkthrough, VISA address formats, and Test Planner prompts are all in the TestFlow product guide.
Yes, for cell-level characterisation. Charge with the supply in CC-CV through a series diode, rest, then discharge with the electronic load in constant current to the cell's cut-off voltage while a DMM logs the cell voltage at the tabs. Capacity is the integrated current, and loads such as the Keysight EL34143A also report it with BATT:MEAS:CAP?.
IEC 61960-3 discharges the cell at 0.2 It for 10 s and records U1, then steps to 1.0 It for 1 s and records U2. The DC resistance is (U1 - U2) / (I2 - I1), with voltage sensed at the cell terminals on separate leads from the current.
When you need many channels in parallel, weeks of unattended cycling, regenerative discharge at module or pack level, or formation and grading. Arbin, Maccor, BioLogic, Chroma, and Neware build dedicated cyclers for that work, all sold by quote as of October 2026.
TestFlow automates bench characterisation: describe the charge, discharge, or internal resistance test in a sentence, and the agent builds the workflow and Python with the SCPI for your supply, load, and meters, runs it from the Windows desktop app, and reports from the logged data. It is not a production battery cycler or a BMS HIL system.
Keysight's E36300 user's guide warns that if over-voltage protection trips with a battery across the output, current from the battery can sink through the supply's internal circuitry and damage it, and it says a diode must be connected in series with the output.
Describe the test. TestFlow builds the workflow, runs it on your instruments, and writes the report. Free to start, no card needed.