ESP32-S3 的 I²C:从“能连设备”到“工程上用得顺”

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 主机的使用流程只有三步:

  1. 初始化 I²C 总线
  2. 把设备挂到总线上
  3. 通过设备句柄进行读写

四、初始化 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 点亮实例(工程级思路)

在工程里,我通常会这样拆:

  1. I²C 总线初始化
  2. OLED 设备注册
  3. OLED 驱动内部封装 I²C 写操作
  4. 上层只关心"显示什么"

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


相关推荐
花间相见2 小时前
【JAVA开发】—— Git常用操作
java·开发语言·git
Swift社区2 小时前
Python 图片验证码库推荐与实践指南
开发语言·python
进击的小头2 小时前
C语言实现设计模式的核心基石
c语言·开发语言·设计模式
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:入门篇-递归初阶
c语言·开发语言·数据结构·c++·算法·visual studio
有谁看见我的剑了?2 小时前
Python更换依赖包下载源
开发语言·python
Java程序员威哥2 小时前
云原生Java应用优化实战:资源限制+JVM参数调优,容器启动快50%
java·开发语言·jvm·python·docker·云原生
多多*2 小时前
程序设计工作室1月21日内部训练赛
java·开发语言·网络·jvm·tcp/ip
AI殉道师2 小时前
从0开发大模型之实现Agent(Bash到SKILL)
开发语言·bash
skywalk81632 小时前
介绍一下 Backtrader量化框架(C# 回测快)
开发语言·c#·量化