How to Use Raspberry Pi to Control Servo Motors in CNC Machines

Micro Servo Motor with Raspberry Pi / Visits:2

When you think about CNC machines, your mind probably jumps to stepper motors, spindle speeds, and G-code. But there's a quiet revolution happening on the bench top—micro servo motors are sneaking into CNC builds for everything from tool changers to Z-axis probes, automatic edge finders, and even laser focus adjustment. The Raspberry Pi, with its GPIO pins and real-time-ish capabilities, is the perfect brain to drive these tiny but mighty actuators. In this guide, I’ll show you exactly how to wire, program, and tune a micro servo motor (like the SG90 or MG90S) into a CNC workflow using a Raspberry Pi, while tackling the quirks that make servos both frustrating and fantastic.

Why Micro Servos? The Unsung Heroes of CNC Accessories

Let’s be honest: a micro servo can’t push a router bit through aluminum. But that’s not its job. Micro servos excel at low-torque, high-speed, positional feedback tasks that keep a CNC machine safe and automated. Think of:

  • Automatic tool length offset (TLO) probes that tap the bit and retract in 0.2 seconds.
  • Dust shoe lift mechanisms that raise the brush head when you’re not cutting.
  • Spindle lock pins that engage only when you swap collets.
  • Laser engraver focus sleds that move a lens by 0.5mm increments.

The beauty of a micro servo is its closed-loop feedback—a potentiometer inside reports the actual shaft position to the controller. Unlike a stepper, which can lose steps if overloaded, a micro servo will fight to hold its commanded angle. That makes it ideal for binary actions (open/close, engage/disengage) and fine analog adjustments (focus, pressure).

But here’s the catch: servos expect a 50Hz PWM signal with a 1–2ms pulse width. The Raspberry Pi’s hardware PWM is limited to one pin (GPIO18), and its Linux kernel can introduce jitter. So how do you get clean, repeatable motion? Let’s break it down.

Hardware Setup: Wiring a Micro Servo to a Raspberry Pi (Safely)

The Power Problem (Don’t Skip This)

Micro servos draw 100–250mA at 5V when idle, but stall current can spike to 1A or more. The Raspberry Pi’s 5V rail is not designed for that. If you power a servo from the Pi’s 5V pin, you’ll get:

  • Random reboots (brownouts)
  • Corrupted SD card writes
  • Servo twitching or stuttering

Correct approach: Use a separate 5V 2A+ power supply for the servo. Connect the servo’s red wire to that supply, black to ground, and only the signal wire (yellow/orange) to a GPIO pin. Also, tie the ground of the Pi to the ground of the servo supply—otherwise, the signal reference floats and you’ll get erratic behavior.

Wiring Diagram (Simplified)

Raspberry Pi GPIO18 (PWM) → Servo Signal (yellow) Raspberry Pi GND → Servo Power GND (black) External 5V Supply → Servo Power VCC (red) External 5V GND → Raspberry Pi GND (common ground)

Pro tip: Add a 100µF electrolytic capacitor across the servo power pins. This absorbs the current spikes when the servo starts moving. If you’re using a high-torque metal-gear servo (MG90S), add a 470µF cap.

Choosing the Right GPIO Pin

The Raspberry Pi has two PWM-capable pins: GPIO12 and GPIO18. But only GPIO18 is wired to the audio jack’s PWM0 (on older models) and is the default for pigpio and rpio. For a CNC, you want hardware PWM to avoid kernel scheduling jitter. Use GPIO18.

If you need to control multiple micro servos (say, 4 for a tool changer), you have three options:

  1. Software PWM on any GPIO (using pigpio library) – easy but jittery at 50Hz.
  2. Adafruit 16-channel PWM breakout (PCA9685) – over I2C, gives you 12-bit resolution. Best for multi-servo CNC rigs.
  3. Servo HAT – pre-built with level shifters and power terminals.

For this article, I’ll focus on the single-servo case with GPIO18, but the code patterns extend to multi-servo via PCA9685.

Software: From Python to Real-Time Control

The 50Hz PWM Trap

A micro servo expects a 20ms period (50Hz). The pulse width determines the angle:

  • 1.0ms → 0° (or -90° depending on brand)
  • 1.5ms → 90° (center)
  • 2.0ms → 180°

Most SG90 servos have a usable range of 0.5ms to 2.5ms for full 180°, but pushing beyond 1.0–2.0ms can hit the mechanical stops. Never command a pulse outside 0.8ms–2.2ms unless you’ve verified the servo’s specs.

The pigpio Library: Your Best Friend

pigpio runs a daemon that uses the Pi’s DMA (Direct Memory Access) to generate very stable PWM—even on software pins. It’s the gold standard for servo control on Raspberry Pi.

bash sudo apt install pigpio sudo systemctl enable pigpiod sudo systemctl start pigpiod

Then, in Python:

python import pigpio import time

pi = pigpio.pi() # connect to local daemon

Set up GPIO18 for servo

SERVOPIN = 18 pi.setservopulsewidth(SERVOPIN, 0) # turn off

def setangle(angle): # Map 0-180 deg to 500-2500 µs (adjust as needed) pulse = 500 + (angle / 180.0) * 2000 pi.setservopulsewidth(SERVOPIN, pulse)

Sweep test

for angle in range(0, 181, 10): set_angle(angle) time.sleep(0.2)

pi.setservopulsewidth(SERVO_PIN, 0) # disable to avoid jitter pi.stop()

Notice the set_servo_pulsewidth function—it’s specifically designed for servos and automatically uses a 50Hz frame rate. The pigpio daemon also removes the jitter you’d get with RPi.GPIO software PWM.

Calibration: Every Micro Servo Is a Snowflake

No two SG90s are identical. The 1.5ms center might be 92° on one unit and 88° on another. For CNC use, you need repeatable positions, not absolute accuracy. So, write a calibration routine:

python def calibrate_servo(): input("Move to MINIMUM angle. Enter pulse width (µs) when ready: ") min_pulse = int(input()) input("Move to MAXIMUM angle. Enter pulse width: ") max_pulse = int(input()) # Store these values in a config file

Then, when you set an angle, linearly interpolate between your calibrated min and max. This ensures your tool changer’s “engage” position is always the same, even if the servo itself drifts with temperature.

CNC Integration: Real-World Use Cases

Case 1: Automatic Tool Probe (Touch Off Plate)

You have a CNC router with a fixed touch plate. A micro servo swings a conductive arm down to touch the bit, completing a circuit. The Raspberry Pi reads the GPIO input to detect contact, then retracts the servo.

Wiring: Servo signal on GPIO18. Touch plate wire to GPIO17 with a pull-up resistor (10kΩ to 3.3V). When the bit touches the plate, GPIO17 goes LOW.

Code logic:

python import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BCM) GPIO.setup(17, GPIO.IN, pullupdown=GPIO.PUD_UP)

Move servo down

set_angle(120) # calibrated "touch" position

Wait for contact

while GPIO.input(17) == GPIO.HIGH: time.sleep(0.01)

Record Z position (from your CNC controller)

zpos = readzfromgrbl()

Retract servo

set_angle(30)

Why micro servo? A stepper would be overkill for a 10mm swing, and a solenoid is too violent. The servo’s soft stall allows the probe arm to press against the bit without bending it.

Case 2: Laser Focus Sled (Analog Control)

For a diode laser engraver, you need to move a lens up/down by fractions of a millimeter. A micro servo with a 180° range and a small lever arm can give you ~10mm of linear travel with ~0.05mm repeatability if you use a long arm and fine pulse control.

The trick: Use pigpio to set pulse widths in 1µs increments. At 50Hz, that’s a 0.1° resolution—enough for focus adjustments.

python def set_focus_position(percent): # 0% = closest focus, 100% = farthest pulse = 1000 + (percent / 100.0) * 500 # 1000-1500µs pi.set_servo_pulsewidth(SERVO_PIN, pulse)

But watch out for servo noise: Mechanical dithering (hunting) can happen if you command a position that’s between two potentiometer wiper steps. Add a small deadband: if the requested pulse is within 5µs of the last, skip it.

Case 3: Automatic Tool Changer (Binary State)

A 3D-printed tool changer for a CNC mill uses two micro servos: one to lock the tool holder, another to rotate the carousel. The challenge is speed vs. torque. A standard SG90 takes ~0.1s/60°, so a 90° swing is 0.15s—fast enough for a tool change under 2 seconds.

The safety issue: If the servo stalls (tool jammed), it will draw max current and overheat. Use a current-sensing resistor or just a timeout:

python start_time = time.time() set_angle(90) # lock position while time.time() - start_time < 1.0: if servo_reached_target(): break time.sleep(0.01) else: # Timeout – likely jammed. Retract and alarm. set_angle(0) raise RuntimeError("Tool changer jammed")

To check if the servo reached its target, you can read the potentiometer feedback via an ADC if you’re using a servo with a feedback wire (like the Feetech FS90R). But for standard SG90, you’ll rely on timing.

Advanced: Closed-Loop Micro Servo with Encoder Feedback

Some micro servos (e.g., Feetech FT90M, DS3218) have a built-in magnetic encoder that outputs a PWM feedback signal. You can read this with a second GPIO using pigpio’s callback function to measure the pulse width. This turns your Pi into a true closed-loop controller, compensating for load-induced position error.

Wiring: Feedback wire to GPIO23. Use pigpio’s set_glitch_filter to debounce.

python pi.setmode(23, pigpio.INPUT) cb = pi.callback(23, pigpio.RISINGEDGE)

def readfeedbackpulse(): # Measure time between rising edges # Returns pulse width in µs

Then, you can implement a simple PID loop to correct the servo’s position:

python error = target_pulse - current_pulse correction = Kp * error pi.set_servo_pulsewidth(SERVO_PIN, commanded_pulse + correction)

This is overkill for most CNC hobbyists, but if you’re building a precision ATC (automatic tool changer) for a production machine, this is the way to go.

Performance Tuning: Reducing Jitter and Overshoot

Jitter Sources

  1. Linux scheduler – Even with pigpio, background processes can cause micro-delays. Fix: set pi.set_PWM_frequency(SERVO_PIN, 50) and use pi.set_PWM_range(SERVO_PIN, 20000) to get 1µs resolution.
  2. Power supply ripple – If the servo’s voltage dips when moving, the potentiometer reading shifts. Fix: add a 470µF low-ESR capacitor and a ferrite bead on the power line.
  3. Mechanical resonance – The servo arm’s natural frequency can cause oscillation. Fix: reduce acceleration in your code. Don’t jump from 0° to 180° in one step; ramp it:

python def smooth_move(from_angle, to_angle, step=2, delay=0.01): if to_angle > from_angle: for a in range(from_angle, to_angle, step): set_angle(a) time.sleep(delay) else: for a in range(from_angle, to_angle, -step): set_angle(a) time.sleep(delay)

Overshoot and Settling Time

Micro servos are notoriously under-damped. They’ll overshoot the target by 5–10° and then oscillate for 200ms. For CNC use, that means your probe might touch the bit, retract, and then wobble before you read the position. Solution: Add a settling delay after the move:

python set_angle(120) time.sleep(0.3) # let it settle

Now read sensor

Or, if you have encoder feedback, implement a derivative term in your PID to damp the oscillation.

Putting It All Together: A Complete CNC Servo Control Script

Here’s a production-ready Python script that combines everything—calibration, smooth moves, and a CNC-style state machine:

python

!/usr/bin/env python3

import pigpio import time import json

class MicroServoCNC: def init(self, pin=18, configfile='servoconfig.json'): self.pi = pigpio.pi() self.pin = pin self.loadconfig(configfile) self.current_pulse = 0

def load_config(self, fname):     with open(fname) as f:         cfg = json.load(f)     self.min_pulse = cfg['min_pulse']  # e.g., 500     self.max_pulse = cfg['max_pulse']  # e.g., 2500     self.min_angle = cfg.get('min_angle', 0)     self.max_angle = cfg.get('max_angle', 180)  def angle_to_pulse(self, angle):     # Linear interpolation with calibrated endpoints     ratio = (angle - self.min_angle) / (self.max_angle - self.min_angle)     return int(self.min_pulse + ratio * (self.max_pulse - self.min_pulse))  def set_angle(self, angle, speed=1.0):     target = self.angle_to_pulse(angle)     if speed >= 1.0:         self.pi.set_servo_pulsewidth(self.pin, target)     else:         # Smooth move         step = max(1, int(abs(target - self.current_pulse) * (1 - speed)))         direction = 1 if target > self.current_pulse else -1         for pulse in range(self.current_pulse, target, direction * step):             self.pi.set_servo_pulsewidth(self.pin, pulse)             time.sleep(0.002)     self.current_pulse = target     time.sleep(0.2)  # settle time  def disable(self):     self.pi.set_servo_pulsewidth(self.pin, 0)     self.pi.stop() 

Example usage in a CNC cycle

if name == "main": servo = MicroServoCNC() try: # Tool probe sequence servo.setangle(120) # swing probe down # ... read touch sensor ... servo.setangle(30) # retract # Tool lock sequence servo.setangle(90) # lock # ... do machining ... servo.setangle(0) # unlock finally: servo.disable()

Save this as servo_cnc.py and run with sudo python3 servo_cnc.py (or ensure your user is in the gpio group).

Common Pitfalls and How to Avoid Them

Pitfall 1: Servo Twitching at Startup

When the Pi boots, GPIO pins float. If your servo is connected, it may jerk to a random position. Fix: Add a pull-down resistor (10kΩ) between the signal pin and ground. Or, better, physically disconnect the servo signal until your script explicitly sets it.

Pitfall 2: Using RPi.GPIO Software PWM

RPi.GPIO generates PWM via a kernel thread. At 50Hz, you’ll see ±0.5ms jitter—enough to make the servo buzz and overheat. Always use pigpio for servo work.

Pitfall 3: Forgetting to Disable the Servo

A servo left enabled draws current and heats up. After your CNC operation, send a 0 pulse to disable it. This also prevents the servo from holding a position and draining your power supply.

Pitfall 4: Signal Voltage Mismatch

Raspberry Pi GPIO is 3.3V. Most micro servos accept 3.3V logic on the signal line, but some cheaper ones expect 5V. If your servo doesn’t respond, use a level shifter (e.g., a simple transistor or a 74HCT245). Never feed 5V into a Pi GPIO pin.

Scaling Up: Multi-Servo CNC with PCA9685

If you’re building a 4-tool changer or a dual-axis laser focus, you’ll outgrow GPIO18. The PCA9685 breakout board gives you 16 channels of 12-bit PWM, controlled via I2C. The Pi sends a simple command, and the PCA9685 generates the clean 50Hz signal—freeing up your CPU.

Wiring: SDA (GPIO2) → SDA on PCA9685, SCL (GPIO3) → SCL, plus 5V and GND. Set the I2C address (default 0x40).

Python with Adafruit_PCA9685:

python from board import SCL, SDA import busio from adafruit_pca9685 import PCA9685

i2c = busio.I2C(SCL, SDA) pca = PCA9685(i2c) pca.frequency = 50

Servo on channel 0

def setservo(channel, pulseus): # Convert pulse (µs) to 12-bit value (0-4095) # 20ms period = 4096 ticks, so 1µs = 4096/20000 ticks ticks = int(pulseus * 4096 / 20000) pca.channels[channel].dutycycle = ticks

set_servo(0, 1500) # center

The PCA9685’s clock is crystal-controlled, so you get zero jitter—better than the Pi’s internal PWM. This is the professional choice for CNC retrofits.

Final Thoughts on Micro Servos in CNC

Micro servos are not precision linear actuators, and they’re not high-torque motors. But they fill a unique niche in CNC machines: fast, cheap, repeatable motion for auxiliary functions. With a Raspberry Pi and pigpio, you can integrate them cleanly into your G-code workflow—no Arduino needed.

The key takeaways:

  • Always use a separate power supply for the servo and common ground.
  • Use pigpio for hardware-timed PWM, not RPi.GPIO.
  • Calibrate your servo’s endpoints for repeatable CNC actions.
  • Add settling delays to avoid false sensor readings.
  • For multi-servo systems, move to a PCA9685.

Now go wire up that SG90 to your Pi, write a quick sweep script, and then integrate it into your next CNC project. Your machine will thank you with smoother tool changes and safer probing routines. And if you ever wonder why your servo is buzzing—check your ground loop first, then your PWM frequency, and finally your coffee intake.

Copyright Statement:

Author: Micro Servo Motor

Link: https://microservomotor.com/micro-servo-motor-with-raspberry-pi/cnc-servo-raspberry-pi.htm

Source: Micro Servo Motor

The copyright of this article belongs to the author. Reproduction is not allowed without permission.

About Us

Lucas Bennett avatar
Lucas Bennett
Welcome to my blog!

Tags