从0开始点亮OLED屏幕(一)IIC时序篇

从0开始点亮OLED屏幕(一)IIC时序篇

1. IIC通信协议基础

第一次接触OLED屏幕开发时,我被IIC时序折腾得够呛。记得当时用STM32F103调试SSD1306屏幕,屏幕死活不亮,最后发现是起始信号时序不对。这种两线制的通信协议看似简单,实际藏着不少魔鬼细节。

IIC(Inter-Integrated Circuit)是飞利浦在1982年推出的串行通信协议,用两根线就能连接多个设备。SDA负责数据传输,SCL提供时钟信号,这种设计特别适合传感器、EEPROM这类低速设备。你可能不知道,现在市面上80%的0.96寸OLED模块都采用IIC接口,因为它比SPI节省引脚资源。

空闲状态下两根线都保持高电平,这让我想起第一次用逻辑分析仪抓波形时的困惑——为什么示波器上总是两条直线?原来需要主设备主动发起通信。协议规定标准模式速率100kHz,快速模式400kHz,高速模式3.4MHz,但实际使用中我发现很多国产OLED在超过200kHz时就会丢数据。

2. 硬件连接与初始化

我的开发板上SCL接PA11,SDA接PA12,这个组合看似随意其实有讲究。STM32的GPIO口有复用功能,但做IIC模拟时序时最好避开I2C1/I2C2默认引脚,避免冲突。有次我把SDA接到PB7(I2C1的SDA),结果死活调不通,后来才发现是硬件I2C模块影响了电平。

初始化GPIO时要特别注意:

#define GPIO_SCL_PORT GPIOA #define GPIO_SCL_PIN GPIO_Pin_11 #define GPIO_SDA_PORT GPIOA #define GPIO_SDA_PIN GPIO_Pin_12 void GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // SCL推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_SCL_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIO_SCL_PORT, &GPIO_InitStructure); // SDA初始化为开漏输出 GPIO_InitStructure.GPIO_Pin = GPIO_SDA_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_Init(GPIO_SDA_PORT, &GPIO_InitStructure); // 初始置高 GPIO_SetBits(GPIO_SCL_PORT, GPIO_SCL_PIN); GPIO_SetBits(GPIO_SDA_PORT, GPIO_SDA_PIN); }

这里有个坑:SDA必须配置为开漏输出(OD),因为IIC协议允许多主设备抢占总线。如果配置成推挽输出(PP),当两个设备同时输出不同电平时会短路。我曾因此烧过一个OLED模块,现在想起来还心疼。

3. 关键时序实现

3.1 起始与停止信号

起始信号就像打电话时的拨号动作:SCL高电平期间,SDA从高跳低。代码实现要注意时序间隔:

void I2C_Start(void) { SDA_OUT(); OLED_SDA(1); OLED_SCL(1); delay_us(5); // 保持时间>4.7us OLED_SDA(0); delay_us(5); OLED_SCL(0); // 准备发送数据 }

停止信号则是挂电话:SCL高电平时SDA从低跳高。有次我忘记加延时,导致从设备没识别到停止信号:

void I2C_Stop(void) { SDA_OUT(); OLED_SDA(0); delay_us(5); OLED_SCL(1); delay_us(5); OLED_SDA(1); // 停止条件建立时间>4us delay_us(5); }

3.2 数据发送与应答

发送字节时要遵循"数据在SCL低电平变化,高电平稳定"的原则。我最开始犯的错误是没处理高位在前:

void Send_Byte(uint8_t dat) { SDA_OUT(); for(uint8_t i=0; i<8; i++) { OLED_SCL(0); OLED_SDA((dat & 0x80) ? 1 : 0); // 先发最高位 delay_us(2); OLED_SCL(1); delay_us(2); dat <<= 1; OLED_SCL(0); } Wait_Ack(); }

应答检测是个容易忽略的环节。有次调试发现屏幕偶尔花屏,最后发现是没正确处理NACK:

uint8_t Wait_Ack(void) { uint8_t ack = 0; SDA_IN(); OLED_SCL(1); delay_us(2); if(READ_SDA() == 0) ack = 1; OLED_SCL(0); SDA_OUT(); return ack; }

4. 实战调试技巧

4.1 时序优化

不同OLED模块对时序要求差异很大。我用过的某款国产屏要求SCL高电平至少600ns,而SSD1306手册只要求400ns。建议用示波器测量实际波形,我通常这样调整延时:

#define IIC_DELAY() \ do { \ volatile int i=3; \ while(i--); \ } while(0)

这个简易延时在72MHz主频下约产生1us延时。如果通信不稳定,可以尝试:

  1. 增加SCL高电平时间
  2. 检查上拉电阻(通常4.7K-10K)
  3. 缩短总线长度(最好<30cm)

4.2 错误排查

当屏幕无反应时,建议按以下步骤排查:

  1. 用万用表测量VCC/GND电压(3.3V或5V)
  2. 检查上拉电阻是否接好
  3. 用逻辑分析仪抓取起始信号
  4. 确认设备地址(0x3C或0x3D)

有次我遇到屏幕闪烁问题,最后发现是电源滤波电容不足。建议在VCC和GND之间加个100nF电容,这是我踩过坑后的经验。

5. 进阶应用

5.1 多设备通信

IIC支持多从设备架构,通过地址区分。比如同时连接OLED和BMP280气压传感器:

#define OLED_ADDRESS 0x3C #define BMP280_ADDRESS 0x76 void Write_Device(uint8_t addr, uint8_t reg, uint8_t dat) { I2C_Start(); Send_Byte(addr << 1); // 写模式 Send_Byte(reg); Send_Byte(dat); I2C_Stop(); }

注意地址左移一位,最低位表示读写模式(0写/1读)。我曾因忘记移位导致设备无响应。

5.2 速率优化

标准模式下每个字节传输约100us,刷新整屏(128x64)需要约8ms。通过优化延时函数可以提升刷新率:

// 快速模式下的延时 #define FAST_DELAY() \ __asm__ volatile("nop;nop;nop;nop")

但要注意,过快的速率可能导致屏幕显示异常。建议从标准模式开始,逐步提高速率测试稳定性。