4. GPIO驱动开发
I2C总线,说实话,在嵌入式系统里太常见了。它只需要两根线——SCL(时钟)和SDA(数据),就能挂载多个设备。我最早接触I2C是在一个温湿度采集项目里,当时觉得这协议简单,结果一上手就踩了坑。嗯,今天咱们就把I2C从协议到实战,彻底讲透。
4.1 I2C协议时序:你得看懂波形
I2C的通信,说白了就是主设备控制时钟,从设备响应数据。每次通信都从起始条件开始,到停止条件结束。
- 起始条件:SCL高电平时,SDA从高变低。
- 停止条件:SCL高电平时,SDA从低变高。
- 数据位:SCL低电平时改变SDA,SCL高电平时采样SDA。
每个字节后面必须跟一个应答位(ACK)。从设备拉低SDA表示应答,不拉低就是非应答(NACK)。
关键点:I2C地址是7位或10位,但发送时左移1位,最低位是读写位(0=写,1=读)。比如SSD1306的7位地址是0x3C,实际发送0x78(写)或0x79(读)。
我遇到过一个问题:示波器上看波形完全正确,但设备就是不响应。后来发现是上拉电阻没焊,SDA和SCL浮空了。I2C总线必须外接上拉电阻,一般4.7kΩ,速度高时用2.2kΩ。
4.2 ESP32作为I2C主设备
ESP32的I2C控制器很灵活。你可以用任意GPIO模拟,也可以用硬件I2C。我个人习惯用硬件I2C,省CPU资源。
配置步骤很简单:
- 调用
i2c_param_config()设置参数。 - 调用
i2c_driver_install()安装驱动。 - 用
i2c_master_write_read_device()读写数据。
#include "driver/i2c.h"
#define I2C_MASTER_SCL_IO 22 // GPIO22
#define I2C_MASTER_SDA_IO 21 // GPIO21
#define I2C_MASTER_NUM 0 // I2C0
#define I2C_MASTER_FREQ_HZ 100000 // 100kHz
void i2c_master_init() {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
i2c_param_config(I2C_MASTER_NUM, &conf);
i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}
小技巧:频率别设太高。100kHz最稳,400kHz需要PCB走线注意。我试过1MHz,结果从设备跟不上,数据全乱。
4.3 ESP32作为I2C从设备
ESP32也能当从设备。这在多主系统或传感器数据中转时很有用。配置时把模式改成 I2C_MODE_SLAVE,然后注册回调函数处理读写请求。
void i2c_slave_init() {
i2c_config_t conf = {
.mode = I2C_MODE_SLAVE,
.sda_io_num = GPIO_NUM_21,
.scl_io_num = GPIO_NUM_22,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.slave.addr_10bit_en = 0,
.slave.slave_addr = 0x50, // 从设备地址
};
i2c_param_config(I2C_NUM_0, &conf);
i2c_driver_install(I2C_NUM_0, conf.mode, 1024, 1024, 0);
}
从设备模式下,你需要用 i2c_slave_read_buffer() 和 i2c_slave_write_buffer() 来收发数据。注意,从设备不能主动发起通信,只能响应主设备的请求。
注意:从设备地址不能和总线上其他设备冲突。我见过有人把两个设备设成同一个地址,结果总线直接锁死。
4.4 驱动SSD1306 OLED屏幕
SSD1306是128×64像素的OLED驱动芯片,支持I2C和SPI。咱们用I2C模式,因为它连线少。
驱动SSD1306,核心就三步:
- 初始化:发送配置命令序列。
- 写数据:把像素数据写入显存。
- 刷新显示:SSD1306会自动从显存刷新,你只需要保证显存数据正确。
初始化序列比较长,我贴个精简版:
void ssd1306_init() {
uint8_t cmds[] = {
0xAE, // 关闭显示
0xD5, 0x80, // 设置振荡频率
0xA8, 0x3F, // 设置多路复用比
0xD3, 0x00, // 设置显示偏移
0x40, // 设置显示起始行
0x8D, 0x14, // 启用电荷泵
0x20, 0x00, // 设置内存地址模式(水平)
0xA1, // 段重映射(左右翻转)
0xC8, // COM扫描方向(上下翻转)
0xDA, 0x12, // 设置COM引脚配置
0x81, 0xCF, // 设置对比度
0xD9, 0xF1, // 设置预充电周期
0xDB, 0x40, // 设置VCOMH电压
0xA4, // 全局显示开启
0xA6, // 正常显示(非反色)
0xAF // 开启显示
};
for (int i = 0; i < sizeof(cmds); i++) {
i2c_master_write_slave(I2C_MASTER_NUM, SSD1306_ADDR, &cmds[i], 1);
}
}
注意:SSD1306的I2C写操作,第一个字节是控制字节。0x00表示后续是命令,0x40表示后续是数据。很多新手忘了这个,直接发命令,屏幕死活不亮。
写像素数据时,你需要维护一个128×64/8 = 1024字节的显存缓冲区。每个bit对应一个像素,1亮0灭。我习惯用 uint8_t buffer[128][8] 来管理。
void ssd1306_draw_pixel(uint8_t x, uint8_t y, uint8_t color) {
if (x >= 128 || y >= 64) return;
if (color) {
buffer[x][y / 8] |= (1 << (y % 8));
} else {
buffer[x][y / 8] &= ~(1 << (y % 8));
}
}
void ssd1306_refresh() {
uint8_t cmd[] = {0x21, 0, 127, 0x22, 0, 7}; // 设置列和页范围
i2c_master_write_slave(I2C_MASTER_NUM, SSD1306_ADDR, cmd, 6);
for (int page = 0; page < 8; page++) {
uint8_t data[129];
data[0] = 0x40; // 数据控制字节
for (int col = 0; col < 128; col++) {
data[col + 1] = buffer[col][page];
}
i2c_master_write_slave(I2C_MASTER_NUM, SSD1306_ADDR, data, 129);
}
}
4.5 知识结构总览
下面这张图,把I2C驱动SSD1306的整个流程串起来了。你一看就明白。
4.6 常见问题与避坑
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 屏幕不亮 | 初始化序列错误或电荷泵未开启 | 检查0x8D 0x14命令是否发送 |
| 显示花屏 | 显存地址模式设置错误 | 确保0x20 0x00(水平模式)已设置 |
| I2C通信超时 | 上拉电阻缺失或频率过高 | 加4.7kΩ上拉,降频到100kHz |
| 从设备无应答 | 地址错误或设备未上电 | 用示波器确认地址波形 |
我曾经在一个项目里,SSD1306怎么调都不亮。折腾了两天,最后发现是I2C地址搞错了。SSD1306的默认地址是0x3C,但有些模块把SA0引脚拉高,地址变成0x3D。你拿到模块后,先看原理图确认地址。
嗯,I2C驱动SSD1306,说白了就是协议加时序加显存管理。你只要把这三块吃透,换任何I2C设备都能快速上手。下次咱们聊聊SPI总线,那个速度更快,但连线也多两根。