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

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

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程度)ことが分かる。

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