
简介这一压缩包面向在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 位地址 0x3CSA0 接地或 0x3DSA0 接 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; // Co0, D/C#0后续字节为命令 uint8_t control_data 0x40; // Co0, 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, // 复用率64128x64 屏 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 字节再处理下半部分页 n1写入后 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下半部分写入 page1 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 对初始化序列中某些命令的处理略有差异尤其是 0xDACOM 引脚硬件配置命令后的参数值。实际项目中同一款 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里是有的但如果你自己精简过初始化命令很容易丢。本文还有配套的精品资源点击获取