SSD1306与SSD1315驱动详解:OLED稳定显示与跨平台移植实践
2026/9/8 1:41:04 网站建设 项目流程

简介:OLED驱动代码是一份面向嵌入式开发者和电子爱好者的显示屏控制示例,适用于SSD1306和SSD1315控制器,解决OLED屏初始化、像素绘制、字符显示及I2C/SPI通信等常见问题。资源包共4个文件,包含2个头文件与2个C源文件,其中font.h/font.c提供字库与字符映射,oled.h/oled.c封装控制器寄存器配置、显示刷新和绘图接口,整体仅13KB,轻量易移植。已有349人浏览学习,适合入门到进阶的STM32、Arduino等平台开发者参考。压缩包内代码按模块清晰划分,可直接阅读控制器寄存器配置、字符与图形绘制流程,也可提取为独立驱动层融入自有工程。这份实现将底层寄存器操作与上层绘图分离,注释清晰,便于二次开发,能帮助理解OLED自发光原理下的灰度控制与刷新机制,缩短驱动调试周期。 上个项目里帮客户调一块0.96寸OLED模块,板子丝印写的SSD1306,拆开看主控却是SSD1315。这种混用情况这两年太常见了,于是我把SSD1306/SSD1315的驱动代码专门整理了一遍。它们指令寄存器基本兼容,但电荷泵、上电闪屏、分辨率这些细节差异会在量产时给你好好上一课。这篇文章不是简单贴一份驱动,而是把“OLED怎么才能稳定点亮、显示汉字、不闪烁、好移植”的完整思路拆开讲:命令通道怎么建、显存怎么组织、函数怎么设计、换平台怎么改、出问题怎么查。适合两类人:刚学会I2C想上手显示屏的新手,还有那种被SSD1315坑过、想把代码一次写稳的工程师。

1. OLED驱动方案选型:SSD1306与SSD1315的关系

1.1 SSD1306和SSD1315到底差在哪里

很多同学以为SSD1306和SSD1315是两个完全不同的芯片,得维护两套驱动。实际不是这样,SSD1315可以理解为SSD1306的升级兼容款,绝大多数寄存器定义一致,I2C地址、页寻址、列寻址、显示RAM的组织方式都相同。一套驱动代码,基本能同时点亮两种芯片。

但量产不是“基本能用”就行的。我踩过最典型的一个坑:某批次屏幕换成了SSD1315后,整机待机电流明显变大,排查半天,最后发现是初始化序列里0x8D 0x14这条电荷泵命令带来的差异。SSD1306必须靠这条命令开启内部升压电路,否则屏不亮;SSD1315集成度更高,对电荷泵的处理逻辑有调整,部分模块开机后会自动管理升压,你在初始化里强行开一次,反而会让部分批次功耗偏高。稳妥做法是保留这条命令,因为它在SSD1306上必须存在,但在SSD1315量产前要实测功耗和显示均匀度。

对比项SSD1306SSD1315
定位经典款,资料极多升级兼容款,逐步成为主流
charge pump必须通过0x8D 0x14开启多数兼容该命令,部分批次可省略
管脚兼容性I2C/SPI均支持与SSD1306基本一致
功耗与ESD正常整体略优,但电荷泵设置不当会反噬
驱动代码移植成本参考代码多可直接复用SSD1306初始化序列

所以我的建议是:代码层面按SSD1306来写,但把初始化序列抽成一个const数组,碰到SSD1315后先跑一版对比上电功耗。差别太大就删掉0x8D 0x14,差别不大就不用管,继续沿用经典序列就行。

1.2 为什么多数模块选I2C接口

OLED驱动接口主流有I2C和SPI两种,少数模块还带并口。SPI速度确实快,4线模式时钟能跑到10MHz甚至更高,适合做动画刷新;I2C标准模式只有100kHz,快速模式400kHz,全屏128x64对应1KB显存,按I2C 400kHz大概算一下,一帧数据约10到15毫秒,刷个菜单、显示个汉字完全够用,但刷视频、做平滑动画就会卡。

真正让我坚持用I2C的原因不是速度,是省引脚。I2C只要SCL和SDA两根线,而且可以跟其他I2C传感器挂同一条总线上;SPI至少要DC、CS、SCLK、MOSI四根线,有些模块还要求MISO回读状态,引脚占用翻倍。小封装主控上,I2C能省出好几个GPIO用作按键、背光控制。另外软件模拟I2C比SPI简单很多,只要是两个普通GPIO就能点亮,移植性极强。所以如果你不是做游戏机、动画表盘这类高刷新场景,I2C是性价比最高的选择。

2. 驱动代码的核心骨架:I2C命令通道与显存模型

2.1 命令和数据怎么区分

OLED驱动代码本质上只有两件事:写命令、写数据。I2C传输时,设备地址之后紧跟着一个控制字节,0x00表示后续字节全部按命令解析,0x40表示按显示数据写入。

这里经常有新手卡住:为什么模块背面写地址是0x3C,代码里却是0x78?因为I2C寻址用的是7位地址,控制器内部实际也是按0x3C来匹配,但I2C协议在总线上发送时要把7位地址左移一位,最低位补0表示写、补1表示读。所以写地址就是0x3C << 1 = 0x78。如果你的模块SA0引脚接了高电平,7位地址会变成0x3D,写地址就是0x7A。不要看到别人都写0x78就照抄,先确认板子上的地址电阻。

底层函数按下面这样设计最清晰:

#define OLED_ADDR_7BIT 0x3C #define OLED_ADDR_WRITE (OLED_ADDR_7BIT << 1) static void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] = {0x00, cmd}; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR_WRITE, buf, 2, 100); } static void OLED_WriteData(uint8_t dat) { uint8_t buf[2] = {0x40, dat}; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR_WRITE, buf, 2, 100); }

单字节命令和数据这么发没问题。但批量写显存时,建议把数据和0x40组合发送,比如连续写一整行128字节,就组一个129字节的buffer再一次性发出去,避免每写一个字节都重新发起一次I2C总线操作,效率能差出好几倍。

2.2 显存模型与页寻址

SSD1306/SSD1315内部自带1KB显示RAM,128x64像素正好对应8192个bit。但它不是按常规的(x,y)坐标直接编址的,而是把屏幕纵切成8个横条,每条高8像素,称为一页。总共有PAGE0到PAGE7共8页,每页又横向分成128列,每一列对应一个字节,该字节的bit0到bit7从上到下对应这8行像素。

我用一个生活化类比帮你记:想象一块LED广告牌被切成8条横幅,每条横幅上有128个小竖排灯珠,每个竖排只有8个灯珠,写入一个字节就能同时控制这8个灯珠的亮灭。点亮某个点(x,y),就得先算出它落在哪个横幅上:page = y / 8,再算出在这个竖排里的偏移量:bit = y % 8,然后把这个字节的第bit位置1。

默认页寻址模式下,每写一个字节,列地址自动加1,但页地址不会变。很多人的花屏问题就出在这里:连续写了几百个字节以为在写整屏,实际上全写在当前这一页里,写满128列之后自动回绕到第0列继续覆盖,屏幕上永远只有一条横带在变化。

所以驱动里必须先实现一个定位函数,告诉控制器“接下来要写第几页、第几列”:

void OLED_SetPos(uint8_t page, uint8_t col) { OLED_WriteCmd(0xB0 + page); // 页地址 0xB0~0xB7 OLED_WriteCmd(0x00 + (col & 0x0F)); // 列地址低4位 OLED_WriteCmd(0x10 + ((col >> 4) & 0x0F)); // 列地址高4位 }

这个函数是整个显示系统的地基,后面所有画点、写字、画图都要靠它先定位再写数据。

2.3 初始化序列逐条拆解

初始化序列不用死记,但一定要理解每条命令在干什么,否则换了一款模块就可能莫名其妙点不亮。下面是一套经典到不能再经典的SSD1306初始化序列,我用的是128x64分辨率:

static const uint8_t OLED_InitCfg[] = { 0xAE, // 关闭显示 0x20, 0x00, // 页寻址模式 0x21, 0x00, 0x7F, // 列地址范围0~127 0x22, 0x00, 0x07, // 页地址范围0~7 0x40, // 显示起始行0 0xA1, // 段重映射 0xC8, // COM扫描方向反向 0xA8, 0x3F, // 1/64路复用 0xD5, 0x80, // 时钟分频与振荡频率 0xD3, 0x00, // 显示偏移0 0xD9, 0xF1, // 预充电周期 0xDA, 0x12, // COM引脚配置 0xDB, 0x30, // VCOMH电压 0x81, 0x7F, // 对比度 0xA6, // 正常显示,非反显 0x8D, 0x14, // 开启charge pump 0xAF // 打开显示 };
命令作用注意点
0xAE / 0xAF关显示 / 开显示初始化开头先关,末尾再开
0x20 0x00设置页寻址模式如果这里设成水平寻址,写显存逻辑会完全不同
0x21 0x22设置列和页范围只影响后续写入边界
0x40显示起始行改成其他值会导致画面上下平移
0xA1 / 0xC8段重映射 + COM反向组合使用保证正像,调反会变镜像
0xA8 0x3F128x64路复用128x32的屏必须改成0x1F
0x8D 0x14开电荷泵SSD1306必须,SSD1315部分批次可省

如果你用的是128x32的OLED,有三处要改:0xA8从0x3F改成0x1F,0x22的页范围从0x00 0x07改成0x00 0x03,0xDA的COM引脚配置从0x12改成0x02。其他基本可以不动。这个坑很典型:128x32屏用了128x64的初始化序列,往往只有上半屏有内容,下半屏是乱的。

3. 显示函数设计与性能优化

3.1 点、线、矩形的底层实现

写过字符显示之前,先要把单点绘制这件事想明白。OLED不像字符型LCD那样有内置字库,所有内容都是靠像素组合出来的,所以画点是最底层的能力。比较推荐的做法是先在MCU内存里准备一个1KB的显存数组g_oled_ram,把控制器里的显存镜像一份。平时画点、画线、写字都只改这块内存,需要更新时再统一推屏。

uint8_t g_oled_ram[8][128]; void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t on) { if (x >= 128 || y >= 64) return; uint8_t page = y / 8; uint8_t bit = y % 8; if (on) { g_oled_ram[page][x] |= (1 << bit); } else { g_oled_ram[page][x] &= ~(1 << bit); } }

这里有个细节容易搞反:一个字节里的bit0对应页内最上面一行还是最下面一行?不同取模软件和不同控制器的习惯可能不一样。标准SSD1306里,字节的bit0对应页内最上面一行,bit7对应最下面一行。写驱动前最好先用0x01和0x80两个值各画一次,确认你的位序假设是对的,不然整个字库全是翻过来的。

清屏函数也要配套写好。注意清屏不只是把数组清零,还要把这1KB全写回控制器,否则下次推屏会把你认为“已经清了”的旧数据又刷上屏:

void OLED_Clear(void) { memset(g_oled_ram, 0, sizeof(g_oled_ram)); for (uint8_t page = 0; page < 8; page++) { OLED_SetPos(page, 0); for (uint8_t col = 0; col < 128; col++) { OLED_WriteData(g_oled_ram[page][col]); } } }

3.2 字符与汉字的显示

字符显示本质上是查字库,然后把字模数据逐字节写到显存。ASCII字符一般用8x6或者8x16点阵,8x6的点阵高度是6,在一页内就能画完;8x16要拆成上下两半,第一页里写上半部分8个字节,第二页里写下半部分8个字节。

我常用的是8x16字体,虽然比8x6占双倍显存,但显示在0.96寸屏上可读性明显更好。核心函数长这样:

void OLED_ShowChar8x16(uint8_t page, uint8_t col, uint8_t ch) { const uint8_t *font = &AsciiFont8x16[ch * 16]; OLED_SetPos(page, col); for (uint8_t i = 0; i < 8; i++) { OLED_WriteData(font[i]); } OLED_SetPos(page + 1, col); for (uint8_t i = 0; i < 8; i++) { OLED_WriteData(font[i + 8]); } }

汉字就更直接了,我用的是16x16点阵,每个汉字32字节。网上取模软件很多,我用的是PCtoLCD2002,关键设置是“横向取模、行行式、每行16位、字节正序”。取出来的数据前16字节对应汉字上半部分,后16字节对应下半部分,显示时用两个页定位:

void OLED_ShowChinese16(uint8_t page, uint8_t col, const uint8_t *hz) { OLED_SetPos(page, col); for (uint8_t i = 0; i < 16; i++) { OLED_WriteData(hz[i]); } OLED_SetPos(page + 1, col); for (uint8_t i = 0; i < 16; i++) { OLED_WriteData(hz[i + 16]); } }

实际项目中我不会一边显示一边直接写屏,而是先往g_oled_ram里填,再统一Flush。至于“OLED汉字”这个话题,很多新手卡在编码转换上。MCU里存汉字字模通常不跟字符串绑定,而是用专门的字库索引表。如果RAM和Flash够大,可以把常用汉字做成数组,GB2312编码直接查下标;如果不够,就只能用U8g2这类库,或者配合外部Flash放字库。

3.3 刷新与局部窗口

很多人在刷屏时会遇到“闪烁”问题。SSD1306这类单色OLED每秒刷新率本身不低,但如果你在主循环里“清屏、画字、推屏”三步连做,视觉上会明显看到内容闪一下。原因很简单:清屏后推了全零数据,再推内容,中间有一帧全黑画面被肉眼捕捉到了。

解决办法就是我在前面说的双缓存思路,但和一般的双缓存不同:SSD1306内部RAM本身就是一帧完整画面,你只需要在MCU内存里维护g_oled_ram作为逻辑画面,推屏时不要整帧推,而是对比新旧缓存,只推变化的字节。这样I2C总线占用低很多,闪烁问题也基本消失。

如果对局部刷新效率有更高要求,可以改用水平寻址模式。在0x20 0x01下设置好列地址范围和页地址范围后,连续写数据会在不同页之间自动跳转,刷新一个矩形区域只需一次连续传输,不用反复发SetPos命令。平时我自己还是默认用页寻址,因为逻辑简单、不容易写错。只有在做类似示波器波形、动态图表这类频繁刷新局部区域的场景,我才会切到水平寻址模式。

4. 跨平台移植的移植点与坑

4.1 标准库与HAL的差异

现在很多人用STM32的HAL库,也有人还在维护老项目的标准库。两种库的底层I2C发送接口不一样,但驱动层完全不用改。关键是把“向控制器发送一段字节”这个动作抽象出来。

HAL库下会这样封装:

static void OLED_I2C_Send(uint8_t control, uint8_t *data, uint16_t len) { uint8_t buf[130]; buf[0] = control; memcpy(&buf[1], data, len); HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR_WRITE, buf, len + 1, 100); }

标准库或者寄存器版,核心思路也类似,无非把HAL_I2C_Master_Transmit换成自己的I2C发送函数。软件模拟I2C则更自由,只要把SCL和SDA的操作宏替换掉,就能在任意单片机上跑:

#define OLED_SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_6) #define OLED_SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define OLED_SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_7) #define OLED_SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_7)

软件I2C踩过的最大坑是延时不足。I2C时序对SCL高电平时间有要求,很多模拟驱动里delay_us(2)写得太小,短距离单块屏可能没事,线一长或者上拉电阻偏小,数据就错乱。这个延时要留足,不能为了追求速度把时序压到临界值,量产稳定性远比那几十微秒重要。

4.2 显示驱动与主业务解耦

驱动代码写到最后,真正值钱的往往是“分层”和“解耦”。我见过不少人把OLED_WriteCmd直接写进业务逻辑里,菜单页面代码和底层I2C收发搅成一锅粥,换个屏幕型号就要重写业务。

我更建议按“应用层-显示抽象层-驱动层-硬件层”来组织:

  • 驱动层只负责命令和数据收发,对外提供SetPos、Clear、ShowChar、ShowString等API。
  • 显示抽象层维护g_oled_ram,把画点、画线、画矩形等绘图操作约束在缓存上。
  • 应用层只调用“显示字符串”“显示数字”“刷新屏幕”这类业务接口,完全不接触I2C寄存器。

这样做的直接好处是:从SSD1306换成SSD1315,甚至换成其他I2C接口的OLED控制器,只需要改驱动层和初始化序列;从STM32换成ESP32、GD32、CH32,只需要替换底层I2C发送函数;从I2C换成SPI接口屏,也只需要把整帧数据通过SPI刷进去,上层一个字都不用动。

5. 常见故障排查与避坑清单

5.1 上电乱亮、黑屏、花屏怎么定位

先说一个经常被误判的现象:OLED屏连上电源就亮,甚至显示一堆随机亮点。这其实不算故障。OLED控制器上电后RAM默认值随机,未初始化时会把RAM内容直接输出到屏上,所以出现雪花点完全正常,初始化序列执行完就会恢复受控。

真正让人头疼的是初始化后依然黑屏。我的排查思路一般按这个顺序来。

第一步,确认I2C通信。用示波器量SCL和SDA,看不到波形就查上拉电阻、地址和代码里的地址是否一致。OLED模块本身大多自带上拉电阻,但如果你用长杜邦线连接,或者模块上拉被焊掉了,I2C可能通信失败。还有一个我遇到过的案例:单片机引脚内部上拉和模块上拉共存的阻值过低,导致SDA被拉低,总线直接被锁死。

第二步,确认电荷泵。SSD1306如果没有执行0x8D 0x14,屏幕大概率是暗的,注意不是一点不亮,而是对比度极低,像没睡醒一样。补上这条命令再调高0x81对比度,效果立竿见影。

第三步,确认控制字节。很多时候屏幕能亮一点,但显示的内容全是乱的,那基本是控制字节写错了,或者命令和数据混着发。0x00开头的报文被当成数据写了,画面会变成一坨完全不可控的噪点。

5.2 显示方向、字符异常与刷新慢的处理

显示方向颠倒或镜像,不是硬件装反了,而是段重映射和COM扫描方向配置反了。常规正像是0xA0不重映射、0xC0 COM正向;如果你用的是0xA1和0xC8,显示就是反过来的效果。反过来用这两个命令,画面就会左右翻转或者上下颠倒。根据自己的安装方向调这两条就行,不用改PCB。

字符显示出来不对,优先怀疑取模方式。很多取模软件默认是“纵向取模”,生成的字节排列顺序跟SSD1306页寻址方式不匹配,显示出来字就是倒的、或者左右镜像。建议先用0x01和0x80测试字节确认位序,再回取模软件里调“横向取模”或“纵向取模”的设置。

刷新慢主要怪I2C速度。128x64整屏是1KB数据,400kHz I2C下理论上限约50KB/s,实际传输加上命令开销,一帧大概要10毫秒以上。如果再加上双缓存整帧推,刷新率会更难看。解决方法要么局部推屏,只刷新变化区域;要么换SPI接口,速度能提升一个数量级。

5.3 常见问题速查表

现象大概率原因处理方式
上电有随机亮点,初始化后正常控制器RAM随机值,正常现象无需处理
初始化后完全黑屏缺0x8D 0x14电荷泵命令,或0xAF后没开显示补电荷泵和0xAF
屏幕有弱影、对比度极低0x81对比度太低,或电荷泵未开启对比度调到0x7F以上
只显示上半屏或画面偏移128x32屏误用128x64初始化参数修改0xA8、0x22、0xDA
字符颠倒或镜像段重映射和COM扫描方向配置不对调0xA0/A1与0xC0/C8
全屏花点、内容不可控控制字节0x00和0x40用混检查命令和数据发送路径
I2C总线被拉死,波形异常上拉电阻缺失或阻值过小模块端补4.7k上拉
内容闪烁严重清屏->绘制->推屏三步产生黑帧使用缓存按差异推屏
刷新率不够,动画卡顿I2C速率限制换SPI,或做局部窗口刷新

很多OLED显示问题,拆开看最后都落在“初始化序列、显存地址、位序、通信时序”这四件事上。只要把这四件事确认清楚,驱动代码基本不会出大问题。

最后分享一个我自己的习惯:拿到一块新OLED屏,我不会急着写满屏汉字,而是先跑三件事——初始化后写0xA5命令全屏点亮,再写0xA4回到RAM显示模式,最后往第一行写0x55和0xAA的循环图案,确认数据位序和I2C通道都正常。只要这三步过了,驱动代码基本就稳了。后面再慢慢调显示方向、字库和刷新策略。如果你手头也有SSD1315和SSD1306混用的情况,建议把初始化序列抽成一个const数组,换屏时只改数组,别动上层显示函数。这个习惯能帮你省掉大量来回查资料的时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询