4、像素操作基础:设置页地址与列地址、写数据到GDDRAM、单像素点亮与熄灭、全屏填充测试
好,咱们今天聊点实在的。
OLED屏驱动,说白了就是跟一块显存打交道。这块显存叫GDDRAM,你往里面写什么,屏幕上就显示什么。听起来简单吧?但很多新手一上来就被各种地址、页、列搞晕了。
我刚开始做这个的时候也踩过坑。有一次调了半天屏幕不亮,最后发现是页地址设错了。嗯,咱们今天就把这些基础操作彻底讲透。
4.1 理解GDDRAM的组织结构
先说说GDDRAM长什么样。
SSD1306这款驱动芯片,内部有128×64位的GDDRAM。但注意,它不是按我们习惯的「行×列」来组织的,而是分成了8页。
为什么是8页?因为每页对应8行像素。64行 ÷ 8 = 8页,刚好。
| 页号 | 对应行范围 | 列范围 |
|---|---|---|
| Page 0 | 第0~7行 | 第0~127列 |
| Page 1 | 第8~15行 | 第0~127列 |
| Page 2 | 第16~23行 | 第0~127列 |
| ... | ... | ... |
| Page 7 | 第56~63行 | 第0~127列 |
你想想看,每次写数据,其实是在写某一页的某一列。一个字节8位,正好对应一页里的8行像素。
举个例子:你往Page 0、Column 0写一个0xFF,那屏幕左上角8个像素全亮。写0x01,就只有最下面那一个像素亮。
核心理解:GDDRAM的每个字节,控制的是同一列上连续8行的像素。高位对应上面,低位对应下面。
4.2 设置页地址与列地址
操作GDDRAM之前,必须先告诉芯片:你要写哪一页、哪一列。
这就像在Excel里填数据,你得先选中单元格。
4.2.1 设置页地址
页地址通过命令0xB0~0xB7来设置。0xB0对应Page 0,0xB1对应Page 1,以此类推。
// 设置页地址为 Page 2
void OLED_SetPage(uint8_t page) {
if(page > 7) page = 7; // 防止越界
OLED_WriteCmd(0xB0 | page);
}
这里有个小细节:0xB0的低4位是页号。所以0xB0 | page,page范围0~7,刚好对应8页。
4.2.2 设置列地址
列地址分两步:高4位和低4位。
为什么这么麻烦?因为列地址范围是0~127,需要7位来表示。但SSD1306的命令格式限制,只能一次传4位数据。
// 设置列地址为 col (0~127)
void OLED_SetColumn(uint8_t col) {
if(col > 127) col = 127;
OLED_WriteCmd(0x10 | (col >> 4)); // 高4位
OLED_WriteCmd(0x00 | (col & 0x0F)); // 低4位
}
我个人习惯把这两个操作封装成一个函数:
void OLED_SetPosition(uint8_t page, uint8_t col) {
OLED_SetPage(page);
OLED_SetColumn(col);
}
小技巧:连续写数据时,列地址会自动递增。你只需要设置一次起始地址,然后连续发数据就行。但页地址不会自动递增,跨页操作需要手动切换。
4.3 写数据到GDDRAM
地址设好了,接下来就是写数据。
写数据用命令0x40(其实这个命令可以省略,因为上电后默认就是写数据模式)。但为了保险,我一般都会显式设置一下。
void OLED_WriteData(uint8_t data) {
// 假设你已经实现了I2C或SPI的底层发送函数
// 这里以I2C为例
I2C_Start();
I2C_SendByte(OLED_ADDR | 0x40); // 0x40表示写数据
I2C_SendByte(data);
I2C_Stop();
}
// 批量写数据
void OLED_WriteDataBuf(uint8_t *buf, uint16_t len) {
I2C_Start();
I2C_SendByte(OLED_ADDR | 0x40);
for(uint16_t i = 0; i < len; i++) {
I2C_SendByte(buf[i]);
}
I2C_Stop();
}
注意看,写数据和写命令的区别就在那个0x40上。写命令是0x00,写数据是0x40。这个搞错了,屏幕不会有任何反应。
注意:我曾经在项目里犯过一个低级错误——写命令和写数据共用一个函数,结果屏幕显示完全错乱。排查了半天才发现是控制字节没区分。所以,强烈建议把写命令和写数据分开封装。
4.4 单像素点亮与熄灭
现在咱们来点一个像素。
点像素的思路是这样的:
- 计算出这个像素所在的页和列
- 读取当前GDDRAM中该位置的值(注意,SSD1306不支持读操作,所以你得在MCU内存里维护一个显存副本)
- 修改对应的位
- 写回GDDRAM
为什么要维护显存副本?因为SSD1306不支持从GDDRAM读数据。你没法「读-改-写」,只能「记-改-写」。
// 在MCU内存中维护的显存副本
uint8_t OLED_Buffer[8][128]; // 8页 × 128列
// 点亮一个像素
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
if(x >= 128 || y >= 64) return; // 边界检查
uint8_t page = y / 8; // 计算页号
uint8_t bit = y % 8; // 计算位位置 (0~7)
uint8_t col = x; // 列号就是x坐标
if(color) {
// 点亮:置1
OLED_Buffer[page][col] |= (1 << bit);
} else {
// 熄灭:清0
OLED_Buffer[page][col] &= ~(1 << bit);
}
// 写回GDDRAM
OLED_SetPosition(page, col);
OLED_WriteData(OLED_Buffer[page][col]);
}
这里有个关键点:bit的计算。y % 8得到0~7,对应8行。但注意,SSD1306的位顺序是:bit0对应页内最下面一行,bit7对应最上面一行。
举个例子:y=0,bit=0,对应Page 0的最下面一行。y=7,bit=7,对应Page 0的最上面一行。
容易搞混的地方:很多人以为y=0是屏幕最上面,但实际上在GDDRAM里,y=0对应的是页内的最低位。所以画出来的图形可能是上下颠倒的。解决办法有两个:要么在绘制时做坐标转换,要么在初始化时设置扫描方向。
4.5 全屏填充测试
写完了单像素操作,咱们来做个全屏填充测试。这是调试屏幕时最常用的手段——看看屏幕能不能正常点亮所有像素。
// 全屏填充
void OLED_Fill(uint8_t data) {
for(uint8_t page = 0; page < 8; page++) {
OLED_SetPage(page);
OLED_SetColumn(0);
// 连续写128个字节
for(uint8_t col = 0; col < 128; col++) {
OLED_WriteData(data);
OLED_Buffer[page][col] = data; // 同步更新缓冲区
}
}
}
// 全屏点亮测试
void OLED_FillTest(void) {
OLED_Fill(0xFF); // 全亮
delay_ms(1000);
OLED_Fill(0x00); // 全灭
delay_ms(1000);
OLED_Fill(0xAA); // 棋盘格效果
delay_ms(1000);
OLED_Fill(0x55); // 反相棋盘格
delay_ms(1000);
}
全屏填充0xFF,如果屏幕上有坏点,一眼就能看出来。0xAA和0x55的交替显示,可以检查相邻像素之间有没有短路或串扰。
我记得有一次做批量测试,发现一批屏幕在填充0xAA时出现竖条纹。排查下来是柔性排线接触不良导致的。全屏填充测试帮我快速定位了问题。
4.6 综合示例:画一个十字
最后,咱们把今天学的知识串起来,画一个十字:
void OLED_DrawCross(void) {
// 先清屏
OLED_Fill(0x00);
// 画水平线:y=32,x从0到127
for(uint8_t x = 0; x < 128; x++) {
OLED_DrawPixel(x, 32, 1);
}
// 画垂直线:x=64,y从0到63
for(uint8_t y = 0; y < 64; y++) {
OLED_DrawPixel(64, y, 1);
}
}
运行这段代码,屏幕上会出现一个十字。如果十字歪了或者断了,说明你的坐标计算或者页地址设置有误。
调试建议:刚开始学的时候,别急着画复杂图形。先画一个点,确认位置对了。再画一条线,确认连续写入没问题。最后再画全屏。一步一步来,出问题了也好排查。
好了,像素操作的基础就这些。说白了就是三件事:设地址、写数据、维护缓冲区。下一章咱们会讲如何用这些基础操作来绘制字符和汉字。
到时候见。