2018年8月16日木曜日

Flash MSC


 祝・FlashをMSCに使えたぞ、の図。
 プログラムだけで動くやつだと写真写り悪いねぇ。

 いろいろトラブルも有りましたが、無事にMSCとして動くようになりました。バンク間の移動もちゃんと動くようになりました。
 今の所100KiB程度の容量ですが。

 一番長くかかったトラブルは、0オリジンと1オリジンの混同でした。これに気がつくのにほぼ丸一日使ってしまった。
 128KiBのバンクは512バイト毎の256セクタに分割され、最初(0番目)のセクタを管理領域として使用しています。そのため、使用可能なセクタを探すコマンドの戻り値は1から255の範囲をとります。一方、管理構造体は最初に16bit x 256の配列があり、その後で8bit x 512 x 255の配列を持ちます。この構造体ではセクタは0から254の範囲を取るわけですが、1-255の値をそのまま使っていたために不思議な挙動になってしまいました。
 結局、16x256と8x512x256をunionで同じ領域に配置することにしました。


 FatFsの低レベルreadにFlashから読み込む機能を追加したので、PCから自由にファイルを読み書きしつつ、マイコンからもファイルを読み出せるようになりました。
 あとはLuaインタプリタを追加すれば、PCから投げたスクリプトを実行できるようになります。もっとも、実用的に使うのであれば、C++でLua向けのHALラッパーを作る必要がありますが。ここが一番面倒ですね。

 とりあえず、最低限プラモデルの電飾に使えるくらいの機能は実装したいなぁ。
 とはいえ、小さなプラモデルに組み込むにはSTBee F4miniはデカいし、大きなプラモデルに組み込もうとすると、そもそもプラモデルを作るのが大変という問題が。
 高層ビルのプラモデルで窓文字とか? いったいどれだけのLEDが必要なんだろうか。。。もちろん、プログラム次第でサーボモータと電飾を一括して制御とかもできるので、軍艦の砲塔を動かしながら砲塔を光らせるとか、戦車の砲塔を動かしながらLEDでマズルフラッシュを光らせて、同時にリコイル動作を再現する、とかも可能なはずですが。

2018年8月15日水曜日

マイコンのFlashでMSCが動いた




 マイコンのFlashでMSCが動くようになりました。
 結局、ISRからでもFlashの書き込みは可能でした。なんで駄目だったかというと、書き込み単位を8byteにしていたからです。
 リファレンスマニュアルによると、8byte単位で書き込むには外部電源(高電圧)が必要なようです。ちゃんとマニュアル読んどけって話ですね。。。。

 130560バイトのFlash領域を使っていますが、108032バイトのメモリとして認識されています。22528バイトがどっかに行ってますが、FATに使われてるのでしょう。

 テキストファイルを投げて、マイコンの電源を切っても、ちゃんとファイルは残っています。
 とりあえず簡単に実装するために、バンク間の転送(重複の除去とイレース)は実装していません。Flashのイレースはそれなりに時間がかかるので、頻繁にこれが発生するとネックになりそうです。
 Trimコマンドは不要になったセクタを使用しないことにより、結果的にバンク間の転送を減らし、ひいてはイレースの回数をへらすのが目的です。なるほど、効果がありそうです。


 あと、イレースはHAL_FLASHx_Erase関数で行いますが、前後にFLASH_UnlockとFLASH_Lockが必要です。が、UnlockしなくてもErase関数からはOKが帰ります。そのかわりEraseでブロックされる時間がほぼゼロですが。
 Unlock/Lockは忘れないようにしましょう。
 不用意にErase関数を呼んでしまわないように(or呼ばれても大丈夫なように)Flashにはロックが掛かっています。それを解除するのがUnlockです。絶対に必要です。


 もう少し頑張ればちゃんとUSBメモリとして使えそうです。


 複数バンクを使う方法も考えてみましたが、結構面倒な感じです。複数バンクにセクタが重複して作成されるので、その前後関係をどうやって認識するかが問題ですね。
 いろいろ方法が考えられますが、とりあえず100KiB程度でも足りるはずなので、しばらくはシングルバンクで試してみます。

 あとは、FatFsをReadOnlyで組めば、マイコン自身からファイルを読み出すことができます。マイコンからの書き込みは排他制御が面倒ですね。mbedだとマイコンが書き込もうとした後はMSCが切断される、という感じの実装になっていたはずです。
 スクリプトの実行だけならReadOnlyでも問題ないので、しばらくはそのようにします。
 CubeMXのコードでもUSBメモリを取り外すような機能があるっぽいので、どうにでもなると思いますが。
 ただ、mbedはメインのチップの他にUSB接続周りのチップが有って、マイコンがリセット状態でもMSCとして認識できます。この場合、マイコンが起動した直後からファイル書き込みを行うようなプログラムが有っても、マイコンをリセットしておけばPCからMSCにアクセスできますが、マイコンワンチップでMSCも作ると、そのような挙動が作れません。方法としては、マイコンリセット直後にスイッチを監視して、スイッチが押されていればFatFsのwriteはエラーで終わる、みたいな感じでしょうか。でも唯一の押しボタンはBOOTモードに使ってるので、スイッチを増設する必要がありそうです。

 ということで、まだまだやることもいろいろありますが、もう少しがんばります。

2018年8月14日火曜日

Flashの配置


FlashをMSCに使うとして、どういう配置にするか、ちょっと考えてみた。

 STM32F405は1バンクあたり128KiB(Flashの後半の場合)なので、この領域をうまく使えればいい。
 とりあえず、1セクタ512バイトに分割すると、1バンクあたり256セクタになる。この内、最初の1セクタを管理領域として使うと、1バンクあたり255セクタのデータ領域となる。
 最初は1バンク全体が0xFFで初期化されている。データを書き込みたい場合、管理領域からidxが0xFFFFになっている場所を探す。0xFFFFが見つかれば、その場所にデータを書き込み、また管理領域にそこに書き込んだセクタ番号を書き込む。
 読み出す場合は、管理領域の最後から前に戻りながら目的のセクタ番号を探し、見つかればその場所のデータを返す。

 同じバンクに上書きしたい場合は、空いているところに書き込むので、後に書き込んだデータが後ろにくる。そのため、読み出し時に後ろから探せば一番新しいデータを見つけることができる。

 書き込める場所がなくなった場合は別のバンクに転送する必要がある。
 後ろから読み出していくのは同じだが、別のバンクに書き込む前に、再び後ろ向きに捜索し、そのセクタが最新であることを確認する。
 別バンクへの移動が終わったら、古いバンクはイレースし、全体を0xFFで埋める。

 使用中のバンクの識別は、管理領域の最初のバンクのバンク番号を確認することで行える。イレースしてしまえば0xFFFFになるので、そのバンクは未使用とわかるし、そのセクタを使うときは必ず最初のバンクから使うので、使えば0xFFFF以外の値が書き込まれる。

 バンク番号0xFFFFはマジックナンバーとして使うが、それでも0x0000から0xFFFEまでは使えるので、32M-512byteまで使用できる。マイコンのFlashが最大1Mなので、実際は多くても256KiB程度だろうから、十分に余裕がある。

 この方法の場合、特定のアドレスだけが早く消耗することがない、というのも利点。1つのセクタを書き込めば、そのバンクが埋まるまではそのセクタが再利用されることは無い。また、少なくとも2つのバンクを交互に使うので、2つのバンクが均等に消耗される。
 ただし、複数のバンクを使う場合、例えばMSCの容量を128KiB以上にする場合は、バンク間の平均化はかなり難しい。(このバンクが何回書き込まれたか、をどうやって管理するかが問題)。
 また、読み込み・書き込み共にある程度のスキャンが必要なので、実行時間が固定じゃないのも問題か。それでも16bit変数256個読むだけなので、ミリ秒レベルはかからないはず。

 もっとも、平均化の問題は、STM32のFlashは書き込み回数が最小1万回保証なので、そこまで気にする必要もないのかもしれないが。
 reserveの16bit領域を使えば、何回書き込んだか(=何回イレースしたか)を記録できるから、6万回程度までならイレース回数が一番小さいバンクに書き込む、といった工夫ができる。Flash書き換えが1万回程度だから、16bitで管理できれば十分。

***

 何をやりたいかというと、mbedのように、MSCでプログラムをマイコンに入れたい、と思っている次第。そして、そのプロプログラムは機械語コードではなく、Luaスクリプトを想定している。
 例えば、ちょっとしたLEDの点滅だったりといった、簡単な処理を行うのであれば、Luaでも用は足りるはず。そして、Luaスクリプトであれば128KiBもあれば十分に転送できるはず。
 以前、C#とLuaで図形を印刷するプログラムを作ったが、その時に作ったAIM-120のペーパークラフトのLuaコードは約1160行で35KiBだった。単純に考えれば、128KiBあれば4000行くらいのLuaコードを実行できる。関数名や変数名をケチったり、インデントをケチったりすればもっと多くの処理ができるかもしれない。それでも足りないようであれば、PC側でバイトコードに変換してしまうという手もある。
 とにかく、スクリプトを入れるだけなら大容量のメモリは必要ないはず。
 ただ、スクリプトを走らせるにはRAMがかなり必要で、特にフラグメンテーションが大変そうな感じ。

 プラモデルの電飾とか、作ったら面白いんじゃないかなぁと思っているんだが。2年くらい前から考えてるんだが全く進まない。
 ウイングスパン2mくらいのB787のラジコン飛行機とか作って、電飾したら楽しいだろうなぁ。

追記
 PCで簡単に試してみた。


 ヒープ領域から256KiB確保してFlashと想定し、いろいろ処理した後にバイナリファイルで書き出して画像化。黒は00h、白はFFhの領域。

 上半分の、128KiB-512byteをFatFsで使うようにして、f_mkfsでファイルシステムを作ってる。
 適当に30個ほどファイルを作るとトータルで234セクタを使用し、内93セクタは重複した(古い部分は捨てて良い)セクタ。
 下半分は、上半分から重複を省いてコピーした部分。全体で141セクタを使用して、重複はゼロ。

 ということで、なんとか動きそうな雰囲気はしてきた。実際にマイコンでどれくらいパフォーマンスが出るか。。


追記
 試しにUSB Dev MSCで、WriteとReadを処理するプログラムを書いてみた。
 最初は全体が0xFFで初期化されているので、ファイルシステムはなにもない。なのでWinからはフォーマットの催促が来る。
 で、フォーマットするとマイコンがハングアップしてしまう。Flash書き込みでコケてるっぽい。
 同じようなコードで、適当なFreeRTOSタスクからFlashへの書き込みを試したら、ちゃんと書き込める。
 もしかしたらISRの中からはFlash書き込みは不可? 有り得そうなシナリオ。(←誤解でした)
 USB MSCちょーめんどくさい。

 とりあえず適当なバッファを作っておいて、Write要求ではバッファに突っ込んでOKを返して、RTOSタスクでバッファからFlashへ転送、という感じか。

 あと、Flashが0xFFの領域に、0x7Fを書き込み、その後に同じ場所に0x3Fを書き込むと、ちゃんと0x3Fとして書き込まれてる。ということで、bit clear操作は可能で、bit set操作が不可、ということなのだろう。
 例えば管理領域を0x0001-0x7FFFの範囲で使って、そのセクタが不要になった場合(Trimとか)は、管理領域を0x0000にクリアする、みたいな使い方はできそう。
 もっとも、CubeMXのUSB MSCではTrimはなさそうだが。

 とりあえず、明日、気が向いたらRTOS taskでFlashを書き換えるようなのを試してみる。

2018年8月13日月曜日

USB Dev MSC

 CubeでUSB DeviceのMSCのコードを出して試してる。

 [メモ] STM32CubeMX: USB MassStorageデバイス - Qiita

 とりあえずSDカードをSPIで初期化して、セクタ数を返すのと、読み出しリクエストでSDカードから読み出す機能を作ってみた。

 いちおう、デバイスの認識とかはできたが、ファイルがデータ化けしてしまう。一部のファイルは問題ないのだが。

 あと、転送要求はUSBのISRから投げられるので、SDカードの転送とかをISRの中で処理しなければいけない。
 SDカードのアクセスは頑張っても600usecくらいかかってるので、かなり大変。


 ISRの中だと都合が悪いし、ということで、なんとかできないかと思ったのだが。

 方法としては、転送要求を一旦FAILで返し、要求されたアドレスを記録しておき、メインループで読み込み、次にそのアドレスの要求が来たときに返す、という方法を考えてみた。しかし、1回FAILしてしまうと、そのアドレスはしばらく読まれないようだ。
 メモリを潤沢に使えるなら10KiBくらいバッファを確保しておき、そこに読んでおくという方法もあるだろうが、なんかもったいない。
 CCMからなら20KiBくらい割いてもいいけど、CCMだとDMA転送できない(SPIはDMA転送したい)。

 ままならんものだな。
 というか、普通のカードリーダーってどういうアルゴリズムで動いてるんだろうか。
 毎回転送要求が来たらそれを拒否れないわけで、となるとUSB Phyの割り込みからデータを読んで、とかする必要があるだろうけど、そうなるとOS上で処理できない。

 USBはかなり謎い。


 とりあえず、カードリーダーを作りたいわけじゃないので、とりあえずマイコンのFlashを使えるようにしてみるかな? 10KiBくらいあればいいし。って、なんか話が再起してる気がする。

 あと、今回、デバッグ用にUSB FSでDev CDC、USB HS(internal FS Phy)でDev MSCを試してみたのだけど、HS側がうごかなかった。うんともすんとも言わないし、Winのデバイスマネージャでも何も変化ないし、オシロでPD/PMを見てもGNDにプルダウンされたままだった。
 以前、USB FSでDev CDC、USB HS(intr FS Phy)でHost MSCを試したときはちゃんと動いたので、同時にDevを2本、とかはできないのかも。


 USB複合デバイスが使えればそれが一番楽なんだけど、Cubeだと生成できないい。
 USBで好きなデバイスを作れればいろいろ便利だろうけど、USBはかなり大変そう。USBカメラで缶サット的なモノを作るとか、USBモニタとしてHUB75に転送したり、できたら面白そうなんだけど。


追記:2018/08/14
 マイコンのFlashをMSCのストレージとして使えないか、と思っているのだけど、そうするとSDカードのような寿命の平均化ができない。
 あと、STM32F405は後半のセクタは1セクタあたり128KBなので、512バイトとか4KiBの書き換えが発生すると、一旦128KB分のメモリを確保してバックアップする必要がある。RAMが128K+CCMなので、どう考えても無理。
 他に取れる方法は、1) Flashの先頭領域をMSCに使う 2) 複数のセクタを使用する あたりか。
 前者の場合、最初の64KiBは16KBのセクタ4個なので、ギリギリでRAM退避ができそう(同時に動いてるプログラムに依存)。
 後者は、Flashは新規書き込みはFlash削除操作が不要で、書き換え時のみFlash削除操作が必要なので、例えばMSCを32KiBにすれば、丸々4回分はFlash削除が必要ない。そして1つ目のセクタが一杯になったら2つ目のセクタに必要なデータと新しいデータを退避して使用しつつ、2つ目が一杯になる前に1つ目のデータをすべて2つ目に移動しておいて、2つ目がいっぱいになったときは1つ目を削除して、みたいにする。自然とすべての領域が均等に使用されるので、寿命の平均化は考えなくていい。ただしデータのブロック番号とFlashのアドレスが一対一にならないので、これをどこに置くか、という問題が発生する。シークが極端に遅くなるだろうし、アドレステーブルをおいたセクタの消耗が激しくなる、という問題が残る。
 前途多難だなぁ。

 MSCを使用しないで、必要なデータは書き換え毎に都度全体をPCから512バイトずつ転送させる、みたいな方法が一番カンタンだし、自分で使うなら絶対そうするのだけど。他人に使わせるとか考えると、OS標準で使えるMSCは魅力的。

2018年8月9日木曜日

C#でWAVEを生成して再生する

 C#で音を出したかったので、自分で生成して再生してみた。
 同時に1chしか出せないこと、たぶん生成しながら再生することができないこと、たぶんリングバッファ状に延々と再生することができないこと、といった制限がある。そのかわり簡単に再生できる。

 手段としては、SoundPlayerにStreamを渡し、そのStreamは自分で作ったWAVEファイルになっている。あとはBackgroundWorkerで再生する。
 SoundPlayerで非同期に再生すると、再生終了を知ることができないらしい。ということでBackgroundWorkerで同期的に再生し、PlaySyncメソッドから戻った時点でSoundPlayerとStreamをDisposeしている。
 BackgroundWorkerは同時にひとつのスレッドしか持てないが、SoundPlayerも同時再生はできないらしいので特に問題ないはず。

 今回は16bit1chの波形のみを扱った。あとサンプリングレートは44.1kHzに固定している。
 波形を生成するときに10000の係数をかけている。本来は32767をかけるべきで、実際Streamをファイルに書き出すならそれで良いのだが、なぜかSoundPlayerでは数倍に増幅されて再生される。そうすると正弦波の上下がクリップされて矩形波状になり、高調波がかなりでる。この係数は環境依存かもしれない。

 正弦波1つを再生するならConsole.Beepで任意周波数を任意時間再生できるが、もうちょっと複雑な音を出そうとするとConsole.Beepでは出せないはず。
 ゲームでBGMを鳴らしながらSEも鳴らして、という用途には向かないだろうが、まぁそこまでする予定はないのでこれでいいのだ。リングバッファ的なモノで再生しようとするとかなり面倒らしいんだよねぇ。


// RunWorkerAsyncの引数に再生するWAVEファイルのストリームを指定する。
// 再生が終了した時点でストリームをDisposeする。
private readonly BackgroundWorker SoundPlayerBackgroundWorker;

public Form1()
{
    InitializeComponent();

    SoundPlayerBackgroundWorker = new BackgroundWorker();
    SoundPlayerBackgroundWorker.DoWork += SoundPlayerBackgroundWorker_DoWork;
}

private void button1_Click(Object sender, EventArgs e)
{
    playSinwave(500, 1);
}

private void button2_Click(Object sender, EventArgs e)
{
    playSinwave(800, 0.5);
}

private void playSinwave(double waveFreq, double seconds)
{
    Int32 amp = 10000;

    if (!SoundPlayerBackgroundWorker.IsBusy)
    {
        const UInt32 samplingRate = 44100;
        Int16[] wave = new Int16[(int)(samplingRate * seconds)];

        for (int i = 0; i < wave.Length; i++)
        {
            double phase = (double)i / samplingRate * waveFreq * Math.PI * 2;
            double sin = Math.Sin(phase);
            wave[i] = (Int16)(sin * amp);
        }

        SoundPlayerBackgroundWorker.RunWorkerAsync(generateWaveData(wave, samplingRate));
    }
}

private void SoundPlayerBackgroundWorker_DoWork(Object sender, DoWorkEventArgs e)
{
    Stream ms = (Stream)e.Argument;

    using (SoundPlayer sp = new SoundPlayer(ms))
    {
        sp.PlaySync();
    }

    ms.Dispose();
}

private MemoryStream generateWaveData(Int16[] data, UInt32 samplingRate)
{
    MemoryStream ms = new MemoryStream();

    ms.Write(Encoding.ASCII.GetBytes("RIFF"), 0, 4); // RIFF hdr
    ms.Write(BitConverter.GetBytes((UInt32)data.Length * 2 + 44 - 8), 0, 4); // file size - 8
    ms.Write(Encoding.ASCII.GetBytes("WAVE"), 0, 4); // RIFF type is "WAVE"
    ms.Write(Encoding.ASCII.GetBytes("fmt "), 0, 4); // "fmt " chunk
    ms.Write(BitConverter.GetBytes((UInt32)16), 0, 4); // fmt chunk size
    ms.Write(BitConverter.GetBytes((UInt16)1), 0, 2); // format ID
    ms.Write(BitConverter.GetBytes((UInt16)1), 0, 2); // number of channels
    ms.Write(BitConverter.GetBytes((UInt32)samplingRate), 0, 4); // sampling rate
    ms.Write(BitConverter.GetBytes((UInt32)samplingRate * 2), 0, 4); // data speed: bytes per sec
    ms.Write(BitConverter.GetBytes((UInt16)2), 0, 2); // block size: bytes per 1 sample (channels * bit depth / 8)
    ms.Write(BitConverter.GetBytes((UInt16)16), 0, 2); // bit depth (bits)
    ms.Write(Encoding.ASCII.GetBytes("data"), 0, 4); // "data" chunk
    ms.Write(BitConverter.GetBytes((UInt32)data.Length * 2), 0, 4); // data chunk size

    foreach (Int16 hoge in data)
    {
        ms.Write(BitConverter.GetBytes(hoge), 0, 2);
    }

    ms.Position = 0;

    return (ms);
}

2018年8月3日金曜日

Kindle PWを買った

 Kindle Paperwhiteを買いました。2個めです。
 5年ほど前にPWの第1世代を買いました。今回買ったものは第3世代のマンガモデルです。

 第1世代と比べて、サクサク漫画が読めます。
 若干解像度も向上しているようです。

 PDFビューアとしても結構優秀です。
 試しにSTM32F4 reference manual JP(24MBくらい)を入れてみましたが、PDFを開いた直後は読み込みに時間がかかれど、普通に読むならさほどストレスは感じません。
 一方で、高速でページを送ったりする際はかなり読み込み待ちが発生します。おそらくページを開いた際に次のページをキャッシュしているのでしょう。キャッシュが間に合わなくなるとかなりモッサリします。が、ゆっくり読み進める分には問題ありません。

 当然、本来の目的である、本を読むとか、マンガを読むとか、そういう用途ではストレスなく使用できます。
 古いKindleと違い、読書画面に時刻を表示できる機能が追加されたので、あんまり時間がないけど…… という場合でも安心して読めます。他にも細々と変わっている部分があります。照度の変更とかはアイコンが変わっていたり、なれるまではちょっと戸惑います。

***

 新しいKindle PWを買ったので、古いKindle PWはお役御免かな、と思っていましたが、そうでもないようです。
 意外と、Kindle 2台併用は便利です。
 同時に複数の本を並行して読みたい、という場合に、「今はAを読む気分だからこっちのKindle」とか、そういう使い分けができます。いちいちホーム画面に戻って本を選ぶ必要がないので、紙の本を読むような感覚で読む本を選ぶことができます。
 1つ問題なのはKindleの見分け方で、第1世代と第3世代ではロゴデザイン等が違いますが、それでも何を開いているかは自分で覚えておく必要があります。
 例えば、待機画面に今読んでる本の表紙を表示する、みたいな機能があれば、Kindle複数台併用には便利だと思います。せっかく電源OFFでも表示を維持できる電子ペーパーなので、こういう機能があったほうが良いと思います。
 Kindle PWは安い端末ではありませんが、それでもセールのときに買うとか、モデルチェンジのときに買うとか、そういうふうに買っていけば2台目、3台目を買うことは不可能ではありません。人によっては値段を気にせず4,5台くらい買うことも可能でしょう。
 そういう場合、やはり待機時に表紙が表示されると、複数台併用がやりやすいはずです。ぜひともこの機能がほしい。