ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32移植u8g2驱动SH1106 OLED硬件SPI全流程指南

STM32移植u8g2驱动SH1106 OLED硬件SPI全流程指南 简介面向STM32F103RCT6的u8g2图形库移植工程基于HAL库并采用硬件SPI驱动中景园1.3寸OLED屏SH1106128x64MDK版本5.34。资源适用于需要在STM32上快速集成OLED显示的中级开发者重点解决引脚配置、SPI时序、字体调用及内存优化等常见问题。工程包共1381个文件核心为670个c源码和251个头文件涵盖u8g2驱动源码、常用字体及应用逻辑其余为MDK编译产生的中间文件如o、crf和链接脚本、映射文件等辅助内容工程完整可直接打开参考或二次移植压缩包约25.12MB。已有2842人学习使用。通过该工程读者能获得一套可运行的u8g2HAL库模板包括SPI初始化代码、OLED底层驱动、字体显示示例及MDK配置细节便于后续开发菜单、图表等界面也能深入理解u8g2库在STM32平台上的移植要点。 之前有个项目需要在OLED上同时显示实时曲线、菜单和几个数字起初拿着SSD1306裸驱动一行一行画像素画到一半自己都想笑。后来把u8g2库移植到STM32上配合HAL库的硬件SPI接口驱动SH1106_128x64屏幕图形、字体、动画全部变成了现成的API调用项目进度一下子快了起来。这篇文章想把从零移植的完整过程记录下来CubeMX怎么配、源码怎么进工程、HAL层回调怎么写、黑屏花屏怎么查以及缓冲区与DMA优化方向。适合手头有SH1106或SSD1306 OLED、打算在STM32上做正经图形界面的朋友参考F103、F401这类带硬件SPI的芯片都能照搬思路。1. 为什么要用u8g2从“点亮OLED”到“画出任何界面”1.1 先分清三方角色很多人第一次接触OLED以为“驱动屏幕”就是把几根引脚接好、初始化寄存器、往显存里写字节。这个理解不算错但一旦要显示中文、画曲线、做菜单纯裸驱动的工作量会迅速失控。u8g2出现之前我光是把一个数值转成字符串再逐字模点阵对齐就写了两百多行代码。所以后来所有带屏幕的项目只要有空间我基本都会把u8g2塞进去。整个系统里其实有三个完全不同的角色u8g2是图形库它负责把文字、线条、矩形、位图画进一块内存bufferSH1106是显示屏控制器它负责把收到的显存数据转换成OLED像素点的亮灭STM32的SPI外设则是两者之间的数据通道。u8g2不直接操作STM32的寄存器也不认识HAL库它只依赖你提供的两个函数指针一个负责“把这串字节发出去”一个负责“管GPIO和延时”。这就是它能在Arduino、ESP32、STM32、Linux framebuffer上到处跑的底层原因。1.2 u8g2的Setup函数是怎么把三者拴在一起的u8g2官方源码里提供了大量“接线即用”的Setup函数比如这次用的u8g2_Setup_sh1106_128x64_f_hw_spi( u8g2, U8G2_R0, u8x8_byte_stm32_hw_spi, u8x8_stm32_gpio_and_delay);别看这个函数名又长又怪里面每一个字段都对应一个决定sh1106_128x64表示屏幕控制器型号和分辨率f表示使用全屏缓冲区hw_spi表示走硬件SPI通道。你在工程里换一块SSD1306的128x64屏只要把Setup函数名改成u8g2_Setup_ssd1306_128x64_f_hw_spi渲染代码完全不用动。这就是u8g2最值钱的地方显示控制器的差异被封装在Setup函数内部业务层只跟统一的绘图API打交道。当然Setup函数最后的两个参数才是STM32移植的核心。u8x8_byte_stm32_hw_spi是我们自己写的字节发送回调u8x8_stm32_gpio_and_delay是我们自己写的GPIO与延时回调。u8g2内部所有命令、数据、片选、复位动作最后都会折算成这两个回调里的消息。理解这一点后面看代码就不会晕。1.3 为什么偏偏用硬件SPIu8g2同时支持软件SPI和硬件SPI。软件SPI的Setup函数名里带sw_spi它用普通GPIO按位翻转时钟和数据线优点是随便挑几个引脚就能用缺点是每发一个字节都要CPU去精确控制电平时序频率再高也高不到哪去。硬件SPI则是把数据交给STM32的SPI外设由硬件外设按设定的时钟速率自动移位输出CPU只需要把数据填进去一个字节一个字节地往外送。在F103C8T6这种主频72MHz的芯片上硬件SPI跑到4.5Mbps、9Mbps都很轻松软件SPI能做到2Mbps就算不错。OLED刷一帧128x64全屏就是1024字节数据速率差了4倍肉眼可见刷新率差别。另外硬件SPI后面接DMA也更顺畅软件SPI想优化都没得优化。所以只要芯片有硬件SPI我一般直接用它项目里这套方案跟HAL库配合非常成熟。2. CubeMX与引脚接线SH1106硬件SPI的底子怎么打2.1 屏幕接口和F103的引脚分配SH1106的屏幕模组通常是7个引脚VCC、GND、SCL、SDA、RES、DC、CS。也有板子把DC写成D/C把SCL写成SCK把SDA写成DIN本质是一样的。SCL接SPI时钟SDA接SPI主输出数据线RES做复位DC决定当前发的是命令还是数据CS做片选。我常用的一组引脚分配如下信号OLED引脚STM32引脚说明VCCVCC3.3V模块供电GNDGNDGND必须共地SCLSCL/SCKPA5SPI1_SCKSDASDA/DINPA7SPI1_MOSIDCDCPB1数据/命令选择RESRES/RSTPB0复位控制CSCSPA4片选控制这套引脚避开了SWD调试用的PA13/PA14也不占用USART1的PA9/PA10日常调试和串口打印都不冲突。你当然可以把DC、RES、CS换到其他任意GPIO上只要记得CubeMX里把它们配成普通推挽输出并且在回调里改成对应引脚宏就行。2.2 SPI模式与时钟频率怎么选CubeMX里把SPI1配成Full-Duplex Master其余几个参数经常让新手纠结其实照下面填就行Hardware NSS SignalDisableClock Prescaler先选32即2.25Mbps左右Clock Polarity (CPOL)LowClock Phase (CPHA)1 EdgeData Size8 BitsFirst BitMSB First这里解释两个关键点。第一硬件NSS一定不要开。如果CubeMX里选了硬件NSSSTM32会在某些条件下自动拉低片选引脚但我们的CS是普通GPIO控制的硬件和软件会互相打架屏幕表现就是偶尔闪着闪着突然黑了。第二SPI模式选择。SH1106手册上支持SPI Mode 0和Mode 3大多数模块用Mode 0就行但不同板厂的走线和电平设计会导致兼容差异。如果显示乱码直接把CPOL改成High、CPHA改成2 Edge再试很多杂牌屏就是Mode 3才正常。时钟频率方面先别急着拉满。我实测F103在9Mbps下近距离短排线确实能跑但一旦排线超过15厘米或者有干扰就很容易出现随机花点。先把Prescaler放到32跑通后再逐步提高这是排错时的基本节奏。2.3 电平与供电的三个检查点SH1106模组基本都是3.3V逻辑电平和F103的GPIO电平兼容PA5、PA7可以直接连。但供电上有几个细节容易翻车有的模组板载了LDO板子标注可以接5V这没问题但如果你从3.3V引脚单独给OLED供电就千万不要再往VCC接5V。STM32和OLED必须共地。如果GND不共SPI信号电平就没有参考点屏幕会乱码甚至完全不亮。屏幕全亮时电流能到20mA以上别从某个排针的弱电源脚取电建议从3.3V主电源处接必要时在屏幕VCC和GND之间并一个10uF电容。只要接线这部分没搞错后面移植u8g2时基本不会出现“屏幕完全没反应”的玄学问题。3. 把u8g2源码编进工程目录、编译器与C993.1 该拷贝哪些文件、工程结构如何安排去u8g2的GitHub仓库下载release版本解压后真正需要的是csrc目录里面是u8g2完整的C源码、头文件、字库和所有屏幕控制器的驱动实现。不要只拷贝u8g2.h和自己的屏幕驱动.c因为Setup函数会调用一整张链表里的函数少一个文件链接时就可能报undefined symbol。我的工程结构很直接Drivers/ CMSIS/ STM32F1xx_HAL_Driver/ Core/ Inc/ Src/ ThirdParty/ u8g2/ csrc/ u8g2.h u8x8.h u8g2_d_setup.c u8g2_d_memory.c u8x8_d_sh1106_128x64.c u8x8_byte.c u8x8_gpio.c u8g2_font.c ... 其他.c App/ u8g2_hal.c u8g2_hal.h把整个csrc目录拷进工程然后在IDE里把csrc下的所有.c文件全部加入编译。u8g2的标准字库和全屏驱动一起编译Keil下大概会多出几十秒编译时间属于正常现象。3.2 Keil和CubeIDE的操作差异如果用的是Keil MDK操作流程是在工程树里新建一个Group叫u8g2然后右键Add Existing Files把csrc目录下所有.c文件选进去。接着在Options for Target - C/C - Include Paths里加上ThirdParty/u8g2/csrc。这一步漏掉的后果是头文件找不到编译直接红成一片。如果用的是STM32CubeIDE拷贝文件后要右键工程点Refresh让IDE感知新文件。然后在Project Properties - C/C Build - Settings - MCU GCC Compiler - Dialect里把语言标准改成gnu99或c99并且在Include paths里添加csrc路径。CubeIDE对Makefile的管理比较自动化刷新后新加的文件通常会被自动扫到。3.3 编译器选项和经典报错u8g2源码对C语言标准有要求。Keil MDK使用AC5编译器时默认不是C99编译u8g2源码通常会报这类错误error: #268: declaration may not appear after executable statement in block意思是在可执行语句后面才声明变量这属于C89的限制而u8g2大量使用了C99风格的for循环内声明。解决方案很明确在Options for Target - C/C选项卡里把C99 Mode前面的勾打上。如果你用AC6或者CubeIDE默认C99兼容就好很多基本不会遇到这个问题。另外提醒一句如果工程开启了微库MicroLIB某些情况下HAL库的HAL_SPI_Transmit内部会使用va_list等机制和MicroLIB的兼容性也偶发问题。我习惯是直接关掉MicroLIB用标准C运行库省得后面踩奇怪的坑。4. 移植心脏HAL底层回调是怎么写出来的4.1 回调机制不只是“发一个字节”很多人在网上搜STM32移植u8g2最后卡住的原因是把回调理解成了“一个发送函数”。实际上u8g2的byte回调是一个消息分发器它接收不同的msg执行不同的动作U8X8_MSG_BYTE_SEND表示发送数据U8X8_MSG_BYTE_SET_DC表示切换DC电平U8X8_MSG_BYTE_SET_CS表示切换片选。同一套机制在I2C模式下还会收到地址消息在软件SPI模式下还会收到时钟和数据引脚的翻转消息。理解了这个设计你就知道为什么不能只写一个spi_send(buf, len)就完事。u8g2在发送一包数据前需要先设置DC引脚表示“接下来是命令还是数据”再拉低CS选中屏幕然后才真正发数据。这些动作都被折算成了byte回调里的不同消息所以我们的回调代码必须把每种消息都处理到。4.2 byte回调CS、DC和数据发送下面是我在HAL库下经过实测的byte回调写法文件放在App/u8g2_hal.c里#include main.h #include u8g2.h extern SPI_HandleTypeDef hspi1; #define OLED_DC_GPIO_Port GPIOB #define OLED_DC_Pin GPIO_PIN_1 #define OLED_CS_GPIO_Port GPIOA #define OLED_CS_Pin GPIO_PIN_4 #define OLED_RES_GPIO_Port GPIOB #define OLED_RES_Pin GPIO_PIN_0 uint8_t u8x8_byte_stm32_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { HAL_StatusTypeDef status HAL_OK; switch (msg) { case U8X8_MSG_BYTE_SEND: status HAL_SPI_Transmit(hspi1, (uint8_t *)arg_ptr, (uint16_t)arg_int, 100); break; case U8X8_MSG_BYTE_SET_DC: HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; case U8X8_MSG_BYTE_SET_CS: HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; case U8X8_MSG_BYTE_INIT: case U8X8_MSG_BYTE_SET_I2C_ADDR: case U8X8_MSG_BYTE_START_TRANSFER: case U8X8_MSG_BYTE_END_TRANSFER: break; default: return 0; } return (status HAL_OK) ? 1 : 0; }几个细节说明一下。U8X8_MSG_BYTE_SEND里的arg_int是本次待发送字节数arg_ptr是数据指针。HAL库的HAL_SPI_Transmit第三个参数是uint16_t长度u8g2单次发送上限是255字节所以直接强转没问题。超时时间给了100ms。正常情况下发一包数据最多几毫秒如果SPI配置错误或总线被拉死100ms超时能让系统尽早返回不至于卡死在HAL库内部。不同的u8g2版本消息名可能从U8X8_MSG_BYTE_SET_DC变成别的变体建议打开csrc/u8x8.h确认一下当前版本里的宏定义。版本差异是这篇文章里最容易被忽略的坑我见过有人在网上抄了老代码结果新版本里消息名已经改了屏幕死活不亮。4.3 GPIO与延时回调RESET和HAL_Delay第二个回调管GPIO和延时核心是复位引脚和毫秒延时uint8_t u8x8_stm32_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch (msg) { case U8X8_MSG_DELAY_MILLI: HAL_Delay(arg_int); break; case U8X8_MSG_GPIO_AND_DELAY_INIT: HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_SET); break; case U8X8_MSG_GPIO_RESET: HAL_GPIO_WritePin(OLED_RES_GPIO_Port, OLED_RES_Pin, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; default: break; } return 1; }CS和DC我放在byte回调里管理但在初始化消息里先把它们都拉高避免上电瞬间屏幕处于未定义状态。RESET引脚则由这个回调负责u8g2初始化屏幕时会按SH1106要求的时序对RES给脉冲对应到HAL_GPIO_WritePin即可。每个HAL_Delay调用都会阻塞CPU实际只有上电初始化时才有几次毫秒级延时平时绘图不会触发太多所以不用太担心性能。4.4 初始化与第一次点亮在main.c里定义一个全局的u8g2_t u8g2;然后在MX_GPIO_Init()和MX_SPI1_Init()之后调用u8g2_Setup_sh1106_128x64_f_hw_spi(u8g2, U8G2_R0, u8x8_byte_stm32_hw_spi, u8x8_stm32_gpio_and_delay); u8g2_InitDisplay(u8g2); u8g2_SetPowerSave(u8g2, 0); u8g2_ClearBuffer(u8g2); u8g2_DrawStr(u8g2, 0, 14, u8g2 OK); u8g2_SendBuffer(u8g2);这里最容易忘的是u8g2_SetPowerSave(u8g2, 0)参数0表示关闭省电模式让屏幕真正进入显示状态。如果漏掉屏幕可能初始化了但一直黑着。U8G2_R0表示不旋转如果屏幕装歪了可以换U8G2_R90、U8G2_R180、U8G2_R270做软件旋转。5. 黑屏、花屏、乱码实测中的排查顺序5.1 用软件SPI互相对照快速定位是屏还是SPI如果你按上面的代码接线和配置大概率一次点亮。万一黑屏第一个建议不是去怀疑SH1106驱动代码而是先换个软件SPI的Setup函数验证屏幕本身。u8g2的软件SPI移植只需要在GPIO回调里增加时钟和数据引脚翻转代码量也不大。把Setup函数临时换成u8g2_Setup_sh1106_128x64_f_sw_spi(u8g2, U8G2_R0, u8x8_byte_4wire_sw_spi, u8x8_stm32_gpio_and_delay);软件SPI一旦能用说明屏幕模块、供电、RESET、DC、CS全部正常问题就锁定在硬件SPI配置上比如模式、频率、引脚映射。如果软件SPI也黑屏那大概率是接线、供电或者初始化时序问题。这种“硬件SPI与软件SPI互相对照”的排查思路能瞬间把问题范围缩小一半比盲猜快太多。5.2 时钟极性、频率、回路上拉逐个排查花屏和乱码的典型原因优先级如下时钟太快。把Prescaler改到64或128先把速率降到1Mbps左右。如果好了就是信号质量或屏体质问题。SPI模式不对。CPOL/CPHA在Mode 0和Mode 3之间切换多数情况是其中一个才对。接线太长或杜邦线质量差。SCK和MOSI两根线尽量短不要和电源线捆在一起。电压不稳。OLED全亮时电流突变如果3090芯片压降太大就会出现随机花点。串个10uF电容在屏电源上很管用。还有一种比较隐蔽的CubeMX里MX_SPI1_Init()调用顺序在MX_GPIO_Init()之前GPIO还没初始化好SPI外设虽然启动了但引脚AF还没配对。CubeMX生成的代码默认先GPIO后外设但手动调整过初始化顺序的工程要特别注意。5.3 显示中文乱码/缺字Flash容量和字库问题u8g2自带一整套字体英文数字的字体都很大但中文全量字体是真正的“Flash杀手”。比如u8g2_font_unifont_t_chinese1一个字体Rom就非常大F103C8T6的64KB Flash很可能直接放不下编译时出现region FLASH overflowed。如果你只是想在128x64屏幕上显示几个中文字有几个思路换大容量芯片比如F103RCT6的256KB Flash或者改用F401等Flash更大的芯片。选GB2312子集字体比如u8g2_font_wqy12_t_gb2312体积比Unicode全量小很多但也要看具体版本。只保留自己用到的少数汉字做成小字模数组u8g2也支持直接用u8g2_DrawXBM绘制自制的字模位图。我实际做产品时如果只需要十几个菜单汉字就抽字模如果需求是灵活输入任意中文干脆换256KB Flash以上的芯片。这个取舍一定要在上板之前考虑清楚不然整个图形界面写好了才发现放不进去返工成本很高。5.4 顺便聊聊ST-Link连接失败移植过程中还有一类问题发生在烧录环节程序跑飞或引脚配置混乱后再次点击下载Keil或CubeIDE直接报类似“no stm32 target found”的错误。多数情况是SWD引脚被程序复用成了普通GPIO或者板子处于低功耗/崩溃状态调试器抓不到内核。处理办法很简单先用STM32CubeProgrammer选择“Connect Under Reset”模式按住板子上的复位键不放点击连接保持住复位状态直到连接成功后松开再擦除Flash。如果这样还连不上就检查ST-Link和板子之间的SWDIO、SWCLK、GND三根线以及目标板是否供电正常。这个坑和u8g2本身无关但很多人在OLED调通后顺手把PA13/PA14配置改掉结果再想烧录就傻眼了所以我每次移植工程都会提醒一句调试引脚尽量不要动。6. 刷新率、DMA与缓冲区选择再做一层优化6.1 full buffer和page buffer怎么选u8g2的Setup函数名里那个f、1、2指的是缓冲区策略后缀缓冲区大小RAM占用特点f整屏128x641024字节绘制和发送逻辑最简单11行页(128x8)128字节RAM占用极小绘制多轮22行页256字节折中方案F103C8T6本身有20KB SRAMfull buffer的1024字节完全无压力所以我直接用f版本。只有在STM32F030这种RAM只有4KB、还要跑不少业务变量的场景下才需要改成1或2的page buffer。使用1或2时渲染代码通常写成u8g2_FirstPage/u8g2_NextPage的循环使用f时可以写成u8g2_ClearBuffer/u8g2_SendBuffer。为了让代码在两种buffer策略之间可切换我一般统一写成FirstPage/NextPage循环这样换屏换buffer都不需要改绘制逻辑。6.2 刷新率实测数据和CPU占用我实际测量了一下SPI时钟配置为4.5Mbps时向SH1106发送1024字节全屏数据理论时间大约1.8ms加上u8g2内部命令和GPIO切换实测单帧刷新大概3ms左右。这个速度跑动画完全够用至少30fps以上是稳的。但要注意u8g2绘制复杂图形时CPU时间不只花在SPI发送上字库解压、像素填充都要占CPU。比如绘制大号抗锯齿字体一帧能从1ms飙到10ms。所以优化不能只看SPI。如果发现绘图慢第一选择是检查是不是字体和绘制API用得太重而不是急着上DMA。6.3 DMA方案怎么做才不翻车DMA能让SPI发送不占CPU听上去很美好实际操作要小心buffer共用问题。u8g2的U8X8_MSG_BYTE_SEND是同步语义它默认希望回调把数据发完再返回以便紧接着切换CS或DC引脚。如果你在回调里用HAL_SPI_Transmit_DMA启动传输后立刻返回u8g2下一步可能就会去操作CS等于在SPI还没发完的时候提前收了片选数据会丢失。稳妥的做法是在DMA发送启动后用HAL_SPI_GetState或等待DMA传输完成的标志等到实际传完再返回。这样DMA只节省了CPU的逐字节搬运对u8g2来说依然是同步阻塞调用。真正想做到后台刷新就得自己维护双缓冲和一套异步发送队列u8g2画到buffer A时DMA在后台发送buffer B等两帧都完成后交换。这个方案能提升界面流畅度但代码复杂度明显上升。我的建议是先跑通同步版本确认显示和功能都没问题再根据实际帧率瓶颈决定是否上异步方案。最后分享一个我自己的使用习惯u8g2的底层回调单独放在u8g2_hal.c里跟业务绘制代码完全分开。以后换屏幕型号只需要改Setup那一行和引脚宏其他地方一字不动换到其他带硬件SPI的MCU平台也只是重写这两个回调。这次用SH1106_128x64加硬件SPI跑通之后我又拿同一套代码去驱动SSD1306的128x32屏幕改一行Setup函数就亮起来了。希望这篇记录能帮你在STM32上少踩几个坑早点把界面跑起来。本文还有配套的精品资源点击获取
返回列表