Instrument Control

Measure Motor Current Accurately

Learn how to measure brushless motor current with a shunt and an INA228, size the shunt, choose the sampling rate and record mean and peak current with limits.

JJulien Buteau
intermediate10 min readOctober 27, 2026

Motor current is the measurement that catches a dragging bearing, a shorted winding or a bad ESC channel on a brushless motor bench, but only if the number is right. Learn how to choose a current sensing method, size a shunt for an INA228 monitor, set the sampling so that the mean and the peaks mean something, and record both with limits in a TofuPilot procedure.

Four Ways to Measure Motor Current

A drone motor bench usually has four current readings available. They are not equivalent.

MethodTypical resolutionBandwidthCalibratedPer motorUse it for
Shunt + INA228 (I2C, 20-bit)0.3 to 0.7 mA with a 0.5 to 1 mOhm shuntSet by conversion time (50 µs to 4.12 ms)Yes, by the shunt tolerance and the INA228 gain errorYes, one shunt per DC inputPass/fail limits
Hall-effect current sensorTens of mA, limited by offset and noiseTens to hundreds of kHzOffset drifts with temperature and remanenceYesGalvanic isolation, fast transients
ESC telemetry current0.01 A field, real accuracy unknownOne frame per motor per polling cycleNoYesCross-check, field diagnostics
Bench supply readback1 to 10 mA displayA few readings per secondYes, but averagedNo, total of all loadsSanity check of the total

A shunt read by a precision monitor is the only option that is calibrated, fast enough and attributable to one motor. A Hall sensor wins when you need isolation or the waveform of a sub-millisecond transient, at the cost of an offset you must zero before every run. The ESC telemetry and the supply readback are useful context, not measurements to put limits on.

On a 4-in-1 ESC there is one DC input for four motors. A shunt on that input measures the sum. To attribute current to one motor, run one motor at a time, or put the shunt between the supply and a single-channel ESC. The phase currents between ESC and motor are AC switching waveforms and are not where a DC shunt goes.

Know the Current Range Before Choosing Parts

The range sets the shunt value, its power rating and the monitor range. Example values measured on one 3115-size, 1250 KV motor (12N14P) on a 6S bus with a BLHeli_32 ESC:

ThrottleSpeedCurrent without propeller
3 %idleabout 0.1 A
25 %8,350 rpm0.65 A
70 %22,600 rpm2.65 A
100 %31,000 rpm3.5 to 4.5 A

With a propeller, the same motor draws tens of amps. For a typical propeller on 6S, 60 to 80 A at full throttle is a reasonable estimate, not a measured value. Size for the propeller case if the bench will ever run one, and check that the resolution still covers the 0.1 A idle case.

Size the Shunt

Three numbers decide the shunt: the full-scale voltage of the monitor, the power dissipated in the shunt, and the resolution at low current.

The INA228 measures the differential shunt voltage with a 20-bit delta-sigma ADC. In its default range (ADCRANGE = 0) the full scale is ±163.84 mV and the LSB is 312.5 nV. The bus voltage input accepts up to 85 V, so a 6S pack (25.2 V fully charged) is well inside it, high side or low side.

shunt-sizing.txt
V_shunt = I x R                 must stay below 163.84 mV at I_maxP_shunt = I^2 x R               shunt power rating >= 2 x P_shuntI_lsb   = 312.5 nV / R          current resolutionV_burden = I x R                voltage the motor no longer sees

Applied to an 80 A peak and a 100 A ESC rating:

ShuntV at 80 AP at 80 AP at 100 AResolutionMax current before clipping
0.5 mOhm40 mV3.2 W5 W0.625 mA327 A
1 mOhm80 mV6.4 W10 W0.3125 mA163 A
2 mOhm160 mV12.8 W20 W0.156 mA81.9 A

The 2 mOhm shunt clips at the first transient above 82 A and dissipates more heat than a small shunt can take. The 0.5 mOhm shunt keeps dissipation low, still resolves 0.1 A idle current to better than 1 %, and costs 40 mV of burden at 80 A, which is 0.18 % of a 22.2 V nominal bus. For a bench that never runs a propeller, a 5 mOhm shunt gives 25 mV at 5 A for 0.125 W.

Three details decide whether the shunt value on the label is the value you get:

  • Use a four-terminal shunt and Kelvin sense lines. At 0.5 mOhm, 0.1 mOhm of solder or screw contact resistance in the sense path is a 20 % error. The sense lines go to the dedicated sense terminals, never to the current lugs.
  • Mind the temperature coefficient. A shunt that heats from 25 °C to 85 °C with a TCR of 50 ppm/°C changes by 0.3 %. Choose a low-TCR alloy and mount the shunt where it can shed heat.
  • Route the sense pair together. Twist it and keep it away from the motor phase leads, which carry the ESC switching edges.

Choose the Sampling Rate, Averaging and What a Peak Means

The current drawn by an ESC is not a DC value. It carries ripple at the ESC PWM frequency (tens of kHz), and transients every time the throttle changes, because the motor needs extra torque to accelerate its rotor and propeller. Three settings of the INA228 decide what you see:

  • Conversion time (VSHCT, 50 µs to 4.12 ms per conversion). The delta-sigma ADC integrates over this window, so PWM ripple averages out inside one conversion.
  • Averaging count (AVG, 1 to 1024 conversions). The chip averages internally before updating the result register.
  • Read rate. Polling over I2C from Python reaches a few hundred readings per second at best, with jitter from the operating system.
GoalVSHCTAVGUpdate period (shunt and bus)What you get
Mean current at steady throttle540 µs16about 17 msLow noise, ripple and short spikes averaged out
Visible acceleration transients150 µs1about 0.3 ms, limited by the read rateNoisier samples, peaks of tens of ms are captured
Sub-millisecond eventsNot the INA228Use a Hall probe on an oscilloscope

The update period is VSHCT x AVG for each enabled channel added together: 2 x 540 µs x 16 = 17.3 ms with shunt and bus both converted. A peak recorded with this setting is the highest 17 ms average, not the instantaneous peak. That is the correct quantity for a thermal or supply-sizing limit, and the wrong one for a MOSFET stress limit. State which one your limit is in the measurement description.

Do not poll faster than the register updates. Reading the same value twice does not add information, and polling slower than the update rate with averaging off skips data between reads, which can make a ripple look like a spike or hide a real one.

Step 1: Write the INA228 Plug

The plug owns the I2C bus and returns plain numbers and lists. This example uses smbus2 on a Linux host with an I2C bus (a single-board computer, or a USB-to-I2C adapter exposed as /dev/i2c-N):

requirements.txt
smbus2>=0.4
plugs/ina228.py
60 lines
import timefrom smbus2 import SMBusREG_CONFIG = 0x00REG_ADC_CONFIG = 0x01REG_VSHUNT = 0x04REG_VBUS = 0x05VSHUNT_LSB = 312.5e-9  # V, ADCRANGE = 0 (+/-163.84 mV)VBUS_LSB = 195.3125e-6  # V# Conversion time codes for VBUSCT / VSHCT / VTCTCT_CODES = {50: 0, 84: 1, 150: 2, 280: 3, 540: 4, 1052: 5, 2074: 6, 4120: 7}AVG_CODES = {1: 0, 4: 1, 16: 2, 64: 3, 128: 4, 256: 5, 512: 6, 1024: 7}MODE_CONT_SHUNT_BUS = 0xBclass INA228:    def __init__(self, bus, address, shunt_ohm):        self.address = address        self.shunt_ohm = shunt_ohm        self.i2c = SMBus(bus)        self.i2c.write_i2c_block_data(self.address, REG_CONFIG, [0x80, 0x00])  # reset        time.sleep(0.01)        self.configure(540, 16)    def __del__(self):        if hasattr(self, "i2c"):            self.i2c.close()    def configure(self, conversion_us, averages):        ct = CT_CODES[conversion_us]        value = (MODE_CONT_SHUNT_BUS << 12) | (ct << 9) | (ct << 6) | (ct << 3) | AVG_CODES[averages]        self.i2c.write_i2c_block_data(self.address, REG_ADC_CONFIG, [value >> 8, value & 0xFF])        # Update period of one shunt + bus result, used to pace sampling        self.period_s = 2 * conversion_us * 1e-6 * averages    def _read20(self, register, signed):        b = self.i2c.read_i2c_block_data(self.address, register, 3)        raw = ((b[0] << 16) | (b[1] << 8) | b[2]) >> 4        if signed and raw & 0x80000:            raw -= 1 << 20        return raw    def read_current(self):        return self._read20(REG_VSHUNT, True) * VSHUNT_LSB / self.shunt_ohm    def read_bus_voltage(self):        return self._read20(REG_VBUS, False) * VBUS_LSB    def sample_current(self, duration_s):        """Return [times_s, currents_a] sampled at the register update rate."""        times, currents = [], []        t0 = time.monotonic()        while (now := time.monotonic() - t0) < duration_s:            times.append(round(now, 4))            currents.append(self.read_current())            time.sleep(self.period_s)        return [times, currents]

The current is computed from the shunt voltage register and the shunt value, which keeps the calibration in one visible number. The INA228 can also compute current internally with its SHUNT_CAL register. Both give the same result; the direct form is easier to audit.

Declare it with its bus, address and shunt value in with:, so a different station or a replaced shunt is a configuration change, not a code change:

procedure.yaml
21 lines
name: Motor Current Checkversion: 0.1.0execution:  workers: 1plugs:  - name: Current Monitor    key: current_monitor    python: plugs.ina228:INA228    scope: station    with:      bus: 1      address: 0x40      shunt_ohm: 0.0005  - name: Motor Driver    key: motor    python: plugs.motor:MotorDriver    scope: station    with:      port: /dev/ttyACM0

workers: 1 matters on a motor bench. TofuPilot runs independent phases in parallel by default, and two phases driving the same motor or reading the same I2C bus at once give meaningless results. The motor plug is whatever sets the throttle on your bench, for example a microcontroller generating DShot over USB serial.

Step 2: Record Mean and Peak Current With Limits

Record the samples as a multi-dimensional measurement, so the dashboard shows the trace, and put the limits on aggregations of that trace:

procedure.yaml
43 lines
main:  - name: Current at 25 % Throttle    key: current_25    python: phases.current:hold_and_measure    timeout: 30s    with:      throttle_pct: 25      settle_s: 2.0      measure_s: 3.0    measurements:      - name: Motor Current 25 %        key: motor_current        description: INA228, VSHCT 540 us, AVG 16. Peak is the highest 17 ms average.        x_axis:          legend: Time          unit: s        y_axis:          - legend: Current            key: current            unit: A            aggregations:              - type: mean                validators:                  - operator: ">="                    expected_value: 0.5                  - operator: "<="                    expected_value: 0.8              - type: max                validators:                  - operator: "<="                    expected_value: 1.2      - name: Bus Voltage        key: bus_voltage        unit: V        validators:          - operator: ">="            expected_value: 21.0          - operator: "<="            expected_value: 25.5teardown:  - name: Stop Motor    python: phases.current:stop_motor

The 0.5 to 0.8 A window brackets the 0.65 A measured on one motor without propeller. These are example values: set yours from a set of known-good motors, then tighten them with the Cpk the dashboard computes for the measurement.

phases/current.py
22 lines
import timedef hold_and_measure(motor, current_monitor, measurements, log, throttle_pct, settle_s, measure_s):    motor.ramp_to(throttle_pct)    # Let the acceleration transient pass before measuring steady current    time.sleep(settle_s)    times, currents = current_monitor.sample_current(measure_s)    measurements.motor_current.x_axis = times    measurements.motor_current.y_axis.current = currents    aggs = measurements.motor_current.y_axis.current.aggregations    aggs.mean = sum(currents) / len(currents)    aggs.max = max(currents)    measurements.bus_voltage = current_monitor.read_bus_voltage()    log.info(f"{len(currents)} samples, mean {aggs.mean:.3f} A, max {aggs.max:.3f} A")def stop_motor(motor):    motor.ramp_to(0)

The teardown phase runs even when a limit fails or the phase times out, so the motor is always brought back to zero. It is a convenience, not a safety function: the bench still needs a hardwired emergency stop that removes power.

Every run then stores the trace, the mean, the max and the bus voltage against the motor serial number. On the dashboard, a mean current that creeps up across a production lot points at bearings or magnets before the limit is crossed.

Why ESC Telemetry Current Is Not a Measurement

BLHeli_32 ESCs report current in their telemetry frame with a 0.01 A field. The field resolution says nothing about accuracy:

  • The ESC sensing is not calibrated per unit. A 4-in-1 ESC measures current with its own low-value shunt or the MOSFET drop, scaled by a firmware constant. Two ESCs of the same model read differently.
  • The offset dominates at low current. Without a propeller the motor draws 0.1 to 4.5 A, which is the bottom of a 100 A sensor range. Readings of 0.00 A at idle are common.
  • The update rate is low. Telemetry is requested one motor at a time, so each motor gets a fresh frame only once per polling cycle.

Record the ESC current as its own measurement without tight limits, next to the INA228 value. A large disagreement between the two flags a wiring or ESC problem; it does not decide pass or fail.

Common Pitfalls

Connecting the sense lines to the current lugs. Contact resistance becomes part of the shunt. Use the Kelvin terminals.

Sizing the shunt for the no-propeller bench, then fitting a propeller. A 10 mOhm shunt that read 5 A cleanly clips at 16 A and dissipates 64 W at 80 A.

Leaving the phases in parallel. Two phases commanding the motor at once ruin both readings. Set workers: 1 or chain the phases with depends_on.

Measuring during the acceleration transient. A mean that includes the spin-up is not a steady-state current. Settle first.

Calling a 17 ms average a peak current without saying so. Write the averaging window in the measurement description.

Putting limits on ESC telemetry current. It is uncalibrated and coarse at low current. Use it as a cross-check.

Key Points

  • A Kelvin-connected shunt read by an INA228 is the reference for motor current; Hall sensors, ESC telemetry and supply readback are secondary.
  • Size the shunt from V = I x R below 163.84 mV, P = I^2 x R with a 2x power margin, and resolution = 312.5 nV / R.
  • Conversion time and averaging decide whether you record a mean or a peak, and what "peak" means.
  • Record the trace as a multi-dimensional measurement with mean and max aggregations carrying the limits.
  • Run motor phases one at a time and stop the motor in teardown.

For the wider picture of motor and actuator test benches, see Motor and Actuator Testing with TofuPilot.

More Guides

Put this guide into practice