2018年3月14日水曜日

UARTのHALで直接割り込み

 久しぶりにHALでUSART割り込み使おうとして覚えてなかったのでメモ。

 HALのUSARTではTXE(Transmit Empty)とRXNE(Read Not Empty)のコールバック関数はない。これは、HALではHandleにバッファのポインタを渡し、HALの中でそこに(/から)転送する、という処理になっているため。
 なので、FreeRTOSのQueueとかで転送したい場合は、自分でUSARTx_IRQHandlerの中身を書く必要がある。

if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE))
{
    extern osMessageQId stdin_queueHandle;
    uint8_t data = huart1.Instance->DR;

    xQueueSendFromISR(stdin_queueHandle, &data, 0);

    return;
}

if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE))
{
    extern osMessageQId stdout_queueHandle;
    uint8_t data = 0;
    BaseType_t status = xQueueReceiveFromISR(stdout_queueHandle, &data, 0);

    if (status == pdPASS)
    {
        huart1.Instance->DR = data;
    }
    else
    {
        __HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE);
    }

    return;
}

 この処理はおそらくHAL_UART_IRQHandler(&huartx);の前に書く必要がある。
 また、TXEをチェックしてからRXNEをチェックすると、すでにRXNEがクリアされてる、という謎の挙動があるので、RXNEを先にチェックする必要がある。


 送信の場合は、xQueueSendでデータを入れた後に__HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE);でTXEを有効化し、割り込みの中でDRに転送している。TXEが有効でかつDRにデータがない場合、たぶんずーっと割り込みが回り続ける。なのでQueueが空の場合はDISABLEで停止し、QueueSendの後に都度ENABLEする、という手順にしている。

 受信の場合は、適当な箇所で__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);で割り込みを有効化しておく。RXNEは、受信しない限り割り込みが発生することもないので、ずっと有効状態でいい。


 注意点はあまり多くないが、割り込みのチェック(__HAL_UART_GET_FLAG)に渡す値と、割り込みの有効化/無効化(__HAL_UART_ENABLE_IT等)に渡す値は別の値であることに注意する必要がある。

STM32 HALのタイムアウト

 HALのタイムアウト処理にガッツリはまって原因が判明するまでものすごい時間かかったのでメモ。


 HALのブロッキングな関数は引数の最後にタイムアウト時間を指定できる。これは典型的にミリ秒を指定する。以降とりあえずミリ秒とする。
 この値は例えば5を指定すると、ブロック時間は「5ミリ秒未満」という処理になる。「5ミリ秒以上」では無い点に注意する必要がある。
 より正確的に言えば、「5回目のTickでタイムアウトする」という処理になる。

 例えば、SPIで1バイトずつポーリングする場合、HAL_SPI_Receive(&hspi1, &buff, 1, 1);というようなコードになる。この場合、1ミリ秒未満、あるいは1回目のTickでタイムアウトする。大抵の場合はこの処理で問題ないが、ある程度の確率で、Receiveを呼んだ直後にTick割り込みが発生する場合があり、その場合はSPIの1バイトを受信する前にタイムアウトエラーでSPI通信処理が終了する。この場合、当然ながら通信が正常に行われていないから、buffには何も書き込まれず、不正な値となってしまう。
 buffのデフォルト値としてビジーの値を書き込んでいると、相手はビジー終了を通知しているにもかかわらず、自分側はビジー値を読んでしまう、という結果になる。
 これはTick割り込みが発生する直前にReceiveを呼んだ場合にのみ発生するから、ある程度確率的な話になる。ということで、それなりの高頻度で発生しながら、よくわからない挙動を示す、という結果になる。


 まとめると、タイムアウトの値には 最低限待っていてほしい時間 + 1 を渡してあげようね、という話でした。


 ちなみに今回は10.5MHzでSPIを動かしてましたが、8bitの転送には760nsecくらいの時間で終わります。1msecの間の特定の760nsecの間で処理が行われるとエラーになる、なので、1300分の1くらいの確率でしょうか。1秒間に14回程度の処理を行い、1回の中でビジーチェックが30回程度行われるとすると、1秒に420回程度の機会があります。ということで、3秒程度でエラーが発生する計算です。実際には10秒前後でエラーを起こしていましたから、当たらずとも遠からず、といった感じです。

 しばらく調べていて、ようやくビジーチェックが怪しいぞ、とあたりを付けた時はSPIクロックを下げたりも試したのですが、一向に改善しませんでした。外の問題ではなく、内の問題だったので、全く見当違いのところを調べていたことになります。

2018年3月11日日曜日

TAR

 TARファイルのはなし。

 tar(4)
 tar の構造


 基本的に1つの変数を除いて、すべてヌル文字終端の文字列として格納されて、その基数は8進。ただし幅が足りない場合はヌル文字を省略できる、という感じらしい。
 ヘッダは512バイトの固定長、データは512バイトを1ブロックとするが、ヘッダに元の大きさが格納されているので、詰め物を除去できないとかの問題はない。
 ヘッダの先頭から1024バイトの0(ヌル文字)が連続したら、そこをTARの終点とする、みたいな処理になってる。厳密に実装するとヘッダを読み込んだらまず512バイトすべて0か探索して、もしそうなら次の512バイトを読んで、それも0かチェックする、みたいな処理になる。ちょーめんどい。今回はsizeの最初の文字がヌル文字なら終了する、というふうにした。

 8進数が実際に使われてるの初めて見た。

 C#だとこんな感じでファイル一覧を吐ける。
 null終端文字列を切る方法を知らないのでてきとーに関数作った。IndexOfしてSubstringしてるだけ。

using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read))
{
    bool loop = true;

    while(loop)
    {
        byte[] buffer = new byte[512];
        fs.Read(buffer, 0, buffer.Length);
                    
        if(buffer[124] != '\0')
        {
            string name = Encoding.ASCII.GetString(buffer, 0, 100);
            string mode = Encoding.ASCII.GetString(buffer, 100, 8);
            string uid = Encoding.ASCII.GetString(buffer, 108, 8);
            string gid = Encoding.ASCII.GetString(buffer, 116, 8);
            string size = Encoding.ASCII.GetString(buffer, 124, 12);
            string mtime = Encoding.ASCII.GetString(buffer, 136, 12);
            string chksum = Encoding.ASCII.GetString(buffer, 148, 8);
            string typeflag = Encoding.ASCII.GetString(buffer, 156, 1);
            string linkname = Encoding.ASCII.GetString(buffer, 157, 100);
            string magic = Encoding.ASCII.GetString(buffer, 257, 6);
            string version = Encoding.ASCII.GetString(buffer, 263, 2);
            string uname = Encoding.ASCII.GetString(buffer, 265, 32);
            string gname = Encoding.ASCII.GetString(buffer, 297, 32);
            string devmagor = Encoding.ASCII.GetString(buffer, 329, 8);
            string devminor = Encoding.ASCII.GetString(buffer, 337, 8);
            string prefix = Encoding.ASCII.GetString(buffer, 345, 155);

            UInt32 data_size_byte = Convert.ToUInt32(size.clip_null_char(), 8);
            UInt32 data_size_block = (data_size_byte + 511) / 512;


            Console.WriteLine(name.clip_null_char() + " " + data_size_byte);

            fs.Position += data_size_block * 512;
        }
        else
        {
            loop = false;
        }
    }
}

 普段使ってる圧縮展開ソフトではTARにできなかった。ファイル数が多すぎたみたい。動画を切り出した連番画像なので3700個もある。
 今回はbashのtarコマンドで作ったけど、*.jpgで全部突っ込んだら単純なASCII文字列でソートした順番になった。自然数順じゃないので、必要に応じて0埋めとかにリネームしておく必要があるかも。ちゃんとしたやりかたはありそうだけど。

 すべて512バイト単位ってのは便利でいいなぁ。格納時に気をつければシーケンシャルリードできるし、まるちめでぃあかーどでも早そう。ファイル名を指定したランダムリードはクッソ遅そう。今回はシーケンシャルで十分だけど。


 とりあえず眠いのでマタアシタ~

電光掲示板


 64x32パネルが届いたので、2枚並べて128x32pxのディスプレイにしてます。
 とりあえず複数パネル直列は簡単なバグフィックスで動きました。やったね!(デバッグかなり手間取ったけど)。
 あとSPIでまるちめでぃあかーど互換めでぃあの読み込みができるようになりました。CubeだとFatFsのリードオンリー構成ができませんが、書き込みしないならWriteは呼ばれないので未実装です。
 あとTJpgDecを組み込んでJPEGファイルから電光掲示板のVRAMに画像を転送できるようにしました。

 画像の読み込みは1枚あたり20msecから200msecくらいです。ものすごい幅がありますが、画像の読み込みが20msec程度で、それより多くかかるのは、ファイルシステムの探索に必要な時間っぽいです。
 今は連番のJPEGをメディアに入れてるだけなので、1枚開く毎にファイルシステムの探索が必要です。Motion JPEGとかで持てば良いんでしょうが、ファイルフォーマット面倒臭そうだなーと思ってます。とにかく複数のファイルに分かれているのが問題なのであって、それが解決できれば済むので、TARのFatFsラッパーとか作れば解決しそうな気がします。そっちのほうが簡単そうだなーって気がします。


 さーて、電光掲示板、何に使うかなぁ。普通に画像とか表示すると発熱だけでも半端ないので、せいぜいちょっとした文字を表示するくらいしか使えないです。うーぬ。

2018年3月9日金曜日

メモ:INA226の読み方

 ストリナのINA226モジュールの。

 定義
namespace
{
uint8_t INA226_device_address = 0x80;

enum INA226_register
{
    configuration_register = 0x00,
    shunt_voltage = 0x01,
    bus_voltage = 0x02,
};
}

 おまじない
I2C_HandleTypeDef *hi2c = &hi2c1;

 レジスタの読み出し
int8_t data[2] = { 0 };
uint16_t *ptr = (uint16_t *)data;
HAL_StatusTypeDef status = HAL_OK;

status = HAL_I2C_Mem_Read(
                hi2c, INA226_device_address, INA226_register::bus_voltage,
                I2C_MEMADD_SIZE_8BIT, data, sizeof(data), 10);

if (status == HAL_OK)
{
    int16_t value = __REVSH(*ptr);
    printf("voltage: %.2f V\n", value * 1.25 / 1000);
}

status = HAL_I2C_Mem_Read(
                hi2c, INA226_device_address, INA226_register::shunt_voltage,
                I2C_MEMADD_SIZE_8BIT, data, sizeof(data), 10);

if (status == HAL_OK)
{
    int16_t value = __REVSH(*ptr);
    printf("current: %.2f A\n", value * 1.25 / 1000);
}


 CubeでI2Cの初期化を行ってしまえば、あとはMem_Readで読み出せる。いちおうI2CのEEPROMがターゲットだろうけど、普通にI2Cデバイスの読み書きに使える。
 エンディアンが違うのでREVSHで変換する必要があるのが面倒。

 INA226はリセット時で約900Hzの継続サンプリングモードに入る。電圧/電流共にサンプリングされるので、初期設定とかは特に不要。電力を読み出すならそのあたりはいろいろ設定する必要があるはず。

 ほんとはINA226のドライバクラスを作るべきなんだろうなー。メンドクセ

2018年3月8日木曜日

電光掲示板

 久しぶりにLEDパネルで遊んでいます。


 制御基板はティアさんの頃のモノを流用しています。右下にちらっと写っているやつですが、そういえばこのブログでこの基板の写真って載せたたこと無いような…? ま、そのうち機会があれば。

 HUB75自体は、ライブラリ書くのも4回目くらいなので、結構スムーズに使えます。

 今回は、以前とは違い、かなりメモリを無駄遣いする構成になっています。
 以前は8bppIndexedと、それを展開するための大きなテーブルをRAMに持たせていました。その方法では、色の変更はVRAMにピクセル単位で書き込めばいい反面、パネルに転送する際に膨大なテーブルアクセスが発生するというデメリットが有りました。具体的には、96x96pxのパネルを50Hzで色深度3bitの表示を行った場合、1秒間に1.4^6回のデータ変換が必要です。データ変換はギリギリまで最適化しているので、1回の処理自体は大したことありませんが、回数が多いのでちりも積もればなんとやら、です。
 今回は、変換後のデータをRAMに展開しているため、set_pixel関数のコストがとても高いです。せめて転送時の計算コストが下がってくれれば、と思っていたのですが、実際にはかなり高負荷です。

 現在はGPIO.ODRにDMAで転送し、1回目の転送でデータを送り、2回目の転送でデータ+クロックを送り、これによりデータホールド期間を確保しています。
 が、DMAは代入しかできず、OR代入のような便利な機能はないので、あらかじめピクセルデータだけの配列から、クロックを含むデータへ変換し直す必要があります。この部分がかなり高負荷な感じです。
 この点は、例えば別のDMAでBSRRにクロックを書き込むとか、ビットバンドでODRを操作するとかすれば良いのかもしれませんが、現在はDMAをMEM2MEMでフリーランさせている状態なので、DMA間の同期が取れなくなってしまいます。
 今回の方法では、96x96pxであれば、VRAMは54KiB程度になります。これなら、2倍の領域(108KiB)をVRAMに割り当て、ここにクロックも埋め込んでしまうのも有りかな、と思っています。
 解像度を増やした時の余裕がありませんが、そもそも現状でも余裕が多い、という状況には程遠いですから、あまり解像度を増やすこともないでしょう。どうしても解像度がほしいというなら、マイコンを複数個並べたほうが楽でしょうし。

 以前はTIMでタイミングを作り、TIMのPWMをピクセルクロックとして使う、といったことも試しましたが、TIMとDMAの同期(位相)を取るのがかなり面倒なのと、適切な位置でTIMを止めるのも面倒で、あんまりあの方法はやりたくないなぁ、と思っています。


 今までの方法では、データソースは8bppIndexedであること、という制約がありました。8bppIdx画像はBitmapや、互換性のある特殊なPNGを使えば表現できますが、逆に言えばそれ以外のデータフォーマットがありません。
 一方で、今回はRGB888の上位3bitを取り出してVRAMに収納するので、大抵の画像フォーマットを扱うことができます。JPEGが使えるので、動画ファイルを扱うもよし、高圧縮ファイルをUSB経由で転送するもよし、いろいろな応用が効きます。


 ということで、実はまだset_pixelすらも作っていなかったりするわけですが、現状報告ということで。
// 64x32pxのパネルを1枚追加で買ってしまったので、はやく満足に使えるようにしなければ。

2018年3月6日火曜日

FSMC

 せっかくなので、STBee(STM32F103VE)のFSMCも試してみた。

 とんすけさんのブログを参考に。
 STBeeボード+2.4インチ液晶パネルYHY024006Aで動画再生60FPS超える|とんすけぶろぐ



 配線オバケ。ロジアナが16chなので、制御4ピンを含めると12chしか見れない。

///
volatile uint16_t *const out_ptr = (uint16_t*)0x6001fffe;

*out_ptr = 0x000;
*out_ptr = 0x001;
*out_ptr = 0x012;
*out_ptr = 0x123;
*out_ptr = 0x234;
*out_ptr = 0x345;
*out_ptr = 0x456;
*out_ptr = 0x567;
*out_ptr = 0x678;
*out_ptr = 0x789;
*out_ptr = 0x89A;
*out_ptr = 0x9AB;
*out_ptr = 0xABC;
*out_ptr = 0xBCD;
*out_ptr = 0xCDE;
*out_ptr = 0xDEF;
*out_ptr = 0xEF0;
*out_ptr = 0xF01;
*out_ptr = 0x012;
///
 こんなコードでFSMCに出せる。volatileをつけないとコンパイラ最適化で削除されるので注意。



 パラメータ次第だろうけど、だいたい3MHz程度かなーと言う感じ。データホールドを短くすればもうちょっとあげられるけど。

 HUB75をFSMCで出せればいいかなーと思っていたのだけど、ちょっと厳しそう。STM32F4なら20MHz程度まではGPIO+DMAで出せるし。