2018年3月31日土曜日

日本周辺LatLon/MGRSデータセット

 久しぶりの更新です。年度末駆け込みエントリ。駆け込むような内容じゃないけど。


 LatLonとMGRSのデータセットを作ってみました。LatLonとMGRSの変換コードとかを書く人はプログラムのチェックとかに使ってみてください。後述するように、日本周辺しかカバーしていないので、ノルウェーとかあの辺りはカバーしていません。

 変換は地理院地図で手作業でやってます。なのでどっかミスってるかも。簡単にチェックした限りでは大丈夫だと思いますが。

 地理院地図


 * データ範囲
 16Nから56Nまで、120Eから156Eまでの矩形。
 GZD(grid zone designator)で51から56、QからUの範囲です。

 1列目:データ番号
 2列目:北緯
 3列目:東経
 4列目:MGRSコード(4+4digit)

0 44.96186516 153.1542827 56TNQ12167872
1 18.7714042 143.0560952 54QYF16737678
2 46.43152517 123.8722651 51TWM67014236
3 34.57617141 151.1546819 56SLD30732759
4 46.80607313 138.1273513 54TTS80828762
5 50.97522775 150.444036 56ULB20565018
6 42.34817779 153.0308327 56TNM02538843
7 28.306578 123.9823699 51RWM96313155
8 34.93075751 146.0695517 55SDU15026575
9 40.8256208 149.5492312 55TGF14962252
10 51.74925858 131.2469221 52UFC55103553
11 45.85395448 134.5753307 53TML67037790
12 18.35453617 136.3214108 53QPA39612991
13 32.71391875 123.9806842 51SWS91901999
14 22.55759173 143.1845675 54QYK24639619
15 28.52762554 139.3176863 54RUS35395680
16 24.13443419 153.2556589 56RNM25976913
17 30.44526514 132.6153275 53RKP71007054
18 39.47509494 126.0155309 52SBJ43287375
19 26.53095451 140.4341724 54RVQ43623461
20 16.44206381 124.1708383 51QXU24991819
21 17.78883466 147.1096265 55QEV11616682
22 32.24670767 152.1072662 56SMA15906813
23 26.37228208 135.0063959 53RNK00631691
24 52.45606586 142.9168586 54UXD30241349
25 34.61370299 150.7056936 56SKD89643259
26 43.2473821 125.2140275 51TXH79739066
27 21.25670397 134.2805044 53QMD25355072
28 50.14047509 125.5887281 51UXR84975745
29 38.2773181 120.8249588 51SUC09743882
30 44.53458017 129.6540982 52TEQ51963145
31 20.37395729 135.9317315 53QNC97235313
32 19.98191373 143.7686473 54QYH89731187
33 55.45592321 135.3999101 53UNB25294560
34 46.61256741 135.1153209 53TNM08836211
35 16.49164717 131.7868028 52QGD97522537
36 42.99508508 128.4849595 52TDN58016039
37 46.84641436 144.9882321 55TCM46619006
38 41.34109508 128.2280872 52TDL35417691
39 29.76365353 143.5512955 54RYT46689532
40 53.87005009 154.8711544 56UPE23027068
41 49.49386466 144.6834442 55UCQ32248493
42 43.36724466 129.3866424 52TEP31320167
43 34.01364544 139.2004572 54SUC33836512
44 39.56023913 137.6020481 53SQD23548218
45 26.59305316 142.3502875 54RXQ34454207
46 50.01263422 147.4384549 55UER31414012
47 33.34293228 123.3076479 51SWS28628934
48 29.11023704 122.2793269 51RVN29882041
49 35.32740487 126.8219815 52SCE02031152
50 54.11905341 143.0157117 54UXE31749864
51 28.36128872 120.203023 51RTM25854040
52 35.67839253 122.7087131 51SVV73644831
53 18.86014498 125.0677693 51QYA17858662
54 51.57016518 141.9936974 54UWC68861370
55 53.49512464 139.52631 54UVE02232836
56 30.7216343 153.6808136 56RNU65189895
57 38.19484205 135.3814567 53SNC33402750
58 46.71081244 143.8655927 54TYS19027701
59 22.80537597 126.2490146 52QBL17592460
60 23.64109735 145.6131361 55QCG58541517
61 21.97749607 122.8904952 51QVE88693034
62 40.97157846 155.1397791 56TPL80043780
63 55.63164644 150.4769347 56ULG41166797
64 34.11977345 141.4619827 54SWC42607553
65 37.60524134 126.0650445 52SBG40916606
66 28.02268182 136.9628807 53RPM92970126
67 40.45923319 148.6484249 55TFE39768003
68 38.9547951 128.7149847 52SDJ75301179
69 16.36678463 148.7955161 55QFU91771035
70 43.17482058 122.3411788 51TVH46458043
71 32.27967715 127.3031004 52SCA40197270
72 31.18582796 147.4932757 55REQ46995030
73 52.48425027 120.1984457 51UUU09771859
74 30.2980949 150.3743381 56RKU47475473
75 39.22704779 143.4443602 54SYJ10994482
76 28.08577903 140.7503122 54RVS75460672
77 23.51874727 131.4197272 52QGM47070302
78 17.16803188 147.2879577 55QEU30629816
79 45.07334959 122.4427623 51TVK56139124
80 43.36859054 142.8083557 54TXP46510333
81 26.76202605 130.7391816 52RFQ72936125
82 26.2274406 154.6776849 56RPQ67580195
83 49.09472783 128.2637125 52UDV46243824
84 31.29198709 155.4945263 56RQV37446464
85 51.58023875 121.8532846 51UVT20541497
86 48.23330635 123.6334546 51UWP47034242
87 45.35690253 135.3736399 53TNL29262266
88 55.94809064 145.7523634 55UDC22080100
89 42.65429001 150.3310178 56TKN81232587
90 47.6126432 128.498543 52TDT62317337
91 47.05889011 135.313476 53TNN23801175
92 19.56330254 135.301159 53QNB31586318
93 20.41021724 151.3785403 56QLH30815771
94 21.36599532 133.9048001 53QLD86456304
95 44.69100228 129.04793 52TEQ03794862
96 17.15687852 133.3283063 53QLU22189767
97 34.47526915 150.9530766 56SLD12011675
98 43.6421101 153.4284252 56TNP34553221
99 54.50784753 151.1125419 56ULF77794166

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で出せるし。

2018年3月5日月曜日

USB Host MSC


USB FSでMSCのホストを行い、FatFsでUSBメモリのファイル一覧を表示する、というところまで動きました。
 CubeではUSB_OTG_HSのInternal FS PhyでHost_Onlyを選択し、USB_HOSTでClass For HS IPからMass Storage Host Classを選択します。またFATFSのUSB Diskをチェックします。
 あとはコードを生成し、FATFSを使用する部分を書くだけでUSBメモリを使用できるようになりました。基本的にCubeのコードに書き加える部分はありませんが、FatFsのマウントだけ自分でやる必要があります。このあたりはディスクの取り出し操作とかの兼ね合いもあるので、自分で処理したほうが安全だと言うことでしょう。

 USBホストの場合、基本的にマイコンのピンとUSBのコネクタを直結するだけで十分です。配線を引き回すならダンピング抵抗とかが有ったほうが良いかもしれませんが、今回は不要でした(オシロで見たわけじゃないですが)。
 USBの規格的には、HostはD+とD-を15kでプルダウンし、Deviceは機能に応じてD+かD-を1.5kでプルアップする必要があります。STM32F1を使ったSTBeeはこのあたりの回路が複雑でした。
 一方、STM32F4に内蔵されているUSB_OTGコアの場合、マイコン内にプルアップやプルダウンの機能を備え、DeviceやHostに応じて適切なプルが実行されるようです。OTGのHost/Deviceで使う場合でも、IDのプルアップが自動的に行われ、この状態に応じてD+/D-のプルも自動的に行われるようです。
 ということで、Full Speedに限って言えばマイコン外にコネクタ以外の部品を置かずにUSB機能が使えます。


 ベンチマークは行っていませんが、SDカードほどの速度は出ないと思います。例えばSPIモードは1bitで25MHzくらいまで出せますし、SDIOなら4bitで25MHzくらいまで出せますが、USB FSの場合は1bitで12MHzしか出せません。しかもUSBプロトコルの上でデータ転送を行いますから、純粋にデータ転送だけを行えるSDカードよりも効率は悪いはずです。ということで、SPIモードの5分の1とか10分の1くらい出れば十分じゃないかなぁ、って気がしてます。


 STBee F4miniもいろいろな用途に使えるようになってきて、そろそろもう1ランク上のボードも欲しくなってきました。144ピンクラスとか。
 ストリナさーん!フルサイズSTBee F4作ってくださーい!

 これで開発環境の構築が楽なら言うこと無いんですけどねぇ。NETMFとかどうなってるんだろう。あれ止めるの(or世に出すの)ちょっと早すぎたんじゃないかなぁ。


追記
 STBee F4miniはPD4をLED用に配線し、ピンヘッダには出してないんだけど、SDIOはPD4が必須なので、このままでは使うことができない。らしい。SDIOは使ったことがないので気が付かなかった。
 STBee F4miniはUSBの5Vがピンヘッダに出てなかったり、細かいところが結構微妙な感じ。

Cubeの続き

 FreeRTOSのスタックをCCMに移動する。

 FreeRTOSConfig.hの最後の方にあるUSER CODE BEGIN Definesの中に #define configAPPLICATION_ALLOCATED_HEAP 1 を追加する。

 freertos_heap.cというファイルを作り、以下の4行を書く。
////
#include "FreeRTOS.h"
#if( configAPPLICATION_ALLOCATED_HEAP == 1 )
uint8_t ucHeap[configTOTAL_HEAP_SIZE];
#endif
////

 MakefileのC_SOURCESに Src/freertos_heap.c \ を追加する

 STM32F405RGTx_FLASH.ldの.ccmramブロックの*(.ccmram*)の下に *freertos_heap.o を追加する。

 これでFreeRTOSで使うスタックがCCM領域に配置される。
 コンパイラによってはもっと簡単な指定方法が有るらしいけど、ウチの環境で動かなかったのでこういう方法になった。
 ちなみにCCM領域はDMA転送に使えないので、そういう場合はstaticで通常のRAM領域に配置するようにする。

 FreeRTOSのスタックとかヒープは紛らわしい。今回はスタック領域をCCMに移動したいのだが、そのスタックはヒープ領域から取ってくるので、マクロ名とか変数名はヒープになる。正しくは「FreeRTOSのヒープをCCMに配置する」なんだろうな。

***

 stdoutの転送速度が遅い問題。
 USB_CDC_stdout_funcのforの中身を以下のように書き換える。
////
static uint8_t buffer[32];
uint16_t count = 0;
BaseType_t dequeue_state = pdPASS;

while (dequeue_state == pdPASS && count < sizeof(buffer))
{
  dequeue_state = xQueueReceive(stdout_queueHandle, &buffer[count], 1);

  if(dequeue_state)
  {
    count++;
  }
}

if (count > 0)
{
  CDC_Transmit_FS(buffer, count);
}
osDelay(1);
////

 短いコードなので読めばわかると思うが、簡単に説明。
 そもそも、元の処理でネックになっていたのは、1文字の転送に1msecかかる、という点。例えばデバッグダンプが200文字あれば、表示に200msec必要になる。これは目で見てもゆっくり転送されているのがわかるような速度。

 下位レイヤのCDC_Transmit_FSは複数バイトの転送が可能なので、わざわざ1文字ずつ転送する理由はコードの単純さ以外に無く、デメリットのほうが多い。

 ということで、上記のような、一旦配列に読み込んでまとめて転送してやれば、一気に数十倍の高速化が可能となる。
 バッファサイズが大きければ大きいほど、大量の転送が発生したときの性能向上が見込めるが、実際の運用では数百ミリ秒毎に数十文字程度を超えることはまれなので、これが入る程度の大きさであれば十分。今回は最大32バイトだが、これはちょっと小さいかな、という程度。

 スタック/ヒープのところでも書いたが、CCM領域はDMA転送に使用できない。USBはDMAを使うので、FreeRTOSのヒープに配列が有るとDMA転送でハングアップする。なのでstaticをつけて通常のRAM領域に置くようにしている。

***

 改行コードをCRLFにした時に正常に処理できない問題。
 scanf, getcharの下に以下を追加する。
////
{
  char *p = strchr(str, '\r');

  if (p)
  {
    *p = '\0';
  }
}
////

 scanfではある程度の大きさを上限としてバッファに文字列を読み込むが、この際LFは削除して読み込む。しかしCRは削除しないので、CRLFを改行コードとした場合にCRが残ってしまう。
 ということで、strchrでCRが有るかを確認し、CRがあればそれをヌル文字に置き換える。
 コードとしては簡単だが、役割(行中のCRをを消す)の割に行数が多いので、前回は扱わなかった機能。

 ちなみに、ウチの環境ではLFだけで改行した場合、次のコマンドを入力せずに改行を送ると前回のコマンドが実行されてしまう。
 これは場合によっては利点だが、コマンドがトグル動作だったり、インクリメント動作だった場合、誤ったコマンドを実行してしまうことになる。ということで、CRを送ってコマンドが残らないようにしておいたほうが良い。

***

 タスク一覧を表示する。

 CubeのFREERTOS Configurationを開き、USE_TRACE_FACILITYとUSE_STATS_FORMATTING_FUNCTIONSをEnableにする。

 先に追加したブロックの下に以下のコードを追加する。
///
if (!strcmp(str, "tasklist"))
{
  char buff[10 * 45];
  osThreadList((uint8_t *)buff);
  printf(
      "Name            State   Priorty Stack   Num\n"
      "*******************************************\n"
      "%s"
      "*******************************************\n",
      buff);
}
///

 "tasklist"というコマンドを与えると、実行中のタスク一覧が表示される。

 例えば以下のようになる。
Name            State   Priorty Stack   Num
*******************************************
console         R       1       611     3
IDLE            R       0       115     4
LED_blink       B       2       999     2
USB_CDC_stdout  B       3       84      1
*******************************************
 フォントの関係で綺麗に表示されないと思うが、ターミナルソフト等なら問題ないはず。
 この一覧からわかるのは、USB_CDC_stdout, LED_blink, console, IDLEという4個のタスクがあり、USB_CDCとLED_blinkはブロック状態、それ以外はランニング状態である。ということ。
 ランニングというのはタスクが実行状態であることを示す。ブロックはキューやセマフォ、ウエイトを待っている状態であることを示す。
 USB_CDCはキューを使っているし、LED_blinkはosDelayを使っている。これを待っているため、ブロックになっている。
 consoleは実行中だが、これはtasklistコマンドを処理している最中。
 IDLEは特別なタスクで、常にランニング状態であり、ブロック状態になることは許可されていない。これは、OSのスケジューリングの条件分岐を省略してパフォーマンスの改善を図る目的によるものだが、詳しく説明するのは面倒なので省略。とにかく「IDLEというタスクがあり、止めることはできない」ということだけ知っていれば十分。

 プライオリティという項目(文字数の関係でスペルは不正)はいろいろな数字が有るが、この数字が大きいほど優先度が高いタスク。
 RTOSは優先度が高いタスクが常に実行されるので、高優先のタスクがランニングの時はそれより優先度が低いタスクは実行されない。上記の状態だと、USB_CDCとLED_blinkがブロック状態なので、それより優先度が低いconsoleを処理できる。IDLEもランニングだが、consoleがブロックになるまで、IDLEは実行されない。

 Stackの項目は、残りのスタック数が表示される。残スタックがゼロを下回ると、いわゆるスタックオーバーフローという状態になる(残数がゼロ未満=使える領域を超えている=オーバーフロー)。
 基本的にこの数値が極端に低くならないようにNew TaskやEdit TaskのStack Sizeを調整することになる。ただし、printfのような多機能な関数呼び出しを行うといきなり大量のスタックを消費するし、小規模な処理しか行わないならスタックの使用量も少なく済む。
 LED_blinkとconsoleはStack Sizeを1024にしているが、LED_blinkはosDelayやGPIO操作しか行っていないから、スタック使用量は少ない(=残数が多い)。一方で、consoleはprintf等を使っているから、スタック使用量が多い(=残数が少ない)。
 USB_CDC_stdoutは残数が極端に少ないが、Stack Sizeは128だから、実際には3割程度しか使われていない。ということで、十分とは言えないまでも、まだ余裕がある状態。
 IDLEもStack Size 128で作成されているはずだが、13しか使われていない。これは無限ループといった軽量な計算しか行っていないため。


 ちなみに、この処理(osThreadList)はちょっとした副作用があるので、極力使用しないことが好ましい。あくまで「よしよし、ちゃんと動いてるな」と悦に入るための機能。
 また、バッファサイズの10*45というのは、タスク1個あたり余裕を見て45バイト、それが10個分、という設定。タスクを20個作るなら25*45くらいにする。


******

 ということで、stdoutが遅いのと、stdinでゴミが残る問題を解決し、悦に入るためのコマンドも実装したし、使い道の少ないCCMをFreeRTOSに活用できるようにもした。
 これによりメインメモリ128kBのほとんどをマイコンの処理に使えるようになり、かつFreeRTOSが使えるスタック領域も数十kB確保したので、タスクやキューも相当な数を使えるようになった。

 あとはコレで何を作るか、という問題だが、何を作るかなぁ。。。

2018年3月4日日曜日

CubeMXでプロジェクトの作り方

 久しぶりにCubeMXで新規プロジェクトを作ったらとっても簡単だったのでメモ。

 ターゲット:STBee F4mini
 目標:Lチカ他

 コマンドラインでビルドするので、arm-none-eabiその他必要なものはあらかじめ入れておくこと(Win10ならWSLのUbuntuでaptから入れれる。古いけど)。



 New ProjectでMCU選択画面を出す。
 Part Number SearchでMCUを入力する。STBee F4miniはSTM32F405RGが乗ってるので、それを入力する。
 そうするとMCUs Listに出てくるので、ダブルクリックで選択する。

***

 メニューのProject→Settingsで設定画面を開く。


 Project NameとProject Locationを選択する。Toolchain / IDEをMakefileにする。
 Project NameとLocationは最初に設定すると変更できないのでたいぽとかしないように。CubeMXのプロジェクトをコピーすれば使い回しができるけど、Project Nameにtemplateと入れてしまったのはやっちゃったパターン。ここは後から変更ができないから、ある程度潰しの効く名前にしておく必要がある。

 それと、Code GeneratorタブでGenerate peripheral initialization as a pair of '.c/cf' files per peripheralにチェックを入れておく。
 あとはOkで設定を閉じる。

***
 

 左のPeripheralsからRCCを開き、High Speed ClockとLow Speed ClockをCrystalにしておく。今回LSEは使わないけど、誤ってPC14/PC15をGPIOとして使って水晶を破壊しないようにするため、設定しておく。

*** 



 Clock Configurationタブを開く。
 Input frequencyをボードの水晶の値(12MHz)に設定する。PLL Source MuxをHSEに設定し、/Mを12にする。*Nを336に設定する。System Clock MuxをPLLCLKに設定する。APB1 Prescalerを4に、APB2 Prescalerを2に設定する。
 赤いところがなければOK。HCLK to AHB bus, core, memory and DMAが168になっていれば大体OK。

***

 次にGPIOの設定を行う。オンボードのLEDとスイッチを使いたい。LEDはPD2に、SWはPA0に接続されている。


 Pinoutタブに戻り、左下のPA0をクリックしてGPIO_Inputを選択し、右クリックでEnter User Labelを選択する。テキストボックスに"USER_SW"と入力する。
 同じように右上のPD2を、GPIO_Outputに設定し、USER_LEDとする。
 また、左のツリーからMiddleWaresのFREERTOSを開き、Enabledにチェックを入れる

***

 Configurationタブを開き、青字に白でMiddlewaresと書いてあるところの、FREERTOSと書いてあるボタンをクリックする。


 Task and Queueというタブを開く(最初に開くとこのタブのはず)。中段のAddボタンを押して、New Task画面を出す。
 Task NameとEnter Functionに適当な名前を設定し、PriorityにosPriorityBelowNormalを設定する。
 New TaskをOKを閉じ、FREERTOS Configuration画面もOkで閉じる。

***

 メニューのProject→Generate Codeをクリックする。Warning: Code Generationというメッセージが出てきて、When FreeRTOS is used, it is(以下略 と表示されるが、とりあえず気にせずYesを押す。ちなみにこれはいつまでたっても出て来る。
 プログレスバーが進んでThe Code is successfuly(以下略 というダイアログが出たら、Closeで閉じる。
 さっき設定したLocationの中にNameのフォルダができてるはず。

 以降はソースコード類の編集作業になる。

***

 とりあえず、Makefileを開いて$(BINPATH)/$(PREFIX)gccみたいな行を探す。この$(BINARY)の後ろにあるスラッシュが邪魔なので、取り除く。
 また、下の方にLDFLAGS = $(MCU) -space=nano.space (以下略 という行があるが、この内-space=nano.spaceを除去する。これは開発環境による。ウチの環境では消したほうがうまくいく。特にprintfでのfloatの表示可否に関係する部分。

 とりあえずMakefileの修正はこの2点。
 これでビルドが通るようになるので、makeを実行する。うまくビルドできるとbuildフォルダにProject Nameで設定した名前の.binとか.hexとか.mapが生成される。
 ただ、まだLチカとか書いてないので、このバイナリを焼いても特に何も起きない。

***

 いよいよLチカのコードを書く。
 Srcフォルダのfreertos.cを開き、LED_blink_funcという関数を探す。
 for(;;)の中にosDelay(1);とだけ書いてあるはずなので、以下の2行に置き換える。
HAL_GPIO_TogglePin(USER_LED_GPIO_Port, USER_LED_Pin);
osDelay(HAL_GPIO_ReadPin(USER_SW_GPIO_Port, USER_SW_Pin) ? 100 : 200);

 それと、上の方に戻り、USER CODE BEGIN Includesというところが有るので、その下に#include "stm32f4xx.h"を書き加える。
 ちなみに、USER CODE BEGIN hogeとUSER CODE END hogeの間に書かれたコード以外は、CubeのGenerate Codeで削除される。基本的にBEGINとENDの間に全て書くようにする。

 とりあえず、この2点の変更をしたら、ファイルを保存して再びビルドする。
 これでLチカのバイナリができたので、何らかの方法でSTBee F4miniに書き込む。そしてリセットすると、LEDが点滅するはず。また、スイッチを押している間は点滅が早くなるはず。

 ということで、以上でLチカは終了。

***

 次はUSBを使ってシリアルポートを開くようにする。

 Cubeに戻って、PinoutからUSB_OTG_FSを開き、ModeにDevice_Onlyを設定する。MiddleWaresのUSB_DEVICEにあるClass For FS IPにCommunication Device Classを設定する。
 Clock Configurationタブを開くとダイアログが出てくるはずなので、とりあえずNoで閉じる。
 Main PLLブロックの、/Qが赤くなっているはずなので、これを7に設定する。右側の48MHz clocksが48になっていて、赤くなければOK。

 Configurationタブで再びFREERTOSの設定を開く。Task and Queueタブで、defaultTaskをダブルクリックする。これの名前を変更する。とりあえず名前はUSB_CDC_stdoutにした。同じようにEntry FunctionもUSB_CDC_stdout_funcにした。

 各ダイアログを閉じて、Generate Codeで生成する。

***

 再びfreertos.cを開き、USB_CDC_stdout_func関数に移動する。
 MX_USB_Device_Init()というのが追加されてるはず。

 とりあえず、for(;;)の中のosDelay(1)を以下の3行に変更する。
osDelay(500);
uint8_t str[] = "hello USB CDC!\n";
CDC_Transmit_FS(str, sizeof(str));

 再びUSER CODE BEGIN Includeに移動し、#include "usbd_cdc_if.h"を追加する。

 またビルドし、書き込む。リセットすると、OSが何らかのUSBデバイスを検知したと音を出すはず。ドライバのインストールが正常に行われれば、新しいCOMポートが追加されるので、teratermとかでそれを開く。
 そうすると、1秒毎に先程設定した文字列が表示されるはず。

 ということで、めでたくUSB CDCで文字を表示できるようになった。

***

 しかし、いちいちCDC_Transmit_FSを呼ぶのは面倒だし、あらかじめ決めておいた文字列しか表示できないのは使い勝手が悪い。ということで、printfを使えるようにする。


 CubeでFREERTOSの設定画面を開き、こんどは下の方に有るAddを押す。するとNew Queueダイアログが出てくるので、NameやSizeを設定する。

 続いて、上のLED_blinkをダブルクリックし、Edit Taskを表示する。Stack Sizeは128になっているはずだが、これを大きくする。とりあえず1024にした。

 そしてGenerate Codeで生成。

***

 またまたfreertos.cを開く。
 USB_CDC_stdout_funcのforの中身を、以下のように書き換える。
uint8_t ch = 0;
xQueueReceive(stdout_queueHandle, &ch, portMAX_DELAY);
CDC_Transmit_FS(&ch, 1);
osDelay(1);

 LED_blink_funcのforの前でint counter = 0;で変数を宣言し、forの中でprintf("%d\n", counter++);のようにしてprintfで変数を出力する。

 下の方のUSER CODE BEGIN Applicationの中に以下のコードを追加する。
_ssize_t _write_r(
  struct _reent *r,
  int file,
  const void *ptr,
  size_t len)
{
  uint8_t *p = (uint8_t *)ptr;
  int i = 0;

  if (__get_IPSR() == 0)
  {
    for (i = 0; i < len; i++)
    {
      xQueueSend(stdout_queueHandle, p++, portMAX_DELAY);
    }
  }
  else
  {
    for (i = 0; i < len; i++)
    {
      xQueueSendFromISR(stdout_queueHandle, p++, 0);
    }
  }

  return (i);
}

 これでビルドして実行すると、ターミナルにカウンタが表示される。スイッチを押せば、表示頻度が高くなる。

 これでprintfが使えるようになった。整数や浮動小数点の表示もできる。
 ちなみに、上記ではCDC_Transmit_FSは1バイトだけ転送しているが、この方法では最大でも1秒間に1000文字しか転送できない。しかしこれを改善しようとするとコード量が増えてしまうので、今回は扱わないことにした。気が向いたら別のエントリで書くかも。

***

 printfがあるのだから、scanfとかも使いたい。ということで、再びCubeへ。
 New Queueでstdin_queueを作る。Queue SizeとItem Sizeはstdout_queueと同様。
 New Taskで新しいタスクを作る。とりあえずNameはconsoleとでもしておき、Entery Functionはconsole_funcとしておいた。Stack Sizeは1024にする。PriorityはosPriorityLowとした。

 再びジェネレートッ!

***

 またまたfreertos.cに戻ってまいります。

 とりあえず#include "string.h"を追加する。

 さきほどの_write_r関数の下に、以下のコードを追加する。
_ssize_t _read_r(
  struct _reent *r,
  int file,
  void *ptr,
  size_t len)
{
  uint8_t *p = (uint8_t *)ptr;
  uint8_t data;
  int i;

  for (i = 0; i < len;)
  {
    xQueueReceive(stdin_queueHandle, &data, portMAX_DELAY);
    p[i++] = data;

    if (data == '\n')
    {
        break;
    }
  }

  return (i);
}

 
 console_funcのforの中身を以下のようにする。
char str[100];
scanf("%99[^\n]%*[^\n]", str);
getchar();

if(!strcmp(str, "hoge"))
{
  printf("piyo!\n");
}

if(!strncmp(str,"sum ", 4))
{
  int a,b;
  if(2==sscanf(str+4,"%d %d", &a,&b))
  {
    printf("result:%d\n", a+b);
  }
}

 あとついでなので、さっき追加したint counterとprintfは削除しておく。

***

 次にusbd_cdc_if.cを開く。
 CDC_Receive_FS関数のreturnの前に以下のコードを追加する。
extern osMessageQId stdin_queueHandle;
uint32_t i = 0;
for (i = 0; i < *Len; i++)
{
  xQueueSendFromISR(stdin_queueHandle, &Buf[i], 0);
}

 それと上の方に#include "cmsis_os.h"を追加する。


 これでターミナルに入力した文字を取り出せるようになった。ということでビルドして書き込んでみよう。
 ターミナルで"sum 1 2"と入力すれば"result:3"と表示される。hugaと挨拶すれば、piyoと返事をしてくれる。
 なお、sumは動くがhugaが動かない、という場合はターミナルソフトの改行コードを確認してほしい。今回のプログラムはLFでしか動作しない。

***

 さて、最初はLEDが点滅するだけだったが、いよいよ挨拶ができるようになったし、電卓としても使えるようになった。

 前述の通り、送信速度はそれほど早くないし、改行コードの問題も残っている。しかしこれらを修正するのはさほど大変な作業ではない。


 今まではUSARTの初期化とかが面倒だったので、昔作ったプロジェクトを使いまわしていたのだけど、ライブラリが古いまま使われ続ける、という問題が有った。
 それと昔のCubeではMakefileやリンカスクリプトは自分で用意する必要があったはず。
 だけどその必要がなくなったなら、その都度プロジェクトを作り直すのもありだなぁと思い始めた次第。


追記:2018/03/05
 [fix]_write_rのポインタの扱いを修正
 [update]_write_rで割り込みから呼ばれている場合はFromISRを使うように変更

 割り込みからprintfなんて使わねぇよ、と思ってFromISRには対応してなかったんだけど、CubeのUSBデバッグ出力がISRからprintfを使っていたので、それに対応。


追記:2018/03/05
 上記の手順に従って、STBee(STM32F103VE)も試してみた。ちゃんとUSB CDCとかも使えた。
 ただしSTBeeはUSBのプルアップを内蔵していないので、PD4をLOWにしてプルアップを有効にする必要がある。これはUSB_CDC_stdout_funcのループの前でWritePinでUSBを有効にするか、あるいは最初からLOWで初期化するか。
 それとリンカスクリプト(STM32F103VETx_FLASH.ld)のMEMORYブロックでFLASHの行を FLASH (rx)      : ORIGIN = 0x8000000 + 12k, LENGTH = 512K - 12k のように書き換える必要がある。これはSTBeeの先頭12KiBはUSB DFUのプログラムが入っているため。またmain.cのUSER CODE BEGIN SysInitで SCB->VTOR = 0x08003000; として割り込みベクタを変更する必要もある。

 STBeeってF4miniより数百円高い割にクロックが低かったり、RAMやROMが少ないけど、GPIOの数は魅力的だよなぁ。

 ちなみにROMは68kほど、RAMは18kほど使っているらしい。ROMは400k、RAMは46kほど使える。
 F4の方は、ROM70kほど、RAM30kほどを使っている(USB Host MSCも含む)。残りはROM954k、RAM162kくらいか。

2018年3月3日土曜日

STM32F4 USART1の送信でUSBが切断される問題

 とりあえず、原因と対症療法がわかったので、メモしておきます。


 まず原因ですが、STM32F405xx/STM32F407xx Datasheetによると、USART1_TXで使っているPA9には、OTG_FS_VBUSという機能が割り当てられています。これは、USBの5Vをセンスすることにより、USBが接続されているか否かを判断するための入力ピンです。USBメモリ等はUSBを接続しないと電源が入りませんが、プリンタ等の外部電源で動作するものはUSBの接続如何にかかわらず電源が入っていますから、USBの挿抜状態を知るための方法が必要になります。
 で、STM32F4のUSBには「VBUSセンスを無効化する」というオプションがあります。というよりは、CubeはデフォルトでVBUSセンスを行いません。しかし、実際にはPA9をVBUSセンスに使おうが使うまいが、Cubeから出力されるコードは(PA9の初期化を除けば)全く同じコードが出力されています。つまり、VBUSセンスON/OFFレジスタは操作していません。
 レジスタの初期値は0ですが、0の場合はVBUSセンスを行い、1の場合はVBUSセンスを行いません。つまり、レジスタの操作を行わない=VBUSセンスを行う、ということです。
 そのままで初期化を行うと、マイコンは当然VBUSを監視します。しかし、すでにそのポートはUSART1_TXで初期化されていますから、外部電圧を知ることはできません。また、PA9とVBUSの接続もありません。
 この状態でVBUSを読み取ると、通常の状態はUARTのリセット値であるHighが読み出されます。Highを読み出すと「電圧がかかっているぞ。じゃぁUSBが接続されているんだな」ということを認識してマイコンはUSB接続を行います。一方、USART1_TXから何らかの値を出すと、ビット0を出すたびにPA9がLowに駆動され、VBUSもLowになりますから、マイコンはUSBが切断されたと認識し、USBの切断処理を行い、それによりホスト側もUSBが切断されたと認識するわけです。

 USB Full Speed/High SpeedはUSB D+をプルアップすることによりホスト側にUSBデバイスが存在することを通知しますが、USBデバイスの存在を通知したくない時はD+を開放すれば、ホスト側のプルダウンによりLowに固定されます。
 STMはVBUSがLowになるとD+を開放し、VBUSがHighになるとD+をプルアップします。ということで、VBUSに十分低速なデジタル波形が流れると、それがそのままD+に出力されてしまいます。それが前回の「USART1_TXがUSB D+に漏れたような」波形というわけです。


 さて、対症療法ですが、MX_USB_DEVICE_Init();の後でUSB_OTG_FS->GCCFG |= USB_OTG_GCCFG_NOVBUSSENS;によりVBUSセンスを停止することができます。

 今のところバスパワーでしか使っていませんが、セルフパワーで動かしても、ホストからのリセット信号が来ない限りはUSBは動きませんし、一定時間ホストから通信がなければ切断されたと認識するはずなので、VBUSセンスを行わなくても問題ないかな、と思っています。VBUSセンスだけを使うと、ACアダプタやモバイルバッテリーのような給電機能しか持たないホストに接続した際に問題が起きますから、あまり強くVBUSセンスには依存していないはずです。じゃぁそもそもVBUSセンスいらねーじゃん。という話になってしまうんですが。。。

 ちなみに、Nucleoとかの回路図を見てみると、PA9はUSB V+に10kOhm程度で接続しているようです。PA9は5Vトレラントなので、5Vを直結しても問題ありません。ただ、サージとかの保護を兼ねて10kOhmを挟んでいるようです。
 また、PA10はUSB IDに直結しています。ここはプルアップで固定し、必要に応じてGNDに地絡するだけで、配線長も自分のUSBコネクタ止まりですから、あまり保護はしなくて良さそうです。
 ということで、PA9をVBUSに接続し、必要に応じて(OTGを使うなら)PA10をUSB IDに接続しておく、という感じで使うのが良いようです。USART1はPB6/7に割り当てられるので、I2CやCANとの兼ね合いもありますが、PA9/10は出来る限り使わないのが良いのかもしれません。


 あと、なぜか唐突にHSIの初期化コードがCubeの出力から消えました。HSIの初期化コードが出力される問題は、STBee F4miniを買った当初に散々悩まされた問題です。クロック48MHzの設定を適切にやったからかな、とも思いましたが、たしか昨日48MHzを設定した時点ではHSIのコードは残っていたはずだし、何が原因かわかりません。
 今のところHSEしか使っていないので、HSIを殺すコードは有ってじゃまになるものでもないので、しばらくは残しておこうと思います。


 ということで、とりあえずUSB CDC VCPが最低限動くようになりました。あとはFreeRTOSのQueueとかを使って送受信できるようにして、それをシステムコールに渡したりできるようになれば、USB1本でDFU書き込みとprintfデバッグができるようになります。
 MSCとCDCを併用できればmbed的な使い方ができて面白いかなーと思ったんですが、Cubeではそういうコードは出せないようです。そういうのが必要なら自分でいろいろ書く必要があるみたいですね。
 STM32F4のFlashの512KiBくらいをストレージとして、MSCでLuaスクリプトファイル投げたらそれを実行できる、みたいな仕組みがあれば便利かなーと思ったんですが。スクリプトファイルにこだわらないなら、CDC経由でテキストデータを投げたり、Vimを動かして、というのでもいいですが、それはそれで面倒な感じ。

2018年3月2日金曜日

STBee F4mini CDC

 STM32F4のUSBでFull Speed/DeviceのCDCを試してみた。
 どーせ動かないんでしょ~、と思ってたけど、あっさり動いてしまって拍子抜け。

 STM32CubeMX その6- USB-CDCの実装 – メモたんく
 ここを参考に、STM → PCは問題なく動いた。
 たぶんPC → STMも、1バイトづつだけど、ちゃんと動いてる。

 ただ、USART1.TXがUSB DPに流れ込む、という謎の挙動が発生していて、悩んでいる。



 最初、なんで動かないんだよ~と、悩んで、そういえばZEROPLUSのロジアナってUSBキャプチャもできたよな、と思ってキャプチャしたら、USARTがUSBに流れ込んでいる、という現場を発見した次第。
 上の画像は、上半分がUSARTで、そのさらに上半分がPC→STMの流れ、下半分がSTM→PCの流れで、画像下半分はUSBのキャプチャ。USARTは0.1152MBaudで、USBは12Mbaudなので、ボーレートはほぼ100倍違う。USB早いなぁ。
 黄色の、STM→PCのデータが数マイクロ秒程度遅れてオレンジのUSB DPにあらわれているのが見える。
 数マイクロ秒は短い時間だけど、光速だと1km前後になるから、配線での遅延ということはないはず。ということは、マイコンの中で何らかの遅延が発生し、それが外に漏れている、ということなのかもしれない。ただ、それにしたって数百クロックのディレイだから、ちょっと大きすぎるんじゃないかな、という気がする。何が原因だろうか。

 STBee F4miniの回路図を見てみると、USB周りはSTBee(STM32F1)とかに比べて非常に簡略化されている。F1はUSBを使うのにトランジスタ等を使った外部プルアップが必要だった。F4はダンピング抵抗で直結されてるだけ。
 ただし、PA10(USART1.RX)がUSB IDに直結されていて、ここは注意する必要がある。基本的にUSB IDはフロートなので無視していいが、USB OTGケーブルを差し込むとIDがGNDに短絡するので、USART1に接続している相手側のTXがGNDに短絡することになる。これの対策としては、1) 基板のパターンを削ってしまう 2) USB OTGケーブルを使用しないように注意する のいずれかしか無いと思う。どっちも嫌な感じ。

 しかし、今回問題になっているのはPA9がPA12に流れ込んでいる、という点であり、PA10はどうでもいい。回路図を見た感じではPA9とPA12には何の関係もなさそうだし、たかが50kHz未満の信号が隣のパターンに回り込むはずもないだろうし。

 ということで、今のところこの問題は謎のまま。今週はSTMに触りすぎてるので、もうちょっと時間を掛けてゆっくりと解決したい感じ。


 ところで、CDCってCommunication Device Classの略なんだけど、アクション映画とかよく見る人間としては、アメリカ疾病予防管理センター(Centers for Disease Control and prevention)のほうが強く連想される。よくゾンビ映画とか、ウイルス系のなんやらで出て来る機関。

 あと、最近のTeraTermって接続断しても自動的に再接続されるのね。昔は手動で切断→接続をやる必要が有った気がする。あと運が悪いとPCを再起動しないと再接続できなくなるような問題も有った気がする。
 マイコンのUSBでVCPを使うと再接続の手順が非常に面倒でデバッグに向かない、と思っていたけど、自動で再接続されるならUSBを使うのもありかなー。


追記
 オシロで確認してみました。赤がUSART1.TXで、黄がUSB D+です。
極めてアナログ的な波形ですね。
 傾向としては、USART1.TXから6usec程度遅れてUSB D+にも現れている感じです。立ち上がりが急峻で立ち下がりがゆっくりなのは、I2Cを始めとしたプルアップ系のアナログ波形とは逆の動作です。
 アナログ的な部分を見る時は、ロジアナだけでは足りませんね。ま、今回はオシロで見たところで何か改善するわけでもないですが。

CANの割り込み(ちょっと微妙ver)

 STM32F4/HALのCANで割り込みを使ってみた。ちょっと微妙な感じ。

 まずCubeでCanRxMsgTypeDefのキューを作る。今回はCAN1_FIFO0_RX_queueという名前で、osMessageQId CAN1_FIFO0_RX_queueHandleという変数経由でアクセスする。これはQueueHandle_tのtypedefで、QueueHandle_tはvoid*のtypedefになっている。ポインタのtypedefなので、ポインタ渡しの関数は直接渡す。

 最初にhcan1.pRxMsgに適当なポインタを設定しておく。

static CanRxMsgTypeDef RX_MSG = { 0 };
hcan1.pRxMsg = &RX_MSG;


 割り込みハンドラ(CAN1_RX0_IRQHandler)でHAL_CAN_IRQHandlerが呼ばれる前に、以下の処理を行う。

if (hcan1.pRxMsg)
{
    hcan1.pRxMsg->StdId = 0x80000000;
    hcan1.pRxMsg->ExtId = 0x80000000;
}


 コールバック(HAL_CAN_RxCpltCallback)で以下の処理を行う。

if (hcan->Instance == CAN1)
{
    if (hcan->pRxMsg && (!(hcan->pRxMsg->StdId & 0x80000000) || !(hcan->pRxMsg->ExtId & 0x80000000)))
    {
        BaseType_t higher_priority_task_woken = pdFALSE;
        xQueueSendFromISR(CAN1_FIFO0_RX_queueHandle, hcan->pRxMsg, &higher_priority_task_woken);

        __HAL_CAN_ENABLE_IT(hcan, CAN_IT_FOV0 | CAN_IT_FMP0);

        portEND_SWITCHING_ISR(higher_priority_task_woken);
    }
}

 コールバック関数ではどのFIFOから読み出したかを判断できないので、FIFO0とFIFO1を併用する場合はこのような手段で判断する必要がある(もっとスマートな方法がありそうな気がするけど)。
 Idは32bit符号なしの変数で、実際に使われるのは11bitか29bitなので、最上位ビットが1になることはない。ということから、あらかじめ最上位ビットをセットしておき、これがクリアされたら新しい値が設定された、と判断している。Stdを受信した場合はExtが書き換わらず、Extで受信した場合はStdが書き換わらないので、両方をチェックする必要がある。!(Std & Ext & 0x8000000)でも良いかもしれないが、未確認。

 コールバックの中でキューに送信し、キューに入れたあとで再び割り込みを有効化し、最後にタスクスイッチを呼んで終了する。

 HALの割り込み処理は、一旦割り込みがかかると以降の割り込みを停止してしまう。そのため自分で再び割り込みを有効化する必要がある。
 これは、メッセージを保管する場所が1つしか無いため、メインループ側でメッセージを取り出す前に新しい割り込みがかかると、古いメッセージが破壊されてしまうため、このような実装になっていると思われる。


 実際に使う時は、HAL_CAN_Receive_IT(&hcan1, CAN_FIFO0);で割り込みを開始する。
 あとはメッセージを受信すればキューに転送されるので、適当なタスクでキューをチェックするなりすれば良い。

 
 HALの処理はコード数が長く、またQueueの転送もかなり大きな範囲を転送する。ということで、CANx_RXy_IRQHandlerが呼ばれると、かなりの期間のリソースを専有してしまう。CANは3段FIFOがあるので、CAN RXの割り込み優先度は若干低めに設定し、タイミングクリティカルな割り込みを妨害しないように注意する必要がある。
 IRQHandlerの最初と最後でGPIOをトグルした所、8usec程度のパルスが出力された。168MHzなので、1.2kクロックくらいか。意外と短い。けどまあ、割り込みハンドラの中でやる処理としては長い方だと思うので、その点は覚えておくこと。

***

 ということで、やっとCANの送信、フィルタ、受信割り込みが最低限使えるようになってきた。月曜から初めた気がするから、1週間丸々使ったのか。何か良い応用先探しておかないとなぁ。

2018年3月1日木曜日

CANのフィルタ

 STM32のCANのフィルタがやっと理解できたので、メモしておきます。
 昔、フィルタの動作が理解できなくてちょっとトラウマ気味でした。

 なお、CANのフィルタにはマスクモードとリストモードがありますが、今回はマスクモードだけを試しました。


 さて、CANのフィルタですが、「何に対してのフィルタなのか」というのが重要です。
 結論から言うと、リファレンスマニュアルで「CAN受信FIFOメールボックス識別子レジスタ(CAN_RIxR)」と呼ばれるレジスタに対してのフィルタになります。決して、メッセージIDに直接フィルタリングを行っているわけではありません(理解してからリファレンスマニュアルを読んでみると、そのように読めるのですが、知らずに読むと難解でした)。
 このレジスタは32bit幅で、bit0が予約、bit1がRTR、bit2がIDE、bit3-15がExId0-12、bit16-20がExId13-17、bit21-31がExId18-28/StId0-10が割り当てられています。


 一方で、CAN_FilterConfTypeDefにはFilterIdHigh, FilterIdLow, FilterMaskIdHigh, FilterMaskIdLowという、32bitのレジスタが4本ありますが、この4本はすべて下位16bitのみ使用されます。
 IdLowはRIxRの下位16bitに割り当てられ、IdHighはRIxRの上位16bitに割り当てられ、それぞれのIdとMaskでビット演算を行い、その結果に応じてFIFOに入れる、という動作になります。具体的には、それぞれ32bitに結合したIdとMaskがあるとして、RIxR & Mask == Id & Maskが真ならFIFOに入れる、という動作です。


 とりあえず動作確認のために以下のようなコマンドを追加して確認しました。

if (!strncmp(str, "filt ", 5))
{
    int flag = 1;

    uint32_t FIFO_number = 0;
    uint32_t filter_number = 0;
    uint32_t mask_low = 0;
    uint32_t mask_high = 0;
    uint32_t id_low = 0;
    uint32_t id_high = 0;

    if (flag)
    {
        flag = 6 == sscanf(str + 5, "%lu %lu %lu %lu %lu %lu", &FIFO_number, &filter_number, &id_low, &id_high, &mask_low,
                            &mask_high);
    }

    if (flag)
    {
        CAN_FilterConfTypeDef filter = { 0 };

        filter.FilterIdHigh = id_high;
        filter.FilterIdLow = id_low;
        filter.FilterMaskIdHigh = mask_high;
        filter.FilterMaskIdLow = mask_low;
        filter.FilterFIFOAssignment = FIFO_number;
        filter.FilterNumber = filter_number;
        filter.FilterMode = CAN_FILTERMODE_IDMASK;
        filter.FilterScale = CAN_FILTERSCALE_32BIT;
        filter.FilterActivation = ENABLE;

        flag = HAL_OK == HAL_CAN_ConfigFilter(&hcan1, &filter);
    }

    printf("%s\n", flag ? "ok" : "error");
}

 filtにつづいて6個の引数を渡して、それをフィルタに設定する、というものです(使っていて思ったけど、こういうときの引数は10進より16進のほうが楽)。

filt 0 0 2 0 2 0 リモートフレームだけを受信する
filt 0 0 0 0 2 0 データフレームだけを受信する
filt 0 0 4 0 4 0 拡張フレームだけを受信する
filt 0 0 0 0 4 0 標準フレームだけを受信する
filt 0 0 0 32 4 65504 標準フレームでIdが1のフレームだけを受信する
filt 0 0 4 0 4 61440 拡張フレームで上位4bitがドミナントのフレームだけを受信する

 とりあえず、引数と結果はこのような感じになります。
 例えば、RTRだけをマスクし、RTRが0という条件にすればデータフレームのみを受信しますし、IDEだけをマスクし、IDEが1という条件にすれば拡張フレームだけを受信します。
 一番最後の例は拡張フレームの上位4bitがドミナント、という条件ですが、CANバスはその特性上、上位ビットの連続ドミナントが長いほど優先度が高く判定されますから、例えばメッセージの重要度を上位4bitで表現し、15なら最低優先度、0なら最高優先度、として、優先度0(最優先)のメッセージだけをFIFO0に入れ、その他はFIFO1に入れる、とすれば、最優先のメッセージを取り逃がす危険性が減る、みたいな用途に使えるはずです。


 フィルタには32bitモードと16bitモードがありますが、16bitモードの時は拡張IDの一部をチェックできません。試していませんが、16bit時はIDE/RTRと標準IDのすべてのビット、および拡張IDの上位14bit分をチェックできるようです。


 ちなみに、STM32のCANはフィルタで弾いたメッセージでも、ACKの送信を行うようです。ACKはCRCの後なので、IDが一致しないならACKを返さない、といった動作もできるはずですが、そもそもCANのACKはバスの健全性を確認するための手段なので、メッセージの中身には関係なくACKを送信する、という動作なのだと思います。


 ということで、だいぶ前にかなり悩みこんだフィルタの動作がわかって、ちょっとスッキリしました。

 とはいえ、HALのCANの割り込み処理はとても不思議な実装で、次はこれを使いこなす方法を考える必要があります。割り込みハンドラの中で以降の割り込みをDISABLEしていたり、受信先のFIFOがわからなかったり、想像を絶する感じです。


 CANバスは本来CANドライバICが必要ですが、極めて短距離の場合は、TXをOpenDrainで駆動し、1.5kくらいでプルアップして、すべてのデバイスのTXとRXを短絡させれば、ちゃんとCANバスとして使えます。250kbaudで全長30cm程度なら立ち上がり時間も問題無いようです。低コスト(&低放射ノイズ)な小型ロボットで3-5個程度のマイコンを接続したい、という用途ならこの程度でも十分かもしれません。I2Cのバスと似たような構成なので、信頼性もその程度で、せいぜい50cm未満が限界でしょうが。