Problem: Which Language Should You Use to Control Motors?
You're starting a motor control project — a robot arm, CNC machine, or EV drivetrain module — and the Arduino vs. MicroPython debate is stalling you. Both run on microcontrollers, both have motor libraries, but they behave very differently under load.
You'll learn:
- Where Arduino (C++) wins on timing-critical motor tasks
- Where MicroPython wins on iteration speed and readability
- How to pick based on your actual hardware and use case
Time: 20 min | Level: Intermediate
Why This Happens
Motor control has two very different worlds: real-time pulse generation (stepper step/dir signals, PWM duty cycles, encoder interrupts) and high-level coordination (speed ramping, PID loops, serial commands). The right tool depends on which world you live in.
Arduino runs C++ compiled directly to machine code. MicroPython runs bytecode on a small VM. That gap matters — but only in specific scenarios.
Common confusion points:
- "MicroPython is too slow" — true for bare-metal ISRs, false for most PID loops
- "Arduino is too hard" — true for beginners, false once you know the ecosystem
- Choosing a language before choosing hardware, which forces the wrong trade-off
Solution
Step 1: Identify Your Timing Requirements
Motor control tasks split cleanly into two categories. Run this mental check first.
Hard real-time (microsecond precision):
- Stepper pulse generation (step/dir at >10kHz)
- Encoder counting at high RPM
- Current-sense ADC sampling in FOC loops
Soft real-time (millisecond precision):
- PID velocity/position loops (typically 1–10ms)
- Speed ramping profiles
- CAN/UART command processing
Hard real-time → Arduino (or bare-metal C)
Soft real-time → Either works; MicroPython is faster to develop
Expected: You should now know which category your project falls into.
If unclear:
- Stepper driver (STEP/DIR pulse): Hard real-time → Arduino
- Brushed DC with encoder feedback: Soft real-time → either
- BLDC with FOC: Hard real-time → Arduino or dedicated FOC chip (SimpleFOC)
Step 2: Compare the Core Trade-offs
Timing and Interrupt Performance
Arduino runs ISRs (interrupt service routines) in native C++ — no VM overhead.
// Arduino: Stepper pulse ISR — executes in ~200ns on ATmega328P
ISR(TIMER1_COMPA_vect) {
// Toggle STEP pin at precise intervals
PORTD ^= (1 << STEP_PIN); // Direct port manipulation, not digitalWrite()
}
MicroPython's machine.Timer callbacks have ~50–200µs latency depending on the board. Fine for PID, not fine for step pulses above ~5kHz.
# MicroPython: Timer-based callback — latency varies by GC pressure
from machine import Timer, Pin
step = Pin(2, Pin.OUT)
def step_pulse(t):
step.toggle() # 50-200µs jitter — acceptable for slow steppers only
tim = Timer(0, freq=1000, callback=step_pulse)
If it fails:
- MicroPython misses steps at high speed: Expected. Move pulse generation to Arduino or use a dedicated stepper driver IC (TMC2209, DRV8825) that handles pulses internally
- Arduino ISR conflicts with Serial: Disable interrupts only for the critical section, not the whole loop
PID Loop Implementation
For velocity or position PID, MicroPython is genuinely competitive — and much faster to tune.
# MicroPython: Clean, readable PID loop
import time
class PID:
def __init__(self, kp, ki, kd):
self.kp, self.ki, self.kd = kp, ki, kd
self.prev_error = 0
self.integral = 0
def compute(self, setpoint, measured, dt):
error = setpoint - measured
self.integral += error * dt
derivative = (error - self.prev_error) / dt
self.prev_error = error
# Returns output — clamp this before writing to PWM
return self.kp * error + self.ki * self.integral + self.kd * derivative
Equivalent Arduino code is more verbose but runs 10–50x faster — relevant only if your loop rate exceeds ~500Hz.
// Arduino: PID loop — necessary if you need >500Hz update rate
float computePID(float setpoint, float measured, float dt) {
static float integral = 0, prevError = 0;
float error = setpoint - measured;
integral += error * dt;
float derivative = (error - prevError) / dt;
prevError = error;
return kp * error + ki * integral + kd * derivative;
}
Motor Driver Library Support
Both ecosystems have solid driver support, but the depth differs.
Arduino:
- AccelStepper — battle-tested, handles acceleration curves
- SimpleFOC — full field-oriented control for BLDC
- Encoder library — hardware interrupt-based, handles high RPM
MicroPython:
machine.PWM— built-in, sufficient for brushed DCsteppercommunity libraries — functional but less tested- No SimpleFOC equivalent; FOC requires Arduino or C
# MicroPython: Brushed DC motor via PWM — this is where it shines
from machine import Pin, PWM
pwm = PWM(Pin(15), freq=20000) # 20kHz avoids audible whine
def set_speed(percent):
# Clamp to 0-100, map to duty cycle
duty = int(min(max(percent, 0), 100) * 1023 / 100)
pwm.duty(duty)
set_speed(75) # 75% speed
Step 3: Match Your Board to Your Language
Not all boards support both equally well.
| Board | Arduino | MicroPython | Best For |
|---|---|---|---|
| Arduino Uno (ATmega328P) | Native | No official support | Classic stepper/servo projects |
| ESP32 | Good | Excellent | WiFi + brushed DC, IoT robots |
| Raspberry Pi Pico (RP2040) | Good | Excellent (official) | Dual-core PIO for steppers |
| STM32 | Excellent | Good (via STM32duino) | Industrial servo/FOC |
| Arduino Mega | Native | No | Multi-axis CNC, many steppers |
The RP2040 is worth highlighting: Its PIO (Programmable I/O) state machines handle step pulses in hardware, so MicroPython code triggers them without timing jitter. This is the best of both worlds for stepper control.
# MicroPython on RP2040: Use PIO for jitter-free step pulses
# PIO handles the timing in hardware — Python just sets the frequency
import rp2
from machine import Pin
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def step_gen():
wrap_target()
set(pins, 1) # STEP high
nop()
set(pins, 0) # STEP low
wrap()
sm = rp2.StateMachine(0, step_gen, freq=20000, set_base=Pin(2))
sm.active(1)
Expected: Clean step pulses at the set frequency, no Python GC interference.
Verification
Test your motor control setup with a basic smoke test before adding complexity.
# MicroPython smoke test: Ramp a brushed DC motor up and down
from machine import Pin, PWM
import time
pwm = PWM(Pin(15), freq=20000)
for speed in range(0, 101, 10):
pwm.duty(int(speed * 1023 / 100))
print(f"Speed: {speed}%")
time.sleep_ms(200)
for speed in range(100, -1, -10):
pwm.duty(int(speed * 1023 / 100))
time.sleep_ms(200)
pwm.duty(0)
print("Done")
// Arduino smoke test: Ramp a stepper with AccelStepper
#include <AccelStepper.h>
AccelStepper stepper(AccelStepper::DRIVER, 2, 3); // STEP=2, DIR=3
void setup() {
stepper.setMaxSpeed(2000);
stepper.setAcceleration(500);
stepper.moveTo(400); // 2 full rotations on 200-step motor
}
void loop() {
if (stepper.distanceToGo() == 0) stepper.moveTo(-stepper.currentPosition());
stepper.run(); // Call as fast as possible — don't block loop()
}
You should see: Smooth motion with no missed steps or stalling. If the motor twitches or stalls, check current limit on the driver before touching code.
What You Learned
- Arduino wins on hard real-time tasks: high-frequency step pulses, encoder ISRs, FOC
- MicroPython wins on iteration speed, readability, and WiFi-connected projects
- The RP2040's PIO closes the gap for stepper control in MicroPython
- Brushed DC + PID is comfortably in MicroPython territory on any modern board
Limitation: This comparison assumes you're writing application-level code. If you're writing motor driver firmware (register-level, safety-critical), neither — use bare-metal C or RTOS.
When NOT to use MicroPython: >10kHz step rates, FOC for BLDC, or any safety system where GC pauses are unacceptable.
Tested on Arduino Uno R3, ESP32-WROOM-32, Raspberry Pi Pico (RP2040), MicroPython 1.23, Arduino IDE 2.3