4. 显存操作与页寻址模式:GDDRAM结构、页地址模式、列地址范围、水平/垂直寻址模式

好,咱们今天聊点实在的。显存操作,说白了就是搞清楚数据怎么在驱动芯片里存放和搬运。很多新手调OLED,上来就写数据,发现屏幕花屏或者显示错位,十有八九是没搞懂GDDRAM的结构和寻址模式。我当年第一次调SSD1306的时候,就因为这个坑折腾了一整天。

4.1 GDDRAM结构:屏幕背后的“画板”

GDDRAM,全称是Graphic Display Data RAM。你可以把它想象成一块二维画板。每个像素点对应画板上的一个bit。点亮就是1,熄灭就是0。

对于一块128×64像素的单色OLED,它的GDDRAM大小是多少?

很简单:128列 × 64行 = 8192个bit = 1024个字节。

但驱动芯片内部并不是把64行当成一个连续平面来管理的。它把64行分成了8个页(Page),每页8行。

参数 数值 说明
总行数 64 COM0 ~ COM63
页数 8 Page0 ~ Page7
每页行数 8 每页包含8个COM
列数 128 SEG0 ~ SEG127
每页字节数 128 每列1个bit,8行凑1字节
总字节数 1024 8页 × 128字节

这里有个关键点:每页的8行,在内存里是纵向排列的。一个字节的bit0对应页内第0行,bit7对应第7行。你写一个字节进去,实际上是同时点亮或熄灭同一列上的8个像素点。

核心理解:GDDRAM的地址 = 页地址 + 列地址。页地址决定哪一组8行,列地址决定哪一列。两者组合,才能定位到唯一的字节位置。

4.2 页地址模式:最常用的操作方式

页地址模式,是SSD1306默认的寻址方式,也是我个人用得最多的模式。为什么?因为它简单、直观,适合逐页刷新。

工作原理是这样的:

  • 你通过命令设置当前页地址(0~7)。
  • 再设置列起始地址(低4位和高4位两个命令)。
  • 然后连续写入数据,列地址会自动递增。
  • 当列地址到达127(最后一列),它会回绕到0,但页地址不变。

我曾经在一个项目中,需要快速刷新屏幕上的某个区域。如果用页地址模式,我只需要切换页地址,然后从指定列开始写数据,效率非常高。

设置页地址的命令很简单:

// 设置页地址为 Page 2
// 命令格式:0xB0 | PageNum
send_command(0xB2);  // 0xB0 | 0x02 = 0xB2

// 设置列起始地址为 32
// 需要两个命令:低4位和高4位
send_command(0x20);  // 设置列起始地址低4位
send_command(0x02);  // 列起始地址 = 32 (0x20)

// 然后就可以连续写入数据了
for (int i = 0; i < 64; i++) {
    send_data(buffer[i]);  // 列地址自动从32递增到95
}

小技巧:页地址模式适合逐行扫描式的显示更新。如果你要更新整个屏幕,通常的做法是:从Page0开始,写满128字节,然后切换到Page1,再写128字节……直到Page7。这样正好1024字节,覆盖整个GDDRAM。

4.3 列地址范围:别让数据跑偏了

列地址范围,决定了你的数据写到了屏幕的哪个水平位置。SSD1306的列地址范围是0~127,对应SEG0~SEG127。

但有个容易踩的坑:列地址的映射方向。

驱动芯片允许你通过命令反转列地址的映射顺序。也就是说,你可以让SEG0对应最左边,也可以让它对应最右边。这个功能由命令0xA0(列地址重映射)控制。

// 正常映射:SEG0 对应最左边
send_command(0xA0);

// 反转映射:SEG127 对应最左边
send_command(0xA1);

我记得有一次,帮同事调试一块屏幕,发现显示的内容是镜像的。查了半天,就是因为他初始化时用了0xA1,而他的数据是从左往右画的。改回0xA0,问题立刻解决。

注意:列地址范围还受显示偏移(Display Offset)命令影响。如果你设置了偏移,实际的列地址和物理像素位置可能不对应。调试时如果发现画面整体偏移,先检查这两个寄存器。

4.4 水平/垂直寻址模式:两种不同的“刷屏”姿势

除了页地址模式,SSD1306还支持水平寻址模式和垂直寻址模式。这两种模式,说白了就是改变了地址递增的方向。

寻址模式 地址递增方式 适用场景
页地址模式 列递增 → 列回绕 → 页不变 局部刷新、逐页操作
水平寻址模式 列递增 → 列回绕 → 页递增 → 页回绕 全屏刷新、连续帧缓冲
垂直寻址模式 页递增 → 页回绕 → 列递增 → 列回绕 竖屏滚动、特殊效果

水平寻址模式:你从(Page0, Col0)开始写数据,列地址一直递增到127,然后自动跳到下一页的Col0。这样连续写1024个字节,就能完整填充整个GDDRAM。我个人觉得,如果你要一次性刷新全屏,这个模式最省事。

垂直寻址模式:这个模式比较冷门。它的递增顺序是先页后列。也就是说,你先写满所有页的Col0,然后才跳到Col1。我在做竖屏滚动字幕的时候用过一次,效果还行,但平时基本用不上。

设置寻址模式的命令:

// 设置寻址模式
// 命令:0x20,后面跟一个参数
send_command(0x20);

// 参数:
// 0x00 = 水平寻址模式
// 0x01 = 垂直寻址模式
// 0x02 = 页地址模式(默认)

// 例如,切换到水平寻址模式
send_command(0x20);
send_command(0x00);

4.5 三种寻址模式对比:一张图看懂

下面这张SVG图,展示了三种寻址模式下,数据写入GDDRAM的路径。你可以直观地看到地址是怎么“走”的。

三种寻址模式数据写入路径对比 页地址模式 Page0: Col0→Col127 然后停止,页不变 需手动切换Page 适合:局部刷新 水平寻址模式 Page0: Col0→Col127 自动跳到Page1: Col0 直到Page7结束 适合:全屏刷新 垂直寻址模式 Col0: Page0→Page7 自动跳到Col1: Page0 直到Col127结束 适合:竖屏滚动 地址递增顺序对比 页地址模式:列递增 → 列回绕 → 页不变 水平寻址模式:列递增 → 列回绕 → 页递增 → 页回绕 垂直寻址模式:页递增 → 页回绕 → 列递增 → 列回绕 注:所有模式在地址到达最大值后都会回绕到起始地址

4.6 实战建议:选哪种模式?

嗯,这里我直接给结论吧,省得你纠结。

  • 默认用页地址模式:兼容性最好,代码逻辑清晰。适合大部分场景。
  • 全屏刷新用水平寻址模式:一次设置,连续写完1024字节,代码最简洁。
  • 垂直寻址模式:除非你做竖屏滚动或者特殊效果,否则别碰它。我用了这么多年,也就用过两次。

避坑指南:我曾经在切换寻址模式后,忘记重新设置列起始地址,结果数据写到了错误的位置。记住:切换寻址模式后,一定要重新设置页地址和列地址,否则驱动芯片会沿用之前的地址指针,导致数据错位。

好了,GDDRAM的结构和寻址模式就讲到这里。你只要记住:页决定哪8行,列决定哪一列,寻址模式决定地址怎么走。把这三点理清楚,显存操作这块就稳了。