OLED屏驱动开发,从原理到代码,零基础实现显示控制
OLED(有机发光二极管)屏幕凭借自发光、高对比度、低功耗、宽视角等优势,成为嵌入式开发中最常用的显示外设之一。本文以最经典的0.96英寸I2C/SPI接口OLED屏(SSD1306驱动芯片)为例,从硬件原理、电路接线、代码实现到功能扩展,完整讲解OLED屏的驱动开发流程,适配STM32单片机,代码可直接移植到Keil MDK环境中使用。
一、OLED屏基础认知
1.1 核心参数(0.96英寸OLED屏)
- 驱动芯片:SSD1306(行业主流,本文核心讲解);
- 分辨率:128×64(128列,64行);
- 接口类型:I2C(2线)/SPI(4线/6线),I2C接口因接线简单更适合新手;
- 供电电压:3.3V(部分兼容5V,需注意电平匹配);
- 显示颜色:单色(蓝/白)、双色(蓝黄/白蓝),本文以单色蓝底白字为例。
1.2 SSD1306驱动原理
SSD1306是专门为OLED屏设计的驱动芯片,核心功能是接收单片机发送的指令和数据,控制OLED像素点的亮灭:
- 指令模式:接收单片机的控制指令(如屏幕初始化、光标定位、显示开关);
- 数据模式:接收像素显示数据,128×64分辨率对应1024字节显存(每字节控制8个像素点);
- 显存映射:屏幕列对应显存的X轴(0127),行按8行分组对应显存的Y轴(07,每组8行)。
二、硬件准备与电路接线
2.1 所需硬件
- 主控:STM32F103C8T6最小系统板(通用Cortex-M3内核,新手友好);
- OLED屏:0.96英寸I2C接口SSD1306 OLED屏;
- 辅助:杜邦线、3.3V电源(或单片机直接供电)、USB转串口模块(调试用)。
2.2 I2C接口接线(最简方案)
0.96英寸OLED屏I2C接口核心引脚仅4个,接线如下(STM32为例):
| OLED屏引脚 | 功能 | STM32引脚(示例) | 备注 |
|---|---|---|---|
| VCC | 供电 | 3.3V | 禁止接5V,避免烧屏 |
| GND | 接地 | GND | 共地保证电平稳定 |
| SCL | I2C时钟线 | PB6 | 可自定义其他GPIO口 |
| SDA | I2C数据线 | PB7 | 可自定义其他GPIO口 |
注:SPI接口OLED屏需额外接DC(数据/指令选择)、RES(复位)、CS(片选)引脚,接线稍复杂,新手优先选I2C版本。
三、软件开发环境搭建
3.1 基础环境
- 开发工具:Keil MDK-ARM V5.x;
- 固件库:STM32F10x标准固件库(或HAL库,本文用标准库);
- 核心:需实现I2C底层驱动(软件I2C,无需依赖硬件I2C,兼容性更强)。
3.2 软件I2C底层驱动实现
新手无需深入理解I2C协议细节,直接复用以下通用软件I2C代码,仅需修改引脚定义即可:
#include "stm32f10x.h"
#include "delay.h" // 需提前实现延时函数(us/ms级)
// ************************* 引脚定义 *************************
#define OLED_SCL_PIN GPIO_Pin_6
#define OLED_SCL_PORT GPIOB
#define OLED_SDA_PIN GPIO_Pin_7
#define OLED_SDA_PORT GPIOB
// 引脚电平控制宏
#define OLED_SCL_H() GPIO_SetBits(OLED_SCL_PORT, OLED_SCL_PIN)
#define OLED_SCL_L() GPIO_ResetBits(OLED_SCL_PORT, OLED_SCL_PIN)
#define OLED_SDA_H() GPIO_SetBits(OLED_SDA_PORT, OLED_SDA_PIN)
#define OLED_SDA_L() GPIO_ResetBits(OLED_SDA_PORT, OLED_SDA_PIN)
/**
* @brief 初始化OLED的I2C引脚
* @param 无
* @retval 无
*/
void OLED_I2C_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置SCL/SDA为推挽输出
GPIO_InitStruct.GPIO_Pin = OLED_SCL_PIN | OLED_SDA_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(OLED_SCL_PORT, &GPIO_InitStruct);
// 初始电平置高
OLED_SCL_H();
OLED_SDA_H();
}
/**
* @brief 软件I2C起始信号
* @param 无
* @retval 无
*/
void OLED_I2C_Start(void)
{
OLED_SDA_H();
OLED_SCL_H();
delay_us(2);
OLED_SDA_L(); // SDA在SCL高电平时拉低,产生起始信号
delay_us(2);
OLED_SCL_L();
}
/**
* @brief 软件I2C停止信号
* @param 无
* @retval 无
*/
void OLED_I2C_Stop(void)
{
OLED_SDA_L();
OLED_SCL_H();
delay_us(2);
OLED_SDA_H(); // SDA在SCL高电平时拉高,产生停止信号
delay_us(2);
}
/**
* @brief 软件I2C发送一个字节
* @param dat:待发送的字节
* @retval 无
*/
void OLED_I2C_SendByte(uint8_t dat)
{
uint8_t i;
for(i=0; i<8; i++)
{
OLED_SCL_L();
delay_us(2);
// 逐位发送(高位先行)
if(dat & 0x80) OLED_SDA_H();
else OLED_SDA_L();
dat <<= 1;
delay_us(2);
OLED_SCL_H();
delay_us(2);
}
// 等待从机应答(省略应答检测,新手简化版)
OLED_SCL_L();
delay_us(2);
OLED_SDA_H();
delay_us(2);
OLED_SCL_H();
delay_us(2);
OLED_SCL_L();
}
/**
* @brief 向OLED发送指令
* @param cmd:指令字节
* @retval 无
*/
void OLED_SendCmd(uint8_t cmd)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); // I2C设备地址(0x3C<<1,SSD1306默认地址)
OLED_I2C_SendByte(0x00); // 指令模式(0x00)
OLED_I2C_SendByte(cmd);
OLED_I2C_Stop();
}
/**
* @brief 向OLED发送数据
* @param dat:显示数据字节
* @retval 无
*/
void OLED_SendData(uint8_t dat)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); // I2C设备地址
OLED_I2C_SendByte(0x40); // 数据模式(0x40)
OLED_I2C_SendByte(dat);
OLED_I2C_Stop();
}
核心说明:
- 软件I2C无需配置单片机硬件I2C外设,仅通过GPIO模拟时序,新手易上手;
delay_us()函数需自行实现(基于SysTick或定时器),是I2C时序的关键;- SSD1306的I2C默认地址为0x3C(部分屏为0x78,需根据实际调整)。
四、OLED屏初始化与基础显示函数
4.1 OLED屏初始化
SSD1306需要发送一系列初始化指令配置显示参数,以下是通用初始化函数:
/**
* @brief OLED屏初始化
* @param 无
* @retval 无
*/
void OLED_Init(void)
{
delay_ms(100); // 上电延时,确保芯片稳定
OLED_I2C_Init(); // 初始化I2C引脚
// SSD1306初始化指令
OLED_SendCmd(0xAE); // 关闭显示
OLED_SendCmd(0x00); // 设置列起始地址低4位
OLED_SendCmd(0x10); // 设置列起始地址高4位
OLED_SendCmd(0x40); // 设置显示起始行
OLED_SendCmd(0xB0); // 设置页地址(Y轴)
OLED_SendCmd(0x81); // 设置对比度
OLED_SendCmd(0xFF); // 对比度值(0~255,越大越亮)
OLED_SendCmd(0xA1); // 段重映射(0xA0左右翻转,0xA1正常)
OLED_SendCmd(0xA6); // 正常显示(0xA6正常,0xA7反显)
OLED_SendCmd(0xA8); // 设置多路复用率
OLED_SendCmd(0x3F); // 64行显示
OLED_SendCmd(0xC8); // 扫描方向(0xC0上下翻转,0xC8正常)
OLED_SendCmd(0xD3); // 设置显示偏移
OLED_SendCmd(0x00); // 偏移量0
OLED_SendCmd(0xD5); // 设置时钟分频
OLED_SendCmd(0x80); // 分频因子
OLED_SendCmd(0xD9); // 设置预充电周期
OLED_SendCmd(0xF1);
OLED_SendCmd(0xDA); // 设置COM引脚配置
OLED_SendCmd(0x12);
OLED_SendCmd(0xDB); // 设置VCOMH电压
OLED_SendCmd(0x40);
OLED_SendCmd(0x8D); // 启用电荷泵
OLED_SendCmd(0x14);
OLED_SendCmd(0xAF); // 开启显示
}
/**
* @brief 设置OLED显示坐标(X:列0~127,Y:页0~7)
* @param x:列坐标
* @param y:页坐标(每页8行)
* @retval 无
*/
void OLED_SetPos(uint8_t x, uint8_t y)
{
OLED_SendCmd(0xB0 + y); // 设置页地址
OLED_SendCmd(((x & 0xF0) >> 4) | 0x10); // 设置列地址高4位
OLED_SendCmd(x & 0x0F); // 设置列地址低4位
}
/**
* @brief 清屏函数
* @param 无
* @retval 无
*/
void OLED_Clear(void)
{
uint8_t x, y;
for(y=0; y<8; y++)
{
OLED_SetPos(0, y);
for(x=0; x<128; x++)
{
OLED_SendData(0x00); // 写入0,关闭所有像素
}
}
}
4.2 字符/数字显示函数
要显示字符,需先准备字模数据(本文用8×16点阵ASCII字模),以下是核心显示函数:
// 8×16 ASCII字模表(截取部分,完整表可网上下载)
const unsigned char OLED_F8x16[] = {
0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00,0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00, // 0
0x00,0x00,0x00,0x70,0x08,0x08,0x08,0x70,0x00,0x00,0x00,0x70,0x08,0x08,0x08,0x70, // 1
// 更多字符字模省略,需补充完整ASCII表
};
/**
* @brief 显示单个字符(8×16点阵)
* @param x:起始列(0~127)
* @param y:起始页(0~7,每字符占2页)
* @param c:待显示字符(ASCII码)
* @retval 无
*/
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t c)
{
uint8_t i;
c -= '0'; // 以数字0为基准(显示字母需调整偏移)
OLED_SetPos(x, y);
// 显示上半部分(第1页)
for(i=0; i<8; i++)
{
OLED_SendData(OLED_F8x16[c*16 + i]);
}
OLED_SetPos(x, y+1);
// 显示下半部分(第2页)
for(i=8; i<16; i++)
{
OLED_SendData(OLED_F8x16[c*16 + i]);
}
}
/**
* @brief 显示数字字符串
* @param x:起始列
* @param y:起始页
* @param num:待显示数字(0~99999999)
* @param len:显示位数
* @retval 无
*/
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)
{
uint8_t i;
for(i=0; i<len; i++)
{
OLED_ShowChar(x + 8*i, y, (num / (uint32_t)pow(10, len-i-1)) % 10 + '0');
}
}
/**
* @brief 显示字符串
* @param x:起始列
* @param y:起始页
* @param str:字符串指针
* @retval 无
*/
void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str)
{
while(*str != '\0')
{
OLED_ShowChar(x, y, *str);
x += 8;
if(x > 120) // 超出屏幕宽度换行
{
x = 0;
y += 2;
}
str++;
}
}
五、实战:OLED屏显示示例
5.1 完整测试代码
将以上函数整合,实现基础显示功能:
#include "stm32f10x.h"
#include "oled.h" // 将上述OLED驱动函数封装到oled.h/oled.c中
#include "delay.h"
int main(void)
{
uint32_t cnt = 0;
// 系统初始化
SystemInit(); // 配置STM32主频(72MHz)
delay_init(); // 初始化延时函数
OLED_Init(); // 初始化OLED屏
OLED_Clear(); // 清屏
while(1)
{
// 显示固定字符串
OLED_ShowString(0, 0, "OLED Test Demo");
OLED_ShowString(0, 2, "Count:");
// 显示递增数字
OLED_ShowNum(40, 2, cnt, 5);
cnt++;
if(cnt > 99999) cnt = 0;
delay_ms(100); // 100ms刷新一次
}
}
5.2 效果验证
- 将代码编译后下载到STM32单片机;
- 上电后OLED屏首先清屏,随后显示:
- 第一行:OLED Test Demo;
- 第三行:Count: 00000(数字每秒递增1);
- 若屏幕无显示,检查接线(尤其是VCC必须为3.3V)、I2C地址、初始化指令是否正确。
六、常见问题与解决方案
6.1 屏幕无显示
- 原因1:供电电压错误(接5V烧屏或电压不足);
解决:严格接3.3V,测量OLED屏VCC引脚电压是否稳定; - 原因2:I2C地址错误;
解决:将OLED_I2C_SendByte(0x78)改为0x38(0x1C<<1)测试; - 原因3:延时函数精度不足;
解决:优化delay_us()函数,确保I2C时序符合SSD1306要求。
6.2 显示乱码/错位
- 原因1:字模点阵与显示函数不匹配;
解决:确保字模是8×16点阵,且高位先行; - 原因2:坐标设置错误;
解决:检查OLED_SetPos()函数,页地址范围07,列地址0127。
6.3 屏幕闪烁
- 原因:清屏/刷新频率过高;
解决:减少不必要的清屏操作,仅刷新变化的区域。
七、功能扩展建议
- 中文显示:添加16×16中文点阵字模,修改显示函数适配16列宽度;
- 图形显示:通过写入显存数据实现点、线、圆、位图显示;
- 滚动显示:利用SSD1306的滚动指令(0x26~0x29)实现文字滚动;
- 低功耗模式:通过
OLED_SendCmd(0xAE)关闭显示,降低功耗。
浙公网安备 33010602011771号