PR

Arduino UNO R4 WiFiで土壌センサーのデータを取得する

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

Arduino環境ではアナログ入力をデジタル値として扱うためのAD変換機能が標準で備わっています。本記事ではこの機能を使って土壌水分センサーの出力電圧を読み取り、水分量の変化を確認して動作確認を行いました。

土壌センサーはMAKER SOIL MOISTURE SENSOR(Cytron製:秋月電子で購入)を使用しています。

Arduino UNOの拡張基板であるSD CARD SHIELDを使ってSDカードに土壌の状態を保存します。

Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリを使用して動作確認したことをまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

土壌センサーの情報を取得する

MAKER SOIL MOISTURE SENSOR(以下土壌センサーとします。)は土壌水分を静電容量検出で測定し、土壌の水分によって容量が変化することを利用して電圧を出力するセンサーです。水分量が多いほど土壌のインピーダンスが低くなるため電圧が低くなります。

下記リンクにセンサーの詳細情報があります。

メーカー用土壌水分センサー(静電容量式)

土壌センサーはLEDインジケータでDRY・MOIST・WETの3段階で土壌の水分量の目安が分かるように設計されています。

ただし、電源のレギュレータによる安定した電源で構成されていないため電源電圧によって判定値が変動する場合があります。

ArduinoのGrove端子に対応しておりGrove端子があれば接続するだけで簡単に使用することができます。標準ライブラリを使用してアナログ値を読み取って土壌の状態を確認することができます。

PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

センサー情報の使用例

if( timdataget == TIME_UP){
  timdataget = TIM_MEAS;
  soil.buf[soil.wp] = analogRead(A0);

  if(++soil.wp >= 8){
    soil.wp = 0;
  }

  sum = 0;
  for(uint8_t i =0; i < 8; i++){
    sum += soil.buf[i];
  }

  soil.humid = sum >> 3;
}

アナログデータはノイズの影響などで実際の値から少し上下し誤差を含むため数回分のデータを移動平均した(平均値)を計算して使用します。

アナログ電圧の取得にはanalogRead()関数を使用します。引数にアナログピンの番号を指定すると、AD変換された値が戻り値として返されるため、変数に代入します。(3行目)

例では1秒毎にアナログ変換し、最新のデータから8個前までのデータで平均値を算出しています。(9~14行目)平均化する際に>>3で3回右にシフトしていますが8で割るのと同じ結果になります。そのため平均値をとるデータ数を2の倍数にしています。

マイコンが計算する際にビットシフトで計算すると計算時間を減らすことができます。演算器を持たないマイコンを使用する場合においては効果的な方法です。

PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール

土壌センサーの検証

土壌センサーの検証

土壌センサーの出力電圧とLEDインジケータでDRY・MOIST・WETの関係を確かめるため、土壌センサーのプローブを徐々に濡らしていきます。

プローブをスポンジに密着させた状態で、霧吹きを使って少しずつ水分を加えていき、そのときのセンサー値の変化をシリアルモニターで確認します。

最初に乾いた状態を確認します。

DRYのシリアルモニターの結果とLEDインジケータの様子
DRYのシリアルモニターの結果とLEDインジケータの様子

乾燥したスポンジをプローブに当てたままシリアルモニターの値を確認しました。AD変換のデジット値は732程度で安定していました。スポンジを外して空気に触れさせても同様の結果でした。LEDインジケータはDRYを示す赤色のLEDが点灯しました。

次にLEDインジケータが切り替わるまで霧吹きでスポンジを濡らしていきました。

MOISTのシリアルモニターの結果とLEDインジケータの様子
MOISTのシリアルモニターの結果とLEDインジケータの様子

LEDインジケータがDRYからMOISTに切り替わるまでスポンジを濡らしていくとAD変換のデジット値が627程度になったとき安定しました。LEDインジケータはMOISTを示す緑色のLEDが点灯しました。

さらにスポンジを濡らしてLEDインジケータがWETに切り替わるまで濡らしていきます。

WETのシリアルモニターの結果とLEDインジケータの様子
WETのシリアルモニターの結果とLEDインジケータの様子

LEDインジケータがMOISTからWETに切り替わるまでスポンジを濡らしていくとAD変換のデジット値が553程度になったとき安定しました。LEDインジケータはWETを示す青色のLEDが点灯しました。参考までに直接水につけた場合のデジット値を確認すると490程度になりました。

スポンジの様子を確認するとWET状態でかなり水を含んでいたことから、植物の根腐れやカビが生えてしまう原因になるため土壌の管理はMOISTからDRYの間になるように管理したほうがよさそうです。

PR:技術系の通信教育講座ならJTEX

SDカードに履歴保存

void saveSd(void){

  if( timsave == TIME_UP ){
    timsave = TIM_SAVE;

    myfile = SD.open("/soil.txt",FILE_WRITE);

    if( myfile ){ //ファイルが開けたら書き込む
      myfile.print(datestr);
      myfile.print(" Value:");
      myfile.println(soil.humid);
      myfile.close(); //ファイルを閉じる
    }
  }
}

SDカードに土壌の水分量の履歴を保存します。SDカードの操作は下記記事にまとめています。

Arduino UNO R4 WiFiでSDカードを操作する

履歴データの時刻はRTCを使って管理しています。RTCの使い方を下記記事にまとめています。

Arduino UNO R4 WiFiのRTCを使用する

土壌は直射日光が当たらない限り乾燥しにくいため頻繁に履歴を保存する必要はありません。本記事では1分ごとに履歴を保存するようにしていますが10分でもよいと思います。

PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!

動作確認

動作確認用の回路図
動作確認用の回路図

SD CARD SHILDをUNOR4-WiFiに挿入して動作確認を行います。土壌センサーのOUTをA0に配線していますがSD CARD SHILDはUNOR4-WiFiのピンを延長しているだけなので配線上同じになります。

土壌センサーのDISはセンサーの測定を操作します。LOWにすると測定が有効になりHIGHにすると測定を停止します。IOTモジュールなど省電力で動作させたい場合に使用すると効果的です。本記事では常に測定できるようにLOWにしています。

動作確認は土壌センサーの検証で確認した通りのものが履歴に保存できているかを確認します。

SDカードに保存した履歴ファイル
SDカードに保存した履歴ファイル

約1分ごとにRTC時刻とAD変換のデジット値が保存されていることが確認できました。

RTC時刻はArduino UNO R4 WiFiのRTCを使用するで作成した自作の電文で変更できます。例えば RTC2026-05-03-00-12-30:30 の文字列を送信すると2026年5月3日(日)12時30分30秒になります。

SDカードに履歴を残しながらWiFiを使ってログをブラウザー表示するなど応用例が広がります。

スポンサーリンク

ソースコード全体

ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。

#include <SD.h>
#include "RTC.h"

#define SD_CS 4
#define SOIL_DIS 7
#define MEAS_MAX 8
#define TIM_MEAS 100
#define TIM_SAVE 6000
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIM_RTC 100
#define RTC_READ 100
#define RING_SZ 128
#define TIM_RX_WAIT 5   //50ms
#define OFFSET 3

struct DIS_MEAN{
  uint8_t wp;
  uint32_t buf[MEAS_MAX];
  uint32_t humid;
};

struct RING_MNG{
  uint8_t wp;
  uint8_t rp;
  uint8_t buf[RING_SZ];
};

uint32_t beforetimCnt = millis();
int16_t  timdataget;
int32_t  timsave;
String filepath = "/soil.txt";
File myfile;
DIS_MEAN soil;
int16_t timrtc;
int16_t timrcv = TIME_OFF;
char datestr[32];
char str[] = {"2025/09/22-00-21:30:30"};
char week[] = {"sun"};
uint8_t Rcvdata[sizeof(str) + OFFSET];
RING_MNG rcv;

void mainTimer(void);
void mainApp(void);
void saveSd(void);
void mainRtc(void);
void WeekSet(int wk );
void rcvdatechk(void);
void ReadPointerAdd(void);
bool chkdata(void);

void setup() {

  Serial.begin(115200);
  pinMode(SOIL_DIS,OUTPUT);

  if(!SD.begin(SD_CS)){
    Serial.println("initialization failed!");
    while (1);
  }

  RTC.begin();
  RTCTime init(3, Month::MAY, 2026, 9, 0, 30, DayOfWeek::SUNDAY, SaveLight::SAVING_TIME_INACTIVE );
  RTC.setTime(init);
  delay(500);
  Serial.println("Init Start!");

  for( uint8_t i=0; i < 10; i++ ){
    timdataget = TIME_UP;
    timrtc = TIME_UP;
    mainRtc();
    mainApp();
    delay(1000);
  }

  Serial.println("Init End!");
}

void loop() {

  mainTimer();
  mainApp();
  mainRtc();
  rcvdatechk();
  saveSd();
}

void mainApp(void){
  uint16_t sum;

  if( timdataget == TIME_UP){
    timdataget = TIM_MEAS;
    soil.buf[soil.wp] = analogRead(A0);

    if(++soil.wp >= MEAS_MAX){
      soil.wp = 0;
    }

    sum = 0;
    for(uint8_t i =0; i < MEAS_MAX; i++){
      sum += soil.buf[i];
    }

    soil.humid = sum >> 3;
    Serial.print(datestr);
    Serial.print(" Moisture Sensor Value:");
    Serial.println(soil.humid);
  }
}
/* SDカード管理 */
void saveSd(void){

  if( timsave == TIME_UP ){
    timsave = TIM_SAVE;

    myfile = SD.open(filepath,FILE_WRITE);

    if( myfile ){ //ファイルが開けたら書き込む
      myfile.print(datestr);
      myfile.print(" Value:");
      myfile.println(soil.humid);
      myfile.close(); //ファイルを閉じる
    }
  }
}
/* タイマ管理 */
void mainTimer(void){

  if ( millis() - beforetimCnt >= BASE_CNT ){
    beforetimCnt = millis();

    if( timdataget > TIME_UP ){
      --timdataget;
    }

    if( timsave > TIME_UP ){
      --timsave;
    }

    if( timrtc > TIME_UP ){
      --timrtc;
    }

    if( timrcv > TIME_UP ){
      timrcv--;
    }
  }
}

/* メイン処理関数 */
void mainRtc(void){

  while(Serial.available()){
    rcv.buf[rcv.wp] = Serial.read();

    if( ++rcv.wp >= RING_SZ ){
      rcv.wp = 0;
    }
  }

  if( timrtc == TIME_UP ){
    timrtc = TIM_RTC;

    RTCTime now;
    if( RTC.getTime(now) ){
      WeekSet(DayOfWeek2int(now.getDayOfWeek(),false));
      sprintf(datestr,"%d/%02d/%02d %s %02d:%02d:%02d",
              now.getYear(),
              Month2int(now.getMonth()),
              now.getDayOfMonth(),
              week,
              now.getHour(),
              now.getMinutes(),
              now.getSeconds() );

      //Serial.println(datestr);
    }
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }
}
/* 曜日文字列生成 SUNDAY が先頭の場合 */
void WeekSet( int wk ){

  switch(wk){
    case 1:
      sprintf(week,"MON");
      break;
    case 2:
      sprintf(week,"TUE");
      break;
    case 3:
      sprintf(week,"WED");
      break;
    case 4:
      sprintf(week,"THU");
      break;
    case 5:
      sprintf(week,"FRI");
      break;
    case 6:
      sprintf(week,"SAT");
      break;
    default:
      sprintf(week,"SUN");
      break;
  }
}

/* 受信データから時刻データを生成 */
void rcvdatechk(void){
  int8_t  rxsz;
  uint8_t allsz;
  uint8_t rp = rcv.rp;  
  uint8_t i;
  uint8_t dat[3];

  if( timrcv == TIME_UP){
    timrcv = TIME_OFF;
    ReadPointerAdd();    
  }

  rxsz = rcv.wp - rcv.rp; //受信データ数の算出
    
  if( rxsz < 0 ){
    rxsz = rxsz + sizeof(rcv.buf);
  }
            
  if( rxsz == 0 ){
    timrcv = TIME_OFF;
  }
  else{
    if( timrcv == TIME_OFF ){
      timrcv = TIM_RX_WAIT;
    }

    if( rxsz >= 3 ){
      for( i = 0; i < 3; i++){//データサイズ算出のため仮おき
        dat[i] = rcv.buf[ rp ];
        if(++rp >= sizeof(rcv.buf) ) rp = 0;
      }

      if( dat[0] == 'R' && dat[1] == 'T' && dat[2] == 'C'){
        allsz = sizeof(str) + OFFSET - 1;
        
        if( rxsz >= allsz){
          timrcv = TIME_OFF;
                          
          for(i=0; i < sizeof(Rcvdata); i++){
            Rcvdata[i] = 0;
          }
                          
          for( i=0; i < allsz; i++ ){
            Rcvdata[i] = rcv.buf[rcv.rp];
            ReadPointerAdd();
          }

          if( chkdata() == false ){
            Serial.println("RTCSET NG!");
          }
        }
      }
    }
  }
}

/* 読み込み位置の更新 */
void ReadPointerAdd(void){
    
  if(++rcv.rp >= sizeof(rcv.buf) ){
    rcv.rp = 0;
  }
}

/* 時刻のチェック */
bool chkdata(void){
  int d;
  Month m;
  int y;
  int h;
  int min;
  int s;
  DayOfWeek dotw;
  SaveLight su;
  bool ret = false;

  //RTC2025-08-10-00-12-00-00
  y = strtol((const char*)&Rcvdata[3], NULL, 10);
  m = (Month)(strtol((const char*)&Rcvdata[8], NULL, 10)-1);
  d = strtol((const char*)&Rcvdata[11], NULL, 10);
  dotw = (DayOfWeek)strtol((const char*)&Rcvdata[14], NULL, 10);
  h = strtol((const char*)&Rcvdata[17], NULL, 10);
  min = strtol((const char*)&Rcvdata[20], NULL, 10);
  s = strtol((const char*)&Rcvdata[23], NULL, 10);
  
  if( y >= 2000 && y < 2100 ){
    if( m >= Month::JANUARY && m <= Month::DECEMBER ){
      if( d >= 1 && d <= 31 ){
        if( h >= 0 && h <= 23 ){
          if( min >= 0 && min <=59 ){
            if( s >= 0 && s <= 59 ){
              if( dotw >= DayOfWeek::SUNDAY && dotw <= DayOfWeek::SATURDAY ){
                RTCTime setT(d, m, y, h, min, s, dotw, SaveLight::SAVING_TIME_INACTIVE);
                RTC.setTime(setT);
                Serial.println("RTCSET OK!");
                ret = true;
              }
            }
          }
        }
      }
    }
  }

  return ret;
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

Raspberry Pi Picoで学べるソフト開発

PR:無料トライアル実施中【PC専用】AIスライド資料作成ツールの利用:イルシル

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました