深入解析u8g2 OLED库:STM32 HAL驱动实战与图形绘制技巧

📅 发布时间:2026/7/10 4:02:39 👁️ 浏览次数:
深入解析u8g2 OLED库:STM32 HAL驱动实战与图形绘制技巧
1. 从零开始STM32 HAL与u8g2的初次握手如果你手头有一块STM32开发板和一块小巧的OLED屏幕想做个显示温湿度的小设备或者给项目加个酷炫的界面那么u8g2库绝对是你的不二之选。我刚开始接触嵌入式图形显示时也试过其他库但u8g2给我的感觉就是“稳”。它不光支持海量的显示器型号从常见的SSD1306到SH1106再到各种彩色屏而且API设计得非常人性化画个圆、写个字就跟搭积木一样简单。更重要的是它对STM32的HAL库支持得很好用CubeMX配置几下就能快速跑起来。很多新手可能会被“库的移植”吓到觉得要改一堆底层驱动。其实对于u8g2来说这个过程已经被大大简化了。它的核心思想是“硬件抽象层”u8g2负责所有高级的图形绘制算法而你需要提供的只是几个最基础的函数比如怎么通过I2C或SPI发送一个字节的数据怎么实现一个微秒级的延时。剩下的画图、显示文字这些“重活”u8g2全包了。我印象很深第一次成功在OLED上显示出“Hello World”时只花了不到半小时这种成就感是驱动新手继续探索的最大动力。那么具体需要准备什么呢首先你得有一个STM32的开发环境我强烈推荐使用STM32CubeIDE它集成了CubeMX配置引脚和中间件特别方便。硬件上一块STM32F1或F4系列的核心板比如常见的“蓝桥杯”板子或正点原子的开发板外加一块0.96寸的I2C接口OLED屏通常是四针的成本也就几十块钱。软件上你需要去GitHub上找到u8g2的源码库把里面关键的几个C文件和头文件添加到你的工程里。别担心这个过程我会在下一节手把手带你过一遍保证清晰无坑。2. 工程搭建与驱动移植避开我踩过的那些坑理论说再多不如动手做一遍。这里我以最常用的STM32F103C8T6蓝色药丸板和SSD1306 128x64 OLED屏I2C接口为例带你走通整个流程。我踩过的坑你就不用再踩了。2.1 使用CubeMX进行硬件配置打开STM32CubeMX创建新工程选择你的芯片型号。首先配置时钟树根据你的外部晶振设置好系统时钟让HCLK跑到72MHz对于F103来说这是常见速度。接下来是关键的两步启用I2C1和启用一个定时器用于延时。在“Pinout Configuration”标签页下找到I2C1。我们的OLED屏通常接在I2C上SCL和SDA引脚你可以自由选择比如我用的是PB6SCL和PB7SDA。将I2C1的模式设置为“I2C”参数保持默认的快速模式100kHz通常就够用了如果想刷新快一点可以调到400kHz。然后我们需要一个提供微秒延时的基础。找到“Timers”下的某个定时器比如TIM2将其时钟源设为内部时钟并开启中断。在“Parameter Settings”里预分频器PSC设置为71自动重载值Counter Period设为最大值65535。这样定时器每计数一次就是1微秒72MHz / (711) 1MHz。配置完成后点击“Project Manager”给工程起个名字选择IDE为STM32CubeIDE然后生成代码。CubeMX会帮你生成所有底层初始化代码包括I2C和TIM2的配置这为我们后续移植u8g2的底层驱动函数铺平了道路。2.2 移植u8g2库文件到工程从GitHub下载u8g2源码通常是一个名为u8g2-master的zip包。解压后我们只需要其中一部分文件。在你的CubeIDE工程目录下新建一个文件夹比如叫u8g2。然后从源码的csrc文件夹里复制以下文件过来u8g2.c(这是核心)u8x8_d_ssd1306_128x64_noname.c(这是针对我们屏幕的驱动)u8x8.c(底层抽象)接着从源码的sys/arm/stm32文件夹里找到u8x8_byte_4wire_hw_spi.c或u8x8_byte_hw_i2c.c。因为我们用的是I2C所以复制u8x8_byte_hw_i2c.c。同时把u8x8_gpio_and_delay_stm32.c也复制过来这个文件包含了我们需要实现的GPIO控制和延时函数。回到CubeIDE在项目资源管理器里右键点击工程名选择“Import...” - “File System”将刚才复制到u8g2文件夹里的所有.c文件导入到工程的Src文件夹下。然后同样地从u8g2源码的u8g2.h所在目录将u8g2.h和u8x8.h两个头文件导入到工程的Inc文件夹。最后别忘了在CubeIDE的工程属性里将包含头文件的路径Inc文件夹和u8g2源码目录添加进去否则编译时会找不到头文件。2.3 实现关键的底层回调函数这是移植的核心也是新手最容易懵的地方。u8g2库需要你提供几个硬件相关的函数它才能正常工作。我们主要修改u8x8_gpio_and_delay_stm32.c这个文件。首先实现微秒延时函数。CubeMX已经为我们配置好了TIM2我们在main.c里写一个简单的延时函数void delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(htim2, 0); HAL_TIM_Base_Start(htim2); while (__HAL_TIM_GET_COUNTER(htim2) us); HAL_TIM_Base_Stop(htim2); }然后在u8x8_gpio_and_delay_stm32.c中找到u8x8_gpio_and_delay函数修改其延时部分uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch(msg) { case U8X8_MSG_DELAY_NANO: // 纳秒延时简单实现即可 break; case U8X8_MSG_DELAY_100NANO: // 同上 break; case U8X8_MSG_DELAY_10MICRO: // 10微秒延时 delay_us(10); break; case U8X8_MSG_DELAY_MILLI: // 毫秒延时 HAL_Delay(arg_int); break; case U8X8_MSG_DELAY_I2C: // I2C通信后的延时通常很短 delay_us(2); break; // GPIO处理通常不需要修改因为I2C通信由HAL库硬件处理 default: return 0; } return 1; }对于I2C字节传输函数在u8x8_byte_hw_i2c.c中u8g2已经为我们写好了适配HAL库的版本它内部会调用HAL_I2C_Master_Transmit。你只需要确保在main.c中初始化了I2C外设HAL_I2C_Init(hi2c1)并且屏幕的I2C地址正确通常是0x78或0x7A。至此所有底层移植工作就完成了。编译工程应该零错误零警告。3. 核心API实战从点亮屏幕到绘制动态图形驱动调通后我们就可以尽情使用u8g2丰富的API了。这一节我们不罗列函数而是通过几个实际的小项目带你掌握最核心的用法。你会发现原来在嵌入式设备上做图形界面可以这么有趣。3.1 初始化与第一行“Hello World”首先在main.c的全局变量区定义一个u8g2对象u8g2_t u8g2;。然后在main函数的初始化部分while(1)之前进行u8g2的初始化。// u8g2 初始化结构体 u8g2_t u8g2; // 初始化函数 void u8g2_init() { // 关键选择正确的构造器屏幕型号、旋转方向、字节传输回调、GPIO延时回调 u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_gpio_and_delay_stm32); // 关联I2C句柄这是HAL库驱动需要的 u8x8_SetI2C(u8g2.u8x8, hi2c1); // 初始化显示 u8g2_InitDisplay(u8g2); // 唤醒屏幕关闭省电模式 u8g2_SetPowerSave(u8g2, 0); // 清空缓冲区 u8g2_ClearBuffer(u8g2); }调用u8g2_init()后屏幕就应该被点亮了可能还是白屏。接下来我们显示第一行文字。u8g2有两种主要的绘图模式页面模式和缓冲区模式。页面模式更省RAM适合内存紧张的MCU缓冲区模式逻辑更简单直观适合F103这类有足够RAM的芯片。我们先看缓冲区模式u8g2_ClearBuffer(u8g2); // 清空缓冲区 u8g2_SetFont(u8g2, u8g2_font_ncenB10_tr); // 设置字体 u8g2_DrawStr(u8g2, 0, 20, Hello U8g2!); // 在坐标(0,20)处绘制字符串 u8g2_SendBuffer(u8g2); // 将缓冲区内容发送到屏幕显示注意坐标(0,20)指的是字符串左下角的位置。u8g2_DrawStr只能绘制ASCII字符英文和符号。如果你需要显示中文或者特殊符号必须使用u8g2_DrawUTF8并配合包含中文字符的字体。把上面几行代码放到main函数的初始化后编译下载你的OLED上就应该出现“Hello U8g2!”了。这一步成功意味着整个软硬件链路完全打通后面就是发挥创意的时候了。3.2 绘制基础几何图形与简单动画有了文字我们再来点图形。u8g2提供了从像素点到复杂形状的一系列函数。我们来画一个简单的仪表盘轮廓。假设我们要在屏幕中央画一个带刻度的圆盘。u8g2_ClearBuffer(u8g2); // 1. 画一个空心圆作为外框 u8g2_DrawCircle(u8g2, 64, 32, 30, U8G2_DRAW_ALL); // 圆心(64,32)半径30画整个圆 // 2. 画几条从圆心发出的刻度线模拟仪表 for(int i0; i12; i) { float angle i * 30 * 3.14159 / 180; // 每30度一个刻度 int x_start 64 25 * cos(angle); int y_start 32 25 * sin(angle); int x_end 64 30 * cos(angle); int y_end 32 30 * sin(angle); u8g2_DrawLine(u8g2, x_start, y_start, x_end, y_end); } // 3. 在圆内写个标题 u8g2_SetFont(u8g2, u8g2_font_7x13B_mf); u8g2_DrawStr(u8g2, 50, 35, RPM); u8g2_SendBuffer(u8g2);这只是一个静态画面。如何让它动起来比如让一根指针旋转。这就涉及到动态刷新。一个简单的办法是在while(1)循环里不断清除缓冲区根据新的角度计算指针端点坐标然后重画所有元素外圆、刻度、指针、文字最后发送缓冲区。虽然听起来效率低但对于STM32F103和128x64这种小分辨率屏幕完全绰绰有余。你可以用HAL_GetTick()获取时间计算出不断变化的角度从而实现指针匀速旋转的动画效果。我实测下来帧率能达到30fps以上视觉效果非常流畅。3.3 显示自定义图像与图标项目里总少不了几个Logo或者状态图标。u8g2支持显示XBM格式的位图。XBM是一种古老的图像格式本质上是C语言源代码用数组表示黑白像素。你可以在网上找到很多在线工具将PNG或JPG图片转换成XBM格式。转换后会得到一个数组像这样static const unsigned char my_logo_bits[] { 0x00, 0x00, 0x78, 0x00, 0x84, 0x00, 0x04, 0x01, ... // 省略后续数据 };把这个数组声明在代码中。然后使用u8g2_DrawXBM函数来绘制它// 假设图片宽50像素高30像素 u8g2_DrawXBM(u8g2, 10, 10, 50, 30, my_logo_bits);这里有个关键技巧u8g2_SetBitmapMode。这个函数决定了绘制位图时是否绘制背景。默认模式是0不透明即黑色部分会用当前绘制颜色填充通常是白色这样会覆盖掉背景。如果你希望图标背景是透明的只显示图标本身的白色线条可以在绘制前调用u8g2_SetBitmapMode(u8g2, 1);。这样位图中的0黑色像素点就会被跳过保留屏幕上原有的内容非常适合在复杂背景上叠加图标。4. 高级技巧与性能优化让你的界面更流畅当你的界面元素多起来或者需要频繁更新数据比如实时波形图时可能会遇到闪烁、卡顿的问题。别急这通常是使用姿势不对。掌握下面几个高级技巧能极大提升显示效果和代码效率。4.1 理解与优化页面模式前面我们一直用的ClearBufferSendBuffer是缓冲区全量更新模式。u8g2还有另一种更经典的页面模式使用u8g2_FirstPage和u8g2_NextPage这对函数。它的工作原理是把屏幕分成若干条带Tile一次只渲染和发送一条带的数据到屏幕循环直到整个屏幕更新完毕。这种模式能极大减少对单片机的RAM占用因为只需要存储一个条带的缓冲区。u8g2_FirstPage(u8g2); do { // 在这里放置所有的绘图命令 u8g2_SetFont(u8g2, u8g2_font_ncenB14_tr); u8g2_DrawStr(u8g2, 0, 20, Page Mode); u8g2_DrawCircle(u8g2, 64, 32, 20, U8G2_DRAW_ALL); } while (u8g2_NextPage(u8g2));这个do...while循环会被执行多次直到所有条带都处理完。重要提示在这个循环内部不要调用u8g2_ClearBuffer或u8g2_SendBuffer页面模式会自动管理缓冲区的清除。对于需要频繁局部更新的场景比如只更新屏幕底部的一行数据页面模式可能不是最高效的因为每次循环它都会重绘整个屏幕的所有元素。这时我们可以用另一个技巧局部更新。4.2 实现局部更新与减少闪烁局部更新的核心思想是只修改缓冲区中变化的部分然后只将受影响的屏幕区域发送出去。u8g2本身没有直接的“局部发送”函数但我们可以通过组合一些函数来模拟。首先使用u8g2_SetClipWindow设置一个裁剪窗口所有绘图操作只会在这个窗口内生效超出部分被忽略。然后我们正常绘图但只调用u8g2_SendBuffer。由于裁剪窗口限制了范围u8g2底层在发送数据时理论上可以优化尽管最终驱动可能还是全屏发送但至少绘图计算量减少了。更实用的防闪烁方法是双缓冲逻辑的思维。虽然u8g2不直接提供双缓冲但我们可以通过精心组织代码来避免“先清屏再绘图”导致的短暂白屏。对于动态变化的数字不要每次都用ClearBuffer清全屏。而是先用背景色例如黑色绘制一个实心矩形覆盖掉旧的数字区域然后再绘制新的数字。这样视觉上就只有数字在变化背景保持稳定彻底杜绝闪烁。我在做一个实时时钟项目时秒数字每跳一次就全屏清空一次闪烁非常明显。后来改用局部覆盖法只重绘秒数字所在的矩形区域视觉效果立刻变得丝滑。4.3 字体管理与内存优化心得u8g2的字体文件是消耗ROM和RAM的大户。一个全字库的中文字体可能高达几百KBSTM32F103的Flash可能都装不下。所以按需取用是基本原则。u8g2的字体是高度模块化的你可以在其源码的tools/font/build目录下找到字体编译工具或者直接使用它预生成的字体。在官方的Wiki页面上有按像素高度、语言分类的字体列表。例如u8g2_font_wqy12_t_gb2312就是一个12像素高、包含GB2312常用汉字的字体体积相对适中。如果你的项目只需要显示几十个特定汉字和英文字母我强烈推荐使用自定义字体子集。网上有一些工具可以帮你从大的中文字库中提取出你需要的字符生成一个极小的专属字体文件可能只有几KB大小。这对于优化存储空间有奇效。另外注意字体设置是全局的。频繁切换不同的字体会增加CPU开销。通常的做法是在初始化时设置好默认字体在需要不同大小字体时再临时切换并在完成绘制后切回默认字体。最后分享一个性能监测小技巧你可以用u8g2_GetStrWidth函数在绘制前获取字符串的像素宽度从而实现文本的居中或右对齐计算避免反复试错调整坐标。例如要在一个宽度为box_width的区域居中显示文本str起始x坐标可以计算为(box_width - u8g2_GetStrWidth(u8g2, str)) / 2。这些细节的打磨能让你的界面看起来更专业、更精致。