tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第9章:I2C通信 + 配套代码13~15" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:I2C 就像教室里的传纸条规则——SCL 是老师拍手的节奏(时钟),SDA 是传的纸条(数据)。每次老师拍一下手(时钟跳变),纸条就传递一位信息。每个同学有唯一的学号(设备地址),只有学号对得上的人才能接收纸条。
I2C(Inter-Integrated Circuit)是一种同步、半双工、多主从的串行通信总线,仅需 2 根线:SCL(时钟线)和 SDA(数据线)。
| 特性 | 说明 |
|---|---|
| 线数 | SCL + SDA 两根 |
| 模式 | 半双工 |
| 速率 | 标准 100Kbps,快速 400Kbps |
| 主从 | 多主多从(通过仲裁解决冲突) |
| 地址 | 7位地址(最多 127 个设备)或 10位地址 |
| 上拉 | 两根线都需要外部上拉电阻(4.7KΩ 典型) |
I2C 协议由几个基本操作组成:
SCL 在高电平时,SDA 从高电平切换到低电平
SCL ────┐ ┌────
│ │
SDA ────┘──┘────
SCL 在高电平时,SDA 从低电平切换到高电平
SCL ────┐ ┌────
│ │
SDA ────┘──┘────
每个字节 8 位,MSB 先发。 SCL 低电平时 SDA 改变数据,SCL 高电平时 SDA 保持稳定。
每个字节后,接收方在第 9 个 SCL 周期拉低 SDA 表示应答(ACK),释放 SDA 表示非应答(NACK)。
主机写数据到从机:
START + 从机地址(W) + ACK + 数据1 + ACK + 数据2 + ACK + ... + STOP
主机从从机读数据:
START + 从机地址(R) + ACK + 数据1 + ACK + 数据2 + NACK + STOP
I2C 总线需要外部上拉电阻(RPU):
VCC(3.3V)
├── RPU(4.7KΩ)
├──── SCL
├── RPU(4.7KΩ)
└──── SDA
软件模拟 I2C 使用 GPIO 的开漏输出模式(CNF=01, MODE=11):
参考:原理图 I2C 接口部分(24C02 电路),参考手册 §16
| 参数 | 值 |
|---|---|
| 容量 | 256 字节(2Kbit) |
| 页大小 | 8 字节(页写最多 8 字节) |
| 地址 | 0xA0(写)/ 0xA1(读) |
| 接口 | I2C |
| 工作电压 | 1.8V~5.5V |
页写限制:24C02 每页 8 字节,跨页写时需要手动分页(超过页边界会自动卷回页开头)。
写周期:每个写操作后需要等待 5ms(内部擦写时间),在此期间不响应任何 I2C 命令。
项目路径: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"
// 引脚宏定义:PB10=SCL, PB11=SDA
#define SCL_HIGH (GPIOB->ODR |= GPIO_ODR_ODR10) // |= SCL置1
#define SCL_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR10) // &=~ SCL清0
#define SDA_HIGH (GPIOB->ODR |= GPIO_ODR_ODR11) // |= SDA置1
#define SDA_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR11) // &=~ SDA清0
#define READ_SDA (GPIOB->IDR & GPIO_IDR_IDR11) // & 读取SDA电平
#define ACK 0
#define NACK 1
#define I2C_DELAY Delay_us(10) // 100KHz:半周期延时10us
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"
void I2C_Init(void)
{
// 开启GPIOB时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
// PB10(PB11) 开漏输出:MODE=11(50MHz), CNF=01(开漏)
GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11); // MODE位置1
GPIOB->CRH &= ~(GPIO_CRH_CNF10_1 | GPIO_CRH_CNF11_1); // CNF[1]清0
GPIOB->CRH |= (GPIO_CRH_CNF10_0 | GPIO_CRH_CNF11_0); // CNF[0]置1
}
// 起始:SCL高电平时,SDA高→低
void I2C_Start(void)
{
SCL_HIGH; // 先确保SCL高
SDA_HIGH; // SDA先高
I2C_DELAY;
SDA_LOW; // SDA从高变低 → 起始条件
I2C_DELAY;
}
// 停止:SCL高电平时,SDA低→高
void I2C_Stop(void)
{
SCL_HIGH; // SCL拉高
SDA_LOW; // SDA先低
I2C_DELAY;
SDA_HIGH; // SDA从低变高 → 停止条件
I2C_DELAY;
}
// 发送ACK:在第9个时钟将SDA拉低
void I2C_Ack(void)
{
SDA_LOW; // SDA拉低表示应答
SCL_HIGH; // SCL产生脉冲
I2C_DELAY;
SCL_LOW;
I2C_DELAY;
SDA_HIGH; // 释放SDA
}
// 发送NACK:在第9个时钟释放SDA
void I2C_NAck(void)
{
SDA_HIGH; // SDA高表示非应答
SCL_HIGH; // SCL产生脉冲
I2C_DELAY;
SCL_LOW;
}
// 等待从机ACK:返回0=有ACK, 1=无ACK
uint8_t I2C_Wait4Ack(void)
{
SDA_HIGH; // 释放SDA(让从机控制)
SCL_HIGH; // SCL高电平采样
I2C_DELAY;
uint8_t ack = READ_SDA; // &测试: 读取SDA电平
SCL_LOW;
I2C_DELAY;
return ack ? NACK : ACK; // SDA=0: 从机应答; SDA=1: 无应答
}
文件: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)
I2C_Wait4Ack(); // 等待24C02应答
I2C_SendByte(0x00); // 写入地址(24C02的字节地址)
I2C_Wait4Ack();
I2C_SendByte(0x55); // 写入数据
I2C_Wait4Ack();
I2C_Stop();
Delay_ms(10); // 等待内部写周期完成
// 读回验证:从地址0x00读取
I2C_Start();
I2C_SendByte(0xA0);
I2C_Wait4Ack();
I2C_SendByte(0x00); // 再次发出地址
I2C_Wait4Ack();
I2C_Start(); // 重启(组合格式)
I2C_SendByte(0xA1); // 从机地址+读 (0xA1)
I2C_Wait4Ack();
uint8_t data = I2C_ReadByte(); // 读取数据
I2C_NAck(); // 主机发NACK表示读取结束
I2C_Stop();
printf("Read data: 0x%02X\r\n", data); // 应输出0x55
while (1);
}
| 函数 | 说明 |
|---|---|
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=应答) |
I2C_Ack() |
主机发送ACK |
I2C_NAck() |
主机发送NACK(读最后一个字节后) |