This is meant to be a quick introduction on a way to handle real-time constraints in an Arduino system, iteratively building up control software for a CNC mill.
Our local hackerspace has been offered a CNC mill, and we've had a few days to play with it.
As we received it, the machine didn't have endstops, so we added some, using microswitches and 3D printed cases to hold them in place.
The stepper driver board is one I've not seen so far:
It is connected to the computer's parallel port
The pinout is fairly simple: the data strobe is used as the stepper enable, and the eight data pins are the step and direction pins for the four supported axes. The four status pins are used to report the endstops back to the computer -- so this would work with really any parallel port.
With the device, we got software to play with, which was running under Windows XP, used direct hardware access to write to the parallel port (with a special driver to work around Windows) and had problems keeping up with the timing of the steppers, unless you'd switch the program to "realtime" mode, at which point the mouse was frozen while the motors were moving.
So we'd rather like to replace the software with something that works, and ideally doesn't require a full PC and an obsolete OS to run.
I still have an Arduino board, so I thought I might put it to good use, so I thought about designing a shield to interface to the parallel port. The ribbon cable I used with the SUB-D connector ends in a standard rectangular connector, and ideally I'd be able to directly connect that.
Because I'd like to keep my timings as exactly as possible, I've made sure that all the "STEP" inputs would be connected to the same port on the Arduino, allowing me to write them simultaneously. The second design goal was to create a single-sided PCB, so I could ideally use the CNC router to cut out the PCB.
Using KiCad, I've created a simple schematic connecting everything in a way that makes sense, and has no crossing wires.
I've left out pin D4, which is the Chip Select line to the SD card, as well, so I can use the SD card later on.
As a PCB, it looks like this:
Obviously, this is a bit of a chicken-and-egg situation here: to route that board, we need the router to work, so I made a few temporary connections using breadboard wires.
With these connections made, it's ready to be powered up. While the debugger is connected, pin D0 is going to be driven by the serial port, so I swapped it with D4 because we're not using the SD card yet.
I use avr-gcc directly, but keep to the Arduino standard of defining
loop() functions. First of all, I define constants for the
bit positions of the I/O pins. Good style would be to include the port
mapping in the constants as well, but in the long run, there will be only
one place in the code that needs to know that, and doing so would add quite
Then, I set up the initial outputs, and the direction registers. No movement should occur yet.
Source code: 01-setup.c
Next up, we'd like to see something moving for instant gratification. One of the safest things we can do is go to the home position, which is reached by moving towards the endstops until they are reached.
To do this, I've inserted code into the loop to check the endstop status,
and toggle the step outputs for the axes that are not there yet. To get a
delay between toggles, I use the
_delay_us function from avr-gcc; you
could use the
delayMicroseconds function in the Arduino library instead.
Source code: 02-home.c
Introducing Real-Time Constraints
Until here, the hardware was operated from the main program code, which means that as soon as the program is busy somewhere, it would stop operating the motors, and the timing is not entirely predictable either, so the motor speed may be variable.
To combat this, I've moved the motor control to a periodic timer interrupt. The handler writes the new hardware settings immediately, and then calculates the next settings -- this gives a defined delay between the occurence of the interrupt, and the reaction of the system.
As a result, the timings are now very exact, and the motor speed remains within tight boundaries -- this will be required for precise movement.
Source code: 03-interrupt.c
Next up, we need a way to execute commands sequentially, and actually move somewhere else than home.
So I've added a 16 position ring buffer of commands, and the interrupt handler executes one and then proceeds to the next. When the queue is empty, nothing happens obviously.
The first implementation has three commands,
HOME to go to the endstops,
MOVE to go to a position, and
SHUTDOWN to shut down the steppers.
We preinitialize the command queue to at least see something, and turn up the speed.
Source code: 04-queue.c