Skip to content

Pen Plotter project

Automation (Stéphane)

There are many options to automate the machine and drive the motors. The most common one being a simple Arduino board with a stepper motor shield and the GRBL firmware installed. At Fablab Sorbonne we have a lot of M5Stack devboards. These boards use an ESP32 and are quite versatile. As the name implies, they are stackable, but they are also beautifully integrated with an LCD screen, programmable buttons and SD card reader. That way we’ll be able to drive the machine from a web interface as well as from the board itself. Also, it’s an ecosystem I have never used, so it’ll be interesting to learn.

Arduino set up

The first step was to set up the development environment and make sure I was able to upload code on the board. So I followed the official tutorial on how to use the Arduino IDE with these boards.

I went through all the steps and when the time came to upload the code, it failed. I got the following error message:

failed uploading: uploading error: exit status 2

It turns out there were 2 USB devices showing up when I connected the board and I hadn’t selected the right one. For it to work I had to select cu.wchusbserial54D80292151.

alt text

After that, the usual “Hello World” program worked just fine!

alt text

GRBL module

On to more interesting things. Now it’s time to do a Hello World with the GRBL module. The plan, once again, is to follow the official documentation.

The GRBL module is basically an ATMega328P with the GRBL firmware loaded and 3 stepper motor drivers. The main board (the ESP32) communicates with the module via I2C.

Connectors

Before I can actually try the module though, I have to change the motor connector… As you can see below, the sockets on the module do not match the connectors that came with the steppers.

alt text

Fortunately we have a kit with the right connectors. The ones required here are 2.54mm JST-XH. I had never crimped those connectors before so I followed a great tutorial on YouTube.

Et voilà! Ready for a first test!

alt text

alt text

Hello GRBL

Once everything was connected I figured I just had to upload the code provided by the tutorial, but that would have been too easy… Instead I got a compilation error: MODULE_GRBL13.2.h:12:9: error: 'TwoWire' does not name a type; did you mean 'TwoWire_h'?

I couldn’t find anything about that specific error with that library, but it seemed like the Wire library was not declared properly somewhere. So I opened MODULE_GRBL13.2.h on my computer and as it turns out the Wire library was indeed not declared! So I just added the following instruction: #include <Wire.h> at the top of the program and then the compilation worked!

But I was not out of the woods just yet, because the motors were not turning.

To debug I tried a lot of different things: - Testing cables/connectors with the multimeter: AOK - Checking the motor output with the oscilloscope: NOK (no changes in the outputs when pressing the buttons) - Testing the buttons: OK with another program, NOK with the GRBL program - Testing for conflicts between libraries: OK (only the _GRBL.readStatus() instruction was blocking the code which means the M5Stack wasn’t able to communicate with the module) - Trying a different I2C address: NOK - Testing the same code with another GRBL module: OK

So the problem came from the first module I used… either it was faulty from the start or I did something that messed it up. I guess we’ll never know for sure. So here is the code tested in the video:

#include <M5Stack.h>
#include <MODULE_GRBL13.2.h>

#define STEPMOTOR_I2C_ADDR 0x70

GRBL _GRBL = GRBL(STEPMOTOR_I2C_ADDR);

void setup() {
    M5.begin();
    M5.Power.begin();
    Wire.begin(21, 22);
    _GRBL.Init(&Wire);
    Serial.begin(115200);

    m5.Lcd.setTextColor(WHITE, BLACK);
    m5.Lcd.setTextSize(3);
    m5.lcd.setBrightness(100);
    M5.Lcd.setCursor(80, 40);
    M5.Lcd.println("GRBL 13.2");
    M5.Lcd.setCursor(50, 80);
    M5.Lcd.println("Press Btn A/B");
    M5.Lcd.setCursor(50, 120);
    M5.Lcd.println("Control Motor");
    M5.Lcd.setCursor(0, 0);

    _GRBL.setMode("absolute");

    Serial.print(_GRBL.readStatus());
    Serial.println("M5Stack started. Beginning Loop.");
}

void loop() {
    M5.update();

    if (M5.BtnA.wasReleased())
    {
        _GRBL.setMotor(5, 5, 0, 200);
        _GRBL.setMotor(0, 0, 0, 200);

        M5.Lcd.print('A');
        Serial.println("Bouton A");
    }

    if (M5.BtnB.wasPressed()) {
        _GRBL.sendGcode("G1 X5Y5Z0 F200");
        _GRBL.sendGcode("G1 X0Y0Z0 F200");

        M5.Lcd.print('B');
        Serial.println("Bouton B");
    }

    if (M5.BtnC.wasReleased()) {
        _GRBL.unLock();

        M5.Lcd.print('C');
        Serial.println("Bouton C");
    }
}

So we can either send Gcode directly (button B) or send coordinates and speeds (button A). Pretty straightforward.

Servo module

We are going to drive the X and Y axis with stepper motors, but the Z axis, which is just pushing the pen down against the paper or the board, will be driven by a simple servo motor. Since the motor will be on the axis, we wanted something light. A stepper motor would have been a little too heavy (even a NEMA14). So to drive the servo, we’ll use another module. I could have used the regular GPIOs, but I wanted to try the module.

So to the previous code, I just added the following instructions so that the servo rotates when you press the C button.

    #include <Wire.h>
    #define SERVOMOTOR_I2C_ADDR 0x53

    // ...
    // then, in the loop

    if (M5.BtnC.wasReleased()) {
        Wire.beginTransmission(SERVOMOTOR_I2C_ADDR);
        Wire.write(0x10 | 0); // 0 is the channel number on the module
        Wire.write(90); // angle
        Wire.endTransmission();

        delay(1000);

        Wire.beginTransmission(SERVOMOTOR_I2C_ADDR);
        Wire.write(0x10 | 0);
        Wire.write(0);
        Wire.endTransmission();

        M5.Lcd.print('C');
        Serial.println("Bouton C");
    }

No fancy M5Stack libraries required here, we just need to initiate an I2C connection with the module and send the channel number to which the motor is connected, as well as the angle at which we wish to set the the motor.

Reading from an SD card

Next step is to learn how to read and write a file to an SD card. We want to store the GCode on the SD card and then read from it. So let’s see how that works. The official documentation this time was not tremendously helpful. It shows how to read a file, but the result is just a bunch of numbers. What it does is simply display the bytes but not decode them. So the solution is to store the byte in a char variable and then display that variable.

I also found a good reference in french on how to use SD cards with Arduino.

Here’s the code to display a list of files and folders:

if (!SD.begin()) { // connect to SD card
  M5.Lcd.println("Card failed, or not present");
  while (1);
}

Serial.println("TF card initialized.");
File d = SD.open("/"); // open the root folder
File f = d.openNextFile(); // open the first file in the folder

while (f) { // go through the files one by one
  M5.Lcd.println(f.name()); // print the file or the folder name on the screen

  f.close(); // close file
  f = d.openNextFile(); // open the next file available
}

Now the code to display the content of a specific file:

if (!SD.begin()) { // connect to SD card
  M5.Lcd.println("Card failed, or not present");
  while (1);
}

Serial.println("TF card initialized.");

File f = SD.open("/test.txt");

while (f) {
  M5.Lcd.println(f.name());

  // display file content
  while (f.available() > 0) {
    char letter = f.read(); // store the byte value in a char variable
    M5.Lcd.print(letter);
  }

  f.close();
}

alt text

Making a screen interface

My first iteration is a simple interface on the M5 LCD screen. A menu from which you can select a gcode file on the SD card and then launch the plotting or delete the file.

Launching the plotting reads the gcode file and analyzes each line. The 2D movements are handled by the GRBL module and the down movements by the servo motor.

I made a simple state machine program. Each loop checks the state and launches the appropriate handler function. Each handler function has a condition that changes the state (waiting for a button or the end of a file for example).

#include <M5Stack.h>
#include <MODULE_GRBL13.2.h>

#define STEPMOTOR_I2C_ADDR 0x70
#define SERVOMOTOR_I2C_ADDR 0x53

#define MAXFILES 50

GRBL _GRBL = GRBL(STEPMOTOR_I2C_ADDR);

enum states_e { BROWSE, OPTIONS, PLOTTING, FINISHED };
enum states_e state = BROWSE;

String files[MAXFILES];
int i = 0;
int fileSelected = 0;
int numFiles = 0;

String options[3] = { "Read", "Delete", "Go back" };

void exploreSD() {
  // Read SD card and store all the file names in an array
  File d = SD.open("/");
  File f = d.openNextFile();

  i = 0;
  while (f) {
    String filename = f.name();
    if( !filename.startsWith(".") ) {
      files[i] = "/";
      files[i].concat(f.name());
      i++;
    }
    f.close();
    f = d.openNextFile();
  }

  numFiles = i;
}

void handle_browse();
void handle_options();
void handle_plotting();
void handle_finished();

void rotateServo(int angle) {
  Wire.beginTransmission(SERVOMOTOR_I2C_ADDR);
  Wire.write(0x10 | 0); // 0 is the channel number on the module
  Wire.write(angle);
  Wire.endTransmission();
}

void setup() {
  M5.begin();
  M5.Power.begin();

  // Configure display
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.setTextSize(3);
  M5.lcd.setBrightness(100);

  if (!SD.begin()) {
    M5.Lcd.println("Card failed, or not present");
    while (1);
  }
  Serial.println("TF card initialized.");

  exploreSD();

  Wire.begin(21, 22);
  _GRBL.Init(&Wire);
  Serial.begin(115200);

  _GRBL.setMode("absolute");

  Serial.print(_GRBL.readStatus());
  Serial.println("M5Stack setup complete. Entering Loop.");
}

void loop() {
  switch(state) {
    case BROWSE:
      handle_browse();
      break;
    case OPTIONS:
      handle_options();
      break;
    case PLOTTING:
      handle_plotting();
      break;
    case FINISHED:
      handle_finished();
      break;
  }
}

void handle_browse() {
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextSize(3);
  M5.update();

  for(i=0 ; i<numFiles ; i++) {
    if(fileSelected == i) {
      M5.Lcd.setTextColor(BLACK, WHITE);
    } else {
      M5.Lcd.setTextColor(WHITE, BLACK);
    }
    M5.Lcd.println(files[i]);
  }

  if (M5.BtnA.wasPressed() && fileSelected > 0) {
    fileSelected--;
  }

  if (M5.BtnB.wasPressed()) {
    M5.Lcd.clear(BLACK);
    state = OPTIONS;
  }

  if (M5.BtnC.wasPressed() && fileSelected < (numFiles-1)) {
    fileSelected++;
  }
}

void handle_options() {
  static int optionSelected = 0;

  M5.Lcd.setCursor(0, 0);
  M5.update();

  for(i=0 ; i<3 ; i++) {
    if(optionSelected == i) {
      M5.Lcd.setTextColor(BLACK, WHITE);
    } else {
      M5.Lcd.setTextColor(WHITE, BLACK);
    }
    M5.Lcd.println(options[i]);
  }

  if (M5.BtnA.wasPressed() && optionSelected > 0) {
    optionSelected--;
  }

  if (M5.BtnB.wasPressed()) {
    switch(optionSelected) {
      case 0:
        M5.Lcd.clear(BLACK);
        state = PLOTTING;
        break;
      case 1:
        if(SD.exists(files[fileSelected])) {
          if(SD.remove(files[fileSelected])) {
            exploreSD();
            M5.Lcd.clear(BLACK);
            state = BROWSE;
          }
        }

        break;
      case 2:
        M5.Lcd.clear(BLACK);
        state = BROWSE;
        break;
    }
  }

  if (M5.BtnC.wasPressed() && optionSelected < 3) {
    optionSelected++;
  }
}

void handle_plotting() {
  String gcodeLine;
  File f = SD.open(files[fileSelected]);

  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextSize(2);

  while (f.available() > 0) {
    char letter = f.read(); // store the byte value in a char variable

    if(letter == '\n') { // if we reach the end of the line, then analyze the line
      M5.Lcd.clear(BLACK);
      M5.Lcd.setCursor(0, 0);
      M5.Lcd.println(gcodeLine);
      if(gcodeLine.startsWith("G")) { // GCODE to put the pen down and plot
        M5.Lcd.println("Putting the pen down.");
        rotateServo(90);
      }
      else if(gcodeLine.startsWith("G")) { // GCODE to put the pen back up
        M5.Lcd.println("Putting the pen back up.");
        rotateServo(0);
      }
      else _GRBL.sendGcode(gcodeLine); // execute GCODE stepper movement

      gcodeLine = ""; // reset variable
    }
    else gcodeLine.concat(letter); // append letter

    delay(100);
  }

  f.close();

  M5.Lcd.clear(BLACK);
  state = FINISHED;
}

void handle_finished() {
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextSize(3);

  M5.Lcd.println("Plotting finished! Click on button B to go back to browsing.");

  while(1) {
    M5.update();

    if (M5.BtnB.wasPressed()) {
      M5.Lcd.clear(BLACK);
      state = BROWSE;
      break;
    }
  }
}

The interface is a little crude, but it’ll do for now. We might do something cleaner during interface week.

Putting it all together

Now that we’ve got all the blocks necessary, let’s put this bad boy together and try to make it work.

Calibration

First thing we need to do is calibrate the machine. To do this we’ll upload the hello-grbl code and see if this works. As it turns out, the motors don’t have enough torque and they are not moving. I can hear them scream but not much else is happening. To get more torque, we’re going to turn the potentiometer near the motor driver very slightly (1/8 of a turn) to increase the current output.

alt text

Now for the distance calibration. We want the axis to move the exact distance we’re telling it to go in the gcode, so first we try to move just one mm with the current settings. I measured more than 10cm… very far off.

One stepper motor rotation is 200 steps. But we can subdivide this with the help of microstepping by just flipping some switches on the board. I configured 1/8 steps. That got us a lot closer. After a simple cross-multiplication I found that there were 162 steps necessary to move one mm. So now I can add the following line of code in the setup.

_GRBL.Init(&Wire, 162, 162, 162, 300); // x_steps per mm, y_steps per mm, z_steps per mm and acceleration

Gcode

Next step is to generate a gcode from an SVG file. To do this I used the open-source software GRBL-Plotter.

We need to configure the software to generate a specific gcode for the servo motor, like so:

alt text

And then import the SVG:

alt text

alt text

The gcode is automatically generated. You can simulate it and even send it directly via USB, but this won’t work in our case. We need to save the gcode in a file and put it on our SD card.

Some other interesting resources on gcode commands:

Code adjustments

I had to make some adjustments to my code for it to work properly. First I had to convert the String I used to store each gcode line into a char array. Then I had to add a pause to wait for each line to be executed in order to be able to handle the servo.

void handle_plotting() {
  String gcodeLine;
  File f = SD.open(files[fileSelected]);

  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextSize(2);

  while (f.available() > 0) {
    char letter = f.read(); // store the byte value in a char variable

    if(letter == '\n') { // if we reach the end of the line, then analyze the line
      M5.Lcd.clear(BLACK);
      M5.Lcd.setCursor(0, 0);
      M5.Lcd.println(gcodeLine);

      if(gcodeLine.startsWith("P92")) { // GCODE to put the pen down and plot
        M5.Lcd.println("Putting the pen down.");
        rotateServo(90);
        delay(1000);
      }
      else if(gcodeLine.startsWith("P90")) { // GCODE to put the pen back up
        M5.Lcd.println("Putting the pen back up.");
        rotateServo(0);
        delay(1000);
      }
      else { // execute GCODE stepper movement
        char code[256];
        memset(code,0,sizeof(char)*256);
        sprintf(code, gcodeLine.c_str()); // convert String to char*

        _GRBL.sendGcode(code);
        delay(500);
        while( _GRBL.readStatus().startsWith("BUSY") ) {} // pausing while command is being executed
      }

      gcodeLine = ""; // reset variable
    }
    else gcodeLine.concat(letter); // append letter
  }

  f.close();

  M5.Lcd.clear(BLACK);
  state = FINISHED;
}