XYテーブル製作シリーズ 第9回
はじめに:原点復帰・位置表示・ソフトリミットで「装置」にする
第8回で基本的なGUI操作ができるようになりました。実際の装置では、さらに原点復帰(Homing)・現在位置の管理・ソフトリミットの3つが重要です。CNCや産業装置でも必ず使われる機能で、今回これらを実装してXYテーブルコントローラの完成度を高めます。
この記事で分かること
- GUIに「原点復帰」ボタンを追加し、X・Yを別スレッドで同時に原点復帰させる
current_pos辞書でX軸・Y軸の現在位置(mm)を管理する- ThreadからGUIを直接触らず、
queue.Queue+root.after(100, ...)で安全に位置表示を更新する - ソフトリミット(
X_MAX_MM/Y_MAX_MM)で移動範囲外の指示を弾く - リミットスイッチをNC(ノーマルクローズ)で配線する理由(断線もフェイルセーフで検知)
- 第9回 完成コード(
xy_no9_gui.py、ZIP配布)
ハードウェア構成(過去回のおさらい)
| 部品 | 型番 |
|---|---|
| マイコン ×1 | Raspberry Pi 3 Model A+ |
| モータードライバ ×2 | A4988 |
| モーター ×2 | NEMA17 |
| 電源 | 12V 5A |
| リミットスイッチ ×2 | オムロン D2JW-01K1A1 |
# X軸 X_STEP=21 X_DIR=20 X_EN=16 X_MS1=17 X_MS2=27 X_MS3=22 X_RESET=5 X_SLEEP=6 X_LIMIT=4
# Y軸 Y_STEP=2 Y_DIR=26 Y_EN=18 Y_MS1=23 Y_MS2=24 Y_MS3=25 Y_RESET=12 Y_SLEEP=13 Y_LIMIT=14
リミットスイッチをNC(ノーマルクローズ)にする理由:通常時は電流が流れ、スイッチが押されると接点が開いて電流が止まります。ケーブルが断線したときも電流が止まるため、断線も異常として検知できます(フェイルセーフ)。CNC・産業装置ではNCが基本です。
原点復帰(Homing)をGUIに追加する
原点復帰は「原点方向へ移動 → リミットスイッチON → 停止 → バックオフ(少し戻る)→ その位置を現在位置0mmとする」という流れです。バックオフはスイッチが押されたままだと次の動作ができないために必要です。
def start_homing():
tx = threading.Thread(target=homing_axis, args=("X軸", X_STEP, X_DIR, X_EN, X_LIMIT,
X_MS1, X_MS2, X_MS3, X_RESET, X_SLEEP))
ty = threading.Thread(target=homing_axis, args=("Y軸", Y_STEP, Y_DIR, Y_EN, Y_LIMIT,
Y_MS1, Y_MS2, Y_MS3, Y_RESET, Y_SLEEP))
tx.start()
ty.start()btn_homing = tk.Button(root, text="原点復帰", command=start_homing, bg="lightgreen")
btn_homing.grid(row=6, column=0, columnspan=2)現在位置の管理と、Queueで安全にGUI更新する
現在位置を current_pos 辞書で管理し、モーターが動くたびに更新します。
current_pos = {"X軸": 0.0, "Y軸": 0.0}TkinterのStringVarで表示
x_pos_var = tk.StringVar(value="X:0.00mm")
y_pos_var = tk.StringVar(value="Y:0.00mm")
tk.Label(root, textvariable=x_pos_var).grid(row=8, column=0)ThreadからGUIを直接更新してはいけない
Tkinterはスレッドセーフではなく、モータースレッドから直接 x_pos_var.set(...) するとエラーやフリーズの原因になります。そこで queue.Queue を挟みます。
message_queue = queue.Queue()
# モータースレッド側:メッセージを入れるだけ
message_queue.put("update_pos")
# GUIスレッド側:100msごとに取り出して画面更新
def process_queue():
while not message_queue.empty():
msg = message_queue.get()
if msg == "update_pos":
update_position_display()
root.after(100, process_queue)ソフトリミット(移動範囲の制限)
ソフトウェア側で移動範囲を制限し、リニアガイドを飛び越すような動作を防ぎます。
X_MIN_MM = 0
X_MAX_MM = 255 # 機械のスペックに合わせて変更
Y_MIN_MM = 0
Y_MAX_MM = 255
# モーター動作前にチェック
target = current_pos[axis_name] + distance_mm
if axis_name == "X軸" and (target < X_MIN_MM or target > X_MAX_MM):
print("X軸 ソフトリミット(範囲外)")
return第9回 完成コード
# ============================================
# 第9回
# XYテーブルGUI + 原点復帰 + 位置表示 + ソフトリミット
# ============================================
import RPi.GPIO as GPIO
import time
import threading
import tkinter as tk
import queue # queueモジュールをインポート 第9回追加
GPIO.setmode(GPIO.BCM)
# ============================================
# GPIO設定
# ============================================
X_STEP = 21
X_DIR = 20
X_EN = 16
X_MS1 = 17
X_MS2 = 27
X_MS3 = 22
X_RESET = 5
X_SLEEP = 6
X_LIMIT = 4
Y_STEP = 2
Y_DIR = 26
Y_EN = 18
Y_MS1 = 23
Y_MS2 = 24
Y_MS3 = 25
Y_RESET = 12
Y_SLEEP = 13
Y_LIMIT = 14
# ============================================
# モーション設定
# ============================================
BELT_PITCH = 2.0
PULLEY_TEETH = 32
MM_PER_REV = BELT_PITCH * PULLEY_TEETH
FULL_STEPS = 200
MICROSTEP = 16
STEPS_PER_REV = FULL_STEPS * MICROSTEP
STEPS_PER_MM = STEPS_PER_REV / MM_PER_REV
ACC_DEC_STEPS = 100
BACKOFF_STEPS = 200 # 原点復帰後のバックオフ量(ステップ数)
stop_flag = False
message_queue = queue.Queue() # queueを追加 第9回追加
# ============================================
# 位置管理(第9回追加)
# ============================================
current_pos = {
"X軸":0.0,
"Y軸":0.0
}
# ソフトリミット(第9回追加)
X_MIN_MM = 0
X_MAX_MM = 255 # 適宜変更してください
Y_MIN_MM = 0
Y_MAX_MM = 255 # 適宜変更してください
# ============================================
# GPIO初期化
# ============================================
def gpio_init_axis(STEP,DIR,EN,MS1,MS2,MS3,RESET,SLEEP):
pins=[STEP,DIR,EN,MS1,MS2,MS3,RESET,SLEEP]
for p in pins:
GPIO.setup(p,GPIO.OUT)
GPIO.setup(X_LIMIT,GPIO.IN,pull_up_down=GPIO.PUD_UP)
GPIO.setup(Y_LIMIT,GPIO.IN,pull_up_down=GPIO.PUD_UP)
GPIO.output(RESET,GPIO.HIGH)
GPIO.output(SLEEP,GPIO.HIGH)
GPIO.output(MS1,GPIO.HIGH)
GPIO.output(MS2,GPIO.HIGH)
GPIO.output(MS3,GPIO.HIGH)
GPIO.output(EN,GPIO.LOW)
# ============================================
# 加減速
# ============================================
def get_intervals(total_steps,acc_steps,total_time):
const_steps=max(total_steps-acc_steps*2,1)
total_ratio=acc_steps*0.5+const_steps+acc_steps*0.5
base_interval=total_time/total_ratio/2
intervals=[]
for i in range(acc_steps):
intervals.append(base_interval*(acc_steps-i)/acc_steps)
for i in range(const_steps):
intervals.append(base_interval)
for i in range(acc_steps):
intervals.append(base_interval*(i+1)/acc_steps)
return intervals
# ============================================
# モーター制御
# ============================================
def run_motor(axis_name,
STEP,DIR,EN,
MS1,MS2,MS3,
RESET,SLEEP,
distance_mm,
cycles):
global stop_flag
global current_pos
gpio_init_axis(STEP,DIR,EN,MS1,MS2,MS3,RESET,SLEEP)
steps=int(abs(distance_mm)*STEPS_PER_MM) #absを追加して正負両対応
intervals=get_intervals(steps,ACC_DEC_STEPS,1.5)
for c in range(cycles):
if stop_flag:
return
# ソフトリミットチェック(第9回追加)
target = current_pos[axis_name] + distance_mm
if axis_name=="X軸":
if target X_MAX_MM:
print("X軸 ソフトリミット(範囲外)")
return
if axis_name=="Y軸":
if target Y_MAX_MM:
print("Y軸 ソフトリミット(範囲外)")
return
# 正転
if axis_name=="X軸":
GPIO.output(DIR,GPIO.LOW)
else:
GPIO.output(DIR,GPIO.HIGH)
for t in intervals:
if stop_flag:
return
GPIO.output(STEP,GPIO.HIGH)
time.sleep(t)
GPIO.output(STEP,GPIO.LOW)
time.sleep(t)
# 逆転
if axis_name == "X軸":
GPIO.output(DIR,GPIO.HIGH)
else:
GPIO.output(DIR,GPIO.LOW)
for t in intervals:
GPIO.output(STEP,GPIO.HIGH)
time.sleep(t)
GPIO.output(STEP,GPIO.LOW)
time.sleep(t)
# 位置更新(第9回追加)
current_pos[axis_name]+=distance_mm # 移動量を加算(正負両対応)
message_queue.put("update_pos") # 移動完了後に位置表示更新を指示
GPIO.output(EN,GPIO.HIGH)
# ============================================
# XY同時実行
# ============================================
def start_xy():
global stop_flag
stop_flag=False
x_dist=float(entry_x_dist.get())
x_cycle=int(entry_x_cycle.get())
y_dist=float(entry_y_dist.get())
y_cycle=int(entry_y_cycle.get())
tx=threading.Thread(
target=run_motor,
args=("X軸",
X_STEP,X_DIR,X_EN,
X_MS1,X_MS2,X_MS3,
X_RESET,X_SLEEP,
x_dist,x_cycle)
)
ty=threading.Thread(
target=run_motor,
args=("Y軸",
Y_STEP,Y_DIR,Y_EN,
Y_MS1,Y_MS2,Y_MS3,
Y_RESET,Y_SLEEP,
y_dist,y_cycle)
)
tx.start()
ty.start()
# ============================================
# 原点復帰(第9回追加)
# ============================================
def start_homing():
global stop_flag
stop_flag=False
GPIO.setup(X_EN,GPIO.OUT)
GPIO.setup(Y_EN,GPIO.OUT)
GPIO.output(X_EN,GPIO.LOW)
GPIO.output(Y_EN,GPIO.LOW)
tx = threading.Thread(
target=homing_axis,
args=("X軸",
X_STEP,X_DIR,X_EN,
X_LIMIT,
X_MS1,X_MS2,X_MS3,
X_RESET,X_SLEEP)
)
ty = threading.Thread(
target=homing_axis,
args=("Y軸",
Y_STEP,Y_DIR,Y_EN,
Y_LIMIT,
Y_MS1,Y_MS2,Y_MS3,
Y_RESET,Y_SLEEP)
)
tx.start()
ty.start()
def homing_axis(axis_name,
STEP,DIR,EN,
limit_pin,
MS1,MS2,MS3,
RESET,SLEEP):
print(axis_name,"原点復帰開始")
gpio_init_axis(STEP,DIR,EN,MS1,MS2,MS3,RESET,SLEEP)
GPIO.setup(limit_pin,GPIO.IN,pull_up_down=GPIO.PUD_UP)
# 原点方向
if axis_name == "X軸":
homing_dir = GPIO.HIGH
backoff_dir = GPIO.LOW
else:
homing_dir = GPIO.LOW
backoff_dir = GPIO.HIGH
GPIO.output(DIR,homing_dir)
time.sleep(0.05)
# 原点探索
while GPIO.input(limit_pin) == GPIO.LOW:
GPIO.output(STEP,GPIO.HIGH)
time.sleep(0.001)
GPIO.output(STEP,GPIO.LOW)
time.sleep(0.001)
if stop_flag:
return
time.sleep(0.05)
# BACKOFF
GPIO.output(DIR,backoff_dir)
for _ in range(BACKOFF_STEPS):
GPIO.output(STEP,GPIO.HIGH)
time.sleep(0.001)
GPIO.output(STEP,GPIO.LOW)
time.sleep(0.001)
GPIO.output(EN,GPIO.HIGH)
current_pos[axis_name] = 0.0
message_queue.put("update_pos") # 原点復帰後に位置表示更新を指示
print(axis_name,"原点復帰完了")
# ============================================
# 緊急停止
# ============================================
def stop_motor():
global stop_flag
stop_flag=True
GPIO.output(X_EN,GPIO.HIGH)
GPIO.output(Y_EN,GPIO.HIGH)
# ============================================
# 位置表示更新(第9回追加)
# ============================================
def update_position_display():
x_pos_var.set(f"X:{current_pos['X軸']:.2f} mm")
y_pos_var.set(f"Y:{current_pos['Y軸']:.2f} mm")
# ============================================
# Queue(第9回追加)
# ============================================
def process_queue():
while not message_queue.empty():
msg = message_queue.get()
if msg == "update_pos":
update_position_display()
root.after(100, process_queue) # 100msごとにキューをチェック
# ============================================
# GUI
# ============================================
root=tk.Tk()
root.title("XY Table Controller")
tk.Label(root,text="X移動量(mm)").grid(row=0,column=0)
entry_x_dist=tk.Entry(root)
entry_x_dist.grid(row=0,column=1)
tk.Label(root,text="Xサイクル").grid(row=1,column=0)
entry_x_cycle=tk.Entry(root)
entry_x_cycle.grid(row=1,column=1)
tk.Label(root,text="Y移動量(mm)").grid(row=2,column=0)
entry_y_dist=tk.Entry(root)
entry_y_dist.grid(row=2,column=1)
tk.Label(root,text="Yサイクル").grid(row=3,column=0)
entry_y_cycle=tk.Entry(root)
entry_y_cycle.grid(row=3,column=1)
# 現在位置表示(第9回追加)
x_pos_var=tk.StringVar(value="X:0.00 mm")
y_pos_var=tk.StringVar(value="Y:0.00 mm")
tk.Label(root,textvariable=x_pos_var).grid(row=4,column=0)
tk.Label(root,textvariable=y_pos_var).grid(row=4,column=1)
tk.Button(root,
text="XY同時実行",
command=start_xy,
bg="lightblue").grid(row=5,column=0,columnspan=2,pady=10)
tk.Button(root,
text="緊急停止",
command=stop_motor,
bg="red",
fg="white").grid(row=6,column=0,columnspan=2,pady=10)
# 原点復帰ボタン(第9回追加)
tk.Button(root,
text="原点復帰",
command=start_homing,
bg="lightgreen").grid(row=7,column=0,columnspan=2,pady=10)
root.after(100, process_queue) # Queue(キュー)の監視処理開始
root.mainloop()
まとめと次回予告
原点復帰・現在位置表示・ソフトリミットを実装し、XYテーブル制御ソフトの完成度が大きく向上しました。次回・第10回では、単軸動作・片道移動・安全なプログラム終了を追加します。
次に読む記事
🎛️ 次回はこちら(第10回) XYテーブル制御GUIに単軸動作・片道移動・安全なプログラム終了を実装 単軸操作・位置決め・×ボタン無効化。 → 🧵 前回(第8.5回) PythonのGUIが固まる理由とThreadの使い方|Tkinterでモーター制御中もボタンを効かせる 今回使った Queue パターンの背景。 → 🗺️ 全体像 自作XYテーブルの全手順ロードマップ|Raspberry Pi+ステッピングモーターで2軸制御 第0回〜第11回の全工程をまとめたロードマップ。 →よくある質問(FAQ)
Qなぜ Thread から直接 GUI(StringVar)を更新してはいけないのですか?
ATkinterはスレッドセーフに設計されていません。別スレッドからウィジェットやStringVarを操作すると、多くの場合は動くものの、タイミングによってクラッシュやフリーズが起きます。queue.Queue にメッセージを積み、root.after でGUIメインスレッドが取り出して更新するのが安全な定番パターンです。
Qroot.after(100, process_queue) の100は何ですか?
A100ミリ秒ごとにキューをチェックして画面を更新する、という意味です。小さくすると表示が滑らかになりますがCPU負荷が上がり、大きくすると表示がカクつきます。位置表示なら50〜200ms程度が実用的です。
Qソフトリミットの範囲(255mm)はどう決めればいいですか?
Aリニアガイドの有効ストロークから、両端に安全マージン(5〜10mm)を引いた値にします。原点復帰でバックオフした位置が0なので、そこから実際に動ける距離を実測して X_MAX_MM に設定します。
Q原点復帰中に緊急停止したいです。
Ahoming_axis のステップループ内で if stop_flag: return をチェックします。緊急停止ボタンで stop_flag=True にし、あわせて GPIO.output(EN, GPIO.HIGH) でドライバの電流を切ると即座に止まります。
Qcurrent_pos が実際の位置とずれていきます。
A脱調(パルスを送ったのにモーターが回っていない)が起きると、指令上の current_pos と実機がずれます。加減速を効かせて脱調を防ぎ、定期的に原点復帰して current_pos をリセットするのが確実です。
XYテーブル製作シリーズ(全12回)
- Raspberry Pi ImagerでのOSインストール手順|microSD書き込みから初期設定まで
- ラズパイでLチカ|PythonとRPi.GPIOでLEDを点灯・点滅させる方法
- ラズパイでステッピングモーターを回す方法|A4988の配線とPython制御・Vref調整
- ステッピングモーターをmm単位で動かす計算式とPython実装|step/mmの求め方
- PythonでX軸・Y軸のステッピングモーターを2軸制御する方法|A4988ドライバー2台
- ステッピングモーターの脱調を防ぐ加減速制御(台形駆動)をPythonで実装する方法
- Raspberry PiでXYテーブルを2軸同時制御|Pythonのthreadingでステッピングモーターを同時に動かす
- Raspberry Pi+PythonでXYテーブルの原点復帰(Homing)を実装|リミットスイッチ・バックオフ処理
- PythonのTkinterでXYテーブルをGUI操作する|移動量入力・XY同時実行・緊急停止ボタン
- PythonのGUIが固まる理由とThreadの使い方|Tkinterでモーター制御中もボタンを効かせる
- XYテーブルGUIに原点復帰・現在位置表示・ソフトリミットを実装|Queueでスレッド安全にGUI更新(この記事)
- XYテーブル制御GUIに単軸動作・片道移動・安全なプログラム終了を実装
- 自作XYテーブルの完成構成と設計資料(図面・BOM・結線図)|完成版Pythonコード配布
補足記事
⚡ 第1.5回・補足 A4988の電流制限(Vref)調整方法|焼損を防ぐ計算式とテスターでの設定手順 モーターを回す前に必ずやる A4988 の電流調整。 → 🔀 番外編 L6470が動かない原因|A4988との違いとSPI通信・デイジーチェーンの罠 高機能ドライバ L6470 を A4988 と同じ感覚で使ってハマった話。 → 🗺️ 全体像 自作XYテーブルの全手順ロードマップ|Raspberry Pi+ステッピングモーターで2軸制御 第0回〜第11回の全工程をまとめたロードマップ。 →Follow me on X
本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕
@ninjin_py_vba をフォローシリーズ記事一覧
電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。
-
FILE.01 — IoT
Raspberry Pi Pico W × GASラズパイPico W×GASで自作!「LINEで動く温湿度&スマートリモコン」完全ロードマップ
「外出中のペットの室温が心配…」そんな思いからスタートしたIoT自作プロジェクトの総集編。温湿度監視からエアコン遠隔操作まで、仕組みをゼロから作りたい方のための全5回の開発記録です。
シリーズを読む -
FILE.02 — CNC
Arduino × レーザー刻印CNCCNC自作シリーズ
高精度な加工を目指し、本格的な自作CNC製作に挑戦中。現在は基幹パーツであるオリエンタルモーターの納品を待つ「設計・準備編」を公開。ハードとソフトの両面から、理想のマシンを形にする過程をリアルタイムにお届けします。
シリーズを読む -
FILE.03 — XY軸制御
Raspberry Pi × ステッピングモーターXYテーブルシリーズ
Raspberry Piとステッパモーターを使い、ゼロから2軸制御に挑む記録。OS設定から回路設計、多軸制御のPythonコードまで、躓きやすいポイントを徹底図解。電子工作初心者が「動く感動」を味わうための実戦ガイドです。
シリーズを読む -
FILE.04 — 業務自動化
Python × Excel × CustomTkinter【Python開発記】NINJIN Mail制作記
PythonとExcelを連携させ、実務で即戦力となるメール送信ツールを開発。CustomTkinterによるUI構築や、ミスを防ぐテンプレート活用術など、現場の「痒い所に手が届く」自動化ノウハウを細部まで丁寧に解説します。
シリーズを読む -
FILE.05 — 業務自動化
Python × Excel × GUIアプリ化Invoice Maker(請求書・見積書自動作成)
手作業の請求書作成から卒業!PythonでExcelデータを読み込み一括PDF化する「Invoice_Maker」の作り方を全3回で解説。基本ロジックからGUIアプリ化まで、実務で役立つ自動化ノウハウが満載です。
シリーズを読む

コメント