前言
在嵌入式开发中,OLED屏幕凭借其高对比度、低功耗和显示清晰等优点,成为了单片机项目中最常用的显示设备之一。而对于51单片机开发者来说,驱动一块0.96寸的IIC接口OLED屏幕,是掌握通信协议和显示控制的绝佳入门项目。
本文将基于中景园电子提供的OLED代码demo,从硬件连接到IIC协议原理,从驱动代码移植到显示原理,逐层剖析整个技术栈。无论你是刚开始学习51单片机的新手,还是想深入理解IIC协议和OLED驱动原理的开发者,这篇文章都会给你带来收获。
一、硬件篇:认识你的OLED屏幕
1.1 屏幕规格与引脚定义
市面上常见的0.96寸OLED屏幕,分辨率为128×64像素,驱动芯片为SSD1306。屏幕模组通常引出4个引脚:
| 引脚 | 名称 | 功能 | 连接 |
|---|---|---|---|
| 1 | GND | 电源地 | 接单片机GND |
| 2 | VCC | 电源正极 | 接单片机5V(兼容3.3V) |
| 3 | SCL | IIC时钟线 | 接单片机P1.5 |
| 4 | SDA | IIC数据线 | 接单片机P1.4 |



注意:部分屏幕模块的引脚顺序可能不同(如VCC和GND位置互换),接线前请务必确认模块上的丝印标记。
1.2 上拉电阻------一个容易被忽视的关键细节
IIC总线是开漏输出 架构,这意味着单片机引脚只能将总线拉低 (输出0),而不能主动拉高 (输出1)。总线的高电平状态完全依赖于连接在SDA和SCL线上的上拉电阻。
上拉电阻的阻值选择直接影响通信稳定性:
-
阻值过大(如10KΩ以上) :总线上升沿时间变长,在高速模式下可能导致建立时间不足
-
阻值过小(如1KΩ以下) :当总线被拉低时电流过大,增加功耗甚至可能损坏IO口
建议:使用4.7KΩ到10KΩ的上拉电阻。如果你的OLED模块已经板载了上拉电阻(大部分模块都有),则无需额外添加。

1.3 SSD1306驱动芯片简介
SSD1306是OLED屏幕的核心驱动芯片,负责:
-
通过IIC/SPI接口接收单片机发送的数据和命令
-
将数据存储到内部的GDDRAM(图形显示数据RAM)中
-
驱动OLED像素点阵,将显存内容显示在屏幕上
SSD1306的显存大小为128×64 = 8192 bit = 1024字节。这1024字节的显存被组织成8页(PAGE0~PAGE7) ,每页包含128列(SEG0~SEG127),每列对应1个字节(8个像素点)。
关键理解:向OLED发送1个字节的数据,实际上控制的是同一列上连续的8个像素点(同一页内的8行)。字节的bit0对应最上面的像素,bit7对应最下面的像素。
二、IIC协议详解------从时序到代码
2.1 什么是IIC?
IIC(Inter-Integrated Circuit)总线是飞利浦公司开发的一种简单、双向、二线制、同步串行总线。它只需要两根线:
-
SCL(Serial Clock Line) :串行时钟线,由主机(单片机)产生,用于同步数据传输
-
SDA(Serial Data Line) :串行数据线,用于双向传输数据
IIC是半双工 通信------同一时间只能单向传输数据。但它的优势在于:只需要两根线就能挂载多达127个设备,每个设备都有唯一的7位地址。

2.2 IIC的四种核心信号
(1)空闲状态
当总线空闲时,SDA和SCL都处于高电平。
(2)起始信号(START)
起始信号的定义:在SCL为高电平时,SDA从高电平跳变到低电平。

代码实现(来自iic.c):
cpp
void IIC_Start(void)
{
IIC_SCL = 1; // SCL拉高
IIC_SDA = 1; // SDA拉高
Delay5us(); // 短暂延时
IIC_SDA = 0; // SDA拉低------产生下降沿
Delay5us();
IIC_SCL = 0; // SCL拉低,准备传输数据
}
(3)停止信号(STOP)
停止信号的定义:在SCL为高电平时,SDA从低电平跳变到高电平。

代码实现:
cpp
void IIC_Stop(void)
{
IIC_SCL = 1; // SCL拉高
IIC_SDA = 0; // SDA拉低
Delay5us();
IIC_SDA = 1; // SDA拉高------产生上升沿
}
(4)数据传输与应答
数据传输的核心原则:
-
SCL低电平期间:允许SDA数据变化
-
SCL高电平期间:SDA数据必须保持稳定(此时数据被采样)
数据高位(MSB)在前 ,逐位传输。每传输完一个字节(8位),接收方需要在第9个时钟周期回复一个应答信号(ACK) :
-
ACK=0(低电平) :表示成功接收
-
ACK=1(高电平) :表示未成功接收(NACK)



代码实现(发送一个字节):
cpp
void IIC_Send_Byte(unsigned char IIC_Byte)
{
char i;
for(i = 0; i < 8; i++)
{
IIC_SDA = IIC_Byte & (0x80 >> i); // 从高位开始发送
IIC_SCL = 1; // SCL拉高,数据被采样
Delay5us();
IIC_SCL = 0; // SCL拉低,准备下一位
Delay5us();
}
}
2.3 SSD1306的IIC从机地址
根据SSD1306数据手册,IIC通信的第一个字节必须是从机地址+读写位:
| b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 1 | 1 | 1 | 0 | SA0 | R/W# |
-
高7位(0b011110 + SA0)是从机地址
-
SA0由D/C#引脚决定:SA0=0时地址为0x3C(写)/0x3D(读);SA0=1时地址为0x3D(写)/0x3E(读)
-
R/W#=0表示写,R/W#=1表示读
大多数OLED模块将SA0固定为0,因此写地址为0x78(0x3C << 1) 。代码中使用的正是这个地址:
cpp
Write_IIC_Byte(0x78); // 从机地址 + 写位
2.4 命令与数据的区分
SSD1306通过IIC接收两种类型的数据:
-
命令(Command) :控制屏幕的工作模式、亮度、寻址方式等
-
数据(Data) :要显示在屏幕上的像素内容
在IIC通信中,发送完从机地址后,需要发送一个控制字节来区分后续是命令还是数据:
cpp
Write_IIC_Byte(0x00); // 后续是命令(Co=0, D/C#=0)
Write_IIC_Byte(0x40); // 后续是数据(Co=0, D/C#=1)
代码中通过OLED_CMD(0)和OLED_DATA(1)来区分:
cpp
#define OLED_CMD 0 // 写命令
#define OLED_DATA 1 // 写数据
三、OLED驱动移植------代码分层解析
3.1 代码的整体架构
整个项目采用了清晰的分层设计:
cpp
┌─────────────────────────────────────────────┐
│ 应用层 (main.c) │
│ 显示汉字、字符串、数字、图片 │
├─────────────────────────────────────────────┤
│ 驱动层 (oled.c) │
│ 初始化、清屏、画点、显示字符/图片 │
├─────────────────────────────────────────────┤
│ 协议层 (iic.c) │
│ IIC起始、停止、发送字节、接收应答 │
├─────────────────────────────────────────────┤
│ 硬件层 (main.h) │
│ SCL=P1.5, SDA=P1.4 引脚定义 │
└─────────────────────────────────────────────┘
3.2 硬件层------引脚定义
在main.h中定义了IIC引脚:
cpp
sbit IIC_SCL = P1^5;
sbit IIC_SDA = P1^4;
在oled.h中进一步封装为宏函数:
cpp
#define OLED_SCLK_Clr() OLED_SCL=0
#define OLED_SCLK_Set() OLED_SCL=1
#define OLED_SDIN_Clr() OLED_SDIN=0
#define OLED_SDIN_Set() OLED_SDIN=1
这种宏定义的方式提高了代码的可读性和可移植性------如果要换引脚,只需修改一处定义。
3.3 协议层------IIC通信函数
iic.c实现了IIC协议的核心函数:
| 函数 | 功能 |
|---|---|
IIC_Start() |
产生起始信号 |
IIC_Stop() |
产生停止信号 |
IIC_Send_Byte() |
发送1个字节 |
IIC_Recv_Ack() |
接收应答信号 |
oled.c在此基础上封装了面向OLED的函数:
cpp
void Write_IIC_Command(unsigned char IIC_Command)
{
IIC_Start();
Write_IIC_Byte(0x78); // 从机地址
IIC_Wait_Ack();
Write_IIC_Byte(0x00); // 控制字节:命令
IIC_Wait_Ack();
Write_IIC_Byte(IIC_Command);
IIC_Wait_Ack();
IIC_Stop();
}
void Write_IIC_Data(unsigned char IIC_Data)
{
IIC_Start();
Write_IIC_Byte(0x78); // 从机地址
IIC_Wait_Ack();
Write_IIC_Byte(0x40); // 控制字节:数据
IIC_Wait_Ack();
Write_IIC_Byte(IIC_Data);
IIC_Wait_Ack();
IIC_Stop();
}
最终统一为OLED_WR_Byte(dat, cmd)函数:
cpp
void OLED_WR_Byte(unsigned dat, unsigned cmd)
{
if(cmd)
Write_IIC_Data(dat);
else
Write_IIC_Command(dat);
}
3.4 驱动层------OLED初始化
OLED初始化是屏幕正常工作的前提。OLED_Init()函数发送了一串初始化命令序列:
cpp
void OLED_Init(void)
{
OLED_WR_Byte(0xAE, OLED_CMD); // 关闭显示
OLED_WR_Byte(0x00, OLED_CMD); // 设置低列地址
OLED_WR_Byte(0x10, OLED_CMD); // 设置高列地址
OLED_WR_Byte(0x40, OLED_CMD); // 设置起始行地址
OLED_WR_Byte(0xB0, OLED_CMD); // 设置页地址
OLED_WR_Byte(0x81, OLED_CMD); // 对比度控制
OLED_WR_Byte(0xFF, OLED_CMD); // 对比度值
OLED_WR_Byte(0xA1, OLED_CMD); // 段重映射
OLED_WR_Byte(0xA6, OLED_CMD); // 正常显示(非反显)
OLED_WR_Byte(0xA8, OLED_CMD); // 设置多路复用率
OLED_WR_Byte(0x3F, OLED_CMD); // 1/64 duty
OLED_WR_Byte(0xC8, OLED_CMD); // COM扫描方向
OLED_WR_Byte(0xD3, OLED_CMD); // 设置显示偏移
OLED_WR_Byte(0x00, OLED_CMD);
OLED_WR_Byte(0xD5, OLED_CMD); // 设置时钟分频
OLED_WR_Byte(0x80, OLED_CMD);
OLED_WR_Byte(0xD8, OLED_CMD); // 设置区域颜色模式
OLED_WR_Byte(0x05, OLED_CMD);
OLED_WR_Byte(0xD9, OLED_CMD); // 设置预充电周期
OLED_WR_Byte(0xF1, OLED_CMD);
OLED_WR_Byte(0xDA, OLED_CMD); // 设置COM引脚配置
OLED_WR_Byte(0x12, OLED_CMD);
OLED_WR_Byte(0xDB, OLED_CMD); // 设置Vcomh
OLED_WR_Byte(0x30, OLED_CMD);
OLED_WR_Byte(0x8D, OLED_CMD); // 设置电荷泵
OLED_WR_Byte(0x14, OLED_CMD); // 使能电荷泵
OLED_WR_Byte(0xAF, OLED_CMD); // 开启显示
}
关键命令解析:
| 命令 | 功能 | 说明 |
|---|---|---|
| 0xAE | 关闭显示 | 初始化期间关闭显示,避免异常闪烁 |
| 0xAF | 开启显示 | 初始化完成后开启显示 |
| 0x81 + 0xFF | 设置对比度 | 值越大亮度越高 |
| 0xA1 | 段重映射 | 列地址与SEG的映射关系 |
| 0xC8 | COM扫描方向 | 行地址与COM的映射关系 |
| 0xA8 + 0x3F | 多路复用率 | 64行对应1/64 duty |
| 0xD5 + 0x80 | 时钟分频 | 设置显示时钟频率 |
| 0x8D + 0x14 | 电荷泵使能 | 内部升压,让3.3V能驱动OLED |
注意:初始化命令的顺序很重要,顺序错了屏幕可能无响应。
3.5 驱动层------显存与显示函数
坐标设置
cpp
void OLED_Set_Pos(unsigned char x, unsigned char y)
{
OLED_WR_Byte(0xb0 + y, OLED_CMD); // 设置页地址(0~7)
OLED_WR_Byte(((x & 0xf0) >> 4) | 0x10, OLED_CMD); // 设置列高地址
OLED_WR_Byte((x & 0x0f), OLED_CMD); // 设置列低地址
}
-
y的范围是0~7,对应8页(每页8行) -
x的范围是0~127,对应128列
显示字符
字符显示依赖于字模数据。oledfont.h中包含了两种字体的字模:
-
6×8字体 :用于显示ASCII字符(
F6x8数组) -
8×16字体 :用于显示ASCII字符(
F8X16数组) -
16×16汉字 :用于显示汉字(
Hzk数组)
以16×16字体显示字符为例:
cpp
void OLED_ShowChar(u8 x, u8 y, u8 chr, u8 Char_Size)
{
unsigned char c = chr - ' '; // 获取偏移值
if(Char_Size == 16)
{
OLED_Set_Pos(x, y);
for(i = 0; i < 8; i++)
OLED_WR_Byte(F8X16[c*16 + i], OLED_DATA); // 上半部分
OLED_Set_Pos(x, y + 1);
for(i = 0; i < 8; i++)
OLED_WR_Byte(F8X16[c*16 + i + 8], OLED_DATA); // 下半部分
}
}
显示汉字
16×16的汉字需要32个字节的数据(16列×2页):
cpp
void OLED_ShowCHinese(u8 x, u8 y, u8 no)
{
OLED_Set_Pos(x, y);
for(t = 0; t < 16; t++)
OLED_WR_Byte(Hzk[2*no][t], OLED_DATA); // 上半部分
OLED_Set_Pos(x, y + 1);
for(t = 0; t < 16; t++)
OLED_WR_Byte(Hzk[2*no+1][t], OLED_DATA); // 下半部分
}
显示BMP图片
OLED_DrawBMP()函数用于显示128×64的整屏图片:
cpp
void OLED_DrawBMP(unsigned char x0, unsigned char y0,
unsigned char x1, unsigned char y1,
unsigned char BMP[])
{
unsigned int j = 0;
unsigned char x, y;
for(y = y0; y < y1; y++)
{
OLED_Set_Pos(x0, y);
for(x = x0; x < x1; x++)
{
OLED_WR_Byte(BMP[j++], OLED_DATA);
}
}
}
BMP1[]和BMP2[]数组就是预先取模好的图片数据(存放在bmp.h中)。
3.6 应用层------main函数
main.c展示了OLED的典型使用流程:
cpp
int main(void)
{
OLED_Init(); // 1. 初始化OLED
OLED_Clear(); // 2. 清屏
// 3. 显示内容
OLED_ShowCHinese(0, 0, 0); // 中
OLED_ShowCHinese(18, 0, 1); // 景
// ... 更多显示内容
while(1)
{
// 4. 循环显示动态内容
OLED_ShowChar(48, 6, t, 16);
t++;
delay_ms(80);
OLED_DrawBMP(0, 0, 128, 8, BMP1); // 显示图片
delay_ms(80);
}
}
四、取模软件的使用
4.1 什么是取模?
取模是将字符或图形转换为二进制点阵数据的过程。每个字节的8个位(bit)对应8个像素点------1表示点亮,0表示熄灭。
4.2 常用取模软件
-
PCtoLCD2002:最常用的取模软件
-
字模提取 V2.1:另一款常用工具
4.3 取模设置要点
使用取模软件时,需要注意以下设置:
-
像素大小:ASCII字符通常用8×16或6×8;汉字用16×16
-
取模方向:需要与驱动代码的显示方向匹配(一般选择"纵向取模、字节倒序"或"横向取模")
-
输出格式:选择C语言数组格式
4.4 图片取模
对于128×64的整屏图片:
-
准备一张128×64分辨率的单色BMP图片
-
使用取模软件打开图片
-
设置阈值为128(将灰度图转换为黑白二值图)
-
生成C语言数组------共1024个字节
bmp.h中的BMP1[]和BMP2[]就是通过这种方式生成的。
五、常见问题与解决方案
5.1 屏幕不亮
| 可能原因 | 解决方案 |
|---|---|
| 电源问题 | 检查VCC和GND是否接反,电压是否为3.3V~5V |
| 上拉电阻缺失 | 确认模块是否板载上拉电阻,没有则外接4.7KΩ |
| 引脚接错 | 确认SCL→P1.5,SDA→P1.4 |
| 初始化失败 | 确认初始化命令序列是否正确 |
5.2 显示乱码或位置不对
| 可能原因 | 解决方案 |
|---|---|
| 字模方向不匹配 | 检查取模软件的设置是否与驱动代码匹配 |
| 坐标计算错误 | 确认OLED_Set_Pos()的x(0~127)和y(0~7)范围 |
| 段/COM映射错误 | 检查初始化中的0xA1和0xC8命令 |
5.3 通信不稳定
| 可能原因 | 解决方案 |
|---|---|
| 延时不够 | 增加Delay5us()的延时时间 |
| 总线电容过大 | 减小上拉电阻值(如用3.3KΩ) |
| 电源纹波 | 在VCC和GND之间加一个10uF电容 |
六、总结
本文从硬件连接到IIC协议,从驱动代码到显示原理,系统地介绍了51单片机驱动0.96寸OLED屏幕的完整过程。核心要点可以概括为:
核心流程:51单片机通过软件模拟IIC协议,向SSD1306驱动芯片发送命令和数据。命令用于配置屏幕的工作模式,数据则是要显示的像素内容。SSD1306将接收到的数据存入GDDRAM显存,并自动刷新到OLED屏幕上。
代码分层:硬件层(引脚定义)→ 协议层(IIC时序)→ 驱动层(OLED命令/数据)→ 应用层(显示内容)。

关键理解:
-
IIC是半双工同步串行总线,需要上拉电阻
-
SSD1306的显存是1024字节,组织为8页×128列
-
1个字节控制8个像素点(同一列上的8行)
-
命令和数据通过控制字节(0x00/0x40)区分
掌握了这些知识,你不仅能用好这块OLED屏幕,更能举一反三,理解其他IIC设备(如传感器、EEPROM等)的驱动原理。希望这篇教程能帮助你在嵌入式开发的道路上更进一步!