制御系のソフトを考える時に…

パソコン関係のプログラムと違い、マイコンで制御を行う場合のプログラムの組み方にはちょっと独特のものがあるので、掻い摘んで入り口だけ解説してみようかな…と(^_^;)。

というのも、プログラムが苦手な人って実は案外「正しい組み方を知らない」だけだったりするんだよな。無理な組み方で頭が爆発して「もう無理だー」ってなってるケースが多いんぢゃなかろうか。なので、そうならないための手ほどきを少し書いてみる。

一応一般的な風味で書いてるけど、最終的にはRCJサッカー方面をネタにする。アレはこういう観点でも興味深い競技だからね。

●全てはココから始まる

一番シンプルな制御プログラムは、マイコンがリセットされたら、任意のポートの値を読み(入力)、それに応じて任意のポートの値をセット(出力)して終わる…というものだろう。入力はスイッチか何かが繋がっていて、出力にはLEDとかでもイイかな。

入力の値はリセット直後しか読み込まれないので、入力の変化を反映させたかったら毎回リセットするしかないが、一応入力を出力に反映しているという点で立派に制御を行なっているとも言える(かもしれない)。

●リアルタイム性を求めて

と言ってもそんなにめんどくさいお話ではなく、これもまだ初歩の初歩。最初の例では、スイッチの状況はリアルタイムには反映されない。当然だよね。では、スイッチの状況をリアルタイムに反映させるにはどうするか。

プログラムが永遠に長く続けられるなら、コピペで入力ポート読むのと出力ポートセットするのをダ〜〜〜っと長く何万行も書けば良い(笑)のだけど、それはいろんな意味でアホらしい(とはいえ、実はこれもタマに有用なコトもある)ので、普通は何らかの方法で繰り返すループ構造のプログラムを組み、その中で入力ポートを読んで出力ポートをセットするという作業を行う。ループが無限に回るようになっていれば、無限に入力ポートが読み出され無限に出力ポートセットが行われる。

たぶん、ここまでぐらいならみんな普通に思いつく。で、実は問題はこの先にあるんだね。ちなみに、Arduinoが最初からsetup()とloop()を持っているのはこの考え方に基づくんだと思う。何事にも初期設定があるのでそれをsetup()で行い、loop()で無限に繰り返すワケだ。

●複数の入力から動きを決めたい

ある機械には複数の入力があって、その入力のそれぞれから出力を決めたいってコトがある…というより、むしろこういうのが大半やね。さて、これを条件付けたい場合どう考えれば良いだろうか。

多くの場合、プログラム言語には判断文とか条件文があって、それで判断を行うコトができる。一番定番なのはif文といって、正か疑かとかオンかオフかとかでで判断する。であれば、複数の入力を順次if文で判断して出力を決めればよい…確かにその通り。

では、入力が2本(それぞれオンかオフかの値を取るとする)の場合、if文はいくつ必要だろうか。最初に一方の値を判断し、そのそれぞれの先でもう一方の値を判断するワケだから、都合3つのif文が必要になる。まぁ、なんとかなるかな。

では入力が3本になったら…4本になったら…5本になったら…そろそろ、if文の数が爆発してるのではなかろうか。ヲイラはコレを「if文の塊」と呼んでいる。そして、どう考えてもこのやり方ではもう、正しい判断を下しているのかどうかを判定することは恐ろしく難しいと言わざるをえない。

ではどうすれば良いのだろうか。いくつかの方法があるのだがヲイラ的なオススメはswitch文を使うコトだ。むしろ、switch文の無い言語ではこういったコトを実装すべきではないとさえ思う。

switch文の多くは、あらかじめいくつかの値が列挙されていて、変数の値と比較し合致する値のトコに書かれてる行を実行するという仕掛けになっている。ならば、ある変数を用意し、各入力に応じて1, 2, 4, 8, 16といった数値を加算した結果をswitch文に判断させればよい。例えば入力Aがオンなら1を加算、入力Bがオフならなにもしない、入力Cがオンなら4を加算という具合だ。

これは16進数のコトを考えればピンとくると思うが、2のn乗の値をそれぞれの入力に応じて加算した数値というのは、入力の状況に応じて一意に決まる。

したがって、switch文の中にある選択用の数値は、そのまま入力の組み合わせに一致するワケだ。あとは、どういう組み合わせの時にどういう処理をするのかを書いていけば良い。

そして、実はむしろコッチが重要なのだが、switch文には基本的にデフォルト行が存在する。これは、列挙されている選択用の数値のどれにも当てはまらなかった場合に実行される行であり、全ての選択肢から漏れた場合にどうすべきかを一箇所に記述できる。if文の塊だと、コレをきちんとやるのが至難の技なんだよね。つか、そんな無駄な苦労をやるべきではない。そんな無駄な苦労をするから、苦手だと思うようになっていくのな。

●複数の入力から複数の動きをさせたい

さて本命のコーナーにようやくたどり着いた。入力が複数あって、なおかつその状況によって出力を変えたい場合はどうするか。先のケースと違って、同じような入力/出力であっても、その意味が違う場合がある。

そろそろRCJサッカー的なお話を混ぜていこうと思うが、例えば単純に移動するのであっても、ボールが相手ゴール側にあるのか自ゴール側にあるのかでは動くべき方向が違う。相手ゴール側ならまっすぐボールへ向かえば良いが、自ゴール側でソレをやると当然オウンゴールになってしまう。

つまり、同じ移動であってもその状況によって異なる動作をしなければならない時にどうするか。無論、これも例によってif文の塊でやれなくはないが、さすがにもう地獄なのは目に見えているよね。

ではswitch文で全て解決するのか?というと、実はそれも案外難しい。なぜなら、入力的には同じ状況であっても意味合いが違う場合があるからだ。さてどうするか。

実はここで「状態(モード)」という考え方を導入し、「状態遷移」という考え方を導入すると、割とスッキリするコトが多い。最初はアイドリングモードから始める場合が多いが、そこからどういう入力の組み合わせの時にどのモードへ遷移し、そこからさらに別のモードへ遷移するにはどういう入力の組み合わせが発生した時かを検討する。

たとえば、アイドリングモードの他に攻撃モード(相手ゴール方向へまっすぐボールを追いかける)、回り込みモード(自ゴール側にあるボールを拾いに行く)などと設定し、それぞれのモード間での遷移条件を決めていく。各モードのそれぞれでループ文を書き、条件判断のためのswitch文を並べていけば、スッキリと整理されたプログラムになるのではないだろうか。

こういう状態遷移を検討/実行する部分のコトを戦略層とヲイラは呼んでいる。機体がどういう判断を下し、どう動いていくのかは、この部分で決定されるワケやね。ロボット業界では行動決定と呼ばれてるかな。

また、これぐらい複雑になってくると、たとえば動力系への指示なんかも一本化しないと手こずるコトになる。アッチコッチでバラバラにポートをセットしていると、もうどこか間違ったのかもわからない。したがって、それらをまとめた関数を作り、必ずその関数経由でポートへアクセスするようにプログラムする必要が出てくる。こういったいわゆる下位層のプログラムは、ドライバ層と呼んでいる。

当然、入力関係もやはりまとめていかないと厳しくなるのは自明なので、同様に下位層としてまとめドライバ層としてプログラムしていくコトになる。

つまり、ドライバ層から得た情報を元に戦略層のプログラムが下した判断を、ドライバ層が実際にハードウエアを動かすコトで実行していくというワケだ。これ、普段使っているパソコンとかでも実は同じような構造になってるんだよね。

こうする事で、後日ハードウエアが軽度の改善を受けた程度であれば、ドライバ層だけ書き直せば済むコトになる。こういったちょっとしたプログラミングのコツなんかは、たぶんあんまり教えるトコはなのかもしれないね。

なお、ドライバ層をきちんと設定することは実はデバッグにも大きな意味を持つ。通常、デバッガは「変数へのやりとり」ではプログラムを停止できないが、「どの関数を呼び出したか」だと簡単に停止できる。つまり、散文的にバラバラにポートへのアクセスを行なっているプログラムは、それだけでデバッグするに値しないプログラムなんだよね。

●緊急回避?

さて、ここまでのお話はいわゆる通常系…つまり、なんらエラーとか問題が発生しなかった場合の動作のお話だった。ここからは異常系(わかりやすいので、まずはこう呼ぶ。実際は違うんだけど)のお話。

ここでいうエラーとか異常というのは、本来の目的(RCJサッカーならボールをゴールへ入れる)以外の部分で制御せねばならない部分のコト…そう、みんな大好き白線踏みですな(笑)。

前章までの内容でボールを上手に追いかけるコトができるようになったとしても、クラスによっては白線を出てはいけないというルールがあるので、それだけでは済まない。ペナルティ時間のコトを考えるに、勝敗の大半は白線から外に出た時点でかなりアウトになるコトを考えると、如何にココを上手に実装するのかが問題になってくるワケだ。

基本的にラインセンサが反応した場合、なんらかの方法で内側へ戻る必要がある。どうやって戻るのかはさておき、そもそもまず反応したコトをどう検知すべきかがシッカリしないと、結局簡単に外に出てしまうコトになる。

たぶん、これには大きく分けて二通りあるんぢゃないだろうか。

一つは、先のswitch文の中にラインセンサの処理も混ぜ込んで書いてしまう方法。ただ、この方法はそれでなくてもかなりの量を処理しているであろうswitch文をさらに肥大化させるコトになる。そのため、ワザとコレだけif文にしてswitch文の手前で判断し、緊急回避を優先するという手法になるのだろうなと思う。

この場合、そのif文の中で回避処理を行うのではなく、そこでは一旦モードを「緊急回避モード」に変更するだけに留めておき、次のループで緊急回避モードとしてのswitch文が動作するようにしとくのがスマートだろうね。

もう一つは、割り込みを使う方法。センサの反応から割り込みを発生させ、モードをいきなり変更してしまう。あとは同じかな。

どちらにもメリット/デメリットがあるので、どういうプログラムにするのかはその人のセンスによるけど、ここまでの整理がきちんとできているなら、どちらもそう難しくはないだろうなと思う。

で、冒頭の「実際は違うんだけど」の例をあげておこうか。実は同じ考え方がキッカーのトリガーにも適用できるというコトにお気づきだろうか。多くの場合、キッカーはボールが所定の位置に来たコトを何らかの方法でセンスし、キッカーを作動させる。

従って、これもどちらかの方法で実装する形で、同時並行的にキッカーを作動させるコトが可能になるというワケだ。時間軸的に考えて、こっちはさすがに割り込みで実装しないと間に合わない気がするけどね。

また、例えば作戦としてあるモードの時にはキッカーを作動させないなんてコトがしたい場合、割り込み関数の中から現在のモードを確認して作動させるか否かなんてコトを判断できたりもする。

こういったコトも、ここまでの整理がなされているからこそ実装可能な「作戦」なんだよね。

●おわりに

こんな面倒なコト考えるのはイヤだ。いきなり頭からif文の塊で実装していった方が早い…たぶん、プログラムを書き始めた頃はそう思うんだろう。だけど、やっていくと早々に行き詰まる。

プログラミングの環境にも依存するだろう。こういった整理された書き方には馴染まない環境(特にパレットとかタイル方式に多く見られる)も結構存在する。

実際にやってみて行き詰まりを感じたら、このページを思い出してもらえば幸い。たぶん、その時こそが、C言語とかの実践的なプログラミング言語に手を出すタイミングなのだろうから。

とりあえず、今はここまで。

=======(以下、永遠の工事中(笑))=======

 

 

Zak について

基本的にヲタクです。いや、別に萌えとかいうのではなく、ハマるとトコトン進めようとする癖があるので、自制が必要だという…。
カテゴリー: ソフトウエア, なんか作る, マイコン, ロボット, 書き物 パーマリンク