
做嵌入式显示类的小项目应该都有过被OLED屏驱动折腾到没脾气的经历。手里一块小屏翻手册、看时序、初始化寄存器、写画点函数折腾几天还没能把一屏内容稳定显示出来。后来我开始用U8g2情况就完全不一样了——这个开源单色图形库帮我把底层驱动全部封装好初始化一行画图一行中英文、图形、曲线、动画全部靠它解决。这篇应用笔记就是我实际用U8g2做项目时的完整记录包含选型逻辑、缓冲模式、字体处理、坐标绘图、初始化避坑、疑难排错和低功耗优化适合准备用OLED或LCD做显示界面或者被现有显示代码折磨得想换方案的朋友参考。1. 为什么我最终选择了U8g21.1 用通用驱动库代替手写时序最开始做小仪表时我习惯自己对着数据手册初始化寄存器然后实现画点函数再封装画线、画矩形、画圆。听起来不难但遇到不同型号的屏幕就要重来一遍。SSD1306和SH1106虽然很像但列偏移和寻址模式有区别ST7565、UC1701这类控制器又完全是另一套指令风格。一旦换了屏前期所有驱动代码几乎都要推翻重写。U8g2把这个过程压缩成一行构造函数。它内部针对几十种常见的单色显示控制器做了完整适配接口统一成u8g2.drawPixel、u8g2.drawLine、u8g2.drawStr这些API。我在项目里从SSD1306切换到SH1106时只改了构造函数的类名和参数绘图代码一行没动。对经常在不同屏幕之间切换的人来说这套封装省下的不是几小时而是几天的开发量。1.2 缓冲模式设计解决了内存焦虑小单片机最怕的就是内存不够。U8g2的设计充分考虑了这个现实三个缓冲模式让开发者按RAM预算选择。RAM多的用全缓冲画完一次性刷屏RAM紧张的开局部缓冲按页刷新只要显示文本的场合甚至可以用无缓冲模式配一个很小的RAM就能跑起来。具体来说128x64的屏幕全缓冲需要1024字节RAM这个数字对许多现代MCU来说没什么压力但遇到RAM只有2KB的老平台就非常紧张。局部缓冲模式可以把内存占用压到一两百字节代价是API刷新逻辑稍复杂。这种细分设计是Adafruit风格驱动库很难给我的选择自由度。2. 缓冲模式的真实差异全缓冲、局部缓冲和无缓冲怎么选2.1 三种模式的内存账单与适用平台U8g2的构造函数类名里有_1_、2、_F_这样的标记这几个字母代表缓冲深度。我用一张表整理了自己的理解模式类名标志RAM占用128x64为例特点与应用场景全缓冲F1024字节绘制完一次性刷新不会看到逐步绘制过程适合动画、波形等频繁整屏更新的场景双页局部缓冲2256字节按两页为单位刷新RAM占用适中适合RAM不太宽裕但需要较复杂界面的项目单页局部缓冲1128字节每次只缓冲8像素高度的页RAM极小适合老平台或小内存单片机无缓冲U8x8U8X8系列几乎为零只能逐字符显示文本做菜单和信息展示足够功耗和资源都最低我自己做项目时有个经验法则RAM在4KB以上的直接用全缓冲开发效率最高RAM在1KB到4KB之间用单页或双页局部缓冲RAM小于1KB老老实实用无缓冲的U8x8文本模式不要硬撑图形界面开发体验会好很多。2.2 局部缓冲的刷新节奏控制局部缓冲模式下显示流程和全缓冲完全不同。全缓冲是画完一帧send一次局部缓冲则是反复执行firstPage和nextPage每次刷新一页。典型代码长这样u8g2.firstPage(); do { // 在这里绘制整帧的内容 u8g2.drawFrame(0, 0, 128, 64); u8g2.drawStr(8, 20, Hello U8g2); } while (u8g2.nextPage());初次接触的人可能会觉得这个do-while循环很怪。实际上它每次循环把一页的缓冲提交给屏幕然后nextPage推进到下一页并清空缓冲。如果你留意显示屏会看到它从上往下逐页刷新速度很快肉眼看不清。但如果页数多、I2C速度快某些环境能感觉到轻微扫描感。对刷新速度敏感的项目直接用全缓冲模式避免这个问题。注意局部缓冲模式下的绘图操作是针对当前页裁剪的超出当前页的绘制会被丢弃。如果你发现图形“画不全”先检查是不是用的局部缓冲把整帧绘制放进do-while循环结构里就能解决。3. 字体系统U8g2 真正的核心优势3.1 自带字体指标与基线坐标U8g2内置了上百种字体从5x7的小字体到能显示复杂符号的大字体都有。选字体时我最关心三个指标字符宽度、字符高度和基线位置。前两个决定布局基线位置决定文本垂直排列的准确性。比如u8g2_font_t0_11这个字体高度是11像素但这个高度不是从屏幕顶部往下11像素而是包含上伸部和下伸部的完整行高。绘制字符串时传入的y坐标是基线位置不是字体的顶部。如果直接把y当作顶部坐标字符会被顶到屏幕上方看起来像被裁掉了一截。我自己封装了一个函数来做文本垂直居中核心逻辑是利用字体的Ascent和Descentint totalHeight u8g2.getAscent() u8g2.getDescent(); int centerY (64 - totalHeight) / 2 u8g2.getAscent(); u8g2.drawStr(0, centerY, Centered);用这套办法不管换哪种字体垂直居中都不会跑偏。这个经验是从一次菜单排版错乱里悟出来的当时直接拿字体高度算y坐标换大字体后文字全部显示在地平线以下。3.2 动态测量宽度实现文本水平居中和垂直居中一样水平居中也不能靠猜要用u8g2.getStrWidth函数测量字符串实际像素宽度。这个函数考虑到了字体间距和比例字符的不同宽度比手算字符数乘以固定宽度可靠得多int w u8g2.getStrWidth(Hello); u8g2.drawStr((128 - w) / 2, 40, Hello);数字时钟应用里我每隔一秒刷新一次时间显示。如果数字宽度不一致秒数变化时整个数字串会左右跳动。正确做法是先测量新字符串宽度始终以右边缘对齐来绘制或者把刷新区域固定在右侧这样界面才稳。3.3 中文显示的实用方案U8g2自带的字体全部是拉丁字符官方没有完整的中文字体。项目里有中文需求我的做法是走取模路线先把要显示的中文用取模软件生成像素数组保存为C语言数组运行时用bitmap方式绘制。16x16的中文字模正好是32字节128x64的屏幕可以排4行8列信息量很足。取模时注意字形方向要设置成“行列式、逐行、高位在前”这样和显示RAM的组织方式一致。我做过一个小试验同样一个字模取模方向错了绘制出来的就是镜像或者斜着的碎片检查起来很费神。解决方法是先用drawXBM函数验证单个字模的摆放方向确认无误后再批量处理。4. 坐标、旋转与绘图U8g2 的图形引擎思维4.1 坐标原点和旋转方向的约定U8g2的默认坐标系统是屏幕左上角为原点x轴向右y轴向下。这个约定和很多图形库一致但U8g2有一个额外参数容易忽略构造函数里的旋转标志。U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);U8G2_R0正常方向不旋转U8G2_R1顺时针旋转90度U8G2_R2旋转180度U8G2_R3旋转270度硬件装配方向决定了应该选哪个R参数。如果屏幕竖着安装但代码按横屏布局整个界面会贴到屏幕边缘排查起来很容易怀疑屏坏了。我的建议是确定硬件结构后就固定使用一个旋转参数全项目不要中途切换否则所有坐标都要跟着换算很容易算错。4.2 常用绘图函数与叠加顺序U8g2提供了一整套基础绘图API我日常用得最多的是以下几类像素与线条drawPixel、drawLine边框和填充矩形drawFrame、drawBox圆形与椭圆drawCircle、drawDisc三角形与多边形drawTriangle位图输出drawXBM、drawBitmap绘制时有个细节后画的图形会覆盖先画的图形但U8g2底层没有透明混合的概念。想实现“叠加”效果只能先画底层图形再在指定区域画不透明的前景。画仪表盘指针时我先画填充圆盘再画指针直线最后在圆盘中心画小实心圆三个步骤顺序不能乱。4.3 自己封装迷你图形组件的经验画了很多界面之后我把常用图表封装成了独立函数。比如带刻度的仪表盘、迷你柱状图、动态刷新曲线。封装时有一个原则任何动态变化区域都单独计算边界矩形然后先调u8g2.setDrawColor(0)擦除旧内容再调u8g2.setDrawColor(1)绘制新内容。这样比每次都clearBuffer再重画全部内容高效得多也避免了闪烁。5. 初始化与硬件连接不亮屏和花屏的常见根源5.1 不同硬件接口的构造函数差异U8g2的构造函数里隐藏了接口类型这是新手上手最容易搞混的地方。同样是SSD1306 128x64的屏有硬件I2C、软件I2C、4线硬件SPI、4线软件SPI等不同版本。拼错一个后缀编译能过但屏就是不工作。// 硬件I2C版本 U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); // 4线硬件SPI版本 U8G2_SSD1306_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, /* cs*/ 10, /* dc*/ 9, /* reset*/ 8);选构造函数前先确认自己板子的接线方案是哪一种。I2C接口只用两根数据线连接简单但刷新速度受限于I2C时钟SPI接口需要CS、DC、SCLK、MOSI四根线硬件SPI的刷新速度明显更快适合动画和波形场景。I2C地址也是常见问题。大部分SSD1306模块的默认地址是0x3C少数是0x3D。U8g2在begin()里面会尝试默认地址如果你的屏飞线改了地址就得在构造函数里传入新的I2C地址参数。我遇到一次屏莫名其妙不亮量电压、查焊接都没问题最后换了一个构造参数就好了原因就是模块批号不同地址位被拉到另一种状态。5.2 上电时序和复位引脚不能偷懒OLED控制器一般需要上电后等待一小段时间再初始化。U8g2的begin()内部已经处理了延时但如果你用的屏没有单独的复位引脚构造函数里就要传U8X8_PIN_NONE。如果硬件上连了复位引脚建议给一个显式的GPIO控制在上电后拉低再拉高确保控制器彻底复位。我在一个项目里偷懒没接复位引脚结果屏幕偶尔出现一半正常一半花屏的怪现象。后来在构造函数里指定了复位引脚并且在begin()前加了一小段手动复位逻辑问题就消失了。这个坑很容易被人忽略因为大部分时候不接复位也能正常显示一旦遇到电源不稳的场合就暴露了。5.3 花屏排查五步法花屏是最难定位的问题我按出现频率整理了排查顺序每次照着走基本能解决检查I2C地址和接口类型是否匹配实际硬件先看波形或用扫描工具确认设备地址检查电源电压3.3V的屏接到5V的I2C总线上逻辑电平不匹配会导致通信错乱检查复位时序确认上电后reset引脚有完整的低脉冲检查屏的驱动电压和对比度设置尝试u8g2.setContrast调整亮度排除“看起来花”其实是供电不足的情况检查SPI模式部分国产屏幕模块对SPI Mode 0和Mode 3的兼容性不同U8g2默认按控制器手册配置个别模块需要软件方式翻转时钟相位花屏问题往往是多个因素叠加单纯改一处可能看不出效果。我的习惯是每改一个变量都重新上电测试不要同时改两个地方否则就算解决了也搞不清根因。6. 实测中的疑难杂症与优化思路6.1 低内存环境下的绘图崩溃某次我把项目从一款大RAM芯片移植到小RAM芯片上启动后屏幕显示一团乱码程序运行一段时间后死机。最后定位到问题是全缓冲模式下RAM爆了。那个环境RAM只有2KB图形缓冲就占掉1KB剩下的动态分配空间所剩无几。换成单页局部缓冲后内存占用直接降到128字节程序稳定运行。这件事让我养成了一个习惯拿到新板子第一件事看数据手册里的RAM容量然后按容量决定缓冲模式而不是沿用上一个项目顺手能跑的配置。U8g2的内存占用是可预测的花几分钟算一笔账比后期排查死机问题省太多时间。6.2 怎么处理“只刷新局部区域”的需求很多时候屏幕内容大部分不变只有某个数值一直在变。全量刷新虽然简单但会造成不必要的功耗和视觉闪烁。U8g2本身没有提供矩形局部刷新的API因为它的设计哲学是整帧缓冲。我自己的方案是分层管理静态内容层开机画一次之后不重绘动态内容层数值变化时先用setDrawColor(0)清除该区域的旧内容再用setDrawColor(1)绘制新内容对局部缓冲模式如果你确实只关心屏幕底部几行变化可以把绘制的循环条件做裁剪让这些页参与刷新循环其他页直接跳过。但要注意U8g2局部缓冲模式下nextPage必须走完整屏页数不能半路跳出否则后续页会停止刷新。实际操作上我建议有局部刷新需求的场合优先用全缓冲模式因为每次刷新也才1024字节普通I2C下大约几十毫秒换来的是代码简单、刷新完整、不容易出错。6.3 低功耗与省电模式的实际调节电池供电的项目对功耗非常敏感。U8g2提供了setPowerSave接口可以让控制器进入睡眠模式。实测下来OLED睡眠时功耗下降非常明显。但要注意睡眠模式下RAM内容在大多数控制器上会保留唤醒后屏幕自动恢复之前的画面不需要重新绘制。具体做法是检测到无操作时调u8g2.setPowerSave(true)过一段时间有按键或传感器数据变化时再调u8g2.setPowerSave(false)。如果再加上降低刷新频率、长时间无变化直接不刷屏续航可以提升很多。对比度也是一个低功耗调节点u8g2.setContrast(0)到setContrast(255)之间调节。OLED的亮度本质上是发光时间占比降低对比度可以显著减少平均电流。我做手表类界面时日常显示用低对比度夜晚或强光下再调高兼顾了功耗和可读性。6.4 刷屏速度的数据参考与SPI优势同样是128x64的屏I2C和SPI的刷新速度差异值得量化。全缓冲一次刷1024字节我实测过接口时钟频率全屏刷新预估时间感受硬件I2C400kHz25ms左右日常菜单够用波形刷新略卡硬件SPI8MHz1-2ms左右波形和动画感受不到刷新过程软件I2C100kHz100ms以上仅适合静态文字别拿来做动画软件SPI4MHz约3ms需要多用几个引脚但比软件I2C快得多这个数据来自我自己用逻辑分析仪粗略测量不同驱动芯片和接线长度会有波动但量级关系不会变。做波形显示或动画界面的优先考虑硬件SPI接口的屏只做菜单和数值显示的I2C完全能胜任。7. 我对 U8g2 使用方式的最终排序如果让我给刚开始接触U8g2的朋友一个明确的起点我会说屏幕选硬件I2C接口的常见型号构造函数用全缓冲模式先把文本显示跑通再逐步加图形和动画。不要一上来就折腾局部缓冲和复杂图表那是有了基础之后的事。我的实际开发流程是先确定显示控制器型号对照U8g2的构造参数表选对类名然后用一句话的绘图代码验证显示链路再根据RAM容量和刷新需求决定缓冲模式最后才进入界面布局和字体设计的阶段。这套顺序帮我避开了很多新手期的坑。U8g2的文档很全例子也很多但它毕竟是通用库每个项目的硬件环境都有一点特殊之处。真正好用的库不是让你永远不看手册而是让你有精力把焦点放到你的产品逻辑上而不是反复折腾那些本不该浪费时间的底层细节。