ESP32-S3 的 I²C:从"能连设备"到"工程上用得顺"
在刚开始接触 ESP32-S3 的时候,我对 I²C 的理解其实非常简单:
两根线,能连传感器,能点亮 OLED。
但真正开始做多外设、多任务的项目后,我才意识到:
I²C 并不是一个"接口",而是一条"共享总线资源"。
如果你只是"会用",问题不大;
但如果你想把系统跑稳,I²C 的设计逻辑一定要想清楚。
一、先从 ESP32-S3 的视角认识 I²C
I²C(Inter-Integrated Circuit)是一种:
- 串行
- 同步
- 半双工
- 支持多设备挂载
的通信协议。
在 ESP32-S3 上,有 2 个 I²C 控制器(Port),每一个控制器:
- 既可以作为 主机
- 也可以作为 从机
但在绝大多数实际项目中:
ESP32-S3 几乎永远是 I²C 主机
因为:
- 传感器是从机
- OLED 是从机
- EEPROM 也是从机
二、I²C 的主从模型,在工程里意味着什么?
主设备(Master)
- 产生 SCL 时钟
- 发起通信
- 指定目标设备地址
- 决定"什么时候读 / 什么时候写"
从设备(Slave)
- 被动响应
- 根据地址应答
- 按协议返回数据
这意味着一件很关键的事情:
I²C 总线的节奏,永远由主机掌控
也正因为如此,ESP32-S3 的 I²C 驱动设计,本质上是围绕"主机"展开的。
三、ESP-IDF 中的 I²C:不是"接口",而是"总线 + 设备"
ESP-IDF 并没有把 I²C 设计成"初始化一次就完事"的接口,而是明确拆成了两层:
- I²C Bus(总线)
- I²C Device(设备)
这点非常重要,因为它直接影响你后面的系统结构。
在 ESP-IDF 中,I²C 主机的使用流程只有三步:
- 初始化 I²C 总线
- 把设备挂到总线上
- 通过设备句柄进行读写

四、初始化 I²C 总线:你是在"创建一条公共资源"
使用 i2c_new_master_bus() 时,本质上是在做一件事:
创建一条可被多个设备共享的通信总线
c
i2c_master_bus_config_t i2c_bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = -1,
.scl_io_num = SCL_IO_PIN,
.sda_io_num = SDA_IO_PIN,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t bus_handle;
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &bus_handle));
i2c_new_master_bus
c
esp_err_t i2c_new_master_bus(const i2c_master_bus_config_t *config, i2c_master_bus_handle_t *bus_handle);
参数说明
config :指向 i2c_master_bus_config_t 结构体的指针,包含 I2C 总线的配置参数。
bus_handle:用于存储新创建的 I2C 总线句柄的指针。成功调用后,该句柄可用于后续的 I2C 操作。
返回值
ESP_OK: 成功创建并初始化 I2C 总线。
这里有两个非常容易被忽略的点:
- SCL / SDA 是整条总线的资源
- 上拉电阻是否存在,直接决定总线是否稳定
五、添加 I²C 设备:设备不是"初始化",而是"注册"
当你调用 i2c_master_bus_add_device() 时:
-
并没有发生任何通信
-
只是告诉驱动:
这条总线上,有一个地址为 XX 的设备
c
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = DEVICE_ADDR,
.scl_speed_hz = 400000,
};
i2c_master_dev_handle_t dev_handle;
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &dev_cfg, &dev_handle));
i2c_master_bus_add_device
c
esp_err_t i2c_master_bus_add_device(i2c_master_bus_handle_t bus_handle, const i2c_device_config_t *dev_cfg, i2c_master_dev_handle_t *dev_handle);
参数说明
bus_handle:已创建的 I2C 总线句柄,由 i2c_new_master_bus 创建并初始化。
dev_cfg:指向 i2c_device_config_t 结构体的指针,包含要添加的 I2C 设备的配置参数。
dev_handle:用于存储新创建的 I2C 设备句柄的指针。成功调用后,该句柄可用于后续与该设备的通信操作。
返回值
ESP_OK: 成功添加 I2C 设备。
这一层设计,直接带来一个好处:
一个总线,可以非常干净地管理多个 I²C 外设
六、I²C 的三种"标准事务",你一定会全部用到
1️⃣ 主机写(Transmit)
最典型的场景:
- OLED 写命令
- 传感器下发测量指令
成功安装 I2C 主机总线之后,可以通过调用i2c_master_transmit来向从机设备写入数据。
c
// 要发送的数据
uint8_t data_to_send[] = {0x01, 0x02, 0x03, 0x04};
// 发送数据
esp_err_t ret = i2c_master_transmit(dev_handle, data_to_send, sizeof(data_to_send), pdMS_TO_TICKS(1000));
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Data transmitted successfully");
} else {
ESP_LOGE(TAG, "Failed to transmit data: %d", ret);
}
i2c_master_transmit
c
esp_err_t i2c_master_transmit(i2c_master_dev_handle_t dev_handle, const uint8_t *data, size_t length, TickType_t ticks_to_wait);
参数说明
dev_handle (i2c_master_dev_handle_t):I2C 设备句柄,该句柄是通过 i2c_master_bus_add_device 函数创建的。
data (const uint8_t *):指向要发送的数据缓冲区的指针。该缓冲区包含要通过 I2C 总线发送的数据字节。
length (size_t):要发送的数据长度(以字节为单位)。指定发送多少个字节的数据。
ticks_to_wait (TickType_t):等待队列可用的时间,单位为 FreeRTOS 的滴答数。如果设置为 portMAX_DELAY,则任务将无限期等待,直到有资源可用;如果设置为 0,则不会等待,直接返回错误。
返回值
ESP_OK: 成功发送数据。
ESP_ERR_TIMEOUT: 在指定时间内无法获取 I2C 总线访问权限或队列满。

2️⃣ 主机读(Receive)
典型场景:
- 读取传感器测量值
c
// 准备接收缓冲区
uint8_t data_received[10] = {0}; // 假设最多接收 10 字节数据
// 接收数据
esp_err_t ret = i2c_master_receive(dev_handle, data_received, sizeof(data_received), pdMS_TO_TICKS(1000));
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Data received successfully: ");
for (int i = 0; i < sizeof(data_received); i++) {
printf("0x%02X ", data_received[i]);
}
printf("\n");
} else {
ESP_LOGE(TAG, "Failed to receive data: %d", ret);
}
i2c_master_receive
c
esp_err_t i2c_master_receive(i2c_master_dev_handle_t dev_handle, uint8_t *data, size_t length, TickType_t ticks_to_wait);
参数说明
dev_handle (i2c_master_dev_handle_t):I2C 设备句柄,表示要通信的具体 I2C 从设备。
data (uint8_t *):指向接收缓冲区的指针。该缓冲区用于存储从 I2C 从设备接收到的数据字节。
length (size_t):要接收的数据长度(以字节为单位)。指定接收多少个字节的数据。
ticks_to_wait (TickType_t):等待队列可用的时间,单位为 FreeRTOS 的滴答数。如果设置为 portMAX_DELAY,则任务将无限期等待,直到有资源可用;如果设置为 0,则不会等待,直接返回错误。
返回值
ESP_OK: 成功接收数据。
ESP_ERR_TIMEOUT: 在指定时间内无法获取 I2C 总线访问权限或队列满。

3️⃣ 写后读(Transmit + Receive)
这是 I²C 里最容易被忽略、但最常用的一种事务。
典型场景:
- EEPROM 读寄存器
- 传感器读寄存器
从一些 I2C 设备中读取数据之前需要进行写入配置,可通过 i2c_master_transmit_receive接口进行配置。
c
// 这里可以继续添加与 I2C 设备通信的代码
// 要发送的数据(例如命令或寄存器地址)
uint8_t command[] = {0x01}; // 假设我们要读取寄存器 0x01 的内容
// 准备接收缓冲区
uint8_t data_received[10] = {0}; // 假设最多接收 10 字节数据
// 发送命令并接收数据
esp_err_t ret = i2c_master_transmit_receive(dev_handle, command, sizeof(command), data_received, sizeof(data_received), pdMS_TO_TICKS(1000));
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Data received successfully: ");
for (int i = 0; i < sizeof(data_received); i++) {
printf("0x%02X ", data_received[i]);
}
printf("\n");
} else {
ESP_LOGE(TAG, "Failed to transmit and receive data: %d", ret);
}
i2c_master_transmit_receive
c
esp_err_t i2c_master_transmit_receive(i2c_master_dev_handle_t dev_handle, const uint8_t *tx_data, size_t tx_length, uint8_t *rx_data, size_t rx_length, TickType_t ticks_to_wait);
参数说明
dev_handle (i2c_master_dev_handle_t):
I2C 设备句柄,表示要通信的具体 I2C 从设备。该句柄是通过 i2c_master_bus_add_device 函数创建的。
tx_data (const uint8_t *):
指向要发送的数据缓冲区的指针。该缓冲区包含要通过 I2C 总线发送的数据字节。
tx_length (size_t):
要发送的数据长度(以字节为单位)。指定发送多少个字节的数据。
rx_data (uint8_t *):
指向接收缓冲区的指针。该缓冲区用于存储从 I2C 从设备接收到的数据字节。
rx_length (size_t):
要接收的数据长度(以字节为单位)。指定接收多少个字节的数据。
ticks_to_wait (TickType_t):
等待队列可用的时间,单位为 FreeRTOS 的滴答数。如果设置为 portMAX_DELAY,则任务将无限期等待,直到有资源可用;如果设置为 0,则不会等待,直接返回错误。
返回值
ESP_OK: 成功发送和接收数据。
中间没有 STOP,这一点对很多器件非常关键。

七、I²C 探测:项目早期非常有用的工具
在调试阶段,我几乎一定会先做一件事:
扫总线,看设备在不在
c
// 探测设备
esp_err_t ret = i2c_master_probe(bus_handle, DEVICE_ADDR, 1000);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Device at address 0x%02X found and responsive", DEVICE_ADDR);
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to find device at address 0x%02X", DEVICE_ADDR);
} else if (ret == ESP_ERR_TIMEOUT) {
ESP_LOGE(TAG, "Timeout while probing device at address 0x%02X", DEVICE_ADDR);
} else {
ESP_LOGE(TAG, "Failed to probe device at address 0x%02X: %d", DEVICE_ADDR, ret);
}
i2c_master_probe
c
该函数用于探测指定地址的 I2C 设备是否存在。如果地址正确并且接收到 ACK(应答)
esp_err_t i2c_master_probe(i2c_master_bus_handle_t bus_handle, uint16_t address, int xfer_timeout_ms)
参数
bus_handle (i2c_master_bus_handle_t):I2C 主设备总线句柄,由 i2c_new_master_bus 创建。
address (uint16_t):要探测的 I2C 设备地址(7 位或 10 位地址)。
xfer_timeout_ms (int):等待超时时间,单位为毫秒。注意:-1 表示无限期等待(不推荐在此函数中使用)。
返回值
ESP_OK: I2C 设备探测成功。
ESP_ERR_NOT_FOUND: I2C 探测失败,未找到指定地址的设备。
如果 probe 都不通过:
- 接线
- 上拉
- 地址
一定有问题。

八、为什么 I²C 特别适合 OLED?
以常见的 SSD1306 OLED 为例:
- 数据量不大
- 刷新频率不高
- 初始化命令多
这正好命中 I²C 的优势区间:
- 省 GPIO
- 协议简单
- 多设备兼容
OLED 只占用 两根线:
| OLED | ESP32-S3 |
|---|---|
| SDA | GPIO 21 |
| SCL | GPIO 20 |
| VCC | 3.3V |
| GND | GND |

九、OLED 点亮实例(工程级思路)
在工程里,我通常会这样拆:
- I²C 总线初始化
- OLED 设备注册
- OLED 驱动内部封装 I²C 写操作
- 上层只关心"显示什么"
oled驱动代码结构如下:
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "oled.h"
#include "driver/i2c_master.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// OLED 配置参数
#define OLED_I2C_ADDRESS 0x3C // SSD1306 的 7 位 I2C 地址(0x78 是 8 位写地址,7 位地址是 0x3C)
#define OLED_I2C_SDA_PIN 21 // SDA 引脚
#define OLED_I2C_SCL_PIN 20 // SCL 引脚
#define OLED_I2C_FREQ 100000 // I2C 频率 100kHz(降低频率以提高兼容性)
#define OLED_WIDTH 128 // OLED 宽度(像素)
#define OLED_HEIGHT 64 // OLED 高度(像素)
#define OLED_PAGES (OLED_HEIGHT / 8) // OLED 页数(每页 8 行)
// SSD1306 命令定义
#define SSD1306_SETCONTRAST 0x81
#define SSD1306_DISPLAYALLON_RESUME 0xA4
#define SSD1306_DISPLAYALLON 0xA5
#define SSD1306_NORMALDISPLAY 0xA6
#define SSD1306_INVERTDISPLAY 0xA7
#define SSD1306_DISPLAYOFF 0xAE
#define SSD1306_DISPLAYON 0xAF
#define SSD1306_SETDISPLAYOFFSET 0xD3
#define SSD1306_SETCOMPINS 0xDA
#define SSD1306_SETVCOMDETECT 0xDB
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5
#define SSD1306_SETPRECHARGE 0xD9
#define SSD1306_SETMULTIPLEX 0xA8
#define SSD1306_SETLOWCOLUMN 0x00
#define SSD1306_SETHIGHCOLUMN 0x10
#define SSD1306_SETSTARTLINE 0x40
#define SSD1306_MEMORYMODE 0x20
#define SSD1306_COLUMNADDR 0x21
#define SSD1306_PAGEADDR 0x22
#define SSD1306_COMSCANINC 0xC0
#define SSD1306_COMSCANDEC 0xC8
#define SSD1306_SEGREMAP 0xA0
#define SSD1306_CHARGEPUMP 0x8D
#define SSD1306_EXTERNALVCC 0x1
#define SSD1306_SWITCHCAPVCC 0x2
// I2C 控制字节
#define OLED_CONTROL_BYTE_CMD_SINGLE 0x80 // 单字节命令
#define OLED_CONTROL_BYTE_CMD_STREAM 0x00 // 命令流
#define OLED_CONTROL_BYTE_DATA_STREAM 0x40 // 数据流
// I2C 句柄
static i2c_master_bus_handle_t bus_handle = NULL;
static i2c_master_dev_handle_t dev_handle = NULL;
// 显示缓冲区(128x64 = 1024 字节)
static uint8_t oled_buffer[OLED_WIDTH * OLED_PAGES];
// 8x8 字体(ASCII 32-127)
static const uint8_t font_8x8[][8] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 空格
{0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // !
{0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // "
{0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // #
{0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // $
{0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // %
{0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // &
{0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // '
{0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // (
{0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // )
{0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // *
{0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // +
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x06, 0x00}, // ,
{0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // -
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // .
{0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // /
{0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x63, 0x3E}, // 0
{0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F}, // 1
{0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x33, 0x3F}, // 2
{0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x33, 0x1E}, // 3
{0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x30, 0x78}, // 4
{0x3F, 0x03, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E}, // 5
{0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x33, 0x1E}, // 6
{0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x0C}, // 7
{0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x33, 0x1E}, // 8
{0x1E, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E}, // 9
{0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // :
{0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x06, 0x00}, // ;
{0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // <
{0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // =
{0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // >
{0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // ?
{0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // @
{0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // A
{0x1F, 0x36, 0x36, 0x1E, 0x36, 0x36, 0x1F, 0x00}, // B
{0x1E, 0x33, 0x03, 0x03, 0x03, 0x33, 0x1E, 0x00}, // C
{0x1F, 0x36, 0x36, 0x36, 0x36, 0x36, 0x1F, 0x00}, // D
{0x3F, 0x06, 0x06, 0x1E, 0x06, 0x06, 0x3F, 0x00}, // E
{0x3F, 0x06, 0x06, 0x1E, 0x06, 0x06, 0x06, 0x00}, // F
{0x1E, 0x33, 0x03, 0x03, 0x73, 0x33, 0x7E, 0x00}, // G
{0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // H
{0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // I
{0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // J
{0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // K
{0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x3F, 0x00}, // L
{0x63, 0x77, 0x7F, 0x6B, 0x63, 0x63, 0x63, 0x00}, // M
{0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // N
{0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // O
{0x1F, 0x33, 0x33, 0x1F, 0x03, 0x03, 0x03, 0x00}, // P
{0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // Q
{0x1F, 0x33, 0x33, 0x1F, 0x1B, 0x33, 0x33, 0x00}, // R
{0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // S
{0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // T
{0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U
{0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // V
{0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // W
{0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // X
{0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // Y
{0x3F, 0x31, 0x18, 0x0C, 0x06, 0x23, 0x3F, 0x00}, // Z
{0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // [
{0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // backslash
{0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // ]
{0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // ^
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // _
{0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // `
{0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // a
{0x07, 0x06, 0x06, 0x1E, 0x36, 0x36, 0x1E, 0x00}, // b
{0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // c
{0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00}, // d
{0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00}, // e
{0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00}, // f
{0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // g
{0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // h
{0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // i
{0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // j
{0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // k
{0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // l
{0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // m
{0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // n
{0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // o
{0x00, 0x00, 0x1F, 0x33, 0x33, 0x1F, 0x03, 0x03}, // p
{0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x30}, // q
{0x00, 0x00, 0x1B, 0x36, 0x06, 0x06, 0x0F, 0x00}, // r
{0x00, 0x00, 0x1E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // s
{0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // t
{0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // u
{0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // v
{0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // w
{0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // x
{0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // y
{0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // z
{0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // {
{0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // |
{0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // }
{0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // ~
};
/**
* @brief 发送命令到 OLED
* @param cmd 命令字节
*/
static void oled_write_cmd(uint8_t cmd)
{
if (dev_handle == NULL) {
printf("OLED device handle is NULL\n");
return;
}
uint8_t data[2] = {OLED_CONTROL_BYTE_CMD_SINGLE, cmd};
esp_err_t ret = i2c_master_transmit(dev_handle, data, 2, pdMS_TO_TICKS(100));
if (ret != ESP_OK) {
printf("OLED write cmd error: %s (cmd=0x%02X)\n", esp_err_to_name(ret), cmd);
}
}
/**
* @brief 发送命令流到 OLED
* @param cmds 命令数组
* @param len 命令数量
*/
static void oled_write_cmd_stream(const uint8_t *cmds, size_t len)
{
uint8_t *data = (uint8_t *)malloc(len + 1);
if (data == NULL) {
return;
}
data[0] = OLED_CONTROL_BYTE_CMD_STREAM;
memcpy(data + 1, cmds, len);
i2c_master_transmit(dev_handle, data, len + 1, -1);
free(data);
}
/**
* @brief 初始化 I2C 总线
* @return ESP_OK 成功,其他值表示失败
*/
static esp_err_t i2c_init(void)
{
if (bus_handle != NULL && dev_handle != NULL) {
printf("I2C bus already initialized\n");
return ESP_OK;
}
// 如果总线已存在但设备不存在,先清理
if (bus_handle != NULL && dev_handle == NULL) {
i2c_del_master_bus(bus_handle);
bus_handle = NULL;
}
i2c_master_bus_config_t i2c_bus_config = {
.i2c_port = I2C_NUM_0,
.sda_io_num = OLED_I2C_SDA_PIN,
.scl_io_num = OLED_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags = {
.enable_internal_pullup = true,
},
};
esp_err_t ret = i2c_new_master_bus(&i2c_bus_config, &bus_handle);
if (ret != ESP_OK) {
printf("Failed to initialize I2C bus: %s\n", esp_err_to_name(ret));
return ret;
}
printf("I2C bus initialized: SDA=%d, SCL=%d\n", OLED_I2C_SDA_PIN, OLED_I2C_SCL_PIN);
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = OLED_I2C_ADDRESS,
.scl_speed_hz = OLED_I2C_FREQ,
};
ret = i2c_master_bus_add_device(bus_handle, &dev_cfg, &dev_handle);
if (ret != ESP_OK) {
printf("Failed to add I2C device (addr=0x%02X): %s\n", OLED_I2C_ADDRESS, esp_err_to_name(ret));
i2c_del_master_bus(bus_handle);
bus_handle = NULL;
return ret;
}
printf("I2C device added: address=0x%02X, speed=%d Hz\n", OLED_I2C_ADDRESS, OLED_I2C_FREQ);
// 等待总线稳定
vTaskDelay(pdMS_TO_TICKS(50));
// 添加i2c_master_probe,确保设备连接
ret = i2c_master_probe(bus_handle, OLED_I2C_ADDRESS, pdMS_TO_TICKS(100));
if (ret != ESP_OK) {
printf("I2C device probe failed (addr=0x%02X): %s\n", OLED_I2C_ADDRESS, esp_err_to_name(ret));
i2c_master_bus_rm_device(dev_handle);
i2c_del_master_bus(bus_handle);
dev_handle = NULL;
bus_handle = NULL;
return ret;
}
return ESP_OK;
}
/**
* @brief 初始化 OLED 显示屏
*/
void oled_init(void)
{
// 初始化 I2C
esp_err_t ret = i2c_init();
if (ret != ESP_OK) {
printf("Failed to initialize I2C, OLED init aborted\n");
return;
}
if (dev_handle == NULL) {
printf("I2C device handle is NULL, OLED init aborted\n");
return;
}
// 等待 OLED 准备就绪
vTaskDelay(pdMS_TO_TICKS(100));
// SSD1306 初始化序列(每个命令后添加小延时)
oled_write_cmd(SSD1306_DISPLAYOFF); // 关闭显示
vTaskDelay(pdMS_TO_TICKS(20));
oled_write_cmd(SSD1306_SETDISPLAYCLOCKDIV); // 设置时钟分频
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(0x80); // 建议值
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_SETMULTIPLEX); // 设置多路复用
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(OLED_HEIGHT - 1); // 64-1 = 63
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_SETDISPLAYOFFSET); // 设置显示偏移
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(0x00); // 无偏移
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_SETSTARTLINE | 0x0); // 设置起始行
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_CHARGEPUMP); // 电荷泵设置
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(0x14); // 启用内部 VCC
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_MEMORYMODE); // 内存模式
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(0x00); // 水平寻址模式
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_SEGREMAP | 0x1); // 段重映射(水平翻转,修复字符串顺序)
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_COMSCANDEC); // COM 扫描方向(垂直翻转)
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_SETCOMPINS); // 设置 COM 引脚配置
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(0x12); // 128x64 配置
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_SETCONTRAST); // 设置对比度
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(0xCF); // 对比度值
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_SETPRECHARGE); // 预充电
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(0xF1); // 预充电值
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_SETVCOMDETECT); // VCOM 检测
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(0x40); // VCOM 值
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_DISPLAYALLON_RESUME); // 全部显示开启(不闪烁)
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_NORMALDISPLAY); // 正常显示(非反色)
vTaskDelay(pdMS_TO_TICKS(1));
oled_write_cmd(SSD1306_DISPLAYON); // 开启显示
// 清空显示缓冲区
memset(oled_buffer, 0, sizeof(oled_buffer));
vTaskDelay(pdMS_TO_TICKS(100));
printf("OLED initialized successfully\n");
}
/**
* @brief 清空显示缓冲区
*/
void oled_clear(void)
{
memset(oled_buffer, 0, sizeof(oled_buffer));
}
/**
* @brief 在指定位置绘制一个字符
* @param x 列位置 (0-127)
* @param y 行位置 (0-7, 每行8像素高)
* @param c 字符
*/
static void oled_draw_char(int x, int y, char c)
{
if (x < 0 || x >= OLED_WIDTH - 7 || y < 0 || y >= OLED_PAGES) {
return;
}
// 获取字符的字体数据(ASCII 32-127)
int char_index = c - 32;
if (char_index < 0 || char_index >= 96) {
char_index = 0; // 空格
}
// 将字符绘制到缓冲区
// font_8x8[char_index][row] 是第row行的8位数据(水平方向)
// 在SSD1306中,每个字节的8位代表垂直方向的8个像素(一列)
// 需要将字体数据的每一列提取出来,写入到对应的列位置
for (int col = 0; col < 8; col++) {
if (x + col < OLED_WIDTH) {
uint8_t column_data = 0;
// 从字体数据的每一行中提取第col列的位
for (int row = 0; row < 8; row++) {
// font_8x8[char_index][row] 的第col位(从右边数,LSB是第0位,修复水平镜像)
if (font_8x8[char_index][row] & (1 << col)) {
// 设置column_data的第row位
column_data |= (1 << row);
}
}
oled_buffer[y * OLED_WIDTH + x + col] = column_data;
}
}
}
/**
* @brief 在指定位置绘制字符串
* @param x 列位置 (0-127)
* @param y 行位置 (0-7)
* @param str 字符串
*/
void oled_draw_string(int x, int y, const char *str)
{
int pos_x = x;
while (*str && pos_x < OLED_WIDTH - 7) {
oled_draw_char(pos_x, y, *str);
pos_x += 8; // 字符宽度为 8 像素
str++;
}
}
/**
* @brief 更新 OLED 显示(将缓冲区内容发送到 OLED)
*/
void oled_update(void)
{
if (dev_handle == NULL) {
printf("OLED device handle is NULL, cannot update display\n");
return;
}
// 设置列地址范围
oled_write_cmd(SSD1306_COLUMNADDR);
oled_write_cmd(0); // 起始列
oled_write_cmd(OLED_WIDTH - 1); // 结束列
// 设置页地址范围
oled_write_cmd(SSD1306_PAGEADDR);
oled_write_cmd(0); // 起始页
oled_write_cmd(OLED_PAGES - 1); // 结束页
// 分块发送显示数据,每次发送 16 字节,避免 I2C 超时
#define CHUNK_SIZE 16
uint8_t data_buf[CHUNK_SIZE + 1]; // 使用栈缓冲区,避免动态分配
for (int page = 0; page < OLED_PAGES; page++) {
uint8_t *page_data = &oled_buffer[page * OLED_WIDTH];
// 将每页数据分成多个块传输
for (int offset = 0; offset < OLED_WIDTH; offset += CHUNK_SIZE) {
int chunk_len = (OLED_WIDTH - offset < CHUNK_SIZE) ? (OLED_WIDTH - offset) : CHUNK_SIZE;
data_buf[0] = OLED_CONTROL_BYTE_DATA_STREAM;
memcpy(data_buf + 1, page_data + offset, chunk_len);
esp_err_t ret = i2c_master_transmit(dev_handle, data_buf, chunk_len + 1, pdMS_TO_TICKS(200));
if (ret != ESP_OK) {
printf("OLED update page %d chunk %d error: %s\n", page, offset / CHUNK_SIZE, esp_err_to_name(ret));
return;
}
// 小延时避免过快传输
vTaskDelay(pdMS_TO_TICKS(1));
}
}
}
/**
* @brief 清屏函数,清空缓冲区并立即更新显示
*/
void oled_clear_display(void)
{
// 清空显示缓冲区
oled_clear();
// 立即更新显示,将清空后的缓冲区发送到 OLED
oled_update();
}
I²C 在这里只是通信手段,而不是业务逻辑的一部分。
而在业务层,我们只需要调用我们的驱动函数:
main.c
c
#include <stdio.h> // 引入标准输入输出库,用于使用 printf 函数
#include "freertos/FreeRTOS.h" // 引入 FreeRTOS 的核心头文件,提供 FreeRTOS 的基本功能
#include "freertos/task.h" // 引入 FreeRTOS 的任务管理头文件,提供任务相关的函数(如 vTaskDelay)
#include "sdkconfig.h"
#include "esp_err.h"
#include "oled.h"
void app_main(void) // ESP-IDF 程序的入口函数,类似于 main 函数
{
oled_init();
oled_clear();
oled_draw_string(0, 0, "Hello ESP32-S3");
oled_update();
while (1)
{
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
代码运行效果如下:

官方文档参考
ESP-IDF I²C(ESP32-S3)
https://docs.espressif.com/projects/esp-idf/zh_CN/v5.4/esp32s3/api-reference/peripherals/i2c.html