CNC controller, two MPGs and two 7 segment DROs

Greetings all,

Well I have built myself a very capable CNC lathe, complete with two hand wheels each with homemade 200 PPR lasercut encoders. I have also salvaged an old table calculator with a nice keypad to use as a controller (data input, Digital Positional Readout, feedrate, Powerfeed, spindle control etc).

I had it all set up using Mach3. The hand wheels were directly wired to the breakout board and the keypad would, through an arduino Mega, interface with Mach3 via Modbus. This allowed me to display the DRO data on the 7 segment display but... It was painfully slow.

I would get a good 1 second latency on the display and keys would need to be held down for the same time before Mach3 picked them up. It was really annoying. Maybe it had something to do with my modbus code but it was so clumsy and Mach3 was just way too overkill for my project. (I have a Router that uses Mach3 thats why I tried it at first).

Then I thought why not just leave it all in the Mega! Read the two hand wheels via interupts, it was already powering the displays, all it needed to do was send step and direction pulses to the two stepper drivers.

So I added AccelStepper to the mix, ditched the modbus and added a simple turn the handle- move the carriage code. Worked super smooth.

Now just need to update the display and were away laughing.. Added the code and what do you know, accelStepper would bog down to almost 2 steps per second.!!

I did a bit of research and fluffing around it seems for loops are out, which is what I was using to update the individual characters of the display. So I broke down the for loop and just left it linear. That works better but still super slow. Just one function led.setChar() was enough to bog the whole system down and reduce the steps per second to unusable.

I haven't worked out exactly how many steps per second I need but 500mm/min should be enough. That would mean 1/8th microstep, 1600p/5mm 320p/1mm 2666p/second = 500mm/min

Question time;

Am I just asking too much from the Mega.? I have found one similar post here and the advise was to go to a faster board.

The requirements are to send steps as fast as possible (for two steppers that's around 6000 pulses/second) and update the display as smooth as possible (no less than 20fps).

Is seems like I need to prioritise the stepping and let the display update when there a gap in sending pulses? Seems reasonable. Not sure if its possible?

I had also thought to let a Nano do the display and free up the Mega to just do the stepping. But then the Mega would need to send the Nano the positional info for X and Z(via serial) which also bogs the system down. I tried just as a proof of concept Serial.write() in the main stepping loop and it reduced the loop speed to unusable like led.setChar().

Am I destined to buy a faster board? Or is this solvable with some optimised code?

I'll attach my current version of code, maybe something else is killing the speed? I've commented out the Display Update section. And as is it works fast enough.

#include "LedControl.h"
#include <AccelStepper.h>
#include <Keypad.h>
AccelStepper stepperX(AccelStepper::DRIVER, 46, 47);
AccelStepper stepperZ(AccelStepper::DRIVER, 48, 49);

volatile unsigned int tempX, counterX = 32000;
volatile unsigned int tempZ, counterZ = 32000;

const byte ROWS = 6; //six rows
const byte COLS = 6; //six columns
char keys[ROWS][COLS] = {
  {'Z', '^', '3', 'C', 'c', 'P'},
  {'d', 'e', 'f', 'a', 'b', 'h'},
  {'=', '+', 'X', 'g', '%', 'i'},
  {'/', 'k', '*', 'm', '-', '<'},
  {'p', '.', '0', '9', '8', '7'},
  {'6', '5', '4', '3', '2', '1'}

};
byte rowPins[ROWS] = {29, 31, 33, 35, 37, 39}; 
byte colPins[COLS] = {22, 24, 28, 32, 36, 40};

Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

LedControl lc = LedControl(12, 11, 10, 2);

int mode = 0;
float angle = 0.75;
int screenState = 0;
long numberX[7] =  { 0 };           // long array, used to write to the display
bool isNegX;                        // used to check if the input is negative and display the '-'
long inputX = 12345;                // Primary DRO input

long numberZ[7] =  { 0 };           // Z is the same as the above X
bool isNegZ;
long inputZ = 12345;

void setup()
{

  //---------- LED Setup -----------------

  lc.shutdown(0, false);
  lc.shutdown(1, false);
  /* Set the brightness to a max values */
  lc.setIntensity(0, 15);
  lc.setIntensity(1, 15);
  /* and clear the display */
  lc.clearDisplay(0);
  lc.clearDisplay(1);

  stepperX.setMaxSpeed(3000.0);      // Stepper Setup
  stepperX.setAcceleration(1000.0);

  stepperZ.setMaxSpeed(3000.0);
  stepperZ.setAcceleration(1000.0);

  pinMode(50, OUTPUT);  // Used for the Stepper enable pins
  pinMode(51, OUTPUT);
  digitalWrite(50, 1); // Not sure if initially set to OUTPUT 
  digitalWrite(51, 1);

  pinMode(2, INPUT_PULLUP); // The following is for the interupts for the two rotary encoders
  pinMode(3, INPUT_PULLUP);
  attachInterrupt(0, ai0, RISING);
  attachInterrupt(1, ai1, RISING);

  pinMode(18, INPUT_PULLUP);
  pinMode(19, INPUT_PULLUP);
  attachInterrupt(4, ai2, RISING);
  attachInterrupt(5, ai3, RISING);
  


}

void loop()
{

  char key = keypad.getKey();
  int keyState = keypad.getState();

  int posX = counterX;
  int posZ = counterZ;

  if ( counterX != tempX ) {
    tempX = counterX;
  }
  if ( counterZ != tempZ ) {
    tempZ = counterZ;
  }

  inputX = (posX - 32000);  // counter(volatile unsigned int, range 0-65000) starts at 32000, then posX -32000 = 0 with range -32000 - 32000
  inputZ = (posZ - 32000);

 
  if (key == '1') {             // Key presses for mode change and LEDs
    mode = 1;
    analogWrite(A1, 128);
    analogWrite(A4, 0);
    analogWrite(A3, 0);

  }
  if (key == '2') {
    mode = 2;
    analogWrite(A4, 128);
    analogWrite(A3, 0);
    analogWrite(A1, 0);

  }
  if (key == '3') {
    mode = 3;
    analogWrite(A3, 128);
    analogWrite(A4, 0);
    analogWrite(A1, 0);
  }


  if (mode == 1) {                           // Manual MPG mode. 
    stepperX.moveTo((posX - 32000)*3);       // Simple turn the handle - move the axis
    stepperZ.moveTo((posZ - 32000)*3);
    stepperX.run();
    stepperZ.run();
    

  }
  if (mode == 2) {                           // Manual MPG angle mode, moves both axis to result in a linear angle move
    stepperX.moveTo(posX - 32000);
    stepperZ.moveTo(((posX - 32000)*tan(angle)) + (posZ - 32000));
    stepperX.run();
    stepperZ.run();

  }

  /*

    //-------------- Update Display  X  ---------------

    if (mode == 3) {             // Normally on, but turns off when adjusting the jog percentage
      long absInputX = abs(inputX);     // First makes the input absolute
      if (inputX < 0) isNegX = true;    // Then checks if it was negative or not and writes to the Bool isNeg
      else isNegX = false;              // Important to seperate the '-' to be able to display it in the next lines

      if (isNegX == true) lc.setChar(0, 7, '-', false);
      else lc.setChar(0, 7, ' ', false);


      for (int i = 0; i <= 7; i++) {    //writes the int. input to the array to be able to display the individual characters
        numberX[i] = absInputX % 10;
        absInputX /= 10;

      }

      for (int i = 0; i <= 6; i++) {             // Here are the numbers from the array assigned to the display
        lc.setChar(0, i, (numberX[i]), false);
        lc.setChar(0, 3, (numberX[3]), true);    // Number 4 is kept seperate to be able to turn on the decimal point.

      }

      //-------------- Update Display  Z  ---------------

      long absInputZ = abs(inputZ);            //Same again, this time for Z
      if (inputZ < 0) isNegZ = true;
      else isNegZ = false;

      if (isNegZ == true) lc.setChar(1, 7, '-', false);
      else lc.setChar(1, 7, ' ', false);

      for (int i = 0; i <= 7; i++) {
        numberZ[i] = absInputZ % 10;
        absInputZ /= 10;
      }

      for (int i = 0; i <= 6; i++) {
        lc.setChar(1, i, (numberZ[i]), false);
        lc.setChar(1, 3, (numberZ[3]), true);

      }

    }
  */


}


void ai0() {
  // ai0 is activated if DigitalPin nr 2 is going from LOW to HIGH
  // Check pin 3 to determine the direction
  if (digitalRead(3) == LOW) {
    counterX++;
  } else {
    counterX--;
  }
}
void ai1() {
  // ai0 is activated if DigitalPin nr 3 is going from LOW to HIGH
  // Check with pin 2 to determine the direction
  if (digitalRead(2) == LOW) {
    counterX--;
  } else {
    counterX++;
  }
}
void ai2() {
  // ai0 is activated if DigitalPin nr 2 is going from LOW to HIGH
  // Check pin 3 to determine the direction
  if (digitalRead(18) == LOW) {
    counterZ++;
  } else {
    counterZ--;
  }
}
void ai3() {
  // ai0 is activated if DigitalPin nr 3 is going from LOW to HIGH
  // Check with pin 2 to determine the direction
  if (digitalRead(19) == LOW) {
    counterZ--;
  } else {
    counterZ++;
  }
}

Jer_emy:
Am I just asking too much from the Mega.?

It's hard to know without testing - for example by commenting out different parts to see where the blockage is.

Do you really need to update the LEDs on every iteration of loop() - maybe only twice per second, or only when the value changes?

And you should suspend interrupts when reading a multibyte variable from an ISR - like this

 noInterrupts();
    int posX = counterX;
    int posZ = counterZ;
  interrupts();

...R

Ah, I didn't know that was a thing (suspending interups) thanks for the insight.

As far as finding blockages goes, I have deduced that trying to call ledControl.setChar() causes a serious delay and it needs to be called for all 16 digits of the 7 segment display. This is tidied up in a for loop. But the duration of the for loop is too long that the stepper function run() times out. Breaking the setChar() functions out of the loop makes the function run() work again but seriously slow.

I did try organising it so that the display would only update when no pulses are being sent but this is unusable. I need to be able to carefully watch the display and wind in the handwheel at the same time. Any latency and you can quickly overshoot the mark. Going slow and just bumping the handle a bit, waiting, checking the display and bumping a bit more is just frustrating.

Could it be that the ledControl library is full of blocking delay()s? Maybe but I don't think so. When running just the handwheels (rotary encoders on interupts) and updating the display every loop() iteration, I get silky smooth action. And when trying to do both, the steps per second just dive.

I guess this has something to do with the way accelStepper works.

I feel like this isn't an optimisation thing, more like no matter what I do I can't have the Mega send 3000 pulses per second and update the displays at the same time.

Then I guess the question is, what board could?

Maybe you could offload the display code to another Atmea chip that just gets the data (by Serial, or I2C or SPI) from the Mega?

I'm not sure what data the other Atmega would need from the Mega - maybe just a +1 or a -1

...R

I had also thought to let a Nano do the display and free up the Mega to just do the stepping. But then the Mega would need to send the Nano the positional info for X and Z(via serial) which also bogs the system down. I tried just as a proof of concept Serial.write() in the main stepping loop and it reduced the loop speed to unusable like led.setChar().

Yeah, had that thought already. I'm not too familiar with serial communication, I2C or SPI. But like I said I tried the Serial.write() and that slowed things down as much as display update. Kinda pointless. Might be worth another shot though. What is the fastest option? I just need to send two positions, both 5 digit Integers. Is it taxing to try and send them both at once as integers? or should I convert them both into a byte array and assemble it on the nano?

Jer_emy:
What is the fastest option? I just need to send two positions, both 5 digit Integers.

Two thoughts come to mind.

Why is it necessary to send the full number other than at startup. Wouldn't it be sufficient just to send a +1 or -1 whenever the control wheel is moved? If so you could toggle two digital I/Os to communicate with the other Arduino.

Alternatively two INTs is just 4 bytes, or maybe 6 bytes if you add a start- and end-marker. You could probably send them one byte at a time between steps. Adding one byte (or even 6 bytes) the the Serial Output Buffer would take only a few microsecs.

2600 steps per second is one every 384 microsecs (if my maths is correct) so there should be ample time both to send the data to the Output Buffer with Serial.write() and for the Serial interrupts to move the data to the UART.

And, after all that, it seems to me that sending 10 characters (or 13) (for two 5-digit numbers expressed as text) should also be capable of interlacing with the steps. 13 steps would take about 5 millisecs so there should be no latency that a human could detect.

...R