Skip to content

Example: geared DC motor

The TT gear motor drives a wheel through a 48:1 gearbox. Neither half contains the word “motor”. The circuit logic is a winding, and the component logic is a torque and a friction on a hinge.

  • Connections: two pins, Red and Black.
  • Rendering: a Wheel mesh with a part body and a hinge joint on the axle.
  • Circuit logic properties: R, L, Ke, BrushDrop, MaxAmps and FailTime.
  • Component logic properties: GearRatio, Efficiency, Direction and RotorInertia.

The two halves share four numbers:

UserData Direction Meaning
[0] in Rotor speed at the motor shaft, rad/s (wheel speed × gear ratio)
[1] out Winding current from the last settled tick, A
[2] out Charge through the winding since the run began, A·s
[3] out Time over which that charge was counted, s

The last two let the physics side average the current over its own step. With PWM at a few hundred hertz the current swings every cycle, and a single sample would land at a random point on the wave.

A brushed motor seen from its leads is V = I·R + L·dI/dt + Ke·ω. It is one backward-Euler branch between the two pins, a conductance of 1 / (R + L/dt) in parallel with a current source that carries the inductor’s history minus the back-EMF. A stalled rotor makes the back-EMF zero, so it draws (V − brush drop) / R with no special case.

This block is the complete circuit logic, exactly as you type it into the editor (see What you write).

private double _geff, _isrc, _i, _iPrev;
private double _overTime; // seconds over MaxAmps; survives Reset, clears on PowerCycle
private double _charge, _counted;
public override bool ConnectionsAreConnected(int l, int r) { return true; }
public override void Reset()
{
base.Reset();
_i = 0.0; _iPrev = 0.0; _charge = 0.0; _counted = 0.0;
}
public override void PowerCycle()
{
base.PowerCycle();
_overTime = 0.0;
}
public override void StampInitial()
{
Simulation.StampRightSideDynamic(ConnectionNodes[Connections.Red]);
Simulation.StampRightSideDynamic(ConnectionNodes[Connections.Black]);
}
public override void PreStep()
{
// Published before the solve, so the physics thread only ever sees a settled current.
UserData[1] = _iPrev;
UserData[2] = _charge;
UserData[3] = _counted;
double dt = Simulation.TimeStep;
double r = Properties.R > 1e-6 ? Properties.R : 1e-6;
double l = Properties.L > 0.0 ? Properties.L : 0.0;
double emf = Properties.Ke * UserData[0] + Properties.BrushDrop * Math.Tanh(_iPrev / 0.02);
_geff = 1.0 / (r + l / dt);
_isrc = _geff * ((l / dt) * _iPrev - emf);
}
public override void Step()
{
Simulation.StampConductanceDynamic(ConnectionNodes[Connections.Red], ConnectionNodes[Connections.Black], _geff);
Simulation.StampCurrentSourceDynamic(ConnectionNodes[Connections.Red], ConnectionNodes[Connections.Black], _isrc);
}
public override void CalculateCurrent()
{
double v = ConnectionVoltages[Connections.Red] - ConnectionVoltages[Connections.Black];
_i = _geff * v + _isrc;
_iPrev = _i;
double dt = Simulation.TimeStep;
_charge += _i * dt;
_counted += dt;
SetCurrent(Connections.Red, _i);
}
public override bool CheckFailure()
{
// Heat over real time above the limit, leaking back down below it.
double dt = Simulation.TimeStep;
if (Math.Abs(_i) > Properties.MaxAmps)
{
_overTime += dt;
if (_overTime >= Properties.FailTime)
{
Fail("winding burnt out, the rotor was held stalled too long");
return true;
}
}
else
{
_overTime = Math.Max(0.0, _overTime - dt);
}
return false;
}

Every physics step, this reads the mean current, turns it into torque on the wheel’s hinge, and hands the rotor speed back so the winding can make its back-EMF. That loop makes the wheel settle at a speed and makes the current rise when the wheel is held.

The gearbox is two numbers: a ratio and an efficiency. The push on the wheel is the motor torque times the ratio. The loss is friction on the hinge, because tooth friction grows with the torque through the teeth and always opposes the turn, so a brake can never make energy.

This block is the complete component logic, as you type it.

private double _ke = 0.00421; // Kt equals Ke in SI units; read from the circuit logic
private double _ratio = 48.0;
private double _eff = 0.267;
private double _dir = 1.0;
private double _lastCharge, _lastCounted;
private bool _haveLast;
public override void Initialize() => ApplyProperties();
public override void OnPropertyUpdated(string propertyName) => ApplyProperties();
public override void OnCircuitPropertyUpdated(string propertyName)
{
double ke = CircuitProperties.Ke;
if (ke > 0.0) _ke = ke;
}
private void ApplyProperties()
{
_ratio = Properties.GearRatio > 0.0 ? Properties.GearRatio : 1.0;
_eff = Math.Max(0.0, Math.Min(1.0, Properties.Efficiency));
_dir = Properties.Direction < 0.0 ? -1.0 : 1.0;
// The rotor's inertia as the wheel feels it, through the ratio squared.
double rotor = Properties.RotorInertia > 0.0 ? Properties.RotorInertia : 0.0;
Rendering.Wheel.Rigidbody.ReflectedInertia = rotor * _ratio * _ratio;
}
public override void Update(double delta) { }
public override void PhysicsUpdate(double delta)
{
if (Circuit == null) return;
// Mean current since the last step, from the winding's running totals.
double charge = Circuit.UserData[2];
double counted = Circuit.UserData[3];
double amps = Circuit.UserData[1];
if (_haveLast && counted > _lastCounted) amps = (charge - _lastCharge) / (counted - _lastCounted);
_lastCharge = charge;
_lastCounted = counted;
_haveLast = true;
// The rotor's speed, not the wheel's: the winding owns Ke and only needs how fast it spins.
double rotorSpeed = Rendering.Wheel.Rigidbody.SpinRate * _ratio * _dir;
double push = _ke * amps * _ratio;
bool motorDrives = amps * rotorSpeed >= 0.0 || Math.Abs(rotorSpeed) < 1.0;
double loss = Math.Abs(push) * (motorDrives ? 1.0 - _eff : 1.0 / Math.Max(_eff, 1e-3) - 1.0);
Rendering.Wheel.Rigidbody.ApplyTorque(push * _dir);
Rendering.Wheel.Rigidbody.ApplyJointFriction(loss);
Circuit.UserData[0] = rotorSpeed;
}

The torque acts about the wheel’s hinge, and the same torque pushes back on whatever the motor body is fixed to, so a robot rocks as it pulls away just as a real one does.