Article Summary

A jammed Great Ball Contraption module keeps its motor stalled, drawing current and making heat. Three layers stop that, from simplest to most capable, a slip clutch in the drive, an Arduino watchdog that cuts power when current is high and the shaft has stopped, and, for Powered Up motors, a Pybricks program that checks for a stall.

A Great Ball Contraption runs for hours, and a single stuck ball can leave a motor stalled, drawing current and heating up while nobody is watching. The fix is protection in layers: a slip clutch so the drive gives way instead of forcing the jam, an electronic watchdog that switches the motor off when it stops turning under load, and for Powered Up motors, a stall check in Pybricks.

Quick answer: put a Technic clutch gear in the drive, and add an Arduino with an INA219 current sensor and a Hall sensor that cuts the motor's power through a relay when the current is above your measured threshold and the shaft has stopped for several checks in a row. On Powered Up hubs, use Pybricks and stop the motor when stalled() returns true.

Layer 1: a slip clutch in the drive

The simplest protection is mechanical. A Technic 24-tooth clutch gear in the drive train slips when the load gets too high, so a jammed lift or wheel stops being forced while the motor keeps turning. It costs nothing in electronics, but the module still stops moving balls, so pair it with one of the layers below to notice the jam and switch the motor off.

Layer 2: an Arduino watchdog

One sensor is not enough. High current alone can mean a heavy but moving load, and no rotation alone can mean the motor was switched off on purpose. Together they are a reliable sign of a jam:

Sensor Measures Jam sign
Hall sensor and a magnet on the shaft Rotation No pulses
INA219 current sensor Motor current Above your threshold

Two details matter in practice. A motor draws a surge of current as it starts, while it is not yet turning, so the watchdog ignores the first two seconds after power is switched on. And the jam condition has to hold for several checks in a row before the power is cut, so one odd reading does not stop the show.

Parts

Which motors it suits

Any motor whose current you can route through the sensor: a 9 V DC motor, or a Power Functions motor fed through a spliced extension cable. Put the INA219's shunt and the relay contact in series with one of the two wires that carry the motor's current. Powered Up motors are driven by their hub and are better protected with Pybricks, below.

Wiring

Motor supply +  ->  INA219 VIN+
INA219 VIN-     ->  relay COM
relay contact   ->  motor
motor           ->  motor supply -

INA219 SDA, SCL  ->  Nano A4, A5
INA219 VCC, GND  ->  Nano 5V, GND
Hall sensor OUT  ->  Nano D2 (interrupt pin)
Reset button     ->  Nano D3 to GND
Relay IN         ->  Nano D7
Buzzer           ->  Nano D8
LED (with resistor) -> Nano D9

All grounds connected together.

The INA219 sits on the high side: current flows in at VIN+ and out at VIN-, through the shunt, before it reaches the relay and the motor. Use the relay's normally open contact if you want the motor off whenever the watchdog is unpowered, or normally closed if the module must keep running without it.

Arduino code

It uses Adafruit's INA219 library. Many relay modules switch on when their input is low; if yours switches on when it is high, swap the two relay constants.

#include <Wire.h>
#include <Adafruit_INA219.h>

#define HALL_PIN   2   // interrupt-capable on the Nano
#define RESET_PIN  3   // push button to ground
#define RELAY_PIN  7
#define BUZZER_PIN 8
#define LED_PIN    9

#define RELAY_ON  LOW   // swap these two if your relay module is active-high
#define RELAY_OFF HIGH

const float CURRENT_LIMIT_MA = 800;           // set from your own measurements
const float MIN_RPM = 10;
const unsigned long CHECK_INTERVAL_MS = 500;
const unsigned long STARTUP_GRACE_MS = 2000;  // ignore the start-up surge
const byte CHECKS_TO_CONFIRM = 3;             // a jam must last this many checks

Adafruit_INA219 ina219;

volatile unsigned long pulseCount = 0;
unsigned long lastPulseCount = 0;
unsigned long lastCheckTime = 0;
unsigned long motorStartTime = 0;
byte suspectChecks = 0;
bool jamDetected = false;

void hallPulseISR() { pulseCount++; }

void enableMotor() {
  jamDetected = false;
  suspectChecks = 0;
  motorStartTime = millis();
  digitalWrite(RELAY_PIN, RELAY_ON);
  digitalWrite(LED_PIN, LOW);
}

void cutMotor() {
  jamDetected = true;
  digitalWrite(RELAY_PIN, RELAY_OFF);
  tone(BUZZER_PIN, 2000, 1000);
  Serial.println("Jam detected: motor power cut. Press reset to restart.");
}

void setup() {
  Serial.begin(115200);
  pinMode(HALL_PIN, INPUT_PULLUP);
  pinMode(RESET_PIN, INPUT_PULLUP);
  pinMode(RELAY_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);

  if (!ina219.begin()) {
    Serial.println("INA219 not found");
    while (true) {}
  }
  attachInterrupt(digitalPinToInterrupt(HALL_PIN), hallPulseISR, FALLING);
  enableMotor();
}

void loop() {
  unsigned long now = millis();

  if (jamDetected) {
    digitalWrite(LED_PIN, (now / 250) % 2);  // blink while jammed
    if (digitalRead(RESET_PIN) == LOW) enableMotor();
    return;
  }

  if (now - lastCheckTime < CHECK_INTERVAL_MS) return;
  lastCheckTime = now;

  noInterrupts();
  unsigned long pulses = pulseCount - lastPulseCount;
  lastPulseCount = pulseCount;
  interrupts();

  float rpm = pulses * 60000.0 / CHECK_INTERVAL_MS;
  float current_mA = ina219.getCurrent_mA();

  Serial.print(current_mA);
  Serial.print(" mA, ");
  Serial.print(rpm);
  Serial.println(" rpm");

  if (now - motorStartTime < STARTUP_GRACE_MS) return;

  if (current_mA > CURRENT_LIMIT_MA && rpm < MIN_RPM) {
    if (++suspectChecks >= CHECKS_TO_CONFIRM) cutMotor();
  } else {
    suspectChecks = 0;
  }
}

The pulse count is read with interrupts paused because an Arduino Nano reads a four-byte number in several steps, and an interrupt in the middle would corrupt it. With one magnet, each pulse is one turn of whatever the magnet is on.

Tuning

  1. Run the module normally and note the current in the serial monitor.
  2. Hold the drive still for a moment and note the stall current.
  3. Set CURRENT_LIMIT_MA between the two.
  4. If slow-moving parts trigger false alarms, lower MIN_RPM or raise CHECKS_TO_CONFIRM; if a slow jam is missed, do the opposite.

Several motors

// One INA219 per motor, addresses set with the board's solder jumpers
Adafruit_INA219 inaLift(0x40);
Adafruit_INA219 inaWheel(0x41);
Adafruit_INA219 inaConveyor(0x44);

Each board needs begin() in setup(), and each motor its own Hall sensor and relay. Four addresses are available: 0x40, 0x41, 0x44 and 0x45.

Layer 3: Powered Up motors with Pybricks

A Powered Up motor gets its power from the hub, so there is no wire to splice. Pybricks, open-source firmware for LEGO hubs, gives every motor a stalled() check, which is true when the motor cannot reach its target speed even at full power. A watchdog is a few lines:

from pybricks.pupdevices import Motor
from pybricks.parameters import Port
from pybricks.tools import wait

lift = Motor(Port.A)
lift.run(500)  # degrees per second

while True:
    if lift.stalled():
        lift.stop()
        print("Jam detected: lift stopped")
        break
    wait(100)
LEGO Technic Hub
Technic Hub
$89.99

Powered Up Technic Hub, which can run Pybricks programs for one or more motors.

View on

LEGO Technic Large Motor
Technic Large Motor
$39.99

Powered Up Technic Large Motor, a motor with built-in rotation sensing that Pybricks can check for a stall.

View on

Resources