12. Mechanical design and machine design

For week 12 I programed our CNC which you can find here.

I first tried to make the code interpret G-codes using the Arduino IDE. So, I looked at G-code commands to understand them and see how to interpret them. The following two websites were useful for that:

https://marlinfw.org/meta/gcode/

https://howtomechatronics.com/tutorials/g-code-explained-list-of-most-important-g-code-commands/

I also made a Python script to send each line from a file containing the code. I encountered some issues, like the data being sent all at once or the motors moving too much.

After doing some research on how I could send G-codes to my machine, I found that GRBL was used for Arduino and ESP32 shields, and connecting them to UGS. To keep things simple, I decided to use a XIAO ESP32 C3. It was supposed to be as simple as downloading a zip file from this website: https://github.com/bdring/Grbl_Esp32. But an error occurred every time I tried to compile the code on the Arduino IDE, probably because the library wasn´t specifically made for this board.

I also tried using VS Code and Platform IO, but experienced some troubles too. I asked Chat GPT for help, hoping it could give me an easy fix, but every time I solved an issue, I got another one. After a while, it suggested making my own library, which helped me to make a “barebones” version of GRBL. To build the library, I created a new project inside VS Code with the Platform IO extension.

I tested the first version that it gave me, but it had some trouble connecting with UGS since the code had to send “ok” after receiving a command.

Error example:

*** Fetching device state

>>> $G

[G90 G21 G17 G94 G54]

ok

*** Connected to GRBL 1.1h

>>> G90

An error was detected while sending '$G': error: Unsupported command. Streaming has been paused.

>>> G0

An error was detected while sending 'G90': error: Unsupported command. Streaming has been paused.

After adding the “ok” response and the support for most of the commands UGS uses, I was able to successfully connect and send a G-code to the board.

void protocol_loop()

        void protocol_loop() {
            if (Serial.available()) {
              String line = Serial.readStringUntil('\n');
              line.trim();
              line.toUpperCase();  // Normalize case for command parsing
          
              if (line.length() == 0) {
                Serial.println("ok");  
                return;
              }
              
              if (line == "?") {
                report_status();
              } else if (line == "$I") {
                Serial.println("[VER:1.1h:Barebones]");
                Serial.println("[OPT:VN]");
                Serial.println("ok");
              } else if (line == "$G") {
                Serial.println("[G90 G21 G17 G94 G54]");
                Serial.println("ok");
              } else if (line == "$$") {
                Serial.println("$0=10");
                Serial.println("$1=25");
                Serial.println("$100=240.0");
                Serial.println("$101=340.0");
                Serial.println("$102=100.0");
                Serial.println("$110=1000.0");
                Serial.println("$111=1000.0");
                Serial.println("$112=500.0");
                Serial.println("ok");
              } else if (line == "$X" || line == "$H") {
                Serial.println("ok");
              } else if (line == "G28") {
                motion_go_home();
                Serial.println("ok");
              } else {
                float x = NAN, y = NAN, z = NAN, f = -1;
                bool has_g90 = false, has_g91 = false, has_motion = false;
          
                char buffer[line.length() + 1];
                strncpy(buffer, line.c_str(), sizeof(buffer));
                buffer[sizeof(buffer) - 1] = '\0';
                char* ptr = buffer;
          
                while (*ptr) {
                  if (*ptr == 'G') {
                    ptr++;
                    long gcode = strtol(ptr, &ptr, 10);
                    switch (gcode) {
                      case 0:
                      case 1: has_motion = true; break;
                      case 90: has_g90 = true; break;
                      case 91: has_g91 = true; break;
                    }
                  } else if (*ptr == 'X') {
                    ptr++;
                    x = strtof(ptr, &ptr);
                  } else if (*ptr == 'Y') {
                    ptr++;
                    y = strtof(ptr, &ptr);
                  } else if (*ptr == 'Z') {
                    ptr++;
                    z = strtof(ptr, &ptr);
                  } else if (*ptr == 'F') {
                    ptr++;
                    f = strtof(ptr, &ptr);
                  } else {
                    ptr++;
                  }
                }
          
                if (has_g90) {
                  motion_set_absolute_mode(true);
                  Serial.println("ok");
                } else if (has_g91) {
                  motion_set_absolute_mode(false);
                  Serial.println("ok");
                } else if (has_motion) {
                  motion_execute_move(x, y, z, f);
                  Serial.println("ok");
                } else {
                  Serial.println("error: Unsupported command");
                }
              }
            }
          }
    

I converted the steps the motors could do to millimeters. To have accurate motion and scaling. To do so, I calculated that the motors needed to move 200 steps to complete a full turn. For both the X and Y axes, we used a band with a pulley whose radius is 14mm.

steps_per_mm = 200.0 / (PI * 14.0) ≈ 4.55

Since we didn´t add limit switches to the machine, I also needed to limit the number of steps the machine could move. For X, I set the limit to 800, and for Y, 1,000.

I decided to add four buttons to my PCB to move both axes and one extra to set the origin. So, after making the conversion from steps to millimeters, I added support for the buttons in the code:

Buttons

        void manual_control_loop() {
            static unsigned long last_move = 0;
            const unsigned long debounce_time = 200;  // ms
          
            if (millis() - last_move < debounce_time) return;
          
            if (digitalRead(BTN_X_POS) == LOW) {
              Serial.println("🔼 X+ Pressed");
              digitalWrite(X_DIR_PIN, HIGH);
              for (int i = 0; i < 10; i++) {
                digitalWrite(X_STEP_PIN, HIGH);
                delayMicroseconds(1000);
                digitalWrite(X_STEP_PIN, LOW);
                delayMicroseconds(1000);
              }
              last_move = millis();
            }
          
            if (digitalRead(BTN_X_NEG) == LOW) {
              Serial.println("🔽 X- Pressed");
              digitalWrite(X_DIR_PIN, LOW);
              for (int i = 0; i < 10; i++) {
                digitalWrite(X_STEP_PIN, HIGH);
                delayMicroseconds(1000);
                digitalWrite(X_STEP_PIN, LOW);
                delayMicroseconds(1000);
              }
              last_move = millis();
            }
          
            if (digitalRead(BTN_Y_POS) == LOW) {
              Serial.println("➡️  Y+ Pressed");
              digitalWrite(Y_DIR_PIN, HIGH);
              for (int i = 0; i < 10; i++) {
                digitalWrite(Y_STEP_PIN, HIGH);
                delayMicroseconds(1000);
                digitalWrite(Y_STEP_PIN, LOW);
                delayMicroseconds(1000);
              }
              last_move = millis();
            }
          
            if (digitalRead(BTN_Y_NEG) == LOW) {
              Serial.println("⬅️  Y- Pressed");
              digitalWrite(Y_DIR_PIN, LOW);
              for (int i = 0; i < 10; i++) {
                digitalWrite(Y_STEP_PIN, HIGH);
                delayMicroseconds(1000);
                digitalWrite(Y_STEP_PIN, LOW);
                delayMicroseconds(1000);
              }
              last_move = millis();
            }
          
            if (digitalRead(BTN_SET_ORIGIN) == LOW) {
              Serial.println("🔄 Origin set (manual)");
              current_position[X_AXIS] = 0.0;
              current_position[Y_AXIS] = 0.0;
              last_move = millis();
            }
          }
          

I also added a function to set the origin. I had to make some changes to the code since the machine was moving in the opposite direction. I had to change the direction of the motors, and I also had to change the way I set the origin. I used a button to set it, but I had to make sure that when I pressed it, the machine was at 0,0. So, I made a function that sets the origin when the button is pressed.

The code for setting the origin is as follows:

Set origin
void set_origin() {
    Serial.println("Setting origin...");
    current_position[X_AXIS] = 0.0;
    current_position[Y_AXIS] = 0.0;
    current_position[Z_AXIS] = 0.0;
    Serial.println("Origin set.");
}

After some trial and error, I figured out that in this position, the origin should be set every time to avoid collisions. On the X axis near the motor, and the Y axis away from the motor.

For the Z axis, we used a servomotor to have an up/down functionality. Initially, I programmed it to move with G0 Z0 and G0 Z10, but I had some trouble because if I sent G0 Z10 more than once, for example, the program would assume that the motor was in Z20, and I had to type Z-20 so it could return to Z0. This happened because it had the same programming as the other two axes. We used a 360° servomotor, so I tried to map the full movement, but I used the ESP32 servomotor library, so it could only move from 0° to 180°, but I solved the command problem.

Z axis
void z_move_to(float z_target) {
// Map from 0-10 → 0-360 degrees
float clamped = constrain(z_target, 0.0, 10.0);  // limits between 0 and 10
int angle = map(clamped * 100, 0, 1000, 0, 360);

Serial.printf("↕ Moving Z (servo) to angle: %d°\n", angle);
z_servo.write(angle);
delay(500);  // Small delay to complete the movement
}

With all these changes, I was ready to upload the first full G-code and see if everything worked correctly. I opened a text file containing a G-code to draw a pentagon on UGS. I got the code by importing a PNG image into mods, I set the origin, and hit the play button. But an error occurred, I couldn´t tell right away what it was, since I was able to send data from the console window.

At the time of the test, we didn´t have the servomotor attached to the CNC, so I taped a pen to test functionality. I tried pasting the whole code into that window. And saw the machine moving. Here´s what it drew:

I clearly had to make some changes to the code since this wasn´t completely right. This is what it should look like:

After making some changes to the code, I got the desired form:

I tried with a simplified version of the pentagon to clearly see what was wrong, I realized that the problem had to do with the speed the stepper motors were moving. They moved at the same time, so one could finish first, making the wrong movement. I asked Chat GPT to help me change the code´s logic to move the stepper motors correctly. Here´s the part I changed to move the motors correctly:

Move motors

            // Calculate step deltas float delta_x = target[X_AXIS] - current_position[X_AXIS]; float delta_y = target[Y_AXIS] - current_position[Y_AXIS]; 

            long steps_x = abs(MM_TO_STEPS_X(delta_x)); long steps_y = abs(MM_TO_STEPS_Y(delta_y)); 
            
            // Direction int dir_x = (delta_x >= 0) ? HIGH : LOW; int dir_y = (delta_y >= 0) ? HIGH : LOW; digitalWrite(dir_pin[X_AXIS], dir_x); digitalWrite(dir_pin[Y_AXIS], dir_y); 
            
            // Total distance using Pythagoras (Euclidean distance) float distance_mm = sqrt(delta_x * delta_x + delta_y * delta_y); 
            
            // Feedrate: convert mm/min to time per step if (f > 0 && distance_mm > 0) { float mm_per_step = distance_mm / max(steps_x, steps_y); step_delay_us = (60000000.0 / f) * mm_per_step; if (step_delay_us < 200) step_delay_us = 200; } else { step_delay_us = 5000; } 
            
            long steps = max(steps_x, steps_y); long counter_x = 0; long counter_y = 0; 
            
            unsigned long last_step_time = micros(); for (long i = 0; i < steps; i++) { while (micros() - last_step_time < step_delay_us); // wait last_step_time = micros(); 
            
            counter_x += steps_x; 
            counter_y += steps_y; 
             
            if (counter_x >= steps) { 
                digitalWrite(step_pin[X_AXIS], HIGH); 
                delayMicroseconds(10); 
                digitalWrite(step_pin[X_AXIS], LOW); 
                counter_x -= steps; 
            } 
             
            if (counter_y >= steps) { 
                digitalWrite(step_pin[Y_AXIS], HIGH); 
                delayMicroseconds(10); 
                digitalWrite(step_pin[Y_AXIS], LOW); 
                counter_y -= steps; 
            } 
              
            
            } 

          

After applying these changes, the CNC successfully drew a pentagon. We tried with different g codes, and they worked.

UGS interface:

Here´s the zip file containing the final library created:

Grbl_barebones.zip

Here´s our CNC:

Summary

Maybe it would´ve been a better option to polish the first code I created to interpret G-codes, but this was the path that led to the CNC fully operating. I made bigger improvements following this path than the other ones. It doesn´t fully support UGS, since I can´t control the machine with the buttons inside the UGS interface, and I didn´t fix the error it gave when I tried to run a file. Nonetheless, UGS was a way to send the code from the console, and it verified each action automatically before sending the next one. It also provided an interface where I could have a preview of the G-code and live axes position. Although I made this library considering the XIAO ESP32 C3, it can be used with other boards; the only things that need to be changed are the servo library, pin configuration, and the platform.ini file, depending on the board.