|
Complete Guide to Motor Control: Stepper/Servo/DC Motors + Closed-Loop Control in Practice

Complete Guide to Motor Control: Stepper/Servo/DC Motors + Closed-Loop Control in Practice

Introduction

Motor control is one of the most fundamental and important skills in embedded development. Whether you’re building a CNC engraver, robot joints, a smart car, or a solar power system, precise motor control is essential.

This guide is built around 5 hands-on projects, covering stepper motors (A4988), servos (SG90/MG996), DC motors (L298N PWM), encoder-based closed-loop PID control, and solar charge controllers (PWM vs MPPT) — consolidating scattered knowledge into a single comprehensive reference.


1. Stepper Motor Precision Control: A4988 Driver Deep Dive

1.1 Why Choose a Stepper Motor?

Stepper motors allow precise angular control without the need for an encoder, enabling open-loop position control. A NEMA 17 (42mm) stepper motor paired with an A4988 driver is the core solution for CNC engravers, 3D printers, and robot gimbals.

1.2 Bill of Materials

ItemModelPriceNotes
Stepper MotorNEMA 17 (42mm)¥25-351.8° step angle, 1.5A
DriverA4988¥8-12Max 2A, supports microstepping
Dev BoardArduino Uno¥25Or any compatible board
Potentiometer10kΩ¥2Adjust motor current
Capacitor100μF¥1Power supply filtering
Power Supply12V 2A¥30Motor power

Total cost: approximately ¥100

1.3 A4988 Key Pins

+-----+
        VMOT ──────┤1   16│────── GND
         GND ──────┤2   15│────── 1B
          VDD ──────┤3   14│────── 1A
          2B ───────┤4   13│────── 2A
          2A ───────┤5   12│────── GND
        MS1 ────────┤6   11│────── VDD
        MS2 ────────┤7   10│────── STEP
        MS3 ────────┤8    9│────── DIR
                    +-----+
  • VMOT/GND: Motor power (8-35V), a 100μF decoupling capacitor is required
  • VDD/GND: Logic power (3-5.5V), connect to Arduino 5V
  • 1A/1B/2A/2B: Motor coil outputs
  • STEP: Pulse signal — each pulse advances one step
  • DIR: Direction control — HIGH for forward, LOW for reverse
  • MS1/MS2/MS3: Microstepping configuration pins

1.4 Microstepping Configuration Table

MS1MS2MS3Microstep ModeSteps per Revolution
000Full step200
1001/2 step400
0101/4 step800
1101/8 step1600
1111/16 step3200

1/16 microstepping is recommended for smoother operation and lower noise.

1.5 Current Adjustment (Important!)

The A4988 sets motor current via the reference voltage Vref using the formula: Vref = Current × 0.8

For a 1.5A NEMA 17: Vref = 1.5 × 0.8 = 1.2V

Adjustment procedure:

  1. Disconnect motor power; prepare a multimeter (set to DC voltage)
  2. Power the A4988 (connect VDD only, not VMOT) to avoid motor movement
  3. Use a screwdriver to slowly turn the potentiometer on the A4988
  4. Measure the voltage at the potentiometer wiper relative to GND until the reading is close to 1.2V

1.6 Arduino Control Code

#include <AccelStepper.h>

// Define stepper pins
#define STEP_PIN 9
#define DIR_PIN 8

// Create stepper object
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);

void setup() {
  Serial.begin(9600);
  stepper.setMaxSpeed(1000);        // Max speed (steps/sec)
  stepper.setAcceleration(500);     // Acceleration (steps/sec²)
  stepper.moveTo(3200);             // One full revolution (@ 1/16 microstep)
  Serial.println("Motor starting");
}

void loop() {
  if (stepper.distanceToGo() == 0) {
    stepper.moveTo(-stepper.currentPosition());  // Reverse
    Serial.print("Current position: ");
    Serial.println(stepper.currentPosition());
  }
  stepper.run();
}

Install library: Arduino IDE → Tools → Manage Libraries → Search “AccelStepper” → Install

Advanced: Multi-Axis Synchronized Control (CNC 3-Axis)

#include <AccelStepper.h>

AccelStepper xAxis(AccelStepper::DRIVER, 2, 3);
AccelStepper yAxis(AccelStepper::DRIVER, 4, 5);
AccelStepper zAxis(AccelStepper::DRIVER, 6, 7);

void setup() {
  xAxis.setMaxSpeed(1000);
  yAxis.setMaxSpeed(1000);
  zAxis.setMaxSpeed(1000);
}

void loop() {
  xAxis.moveTo(1000);
  yAxis.moveTo(500);
  zAxis.moveTo(200);
  xAxis.run();
  yAxis.run();
  zAxis.run();
}

1.7 Speed Calculation

RPM = (Pulse Frequency × 60) / Steps per Revolution

For example, at 1/16 microstepping with 1000 pulses per second: RPM = (1000 × 60) / 3200 = 18.75 RPM

Note: The A4988’s maximum pulse frequency is approximately 200kHz, but in practice it’s recommended to stay below 50kHz — torque drops at higher speeds.

1.8 Common Troubleshooting

Motor hums but doesn’t turn:

  1. Check that coil wiring is correct — use a multimeter in continuity mode to identify coil pairs
  2. Increase the Vref current setting (but don’t exceed the motor’s rated current)
  3. Try turning the motor shaft by hand to check for mechanical binding

Driver overheating:

  1. Check if Vref is set too high — reduce to the motor’s rated current
  2. Add a heatsink (aluminum heatsinks work well)
  3. Check ventilation — avoid enclosed spaces
  4. Consider upgrading to a DRV8825 or TMC2208 (higher efficiency)

Motor jitters or moves erratically:

  1. Switch to a higher microstepping mode (1/16 or 1/32)
  2. Avoid resonance speed ranges (typically in the low-to-medium speed range)
  3. Check that all connections are secure — poor dupont wire contact is a common cause

Missed steps at high speed:

  1. Reduce acceleration to give the motor more response time
  2. Increase current within the rated range
  3. Check that the power supply is adequate (voltage sag reduces torque)

Arduino resets:

  1. Use separate power supplies for the motor and Arduino — share only ground
  2. Add a 100μF electrolytic capacitor across VMOT
  3. Verify all ground connections are solid
  4. Use optocouplers to isolate STEP/DIR signals if necessary

1.9 Project Applications

Once you’ve mastered stepper motor control, you can build:

  1. DIY mini CNC engraver (3-axis coordinated motion)
  2. 3D printer (with hotend and extruder)
  3. XY plotter robot (CoreXY configuration)
  4. Automated conveyor belt (precision feeding)
  5. Equatorial mount for astronomy (tracking celestial motion)
  6. Peristaltic pump (precise fluid control)

2. Servo Angle Control: SG90 vs MG996 Comparison

2.1 How Servos Work

A servo is an integrated closed-loop system: PWM signal → Control circuit → Motor → Gear train → Potentiometer feedback → Comparator circuit

The PWM signal has a 20ms period (50Hz), with pulse widths of 0.5–2.5ms corresponding to 0–180 degrees.

2.2 SG90 vs MG996 Core Comparison

FeatureSG90MG996
Gear MaterialPlasticMetal
BearingsPlastic bushingDual ball bearings
Torque1.6kg/cm10kg/cm
Dead bandLarge (~5°)Small (~2°)
Accuracy±5°±2°
Price¥8-12¥35-45

2.3 Measured Data

Target AngleSG90 MeasuredSG90 ErrorMG996 MeasuredMG996 Error
+3°+1°
45°47°+2°46°+1°
90°92°+2°91°+1°
135°133°-2°134°-1°
180°177°-3°178°-2°

Conclusion: The MG996 is significantly more accurate, with an average error of ±1° vs ±2-3° for the SG90.

2.4 Arduino Control Code

#include <Servo.h>

Servo myServo;
int lastAngle = -1;
const int DEAD_ZONE = 3;  // 3-degree dead zone to reduce jitter

void moveToAngle(int targetAngle) {
  if (abs(targetAngle - lastAngle) > DEAD_ZONE) {
    myServo.write(targetAngle);
    lastAngle = targetAngle;
    delay(300);
  }
}

void setup() {
  // Manually specify pulse width range (microseconds) — calibrate for your servo
  myServo.attach(9, 500, 2500);
  Serial.begin(9600);
}

void loop() {
  moveToAngle(0);
  delay(1000);
  moveToAngle(90);
  delay(1000);
  moveToAngle(180);
  delay(1000);
}

2.5 Common Issues

Servo doesn’t rotate: Check that the independent 5V supply is adequate (MG996 can peak at 1A+); confirm the PWM signal wire is connected to an Arduino PWM pin; verify that the Arduino and servo share a common ground; test with known-working code to rule out software issues.

Severe servo jitter: Insufficient power is the #1 cause — switch to a 5V 3A+ supply; add a dead zone (3-5°) in code; check for excessively long or loose PWM wires; avoid commanding angles beyond the servo’s actual range (some servos aren’t precisely 0-180°).

Arduino resets with multiple servos: The combined startup current of multiple servos causes the Arduino’s supply voltage to sag. Solutions: use an independent power supply (at least 5V 3A); add a 470μF bulk capacitor at the power supply; initialize servos one at a time to avoid simultaneous startup.

2.6 Selection Guide

  • Choose SG90: Budget-constrained, light load (<500g), low precision requirements, learning projects
  • Choose MG996: Need reliability and durability, heavier load (500g-1.5kg), ±2° precision required, long-term operation
  • Other options: MG90S (metal gears, great value), DS3235 (digital servo, ±0.5°, high-end projects), bus servos (daisy-chain multiple servos, saves I/O pins)

3. DC Motor PWM Speed Control: L298N in Practice

3.1 PWM Speed Control Principle

PWM (Pulse Width Modulation) controls average voltage by rapidly switching the power on and off:

Duty cycle 100%: ████████████████████  Average voltage = 5V
Duty cycle  50%:  ████████████████      Average voltage = 2.5V
Duty cycle  25%:  ████                  Average voltage = 1.25V

Arduino’s analogWrite() generates a ~490Hz PWM signal with a duty cycle range of 0-255.

3.2 L298N Forward/Reverse Control Logic

IN1IN2Motor State
HIGHLOWForward
LOWHIGHReverse
LOWLOWStop
HIGHHIGHStop (brake)

3.3 Basic Speed Control Code

const int ENA = 9;    // PWM speed control pin
const int IN1 = 8;
const int IN2 = 7;

void setup() {
  pinMode(ENA, OUTPUT);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  // Forward acceleration
  digitalWrite(IN1, HIGH);
  digitalWrite(IN2, LOW);
  for (int speed = 0; speed <= 255; speed += 10) {
    analogWrite(ENA, speed);
    delay(200);
  }

  // Reverse deceleration
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, HIGH);
  for (int speed = 255; speed >= 0; speed -= 10) {
    analogWrite(ENA, speed);
    delay(200);
  }
}

3.4 Soft Start / Soft Stop

Avoid mechanical shock from sudden motor start/stop:

void softStart(int targetSpeed, int stepTime = 10) {
  for (int s = 0; s <= targetSpeed; s += 5) {
    analogWrite(ENA, s);
    delay(stepTime);
  }
}

void softStop(int stepTime = 10) {
  int currentSpeed = 255;  // Assume currently at full speed
  for (int s = currentSpeed; s >= 0; s -= 5) {
    analogWrite(ENA, s);
    delay(stepTime);
  }
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, LOW);
}

3.5 Differential Steering for Dual Motors

Classic approach for smart car steering:

void setMotor(int motor, int speed) {
  int in1, in2, en;
  if (motor == 1) { in1 = IN1; in2 = IN2; en = ENA; }
  else { in1 = 3; in2 = 4; en = 10; }

  if (speed > 0) {
    digitalWrite(in1, HIGH);
    digitalWrite(in2, LOW);
  } else {
    digitalWrite(in1, LOW);
    digitalWrite(in2, HIGH);
    speed = -speed;
  }
  analogWrite(en, constrain(speed, 0, 255));
}

void turnLeft(int speed) {
  setMotor(1, -speed);  // Left motor reverse
  setMotor(2, speed);   // Right motor forward
}

3.6 Common Issues and Upgrade Options

Motor doesn’t spin: Check the motor supply voltage; make sure the ENA jumper cap is removed (required when using PWM); verify IN1/IN2 direction pins are correct; measure OUT1/OUT2 with a multimeter to confirm output.

Excessive heat: The L298N gets hot under high current — this is normal. Keep continuous current below 2A. If it’s too hot to touch, add a heatsink or consider upgrading to the TB6612FNG (MOSFET-based, higher efficiency, less heat).

Driver ModuleProsConsPrice
L298NCheap, well-documentedHigh heat, low efficiency¥15-25
TB6612FNGHigh efficiency, low heatLower current (1.2A)¥20-30
DRV8833Dual channel, overcurrent protectionRequires 3.3V logic¥15-25
VNH2SP30High current (30A)Expensive, large¥50-80

4. Encoder Feedback: Closed-Loop PID Control

4.1 Why Closed-Loop Control?

The fatal flaw of open-loop control: you don’t know how much the motor actually turned. Load changes, voltage fluctuations, and friction variations all cause the actual position to deviate from the expected one. Closed-loop control uses real-time encoder feedback for precise control — which is why CNC machines and robot joints all use closed-loop systems.

4.2 Encoder Types

TypeAccuracyCostContamination ResistanceTypical Resolution
OpticalHighMediumPoor1000-5000 PPR
MagneticMediumLowGood100-4096 PPR
CapacitiveHighHighModerate1000-10000 PPR

For DIY projects, the magnetic encoder AS5600 is recommended (I2C interface, 12-bit resolution, ¥15).

4.3 PID Control Principle

Error = Target Position - Actual Position
Output = Kp×Error + Ki×∫Error + Kd×d(Error)/dt
  • P (Proportional): Larger error → larger output. P alone has steady-state error
  • I (Integral): Accumulates historical error to eliminate steady-state error. Too much causes overshoot
  • D (Derivative): Predicts error trend to suppress overshoot. Sensitive to noise

4.4 Complete Closed-Loop Position Control Code

#include <Wire.h>
#include <AS5600.h>

AS5600 as5600;

const int PWM_PIN = 9;
const int IN1_PIN = 7;
const int IN2_PIN = 8;

// PID parameters
float Kp = 2.0, Ki = 0.5, Kd = 1.0;

int targetPosition = 0;
float integral = 0;
float lastError = 0;
unsigned long lastTime = 0;

void setup() {
  Serial.begin(115200);
  Wire.begin();
  as5600.begin(Wire);
  pinMode(PWM_PIN, OUTPUT);
  pinMode(IN1_PIN, OUTPUT);
  pinMode(IN2_PIN, OUTPUT);
  analogWrite(PWM_PIN, 0);
}

void loop() {
  if (Serial.available()) {
    targetPosition = Serial.parseInt();
    integral = 0;
    lastError = 0;
  }

  int currentPosition = as5600.readAngle();  // 0-4095
  int error = targetPosition - currentPosition;
  unsigned long currentTime = millis();
  float dt = (currentTime - lastTime) / 1000.0;

  if (dt > 0) {
    integral += error * dt;
    float derivative = (error - lastError) / dt;
    float output = Kp * error + Ki * integral + Kd * derivative;
    output = constrain(output, -255, 255);
    driveMotor(output);
    lastError = error;
    lastTime = currentTime;
  }

  Serial.print("Pos: "); Serial.print(currentPosition);
  Serial.print(" Err: "); Serial.print(error);
  Serial.print(" Out: "); Serial.println((int)output);
  delay(10);  // 100Hz control frequency
}

void driveMotor(float pwm) {
  if (pwm > 0) {
    digitalWrite(IN1_PIN, HIGH);
    digitalWrite(IN2_PIN, LOW);
    analogWrite(PWM_PIN, (int)pwm);
  } else {
    digitalWrite(IN1_PIN, LOW);
    digitalWrite(IN2_PIN, HIGH);
    analogWrite(PWM_PIN, (int)(-pwm));
  }
}

4.5 PID Tuning Tips

  1. P first, then D, then I: Start by increasing Kp until the system responds but begins to oscillate, then add Kd to damp oscillation, and finally add Ki to eliminate steady-state error
  2. Keep the control frequency stable: Use a timer rather than delay to ensure a fixed sampling period
  3. Integral clamping: Prevent integral windup
  4. Dead zone: Stop output when error is below a threshold to avoid continuous micro-adjustments that cause wear

4.6 Practical Application Scenarios

  • Gimbal cameras: Stabilize the camera, counteract hand shake
  • Robot joints: Precisely control robotic arm angles
  • Self-balancing vehicles: Real-time motor adjustments to maintain balance
  • CNC feed axes: Precise tool position control
  • Winches: Constant tension control

5. Solar Charge Controllers: PWM vs MPPT

5.1 PWM vs MPPT Comparison

FeaturePWMMPPT
Conversion Efficiency60%~75%90%~98%
Cost¥20~50¥100~500+
Circuit ComplexityLowHigh
Low Temp / Cloudy PerformanceAverageSignificantly better
DIY Difficulty★★☆☆☆★★★★☆

Recommendation: PWM is sufficient for small 12V systems (solar panel under 20W); for systems above 20W or to maximize solar energy harvest, go with MPPT.

5.2 PWM Charge Controller DIY

Core idea: Use a MOSFET as a switch, controlled by an MCU that adjusts duty cycle based on battery voltage. Total cost approximately ¥20.

Three charging stages (for 12V lead-acid batteries):

typedef enum {
    CHARGE_BULK,        // Constant current (full power)
    CHARGE_ABSORPTION,  // Constant voltage (gradually reducing current)
    CHARGE_FLOAT,       // Float charge (low current maintenance)
    CHARGE_OFF          // Charging stopped
} ChargeState_t;

5.3 MPPT Perturb & Observe Method

The core of MPPT is a DC-DC buck converter with a tracking algorithm. Total cost approximately ¥65.

void mppt_algorithm(void) {
    uint16_t v_solar = read_solar_voltage();
    uint16_t i_solar = read_solar_current();
    uint32_t power = (uint32_t)v_solar * i_solar / 1000;

    uint32_t delta_p = power - prev_power;
    int16_t delta_v = v_solar - prev_voltage;

    if (delta_p > 0) {
        // Power increased — continue perturbing in the same direction
        if (delta_v > 0) set_buck_duty(get_buck_duty() - duty_step);
        else set_buck_duty(get_buck_duty() + duty_step);
    } else {
        // Power decreased — reverse the perturbation direction
        if (delta_v > 0) set_buck_duty(get_buck_duty() + duty_step);
        else set_buck_duty(get_buck_duty() - duty_step);
    }
    prev_voltage = v_solar;
    prev_power = power;
}

How Perturb & Observe works:

  1. Each iteration adjusts the voltage/duty cycle by a small step (e.g., 50mV)
  2. Measure the solar panel’s output power before and after the adjustment
  3. If power increases, the adjustment direction is correct — continue in the same direction
  4. If power decreases, the direction was wrong — reverse it
  5. Repeat until the system stabilizes near the maximum power point

5.4 Solar Panel Sizing

Solar Panel Power (W) = Battery Capacity (Ah) × Battery Voltage (V) ÷ Effective Sun Hours × System Efficiency

Example for a 12V 50Ah lead-acid battery: 600Wh ÷ 5h ÷ 0.75 ≈ 160W

5.5 Lithium vs Lead-Acid Battery Charging Parameters

Parameter12V Lead-Acid12V Li-ion (4S)
Bulk Voltage14.4V16.8V
Absorption Voltage13.8V16.8V
Float Voltage13.2V14.6V
Low Voltage Cutoff11.5V12.0V
Max Charge Current0.1C~0.25C0.5C~1C

Warning: Lithium batteries must always be used with a BMS! The charge controller manages only the charging curve — the BMS provides the final safety safeguard.


Summary

This guide covers five core areas of motor control:

TopicKey Chip/ModuleCore Skills
Stepper MotorA4988Microstepping config, Vref current adjustment, AccelStepper library
ServoSG90/MG996PWM pulse width calibration, dead zone handling, multi-servo coordination
DC MotorL298NH-bridge control, PWM speed control, soft start, differential steering
Closed-Loop ControlAS5600 + TB6612PID tuning, encoder reading, position/speed closed-loop
Solar ControlSTM32 + MOSFETPWM/MPPT charging strategies, perturb & observe method

From open-loop to closed-loop, from a single motor to multi-axis coordination — mastering these topics will prepare you for the vast majority of embedded motor control projects. Suggested learning path: DC motor PWM → Stepper motor open-loop → Servo angle control → Encoder closed-loop PID → Solar power system.

Hope this complete guide is helpful!