SSD1306 OLED 驱动学习笔记(一):概述、原理与初始化
这篇笔记是我跟完江协科技 OLED 教程之后整理的学习记录,同时对照了 SSD1306 的官方数据手册。
视频地址:https://www.bilibili.com/video/BV1EN41177Pc/
我当时调OLED好久没亮,芯片也没有任何报错。后来把自己的初始化序列和手册逐条对比,才发现电荷泵那条命令我写成了0xBD,正确的应该是0x8D。这篇笔记就是那次之后,把 SSD1306 从硬件到初始化重新理了一遍的结果。
一、概述(General Description)
1.1 芯片基本信息
0.96 英寸 OLED 显示屏,驱动芯片是 SSD1306。
- 128 个段(SEG)输出和 64 个公共端(COM)输出,对应 128×64 像素
- 供电要求:
- VDD = 1.65~3.3V,IC 逻辑供电
- VCC = 7~15V,面板驱动供电
不过芯片内部有升压电路,所以实际只需要接一个 3.3V 电源。
这里要注意的是,面板本身需要 7~15V 才能点亮,我们只给了 3.3V,中间靠的就是内部电荷泵升压。如果电荷泵没使能,芯片逻辑是正常的,命令也照收,但面板没电,屏幕就是全黑的。我那次黑屏就是这个原因。
1.2 通信接口
| 接口类型 | 说明 |
|---|---|
| 8 位并行接口 | 6800 / 8080 系列 |
| SPI 接口 | 3 线 / 4 线 |
| I2C 接口 | 我使用的方案,用软件 I2C 模拟 |
1.3 芯片的主要任务
- 接收数据(来自 MCU)
- 显示存储(存入 GDDRAM)
- 扫描刷新(把 GDDRAM 的内容输出到屏幕)
二、原理讲解
2.1 数据流的五个环节
接收到一个字节后,芯片会先做分流,我们可以指定这个字节是命令还是数据:
| 环节 | 作用 |
|---|---|
| 1 | 分流:判断收到的是命令还是数据 |
| 2 | 命令走这里,控制内部电路执行,比如开启显示 |
| 3 | 数据走这里,进入图形显示存储器 GDDRAM |
| 4 | 扫描 GDDRAM,把数据刷新到屏幕上 |
| 5 | 控制器输出,SEG 的 0~127 对应 128 列,COM 的 0~63 对应 64 行 |
2.2 引脚说明
类型图例:I = 输入,O = 输出,I/O = 双向,P = 电源引脚,NC = 不连接。
| 引脚名 | 类型 | 说明 |
|---|---|---|
| VDD | P | 核心逻辑电源引脚 |
| VCC | P | 面板驱动电源,也是最高正电压电源引脚 |
| VSS | P | 地引脚 |
| VLSS | P | 模拟地引脚,外部应与 VSS 相连 |
| VCOMH | O | COM 信号非选中电平引脚。需要在此脚与 VSS 之间接一个电容 |
| VBAT | P | 保留引脚,应连接至 VDD |
| BGGND | P | 保留引脚,应接地 |
| C1P/C1N、C2P/C2N | I | 保留引脚,保持 NC |
| VBREF | P | 保留引脚,保持 NC |
| BS0/BS1/BS2 | I | MCU 总线接口选择引脚,详见下方接口选择 |
| IREF | I | 段输出电流基准引脚。需要在此脚与 VSS 间接一个电阻,使 IREF 电流维持在 12.5 µA |
| FR | O | 输出 RAM 写同步信号,用于协调 MCU 写数据与帧显示时序,防止撕裂效应。不用时保持 NC |
| CL | I | 外部时钟输入。内部时钟使能时(CLS 为高)此脚不用,接 VSS;内部时钟禁用时(CLS 为低)此脚作为外部时钟源输入 |
| CLS | I | 内部时钟使能。拉高(接 VDD)启用内部时钟;拉低禁用内部时钟,此时必须由 CL 脚提供外部时钟 |
| RES# | I | 复位信号输入。拉低执行芯片初始化,正常工作时保持高电平(接 VDD) |
| CS# | I | 片选输入,低有效 |
| D/C# | I | 数据/命令控制引脚。拉高(接 VDD)写数据,拉低写命令。 I2C 模式下此脚作为 SA0,用于从机地址选择。 3 线 SPI 模式下此脚必须接 VSS |
| E (RD#) | I | 6800 系列:作为 Enable(E) 信号,拉高且片选时启动读/写。 8080 系列:接收 Read(RD#) 信号,拉低且片选时启动读操作。 串口模式下必须接 VSS |
| R/W# (WR#) | I | 读/写控制输入。 6800 系列:作为 R/W# 选择,高为读、低为写。 8080 模式:作为 WR#,拉低且片选时启动写操作。 串口模式下必须接 VSS |
| D0~D7 | I/O | 8 位双向数据总线,接微处理器数据总线。 串口模式下:D0 = SCLK(串行时钟),D1 = SDIN(串行数据),D2 保持 NC |
2.3 接口选择说明
不同接口模式下各引脚的接法(对应手册 Table 8-1):
| 总线接口 | 数据引脚接法 | 控制引脚接法 |
|---|---|---|
| 3 线 SPI | D7~D3 接低,D2 = NC,D1 = SDIN,D0 = SCLK | E 接低,R/W# 接低,CS# 接片选,D/C# 接低,RES# 接复位 |
| 4 线 SPI | 同上 | E 接低,R/W# 接低,CS# 接片选,D/C# 接数据/命令,RES# 接复位 |
| I2C | D7~D3 接低,D2 = SDA_OUT,D1 = SDA_IN,D0 = SCL | E 接低,R/W# 接低,CS# 接低,D/C# 接 SA0,RES# 接复位 |
三、I2C 通信协议说明
3.1 一次 I2C 传输的组成
S(起始条件)
起始条件之后的第一个字节,必须是 7 位从机地址加 1 位读写位的格式。
SSD1306 的从机地址格式是:
0 1 1 1 1 0 SA0 R/W# └──── 7 位从机地址 ────┘ └ 读写位 ┘SA0 由 D/C# 引脚电平配置,用来改变从机地址的最低位。R/W# 是读写选择,因为串行模式只能写入,所以我们给 0。
写模式下的完整字节是0111 1000,也就是 0x78。如果 SA0 接高电平,则变成0111 1010,也就是 0x7A。
ACK(应答)
从机收到一个字节后拉低 SDA 表示应答。
Co(连续模式)
- 置 1:每个 Data byte 前都要加 1 个 Control byte
- 置 0:1 个 Control byte 后可以跟多个 Data byte
Control byte 里的 D/C# 位指定后面是数据还是命令:
- 0x00:后面是命令
- 0x40:后面是数据
P(终止时序)
主机发送停止条件,结束本次传输。
四、显存结构(GDDRAM)
4.1 为什么要分页
- X 轴是 0~127,一共 128 列
- Y 轴是 Page0~Page7,一共 8 页,每页 8 行像素
之所以要分页,是因为 1 个字节占 8 位。如果 8 位只表示一个像素点,那就太浪费了。所以这里把 8 位竖着展开、从上到下排列,低位在上,高位在下。也就是说,每次写入 1 个字节,会控制纵向的 8 个像素点,因此以 8 位为一组进行分页。
写完一个字节会自动右移一列。
总显存 = 8 页 × 128 字节 = 1024 字节。
4.2 页地址和列地址怎么设置
可以通过写命令,根据相应的命令表来决定页地址和列地址。
列地址(X)需要拆成两条命令发送。D/C# 写 0 表示写命令:
| 命令 | 位模式 | 说明 |
|---|---|---|
| 第一行 | 0000 xxxx | D7~D4 为命令码 0000,D3~D0 是列地址的低 4 位 |
| 第二行 | 0001 xxxx | D7~D4 为命令码 0001,D3~D0 是列地址的高 4 位 |
| 第三行 | 1011 0xxx | D7~D3 为命令码 10110,D2~D0 指定页地址 Page0~Page7 |
三行组合起来,就能覆盖 128 列 × 8 页的完整显存。
对应到代码:
// 低 4 位:0000 xxxxOLED_WriteCommand(0x00|(x&0x0F));// 高 4 位:0001 xxxxOLED_WriteCommand(0x10|((x&0xF0)>>4));// 页地址:1011 0xxxOLED_WriteCommand(0xB0|(y&0x07));五、初始化过程(内部提供 VCC)
5.1 写命令代码
voidOLED_WriteCommand(uint8_tCommand){MyI2C_Start();MyI2C_SendByte(0x78);// 从机地址:0111100 + R/W#=0,也就是 0x78MyI2C_ReceiveAck();MyI2C_SendByte(0x00);// Control byte:0x00 表示后面是命令MyI2C_ReceiveAck();MyI2C_SendByte(Command);// 命令本身MyI2C_ReceiveAck();MyI2C_Stop();}5.2 写数据代码
voidOLED_WriteData(uint8_tData){MyI2C_Start();MyI2C_SendByte(0x78);// 从机地址MyI2C_ReceiveAck();MyI2C_SendByte(0x40);// Control byte:0x40 表示后面是数据MyI2C_ReceiveAck();MyI2C_SendByte(Data);// 数据本身MyI2C_ReceiveAck();MyI2C_Stop();}0x78 的来历:7 位从机地址0111100(SA0 默认接低电平,为 0),加上 1 位读写位 0(写入),得到0111 1000,也就是 0x78。
5.3 完整初始化代码
/* ============================================================================ * OLED_Init() —— SSD1306 初始化(软件 I2C 版) * 每条命令的作用都对照 SSD1306 数据手册(Solomon Systech Rev 1.0)标注 * 供电结构: * VDD = 1.65~3.3V(逻辑) * VCC = 7~15V(面板驱动,靠内部电荷泵升压) * ============================================================================ */voidOLED_Init(void){MyI2C_Init();// 初始化软件 I2C 的 GPIO(SCL/SDA 引脚)Delay_ms(100);// 上电延时,等电源稳定(手册要求 VDD 稳定后再发命令)/* 1. 先关显示,避免初始化过程中出现花屏 */OLED_WriteCommand(0xAE);// AEh = Set Display OFF,关闭显示(RESET 默认值)/* 2. 设置显示时钟分频比 / 振荡器频率 */OLED_WriteCommand(0xD5);// D5h = Set Display Clock Divide Ratio / Oscillator FrequencyOLED_WriteCommand(0x80);// 高4位是振荡频率(8,默认),低4位是分频比(0,分频=1)/* 3. 设置复用比(MUX Ratio) */OLED_WriteCommand(0xA8);// A8h = Set Multiplex Ratio,MUX 比率 = N+1OLED_WriteCommand(0x3F);// 0x3F = 63,即 64 MUX,对应 128×64 屏幕的 64 行/* 4. 设置显示偏移(垂直平移) */OLED_WriteCommand(0xD3);// D3h = Set Display Offset,COM 垂直平移 0~63OLED_WriteCommand(0x00);// 偏移 0,128×64 模块一般填 0/* 5. 设置显示起始行 */OLED_WriteCommand(0x40);// 40h~7Fh = Set Display Start Line,0x40 表示从第 0 行开始/* 6. 段重映射(左右镜像) */OLED_WriteCommand(0xA1);// A1h = 列地址 127 映射到 SEG0,即左右镜像// 填 A0h 则不镜像。屏幕上图左右反了,就改这条/* 7. COM 输出扫描方向(上下翻转) */OLED_WriteCommand(0xC8);// C8h = COM63 到 COM0 反向扫描// 填 C0h 则正向扫描。屏幕上下颠倒时改这条/* 8. COM 引脚硬件配置 */OLED_WriteCommand(0xDA);// DAh = Set COM Pins Hardware ConfigurationOLED_WriteCommand(0x12);// 0x12 = 交替 COM 配置 + 左右反置// 128×64 模块常用值,顺序配置是 0x02/* 9. 设置对比度(亮度) */OLED_WriteCommand(0x81);// 81h = Set Contrast Control,双字节命令OLED_WriteCommand(0xCF);// 0x00~0xFF,值越大越亮,0xCF 约 81%// 屏幕太暗或太亮,优先改这个值/* 10. 设置预充电周期 */OLED_WriteCommand(0xD9);// D9h = Set Pre-charge PeriodOLED_WriteCommand(0xF1);// 高4位是阶段1周期数(F=15),低4位是阶段2周期数(1)// 手册规定 0 是无效值/* 11. 设置 VCOMH 取消选择电平 */OLED_WriteCommand(0xDB);// DBh = Set VCOMH Deselect LevelOLED_WriteCommand(0x30);// 00h 是 0.65×VCC,20h 是 0.77×VCC(复位值),30h 是 0.83×VCC// 0x30 是最亮的一档/* 12. 整个显示 ON/OFF */OLED_WriteCommand(0xA4);// A4h = 输出跟随 GDDRAM 内容(正常显示)// A5h 则是强制全屏点亮、忽略 RAM 内容,一般只用于测试/* 13. 设置正常/反显 */OLED_WriteCommand(0xA6);// A6h = 正常显示,RAM 中 1 对应像素亮// A7h 则是反显,1 对应像素灭/* 14. 设置电荷泵,我黑屏就出在这条 */OLED_WriteCommand(0x8D);// 8Dh = Set Charge Pump,双字节命令// 作用是把 3.3V 升压到面板需要的 7~15V// 之前写成 0xBD,芯片当作非法命令丢弃了// 结果电荷泵从未开启,面板没电,屏幕全黑OLED_WriteCommand(0x14);// 0x14 = 使能内部电荷泵(0x10 = 关闭)// 用内部 DC/DC 供电的模块,这条是必需的/* 15. 开启显示 */OLED_WriteCommand(0xAF);// AFh = Set Display ON,真正点亮屏幕,放在最后OLED_Clear();// 清屏,防止上电随机噪点Delay_ms(100);// 等显示稳定}5.4 设置光标位置(列地址 + 页地址)
voidOLED_SetCursor(uint8_tx,uint8_ty){// 列地址低 4 位:命令 0000xxxxOLED_WriteCommand(0x00|(x&0x0F));// 列地址高 4 位:命令 0001xxxxOLED_WriteCommand(0x10|((x&0xF0)>>4));// 页地址:命令 10110xxxOLED_WriteCommand(0xB0|(y&0x07));}位运算拆解,以 x = 0x5A(0101 1010)为例:
| 步骤 | 运算 | 结果 | 作用 |
|---|---|---|---|
| 1 | x & 0x0F | 0000 1010= 0x0A | 用 0x0F(0000 1111)保留低 4 位,把高 4 位清零 |
| 2 | x & 0xF0 | 0101 0000= 0x50 | 用 0xF0(1111 0000)保留高 4 位,把低 4 位清零 |
| 3 | (x & 0xF0) >> 4 | 0000 0101= 0x05 | 把高 4 位移到低位,方便拼命令 |
| 4 | 0x10 | 0x05 | 0001 0101= 0x15 | 用或运算套上命令前缀 0001 |
关于这两个位运算,我记的口诀是:
- 与运算:和 1 运算的结果照抄原数,和 0 运算的结果永远是 0。也就是全 1 才得 1,有一个 0 结果就是 0。
- 或运算:和 1 运算的结果永远是 1,和 0 运算的结果照抄原数。也就是有一个 1 结果就是 1。
参考资料
- 江协科技 OLED 教程:https://www.bilibili.com/video/BV1EN41177Pc/
- SSD1306 数据手册,Solomon Systech,Rev 1.0,2007-09
- 命令表:Table 9-1,第 28~32 页
- 命令详细说明(含电荷泵):第 10 章,第 34 页起
- 引脚说明:Table 7-1,第 13~14 页
- 官方初始化示例:第 4.4 节
- 0.96 寸 OLED 模块规格书,Allvision QG-2864KLBEG01