CNC Engraving Machine.

Meet Our Team

Apolinar Velazquez
Apolinar Velazquez
Leonardo Zamora
Leonardo Zamora
Member 4
Ramon Romero
Jose Zarate
Jose Zarate
Descripción de la imagen 1
This video demonstrates the CNC machine fully assembled and explains how it operates during the process.

Materials List

Electronics Design

The design of the electronics started with the development of the first PCB prototype, in which the use of two A4988 controllers for the X and Y axis control was considered. For the Z axis, a 20-gram servo was chosen. Given the current consumption of the servo, it was necessary to incorporate a 5V regulator for its power supply. The main source of the system will be 12V, which will also be used to power the stepper motors.

The Seeed Studio XIAO ESP32-C3 was selected as the microcontroller, due to its compact size, ease of use and sufficient number of pins to handle all the inputs and outputs required by the system.

To allow physical control of the stepper motors and to establish a point of origin, five buttons configured in pull-up mode were added. Four of these buttons allow manual movement in the X and Y axes, while the fifth is intended to set the origin point of the system.

Descripción de la imagen 1 Descripción de la imagen 1

The SVG file including traces, drill holes and edge cuts was obtained. Subsequently, this file is placed in the SMR-20 CNC for processing.

Once the CNC completed its task, the components were soldered into place. However, no which was affected by the faulty regulator. An attempt was made to use another 5V regulator, specifically the AMS 1117 model with an SOT-223 package, but it was limited to a capacity of 1A. Consequently, a search was conducted for an integrated circuit capable of supporting at least 1.5A, leading to the discovery of one with a D2PAK package. Nevertheless, its significantly larger size necessitated the redesign of the PCB to accommodate this new component.

Descripción de la imagen 1
Descripción de la imagen 1

"After soldering all the components, the PCB presented an aesthetically pleasing appearance. With everything properly assembled, testing was initiated to ensure functionality and performance.

Descripción de la imagen 1

"There were initial issues with the soldering, as it did not fully contact the traces, likely due to the absence of soldering paste. After addressing these problems, current consumption was successfully observed on the power supply. Subsequently, code was uploaded from the Arduino IDE to the XIAO C3 microcontroller, which enabled the step motors and servo to move from right to left. All functions, including the buttons, worked flawlessly during testing.

The design and development of the electronic system was successful, managing to integrate the A4988 controllers, the servo and the Seeed Studio XIAO ESP32-C3 microcontroller on a functional PCB. Despite the challenges with the 5V regulator, a suitable solution was found that allowed the system to operate correctly. Final testing confirmed that all components and functions, including the control buttons, operate efficiently, meeting the project objectives.

CNC Mechanical Parts

For the parts used within our CNC, there are some that we did not design ourselves because they are components that were purchased. Therefore, their 3D designs are available online, which are:

For the parts used within our CNC, there are some that we did not design ourselves because they are components that were purchased. Therefore, their 3D designs are available online:

Stepper Motor: Download 3D Model
Linear Bearing: Download 3D Model
MG995 Servo Motor: Download 3D Model
Timming Pulley 20T: Download 3D Model
Angle 20x20: Download 3D Model

Before continuing, it's best to show the assembled machine from the program

Descripción de la imagen 1

CNC Parts Description

Base

This is one of the two parts that join the base of the CNC. Their functions are to hold a pulley for the belt that moves the Y-axis carriage, to hold the two smooth rods for the same Y-axis carriage, and lastly, the most obvious function, to join the base profiles.

Base part image

Base Stepper

This part has exactly the same functions as the previous one, but with the difference that this one doesn't hold a pulley; it holds the entire stepper motor.

Base stepper part image

Base X

This part is for joining the profiles of the base square and the ones that hold the X-axis.

Base X part image

X-axis Carriage

As its name suggests, this is one of the two parts of the carriage that travels along the X-axis. It has 3 slots for linear bearings each, another 2 to insert the belt, which are tightened with screws, a hole between the bearing spaces so the belt can move freely, and a space on the top to place a servo motor.

Carrito X part image

X-axis Carriage 2

Being the second part of the carriage, it has the same slots except for the ones to hold the belt. On the other hand, it has two trapezoidal shapes on the back that act as rails so that our spindle moves up and down without wobbling.

Carrito X 2 part image

Servo Gear

This gear is the one that moves the spindle holder up and down.

Engrane Servo part image

Spindle Holder

As the name suggests, it holds the motor that rotates the spindle. It has a couple of tabs to tighten the motor and prevent it from rotating. On the back, as can be seen, it has 2 trapezoid-shaped holes that function as rails and has a rack so the gear can move it up and down.

Sujetador Husillo part image

Y-axis Carriage

The Y-axis carriage has four spaces to place the corresponding bearings. The hexagonal holes have no function beyond saving material, and that shape was chosen because it offers the best force distribution, similar to a honeycomb.

Carrito Y

Bed

This is the sacrificial bed for the CNC.

Bed

X Support

This is one of the two parts that support the X-axis. It holds the stepper motor, both smooth rods for the Y-axis, and is mounted on a profile with a couple of screws.

X Support

X Support 2

This is the counterpart of the previous part. It has the same functions but holds a pulley.

X Support 2

Trapezoid

The only function of this part is to be inserted into the profile slots and hold screws to attach parts to them. This is not visible in the machine assembly, as it’s not necessary to hold anything there, but it is used in the actual machine.

Trapezoid

Smooth Rod

This simulates a generic smooth rod. Since it wasn't hard to replicate, the file was not downloaded from the internet.

Smooth Rod

Machine Modifications

       

The following issues were identified and addressed:

       

Enclosure Design for Circuits

An enclosure designed in SolidWorks to protect circuits is primarily intended to provide a safe environment for electronic components, protecting them from external factors such as impacts, moisture, or dust.

The design process begins with the creation of a solid base, followed by sidewalls and a lid. These parts are crafted to be practical and ensure precise assembly for optimal protection.

Strategic openings allow for cable connections and ensure proper ventilation, essential for heat-generating circuits. Additionally, the design is optimized through the use of tight tolerances and features like rounded edges and internal supports to enhance structural integrity.

This enclosure can be manufactured using 3D printing or materials such as ABS, combining functionality and simplicity. In this way, the safety and durability of the components are guaranteed, contributing to the proper functioning of the circuit in various environments.

Smooth Rod Example 2

Programming

Initially, efforts were focused on interpreting G-codes through the Arduino IDE. To achieve this, research was conducted to understand G-code commands and their interpretation, utilizing resources like Marlin G-code Reference and HowToMechatronics G-code Guide.

A Python script was developed to transmit each line of code from a file containing G-codes. Challenges arose, such as the data being sent all at once or the motors moving excessively.

Further investigation revealed that GRBL was commonly used with Arduino and ESP32 shields for transmitting G-codes, alongside Universal G-code Sender (UGS). To streamline operations, a XIAO ESP32 C3 was selected. The process seemed straightforward, requiring the download of a zip file from Grbl_Esp32 GitHub Repository. However, compilation errors occurred in the Arduino IDE, likely due to the library's incompatibility with this specific board.

Alternative tools like VS Code and Platform IO were explored, though various issues persisted. External guidance was sought from ChatGPT, which suggested creating a custom library. This approach facilitated the development of a "barebones" version of GRBL. The library was created within a new project in VS Code using the Platform IO extension.

Testing the initial version revealed connectivity issues with UGS, as the code needed to send an "ok" signal after receiving each 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, we were able to successfully connect and send a G-code to our board.
    
    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"); //allows empty lines  
          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");
          }
        }
      }
    }
        

To ensure accurate motion and scaling, the motor steps were converted into millimeters. It was calculated that the motors required 200 steps to complete a full rotation. For both the X and Y axes, a belt-driven system with a pulley of 14mm radius was employed, resulting in the following calculation: steps_per_mm = 200.0 / (π * 14.0) ≈ 4.55. As the machine was built without limit switches, it was necessary to restrict the number of steps the machine could move. The X-axis was limited to 800 steps, and the Y-axis to 1,000 steps.

To enhance functionality, four buttons were integrated into the PCB: two for controlling each axis and an additional button to set the origin. After the conversion from steps to millimeters was finalized, code support for the buttons was implemented.

        
        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(); 
        } 
        
        
Descripción de la imagen 1

After testing different configurations, it was determined that the origin should be set every time to prevent collisions. For the X axis, the origin was positioned close to the motor, whereas for the Y axis, it was set farther from the motor. Regarding the Z axis, a servomotor was implemented to enable up/down functionality. Initially, the movement was programmed using commands such as G0 Z0 and G0 Z10. However, issues arose when the G0 Z10 command was sent repeatedly; the program mistakenly assumed the motor was at Z20, requiring a manual adjustment with Z-20 to return to Z0. This problem occurred due to the Z axis being programmed similarly to the X and Y axes.

A 360° servomotor was utilized, and attempts were made to map its full range of motion. However, due to the limitations of the ESP32 servomotor library, the motor's movement was restricted to 0°–180°. Despite these constraints, the issue with the command logic was successfully resolved.

Map from 0-10 → 0-360 degrees

        
            float z_target = 5.0; // Example input value between 0 and 10
            
            // Limit the value between 0 and 10
            float clamped = constrain(z_target, 0.0, 10.0);  // Constrains the value between 0 and 10
            
            // Map the value from 0-10 to 0-360 degrees
            int angle = map(clamped * 100, 0, 1000, 0, 360);  
            
            // Print the angle to the serial monitor
            Serial.printf("↕ Moving Z (servo) to angle: %d°\n", angle); 
            
            // Move the servo to the calculated angle
            z_servo.write(angle); 
            
            // Small delay to complete the movement
            delay(500);  
            
     

With all the modifications in place, we proceeded to upload our first complete G-code to test the functionality of the system. A text file containing the G-code for drawing a pentagon was prepared and opened in Universal G-code Sender (UGS). The G-code was generated by importing a PNG image into mods. After setting the origin, the play button was pressed to initiate the process. However, an error occurred, and its cause was not immediately apparent, despite being able to successfully send data through the console window.

Descripción de la imagen 1

During the test, the servomotor had not yet been attached to the CNC. To assess functionality, a pen was taped in place as a temporary solution. The complete G-code was pasted into the console window, and the machine began to move. Here is the result of what it drew

Descripción de la imagen 1

It became evident that adjustments to the code were necessary, as the output did not align with the intended result. Below is the reference for how the final design should appear.

Descripción de la imagen 1

To better identify the issue, a simplified version of the pentagon was used. It became apparent that the problem stemmed from the speed at which the stepper motors operated. Since both motors moved simultaneously, one could complete its motion before the other, resulting in incorrect movements. To address this, assistance was sought to modify the code's logic, ensuring proper synchronization of the stepper motors. Below is the updated segment of code that corrected the motors' movement:

Calculate Step Deltas and Control Motors


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. Here´s the zip file containing the final library we created:

Dowload Grbl_barebones.zip

It might have been a better option to refine the first code developed to interpret G-codes; however, this was the path that led to the CNC being fully operational. Greater progress was achieved by following this approach compared to the other one.

The support for UGS is not complete, as the machine cannot be controlled through the buttons in the UGS interface, and the error that occurred when trying to run a file was not resolved. Nonetheless, UGS allowed for sending the code from the console and automatically verifying each action before proceeding to the next one. Additionally, it provided an interface offering a preview of the G-code and the real-time position of the axes.

Although this library was designed with the XIAO ESP32 C3 in mind, it can be used with other boards. The only required modifications are the servo library, pin configuration, and the platform.ini file.

Improvements to Make