Arduino温度传感器数据可视化:LM35与ST7735彩屏动态折线图实现

📅 2026/7/28 9:26:21
Arduino温度传感器数据可视化:LM35与ST7735彩屏动态折线图实现
1. 项目缘起从数字到图形的温度感知升级最近在整理工作室的旧项目时翻出了一个经典的LM35温度传感器和一块吃灰已久的ATFC043彩色TFT屏幕。相信很多玩Arduino的朋友手头都有这两样东西LM35以其线性输出、无需额外校准的特性成为入门模拟量读取的首选而ATFC043这块1.44寸的彩屏因为驱动简单、价格亲民也是很多显示项目的常客。大多数教程止步于在串口监视器里打印出“Temperature: 25.6 C”这样的字符或者用LCD1602显示几行数字。这当然没问题但总觉得少了点直观性。温度是连续变化的用一串跳动的数字去感知它远不如一条实时绘制的曲线来得生动和直接。于是我萌生了一个想法能不能让ATFC043这块小彩屏不再只是静态地显示数字而是变身成一个迷你的温度趋势图显示器把LM35采集到的温度值以折线图的形式动态地画在屏幕上实时反映温度的变化过程。这个想法听起来简单但实操起来会涉及到模拟信号采集的稳定性处理、屏幕图形库的高效使用、以及如何在一个资源有限的Arduino Uno上实现动态绘图而不卡顿等一系列问题。这正是本次项目要解决的核心将线性的温度数据转化为直观的图形化界面打造一个极简的硬件温度记录仪。无论你是想监控机箱温度、观察室内外温差还是单纯想学习如何用Arduino驱动彩屏进行绘图这个项目都能给你一套完整的、可复现的解决方案。2. 核心器件选型与电路搭接为什么是它们在开始写代码之前我们先得把硬件通路打通。硬件的稳定是整个项目的基石这里每一个器件的选择和连接方式都直接影响到后续数据的准确性和程序的稳定性。2.1 LM35模拟温度传感器的“老实人”LM35之所以经典在于它的“省心”。它输出电压与摄氏温度呈线性关系比例系数为10mV/°C。这意味着在0°C时输出0mV25°C时输出250mV。它不需要像热敏电阻NTC那样进行复杂的对数计算和分压电阻匹配也无需像DS18B20那样处理单总线协议。对于Arduino的初学者来说LM35几乎是最友好的温度传感入门器件。注意LM35有三个引脚面对扁平一面从左至右分别是VCC、Vout、GND。常见的TO-92封装工作电压范围很宽4V到30V都可以我们直接用Arduino的5V供电即可。连接上将LM35的Vout引脚连接到Arduino Uno的任何一个模拟输入引脚我习惯用A0。这里有一个关键细节LM35的输出电压范围是0mV到1550mV对应-55°C到150°C但常用范围是0°C-100°C即0-1000mV。而Arduino Uno的模拟输入引脚在默认的5V基准电压下其ADC模数转换器的输入范围是0-5V。直接将LM35的输出接入相当于只用了ADC量程5V5000mV的前五分之一理论分辨率会降低。但实测在室温范围内其变化约250mV-300mV对应的ADC值变化也有50-60个点对于趋势观察来说完全足够。如果你追求更高精度可以考虑使用Arduino的analogReference(INTERNAL)将基准电压切换到板载的1.1V这样ADC的输入范围变为0-1.1V分辨率会显著提高但需要重新计算转换公式并确保LM35的输出电压在任何情况下都不会超过1.1V即110°C以下。2.2 ATFC043彩屏SPI驱动的图形画布ATFC043屏幕的核心驱动芯片是ST7735S。这是一款通过SPI串行外设接口通信的控制器意味着我们只需要占用Arduino的少数几个数字引脚就能控制这块拥有128x128分辨率、65K色的屏幕。相比于并口屏SPI屏接线简单但刷新速度会慢一些。不过对于每秒更新几次的温度曲线来说SPI的速度绰绰有余。接线是第一个容易踩坑的地方。ATFC043模块的引脚定义可能因批次和卖家而异但核心引脚通常包括VCC/GND电源接5V和GND。SCL/SCKSPI时钟线接Arduino的13脚SCK。SDA/SDI/MOSISPI数据输入线主设备输出从设备输入接Arduino的11脚MOSI。RES/RST复位线接任意数字引脚如8用于硬件复位屏幕。DC/RS/A0数据/命令选择线接任意数字引脚如9。这个引脚至关重要它告诉屏幕控制器接下来发送的是命令如设置显示区域还是数据如像素颜色。CS/SS片选线接任意数字引脚如10。当有多个SPI设备时用此引脚选择要通信的设备。即使只有一个也必须正确连接并控制。BL/LED背光控制通常接一个限流电阻后接5V或通过一个三极管/引脚控制。为简化我们可以直接接5V让背光常亮。我的连接方案如下请务必对照你的屏幕引脚标签ATFC043 VCC - Arduino 5VATFC043 GND - Arduino GNDATFC043 SCL - Arduino 13 (SCK)ATFC043 SDA - Arduino 11 (MOSI)ATFC043 RES - Arduino 8ATFC043 DC - Arduino 9ATFC043 CS - Arduino 10ATFC043 BL - Arduino 5V (通过一个220欧姆电阻)2.3 整体电路与电源考量将LM35和ATFC043同时接上Arduino Uno。这里要留意电源噪声。LM35对电源电压的波动比较敏感因为它的输出直接与VCC相关。如果屏幕在刷新时产生较大的电流波动可能会通过共用的5V电源线干扰到LM35的模拟输出导致温度读数出现毛刺。一个简单的改进方法是给Arduino的5V输出端并接一个100uF的电解电容再并一个0.1uF的陶瓷电容用于滤除低频和高频噪声。更彻底的方案是使用一个低压差线性稳压器LDO如AMS1117-3.3单独为LM35提供3.3V的纯净电源同时将Arduino的模拟基准也设置为3.3VanalogReference(EXTERNAL)这样可以完全隔离数字电路的干扰。本次为求简洁我们先采用并电容的方案。3. 软件架构与库的选择站在巨人的肩膀上硬件连接好后软件层面我们需要解决两个问题1. 如何驱动ST7735S屏幕绘图2. 如何实现动态折线图的算法。3.1 摒弃“硬肝”拥抱Adafruit_ST7735库早期有些教程会教你通过SPI直接发送底层命令来初始化屏幕、画点、画线这个过程极其繁琐且容易出错。现在我们有更好的选择Adafruit_ST7735库以及它的好搭档Adafruit_GFX图形核心库。Adafruit_GFX库提供了一套丰富的图形绘制函数画点、线、矩形、圆、打印文字等而Adafruit_ST7735库则负责实现这些函数在ST7735S芯片上的具体驱动。安装方法很简单在Arduino IDE的库管理中搜索“Adafruit ST7735”并安装它会自动关联安装Adafruit_GFX库。这个库大大简化了我们的工作让我们可以像在电脑上调用绘图API一样在屏幕上操作。3.2 动态折线图算法的核心思路在128x128的像素画布上绘制折线图我们需要定义几个要素绘图区我们不能占满整个屏幕需要留出边距用于显示坐标轴、标题和当前数值。例如我们可以设定左上角坐标(10,10)右下角坐标(118, 118)为一个108x108像素的绘图区域。坐标映射我们需要将物理量温度值、时间映射到屏幕像素坐标。Y轴温度假设我们关注10°C到40°C这个室温范围。那么屏幕绘图区的底部y118对应10°C顶部y10对应40°C。映射公式为screenY 118 - (temperature - 10) * (108 / (40-10))。这样温度越高点在屏幕上的Y坐标越小越靠上。X轴时间我们不是实时时钟而是记录最近一段时间的数据点。我们可以定义一个数组tempHistory[100]用来存储最近100个温度读数。每次读取新温度后将其存入数组并将所有已有数据在X方向上“左移”一位。在屏幕上最右边的像素点代表最新的数据最左边代表最旧的数据。X坐标的映射就是简单的线性分布screenX 10 i * (108 / 99)其中i是数据在数组中的索引从0到99。动态绘制每次更新时我们不能清空整个屏幕重画所有图形效率低下且闪烁。更好的方法是只清除绘图区内的旧折线。根据更新后的tempHistory数组在绘图区内绘制新的折线。在固定位置更新显示当前温度值的文本。这个“清除旧线-绘制新线”的过程是动态绘图不闪烁的关键。如果直接fillScreen(ST77XX_BLACK)清全屏再画坐标轴和曲线闪烁会非常明显。我们需要一个更精细的“局部刷新”策略。4. 代码逐行解析与优化技巧下面我将结合代码片段详细解释如何实现上述思路并分享几个优化和避坑的技巧。4.1 库引入与引脚定义#include Adafruit_GFX.h #include Adafruit_ST7735.h #include SPI.h // 定义ATFC043屏幕的引脚 #define TFT_CS 10 #define TFT_RST 8 #define TFT_DC 9 // 初始化ST7735对象使用硬件SPI Adafruit_ST7735 tft Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); // 定义LM35连接的模拟引脚 const int tempPin A0; // 温度历史记录数组及索引 float tempHistory[100]; int historyIndex 0; bool historyFilled false; // 标记数组是否已被填满一次这里使用了硬件SPI通信速度最快。historyFilled标志位很重要它用于区分数组还未填满前100次循环和已填满的两种状态这关系到绘制折线时起点和终点的正确连接。4.2 初始化设置屏幕与坐标轴void setup() { Serial.begin(9600); // 初始化屏幕使用ST7735_BLACKTAB参数针对常见的1.44寸屏变体 tft.initR(INITR_BLACKTAB); tft.setRotation(3); // 根据你的屏幕方向调整3通常为竖屏USB口在下 tft.fillScreen(ST77XX_BLACK); // 绘制静态的坐标轴和标题 drawStaticGraphics(); // 初始化温度历史数组 for (int i 0; i 100; i) { tempHistory[i] 0.0; } } void drawStaticGraphics() { // 1. 绘制标题 tft.setTextColor(ST77XX_WHITE); tft.setTextSize(1); tft.setCursor(15, 2); tft.print(Temperature Monitor); // 2. 绘制Y轴温度轴刻度和标签 // 在绘图区左侧画一条竖线 tft.drawLine(10, 10, 10, 118, ST77XX_WHITE); // 绘制刻度线及标签10, 20, 30, 40°C tft.setCursor(2, 112); // 10°C 底部 tft.print(10C); tft.drawLine(8, 118, 12, 118, ST77XX_WHITE); // 短刻度线 // ... 类似地绘制20C, 30C, 40C的刻度线和标签 // 3. 绘制X轴时间轴 tft.drawLine(10, 118, 118, 118, ST77XX_WHITE); tft.setCursor(110, 122); tft.print(Now); // 4. 预留当前温度显示区域绘图区右上角 tft.fillRect(80, 2, 48, 16, ST77XX_BLACK); // 先清空一块区域 tft.setCursor(82, 4); tft.setTextColor(ST77XX_YELLOW); tft.print(--.- C); }INITR_BLACKTAB是初始化参数针对一种常见的ST7735S屏幕变体。如果你的屏幕初始化后颜色不对或偏移可以尝试INITR_GREENTAB或INITR_REDTAB。setRotation(3)设置了屏幕方向如果你发现图形是横着的可以尝试0,1,2,3这几个值。静态图形坐标轴、标题只在setup()中绘制一次这是避免闪烁的重要原则。4.3 主循环数据采集、更新与绘图loop()函数是核心它需要高效地完成三件事读取温度、更新历史数组、刷新图形。void loop() { // 1. 读取并计算温度 float currentTemp readTemperature(); // 2. 更新温度历史数组 tempHistory[historyIndex] currentTemp; historyIndex; if (historyIndex 100) { historyIndex 0; historyFilled true; // 数组第一次被填满 } // 3. 更新动态图形折线和当前值 updateDynamicGraphics(currentTemp); // 控制更新频率每秒2次足够观察温度变化 delay(500); } float readTemperature() { // 读取10次取平均值平滑数据 int sum 0; for (int i 0; i 10; i) { sum analogRead(tempPin); delay(1); // 短暂延时让ADC稳定 } int avgADC sum / 10; // 将ADC值转换为电压基准5V10位ADC float voltage avgADC * (5.0 / 1023.0); // LM35输出10mV/°C电压转换为温度 float temperature voltage * 100.0; // 可选串口输出用于调试 // Serial.print(Voltage: ); Serial.print(voltage); Serial.print( V, ); // Serial.print(Temperature: ); Serial.print(temperature); Serial.println( C); return temperature; }readTemperature()函数中的10次读取取平均是一个非常重要的技巧它能有效抑制单次读取可能遇到的随机噪声和干扰得到更稳定的温度值。delay(1)给Arduino的ADC一点稳定时间有助于提高精度。4.4 动态图形更新消除闪烁的关键updateDynamicGraphics函数是整个项目的精华也是性能瓶颈所在。void updateDynamicGraphics(float currentTemp) { // 1. 更新右上角当前温度显示 tft.fillRect(80, 2, 48, 16, ST77XX_BLACK); // 局部清空旧文本 tft.setCursor(82, 4); tft.setTextColor(ST77XX_YELLOW); tft.setTextSize(1); tft.print(currentTemp, 1); // 显示一位小数 tft.print( C); // 2. 清除上一帧的整个折线用背景色重绘画图区 // 更优方案只清除折线经过的区域但实现复杂。这里采用清空绘图区矩形。 tft.fillRect(11, 11, 107, 107, ST77XX_BLACK); // 注意边界不要擦掉坐标轴 // 3. 绘制新的温度折线 tft.startWrite(); // 开始连续写入提升绘制效率 int startPoint 0; // 如果数组还没填满从第1个有效数据开始画 if (!historyFilled historyIndex 1) { startPoint 1; } for (int i startPoint; i (historyFilled ? 100 : historyIndex); i) { int x0 mapToScreenX(i-1); int y0 mapToScreenY(tempHistory[i-1]); int x1 mapToScreenX(i); int y1 mapToScreenY(tempHistory[i]); // 使用抗锯齿效果更好的drawLine而不是多个drawPixel tft.drawLine(x0, y0, x1, y1, ST77XX_GREEN); } tft.endWrite(); // 结束连续写入 } // 将数据索引映射到屏幕X坐标 int mapToScreenX(int index) { int effectiveLength historyFilled ? 100 : historyIndex; if (effectiveLength 1) return 11; // 防止除以0 // 将索引映射到绘图区宽度内最新数据在最右边 // 注意当数组未填满时index就是时间顺序。填满后historyIndex指向最老的数据。 int drawIndex; if (historyFilled) { // 环形数组的映射将逻辑索引转换为绘制索引0最老99最新 drawIndex (index - historyIndex 100) % 100; } else { drawIndex index; } return 10 (drawIndex * 108 / (effectiveLength - 1)); } // 将温度值映射到屏幕Y坐标 int mapToScreenY(float temp) { // 将温度10-40°C映射到屏幕Y坐标118-10底部到顶部 // 限制温度在显示范围内 temp constrain(temp, 10.0, 40.0); return 118 - (int)((temp - 10.0) * (108.0 / 30.0)); }关键优化点解析tft.startWrite()和tft.endWrite()这是Adafruit_GFX库提供的性能优化函数。它们会暂时禁用SPI事务之间的片选CS信号切换将多条画线命令打包成一次SPI通信可以显著提升连续绘图的效率减少屏幕更新时的肉眼可见的延迟。环形数组与索引映射mapToScreenX函数中的逻辑是处理动态折线图的核心难点。当数组填满后我们使用了一个“环形缓冲区”。historyIndex始终指向下一个要写入的位置也就是最老的数据。为了在屏幕上正确地从左老到右新绘制我们需要将环形数组的逻辑索引重新计算为从0最老到99最新的连续绘制索引。这个计算drawIndex (index - historyIndex 100) % 100实现了这一点。局部刷新与全局清除的权衡代码中使用了tft.fillRect(11, 11, 107, 107, ST77XX_BLACK)来清除整个绘图区。这是最简单但非最优的方法因为它会重绘整个背景包括可能不需要更新的空白区域。更高效的方案是“只重绘折线变化的部分”例如记录上一帧折线的所有线段坐标然后用背景色重画这些旧线段再画新线段。但这需要更多的内存来存储上一帧的状态实现也更复杂。对于100个点、每秒2帧的应用当前方案在Arduino Uno上已足够流畅。如果你发现闪烁可以尝试减少历史数组的长度如50个点或者进一步降低刷新频率。5. 实测调试与常见问题排查将代码编译上传后你可能会遇到一些预期之外的情况。下面是我在实测中遇到的一些问题及解决方法。5.1 屏幕无显示或花屏检查接线这是最常见的问题。务必确认VCC和GND没有接反SPI线SCK, MOSI是否接在了Arduino Uno对应的13和11脚对于其他板子可能不同。RESET和DC引脚必须接并且要在代码中正确定义。检查初始化参数INITR_BLACKTAB不适用于所有屏幕。如果屏幕亮但显示乱码或颜色怪异尝试换成INITR_GREENTAB或INITR_144GREENTAB。也可以注释掉setRotation试试不同方向。电源问题ATFC043屏幕在全白高亮时功耗较大可能超过USB口或某些劣质电源的供电能力导致Arduino复位或屏幕工作不正常。尝试单独给屏幕的VCC引脚供电需共地或者降低屏幕亮度如果支持PWM控制背光。5.2 温度读数跳动剧烈软件滤波确保使用了readTemperature()函数中的多次采样平均法。可以增加采样次数比如20次但会增加每次读数的耗时。硬件滤波在LM35的输出引脚Vout和地GND之间并联一个0.1uF的陶瓷电容可以很好地滤除高频噪声。这是成本最低且最有效的硬件稳定措施。远离干扰源将LM35的连线远离屏幕的排线、Arduino的晶振等数字噪声源。5.3 图形更新缓慢或闪烁严重减少绘图操作确保静态元素坐标轴、标题只在setup()中绘制一次。检查updateDynamicGraphics中是否有多余的全屏清除fillScreen操作。优化绘图函数使用drawLine代替连续多个drawPixel。使用startWrite()/endWrite()包裹连续绘图命令。降低刷新率将loop()中的delay(500)增加到delay(1000)或更长牺牲实时性换取流畅度。缩减数据点将tempHistory数组大小从100减至50或30。点数越少需要绘制的线段越少速度越快。5.4 折线图显示逻辑错误线条错乱检查映射函数重点调试mapToScreenX和mapToScreenY函数。可以在串口监视器中打印出index,drawIndex,screenX,screenY的值观察映射是否符合预期X是否从左到右递增Y是否温度越高位置越高屏幕Y坐标值越小。验证环形缓冲区逻辑在数组填满后historyFilled为true手动计算几个位置的drawIndex看它是否正确地实现了“historyIndex指向最老数据historyIndex-1指向最新数据”的环形遍历。数组初始化确保在setup()中将tempHistory数组全部初始化为一个合理的值如当前室温而不是0。否则前100秒的曲线会从0°C开始画导致图形突然跳变。6. 项目进阶与扩展思路这个基础版本已经实现了一个可用的温度图形化显示器。在此基础上我们可以进行很多有趣的扩展增加温度上下限报警在代码中设定一个高温阈值如35°C和低温阈值如15°C。当温度超过阈值时改变折线颜色如变为红色或蓝色或者在屏幕角落显示一个警报图标甚至驱动一个蜂鸣器或LED。显示统计信息在屏幕空白处实时计算并显示最近一段时间内的最高温度、最低温度和平均温度。改变时间尺度目前是固定存储100个点每个点间隔500ms总共显示50秒的数据。你可以增加一个按钮切换时间尺度。例如按一下按钮变为每5秒记录一个点显示500秒8分多钟的趋势再按一下变为每1秒记录一个点。添加SD卡存储使用SD卡模块将温度数据连同时间戳需要DS3231等RTC模块保存到CSV文件中。这样你就可以将设备放在某个地方长期记录事后用电脑分析数据。无线传输与云端显示用ESP8266或ESP32替换Arduino Uno将温度数据通过Wi-Fi发送到MQTT服务器或HTTP API然后在手机App或网页上显示漂亮的图表。这样你就拥有了一个简单的物联网温度监测节点。美化界面Adafruit_GFX库支持绘制填充矩形、圆形和三角形。你可以用这些图形元素绘制更美观的仪表盘背景或者用不同颜色填充折线图下方的区域形成面积图。这个项目的价值不仅仅在于最终那个跳动的曲线图更在于从传感器数据读取、SPI设备驱动、到图形算法实现、再到性能优化和问题排查的完整流程。它把Arduino编程中几个关键的知识点串联了起来。当你成功看到屏幕上那条随着你手指温度而起伏的绿色曲线时那种把物理世界信号转化为可视化信息的成就感正是嵌入式开发的乐趣所在。