一、OLED驱动接口:SPI vs I2C的选型博弈

OLED屏常见接口包括SPI(4线/3线)和I2C,两者在引脚占用、传输速率和复杂度上差异显著。

  • SPI接口

    • 引脚:SCLK、MOSI、DC(数据/命令)、CS(片选)、RST(复位),共5-6根。
    • 速率:可达10Mbps以上,适合高刷新率或动画显示。
    • 优势:传输快,可配合DMA实现无CPU干预的刷屏。
    • 劣势:占用引脚多,且需额外控制DC线。
  • I2C接口

    • 引脚:SCL、SDA,仅2根,支持多设备挂载。
    • 速率:标准模式100kbps,快速模式400kbps,远低于SPI。
    • 优势:节省引脚,接线简单,适合资源紧张的板卡。
    • 劣势:速率低,刷屏时CPU占用高,且需处理地址和ACK。

选型建议:若项目对显示流畅度要求高(如波形刷新),优先SPI+DMA;若仅显示静态信息且引脚紧张,I2C足够。此外,部分OLED模块同时支持两种接口,可通过电阻配置切换,设计时需预留兼容性。

二、底层驱动原理:从时序到初始化

无论SPI还是I2C,OLED驱动核心是向SSD1306(常见控制器)发送命令和数据。命令用于配置显示参数,数据则填充显存。

2.1 SPI时序要点

  • 模式:CPOL=0,CPHA=0(模式0),即空闲时SCLK为低,数据在上升沿采样。
  • DC线:高电平表示数据,低电平表示命令。
  • 传输流程:拉低CS→设置DC→发送字节→拉高CS。

2.2 I2C时序要点

  • 设备地址:通常为0x3C(写)或0x3D(读),由SA0引脚决定。
  • 控制字节:紧跟地址后,0x00表示命令,0x40表示数据。
  • 需注意:每次传输需等待ACK,且发送命令时需先发送控制字节。

2.3 初始化序列(以SSD1306为例)

初始化需配置显示关闭、时钟分频、复用比、偏移、起始行、电荷泵等。以下为精简序列:

void OLED_Init(void) {
    // 复位(若使用硬件复位引脚)
    OLED_RST_Clr(); delay_ms(100); OLED_RST_Set(); delay_ms(100);

    OLED_WriteCmd(0xAE); // 关闭显示
    OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 时钟分频
    OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 复用比(1/64)
    OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移
    OLED_WriteCmd(0x40); // 起始行0
    OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 电荷泵使能
    OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); // 内存寻址模式(水平)
    OLED_WriteCmd(0xA1); // 段重映射
    OLED_WriteCmd(0xC8); // COM扫描方向
    OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚配置
    OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 对比度
    OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期
    OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH
    OLED_WriteCmd(0xA4); // 全局显示开启
    OLED_WriteCmd(0xA6); // 正常显示
    OLED_WriteCmd(0xAF); // 开启显示
    OLED_Clear();
}

三、中文字库的存储与渲染

OLED显存为1KB(128*64/8),直接存储全字库不现实。常用方案是提取所需汉字模,存储于外部Flash或MCU内部Flash。

3.1 字模生成与格式

使用取模软件(如PCtoLCD2002)生成16x16点阵字模,格式选择“阴码、逐行式、列行式”等。通常每个汉字占32字节(16行*2字节)。

3.2 字库存储策略

  • 全字库存储:将GB2312常用汉字(约6763个)存于外部SPI Flash,占用约216KB,适合大容量场景。
  • 动态加载:仅存储常用汉字(如100个),按需从Flash读取,节省空间。
  • 内部Flash存储:若MCU Flash充足,可直接定义为常量数组,访问最快。

3.3 渲染函数实现

以下代码演示如何根据GB2312编码获取字模并显示:

// 假设字模存储在外部Flash,通过SPI读取
#define FONT_WIDTH  16
#define FONT_HEIGHT 16
#define FONT_BYTES  (FONT_WIDTH * FONT_HEIGHT / 8)

// 从字库文件读取字模(简化版)
void GetFontData(uint16_t code, uint8_t *buf) {
    uint32_t offset = (code - 0xB0A1) * FONT_BYTES; // GB2312区位码偏移
    // 从Flash读取offset处FONT_BYTES字节到buf
    SPI_Flash_Read(buf, offset, FONT_BYTES);
}

// 显示一个汉字(x,y为左上角坐标)
void OLED_ShowChinese(uint8_t x, uint8_t y, uint16_t code) {
    uint8_t buf[FONT_BYTES];
    GetFontData(code, buf);
    for (uint8_t row = 0; row < FONT_HEIGHT; row++) {
        for (uint8_t col = 0; col < FONT_WIDTH / 8; col++) {
            OLED_SetPos(x + col*8, y + row);
            OLED_WriteData(buf[row * (FONT_WIDTH/8) + col]);
        }
    }
}

3.4 内存优化技巧

  • 使用分页显示:仅更新变化区域,避免全屏刷新。
  • 双缓冲:在RAM中维护显存副本,批量修改后一次性写入,减少I/O操作。
  • 压缩字模:对稀疏字模进行RLE压缩,解压时动态生成。

四、完整代码示例:SPI驱动+中文字库

以下为基于STM32 HAL库的SPI驱动示例,包含初始化、显示字符和汉字。

// spi_oled.h
#ifndef __SPI_OLED_H
#define __SPI_OLED_H

#include "stm32f1xx_hal.h"

// 引脚定义(根据实际连接修改)
#define OLED_CS_PIN    GPIO_PIN_4
#define OLED_CS_PORT   GPIOA
#define OLED_DC_PIN    GPIO_PIN_2
#define OLED_DC_PORT   GPIOA
#define OLED_RST_PIN   GPIO_PIN_3
#define OLED_RST_PORT  GPIOA

void OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowString(uint8_t x, uint8_t y, char *str);
void OLED_ShowChinese(uint8_t x, uint8_t y, uint16_t code);

#endif
// spi_oled.c
#include "spi_oled.h"
#include <string.h>

extern SPI_HandleTypeDef hspi1;

static void OLED_WriteCmd(uint8_t cmd) {
    HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_RESET); // DC=0
    HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_RESET); // CS=0
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
    HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_SET);
}

static void OLED_WriteData(uint8_t data) {
    HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_SET); // DC=1
    HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&hspi1, &data, 1, 100);
    HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_SET);
}

static void OLED_SetPos(uint8_t x, uint8_t y) {
    OLED_WriteCmd(0xB0 + y / 8); // 页地址
    OLED_WriteCmd(0x00 + (x % 16)); // 列低4位
    OLED_WriteCmd(0x10 + (x / 16)); // 列高4位
}

void OLED_Init(void) {
    // 复位
    HAL_GPIO_WritePin(OLED_RST_PORT, OLED_RST_PIN, GPIO_PIN_RESET);
    HAL_Delay(100);
    HAL_GPIO_WritePin(OLED_RST_PORT, OLED_RST_PIN, GPIO_PIN_SET);
    HAL_Delay(100);

    // 初始化序列(同前)
    // ... 省略,参考上文
}

void OLED_Clear(void) {
    for (uint8_t page = 0; page < 8; page++) {
        OLED_SetPos(0, page * 8);
        for (uint16_t i = 0; i < 128; i++) {
            OLED_WriteData(0x00);
        }
    }
}

void OLED_ShowChar(uint8_t x, uint8_t y, char ch) {
    // 从ASCII字模表获取8x16点阵,此处省略字模数组
    // 实现类似ShowChinese,但字模为8x16
}

void OLED_ShowString(uint8_t x, uint8_t y, char *str) {
    while (*str) {
        if (*str < 0x80) { // ASCII字符
            OLED_ShowChar(x, y, *str);
            x += 8;
        } else { // 汉字(GB2312双字节)
            uint16_t code = ((uint8_t)*str << 8) | (uint8_t)*(str+1);
            OLED_ShowChinese(x, y, code);
            str++; // 跳过第二字节
            x += 16;
        }
        str++;
        if (x > 112) { x = 0; y += 16; } // 换行
    }
}

// 字模获取函数需根据实际存储介质实现
void OLED_ShowChinese(uint8_t x, uint8_t y, uint16_t code) {
    // 调用GetFontData并显示,参考上文
}

五、注意事项与调试技巧

  • 时序验证:使用逻辑分析仪抓取SPI/I2C波形,确认时钟极性和相位。
  • 电源稳定性:OLED电荷泵需稳定3.3V供电,避免纹波导致花屏。
  • 字模取模方向:务必与显示方向一致,否则汉字会镜像或颠倒。
  • DMA优化:若使用SPI+DMA,需注意缓冲区生命周期,避免数据被覆盖。
  • I2C速率:若总线有其他设备,适当降低速率至100kbps以保证稳定。
  • 调试建议:先点亮单像素,再显示字符,最后测试汉字,逐步排查。

通过合理选型接口和优化字库存储,OLED显示驱动可兼顾性能与资源。希望本文能助你在嵌入式显示领域更进一步。