マイコンを扱う上で最も基本的な要素のひとつであるシリアル通信の使い方を解説します
使用するボードはnucleo-G474REで開発環境は以下記事で紹介しているPlatformIO+STM32CubeMXの構成です

シリアル通信の構成
STM32CubeMXで生成されるHALドライバには大きく三種類の構成が用意されています
- ブロッキング処理によるシリアル通信(末尾なし)
- ノンブロッキング処理(割り込み)によるシリアル通信(末尾”IT”)
- ノンブロッキング処理(DMA)によるシリアル通信(末尾”DMA”)
ブロッキング処理によるシリアル通信
最も基本的なシリアル通信の処理です
使用する関数は「HAL_UART_Transmit」など末尾に何も付いていないものとなります
コード例として受信した文字(1byte)をそのままエコーバックするプログラムを作成しました
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_LPUART1_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
uint8_t buf;
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if( HAL_UART_Receive(&hlpuart1, &buf, 1, 100) == HAL_OK )
{
HAL_UART_Transmit(&hlpuart1, &buf, 1, 100);
}
}
/* USER CODE END 3 */
}
HAL_UART_Receive関数はデータ受信の処理を行います
関数の引数に使用するUARTハンドラ(今回はLPUART1を使うのでhlpuart1を指定)、受信したデータを格納する変数のアドレス(buf)、受信するバイト数(1byte)、タイムアウト時間(100ms)を指定します
関数は、データが受信されるかタイムアウトが発生するまで処理が継続します(ブロッキング処理)
関数の戻り値がHAL_OKの場合は引数で指定したバイト数のデータが受信され、指定した変数に受信したデータが格納されています
HAL_UART_Transmit関数はデータ送信の処理を行います
関数の引数はHAL_UART_Receive関数と同様で、送信するデータが格納された変数のアドレスと送信バイト数を指定します
関数は、データが送信されるかタイムアウトが発生するまで処理が継続します(ブロッキング処理)
基本的に送信側はタイムアウトは発生しないので戻り値のチェックはしていません(本来ならエラー処理等入れるべきですが今回は簡単のため省略、、、)
作成したプログラムをTera Termにて動作確認します
シリアルポートで接続して適当なキーを入力すると入力したキーがそのままエコーバックされます(1234567890と入力)

ブロッキング処理なので特に難しいことなくシリアル通信を構成することが出来ました
簡単なアプリケーションであればブロッキング処理で構成してもOKかと思います
ノンブロッキング処理(割り込み)によるシリアル通信
より実用的な方法としてノンブロッキング処理のシリアル通信として割り込みを利用する方法があります
使用する関数は「HAL_UART_Transmit_IT」など末尾に”IT”が付いているものを使用します
nucleoの初期設定ではLPUART1の割り込みは無効になっているのでまずはSTM32CubeMXで設定を行います

エコーバックのプログラムをブロッキング処理(割り込み)での構成に変更しました
/* USER CODE BEGIN PV */
static uint8_t rxBuf[128];
static uint16_t rxTop = 0, rxBtm = 0;
static uint8_t txBuf[128];
static uint16_t txTop = 0, txBtm = 0;
/* USER CODE END PV */
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_LPUART1_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
HAL_UART_Receive_IT(&hlpuart1, rxBuf+rxTop, 1);
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
while( rxTop != rxBtm )
{
txBuf[txTop] = rxBuf[rxBtm];
txTop = (txTop+1) & ( sizeof(txBuf)-1 );
rxBtm = (rxBtm+1) & ( sizeof(rxBuf)-1 );
}
if( txTop != txBtm )
{
if( HAL_UART_Transmit_IT( &hlpuart1, txBuf+txBtm, (txTop-txBtm)&(sizeof(rxBuf)-1) ) == HAL_OK )
{
txBtm = txTop;
}
}
}
/* USER CODE END 3 */
}
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == LPUART1)
{
rxTop = (rxTop+1) & (sizeof(rxBuf)-1);
HAL_UART_Receive_IT(&hlpuart1, rxBuf+rxTop, 1);
}
}
/* USER CODE END 4 */
HAL_UART_Receive_IT関数はデータ受信の処理を行います
関数の引数はブロッキング処理の関数と基本的に同じでノンブロッキング処理なのでタイムアウトの設定がありません
関数は、データの受信を待たずに処理が完了します(ノンブロッキング処理)
指定したバイト数のデータを受信した場合割り込み処理が発生し、指定した変数に受信したデータが格納されます
また、割り込みが発生すると受信コールバック関数(HAL_UART_RxCpltCallback)が実行され、受信処理が完了したことを検出することが出来ます
今回のコード例では受信したデータを格納する変数(rxBuf)を循環バッファの構成とし、コールバック関数にて再度HAL_UART_Receive_IT関数を実行することで常にデータ受信を行うようにしています
HAL_UART_Transmit_IT関数はデータ送信の処理を行います
関数の引数はブロッキング処理の関数と基本的に同じでノンブロッキング処理なのでタイムアウトの設定がありません
関数は、データの受信を待たずに処理が完了します(ノンブロッキング処理)
データ送信の処理は割り込み処理にて実行され、送信が完了すると送信コールバック関数(HAL_UART_TxCpltCallback)が実行され、送信処理が完了したことを検出することが出来ます
今回のコード例ではコールバック関数は使用せず、HAL_UART_Transmit_IT関数の戻り値にて判定しています(HAL_OKが返る=送信処理が完了している、それ以外=送信処理中なので再度実行する)
また、送信するデータを格納する変数(txBuf)も循環バッファの構成としています
HAL_UART_Transmit_IT関数はメインループ処理内にて受信用の循環バッファの更新があった場合(=新たなデータを受信した場合)に実行するようにしています
工夫した点として、送信処理は必要に応じて複数バイトを一括で送信できるようにしました
なお、循環バッファのバッファ数を増やす場合は2のべき乗の数値にする必要があります(128→256→512→、、、)
同様にTera Termにて動作確認を行い、期待通りの動作になることを確認しました
ノンブロッキング処理(DMA)によるシリアル通信
ノンブロッキング処理はDMA(Direct Memory Access)を使って構成することも可能です
簡単に説明すると割り込みで送受信データを変数に格納していたのをDMAというマイコンの機能に置き換えるイメージです
DMAはCPUを介さずにデータ転送を行う仕組みでCPUを占有しないことからより高速の通信が可能となります

割り込み処理はCPUを介するので、割り込みの頻度が多くなる場合(=高速通信)にCPUの占有率が高くなってしまいます
しかしながらこのDMAという機能、非常に癖があるためあまり好んで使わないです
実際にこの記事を書くにあたって色々と試行錯誤する羽目にあいました・・・
最終的にはこちらの記事を参考にさせて頂きました
まずはCubeMXでの設定です


TX側もCircular Modeに設定した場合、送信処理を開始すると指定したアドレスから順々にデータ送信していき、指定したバイト数に達すると再び最初のアドレスから順々にデータを送信するといった具合に、永久にデータを送信し続ける動作となります
送信を止めるためには強制的に停止させてやる必要があるのですが、あまりスマートな構成ではないのでTX側はNormal Modeで構成しました
Normal Modeの場合は指定したバイト数に達すると停止するのでこちらの方が使いやすいように感じました
ただし、UART側の設定でUARTの割り込み処理を使うようにする必要がある点に注意してください
これをしないと送信処理が完了したことを誰も検知しないので、Busy状態を解除することが出来ません

DMAはあくまでもUARTの送信バッファにデータを転送する機能しかないので送信処理が完了したことを検知するためにはUART側の送信完了割り込みかポーリングでフラグをチェックするしかないようです。よって、送信処理にDMAを使うメリットは(個人的には)あまりないと思います
ちなみにRX側のCircular Modeは受信したデータをDMAが勝手に循環バッファに格納してくれるので使いやすいです
エコーバックのプログラムをブロッキング処理(DMA)での構成に変更しました
int main(void)
{
/* USER CODE BEGIN 1 */
static uint8_t rxBuf[128];
static uint16_t rxBtm = 0;
static uint8_t txBuf[128];
static uint16_t txTop = 0, txBtm = 0;
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_LPUART1_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
HAL_UART_Receive_DMA(&hlpuart1, rxBuf, sizeof(rxBuf) );
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
uint16_t rxTop = sizeof(rxBuf) - hlpuart1.hdmarx->Instance->CNDTR;
while( rxTop != rxBtm )
{
txBuf[txTop] = rxBuf[rxBtm];
txTop = (txTop+1) & ( sizeof(txBuf)-1 );
rxBtm = (rxBtm+1) & ( sizeof(rxBuf)-1 );
}
if( txTop != txBtm )
{
if( HAL_UART_Transmit_DMA( &hlpuart1, txBuf+txBtm, (txTop-txBtm)&(sizeof(rxBuf)-1) ) == HAL_OK )
{
txBtm = txTop;
}
}
}
/* USER CODE END 3 */
}
HAL_UART_Receive_DMA関数はデータ受信の処理を行います
今までと違う点として、受信データの格納先(循環バッファ)のアドレスとバッファサイズを引数で指定します
この関数を実行すると受信したデータをDMAが勝手に循環バッファに転送してくれます
バッファの残りサイズはhlpuart1.hdmarx->Instance->CNDTRを読み出すことで知ることが出来るので、バッファサイズから残サイズを引くことで次の受信データの格納先(rxTop)を算出することが出来ます
後の処理は割り込みでの構成の場合と全く一緒です
こちらも同様にTera Termにて動作確認を行い、期待通りの動作になることを確認しました
受信をDMA、送信はブロッキング処理のハイブリッド構成が最も使いやすいかも?
今回説明のために受信/送信共に同じ処理の構成としましたが、別に揃える必要はありません
シリアル通信処理で最も大切なことはいかにデータを取りこぼさないようにするかだと思います
特に受信データは処理の応答性によってはデータを取りこぼすリスクがあります
よって受信処理は最速のDMAが最適解と考えます
一方で送信処理は送信データを取りこぼすことはないので最速を目指すメリットはあまりないと考えます
通信速度や通信するバイト数などにも左右されますが、ブロッキング処理でも十分実用レベルだと思います
以上を踏まえた上で最終的なエコーバック通信のコード例を紹介します
なお、最後のDMA構成よりCubeMXで以下の通り設定変更を行っています
- DMA設定のLPUART1_TXをDelete
- LPUART1設定のNVIC SettingよりLPUART1 global interruptのEnableをOFF
int main(void)
{
/* USER CODE BEGIN 1 */
static uint8_t rxBuf[128];
static uint16_t rxBtm = 0;
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_LPUART1_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
HAL_UART_Receive_DMA(&hlpuart1, rxBuf, sizeof(rxBuf) );
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
uint16_t rxTop = sizeof(rxBuf) - hlpuart1.hdmarx->Instance->CNDTR;
while( rxTop != rxBtm )
{
HAL_UART_Transmit( &hlpuart1, rxBuf+rxBtm, (rxTop-rxBtm)&(sizeof(rxBuf)-1), 100 );
rxBtm = rxTop;
}
}
/* USER CODE END 3 */
}