news 2026/2/9 19:17:15

u8g2绘制位图图像的操作指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
u8g2绘制位图图像的操作指南

如何用 u8g2 在嵌入式设备上高效绘制位图:从图像转换到屏幕显示的完整实战指南

你有没有遇到过这样的场景?手头一块STM32开发板,接了个128x64的OLED屏,想在开机时显示个Logo,结果翻遍资料发现:要么代码跑不起来,要么图像歪了、花屏了,甚至直接卡死?

别急——这正是我们今天要解决的问题。

在资源极其有限的嵌入式系统中实现图形显示,不是“能不能”的问题,而是“怎么做得对”的问题。而u8g2,就是那个能让8位单片机也玩转图形界面的秘密武器。

本文不讲空泛理论,也不堆砌API列表。我们将以真实项目视角,带你走完“一张图片 → 编译进Flash → 成功显示在OLED上”这一完整链路。重点聚焦位图绘制这一高频需求,深入剖析数据布局、工具链选择、代码集成和常见坑点,让你一次搞懂,永久避坑。


为什么是 u8g2?它凭什么成为嵌入式GUI的“万能胶”

先说结论:如果你正在用STM32、ESP32或Arduino做带屏幕的小项目,又不想为每块不同的OLED重写驱动,那 u8g2 几乎是你目前最好的选择。

它不像LVGL那样功能强大但吃内存,也不像Adafruit GFX那样只照顾Arduino生态。u8g2 的定位很清晰——为资源受限环境提供稳定、轻量、跨平台的单色图形支持

它的作者 Oliver Kraus 是个狠人,一个人维护着超过150种显示屏的驱动适配。这意味着无论你是用 SSD1306、SH1106 还是 NHD-C12864,只要换一个初始化函数,其他代码基本不用动。

更重要的是,它对RAM极其友好。在“页模式”下,哪怕只有几百字节RAM的MCU也能流畅运行。比如一个常见的配置:

u8g2_Setup_ssd1306_i2c_128x64_noname_nw(&u8g2, U8G2_R0, cb_byte, cb_gpio);

这里的_nw后缀代表“no wide”,即使用最小缓冲区(仅一行像素),RAM占用不到130字节!

这种设计哲学决定了 u8g2 特别适合工业控制面板、手持仪器、传感器节点这类“需要一点可视化,但不能多花钱买RAM”的场景。


位图是怎么“画”上去的?理解物理内存与逻辑数据的关系

很多人第一次调用u8g2_DrawXBM()发现图像旋转90度或者上下颠倒,根本原因是对OLED内部存储结构不了解。

我们以最常用的SSD1306为例,它的显存组织方式叫“页列结构”(Page-Column Addressing):

  • 屏幕垂直方向被分成8页(page),每页8行(共64行);
  • 每页内按列(column)寻址,共128列;
  • 每个地址存放一个字节(8位),对应当前页中该列上的8个像素(MSB在上);

换句话说,数据是以“竖条”为单位存储的。如下图所示:

Column 0 Column 1 Column 127 +------+ +------+ +------+ | bit7 | --> | bit7 | --> ... -->| bit7 | ← Page 0 (Row 0~7) | bit6 | | bit6 | | bit6 | ... ... ... | bit0 | | bit0 | | bit0 | ← Page 0 (Row 7) +------+ +------+ +------+ [下一个字节仍属于同一列,进入Page 1]

所以当你准备位图数据时,必须按照这个规则打包:
每一列连续8行作为一个字节,高位在上,低位在下

如果你拿Photoshop导出一个横向扫描的BMP,直接转成数组去画,大概率会得到一堆乱码。这就是为什么我们必须借助专用工具进行格式转换。


图像处理四步法:把PNG变成可执行的C数组

要在屏幕上显示一个Logo,你需要完成以下四个步骤:

第一步:选图 & 预处理

建议使用黑白PNG或BMP作为源文件,尺寸尽量匹配目标区域。例如你要在一个128x64屏幕上居中显示一个64x32的图标,那就提前裁剪好。

关键设置:
- 分辨率:无需过高,128dpi足够;
- 模式:强制转为1-bit Bitmap(纯黑白);
- 背景:透明背景最好填充为白色(即0xFF),避免后期误判。

第二步:使用专业工具生成C数组

推荐两个高效工具:

✅ LCD Image Converter(Windows桌面端)

这是老牌神器,支持导入BMP/PNG,自动二值化,并可预览输出效果。

操作流程:
1. File → Load Image → 选择你的PNG;
2. Conversion → Convert to Monochrome;
3. Output → Export as C Array;
4. 格式选择 “XBM” 或 “Horizontal scanning”;
5. 导出.h文件。

输出示例:

// logo.h static const unsigned char logo_bits[] = { 0xff, 0xff, 0xc3, 0xc3, 0xc3, 0xc3, 0xff, 0xff, 0x81, 0x81, 0x7e, 0x7e, 0x00, 0x00, 0x00, 0x00 }; #define LOGO_WIDTH 16 #define LOGO_HEIGHT 16

⚠️ 注意:默认可能是“vertical scan”模式,记得勾选“horizontal bytes”确保兼容 u8g2 的DrawXBM

✅ image2cpp (在线网页版)

跨平台首选,无需安装。

亮点功能:
- 支持调整阈值、反转颜色;
- 可指定输出为 u8g2 兼容格式;
- 自动生成坐标偏移参考框;
- 支持RLE压缩(需启用库选项);

导出后保存为icon_logo.h并加入工程即可。


实战代码详解:STM32 + SSD1306 I²C 显示位图全过程

下面我们以 STM32F103 + HAL库 + SSD1306 OLED(I²C接口)为例,写出完整的可运行代码框架。

硬件连接

OLED引脚MCU连接
VCC3.3V
GNDGND
SCLPB6 (I2C1_SCL)
SDAPB7 (I2C1_SDA)
RESPA8 (可选)

提示:有些模块内置升压电路,支持5V供电;RES建议接GPIO以便软件复位。


初始化部分:HAL层回调函数必须写对

u8g2通过两个核心回调函数与硬件交互:

  • byte_cb:负责I²C/SPI数据传输;
  • gpio_and_delay_cb:处理延时和控制引脚(DC、CS、RES等);
通信层(I²C传输)
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8g2, uint8_t msg, uint8_t arg_int, void *arg_ptr) { static uint8_t buffer[32]; static uint8_t buf_idx = 0; switch (msg) { case U8X8_MSG_BYTE_SEND: memcpy(&buffer[buf_idx], arg_ptr, arg_int); buf_idx += arg_int; break; case U8X8_MSG_BYTE_START_TRANSFER: buf_idx = 0; break; case U8X8_MSG_BYTE_END_TRANSFER: // 发送I²C数据,注意地址左移一位(7位地址) HAL_I2C_Master_Transmit(&hi2c1, 0x3C << 1, buffer, buf_idx, 100); break; default: return 0; } return 1; }
GPIO与延时层
uint8_t u8x8_gpio_and_delay_stm32(u8x8_t *u8g2, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch (msg) { case U8X8_MSG_GPIO_AND_DELAY_INIT: MX_GPIO_Init(); // 初始化相关引脚 break; case U8X8_MSG_DELAY_MILLI: HAL_Delay(arg_int); break; case U8X8_MSG_GPIO_DC: HAL_GPIO_WritePin(DC_PORT, DC_PIN, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; case U8X8_MSG_GPIO_RESET: HAL_GPIO_WritePin(RES_PORT, RES_PIN, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; case U8X8_MSG_GPIO_CS: // I²C模式下通常不使用CS break; default: return 0; } return 1; }

📌 补充说明:
-DC_PIN对应数据/命令切换线(Data/Command),有些模块标记为 D/C 或 SA0;
- 若无独立RES引脚,可在初始化时跳过复位操作;


主程序:清屏 → 绘图 → 刷新

#include "u8g2.h" #include "logo.h" // 包含位图数据 u8g2_t u8g2; void init_display(void) { u8g2_Setup_ssd1306_i2c_128x64_noname_f( &u8g2, U8G2_R0, // R0表示正常方向 u8x8_byte_hw_i2c, // I²C发送函数 u8x8_gpio_and_delay_stm32 // GPIO控制函数 ); u8g2_InitDisplay(&u8g2); u8g2_SetPowerSave(&u8g2, 0); // 关闭省电模式 } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); init_display(); while (1) { u8g2_ClearBuffer(&u8g2); // 在 (32, 16) 坐标绘制位图 u8g2_DrawXBM(&u8g2, 32, 16, LOGO_WIDTH, LOGO_HEIGHT, logo_bits); // 添加文字说明 u8g2_SetFont(&u8g2, u8g2_font_6x10_tf); u8g2_DrawStr(&u8g2, 50, 55, "Hello!"); u8g2_SendBuffer(&u8g2); // 必须调用才能刷新屏幕 HAL_Delay(2000); } }

📌 关键点解析:

  • u8g2_ClearBuffer():每次绘图前务必清屏,否则旧内容残留;
  • u8g2_DrawXBM():专用于XBM格式图像,参数顺序为(x, y, width, height, data)
  • u8g2_SendBuffer():触发实际刷新,底层会分页写入显存;
  • 字体可自由切换,内置几十种大小字体,支持简单中文(如u8g2_font_unifont_t_chinese2);

常见问题与调试秘籍:这些坑我都替你踩过了

❌ 图像显示为竖线或完全错位?

→ 检查是否使用了正确的“横向扫描”数据格式。很多工具默认输出纵向排列,必须手动勾选“horizontal scan”。

✅ 正确的数据特征:宽度越大,数组越长;而不是高度越大数组越长。

❌ 屏幕全黑或部分点亮?

→ 检查I²C地址是否正确。SSD1306常见地址有0x3C0x3D,可用I2CScanner工具确认。

→ 查看供电是否稳定,某些模块需要至少3.3V以上电压才能点亮。

❌ 编译报错:“undefined reference to u8g2_xxx”

→ 确保已将u8g2源文件添加到工程:
-u8g2.c
-u8x8.c
-clib/u8g2_bitmap.c
- 所有字体文件(若使用)

建议使用官方提供的/sys/arm-stm32-hardfloat/lib/示例目录结构进行整合。

❌ 程序卡死在HAL_I2C_Master_Transmit

→ 添加超时检测机制,防止总线挂死:

if (HAL_I2C_Master_Transmit(&hi2c1, addr, data, len, 100) != HAL_OK) { // 尝试恢复I2C(如重新初始化) }

→ 或者启用硬件DMA提升稳定性。


设计建议:如何让图形系统更健壮、更易维护

1. 使用页模式还是全缓冲?

条件推荐模式
RAM ≥ 1KBFull Buffer (_f),刷新快,支持复杂动画
RAM < 512BPage Buffer (_nf_uf),节省内存但刷新慢

例如:
-_noname_f→ 全缓冲
-_noname_nw→ 最小缓冲(仅一行)

2. 图像尺寸优化技巧

  • 宽度尽量是8的倍数,减少位操作开销;
  • 高度最好是8的倍数,避免跨页渲染效率下降;
  • 多个小图标可合并为雪碧图(Sprite Sheet),统一加载减少函数调用次数;

3. 动态画面要不要频繁刷新?

静态界面(如启动页)只需绘制一次。可以这样做:

static uint8_t initialized = 0; if (!initialized) { u8g2_ClearBuffer(&u8g2); u8g2_DrawXBM(...); u8g2_SendBuffer(&u8g2); initialized = 1; } // 之后不再刷新

4. 中文显示可行吗?

可以!u8g2 内置部分GB2312字符集,启用方法:

u8g2_SetFont(&u8g2, u8g2_font_wqy12_t_chinese2); // 文泉驿12px u8g2_DrawUTF8(&u8g2, 10, 30, "你好世界");

缺点是字体较大(约4KB),需评估Flash空间。


结语:掌握位图绘制,你就掌握了嵌入式GUI的入门钥匙

看到这里,你应该已经明白:在嵌入式系统中显示一张图片,远不止“调个函数”那么简单。它背后涉及图像格式、内存模型、通信协议和资源权衡等多个层面。

而 u8g2 的价值,就在于把这些复杂的细节封装成简洁的API,让我们可以用几行代码完成原本需要上千行的工作。

更重要的是,一旦你掌握了位图绘制这套流程,后续扩展就变得非常自然:

  • 显示电池图标?加一组XBM数据就行;
  • 做菜单动画?用多个位图帧循环播放;
  • 实现波形图?结合DrawLine和定时器实时更新;

下次当你面对一块小小的OLED屏时,请记住:再简单的屏幕,也能讲出精彩的人机交互故事

如果你在实现过程中遇到了具体问题,欢迎留言讨论。我可以帮你分析数据格式、检查初始化配置,甚至远程“会诊”I²C波形。

毕竟,每一个成功的u8g2_SendBuffer()背后,都曾有过无数次失败的尝试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/6 22:27:08

UNIAPP原型开发:1小时验证你的产品创意

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 快速构建一个外卖点餐应用的UNIAPP原型&#xff0c;包含&#xff1a;1)餐厅列表页&#xff1b;2)菜单选择页&#xff1b;3)购物车和结算流程。不要求完整功能实现&#xff0c;但要…

作者头像 李华
网站建设 2026/2/3 5:44:46

JS every()方法:零基础图解教程

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 制作一个面向初学者的JS every()方法教学示例&#xff0c;要求&#xff1a;1. 用比喻解释every()的工作原理&#xff08;如全班同学是否都及格&#xff09;&#xff1b;2. 提供3个…

作者头像 李华
网站建设 2026/2/9 16:07:46

DCOM批量管理效率提升300%的秘诀

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 编写一个跨服务器的DCOM批量管理工具&#xff0c;功能要求&#xff1a;1) 通过AD域自动发现目标服务器 2) 并行执行DCOM配置变更 3) 支持配置模板的导入导出 4) 提供变更前后配置差…

作者头像 李华
网站建设 2026/2/3 22:49:53

ANTFLOW实战:构建电商订单自动化处理系统

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 在ANTFLOW平台上开发一个电商订单自动化处理系统。功能包括&#xff1a;1. 实时接收并解析电商平台的订单数据&#xff1b;2. 自动检查库存并更新库存状态&#xff1b;3. 生成发货…

作者头像 李华
网站建设 2026/2/8 19:48:40

Minimal Bash-like Line Editing在实际开发中的应用案例

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个实战案例&#xff0c;展示Minimal Bash-like Line Editing在自动化脚本中的应用。案例应包括一个简单的脚本&#xff0c;使用Bash-like Line Editing功能进行文件处理和日…

作者头像 李华
网站建设 2026/2/3 14:00:15

基于STM32的L298N驱动教程:零基础也能学会

从零构建电机控制系统&#xff1a;L298N STM32 的实战全解析你有没有遇到过这样的情况&#xff1f;手里的智能小车说走就走&#xff0c;但方向一乱、速度不稳&#xff0c;调试半天也找不到问题出在哪。或者&#xff0c;在做毕业设计时&#xff0c;明明代码写得没问题&#xff…

作者头像 李华