简介这份资源面向嵌入式开发初学者与需要上手OLED显示的工程师提供STM32F407平台下通过SPI接口驱动OLED显示屏的完整工程实例重点解决从STM32F103平台移植驱动时的时钟配置、GPIO初始化与SPI参数匹配问题。压缩包共159个文件、约9.39MB以C源文件与头文件为主体辅以Keil工程配置、编译中间文件及可直接烧录的hex与axf固件涵盖HAL库外设驱动、OLED初始化、帧缓冲区管理与字符图形绘制等模块。资源围绕SSD1306类驱动IC的指令与数据传输展开封装了清屏、坐标设置、写字符与画点线矩形等API代码结构清晰、注释完整便于理解SPI通信机制与显示驱动分层设计。目前已有3833人学习下载适合作为嵌入式显示开发的参考范例与移植排错对照。1. 一块 0.96 寸屏点亮之后才算真正摸到 STM32F407 的边很多人第一次拿到 STM32F407 开发板跑通串口打印“Hello World”就以为入门了直到把一块 0.96 寸 OLED 接上去屏幕一片漆黑才开始怀疑人生。这个项目就是围绕 STM32F407 驱动 OLED 显示展开的完整工程核心走的是 SPI 接口把屏幕点亮、字符显示、图形绘制这一整套链路跑通。它适合刚上手 F407、想搞明白“代码到底怎么把像素点送到屏上”的嵌入式新手也适合需要给产品加一块小屏做状态显示、又不想引入复杂 GUI 框架的开发者。SPI 驱动 OLED 这件事看着简单真拆开看时序、显存映射、字模取模三块哪一块翻车屏幕都不会给你好脸色。2. 工程结构与技术选型SPI 四线驱动 OLED 的最小闭环2.1 为什么选 SPI 而不是 I2C0.96 寸 OLED 常见的控制芯片是 SSD1306它同时支持 I2C 和 SPI 两种接口。这个工程选 SPI原因很实际I2C 在 400kHz 标准速率下刷一整屏 128×64 的数据要几十毫秒滚动或者动画会明显卡顿SPI 挂到 F407 的硬件 SPI 上时钟轻松跑到 10MHz 以上整屏刷新压到几毫秒视觉上就顺滑了。代价是多占两根线DC 和 RES但对于 F407 这种引脚富裕的片子这点开销可以忽略。工程里 SPI 用的是硬件 SPI1引脚分配大致是SCK 走 PA5MOSI 走 PA7DC 和 RES 用普通 GPIO 控制。这里有个容易忽略的点——SSD1306 的 SPI 是单向写没有 MISO所以配置 SPI 时要把方向设成单线发送模式别傻乎乎地配成全双工等着收数据。2.2 目录结构长什么样一个能跑的 F407 OLED 工程目录通常是这样组织的STM32F407_OLED/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── oled.h // OLED 驱动对外接口 │ │ └── oledfont.h // 字模数据 │ └── Src/ │ ├── main.c │ ├── oled.c // 驱动实现 │ └── stm32f4xx_it.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F4xx_HAL_Driver/ ├── OLED.ioc // CubeMX 工程文件 └── MDK-ARM/ // Keil 工程oled.c和oled.h是整个项目的核心oledfont.h里放的是 ASCII 字模和常用图形点阵。这种分层的好处是驱动和业务彻底分开main.c里只管调OLED_ShowString()这类接口换屏幕型号时只动oled.c上层逻辑不用碰。2.3 驱动分层从 GPIO 到像素的三层抽象拆开看这个驱动其实分三层。最底层是OLED_WR_Byte()负责把一个字节按 SPI 时序送出去同时根据是命令还是数据拉高拉低 DC 引脚中间层是OLED_Set_Pos()和OLED_Refresh()管的是 SSD1306 的页地址和列地址以及把本地显存刷到屏幕最上层是OLED_ShowChar()、OLED_ShowString()、OLED_DrawBMP()这些面向业务的接口。这种分层的价值在于底层时序只写一次上层想画什么就画什么。我见过不少人把所有逻辑塞进一个函数里结果想加个画圆功能就得复制粘贴一大段 SPI 发送代码改起来是血泪经验。3. 核心代码拆解一个字节怎么变成屏幕上的一个像素3.1 底层写字节DC 引脚决定命运SSD1306 的 SPI 协议里每个字节前面靠 DC 引脚区分是命令还是数据DC 拉低表示接下来是命令拉高表示是显示数据。这个函数是整个驱动的地基// 向 OLED 写入一个字节 // dat: 要写入的数据 // cmd: 0 表示命令, 1 表示数据 void OLED_WR_Byte(uint8_t dat, uint8_t cmd) { if (cmd) OLED_DC_Set(); // DC 拉高, 表示数据 else OLED_DC_Clr(); // DC 拉低, 表示命令 OLED_CS_Clr(); // 片选拉低, 选中屏幕 HAL_SPI_Transmit(hspi1, dat, 1, 100); // 硬件 SPI 发送 OLED_CS_Set(); // 片选拉高, 结束本次传输 }HAL_SPI_Transmit的第四个参数是超时时间单位毫秒这里给 100 足够。如果屏幕没反应第一件事就是拿逻辑分析仪或者示波器看 SCK 上有没有波形——没有波形说明 SPI 根本没初始化成功有波形但屏幕不亮多半是 DC 或者 RES 的时序不对。OLED_CS_Clr()和OLED_CS_Set()这对操作如果屏幕的 CS 直接接地很多模块默认如此其实可以省掉但保留着更通用。3.2 显存映射128×64 为什么是 1024 字节SSD1306 的显存不是按“一行 128 个像素”线性排的而是分成 8 个 Page每个 Page 高 8 像素、宽 128 列。所以整屏显存是 8×128 1024 字节每个字节的 8 个 bit 对应垂直方向的 8 个像素。这个映射关系是新手最容易懵的地方// 本地显存, 1024 字节对应 128x64 屏幕 uint8_t OLED_GRAM[8][128]; // 设置显示位置: x 为列(0~127), y 为页(0~7) void OLED_Set_Pos(uint8_t x, uint8_t y) { OLED_WR_Byte(0xB0 y, OLED_CMD); // 设置页地址 OLED_WR_Byte(((x 0xF0) 4) | 0x10, OLED_CMD); // 设置列地址高 4 位 OLED_WR_Byte(x 0x0F, OLED_CMD); // 设置列地址低 4 位 }列地址要拆成高低两半分别发这是 SSD1306 命令格式决定的0x10是列地址高位的命令前缀0x00是低位的。写错这里画面会整体偏移或者错位我踩过这个坑调了半天以为是字模问题。3.3 刷新函数把本地显存推上屏有了本地显存画图时只改OLED_GRAM最后统一调一次刷新这样能避免边画边刷导致的闪烁// 将本地显存刷新到屏幕 void OLED_Refresh(void) { uint8_t i, n; for (i 0; i 8; i) { OLED_Set_Pos(0, i); // 定位到第 i 页起始列 for (n 0; n 128; n) { OLED_WR_Byte(OLED_GRAM[i][n], OLED_DATA); // 逐字节发送 } } }外层循环 8 页内层 128 列一共 1024 次写数据。如果 SPI 跑在 10MHz理论上 1024 字节不到 1 毫秒就能传完实际加上函数调用开销也就几毫秒。这里有个优化点可以把OLED_Set_Pos提到内层循环外因为 SSD1306 写完一个字节后列地址会自动加一不需要每写一个字节都重设位置。工程里为了代码清晰保留了逐字节设置追求极致刷新的可以自己改。3.4 字符显示字模怎么对上像素显示一个 ASCII 字符本质是把字模数组里的字节按列写进显存。以 8×16 字体为例一个字符占 16 字节8 列 × 2 页// 显示一个 8x16 的 ASCII 字符 // x: 列坐标(0~127), y: 页坐标(0~7), chr: 字符 void OLED_ShowChar(uint8_t x, uint8_t y, char chr) { uint8_t i; uint8_t c chr - ; // 字模从空格开始编号 for (i 0; i 8; i) { // 上半部分: 第 y 页 OLED_GRAM[y][x i] oledfont_8x16[c * 16 i]; // 下半部分: 第 y1 页 OLED_GRAM[y 1][x i] oledfont_8x16[c * 16 i 8]; } }chr - 是因为字模表通常从空格ASCII 32开始存减去偏移才能对上索引。c * 16定位到该字符字模的起始位置前 8 字节是上半部分后 8 字节是下半部分。如果显示出来是乱码或者上下颠倒八成是字模取模方式不对——取模软件里要选“列行式”还是“行列式”、字节是高位在前还是低位在前必须和代码里的写入顺序一致这是玄学重灾区。4. 从零跑起来CubeMX 配置到屏幕点亮4.1 CubeMX 里 SPI 和 GPIO 怎么配打开OLED.iocSPI1 的配置要点Mode 选 Transmit Only Master单线发送Data Size 8 BitsCPOL 和 CPHA 按 SSD1306 手册选通常 CPOLHigh、CPHA2Edge也就是 SPI 模式 3Prescaler 先给个 8 分频F407 的 APB2 是 84MHz8 分频后约 10.5MHz稳一点。NSS 选 Software因为片选我们自己用 GPIO 控制。DC、RES、CS 三个引脚配成 GPIO_Output初始电平RES 拉高、DC 随意、CS 拉高。生成代码后在main.c的MX_GPIO_Init()之后、while(1)之前加上初始化和测试代码OLED_Init(); // 屏幕初始化 OLED_Clear(); // 清屏 OLED_ShowString(0, 0, STM32F407, 16); // 第 0 页显示字符串 OLED_ShowString(0, 2, OLED SPI, 16); // 第 2 页显示 OLED_Refresh(); // 刷新上屏OLED_Init()里是一串 SSD1306 的初始化命令包括关显示、设时钟分频、设复用率、设对比度、开电荷泵等等这些命令值直接照搬数据手册的推荐配置即可不用自己发明。4.2 初始化序列里几个不能省的命令初始化命令里0x8D后面跟0x14是开启电荷泵这条不写屏幕永远不亮因为 OLED 需要内部升压才能驱动像素0xAF是开显示写在最后0x20, 0x00是设置内存寻址模式为水平寻址。这几条是新手最容易漏的漏了电荷泵现象就是“代码全对但屏幕全黑”特别打击人。4.3 编译下载与现象确认Keil 里选好 F407 对应的器件型号Debug 选 ST-Link 或 J-Link编译下载。上电后如果屏幕先闪一下再显示字符说明 RES 复位时序正常如果一直黑先量 RES 引脚有没有一个低电平脉冲再量电荷泵相关——不过电荷泵是内部的量不到只能靠命令确认。显示正常后可以试着改OLED_ShowString的坐标参数观察字符位置变化确认坐标映射理解无误。5. 避坑与排查屏幕不亮、花屏、乱码的常见原因5.1 屏幕全黑代码编译无报错现象下载后屏幕一点反应没有串口打印正常。原因多半是初始化命令里漏了电荷泵开启0x8D, 0x14或者 RES 引脚没有正确复位。解决先确认OLED_Init()里电荷泵命令存在再用万用表量 RES 引脚上电时是否有短暂低电平没有的话检查 GPIO 初始化和复位函数调用顺序。5.2 显示花屏或者雪花点现象屏幕亮但内容是乱的像雪花。原因通常是 SPI 速率太高导致时序不稳或者 CPOL/CPHA 配错。解决把 SPI 分频调大比如 16 分频降到 5MHz 左右先排除速率问题再核对 SPI 模式是否与 SSD1306 要求一致。我一般会先降速验证能显示再逐步提频这是排查时序问题的后悔药。5.3 字符显示上下颠倒或左右镜像现象字能显示但方向不对。原因在字模取模设置和代码写入顺序不匹配。解决回到取模软件确认取模方向纵向取模/横向取模、字节位序高位在前/低位在前与OLED_ShowChar里的写入逻辑一致。改代码比改字模快实在对不上就调整OLED_GRAM[y][xi]的索引方式试。5.4 刷新后画面闪烁现象每次刷新整屏有明显闪动。原因是先清屏再画再刷新的顺序导致中间出现空白帧。解决改成只改显存、最后统一OLED_Refresh()不要在绘制过程中调用清屏如果必须清先清本地OLED_GRAM再重绘别直接发清屏命令给屏幕。6. 二次开发把驱动改成自己的显示框架拿到这个工程后最值得做的改造是把“逐字符显示”升级成“带缓冲的文本层”。具体做法是维护一个字符缓冲区业务层只往里写字符串刷新时统一渲染这样滚动字幕、状态栏局部刷新都好实现。另一个方向是加图形接口基于OLED_GRAM写OLED_DrawLine()和OLED_DrawRect()用 Bresenham 算法画线几十行代码就能让屏幕从“只能显示字”变成“能画简单界面”。验证改造是否成功我习惯用三步先画一个边框确认坐标系统正确再在边框内显示递增数字确认刷新不闪最后快速切换两帧不同内容看有没有残影。残影说明清屏逻辑有漏坐标错位说明页列映射没吃透。改造方向改动文件验证方法文本缓冲层oled.c / oled.h滚动字幕无撕裂图形绘制oled.c 新增函数画矩形边框四角对齐多字体支持oledfont.h切换字号不串位有个教训我记到现在早期我图省事在中断里直接调OLED_Refresh()结果主循环里的显存写入被中断打断屏幕偶尔出现半帧旧数据。从那以后我每次加显示相关代码都强制走一遍“显存只在一个上下文改、刷新只在主循环调”的规矩再没出过撕裂。希望这些拆解能帮到你把这块小屏稳稳点起来。本文还有配套的精品资源点击获取