Skip to content

第五章:I2C 与 SPI 通信

5.1 I2C 协议基础

I2C 特性

I2C(Inter-Integrated Circuit):
  引脚:SDA(数据)+ SCL(时钟),仅 2 根线
  拓扑:一主多从,每个从设备有唯一 7 位地址(0x00~0x7F)
  速度:标准 100kHz / 快速 400kHz / 高速 1MHz+
  特点:需要上拉电阻(通常 4.7kΩ)

常见 I2C 设备:
  0x3C / 0x3D  → SSD1306 OLED 显示屏
  0x68 / 0x69  → MPU6050 六轴传感器
  0x76 / 0x77  → BMP280 气压传感器
  0x48~0x4B    → ADS1115 ADC
  0x57         → AT24C EEPROM

I2C 时序

START  SCL ─────┐  ┌─┐  ┌─┐  ┌─┐  ┌─┐  ┌─┐  ┌─┐  ┌─┐  ┌─┐  STOP
                └──┘  └──┘  └──┘  └──┘  └──┘  └──┘  └──┘  └──┘
       SDA ──┐                                              ┌────
             └─[A6][A5][A4][A3][A2][A1][A0][R/W][ACK]─────┘
              ←────────── 地址帧(9位)──────────────→

5.2 I2C 驱动(ESP-IDF v5.x 新 API)

主机初始化

c
#include "driver/i2c_master.h"
#include "esp_log.h"

#define I2C_SCL_PIN     GPIO_NUM_22
#define I2C_SDA_PIN     GPIO_NUM_21
#define I2C_FREQ_HZ     400000      // 400kHz 快速模式

static const char *TAG = "I2C";
static i2c_master_bus_handle_t i2c_bus;

void i2c_init(void)
{
    i2c_master_bus_config_t bus_cfg = {
        .i2c_port        = I2C_NUM_0,
        .sda_io_num      = I2C_SDA_PIN,
        .scl_io_num      = I2C_SCL_PIN,
        .clk_source      = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt = 7,
        .flags.enable_internal_pullup = true,  // 使用内部上拉
    };
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &i2c_bus));
    ESP_LOGI(TAG, "I2C 总线初始化完成");
}

添加从设备

c
// 每个 I2C 从设备需要单独添加
i2c_device_config_t dev_cfg = {
    .dev_addr_length = I2C_ADDR_BIT_LEN_7,
    .device_address  = 0x68,    // MPU6050 地址
    .scl_speed_hz    = 400000,
};
i2c_master_dev_handle_t mpu6050_handle;
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus, &dev_cfg, &mpu6050_handle));

读写操作

c
// 写寄存器
esp_err_t i2c_write_reg(i2c_master_dev_handle_t dev,
                         uint8_t reg, uint8_t val)
{
    uint8_t buf[2] = {reg, val};
    return i2c_master_transmit(dev, buf, 2, pdMS_TO_TICKS(100));
}

// 读寄存器(写地址,然后读数据)
esp_err_t i2c_read_reg(i2c_master_dev_handle_t dev,
                        uint8_t reg, uint8_t *data, size_t len)
{
    // 先写寄存器地址,再读数据
    return i2c_master_transmit_receive(dev, &reg, 1,
                                       data, len,
                                       pdMS_TO_TICKS(100));
}

5.3 实战:MPU6050 六轴传感器

MPU6050 关键寄存器

0x6B  PWR_MGMT_1    电源管理(写 0x00 唤醒)
0x75  WHO_AM_I      设备 ID(读到 0x68 表示正常)
0x3B  ACCEL_XOUT_H  加速度 X 高字节(共 14 字节:6轴+温度)
0x43  GYRO_XOUT_H   陀螺仪 X 高字节

完整驱动示例

c
#include "driver/i2c_master.h"
#include "esp_log.h"
#include <math.h>

#define MPU6050_ADDR    0x68
#define REG_PWR_MGMT    0x6B
#define REG_WHO_AM_I    0x75
#define REG_ACCEL_OUT   0x3B

static const char *TAG = "MPU6050";
static i2c_master_dev_handle_t mpu_dev;

typedef struct {
    float ax, ay, az;   // 加速度 (g)
    float gx, gy, gz;   // 角速度 (°/s)
    float temp;          // 温度 (°C)
} mpu6050_data_t;

esp_err_t mpu6050_init(i2c_master_bus_handle_t bus)
{
    i2c_device_config_t cfg = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,
        .device_address  = MPU6050_ADDR,
        .scl_speed_hz    = 400000,
    };
    ESP_ERROR_CHECK(i2c_master_bus_add_device(bus, &cfg, &mpu_dev));

    // 检查设备 ID
    uint8_t who_am_i;
    i2c_read_reg(mpu_dev, REG_WHO_AM_I, &who_am_i, 1);
    if (who_am_i != 0x68) {
        ESP_LOGE(TAG, "MPU6050 未找到,WHO_AM_I=0x%02X", who_am_i);
        return ESP_ERR_NOT_FOUND;
    }

    // 唤醒(清除睡眠位)
    i2c_write_reg(mpu_dev, REG_PWR_MGMT, 0x00);
    ESP_LOGI(TAG, "MPU6050 初始化成功");
    return ESP_OK;
}

esp_err_t mpu6050_read(mpu6050_data_t *data)
{
    uint8_t raw[14];
    esp_err_t ret = i2c_read_reg(mpu_dev, REG_ACCEL_OUT, raw, 14);
    if (ret != ESP_OK) return ret;

    // 合并高低字节(大端序)
    int16_t ax_raw = (int16_t)(raw[0]  << 8 | raw[1]);
    int16_t ay_raw = (int16_t)(raw[2]  << 8 | raw[3]);
    int16_t az_raw = (int16_t)(raw[4]  << 8 | raw[5]);
    int16_t t_raw  = (int16_t)(raw[6]  << 8 | raw[7]);
    int16_t gx_raw = (int16_t)(raw[8]  << 8 | raw[9]);
    int16_t gy_raw = (int16_t)(raw[10] << 8 | raw[11]);
    int16_t gz_raw = (int16_t)(raw[12] << 8 | raw[13]);

    // 转换为物理量(±2g 量程,±250°/s 量程)
    data->ax   = ax_raw / 16384.0f;
    data->ay   = ay_raw / 16384.0f;
    data->az   = az_raw / 16384.0f;
    data->gx   = gx_raw / 131.0f;
    data->gy   = gy_raw / 131.0f;
    data->gz   = gz_raw / 131.0f;
    data->temp = t_raw / 340.0f + 36.53f;

    return ESP_OK;
}

void app_main(void)
{
    i2c_init();
    mpu6050_init(i2c_bus);

    mpu6050_data_t data;
    while (1) {
        if (mpu6050_read(&data) == ESP_OK) {
            ESP_LOGI(TAG, "加速度: X=%.2fg Y=%.2fg Z=%.2fg",
                     data.ax, data.ay, data.az);
            ESP_LOGI(TAG, "角速度: X=%.1f°/s Y=%.1f°/s Z=%.1f°/s",
                     data.gx, data.gy, data.gz);
            ESP_LOGI(TAG, "温度: %.1f°C", data.temp);
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

I2C 扫描工具(调试必备)

c
// 扫描总线上所有 I2C 设备
void i2c_scan(i2c_master_bus_handle_t bus)
{
    ESP_LOGI(TAG, "开始 I2C 扫描...");
    int found = 0;

    for (uint8_t addr = 0x08; addr < 0x78; addr++) {
        i2c_device_config_t cfg = {
            .dev_addr_length = I2C_ADDR_BIT_LEN_7,
            .device_address  = addr,
            .scl_speed_hz    = 100000,
        };
        i2c_master_dev_handle_t dev;
        if (i2c_master_bus_add_device(bus, &cfg, &dev) == ESP_OK) {
            // 尝试发送一个字节
            uint8_t dummy = 0;
            if (i2c_master_transmit(dev, &dummy, 1, pdMS_TO_TICKS(10)) == ESP_OK) {
                ESP_LOGI(TAG, "  发现设备: 0x%02X", addr);
                found++;
            }
            i2c_master_bus_rm_device(dev);
        }
    }
    ESP_LOGI(TAG, "扫描完成,共发现 %d 个设备", found);
}

5.4 SPI 协议基础

SPI 特性

SPI(Serial Peripheral Interface):
  引脚:MOSI + MISO + SCLK + CS(每设备一个 CS)
  拓扑:一主多从,CS 低电平选中设备
  速度:可达 80MHz+(比 I2C 快得多)
  特点:全双工,无地址概念,靠 CS 区分设备

常见 SPI 设备:
  SSD1306(SPI版)  → OLED 显示屏
  ST7789 / ILI9341  → TFT LCD 显示屏
  W25Q128           → SPI Flash
  SD 卡             → 文件存储
  MAX31855          → 热电偶温度传感器

SPI 时序模式

模式  CPOL  CPHA  空闲时钟  采样边沿
  0    0     0    低电平    上升沿
  1    0     1    低电平    下降沿
  2    1     0    高电平    下降沿
  3    1     1    高电平    上升沿

最常用:模式 0 和模式 3

5.5 SPI 驱动

SPI 主机初始化

c
#include "driver/spi_master.h"

#define SPI_MOSI    GPIO_NUM_11
#define SPI_MISO    GPIO_NUM_13
#define SPI_SCLK    GPIO_NUM_12
#define SPI_CS      GPIO_NUM_10

static spi_device_handle_t spi_dev;

void spi_init(void)
{
    // 1. 配置总线
    spi_bus_config_t bus_cfg = {
        .mosi_io_num   = SPI_MOSI,
        .miso_io_num   = SPI_MISO,
        .sclk_io_num   = SPI_SCLK,
        .quadwp_io_num = -1,    // 不用 QSPI
        .quadhd_io_num = -1,
        .max_transfer_sz = 4096,
    };
    ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO));

    // 2. 添加设备
    spi_device_interface_config_t dev_cfg = {
        .clock_speed_hz = 10 * 1000 * 1000,  // 10MHz
        .mode           = 0,                   // SPI 模式 0
        .spics_io_num   = SPI_CS,
        .queue_size     = 7,
        .pre_cb         = NULL,
        .post_cb        = NULL,
    };
    ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &dev_cfg, &spi_dev));
}

SPI 读写

c
// 发送并接收(全双工)
esp_err_t spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len)
{
    spi_transaction_t t = {
        .length    = len * 8,   // 位数
        .tx_buffer = tx,
        .rx_buffer = rx,
    };
    return spi_device_transmit(spi_dev, &t);
}

// 只发送
esp_err_t spi_write(const uint8_t *data, size_t len)
{
    spi_transaction_t t = {
        .length    = len * 8,
        .tx_buffer = data,
        .rx_buffer = NULL,
    };
    return spi_device_transmit(spi_dev, &t);
}

// 写单字节寄存器(常见模式)
esp_err_t spi_write_reg(uint8_t reg, uint8_t val)
{
    uint8_t buf[2] = {reg & 0x7F, val};  // 最高位 0 = 写
    return spi_write(buf, 2);
}

// 读单字节寄存器
esp_err_t spi_read_reg(uint8_t reg, uint8_t *val)
{
    uint8_t tx[2] = {reg | 0x80, 0x00};  // 最高位 1 = 读
    uint8_t rx[2] = {0};
    esp_err_t ret = spi_transfer(tx, rx, 2);
    *val = rx[1];
    return ret;
}

5.6 实战:SSD1306 OLED(I2C)

c
// 使用 ESP-IDF 组件管理器安装驱动
// idf.py add-dependency "espressif/esp_lcd_ssd1306"

#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_ssd1306.h"

#define OLED_SCL    GPIO_NUM_22
#define OLED_SDA    GPIO_NUM_21
#define OLED_ADDR   0x3C
#define OLED_W      128
#define OLED_H      64

esp_lcd_panel_handle_t panel;

void oled_init(void)
{
    // I2C 总线(复用之前初始化的)
    esp_lcd_panel_io_handle_t io;
    esp_lcd_panel_io_i2c_config_t io_cfg = {
        .dev_addr            = OLED_ADDR,
        .control_phase_bytes = 1,
        .dc_bit_offset       = 6,
        .lcd_cmd_bits        = 8,
        .lcd_param_bits      = 8,
    };
    esp_lcd_new_panel_io_i2c(i2c_bus, &io_cfg, &io);

    esp_lcd_panel_dev_config_t panel_cfg = {
        .bits_per_pixel = 1,
        .reset_gpio_num = -1,
    };
    esp_lcd_new_panel_ssd1306(io, &panel_cfg, &panel);
    esp_lcd_panel_reset(panel);
    esp_lcd_panel_init(panel);
    esp_lcd_panel_disp_on_off(panel, true);
}

// 显示文字(需要字体库,这里用简单示例)
void oled_show_text(const char *text)
{
    // 实际项目中使用 LVGL 或 u8g2 库
    ESP_LOGI("OLED", "显示: %s", text);
}

📝 第五章练习题

练习 5-1:I2C 扫描(基础)

目标:掌握 I2C 初始化和调试

任务

  • 初始化 I2C 总线(SCL=GPIO22, SDA=GPIO21)
  • 实现 I2C 总线扫描,打印所有响应的设备地址
  • 格式:发现设备: 0x3C (可能是 SSD1306 OLED)
  • 建立常见地址对照表(至少 10 个设备)

练习 5-2:读取 MPU6050(基础)

目标:I2C 寄存器读写

任务

  • 初始化 MPU6050
  • 每 200ms 读取一次加速度和陀螺仪数据
  • 计算并打印倾斜角度(使用加速度计算 Roll/Pitch)

倾斜角计算

c
float roll  = atan2f(ay, az) * 180.0f / M_PI;
float pitch = atan2f(-ax, sqrtf(ay*ay + az*az)) * 180.0f / M_PI;

练习 5-3:SPI Flash 读写(进阶)

目标:SPI 通信 + W25Q 系列 Flash

任务

  • 初始化 SPI 连接 W25Q128(或 W25Q32)
  • 实现以下操作:
    • 读取 JEDEC ID(命令 0x9F)
    • 读取状态寄存器
    • 擦除扇区(4KB)
    • 写入 256 字节数据
    • 读回并验证数据

W25Q 常用命令

0x9F  JEDEC ID
0x05  读状态寄存器1
0x06  写使能(写操作前必须执行)
0x20  扇区擦除(4KB)
0x02  页编程(256字节)
0x03  读数据

练习 5-4:OLED 显示(进阶)

目标:综合 I2C + 显示驱动

任务:使用 SSD1306 OLED(128×64)显示:

  • 第1行:系统运行时间(秒)
  • 第2行:MPU6050 温度
  • 第3行:Roll/Pitch 角度
  • 第4行:简单进度条(随时间变化)

推荐库:使用 u8g2esp_lcd + LVGL


练习 5-5:I2C 多设备(挑战)

目标:I2C 总线多设备管理

任务:在同一 I2C 总线上同时驱动:

  • SSD1306 OLED(地址 0x3C)
  • MPU6050(地址 0x68)
  • AT24C02 EEPROM(地址 0x50)

实现:

  • 每次开机从 EEPROM 读取上次保存的计数值
  • 计数值每 10 秒 +1 并写入 EEPROM
  • OLED 实时显示计数值和传感器数据
  • 断电重启后计数值不丢失

练习 5-6:思考题

  1. I2C 为什么需要上拉电阻?如果不加上拉电阻会发生什么?内部上拉(45kΩ)和外部上拉(4.7kΩ)在高速通信时有什么区别?

  2. SPI 比 I2C 快得多,为什么还有很多设备选择 I2C?

  3. I2C 地址只有 7 位(128 个地址),如果总线上有两个相同地址的设备怎么办?

  4. SPI 的 DMA 传输(SPI_DMA_CH_AUTO)和 CPU 传输有什么区别?什么时候应该用 DMA?


本章小结

知识点掌握程度自评
I2C 协议原理⬜⬜⬜⬜⬜
I2C 主机驱动配置⬜⬜⬜⬜⬜
I2C 寄存器读写⬜⬜⬜⬜⬜
SPI 协议原理⬜⬜⬜⬜⬜
SPI 主机驱动配置⬜⬜⬜⬜⬜
传感器数据解析⬜⬜⬜⬜⬜

上一章← 串口通信 UART下一章ADC 与 DAC →

个人知识库