Taking a break from the quadcopter development, turned my hand to building a CNC machine. Initially with the idea of cutting soft stuff such as balsa, or foam that can then be used to make a mold. As I've never done this before and the options looks really expensive, the first thought was to build a machine that did something and with that it would give a better idea where the limitations lay and what was practical to cut/shape with various types of machine.


To the casual observer, a CNC machine, like a 3D printer, can do virtually anything, if only there was one sitting on the desk. Experiences with the quad and observations of many a youtube video demonstrate that a large amount of what is seen is carefully setup, rehearsed and edited. You only see the extreme successes or the total failures, you do not see the setup time and you don't see all the ideas that didn't quite work. So before spending a reasonable pile of cash on parts that turn out to be extremely limited in practice, I thought this needs a prototype that avoids the expensive heavy metal and instead uses what is cheap:- pivots (in the form of bearings) and drawer runners.

What I couldn't easily avoid is the electronic drive train. Sure it easily possible to etch a board containing opto-couplers, stepper-drivers and H-bridges, but for 60 quid from ebay for a parallel port to 5-axis stepper driver, as in, probably all I will ever want, I didn't see the point in building one. I certainly wouldn't learn anything from the exercise. The same could be said of the steppers, belts and pulleys - though quite expensive, there is also no doubt that dealing with heterogeneous steppers obtained from various sources would be a huge time sink.


The main problem with CNC machines is the number of sliding parts, which are heavy and expensive. Hexapod style machines are cute but tricky to build from scratch. To counter this the original plan was to use pivots rather than rails for everything, after-all, bearings are cheap. Turns out this type of machine is called SCARA - though I never planed on mounting pivoting arms on pivoting arms as I was still keen to avoid the heaviness and inertia that could come from mounting one axis on the other. To this end, and not far from the hexapod concept, the machine is based on three joints (LinuxCNC terminology, like an axis but not necessarily a pure Cartesian axis).  Each joint loosely does X,Y and Z, but with varying amounts of overlap. The Z arm hangs from a pivot, high above the work piece, so that the end near the work piece can swing in X and Y direction but not rotate. Z can vary in height. The cutting tool is attached to the Z.

I cheated at this point and only used bearings for the Z axis, the other two axis used drawer runners. At the time it appeared this would be easier to build, as those wood discs take a long time to cut and align. It also seemed like a dead-end as far as scale was concerned, 32cm diameter was as big as I could cut. Looking back, the solution was to not cut a disc of wood, but to make an arc of screws in wood that hold a toothed belt in an arc. The stepper can then be mounted on an arm, just outside the diameter of the arc, and use bearings to keep the belt mostly around the stepper pulley and close the the arc. In effect, like the Z wheel, but the wheel is fixed and the stepper rotates around the wheel. With a brace at 90degrees to the plane of rotation, the result would be solid and yet only use four bearings.

Z joint is a rotating wheel, 2.4 meters up, 32cm in diameter. Hanging from an offset pivot (7cm) is an arm that nearly reaches the ground. Attached to the arm is the cutting device, a multi-tool with routing bit. The arm is free to swing left/right/back/forward but has torsional stiffness, ie, it doesn't want to rotate around Z. Here it is mounted to my metal shelf - which I've covered with a dust sheet to make it less "distracting".

X joint is a drawer runner with 200mm travel, on the ground, to the left of the hanging Z arm. The X joint is attached to the Z arm by a ball-jointed arm, 1m long.

Y joint is a drawer runner with 200mm travel, on the ground, in front of the hanging Z arm. The Y joint is attached to the Z arm by a ball-jointed arm, 1m long.

The three arms come together, with ball joints attaching the X to Z and Y to Z, and the cutting tool on the Z.

As shown here, after the first attempt (2nd use) at cutting balsa.

To get the LinuxCNC config close to correct, first run StepConf to get an approximate config, and then merge changes in an expendable config into your real config using diff. This whole process is surprisingly unfriendly and disappointing, presumably because the people who write the software only have to do it once and already know exactly what to do and it what order.


LinuxCNC has the option to support this sort of physics, albeit not automatically. If it isn't one of the pre-existing types, you must write the function to convert joint values into Cartesian values and the inverse function. The result is run as a C kernel module, where floating point printf() is not available. I strongly recommend prototyping and debugging kinematics code in gawk and then embedding the resulting C(ish) code into the kernel module (ie, remove gawk specific lines and add kernel module header lines, with careful use of sed and grep). Okay, so I didn't do the latter - it was written and debugged in gawk and then manually added the header lines and variable declarations to make it a C kernel module - so the final debugging, which is tweaking parameters and making sure the positives and negatives move in the direction intended, must be done by editing the C kernel module, which has left the gawk source code out-of-date.

The kinematics file is millkins.c. Looking through the top you will see offsets are added and signs changed, but as this is done consistently, the core equations remain valid, which is the only reason it was practical to debug the kernel module. One thing they don't tell you on the net, is that you can *1000 the values and printk() the integer result, that together with only printk'ing values that have changed (using last* variables and abs()) and then the line:

while true ; do dmesg | tail -n 1 ; sleep 0.25 ; done

to give some idea about what the values currently are while running LinuxCNC. The various parameters come from this diagram. As this is a 3D diagram, to keep it readable only the major parameters are mentioned, ie, for the X joint, the x parameters are shown but not the corresponding y or z. The *len parameters are the arm lengths, the *dst and zang are the joint positions, the *dst* parameters are the offsets from a nominal (0,0,0) to the zero position of the joint. If anybody asks, I'll be forced to make diagrams from different angles, which creates the space to list them all.


It turns out this setup is extremely sensitive to the geometry parameters in millkins.c, which makes getting it right a time consuming process. Absolute perfection is unlikely, as the kinematics assumes the X-Z, Y-Z and cutting tool are all at exactly the same point in space. In reality, the ball joints and cutting tool are near-by. As an approximation, the X distance from the X pivot on the Z arm to the cutting tool must be added to the X distance from the nominal 0,0,0 location to the JointX=0 position, same for Y, ie, make the X and Y joints effectively further away. This sketch shows which parameters are which. When configured correctly (by re-compling millkins.c and restarting LinuxCNC), you should be able to move from machine coordinates (the joints) to world coordinates (Cartesian) and the steppers do the Right Thing. Moving X and Y to the lower and upper limits should give you a constant Z. Moving X and Y to the upper and lower limits should also result in a right-angled movement. Careful measurement is required, also to be measuring the right thing. In the mathematical world it is easy enough, in the real world, accurate measurement of most of these values is quite difficult.


Post calibration, attempt 1:

After calibration, the first test was to clamp a felt-tip pen to the arm and draw the LinuxCNC logo. That showed that although really close, the Z calibration is not perfect. Some letters were drawn correctly, others the pen dragged and stuttered, and then others, it missed the paper even though it looked to be touching.

Attempt 2:

Next, a mount was made for the multi-tool, out of hot-glue and cable ties. The kinematics calibration had been changed slightly to hopefully approximate for the X-Z  Y-Z cutting tool inaccuracy. A 1.5mm routing tool was in the multi-tool and it was used to mill out a picture of my eldest daughter into balsa wood, using tool paths generated by image-to-gcode. The result was reasonable for a first attempt.  At 5.5x4.5cm, 8mm deep, it demonstrated three things: i) the importance of roughing to remove most of the wood, or it will take hours/days ii) it is very easy to make a mistake and ram the cutting bit into the base, bending the bit and giving an effective diameter of 2mm. iii) At 5.5x4.5cm, 2mm diameter cuts are huge.

It also showed the problems with the machine in general, a slow Z made following the contours a slow process. It also shows the limitations of the image-to-gcode software, going over the same spot time and time again, where something more intelligent would raise the cutting and do a rapid move to the next area that needs cutting, or turn around and do the next row. Having done 4 depth passes, the fifth seemed unnecessary and the milling was stopped, with this result:


Attempt 3:

Hasn't happened yet. Bought a chuck for the multi-tool, as those routing bits are expensive and hard to find. The plan is to use cheap 3mm and 1mm drill bits, cut the end off square, and use those. The drill bits have been cut, but the 1mm bit was surprising hard to cut square. Ended up flatting it so it was more like a paddle. Hopefully that is enough for balsa wood. The plan is also to use dmap2gcode to generate a roughing gcode with the 3mm bit. The software plan morphed into writing some software to better calculate the required paths and avoid cutting areas that have already been cut - though a perfect solution to this turns out to be NP-Hard.

My current software solution is estimated to take 5 hours 58 minutes to do the rough and fine cuts for (512x512 pixels, 150mm x 150mm x 25mm, 3mm diameter roughing to 6mm per depth, 1mm diameter fine routing to 1.6mm depth):

Whereas dmap2gcode is estimated to take 5 hours 59 minutes - which doesn't seem to be worthwhile until the deep fine cuts limitation of dmap2gcode is considered. That said, dmap2gcode supports text and a few other options that my code does not. Also... my C code has no front end or even command-line options, which is the least required to make it accessible to the non-programmer.


1. Turns out the Z doesn't have enough torsional stiffness. It is fine for routing balsa, but anything that resists will twist the cutter to the path of least resistance.

2. Also turns out the Z isn't fast enough (at less than a tenth of the speed of the X and Y). For some reason it drops steps if run any faster. This is more of a problem for this type of machine because Z must be continually adjusted by the kinematics to maintain a constant Cartesian Z.

3. The "following error" limits are wildly too large, anything less and I get a following error from LinuxCNC. I don't know why.

Software tools (open-source)

LinuxCNC - to drive the steppers with your gcode, on a dedicated machine with real parallel port.

HeeksCNC - to generate gcode from 3D object files.

dmap2gcode - to generate gcode from images.

OpenSCAM - virtual CNC, test your gcode and estimate run-time.

Software tools (quick findings)

LinuxCNC is where I started this journey. It has a steep learning curve and works well when setup, but getting to that stage takes a lot of time and false starts.

image-to-gcode comes with LinuxCNC is what I first used to generate some custom gcode. It works but doesn't do roughing, yet roughing is the only practical way to do fine milling.

dmap2gcode is based on image-to-gcode, it does roughing as a separate gcode file. Doesn't do any optimisations such as skipping already done passes. It also assumes the fine cutting can be done in a single pass, so small holes that the roughing couldn't fit into will be attempted regardless of how much depth must be milled in a single pass.

OpenSCAM works well but has its limitations. For some reason anything with that cuts rows in image/dmap2gcode produces warnings and a bad rendering. I guess the XZ arc G code parser is broken.

HeeksCNC hasn't been used in anger. I've played with it a little but didn't have anything that I could apply it to. The cutting modes didn't seem to be flexible enough for my purpose - which is efficiently cutting depth maps.

Written by Greg on 2014/01/09.