OLED SSD1306驱动开发:I2C与SPI双协议实现

文章目录


每日一句正能量

但行好事,莫问前程,你只管播种善良,时间自会给出答案。

把注意力从"未来会怎样"收回到"此刻我该做什么"。善良不是交易,不等同于即时回报。但长期看,你散发的能量、结下的善缘、练就的品行,会以你意想不到的方式,在未来某个节点回到你身上。

引言:当128×64像素遇上嵌入式开发

SSD1306是嵌入式领域最经典的OLED控制器之一,驱动着从智能手表到工业仪表的无数显示屏。它支持128×64或128×32分辨率,通过I2C或SPI与主控通信,内置电荷泵将3.3V升压至驱动OLED所需的7.5V。

然而,SSD1306的驱动开发远不止"发送几个命令点亮屏幕"那么简单。从显存的页寻址架构、到字库的点阵设计、再到Bresenham图形算法,一个完整的驱动涉及硬件抽象内存管理计算机图形学三个层面的知识。本文将带你从零构建一个支持I2C/SPI双协议、具备完整图形API的SSD1306驱动。


一、双协议硬件架构:I2C vs SPI

1.1 硬件连接对比

特性 I2C接口 SPI接口
引脚数 4-pin (VCC, GND, SCL, SDA) 7-pin (VCC, GND, SCK, MOSI, CS, DC, RES)
上拉电阻 需要4.7kΩ上拉 不需要
额外GPIO CS, DC, RES各需1个GPIO
时钟速度 400kHz (Fast Mode) 10MHz (SSD1306上限)
全屏刷新时间 ~3.5ms ~0.8ms
代码复杂度 中等(需处理ACK) 简单
适用场景 引脚受限、低速UI 动画、视频、高帧率

1.2 协议选择决策

c 复制代码
// ssd1306_config.h - 编译时选择协议
#ifndef SSD1306_CONFIG_H
#define SSD1306_CONFIG_H

// 选择通信协议(二选一)
#define SSD1306_USE_I2C
// #define SSD1306_USE_SPI

#ifdef SSD1306_USE_I2C
    #define SSD1306_I2C_ADDR    0x3C  // SA0=GND时的地址
    #define SSD1306_I2C_HANDLE  hi2c1  // STM32 HAL I2C句柄
#endif

#ifdef SSD1306_USE_SPI
    #define SSD1306_SPI_HANDLE  hspi1  // STM32 HAL SPI句柄
    #define SSD1306_CS_PORT     GPIOA
    #define SSD1306_CS_PIN      GPIO_PIN_4
    #define SSD1306_DC_PORT     GPIOA
    #define SSD1306_DC_PIN      GPIO_PIN_3
    #define SSD1306_RES_PORT    GPIOA
    #define SSD1306_RES_PIN     GPIO_PIN_2
#endif

// 显示配置
#define SSD1306_WIDTH       128
#define SSD1306_HEIGHT      64
#define SSD1306_PAGES       (SSD1306_HEIGHT / 8)  // 8 pages

// 缓冲策略
#define SSD1306_USE_BUFFER  1  // 1=Full buffer, 0=Direct write

#endif

二、显存架构:页寻址模式的奥秘

2.1 GDDRAM组织结构

SSD1306的显存(GDDRAM)为128列×64行 ,但物理组织为8个Page,每个Page包含128字节。每个字节控制8个垂直像素(bit0=顶部,bit7=底部)。

复制代码
GDDRAM物理布局:
Page 0: [Byte0][Byte1]...[Byte127]  → 控制 Row 0~7
Page 1: [Byte0][Byte1]...[Byte127]  → 控制 Row 8~15
...
Page 7: [Byte0][Byte1]...[Byte127]  → 控制 Row 56~63

每个Byte的位映射:
Bit 7 → Row (page*8 + 7)
Bit 6 → Row (page*8 + 6)
...
Bit 0 → Row (page*8 + 0)

2.2 三种寻址模式

模式 命令 特点 适用场景
页寻址 0x20, 0x02 写完一列后列地址自动递增,页不变 文本显示、局部更新
水平寻址 0x20, 0x00 列和页都自动递增 全屏刷新、图像显示
垂直寻址 0x20, 0x01 列递增,页自动递增 特殊应用
c 复制代码
// 设置寻址模式
void SSD1306_SetAddressingMode(uint8_t mode)
{
    SSD1306_WriteCommand(0x20);      // 设置寻址模式命令
    SSD1306_WriteCommand(mode & 0x03); // 0x00=水平, 0x01=垂直, 0x02=页
}

// 设置页寻址模式的页和列起始地址
void SSD1306_SetPageAddress(uint8_t page, uint8_t col)
{
    SSD1306_WriteCommand(0xB0 + page);        // 设置页起始地址 (B0~B7)
    SSD1306_WriteCommand(0x00 + (col & 0x0F)); // 设置列低4位
    SSD1306_WriteCommand(0x10 + (col >> 4));  // 设置列高4位
}

2.3 帧缓冲设计

c 复制代码
// ssd1306_buf.c
#if SSD1306_USE_BUFFER
    // 完整帧缓冲: 128×64 / 8 = 1024 bytes
    static uint8_t s_frame_buffer[SSD1306_PAGES][SSD1306_WIDTH];
    
    // 脏页标记: 只更新修改过的页
    static uint8_t s_dirty_pages[SSD1306_PAGES];
#endif

void SSD1306_DrawPixel(uint8_t x, uint8_t y, SSD1306_Color_t color)
{
    if (x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT) return;
    
#if SSD1306_USE_BUFFER
    uint8_t page = y / 8;
    uint8_t bit = y % 8;
    
    if (color == SSD1306_COLOR_WHITE) {
        s_frame_buffer[page][x] |= (1 << bit);
    } else {
        s_frame_buffer[page][x] &= ~(1 << bit);
    }
    
    s_dirty_pages[page] = 1;  // 标记脏页
#else
    // 直接写入模式(无缓冲)
    uint8_t page = y / 8;
    uint8_t bit = y % 8;
    
    SSD1306_SetPageAddress(page, x);
    
    // 需要先读取当前值,修改对应位,再写回
    // 这是直接模式的缺点:需要读-修改-写操作
    uint8_t current = SSD1306_ReadData();  // 如果支持读取
    if (color == SSD1306_COLOR_WHITE) {
        current |= (1 << bit);
    } else {
        current &= ~(1 << bit);
    }
    SSD1306_WriteData(current);
#endif
}

// 刷新显示(仅缓冲模式)
void SSD1306_Display(void)
{
#if SSD1306_USE_BUFFER
    // 使用水平寻址模式一次性写入整个帧缓冲
    SSD1306_WriteCommand(0x20);  // 水平寻址
    SSD1306_WriteCommand(0x00);
    SSD1306_WriteCommand(0x21);  // 设置列地址
    SSD1306_WriteCommand(0x00);  // 起始列
    SSD1306_WriteCommand(0x7F);  // 结束列 (127)
    SSD1306_WriteCommand(0x22);  // 设置页地址
    SSD1306_WriteCommand(0x00);  // 起始页
    SSD1306_WriteCommand(0x07);  // 结束页 (7)
    
    // 突发写入1024字节
    SSD1306_WriteDataMulti(&s_frame_buffer[0][0], SSD1306_WIDTH * SSD1306_PAGES);
#endif
}

三、驱动初始化:16条命令的精密舞蹈

3.1 初始化命令序列

c 复制代码
// ssd1306.c - 初始化函数
bool SSD1306_Init(void)
{
    // 1. 硬件复位(SPI模式需要,I2C模块通常已内置复位电路)
    #ifdef SSD1306_USE_SPI
    HAL_GPIO_WritePin(SSD1306_RES_PORT, SSD1306_RES_PIN, GPIO_PIN_RESET);
    HAL_Delay(10);
    HAL_GPIO_WritePin(SSD1306_RES_PORT, SSD1306_RES_PIN, GPIO_PIN_SET);
    HAL_Delay(10);
    #endif
    
    // 2. 发送初始化命令序列
    static const uint8_t init_cmds[] = {
        0xAE,           // Display OFF (sleep mode)
        0xD5, 0x80,     // Set Display Clock Divide Ratio / Oscillator Frequency
                        // 0x80: divide ratio = 1, oscillator frequency = default
        0xA8, 0x3F,     // Set Multiplex Ratio (64 - 1 = 0x3F for 128×64)
        0xD3, 0x00,     // Set Display Offset (0)
        0x40,           // Set Display Start Line (0)
        0x8D, 0x14,     // Charge Pump Setting (0x14 = enable charge pump)
        0x20, 0x00,     // Set Memory Addressing Mode (0x00 = horizontal)
        0xA1,           // Set Segment Re-map (0xA1 = column 127 mapped to SEG0)
        0xC8,           // Set COM Output Scan Direction (0xC8 = remapped mode)
        0xDA, 0x12,     // Set COM Pins Hardware Configuration (0x12 = alternative, no remap)
        0x81, 0x7F,     // Set Contrast Control (0x7F = 127)
        0xD9, 0xF1,     // Set Pre-charge Period (0xF1 = phase 1: 15 DCLK, phase 2: 1 DCLK)
        0xDB, 0x40,     // Set VCOMH Deselect Level (0x40 = ~0.77×Vcc)
        0xA4,           // Entire Display ON (0xA4 = resume to RAM content display)
        0xA6,           // Set Normal/Inverse Display (0xA6 = normal)
        0xAF,           // Display ON (normal mode)
    };
    
    for (size_t i = 0; i < sizeof(init_cmds); i++) {
        if (!SSD1306_WriteCommand(init_cmds[i])) {
            return false;
        }
    }
    
    // 3. 清屏
    SSD1306_Clear();
    SSD1306_Display();
    
    return true;
}

3.2 命令发送抽象层

c 复制代码
// ssd1306_i2c.c
#ifdef SSD1306_USE_I2C

#define SSD1306_I2C_CMD_MODE  0x00  // Co=0, D/C#=0 (命令)
#define SSD1306_I2C_DATA_MODE 0x40  // Co=0, D/C#=1 (数据)

bool SSD1306_WriteCommand(uint8_t cmd)
{
    uint8_t buf[2] = {SSD1306_I2C_CMD_MODE, cmd};
    HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(
        &SSD1306_I2C_HANDLE,
        SSD1306_I2C_ADDR << 1,
        buf, 2,
        100
    );
    return (status == HAL_OK);
}

bool SSD1306_WriteData(uint8_t data)
{
    uint8_t buf[2] = {SSD1306_I2C_DATA_MODE, data};
    HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(
        &SSD1306_I2C_HANDLE,
        SSD1306_I2C_ADDR << 1,
        buf, 2,
        100
    );
    return (status == HAL_OK);
}

// 突发写入(关键优化:减少I2C事务开销)
bool SSD1306_WriteDataMulti(const uint8_t *data, uint16_t len)
{
    // 使用I2C突发模式:第一个字节是控制字节(0x40),后面全是数据
    uint8_t *buf = malloc(len + 1);
    if (!buf) return false;
    
    buf[0] = SSD1306_I2C_DATA_MODE;
    memcpy(buf + 1, data, len);
    
    HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(
        &SSD1306_I2C_HANDLE,
        SSD1306_I2C_ADDR << 1,
        buf, len + 1,
        1000
    );
    
    free(buf);
    return (status == HAL_OK);
}

#endif

// ssd1306_spi.c
#ifdef SSD1306_USE_SPI

bool SSD1306_WriteCommand(uint8_t cmd)
{
    HAL_GPIO_WritePin(SSD1306_DC_PORT, SSD1306_DC_PIN, GPIO_PIN_RESET); // DC=0 (command)
    HAL_GPIO_WritePin(SSD1306_CS_PORT, SSD1306_CS_PIN, GPIO_PIN_RESET); // CS=0
    
    HAL_StatusTypeDef status = HAL_SPI_Transmit(
        &SSD1306_SPI_HANDLE,
        &cmd, 1,
        100
    );
    
    HAL_GPIO_WritePin(SSD1306_CS_PORT, SSD1306_CS_PIN, GPIO_PIN_SET); // CS=1
    return (status == HAL_OK);
}

bool SSD1306_WriteData(uint8_t data)
{
    HAL_GPIO_WritePin(SSD1306_DC_PORT, SSD1306_DC_PIN, GPIO_PIN_SET);  // DC=1 (data)
    HAL_GPIO_WritePin(SSD1306_CS_PORT, SSD1306_CS_PIN, GPIO_PIN_RESET); // CS=0
    
    HAL_StatusTypeDef status = HAL_SPI_Transmit(
        &SSD1306_SPI_HANDLE,
        &data, 1,
        100
    );
    
    HAL_GPIO_WritePin(SSD1306_CS_PORT, SSD1306_CS_PIN, GPIO_PIN_SET); // CS=1
    return (status == HAL_OK);
}

// SPI突发写入(无需每字节切换DC,可优化为连续传输)
bool SSD1306_WriteDataMulti(const uint8_t *data, uint16_t len)
{
    HAL_GPIO_WritePin(SSD1306_DC_PORT, SSD1306_DC_PIN, GPIO_PIN_SET);  // DC=1
    HAL_GPIO_WritePin(SSD1306_CS_PORT, SSD1306_CS_PIN, GPIO_PIN_RESET); // CS=0
    
    HAL_StatusTypeDef status = HAL_SPI_Transmit(
        &SSD1306_SPI_HANDLE,
        (uint8_t *)data, len,
        1000
    );
    
    HAL_GPIO_WritePin(SSD1306_CS_PORT, SSD1306_CS_PIN, GPIO_PIN_SET); // CS=1
    return (status == HAL_OK);
}

#endif

四、字库设计:从点阵到字符

4.1 5×7 ASCII字库

c 复制代码
// ssd1306_font.c
// 5×7 ASCII字库 (96个字符, 32-127)
// 每个字符 = 5 bytes, 每byte = 1列 (MSB = top pixel)

const uint8_t font_5x7[][5] = {
    // Space (0x20)
    {0x00, 0x00, 0x00, 0x00, 0x00},
    // ! (0x21)
    {0x00, 0x00, 0x5F, 0x00, 0x00},
    // " (0x22)
    {0x00, 0x07, 0x00, 0x07, 0x00},
    // ... (省略中间字符)
    // A (0x41)
    {0x7E, 0x09, 0x09, 0x09, 0x7E},
    // B (0x42)
    {0x7F, 0x49, 0x49, 0x49, 0x36},
    // C (0x43)
    {0x3E, 0x41, 0x41, 0x41, 0x22},
    // ... (省略)
    // ~ (0x7E)
    {0x40, 0x20, 0x10, 0x08, 0x04},
};

// 字库元数据
const FontDef_t font_5x7_def = {
    .width = 5,
    .height = 7,
    .first_char = 32,
    .last_char = 127,
    .data = (const uint8_t *)font_5x7,
    .bytes_per_char = 5
};

// 字符绘制函数
void SSD1306_DrawChar(uint8_t x, uint8_t y, char c, const FontDef_t *font, SSD1306_Color_t color)
{
    if (c < font->first_char || c > font->last_char) return;
    
    uint16_t offset = (c - font->first_char) * font->bytes_per_char;
    const uint8_t *char_data = font->data + offset;
    
    for (uint8_t col = 0; col < font->width; col++) {
        uint8_t column_byte = char_data[col];
        
        for (uint8_t row = 0; row < font->height; row++) {
            if (column_byte & (1 << row)) {
                SSD1306_DrawPixel(x + col, y + row, color);
            }
        }
    }
}

void SSD1306_DrawString(uint8_t x, uint8_t y, const char *str, const FontDef_t *font, SSD1306_Color_t color)
{
    while (*str) {
        SSD1306_DrawChar(x, y, *str, font, color);
        x += font->width + 1;  // 字符间距1像素
        str++;
        
        if (x + font->width > SSD1306_WIDTH) break;  // 换行或截断
    }
}

4.2 中文点阵字库(GB2312)

c 复制代码
// 中文16×16字库(仅常用汉字,使用索引查找)
// 每个汉字 = 32 bytes (16列 × 16行 / 8)

// 区位码查找表
typedef struct {
    uint16_t unicode;   // Unicode编码
    const uint8_t *bitmap;  // 32字节点阵数据
} ChineseChar_t;

// 示例:"温"字 16×16点阵
const uint8_t bitmap_wen[] = {
    0x00, 0x40, 0x20, 0x40, 0x10, 0x40, 0x0C, 0x40,
    0x07, 0x40, 0x00, 0x40, 0x00, 0x40, 0xF0, 0x7F,
    0x10, 0x40, 0x10, 0x40, 0x10, 0x40, 0x10, 0x40,
    0x10, 0x40, 0x10, 0x40, 0x10, 0x40, 0x00, 0x00
};

// 使用哈希或二分查找加速
const ChineseChar_t chinese_font[] = {
    {0x6E29, bitmap_wen},  // 温
    // ... 更多汉字
};

void SSD1306_DrawChinese(uint8_t x, uint8_t y, uint16_t unicode, SSD1306_Color_t color)
{
    // 二分查找汉字点阵
    const uint8_t *bitmap = FindChineseBitmap(unicode);
    if (!bitmap) return;
    
    for (uint8_t page = 0; page < 2; page++) {  // 16行 = 2页
        for (uint8_t col = 0; col < 16; col++) {
            uint8_t byte_idx = page * 16 + col;
            uint8_t byte_data = bitmap[byte_idx];
            
            for (uint8_t bit = 0; bit < 8; bit++) {
                if (byte_data & (1 << bit)) {
                    SSD1306_DrawPixel(x + col, y + page * 8 + bit, color);
                }
            }
        }
    }
}

五、图形API:Bresenham算法与图形基元

5.1 图形API架构

5.2 Bresenham画线算法

c 复制代码
// ssd1306_gfx.c

// Bresenham画线算法(仅整数运算,适合无FPU的MCU)
void SSD1306_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, SSD1306_Color_t color)
{
    int16_t dx = abs(x1 - x0);
    int16_t dy = abs(y1 - y0);
    int16_t sx = (x0 < x1) ? 1 : -1;
    int16_t sy = (y0 < y1) ? 1 : -1;
    int16_t err = dx - dy;
    
    while (1) {
        SSD1306_DrawPixel(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;
        }
    }
}

5.3 中点画圆算法

c 复制代码
// 中点画圆算法(利用8对称性,只计算1/8圆弧)
void SSD1306_DrawCircle(uint8_t cx, uint8_t cy, uint8_t r, SSD1306_Color_t color)
{
    int16_t x = 0;
    int16_t y = r;
    int16_t d = 1 - r;  // 决策参数
    
    while (x <= y) {
        // 利用8对称性,一次绘制8个点
        SSD1306_DrawPixel(cx + x, cy + y, color);
        SSD1306_DrawPixel(cx + y, cy + x, color);
        SSD1306_DrawPixel(cx + y, cy - x, color);
        SSD1306_DrawPixel(cx + x, cy - y, color);
        SSD1306_DrawPixel(cx - x, cy - y, color);
        SSD1306_DrawPixel(cx - y, cy - x, color);
        SSD1306_DrawPixel(cx - y, cy + x, color);
        SSD1306_DrawPixel(cx - x, cy + y, color);
        
        if (d < 0) {
            d += 2 * x + 3;
        } else {
            d += 2 * (x - y) + 5;
            y--;
        }
        x++;
    }
}

// 填充圆(使用水平线填充)
void SSD1306_FillCircle(uint8_t cx, uint8_t cy, uint8_t r, SSD1306_Color_t color)
{
    int16_t x = 0;
    int16_t y = r;
    int16_t d = 1 - r;
    
    while (x <= y) {
        // 绘制水平线填充
        SSD1306_DrawLine(cx - x, cy + y, cx + x, cy + y, color);
        SSD1306_DrawLine(cx - y, cy + x, cx + y, cy + x, color);
        SSD1306_DrawLine(cx - y, cy - x, cx + y, cy - x, color);
        SSD1306_DrawLine(cx - x, cy - y, cx + x, cy - y, color);
        
        if (d < 0) {
            d += 2 * x + 3;
        } else {
            d += 2 * (x - y) + 5;
            y--;
        }
        x++;
    }
}

5.4 矩形与位图

c 复制代码
// 绘制矩形
void SSD1306_DrawRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, SSD1306_Color_t color)
{
    SSD1306_DrawLine(x, y, x + w - 1, y, color);           // 上边
    SSD1306_DrawLine(x, y + h - 1, x + w - 1, y + h - 1, color); // 下边
    SSD1306_DrawLine(x, y, x, y + h - 1, color);           // 左边
    SSD1306_DrawLine(x + w - 1, y, x + w - 1, y + h - 1, color); // 右边
}

// 填充矩形(利用页对齐优化)
void SSD1306_FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, SSD1306_Color_t color)
{
    // 检查是否页对齐(y是8的倍数,h是8的倍数)
    if ((y % 8 == 0) && (h % 8 == 0)) {
        // 页对齐优化:直接操作字节,无需逐像素
        uint8_t start_page = y / 8;
        uint8_t pages = h / 8;
        uint8_t fill_byte = (color == SSD1306_COLOR_WHITE) ? 0xFF : 0x00;
        
        for (uint8_t p = 0; p < pages; p++) {
            for (uint8_t col = x; col < x + w; col++) {
                s_frame_buffer[start_page + p][col] = fill_byte;
            }
            s_dirty_pages[start_page + p] = 1;
        }
    } else {
        // 非对齐:逐像素填充
        for (uint8_t row = y; row < y + h; row++) {
            for (uint8_t col = x; col < x + w; col++) {
                SSD1306_DrawPixel(col, row, color);
            }
        }
    }
}

// 绘制位图(XBM格式)
void SSD1306_DrawBitmap(uint8_t x, uint8_t y, const uint8_t *bitmap, 
                        uint8_t w, uint8_t h, SSD1306_Color_t color)
{
    for (uint8_t page = 0; page < (h + 7) / 8; page++) {
        for (uint8_t col = 0; col < w; col++) {
            uint8_t byte_data = bitmap[page * w + col];
            
            for (uint8_t bit = 0; bit < 8; bit++) {
                if (byte_data & (1 << bit)) {
                    SSD1306_DrawPixel(x + col, y + page * 8 + bit, color);
                }
            }
        }
    }
}

六、完整驱动代码架构

6.1 头文件设计

c 复制代码
// ssd1306.h - 公共API
#ifndef SSD1306_H
#define SSD1306_H

#include <stdint.h>
#include <stdbool.h>

// 颜色定义
typedef enum {
    SSD1306_COLOR_BLACK = 0,
    SSD1306_COLOR_WHITE = 1
} SSD1306_Color_t;

// 字体定义
typedef struct {
    uint8_t width;
    uint8_t height;
    uint8_t first_char;
    uint8_t last_char;
    const uint8_t *data;
    uint8_t bytes_per_char;
} FontDef_t;

// 核心API
bool SSD1306_Init(void);
void SSD1306_DeInit(void);
void SSD1306_Display(void);
void SSD1306_Clear(void);

// 像素操作
void SSD1306_DrawPixel(uint8_t x, uint8_t y, SSD1306_Color_t color);
SSD1306_Color_t SSD1306_GetPixel(uint8_t x, uint8_t y);

// 图形基元
void SSD1306_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, SSD1306_Color_t color);
void SSD1306_DrawRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, SSD1306_Color_t color);
void SSD1306_FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, SSD1306_Color_t color);
void SSD1306_DrawCircle(uint8_t cx, uint8_t cy, uint8_t r, SSD1306_Color_t color);
void SSD1306_FillCircle(uint8_t cx, uint8_t cy, uint8_t r, SSD1306_Color_t color);
void SSD1306_DrawBitmap(uint8_t x, uint8_t y, const uint8_t *bitmap, uint8_t w, uint8_t h, SSD1306_Color_t color);

// 文本
void SSD1306_DrawChar(uint8_t x, uint8_t y, char c, const FontDef_t *font, SSD1306_Color_t color);
void SSD1306_DrawString(uint8_t x, uint8_t y, const char *str, const FontDef_t *font, SSD1306_Color_t color);
void SSD1306_Printf(uint8_t x, uint8_t y, const FontDef_t *font, SSD1306_Color_t color, const char *fmt, ...);

// 高级功能
void SSD1306_SetContrast(uint8_t contrast);
void SSD1306_SetDisplayOn(bool on);
void SSD1306_InvertDisplay(bool invert);
void SSD1306_ScrollRight(uint8_t start_page, uint8_t end_page, uint8_t speed);
void SSD1306_ScrollLeft(uint8_t start_page, uint8_t end_page, uint8_t speed);
void SSD1306_StopScroll(void);

#endif

6.2 使用示例

c 复制代码
// main.c
#include "ssd1306.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    // 初始化I2C/SPI
    MX_I2C1_Init();  // 或 MX_SPI1_Init();
    
    // 初始化OLED
    if (!SSD1306_Init()) {
        printf("OLED init failed!\n");
        while (1);
    }
    
    // 清屏
    SSD1306_Clear();
    
    // 绘制文本
    SSD1306_DrawString(0, 0, "Hello OLED!", &font_5x7_def, SSD1306_COLOR_WHITE);
    SSD1306_DrawString(0, 10, "I2C + SPI Driver", &font_5x7_def, SSD1306_COLOR_WHITE);
    
    // 绘制图形
    SSD1306_DrawRectangle(0, 25, 60, 20, SSD1306_COLOR_WHITE);
    SSD1306_FillRectangle(2, 27, 10, 16, SSD1306_COLOR_WHITE);
    SSD1306_DrawCircle(90, 35, 10, SSD1306_COLOR_WHITE);
    
    // 绘制进度条
    SSD1306_DrawRectangle(0, 50, 100, 8, SSD1306_COLOR_WHITE);
    SSD1306_FillRectangle(2, 52, 50, 4, SSD1306_COLOR_WHITE);
    
    // 刷新显示
    SSD1306_Display();
    
    while (1) {
        // 动态更新
        static uint8_t progress = 0;
        progress = (progress + 1) % 96;
        
        SSD1306_FillRectangle(2, 52, 96, 4, SSD1306_COLOR_BLACK);  // 清除
        SSD1306_FillRectangle(2, 52, progress, 4, SSD1306_COLOR_WHITE);  // 重绘
        SSD1306_Display();
        
        HAL_Delay(50);
    }
}

七、性能优化与调试技巧

7.1 性能优化策略

优化点 方法 效果
I2C突发写入 单次事务发送1024字节 减少90%的I2C开销
脏页追踪 只更新修改过的页 5-10倍速度提升
页对齐填充 矩形填充时直接操作字节 8倍速度提升
DMA传输 SPI/I2C使用DMA CPU零等待
字体缓存 常用字符预渲染 减少重复计算

7.2 常见问题排查

症状 可能原因 解决方案
屏幕全黑 未使能电荷泵 发送0x8D, 0x14
显示倒置 扫描方向错误 调整0xA0/0xA1和0xC0/0xC8
对比度低 对比度值太小 增加0x81后的值
I2C无ACK 地址错误或模块损坏 检查0x3C/0x3D,用示波器抓波形
显示花屏 显存数据错误 检查寻址模式,确保列/页地址正确
闪烁严重 刷新率太低 提高I2C速度或改用SPI

7.3 调试工具

c 复制代码
// 调试模式:显示帧缓冲内容到串口
void SSD1306_DumpBuffer(void)
{
    printf("Frame Buffer Dump:\n");
    for (uint8_t page = 0; page < SSD1306_PAGES; page++) {
        printf("Page %d: ", page);
        for (uint8_t col = 0; col < SSD1306_WIDTH; col++) {
            printf("%02X ", s_frame_buffer[page][col]);
            if ((col + 1) % 16 == 0) printf("\n");
        }
    }
}

// 测试模式:显示棋盘格
void SSD1306_TestPattern(void)
{
    for (uint8_t y = 0; y < SSD1306_HEIGHT; y++) {
        for (uint8_t x = 0; x < SSD1306_WIDTH; x++) {
            SSD1306_DrawPixel(x, y, ((x / 8 + y / 8) % 2) ? SSD1306_COLOR_WHITE : SSD1306_COLOR_BLACK);
        }
    }
    SSD1306_Display();
}

八、总结

SSD1306驱动开发是嵌入式图形编程的入门必修课。通过本文的讲解,你应该掌握了:

  1. 双协议抽象:I2C和SPI的硬件差异,以及如何通过编译时配置统一API
  2. 显存架构:理解页寻址模式,设计高效的帧缓冲和脏页追踪
  3. 初始化序列:16条精密命令背后的硬件原理
  4. 字库设计:点阵字模的存储格式和绘制方法
  5. 图形算法:Bresenham画线和中点画圆的整数算法
  6. 性能优化:从突发写入到DMA传输的完整优化链路

一个优秀的SSD1306驱动不仅是"能显示",更应该是可移植 (分离硬件抽象层)、可扩展 (支持多种字体和图形)、高性能(脏页追踪+DMA)的工程化代码。


转载自:https://blog.csdn.net/u014727709/article/details/162279123

欢迎 👍点赞✍评论⭐收藏,欢迎指正