【Arduino初心者向け】土壌水分量を測定してLCDにリアルタイム表示する（ESP32+SEN0193+LCD）

# :hammer:作るもの
<iframe width="560" height="315" src="https://www.youtube.com/embed/CdsgoTjHQ9E?si=wwB-e-SlQ11dHMp_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

<br>
土壌水分センサを使って、水分量を1秒ごとLCDに出力するモジュールを作ります！<br>
また、スイッチで画面を切り替えてキャリブレーション(校正)できるようにします。<br>
応用として、一定値を下回ったら自動でお水をやったり、通知する例が考えられます。<br>


<br>実際に作らずとも、Arduinoの使い方の一例として参考になれば嬉しいです！

# :paperclip:材料・配線図
1. [マイコン (ESP32)](https://www.amazon.co.jp/gp/product/B078WM1YQW/ref=ppx_yo_dt_b_asin_title_o04_s00?ie=UTF8&psc=1) ¥1,380
2. [静電容量式土壌水分センサ (SEN0193)](https://www.switch-science.com/products/4024?_pos=1&_sid=f92390e1b&_ss=r) ¥1,056
3. [LCDモジュールシールド](https://www.amazon.co.jp/Youmile-Arduino-MEGA-R3%E7%94%A8%E3%81%AE%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%AB2004-LCD%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%B7%E3%83%BC%E3%83%AB%E3%83%89/dp/B084VNTGWX/ref=d_pd_sbs_sccl_3_3/357-4697444-5023602?pd_rd_w=SrdDR&content-id=amzn1.sym.5e5de9cb-2ac2-4b73-8a61-cd4c469e9211&pf_rd_p=5e5de9cb-2ac2-4b73-8a61-cd4c469e9211&pf_rd_r=WG7HWQ5DYVQTDNRF92FD&pd_rd_wg=vLh2P&pd_rd_r=fbed3dc8-22a0-4c1f-8fa9-6cbbdd9808fc&pd_rd_i=B084VNTGWX&psc=1) ¥999
4. タクトスイッチ
5. 抵抗 (10kΩ)
6. コンデンサー (1μF 50V)
7. ジャンパワイヤ
8. ブレッドボード

![pic_SoilMoistureArticle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3560098/345ff20c-c158-ed2e-a0a3-1bc24af86e3b.png)

### 1. マイコン (ESP32)
本記事のプログラムはインターネット通信をしていませんが、Wi-Fiモジュールを搭載しているESP-WROOM-32を使っているため、インターネット通信が可能です。

### 2. 静電容量式土壌水分センサ (SEN0193)
より安価な[電気抵抗式の土壌水分センサ](https://www.amazon.co.jp/KKHMF-%E5%9C%9F%E5%A3%8C%E6%B0%B4%E5%88%86%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB-LM393-%E5%9C%9F%E5%A3%8C%E6%B0%B4%E5%88%86%E6%A4%9C%E5%87%BA-%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%81%E3%83%83%E3%82%AF%E7%B5%A6%E6%B0%B4%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0/dp/B08P7D2HQZ/ref=sr_1_9?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=RA9OME66MJL9&keywords=%E5%9C%9F%E5%A3%8C%E6%B0%B4%E5%88%86%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC&qid=1695016780&s=industrial&sprefix=%E5%9C%9F%E5%A3%8C%E6%B0%B4%E5%88%86%E3%82%BB%E3%83%B3%E3%82%B5%2Cindustrial%2C161&sr=1-9)でも良いのですが、今回は耐久性・精度が優れた静電容量式のセンサを使いました。<br>自動水やり機のように土に常に挿す場合は、耐久性のある静電容量式の方が適しています。

| マイコン | 土壌水分センサ |
| ---- | ---- |
| 3.3V | Vcc |
| GND | GND |
| GPIO26 | アナログ出力電圧 |

**参考**
[製造元の解説記事](https://wiki.dfrobot.com/Capacitive_Soil_Moisture_Sensor_SKU_SEN0193)
[静電容量型の土壌湿度センサを使ってArduinoで土の水分量測定](https://101010.fun/arduino-soil-moisture.html)

### 3. LCDモジュール
モジュールシールドが付いており、はんだ付けが不要なものを購入しました。
LCDの裏はこのようになっています。つまみを回すことで、画面の明るさを調整できます。<br>

![pic_LCD.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3560098/6597e6b7-6ffd-9c62-a26d-c2f3bab08352.png)


| マイコン | LCDモジュール |
| ---- | ---- |
| 5V | Vcc |
| GND | GND |
| SDA | SDA (GPIO21) |
| SCL | SCL (GPIO22) |

**参考**
[ArduinoでLCDに文字表示](https://101010.fun/arduino-lcd-i2c.html)

### 4. タクトスイッチ
キャリブレーションのモード切り替えのために使いました。
今回はマイコンに対してのみON/OFFを入力するため、上下の端子とも同じ配線にしています。
タクトスイッチの原理については、下記サイトさんの解説がとてもわかりやすいです。<br>
**参考**
[Arduino（アルディーノ）電子工作の基本⑥ スイッチの状態を読み取る](https://deviceplus.jp/arduino/arduino_f06/)

### 5. 抵抗 (10kΩ)
抵抗は、`マイコン <-> タクトスイッチ` 接続部のプルアップのために使いました。
プルアップ抵抗とは、電源やGNDとモジュール間の接続がない状況を避け、動作を安定させるために追加します。
詳しい原理については、下記サイトさんの解説がとてもわかりやすいです。<br>
**参考**
[プルアップ抵抗・プルダウン抵抗とは？電子回路に必須の考え方](https://voltechno.com/blog/pullup-pulldown/)

### 6. コンデンサー (1μF 50V)
コンデンサーは、ESP32へ安定してスケッチを書き込むために使いました。
ESP32にそのままスケッチを書き込もうとした際、以下のように、ESP32とPCが正常に通信できなかったと怒られました_:(´ཀ`」 ∠):
```
A fatal error occurred: Timed out waiting for packet header
```
そこで、`EN <-> GND` へ0.1μF~2.2μF程度のコンデンサを追加することで、正常に書き込めるようになりました。
コンデンサーを追加してもエラーが解決しない場合は、下記サイトさんをご参照ください。<br>
**参考**
[書込エラー対応「A fatal error occurred: xxxxx」](https://kit.socinno.com/2020/02/26/timed-out-packet-header/)


# :pen_fountain:プログラム
```c:土壌水分量測定プログラム
#include <Arduino.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>

#define SEN0193_PIN 26
#define SWITCH_PIN 27

LiquidCrystal_I2C lcd(0x27,16,4);

int ADC_AIR = 0; // 空気中の実測値
int ADC_WET = 0; // 水に浸した実測値

int mode = 0;    // モード切り替え用

/* センサの測定値を水分量[%]に変換する関数 */
int getMoisture(int val) {
    if (val > ADC_AIR) {
        val = ADC_AIR;
    }
    else if (val < ADC_WET) {
        val = ADC_WET;
    }

    int mois = 100 - map(val, ADC_WET, ADC_AIR, 0, 100);
    return mois;
}

/* ADC_AIR,ADC_WATにEEPROMの値を格納する関数 */
void getEEPROM() {
    EEPROM.get(0, ADC_AIR);
    EEPROM.get(4, ADC_WET);
}

/* setup関数 */
void setup(void) {
    Serial.begin(115200);

    pinMode(SEN0193_PIN, INPUT);
    pinMode(SWITCH_PIN, INPUT);

    lcd.init(); 
    lcd.backlight();
    lcd.setCursor(0, 0);
    lcd.print("Ready...");

    EEPROM.begin(4*2); // int=4byte * 2vals
    delay(1000);
    getEEPROM();

    delay(3000);
}

/* モードを変更する関数 */
void changeMode() {
    if(mode == 0)      { mode = 1; }
    else if(mode == 1) { mode = 2; }
    else if(mode == 2) { mode = 3; }
    else if(mode == 3) { mode = 0; }
}

/* loop関数 */
void loop(void) {
    int vol = analogRead(SEN0193_PIN);
    int moisture = getMoisture(vol);

    /* ボタンが押されたらモード切り替え */
    if(digitalRead(SWITCH_PIN) == LOW) {
        changeMode();
    }

    switch(mode) {
        /* mode0: 毎秒測定して水分量を表示する */
        case 0:
            lcd.clear();
            lcd.setCursor(0, 0);
            lcd.print("MODE0: Measure");
            lcd.setCursor(0, 1);
            lcd.print("Vol : " + String(vol));
            lcd.setCursor(0, 2);
            lcd.print("Mois: " + String(moisture) + " %");
            lcd.setCursor(0, 3);
            lcd.print("Set : A=" + String(ADC_AIR) + " W=" + String(ADC_WET));
            
            Serial.println("Vol: " + String(vol) + "  Mois: " + String(moisture) + " %");

            break;
        
        /* mode1: 水分量0%をキャリブレーションする */
        case 1:
            lcd.clear();
            lcd.setCursor(0, 0);
            lcd.print("MODE1: Calib AIR");
            lcd.setCursor(0, 1);
            lcd.print("Vol : " + String(vol));
            ADC_AIR = vol;

            break;
        
        /* mode2: 水分量100%をキャリブレーションする */
        case 2:
            lcd.clear();
            lcd.setCursor(0, 0);
            lcd.print("MODE2: Calib WET");
            lcd.setCursor(0, 1);
            lcd.print("Vol : " + String(vol));
            ADC_WET = vol;
            
            break;

        /* mode3: キャリブレーションした値をEEPROMに保存する */
        case 3:
            lcd.clear();
            lcd.setCursor(0, 0);
            lcd.print("MODE3: Saved ^-^");
            lcd.setCursor(0, 3);
            lcd.print("Set : A=" + String(ADC_AIR) + " W=" + String(ADC_WET));

            EEPROM.put(0, ADC_AIR);
            EEPROM.put(4, ADC_WET);
            EEPROM.commit();  // EEPROMへ上書き

            delay(1000);

            changeMode();  // mode0に切り替え

            break;
    }

    delay(1000);
}
```

### 4つのモード
プログラムには以下の4つのモードがあり、スイッチが押されると次のモードに推移します。(正確には、loop関数が呼び出されるタイミングで押されていたら推移します)<br>

| Mode | 実行内容 |
| :---: | ---- |
| 0 | **水分量を測定する**モード。<br>LCDの2行目に測定値を、3行目に水分量の計算結果を、4行目に現在設定されている0%,100%時の測定値を表示する。 |
| 1 | **0%時の測定値**をキャリブレーションするモード。<br>LCDの2行目に現在の測定値を表示し、スイッチが押されたタイミングの測定値を0%として保存する。 |
| 2 | **100%時の測定値**をキャリブレーションするモード。<br>LCDの2行目に現在の測定値を表示し、スイッチが押されたタイミングの測定値を100%として保存する |
| 3 | 測定結果を**EEPROMに保存する**モード。<br>保存後はMode0に自動で戻る。 |

### 水分量の計算
水分量の計算 `getMoisture()` は、空気中での測定値を最小値0%、水中での測定値を最大値100%と仮定して、簡易的に線形マッピングしているだけです。
実際の土を使う場合、乾燥した状態で空気中の測定値を、十分湿らせた状態で水中の測定値をキャリブレーションすることで、より細かく水分量を算出できると思います。


### EEPROM (不揮発性メモリ)
このプログラムでは、キャリブレーションした0%,100%の測定値をもとに水分量を計算していますが、これらの値が電源を切って消えてしまうと、再度キャリブレーションしなくてはなりません。
それは非常に面倒なので、電源を切っても値を保持しておけるEEPROM (不揮発性メモリ) を使用しています。
ここに格納することで、再起動しても直近のキャリブレーション時の測定値をもとに水分量を測定できます。<br>
ただし、EEPROMの物理的な構造上**書き込み回数に制限があります。**<font color="gray">
(といっても10万回くらいはいけるようです)
</font>そのため、このプログラムでは、`setup()`で1回、`loop()`でMode3になった時のみ1回使用するようにしています。
このような物理的な制約があるのも、電子工作の面白いところです(´・ω・`)<br>

**参考**
[ArduinoのEEPROMを使う](http://7ujm.net/micro/arduino_eeprom.html)<br>


# :sparkles:終わりに
私は農業のIoT化をテーマに共同研究をしていて、その遊びがてらこのモジュールを作成しました。今回はインターネット通信をしていないのでIoTではないですが…笑
水分量(体積含水率)の正確な測定には、塩害やセンサの寿命を考慮する必要があり、土壌水分センサ1つ+今回のような簡易的なキャリブレーションでは不十分です。
現在は畑での灌水モジュールを作成中で、論文サーベイや議論をする毎日です。農学系の方と話すと、自分と全く視点が違ってとても勉強になります。<br>
ありがとうございました＞ω＜/