My homemade gear hobber

Home Model Engine Machinist Forum

Help Support Home Model Engine Machinist Forum:

This site may earn a commission from merchant affiliate links, including eBay, Amazon, and others.

Ross Donelly

Member
Joined
Jul 9, 2018
Messages
11
Reaction score
32
Location
california
I thought some of you may be interested in this. I modified my milling machine to do gear hobbing. The goal is to turn the gear blank by exactly 1 tooth per revolution of the mill. To achieve this, I use a motor, shaft encoders on the mill motor and the gear blank motor, and an arduino running PID controller software to keep them exactly in sync. I also needed to tilt the mill head by the angle of the hob teeth, which I did with a thick shim.

837.JPG


Here's a picture of the hob cutting a plastic gear (just for a test). This was before I hardened the hob so I wanted to cut something soft.

841.JPG


And here are some gears I've made. Some spur gears and a worm gear (the worm was made on the lathe of course). I also used this to make the gears in my Webster engine.

004.JPG


Cheers,
Ross.
 
This is a significant achievement. If you’re willing, please tell us more of how you made and configured this modified mini-mill. It appears that you have mounted an optical chopper/counter on the spindle of the mill, but is the spindle drive also a stepping motor? How do you synchronize the rotation of the mill spindle and the rotation of the a-axis holding the gear blank?
 
Sure. There's a 2-bit opto-interruptor mounted on the mill motor frame and an interruptor disc with 18 notches mounted on the end of the motor shaft. The 2 bits feed the microcontroller, which is set up to count transitions. So the microcontroller knows how many notches have gone by and in which direction. Also knowing the gear ratio of the mill, it knows quite precisely the rotational position of the mill spindle. I have a similar setup on the gear blank motor (a-axis). Now given that we know both positions, we can measure the error in the a-axis position. The goal error is zero. A PID routine is set up to control the motor voltage and keep the motor moving at just the right speed that the error stays at zero (or as close as possible).

The Arduino has PWM outputs that make motor control easy and accurate, and its inputs can trigger interrupts for counting. It's surprising how well this works. Once stabilized, it's accurate to within about 0.2 degrees. The one minor problem I've had is mill vibration occasionally getting into the opto interruptor, but I could fix that by mounting the LED/phototransistors more solidly.
 
Thanks for the post! This cries out for a video though, a thousand words and all.
 
great post but I'm missing something .... if the hob is spiral, why do you need to tilt the mill ---- or is the hob straight?
 
great post but I'm missing something .... if the hob is spiral, why do you need to tilt the mill ---- or is the hob straight?
The hobbing gear cutter has spiral hence it has a angle, when you are creating the gear wheel, then the hob must be tilted to a right angle then the gear tooth is straight. Each hobbing gear cutter has angle marked on then one can set the correct angle on hobbing gear cutter to make straight tooth. If one want the helical gear, then angle + helical angle or angle - helical angle.
 
Thank you for the quick response. I had assumed that the spur blank rotated upward to match the helical rise of the hob so that the cutter always engaged the same spur. Part of my assumption was that if the head was tilted so that the hob cutter is flat, then the blank would be incremented only when the next spur is to be cut. I'll need to re-think this.
 
I hope that you had a good vacation. Now that you’re back, thanks for posting a video of your modified mini-mill. This setup has much potential for CNC of small metal hobby parts without too much expense. Hence, I’m interested knowing more of you system design.
An overall schematic illustrating how the different hardware components are electrically connected would be informative of how you control your mill. Did you really do this without the use of MACH4 or G-Code and only programming the Arduino and synchronizing to spindle and A-axis motors?
 
Hope vacation went well!!!

As for the video that is icing on the cake.

As for your code I'm a bit surprised that you found it easy. In my mind anyways keeping those two spindles in sync and not loosing timing would be a major headache.
 
Yes, this was done without Mach4 or G-code. Below is a look inside the box. There is an Arduino Due clone and a motor driver (VNH5019 from pololu.com). I'm not sure if an Uno would be fast enough for this application.

The orange/brown wires go to the motor position encoder. The green/blue wires go to the mill position encoder. The red wires go to the motor and PC power supply (0V/+5V/+12V). Everything runs off 5V except the motor which is 12V.

20180923_195205.jpg


Here is a picture of the mill encoder board. There's an IR LED and a 1.2K resistor as light source, and two IR phototransistors (with 10K pull-down resistors) right next to each-other.


20180923_194821.jpg


Here is a picture of the end of the motor, which is a cheap 12V 100RPM DC worm gear motor (PN01007-100, $64 from makermotor.com). You can see the two IR LEDs (with 1K resistors) press-fit into holes I drilled in the end of the motor case. There are also two hidden phototransistors at the other end of the motor, tucked in through holes I drilled in the side. The armature of the motor has 12 poles, and these act as an interruptor disc (the light shines through the gaps in the armature as it goes past). The beams need to be almost (but not quite) 180 degrees apart.

20180923_201912.jpg


This is the arduino sketch. It's all controlled from a PC via a USB cable. When it's running, it's constantly dumping info on the state of the PID and how synchronized it is.

Code:
#define HIGH_SPEED_MODE 0   // set if mill is in high speed gear setting
#if HIGH_SPEED_MODE
#define MREV  (18 * 4 * (30 / 14.0) * (21 / 20.0)) // 162
#else
#define MREV  (18 * 4 * (30 / 14.0) * (29 / 12.0)) // 372.857143
#endif
#define GREV  (12 * 4 * 56.0)
#define NTEETH  23
#define M       2.5          // motor power limit, 2.5 = full power

#define MOTORPWM 13
#define MOTORA 12
#define MOTORB 11
#define GPOS1  10
#define GPOS2  9
#define MPOS1  8
#define MPOS2  7
void setup()
{
  pinMode(MOTORPWM, OUTPUT);
  pinMode(MOTORA, OUTPUT);
  pinMode(MOTORB, OUTPUT);
  pinMode(GPOS1, INPUT);
  pinMode(GPOS2, INPUT);
  pinMode(MPOS1, INPUT);
  pinMode(MPOS2, INPUT);
  digitalWrite(MOTORPWM, LOW);
  digitalWrite(MOTORA, LOW);
  digitalWrite(MOTORB, LOW);
  Serial.begin(115200);
  attachInterrupt(digitalPinToInterrupt(GPOS1), handle_gpos_change, CHANGE);
  attachInterrupt(digitalPinToInterrupt(GPOS2), handle_gpos_change, CHANGE);
  attachInterrupt(digitalPinToInterrupt(MPOS1), handle_mpos_change, CHANGE);
  attachInterrupt(digitalPinToInterrupt(MPOS2), handle_mpos_change, CHANGE);
}

void printSigned(double x)
{
  if (x >= 0)
    Serial.print("+");
  Serial.print(x);
}

void get_gpos(int *ppos1, int *ppos2)
{
  *ppos1 = digitalRead(GPOS1);
  *ppos2 = digitalRead(GPOS2);
}

void get_mpos(int *ppos1, int *ppos2)
{
  *ppos1 = digitalRead(MPOS1);
  *ppos2 = digitalRead(MPOS2);
}

int enc(int pos1, int pos2)
{
  if (pos1 == 0 && pos2 == 0)
    return 0;
  if (pos1 == 0 && pos2 == 1)
    return 1;
  if (pos1 == 1 && pos2 == 1)
    return 2;
  if (pos1 == 1 && pos2 == 0)
    return 3;
}

volatile int gposition = 0;
volatile int gpos1 = -1, gpos2 = -1;
void monitor_motor()
{
  int pos1, pos2;
  get_gpos(&pos1, &pos2);
  if (pos1 != gpos1 || pos2 != gpos2)
  {
    int old_enc = enc(gpos1, gpos2);
    int new_enc = enc(pos1, pos2);
    if (old_enc != -1 && new_enc != old_enc)
    {
      if (new_enc == (old_enc + 1) % 4)
      {
        --gposition;
      }
      else if (new_enc == (old_enc + 3) % 4)
      {
        ++gposition;
      }
      else
      {
        Serial.print("skipped motor encoding ");
        Serial.print(old_enc);
        Serial.print(" -> ");
        Serial.print(new_enc);
        Serial.print("\n");
      }
    }
    gpos1 = pos1;
    gpos2 = pos2;
  }
}

volatile int mposition = 0;
volatile int mpos1 = -1, mpos2 = -1;
void monitor_mill()
{
  int pos1, pos2;
  get_mpos(&pos1, &pos2);
  if (pos1 != mpos1 || pos2 != mpos2)
  {
    int old_enc = enc(mpos1, mpos2);
    int new_enc = enc(pos1, pos2);
    if (old_enc != -1 && new_enc != old_enc)
    {
      if (new_enc == (old_enc + 1) % 4)
      {
        ++mposition;
      }
      else if (new_enc == (old_enc + 3) % 4)
      {
        --mposition;
      }
      else
      {
        Serial.print("skipped milling encoding ");
        Serial.print(old_enc);
        Serial.print(" -> ");
        Serial.print(new_enc);
        Serial.print("\n");
      }
    }
    mpos1 = pos1;
    mpos2 = pos2;
  }
}

void handle_gpos_change()
{
  monitor_motor();
}

void handle_mpos_change()
{
  monitor_mill();
}

void set_speed(double speed)
{
  int dir = (speed >= 0);
  double s = speed;
  if (s < 0)
    s = -s;
  int v = (int)(255 * s);
  digitalWrite(MOTORB, dir ? HIGH : LOW);
  digitalWrite(MOTORA, dir ? LOW : HIGH);
  analogWrite(MOTORPWM, v);
}

void run_at_speed(int dir, double speed, int duration)
{
  Serial.print(gposition);
  Serial.print("\n");
  set_speed(dir ? speed : -speed);
  delayMicroseconds(duration);
  digitalWrite(MOTORA, LOW);
  digitalWrite(MOTORB, LOW);
}

void test_motor()
{
  while (1)
  {
    for (int dir = 0; dir < 2; ++dir)
    {
      for (double speed = 0; speed <= 1.0; speed += 0.01)
        run_at_speed(dir, speed, 100000);
      run_at_speed(dir, 1.0, 1000000);
      for (double speed = 1.0; speed >= 0; speed -= 0.01)
        run_at_speed(dir, speed, 100000);
    }
  }
}

void test_mill_pos()
{
  while (1)
  {
    Serial.print(mpos1);
    Serial.print(",");
    Serial.print(mpos2);
    Serial.print(" ");
    Serial.print(mposition);
    Serial.print("\n");
    delay(100);
  }
}

/* PID coeffs */
/* crappy
#define KP  0.001
#define KI  0.0002
#define KD  0.001
*/
/* good */
/*
#define KP  0.05
#define KI  0.0005
#define KD  0
*/
/* less overshoot */
/*
#define KP  0.05
#define KI  0.0005
#define KD  0.001
*/
/* WITH MAX=2.5 */
/* overshoot 43, final error 1 */
/*
#define KP  0.05
#define KI  0.002
#define KD  0
*/
/* overshoot 7, final error <1 */
/*
#define KP  0.05
#define KI  0.002
#define KD  0.001
*/
/* more aggressive, tuned after final assembly */
#define KP  0.05
#define KI  0.02
#define KD  0

void run_pid()
{
  double tlast = 0;
  double sum_error = 0;
  double elast = 0;
  double spindle_pos_last = 0;

  /* PID */
  while (1)
  {
    delayMicroseconds(10000);
    double t = micros() * 1e-6;
    //double target_pos = 1000 + t * 500;
    double spindle_pos = mposition / MREV;
    double target_pos = (GREV * spindle_pos) / NTEETH;
    double error = target_pos - gposition;
    double dt = t - tlast;
    double derror = (error - elast) / dt;
    sum_error += error * dt;
    tlast = t;
    elast = error;
    double v = KP * error + KI * sum_error + KD * derror;
    double rpm = (spindle_pos - spindle_pos_last) * 60 / dt;
    spindle_pos_last = spindle_pos;
    if (v > M)
      v = M;
    if (v < -M)
      v = -M;
    set_speed(-v * 0.4);
    Serial.print("t=");
    Serial.print(t);
    Serial.print(" rpm=");
    Serial.print(rpm);
    Serial.print(" p=");
    Serial.print(gposition);
    Serial.print(" tgt=");
    Serial.print(target_pos);
    Serial.print(" error=");
    printSigned(error);
    Serial.print(" P=");[ATTACH=full]104370[/ATTACH] [ATTACH=full]104371[/ATTACH] [ATTACH=full]104372[/ATTACH]
    printSigned(KP * error);
    Serial.print(" I=");
    printSigned(KI * sum_error);
    Serial.print(" D=");
    printSigned(KD * derror);
    Serial.print(" v=");
    printSigned(v);
    Serial.print("\n");
  }
}

void loop()
{
  //set_speed(1);
  //while (1)
  //  ;
  run_pid();
  //test_mill_pos();
}
 
Last edited:
Thanks for the code, I'll have a study of it later on. One thing that stuck out was all the serial reporting though. Obviously fine for you in this instance but you mention a Uno might not be fast enough. I was mucking around with a Uno a couple of months ago and had serial output continually reporting servo states while I fine tuned a robotic arm range of motion/interferences. Once I had it all set up good enough I disabled the serial output and the speed of execution more than quadrupled. I didn't realise it would have such an effect, at least on a lowly Uno, but if you run into speed issues in the future consider disabling/reducing serial outputs and see if that fixes your issue.
 
New to the forum, so be gentle on me...

Are you able to change the ratio for the mill spindle speed to output speed on your a-axis?
Say if your making a different tooth gear for example?
 

Latest posts

Back
Top