M5StackでCNCを作る

M5Stackとそのモジュールを使ってCNCを作ってみました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> でCNCを？！<br><br>できらぁ！ <a href="https://t.co/QIjMsJWq54">pic.twitter.com/QIjMsJWq54</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1527253172867444736?ref_src=twsrc%5Etfw">May 19, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## 使用するデバイス


基本的にスイッチサイエンスさんとAmazonで買ったものを使用しています。

M5Stack

https://www.switch-science.com/products/7362?_pos=2&_sid=4e733eb06&_ss=r

ESP32にディスプレイ、ボタン、スピーカー、バッテリーにSDカードまで合体した欲張りセット。
これがあれば「あれ作りたいな～」ってなったとき大体何とかなります。

GRBLモジュール

https://www.switch-science.com/products/6994?_pos=15&_sid=f956b883f&_ss=r

M5Stackの下にスタックすることでステッピングモーターが使えるようになります。

今は新しいのが出ているようです。

https://www.switch-science.com/products/7476?_pos=2&_sid=4011f5276&_ss=r

Amazon | SUS(エスユウエス) SF-20・20 SF9-202 200mm 4本入 (アルミフレーム) | 棒
https://www.amazon.co.jp/gp/product/B072VHBJ5T/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/cfd42780-c693-780a-399a-bccca40dfdca.png)

これでフレームを構築します。
7本必要です。

Amazon.co.jp: 3DプリンタガイドレールセットT8リードスクリュー ピッチ1mmリード1mm +リニアシャフト8 * 100mm + KP08 SK8 SC8UU +ナットハウジング+カップリング+ステップモータ (200mm) : 産業・研究開発用品

https://www.amazon.co.jp/gp/product/B07D3P1YST/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&th=1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/56b1091a-bfbf-120d-1b9f-6175bac81d66.png)

ステッピングモーターとガイドレールのセット
モーターもついててこの値段はお買い得

レイアウトの自由度が上がるので30cmにした方が良かったかも？![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/10f6bc71-bcb5-275a-43f3-0107fa6cfe10.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/61c10220-6a23-8cc6-c8d0-e369b2bbd60a.png)

これでフレームとレールなどを固定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/9347a2f3-1fda-d22b-f3b7-f219eee5c35c.png)

マイクロスイッチをリミットスイッチとして使用します。

## ハードを構築する

CNCのハード周りを構築していきます。

アルミフレームを長方形に組んで土台にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/6e0ce46c-9ff1-b050-15b6-8c1286fcf5df.png)

手前に見えてるのは以前作った安いリニアレールで作ったペンプロッタです。
安いって言っても1000円~2000円くらいするからモーターセットがお買い得すぎるんですよね・・・

Y軸を設置するためにやぐらを立てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/3fbdc17a-7720-6983-d0ca-090de5d16e50.png)

ガイドレールセットにブラケットがついてなかったので3Dプリンタで作成しました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/0e0a5335-a0d6-ed83-9ef8-144aacda9974.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/2b793284-2877-6912-ca97-f43bef84f8ef.png)


レールをすべて固定したらブロックを固定する板を3Dプリンタで作成して固定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/f270a4b8-cb22-9e50-e659-252a66570d87.png)

ホームセンターで板を買ってきてねじ穴をドリルであけてブロックに固定しました。
これでXステージは完成です！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ネジ止めしてXステージ完成！<br><br>Y軸も出来たも同然なんだけどペンホルダーのアイデアが出ません(´・ω・`) <a href="https://t.co/8VWJQ0YPBs">pic.twitter.com/8VWJQ0YPBs</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1511336178561466375?ref_src=twsrc%5Etfw">April 5, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

Y軸もX軸と同様にレールを固定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/1a6535d8-9d77-a586-73bd-6237e214bbd3.png)

マイクロスイッチでリミットスイッチを作成します。
3Dプリンタでジグを作ってマイクロスイッチを固定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/4be85c60-3071-6375-9142-f0b2b3c42548.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/bc47b341-6ddf-3b6b-32e1-31d1f299edb2.png)
GRBLモジュールのリミットスイッチの端子はGNDが共通なので接続しようとするとこんなふうになりますｗ(新しく出てるステッピングモーターモジュールは改善されてる)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/e3c122e1-14ab-c1f4-5c26-26f9895d5fe2.png)

Z軸のペン上下は紆余曲折があったのですが、
ホームセンターでこんな部品を見つけました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/a5ff1824-a46a-0684-963d-b08d2ddb2f41.png)
これ多分アルミサッシとかに入ってる奴ですね。

んで3Dプリンタで台車とフレームをプリントすると・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/c5ec14d6-6f76-477a-9cf4-a171a0f015f6.png)
こうなって
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/2f184a35-101c-2c55-32ad-52f4d2564dd6.png)
こうじゃ！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/57b8ae7e-a1bb-477d-c7a3-e8cb5bf891fb.png)

いろいろ改善点はあるのですがこれを今も使っています。
ペンの上下はできるようになったのでペンホルダーを作っていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/6df49b9e-fc19-8eae-50ef-9655961d1027.png)

さっきの上下する部分にM4の穴を開けておいたのでボルトで固定して簡単に交換できるようにしてあります。

ボルトで固定するとこんな感じになります。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">リニアレールいい感じ(ﾟ∀ﾟ)<br><br>これは誤差相当小さくなったのでは(ﾟ∀ﾟ)<a href="https://twitter.com/hashtag/Fusion360?src=hash&amp;ref_src=twsrc%5Etfw">#Fusion360</a> <a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://t.co/KQuB1y3rFY">pic.twitter.com/KQuB1y3rFY</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1519388070742982656?ref_src=twsrc%5Etfw">April 27, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これでハード周りは完成です！

## ソフトを構築する

ステッピングモーターを動かすのはGRBLモジュールを使います。

m5-docs
https://docs.m5stack.com/en/module/grbl13.2

MODULE_GRBL13.2/xyz_control.ino at master · m5stack/MODULE_GRBL13.2 · GitHub
https://github.com/m5stack/MODULE_GRBL13.2/blob/master/examples/xyz_control/xyz_control.ino

こちらのサンプルを見るといくつか操作方法がありますがGcodeを使って操作します。

```cpp
#include <M5Stack.h>
#include "MODULE_GRBL13.2.h"

#define STEPMOTOR_I2C_ADDR 0x70

GRBL _GRBL = GRBL(STEPMOTOR_I2C_ADDR);

void setup() {
  M5.begin();
  _GRBL.Init(200, 200, 40, 50);
  _GRBL.setMode("absolute");
}
void loop() {
  if (M5.BtnB.wasPressed()){
      _GRBL.sendGcode("G1 X5Y5Z5 F200");
      _GRBL.sendGcode("G1 X0Y0Z0 F200");
  }
  M5.update();
}

```

Init()の引数が違いますがこれはGcodeのXYZ1.0当たりのステップ数を変更しています。
G1 X1Y1Z1としたときに1mm動くようにモーターによって変更してください。

これでGcodeでペンを動かせるようになったのですが、
手打ちでGcodeを書いていくのは大変なのでおもむろにFusion360をインストールします。

Fusion 360 | 3D CAD/CAM/CAE/PCB クラウドベースのソフトウェア | Autodesk
https://www.autodesk.co.jp/products/fusion-360/overview

Fusion360は非商用なら3D CADやCAMが無料で使える意味が分からないソフトです。

無料でCNCのシミュレーションができてGcodeも出力できます(本当に無料でいいの？)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Fusion360賢すぎんか・・・？🤔 <a href="https://t.co/JaOFeWkhnX">pic.twitter.com/JaOFeWkhnX</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1520764670730469377?ref_src=twsrc%5Etfw">May 1, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

テストという事で2cm角にCの字を削り出す想定にしてみました。

Fusion360のCAMで出力したGcodeをM5StackのSDカードに置いて、
GBRLモジュールのライブラリに流し込んでペンを動かします。

```cpp

G0 X0 Y0 Z0 F100
G1 Z2.
G1 Z1.3 F333.
G1 X15.279 Y15.261 Z1.228
G1 X15.275 Y15.231 Z1.163
G1 Y15.178 Z1.104
G1 X15.289 Y15.11 Z1.065
G1 X15.303 Y15.074 Z1.058
G1 X15.322 Y15.04 Z1.05
G3 X15.851 Y15.372 Z1.016 I0.265 J0.166
G3 X15.322 Y15.04 Z0.981 I-0.265 J-0.166
G3 X15.851 Y15.372 Z0.947 I0.265 J0.166
G3 X15.322 Y15.04 Z0.913 I-0.265 J-0.166
G3 X15.851 Y15.372 Z0.879 I0.265 J0.166
G3 X15.322 Y15.04 Z0.844 I-0.265 J-0.166
G3 X15.851 Y15.372 Z0.81 I0.265 J0.166


void gcode() {
  File f = SD.open("/gcode.nc");
  String str = "";
  int temp = 0;

  while (f.available()) {
    temp = f.read();
    if (temp != '\n') {
      str += String((char)temp);
    }
    else {
      Serial.printf("%s\n", str.c_str());

      Serial.println();
      _GRBL.Gcode((char *)(str.c_str()));
      str = "";
      _GRBL.WaitIdle();
    }
  }
}

```
GRBLモジュールはバッファが小さい？みたいでGcodeをドカンと流し込むとスキップされちゃうのでWaitIdle()を入れていますが、
ウエイトが長すぎるので(モーターが動き終わってから0.5秒くらい止まる)調整した方がいいと思います。

ペンでテストしてみたところいい感じに動いたので、いよいよCNCに挑戦します。
!<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">加速度を5にしたら動作が遅くなったけど脱調ゼロ達成！<br><br>まぁ速度より安定性だよね(´・ω・`) <a href="https://t.co/GIHS3tiy2M">pic.twitter.com/GIHS3tiy2M</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1526976027997908992?ref_src=twsrc%5Etfw">May 18, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## ペンプロッタをCNCにする

CNCにするという事でリューターでも買おうかなぁと思っていたのですがタイミングよくDIMEという雑誌にUSB駆動のルーターがついていたので購入しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/966eb89b-c00b-1a51-ae5d-0cf5dacd977a.png)

ルーターに合わせてFusion360でホルダーをデザインしてプリントしました(角ばってるのはサポートをプリントしたくなかったから)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/badc437b-4af9-eed4-eaaf-c1dee351436c.png)

ペンホルダーと換装すればCNCの完成です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/1c58a125-2db6-5c0d-8eb2-4a8205700cb2.png)

というわけで発泡スチロールを削ってみました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> でCNCを？！<br><br>できらぁ！ <a href="https://t.co/QIjMsJWq54">pic.twitter.com/QIjMsJWq54</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1527253172867444736?ref_src=twsrc%5Etfw">May 19, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

撮影中に疲労からか不幸にも黒塗りの高級車に脱調してしまったので緊急停止したのですが、
なかなかいい感じに削れてるのではないでしょうか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/e84a1c0b-73b3-9eb3-77cb-bad4db991a56.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/7b366d5b-503f-6225-0e51-01fedacf96af.png)

現在はどうしても脱調が無くせないので開発は止まっています(´・ω・`)

モチベーションが復活したらパラメーターをいじったりして改善していこうと思っています。

## ソースコード

ソースコードを残しておきます。
ただしここでは使ってない機能も残っているので見づらいですが・・・

<details><summary>ソースコード</summary>


```cpp

#include <ArduinoJson.h>
#include <M5Stack.h>
#include <M5TreeView.h>
#include <string.h>

#include "GrblControl.h"
#include "WiFi.h"
#include "esp_wps.h"

#define STEPMOTOR_I2C_ADDR 0x70
GRBL _GRBL = GRBL(STEPMOTOR_I2C_ADDR);

int degree = 0;

int PEN_UP = 90;
int PEN_DOWN = 30;

float x = 0.0;
float y = 0.0;

M5TreeView tv;

StaticJsonDocument<2048> jsonObject;

void penUp() {
  String command = "G1 Z0 F800";
  Serial.println(command.c_str());
  _GRBL.Gcode((char *)(command.c_str()));
  // _GRBL.WaitIdle();
}
void penDown() {
  String command = "G1 Z-5 F800";
  Serial.println(command.c_str());
  _GRBL.Gcode((char *)(command.c_str()));
  // _GRBL.WaitIdle();
}

void resetOrigin(char *axis) {

  _GRBL.SetMode("distance"); //　相対移動モード

  String xyz = String(axis);
  String string = "";
  string = "G1 " + xyz + "1 F500";

  // USE Gcode
  _GRBL.Gcode(const_cast<char *>(string.c_str())); // +1移動

  if (axis == "Z") {
    string = "G1 Z-1 F200";
  }
  else {
    string = "G1 " + xyz + "-1 F500";
  }

  while (!_GRBL.InLock()) {
    _GRBL.Gcode(
        const_cast<char *>(string.c_str())); // -0.2ずつロックされるまで移動
    delay(100);
  }

  Serial.println("Locked");
  Serial.println("Back to Origin");

  if (axis == "Z") {
    string = "G1 Z5 F500";
    while (_GRBL.InLock()) {
      _GRBL.UnLock(); // リミットスイッチのロックを解除する
      _GRBL.SetMode("distance"); // absoluteに戻るので再設定
      _GRBL.Gcode(const_cast<char *>(string.c_str()));
      delay(500);
    }
  }
  else {
    string = "G1 " + xyz + "1 F500";
    while (_GRBL.InLock()) {
      _GRBL.UnLock(); // リミットスイッチのロックを解除する
      _GRBL.SetMode("distance"); // absoluteに戻るので再設定
      _GRBL.Gcode(const_cast<char *>(string.c_str()));
      delay(500);
      // スイッチが押しっぱなしだと即座にロックに戻るので移動してから少し待つ
    }
  }

  _GRBL.SetMode("absolute");
  Serial.println("Moved to Origin");
}

// XY:1>1 Z:11>1 ZMax:27mm
void moveTo(float toX, float toY) {
  _GRBL.WaitIdle();

  // toX /= 0.5;
  // toY /= 0.5;
  String command = "G1 X";
  command += String(toX);
  command += " Y";
  command += String(toY);
  command += " F800";
  Serial.println(command.c_str());
  _GRBL.Gcode((char *)(command.c_str()));
}

void tap() {
  penDown();
  penUp();
}

void gcode() {
  File f = SD.open("/gcode.nc");
  String str = "";
  int temp = 0;
  float x = 0;
  float y = 0;
  float prex = 0;
  float prey = 0;

  while (f.available()) {
    temp = f.read();
    if (temp != '\n') {
      str += String((char)temp);
    }
    else {
      Serial.printf("%s\n", str.c_str());

      if (str.indexOf("X") != -1) {

        String temp = str.substring(str.indexOf("X") + 1);
        if (temp.indexOf(" ") != -1) {
          temp = temp.substring(0, temp.indexOf(" "));
        }

        x = temp.toFloat();
        // Serial.printf("X=:%f ", x);
      }
      if (str.indexOf("Y") != -1) {

        String temp = str.substring(str.indexOf("Y") + 1);
        if (temp.indexOf(" ") != -1) {
          temp = temp.substring(0, temp.indexOf(" "));
        }

        y = temp.toFloat();
        // Serial.printf("Y=:%f", y);
      }
      Serial.println();
      _GRBL.Gcode((char *)(str.c_str()));
      float length = sqrt(pow(prex - x, 2) + pow(prey - y, 2));
      if (5 < length) {
        Serial.printf("length=%f4.1\n", length);
      }
      prex = x;
      prey = y;
      if (2 < length) {
        _GRBL.WaitIdle();
      }
      else {
        delay(400);
      }
      // Serial.println();
      str = "";
    }
  }
}

void decodeCommand(String str) {
  Serial.printf("command:%s\n", str.c_str());

  int index = 0;

  while (true) {
    String com = str.substring(index, str.indexOf(",", index));
    Serial.printf("%s\n", com);

    index = str.indexOf(",", index) + 1;

    if (com.startsWith("RS")) {
      resetOrigin("X");
      resetOrigin("Y");
      resetOrigin("Z");
    }
    else if (com.startsWith("BO")) {
      _GRBL.Gcode("G0 X0 Y0 F500");
    }
    else if (com.startsWith("GC")) {
      gcode();
    }
    else if (com.startsWith("TE")) {
      _GRBL.Gcode("G0 X0 Y0 F500");
      _GRBL.WaitIdle();
      penDown();
      _GRBL.Gcode("G1 X20 F500");
      _GRBL.WaitIdle();
      _GRBL.Gcode("G1 Y20 F500");
      _GRBL.WaitIdle();
      _GRBL.Gcode("G1 X0 F500");
      _GRBL.WaitIdle();
      _GRBL.Gcode("G1 Y0 F500");
      _GRBL.WaitIdle();
      _GRBL.Gcode("G1 X20 Y20 F500");
      _GRBL.WaitIdle();
      penUp();
      _GRBL.WaitIdle();
      _GRBL.Gcode("G0 X0 F500");
      _GRBL.WaitIdle();
      penDown();
      _GRBL.Gcode("G1 X20 Y0 F500");
      penUp();
      _GRBL.WaitIdle();
      _GRBL.Gcode("G0 X0 Y0 F500");
      _GRBL.WaitIdle();
      _GRBL.Gcode("G0 Z0 F500");
      _GRBL.WaitIdle();
    }

    if (index == 0) {
      break;
    }
  }
}

void func(MenuItem *mi) {
  Serial.print(mi->parentItem()->tag);
  Serial.print(":");
  Serial.println(mi->tag);

  JsonArray jsonArry = jsonObject["data"].as<JsonArray>();
  Serial.printf("sd:jsonArry.size=%d\n", jsonArry.size());

  String str = String(mi->tag);

  str = "wave" + str;
  Serial.printf("name:%s\n",
                (const char *)jsonArry[mi->parentItem()->tag]["name"]);
  Serial.printf(
      "%s:%s\n", str,
      (const char *)(jsonArry[mi->parentItem()->tag]["command"][str]));
  decodeCommand(jsonArry[mi->parentItem()->tag]["command"][str]);
}

void setup() {
  M5.begin(true, true, true, true);

  _GRBL.Init(200, 200, 40, 50);
  _GRBL.SetMode("absolute");
  dacWrite(25, 0); //　ノイズ対策

  tv.itemHeight = 25;
  tv.itemWidth = 100;
  tv.setTextFont(2);

  File f = SD.open("/fgo.json");

  DeserializationError error = deserializeJson(jsonObject, f);

  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }
  JsonArray jsonArry = jsonObject["data"].as<JsonArray>();
  Serial.printf("sd:jsonArry.size=%d\n", jsonArry.size());

  for (int i = 0; i < jsonArry.size(); i++) {
    Serial.printf("sd:jsonArry[0].name=%s\n",
                  (const char *)(jsonArry[i]["name"]));
  }
  for (int i = 0; i < jsonArry.size(); i++) {
    tv.addItems(std::vector<MenuItem *>{
        new MenuItem((const char *)(jsonArry[i]["name"]), i,
                     std::vector<MenuItem *>{new MenuItem("Wave1", 1, func),
                                             new MenuItem("Wave2", 2, func),
                                             new MenuItem("Wave3", 3, func

                                                          )})});
  }
  tv.begin();
  // penUp();
  delay(1000);
  // penUp();
}

void loop() {
  M5.update();
  tv.update();

  delay(1);
}



```
</details>