2017年11月13日月曜日

ListView Extensions ver.1.0.0-beta4

beta2に引き続いてbeta4をリリースしました。
えっ?beta3はどこに行ったって?beta2リリース直後にバグを見つけてこっそり差し替えていました。テヘペロッ(๑´ڡ`๑)

ListView Extensions - Nuget

バグフィックスのbeta2,3に比べたら今回はかなり大きなアプデです。バージョン変えようかと思いましたが、1.0.0-beta*という名前である以上、1.0.0をリリースしない限りバージョン変えられないなと思い、結局このままになっています…。
これで安定していたら今度こそ正式リリースするぞ!

今回の変更点

さて、今回のアプデ内容はただ一つにして大きい内容です。コレクションのスレッドセーフ化です。
シングルスレッドが許されるのは小学生までだよねー。

名前は「SyncedSortableObsevableCollection」にしようかと思いましたが、いまどきスレッドセーフじゃないCollectionなんてWPFで使う上では用途が無いと判断し、名前は「SortableObservableCollection」のままとしました。インターフェースは互換性を維持していますので、そのままコンパイルは通るはずです。
一応今までの実装は残していて、同じ名前空間にUnsyncedSortableObservableCollectionとしてObsolate属性を付けています。問題がある方はこっちを使ってください。

なお、スレッドセーフ化する過程で継承構造をより素直な形にしました。以前のバージョンは「SortableCollection」というクラスを継承していましたが、こちらもObsolateになっており、基底クラスは「SyncedObservableCollection」にしております。

SyncedObservableCollection

これは私がフルで実装したクラスで、スレッドセーフな変更通知可能なコレクションです。ReaderWriterLockSlimを使って効率的なロックを行っております。要素技術としては詳しくは前回の記事を見て下さい。

なお、このクラスは非ジェネリックのICollectionインターフェースを継承しているため、IsSynchronizedプロパティとSyncRootプロパティを実装しています。コレクションを外部から操作するときに複数回のメソッド呼び出しにわたってlockが必要な場合はこのSyncRootを使ってlockを行ってください。

例えば次のようなケースです。

SortableObservableCollection<Person> People = new SortableObservableCollection<Person>();

//中略

lock(People.SyncRoot)
    People.RemoveAt(People.Count - 1);    //Remove last element

最後の要素を削除する処理ですが、これは
  1. コレクションの長さを取得
  2. コレクションの長さ-1の要素を削除
の2回のコレクションへのアクセスを行っています。
この1.と2.の間に他のスレッドからの書き込みアクセスがあって要素数が増減した場合、「コレクションの長さ-1」が最後の要素のインデックスではなくなってしまいます。これは問題です。

そこで、対策としてSyncRootを使ったlockを行っております。これによって他のスレッドからの書き込みアクセスを阻止しています。
これこそ「ReaderWriterLockSlimを使うべきじゃないの?」と思うかもしれませんが、このようなケースは書き込みアクセスを伴うことが圧倒的に多く、書き込みアクセスをする場合はReaderWriterLockもlock構文も大差がありません。ですので、既存の枠組みで簡単に実現できるこの方法を使うことにしました。
もちろん、コレクションの外でlock構文を使って書き込みをしている間も、中でReaderWriterLockSlimを併用していますので、書き込みのタイミング以外(上記で言えばPeople.Count読み取り時など)は他のスレッドからの読み込みアクセスは許可されます。

ちなみに、本家のObservableCollectionはAddRange/InsertRange/RemoveRangeといった複数の項目を一気に操作する機能は付いていません。しかし、INotifyCollectionChangedインターフェースの機能としてはNotifyCollectionChangedEventArgsクラスを見てもわかりますが複数項目のコレクション操作機能にも対応しています。
なので、このSyncedObservableCollectionではそれらの機能にも対応しています。ループでAddメソッドを複数回呼び出すとその呼び出しごとにイベントが発生しますが、AddRangeだと1回で済むので効率が格段と高まります。

ListViewViewModel

ListViewViewModelは今までもModelをUI以外のスレッドから操作した場合に発生した変更通知をUIのDispatcher経由でViewに反映させる機能を持っていました。ただし、肝心のスレッドセーフ設計は(面倒なので)やっていませんでした。

今回は、このSyncedObservableCollectionを活用してスレッドセーフに設計しています。今までの実装はSortableObservableCollectionと同じくUnsyncedListViewViewModeに移したうえでObsolate属性を付けています。

デッドロックに注意

ここで、ListViewViewModelを使う上での注意を紹介します(私もハマってしばらく原因探しに苦戦していました)。

ListViewViewModelはViewに設置されたボタンなどの入力を受け付けてModelに渡すためのコマンドをいくつか持っています。
例えばRemoveCommandはListViewのうち選択されている項目を削除するコマンドです。Viewのボタン押下を受けてCommandが発火し、Model(SortableObservableCollection)のRemoveメソッドを呼び出します。

気を付けないとここでデッドロックが発生しうるのです。




UIスレッドからのSortableObservableCollectionへのアクセスの直前にその他のスレッドから同じくSortableObservableCollectionに書き込みアクセスがあったとします。
そうすると、先に書き込みアクセスを始めたほうがLockを保持するのでUIスレッドはそのLockが解放されるまで待機します。

しかし、その他のスレッドは変更後に変更通知イベントを発生させ、UIのスレッドでUIの更新作業をしようとします。このときは、すでにUIスレッドはSortableObservableCollectionのLockが解放されるのを待っているのでUIスレッドでのInvokeが行えずデッドロックが発生してしまいます。

AとBの排他制御機構があって、AをLockしたうえでBをLockしようとするスレッドと、BをLockしたうえでAをLockしようとするスレッドがあるとデッドロックが発生する、というのはマルチスレッドプログラミングの基本中の基本です。
今回はUIスレッドとSortableObservableCollectionがそれぞれの排他制御機構となっています。


さてさて、これに対抗する手段もListViewViewModelには追加しております。


UIスレッドが直接SortableObservableCollectionへアクセスしなければよいのです。
UIスレッドからSortableObservableCollectionへのアクセスを行いたいときは別のスレッドに書き込みアクセスを依頼したうえですぐに処理をUIスレッドに戻すようにします。
そうすることで、先にその他のスレッドがSortableObservableCollectionのLockを保持していたとしてもUIスレッドはアイドル状態になっており、その他のスレッドからのUI更新作業もブロックされることはありません。この機構によってデッドロックが回避されるわけです。

ただ、重い作業を並列実行するわけでもないのに別スレッドを起動するので単純に処理が重い上、UI起点の処理をやっているのにその処理が終わる前にUIがいじれるようになるので不都合が起きることもあります。

そこで、これはオプションとし、デフォルトではOFFにしています。
別スレッドからの書き込みとUIスレッドからの書き込みが衝突する可能性がある場合は、ListViewViewModelにてUseNonUIThreadWhenCallingModelプロパティをtrueにしてください。
また、DisableCommandWhenCommandTaskRunningを使うことによってCommand起点のタスクが別スレッドで実行されているときにCommandを無効化することができます。Command起点のタスクが実行中かどうかはCommandTaskRunningプロパティでわかります。


なお、他の注意点として、変更通知イベントを受けたUI更新作業のときに、UIスレッドから直接SortableObservableCollectionに書き込みアクセスをしようとするとやはりデッドロックが起こりますので注意してください。この場合は確実に起きます。
ただ、UI更新作業中にSortableObservableCollectionを書き込むなんておかしな話です。なんでUIの更新作業の名目でデータの変更作業までしてんねんって話です。そんなデッドロックを起こすようなコードを書く人のことは知りません。

ListViewViewModelは同一インスタンスの要素を複数持ってはいけない

これは実は前からある制約です。
ですが、自分自身ですら忘れていたので書いておきます。

SortableObservableCollectionは問題ないのですが、ListViewViewModelは同一インスタンスの要素を複数持てません。
もしも同一インスタンスを追加しようとすると例外(ArgumentException)が発生します。ListViewViewModelのTViewModelが構造体やプリミティブ型の場合は、同値だった場合に例外が発生してしまいます。

なぜこんな仕様になっているかというと、ひとえにSelectedItemsのせいです。

WPFのListViewは複数のアイテムを選択している場合はSelectedItemsでしかその選択されたアイテムをすべて取得することができません。インデックスではなく選択されているアイテムのViewModelインスタンスのコレクションとなります。
この際、複数の同じインスタンスがあった場合、どちらのアイテムが選択されているのか求めることができません。ですので、このような不便な制約があるわけです。


しかし、MVVMモデルをなぞって実装しているのならば、ListViewの各行のアイテムとしてバインディングするのは、通常はSortableObservableCollectionの要素の型に対応するViewModelになるはずです。 なので、SortableObservableCollectionが複数の同一インスタンスを持っていたとしてもViewModelの要素としては別個のインスタンスを作ればいいだけなのです。

これはどのように実現するかというと、ListViewViewModelのコンストラクタで指定します。

public ListViewViewModel(ISortableObservableCollection<TModel> source, Func<TModel, TViewModel> converter, Dispatcher dispatcher);
public ListViewViewModel(ISortableObservableCollection<TModel> source, Func<TModel, TViewModel> converter, Dispatcher dispatcher, DispatcherPriority priority);

ListViewViewModelは2つのコンストラクタを持っています。ですが、Dispatcherの優先度を指定するかデフォルト値(Normal)を採用するかの違いだけで、それ以外は同じです。

sourceはModelのコレクションとなります。SortableObservableCollection<T>を指定する場合が圧倒的に多いでしょう。

問題は2番目の引数です。
このconverterがSortableObservableCollectionで要素が追加されたときに、ViewModelの要素を追加するために呼ばれるデリゲートになります。
ModelをViewModelに変換(ラッピング)するわけですから、ここで

model => new ElementViewModel(model)

のようなラムダ式を渡してやれば十分でしょう。
これでどのようなインスタンスが追加されたときも全自動で新しいインスタンスが生成されていくのでListViewViewModelに同一インスタンスが複数追加されることはありません。

めんどくさがって

model => model

にした場合は例外が飛ぶことがあるので注意してください。


ちなみに、要素がRemoveされたり、別の要素で書き換えられたときは、ElementViewModelがIDisposableを継承していればちゃんとDispose()を呼びますのでご安心を。



さーて、次回こそ正式リリースをするぞ!!!

2017年11月5日日曜日

C#の排他制御 - UpgradeableReadLock

.NET4.0/C#5.0以降、どんどん非同期処理に関する機能が増強されてきています。
特にasync/await構文なんかは、今までは非同期で書こうとすると非常に読みにくいコードになっていたのを、上から下へ流れるようにプログラムを書くだけで非同期処理が実現できてしまうという非常に画期的なものでした。

しかし、そこには非同期処理特有の問題が付きまといます。いくら簡単に非同期処理が書けるからと言って、何も知らずに非同期処理を確実に動かすことはできないのです。

さて、非同期処理で注意しなければならないことの1つとして、「データの整合性」が挙げられます。
非同期処理は複数のスレッドが同時に走るわけですから、それぞれのスレッドから1つのデータにアクセスした場合どのような結果になるか、ということを常に想像しながらコードを書かなければなりません。
そして、必要ならば排他制御を行い、片方のスレッドからデータにアクセスしている場合はもう片方のスレッドがデータにアクセスするのを待ってもらう必要があります。

はじめに - lock構文

C#にはそれに関して便利な構文が用意されています。
ご存知、lock構文です。
Stopwatch sw = new Stopwatch();
sw.Start();

Parallel.ForEach(Enumerable.Range(0, 4), i => {
    Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Start");
    Thread.Sleep(TimeSpan.FromSeconds(1));
    Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Finished");
});

sw.Stop();
まず、こちらがlock構文を使わない場合です。各スレッドで1秒待ってもらっていますが、すべてが並列で実行されるので下記のような実行結果になります(実行結果は環境によって異なります。以下同じ)。

 42ms, 0: Start
 44ms, 2: Start
 44ms, 1: Start
 44ms, 3: Start
1068ms, 0: Finished
1068ms, 1: Finished
1068ms, 2: Finished
1068ms, 3: Finished

基本ですね。

続いてlock構文を入れて、Sleepに入れるのを1つのスレッドのみに絞ってみます。
Stopwatch sw = new Stopwatch();
object lockobj = new object();

sw.Start();

Parallel.ForEach(Enumerable.Range(0, 4), i => {
    lock(lockobj) {
        Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Start");
        Thread.Sleep(TimeSpan.FromSeconds(1));
        Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Finished");
    }
});

sw.Stop();
 41ms, 0: Start
1068ms, 0: Finished
1068ms, 1: Start
2070ms, 1: Finished
2070ms, 2: Start
3073ms, 2: Finished
3073ms, 3: Start
4078ms, 3: Finished

今度は1つのスレッドが確実に終わってから次のスレッドへ移るような実行結果になりました。

今回はlock構文の動作を確かめるための簡単なプログラムですが、通常はデータの読み書きなど複数のスレッドから同時に行われたら都合が悪い部分をlockで囲み排他制御を行うのに使います。

ReadLockとWriteLock

さて、ここまで来て勘のいいひとなら気づいたでしょう。
何でもかんでもデータアクセスにlockをかけるのは決して効率の良いことではないことだと。

データを読み込むだけなら複数のスレッドから同時に行っても問題ないはずです。
ですが、データを読み込んでいる途中に他のスレッドから書き込みが行われたり、データを書き込むときに他のスレッドから書き込みや読み込みを行われては困ります。
//データを書き込みしている途中でも書き込み前後のどちらかの値が取得できさえすればいいなら読み込みを許可してもいいのでは?と思うかもしれませんが、場合によっては書き込み中には書き込み前後のどちらの値でもない値になりうるのです。

lock構文は有無も言わせず排他制御をする構文です。たとえ複数のスレッドがすべて読み込みだけをしたくても同時に行うことを許しません。


.NETにはそれをサポートするためのクラスがあります。

ReaderWriterLockSlim
//名前がいびつですが、実はReaderWriterLockというクラスもあります。このクラスを整理してもっとシンプルにしたものということで、このような名前になっているようです。

機能は上記の通りで、読み取りロックが取得されているときは他のスレッドからも読み取りロックを取得することができますが書き込みロックは取得できません。書き込みロック取得中は読み取り/書き込みロック両方とも取得できません。
ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
Stopwatch sw = new Stopwatch();
sw.Start();

Parallel.ForEach(Enumerable.Range(0, 12), i => {
    if(i % 2 == 0) {
        try {
            rwlock.EnterReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterReadLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitReadLock");
            rwlock.ExitReadLock();
        }
    } else if(i % 2 == 1) {
        try {
            rwlock.EnterWriteLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterWriteLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitWriteLock");
            rwlock.ExitWriteLock();
        }
    }

});

sw.Stop();
一気にインデントが多くなって見にくくなってしまいました。Lockは取得したら確実に解放しなければならないので、try-finally構文を使っているためです。
//lock構文も内部的にはMonitorクラスを使ったtry-finally文に展開されます。

これを実行すると次のような実行結果になります。

 70ms, 0: EnterReadLock
 71ms, 6: EnterReadLock
 71ms, 4: EnterReadLock
 71ms, 2: EnterReadLock
 95ms, 8: EnterReadLock
1071ms, 0: ExitReadLock
1095ms, 6: ExitReadLock
1095ms, 4: ExitReadLock
1096ms, 8: ExitReadLock
1096ms, 2: ExitReadLock
1096ms, 11: EnterWriteLock
2096ms, 11: ExitWriteLock
2097ms, 7: EnterWriteLock
3098ms, 7: ExitWriteLock
3098ms, 1: EnterWriteLock
4099ms, 1: ExitWriteLock
4099ms, 5: EnterWriteLock
5099ms, 5: ExitWriteLock
5099ms, 3: EnterWriteLock
6100ms, 3: ExitWriteLock
6100ms, 9: EnterWriteLock
7101ms, 9: ExitWriteLock
7101ms, 10: EnterReadLock
8102ms, 10: ExitReadLock


最初は5つのスレッドが同時にReadLockに入っていますが、WriteLockはおそらくブロックされていることがわかります。そして、ReadLockがすべて解放されると待っていたWriteLockに入っています。WriteLockは1つずつ入っており、10番のReadLockもブロックされていることが分かります。

lock構文のような便利な構文が無いのでtry-finallyの冗長さが目立ってしまう書き方ですが、単純なlockより効率的なデータアクセスが見込めるでしょう。

UpgradeableReadLock

さて、話がどんどん大きくなっていきます。

例えばスレッドセーフなDictionaryを設計しているとき、新たな値を渡されたらどういう処理になるでしょうか。
  1. すでにキーが存在するか確かめる
  2. キーが存在しなければキーと値を追加する
  3. キーが存在すればそのキーの値が同じか確かめる
  4. 値が違っていたら値を書き換える
という流れになるかと思います。
ここで、青色で書いたのは読み取りで、赤色で書いたのは書き込みです。
じゃあこれを実装するときは、青の部分のコードはReadLockを掛けて、赤の部分のときはWriteLockを掛ければいいのかと言えば、答えはNoです。

ReadLockをExitしてからWriteLockを取得するまでの間に別のスレッドがWriteLockを取得して値を書き換えたらどうなるでしょう。1.をやって例えばNoと判定したのに、その直後に別スレッドがそのキーを追加していたら2.を実行するときに意図していない状態になってしまいます。
かといって、ReadLockを取得したままWriteLockを取ろうとするとデッドロックを起こします。他のスレッドもReadLock取得中にWriteLockへ昇格しようとしていた場合、互いに他のスレッドのReadLockが解放されるのを待ってしまい、どちらのスレッドもそこから先へ進めなくなってしまうのです。

じゃあどうするか、一つの答えは全体をWriteLockにすれば良いでしょう。ですが、1.や3.を行っている間は別スレッドが読み取り操作をするだけなら許されるべきです。全体をWriteLockにしてしまうとそれすら許されなくなってしまいます。lock構文は条件が厳しすぎて効率が悪いからReadLockとWriteLockに分けようという話になったのに、結局それが生かせなくなってしまいます。


そこで登場するのがUpgradeableReadLockです。
「WriteLockに昇格予定があるReadLock」は唯一にすることで、昇格時にデッドロックを起こすのを防ぎます。逆に言えば、ただのReadLockは昇格予定の無いReadLockということになります。

具体的には、下表のような動きをします。
現在のLock\取得しようとしているLockReadLockUpgradeableReadLockWriteLock
なし○○○
ReadLock○○×
UpgradeableReadLock○××
WriteLock×××

○がブロックされない(取得できる)Lock、×がブロックされるLockということになります。

実際に動作を試してみましょう。
ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
Stopwatch sw = new Stopwatch();
sw.Start();

Parallel.ForEach(Enumerable.Range(0, 12), i => {
    if(i % 3 == 0) {
        try {
            rwlock.EnterUpgradeableReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterUpgradeableReadLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitUpgradeableReadLock");
            rwlock.ExitUpgradeableReadLock();
        }
    } else if(i % 3 == 1) {
        try {
            rwlock.EnterReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterReadLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitReadLock");
            rwlock.ExitReadLock();
        }
    } else if(i % 3 == 2) {
        try {
            rwlock.EnterWriteLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterWriteLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitWriteLock");
            rwlock.ExitWriteLock();
        }
    }

});

sw.Stop();
 59ms, 0: EnterUpgradeableReadLock
 59ms, 7: EnterReadLock
 59ms, 1: EnterReadLock
 59ms, 4: EnterReadLock
1059ms, 0: ExitUpgradeableReadLock
1060ms, 4: ExitReadLock
1060ms, 1: ExitReadLock
1060ms, 7: ExitReadLock
1061ms, 8: EnterWriteLock
2062ms, 8: ExitWriteLock
2063ms, 11: EnterWriteLock
3064ms, 11: ExitWriteLock
3064ms, 5: EnterWriteLock
4065ms, 5: ExitWriteLock
4065ms, 2: EnterWriteLock
5066ms, 2: ExitWriteLock
5066ms, 10: EnterReadLock
5066ms, 6: EnterUpgradeableReadLock
6068ms, 10: ExitReadLock
6069ms, 6: ExitUpgradeableReadLock
6069ms, 3: EnterUpgradeableReadLock
7069ms, 3: ExitUpgradeableReadLock
7069ms, 9: EnterUpgradeableReadLock
8070ms, 9: ExitUpgradeableReadLock

UpgradeableReadLock中はReadLockは許されていますが、WriteLockや他のUpgradeableReadLockはブロックされているであろうことがわかります。


ちなみに、UpgradeableReadLock中にWriteLockに昇格するには単にEnterWriteLock()~ExitWriteLock()の一連の構文を入れればいいだけです。
ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
Stopwatch sw = new Stopwatch();
sw.Start();

Parallel.ForEach(Enumerable.Range(0, 20), i => {
    if(i % 10 == 0) {
        try {
            rwlock.EnterUpgradeableReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterUpgradeableReadLock");
            //Thread.Sleep(TimeSpan.FromSeconds(1));
            try {
                rwlock.EnterWriteLock();
                Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Upgraded");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
            finally {
                Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Downgraded");
                rwlock.ExitWriteLock();
            }
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitUpgradeableReadLock");
            rwlock.ExitUpgradeableReadLock();
        }
    } else {
        try {
            rwlock.EnterReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterReadLock");
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitReadLock");
            rwlock.ExitReadLock();
        }
    }
});

sw.Stop();
 49ms, 3: EnterReadLock
 49ms, 1: EnterReadLock
 49ms, 2: EnterReadLock
 49ms, 4: EnterReadLock
 49ms, 0: EnterUpgradeableReadLock
 83ms, 5: EnterReadLock
 86ms, 6: EnterReadLock
 86ms, 7: EnterReadLock
 88ms, 8: EnterReadLock
1050ms, 3: ExitReadLock
1084ms, 1: ExitReadLock
1084ms, 2: ExitReadLock
1085ms, 4: ExitReadLock
1085ms, 5: ExitReadLock
1087ms, 7: ExitReadLock
1087ms, 6: ExitReadLock
1088ms, 8: ExitReadLock
1088ms, 0: Upgraded
2089ms, 0: Downgraded
2089ms, 17: EnterReadLock
2089ms, 15: EnterReadLock
2089ms, 13: EnterReadLock
2089ms, 12: EnterReadLock
2089ms, 14: EnterReadLock
2089ms, 18: EnterReadLock
2089ms, 11: EnterReadLock
2089ms, 16: EnterReadLock
2089ms, 9: EnterReadLock
3037ms, 19: EnterReadLock
3089ms, 0: ExitUpgradeableReadLock
3089ms, 10: EnterUpgradeableReadLock
3092ms, 15: ExitReadLock
3092ms, 17: ExitReadLock
3093ms, 12: ExitReadLock
3093ms, 14: ExitReadLock
3094ms, 11: ExitReadLock
3093ms, 13: ExitReadLock
3095ms, 16: ExitReadLock
3095ms, 9: ExitReadLock
3093ms, 18: ExitReadLock
4037ms, 19: ExitReadLock
4037ms, 10: Upgraded
5038ms, 10: Downgraded
6038ms, 10: ExitUpgradeableReadLock

UpgradeableReadLockからWriteLockに昇格している間は他スレッドがReadLockを取得するのをブロックされていますが、WriteLock終了後(降格後)はUpgradableReadLockをExitしていなくても再びReadLockが取れるようになっていることがわかります。


この節の最初で「UpgradeableReadLockはスレッドセーフなDictionaryを作るのに使える」と言いましたが、多分Dictionary程度だとこの程度の効率化はたかが知れています。
しかし、MVVMなどのフレームワークにおいては大きく効果が出ます。値の変更後に逐一イベントが飛んで、イベントでいろいろな更新作業を行います。 その更新作業中ずっとWriteLockにして他のReadアクセスをブロックしていたら効率がかなり落ちてしまいます。このUpgradeableReadLockそのような場面で威力を発揮する排他制御手法となるでしょう。

2017年10月24日火曜日

ListView Extensions ver.1.0.0-beta2

最近久しぶりにWPFを触ったのですが、その中でListViewを使う機会がありました。

身近なListViewと言えばWindowsのエクスプローラーですが、例えばこれはヘッダーをクリックすることで思い思いの要素でファイルのソートができるのに対して、WPFのListViewにはそのような基本機能が備えられていません。
とは言っても、備えるのもかなりの根気がいる作業ですし、そもそも元のコレクションに介入する作業はPresentation Foundationと名の付くフレームワークがやるべきことではない気がします。

そこで、ListView Extensionsです。

MVVMスタイルの形をしながら、ListViewでの基本機能が多数取り揃えられているライブラリです。
例えば、ListViewのソート、選択項目に対する移動、削除などです。
このような機能を実現するにあたって、View⇔ViewModel⇔Modelのすべての領域にわたって素晴らしいクラスが提供されています。

完全に自画自賛です。


ところで、バグがありました。

ListViewViewModelでClear()を呼び出したとき、より正確にはRemoveなどで最後の1項目を削除したとき、InvalidOperationExceptionが発生することがあるバグがありました。

MoveUpSelectedItemCommand/MoveDownSelectedItemCommandの実装にて、このコマンドが有効になる条件として「選択されているアイテムの個数が1以上、かつリストの先頭/最後尾のアイテムが選択されていない」というロジックを組んでいました。
しかし、 最後の1つの項目を削除したときにSelectedItemsの反映がリストのコレクションの反映より遅くなることがあるようで、その場合、「選択されているアイテムが1個あるがリストのコレクションのアイテムは0個」という状況が起こってしまうようです。その時に、1つ目の条件をすり抜けて2つ目の条件の判定に入った際、リストの先頭または最後尾のアイテムを取得しようとしてInvalidOperationExceptionがスローされているようでした。

このバグは、「リストのコレクションの数が1個以上」という条件を追加することによって回避しました。


あと、自分で作っておいて少しはまったところですが、このライブラリ、UI以外のスレッドからアイテムの操作をしてもUIに正常に反映される仕組み(Dispatcher経由でのアクセス)に対応しているにもかかわらず、スレッドセーフに作られていません。
気が向いたらSorableSynchronizedObservableCollectionなどを作るかもしれませんが、それまでは皆さん自分でlockなどをして使ってくださいね…。

とりあえず、例の最後の1個を消したら例外を吐くバグを直したものをbeta2としてうpしておきました。
ListViewExtensions 1.0.0-beta2
細かな使い方はbeta1をリリースしたときの記事を確認してください。

いつになったらプレリリース外そうかな…。 

2017年8月19日土曜日

BME280をPIC32MX250F128B+Harmonyで使う

前回に引き続き、I2CセンサをPIC32で扱うお話です。

BME280はBOSCH製の温湿度・気圧センサです。
個人的にはBOSCHと言えばドイツの自動車部品メーカーというイメージですが、こういった半導体部品も作ってるんですね。驚きです。

さてさて、こいつもI2C/SPI両対応のチップで、分解能が温度0.01℃、湿度0.008%、気圧0.18Paとかなり細かくデータを取ることができます。データシートもドイツメーカーだからか英語が非常にシンプルで読みやすく、内容もコンパクトにまとめられています。

このセンサの使用方法としては
  1. 設定の書き込み
  2. Trimming Parameterの読み込み
  3. (生の)測定データの読み込み
  4. 測定データの演算
の流れとなっています。
Trimming ParameterはCalibration Dataとも書かれており、生の測定値を℃、%、hPaに変換するにあたって使われる係数です。
センサによって個体差があるので、メーカー出荷時にその個体差に合わせて係数を設定しておき、ユーザーは生の測定データをTrimming Parameterを使って演算することで正確な温湿度や気圧を得ることができます。

順に追って見ていきましょう。

void I2CEventHandler(DRV_I2C_BUFFER_EVENT event, DRV_I2C_BUFFER_HANDLE bufferHandle, uintptr_t context)
{
    /*
    switch (event)
    {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            break;
        case DRV_I2C_BUFFER_EVENT_ERROR:
            break;
        default:
            break;
    }
    */
}

_Bool I2C_InitAndOpen(I2C_HANDLE *pobj, SYS_MODULE_INDEX index)
{
    pobj->hDriver = DRV_I2C_Open(index, DRV_IO_INTENT_READWRITE);
    pobj->hBufferDriver = NULL;
    
    if(pobj->hDriver == (DRV_HANDLE)NULL)
        return false;
    
    DRV_I2C_BufferEventHandlerSet(pobj->hDriver, I2CEventHandler, (uintptr_t)pobj);

    return true;
}

DRV_I2C_BUFFER_EVENT I2C_GetTransferStatus(I2C_HANDLE *pobj)
{
    return DRV_I2C_TransferStatusGet(pobj->hDriver, pobj->hBufferDriver);
}

_Bool I2C_IsCommunicating(I2C_HANDLE *pobj)
{
    if(pobj->hBufferDriver == NULL)
        return false;
    else {
        DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj);        
        return (event != DRV_I2C_BUFFER_EVENT_COMPLETE) && (event != DRV_I2C_BUFFER_EVENT_ERROR);
    }
}

uint32_t I2C_GetTransferredBytes(I2C_HANDLE *pobj)
{
    return DRV_I2C_BytesTransferred(pobj->hDriver, pobj->hBufferDriver);
}

まずはI2C周りのコードです。I2CバスをBME280とMPU-9250で共有しているので、共通するI2C関係のプログラムを分離しています。

#define BME280_SENDDATA_COUNT_MAX  4

typedef struct {
    I2C_HANDLE *pHandle;
    uint8_t WriteBuffer[BME280_SENDDATA_COUNT_MAX * 2];
    uint8_t SlaveAddress;
    
    uint16_t dig_T1;
    int16_t dig_T2;
    int16_t dig_T3;
    uint16_t dig_P1;
    int16_t dig_P2;
    int16_t dig_P3;
    int16_t dig_P4;
    int16_t dig_P5;
    int16_t dig_P6;
    int16_t dig_P7;
    int16_t dig_P8;
    int16_t dig_P9;
    uint8_t dig_H1;    
    int16_t dig_H2;    
    uint8_t dig_H3;    
    int16_t dig_H4;    
    int16_t dig_H5;    
    int8_t dig_H6;    
} BME280;

_Bool BME280_Init(BME280 *pobj, I2C_HANDLE *pHandle, uint8_t SlaveAddr)
{
    pobj->SlaveAddress = SlaveAddr;
    pobj->pHandle = pHandle;

    return true;
}

そのためBME280の初期化関数はアドレスの登録だけになっています。
ちなみにBME280のデータを保持する構造体にはdig_**みたいな変数をたくさん用意してありますがこれらがTrimming Parametersです。配列にしたほうがカッコイイ気もしますが、データシート等に倣ってこう書いておくことにします。

_Bool BME280_StartWritingResistor(BME280 *pobj, uint8_t address, uint8_t data)
{
    BME280_DATA_ADDRESS_PAIR pair;
    
    pair.Address = address;
    pair.Data = data;
    
    return BME280_StartBurstWritingResistor(pobj, &pair, 1);
}

_Bool BME280_StartBurstWritingResistor(BME280 *pobj, BME280_DATA_ADDRESS_PAIR *pairs, int count)
{
    int i;
    
    if(count <= 0 || count > BME280_SENDDATA_COUNT_MAX || I2C_IsCommunicating(pobj->pHandle))
        return false;
    
    for(i = 0; i < count; i++) {
        pobj->WriteBuffer[i * 2 + 0] = pairs[i].Address;
        pobj->WriteBuffer[i * 2 + 1] = pairs[i].Data;
    }
    
    pobj->pHandle->hBufferDriver = DRV_I2C_Transmit(pobj->pHandle->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, count * 2, NULL);
    
    return pobj->pHandle->hBufferDriver != NULL;
}

_Bool BME280_StartReadingResistor(BME280 *pobj, uint8_t address, uint8_t *pdata)
{
    return BME280_StartBurstReadingResistor(pobj, address, pdata, 1);
}

_Bool BME280_StartBurstReadingResistor(BME280 *pobj, uint8_t address, uint8_t *pdata, int length)
{
    if(I2C_IsCommunicating(pobj->pHandle))
        return false;
    
    pobj->WriteBuffer[0] = address;

    pobj->pHandle->hBufferDriver = DRV_I2C_TransmitThenReceive(pobj->pHandle->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 1, pdata, length, NULL);
    
    return pobj->pHandle->hBufferDriver != NULL;
}

送受信関係のコードです。
基本的には、書きこみの場合はレジスタアドレスを送ってからデータ内容を送信、読み込みの場合はレジスタアドレスを送ってから読み込みモードでリスタートして受信という形になります。

ただ、複数レジスタを同時に読み書きするburst write / readは読み込むときと書きこむときで仕様が違います。
読み込みの場合はレジスタが自動的にインクリメントされるので、読み込みたいアドレスの先頭のアドレスを指定してやって読みたいバイト数分のデータを連続で読み込めば良いです。
書き込みの場合は自動的にインクリメントはされません。その代わり、「レジスタアドレス+書きこむ値」の計2バイトをセットで送る形になります。
読み込みは測定値のレジスタアドレスが連番になっていますから自動インクリメントが助かりますが、書きこみの場合は設定になるので連番とは限らず、このような書きこみ方ができるとかえって助かります。

ところで、湿度は16bit、温度と気圧は20bit値なわけで、2バイトないしは3バイトで1つのデータとなっています。この手の複数バイトを1バイトずつ分割して転送する処理をするときは「最初のバイトを送信してから最後のバイトを送信し終わるまでの間に値が更新されたらどうなるのか?」という疑問が付きまといます。
MPU-9250のときも気になってデータシートを探したのですが、特にそのようなことは書かれていませんでした。 ですが、このBME280にはそれに関する記述がありました。
In normal mode, the timing of measurements is not necessarily synchronized to the readout by the user. This means that new measurement results may become available while the user is reading the results from the previous measurement. In this case, shadowing is performed in order to guarantee data consistency. Shadowing will only work if all data registers are read in a single burst read. Therefore, the user must use burst reads if he does not synchronize data readout with the measurement cycle. Using several independent read commands may result in inconsistent data.
If a new measurement is finished and the data registers are still being read, the new measurement results are transferred into shadow data registers. The content of shadow registers is transferred into data registers as soon as the user ends the burst read, even if not all data registers were read.
The end of the burst read is marked by the rising edge of CSB pin in SPI case or by the recognition of a stop condition in I2C case. After the end of the burst read, all user data registers are updated at once. (データシート4.1章「Data register shadowing」より引用)
(拙訳)ノーマルモードでは、測定のタイミングはデータの読み出しと同期されるとは限りません。これはすなわち、ひとつ前の測定値を読み出しているときに新たな測定が完了する可能性があるということを意味しています。そのような場合は、シャドーイングがデータの整合性を保証します。シャドーイングはデータレジスタを1回のバーストリードで読み出しているときのみ機能します。そのため、データの測定サイクルに同期せずに読み出す場合は必ずバーストリードを行う必要があります。個々のレジスタを個別に読みだすとデータの整合性が崩れる可能性があります。
測定が終了したときがレジスタの読み出しをしている最中だった場合、その新しい測定値はシャドーレジスタに転送されます。バーストリードが終了したタイミングでシャドーレジスタの値はデータレジスタに転送されるため、他のレジスタをこれから読み出す予定だったとしてもそれは感知されません。
バーストリードの終了は、SPI接続の場合はCSBピンの立ち上がりエッジ、I2C接続の場合はストップビットで判断されます。バーストリードの終了時に、すべてのデータレジスタは一度に更新されます。
英文を読んでいると何度も同じようなことを言っていて混乱してきますが、要するに、バーストリード中に測定が完了した場合、新しい測定値はシャドーレジスタ(本来読み出すレジスタに書き込みができないときに、いったん測定データを保持しておくためのレジスタ。本来のレジスタの影のような存在であるため、このような名前になっている。)に一旦保存され、バーストリードの終了とともに読み出し用レジスタに転送されます。このため、データの整合性は保証されるということです。
データの整合性とは、マルチバイトデータのそれぞれのバイトが単一の測定から得られたものか、という意味以外にも、温度データと気圧データが同じ時刻に測定されたものである、という意味まで含んでいると思われます。ですので、原則として、このセンサではすべての測定値を同時にバーストリードする必要があります。


さて、今回は設定をこのようにしました。

BME280_RV_CONFIG_t config;
BME280_RV_CTRL_HUM_t ctrl_hum;
BME280_RV_CTRL_MEAS_t ctrl_meas;
BME280_DATA_ADDRESS_PAIR data[3];

ctrl_hum.OSRS_H = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.OSRS_P = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.OSRS_T = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.MODE = BME280_VAL_MODE_NORMAL;
config.T_SB = BME280_VAL_TIME_STANDBY_0_5;
config.FILTER = BME280_VAL_FILTER_OFF;
config.SPI3W_EN = 0;

data[0].Address = BME280_RA_CTRL_HUM;
data[0].Data = ctrl_hum.Value;
data[1].Address = BME280_RA_CTRL_MEAS;
data[1].Data = ctrl_meas.Value;
data[2].Address = BME280_RA_CONFIG;
data[2].Data = config.Value;

if(BME280_StartBurstWritingResistor(&appData.bme280, data, 3))
    appData.state = APP_STATE_BME280_CHECK_CONFIG;
else {
    strcpy(szErrorMessage, "ERROR on APP_STATE_BME280_WRITE_CONFIG\r\n");
    appData.state = APP_STATE_ERROR;
}
break;

各設定用レジスタの値はビットフィールド構造体を作ってあげて、マクロでわかりやすく設定できるようにしました。
同じ物理量を複数回測定することで精度を出す手法「オーバーサンプリング」を使うことができますが、その分サンプリング周波数が落ちるので今回は使わないことにしています。
モードはスリープ(SLEEP)、1回のみ測定(FORCE)、連続で測定(NORMAL)の3種類から選べます。今回はもちろんNORMALです。
また、測定をしてから次の測定までのインターバルを設定することができます。今回は最短の0.5msにしましたが、あくまでもそれはインターバルで、測定周期とは異なることに注意が必要です。この辺の時間の見積もり方はデータシートに詳しく載っています。

さて、設定をしたら次はTrimming Parameterを読み込まなくてはいけません。
レジスタはアドレス0x88~0xA1の26バイトと、0xE1~0xF0の16バイトに分かれて格納されていますが、このうち使うのは前者26バイト分と後者7バイト分のようです。
かなり不規則なデータの入れ方をしていますが、格納するときはこのようなプログラムになります。

DRV_I2C_BUFFER_EVENT BME280_CheckAndSetTrimingParam_Low(BME280 *pobj, uint8_t *pdata)
{
    DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj->pHandle);
    if(event == DRV_I2C_BUFFER_EVENT_COMPLETE) {
        pobj->dig_T1 = ((uint16_t)pdata[ 1] << 8) | pdata[ 0];
        pobj->dig_T2 = ((uint16_t)pdata[ 3] << 8) | pdata[ 2];
        pobj->dig_T3 = ((uint16_t)pdata[ 5] << 8) | pdata[ 4];
        pobj->dig_P1 = ((uint16_t)pdata[ 7] << 8) | pdata[ 6];
        pobj->dig_P2 = ((uint16_t)pdata[ 9] << 8) | pdata[ 8];
        pobj->dig_P3 = ((uint16_t)pdata[11] << 8) | pdata[10];
        pobj->dig_P4 = ((uint16_t)pdata[13] << 8) | pdata[12];
        pobj->dig_P5 = ((uint16_t)pdata[15] << 8) | pdata[14];
        pobj->dig_P6 = ((uint16_t)pdata[17] << 8) | pdata[16];
        pobj->dig_P7 = ((uint16_t)pdata[19] << 8) | pdata[18];
        pobj->dig_P8 = ((uint16_t)pdata[21] << 8) | pdata[20];
        pobj->dig_P9 = ((uint16_t)pdata[23] << 8) | pdata[22];
        pobj->dig_H1 =            pdata[25];
    }
    return event;
}

DRV_I2C_BUFFER_EVENT BME280_CheckAndSetTrimingParam_High(BME280 *pobj, uint8_t *pdata)
{
    DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj->pHandle);
    if(event == DRV_I2C_BUFFER_EVENT_COMPLETE) {
        pobj->dig_H2 = ((uint16_t)pdata[1] << 8) |   pdata[0];
        pobj->dig_H3 =                               pdata[2];
        pobj->dig_H4 = ((uint16_t)pdata[3] << 4) | ( pdata[4]       & 0x0F);
        pobj->dig_H5 = ((uint16_t)pdata[5] << 4) | ((pdata[4] >> 4) & 0x0F);
        pobj->dig_H6 =                               pdata[6];
    }
    return event;
}

前半部分で偶数バイトは下位、奇数バイトは上位としているのかと思いきやdig_H1だけ違ったり、後半部分では4ビットずつ使うところなんかが出てきていささか気持ち悪いですが、とりあえずこんな感じのコードになりました。


さて、今度はこれを使って生の測定値を変換することになりますが、これまた非常にわかりにくい処理になっています。

データシートを見ると、コードが書いてあるだけで、具体的にどのような数式で処理をしているかはっきりしません。変数もval1などと言った意味のない名前で、使いまわしもされており、完全に人に読ませるようなコードではありません。
もっと言えば、データシートでは符号付き整数型のシフト演算がされており、負の値に対するシフト演算はC言語では未定義(処理系依存)の動作となります。そのうえ、データシートに「値の変換はBosch Sensortecから出ているAPIを使うことを推奨します」と書かれている始末です。じゃあなんのためにデータシートにこんなわけわからんコード載せてんねん。
Bosch Sensortec BME280 sensor driver
これが例のAPIです。
見るとデータシートのコードから修正され、除算演算子を使った計算に置き換えられています。
実際は2^nの除算しか使ってないので、コンパイラの腕の見せ所(最適化でシフト処理に置き換えられるか)ですが、さすがに処理系依存のコードを書くわけにもいかないのでこの辺が妥当でしょう。

生の測定値とはいったい何を表している値なのか、この演算処理はどういった計算をしているのかはデータシートでの説明が皆無ですので置いておくとして、ここで重要なことは気圧や湿度の変換には温度データが必要な点です。上記APIには「t_fine」という変数に温度が保存され、気圧、湿度の変換関数で使われていますが、このt_fineには温度[℃]の値が5120倍された数値になっていて保存されています。
その点に注意しながら上記APIのコードを使いましょう。

これで無事測定ができるようになりました。


これは、外出から帰ってきたときからの部屋の温湿度・気圧の推移です。最初は32℃くらい室温がありましたが、エアコンを入れたので28℃くらいまで下がっている様子がわかります。湿度もエアコンによって下がっていますね。


今回はBME280に絞った記事ですが、実際は先日のMPU-9250と同時にデータを取っています。
もうちょっとプログラムに磨きを掛けて、細かな完成度を上げていきたいです。

2017年8月14日月曜日

MPU-9250をPIC32MX250F128B+Harmonyで使う

前回の記事でUSBメモリーの読み書きをやりました。
さて、なぜこれをやったかというと、ロガー的なものを作ってみたかったからなんですね。

今回は、9軸モーションセンサであるInvenSense製MPU-9250をいじってみようと思います。

MPU-9250はジャイロセンサ、加速度センサ、地磁気センサの3つのセンサが搭載されているセンサで、I2CまたはSPIでその値を読みだすことができます。
ほかにもDMP(Digital Motion Processor)という計算チップが入っており、これら3つのセンサの値をから姿勢を算出し、オイラー角や四元数などの値を吐き出すこともできるようです。

今回は、とりあえずとっかかりとしてI2C接続でセンサの吐き出す加速度と角速度、そしておまけで付いてくる温度を読み出してUSBメモリーに記録していきたいと思います。

ちなみに、なぜ地磁気センサを読み出さないかというと、MPU-9250の中で別チップになっているからです。MPU-9250の中には加速度センサやジャイロセンサとは独立して、完全に別チップ(旭化成製AK8963)として地磁気センサが入っています。そのため、加速度センサの値等とは完全に別枠で制御してやる必要があります。なので、これはまた今度の課題とします。


さて、細かなスペックはデータシートとレジスターマップを見ればだいたいわかります。
ですので、どんどんコードを書いていきたいと思います。

typedef struct {
    DRV_HANDLE hDriver;
    DRV_I2C_BUFFER_HANDLE hBufferDriver;
    uint8_t WriteBuffer[2];    
    uint8_t SlaveAddress;
} MPU9250;

まずは、MPU-9250にアクセスする用のデータをストックする構造体を作っておきます。

void I2CEventHandler(DRV_I2C_BUFFER_EVENT event, DRV_I2C_BUFFER_HANDLE bufferHandle, uintptr_t context)
{
    /*
    switch (event)
    {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            break;
        case DRV_I2C_BUFFER_EVENT_ERROR:
            break;
        default:
            break;
    }
    */
}

_Bool MPU9250_Init(MPU9250 *pobj, SYS_MODULE_INDEX index, uint8_t SlaveAddr)
{
    pobj->SlaveAddress = SlaveAddr;
    
    pobj->hDriver = DRV_I2C_Open(index, DRV_IO_INTENT_READWRITE);
    pobj->hBufferDriver = NULL;
    DRV_I2C_BufferEventHandlerSet(pobj->hDriver, I2CEventHandler, (uintptr_t)pobj);

    return pobj->hDriver != (DRV_HANDLE)NULL;
}

次は初期化の関数です。特に変なところは無いと思います。
I2Cの送受信が終わったときなどにそのイベントを受け取る関数を一応用意していますが、今回は使わないので中身は空にしています。

DRV_I2C_BUFFER_EVENT MPU9250_GetTransferStatus(MPU9250 *pobj)
{
    return DRV_I2C_TransferStatusGet(pobj->hDriver, pobj->hBufferDriver);
}

_Bool MPU9250_IsI2CCommunicating(MPU9250 *pobj)
{
    if(pobj->hBufferDriver == NULL)
        return false;
    else {
        DRV_I2C_BUFFER_EVENT event = MPU9250_GetTransferStatus(pobj);        
        return (event != DRV_I2C_BUFFER_EVENT_COMPLETE) && (event != DRV_I2C_BUFFER_EVENT_ERROR);
    }
}

I2Cの状態を確認するラッパー関数です。
MPU9250_IsI2CCommunicating関数は、これがTRUEを返すと現在送受信中で、FALSEを返すと現在アイドル中であることを表します。

_Bool MPU9250_StartWritingResistor(MPU9250 *pobj, uint8_t address, uint8_t data)
{
    if(MPU9250_IsI2CCommunicating(pobj))
        return false;
    
    pobj->WriteBuffer[0] = address;
    pobj->WriteBuffer[1] = data;
    
    pobj->hBufferDriver = DRV_I2C_Transmit(pobj->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 2, NULL);
    
    return pobj->hBufferDriver != NULL;
}

_Bool MPU9250_StartReadingResistor(MPU9250 *pobj, uint8_t address, uint8_t *pdata)
{
    return MPU9250_StartBurstReadingResistor(pobj, address, pdata, 1);
}

_Bool MPU9250_StartBurstReadingResistor(MPU9250 *pobj, uint8_t address, uint8_t *pdata, int length)
{
    if(MPU9250_IsI2CCommunicating(pobj))
        return false;
    
    pobj->WriteBuffer[0] = address;

    pobj->hBufferDriver = DRV_I2C_TransmitThenReceive(pobj->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 1, pdata, length, NULL);
    
    return pobj->hBufferDriver != NULL;
}

これが実際に送受信をする関数です。

注意しなければならないことは、スレーブアドレスはそのまま入れられないということです。
I2Cは最初の8bit中上位7bitでスレーブアドレスを送り、下位1bitでリードモードかライトモードかを指定します。
Harmonyのドライバ関数では、そのリード/ライトのビット付加は自動でやってくれますが、アドレスの1bitシフトはやってくれません。ですので、スレーブアドレスは左に1bitシフトしたうえで渡す必要があります。

また、これらの関数は読み書きをしたいメモリアドレスをパラメーターとして受け取りますが、DRV_I2C_TransmitThenReceive関数などでは内部でこの送信内容のデータのポインタを保持しています。ですので、この関数から制御が戻ると値が不定となるスタック領域ではだめで、MPU9250構造体の中に確保したメモリにアドレスをコピーしてそれを参照するようにしています。

uint16_t MPU9250_ParseUInt16(uint8_t *pData)
{
    return (uint16_t)pData[0] << 8 | pData[1];
}

int16_t MPU9250_ParseInt16(uint8_t *pData)
{
    return (int16_t)MPU9250_ParseUInt16(pData);
}

void MPU9250_ParseVector(uint8_t *pData, VECTOR_3D *pOut)
{
    pOut->X = MPU9250_ParseInt16(pData);
    pOut->Y = MPU9250_ParseInt16(pData + 2);
    pOut->Z = MPU9250_ParseInt16(pData + 4);
}

あとはユーティリティ的なものですが、MPU-9250は16bit値は上位8bitのほうが若番のアドレスが振られています。多くの処理系とは異なる(とか言うと怒られそうですが)ので、手動でシフト演算するように作っています。

さて、このライブラリを使う側(app.c)は、だいたいこんな実装になります。

case APP_STATE_TMR_I2C_INIT:
    //Initialize I2C and Send 'Who am I' command
    if(MPU9250_Init(&appData.mpu9250, DRV_I2C_INDEX_0, MPU9250_I2C_ADDRESS_0)) {
        MPU9250_StartReadingResistor(&appData.mpu9250, MPU9250_RA_WHO_AM_I, RXBuffer);
        appData.state = APP_STATE_MPU9250_CHECK_WHO_AM_I;
    } else
        appData.state = APP_STATE_ERROR;

    break;            
case APP_STATE_MPU9250_CHECK_WHO_AM_I:            
    switch(MPU9250_GetTransferStatus(&appData.mpu9250)) {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            if(RXBuffer[0] == MPU9250_RESPONSE_WHO_AM_I)
                appData.state = APP_STATE_READ_RAW_DATA;
            else
                appData.state = APP_STATE_ERROR;
            break;                    
        case DRV_I2C_BUFFER_EVENT_ERROR:
            appData.state = APP_STATE_ERROR;
            break;
        default:
            break;
    }            
    break;            
case APP_STATE_READ_RAW_DATA:
    if(MPU9250_StartBurstReadingResistor(&appData.mpu9250, MPU9250_RA_ACCEL_XOUT_H, RXBuffer, 14)) {
        i2cWaitCounter = 0;
        appData.state = APP_STATE_CHECK_RAW_DATA;
    } else {
        appData.state = APP_STATE_ERROR;
    }
    break;            
case APP_STATE_CHECK_RAW_DATA:
{
    DRV_I2C_BUFFER_EVENT event = MPU9250_GetTransferStatus(&appData.mpu9250);
    switch(event) {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            MPU9250_ParseVector(&RXBuffer[0], &appData.accel);
            appData.temperature = MPU9250_ParseInt16(&RXBuffer[6]);
            MPU9250_ParseVector(&RXBuffer[8], &appData.gyro);
            
            appData.state = APP_STATE_READ_RAW_DATA;

            break;                    
        case DRV_I2C_BUFFER_EVENT_ERROR:
            appData.state = APP_STATE_ERROR;
            break;
        default:    //I2C communication in operating
            if(i2cWaitCounter++ > 1000) {
                appData.state = APP_STATE_ERROR;
            }
            break;
    }
    break;
}            

まずはWhoAmIコマンドを送り、MPU-9250がちゃんと機能しているかを確認します。
その次は、加速度センサのx軸の上位8bitから14bytes連続で読みだします。ここに加速度センサの各軸の値、温度、ジャイロセンサの各軸の値が含まれます。
I2Cの読み込みが終了したら、値を分離して保存してあげます。
デフォルトで加速度は±2gFSですので、測定値の生の値を2^(16-2)で割ってから重力加速度の9.8を掛けてあげればm/s^2になります。


測定データの加速度の値です。ロギング中に基板をグリグリと回してあげたのでこのような測定結果が得られています。静置時にz軸方向におよそ9.8m/s^2が出ているので、値としてもおかしくないと思います。



…とまあここまですんなりと書きましたが、ハマりポイントはありました。

実際はこのI2C通信以外にもUSBメモリーの書き込み処理や、時間を計測するためのタイマー処理などをやっているわけで、コードは結構複雑になっています。
そんな中、データを取っていると0~1秒程度でハングアップしてしまう現象にかなり悩まされました。
I2Cがハングアップしているのか、USBがハングアップしているのか、LEDデバッグという超縛りプレイ環境のせいで切り分けに苦労しましたが 、結局はI2Cがハングアップしていました。

不具合の内容としては、上記プログラムの中でMPU9250_GetTransferStatus関数を呼び出し、I2Cの通信が完了するのを待つというところがありますが、いつまでたってもI2Cの通信が終わらないというものでした。
ですが、I2Cの不具合となれば、USBメモリーの読み書き機能が使えますので、実際にMPU9250_GetTransferStatus関数(DRV_I2C_TransferStatusGet関数)が返すDRV_I2C_BUFFER_EVENTの値と、DRV_I2C_BytesTransferred関数が返す送受信済みバイト数を記録してみました。


青線がDRV_I2C_BUFFER_EVENTの値で橙線が送受信済みバイト数です。
レジスタアドレス送信+14bytesリードで計15bytesの読み書きが行われています。
statusとしては正常に送信要求→リスタート→ACK送信の流れで読み書きができていますが、最後は5bytes分読み込んだところでブツンと受信が息絶えてしまっています。

そもそもI2Cはマスター側がクロックを発生させて、そのタイミングでスレーブがデータを送信しますので、スレーブがそれに反応できなかったとしてもプルアップ抵抗のために0xFFのデータが受かるだけです。そのため、受信中にハングアップするのは、明らかにスレーブではなくマスターのせいなわけです。


さてさて、こうなるとHarmonyのI2CドライバがDRV_I2C_TransmitThenReceive関数の呼び出しを受けて処理を開始し、各タイミングで発生する割り込みを処理し、送受信を完了させるまでの間にどこか問題があるということになって、一ユーザーがどうにかできるような領域じゃなくなってきてしまいます。困った困った。

ん…?各タイミングで発生する割り込み処理…?


設定を見ると、デフォルトでI2Cの割り込み優先度が1になっています。
タイマやUSBは4に設定されており、I2C割り込み中にもUSBの割り込みが発生することができます。

I2Cの割り込み処理をしている間に他の割り込みが入ってきて、受信データの処理が間に合わなくなったらどうなる…?ACKを送るタイミングを逃したらNACKだとスレーブが勘違いして送るのをやめるのでは…?

というわけで、優先度を最大の7にしてみました。
そうすると、無事、安定して動きましたとさ。



ふぅ、これで3日間くらい格闘してたぜ…。
これでやっと次のステップに移れる…。

2017年8月11日金曜日

PIC32MX250F128B+HarmonyでUSBメモリーに書き込みをする

やはりUSB Hostを手軽にやりたいならばPIC32MX250F128Bです。
以前、USBメモリーにアクセスしたり、 それを一部改造してファイルシステムはFatFsで読み書きしたりして遊んでいましたが、久々にUSBメモリーを使いたくなってもう一度この時使ったPIC32MX250を取り出してきました。

当時とは環境を一新してやろうとしたところ、どうもライブラリに新しいものが出てきているようでした。

MPLAB Harmony

MPLAB HarmonyはPIC32シリーズ専用のライブラリ群(公式サイトにはフレームワークって書いてありますが)で、柔軟で抽象度の高いプログラムを書くことを目的に開発されたもののようです。Harmonyという名前からも、各機能が調和し合って完成していくような様を表しているのでしょう。

以前、PIC32MX250でUSBメモリーにアクセスしたときはMicrochip Libraries for Application(MLA)というライブラリを使っておりました。MicrochipはこれをMPLAB Harmonyで置き換えようとしているみたいで、新しいバージョンのMPLAB XやXC32でMLAを使おうとしたところ鬱陶しいほどの警告が出てきました。


というわけで、今回はMPLAB Harmonyを使ってUSBメモリーにアクセスしてみたいと思います。回路はMLAのときとほぼ同じですが、LEDの場所だけ都合によって変えました。

とその前に、今回使用した環境だけ明記しておきますね。こういうの、バージョンによって結構変わってきそうなので。
  • MPLAB X IDE v3.65
  • XC32 v1.44
  • MPLAB Harmony v1.11
まずは、HarmonyをインストールしてHarmonyのプロジェクトを作れるようにします。これはググればいろいろなサイトで出てくるのでそこまで苦労しないでしょう。


プロジェクトテンプレートが用意されているので、今までのようにサンプルプログラムから必要なソースコードをコピペしてきたリ、いちいちライブラリのincludeディレクトリに参照を追加したりという面倒な作業が必要無くなります。

そして何よりも強力なのがこの「MPLAB Harmony Configurator」です。
PICでプログラミングするうえで、初心者殺し(そして慣れれば単に面倒)だった作業の1つであるConfigurationビットの設定がGUIでできてしまいます。そして、Configurationビットだけにとどまらず、どのライブラリをインポートするか、どのような設定で使うかなどということまでここで設定できてしまうためかなり楽ちんになっています。


今回はUSB HostでMSD(Mass Storage Device)を使うので上記のような設定にします。
そして、Harmony Configuratorで最も強力なのがこのClock Diagramです。


複雑な構造のクロックのプリスケーラ、ポストスケーラ等の設定が図の上で簡単に設定できてしまいます。
今回は8MHzのセラロックを付けたので、そのように設定し、USBには48MHz、システムクロックには60MHzが供給できるように設定します。

最後に「Pin Settings」でLEDを接続したピンをデジタル出力に設定して、「Generate Code」ボタンをクリックすれば自動的にこの設定に従ったソースコードをプロジェクトに設定してくれます。
ああ、なんとゆとりな時代になったのでしょう。

ちなみに、ユーザーが編集するソースコードは[ProjectName]/Source Files/app/app.cと[ProjectName]/Header Files/app/app.hの2つだけです。
これ以外は上記のGeneratorが自動的に作ってくれるコードですので、設定を変えたらせっかく自分が編集していた分も消え去ってしまいます。

おっと、最後に1つ注意事項。
ヒープ領域を設定します。
これを設定しないと、謎の実行時エラー(USB_HOST_EVENT_DEVICE_UNSUPPORTED)に悩まされることになります。これはMLAの時もハマりポイントでした。適当に2000bytesくらいをヒープ領域に割り当てておけばよいでしょう。



さて、肝心のapp.cとapp.hですが、これはデモプログラムの移植がとっつきやすいでしょう。
Harmonyのインストールフォルダ(C:\microchip\harmony\v1_11\apps\usb\host\msd_basic\firmware\src)の中にあるapp.cとapp.hを今作ったプログラムのapp.cとapp.hにコピペします。

コンパイルするとLED関係の関数呼び出し(マイコンボード用?)がエラーになりますが、必須ではないのでコメントアウトしてやれば良いでしょう。


ちなみにですが、今回はRB13にLEDを付けましたので、アクセスランプとして書き込み中に点灯するように改造しておきます。
case APP_STATE_WRITE_TO_FILE:

    // Try writing to the file
    
    PLIB_PORTS_PinSet(PORTS_ID_0, PORT_CHANNEL_B, PORTS_BIT_POS_13);
    
    if (SYS_FS_FileWrite( appData.fileHandle, "Hello, world!\r\n", 15 ) == -1)
    {
        // Write was not successful. Close the file and error out.
        SYS_FS_FileClose(appData.fileHandle);
        appData.state = APP_STATE_ERROR;

    }
    else
    {
        // We are done writing. Close the file
        appData.state = APP_STATE_CLOSE_FILE;
    }

    PLIB_PORTS_PinClear(PORTS_ID_0, PORT_CHANNEL_B, PORTS_BIT_POS_13);
    
    break;
GPIOの制御はPLIB_PORTS_PinSet関数などを使います。
直接LATBなどを叩いていたらライブラリによる抽象化が台無しですからね。

余談ですが、Harmonyはファイルシステムの制御モジュールとしてFatFsを使っているようです。
ついにMicrochipに使われるようになったかFatFs。素晴らしいライブラリです。ですので、MLAのときにあったタイムスタンプが書きこまれないトラブルも回避されています。


さて、前回はUSBメモリーへの書き込みをやっただけで満足して終わりでしたが、今回はちゃんとアプリケーションを作るぞー!

2017年5月29日月曜日

パナソニックの照明を制御する

我が家の建物にはパナソニック製の照明が備え付けられています。
そして、入居してしばらくは気づかなかったのですが、どうもこの照明、リモコンに対応しているようです。部屋にはリモコンが備え付けられていませんでした。

型番をググって説明書を見ると、リモコンはHK9487MMというものらしいです。Amazonで2500円程度…。高いな…。

さて、説明書やリモコンの画像を見ると、どうも下記のボタンがあるようです。
  • 点灯(普段)
  • 全灯
  • 常夜灯
  • 消灯
  • 白い色(調色ボタン)
  • 暖かい色(調色ボタン)
  • 明るい(明暗ボタン)
  • 暗い(明暗ボタン)
  • おやすみ30(30分タイマー)
  • チャンネル選択(1,2,3)
  • チャンネル確定
ほうほう、壁のボタンだと全灯→暗め→常夜灯しか切り替えられませんが、こんなにいろいろな機能があるようです。

手元にリモコンがあれば、赤外線フォトトランジスタとかを使ってコードを解析すればいいのですが、無いのでどうしようもありません。

赤外線通信の方式

どうもググるところによると、この手の赤外線通信はおおよそ3種類あるそうです。NECフォーマット、SONYフォーマット、家電協フォーマットがその3種類ですが、パナソニック製照明はどうも家電協フォーマットのようです。

赤外線リモコンの通信フォーマット

細かいプロトコルはこのページに詳しく書いてあります。
T=425usとして、赤外線LEDを38kHzで点滅させている状態(duty比1:2)をON、消灯させている状態をOFFとすると、0はONを1T、OFFを1Tで1はONを1T、OFFを3Tで表現するようです。

ここでハマったことは、最後のビットを送信し終わった後にONを1Tだけ送ってストップビットとすることです。このストップビットを送らないと、最後のビットでOFFが1T続いたのか3T続いたのかがわからないので、最後のビットを読むことができません。結果、デコードエラーとなり受け付けてくれません。

送信データの内容

さて、具体的にどんな信号を送ればいいかと言うと、このサイトに書いてありました。

iRemo2 リモコン・データベース

例えば、CH1の全灯コマンドは

0x344A9034A4

です。
上記のフォーマットに当てはめてみると、「0x344A」がパナソニックのカスタマーコードのようです。その次の「9」が、この16bit値を4bitずつ区切ってXORを取ったパリティです。
次は、「0x034」が命令コード本体で、最後の「A4」は、カスタマーコードのパリティを含んだ「0x9034」の16bit値を8bitずつ区切ってXORを取ったパリティのようです。

すなわち、パナソニック照明に何かしらのコマンドを送るのにキーとなるのはたったの12bit分です。
しかも、上記のページにはコマンドがある程度書いてあります。

さて、その肝になる12bit分をリストに書いてみるとこうなります。

0000 0101 0100 CH1 明るく
0000 0100 1100 CH2 明るく
0000 0101 1100 CH3 明るく
0000 1011 0100 CH1 点灯(普段)
0000 1010 1100 CH2 点灯(普段)
0000 1011 1100 CH3 点灯(普段)
0000 1101 0100 CH1 暗く
0000 1100 1100 CH2 暗く
0000 1101 1100 CH3 暗く
0000 1111 0100 CH1 消灯
0000 1110 1100 CH2 消灯
0000 1111 1100 CH3 消灯
0000 0111 0100 CH1 常夜灯
0000 0110 1100 CH2 常夜灯
0000 0111 1100 CH3 常夜灯
0000 0011 0100 CH1 全灯
0000 0010 1100 CH2 全灯
0000 0011 1100 CH3 全灯

さて、ここまで見ると(わざとらしく色分けしていますが)見えてきます。
最初の4bit、最後の3bitはよくわかりませんが、5~7bit目は点灯内容を表すコード、8,9bit目はチャンネルに対応しています。

ここまでの話はMSBからデータを送信しているとの仮定ですが、LSBから送信していたとするとチャンネルのbitがそのままチャンネル番号に対応していると言えそうですが、まあ細かいことなのであまり気にしないことにしましょう。MSBから送っていると考えたほうがわかりやすいですし。

他の送信コード

最初にリモコンの外見等から見た機能の中で、上記のコードに含まれていないものがあります。
  • 白く
  • 黄色く
  • おやすみ30(30分タイマー)
  • チャンネル確定
といったところでしょうか。
最初は上記の青色部分が「000」と「100」の2通りが無いので、これが「白く」と「黄色く」に対応するのかなと思いましたが、どうも違うようです。

とりあえず、チャンネルの位置を8,9bit目として他の部分で10bit分。これを総当たりすれば1024通りです。「白く」「黄色く」の調色ボタンは1回押しただけでは変化がわかりにくいので、20回くらい連打するべきでしょう。
20回の連打に3秒かければ、だいたい1時間くらいで総当たりが終わります。その間にゴロゴロしながらアニメでも見ていればいいでしょうと思いながらやってみました。

そうすると、一部のコードが見つかりましたが全部は見つかりませんでした。
うーん、こうなったら12bit総当たりだ!それでも4時間くらいですしまあ何とかなるでしょう。

というわけで洗いだしたコードが下記の通りになります。

1100 110‬‬1 0001 CH1 黄色
1100 1011 0001 CH2 黄色
1100 1111 0001 CH3 黄色
1100 100‬0 1001 CH1 黄色く
1100 1010 1001 CH2 黄色く
1100 100‬1 1001 CH3 黄色く
1100 010‬1 0001 CH1 白
1100 0011 0001 CH2 白
1100 0111 0001 CH3 白
1100 0000 1001 CH1 白く
1100 0010 1001 CH2 白く
1100 000‬1 1001 CH3 白く
1100 1000 0101 CH1 タイマー
1100 0101 0101 CH2 タイマー
1100 110‬0 1101 CH3 タイマー
1100 010‬1 1011 CH1 チャンネル変更
1100 110‬‬1 1011 CH2 チャンネル変更
1100 001‬‬‬1 1011 CH3 チャンネル変更

リモコンにはない、「(最も)黄色にする」「(最も)白にする」 というコマンドもオマケで見つかりました。
ですが、これを見てもどこがチャンネルと言うべきか、どこが点灯内容と言うべきかいまいちよくわからないコードです。色関係のコマンドでは少し共通性はありますが、タイマーやチャンネル変更を合わせたコマンドを見てもいまいち規則性はわかりませんでした。
誰か解読できた人がいたら教えてください。

実装(ダイジェスト)

さて、これをESP8266に実装し、基板を作成して箱に収めるとこんな感じになります。



ESP8266なのでWi-Fiで操作できます。
ウェブブラウザでも操作できますが、せっかくなのでスマホアプリも作ってみました。
まあこの辺の製作話はまた今度、気が向いたらにしましょう。

ではでは。