Appearance
第五章: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 EEPROMI2C 时序
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, ®, 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 和模式 35.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行:简单进度条(随时间变化)
推荐库:使用 u8g2 或 esp_lcd + LVGL
练习 5-5:I2C 多设备(挑战)
目标:I2C 总线多设备管理
任务:在同一 I2C 总线上同时驱动:
- SSD1306 OLED(地址 0x3C)
- MPU6050(地址 0x68)
- AT24C02 EEPROM(地址 0x50)
实现:
- 每次开机从 EEPROM 读取上次保存的计数值
- 计数值每 10 秒 +1 并写入 EEPROM
- OLED 实时显示计数值和传感器数据
- 断电重启后计数值不丢失
练习 5-6:思考题
I2C 为什么需要上拉电阻?如果不加上拉电阻会发生什么?内部上拉(45kΩ)和外部上拉(4.7kΩ)在高速通信时有什么区别?
SPI 比 I2C 快得多,为什么还有很多设备选择 I2C?
I2C 地址只有 7 位(128 个地址),如果总线上有两个相同地址的设备怎么办?
SPI 的 DMA 传输(
SPI_DMA_CH_AUTO)和 CPU 传输有什么区别?什么时候应该用 DMA?
本章小结
| 知识点 | 掌握程度自评 |
|---|---|
| I2C 协议原理 | ⬜⬜⬜⬜⬜ |
| I2C 主机驱动配置 | ⬜⬜⬜⬜⬜ |
| I2C 寄存器读写 | ⬜⬜⬜⬜⬜ |
| SPI 协议原理 | ⬜⬜⬜⬜⬜ |
| SPI 主机驱动配置 | ⬜⬜⬜⬜⬜ |
| 传感器数据解析 | ⬜⬜⬜⬜⬜ |
上一章:← 串口通信 UART下一章:ADC 与 DAC →