/**********************************************************************
|
* Copyright: (C)2021 LingYun IoT System Studio <www.weike-iot.com>
|
* Author: GuoWenxue<guowenxue@gmail.com> QQ: 281143292
|
* Description: ISKBoard OLED(N091-2832TSWFG02-H14, 128x32) SSD1306 driver
|
*
|
* ChangeLog:
|
* Version Date Author Description
|
* V1.0.0 2021.08.10 GuoWenxue Release initial version
|
* V2.0.0 2026.04.29 Optimized GRAM buffered rendering
|
***********************************************************************/
|
|
#include <string.h>
|
#include "oled_drv.h"
|
#include "oled_font.h"
|
#include "miscdev.h"
|
|
/* I2C 设备 OLED */
|
static i2c_dev_t oled;
|
|
/* -----------------------------------------------------------------------
|
* 屏幕本地显存镜像缓冲区(Shadow GRAM): 128列 x 4页
|
* 所有绘制操作先写入此缓冲区,调用 OLED_Refresh() 后才真正发送到屏幕,
|
* ----------------------------------------------------------------------- */
|
static uint8_t s_gram[OLED_PAGES][X_WIDTH];
|
|
/* -----------------------------------------------------------------------
|
* I2C 突发传输数据打包缓冲区,大小为 128列 + 1字节控制头 = 129 字节。
|
* 用于在冲刷(Refresh)每一页显存时,将控制字节 (OLED_DATA) 与一页的
|
* 128 字节 GRAM 数据在连续的内存中拼接,从而实现单次 I2C 事务的流式高效写入。
|
* ----------------------------------------------------------------------- */
|
static uint8_t s_tx_packet[X_WIDTH + 1];
|
|
/*
|
* ====================================
|
* 1. OLED 底层内部私有通信函数
|
* ====================================
|
*/
|
|
static int oled_write_cmd(uint8_t cmd)
|
{
|
i2c_msg_t msg;
|
int ret = -2;
|
uint8_t buf[2];
|
|
/* 准备数据包:控制字节(OLED_CMD) + 命令字节 */
|
buf[0] = OLED_CMD;
|
buf[1] = cmd;
|
|
/* 配置 I2C 消息结构体 */
|
msg.addr = oled.dev_addr; /* 7位从机地址 */
|
msg.flags = I2C_M_WR; /* 写标志 */
|
msg.len = 2; /* 数据长度 = 控制字节 + 命令字节 */
|
msg.buf = buf; /* 数据指针 */
|
|
/* 将I2C总线加锁让设备独占总线 */
|
i2c_lock_dev(&oled);
|
|
/* 执行 I2C 传输 */
|
if (i2c_transfer(oled.bus, &msg, 1) != I2C_OK)
|
{
|
goto cleanup;
|
}
|
|
ret = 0;
|
|
cleanup:
|
/* 释放设备的 I2C 总线锁 */
|
i2c_free_dev(&oled);
|
return ret;
|
}
|
|
int OLED_Init(i2c_bus_t *bus)
|
{
|
if (!bus)
|
return -1;
|
|
if( i2c_bind_dev(&oled, bus, OLED_I2C_ADDR) < 0)
|
return -2;
|
|
mdelay(100);
|
|
oled_write_cmd(0xAE); /* Display OFF */
|
oled_write_cmd(0x40); /* Display Start Line = 0 */
|
oled_write_cmd(0xB0); /* Page Start Address = 0 */
|
oled_write_cmd(0xC8); /* COM Scan: remapped (垂直翻转修正) */
|
|
oled_write_cmd(0x81); /* Contrast Control */
|
oled_write_cmd(0xFF); /* 最大对比度 */
|
oled_write_cmd(0xA1); /* Segment Remap: SEG127→SEG0 */
|
oled_write_cmd(0xA6); /* Normal Display (非反色) */
|
|
oled_write_cmd(0xA8); /* Multiplex Ratio */
|
oled_write_cmd(0x1F); /* MUX=31 → 32行 (128×32屏) */
|
oled_write_cmd(0xD3); /* Display Offset */
|
oled_write_cmd(0x00); /* 垂直偏移=0 */
|
|
oled_write_cmd(0xD5); /* Display Clock Divide Ratio / OSC */
|
oled_write_cmd(0xF0); /* 高频率 + 默认分频,提高刷新率 */
|
oled_write_cmd(0xD9); /* Pre-charge Period */
|
oled_write_cmd(0x22); /* Phase1=2, Phase2=2 DCLK */
|
|
oled_write_cmd(0xDA); /* COM Pins Hardware Config */
|
oled_write_cmd(0x02); /* 适用于 128×32(128×64 用 0x12) */
|
|
oled_write_cmd(0xDB); /* VCOMH */
|
oled_write_cmd(0x49); /* ≈ 0.83×Vcc */
|
|
/* 开启 OLED 显示面板 */
|
OLED_Display_On();
|
|
/* 将显示屏黑屏 */
|
OLED_Clear();
|
OLED_Refresh();
|
|
return 0;
|
}
|
|
/**
|
* @brief 开启 OLED 显示面板
|
*/
|
void OLED_Display_On(void)
|
{
|
oled_write_cmd(0x8D); /* Charge Pump Setting */
|
oled_write_cmd(0x14); /* 电荷泵使能 */
|
oled_write_cmd(0xAF); /* Display ON */
|
}
|
|
/**
|
* @brief 关闭 OLED 显示面板
|
*/
|
void OLED_Display_Off(void)
|
{
|
oled_write_cmd(0x8D); /* Charge Pump Setting */
|
oled_write_cmd(0x10); /* 电荷泵停用 */
|
oled_write_cmd(0xAE); /* Display OFF */
|
}
|
|
/* 设置页地址(Page Address), SSD1306 显示屏按页寻址,每页 8 行像素
|
* page 表示页号(0~3,对应 32px 高度的 4 页)
|
*/
|
void oled_set_pos(uint8_t x, uint8_t page)
|
{
|
oled_write_cmd( 0xB0 | (page & 0x07) ); /* 页地址 */
|
oled_write_cmd( 0x10 | ((x & 0xF0) >> 4) ); /* 列高4位 */
|
oled_write_cmd( x & 0x0F ); /* 列低4位 */
|
}
|
|
/**
|
* @brief 将本地 GRAM 缓冲区突发(Burst)全量更新至屏幕物理显存
|
*/
|
void OLED_Refresh(void)
|
{
|
uint8_t page;
|
i2c_msg_t msg;
|
|
/* 锁住整个整幅图像的刷新周期,避免多页之间总线控制权被切走造成画面闪烁 */
|
i2c_lock_dev(&oled);
|
|
msg.addr = oled.dev_addr;
|
msg.flags = I2C_M_WR;
|
msg.len = X_WIDTH + 1;
|
msg.buf = s_tx_packet;
|
s_tx_packet[0] = (uint8_t)OLED_DATA;
|
|
for (page = 0; page < OLED_PAGES; page++)
|
{
|
oled_set_pos(0, page);
|
|
memcpy(&s_tx_packet[1], s_gram[page], X_WIDTH);
|
|
if (i2c_transfer(oled.bus, &msg, 1) != I2C_OK)
|
{
|
goto cleanup;
|
}
|
udelay(5);
|
}
|
|
cleanup:
|
i2c_free_dev(&oled);
|
}
|
|
/*
|
* ====================================
|
* 2. 内存渲染 API (无需涉及I2C总线操作)
|
* ====================================
|
*/
|
|
/**
|
* @brief 清空软件 GRAM 缓冲区(全黑), 需配合 OLED_Refresh() 使用。
|
*/
|
void OLED_Clear(void)
|
{
|
memset(s_gram, 0x00, sizeof(s_gram));
|
}
|
|
/**
|
* @brief 点亮软件 GRAM 缓冲区中所有像素(全白), 需配合 OLED_Refresh() 使用。
|
*/
|
void OLED_On(void)
|
{
|
memset(s_gram, 0xFF, sizeof(s_gram));
|
}
|
|
/**
|
* @brief 在本地显存区绘制一个 ASCII 字符
|
*/
|
void OLED_ShowChar(uint8_t x, uint8_t page, char chr, uint8_t char_size)
|
{
|
uint8_t c;
|
uint8_t i;
|
uint8_t col;
|
|
if (chr < ' ' || chr > '~')
|
{
|
chr = '?';
|
}
|
c = chr - ' ';
|
|
if (char_size == OLED_FONT16)
|
{
|
for (i = 0; i < 8; i++)
|
{
|
col = x + i;
|
if (col >= X_WIDTH)
|
{
|
break;
|
}
|
if (page < OLED_PAGES)
|
{
|
s_gram[page][col] = F8X16[c * 16 + i];
|
}
|
if ((page + 1) < OLED_PAGES)
|
{
|
s_gram[page + 1][col] = F8X16[c * 16 + i + 8];
|
}
|
}
|
}
|
else
|
{
|
for (i = 0; i < 6; i++)
|
{
|
col = x + i;
|
if (col >= X_WIDTH)
|
{
|
break;
|
}
|
if (page < OLED_PAGES)
|
{
|
s_gram[page][col] = F6x8[c][i];
|
}
|
}
|
}
|
}
|
|
/**
|
* @brief 在本地显存区绘制字符串
|
*/
|
void OLED_ShowString(uint8_t x, uint8_t page, const char *str, uint8_t font_size)
|
{
|
uint8_t char_width;
|
uint8_t char_pages;
|
|
char_width = (font_size == OLED_FONT16) ? 8 : 6;
|
char_pages = (font_size == OLED_FONT16) ? 2 : 1;
|
|
if (!str)
|
{
|
return;
|
}
|
|
while (*str != '\0')
|
{
|
if (page + char_pages > OLED_PAGES)
|
{
|
break;
|
}
|
|
OLED_ShowChar(x, page, *str, font_size);
|
x += char_width;
|
if (x + char_width > X_WIDTH)
|
{
|
x = 0;
|
page += char_pages;
|
}
|
str++;
|
}
|
}
|
|
/**
|
* @brief 在本地显存区绘制 16x16 汉字
|
*/
|
void OLED_ShowChinese(const uint8_t (*Hzk)[32], uint8_t x, uint8_t page, uint8_t index)
|
{
|
uint8_t i;
|
uint8_t col;
|
|
if (!Hzk || (page + 1 >= OLED_PAGES))
|
{
|
return;
|
}
|
|
for (i = 0; i < 16; i++)
|
{
|
col = x + i;
|
if (col >= X_WIDTH)
|
{
|
break;
|
}
|
s_gram[page][col] = Hzk[2 * index][i];
|
s_gram[page + 1][col] = Hzk[2 * index + 1][i];
|
}
|
}
|
|
/**
|
* @brief 在本地显存区绘制单色 BMP 位图
|
*/
|
void OLED_DrawBMP(uint8_t x0, uint8_t page0, uint8_t x_width, uint8_t y_width, const uint8_t *bmp)
|
{
|
uint8_t page;
|
uint8_t x;
|
uint8_t pages;
|
uint32_t j;
|
|
pages = (y_width + 7) / 8;
|
j = 0;
|
|
if (!bmp)
|
{
|
return;
|
}
|
|
for (page = page0; page < page0 + pages && page < OLED_PAGES; page++)
|
{
|
for (x = x0; x < x0 + x_width && x < X_WIDTH; x++)
|
{
|
s_gram[page][x] = bmp[j++];
|
}
|
}
|
}
|
|
/*
|
* ====================================
|
* 3. OLED 驱动测试及示范代码
|
* ====================================
|
*/
|
|
void oled_test_example(void)
|
{
|
uint8_t i;
|
const char *title = "ISKBoard";
|
|
/* 1. 硬件总线与外设初始化 */
|
OLED_Init(&i2c1_bus);
|
mdelay(500);
|
|
/* 2. 第一阶段:全屏点阵 Logo 图像显示测试 */
|
OLED_Clear();
|
/* 参数说明: 起始X=0, 起始页=PAGE0, 宽度=128, 高度=32, 数据源 */
|
OLED_DrawBMP(0, OLED_PAGE0, BMP_LOGO_WIDTH, BMP_LOGO_HEIGHT, bmp_logo);
|
OLED_Refresh(); /* 冲刷到物理屏幕 */
|
mdelay(3000); /* 保持显示 3 秒 */
|
|
|
/* 3. 第二阶段:16x16 宋体汉字 "凌云实验室" 显示测试 */
|
OLED_Clear();
|
/* * 基于 index 寻址:
|
* 传入 index=0,函数内部会自动抓取 Hzk_LingYun[0] 和 Hzk_LingYun[1] 组合成“凌”
|
* 传入 index=1,函数内部会自动抓取 Hzk_LingYun[2] 和 Hzk_LingYun[3] 组合成“云”
|
* 每个字横向占 16 像素,起始 X 从 24 开始,使 5 个字在 128 像素宽的屏幕上完美居中
|
*/
|
OLED_ShowChinese(Hzk_LingYun, 24, OLED_PAGE1, 0); /* “凌” (index: 0) */
|
OLED_ShowChinese(Hzk_LingYun, 40, OLED_PAGE1, 1); /* “云” (index: 1) */
|
OLED_ShowChinese(Hzk_LingYun, 56, OLED_PAGE1, 2); /* “实” (index: 2) */
|
OLED_ShowChinese(Hzk_LingYun, 72, OLED_PAGE1, 3); /* “验” (index: 3) */
|
OLED_ShowChinese(Hzk_LingYun, 88, OLED_PAGE1, 4); /* “室” (index: 4) */
|
OLED_Refresh(); /* 将渲染好的 5 个宋体汉字一次性刷入屏幕 */
|
mdelay(2500); /* 保持显示 2.5 秒 */
|
|
/* 4. 第三阶段:标准 ASCII 字符串动效打字机显示测试 */
|
OLED_Clear();
|
OLED_Refresh();
|
|
/* 动态计算位置,从第 32 列、第 1 页开始以 16 号大字体逐字敲出 "ISKBoard" */
|
for (i = 0; title[i] != '\0'; i++)
|
{
|
OLED_ShowChar(32 + (i * 8), OLED_PAGE1, title[i], OLED_FONT16);
|
OLED_Refresh(); /* 每写一个字符,立即同步刷新到屏幕,形成打字机动效 */
|
mdelay(150);
|
}
|
mdelay(2000);
|
|
/* 5. 第四阶段:测试结束,清屏复位 */
|
OLED_Clear();
|
OLED_ShowString(24, OLED_PAGE1, "- Test Over -", OLED_FONT16);
|
OLED_Refresh();
|
mdelay(1000);
|
|
/* 6. 将显示屏黑屏并关闭显示 */
|
OLED_Clear();
|
OLED_Refresh();
|
OLED_Display_Off();
|
}
|