Power measurements · Coin-cell load step
A coin cell can read 3.15 V with a high-impedance probe and sag when a device draws current. In this experiment, a spring-return switch connects one known resistor for about a second. A second oscilloscope channel records when the switch actually closes and opens. Compare the same cell at two currents, and compare the beginning and end of each pulse. That gives you four voltage drops at known currents and times, plus a record of how the holder voltage recovers after release.
Fixture boundary. Use an untabbed, non-rechargeable Panasonic Energy CR2032 in a covered holder, at ordinary indoor temperature, with one of the two specified resistors. Keep the cell and fixture away from children. Use the cell as the fixture’s sole power source. Never charge it, short it, heat it, solder to it, or fit a rechargeable look-alike. Keep the resistor in series throughout the experiment. The example traces and numbers below are synthetic.
BT1 is the standard, untabbed Panasonic Energy CR2032, with a 20.0 mm maximum diameter and 3.2 mm maximum height. Fit it in an MPD BHSD-2032-PC through-hole holder with its BHSD-2032-COVER. Use an insulated board with no loose conductors under it. Identify the holder contacts from their polarity and continuity with the cell removed. The face marked “+” goes to the positive holder contact. Use a spring-return, normally open pushbutton suitable for low-current signal contacts. A switch with a dry-circuit or signal-contact rating is preferable to a switch intended only for higher-current loads.
| Point | Make this connection | What it gives you |
|---|---|---|
| B+ / VBAT | Positive holder contact, one end of RLOAD, CH1 tip | Cell voltage at the holder, measured relative to B− |
| VSW | Other end of RLOAD, one contact of S1, CH2 tip | Voltage across the low-side switch |
| B− | Negative holder contact, other contact of S1, both probe ground clips | One shared reference for both scope inputs |
Install one measured 1% resistor, 22 kΩ for the first capture and 4.7 kΩ for the second. A 0.125 W or 0.25 W part is sufficient. Keep the clips on the holder terminals, with no separate path through a bench supply, charger, signal generator, or another powered circuit. A scope with common channel grounds can short a node when a ground clip lands on it. Check both clips are on B− before inserting the cell.
Use two DC-coupled, high-impedance inputs with matched 10× passive probes. A RIGOL DHO804 with two PVP2150 probes is one documented setup. The probe guide gives 10 MΩ nominal at DC in 10× mode. Set the hardware switches and the scope channel attenuation to 10×. Compensate the probes with the scope calibrator before connecting the cell, then return both probes to the isolated fixture. Check the vertical offset and ranges so CH1 can resolve the loaded change without clipping and CH2 can show both switch states.
The manufacturer lists 3 V nominal, 225 mAh nominal, and a 0.2 mA continuous standard drain for this model. The standard drain is a reference for the published cell data. This fixture uses one brief, resistor-limited pulse per capture. Panasonic’s separate active-RFID example uses a 10 mA, 10 ms pulse at −20 °C. It illustrates how specifically the test conditions must be reported. The room-temperature pulses in this fixture have much lower current and longer duration. Use the manufacturer figure only within its own test conditions.
Using the 3.35 V upper fixture screen and the low end of each 1% resistor gives the following upper bounds. The calculation assumes a zero-voltage closed switch to maximize resistor current. At 3.35 V, a 10 MΩ CH1 draws another 0.335 µA. A measured resistor value should replace the nominal value during analysis.
| Load | 1% lower bound | Upper resistor current at 3.35 V | Upper resistor dissipation |
|---|---|---|---|
| 22 kΩ | 21.78 kΩ | 0.154 mA | 0.516 mW |
| 4.7 kΩ | 4.653 kΩ | 0.720 mA | 2.412 mW |
Even with a CH1 probe as low as 8 MΩ, the larger calculated cell current stays below 0.721 mA in this fixture. For a 1.2 s pulse, that is under 0.00025 mAh. It bounds this individual event and gives no estimate of the cell’s remaining capacity. A stuck button changes the experiment. Use a switch that opens when you let go and release it immediately if the voltage is lower than expected.
Icell(t) ≈ [VBAT(t) − VSW(t)] / RLOAD + VBAT(t) / RCH1
The resistor term includes the current drawn by CH2 through RLOAD. When S1 is open, CH2 is still a path to B− through about 10 MΩ. Its voltage sits near VBAT, and CH1 still draws its own small current. The baseline is therefore a lightly loaded reading. Compute the current step by subtracting the measured baseline current from the loaded current. The equation uses the DC input model, so the analyzer excludes switch-edge intervals where the probes and wiring also have transient capacitance.
Keep the CH1 and CH2 timestamps paired. Convert the scope export to exactly time_s,vbat_v,vsw_v, in seconds and volts, with one header and three numeric columns. Strip instrument headers and unit suffixes. Preserve monotonically increasing times and any gap. A common time offset is fine because the program locates both edges itself. Keep the raw file separately so you can audit clipping or any conversion later. The blank observations sheet is for your measurements and settings.
Baseline is the median from 200 to 50 ms before closure. Loaded values use ±20 ms windows centered 100 and 600 ms after closure. Recovery uses the same width centered 200 ms and 1 s after opening. The program takes the median voltage and median calculated current in each window. The two loaded windows can show whether the response changes during an otherwise steady current step.
ΔV = Vbaseline − Vloaded · ΔI = Iloaded − Ibaseline · Reff(t) = ΔV / ΔI
Report the ratio with the cell, holder, current step, time after closure, and temperature. It includes the cell’s time-dependent response plus resistance between the electrochemical cell and the probed holder points. A different switch resistance affects the current calculation through VSW. A different holder contact can also alter what CH1 reads. An estimate can change between 100 ms and 600 ms even when no component has changed. The ratio describes this loaded step and should stay separate from a component’s impedance at a specified AC test frequency.
These values come from the bundled synthetic files with a 2 mV floor. Both examples start at 3.150 V. The graph magnifies tens of millivolts. This lets you see the difference in the two generated traces without implying that a particular fresh cell will reproduce it.
| Example load | ΔV at 100 ms | ΔV at 600 ms | ΔI at 600 ms | Ratio at 600 ms | VBAT at 200 ms / 1 s after release |
|---|---|---|---|---|---|
| 22 kΩ synthetic | 8.09 mV | 10.99 mV | 0.1424 mA | 77.16 Ω | 3.1465 V / 3.1496 V |
| 4.7 kΩ synthetic | 37.45 mV | 50.67 mV | 0.6590 mA | 76.90 Ω | 3.1336 V / 3.1483 V |
When the lighter load’s drop approaches the noise floor, report the current step and the unresolved drop. Similar ratios in these examples follow from how the traces were generated. A larger difference in actual captures can come from noise, cell history, contact movement, or a genuinely current-dependent response. Inspect both time traces before assigning the difference to the cell.
Download the experiment archive for the program, tests, synthetic inputs and JSON outputs, editable diagrams, and worksheet. The script uses the Python standard library. Run the examples with python3 coin_pulse.py synthetic-22000.csv --kind synthetic --load-ohms 22000 --drop-floor-mv 2 or change both file name and load value to 4700. Use --kind observed for your own capture, with the measured load and chosen floor. For a documented non-default CH1 input near 10 MΩ, supply its resistance in ohms with --ch1-probe-ohms.
The analyzer uses VSW ≤ 15% of VBAT to locate the low state and VSW ≥ 80% of VBAT to qualify the idle state away from the edges. It checks the whole capture for missing samples and voltage outside the fixture limits, rejects out-of-range pulse lengths, and reports an error if any required measurement window is incomplete. A negative drop beyond the floor is flagged as a voltage rise. These are host-side checks of CSV processing. The circuit and bench observations must be verified by the person performing the experiment.
#!/usr/bin/env python3
"""Report the timed, in-fixture voltage and current response to one cell load pulse."""
import argparse
import csv
import json
import math
from collections import namedtuple
from pathlib import Path
from statistics import median
Row = namedtuple("Row", "time vbat vsw")
HEADER = ["time_s", "vbat_v", "vsw_v"]
MAX_GAP = 0.005
EDGE_GUARD = 0.020
def read_trace(path):
with path.open(encoding="utf-8-sig", newline="") as source:
lines = csv.reader(source, strict=True)
if next(lines, None) != HEADER:
raise ValueError("expected header time_s,vbat_v,vsw_v")
data = []
for line, values in enumerate(lines, start=2):
if len(values) != 3:
raise ValueError(f"line {line}: expected three numbers")
row = Row(*(float(v) for v in values))
if not all(math.isfinite(v) for v in row):
raise ValueError(f"line {line}: a value is not finite")
if not 2.4 <= row.vbat <= 3.35:
raise ValueError(f"line {line}: cell voltage outside the fixture envelope")
if not -0.02 <= row.vsw <= row.vbat + 0.03:
raise ValueError(f"line {line}: switch voltage outside the fixture envelope")
if data:
gap = row.time - data[-1].time
if gap <= 0 or gap > MAX_GAP + 1e-9:
raise ValueError(f"line {line}: sample gap or non-increasing time")
data.append(row)
if not data:
raise ValueError("trace contains no samples")
return data
def measure(row, load, probe):
branch = (row.vbat - row.vsw) / load
return branch + row.vbat / probe
def window(data, start, end, load, probe, name):
chosen = [row for row in data if start <= row.time <= end]
if (len(chosen) < 3 or chosen[0].time - start > MAX_GAP + 1e-9
or end - chosen[-1].time > MAX_GAP + 1e-9):
raise ValueError(f"missing or incomplete {name} window")
return {
"samples": len(chosen),
"vbat_v": median(row.vbat for row in chosen),
"current_a": median(measure(row, load, probe) for row in chosen),
}
def find_pulse(data):
active = [index for index, row in enumerate(data) if row.vsw <= 0.15 * row.vbat]
if not active or active[0] == 0 or active[-1] == len(data) - 1:
raise ValueError("pulse requires both switch transitions and idle data")
start = data[active[0]].time
end = data[active[-1] + 1].time
if not 0.85 - 1e-9 <= end - start <= 1.20 + 1e-9:
raise ValueError("pulse duration outside 0.85 to 1.20 seconds")
for row in data:
if start + EDGE_GUARD < row.time < end - EDGE_GUARD:
valid = row.vsw <= 0.15 * row.vbat
elif row.time < start - EDGE_GUARD or row.time > end + EDGE_GUARD:
valid = row.vsw >= 0.80 * row.vbat
else:
continue
if not valid:
raise ValueError("unstable switch state away from an edge")
return start, end
def analyze(data, load, probe, floor, kind):
start, end = find_pulse(data)
baseline = window(data, start - .20, start - .05, load, probe, "baseline")
if baseline["vbat_v"] < 2.60:
raise ValueError("baseline cell voltage below 2.60 V")
loaded = []
for label, age in (("100 ms", .10), ("600 ms", .60)):
point = window(data, start + age - .02, start + age + .02, load, probe, label)
drop = baseline["vbat_v"] - point["vbat_v"]
step = point["current_a"] - baseline["current_a"]
if step <= 0:
raise ValueError("loaded current must exceed the baseline current")
resistance = None
# Two decimal CSV voltages can straddle an equal floor after subtraction.
if abs(drop) <= floor + 1e-12:
quality = "drop_below_floor"
elif drop < 0:
quality = "voltage_rose_under_load"
else:
quality = "resolved"
resistance = drop / step
loaded.append({**point, "time_after_close_s": age, "drop_v": drop, "step_a": step,
"effective_ohms": resistance, "quality": quality})
recovery = {
label: window(data, end + age - .02, end + age + .02, load, probe, label)
for label, age in (("0.2s", .20), ("1.0s", 1.0))
}
return {
"kind": kind, "load_ohms": load, "ch1_probe_ohms": probe,
"drop_floor_v": floor, "close_time_s": start, "open_time_s": end,
"pulse_s": end - start, "baseline": baseline, "loaded": loaded,
"recovery": recovery, "scope": "Timed voltage and current response at the holder terminals.",
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("trace", type=Path)
parser.add_argument("--kind", choices=("synthetic", "observed"), required=True)
parser.add_argument("--load-ohms", type=float, required=True)
parser.add_argument("--ch1-probe-ohms", type=float, default=10e6)
parser.add_argument("--drop-floor-mv", type=float, required=True)
args = parser.parse_args()
if not (4653 <= args.load_ohms <= 4747 or 21780 <= args.load_ohms <= 22220):
parser.error("load resistance outside the 4.7 kΩ or 22 kΩ 1% fixture ranges")
if not 8e6 <= args.ch1_probe_ohms <= 12e6:
parser.error("expected a CH1 probe near 10 MΩ")
if not math.isfinite(args.drop_floor_mv) or not 0 < args.drop_floor_mv <= 50:
parser.error("drop floor must be positive and no greater than 50 mV")
try:
result = analyze(read_trace(args.trace), args.load_ohms, args.ch1_probe_ohms,
args.drop_floor_mv / 1000, args.kind)
except (OSError, UnicodeError, csv.Error, ValueError) as exc:
parser.error(str(exc))
print(json.dumps(result, indent=2, allow_nan=False))
if __name__ == "__main__":
main()
Imagine that the lighter and heavier pulses both start at 3.150 V. At 600 ms, one drops 11 mV and the other drops 51 mV. The measured steps are 0.142 mA and 0.659 mA. Which comparison tells you whether this result is consistent with the same effective ratio? What would change if the selected resolution floor were 15 mV?
Compare each drop with its own current step. Dividing 11 mV by 0.142 mA and 51 mV by 0.659 mA gives about 77 Ω for both. A 15 mV floor leaves the 11 mV drop unresolved. Keep its current step and the voltage observations in your notes. The other ratio still describes the holder, timing, and current of its individual test.