Welcome to Jon's Workshop.

CNC PROGRAMME DEVELOPMENT TO PRODUCE INTERESTING SHAPES

BRONZE SKIRT AT THE BASE OF THIS MINIATURE LOCOMOTIVE CHIMNEY

FIRST DRAFT – Issue 1. 17th June 2011

Author Jon Freeman B. Eng. Hons


This article traces the development of the CNC programmes used with a Sieg KX3 CNC milling machine to produce the chimney skirt shown, from an old bronze bush. This will involve us in learning something about 'G Code', the ancient and not very user-friendly language of CNC machines, or more precisely, the dialect of G Code recognised by 'Mach 3' machine control software.

One good thing about G Code as a language is that there isn't much of it to learn in the first place, and of what there is, very little needs be used for this task. Therefore we need not feel daunted by this computer programming language. Along the way we'll encounter trig functions and some other simple maths, but have no fear, there's no need to reach for the slide rule, four figure log tables or tranquillisers, we'll plan it so the computer gets to do all the tedious number crunching. I've heard they're quite good at this sort of thing.


We will encounter programme loops (loops within loops, even), subroutines and parameters, and prove there doesn't have to be anything scary about any of them, quite the opposite in fact. Judicious use of loops, subroutines and parameters can lead to creating some very compact programmes, helping to keep them easy to use, read and understand.

Although the final programmes will be in G Code, we'll do the thinking and programme development along the way in a top-level pseudo-code as close to 'Plain English' as possible. Early in the programme design process we'll find out how to create a programme file, and how to include comments within it. We'll then design our pseudo-code and write it all in the form of comments within the programme file. This will make our task of translating into G codes, almost a pleasure.


A CNC machine is not needed to design or test the programmes. Mach 3 software may be freely downloaded and run on a PC in 'simulation mode' with no limitations. Programmes can be written and developed using a plain text editor, and you can keep check along the way by running your programmes on the Mach 3 simulator. It's far better to see those unintended tool crashes in a simulation, where the consequences are not expensive!

But before we get involved in programming, we need to develop a clear idea of what it is we're setting out to produce, and how, realistically, we might instruct the machine to produce the item. We need a method.

FINDING A METHOD

Looking at the finished article, three separate machining operations can be identified: machining the curved underside to fit to the smokebox, machining the upper surface to some fairly complicated shape that needs to look about right, and boring out the centre to fit the chimney. Thinking about the order of machining operations suggests machining the bore first. We can then hold the work piece by the bore using a three jaw chuck secured to the milling machine table, on its back, jaws upwards. This is a workable setup for machining the outside top and undersides.

So the conventional lathe looks to be the machine of choice for completing the bore, facing the stock to length and, as most lathes will be capable of turning expensive stock material into swarf far quicker than the KX3, rough turning down the outside where the metal needs to be removed all the way around. That's the first machining operation dealt with. How much harder can it get?


Next decision, which side to tackle first? If the underside curve was machined first, it would make mounting the job the other way up for machining the upper surface more difficult because there'd no longer be a flat surface to register against the chuck jaws. If we consider machining the upper surface first, then we can make alignment easier by drilling a small radial hole in an area that will eventually be machined away on the underside. A drill bit or pin in the hole could then assist with orientation for both operations. Upper surface first it is then.

With a reasonably sound looking method now in place, we can think about how we want the CNC machine to move to cut the metal. We have two distinct operations to work through, the arc shaped underside, and the more complicated upper surface. Let's look at the easier of the two first, even though we'll be machining it last.


DEVELOPMENT OF A CNC PROGRAMME TO CARVE ARC SECTIONED CHANNELS

Machining an arc sectioned channel is the sort of job that will crop up again, for example machining the smokebox saddle is essentially the same job, but wider. Therefore it's a good idea to think about programming in some generic style, easily adaptable for reuse.

Thinking of how to cut the shape, there aren't too many methods to choose from, we can either devise a programme that moves the tool against the work in a series of pendulum-like arcs working slowly along the length, or we can move back and forth along the channel length adjusting the tool position on each pass. Of these, the first probably wouldn't be very fast or efficient, back and forth looks like a better choice.

Next we need to decide which horizontal method to use. When working with symmetrical shapes such as this there are often advantages in writing shorter, easier to follow programmes by making use of the symmetry. This suggests an algorithm that cuts along one side of the channel moving left to right and performing the corresponding cut to the other side of the channel on the return right to left traverse. This then presents two options: to start from the centre working up and outwards, or, starting from the outside working down and inwards to the centre. Of these, the former has the disadvantage that the first cut is the deepest, and would occupy the full width of the tool. With the latter, the tool starts with the lighter cuts, none of which will get to be as wide as the tool, so this seems the better strategy to adopt. Additionally, this will save time if several roughing cuts are required by starting cutting a narrow and shallow channel, increasing the width and hence depth on each pass means the tool wastes little time cutting air.

Thinking about the sequence of movements involved in cutting an arc sectioned channel in this way, we can come up with a simple descriptive name for the strategy, or algorithm. How about “Ever Decreasing Rectangles”?

That's a great name for the method, the algorithm, but maybe not so good as a file name for our programme file. A better file name might be “Smokebox Saddle”, or anything which will provide a clue as to what it does, rather than how it does it, when searching for it in a long list of programmes in the future.

The best surface finish, and the closest adherence to the desired shape will be obtained using a ball-nose cutter and a large number of rectangles, each close in width to the previous and the next. This is the ideal finishing cut run, but swarf can be made far quicker with an end mill or ripper cutter, although a little thought tells us we can not get this particular shape finished accurately with only a flat ended tool, we would always end up with a flat bit in the middle, up to twice as wide as the tool diameter. So it might be a good idea to make the number of rectangles an easily alterable parameter so that course roughing cut runs, cutting fewer rectangles, can be used to get the bulk roughed out quickly. This also suggests that our programme might be written to run the algorithm once only, leaving it to the user to re-run the programme as many times as they wish, with whatever tool changes and parameter tweaks between runs.

FIGURE 2 - End Mill or slot drill

FIGURE 3 - Ball nose cutter

It may be that a small flat in the centre is acceptable, under a smokebox it would probably not be visible on a finished loco, and it wouldn't matter in this case. For a job like this an end mill is the tool of choice to get the job done speedily. For more precise machining a ball nose tool is required, at least for the finishing cuts.

Some thought about the "controlled point" is useful here. Any CNC machine and the motion control software that drives it is ultimately concerned with ensuring all axes are at the desired position at any instant, tracing some moving controlled point in space. What material is being removed from where is then determined by the shape and size of the tool and its relationship to the controlled point. Referring to the illustrations above, with the ball nose cutter the controlled point is to be the centre of the spherical part of the tool such that the controlled point will always coincide with the smaller radius arc shown. When using a flat ended tool such as an end mill, the controlled point Z co-ordinate coincides with the end of the cutter, but the Y co-ordinate of the controlled point is one tool radius nearer the centre line of the channel.

The desire to keep our programme as generic as possible suggests we provide an easy means of indicating to it which type of cutter is being used. When considering this, it is helpful to realise the subtle differences to the algorithm between these two tool types. With a ball nose tool, the machine simply works to a radius that is the desired channel radius, less the radius of the ball nose tool. An end mill, on the other hand, may be considered to have a point radius of zero on its corners but will require to be offset by an amount equal to its own radius in order not to remove material that should remain. This suggests that two parameters describing the tool would be sufficient to inform our programme: the radius of a flat ended tool, and the ball nose radius. We can then specify a 'FlatEndToolRadius' of zero when using a ball nose cutter, or a 'BallNoseRadius' of zero when using a flat-ended tool such as an end mill. The programme will then subtract the 'FlatEndToolRadius' from each calculated 'Y' co-ordinate to correctly compensate for tool size in all positions except close to the centre, where the opposite corner of the tool will be cutting the inevitable flat bit. When 'BallNoseRadius' is above zero, this radius is subtracted from the 'ChanRadius', resulting in the wanted channel radius being formed. Clearly at any time one or other of these parameters should be entered as zero, and it would be an error if neither or both were zero.

We now have a good idea of the information we'll need to enter to feed our generic 'Ever Decreasing Rectangles' algorithm, the list below has it covered:-

; ChanWidth = ?
; ChanLength = ?
; ChanRadius = ?
; FlatEndToolRadius = ?
; BallNoseRadius = ?
; Number_of_Rectangles = ?

These should all be grouped together in one place near the top of the programme, so that the user knows where to look and can find and change any of them easily.

As a general rule, any value that might need to get tweaked should be defined once only in this way. You could instead type '3.175' (or whatever it may be) in at every point in the programme where you use a tool radius, for example, but then it becomes a nightmare trawling through the entire programme trying to find each and every reference to it when it needs changing – even more so when some other parameter also happens to have the same value. This thought leads us naturally toward planning the structure of our programme.

G code imposes some restriction on how we structure our code - the what we put where. For example, all loops and subroutines must be positioned in the file after the end of the main programme. This is no bad thing.

It is useful to have a generic G code programme template file usable as the starting point for any G code programme. One such is shown below.


; CNC G Code programme template file

; Put name and brief description of programme in this section.

; _____________________________________________
; Put user alterable parameter values in this section
; User is invited to alter the ??? parameters in this section.

; _____________________________________________
; User is advised not to alter anything below here

; This Section to put machine into known, safe state
M5	;Stop spindle
G17	;Select XY plane
G21	;Units are mm
G40	;Cancel cutter radius compensation
G49	;Cancel tool length offset
G61	;Exact stop
G50	;Reset all scale factors to 1.0
G90	;Absolute distance mode
G94	;Feed per minute mode - mm/min as mm selected above by G21
; _____________________________________________
; Section where user data processed

; _____________________________________________
; Programme proper starts here
F10    S125   ;Feed rate mm/min and spindle speed RPM
M3	; Start spindle clockwise at 'S' above RPM



M5 M30	; Stop spindle, Programme End and Rewind
; Programme has ended  here
; _____________________________________________

; The Section below is for subroutines


; End of file.


This is a valid G code programme in its own right which can be loaded into Mach 3 and run without error. Although it does not do anything obviously useful, arguably it does execute two of the most important sequences in any programme, first putting the machine into a known safe state, and second, stopping the machine and ending the programme safely. The lines that set the machine into a known safe state can be edited to suit different requirements, however caution should be exercised. The first line in the 'programme proper' section setting feed rate and spindle speed will need editing for practical programmes, but with safety ever in mind, these default speeds are slow.

To write and edit programme files we need a plain text editor, not a word processor. If using a PC with Microsoft on it, the 'Notepad' editor is suitable.

Creating the programme may then begin by opening the generic template file using Notepad, editing it to include the distillation of our thought processes thus far, and then saving the file with some sensible name such as “Smokebox Saddle.tap”. Note the '.tap' file name extension is historically the one to use for G Code files, although Mach 3 doesn't seem to mind the default '.txt', if that's what your computer seems to insist you will use. The first few lines of the file are shown in "SmokeboxSaddle1.png" below, the remainder of the file is unaltered so far.


; This CNC programme is for cutting smokebox saddles or
; other concave arc profiled channels.
; It employs the "Ever Decreasing Rectangles" algorithm.
; Wirtten and devised by  Jon Freeman  June 2011

; Rough using end mill or ripper, finish using ball nose,
; XY origin, X=0, Y=0 is centre of job
; Set Z=0.0 for tool just touching top of job
; Ensure 'ChanLength' sufficient for tool to clear both ends

; Tip - for multiple roughing cuts, start too narrow and increase
; ChanWidth between runs.  This minimises air cutting time.

; _____________________________________________
; Put user alterable parameter values in this section
; User is invited to alter the six parameters below

; ChanWidth = ?
; ChanLength = ?
; ChanRadius = ?
; FlatEndToolRadius = ? - set to 0.0 when using ball nose
; BallNoseRadius = ? - set to 0.0 when using end mill
; Number_of_Rectangles = ?

; _____________________________________________
; User is advised not to alter anything below here

; This Section to put machine into known, safe state
M5	;Stop spindle
G17	;Select XY plane
G21	;Units are mm
G40	;Cancel cutter radius compensation
G49	;Cancel tool length offset
G61	;Exact stop
G50	;Reset all scale factors to 1.0
G90	;Absolute distance mode
G94	;Feed per minute mode - mm/min as mm selected above by G21
; _____________________________________________
; Section where user data processed

; _____________________________________________
; Programme proper starts here
F10    S125   ;Feed rate mm/min and spindle speed RPM
M3	; Start spindle clockwise at 'S' above RPM



M5 M30	; Stop spindle, Programme End and Rewind
; Programme has ended  here
; _____________________________________________

; The Section below is for subroutines


; End of file.


Note the semi-colon that appears somewhere on most lines. In a G code file, everything appearing on a line to the right of a semi-colon, is a comment. As all of our top level pseudo-code will be comment, most lines we write for now should begin with a semi-colon.

A word on what goes toward good programming practice. Generally, although we are writing a programme in the hope that eventually “Something Good Will Happen”, we should put at least as much, or arguably more, effort into trying to ensure that “Nothing Bad Will Happen”.

Returning to consideration of how our 'Ever Decreasing Rectangles' algorithm might work, and to how we might pseudo-code it, we can see that a loop structure fits the requirement as it is to perform a similar sequence of commands, cutting a rectangle, some number of times. The G Code language allows loops, but the only looping mechanism provided involves repeated calls to a subroutine. A subroutine is simply a piece of programme that is written elsewhere in the programme listing, after the end of the main programme text. When a 'Call' to a subroutine is encountered, the effect is to branch out of the programme and continue executing instructions from the top of the particular subroutine text and working down through it until a 'Return' instruction is encountered. Programme execution then resumes from the line after the original subroutine call. However, the G code language allows us to specify a 'repeat number of times' number on a subroutine 'Call'ing line, and this is the one and only G Code method to repeat a sequence of commands, or to loop, some fixed number of times.

Now we've thought it through sufficiently to start writing a top-level pseudo-code list. It might go something like:

; From ChanWidth and ChanRadius, find 'HalfAngle', from side to centre.

; Calculate 'AngleStep' = 'HalfAngle' / 'Number_of_Rectangles'

; Call subroutine 'Rectangle', 'Number_of_Rectangles' times

; After cutting 'Number_of_Rectangles', programme resumes here

M5 M30 ;M5 code Stops spindle, M30 is End Programme and Rewind

That's the main programme written in a close to 'Plain English' style, but with the real machine codes, M5 and M30, to stop the spindle and end the programme included to encourage a safe method of working.

Turning to the 'Rectangle' subroutine loop, there are only two functions required of it. It has to calculate co-ordinates in preparation of cutting a rectangle, and then to cut a rectangle. The bones of the 'Rectangle' subroutine then boil down to look like:

; In the subroutine section

; 'Rectangle' Subroutine

; Reduce 'HalfAngle' by 'AngleStep'

; Using 'HalfAngle', calculate new width and depth

; Cut Rectangle

; Return

Our top level programme can now be re-written with a little more detail as shown in SmokeboxSaddle2.png and described here.



; This CNC programme is for cutting smokebox saddles or
; other concave arc surfaces.
; It employs the "Ever Decreasing Rectangles" algorithm.
; Wirtten and devised by  Jon Freeman  June 2011

; Rough using end mill or ripper, finish using ball nose,
; XY origin, X=0, Y=0 is centre of job
; Set Z=0.0 for tool just touching top of job
; Ensure 'ChanLength' sufficient for tool to clear both ends

; Tip - for multiple roughing cuts, start too narrow and increase
; ChanWidth between runs.  This minimises air cutting time.

; User is invited to alter the six parameters below

; ChanWidth = ?
; ChanLength = ?
; ChanRadius = ?
; FlatEndToolRadius = ? - set to 0.0 when using ball nose
; BallNoseRadius = ? - set to 0.0 when using end mill
; Number_of_Rectangles = ?

; _____________________________________________
; User is advised not to alter anything below

; This Section to put machine into known, safe state
M5	;Stop spindle
G17	;Select XY plane
G21	;Units are mm
G40	;Cancel cutter radius compensation
G49	;Cancel tool length offset
G61	;Exact stop
G50	;Reset all scale factors to 1.0
G90	;Absolute distance mode
G94	;Feed per minute mode - mm/min as mm selected above by G21
F150    S1250   ;Feed rate mm/min and spindle speed RPM

; _____________________________________________
; Section where user data processed

; HalfLen = FlatEndToolRadius + BallNoseRadius + [ChanLength / 2]
; HalfWid = ChanWidth / 2
; WorkingRadius = ChanRadius - BallNoseRadius
; HalfAngle = asin [HalfWid / WorkingRadius]
; AngleStep = HalfAngle / Number_of_Rectangles
; CentreDepth = ChanRadius * [1.0 - cos[asin[HalfWid / ChanRadius]]]
; HalfAngle = HalfAngle + AngleStep / 2	;a wheeze to miss the centre
; _____________________________________________
; Programme proper starts here
;		Start of 'Ever Decreasing Rectangles' Algorithm
;  Start spindle clockwise
;  Raise tool clear of work, it should be already
;  MoveRapid X = - HalfLen, Y = + HalfWid   ;Move to top left corner
;  MoveCut Z = 0    ;Lower tool to job slowly just in case
;  Call 'Rectangle' subroutine, 'Number of Rectangles' times
;  Subroutine returns to here, Nothing more do, so
M5 M30  ;M5 code Stops spindle, M30 is End Programme and Rewind
; Programme has ended  here
; _____________________________________________

; The Section below is for subroutines and loops

;   Start of  'Rectangle' Subroutine
;  Reduce 'HalfAngle' by 'AngleStep'
;  NewY = WorkingRadius * sin[HalfAngle] calculates new width
;  NewDepth = WorkingRadius * [1.0 - cos[HalfAngle]] - CentreDepth
;  MoveRapid X = - HalfLen, Y = NewY, Z = NewDepth
;  MoveCut X = + HalfLen cut along one side
;  MoveRapid Y = - NewY
;  MoveCut X = - HalfLen cut along opposite side
;  Return
;   End of 'Ever Decreasing Rectangles' Algorithm

; End of file.

The origin X = 0, Y = 0 has been specified to be at the centre of the job to exploit any benefits from symmetry. Internally derived intermediate parameter 'HalfLen' is half the channel length plus the cutter radii. When used to move to + HalfLen or - HalfLen, the tool should be just clear of the work at the end of either cut. This enables rapid movement to the other side of the channel without risk of crashing the tool.

Derivation of HalfAngle uses the 'asin' function. This is arcsin, or the inverse sine function meaning "the angle whose sine is".

Derivation of CentreDepth uses 'asin' and 'cos' functions, 'cos' is the cosine function.

The addition of AngleStep / 2 to HalfAngle is to avoid cutting along the centre line twice, but statements like

HalfAngle = HalfAngle + AngleStep / 2

may look strange. In the sense we're probably more familiar with, the arithmetic sense, '=' is taken to mean 'is equal to'. However, in this computer programming context, the '= ' equals sign has the meaning 'is assigned to', or 'takes the value of', or 'becomes'. So in this statement, the value of the variable parameter named 'HalfAngle' has half of the value of the variable named 'AngleStep' added to it. More generally, the destination variable to the left of the '=' symbol is where the result of the expression to the right, is placed.

To implement our 'Ever Decreasing Rectangles' algorithm, the 'Rectangle' subroutine is called 'Number_of_Rectangles' times. Each time, the angle is reduced by 'AngleStep'. This updated angle is used to recalculate the Y and Z values required to cut the rectangle, which is then cut. The only function used not already encountered is 'sin', the sine function.

At this stage it should be possible to load the file into Mach 3 and run it. It won't do anything yet, except to allow Mach 3 to look for errors.

Now that we've finished developing our programme, the only task remaining is converting it to executable G codes.

CONVERTING TO G CODE

As stated earlier, as far as computer programming languages go, G Code must be one of the smallest and simplest, although some of the syntax isn't very human-friendly. There really isn't very much to learn before you can produce some impressive work. The only G code reference document used in preparing this article was one downloaded with Mach 3, titled "Using Mach3 Mill" – a very good read. However, small languages tend by nature to incorporate some irritating shortcomings, as we shall discover.

Starting at the top of the programme proper, inserting M3 before the comment

'; Start spindle clockwise', should cause exactly that.

The codes for what we've called MoveRapid and MoveCut are G0 (that's G zero) and G1 (G one) respectively. G0 is for rapid positioning, G1 advances the cut at the feed rate 'F' last specified. Any combination of X, Y and Z axis values can be included in G0 and G1 commands, unmentioned axes are not moved. With our settings above specifying metric units, the command line

G0 Z10

should rapidly move the Z axis only to point Z = 10, leaving the X and Y positions unchanged. Having included these two commands, the file was saved and loaded into Mach 3 (without the machine) and run. Sure enough, the simulated Z axis moved to settle on 10.000.

All remaining machine moves are covered by G0 or G1 commands, and once we've found out about the syntax of subroutine calls and returns, we should be home and dry!

However, we have some G code programming irritations to contend with.

First, the wrong brackets. The rest of the world use parentheses () in arithmetic and maths, as in 3(p+a)/(q-b). In G code, () are reserved for an alternative and non-useful means of including comments, and square brackets [] are used where everyone else would use (). We can't even use parentheses within comments without Mach 3 throwing an error. Apart from that the rules for use of square brackets [] hold few surprises, if in doubt, throw an extra pair in.

Another weakness is in the implementation of the users variables, or parameters as they're called in G code. In our programme development we have used sensible, or sensible-ish names for our variables, ChanWidth, for example. All modern programming languages allow, and encourage, the use of sensible variable names, but not G code. We are stuck with a fixed, numbered list of around 10,000 parameters (think of them as numbered pigeon holes if it helps), many of which are reserved for specific purposes. Plenty remain for our use but care needs to be taken to avoid the reserved ones, and we have no means of giving them sensible names. For example:-

#1000 = 3.14159

The above line places the value 3.14159 into the parameter named #1000. It might have been good to name this one ' PI ', instead of #1000.

A final gripe about G Code is the lack of a conditional test. There is no ' IF '. It is a good idea when writing any programme, to do some basic error checking. It would be nice, for example, to include in our programme something like:

IF ChanWidth > 120 mm THEN message "Channel too wide for this machine!".

But alas. Errors like setting the width too wide may escape detection even in simulation, and will only show up when the machine stops, having tripped a limit switch.


THE SUBROUTINE

Returning to the only remaining missing detail to complete our programme, the mechanism of calling, and returning from, subroutines.

The 'Return' command is M99, and every subroutine must end with a M99 code. If it is missing, code execution continues down the file either until it finds one, or something nasty happens, all too often the latter. When writing a subroutine, it isn't a bad idea to make the M99 code line, the very first line you write:

M99 ; Return

The 'Call' command is M98, and the rather unfriendly syntax required in use is:

M98 P n

where n represents some unique group of digits used as a label, without any numerical significance. The above is how you would call a subroutine once. To cause a subroutine loop to be executed repeatedly, as we require here, the primitive subroutine call is extended using the 'L' indicator:

M98 P n L m

where m is the number of times to repeat. Our calling line can then become:

M98 P 1234 L #105 ; where #105 is the parameter containing 'Number_of_Rectangles'

On a line of its own at the start of the subroutine code, we are required to insert the letter 'O', not zero this time, followed by our arbitrary '1234' digit group. This syntax is again a little strange and user-unfriendly, but that's fairly typical of small and ancient programming languages.

FILLING IN THE DETAILS – MAKING IT ALL WORK

Working through line by line, implementing the commands and arithmetic, saving often and reloading into Mach 3 helped iron out the wrinkles and find fixes for things that looked as if they should have worked but didn't. For example the line

G1 X #110

does work, the machine moves at feed rate 'F' to the X co-ordinate stored in variable named '#110', so we might expect the line:

G1 X - #110

to cause a similar move to a mirror image X position. However, this causes Mach 3 to report the error "No digits found where real number should be Line nn". One way around this problem is to put it in a more explicit form:

G1 X [0.0 - #110]

which does work. Likewise, lines assigning values to parameters often caused error reports that were made to go away by enclosing whole expressions in square brackets.

Starting at the top of the programme file, some real variables, or parameters, were assigned to our six user parameters and given real numeric values:

#100 = 90.3 ; ChanWidth = ?

#101 = 19.5 ; ChanLength = ?

#102 = 88.1 ; ChanRadius = ?

#103 = 0.0 ; FlatEndToolRadius = ? Set to 0.0 when using ball nose

#104 = 3.175 ; BallNoseRadius = ? Set to 0.0 when using end mill

#105 = 48 ; Number_of_Rectangles = ?

Here the six variables named '#100' to '#105' have been assigned to their own purpose and given some real world values. What we had on the lines to start with remain as comments to remind us what we mean.

The original working out of the maths involved scribbled sketches and notes on the back of an envelope. Translating our six user-friendly parameters above into a form suited to the workings of the algorithm closely resembles the stages of scribbled algebra, and a few intermediate variables with values derived from the user input will be used by the algorithm itself. Named '#110' to '#115', these intermediate variables are set up in the 'Section where user data processed'.



; This CNC programme is for cutting smokebox saddles or
; other concave arc profiled channels.
; It employs the "Ever Decreasing Rectangles" algorithm.
; Wirtten and devised by  Jon Freeman  June 2011

; Rough using end mill or ripper, finish using ball nose,
; XY origin, X=0, Y=0 is centre of job
; Set Z=0.0 for tool just touching top of job
; Ensure 'ChanLength' sufficient for tool to clear both ends

; Tip - for multiple roughing cuts, start too narrow and increase
; ChanWidth between runs.  This minimises air cutting time.

; User is invited to alter the six parameters below

#100 = 90.3	; ChanWidth = ?
#101 = 19.5	; ChanLength = ?
#102 = 88.1	; ChanRadius = ?
#103 = 0.0	; FlatEndToolRadius = ? Set to 0.0 when using ball nose
#104 = 3.175	; BallNoseRadius = ? Set to 0.0 when using end mill
#105 = 48	; Number_of_Rectangles = ?

; _____________________________________________
; User is advised not to alter anything below

; This Section to put machine into known, safe state
M5	;Stop spindle
G17	;Select XY plane
G21	;Units are mm
G40	;Cancel cutter radius compensation
G49	;Cancel tool length offset
G61	;Exact stop
G50	;Reset all scale factors to 1.0
G90	;Absolute distance mode
G94	;Feed per minute mode - mm/min as mm selected above by G21

; _____________________________________________
; Section where user data processed

#110 = [#103 + #104 + [#101 / 2]]	; HalfLen = FlatEndTR + BallRad + [ChanLength / 2]
#111 = [#100 / 2]		; HalfWid = ChanWidth / 2
#112 = [#102 - #104]		; WorkingRadius = ChanRadius - BallNoseRadius
#113 = [asin [#111 / #112]]	; HalfAngle = asin [HalfWid / WorkingRadius]
#114 = [#113 / #105]		; AngleStep = HalfAngle / Number_of_Rectangles
; CentreDepth = ChanRadius * [1.0 - cos[asin[HalfWid / ChanRadius]]]
#115 = [#102 * [1.0 - cos [asin [#111 / #102]]]]	; CentreDepth
#113 = [#113 + #114 / 2]	; HalfAngle += AngleStep / 2	;prevent double centre cut
; _____________________________________________
; Programme proper starts here

F250    S1000   ;Feed rate 'F' mm/min and spindle speed 'S' RPM
M3	;  Start spindle clockwise
G0 Z10.0	;  Raise tool clear of work, it should be already
G0 X[0.0 - #110] Y#111	;  MoveRapid X = - HalfLen, Y = + HalfWid, top left corner
G1 Z0.0	;  MoveCut Z = 0    ;Lower tool to job slowly just in case
	;  Call 'Rectangle' subroutine, 'Number of Rectangles' times
M98 P 1234 L #105	;where #105 contains Number_of_Rectangles
	;  Subroutine returns to here
	;   End of 'Ever Decreasing Rectangles' Algorithm
G0 X0.0 Y0.0 Z20.0	;lift tool clear of the job
M5 M30  ;M5 code Stops spindle, M30 is End Programme and Rewind
	; Programme has ended  here
; _____________________________________________

; The Section below is for subroutines and loops
	;   Start of  'Rectangle' Subroutine
O 1234
#113 = [#113 - #114]		;  HalfAngle = HalfAngle - AngleStep
#200 = [#112 * sin [#113] - #103]	;  NewY = WorkingRadius * sin[HalfAngle]
	;  NewDepth = WorkingRadius * [1.0 - cos[HalfAngle]] - CentreDepth
#201 = [#112 * [1.0 - cos [#113]] - #115]
	;  MoveRapid X = - HalfLen, Y = NewY, Z = NewDepth
G0 X[0.0 - #110] Y#200 Z#201
G1 X#110		;  MoveCut X = + HalfLen
G0 Y[0.0 - #200]	;  MoveRapid Y = - NewY
G1 X[0.0 - #110]	;  MoveCut X = - HalfLen
M99	;  Return
	;   End of 'Rectangle' Subroutine
; End of file.

In the programme listing above, all the missing detail is filled in, and we might have a workable programme. In setting up the values to put into the intermediate variables, some of the calculation uses trig functions 'sin', 'cos, and 'asin', the rest of the arithmetic is simply addition, subtraction, multiplication and division. The 'HalfLen' and 'HalfWid' variables are useful because an early decision was to exploit symmetry by placing the XY origin (X = 0, Y = 0) at the centre of the work piece. The machine then needs to move between plus and minus half the width and length.

In the 'Programme proper' section, the feed rate and spindle speeds are set and the spindle started. The tool is lifted well clear of the job and positioned ready to begin cutting the first rectangle, it is then lowered slowly to just meet the work surface. The 'Rectangle' subroutine is then invoked 'Number_of_Rectangles' times, and that's it. Job well done? The proof is in what Mach 3 makes of it.

Mach 3 - Ever Decreasing Rectangles plot

This figure shows the Mach 3 Tool Path window, probably the most useful screen for simulation purposes as it allows rotation of the plot as a 3D image. Fun may be found in editing our six user variables, saving the file and reloading it into Mach 3 to see what difference is made. The plot as shown has Number_of_Rectangles set to 48 and ChanLength set quite short as this produced a nice image. The red lines indicate tool movements using the G0 rapid move command, the blue lines represent G1 moves at cutting feed rate. It looks as if we have a programme to cut arc sectioned channels.

HOW ARE WE DOING SO FAR - A CRITIQUE

As the simulation shows, we have created a programme that promises to do what we want it to do, and as will be seen, it does drive the KX3 milling machine to cut metal. All well and good, but at the outset we promised to make it generic, let the computer do all the hard work for us, and to try to keep everything easy to use, read and understand. To that end a reasonable job has been done in grouping the user alterable parameters in one place, but there's perhaps one fairly obvious improvement worth looking into.

In using 'Number_of_Rectangles' as one of our user alterable parameters, we've chosen a parameter to suit the algorithm we devised, rather than being convenient for the user. Mechanical engineers won't think in these terms, rather they will be concerned with depth of cut, or how far the tool advances between rectangle cuts. We can address this quite simply by allowing the user to alter a more intuitively named variable, 'CutDepth' for instance, instead of 'Number_of_Rectangles'.

Given 'CutDepth' and the other user values entered, it is simple enough to calculate the 'Number_of_Rectangles' value required by the algorithm. Starting with the length of the arc described by 'HalfAngle', let's call that 'ArcLength'.

ArcLength = 2 * PI * ChanRadius * HalfAngle / 360

then

Number_of_Rectangles = ArcLength / CutDepth

which all simplifies and reduces to:

Number_of_Rectangles = 0.0175 * ChanRadius * HalfAngle / CutDepth

However, this creates a hole for us to fall into. The 'Number_of_Rectangles' obviously needs to be a whole number, or integer. Our calculation above is extremely unlikely to give us a whole number integer result, so we would be well advised to try to avoid any situation where our subroutine loop gets called 27.83759 (or whatever the number is) times! Fortunately two operations are provided to help us out in cases like this, the quaintly named FIX and FUP operations. FIX rounds a number down to the next more negative integer, while FUP rounds a number up to the next more positive integer value.

Using FUP to round up the Number_of_Rectangles gives:

Number_of_Rectangles = FUP [0.0175 * ChanRadius * HalfAngle / CutDepth]

So to edit the file, the line

#105 = 48 ; Number_of_Rectangles

needs to be deleted and replaced in the same place by:

#106 = 1.25 ; 'CutDepth', in this example set to 1.25

Then in the "Section where user data processed" can be inserted:

#105 = FUP [0.0175 * #102 * #113 / #106] ; Number_of_Rectangles

Care needs to be taken about where exactly in the listing to insert this line. It must be positioned after the line that works out #113 'HalfAngle' because it uses the value 'HalfAngle', but before any line that uses #105 'Number_of_Rectangles'.

Any ideas to aid readability of the programme should be welcomed. One such is in helping to see which values are integers, and which can have fractional parts. It should be a rule that integers are entered without a decimal point. All modern computer programming languages strictly enforce a distinction between integer and 'floating point' values, G code does not. Similarly, it is a good idea to include a decimal point in all values that are not integers. Suppose we have a tool radius of 3 mm. We could enter this simply as '3', but writing it as '3.0' helps to remind us that '3.175' is also possible.

This completes our first programme, all in about 80 lines, some of which are blank, and many of which are simply comments. There are only 40 lines in it that do all the work, and the rest could all be deleted, however that would make life difficult the next time you wanted to use it.


TOP SHAPE - A shade more complicated ?

Skirt sketches

Referring to the "skirt.png" figure, the top may comprise as many as three sections. There may be a cylindrical section on the top of some practical 'Cylinder Height', which may also be zero. Below this is the curved fillet infill section with some 'Fillet Radius' as shown. Around the outside may be a shape of constant thickness that hugs the smokebox, a sort of curved flange, of any practical width including zero.

Dealing with these three sections one after another seems sensible. This suggests moving the tool repeatedly around the piece in a series of circles as viewed from above, with the tool height modulated to fit the shape. We could start at the top and work outwards and downwards, or we could start at the bottom and outside, and work inwards and upwards. Of the two, the latter has the advantage of taking care of any cylindrical top part for us, it's simply part of what's left when we've finished cutting the fillet infill section. A perfect name for this algorithm? - “Ever Decreasing Circles”.

Then how do we deal with the undulating shape? It would be very much more difficult to track this shape using a ball nose tool. Three dimensional tool offset compensation is too ambitious for this project. Using a flat ended milling or ripper cutter keeps cutter offset compensation as simple as adding the circle and tool radii to calculate the controlled point radius.

In our first programme we used the G0 and G1 commands, and no others, to cause movement. To cut a clockwise circular or helical arc we use the (steady now) G2 command. When used for cutting arcs in the X-Y plane, the G2 command may also take an updated value for the Z axis. Thus it is simple to cut a helical arc, rising or falling with a constant gradient. Looking at the shape we want, and having chosen circular tool paths, we are faced with trying to cut circles with a constantly changing gradient. This we can not do, but by forming the circle from a large number of small arcs, we can cut a good approximation to the shape required, certainly good enough for our purposes. This identifies one of our programme parameters, let's call this one 'Arcs per Circle', that needs to be located in one place only, in the list of user alterable parameter values, so the user can tweak it easily and safely to optimise the compromise between run time and accuracy.

Our first programme was designed using a top down approach, to good effect. For this programme, being a little more complex, we can combine elements using both top down and bottom up approaches. Already identified are the 'Curved Flange' shape and the 'Fillet Infill' regions. These different operations may be performed using separate subroutines. Then a top down fragment from the main programme might look like:

; Ever Decreasing Circles algorithm

; process input data to machine curved flange

M98 P 1000 ; Go and machine the 'curved flange' region

; further process input data for machining fillet infill

M98 P 2000 ; Go and machine the 'fillet infill' region

M5 M30 ;M5 code Stops spindle, M30 is End Programme and Rewind

; Programme has ended here

Then in the subroutines section:

O 1000 ; This is where we machine the 'curved flange' region

... ; commands to do the job go here

M99 ; Return

O 2000 ; This is where we machine the 'fillet infill' region

... ; commands to do the job go here

M99 ; Return

We've also alluded to some undulating, or Z axis modulated, 'Circle' function subroutine, but the circle is made by repetitive use of an 'Arc' function subroutine. One subroutines calling another, so called nested subroutines. In this programme the top level main programme calls a subroutine to cut a region, which calls 'Circle', which in turn calls 'Arc'. This is nesting to a depth of three.

As 'Arc' will only ever get called from 'Circle', and it is 'Arc' that will perform all the cutting moves, then perfecting 'Arc', followed by 'Circle' would be working from the bottom up.

Our 'Arc' and 'Circle' subroutine begin to take shape:

O 3000 ; 'Circle' Subroutine

; 'AngleAround' = 0.0 specifies all circles start from the same 0 degrees compass point

; Move to start position X = 0.0, Y = Radius, Z = ? co-ordinates

M98 P 4000 L 'Arcs per Circle' ;cut enough short arcs to complete one circle

M99 ; Return

O 4000 ; 'Arc' subroutine

; Add AngleIncrement to 'AngleAround' to move a bit further around the circle

; Using 'AngleAround', calculate new X, Y and Z co-ordinates

G2 X Y Z R ; Cut arc (of radius 'R' specified elsewhere) to new X, Y, Z

M99 ; Return

Keeping at our bottom up approach for the time being, we will flesh-out the 'Arc' subroutine, the one that ultimately does all the real work. The fun task before us is in calculating the X, Y and Z co-ordinates at the end of the arc to be cut. If, as before with 'Ever Decreasing Rectangles', we specify the origin of the XY plane (X = 0.0, Y = 0.0) to be the centre of the piece, then the calculations for the X and Y co-ordinates reduce to the blissfully simple:

; X = Radius * Sine (AngleAround)

; Y = Radius * Cosine (AngleAround)

TO MACHINE THE CURVED FLANGE SECTION

In calculating Z, we'll begin by considering a circular path in the curved flange region. Having worked out how to do this, we'll consider how to adapt and apply the same or similar arithmetic to cutting the rising fillet section.

Depth Diagram

The values we need to work with are:

Radius of the circle being cut = 'cr'

Radius of smokebox + flange thickness = 'rs'

Current value of 'AngleAround' the circle being cut

The distance 'd' is found in a similar way to the new 'X' co-ordinate above, the only difference being the radius used in finding 'X' has the tool radius added to the required cut radius 'cr'.

d = cr * Sine (AngleAround)

Knowing 'd', 'a' may be found by Pythagoras

a = sqrt (rs * rs – d * d), where 'sqrt' is the computer square root function

the required z drop value is then:

z_drop = rs – a

z_drop = rs – sqrt (rs * rs – d * d)

To complete the calculation for the Z co-ordinate we need to take into account the height of the fillet and cylindrical sections in order to stick to the perfectly sensible convention of using zero on the Z axis as the height where the tool just meets the top surface of the piece. Positive Z values lift the tool clear, and negative Z values lower the tool for cutting.

Z = 0 - ('FilletRadius' + 'CylinderHeight' + rs - sqrt (rs * rs – d * d))

With this equation embedded in the 'Arc' routine, the higher level 'Curved Flange' routine will only need to call 'Circle' some number of times, reducing radius 'cr' each time until the whole width of the curved flange section has been cut.

TO MACHINE THE FILLET INFILL SECTION

A first attempt at the fillet infill section involved a simple 'FilletInfill' subroutine that worked its way up the fillet using an angle step mechanism between zero and 90 degrees. At each pass, the angle up or around the fillet was used to calculate the new height and circle radius 'cr' values.

While this worked (in simulation), the effect was not the smooth blend aimed for, rather it had an appearance similar to a smaller pipe welded into the side of a larger pipe with a very neat weld fillet. This is seen in the Mach 3 plot below. Some further thought was needed.

Mach 3 - Not Quite Right Circles

PERFECTING THE RADIUS BLEND

After venturing down one or two blind alleys, a viable scheme was hit upon. The first cut, at the bottom outside of this section, is virtually identical to the final cut of the previous curved flange, with all the 'Z tool height modulation' the same. But at the top of this section, where it meets any cylindrical section, the effect of any 'Z modulation' needs to be reduced to zero. The answer then, is to include a gain control to the Z modulation calculation, such that:

z_drop = 'Gain' * (rs - sqrt (rs * rs – d * d)), and

Z = 0 - ('FilletRadius' + 'CylinderHeight' + ('Gain' * (rs - sqrt (rs * rs – d * d))))

The 'Gain' value is initialised to be 1.0 so as not to alter anything in the curved flange section. If we name the current angle up and around the fillet 'FilletAngle', a perfectly good gain control is formed by:

'Gain' = Cosine ('FilletAngle')

with 'FilletAngle' initialised to 0.0

THE FINISHED PROGRAMME

All of the programme fragments, ideas and comments above were typed into the standard template file, leaving little work for completion. Eight user variable parameters are required, and the main programme then has only these few functions to perform:

Reset machine to known, safe state

Process input information into formats required

Cut 'p' circles in the 'CurvedFlange' region

Cut 'q' circles in the 'RadiusedBlend' region

Stop machine and end programme.

This describes the top level structure showing use of two separate subroutines, one for each of the two distinct cut regions. These are called 'p' and 'q' times respectively, where 'p' and 'q' are determined by the width of each cut region and the chosen depth of cut. They differ only in the set of values being tweaked on each call before calling the 'Circle' cutting subroutine. Both routines are simple, taking only four, and seven lines of code. These contain the only two calls to 'Circle' in the programme.

The 'Circle' subroutine initiates cutting of a circle using parameters and co-ordinates set up by the calling function. All that happens in 'Circle' is the tool is moved to the start of cut position, a local variable 'AngleAround' is zeroed, then the 'Arc' subroutine is called a number of times we set at the top of the programme - larger number for smoother finish but not so large as to slow it all down too much. 100 might be a good choice and was the value used to cut the real part shown. 1000 is a bad choice as it causes Mach 3 to report an undocumented error, "Too Many Nests" !

And finally, where most of the number-crunching, and all of the cutting takes place, the 'Arc' subroutine, the lowest level subroutine that ends up doing almost all the work. Even so, it contains only eight lines of code that do anything! On each call, 'AngleAround' is increased, new X, Y and Z co-ordinates are calculated and a short arc cut to those co-ordinates.

The whole programme, comments included, fits into fewer than one hundred lines. A simulated run is shown here.

Mach 3 - Ever Decreasing Circles plot

PICS OF KX3 MACHINE IN ACTION

Having just completed a rough cut using a 6mm ripper cutter and having put a fairly large 'CutDepth' value into the programme. Also the 'tool radius' in the programme was set to 3.5 mm. This caused at least 0.5 mm to be left for removal by the finish cut with tool radius set correctly to 3.0

Almost completed the finishing cut, still using the 6mm ripper cutter but with 'CutDepth' in the programme reduced to 0.15 mm.

Note the small hole visible near the bottom edge. This was drilled to assist alignment, the metal will be removed when the underside is cut.

Cutting the top shape - not long after start

Cutting the top shape - nearly finished

Roughing out the underside using a 6mm ripper cutter

Finish cutting the underside using a ball nose cutter


Ever Decreasing Circles Programme Listing

;Chimney Skirt for 6412s
;Last mods 12th May 2011

#1000=[88.0]    ;boiler_rad
#1005=[5.0]	;Height of cylindrical part on top
#1010=[40.00]    ;flange_rad inner where it meets outside of root radius part
#1011=[40.10]    ;flange_rad outer using Braunton bronze
#1012=[11.0]    ;radius of root up to parallel dome sides
#1015=[3.1]     ;tool_rad
#1027=[0.17]        ;mm advance per cut
#1017=[FUP[#1011 - #1010] / #1027]      ;number of  passes to cut boiler profile hugging part, and up rad to dome
#1025=[100] ;Numof short arc sections used  to make circle
#1016=[#1011 + #1015]   ;flange_rad_outer + tool_rad
#1018=[[#1011 - #1010] / #1017]    ;derived step size between profile hugging flange cuts
#1020=[0.0] ;instantaneous angle around bush boss
#1021=[0.0] ;instantaneous angle up root radius to parallel dome
#1052=[1.0] ;used for cos[#1021] later
#1022=[0.0] ;derived Z addition due to root radius climb
#1023=[0.0] ;derived radius reduction due to root radius climb
;   //angle = 0 defined as parallel to boiler x axis
;   //double dy = flange_rad * sin(angle);    //distance off x axis
;   //double dx = flange_rad * cos(angle);    //distance along x axis
;double dz = boiler_rad * [1.0 - cos[asin[ flange_rad * sin[angle]/ boiler_rad]]];

		;	Safety, reset machine to known, safe state.
M5	(Stop spindle)
G17	(Select XY plane)
G21	(Units are mm)
G40	(Cancel cutter radius compensation)
G49	(Cancel tool length offset)
G61	(Exact stop)
G50	(Reset all scale factors to 1.0)
G90	(Absolute distance mode)
G94	(Feed per minute mode - mm/min as mm selected above by G21)
F266    S1500   M3

G0  X0  Y0  Z8.0
G0  Y#1016
M98 P2000 L#1017    ;Boiler profile hugging flange part
#1031=[#1011]
;recalculate #1017 here
#1017=[FUP [[1.5708 * #1012] / #1027]]
#1019=[90.0 / #1017]    ;derived angle at each step up root radius to parallel part of dome

M98 P3000 L[1.0 + #1017]   ;Working up root radius towards vertical parallel part of dome
G0	Z8.0
M5
M30 ;prog end and rewind

O   1000
G1  Z[#1022 - [#1005 + #1012]]
G1  X0.0  Y#1016
M98 P1001   L[0.0 + #1025]
#1020=[0.0] ;reset angle around dome to zero
M99

O   1001    ;Call this 360 times for boiler bush crinkle cut
#1020=[#1020 + [360 / #1025]]    ;inc angle for next go
#1070=[#1016 * sin[#1020]]  ;dx
#1080=[#1016 * cos[#1020]]  ;dy
#1090=[#1052 * [#1000 * [1.0 - cos[asin[#1011 * sin[#1020] / #1000]]]]]   ;dz
G2  X#1070  Y#1080  Z[#1022 - #1012 - #1090 - #1005] R#1016  ;+ for  boiler bush crinkle cut, - for dome skirt
M99

O   2000            ;Deals with part of flange hugging boiler contour
M98 P1000
#1011=[#1011 - #1018]   ;wind in towards centre for next go around
#1016=[#1011 + #1015]   ;flange_rad + tool_rad
M99

O   3000            ;Deals with radiused blend from dome or chimney to contour hugging part
;Number of times to execute = 1 + #1017
;change in radius to profile while climbing root radius #1023 = RR#1012 * sin ang#1021
;change in Z to profile while climbing root radius #1022 = RR#1012 * [1-cos ang#1021]
M98 P1000
#1021=[#1021 + #1019]   ;next angle degrees up root radius to parallel dome sides
#1011=[#1031 - [#1012 * sin[#1021]]]
#1016=[#1011 + #1015]   ;flange_rad + tool_rad
#1052=[cos[#1021]]
#1022=[#1012 * [1.0 - #1052]]
M99