You insert a current monitor, turn on a load and see 0.000A. That might mean the load is off. It might also mean the chip is measuring a voltage while its current calculation has never been configured. This experiment makes that difference visible: first inspect the raw shunt measurement, then qualify calibration and conversion readiness before trusting a current value.

The exact device is Texas Instruments INA219AIDR, the A-grade eight-pin small-outline integrated circuit (SOIC) package, on a passive adapter. The controller is an original Raspberry Pi Pico RP2040 with official MicroPython1.26.0. We use a5V, approximately50mA resistor load; this is a low-voltage learning instrument, not a replacement for a calibrated multimeter or a power-system protection circuit.

Verification: the actual supplied register driver and console loop pass host tests with fake bus responses. Nominal circuit calculations are checked separately from measurements. No INA219 or Pico execution, firmware build, supply-transient test or physical current reading is claimed.

The shunt is a component with a job

A shunt is a small known resistor placed in the current path. It turns current into a voltage drop: Vshunt=I×Rshunt. The monitor measures that difference between IN+ and IN−, and separately measures IN− relative to ground as its bus voltage. High-side sensing means the shunt is in the positive supply lead, leaving the load's ground close to system ground.

ADC means analog-to-digital converter, which turns an input voltage into a numerical code. The INA219 has an ADC and a digital calculation engine. The raw shunt and bus registers can update even if its Calibration register is zero. The Current and Power registers depend on that scaling setting; their initial zero values are not evidence that no physical current flows.

Programming a Calibration register is not the same as measuring and calibrating an entire instrument. It tells the calculation engine what scale to use. A wrongly marked shunt, tolerance, temperature change, wiring error or input offset remains an error after a successful register write. The program therefore also reports current derived directly from the raw shunt value as a useful consistency check.

Build the named low-energy fixture

The0.100Ω value matters: “R100” marking, if present, normally denotes0.100Ω in this context, but verify the received part's manufacturer documentation. A board with a different shunt cannot use this fixed scaling unchanged. A two-wire ohmmeter reading near0.1Ω may mostly contain its leads and contacts; do not pretend that it establishes an accurate shunt value.

The integrated circuit (IC) supply is3.3V, inside its3–5.5V operating range. Its analog common-mode range and bus ADC scaling are different from that supply. The published32V ADC range option does not authorize32V on the inputs; normal input operation is limited to26V, and this experiment uses only5V. The A-grade and B-grade error specifications are not interchangeable. The current official PDF reviewed wasSBOS448G,revisionG; the product listing and a targeted errata search did not reveal a separate applicable errata sheet. That is not proof of absence.

Give load current and sense voltage different routes

INA219AIDR SOIC8: VS pin5 at3.3V; GND6, A0 pin2 and A1 pin1 grounded; SDA3 and SCL4 to Pico. A5V current-limited force path uses a0.100ohm shunt and100ohm1W load. Separate10ohm sense leads join upstream to IN+8 and downstream to IN−7.
Original schematic with named S+ and S− nets. Load current flows through the shunt; the separate sense leads do not carry the load current.

The load path is 5V positive→shunt→100Ω load→source return. Run separate short sense wires from the shunt terminals: upstream through10Ω toIN+, downstream through10Ω toIN−. These are Kelvin sense connections: the low-current measurement leads join at the component boundary so the force-lead voltage drop is largely excluded. Do not move a sense wire to a convenient point beyond a resistive breadboard contact and still claim to measure only the shunt.

INA219AIDR SOIC pinConnection
1 A1;2 A0BothGND, selecting seven-bit Inter-Integrated Circuit (I²C) address0x40.
3 SDA;4 SCLPicoGP0 physical1 andGP1 physical2, each with4.7kΩ to3.3V.
5 VS;6 GNDPico3V3 OUT physical36 andGND physical38;100nF acrossVS/GND near the IC.
7 IN−;8 IN+Downstream and upstream shunt sense points respectively, each through10Ω. Both remain near the5V positive rail.

Connect the bench return, IC ground and Pico ground. The two positive supplies remain separate: Pico3.3V powers the IC logic; the bench5V powers the resistor load. Turn on the Pico/IC supply first, then the5V fixture; turn the5V fixture off first during shutdown. Make changes with both off. There are no motors, batteries, large capacitors or deliberate short-circuit tests in this fixture. The10Ω sense resistors follow the manufacturer's input-transient guidance; they do not certify arbitrary transient immunity or remove the need to inspect wiring.

Predict signal size and heat

For an ideal5.00V source and nominal resistances, I=5/(100+0.100)=0.049950A. The shunt drop is about4.995mV. The100Ω load dissipates I²R≈0.2495W, so the1W part provides margin subject to its mounting and derating. It can still become warm. The shunt dissipates about0.0002495W, much less in this particular fixture.

The selected shunt full-scale range is±40mV, corresponding nominally to±0.4A with0.100Ω. That range is not a recommendation to raise current: our100mA supply limit and100Ω load remain unchanged. At a10µV shunt-register step, one count represents0.1mA for this shunt. Resolution is not accuracy; an offset of tens of microvolts, resistor tolerance and contact placement can be important compared with a5mV signal.

Choose Current_LSB=100µA, where LSB means least significant bit, the value represented by one register count. The nominal register setting is CAL=trunc(0.04096/(0.0001×0.100))=4096. Power scaling is20times current scaling, or2mW per power-register count. The code fixes4096 explicitly for this exact circuit instead of allowing floating-point rounding to select a neighboring value silently.

A fresh result has a protocol

Protocol distinguishes calibration zero from physical zero, triggers a shunt-and-bus one-shot, waits for CNVR, reads Power last among measurement registers, then checks configuration and calibration before READY.
Original register-state diagram. The500-count example is synthetic and illustrates scaling, not observed performance.

I²C, Inter-Integrated Circuit, carries addressed two-wire transactions. A register is a small named storage location inside the IC. Words are transferred most-significant byte first, so bytes0x01,0xF4 form the integer500. Shunt and Current are signed16-bit two's-complement values:0xFE0C represents−500, not a very large positive current. Bus data must be shifted right by three before multiplying by4mV; its low bits include status information.

The program writes Calibration4096 and verifies it after a10µs wait, exceeding the documented4µs register-update interval. Each sample checks calibration, writes configuration0x019B and thereby requests a new shunt-and-bus one-shot. The chosen fields are16V bus range,±40mV shunt range,12-bit conversions andMODE011. Shunt and bus conversions occur successively, not simultaneously.

CNVR, conversion ready, is bit1 of the Bus Voltage register0x02. OVF, math overflow, is bit0. The program polls the bus register until ready, rejects overflow, then reads results and checks configuration and calibration again. It reads Power last among the measurement registers because that read clears CNVR; configuration and calibration guards follow that read. The detailed register section of the datasheet defines this behavior; earlier overview prose refers loosely to a “Status register” and a nonexistent convert pin. No extra convert pin or separate status address is invented here.

The default limit is25polls with2ms between not-ready responses. This bounds the number of polls; it is not a guaranteed50ms wall-clock deadline, because bus transactions and interpreter work take time. SoftI2C also has a50,000µs timeout parameter. A disconnected or stuck bus still needs real hardware verification of its failure behavior. A timeout, lost Calibration register, changed configuration or range-edge code becomes FAULT with current_A=None, not a reassuring zero.

Complete program

Copy the complete file to the Pico and run it with the USB serial console. No third-party monitor library is required. USB means Universal Serial Bus. Stop with Ctrl+C to inspect the retained sensor object at the REPL, the interactive read–evaluate–print loop.

Complete program
"""Exact INA219AIDR +0.100ohm lab; MicroPython1.26RP2. Not an ammeter standard."""
ADDRESS = 0x40
CALIBRATION = 4096  # Fixed0.100ohm and100uA/current-register-count.
CONFIG_TRIGGER = 0x019B  # 16V bus,40mV shunt,12bit each,shunt+bus one-shot.
CURRENT_LSB_A = 0.0001
POWER_LSB_W = 0.002
SHUNT_OHM = 0.100


def signed16(raw):
    if type(raw) is not int or not 0 <= raw <= 0xFFFF:
        raise ValueError("not a16bit word")
    return raw - 65536 if raw & 0x8000 else raw


class Monitor:
    def __init__(self, bus, sleep_us, sleep_ms):
        self.bus = bus
        self.sleep_us = sleep_us
        self.sleep_ms = sleep_ms

    def read_word(self, register):
        data = self.bus.readfrom_mem(ADDRESS, register, 2)
        if len(data) != 2:
            raise ValueError("short register read")
        return (data[0] << 8) | data[1]

    def write_word(self, register, value):
        self.bus.writeto_mem(ADDRESS, register, bytes((value >> 8, value & 255)))
        self.sleep_us(10)  # More than4us register-update requirement.

    def calibrate(self):
        self.write_word(5, CALIBRATION)
        if self.read_word(5) != CALIBRATION:
            raise ValueError("calibration write/readback failed")

    def sample(self, max_polls=25):
        if type(max_polls) is not int or not 1 <= max_polls <= 100:
            raise ValueError("bounded poll count required")
        if self.read_word(5) != CALIBRATION:
            raise ValueError("CALIBRATION_MISSING_OR_CHANGED")
        self.write_word(0, CONFIG_TRIGGER)  # Retriggers even if same mode was set.
        for _ in range(max_polls):
            bus_word = self.read_word(2)  # CNVR is bit1 here; reading bus does not clear it.
            if bus_word & 1:
                raise ValueError("MATH_OVERFLOW")
            if bus_word & 2:
                break
            self.sleep_ms(2)
        else:
            raise ValueError("CONVERSION_NOT_READY")
        shunt_raw = signed16(self.read_word(1))
        current_raw = signed16(self.read_word(4))
        power_raw = self.read_word(3)  # Last measurement read; clears CNVR.
        config = self.read_word(0)
        if (config & 0x3FF8) != (CONFIG_TRIGGER & 0x3FF8):
            raise ValueError("CONFIGURATION_CHANGED")
        if self.read_word(5) != CALIBRATION:
            raise ValueError("CALIBRATION_LOST_DURING_SAMPLE")
        if abs(shunt_raw) >= 4000:
            raise ValueError("SHUNT_AT_OR_OUTSIDE40mV_RANGE")
        if (bus_word >> 3) >= 4000:
            raise ValueError("BUS_AT_OR_OUTSIDE16V_RANGE")
        # CAL4096 means the current and shunt integer results should agree.
        if abs(current_raw - shunt_raw) > 1:
            raise ValueError("CURRENT_SHUNT_INCONSISTENT")
        shunt_v = shunt_raw * 0.00001
        current_a = current_raw * CURRENT_LSB_A
        return {"state": "READY", "bus_V": (bus_word >> 3) * 0.004,
                "shunt_V": shunt_v, "current_A": current_a,
                "raw_shunt_current_A": shunt_v / SHUNT_OHM,
                "power_W": power_raw * POWER_LSB_W if current_a >= 0 else None,
                "power_register_raw": power_raw,
                "reverse_current": current_a < 0,
                "simultaneous_measurement": False,
                "calibration_register": CALIBRATION}


def run(monitor, sleep, output=print, samples=None):
    try:
        monitor.calibrate()
    except (OSError, ValueError) as error:
        output({"state": "FAULT", "current_A": None, "error": str(error)})
        return
    count = 0
    while samples is None or count < samples:
        try:
            output(monitor.sample())
        except (OSError, ValueError) as error:
            output({"state": "FAULT", "current_A": None, "error": str(error)})
        count += 1
        sleep(1)


if __name__ == "__main__":
    from machine import Pin, SoftI2C
    from time import sleep, sleep_ms, sleep_us
    sensor = Monitor(SoftI2C(sda=Pin(0), scl=Pin(1), freq=100000, timeout=50000), sleep_us, sleep_ms)
    try:
        run(sensor, sleep)
    except KeyboardInterrupt:
        print("Stopped; sensor remains available at the REPL")

The program reports the chip's unsigned power register only as a positive-flow power value. For negative current it leaves power_W=None and preserves the raw register separately; it does not invent a signed power format. This fixed-range driver is intentionally small. It is not an auto-ranging API, current integrator or battery fuel gauge. Use it as the only configuration-writing program on this bus.

See the difference between no current and no scaling

  1. Identify and inspect. Record the IC marking, SOIC pin1 orientation, shunt value/provenance, load rating and source current limit. Confirm common ground and the3.3V bus levels before attaching the IC.
  2. Run the baseline. Enable the5V source with its100mA limit and run the complete program. Record raw shunt-derived current, Current register value, bus voltage and state. Compare with the nominal calculation, keeping actual source/load tolerances visible.
  3. Remove the load current safely. Turn off both supplies, open the load path, restore the same supply sequence and run again. A qualified zero now means that a completed conversion with the expected configuration produced zero counts; it still has analog offset/quantization limits.
  4. Lose scaling in software. With the normal load restored, stop the loop. Write zero deliberately with sensor.write_word(5,0). The raw shunt register can remain nonzero while a subsequent sensor.sample() raises CALIBRATION_MISSING_OR_CHANGED. Re-run sensor.calibrate() explicitly before trying again. This experiment changes configuration, not the load current.
  5. Reset evidence. A real device power cycle resets its state; use the normal full power-down sequence, then restart the program. Record the behavior rather than assuming an old Calibration value survived. The host suite injects a reset during a sample and verifies that the result is rejected.
  6. Reproduce bus/readiness faults on the host. Tests provide short reads, never-ready conversion, overflow, configuration changes and reverse-current words. Do not induce an electrical short or reverse a powered source to manufacture these cases.

Use the blank fixture worksheet. If you compare against another instrument, document its current connection and burden voltage; accidentally putting a current-range meter across the source would short it. A voltage-only comparison across the shunt avoids interrupting the force path but requires sufficient millivolt accuracy.

Interpret discrepancies before adding decimal places

If the two current calculations disagree substantially, check the Calibration register and byte order first. WithCAL4096, the signed Current and Shunt integers should essentially agree; the code permits one count of difference and rejects larger disagreement. Agreement is still not independent calibration: both numbers originate from the same ADC result. They can agree perfectly while the physical shunt is wrong.

A raw shunt code of500 corresponds to5mV and50mA only for the declared0.100Ω resistor. If its true resistance is1% high, using the nominal resistance biases the calculated current by approximately1%. Heating, input offsets, sense-series effects and wiring add other terms. The load's own tolerance changes the physical current even with a perfect monitor. Start with an error budget and a repeatable fixture; more printed digits cannot remove these causes.

Challenge: Current=0, raw shunt=500, Calibration=0

Is the load off? The raw shunt code is consistent with a5mV drop, while the calculation engine lacks its scaling. Report an invalid/unqualified current, not0A. Program and verify the intended Calibration setting, request a new conversion and qualify readiness before comparing results. None of that proves the physical shunt value.

The host suite executes the actual delivered byte parser, one-shot sequence, guard checks and console loop. It does not test IC input transients, sensor accuracy, the PicoI²C peripheral or a finished instrument. Continue with the electronics lessons for voltage/current/resistor fundamentals. Any logger built on this lesson must store the state alongside the number.

Primary references