简介:u8glib_arduino 是面向 Arduino 开发者的轻量级图形显示库,专为在低功耗、小内存的嵌入式平台上驱动 LCD 与 OLED 屏幕而设计。它支持 SSD1306、SH1106、PCF8574 字符型 LCD 等多种显示设备,覆盖 I2C、SPI 与串行接口,提供文本输出、图形绘制、位图显示等丰富功能,适合制作仪表盘、数据可视化界面、小型游戏等交互项目,对刚接触嵌入式图形编程的爱好者与进阶开发者均有参考价值。资源包共 129 个文件,以 105 个 c 源文件为核心,辅以 18 个 ino 示例、2 个 h 头文件、1 个 cpp 及若干 txt、changelog 说明文档,压缩包约 1.05MB,目录结构清晰,便于按模块查阅底层驱动与示例代码。目前已有 724 人学习下载。通过研读源码与示例,读者可掌握 u8g 对象初始化、drawStr/drawCircle/drawBox 等绘图命令的调用方式,理解 firstPage 与 nextPage 的刷新机制,并积累内存管理、显示效率与功耗控制方面的排错思路。
1. 老屏点亮那点事:u8glib_arduino 到底解决什么问题
手里攒了几块 12864 单色液晶,ST7920、SSD1306、PCD8544 各来一块,想用 Arduino 统一驱动,结果每个屏的初始化序列、显存布局、字库格式都不一样,光看数据手册就能耗掉一整个周末。u8glib_arduino 就是冲着这个场景来的:它把几十种单色屏的底层时序、页地址映射、读写方向全部封装成统一的绘图 API,你只调 drawStr、drawLine、drawBitmap,换屏时改一行构造参数就能跑。它适合谁?适合手上有老式单色屏、想在资源受限的 8 位单片机上做菜单、波形、仪表盘,又不想为每块屏重写驱动的嵌入式开发者。这个库不是新东西,但它在低端屏驱动这块的覆盖面,至今没有几个库能完全替代。
2. 把 u8glib_arduino 装进 Arduino IDE:从库目录到第一行像素
2.1 库的获取与目录结构确认
u8glib_arduino 的发布形式是标准的 Arduino 库压缩包,解压后目录名通常带版本后缀。Arduino IDE 只认根目录下有src或直接有.h/.cpp的库文件夹,所以解压后如果多套了一层同名目录,要手动把内层提出来。常见做法是放到用户目录下的libraries里,而不是 IDE 安装目录,这样升级 IDE 不会丢库。
# 以 Linux 为例,Arduino 用户库目录通常在 ~/Arduino/libraries # 假设压缩包解压后得到 u8glib_arduino 文件夹 unzip u8glib_arduino.zip -d ~/Arduino/libraries/ # 确认目录结构:根目录下应直接看到 src 或 .h 文件 ls ~/Arduino/libraries/u8glib_arduino/ # 期望输出类似:src examples library.properties README.md逻辑说明:Arduino IDE 1.5 以后支持src子目录递归编译,所以库根目录下有src是正常结构。如果解压出来是u8glib_arduino/u8glib_arduino/src,IDE 会找不到头文件,必须把内层目录整体移到libraries下。参数上唯一要注意的是目录名不要带空格和中文,否则 Windows 下编译会报路径错误。
2.2 构造函数的选型:U8GLIB_* 类名怎么挑
u8glib_arduino 不给一个通用构造函数,而是为每种屏和接线方式预定义了类名,比如U8GLIB_ST7920_128X64_1X、U8GLIB_SSD1306_128X64、U8GLIB_PCD8544。类名里的数字和字母编码了分辨率、接口类型和是否使用硬件 SPI。选错类名不会报编译错误,但屏幕会白屏或花屏,这是新手最容易翻车的地方。
#include "U8glib.h" // ST7920 128x64,使用软件 SPI,CS 接 pin 10 // 类名后缀 _1X 表示单屏、软件模拟 SPI 时序 U8GLIB_ST7920_128X64_1X u8g(10); // SSD1306 128x64,I2C 接口,地址默认 0x3C // U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE); void setup() { // 设置显示旋转方向,0 为默认 u8g.setRot180(); } void loop() { u8g.firstPage(); do { u8g.setFont(u8g_font_6x10); u8g.drawStr(0, 12, "Hello u8glib"); } while (u8g.nextPage()); }逻辑说明:firstPage()和nextPage()构成一个 do-while 循环,这是 u8glib 特有的分页渲染机制。它不维护完整显存缓冲,而是把绘制命令分页写入屏幕控制器,所以内存占用极低,但代价是每帧都要重新执行整个绘制循环。参数上,构造函数括号里的数字是 CS 或 DC 引脚号,I2C 屏则传U8G_I2C_OPT_NONE或指定地址。setRot180()在初始化阶段调用一次即可,放在loop里会反复翻转导致闪烁。
2.3 最小验证:让屏幕出现可读字符
烧录后如果屏幕全黑,先别怀疑库,用三用表量背光和对比度引脚。ST7920 这类屏需要 VLCD 引脚有合适电压才能出字,很多模块板载了电位器,但出厂位置不一定对。软件侧可以先用drawBox画一个填充矩形,确认控制器在响应,再换drawStr排查字库问题。
void loop() { u8g.firstPage(); do { // 先画一个实心矩形,验证像素寻址是否正常 u8g.drawBox(0, 0, 20, 20); // 再画字符,验证字库和坐标 u8g.setFont(u8g_font_unifont); u8g.drawStr(25, 15, "OK"); } while (u8g.nextPage()); delay(500); }逻辑说明:drawBox(x, y, w, h)的坐标原点在左上角,x 向右增,y 向下增。如果矩形出现但字符不出现,问题在字库选择或setFont未生效;如果矩形位置偏移,检查setRot180或屏幕初始化方向。u8g_font_unifont是内置的 Unicode 字体,体积较大,8 位机上如果 flash 紧张可以换u8g_font_6x10。延时 500ms 是为了让分页刷新肉眼可见,实际产品中应去掉。
3. 绘图 API 的边界:坐标、字体与显存分页的真实约束
3.1 坐标系统与裁剪行为
u8glib 的坐标是 16 位有符号整数,但实际有效范围由屏幕分辨率决定。超出边界的绘制不会崩溃,而是被裁剪掉,这个特性在画滚动波形时很有用,但也会掩盖坐标计算错误。比如你想在 128 宽屏幕上画一条从 -10 到 140 的线,库会静默裁掉两端,看起来像线变短了,实际是坐标越界。
// 画一条超出屏幕范围的线,观察裁剪行为 u8g.drawLine(-20, 32, 150, 32); // 实际渲染结果:从 x=0 到 x=127 的横线逻辑说明:裁剪在库内部通过边界判断实现,不消耗额外显存。参数上,drawLine的四个参数都是u8g_uint_t类型,传负数会被隐式转换,所以不要依赖负数坐标做偏移,应该在调用前自己 clamp。常见误用是用drawStr时 y 坐标传 0,结果字符顶部被切掉,因为 y 是基线位置,不是顶部。
3.2 字体选择的取舍:flash 占用与可读性
u8glib 内置字体从 4x6 到 10x20 不等,还有 Unicode 字体。8 位 AVR 的 flash 通常只有 32KB,选u8g_font_unifont会占掉好几 KB,留给程序的空间就很紧张。我的习惯是:菜单和状态用u8g_font_6x10,标题用u8g_font_7x14B,只有必须显示中文时才上 Unicode 字体,并且把中文字库单独裁剪。
| 字体名 | 字符宽x高 | 大致 flash 占用 | 适用场景 |
|---|---|---|---|
| u8g_font_4x6 | 4x6 | 约 0.5KB | 密集数据、小屏 |
| u8g_font_6x10 | 6x10 | 约 1KB | 通用菜单、状态 |
| u8g_font_7x14B | 7x14 | 约 1.5KB | 标题、强调 |
| u8g_font_unifont | 可变 | 约 8KB 以上 | 多语言、中文 |
逻辑说明:字体数据存放在 flash 中,运行时通过setFont切换指针。参数上,setFont只改变后续绘制使用的字体,不影响已绘制内容。如果 flash 实在不够,可以用u8g_font_6x10配合自定义点阵数组,只取需要的几个汉字。
3.3 分页渲染与内存占用的关系
u8glib 的核心设计是分页渲染:它不分配全屏缓冲,而是把屏幕按页(通常是 8 行像素一页)分多次写入。firstPage重置页指针,nextPage推进到下一页并返回是否还有页。这意味着你的绘制代码会被执行多次,每次只渲染当前页的内容。如果绘制代码里有副作用(比如改变量、发串口),会重复执行,这是血泪经验。
int counter = 0; // 全局变量 void loop() { u8g.firstPage(); do { counter++; // 错误!每页都会加一次 u8g.drawStr(0, 10, String(counter).c_str()); } while (u8g.nextPage()); }逻辑说明:上面的代码在 128x64 屏幕上会执行 8 次counter++,因为屏幕分 8 页。正确做法是把状态更新放在firstPage之前,绘制循环里只读不写。参数上,nextPage返回 0 时表示所有页已渲染完,循环退出。这个机制让 u8glib 在 2KB RAM 的 ATmega328P 上也能驱动 128x64 屏,代价是绘制逻辑必须无副作用。
4. 避坑与排查:u8glib_arduino 最常见的五类翻车
4.1 屏幕全黑但背光亮
现象:烧录后背光正常,屏幕没有任何像素变化。原因:对比度电压不对,或者构造函数选错接口类型。解决:先调模块上的电位器,用drawBox画实心块确认控制器响应;如果仍无反应,检查类名是否匹配屏型号,比如把 I2C 屏误用成 SPI 类名。ST7920 在并行模式下还需要检查 PSB 引脚电平。
4.2 显示内容闪烁或残影
现象:每帧刷新时旧内容没清干净,出现叠字。原因:firstPage/nextPage循环里没有清屏,或者清屏命令放在循环外。解决:u8glib 不会自动清屏,需要在绘制前调用u8g.drawBox(0, 0, width, height)填充背景,或者用u8g.setColorIndex(0)后画全屏矩形。注意清屏也要放在分页循环内,否则只清当前页。
4.3 编译报错 “U8glib.h: No such file”
现象:IDE 提示找不到头文件。原因:库目录层级不对,或者库文件夹名带特殊字符。解决:确认libraries/u8glib_arduino/下直接有U8glib.h或src/U8glib.h。如果是从 GitHub 下载的源码包,可能缺少library.properties,手动补一个或者把src内容提到根目录。Windows 下路径不要有中文和空格。
4.4 汉字显示乱码或方块
现象:英文正常,中文变成乱码。原因:源文件编码不是 UTF-8,或者字体不支持对应汉字。解决:Arduino IDE 默认保存为 UTF-8,但某些编辑器会存成 GBK。用u8g_font_unifont时,字符串里的汉字必须是 UTF-8 编码。如果字体里没有该字,会显示空白或方块,需要换用带该字的字库或自己取模。
4.5 刷新率低到肉眼可见
现象:波形更新卡顿,一帧要几百毫秒。原因:软件 SPI 时序慢,或者每帧绘制内容太多。解决:换用硬件 SPI 类名(如U8GLIB_ST7920_128X64_1X换成带HW后缀的版本),减少每帧绘制图元数量,避免在循环里用String拼接。I2C 屏可以降低 I2C 速率到 400kHz 以下反而更稳,但刷新率会进一步下降,需要权衡。
5. 进阶技巧:用 u8glib 做流畅波形与低内存菜单
5.1 环形缓冲实现滚动波形
在 128x64 屏上画实时波形,常见做法是维护一个环形缓冲,每次新采样到来时覆盖最旧数据,然后全屏重绘。u8glib 的分页机制下,重绘 128 个点大约需要 8 次页循环,在 16MHz AVR 上可以做到 20fps 以上。关键是把采样和绘制分离,采样在定时器中断里做,绘制在主循环里做。
#define WAVE_LEN 128 uint8_t waveBuf[WAVE_LEN]; uint8_t writeIdx = 0; void loop() { // 模拟新采样,实际应来自 ADC 或传感器 waveBuf[writeIdx] = analogRead(A0) >> 2; // 10位转8位 writeIdx = (writeIdx + 1) % WAVE_LEN; u8g.firstPage(); do { for (uint8_t x = 0; x < WAVE_LEN; x++) { uint8_t idx = (writeIdx + x) % WAVE_LEN; uint8_t y = 63 - (waveBuf[idx] >> 1); // 映射到0-63 u8g.drawPixel(x, y); } } while (u8g.nextPage()); }逻辑说明:writeIdx指向下一个要写入的位置,绘制时从writeIdx开始读,保证最旧数据在左边。analogRead返回 0-1023,右移 2 位得到 0-255,再右移 1 位映射到 0-63 的 y 坐标。参数上,WAVE_LEN等于屏幕宽度,这样每个 x 对应一个采样点。如果屏幕更宽,可以增大缓冲,但 RAM 占用也会增加,ATmega328P 的 2KB RAM 下 128 字节缓冲是安全的。
5.2 低内存菜单的状态机写法
用 u8glib 做多级菜单,不要用递归或大数组存菜单项,而是用状态机加函数指针。每个菜单页是一个函数,负责绘制和响应按键。这样 flash 占用小,RAM 几乎不增加。按键扫描放在主循环,用millis做消抖。
enum MenuState { MENU_MAIN, MENU_SETTING, MENU_ABOUT }; MenuState state = MENU_MAIN; void drawMain() { u8g.setFont(u8g_font_6x10); u8g.drawStr(0, 12, "> Main"); u8g.drawStr(0, 24, " Setting"); u8g.drawStr(0, 36, " About"); } void loop() { u8g.firstPage(); do { switch (state) { case MENU_MAIN: drawMain(); break; case MENU_SETTING: /* drawSetting() */ break; case MENU_ABOUT: /* drawAbout() */ break; } } while (u8g.nextPage()); // 按键处理略,根据按键改变 state }逻辑说明:state变量决定当前绘制哪个菜单页,切换菜单只改变量,不重建界面。参数上,每个绘制函数内部用setFont设定自己的字体,互不干扰。按键消抖建议用 20ms 的millis窗口,避免在分页循环里读按键导致重复触发。这种写法在 32KB flash 的板子上可以塞下十几个菜单页。
5.3 验证刷新率与内存占用的实用手段
想知道当前配置下 u8glib 的实际刷新率和 RAM 余量,可以在loop里翻转一个空闲 IO,用示波器测周期;RAM 余量则用freeMemory()函数(需自己实现,读__brkval和栈指针)在串口打印。我的习惯是:每加一个绘制元素,就测一次帧时间,超过 50ms 就说明该优化了。另外,u8g.setFont调用次数越少越好,字体切换有开销,能在初始化时定好就定好。
注意:分页渲染下,任何在
firstPage/nextPage循环内的串口打印都会重复多次,调试信息务必放在循环外。
我自己的教训是,早期做菜单时把String拼接放在绘制循环里,结果每页都重新分配内存,跑几分钟就死机。后来改成固定字符数组,问题消失。这个库不复杂,但它的分页模型和常规显存驱动完全不同,理解这一点,后面就顺了。希望帮到你。
本文还有配套的精品资源,点击获取