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未満が限界でしょうが。