2020年10月10日

STM32 USB CDC注意点

STM32マイコンでUSBデバイス(USB_HSやUSB_FS)に対応している場合、CubeMX上で容易にUSB-CDC(Communications Device Class:CDCもしくはVirtual COM Port:VCP)を利用することができます。CubeMXの設定については今回は割愛しますが、CDCを利用する際に知っておきたい挙動、注意点について下記の3点を紹介します。

@usbd_cdc_if.cの関数ポインタ
Aバッファは割込み専用バッファ
B64byte以上のデータの送受信


@usbd_cdc_if.cの関数ポインタ
 CubeMX上で生成されてユーザがCDC機能を利用する場合、usbd_cdc_if.c内の関数を利用することになります。usbd_cdc_if.c内ではCDCの送受信の関数の他にバッファの初期化設定やCDCクラスの要求処理等の関数があります。これらの関数は関数ポインタで呼び出されるため、プロジェクト内で関数をgrepしても個々の関数は見つかりません。USBD_Interface_fops_FSでポインタが管理されています。そのため、usbd_cdc_if.cの関数の初期化や受信処理等の一通りの処理はCubeMX生成時点で既に実装されており、自動的に関数が実行されます。関数ポインタで管理、呼び出されるため、usbd_cdc_if.c内に安易に自作関数を配置することは避けたほうが賢明です。自作関数を置く場合、関数ポインタにも自作関数を加えて、さらに他のUSBライブラリとの整合を取る必要があります。関数ポインタの整合を取らないと関数が正常に呼び出されず、挙動がおかしくなります。

Aバッファは割込み専用バッファ
CubeMX生成時点で自動でバッファUserRxBufferFS、UserTxBufferFSが生成されます。このバッファは割込みorDMAのバッファとして使用されるため、ユーザー用途のデータバッファとして利用するのは避けた方が賢明です。バッファは受信時に読み込み、送信時の書き込みだけで利用します。このバッファは受信時はUserRxBufferFSにデータが入り、次の受信で自動的にクリアされて新しいデータが上書きされます。送信のUserTxBufferFSについても同様です。MX_USB_DEVICE_Init()の初期化の際にバッファの設定が有効化されます。CDC受信時のCDC_Receive_FS関数内のUSBD_CDC_SetRxBufferでバッファを再度設定していますが、例えば受信の都度、バッファをクリアやバッファの位置を変更、他のバッファに設定するといった自動で生成されたバッファに手を加えると2回目以降の送受信ができないといったCDCの不具合が発生しました。

B64byte以上のデータの送受信
USB_FSの場合、USBの仕様上、通信1回のデータサイズが最大64byteに制限されます。通常は相手側(通常はPC)ではデータの分割、結合が自動で行われるため、無理に独自規格で64byte以上に実装し直す必要はありません。一方、STM32マイコン側はデータの分割、結合の処理が記述されていないため、64byte以上の送受信をするとデータが欠落します。Aで少し触れましたが、CDCの通信バッファであるUserRxBufferFSとUserTxBufferFSは毎回の通信で自動的に上書きされます。例えば受信の場合は毎回上書きされるため、64byte以上のデータを受信した場合は64byteの端数(余り)のデータがバッファに残ります。

64byte以上を送信する際にはUSBD_StatusTypeDefを64byte毎にCDC_Transmit_FSを呼び出して、USBの状態を見てバッファが空になってから次の64byteを送信するようにします。

64byte以上を受信する際には下記のようにUserRxBufferFSとは別にバッファ(CDC_RxBuffer、CDC_RxBufferLen)を用意し、上書きされる前にグローバル変数の受信バッファにデータを退避させます。必要に応じてユーザープログラム側でCDC_RxBufferの読み出し、CDC_RxBufferLenのクリアなどの処理を行ってください。


//グローバル変数としての受信バッファ
uint16_t CDC_RxBufferLen=0;
uint8_t CDC_RxBuffer[2048];


static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, Buf);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
memcpy(CDC_RxBuffer+CDC_RxBufferLen,Buf,*Len);//データ退避処理追加
CDC_RxBufferLen=CDC_RxBufferLen+*Len;//データ長さ処理追加
return (USBD_OK);
/* USER CODE END 6 */
}


STM32マイコンのCDCに関する記事は多くありますが、CDC機能を応用しようとするとCubeMXで生成された関数の挙動を理解する必要があります。その他にもUSBの接続状態やポートをソフトウェアで開いたタイミングの検知などもできるため、機会があれば紹介したいと思います。
posted by Crescent at 00:00| Comment(0) | 電子工作 | このブログの読者になる | 更新情報をチェックする

2020年10月03日

RTC水晶発振子

多くのSTM32マイコンではRTC機能を備えており、VBATにリチウム電池と32.768kHzの水晶発振子を接続することでVCC電源断でも時刻を保持することが容易に実現できます。一方で設計によっては精度20ppm前後の一般的な水晶発振子を使用したにも関わらず、20ppmよりも時刻が経時でずれるといった場合があります。

時刻が経時でずれるといった場合、環境温度や周辺ノイズ、不安定な電源を除くと、多くは水晶発振子の外部負荷容量が適切でないことが原因です。安物の時計で時刻が進む、遅れるということが生じる原因も多くはこれが原因です。安物の時計の場合、水晶発振子の外部負荷容量のコンデンサ自体のばらつきが大きい部品を使用していることも一因です。


水晶発振子をRTCで使用する場合には水晶発振子と外部負荷容量の調整としてコンデンサを回路に入れます。一部のRTC ICでは外部負荷容量のコンデンサを内蔵しているものもありますが、使用する水晶発振子と接続先に合わせて外部負荷容量を調整する必要があります。STM32マイコンの評価ボードとして販売されているNucleoボードでは下記のように負荷容量6pFの水晶発振子X2に対して4.3pFのコンデンサC31,C32が外部負荷容量として接続されています。

STM32_NUCLEO64.jpg



負荷容量と周波数偏差の関係は下記のような反比例のような関係があります。水晶発振子によって適切な負荷容量は異なりますが、下記の例では6pF前後が周波数偏差0付近となり、適切な負荷容量と分かります。

Crystal.jpg

負荷容量と周波数偏差の関係から負荷容量が適切でない場合は周波数偏差がプラスにもマイナスにもずれることを意味します。

例えば、周波数偏差マイナスの場合は1秒間の発振回数が減少することになります。32.768kHzの水晶発振子で32768回カウントして1秒になるはずが、32768回カウントで1秒よりも多く秒数を要するため、経時で時計が遅れることになります。反対に周波数偏差プラスの場合は1秒間の発振回数が増加することになります。32768回カウントして1秒になるはずが、1秒よりも少ない秒数で32768回カウントされるため、経時で時計が進むことになります。

つまり、安い時計で時計が遅れるという場合は外部負荷容量を交換して容量の少ないコンデンサに置き換える必要があります。一方、時計が進む場合は外部負荷容量を容量の大きいコンデンサに置き換える必要があります。

実際に手持ちの時計が1か月で数分遅れるため、12pF程度から8pFに変更したところ、遅れがほぼなくなりました。外部負荷容量は接続する先のマイコンデータシート等から計算で求められますが、構成によっては理論通りにならないこともあります。なお、STM32マイコンの場合はソフトウェア側で周波数偏差を微調整、キャリブレーションすることが可能ですが、ハードウェア側で可能な限り対応した上でソフトウェアで微調整する方が確実です。RTCの水晶発振子は時刻のずれとして明確に影響が出るため、最終的には数台の実物を準備し、個体差を含めて半月や1か月で何秒くらいずれるか負荷容量を数pF単位で調整した方が良さそうです。
posted by Crescent at 00:00| Comment(0) | 電子工作 | このブログの読者になる | 更新情報をチェックする

2020年09月26日

無印ダイヤル式キッチンタイマ構造

今回は先日、近所の無印良品で購入したダイヤル式キッチンタイマについて紹介したいと思います。シンプルで小型なデザインでありながら、START / STOPボタン、RESETボタン、時間設定のダイヤル(筐体側面のリングを回転させる)と3つのインタフェースを搭載しています。小型な筐体にどのように3つのインタフェースを搭載しているのか、以前から気になったため、分解してみました。


timer1.jpg

分解は裏側のマグネットにネジが隠れており、両面テープで貼られたマグネットを剥がすことでネジを外すことができます。

timer2.jpg

ネジを外すと比較的簡単に内部の構造を確認できました。ケーブルは音のスピーカ配線とマイナス側の電池の配線がありました。電池のプラス側は基板側に接点がある構造です。汎用マイコンでなく、タイマー用IC(IC1)を使用しているようです。スピーカを駆動させるためのTrかFetとしてQ1があり、ノイズを除くため(想定)にスピーカと直列で大きめのインダクタL1があります。上下にSTART / STOPボタン、RESETボタンのプッシュスイッチが基板裏にあります。また、スイッチのチャタリング防止としてC1、R1、C2、R2でローパスフィルタが実装されています。右側の白い部分は時間合わせのための回転エンコーダです。


timer3.jpg


側面からみると中央の2つの軸を中心としてシーソーのような構造をしており、筐体表面が押しボタンとして押せるような構造になっています。片側に倒すとSTART / STOPボタン、もう片側に倒すとRESETボタンが押せるようになっています。また、写真中央の水色の部分がエンコーダとなっており、同軸上のギヤに繋がっています。エンコーダのギヤは筐体側面の筒内部のギヤとかみ合わさるようになっています。水色のエンコーダはパソコンのマウスのホイールに搭載されるものと同じようなエンコーダが搭載されていました。シーソー構造の回転中心付近にエンコーダのギヤのかみ合わせ部があり、ボタン操作と回転ダイヤル操作の干渉が少なくなるように工夫されています。

timer4.jpg

筐体や基板を除いて使用されている部品としては汎用的な部品を多く使用していることが分かりました。シンプルで小型なデザインでありながら、3つのインタフェースを搭載するためにシーソー構造やダイヤルのギヤなど様々な工夫があり、勉強になりました。構造を確認後は再度、組み立ててキッチンタイマとして使用することにしました。
posted by Crescent at 00:00| Comment(0) | 電子工作 | このブログの読者になる | 更新情報をチェックする

2020年08月29日

外部高速ADC接続方法 その2

以前に外部高速ADC接続方法検討として、MISOがパラレルになっている外付けADCをどうSTM32マイコンと接続するか検討しました。アナログデバイセズのサイトでも接続方法について解説がありました。AnalogDevicesのLTC2358、LTC2458といったADCではMISOがパラレルになっており、アナログデバイセズのサイトでも接続方法の中でもSolution4を適用できます。

以前の外部高速ADC接続方法検討では複数SPIを同時に利用するためにDMA転送は必須と説明しましたが、厳密にいえば必須ではありません。HALライブラリを使用した場合には必須ですが、アナログデバイセズのサイトの例をみて頂ければ分かる通り、HALライブラリを使用せずに直接SPIのレジスタ、バッファにアクセスすることで同時に複数のSPIをDMA転送を使用せずに通信することが可能です。

アナログデバイセズのサイトの各SolutionではSTM32F4を使用してコード例を記載していますが、STM32H7等ではレジスタ等が異なるため、そのまま適用できません。今回はSTM32H7等でHALライブラリを使用せずに直接SPIのレジスタ、バッファにアクセスする方法について紹介します。

SPI1をFull-Duplex Master、SPI2をRX-only Slaveとして、SPI1とSPI2を同時に使用する場合を紹介します。ADCのSDOラインが2つ以上ある場合はRX-only Slaveを増やして対応することが可能です。

■SPI初期化
初期化ではHALライブラリをそのまま使用します。設定のポイントとして、SPI1をマスタ、SPI2をスレーブとして設定する他にNSSを共にソフトウェアに設定する点です。

void MX_SPI1_Init(void)
{
 hspi1.Instance = SPI1;
 hspi1.Init.Mode = SPI_MODE_MASTER;
 hspi1.Init.Direction = SPI_DIRECTION_2LINES;
 hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
 hspi1.Init.NSS = SPI_NSS_SOFT;
 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
 hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
 hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
 hspi1.Init.CRCPolynomial = 0x0;
 hspi1.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;
 hspi1.Init.NSSPolarity = SPI_NSS_POLARITY_LOW;
 hspi1.Init.FifoThreshold = SPI_FIFO_THRESHOLD_01DATA;
 hspi1.Init.TxCRCInitializationPattern = SPI_CRC_INITIALIZATION_ALL_ZERO_PATTERN;
 hspi1.Init.RxCRCInitializationPattern = SPI_CRC_INITIALIZATION_ALL_ZERO_PATTERN;
 hspi1.Init.MasterSSIdleness = SPI_MASTER_SS_IDLENESS_00CYCLE;
 hspi1.Init.MasterInterDataIdleness = SPI_MASTER_INTERDATA_IDLENESS_00CYCLE;
 hspi1.Init.MasterReceiverAutoSusp = SPI_MASTER_RX_AUTOSUSP_DISABLE;
 hspi1.Init.MasterKeepIOState = SPI_MASTER_KEEP_IO_STATE_DISABLE;
 hspi1.Init.IOSwap = SPI_IO_SWAP_DISABLE;
 if (HAL_SPI_Init(&hspi1) != HAL_OK)
 {
  Error_Handler();
 }
}

void MX_SPI2_Init(void)
{
 hspi2.Instance = SPI2;
 hspi2.Init.Mode = SPI_MODE_SLAVE;
 hspi2.Init.Direction = SPI_DIRECTION_2LINES_RXONLY;
 hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
 hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
 hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
 hspi2.Init.NSS = SPI_NSS_SOFT;
 hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
 hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
 hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
 hspi2.Init.CRCPolynomial = 0x0;
 hspi2.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;
 hspi2.Init.NSSPolarity = SPI_NSS_POLARITY_LOW;
 hspi2.Init.FifoThreshold = SPI_FIFO_THRESHOLD_01DATA;
 hspi2.Init.TxCRCInitializationPattern = SPI_CRC_INITIALIZATION_ALL_ZERO_PATTERN;
 hspi2.Init.RxCRCInitializationPattern = SPI_CRC_INITIALIZATION_ALL_ZERO_PATTERN;
 hspi2.Init.MasterSSIdleness = SPI_MASTER_SS_IDLENESS_00CYCLE;
 hspi2.Init.MasterInterDataIdleness = SPI_MASTER_INTERDATA_IDLENESS_00CYCLE;
 hspi2.Init.MasterReceiverAutoSusp = SPI_MASTER_RX_AUTOSUSP_DISABLE;
 hspi2.Init.MasterKeepIOState = SPI_MASTER_KEEP_IO_STATE_DISABLE;
 hspi2.Init.IOSwap = SPI_IO_SWAP_DISABLE;
 if (HAL_SPI_Init(&hspi2) != HAL_OK)
 {
  Error_Handler();
 }
}



■SPIの通信
SPIの通信部分ではHALライブラリを使用せずにレジスタ、バッファに直接アクセスすることで同時にSPIを使用することができます。


HAL_GPIO_WritePin(CS_Port,CS_Pin,0);

//今回は6バイトの送受信を行うため、サイズに6を設定します。
MODIFY_REG(SPI1_HANDLE.Instance->CR2, SPI_CR2_TSIZE, 6);
MODIFY_REG(SPI2_HANDLE.Instance->CR2, SPI_CR2_TSIZE, 6);

//SPI通信有効化
__HAL_SPI_ENABLE(&SPI1_HANDLE);
__HAL_SPI_ENABLE(&SPI2_HANDLE);

//SPIスタート、マスタのみ
SET_BIT(SPI1_HANDLE.Instance->CR1, SPI_CR1_CSTART);//Only Master

//送信バッファに送信データを格納。なお、FIFIOは最大16Byte
for(size_t spiIndex=0;spiIndex<6;spiIndex++)
{

  *(__IO uint8_t *)&SPI1_HANDLE.Instance->TXDR = WriteData[0][spiIndex];
  *(__IO uint8_t *)&SPI2_HANDLE.Instance->TXDR = 0x00;

}
//送信後、受信バッファに格納されるまで待機
while ((SPI1_HANDLE.Instance->SR & SPI_FLAG_EOT) == RESET);
//受信バッファの読み出し
for(size_t spiIndex=0;spiIndex<6;spiIndex++)
{
   ReadData[0][spiIndex]=*(__IO uint8_t *)&SPI1_HANDLE.Instance->RXDR;
   ReadData[1][spiIndex]=*(__IO uint8_t *)&SPI2_HANDLE.Instance->RXDR;
}

//各エラーフラグクリア
SPI1_HANDLE.Instance->IFCR=0xFFFFFFFF;
SPI2_HANDLE.Instance->IFCR=0xFFFFFFFF;

//SPI通信無効化
__HAL_SPI_DISABLE(&SPI1_HANDLE);
__HAL_SPI_DISABLE(&SPI2_HANDLE);


HAL_GPIO_WritePin(CS_Port,CS_Pin,1);

HALライブラリを使用せずに直接SPIのレジスタ、バッファにアクセスすることで同時に複数のSPI通信する方法を紹介しました。STM32F4シリーズ等では送信受信でバッファが共通のDRレジスタとなっていましたが、STM32H7シリーズ等では送受信で個別のバッファとなっています。アナログデバイセズのサイトではADCの接続方法に限らず、様々なレポートが公開されており、非常に興味深いと思いました。
posted by Crescent at 00:00| Comment(0) | 電子工作 | このブログの読者になる | 更新情報をチェックする

2020年08月22日

Node-Redを用いたアナログ値読み込み

前回紹介したNode-RedからのI2Cデバイス制御する方法に続いて、応用例を紹介します。今回はGroveADCモジュールUSB-I2C変換アダプタを使用してNode-Redを用いてAD変換をしてみました。

GroveADCモジュールADC121C021を搭載しており、I2Cで簡単にAD変換することが可能です。初期化の際に連続変換モードに設定すれば、自動的に最新のAD変換値がレジスタに入るため、値を読み出すだけでAD変換値を得ることができます。今回はGroveADCモジュールには可変抵抗を接続して手動で値を変えられるようにしました。

また、Node-Redのdashboardパレットを追加すると簡単に可視化も可能です。今回は簡易的にグラフとして電圧値を出力するUIを作成してみました。実際にNode-Redで作成してフローは下記です。

grove_adc_flows.jpg

作成したUIはこんな感じです。

grove_adc_UI.jpg

クリアボタンでグラフをクリアできます。また、スタートストップの切り替えスイッチも作成してみました。
今回のサンプルフローもこちらに追加しています。

USB-I2C変換アダプタとNode-Redを用いて簡単にAD変換ができました。光センサや温度センサなどアナログで出力する様々なセンサを接続して取り込んでみてください。なお、USB-I2C変換アダプタはUSB電源からの給電で動作するため、USBポート(特にパッシブUSBハブ)によっては電源電圧が低く、4V~5V付近のアナログ値を得られない場合があります。その場合はPC直のUSBポートか、ACアダプタ付きのUSBハブ等を使用して電圧が得られるUSBポートを使用してください。


posted by Crescent at 00:00| Comment(0) | 電子工作 | このブログの読者になる | 更新情報をチェックする