AT32 I2C — 读写 EEPROM(最常用的 I2C 从设备)

AT32F403A I2C 接口读写 AT24C02 EEPROM,三种传输模式:轮询、中断、DMA

I2C — 读写 EEPROM

难度: ⭐⭐(需要了解 I2C 基本概念)
固件库: AT32F403A_407_Firmware_Library V2.2.2
源文件: project/at_start_f403a/examples/i2c/eeprom/src/main.c


这个实验是干嘛的?

EEPROM(Electrically Erasable Programmable Read-Only Memory)是一种掉电不丢失的存储芯片。它不像 Flash 那样需要擦除整块扇区,可以逐字节读写,非常适合存储配置参数、校准数据、用户设置等小量关键数据。

AT24C02 是最经典的 EEPROM 芯片,容量 256 字节,I2C 接口,只要 2 根线就能通信。

本实验做的事情

  1. 准备 3 组不同的测试数据
  2. 分别用轮询、中断、DMA 三种方式写入 EEPROM
  3. 读回数据并比对
  4. 三种方式都成功 → LED 亮

硬件准备

你需要一个 AT24C02 EEPROM 模块(淘宝 2~3 块):

1
2
3
4
5
开发板            AT24C02 模块
  PB10 (SCL) ──→  SCL
  PB11 (SDA) ←──→ SDA
  3.3V       ──→  VCC
  GND        ──→  GND

重要

  • I2C 总线需要上拉电阻(一般 4.7KΩ)
  • 大多数模块已经焊好了上拉电阻,不用你加
  • 如果自己搭电路,SCL 和 SDA 各接一个 4.7K 上拉到 VCC

I2C 基础知识

I2C 只需要 2 根线就能连接多个设备:

信号 全称 说明
SCL Serial Clock 时钟信号,主机产生
SDA Serial Data 数据信号,双向

I2C 的核心概念

1. 主从模式

  • 主机(Master):发起通信,产生时钟
  • 从机(Slave):被动响应,每个从机有唯一地址

2. 7 位地址

  • 每个 I2C 设备有一个 7 位地址(如 AT24C02 的地址是 0x50
  • 一根总线上可以挂多个设备,靠地址区分

3. 读写操作

  • 写操作:主机发送 [从机地址+W] + [数据]
  • 读操作:主机发送 [从机地址+R],然后从机返回数据

I2C vs SPI 对比

特性 I2C SPI
线数 2 根(SCL+SDA) 4 根(SCK+MOSI+MISO+CS)
速度 100K/400K/1M 几十 MHz
多设备 地址区分,同一总线挂多个 每个设备一根 CS 线
全双工 否(半双工)
适用场景 低速传感器、EEPROM 高速 Flash、LCD

完整代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include "at32f403a_407_board.h"
#include "at32f403a_407_clock.h"
#include "eeprom.h"

/* ===== I2C 配置参数 ===== */
#define I2C_TIMEOUT    0xFFFFFFFF
#define I2Cx_SPEED     100000       // 100KHz 标准模式
#define I2Cx_ADDRESS   0xA0         // EEPROM 地址(7位地址左移1位)
#define I2Cx_PORT      I2C2
#define I2Cx_SCL_PIN   GPIO_PINS_10 // PB10 = SCL
#define I2Cx_SDA_PIN   GPIO_PINS_11 // PB11 = SDA
#define BUF_SIZE       12

/* 3 组测试数据 */
uint8_t tx_buf1[BUF_SIZE] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C};
uint8_t tx_buf2[BUF_SIZE] = {0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0};
uint8_t tx_buf3[BUF_SIZE] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC};
uint8_t rx_buf1[BUF_SIZE] = {0};
uint8_t rx_buf2[BUF_SIZE] = {0};
uint8_t rx_buf3[BUF_SIZE] = {0};

i2c_handle_type hi2cx;

/* 缓冲区比对 */
uint32_t buffer_compare(uint8_t* buf1, uint8_t* buf2, uint32_t len)
{
  for(uint32_t i = 0; i < len; i++)
  {
    if(buf1[i] != buf2[i]) return 1;
  }
  return 0;
}

/* 错误处理:LED 快闪 */
void error_handler(uint32_t error_code)
{
  while(1)
  {
    at32_led_toggle(LED2);
    delay_ms(500);
  }
}

/* I2C 底层初始化(GPIO + 中断 + DMA) */
void i2c_lowlevel_init(i2c_handle_type* hi2c)
{
  gpio_init_type gpio_initstructure;

  /* 使能时钟 */
  crm_periph_clock_enable(CRM_I2C2_PERIPH_CLOCK, TRUE);
  crm_periph_clock_enable(CRM_GPIOB_PERIPH_CLOCK, TRUE);

  /* GPIO 配置:开漏输出,复用功能 */
  gpio_initstructure.gpio_out_type = GPIO_OUTPUT_OPEN_DRAIN;
  gpio_initstructure.gpio_pull = GPIO_PULL_NONE;
  gpio_initstructure.gpio_mode = GPIO_MODE_MUX;
  gpio_initstructure.gpio_drive_strength = GPIO_DRIVE_STRENGTH_MODERATE;

  gpio_initstructure.gpio_pins = I2Cx_SCL_PIN;
  gpio_init(GPIOB, &gpio_initstructure);

  gpio_initstructure.gpio_pins = I2Cx_SDA_PIN;
  gpio_init(GPIOB, &gpio_initstructure);

  /* 中断配置 */
  nvic_irq_enable(I2C2_EVT_IRQn, 0, 0);
  nvic_irq_enable(I2C2_ERR_IRQn, 0, 0);
  nvic_irq_enable(DMA1_Channel4_IRQn, 0, 0);
  nvic_irq_enable(DMA1_Channel5_IRQn, 0, 0);

  /* DMA 配置 */
  crm_periph_clock_enable(CRM_DMA1_PERIPH_CLOCK, TRUE);
  hi2c->dma_tx_channel = DMA1_CHANNEL4;
  hi2c->dma_rx_channel = DMA1_CHANNEL5;

  /* I2C 初始化:100KHz,7位地址 */
  i2c_init(hi2c->i2cx, I2C_FSMODE_DUTY_2_1, I2Cx_SPEED);
  i2c_own_address1_set(hi2c->i2cx, I2C_ADDRESS_MODE_7BIT, I2Cx_ADDRESS);
}

int main(void)
{
  i2c_status_type i2c_status;

  nvic_priority_group_config(NVIC_PRIORITY_GROUP_4);
  system_clock_config();
  at32_board_init();

  hi2cx.i2cx = I2Cx_PORT;
  i2c_config(&hi2cx);  // 调用底层初始化

  while(1)
  {
    /* 等待按键按下 */
    while(at32_button_press() != USER_BUTTON) {}

    /* ===== 方式一:轮询模式写入 ===== */
    eeprom_write_buffer(&hi2cx, EE_MODE_POLL, I2C_MEM_ADDR_WIDIH_8,
                        I2Cx_ADDRESS, 0x00, tx_buf1, BUF_SIZE, I2C_TIMEOUT);
    delay_ms(1);  // EEPROM 写入需要时间
    eeprom_read_buffer(&hi2cx, EE_MODE_POLL, I2C_MEM_ADDR_WIDIH_8,
                       I2Cx_ADDRESS, 0x00, rx_buf1, BUF_SIZE, I2C_TIMEOUT);
    delay_ms(5);

    /* ===== 方式二:中断模式写入 ===== */
    eeprom_write_buffer(&hi2cx, EE_MODE_INT, I2C_MEM_ADDR_WIDIH_8,
                        I2Cx_ADDRESS, 0x00, tx_buf2, BUF_SIZE, I2C_TIMEOUT);
    delay_ms(1);
    eeprom_read_buffer(&hi2cx, EE_MODE_INT, I2C_MEM_ADDR_WIDIH_8,
                       I2Cx_ADDRESS, 0x00, rx_buf2, BUF_SIZE, I2C_TIMEOUT);
    delay_ms(5);

    /* ===== 方式三:DMA 模式写入 ===== */
    eeprom_write_buffer(&hi2cx, EE_MODE_DMA, I2C_MEM_ADDR_WIDIH_8,
                        I2Cx_ADDRESS, 0x00, tx_buf3, BUF_SIZE, I2C_TIMEOUT);
    delay_ms(1);
    eeprom_read_buffer(&hi2cx, EE_MODE_DMA, I2C_MEM_ADDR_WIDIH_8,
                       I2Cx_ADDRESS, 0x00, rx_buf3, BUF_SIZE, I2C_TIMEOUT);
    delay_ms(5);

    /* 验证三组数据 */
    if(buffer_compare(tx_buf1, rx_buf1, BUF_SIZE) == 0 &&
       buffer_compare(tx_buf2, rx_buf2, BUF_SIZE) == 0 &&
       buffer_compare(tx_buf3, rx_buf3, BUF_SIZE) == 0)
    {
      at32_led_on(LED3);  // 全部成功
    }
    else
    {
      error_handler(i2c_status);  // 失败,LED 快闪
    }
  }
}

代码分段讲解

1. I2C 地址

1
#define I2Cx_ADDRESS   0xA0

AT24C02 的 7 位地址是 0x50,左移 1 位变成 0xA0。为什么?

I2C 传输时,地址后面跟一个读写位:

  • 写操作:0x50 << 1 | 0 = 0xA0
  • 读操作:0x50 << 1 | 1 = 0xA1

2. GPIO 配置

1
gpio_initstructure.gpio_out_type = GPIO_OUTPUT_OPEN_DRAIN;

I2C 必须用开漏输出! 为什么?

  • I2C 总线上有多个设备,任何设备都可以拉低 SDA/SCL
  • 开漏模式:只能拉低,不能主动拉高(靠上拉电阻拉高)
  • 如果用推挽输出,两个设备同时输出不同电平会短路!

3. 三种传输模式

模式 原理 优点 缺点
轮询 CPU 不断检查标志位 简单直接 占用 CPU
中断 传输完成触发中断 CPU 可以做其他事 需要中断服务函数
DMA DMA 自动搬运数据 CPU 完全不参与 配置复杂

4. 写入后延时

1
delay_ms(1);  // EEPROM 写入需要时间

EEPROM 写入是一个内部编程过程,需要几毫秒。在编程期间,EEPROM 不会应答(NACK),主机必须等待。


实验现象

  1. 按下 USER 按键
  2. 三种模式依次写入并读回
  3. LED3 亮起 → 三种模式全部成功
  4. LED2 快闪 → 有失败,检查接线

常见问题

Q: I2C 通信超时?

  • 检查上拉电阻(4.7KΩ)
  • 检查地址是否正确(7 位 vs 8 位地址的区别)
  • 检查 SCL/SDA 是否接反

Q: 写入后读出来不对?

  • EEPROM 写入需要时间,写入后至少等 5ms 再读
  • 检查 EEPROM 地址引脚(A0/A1/A2)是否与代码一致

Q: 7 位地址和 8 位地址的区别?

  • 7 位地址:0x50(芯片手册上的地址)
  • 8 位地址:0xA0(左移 1 位,包含读写位)
  • 不同库函数可能要求不同格式,注意区分

小结

概念 说明
I2C 2 线制低速通信总线
EEPROM 掉电不丢失,逐字节读写
开漏输出 I2C 必须用,避免短路
上拉电阻 4.7KΩ,必须有
7 位地址 芯片手册上的地址
写入延时 EEPROM 编程需要几毫秒

学了 I2C 能做什么?

I2C 是连接"低速外设"的万能总线,几乎所有传感器都支持 I2C:

传感器

项目 原理 难度
温湿度检测 SHT30/DHT20 + I2C → 温湿度数据 ⭐⭐
气压检测 BMP280 + I2C → 气压/海拔 ⭐⭐
光照检测 BH1750 + I2C → 光照强度
加速度/陀螺仪 MPU6050 + I2C → 6 轴姿态 ⭐⭐⭐
RTC 时钟 DS3231 + I2C → 精确时间 ⭐⭐
磁场检测 QMC5883L + I2C → 电子罗盘 ⭐⭐⭐
触摸检测 TTP223 电容触摸 + I2C ⭐⭐

存储与显示

项目 原理 难度
配置参数存储 AT24C02 + I2C → 存校准数据
OLED 显示 SSD1306 + I2C → 0.96 寸 OLED ⭐⭐
LCD 显示 I2C 接口的字符 LCD ⭐⭐

实际应用场景

  • 气象站:I2C 温湿度 + 气压 + 光照 → 环境监测
  • 平衡车:I2C MPU6050 → 姿态检测 + PID 控制
  • 智能手表:I2C 心率 + 加速度 + RTC → 健康监测
  • 工业仪表:I2C ADC(ADS1115)→ 高精度电压测量

核心思路:I2C 的核心优势是省线。2 根线就能挂 128 个设备。传感器、EEPROM、RTC、OLED……几乎所有低速外设都用 I2C。掌握了 I2C,你就有了连接整个传感器世界的能力。


下一步:学会了 I2C,可以学 TIM PWM 输出,用定时器生成 PWM 信号控制 LED 亮度、电机转速。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计