1. 从点亮到画图:为什么需要更强大的OLED驱动库?
如果你已经跟着网上的教程,用STM32F103C8T6和HAL库成功点亮了那块0.96寸的OLED屏幕,并且能显示几行文字,恭喜你,你已经迈出了第一步!但不知道你有没有和我一样的感受:当你想画个进度条、显示个传感器波形,或者做个简单的小图标菜单时,面对那一堆直接操作SSD1306寄存器的WriteCmd和WriteDat函数,是不是有点无从下手?
我刚开始玩STM32和OLED的时候也是这样。自己写的驱动,显示字符和数字没问题,但一旦涉及到画线、画圆、显示图片,就得自己算坐标、写算法,代码又长又容易出错。更别提想做个动态效果或者稍微复杂点的界面了,那工作量简直让人头大。后来我发现,其实有更“聪明”的办法——那就是直接移植那些经过千锤百炼的、功能强大的开源图形库,比如 U8g2 或者 Adafruit GFX。
这两个库可以说是嵌入式图形显示的“瑞士军刀”。它们把底层繁琐的像素操作封装成了一个个简单的函数,比如 drawLine(), drawCircle(), print()。你不需要关心屏幕内存是怎么组织的,也不需要自己去算一个圆要用哪些像素点来画,调用一个函数就能搞定。这就像是你想盖房子,不需要从烧砖开始,而是直接使用标准化的预制件,效率和质量都高得多。
对于我们的STM32F103C8T6(也就是常说的“蓝桥杯”板或最小系统板)来说,虽然资源有限(64KB Flash,20KB RAM),但驱动一个128x64的单色OLED,运行U8g2这样的库是完全没有问题的。移植成功后,你就能获得一个完整的图形API接口,从此告别“点灯”时代,真正进入“图形化”应用开发。无论是做毕业设计、电子竞赛,还是个人DIY项目,界面的美观度和开发速度都能提升好几个档次。接下来,我就手把手带你,把U8g2库移植到我们的HAL库工程里,并实现一些炫酷的图形功能。
2. 工程准备与U8g2库的获取
在开始移植之前,我们需要一个干净的基础工程。这个工程已经配置好了I2C用来驱动OLED,就像原始文章里做的那样。如果你还没有,可以快速用STM32CubeMX生成一个。
2.1 基础工程创建
打开STM32CubeMX,选择STM32F103C8Tx芯片。在Pinout & Configuration标签页下,配置时钟树(通常选择外部晶振,主频拉到72MHz)。接着,找到I2C1,将其模式设置为I2C。我这里和原始文章一样,使用PB6和PB7作为I2C的SCL和SDA引脚。然后,在Project Manager标签页,给工程起个名字,比如OLED_U8g2_Demo,选择MDK-ARM作为Toolchain/IDE。关键一步是在Code Generator里,选择“Copy only the necessary library files”,这样工程会更清爽。最后点击GENERATE CODE,用Keil打开工程。
2.2 获取U8g2库源码
U8g2库的官方仓库在GitHub上(搜索“olikraus/u8g2”)。对于嵌入式开发者,我们不需要下载整个仓库的历史记录。最方便的方法是直接去Release页面下载最新的u8g2.zip源码包。这个包里包含了所有源代码和示例。下载后解压,我们重点关注以下几个文件夹:
csrc: 这是核心所在,里面是所有C源码文件。sys: 包含了一些系统级的抽象层,比如u8x8_d_ssd1306_128x64_noname.c这个文件,它就是专门针对我们这种SSD1306驱动的128x64 OLED的显示驱动描述文件。cppsrc: C++的封装,我们纯C环境用不到。
对于STM32的HAL库,我们还需要关注sys文件夹下的u8x8_d_ssd1306_128x64_noname.c文件,以及如何实现底层的byte-level通信函数。U8g2库结构清晰,但文件较多,我们只需要挑选出必要的部分加入到我们的工程中,避免编译缓慢和资源浪费。
3. 精简与移植:将U8g2库融入HAL工程
直接把整个csrc文件夹拖进工程会引入大量无关的驱动文件(支持上百种显示器),导致编译慢且占用过多Flash。我们需要进行“瘦身”。
3.1 在工程中创建U8g2组并添加核心文件
在Keil的Project窗口,右键Source Group,新建一个组,命名为U8g2。然后,从解压的U8g2源码中,只添加我们必需的文件:
u8g2.h和u8g2.c: 库的主框架和API实现。u8x8.h和u8x8.c: 底层硬件抽象层。u8x8_d_ssd1306_128x64_noname.c: 针对我们这款OLED的显示驱动。u8x8_d_ssd1306_128x64_noname.c文件依赖u8x8_d_ssd1306_xxx.c中的一些通用函数,所以最好也把u8x8_d_ssd1306.c加进来。这样我们就集成了SSD1306系列的全部基础支持。
3.2 实现关键的硬件抽象回调函数
这是移植的核心步骤。U8g2库通过几个回调函数(callback)来操作硬件,我们需要根据HAL库的I2C函数来实现它们。在工程中新建一个文件,比如叫u8g2_hal.c,并包含对应的头文件。
首先,我们需要实现字节传输函数uint8_t u8x8_byte_hw_i2c。这个函数会被库调用来发送命令或数据。其原型在u8x8.h中定义。我们的实现大致如下:
#include “u8x8.h”
#include “main.h” // 包含hi2c1的定义
extern I2C_HandleTypeDef hi2c1; // 假设你的I2C句柄叫这个
#define OLED_I2C_ADDR 0x78 // OLED的I2C写地址,通常是0x78
uint8_t u8x8_byte_hw_i2c(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr) {
switch(msg) {
case U8X8_MSG_BYTE_SEND: // 库准备发送一批数据
// arg_ptr指向待发送数据缓冲区,arg_int是数据长度
if (HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR, (uint8_t*)arg_ptr, arg_int, HAL_MAX_DELAY) != HAL_OK) {
return 0; // 传输失败
}
break;
case U8X8_MSG_BYTE_INIT: // 初始化,可以放空或初始化硬件
// 硬件初始化已在main.c中完成,这里通常不需要操作
break;
case U8X8_MSG_BYTE_SET_DC: // 设置数据/命令引脚(对于I2C接口的OLED,这个信号通常通过I2C字节中的控制位区分,所以这里我们不需要操作GPIO)
// I2C模式下,DC(数据/命令)位是包含在传输的第一个字节里的,所以这个回调通常为空。
break;
case U8X8_MSG_BYTE_START_TRANSFER: // 开始传输(可空)
case U8X8_MSG_BYTE_END_TRANSFER: // 结束传输(可空)
break;
default:
return 0; // 未知消息,返回0表示失败
}
return 1; // 成功返回1
}
注意,对于I2C接口的SSD1306,每个通信序列的第一个字节是控制字节,其中最低位(Co bit)为0表示后续是命令,为1表示是数据。U8g2库在调用u8x8_byte_hw_i2c之前,已经帮我们组装好了这个控制字节,所以我们这里的传输函数非常简单,直接调用HAL的HAL_I2C_Master_Transmit把缓冲区发出去就行。
3.3 实现GPIO和延时回调 除了字节传输,U8g2还需要复位(Reset)和延时功能。对于我们的模块,如果硬件上没有把复位引脚接到STM32,或者我们不想控制它,可以忽略复位回调。延时回调必须实现,因为库需要微秒级的延时(例如初始化序列)。
// 复位回调(如果硬件连接了复位引脚则实现,否则返回1即可)
uint8_t u8x8_gpio_and_delay_stm32(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr) {
switch(msg) {
case U8X8_MSG_GPIO_AND_DELAY_INIT: // 初始化GPIO和延时(可空)
// 如果需要,可以在这里初始化复位引脚为输出
break;
case U8X8_MSG_DELAY_MILLI: // 毫秒延时
HAL_Delay(arg_int);
break;
case U8X8_MSG_DELAY_10MICRO: // 10微秒延时(用于I2C时序)
// 实现一个简单的10微秒延时循环
for (uint16_t i = 0; i < arg_int * 10; i++) {
__NOP(); // 空操作
}
break;
case U8X8_MSG_DELAY_100NANO: // 100纳秒延时(通常不需要)
__NOP();
break;
case U8X8_MSG_GPIO_RESET: // 控制复位引脚
// 如果连接了复位引脚,例如接到PA0
// HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, (GPIO_PinState)arg_int);
break;
default:
break;
}
return 1;
}
3.4 构造U8g2对象并初始化
现在,硬件底层驱动已经“嫁接”好了。接下来我们在主程序(比如main.c)中创建并初始化U8g2对象。
首先,在文件顶部包含头文件并声明对象:
#include “u8g2.h”
u8g2_t u8g2; // 定义一个全局的U8g2对象
然后,在main函数的初始化部分(HAL_Init()和SystemClock_Config()之后,while(1)之前),添加以下初始化代码:
// 初始化U8g2库
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_gpio_and_delay_stm32);
// 参数解释:
// &u8g2: 我们定义的u8g2对象
// U8G2_R0: 旋转方向(0度旋转)
// u8x8_byte_hw_i2c: 我们实现的字节传输函数
// u8x8_gpio_and_delay_stm32: 我们实现的GPIO和延时函数
// 调用初始化函数,这会向OLED发送一系列初始化命令
u8g2_InitDisplay(&u8g2);
// 清空屏幕缓冲区
u8g2_ClearBuffer(&u8g2);
// 唤醒显示屏
u8g2_SetPowerSave(&u8g2, 0);
这里u8g2_Setup_ssd1306_i2c_128x64_noname_f是一个“构造器”函数,它把我们的硬件驱动函数和具体的显示屏型号绑定在一起。函数名中的f代表“full buffer”模式,即库会在内存中维护一个完整的128x64位(即1024字节)的帧缓冲区。所有绘图操作都先在这个缓冲区中进行,最后一次性发送到屏幕。这种方式编程最简单直观,但会占用1KB的RAM。对于STM32F103C8T6的20KB RAM来说,完全负担得起。
4. 释放图形能力:使用U8g2绘制基础图形与文字
库初始化成功后,你就可以像在PC上画图一样使用丰富的API了。所有绘图函数都需要在u8g2_FirstPage和u8g2_NextPage构成的循环内调用,这是U8g2的页面缓冲区处理机制。
4.1 绘制文字与数字 显示文字变得异常简单。U8g2内置了多种字体,你需要先设置字体,然后指定位置打印。
u8g2_ClearBuffer(&u8g2); // 清空缓冲区
// 设置字体(字体名称反映了高度和宽度,如u8g2_font_6x10_tf)
u8g2_SetFont(&u8g2, u8g2_font_6x10_tf);
// 在坐标(0, 10)处绘制字符串(原点在左上角,y向下递增)
u8g2_DrawStr(&u8g2, 0, 10, “Hello U8g2!”);
// 设置更大的字体
u8g2_SetFont(&u8g2, u8g2_font_10x20_tf);
u8g2_DrawStr(&u8g2, 0, 35, “STM32F103C8T6”);
// 显示变量(数字)
int sensorValue = 1234;
char tempBuffer[20];
sprintf(tempBuffer, “ADC: %d”, sensorValue);
u8g2_SetFont(&u8g2, u8g2_font_7x13_tf);
u8g2_DrawStr(&u8g2, 0, 55, tempBuffer);
// 将缓冲区内容发送到屏幕显示
u8g2_SendBuffer(&u8g2);
你不需要再自己编写字模数组,U8g2库内置了海量的字体(从4x6像素到几十像素高的字体都有),只需要在u8g2_fonts.c文件中包含你需要的字体即可。为了节省空间,通常只添加用到的字体。
4.2 绘制几何图形 画线、画框、画圆,都是一行代码的事。
u8g2_ClearBuffer(&u8g2);
// 画一条从(10,10)到(118,54)的对角线
u8g2_DrawLine(&u8g2, 10, 10, 118, 54);
// 画一个矩形框,左上角(20,20),宽40,高30
u8g2_DrawFrame(&u8g2, 20, 20, 40, 30);
// 画一个实心矩形,左上角(70,20),宽40,高30
u8g2_DrawBox(&u8g2, 70, 20, 40, 30);
// 画一个圆,圆心(64,32),半径20
u8g2_DrawCircle(&u8g2, 64, 32, 20, U8G2_DRAW_ALL); // U8G2_DRAW_ALL表示绘制整个圆
// 画一个圆角矩形框,左上角(10,50),宽108,高10,圆角半径3
u8g2_DrawRFrame(&u8g2, 10, 50, 108, 10, 3);
// 绘制一个三角形,三个顶点坐标
u8g2_DrawTriangle(&u8g2, 100, 10, 110, 30, 90, 30);
u8g2_SendBuffer(&u8g2);
4.3 制作一个动态进度条 结合图形和文字,我们可以轻松创建UI元素。下面是一个简单的进度条示例,它会随着一个变量(比如ADC采样值)变化而更新。
// 假设在某个循环或定时器中断中更新
static uint8_t progress = 0;
void updateProgressBar(void) {
u8g2_ClearBuffer(&u8g2);
// 绘制进度条外框
u8g2_DrawFrame(&u8g2, 10, 20, 108, 12);
// 绘制进度条填充部分,宽度随progress变化(0-100)
u8g2_DrawBox(&u8g2, 12, 22, (uint16_t)progress * 104 / 100, 8);
// 显示百分比文字
u8g2_SetFont(&u8g2, u8g2_font_7x13_tf);
char pctText[10];
sprintf(pctText, “%d%%”, progress);
// 计算文字宽度使其居中
uint8_t textWidth = u8g2_GetStrWidth(&u8g2, pctText);
u8g2_DrawStr(&u8g2, 64 - textWidth/2, 50, pctText);
u8g2_SendBuffer(&u8g2);
// 模拟进度增加
progress++;
if (progress > 100) progress = 0;
}
5. 进阶应用:显示自定义图标与位图
虽然U8g2内置了丰富的图形函数,但显示复杂的图标、Logo或者图片,还是需要用到位图(Bitmap)。U8g2支持XBM格式的位图,这是一种纯文本的位图格式,可以直接包含在C代码中。
5.1 使用在线工具生成XBM位图 你可以用任何图像处理软件(如Photoshop、GIMP)制作一个单色(黑白)图片,保存为PNG或BMP格式。然后,使用在线转换工具(搜索“image to xbm converter”)将其转换为XBM格式。XBM文件本质上是一个C语言数组定义。例如,一个16x16的图标转换后可能如下:
#define icon_width 16
#define icon_height 16
static unsigned char icon_bits[] = {
0x00, 0x00, 0x06, 0x60, 0x0e, 0x70, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,
0x60, 0x06, 0xc0, 0x03, 0xc0, 0x03, 0x60, 0x06, 0x70, 0x0e, 0x38, 0x1c,
0x1c, 0x38, 0x0e, 0x70, 0x06, 0x60, 0x00, 0x00 };
5.2 使用U8g2绘制位图
在你的工程中新建一个头文件(如my_bitmaps.h),将上面的数组定义放进去。然后在主程序中,使用u8g2_DrawXBM函数来绘制它。
#include “my_bitmaps.h”
void drawIcon(void) {
u8g2_ClearBuffer(&u8g2);
// 在坐标(56, 24)处绘制图标
u8g2_DrawXBM(&u8g2, 56, 24, icon_width, icon_height, icon_bits);
u8g2_SendBuffer(&u8g2);
}
u8g2_DrawXBM函数会自动处理位图的显示。你还可以结合之前的图形函数,为图标添加一个外框或者背景,制作成更精美的UI按钮。
5.3 优化与内存管理
使用U8g2的full buffer模式(_f后缀)最方便,但消耗1KB RAM。如果你的项目RAM非常紧张,可以考虑使用page buffer模式(_1、_2后缀,如u8g2_Setup_ssd1306_i2c_128x64_noname_1)。这种模式下,库一次只处理一页(8行)的数据,内存占用小(比如128字节),但编程模型略有不同,绘图指令需要放在一个特定的循环结构里(u8g2_FirstPage/u8g2_NextPage),每页都会被重复调用多次。对于STM32F103C8T6,我建议初学者先用full buffer模式,开发更顺畅,1KB的RAM开销在大多数应用中是完全可以接受的。
移植完成后,你会发现原本需要大量底层代码的图形功能,现在变成了简单的函数调用。这不仅仅是代码量的减少,更是开发思维的升级——从“如何控制每个像素”转向“如何设计界面和交互”。你可以用更多的精力去构思项目功能本身,而不是陷在驱动的泥潭里。我自己的好几个小项目,比如温湿度计、简易示波器、游戏机,都是在U8g2的帮助下快速实现了漂亮的界面。下次当你再想用OLED做点有趣的东西时,试试这个强大的库吧,它绝对能让你的开发体验焕然一新。
767



被折叠的 条评论
为什么被折叠?



