DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Moderators: TomKerekes, dynomotion

Tim G
Posts: 38
Joined: Wed Jan 13, 2021 9:51 pm

DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by Tim G » Fri Sep 05, 2025 6:53 pm

I was hoping I could pester someone for help with a DIY ATC system I put together (pictures below). I wanted a customized version of the RapidChangeATC setup and thought I'd take a stab at making a collet nut spinning ATC myself. I've since come up with a design and put it all together on a pneumatic sliding table. All seems to be functioning well mechanically. I've tested the absolute positions of each tool bay (there are 16 altogether) and can consistently locate the spindle over each bay and spin the collet nuts on/off with some gcode files I put together.

The concept has proven out enough now that I'm ready to start digging into the c programming side of things. I have very low experience with C coding, just having modified files for my CNC router over the years by Googling what different commands do. I was looking over the Linear4ToolHolder and associated example files to see what I could figure out. I can tell where I would enter absolute coordinates, but my system isn't using a traditional pneumatic ATC spindle with the specialized collets. I will just be spinning on/off regular ER20 collet nuts by plunging the router head into a tool bay while running it at around 1500rpm for putting the tools/collet nuts on, and at 1600rpms or so with an M4 command to spin them back off.

This should simplify things on the coding side to some extent, but I'm really struggling to understand what's going on in these files well enough to know what I can eliminate and what/where to add code. My CNC is also running with imperial units, so I'm hoping it's okay to just switch everything over to inches in the C files. Any chance someone could help walk me through what changes I'd need to make?

For reference, putting a tool on with my setup (assuming a spindle with no tool/collet currently on) could be as simple as the code below for my T1 tool bay:

G53 G0 Z0
G53 G0 X1.7333 Y30.1336
G53 G0 Z-3
M3 S1500
G53 G1 Z-4 f60
G53 G1 Z-3 f200
G53 G1 Z-4 f60
G53 G0 Z0
M5
G54
M30

I could adjust the X position for each one of the 16 tool bays and have the exact coordinates for each at the ready. Removing a tool could look pretty similar, with an M4 command instead of M3, slightly higher spindle speed, and probably just one plunge into the tool bay with a 1 second wait after plunging before retracting to machine coordinates Z0 again.

I would then have it do a tool touch off on a tool setter I installed in the center of the tool table (in the pictures). I'm not sure about the best way to integrate the tool setter with the Z zero touch plate I use for initial setup. I would assume I could just establish Z zero on the top surface of my work piece with my touch plate, have a C program make a note of the difference between the machine coordinates Z zero and this touch plate location just established, then later use the tool setter with a known offset from machine coordinate Z zero to adjust when new tools are installed. No clue how to write this code though, so I'm hoping this is how things are setup in the example file and can be adapted for my use case.

I should be able to remove all of the CLAW_EJECT, SPINDLE_CLEAN , CLAW_LOOSE, and TOOL_SENSE commands, but wanted to confirm. I may later try to add a simple infrared LED beam sensor somewhere near my tool setter to mimic the RapidChangeATC's pro/premium lines. The hope would be that I can use it to sense whether the collet nut has been removed/installed successfully before moving to the new bay or next task. I'm not sure if I could use the TOOL_SENSE command for this purpose down the line, but I don't currently have this LED beam sensor setup integrated yet, so it's not a problem I need to solve immediately.

The only other thing I would need to add is a tool tray extend and retract command before and after tool changes. I have the tool table set up on a relay, triggered by IO19, where running the bit high extends the tray, and low pulls it back in. If this is a bit too much to tackle with me under the free help umbrella, I certainly wouldn't mind having to pay a bit towards consulting on this. I'm knee-deep into the project now, so I'm really hoping I can pull it off:).


Image
Image

User avatar
TomKerekes
Posts: 2868
Joined: Mon Dec 04, 2017 1:49 am

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by TomKerekes » Sat Sep 06, 2025 4:56 pm

For reference, putting a tool on with my setup (assuming a spindle with no tool/collet currently on) could be as simple as the code below for my T1 tool bay:

G53 G0 Z0
G53 G0 X1.7333 Y30.1336
G53 G0 Z-3
M3 S1500
G53 G1 Z-4 f60
G53 G1 Z-3 f200
G53 G1 Z-4 f60
G53 G0 Z0
M5
G54
M30
If you are using Linear4ToolHolders Rev2.c it has functions MoveXY and MoveZ. So in C instead of:

Code: Select all

G53 G0 X1.7333 Y30.1336
use

Code: Select all

MoveXY(1.7333, 30.1336, 1.0);  // Move XY at 1 inch per second (60 in/minute)
I'm not sure how your Spindle is controlled but most likely you can command it to rotate at some Speed with the Jog function. eg. Jog(3, Speed). To stop command zero speed.


If you are using inches replace everywhere CNT_PER_MM_X with CNT_PER_INCH_X and change the define to the appropriate value.

In general I'd recommend performing a tool change step by step manually writing down every step. Then you just need to code each step in C.

After you touch off a tool I think you will want to update the Tool length in the Tool Table for that tool. See the example ToolTableSet.c

HTH
Regards,

Tom Kerekes
Dynomotion, Inc.

Tim G
Posts: 38
Joined: Wed Jan 13, 2021 9:51 pm

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by Tim G » Sat Sep 06, 2025 5:06 pm

Thanks! The C code motion examples are a huge help. I'll dig into this and make all of those changes, and will post the code after I finish up.

Tim G
Posts: 38
Joined: Wed Jan 13, 2021 9:51 pm

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by Tim G » Sat Sep 06, 2025 9:23 pm

I was digging through the changes you mentioned and ran into some trouble areas I was hoping to get some help with.

I noticed the AXIS_SAFE_DISTANCE_Y section and I believe I don't need this at all since my setup approaches retrieving/removing collets vertically (no need to have a safe Y distance for moving between tools, just a safe z height).

Question 1: Can I just comment out the AXIS_SAFE_DISTANCE_Y section in my case or is there something I'm missing here?

The SAFE_HEIGHT_Z section mentions a relative measurement, but I don't see a refence point for Z anywhere in the logic that it would be moving this amount away from. I would prefer the Z axis move entirely up for any XY movements during tool changes.

Question 2: Is there a way to set this SAFE_HEIGHT_Z section to an absolute position or at least ensure that the measurement put here moves the Z axis all the way up?

Concerning adapting my Gcode movements to C program logic, I think I got most of it adapted (below), but I ran into a couple of issues noted in the comments next to the commands.

For reference - On KFLOP, Spindle FWD is IO 0, REV is IO 45, PWM speed control is IO 26. I don't believe my spindle is set up as an axis, so I'm not sure how to code for spindle on/off, forward/reverse, and speed at 1500 rpm for FWD and 1600 rpm for REV.

Load Tool T1:

Code: Select all

MoveZ(0, 5) // Move Z at 5 inch per second (300 in/minute)
MoveXY(1.733, 30.1336, 10)  // Move XY at 10 inch per second (600 in/minute)
MoveZ(-3, 5) // Move Z to tool bay at 5 in per second (300 in/minute) - Tool lowers into bay 1 inch above engagement with socket bearings

**Jog(3 , 1500) - / Worried this will jog z axis stepper, not control spindle speed **

MoveZ(-4, 1) // Move Z at 1 inch per second (60 in/min) to engage collet nut
MoveZ(-3, 3) // Move Z  at 3 inch per second (180 in/min) upward by an inch to disengage collet nut from socket
MoveZ(-4, 1) // Move Z at 1 inch per second (60 in/min) to engage/tighten collet nut
MoveZ(0, 5) // Move Z at 5 inch per second (300 In/min) to machine coordinates Z zero (fully up and clear of tool bays)
Unload Tool T1:

Code: Select all

MoveZ(0, 5) // Move Z at 5 inch per second (300 in/minute)
MoveXY(1.733, 30.1336, 10)  // Move XY at 10 inch per second (600 in/minute)
MoveZ(-3, 5) // Move Z to tool bay at 5 in per second (300 in/minute) - Tool lowers into bay 1 inch above engagement with socket bearings

**Jog(3 , 1600) - / Same concerns about Z axis movement instead of spindle control - needs to spin in REV at slightly higher RPM to gain additional torque force for removal of collet nut**

MoveZ(-4, 1) // Move Z at 1 inch per second (60 in/min) to engage collet nut

**sleep(1000) / Is this the correct way to create a 1 second delay before continuing to execute code? I need the spindle to dwell in this position for 1 second to allow the collet nut to spin off before moving upward again. **

MoveZ(-3, 1) // Move Z  at 1 inch per second (180 in/min) upward by an inch to disengage collet nut from socket
MoveZ(0, 5) // Move Z at 5 inch per second (300 In/min) to machine coordinates Z zero (fully up and clear of tool bays)
Question 3: How would I either define the Spindle as an axis to use the job command or activate pins to engage forward or reverse at a specific rpm?

I'm also not sure where I would actually insert this movement C code into the file or if any of the code already there needs to be altered/removed. It seems like there's a lot of other error-checking logic that I don't understand well enough to know what needs to be done there.

Question 4: Would you mind helping me identify the code I need to keep/remove in the Load and Unload tool sections as well as where to insert the new movement code? Also, do I need to modify anything in the other sections? For example, DoToolChange, EjectTool (comment out?), GetCurrentTool, etc. It didn't seem like it, but I figured I'd better confirm that with you.

User avatar
TomKerekes
Posts: 2868
Joined: Mon Dec 04, 2017 1:49 am

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by TomKerekes » Sun Sep 07, 2025 4:24 pm

Question 1: Can I just comment out the AXIS_SAFE_DISTANCE_Y section in my case or is there something I'm missing here?
It seems you will load the tool approaching in Z instead of Y so this shouldn't be needed
The SAFE_HEIGHT_Z section mentions a relative measurement, but I don't see a refence point for Z anywhere in the logic that it would be moving this amount away from. I would prefer the Z axis move entirely up for any XY movements during tool changes.
MoveZ functions make absolute moves so you can move to the entirely up position if you wish. He calls it relative because he uses it as HOLDER_Z+SAFE_HEIGHT_Z. So it is relative to his Holder_Z height.
Question 2: Is there a way to set this SAFE_HEIGHT_Z section to an absolute position or at least ensure that the measurement put here moves the Z axis all the way up?
Yes define such a height and move there.
For reference - On KFLOP, Spindle FWD is IO 0, REV is IO 45, PWM speed control is IO 26. I don't believe my spindle is set up as an axis, so I'm not sure how to code for spindle on/off, forward/reverse, and speed at 1500 rpm for FWD and 1600 rpm for REV.
I don't see where you told us how your Spindle functions. But the methods already used to control the Spindle would be used. See how your S, M3, M4, M5 Actions are configured in KMotionCNC. It might involve writing to a PWM instead of Jogging an axis
**sleep(1000) / Is this the correct way to create a 1 second delay before continuing to execute code? I need the spindle to dwell in this position for 1 second to allow the collet nut to spin off before moving upward again. **
No. Use Delay_sec(1.0);
Question 3: How would I either define the Spindle as an axis to use the job command or activate pins to engage forward or reverse at a specific rpm?
KFLOP PWMs are not directly supported as an Axis Output Mode. So a C Program running in a forever loop in your Initialization Program is needed to write the Axis's Servo Output to the PWM. This is basically 1 line of code possibly like:

Code: Select all

for (;;)  // Loop forever
{
    FPGA(IO_PWMS)  = ch4->Output ; 	// set the Axis's output to PWM
}
You might read this and in particular this. Note a DAC (Voltage) controlled spindle works in a very similar way to a PWM controlled spindle except a DAC output is handled by the axis itself. In the case of a PWM set the output mode to "No Output" and have your C Code write the output to the PWM.
I'm also not sure where I would actually insert this movement C code into the file or if any of the code already there needs to be altered/removed. It seems like there's a lot of other error-checking logic that I don't understand well enough to know what needs to be done there.
The code sequence that loads and unloads a tool would be placed into the LoadTool and UnloadTool functions. I'd suggest removing error-checking logic initially until you get things to work, then add in whatever checks you feel are necessary for your type of system

Question 4: Would you mind helping me identify the code I need to keep/remove in the Load and Unload tool sections as well as where to insert the new movement code? Also, do I need to modify anything in the other sections? For example, DoToolChange, EjectTool (comment out?), GetCurrentTool, etc. It didn't seem like it, but I figured I'd better confirm that with you.
Well basically I think you need to completely repace the code in LoadTool and Unload Tool as your system operates differently. The EjectTool is really part of his UnloadTool. That's the only place where it is called. If your UnloadTool sequence completely removes the Tool then it wouldn't be needed.

HTH
Regards,

Tom Kerekes
Dynomotion, Inc.

Tim G
Posts: 38
Joined: Wed Jan 13, 2021 9:51 pm

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by Tim G » Mon Sep 08, 2025 7:27 pm

This is a huge help. Thanks for your patience with me on this! I attempted to set up the spindle speed output as a DAC, flashing ch. 7 with the mentioned settings, redirecting the M3, M4, M5, and S commands to the CSS jog versions of the files. I didn't attempt the actual CSS file include because I got a bit confused there. I don't have an encoder on my spindle, so it didn't seem like that made sense for my setup. Long story short, I couldn't get that functioning at all, unfortunately. I did, however, review the C program files I'm currently using to control those commands and was able to hobble together some logic to control the spindle that seems to be working. I basically copied from the S file used for speed control and changed the line where it looks for an S gcode command for speed and just gave it a specific speed instead. Let me know if you think there are some issues with the way I've done it though (shown in UnloadTool and LoadNewTool code sections below).

I gave the file a try, referencing it in KmotionCNC for the M6 command, and it does attempt to do a tool change, but the motion is wrong. For reference, my router has channels 0 and 1 slaved for the Y axis, channel 2 as the X axis, and channel 3 as the Z axis. In the C file, it defines each axis (I don't really understand why since this is handled in the Init file already). I only put channel 0 for Y, not knowing if/how I could specify two axes that are slaved.

At first, I had the T1 tool bay position permanently referenced in the LoadNewTool and UnloadTool sections for testing. When given an M6 command the router would move Z up to machine coordinate 0 (as requested), then move to the given X position, but wouldn't move at all in the Y direction. Unfortunately, it would also slowly move the Z axis up any time it moved in X. This would push Z up and trigger the limit switch. If it was already close enough to the x position for the bay, it wouldn't have time to trigger the Z limit switch and could continue with the code. The rest of the Z movements seemed accurate and the spindle turned on/off in the appropriate direction/rpms when called to. I can't figure out how my code could cause the random upward Z movement during the X movement though, nor why the Y axis movement didn't occur at all.

Question 1: Do you know what could cause the Z axis to drift up during X moves?

Question 2: Do you think I need to somehow indicate in the #define AXISY section that it's a channel 0 & 1 slaved axis setup for Y instead of "#define AXISY 0" in order to get the Y axis working during M6 commands?

Next, I tried switching out the T1 tool bay position for the generic tool bay code originally in the file. This code, I assumed, would reference whatever tool was called for with the M6 command (for example; T2 M6). It seems to now be partially working for X movements, but no movement at all for Y, and the Z axis still creeps upward during X moves. I say partially for X, because movements to HOLDER_X positions 1 through 4 are accurate, however, after that point, all X movements are off, but go the same incorrect positions each time. For example, when I try slot 7, it goes to 15.2333 every time, but the value I have for HOLDER_X_7 is 16.5893. This continues for slots 5 through 16, with some being off as far as almost 10 inches (Slot 9 goes to 19.7333, but HOLDER_X_9 is set to 28.2833).

UPDATE: There was a small bit of code underneath the Eject Tool section that I had commented out (shown below question 3). Before enabling this code, the ToolPositionX commands did not work and the file could not compile and run (received an error in KmotionCNC when an M6 command was given). Enabling this code allowed the ToolPositionX lines to stop throwing errors and got the M6 commands working. However, looking closer at the code, it's designed to calculate the spacing between tool slots and seems to be overriding my HOLDER_X values after bay 4. I believe this is due to the uneven spacing of my tool changer design. The first 4 bays have even spacing, then the gap is larger between bays 4 and 5, 8 and 9, as well as 12 and 13. This is because I have 4 cassettes, each with 4 bays. I arranged them symmetrically around the tool setter, so this creates gaps between the cassettes that aren't consistent with the bay spacing within each cassette (The pictures in my first post are worth 1000 words here).

Question 3: Do you know how I could alter the code so that the ToolPositionX command just references the HOLDER_X_% value for the slot needed directly instead of trying to calculate for a consistent slot spacing?

Code: Select all

//return x position of tool holder as a function of the tool
float ToolPositionX(int tool)
{
	return (HOLDER_X_2-HOLDER_X_1)*(tool-1) + HOLDER_X_1;
}
Lines 36-38 (below) reference TOOL_CHANGE_SAFE_POS_X and Y absolute positions that are "safe to move down in Z." I think this could be another section related to moving the spindle a bit in front of the tool table in Y that I don't need for my set up. It would make more sense for me to simply have a safe Z height, where X and Y travel cannot occur before it reaches that height.

Question 4: Can you see any issue with just commenting out or deleting this code below concerning tool change safe positions for XY?

Code: Select all

// absolute position to move to that is permanently unobstructed, and safe to move down in Z
#define TOOL_CHANGE_SAFE_POS_X 23.5613 
#define TOOL_CHANGE_SAFE_POS_Y 30.1336
One other issue I noticed is an error for "invalid tool number" that happens when I request a tool number higher than T4. I have 16 bays on my tool changer, so I added a bunch in the code below.

UPDATE: I think I figured out the tool number issue. Under the BOOL ToolNumberValid(int tool) section in the code, I noticed it specified 4 or less tools as valid, so I'd assume that was what caused the problem. I adjusted that to 16. I also configured the Tool section in KmotionCNC to have 16 Slots setup, and this seems to allow me to pick any slot needed, though slots 5-16 do not go to the correct X location so far.

Thanks again for all of the help so far!

Here's my entire file so far:

Code: Select all

#include "KMotionDef.h"
#define TMP 10 // which spare persist to use to transfer data
#include "C:\Users\NUA_TIM\Desktop\KMotion5.4.0\C Programs\KflopToKMotionCNCFunctions.c"
#include "C:\Users\NUA_TIM\Desktop\Machine Programs\CorrectAnalogFunction.c"
#define RPM_FACTOR 22200.0 // RPM for full duty cycle (max analog out)

//-----------------------------------------
//		LINEAR TOOL CHANGING
//-----------------------------------------
#define AXISX 2
#define AXISY 0
#define AXISZ 3

//---------Absolute position of tool holders
#define HOLDER_X_1 1.7333
#define HOLDER_X_2 3.9833
#define HOLDER_X_3 6.2333
#define HOLDER_X_4 8.4833
#define HOLDER_X_5 12.0893
#define HOLDER_X_6 14.3393
#define HOLDER_X_7 16.5893
#define HOLDER_X_8 18.8393
#define HOLDER_X_9 28.2833
#define HOLDER_X_10 30.5333
#define HOLDER_X_11 32.7833
#define HOLDER_X_12 35.0333
#define HOLDER_X_13 38.6393
#define HOLDER_X_14 40.8893
#define HOLDER_X_15 43.1393
#define HOLDER_X_16 45.3893
#define HOLDER_Y 30.1336
#define HOLDER_Z -1

// absolute position of the tool height setting plate
#define TOOL_HEIGHT_PLATE_X 23.5613 
#define TOOL_HEIGHT_PLATE_Y 30.1336

// absolute position to move to that is permanently unobstructed, and safe to move down in Z
//#define TOOL_CHANGE_SAFE_POS_X 23.5613 
//#define TOOL_CHANGE_SAFE_POS_Y 30.1336


#define TOOL_VAR 9        	// Tool changer desired new tool Var

// Tool changer Last tool loaded is saved globally in this Var
#define LAST_TOOL_VAR 8   	//  -1=Spindle empty, 0=unknown, 1-4 Tool Slot loaded into Spindle
#define TOOL_DISK_FILE "c:\\Temp\\ToolChangerData.txt"


//#define CLAMP_TIME 1.0    	// seconds to wait for the clamp/unclamp
#define TOOL_HEIGHT_BIT	29	//bit to read tool height plate (KONNECT INPUT 31)

#define SAFE_HEIGHT_Z -1   // relative distance in inch to move to clear the top of the tool taper
#define TOOL_RETRACT_SPEED_Z 5	//speed in inch/second to move spindle up after tool has been ejected

#define SlowSpeed 0.4 //inch/sec

#define CNT_PER_INCH_X 3175 
#define CNT_PER_INCH_Y 3180.3 
#define CNT_PER_INCH_Z 10000 

// function prototypes
int DoToolChange(int ToolSlot);
int GetCurrentTool(int *tool);
int SaveCurrentTool(int tool);
BOOL ToolNumberValid(int tool);
float ToolPositionX(int tool);
int MoveXY(float x, float y, float Speed);
int MoveZ(float z, float Speed);
int UnloadTool(int CurrentTool);
int LoadNewTool(int Tool);
int EjectTool(void);



main()
{
	int ToolSlot = persist.UserData[TOOL_VAR];  // Requested tool to load (value stored an integer) 

	if (DoToolChange(ToolSlot))  // perform Tool Change
	{
		// error, Halt Job
		DoPC(PC_COMM_HALT);
	}
}

// Perform Tool Change.  Return 0=Success, 1=Failure
int DoToolChange(int ToolSlot)
{
	int CurrentTool;

	if (GetCurrentTool(&CurrentTool)) return 1;  //  -1=Spindle empty, 0=unknown, 1-4 Tool Slot loaded into Spindle

	printf("Load Tool Slot %d requested, Current Tool %d\n",ToolSlot, CurrentTool);
	
	if (!ToolNumberValid(ToolSlot))  // check if invalid
	{
		char s[80];
		sprintf(s,"Invalid Tool Change Number %d\n",ToolSlot);
		printf(s);
		MsgBox(s, MB_ICONHAND | MB_OK);
		return 1;
	}
	
	if (CurrentTool!=-1) // is there a tool in the Spindle??
		if (UnloadTool(CurrentTool)) return 1;  // yes, unload it
		
	// Now Spindle is empty, load requested tool
	if (LoadNewTool(ToolSlot)) return 1;
	
	SaveCurrentTool(ToolSlot);  // save the one that has been loaded
	return 0;  // success
}


// - Load new Tool (Spindle must be empty)
int LoadNewTool(int Tool)
{
	//I picked a random very low Z point for testing (-5) to give some room for that upward Z drift issue to occur but not trip the limit switch so I 							
	//could test the other actions. I also lowered the speeds to make it safer while troubleshooting. 
	MoveZ(-5, 2); // Move Z at 5 inch per second (300 in/minute) 
    	MoveXY(ToolPositionX(Tool),HOLDER_Y, 3);  // Move XY at 10 inch per second (600 in/minute)
    	MoveZ(-3, 2); // Move Z to tool bay at 5 in per second (300 in/minute) - Tool lowers into bay 1 inch above engagement with socket bearings
    	float speed = 1500.0; // Hardcoded speed value in RPM
    	SetBitDirection(26,1); // define bit as an output
    	FPGA(IO_PWMS_PRESCALE) = 46; // divide clock by 46 (1.4 KHz)
    	FPGA(IO_PWMS+1) = 1; // Enable
    	FPGA(IO_PWMS) = CorrectAnalog(speed/RPM_FACTOR); // Set PWM
    	ClearBit(0);
    	MoveZ(-4, 1); // Move Z at 1 inch per second (60 in/min) to engage collet nut
    	MoveZ(-3, 3); // Move Z  at 3 inch per second (180 in/min) upward by an inch to disengage collet nut from socket
    	MoveZ(-4, 1); // Move Z at 1 inch per second (60 in/min) to engage/tighten collet nut
    	MoveZ(-3, 3); // Move Z up 1 inch to disengage collet nut from socket
    	SetBit(0);
    	MoveZ(0, 2); // Move Z at 5 inch per second (300 In/min) to machine coordinates Z zero (fully up and clear of tool bays)
    
    // - Move to position of requested tool
	// - Rapid move to absolute position of new tool only in X and Y
	//if (MoveXY(ToolPositionX(Tool),HOLDER_Y,SlowSpeed)) return 1;

	// - Move to tool Z position at TOOL_RETRACT_SPEED_Z
	//if (MoveZ(HOLDER_Z,SlowSpeed)) return 1;

	// - Engage new tool
	// - CLAW_EJECT and SPINDLE_CLEAN bits are currently high from tool removal operation
	// - Turn off CLAW_EJECT and SPINDLE_CLEAN bits to engage tool
	//ClearBit(CLAW_EJECT);
	//ClearBit(SPINDLE_CLEAN);

	// - Wait for time in seconds defined by CLAMP_TIME
	//Delay_sec(CLAMP_TIME);

	// - Check to see if CLAW_LOOSE and TOOL_SENSE are high; if either are not, 
	//		something has gone wrong; halt everything and display message indicating failure
	// - Tool has been engaged
	//if (!ReadBit(CLAW_LOOSE))
	//{
	//	printf("Claw Still Loose Error\n");
	//	MsgBox("Claw Still Loose Error\n", MB_ICONHAND | MB_OK);
	//	return 1;
	//}
	//if (!ReadBit(TOOL_SENSE))
	//{
	//	printf("Tool Sense Error\n");
	//	MsgBox("Tool Sense Error\n", MB_ICONHAND | MB_OK);
	//	return 1;
	//}

	// - Leave tool holder by moving Y axis by the negative value of Y_AXIS_SAFE_DISTANCE
	// - Move to position of requested tool
	// - Rapid move to absolute position of new tool only in X and Y
	//if (MoveXY(ToolPositionX(Tool),HOLDER_Y+AXIS_SAFE_DISTANCE_Y,SlowSpeed)) return 1;

	// - Rapid to Z home
	//if (MoveZ(0.0,SlowSpeed)) return 1;

	//return 0; //success
}


// - Remove tool in spindle by going to holder of current tool
int UnloadTool(int CurrentTool)
{
	MoveZ(-5, 2); // Move Z at 5 inch per second (300 in/minute)
    	MoveXY(ToolPositionX(CurrentTool),HOLDER_Y,3);  // Move XY at 10 inch per second (600 in/minute)
    	MoveZ(-3, 2); // Move Z to tool bay at 5 in per second (300 in/minute) - Tool lowers into bay 1 inch above engagement with socket bearings
    	float speed = 1600.0; // Hardcoded speed value in RPM
    	SetBitDirection(26,1); // define bit as an output
    	FPGA(IO_PWMS_PRESCALE) = 46; // divide clock by 46 (1.4 KHz)
    	FPGA(IO_PWMS+1) = 1; // Enable
    	FPGA(IO_PWMS) = CorrectAnalog(speed/RPM_FACTOR); // Set PWM
    	SetBit(45);
    	MoveZ(-4, 1); // Move Z at 1 inch per second (60 in/min) to engage collet nut
    	MoveZ(-3, 1); // Move Z  at 1 inch per second (180 in/min) upward by an inch to disengage collet nut from socket
    	ClearBit(45);
    	MoveZ(0, 2); // Move Z at 5 inch per second (300 In/min) to machine coordinates Z zero (fully up and clear of tool bays)
    
    // - Rapid to Z Home to clear any work that may be on the table
	//if (MoveZ(0.0,SlowSpeed)) return 1;

	// - Rapid to TOOL_CHANGE_SAFE_POS to execute a safe negative Z move
	//if (MoveXY(TOOL_CHANGE_SAFE_POS_X,TOOL_CHANGE_SAFE_POS_Y,SlowSpeed)) return 1;
	
	// - Approach tool holder by matching Z height of tool flange currently in spindle with tool holder                            claw
	//if (MoveZ(HOLDER_Z,SlowSpeed)) return 1;

	// - After matching height above, approach tool holder by moving to holder X position
	//if (MoveXY(ToolPositionX(CurrentTool),TOOL_CHANGE_SAFE_POS_Y,SlowSpeed)) return 1;

	// - After matching X position, match tool Y position
	//if (MoveXY(ToolPositionX(CurrentTool),HOLDER_Y,SlowSpeed)) return 1;

	// - Move only in Y position until current position matches tool holder position (maybe disable X)                          axis?)
	// ???
	
	// - Eject tool
	//if (EjectTool()) return 1;


	//return 0; //success
}


// - Eject tool
//int EjectTool(void)
//{ 
	// - Turn on CLAW_EJECT bit to remove tool from spindle
	//SetBit(CLAW_EJECT);

	// - Turn on SPINDLE_CLEAN bit to remove any debris from taper and tools
	//SetBit(SPINDLE_CLEAN);

	// - Wait for time in seconds defined by CLAMP_TIME
	//Delay_sec(CLAMP_TIME);
	
	// - Read CLAW_LOOSE bit to see whether the tool is loose, to make a safe Z move without  
    	//      destroying tool holder
	// - If CLAW_LOOSE bit is high, something has gone wrong;
	//		halt everything and display message indicating failure
	//if (ReadBit(CLAW_LOOSE))
	//{
	//	printf("Claw Loose Error\n");
	//	MsgBox("Claw Loose Error\n", MB_ICONHAND | MB_OK);
	//	return 1;
	//}

	// - Move Z axis up at speed defined by 'Z_TOOL_RETRACT_SPEED', to Z_SAFE_HEIGHT
	//if (MoveZ(HOLDER_Z+SAFE_HEIGHT_Z,TOOL_RETRACT_SPEED_Z)) return 1;

	// - Read TOOL_SENSE bit to see whether the tool has been successfully ejected from the spindle
	// - If TOOL_SENSE bit is high, something has gone wrong; 
	//		halt everything and display message indicating failure
	//if (ReadBit(TOOL_SENSE))
	//{
	//	printf("Tool Sense Release Error\n");
	//	MsgBox("Tool Sense Release Error\n", MB_ICONHAND | MB_OK);
	//	return 1;
	//}
	//return 0; // success
//}



//return x position of tool holder as a function of the tool
float ToolPositionX(int tool)
{
	return (HOLDER_X_2-HOLDER_X_1)*(tool-1) + HOLDER_X_1;
}



// Get the last loaded tool.  Parameter points to where to return tool
// First try to get from KFLOP memory
// if memory is invalid, try to read from disk
// if can't read disk then ask Operator
// returns 0 on success, 1 on fail or Operator asked to abort

int GetCurrentTool(int *ptool)
{
	int success,Answer,result,tool;
	float value;

	tool = persist.UserData[LAST_TOOL_VAR];
	success = ToolNumberValid(tool);  // check if valid

	if (!success)   // invalid after power up, try to read from PC Disk File
	{
		// Try to open file
		FILE *f=fopen(TOOL_DISK_FILE,"rt");
		if (f)  // did file open?
		{
			// read a line and convert it
			result=fscanf(f,"%d",&tool);
			fclose(f);
			
			if (result==1 && ToolNumberValid(tool))
			{
				printf("Read Disk File Value of %d\n",tool);
				success=TRUE; // success if one value converted
			}
		}
		
		if (!success) printf("Unable to open/read file:%s\n",TOOL_DISK_FILE);  
	}

	if (!success)   // if still no success ask Operator
	{
		Answer = InputBox("Tool in Spindle or -1",&value);
		if (Answer)
		{
			printf("Operator Canceled\n");
			return 1;
		}
		else
		{
			tool=value;
			printf("Operator Entered Value of %d\n",tool);
		}
	}

	if (!ToolNumberValid(tool))  // check if invalid
	{
		char s[80];
		sprintf(s,"Invalid Current Tool Number %d\n",tool);
		printf(s);
		MsgBox(s, MB_ICONHAND | MB_OK);
		return 1;
	}
	
	printf("Current tool = %d\n",tool);
	*ptool = tool;  // return result to caller
	return 0;  //success
}

// save the tool number to KFLOP global Variable and to PC Disk file in case we loose power
int SaveCurrentTool(int tool)
{
	persist.UserData[LAST_TOOL_VAR]=tool;
	FILE *f=fopen(TOOL_DISK_FILE,"wt");
	fprintf(f,"%d\n",tool);
	fclose(f);
	return 0;
}

// check if Current Tool number Valid
// -1 = no tool loaded
// 1-16 = valid tool
BOOL ToolNumberValid(int tool)
{
	return tool == -1 || (tool>=1 && tool<=16);
}


// Move Axis XY at specified Speed and wait until complete
// return 0 = success, 1 if axis disabled
int MoveXY(float x, float y, float Speed)
{
	MoveAtVel(AXISX, x * CNT_PER_INCH_X, Speed * CNT_PER_INCH_X);
	MoveAtVel(AXISZ, y * CNT_PER_INCH_Y, Speed * CNT_PER_INCH_Y);
	
	while (!CheckDone(AXISX) || !CheckDone(AXISY))
	{
		if (!chan[AXISX].Enable)
		{
			printf("Error X Axis Disabled\n");
			MsgBox("Error X Axis Disabled\n", MB_ICONHAND | MB_OK);
			return 1;
		}
		if (!chan[AXISY].Enable)
		{
			printf("Error Y Axis Disabled\n");
			MsgBox("Error Y Axis Disabled\n", MB_ICONHAND | MB_OK);
			return 1;
		}
	}
	return 0;  //success
}

// Move Axis Z at specified Speed and wait until complete
// return 0 = success, 1 if axis disabled
int MoveZ(float z, float Speed)
{
	MoveAtVel(AXISZ, z * CNT_PER_INCH_Z, Speed * CNT_PER_INCH_Z);
	
	while (!CheckDone(AXISZ))
	{
		if (!chan[AXISZ].Enable)
		{
			printf("Error Z Axis Disabled\n");
			MsgBox("Error Z Axis Disabled\n", MB_ICONHAND | MB_OK);
			return 1;
		}
	}
	return 0;  //success
}

User avatar
TomKerekes
Posts: 2868
Joined: Mon Dec 04, 2017 1:49 am

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by TomKerekes » Tue Sep 09, 2025 3:46 pm

Hi Tim,
Question 1: Do you know what could cause the Z axis to drift up during X moves?
I see a

MoveAtVel(AXISZ...

in the MoveXY function

Question 2: Do you think I need to somehow indicate in the #define AXISY section that it's a channel 0 & 1 slaved axis setup for Y instead of "#define AXISY 0" in order to get the Y axis working during M6 commands?
No if you command the Master axis to move the Slave axis will automatically move with it.

Question 3: Do you know how I could alter the code so that the ToolPositionX command just references the HOLDER_X_% value for the slot needed directly instead of trying to calculate for a consistent slot spacing?
Yes he used a trick as the spacings were all equal. In you case you might specify them all explicitly.

Code: Select all

//return x position of tool holder as a function of the tool
float ToolPositionX(int tool)
{
	if (tool == 1) return HOLDER_X_1;
	if (tool == 2) return HOLDER_X_2;
	if (tool == 3) return HOLDER_X_3;
	.
	.
	.
	if (tool == 16) return HOLDER_X_16;
	return 0;  // should never happen
}
Question 4: Can you see any issue with just commenting out or deleting this code below concerning tool change safe positions for XY?
No
Regards,

Tom Kerekes
Dynomotion, Inc.

Tim G
Posts: 38
Joined: Wed Jan 13, 2021 9:51 pm

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by Tim G » Tue Sep 09, 2025 7:37 pm

Thanks a ton! That fixed the Z drift and the X position errors. I'm successfully changing tools now:). I noticed every now and then, after a tool change, the Z axis will be at 0 (fully up position) and it's as if the Z safe height halt is triggered, because the Z will suddenly try to raise up and then it triggers the limit switch and stops all motion. This has been an intermittent problem I've had over the years that only seemed to occur when I would zero out the X,Y, or Z axis. It never caused much of a problem because it would just raise up an inch or two after zeroing, but not lose position. Now, however, it will halt the tool change procedure when this occurs due to triggering the limit switch.

I added the tool table code to push out the tool table before changes and pull it back after a change. After adding this code, now this Z safe height issue happens every time it finishes the tool change, right when the tool table is pulled back in.

I'm not positive it is the Safe Z Height program that's being triggered, but it seems like the most likely option to me so far. I've put the c file for that below for reference:

Code: Select all

#include "KMotionDef.h"

#define SPINDLE_BIT 0
#define ZAXIS 3
//#define SAFE_Z_HEIGHT 10000

main()
{
	//if (ReadBit(0)) // only move up if Spindle was on
	{
		MoveRel(ZAXIS,18000);	
		Delay_sec(1);
		ClearBit(SPINDLE_BIT);   //spindle off
		SetBit(0);
		//Move(ZAXIS,SAFE_Z_HEIGHT);
		while (!CheckDone(ZAXIS));
		printf("SafeZ\n");
	}
}
I have the pneumatic tool table running off of an opto-isolated relay that triggers a pneumatic solenoid valve. To control, I simply set the bit high to extend the table and set the bit low to pull it back in (just added SetBit(19) before the unload and load procedures, and added ClearBit(19) after the the load procedure). I'm not sure if it would be best to try to figure out what's triggering the Safe Z Height or write some logic that temporarily turns off the Safe Z on halt function during tool changes.

Question 1: What do you think the best course of action would be to address the Safe Z Height issue?

I'm also hoping to use a tool setter I purchased...
(Product: https://www.amazon.com/dp/B0DGKZCVVL?re ... tle_1&th=1)

It has two switches inside. One is for the tool setter function, the other is an overtravel safety feature that's meant to trigger an E-stop if the bit pushes the tool setter down further than the tool setter trigger. I have the tool setter switch circuit on IO29 and the e-stop switch circuit on IO30. I was hoping I could set up the tool setter switch circuit to work with my Z height touch-off block. My idea was to use the Z height touch-off block to establish job Z zero, and then use the tool setter to store the offset between the first tool tip and the job Z zero. This value could then be used as the reference point when a new tool is selected. It could compare the new tool's offset to the first tool's offset and adjust as necessary.

Question 2: Any chance you could help me with the code to accomplish this with the tool setter? Also, if there's a better way to do this, I'm certainly willing to modify the approach.

Here's the current code used for my Z touch off block in case that's needed:

Code: Select all

#include "KMotionDef.h"
#define TMP 10 
#include "KflopToKMotionCNCFunctions.c"
main()
{
    
   
    int SaveX0Limits,SaveX1Limits,SaveYLimits,SaveZLimits;  //place to save limit switch settings
    FPGA(STEP_PULSE_LENGTH_ADD) = 63 + 0x80;
  //home Z
  //EnableAxis(3); 
   ch3->LimitSwitchOptions=0;
    Jog(3,-3000);              // jog slowly negative
    while (ReadBit(7));      // loop until IO bit goes low
    Jog(3,0);                // stop
    while (!CheckDone(3)); // loop until motion completes 
    //DisableAxis(3);
     //Zero(3); 
    //EnableAxis(3);  
    Delay_sec(2);          // re-enable the ServoTick
    MoveRelAtVel(3,4840,12000);    // move some amount inside the limits
    while (!CheckDone(3)) ; // loop until motion completes 
    //Zero(3);
    DoPCFloat(PC_COMM_SET_Z,1.0);
       
     
    ch3->LimitSwitchOptions = 0x11a; // restore limit settings
    

               
 
 }

User avatar
TomKerekes
Posts: 2868
Joined: Mon Dec 04, 2017 1:49 am

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by TomKerekes » Tue Sep 09, 2025 8:21 pm

Question 1: What do you think the best course of action would be to address the Safe Z Height issue?
It is making a relative move up 18000 counts from where ever it happens to be. I think it would be better to move to an absolute height. Probably 0. Change MoveRel() to Move()

Question 2: Any chance you could help me with the code to accomplish this for this switch? Also, if there's a better way to accomplish this, I'm certainly willing to modify the approach.
I don't really follow that. I think there are several different approaches. When you touch off a tool you probably want to update its length in the tool table. Can your system remove a tool and replace it back without any change?

You might read this Thread

That same program should work for the probe by just changing to the different input. But what you do/update next will change. You might also see the ToolTableSet.c example.
Regards,

Tom Kerekes
Dynomotion, Inc.

Tim G
Posts: 38
Joined: Wed Jan 13, 2021 9:51 pm

Re: DIY Collet-Spinning ATC - Adapting Linear ATC C Programs?

Post by Tim G » Tue Sep 09, 2025 9:35 pm

Perfect, that tip about the absolute position for the Safe Z Halt worked, thanks a ton! In looking over the thread you linked, I'm a bit concerned this sort of setup will only work with collet types that lock the bits firmly in position when they're off the spindle (iso30 style). Sadly, with standard ER20 collets and collet nuts, there's going to be some tool slippage, so the tool length will vary a bit each time it's loaded. I plan on 3D printing some TPU collet flanges that will help put some tension on the tools and limit slippage, but I think it will still be a slight problem.

As I've been reading/watching videos a bit more about similar setups, I've found that the Onefinity CNCs have a tool setter option that functions exactly as I have been attempting to describe. They use an XYZ touch probe with the first tool to establish Z-zero on the workpiece surface, then they touch off on their "Easy-Z Tool Setter" to establish a Z offset between those two points. After a tool change, the new tool automatically touches off on the tool setter, and code compares this new offset with the first tool's offset, then adjusts the Z-offset to match so that it's zeroed to the work surface. (Video for reference: https://www.youtube.com/watch?v=4yHbQxBy7FA)

As far as the coding goes, assuming I zero to the workpiece surface before starting a job, I could either set up the post processor to start all gcode with a tool setter touch off, or I could set up my Z-touchplate routine to complete a Tool Setter touch off as well automatically after it zeroes the Z-touchplate to the work surface (I believe this is how Onefinity does it). In either situation, it seems like we could create C logic to calculate the distance between those two points (Z-touchplate zero vs. Tool Setter trigger point) and store the value as a Z offset that could be called/applied to the Z job coordinate anytime the tool setter is triggered.

Question 1:Does that seem like a solid method for my use case, or is there a better way I could handle this that accounts for unpredictable tool slippage?

I checked out that ToolTableSet.c file. It's a little hard to follow for me, but it seems like the core calculation, NewToolLength = RoundToReasonable(Machinez - OriginOffsetZ - AxisOffsetZ,Units), might be doing this with the Machinez - OriginOffsetZ part, though I'm not entirely sure. I don't really understand what the AxisOffsetZ is referring to. I'm pretty confused about what I would do with this file as well. I'm not sure if I would simply be copying sections to my tool change file or referencing it as an include.

Questions 2: I think I'm a bit out of my depth trying to figure out how to modify or use ToolTableSet.c with my setup. Any chance I could get some help with the code for the tool setter to function like the Onefinity setup?

Here's what I've coded so far concerning the tool setter. It's basically just combining some movements to locate the spindle in the tool setter probing position, push out the tool table (has the tool setter on it), and probe in the same manner my z-touchplate does (only using pin 19 now since I hooked up the tool setter circuit to a different pin from the touchplate):

Code: Select all

#include "KMotionDef.h"
#define TMP 10 
#include "KflopToKMotionCNCFunctions.c"
main()
{
    
	Move (3, 0); // Move Z to machine 0
    	while (!CheckDone(3)); // Check that Z move is finished before moving on
    	SetBit(19); // Move tool table out
	Move (0, 95834); // Move Y axis to park/tool setter position
	Move (2, 74707.75); // Move Y axis to park/tool setter position
    	while (!CheckDone(2)); // Check that X move is finished before moving on
    	while (!CheckDone(0)); // Check that Y move is finished before moving on
    	int SaveX0Limits,SaveX1Limits,SaveYLimits,SaveZLimits; // Save limit switch settings
    	FPGA(STEP_PULSE_LENGTH_ADD) = 63 + 0x80;
    	ch3->LimitSwitchOptions=0;
    	Jog(3,-3000); // Jog Z slowly negative
    	while (ReadBit(19)); // Loop until IO bit goes low (normally closed switch)
    	Jog(3,0); // Stop
    	while (!CheckDone(3)); // Loop until motion completes   
    	Delay_sec(2); // Re-enable the ServoTick (not sure why it says this for a delay command)
    	MoveRelAtVel(3,4840,12000); // Move some amount inside the limits (1 inch above work surface)
    	while (!CheckDone(3)) ; // Loop until motion completes 
    	DoPCFloat(PC_COMM_SET_Z,1.0); // Set Z axis to 1 inch
       
     
    ch3->LimitSwitchOptions = 0x11a; // Restore limit settings
       
 }
 
I'm guessing I could add the ToolTableSet.c code to my file, but I don't think I understand all of the special functions well enough to know what I would copy over or how I would modify it to do what do what the Onefinity setup does.

Another thing I didn't realize was happening at first is that no matter which tool number I pick up, when I go to switch to another tool, It always attempts unloading the first tool to Slot 1. It sounds like something isn't working correctly with the LAST_TOOL_VAR / TOOL_DISK_FILE setup it's using to store the tool number. Ichecked in the temporary file generated by the TOOL_DISK_FILE logic and it always shows 1 recorded. Here's the code that references it, followed by the code sections where it reads and writes the tool slots (see update).

Question 2: Any idea what changes I need to make to ensure it stores the last tool loaded and returns each tool to the correct slot?

UPDATE: I'm pretty mind-blown that this worked so well, but I figured I'd dig into this issue with Grok AI and managed to solve it. Grok suggested a number of code edits to add quite a few print result section so I could see if the tools were getting stored to the VAR and written to the file. It walked me through some permission checks and suggested moving the file to a new location and updating the file pathway to ensure permissions won't be an issue in the future. In the end, it seems to have been a stuck VAR holding things up. I ended up with a partially rewritten file with a lot of helpful debugging console messages and, most importantly, it works now! It's returning tools to the correct bays, even after turning the machine off and exiting KmotionCNC, starting a new session later. Progress:).

Post Reply