2019-08-09

Raspberry Pi3を使ったリアル波形描画(9) アーキテクチャの設計思想について

本特集記事の「Raspberry Pi3を使ったリアル波形描画」を実現するにあたり、採用したアーキテクチャの設計思想を説明しようと思う。

なお、今回より、Qt で作成したリアル波形描画のシステム(基本フレームワークだけで、フィルタや心拍検出等の中身はスケルトンにしてある)について解説するのだが、実際に動いた実績のあるソースコードを見ながら解説を読んでもらった方がよいと思う。そこで、Qt でコンパイルできるソースファイル一式と Qt の設定画面を PDF にしたものを希望する方が入手できるようにした。(Qt の設定画面を付けたのは、同じ環境にするのは意外と大変なので)

リアル波形描画のソースコード入手方法

入手方法は第一回目の記事『Raspberry Pi3を使ったリアル波形描画(1) イントロダクション』で Windows で実行できるデモファイルを配布したときと同様に、Google Drive の共有設定で共有できるフォルダにパスワード付きの圧縮ファイルを格納してダウンロードできるようにし、パスワードはメールの自動返信機能で送るようにした。

なお、最近、WEBブラウザのセキュリティが強化され、ファイルをダウンロードするときにWEBブラウザによっては、いろいろ聞いてくるようになったようだ。gmailのアドレスを持っていなくてもダウンロードできるはず。共有フォルダがからファイルをダウンロードする手順については IE11 と Microsoft Edge ではできることを確認したので、上手くいかなかったときは、これらのWEBブラウザで試して欲しい。

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

リアル波形描画のソースコードについて

本ソースコードは 2019年8月時点で Qt 5.12.2 / Qt creator 4.9.0 にてコンパイル・リンクし、Windows 10上で 動作しているものである。

自分のパソコンに Qtをインストールし、コンパイル/リンクして、実際に動くことを確認した上で、どのようなソースコードで動いているのか確認してみて欲しい。

どんな環境であれ、「実際に動いている」という実績は設計者に取って絶大な安心感につながる。裏返せば、ソフトウェアは一行どころか、1文字変更しただけで、システムが動かなくなるということもある。だから、一度、ちゃんと動いているという確信を得た上で、ソフトウェアを修正しては動作を確認しという作業を繰り返していれば、上手くいかなかったときに少しの後戻りでまたちゃんと動いていたところに戻ることができる。

※設計せずにコーディングをするのはダメという金言をよく聞くが、小さい実験システムでも、まずは動くコードを押さえて安心した上で、コードも設計書もきれいにするというのが自分のスタイルだ。小さいながらも「動くコード」がない状態で、設計書だけを書き進めていって実装する壁にぶつかって止まるのはイヤだ。

さて、「Raspberry Pi3を使ったリアル波形描画アプリ」のユーザーインターフェースは次のようになっている。

画面の説明







これがリアル波形描画のソフトウェアをWindows 上で実行したときの画面になる。
  1. 波形表示領域:上部の3種類のデジタルフィルタを通した後の波形。
  2. デバッグ用波形表示領域:内部処理が意図通りに動いているかどうかを確認するための表示領域。
  3. 入力ソースの切り替え(DEMO, A/D変換器, SIN波):画面に映っているのはDEMO用の三角波。
  4. 波形の切り替え(三角波1, 三角波2, 疑似心電図):三角波1 は心拍数が60になるように調節してある。入力ソースが DEMOのときのみ有効。
  5. 感度切り替え(×1/4, ×1/2, ×1, ×2, ×4)
  6. 波形掃引の開始/停止ボタン:4msインターバルと10msインターバルのスレッドを起動/終了している。
  7. 心拍数の表示:心電図のとがった部分(QRS波形)が1分間に何回計測したかをカウントして表示している。※マークはQRS波形検出時に200ms表示している。
  8. 電源ノイズフィルターON/OFF:心電図に重畳した電源ノイズ50Hz/60Hzを除去するためのデジタルフィルターのON/OFFスイッチ。ソースの中身は空となっている。(自分でフィルタを設計して実装する)
  9. ドリフトフリーフィルタON/OFF:心電図は体動などの影響で基線動揺が起こる。基線が動揺すると画面からはみ出したりして見にくいので、ドリフトフリーフィルタ(ローカットフィルタ)を入れる。ソースの中身は空となっている。(自分でフィルタを設計して実装する)
  10. ハイカットフィルターON/OFF:A/D変換前にアナログのハイカットフィルターを入れるが、それよりも低い周波数帯域のハイカットデジタルフィルター。ソースの中身は空となっている。(自分でフィルタを設計して実装する)
  11. フィルタの遅れ時間:デジタルフィルタをかけると遅れ時間が生じる。特にドリフトフリーフィルタは遅れ時間が大きい。入力波形に対して表示している波形がどれくらい遅れているのか表示する。
  12. 入力波形:フィルタ後の入力波形。
  13. 心拍検出のためのスレッショルドレベルの変化の様子:心拍数を計測するために14のフィルタ出力値のピークを計測して、そのピークを階段状に下げていき、スレッショルドレベルを設定している。(簡易的なものを実装している。このスレッショルドレベルを超えた時が新たなQRSの起点となる。)
  14. 心電図波形の微分値:心拍数を計測するために、入力波形のうち心電図のとがった部分(QRS波形)だけを際立たせるようなフィルタ(現在はサンプルで入力波形を微分して、絶対値を取っている。暫定的なもの。)をかけている。
心拍検出部分とデジタルフィルタをスケルトン(メソッドと入出力のみ実装し、中身を空にしてある)にしているのは、そこに業務ドメイン上のノウハウがあると考えているからであり、その他の部分のソースコードを公開したのは、そのアーキテクチャ自体はドメインには特化していない技術であり、学ぼうと思えばデザインパターンの本など教材は入手する方法がいろいろあるからである。

繰り返すが、もの作りをするソフトウェアエンジニアにとって、「実際に動いている」実績のあるソースコードは貴重で、「動いている」ことを確認した上で、自分自身で一から同じものを作り上げてみて、同じように「動いている」状態にまで仕上げることと、アーキテクチャを含めより理解が深まるし、自信につながる。

こちらはPC上で波形表示を行ったデモ画像


こちらは 5インチのLCD上で波形表示を行ったデモ画像

アーキテクチャの目標

今回のソフトウェアのアーキテクチャの目標は次の通り。
  1. 一発勝負ではなく、商品や商品群として10年間は骨格を変えずに使えるアーキテクチャにしたい。
  2. 変わりやすい部分(画面表示を含むUI)と変わりにくい部分(デジタルフィルタや心拍数検出の論理など)を分離したい。
  3. 心拍数計測等のアルゴリズムの改善作業を見越して、実際の生体信号の入力(A/D変換)ではなく、サンプルデータやSIN波を入力ソースとできるようにしたい。
  4. デジタルフィルタや心拍数計測アルゴリズムが意図通りに働いているかどうかをデバッグ画面で確認できるようにしたい。
  5. 普段はPC上で検討を重ね、シミュレーションで上手くいくことが検証をかさね、その後、実機でバリデーションするようにしたい。(開発効率を高めるため)
  6. タイマー制御、スレッド制御といったリアルタイム制御に関する部分は Qt で行い、作成するアプリケーションソフトウェアからはできるだけ隠蔽したい。
1回動くものができればいいという発想ではなく、商品群として長く保守や性能向上を継続し、開発効率が良く、競争力が高く、研究開発もでき、保守しやすいアーキテクチャを目指している。

ただただ早く動くものを作りたいという考え方でソフトウェアの開発を続けていくと、開発効率は上がらず、過去の作ったソフトウェアの保守に追われるという悪循環から抜け出すことができない。

そのような負のスパイラルから脱却するためには、長持ちして、競争力が高く、保守もしやすいアーキテクチャを目指す必要がある。

実現の可能性(Feasibility Study)の結果

上記の目標を実現するのに重要な「実現可能性」のポイントは、Qt で正確なタイマ割り込みを発生させ、かつ、優先度の高いスレッドと低いスレッドを走らせることができるかどうかだった。

Raspberry Pi3B+ のCPU は ARM Cortex-A53 1.4GHz でクワッドコア(CPU4つ)なので、4ms の定期割り込みの発生など、CPUスペックから考えれば楽勝と思えるのだが、そう簡単にはいかない。

なにしろ、Linux はリアルタイムOSではないし、OSの制限もあり、msオーダーの定期割り込みを発生させるのは簡単ではない。

さらに、このシステムを実現するには、マルチスレッドが必須であり、かつスレッド間のデータの受け渡しが必要で、これが意外に難しい。

でも、Qt の SIGNAL/SLOT のしくみを使うことによって、マルチスレッドとイベントの伝達、データの受け渡しができることが分かった。

ただ、できれば、CPUコアが4つあるので、4msインターバルの割り込み処理を行うスレッドは4つのコアのうち一つに割り当てたかったのだが、Qt 上でその設定ができなかった。

その代わり、定期インターバルの起動間隔に大きなバラツキが発生したいないことは、常に確認できるようにした。

10ms 間隔で起動されるスレッドで、システムタイマーの値を読み込んで、前回のタイマーの値との差分を printf で表示するようにしてある。下記は Windows で走らせたときの様子だ。これを Raspberry Pi3B+ で実行したときも、間隔に大きなずれがなければ、定期割り込みは成功していることになる。ちなみに、下記の例ではインターバル間隔に 0.5% 程度の誤差(ジッター)がある。この誤差がシステムとして許容できないようならば、Raspberry Pi3B+ の外側で発生させるか、Raspberry Pi3B+ にリアルタイムOS を搭載する。今回は、できる限り、ハードウェアの追加なしで実装したかったので、0.5%のジッターは許容したが、常にブレが大きくなっていないかどうかをWatchしておくことは重要だ。

time: 1475803.318404771 CLOCK_REALTIME 09987 DIFF:uSec
time: 1475803.328394707 CLOCK_REALTIME 09989 DIFF:uSec
time: 1475803.338389771 CLOCK_REALTIME 09995 DIFF:uSec
time: 1475803.348481033 CLOCK_REALTIME 10091 DIFF:uSec
time: 1475803.358499317 CLOCK_REALTIME 10018 DIFF:uSec
time: 1475803.368381897 CLOCK_REALTIME 09882 DIFF:uSec
time: 1475803.378349216 CLOCK_REALTIME 09967 DIFF:uSec
time: 1475803.388392228 CLOCK_REALTIME 10043 DIFF:uSec
time: 1475803.398597482 CLOCK_REALTIME 10205 DIFF:uSec
time: 1475803.408382657 CLOCK_REALTIME 09785 DIFF:uSec
time: 1475803.418337612 CLOCK_REALTIME 09954 DIFF:uSec
time: 1475803.428369466 CLOCK_REALTIME 10031 DIFF:uSec
time: 1475803.438366942 CLOCK_REALTIME 09997 DIFF:uSec
time: 1475803.448362910 CLOCK_REALTIME 09995 DIFF:uSec
time: 1475803.458338070 CLOCK_REALTIME 09975 DIFF:uSec
time: 1475803.468366909 CLOCK_REALTIME 10028 DIFF:uSec
time: 1475803.478349608 CLOCK_REALTIME 09982 DIFF:uSec
time: 1475803.488375431 CLOCK_REALTIME 10025 DIFF:uSec
time: 1475803.498362352 CLOCK_REALTIME 09986 DIFF:uSec
time: 1475803.508351083 CLOCK_REALTIME 09988 DIFF:uSec
time: 1475803.518362431 CLOCK_REALTIME 10011 DIFF:uSec
time: 1475803.528348146 CLOCK_REALTIME 09985 DIFF:uSec
time: 1475803.538355272 CLOCK_REALTIME 10007 DIFF:uSec
time: 1475803.548384713 CLOCK_REALTIME 10029 DIFF:uSec
time: 1475803.558325194 CLOCK_REALTIME 09940 DIFF:uSec
time: 1475803.568313623 CLOCK_REALTIME 09988 DIFF:uSec
time: 1475803.578347889 CLOCK_REALTIME 10034 DIFF:uSec
time: 1475803.588317923 CLOCK_REALTIME 09970 DIFF:uSec
time: 1475803.598452309 CLOCK_REALTIME 10134 DIFF:uSec
time: 1475803.608321620 CLOCK_REALTIME 09869 DIFF:uSec
time: 1475803.618302208 CLOCK_REALTIME 09980 DIFF:uSec

ちなみに、この部分を実験したのが『Raspberry Pi3を使ったリアル波形描画(6) Qtでマルチスレッド処理をやってみる』の記事になる。 この実験によって、およそQt上で4ms と 10ms の間隔のタイマー割り込みを発生させ、マルチスレッドで優先度の高いスレッドにタイマー割り込み処理を任せることができた。(目標6) その確認なくして、Qtを使ったリアル波形描画 の構想はありえないし、意味がない。

 また、Qt そのものの特長により、マルチプラットフォームでの開発と、UI部分の開発効率を高めることができることも分かった。(目標2,5) 以前の記事でアーキテクトは「実現の可能性(Feasibility Study)」について「いける!」という感覚をつかめるかどうかが重要だと言うことを書いた。 「実現の可能性(Feasibility Study)」について「いける」のか「いけないのか」を判断するには、その商品に対して求められている要求と要求の優先度が頭の中に入っており、その要求を実現するための実現方法の引き出しをたくさん持っている必要がある。

要求はさまざまあり、全部実現しようとするとコストが上がったり、開発の時間が足りなくなったりするので、競争力の高い商品にするためには、どの部分に独自の研究開発のリソースをつぎ込んで、どの部分はすでにある技術や市販のソフトウェアで補うのか、一番いいバラインスポイントを探す必要がある。

それには深いドメイン知識と、広いソフトウェアエンジニアリングの技術、豊富なソリューションの引き出しを持っていることが大事だ。

ちなみに、オブジェクト思考設計のコンサルタントは「広いソフトウェアエンジニアリングの技術」、「豊富なソリューションの引き出し」を持っているが、「深いドメイン知識」はない。一般的に組込みソフトウェア製品の企業のエンジニアは「深いドメイン知識」を持っているが、「広いソフトウェアエンジニアリングの技術」と「豊富なソリューションの引き出し」を持っている者は少ない。

よって、長持ちして、競争力が高く、保守もしやすいアーキテクチャを目指すためには、両者が協力するか、企業のエンジニアががんばって知識やスキルを身につける必要がある。

なお、ドメイン知識として、ユーザーまたは業務ドメインにおける市場要求の分析は、製品企業側で分析しておく必要がある。お客さんの声を吸い上げることができるのは、製品を設計販売している企業であり、そこに実現すべき機能や性能のもととなる要求がある。

要求の整理についてはQFD(品質機能展開。左図が一例)などを使うとよい。または、マインドマップなどで整理してもよい。 それらの要求が実現可能かどうかは、さまざまな条件(コスト、リソース、エンジニアが使いこなせるかどうか等々)があるので、単純ではない。なにしろ幅広い知識(引き出し)と、最新の情報にアンテナを張っていることが必要になる。 今の世の中情報はあふれかえっているので、やりたいこと、実現したいことは常に頭の中に記憶しておいて、やりたいことの実現に関係ある情報を見つけたら、その情報をクリップしておいて掘り下げるという習慣を付けておく必要がある。 働き方改革で、オンとオフをキッチリ分けて休むときは休んでエネルギーを充電するのはいいと思うが、アンテナはオフのときも立てておいて、実現可能性の種がピピッときたらメモっておくぐらいのとをやっていないと、なかなか競争には勝てない。

静的構造の説明(3層構造)



さて、「Raspberry Pi3を使ったリアル波形描画」のアーキテクチャ(静的な構造、クラス図)について説明していこうと思う。 なお、このアーキテクチャについては今回記事の冒頭でソースコードを入手できるようにしてあるので、ソースコードを見ながらクラス図を眺めて欲しい。

デジタルフィルタと心拍数検出のアルゴリズムについては、それぞれソースコードはインタフェースだけ用意してあり、中身は空または簡易的なものにしてある。(ここが最も重要なノウハウ=コア資産と考えているため)

この部分の性能を高めることができれば、他社に対する競合力となり、その資産は組織の財産となる。また、その資産が真似しにくい若しくは特許で保護されていれば、商品群のコア資産として商品の価値を生み出す宝となりうる。

 このアーキテクチャは プレゼンテーションレイヤー、ドメインレイヤー、データソースレイヤーの3層構造となっている。 プレゼンテーションレイヤーは、ユーザーとのユーザインタフェースを担う部分であり、表現方法は商品群の中でも変わりやすい(ハイエンド製品は大画面、ローエンド機器は小画面など)。逆に言えば、プレゼンテーションレイヤー部分を変えることで、ラインナップを揃えたり、マイナーチェンジをして商品を新しく見せることができる。

仕向け地や特別なユーザー用にカスタマイズすることもあるかもしれない。 プレゼンテーションレイヤーは変わりやすく、変化に柔軟に対応できるようにしておく必要がある。

プレゼンテーションレイヤーの部分の変化に対する対応は Qt Creator を使うことで、簡便にいろいろな画面を実現することが可能だ。

また、ドメインレイヤーは、その商品あるいはその商品群に特有の資産を配置している。ドメインに特化しているため、その商品の本質的な機能や性能を実現していることが多い。別な言い方をすれば「本業の強み」を実現する部分だ。場合によってはソフトウェアだけでなくハードウェアも含めてコア資産となる場合もある。 基本、このドメインは「変わりにくい部分」であり、ノウハウとなり得る。

なお、今回のケースでは、黄色背景のタイマー処理とスレッドのクラスもドメインレイヤーに配置した。これらはドメインに特化したクラスではないものの、プレゼンテーションレイヤーやデータソースレイヤーとも異なり、リアルタイムシステム実現のための要素でり、システム実現の根幹と考えたからである。

データソースレイヤーはその名の通り、システムが使用する入力データの元となるレイヤーであり、ここは、 EcgAcquirer (心電図の受入れ者)から派生させた、3つのクラスを実装している。 その意図は、入力クラスのインタフェースを共通にしておき、入力元の実態を A/D変換器と、サンプルデータと sin波 の3種類で切り替えることができるようにしている。 信号処理アルゴリズムの設計、性能分析、性能向上を検討するためには、リファレンスとなる波形データや、問題が起こったときの 波形データをファイルから読み込んでシミュレーションでさまざまな確認を行うことが重要となる。

アルゴリズムを変更したときなど、今まで動いていた機能や性能が、意図通りに動いているかどうかを確認することはとても重要であり、その確認・検証には、検証に使うシミュレーション用のデータがあるとよい。そのための入力切り替えでもある。

また、デジタルフィルタの効き具合を確かめるのに、シミュレーションでsin波を入力できると都合がよい。 入力の切り替えのしくみは、C言語でも条件コンパイルを駆使したりして実現は可能だが、オブジェクト指向言語であるC++を使えば、スマートに実現できるし、クラス図でその設計思想を表現するのもやりやすい。

実際、このクラス図も、Qtで設計して上手くいくことを確認したC++のソースファイルを UMLツールである Enterprise Architect に読み込ませて、整えることでクラス図にした。 そうすれば、実際のソースコードの設計思想を正確にクラス図で表現できるからだ。 入力の切り替えは、紫背景のQt Creator で作成したボタンの押下をトリガーにして、緑背景の EcgMonitor で入力のオブジェクトインスタンスを入れ替えることで、簡単に切り替えることができる。 これができると非常に気持ちがいい。また、入力のインタフェースが変わらなければ、さらに別の入力例えば、既存の心電図データベースを追加することもできる。
る。

Qt Creator の画面

左記が QtCreator で RealWaveDisplay01 のプロジェクトファイルを開いたときの画面である。ざっと、ソースファイルの説明をしておく。

まず、頭に ap_ がついているファイルは、リアル波形描画特有のアプリケーションを意味する。

qt_ がついているファイルは、Qt に特化した部分、今回は周期起動するスレッド処理。

qcustomplot.h と qcustomplot.cpp はオシロスコープのように右から左に波形を流すために使っているソースで、『Raspberry Pi3を使ったリアル波形描画(3) Qt + QCustomPlot で静止グラフを描いてみる』の記事の一番下に使い方の解説動画がある。

main.cpp は Qt のお決まりの入り口関数で、mainwindow.h と mainwindow.cpp は Qt のメインウインドウの画面の初期化やら、画面で使っている変数や、ボタンが押されたときの処理のメソッドなどが入っている。

アプリケーションのソフトウェアのファイルの説明は次の通り。
  • ap_EcgAcquirorFromSampleData.cpp :DEMO波形のデータ
  • ap_EcgAcquirorFromADConvertor.cpp :A/Dコンバータからの入力(現在スケルトン)
  • ap_EcgAcLineNoiseFilter.cpp :電源ノイズフィルター(現在スケルトン)
  • ap_EcgHighCutFilter.cpp :ハイカットフィルター(現在スケルトン)
  • ap_EcgDriftFreeFilter.cpp :ドリフトフリーフィルタ(現在スケルトン)
  • ap_EcgMonitor.cpp:波形計測の中心となるクラス(各種オブジェクトの生成も行う)
  • ap_EcgAcquirorFromSinWave.cpp:SIN波発生
  • ap_EcgDetectQrsTypeTest1.cpp:QRS検出(簡易的なアルゴリズムにしている)

mainwindow.cpp の説明

mainwindow.cpp 内の MainWindow クラスは QMainWindow クラスから派生したクラスでヘッダは次のようになっている。

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    // コンストラクタ
    explicit MainWindow(QWidget *parent = nullptr);
    // デストラクタ
    ~MainWindow();

    // グラフ初期化
    void intializeGraph();

private slots:
    // Run/Stop ボタンを押された時の処理
    void on_pbt_run_clicked();
    // 波形タイプボタンを押された時の処理
    void on_pbt_wavetype_clicked();
    // 電源ノイズフィルターボタンが押された時の処理
    void on_pbt_ac_line_filter_clicked();
    // ドリフトフリーフィルタボタンが押された時の処理
    void on_pbt_drift_free_filter_clicked();
    // ハイカットフィルタボタンが押されたときの処理
    void on_pbt_hicut_filter_clicked();
    // 表示感度ボタンが押された時の処理
    void on_pbt_displaygain_clicked();
    // 入力ソースボタンが押されたときの処理
    void on_pbt_input_type_clicked();
    // 周波数スピンボックスの値が変化したときの処理
    void on_doubleSpinBox_frequency_valueChanged(double arg1);

public slots:
    // 4ms周期処理
    void work4msPeriodicOnMainWindow(void);
    // 10ms周期処理
    void work10msPeriodicOnMainWindow(void);
private:
    Ui::MainWindow *ui;

    CyclicThread *mpCyclicThread4ms     = new CyclicThread;      // 周期起動のスレッドオブジェクト(プライベートメンバ変数)
    CyclicThread *mpCyclicThread10ms    = new CyclicThread;      // 周期起動のスレッドオブジェクト(プライベートメンバ変数)

    float mTimekey_wave = 0;                        // 波形表示間隔 (s)
    float mWave_data = 0;                           // 波形データ
    float mAdd_data = 0.1F;

    float mTimekey_debugwave=0;                     // デバッグ用波形表示間隔
    float mQrsFilterWave_data=0;                    // QRSフィルt出力用波形データ
    float mThresholdLevel_data=0;                   // スレッショルドレベルデータ

    unsigned short mDisplayCounter=0;               // ディスプレイ用のカウンタ(10msの倍数で表示する)

    bool mRunStopFlag = false;                      // Start/Stop フラグ

    unsigned short mEcgDemoCounter=0;               // 心電図デモカウンター
    unsigned short mDemoWaveType=0;                 // デモ波形タイプ0:三角波 HR60 1:三角波 HR300 2:疑似心電図,);
    float mMaxWave=0;                               // 最大波形
    float mMinWave=0;                               // 最小波形
    float mPreviousPlotData=0;                      // 前回のプロットデータ

    bool mRealWaveFlag=false;                       // リアル波形<->デモ波形スイッチ

    // 心電図モニタクラスを生成
    EcgMonitor  *mpEcgMoniter = new EcgMonitor;     // 心電図モニタクラス

    bool mAcLineFilterFlag;                         // 電源ノイズファルタフラグ
    bool mDriftFreeFilterFlag;                      // ドリフトフリーフィルタフラグ
    bool mHicutFilterFlag;                          // ハイカットフィルターフラグ
    unsigned short mHRDisplayCounter;               // ハートレート表示用カウンタ(1秒)
    unsigned short mQrsMarkDislpayCounter;          // QRSマーク表示用カウンタ
    bool mQrsMarkDislpayFlag;                       // QRSマーク表示用フラグ

    unsigned short mDisplayGain;                    // 表示用の感度変数
    char mGainText[5][6]={("x1/4"), ("x1/2"),("x1  "),("x2  "),("x4  ")};
    unsigned short mInputType;                      // 入力種別
    char mInputTypeText[3][18]={("Demo             "),
                                ("A/D Convertor    "),
                                ("Sin Wave         ")};
    double mFrequency;                              // sin波の周波数

};

頭が on_pbt_・・・ となっているメソッドは、画面上でボタンが押されたときに、呼ばれるメソッドとなる。

各種変数の定義と初期化、4msと10msの周期起動のスレッドの生成、心電図モニタメインクラスの生成などを行っている。(青字部分)

入力の切り替え


入力の切り替え部分を説明する。登場人物の説明
  • EcgAcquirorクラス:心電図入力の基底クラスで実態を持たない。
  • EcgAcquirorFromADConvertor:EcgAcquirorクラスから派生したクラスで、A/D変換器からの入力を意図している。(公開ソースでは、スケルトンにしてある)
  • EcgAcquirorFromSampleData:EcgAcquirorクラスから派生したクラスで、データで定義したサンプルデータ(三角波二種類と疑似心電図データ)を供給することを意図している。
  • EcgAcquirorFromSinWave:EcgAcquirorクラスから派生したクラスで、SIN波のデータをEcgMonitorクラスに供給することを意図している。
EcgAcquiror は 心電図入力の基底クラスで getEcgWaveData() を純粋仮想関数にして、心電図波形データの入力ソースを切り替えられるようにしている。

EcgAcquiror 入力のインタフェースを固定しておき、EcgAcquiror クラスから 派生させた入力ソースのクラスから、さまざまなデータを EcgMonitor クラスに共有する。

EcgAcquirorFromADConvertorクラス以外は、実機(製品)では使わないのだが、デジタルフィルタや心拍計測アルゴリズムの検証したり、デモンストレーションしたりするときに、EcgAcquirorFromSampleDataクラスや、EcgAcquirorFromSinWaveクラスはあると便利だ。

また、例えば、実際にA/D変換して取り込んだ心電図データや、いろいろな心電図のデータライブラリをファイルから読み込むような時には、EcgAcquirorクラスから派生させたクラスを新たに作成して、EcgMonitor クラスに持たせれば 簡単かつ、安全に入力ソースを追加することができる。

この「簡単かつ安全に追加できる」ということがオブジェクト指向言語である C++ を使う目的であり、組込みソフトウェアでも C言語にこだわっている時代ではないと思う理由だ。C言語でも同じことはできると思うが、「簡単かつ安全に追加する」ためにはC++を使った方がよいと思う。

具体的には EcgMonitor クラス のメソッドに 心電図入力オブジェクトのポインタ変数を作っておき、派生させた3つのクラスを new で作成してそれらのポインタをポインタ変数に格納している。

protected:
    // 心電図入力オブジェクトのポインタ(メンバ)をする
    EcgAcquiror *mpEcgAcquiror;
    EcgAcquirorFromSampleData   *mpEcgAcquirorDemo  = new EcgAcquirorFromSampleData;
    EcgAcquirorFromADConvertor  *mpEcgAcquirorADC   = new EcgAcquirorFromADConvertor;

    EcgAcquirorFromSinWave      *mpEcgAcquirorSin   = new EcgAcquirorFromSinWave;


void EcgMonitor::activateEcgDemoMode(void)
{
    // 心電図入力オブジェクトにデモ波形オブジェクトを設定して、デモタイプを三角波にする
    mpEcgAcquiror       = mpEcgAcquirorDemo;
    mpEcgAcquirorDemo->setEcgDemoType(0);

}


void EcgMonitor::activateEcgAdcMode(void)
{
    // 心電図入力オブジェクトにADCオブジェクトを設定する
    mpEcgAcquiror       = mpEcgAcquirorADC;

}


void EcgMonitor::setEcgDemoType(unsigned short type)
{
    if ( type > 2) type = 0;        // 3以上だったら強制的に 0:ECGにする

    // 波形種別を設定する。
    mpEcgAcquirorDemo->setEcgDemoType(type);

}


そして、入力モードの切り替えが指示されたら、mpEcgAcquiror に各、入力ソースのポインタをセットしてあげればよい。

それだけで、その後の処理は何も変える必要がない。

   //----------------------------------------------
    //     内容:疑似心電図三角波
    // サンプリング: 4ms
    //   データ数: 250
    //     振幅:1000
    //----------------------------------------------
    // HR = 60のデータ
        {
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.000 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.010 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.020 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.030 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.040 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.050 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.060 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.070 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.080 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.090 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.100 ~
               0, 100, 200, 300, 400, 500, 600, 700, 800, 900, // No.110 ~
            1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, // No.120 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.130 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.140 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.150 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.160 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.170 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.180 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.190 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.200 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.210 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.220 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.230 ~
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0 // No.240 ~

        },

サンプルデータはこんな感じでデータが定義されている。

そして、このような切り替え可能な構造になっていいることは、作成したソースコードを UML ツールに読み込んで、整形して クラス図にすることで、モデルで確認することができる。

それが、上記のクラス図だ。クラス図を描いてからソースコードを作ったり、ソースコードをリバースエンジニアリングしてクラス図を作ったりを繰り返していると、だんだん派生のしくみや、クラス図どおりに実装するにはどうすればよいかが分かってくる。

次回以降は、入力部以外のアーキテクチャを解説していく。

2019-06-10

Raspberry Pi3を使ったリアル波形描画(8) アーキテクトの腕の見せどころ

この特集記事とは別に『ソフトウェアアーキテクチャとは何かを考える』という記事を書いた。

ここで説明したのはリアル建築で例えれば「どんな生活をしたいのか」「家主の要望は何か」「予算はどれくらいか」などによって,家の構造設計は変わるということだ。

ソフトウェアのアーキテクチャも何をしたいのか,どんな困りごとを解決したいのかによって,ソフトウェアシステムのアーキテクチャは変わる。ソフトウェアのアーキテクチャの重要なポイントは「設計思想」だと思う。設計思想こそが「アーキテクトの腕の見せどころ」のポイントなのだ。

『ソフトウェアアーキテクチャとは何かを考える』にも書いたが,ソフトウェアの場合は,「大改造!!劇的ビフォーアフター」のようにリフォーム後に アーキテクトの仕事の成果を顧客とともに見て回り,リフォームのできに感動してもらうといったシーンがない。ソフトウェアは見えないから,アーキテクチャの善し悪しは分かる人にしか分からない。そこがなんとも悲しいところなのだが,だからこそ,アーキテクトはそのシステムのアーキテクチャの設計思想を他人にキチンと説明できるようにしておかないとダメなのだろう。

そう思い,今回の記事では 『Raspberry Pi3を使ったリアル波形描画』の取り組みの中で採用しようとしているアーキテクチャの設計思想を説明しておきたいと思う。

考え方はInterface誌 2003年1月号に投稿した記事『オブジェクト思考を使ったリアルタイム信号計測システムの開発』と同じなので,まずは,この記事で想定した 1ch のデジタルオシロスコープを例に説明しようと思う。

ここに 1ch のデジタルオシロスコープの完成予想図がある。この機器の目的は,電気回路の信号をピックアッププローブで拾って,高周波のノイズを取り除いて,目的の信号だけを見えるようにして,結果をUSB経由でPCに保存するというものだ。

この製品のソフトウェアシステムを設計したい。設計の前に解決したい問題を確認しておこうと思う。

解決したい問題

この商品の開発の前提として,この商品は単発の商品とするのではなく,商品ラインナップにして,基本の機能はさまざまなオシロスコープ製品群に使いたいと思っている。

この商品の基本機能はノイズを除去するためのデジタルフィルターであり,ハイエンド製品では,さまざまな,フィルターを搭載できるようにしたり,切り替えたりできるようにしたい。よいデジタルフィルターを設計し,搭載することができれば,それがこの商品群のコア資産となり,その資産が他社の商品と競合したときのアドバンテージとなる。

したがって,アーキテクチャの第一の目的は「コア資産となるデジタルフィルタのソフトウェアモジュール」の再利用しやすく設計し,さまざまな商品群にソフトウェアにまったく手を加えることなく※1,利用できるようにすることである。

※1 コア資産の主要部分(パラメータ設定以外)についてはソースコードを1行も変更しないで複数の製品に使用できることを「再利用」と言っている。ソフトウェアは簡単に変更することができ,1行変更しただけで動かなくなったり,デグレードしたりするので「再利用」は基本,ソースコードの変更はなしでできることが前提。

それがうまくいけば「競争力の高いソフトウェア資産」使って,さまざまな商品展開ができ,顧客満足が高まり,商売も儲かる。

また,「競争力の高いソフトウェア資産」に問題があり,それが市場で発覚すると,組織に大きなダメージを与えるため,コア資産となるソフトウェアの信頼性は十分に確認しておきたい。これらの要求をまとめると次のようになる。
  1. コア資産となるデジタルフィルタのモジュールを再利用可能な資産(ソフトウェア本体にまったく手を入れることなく再利用可能)にする。
  2. コア資産となるデジタルフィルタはさまざまなバリエーションを持たせる。
  3. 新しいフィルタを開発することと,採用するフィルタの妥当性確認(バリデーション)をしやすくするために,実機ではなくPC上でフィルタ効果を確認できるようにしたい。
  4. フィルタの入力は信号ピックアッププローブから入力するだけでなく,検証用に持っている信号データや,擬似的に作成したデータを入力できるようにしたい。
  5. データの保存はUSB経由を想定しているが,将来は Wi-Fi や ブルートゥース経由でPCへ保存できるようにしたい。
ちなみにソフトウェアの「再利用」ではなく「流用」ということばを使っている技術者をよく見かけるが,自分は「流用」ということばは絶対に使わない。「流用」ということばの響きに「再利用のための設計思想はありません」というニュアンスを感じるからだ。

設計思想のないアーキテクチャには将来ろくな事がないと思う。設計思想なく「流用」されたソフトウェアは,最初に設計したときの思想が伝わることなく,別のシステムにはめ込まれ,予想もしなかったような不具合が起きることがある。

さて,これらの要求を実現するための概念的な構造を図で表してみると左図のようになる。(設計の思想)

入力は製品では信号ピックアッププローブから入力(A/D変換)するが,実験,検証のために信号データファイルからも入力できるようにしたい。

この入力の切り替えは簡単にスイッチできるようにしたいし,その後のソフトウェア(デジタルフィルタやディスプレイ表示やデータ保存)には影響を与えたくない。(1行もソフトウェアを変えないで済むようにしたい)

信号処理部のデジタルフィルターは,さまざまなものをラインナップし,こちらも簡単に切り替えられるようにしたい。新しいフィルタも簡単に試せるようにしたい。

波形の出力は 小さなLCDディスプレイに表示するが,このLCDディスプレイは商品によってサイズや解像度が変わる。また,後継機種を出す際にはLCD部品が変わる可能性が高いので,これらの変化に柔軟に対応できるようにしたい。

さらに,データの出力機能としては,当初は USB I/FでデータをPCに送るが,将来はWi-Fi や ブルートゥース経由でデータを格納できるようにしたい。

このシステムにおけるコア資産は何か?変わりない部分と変わりやすい部分

この商品または商品群における「コア資産」はデジタルフィルタA, B, ・・・ Z である。これらは有効性が確かめられれば,今後,ずっと代えることなく使い続けることのできる資産である。長い間変わりのない部分となる。そして,ずっと変わらず性能がよければ他社との競争力の源泉にもなる。これらのコア資産を最大限活かすことが,今回のアーキテクトの設計思想(腕の見せどころ)だ。

なお,出力部のLCDディスプレイは変更となる可能性が高い。また,出力の I/F は時代とともに新たなものが増えていく。ようするに,将来の商品開発を見据えて長い目でみれば,変わりやすい部分と言える。

変わりやすい部分の影響を受けて,コア資産にも変更が加わるようにはしたくない。ソフトウェアアーキテクチャ上,信号処理部と出力部はキッチリ分けて,信号処理部から出力部へのデータ渡しの I/F はできるだけ変わらないようにしたい。

また,入力部の信号ピックアッププローブと信号データファイルについても,その後ろの工程からみたらその差について気にしないですむようにしたい。


信号ピックアッププローブと信号データファイルの切り替えのイメージはこのような感じとなる。

製品では A/D変換器から取り込んだ信号でデータにデジタルフィルタをかけ,結果を表示する。

これをリアルシステムとすれば,バーチャルシステム(仮想システム)では,信号データはファイルたか取り込んで,デジタルフィルタをかけ,その結果を開発マシンのPCのディスプレイ上に表示したい。

「バーチャルシステムでコア資産の性能を検証し,商品として使えそうであること(有効性)が確認でき次第,コア資産のソフトウェアに手を加えることなく,リアルシステムで動かす」これが,今回のシステムの設計思想となる。

ちなみに,コア資産は変わりにくいとはいえ,一度作ったら終わりということにはならない。競合他社と競争しているわけだから,日々改善,性能向上をしていく必要もある。そのき,改善したコア資産の中身だけを入れ替えて,コア資産の入力と出力のI/Fを代えなくて済めば,安全に性能向上のバージョンアップができる。

コア資産の改善と,入出力の仕様の変化の対応を同じレベルで考えてはいけない,コア資産の改善は,その商品や商品群の屋台骨を強くするための取り組みであり,入出力の仕様の変化に対応するのは,ユーザインタフェースの改善が目的である。後者は商品の本質的な価値に関係し,後者は商品の(表面的な)魅力に関係する。どちらも,商品が売れるためには必要だが,コア資産は他社にまマネできない競争力の源泉であり,後者は他社が真似しようと思えば真似できる。だから,コア資産は機能や性能を確認し,信頼性が十分に高い状態を維持し,ユーザインタフェースの部分は時代に合わせて素早く変化できるよにしておくことが重要となる。

コア資産の性能向上を検討したときに,デグレード(今までできていたことができなくなったり,性能が落ちたりすること)してはまずいので,これまでできていたことの検証もできるようにしておきたい。

そのとき,実機でさまざまな入力信号を再生して,結果を確認するのは多大な工数を必要とする。だから,この検証部分は バーチャルシステムで網羅性の高い確認を行い,最後にリアルシステムでサンプル検証する方法を採りたい。

これらができると飛躍的に開発効率が高まる。
  • 新しい性能(今回はフィルタの特性)の確認が実機なしで確認できる。
  • さまざまな入力データを新しいコア資産に通した結果をシミュレーション環境で確認できる。
  • ソフトウェアをいじることなく,かつ,安全に新しいコア資産をリアルシステムに実装できる。
  • フィールドで問題が起きたとき(例えば,結果が期待通りにならないケース)入力データを記録できれば,そのデータを繰り返しシミュレーション環境で再現できる。

これを図で示すとこんな感じとなる。

仮想システムでコア資産となるデジタルフィルタにサンプルデータを流し込み,開発環境のPCのディスプレイに表示する。

このとき,デバッグ用に内部データをPCに表示することも可能。

そして,さまざまな入力データで網羅性の高い検証を行ったのち,リアルシステムにスイッチして実機環境にて,シミュレーション環境と結果が同じになることを確認して実装を完了する。

今回のシステムでやりたいことは,こういったことだ。

ここまでの説明でお分かりかと思うが,ソフトウェアのシステムアーキテクチャの設計にあたって,対象の商品や商品群,提供するサービス,顧客の要望,開発効率を妨げていることなどの情報が必要となる。

そして,商品や商品群のコア資産が何か,そのコア資産を再利用して効率よくまた,安全に派生開発できるようにするには,どんなアーキテクチャが最適かを考える。

ソフトウェア開発を単発で考えているとこの発想にはならない。製品のソフトウェアを外部の協力会社に丸投げしているようなプロジェクトでは,およそこういった発想にはならないだろう。商品開発とソフトウェアの再利用,開発の効率化,安全性信頼性の向上を頭にいれたアーキテクチャ設計が必要になる。

5年とか10年,また,複数の商品,商品群で共通に利用するコア資産をベースにした再利用開発を想定したアーキテクチャ設計を行う,これがソフトウェアアーキテクトの腕の見せどころとなる。

このようなシステムを実現するためのソフトウェアアーキテクチャは具体的にどう設計するのかを次回以降解説する。

今回の記事には書かなかったが『Raspberry Pi3を使ったリアル波形描画(6) Qtでマルチスレッド処理をやってみる』に書いたように,マルチスレッドによる正確なタイムインターバルの実現と,タイマーイベントとデジタルフィルタのモジュールはパッシブに(『ソフトウェアアーキテクチャとは何かを考える』で解説したデータ結合のような疎結合状態)つながるようにしないといけない。

また,デバッグ用の画面も含めたユーザインタフェースはゼロから作るのは効率が悪い。今の時代,さまざまなお助けツールがある。

今回の特集記事で Qt を使った事例を書いているのは,ここまで説明してきたアーキテクチャの基本設計の方針を Qt を使うと上手に実現できそうだということが分かったからだ。(特にユーザインタフェースの部分は相当お助けになる)

波形描画やボタンなどのユーザインタフェースは自分で作ると結構時間がかかる。しかも,UIは時代とともにどんどん変わっていくし,ユーザもリッチなUIに慣れてきているので,プアーなUIだと中身の性能が良くても,商品として評価が低くなるかもしれない。

だから,Qt を使うことでユーザインタフェースは最大限作業を省くことができたし,デバッグ用の内部のデータ表示なども,サクッと作ることができた。

そして,PC上で表示した画面をソースコードを変えることなく,そのまま,Raspberry Pi3 上で使えるのも魅力的だった。

さらに,4ms の正確なタイムインターバルを発生させて,Qt のSIGNAL/SLOTのしくみをつかって,他のコア資産にイベントを伝達できることも分かった。

ソフトウェアアーキテクチャの設計思想がよくても,実現の方法や条件が満たされていないと上手くいかない。さまざまな制約条件をクリアしつつ,目的のアーキテクチャを設計思想通りに実現する,これがソフトウェアアーキテクトの腕の見せどころなのだ。

制約があればあるほど,燃えるのがプロの職人であり,そのためには,数多くの経験とソフトウェア設計に関する多くの引き出しがないといけない。いろいろな制約条件がある中で,設計思想にぴったりくるアーキテクチャが見つかったときに「いける!」と感じる瞬間がある。

それは,開発の相当初期の段階で,実現可能性の検討段階(フィージビリティ・スタディ)のときでないとまずい。作り始めてしまったあとに,最初の設計思想が実現できないことがわかり,妥協していくと,グダグダになってプロの仕事でなくなってしまう。

制約条件をクリアしながら,価値の高い商品を効率良く生み出していくアーキテクチャ,これを実現するのがプロのソフトウェアアーキテクトだと思う。

今回,ソフトウェア自体(ソースコードやクラス図など)に一切触れなかったのは,アーキテクチャを決定するのに,いかに設計思想が重要かということを理解してもらいたかったからだ。

プログラマーとしてプロならば,デザインパターンなどの定石や手筋が習得できているだけでいいのかもしれないが,ソフトウェアアーキテクトしてプロの仕事をするのならば,効率良く儲かる,ユーザにも高い満足を与えることができる設計が必要だ。

リアル建築に例えれば,一級建築士の資格をもって,個人事務所を持ち,限られた予算でさまざまな要求を持つ顧客を満足させる家を実現するために最適な図面を書き,設計思想どおりに家ができるようにコントロールする役割が求められる。

同じ要求のユーザはほとんどいない。業務ドメインによって,求められる構造も,制約条件も変わる。今回紹介しているリアル波形描画のシステムにしても,ドメインに依存しており,処理の時間制約が厳しいという条件に特化していると言える。

だから,このアーキテクチャはどの業務ドメインにも使えるというわけではない。しかし,だからこそ,アーキテクトは幅広い見識と経験,多くの引き出しが必要だ。銀の弾丸はなく,どのドメインにも最適な解はない。

なお,『リアルタイムOSから出発して 組込みソフトエンジニアを極める[改装版]』は,全編 仮想の電子レジスター 商品群をテーマに,今回書いているようなことを解説している。

今回の特集記事は,この本で書いたことを,実機で実現できると証明するために書いた。机上の空論と言われたくなかったからだ。

次回からは,実際にどのような構造にすると実現できるのかを説明していく。

大規模・複雑化している組込みソフトウェアにC言語だけで取り組んでいるエンジニアに,オブジェクト指向設計やC++などのオブジェクト指向言語を使うと,開発がこんなに楽になるよと伝えたいというのも,この特集記事を書いている理由だ。

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

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