第3章 OLED显示驱动基础:SSD1306/SSH1106驱动芯片介绍、I2C/SPI通信协议、像素与显存映射
好,咱们进入正题。这一章是血氧仪显示部分的核心基础。说白了,你要让屏幕上显示波形和数字,就得先搞懂屏幕是怎么听话的。
我最早做血氧仪项目时,选的就是0.96寸的OLED屏。当时市面上主流的驱动芯片就两种:SSD1306和SSH1106。很多人以为它们差不多,其实坑不少。今天我就把这两兄弟的底细给你扒清楚。
3.1 SSD1306 vs SSH1106:你该选谁?
先看一张对比表,心里有个数:
| 特性 | SSD1306 | SSH1106 |
|---|---|---|
| 分辨率 | 128×64 | 132×64 |
| 显存大小 | 128×64 = 8192字节 | 132×64 = 8448字节 |
| 页模式 | 8页(每页8像素高) | 8页(每页8像素高) |
| I2C地址 | 0x3C 或 0x3D | 0x3C 或 0x3D |
| SPI支持 | 4线/3线 | 4线/3线 |
| 常见问题 | 列偏移需软件修正 | 列数多4列,需注意边界 |
嗯,这里有个关键点。SSD1306的显存是128列,但SSH1106是132列。我当年第一次用SSH1106时,直接套用了SSD1306的驱动代码,结果屏幕最右边多了4列乱码。排查了半天才发现是列地址范围没改。
3.2 I2C vs SPI:通信协议怎么选?
血氧仪这种小设备,引脚资源很宝贵。我个人习惯是:
- I2C:只用两根线(SDA、SCL),适合引脚少的MCU。速度一般在400kHz,刷新30帧没问题。
- SPI:需要4根线(CS、DC、SCLK、MOSI),但速度快得多,能到10MHz以上。如果你要显示动画或高频波形,SPI是首选。
你想想看,血氧仪显示波形时,每秒要刷新几十次。用I2C的话,每次传输一帧数据大约需要:
帧大小 = 128 × 64 / 8 = 1024字节
I2C传输时间 ≈ 1024 × 9 / 400kHz ≈ 23ms
加上命令开销,一帧约30ms
30ms一帧,也就是33fps,够用了。但如果你用SPI,同样的数据量只需要2-3ms。所以我的建议是:
3.3 像素与显存映射:屏幕是怎么记住画面的?
这个知识点,说白了就是「屏幕的脑子长什么样」。OLED的显存是按页组织的,每页8个像素高。128×64的屏幕,纵向被分成8页:
页0: 行0-7
页1: 行8-15
页2: 行16-23
...
页7: 行56-63
每一页有128个字节,每个字节对应一列上的8个像素。比如页0的第0个字节,控制的是第0列、第0行到第7行的像素。字节的bit0对应最上面一行,bit7对应最下面一行。
我刚开始学的时候,总觉得这个映射关系很绕。后来画了个图才明白:
列0 列1 列2 ... 列127
+----+----+----+ +----+
| b0 | b0 | b0 | | b0 | 行0
| b1 | b1 | b1 | | b1 | 行1
| b2 | b2 | b2 | | b2 | 行2
| b3 | b3 | b3 | | b3 | 行3
| b4 | b4 | b4 | | b4 | 行4
| b5 | b5 | b5 | | b5 | 行5
| b6 | b6 | b6 | | b6 | 行6
| b7 | b7 | b7 | | b7 | 行7
+----+----+----+ +----+
字节0 字节1 字节2 字节127
所以,如果你想在屏幕左上角点亮一个像素,位置是(0,0),你需要把页0、列0的字节写成0x01。如果想点亮(0,7),就是0x80。
页号 = y / 8
列号 = x
字节内偏移 = y % 8
该像素的掩码 = 1 << (y % 8)
3.4 实战:初始化序列与显存操作
好,理论说完了,咱们直接上代码。这是我常用的SSD1306初始化序列,I2C模式:
void OLED_Init(void) {
// 复位(如果RES引脚接了MCU)
OLED_RES_LOW();
delay_ms(10);
OLED_RES_HIGH();
delay_ms(10);
// 初始化命令序列
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置振荡频率
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); // 设置多路复用比
OLED_WriteCmd(0x3F); // 64行
OLED_WriteCmd(0xD3); // 显示偏移
OLED_WriteCmd(0x00);
OLED_WriteCmd(0x40); // 起始行
OLED_WriteCmd(0x8D); // 电荷泵
OLED_WriteCmd(0x14); // 开启
OLED_WriteCmd(0x20); // 内存地址模式
OLED_WriteCmd(0x00); // 水平模式
OLED_WriteCmd(0xA1); // 列段重映射
OLED_WriteCmd(0xC8); // COM扫描方向
OLED_WriteCmd(0xDA); // COM引脚配置
OLED_WriteCmd(0x12);
OLED_WriteCmd(0x81); // 对比度
OLED_WriteCmd(0xCF);
OLED_WriteCmd(0xD9); // 预充电周期
OLED_WriteCmd(0xF1);
OLED_WriteCmd(0xDB); // VCOMH电压
OLED_WriteCmd(0x40);
OLED_WriteCmd(0xA4); // 全局显示开启
OLED_WriteCmd(0xA6); // 正常显示(非反色)
OLED_WriteCmd(0xAF); // 开启显示
}
这里有个细节。我见过很多新手把0xA1和0xC8搞反。0xA1是列地址从左到右,0xC0是从上到下。如果你发现显示的内容是镜像的,八成是这两个参数配错了。
显存操作的核心函数就两个:
// 设置光标位置(页和列)
void OLED_SetCursor(uint8_t page, uint8_t col) {
OLED_WriteCmd(0xB0 + page); // 设置页地址
OLED_WriteCmd(0x00 + (col & 0x0F)); // 列低4位
OLED_WriteCmd(0x10 + (col >> 4)); // 列高4位
}
// 写一个字节到显存
void OLED_WriteData(uint8_t data) {
// I2C或SPI发送数据字节
I2C_Start();
I2C_WriteByte(0x3C << 1); // 设备地址+写
I2C_WriteByte(0x40); // 控制字节:数据模式
I2C_WriteByte(data);
I2C_Stop();
}
3.5 血氧仪中的实际应用
在血氧仪里,我们通常需要显示:
- 血氧值:大号数字,比如98%
- 脉率:中号数字,比如72bpm
- 脉搏波形:实时滚动的曲线
- 电池图标:小图标
波形显示是难点。我用的方法是:
- 维护一个128字节的环形缓冲区,存储最近128个采样点的幅值(0-63)
- 每次采样后,更新缓冲区
- 将缓冲区数据映射到显存:每个采样点对应一列,幅值决定该列点亮哪些像素
- 整屏刷新
代码大概长这样:
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
uint8_t waveform_buffer[SCREEN_WIDTH]; // 存储幅值
void DrawWaveform(void) {
uint8_t page, col, bit_mask;
// 清空显存缓冲区
memset(frame_buffer, 0x00, 1024);
// 绘制波形
for(col = 0; col < SCREEN_WIDTH; col++) {
uint8_t y = waveform_buffer[col]; // 幅值0-63
if(y >= SCREEN_HEIGHT) y = SCREEN_HEIGHT - 1;
page = y / 8;
bit_mask = 1 << (y % 8);
// 点亮该列对应的像素
frame_buffer[page * SCREEN_WIDTH + col] |= bit_mask;
// 如果幅值较大,可以画一条垂直线(从底部到幅值点)
// 这里省略,实际项目中会加
}
// 刷到屏幕
OLED_Refresh();
}
嗯,这里要注意。波形显示时,如果直接画点,看起来就是一堆离散的点。我习惯在点之间画连线,或者用垂直线表示幅值,视觉效果会好很多。
3.6 本章小结
这一章我们聊了:
- SSD1306和SSH1106的区别,选型时别搞混
- I2C和SPI的取舍,血氧仪用I2C够用,SPI更爽
- 显存映射的页-列-字节关系,这是所有绘图的基础
- 初始化序列和显存操作的实际代码
- 血氧仪波形显示的思路和代码
下一章,我们会深入讲字体渲染和数字显示。毕竟血氧仪上最核心的就是那几个大数字,怎么让它们清晰、美观、省显存,都是学问。
好,今天就到这里。有什么问题,咱们下节课见。