DS18B20 防水温度探头接 Arduino:鱼缸水温 OLED 监测器

Cartoon of a silver stainless-steel DS18B20 waterproof probe hanging into a teal aquarium tank on its black cable with red, yellow and black bare tails, beside a dark-blue OLED module showing abstract white pixel blocks and a white breadboard holding a small blue resistor

一支防水 DS18B20 只靠 Arduino 的一支脚就能读出水温:红线接 5 V、黑线接 GND、黄线接 D2,再用一颗 4.7 kΩ 电阻,从那条黄线拉到 5 V。这颗电阻不是可有可无的讲究:在 1-Wire 总线上,它是唯一能把信号线拉高的东西,所以少了它,探头根本不会应答 —— 扫描总线时,一支都找不到。

用 Arduino 监测鱼缸水温,需要哪些零件?

DS18B20 防水探头是一颗直径大约 6 mm 的不锈钢金属胶囊,后面接着一条一米长的黑色线,末端是三根裸露的多股线 —— 红、黄、黑。那一米就是这个作品的边界:它够从水里拉到鱼缸旁边一处干燥的地方,再远就不行了。清单上其余的东西,只是给它一块主板、一个显示屏,还有那一颗电阻。

选配 —— 第二支探头,以及一条 USB 线(如果你还缺)

ItemPriceQty
Temperature Sensor DS18B20 Temperature Measurement Probe WaterProof 18B20Temperature Sensor DS18B20 Temperature Measurement Probe WaterProof 18B20DS18B20RM4.95
Data Cable Type-A Type-C MicroUSB Type-B 0.5m 1m 30cm 0.3m 100cm Data Transfer Upload Code - TYPE-A TO TYPE-B (1.0M)Data Cable Type-A Type-C MicroUSB Type-B 0.5m 1m 30cm 0.3m 100cm Data Transfer Upload Code - TYPE-A TO TYPE-B (1.0M)ATBC100RM4.90

第二支 DS18B20 和第一支共用同一支脚、同一颗上拉电阻 —— 想同时看进水口和出水口,或者一块板管两个缸,要的就是这个。USB A 转 B 线是 Uno 那个方形大插座用的线,抽屉里没有这种形状的话就加一条。

接头的形状决定这些零件到底接不接得上。0.96 吋白色 OLED 出厂就焊好了四针公排针,丝印是 GND、VCC、SCL、SDA —— 地在最前面,请对着板子读,别凭印象,因为它的邻居就是 5 V。这排针插进400 孔面包板,再由公对公杜邦线把每一行接到 Uno 的母座上。探头则完全没有接头:三根裸线直接插进面包板的孔里。

DS18B20 为什么非要一颗 4.7 kΩ 上拉电阻不可?

DS18B20 的数据脚是开漏结构:探头内部的 DQ 接着一颗晶体管,它只能把信号线往下拉到地,没有任何东西能把线往上推。Arduino 的引脚在说话的时候也是同一副样子。所以在 1-Wire 总线上,每一个成员都造得出低电平,却没有一个造得出高电平 —— 那颗拉到 5 V 的电阻,就是这条线唯一升得上去的机制。少了它,线就只是浮空:没有空闲高电平,没有复位脉冲,也就没有回应。

DS18B20 的数据手册把这件事变成了一个死线。要发出一个 1,主机先把线拉低,再放开,而线必须在时隙开始后 15 µs 之内回到有效的高电平。电阻和总线电容组成一条 RC 充电曲线,所以这条死线其实是一份电阻预算。一米探头线加上一行面包板,量级大约是 100 pF:4.7 kΩ × 100 pF 是 0.47 µs 的时间常数,线大约一微秒就恢复了 —— 比那道窗口早了十五倍。换成 10 kΩ 则翻倍到大约两微秒,这也正是为什么「一支探头、一米线」用 10 kΩ 确实跑得起来。可是一旦探头和线长加上去,电容就跟着爬升;到了 1.5 nF 附近,10 kΩ 那条曲线在 15 µs 死线到期时才走了三分之二,位就开始掉,而 4.7 kΩ 仍然留着余量。

所以采购上的答案是:整条总线只要一颗 4.7 kΩ、1/4 W 的电阻 —— 一颗,不是一支探头配一颗,因为几支探头是并在同一条线上的。它没有极性,正反随便插。如果你是从混装包里翻,4.7 kΩ 在蓝色五色环金属膜电阻上是黄-紫-黑-棕(470 × 10),在比较老的四色环碳膜电阻上是黄-紫-红。两颗 10 kΩ 并在同一对孔位上,等于 5 kΩ。

开漏 1-Wire 总线的卡通示意图:一条深蓝色横线,下方有三个往地拉的开关,对应两支银色探头和一块蓝色板子的边缘,另有一颗蓝色电阻把线往上拉到红色电源轨
1-Wire 总线上人人都能把线拉低,只有那颗电阻能把它拉回高电平 —— 所以少了它,是整条总线不通,而不是性能变差而已。

DS18B20 要怎么接到 Arduino Uno?

DS18B20 的三根线是细多股线,末端镀了锡而且散开,面包板的孔却是为硬针脚做的。每一根线在靠近板子之前,先用手指捻紧 —— 一根散出去的铜丝搭到隔壁那一行,就是把 5 V 送上了数据线。家里有烙铁的话,在捻好的线头上点一滴锡,是更牢靠的做法。

线 接到 为什么
探头红线 Uno 5V VDD —— 正常供电,芯片就不必去数据线上抢电流
探头黑线 Uno GND 总线的地参考
探头黄线 Uno D2 DQ,也就是 1-Wire 数据线 —— 只要在代码里改掉,除了 0 和 1 以外任何数字脚都行,那两支要留给串口监视器
4.7 kΩ 电阻 黄线那一行 → 5V 上拉电阻。不管接几支探头,整条总线只要一颗
OLED GND Uno GND 排针上的第一支 —— 插之前先看丝印
OLED VCC Uno 5V 显示屏供电,和探头共用同一条电源轨
OLED SCL Uno A5 I²C 时钟 —— 在 Uno 上是固定脚位,不能随便换
OLED SDA Uno A4 I²C 数据

先给面包板送电:一条杜邦线从 Uno 的 5V 母座接到面包板的红线轨,一条从 GND 接到蓝线轨。上面那张表里有三行都要 5 V,而 Uno 只有一个 5V 母座 —— 所以所有东西都留在那两条杜邦线所在的半块板上。接下来,一列五个孔就是整条总线:黄线、4.7 kΩ 电阻的一只脚,还有通往 D2 的那条杜邦线,全部落在这一列上,而电阻的另一只脚够得到 5 V 轨。

密封的只有那颗钢制胶囊和它的线,Uno、面包板和显示屏都不防水,所以请把它们放在水面以上的干燥架子上,只让线从缸沿垂下去。胶囊要吊在开阔的水域里,避开加热棒的出水口:贴着加热棒放,量到的是加热棒,不是这缸水。不锈钢在海水里也会起锈斑 —— 清洗海水缸的时候,顺手检查一下胶囊。这里所有东西都是 5 V 直流;鱼缸加热棒是市电电器,这块面包板上没有任何东西应该被接去控制它。

卡通对比图:左边一根黄色多股线末端散开,银色细丝碰到隔壁的面包板孔,标着橙色叉;右边同一根线捻成尖头,端正地插进一个孔里,标着青色勾
左:散开的铜丝伸进了隔壁那一行,等于 5 V 和数据线短路。右:同一根线捻紧之后,只坐在一个孔里。

DS18B20 的 Arduino 代码要用哪个库 —— OneWire 还是 DallasTemperature?

DS18B20 需要两个库,各做各的活。Paul Stoffregen 的 OneWire 用软件模拟出总线要求的微秒级时序;Miles Burton 的 DallasTemperature 架在它上面,把那些时序变成看得懂的指令。两个都从库管理器装,另外再装 Adafruit SSD1306 和 GFX 这一对 —— 我们那篇 DHT11 空气温湿度监测器用的也是它们。

/*
  DS18B20 防水探头 -> 0.96" OLED 水温监测器
  Arduino Uno + OneWire + DallasTemperature + Adafruit SSD1306

  黄色 DATA 线和 5V 之间要接一颗 4.7k 电阻。不管这支脚上挂了
  几支探头,整条总线都只要这一颗。
*/
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const uint8_t ONE_WIRE_PIN = 2;   // 探头的黄线接在这里
const uint8_t MAX_PROBES   = 2;   // 要加更多探头就把这个数字调大

Adafruit_SSD1306 oled(128, 64, &Wire, -1);
OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature sensors(&oneWire);

DeviceAddress probe[MAX_PROBES];
uint8_t found = 0;

void printRom(const uint8_t *addr) {
  for (uint8_t i = 0; i < 8; i++) {
    if (addr[i] < 0x10) Serial.print('0');
    Serial.print(addr[i], HEX);
  }
}

void scanBus() {
  sensors.begin();                 // 从头重跑一次 1-Wire 搜索
  uint8_t onBus = sensors.getDeviceCount();
  found = (onBus < MAX_PROBES) ? onBus : MAX_PROBES;

  Serial.print(F("Probes on the bus: "));
  Serial.println(onBus);

  for (uint8_t i = 0; i < found; i++) {
    if (sensors.getAddress(probe[i], i)) {
      Serial.print(F("  index "));
      Serial.print(i);
      Serial.print(F("  ROM "));
      printRom(probe[i]);          // 把它抄下来,在那条线上贴个标签
      Serial.println();
      sensors.setResolution(probe[i], 12);   // 0.0625 C 一档,转换 750 ms
    }
  }
}

void setup() {
  Serial.begin(9600);

  // 多数 0.96" 模块的地址是 0x3C,少数被设成 0x3D。
  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("OLED not answering - check SDA/SCL and 5V"));
    for (;;) { }
  }
  oled.setTextColor(SSD1306_WHITE);

  scanBus();

  // 这里印出 "YES",表示有探头把 VDD 接到了 GND,正在从数据线
  // 上偷电 —— 读数卡在 85.00 通常就是这个原因。
  Serial.print(F("Parasite power: "));
  Serial.println(sensors.isParasitePowerMode() ? F("YES") : F("no"));
}

void loop() {
  if (found == 0) {                // 少了上拉电阻,长的是这个样子,不是 -127
    oled.clearDisplay();
    oled.setTextSize(1);
    oled.setCursor(0, 0);
    oled.print(F("NO PROBE FOUND"));
    oled.setCursor(0, 16);
    oled.print(F("CHECK 4.7k -> 5V"));
    oled.setCursor(0, 28);
    oled.print(F("AND YELLOW ON D2"));
    oled.display();
    scanBus();                     // 一直找下去,所以线接好了就会自己恢复
    delay(1000);
    return;
  }

  sensors.requestTemperatures();   // 一直等到探头说它转换完了(最长 750 ms)

  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  oled.print(F("WATER TEMP"));

  for (uint8_t i = 0; i < found; i++) {
    float c = sensors.getTempC(probe[i]);

    oled.setTextSize(2);
    oled.setCursor(0, 18 + i * 24);

    if (c <= DEVICE_DISCONNECTED_C) {        // -127:没有人应答
      oled.print(F("--.- C"));
      Serial.println(F("-127 : no reply - check the 4.7k and the wiring"));
    } else {
      oled.print(c, 1);
      oled.print(F(" C"));
      Serial.print(i);
      Serial.print(F(" : "));
      Serial.println(c, 2);
      if (c > 84.9 && c < 85.1) {             // 上电后暂存器的默认值
        Serial.println(F("   85.00 : read before conversion, or starved supply"));
      }
    }
  }

  oled.display();
  delay(1000);
}

这份代码在 Uno 上编译出来占 19,620 字节的 Flash —— 32 KB 的 60% —— 静态 RAM 用掉 595 字节;接着显示库在 begin() 执行时还会再要一块 1,024 字节的帧缓冲,所以在你自己的变量出场之前,Uno 那 2 KB 里已经有大约 1.6 KB 名花有主。这也是为什么这里每一条固定字符串都包在 F() 里:不包的话,它们会被复制到剩下的那点空间里,而不是留在 Flash。requestTemperatures() 会一直阻塞到探头回报转换完成 —— DallasTemperature 库是去轮询总线,而不是固定睡一段时间,所以这段等待是被数据手册里 12 位下 750 ms 的上限「封顶」,而不是每次都等满;改成 9 位可以把上限压到 93.75 ms,代价是步进变粗到 0.5 °C。

如果你已经在跑我们那台 TDS 水质检测仪,这个读数还有第二份工作。电导率大约每上升 1 °C 就变化 2%,而那个作品是按 25 °C 假设的,所以水温偏低时它读出来会偏小。给它一支独立脚上的 DS18B20,就能把一个真实数字交给那份代码 —— 同一块 Uno、多一支探头,假设就变成了测量。

几支 DS18B20 要怎么共用 Arduino 的同一支脚?

每一支 DS18B20 出厂时都被激光刻上了一组 64 位 ROM 码:8 位家族码(这颗零件是 28h)、48 位属于这一颗晶片的序列号,再加上覆盖前七个字节的 8 位 CRC。没有两支探头的号码一样,所以主机可以在同一条线上一次点名一支。它是靠一位一位地走遍总线来找到它们的 —— 遇到两支探头答案不同的位,就先选一条分支、把那个地址走完,再回头处理另一条 —— 上面那份代码里的 getAddress() 读出来的,就是这些地址。

实际做法是:两根黄线插进面包板上同一列,两根红线接红轨,两根黑线接蓝轨,那颗 4.7 kΩ 电阻原地不动 —— 再加第二颗只会把上拉阻值减半,一点好处也没有。把串口监视器调到 9600 波特再跑这份代码,每一组 ROM 码都会在启动时打印一次。想知道哪支是哪支,就用手捧着其中一支泡进温水,看哪个索引在动,然后在那条线上缠一小片胶带做记号。

几支探头怎么落在同一条电源轨上,看一遍比讲一遍清楚:

DS18B20 出现 −127 和 85.00,是什么意思?

DS18B20 有两个读数其实是消息,不是温度 —— 另外还有一个消息,压根就不会变成读数。

读数 它真正的意思 该往哪里查
Probes on the bus: 0 搜索跑完了,没有任何东西应答 —— 从来没有设备把线拉低来报到 先查上拉电阻:没有它,线就无法空闲在高电平,复位脉冲自然得不到回应。接着查黄线那一列,再查代码里声明的引脚号
−127.00 DEVICE_DISCONNECTED_C —— 主机发了复位脉冲,没有设备应答 某支启动时找得到、后来不见了的探头:线头松了、某根铜丝时接时断,或者线被缸盖压住了
85.00 温度寄存器上电后的默认值,还没被写过 在转换完成之前就去读了,或者供电在转换途中掉压
数字合理但不动 一个真实数值,只是永远不更新 requestTemperatures() 写在了循环外面,所以读的永远是同一份暂存器

那个 +85 °C 是温度寄存器本身的上电复位值,所以一颗已经上电、但转换还没做完的芯片,会把它交给任何一次读取。它同时也指向供电不足,而这就牵出寄生供电:DS18B20 可以把 VDD 接到地,在信号线空闲为高时从线上取电。我们这支探头有三根线,所以把红线和 5 V 给 VDD:这就是正常供电,也是这里用的模式。寄生模式反过来要求主机在整个转换期间把线死死钳在高电平,因为 4.7 kΩ 的电阻供不出芯片转换时需要的约 1.5 mA。代码里如果看到这种状态就会打印 Parasite power: YES,意思是那根红线没做到它该做的事。

我们从真实客户那里常看到的错误

买了探头,忘了电阻。有位客户来问我们:“For the resistor: do you have a 4.7k or 10k 1/4W resistor available? I need about 10 pcs for my DS18B20 pull-up.” 阻值他记对了,数量则买得很大方:一条总线只要一颗。探头出货就是探头本身 —— 没有转接板,也没有电阻。

以为入门套件里已经有了。另一位客户问一套零件里包不包含 “220 Ohm, 1K Ohm, 4.7K Ohm and 10K Ohm”。手上已经有的,请照上面那组色环去读,别靠猜;如果是要买而不是要翻,就直接买这个阻值 —— 一颗 4.7 kΩ 1/4 W,整条总线就搞定了。

以为显示屏会自己亮。探头明明在跑、OLED 却一片空白,几乎都是 I²C 地址的问题:多数 0.96 吋模块的地址是 0x3C,少数是 0x3D,而 File → Examples → Wire → i2c_scanner 会把 A4/A5 上真正存在的地址印出来,于是你是知道答案之后再去改 oled.begin() 里那一个数字,而不是靠试。这个习惯,我们那篇 LCD1602 I²C 教程也一样在教。

常见问题

DS18B20 真的一定要接电阻吗?

要 —— 数据线和 5 V 之间接一颗 4.7 kΩ 1/4 W 电阻,整条总线一颗就够。探头和 Arduino 都只能把线拉低,所以这颗电阻是唯一能把线抬起来的元件;没有它,总线永远空闲不到高电平,扫描时一支探头也找不到。

我的 DS18B20 为什么读到 −127?

−127 是库里的 DEVICE_DISCONNECTED_C 哨兵值:复位脉冲没有得到回应。如果探头从头到尾就没被找到,代码在启动时会报设备数为零,那问题是上拉电阻没接、电阻插错了列,或者引脚选错。真正的 −127 表示它启动时应答过、后来才停 —— 通常是线头松了,或者某根铜丝只是勉强碰着。

可以用 10 kΩ 电阻代替 4.7 kΩ 吗?

如果只有一支探头、用的是标配那一米线,可以:面对 15 µs 的死线,线大约两微秒就回到有效高电平。但探头和线长一多,电容上去,这点余量就缩水,所以该守住的值还是 4.7 kΩ。两颗 10 kΩ 并联等于 5 kΩ。

一支 Arduino 引脚可以接几支 DS18B20?

够任何一套鱼缸用了。每一支都带着独一无二的 64 位 ROM 地址,所以它们共用一支脚、共用一颗上拉电阻。真正的上限是电学上的,而不是库里写死的数字:线材总电容累积到一定程度,上升沿就会被磨钝。

DS18B20 用来量鱼缸,精度够吗?

绰绰有余。它的标称精度是 −10 °C 到 +85 °C 之间 ±0.5 °C,12 位下的分辨率是 0.0625 °C 一档,所以加热棒出问题,会在鱼察觉之前很久就先显示出来。

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

Leave a Reply

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