ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP32驱动0.96寸OLED屏幕:SSD1306接线与Arduino显示实战

ESP32驱动0.96寸OLED屏幕:SSD1306接线与Arduino显示实战 1. 项目概述与整体思路1.1 为什么给ESP32配一块OLED屏幕调ESP32的板子前期最痛苦的一件事就是“看不见”。串口打印虽然能用但每次想看数据都得插着USB线开着串口监视器日志滚动起来眼睛跟不上。更别说做到一半想脱离电脑跑个独立小设备状态显示全凭猜——LED灯闪几下闪烁节奏还得自己去查代码才能回忆起来。这种时候给ESP32配一块0.96寸的OLED显示屏几乎是最低成本换来最大方便的做法。这块屏体积只有指甲盖大小却能把关键信息直接摆在眼前传感器读数、IP地址、WiFi连接状态、按钮触发结果、程序运行到哪一步了全部一目了然。调试的时候不用再开串口做成品的时候还能当交互界面。0.96寸OLED模块市面上零售价也就十几块到二十几块散件拿过来四根线就能接对零基础选手非常友好。这篇文章面向的读者是那种“刚拿到ESP32板子、还没写过几行代码、想做出第一个带屏幕的东西”的人。跟着步骤走你可以在一两个小时之内点亮屏幕然后在上面显示文字、数字、图形再进阶到汉字、传感器数据、菜单切换这些实际玩法。我会把从硬件接线、开发环境、代码实现到常见坑点全部讲一遍全是实际跑过之后沉淀下来的经验。1.2 0.96寸OLED屏幕的核心规格很多新手第一次看到OLED模块容易把它跟LCD液晶屏搞混。其实两者的显示原理完全不同OLED是自发光器件每个像素点自己发光所以它不需要背光黑色区域是真正“全黑”的对比度非常高。而LCD需要背光板黑色其实是压暗的灰色。这一点直接决定了两者在功耗和观感上的差异——OLED模块正常显示时电流也就十几毫安到二十几毫安工作电压3.3V就能跑跟ESP32是绝配。市面上最常见的0.96寸OLED模块分辨率是128x64像素意思是横向128个点、纵向64个点。控制芯片绝大多数是SSD1306这是一个非常成熟的驱动IC几乎所有主流库都对它有完善支持。模块上会引出四个引脚VCC、GND、SCL、SDA对应IIC通信接口。有些模块还带有复位引脚RESET和命令/数据选择引脚DC那是SPI版本才会用到的。买的时候认准“IIC接口”或“I2C”字样的版本四根线就够用了。这块屏幕画一个字符默认字号下大概是6x8像素一屏满打满算能排21个英文字符乘8行显示信息量不算大但显示关键状态完全够用。1.3 方案选型IIC接口和SPI接口怎么选OLED模块常见的通信接口有IIC和SPI两种。IIC只需要两根信号线SCL时钟、SDA数据加上电源和地总共四根线接法简单不容易错。缺点是传输速率相对慢一些刷新率低做动画会有肉眼可见的卡顿。SPI则需要五根以上的线SCLK、MOSI、DC、CS、RESET接线繁琐还需要占用更多GPIO但刷新速度快很多适合做图形动画、波形绘制这类对刷新率要求高的场景。对零基础入门来说我的建议非常明确选IIC版本。理由第一是接线少四条线插上就能用新手不容易接错第二是示例代码多搜索“ESP32 OLED”出来的教程十有八九都是IIC版本第三是日常显示文字数字IIC的刷新率完全够用你不是拿它看电影。等你玩熟了再考虑SPI版本追求更高刷新率也不迟。这里还要提醒一个容易忽略的点IIC和SPI版本的模块价格几乎一样但引脚定义不同买的时候一定看清商品标题里写的是“IIC接口”还是“SPI接口”别等拿到手才发现针脚对不上。我的第一块OLED就是因为没仔细看买成了SPI版本结果多买了一根杜邦线不说代码也得换成SPI模式的例程凭空多绕了半小时。2. 开发环境搭建与硬件接线2.1 Arduino IDE安装ESP32支持包驱动OLED这块屏幕我推荐用Arduino IDE来写代码主要原因是生态成熟、库丰富、社区例子多而且对新手来说门槛最低。如果你完全从零开始先到官网下载Arduino IDE装好后打开“文件”菜单里的“首选项”在“附加开发板管理器网址”一栏填入ESP32的官方JSON地址https://espressif.github.io/arduino-esp32/package_esp32_index.json填完保存后打开“工具”菜单进入“开发板”下的“开发板管理器”搜索“esp32”找到“esp32 by Espressif Systems”点击安装。这一步会下载一份不小的文件耗时取决于网络情况慢的时候可能要十几分钟。装完之后在“工具”菜单的“开发板”列表里就能看到ESP32系列的各种板型了常用的比如“ESP32 Dev Module”“NodeMCU-32S”都可以选。新手在这一步最容易遇到的问题是安装过程中提示“Timeout”或者下载失败网络环境不稳定的时候尤其常见。解决办法有两个方向一是换个时间段重试或者把网络从WiFi切换到手机热点很多时候热点更稳定二是用脚本下载离线安装包手动解压到Arduino的硬件目录里虽然操作稍复杂但一劳永逸。目前最新版的Arduino IDE在安装ESP32支持包时已经比旧版顺畅不少大概率重试几次就能成功。2.2 引脚接线与IIC通信原理接线是整个项目里最简单但也最容易被忽视的一步。ESP32开发板上通常有多组IIC引脚可用但默认最常用的是GPIO21作为SDA数据线、GPIO22作为SCL时钟线。如果你用的是常见的ESP32 DevKit开发板直接在板子两侧排针上找到标注为“21”“22”的引脚然后按下面对照表接线OLED模块引脚ESP32开发板引脚VCC3V3GNDGNDSCLGPIO22SDAGPIO21有些开发板会对SDA和SCL标注成D21、D22注意对应的就是GPIO编号。接好后上电时OLED屏幕会闪一下白屏这是正常的说明屏已经通电了。如果板上没有标注也可以查你手里那块开发板的引脚图确认不同厂家的丝印可能不一样但GPIO编号是统一的。IIC通信本身原理不复杂打个比方SCL是时钟线负责打节拍SDA是数据线负责在节拍下一个个位地传数据。总线上可以挂多个设备每个设备有独立的7位地址主机发起通信时先发送要访问的设备地址只有地址匹配的设备才会回应。SSD1306这颗驱动芯片在硬件上最常见的地址是0x3C少数模块会做成0x3D后面写代码的时候如果找不到设备先把地址换成0x3D试试。2.3 安装SSD1306驱动库Arduino生态里驱动SSD1306最常见的是两套库一套是Adafruit出品的“Adafruit SSD1306”配合“Adafruit GFX Library”使用功能全面、绘图函数丰富适合绝大多数场景另一套是“U8g2”对中文显示支持更好、内置字体多适合需要显示汉字的项目。入门阶段建议先装Adafruit这套因为它的API更接近Arduino的习惯初学者理解起来不费劲。在Arduino IDE的库管理器里工具菜单或左侧边栏进入搜索“Adafruit SSD1306”找到后点击安装。安装过程中它会提示需要安装依赖库“Adafruit GFX Library”和“Adafruit BusIO”选择安装所有依赖即可。同样如果你后面想用U8g2直接在库管理器里搜索“U8g2”安装它是一个单体库没有额外依赖。装完库之后建议先做一个最简单的IIC扫描测试确认ESP32能发现屏幕。下面这段代码会扫描IIC总线上的所有设备地址并把结果显示在串口监视器里#include Wire.h void setup() { Serial.begin(115200); Wire.begin(); // 默认使用GPIO21(SDA)和GPIO22(SCL) Serial.println(I2C Scanner); } void loop() { byte error, address; int nDevices 0; for (address 1; address 127; address) { Wire.beginTransmission(address); error Wire.endTransmission(); if (error 0) { Serial.print(I2C device found at address 0x); if (address 16) Serial.print(0); Serial.println(address, HEX); nDevices; } } if (nDevices 0) Serial.println(No I2C devices found); delay(2000); }打开串口监视器波特率设为115200如果能看到“I2C device found at address 0x3C”说明硬件接线和IIC通信链路都正常可以进入下一步写显示代码了。如果没扫描到设备先别急着怀疑库多半是接线问题VCC和GND接反了吗SCL和SDA是不是插反了逐一排查。3. 点亮屏幕从基础代码到核心实现3.1 第一个例程显示固定的英文字符在确认IIC扫描到设备之后就可以跑第一个真正让屏幕显示内容的程序。Adafruit SSD1306库自带的示例代码可以直接用打开“文件”菜单进入“示例”找到“Adafruit SSD1306”选择“ssd1306_128x64_i2c”即可。示例代码里会有类似下面这一段核心初始化逻辑#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, OLED_RESET); void setup() { if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F(SSD1306 allocation failed)); for (;;); } display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println(Hello OLED); display.display(); } void loop() { }这一段代码有几点值得逐行解释一下。OLED_RESET设置为-1表示不使用复位引脚因为IIC版本的模块通常不接RESET引脚这个值告诉库我们靠软件初始化就行。display.begin(SSD1306_SWITCHCAPVCC, 0x3C)里面第一个参数是供电方式0.96寸模块用默认的SSD1306_SWITCHCAPVCC即可第二个参数0x3C就是前面扫描到的IIC设备地址。显示文字的流程分两步先调用clearDisplay()清空显存然后通过setCursor、println等函数把内容“画”进内存缓冲区最后必须调用display()才会真正把缓冲区内容刷到屏幕上。新手常犯的错就是写完println之后忘了调用display()结果屏幕一直黑着误以为是硬件坏了。记住这个流程清屏、画内容、刷新三步缺一不可。3.2 用Adafruit GFX库绘制图形和变量数据Adafruit GFX库提供了丰富的绘图函数掌握这些基本函数之后屏幕上想画什么基本都能实现。最常用的有这么几个drawPixel(x, y, color)画点drawLine(x0, y0, x1, y1, color)画线drawRect(x, y, w, h, color)画矩形边框fillRect画实心矩形drawCircle画圆drawTriangle画三角形。坐标原点在屏幕左上角x轴向右增加y轴向下增加。对于显示动态数据最核心的就是setCursor(x, y)配合println()的组合。比如你想实时显示温度和湿度代码思路大概是这样的display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.print(Temp: ); display.print(temperature); display.println( C); display.setCursor(0, 16); display.print(Humi: ); display.print(humidity); display.println( %); display.display();有一点要注意Adafruit GFX库在换行时会自动处理但如果你在屏幕上反复刷新数字数字长度变化时末尾可能会残留上一轮的字符。比如上一轮温度是25.60这一轮变成9.80那25.60里残留的“6”可能会残留在屏幕上。解决办法是在刷新动态数字之前先用fillRect把这块区域擦掉或者显示固定位数比如用print(9.8, 1)强制保留一位小数。这个细节看似不起眼但实际做项目时几乎都会遇到。另外值得说明的是这块屏幕的显存是1KB128x64像素1位色深128*64/81024字节Adafruit库在初始化时会在ESP32的内存里分配这块缓冲区。ESP32有足够的内存完全不用担心但如果你用的是一块小内存的ATmega328P开发板就要留意内存溢出问题ESP32则完全不需要操心。3.3 显示中文取模与U8g2方案英文字符显示简单但实际项目中总要显示中文标题或提示信息比如“温度”“湿度”“状态”。SSD1306内部没有中文字库而Adafruit GFX库本身只内置了ASCII字体所以直接显示中文是不行的。解决思路有两个一是用取模软件把汉字变成位图数据再通过drawBitmap画出来二是换用U8g2库它内置了多种中文字库可以直接输出中文。先说说取模方案。常用的取模软件有PCtoLCD2002、Img2Lcd等。操作步骤大致是新建一个16x16像素的画布输入你要显示的汉字设置取模方式为“列行式”或“逐行式”生成十六进制数组。然后把数组复制到代码里调用display.drawBitmap(x, y, myBitmap, 16, 16, SSD1306_WHITE)就能显示出来。这个方法灵活但每个字都要手动取模、手动放数组十几个汉字下来很繁琐。更推荐用U8g2库来显示中文。U8g2安装后初始化方式略有不同常用的是软件IIC模式#include U8g2lib.h #ifdef U8X8_HAVE_HW_SPI #include SPI.h #endif #ifdef U8X8_HAVE_HW_I2C #include Wire.h #endif U8G2_SSD1306_128X64_NONAME_1_4W_HW_I2C u8g2(U8G2_R0, /* clock*/ SCL, /* data*/ SDA, /* reset*/ U8X8_PIN_NONE); void setup() { u8g2.begin(); u8g2.enableUTF8Print(); } void loop() { u8g2.firstPage(); do { u8g2.setFont(u8g2_font_wqy12_t_gb2312); u8g2.setCursor(0, 20); u8g2.print(温度: 25.6 C); } while (u8g2.nextPage()); delay(1000); }这里核心是u8g2_font_wqy12_t_gb2312这个字体它内置了GB2312编码的中文字库enableUTF8Print()启用后u8g2.print(温度)就能直接输出中文。U8g2的刷新机制是firstPage()加nextPage()的页面循环头一次接触会觉得写法别扭实际上就是把要绘制的内容放在循环体里库会自动处理分页缓冲。用习惯之后其实比Adafruit库还顺手尤其适合中文项目。4. 进阶玩法让OLED真正实用起来4.1 做一个带菜单的交互界面很多实际项目不希望屏幕一直固定显示一个画面而是希望通过按键切换不同的页面。这一步做出来后屏幕上显示什么就完全由代码逻辑决定了项目的交互性一下子提升不少。思路也很简单用GPIO接一个到两个按钮读按钮状态修改一个全局变量pageIndex然后在loop里根据pageIndex的值来决定显示哪一页。举个例子定义一个状态变量0表示首页、1表示传感器页、2表示设置页。每次按键pageIndex加一超过最大值就归零。显示部分写成一个renderPage()函数内部用switch判断当前页并调用各自的绘制函数int pageIndex 0; const int buttonPin 0; void renderPage() { display.clearDisplay(); switch (pageIndex) { case 0: drawHomePage(); break; case 1: drawSensorPage(); break; case 2: drawInfoPage(); break; } display.display(); }按钮扫描要注意消抖。最简单的方法是在检测到按下之后delay(50)再读一次确认还是按下状态才执行切换逻辑。更专业的做法是用时间戳判断两次有效触发之间的间隔避免一次按压因为抖动导致页面连跳好几页。我自己的习惯是记录上一次按键处理的时间间隔小于200毫秒的直接忽略效果非常稳定。如果你还想让用户体验更好可以在页面顶部画一个进度条或者用反色高亮当前选中项比如用fillRect把当前选项的背景填充为白色、文字改为黑色就实现了“选中高亮”的效果。这些看起来很小的事情做完之后的成就感非常强。4.2 显示传感器数据结合温湿度项目的完整示例OLED最常见的实际用途之一就是当仪表盘显示传感器数据。这里用DHT11温湿度传感器作为例子完整演示“读传感器—处理数据—显示到OLED”的全流程。DHT11是一款很便宜的数字温湿度传感器单总线通信一个GPIO就能读数据。接线方面DHT11的DATA引脚接到ESP32的GPIO4VCC接3V3GND接GND。代码里使用DHT库在库管理器搜索“DHT sensor library”安装同时它会依赖Adafruit Unified Sensor库一并安装。核心代码如下#include DHT.h #include Adafruit_SSD1306.h #define DHTPIN 4 #define DHTTYPE DHT11 DHT dht(DHTPIN, DHTTYPE); Adafruit_SSD1306 display(128, 64, Wire, -1); void setup() { dht.begin(); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); display.setTextSize(2); } void loop() { float h dht.readHumidity(); float t dht.readTemperature(); display.clearDisplay(); display.setCursor(0, 0); display.print(Temp:); display.setCursor(0, 20); display.print(t); display.print( C); display.setCursor(0, 40); display.print(Humi:); display.setCursor(0, 60); display.print(h); display.print( %); display.display(); delay(2000); }DHT11读取有个特点每次读取间隔至少要1秒以上否则会返回无效数据所以loop里delay(2000)不仅是控制刷新率更是保证传感器有足够的时间完成测量。如果你用的是DHT22或SHT30读取间隔可以缩短但DHT11的2秒刷新对OLED显示来说已经足够流畅。如果你要做的是一个长期运行的温湿度监测设备我强烈建议在屏幕上同时显示更新时间并且把温度数据做一下简单的滤波处理比如取最近十次读数的平均值。因为DHT11本身精度有限读数波动有时达到±1度直接显示原始值会让人怀疑设备坏了。4.3 联网显示时间NTP校时的OLED时钟OLED显示时钟是很多人的入门练手项目但单纯用ESP32内部的时间来计时断电后就归零了每次上电都要重新设置。更好的做法是让ESP32连上WiFi通过NTP网络时间协议自动同步时间。这样只要设备联网屏幕上显示的就是准确时间不需要任何手动校准。核心库是time.hESP32的Arduino核心已经内置了NTP相关支持。联网后调用configTime(gmtOffset_sec, daylightOffset_sec, ntp.aliyun.com)配置NTP服务器然后调用getLocalTime(timeinfo)获取本地时间再依次读取tm_hour、tm_min、tm_sec显示到屏幕上。#include WiFi.h #include time.h const char* ssid yourSSID; const char* password yourPASSWORD; void setup() { WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); } configTime(8 * 3600, 0, ntp.aliyun.com); } void loop() { struct tm timeinfo; if (getLocalTime(timeinfo)) { display.clearDisplay(); display.setTextSize(2); display.setCursor(20, 10); char timeStr[10]; sprintf(timeStr, %02d:%02d:%02d, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); display.print(timeStr); display.setCursor(20, 40); char dateStr[20]; sprintf(dateStr, %04d-%02d-%02d, timeinfo.tm_year 1900, timeinfo.tm_mon 1, timeinfo.tm_mday); display.print(dateStr); display.display(); } delay(1000); }时区设置这里有个小坑中国标准时间比UTC快8小时所以configTime的第一个参数要传8 * 3600也就是28800秒。如果你忽略了这一步屏幕上的时间会慢8个小时。daylightOffset_sec设为0即可因为我们没有夏令时的概念。另外注意tm_year是从1900年开始计算的所以要加1900才是实际年份tm_mon是从0开始的所以要加1。NTP时钟做好之后加上前面讲过的传感器显示就是一个相当完整的“桌面多功能信息屏”了时间、温湿度一屏同时展示。再配一个3D打印外壳放桌面上当摆件用非常拿得出手。5. 常见问题与排查技巧实录5.1 “加了OLED函数就卡死”的三个真实原因网上搜“oled 卡死”能找到一堆帖子很多人写了好几行显示代码一上电程序就跑不起来或者跑着跑着卡死在某个地方。我排查过不少类似问题绝大多数情况逃不过这三个原因。第一个是IIC地址不匹配。程序里写死了0x3C但你的模块实际地址是0x3Dbegin()函数进去之后一直在等待设备应答程序就“卡”在初始化那里。排查方法就是最开始说的IIC扫描程序先把实际地址扫出来再把代码里的地址改成扫描结果。这也提醒我们begin()是有返回值的写成if(!display.begin(...))的时候可以加上串口打印快速定位问题。第二个是初始化顺序的问题。有些新手会先创建Adafruit_SSD1306对象然后在setup()里先跑WiFi连接、传感器读取最后才初始化OLED。WiFi连接本身是个阻塞操作如果连不上可能一直重试屏幕自然一直没有反应看起来就像“加了OLED卡死”。实际上不是OLED卡死的是前面的代码把程序堵住了。建议的写法是先初始化OLED显示一个启动画面然后再去执行WiFi连接等耗时操作这样至少屏幕上能看到状态提示。第三个是内存分配失败。display.begin()内部需要为显存缓冲区分配内存如果在这份代码里同时启用了太多功能比如蓝牙、WiFi、SD卡等都可能因为内存不足导致分配失败返回false程序随后进入for(;;)死循环。解决办法是确保代码里不重复初始化相同功能的库尤其是Wire库不要同时被两个库分别初始化。5.2 屏幕完全不亮或白屏的排查流程遇到屏幕完全不亮先别急着怀疑模块坏了按顺序排查。第一步检查供电拿万用表量一下OLED的VCC和GND之间的电压正常应该在3.3V左右。如果只有零点几伏大概率是接线松了或者接到了错误的引脚上。第二步检查信号线和数据线是否接反SCL和SDA对调一下试试这个错误的发生率非常高因为很多新手只注意颜色对应没注意引脚名称。第三步再跑一次IIC扫描程序。如果扫描不到设备但接线看起来没问题尝试换一根杜邦线或者把模块插到面包板的另一组孔位。杜邦线质量差也会导致信号不稳定接触不良是很常见的事。如果IIC扫描到了设备但屏幕依然全黑试着在初始化后加一行display.drawPixel(0, 0, SSD1306_WHITE); display.display();看到左上角出现一个白点说明驱动是正常的只是你后面绘制的内容坐标或者设置有问题。白屏还有一种可能是复位引脚的问题。虽然IIC版本不强制接RESET引脚但个别模块的复位引脚悬空时电平不稳定会导致芯片一直处于复位状态。这时候可以尝试在初始化代码前用普通GPIO手动拉一次低电平再拉高也可以用杜邦线把RESET引脚接到ESP32的一个GPIO上然后调用pinMode和digitalWrite手动控制复位时序大多数情况能解决问题。5.3 显示内容闪烁、残影的应对思路OLED屏幕本身响应速度快几乎不存在残影如果你看到屏幕上内容有“鬼影”或者闪烁问题基本出在刷新逻辑上。最常见的原因是刷新频率过低且没有做清屏处理比如你更新半个屏幕的内容而没有把不显示的部分清除干净新旧内容叠加就会看起来像有残影。我的建议是如果你用Adafruit库每次刷新前先clearDisplay()再重新绘制。虽然会多花一点时间但对128x64的分辨率来说这点开销完全可以忽略。如果你觉得闪烁严重还可以考虑使用U8g2库的页面缓冲模式就是firstPage()和nextPage()它内部会做双缓冲处理显示效果比Adafruit库默认方式更稳。另外还有一种进阶做法不整屏刷新只更新变化的部分。比如时钟的秒数变化你可以在固定区域先fillRect擦掉旧数字再绘制新数字其余部分保持不变。这样不仅省电而且局部刷新看起来更干净利落。OLED还有一个特性就是刷新越频繁越耗电对电池供电的项目来说减少整屏刷新次数能明显延长续航。5.4 快速排查对照表为了方便以后遇到问题能快速定位我把常见症状和对应的解决方向整理成一张表贴在这里方便查阅现象可能原因排查与解决屏幕完全不亮供电异常、接线错误量VCC/GND电压核对引脚定义扫描不到IIC设备SCL/SDA接反、杜邦线接触不良交换信号线换线重插初始化一直失败IIC地址错误、模块RESET悬空用扫描程序确认地址尝试手动复位显示内容乱码字体设置错误、库版本冲突检查setTextSize更新或卸载重装库显示有残影未清屏、局部刷新逻辑不对每次刷新前clearDisplay()数字变化后残余旧数字新数字比旧数字短固定显示位数或先擦除该区域程序启动后卡死内存不足、WiFi阻塞先初始化OLED减少功能堆叠中文字符显示方框字体库不支持、UTF8未启用用U8g2并选用GB2312字体排查问题最忌讳的就是一次改多个变量。我的习惯是每次只改动一个地方比如先换线再跑扫描、改完地址再看效果这样出了问题能准确知道是哪一步引入的。折腾硬件本身就是学习的一部分多踩几次坑之后你对IIC、对OLED的理解会扎实很多。最后再分享一个我个人的经验拿到一块新的OLED模块别急着往项目里塞先单独用一块主板、一个扫描例程、一段显示例程把它跑通确认模块本身没问题再合并到项目里去。这样每一步的边界都很清晰出了问题能迅速定位。很多人喜欢一次把传感器、屏幕、WiFi全部接好再写代码结果一上电全乱套根本不知道是哪个环节出了问题。有了这块小小的OLED显示屏ESP32就像“长出了眼睛”你写的每一行代码、采集到的每一个数据都能实实在在地看见这种正反馈对新手时期来说特别重要。
返回列表