May 31, 2025
Description
About a year ago, I published the Proxxon KT70 PCB CNC Micro Mill—a compact and precise CNC machine for milling PCBs and small parts. It quickly became a valuable tool in my workshop.
However, I often found myself wishing for a more hands-on solution—something that allowed for quick adjustments or simple manual operations without needing to go through the full CNC workflow. That’s what led me to create the µMill: a hybrid manual micro mill with digital position readout, compact electronics, and user-friendly control.
Smooth and accurate manual movement along the X and Y axes, measured using high-resolution rotary encoders and displayed on a 128x64 LCD screen. Both axes can be zeroed independently using dedicated buttons—perfect for relative measurements and repositioning.
The top-mounted potentiometer adjusts the spindle speed, with the expected RPM shown on screen. The spindle is activated via a green button.
A red safety button immediately disables the motor and shorts the spindle wires to quickly brake the rotation—offering both convenience and peace of mind.
The µMill is powered by a custom Arduino-based controller, which handles user input, spindle control, and position tracking. The schematic can be found in documentation folder. To have GRBL control, to operate as a CNC you just need an additional Arduino UNO with the CNC shield, 3 stepper motors and stepper drivers.
AS5600 encoders require magnets to read rotation. I have added print files to make it easier to place the magnets exactly at the center of the stepper motor shafts. Use epoxy and leave it overnight.
Here are a few PCB projects designed and milled with this setup:
Optocoupler Endstop Filter
First Version of Manual Control PCB (failed)
I hope this project inspires others to build their own µMill. It was a fun and rewarding challenge, and it turned out to be more useful than I expected—especially for quick manual tasks with precise positioning.
If there’s enough interest, I’d be happy to improve and expand the project further with the community. Possible future upgrades could include:
If you run into any issues or have questions about the build, feel free to ask—I'll do my best to help and explain everything clearly.
For now, I'm sharing the firmware code directly below.
#include <Wire.h>
#include <AS5600.h>
#include <U8g2lib.h>AS5600 as5600; // Object for AS5600 sensor
// Initialize 128x64 LCD (SPI), rotated 270°
U8G2_ST7920_128X64_1_HW_SPI u8g2(U8G2_R3, /* CS=*/ 10, /* RST=*/ 9);#define TCA9548A_ADDRESS 0x70 // I2C address of the multiplexer
#define RESET_X_PIN 3 // Button to reset X axis
#define RESET_Y_PIN 4 // Button to reset Y axis
#define PWM_PIN 5 // PWM output (motor control)
#define POT_PIN A0 // Potentiometer input// Position tracking variables
float total_mm_x = 0.0;
float total_mm_y = 0.0;
float last_angle_x = 0.0;
float last_angle_y = 0.0;
float reference_angle_x = 0.0;
float reference_angle_y = 0.0;
int revolutions_x = 0;
int revolutions_y = 0;// Timing control
unsigned long last_sensor_read = 0;
unsigned long last_display_update = 0;
const unsigned long SENSOR_INTERVAL = 20; // Sensor read interval (ms)
const unsigned long DISPLAY_INTERVAL = 100; // LCD update interval (ms)// Select a channel on the TCA9548A multiplexer
void tcaSelect(uint8_t channel) {
if (channel > 7) return;
Wire.beginTransmission(TCA9548A_ADDRESS);
Wire.write(1 << channel);
Wire.endTransmission();
}void setup() {
Serial.begin(9600);
Wire.begin();
Wire.setClock(400000); // Set I2C speed to 400kHz// Initialize LCD
u8g2.begin();
u8g2.setFont(u8g2_font_profont12_tf);
u8g2.clearBuffer();
u8g2.drawStr(0, 10, "Initializing...");
u8g2.sendBuffer();// Configure pins
pinMode(RESET_X_PIN, INPUT_PULLUP);
pinMode(RESET_Y_PIN, INPUT_PULLUP);
pinMode(PWM_PIN, OUTPUT);
pinMode(POT_PIN, INPUT);// Initialize AS5600 sensor for X axis
tcaSelect(0);
if (as5600.begin()) {
reference_angle_x = as5600.rawAngle() * 0.087890625;
last_angle_x = 0.0;
} else {
Serial.println(F("Error: X axis sensor not detected"));
}// Initialize AS5600 sensor for Y axis
tcaSelect(1);
if (as5600.begin()) {
reference_angle_y = as5600.rawAngle() * 0.087890625;
last_angle_y = 0.0;
} else {
Serial.println(F("Error: Y axis sensor not detected"));
}
}void loop() {
unsigned long current_time = millis();// Reset X axis if button pressed
if (digitalRead(RESET_X_PIN) == LOW) {
tcaSelect(0);
total_mm_x = 0.0;
revolutions_x = 0;
reference_angle_x = as5600.rawAngle() * 0.087890625;
last_angle_x = 0.0;
delay(50);
while (digitalRead(RESET_X_PIN) == LOW);
}// Reset Y axis if button pressed
if (digitalRead(RESET_Y_PIN) == LOW) {
tcaSelect(1);
total_mm_y = 0.0;
revolutions_y = 0;
reference_angle_y = as5600.rawAngle() * 0.087890625;
last_angle_y = 0.0;
delay(50);
while (digitalRead(RESET_Y_PIN) == LOW);
}// Read AS5600 sensors every 20 ms
if (current_time - last_sensor_read >= SENSOR_INTERVAL) {
tcaSelect(0);
float current_angle_x = as5600.rawAngle() * 0.087890625;
float relative_angle_x = current_angle_x - reference_angle_x;
float delta_angle_x = relative_angle_x - last_angle_x;
while (delta_angle_x > 180.0) {
revolutions_x--;
delta_angle_x -= 360.0;
}
while (delta_angle_x < -180.0) {
revolutions_x++;
delta_angle_x += 360.0;
}
total_mm_x = revolutions_x + (relative_angle_x / 360.0);
last_angle_x = relative_angle_x;tcaSelect(1);
float current_angle_y = as5600.rawAngle() * 0.087890625;
float relative_angle_y = current_angle_y - reference_angle_y;
float delta_angle_y = relative_angle_y - last_angle_y;
while (delta_angle_y > 180.0) {
revolutions_y--;
delta_angle_y -= 360.0;
}
while (delta_angle_y < -180.0) {
revolutions_y++;
delta_angle_y += 360.0;
}
total_mm_y = revolutions_y + (relative_angle_y / 360.0);
last_angle_y = relative_angle_y;last_sensor_read = current_time;
}// Read potentiometer and compute PWM and expected RPM
int pot_value = analogRead(POT_PIN); // Value from 0 to 1023
int pwm_value = map(pot_value, 0, 1023, 0, 255);
analogWrite(PWM_PIN, pwm_value);// Map potentiometer value to expected RPM (0 to 10,000)
unsigned long expected_rpm = map(pot_value, 0, 1023, 0, 10000);// Update display every 100 ms
if (current_time - last_display_update >= DISPLAY_INTERVAL) {
u8g2.firstPage();
do {
char buffer[20];
char value_str[8];// Display expected RPM
snprintf(buffer, sizeof(buffer), "RPM: %lu", expected_rpm);
u8g2.drawStr(0, 10, buffer);// Display X axis
dtostrf(total_mm_x, 6, 2, value_str);
snprintf(buffer, sizeof(buffer), "X:%s mm", value_str);
u8g2.drawStr(0, 48, buffer);// Display Y axis
dtostrf(total_mm_y, 6, 2, value_str);
snprintf(buffer, sizeof(buffer), "Y:%s mm", value_str);
u8g2.drawStr(0, 85, buffer);// µMill title
u8g2.drawStr(36, 125, "\xB5Mill");} while (u8g2.nextPage());
last_display_update = current_time;
}
}
https://docs.google.com/spreadsheets/d/16nbs9V1qnrR16r7QubPuwHfgM05_ZPlatjmNZF1QJMQ/edit?usp=sharing
The assembly process is largely self-explanatory through the provided images and animations. However, important notes and additional explanations are included to clarify the more critical steps.
During the build, I practiced exporting GIF animations from SolidWorks and putting together simple work instructions — I hope this will make everything as clear as possible.
💡 Tip: If anything seems unclear or you're missing a specific step, feel free to check the labeled images or reach out!
🧩 Start of Assembly
Start by inserting the bearings into their seats, then add the nuts and tighten everything with M3 screws; finally, prepare the motor mount and fasten it using two M5 screws and nuts, at the end fasten the T8 nut (see GIF bellow):
Assemble the Z-axis by attaching the motor mount to the carriage, adding T-nuts and bolts, then installing the leadscrew and linear rods:
make sure to use a shaft collar to relieve stress on the motor bearings*
Assemble all individual components together (see the GIF below), and don’t forget to add the two extra T-nuts as shown in the warning image.
Assemble the Z-axis endstop holder and install it right away, but don’t fully tighten it so you can adjust it later; tighten it firmly only after setting the trigger point in relation to the spindle mount (there’s limited space, but it works). Also add the Z top holder.
Again don’t forget to add the shaft collar at the top end of the leadscrew.
Assemble the back panel, which is easiest to 3D print with 100% infill, and then drill holes for each power supply – this part is still a work in progress.
Mount the AS5600 sensor and its holder onto the stepper motor, and insert all the required T-nuts at the same time (YOU NEED 8 not 6 im sorry :) )
Install the electronics enclosure and attach the side panels.
Assemble the X-axis mount:
You can use M3 nuts, and M3x30 mm bolts for better strenght (
instead of M3x20).
Use heat inserts for this part:
Simply insert the X-axis mount into the slots of the Proxxon table, tighten the motor coupler onto the table’s leadscrew – this completes the mechanical assembly for CNC operation:
For manual readout functionality, a small modification is needed – replace the two M5 screws (7 mm long) with M5 screws of 15 mm length:
Assemble the enclosure for the Arduino and all associated electronics, and mount it in the designated position:
This completes the assembly!
License:
BY-NC-SA