µMill – Manual Micro CNC Mill with Digital Readout
Obliczymy ostateczną cenę na podstawie wybranego pliku. Sprawdzisz ją i zaakceptujesz przed płatnością.
Zapisane na tym urządzeniu.
Upgraded Proxxon KT70 PCB CNC Micro Mill, with manual control and better reliability
- Ocena
- 0 / 5
- Polubienia
- 32
- Pobrania
- 86
- Wydruki
- 0
- Wyświetlenia
- 1006
- Oceny
- 0
Aktualizacja u źródła:
Informacje o modelu
- Źródło
- Printables
- Kategoria
- Tools
- Licencja
- Creative Commons — Attribution — Noncommercial
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, while still being capable of CNC operations.
🔧 Key Features
Manual XY Control with Digital Readout
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.
Spindle Speed Control and Display
The top-mounted potentiometer adjusts the spindle speed, with the expected RPM shown on screen. The spindle is activated via a green button.
Emergency Stop System
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.
🛠 Applications
- Manual PCB edits and corrections
- Milling small plastic or aluminum parts
- Deburring, engraving, and surface cleanup
- Learning platform for working with digital readouts (DRO)
💡 Electronics & Firmware Overview
The µMill is powered by a custom Arduino-based controller, which handles user input, spindle control, and position tracking.
Components:
- Arduino Nano (or compatible) – main microcontroller
- 2× AS5600 magnetic encoders – for X and Y position sensing
- 128×64 SPI LCD display – shows RPM and live XY positions
- 10k potentiometer – sets spindle speed and displays RPM
- TIP120 transistor – drives the spindle with PWM
- Red Emergency Stop – disables and brakes the spindle
- Green Spindle Enable – turns on spindle at set speed
- Black Reset Buttons – zero the X or Y axis individually
- DC Spindle Motor – controlled via PWM
Firmware Features:
- Real-time Measurement:
High-resolution rotary encoders provide smooth, accurate displacement readings. Encoder data is filtered in software and updated every 20ms. - Graphical Interface:
The LCD shows:- Estimated spindle RPM
- X and Y displacement (in mm)
- µMill logo
- Speed Control:
The analog value from the potentiometer is mapped to PWM output and to a live RPM estimate on screen. - Emergency Safety:
The red stop button both cuts power and actively brakes the motor for safe and fast stopping.
🧪 Projects Made with µMill
Here is a PCB project designed and milled with the CNC setup:
Optocoupler Endstop Filter:

💬 Final Thoughts
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:
- Z-axis support
- Custom PCB with all components
- Detailed assembly and wiring guide
- Public GitHub repository
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;
}
}
📦BOM:
https://docs.google.com/spreadsheets/d/16nbs9V1qnrR16r7QubPuwHfgM05_ZPlatjmNZF1QJMQ/edit?usp=sharing