tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第9章:I2C通信 + 配套代码13~15" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:I2C 就像教室里的传纸条游戏——SCL 是老师拍手的节奏(时钟线),SDA 是传的纸条(数据线)。老师每拍一次手(时钟周期),大家就传递一位。每个同学有唯一的学号(设备地址 7 位),老师喊学号时对应同学才回答。STOP 条件就像下课铃——老师拍手节奏变了,表示传纸条结束。
I2C(Inter-Integrated Circuit,读作 I-squared-C)是一种同步、半双工、多主从的串行通信总线,由 Philips 公司(现 NXP)发明。
| 特性 | 说明 |
|---|---|
| 线数 | SCL(时钟)+ SDA(数据)两根线 |
| 模式 | 半双工(同一时刻只能单向传输) |
| 速率 | 标准 100Kbps / 快速 400Kbps / 高速 3.4MHz |
| 主从 | 多主多从(通过仲裁解决冲突) |
| 地址 | 7 位地址(最多 127 个设备)或 10 位地址 |
| 上拉电阻 | 两根线都需要外部上拉电阻(4.7KΩ 典型) |
参考:参考手册 §16(I2C 寄存器描述) 参考:NXP I2C 总线规范(UM10204)
SCL 和 SDA 都是高电平(由上拉电阻拉高)。
SCL 为高电平时,SDA 从高电平切换到低电平
代码实现:
SCL_HIGH; // SCL = 1
SDA_HIGH; // SDA = 1
I2C_DELAY; // 等待(数据建立时间)
SDA_LOW; // SDA = 0 → 起始条件(在 SCL 高电平时 SDA 变低)
I2C_DELAY; // 等待(保持时间)
SCL 为高电平时,SDA 从低电平切换到高电平
代码实现:
SCL_LOW; // SCL = 0(准备)
SDA_LOW; // SDA = 0
SCL_HIGH; // SCL = 1
I2C_DELAY;
SDA_HIGH; // SDA = 0 → 1 → 终止条件(在 SCL 高电平时 SDA 变高)
I2C_DELAY;
每个字节 8 位,MSB(最高位)先发。SCL 低电平时允许 SDA 变化,SCL 高电平时 SDA 必须稳定(接收方采集数据)。
SCL: ████░░░░████░░░░████░░░░....████░░░░██
SDA: ██░░██░░██░░██░░ ██░░██░░
↑MSB ↑LSB
SCL低时SDA可以变 SCL高时采集
每个字节后,接收方在第 9 个 SCL 周期控制 SDA:
NACK:SDA 释放(高电平,表示"收完"或"不再接收")
主机发送8位数据后:
主机释放SDA → 从机在第9个SCL脉冲拉低SDA → 应答(ACK)
主机写数据到从机:
START + 7位从机地址 + R/W(0=写) + ACK + 数据1 + ACK + 数据2 + ACK + ... + STOP
主机从从机读数据:
START + 7位从机地址 + R/W(1=读) + ACK + 从机发数据1 + 主机ACK + ... + 从机发数据N + 主机NACK + STOP
组合格式(先写地址再读):
START + 从机地址(W) + ACK + 寄存器地址 + ACK + RESTART + 从机地址(R) + ACK + 数据 + NACK + STOP
VCC(3.3V) VCC(3.3V)
├── Rpu(4.7KΩ) ├── Rpu(4.7KΩ)
│ │
├──── SCL ─────────┤
│ PB10 │ 24C02 SCL
├──── SDA ─────────┤
│ PB11 │ 24C02 SDA
│ │
GND ────────────── GND
I2C 总线需要外部上拉电阻(典型值 4.7KΩ),因为 I2C 引脚使用开漏输出:
参考:开发板原理图 I2C 接口部分(24C02 电路) 参考:参考手册 §16(I2C 寄存器)
24C02 是一个 2Kbit(256 字节) 的 I2C 串行 EEPROM。
| 参数 | 值 |
|---|---|
| 容量 | 256 字节(2Kbit) |
| 页大小 | 8 字节(一次页写最多 8 字节) |
| 写周期 | 5ms(内部擦写时间,期间不响应) |
| I2C 地址 | 0xA0(写)/ 0xA1(读) |
| 地址引脚 | A0/A1/A2(可配置,最多 8 个同型号设备共用总线) |
| 工作电压 | 1.8V~5.5V |
| 接口 | I2C(标准/快速模式) |
地址计算:
设备地址 = 1010 + A2 + A1 + A0 + R/W
[7:4] [3] [2] [1] [0]
1010 0 0 0 0/1
写地址 = 1010 0000 = 0xA0
读地址 = 1010 0001 = 0xA1
页写注意事项:24C02 的页大小为 8 字节,一次页写操作只能在同一页内。如果尝试跨页写入(如从地址 0x07 开始写 3 字节),地址会自动卷回到页开头(地址 0x00),覆盖已有数据。跨页时必须手动分页写入。
项目路径:stm32_base/13_i2c_software_register
文件:stm32_base/13_i2c_software_register/Hardware/I2C/i2c.h
#ifndef __I2C_H
#define __I2C_H
#include "stm32f10x.h"
// GPIO 引脚: PB10=SCL, PB11=SDA
// 使用宏定义简化位操作,提升代码可读性
#define SCL_HIGH (GPIOB->ODR |= GPIO_ODR_ODR10) // |= 将ODR10置1
#define SCL_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR10) // &=~ 将ODR10清0
#define SDA_HIGH (GPIOB->ODR |= GPIO_ODR_ODR11) // |= 将ODR11置1
#define SDA_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR11) // &=~ 将ODR11清0
#define READ_SDA (GPIOB->IDR & GPIO_IDR_IDR11) // & 读取IDR11位
#define ACK 0
#define NACK 1
// I2C 延时: 100KHz 模式下 10us ≈ 半个时钟周期
// 调整此值可改变 I2C 通信速率
#define I2C_DELAY Delay_us(10)
void I2C_Init(void);
void I2C_Start(void);
void I2C_Stop(void);
void I2C_Ack(void);
void I2C_NAck(void);
uint8_t I2C_Wait4Ack(void);
void I2C_SendByte(uint8_t byte);
uint8_t I2C_ReadByte(void);
#endif
文件:stm32_base/13_i2c_software_register/Hardware/I2C/i2c.c
#include "i2c.h"
// I2C 初始化: PB10(SCL) + PB11(SDA) 开漏输出
void I2C_Init(void)
{
// 1. 开启 GPIOB 时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // |=: IOPBEN位置1
// 2. PB10, PB11 开漏输出 50MHz
// CNF=01(开漏输出), MODE=11(50MHz)
// MODE10[1:0] 在 CRH 第 8~9 位, CNF10[1:0] 在第 10~11 位
// MODE11[1:0] 在 CRH 第 12~13 位, CNF11[1:0] 在第 14~15 位
GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11); // MODE=11
GPIOB->CRH &= ~(GPIO_CRH_CNF10_1 | GPIO_CRH_CNF11_1); // CNFx[1]=0
GPIOB->CRH |= (GPIO_CRH_CNF10_0 | GPIO_CRH_CNF11_0); // CNFx[0]=1
// CNF[1:0]=01 → 开漏输出
}
// 起始条件: SCL=1 时 SDA 下降沿
void I2C_Start(void)
{
SCL_HIGH; // 先确保 SCL 高电平(准备总线)
SDA_HIGH; // SDA 先高
I2C_DELAY;
SDA_LOW; // SDA 从高变低 → 起始条件
I2C_DELAY;
// 起始后 SCL 可拉低准备传输数据
SCL_LOW;
}
// 停止条件: SCL=1 时 SDA 上升沿
void I2C_Stop(void)
{
SCL_LOW; // 先确保 SCL 低
SDA_LOW; // SDA 拉低
I2C_DELAY;
SCL_HIGH; // SCL 先高
I2C_DELAY;
SDA_HIGH; // SDA 从低变高 → 停止条件
I2C_DELAY;
}
// 发送应答: 在第 9 个时钟拉低 SDA
void I2C_Ack(void)
{
SDA_LOW; // SDA 拉低 = 应答
I2C_DELAY;
SCL_HIGH; // SCL 脉冲(从机采样)
I2C_DELAY;
SCL_LOW;
I2C_DELAY;
SDA_HIGH; // 释放 SDA
}
// 发送非应答: 在第 9 个时钟释放 SDA
void I2C_NAck(void)
{
SDA_HIGH; // SDA 高 = 非应答
I2C_DELAY;
SCL_HIGH; // SCL 脉冲
I2C_DELAY;
SCL_LOW;
I2C_DELAY;
}
// 等待从机应答: 返回 0=ACK, 1=NACK
uint8_t I2C_Wait4Ack(void)
{
SDA_HIGH; // 释放 SDA(让从机控制)
I2C_DELAY;
SCL_HIGH; // SCL 高 = 从机可应答
I2C_DELAY;
uint8_t ack = READ_SDA; // & 测试: 读取 SDA 电平
// SDA=0 → 从机应答(ACK); SDA=1 → 从机非应答(NACK)
SCL_LOW; // 完成应答位
I2C_DELAY;
return ack ? NACK : ACK;
}
// 发送 1 字节(MSB first)
void I2C_SendByte(uint8_t byte)
{
for (uint8_t i = 0; i < 8; i++)
{
// SCL 低电平时改变 SDA
if (byte & 0x80) // &测试: 最高位=1?
SDA_HIGH;
else
SDA_LOW;
byte <<= 1; // 左移, 准备下一位
// SCL 高电平期间 SDA 保持稳定(从机采样)
SCL_HIGH;
I2C_DELAY;
SCL_LOW;
I2C_DELAY;
}
}
// 读取 1 字节(MSB first)
uint8_t I2C_ReadByte(void)
{
uint8_t byte = 0;
SDA_HIGH; // 释放 SDA(让从机控制)
for (uint8_t i = 0; i < 8; i++)
{
byte <<= 1; // 左移留出低位
SCL_HIGH; // SCL 高 → 从机将数据放到 SDA
I2C_DELAY;
if (READ_SDA) // & 测试: 读取 SDA 电平
byte |= 1; // 如果 SDA=1, 置当前位为 1
SCL_LOW; // SCL 低 → 从机准备下一位
I2C_DELAY;
}
return byte;
}
文件:stm32_base/13_i2c_software_register/User/main.c
#include "i2c.h"
int main(void)
{
USART1_Init(); // 串口初始化(调试输出用)
I2C_Init(); // I2C GPIO 初始化
/* ========== 写操作: 向 24C02 地址 0x00 写入 0x55 ========== */
I2C_Start();
I2C_SendByte(0xA0); // 从机地址 + 写标志 (0xA0 = 1010 0000)
I2C_Wait4Ack(); // 等待 24C02 应答
I2C_SendByte(0x00); // 要写入的 24C02 内部字节地址
I2C_Wait4Ack();
I2C_SendByte(0x55); // 要写入的数据
I2C_Wait4Ack();
I2C_Stop();
// 等待 24C02 内部写周期完成(约 5ms)
Delay_ms(10);
/* ========== 读操作: 从 24C02 地址 0x00 读取数据 ========== */
// 组合格式: START + 写地址 + 寄存器地址 + RESTART + 读地址 + 数据
I2C_Start();
I2C_SendByte(0xA0); // 写地址(先写寄存器地址)
I2C_Wait4Ack();
I2C_SendByte(0x00); // 要读取的字节地址
I2C_Wait4Ack();
I2C_Start(); // RESTART: 不发送 STOP 直接发第二个 START
I2C_SendByte(0xA1); // 读地址
I2C_Wait4Ack();
uint8_t data = I2C_ReadByte(); // 读取数据
I2C_NAck(); // 最后一字节主机发 NACK
I2C_Stop();
printf("写入 0x55, 读出 0x%02X\r\n", data);
// 预期输出: "写入 0x55, 读出 0x55"
while (1);
}
| I2C 操作 | 函数 | 说明 |
|---|---|---|
| 初始化 | I2C_Init() |
PB10(SCL)+PB11(SDA) 开漏输出 |
| 起始条件 | I2C_Start() |
SCL高→SDA高→低切换 |
| 停止条件 | I2C_Stop() |
SCL高→SDA低→高切换 |
| 发送字节 | I2C_SendByte(byte) |
8 位逐位发送,MSB first |
| 读取字节 | I2C_ReadByte() |
8 位逐位读取 |
| 等待应答 | I2C_Wait4Ack() |
返回 ACK(0) 或 NACK(1) |
| 发应答 | I2C_Ack() |
拉低 SDA 表示继续接收 |
| 发非应答 | I2C_NAck() |
释放 SDA 表示停止接收 |
I2C_Start() 但不先发 I2C_Stop()