把数据记录扩展板叠在 Uno 上,调用 SD.begin(10),再把逗号分隔的一行行文字写进文件 —— 但这些字节要等到库里那块 512 字节的缓冲填满、或者你自己调用 flush(),才真的落到卡上。扩展板带来的是一个全尺寸 SD 卡座,外加一颗负责盖时间戳的 DS1307 时钟。卡和 CR1220 电池得你自己准备。
只用低压直流:USB,或者 Uno 的圆孔电源座。板子通电时绝对不要插拔存储卡;探头切口那一端要保持干燥 —— 防水的只有那截不锈钢管。
数据记录扩展板上有什么,又没有什么?
数据记录扩展板出货就是焊好的整块板,从袋子里拿出来直接压到 Uno 上就行。深蓝色板子的左半边,被一个带弹簧退卡的全尺寸 SD 卡座占满;卡座下方那个半透明枣红色的电池座,是给 DS1307 实时时钟做后备的。右边三分之一是一片洞洞板,留着以后把接线焊成永久的。Uno 的每一支引脚,在扩展板自己的母排座上都重新引了出来,所以没有一支被埋掉。卡座旁边那颗 14 脚芯片是一颗四路缓冲器,干的是电平转换:SD 卡是严格的 3.3 V 器件,而 Uno 跑的是 5 V 逻辑,这一层翻译,光有一个卡座的话就得你自己来做。
这个作品需要的两样东西不在袋子里,而且这两样我们都没有卖:
- 一张全尺寸 SD 卡,不是 microSD。Arduino 自带的 SD 库自称能存取“FAT16 and FAT32 files on SD and SDHC cards”(
SdFat.h),而 SDHC 按规范最大只到 32 GB,所以一张 64 GB 的 exFAT 卡挂不上。microSD 插在全尺寸转接卡里也没问题,只是转接卡又多加了一组弹簧触点 —— 怪扩展板之前,先换一张转接卡试试。 - 一颗 CR1220 纽扣电池。电池座出货是空的,从它中间能看到裸露的蓝色板面。这块扩展板沿用 Adafruit 的参考设计,吃的是同一款 12 mm 电池。没有它照样能记录 —— 01/01/2000 那一节写清楚了你会失去什么。
洞洞板上方那几个标着 CS、L1、L2 的孔,还有复位按钮旁边那两颗小 LED,都是没有接线的焊盘,不是工作指示灯。这块 V1.0 板子的实机走一遍:
材料清单 —— 板子、扩展板、探头,以及把它们接起来的线
Parts list
| Item | Price | Qty | |
|---|---|---|---|
XL830L Digital Multimeter with backlight Portable Multimeter Electronics ProjectMULTIME | RM25.95 |
在这个作品上,它专门有两件事可做:用欧姆挡确认你从一包二十种阻值里真正抽出来的是哪一颗;还有,当卡怎么都挂不上时,量一量扩展板的 5V 焊盘 —— 一张耗电的卡把 USB 电源轨拖垮的样子,跟代码出错一模一样。
扩展板已经占掉了 Arduino 的哪几支引脚?
数据记录扩展板占走了 Uno 的六支引脚,分在两条总线上:SPI 通到卡,I2C 通到时钟。所以探头落在 D2 上。
| 引脚 | 被谁占用 | 你还能用吗? |
|---|---|---|
| D10 | SD 片选(同时也是 ATmega328P 的硬件 SS 脚) | 不能 |
| D11、D12、D13 | 通到卡的 SPI 数据与时钟 | 不能 |
| A4、A5 | 通到 DS1307 时钟的 I2C | 不能 |
| D0、D1 | 与 USB 串口共用 | 避开 |
| D2–D9、A0–A3 | 没人占 | 可以 |
在 ATmega328P 上,D10 既是这块扩展板的片选脚,也是硬件 SS 脚,而 SPI 外设会一直盯着 SS —— 把它留成输入、再让什么东西把它拉低,硬件就会切进从机模式,主机也就不说话了。SD.begin() 已经替你处理好了:它会调用 SPI.begin(),而 AVR 内核的 SPI.cpp 就在那里执行 pinMode(SS, OUTPUT),旁边还写着一句警告:“if the SS pin ever becomes a LOW INPUT then SPI automatically switches to Slave, so the data direction of the SS pin MUST be kept as OUTPUT”。之后千万别再把 D10 拿回来做别的用途。
探头为什么需要一块面包板和一颗电阻?
防水版 DS18B20 引出来的是三根裸露的镀锡飞线 —— 红色是电源、黑色是地、黄色是数据 —— 线头上没有任何接插件;而扩展板对用户开放的接口,只有一排排母座。裸露的镀锡线在杜邦母头里是夹不住的。一块 170 孔迷你面包板就把这件事解决了:每五个孔一列,底下是连通的,所以镀锡线头和电阻的引脚直接插进去就好,再由公对公杜邦线把这几条网络送上扩展板。这种迷你规格没有电源轨,所以 5V 和地也住在普通的列里 —— 而既然一列就是一条网络,三根线就得分占三列:红线和黑线共用一列,等于把 5V 直接接到地。
那颗电阻不是可选项,原因藏在输出级里。DS18B20 的 DQ 脚是开漏的:它内部有一颗能把线拉到地的晶体管,却没有任何东西能把线抬上去。一松手,这条线就漂在附近的杂散电荷上。往 5V 接的那颗外部电阻,才是高电平的来源 —— 这也正是为什么没接电阻的探头会回传 -127.00(库里“没人应答”的那个值),或者卡在 85.00 不动(传感器上电后、还没完成任何一次转换时的默认值)。
DS18B20 数据手册要的是大约 5 kΩ,而在这颗探头一米长的引线上只挂一颗传感器时,4.7 kΩ 到 10 kΩ 都合用:电阻和总线电容凑成一对 RC,所以电阻越大,线上的电平爬升得越慢,只有长总线或者串好几颗传感器时余量才会不够。我们的 400 颗装电阻包把每个阻值用油墨印在编带的纸带上,不用去认色环 —— 这里用 6.8 kΩ 那一条正合适。

| DS18B20 引线 | 接到 | 扩展板上的位置 |
|---|---|---|
| 红(VDD) | 5V | 下排电源排座 |
| 黑(GND) | GND | 下排电源排座 |
| 黄(DQ) | D2 | 上排数字排座 |
| 4.7 kΩ–10 kΩ 电阻 | 在面包板上跨接红线那一列和黄线那一列 —— 也就是把 DQ 上拉到 5V | |
哪一段 sketch 把读数写进卡里?
先从库管理器装上 SD、RTClib(Adafruit)、OneWire 和 DallasTemperature(Miles Burton),并把串口监视器开着、波特率设 9600 —— 初始化只要有一步失败,sketch 就会停住,并把原因打在那里。这段 sketch 每五秒采样一次,用 DS1307 盖时间戳,每读一次就往文件尾追加一行 CSV —— 记录 HX711 称重传感器或 MLX90614 用的也是同一套写法。要多记东西,就往同一个文件加一列,别另开新文件 —— 一个宽的 CSV 一次就能画出图来。
// Arduino Uno + 数据记录扩展板 V1.0 + 防水 DS18B20。
// 每 5 秒往全尺寸 SD 卡上的 TEMPLOG.CSV 写一行 CSV,
// 时间戳由扩展板上的 DS1307 实时时钟提供。
//
// 扩展板占用:D10(SD 片选)、D11/D12/D13(SPI)、A4/A5(时钟的 I2C)。
// 所以传感器落在 D2,并从 D2 往 5V 接一颗 4.7k-10k 的上拉电阻。
// 只用低压直流:USB 或 Uno 的圆孔电源座。板子通电时
// 绝对不要插拔存储卡。
#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include <RTClib.h>
#include <OneWire.h>
#include <DallasTemperature.h>
const uint8_t SD_CS = 10; // 这块扩展板的 SD 片选脚
const uint8_t ONE_WIRE_PIN = 2; // DS18B20 的黄线(DQ)
const uint16_t SAMPLE_MS = 5000; // 每 5 秒读一次
const uint8_t FLUSH_EVERY = 12; // 每分钟往卡上落一次盘
RTC_DS1307 rtc;
OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature sensors(&oneWire);
File logFile;
uint8_t samplesSinceFlush = 0;
uint32_t nextSampleAt = 0;
// 与其记录一堆垃圾数据,不如大声停住。
void stopHere(const __FlashStringHelper *why) {
Serial.println(why);
while (1) { }
}
void setup() {
Serial.begin(9600);
// D10 既是 SD 片选脚,也是 ATmega328P 的硬件 SS 脚。SD.begin() 会调用
// SPI.begin(),而后者把 SS 设成输出("the data direction of the SS pin
// MUST be kept as OUTPUT"),所以这之后千万别再把 D10 拿回来当输入用 —
// SPI 会掉进从机模式,卡也就没声了。
pinMode(SD_CS, OUTPUT);
if (!SD.begin(SD_CS)) {
stopHere(F("No card. Check it is FAT16/FAT32, <=32GB, fully pushed in."));
}
Wire.begin();
if (!rtc.begin()) {
stopHere(F("No DS1307 on A4/A5. Is the shield seated on all its pins?"));
}
// isrunning() 读的是 DS1307 的 CH 位。数据手册说 CH 在首次上电时是被置位的,
// 所以电池座空着的话,振荡器每次重启都是停摆状态,
// 日期也就每次都退回 01/01/2000。
if (!rtc.isrunning()) {
Serial.println(F("RTC was halted - setting it from this sketch."));
Serial.println(F("Fit a CR1220 if you want the time to survive a power cut."));
rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // 编译时间,不是此刻
}
sensors.begin();
sensors.setResolution(12); // 0.0625 C 的步进,每次转换最长 750 ms
if (sensors.getDeviceCount() == 0) {
stopHere(F("No DS18B20 answered. Check the pull-up between D2 and 5V."));
}
logFile = SD.open("TEMPLOG.CSV", FILE_WRITE); // 只认 8.3 格式的文件名
if (!logFile) {
stopHere(F("Could not open TEMPLOG.CSV for writing."));
}
if (logFile.size() == 0) { // 全新的文件,先写一行表头
logFile.println(F("timestamp,celsius"));
logFile.flush();
}
Serial.println(F("Logging to TEMPLOG.CSV"));
}
void loop() {
if ((int32_t)(millis() - nextSampleAt) < 0) { // 能扛住翻转的比较写法
return;
}
nextSampleAt += SAMPLE_MS;
sensors.requestTemperatures(); // 12 位下会阻塞约 750 ms
float celsius = sensors.getTempCByIndex(0);
if (celsius == DEVICE_DISCONNECTED_C) { // -127.00:没人应答
Serial.println(F("Sensor did not reply - skipping this sample."));
return;
}
DateTime now = rtc.now();
char stamp[20];
snprintf(stamp, sizeof(stamp), "%04u-%02u-%02u %02u:%02u:%02u",
now.year(), now.month(), now.day(),
now.hour(), now.minute(), now.second());
logFile.print(stamp);
logFile.print(',');
logFile.println(celsius, 2);
Serial.print(stamp);
Serial.print(F(","));
Serial.println(celsius, 2);
// 这些字节还留在 SD 库那块 512 字节的 RAM 缓冲里。flush() 会把这一块
// 连同目录项一起推到卡上;每采一次样就做一次既费时间又磨闪存,
// 所以我们改成每分钟做一次。
if (++samplesSinceFlush >= FLUSH_EVERY) {
logFile.flush();
samplesSinceFlush = 0;
}
}
三个细节。TEMPLOG.CSV 是一个 8.3 格式的文件名 —— 最多八个字符、一个点、再最多三个 —— 因为这个库只认这种名字,名字长了会安静地打不开。setResolution(12) 换来的是 0.0625 °C 的步进,代价是一次最长 750 ms 的转换,而 requestTemperatures() 会一直等它等完 —— 所以每采一次样,主循环会停住将近一秒。还有,判断时间到了没有,用的是 (int32_t)(millis() - nextSampleAt) < 0 而不是 millis() < nextSampleAt,这样 millis() 在 49.7 天时翻转它也照样撑得过去 —— 而那正是一台记录仪注定要跨过的那道坎。
断电之后,我的 CSV 为什么是空的?
因为“写进文件”和“写进卡”是两件不同的事。SD 库在 RAM 里留着一块 512 字节的缓冲 —— SdFat.h 里那句 static cache_t cacheBuffer_ 上面就写着 “512 byte cache for device blocks” —— 你的 println() 落进去的就是这里。只有当这一块填满、你调用 flush()、或者 close() 掉文件时,卡才会被真正碰到。在这中间拔掉电源,对卡来说那几行从来没存在过。
另一半原因,解释了为什么文件读起来是“空的”,而不只是“短了一截”。flush() 会调用 SdFile::sync(),它既写回缓存的那一块,也把文件当前的大小抄回它的目录项里。在这件事发生之前,目录里记的还是旧的大小,所以那些已经物理写到卡上的字节,就落在文件自称拥有的范围之外。
每采一次样就 flush 一次,是反过来的那个错误。每次 flush 都是两次写入 —— 数据块加目录项 —— 再加上等卡确认的那段停顿;而闪存单元能擦写的次数是有限的,得留给控制器去分摊。给它定一个节奏:这段 sketch 每十二次采样 flush 一次,在五秒间隔下就是每分钟一次,所以一次断电最多损失一分钟的读数,跑满一天是 1,440 次 flush,而不是 17,280 次。
拔卡之前先把 USB 线拔掉 —— 带电抽卡,是把文件系统弄坏的经典路子。TEMPLOG.CSV 是纯文本,任何表格软件都画得出图;而 FILE_WRITE 是追加模式:下次上电会接着写同一个文件,不会把它覆盖掉。

为什么每一行的时间戳都是 01/01/2000?
DS1307 不只是上电时间不对 —— 它上电时是停着的。它的数据手册写明:首次上电时寄存器复位成 01/01/00,秒寄存器里的 CH 位被置为 1 —— 而 CH 置位就代表振荡器停摆。后备电池的作用,就是不让“首次上电”在每一次开机时重演;电池座空着,时钟就每次都停着重启,每一行也就带着同一个冻住的日期。数据手册还说,不使用后备电源时 VBAT 脚必须接地,而空的电池座是让那支脚悬着 —— 这是把电池装上的又一个理由。

这段 sketch 把这件事摊在明面上。rtc.isrunning() 读的正是那个 CH 位;时钟停着时,sketch 会把它启动,并用 __DATE__ 和 __TIME__ 这两个宏去设时间 —— 而这两个宏装的,是 sketch 被编译的那一刻。没有电池时,这就是诚实的上限:采样间隔依旧准确,绝对日期则在每次上电时退回编译时间。事后再补装电池才是那个陷阱 —— 那时时钟已经在跑了,这段代码再也不会执行,于是每一行都带着一个过期的编译时间。要么在第一次上传之前就把电池装好,要么把 rtc.adjust() 挪到 isrunning() 判断的上面,先上传一次。
装上 CR1220,时钟就能撑过断电。它仍然会走偏,因为 DS1307 数的是一颗普通的 32.768 kHz 晶振,没有温度补偿,而晶振的频率会随温度移动 —— 一台塞在闷热屋顶夹层里的记录仪,走得比放在桌上的那台更快。如果半年之后时间戳还必须是准的,我们数字时钟教程里那颗 DS3231 把晶振和温度计都做进了芯片内部。
我们常见到的错误
microSD 卡插不进去。卡座是带弹簧退卡的全尺寸 SD,不是 microSD。一张普通的全尺寸转接卡就能解决 —— 把卡格式化成 FAT32,容量控制在 32 GB 以内。
文件在,可里面是空的,或者写到一半就断了。这是缓冲没落盘,不是故障:断电时还有一整块数据留在 RAM 里。要么 flush 得勤一点,要么给记录仪一个明确的停止动作 —— 一个按钮,或者一条串口命令,去调用 close() —— 等它做完再断电。拔掉传感器救不了:那只是不再产生新行,已经缓冲起来的那些照样留在 RAM 里。在你加上那个停止动作之前,你选的 flush 节奏就是你的损失窗口。
每一行都写着 01/01/2000。电池座是空的,DS1307 停着重启。装一颗 CR1220;或者在第二列里记下经过的毫秒数,接受时间基准会归零这件事。
-127.00,或者冻住的 85.00。两个都指向 1-Wire 那条线,而不是传感器本身。检查那颗电阻是不是真的跨在黄线那一列和红线那一列之间。
常见问题
Arduino 数据记录扩展板能插 microSD 卡吗?
不能 —— 它是带弹簧退卡的全尺寸 SD 卡座。microSD 装在转接卡里用完全没问题。格式化成 FAT32,容量控制在 32 GB 以内:Arduino 的 SD 库只读 SD 与 SDHC 卡上的 FAT16 和 FAT32。
串口明明看到读数了,为什么 SD 卡上的文件是空的?
这个库在 RAM 里缓冲一块 512 字节的数据,只有在这一块填满、调用 flush() 或 close() 时才落盘。串口输出不经过这块缓冲,所以它看起来一切正常,卡上却什么都没有。
Arduino 数据记录扩展板一定要装 CR1220 电池吗?
记录数据不需要。没有 CR1220 的话,DS1307 每次上电都从 01/01/2000 停着重启,所以每次开机都得由 sketch 去设一次时间。读数和采样间隔不受影响。
为什么我的 DS18B20 在数据记录扩展板上读到 -127?
它的 DQ 脚是开漏的 —— 能把线拉低,却没办法把线抬高 —— 所以它需要一颗 4.7 kΩ 到 10 kΩ 的电阻,从黄线上拉到 5V。没有这颗电阻,线就是浮空的,库会报出 -127.00,也就是“没有设备应答”。
最后更新:2026年8月。有问题?WhatsApp 联系我们。



Arduino Uno Compatible SMD UNO R3 with Type B Cable - ATMEGA328P with CH340G-Microcontroller Project
Data Logging Shield Data Logger Module Data Recorder with Built in RTC For Arduino IOT Application
Temperature Sensor DS18B20 Temperature Measurement Probe WaterProof 18B20
MB102 Breadboard 170 400 830 Holes Breadboard Donut Board Arduino Prototype Multi Color - MINI BREADBOARD 170 HOLES (WHITE)
40pcs Dupont Wire 10cm 20cm 30cm for Breadboard DIY Experiment Jumper Wire Breadboard wire - DUPONT WIRE M-M 30CM
400 pcs 1/4W Resistor Pack Resistor Kit 20 Common Value with 20 each
XL830L Digital Multimeter with backlight Portable Multimeter Electronics Project