这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » Let'sdo2026年第2期—OLED驱动

共1条 1/1 1 跳转至页

Let'sdo2026年第2期—OLED驱动

工程师
2026-10-07 22:27:59     打赏

0.91寸OLED的分辨率是 128 × 32,主控芯片是SSD1306。它通过 I2C 和 MCU 通信,地址 0x3C,因此定义基本参数如下

#define OLED_I2C              LPI2C0
#define OLED_I2C_BAUDRATE     400000U
#define OLED_COMMAND_CONTROL  0x00U
#define OLED_DATA_CONTROL     0x40U
#define SSD1306_I2C_ADDRESS  0x3CU

定义SSD1306的写数据,SSD1306的IIC帧格式是:从机地址 + 控制字节 + 后续字节。控制字节是0x00表示后面全是命令,是0x40表示后面全是显存数据。编写SSD1306写数据代码如下

static bool SSD1306_Write(uint8_t control, const uint8_t *data, size_t size)
{
    lpi2c_master_transfer_t transfer = {
        .flags = kLPI2C_TransferDefaultFlag,
        .slaveAddress = SSD1306_I2C_ADDRESS,
        .direction = kLPI2C_Write,
        .subaddress = control,
        .subaddressSize = 1U,
        .data = (void *)(uintptr_t)data,
        .dataSize = size,
    };

    g_ssd1306LastStatus = (int32_t)LPI2C_MasterTransferBlocking(OLED_I2C, &transfer);
    reurn g_ssd1306LastStatus == (int32_t)kStatus_Success;
}

接着进行OLED屏幕初始化,初始化分两步:先把 LPI2C 外设配置好(时钟源挂到 FRO 12M、分频 1、波特率 400kHz),上电后延时 100ms 等待面板供电稳定,然后一次性下发命令表:

static const uint8_t initCommands[] = {
    0xAEU,       /* 先关显示,配置期间不刷屏 */
    0xD5U, 0x80U, /* 振荡器频率/分频比:默认值(约 105 帧/s) */
    0xA8U, 0x1FU, /* 复用率 = 31 → 1/32 占空比,对应 32 行面板 */
    0xD3U, 0x00U, /* 显示偏移 = 0 */
    0x40U,        /* 显示起始行 = 0 */
    0x8DU, 0x14U, /* 开启内部电荷泵(模块通常没有外部升压) */
    0x20U, 0x02U, /* 显存寻址模式 = 页模式(page mode) */
    0xA1U,        /* 段重映射:左右镜像,保证文字方向正确 */
    0xC8U,        /* COM 扫描方向反向:上下方向修正 */
    0xDAU, 0x02U, /* COM 引脚配置 = 交替模式(128x32 面板必须) */
    0x81U, 0x8FU, /* 对比度 = 143 */
    0xD9U, 0xF1U, /* 预充电周期(相位1/相位2 = 1/15) */
    0xDBU, 0x40U, /* VCOMH 取消选择电平 */
    0xA4U,        /* 输出跟随显存内容(而不是全部点亮) */
    0xA6U,        /* 正常显示(不反色) */
    0x2EU,        /* 关闭滚动 */
    0xAFU,        /* 打开显示,配置生效 */
};

设计刷屏函数,用于实现屏幕清屏的操作。由于初始化使用页寻址模式,刷屏时需要手动指定“写到哪一页哪一列”:0xB0|page 设页地址,低 4 位列命令(0x00~0x0F)和高 4 位列命令(0x10~0x1F)设列地址。由于 0.91 寸模块128x32 面板的 128 个像素列在硬件上是从 SEG1 开始映射的,所以列地址要偏移 2,否则画面会整体右移/错位

#define OLED_COLUMN_OFFSET    2U

bool SSD1306_Update(void)
{
    uint8_t page;

    for (page = 0U; page < SSD1306_HEIGHT / 8U; page++)      /* 共 4 页 */
    {
        uint8_t addressCommands[] = {
            (uint8_t)(0xB0U | page),                          /* 设置页地址 0~3 */
            (uint8_t)(OLED_COLUMN_OFFSET & 0x0FU),            /* 列地址低 4 位 */
            (uint8_t)(0x10U | (OLED_COLUMN_OFFSET >> 4U)),    /* 列地址高 4 位 */
        };

        if (!SSD1306_WriteCommands(addressCommands, sizeof(addressCommands)) ||
            !SSD1306_Write(OLED_DATA_CONTROL,
                           &s_frameBuffer[(uint32_t)page * SSD1306_WIDTH],
                           SSD1306_WIDTH))                     /* 该页 128 字节一口气写入 */
        {
            return false;
        }
    }
    return true;
}

画点是所有绘图的原子操作,像素 (x, y) 的位置换算是:所在页 = y/8,所在字节 = 页号×128 + x,所在位 = y%8。设置用 |,清除用 & ~,互不干扰相邻像素,代码如下

void SSD1306_DrawPixel(uint8_t x, uint8_t y, bool color)
{
    uint32_t index;
    uint8_t mask;

    if ((x >= SSD1306_WIDTH) || (y >= SSD1306_HEIGHT))
    {
        return;                                 /* 越界直接丢弃 */
    }

    index = x + ((uint32_t)y / 8U) * SSD1306_WIDTH;   /* 字节偏移:先按页跨行,再加列 */
    mask = (uint8_t)(1U << (y & 7U));                 /* 页内行号 -> bit 掩码 */
    if (color)
    {
        s_frameBuffer[index] |= mask;
    }
    else
    {
        s_frameBuffer[index] &= (uint8_t)~mask;
    }
}

绘制字符:字库里每个字符是 5 列 × 7 行的点阵,按列存储:g_oledFont5x7[字符][列] 的第 n 个 bit 表示该列第 n 行是否点亮。绘制时逐列逐位还原成像素调用 DrawPixel。

#define OLED_CHARACTER_WIDTH  (OLED_FONT_WIDTH + 1U)   /* 5 列字模 + 1 列间隔 */
#define OLED_CHARACTER_HEIGHT 8U

void SSD1306_DrawChar(uint8_t x, uint8_t y, char character, bool foreground, bool background)
{
    uint32_t glyphIndex;
    uint8_t column, row;

    if (((uint8_t)character < OLED_FONT_FIRST_CHAR) || ((uint8_t)character > OLED_FONT_LAST_CHAR))
    {
        character = '?';
    }
    glyphIndex = (uint8_t)character - OLED_FONT_FIRST_CHAR;

    for (column = 0U; column < OLED_CHARACTER_WIDTH; column++)
    {
        for (row = 0U; row < OLED_CHARACTER_HEIGHT; row++)
        {
            bool pixel = (column < OLED_FONT_WIDTH) &&
                         ((g_oledFont5x7[glyphIndex][column] & (1U << row)) != 0U);
            SSD1306_DrawPixel((uint8_t)(x + column), (uint8_t)(y + row),
                              pixel ? foreground : background);
        }
    }
}

绘制字串:DrawString 维护一个光标 x,每写一个字符右移 6 像素。两种情况触发换行:遇到 \n,或者当前行放不下一个完整字符。换行时回到起始 x,y 下移一个字高(8 像素)

void SSD1306_DrawString(uint8_t x, uint8_t y, const char *text, bool foreground, bool background)
{
    uint8_t cursorX = x;

    if (text == NULL)
    {
        return;
    }
    while (*text != '\0')
    {
        if ((*text == '\n') || (cursorX + OLED_CHARACTER_WIDTH > SSD1306_WIDTH))
        {
            cursorX = x;                                  /* 回到行首 */
            y = (uint8_t)(y + OLED_CHARACTER_HEIGHT);     /* 下移一行 */
            if (*text == '\n')
            {
                text++;
                continue;
            }
        }
        SSD1306_DrawChar(cursorX, y, *text, foreground, background);
        cursorX = (uint8_t)(cursorX + OLED_CHARACTER_WIDTH);
        text++;
    }
}

在主程序中,整体调用逻辑可以总结为使用SSD1306_Init()初始化 + 清屏 + 首次刷新 -> 使用SSD1306_Clear()清空帧缓冲区 -> 使用SSD1306_DrawString()将字符信息写入内存 -> 使用SSD1306_Update()将512字节一次性推到GDDRAM




关键词: 0.91寸OLED     OLED    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]