## Tuesday, 1 November 2016

### Cams without CAM

I am about to start work on a project which needs a complex set of cams. Each cam is a set of 7 or 8 tracks and 6, 10, or 12 positions.  I intend to make them by CNC machining.

This is liable to be a rather dull blog post, so perhaps I should turn things upside-down and show what the final result is:

The simple way to do this would be to just move the A (rotary) axis while bobbing up and down in Z, but this would not give flat faces on the cams. I could do it with a woodruff-form cutter, but the concave arc radius that I want is too small, so there would be no room for the shank.

I imagine that, in theory, I could do it with a reverse-dovetail cutter and with the head of the mill tilted. And that might be a thought I come back to, now I have had it.

I could, also, model the required shape in CAD and let the CAM software do the hard-lifting. But modelling the cams individually would be tedious, and if I change the combinations that I want, then it all needs to be re-modelled.

So, I have decided to write some parametric G-code for the task. And this blog is actually mainly for my own benefit so that I can remind myself how it was all worked out later.

Firstly, I have had to remind myself of school-level geometry. The cams will have two basic radiuses: Rmajor and Rminor. Each lobe will have lead-in and lead-out radiuses r.

The external cam profile is the green line. The cutter path (with the axis of the cutter in the plane of the page) will start at the top, then trace the curve around the circle until the straight portion is horizontal, followed by a straight cut until the next transition, then tracing a seconf internal circle.

The red lines show the centre of each cam lobe,  the angle between each cam lobe θ is simply 360/N where N is the number of positions.

θ = 360 / N                                                       #50

Each cam has a dwell angle to reduce the need for accurate indexing. The angle ACB is given by

φ = θ - 2.dwell                                                   #51

It isn't immediately obvious from the drawing but the arc radius is not the same as the difference between Rmajor and Rminor. This means that the lines AC and BC are not the same length.

AC = Rmajor - r                                                  #52
BC = Rminor + r                                                 #53

To work out how far the work has to rotate to make the cam flank horizontal we need the angle BAC.
We also need (and it took me a lot of adjusting G-code to realise this) the different angle that the low-to-high cam flanks make to the dwell angle line when cutting in the same (anticlockwise) direction.

The angle BAC can be worked out from the Cosine Rule (which I had forgotten). For a triangle with angles A, B and C and opposite sides a, b and c:

c = a2+ b2 - 2ab Cos(C)

In this case we first need the distance AB

AB = (AC)2 + (BC)2 - 2(AC)(BC)Cos(φ)           #54

And the angle from horizontal of line AB  can be worked out from the Sine Rule:

a/Sin(A) = b/Sin(B) = c/Sin(C)

In the terms of the geometry above:

AB/Sin(φ) = BC/Sin(BAC)

BAC = Arcsin(BC * Sin(φ) / AB)                       #55

so the angle from horizontal

90 - Arcsin(BC * Sin(φ) / AB)                           #56

The really important angle, though, is that of the tangent angle, in orange below:

This is a further λ degrees past the angle (#55) above.

This is a simple right-angle triangle made up of AB/2 and r

λ = Arcsin( 2r / AB)                                                #57

And the angle from horizontal is:

90 - Arcsin(BC * Sin(φ) / AB)   Arcsin( 2r / AB)  #58

Having got the angle we need to rotate by to get the flank angle for high to low, we can start to think about the cutter path.
This is the same diagram, but rotated to the correct angle to cut a high-to-low cam flank.

The angle klm is the same as the angle that we have rotated the work through, and this is true throughout the initial rotation. So the tool needs to track a point perpendicularly above the arc centre, l. This is a point given by:

Y = ( Rmajor - r  ) Sin(klm)
Z  = ( Rmajor - r  ) Cos(klm) + r

and the first arc radius is cut by tracking this position as the work rotates. There then needs to be a straight move from A to B. This starts at the point above, and stops at a point perpendicularly below point p. Point p is:

Y = ( Rminor + r  ) Sin(kpq)
Z  = ( Rminor + r  ) Cos(kpq) + r

And all we need to do to define that point is work out the angle kpq.
I noticed when checking my calculations agains a CAD model that the angle kpq is the "other" cam flank angle, and is given by

kpq = klm -  φ (or in G-code terms #58 - #51)                                   #59

I haven't actually done the construction to see why this is, I leave it as an excrcise for the reader ;-)
The low-to-high transition is defined by the same two angles, starting with a rotation to angle kpq (#59) to a point defined by angle klm (#58)

With the governing numbers now calculatable we can start to think about G-code. First set up the basic geometry:

#<r_major> = 32.5  ; top radius

#<r_minor> = 22.5  ; bottom radius
#<groove> = 20     ; groove radius
#<ramp_r> = 3      ; radius of ramp transitions
#<dwell> = 3       ; half-angle of cam dwell
#<gap> = 0         ; gap width
#<width> = 10      ; cam width
#<tool_dia> = 6    ; tool dia
#<cut> = 2         ; cut depth (radial)
#<depth> = 2       ; cut depth (axial)
#<rows> = 1        ; number of rows
#<pos> = 10        ; number of positions

And the feed rates.

#<s> = 1000        ; spindle speed
#<lin_feed> = 5000 ; Feed rate linear
#<ang_feed> = [#<lin_feed> * 360 / [2 * 3.14 * #<r_major>]]  ; Angular feed rate

The cam shapes are encoded in "decimal coded binary" in that these are actually decimal numbers because G-code doesn't have binary constants or bitwise operators, but a 1 means Rmajor and a 0 means Rminor. Handily, G-code allows computed variable names, if #22 = 3 then #[40 + #22] returns the value of #43. 7 rows of 10 positions

;     0123456789AB
#41 = 1010011111   ;bottom
#42 = 1100111111   ;bottom right
#43 = 1010001010   ;bottom left
#44 = 0011111011   ;middle
#45 = 1101100111   ;top right
#46 = 1010111011   ;top left
#47 = 1011011111   ;top

Set up some variables to hold current position.

#<X> = [[#<rows> + 1] * #<gap> + #<rows> * #<width> - [#<tool_dia> / 2]]
#<Y> = 0
#<Z> = #<r_major>
#<A> = #<_A>                      ; start at the current A value

Start the spindle and set up some potentially useful params.

M3 S#<s>
G4 P2
F #<lin_feed>
G19                     ; YZ Arcs

G91.1                   ; Absolute Arc centres

O100 is the outer loop, looping through the number of cams. Between each cam is an (optional) gap

O100 WHILE [#<rows> GT -1]
;cut a space
#<cut_a> = [[#<gap> - #<tool_dia>] / FUP[[#<gap> - #<tool_dia>] / #<cut>]] ; adjusted cut
#<depth_a> = [[#<r_major> - #<groove>] / FUP[[#<r_major> - #<groove>] / #<depth>]]
(debug, adjusted cut is #<cut_a> x #<depth_a>)
#<X> = [[#<rows> + 1] * #<gap> + #<rows> * #<width> - [#<tool_dia> / 2]] ; X
O200 WHILE [#<X> GE [#<rows> * #<gap> + #<rows> * #<width> + [#<tool_dia> / 2]]]
G0 Z[#<Z> + #<cut>]
G0 X#<X> Y#<Y>
O201 WHILE [#<Z> GT #<groove>]
#<Z> = [#<Z> - #<depth_a>]
G1 F#<lin_feed> Z#<Z> A[#<A> - 20]
#<A> = [#<A> - 390]
G1 F#<ang_feed> A#<A>
O201 ENDWHILE
#<Z> = #<r_major>
#<X> = [#<X> - #<cut_a>]
O200 ENDWHILE

O300 is the loop along X to cut the cam in multiple passes. Each cut starts with finding somewhere on the cam that is high rather than low. The #<h> parameter is how we find a "bit" in the decimal-coded binary cam pattern.

O300 WHILE [#<X> GE [#<rows> * #<gap> + [#<rows> - 1] * #<width> - [#<tool_dia> / 2]] AND #<rows> GT 0]
G0 Z[#<r_major> + #<cut>]
G0 X#<X> Y0
#<A> = [360 * FIX[#<A> / 360]]                                  ;reset to index
G0 A#<A>
;find a high spot to start
#<index> = 0                                                    ; cam index
#<h> = [FIX[#24 / [10 ** [#<pos> - #<index>]] MOD 10]]          ; are we high or are we low?
#<old_h> = 1                                                    ; old level (#<h>)
O301 WHILE [#<h> EQ 0]
#<index> = [#<index> + 1]
#<A> = [#<A> + #50]
#<h> = [FIX[#24 / [10 ** [#<pos> - #<index>]] MOD 10]]
O301 ENDWHILE

O400 is a loop in Z-depth. First an adapted cut-depth is calculated to make up the distance from  Rmajor and Rminor in an integer number of cuts, then the apparent position of the Rminor arc circle is moved in by this amount each iteration. This isn't the most efficient way possible, but skipping the air-cuts is more trouble than I care for.

O400 WHILE [#<r_temp> GE #<r_minor>]     ; work down in depth
#<r_temp> = [#<r_temp> - #<depth_a>]

; Calculate geometric parameters
#50 = [360 / #<pos>]                        ; cam-to-cam angle
#51 = [#50 - #<dwell>]                      ; transition centre angle
#52 = [#<r_major> - #<ramp_r>]              ; transition high arc centre radius
#53 = [#<r_temp> + #<ramp_r>]               ; transition low arc centre radius (current)
#54 = [52**2 * 53**2 - 2*52*53*COS]     ; transition centre distance
#55 = [90 - ASIN[#53 * SIN / #54]       ; transition centre angle from vertical
#56 = [#55 + ASIN[2 * #<ramp_r> / #55]      ; cam flank angle

G1 X#<X> Y#<Y> Z #<Z> F #<lin_feed>

And then we calculate whether the required move is an up, a down, or a stay-the-same for each position round the cam.

G1 X#<X> Y#<Y> Z #<Z> F #<lin_feed>

O302 REPEAT [#<pos>]
#<index> = [[#<index> + 1] MOD #<pos>]
#<h> = [FIX[#24 / [10 ** [#<pos> - 1 - #<index>]] MOD 10]]

And make a move accordingly:

O303 IF [#<old_h> GT #<h>]                                    ; high-to-low move
#<A> = [#<A> + #<dwell>]
G1 F#<ang_feed> A#<A>
#30 = 0
O3031 WHILE [#30 LT #58]
#31 = [-#52 * SIN[#30]]
#32 = [#52 * COS[#30] + #<ramp_r>]
#33 = [#<A> + #30]
G1 F#<lin_feed> Y#31 Z#32 A#33
#30 = [#30 + 0.1]
O3031 ENDWHILE
#30 = [#59]
O3032 WHILE [#30 GE 0]
#31 = [-#53 * SIN[#30]]
#32 = [#53 * COS[#30] - #<ramp_r>]
#33 = [#<A> + #30 + #51]
G1 F#<lin_feed> Y#31 Z#32 A#33
#30 = [#30 - 0.1]
O3032 ENDWHILE
#<A> = [#<A> + #51 + #<dwell>]
G1 F#<ang_feed> A#<A>
O303 ELSEIF [#<old_h> LT #<h>]                                  ; low to high move
#<A> = [#<A> + #<dwell>]
G1 F#<ang_feed> A#<A>
#30 = 0
O3033 WHILE [#30 LT #59]
#31 = [#53 * SIN[#30]]
#32 = [#53 * COS[#30] - #<ramp_r>]
#33 = [#<A> - #30 ]
G1 F#<lin_feed> Y#31 Z#32 A#33
#30 = [#30 + 0.1]
O3033 ENDWHILE
#30 = #58
O3034 WHILE [#30 GE 0]
#31 = [#52 * SIN[#30]]
#32 = [#52 * COS[#30] + #<ramp_r>]
#33 = [#<A> + #51 - #30 ]
G1 F#<lin_feed> Y#31 Z#32 A#33
#30 = [#30 - 0.1]
O3034 ENDWHILE
#<A> = [#<A> + #51 + #<dwell>]
G1 F#<ang_feed> A#<A>
O303 ELSE                                                       ; remain at the same level
#<A> = [#<A> + #50]
G1 F#<ang_feed> A#<A>
O303 ENDIF

Then all that remains is to store the previous height to determine what the next move is, and close the loops.

#<old_h> = #<h>

O302 ENDREPEAT
O400 ENDWHILE
#<X> = [#<X> - #<cut_a>]
O300 ENDWHILE

#<rows> = [#<rows> - 1]
O100 ENDWHILE

M2

This ends up as a total of 160 lines of G-code that can make any cam of this type. It is trivial to make changes to the geometry and to the cam pattern. Compare that to the nature of the G-code produced to do the same thing with a CAM package. And, in the case of using a CAM package to make a cam, any change to the cam pattern would be a great deal of tedious re-modelling and a re-processing of the model