2022年5月8日日曜日

【Android USB oscilloscope】(29) STM32とスマホでBULK転送うまく動いたらしい(重箱の隅)

余っているAndroidスマホをオシロにしよう!

BULK転送がなかなかうまく動きませんでしたがようやく動いたみたいです.
STM32CubeIDEのproject詰め合わせは後ほどupします.下の方を見てください.

今回も重箱の隅なのでご注意ください.

STM32CubeIDE→MiddeWare→virtual COMが吐き出すsample codeをひな形にしています.しかし単純なBULK転送をどうやったら良いのだ?という素朴な疑問には答えてくれません.困ってしまってかれこれ1weekが経ちました.

USBプロトコルアナライザ(ハード)なんつう高級機材は持ってないので、内部状態をdumpさせたりして問題抽出に苦労しました.

【問題】
BULK送信がpacket落ちする.
大雑把にいえばこんなトラブルが起きている.
 hostは毎秒送信したい
  →deviceが速やかに受信するとは限らない
   →やがてbuffer overflowで死亡
    →死んでるのがhostなのかdeviceなのかは不明
期待としては、deviceがもたついているなら再送信してもよいのでpacket落ちだけは回避したい.

【問題の詳細】
不活性なdeviceの状態をhostがどうやって察知するかが焦点でした.

deviceが活性なとき:
STM32 hostがBULK転送すると、STM32内部にはHC_XFRCフラグが立つ

deviceが不活性なとき:
STM32 hostがBULK転送すると、STM32内部にはHC_NAKフラグが立つ

フラグの挙動はこんなもんでしょうが運用にコツが要るかんじ.

HC_NAKだったらどう対処するか?
deviceがACKになるまでdeviceのstatusをpollingし続ければいいとフツーは考えます.ところが、USB_status()みたいな関数はあれど、STM32内部のwork areaのオウム返しなのでdeviceの最新情報ではないため役に立たないんです.つかえねぇー

sample codeのどこを見てもdevice statusをpollする関数なんかありません.そんなバカなと思ってさんざ探しましたけど無いんです.ならば割り込みかと探してもそんなのは無い.どうしたらいいんだ?

【解決の作法】
「device statusをpollする関数が無い」のですから、別の手段でpollするしかありません.
別の手段とは、、、再度BULK送信を行う というのがUSBの作法であるようです.
すなわちこういうこと.
 前回の結果を見る
  →deviceが不活性(NAK)だったなら旧いdataを再送する
  →deviceが活性(ACK)だったなら新dataを送る
   →新しい結果がSTM32のメモリ上に残される
ゆえにdeviceが受信バッファを消化してくれない限りdata再送で無限loopします.

肝の部分のcodeはこうなります.
①で判るのは、前回の送信の終了時のstatusです.最新のstatusではありません
②HC_IDLE:前回の送信後にUSBがIDLEの意 →新dataを送信する  ※再送すべきかもしれませんが確信持てません
 HC_XFRC:前回が送信完了だった意 →新dataを送信する
 HC_NAK:前回の送信時にdeviceが不活性だった意 →前回dataを再送する
③④dataを準備する.IDLEかXFRCなら新data.NAKなら前回dataのまま
⑤BULK転送関数
HCD_HCStateTypeDef r;
r = HAL_HCD_HC_GetState(phost->pData, phost->Control.pipe_out); ①
if(r == HC_IDLE || r == HC_XFRC || r==HC_NAK) { ②
   if(r!=HC_NAK) N++; ③
   sprintf((char*)buffer,"bulk transfer test %d\n",N); ④
  USBH_BulkSendData(phost, ⑤
                    buffer,
                    strlen((char*)buffer),
                    phost->Control.pipe_out,
                    1);
}
上位レイヤーでもっとエレガントにやれるように仕組まれている可能性はあります.しかしそこまでcodeを見切れてない.

【試験環境】
諸条件:
CPU STM32F205
スマホ Huawei P9 lite
STM32自作プリント基板  (ミスを含む回路図
接続はUSB2.0
スマホ接続はAOA

code:
project folder詰め合わせをupしときます.
開発環境は、スマホアプリはAndroid Studio、STM32はSTM32CubeIDE.
スマホのアプリ →使い方はこちら.ボタンを押すとUSBを読んで表示するだけのもの
STM32 project →STM32CubeIDEが出力するVirtuelCOM(CDC) sampleをひな型に改造を加えました.test用codeを多く含むのですげー汚いです.test codeはUARTに内部状態を出力しています.hira_senduart_...()がUART出力関数です.


以下はSTM32のcodeの説明です.STM32のCDC sampleをひな型にしています.

【AOA接続処理】
スマホがSTM32に接続されるとdescriptorを読んだりいろいろなUSB接続処理が行われます.sample codeの本来の意図はCDC deviceをSTM32へ接続することです.しかしスマホはCDCではありませんのでCDC設定へ進めずに途中で断念します.断念したところへAOA処理を突っ込んでいます.

AOAへ至る流れはmain()のwhile loopから繰り返し呼び出されます.
MX_USB_HOST_Process()
 →USBH_Process()
  →aoa_setupAccessory()   これがAOA処理の本体

AOAの重要部分だけ抜粋します.
int aoa_setupAccessory( USBH_HandleTypeDef *phost ) {
↓VID/PIDを取得する.これが呼び出されるまでにdescriptorは取得済なのでVID/PIDなどはメモリに在る.
 uint16_t vid = phost->device.DevDesc.idVendor;
 uint16_t pid = phost->device.DevDesc.idProduct;
↓既にAOA接続してる場合はおしまい.VID/PID=18D1/2D01か18D1/2D00ならAOA.
 if( vid == ACCESSORY_VID && pid == ACCESSORY_PID ) {
   aoa_ok = 1;
   return 0;
 }
 else if( vid == ACCESSORY_VID && pid == ACCESSORY_PID_ALT ) {
   aoa_ok = 1;
   return 0;
 }
↓違うVID/PIDだったらAOA接続処理を始める
 else {
↓control転送でスマホのAOA versionを採取する.ゼロだったらAOAをサポートしてないのでexitする.なぜかいつもver2みたいよ.
   aoa_usb_ctrlreq( phost, 0xC0, 51, 0, 0, buffer, 2, 0);
   devVersion = buffer[1] << 8 | buffer[0];
   if(devVersion==0) return -1;
↓謎のdelay 50mSec.長くしすぎないこと.→その理由
   USBH_Delay(50);
↓スマホに"bangflat","aoatest"の文字が登録されているかどうかをチェックする.この文字はスマホアプリに記述してあるもの.アプリがスマホにインストされていればOK
   strcpy((char*)buffer,MANUFACTURER); ←bangflat
   aoa_usb_ctrlreq(phost,0x40,52,0,0,buffer,
                strlen((char*)buffer),0);
   strcpy((char*)buffer,MODEL);  ←aoatest
   aoa_usb_ctrlreq(phost,0x40,52,0,1,buffer,
                strlen((char*)buffer),0);
↓続けて謎のコマンドを送るとスマホの接続が勝手に切れて、AOAのVID/PIDで再接続してくる
   strcpy((char*)buffer," ");
   aoa_usb_ctrlreq(phost,0x40,52,0,2,buffer,1,0);
   aoa_usb_ctrlreq(phost,0x40,52,0,3,buffer,1,0);
   aoa_usb_ctrlreq(phost,0x40,52,0,4,buffer,1,0);
   aoa_usb_ctrlreq(phost,0x40,52,0,5,buffer,1,0);
   aoa_usb_ctrlreq(phost,0x40,53,0,0,buffer,0,0);
   USBH_Delay(50);
↓上と同じようにVID/PIDチェック
   if( vid == ACCESSORY_VID && pid == ACCESSORY_PID ) {
      aoa_ok = 1;
      return 0;
   }
   else if( vid == ACCESSORY_VID && pid == ACCESSORY_PID_ALT ) {
      aoa_ok = 1;
      return 0;
   }
   else {
      aoa_ok = 0;
      return -1;
   }
 }
}
AOA処理ルーチンは以上です.謎のコントロールコマンドはそうゆうもんだと思うしかないですね.

上で出てくるaoa_usb_ctrlreq()は何をやっているのか?
↓まず引数はUSBコントロールコマンドの羅列です.
int aoa_usb_ctrlreq(
  USBH_HandleTypeDef *phost,
  uint8_t bmRequestType,
  uint8_t bRequest,
  uint16_t wValue,
  uint16_t wIndex,
  uint8_t *pbuf,
  uint16_t wLength,
  uint16_t timeout){
  do{
↓構造体へコントロールコマンド情報を記入.CMD_SENDは「送り終わった」という意味だったかな
   if (phost->RequestState == CMD_SEND) {
     phost->Control.setup.b.bmRequestType = bmRequestType;
     phost->Control.setup.b.bRequest = bRequest;
     phost->Control.setup.b.wValue.w = wValue;
     phost->Control.setup.b.wIndex.w = wIndex;
     phost->Control.setup.b.wLength.w = wLength;
   }
  }
↓OKフラグになるまで何度もloopするというなんだかなーな構造になっている.裏でシーケンサが上手くやってくれてます
  while(USBH_CtlReq(phost, pbuf, wLength)!=USBH_OK);
  return USBH_OK;
}

【AOA後のOPEN処理】
AOA接続ができたとして、次はBULK転送するpipeを作ってやらなくちゃいけません.
main()のwhile loopで行います.
①は上で述べたAOA接続をやってくれます.AOA接続が完了したらaoa_ok=1になる
②はAOA接続できたら、、、、
③はOPEN処理
④はopen完了の意味のフラグを立てとく
  while (1)  {
    MX_USB_HOST_Process(); ①
    if(aoa_ok==1) { ②
    USBH_SelectInterface(&hUsbHostHS, 0); ③
    if(aoa_getPipe(&hUsbHostHS)!=0) break; ③
     aoa_openPipe(&hUsbHostHS); ③
     aoa_ok=2; ④
    }
  }

aoa_getPipe()は未割当のPipe Numberを取得します.コントロールパイプは0と1なのでここでは2と3になるのかな? それだけでなく、Pipe NumberとEndPoint Numberとの対応付けもします.構造体の深いところに欲しいデータがあります.
ぶっちゃけこんな感じになるんです.
 パイプ2 IN  EP83
 パイプ3 OUT EP02
int aoa_getPipe( USBH_HandleTypeDef *phost ){
 uint8_t NumEndpoints =
   phost->device.CfgDesc.Itf_Desc[0].bNumEndpoints;
 if(NumEndpoints!=2) return -1;

 uint8_t ep1 = 
  phost->device.CfgDesc.Itf_Desc[0].Ep_Desc[0].bEndpointAddress;
 uint8_t ep2 = 
  phost->device.CfgDesc.Itf_Desc[0].Ep_Desc[1].bEndpointAddress;
 uint8_t ep_in,ep_out;
 if( (ep1 & 0x80) == 0x80 ) {
   ep_in = ep1;
   ep_out = ep2;
 }
 else {
   ep_in = ep2;
   ep_out = ep1;
 }
 phost->Control.pipe_in  = USBH_AllocPipe(phost, ep_in);
 phost->Control.pipe_out = USBH_AllocPipe(phost, ep_out);
 return 0;
}

【OPEN後のBULK送信】
ようやくBULKです.現在は試験的に動かすだけなので、TIM7の1秒割込みでBULK送信しています.aoa_ok=2になっていたらOPEN完了してますのでBULK送信します.
void TIM7_IRQHandler(void) {
   if(aoa_ok==2) aoa_bulkTransfer(&hUsbHostHS);
}

aoa_bulkTransfer()は【解決の作法】で説明済です.
void aoa_bulkTransfer( USBH_HandleTypeDef *phost ) {
 HCD_HCStateTypeDef r;
 r = HAL_HCD_HC_GetState(phost->pData, phost->Control.pipe_out);
 if(r == HC_IDLE || r == HC_XFRC || r==HC_NAK) {
   if(r!=HC_NAK) N++;
   sprintf((char*)buffer,"bulk transfer test %d\n",N);
   USBH_BulkSendData(phost, 
                 buffer, 
                 strlen((char*)buffer), 
                 phost->Control.pipe_out, 
                 1);
 }
}

今宵はこれまでにしとうございます.これで先へ進めます.

28へ    30へ

かしこ

0 件のコメント:

コメントを投稿