一、OLED驱动基础:SSD1306驱动芯片介绍、I2C通信协议详解、OLED像素矩阵与显存映射
各位同学,欢迎来到《血压计OLED屏幕驱动与UI设计实战》的第一课。
说实话,做嵌入式UI这么多年,我接触过各种各样的屏幕。从最古老的段式LCD,到TFT彩屏,再到今天我们要讲的OLED。但要说哪个屏幕最适合做小尺寸、低功耗的医疗设备?我个人首选就是SSD1306驱动的OLED屏。为什么?因为它简单、便宜、功耗极低,而且显示效果在室内环境下非常棒。
1.1 SSD1306驱动芯片介绍
SSD1306,这是一颗来自Solomon Systech的驱动芯片。它专门为OLED点阵屏而生。我最早接触它是在2016年,做一个便携式血氧仪的项目。当时选型时对比了好几款驱动芯片,最后还是选了它。
这颗芯片有几个关键特性,我给大家列一下:
- 分辨率支持:常见的是128×64像素,也有128×32的版本。我们血压计项目用128×64就足够了。
- 通信接口:支持I2C、SPI、并行接口。我建议初学者先用I2C,因为接线少,只有两根线。
- 工作电压:1.65V到3.3V,兼容3.3V的MCU系统。注意,它不兼容5V!
- 内部集成:内置了DC-DC升压电路、振荡器、显示RAM。说白了,你给它供电和指令,它自己就能干活。
- 功耗:正常显示时约20mA,休眠模式可以降到微安级别。这对电池供电的血压计来说,太重要了。
核心要点:SSD1306本质上是一个「显存映射型」驱动芯片。你往它的内部RAM写什么数据,屏幕上就显示什么内容。这个思想贯穿整个课程,请务必理解。
1.2 I2C通信协议详解
好,接下来我们聊聊I2C。很多初学者觉得I2C协议很复杂,其实不然。你想想看,它只有两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,通过地址来区分。
SSD1306的I2C地址是多少?通常是0x3C(写操作)或0x3D(读操作)。为什么有两个?因为I2C协议规定,地址的最后一位是读写标志位。0表示写,1表示读。所以实际器件地址是0x78(7位地址),左移一位后变成0x3C(写)和0x3D(读)。
嗯,这里要注意:有些模块的地址可能是0x3C,有些是0x3D。我遇到过好几次,买回来的OLED模块地址不对,折腾了半天才发现是地址搞错了。所以拿到模块后,第一件事就是看数据手册确认地址。
I2C通信的基本流程是这样的:
- 主机发送起始信号(SCL高电平时,SDA从高变低)
- 主机发送7位器件地址 + 1位读写位
- 从机应答(拉低SDA)
- 主机发送数据字节(8位),每发完一个字节等待从机应答
- 所有数据发送完毕,主机发送停止信号(SCL高电平时,SDA从低变高)
用代码实现的话,我习惯这样写I2C写操作:
// 伪代码示例:向SSD1306写入一个字节
void ssd1306_write_byte(uint8_t data) {
i2c_start(); // 起始信号
i2c_write_byte(0x3C); // 器件地址 + 写标志
i2c_wait_ack(); // 等待应答
i2c_write_byte(0x00); // 控制字节:0x00表示后续是命令,0x40表示数据
i2c_wait_ack();
i2c_write_byte(data); // 实际数据
i2c_wait_ack();
i2c_stop(); // 停止信号
}
个人经验:I2C通信最容易出问题的地方是时序。我曾经在一个项目里,因为SCL时钟频率设置得太高(超过400kHz),导致OLED屏幕偶尔显示异常。后来把频率降到100kHz,问题就解决了。所以,如果遇到显示不稳定,先检查I2C时钟频率。
1.3 OLED像素矩阵与显存映射
这是今天最核心的内容。理解了显存映射,你就掌握了SSD1306的「灵魂」。
SSD1306的显存大小是128×64位,也就是1024字节(128×64÷8)。为什么是字节?因为每个像素用1位表示:1代表点亮,0代表熄灭。所以128列×64行,每8行组成一个字节,一共64行÷8=8页(Page)。
显存的组织方式是这样的:
| 页(Page) | 行范围 | 列范围 | 字节数 |
|---|---|---|---|
| Page 0 | 第0~7行 | 第0~127列 | 128字节 |
| Page 1 | 第8~15行 | 第0~127列 | 128字节 |
| ... | ... | ... | ... |
| Page 7 | 第56~63行 | 第0~127列 | 128字节 |
你想想看,如果我想在屏幕的(10, 20)位置点亮一个像素,该怎么做?
首先,计算它属于哪一页:20 ÷ 8 = 2(Page 2)。然后,计算它在页内的行偏移:20 % 8 = 4(第4行)。最后,列就是10。
所以,我需要往Page 2、第10列的那个字节里,写入一个值,让第4位(bit 4)为1。这个值就是0x10(二进制00010000)。
用代码实现就是:
// 设置像素点 (x, y) 为点亮状态
void ssd1306_set_pixel(uint8_t x, uint8_t y) {
uint8_t page = y / 8;
uint8_t bit_pos = y % 8;
// 先读取当前显存值,再修改对应位
buffer[page][x] |= (1 << bit_pos);
}
// 将缓冲区数据刷新到OLED
void ssd1306_refresh(void) {
for (uint8_t page = 0; page < 8; page++) {
ssd1306_write_command(0xB0 + page); // 设置页地址
ssd1306_write_command(0x00); // 设置列地址低4位
ssd1306_write_command(0x10); // 设置列地址高4位
for (uint8_t col = 0; col < 128; col++) {
ssd1306_write_data(buffer[page][col]);
}
}
}
避坑指南:我曾经犯过一个低级错误——忘记维护本地缓冲区。每次要显示新内容时,直接往OLED显存里写。结果导致屏幕闪烁严重,因为频繁的I2C写入占用了大量时间。正确的做法是:先在MCU的RAM里维护一个128×8字节的缓冲区,所有绘图操作都在缓冲区上进行,最后一次性刷新到OLED。这样既高效又稳定。
1.4 初始化流程与实战要点
SSD1306的初始化其实不复杂,但顺序很重要。我总结了一个「三步走」流程:
- 上电复位:给OLED供电后,等待至少100ms,让内部电路稳定。
- 发送初始化指令序列:包括关闭显示、设置对比度、设置扫描方向、设置COM引脚配置、开启显示等。
- 清屏并刷新:将本地缓冲区全部清零,然后调用刷新函数。
这里给出一份我常用的初始化代码:
void ssd1306_init(void) {
delay_ms(100); // 等待上电稳定
ssd1306_write_command(0xAE); // 关闭显示
ssd1306_write_command(0xD5); // 设置振荡器频率
ssd1306_write_command(0x80);
ssd1306_write_command(0xA8); // 设置多路复用比
ssd1306_write_command(0x3F); // 64行
ssd1306_write_command(0xD3); // 设置显示偏移
ssd1306_write_command(0x00);
ssd1306_write_command(0x40); // 设置显示起始行
ssd1306_write_command(0x8D); // 启用电荷泵
ssd1306_write_command(0x14);
ssd1306_write_command(0x20); // 设置内存地址模式
ssd1306_write_command(0x00); // 水平寻址模式
ssd1306_write_command(0xA1); // 列段重映射(左右翻转)
ssd1306_write_command(0xC8); // COM扫描方向(上下翻转)
ssd1306_write_command(0xDA); // 设置COM引脚配置
ssd1306_write_command(0x12);
ssd1306_write_command(0x81); // 设置对比度
ssd1306_write_command(0xCF);
ssd1306_write_command(0xD9); // 设置预充电周期
ssd1306_write_command(0xF1);
ssd1306_write_command(0xDB); // 设置VCOMH电压
ssd1306_write_command(0x40);
ssd1306_write_command(0xA4); // 全局显示开启
ssd1306_write_command(0xA6); // 正常显示(非反色)
ssd1306_write_command(0xAF); // 开启显示
// 清屏
memset(buffer, 0x00, 1024);
ssd1306_refresh();
}
小技巧:如果你发现屏幕显示的内容是镜像的,或者上下颠倒了,别慌。检查一下0xA1(列重映射)和0xC8(COM扫描方向)这两个指令。我习惯把0xA1改成0xA0,0xC8改成0xC0,就能翻转显示方向。这个在血压计UI设计时经常用到,因为不同安装方向需要不同的显示方向。
1.5 本章小结
好了,今天的内容就到这里。我们讲了SSD1306的基本特性、I2C通信协议、显存映射原理,以及初始化流程。这些都是后续UI设计的基础。
说实话,OLED驱动并不难,难的是理解「显存映射」这个思想。一旦你理解了,后面画点、画线、画汉字、画波形图,都是在这个基础上做文章。
下一章,我们会正式开始UI设计——从画一个简单的血压计界面开始。到时候,你会看到今天学的这些知识是如何落地到实际项目中的。
记住:纸上得来终觉浅,绝知此事要躬行。赶紧拿出你的开发板,把今天的代码跑一遍吧!