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
我要赚赏金
