Writing a driver for a home build, Raspberry Pi controlled CNC machine
You can find all the code referenced here and a lot more over on github.com/LukasOtis/pathfind3r
The idea of the pathfind3r project was to build a CNC Machine that would be a lot cheaper than bought ones. Writing the controlling driver ourselves enabled us to understand a lot more about the difficulties in transforming data and controlling hardware reliably. One of the biggest issues that we still face is the one of parallel, synchronised movement of several motors. So this is a work in progress – for sure.
We took a Raspberry Pi 3 and started lighting LEDs. Soon the code got more complex. With a concious effort on understandability and maintainability we hacked together something that works for us. This series of blog posts will kind of go through the steps of what pathfind3r does and how you can possibly use parts of is in your own project. May it be building a 3d Printer, a CNC Machine or anything else.
Our goal was to code as much as we can ourselves and to not take in too many external dependencies. After building Models in Software like 3D Max we export them as a CSV, parse that into series of steps and control which motor does which step when.
A realisation we quickly made: Doing this right is hard. Parsing the exported G-Code by itself can be tricky. There is a lot of different formats and G-Code actually offers a myriad of options to pass along. In this project we wrote our own minimal G-Code parser that only allows for a very limited amount of options and formats.
But let’s start at the beginning. After having that Raspberry Pi hooked up the first step is to talk to those motors we want to drive.
Part 1: Talking to Motors
Setting up Motors
Every Raspberry Pi tutorial comes to the point where you want to control a motor. For this CNC Machine Project this was the first and most vital step.
Since our machine needs to have three independent axis we also need to control three stepping motors driving the cutting head to an exact position. The Raspberry Pi ships with a neat library to access the GPIO Pins on the board.
Setting up the board and a single pin (in this case pin 12) is as easy importing the library and running two commands:
Import RPi.GPIO as GPIO GPIO.setmode(GPIO.BOARD) GPIO.setup(12, GPIO.OUT)
Moking the GPIO lib (fakegpio)
Here I already ran into the first problem though. I would find myself trying to code a new feature when somewhere on the road and not being able to run my code due to no RaspberryPi being present. And writing code on your computer, waiting until your home and then copying it over before running it on the actual raspberry is just not an option.
So turns out you can easily mock this GPIO Library because we used only a couple of methods including setup, input and output.
#!/usr/bin/python """FakeGpio.""" class FakeGPIO(object): """Stub the basic Rpi Gpio commands.""" BCM = True BOARD = False OUT = True IN = False HIGH = True LOW = False def setup(*kwds): """Stub Rpi GPIO function.""" return 0 def output(*args): """Stub Rpi GPIO function.""" return 0 def input(*args): """Stub Rpi GPIO function.""" return 0 def cleanup(*args): """Stub Rpi GPIO function.""" return 0 def setmode(*args): """Stub Rpi GPIO function.""" return 0
This FakeGPIO class can now accept those method calls we need including variable parameters. The
(*args) statement allows for an unknown number of passed in parameters which makes the moking a whole lot easier.
Now that we have the methods stubbed we can do a conditional import on it like so:
try: import RPi.GPIO as GPIO except ImportError: from fake_gpio import FakeGPIO as GPIO
Basically we are trying to import the Raspberry Pi Library and if that doesn’t work we will use the FakeGPIO Class instead. With the
as GPIO we allow all further calls to look exactly the same no matter with which Import they are being called on.
Talking to Motors (motor.py)
Now we can actually start working with the motors. Let’s define a class called Motors and give it the necessary fields it can be constructed with.
class Motor(): """Defines configuration and functionality to control all motors with""" def __init__(self, xdir, xstep, ydir, ystep, zdir, zstep, enable, sleep): """Stepping motors take a directional pin as well as a stepping pin""" self.xdir = xdir self.xstep = xstep self.ydir = ydir self.ystep = ystep self.zdir = zdir self.zstep = zstep self.enable = enable self.sleep = sleep
Each stepping motor needs only to pins to be controlled correctly. A directional pin as well as stepping pin. The sleep time is something we played around with to give the motor time to perform each movement before jumping to the next.
Note: This isn’t perfect. I’d like to have an instance of a Motor Class for each motor. Since we currently can’t control the motors in parallel and independent of each other this class holding the configuration and functionality for all the motors seemed like the most pragmatic way to go about it.
Now it is time to setup the Board. Let’s define a setup function:
def setup(self): # configure RPI-GPIO GPIO.setmode(GPIO.BOARD) # set motor_controll pin GPIO.setup(self.enable, GPIO.OUT) GPIO.output(self.enable, 0) # set X Axis Pins GPIO.setup(self.xdir, GPIO.OUT) GPIO.output(self.xdir, 0) GPIO.setup(self.xstep, GPIO.OUT) GPIO.output(self.xstep, 0) # set y Axis Pins GPIO.setup(self.ydir, GPIO.OUT) GPIO.output(self.ydir, 0) GPIO.setup(self.ystep, GPIO.OUT) GPIO.output(self.ystep, 0) # set z Axis Pins GPIO.setup(self.zdir, GPIO.OUT) GPIO.output(self.zdir, 0) GPIO.setup(self.zstep, GPIO.OUT) GPIO.output(self.zstep, 0)
This takes care of setting all the pins to output and putting them on 0 by default.
Let’s quickly define a cleanup function as well. After running our code on the board this makes sure no pins are left in a state we don’t want them in:
def cleanup(self): GPIO.cleanup()
Great, finally we are able to control a motor (let’s assume we want to move the x motor 2000 steps forward):
steps = 2000 for step in range(steps): GPIO.output(self.xstep, 1) time.sleep(self.sleep) GPIO.output(self.zstep, 0)
So to recap, we setup the Raspberry Pi to use the right pins, mocked the library needed for that and created a class that can control the setup, movement and cleanup of those pins.
Next up we are going to go over how to parse G-Code that was exported as a CSV and use that data to move our motors.