毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享

基于同一款 SSD1306 OLED 模块,分别在 STM32、ESP32、Linux、FPGA 四个平台上完成驱动开发

重点讲解驱动架构设计、分层抽象、移植策略等工程实践方法。


不建议重复造轮子!!!但需要了解模块原理,需要源码可以免费分享。如何使用源码看文章最后的B站视频。

0、驱动使用方法及演示教程

资料获取方式:【腾讯文档】模块资料免费分享,骗人是小狗!https://docs.qq.com/doc/DZXJLQWVYT0J0aENh

一、驱动架构总览:分层设计思想

编写任何硬件驱动,核心原则都是分层解耦。将驱动划分为三个独立层次,每一层只与相邻层交互:

复制代码
+------------------------------------------+
|           应用层 (Application)            |  显示字符串、数字、图片、汉字
+------------------------------------------+
|          逻辑层 (Logic / Display API)      |  OLED_ShowChar, OLED_ShowNum, OLED_Clear...
+------------------------------------------+
|          传输层 (Transport / HAL)          |  OLED_WR_Byte(dat, cmd) ← 唯一抽象接口
+------------------------------------------+
|          硬件层 (HW Abstraction)           |  软件 IIC / 硬件 IIC / Linux /dev/i2c-1
+------------------------------------------+
|          硬件 (SSD1306)                   |
+------------------------------------------+

关键设计 :只要传输层 实现了 OLED_WR_Byte(dat, cmd) 这个统一接口,逻辑层应用层 的代码就可以在所有平台之间零修改复用。下面通过四个平台的实际代码,具体说明各层如何编写。


二、传输层(核心差异层)

传输层是四个平台差异最大的部分,也是驱动移植的核心工作。本节逐平台分析。

2.1 STM32:软件模拟 IIC

STM32 方案使用 GPIO 位操作直接模拟 IIC 时序,适用于没有硬件 IIC 外设或需要灵活选择引脚的场景。

2.1.1 GPIO 抽象宏(lt_oled.h

通过宏将 GPIO 操作封装为平台无关的接口:

c 复制代码
/* lt_oled.h --- STM32 HAL 库版本 */
#include "main.h"

#define delay_ms(x) HAL_Delay(x)

#define OLED_SCLK_Clr() HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL_Pin, GPIO_PIN_RESET)
#define OLED_SCLK_Set() HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL_Pin, GPIO_PIN_SET)
#define OLED_SDIN_Clr() HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA_Pin, GPIO_PIN_RESET)
#define OLED_SDIN_Set() HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA_Pin, GPIO_PIN_SET)

#define OLED_CMD  0   // 写命令
#define OLED_DATA 1   // 写数据

设计要点

  • 引脚定义(OLED_SCL_GPIO_PortOLED_SDA_Pin)不在驱动文件中写死,而是在 main.h 中定义,由 CubeMX 生成。用户只需在 CubeMX 中修改引脚即可完成硬件适配,驱动源码无需任何改动。
  • delay_ms 直接映射到 HAL_Delay(),避免引入额外依赖。

2.1.2 软件 IIC 时序实现(lt_oled.c

c 复制代码
/* lt_oled.c --- STM32 软件 IIC 实现 */

/* IIC 起始信号:SCL 高电平时 SDA 产生下降沿 */
void IIC_Start(void)
{
    OLED_SCLK_Set();
    OLED_SDIN_Set();
    OLED_SDIN_Clr();   // SDA 下降沿(SCL 仍为高)
    OLED_SCLK_Clr();   // 拉低 SCL,准备传输
}

/* IIC 停止信号:SCL 高电平时 SDA 产生上升沿 */
void IIC_Stop(void)
{
    OLED_SCLK_Set();
    OLED_SDIN_Clr();
    OLED_SDIN_Set();   // SDA 上升沿(SCL 仍为高)
}

/* 逐位发送,MSB 优先 */
void Write_IIC_Byte(unsigned char IIC_Byte)
{
    unsigned char i, m, da;
    da = IIC_Byte;
    OLED_SCLK_Clr();
    for (i = 0; i < 8; i++) {
        m = da & 0x80;          // 取最高位
        if (m == 0x80)
            OLED_SDIN_Set();
        else
            OLED_SDIN_Clr();
        da = da << 1;
        OLED_SCLK_Set();         // SCL 上升沿,从机采样
        OLED_SCLK_Clr();         // SCL 下降沿,准备下一位
    }
}

/* 写命令:Start → 地址 → 控制字节(0x00) → 命令 → Stop */
void Write_IIC_Command(unsigned char IIC_Command)
{
    IIC_Start();
    Write_IIC_Byte(0x78);           // SSD1306 写地址 (SA0=0 → 0x3C<<1 = 0x78)
    IIC_Wait_Ack();
    Write_IIC_Byte(0x00);           // 控制字节:后续为命令
    IIC_Wait_Ack();
    Write_IIC_Byte(IIC_Command);
    IIC_Wait_Ack();
    IIC_Stop();
}

/* 写数据:Start → 地址 → 控制字节(0x40) → 数据 → 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();
}

/* 统一的写字节接口 --- 传输层的唯一入口 */
void OLED_WR_Byte(unsigned dat, unsigned cmd)
{
    if (cmd)
        Write_IIC_Data(dat);       // 写数据
    else
        Write_IIC_Command(dat);     // 写命令
}

设计要点

  • 命令和数据通过控制字节 区分:0x00 表示命令,0x40 表示数据。这是 SSD1306 IIC 协议的规定。
  • 软件 IIC 不需要额外的延时函数,因为 GPIO 操作本身需要一定执行时间(72MHz 下约 100ns),足以满足 IIC 的时序要求。
  • 每个字节独立发送 Start/Stop,简化了逻辑但牺牲了效率。对 OLED 来说,显示内容不多,这个简化是合理的。

2.1.3 编译条件切换 IIC/SPI

STM32 版本通过宏 OLED_IIC 编译切换 IIC 和 SPI 两种传输方式:

c 复制代码
/* lt_oled.h */
#define OLED_IIC  1    // 1=IIC模式, 0=SPI模式

#if OLED_IIC
    // IIC 模式的引脚宏和函数声明
#else
    // SPI 模式额外需要 RES/DC/CS 三个引脚
    #define OLED_RST_Clr() HAL_GPIO_WritePin(OLED_RES_GPIO_Port, OLED_RES_Pin, GPIO_PIN_RESET)
    #define OLED_RST_Set() HAL_GPIO_WritePin(OLED_RES_GPIO_Port, OLED_RES_Pin, GPIO_PIN_SET)
    #define OLED_DC_Clr()  HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_RESET)
    #define OLED_DC_Set()  HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_SET)
    #define OLED_CS_Clr()  HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_RESET)
    #define OLED_CS_Set()  HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_SET)
#endif

对应 SPI 模式的 OLED_WR_Byte 实现也通过 #if OLED_IIC 条件编译:

c 复制代码
/* lt_oled.c --- SPI 模式 */
void OLED_WR_Byte(unsigned char dat, unsigned char cmd)
{
    unsigned char i;
    if (cmd)
        OLED_DC_Set();       // DC=1: 数据
    else
        OLED_DC_Clr();       // DC=0: 命令

    OLED_CS_Clr();           // 片选有效
    for (i = 0; i < 8; i++) {
        OLED_SCLK_Clr();
        if (dat & 0x80)
            OLED_SDIN_Set();
        else
            OLED_SDIN_Clr();
        OLED_SCLK_Set();     // 上升沿采样
        dat <<= 1;
    }
    OLED_CS_Set();           // 片选无效
    OLED_DC_Set();
}

设计要点

  • SPI 模式通过 DC 引脚区分命令/数据,无需控制字节。这是 SPI 接口与 IIC 接口的核心区别。
  • SPI 模式有硬件复位引脚 RES,初始化时需要手动拉低再拉高复位芯片。
  • 条件编译使得同一份代码可以适配不同接口模块,只需修改一个宏即可切换。

2.2 ESP32:硬件 IIC(ESP-IDF 驱动)

ESP32 方案利用 ESP-IDF 的 i2c_master_write_to_device() API 完成硬件 IIC 通信,不再手动操作 GPIO。

2.2.1 头文件设计(lt_oled.h

c 复制代码
/* lt_oled.h --- ESP32 版本 */
#include <stdint.h>
#include <unistd.h>

#define OLED_I2C_ADDR  0x3C    // 7位地址,ESP-IDF 自动左移

#define OLED_CMD  0
#define OLED_DATA 1

/* API 函数声明 */
int OLED_Init(uint8_t oled_iic_num);   // 传入 I2C 总线编号
void OLED_WR_Byte(uint8_t dat, uint8_t cmd);
// ... 其余逻辑层函数声明(与 STM32 完全一致)

与 STM32 版本的关键差异

  • 不再需要 GPIO 操作宏(OLED_SCLK_Set() 等),由 ESP-IDF 的 IIC 驱动接管底层。
  • OLED_Init() 接收 I2C 总线编号作为参数,允许选择使用哪条 I2C 总线。
  • I2C 地址使用 7 位格式 0x3C ,ESP-IDF 内部自动左移为 8 位地址;而 STM32 软件模拟使用的是 8 位写地址 0x780x3C << 1)。两者本质相同,只是表示方式不同。

2.2.2 硬件 IIC 传输实现(lt_oled.c

c 复制代码
/* lt_oled.c --- ESP32 硬件 IIC 实现 */
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "iic.h"        // ESP32 IIC 初始化函数

static uint8_t oled_iic = 0;

/* 通过 ESP-IDF API 一次性写入 2 字节(控制字节 + 数据/命令) */
int I2C_WriteByte(uint8_t type, uint8_t data)
{
    uint8_t buffer[2];
    buffer[0] = type;    // 0x00=命令, 0x40=数据
    buffer[1] = data;

    esp_err_t ret = i2c_master_write_to_device(
        oled_iic,              // I2C 总线编号
        OLED_I2C_ADDR,         // 从机 7 位地址
        buffer, 2,             // 数据缓冲区和长度
        2 / portTICK_PERIOD_MS // 超时时间
    );

    if (ret != ESP_OK) {
        printf("i2c_master_write_to_device err, code:%d\r\n", ret);
        return -1;
    }
    return 0;
}

/* 统一写接口 */
void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{
    if (cmd == OLED_CMD)
        I2C_WriteByte(0x00, dat);   // 命令
    else
        I2C_WriteByte(0x40, dat);   // 数据
}

/* 初始化 */
int OLED_Init(uint8_t oled_iic_num)
{
    esp_err_t ret = lt_iic_init(oled_iic_num, 400000);  // 400kHz IIC 时钟
    if (ret != ESP_OK) {
        printf("i2c init failed");
        return -1;
    }
    oled_iic = oled_iic_num;

    // SSD1306 初始化命令序列(与 STM32 完全相同)
    OLED_WR_Byte(0xAE, OLED_CMD);  // display off
    OLED_WR_Byte(0x00, OLED_CMD);  // set low column address
    // ... 其余命令 ...

    vTaskDelay(100);                 // FreeRTOS 延时
    OLED_Clear();
    return 0;
}

设计要点

  • ESP-IDF 的 i2c_master_write_to_device() 内部自动处理 Start/Stop/ACK,无需手动实现 IIC 时序。
  • 每次写入 2 字节(控制字节 + 数据),由硬件自动拼接为一个 IIC 事务。
  • 延时使用 vTaskDelay()(FreeRTOS 系统调用),而非忙等待,不占用 CPU。
  • 与 STM32 版本对比:传输层代码从 ~50 行缩减到 ~10 行,且时序更精确、速度更快(硬件 IIC 支持 400kHz 快速模式)。

2.3 Linux:/dev/i2c 设备节点

Linux 方案通过内核 IIC 子系统的 /dev/i2c-N 设备节点与 OLED 通信,实现用户空间驱动。

2.3.1 头文件设计(lt_oled.h

c 复制代码
/* lt_oled.h --- Linux 版本 */
#include <stdint.h>
#include <unistd.h>

#define OLED_I2C_ADDR  0x3C
#define OLED_CMD  0
#define OLED_DATA 1

/* 初始化传入设备路径和地址 */
int OLED_Init(const char *i2c_device, uint8_t addr);
void OLED_Close(void);   // Linux 特有:关闭文件描述符
// ... 其余函数声明

设计要点

  • OLED_Init() 接收设备路径(如 "/dev/i2c-1")和 I2C 地址,使驱动可以在不同 Linux 板子上使用不同 I2C 总线。
  • 新增 OLED_Close() 函数用于关闭文件描述符、释放资源。这是 Linux 驱动特有的需求。

2.3.2 Linux IIC 传输实现(lt_oled.c

c 复制代码
/* lt_oled.c --- Linux I2C 实现 */
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>

static int i2c_fd = -1;
static uint8_t oled_addr = OLED_I2C_ADDR;

void Delay_1ms(uint32_t Del_1ms)
{
    usleep(Del_1ms * 1000);
}

int I2C_WriteByte(uint8_t type, uint8_t data)
{
    uint8_t buffer[2];
    buffer[0] = type;
    buffer[1] = data;

    if (write(i2c_fd, buffer, 2) != 2) {
        return -1;
    }
    return 0;
}

void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{
    if (cmd == OLED_CMD)
        I2C_WriteByte(0x00, dat);
    else
        I2C_WriteByte(0x40, dat);
}

int OLED_Init(const char *i2c_device, uint8_t addr)
{
    /* 打开 I2C 设备节点 */
    i2c_fd = open(i2c_device, O_RDWR);
    if (i2c_fd < 0) {
        perror("open i2c device failed");
        return -1;
    }

    /* 通过 ioctl 设置从机地址 */
    if (ioctl(i2c_fd, I2C_SLAVE, addr) < 0) {
        perror("ioctl set slave addr failed");
        close(i2c_fd);
        return -1;
    }

    oled_addr = addr;

    /* SSD1306 初始化序列 */
    OLED_WR_Byte(0xAE, OLED_CMD);  // display off
    // ... 其余命令与 STM32 完全相同 ...

    Delay_1ms(100);
    OLED_Clear();
    return 0;
}

void OLED_Close(void)
{
    if (i2c_fd >= 0) {
        OLED_Display_Off();
        close(i2c_fd);
        i2c_fd = -1;
    }
}

设计要点

  • Linux 用户空间 IIC 的三步操作:open()ioctl(I2C_SLAVE)write()。其中 ioctl 将文件描述符绑定到特定从机地址,之后每次 write() 就是一次 IIC 写事务。
  • write(i2c_fd, buffer, 2) 一次性发送 2 字节,内核 IIC 驱动自动处理 Start/Stop/ACK。
  • 延时使用 usleep(),精度足够(OLED 不需要微秒级精度)。
  • 新增了 OLED_Close() 进行资源清理,这是良好的工程习惯。

2.4 FPGA:纯硬件 IIC 状态机

FPGA 方案与上述三个软件平台截然不同,使用 Verilog 硬件状态机 在 RTL 层面实现 IIC 协议。

2.4.1 模块端口定义

verilog 复制代码
/* oled_driver.v --- FPGA OLED 驱动顶层模块 */
module oled_driver (
    input  reset_n,           // 复位(低有效)
    input  sysclk,            // 系统时钟

    // 数据接口(来自显示逻辑模块)
    input  [7:0] s_data,      // 待显示数据
    input        s_data_en,  // 数据有效信号
    input  [7:0] s_data_lenght,  // 数据长度
    input  [7:0] s_x,        // X 坐标 (0~127)
    input  [2:0] s_y,        // Y 坐标 (页地址 0~7)

    // IIC 物理接口
    output oled_scl,          // IIC 时钟
    inout  oled_sda,          // IIC 数据(双向)

    // 状态输出
    output        oled_init,  // 初始化完成标志
    output [7:0]  s_data_cnt,// 已发送数据计数
    output        s_data_end // 数据发送完成标志
);

parameter SLAVE_ADDR = 7'b0111100;  // 7位地址 0x3C
parameter CLK_FREQ   = 26'b10111110101111000010000000;  // 系统时钟频率
parameter I2C_FREQ   = 18'b110000110101000000;           // IIC 目标频率

2.4.2 模块层次结构

FPGA 驱动采用自顶向下的模块化设计,包含三个子模块:

复制代码
oled_driver (顶层)
├── oled_init_inst    : 初始化状态机(发送 SSD1306 配置命令序列)
│   └── oled_clear_init : 清屏状态机(发送 8 页全零数据)
├── oled_show_inst    : 显示数据状态机(接收坐标和数据,发送到 OLED)
└── u_oled_iic_write  : IIC 写底层状态机(产生 SCL/SDA 时序)

2.4.3 顶层状态调度

verilog 复制代码
/* 顶层状态机:依次执行 初始化 → 清屏 → 等待显示数据 → 发送显示数据 */
parameter OLED_RST      = 8'b00000000;  // 复位状态
parameter OLED_INIT     = 8'b00000001;  // 发送初始化命令
parameter OLED_INIT_WAT = 8'b00000010;  // 等待初始化完成
parameter OLED_DATA     = 8'b00000011;  // 发送显示数据
parameter OLED_DATA_WAT = 8'b00000100;  // 等待发送完成

wire [7:0] oled_state;
wire i2c_exec;       // IIC 写请求
wire i2c_rh_wl;      // 读写方向(0=写)
wire [7:0] i2c_addr; // IIC 地址
wire [7:0] i2c_data_w;  // IIC 写数据

/* 状态机根据当前状态选择数据源 */
assign i2c_addr   = (oled_state == OLED_INIT) ? oled_init_inst/i2c_addr
                   : oled_show_inst/i2c_addr;
assign i2c_data_w = (oled_state == OLED_INIT) ? oled_init_inst/i2c_data_w
                   : oled_show_inst/i2c_data_w;

2.4.4 设计要点

  • 纯硬件并行:初始化、清屏、显示数据由不同状态机并行处理,通过顶层仲裁决定当前 IIC 总线的使用者。
  • IIC 分频 :通过 CLK_FREQI2C_FREQ 参数计算分频计数器,自动适配不同系统时钟频率。
  • 地址参数化SLAVE_ADDR 通过 parameter 定义,可灵活修改为其他 IIC 从机地址。
  • 与软件方案的对比
    • 软件:串行执行(初始化→清屏→显示),一次只能做一件事
    • FPGA:状态机可并行,初始化完成后自动切换到数据传输状态

三、传输层对比总结

维度 STM32 (软件IIC) ESP32 (硬件IIC) Linux (/dev/i2c) FPGA (RTL)
IIC 实现 GPIO 位操作模拟 ESP-IDF IIC 驱动 内核 IIC 子系统 Verilog 状态机
时钟速度 ~100kHz(受限于GPIO操作) 400kHz(硬件支持) 100/400kHz(内核配置) 可精确控制分频
地址格式 8位写地址 0x78 7位地址 0x3C 7位地址 0x3C 7位参数
Start/Stop 手动模拟 API 自动处理 write() 自动处理 状态机产生
延时方式 HAL_Delay() vTaskDelay() usleep() 时钟计数器
多设备支持 修改宏 传入 i2c_num 传入设备路径 参数化实例化
代码量 ~50行 ~10行 ~15行 ~数百行(状态机)

四、逻辑层(平台无关的显示 API)

逻辑层包含所有显示功能的实现逻辑。由于 OLED_WR_Byte(dat, cmd) 是统一的传输接口,逻辑层代码在四个平台上完全一致(STM32 的 IIC 和 SPI 分支除外)。

4.1 显存管理

4.1.1 显存数组

c 复制代码
/* 128 列 x 8 页,共 1024 字节 */
unsigned char OLED_GRAM[128][8];

SSD1306 的显存按 页(Page) 组织,每页高 8 像素,共 8 页覆盖 64 行:

复制代码
Page 0 → Y: 0~7     (OLED_GRAM[0~127][0])
Page 1 → Y: 8~15    (OLED_GRAM[0~127][1])
...
Page 7 → Y: 56~63   (OLED_GRAM[0~127][7])

4.1.2 显存刷新函数

c 复制代码
void OLED_Refresh_Gram(void)
{
    unsigned char i, n;
    for (i = 0; i < 8; i++) {
        OLED_WR_Byte(0xb0 + i, OLED_CMD);   // 设置页地址
        OLED_WR_Byte(0x00, OLED_CMD);         // 列低地址
        OLED_WR_Byte(0x10, OLED_CMD);         // 列高地址
        for (n = 0; n < 128; n++)
            OLED_WR_Byte(OLED_GRAM[n][i], OLED_DATA);
    }
}

注意 :这里的刷新方式是逐字节写入,每次写入都需要 Start+地址+控制字节+数据+Stop,效率较低。STM32 版本的 OLED_ShowChar 采用了另一种方式------直接定位写入位置,跳过显存数组。

4.2 坐标设置

c 复制代码
void OLED_Set_Pos(unsigned char x, unsigned char y)
{
    /* Flowers_screen 宏处理不同批次模块的列偏移问题 */
    #if Flowers_screen
    x = x;
    #else
    x = x + 2;   // 老版模块需要偏移 2 列
    #endif

    OLED_WR_Byte(0xb0 + y, OLED_CMD);               // 页地址
    OLED_WR_Byte((((x) & 0xf0) >> 4) | 0x10, OLED_CMD);  // 列高 4 位
    OLED_WR_Byte(((x) & 0x0f), OLED_CMD);           // 列低 4 位
}

设计要点Flowers_screen 宏用于处理不同批次 OLED 模块的显示偏移问题。老版模块内部有 2 列的偏移,新版模块正常。这是实际产品开发中常见的硬件兼容性处理。

4.3 画点函数

画点是所有图形操作的基础。核心是将 (x, y) 坐标映射到显存数组的正确位置:

c 复制代码
void OLED_DrawPoint(unsigned char x, unsigned char y, unsigned char t)
{
    unsigned char pos, bx, temp = 0;
    if (x > 127 || y > 63) return;   // 边界保护

    pos = 7 - y / 8;      // 页号(从底部算起,因为 0xb0+0 对应顶部)
    bx = y % 8;            // 页内行号
    temp = 1 << (7 - bx);  // 对应位的掩码

    if (t)
        OLED_GRAM[x][pos] |= temp;    // 置位(点亮)
    else
        OLED_GRAM[x][pos] &= ~temp;   // 清位(熄灭)
}

坐标映射关键理解

  • y=0pos=7(Page 7 是屏幕顶部,因为 SSD1306 默认从顶部开始映射)
  • y=63pos=0(Page 0 是屏幕底部)
  • 每个字节中,bit 7 对应页内最上面一行,bit 0 对应最下面一行

4.4 字符显示

c 复制代码
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr, uint8_t Char_Size)
{
    unsigned char c = chr - ' ';   // ASCII 偏移(字库从空格开始)

    if (x > Max_Column - 1) { x = 0; y = y + 2; }  // 自动换行

    if (Char_Size == 16) {
        // 16x16 字体:上半部分 8 字节写入当前页,下半部分 8 字节写入下一页
        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);  // 下半部
    } else {
        // 6x8 字体:6 字节写入单页
        OLED_Set_Pos(x, y);
        for (i = 0; i < 6; i++)
            OLED_WR_Byte(F6x8[c][i], OLED_DATA);
    }
}

设计要点

  • 16x16 字体的字库数组每个字符占 16 字节:前 8 字节为上半部分(对应一页),后 8 字节为下半部分(对应下一页)。写入时需要两次 OLED_Set_Pos() 分别定位到上下两页。
  • 6x8 字体的字库每个字符仅 6 字节,只占一页高度(8 像素),因此只需一次 OLED_Set_Pos()
  • 字库数组 F8X16[]F6x8[][]oledfont.h 中定义,通过 PCtoLCD2002 等字模软件生成。

4.5 汉字显示

c 复制代码
void OLED_ShowCHinese(uint8_t x, uint8_t y, uint8_t no)
{
    uint8_t col;

    for (col = 0; col < 16; col++) {
        // 上半部分:逐列写入
        OLED_Set_Pos(x + col, y);
        OLED_WR_Byte(Hzk[no * 2][col], OLED_DATA);

        // 下半部分:逐列写入
        OLED_Set_Pos(x + col, y + 1);
        OLED_WR_Byte(Hzk[no * 2 + 1][col], OLED_DATA);
    }
}

字库结构解析 :汉字字库 Hzk[][] 中,每个 16x16 汉字占 2 个 16 字节数组:

  • Hzk[no*2]:上半部分的 16 列数据
  • Hzk[no*2+1]:下半部分的 16 列数据

这种逐列存储 的格式与 ASCII 字体的逐行存储 格式(F8X16 是上 8 行 + 下 8 行)不同。汉字字库是按列方向组织的,每列一个字节(8 bit),16 列共 16 字节,所以每个汉字上下两部分各 16 字节。

4.6 数字与字符串

c 复制代码
static uint32_t oled_pow(uint8_t m, uint8_t n)
{
    uint32_t result = 1;
    while (n--) result *= m;
    return result;
}

void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size2)
{
    uint8_t t, temp;
    uint8_t enshow = 0;  // 前导零抑制标志

    for (t = 0; t < len; t++) {
        temp = (num / oled_pow(10, len - t - 1)) % 10;  // 提取每一位
        if (enshow == 0 && t < (len - 1)) {
            if (temp == 0) {
                OLED_ShowChar(x + (size2 / 2) * t, y, '0', size2);  // 前导零显示为'0'
                continue;
            } else {
                enshow = 1;
            }
        }
        OLED_ShowChar(x + (size2 / 2) * t, y, temp + '0', size2);
    }
}

前导零处理策略 :当前实现中 enshow 标志用于在非最后一位 遇到非零数字时激活显示。遇到前导零时显示 '0' 而非空格,这是一个设计选择------某些场景需要占位零,某些需要空格。根据需求修改即可。

4.7 图片与填充

c 复制代码
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;

    if (y1 % 8 == 0) y = y1 / 8;
    else y = y1 / 8 + 1;

    for (y = y0; y < y1; y++) {
        OLED_Set_Pos(x0, y);
        for (x = x0; x < x1; x++) {
            OLED_WR_Byte(BMP[j++], OLED_DATA);  // 逐字节写入
        }
    }
}

注意 :STM32 软件模拟 IIC 版本在 OLED_DrawBMP 中加入了 delay_ms(1) 延时,这是因为软件 IIC 速度较慢,连续大量写入可能导致从机来不及处理。


五、初始化序列详解

无论哪个平台,SSD1306 的初始化命令序列都相同。以下逐条解释每个命令的作用:

c 复制代码
void OLED_Init(void)
{
    delay_ms(200);  // 等待 OLED 上电稳定(VCC 达到工作电压)

    // ① 关闭显示(先配置再开显示,避免配置过程中闪烁)
    OLED_WR_Byte(0xAE, OLED_CMD);

    // ② 设置列地址范围(初始化时通常设为 0~127 全范围)
    OLED_WR_Byte(0x00, OLED_CMD);  // 列低地址 = 0
    OLED_WR_Byte(0x10, OLED_CMD);  // 列高地址 = 0

    // ③ 设置显示起始行(0x40 = 从第 0 行开始)
    OLED_WR_Byte(0x40, OLED_CMD);

    // ④ 设置对比度(0x00~0xFF,越大越亮)
    OLED_WR_Byte(0x81, OLED_CMD);  // 对比度命令
    OLED_WR_Byte(0xCF, OLED_CMD);  // 对比度值 = 0xCF

    // ⑤ 段重映射(控制左右翻转)
    OLED_WR_Byte(0xA1, OLED_CMD);  // 0xA0=左右反置, 0xA1=正常

    // ⑥ COM 扫描方向(控制上下翻转)
    OLED_WR_Byte(0xC8, OLED_CMD);  // 0xC0=上下反置, 0xC8=正常

    // ⑦ 正常显示模式(非反色)
    OLED_WR_Byte(0xA6, OLED_CMD);  // 0xA6=正常, 0xA7=反色

    // ⑧ 设置复用率(屏幕行数)
    OLED_WR_Byte(0xA8, OLED_CMD);
    OLED_WR_Byte(0x3F, OLED_CMD);  // 64 行(0x3F = 64-1)

    // ⑨ 显示偏移(垂直滚动偏移)
    OLED_WR_Byte(0xD3, OLED_CMD);
    OLED_WR_Byte(0x00, OLED_CMD);  // 无偏移

    // ⑩ 时钟分频 / 振荡器频率
    OLED_WR_Byte(0xD5, OLED_CMD);
    OLED_WR_Byte(0x80, OLED_CMD);  // 分频比=1, 振荡器频率=1000

    // ⑪ 预充电周期
    OLED_WR_Byte(0xD9, OLED_CMD);
    OLED_WR_Byte(0xF1, OLED_CMD);  // Phase1=15, Phase2=1

    // ⑫ COM 引脚硬件配置
    OLED_WR_Byte(0xDA, OLED_CMD);
    OLED_WR_Byte(0x12, OLED_CMD);  // Sequential COM pin config

    // ⑬ VCOMH 电压
    OLED_WR_Byte(0xDB, OLED_CMD);
    OLED_WR_Byte(0x40, OLED_CMD);  // ~0.77V

    // ⑭ 电荷泵使能(必须开启,否则屏幕不亮)
    OLED_WR_Byte(0x8D, OLED_CMD);
    OLED_WR_Byte(0x14, OLED_CMD);  // 开启内部电荷泵

    // ⑮ 开启显示
    OLED_WR_Byte(0xAF, OLED_CMD);

    delay_ms(200);
    OLED_Clear();  // 初始化完成后清屏
}

不同模块的初始化差异 :STM32 版本通过 OLED_30Pin 宏区分两种不同批次的模块。30Pin 模块和非 30Pin 模块的初始化序列在个别命令上有差异(如 VCOMH 值),实际使用时应根据具体模块调整。


六、驱动移植实战步骤

当需要将 OLED 驱动移植到新平台时,遵循以下步骤:

步骤 1:确定传输方式

  • 有硬件 IIC → 使用硬件 IIC(效率最高)
  • 无硬件 IIC → 软件 GPIO 模拟
  • 高速需求 → SPI 接口

步骤 2:实现传输层

只需实现一个函数:OLED_WR_Byte(dat, cmd)

c 复制代码
// 伪代码:新平台的传输层实现
void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{
    uint8_t ctrl = cmd ? 0x40 : 0x00;  // 数据/命令控制字节
    // 调用平台的 IIC 写函数,发送 [ctrl, dat] 两个字节
    platform_iic_write(SLAVE_ADDR, ctrl, dat);
}

步骤 3:实现初始化函数

根据平台实现 OLED_Init(),内部只需:

  1. 初始化通信接口(IIC/SPI)
  2. 复制 SSD1306 初始化命令序列(跨平台通用)
  3. 调用 OLED_Clear()

步骤 4:直接复用逻辑层

OLED_ShowCharOLED_ShowStringOLED_ShowNumOLED_ShowCHineseOLED_DrawBMP 等函数直接复制,无需修改。

步骤 5:添加字库数据

oledfont.h(包含 ASCII 和汉字字库数组)加入工程。

步骤 6:编写应用代码

c 复制代码
// 伪代码:应用层示例(所有平台通用)
OLED_Init();
OLED_Clear();
OLED_ShowString(0, 0, "Hello!", 16);
OLED_ShowNum(0, 2, 2026, 4, 16);
OLED_ShowCHinese(0, 4, 0);  // 显示字库中第 0 个汉字

七、工程技巧与踩坑总结

7.1 IIC 地址的三种表示

表示方式 使用场景
7 位地址 0x3C ESP-IDF、Linux ioctl、FPGA parameter
8 位写地址 0x78 = 0x3C << 1 STM32 软件 IIC
8 位读地址 0x79 = `(0x3C << 1) 1`

7.2 花屏问题处理

STM32 版本通过 Flowers_screen 宏处理不同批次模块的列偏移:

c 复制代码
#define Flowers_screen 1   // 老版=0(偏移2列), 新版=1(无偏移)

如果移植后出现左偏 2 列的花屏现象,检查列地址低位的设置。

7.3 字模取模配置

使用 PCtoLCD2002 提取字模时,必须确保以下配置与驱动解析方式一致:

配置项 ASCII 字库 F8X16 汉字字库 Hzk
取模方式 阴码(1=亮点) 阴码
取模走向 逐行式 逐列式
位序 高位在前 高位在前
数据排列 上半8字节 + 下半8字节 上半16字节 + 下半16字节

如果不一致,显示出来的字符/汉字会乱码或旋转。

7.4 FPGA 特有注意事项

  • FPGA 的 IIC 状态机需要精确的时钟分频计算,确保 SCL 频率在 100kHz~400kHz 之间。
  • SDA 是双向信号(inout),需要三态门控制:发送时为输出,接收时为高阻。
  • FPGA 没有操作系统,延时通过时钟计数器实现,不存在 delay_ms()

7.5 Linux 权限问题

Linux 下访问 IIC 设备需要 root 权限或添加用户到 iic 组:

bash 复制代码
# 方法 1:sudo 运行
sudo ./oled_demo

# 方法 2:添加 udev 规则(永久解决)
echo 'KERNEL=="i2c-[0-9]*", MODE="0666"' | sudo tee /etc/udev/rules.d/99-i2c.rules
sudo udevadm control --reload-rules

八、四个平台的 API 对比速查表

函数 STM32 ESP32 Linux FPGA
初始化 OLED_Init() OLED_Init(i2c_num) OLED_Init(dev, addr) 复位后自动初始化
关闭 OLED_Close()
清屏 OLED_Clear() OLED_Clear() OLED_Clear() oled_clear_init 模块
显示字符 OLED_ShowChar(x,y,chr,size) 相同 相同 通过 s_data 总线传入
显示字符串 OLED_ShowString(x,y,*p,size) 相同 相同 同上
显示数字 OLED_ShowNum(x,y,num,len,size) 相同 相同 同上
显示汉字 OLED_ShowCHinese(x,y,no) 相同 相同 同上
显示图片 OLED_DrawBMP(x0,y0,x1,y1,BMP) 相同 相同 同上
画点 OLED_DrawPoint(x,y,t) 相同 相同 同上
填充区域 OLED_Fill(x1,y1,x2,y2,dot) 相同 相同 同上

从表中可以看出,除了初始化函数和 Linux 的关闭函数外,所有显示 API 在四个平台上完全一致。这就是分层设计的价值。


本文基于零涂科技团队实际项目代码整理,代码覆盖 STM32 HAL、ESP-IDF、Linux 用户空间、FPGA Verilog 四个平台的完整 OLED 驱动实现。

相关推荐
tedcloud1231 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
SakitamaX2 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.2 小时前
Linux系统管理
linux·运维
阿龙的工作记录2 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
ye150127774552 小时前
220V转5V1A隔离芯片WT5602
单片机·嵌入式硬件·其他·硬件工程
代码村新手2 小时前
Linux的基本指令
linux·运维·服务器
ye150127774554 小时前
DC24V-100V转3.3V500mA隔离芯片WT5602
单片机·嵌入式硬件·其他·硬件工程
Lumos6fly4 小时前
51单片机从零到实战(15)——数字时钟温度计
单片机·嵌入式硬件·51单片机
撩得Android一次心动5 小时前
Linux编程笔记3【个人用】
linux·笔记