简介:这一压缩包面向在STM32平台上驱动SSD1306 OLED显示屏的开发者,提供精简的驱动库实现。核心文件包含1个C源文件和1个头文件,共2个文件,压缩包仅5KB,轻量易用。SSD1306支持128×64像素,通过I2C或SPI接口与STM32通信,适用于嵌入式项目、物联网设备以及DIY显示模块开发。已有234人学习下载,小巧包体内可直接查看lcd.c与lcd.h的驱动代码,涵盖初始化、命令写入、数据传输及基础图形接口。对于需要快速集成OLED显示、了解SSD1306底层驱动写法或参考STM32外设配置的读者,这份代码能提供直观、简洁的实现思路,节省自行查阅数据手册与反复调试的时间。
1. 一块 0.96 寸屏,为什么值得单独封装成库
拿到lcd_ssd1306_stm32.rar这类压缩包,很多人第一反应是“不就是个 OLED 驱动吗,网上开源的一抓一大把”。但实际在 STM32 上把 SSD1306 调通、调稳、调到自己能随意画点画线,远不是复制一份lcd.c那么简单。我见过太多项目卡在初始化序列不对导致花屏、I2C 时序不满足导致偶尔黑屏、显存更新策略不对导致刷新闪烁这些问题上。SSD1306 本质是一颗带 1KB GDDRAM 的 OLED 驱动芯片,支持 128x64 分辨率,通过 I2C 或 SPI 接收命令和数据。真正值得研究的不是“怎么点亮”,而是“怎么把点亮这件事做成一个可复用的软件模块”。这份资源正好落在这个点上:它提供的是lcd.c和lcd.h,一套面向 STM32 的 SSD1306 驱动封装,而不是零散的初始化片段。适合正在做嵌入式项目、需要快速集成显示功能,或者想理解显示驱动内部机制的开发者。
2. I2C 时序与 SSD1306 的通信约定(DMA/中断/阻塞三选一)
2.1 先搞清地址位和传输格式,再写代码
SSD1306 在 I2C 模式下的从机地址是 7 位地址 0x3C(SA0 接地)或 0x3D(SA0 接 VDD)。但实际写代码时,你传给 HAL 或标准库的地址往往是左移一位后的值,即 0x78 或 0x7A,取决于你的 I2C 库是否已经帮你完成了地址位拼接。这里是非常容易踩坑的地方:HAL 库的HAL_I2C_Mem_Write()需要的是 7 位地址左移后的值(即 8 位地址格式),而标准库自己写的模拟 I2C 则多半直接用 7 位地址。先确认你的底层用的是哪种,再决定填 0x3C 还是 0x78,否则设备 ACK 都不会回复。
SSD1306 的 I2C 传输格式分为控制字节和数据字节两类。控制字节的高 7 位固定为 0,最低位 Co 为 0 表示后续字节全是数据,D/C# 位为 0 表示后续字节是命令,为 1 表示是数据。举个例子:要发送命令 0xAF(开启显示),你要先发控制字节 0x00,再发 0xAF;要发送显示数据,先发控制字节 0x40,再发数据字节。理解了这个格式,你就能解释为什么驱动库里到处都是0x00和0x40这两个魔数,也能在移植到其他平台时自己构造发送函数。
// 以 STM32 HAL 库为例 uint8_t control_cmd = 0x00; // Co=0, D/C#=0,后续字节为命令 uint8_t control_data = 0x40; // Co=0, D/C#=1,后续字节为数据 uint8_t cmd = 0xAF; // SSD1306_DISPLAYON HAL_I2C_Mem_Write(&hi2c1, 0x78, control_cmd, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 100);代码说明:HAL_I2C_Mem_Write()是 STM32 HAL 库中带寄存器地址的 I2C 写函数。SSD1306 没有寄存器地址的概念,这里的control_cmd被当作“内存地址”传入,实际上是在利用该函数的封装结构,把控制字节和后续数据一起发出。I2C_MEMADD_SIZE_8BIT表示地址是 8 位宽度,100是超时时间(毫秒)。注意地址参数填的是0x78,这是 0x3C 左移一位的结果,HAL 内部不会再移位了。
2.2 初始化序列:逐条命令拆解
不管你是用 STM32 标准库、HAL 库,还是 LL 库,SSD1306 的上电初始化序列是绕不开的核心。网上流传的初始化代码大多来自 Adafruit 或 U8g2 的简化版,但直接抄容易忽略一些细节。下面这份序列是兼容性较好的一套,逐条解释每条命令的作用。
uint8_t init_cmds[] = { 0xAE, // 关闭显示 0xD5, 0x80, // 显示时钟分频/振荡频率 0xA8, 0x3F, // 复用率=64(128x64 屏) 0xD3, 0x00, // 显示偏移=0 0x40, // 起始行=0 0x8D, 0x14, // 电荷泵使能 0x20, 0x00, // 内存寻址模式:水平寻址 0xA1, // 列段重映射(左右镜像修正) 0xC8, // 行扫描方向(上下镜像修正) 0xDA, 0x12, // COM 引脚硬件配置 0x81, 0xCF, // 对比度=207 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH 电平 0xA4, // 使用 GDDRAM 内容 0xA6, // 正常显示(非反色) 0x2E, // 关闭滚动 0xAF // 开启显示 };参数说明:0xD5和0x80组合里,0x80表示分频比和振荡频率保持默认;0xA8后的0x3F对应 64 行,如果是 128x32 屏要改成0x1F。0xDA后面的0x12是 128x64 屏的常见配置,如果换成 128x32 或异形屏,这里可能需要改成0x02。0x81后面的0xCF是对比度,调节范围是 0x00~0xFF,值越大越亮,但功耗也越高。
2.3 阻塞、中断还是 DMA:不同场景的选择
SSD1306 的 I2C 速率一般可以跑到 400kHz(快速模式)甚至 1MHz(快速模式+)。在 STM32 上,最简单的实现是阻塞式发送,即HAL_I2C_Mem_Write()加超时。但如果你在主循环里频繁刷新屏幕,阻塞式发送会让 CPU 白白等 I2C 总线传输完成,每次刷全屏 1KB 数据在 400kHz 下大约需要 20ms。如果是做菜单切换、动画效果,这个时间会被放大成可感知的卡顿。
我一般在低功耗或需要主循环及时响应的项目里用 DMA 方式发送显存。方式是先HAL_I2C_Mem_Write_DMA()发起传输,传输完成在中断回调里置标志位。需要注意的一点是,DMA 发送期间不能修改被发送的缓冲区,也就是你的显存数组,否则数据会错乱。常见做法是双缓冲:一个缓冲区用于 DMA 发送,另一个用于绘图,发送完成后交换指针。
3. lcd.c 的内部结构与显存管理策略
3.1 显存数组的大小和布局:Page 的概念
SSD1306 的内部 GDDRAM 按页(Page)组织,一页是 8 个像素行,128 列。128x64 的屏总共有 8 页,每页 128 字节,合计 1024 字节。你的驱动库核心就是维护这 1024 字节的显存数组,所有绘图操作都先改这个数组,最后才一次性推送到屏幕。这个设计的意义在于:SSD1306 不支持读回 GDDRAM 内容,如果你直接往屏幕写点而不维护本地显存,就无法实现擦除、覆盖、画矩形这类操作。
// lcd.h 中的典型定义 #define SSD1306_WIDTH 128 #define SSD1306_HEIGHT 64 #define SSD1306_PAGES (SSD1306_HEIGHT / 8) // 8 static uint8_t framebuffer[SSD1306_PAGES][SSD1306_WIDTH];代码说明:这个二维数组的布局是“页优先”而不是“行优先”。第一维是页编号(0~7),第二维是列编号(0~127)。写代码时要注意,画一个像素点时,需要同时计算它属于哪一页、在该页内的哪一位。页编号 = y / 8,页内位偏移 = y % 8。如果你用一维数组uint8_t framebuffer[1024],那么访问方式就是framebuffer[page * 128 + x],两种方式本质上等价,但二维数组可读性更好。
3.2 像素操作:setPixel 是绘图库的根基
所有高级绘图函数,包括画线、画矩形、画圆、显示字符,最终都会落到setPixel(x, y, color)这一个函数上。这个函数的质量直接决定了整个库的稳定性和效率。一个常见错误是忘了对坐标做边界检查,导致数组越界写坏内存——这在嵌入式环境里极难排查,因为表现出来可能是某个无关变量被莫名修改。
void SSD1306_SetPixel(uint8_t x, uint8_t y, uint8_t color) { if (x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT) return; // 边界检查 if (color == SSD1306_COLOR_WHITE) { framebuffer[y / 8][x] |= (1 << (y % 8)); } else { framebuffer[y / 8][x] &= ~(1 << (y % 8)); } }逻辑说明:y / 8定位到页,y % 8定位到该页内的位偏移。置白用按位或,置黑用按位与加深色掩码。这个实现是标准做法,时间复杂度 O(1),不依赖任何库函数。边界检查放在最前面,保证传入的坐标非法时直接返回,不产生副作用。
3.3 刷新函数:全量刷新与增量刷新
lcd.c里最耗时的函数就是刷新。最基本的实现是遍历所有页,把整块显存通过 I2C 发送出去。这样做简单可靠,但存在两个问题:一是数据量固定为 1KB,无论你只改了一个像素还是全屏变化,耗时都一样;二是在 400kHz I2C 下,1KB 数据加上控制字节,实际耗时约 21ms,这还不算命令开销。
增量刷新是常见的优化手段。思路是记录脏区域(dirty rectangle),只发送被修改过的页和列范围。SSD1306 支持设置列地址范围(0x21 命令)和页地址范围(0x22 命令),发送命令后只需传输对应区域的数据。这个优化对菜单、时钟这类局部变化的界面效果显著,全屏刷新率可以从约 46 FPS 提升到 100 FPS 以上。
4. 绘图库扩展:线、矩形、字符和中文显示
4.1 Bresenham 画线算法在 1KB 显存上的实现
基础setPixel有了之后,接下来就是画线、画矩形这些图元。直线绘制有一个经典算法叫 Bresenham,它只用整数加法和比较,不使用浮点和乘法,非常适合单片机。以下是适用于 SSD1306 显存结构的实现,处理了斜率绝对值大于 1 的情况,兼容各个方向的线段。
void SSD1306_DrawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) { int16_t dx = x1 > x0 ? x1 - x0 : x0 - x1; int16_t dy = y1 > y0 ? y1 - y0 : y0 - y1; int16_t sx = x0 < x1 ? 1 : -1; int16_t sy = y0 < y1 ? 1 : -1; int16_t err = dx - dy; while (1) { SSD1306_SetPixel(x0, y0, color); if (x0 == x1 && y0 == y1) break; int16_t e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } }逻辑说明:dx和dy取绝对值,sx和sy表示 x 和 y 方向的步进方向。核心是误差项err,每次循环比较2*err和dy、dx的关系来决定下一步沿 x 还是沿 y 方向走。该算法的优点是只用整数运算,栈开销极小,在 STM32F103 这类 Cortex-M3 内核上执行很快,不需要math.h。
4.2 字符显示:从 8x6 字体到任意尺寸缩放
字符显示的常见做法是把 ASCII 字符集做成点阵数组,每个字符用一个 8x6 或 8x8 的位图表示。lcd.c里一般会包含一个font8x6[]数组,每个字符占 6 字节,每字节对应一列、每位的 1 表示该像素点亮。显示字符时,先定位到目标页,然后把字模按位移入显存。注意 SSD1306 的纵向寻址特性,字符要按列展开,而不是按行展开。这也是新手移植字库时最容易搞错的地方,直接把 PC 上按行存储的字模数据拷进去,屏幕上就是乱码。
如果要显示更大的字体,可以做一个缩放函数。思路是逐位判断源字模的像素,如果是 1,就在目标区域画一个scale x scale的方块。缩放到 2 倍就是 16x12 字号,缩放到 3 倍就是 24x18,代价是显存占用按平方增长,且刷新时间变长。对于 128x64 的屏,2 倍缩放是性价比最高的选择。
4.3 中文显示:16x16 点阵字库的取模与寻址
热词里频繁出现“lcd屏显示中文”,这确实是 SSD1306 使用中的一个共性需求。SSD1306 内部没有中文字库,要显示中文必须自己做字库。最常见的方式是用取模软件(如 PCtoLCD2002)生成 16x16 点阵字模,取模方式选择“横向取模、字节正序、高位在前”。每个 16x16 汉字占用 32 字节,存储顺序是先左半部分 16 字节(从上到下两列一组),再右半部分 16 字节。
显示中文时,关键是理解字模数据和显存页的对应关系。一个 16x16 汉字在纵向上跨越 2 页(每页 8 像素行),在横向上占 16 列。所以显示函数需要分两部分:先处理上半部分(页 n),写入前 16 字节;再处理下半部分(页 n+1),写入后 16 字节。如果汉字的 y 坐标不是 8 的倍数,处理起来会更麻烦,需要移位拼接。我的常见做法是把所有中文的显示坐标都对齐到 8 的倍数,即 y = 0, 8, 16, 24…,这样避免移位操作,代码简洁且不会出错。
void SSD1306_DrawChinese(uint8_t x, uint8_t page, const uint8_t *chars, uint8_t count) { for (uint8_t i = 0; i < count; i++) { const uint8_t *glyph = chars + i * 32; // 每个汉字 32 字节 // 上半部分写入 page,下半部分写入 page+1 for (uint8_t col = 0; col < 16; col++) { framebuffer[page][x + col] = glyph[col]; // 上半部分 framebuffer[page + 1][x + col] = glyph[col + 16]; // 下半部分 } x += 16; // 每个汉字占 16 列 } }逻辑说明:page是起始页编号,chars指向中文字模数组首地址。每个汉字按“上半页 16 字节 + 下半页 16 字节”排列,直接写入对应页的对应列。这个函数假设起始页和起始列都是对齐的,实际工程中足够用了。参数x是起始列,count是连续显示的汉字个数,可用于一次输出一个中文字符串。
5. 对比度调节、SSD1315 兼容性与低功耗设计
5.1 对比度命令 0x81 与 OLED 亮度调节的误区
很多人想调亮度第一反应是改供电电压或用 PWM 控制电源,其实 SSD1306 提供了原生的对比度寄存器,命令是 0x81,后跟一个 0x00~0xFF 的值。这个寄存器控制的是 OLED 驱动电流的占空比,值越大显示越亮。但它的副作用是:对比度调高时功耗增加,长时间高亮度会加速 OLED 像素老化(烧屏)。在 128x64 的屏上,建议日常使用 0x7F~0xCF 之间,需要高亮阳光下直射时才调到 0xFF。
除了对比度寄存器,还有电荷泵电压选项可以间接影响亮度。命令 0x8D 后跟 0x14 是使能电荷泵,调整为 0x1C 可以切换 DC-DC 电压。但这片区域不同厂商的模块设计差异较大,有些模块的电荷泵电路已经在 PCB 上固定了参数,软件调节空间有限。我的经验是:优先调对比度寄存器,效果不明显再去查模块原理图决定是否动电荷泵。
5.2 SSD1306 和 SSD1315 的兼容性:拿到屏先确认芯片
热词里反复出现“ssd1306和ssd1315的区别”,这里有必要解释清楚。SSD1315 是 SSD1306 的升级替代品,两者在指令集上高度兼容,但有一个关键差异:SSD1315 内部集成了 DC-DC 电荷泵所需的电容和电阻,PCB 设计更简洁,而 SSD1306 通常需要外部电荷泵电容。软件层面,SSD1315 对初始化序列中某些命令的处理略有差异,尤其是 0xDA(COM 引脚硬件配置)命令后的参数值。
实际项目中,同一款 0.96 寸 OLED 模块,早期可能用 SSD1306,后期改版可能换成了 SSD1315,外观和引脚定义完全一样。如果你用本项目的lcd.c驱动新买的屏,出现初始化正常但显示残缺、对比度异常偏低或竖条纹闪烁,优先怀疑芯片型号变了。处理方式有两种:一是按 SSD1315 的数据手册微调初始化序列中的0xDA参数,二是在初始化完成后多发一次0xAF和0xA5(忽略 RAM 内容显示)来确认屏幕是否响应。
5.3 低功耗场景:显示休眠与唤醒
对于电池供电的设备,OLED 全亮时的功耗大约在 20~30mA,这在很多场景下是不可接受的。SSD1306 提供了 Display Off 命令(0xAE)和 Display On 命令(0xAF),进入关闭状态后屏幕熄灭,功耗大幅下降。但注意,仅仅发 0xAE 并不够,电荷泵还在工作,仍然有毫安级的电流消耗。彻底省电需要发 0x8D 命令关掉电荷泵,即 0x8D, 0x10。唤醒时恢复的顺序是:先开电荷泵 0x8D, 0x14,延时约 100ms 等电压稳定,再发 0xAF 开启显示。
void SSD1306_Sleep(void) { uint8_t cmd = 0xAE; // 关闭显示 SSD1306_WriteCmd(&cmd, 1); cmd = 0x8D; // 关闭电荷泵 SSD1306_WriteCmd(&cmd, 1); cmd = 0x10; SSD1306_WriteCmd(&cmd, 1); } void SSD1306_WakeUp(void) { uint8_t cmd = 0x8D; // 开启电荷泵 SSD1306_WriteCmd(&cmd, 1); cmd = 0x14; SSD1306_WriteCmd(&cmd, 1); HAL_Delay(100); // 等待电荷泵稳定 cmd = 0xAF; // 开启显示 SSD1306_WriteCmd(&cmd, 1); }逻辑说明:SSD1306_WriteCmd()是对底层 I2C 写命令函数的封装,内部会构造控制字节 0x00 和命令字节一起发送。休眠时先关显示再关电荷泵,避免直接断电荷泵导致屏幕出现残影。唤醒时顺序反过来,先开电荷泵再开显示,中间加延时是给 DC-DC 电路一个稳定时间,这是经验值,实际调试中如果发现唤醒后屏幕有闪烁,可以把延时加到 150ms。
5.4 滚动功能:用 0x26/0x27 命令实现无 CPU 开销的动画
SSD1306 内置了硬件滚动功能,不需要 CPU 干预画面就能自动上下或左右滚动。命令格式是 0x26(右滚)或 0x27(左滚),后跟 5 个参数:空字节、起始页、滚动间隔、结束页、空字节。滚动间隔有 0x00~0x07 共 8 档,对应从 2 帧到 256 帧的间隔。这个功能在实现跑马灯、通知条滚动时很实用,完全不占 CPU。
想停止滚动用 0x2E 命令,想重新开始用 0x2F。注意一个坑:设置了滚动后,如果此时往显存写数据或调用刷新函数,滚动效果可能会异常或停不下来。稳妥的做法是:启动滚动前关闭显示,设置参数,开始滚动,最后再开显示。调试时如果发现画面滚动异常,先查是不是初始化序列里漏了 0x2E(关闭滚动)命令——这一点在项目自带的lcd.c里是有的,但如果你自己精简过初始化命令,很容易丢。
本文还有配套的精品资源,点击获取