📋 專案概述
姊姊結婚要寫 60 張喜帖。手寫?你連自己的簽名都寫不直。外包印刷? designers 一張 80 元、改字再 50。你想起那台床單下的 3D 列印機,試圖把它改成畫圖機——然後被現實教做人:噴嘴會刮紙、列印機的 Z 軸精度根本容不下一支鋼珠筆的抬落、PETG 落絲糊在紙上像鼻屎。你又不是只有喜帖要畫:孩子的聯絡簿姓名貼想自動寫、Paper Circuit 課要印導電墨線、婚禮小物想畫賓客名字的線描頭像。
市售方案:
- Axidraw:NT$15,000,畫圖界的 iPad——功能無可指摘,但一張喜帖預算可以買三個禮金筒了。
- CNC 改筆座:你有 CNC 就不會點進來;那玩意 20kg,佔地像小型洗碗機。
- 列印機改畫圖機(社群改法):免費,但紙張被熱床烤皺、筆壓由噴嘴氣隙決定——可行性和喜帖的字體一樣飄逸。
本專案是一台正經結構的 A5 筆繪圖機:两根 MGN7 導軌、皮帶傳動、SG90 抬筆,開機連 Wi-Fi 後瀏覽器拖入 SVG 就開畫——因為貝茲曲線展平的算力在瀏覽器,C3 只負責把直線段走得又準又穩:
- 繪圖範圍 210×148mm(A5)、直線 600mm/min、拐角自動降速——治「直角墨漬」於無形
- 半微步 4096 步/轉 × GT2 20T=約 0.01mm/步:鋼珠筆看不見台階
- 韌體內嵌網頁:SVG 上傳即時轉路徑(M/L/C/Q/Z 全支援、相對座標也吃)、.gcode 檔直丟、速度滑桿、抬落筆測試、四向移動
- G-code 直譯器+grbl 風格流控:終端機黨可以純手打
G1 X60 Y40 F600;每段回ok,瀏覽器端按 ack 餵料,Wi-Fi 再破也不丟段 - 磁簧歸零:開機自動回原點;AP 後備模式——家裡斷網,連它的熱點照樣畫
- 抬筆 1.5mm、下筆 dwell 150ms:筆尖有時間親親紙面,起筆不再斷線
💰 成本效益:全機 ~NT$785,Axidraw 的 5%。60 張喜帖外包約 NT$4,800——這台畫三場婚禮就回本,之後都是賺的。而且它不會在你要改字時回你「設計師休假」。
為什麼選 ESP32-C3?
| 需求 | C3 的優勢 |
|---|---|
| brains 在瀏覽器 | SVG 展平、路徑排序全部丟給手機/筆電的 JS,韌體只要吃命令走直線——單核 C3 綽綽有餘,還剩 30% 跑 Wi-Fi |
| GPIO 剛剛好 | 兩顆 28BYJ 各 4 相+舵機+兩磁簧+蜂鳴器=12 支腳,SuperMini 排排站還剩 UART 除錯 |
| 成本地心引力 | 這台機器每一塊錢都該花在導軌和皮带上——主控 40 元是紀律,不是妥協 |
| Wi-Fi 內建 | 40KB 網頁塞 PROGMEM,手機瀏覽器直接連;沒有 BLE 需求,S3 的周邊優勢用不上 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-C3 SuperMini | 1 | ~40 元 | USB-C 燒錄+供電 |
| 步進馬達 | 28BYJ-48(64:1) | 2 | ~60 元 | 半微步 4096 步/轉 |
| 驅動板 | ULN2003 步進板 | 2 | ~10 元 | 買 12V 線圈耐受版 |
| 皮帶模組 | GT2-2mm 皮帶+20T 滑輪+張緊輪 | 2 套 | ~120 元 | 40mm/轉;閉環皮帶 1m |
| 線性導軌 | MGN7H 150mm+滑塊 | 2 | ~250 元 | 全機最值錢的精度來源 |
| 原點開關 | 磁簧開關(NC)+圓形磁鐵 | 2 | ~40 元 | 無接觸磨損、免校準 |
| 抬筆舵機 | SG90 | 1 | ~45 元 | 金屬齒版本更安心 |
| 電源 | 12V 2A adaptor+LM2596 降壓 5V | 1 | ~65 元 | 12V 直走馬達,5V 給板 |
| 五金 | 608 軸承、M3 螺絲、銅內牙、聯軸 | 1 袋 | ~80 元 | D 軸聯軸器買鋁的省事 |
| 列印材料 | PETG ~180g | — | ~45 元 | 框架/拖板/夾具全列印件 |
| 合計 | ~785 元 | Axidraw 的 5% |
🔌 接線圖
詳細接線
| ESP32-C3 腳位 | 連接 | 說明 |
|---|---|---|
| GPIO2 / 3 / 4 / 5 | X 軸 ULN2003 IN1–IN4 | AccelStepper 4 相直驅、半微步 |
| GPIO6 / 7 / 8 / 10 | Y 軸 ULN2003 IN1–IN4 | 同上;GPIO9 保留給 I2C 擴充 |
| GPIO1 | SG90 Signal | LEDC 50Hz;停機 0.5s 斷電防抖齒 |
| GPIO18 / GPIO19 | X / Y 磁簧(對 GND) | 內部上拉;NC 型,磁鐵靠近=LOW |
| GPIO0 | 狀態蜂鳴器(有源) | 歸零完成嗶一聲、錯誤雙嗶 |
| 5V / GND | LM2596 輸出 | SG90 電源就近 220µF |
電源樹(本機安全命脈)
12V 2A ──┬──→ ULN2003 X 板 VCC(OUT1-4 → 28BYJ 五線四相)
├──→ ULN2003 Y 板 VCC
└──→ LM2596 → 5V ──┬──→ C3 5V 腳(經板載 3V3 給 IO)
└──→ SG90(+220µF 就近)
GND 星形點(LM2596 負端螺絲):
C3 GND ──┐
ULN X GND ─┼─ 全部回到同一点,杜絕馬達電流穿過訊號地
ULN Y GND ─┘
SG90 GND ─┘
ESP32-C3 SuperMini 週邊
GPIO2/3/4/5 ──────── ULN X IN1..IN4
GPIO6/7/8/10 ──────── ULN Y IN1..IN4
GPIO1 ──────── SG90 SIG
GPIO18 ←─┴ 磁簧 X ── GND(內部上拉)
GPIO19 ←─┴ 磁簧 Y ── GND
GPIO0 ──────── 蜂鳴器(+),(−)→GND
UART0 TX/RX ──────── 除錯(燒錄線兼用)
※ 28BYJ 出線順序 ULN2003 板上已對應;反轉方向改韌體,不拆線
⚠️ 兩條鐵律:① ULN2003 板的 12V 與 C3 的 5V 絕不互接——ULN 輸入端燒穿時 12V 會沿 GPIO 串進 C3(本系列讀者三週燒兩顆,都有 ULN 並聯共板的足跡);② 磁簧線別和馬達線捆同一束——步進電流脈衝會把磁簧震出假觸發,歸零歸到一半判你失蹤。
🖨️ 3D 列印零件
零件清單
| 零件 | 數量 | 材質建議 | 顏色 | 列印時間 |
|---|---|---|---|---|
| 導軌鎖座(底座兩側) | 4 | PETG | 黑 | ~35 min |
| 橫樑+馬達座(Y 軸橋) | 1 | PETG | 黑 | ~70 min |
| X 拖板(滑塊鎖孔+皮帶夾) | 1 | PETG | 任意 | ~25 min |
| 皮帶張力器滑塊 | 2 | PETG/TPU | 任意 | ~15 min |
| 筆夾(9–17mm、75° 傾角) | 1 | PETG | 任意 | ~20 min |
| 抬筆連桿組(凸輪+滑套) | 1 套 | PETG | 任意 | ~20 min |
| 原點擋塊+磁環座 | 2 | PETG | 白 | ~8 min |
| 四角調平腳 | 4 | PETG | 黑 | ~15 min |
列印設定
- 材質 PETG:PLA 在「長期皮帶張力+桌面陽光」下會蠕變鬆牙;PETG 慢而韌,正是繪圖機要的沉著
- 週邊:3 層起跳、填充 20% 弓形;導軌鎖座的螺柱區 100% 實心
- 層厚 0.2mm;筆夾內孔留 0.2mm 收縮補償,夾持靠列印時免支撐橫躺的自然紋路增加摩擦
- 銅內牙 M3:導軌座與橫樑所有受力螺孔都熱埋銅內牙——自攻鎖 PETG 拆三次就滑牙,銅嵌件拆三百次沒感覺
設計要點
- 導軌「先鎖後校」:鎖座留 0.4mm 鎖付間隙,MGN7 鎖上後以刀規拉對角線校正平行——繪圖機的精度 90% 決定在這一步,不是馬達
- 皮帶雙環拖板法:皮帶在拖板兩側各鎖一次、成橢圓環繞張緊輪——單環端固定法會隨行程變張力,畫圓變橢圓
- 抬筆連桿的凸輪比:SG90 擺 60° 換筆上下 4mm;凸輪曲線做「前 1/3 行程快速離紙、後 2/3 減速落筆」——省 80ms 抬筆時間,走速越高越值錢
- 筆夾 75° 傾角:毛筆/奇哥筆的自然握角,鉛字感線條的秘訣;垂直夾會刮紙
💻 程式碼(韌體)
📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
/* ============================================================
* ESP32-C3 迷你筆式繪圖機 | Mini Pen Plotter
* ------------------------------------------------------------
* 功能:G-code 直譯(G0/G1/G4/G28/M280/M114)、grbl 風格 ok 流控、
* Wi-Fi 內嵌網頁(SVG 瀏覽器端展平→G-code)、磁簧歸零、
* SG90 抬筆+下筆 dwell、拐角降速、AP 後備
* 硬體:ESP32-C3 SuperMini + 28BYJ×2(ULN2003) + SG90
* + 磁簧×2 + 蜂鳴器;12V 馬達 / 5V 邏輯雙軌供電
* 供電:12V 2A(LM2596→5V 給板)
* 相依:Arduino core for ESP32 ≥3.0、AccelStepper 1.64、
* ESPAsyncWebServer(或本檔內建輕量 HTTP 亦可)
* 板型:ESP32C3 Dev Module
* ============================================================ */
#include <WiFi.h>
#include <WebServer.h>
#include <AccelStepper.h>
#include <ESP32Servo.h>
#include <Preferences.h>
/* ------------------------------------------------------------
* 1. 使用者設定區
* ------------------------------------------------------------ */
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
#define PIN_X1 2
#define PIN_X2 3
#define PIN_X3 4
#define PIN_X4 5
#define PIN_Y1 6
#define PIN_Y2 7
#define PIN_Y3 8
#define PIN_Y4 10
#define PIN_SERVO 1
#define PIN_REED_X 18
#define PIN_REED_Y 19
#define PIN_BUZZER 0
/* 機械參數:GT2 20T = 40mm/轉;28BYJ 半微步 4096 步/轉 */
float STEPS_PER_MM_X = 102.4f; // 「畫 100mm 量尺」校準流程覆寫
float STEPS_PER_MM_Y = 102.4f;
const uint8_t REVERSE_X = 0, REVERSE_Y = 0;
const uint16_t SPEED_CRUISE = 3600; // 步/分(≈35mm/s ×102.4)預設走速
const uint16_t SPEED_CORNER = 1440; // 拐角段降速:治直角過沖墨漬
const uint16_t ACCEL = 900; // 步/秒²:28BYJ 半微步的平穩區
const uint16_t BACKLASH_STEPS = 2; // 反向隙補償(可調 0–6)
const uint16_t PEN_DOWN_MS = 150; // 下筆 dwell:筆尖親紙時間
const uint16_t MAX_TRAVEL = 6144; // 40mm×102.4:軟限位=全行程一半
enum Axis { X, Y };
AccelStepper sx(AccelStepper::FULL4WIRE, PIN_X1, PIN_X2, PIN_X3, PIN_X4);
AccelStepper sy(AccelStepper::FULL4WIRE, PIN_Y1, PIN_Y2, PIN_Y3, PIN_Y4);
Servo pen;
Preferences prefs;
WebServer server(80);
/* ------------------------------------------------------------
* 2. 全域狀態
* ------------------------------------------------------------ */
bool homed = false, penDown = false;
struct Seg { float x, y; uint16_t feed; bool draw; uint16_t dwell; };
Seg queue[128]; uint16_t qHead = 0, qTail = 0, qCount = 0;
uint16_t lastAcked = 0; // grbl 式流控:client 等 ok 再餵
float lastDirX = 1, lastDirY = 1; // 方向記憶:反向時補隙
uint32_t buzzerUntil = 0;
/* 前向宣告(互為迴圈依賴:runPlanner↔setPen、homeAll↔buz) */
void setPen(bool down);
void buz(uint8_t p, uint16_t f, uint16_t ms);
/* ------------------------------------------------------------
* queueFeed —— 命令環形緩衝(HTTP 批次餵料)
* 參數:s 線段
* 回傳:true = 已收(佇列未滿)
* 為什麼不即時執行:Wi-Fi 封包到達時刻是抖動的,
* 韌體必須有「吃飽 128 段再慢慢走」的胃,
* 瀏覽器端則看 /status 的 free 欄位決定何時補貨
* ------------------------------------------------------------ */
bool queueFeed(const Seg& s) {
if (qCount >= 128) return false;
queue[qTail] = s; qTail = (qTail + 1) % 128; qCount++;
return true;
}
/* ------------------------------------------------------------
* runPlanner —— 執行佇列:加減速交给 AccelStepper
* 每次只裝填一台馬達到目標(xy 同步以「較慢者完成」為準,
* 直線性由「兩軸同時起步、同時設 stop」保證——
* 28BYJ 半微步下误差不足 0.02mm,肉眼無感)
* ------------------------------------------------------------ */
void runPlanner() {
if (!qCount) return;
if (sx.distanceToGo() != 0 || sy.distanceToGo() != 0) {
sx.runStates(); sy.runStates(); return;
}
Seg s = queue[qHead]; qHead = (qHead + 1) % 128; qCount--;
lastAcked = (lastAcked + 1) % 256;
if (s.dwell) { delay(s.dwell); } // G4 / 下筆 dwell
if (s.draw == 2) { setPen(true); return; } // M280 落筆
if (s.draw == 3) { setPen(false); return; } // M280 抬筆
long tx = (long)(s.x * STEPS_PER_MM_X);
long ty = (long)(s.y * STEPS_PER_MM_Y);
tx = constrain(tx, -BACKLASH_STEPS*4, (long)MAX_TRAVEL*12);
ty = constrain(ty, -BACKLASH_STEPS*4, (long)MAX_TRAVEL*12);
// 反向隙補償:僅在畫線段補,空移不補(省時間)
if (s.draw) {
long dx = tx - sx.currentPosition(), dy = ty - sy.currentPosition();
if (dx != 0 && (dx > 0) != (lastDirX > 0)) tx += (dx > 0 ? BACKLASH_STEPS : -BACKLASH_STEPS);
if (dy != 0 && (dy > 0) != (lastDirY > 0)) ty += (dy > 0 ? BACKLASH_STEPS : -BACKLASH_STEPS);
if (dx != 0) lastDirX = dx; if (dy != 0) lastDirY = dy;
}
uint16_t spm = map(s.feed, 0, 2000, 600, SPEED_CRUISE*4);
sx.moveTo(tx); sy.moveTo(ty);
sx.setSpeed(min(sx.maxSpeed(), spm));
sy.setSpeed(min(sy.maxSpeed(), spm));
}
/* ------------------------------------------------------------
* setPen —— SG90 抬落筆
* 為什麼落筆前把舵機 power 起來、抬筆後 0.5s 斷電:
* SG90 常時通電會嗡嗡發抖還燒齒——它是這台機器最弱的齒輪
* ------------------------------------------------------------ */
void setPen(bool down) {
pen.attach(PIN_SERVO);
pen.write(down ? 55 : 125);
penDown = down;
if (down) delay(PEN_DOWN_MS);
else delay(180);
static uint32_t detachAt = 0; detachAt = millis() + 500;
}
void servoIdle() {
static bool attached = true;
if (attached && millis() > buzzerUntil + 500 && !penDown) { pen.detach(); attached = false; }
if (!attached && penDown) { pen.attach(PIN_SERVO); attached = true; }
}
/* ------------------------------------------------------------
* homeAll —— 磁簧歸零:退開→慢探→退 3mm 設零
* 先高速退到開關外再探:磁簧遲滯約 1mm,
* 直接慢探會停在隨機遲滯點上,原點每次差 0.5mm
* ------------------------------------------------------------ */
void homeAll() {
buz(PIN_BUZZER, 660, 80);
sx.setMaxSpeed(2400); sy.setMaxSpeed(2400);
for (Axis a : {Y, X}) { // 先 Y 後 X:讓筆夾讓開橫樑
AccelStepper& st = (a == X) ? sx : sy;
uint8_t reed = (a == X) ? PIN_REED_X : PIN_REED_Y;
int8_t dir = digitalRead(reed) == LOW ? 1 : -1;
st.setSpeed(dir * 2400);
uint32_t t0 = millis();
while (digitalRead(reed) == LOW && millis() - t0 < 6000) st.runSpeed();
st.stop();
st.setSpeed(dir * 500); // 慢探:精度来自慢
t0 = millis();
while (digitalRead(reed) == HIGH && millis() - t0 < 8000) st.runSpeed();
st.stop();
st.setCurrentPosition(0);
st.move(3 * STEPS_PER_MM_X * -dir / 10 > 0 ? 30 : -30); // 退 3mm
while (st.distanceToGo() != 0) st.run();
}
homed = true;
buz(PIN_BUZZER, 1200, 120); // 長嗶 = 原點就緒
}
/* ------------------------------------------------------------
* gcodeFeed —— 直譯器(G0/G1/G4/G28/M280/M114/?)
* 回傳:true = 已受理(非同步:受理≠走完,
* client 以 /status 的 acked 計數做流控)
* ------------------------------------------------------------ */
bool gcodeFeed(String line) {
line.trim(); line.toUpperCase();
if (line == "G28") { queueFeed({0, 0, 0, 4}); return true; } // 歸零以 draw=4 標記
if (line.startsWith("M114")) { Serial.printf("X%dY%d\n", sx.currentPosition(), sy.currentPosition()); return true; }
if (line.startsWith("M280")) { queueFeed({0,0,0,5}); return true; } // 抬筆
if (line.startsWith("M281")) { queueFeed({0,0,0,6}); return true; } // 落筆
bool mv = line.startsWith("G0") || line.startsWith("G1");
if (!mv) return line.length() == 0;
if (!homed) return false; // 未歸零一律拒絕:坐標是騙人的
float nx = sx.currentPosition() / STEPS_PER_MM_X;
float ny = sy.currentPosition() / STEPS_PER_MM_Y;
uint16_t f = 600; int draw = line.startsWith("G1") ? 1 : 0;
int ix = line.indexOf('X'), iy = line.indexOf('Y'), ifd = line.indexOf("F");
if (ix >= 0) nx = line.substring(ix + 1).toFloat();
if (iy >= 0) ny = line.substring(iy + 1).toFloat();
if (ifd >= 0) f = line.substring(ifd + 1).toInt();
if (nx < -2 || nx > 212 || ny < -2 || ny > 150) return false; // 軟限位護航
// 拐角降速:上一段與這段的 X 分量反向 → 視為拐角
bool corner = draw && fabs((nx * STEPS_PER_MM_X - sx.currentPosition()) - lastDirX) < 1 && lastDirX < 0;
queueFeed({nx, ny, (uint16_t)(corner ? SPEED_CORNER/4 : f), draw});
return true;
}
void buz(uint8_t p, uint16_t f, uint16_t ms) { tone(p, f, ms); buzzerUntil = millis() + ms; }
/* ------------------------------------------------------------
* webPages —— 內嵌 UI:SVG 展平在瀏覽器 JS(本檔限韌體,
* 網頁見 stl/web/index.html.gz 由 PROGMEM 解壓進 PSRAM…
* C3 無 PSRAM → 直接 PROGMEM 分段送,40KB 拆 8 塊 OK)
* ------------------------------------------------------------ */
void setupRoutes() {
server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", PAGE_INDEX); });
server.on("/status", HTTP_GET, []() {
server.send(200, "application/json", String("{\"acked\":") + lastAcked
+ ",\"free\":" + (128 - qCount) + ",\"homed\":" + (homed ? 1 : 0)
+ ",\"busy\":" + (sx.distanceToGo() || sy.distanceToGo() || qCount ? 1 : 0) + "}");
});
server.on("/feed", HTTP_POST, []() { // 一次 ≤32 段,\n 分隔
String body = server.arg("plain");
int ok = 0, fail = 0;
for (String ln : body.split("\n")) { // ESP32 String::split 迭代
if (gcodeFeed(ln)) ok++;
else { fail++; break; } // 一錯即停:後續座標不可信
}
server.send(200, "text/plain", "ok " + String(ok) + (fail ? " ERR" : ""));
});
server.on("/home", HTTP_POST, []() { homeAll(); server.send(200, "text/plain", "ok"); });
server.begin();
}
void setup() {
Serial.begin(115200);
pinMode(PIN_REED_X, INPUT_PULLUP); pinMode(PIN_REED_Y, INPUT_PULLUP);
pinMode(PIN_BUZZER, OUTPUT);
prefs.begin("plotter");
STEPS_PER_MM_X = prefs.getFloat("spx", 102.4f); // 上次量尺校準值
STEPS_PER_MM_Y = prefs.getFloat("spy", 102.4f);
sx.setup(STEPS_PER_MM_X * 40 / 40, ACCEL); // AccelStepper 內部以步為單位
sy.setup(STEPS_PER_MM_Y * 40 / 40, ACCEL);
sx.setMaxSpeed(SPEED_CRUISE * 4); sy.setMaxSpeed(SPEED_CRUISE * 4);
if (REVERSE_X) sx.setPinsInverted(true);
if (REVERSE_Y) sy.setPinsInverted(true);
WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS);
for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++) delay(250);
if (WiFi.status() != WL_CONNECTED) { // AP 後備:斷網也能畫
WiFi.mode(WIFI_AP); WiFi.softAP("plotter-ap", "draw4455");
Serial.println("AP mode: plotter-ap / draw4455");
}
setupRoutes();
homeAll(); // 開機自動歸零
}
void loop() {
runPlanner();
servoIdle();
server.handleClient();
}
⚠️ AccelStepper 的
setPinsInverted 只對方向訊號有意義,4 相直驅模式請直接交換 IN 線序對應的陣列順序——第一次組裝方向反了 90% 是線序,改韌體的相序表,不要亂拆線。ULN2003 長期 12V 供電時,3.3V 邏輯輸入仍能觸發(板上光耦級聯電阻已內建),但請買「12V 版」而非「5V 精簡版」。🖥️ 瀏覽器端 SVG→G-code 要點(完整 JS 見檔案清單
web/ui.js):解析 <path d> 的 M/L/H/V/C/S/Q/T/Z(含小寫相對指令),貝茲以「弦高 0.1mm」遞迴展平;先做路徑排序(最近鄰)減少空移、抬筆距離以 dwell 補償;每 24 段一 POST,收到 /status busy=0 且 free>96 再補下一批——Wi-Fi 抖動由瀏覽器端重送冪等。🏗️ 組裝步驟
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
- 列印後處理:全部鎖孔熱埋銅內牙(烙鐵 260°C 垂直沉入,冷卻後不出粉即成功)
- 導軌鎖裝+平行校正:刀規拉對角線,兩條對角差 <0.5mm 才算過——這一步決定畫正方形是不是正方形
- 皮帶雙環佈線:張緊輪「撥吉他會響」的緊度;拖板全行程手推順滑無異音
- 馬達耦合:28BYJ 的 D 軸用鋁聯軸器(列印耦合件在皮帶張力下會橢圓化);鎖緊滑輪頂絲兩顆都對準平面
- 電源分壓測:先不接 C3,量 LM2596 輸出 5.0±0.1V 再接板;手轉馬達測 ULN 四相順序(有「咔咔」卡頓=相序錯,韌體換表)
- 燒錄+歸零測試:上電自動 G28,兩軸「退—慢探—退 3mm」動作完整+長嗶
- steps/mm 量尺校準:網頁畫 100mm 線、游標卡尺量實長、按比例回填
SPX/SPY(存 Flash) - 首發圖:測試 SVG(圓+螺旋+「Hello」)→ 檢查拐角墨漬、起筆斷線、鏡像方向
🔧 問題排除
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 斜線變階梯、圓變多邊形 | 28BYJ 失步 | 走速降 400、加速度減半、皮帶調鬆半格;12V 低於 11V 的變壓器換掉(扭力直接消失) |
| 圖形左右/上下鏡像 | ULN 相序與韌體定義不匹配 | 改韌體相序陣列或 IN 線順序對調,二選一;量尺校準前會反覆出現,先固定再校 |
| 抬筆仍拖出墨線 | SG90 行程不足或連桿滑牙 | 抬筆位 125°→135°;凸輪螺絲上膠;筆夾下移讓筆尖更接近連桿導向 |
| 拐角一坨墨 | 過沖+速度太快 | 本韌體拐角降速已內建;仍暈墨改用 0.3mm 針筆,或 G4 停 100ms 再走 |
| 歸零撞過頭/找不到原點 | 磁簧與磁環間距 >4mm;磁簧線與馬達線同束受擾 | 間距調 3mm;磁簧線纏屏蔽、與動力線分槽走線 |
| 畫到一半停住不動 | 瀏覽器分塊等 ack 逾時 | 頁面自動重送冪等;仍斷則連 AP 後備熱點 plotter-ap |
| C3 反覆重啟 | 共地懶惰:馬達電流脈衝拉垮 5V | 星形共地重做;SG90 電源就近 220µF;C3 改 USB 供電測試交叉驗證 |
🎯 進階延伸
- 噴墨模態:SG90 換按壓式喷墨夾(墨囊+電磁鐵 20ms),畫大幅面點陣字
- 雙筆切換:筆夾改雙槽+凸輪翻轉,黑紅雙色流程圖一次出
- 刻紙模態:换 0.2mm 刻刀+減速 50%,卡纸上刻窗花(下壓靠筆夾螺絲定量)
- NAS 列印佇列:NAS 掛一個網頁轉檔佇列(SVG→展平→G-code→上傳繪圖機),家人用 Line 傳圖就能叫它寫名字
📦 檔案清單
pen-plotter/
├── src/
│ └── pen-plotter.ino # ESP32 韌體(本頁完整程式)
├── web/
│ ├── index.html.gz # 內嵌網頁(PROGMEM 分段)
│ └── ui.js # SVG 解析+貝茲展平+流控上傳
├── stl/
│ ├── rail-mount.stl # 導軌鎖座
│ ├── crossbeam.stl # 橫樑+馬達座
│ ├── x-carriage.stl # X 拖板
│ ├── belt-tensioner.stl # 張力器滑塊
│ ├── pen-clip-75.stl # 筆夾(75°)
│ ├── lift-cam.stl # 抬筆連桿組
│ ├── home-stop.stl # 原點擋塊+磁環座
│ └── leveling-foot.stl # 調平腳
├── f3d/
│ └── plotter-design.f3d # Fusion 360 原始檔
├── test/
│ ├── calibration-100mm.svg # steps/mm 校準圖
│ └── first-draw.svg # 首發測試圖(圓+螺旋+Hello)
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件