ESP-IDF保姆级入门06|I2C通信详解 + 0.96寸OLED屏幕驱动实战:显示文字_数字_动态数据(零基础可复现)
专栏前言
前面我们已经掌握了 GPIO 输入输出、UART 串口通信,完成了ESP32最基础的两大外设。
从本篇开始,我们进入串行通信外设实战阶段,第一个核心就是:I2C 总线。
I2C 是嵌入式最常用的低速外设总线,传感器、屏幕、存储芯片、扩展IO几乎都依赖它。而0.96寸OLED(SSD1306)则是物联网项目的标配显示器件,成本低、功耗低、接线简单,非常适合新手入门。
本篇带你从零打通:
I2C 核心原理 → ESP-IDF 硬件I2C配置 → OLED驱动移植 → 文字/数字/图标显示 → 动态数据刷新。
全程代码可直接编译烧录,零基础也能做出自己的第一个显示界面。
一、I2C 通信核心基础(新手必懂)
1. 什么是I2C?
I2C 是一种半双工、两线制、多设备串行总线,仅需两根线即可挂载多个从设备:
- SCL:时钟线,主机控制通信节奏
- SDA:数据线,双向传输数据
和UART相比,I2C最大优势:两根线可挂载十几个设备,通过不同设备地址区分,大大节省IO资源。
2. ESP32 硬件I2C资源
ESP32 内置2 路硬件I2C控制器,支持标准模式(100kHz)、快速模式(400kHz),同样支持任意GPIO软件映射,无需固定引脚。
I2C0:常用,可自由配置
I2C1:扩展使用
二、硬件准备与接线说明
1. 物料清单
ESP32-S3 / ESP32 开发板
0.96寸 OLED 屏幕(SSD1306 驱动芯片,I2C接口)
杜邦线 4 根
2. 标准接线表
| OLED 引脚 | ESP32 引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 电源供电,禁止接5V |
| GND | GND | 共地 |
| SCL | GPIO9 | 时钟线,可自定义 |
| SDA | GPIO8 | 数据线,可自定义 |
重要提醒:OLED 默认 I2C 地址一般为0x3C或0x3D,不显示优先排查地址是否正确。
三、ESP-IDF 硬件I2C 初始化配置
ESP-IDF 提供标准化 I2C 驱动库driver/i2c.h,初始化固定三步:配置参数→安装驱动→完成就绪。
标准初始化代码(可直接复用)
#include "driver/i2c.h" #define I2C_MASTER_SCL_IO GPIO_NUM_9 #define I2C_MASTER_SDA_IO GPIO_NUM_8 #define I2C_MASTER_NUM I2C_NUM_0 #define I2C_MASTER_FREQ_HZ 400000 static void i2c_master_init(void) { i2c_config_t conf = { .mode = I2C_MODE_MASTER, // 主机模式 .sda_io_num = I2C_MASTER_SDA_IO, // SDA引脚 .scl_io_num = I2C_MASTER_SCL_IO, // SCL引脚 .sda_pullup_en = GPIO_PULLUP_ENABLE, // 开启内部上拉 .scl_pullup_en = GPIO_PULLUP_ENABLE, // 开启内部上拉 .master.clk_speed = I2C_MASTER_FREQ_HZ, // 时钟频率400kHz }; // 加载配置 i2c_param_config(I2C_MASTER_NUM, &conf); // 安装驱动 i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0); }关键知识点
ESP32 内部自带上拉电阻,一般无需外部接上拉
标准模式 100kHz,快速模式 400kHz,新手建议先用 100kHz 调试
必须配置为主机模式,由ESP32主动发起通信
四、实战:SSD1306 OLED 完整驱动与显示
我们采用SSD1306 128×64 分辨率屏幕,驱动逻辑分为三层:底层I2C写入 → 命令/数据发送 → 应用层显示函数。
1. 完整驱动源码(复制即用)
#include <stdio.h> #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/i2c.h" #include "font.h" // 引脚与地址定义 #define I2C_SCL_PIN GPIO_NUM_9 #define I2C_SDA_PIN GPIO_NUM_8 #define OLED_I2C_ADDR 0x3C #define I2C_PORT I2C_NUM_0 // OLED命令/数据标记 #define OLED_CMD 0 #define OLED_DATA 1 // 写命令函数 static void oled_write_cmd(uint8_t cmd) { uint8_t buf[2] = {0x00, cmd}; i2c_master_write_to_device(I2C_PORT, OLED_I2C_ADDR, buf, 2, pdMS_TO_TICKS(10)); } // 写数据函数 static void oled_write_data(uint8_t data) { uint8_t buf[2] = {0x40, data}; i2c_master_write_to_device(I2C_PORT, OLED_I2C_ADDR, buf, 2, pdMS_TO_TICKS(10)); } // OLED初始化 void oled_init(void) { vTaskDelay(pdMS_TO_TICKS(100)); // 上电延时 oled_write_cmd(0xAE); // 关闭显示 oled_write_cmd(0xD5); // 设置时钟分频 oled_write_cmd(0x80); oled_write_cmd(0xA8); // 设置分辨率 oled_write_cmd(0x3F); oled_write_cmd(0xD3); // 显示偏移 oled_write_cmd(0x00); oled_write_cmd(0x40); // 起始行 oled_write_cmd(0x8D); // 电荷泵 oled_write_cmd(0x14); oled_write_cmd(0x20); // 内存寻址模式 oled_write_cmd(0x02); oled_write_cmd(0xA1); // 段重映射 oled_write_cmd(0xC8); // 扫描方向 oled_write_cmd(0xDA); // 引脚配置 oled_write_cmd(0x12); oled_write_cmd(0x81); // 对比度 oled_write_cmd(0xCF); oled_write_cmd(0xD9); // 预充电周期 oled_write_cmd(0xF1); oled_write_cmd(0xDB); // VCOMH oled_write_cmd(0x30); oled_write_cmd(0xA4); // 全局显示开启 oled_write_cmd(0xA6); // 正常显示 oled_write_cmd(0xAF); // 开启显示 oled_clear(); } // 清屏函数 void oled_clear(void) { for(uint8_t i=0; i<8; i++) { oled_write_cmd(0xB0 + i); oled_write_cmd(0x00); oled_write_cmd(0x10); for(uint8_t j=0; j<128; j++) { oled_write_data(0x00); } } } // 设置光标位置 void oled_set_pos(uint8_t x, uint8_t y) { oled_write_cmd(0xB0 + y); oled_write_cmd(((x&0xF0)>>4) | 0x10); oled_write_cmd((x&0x0F) | 0x00); } // 显示单个ASCII字符 void oled_show_char(uint8_t x, uint8_t y, uint8_t chr) { uint8_t c = 0; c = chr - ' '; // 偏移 oled_set_pos(x, y); for(uint8_t i=0; i<8; i++) { oled_write_data(F8X16[c*16 + i]); } oled_set_pos(x, y+1); for(uint8_t i=0; i<8; i++) { oled_write_data(F8X16[c*16 + i + 8]); } } // 显示字符串 void oled_show_string(uint8_t x, uint8_t y, char *str) { uint8_t i = 0; while(str[i] != '\0') { oled_show_char(x + 8*i, y, str[i]); i++; } } // 显示数字 void oled_show_num(uint8_t x, uint8_t y, uint32_t num, uint8_t len) { char buf[16]; snprintf(buf, sizeof(buf), "%lu", num); oled_show_string(x, y, buf); }2. 字库文件说明
代码中用到的font.h为标准 8×16 ASCII 字库,包含常用英文字符、数字、符号。中文字符需额外取模生成,后续进阶篇会详细讲解。
五、主函数实战:静态文字 + 动态计数刷新
结合前面的 I2C 初始化与 OLED 驱动,实现开机显示标题、底部实时刷新运行秒数。
// I2C初始化函数(复用上面的i2c_master_init) static void i2c_master_init(void) { i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = I2C_SDA_PIN, .scl_io_num = I2C_SCL_PIN, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = 100000, // 新手先用100kHz更稳定 }; i2c_param_config(I2C_PORT, &conf); i2c_driver_install(I2C_PORT, conf.mode, 0, 0, 0); } void app_main(void) { // 初始化I2C i2c_master_init(); // 初始化OLED oled_init(); // 显示静态标题 oled_show_string(0, 0, "ESP32 I2C OLED"); oled_show_string(0, 2, "Hello ESP-IDF!"); oled_show_string(0, 4, "Run Time(s):"); uint32_t sec = 0; while(1) { // 动态刷新运行时间 oled_show_num(88, 4, sec, 5); sec++; vTaskDelay(pdMS_TO_TICKS(1000)); } }预期效果
第0行:标题文字
第2行:欢迎字符串
第4行:运行时间秒数,每秒+1
六、新手进阶拓展方向
中文显示:使用字模软件生成中文点阵,存入数组即可显示
图片/图标显示:取模工具生成位图数据,逐页写入显存
滚动动画:SSD1306自带硬件滚动命令,无需软件刷新
多设备挂载:同一条I2C总线挂载OLED、传感器、RTC时钟
七、I2C + OLED 高频踩坑汇总
屏幕完全不亮?先查供电是否3.3V、地址是否正确、SCL/SDA是否接反
花屏乱码?降低I2C时钟频率、检查接线接触不良、初始化顺序是否正确
部分区域不显示?坐标计算错误、显存分页越界
通信失败报错?开启内部上拉、共地是否良好、设备地址扫描确认
中文显示乱码?取模方式与驱动写入方式不匹配,需统一行列顺序
下一篇预告(07篇)
ESP-IDF保姆级入门07|DHT11温湿度传感器 + OLED 数据显示,打造第一个完整物联网监测小项目,实现数据采集→屏幕显示→串口打印全链路!