Build a Pico Mini Multimeter for Low-Voltage Experiments
A pocket instrument can turn “the circuit does not work” into a useful observation: the supply is 3.28 V, the resistor belongs in the 10 kΩ drawer, or the jumper is open. This project combines a Raspberry Pi Pico, an external voltage converter and a small display. You build the measurement path as well as the software, so you can explain what the number means.
This is MiniMeter v0.1, an untested bench design for small, current-limited experiments. Its intended functions are positive 0–12 V direct-current voltage, approximately 1–100 kΩ resistance after calibration, and a low-resistance continuity indication. Direct current, abbreviated DC, has a fixed polarity here. Use it only on low-energy circuits such as our 3.3 V and 5 V lessons. It has no mains measurement-category rating and is not for wall sockets, mains-connected circuits, car batteries or unknown supplies. Disconnect probes before turning it off or connecting a computer.

1. Why these parts?
The original Raspberry Pi Pico with RP2040 runs the program. The Texas Instruments ADS1115, on an Adafruit #1085 STEMMA QT breakout, is the analog-to-digital converter, or ADC: it turns input voltage into a number. It includes its own reference and selectable measurement scale. Its nominal 16-bit output is resolution, not a promise of 16-bit accuracy for this assembled instrument.
The Adafruit #4440 128×32 SSD1306 display uses an organic light-emitting diode panel, abbreviated OLED. Both boards communicate on I²C, Inter-Integrated Circuit, a two-wire bus with a data line, SDA, and a clock line, SCL. Power both modules from the Pico’s regulated 3.3 V output so their bus pull-ups also use 3.3 V. Do not substitute a 5 V pull-up module without examining its circuit.
Three terminals make the intended connection clear: black COM is common ground, red V+ is voltage input, and a separate Ω terminal is for an unpowered resistor or contact. Use one red probe at a time. A two-pole momentary OHM button simultaneously connects a small test current and tells the software to display resistance. Released, it returns to voltage measurement. This mechanical separation avoids an always-powered resistance probe.

2. Parts and their jobs
| Reference | Exact part / value | Quantity | Purpose |
|---|---|---|---|
| U1 | Original Raspberry Pi Pico, RP2040 non-wireless Rev3 | 1 | Runs MicroPython; original-board GP25 LED pin. |
| U2 | Adafruit #1085 ADS1115 STEMMA QT breakout | 1 | External ADC; power VIN at 3.3 V, default 0x48. |
| U3 | Adafruit #4440 SSD1306 128×32 I²C OLED | 1 | Auto-reset module; address 0x3C, VIN 3.3 V. |
| R1, R4 | 100 kΩ, 0.1%, ≥0.125 W metal film | 2 | Upper divider arms; restrict input current. |
| R2, R5 | 20 kΩ, 0.1%, ≥0.125 W metal film | 2 | Lower divider arms; nominal scale factor six. |
| R3, R6 | 470 Ω, 1%, ≥0.125 W | 2 | Local input RC filter series resistors. |
| R7 | 10 kΩ, 0.1%, ≥0.125 W metal film | 1 | Resistance reference; measure its actual value. |
| R8 | 1 kΩ, 1%, ≥0.125 W | 1 | Limits piezo drive current. |
| R9 | 3.3 kΩ, 1%, ≥0.125 W | 1 | About 1 mA rail load; not a voltage regulator. |
| C1, C2, C3 | 100 nF X7R ceramic, ≥16 V | 3 | Two input filters and supply decoupling. |
| C4 | 10 µF, ≥10 V | 1 | Supply decoupling; positive end to 3V3 if polarised. |
| D1, D2 | Nexperia BAS70-04, SOT23 (TO-236AB) | 2 | Series dual Schottky; labelled adapters may help assembly. |
| D3 | Vishay 1N5817, DO-204AL / DO-41 | 1 | Battery series Schottky: cathode band toward VSYS. |
| S1 | DPST-NO momentary button, two isolated contacts | 1 | A DPDT momentary switch may use each COM/NO pair; verify pairs. |
| S0 | SPST battery on/off switch | 1 | Opens the battery positive feed. |
| BZ1 | Murata PKM13EPYH4000-A0 external-drive piezo | 1 | Passive capacitive sounder; not an active or magnetic buzzer. |
| BT1 | Three-cell AAA holder and three alkaline AAA cells | 1 set | Independent supply. No charger connection. |
| J_V, J_OHM, J_COM | Insulated V+, Ω and COM probe terminals | 3 | Label clearly; use one red probe at a time. |
| Assembly | Board, short wires, enclosure; labelled SOT23 adapters as needed | 1 set | Keep input/ground paths short; do not assume breadboard contact quality. |
The clamp diodes are the only small surface-mount parts added by this design; use labelled SOT23-to-through-hole adapters if building on a breadboard. “BAS70-04” means the specified series-connected dual diode, not the differently connected −05 or −06 variants. Read the adapter’s pin numbers: visual similarity is not a pinout. All numbered circuit parts below are additional to components already on the purchased modules.
3. Make every connection explicit
| Reference | Value / pin group | Electrical connections |
|---|---|---|
| R1 | 100000 Ω | V_IN ↔ V_DIV |
| R2 | 20000 Ω | V_DIV ↔ GND |
| R3 | 470 Ω | V_DIV ↔ AIN0 |
| R4 | 100000 Ω | OHM_IN ↔ R_DIV |
| R5 | 20000 Ω | R_DIV ↔ GND |
| R6 | 470 Ω | R_DIV ↔ AIN1 |
| R7 | 10000 Ω | 3V3 ↔ OHM_FEED |
| R8 | 1000 Ω | PIEZO_DRIVE ↔ PIEZO |
| R9 | 3300 Ω | 3V3 ↔ GND |
| C1 | 1e-07 F | AIN0 ↔ GND |
| C2 | 1e-07 F | AIN1 ↔ GND |
| C3 | 1e-07 F | 3V3 ↔ GND |
| C4 | 1e-05 F | 3V3 ↔ GND |
| D1 | Pins | 1_A1 → GND; 2_K2 → 3V3; 3_K1_A2 → AIN0 |
| D2 | Pins | 1_A1 → GND; 2_K2 → 3V3; 3_K1_A2 → AIN1 |
| D3 | Pins | A → BAT_SWITCHED; K_banded → VSYS |
| S0 | Pins | 1 → BAT_PLUS; 2 → BAT_SWITCHED |
| S1A_NO | Pins | 1 → OHM_FEED; 2 → OHM_IN |
| S1B_NO | Pins | 1 → OHM_BUTTON; 2 → GND |
| U1_Pico | Pins | 1_GP0 → SDA; 2_GP1 → SCL; 20_GP15 → OHM_BUTTON; 21_GP16 → PIEZO_DRIVE; 36_3V3 → 3V3; 38_GND → GND; 39_VSYS → VSYS |
| U2_ADS1115 | Pins | VIN → 3V3; GND → GND; SDA → SDA; SCL → SCL; A0 → AIN0; A1 → AIN1; A2 → 3V3; A3 → GND; ALRT → NC |
| U3_OLED | Pins | VIN → 3V3; GND → GND; SDA → SDA; SCL → SCL; RST_optional → NC |
| BZ1 | Pins | 1 → PIEZO; 2 → GND |
| BT1 | Pins | + → BAT_PLUS; - → GND |
| J_V | Pins | 1 → V_IN |
| J_OHM | Pins | 1 → OHM_IN |
| J_COM | Pins | 1 → GND |
Place C1/C2 and D1/D2 near the ADC board, with short ground returns. Keep probe wiring away from the buzzer line. On #1085, leave the back address jumper open for 0x48; on older layouts with an ADDR header, use the documented ground selection. Do not mistake an address jumper named A0 for the analog input A0. Leave ALRT unconnected because the program polls conversion completion. Ground unused A3. Do not drive the breakout’s A+ or A− supply-output pads.
4. The voltage range is made by resistors
R1 and R2 form a voltage divider: 100 kΩ above the measuring node and 20 kΩ below it. Ignoring ADC loading, the node voltage is Vin × 20/(100 + 20) = Vin/6. Therefore 12 V at the socket becomes 2.00 V at the ADC, comfortably below its 3.3 V supply. The program selects a ±4.096 V full-scale setting; that is a conversion scale, not permission to put 4.096 V on a pin powered from 3.3 V. The ADS1115 recommended pin range remains ground to its supply.
One output count represents 4.096/32,768 = 125 µV at this ADC scale, or 0.75 mV at the socket in the ideal divider. The screen deliberately rounds to 0.01 V. Two-point calibration, noise, resistor ratio, ADC loading and diode leakage matter more than the number of displayed digits. With opposing 0.1% resistor errors, the ideal scale factor spans about 5.990–6.010 before those other effects.
R3 and C1 slow fast changes before conversion. The divider’s equivalent source resistance is 100 kΩ in parallel with 20 kΩ, approximately 16.7 kΩ; with R3 and 100 nF, the time constant is about 1.71 ms. Five time constants are about 8.6 ms. The firmware takes multiple completed conversions and updates the display a few times per second, so this is a slow bench instrument, not an oscilloscope for pulses.
5. Resistance needs a test source—and a loading correction
With OHM held, S1A connects 3.3 V through R7, 10 kΩ, to the Ω terminal. The unknown resistor connects from Ω to COM. Its node is also loaded by R4 + R5, approximately 120 kΩ. Pretending that divider is absent would make a 100 kΩ resistor look like roughly 54.5 kΩ in the elementary two-resistor calculation.
Call the measured excitation voltage E, the reconstructed Ω-terminal voltage X, and their ratio y = X/E. Let Rref be R7 and Rload be the effective input load. Then 1/Runknown = (1 − y)/(Rref × y) − 1/Rload. The code performs that correction and refuses a negative or zero conductance rather than displaying a fictitious enormous resistor.
For an ideal 10 kΩ sample, its parallel combination with 120 kΩ is 9,230.8 Ω. With E = 3.3 V, X becomes 1.584 V and A1 sees 0.264 V. The corrected result returns 10 kΩ. An open Ω socket still has the internal 120 kΩ path: it produces X ≈ 3.046 V and A1 ≈ 0.508 V, not an ADC over-range. The display says “OL / >100k”; it does not claim to distinguish an open circuit from every resistance above its useful range.
For Ω measurements the ADC uses a ±1.024 V scale, or 31.25 µV per count. Near a short this corresponds to about 0.57 Ω per ideal count, but leakage and offsets prevent that from being an accuracy specification. Values below 1 kΩ are shown as “LOW”. Continuity starts below an estimated 40 Ω and stops at 80 Ω. These two thresholds are hysteresis: they reduce chatter near the boundary. The sound follows repeated valid readings and may pulse between acquisitions; it is not a specification for finding milliohm solder joints.
6. Protection is modest, and it affects accuracy
R1/R4 restrict current from the probe inputs; D1/D2 provide external Schottky paths toward ground and 3.3 V. Their exact orientation is pin 1 to ground, pin 2 to 3.3 V, and pin 3 to the corresponding ADC input. R9 draws about 1 mA from 3.3 V, providing a small constant rail load. It does not create isolation or guarantee a safe unpowered input.
At the specified 0–12 V input range the divider, not a conducting clamp, keeps the ADC in range. As a fault-current estimate only, a reversed 12 V source through 100 kΩ produces a current of order 0.12 mA. That is not a reverse-voltage qualification: clamp forward voltage, temperature, rail injection and the ADC’s absolute pin limits still matter. Do not deliberately apply reversed or excessive voltage to “test the protection.” The design has no fuse-rated current socket, surge absorber or high-energy fault rating.
The BAS70-04 data sheet specifies up to 100 nA reverse leakage at its stated 50 V, 25°C test condition; that is not a measured leakage at our 3.3 V bias. An illustrative 100 nA net leakage through a 16.7 kΩ source resistance shifts the ADC node about 1.67 mV, equivalent to 10 mV at the probe. It shows why a clamp cannot be treated as an ideal open circuit during measurement. Temperature changes can alter that error.
The ADS1115 also has finite, gain-dependent input impedance. With this simple high-resistance divider, its typical input impedances imply an additional gain error on the order of a percent, not a guaranteed precision bound. Calibration is compulsory before trusting comparisons. We leave the screen marked UNCAL until you measure the assembled instrument. No overall ±accuracy or battery runtime is claimed before bench results exist.
7. Where to use it in our other experiments
| Experiment | Use the MiniMeter this way |
|---|---|
| 3.3 V / 5 V supply checks | Good starting use: a regulated supply has low source resistance. Compare with a reference meter first. |
| A 1 kΩ / 1 kΩ divider on 5 V | Nominally about 2.490 V with the 120 kΩ meter load, instead of the unloaded 2.500 V. |
| A 100 kΩ / 100 kΩ divider on 5 V | Expect about 1.765 V, not 2.500 V: the meter substantially changes the circuit. Add a suitable buffer or use a higher-impedance instrument. |
| LED current through a known series resistor | Measure across the resistor and calculate I = V/R. For example 2.20 V across 470 Ω means about 4.68 mA through that resistor. The meter adds its own small parallel current. |
| A 10 kΩ / 470 µF charging experiment | Including this meter changes the ideal final voltage on a 5 V supply to about 4.615 V and the time constant to 4.338 s. Use that loaded model. |
| Fast digital edges, mains or direct current ranges | Outside this design’s measurement scope. |
For a voltage measurement across a component, COM need not be the project’s ground, but the battery-powered meter’s entire ground then follows that connection. Unplug USB first. A computer cable creates another ground path and destroys the intended floating arrangement. Keep every circuit and connection within the stated low-energy 0–12 V scope. This is functional floating operation, not certified safety isolation.
8. Complete program and visible failure states
The code targets MicroPython 1.27 on the original non-wireless Pico. It includes the ADS1115 driver, the SSD1306 display driver, filtering, calibration arithmetic, button logic and buzzer control; no third-party Python package is required. Save it as pico_minimeter.py, then save import pico_minimeter; pico_minimeter.run() as main.py. Flash the correct Pico MicroPython image first. These sources have been exercised on a host computer with simulated interfaces; they have not run on a Pico yet.
Each ADC request starts a single conversion, waits for its completion and enforces a timeout. A median of five readings rejects isolated spikes; it cannot fix systematic errors. Resistance mode samples the excitation before and after the measurement and rejects a rail change above 20 mV. The released Ω input must have appeared near zero for at least 300 ms before the software accepts a held-button result. This warning check cannot disconnect S1A or prove that a circuit is unpowered. The physical contact has already connected R7 when you press it.
An error silences the piezo and replaces the reading when the display still responds. If the shared I²C bus has failed, the OLED itself may retain an old number: the Pico’s onboard light then flashes in groups of three. During normal operation a dot/star on the screen and the onboard light keep changing. A frozen heartbeat means “do not trust the number.” A watchdog resets a stalled program; after restart, a held OHM button remains blocked until released. The software does not silently reuse a previous conversion as a fresh measurement.
Download pico_minimeter.py · Machine-readable connection list
"""Tricky Circuit MiniMeter v0.1, Pico RP2040 + ADS1115 + OLED #4440.
MicroPython 1.27 API target. Low-energy, positive 0..12 V DC only.
Not hardware tested. All ground-labelled points are one electrical net.
Save as pico_minimeter.py; main.py: import pico_minimeter; pico_minimeter.run()
"""
import math
import time
# Replace these only with measurements made during the article's calibration.
CALIBRATED = False
CAL_VIEW = False # OLED raw readings for calibration
V_ZERO = 0.0 # ADC volts, A0 at +/-4.096 V, input shorted
V_FACTOR = 6.0 # socket volts / zero-corrected A0 volts
R_ZERO = 0.0 # ADC volts, A1 at +/-1.024 V, OHM shorted
R_FACTOR = 6.0 # socket volts / zero-corrected A1 volts
R_REFERENCE = 10000.0 # R7, measured before assembly
R_LOAD = 120000.0 # input network; refine using fit_resistance()
PRECHECK_FACTOR = 6.0 # rough A1 scale at +/-4.096 V
ADS_ADDRESS = 0x48
OLED_ADDRESS = 0x3C
def config_word(channel, pga):
if channel not in (0, 1, 2, 3) or pga not in (1, 3):
raise ValueError('Unsupported ADC channel or gain')
# Single-ended MUX, start, single-shot, 128 samples/s, comparator disabled.
return 0x8000 | ((4 + channel) << 12) | (pga << 9) | 0x0100 | 0x0080 | 3
def signed16(raw):
return raw - 65536 if raw & 0x8000 else raw
def median(values):
if not values:
raise ValueError('No samples')
s = sorted(values)
return s[len(s) // 2]
class ADS1115:
def __init__(self, bus, clock=time, address=ADS_ADDRESS):
self.bus, self.clock, self.address = bus, clock, address
def register(self, reg):
data = self.bus.readfrom_mem(self.address, reg, 2)
if len(data) != 2:
raise OSError('Short ADC read')
return data[0] << 8 | data[1]
def wait_ready(self, timeout_ms=50):
start = self.clock.ticks_ms()
while not self.register(1) & 0x8000:
if self.clock.ticks_diff(self.clock.ticks_ms(), start) >= timeout_ms:
raise OSError('ADC conversion timeout')
self.clock.sleep_ms(1)
def volts(self, channel, pga=1):
self.wait_ready()
cfg = config_word(channel, pga)
self.bus.writeto_mem(self.address, 1, bytes((cfg >> 8, cfg & 255)))
self.clock.sleep_ms(2) # do not accept an immediate old ready state
self.wait_ready()
code = signed16(self.register(0))
if code >= 32760 or code <= -32760:
raise ValueError('ADC full scale')
return code * (4.096 if pga == 1 else 1.024) / 32768.0
def filtered(self, channel, pga=1, samples=5):
return median([self.volts(channel, pga) for _ in range(samples)])
def voltage_from_adc(adc, zero=V_ZERO, factor=V_FACTOR):
if not all(math.isfinite(x) for x in (adc, zero, factor)) or factor <= 0:
raise ValueError('Invalid voltage calibration')
v = (adc - zero) * factor
if v < -0.03 or v > 12.0:
raise ValueError('Outside 0..12 V')
return max(0.0, v)
def resistance_from_adc(adc, excitation, zero=R_ZERO, factor=R_FACTOR,
reference=R_REFERENCE, load=R_LOAD):
if not all(math.isfinite(x) for x in (adc, excitation, zero, factor, reference, load)):
raise ValueError('Invalid resistance input')
if not 3.0 <= excitation <= 3.6 or min(factor, reference, load) <= 0:
raise ValueError('Check rail/cal')
vx = (adc - zero) * factor
if vx < -0.02 or vx > excitation + 0.02:
raise ValueError('Check OHM input')
if vx <= 0:
return 0.0
# Unknown resistor is in parallel with the permanently connected divider.
y = vx / excitation
conductance = (1.0 - y) / (reference * y) - 1.0 / load
if conductance <= 0:
return None
r = 1.0 / conductance
# Allow floating-point rounding at the endpoint, not an accuracy allowance.
return None if r > 100000.0 * (1 + 1e-9) else min(r, 100000.0)
def fit_resistance(r1, y1, r2, y2):
"""Fit effective reference/load from two KNOWN resistors, 1k and 47k suggested.
y is calibrated OHM socket voltage divided by measured excitation voltage.
No invented observations or nominal precision claims.
"""
if not all(math.isfinite(x) for x in (r1, y1, r2, y2)):
raise ValueError('Non-finite calibration')
if min(r1, r2) <= 0 or r1 == r2 or not (0 < y1 < 1 and 0 < y2 < 1):
raise ValueError('Bad reference readings')
q1, q2 = 1.0 / y1 - 1.0, 1.0 / y2 - 1.0
rr = (q1 - q2) / (1.0 / r1 - 1.0 / r2)
intercept = q1 - rr / r1
if rr <= 0 or intercept <= 0:
raise ValueError('Calibration has no physical fit')
rl = rr / intercept
if not (8000 < rr < 12000 and 90000 < rl < 150000):
raise ValueError('Check wiring/reference values')
return rr, rl
class Continuity:
def __init__(self):
self.on = False
def update(self, resistance, valid=True):
if not valid or resistance is None or not math.isfinite(resistance) or resistance < 0:
self.on = False
elif self.on:
self.on = resistance < 80.0
else:
self.on = resistance < 40.0
return self.on
class OhmGate:
"""A warning gate, not a safety disconnect: S1A switches R7 mechanically."""
def __init__(self, clock=time):
self.clock = clock
self.quiet_since = None
self.last_quiet = None
self.pressed = False
self.allowed = False
def release(self, socket_volts, now):
self.pressed = self.allowed = False
if math.isfinite(socket_volts) and abs(socket_volts) <= 0.03:
if self.quiet_since is None:
self.quiet_since = now
self.last_quiet = now
else:
self.quiet_since = self.last_quiet = None
def press(self, now):
if not self.pressed:
self.allowed = (self.quiet_since is not None and self.last_quiet is not None
and self.clock.ticks_diff(now, self.quiet_since) >= 300
and self.clock.ticks_diff(now, self.last_quiet) <= 300)
self.pressed = True
self.quiet_since = self.last_quiet = None
return self.allowed
class OLED128x32:
def __init__(self, bus, fb_module, address=OLED_ADDRESS):
self.bus, self.address = bus, address
self.data = bytearray(512)
self.fb = fb_module.FrameBuffer(self.data, 128, 32, fb_module.MONO_VLSB)
# SSD1306, 128x32, internal charge pump, horizontal addressing.
for cmd in (0xAE, 0xD5, 0x80, 0xA8, 0x1F, 0xD3, 0x00, 0x40,
0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x02,
0x81, 0x7F, 0xD9, 0xF1, 0xDB, 0x20, 0xA4, 0xA6, 0xAF):
self.command(cmd)
self.lines(['MINIMETER v0.1', 'STARTING', 'REMOVE PROBES', ''])
def command(self, value):
self.bus.writeto(self.address, bytes((0x00, value)))
def lines(self, lines):
self.fb.fill(0)
for i, text in enumerate(lines[:4]):
self.fb.text(str(text)[:16], 0, i * 8, 1)
for cmd in (0x21, 0, 127, 0x22, 0, 3):
self.command(cmd)
self.bus.writeto(self.address, b'\x40' + self.data)
def run():
from machine import Pin, SoftI2C, PWM, WDT
import framebuf
button = Pin(15, Pin.IN, Pin.PULL_UP)
buzzer = PWM(Pin(16), freq=4000, duty_u16=0)
led = Pin(25, Pin.OUT, value=0) # original non-wireless Pico only
watchdog = WDT(timeout=2000)
display = None
gate, beeper = OhmGate(), Continuity()
status = 'CAL' if CALIBRATED else 'UNCAL'
try:
while True:
try:
bus = SoftI2C(sda=Pin(0), scl=Pin(1), freq=100000, timeout=50000)
devices = bus.scan()
if ADS_ADDRESS not in devices or OLED_ADDRESS not in devices:
raise OSError('Missing I2C device')
adc = ADS1115(bus)
display = OLED128x32(bus, framebuf)
gate = OhmGate() # no old permission after reconnect/reset
while True:
buzzer.duty_u16(0) # any read fault leaves sound off
heartbeat = '*' if (time.ticks_ms() // 500) % 2 else '.'
led.value(heartbeat == '*')
if button.value():
beeper.update(None)
raw_ohm = adc.filtered(1, 1, 3)
gate.release(raw_ohm * PRECHECK_FACTOR, time.ticks_ms())
raw_v = adc.filtered(0, 1)
if CAL_VIEW:
rail = adc.volts(2, 1)
raw_cal = adc.filtered(1, 3)
display.lines(['RAW ADC VOLTS', 'A0 %.6f' % raw_v,
'A1 %.6f' % raw_cal, 'E %.4f %s' % (rail, heartbeat)])
else:
volts = voltage_from_adc(raw_v)
note = 'OHM: CHECK POWER' if abs(raw_ohm * PRECHECK_FACTOR) > 0.03 else 'HOLD OHM: TEST'
display.lines(['DC + 0..12V', '%.2f V' % volts, note, status + ' ' + heartbeat])
else:
time.sleep_ms(35) # contact bounce and RC settling
if button.value():
continue
if not gate.press(time.ticks_ms()):
beeper.update(None)
display.lines(['OHM BLOCKED', 'RELEASE BUTTON', 'CHECK POWER/OHM', status + ' ' + heartbeat])
else:
e1 = adc.volts(2, 1)
raw_r = adc.filtered(1, 3)
e2 = adc.volts(2, 1)
if button.value():
continue
if abs(e1 - e2) > 0.02:
raise ValueError('Unstable rail')
excitation = (e1 + e2) / 2
resistance = resistance_from_adc(raw_r, excitation)
if CAL_VIEW:
display.lines(['RAW OHM / HELD', 'A1 %.6f' % raw_r,
'E %.6f' % excitation, 'CAL VIEW ' + heartbeat])
beeper.update(None)
else:
sound = beeper.update(resistance)
buzzer.duty_u16(32768 if sound else 0)
reading = 'OL / >100k' if resistance is None else ('LOW (<1k)' if resistance < 1000 else '%.2f kOhm' % (resistance / 1000))
display.lines(['OHM (UNPOWERED)', reading,
'CONTINUITY' if sound else 'RELEASE: DC V', status + ' ' + heartbeat])
watchdog.feed()
time.sleep_ms(60)
except (OSError, ValueError) as exc:
buzzer.duty_u16(0)
beeper.update(None)
gate = OhmGate()
print('MiniMeter:', exc)
try:
if display:
display.lines(['MEASUREMENT ERR', str(exc), 'REMOVE PROBES', 'RETRYING'])
except OSError:
pass # a failed shared bus can also freeze the display
# Triple LED flash identifies errors even if OLED cannot update.
for _ in range(3):
led.value(1); time.sleep_ms(80)
led.value(0); time.sleep_ms(80)
watchdog.feed()
time.sleep_ms(200)
finally:
buzzer.duty_u16(0)
buzzer.deinit()
if __name__ == '__main__':
run()
9. Assemble, calibrate, then decide what it is good enough for
- Inspect without power. Verify all ground nets, diode pin numbers, battery polarity and the two electrically separate OHM contacts. Check R1/R4 = 100 kΩ, R2/R5 = 20 kΩ and R7 = 10 kΩ before installation; record actual measured values when a reference meter is available. Check that no terminal labelled V+ connects directly to a Pico pin.
- Start with probes removed. Program using USB, disconnect USB, then use the three-cell battery pack. Confirm about 3.3 V on the module supply using a separate meter. Verify the screen, heartbeat and released-button state. Start with a current-limited supply during initial assembly if available; do not use the measured circuit to power this instrument.
- Measure the two zero readings. Set CAL_VIEW = True in the file, reload it with probes removed, then return to battery operation. Short V+ to COM and record raw A0 for V_ZERO. Short Ω to COM; after a quiet released interval hold OHM and record raw A1 for R_ZERO. Do not invent zero from the nominal circuit. Single-ended readings near ground may be limited by the ADC’s offset behaviour.
- Calibrate the voltage factors. Apply a known, independently measured low-impedance voltage, such as about 5 V, to V+ with correct polarity. V_FACTOR = reference voltage / (raw A0 − V_ZERO). For R_FACTOR, apply about 1 V to Ω with OHM released; CAL_VIEW reads A1 at the same ±1.024 V scale used for resistance. R_FACTOR = reference voltage / (raw A1 − R_ZERO). Remove that source before any Ω test. The raw diagnostic mode is not for voltages above its ADC scale.
- Refine resistance with real references. Use independently known 1 kΩ and 47 kΩ resistors, each from Ω to COM. In held CAL_VIEW record raw A1 and excitation E. For each, y = (raw A1 − R_ZERO) × R_FACTOR / E. The included
fit_resistance(R1, y1, R2, y2)returns effective R_REFERENCE and R_LOAD. Here R1/R2 are the two known test resistances, not schematic designators. It rejects a nonphysical fit; check readings rather than forcing a result. - Validate with values not used for calibration. Test, for example, independently measured 3.3 V and 9 V sources, and 4.7 kΩ, 10 kΩ and 100 kΩ resistors. Record error, battery state and approximate room temperature. Check shorted clips, a 100 Ω resistor and open clips: short should sound, 100 Ω and open should not. Remove the resistor and verify “OL / >100k”. Recheck after handling and warming the enclosure.
- Keep the evidence. Enter CALIBRATED = True only after your checks justify it, then return CAL_VIEW = False. Keep a table of reference value, indicated value, difference, conditions and date. If the errors are too large for an experiment, use a reference instrument or improve the input stage. The label means you performed calibration, not that this design acquired a certified accuracy class.
With measurement leads removed, test an ADC disconnection: the number must not continue to update as though valid, the sound must stop, and either the error text or the triple-flash light must identify the failure. Reconnect with power off. Also start the meter while holding OHM; it should request release. These tests check behaviour without injecting an overvoltage fault.
Calibration arithmetic in plain language
The two-resistor fit uses q = 1/y − 1 = Rref/Runknown + Rref/Rload. Two known resistors give two equations. The slope against 1/Runknown determines Rref, and the remaining intercept determines Rref/Rload. It is an effective calibration around those conditions; it cannot make leakage, temperature drift, dirty contacts or a wrongly wired board disappear. The software does not write fabricated values into the calibration constants.
10. Your first instrument experiment
Build a 5 V divider first with two 1 kΩ resistors, then with two 100 kΩ resistors. Before connecting the MiniMeter, predict whether both will display 2.50 V. Next, use the meter to check a loose 10 kΩ resistor and then the same resistor while it remains in parallel with another 10 kΩ part.
Show the explanation
The unloaded dividers both produce 2.50 V, but the meter’s approximately 120 kΩ input loads them differently: about 2.490 V for the first and 1.765 V for the second. In resistance mode, two external 10 kΩ parts in parallel are 5 kΩ; correcting the meter’s own divider does not remove that second external resistor. The lesson is to draw every current path, including the measuring instrument.
Verification status: schematic connections, nominal calculations, provided Python behaviour and local page layout have been reviewed. No target firmware execution, physical accuracy, clamp abuse test, enclosure fit or battery-runtime measurement is claimed. A reference meter and the owner’s bench results are still needed before using this prototype to validate other builds.
Primary sources and related reading
- TI ADS1115 data sheet, SBAS444E, December 2024: input limits, impedance, PGA and conversion registers
- TI ADS1115 product documentation; check again for updated documents before building
- Raspberry Pi Pico data sheet: header allocation and VSYS battery power
- Adafruit #1085 ADC module
- Adafruit ADC breakout pinouts and address selection
- Adafruit #4440 auto-reset OLED module
- Adafruit 128×32 I²C display wiring and address
- Solomon Systech SSD1306 Rev 1.1 command tables (Adafruit-hosted manufacturer PDF)
- Nexperia BAS70-04 data sheet, Rev 11, 1 January 2023: exact pinout and diode characteristics
- Vishay 1N5817 data sheet: polarity and package
- Murata PKM external-drive piezo series
- MicroPython 1.27 RP2 quick reference
Use this with why a voltage divider drops under load, measuring LED current with a voltmeter, and our interactive circuit lessons.