U8g2嵌入式显示库:从硬件抽象到性能优化的全攻略

📅 2026/7/29 8:33:17 👁️ 阅读次数 📝 编程学习
U8g2嵌入式显示库:从硬件抽象到性能优化的全攻略

1. 项目概述:为什么U8g2是嵌入式显示开发的“瑞士军刀”?

如果你正在玩ESP32、Arduino或者STM32,想在小小的OLED或者LCD屏幕上显示点东西,大概率会听到一个名字:U8g2。我第一次接触它是在一个温湿度监测项目上,当时手头只有一块0.96寸的SSD1306 OLED屏,网上搜了一圈例程,发现十个有八个都在用U8g2。折腾了几个晚上,从点亮屏幕到画出复杂的图表,我算是彻底被这个库的“全能”给折服了。它不像某些驱动库,只针对某一款特定控制器,U8g2几乎通吃了市面上你能见到的绝大多数单色和灰度显示屏,从常见的SSD1306、SH1106,到稍冷门的ST7920、UC1701,它都能驾驭。更重要的是,它提供了一套高度统一的API,让你学会一种写法,就能在几十种屏幕上跑起来,这种“一次学习,到处使用”的体验,对于需要快速原型开发的工程师和爱好者来说,价值巨大。

简单来说,U8g2是一个用于嵌入式系统的单色图形显示库。它的核心价值在于硬件抽象功能完备。它把底层繁琐的通信协议(I2C、SPI、8位/4位并行等)和不同控制器芯片的初始化序列、显存操作都封装了起来,给你一个干净、统一的画图接口。你不需要再去啃那些动辄几十页的数据手册,研究如何发送一个初始化命令,只需要关心“我想在这里画个圆,在那里写段字”。对于新手,它能极大降低入门门槛;对于老手,它能显著提升开发效率,把精力集中在应用逻辑而非底层驱动上。接下来,我们就把它拆开揉碎了,看看这把“瑞士军刀”到底怎么用,以及如何避开那些我踩过的坑。

2. 核心架构与设计思路拆解

2.1 理解U8g2的“双层”驱动模型

U8g2的架构设计得很巧妙,理解它有助于你后续选型和排错。它不是简单的一层代码,而是采用了“驱动层(Driver)” + “图形层(Graphics)”的双层模型。

驱动层是直接和硬件打交道的部分。这一层又细分为两个子层:

  1. 通信子层:负责处理物理连接,比如I2C的Wire库调用、SPI的transfer函数,或者软件模拟IO口的时序。U8g2内置了数十种通信过程的实现。
  2. 控制器子层:负责与具体的显示控制器芯片(如SSD1306)对话。它知道该芯片的初始化命令序列、显存结构(是页式、列式还是其他)、支持的分辨率等。U8g2为超过250种控制器提供了驱动。

当你选择一个构造函数,例如U8G2_SSD1306_128X64_NONAME_F_HW_I2C时,你实际上是在同时指定控制器(SSD1306)、分辨率(128x64)、变体(NONAME)、帧缓冲模式(F)和通信方式(HW_I2C)。库在内部会自动将对应的驱动层组件组合起来。

图形层是上层应用接口。它提供了一系列绘图函数,如drawPixel,drawLine,drawCircle,drawStr等。你的所有绘图操作,最终都会由图形层转化为对一块内存缓冲区(即“帧缓冲区”)的修改。这个缓冲区在内存中模拟了整个屏幕的像素状态。当你调用sendBuffer()函数时,驱动层才会将这块缓冲区里的数据,按照控制器要求的方式,一次性发送到实际的显示屏上。

注意:这里有一个关键选择——帧缓冲区模式。构造函数中的“F”、“1”、“2”就代表这个。

  • _F_(Full buffer):全缓冲模式。在RAM中开辟一个与屏幕总像素数对应的完整缓冲区。优点是绘图操作瞬时完成,最后sendBuffer()一次性更新屏幕,无闪烁。缺点是极其消耗内存。一个128x64的单色屏需要1024字节(1KB),对于只有2KB RAM的Arduino Uno来说,这占了半壁江山。
  • _1__2_(Page buffer):页缓冲模式。缓冲区只存储一“页”或几“页”高度的像素数据。绘图时,U8g2会计算哪些页受影响,只更新这些页对应的缓冲区部分,然后分多次发送。大幅节省内存(可能只需128字节),但复杂的全屏更新会导致多次通信,可能感觉稍慢或有轻微闪烁。这是资源紧张型MCU(如ATmega328P)的首选。

2.2 构造函数命名规则:如何从名字看懂一切

U8g2的构造函数名字看起来又长又复杂,但其实它是“自描述”的,遵循一个清晰的命名规则。读懂它,你就能瞬间知道这个对象适合什么硬件。规则如下:

U8G2_+控制器芯片+分辨率+变体+帧缓冲模式+通信接口

我们来拆解一个最常见的例子:U8G2_SSD1306_128X64_NONAME_F_HW_I2C

  • U8G2_: 库前缀。
  • SSD1306: 显示控制器芯片型号。
  • 128X64: 屏幕的像素分辨率,宽x高。
  • NONAME: 屏幕变体。常见的有NONAME(通用款)、ADA(Adafruit出品)、SEEED(Seeed Studio出品)等。不同变体的初始化参数可能略有差异,如果屏幕不亮,尝试换一个变体(如NONAME换成ADA)有时能奇迹般解决。
  • F: 帧缓冲模式。F代表全缓冲,12代表页缓冲。
  • HW_I2C: 通信接口。HW_I2C代表硬件I2C,使用MCU的硬件I2C外设。其他常见选项有:
    • SW_I2C: 软件模拟I2C,可以自定义任意两个IO口作为SCL和SDA。
    • _4W_HW_SPI: 4线硬件SPI(需要CS/DC/RESET引脚)。
    • _3W_SW_SPI: 3线软件模拟SPI(节省一根数据线,但速度慢)。

所以,看到这个名字,你就知道:这是一个针对SSD1306控制器、128x64分辨率、通用型号、使用全缓冲区并通过硬件I2C驱动的对象。如果你的屏幕是SH1106驱动的1.3寸OLED,用软件SPI连接,可能就会选择U8G2_SH1106_128X64_NONAME_1_4W_SW_SPI

3. 从零开始的完整配置与初始化流程

3.1 硬件连接与引脚定义

在写代码之前,正确的硬件连接是第一步。我们以最普遍的I2C接口0.96寸OLED(SSD1306)和SPI接口OLED为例。

对于I2C接口(最常用,接线简单):

  • SCL-> 开发板的I2C时钟引脚(Arduino Uno的A5, ESP32的GPIO22, 通常标为SCL)。
  • SDA-> 开发板的I2C数据引脚(Arduino Uno的A4, ESP32的GPIO21, 通常标为SDA)。
  • VCC-> 3.3V 或 5V(务必查看屏幕规格书,多数OLED是3.3V逻辑电平,接5V可能烧毁!)。
  • GND-> GND。

很多I2C OLED模块还带有地址选择焊盘。默认地址通常是0x3C,但也有是0x3D的。如果后续初始化不成功,这是首要排查点。

对于SPI接口(速度更快,引脚多):

  • SCK-> 开发板的SPI时钟引脚(如Arduino的13脚)。
  • SDA/MOSI-> 开发板的SPI主出从入引脚(如Arduino的11脚)。
  • CS/SS-> 片选引脚(可自定义,如引脚10)。
  • DC/A0-> 数据/命令选择引脚(可自定义,如引脚9)。
  • RES/RST-> 复位引脚(可自定义,如引脚8)。
  • VCC&GND-> 电源。

SPI接线时,DCRESET引脚非常重要且必须正确连接,它们控制了命令和数据的切换以及硬件复位。

3.2 软件初始化代码详解

连接好硬件后,我们开始编写代码。一个完整的U8g2初始化流程包含几个关键步骤。

// 示例:使用硬件I2C驱动SSD1306 128x64 OLED #include <U8g2lib.h> // 包含U8g2库 // 步骤1:根据你的硬件,选择正确的构造函数 // 这里我们创建了一个全缓冲、硬件I2C的对象 U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); void setup() { // 步骤2:初始化显示对象 u8g2.begin(); // 对于I2C,begin()会尝试与屏幕通信。如果失败,程序通常会卡住或屏幕无显示。 // 步骤3:(可选但推荐)设置字体 // U8g2内置了多种字体,需要先设置才能使用drawStr u8g2.setFont(u8g2_font_ncenB08_tr); // 设置一个8像素高的字体 u8g2.setFontMode(0); // 设置字体模式:0为覆盖模式(前景色绘制),1为异或模式 // 步骤4:(可选)开启反色显示 // u8g2.setDrawColor(2); // 颜色模式2为异或,可以实现闪烁效果 // 但更常用的是直接设置反色 u8g2.setFontRefHeightExtendedText(); u8g2.setDrawColor(1); u8g2.setFontPosTop(); } void loop() { // 绘图操作通常放在loop中,或由事件触发 u8g2.clearBuffer(); // 清除内部缓冲区,准备新一帧 u8g2.drawStr(0, 10, "Hello World!"); // 在坐标(0,10)处绘制字符串 u8g2.drawRFrame(5, 20, 50, 20, 3); // 画一个圆角矩形框 u8g2.sendBuffer(); // 将缓冲区内容发送到显示屏 delay(1000); }

关键点解析:

  • U8G2_R0: 这是旋转参数。R0代表不旋转,R1代表顺时针旋转90度,R2为180度,R3为270度。旋转是在驱动层处理的,设置后所有绘图坐标的坐标系都会相应改变。
  • U8X8_PIN_NONE: 表示我们没有连接硬件复位引脚(RESET)。如果你的模块有复位引脚并已连接,这里需要填入对应的Arduino引脚编号。使用硬件复位有时能解决启动时屏幕乱码的问题。
  • begin(): 这个函数至关重要。它执行了硬件初始化序列。对于I2C,它还会尝试进行总线扫描(如果启用)。如果屏幕没反应,首先检查begin()是否被执行,以及接线和地址。
  • clearBuffer()sendBuffer(): 这是U8g2绘图的核心循环。永远记住这个顺序clearBuffer()-> 一系列drawXxx()->sendBuffer()clearBuffer()只是清空内存中的缓冲区,直到sendBuffer()被调用,屏幕上才会看到变化。

4. 核心绘图API深度解析与实战技巧

U8g2的图形API非常丰富,我们将其分为几个核心类别来讲解。

4.1 基础图形绘制:点、线、框、圆

这些函数构成了图形界面的骨架。

// 设置绘制颜色(对于单色屏,1=点亮像素,0=熄灭像素) u8g2.setDrawColor(1); // 1. 点 u8g2.drawPixel(10, 10); // 在(10,10)画一个点 // 2. 线 u8g2.drawLine(0, 0, 127, 63); // 从(0,0)到(127,63)画对角线 u8g2.drawHLine(10, 20, 50); // 从(10,20)开始画一条50像素长的水平线 u8g2.drawVLine(30, 0, 40); // 从(30,0)开始画一条40像素长的垂直线 // 技巧:drawHLine和drawVLine比通用的drawLine效率更高,因为算法更简单。 // 3. 矩形 u8g2.drawFrame(5, 5, 30, 20); // 画空心矩形 (x, y, 宽, 高) u8g2.drawBox(40, 5, 30, 20); // 画实心矩形 u8g2.drawRFrame(5, 30, 40, 25, 5); // 画圆角空心矩形,最后一个参数是圆角半径 u8g2.drawRBox(50, 30, 40, 25, 7); // 画圆角实心矩形 // 4. 圆和椭圆 u8g2.drawCircle(64, 32, 20); // 画空心圆 (圆心x, 圆心y, 半径) u8g2.drawDisc(100, 32, 15); // 画实心圆 u8g2.drawEllipse(64, 55, 25, 10, U8G2_DRAW_ALL); // 画椭圆 (x, y, rx, ry, 选项) // 注意:圆和椭圆的绘制算法(中点圆算法)比画线更耗时,在低性能MCU上频繁调用可能影响帧率。

实操心得:

  • 坐标系统:原点(0,0)在屏幕的左上角。X轴向右递增,Y轴向下递增。这和很多数学坐标系不同,容易搞错。
  • 性能考量:在loop中,尽量减少全屏范围的图形重绘。例如,更新一个进度条,可以只重绘进度条变化的区域,而不是每次都clearBuffer()整个屏幕再重画所有元素。这能有效提升刷新速度,避免闪烁。

4.2 文本显示:字体管理与排版艺术

显示文本是嵌入式GUI最常用的功能,U8g2的字体系统强大但需要一些理解。

// 1. 设置字体(必须在绘制文字前设置) u8g2.setFont(u8g2_font_6x10_tr); // 使用6x10像素的字体 // 字体命名规则:u8g2_font_<字体名>_<像素高度><编码>。tr表示包含透明背景的常规字符集。 // 2. 获取字体信息,用于动态排版 uint8_t font_height = u8g2.getAscent() - u8g2.getDescent(); // 字体的像素高度 int16_t string_width = u8g2.getStrWidth("Hello"); // 获取字符串“Hello”的像素宽度 // 这是实现文本居中对齐的关键! // 3. 绘制字符串 u8g2.drawStr(0, 10, "Left Align"); // 在(0,10)绘制,基线对齐 // 实现居中对齐: char* text = "Center"; int16_t text_width = u8g2.getStrWidth(text); int16_t x_pos = (128 - text_width) / 2; // 假设屏幕宽度128 u8g2.drawStr(x_pos, 20, text); // 4. 绘制数值(需要先转换) int sensorValue = 1234; char buffer[10]; // 确保缓冲区足够大 sprintf(buffer, "%d", sensorValue); // 整数转字符串 // 或者使用更安全的snprintf snprintf(buffer, sizeof(buffer), "Temp:%.1fC", 23.5); u8g2.drawStr(0, 40, buffer);

字体选择的坑与技巧:

  • 内存消耗:U8g2的字体以位图形式存储,越大的字体消耗的Flash空间越大。一个复杂的汉字字体可能轻松占用几十KB。在资源紧张的AVR单片机(如Uno)上,务必谨慎选择字体,并使用PROGMEM关键字将不常用的字体存放到程序存储区(如果库支持)。
  • 内置字体与自定义字体:U8g2自带大量西文字体(在u8g2_fonts.c文件中),基本够用。如果需要显示中文,必须使用自定义字体。你需要使用U8g2官方提供的字体转换工具(bdfconv),将你的.ttf或.bdf字体文件转换成C数组,然后包含到项目中。这个过程稍显复杂,但网上有详细教程。
  • setFontPosTop()vssetFontPosBaseline():这是文本垂直对齐的关键。Baseline(基线)是西文字母(如“a”, “x”)对齐的线,而Top是字体最高点的对齐线。对于多行文本排版,使用setFontPosTop()并配合getAscent()计算行高会更直观、更不容易出错。我个人的习惯是在setup()里统一设为setFontPosTop()

4.3 位图与图标显示

在界面上显示Logo或小图标,位图功能必不可少。

// 1. 首先,你需要将图片转换为XBM格式或U8g2专用的位图数组。 // 可以使用在线工具(如https://github.com/olikraus/u8g2/tree/master/tools/font/bdfconv)或Image2Cpp等工具。 // 假设你有一个16x16像素的图标,转换后得到一个数组: static const unsigned char my_icon_bits[] U8X8_PROGMEM = { 0x00, 0x00, 0x06, 0x60, 0x09, 0x90, ... // 这里是位图数据 }; // 2. 绘制位图 // drawXBM(x, y, width, height, bitmap_array) u8g2.drawXBM(0, 0, 16, 16, my_icon_bits); // 在(0,0)绘制16x16图标 // 3. U8g2还支持绘制带透明色的位图(drawXBMP),但需要数据是PROGMEM存储的。

注意事项:

  • 存储空间:位图同样非常消耗Flash。一个128x64的全屏单色位图需要1KB。务必优化图标尺寸和数量。
  • 转换工具:确保转换工具输出的数组格式与U8g2的drawXBM函数要求一致。通常需要勾选“字节顺序”等选项,如果显示出来图片是乱的,大概率是字节顺序或位顺序错了。

4.4 高级功能:页面循环与用户界面元素

U8g2内置了一个简单的“页面循环”机制,非常适合制作多页菜单或仪表盘。

// 这是一个典型的U8g2用户界面回调函数结构 void drawUI() { // 这个函数会被多次调用,每次绘制一“页”或一部分 u8g2.setFont(u8g2_font_6x10_tr); u8g2.drawStr(0, 10, "Page 1: Sensor Data"); u8g2.drawStr(0, 25, "Temp: 25.6C"); // ... 绘制其他内容 } void loop() { u8g2.firstPage(); // 开始页面循环 do { drawUI(); // 在这个循环内调用你的绘图函数 } while (u8g2.nextPage()); // 自动处理分页发送 delay(2000); }

firstPage()/nextPage()机制详解:这个机制是U8g2用于页缓冲模式下优化绘制的核心。它的工作流程是:

  1. firstPage(): 初始化内部状态,准备开始一帧的绘制。
  2. do { ... } while (nextPage());: 在这个循环里,你的drawUI()函数会被执行至少一次
  3. nextPage(): 这个函数会判断当前绘制的“页”是否完成。如果使用全缓冲(_F_),它会在第一次调用nextPage()后就返回false,结束循环,因为所有内容已在内存中。如果使用页缓冲(_1_),它会根据你绘图函数中涉及到的Y坐标范围,智能地决定是否需要再次调用drawUI()来绘制下一“页”的内容,直到整个屏幕区域更新完毕。

它的最大优点是:你的绘图代码drawUI()只需要关心“画什么”,而不需要关心“怎么分页发送”。库会自动处理分页逻辑,在内存和速度之间取得平衡。对于复杂界面,务必使用此机制,而不是手动clearBuffer->draw->sendBuffer

5. 性能优化与内存管理实战

在资源受限的嵌入式环境中,优化U8g2的性能和内存使用是必须掌握的技能。

5.1 帧缓冲区模式的选择策略

这是最根本的优化决策。

  • 场景一:ATmega328P (Arduino Uno, 2KB RAM) + 128x64 OLED

    • 绝对不要用_F_(全缓冲)。1024字节的缓冲区会吃掉一半的RAM,极易导致程序崩溃或行为异常。
    • 必须使用_1_(页缓冲)。例如U8G2_SSD1306_128X64_NONAME_1_HW_I2C。缓冲区可能只有128字节,安全。
    • 绘图策略: 使用firstPage()/nextPage()循环。避免在loop中做大量浮点运算或字符串处理,这些操作应在进入页面循环前完成。
  • 场景二:ESP32 (520KB RAM) 或 STM32F103 (20KB RAM) + 128x64 OLED

    • 推荐使用_F_(全缓冲)。1KB的内存占用对它们来说微不足道。可以获得最流畅的无闪烁更新体验。
    • 优势: 你可以使用clearBuffer()sendBuffer()的简单模式。在需要高速动画(如游戏、快速更新的图表)时,性能更好。
  • 场景三:高分辨率屏幕(如320x240)

    • 即使是ESP32,全缓冲也需要320*240/8 = 9600字节,约9.4KB。这仍然可以接受,但如果你同时需要很多网络缓冲区或其他大数组,就需要权衡。
    • 一个折中方案是使用部分重绘。即使在全缓冲下,也不总是清空整个缓冲区。你可以只更新屏幕上发生变化的那一小块区域,然后调用updateDisplayArea(x, y, w, h)来只发送那一部分数据,这能极大提高刷新效率。

5.2 通信接口的优化选择

  • I2C vs SPI

    • I2C: 引脚少(2根),接线简单,但速度慢(标准模式100kbps,快速模式400kbps)。对于128x64的屏幕,全屏更新一次需要传输1KB数据,在100kbps下需要约80ms,帧率理论上限约12FPS。适合数据更新不频繁的场合(如传感器数据显示)。
    • SPI: 引脚多(通常4-5根),但速度快得多(通常可达10Mbps以上)。全屏更新可能只需1-2ms。如果你需要流畅的动画或快速刷新,SPI是唯一的选择。记得在构造函数中选择HW_SPI以利用硬件加速。
  • 硬件 vs 软件

    • 优先选择HW_I2CHW_SPI。它们由MCU的专用硬件处理,不占用CPU时间。
    • 只有在硬件引脚被占用或需要特殊引脚布局时,才考虑SW_I2CSW_SPI(软件模拟)。软件模拟会消耗大量CPU周期进行位操作,在高刷新率下可能导致系统卡顿。

5.3 绘图操作本身的优化

  • 减少drawStr调用: 字符串绘制是比较耗时的操作,尤其是使用大字体时。对于频繁更新的数字(如秒数),可以考虑:
    1. 只重绘变化的字符区域,而不是整个字符串区域。
    2. 将固定文本和变化数字分开绘制。固定文本只在初始化时画一次(或使用多缓冲技巧),循环中只更新数字部分。
  • 慎用复杂图形drawCircle,drawEllipse,drawTriangle等函数涉及浮点或复杂整数运算,比drawLine,drawBox,drawPixel慢得多。在性能关键的循环中,尽量使用基础图形组合。
  • 启用裁剪区域: 如果你只想更新屏幕的一小部分,可以使用setClipWindow(x0, y0, x1, y1)设置一个裁剪窗口。在此之后的所有绘图操作,只有落在窗口内的部分才会被实际绘制到缓冲区。这能有效避免不必要的像素计算和缓冲区写入。记得在完成局部更新后调用setMaxClipWindow()恢复全屏绘制

6. 典型问题排查与调试技巧实录

即使按照教程操作,屏幕不亮、显示乱码也是家常便饭。下面是我总结的排查清单。

6.1 屏幕完全无显示(背光可能亮,但无内容)

  1. 电源与电压:这是头号杀手!用万用表测量VCC和GND之间的电压。绝大多数OLED模块是3.3V逻辑电平,如果VCC接5V但模块上没有电平转换芯片,很可能已经损坏。同时,确保GND共地。
  2. I2C地址错误:在setup()里添加Serial.begin(115200);Wire.begin();,然后使用I2C扫描工具(Arduino IDE示例中有Wire库的Scanner示例)扫描地址。确认地址是0x3C还是0x3D,然后在构造函数中指定:u8g2.setI2CAddress(0x3C * 2);(注意,U8g2内部地址需要左移一位,所以0x3C对应设置0x78,但setI2CAddress可能已处理,最好查文档。更简单的方法是换一个地址正确的构造函数变体,或者检查模块上的地址选择电阻)。
  3. 构造函数选择错误:确认你的屏幕控制器型号(SSD1306? SH1106?)。买到的模块可能和卖家描述不符。尝试最常见的几种:SSD1306SH1106。同时尝试不同的变体,如NONAME换成ADA
  4. 复位引脚问题:如果你的模块有RESET引脚且已连接,确保在构造函数中传入了正确的引脚号,并且该引脚在begin()之前被设置为OUTPUT并拉高。如果没接,则使用U8X8_PIN_NONE。有时,在begin()前手动触发一下复位能解决问题:
    if (U8X8_PIN_NONE != reset_pin) { pinMode(reset_pin, OUTPUT); digitalWrite(reset_pin, LOW); delay(10); digitalWrite(reset_pin, HIGH); delay(10); } u8g2.begin();
  5. 库冲突或版本问题:确保你安装的是最新的U8g2库(通过Arduino库管理器)。有时系统中存在多个显示库(如Adafruit_SSD1306)可能会引起冲突。尝试一个最精简的测试程序。

6.2 屏幕有显示但内容错乱、花屏、闪烁

  1. 通信速度过快:对于长导线或质量一般的模块,过高的I2C或SPI速度可能导致数据错误。尝试降低速度。对于I2C,可以在begin()后调用:
    u8g2.setBusClock(100000); // 将I2C时钟设为100kHz(标准模式)
    对于软件SPI,可以调整U8X8_MSG_DELAY_NANO等宏定义(在库源码中),但比较复杂。
  2. 电源噪声:在VCC和GND之间并联一个100uF的电解电容和一个0.1uF的陶瓷电容,尽可能靠近屏幕模块的电源引脚,这能有效滤除电源噪声,解决随机花屏问题。
  3. 缓冲区溢出或内存损坏:如果你在页缓冲模式下,在firstPage()/nextPage()循环之外调用了绘图函数,或者你的绘图计算导致了数组越界,都可能破坏内存,导致随机花屏。仔细检查代码逻辑。
  4. 屏幕初始化参数不匹配:尝试不同的构造函数变体。NONAME,ADA,SEEED之间的初始化序列有细微差别。这是解决“显示偏移”、“屏幕只有一部分有内容”等问题的常见方法。
  5. SPI的DC和RESET引脚接错或未定义:这是SPI接口特有的问题。确保在构造函数中正确指定了DC和RESET引脚编号,并且在硬件上已连接。例如:
    U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);

6.3 编译时提示内存不足

  1. 字体和位图是主要元凶:检查你是否引入了过大的字体文件(尤其是中文字体)。使用avr-size工具(Arduino IDE编译后会输出)查看Flash使用情况。尝试移除不用的字体,或者使用更小的字体。
  2. 切换到页缓冲模式:如果正在使用全缓冲(_F_),切换到页缓冲(_1_)能立即释放大量RAM。
  3. 优化全局变量和数组:检查你的代码中是否有大型全局数组。考虑使用PROGMEM将它们存放到Flash中,或者使用动态内存分配(但在小内存MCU上需谨慎)。

调试时,串口打印是你的好朋友。在代码关键位置(如setup开始、begin之后、绘图循环中)加入Serial.println()语句,输出状态、变量值或简单的标记(如“>进入绘图循环”),可以帮助你快速定位程序卡在哪个环节。