こんにちは、ENGかぴです。
Arduino UNO R4 WiFiのSDライブラリを使用するとSDカードを操作してデータの読み書きができます。Arduino UNOの拡張基板であるSD CARD SHIELDを使ってSDカードを操作する方法をまとめました。
SDライブラリの使用例として地磁気センサー(BM1422)から取得したデータをSDカードに保存して動作確認を行いました。
SD CARD SHIELDはアイキャッチ画像のようにArduino UNOに差し込むだけで使用することができます。地磁気センサーモジュールにAE-BM1422AGMV(秋月電子)を使用しています。
以下ではArduino UNO R4 WiFiをUNOR4-WiFiと表記します。Arduinoのライブラリを使用して動作確認したことをまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
SDライブラリを使用する
UNOR4-WiFiはArduino UNOに準拠しているためArduino環境のSDライブラリを使用することができます。SDライブラリはSPI通信を使ってSDカードを操作します。
SDライブラリのインクルードからを初期化の方法とファイルの作成方法、データの書き込み方法を説明します。
SDクラスのメンバー関数とFileクラスのメンバー関数を赤文字で記載します。
ライブラリの準備と初期化
#include <SD.h>
#define SD_CS 4
File myfile; //SDカードの状態を格納数変数
void setup() {
if(!SD.begin(SD_CS)){
Serial.println("initialization failed!");
while (1);
}
}
SDカードの操作するためにSD.hをインクルードする必要があります。SPI通信を使用しますが、SDライブラリ内でSPI通信の初期設定などが行われます。
SDカードの状態を管理するためFile型のクラス変数の変数をインスタンス化します。例ではmyfileを宣言してインスタンス化しています。このmyfileを使ってSDカードの操作に関する情報の管理を行います。
SDライブラリのbegin()関数で初期化を行います。引数にはSDカードを選択するためのスレーブセレクト(CS)のピン番号を指定します。SD CARD SHIELDは4ピンがスレーブセレクトになるように設計されているので4ピンを引数で指定します。
SDカードが挿入されていないなど初期化に失敗した場合、戻り値がfalseになります。初期化に失敗するとSDカードの操作が行えないので際の処理に進まないようにwhile(1)で無限ループするようにしています。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
SDカードのファイルを削除
String filepath = "bm1422.txt"; //SDカード内に保存するファイル名
if (SD.exists(filepath)) {
Serial.println("file exists.");
SD.remove(filepath); //ファイルを削除
Serial.println("file delete");
}
else{
Serial.println("file doesn't exist.");
}
BM1422から取得したデータは電源ONからの時間となるためファイルが残った状態で書き込むと追加してデータを書き込むため、いつのデータか分からなくなります。そのためファイルが存在している場合は、exists()関数で削除するようにします。引数には開くファイル名を含めたパスを指定します。
ファイルが存在する場合はremove()関数でファイルを削除します。引数に引数には開くファイル名を含めたパスを指定します。
SDカードにデータを書き込む
myfile = SD.open(filepath,FILE_WRITE);
if( myfile ){ //ファイルが開けたら書き込む
myfile.print("X:"); myfile.print(mag[0]); myfile.print(",");
myfile.print("Y:"); myfile.print(mag[1]); myfile.print(",");
myfile.print("Z:"); myfile.print(mag[2]); myfile.println("");
myfile.close(); //ファイルを閉じる
}
open()関数でファイルを書き込みモードで開きます。第1引数にはファイルのパスを指定し、第2引数に書き込みを示すFILE_WRITEを指定します。
ファイルの情報が戻り値にセットされるのでmyfile(Fileクラスでインスタンス化)に格納します。以降のファイルの書き込みはmyfileを使って行います。
ファイルを開いてprint()関数で書き込みます。write()関数を使用してもデータの書き込みはできますが、文字列を書き込む場合はprint()関数の方が効率よく処理できます。
X:の文字列とBM1422から取得したデータ(mag[0])を書き込んでいます。データの区切りとして「,」を入れています。これをY:、Z:と3要素分書き込みます。
最後にファイルを閉じるためclose()関数を使用します。
SDカード操作に使用する関数
SDカードの操作に頻繁に使用する関数をまとめました。
| 関数 | 説明 |
|---|---|
| Read() Read(引数1,引数2) | SDカードのデータを読み込みます。1バイトずつ読み込む場合はRead()を使用します。複数バイトをまとめて読み込む場合はRead(引数1,引数2)を使用します。引数1は読み込んだデータを格納する配列などのアドレス、引数2は読み出すデータ数を指定します。 |
| ReadStringUntil(引数) | 引数に指定した文字を確認するまで文字列として読み込みます。改行コードを区切りにする場合は’\n’を指定します。 |
| remove(引数) | 引数で指定したファイルを削除します。 |
| mkdir(引数) rmdir(引数) | mkdir()は引数に指定したSDカードのパスにディレクトリを生成します。rmdir()は指定したディレクトリを削除します。ただしファイル等がある場合は削除不可となります。 |
| write(引数) write(引数1,引数2) | 1バイトずつ書き込む場合はwrite()を使用します。引数に書き込むデータを指定します。複数バイトを一気に書き込む場合はwrite(引数1,引数2)を使用します。引数1は書き込む値を格納している配列などのアドレス、引数2は書き込むサイズを指定します。 |
| print(引数) println(引数) | 引数に文字列を格納している配列などのアドレスを指定します。直接文字列を入力することもできます。println()関数を使用すると文字列の最後尾に改行コードが挿入されます。 |
テキストデータで保存すると文字列でデータが保存されるため確認がしやすくなります。print()関数は文字列で書き込むため使用頻度が多くなります。またテキストデータの区切りを判断するため改行コードを使用することが多いためReadStringUntil()関数を使用する頻度が多くなる印象です。
動作確認

SD CARD SHILDをUNOR4-WiFiに挿入して動作確認を行います。
BM1422はSD CARD SHILDのWireのピンに配線していますがSD CARD SHILDはUNOR4-WiFiのピンを延長しているだけなので配線は同じです。
BM1422が測定した結果をSDカードに保存しますが、シリアルプロッタの結果と比較して動作確認を行いました。
シリアルプロッタを開始して電源を入れます。BM1422の測定を1秒ごとに行い、測定結果をSDカードに書き込みます。

BM1422に磁石を近づけたり向きを変えたりしながら測定結果を確認しました。次に電源を切ってSDカードを取り出してファイルを確認しました。

「BM1422.TXT」が生成されていることが確認できました。更新日時がデフォルトの時刻になっていますが、ファイルを一度開いて上書きすると更新日時が表示されるようになります。

ファイルを開くとBM1422から取得したX軸、Y軸、Z軸のデータが保存されていることが確認できました。
このファイルの拡張子を「.csv」に変更しエクセルでグラフ表示用のデータに加工してグラフ化しました。

赤枠部分はシリアルプロッタの表示部分と同一の箇所ですが見た目で一致していることが確認できました。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
#include <Wire.h>
#include <SD.h>
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIME_BM1422 100 //BM1422の計測タイマ
#define TIME_OUT 20 //BM1422の通信タイムアウト
#define TIME_LED 50
#define BM1422_ADRS 0x0E
#define CTRL1_ADRS 0x1B
#define CTRL2_ADRS 0x1C
#define CTRL3_ADRS 0x1D
#define CTRL4_ADRS 0x5C
#define CTRL4_ADRS2 0x5D
#define STA1_ADRS 0x18
#define DATA_ADRS 0x10
#define CTRL1_SET 0xC2
#define CTRL2_SET 0x0C //未使用
#define CTRL3_SET 0x40
#define CTRL4_SET 0x00
#define SD_CS 4
typedef enum{
BM1422_STEP1 = 0,
BM1422_STEP2,
BM1422_STEP3,
BM1422_STEP4,
BM1422_STEP5,
BM1422_IDLE,
BM1422_MAX
}BM1422_MODE;
uint32_t beforetimCnt = millis();
int16_t timbm1422;
int16_t timled;
int16_t timMeas;
BM1422_MODE md;
uint8_t cmadbuf[3]; //送信するデータを格納
uint8_t mdata[6]; //測定データ換算前
float mag[3]; //換算後データ
String filepath = "bm1422.txt";
File myfile;
void mainApp(void);
void mainTimer(void);
bool BM1422Cmd(uint8_t* cmd, uint8_t len);
bool BM1422GetData(uint8_t adr, uint8_t* reg_data, uint16_t len);
void MagSet(void);
void setup() {
Serial.begin(115200);
Wire.begin();
if(!SD.begin(SD_CS)){
Serial.println("initialization failed!");
while (1);
}
Serial.println("initialization done.");
if (SD.exists(filepath)) {
Serial.println("file exists.");
SD.remove(filepath); //ファイルを削除
Serial.println("file delete");
}
else{
Serial.println("file doesn't exist.");
}
}
void loop() {
mainTimer();
mainApp();
}
/* タイマ管理 */
void mainTimer(void){
if ( millis() - beforetimCnt > BASE_CNT ){
beforetimCnt = millis();
if( timbm1422 > TIME_UP ){
--timbm1422;
}
if( timled > TIME_UP ){
--timled;
}
if( timMeas > TIME_UP ){
--timMeas;
}
}
}
/* メイン処理 */
void mainApp(void){
switch(md){
case BM1422_STEP1:
cmadbuf[0] = CTRL1_ADRS;
cmadbuf[1] = CTRL1_SET;
if( BM1422Cmd(cmadbuf, 2)){
timbm1422 = TIME_OUT;
md = BM1422_STEP2;
}
else{
Serial.println("STEP1 NG");
}
break;
case BM1422_STEP2:
if( timbm1422 == TIME_UP ){
cmadbuf[0] = CTRL4_ADRS;
cmadbuf[1] = CTRL4_SET;
cmadbuf[2] = CTRL4_SET;
if( BM1422Cmd(cmadbuf, 3) ){
timbm1422 = TIME_OUT;
md = BM1422_STEP3;
}
else{
Serial.println("STEP2 NG");
}
}
break;
case BM1422_STEP3: //データ測定開始
if( timbm1422 == TIME_UP ){
cmadbuf[0] = CTRL3_ADRS;
cmadbuf[1] = CTRL3_SET;
if( BM1422Cmd(cmadbuf, 2 )){
timbm1422 = TIME_OUT;
md = BM1422_STEP4;
}
else{
Serial.println("STEP3 NG");
}
}
break;
case BM1422_STEP4:
if( timbm1422 == TIME_UP ){
timbm1422 = TIME_OUT;
uint8_t sta;
BM1422GetData(STA1_ADRS, &sta, 1);
if( sta &= 0x40 ){
md = BM1422_STEP5;
//Serial.println("Measure OK");
}
}
break;
case BM1422_STEP5:
if( timbm1422 == TIME_UP ){
timbm1422 = TIME_OUT;
BM1422GetData(DATA_ADRS, mdata, sizeof(mdata));
MagSet();
md = BM1422_IDLE;
timMeas = TIME_BM1422;
}
break;
case BM1422_IDLE:
if( timMeas == TIME_UP ){
timMeas = TIME_OFF;
md = BM1422_STEP3;
}
break;
}
if( timled == TIME_UP ){
timled = TIME_LED;
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
}
/* 磁気センサーの値を換算 */
void MagSet(void){
int16_t x;
int16_t y;
int16_t z;
x = (mdata[1] << 8) + mdata[0];
y = (mdata[3] << 8) + mdata[2];
z = (mdata[5] << 8) + mdata[4];
mag[0] = (float)x * 0.042;
mag[1] = (float)y * 0.042;
mag[2] = (float)z * 0.042;
Serial.print("X:"); Serial.print(mag[0]); Serial.print(",");
Serial.print("Y:"); Serial.print(mag[1]); Serial.print(",");
Serial.print("Z:"); Serial.print(mag[2]); Serial.println("");
myfile = SD.open(filepath,FILE_WRITE);
if( myfile ){ //ファイルが開けたら書き込む
myfile.print("X:"); myfile.print(mag[0]); myfile.print(",");
myfile.print("Y:"); myfile.print(mag[1]); myfile.print(",");
myfile.print("Z:"); myfile.print(mag[2]); myfile.println("");
myfile.close(); //ファイルを閉じる
}
}
/* コマンド送出 */
bool BM1422Cmd(uint8_t* cmd, uint8_t len){
bool ret = false;
Wire.beginTransmission(BM1422_ADRS); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){ //スレーブが存在する場合下の処理
Wire.beginTransmission(BM1422_ADRS);
for(uint8_t i= 0; i < len; i++ ){
Wire.write(*cmd);
++cmd;
}
Wire.endTransmission(); //ストップ・コンディションの発行
ret = true;
}
return ret;
}
/* データを取得 */
bool BM1422GetData(uint8_t adr, uint8_t* reg_data, uint16_t len){
bool ret = false;
Wire.beginTransmission(BM1422_ADRS); //スタート・コンディションの発行
Wire.write(adr); //書き込む対象のアドレスをセット(ライトで指定)
byte error = Wire.endTransmission();
if( error == 0){ //スレーブが存在する場合下の処理
if( Wire.requestFrom(BM1422_ADRS, len) == len ){
for( uint16_t i=0; i < len; i++ ){
*reg_data = Wire.read(); //len分だけデータをリードする
++reg_data;
}
ret = true;
}
}
return ret;
}
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:外資系・IT業界などハイクラスの転職に強い【AXIS Agent】
最後まで、読んでいただきありがとうございました。

RTCで時刻を設定した後で、時刻を含めてデータを保存するとファイルを削除する必要はありませんが、ファイルの残し方は好みになります。