PR

Python×Raspberry PiでXYテーブルを制御するGUIを作る(原点復帰・位置表示・ソフトリミット実装)【第9回】

スポンサーリンク
RaspberryPi
スポンサーリンク

はじめに

第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テーブル製作シリーズはこちらから

コメント