2019-05-31

Raspberry Pi3を使ったリアル波形描画(7) デジタル信号処理の基本を理解する

本特集は「Raspberry Pi3を使ったリアル波形描画」がテーマだが,もちろん「描画すること」が目的ではない,何らかのアナログ信号をA/D変換して,その波形を描画したり,二次処理することがしたい。

そのためには,デジタル信号処理の基本を理解しておく必要がある。前回の特集記事『Raspberry Pi3を使ったリアル波形描画(6)Qtでマルチスレッド処理をやってみる で』4msぴったりのサンプリングにこだわっていたのは,例えば,心電図などのデジタル信号処理としてブレのない(ジッターのない)信号サンプリングは必須の条件だからだ。

Raspberry Pi3 ではアナログの信号を取り込む際,多くの場合 A/D変換器は外付けで用意することになる。

実際の工作については『はじめてのAD変換(RaspberryPi3で試すアナログ・デジタル変換)』の記事を参考にしていただきたい。

問題は,A/D変換器に対して,定期的なA/D変換のタイミングを作るのは誰か?ということだ。例えば,部屋の温度が何度なんかを知りたいのならば,A/D変換のタイミングはかなり遅くて適当(例えば5分おきとか)でもいいだろう。もともとの信号の変化がそんなに早くないからだ。

一方で,例えば生体信号である心電図などを計測する際には,早いサンプリングが必要となる。なぜなら,心電図の変化,例えば,心臓がドクンとしたときの急峻な立ち上がりの部分(左図のQRS波形)はおよそ幅が10msくらいなので,それよりも早いサンプリングでないとこのQRS波形をA/D変換器で取り込んで再現することができなくなる。

QRS波形の幅が10msなのに10msでサンプリングしていたら,タイミングによってはQRS波形をすっぽり取りこぼしてしまうかもしれない。

だから心電図をA/D変換するならば最低でも4msのサンプリング間隔(250Hz)は必要と言われる。

そして,そのサンプリング間隔にブレ(ジッター)があると,本当の心電図が正確に再現できないということは容易に想像できるだろう。

したがって,A/D変換器に対して,例えば心電図の取り込みならば,正確に4ms間隔の割り込みを発生させる必要がある。4ms, 3ms, 5ms, 4ms というような感じではだめだ。ブレは 4ms ± 10μs (0.25%)ぐらいには押さえたい。

このタイムインターバルのトリガーを Raspberry Pi3 の外側で発生させるのであれば,水晶発振子で定期的な割り込みを発生させれば,相当正確なサンプリング間隔を作れる。(数十キロHz~)

ただ『Raspberry Pi3を使ったリアル波形描画(6) Qtでマルチスレッド処理をやってみる』の記事に書いたように,Raspberry Pi3自身で(ある程度)正確なサンプリング間隔を作れるのであれば,わざわざ外部でサンプリングの間隔を作る回路は必要なくなるので都合がよい。Raspberry Pi3 で4ms間隔のタイミングを作ることができれば,それをトリガーにして,外部のA/D変換器から GPIOを経由して,SPI通信やI2C通信を使って A/D変換した値を取り込む例はいろいろな資料で公開されている。

外付けのA/D変換器とアナログアンプと電極があれば,心電図を取り込むこと自体はそう難しいことではない。

ただし,デジタル信号処理の基本を理解していないと,正しい計測はできない。

デジタル信号処理の基本

デジタル信号処理の基本を理解するのに『ディジタル信号処理の基礎』がいいと思ったのだが,なんとこの本は絶版になってしまっている。(最近,よいと思う本が絶版になってることがよくあり寂しい)

そこで,この本に書いてあることの一部を紹介していこうと思う。こちらの記事『アナログ信号のサンプリング処理』も参照されたし。

まず,アナログとデジタルの違いについて。

【アナログとデジタル】
  • アナログとは連続的に変化する信号を無段階に表すことを指し、デジタルとはある値を離散的な数値によって表すことを指します。組込み機器では一般に、アナログデータをセンサによって電流や電圧などに変換し、その変化をADC(アナログ-デジタル変換器)などでデジタルデータにして処理を行います。変換されたデジタルデータはデジタルのまま利用されたり、DAC(デジタル-アナログ変換器)などで再度アナログデータに変換したりします。
  • アナログシステムでは使用する部品の性能に誤差があるため、温度などの周辺の環境が変わっても同じような動作をさせるためには誤差を見込んだ設計や、機器生産時の調整や校正を行う必要があります。
  • これに対してデジタルシステムでは、安定した動作、高再現性、変更のしやすさなどのメリットがあり、最近の組み込みシステムではセンサからのアナログ出力をできるだけ早い段階でデジタルに変換して処理や制御に用いるケースが多くなっています。
  • しかしながら、デジタルシステムでは量子化時の誤差やプロセッサの処理速度の限界などがあるため、システムのリアルタイム性や連続性、コストメリットなどを考慮すると要求仕様をアナログシステムで実現した方がよい場合もあります。
デジタル信号処理を行うにあたっては,シャノンのサンプリング定理を理解しておく必要がある。

【シャノンのサンプリング定理】
  • 関数 f(t) がW Hz 以上の周波数を含まないとき、その関数は(1/2W)秒間隔で抽出した標本値により完全に表現できる。

たぶん,これだけ読んでも直感的に理解できないと思うので,『ディジタル信号処理の基礎』の図(p40 図3.8)を使って説明したい。

シャノンのサンプリング定理を簡単に言えば「計測したい信号の周波数の倍以上の周波数でサンプリングしないと元の信号を正しく再現できない」ということになる。

これを理解するために左図の右上の波形を見て欲しい。

右上の実線のsin波に対して,□のポイントでサンプリングしている。(見た目,サンプリングが遅い!)元波形(実線のsin波)を再現しようとすると,点線のsin波もまったく同じ□の点を通っていることが分かる。

これがいわゆる「エリアシング」という現象で,A/D変換する前に元波形からこのような高調波を取り除いてあげなければ,元にはなかったはずの波形が「ゴースト」のように現れてしまうのだ。(音声ではビート音,画像ではモアレ)

まとめると
  • アナログ信号を正確に表現するためには、最低サンプリング周波数は原信号の最高周波数の2倍に等しいか、それ以上でなければならない。
  • サンプリング周波数の1/2の周波数をナイキスト周波数と呼ぶ。
  • ナイキスト周波数を超える周波数成分は標本化した際に折り返し(エリアシングとも言う)という現象を生じ、再生時に元の信号として忠実には再現されない。
ということになる。実際には,上図の左下のように元波形の周波数のちょうど倍くらいのサンプリングにすることはなく,上図の右下のようにターゲットとなる波形の早い周波数成分の数倍の周波数でサンプリングをすることが多い。

なお,それでもシャノンのサンプリング定理は生きているので,必ずサンプリング周波数の1/2以上の周波数(ナイキスト周波数)を超える周波数成分はA/D変換する前に限りなくゼロにしておく必要がある。

これはアナログフィルタで取り除くしかない。このフィルタを「アンチエリアシングフィルタ」という。

デジタル信号処理を行うにあたっては,シャノンのサンプリング定理は必ず知っていないといけないし,A/D変換器の前にアナログのアンチエリアシングフィルタは必要だ。

それを知らないでいると 元波形にはなかった「ゴースト波形」に悩まされることになる。ソフトウェアエンジニアだからといってデジタル信号処理やアナログ回路のことは知らなくてもいい訳ではなく,何をやりたいかによっては,広い範囲の知識や原理原則を知る必要がある。(手段そのものの理解ではなく,目的と目的を実現する原理と,目的を実現するための方法を理解しないと,原理から外れた変更をしてしまい失敗したりする)

シャノンのサンプリング定理やアンチエリアシングフィルタのことは,ハード屋さんが考えることだと決めつけていると,ソフトウェア技術者がよかれと思ってサンプリング周波数を早くしたり,遅くしたりしたときに,アンチエリアシングフィルタとの相性が合わない事態になり,そのことに気が付かず,「ゴースト波形」が出て「なぜ?」と悩むことが起こる。

デジタル信号を扱うのならばデジタル信号の基礎は,ハード屋さんも,ソフト屋さんも知っている必要がある。

なお,本当に人間の心電図を計測するならば,電気安全対策は必ず理解して実施しておかないと危ないのでこちらの記事【トランジスタ技術2006年01月号】 投稿記事『心電計の製作』をよく読んでおくべし。

デジタル信号処理の流れ


ディジタル信号処理の基礎』の図(p70 図3.55) を引用してデジタル信号処理の流れを説明する。

【デジタル信号処理の流れ】
  1. まず,低域通過フィルタ(「アンチエリアシングフィルタ」)にて,サンプリング周波数の1/2以上の周波数をカットする。
  2. A/D変換器で A/D変換する間アナログ信号をサンプル・ホールドする。(最近のA/D変換器はサンプル・ホールドも込みでやってくれる場合が多い)
  3. A/D変換したデータをCPU(上図ではDSP)に取り込む。(Raspberry Pi3 では,GPIOを経由して,SPI通信やI2C通信で取り込むのが簡単)
  4. 取り込んだデジタルデータに対して,デジタルフィルタなどの信号処理を行う。
  5. 加工が済んだら,デジタルデータをD/A変換する。(加工データを外部にアナログ出力する場合)
  6. D/A変換したアナログ信号に対して,低域通過フィルタをかける。
4の,デジタルフィルタや信号処理のところで,何をするのかは,何の信号を取扱い,その信号で何をしたいのかによって,いろいろ変わる。

例えば,心電図を取り扱うのであれば,次のようなことをソフトウェアで実施することになる。(【トランジスタ技術2006年01月号】 投稿記事『心電計の製作』を参照のこと)
  • 目的の周波数成分(エリアシングフィルタよりも低い周波数成分)だけを取り出すために,デジタルフィルタでハイカット(低域通過)フィルタをかける。(ノイズ除去フィルタ)
  • 電源ノイズ(関東:50Hz,関西:60Hz)だけを取り除く,フィルタ(電源ノイズ除去フィルタ)をかける。
  • 基線のゆっくりした動揺を取り除くための ローカット(高域通過)フィルタをかける。
  • 心拍数を計測するためには,心電図波形のQRS波形をカウントする必要がある。QRS波形をカウントするには,QRSを強調するようなデジタルフィルタをかけて,その上でスレッショルドレベルを設定して,スレッショルドを超えたらQRS波形としてカウントする。
なお,上記のハイカットフィルタやローカットフィルタ(ドリフトフリーフィルタ)をかけたことにより,もとの心電図波形が大きくひずんでしまっては,元も子もないので,目的に合った「有限インパルス応答」(FIR)フィルタを適用することが重要となる。

デジタルフィルタとリングバッファ

A/D変換した信号に継続的にデジタルフィルタをかける場合,データを格納する領域は有限なので,データを格納するためにリングバッファを用意する。『ディジタル信号処理の基礎』の図(p103 図4.33) 

図はリング上になっているが,メモリ領域はリング上にはなっていないので,データの格納ポイントをインクリメントしていってバッファの範囲を超えたらポインタを元に戻すという処理が必要になる。

リングバッファ処理やデジタルフィルタはサンプリング毎に実施しなければいけないため,いくら今どきのCPUの処理スピードが早くなったとはいえ,4ms以下で毎回やらなければいけない処理はできるだけ軽くしておいた方がよい。

蛇足だが,整数と浮動小数点の違いを意識せずに,処理時間があまりない中で浮動小数点の演算をバンバン気にせずにやってしまうソフトウェアの新人技術者をよく見かける。CやC++で書いたソースコードが機械語に翻訳されたコードの量を見たことがないのだろう。浮動小数点の演算を専用のプロセッサを使わずに行うとものすごく機械語のコードが増えるので,それを知っていると,できるだけ整数演算で済ませられないかといつも考えるようになる。

さて,リングバッファを用意する際には,バッファサイズは2のn乗であると都合がよい。

リングバッファのポインタは,バッファの上限を超えたら,バッファ数ぶん引かないとポイントが誤った位置を指し示してしまう。バッファサイズは2のn乗であれば,簡単な計算で知りたいポインタの位置を知ることができる。

そのことを説明しよう。

例えばバッファサイズが8で、ポインタが現在 5 で、+4 の位置を知りたいなら
i = (( i + 4 ) & 0x07 ) を計算すればよい。 答え (5 + 4)- 8 = 1

ポインタが現在 5 で、-6 の位置を知りたいなら
i = (( i - 6 ) & 0x07 )を計算すればよい。 答え (5 - 6) + 8 = 7

この計算ならば処理が高速で,意味さえ理解できれいればコーディングミスが少ない。

欲しいビットフィールドを1にしてANDを取る意味を考えてみよう。

( Index + i ) & 0x07 で 5 + 4 → 1 になるしくみ

10進数の5は2進数なら  0101
10進数の4は2進数なら  0100
足すと    9                    1001
10進数の7(8-1)は2進数なら 0111
 (1001 を 0111=7でマスクする。すなわちANDを取る)
0001 = 10進数の1 になる。

( Index + i ) & 0x07 で 5  - 6 → 7 になるしくみ

10進数の5は2進数なら  0101
10進数の6は2進数なら  0110
引くと     -1                    1111
10進数の7(8-1)は2進数なら 0111
 (1111 を 0111=7でマスクする。すなわちANDを取る)
0111 = 10進数の7 になる。

このように,バッファサイズが 2のn乗ならば,現在のポインタに対して 欲しい相対位置を足したり引いたりして,バッファサイズ-1 と AND を取ると 正しい絶対位置を計算できる。

これを,バッファサイズが2のn乗でないときと,2のn乗のときのC言語のコードの違いは次のようになる。

signed short TempIndex1:
unsigned short TempIndex2:

InputBuffer[index] = InputData;

// バッファサイズが2のn乗でない=13などのとき
TempIndex1 = (signed short)(index - 12);

if ( TempIndex1 >= BUFFER_SIZE ) (
     TempIndex1 = TempIndex1 - BUFFER_SIZE;
)
else if ( TempIndex1 < 0 ) (
     TempIndex1 = TempIndex1 + BUFFER_SIZE;
)

// バッファサイズが2のn乗=8のときの例
    TempIndex2 = ( (signed short)Index + 4 ) & 0x07;
    TempIndex2 = ( (signed short)Index - 6 ) & 0x07;

ちょっとした違いなのだが,下の方がずっといいと思うのは自分だけだろうか?
この計算をマクロに登録しておくと,すごくスッキリしたコードになる。

デジタルフィルタの効果を視覚的に確認する

今度は,Interface誌 2003年1月号に投稿した記事『オブジェクト思考を使ったリアルタイム信号計測システムの開発』の記事から引用する。

左図は 1Hz のsin波に10Hzのノイズに見立てた sin波を重畳させた波形だ。

デジタルフィルタを作成して 10Hz のノイズだけを取り除いてみる。

このとき Filter A と Filter B を試してみて,その結果を画面上で確認してみたのが次の図になる。

デジタルフィルタがいいのは,こういった「試しにやってみる」の効果を確認するのが簡単でできることだ。

左図の左側がノイズを重畳させた元波形とカットオフ周波数 8.4Hz のハイカットフィルタを通した結果の波形(赤色)で,右波形が元波形とカットオフ周波数4.3Hzのハイカットフィルタを通した波形(グリーン)だ。

ちなみに,カットオフ周波数(遮断周波数)とは,その周波数を越えると(あるいは下回ると)回路の利得が通常値の 3 dB 低下する値のことである。(Wikipediaの解説

ようするに欲しい周波数帯域におけるsin波の振幅を1としたときに,ハイカットフィルタをかけると,周波数高域になるにしたがって振幅が小さくなってくる。(フィルタの特性によっては単調に減少するとは限らない)
そのときに,振幅1に対して-3dB(およそ 0.73)になる周波数をカットオフ周波数という。

 左図は,青が1Hzに10Hzを重畳させた元波形で,10Hzの sin波が 1Hz で上下している。

紫の波形が Filter A(カットオフ周波数 8.4Hz)をかけた結果で,10Hzのノイズは低減しているものの取り切れていない。

一方,黄色の波形は Filter B(カットオフ周波数 4.3Hz)をかけた結果で,10Hzのノイズは奇麗に取れて 1Hzの sin波だけが残っている。

左図は,5Hzの sin波を Filter A と Filter B に通したときに,どれくらい時間遅れが発生しているかを見たものだ。

元波形に対して 振幅が減衰し,波形のピーク点が僅か左にずれていることが分かる。

位相ひずみがないフィルタ(FIRフィルタ)をかけると,時間遅れが発生する。ドリフトフリーフィルタ(高域通過フィルタ)などでは,この時間遅れが数百msec にもなる場合があるので,どれくらい遅れているのかが分かるようにしておくもとも重要だ。

次回は,Interface誌 2003年1月号に投稿した記事『オブジェクト思考を使ったリアルタイム信号計測システムの開発』をベースに,波形の入力を A/D変換器や サンプルデータやファイルデータに 簡単に切り替えるオブジェクト指向設計を使った事例を紹介する。

デジタル信号処理のポイントは次のようなことになる。

  • 入力したい信号の最高周波数を調べる。
  • シャノンのサンプリング定理を理解し,サンプリング周波数を定め,アンチエリアシングフィルタにて,サンプリング周波数の1/2以上の周波数成分をアナログフィルタで取り除く。
  • 正確なサンプリング間隔を作り,デジタルデータを取り込む。
  • デジタルデータをバッファリングして,デジタルフィルタをかける。
  • その他,二次処理を行う。
そして,デジタル信号処理のシステムの保守性や再利用性を高め,新しい機能を安全に追加できるようにするためには,ソフトウェアのアーキテクチャを工夫する必要がある。

C言語でゴリゴリ書いてしまい「動くソフトウェア」を作ることはできる。しかし,それは,『ソフトウェアアーキテクチャとは何かを考える』の記事で書いたように,丸太組工法で高級マンションを建てようとしているように思える。

どのようにスマートに設計するか,できるのかを次回説明しようと思う。

2019-05-25

Raspberry Pi3を使ったリアル波形描画(6) Qtでマルチスレッド処理をやってみる

今回はリアル波形描画で必須となるマルチスレッド処理を Qt でやってみたいと思う。

ちなみに Raspberry Pi3 には,DebianベースのRaspberry Pi用の小規模な開発者向けのLinuxであるRaspbianを搭載することを想定している。(ラズパイの入門書でもRaspbianをインストール例が多い)

その場合,OS は Linux なのでマルチタスクでスレッドに優先度は付けられるものの,リアルタイムOSではないので インターバルの短いスレッド(RTOSならタスク)を正確に起動するのが難しい。

リアル波形を取り込む場合,正確なインターバルでA/D変換することが非常に重要になる。正確なインターバルでA/D変換ができないと正確な波形の再現ができない。

例えば,心電図をA/D変換するならば,4msインターバル(250Hz)程度のサンプリングはしたいところだ。しかし,いかに1.2GHzのマルチコアのCPUを搭載しているRaspberry Pi3と言えども,Raspbian の標準設定では 4ms以下の定時起動のスレッドを作るのは難しい。

そこで,マルチスレッドを実装する前に,リアルタイムOSとマルチタスクOSの違いについて考えてみたい。

この後説明するのは,もう何十年も前に CQ出版社の Interface に載っていたリアルタイムマルチタスクを理解するための示されていた例だ。参照元を書こうと思ったのだが,あまりにも古くで検索しても見つからない。自分の記憶だけで解説を再現する。

昔の風呂焚きの作業をリアルタイムシステムとして考える

昔の五右衛門風呂の風呂焚きの作業をリアルタイムシステムとして考えてみる。

今どき風呂焚きはボタン一発なので,「なにそれ」と思うかもしれないが,まあ,原始的な風呂焚きはそうなんだと思って欲しい。

原始的な,五右衛門風呂を沸かす手順は次の通りだ。

  1. 風呂釜を洗い栓をして水を注ぐ。
  2. 約15分たったら水を止める。
  3. 水がいっぱいになったら薪を燃やす。
  4. 約40分たったら湯加減をみてちょうど良ければ火を消す。
ポイントは次の通り。
  • 複数の作業を自分一人でこなさなければいけない。
  • 時間を含む限られた資源を有効に使いたい。
  • 水を止めそびれると水が溢れてしまう。
  • 火を消し忘れると火事になるかもしれない。
  • 水を入れているとき,風呂を沸かしている間は他のことをしいてもよい。
Inerface の特集記事では,確か,風呂を沸かしている間に原稿を書いたり,電話に出たりするという設定だったと記憶している。

これらのイベント/タスクを時間経過をX軸に,優先順位をY軸にとって書くと次のようになる。



「水を止める」「火を消す」いったタスクは,すぐにやらないと水が溢れたり,火事になったりするので優先度が高い。

一方で,原稿を書いたり,風呂に入ったりするタスクの優先度は低く,空き時間があるときにやればよい。

タスクの優先度と時間経過の関係を図にしたのが左の図となる。水張り15分後の「水を止める」や風呂焚き40分後の「火を消す」はタイムアウトのイベント発生後,他のタスク(例えば原稿書き)が動いていても,そのタスクを中断して,実施すなければならないタスクである。

このようにイベントが発生して優先度の高いタスクにリソースを割り当てなければいけないのたリアルタイムシステムであり,それを実現するためのOSがリアルタイムOSである。

CPUがシングルコアの時代,発生したイベントによってすぐさま優先度の高いタスクに切り替える必要があるようなイベントドリブンのリアルタイムシステムではリアルタイムOSを使う必要があった。

Raspberry Pi3 はマルチコアのCPUである Cortex-A53 (4コア)を搭載しているので,個々のコアにスレッドを割り当てることができれば,リアルタイムOSでなくてもいいのだが,Qt で用意されているスレッドのライブラリで コアの割り当てる方法は分からなかった。(おそらく,まだないと思う)

リアルタイムシステムのマルチタスクと非リアルタイムのマルチタスクの違いを左図に示す。

リアルタイムシステムの場合,イベントが発生するとただちに優先度の高いタスクに切り替わるが,非リアルタイムシステムの場合,一定時間ごとにタスク(スレッド)が切り替わるので,イベントが発生した後,待ち時間が発生する可能性がある。

最近の組込み系のCPUも待ち時間が気にならないほど,切り替えは早くなっているが,msオーダーのイベントでは追随できないこともある。

各タスクに処理の順番が回ってくる周期が遅いと,風呂焚きの例でいえば,水が溢れたり,湯が熱くなりすぎたりするということになる。

非力なCPUの時代の Windows では,ウインドウをたくさん開くとマウスのレスポンスが悪くなるような現象を経験したことはないだろうか。これは,ウインドウを開けば開くほど,一つのウインドウに処理が回ってくるインターバルが長くなるからだ。

非リアルタイムシステムで4msインターバルのマルチスレッドを作成してみる

では,本題に戻るとして,メインのスレッドとは別に4msインターバルで動くスレッドを Qt で実現してみる。

Qtでマルチスレッドを実現するには QtConcurrent や QTread を使う。

実際 Qt で実現するのに次の記事を参考にした。

QtConcurrent に関する記事 『QtConcurrentでマルチスレッドに挑戦
QTread に関する記事 『Qtでスレッドを使う前に知っておこう

QTread から定期的に必要な処理を起動するしくみは『スレッドを使った並列処理』を参照している。

クラス図でそれぞれの関係を示すと上図のようになる。メインウインドウのクラス MainWindow は QThead のクラス MyTread を持ち,MyTread は Qtimer のクラスtimer を持っている。

信号処理において正確なサンプリングができることは極めて重要で,サンプリング間隔が揺れてしまうと,波形がひずんでしまう。例えばもとの波形が sin波であっても,再現した 波形が奇麗な sin波でなくなってしまう。

よって,システムタイマーによる割り込みをスレッド処理で実現したら,正確にインターバルが取れているかどうか,常に確認しておきたい。この例では,4msのタイムインターバルがどれくらい正確に実現できているかどうかをインターバル間隔を表示することで確認している。
  • SIGNAL/SLOT の設定
  • periodic_work の処理を設定
  • ボタンの押下でスレッドの開始/終了を行う
  • スレッドが始まるときに呼ぶ run() メソッドを作成する。
  • run() でタイマーを生成し,タイムアウトしたときに Qtの SIGNAL/SLOT で timerHit()が呼ばれるようにする。
  • run() でタイマーを指定時間に設定する。(ms単位)
  • exec()を実行してスレッド動作を開始する。
  • なお,スレッドが終了すると exec()以下が実行される。
  • スレッド終了後に timer を停止する。
  • タイマーがタイムアウトしたら timerHit() が呼ばれる。
  • timerHit()が呼ばれたら Qt のシグナル data_update を発生させて,SLOT で受ける。
4msインターバルのスレッドを起動するときのボタンをウインドウがこれ。


MainWindows と MyThead の関係を示した図が下になる。

main.cpp のソース

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
  QApplication app(argc, argv);
  MainWindow testwindow;

  testwindow.show();

  return app.exec();
}


mainwindow.cpp のソース

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>

MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //----------------------------------------------------------
  // Sendar :my_thread Signal :data_upda
  // Receiver :MainWindows Slot :periodic_work
  //----------------------------------------------------------
  QObject::connect(&my_thread, SIGNAL(data_update(int)), this, SLOT(periodic_work(int)) );

}

MainWindow::~MainWindow()
{
  my_thread.quit();
  my_thread.wait();
  delete ui;
}

void MainWindow::periodic_work( int data){
  char str[256];
  static int i=0;
// sprintf(str,"i=%d",i++);

  i++;

  // デバッグ用の表示文字列を作成
  sprintf(str,"No= %6d Interval: %d [uSec]",i, data);

  // ウインドウにカウンタを表示
  ui->textBrowser->append( QString::fromLocal8Bit(str) ); // カウンタを追記表示

}


void MainWindow::on_pushButton_clicked()
{
  static int flag_run=0;

  // push ボタンがクリックされて,スタートならスレッドをスタートする。
  if(flag_run==0){
    flag_run = 1;
    ui->pushButton->setText(QString::fromLocal8Bit("Stop thread"));

  // QTread の最優先度を設定する
  my_thread.setPriority(QThread::HighestPriority);
  my_thread.start();

  // push ボタンがクリックされて,ストップなら,スレッドを抜ける
  }else{
    flag_run = 0;
    ui->pushButton->setText(QString::fromLocal8Bit("Run thread"));
    my_thread.quit();

  }
}

mytread.cpp のソース


#include "mythread.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>


MyThread::MyThread(QObject *parent) :
  QThread(parent)
{
}

void MyThread::run(){
  QTimer timer; // Qtimer オブジェクト timer を生成

  //----------------------------------------------------------
  // Sendar :timer Signal :timerのタイムアウト
  // Receiver :このスレッド Slot :timeHit関数
  //----------------------------------------------------------
  // timerをスタートさせ,指定時間のタイムアウトが発生したら,Qt の SIGNAL/SLOT を使ってtimerHit関数を呼ぶ
  //----------------------------------------------------------
  connect(&timer, SIGNAL(timeout()), this, SLOT(timerHit()), Qt::DirectConnection );

  timer.setInterval(4); // Qtimerオブジェクトの timerの 時間間隔を 4ms に設定する
  timer.start(); // Qtimerオブジェクト timer をスタートする。

  printf("debug : Thread Starts\n"); fflush(stdout);
  exec(); //Start event loop
  printf("debug : Thread Stops\n"); fflush(stdout);

  timer.stop();
}

void MyThread::timerHit(){

  // システムのタイマーの構造体の変数を作成する
  struct timespec ts; // 現在のシステム時間を格納する構造体
  static struct timespec ts_before; // 前回のシステム時間を格納する構造体
  long diff = 0; // 前回と今回の差分を格納する変数

  // システムクロックをgetする
  clock_gettime(CLOCK_REALTIME, &ts);
  // 前回のシステム時間と今回のシステム時間の差分ナノ秒単位で計算し,ミリ秒単位に変換する
  diff = (long(ts.tv_nsec) - long(ts_before.tv_nsec))/1000;

  // 次回のために今回取得したシステムクロックを格納する
  ts_before = ts;

  // システムの実時間を秒とナノ秒に分けて表示し,前回と今回の差をミリ秒で表示する。
  printf("time: %10ld.%09ld CLOCK_REALTIME %05ld DIFF:uSec\n", ts.tv_sec, ts.tv_nsec, diff);

  // emit で前回との時間差(ms) を Qt の SIGNAL/SLOT で受け渡す
  emit data_update( int(diff) ); //emit signal
}


mainwindow.h のソース

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "mythread.h"
#include <stdio.h>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
  Q_OBJECT

public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

public slots:
  void periodic_work( int i);

private slots:
  void on_pushButton_clicked();

private:
  Ui::MainWindow *ui;
  MyThread my_thread;
};

#endif // MAINWINDOW_H

mytread.h のソース


#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>
#include <QTimer>

class MyThread : public QThread
{
  Q_OBJECT
public:
  explicit MyThread(QObject *parent = nullptr);

  void run();

signals:
  void data_update(int i);

public slots:

private slots:
  void timerHit();
};

#endif // MYTHREAD_H



スレッドをWindows上で実施した例。インターバル間隔が,ぴったり4msではなくわずかにずれている(数μs程度)ことが分かる。

まあ,この程度なら実験としては問題ない範囲だろう。

2019-05-20

ソフトウェアアーキテクチャとは何かを考える

最近,協力会社に開発を任せていてソフトウェアの中身が分からず,意図した動きにならない(レスポンスが遅いとか,画面が思ったように遷移しないとか)という話を聞いた。

また,「ソフトウェアを継ぎ足し続けた結果,肥大化・複雑化して,原因を追及しにくい不具合が発生して困っている。リファクタリングしたい」という話も聞いた。

どちらも,ソフトウェアアーキテクチャ(構造設計)ができていない,把握できていないのではないかと思う。また,現在のアーキテクチャがシステムの用途・目的に合っていないのではないかと思う。

ソフトウェア設計のプロセス的に言えば,要求分析や実現可能性の実験(フィージビリティ・スタディ)や機能要件・非機能要件を考慮したアーキテクチャ設計ができていないとかなんだろうが,一言で言えば「アーキテクト不在」という問題が一番大きい。

アーキテクト(=建築士)不在ということが,どういうことなのかを,住宅の設計の例で考えてみたいと思う。

建設転職ナビより転載

一級建築士について
一級建築士とは、国土交通大臣から認可を受けた国家資格です。家屋、学校や体育館、商業施設や病院など、ありとあらゆる建造物の設計をします。街や村など人々の生活を豊かにしていく、私達には欠かせない大切な仕事を担っています。 安全性を考慮して建物の設計図を書き、それを元に工事を進めていきます。

試験の受験資格の面でも、一級建築士の場合は実務経験がないと受験資格を得られないという決まりがあります。二級までは、建築学部などで履修科目を学んでいれば実務経験がなくても受験資格を得ることができます。十分な経験を積み、高い知識を持ちあわせている人だからこそ、一級建築士になれるのです。

一級建築士にできること
一級建築士は、一級というだけあり、建築できる建造物に制限がありません。
二級建築士までだと、建物の高さや面積により建築できる建造物に制限があります。 二級建築士は、主に戸建住宅などの小規模な建造物のみを設計・建築できます。 また、木造建築士においては、木造の建物かつ2階建てまでの建物しか取り扱うことができませんが、一級建築士の場合、高さや構造に制限がないため、学校や病院、商業施設など大規模な設計・建築に携わることができます。

【転載終わり】

ようするに,建築士の資格を持ったものが,さまざまが建物の設計図を書き,それに基づいて工事が進められる。自分は,「大改造!!劇的ビフォーアフター」というリフォーム番組が好きでよく見ている。この番組では,いろいろな悩みを持った依頼主が,さまざまな得意スキルを持った一級建築士に家のリフォームを一任し,依頼主の悩みを解決して家を新しくする様を見る番組だ。

建て直すわけではなく骨格は残すので,大元の家の骨組みは残すが,部屋割りや構造の補強など,建築士のアイデアで大改造を行う。

ソフトウェアで言えば,ソフトウェアアーキテクト(一級建築士)がユーザの悩み事や要望を聞いて,システムのアーキテクチャをリファクタリングすることになる。

リアル建築との違いがあるとすれば,「大改造!!劇的ビフォーアフター」の場合は,リフォームした家(リファクタリングが完了したシステム)を眺めて,その出来映えや使いやすくなったところをユーザと共有し共感することができることだ。一方,悲しいかなソフトウェアの方はリファクタリングが完了しても,ユーザとその出来映えを共有・共感することは難しい。

「すばらしいアーキテクチャに変わりましたね」という感想がユーザだけでなく,プロジェクトメンバからも口々に語られることは,おそらくない。リファクタリング後に,ソフトウェアの修正が楽になったとか,デグレードが発生しなくなったとか,派生製品を作るときの開発期間が短くなったという感想は聞けるかもしれないが,しばらく時間がたたないとそういった感想にはならない。

例えば,アーキテクチャを意識せず,または,設計当初はアーキテクチャの設計思想があったのに,その後の度重なる修正によりアーキテクチャが崩れてしまった場合,各ソフトウェアモジュール間の結合が強くなり,また凝集性が弱くなることで,テストケースが爆発的に増え,デグレードや欠陥の漏れが多くなる。

ソフトウェアモジュール間の結合度が小さく,凝集が強いということは,責務が明確で他のモジュールとの関係性が小さいということだから,修正が他のモジュールに与える影響は小さくなり,また,どこに影響が及ぶかが分かり易い。(凝集が強く,責務が明確なので,その機能に責任を持っているモジュールがどこなのかがすぐに分かる)

ソフトウェアのアーキテクチャが度重なる修正により崩れていくと,納期を急ぐあまり,自分の責務ではない機能をソフトウェアモジュールに入れ込んでしまったりして,テストが終わらなくなる。納期を急いでいるため時間切れとなり,テストされていない箇所が残り後に不具合となって現れる。

ソフトウェアは見えないだけに,悪い状態もよくなったことも,分かりにくい。だから,まずは「アーキテクチャとは何か」についてイメージだけでも押さえてもらいたいと思う。




まず,実際の住宅の構造(アーキテクチャ)にいろいろな種類があることを理解しよう。住宅の構造だけでも,木造在来工法や2×4工法,軽量鉄骨を使った鉄骨軸組工法,ユニット工法などさまざまな工法がある。

それぞれの工法には特徴があって,コストや施工の早さや,間取りの柔軟性等々,ユーザの要求や懐事情によって基本構造(アーキテクチャ)も変わる。

リアル住宅の場合も完成してしまうと,構造(アーキテクチャ)がどうなっているかは外からは分かりにくい。(丸太組工法などは例外)

例えば3階建てにしたいとか,地下室が欲しいとか,間取りにこだわりがあるとか,地震に強くしたいとかいったユーザの要望によって,建築士は構造(アーキテクチャ)を選択する。

別荘に丸太組工法のログハウスを提案することはあっても,普段生活をするための住宅に丸太組工法を提案することはあまりないだろう。

問題は,ソフトウェアの場合は,丸太組工法しか知らないソフトウェアエンジニアが丸太組工法で一般住宅や雑居ビルを作ってしまうケースがあるということである。


また,よくありがちなのは,狭い敷地に雑居ビルがひしめき合っているようなソフトウェアの作りにしてしまうケースや,老舗の旅館に別館や新館を建てまして迷路のようになってしまうケースだ。

収容できるお客は増えるのだが,泊まり客へのサービスはしにくいし,メインテナンスもたいへんだ。
雑居ビルもごちゃごちゃしていて,ビルメインテナンスがしにくい。

そういうソフトウェアになってしまうのは,設計を開始する際に建築士が要求に対して適切な設計図(アーキテクチャ)を作成していないからではないだろうか。

ただ,そういった場合であっても「大改造!!劇的ビフォーアフター」のように,リファクタリングをすることは不可能ではない。

ユーザの困りごとやシステムに対する要求,シリーズ製品のラインナップ等々を調査した上で,何をどう変更するのかを考える。

その際に,何に困っているのか,どんなシステム要求があるのか,ソフトウェアが動くプラットフォームの性能などの情報がないと,適切な工法を選ぶことができない。

【この後『C/C++による組み込みソフトウェア開発技法 オブジェクト指向を取り入れた理論と実践』から適宜参照。ソフトウェアアーキテクチャを基本から学びたい方は是非読んで欲しい。】

ソフトウェアのアーキテクチャを考える上で,構造化設計の基本を押さえておく必要がある。言わば,設計士の資格を得るための基礎知識といったところ。

構造化設計では、モジュールごとに固有の機能が割り当てられているソフトウェア構造であるほど、開発や修正が容易となる。

ソフトウェア構造が適切に分割されているか判定するために、エドワード・ヨードンとラリー・コンスタンチンが,著書“Structured Design”において、モジュールの結合度(Coupling)と呼ばれる基準を考案した。

左図にモジュールの結合度と特徴を示す。ソフトウェア内のモジュール間の結合度が低いほど、独立した機能と処理を担当するソフトウェア構造を持っていることを示す。

多くのモジュールが結合度の小さいデータ結合になっていればよいが,実際にはそうはなっていない。

モジュールの結合度と,後述するモジュールの凝集度は意識してソフトウェアを設計しなければ,なかなかそうはならない。ようするに,ソフトウェアアーキテクチャを「高凝集」「疎結合」なモジュールで構築しようという設計思想がなければ自然にはそうならない。まれに,直感で修正しやすいソフトウェアを作ろうとして「高凝集」「疎結合」なモジュールを作れるソフトウェアエンジニアもいるかもしれないが少数だろう。

だから,多くのソフトウェアエンジニアは,ソフトウェアモジュール同士の関係が「高凝集」「疎結合」とはどんなことかを頭に入れておいて,できるだけモジュール間の結合を疎に,役割や責務を集約するようにしようと常に考えておく必要がある。

下記に上記のモジュール結合度のタイプ6種類を示す。







理想的には①データ結合,②スタンプ結合,③制御結合ですべてのソフトウェアモジュールが構成されているのがよいが,特にC言語で作っていると④外部結合や⑤共有結合や⑥内容結合になってしまう傾向がある。

C++のようなオブジェクト指向言語を使えば,言語仕様からして④外部結合,⑤共有結合,⑥内容結合は作りにくくなる。

次にモジュールの凝集度について説明する。

ソフトウェアの凝集度(Cohesion)は、エドワード・ヨードンとラリー・コンスタンチン、ウェイン・スティーブンス、グレンフォード・メイヤーズらによって考案されたソフトウェア機能の固有性の尺度で,凝集度が高いモジュールは、明確な責任を持ち、シンプルで独立性の高い機能を実装していることを示す。

この指標はオブジェクト指向設計の目標と一致する。
一般的に凝集度が高いソフトウェアほど保守性や拡張性が高く、再利用も容易な傾向がある。

ソフトウェアの新人技術者の教育をやっていると,機能の実現を関数を積み重ねることで作り上げていき,システムの規模が大きくなっても,そのやり方を続ける者が多い。

システムの規模が大きくなったときは,機能の実現というよりも,責務の分割という視点で設計した方が圧倒的に保守性,拡張性,再利用性が高いという実感がある。

ソフトウェアエンジニアはどこかの時点でその考え方のシフトが必要なんだと思う。そのことを『リアルタイムOSから出発して組込みソフトエンジニアを極める』に書いたつもり。

さて,ソフトウェアの凝集度(Cohesion)について見てみよう。



①暗号的凝集/偶発的凝集(Coincidental Cohesion)
  • 互いに関連のない複数の機能を含むモジュールの凝集度を指す。
  • たまたまタイマイベントによってメッセージ表示とファイル書き込みの処理が開始するため1つの関数内に両方を実装した場合などに、このような構造が現れる。
  • 機能同士はタイミングも、処理内容も、種別もまったく関連がない。そのため修正や機能追加の影響範囲が広いうえに可読性が低く、保守や修正が困難なソフトウェア構造。
②論理的凝集(Logical Cohesion)
  • 実際の内容は異なるが、同じカテゴリに属する処理を集めたモジュールの凝集度。
  • たとえばLCD表示を処理する関数に、アイコンの制御とメッセージ表示と背景画像の描画処理を実装している場合などが該当する。
  • ひとたび論理的凝集状態にあるモジュールを実装すると、関連する機能を追加する場合に分離が難しくなる。
  • LCD制御の処理やデータがすでに集まっている関数がある場合に新たに時計表示を追加しようとすると、既存処理を利用するには同じ関数内に実装せざるをえないことなる。
  • このため機能は膨れあがり、次第に修正と保守が困難になる。

③時間的凝集(Temporal Cohesion)
  • 同じタイミングで実行される処理を集めたモジュールの凝集度。
  • カーナビゲーションシステムで目的地に到着したタイミングでメッセージ表示と音声再生と画像更新を実行するために、一つの関数にこれらの処理を実装した場合などに発生する。
  • 機能的関連がないため共通化や修正が難しい。

④手続き的凝集(Procedural Cohesion)
  • 一連の処理を実行するために必要な機能を集めたモジュールの凝集度。
  • 内蔵チップを初期化する機能として、初期化開始処理・初期化完了待ち処理・初期値のデータ設定処理などを1つのモジュールに実装した場合などに発生する。
  • ひとまとまりの処理として実行されるだけで、機能同士の関連が弱いため、ある箇所を修正すると次の処理にも影響が及ぶ結果となり保守性が低下する。

⑤通信的凝集(Communicational Cohesion)
  • 同じデータを扱う処理を集めた凝集度。
  • 処理に順序性がなくデータの共通性によって結びついているため、データ構造の変更が複数の箇所に影響する可能性がある。
  • クラスによっては最適な設計の結果が通信的凝集の状態になる場合もある。

⑥逐次的凝集(Sequential Cohesion)
  • 同じデータを扱う処理を、順序性を持たせて集約したモジュールの凝集度。
  • ある処理の出力が次の処理の入力となり、処理を分離してもソフトウェアの構造が単純化されない(またはかえって複雑となる)ような機能を持つ状態を指す。
⑦機能的凝集(Functional Cohesion)
  • 1つの関数やモジュールが単一の役割と目的を持っており、固有の機能を実装する状態の凝集度。
  • クラス設計においても独立したシンプルなクラスとすることができます。
  • すべてのモジュールを機能的凝集度で実装することはできないが、可能な限りこのレベルに近づけるように検討すべき。
次に,すでに機能の積み重ねで作ってしまい大規模・複雑化してしまったシステム,バグが多く混在する未熟な組込みシステムから,システムの中で価値の高いコア資産を分離する(高凝集,疎結合にする)イメージを示す。

2005年のESEC専門セミナーで講演した内容からの抜粋。







あくまでもイメージであり,実際のソフトウェアのリファクタリングはそんなに簡単ではないが,まずはどうなっていればいいかというイメージを持つことが重要だ。

ソフトウェアシステムのソフトウェア開発に携わるソフトウェアエンジニアは,まず,ソフトウェアのモジュールを作成する際には「高凝集」「疎結合」とすべきであることを常に認識しておく。作成したモジュールが結合の分類,凝集の分類のどれに当たるのかを把握しておくとよいだろう。

そしてソフトウェアシステムのアーキテクチャを考えるアーキテクトは,システムに対する要求や条件,実現の可能性等を考慮して,どのような構造(アーキテクチャ)にするのが最適であるかを考える。その際には,今回対象の製品だけでなく,製品群で共通に使用するコアとなる資産が何になるのかを意識した方がよい。コア資産となるソフトウェアモジュールほど,高凝集,疎結合であった方が再利用性が高くなり,保守もしやすい。

リアル建築士のように,ソフトウェアのアーキテクトも必要な知識やスキルがあるかどうかを検定して一級,二級といったソフトウェアアーキテクトの資格を作った方がいいのかもしれない。

アーキテクト不在のソフトウェアシステムは恐ろしいので,ソフトウェア開発には少なくとも一人はアーキテクトを含めるようにした方がよいだろう。

「ソフトウェアアーキテクトなんかいない」という組織やプロジェクトではどうすればいいのか。それは,外部の協力会社のアーキテクトをプロジェクトに引き入れたり,メンターとなってくれるコンサルタントに支援を要請する。

ちなみに,ソフトウェアアーキテクトの素養のある人は,ソフトウェアエンジニア20人に1人くらいの割合だろうと言われている。みんながみんな一級建築士になれるわけではないのは,リアル建築と同じだ。

ちなみに,実感として,ソフトウェアアーキテクトの素養のある人が,その能力を適正に評価されているようには思えない。ソフトウェアの構造(アーキテクチャ)が良い,美しいことは,外側からはよく分からないからだろう。

「大改造!!劇的ビフォーアフター」のように依頼者が涙を流して,アーキテクトに感謝してくれるといったシーンが,ソフトウェアの世界では期待できない。

そこがソフトウェアアーキテクトの悲しいところだ。

ソフトウェアもリファクタリングは可能と書いたが,現実にはすでに出来上がってしまったソフトウェアシステムを静的な構造として解析すると(どのクラス,どのモジュールがお互いを呼び出している関係)左図の左のようになっていたりして,リファクタリングはそう簡単ではない。

全体的なリファクタリングは諦めて,後継機種を開発するときに着手しようという結果になることも多い。

本来ならば,上から下へといった呼び出し関係(上図の右)となるような,レイヤードアーキテクチャになっていた方が,保守性が高まるが,そういうアーキテクチャにしようという意図なく開発していたら,そうそう,作り直すのは簡単ではない。

最初の問題提起に戻ると,ソフトウェア開発する際に「アーキテクト不在」では何事もうまくいかず,失敗プロジェクトになってしまう可能性が高まる。

2019-05-13

Raspberry Pi3を使ったリアル波形描画(5) スタートレックゲームの一部を作ってみる

今回はテキストで遊ぶスタートレックゲームのほんの一部分を c++ で書いてみる。
オリジナルのスタートレックゲームはテキストのゲームでありながら,自分がスタートレック号にのって宇宙を移動しながらクリンゴンを倒していく雰囲気を十分に味わうことができた。

iPhone を持っている人は App Store に Old Trek という名前で登録がある。(120円)
詳しくはこちらを参照のこと。

スタートレックゲームについてより詳しく知りたい方はこちらを参照のこと。

スタートレックシミュレーションゲーム(Wikipedia)

<スタートレックゲームのオリジナルの動画>
  1. スタートレックゲームのソースコードと解説
  2. MZ-80 でスタートレックゲームを遊んでいる動画
  3. Apple II でスタートレックゲームを遊んでいる動画
スタートレックゲームのイメージは概ねこんな感じだ。左下の8×8のマスがショートレンジセンサーの結果で,上の8×8のマスがロングレンジセンサーのマップになる。

ショートレンジセンサーの8×8のエリアが64個ぶん広がっている宇宙があるという設定だ。ロングレンジセンサー[517]の数字の意味は,5つの敵=K,1つの基地=B,7つの星=※があるという意味。ショートレンジセンサーのマップのEがエンタープライズ号を示している。Bの基地まで移動するとエネルギーを補給できる。

クリンゴンは光子魚雷(Photon Torpedos)か,フェーザー砲(Fire Phaser)で攻撃する。フェーザー砲はエネルギー値を設定でき範囲攻撃ができる。光子魚雷は当たればクリンゴンを破壊できる。クリンゴンから攻撃を受けることもある。移動は短距離はインパルスエンジン,長距離はワープエンジンを使用する。

今回は,8×8のショートレンジセンサーのレンジを用意し,ランダムにクリンゴンと星とエンタープライズ号を配置し,エンタープライズ号をインパルスエンジンで移動するプログラムを作ってみる。

移動だけで,攻撃とかワープとか補給などはできない。前回のタートルクラスを参考にして(派生させて),エンタープライズ号とクリンゴンと星のオブジェクトを生成して,それぞれのオブジェクトを動かせるようにしてみる。

方向は0~360° で方向を指定する。これは亀(タートルクラス)のときと同じ。

            0°
           ↑
     270° ←  亀   →  90°
                   ↓
           180°

オブジェクト思考設計をする意味は,前回も述べたようにソフトウェアを再利用可能な資産にして,毎回ゼロから作り直すようなことをしない点にある。

今回のような仕様のソフトウェアはいろいろな方法で作成することができるが,エンタープライズ号とクリンゴンと星はどれも同じような特長(移動方法など)があるので,これらをオブジェクト指向設計でうまく設計すれば効率的にソフトウェアを作ることができる。

事前準備

  • タートルクラス(x,yの位置情報を持ち,向きの変更と前進ができる)を応用して,宇宙船クラスを作成。
  • 小宇宙クラスを作成し 8x8 のマップを作成。
  • クリンゴン(2個以上5個以下)と,惑星(4個以上7個以下)をランダムにマップに配置。
  • エンタープライズ号を配置。
  • エンタープライズ号を移動できるようにして,マップを表示させる。
  • これらをテキスト表示で行う。


Spaceship クラスは,方向とX座標とY座標を内部変数として持っている。そして,前進,右向き,左向き,任意の位置に移動などのサービスを提供する。

このクラスから実態となる宇宙船,クリンゴン,惑星を生成しマップに配置する。

【ap_smallspace.h のソース】

#ifndef AP_SMALLSPACE_H
#define AP_SMALLSPACE_H

#include "ap_spaceship.h"


class smallspace
{
public:
    smallspace();
    void deployPlanets(int num);
    void deployKlingons(int num);
    void deployEnterprise(void);
    unsigned short mSpaceMap[8][8];

    Spaceship Enterprise;
    Spaceship Klingons[8];
    Spaceship Planets[8];
    int KlingonsNum;
    int PlanetsNum;

    void displayMap(void);
private:

};


#endif // AP_SMALLSPACE_H

【ap_spaceship.h のソース】

#ifndef AP_SPACESHIP_H
#define AP_SPACESHIP_H

class Spaceship
{
public:
    Spaceship();
    void turnleft();                // 左を向く
    void turnright();               // 右を向く
    void forward(int distanse);     // 前進する
    int getXPosition(void);        // 現在のX軸の位置を得る
    int getYPosition(void);        // 現在のY軸の位置を得る
    int getDiraction(void);        // 現在の向きを得る
    void setDiraction(int);        // 方向を設定する
    void resetPosition();
    void warp(int x, int y);

private:
    int mX;          // X座標
    int mY;          // y座標
    int mD;          // 向き
};


#endif // AP_SPACESHIP_H


【ap_stertrekgame.h のソース】

#ifndef AP_STARTREKGAME_H
#define AP_STARTREKGAME_H

#include "ap_smallspace.h"

class startrekgame
{
public:
    startrekgame();
    ~startrekgame();

    // 惑星・エンタープライズ号・クリンゴンを配置
    void initStarTrekGame();
    // 小宇宙のマップを表示
    void displaySpaceMap();
    // コマンド受け付け
    int inputCommand();
    // コマンド実行
    void execCommand();

private:
    smallspace* mSmallspace;        // 小宇宙ポインタ

    int mCommand = 0;               // コマンド
    int mFunction = 0;              // ファンクション

    void inputMoveFunction();
};


#endif // AP_STARTREKGAME_H

【ap_smallspace.cpp のソース】

#include "ap_smallspace.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>


smallspace::smallspace()
{
    for (int i=0; i<8 ;i++){
        for (int j=0; j<8; j++){
            mSpaceMap[i][j]=0;
        }
    }
}

void smallspace::deployPlanets(int num)
{
    int i = 0;
    int posX, posY;
    // 現在時刻を元に乱数の種を srand 関数で生成する。unsgined int に明示的にキャストしておく
    srand(static_cast<unsigned int>(time(nullptr)));
    // 3から7の間の乱数を作成する
    if (num==0) num = rand() % 4 + 3;
    PlanetsNum=num;

    while (i<(num)) {
        posX =rand() % 8;
        posY =rand() % 8;

        if (mSpaceMap[posX][posY]==0){
            mSpaceMap[posX][posY]=100;
            Planets[i].warp(posX,posY);
            i++;
        }
    }
}

void smallspace::deployKlingons(int num)
{
    int i = 0;
    int posX, posY;
    srand(static_cast<unsigned int>(time(nullptr)));

    if (num==0) num = rand() % 3 + 2;
    KlingonsNum=num;

    while (i<(num)) {
        posX =rand() % 8;
        posY =rand() % 8;

        if (mSpaceMap[posX][posY]==0){
            mSpaceMap[posX][posY]=10;
            Klingons[i].warp(posX,posY);
            i++;
        }
    }
}

void smallspace::deployEnterprise()
{
    int i=0;
    int posX, posY;
    srand(static_cast<unsigned int>(time(nullptr)));

    while (i<1) {
        posX =rand() % 8;
        posY =rand() % 8;

        if (mSpaceMap[posX][posY]==0){
            mSpaceMap[posX][posY]=1;
            Enterprise.warp(posX, posY);
            i++;
        }
    }
}

void smallspace::displayMap()
{
    int x, y;
    printf ("\n");
    printf ("    1  2  3  4  5  6  7  8 \n");
    for (y=0; y<8; y++){
        printf( "%2d ", y+1);
        for (x=0; x<8; x++){
            if (mSpaceMap[x][y]==0) printf(" . ");
            if (mSpaceMap[x][y]==100) printf(" * ");
            if (mSpaceMap[x][y]==10 ) printf(" K ");
            if (mSpaceMap[x][y]==1 )  printf(" E ");
        };
        printf ("\n");
    };
    printf ("\n");
}


【ap_spaceship.cpp のソース】
#include "ap_spaceship.h"

Spaceship::Spaceship()
{
    resetPosition();
}

void Spaceship::turnleft()
{
    mD=mD-90;
    if (mD < 0) mD = mD+360;
}

void Spaceship::turnright()
{
    mD=mD+90;
    if (mD>360) mD=mD-360;
}

void Spaceship::forward(int distanse)
{
    if (mD==0) mY=mY-distanse;
    if (mD==180) mY=mY+distanse;
    if (mD==90) mX=mX+distanse;
    if (mD==270) mX=mX-distanse;
}

int Spaceship::getXPosition()
{
    return mX;
}

int Spaceship::getYPosition()
{
    return mY;
}

int Spaceship::getDiraction()
{
    return mD;
}

void Spaceship::setDiraction(int d)
{
    switch (d) {
    case 0:
        mD=0;
        break;
    case 90:
        mD=90;
        break;
    case 180:
        mD=180;
        break;
    case 270:
        mD=270;
        break;
    default:
        mD=0;
        break;
    }
}

void Spaceship::resetPosition()
{
    mX=0;
    mY=0;
    mD=0;
}

void Spaceship::warp(int x, int y)
{
    resetPosition();
    turnright();
    forward(x);
    turnright();
    forward(y);
}

【ap_stertrekgame.cpp のソース】

#include "ap_startrekgame.h"
#include "ap_smallspace.h"

#include <stdio.h>

startrekgame::startrekgame()
{
    mSmallspace = new smallspace;
    initStarTrekGame();
}


startrekgame::~startrekgame()
{
    delete mSmallspace;
}

void startrekgame::initStarTrekGame()
{

    mSmallspace->deployKlingons(0);
    mSmallspace->deployPlanets(0);
    mSmallspace->deployEnterprise();

//    printf ("x1:%3d , y1:%3d\n", mSmallspace->Enterprise.getXPosition()+1, mSmallspace->Enterprise.getYPosition()+1);

}

void startrekgame::displaySpaceMap()
{
    mSmallspace->displayMap();
}

int startrekgame::inputCommand()
{
    mCommand = 0;
    // コマンドを入力
    printf ("COMMAND (-1:quit 0:map 1:move) ? ");
    scanf ("%d", &mCommand);

    return mCommand;
}

void startrekgame::execCommand()
{
    switch (mCommand) {
    case 0:
        mSmallspace->displayMap();
        break;
    case 1:
        inputMoveFunction();
        break;

    default:
        break;
    }
}

void startrekgame::inputMoveFunction()
{
    int distance=0;
    int direction=0;
    int x1, y1, x2, y2;
    Spaceship tempSpaceship = mSmallspace->Enterprise;

    x1=tempSpaceship.getXPosition();
    y1=tempSpaceship.getYPosition();

    printf ("direction ? ");
    scanf ("%d", &direction);

    if (direction>=0 && direction<360){
        tempSpaceship.setDiraction(direction);

        printf ("distance ? ");
        scanf ("%d", &distance);

            x1=tempSpaceship.getXPosition();
            y1=tempSpaceship.getYPosition();

//            printf ("x1:%3d , y1:%3d \n", x1+1, y1+1);

            tempSpaceship.forward(distance);
            x2=tempSpaceship.getXPosition();
            y2=tempSpaceship.getYPosition();

//            printf ("x2:%3d , y2:%3d\n", x2+1, y2+1);

        if ( (x2<0) || (x2>7 ) || (y2<0) || (y2>7) ){
            printf ("Out of range!\n\n");
            return;
        }

        if (mSmallspace->mSpaceMap[x2][y2]==0){
            mSmallspace->mSpaceMap[x1][y1]-=1;
            mSmallspace->mSpaceMap[x2][y2]+=1;
            mSmallspace->Enterprise.setDiraction(direction);
            mSmallspace->Enterprise.forward(distance);
            mSmallspace->displayMap();
        } else {
            printf ("Can not move!\n\n");
        }

    } else {
        printf ("Input 0-360!\n\n");
    }
}


【main.cpp のソース】

#include <QCoreApplication>
#include "ap_spaceship.h"
#include "ap_smallspace.h"
#include "ap_startrekgame.h"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    // ゲームを作成
    startrekgame* startrekgameA = new startrekgame;

    // マップを表示
    startrekgameA->displaySpaceMap();

    int command = 0;

    // 終了コマンドが入力されるまで繰り返し
    while (command >=0 ){

        // コマンドを入力
        command = startrekgameA->inputCommand();

        // コマンドを実行
        if (command>=0){
            startrekgameA->execCommand();
        }
    };

    // ゲームを終了する
    printf ("\nGame End.\n");

    // ゲームを消去
    delete startrekgameA;

    return app.exec();
}


前回 Turtleクラス を作成し,方向転換や前進できるようなクラスを作り,それを応用して スタートレックゲームの一部の部分を作ってみた。

オブジェクト思考設計で大事なのは,なぜ,オブジェクト思考設計をするのかという目的を常に忘れないことだ。

自分が楽になる,開発期間を短縮する,毎回一から作り直さない ようにするには,ソフトウェアの再利用が必ず必要となる。ただ,コピペするのではなく,作り直しをできるだけ少なくして,目的を達成するためにはどうすればよいかを考える。

Qt は非常に便利だが,C++ やオブジェクト指向設計を理解していないと,なぜ,それができるのかが分からず,商品化まで到達するのは難しいと思う。

次回は,リアル波形を表示するために必要なマルチスレッド処理をQtでやってみる。

P.S.

今回紹介した超簡易版スタートレックゲームの Qtプロジェクトを含むソースファイルを入手できるようにしました。

【スタートレックゲームのソースファイルの入手方法】
次のメールアドレスに件名:スタートレックソースファイル送信 と書いてメールをお送りください。(件名は”スタートレックソースファイル送信”でなければ返信されません。メール本文は何でも構いません。
メールアドレス:CriticalSoftwareConsulting[あっとまーく]gmail.com