用 JSN-SR04T 做水塔水位监控器:Arduino 完整教程

Tangki air keratan rentas dengan probe JSN-SR04T dibenam pada penutup, dan kotak kering berisi Arduino Uno, papan pemacu biru, buzzer dan OLED di luar tangki

想知道屋顶水塔几时快见底?把 JSN-SR04T 的 probe 装在水塔盖上、朝下对着水面——它测出到水面的距离,Arduino Uno 把距离换算成百分比,显示在小小的 OLED 上,水位跌破 20% 就响 buzzer 提醒你。只有黑色 probe 防水,蓝色驱动板必须放在水塔外保持干燥。

为什么要用 JSN-SR04T,不用普通的 HC-SR04?

JSN-SR04T 传感器分成两部分:一个完全密封的黑色圆柱形 probe(IP67 防护等级),接在一条带白色 2-pin 插头的长线上;另一块是独立的蓝色驱动板,带 4 pin header。普通 HC-SR04 有两颗外露的「眼睛」,在水塔里潮湿的空气中很快就坏——做水位监测这种活,全密封的 probe 才是正解。

Manorshi 原厂 datasheet,这个模块在 DC 3.0–5.5V 供电下测量 21 cm 到 600 cm,trigger 和 echo 的用法与 HC-SR04 相同——超声波测距的基本原理,我们在 HC-SR04 教程里已经讲过。

长期安装用

ItemPriceQty
Power Supply Adapter DC Universal AC to DC Converter PSU 5V2A 5V3A 9V2A 12V2A - P.S. ADAPTOR (12V2A)Power Supply Adapter DC Universal AC to DC Converter PSU 5V2A 5V3A 9V2A 12V2A - P.S. ADAPTOR (12V2A)PSA1202RM15.95

12V 适配器直接插 Uno 的 DC 插座——不用接电脑,监控器 24 小时运行。

probe 装在水塔哪个位置最合适?

JSN-SR04T 有一个重要限制:盲区。太近的东西它测不到——Manorshi 3.0 版 datasheet 写最小 21 cm,旧版 datasheet 写 25 cm。这个数字随板子版本而不同,所以按更安全的那个来设计:把盲区当作 25 cm。

实际安装时,probe 至少要装在水位上方 30 cm——水塔满的时候,水面仍在盲区之外,这个 offset 之后会算进代码的公式里。probe 的波束是约 75 度的宽锥形,所以要装在盖子正中,远离塔壁和进水口,免得壁面回波和进水的波纹干扰读数。

别让 probe 吊在线上——它会晃、会歪。在盖子中央按 probe 头的尺寸钻一个孔,把 probe 嵌进去,让它的面正对垂直向下,四周用 silicone sealant 或 cable gland 封好。驱动板、Uno 和 buzzer 放在水塔外阴凉干燥的盒子里;只有 probe 头穿过盖子。上屋顶作业有规矩:别一个人爬,线缆沿现有水管或结构扎牢,市电电线远离水塔。长期安装的话,用 12V 适配器从 Uno 的 DC 插座供电——插座留在屋里,只有低压 DC 线上到电子盒。

水塔剖面图:probe 装在盖子中央,probe 面下方标出盲区带,再往下才是满水位
盲区就在 probe 正下方——probe 至少装在满水位上方 30 cm,并把这个 offset 算进公式

JSN-SR04T、OLED 和 buzzer 怎么接到 Uno?

JSN-SR04T 的驱动板用四个 pin——Vcc、GND、Trig、Echo——probe 线上的白色 2-pin 插头直接插到驱动板上的插座。0.91 寸 128×32 OLED 则走 I2C:在 Arduino Uno 上,I2C 引脚是 A4(SDA)和 A5(SCL)。留意这块 OLED 排针的丝印顺序:GND、VCC、SCK、SDA——这里的 SCK 就是 I2C 时钟线 SCL,只是印字不同。

元件 / 引脚 接到
JSN-SR04T Vcc Uno 5V
JSN-SR04T GND Uno GND
JSN-SR04T Trig Uno D9
JSN-SR04T Echo Uno D8
probe 线(白色 2-pin 插头) 驱动板上的插座
OLED GND Uno GND
OLED VCC Uno 5V
OLED SCK(即 SCL 线) Uno A5
OLED SDA Uno A4
Buzzer 红线(+) Uno D7
Buzzer 黑线(−) Uno GND

SFM-20B buzzer 是有源 piezo 型——规格标明最大 10 mA(在额定 12V 下,5V 时电流更低)——远低于 Uno 每个引脚 20 mA 的上限,所以可以直接用数字 pin 驱动,不需要三极管(不像电磁式 buzzer 那么吃电流)。它也够响,大约 95 dB——所以之后的代码响的是间歇短哔声,不是长鸣。

JSN-SR04T 驱动板、I2C OLED 和 piezo buzzer 接到 Arduino Uno 的接线图
完整接线:四条线到驱动板,四条到 OLED,两条到 buzzer——probe 插头插在驱动板上

开工之前想看看这颗传感器配 OLED 的实际安装过程,这条短片一步一步示范:

距离怎么换算成水位百分比?

Arduino Uno 只知道一件事:probe 面到水面的距离。要把距离换成水位,需要从你自己的水塔量两个固定数字——满水距离(probe 到满水位,例如 30 cm)和空塔距离(probe 到塔底,例如 150 cm)。水深 = 空塔距离 − 读数;百分比 = 水深 ÷(空塔距离 − 满水距离)× 100。

举个例子:上面那个水塔,传感器读到 90 cm。水深 = 150 − 90 = 60 cm,百分比 = 60 ÷ (150 − 30) × 100 = 50%。安装时用卷尺量好这两个固定数字——整个系统的准确度全靠它们。

为什么读数在水塔里跳来跳去?怎么稳定它?

JSN-SR04T 在真实水塔里的读数很少安安静静。进水管一冲水面就晃,细小的波纹会把回波打散,75 度的波束锥可能扫到塔壁,水汽还会在 probe 面上结露——每一样都可能触发乱跳的读数。

最有效的解法:连取几个读数,用中位数(median),不是平均值。七个样本里混进一个 400 cm 的野值,平均值会被拉得老远;中位数只取排序后的中间值——野值直接被丢掉。对这种爱突跳的超声波数据,median 几乎稳赢。

温度也会影响声速:0°C 时约 331.5 m/s,每升高 1°C 增加约 0.6 m/s。代码按 343 m/s(约 20°C)计算;大热天水塔里的空气可以去到 40°C,读数会偏差接近 4%——每米距离大约 3 到 4 cm。对百分比显示和警报来说可以忽略;想要厘米级精度,就挑中午时分在现场校准空塔距离。

两个水塔对比:probe 装在盖子中央读数稳定,probe 靠塔壁、正对进水口则收到斜射的干扰回波
probe 的位置决定读数稳不稳:装在盖子中央、远离塔壁和进水口(左),不要贴边、正对进水水流(右)

这个监控器的完整代码是什么?

这个 sketch 需要在 Arduino IDE 的 Library Manager 装两个 library:Adafruit GFX LibraryAdafruit SSD1306Wire 是内置的)。这块 0.91 寸模块的 I2C 地址是 0x3C。按你的水塔改好 JARAK_PENUH(满水距离)和 JARAK_KOSONG(空塔距离),sketch 就能直接用;屏幕上的提示文字维持拉丁字母字符串——SSD1306 的默认字体显示不了中文。

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 oled(128, 32, &Wire, -1);

const int PIN_TRIG   = 9;
const int PIN_ECHO   = 8;
const int PIN_BUZZER = 7;

// 你的水塔尺寸——按实际改这两个值
const float JARAK_PENUH  = 30.0;   // cm:probe 到满水位(保持在 25 cm 盲区之外)
const float JARAK_KOSONG = 150.0;  // cm:probe 到塔底

const int AMBANG_ALARM = 20;   // 低于 20% buzzer 开始响
const int AMBANG_RESET = 30;   // 回升超过 30% 才重新安静(迟滞)
const int BIL_SAMPEL   = 7;    // 取中位数的采样次数

bool alarmAktif = false;

float bacaJarakSekali() {
  digitalWrite(PIN_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(PIN_TRIG, HIGH);
  delayMicroseconds(10);                    // 按 datasheet 给 10us 触发脉冲
  digitalWrite(PIN_TRIG, LOW);
  unsigned long tempoh = pulseIn(PIN_ECHO, HIGH, 40000UL);
  if (tempoh == 0) return -1.0;             // 没收到回波
  return tempoh * 0.0343 / 2.0;             // 343 m/s(约 20°C)
}

float bacaJarakMedian() {
  float sampel[BIL_SAMPEL];
  int n = 0;
  for (int i = 0; i < BIL_SAMPEL; i++) {
    float j = bacaJarakSekali();
    if (j > 0) {
      int k = n;                            // 升序插入排序
      while (k > 0 && sampel[k - 1] > j) {
        sampel[k] = sampel[k - 1];
        k--;
      }
      sampel[k] = j;
      n++;
    }
    delay(60);                              // 等旧回波先消散
  }
  if (n == 0) return -1.0;
  return sampel[n / 2];                     // 中间值 = 中位数
}

void setup() {
  pinMode(PIN_TRIG, OUTPUT);
  pinMode(PIN_ECHO, INPUT);
  pinMode(PIN_BUZZER, OUTPUT);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);   // 0.91 寸模块的 I2C 地址
  oled.clearDisplay();
  oled.display();
}

void loop() {
  float jarak = bacaJarakMedian();
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);

  if (jarak < 0) {
    oled.setTextSize(1);
    oled.setCursor(0, 12);
    oled.print(F("Tiada bacaan sensor"));   // 无读数——检查 probe 插头和接线
    oled.display();
    return;
  }

  // 把距离换算成水深和百分比
  float julat = JARAK_KOSONG - JARAK_PENUH;
  float dalamAir = JARAK_KOSONG - jarak;
  dalamAir = constrain(dalamAir, 0.0, julat);
  int peratus = round(dalamAir / julat * 100.0);

  oled.setTextSize(2);                      // 左边大字显示百分比
  oled.setCursor(0, 9);
  oled.print(peratus);
  oled.print(F("%"));
  oled.setTextSize(1);                      // 右边小字显示水深
  oled.setCursor(72, 6);
  oled.print(F("Dalam:"));
  oled.setCursor(72, 18);
  oled.print(dalamAir, 0);
  oled.print(F(" cm"));
  oled.display();

  // 迟滞:低于 20% 响起,只有回升超过 30% 才停
  if (!alarmAktif && peratus < AMBANG_ALARM) alarmAktif = true;
  if (alarmAktif && peratus > AMBANG_RESET) alarmAktif = false;

  if (alarmAktif) {                         // 短哔声,不是持续长鸣
    digitalWrite(PIN_BUZZER, HIGH);
    delay(150);
    digitalWrite(PIN_BUZZER, LOW);
  }

  delay(1000);
}

留意那两个警报门槛:水位跌破 20% buzzer 开始响,但要等水位回升超过 30% 才会停。没有这种迟滞处理,水面在 20% 附近晃来晃去,buzzer 就会响响停停闹个没完。下一步很多人都会做:加 relay 和水泵,让水塔自己补水——概念和我们的 Arduino 自动浇花系统一样。

我们常看到的错误

读数一直是 0 或卡在同一个数字。多半是 probe 的白色插头没插紧在驱动板上,或者 Trig 和 Echo 接反了。把 probe 插头重新插稳,对照上表检查 Trig 接 D9、Echo 接 D8。

读数疯狂乱跳。通常是 probe 离塔壁太近,或正对着进水口的水流——移到盖子正中。如果水装太满、水面进了 25 cm 盲区,读数也会变乱码:把 probe 的位置抬高。

在桌上好好的,进了水塔就乱讲。在桌上,probe 对着又平又静的地板——理想条件。在水塔里,要确保 probe 正对垂直向下;歪几度回波就打到塔壁,不再回到 probe——所以 probe 必须稳稳嵌在盖子钻的孔里,不能吊在线上晃。

装在外面几个星期后系统就死了。几乎都是驱动板或 Uno 淋了雨、沾了露水。再说一次:只有 probe 防水——所有板子都要装在密封盒里,走线孔也要封好。

关于水塔水位传感器的常见问题

多高的水塔可以用?

JSN-SR04T 最远测到 600 cm,扣掉 30 cm 的安装 offset,5 米深的水塔也还在量程内。马来西亚住家常见的 poly 水塔多是 1 到 2 米——对这颗传感器绰绰有余。

水面晃动的话,读数会不会乱?

一般的晃动已经被代码里 7 次读数取中位数吸收掉了。如果你的水塔长期晃得厉害,把 BIL_SAMPEL 提高到 11,或在 probe 下方垂一根竖直的 PVC 管当「稳流井」(stilling well)——管里的水面比开放水面平静得多。

probe 顶得住屋顶的太阳吗?

probe 是 IP67 等级,datasheet 标明工作温度 -20°C 到 +70°C,马来西亚屋顶的热度还在范围内。真正要保护的是电子部分:驱动板和 Uno 要装在阴凉处的密封盒里,不要直接暴晒。

可以把警报发到手机吗?

Arduino Uno 没有 WiFi,所以只有 OLED 和 buzzer。升级到 ESP32,几乎一样的代码就能把通知推送到手机——看我们的 ESP32、ESP8266 和 Uno 对比挑一块合适的板。

地下水箱适合吗?

适合——原理一样:probe 装在顶盖、朝下测。只是地下水箱的空气更潮湿,probe 面更容易结露;每次保养时擦一擦 probe 面,驱动板则要留在地面以上的干燥盒子里。

最后更新:2026年8月。有问题?WhatsApp 联系我们

Leave a Reply

Your email address will not be published. Required fields are marked *