基于Arduino的DIY温湿度时钟:DHT11、DS1307与5110液晶屏实战指南

1. 项目缘起:为什么需要自己动手做一个温湿度计兼时钟?

几年前,我还在用着那种超市里十几块钱买的电子温湿度计,显示的数字时大时小,湿度读数经常飘忽不定,最要命的是里面的纽扣电池一年得换好几次,每次换完还得重新对时。后来我接触了Arduino,看着手头闲置的DHT11温湿度传感器、DS1307实时时钟模块和一块诺基亚5110液晶屏,一个想法就冒了出来:为什么不自己做一个呢?一个能精确显示时间、实时监测环境温湿度,并且可以自己编程控制、甚至未来扩展功能的桌面小设备。

这个想法背后的需求其实很具体:我需要一个稳定、可靠、可定制的环境监测终端。市售产品要么功能单一,要么数据不准,要么就是封闭系统,你想加个数据记录或者超限报警功能根本无从下手。而用DHT11、DS1307和5110液晶这三样经典器件组合,恰恰能完美解决这些问题。DHT11负责采集温湿度,虽然精度不算顶级,但对于家庭和办公室环境监测完全够用,关键是它数字输出,抗干扰能力比那些模拟传感器强得多。DS1307提供精准的计时,自带电池备份,断电也不用担心时间丢失。5110液晶屏则是经典中的经典,84x48的分辨率显示这些信息绰绰有余,功耗极低,而且驱动简单。

更重要的是,这个项目是一个绝佳的嵌入式开发入门实践。它涵盖了传感器数据读取、I2C总线通信、SPI液晶驱动、时间管理等多个核心知识点,代码量适中,硬件连接清晰,成功率高,非常适合从点亮LED灯进阶到实际应用的爱好者。做完之后,你得到的不仅仅是一个工具,更是一套可复用的技能,未来无论是想做更复杂的智能家居节点,还是给其他项目添加时间或环境感知功能,都能直接套用这里的经验。

2. 核心器件选型与特性深度剖析

选择这三样器件并非偶然,它们都是在开源硬件领域久经考验、资料丰富、性价比极高的代表。理解它们的特性,是项目成功和后续调试的基础。

2.1 DHT11温湿度传感器:够用且稳定的数字采集单元

DHT11是一款复合型温湿度传感器,内部集成了一个电阻式感湿元件和一个NTC测温元件,并通过一个8位单片机进行校准和数字信号输出。它的核心优势在于单总线数字信号输出。这意味着你只需要一根数据线(除了电源和地)就能读取数据,极大简化了布线,也避免了模拟信号在长距离传输中容易受到的干扰。

它的参数决定了其应用场景:

  • 测量范围:湿度20-90%RH,温度0-50℃。这基本覆盖了室内环境的常态范围。
  • 精度:湿度±5%RH,温度±2℃。对于需要高精度科研或工业控制的场景,这个精度不够,DHT22或SHT系列是更好的选择。但对于“今天是不是比昨天干燥”、“房间温度是否适宜”这类定性或半定量判断,DHT11完全胜任。很多市售廉价温湿度计用的就是同级别甚至更差的传感器。
  • 响应时间:湿度响应较慢,通常需要1-2秒才能稳定。编程时需要注意,两次读取之间必须间隔至少2秒,否则可能读到错误数据或没有响应。
  • 供电:3.3V-5.5V,与Arduino的5V或3.3V输出完美兼容。

注意:DHT11的塑料壳体顶部有一个小小的网格窗口,那是传感器的感应区,务必保持通畅,不要被遮挡或沾上灰尘、水渍,否则会影响测量准确性。

2.2 DS1307实时时钟模块:永不掉线的“时间管家”

Arduino本身没有硬件时钟,断电后时间信息就丢失了。DS1307就是为解决这个问题而生的。它是一个低功耗的I2C接口实时时钟/日历芯片,最大特点就是自带电池备份。模块上通常有一颗CR2032纽扣电池,当主电源(如Arduino的5V)断开时,芯片由这颗电池供电,继续走时,数据完全不会丢失。

它的关键特性包括:

  • I2C接口:只需要两根线(SDA数据线,SCL时钟线)就能与Arduino通信,节省IO口。I2C是主从模式,Arduino作为主机发起读写请求。
  • 时钟精度:典型精度还不错,但受温度影响会有漂移,一个月误差几分钟是正常的。对于日常显示完全可接受,如果追求极致精度,可以选择内置温度补偿的DS3231,但价格也更高。
  • 内部寄存器:DS1307的时间信息(秒、分、时、日、月、年、星期)存储在特定的寄存器中。我们编程的本质,就是通过I2C协议去读写这些寄存器。初次使用或更换电池后,必须通过程序向其“写入”一个正确的时间初始值。
  • 方形波输出:它还有一个SQW/OUT引脚,可以编程输出几种频率的方波信号,在某些需要定时触发的应用中可能用到,本项目可以暂不连接。

2.3 诺基亚5110液晶屏:复古可靠的显示终端

这块屏源于古老的诺基亚5110手机,因其极高的性价比和极低的功耗在电子爱好者中焕发第二春。它是一款84x48像素的图形液晶,单色显示,背光可调(通常是LED背光)。

它的驱动特点:

  • SPI接口:通常使用软件模拟SPI(Software SPI)驱动,只需要4-5根线(RST复位、CE片选、DC命令/数据、DIN数据输入、CLK时钟)。相比并行驱动的LCD1602,它占用IO口更少,且能显示任意图形。
  • PCD8544控制器:屏幕的核心驱动芯片。我们需要通过SPI向这个控制器发送命令和数据,来控制显示内容、对比度等。好在有成熟的库(如Adafruit_PCD8544U8g2)封装了这些底层操作,让我们可以像在画布上画点、线、文字一样简单。
  • 对比度调节:5110屏的显示效果非常依赖对比度设置。对比度电压由模块上的一个可调电阻(或通过程序)控制。设置不当,屏幕可能全黑、全白或显示淡得看不清。通常需要通过程序尝试一个值(比如lcd.setContrast(60))来获得最佳视觉效果。

3. 硬件系统搭建与连接详解

理论清楚了,接下来就是动手连接。清晰的硬件连接是软件调试的前提。这里我们以最经典的Arduino Uno作为主控板来讲解连接方案。

3.1 系统连接图与引脚定义

首先,我们需要规划好各个模块连接到Arduino的哪个引脚。遵循两个原则:一是优先使用模块库推荐的默认引脚以便于示例代码运行;二是合理分配,避免冲突。

下面是一个经过实践验证的可靠连接方案:

模块引脚连接到 Arduino Uno 引脚说明
DHT11VCC5V电源正极
GNDGND电源地
DATADigital 2数据引脚,需接一个4.7K-10K的上拉电阻到VCC
DS1307 RTCVCC5V电源正极
GNDGND电源地
SDAAnalog A4I2C数据线,在Uno上固定为A4
SCLAnalog A5I2C时钟线,在Uno上固定为A5
5110 LCDVCC3.3V5V重要:接5V时背光更亮,但需确保模块支持5V输入。稳妥起见接3.3V。
GNDGND电源地
RST (RESET)Digital 8复位引脚,低电平有效
CE (CS)Digital 7片选引脚,低电平选中
DC (D/C)Digital 6数据/命令选择引脚
DIN (MOSI)Digital 5数据输入引脚(主设备输出,从设备输入)
CLK (SCK)Digital 4时钟引脚
LED (背光)通过220Ω电阻接Digital 3背光阳极,阴极接GND。用PWM引脚可调亮度。

连接要点与避坑指南:

  1. DHT11的上拉电阻:这是最容易忽略导致读取失败的点。DHT11的数据线是开漏输出,必须接一个4.7KΩ - 10KΩ的电阻到VCC(5V),为总线提供一个稳定的高电平。很多模块已经内置了这个电阻,如果购买的是模块,可以检查一下。如果是单独的传感器,务必自己加上。
  2. I2C引脚固定:在Arduino Uno(以及基于ATmega328P的板子)上,SDA对应A4,SCL对应A5,这是硬件的物理映射,不能更改。其他板子如Mega、Leonardo、ESP32等引脚不同,需要查对应板子的资料。
  3. 5110液晶的电源选择:这是一个经典的坑。5110液晶的逻辑电平是3.3V,但其供电范围较宽。如果模块本身有3.3V稳压芯片(LDO),那么接5V是安全的,电压会被降到3.3V。如果模块没有LDO,直接接5V可能会烧毁屏幕!最安全的做法是:先用万用表测量模块上VCC和GND之间的电压,或者仔细查看产品说明。不确定时,一律接Arduino的3.3V输出引脚。接3.3V的缺点是背光可能稍暗。
  4. 背光控制:背光LED是一个发光二极管,必须串联一个限流电阻(通常220Ω),然后由Arduino的一个数字引脚驱动。我推荐接到Digital 3,因为它是PWM引脚,你可以通过analogWrite(3, brightness)来调节背光亮度,实现夜间模式等功能。
  5. 电源去耦:如果系统工作不稳定(如屏幕闪烁、数据读取偶尔错误),可能是电源噪声导致。可以在每个模块的VCC和GND之间就近焊接一个0.1uF(104)的陶瓷电容,效果立竿见影。

3.2 面包板布局与焊接建议

对于原型验证,面包板是最佳选择。布局时,尽量让电源线(红色+,黑色-)沿着面包板两侧的电源轨走,形成清晰的主干。各个模块围绕Arduino放置,数据线尽量短且整齐。这样既美观,也减少了信号串扰的可能性。

如果你打算做一个永久性的作品,焊接是更好的选择。可以使用一块洞洞板(万用板),按照连接图进行焊接。建议:

  • 先焊接电源和地线的主干道。
  • 给Arduino、DS1307、DHT11的电源入口处都加上一个0.1uF的贴片电容。
  • 5110液晶最好使用排母焊接,这样屏幕可以插拔,方便更换或调试。
  • 所有连接线使用不同颜色的导线区分功能(红-5V,黑-GND,黄-数据,绿-时钟等),后期排查故障会轻松无数倍。

4. 软件开发环境与核心库配置

硬件连好了,接下来就是让大脑(程序)运转起来。我们将使用Arduino IDE进行开发。

4.1 库的安装与管理

这个项目需要三个核心库,它们封装了与硬件通信的复杂细节。

  1. DHT Sensor Library:用于驱动DHT11(也支持DHT22等)。这是读取温湿度的关键。

    • 在Arduino IDE中,点击工具->管理库...,打开库管理器。
    • 在搜索框中输入“DHT sensor”,找到由Adafruit发布的“DHT sensor library”,点击安装。通常它会提示你同时安装依赖库“Adafruit Unified Sensor”,一并确认安装。
  2. RTClib:用于驱动DS1307(也支持DS3231等)。这是读写时间的核心。

    • 同样在库管理器中搜索“RTClib”,找到由Adafruit发布的版本进行安装。这是一个非常通用且稳定的RTC库。
  3. Adafruit PCD8544 Nokia 5110 LCD Library:用于驱动5110液晶屏。

    • 在库管理器中搜索“PCD8544”,找到“Adafruit PCD8544 Nokia 5110 LCD library”进行安装。这个库提供了基本的画点、画线、显示文本和位图的功能。

安装完成后,你可以在文件->示例菜单中找到这些库的示例程序,强烈建议先分别运行一下每个示例,单独测试每个模块是否工作正常。比如,先运行DHT库的示例,看看串口监视器里能否打印出温湿度;再运行RTC库的示例,设置并读取时间;最后运行5110库的示例,看看屏幕能否点亮并显示测试图形。这一步的“分步调试”能帮你快速定位问题是出在硬件连接、库安装还是代码逻辑上。

4.2 代码架构与核心逻辑解析

当三个模块单独测试都OK后,我们就可以编写整合代码了。完整的代码较长,这里我重点拆解核心逻辑和容易出错的环节。

首先,包含必要的头文件和定义引脚:

#include <DHT.h> #include <Wire.h> #include "RTClib.h" #include <Adafruit_GFX.h> #include <Adafruit_PCD8544.h> // 引脚定义 (必须与硬件连接一致!) #define DHTPIN 2 #define DHTTYPE DHT11 #define LCD_RST 8 #define LCD_CE 7 #define LCD_DC 6 #define LCD_DIN 5 #define LCD_CLK 4 #define BACKLIGHT_PIN 3 // 初始化对象 DHT dht(DHTPIN, DHTTYPE); RTC_DS1307 rtc; Adafruit_PCD8544 display = Adafruit_PCD8544(LCD_CLK, LCD_DIN, LCD_DC, LCD_CE, LCD_RST);

setup()函数中,我们需要完成一系列初始化:

void setup() { Serial.begin(9600); // 用于调试输出 Wire.begin(); // 初始化I2C总线 dht.begin(); // 初始化DHT传感器 display.begin(); // 初始化液晶屏 display.setContrast(60); // 设置对比度,这个值需要根据你的屏幕调整,范围0-127 display.clearDisplay(); // 清屏 display.display(); // 将缓冲区内容刷到屏幕 pinMode(BACKLIGHT_PIN, OUTPUT); analogWrite(BACKLIGHT_PIN, 150); // 设置背光亮度(PWM值0-255) // 初始化RTC,如果RTC未运行,则设置一个初始时间 if (!rtc.begin()) { Serial.println("找不到RTC模块!"); while (1); // 停止程序 } if (!rtc.isrunning()) { Serial.println("RTC未运行,设置初始时间..."); // 这行代码只在第一次运行,或者RTC电池没电后需要运行。 // 设置时间为编译此程序的时间。 rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // 你也可以手动设置一个固定时间,例如: // rtc.adjust(DateTime(2024, 5, 27, 14, 30, 0)); // 2024年5月27日 14:30:00 } }

关键点解析

  • rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));这行代码非常巧妙,它使用编译器宏__DATE____TIME__获取你编译程序时的电脑时间,并将其设置为RTC的初始时间。这意味着你只需要在第一次烧录程序时,确保电脑时间是准确的,RTC就会获得一个近乎正确的时间。之后每次上电,RTC都会从这个起点继续走时。切记,这行代码在设置一次后应该注释掉,否则每次重启Arduino,时间都会被重置为编译时间。

主循环loop()的逻辑是项目的核心:

void loop() { // 1. 读取RTC时间 DateTime now = rtc.now(); // 2. 读取DHT11温湿度 (注意间隔至少2秒!) float humidity = dht.readHumidity(); float temperature = dht.readTemperature(); // 读取摄氏温度 // 检查读取是否成功 if (isnan(humidity) || isnan(temperature)) { Serial.println("读取DHT11失败!"); // 可以在屏幕上显示错误信息,这里简单返回,下次循环再试 display.clearDisplay(); display.setCursor(0,0); display.println("Sensor Error"); display.display(); delay(2000); // 等待一段时间再重试 return; } // 3. 在5110液晶上显示信息 display.clearDisplay(); // 清除显示缓冲区 display.setTextSize(1); // 设置字体大小(1是默认,每个字符6x8像素) display.setTextColor(BLACK); // 设置字体颜色(单色屏只有黑和白) // 显示时间 (第一行) display.setCursor(0,0); display.print(now.year(), DEC); display.print('/'); display.print(now.month(), DEC); display.print('/'); display.print(now.day(), DEC); display.print(' '); display.print(now.hour(), DEC); display.print(':'); if(now.minute() < 10) display.print('0'); // 分钟补零 display.print(now.minute(), DEC); display.print(':'); if(now.second() < 10) display.print('0'); // 秒钟补零 display.print(now.second(), DEC); // 显示温度 (第二行) display.setCursor(0, 10); display.print("Temp: "); display.print(temperature, 1); // 显示一位小数 display.print(" C"); // 显示湿度 (第三行) display.setCursor(0, 20); display.print("Humi: "); display.print(humidity, 1); // 显示一位小数 display.print(" %"); // 4. 将缓冲区内容发送到屏幕,真正显示出来 display.display(); // 5. 延时。DHT11需要至少2秒的读取间隔,这里延时3秒进行一次更新。 delay(3000); }

代码中的经验与技巧

  • DHT11读取间隔delay(3000)是必须的,遵守了传感器的最小采样间隔要求。如果你想更实时地更新时间显示(每秒刷新),可以将时间显示和传感器读取分开。例如,在主循环里每秒更新一次时间,但每3秒才读取一次DHT11并更新温湿度显示。这需要用到millis()进行非阻塞定时,是更高级的编程技巧。
  • 显示格式化:代码中对分钟和秒钟进行了补零操作(if(now.minute() < 10) display.print('0');),这样“14:05:09”就不会显示成“14:5:9”,看起来更专业。
  • 错误处理:对dht.read的返回值进行了isnan()检查。这是非常良好的习惯,能防止因为偶尔的读取失败(可能是信号干扰)导致屏幕上显示“NaN”之类的乱码,而是给出明确的错误提示。
  • 显示缓冲区display.clearDisplay()display.display()是关键。前者清空的是内存中的图形缓冲区,后者才将缓冲区的内容一次性发送到屏幕。所有printdraw操作都是在修改缓冲区,最后调用display()才生效。这避免了屏幕刷新时的闪烁。

5. 功能优化与深度扩展思路

一个能显示时间和温湿度的基础版本已经完成。但作为一个DIY项目,它的魅力在于可扩展性。这里分享几个我实践过的优化和扩展方向。

5.1 提升显示效果与交互体验

基础的文本显示有些单调。我们可以利用5110液晶的图形能力做得更好。

  • 绘制简易图标:用display.drawBitmap()函数或自己画点display.drawPixel(),在温度值旁边画一个温度计图标,在湿度旁边画一个水滴图标,直观性大大提升。
  • 自定义字体:Adafruit_GFX库支持自定义字体。你可以找到或制作一套小号的数字字体,专门用来显示时间,让数字更大更醒目。
  • 添加背光自动控制:通过一个光敏电阻检测环境光照,在光线暗时自动调低或关闭背光(analogWrite(BACKLIGHT_PIN, 0)),节省电量,也避免夜间刺眼。这需要增加一个模拟输入引脚读取光敏电阻值。
  • 按钮切换显示页面:增加1-2个按钮,实现显示内容的切换。比如,页面1显示当前时间温湿度;页面2显示今日的最高/最低温湿度记录;页面3显示一个简单的温湿度随时间变化的趋势图(需要记录历史数据)。

5.2 数据记录与离线分析

单纯的实时显示价值有限,记录历史数据才能分析趋势。

  • 添加SD卡模块:使用SPI接口的SD卡模块,每隔一段时间(如10分钟)将时间戳、温度、湿度数据以CSV格式写入文件。这样就能获得长时间的环境数据日志。
  • 使用EEPROM:如果数据量不大(比如只记录每天的最高最低值),可以使用Arduino板载的EEPROM来存储。断电后数据不会丢失。但EEPROM有擦写次数限制(约10万次),不适合频繁写入。
  • 简单的趋势显示:在5110液晶上实现一个简单的滚动图表。在内存中维护一个固定长度的数组(比如84个数据点,对应屏幕宽度),每次新的温度读数进来,就移除最旧的点,绘制新的折线图。这能直观地看到最近一段时间内的温度变化趋势。

5.3 联网与智能告警

这是将本地设备升级为物联网节点的关键一步。

  • 更换主控为ESP8266或ESP32:这两款芯片自带Wi-Fi功能,价格与Arduino Uno相仿,但性能更强。将代码移植过去(引脚定义需要修改,库大部分通用)。
  • 数据上传云端:通过Wi-Fi,可以将温湿度数据上传到免费的物联网平台,如Thingspeak、Blynk,或者自建的Home Assistant服务器。这样你就可以在手机App或网页上远程查看数据,甚至生成精美的图表。
  • 实现超限报警:在代码中设定温度和湿度的合理范围(如温度高于28℃或湿度低于30%)。当数据超限时,可以通过控制蜂鸣器发出警报,或者通过Wi-Fi向你的手机发送一条推送通知(需要配合云平台或IFTTT等服务)。

5.4 提升精度与稳定性

如果对数据质量有更高要求,可以从硬件和软件两方面入手。

  • 传感器升级:将DHT11更换为DHT22SHT30。DHT22测量范围更广(-40~80℃),精度更高(湿度±2%,温度±0.5℃)。SHT30则是I2C接口的工业级传感器,精度和稳定性更上一层楼,当然价格也更高。更换传感器后,只需在代码中修改传感器类型常量(如#define DHTTYPE DHT22)并调用对应的库即可,整体框架不变。
  • RTC升级:将DS1307更换为DS3231。DS3231内部有温度补偿晶体振荡器,年误差可控制在分钟级别,远优于DS1307。它们都使用RTClib库,且I2C地址相同,通常是直接替换,代码无需改动。
  • 软件滤波:对于DHT11这类偶尔会有小跳变的传感器,可以采用简单的软件滤波算法。比如“移动平均滤波”:连续读取5次温度,去掉一个最大值和一个最小值,然后对剩下的3个值取平均。这能有效平滑数据,消除偶然的毛刺。注意,这需要额外的读取时间,要平衡好实时性和稳定性。

从一堆零散的模块,到最终一个稳定运行、显示清晰的桌面小设备,这个过程充满了调试的乐趣和解决问题的成就感。这个项目像一把钥匙,打开了一扇门,门后是传感器应用、人机交互、数据记录乃至物联网的广阔世界。我自己的那个版本,后来加了一个小外壳,放在书桌上已经稳定运行了两年多,期间只换过一次RTC的电池。每当看到它准确显示着时间和环境数据,都会想起当初连接第一根线、点亮第一行代码时的兴奋。动手去实现它,你收获的将远不止一个温湿度计。