はじめに
第8回では、PythonのGUI(Tkinter)を使ってXYテーブルを操作できるようになりました。
- 移動量入力
- XY同時制御
- 緊急停止
これで基本的な操作は可能になりました。
しかし実際の装置では、さらに重要な機能があります。
それが次の3つです。
原点復帰(Homing)
現在位置の管理
ソフトリミット
これらは、CNCや産業装置などでも必ず使われる機能です。
今回の記事では、この3つを実装して
XYテーブルコントローラとしての完成度を高めていきます。
ハードウェア構成(過去回のおさらい)
| 部品 | 型番 |
|---|---|
| マイコン 1個 | Raspberry Pi 3 Model A+ |
| モータードライバ 2個 | A4988 |
| モーター 2個 | Nema17 |
| 電源(モーター2個共用可能) | 12V 5A |
| リミットスイッチ 2個 | オムロン製:D2JW-01K1A1 |




今回の配線 (過去回のおさらい)
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

ここで小話
リミットスイッチは何故NC(ノーマルクローズ)で接続するのか
主に 安全性と異常検知のためです。
CNC・産業装置・工作機械では NCが基本になっています。
最大の理由は断線しても異常検知できる
通常状態
電流:流れている
制御:正常
リミットスイッチが押される
接点が開く
↓
電流が止まる
↓
リミット検出
ケーブル断線
ケーブル断線
↓
電流が止まる
↓
リミット検出
つまり「断線」も異常検知します
これを フェイルセーフ と言います
(安全側に倒れる設計)。
原点復帰(Homing)
原点復帰とは

装置制御では、まず最初に「原点」を決める必要があります。
なぜなら、ステッピングモーターは「今どこにいるのか」を自動では認識できないからです
そのため、装置では「リミットスイッチ」を使用して原点を決定します
本シリーズで使用するリミットスイッチは
「オムロン製:D2JW-01K1A1」です
原点復帰の動作
原点復帰は次のような動作になります。
モーターを原点方向へ移動
↓
リミットスイッチON
↓
停止
↓
BACK OFF(少し戻る)
この「BACK OFF(少し戻る)」処理は重要です。
理由は、スイッチが押されたままだと、次の動作ができないためです。
この「BACKOFF」が実施され、少し戻った位置を
現在位置 = 0mm
として制御装置では扱います
原点復帰処理(Pythonコード)
まず、原点復帰関数を作ります。
ここではX軸とY軸をでそれぞれで規定し、「Thread」を使用してX軸とY軸が同時に原点復帰動作をするように指示します。
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()
原点復帰ボタンをGUIに追加
次に、GUIに「Tkinter」を使用してボタンを追加し、ボタンが押されたことで、上記の「start_homing関数」が動作するように指示します。
btn_homing = tk.Button(
root,
text="原点復帰",
command=start_homing,
bg="lightgreen"
)
btn_homing.grid(row=6,column=0,columnspan=2)
現在位置の管理
装置では、**今どこにいるのか(現在位置)**を表示できるようにしておくと便利です。
現在位置を表示することで、次のようなメリットがあります。
- 動作の進捗状況を確認できる
- 実機の動きとアプリからの指示が一致しているかを目視で確認できる
このように、現在位置の表示は装置の状態を把握するための重要な情報になります。
そこで今回は「current_pos」という名前の変数を用意します
current_pos
current_pos = {
"X軸":0.0,
"Y軸":0.0
}
この用意した変数に、モーターが動くたび「現在位置(各軸の何mm)」を保存(更新)します。
GUIに位置表示(機能)を追加
Tkinterでは「StringVar」を使うと簡単にGUI上で変数を表示することが出来ます
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)
tk.Label(root,textvariable=y_pos_var).grid(row=8,column=1)
ThreadからGUI更新する方法
ここで1つ問題があります。
PythonのTkinterでは「Threadから直接GUIを更新」をすると、エラーになる場合があります
モータースレッド
↓
GUI更新
上記のルートは危険です。
Queueを使った安全なGUI更新
Thread→GUIは危険なので、「Queue」を使います
まずQueueを作ります
message_queue = queue.Queue()
モーター側からメッセージを送信します
message_queue.put(("pos", axis, current_mm))
GUI側でメッセージを受信します。
def process_queue():
while not message_queue.empty():
msg = message_queue.get()
if msg[0] == "pos":
axis = msg[1]
pos = msg[2]
if axis == "X軸":
x_pos_var.set(f"X:{pos:.2f}mm")
この方法を使うことで
Thread
↓
Queue #ThreadとGUIの間にQueueを入れることで安全に通信
↓
GUI
という安全な通信ができます。
ソフトリミット
次に、装置の安全機能を追加します。
それが「ソフトリミット」です。
ソフトリミットとは、その名のとおり「ソフトウェア側で移動範囲を制限する」事をです
例えば
X軸 0〜255mm #この範囲は動作可能、これを超える値は制限しNGとする
Y軸 0〜255mm
ハードウェアのスペックを考慮して、上記の範囲にしたいと考えたとします
ソフトリミットの設定(Python)
まず、何mmまでを範囲とするか規定する
X_MAX_MM = 255
Y_MAX_MM = 255
X_MIN_MM = 0
Y_MIN_MM = 0
モーター動作前に、入力された値が、上記で規定した範囲内なのか範囲外なのか「If」を使用してチェックします。
target_pos = start_pos + distance_mm
if target_pos > X_MAX_MM:
print("移動範囲外")
return
これを
ソフトリミット
と呼び、リニアガイドを跳び越す動作を防ぐ事ができます。
GUIの完成イメージ
第9回で作るGUI

X移動量(mm)
Xサイクル
Y移動量(mm)
Yサイクル
X: 0.00mmY: 0.00mm
[XY同時実行]
[緊急停止]
[原点復帰]
第9回 完成コード
クリックしてPythonフルコードを表示する
# ============================================
# 第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_MIN_MM or target > X_MAX_MM:
print("X軸 ソフトリミット(範囲外)")
return
if axis_name=="Y軸":
if target < Y_MIN_MM or 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_no9_gui.py をダウンロード(ZIP)まとめ
今回実装した機能
原点復帰
現在位置表示
ソフトリミット
これにより
XYテーブル制御ソフトとして完成度が大きく向上しました。
次回予告
次回はさらに完成度を上げます。
追加機能
加減速制御
モーター保護
安全停止
これで
高精度XYテーブルコントローラ
が完成します。
過去のXYテーブル製作シリーズはこちらから
- 【第0回】Raspberry PiのOSインストール完全ガイド|初心者向けにゼロから解説
- 【第1回】PythonでLEDを光らせる|GPIOの基本をやさしく解説
- 【第2回】ラズパイ+A4988でステッピングモーターを回す
- 【第3回】ステッピングモーターをmm単位で動かす|ベルト駆動の計算方法まで解説
- 【第1.5回】 A4988のVref調整手順(XYテーブル製作シリーズ)
- 【番外編】L6470が動かない?A4988と同じ感覚で使ってハマった話
- 【第4回】XYテーブル化の第一歩|X軸とY軸を2台動かす
- 【第5回】Pythonでステッピングモーターの加減速制御を実装する
- 【第6回】Pythonでステッピングモーターを2軸同時に動かす
- 【第7回】Raspberry PiでXYテーブル制御|リミットスイッチと原点復帰(Homing)の実装
- 【第8回】【Python × Raspberry Pi】XYテーブルをGUI操作する(Tkinter)


コメント