F767ZIのSDMMC機能を使用してSDカードにデータを書き込んだ話

# はじめに
STM32マイコンの一部にはSDIO，SDMMCといったSDカードとの通信を行える機能があるそうです．

↓ ST公式によるSDMMCの資料
https://www.st.com/resource/en/product_presentation/35.stm32l4-peripheral-sdmmc-wspc-interface-wspc-(sdmmc)-wspc-final_jp.pdf

今回はNucleo-F767ZIでSDMMC機能を用い，SDカードにデータを書き込むことに成功したのでその方法を紹介したいと思います．

# 開発環境
使用ボード：Nucleo-F767ZI
CubeIDEバージョン: 1.6.1
SDカードスロット：[これ](https://akizukidenshi.com/catalog/g/gK-05488/)

# プロジェクト作成からコード生成まで
## プロジェクト作成
・プロジェクト名を適当に設定．
・「Initialize all peripherals with their default Mode?」では「Yes」を選択．

## Ethernetを無効化
・「Connectivity」->「ETH」を選択
・「Mode」->「Disable」を選択

私の環境ではEthernetの初期化でエラーを吐いてしまったのでEthernetを無効化しました．
![Ethernet無効化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/ee4fb1af-480f-498b-5e6e-a0576b4b5796.png)

## SDMMCの設定
・「Connectivity」->「SDMMC1」を選択
・「Mode」->「SD 4 bits Wide bus」を選択
・「Configuration」->「NVIC Settings」->「SDMMC1 global interrupt」を有効化
![SDMMC設定1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/14564cc9-7d92-50f6-4dfc-c1b0f3a76123.png)


ここで，Clock Configurationが×マークを表示するのでクロックの設定を行う．
「自動でクロックを設定するか」といった表示が出た場合は「OK」を選択．

SDMMC1へのクロック周波数を確認．今回は48MHzに設定されていた．
![SDMMC設定2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/f85c5ee0-9f5b-6ea7-c86d-cc36cab23b16.png)

「Pinout & Configuration」に再び戻り，
・「Configuration」->「DMA Settings」で「Add」ボタンから
「SDMMC1_RX」「SDMMC_TX」を追加．
（DMA Request SettingsはデフォルトのままでOK）

![SDMMC設定3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/235c39c4-3aab-9193-6429-8f3161ba87a7.png)


・「Configuration」->「GPIO Settings」でSDカードとの接続に使用するピンが確認できる．
（使用するピンは計6つ）

![SDMMC設定5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/33533e9a-ff76-3c14-95d0-f93cf3bdcbe1.png)

## FATFSの設定
FatFsはChaN氏によるFATファイルシステム用ライブラリ[1]のこと．
STM32CubeIDEではそれをサポートしている．

・「Middleware」->「FATFS」を選択．
・「Mode」->「SD Card」をチェック．
・「Configuration」->「Advanced Settings」から「Use dma template」で「Enabled」を選択．

![FATFS設定6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/6424c631-65d5-585e-acd1-48e0eb71179c.png)


## ヒープ・スタック領域の拡大
「Project Manager」で
・「Minimum Heap Size」を「0x400」
・「Minimum Stack Size」を「0x800」
にそれぞれ変更．
Middlewareを使用するため，この設定は必須とのこと．
![Project Manager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/7ac5586a-7790-8e4e-cfe7-8323b4c42c9f.png)

## コード生成
以上で設定は完了．コードを生成する．
その際に下図のような表示が出るが「Yes」を選択．
(SDカードの検出に関する設定らしい)
![Code Generation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/a73a45ce-42f8-a401-3a9b-88e0794b0c6a.png)

# ソースコード
## インクルード設定
必要なライブラリをインクルードします．

```c++:main.c
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "string.h"
/* USER CODE END Includes */
```

## 使用する変数の宣言
「USER CODE BEGIN/END PV」内に以下を記述．

```c++:main.c
/* USER CODE BEGIN PV */
FRESULT res;
uint32_t byteswritten, bytesread;
uint8_t wtext[] = "STM32 FATFS works great!\r\n";
char write_buffer[_MAX_SS];
uint8_t rtext[_MAX_SS];
int value = 10;
float value2 = 2.3;
static const char* filename = "STM32.TXT";
/* USER CODE END PV */
```


## main文
今回はSDカードに5回文字列を書き込んでみます．
書き込みが終わったらwhile文内でLチカさせて終了を知らせるようにしました．

```c++:main.c
int main(void)
{
  // (中略)

  /* USER CODE BEGIN 2 */
  // mount SD
  if(f_mount(&SDFatFS, (TCHAR const*)SDPath, 0) != FR_OK){
            Error_Handler();
    }else{
      	  //Open file for writing (Create)
  		  if(f_open(&SDFile, filename, FA_CREATE_ALWAYS | FA_WRITE) != FR_OK){
  			  Error_Handler();
  		  }else{
  			  // Write to the text file
  			  res = f_write(&SDFile, wtext, strlen((char *)wtext), (void *)&byteswritten);

  			  // Write int value
  			  sprintf(write_buffer, "value is %d\r\n", value);
  			  res = f_write(&SDFile, write_buffer, strlen((char *)write_buffer), (void *)&byteswritten);

  			  // write float value
  			  // Do not forget to set "-u _printf_float" argument
  			  sprintf(write_buffer, "value2 is %f\r\n", value2);
  			  res = f_write(&SDFile, write_buffer, strlen((char *)write_buffer), (void *)&byteswritten);

  			  // whether writing is completed or not
  			  if((byteswritten == 0) || (res != FR_OK)){
  				  Error_Handler();
  			  }else{
  				  f_close(&SDFile);
              }
          }
    }

    f_mount(&SDFatFS, (TCHAR const*)NULL, 0);
  /* USER CODE END 2 */

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

    /* USER CODE BEGIN 3 */
	  HAL_GPIO_TogglePin(LD1_GPIO_Port, LD1_Pin);
	  HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}

// （中略）

void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
	  HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
  }
  /* USER CODE END Error_Handler_Debug */
}
```


## 詳細
詳細について説明します．

```c:
/* mount SD-card */
if(f_mount(&SDFatFS, (TCHAR const*)SDPath, 0) != FR_OK){
    Error_Handler();
}
```
SDカードをマウントします．
失敗したらError_Handler()関数が呼ばれます．
Error_Handler()関数はmain.cの最下部にあるのでデバッグの際は何かしら処理を記述しておくと役に立つかも．
（自分はエラー検出用のLEDを光らす処理を入れたりしています）

```c:
/* Open file for writing (Create) */
if(f_open(&SDFile, filename, FA_CREATE_ALWAYS | FA_WRITE) != FR_OK){	// error
    Error_Handler();	// could not open / create the file
}
```
ファイルを新たに作成し，書き込みモードで開く処理です．

```c:
// Write to the text file
res = f_write(&SDFile, wtext, strlen((char *)wtext), (void *)&byteswritten);

// Write int value
sprintf(write_buffer, "value is %d\r\n", value);
res = f_write(&SDFile, write_buffer, strlen((char *)write_buffer), (void *)&byteswritten);

// write float value
// Do not forget to set "-u _printf_float" argument
sprintf(write_buffer, "value2 is %f\r\n", value2);
res = f_write(&SDFile, write_buffer, strlen((char *)write_buffer), (void *)&byteswritten);
```
ファイルに書き込む処理です．
変数の値をSDカードに書き込みたい場合はsprintf()関数で書き込むバッファ（write_buffer）内にデータを詰め，f_write()関数でSDカードに書き込みを行います．
この時の第三引数は”何バイト書き込みを行うか”を与えるのでstrlen()関数で文字列の長さを渡しています．
また，第四引数には”実際に何バイト書き込まれたか”が記録されます．


```c:
// whether writing is completed or not
if((byteswritten == 0) || (res != FR_OK)){
    Error_Handler();
}else{
    f_close(&SDFile);
}
```
書き込み処理が終了したら，エラーチェックを行います．
1バイトも書き込めなかった，もしくは正常でない状態を返した場合はError_Handler()を実行させます．

正常に書き込めていたらf_close()関数でファイルを閉じます．

```c:
/* close the file */
f_close(&SDFile);

/* unmount the SD-card */
f_mount(&SDFatFS, (TCHAR const*)NULL, 0);
```
SDカード内のファイルを閉じ，SDカードをアンマウントする処理です．
この処理を行わないとデータが保存されないので注意．

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

    /* USER CODE BEGIN 3 */
	  HAL_GPIO_TogglePin(LD1_GPIO_Port, LD1_Pin);
	  HAL_Delay(1000);
  }
```
LEDを1秒間隔で点滅させます．SDカードの処理はこの処理が行われる前に完了しているので，完了の合図としています．

```c:
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
	  HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
  }
  /* USER CODE END Error_Handler_Debug */
}
```
エラーが起きた際に実行する関数です．定義はmain.cの一番下部にあります．
今回はエラーが起きたらLD2を光らせるようにしています．

# 浮動小数点を出力するための設定
浮動小数点を出力するための設定を行います．
上部のタブから「Project」->「Properties」をクリック
![properties.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/4af29127-af0d-9d0f-a8ee-527be7506c42.png)

・「C/C++ Build」->「Settings」をクリック
・「MCU G++ Linker」->「Miscellaneous」->「Other flags」に
「**-u _printf_float**」を追加
・「Apply and Close」をクリックして終了
![properties2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/0aa79504-55fd-5430-4643-fd3f6a370cd9.png)


以上で設定は完了です．
金槌のマークをクリックしてビルドしてみます．
エラーが無いことを確認しましょう．
![build_icon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/930cf739-e967-e78a-6c66-9d691361836b.png)


# 実行
ビルドに成功したら実際にSDカードへ書き込んでみます．
まずはSDカードとF767ZIを接続します．

「SDMMC1」の「GPIO Setting」で確認したピンと，SDカードスロットの対応するピンを接続します．
VDDに3.3Vを，VSSとGNDを接続するのを忘れないように注意．

この時，
・D0
・D1
・D2
・D3
・CMD
の**5つのピンはプルアップを行うこと．**
今回は10kΩのプルアップ抵抗を使用しました．
![P_20210611_130238.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/3eb6e46c-fd41-1d4b-798d-45a29ccc1dd9.jpeg)

また，念のためにSDカードもフォーマットしておきましょう．
形式は「FAT32」です．

# 結果
マイコンボードとPCをUSBで接続し，「Run」ボタンからプログラムを実行します．
実行する前にSDカードをスロットに挿入するのを忘れずに！
![run_icon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/838af971-faa8-2fc8-352e-c70d505ae101.png)

プログラムを実行したら，LEDが点滅するまで待ちます．
点滅し始めたら無事SDカードに書き込めたので，マイコンボードからUSBケーブルを外し，SDカードもスロットから抜いておきます．

SDカードをPCに接続し，中身を見てみましょう．
SDカード内に「STM32.txt」というファイルがあり，画像のような文字が出力されているはずです．
![result1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/13fb4acb-8caf-faa2-672e-ae7521cce9a1.png)
![result2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320997/e44e75c5-7655-a904-6b2d-e7006eacf26b.png)

# おわりに
今回はSDMMC機能を使用してSDカードにデータを書き込んでみました．
SPIによる方法よりも高速かつCPUに負担がかからない方法とのことなので，FreeRTOSと組み合わせることで高速なデータロガーが作れるのではないでしょうか．

# 参考資料
[1] http://elm-chan.org/fsw/ff/00index_e.html
[2] https://youtu.be/I9KDN1o6924（ST公式による解説動画）
[3] https://programmersought.com/article/47326972171/