STM32 HAL USB-CDC

 Blue Pill(STM32F103)のUSBポートとPCを繋いでPCとの間で通信を行ってみる。以前にやっているが、メモ程度だったので再度記述する。

はじめてのマイコンを手にするとLチカからはじめる。これはデジタルのアウトプットを実行する事を身に着けるため。その後デジタルのインプットでスイッチをONするとLEDを連動させて点灯させる・・と。これらの時LEDが表示器、つまりプログラム通りに動いているか?を確認する術となる。

センサから値が読めているか?とセンサ値を確認する場合表示器としてなんらかのディスプレイが欲しくなる。圧倒的にデバッグ時間も短縮できる。そこでUSBポートを経由して結果をPCに映せるようにする事が目的。(Arduinoなんかは、すぐにできるが Blue Pill(STM32F103)はそうはいかない)

前回のおさらい

前回用意した、ブレッドボードにblue pillを設けたスイッチを押すとLEDが付くところまでを改めて作成する。→前回の内容

スイッチを押したらLEDが付くところまで確認する。

USBの設定

Device configuration tool を開く。画面右上に、MXのアイコンがあるので押すと開く。

Categoriesの下に”Connectivity”があるので、それをクリックして展開する。下の方にUSBがあるので選択する。USB Mode and ConfigurationのModeの欄に□Device(FS)があるのでチェックを入れる。

するとCategoriesの下のMiddlewareの所に”USB_DEVICE"が反転し選択できるようになる。USB Mode and ConfigurationにClass For FS IPが現れてDisableになっているが、選択できるようになる。

Communication Device Class(Virtual Port Com)を選択する。

以下STM32で何を実現するかによって選択をかえるざっと説明すると、

Audio:オーディオ機器を接続する際に利用

CDC(Communication Device Class):デバイス間のデータのやりとりを行うための通信をするとき

DFU: Device Firmware Upgrade ブートローダーを書いてUSB経由でファームをアップロードできるようにする

HID:Human Interface Deviceなんでキーボードなんかを作る時

Strage:USBハードディスク、USBメモリ、メモリーカードのデバイスを作るとき

  といった感じ。今回の目的からCDCを選ぶ。

クロックの設定

□Device(FS)のチェックボタンにチェックを入れた時に実は、もう一か所画面に変更がされている。


Clock Configurationのタブの左に赤いx印がつくようになっている。これはクロックの設定が正しくないですよという警告。



Clock Configurationのタブを開くと、自動でクロック問題を解きますか?とダイアログが開くがNoを選択。YESを選択しても解は見つからない。


まず、エラーが出ている状態を見てみる。HSI RCとLSI RCが青くなってそこからクロックが供給されている事がわかる。HSIは高速内蔵8MHzのRCオシレータでLSIは低速内臓の40kHzのオシレーター。RCからなる共振器は水晶振動子がいらないので回路が簡単に形成できる一方で、外部と通信するなど周波数の絶対値が必要な用途では温度の影響などで周波数変動が起こるので、うまく通信できない事象が起こる。そこで外部に設けた精度の良い外部クロック(水晶振動子/セラミック振動子)を利用する。


Categoriesの下のSytem Coreを展開し、RCCを選択する。Reset and Clock Controlの略。

High Speed ClockU(HSE)をCrystal/Ceramic Resonatorを選択することで外部クロックがMPUに接続される。PD0とPD1ピンが接続される事が確認できる。


再度Clock Configurationのタブを開くと左側のボード上に設けられた8MHzの水晶振動子と接続がなされる絵に変更されている事が確認できる。このままでもよいが、バススピードが最大になるようにPLLの倍率を9倍に変更しUSBのプリスケーラを/1.5に設定する。USBは48MHzにする必要がある。

ここまでできたら保存(Ctrl+S)して自動コードを生成する。

コーディング(マイコン→PC)


まず、プライベートのincludesを置く所に、

/* Private includes ---------------------------------------------------*/

/* USER CODE BEGIN Includes */

#include "usbd_cdc_if.h"

/* USER CODE END Includes */

usbd_cdc_if.hのヘッダーを読み込む。通常、自動コード生成で必要なものはincludeされるが、この子はされてこない。

implicit declaration of function 'CDC_Transmit_FS' [-Wimplicit-function-declaration]

CDC_Transmit_FSという後ほど使う関数が”暗黙的な宣言だ!”と警告がでる。無くてもエラーにはならず動作はする。

次に、43行目あたりにプライベートな変数を記載する箇所があるので、char型の変数を定義しておく。

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

char cdcBuffer[64];

/* USER CODE END PV */

続いて、無限ループの箇所に

 /* Infinite loop */

  /* USER CODE BEGIN WHILE */

  while (1)

  {

    /* USER CODE END WHILE */

  sprintf(cdcBuffer, "Hello\n\r");

  while(CDC_Transmit_FS((uint8_t*)cdcBuffer, strlen(cdcBuffer)) == USBD_OK) {}

  HAL_Delay(500);

    /* USER CODE BEGIN 3 */

  }

と追記をする。

BOOT0のジャンパーピンを1にしてリセットを押して書き込みを行う。

無事完了したらジャンパーピンを戻す。



TeraTarmを立ち上げて、シリアルにチャックを入れる。ポートにCOMX(Xはなんかの数字)にシリアルデバイスが見えるので選択する。

0.5秒間隔でHelloの文字が表示される事が確認できる。

プログラムを編集した場合は、USBポートを抜き差ししてあげる必要があるのが欠点。
(スイッチ付のUSBハブを使って切断すると手間が少ない)

数字(int, float)の転送

文字を映したので、次は数字
  /* USER CODE END WHILE */
 uint8_t int_test;
  float f_test;
  int_test=3;
  f_test=3.141592;
  sprintf(cdcBuffer, "int=%d, float=%f\n\r",int_test,f_test);
  while(CDC_Transmit_FS((uint8_t*)cdcBuffer, strlen(cdcBuffer)) == USBD_OK) {}
  HAL_Delay(500);
と編集する。
すると、sprintfの行になんやらマークがつく。カーソルをもっていくと以下のようなメッセージが見える。


Problem description: The float formatting support is not enabled, check your 
 MCU Settings from "Project Properties > C/C++ Build > Settings > Tool 
 Settings", or add manually "-u _printf_float" in linker flags.
と書いてあって、その通りに設定してやる必要がある。


Project Explorerのフォルダを右クリックしてPropertiesを開く.
>C/C++Buildを展開して、Settingを選択。Toolsettingのタブの下にあるMCU Settingsをクリックする。□Use float with printf from newlib-nano(-u _pringf_float)にチェックをいれる。この状態でコンパイルして焼き付けてあげるとちゃんと表示がされる。


整数も、フロートも表示さえる事が確認できる。

コーディング(PC→マイコン)

今度は、PCからマイコンに文字列を送ってみる。

まず受信するための変数を用意する。送信と同じように、cdc_RX_Bufferという名で用意してみる。

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
uint8_t cdc_TX_Buffer[64];
uint8_t cdc_RX_Buffer[64];
/* USER CODE END PV */

次にちょっと小細工をする。



Project Explorerのプロジェクトの下にUSB_DEVICEというのがあるので、その中のusbd_cdc_if.cというのをダブルクリックして開く。

30行目過ぎの所に、プライベート変数として先ほどmain.cで宣言したRX用の変数を外部参照として定義する。

/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
extern uint8_t cdc_RX_Buffer[64];
/* USER CODE END PV */

次に、260行目ぐらいにCDC_Receive_FSという関数を探し編集する。4行を追加する。

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);

   memset (cdc_RX_Buffer, '\0', 64);  // clear the buffer
   uint8_t len = (uint8_t)*Len;
   memcpy(cdc_RX_Buffer, Buf, len);  // copy the data to the buffer
   memset(Buf, '\0', len);   // clear the Buf also


  return (USBD_OK);
  /* USER CODE END 6 */
}

CDC_Receive_FSという関数は、文字を受信するとコールバックされる関数になっているので文字を受信するとここに飛んでくる。mainと共有している変数cdc_RX_Bufferに受け取ったものを格納してあげる。

memset関数はメモリに指定バイト数分の値をセットするC言語の関数で、cdc_RX_Bufferを受信文字列を格納する前に'\0'で埋める(空にする)。前に受け取った文字列が8文字で、次受け取ったのがそれより短い4文字だとすると、後半4文字は前のものが残ってしまうので、いったん空にする。

 uint8_t len = (uint8_t)*Len;

これは単に小文字のlenという変数を用意しているだけ。(以後Lenを使ってもよく多分これいらない)

 memcpy(cdc_RX_Buffer, Buf, len);  // copy the data to the buffer

memcpy関数も、C言語の関数で、Bufcdc_RX_Bufferにしていバイト数だけコピーする。

memset(Buf, '\0', len);   // clear the Buf also

最後にCDC_Receive_FS関数の受け取り文字列格納されるBufも空にしてあげる。理由は同じで文字列の長さ違いが入ってきた時の対処のため。


usbd_cdc_if.cの編集はここまでなので保存する。

再度main.cに戻りメインループを記載する。

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

char RESP_TEST1[] = "ON\r\n";
char RESP_TEST2[] = "OFF\r\n";

  if (strcmp((char *)cdc_RX_Buffer, RESP_TEST1) == 0)
  {
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
  sprintf((char*)cdc_TX_Buffer, "LED turned on.\n\r");
    while(CDC_Transmit_FS(cdc_TX_Buffer, strlen((char*)cdc_TX_Buffer)) == USBD_OK) {}

  }
  else if (strcmp((const char *)cdc_RX_Buffer, RESP_TEST2) == 0)
  {
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
  sprintf((char*)cdc_TX_Buffer, "LED turned off.\n\r");
    while(CDC_Transmit_FS(cdc_TX_Buffer, strlen((char*)cdc_TX_Buffer)) == USBD_OK) {}

  }
memset(cdc_RX_Buffer, '\0', strlen(cdc_RX_Buffer)); 
  }
  /* USER CODE END 3 */

解説していくと

次に、RESP_TEST1RESP_TEST2と二つの文字列の配列を用意。ここでは”ON"と”OFF"というコマンドを定義してみる。

  if (strcmp((char *)cdc_RX_Buffer, RESP_TEST1) == 0)

strcmpという関数は2つの文字列の比較をして一致すると0を返すというC言語の関数。受け取った文字列が”ON"と一致するなら{}の中を実行する。
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
でONと一致するならPC13をGNDに接続してLEDを点灯させる。
その後、
sprintf((char*)cdc_TX_Buffer, "LED turned off.\n\r");
LED turned on.”とLEDついたよ!とメッセージをPCに送ってやる。

 while(CDC_Transmit_FS(cdc_TX_Buffer, strlen((char*)cdc_TX_Buffer)) == USBD_OK) {}

USBの送信がOKになるまで待機。OFF側も同じ。
最後に、
memset(cdc_RX_Buffer, '\0', strlen(cdc_RX_Buffer)); 

cdc_RX_Bufferを空にしてあげる。









コメント

このブログの人気の投稿

Attiny85とAQM0802A(LCD)のI2C接続

Attiny85 FuseRest

HS101 STM32の自作お手軽オシロスコープ