【備忘録】Seeeduino Nano+Groveで水温・TDS・濁度・GPSを同時に読む|USV用センシング装置の配線とスケッチ

【備忘録】Seeeduino Nano+Groveで水温・TDS・濁度・GPSを同時に読む|USV用センシング装置の配線とスケッチ

USV(無人水上艇)を作っています。個人の技術検証です。

前回は、USV に載せる映像伝送装置を作りました。記事は Raspberry Pi 5 + USB カメラ2台の映像伝送装置 にまとめてあります。

USV を作るので、今回は、水中を手軽にセンシングできる装置に着手しました。

  • 水温を測りたい
  • 水質の目安(TDS)を測りたい
  • 濁りの度合いを測りたい
  • 測った場所(緯度・経度)も一緒に残したい

この4つを、Seeeduino Nano と Grove のセンサで1台にまとめました。はんだ付けは1か所だけです。

この記事は、私自身の備忘録です。配線と、完成版のスケッチを丸ごと残します。

この記事でできるようになること

  • Seeeduino Nano 1台で、水温・TDS・濁度・GPS の4つを同時に読めるようになる
  • Grove コネクタが付いていない DS18B20 防水温度センサーを、Grove のポートにつなげるようになる
  • Seeeduino Nano を PC につないでも COM ポートが出ないときに、原因と対処が分かる
  • 測定値を JSON で1行ずつ出力し、Raspberry Pi で受け取れる形にできる

前提条件と構成

ハードウェアはこの組み合わせです。

役割実体接続先
マイコンSeeeduino Nano
拡張基板Grove Shield for Arduino NanoSeeeduino Nano に載せる
水温DROK DS18B20 防水温度センサー(2個セットのうち1個)D2 ポート
水質Grove TDS水質センサA0 ポート
濁りGrove 濁度センサA2 ポート
位置Grove GPSD4 ポート

ライブラリは次の3つを Arduino IDE のライブラリマネージャから入れます。SoftwareSerial は Arduino IDE に最初から入っています。

  • OneWire
  • DallasTemperature
  • TinyGPSPlus

DS18B20 の通信方式は 1-Wire で、動作電圧は 3.0V〜5.5V です。GPS は 9600bps のシリアルで、SoftwareSerial を使って D4 と D5 で受けます。

最初に詰まった箇所:COM ポートが表示されない

今回、私が詰まったのはここだけです。ただし、一番最初に当たります。

Seeeduino Nano を USB で PC につないでも、Arduino IDE のポート一覧には COM1 しか出ませんでした。Seeeduino Nano のポートが一覧にないので、書き込み先を選べません。

原因は、USB シリアル変換のドライバーが PC に入っていなかったことです。Seeeduino Nano は、つなぐだけでは認識されません。ドライバーが必要です。 私は、これに後から気付きました。

入れたのは、Silicon Labs の CP210x USB to UART Bridge VCP Drivers です。

CP210x USB to UART Bridge VCP Drivers(Silicon Labs)

ドライバーを入れると、ポート一覧に COM1 以外のポートが現れます。それが Seeeduino Nano です。

ケーブルやボードの故障を疑う前に、まずポート一覧を見てください。COM1 しか出ていなければ、ドライバーです。

DS18B20 は Grove にそのまま挿さらない

DROK の DS18B20 防水温度センサーは、先端がコネクタなしのバラ線3本です。

線の色役割
VCC
DATA
GND

Grove のコネクタは4ピンなので、このままでは挿さりません。私は、Grove ケーブルを途中で切り、センサーの線とはんだ付けしました。

Grove ケーブルの配色は、黄が信号、白が未使用、赤が VCC、黒が GND です。センサー側と色の意味がそろっているので、同じ色どうしをつなげば済みます。

  • センサーの赤 → Grove の赤
  • センサーの黄 → Grove の黄
  • センサーの黒 → Grove の黒
  • Grove の白 → 使わない

できたケーブルを、Grove Shield の D2 ポートに挿します。

4.7kΩ の抵抗は、赤と黄の間に入れる

DS18B20 には、4.7kΩ のプルアップ抵抗が必要です。DROK のセンサーには、この抵抗が同梱されています。

私は、赤(VCC)と黄(DATA)の間に入れました。

抵抗が要る理由は、DS18B20 の DATA 線の作りにあります。DS18B20 は、DATA 線を Low(0V)に引くことしかできません。High に戻す役目は、プルアップ抵抗が担います。

抵抗がないと、DATA 線が High に戻らず、温度を読めません。 典型的な症状は、読み値が -127℃ や 85℃ のまま動かない、またはセンサーが検出されない、です。

Grove ケーブル自体には、この抵抗は入っていません。バラ線の DS18B20 を Grove につなぐときは、自分で入れます。

Grove 濁度センサは A2 に挿すだけ

水質を見るなら、濁りも一緒に測りたくなります。追加したのは、Grove の濁度センサ(Seeed、型番 SEEED-101020752)です。

Grove – 濁度センサ(スイッチサイエンス)

DS18B20 と違い、こちらは加工が要りません。20cm の Grove ケーブルが付属するので、Grove Shield の A2 ポートに挿すだけです。動作電圧は 3.3V/5V で、Seeeduino Nano の 5V でそのまま動きます。

つなぐ手順は3つです。

  1. プローブを、付属の接続用ケーブルでセンサー基板につなぐ
  2. 基板上の切替スイッチを「A」(アナログ)側にする
  3. 付属の Grove ケーブルで、センサー基板を A2 ポートにつなぐ

切替スイッチが「D」側のままだと、しきい値を超えたかどうかの ON/OFF しか出ません。 濁りの度合いが要るので、「A」側にします。

基板上の青いつまみ(半固定抵抗)は、デジタル出力のしきい値を調整する部品です。アナログ側の値は、このつまみを回しても変わりません。

扱いで注意する点が3つあります。

  • プローブの上面(ケーブルが出ている側)は、防水ではありません。 水に入れるのは、先端の U 字の部分だけです。全体を沈めると壊れます
  • このセンサーは、光の透過量で濁りを測ります。直射日光や強い外光が当たると、値がずれます
  • U 字の隙間に気泡が付くと、値が下がります。値が落ち着かないときは、プローブを軽く振って気泡を落とします

出力は電圧です。光がよく通るほど電圧は高く、濁るほど低くなります。

まず配線を確かめるスケッチ(シリアル表示版)

丸ごとコピーして、Arduino IDE に貼って書き込みます。このスケッチは、水温・TDS・GPS の3つだけを人が読める形で出します。濁度センサは、後の最終版で足します。

#include <OneWire.h>
#include <DallasTemperature.h>
#include <SoftwareSerial.h>
#include <TinyGPSPlus.h>

const int PIN_DS18B20 = 2;    // D2ポート:DS18B20防水温度センサー
const int PIN_TDS     = A0;   // A0ポート:Grove TDS水質センサ
SoftwareSerial gpsSerial(4, 5);   // D4ポート:Grove GPS(RX=4, TX=5)

OneWire oneWire(PIN_DS18B20);
DallasTemperature sensors(&oneWire);
TinyGPSPlus gps;

unsigned long lastPrint = 0;
float waterTemp = 25.0;

// GPSのデータを読み込む
void readGPS() {
  while (gpsSerial.available()) {
    gps.encode(gpsSerial.read());
  }
}

void setup() {
  Serial.begin(9600);
  gpsSerial.begin(9600);
  sensors.begin();
  sensors.setWaitForConversion(false);  // 温度変換中もGPS受信を止めない
  sensors.requestTemperatures();
}

void loop() {
  readGPS();

  // 2秒ごとに測定値を表示する
  if (millis() - lastPrint >= 2000) {
    lastPrint = millis();

    // 水温
    float t = sensors.getTempCByIndex(0);
    if (t != DEVICE_DISCONNECTED_C) waterTemp = t;
    sensors.requestTemperatures();   // 次回分の変換を開始

    // TDS:30回の平均を取る(合間にGPSも読む)
    long sum = 0;
    for (int i = 0; i < 30; i++) {
      sum += analogRead(PIN_TDS);
      delay(2);
      readGPS();
    }
    float v = (sum / 30.0) * 5.0 / 1024.0;

    // TDS:水温で25℃基準に補正
    float vComp = v / (1.0 + 0.02 * (waterTemp - 25.0));
    float tds = (133.42 * vComp * vComp * vComp
               - 255.86 * vComp * vComp
               + 857.39 * vComp) * 0.5;

    // 表示
    Serial.print("水温: ");  Serial.print(waterTemp); Serial.print(" ℃  ");
    Serial.print("TDS: ");   Serial.print(tds, 0);    Serial.print(" ppm  ");

    if (gps.location.isValid()) {
      Serial.print("緯度: ");   Serial.print(gps.location.lat(), 6);
      Serial.print("  経度: "); Serial.print(gps.location.lng(), 6);
    } else {
      Serial.print("GPS: 測位中");
    }
    Serial.print("  受信文字数: "); Serial.print(gps.charsProcessed());
    Serial.print("  衛星数: ");     Serial.println(gps.satellites.value());
  }
}

書き込んだら、シリアルモニタを 9600bps で開きます。2秒ごとに1行、水温・TDS・GPS の状態が出ます。

スケッチのポイント3つ

後から読み返したときに理由を忘れそうな箇所を、3つ書いておきます。

1. 温度変換の待ち時間で GPS を止めない

DS18B20 は、温度の変換に時間がかかります。DallasTemperature ライブラリは、既定ではこの変換が終わるまで待ちます。

待っている間、SoftwareSerial で受けている GPS のデータを読めません。そこで setWaitForConversion(false) を指定し、待たずに先へ進むようにしています。

流れはこうです。変換を依頼しておき、2秒後に結果を受け取り、その場で次の変換を依頼します。温度は「前回依頼した分」を読む形になります。 水温は2秒では大きく変わらないので、これで足ります。

2. TDS は30回の平均で読む

TDS センサの値は、1回読みだと揺れます。手元では、平均を入れる前は ±5 ppm ほど揺れていました。30回の平均にしてからは、値が安定しました。

平均を取るループの中でも、毎回 readGPS() を呼んでいます。30回 × 2ms の間に届いた GPS のデータを、取りこぼさないためです。

3. TDS は水温で補正する

TDS の値は、水温で変わります。スケッチでは、DS18B20 で測った水温を使って、25℃ 基準の値に補正しています。

float vComp = v / (1.0 + 0.02 * (waterTemp - 25.0));

水温センサと TDS センサを同じマイコンにつないだのは、この補正のためでもあります。DS18B20 が読めなかったときは、waterTemp の初期値の 25.0℃ で計算します。

動作確認:水道水を測った結果

水道水を測った値は、次のとおりでした。

項目
水温23.69 ℃
TDS106 ppm

GPS は、屋内で確認しました。受信文字数は、411 → 759 → 1104 → 1449 と、1行ごとに約350ずつ増えました。衛星数は 0 でした。

この2つの数字を分けて表示しているのは、切り分けのためです。

表示意味
受信文字数が増えていくGPS モジュールからデータが届いている。配線は正しい
受信文字数が 0 のままデータが届いていない。D4 ポートに挿さっているかを確認する
衛星数が 0衛星を捕捉していない。屋内ではこれが普通
衛星数が 4 以上「GPS: 測位中」が、緯度と経度の表示に変わる

「測位中」のままでも、受信文字数が増えていれば配線は合っています。 直すのは配線ではなく、置き場所です。窓際か屋外に持っていきます。初回の測位は、数分かかります。

屋外で確認するときは、ノート PC につないだまま持ち出します。モバイルバッテリーで給電すると動きはしますが、シリアルモニタを見られません。

濁度センサは、水道水と遮光で両端を測る

濁度センサは、きれいな水道水での値と、完全に遮光したときの値の2つを測ります。手元の値は次のとおりでした。

状態濁度センサの電圧
きれいな水道水に、プローブの U 字だけを入れた状態3.45 V
U 字の隙間を完全に遮った状態0.03 V

遮ると下がり、遮るのをやめると 3.45V に戻れば、センサーと配線は合っています。 実際の濁った水は、この2つの値の間に入ります。水が濁るほど、3.45V から下がっていきます。

この2つの値が、後で濁り度(%)に変換するときの基準値になります。

実際に使うときのスケッチ(JSON 出力版・4センサ)

ここまでのスケッチは、人がシリアルモニタで読むための表示でした。実際に使うときは、濁度センサを足し、出力を JSON 形式に変えたスケッチを書き込みます。JSON にした理由は、Raspberry Pi で読めるようにするためです。

表示版から変えたのは3点です。配線とライブラリは同じです。

  • A2 の濁度センサを足した
  • アナログ入力を30回読む処理を readVoltage() という関数にまとめ、TDS と濁度の両方で使うようにした
  • 出力を JSON の1行にし、濁り度(%)を足した
#include <OneWire.h>
#include <DallasTemperature.h>
#include <SoftwareSerial.h>
#include <TinyGPSPlus.h>

const int PIN_DS18B20 = 2;    // D2ポート:DS18B20防水温度センサー
const int PIN_TDS     = A0;   // A0ポート:Grove TDS水質センサ
const int PIN_TURB    = A2;   // A2ポート:Grove 濁度センサ
SoftwareSerial gpsSerial(4, 5);   // D4ポート:Grove GPS(RX=4, TX=5)

// 濁度センサの基準値(設置場所で測り直したら、この2つを変える)
const float TURB_CLEAR_V = 3.45;  // きれいな水での電圧(濁り度0%)
const float TURB_DARK_V  = 0.03;  // 完全に遮光したときの電圧(濁り度100%)

OneWire oneWire(PIN_DS18B20);
DallasTemperature sensors(&oneWire);
TinyGPSPlus gps;

unsigned long lastPrint = 0;
float waterTemp = 25.0;

// GPSのデータを読み込む
void readGPS() {
  while (gpsSerial.available()) {
    gps.encode(gpsSerial.read());
  }
}

// アナログ入力を30回読んで、平均の電圧(V)を返す
float readVoltage(int pin) {
  analogRead(pin);                 // ピン切替直後の1回目は捨てる
  long sum = 0;
  for (int i = 0; i < 30; i++) {
    sum += analogRead(pin);
    delay(2);
    readGPS();
  }
  return (sum / 30.0) * 5.0 / 1024.0;
}

void setup() {
  Serial.begin(9600);
  gpsSerial.begin(9600);
  sensors.begin();
  sensors.setWaitForConversion(false);  // 温度変換中もGPS受信を止めない
  sensors.requestTemperatures();
}

void loop() {
  readGPS();

  // 2秒ごとに測定値を出力する
  if (millis() - lastPrint >= 2000) {
    lastPrint = millis();

    // 水温
    float t = sensors.getTempCByIndex(0);
    if (t != DEVICE_DISCONNECTED_C) waterTemp = t;
    sensors.requestTemperatures();   // 次回分の変換を開始

    // TDS:水温で25℃基準に補正
    float v = readVoltage(PIN_TDS);
    float vComp = v / (1.0 + 0.02 * (waterTemp - 25.0));
    float tds = (133.42 * vComp * vComp * vComp
               - 255.86 * vComp * vComp
               + 857.39 * vComp) * 0.5;

    // 濁度:電圧(V)と濁り度(%)
    float turb = readVoltage(PIN_TURB);
    float turbPct = (TURB_CLEAR_V - turb) / (TURB_CLEAR_V - TURB_DARK_V) * 100.0;
    if (turbPct < 0)   turbPct = 0;
    if (turbPct > 100) turbPct = 100;

    // 出力(JSON形式:1行が1件の測定データ)
    Serial.print("{\"temp\":");      Serial.print(waterTemp, 2);
    Serial.print(",\"tds\":");       Serial.print(tds, 0);
    Serial.print(",\"turb\":");      Serial.print(turb, 2);
    Serial.print(",\"turb_pct\":");  Serial.print(turbPct, 1);
    Serial.print(",\"sats\":");      Serial.print(gps.satellites.value());
    if (gps.location.isValid()) {
      Serial.print(",\"lat\":");     Serial.print(gps.location.lat(), 6);
      Serial.print(",\"lng\":");     Serial.print(gps.location.lng(), 6);
    } else {
      Serial.print(",\"lat\":null,\"lng\":null");
    }
    Serial.println("}");
  }
}

2秒ごとに、1行で1件の測定データが出ます。手元の値を当てはめると、1行はこの形になります。

{"temp":24.50,"tds":111,"turb":3.45,"turb_pct":0.0,"sats":0,"lat":null,"lng":null}
キー中身
temp水温(℃、小数2桁)
tdsTDS(ppm、整数)
turb濁度センサの電圧(V、小数2桁)
turb_pct濁り度(%、小数1桁)
sats捕捉している衛星の数
lat緯度(小数6桁)。測位できていないときは null
lng経度(小数6桁)。測位できていないときは null

測位できていないときも、行は欠けません。 緯度と経度が null になるだけで、水温・TDS・濁度は毎回出ます。受け取る Raspberry Pi 側は、1行ずつ JSON として読めば済みます。

表示版にあった「受信文字数」は、JSON 版では出していません。配線の切り分けは表示版で済ませ、確認が終わってから JSON 版に書き換える、という順番で使います。

濁り度(%)の考え方

濁度センサの出力は電圧なので、そのままでは濁り具合が直感的に分かりません。そこで、きれいな水を 0%、完全な遮光を 100% として、電圧を割合に直しています。

float turbPct = (TURB_CLEAR_V - turb) / (TURB_CLEAR_V - TURB_DARK_V) * 100.0;

基準値をスケッチの先頭にまとめてあるのは、後から測り直すためです。設置場所が決まったら、その場所のきれいな水と、完全に遮った状態の2つを測り、TURB_CLEAR_VTURB_DARK_V を書き換えます。 外光の明るさで、基準値は少し変わります。

2点だけ、注意があります。

  • 濁り度(%)は、光の遮られ具合を示す独自の目安です。濁度の正式な単位(NTU)ではありません。 NTU が要る場合は、濁度の標準液で校正します
  • Seeed は、このセンサーの電圧から NTU への換算式を公開していません。ネット上にある換算式は、他社製センサー向けのものが多く、そのまま当てはめると値がずれます

USV に載せて水質の変化を追う用途であれば、NTU の絶対値より、同じ場所の値がどう動いたかのほうが役に立ちます。基準値からの下がり方で見るのが実用的です。

まとめ

  • Seeeduino Nano + Grove Shield for Arduino Nano に、DS18B20(D2)、Grove TDS水質センサ(A0)、Grove 濁度センサ(A2)、Grove GPS(D4)をつなぐと、水温・水質・濁り・位置を1台で同時に読めます
  • Seeeduino Nano をつないでも COM1 しか出ないときは、Silicon Labs の CP210x ドライバーを入れます。 私が詰まったのは、ここだけでした
  • バラ線の DS18B20 は、Grove ケーブルを切ってはんだ付けすればつながります。4.7kΩ の抵抗を、赤(VCC)と黄(DATA)の間に入れるのを忘れないでください
  • 濁度センサは加工が要りません。ただし基板の切替スイッチを「A」側にすること、プローブの上面を水に沈めないことの2つは外せません
  • GPS と DS18B20 を同時に使うときは、setWaitForConversion(false) で温度変換の待ち時間をなくします。TDS と濁度は30回平均で安定します
  • GPS が「測位中」のままでも、受信文字数が増えていれば配線は正しいです。屋外に出してください
  • 実際に使うときは、出力を JSON にしたスケッチに書き換えます。1行が1件の測定データで、測位できていないときは緯度と経度が null になります

電源まわりで Arduino が落ちる場合は、Arduinoの電源が「カチカチ落ちる」原因はこれだった も参考になります。

「センサをいくつか組み合わせて、現場で測れる装置にしたい」「試作はできたが、屋外で動かし続ける構成に自信がない」という方は、無料の30分オンライン診断で状況をお伺いします。AI技術顧問サービスでは、こうした構成の判断から現場で動くところまで一緒に進めています。

この記事の技術を、現場で実装したい方へ

AI×IoTの技術顧問として、月額契約で継続伴走しています。PoC設計・技術判断・組織設計・ベンダー管理・実装支援まで、現場で動くまで一緒に進めます。受託開発(請負)ではありません。

AI技術顧問サービスの詳細無料30分オンライン診断料金一覧

About The Author

Hideki
AI coordinator株式会社 代表取締役/東京海洋大学 産学官連携研究員。AIロボティクスのスタートアップで執行役員CTO 兼 VPoEとして収穫ロボットの開発組織を統括(2026年7月まで)。画像認識関連のAI特許3件取得・1件出願中。現在はAI・ロボティクス・IoTの現場実装を、月額の技術顧問契約で「現場で動くまで」伴走しています。
会社概要 → www.ai-coordinator.co.jp