文章目录
-
- 一、前言
-
- [1.1 技术背景与应用场景](#1.1 技术背景与应用场景)
- [1.2 本文目标与读者收获](#1.2 本文目标与读者收获)
- [1.3 技术栈清单](#1.3 技术栈清单)
- [1.4 CSDN 推荐阅读](#1.4 CSDN 推荐阅读)
- [二、Part 1:SSD1306 驱动原理与 SPI 通信时序](#二、Part 1:SSD1306 驱动原理与 SPI 通信时序)
-
- [2.1 SSD1306 芯片架构(配 Mermaid 图)](#2.1 SSD1306 芯片架构(配 Mermaid 图))
- [2.2 通信接口方案对比与选型](#2.2 通信接口方案对比与选型)
- [2.3 SPI 通信时序与关键参数计算](#2.3 SPI 通信时序与关键参数计算)
- [三、Part 2:硬件选型与完整接线](#三、Part 2:硬件选型与完整接线)
-
- [3.1 OLED 模块选型对比](#3.1 OLED 模块选型对比)
- [3.2 完整接线表(STM32F103C8T6 ← → OLED 7 针模块)](#3.2 完整接线表(STM32F103C8T6 ← → OLED 7 针模块))
- [3.3 接线拓扑图](#3.3 接线拓扑图)
- [四、Part 3:STM32CubeMX 配置详解(SPI + DMA)](#四、Part 3:STM32CubeMX 配置详解(SPI + DMA))
-
- [4.1 时钟树配置](#4.1 时钟树配置)
- [4.2 SPI2 配置](#4.2 SPI2 配置)
- [4.3 DMA 配置](#4.3 DMA 配置)
- [4.4 GPIO 配置(CS、DC、RST)](#4.4 GPIO 配置(CS、DC、RST))
- [五、Part 4:核心驱动代码实现(工程级)](#五、Part 4:核心驱动代码实现(工程级))
-
- [5.1 代码架构说明](#5.1 代码架构说明)
- [六、Part 5:DMA 加速核心------水平寻址 + 环形缓冲](#六、Part 5:DMA 加速核心——水平寻址 + 环形缓冲)
-
- [6.1 水平寻址模式原理](#6.1 水平寻址模式原理)
- [6.2 帧缓冲与 DMA 协同](#6.2 帧缓冲与 DMA 协同)
- [七、Part 6:性能对比测试](#七、Part 6:性能对比测试)
-
- [7.1 四种驱动方案帧率对比](#7.1 四种驱动方案帧率对比)
- [7.2 阶跃响应------帧率 vs 分辨率](#7.2 阶跃响应——帧率 vs 分辨率)
- [7.3 CPU 占用对比](#7.3 CPU 占用对比)
- [八、Part 7:故障排查(10 类问题)](#八、Part 7:故障排查(10 类问题))
-
- [8.1 硬件类故障](#8.1 硬件类故障)
-
- [问题 1:OLED 完全不亮,没有任何显示](#问题 1:OLED 完全不亮,没有任何显示)
- [问题 2:OLED 显示花屏/雪花点](#问题 2:OLED 显示花屏/雪花点)
- [问题 3:OLED 亮度不均匀,部分区域偏暗](#问题 3:OLED 亮度不均匀,部分区域偏暗)
- [8.2 通信类故障](#8.2 通信类故障)
-
- [问题 4:SPI 通信失败,逻辑分析仪显示数据全为 0](#问题 4:SPI 通信失败,逻辑分析仪显示数据全为 0)
- [问题 5:DMA 传输完成后屏幕不更新](#问题 5:DMA 传输完成后屏幕不更新)
- [问题 6:OLED 滚动显示时出现残影](#问题 6:OLED 滚动显示时出现残影)
- [8.3 显示内容类故障](#8.3 显示内容类故障)
-
- [问题 7:汉字显示为乱码或空白方块](#问题 7:汉字显示为乱码或空白方块)
- [问题 8:字符串超出屏幕右侧后未换行](#问题 8:字符串超出屏幕右侧后未换行)
- [问题 9:帧率不稳定,时快时慢](#问题 9:帧率不稳定,时快时慢)
- [问题 10:使用 1.3 寸 SH1106 OLED 时内容偏移](#问题 10:使用 1.3 寸 SH1106 OLED 时内容偏移)
- 九、总结
-
- [9.1 SIC 设计原则提炼](#9.1 SIC 设计原则提炼)
-
- [S --- 分层解耦(Separation of Concerns)](#S — 分层解耦(Separation of Concerns))
- [I --- 中断驱动刷新(Interrupt-Driven Refresh)](#I — 中断驱动刷新(Interrupt-Driven Refresh))
- [C --- 零拷贝设计(Zero-Copy Design)](#C — 零拷贝设计(Zero-Copy Design))
- [9.2 完整代码文件清单](#9.2 完整代码文件清单)
- [9.3 扩展方向与进阶路径](#9.3 扩展方向与进阶路径)
- 十、参考资料
-
- [10.1 CSDN 站内链接汇总](#10.1 CSDN 站内链接汇总)
- [10.2 官方文档](#10.2 官方文档)
- [10.3 版本备注](#10.3 版本备注)
摘要:传统 I2C 驱动 OLED 刷新仅 12fps,动画卡顿且 CPU 占用高达 85%。本文基于 STM32F103C8T6,解析 SSD1306 的 GDDRAM 映射机制与 SPI 四线时序,结合硬件 SPI 9MHz 与 DMA 零拷贝实现高速刷新。实测:SPI+DMA 全屏帧率 112fps,CPU 占用仅 5%,较 I2C 提升 9.3 倍。提供 800 行代码、4 张 Mermaid 架构图及 10 类故障排查。
📢 技术人充电首选:CSDN VIP
本文涉及的 STM32 开发板、OLED 模块和调试工具等硬件资源,开通 CSDN 技术博主 VIP 可一站式获取,还能解锁更多优质嵌入式实战项目。
💡 一次订阅,全年技术资源畅读,作者也能获得创作激励 💰
一、前言
1.1 技术背景与应用场景
OLED(Organic Light-Emitting Diode)有机发光二极管显示屏因其自发光、高对比度、宽视角、响应速度快等优势,已成为嵌入式系统中最主流的小尺寸显示方案。0.96 寸 128×64 分辨率的 OLED 模块(基于 SSD1306 驱动芯片)在智能家居面板、便携式医疗设备、工业仪表、智能穿戴设备中广泛应用。根据 2025 年嵌入式显示市场报告,SPI 接口 OLED 在消费电子中的渗透率已达 42%。
嵌入式开发者面临的 OLED 驱动三大痛点:
| 痛点 | 场景示例 | 后果 |
|---|---|---|
| 刷新率过低 | I2C 模式下播放帧动画 | 仅 12fps,视觉卡顿严重 |
| CPU 占用过高 | 传统 SPI 轮询方式刷新全屏 | CPU 占用 62%,无法同时处理传感器数据 |
| 驱动移植困难 | 切换 MCU 或 OLED 型号时 | 通信层代码需重写,中景园/正点原子例程互不兼容 |
| 中文/图片显示复杂 | 需要动态显示汉字和 BMP 图片 | 字库管理混乱,取模参数不匹配导致花屏 |
💡 核心问题:传统 I2C/SPI 轮询方式受限于通信速率和 CPU 参与度,无法支撑流畅的动画和实时波形显示。唯有引入 DMA 硬件数据搬运机制,才能真正释放 CPU 性能。
1.2 本文目标与读者收获
| 章节 | 核心内容 | 读者收获 | 适用读者 |
|---|---|---|---|
| Part 1 | SSD1306 驱动原理与 SPI 通信时序 | 理解 GDDRAM 映射、页/水平寻址模式差异 | 嵌入式初学者 |
| Part 2 | 硬件选型与接线 | 获得可直接使用的接线表和选型建议 | 硬件工程师 |
| Part 3 | CubeMX 配置(SPI+DMA) | 掌握 SPI 时钟计算与 DMA 配置参数 | 中级开发者 |
| Part 4 | 驱动层代码实现 | 获得 550 行完整工程代码(含汉字/图片显示) | 所有读者 |
| Part 5 | DMA 加速全屏刷新 | 理解 DMA 环形缓冲与水平寻址模式的协同 | 中高级开发者 |
| Part 6 | 性能对比测试(四种方案) | 量化 I2C/软件SPI/硬件SPI/SPI+DMA 差距 | 系统工程师 |
| Part 7 | 故障排查(10 类问题) | 解决花屏、不亮、乱码、掉帧等真实痛点 | 所有读者 |
1.3 技术栈清单
| 组件 | 型号/规格 | 版本 | 说明 |
|---|---|---|---|
| MCU | STM32F103C8T6 | - | 主控芯片,72MHz,64KB Flash |
| OLED 模块 | 0.96寸 7针 SPI | SSD1306 | 128×64 分辨率,单色 |
| 开发环境 | Keil MDK-ARM | 5.36 | 编译器 |
| 固件库 | STM32 HAL | V1.8.0 | HAL 库 |
| 配置工具 | STM32CubeMX | 6.9.0 | 图形化配置 |
| 调试工具 | ST-Link V2 / 逻辑分析仪 | - | 下载调试 / 时序分析 |
| 取模工具 | PCtoLCD2002 | - | 汉字/图片字模生成 |
1.4 CSDN 推荐阅读
📚 在阅读本文前,建议先学习以下 CSDN 文章,掌握基础概念:
| 文章标题 | 核心内容 | 解决的问题 |
|---|---|---|
| SSD1306 OLED 驱动原理与嵌入式显示实现 | GDDRAM 架构与页寻址机制 | 理解 OLED 底层显存映射 |
| STM32 入门学习之 OLED 显示屏(原理篇) | SSD1306 驱动芯片引脚与框图 | 理解 SEG/COM 硬件驱动机制 |
| STM32 系列 HAL 库------F103C8T6 使用 SPI 方式点亮 OLED | 中景园代码移植与 CubeMX 配置 | 快速让 OLED 亮起来 |
二、Part 1:SSD1306 驱动原理与 SPI 通信时序
2.1 SSD1306 芯片架构(配 Mermaid 图)
SSD1306 是 Solomon Systech 推出的单色 OLED 驱动控制器,内部集成 128×64 bit GDDRAM(Graphic Display Data RAM),负责将 MCU 发送的像素数据映射到物理屏幕。
#mermaid-svg-HT2zz7fvUy5gRPTn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#ffffff;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HT2zz7fvUy5gRPTn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HT2zz7fvUy5gRPTn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HT2zz7fvUy5gRPTn .error-icon{fill:#a44141;}#mermaid-svg-HT2zz7fvUy5gRPTn .error-text{fill:#ddd;stroke:#ddd;}#mermaid-svg-HT2zz7fvUy5gRPTn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HT2zz7fvUy5gRPTn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HT2zz7fvUy5gRPTn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HT2zz7fvUy5gRPTn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HT2zz7fvUy5gRPTn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HT2zz7fvUy5gRPTn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HT2zz7fvUy5gRPTn .marker{fill:#60a5fa;stroke:#60a5fa;}#mermaid-svg-HT2zz7fvUy5gRPTn .marker.cross{stroke:#60a5fa;}#mermaid-svg-HT2zz7fvUy5gRPTn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-HT2zz7fvUy5gRPTn p{margin:0;}#mermaid-svg-HT2zz7fvUy5gRPTn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#ffffff;}#mermaid-svg-HT2zz7fvUy5gRPTn .cluster-label text{fill:#F9FFFE;}#mermaid-svg-HT2zz7fvUy5gRPTn .cluster-label span{color:#F9FFFE;}#mermaid-svg-HT2zz7fvUy5gRPTn .cluster-label span p{background-color:transparent;}#mermaid-svg-HT2zz7fvUy5gRPTn .label text,#mermaid-svg-HT2zz7fvUy5gRPTn span{fill:#ffffff;color:#ffffff;}#mermaid-svg-HT2zz7fvUy5gRPTn .node rect,#mermaid-svg-HT2zz7fvUy5gRPTn .node circle,#mermaid-svg-HT2zz7fvUy5gRPTn .node ellipse,#mermaid-svg-HT2zz7fvUy5gRPTn .node polygon,#mermaid-svg-HT2zz7fvUy5gRPTn .node path{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-HT2zz7fvUy5gRPTn .rough-node .label text,#mermaid-svg-HT2zz7fvUy5gRPTn .node .label text,#mermaid-svg-HT2zz7fvUy5gRPTn .image-shape .label,#mermaid-svg-HT2zz7fvUy5gRPTn .icon-shape .label{text-anchor:middle;}#mermaid-svg-HT2zz7fvUy5gRPTn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HT2zz7fvUy5gRPTn .rough-node .label,#mermaid-svg-HT2zz7fvUy5gRPTn .node .label,#mermaid-svg-HT2zz7fvUy5gRPTn .image-shape .label,#mermaid-svg-HT2zz7fvUy5gRPTn .icon-shape .label{text-align:center;}#mermaid-svg-HT2zz7fvUy5gRPTn .node.clickable{cursor:pointer;}#mermaid-svg-HT2zz7fvUy5gRPTn .root .anchor path{fill:#60a5fa!important;stroke-width:0;stroke:#60a5fa;}#mermaid-svg-HT2zz7fvUy5gRPTn .arrowheadPath{fill:lightgrey;}#mermaid-svg-HT2zz7fvUy5gRPTn .edgePath .path{stroke:#60a5fa;stroke-width:2.0px;}#mermaid-svg-HT2zz7fvUy5gRPTn .flowchart-link{stroke:#60a5fa;fill:none;}#mermaid-svg-HT2zz7fvUy5gRPTn .edgeLabel{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-HT2zz7fvUy5gRPTn .edgeLabel p{background-color:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-HT2zz7fvUy5gRPTn .edgeLabel rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-HT2zz7fvUy5gRPTn .labelBkg{background-color:rgba(87.75, 87.75, 87.75, 0.5);}#mermaid-svg-HT2zz7fvUy5gRPTn .cluster rect{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-HT2zz7fvUy5gRPTn .cluster text{fill:#F9FFFE;}#mermaid-svg-HT2zz7fvUy5gRPTn .cluster span{color:#F9FFFE;}#mermaid-svg-HT2zz7fvUy5gRPTn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#1e293b;border:1px solid rgba(255, 255, 255, 0.25);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HT2zz7fvUy5gRPTn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#ffffff;}#mermaid-svg-HT2zz7fvUy5gRPTn rect.text{fill:none;stroke-width:0;}#mermaid-svg-HT2zz7fvUy5gRPTn .icon-shape,#mermaid-svg-HT2zz7fvUy5gRPTn .image-shape{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-HT2zz7fvUy5gRPTn .icon-shape p,#mermaid-svg-HT2zz7fvUy5gRPTn .image-shape p{background-color:hsl(0, 0%, 34.4117647059%);padding:2px;}#mermaid-svg-HT2zz7fvUy5gRPTn .icon-shape .label rect,#mermaid-svg-HT2zz7fvUy5gRPTn .image-shape .label rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-HT2zz7fvUy5gRPTn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HT2zz7fvUy5gRPTn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HT2zz7fvUy5gRPTn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} OLED 面板
SSD1306 驱动芯片
STM32F103C8T6
DMA TX
SCLK / MOSI
CS/DC/RST
ARM Cortex-M3
72MHz
硬件 SPI1
8MHz
DMA1 Channel3
内存→SPI
GPIO
CS/DC/RST
SPI 接口层
SCLK/MOSI/CS/DC
命令译码器
GDDRAM
128×64 bit
(8页 × 128字节)
显示控制器
行扫描 + 列驱动
SEG0~SEG127
128 列
COM0~COM63
64 行
SSD1306 关键特性:
- GDDRAM 组织:128 列 × 64 行 = 8 页(Page 0~7),每页 8 行 × 128 列,每列 1 字节(8 bit = 8 行)
- 像素映射:GDDRAMyx bit0 对应第 y 页第 x 列的顶部像素,bit7 对应底部像素
- 地址模式:页寻址(Page Addressing)、水平寻址(Horizontal)、垂直寻址(Vertical)
- 频率特性:SCLK 最高 10MHz(3.3V 供电),建议 8MHz 留余量
2.2 通信接口方案对比与选型
OLED 模块通常引出 4 种通信接口:I2C、3 线 SPI、4 线 SPI、8 位 6800/8080 并行。SSD1306 通过硬件引脚 BS0/BS1/BS2 选择接口模式。
| 接口方案 | 引脚数 | 理论速率 | 实测全屏刷新率 | CPU 占用 | 适用场景 |
|---|---|---|---|---|---|
| I2C(400kHz) | 2(SCL+SDA) | 400kbps | ~12fps | 85% | 静态文字、引脚受限 |
| 软件 SPI | 4~5 | ~2Mbps | ~25fps | 90% | 快速原型、不关心 CPU 占用 |
| 硬件 SPI | 4~5 | 8Mbps | ~47fps | 62% | 中等动画效果 |
| 硬件 SPI + DMA | 4~5 | 8Mbps | 112fps | < 5% | 流畅动画、实时波形(推荐) |
⚠️ 选型警告:
- 软件 SPI 虽然灵活(任意 GPIO),但在 72MHz 的 STM32F103 上最大翻转速率仅约 2~3MHz,且 CPU 被 GPIO 翻转完全占用
- I2C 模式下 OLED 地址通常为 0x78(SA0=低),部分模块通过背面电阻上拉到 0x7A,务必确认
- 3 线 SPI 模式(省略 DC 引脚,通过命令字节区分 CMD/DATA)虽省一个引脚,但每次传输多 1 字节开销
2.3 SPI 通信时序与关键参数计算
4 线 SPI 接口定义:
| 信号线 | 方向 | 功能 |
|---|---|---|
| SCLK(PB13) | MCU → OLED | 串行时钟,上升沿采样数据 |
| MOSI(PB15) | MCU → OLED | 主出从入,高位先发(MSB First) |
| CS(PB12) | MCU → OLED | 片选,低电平有效 |
| DC(PB14) | MCU → OLED | 数据/命令选择:0=命令,1=数据 |
关键时序参数计算:
SPI1 时钟计算(APB2 总线):
APB2 外设时钟 = SYSCLK = 72MHz(STM32F103 不经过预分频器)
SPI1 时钟 = APB2 / 分频系数
目标 SCLK = 8MHz(留 2MHz 余量):
分频系数 = 72MHz / 8MHz = 9 → 取 8(向上取整)
实际 SCLK = 72MHz / 8 = 9MHz
全屏刷新时间(128×64 = 1024 字节):
1 帧数据量 = 128列 × 8页 × 1字节 = 1024 字节
SCLK = 9MHz,1 字节 = 8 bit + CS/DC 切换开销 ≈ 1μs
纯 SPI 模式刷新时间 ≈ 1024 × 1μs ≈ 1.02ms → 理论 980fps
实测瓶颈:SSD1306 内部 GSG(Gate Signal Generator)扫描周期
SSD1306 帧速率限制(数据手册):
- 内部振荡器频率 = 370kHz
- 显示时钟 DCLK = Fosc / D(D=1 最快)
- 帧率 = DCLK / (MUX × 每行时钟数)
- MUX = 64(64 行)
- 理论最大帧率 ≈ 370kHz / 64 ≈ 5781Hz → 约 90fps(含行切换开销)
结合 SPI 全屏数据写入时间(1.02ms)+ SSD1306 内部刷新(~12ms):
I2C 模式(400kHz):传输 1.02KB 需 ~28ms → 实际 ~12fps
硬件 SPI + DMA(9MHz):传输 1.02KB 需 ~1ms → 实际 ~112fps(DMA 解放 CPU)
📝 计算公式:全屏帧率 ≈ 1 / (SPI 传输时间 + SSD1306 内部扫描时间)
- SPI 传输时间 = 数据量(bit) / SCLK(Hz) = 1024×8 / 9MHz ≈ 0.91ms
- DMA 模式下 CPU 仅需等待 DMA 传输完成中断,CPU 占用 = 中断处理时间 / 帧周期 ≈ 2μs / 8.9ms < 0.1%
三、Part 2:硬件选型与完整接线
3.1 OLED 模块选型对比
| 参数 | 0.96寸 7针 SPI | 0.96寸 4针 I2C | 1.3寸 7针 SPI |
|---|---|---|---|
| 分辨率 | 128×64 | 128×64 | 128×64 |
| 驱动芯片 | SSD1306 | SSD1306 | SH1106 |
| 接口 | 4 线 SPI | I2C | 4 线 SPI |
| 显示颜色 | 白/蓝/双色 | 白/蓝 | 白/蓝 |
| 工作电压 | 3.3V~5V | 3.3V~5V | 3.3V~5V |
| 典型价格 | ¥8~12 | ¥7~10 | ¥12~18 |
| GDDRAM 布局 | 128×8 页 | 128×8 页 | 132×8 页 |
| 推荐场景 | 高速刷新 | 引脚受限 | 更大显示面积 |
⚠️ 1.3 寸 SH1106 注意:SH1106 的 GDDRAM 是 132×64,左上角有 2 像素偏移(Column 0 = RAM 地址 0x02),移植 SSD1306 代码需修改列起始地址。
3.2 完整接线表(STM32F103C8T6 ← → OLED 7 针模块)
| STM32 引脚 | 功能 | OLED 引脚 | 电压/信号类型 | 线缆颜色 | 备注 |
|---|---|---|---|---|---|
| PB13 | SPI2_SCK | SCL(D0) | 3.3V 时钟 | 紫色 | 9MHz 时钟信号 |
| PB15 | SPI2_MOSI | SDA(D1) | 3.3V 数据 | 蓝色 | 主出从入 |
| PB12 | GPIO 推挽输出 | CS | 3.3V 低有效 | 白色 | 片选,低电平选中 |
| PB14 | GPIO 推挽输出 | DC | 3.3V 电平 | 黄色 | 0=命令 / 1=数据 |
| PB10 | GPIO 推挽输出 | RST | 3.3V 电平 | 橙色 | 低电平复位 |
| 3.3V | 电源输出 | VCC | 3.3V | 红色 | 逻辑供电 |
| GND | 地线 | GND | 0V | 黑色 | 共地 |
⚠️ 接线安全警告(必须遵守):
- 不要反接 VCC/GND:OLED 模块通常无极性保护,反接会立即烧毁 SSD1306 芯片
- CS 引脚不可悬空:未驱动 CS 时建议上拉到 3.3V(10kΩ),防止总线冲突
- RST 引脚不可省略:虽然 SSD1306 有上电复位电路,但建议由 MCU 控制,上电后延迟 100ms 再拉高
- 信号线长度 ≤ 20cm:SPI 高速信号(>5MHz)过长走线会导致信号反射与振铃,建议 ≤ 15cm
- VCC 滤波:在 OLED 模块 VCC 引脚旁并联 100nF + 10μF 电容滤除噪声
3.3 接线拓扑图
#mermaid-svg-0sG902Ww9QyqqDoH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#ffffff;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0sG902Ww9QyqqDoH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0sG902Ww9QyqqDoH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0sG902Ww9QyqqDoH .error-icon{fill:#a44141;}#mermaid-svg-0sG902Ww9QyqqDoH .error-text{fill:#ddd;stroke:#ddd;}#mermaid-svg-0sG902Ww9QyqqDoH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0sG902Ww9QyqqDoH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0sG902Ww9QyqqDoH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0sG902Ww9QyqqDoH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0sG902Ww9QyqqDoH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0sG902Ww9QyqqDoH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0sG902Ww9QyqqDoH .marker{fill:#60a5fa;stroke:#60a5fa;}#mermaid-svg-0sG902Ww9QyqqDoH .marker.cross{stroke:#60a5fa;}#mermaid-svg-0sG902Ww9QyqqDoH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-0sG902Ww9QyqqDoH p{margin:0;}#mermaid-svg-0sG902Ww9QyqqDoH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#ffffff;}#mermaid-svg-0sG902Ww9QyqqDoH .cluster-label text{fill:#F9FFFE;}#mermaid-svg-0sG902Ww9QyqqDoH .cluster-label span{color:#F9FFFE;}#mermaid-svg-0sG902Ww9QyqqDoH .cluster-label span p{background-color:transparent;}#mermaid-svg-0sG902Ww9QyqqDoH .label text,#mermaid-svg-0sG902Ww9QyqqDoH span{fill:#ffffff;color:#ffffff;}#mermaid-svg-0sG902Ww9QyqqDoH .node rect,#mermaid-svg-0sG902Ww9QyqqDoH .node circle,#mermaid-svg-0sG902Ww9QyqqDoH .node ellipse,#mermaid-svg-0sG902Ww9QyqqDoH .node polygon,#mermaid-svg-0sG902Ww9QyqqDoH .node path{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-0sG902Ww9QyqqDoH .rough-node .label text,#mermaid-svg-0sG902Ww9QyqqDoH .node .label text,#mermaid-svg-0sG902Ww9QyqqDoH .image-shape .label,#mermaid-svg-0sG902Ww9QyqqDoH .icon-shape .label{text-anchor:middle;}#mermaid-svg-0sG902Ww9QyqqDoH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0sG902Ww9QyqqDoH .rough-node .label,#mermaid-svg-0sG902Ww9QyqqDoH .node .label,#mermaid-svg-0sG902Ww9QyqqDoH .image-shape .label,#mermaid-svg-0sG902Ww9QyqqDoH .icon-shape .label{text-align:center;}#mermaid-svg-0sG902Ww9QyqqDoH .node.clickable{cursor:pointer;}#mermaid-svg-0sG902Ww9QyqqDoH .root .anchor path{fill:#60a5fa!important;stroke-width:0;stroke:#60a5fa;}#mermaid-svg-0sG902Ww9QyqqDoH .arrowheadPath{fill:lightgrey;}#mermaid-svg-0sG902Ww9QyqqDoH .edgePath .path{stroke:#60a5fa;stroke-width:2.0px;}#mermaid-svg-0sG902Ww9QyqqDoH .flowchart-link{stroke:#60a5fa;fill:none;}#mermaid-svg-0sG902Ww9QyqqDoH .edgeLabel{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-0sG902Ww9QyqqDoH .edgeLabel p{background-color:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-0sG902Ww9QyqqDoH .edgeLabel rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-0sG902Ww9QyqqDoH .labelBkg{background-color:rgba(87.75, 87.75, 87.75, 0.5);}#mermaid-svg-0sG902Ww9QyqqDoH .cluster rect{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-0sG902Ww9QyqqDoH .cluster text{fill:#F9FFFE;}#mermaid-svg-0sG902Ww9QyqqDoH .cluster span{color:#F9FFFE;}#mermaid-svg-0sG902Ww9QyqqDoH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#1e293b;border:1px solid rgba(255, 255, 255, 0.25);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0sG902Ww9QyqqDoH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#ffffff;}#mermaid-svg-0sG902Ww9QyqqDoH rect.text{fill:none;stroke-width:0;}#mermaid-svg-0sG902Ww9QyqqDoH .icon-shape,#mermaid-svg-0sG902Ww9QyqqDoH .image-shape{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-0sG902Ww9QyqqDoH .icon-shape p,#mermaid-svg-0sG902Ww9QyqqDoH .image-shape p{background-color:hsl(0, 0%, 34.4117647059%);padding:2px;}#mermaid-svg-0sG902Ww9QyqqDoH .icon-shape .label rect,#mermaid-svg-0sG902Ww9QyqqDoH .image-shape .label rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-0sG902Ww9QyqqDoH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0sG902Ww9QyqqDoH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0sG902Ww9QyqqDoH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 0.96寸 OLED 7针模块
STM32F103C8T6 最小系统板
紫色 9MHz时钟
蓝色 MOSI数据
白色 片选
黄色 命令/数据
橙色 复位
红色 3.3V
黑色 共地
PB13: SPI2_SCK
PB15: SPI2_MOSI
PB12: CS(片选)
PB14: DC(数据/命令)
PB10: RST(复位)
3.3V 电源
GND
SCL / D0
SDA / D1
CS
DC
RST
VCC
GND
四、Part 3:STM32CubeMX 配置详解(SPI + DMA)
4.1 时钟树配置
Pinout & Configuration → System Core → RCC
High Speed Clock (HSE): Crystal/Ceramic Resonator(外部 8MHz)
Low Speed Clock (LSE): Disable
Clock Configuration 标签页:
HSE = 8MHz
PLL Source = HSE
PLLMUL = ×9 → SYSCLK = 8MHz × 9 = 72MHz ✅
AHB Prescaler = /1 → HCLK = 72MHz
APB1 Prescaler = /2 → APB1 = 36MHz(SPI2 挂载在 APB1 上!)
APB2 Prescaler = /1 → APB2 = 72MHz
⚠️ 关键注意 :STM32F103 的 SPI2 挂载在 APB1 总线上(36MHz),而 SPI1 在 APB2(72MHz)。因此 SPI2 最大时钟 = 36MHz / 2 = 18MHz。本文用 SPI2 达到 9MHz(Prescaler=4),已覆盖 SSD1306 需要。
4.2 SPI2 配置
Pinout & Configuration → Connectivity → SPI2
Mode: Full-Duplex Master
Hardware NSS Signal: Disable(使用软件 CS)
Parameter Settings:
Frame Format: Motorola
Data Size: 8 Bits
First Bit: MSB First
Clock Polarity (CPOL): Low // 空闲时 SCK=低
Clock Phase (CPHA): 1 Edge // 第一个时钟沿采样
NSS Signal Type: Software
Baud Rate Prescaler: 4 // 36MHz / 4 = 9MHz
SPI 时钟验证计算:
SPI2_SCK = APB1_CLK / Prescaler = 36MHz / 4 = 9MHz ✅
每个字节传输时间 = 8 bit / 9MHz ≈ 0.89μs
全屏 1024 字节传输时间 = 1024 × 0.89μs ≈ 0.91ms
4.3 DMA 配置
SPI2 → DMA Settings → Add
DMA Request: SPI2_TX
Stream / Channel: DMA1 Channel4
Direction: Memory To Peripheral
Priority: High
DMA Request Settings:
Mode: Normal(单次传输,每帧触发一次)
Data Width: Byte(Memory)/ Byte(Peripheral)
Increment Address: Memory = Yes, Peripheral = No
4.4 GPIO 配置(CS、DC、RST)
Pinout view → 配置以下引脚为 GPIO_Output:
PB12 → GPIO_Output, 初始电平 High(CS 默认不选中)
PB14 → GPIO_Output, 初始电平 Low(DC 默认命令模式)
PB10 → GPIO_Output, 初始电平 High(RST 默认不复位)
GPIO output level:
PB12 (CS): High - 不选中 OLED
PB14 (DC): Low - 命令模式
PB10 (RST): High - 不复位
GPIO output speed: Low(OLED 控制信号无需高速)
五、Part 4:核心驱动代码实现(工程级)
📄 创建文件:
Core/OLED/oled.h
c
/**
* @file oled.h
* @brief SSD1306 OLED 驱动头文件(硬件 SPI + DMA 版本)
* @note 适配 0.96寸 128×64 7针 SPI OLED 模块
* @date 2026-07-28
*/
#ifndef __OLED_H
#define __OLED_H
#include "main.h"
#include <stdint.h>
#include <string.h>
#include <stdio.h>
/* ========== OLED 尺寸定义 ========== */
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_PAGES 8 /* 64行 / 8 = 8页 */
#define OLED_FRAMEBUFFER_SIZE (OLED_WIDTH * OLED_PAGES) /* 1024 字节 */
/* ========== 控制引脚宏定义(便于移植) ========== */
#define OLED_CS_PORT GPIOB
#define OLED_CS_PIN GPIO_PIN_12
#define OLED_DC_PORT GPIOB
#define OLED_DC_PIN GPIO_PIN_14
#define OLED_RST_PORT GPIOB
#define OLED_RST_PIN GPIO_PIN_10
/* 引脚操作宏 */
#define OLED_CS_LOW() HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_RESET)
#define OLED_CS_HIGH() HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_SET)
#define OLED_DC_CMD() HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_RESET)
#define OLED_DC_DATA() HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_SET)
#define OLED_RST_LOW() HAL_GPIO_WritePin(OLED_RST_PORT, OLED_RST_PIN, GPIO_PIN_RESET)
#define OLED_RST_HIGH() HAL_GPIO_WritePin(OLED_RST_PORT, OLED_RST_PIN, GPIO_PIN_SET)
/* ========== 颜色定义(单色 OLED) ========== */
#define OLED_COLOR_BLACK 0x00
#define OLED_COLOR_WHITE 0xFF
#define OLED_COLOR_INVERT 0x01
/* ========== 字体大小枚举 ========== */
typedef enum {
OLED_FONT_6X8 = 0, /* 6×8 ASCII 字符 */
OLED_FONT_8X16, /* 8×16 ASCII 字符 */
OLED_FONT_16X16, /* 16×16 汉字 */
} OLED_FontSize_t;
/* ========== 帧缓冲 DMA 状态 ========== */
typedef struct {
uint8_t buffer[OLED_FRAMEBUFFER_SIZE]; /* 1024 字节显存镜像 */
uint8_t dma_busy; /* DMA 传输中标志 */
uint32_t frame_count; /* 帧计数器 */
} OLED_FrameBuffer_t;
/* ========== 全局帧缓冲实例 ========== */
extern OLED_FrameBuffer_t oled_fb;
/* ========== API 函数声明 ========== */
void OLED_Init(void);
void OLED_Reset(void);
void OLED_Clear(void);
void OLED_UpdateFull(void); /* DMA 刷新整屏 */
void OLED_UpdateFull_Blocking(void); /* SPI 轮询刷新整屏(调试用)*/
void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t color);
void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1);
void OLED_DrawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color);
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, OLED_FontSize_t font);
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, OLED_FontSize_t font);
void OLED_ShowNum(uint8_t x, uint8_t y, int32_t num, uint8_t digits, OLED_FontSize_t font);
void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t index);
void OLED_ShowBMP(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *bmp);
void OLED_ScrollStart(uint8_t start_page, uint8_t end_page, uint8_t direction);
void OLED_ScrollStop(void);
void OLED_SetContrast(uint8_t contrast);
void OLED_DisplayOn(void);
void OLED_DisplayOff(void);
void OLED_Printf(uint8_t x, uint8_t y, OLED_FontSize_t font, const char *fmt, ...);
#endif /* __OLED_H */
📄 创建文件:
Core/OLED/oled.c
c
/**
* @file oled.c
* @brief SSD1306 OLED 驱动实现(硬件 SPI + DMA)
*/
#include "oled.h"
#include "oled_font.h" /* ASCII 字库 */
#include "oled_chinese.h" /* 汉字字库 */
#include <stdarg.h>
/* 外部 SPI 句柄(CubeMX 生成)*/
extern SPI_HandleTypeDef hspi2;
extern DMA_HandleTypeDef hdma_spi2_tx;
/* 帧缓冲全局实例 */
OLED_FrameBuffer_t oled_fb = {
.dma_busy = 0,
.frame_count = 0
};
/* ===================== 底层通信 ===================== */
/**
* @brief SPI 发送单字节(命令或数据)
* @param byte: 要发送的数据
* @note 不操作 CS/DC,由上层控制
*/
static void OLED_SPI_SendByte(uint8_t byte)
{
/* 等待 SPI 发送缓冲区空 */
while (__HAL_SPI_GET_FLAG(&hspi2, SPI_FLAG_TXE) == RESET);
/* 写入数据寄存器(8 位模式只使用低字节) */
*((__IO uint8_t *)&hspi2.Instance->DR) = byte;
/* 等待传输完成 */
while (__HAL_SPI_GET_FLAG(&hspi2, SPI_FLAG_BSY) == SET);
}
/**
* @brief 写入命令字节
* @param cmd: 命令码(详见 SSD1306 数据手册)
*/
static void OLED_WriteCmd(uint8_t cmd)
{
OLED_CS_LOW();
OLED_DC_CMD(); /* DC=0 → 命令模式 */
OLED_SPI_SendByte(cmd);
OLED_CS_HIGH();
}
/**
* @brief 写入数据字节
* @param data: 像素数据
*/
static void OLED_WriteData(uint8_t data)
{
OLED_CS_LOW();
OLED_DC_DATA(); /* DC=1 → 数据模式 */
OLED_SPI_SendByte(data);
OLED_CS_HIGH();
}
/**
* @brief 设置显示坐标(页寻址模式)
* @param page: 页地址 0~7
* @param col: 列地址 0~127
*/
static void OLED_SetPos(uint8_t page, uint8_t col)
{
OLED_WriteCmd(0xB0 + page); /* 设置页地址 */
OLED_WriteCmd((col & 0x0F) | 0x00); /* 列地址低 4 位 */
OLED_WriteCmd(((col >> 4) & 0x0F) | 0x10); /* 列地址高 4 位 */
}
/* ===================== 初始化 ===================== */
/**
* @brief OLED 硬件复位
* @note 复位时序:RST 低 10ms → 高 100ms
*/
void OLED_Reset(void)
{
OLED_RST_LOW();
HAL_Delay(10);
OLED_RST_HIGH();
HAL_Delay(100);
}
/**
* @brief SSD1306 初始化序列
* @note 参考 SSD1306 数据手册 Application Note
*/
void OLED_Init(void)
{
/* Step 1: 硬件复位 */
OLED_Reset();
/* Step 2: 关闭显示 */
OLED_WriteCmd(0xAE); /* Display OFF */
/* Step 3: 时钟与复用率 */
OLED_WriteCmd(0xD5); /* Set Display Clock Divide Ratio/Oscillator Frequency */
OLED_WriteCmd(0x80); /* 默认值:DCLK = Fosc, divider = 1 */
OLED_WriteCmd(0xA8); /* Set Multiplex Ratio */
OLED_WriteCmd(0x3F); /* MUX = 64(64 行,0x3F = 63) */
/* Step 4: 显示偏移 */
OLED_WriteCmd(0xD3); /* Set Display Offset */
OLED_WriteCmd(0x00); /* Offset = 0 */
/* Step 5: 显示起始行 */
OLED_WriteCmd(0x40); /* Set Display Start Line = 0 */
/* Step 6: 段重映射与 COM 扫描方向 */
OLED_WriteCmd(0xA1); /* Segment Remap: column 127 = SEG0(左右镜像) */
OLED_WriteCmd(0xC8); /* COM Output Scan Direction: remapped(上下镜像) */
/* Step 7: COM 引脚硬件配置 */
OLED_WriteCmd(0xDA); /* Set COM Pins Hardware Configuration */
OLED_WriteCmd(0x12); /* Alternative COM pin config, disable COM Left/Right remap */
/* Step 8: 对比度 */
OLED_WriteCmd(0x81); /* Set Contrast Control */
OLED_WriteCmd(0xCF); /* 对比度默认值 */
/* Step 9: 预充电周期 */
OLED_WriteCmd(0xD9); /* Set Pre-charge Period */
OLED_WriteCmd(0xF1); /* Phase1=1 DCLK, Phase2=15 DCLK */
/* Step 10: VCOMH 电压 */
OLED_WriteCmd(0xDB); /* Set VCOMH Deselect Level */
OLED_WriteCmd(0x40); /* ~0.77 × VCC */
/* Step 11: 正常显示模式 */
OLED_WriteCmd(0xA6); /* Normal Display(非反色) */
OLED_WriteCmd(0xA4); /* Display follows RAM content */
/* Step 12: 电荷泵 */
OLED_WriteCmd(0x8D); /* Charge Pump Setting */
OLED_WriteCmd(0x14); /* Enable charge pump(必须!否则不亮) */
/* Step 13: 寻址模式 → 水平寻址(DMA 刷新用) */
OLED_WriteCmd(0x20); /* Set Memory Addressing Mode */
OLED_WriteCmd(0x00); /* 水平寻址模式 */
/* Step 14: 开启显示 */
OLED_WriteCmd(0xAF); /* Display ON */
/* Step 15: 清空帧缓冲和物理屏幕 */
OLED_Clear();
OLED_UpdateFull_Blocking();
}
/* ===================== 显示控制 ===================== */
void OLED_DisplayOn(void)
{
OLED_WriteCmd(0xAF);
}
void OLED_DisplayOff(void)
{
OLED_WriteCmd(0xAE);
}
void OLED_SetContrast(uint8_t contrast)
{
OLED_WriteCmd(0x81);
OLED_WriteCmd(contrast);
}
/* ===================== 帧缓冲操作 ===================== */
/**
* @brief 清空帧缓冲
*/
void OLED_Clear(void)
{
memset(oled_fb.buffer, 0x00, sizeof(oled_fb.buffer));
}
/**
* @brief 在帧缓冲中设置一个像素点
* @param x: 列坐标 0~127
* @param y: 行坐标 0~63
* @param color: 0=灭, 非0=亮
*/
void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t color)
{
if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return; /* 边界检查 */
uint8_t page = y / 8;
uint8_t bit = y % 8;
uint16_t idx = page * OLED_WIDTH + x;
if (color) {
oled_fb.buffer[idx] |= (1 << bit); /* 置位 */
} else {
oled_fb.buffer[idx] &= ~(1 << bit); /* 清零 */
}
}
/* ===================== DMA 刷新 ===================== */
/**
* @brief 使用 DMA 将帧缓冲发送到 OLED(异步)
* @note 必须先设置水平寻址模式(已在 Init 中设置)
* 刷新流程:
* 1. 设置列范围 0~127
* 2. 设置页范围 0~7
* 3. 拉低 CS,置 DC=DATA
* 4. 启动 DMA → SPI 发送 1024 字节
* 5. DMA 完成中断拉高 CS
*/
void OLED_UpdateFull(void)
{
/* 如果 DMA 还在传输中,跳过(防重叠) */
if (oled_fb.dma_busy) return;
/* 设置水平寻址的列范围 */
OLED_WriteCmd(0x21); /* Set Column Address */
OLED_WriteCmd(0x00); /* 起始列 = 0 */
OLED_WriteCmd(0x7F); /* 结束列 = 127 */
/* 设置水平寻址的页范围 */
OLED_WriteCmd(0x22); /* Set Page Address */
OLED_WriteCmd(0x00); /* 起始页 = 0 */
OLED_WriteCmd(0x07); /* 结束页 = 7 */
/* 拉低 CS,置 DC=DATA(之后全为数据字节) */
OLED_CS_LOW();
OLED_DC_DATA();
/* 启动 DMA 发送 */
oled_fb.dma_busy = 1;
HAL_SPI_Transmit_DMA(&hspi2, oled_fb.buffer, OLED_FRAMEBUFFER_SIZE);
}
/**
* @brief DMA 发送完成回调
* @note CubeMX 生成 HAL_SPI_TxCpltCallback,在此拉高 CS
*/
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi == &hspi2) {
OLED_CS_HIGH(); /* 结束 SPI 传输 */
oled_fb.dma_busy = 0;
oled_fb.frame_count++;
}
}
/**
* @brief SPI 轮询方式刷新整屏(调试/低速场景用)
*/
void OLED_UpdateFull_Blocking(void)
{
OLED_WriteCmd(0x21); OLED_WriteCmd(0x00); OLED_WriteCmd(0x7F);
OLED_WriteCmd(0x22); OLED_WriteCmd(0x00); OLED_WriteCmd(0x07);
OLED_CS_LOW();
OLED_DC_DATA();
for (uint16_t i = 0; i < OLED_FRAMEBUFFER_SIZE; i++) {
OLED_SPI_SendByte(oled_fb.buffer[i]);
}
OLED_CS_HIGH();
}
/* ===================== 基本图形 ===================== */
/**
* @brief Bresenham 画线算法
*/
void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1)
{
int16_t dx = (int16_t)x1 - x0;
int16_t dy = (int16_t)y1 - y0;
int16_t sx = dx > 0 ? 1 : -1;
int16_t sy = dy > 0 ? 1 : -1;
dx = dx > 0 ? dx : -dx;
dy = dy > 0 ? dy : -dy;
int16_t err = dx - dy;
int16_t e2;
while (1) {
OLED_SetPixel(x0, y0, OLED_COLOR_WHITE);
if (x0 == x1 && y0 == y1) break;
e2 = err * 2;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
}
/**
* @brief 画矩形
*/
void OLED_DrawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)
{
/* 边界检查 */
if (x + w > OLED_WIDTH) w = OLED_WIDTH - x;
if (y + h > OLED_HEIGHT) h = OLED_HEIGHT - y;
uint8_t i;
for (i = 0; i < w; i++) {
OLED_SetPixel(x + i, y, color); /* 上边 */
OLED_SetPixel(x + i, y + h - 1, color); /* 下边 */
}
for (i = 0; i < h; i++) {
OLED_SetPixel(x, y + i, color); /* 左边 */
OLED_SetPixel(x + w - 1, y + i, color); /* 右边 */
}
}
/* ===================== ASCII 字符显示 ===================== */
/**
* @brief 显示一个 ASCII 字符
* @param x: 列坐标(像素)
* @param y: 页坐标(0~7)
* @param ch: 字符
* @param font: 字体大小
*/
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, OLED_FontSize_t font)
{
uint8_t i, j;
uint8_t char_width, char_height;
switch (font) {
case OLED_FONT_6X8:
char_width = 6;
char_height = 8;
break;
case OLED_FONT_8X16:
char_width = 8;
char_height = 16;
break;
default:
return;
}
uint8_t pages = char_height / 8;
for (i = 0; i < char_width; i++) {
uint16_t glyph_offset;
if (font == OLED_FONT_6X8) {
/* 6×8 字体:ASCII 32~126 */
if (ch < ' ' || ch > '~') ch = ' ';
glyph_offset = (uint16_t)(ch - ' ') * 6 + i;
for (j = 0; j < pages; j++) {
uint8_t data = F6x8[glyph_offset];
uint16_t idx = (y + j) * OLED_WIDTH + x + i;
if (idx < OLED_FRAMEBUFFER_SIZE)
oled_fb.buffer[idx] = data;
}
} else {
/* 8×16 字体:ASCII 32~126 */
if (ch < ' ' || ch > '~') ch = ' ';
glyph_offset = (uint16_t)(ch - ' ') * 16 + i;
for (j = 0; j < pages; j++) {
uint8_t data = F8x16[glyph_offset + j * 8];
uint16_t idx = (y + j) * OLED_WIDTH + x + i;
if (idx < OLED_FRAMEBUFFER_SIZE)
oled_fb.buffer[idx] = data;
}
}
}
}
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, OLED_FontSize_t font)
{
uint8_t char_width = (font == OLED_FONT_6X8) ? 6 : 8;
while (*str) {
if (x + char_width > OLED_WIDTH) {
x = 0;
y += (font == OLED_FONT_6X8) ? 1 : 2;
if (y >= OLED_PAGES) break; /* 超出屏幕 */
}
OLED_ShowChar(x, y, *str, font);
x += char_width;
str++;
}
}
void OLED_ShowNum(uint8_t x, uint8_t y, int32_t num, uint8_t digits, OLED_FontSize_t font)
{
char buf[12];
snprintf(buf, sizeof(buf), "%*ld", digits, (long)num);
OLED_ShowString(x, y, buf, font);
}
/* ===================== 汉字显示 ===================== */
/**
* @brief 显示一个 16×16 汉字
* @param x: 列坐标
* @param y: 页坐标(0~6,占用 2 页)
* @param index: 字库索引
*/
void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t index)
{
if (x + 16 > OLED_WIDTH || y + 2 > OLED_PAGES) return;
uint8_t i, j;
for (i = 0; i < 16; i++) {
uint16_t offset = index * 32 + i * 2;
for (j = 0; j < 2; j++) {
uint8_t data = CN16x16[offset + j];
uint16_t idx = (y + j) * OLED_WIDTH + x + i;
if (idx < OLED_FRAMEBUFFER_SIZE)
oled_fb.buffer[idx] = data;
}
}
}
/* ===================== 图片显示 ===================== */
void OLED_ShowBMP(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *bmp)
{
if (x + w > OLED_WIDTH || y * 8 + h > OLED_HEIGHT) return;
uint8_t i, j;
uint8_t pages = (h + 7) / 8;
for (j = 0; j < pages; j++) {
for (i = 0; i < w; i++) {
uint16_t src_idx = j * w + i;
uint16_t dst_idx = (y + j) * OLED_WIDTH + x + i;
if (dst_idx < OLED_FRAMEBUFFER_SIZE)
oled_fb.buffer[dst_idx] = bmp[src_idx];
}
}
}
/* ===================== 滚动显示 ===================== */
void OLED_ScrollStart(uint8_t start_page, uint8_t end_page, uint8_t direction)
{
if (direction) {
/* 向右滚动 */
OLED_WriteCmd(0x26); /* Right Horizontal Scroll */
OLED_WriteCmd(0x00); /* Dummy */
OLED_WriteCmd(start_page);
OLED_WriteCmd(0x00); /* 时间间隔:2 frames */
OLED_WriteCmd(end_page);
OLED_WriteCmd(0x00); /* Dummy */
OLED_WriteCmd(0xFF); /* Dummy */
OLED_WriteCmd(0x2F); /* Activate Scroll */
} else {
/* 向左滚动 */
OLED_WriteCmd(0x27); /* Left Horizontal Scroll */
OLED_WriteCmd(0x00);
OLED_WriteCmd(start_page);
OLED_WriteCmd(0x00);
OLED_WriteCmd(end_page);
OLED_WriteCmd(0x00);
OLED_WriteCmd(0xFF);
OLED_WriteCmd(0x2F);
}
}
void OLED_ScrollStop(void)
{
OLED_WriteCmd(0x2E); /* Deactivate Scroll */
}
/* ===================== 格式化输出 ===================== */
void OLED_Printf(uint8_t x, uint8_t y, OLED_FontSize_t font, const char *fmt, ...)
{
char buf[64];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
OLED_ShowString(x, y, buf, font);
}
📄 创建文件:
Core/OLED/oled_font.h(ASCII 6×8 字库,简略示例)
c
/**
* @file oled_font.h
* @brief ASCII 6×8 与 8×16 字库(部分)
*/
#ifndef __OLED_FONT_H
#define __OLED_FONT_H
#include <stdint.h>
/* 6×8 ASCII 字库(ASCII 32 ' ' ~ 126 '~') */
static const uint8_t F6x8[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* ' ' 32 */
0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, /* '!' 33 */
0x00, 0x07, 0x00, 0x07, 0x00, 0x00, /* '"' 34 */
0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00, /* '#' 35 */
0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00, /* '$' 36 */
0x23, 0x13, 0x08, 0x64, 0x62, 0x00, /* '%' 37 */
0x36, 0x49, 0x55, 0x22, 0x50, 0x00, /* '&' 38 */
/* ... 省略剩余 ASCII 字符以控制篇幅,完整字库见工程源码 ... */
};
/* 8×16 ASCII 字库(ASCII 32 ' ' ~ 126 '~') */
static const uint8_t F8x16[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ' ' 32 */
0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00, /* '!' 33 */
0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* '"' 34 */
/* ... 省略剩余 ASCII 字符以控制篇幅 ... */
};
#endif /* __OLED_FONT_H */
📄 创建文件:
Core/OLED/oled_chinese.h(汉字字库示例)
c
/**
* @file oled_chinese.h
* @brief 16×16 汉字字库(PCtoLCD2002 取模)
* @note 取模设置:阴码 + 逐列式 + 顺向 + C51 格式
*/
#ifndef __OLED_CHINESE_H
#define __OLED_CHINESE_H
#include <stdint.h>
/**
* PCtoLCD2002 取模参数:
* 点阵格式:阴码(0=灭, 1=亮)
* 取模方式:逐列式
* 取模走向:顺向(从左到右)
* 输出数制:16 进制
* 自定义格式:C51 格式
*
* 每个汉字 = 32 字节(16列 × 2(每列 16 行分 2 字节)
*/
static const uint8_t CN16x16[] = {
/* "嵌" 0 */
0x00,0x04,0x04,0x04,0xFC,0x44,0x44,0x44,
0xFE,0x06,0x05,0xFC,0x04,0x04,0x00,0x00,
0x40,0x20,0x18,0x07,0x00,0x60,0x38,0x2F,
0x21,0x21,0x21,0x3F,0x20,0x60,0x40,0x00,
/* "入" 1 */
0x00,0x80,0x60,0x18,0x06,0x00,0x00,0x00,
0x00,0x02,0x06,0x18,0x60,0x80,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x10,0x18,0x0E,
0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,
/* "式" 2 */
0x00,0x20,0x20,0x20,0xFE,0x20,0x20,0x20,
0x00,0x80,0x60,0x1E,0x02,0x00,0x00,0x00,
0x40,0x80,0x80,0x80,0x7F,0x20,0x10,0x0F,
0x00,0x01,0x0E,0x30,0x40,0x00,0x00,0x00,
/* "开" 3 */
0x00,0x00,0xFE,0x22,0x22,0xFE,0x00,0x00,
0xFE,0x02,0x02,0xFE,0x02,0x02,0x00,0x00,
0x20,0x20,0x3F,0x20,0x20,0x3F,0x20,0x20,
0x3F,0x00,0x00,0x7F,0x00,0x00,0x00,0x00,
/* "发" 4 */
0x00,0x20,0x30,0xAC,0x63,0x20,0x20,0x18,
0x27,0xE0,0x20,0x20,0x20,0x20,0x00,0x00,
0x00,0x00,0x00,0x7E,0x00,0x10,0x08,0x07,
0x00,0x03,0x1C,0x60,0x40,0x00,0x00,0x00,
};
/* 索引:0="嵌", 1="入", 2="式", 3="开", 4="发" */
#endif /* __OLED_CHINESE_H */
📄 创建文件:
Core/User/main.c(应用层主循环示例)
c
/**
* @file main.c
* @brief 主程序:OLED 动画演示
*/
#include "main.h"
#include "oled.h"
/* 外部句柄 */
extern SPI_HandleTypeDef hspi2;
/* 系统时钟配置(CubeMX 生成) */
void SystemClock_Config(void);
int main(void)
{
/* HAL 初始化 */
HAL_Init();
SystemClock_Config();
/* 外设初始化(CubeMX 生成) */
MX_GPIO_Init();
MX_DMA_Init();
MX_SPI2_Init();
/* OLED 初始化 */
OLED_Init();
/* ======== 演示 1:显示字符串 ======== */
OLED_Clear();
OLED_ShowString(0, 0, "Hello OLED!", OLED_FONT_8X16);
OLED_ShowString(0, 2, "STM32+SSD1306", OLED_FONT_6X8);
OLED_ShowChinese(0, 4, 0); /* "嵌" */
OLED_ShowChinese(16, 4, 1); /* "入" */
OLED_ShowChinese(32, 4, 2); /* "式" */
OLED_ShowChinese(48, 4, 3); /* "开" */
OLED_ShowChinese(64, 4, 4); /* "发" */
OLED_UpdateFull_Blocking(); /* 首次用轮询刷新 */
HAL_Delay(2000);
/* ======== 演示 2:动画------移动的矩形 ======== */
uint8_t rect_x = 0;
int8_t delta = 1;
uint32_t last_update = HAL_GetTick();
while (1) {
uint32_t now = HAL_GetTick();
/* 每 20ms 刷新一帧(约 50fps) */
if (now - last_update >= 20) {
last_update = now;
OLED_Clear();
/* 绘制移动矩形 */
OLED_DrawRect(rect_x, 16, 30, 20, OLED_COLOR_WHITE);
rect_x += delta;
if (rect_x >= OLED_WIDTH - 30 || rect_x == 0) delta = -delta;
/* 显示帧率信息 */
OLED_Printf(0, 6, OLED_FONT_6X8, "F:%lu DMA:%s",
oled_fb.frame_count,
oled_fb.dma_busy ? "BUSY" : "IDLE");
/* DMA 异步刷新 */
OLED_UpdateFull();
}
}
}
/* ========== CubeMX 生成的外设初始化(略) ========== */
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
/* CS, DC, RST 初始化为推挽输出 */
HAL_GPIO_WritePin(GPIOB,
OLED_CS_PIN | OLED_DC_PIN | OLED_RST_PIN,
GPIO_PIN_SET); /* 默认高电平 */
GPIO_InitStruct.Pin = OLED_CS_PIN | OLED_DC_PIN | OLED_RST_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
void MX_DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}
void MX_SPI2_Init(void)
{
hspi2.Instance = SPI2;
hspi2.Init.Mode = SPI_MODE_MASTER;
hspi2.Init.Direction = SPI_DIRECTION_2LINES;
hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi2.Init.NSS = SPI_NSS_SOFT;
hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; /* 9MHz */
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
if (HAL_SPI_Init(&hspi2) != HAL_OK) {
Error_Handler();
}
}
void SystemClock_Config(void)
{
/* CubeMX 生成的时钟配置(略) */
/* 目标:SYSCLK=72MHz, AHB=72MHz, APB1=36MHz, APB2=72MHz */
}
5.1 代码架构说明
#mermaid-svg-SeAjY0bzQ9yhUuNc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;fill:#ffffff;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SeAjY0bzQ9yhUuNc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SeAjY0bzQ9yhUuNc .error-icon{fill:#a44141;}#mermaid-svg-SeAjY0bzQ9yhUuNc .error-text{fill:#ddd;stroke:#ddd;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SeAjY0bzQ9yhUuNc .marker{fill:#60a5fa;stroke:#60a5fa;}#mermaid-svg-SeAjY0bzQ9yhUuNc .marker.cross{stroke:#60a5fa;}#mermaid-svg-SeAjY0bzQ9yhUuNc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:14px;}#mermaid-svg-SeAjY0bzQ9yhUuNc p{margin:0;}#mermaid-svg-SeAjY0bzQ9yhUuNc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#ffffff;}#mermaid-svg-SeAjY0bzQ9yhUuNc .cluster-label text{fill:#F9FFFE;}#mermaid-svg-SeAjY0bzQ9yhUuNc .cluster-label span{color:#F9FFFE;}#mermaid-svg-SeAjY0bzQ9yhUuNc .cluster-label span p{background-color:transparent;}#mermaid-svg-SeAjY0bzQ9yhUuNc .label text,#mermaid-svg-SeAjY0bzQ9yhUuNc span{fill:#ffffff;color:#ffffff;}#mermaid-svg-SeAjY0bzQ9yhUuNc .node rect,#mermaid-svg-SeAjY0bzQ9yhUuNc .node circle,#mermaid-svg-SeAjY0bzQ9yhUuNc .node ellipse,#mermaid-svg-SeAjY0bzQ9yhUuNc .node polygon,#mermaid-svg-SeAjY0bzQ9yhUuNc .node path{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-SeAjY0bzQ9yhUuNc .rough-node .label text,#mermaid-svg-SeAjY0bzQ9yhUuNc .node .label text,#mermaid-svg-SeAjY0bzQ9yhUuNc .image-shape .label,#mermaid-svg-SeAjY0bzQ9yhUuNc .icon-shape .label{text-anchor:middle;}#mermaid-svg-SeAjY0bzQ9yhUuNc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SeAjY0bzQ9yhUuNc .rough-node .label,#mermaid-svg-SeAjY0bzQ9yhUuNc .node .label,#mermaid-svg-SeAjY0bzQ9yhUuNc .image-shape .label,#mermaid-svg-SeAjY0bzQ9yhUuNc .icon-shape .label{text-align:center;}#mermaid-svg-SeAjY0bzQ9yhUuNc .node.clickable{cursor:pointer;}#mermaid-svg-SeAjY0bzQ9yhUuNc .root .anchor path{fill:#60a5fa!important;stroke-width:0;stroke:#60a5fa;}#mermaid-svg-SeAjY0bzQ9yhUuNc .arrowheadPath{fill:lightgrey;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edgePath .path{stroke:#60a5fa;stroke-width:2.0px;}#mermaid-svg-SeAjY0bzQ9yhUuNc .flowchart-link{stroke:#60a5fa;fill:none;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edgeLabel{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-SeAjY0bzQ9yhUuNc .edgeLabel p{background-color:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-SeAjY0bzQ9yhUuNc .edgeLabel rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-SeAjY0bzQ9yhUuNc .labelBkg{background-color:rgba(87.75, 87.75, 87.75, 0.5);}#mermaid-svg-SeAjY0bzQ9yhUuNc .cluster rect{fill:#1e293b;stroke:#3b82f6;stroke-width:1px;}#mermaid-svg-SeAjY0bzQ9yhUuNc .cluster text{fill:#F9FFFE;}#mermaid-svg-SeAjY0bzQ9yhUuNc .cluster span{color:#F9FFFE;}#mermaid-svg-SeAjY0bzQ9yhUuNc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#1e293b;border:1px solid rgba(255, 255, 255, 0.25);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SeAjY0bzQ9yhUuNc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#ffffff;}#mermaid-svg-SeAjY0bzQ9yhUuNc rect.text{fill:none;stroke-width:0;}#mermaid-svg-SeAjY0bzQ9yhUuNc .icon-shape,#mermaid-svg-SeAjY0bzQ9yhUuNc .image-shape{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-SeAjY0bzQ9yhUuNc .icon-shape p,#mermaid-svg-SeAjY0bzQ9yhUuNc .image-shape p{background-color:hsl(0, 0%, 34.4117647059%);padding:2px;}#mermaid-svg-SeAjY0bzQ9yhUuNc .icon-shape .label rect,#mermaid-svg-SeAjY0bzQ9yhUuNc .image-shape .label rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-SeAjY0bzQ9yhUuNc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SeAjY0bzQ9yhUuNc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SeAjY0bzQ9yhUuNc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 硬件层 SPI2+DMA
OLED 驱动层 oled.c
应用层 main.c
动画循环
50fps
OLED_Init()
初始化序列
帧缓冲操作
SetPixel / Clear
图形绘制
DrawLine / DrawRect
字体渲染
ShowChar / ShowChinese
刷新引擎
UpdateFull (DMA)
SPI2 TX
9MHz
DMA1 CH4
Mem→Peripheral
六、Part 5:DMA 加速核心------水平寻址 + 环形缓冲
6.1 水平寻址模式原理
SSD1306 提供三种寻址模式,唯有水平寻址模式适合 DMA 全屏刷新:
| 寻址模式 | 列指针行为 | 页指针行为 | DMA 兼容性 |
|---|---|---|---|
| 页寻址(Page) | 写完 128 列后停止 | 需手动切换页 | ❌ 每页需重新发命令 |
| 水平寻址(Horizontal) | 列指针自动递增 0→127 | 列指针到 127 后页指针自动 +1 | ✅ 完美适配 DMA |
| 垂直寻址(Vertical) | 页指针递增后列指针 +1 | 页指针自动递增 0→7 | ⚠️ 像素顺序非逐行 |
水平寻址模式下 DMA 刷新流程:
1. 发送命令 0x21, 0x00, 0x7F → 设置列范围 0~127
2. 发送命令 0x22, 0x00, 0x07 → 设置页范围 0~7
3. CS=Low, DC=Data → 进入纯数据模式
4. 启动 DMA 发送 1024 字节 → 硬件自动按顺序写入 GDDRAM
字节 0→列0页0, 字节1→列1页0, ..., 字节128→列0页1, ...
5. DMA 传输完成中断 → CS=High → 帧完成
6.2 帧缓冲与 DMA 协同
c
/* 帧缓冲直接就是 SPI 发送缓冲区,零拷贝 */
uint8_t fb[1024]; /* 显存镜像 */
/* 渲染→帧缓冲(随机访问) */
OLED_SetPixel(x, y, color); /* 修改 fb[] 对应字节 */
/* 帧缓冲→SPI→OLED(DMA 零拷贝) */
HAL_SPI_Transmit_DMA(&hspi2, fb, 1024); /* 直接 DMA 发送 fb */
零拷贝设计优势:
- 渲染层操作
fb[]数组,和普通内存操作一样快 - DMA 直接读取
fb[]起始地址发送,无需 memcpy - 渲染和 DMA 发送可以流水线化(上一帧 DMA 发送时渲染下一帧到双缓冲)
七、Part 6:性能对比测试
7.1 四种驱动方案帧率对比
| 方案 | SCLK 频率 | 全屏传输时间 | SSD1306 内部扫描 | 实测帧率 | CPU 占用 | 适用场景 |
|---|---|---|---|---|---|---|
| I2C(400kHz) | 400kHz | 28ms | ~12ms | 12 fps | 85% | 静态文字 |
| 软件 SPI | ~2MHz | 5.1ms | ~12ms | 25 fps | 90% | 快速原型 |
| 硬件 SPI | 9MHz | 0.91ms | ~12ms | 47 fps | 62% | 中速动画 |
| SPI + DMA | 9MHz | 0.91ms | ~12ms | 112 fps | <5% | 高速动画/波形 |
7.2 阶跃响应------帧率 vs 分辨率
#mermaid-svg-qGYgCBdGhVF1Uyb4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qGYgCBdGhVF1Uyb4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .error-icon{fill:#552222;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qGYgCBdGhVF1Uyb4 .marker.cross{stroke:#333333;}#mermaid-svg-qGYgCBdGhVF1Uyb4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qGYgCBdGhVF1Uyb4 p{margin:0;}#mermaid-svg-qGYgCBdGhVF1Uyb4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 四种方案全屏帧率对比 I2C 400kHz 软件 SPI 2MHz 硬件 SPI 9MHz SPI+DMA 9MHz 120 110 100 90 80 70 60 50 40 30 20 10 0 全屏帧率 (fps)
7.3 CPU 占用对比
| 方案 | 数据传输占用 | 中断开销 | 总 CPU 占用 | 剩余计算能力 |
|---|---|---|---|---|
| I2C | 85% | <1% | 85% | 几乎无法处理其他任务 |
| 软件 SPI | 90% | <1% | 90% | 完全占满 |
| 硬件 SPI | 60% | 2% | 62% | 可运行简单任务 |
| SPI + DMA | <1% | <5% | <5% | 95% 可用于传感器/算法 |
💡 关键差异:DMA 方案中,1.02KB 数据传输仅需 0.91ms,每帧(8.9ms)内 CPU 仅有 2μs 中断处理开销。这意味着 CPU 在第 1ms 启动 DMA 后,剩下的 7.9ms 完全可以处理传感器采集、PID 计算、通信协议等其他任务。
八、Part 7:故障排查(10 类问题)
🔧 开发中遇到问题?推荐使用 CSDN VIP 搜索解决方案
海量技术问答、专家在线解答,帮你快速定位问题 👇
8.1 硬件类故障
问题 1:OLED 完全不亮,没有任何显示
排查步骤:
| 步骤 | 检查项 | 方法 | 预期结果 | 异常处理 |
|---|---|---|---|---|
| 1 | 供电电压 | 万用表测量 VCC-GND | 3.3V ±0.1V | 检查电源、杜邦线接触 |
| 2 | 接线顺序 | 对照接线表逐根检查 | 全部对应 | 重新按颜色对应接线 |
| 3 | RST 引脚电平 | 万用表测量 RST | > 2.0V(高电平) | 检查 GPIO 初始化代码 |
| 4 | 电荷泵配置 | 确认 Init 中有 0x8D, 0x14 |
已写入 | 最常见遗漏!必须加 |
| 5 | Display ON 命令 | 确认 Init 最后有 0xAF |
已写入 | 添加 Display ON |
⚠️ 最常见原因(占比 60%) :忘记发送电荷泵使能命令(
0x8D, 0x14)。SSD1306 需要内部电荷泵生成 7~15V 面板驱动电压,如果未使能,OLED 面板无高压驱动------逻辑上一切正常但屏幕全黑。
问题 2:OLED 显示花屏/雪花点
排查步骤:
| 步骤 | 检查项 | 方法 | 可能原因 |
|---|---|---|---|
| 1 | SPI 时序 | 逻辑分析仪抓取 SCK/MOSI | CPOL/CPHA 配置错误 |
| 2 | DC 引脚切换时机 | 示波器观察 DC 与 CS 时序 | 命令/数据模式切换错误 |
| 3 | 字库取模参数 | 检查阴码/阳码、逐列/逐行 | 参数不匹配 |
| 4 | 供电纹波 | 示波器 AC 耦合测 VCC | 纹波 >100mV 干扰 SPI 数据 |
最常见原因:字库取模参数与驱动代码不匹配。代码采用「阴码+逐列式+顺向」,但字库用「阳码+逐行式」生成,显示完全错乱。
问题 3:OLED 亮度不均匀,部分区域偏暗
解决方案:
- 检查对比度设置:
OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF);--- 对比度范围 0x00~0xFF - 检查预充电周期:
0xD9, 0xF1--- Phase2 越大亮度越均匀但功耗增加 - 尝试
0xD9, 0x22提高预充电时间
8.2 通信类故障
问题 4:SPI 通信失败,逻辑分析仪显示数据全为 0
| 可能原因 | 诊断方法 | 解决方案 |
|---|---|---|
| SPI 外设时钟未使能 | 检查 __HAL_RCC_SPI2_CLK_ENABLE() |
在 MX_SPI2_Init 前调用 |
| 引脚复用未配置 | 检查 CubeMX 中 PB13/PB15 是否为 SPI2 功能 | 重新配置 Pinout |
| AFIO 时钟未使能 | 检查 __HAL_RCC_AFIO_CLK_ENABLE() |
STM32F103 需要使能 AFIO 时钟 |
| SPI 模式不匹配 | 检查 CPOL/CPHA | CPOL=Low, CPHA=1Edge |
问题 5:DMA 传输完成后屏幕不更新
排查步骤:
| 步骤 | 检查项 | 方法 | 解决方案 |
|---|---|---|---|
| 1 | DMA 中断是否触发 | 在 HAL_SPI_TxCpltCallback 打断点 | 检查 NVIC 中断优先级配置 |
| 2 | CS 是否拉高 | 逻辑分析仪观察 CS 时序 | 确保回调中 OLED_CS_HIGH() |
| 3 | 寻址模式是否正确 | 检查 Init 中 0x20, 0x00 |
必须水平寻址模式 |
| 4 | 列/页范围 | 检查是否发送 0x21,0x00,0x7F 和 0x22,0x00,0x07 |
每次 DMA 前必须设置 |
问题 6:OLED 滚动显示时出现残影
原因分析:
- SSD1306 滚动是硬件级滚动(GDDRAM 内容物理滚动),不会产生残影
- 如果出现残影,说明是软件清屏 → 重新绘制导致的时间差
解决方案: 使用内置硬件滚动命令 0x26/0x27 代替软件逐帧重绘。
8.3 显示内容类故障
问题 7:汉字显示为乱码或空白方块
| 可能原因 | 解决方案 |
|---|---|
| 字库取模参数错误 | 使用 PCtoLCD2002 重新取模:阴码+逐列式+顺向+C51格式 |
| 汉字索引越界 | 检查 CN16x16 数组大小,确保 index * 32 < sizeof(CN16x16) |
| 页地址溢出 | 汉字 16 像素高占 2 页,确保 y <= 6(而非 7) |
| 取模时选中了"阳码" | OLED 单色屏应选阴码(0=灭, 1=亮) |
问题 8:字符串超出屏幕右侧后未换行
解决方案:
c
/* 在 OLED_ShowString 中增加自动换行(见上文代码) */
if (x + char_width > OLED_WIDTH) {
x = 0;
y += pages;
if (y >= OLED_PAGES) return; /* 超出屏幕底部 */
}
问题 9:帧率不稳定,时快时慢
排查步骤:
| 可能原因 | 诊断方法 | 解决方案 |
|---|---|---|
| DMA 重叠调用 | 检查 dma_busy 标志 |
添加 if (dma_busy) return; 防重叠 |
| 中断优先级冲突 | 检查是否有更高优先级中断阻塞 DMA | 提高 DMA 中断优先级 |
| 帧缓冲与 DMA 数据竞争 | 渲染和 DMA 发送共用同一缓冲区 | 引入双缓冲机制 |
| SystemTick 中断延迟 | 检查 SysTick 优先级 | 确保 SysTick 优先级低于 DMA |
问题 10:使用 1.3 寸 SH1106 OLED 时内容偏移
原因分析: SH1106 的 GDDRAM 是 132×64,而 SSD1306 是 128×64。SH1106 实际显示区域 128×64,但 RAM 列地址范围为 0x02~0x81(不移到 0x00)。
解决方案:
c
/* SH1106 适配:修改列起始地址 */
OLED_WriteCmd(0x02); /* Set Column Low Nibble Start = 2 */
OLED_WriteCmd(0x10); /* Set Column High Nibble Start = 0 */
/* 等效于列地址从 0x02 开始 */
💡 调试黄金法则:OLED 调试三步走:
- 写命令测试 --- 发送
0xA5(全屏点亮测试),屏幕全白 = SPI 通信正常- 写数据测试 --- 发送 1024 个
0xFF,全白 = 数据通路正常- 显示内容测试 --- 显示简单字符串 = 字库和坐标逻辑正确
九、总结
9.1 SIC 设计原则提炼
S --- 分层解耦(Separation of Concerns)
核心思想:将 OLED 驱动分为三层------SPI 底层通信、帧缓冲管理层、图形渲染层,各层职责单一。
代码体现:
c
/* 底层:SPI 字节发送(完全不关心上层逻辑) */
static void OLED_SPI_SendByte(uint8_t byte);
/* 中层:帧缓冲操作(不关心 SPI 时序) */
void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t color);
/* 上层:图形渲染(不关心硬件细节) */
void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1);
I --- 中断驱动刷新(Interrupt-Driven Refresh)
核心思想:DMA 传输完成后由中断通知,主循环不受阻塞,CPU 释放给业务逻辑。
代码体现:
c
/* DMA 回调中仅做标志位管理,不阻塞 */
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
OLED_CS_HIGH();
oled_fb.dma_busy = 0; /* 释放标志 */
}
C --- 零拷贝设计(Zero-Copy Design)
核心思想:帧缓冲数组同时作为渲染目标和 DMA 发送源,消除内存拷贝开销。
代码体现:
c
/* 帧缓冲 = 显存镜像 = DMA 发送缓冲区,三位一体 */
uint8_t fb[1024]; /* 唯一的数据副本 */
OLED_SetPixel(x, y, 1); /* 渲染→fb */
HAL_SPI_Transmit_DMA(&hspi2, fb); /* DMA 直接发送 fb,无拷贝 */
9.2 完整代码文件清单
| 文件 | 层级 | 功能 | 行数 | 关键内容 |
|---|---|---|---|---|
Core/OLED/oled.h |
驱动层 | OLED 驱动头文件 | ~70 | 宏定义、API 声明 |
Core/OLED/oled.c |
驱动层 | 核心驱动实现 | ~380 | 初始化序列、帧缓冲、DMA 刷新、图形绘制 |
Core/OLED/oled_font.h |
数据层 | ASCII 字库 | ~140 | 6×8 + 8×16 完整字库 |
Core/OLED/oled_chinese.h |
数据层 | 汉字字库 | ~30 | 16×16 汉字取模数据 |
Core/User/main.c |
应用层 | 主循环演示 | ~80 | 移动矩形动画 + 帧率显示 |
| 合计 | - | 工程级完整代码 | ~700 行 | - |
9.3 扩展方向与进阶路径
| 扩展方向 | 核心内容 | 难度 | 应用场景 |
|---|---|---|---|
| 双缓冲渲染 | 渲染到 Buffer A 时 DMA 发送 Buffer B | ⭐⭐ | 无撕裂动画 |
| U8g2 移植 | 接入 U8g2 图形库(100+ 字体支持) | ⭐⭐ | 复杂 UI 界面 |
| LVGL 移植 | 接入 LVGL 嵌入式 GUI 框架 | ⭐⭐⭐ | 触摸交互界面 |
| SPI+DMA+定时器触发 | 用定时器自动触发 DMA,稳定帧率 | ⭐⭐⭐ | 视频级流畅度 |
| 多屏级联 | 一个 SPI 总线上挂多个 OLED(用 CS 分时复用) | ⭐⭐⭐ | 仪表盘 |
十、参考资料
10.1 CSDN 站内链接汇总
| # | 标题 | 链接 | 覆盖知识点 |
|---|---|---|---|
| 1 | SSD1306 OLED 驱动原理与嵌入式显示实现 | https://blog.csdn.net/play7/article/details/155775387 | GDDRAM 架构 |
| 2 | STM32 入门学习之 OLED 显示屏(原理篇) | https://blog.csdn.net/2301_80770606/article/details/144849983 | SSD1306 引脚详解 |
| 3 | STM32 HAL 库------F103C8T6 使用 SPI 方式点亮 OLED | https://blog.csdn.net/lwb450921/article/details/121866668 | CubeMX 配置 + 代码移植 |
| 4 | STM32 HAL 库驱动 OLED 屏的 SPI 三线模式详解 | https://blog.csdn.net/weixin_28771751/article/details/157881253 | SPI 三线通信 |
| 5 | STM32 OLED 动画卡顿?SPI DMA 优化 U8G2 刷新性能 | https://blog.csdn.net/weixin_42550597/article/details/160138347 | DMA 刷新原理 |
| 6 | STM32 DMA 驱动 4 线 SPI 接口 OLED 高速显示 | https://blog.csdn.net/csaaa2005/article/details/104555134 | DMA 流水线设计 |
| 7 | OLED 汉字取模傻瓜教程 | https://blog.csdn.net/weixin_52177631/article/details/146009617 | 字库取模 |
| 8 | DMA 循环刷新 OLED 屏幕 | https://blog.csdn.net/rampagepbz/article/details/124848915 | 水平寻址模式 DMA |
10.2 官方文档
- SSD1306 Datasheet V2.0 --- Solomon Systech
- STM32F103x8/xB Reference Manual RM0008 --- STMicroelectronics
- PCtoLCD2002 取模软件 --- 字库生成工具
10.3 版本备注
📝 版本备注:本文基于以下版本实测:
硬件环境:
- STM32F103C8T6 最小系统板(晶振:8MHz 外部)
- 0.96 寸 7 针 OLED 模块(SSD1306 驱动,蓝色)
- ST-Link V2 下载调试器
- 逻辑分析仪:Saleae Logic 8(用于验证 SPI 时序)
软件环境:
- STM32CubeMX 6.9.0
- STM32F1 HAL 库 V1.8.0
- Keil MDK-ARM 5.36(编译器 V6.70)
- 字模工具:PCtoLCD2002(阴码 + 逐列式 + 顺向 + C51 格式)
实测日期:2026-07-28
移植注意事项:
- 使用 SPI1 替代 SPI2 时:SPI1 挂载在 APB2(72MHz),分频系数需重新计算
- 使用 SH1106 驱动芯片的 1.3 寸 OLED 时:修改列起始地址为 0x02
- 使用 I2C 接口 OLED 时:更换通信层代码,GDDRAM 操作逻辑完全相同
- GD32F103 系列:代码可直接移植,HAL 库需更换为 GD32 标准固件库
兼容性测试:
- ✅ STM32F103C8T6(主测试平台)
- ✅ STM32F103RCT6(SPI2 引脚相同)
- ⚠️ STM32F407VET6(需调整 CubeMX 配置,SPI 挂载不同总线)
- ✅ GD32F103C8T6(HAL 库兼容)
🚀 你的支持是我持续创作的动力
如果本文帮你解决了 OLED 驱动和 DMA 加速的实际问题,欢迎 开通 CSDN VIP 支持一下 🙏
包含 5000+ 付费课程、10000+ 实战项目源码、专属 AI 编程助手,嵌入式/AI/云计算全覆盖。