秋月電子通商で売られているSTM32F042K6T6を使ってUSBシリアルデバイスを作る


 秋月電子通商で売られているSTM32マイコンのSTM32F042K6T6には、USBデバイスとして機能するコントローラが組み込まれており、簡単にUSBデバイスを作ることができる。mbedのSDKを使用してソフトウェア開発をもくろんでいたところ、いざビルドすると、このSoCに対してはUSB DeviceのCDC Classは非対応であることが判明。SRAM 6KByteは非力すぎるらしい。
 STM32CubeMXは、使用するペリフェラルをGUIで選択すると、適切な初期化コードを含むソースコードを生成するジェネレータ機能を持っている。最小限のリソースでアプリケーションをインプリメントするには、とても有用なツールであり、これを用いてアプリケーション開発を行った。ツールの紹介等は、他のブログにおまかせするが、生成されたコードをどのように調理してデバイスを組み上げるかという説明が本当に少ないので、調べたことをここに書く。

ツールのインストールとセットアップ

 STM32CubeMXツール本体は、st.comのページの一番下にあるソフトウェア入手ボタンのリンクからダウンロードする。
 コンパイラは、ARMのサイトからgccのツールチェーンをダウンロードして展開し、パスを通す。
 CUIベースであれば、これだけで開発ができる。

ハードウェア製作

 SoCで直接USBの信号ラインをドライブできるので、保護用の抵抗かパルストランスを経由して、USBコネクタを接続する。USBのスピードを選択するための1.5KΩのプルアップ抵抗は、SoCに内蔵されているので、外付けする必要はない。
 また、本来は正確なUSBクロックを供給するために、水晶発振器が必要だが、F042は、CR発振器の使用時、ホストのクロックで補正する機能を備えているため、水晶発振子をSoCに接続しなくてもよい。

 NUCLEO-F042K6ボードとDIP化USBコネクタで、ブレットボードで組んでもおそらく動作するが、安定動作のために、空中配線は避けるべきだろう。

ペリフェラルの選択とコードの生成

 「Pinout」タブで、「Peripherals」 - 「USB」 - 「Device(FS)」を選択する。

「RCC」-「CRS SYNC」を「CRS SYNC Source USB」にセットする。USBのSOFパケットによるHSI48クロック補正が適用される。

 「Peripherals」でUSBデバイスを選択すると、「MiddleWares」 - 「USB_DEVICE」 - 「Class For FS IP」が選択できるようになるので、「Communication Device Class (Virtual Port Com)」を選択する。

 「Configuration」タブで、「Connectivity」の「USB」ボタンを押し、USB Configurationの「Parameter Settings」で、Speedは「Full Speed 12MBit/s」、Endpoint 0 Max Packet sizeは「64Bytes」になっていることを確認する。

 「Middleware」の「USB_Device」ボタンを押し、「Parameter Settings」タブを選択する。Class Parametersのバッファサイズが、デフォルトで1000Bytesになっているが、このままだとビルド時にメモリがオーバーフローするので、小さな値に設定する。
 受信バッファは、USBのパケットサイズの64Bytesより大きくしなければならない。送信バッファは、実質的にライブラリ内では使用していないので、適当なサイズを設定してよい。

 その他の項目は適宜設定し、ソースコードを生成する。STM32CubeMXアプリで生成したソースコードは、そのままビルドすると、デバイスの初期化コードのみのアプリケーションが出来上がる。ペリフェラルに対して入出力を行うコードを挿入するだけで、アプリケーションになる。
 プロジェクト設定で、ターゲットIDEを「Makefile」を選択してコード出力を行うと、C言語のソースコードとMakefileが生成される。Makefile内のBINPATHにツールチェーンのbinディレクトリへのパスを設定すると、makeコマンドで自動的にアプリケーションをビルドすることができる。ただ、どういうわけか、C_SOUCESに指定されるファイルに、同じものが重複して書き込まれていてリンク時にエラーになるので、修正が必要だった。

USB CDC Class Deviceの使用

 デバイスからホストへのデータ送信は、Src/usbd_cdc_if.c内に定義されているCDC_Transmit_FS()関数で行う。データを格納したuint8_t型の配列へのポインタとデータサイズをパラメータで渡すとデータ送信を開始する。関数から制御が戻った時点では、まだデータ送信は完了していないので、連続して次のデータを送るときは、データ送信の完了を待って、次のCDC_Transmit_FS()関数を呼び出す。
 ホストからデバイスへのデータ受信は、Src/usb_cdc_if.c内で定義されているCDC_Receive_FS()関数で行われる。この関数は割込みによって実行されるので、アプリケーションから直接呼び出すことはない。自動生成したコードひな形では、受信データをUserRxBufferFSにコピーするだけなので、この関数を書き換えてアプリケーション内に取り込む。

usb_cdc_if.c
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  return (USBD_OK);
  /* USER CODE END 6 */
}

 アプリケーションのコードは、USER CODE BEGINコメントからUSER CODE ENDコメントまでの間に挿入する。関数のパラメータのBufには、hUsbDeviceFS.pClassData->RxBufferの値がセットされ、Lenには受信したデータのサイズへのポインタがセットされる。関数内で呼び出されているUSBD_CDC_SetRxBuffer()は、hUsbDeviceFS.pClassData->RxBufferに引数のポインタを保存するので注意しなければならない。引数Bufが指すuint8_tの配列は、USB_FS_MAX_PACKET_SIZE(=Endpoint 0 Max Packet Size) より大きな領域が確保されている必要がある。
 USBD_CDC_ReceivePacket()を実行するとデータがバッファにコピーされるので、次のデータを受信できるよう、データをコピーしてバッファを空けるか、バッファポインタを移動してリングバッファを構成するかの処理を追加しなければならない。この関数は割込みハンドラとして実行されるので、重い処理や他の割込みを必要とする処理は避けなければならない。
 最も単純な処理としては、受信データを保存したことを示すフラグを介して割込みルーチンからメインスレッドにバッファデータをコピーする。

usb_cdc_if.c
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
extern uint8_t*  rBuf;
extern int  rBufSize;
/* USER CODE END PV */
              :
              :
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  if (rBufSize == 0) {
    rBufSize = *Len;
    if (rBufSize > 64) rBufSize = 64;
    memcpy (rBuf, Buf, rBufSize);
  } else {
    // overrun
  }
  return (USBD_OK);
  /* USER CODE END 6 */
}
main.c
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
volatile uint8_t  rBuf[64];
volatile int  rBufSize = 0;
/* USER CODE END PV */
              :
              :
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    if (rBufSize > 0) {
      // process (rBuf, rBufSize);
      rBufSize = 0;
    }
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }

 このサンプルコードは、ホストが連続してデータを送信すると、オーバーランする。本格的なアプリケーションを開発する場合は、適切な処理を実装する。

訂正

  • main.cで宣言しているrBuf[], rBufSizeにvolatile修飾子が必要であったので追加した。