複数の入出力を制御するために、産業用途や自動化システムではシーケンサ(PLC:Programmable Logic Controller)を使った「ラダー図」による制御が広く普及しています。
「ラダー図」を使用することで、工場の生産ラインの機械や、モーター、バルブ、ポンプなどの駆動装置をスイッチやセンサーと連携させ、必要なタイミングでON/OFF制御することが容易になります。
「ラダー図」を個人で学習するためには、ラズパイを利用した「CODESYS」が有名ですが、無償の個人利用でも使用時間に制限(2時間ごとに再接続)があり、環境の構築にも少々手間がかかります。
動作確認を行うための「ラズパイPico」を使用したシーケンサ基板については以下のリンクで詳しく紹介しています。
1.シーケンサ(ラダー図)とは
2.Raspberry Pi Picoとは
3.動作確認方法、配線図
4.ラダー図の基本
・a接点、b接点
・AND回路 と OR回路
・インタロック回路
・自己保持回路
・タイマ と カウンタ
5.ラダー図からPythonへの置き換え方法
・スキャン動作(リフレッシュ方式)の実装
・条件分岐(if〜else 文)の省略表記
・タイマー割り込みによる常時タイマカウント
・カウンタークラス作成によるカウンタ
6.サンプルプログラム
・サンプル1:a接点、b接点、AND回路、OR回路
・サンプル2:自己保持回路
・サンプル3:タイマ、カウンタ
・サンプル4:モーターの正逆繰り返し例
7.まとめ
1.シーケンサ(ラダー図)とは
シーケンサの「ラダー図」とは下図のようなものです。
右側に配置されているものが「コイル」と呼ばれ、左側に入力(X000〜)や各コイルの「接点」と呼ばれるものが配置されています。
各「接点」のON/OFF状態に応じて各「コイル」のON/OFFが変化します。
この「ラダー図」の動作は以下のようになります。
「M1」がONしているので「Y000:起動」がONします。
「M1」がONしたことで「X001」がOFFしても「M1」はONしたまま(自己保持)になり「Y000」はONし続けます。
「X000:停止ボタン」が押されるか、「T0」のカウントが終了して「T0」の接点がONすると「M1」がOFFし「Y000」もOFFします。
ラダー動作の基本は以下です。
※この1連の動作をスキャンサイクルと言います。
同様の動作を実現するにはいろいろな方法がありますが「ラダー図」を使用することで、視覚的に動作を確認しながらプログラムの作成をすることができます。
2.Raspberry Pi Picoとは
Raspberry Pi Pico(ラズパイ Pico)とは、Raspberry Pi財団が開発したマイコンボードです。「Python」や「C言語」でプログラミングすることができ、学習やホビー、組込み用途などに最適です。
本体基板上の入出力端子に、スイッチやセンサ、各種制御モジュール、通信モジュールを接続することで、それらをプログラミングによって制御して、動作確認を行うことができます。
「ラズパイ Pico」の端子配列は以下のようになります。
(Wi-Fi通信機能搭載のRaspberry Pi Pico Wも基本的な端子配列は同じです。)
「ラズパイ Pico」の使い方や端子機能、開発環境の準備については、以下のリンク先で詳しく紹介しています。
3.動作確認方法、配線図
動作確認は下画像のような配線図で、ブレッドボードにスイッチとLEDを配線して行いました。
シーケンサとしての入出力点数は、入力(X)4点、出力(Y)4点、補助コイル(M)20点、タイマー(T)とカウンター(C)はそれぞれ10点を想定しています。(拡張可能)
「ラズパイ Pico」は端子が実装されていないものもあります。
自分で半田付けできる方はいいですが、半田付けが苦手な方は端子が半田付けされたものを準備しましょう。
ブレッドボードは安いものはありますが、1列5穴のものがほとんどです。1列6穴あると色々応用が効くので1列6穴のサンハヤト製がおすすめです。
LEDの使い方については、以下のリンク先で詳しく紹介しています。
4.ラダー図の基本
まずは「ラダー図」に馴染みのない方のためにも、基本的な「ラダー図」について紹介します。
・a接点、b接点
スイッチやセンサのON/OFF状態を表すために「a接点」「b接点」という表現が使われます。
- a接点:ONした時に接点が繋がる。
- b接点:OFFしている時に接点が繋がっていて、ONすると離れる。
上の「ラダー図」では、接点「X0」がONすると、出力コイル「Y0」がONし「Y1」はOFFします。
・AND回路 と OR回路
AND回路
AND回路は、全ての接点が全て繋がった状態の時に、右側コイルをONさせる回路です。
上のラダー図では、「X0」がON かつ「X1」がONの時に、出力コイル「Y0」がONします。
OR回路
OR回路は、いずれかの接点が繋がった状態の時に、右側コイルをONさせる回路です。
上のラダー図では、「X0」がON または「X1」がON または両方がONの時に、 出力コイル「Y0」がONします。
・インタロック回路
「インタロック回路」とは、ある動作が実行中に、他の動作を実行させないようにするためのものです。
例えば、機械の扉が開いている時に動作しないようにしたり、モーターが正転中に逆転が実行されないようにすることで、機械の安全性や、制御順序を最適に実行させるために使用されます。
上のラダー図では、補助コイル「M0」の接点がONすると「Y0」がONします。
「Y0」がONしている時に「M1」の接点がONしても、インタロック「Y0(b接点)」がONして接点が離れているため「Y1」はONしません。
「M0」がOFFし「Y0」がOFFすると、インタロック「Y0(b接点)」がOFFして接点が繋がるため「Y1」がONできるようになります。
・自己保持回路
「自己保持回路」とは接点がONした後にOFFしても、その接点がONしたことを保持(記憶)しておくための回路です。
ON状態を保持するために、ONしたコイル自身の接点で自分のON状態を保持するため「自己保持回路」と呼ばれます。
上のラダー図では、接点「X0」がONすると、補助コイル「M0」がONして、接点「M0」がONすることで「M0」のON状態が保持されます。
この状態で「X0」がOFFしても「M0」はONのままです。
「X1」がONすることで自己保持が解除され「M0」はOFFします。
・タイマ と カウンタ
タイマ
「タイマ」は動作のタイミングを制御したいときに使用します。
ある処理を実行する前に一定時間待機させたり、ある動作を一定時間継続させたりすることができます。
上のラダー図では、「自己保持回路」と組み合わせて、「X0」がONすると「M0」がONし「T0」のカウントが開始されます。(カウント数はK10[三菱シーケンサの場合]のように設定され、0.1sタイマの場合は0.1秒×10で1.0秒のカウントになります。)
「X0」がOFFしても「T0」のカウントは継続されます。
カウントが終了して「T0」の接点がONすると「M0」がOFFし「T0」もOFFしてカウントもリセットされます。
以上のように「X0」をONするごとに「M0」を1秒間だけONさせる動作が実現できます。
カウンタ
「カウンタ」は接点がONした回数をカウントし、設定した回数ONした時にある動作を実行、または終了させたいときに使用されます。
上のラダー図では、「X0」が1回ONすると「C0」のカウント数が+1します。(カウント数はK5[三菱シーケンサの場合]のように設定され、K5は5回のカウント数になります。)
「X0」が5回ONすると「C0」の接点がONして「Y0」がONします。
カウンタは一度ONするとON状態を保持するので「Y0」はONしたままになります。
「C0」をOFFするためにはカウンタをリセットする必要があるため、リセットコマンドを別の接点条件で実行します。
上のラダーでは「X1」をONすることで「C0」のカウント数をリセットし「C0」の接点がOFFすることで「Y0」もOFFします。
5.ラダー図からPythonへの置き換え方法
ラダー図から「Python」への置き換えは、基本的には条件分岐の「if〜else 文」を使用しますが、普通に書くと長くなり、可読性も悪いので省略表記で簡潔に書きます。
タイマは「タイマー割り込み」で常時カウントできるようにし、同時に複数のタイマを正確にカウントしています。
カウンタは「カウンタクラス」を作成して必要な時に呼び出して回数をカウントします。
・スキャン動作(リフレッシュ方式)の実装
まずはシーケンサとしての基本動作を実装します。
シーケンサの最大の特徴は「最初に入力状態を取得し、作成した「ラダー」条件を、左から右へ、上から下へ処理し、最後に出力を実行するという処理を繰り返す」ことです。
この動作を「リフレッシュ方式」と言い、この1連の動作サイクルを「スキャンサイクル」と言います。
「スキャンサイクル」ごとに、最初に入力状態を取得してから「ラダー」を実行します。
実行後に内部コイルの状態を更新し出力を実行し、次の「スキャンサイクル」では取得した入力条件と更新した内部コイルの条件で再び「ラダー」が実行されます。
この動作を実装するために、プログラム内のメイン処理ループ内では以下のようにプログラムを作成します。
# メイン処理 ---------------------------------------------------------------
while True: # ずっと繰り返し
# 入力状態スキャン
x0, x1, x2, x3 = not inputs[0].value(), not inputs[1].value(), not inputs[2].value(), not inputs[3].value()
# ************* シーケンス処理(ラダーから変換) *************
'''
ここにラダー処理を記入
'''
# ************* シーケンス処理 ここまで *************
# 出力実行
outputs[0].value(y0)
outputs[1].value(y1)
outputs[2].value(y2)
outputs[3].value(y3)
メイン処理のループを実行する前に「4行目」で「ラズパイ Pico」の入力端子に接続した「ボタンスイッチ」等のON/OFF状態を取得します。
その下からラダー処理を記入していきます。
メイン処理ループの最後にラダー処理の結果に応じて「13〜16行目」のように「ラズパイ Pico」の出力端子に接続したLED等のON/OFFを実行します。
・条件分岐(if〜else 文)の省略表記
「ラダー図」を「Python」へ変換するための基本は「if〜else 文」です。
しかし、一般的な「if文」の表記ではプログラム全体が長文となり、また可読性も悪いため、省略表記で記述することで簡潔に見やすく書くことができます。
以下のラダー図を例に紹介します。
このラダー図の動作は以下になります。
「X0」がOFFしても「M0」は自己保持されてONのままなので「Y0」はONし続けます。
「X1」がONすると「M0」の自己保持が解除され「M0」はOFFし「Y0」もOFFします。
この動作を「if〜else 文」で書くために以下のように言い変えてみます。
ON状態を「True」とし、OFF状態を「False」とします。
もし「X1」が Trueなら「M0」を False
もし「M0」が Trueなら「Y0」を True、そうでなければ「Y0」をFalse
これを実際に「if〜else 文」で書くと以下になります。
if (x0 == True and x2 == False) or m0 == True:
m0 = True
if x1 == True:
m0 = False
if m0 == True:
y0 = True
else:
y0 = False
これでもラダー図の動作を再現できていますが、毎回これを書くのは大変ですし、読みにくいです。
これは以下のように省略して書くことができ、簡潔で見やすくすることができます。
m0 = (x0 and not x2) or m0 if not x1 else False
y0 = m0
上記の書き方を、もう一度以下のラダー図と見比べると、1行づつがラダー図そのものを表現していることが確認できると思います。
慣れないうちは大変に感じるかもしれませんが、パターンは決まっているので、大規模な「ラダー図」でも、簡単に「Python」で置き換えることができます。
・タイマー割り込みによる常時タイマカウント
「ラズパイ Pico」のプログラムで遅延時間を簡単に作る時は「time.sleep(1.0)」のように書きますが、この場合、タイマカウント中は割り込み処理を設定しない限り他の処理を受け付けなくなります。
入力端子ごとに割り込み処理を設定しても良いですが、「タイマー割り込み」を使用して、シーケンサの「リフレッシュ方式」を実装することで、全ての入出力端子で並列処理しているような動作を実現することができます。
サンプルプログラム内では以下のようにタイマー割り込みを実装しています。
#タイマON/OFF状態格納変数
t0 = t1 = t2 = t3 = t4 = t5 = t6 = t7 = t8 = t9 = False # タイマ(10点)
# タイマ割り込み間隔設定(ms)
PERIOD = 100
# 汎用タイマ初期設定
timer_start = [False] * 10 # タイマカウント開始信号
TIM_SET = [0] * len(timer_start) # タイマカウント値設定用初期値0(x100ms)
time_value = [0] * len(timer_start) # タイマカウント現在値格納用
# インターバルタイマ初期設定
TIM_INTERVAL0 = 5 # インターバルタイマON/OFF間隔(x0.1s)
tim_interval0 = 0 # インターバルタイマカウント現在値格納用
flicker = False # フリッカON/OFF出力用
# タイマー割り込み発生時実行関数 ----------------------------------------------
def timer_callback(timer):
# グローバル変数を宣言
global TIM_INTERVAL0, tim_interval0, flicker
global timer_start, time_value
# 汎用タイマカウント処理
t_values = [] # タイマのON/OFF結果を保持するリストを準備
# 汎用タイマカウント(汎用タイマの数だけループ)
for i in range(len(timer_start)):
if timer_start[i] and time_value[i] < TIM_SET[i]: # カウント条件ONでカウント値が設定値より小さければ
time_value[i] += 1 # タイマカウント+1
print("T{} = {}".format(i, time_value[i]))
t_values.append(time_value[i] == TIM_SET[i]) # タイマカウントが完了したかを確認しON/OFF結果を格納
time_value[i] = 0 if not timer_start[i] else time_value[i] # カウント条件OFFなら初期化
# 汎用タイマのON/OFF結果をタイマ変数に設定(汎用タイマの数だけループ)
for i, value in enumerate(t_values):
globals()[f"t{i}"] = value
# インターバルタイマ(フリッカ)
if not flicker: # インターバルOFFならカウントアップ
tim_interval0 += 1
flicker = (tim_interval0 == TIM_INTERVAL0) # カウントMAXでインターバルOFF
else: # インターバルONならカウントダウン
tim_interval0 -= 1
flicker = not (tim_interval0 == 0) # カウントMAXでインターバルOFF
# タイマー割り込み設定(間隔, モード, コールバック関数)
timer = Timer() # タイマーオブジェクトを作成
timer.init(period=PERIOD, mode=Timer.PERIODIC, callback=timer_callback)
「2行目」で「タイマON/OFF状態格納変数」を準備しています。
タイマはラダー処理の条件式内で複数回記述することになるので、簡潔に書くためにリストではなく変数で個別に宣言しています。
「5行目」でタイマ割り込み時間間隔を「ms」単位で設定します。
この時間がタイマカウントの最小値になります。
時間を短くする場合はラダー処理の「1スキャンタイム」を超えないように注意する必要があります。
「8行目」で使用するタイマの数を指定しています。初期値は10点です。
タイマを増やす場合は「タイマON/OFF状態格納変数」を増やして、ここの要素数も増やします。
「13〜15行目」では汎用的に使用できる「0.5秒のフリッカ(ON/OFF繰り返し)タイマ」を準備しています。必要に応じて使用してください。
サンプルプログラムでは「ラズパイ Pico」本体のLEDを点滅させるために使用しています。
「18行目」からタイマー割り込み発生時に呼び出す関数を定義しています。
ラダー処理内でタイマのカウント条件が成立している時に、各タイマに設定された時間カウント処理が行われます。フリッカタイマのカウントもここで行います。
「47,48行目」でタイマー割り込みを使用するための設定を行います。
タイマーを使用するにはメインループ内の「シーケンス処理記入部」に以下のように記入します。
以下は「T0:タイマ」を使用する例です。
# T0 タイマ例 ****************************
x2 t0
--| |-┬-|/|-┬----------( m1 )--
m1 | |
--| |-┘ └--------(t0 K10)--
m1
--| |------------------( y1 )--
'''
# Pythonへ変換(T0 タイマ例)
m1 = x2 or m1 if not t0 else False
TIM_SET[0] = 10 # カウント時間設定(x0.1s)
timer_start[0] = m1 # タイマ0 のカウント条件として m1 を指定
y1 = m1
上コードの「12行目」でタイマのカウント時間を設定しています。
タイマ「T0」のカウント時間を設定するには「TIM_SET」リストの要素「0」に数値を指定します。
「13行目」でタイマカウントを開始する条件を設定しています。
タイマ「T0」のカウント条件は「timer_start」リストの要素「0」に指定し、この条件が「True」になるとカウントが開始されます。
タイマカウントが終了するとタイマ変数「t0」が「True」になります。
カウント条件が「False」になると「t0」は「False」になり、カウント時間もリセットされます。
・カウンタークラス作成によるカウンタ
カウンタの回数カウントは「カウンタークラス」を作成して、必要な時に呼び出してカウントを行います。
#カウンタON/OFF状態格納変数
c0 = c1 = c2 = c3 = c4 = c5 = c6 = c7 = c8 = c9 = False # カウンタ(10点)
# カウンタ処理クラス ---------------------------------------------------------
class Counter:
# インスタンス変数
def __init__(self):
self.c_state = [False] * 10 # カウンタ状態格納用(10点)
self.cnt = [0] * 10 # カウンタカウント数保持用(10点)
# カウント実行(self, カウント条件, カウンタ番号, カウント値)
def counter(self, coil, num, value):
"""
カウンタの値を1つ増やします。
Args:
coil (bool): カウント条件が満たされたかどうかを表すブール値
num (int): カウンタの番号
value (int): カウントする最大値
Returns:
bool: カウンタが指定の最大値に達した場合はTrue、それ以外の場合はFalse
"""
if coil and not self.c_state[num] and self.cnt[0] < value:
self.cnt[num] += 1
self.c_state[num] = True
print("C = ", self.cnt[num])
if self.cnt[num] == value:
print("C{} ON!".format(num))
return True
if not coil: # カウント条件がOFFなら カウンタ状態をFalseへ
self.c_state[num] = False
return False
# カウント数リセット
def reset_counter(self, num):
self.cnt[num] = 0
count = Counter() # カウンタのインスタンスを作成
「2行目」で「カウンタのON/OFF状態格納変数」を準備しています。
ラダー処理の条件式内で複数回記述することになるので、簡潔に書くためにリストではなく変数で個別に宣言しています。
「カウンタークラス」内の「8,9行目」で使用するカウンタの数を指定しています。初期値は10点です。
カウンタを増やす場合は「カウンタON/OFF状態格納変数」を増やして、ここの要素数も増やします。
「12行目」の「count.counter()」関数を呼び出すことでカウンタの回数カウントを実行します。
この関数は以下のように呼び出します。
・カウント条件:カウントする条件を設定します。x1 やm1 のように接点変数を指定してもいいですし、「x1 and not m1(x1がON かつ m1がOFF なら)」のように条件式を設定してもいいです。
・カウンタ番号:使用するカウンタ番号を、1 や2 のように数値で設定します。
・カウント数:カウント回数の設定値を、3 や10 のように数値で設定します。
実際は以下のように指定します。
以下の例ではカウンタ「c0」を使用して「x1」がONした回数を3回カウントします。
if not c0: # カウンタC0がOFFなら
c0 = count.counter(x1, 0, 3) # カウント実行関数呼び出し(カウント条件, カウンタ番号, カウント数)
「34行目」でカウンタをリセットする「count.reset_counter()」関数を定義しています。
カウンタをリセットするときは、リセットしたいカウンタ番号を指定して以下のように「count.reset_counter()」呼び出します。
if x1: # X1がONなら
c0 = False # カウンタC0をOFF
count.reset_counter(0) # カウンタC0のカウント数を0リセット(カウンタ番号)
上コードでは「x1」がONしたらカウンタ「c0」を「False」に設定し、リセット関数を呼び出し、カウント数をリセットしています。
6.サンプルプログラム
実際にサンプルプログラムを使用して「ラダー図」を「Python」に変換して動作確認してみましょう。
各サンプルプログラムを開発環境(Thonny等)にコピペで貼り付けて実行してください。
開発環境「Thonny」のインストール方法や使い方は以下のリンクで詳しく紹介しています。
・サンプル1:a接点、b接点、AND回路、OR回路
基本的なラダープログラム「a接点」「b接点」「AND回路」「OR回路」の「Python」での記述方法を確認し、サンプルプログラムで動作確認を行います。
ラダー図はサンプルプログラム内のコメントで書いてあります。
「ラズパイ Pico」を使用した動作は以下のようになります。
[LED赤(Y0)・青(Y1)]で「a接点」「b接点」の動作確認を行います。
[LED青(Y1)]は「b接点」に繋がっているので最初から点灯しています。
[スイッチ赤(X0)]を押すと[LED青(Y1)]が消灯し、[LED赤(Y0)]が点灯します。[本体LED緑]も[LED赤(Y0)]と一緒に点灯します。
[LED緑(Y2)]で「AND回路」の動作確認を行います。
[スイッチ赤(X0)と青(X1)]を同時に押すと、[LED緑(Y2)]が点灯します。
[LED黄(Y3)]で「OR回路」の確認を行います。
[スイッチ白(X2)]を押すと[LED黄(Y3)]が点灯します。
[スイッチ黄(X3)]を押しても[LED黄(Y3)]が点灯します。
[スイッチ白(X2)と黄(X3)]両方を押しても[LED黄(Y3)]が点灯します。
サンプルプログラムは以下になります。コピペで貼り付けて実行してください。
黒枠内の右上アイコンのクリックでもコピーできます。
from machine import * # 制御用モジュールを準備
# 本体LED(GP25)を出力設定
led = Pin(25, Pin.OUT)
# INPUT:入力端子(x0〜x3)設定(プルアップ)
input_pins = [3, 7, 11, 15] # 使用する入力端子を設定(リスト作成)
inputs = [Pin(pin, Pin.IN, Pin.PULL_UP) for pin in input_pins]
# OUTPUT:出力端子(y0〜y3)設定
output_pins = [28, 26, 21, 16] # 使用する出力端子を設定(リスト作成)
outputs = [Pin(pin, Pin.OUT) for pin in output_pins]
# 初期化
x0 = x1 = x2 = x3 = False # 入力接点(4点)
y0 = y1 = y2 = y3 = False # 出力リレー(4点)
m0 = m1 = m2 = m3 = m4 = m5 = m6 = m7 = m8 = m9 = \
m10 = m11 = m12 = m13 = m14 = m15 = m16 = m17 = m18 = m19 = False # 補助リレー(20点)
# メイン処理 ---------------------------------------------------------------
while True: # ずっと繰り返し
# 入力状態スキャン
x0, x1, x2, x3 = not inputs[0].value(), not inputs[1].value(), not inputs[2].value(), not inputs[3].value()
# ************* シーケンス処理(ラダーから変換) *************
'''
# a接点、b接点 *********************
x0
--| |------------------( y0 )--
x0
--|/|------------------( y1 )--
'''
# Pythonへ変換(基本ラダー例)
y0 = x0
y1 = not x0
'''
# AND、OR *************************
x0 x1
--| |---| |------------( y2 )--
x2
--| |-┬----------------( y3 )--
x3 |
--| |-┘
'''
y2 = x0 and x1
y3 = x2 or x3
# 本体LED動作(y0がONなら点灯)
led_state = y0
led.value(led_state)
# ************* シーケンス処理 ここまで *************
# 出力実行
outputs[0].value(y0)
outputs[1].value(y1)
outputs[2].value(y2)
outputs[3].value(y3)
・サンプル2:自己保持回路、出力方法の確認
「自己保持回路」の「Python」での記述方法を確認し、サンプルプログラムで動作確認を行います。
出力コイルを使用した、各条件成立時の出力方法についても確認します。
ラダー図はサンプルプログラム内のコメントで書いてあります。
「ラズパイ Pico」を使用した動作は以下のようになります。
[LED赤(Y0)]で「自己保持回路」の確認を行います。
[スイッチ赤(X0)]を押すと[LED赤(Y0)]が点灯します。
[LED黄(Y3)]は出力部で[LED赤(Y0)]とOR条件で出力設定しているため、一緒に点灯します。
[スイッチ赤(X0)]を離しても「自己保持」されているため、[LED]は点灯したままです。
[スイッチ青(X1)]を押すと「自己保持」が解除されて[LED]が消灯します。
[LED青(Y1)]で複合条件での「自己保持」動作の確認を行います。
[スイッチ赤(X0)と青(X1)]を同時に押した時に[LED青(Y1)]が点灯し「自己保持」されます。
[LED青(Y1)]の「自己保持」は、[スイッチ白(X2)]を押しても、[スイッチ黄(X3)]を押しても解除されて消灯します。
[スイッチ赤(X0)]と[スイッチ青(X1)]の押し方と順番によっては、上写真のように全ての[LED]を点灯させることができます。
出力部の「出力条件AND/OR」を確認しながら、どのような押し方をしたら全点灯できるか考えてみましょう。
サンプルプログラムは以下になります。コピペで貼り付けて実行してください。
黒枠内の右上アイコンのクリックでもコピーできます。
from machine import * # 制御用モジュールを準備
# 本体LED(GP25)を出力設定
led = Pin(25, Pin.OUT)
# INPUT:入力端子(x0〜x3)設定(プルアップ)
input_pins = [3, 7, 11, 15] # 使用する入力端子を設定(リスト作成)
inputs = [Pin(pin, Pin.IN, Pin.PULL_UP) for pin in input_pins]
# OUTPUT:出力端子(y0〜y3)設定
output_pins = [28, 26, 21, 16] # 使用する出力端子を設定(リスト作成)
outputs = [Pin(pin, Pin.OUT) for pin in output_pins]
# 初期化
x0 = x1 = x2 = x3 = False # 入力接点(4点)
y0 = y1 = y2 = y3 = False # 出力リレー(4点)
m0 = m1 = m2 = m3 = m4 = m5 = m6 = m7 = m8 = m9 = \
m10 = m11 = m12 = m13 = m14 = m15 = m16 = m17 = m18 = m19 = False # 補助リレー(20点)
# メイン処理 ---------------------------------------------------------------
while True: # ずっと繰り返し
# 入力状態スキャン
x0, x1, x2, x3 = not inputs[0].value(), not inputs[1].value(), not inputs[2].value(), not inputs[3].value()
# ************* シーケンス処理(ラダーから変換) *************
'''
# 自己保持1 *************************
x0 x1
--| |-┬-|/|---------------( m0 )--
m0 |
--| |-┘
'''
m0 = x0 or m0 if not x1 else False
'''
# 自己保持2 *************************
x0 x1 x2 x3
--| |---| |-┬-|/|---|/|---( m1 )--
m1 |
--| |-------┘
'''
m1 = (x0 and x1) or m1 if (not x2 and not x3) else False
'''
# 出力 *****************************
x0
--| |---------------------( y0 )--
m1
--| |---------------------( y1 )--
m0 m1
--| |---| |---------------( y2 )--
m0
--| |-┬-------------------( y3 )--
m1 |
--| |-┘
'''
y0 = m0
y1 = m1
y2 = m0 and m1
y3 = m0 or m1
# 本体LED動作(y0がONなら点灯)
led_state = y0
led.value(led_state)
# ************* シーケンス処理 ここまで *************
# 出力実行
outputs[0].value(y0)
outputs[1].value(y1)
outputs[2].value(y2)
outputs[3].value(y3)
・サンプル3:タイマ、カウンタ
「タイマ」「カウンタ」を使用したラダーの「Python」での記述方法を確認し、サンプルプログラムで動作確認を行います。
ラダー図はサンプルプログラム内のコメントで書いてあります。
「ラズパイ Pico」を使用した動作は以下のようになります。
サンプルプログラムを実行すると[本体LED緑]が点滅します。
「タイマ」カウントには「自己保持回路」使用するので[LED赤(Y0)]でおさらいです。
[スイッチ赤(X0)]を押すと[LED赤(Y0)]が点灯し「自己保持」されます。
[スイッチ青(X1)]を押すと[LED赤(Y0)]は消灯します。
「タイマ」の動作確認をします。
[スイッチ白(X2)]を押すと[LED青(Y1)]が点灯します。
[スイッチ白(X2)]を押してから1秒後に[LED青(Y1)]は消灯します。
[スイッチ白(X2)]をどんなタイミングで押しても正確に1秒後に消灯し、[本体LED緑]は一定間隔で点滅を繰り返し続けることが確認できます。
「カウンタ」の動作確認を行います。
「カウント数」の設定値は3です。
[スイッチ黄(X3)]を押すと[LED緑(Y2)]が点灯しカウンタ「C0」のカウント数が+1されます。(シリアルモニタで確認できます)
[スイッチ黄(X3)]を合計3回押すとカウンタ「C0」がONし、[LED黄(Y3)]が点灯します。
[スイッチ青(X1)]を押すと[LED黄(Y3)]が消灯し、カウンタ「C0」がOFFしてカウント数もリセットされます。
サンプルプログラムは以下になります。コピペで貼り付けて実行してください。
黒枠内の右上アイコンのクリックでもコピーできます。
from machine import * # 制御用モジュールを準備
# 本体LED(GP25)を出力設定
led = Pin(25, Pin.OUT)
# INPUT:入力端子(x0〜x3)設定(プルアップ)
input_pins = [3, 7, 11, 15] # 使用する入力端子を設定(リスト作成)
inputs = [Pin(pin, Pin.IN, Pin.PULL_UP) for pin in input_pins]
# OUTPUT:出力端子(y0〜y3)設定
output_pins = [28, 26, 21, 16] # 使用する出力端子を設定(リスト作成)
outputs = [Pin(pin, Pin.OUT) for pin in output_pins]
# 初期化
x0 = x1 = x2 = x3 = False # 入力接点(4点)
y0 = y1 = y2 = y3 = False # 出力リレー(4点)
m0 = m1 = m2 = m3 = m4 = m5 = m6 = m7 = m8 = m9 = \
m10 = m11 = m12 = m13 = m14 = m15 = m16 = m17 = m18 = m19 = False # 補助リレー(20点)
t0 = t1 = t2 = t3 = t4 = t5 = t6 = t7 = t8 = t9 = False # タイマ(10点)
c0 = c1 = c2 = c3 = c4 = c5 = c6 = c7 = c8 = c9 = False # カウンタ(10点)
# タイマ割り込み間隔設定(ms)
PERIOD = 100
# 汎用タイマ初期設定
timer_start = [False] * 10 # タイマカウント開始信号
TIM_SET = [0] * len(timer_start) # タイマカウント値設定用初期値0(x100ms)
time_value = [0] * len(timer_start) # タイマカウント現在値格納用
# インターバルタイマ初期設定
TIM_INTERVAL0 = 5 # インターバルタイマON/OFF間隔(x0.1s)
tim_interval0 = 0 # インターバルタイマカウント現在値格納用
flicker = False # フリッカON/OFF出力用
# タイマー割り込み発生時実行関数 ----------------------------------------------
def timer_callback(timer):
# グローバル変数を宣言
global TIM_INTERVAL0, tim_interval0, flicker
global timer_start, time_value
# 汎用タイマカウント処理
t_values = [] # タイマのON/OFF結果を保持するリストを準備
# 汎用タイマカウント(汎用タイマの数だけループ)
for i in range(len(timer_start)):
if timer_start[i] and time_value[i] < TIM_SET[i]: # カウント条件ONでカウント値が設定値より小さければ
time_value[i] += 1 # タイマカウント+1
print("T{} = {}".format(i, time_value[i]))
t_values.append(time_value[i] == TIM_SET[i]) # タイマカウントが完了したかを確認しON/OFF結果を格納
time_value[i] = 0 if not timer_start[i] else time_value[i] # カウント条件OFFなら初期化
# 汎用タイマのON/OFF結果をタイマ変数に設定(汎用タイマの数だけループ)
for i, value in enumerate(t_values):
globals()[f"t{i}"] = value
# インターバルタイマ(フリッカ)
if not flicker: # インターバルOFFならカウントアップ
tim_interval0 += 1
flicker = (tim_interval0 == TIM_INTERVAL0) # カウントMAXでインターバルOFF
else: # インターバルONならカウントダウン
tim_interval0 -= 1
flicker = not (tim_interval0 == 0) # カウントMAXでインターバルOFF
# タイマー割り込み設定(間隔, モード, コールバック関数)
timer = Timer() # タイマーオブジェクトを作成
timer.init(period=PERIOD, mode=Timer.PERIODIC, callback=timer_callback)
# カウンタ処理クラス ---------------------------------------------------------
class Counter:
# インスタンス変数
def __init__(self):
self.c_state = [False] * 10 # カウンタ状態格納用(10点)
self.cnt = [0] * 10 # カウンタカウント数保持用(10点)
# カウント実行(self, カウント条件, カウンタ番号, カウント値)
def counter(self, coil, num, value):
"""
カウンタの値を1つ増やします。
Args:
coil (bool): カウント条件が満たされたかどうかを表すブール値
num (int): カウンタの番号
value (int): カウントする最大値
Returns:
bool: カウンタが指定の最大値に達した場合はTrue、それ以外の場合はFalse
"""
if coil and not self.c_state[num] and self.cnt[0] < value:
self.cnt[num] += 1
self.c_state[num] = True
print("C = ", self.cnt[num])
if self.cnt[num] == value:
print("C{} ON!".format(num))
return True
if not coil: # カウント条件がOFFなら カウンタ状態をFalseへ
self.c_state[num] = False
return False
# カウント数リセット
def reset_counter(self, num):
self.cnt[num] = 0
count = Counter() # カウンタのインスタンスを作成
# メイン処理 ---------------------------------------------------------------
while True: # ずっと繰り返し
# 入力状態スキャン
x0, x1, x2, x3 = not inputs[0].value(), not inputs[1].value(), not inputs[2].value(), not inputs[3].value()
# ************* シーケンス処理(ラダーから変換) *************
'''
# 自己保持動作確認 *************************
x0 x1
--| |-┬-|/|------------( m0 )--
m0 |
--| |-┘
m0
--| |------------------( y0 )--
'''
# Pythonへ変換(基本ラダー例)
m0 = x0 or m0 if not x1 else False
y0 = m0
'''
# T0 タイマ例 ****************************
x2 t0
--| |-┬-|/|-┬----------( m1 )--
m1 | |
--| |-┘ └--------(t0 K10)--
m1
--| |------------------( y1 )--
'''
# Pythonへ変換(T0 タイマ例)
m1 = x2 or m1 if not t0 else False
TIM_SET[0] = 10 # カウント時間設定(x0.1s)
timer_start[0] = m1 # タイマ0 のカウント条件として m1 を指定
y1 = m1
'''
# C0 カウンタ例 ****************************
x3
--| |---------------( c0 K3 )--
x1
--| |--------------( RST c0 )--
x3 c0
--| |---|/|------------( y2 )--
c0
--| |------------------( y3 )--
'''
# Pythonへ変換(C0 カウンタ例)
if not c0: # 動作サイクルカウンタ
c0 = count.counter(x3, 0, 3) # (カウント条件, カウンタ番号, カウント数)
if x1: # C0カウンタリセット
c0 = False
count.reset_counter(0) # カウンタリセット(カウンタ番号)
y2 = x3 and not c0
y3 = c0
# 本体LED動作(常時点滅、m0がONなら点灯)
led_state = flicker or m0
led.value(led_state)
# ************* シーケンス処理 ここまで *************
# 出力実行
outputs[0].value(y0)
outputs[1].value(y1)
outputs[2].value(y2)
outputs[3].value(y3)
・サンプル4:モーターの正逆繰り返し例
最後に応用編として実用的な「モーターの正逆繰り返し」動作を想定した「ラダー図」を「Python」に置き換えて動作確認していきます。
動作は以下のようになります。
今回は20行(62ステップ)あるラダーをそのまま「Python」に置き換えて記述しています。
これには「AND、OR、インタロック、自己保持、タイマ、カウンタ」の全てを含みます。
ラダー全体画像も載せていますので、見比べながら確認してみてください。
「ラズパイ Pico」を使用した動作は以下のようになります。
LED赤が「正転」、LED青が「逆転」
LED緑が「逆転起動待ち状態」、LED黄が「正転起動待ち状態」を表します。
スイッチ赤は「自動運転ボタン」、スイッチ青は「停止ボタン」
スイッチ白は「手動正転ボタン」、スイッチ黄は「手動逆転ボタン」として使用します。
プログラムを実行すると[本体LED緑]が点灯します。
[スイッチ赤(自動運転ボタン)]を押すと自動運転が開始され、[LED赤(正転)]が点灯します。
自動運転中は[本体LED緑]が点滅し続けます。
[スイッチ青(停止ボタン)]が押されると自動運転は終了します。
2秒後に[LED赤(正転)]が消灯し、[LED緑(逆転起動待ち状態)]が点灯します。
1秒後に[LED青(逆転)]が点灯します
2秒後に[LED青(逆転)]が消灯し、[LED黄(正転起動待ち状態)]が点灯します。
1秒後に[LED緑・黄]が消灯し再度[LED赤(正転)]点灯から繰り返されます。
この動作を3回繰り返すと自動停止します。
使用したラダー図(モーターの正逆繰り返し)は下画像のようになります。
サンプルプログラムは以下になります。コピペで貼り付けて実行してください。
黒枠内の右上アイコンのクリックでもコピーできます。
from machine import * # 制御用モジュールを準備
# 本体LED(GP25)を出力設定
led = Pin(25, Pin.OUT)
# INPUT:入力端子(x0〜x3)設定(プルアップ)
input_pins = [3, 7, 11, 15] # 使用する入力端子を設定(リスト作成)
inputs = [Pin(pin, Pin.IN, Pin.PULL_UP) for pin in input_pins]
# OUTPUT:出力端子(y0〜y3)設定
output_pins = [28, 26, 21, 16] # 使用する出力端子を設定(リスト作成)
outputs = [Pin(pin, Pin.OUT) for pin in output_pins]
# 初期化
x0 = x1 = x2 = x3 = False # 入力接点(4点)
y0 = y1 = y2 = y3 = False # 出力リレー(4点)
m0 = m1 = m2 = m3 = m4 = m5 = m6 = m7 = m8 = m9 = \
m10 = m11 = m12 = m13 = m14 = m15 = m16 = m17 = m18 = m19 = False # 補助リレー(20点)
t0 = t1 = t2 = t3 = t4 = t5 = t6 = t7 = t8 = t9 = False # タイマ(10点)
c0 = c1 = c2 = c3 = c4 = c5 = c6 = c7 = c8 = c9 = False # カウンタ(10点)
# タイマ割り込み間隔設定(ms)
PERIOD = 100
# 汎用タイマ初期設定
timer_start = [False] * 10 # タイマカウント開始信号
TIM_SET = [0] * len(timer_start) # タイマカウント値設定用初期値0(x100ms)
time_value = [0] * len(timer_start) # タイマカウント現在値格納用
# インターバルタイマ初期設定
TIM_INTERVAL0 = 5 # インターバルタイマON/OFF間隔(x0.1s)
tim_interval0 = 0 # インターバルタイマカウント現在値格納用
flicker = False # フリッカON/OFF出力用
# タイマー割り込み発生時実行関数 ----------------------------------------------
def timer_callback(timer):
# グローバル変数を宣言
global TIM_INTERVAL0, tim_interval0, flicker
global timer_start, time_value
# 汎用タイマカウント処理
t_values = [] # タイマのON/OFF結果を保持するリストを準備
# 汎用タイマカウント(汎用タイマの数だけループ)
for i in range(len(timer_start)):
if timer_start[i] and time_value[i] < TIM_SET[i]: # カウント条件ONでカウント値が設定値より小さければ
time_value[i] += 1 # タイマカウント+1
print("T{} = {}".format(i, time_value[i]))
t_values.append(time_value[i] == TIM_SET[i]) # タイマカウントが完了したかを確認しON/OFF結果を格納
time_value[i] = 0 if not timer_start[i] else time_value[i] # カウント条件OFFなら初期化
# 汎用タイマのON/OFF結果をタイマ変数に設定(汎用タイマの数だけループ)
for i, value in enumerate(t_values):
globals()[f"t{i}"] = value
# インターバルタイマ(フリッカ)
if not flicker: # インターバルOFFならカウントアップ
tim_interval0 += 1
flicker = (tim_interval0 == TIM_INTERVAL0) # カウントMAXでインターバルOFF
else: # インターバルONならカウントダウン
tim_interval0 -= 1
flicker = not (tim_interval0 == 0) # カウントMAXでインターバルOFF
# タイマー割り込み設定(間隔, モード, コールバック関数)
timer = Timer() # タイマーオブジェクトを作成
timer.init(period=PERIOD, mode=Timer.PERIODIC, callback=timer_callback)
# カウンタ処理クラス ---------------------------------------------------------
class Counter:
# インスタンス変数
def __init__(self):
self.c_state = [False] * 10 # カウンタ状態格納用(10点)
self.cnt = [0] * 10 # カウンタカウント数保持用(10点)
# カウント実行(self, カウント条件, カウンタ番号, カウント値)
def counter(self, coil, num, value):
"""
カウンタの値を1つ増やします。
Args:
coil (bool): カウント条件が満たされたかどうかを表すブール値
num (int): カウンタの番号
value (int): カウントする最大値
Returns:
bool: カウンタが指定の最大値に達した場合はTrue、それ以外の場合はFalse
"""
if coil and not self.c_state[num] and self.cnt[0] < value:
self.cnt[num] += 1
self.c_state[num] = True
print("C = ", self.cnt[num])
if self.cnt[num] == value:
print("C{} ON!".format(num))
return True
if not coil: # カウント条件がOFFなら カウンタ状態をFalseへ
self.c_state[num] = False
return False
# カウント数リセット
def reset_counter(self, num):
self.cnt[num] = 0
count = Counter() # カウンタのインスタンスを作成
# メイン処理 ---------------------------------------------------------------
while True: # ずっと繰り返し
# 入力状態スキャン
x0, x1, x2, x3 = not inputs[0].value(), not inputs[1].value(), not inputs[2].value(), not inputs[3].value()
# ************* シーケンス処理(ラダーから変換) *************
# M0:自動運転中
m0 = (x0 and not x2 and not x3) or m0 if m1 else False
# M1:停止指令
m1 = not x1 and not c0
# M2:自動正転指令
m2 = m0 and not m3
TIM_SET[0] = 20
timer_start[0] = m2 # 正転時間タイマ
# M3:自動逆転停止信号
m3 = t0 or m3 if m0 and not t3 else False
TIM_SET[1] = 10
timer_start[1] = m3 # 逆転起動待ちタイマ
# M4:自動逆転指令
m4 = t1 or m4 if m0 and not t3 and not m5 else False
TIM_SET[2] = 20
timer_start[2] = m4 # 逆転時間タイマ
# M5:自動逆転停止信号
m5 = t2 or m5 if m0 and not t3 else False
TIM_SET[3] = 10
timer_start[3] = m5 # 正転起動待ちタイマ
if not c0: # 動作サイクルカウンタ
c0 = count.counter(m5, 0, 3) # (カウント条件, カウンタ番号, カウント数)
if x1: # C0カウンタリセット
c0 = False
count.reset_counter(0) # カウンタリセット(カウンタ番号)
# M6::手動正転指令
m6 = x2 and not m0 and not m7
# M7:手動逆転指令
m7 = x3 and not m0 and not m6
# 出力処理
y0 = m2 or m6 if not y1 else False # 正転
y1 = m4 or m7 if not y0 else False # 逆転
y2 = m3 # 逆転待ち(内部補助リレー確認用)
y3 = m5 # 正転待ち(内部補助リレー確認用)
# 本体LED動作(自動運転ONで点滅、OFFで点灯)
led_state = (m0 and flicker) or not m0
led.value(led_state)
# ************* シーケンス処理 ここまで *************
# 出力実行
outputs[0].value(y0)
outputs[1].value(y1)
outputs[2].value(y2)
outputs[3].value(y3)
7.まとめ
シーケンサのラダーを「Python」で書く方法について詳しく紹介しました。
シーケンサは、工場の生産ラインの機械や、モーター、バルブ、ポンプなどの駆動装置をスイッチやセンサーと連携させ、必要なタイミングでON/OFF制御することが容易なため、産業用途で広く普及していますが、学習コストは比較的高く、学習できる環境も多くはありません。
そんなシーケンサのラダーは「Python」で表現することができ、「ラズパイ Pico」のようなマイコンボードを使用することで簡単に動作確認できます。
今回は「Python」を使用しましたが、基本は条件分岐の「if〜else 文」を使用するだけなので「C言語」等の他の言語でも置き換えられます。
シーケンサのラダーを理解し、プログラム言語へ置き換える方法を理解することで、シーケンサで開発した制御プログラムを「ラズパイ Pico」のようなマイコンボードに簡単に置き換えることもできます。
シーケンサで開発したものを量産のために基板化することも容易になり、プログラムに変更があった場合でも「ラダー図」の修正から置き換えるだけで、細かいタイミング調整を気にする必要がないため、デバッグも容易になります。
今回で「Python」を使用したシーケンサの基本的な動作を実装することができました。
もっと効率の良い実装方法はあると思いますが・・・ひとまず満足です♪
次回は、もっと実用的に「ラズパイ Pico」をシーケンサとして使用できるように、周辺回路を実装した基板設計について紹介しています。
基板設計は何年振りでしょう^^; まずは思い出すところからですが、今は個人で基板を設計するのも、かなり安くできるようなので、プログラミングはちょっとお休みして、ひさしぶりに基板製作を楽しみたいと思います♪
基板製作でもやっぱり「Let’s 路地からThinking〜!!」でいきます。ぜひご覧ください!
コメント