☰
51单片机驱动OLED与DS18B20显示温度时间
2026/10/21 9:05:16 网站建设 项目流程

简介:这份zip压缩包是一个基于51单片机、DS18B20数字温度传感器与OLED显示屏的实时温度时间显示项目,面向单片机初学者、电子爱好者及嵌入式开发者,也适合作为课程设计或毕业设计的参考。压缩包共31个文件,大小约107KB,包含C语言源文件(main.c、iic.c、temp.c等)、头文件、Keil工程文件(uvproj)、hex烧录文件以及一张效果图,此外还有编译生成的obj与lst中间文件,方便查看工程全貌。资源已有2347人学习下载。项目核心涉及DS18B20的一线总线协议读取、OLED屏的I2C驱动,以及时间显示逻辑,代码结构清晰,注释与模块划分便于理解。通过烧录hex文件或在Keil中打开源码进行编译调试,读者可直观掌握传感器数据采集与显示设备交互的方法,同时也能学习51单片机外设编程和软硬件协同设计的实用技巧。

1. OLED显示屏 + DS18B20:一块小屏,把温度和时间都装进去

空调房里永远吵不准温度是几度,买的成品温度计电池半年一换,屏幕还灰蒙蒙看不清。某开发者手里刚好有一堆吃灰的 51 单片机最小系统板、一块 0.96 寸 OLED、一个 DS18B20 温度传感器,于是花了两个晚上拼出这个「OLED 显示屏 + DS18B20 显示温度和时间」的小项目。它做的是这么一件事:用 51 单片机同时驱动两块外设,一块是四针 IIC 接口的 OLED 显示屏,另一块是单总线协议的 DS18B20 数字温度传感器,中间再挂一个 DS1302 时钟芯片,让屏幕不仅显示实时温度,还同步显示年月日、时分秒。适合的人群很明确:刚学完单片机基础、想把手头传感器整合成完整小系统的学生开发者,以及想给智能家居做个低成本状态面板的嵌入式爱好者。整个项目逻辑不复杂,但把通信协议、时序配合、显示刷新这些基本功全练了一遍。

2. 硬件架构和通信原理:OLED、DS18B20、DS1302 是怎么凑到一起的

2.1 系统整体框图:三条总线的分工

这个项目的本质是一个多外设协同的单片机系统。主控是经典的 STC89C52 或者 STC12 系列,工作在 5V 供电下。三个外设分别挂在不同的通信接口上,避免相互干扰。

  • OLED 显示屏:标准 0.96 寸,分辨率 128x64,驱动芯片是 SSD1306,走 IIC(I2C)总线,默认从机地址 0x78(写)或 0x7A(读)。IIC 只需要两根线:SCL(时钟)和 SDA(数据)。
  • DS18B20 温度传感器:走 1-Wire 单总线协议,一根数据线既供电又传数据,DQ 引脚需要外接一个 4.7kΩ 上拉电阻到 VCC。
  • DS1302 时钟芯片:也是 3 线接口,CE(片选)、SCLK(时钟)、IO(数据),用来记录时间,系统断电后靠纽扣电池 CR2032 继续走时。

为什么这么组合?因为这三样东西刚好是嵌入式入门阶段最典型的三类通信方式:IIC、单总线、三线 SPI 变体。做完这个项目,你对这三种总线的手动时序模拟就都有了代码层面的理解,不是对着数据手册空看,而是自己写代码去操作每一位。

2.2 引脚分配表:一个可行的默认接法

这里给出一个常用的引脚分配,方便你对照自己的开发板接线。不同开发板的 P2 口、P3 口位置可能不一样,但无所谓,只要在宏定义里改引脚编号就行。

外设引脚单片机端口说明
OLEDSCLP1.0IIC 时钟线
OLEDSDAP1.1IIC 数据线
DS18B20DQP1.2单总线数据线,需外接上拉电阻
DS1302CEP1.3时钟芯片片选
DS1302SCLKP1.4时钟芯片串行时钟
DS1302IOP1.5时钟芯片数据线

DS18B20 的 VCC 和 GND 分别接 5V 和地,DS1302 的 VCC2 接 5V,VCC1 接 3V 纽扣电池。OLED 是四针模块,VCC 接 5V 也能工作,因为模块上通常自带稳压和电平转换,但长时间的稳妥做法还是参考模块说明书,有些纯裸屏必须接 3.3V。

接线完成后先别急着上电。用万用表量一遍 VCC 和 GND 之间有没有短路,这是做硬件最容易翻车的一步:杜邦线插错一个孔位,上电瞬间芯片就冒烟了。

2.3 SSD1306 驱动原理:写命令和写数据的区别

OLED 屏幕的核心是 SSD1306 驱动芯片,它内部有一块 1KB 的显存,分成 8 页(Page),每页 128 个字节。你往这块显存里写什么,屏幕就显示什么。写显存有两个基本操作:写命令和写数据。

写命令时,IIC 总线上的字节格式是这样走的:

START -> 从机地址(0x78) -> 控制字节(0x00) -> 命令字节 -> STOP

控制字节为 0x00 表示后面跟的是命令,为 0x40 表示后面跟的是显示数据。这个区别是整个驱动代码的基础。初始化屏幕时,你要发一串初始化命令,比如关闭显示、设置显示时钟分频比、设置多路复用比、设置显示偏移、开启显示等。多数代码段里你会看到类似这样的初始化函数:

void OLED_Init(void) { OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x02); // 页寻址模式 OLED_WriteCmd(0xC8); // 扫描方向,从上到下 OLED_WriteCmd(0x40); // 起始行 0 OLED_WriteCmd(0x81); // 对比度设置 OLED_WriteCmd(0xCF); // 对比度值 OLED_WriteCmd(0xA1); // 段重映射,正常方向 OLED_WriteCmd(0xA6); // 正常显示(不是反色) OLED_WriteCmd(0xA8); // 多路复用比设置 OLED_WriteCmd(0x3F); // 64 行 OLED_WriteCmd(0xD3); // 显示偏移 OLED_WriteCmd(0x00); // 偏移 0 OLED_WriteCmd(0xD5); // 显示时钟分频 OLED_WriteCmd(0x80); // 分频值 OLED_WriteCmd(0x8D); // 电荷泵设置 OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); }

这段初始化的关键点是电荷泵设置。0x8D 命令后面必须跟 0x14 来开启内部升压电路。很多人屏幕亮不起来的典型原因就是漏了这一步或者写成了 0x10 关闭电荷泵。OLED 本身不自发光,如果没有内部电荷泵升压,屏幕就是漆黑一片。初始化完成后,整屏显示什么由显存内容决定。

2.4 DS18B20 时序:读温度的核心难点

DS18B20 是单总线器件,所有通信都在一根线上完成,所以时序要求非常严。读写每一位数据都要精确控制时间和顺序。读一次温度的基本流程分为四步:复位、跳过 ROM(因为总线上只有一个传感器)、启动温度转换、读暂存器数据。时序代码的要点是:

// 复位 DS18B20 bit DS18B20_Reset(void) { bit presence; DQ = 0; delay_us(480); // 拉低至少 480us DQ = 1; delay_us(60); // 释放总线后等 60us presence = DQ; // 读存在脉冲,0 表示有设备应答 delay_us(420); return presence; }

复位时序里,主机先拉低总线 480 微秒以上,然后释放。DS18B20 会在 60~240 微秒内把总线拉低作为应答脉冲。如果你读到 presence 不为 0,说明总线上没有设备或者接线有问题。这里容易踩的坑是延时函数的精度:单片机主频不同,同样写一个delay_us(10)实际延时可能差好几倍。最好的做法是用逻辑分析仪或者示波器看波形,没有的话就观察温度读取是否稳定,不稳定就调整延时参数。

接下来是写一位和读一位的时序。写 1 位时,主机拉低总线后,在 15 微秒内释放总线表示写 1,保持拉低则表示写 0。读一位时,主机拉低总线 1 微秒后释放,紧接着采样总线电平。

// 读一个字节 unsigned char DS18B20_ReadByte(void) { unsigned char i, data_byte = 0; for (i = 0; i < 8; i++) { data_byte >>= 1; DQ = 0; _nop_(); _nop_(); DQ = 1; _nop_(); _nop_(); if (DQ) data_byte |= 0x80; delay_us(60); } return data_byte; }

这段代码里的data_byte >>= 1把上一次的数据往低位移动,然后判断当前位是否为 1,如果是就把最高位置 1,这样最后组出来的字节是最低位先接收,符合小端顺序。读出来的温度是两个字节,低字节的高 4 位是小数部分,高字节是符号位和整数部分。要把原始值换算成可显示的温度值,需要按 0.0625 的精度进行转换:

temp_value = (read_val & 0x07FF) * 0.0625; // 去除符号位后乘精度 if (read_val & 0x8000) temp_value = -temp_value; // 负数处理

2.5 DS1302 时钟芯片:与按键配合的校时思路

DS1302 负责提供时间数据,它内部维护了一套寄存器,分别对应秒、分、时、日、月、周、年。每个寄存器存的都是 BCD 码,所以写入时要转换,读出时也要转换。比如十进制数 23 在 BCD 码里是 0x23,直接往寄存器里写 23 是不对的,写 0x23 才能表示 23 点。

读时间的代码一般长这样:

unsigned char DS1302_ReadByte(void) { unsigned char i, temp = 0; for (i = 0; i < 8; i++) { temp >>= 1; if (IO) temp |= 0x80; SCLK = 1; SCLK = 0; } return temp; }

含义是每一个时钟上升沿读取一位数据,因为 DS1302 是低位先出的结构,所以组装的时候用右移。这个习惯和 DS18B20 一致,但是和很多 SPI 外设(高位先出)不一样,写驱动脚本时要注意区分,否则读出来的数据就像打乱了一样。

3. 核心代码逐段拆解:从初始化到屏幕上出现一行温度

3.1 主程序骨架:一个大循环里的三步流程

程序的结构非常清晰,分为三步:初始化硬件、在 while 循环里读传感器数据、将格式化后的文本渲染到 OLED 上。主函数如下:

void main(void) { OLED_Init(); // 初始化 OLED DS1302_Init(); // 初始化时钟芯片 DS18B20_Init(); // 初始化温度传感器端口 OLED_ShowString(0, 0, "Temp:", 16); // 静态提示文字 while (1) { DS18B20_StartConvert(); // 启动一次温度转换 temp = DS18B20_GetTemp(); // 读取转换后的温度值 DS1302_GetTime(); // 读取当前时间,存到全局结构体 OLED_ShowNum(0, 2, temp, 3, 16); // 显示温度整数部分 OLED_ShowString(45, 2, ".", 16); // 小数点 OLED_ShowNum(53, 2, temp_decimal, 1, 16); // 小数部分 OLED_ShowNum(0, 5, time.year, 2, 16); OLED_ShowString(32, 5, "-", 16); OLED_ShowNum(40, 5, time.month, 2, 16); // ... 时间显示略 delay_ms(200); // 刷新率控制,太快会闪烁 } }

这里有个常见的性能问题:DS18B20 每次温度转换需要 750ms 的最大转换时间,如果在转换过程中delay_ms死等,整个系统会卡顿,时间的秒数跳动会不流畅。常见做法是启动转换后先去读时钟、刷新显示,等下一轮循环再读温度。这样温度滞后一点,但界面不会卡顿。我开始写这个项目时用了死等的办法,结果显示的时间每秒钟跳两次,界面总是一顿一顿的。

3.2 OLED 显示函数设计:坐标和大小如何控制

为了让屏幕上的文字排版可控,需要封装一整套坐标定位的底层函数。SSD1306 的最小操作单位是页(每页高 8 像素),横坐标 0~127。常见的字库大小是 6x8(ASCII 字符占 6 列 8 行)和 8x16(占 8 列 16 行)。我的显示函数包含下面几个:

  • OLED_ShowChar(x, y, char, size):先判断字符码,如果是换行符就换行处理,否则从字库里取模数据,然后写入显存。
  • OLED_ShowString(x, y, str, size):循环调 ShowChar,每显示一个字符 x 坐标加 (size/2+2)。
  • OLED_ShowNum(x, y, num, len, size):把一个数字拆成个十百千位,每位加 '0' 转成字符显示。

字符串显示函数是其中最容易出错的地方。如果 x 坐标递增方式不对,连续显示的长字符串在屏幕中间会出现断裂重叠。我一般固定用 len 参数控制显示长度,先把数字按% 10拆成单个数位,再循环处理,这样无论是两位数温度还是四位年份都能对齐。实际上手时很难一次写对,调试建议是先用 OLED 显示固定字符串,确认坐标正确,再接入动态数据。

这里也推荐采用查表法做字库,方便特殊符号定制。在这个项目里我用到的字模是自行提取的 ASCII 码 0~9、A~Z 加上摄氏度符号。DS18B20 温度要显示为类似 "26.5",其中小数点右侧是 .5 而不是 0.5,所以数据结构上把整数部分和小数部分从函数里分离出来会更好。小数部分来自 DS18B20 的低字节:低四位变化范围是 0~15,对应 0.0 到 0.9375 度。简单处理时用它除以16*10得到一位小数,这么做虽然不算精确,但视觉上够用,而且显示格式更加自然。

3.3 串口打印辅助调试:看不见的时候先打出来

很多人 OLED 上没有任何输出时,第一反应是改显示代码,但往往真正的问题出在传感器数据读取。这种场景下我是强烈建议留一个串口调试输出的。51 单片机的 UART 初始化和发送一个字节的代码很简单:

void UART_Init() { SCON = 0x50; // 模式1,8位UART,允许接收 TMOD |= 0x20; // 定时器1工作于模式2(8位自动重装) TH1 = 0xFD; // 波特率 9600 @ 11.0592MHz TL1 = 0xFD; TR1 = 1; // 启动定时器1 } void UART_SendByte(unsigned char dat) { SBUF = dat; while (!TI); TI = 0; }

波特率设置的关键在于晶振频率。11.0592MHz 晶振下 TH1 = 0xFD 对应 9600 波特率,用 12MHz 晶振时算出来的波特率不准,串口收到的就是乱码。所以做这个项目请尽量用 11.0592MHz 晶振的板子。把读出的温度和时间的原始十六进制数通过串口发到电脑,用串口助手看一遍,就能判断是传感器的问题还是屏幕显示的问题。这算是调嵌入式的一个基本心法:显示是最容易隐藏真相的环节,先把数据原始值打出来,能省两小时排查时间。

3.4 一个完整的中断驱动:刷新率与按键扫描的配合

主循环里除了读数据,还有按键扫描的任务。为了让屏幕刷新流畅,需要把按键扫描放在主循环里轮询,避免用中断去处理机械按键的抖动。项目里一般设置两个按键:一个切换显示模式(温度/时间/同时显示),另一个进入时间校准模式。

按键消抖的稳妥写法是状态机方式。判断按键按下,等 10ms 再读一次,确实是低电平才认为是有效按下。这种写法不会出现松手误触发,而且在中断里敲抖容易把时间片吃掉,所以我建议都放在主循环里,每轮扫描一遍,当前键值保存到全局变量。校时状态下,短按“模式”键来到下一个要调整的字段(年/月/日/时/分/秒),长按“调整”键完成数字递增,把 BCD 码和十进制做转换后写回 DS1302 寄存器。具体实现上,很多模块的做法是先把要修改的字段从time结构体里转成十进制,修改后重新转换为 BCD 再写入,同时通过一个标志位指明当前写的是哪个寄存器地址。

4. 从零跑起来:搭建电路、烧录固件、整机调试与常见踩坑

4.1 需要的硬件清单和连线确认

这里先给一份完整的物料清单,方便核对手里缺什么:

  • 51 单片机开发板或最小系统板(STC89C52RC / STC12C5A60S2 均可)
  • 0.96 寸 IIC 接口 OLED,SSD1306 驱动
  • DS18B20 温度传感器(TO-92 封装,要注意引脚顺序)
  • DS1302 时钟芯片 + 32.768KHz 晶振 + 纽扣电池座
  • 10kΩ 电位器或者直接接一个 4.7kΩ 电阻
  • 杜邦线若干
  • 5V 电源(USB-TTL 模块就能供电)
  • USB-TTL 下载模块(比如 CH340 芯片的那种)

实物连线时,DS18B20 的引脚朝向要让半圆形面对自己,从左到右分别是 GND、DQ、VCC。这个顺序和常见的三极管封装一样,很多人接反了导致温度读出来是 85 度。DS1302 的晶振要尽量靠近芯片引脚,如果用长导线连接晶振,时钟极容易停振或者走时不准。OLED 模块的 SDA 和 SCL 不要接反,否则屏幕完全没反应。IIC 总线上拉电阻一般模块已经自带,如果你用的是裸屏,就要在 SCL 和 SDA 上各接一个 4.7kΩ 上拉电阻到 VCC。

4.2 烧录和验证:下载程序后如何判断每条链路是否通了

STC 单片机的下载方式是冷启动下载,先点下载按钮再给单片机上电。打开烧录软件,选择对应的芯片型号,波特率可以从 9600 开始尝试,软件会自动识别。如果提示握手失败,检查串口号是否选对,以及板子上有没有把串口模块的 TX 和 RX 接反。

下载成功后,第一件事不是看温度,而是看 OLED 上有没有显示静态文字。如果屏幕有显示但温度是 85 度,这是 DS18B20 没有正确复位成功的典型表现。85 度是 DS18B20 上电后的默认温度值,复位成功并且读取到非零数据就会刷新掉。如果温度固定不变且一直是 0 或者 127,多半是数据线接触不良或上拉电阻缺失。第二件事是看时间是否在走。如果时间在走,且比真实时间慢很多,检查 32.768KHz 晶振的负载电容,一般 6pF 到 12.5pF 均可。如果时间直接不走,很可能 IO 线序接错了,重点查 CE 和 SCLK 是否接对。

OLED 无显示时,用万用表测 SCL 和 SDA 的电压,正常应当在 3.3V 左右。如果电压几乎为 0 且模块发烫,多半是模块的 VCC 接到了 GND。屏幕有背光但无字符,则检查 IIC 从机地址,有的 OLED 模块地址是 0x7A 而不是 0x78,在驱动代码头部把地址宏改掉即可。整机调试顺序建议是先把 OLED 点亮,再单独读 DS18B20 测试程序,最后才接 DS1302,分步验证可以大幅缩小排查范围。

4.3 核心代码编译环境的搭建

代码使用 Keil C51 开发环境,这不是最新的工具,但是这个项目用起来最稳。打开 Keil,新建工程,选择芯片型号为 AT89C52 或者对应 STC 型号,然后在工程里添加main.c、oled.c、ds18b20.c、ds1302.c等源文件。编译前需要改两个地方:在 Options for Target 里,把晶振频率改成 11.0592MHz,这个值影响串口波特率的计算,但是对普通延时函数也有影响。查看代码里的延时函数,确认和晶振频率一致。

我在处理这个项目时,喜欢设置生成 hex 文件:

// Keil 配置路径:Options for Target -> Output -> Create HEX File // 勾选后,编译会在工程目录下生成 .hex 文件; // 烧录软件打开这个文件即可写入单片机 Flash。

实际项目里常见的编译报错是缺少头文件reg51.h或者intrins.h,这两个文件在 Keil 安装目录的INC文件夹下。如果使用的是开源 SDCC 编译器,头文件的写法略有不同,需要把sbit P1_0 = P1^0;改成__sbit __at (0x90) P1_0;,但对新手来说直接用 Keil 最省心。编译通过但烧录后没有现象时,先核对生成的 hex 文件的大小,如果只有几十个字节,有可能是 main 函数是空的或者代码根本没写进编译路径,检查工程里的 C 文件是否全部添加了。

4.4 避坑记录:这个项目最常见的 5 个问题

以下是我做这类 OLED + DS18B20 项目时积累的几条真实踩坑记录,你可以直接对着排查。

现象 1:DS18B20 温度始终显示 85,修改延时也没用。原因是传感器根本没有被复位成功过。解决:用万用表量 DQ 引脚电压,正常空闲时应该被上拉到高电平,约 4.8V 以上。如果量到的是低电平,检查 DQ 是否误接到了 GND,或者杜邦线内部断路。还有一种可能,DS18B20 的 VCC 接成了 3.3V,部分型号在低压下工作不稳定。

现象 2:OLED 屏幕亮但没有文字,背光亮,画面全黑。原因是 IIC 通信失败,可能地址错误或者 OLED 的 RST 引脚悬空。解决:先把 RST 引脚接到 VCC 或者单片机的某个 IO 口拉高,然后检查 IIC 地址宏,在抓不到波形的情况下,用示波器或者逻辑分析仪看 SDA 上有没有非 ACK 的反馈。更诡异的情况是 OLED 控制器在初始化时被反复复位,命令被丢弃,这种情况检查 OLED 的 RST 引脚有没有被别的代码误操作。

现象 3:时钟能走但偏慢,一天慢十几分钟。原因是 32.768KHz 晶振匹配电容不对。解决:DS1302 的晶振引脚下一般接 6pF 电容,不要使用直插晶振的长引脚,尽量剪短。还有模块上如果你用的是芯片内部晶振,不接外部晶振也没问题,但 K 级晶振和 C 级负载电容要匹配。给 DS1302 换一颗晶振是最快的验证法。

现象 4:屏幕内容闪烁或者有残影。原因是 OLED 刷新方式不对,或主循环太频繁,或每次清屏后立即重新绘制,导致人眼可见闪烁。解决:不要每次调用OLED_Clear(),改为只更新数字变化区域,例如把温度显示位置设置成局部刷新函数OLED_ShowNum,只重绘那 16x24 像素区域,不要全部刷新。或者把刷新周期拉长到 100ms 以上,但更优雅的做法是画一个带背景色的矩形覆盖旧值再写新值。

现象 5:串口调试输出乱码。原因是波特率计算和实际晶振不匹配。解决:确认板子上晶振是 11.0592MHz 还是 12MHz。如果是 12MHz,把 TH1 改成 0xF3 左右(对应 9600 会有 2% 误差),不行就直接改用 STC 下载软件送的“串口助手”里的自动波特率功能,先发送 0xAA 同步。读取这一串数据时,把温度原始数据用十六进制打印,比较明确。

5. 二次开发的改造思路:显示升级、数据记录、低功耗化

5.1 显示界面改造:从文本模式到图形化仪表盘

原项目默认是纯文本模式,温度和时间以字符串形式显示,改造成图形界面并不难。SSD1306 支持在显存里直接画点,可以通过画矩形框、填充区块、画进度条等方式做仪表效果。常见的设计是上半屏用大字显示温度,下半屏用进度条显示温度区间(比如 0~50 度对应到 128 像素的横向进度条),时间用小字号放在右上角。实现上只需要扩展几个基础画图函数:

void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { // 使用逐点插值方式,计算斜率后逐像素写显存 } void OLED_FillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t mode) { uint8_t i, j; for (i = y; i < y + h; i++) { for (j = x; j < x + w; j++) { OLED_DrawPoint(j, i, mode); } } }

实际效果类似实时仪表盘,温度的数值变化就变成了进度条长度的变化。这里建议在OLED_FillRect里先判断区域是否越界,尤其是 x 坐标可能为负数的情况。因为进度条随着温度变化可能会出现负数坐标,如果不做边界裁切,写入的位置会错乱到其他页。

界面改造的另外一点是增加状态切换动画,比如按下按键时用OLED_DisplayOff()和OLED_DisplayOn()实现屏幕进入待机,也可以做成短按切换显示模式(只看时间/只看温度/同时显示)。切换动画不要做得太复杂,SSD1306 刷新率本身只有几十帧,动画太频繁反而造成闪烁。

5.2 扩展数据记录功能:把温度历史画成曲线

DS18B20 的温度读取频率并不高,每次转换要 750ms 左右,这反而适合做定时采样记录。改造思路是让主循环每 10 秒读一次温度,存到一个环形缓冲区里,然后在屏幕上绘制反映最近 1 小时温度变化的小型折线图。OLED 只有 128x64 分辨率,水平方向能画 128 个点,可以画成一小时内的 120 个采样点,每点间隔 5 秒。实现折线图的核心是维护一个与温度对应的 Y 坐标数组:

#define TEMP_HISTORY_SIZE 120 uint8_t temp_history[TEMP_HISTORY_SIZE]; // 存换算后的温度值 uint8_t hist_index = 0; void UpdateHistory(uint8_t current_temp) { if (hist_index < TEMP_HISTORY_SIZE) { temp_history[hist_index++] = current_temp; } else { // 循环覆盖最旧的数据点 memmove(&temp_history[0], &temp_history[1], TEMP_HISTORY_SIZE - 1); temp_history[TEMP_HISTORY_SIZE - 1] = current_temp; } }

注意memmove移动数组需要包含string.h,在 51 环境下这会增加编译体积,但一般 Keil 默认就支持。这个函数完成的是数组整体移位,不是内存波动。绘制折线图时,把每个历史点换算成屏幕 Y 坐标后,依次连接相邻点。连接两个相邻点用的是前面提到的OLED_DrawLine函数,如果相邻点对应的 Y 坐标差距超过 1 像素,中间用补点的方式画出竖线。这部分是纯显存操作,不存在复杂的算法坑,但要留意:显示区域只有 64 行高,如果温度超过 50 度,Y 坐标会直接超出范围,必须在换算时做钳位:

y = 60 - (temp * 60) / 50; // 把 0~50 度映射到 0~60 行 if (y < 0) y = 0; // 超出屏幕上边界直接截断

钳位是这类图形显示里最容易忽略的步骤,温度异常偏高时,曲线会反向画到屏幕顶部和状态栏重叠,看上去像是屏幕上多了一根乱线。加了这个判断后无论如何数据不会越界。

5.3 低功耗版本:电池供电的改造要点

原项目接 USB 供电,整机功耗折算下来大概在 50~80mA。想改成纽扣电池或者锂电池供电,最大的问题在 OLED 和降压电路上。SSD1306 待机时可执行OLED_DisplayOff()但显存内容不变,待机电流能降到几十微安。单片机则可以用 STC 的掉电模式,定时唤醒读一次温度并刷新屏幕,然后继续睡眠。一个可行的电池版结构是:3.7V 锂电池经过 HT7333 稳压到 3.3V 给单片机和传感器供电;DS1302 走独立电池,这样主机关机也能保持时间;OLED 的对比度在电池模式下需要调低,原初始化命令里 0x81 对比度可以改成 0x40,屏幕暗一点但电池能多用一倍时间。

唤醒逻辑如果用 STC 单片机内部定时器或者外部 32.768KHz 晶振的定时唤醒,可以做成一个每 30 秒唤醒一次、点亮屏幕 3 秒后关闭的“息屏显示”模式。有人会想用 DS1302 闹钟功能来做定时唤醒,思路是可以的,但 51 单片机的掉电唤醒需要特定的中断引脚支持,需要查看具体型号的数据手册。这里有一个经验:掉电模式用外部中断唤醒最稳定,按下按键立刻唤醒屏幕,比定时唤醒简单,也不用额外占用 DS1302 的闹钟寄存器。成本角度讲,电池供电加上 OLED 模块仍然比同类的 温湿度墨水屏便宜很多,作为 DIY 项目已经很划算。

5.4 从原项目到自己的框架:代码组织方式的改进

原项目的所有代码往往放在几个大文件里,函数之间直接互相调用,改起来一开始还行,后面扩展曲线、新增设置页时,全局变量满天飞。建议自己重构的时候把公共部分抽出来,比如:

  • bsp_oled.c / bsp_oled.h:所有屏幕底层驱动和画图接口
  • bsp_ds18b20.c / bsp_ds18b20.h:温度传感读时序
  • bsp_ds1302.c / bsp_ds1302.h:时钟芯片驱动
  • app_main.c:业务逻辑,显示数据编排,按键状态机

这样后续想换用 0.96 寸彩色 OLED 或者换用 DHT11 温湿度传感器,只需要改对应驱动文件,主界面逻辑不用动。头文件里用宏定义做接口,比如温度读取函数名称GetTemperature()在ds18b20.h里声明,实际在bsp_ds18b20.c中实现。如果想更换传感器,只需要修改底层函数实现,而上层的app_main.c中即使调用了同一个函数名,替换链接库即可。我从这次拆解过程中得到的最大习惯是:每接一个外设,先从数据手册提取它的最小操作时序,再写一个独立的测试小工程,点亮了才接进总工程。从那以后我每次做多外设项目都强制先走一遍这个流程,把每个模块单独验证可靠,再整机联调,很少再遇到画面乱跳、数据不对这种玄学层面的问题。希望这套拆机思路帮到你,尤其在你准备把手头的开发板和传感器拼成真正有用的东西时。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询