STM32之IIC详解
一、IIC
1. IIC or I2C 概述
IIC(Inter-Integrated Circuit)中文全称为集成电路总线,是由飞利浦公司(现恩智浦 NXP)在 1980 年代开发的一种 简单、双向、二线制、同步串行通信总线。
它主要用于连接同一块电路板上的各个集成电路(IC),让芯片之间能够以 低速、短距离 的方式进行通信,因其 设计简洁、占用引脚少 而被广泛应用。
应用场景分类:
-
访问传感器:
这是 IIC 最经典的应用。许多小型、低功耗的传感器都采用 IIC 接口与主控制器(如 MCU)通信:- 温度 / 湿度传感器:如 SHT30, SHT40
- 加速度计 / 陀螺仪:如 MPU-6050(同时集成了 3 轴加速度计和 3 轴陀螺仪)
- 磁力计:如 QMC5883L(电子罗盘)
- 气压计:如 BMP280
- 光强度传感器:如 BH1750
-
控制执行器或驱动芯片:
- LED 驱动芯片:如控制 LED 点阵屏或背光亮度的芯片(MAX7219, PCA9685 舵机驱动板)。
- IO 扩展芯片:当 MCU 的 GPIO 口不够用时,可以使用 IIC 接口的芯片来扩展输入输出口(如 PCF8574)。
- 音频编码 / 解码芯片:许多编解码芯片(Codec)支持 IIC 接口进行配置。
-
访问存储器:
- 一些小容量、用于存储配置参数或数据的非易失性存储器也使用 IIC 接口。
- EEPROM:如 24C02, 24C64 等系列芯片,用于存储设备参数、校准数据等。
-
与实时时钟(RTC)通信:
- 实时时钟芯片负责在系统断电时继续计时,通常通过 IIC 接口被主 CPU 读取或设置时间。
- 常用芯片:DS1307, PCF8563
2. IIC特性
3. IIC 数据帧分析
字段 | 位宽 | 功能描述 |
---|---|---|
起始位 | 1 位 | 触发 IIC 总线进入工作模式(标志通信开始)。 |
设备地址 | 7 位 | MCU 通过 7 位地址选择 IIC 总线上的目标设备,地址范围0~127 ,理论支持 128 个设备。 |
读 / 写位 | 1 位 | 告知目标设备:MCU 是要写入数据(写模式)还是读取数据(读模式)。 |
应答信号 ACK | - | 目标 IIC 设备接收请求后,回传应答信号,确认连接和操作形式(标志通信有效)。 |
寄存器地址 | 8 位 | 指定目标设备内的数据存储位置,地址范围0~255 ,对应最大存储容量256 Byte(仅针对写入操作场景)。 |
应答信号 ACK | - | 目标 IIC 设备确认收到寄存器地址,准备接收后续数据。 |
数据 Data | 8 位 | MCU 发送 1 字节数据,目标设备根据寄存器地址 + 数据 写入指定存储位置(写入操作场景)。 |
停止位 | 1 位 | 标志一次 IIC 操作结束,总线释放。 |
4. 起始位和停止位
-
IIC 的通信线:
IIC 总线基于 两根线 实现数据传递:SDA
==> 数据线(Serial Data Line,传输数据和地址)SCL
==> 时钟线(Serial Clock Line,提供同步时钟)
-
IIC 空闲状态:
当总线无数据传输时,处于 “空闲” 状态,此时:SCL
时钟线 保持高电平;SDA
数据线在SCL
高电平的同时,也 保持高电平。
空闲状态的电平规则是 IIC 协议的基础 —— 后续通信中,当 SCL 为高电平时,SDA 的电平跳变会被识别为 “起始信号”(SDA 从高→低)或 “停止信号”(SDA 从低→高),用于标记通信的开始和结束。
起始信号(Start)
- SCL 和 SDA 同时处于高电平状态。
- SCL 保持高电平状态的情况下,SDA 进行了 【高电平到低电平】 跳变。
- 触发 IIC 通信的起始。
停止信号(End)
- SCL 处于高电平状态,SDA 处于低电平状态。
- SCL 保持高电平状态的情况下,SDA 进行了 【低电平到高电平】 跳变。
- 触发 IIC 通信的停止。
核心规则:SCL 高电平时,SDA 的电平变化才会被识别为起始 / 停止信号,以此避免时钟线低电平时的误判。
5. IIC 数据传递 0 和 1
IIC 数据传递 1(逻辑 1)
- SCL 和 SDA 处于同一个 时钟周期 中,不存在上升沿或下降沿跳变,同时处于 高电平模式,此时 IIC 传递 逻辑 1。
- 一般情况下,SDA 会在时钟前半个周期就进入高电平状态,且在时钟后半个周期始终保持高电平状态。这样可保证数据传递的完整性,避免 SDA 跳变误触发起始或停止信号。
IIC 数据传递 0(逻辑 0)
- SCL 和 SDA 处于同一个 时钟周期 中,不存在上升沿或下降沿跳变;SCL 保持高电平时,SDA 在整个时钟周期中处于 低电平状态,此时 IIC 传递 逻辑 0。
- 一般情况下,SDA 会在时钟前半个周期就进入低电平状态,且在时钟后半个周期始终保持低电平状态。这样可保证数据传递的完整性,避免 SDA 跳变误触发起始或停止信号。
IIC 是 同步通信,数据在 SCL 的 “节拍” 下传输 ——每个时钟周期传输 1 位数据。核心规则:
- ✅ SCL 低电平时:SDA 可以自由切换电平(准备下一位数据);
- ✅ SCL 高电平时:SDA 必须保持稳定(此时电平代表有效数据,避免误判为起始 / 停止信号)。
这样设计既保证了数据采样的可靠性,又区分了 “控制信号(起始 / 停止)” 和 “数据信号”。)
6. 读写标志位
-
地址数据组成:
IIC 发送设备地址时,1 字节数据由 7 位设备地址 + 1 位读写标志 构成。 -
地址格式优化示例:
假设设备地址为二进制101 0000
(前 7 位),可规整表示为1010 000X
(X
为读写标志位,占第 8 位)。 -
读写标志定义:
0
:主设备 → 从设备 写入数据(写操作);1
:主设备 ← 从设备 读取数据(读操作)。
-
通信实例(设备地址为
1010000
二进制):- 写操作:发送
1010 0000
(二进制)→ 十六进制0xA0
; - 读操作:发送
1010 0001
(二进制)→ 十六进制0xA1
。
- 写操作:发送
7. 应答信号
-
触发场景:
当主机发送完一组数据(如设备地址、寄存器地址等)后,进入等待状态,此时 从机设备需响应一个应答数据(ACK)。 -
电平检测逻辑:
数据发送完毕后,SDA 处于低电平状态;在 一个时钟周期 内,主机检测 SDA 的电平情况,判断从机的应答状态:- 若 SDA 保持 低电平:代表从机成功接收数据(应答有效);
- 若 SDA 保持 高电平:代表从机未正确接收(应答失败,主机可触发重试或报错)。
从机通过主动拉低 SDA 实现应答,是 IIC 保证通信可靠性的 “握手” 设计 —— 避免数据传输失控。若从机繁忙,会维持 SDA 高电平,主机可据此处理异常流程。
8. 寄存器地址数据内容
-
传输方式与范围:
寄存器地址的数据传递 遵循 IIC 数据传递 0 和 1 的电平规则(即 SCL 高电平时 SDA 保持稳定电平表示数据),支持的地址范围为0000 0000 ~ 1111 1111
(对应十进制 0~255 ,共 256 个可寻址单元)。 -
读写操作关联:
当 IIC 主机明确目标设备的寄存器地址后,会 根据读写标志位(设备地址的第 8 位),决定对该地址执行 写入数据操作 或 读取数据操作:- 若为写标志(0):主机向该寄存器地址写入数据;
- 若为读标志(1):主机从该寄存器地址读取数据。
9. 数据内容【发送/写操作】到 IIC 中
-
前提条件:
主机发送完寄存器地址,且明确收到从机返回的 ** 应答信号(ACK)** 后,进入数据发送流程。 -
数据传输细节:
主机向目标 IIC 设备发送 8 位(1 字节)的数据内容,其中数据的 “0” 和 “1”,严格遵循 IIC 数据传递 0 或 1 的电平规范 传输:- 若数据位为
1
:SCL 高电平时,SDA 维持高电平; - 若数据位为
0
:SCL 高电平时,SDA 维持低电平;
(通过 “SCL 高电平下 SDA 稳定” 的规则,保证数据被从机正确采样,避免信号误判。)
- 若数据位为
10. 读取数据操作数据帧内容

IIC 读写流程(写地址→读数据)
-
主机写寄存器地址:
主机通过 设备地址 定位目标 IIC 从机,选择【写入操作】,将 “待读写的寄存器地址” 告知从机。 -
重新发起 IIC 通信:
主机发送 重复起始信号(不发停止位,直接发新起始位),进入新通信阶段。 -
主机发起读操作:
主机再次通过 设备地址 定位同一从机,此时地址的读写标志设为【读取操作】,从机接收该指令。 -
从机回传数据:
从机根据 此前主机指定的寄存器地址,将对应数据发送给主机。
(注:该流程利用 “重复起始” 机制 切换读写方向,避免总线释放后被干扰,是 IIC“先写地址、再读数据” 的经典实现。)
11. IIC 协议核心内容总结
二、IIC 操作 EEPROM 存储设备
1. EEPROM 设备概述
EEPROM 概述
- EEPROM(也常写作 E²PROM)是 “电可擦除可编程只读存储器”(Electrically-Erasable Programmable Read-Only Memory)的缩写。它是一种非易失性存储器,即使在断电后也能长期保留存储的数据。
- 顾名思义,它的核心特性是可以通过电信号来擦除和重新编程,这使其在需要频繁修改小量数据的应用中变得不可或缺。
-
核心特征
- 非易失性:断电后数据不会丢失。
- 电可擦除与可编程:无需像老式 EPROM 那样用紫外线擦除,直接通过电路施加特定电压即可完成擦写操作,非常方便。
- 字节级擦写:这是 EEPROM 与 FLASH 存储器的一个关键区别。EEPROM 可以按字节(Byte) 进行擦除和写入,无需擦除整个扇区。
- 有限的擦写次数:每个存储单元都有擦写寿命周期,通常在 10 万次到 100 万次 之间。超过此限制后,该单元可能变得不可靠或无法使用。
- 相对较慢的写入速度:写入一个字节通常需要几毫秒(ms)的时间,比 RAM 和 FLASH 慢几个数量级。
- 容量相对较小:由于单元结构复杂(每个晶体管都需要一个额外的控制晶体管),其存储密度低于 FLASH,容量通常从几 Kbit 到几 Mbit,远小于现代 NAND FLASH。
2. 24C02 芯片
一、24C02 概述
24C02 是一款 经典串行 EEPROM 芯片,由 Microchip、ON Semiconductor、ST 等多家半导体公司生产。其命名规则解析:
- “24”:代表属于 I²C 协议家族;
- “C”:代表芯片系列;
- “02”:代表存储容量为 2 Kbit(换算为 256 字节,计算:\(2 \times 1024 \div 8 = 256\))。
二、24C02 核心特性
容量与地址范围:
- 容量:2 Kbit(对应 256 字节);
- 地址范围:
0x00 ~ 0xFF
(共 256 个可寻址存储单元)。通信接口: 采用 I²C(Inter-Integrated Circuit)串行接口(两线制协议),节省 MCU 引脚资源:
- SDA:串行数据线,负责双向数据传输;
- SCL:串行时钟线,由主设备(如 MCU)提供时钟信号。
工作电压: 兼容宽电压范围(如 1.7V ~ 5.5V),支持 3.3V 和 5V 系统。
寿命与保存期:
- 擦写寿命:约 100 万次 擦写循环;
- 数据保存期:约 100 年(断电后数据长期留存)。
写保护功能: 内置 WP 引脚:
- WP 接 VCC(高电平):存储器进入写保护,仅能读、无法写;
- WP 接 GND(低电平):允许正常读写操作,防止数据意外篡改。
页写模式: 支持 16 字节页写操作,可连续写入最多 16 字节,比单字节写入效率更高。
3. 24C02 存储芯片原理图分析

一、IIC 引脚配置
需对 MCU 的 PB6、PB7 引脚进行功能配置:
- PB6:复用为 IIC 的 SCL 时钟线(提供通信同步时钟);
- PB7:复用为 IIC 的 SDA 数据线(双向传输地址、数据)。
二、写保护(WP 引脚)设置
24C02 的 WP 引脚连接 GND:
- 当 WP = GND(低电平)时,芯片解除写保护,支持 读写操作;
- 若 WP = VCC(高电平),芯片进入写保护,仅允许读操作。
三、设备地址计算
24C02 的设备地址由 固定位 + 引脚 A2/A1/A0 电平 决定:
- 固定位:24C02 的 I²C 设备地址高 4 位为
1010
(协议规定);- 引脚电平:芯片的 A0、A1、A2 引脚均接 GND(低电平,对应二进制
0
);- 地址拼接:设备地址格式为
1010 A2 A1 A0
,代入电平后为1010 000
(二进制),换算为十六进制即 0xA0。
4. 补充 GPIO BSRR 和 BRR 寄存器使用
5. I2C 头文件内容
myiic.h:
#ifndef _MY_IIC_H
#define _MY_IIC_H
#include "stm32f10x.h"
#include "systick.h"
#define EEPROM_24C02_ADDRESS (0xA0)
#define I2C_DELAY_US (5)
// PB6 ==> SCL
#define I2C_SCL_PIN (0x01 << 6)
// PB7 ==> SDA
#define I2C_SDA_PIN (0x01 << 7)
/**
* @brief I2C 对应引脚初始化,目前使用的 IIC 协议对应引脚
* PB6 ==> IIC_SCL 时钟线
* PB7 ==> IIC_SDA 数据线
*/
void I2C_GPIO_Init(void);
/**
* @brief 设置 SCL 时钟线高低电平
*
* @param state 对应数据为 1,SCL 拉高,对应数据为 0,SCL 拉低
*/
void SCL_Set(u8 state);
/**
* @brief 设置 SDA 数据线高低电平
*
* @param state 对应数据为 1,SDA 拉高,对应数据为 0,SDA 拉低
*/
void SDA_Set(u8 state);
/**
* @brief 设置 SDA 对应 PB7 GPIO 工作模式改为输入模式
* 主要用于
* 1. 24C02 设备应答信息获取
* 2. 读取设备存储数据操作使用。
*/
void SDA_Input_Mode(void);
/**
* @brief 设置 SDA 对应 PB7 GPIO 工作模式改为输出模式
* 主要用于
* 1. 应答操作完成模式切换
* 2. 读取操作完成模式切换
*/
void SDA_Output_Mode(void);
/**
* @brief 读取 SDA 数据上 IDR 输入端的电平情况
* 主要用于
* 1. IIC 从设备应答信号
* 2. IIC 存储设备读取信息反馈
*/
u8 SDA_Read(void);
/**
* @brief 读取 SCL 数据上 IDR 输入端的电平情况
* 主要用于
* 1. 主要用于判断读取数据 0 或者 1 情况
*/
u8 SCL_Read(void);
/**
* @brief I2C 起始信号,流程
* 1. SCL 和 SDA 高电平。I2C_Delay
* 2. SDA 低电平,I2C_Delay
* 3. SCL 低电平 I2C_Delay
*/
void I2C_Start(void);
/**
* @brief I2C 停止信号,流程
* 1. SCL 和 SDA 低电平。I2C_Delay
* 2. SCL 高电平,I2C_Delay
* 3. SDA 高电平, I2C_Delay
* 此时 I2C 进入【空闲状态】
*/
void I2C_Stop(void);
/**
* @brief I2C 延时控制函数,延时时间为 5 us
*/
void I2C_Delay(void);
/**
* @brief I2C 发送一个字节数据到 I2C 总线
* 主要用于:
* 1. 设备地址【7 位】 + 读写标志位发送【1位】 ==> 1 字节
* 2. I2C 设备寄存器地址【8位】==> 1 字节
* 3. 写入到 I2C 设备的字节数据【8位】 ==> 1 字节
*
* @return 发送成功 ACK 应答信息已获取,返回 0,否则返回 1
*/
u8 I2C_SendByte(u8 data);
/**
* @brief I2C 从数据总线中读取 1 个字节数据内容
* 主要用于:
* 1. MCU 读取设备中数据存储字节。
*
* @param ack 如果 ACK 为 0 表示继续读取,如果为 1 表示 NACK 不再读取
*
* @return 读取到的 1 个字节数据内容
*/
u8 I2C_ReadByte(u8 ack);
/**
* @brief 写入一个字节数据到 EERPOM 指定寄存器位置
*
* @param addr 目标寄存器存储当前数据的地址
* @param data 目标写入到 EERPOM 存储器中的数据
* @return 写入成功返回 0,失败返回 1
*/
u8 EEPROM_WriteByte(u8 addr, u8 data);
/**
* @brief 读取 EEPROM 一个字节数据
*
* @param addr 目标寄存器存储当前数据的地址
* @return 返回是读取到的数据内容
*/
u8 EEPROM_ReadByte(u8 addr);
/**
* @brief 写入一页数据到 EEPROM 芯片中。一页数据最大 8 个字节
*
* @param addr 目标寄存器存储当前数据的起始地址
* @param data 目标写入到 EERPOM 存储器中的数据
* @param len 目标写入到当前 EEPROM 中的数据个数
* @return 写入成功返回 0,失败返回 1
*/
u8 EEPROM_WritePage(u8 addr, u8 *data, u8 len);
/**
* @brief 读取 EEPROM 芯片中一页数据存储到 buffer 中 。一页数据最大 8 个字节
*
* @param addr 目标寄存器读取数据的起始地址
* @param buffer 存储 EERPOM 临时空间
* @param len 临时空间空间字节数
* @return 写入成功返回 0,失败返回 1
*/
u8 EEPROM_ReadPage(u8 addr, u8 *buffer, u8 len);
#endif
myiic.c:
#include "myiic.h"void I2C_GPIO_Init(void){/*PB6 ==> IIC_SCL 时钟线 PB7 ==> IIC_SDA 数据线*//*1. RCC 使能当前 GPIOB 组引脚*/RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;/*2. 通过 GPIOB CRL 控制 PB6 和 PB7 引脚根据官方手册提示 IIC_SCL IIC_SDA 对应复用开漏模式*/GPIOB->CRL &= ~(0x77000000);GPIOB->CRL |= 0x77000000;/*3. 利用 GPIOB_BSRR 寄存器,控制 PB6 和 PB7 引脚 ODR 对外输出高电平,进入 IIC 空闲状态。*///等价于操作ODR,同时拉高SDA和SCL引脚GPIOB->BSRR |= (0x3 << 6);
}void SCL_Set(u8 state){//PB6 ==>SCLif(state){//利用BSRR拉好输出,控制ODR为1GPIOB->BSRR |= I2C_SCL_PIN;}else{//利用BRR拉低输出,控制ODR为0GPIOB->BRR |= I2C_SCL_PIN;}
}void SDA_Set(u8 state){//PB7 ==> SDAif(state){GPIOB->BSRR |= I2C_SDA_PIN;}else{GPIOB->BRR |= I2C_SDA_PIN;}
}void SDA_Input_Mode(void){// PB7 ==> SDA/*SDA 输入模式选择【浮空输入】因为外部的 IIC 设备具备明确的驱动能力和明确的高低电平信号利用浮空模式完全取决于外部设备信号的情况,获取相关的读取数据内容*/GPIOB->CRL &= ~(0xF0000000);GPIOB->CRL |= (0x40000000);
}void SDA_Output_Mode(void)
{// PB7 ==> SDA/*SDA 从输入模式回到复用开漏模式*/GPIOB->CRL &= ~(0xF0000000);GPIOB->CRL |= (0x70000000);
}u8 SDA_Read(void)
{// PB7 ==> SDA return (GPIOB->IDR & I2C_SDA_PIN) ? 1 : 0;
}u8 SCL_Read(void)
{// PB6 ==> SCLreturn (GPIOB->IDR & I2C_SCL_PIN) ? 1 : 0;
}void I2C_Start(void){/*【注意】保证 SDA 处于输出工作模式。*/SDA_Output_Mode();//SDA和SCL都是高电平状态SDA_Set(1);SCL_Set(1);I2C_Delay(); //5ms延迟//拉低SDA,触发起始信号SDA_Set(0);I2C_Delay(); // 5 us 延时// SCL 拉低,进入后续的数据发送传递过程。SCL_Set(0);I2C_Delay(); // 5 us 延时
}void I2C_Stop(void)
{/*【注意】保证 SDA 处于输出工作模式。*/SDA_Output_Mode();// SDA 和 SCL 同时设置为低电平状态。处于应答信号完成阶段。SDA_Set(0);SCL_Set(0);I2C_Delay(); // 5 us 延时// SCL 拉高SCL_Set(1);I2C_Delay(); // 5 us 延时// SDA 拉高,SDA 在 SCL 高电平状态中,进行了【低电平到高电平】跳变// 触发停止信号【STOP】SDA_Set(1); I2C_Delay(); // 5 us 延时// 完成操作之后,当前 SCL 和 SDA 都处于高电平状态【空闲状态】
}void I2C_Delay(void)
{/*利用当前 5 us 延时控制,进行高低电平切换每 10 us 是一个高低电平工作时间,整个程序中周期对应 100 KHz对应 I2C 标准模式 100Kbit/s 传输速度*/SysTick_Delay_us(I2C_DELAY_US);
}u8 I2C_SendByte(u8 data){u8 i = 0;u8 ack = 0;/*【注意】保证 SDA 处于输出工作模式。*/SDA_Output_Mode();// 【数据发送代码】for(i = 0;i < 8;i++){//时钟线拉低,此时处于时钟开始阶段SCL_Set(0);I2C_Delay();//跟0x80与运算可以提取u8的最高位if(data & 0x80){//要发送'1'SDA_Set(1);}else{SDA_Set(0); // I2C 数据 0}I2C_Delay(); // 5 us 延时//改完SDA电平之后,拉高SCL,以便从设备读取SDASCL_Set(1);I2C_Delay(); // 5 us 延时// 数据需要左移 1 位,抹掉原本最高位data <<= 1;}// 【I2C 设备应答信号】//数据发送完成之后,SCL先拉低SCL_Set(0);I2C_Delay();//准备接收从设备的应答信号,所以要改为输入模式/*SDA 从输出模式改为输入模式,用于接收 I2C 设备的应答信号输入模式 ==> 浮空输入*/SDA_Input_Mode();// 设置当前 SDA ODR 为 1 电平处于高电平状态。也可以认为是// 释放当前 SDA 数据线。电平状态交给外部设备控制,利用浮空读取SDA_Set(1); I2C_Delay();// SCL 时钟拉高,时钟触发!!!通知从设备,我要开始采样了,你该发应答了!!!SCL_Set(1);I2C_Delay();/*读取当前 SDA 数据线的输入数据情况,判断当前 ACK 数据。*/ack = SDA_Read();// SCL 时钟拉低SCL_Set(0);/*修改 SDA 处于输出工作模式。*/SDA_Output_Mode();return ack;
}u8 I2C_ReadByte(u8 ack)
{u8 data = 0;u8 i = 0;/*【注意】SDA 工作模式修改为输入模式,同时 SDA 设置为高电平状态,利用浮空输入模式,通过外部设备修改当前 SDA 输入电平信号*/SDA_Input_Mode();SDA_Set(1);for (i = 0; i < 8; i++){/*时钟线拉低,进入准备状态*/SCL_Set(0);I2C_Delay();// 时钟线拉高,进入数据读取状态SCL_Set(1);I2C_Delay();data <<= 1;// 在 SCL 高电平状态下,判断当前 SDA 输入电平情况if (SDA_Read()){// 如果 SDA_Read() 返回值为 1 表示读取到高电平data |= 0x01;}}// 发送 ACK 或者 NACK 操作SCL_Set(0);I2C_Delay();/*修改当前 SDA 为输出模式,用于 MCU 发送应答信号ACK 表示接受数据成功,可以继续读取NACK 表示数据接收完成,不再继续读取数据。*/SDA_Output_Mode();if (ack){SDA_Set(1); // 发送 NACK 不再读取后续内容}else{SDA_Set(0); // 发送 ACK 表示继续读取后续数据内容}I2C_Delay();// SCL 时钟拉高将 SDA 数据明确发送SCL_Set(1);I2C_Delay();// SCL 时钟拉低,进入下一次时钟周期SCL_Set(0);return data;
}u8 EEPROM_WriteByte(u8 addr,u8 data){u8 retry = 3; //重试次数u16 timeout = I2C_TIMEOUT;//利用retry控制I2C写入一个字节数据到设备,尝试3次while(retry--){// 1.起始位I2C_Start();/*2. 发送目标设备地址 + R/W 标志位*/if(I2C_SendByte(EEPROM_24C02_ADDRESS)){// 所有发送操作一致,如何一个发送失败,直接停止位// 利用 continue 会到 while 循环控制,进入下一次// 数据发送完整流程。I2C_Stop();continue;}/*3. 目标存储数据寄存器的地址*/if(I2C_SendByte(addr)){I2C_Stop();continue;}/*4. 发送数据到I2C设备*/if(I2C_SendByte(data)){I2C_Stop();continue;}//5. 停止位I2C_Stop();/*一旦发送数据完毕,24C02 设备进入【冥想状态】,需要内部进行数据存储处理,需要一定的周期时间。此时,24C02 不会应答任何外部数据。*/while(timeout--){I2C_Start();/*I2C_SendByte(EEPROM_24C02_ADDRESS) 发送设备地址,找到目标设备判断是否应答1. 没有应答,24C02 处于数据处理过程2. 如果有应答,24C02 数据处理完毕*/if(!I2C_SendByte(EEPROM_24C02_ADDRESS)){I2C_Stop();return 0;}I2C_Stop();}}return 1;
}u8 EEPROM_ReadByte(u8 addr)
{u8 retry = 3;u8 data = 0;while (retry--){// 1. I2C 起始位I2C_Start();/*2. 发送目标设备地址 + R/W 标志位【目标设备地址 + W操作】*/if (I2C_SendByte(EEPROM_24C02_ADDRESS)){// 所有发送操作一致,如何一个发送失败,直接停止位// 利用 continue 会到 while 循环控制,进入下一次// 数据发送完整流程。I2C_Stop();continue;}/*3. 目标存储数据寄存器地址*/if (I2C_SendByte(addr)){I2C_Stop();continue;}/*4. 直接开始 I2C 起始位,作为读取操作数据帧第二段内容开启*/I2C_Start();/*5. 发送目标设备地址 + R 读取标志位*/if (I2C_SendByte(EEPROM_24C02_ADDRESS | 0x01)){I2C_Stop();continue;}/*6. 读取数据,并且本次读取是读取一个字节数据,需要发送 NACK告知设备,读取已完成*/data = I2C_ReadByte(1);I2C_Stop();return data;}return 0;
}u8 EEPROM_WritePage(u8 addr,u8 *data,u8 len){u8 i = 0;u16 timeout = I2C_TIMEOUT;//1. 判断用户提供的写入数据长度是否大于8,24c02一页对应8个字节if(len > 8){//直接返回1,表示操作错误return I2C_ERROR;}// 1.起始位I2C_Start();// 2.目标设备地址 + w写入标记if(I2C_SendByte(EEPROM_24C02_ADDRESS)){I2C_Stop();return I2C_ERROR;}//3. 目标写入数据寄存器首位寄存器地址if(I2C_SendByte(addr)){I2C_Stop();return I2C_ERROR;}//4.循环发送数据到24c02for(i = 0;i < len;i++){/*data 直接按照数组行为进行操作。*/if(I2C_SendByte(data[i])){I2C_Stop();return I2C_ERROR;}}I2C_Stop();/*一旦发送数据完毕,24C02 设备进入【冥想状态】,需要内部进行数据存储处理,需要一定的周期时间。此时,24C02 不会应答任何外部数据。*/while(timeout--){I2C_Start();/*I2C_SendByte(EEPROM_24C02_ADDRESS) 发送设备地址,找到目标设备判断是否应答1. 没有应答,24C02 处于数据处理过程2. 如果有应答,24C02 数据处理完毕*/if (!I2C_SendByte(EEPROM_24C02_ADDRESS)){I2C_Stop();return I2C_SUCCESS;}I2C_Stop();}return I2C_SUCCESS;
}u8 EEPROM_ReadPage(u8 addr, u8 *buffer, u8 len)
{u8 i = 0;// 1. 判断用户提供的写入数据长度是否大于 8 ,24C02 一页对应 8 个字节if (len > 8){return I2C_ERROR;}// 1. I2C 起始信号I2C_Start();// 2. 目标设备地址 + W 写入标记if (I2C_SendByte(EEPROM_24C02_ADDRESS)){I2C_Stop();return I2C_ERROR;}// 3. 目标读取数据寄存器首位寄存器地址if (I2C_SendByte(addr)){I2C_Stop();return I2C_ERROR;}// 4. 直接重新开启 I2C 起始位操作I2C_Start();// 5. 目标设备地址 + R 读取标记if (I2C_SendByte(EEPROM_24C02_ADDRESS | 0x01)){I2C_Stop();return I2C_ERROR;}// 6. 利用 for 循环读取数据内容,存储到 buffer 缓冲区中for (i = 0; i < len; i++){/*读取操作分为两种情况1. 在最后一个读取内部之前,每一次读完完成,都需要发送 ACK 应答表示数据继续读取。2. 最后一个字节读取完成,发送 NACK 应答,表示读取完成。*/if (i == len - 1){buffer[i] = I2C_ReadByte(1);}else {buffer[i] = I2C_ReadByte(0);}}I2C_Stop();return I2C_SUCCESS;
}
main.c:
#include "stm32f10x.h"#include "led.h"
#include "key.h"
#include "delay.h"
#include "beep.h"
#include "usart1.h"
#include "adc.h"
#include "systick.h"
//#include "tim6.h"
//#include "tim3.h"
//#include "sg90.h"
#include "myiic.h"int main(void)
{Led_Init();USART1_Init(115200);USART1_Interrupt_Enable();I2C_GPIO_Init();u8 data[5] = {0x11, 0x12, 0x13, 0x14, 0x15};u8 buffer[5] = {0};EEPROM_WritePage(0x11, data, 5);EEPROM_ReadPage(0x11, buffer, 5);for (u8 i = 0; i < 5; i++){printf("data[%d] = %d\n", i, data[i]);printf("buffer[%d] = %d\n", i, buffer[i]);}while (1){Led1_Ctrl(1);}}