はじめに
この記事はマイクロマウス Advent Calendar 2022の20日目です。
前回の記事はtennisyiさんの東北大会のすゝめでした。
マイクロマウスのようなロボコン競技では大会に参加することが、モチベーション維持の観点から特に重要かと思います。学生のときから東北大会には参加していますが、社会人になってもまた参加したいと思える大会の雰囲気が好きです。
さて、今年のAdventCalendarの内容ですが、昨年のAdvent Calendarで書いた内容の続きを1年越しに書いてみます。
テーマ
マイクロマウスのソフトをどのように書くかについて、マイクロマウスの新作を作るたびに一から書き直すというプロセスを何度か経て現状しっくりきている形を紹介します。一つの記事ですべてを記述すると大変長くなってしまうので、以下のように何回かに分けて記述していきます。
- ソフトの階層構造 <- 2021年のAdvent Calendar
- アプリケーションの基本骨格 <- 今回はこの話
- アプリケーション実行単位(Moduleの実装方法)
- アプリケーション実行単位(Activityの実装方法)
- マイクロマウスのModule、Activity構成の一例
目標
ソフトを書くに当たって以下を目標としています。
- マイコンの変更に強く移植性の高いソフト構造を実現したい
- テストを記述しやすい構造としたい
- 同一コードをマイコンでもPC上でも実行可能としたい
※ CやC++言語で記述することを想定しています。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
アプリケーションの骨格
一つ前の記事ではソフトの階層構造として、以下の3つのレイヤーを提案しPDLとHALの簡単な説明を行いました。そのため、次はALについて説明していきます。
- PDL(Peripheral Driver Layer)
- HAL(Hardware Abstraction Layer)
- AL (Application Layer)
アプリケーションを記述するにあたって、制御や経路計画といったオブジェクトのモデリングをしたくなるところですが、その前の仕込みとして自分で作ったオブジェクトが動く基盤部分を作っていった方が、基礎がしっかりとした建物のように最終的に良いコードになります。
オブジェクトが動く基盤部分と呼んだ部分はゲームのプログラムを作ったことがある人はゲームエンジンという概念を知っていると思われますのですっと入ってくるのではないでしょうか。これから紹介するアプリケーションの骨格の書き方は新・ゲームプログラミングの館 という有名なサイトの影響をかなり受けてます。よければこのサイトの内容も一読してみてください。
ここから具体的な話に移っていきます。マイクロマウスのプログラムを何度か一から書き直すことを繰り返して、重要だと感じたのは以下の3つの粒度感や記述ルールを定めることでした。
- Activity (ゲームなんかではモードやシーンなどと呼ばれることが多いかも)
- Module
- その他一般オブジェクト
では、一つずつ見ていきます。
Activity
マイクロマウスでActivityというと粒度感としては、探索走行、最短走行、各種デバッグコードの呼び出し、といった単位を想像していただければと思います。私のプログラムではこれらの単位のことを「Activity」と名付けています。そして、Activityが満たすべき要件を定め最終的にC++のソースコードに落とし込みました。ただ、本記事では具体的な実装方法までは踏み込まず、その一歩手前までを記述することとします。(具体的な実装方法は次回の記事で解説予定)
Activityの要件
- Activityのライフサイクルを定め、すべてのActivityはそれを準拠するものとする
- BaseActivityクラスを定義し、Activityクラスを新たに定義するときはBaseを継承する
- Activity間の遷移の仕組みを用意する
- Activityの更新はメインループで行う
- Activity間のデータの受け渡しのための仕組みを用意する
Activityのライフサイクル
シンプルに初期化処理、ループ処理、終了処理の三つのサイクルを定義しました。
そして、純粋仮想関数として上記の各サイクルを関数化したものを持つBaseActivityを継承元に新たなActivityを定義することをルールとしました。
Activity間の遷移の仕組み
Activityの遷移については以下のような仕組みにしてみました。
Activity起動時に他のActivityを呼び出すことで遷移を実現し、既に実行しているActivityは一度バックグラウンドに退避し、新しいActivityの処理が終わったらまた元のAcitivityを再開するといった仕組みです。
AndroidアプリをJavaで開発したことがある人はActivityという名前から気づかれるかもしれませんが、そこで用意されている仕組みを参考にしています。
Activity間のデータ受け渡しの仕組み
Activityを起動する際にデータ受け渡し用のデータ構造として「Intent」というものを用意しました。そして、起動したActivityの中でIntentに情報を付与し、Activityが終了する際にActivityの呼び出し元にIntentを返すという仕組みでデータが受け渡しできるようにしました。Intentは任意の型のデータが書き込める箱のようなクラスとして実装しています。
Module
粒度感として周期的に更新処理を行う必要があり、プログラム中で永続的に存在し続けるような単位を「Module」と名付けることにしました。具体例で言うと自己位置推定器、慣性センサ、エンコーダ、機体運動制御器のような単位をModuleとしています。
マイクロマウスの機体制御は1msec程度の周期で行えば問題ないと、多くの競技者は経験的に感じていると思います。この1msecの定期的な処理はマイコンのペリフェラルにてタイマ割り込み関数を利用して実現されることがほとんどではないでしょうか。ならば、処理はタイマ割り込み関数にすべて記述すればよい…というわけにはいきません。例えば処理の中には足立法のポテンシャルマップ更新のように1msecで終わらないものも存在するわけで、このような処理はmainループで実行する必要があります。
そのため、Moduleを導入するにあたって、その更新処理をすっきり記述できるような骨格を作ることを目標としました。
RTOSを導入し、Moduleの更新処理をタスクとして記述しタスクの実行をRTOSに委ねれば上記は達成されるでしょう。しかし、マイコンにRTOSを導入することはそれはそれで骨が折れることです。
Moduleの要件
- Moduleはシングルトンなオブジェクトとする
- BaseModuleクラスを定義し、Moduleクラスを新たに定義するときはBaseを継承する
- Moduleの更新は一定周期で行えるようにする
- Moduleで処理時間が長いものを実行するための仕組みを設ける
- Module間のデータの受け渡しの仕組みを設ける
- Moduleの処理中に他のModuleの操作は極力行わないものとする
Module更新の仕組み
Moduleの更新を記述する箇所を一定周期で実行されるタイマ割り込み関数およびmainループ内に設け、すべてのModuleの処理は一貫して同じような書き方で行うようにしています。工夫としては1msec間隔の割り込み関数を利用するのではなく、250usec周期のタイマ割り込み関数4回分の実行を1セットで1msecを作っているところです。
250usec周期のタイマ割り込み関数を4つ用意する等も考えられますが、多重割り込みを調停する仕組みが必要になるため基本的に割り込みの定義は必要最小限に抑えた方がよいです。
Module間のデータの受け渡しの仕組み
メッセージというデータ構造を定義し、いわゆるpub/subパターンでデータをやり取りできる仕組みを構築しています。ここの部分については解説すると長くなるので別の記事で解説できればと思います。
その他一般オブジェクト
ModuleやActivity内の処理の記述をすっきりさせるために迷路の壁情報やターン軌跡等のオブジェクトは積極的にクラス化するようにしています。これらの一般的なオブジェクトにはActivityやModuleのような要件は特に設けず自由に定義して利用してよいものとしています。
おわりに
マイクロマウスのソフトをどのように書くかというテーマの2回目として、アプリケーションの骨格について述べました。1回目から2回目まで更新に1年かかってしまったので3回目は1年以内に更新することを目標とします。
マイクロマウス Advent Calendar 2022の21日目の記事はコヒロさんの社内アイデアコンテストで優勝した話です。
社会人でマイクロマウスをしつつさらに他のことに手を出すとだいぶ寿命が削れると思います。