榧野製作所です。
今回はGrblにマイコンからGcodeを送信するときに、つまづいた点をまとめようと思います。
1.背景
小型のペンプロッタを各種イベントに持っていく際、ネックとなるのはパソコンの管理です。自分の持っているパソコンはやや古いため、質量が重いうえにサイズも大きいです。イベントでは、主にペンプロッタに対してGcodeを送信するのみで、他の仕事もないため、会場では場所をとり、行き帰りでは労力を伴います。そのため、Gcodeを送信するだけの仕事をするデバイスが欲しかったのですが、なかなか見かける機会がありませんでした。(アリエクに売っているという噂は聞きますが、怖いので手を出していません…)
そこで、マイコンを用いてGcode送信機を制作することにしましたが、Arduino側(Grbl)とシリアル通信をするといつも上手くいかず、あきらめて1年半ほど放置していました。先日、ようやく上手く動かすことに成功したため、その備忘録としてこれを記します。
また、Gcode送信機の製作にあたって参考・相談をさせていただきました赤井さしみさんに感謝を申し上げます。
2.課題
Gcode送信機を制作するにあたってぶつかった障壁として、GcodeSenderで検索しても、同名のソフトウェア(これもパソコンからGcodeを送信するもの)がヒットしてしまい、なかなか情報が得られない、というのが挙げられると思います。余談と言えば余談ですが、同様に製作してる人やつまづいている人を参考にできなかったのは、電子工作初心者の私にとって非常に大変でした。
ここからは実際に送信機を制作して、シリアル通信をしようとした場合の話をします。
まず、作業環境として、Arduino(Grbl・ペンプロッタ側)、Esp32(Gcode送信側)の二つのマイコンをUart通信で接続することを前提とします。使用したIDEはArduinoのものです。
Esp32側のコードとしては、ざっくりですが、SDカード内のテキストファイルを開き、1行を取り出してArduinoにシリアル通信し、向こうからのレスポンス(“ok”や”Error!”など)をreadStringUntil(‘\n’)で待って次の行を送信する、というものでした。
#include <SPI.h>
#include <SD.h>
#define CS_PIN 5 // SDカードのCSピン(基板に合わせて変更してください)
File myFile;
void setup() {
Serial.begin(115200); // PCモニタ用
Serial2.begin(9600); // Arduinoとの通信 (TX2=17, RX2=16 が多い)
if (!SD.begin(CS_PIN)) {
Serial.println("SDカード初期化失敗!");
while (1);
}
myFile = SD.open("/test.txt");
if (!myFile) {
Serial.println("ファイルが開けません");
while (1);
}
}
void loop() {
static String line = "";
static bool waitingOK = false;
if (!waitingOK && myFile.available()) {
// 1行読み取り
line = myFile.readStringUntil('\n');
line.trim(); // 改行削除
if (line.length() > 0) {
Serial2.println(line); // Arduinoへ送信
Serial.print("送信: "); Serial.println(line);
waitingOK = true;
}
}
// ArduinoからOKを受信したら次の行へ
if (waitingOK && Serial2.available()) {
String resp = Serial2.readStringUntil('\n');
resp.trim();
if (resp == "ok") {
Serial.println("OK受信");
waitingOK = false;
}
}
// ファイル終端なら終了
if (!myFile.available() && !waitingOK) {
Serial.println("送信完了!");
myFile.close();
while (1);
}
}
こんな感じのコードを組んでいましたが、このまま実行すると最初の数行は読み込んでくれるのですが、少しするとペンプロッタの動作がカクついたり、止まったりします。
シリアル通信を確認すると
00:53:57.851 -> > G3.00 X109.94 Y-118.49 I-5.01 J-0.92
00:53:57.921 -> < ok
00:53:57.921 -> > G3.00 X109.83 Y-118.19 I-2.56 J-0.81
00:53:58.031 -> < erroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerroerrook
00:53:58.031 -> < ok
02:32:00.189 -> > G01 F9500.00
02:32:00.305 -> < ok
02:32:00.305 -> < ok
02:32:00.305 -> < ok
02:32:00.305 -> < ok
このようにerrorメッセージが蓄積したり、okが複数回返されることがありました。
3.間違った解法
私は、これがGrbl側のバッファがオーバーフローしたため発生したと考え、Esp32側で小数点第2桁以降を丸めたり、不要な行は送信しないようにする関数を実装し、Arduino側のバッファの負担を軽減させようとした他、送信頻度を抑えるためにdelayをかませたりしました。
結論としてこれは解決に寄与しなかったうえ、余計な問題を加速させました。
$10=2を設定し、”?”によって得られるリアルタイムステータスレポートを用いてArduinoの空きバッファを確認しながら送信するしかないのか~!と大変迷走しながら頭を抱えていました。
なにせ普段Gcodeの送信に使用しているソフトであるCNCjsでは、シリアル通信部を見ることができず、ソフト側でどのような処理をしているのか全く分からなかったからです。
3.効果的だった解法
実際に動作したコードを示します。(これはChatGPTを使用しながら組みました。)
#include <SPI.h>
#include <SD.h>
#define CS_PIN D3
#define BUTTON_PIN D2
File gcodeFile;
bool sending = false;
// G-code をきれいにする関数
String sanitizeGcodeLine(const String &raw) {
String s = raw;
s.trim();
if (s.length() == 0) return ""; // 空行は無視
if (s == "%") return ""; // % だけの行は無視
// コメント () を削除
String out;
int depth = 0;
for (unsigned i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') { depth++; continue; }
if (c == ')' && depth > 0) { depth--; continue; }
if (depth == 0) out += c;
}
out.trim();
if (out.length() == 0) return "";
// 改行は統一
out += "\n";
return out;
}
// 1行送信
void sendLine(String line) {
Serial1.print(line); // GRBLへ
Serial.print("> "); // デバッグ出力
Serial.print(line);
}
// GRBLからの応答を処理
bool waitForOk() {
String response = "";
unsigned long start = millis();
while (millis() - start < 3000) { // タイムアウト 3s
while (Serial1.available()) {
char c = Serial1.read();
if (c == '\n' || c == '\r') {
if (response.length() > 0) {
Serial.print("< ");
Serial.println(response);
if (response.startsWith("ok")) {
return true;
} else if (response.startsWith("error")) {
return false;
}
response = "";
}
} else {
response += c;
}
}
}
Serial.println("!! Timeout waiting for ok");
return false;
}
void setup() {
Serial.begin(115200);
Serial1.begin(115200); // GRBL 接続
pinMode(BUTTON_PIN, INPUT_PULLUP);
if (!SD.begin(CS_PIN)) {
Serial.println("SD init failed!");
while (1);
}
Serial.println("SD init done.");
}
void loop() {
if (!sending) {
if (digitalRead(BUTTON_PIN) == LOW) {
delay(200); // debounce
gcodeFile = SD.open("/test.gcode"); // ファイル名は適宜変更
if (gcodeFile) {
sending = true;
Serial.println("Start sending...");
} else {
Serial.println("File open failed");
}
}
} else {
if (gcodeFile.available()) {
String rawLine = gcodeFile.readStringUntil('\n');
String line = sanitizeGcodeLine(rawLine);
if (line.length() > 0) {
sendLine(line);
if (!waitForOk()) {
Serial.println("!! Error received, stop.");
sending = false;
gcodeFile.close();
}
}
} else {
Serial.println("Finished sending file.");
gcodeFile.close();
sending = false;
}
}
}
115200bpsでシリアル通信をしていますが、通信処理が遅くなるとお互いのタイミングがずれてテキストの受け渡しが上手くいかなくなるので、過度にGcodeを丸める処理を入れたり、ディレイを嚙ませるのはご法度でした。
また、Arduinoからの反応をreadStringUntil(‘\n’)で拾うのも良くなかったようで、一文字ずつ逐一拾うようにしたら上手くいきました。
(ブロッキングで待つことがあるらしいのですが、詳しいことはよく分かりません…)
4.最後に
これは初歩中の初歩のミスですが、ArduinoとEsp32の通信の際に電圧レベルを合わせたり、グラウンドを接続することを忘れる(というか知らなかったです)ことがありました。Arduinoの動作電圧が5V、Esp32が3.3Vなので、それぞれの信号レベルを合わせてあげる必要があります。また、底のレベルを合わせるためにGNDを共通させてあげることも大切です。
これを一つで行ってくれるのが秋月に売っていました。
https://akizukidenshi.com/catalog/g/g113837/
これを使用するのがいいと思います。
製作した基板はこんな感じです。
最小構成にしようとしたところ似た点が多くなってしまいました。

Seeed Studio XIAO ESP32C3
https://akizukidenshi.com/catalog/g/g117454/
(この構成ならピンの数はピッタリです。サイズも小さくて良好)
・4ビット双方向ロジックレベル変換モジュール BSS138使用
https://akizukidenshi.com/catalog/g/g113837/
(ArduinoとEsp32の通信のため)
・0.96インチ 128×64ドット有機ELディスプレイ(OLED) 白色
https://akizukidenshi.com/catalog/g/g112031/
(ファイル名や動作状況の確認のため・簡易的なデバックにも)
・microSDカードスロット レベルシフター付きブレークアウト基板キット
https://akizukidenshi.com/catalog/g/g114015/
(Gcodeファイルを呼び出すため)
・タクトスイッチ(黒色)
https://akizukidenshi.com/catalog/g/g103647/
(Gcode送信の実行や、様々なアクションの実行に)
・片面ガラスコンポジット・ユニバーサル基板 Cタイプ めっき仕上げ (72×47mm) 日本製
https://akizukidenshi.com/catalog/g/g103229/
(このサイズ感がギリ最小な気がします)
全部で3000円くらいです
この記事がペンプロッタ制作をする人の手助けになることを望みます。
それでは!