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 单像素点亮与熄灭

现在咱们来点一个像素。

点像素的思路是这样的:

  1. 计算出这个像素所在的页和列
  2. 读取当前GDDRAM中该位置的值(注意,SSD1306不支持读操作,所以你得在MCU内存里维护一个显存副本)
  3. 修改对应的位
  4. 写回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);
    }
}

运行这段代码,屏幕上会出现一个十字。如果十字歪了或者断了,说明你的坐标计算或者页地址设置有误。

调试建议:刚开始学的时候,别急着画复杂图形。先画一个点,确认位置对了。再画一条线,确认连续写入没问题。最后再画全屏。一步一步来,出问题了也好排查。

好了,像素操作的基础就这些。说白了就是三件事:设地址、写数据、维护缓冲区。下一章咱们会讲如何用这些基础操作来绘制字符和汉字。

到时候见。