STM32移植U8g2库驱动OLED:源码精简与硬件适配实战
1. 项目概述与核心思路之前玩ESP8266的时候在Arduino环境下用U8g2库驱动OLED画点线面、显示文字确实方便。但很多实际项目尤其是对成本、功耗有要求的还是绕不开STM32这类更纯粹的MCU。最近有个小项目需要在STM32F103C8T6这块“蓝色小药丸”上驱动一块0.96寸的OLED屏显示复杂的菜单和图表自然就想到了功能强大的U8g2库。然而直接从GitHub上把U8g2的源码拖下来一股脑儿塞进Keil MDK工程里编译十有八九会报错不是内存爆了就是一堆未定义的符号。这很正常U8g2为了通用性源码包罗万象支持上百种驱动芯片和屏幕。我们的目标很明确让它在STM32上驱动我们手头这块SSD1306芯片的128x64 OLED跑起来无论是IIC还是SPI接口。所以这次移植的核心思路就两个字精简。我们要像外科手术一样从庞大的U8g2源码库中精准地剥离出我们需要的部分然后为STM32的硬件环境“量身定制”几个关键函数。整个过程我会把每一步的原理、为什么这么做、以及我踩过的坑都详细拆解出来。只要你手头有STM32的开发环境我用的是Keil MDK和一块OLED屏跟着做一定能点亮。2. U8g2库源码结构与精简策略U8g2的源码结构非常清晰但初次接触可能会觉得文件繁多。理解它的组织方式是成功移植的第一步。2.1 源码目录解析从GitHub克隆或下载的U8g2库我们主要关注csrc目录这里是纯C语言的实现也是STM32移植的基础。u8g2.h/u8x8.h: 核心头文件定义了所有的数据结构、函数原型和宏。u8g2_*.c: 一系列文件实现了U8g2层的高级API比如画图、字体渲染等。这部分我们通常全部保留因为它们是图形功能的核心。u8x8_*.c: 实现了U8x8层底层驱动抽象层的各类函数。我们需要重点关注的是u8x8_d_*.c显示驱动和u8x8_cad_*.c通信抽象。u8g2_d_setup.c: 这个文件包含了所有屏幕的初始化模板函数。比如u8g2_Setup_ssd1306_i2c_128x64_noname_f就是为我们这种屏幕准备的。这是第一个需要动刀子的地方。u8g2_d_memory.c: 这个文件管理显示缓冲区buffer的内存分配。这是第二个也是最重要的精简点处理不当直接导致编译失败。2.2 驱动文件的选择与删除我们的屏幕是SSD1306驱动128x64分辨率IIC接口。那么在csrc目录下找到所有u8x8_d_*.c文件。除了u8x8_d_ssd1306_128x64_noname.c或类似名称具体看你的屏幕驱动芯片和分辨率其他的都可以安全删除。例如u8x8_d_ssd1316.c,u8x8_d_sh1106.c等等都是给其他屏幕用的留着只会增加代码体积。同理u8x8_cad_*.c文件我们只需要保留与IIC通信相关的。对于软件IIC我们即将采用的方式保留u8x8_cad.c即可它内部通过回调调用我们的GPIO函数。对于硬件IIC或SPI则需要保留对应的u8x8_cad_ssd13xx_fast_i2c.c或u8x8_cad_001.c等。为了简单起见本次移植统一使用软件模拟所以只留u8x8_cad.c。注意这里“删除”指的是从你的STM32工程中移除这些源文件的引用或者将它们移到工程目录外。直接在源码包里删除原文件不是好习惯建议拷贝一份干净的U8g2csrc目录到你的项目里在里面进行操作。2.3 核心配置文件的精准裁剪这是减少代码体积和避免内存问题的关键。2.3.1 精简u8g2_d_setup.c打开这个文件你会看到几十个甚至上百个u8g2_Setup_xxx函数。我们的目标只有一个找到并保留u8g2_Setup_ssd1306_i2c_128x64_noname_f。其他所有函数都可以用预编译指令注释掉或者直接删除。为什么是_f后缀U8g2为不同内存需求的场景提供了不同缓冲区大小的初始化函数_1: 缓冲区128字节。适用于极低内存环境但需要配合分页绘制API使用复杂。_2: 缓冲区256字节。折中方案。_f: 缓冲区1024字节128*64/8。全帧缓冲区可以一次性在内存中构建完整画面然后一次性发送到屏幕编程最简单直观也是我们最常用的方式。对于STM32F103C8T620K RAM来说1KB的缓冲区完全能接受。所以保留u8g2_Setup_ssd1306_i2c_128x64_noname_f其他全部注释掉。最终这个文件应该看起来非常简洁#include “u8g2.h” /* ssd1306 f */ void u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb) { uint8_t tile_buf_height; uint8_t *buf; u8g2_SetupDisplay(u8g2, u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, byte_cb, gpio_and_delay_cb); buf u8g2_m_16_8_f(tile_buf_height); u8g2_SetupBuffer(u8g2, buf, tile_buf_height, u8g2_ll_hvline_vertical_top_lsb, rotation); }2.3.2 精简u8g2_d_memory.c—— 避免内存不足的关键这是最容易出错的一步。打开这个文件里面有很多u8g2_m_xx_xx_x函数。每个u8g2_Setup_xxx函数都会调用其中一个来分配缓冲区。我们的u8g2_Setup_ssd1306_i2c_128x64_noname_f调用了u8g2_m_16_8_f。因此我们必须只保留这一个函数其他所有函数都必须注释或删除。为什么链接器在链接时会把所有未被调用的函数也打包进最终的可执行文件除非开启了严格的函数级优化。如果保留了几十个内存分配函数它们每个都可能定义了一个静态缓冲区就像u8g2_m_16_8_f里的static uint8_t buf[1024]即使这些缓冲区没有被实际使用编译器也会为它们分配空间。这会在编译阶段就耗尽STM32F103C8T6那宝贵的20K RAM编译报错通常指向.bss或.data段溢出或者在链接阶段报错。精简后的u8g2_d_memory.c应该像这样#include “u8g2.h” uint8_t *u8g2_m_16_8_f(uint8_t *page_cnt) { #ifdef U8G2_USE_DYNAMIC_ALLOC *page_cnt 8; return 0; #else static uint8_t buf[1024]; // 这就是那个1KB的全帧缓冲区 *page_cnt 8; return buf; #endif } // 其他函数全部删除或注释掉 // uint8_t *u8g2_m_16_4_1(uint8_t *page_cnt) { … } // uint8_t *u8g2_m_16_4_2(uint8_t *page_cnt) { … } // …实操心得我第一次移植时就在这里栽了跟头编译顺利通过但链接时疯狂报错“.bss” segment overflow。排查了半天才发现是u8g2_d_memory.c里几十个缓冲区函数没删它们定义的静态数组加起来远远超过了20KB。所以这一步务必干净利落。3. 硬件接口的适配与驱动编写精简完源码接下来就要告诉U8g2如何与我们STM32的GPIO“对话”了。U8g2通过一个叫做u8x8_gpio_and_delay的回调函数来抽象所有硬件操作我们需要实现它。3.1 GPIO初始化首先根据你的硬件连接初始化IIC对应的两个GPIO引脚。这里以常见的PB6(SCL), PB7(SDA)为例使用推挽输出模式。// 根据自己的硬件连接修改引脚和端口 #define OLED_IIC_SCL_PIN GPIO_Pin_6 #define OLED_IIC_SDA_PIN GPIO_Pin_7 #define OLED_IIC_GPIO_PORT GPIOB #define OLED_IIC_GPIO_CLK RCC_APB2Periph_GPIOB void OLED_IIC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(OLED_IIC_GPIO_CLK, ENABLE); GPIO_InitStructure.GPIO_Pin OLED_IIC_SCL_PIN | OLED_IIC_SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(OLED_IIC_GPIO_PORT, GPIO_InitStructure); // 初始置高总线空闲 GPIO_SetBits(OLED_IIC_GPIO_PORT, OLED_IIC_SCL_PIN | OLED_IIC_SDA_PIN); }如果你用的是SPI接口那么就需要初始化SCK、MOSI、CS、DC命令/数据和RESET这几个引脚模式同样是推挽输出。3.2 实现u8x8_gpio_and_delay回调函数这是移植的核心。U8g2底层驱动u8x8会调用这个函数来实现所有延迟和GPIO控制。// 你需要实现的延时函数1微秒和1毫秒级别基于SysTick或定时器 extern void delay_us(uint32_t us); extern void delay_ms(uint32_t ms); 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: // 延时 arg_int 纳秒通常用空指令实现 // 对于STM32通常不实现纳秒级延时留空或简单处理 break; case U8X8_MSG_DELAY_100NANO: // 延时 arg_int * 100 纳秒 // 同样简单处理或留空对IIC时序影响不大 break; case U8X8_MSG_DELAY_10MICRO: // 延时 arg_int * 10 微秒 delay_us(arg_int * 10); break; case U8X8_MSG_DELAY_MILLI: // 延时 arg_int 毫秒用于初始化等 delay_ms(arg_int); break; case U8X8_MSG_DELAY_I2C: // I2C速度延时arg_int单位是100kHz。例如400kHz时arg_int4 // 软件IIC下用于控制SCL高低电平的保持时间实现时序 // 这里我们用一个简单的微秒延时具体值需要根据IIC速度和CPU频率调整 delay_us(2); // 假设400kHz IIC半周期约1.25us这里给2us留有余量 break; // GPIO控制消息 (软件IIC) case U8X8_MSG_GPIO_I2C_CLOCK: // 控制SCL引脚, arg_int0输出低arg_int1输出高 if(arg_int) GPIO_SetBits(OLED_IIC_GPIO_PORT, OLED_IIC_SCL_PIN); else GPIO_ResetBits(OLED_IIC_GPIO_PORT, OLED_IIC_SCL_PIN); break; case U8X8_MSG_GPIO_I2C_DATA: // 控制SDA引脚, arg_int0输出低arg_int1输出高 if(arg_int) GPIO_SetBits(OLED_IIC_GPIO_PORT, OLED_IIC_SDA_PIN); else GPIO_ResetBits(OLED_IIC_GPIO_PORT, OLED_IIC_SDA_PIN); break; // 以下是一些菜单功能相关的GPIO如果不用可以返回0或1 case U8X8_MSG_GPIO_MENU_SELECT: case U8X8_MSG_GPIO_MENU_NEXT: case U8X8_MSG_GPIO_MENU_PREV: case U8X8_MSG_GPIO_MENU_HOME: u8x8_SetGPIOResult(u8x8, 0); // 假设没有连接这些按钮 break; default: u8x8_SetGPIOResult(u8x8, 1); // 默认返回1 break; } return 1; // 总是返回1表示成功 }关键点解析U8X8_MSG_DELAY_I2C: 这个延时决定了软件IIC的时钟速度。arg_int参数传递的是以100kHz为单位的IIC速度值。例如标准模式100kHz对应arg_int1快速模式400kHz对应arg_int4。我们实现的延时如delay_us(2)需要满足这个速度下高低电平的最小保持时间。如果屏幕初始化或通信不稳定可以尝试增大这个延时。U8X8_MSG_GPIO_I2C_CLOCK/DATA: 这就是软件模拟IIC的底层操作。U8g2的u8x8_byte_sw_i2c函数会通过调用这些消息配合延时一步步“拼”出完整的IIC起始、停止、发送字节、接收应答等时序。我们不需要自己写IIC协议只需要提供最基本的引脚高低电平控制。对于SPI接口的实现如果你的OLED是SPI接口那么u8x8_gpio_and_delay函数的内容会完全不同你需要处理U8X8_MSG_GPIO_SPI_CLOCK,U8X8_MSG_GPIO_SPI_DATA,U8X8_MSG_GPIO_CS,U8X8_MSG_GPIO_DC等消息并调用你写好的SPI底层发送函数。同时初始化函数也要换成u8g2_Setup_ssd1306_128x64_noname_f注意没有i2c字样。3.3 U8g2初始化封装函数编写一个初始化函数将前面精简好的设置组合起来。#include “u8g2.h” // 声明我们写好的回调函数 extern uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr); void u8g2_ssd1306_128x64_i2c_init(u8g2_t *u8g2) { // 关键初始化函数参数依次为 // u8g2: 上下文结构体 // U8G2_R0: 旋转方向0度不旋转 // u8x8_byte_sw_i2c: U8g2提供的软件IIC字节传输函数 // u8x8_gpio_and_delay: 我们刚实现的硬件抽象函数 u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_gpio_and_delay); u8g2_InitDisplay(u8g2); // 向OLED发送初始化序列硬件复位后必须调用 u8g2_SetPowerSave(u8g2, 0); // 唤醒OLED0打开显示1进入省电模式 u8g2_ClearBuffer(u8g2); // 清空内部显示缓冲区 }4. 工程集成与显示测试4.1 在Keil MDK中添加源码和头文件路径添加源文件在你的STM32工程中新建一个分组例如命名为U8g2。将我们精简后的csrc目录下所有.c文件除了那些明确删除的驱动文件添加到这个分组中。务必确认u8g2_d_setup.c和u8g2_d_memory.c是我们修改后的版本。添加头文件路径在项目的“Options for Target” - “C/C” - “Include Paths”中添加U8g2csrc目录的路径。这样编译器才能找到u8g2.h。4.2 主程序编写与测试在主函数中按顺序进行初始化然后就可以使用U8g2丰富的API进行绘制了。#include “stm32f10x.h” #include “delay.h” // 你的延时函数头文件 #include “u8g2.h” #include “oled_i2c.h” // 包含了你写的 GPIO_Init 和 u8x8_gpio_and_delay 等函数 u8g2_t u8g2; // 定义一个全局的U8g2上下文结构体 int main(void) { SystemInit(); // 系统初始化 delay_init(); // 延时初始化 OLED_IIC_GPIO_Init(); // 初始化IIC GPIO u8g2_ssd1306_128x64_i2c_init(u8g2); // 初始化U8g2和OLED // 测试绘制 u8g2_SetFont(u8g2, u8g2_font_ncenB08_tr); // 设置字体 u8g2_DrawStr(u8g2, 0, 12, “Hello U8g2!”); // 在坐标(0,12)绘制字符串 u8g2_DrawFrame(u8g2, 5, 20, 118, 40); // 画一个矩形框 u8g2_DrawCircle(u8g2, 64, 40, 15, U8G2_DRAW_ALL); // 画一个实心圆 // 将缓冲区内容发送到OLED显示 u8g2_SendBuffer(u8g2); while(1) { // 可以在这里实现动态刷新 // 例如使用 u8g2_FirstPage / u8g2_NextPage 循环进行动态绘图 // u8g2_FirstPage(u8g2); // do { // // 绘制代码 // } while ( u8g2_NextPage(u8g2) ); } }4.3 使用页面缓冲模式进行动态显示上面的例子用的是u8g2_SendBuffer它一次性发送整个1KB的缓冲区。对于动态画面更高效的方式是使用页面缓冲模式。void dynamic_draw_test(void) { char count_str[10]; static uint8_t count 0; u8g2_FirstPage(u8g2); // 开始页面循环 do { // 在循环内绘制每一帧的内容 u8g2_SetFont(u8g2, u8g2_font_logisoso16_tf); sprintf(count_str, “%03d”, count); u8g2_DrawStr(u8g2, 40, 30, count_str); u8g2_DrawRFrame(u8g2, 10, 10, 108, 44, 5); // 圆角框 } while ( u8g2_NextPage(u8g2) ); // 自动发送当前页并判断是否还有下一页 count; delay_ms(200); }在main的while(1)中调用dynamic_draw_test()你会看到一个不断递增的数字在屏幕上刷新。u8g2_FirstPage/u8g2_NextPage机制会自动管理缓冲区的分页发送比手动调用SendBuffer更适用于动画或频繁更新的界面。5. 常见问题排查与深度优化移植过程很少一帆风顺这里总结几个典型问题及解决方法。5.1 编译链接错误问题编译通过但链接时报错“.bss” segment overflow或“.data” overflow。原因几乎可以肯定是u8g2_d_memory.c文件没有精简干净里面多个静态缓冲区定义占用了大量RAM。解决再次彻底检查u8g2_d_memory.c确保只保留了u8g2_m_16_8_f这一个函数。问题提示undefined symbol u8x8_d_ssd1306_128x64_noname等未定义错误。原因对应的驱动源文件如u8x8_d_ssd1306_128x64_noname.c没有添加到工程中或者头文件路径不正确。解决检查工程文件列表和头文件包含路径。5.2 屏幕无显示或显示乱码问题屏幕完全不亮或者亮但显示雪花点、乱码。排查步骤硬件检查确认OLED的VCC、GND连接正确IIC的上拉电阻通常4.7K或10K是否接好。用逻辑分析仪或示波器抓一下SCL和SDA的波形是最直接的。初始化顺序确保先执行了u8g2_InitDisplay再执行u8g2_SetPowerSave(u8g2, 0)。顺序反了屏幕不会开启。IIC地址大部分0.96寸OLED的IIC地址是0x78写地址或0x7A。但有些可能是0x3C。U8g2的u8x8_d_ssd1306_128x64_noname驱动默认使用0x3C。如果不对需要修改驱动文件。在u8x8_d_ssd1306_128x64_noname.c文件中找到u8x8_d_ssd1306_128x64_noname这个结构体里面有一个i2c_address成员将其改为0x78。延时问题u8x8_gpio_and_delay中的U8X8_MSG_DELAY_I2C延时太短可能导致IIC时序不满足屏幕要求。尝试将delay_us(2)增大到delay_us(5)或delay_us(10)。缓冲区未发送绘制完成后忘记调用u8g2_SendBuffer或使用页面循环。绘图操作只是在内存缓冲区中进行必须发送才能显示。5.3 显示内容错位或镜像问题文字或图形显示的位置不对或者上下/左右镜像了。原因屏幕的扫描方向或COM引脚配置与驱动默认值不符。解决在初始化后调用U8g2的屏幕旋转或重映射命令。例如u8g2_SetDisplayRotation(u8g2, U8G2_R2); // 旋转180度 // 或者使用更底层的重映射命令 u8g2_SendF(u8g2, “c”, 0xA0); // 列地址重映射 u8g2_SendF(u8g2, “c”, 0xC0); // COM扫描方向重映射具体命令需要查阅SSD1306的数据手册。U8g2的u8g2_SendF函数可以直接发送原始命令到屏幕。5.4 性能优化与字体管理问题绘制复杂界面或大量文字时感觉慢。优化建议字体选择U8g2内置了大量字体但有些字体文件很大。使用u8g2_font_开头的字体是完整字体而u8g2_font_xxx_mr或u8g2_font_xxx_tr是经过裁剪的字体只包含ASCII字符集体积小很多。在u8g2.h附近有一个u8g2_fonts.c文件你可以只把你需要的字体文件加入工程而不是全部。减少全局刷新对于局部更新可以只清除和重绘发生变化的部分区域而不是每次都u8g2_ClearBuffer全屏重绘。提升IIC速度在保证稳定的前提下减小U8X8_MSG_DELAY_I2C的延时值。如果使用硬件IIC速度会有显著提升需要实现对应的u8x8_byte_hw_i2c回调。5.5 移植到其他STM32型号或开发环境从F103到其他系列如F4, H7:整个过程完全通用。只需要注意你的delay_us和delay_ms函数需要针对新的时钟频率进行调整。GPIO初始化部分的寄存器操作可能不同如果使用HAL库则调用HAL库函数。对于RAM更大的型号如F407你甚至可以不用精简u8g2_d_memory.c但为了代码整洁建议还是精简。在STM32CubeIDE或PlatformIO中移植步骤完全一致。添加文件到项目设置头文件路径。重点依然是那两个.c文件的精简和u8x8_gpio_and_delay函数的实现。这些IDE的构建系统可能对未使用的函数有更好的优化但手动精简是最保险的做法。移植U8g2到STM32本质上是一个“对接”工作将通用的图形库与特定的硬件平台连接起来。一旦打通了这个环节U8g2强大的图形和字体功能就能为你所用在小小的OLED上实现复杂的UI就不再是难题。整个过程最需要耐心和细心的地方就是源码的精简和硬件回调函数的调试。希望这份详细的记录能帮你少走弯路。

相关新闻

最新新闻

日新闻

周新闻

月新闻