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像素点的亮灭:

  1. 指令模式:接收单片机的控制指令(如屏幕初始化、光标定位、显示开关);
  2. 数据模式:接收像素显示数据,128×64分辨率对应1024字节显存(每字节控制8个像素点);
  3. 显存映射:屏幕列对应显存的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 效果验证

  1. 将代码编译后下载到STM32单片机;
  2. 上电后OLED屏首先清屏,随后显示:
    • 第一行:OLED Test Demo;
    • 第三行:Count: 00000(数字每秒递增1);
  3. 若屏幕无显示,检查接线(尤其是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 屏幕闪烁

  • 原因:清屏/刷新频率过高;
    解决:减少不必要的清屏操作,仅刷新变化的区域。

七、功能扩展建议

  1. 中文显示:添加16×16中文点阵字模,修改显示函数适配16列宽度;
  2. 图形显示:通过写入显存数据实现点、线、圆、位图显示;
  3. 滚动显示:利用SSD1306的滚动指令(0x26~0x29)实现文字滚动;
  4. 低功耗模式:通过OLED_SendCmd(0xAE)关闭显示,降低功耗。
posted @ 2026-01-14 22:34  wo是个狠人  阅读(0)  评论(0)    收藏  举报