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 通信的停止。
    这两个信号是 IIC 通信的 "开关"------ 起始信号宣告通信开始 ,总线上的设备开始监听; 停止信号宣告通信结束 ,总线释放回空闲状态。
    核心规则: SCL 高电平时,SDA 的电平变化才会被识别为起始 / 停止信号,以此避免时钟线低电平时的误判。

5. IIC数据传递01

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 必须保持稳定(此时电平代表有效数据,避免误判为起始 / 停止信号)。
    这样设计既保证了数据采样的可靠性,又区分了 "控制信号(起始 / 停止)" 和 "数据信号"。)


根据数据传递规则,对应的 IIC 时序传递数据图例:

**6.**读写标志位

  • 地址数据组成

    IIC 发送设备地址时,1 字节数据由 7 位设备地址 + 1 位读写标志 构成。

  • 地址格式优化示例

    假设设备地址为二进制 101 0000(前 7 位),可规整表示为 1010 000XX 为读写标志位,占第 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 核心特性
  1. 容量与地址范围

    • 容量:2 Kbit(对应 256 字节);
    • 地址范围:0x00 ~ 0xFF(共 256 个可寻址存储单元)。
  2. 通信接口 : 采用 I²C(Inter-Integrated Circuit)串行接口(两线制协议),节省 MCU 引脚资源:

    • SDA:串行数据线,负责双向数据传输;
    • SCL:串行时钟线,由主设备(如 MCU)提供时钟信号。
  3. 工作电压 : 兼容宽电压范围(如 1.7V ~ 5.5V),支持 3.3V 和 5V 系统。

  4. 寿命与保存期

    • 擦写寿命:约 100 万次 擦写循环;
    • 数据保存期:约 100 年(断电后数据长期留存)。
  5. 写保护功能 : 内置 WP 引脚

    • WP 接 VCC(高电平):存储器进入写保护,仅能读、无法写;
    • WP 接 GND(低电平):允许正常读写操作,防止数据意外篡改。
  6. 页写模式 : 支持 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 电平 决定:

  1. 固定位 :24C02 的 I²C 设备地址高 4 位为 1010(协议规定);
  2. 引脚电平 :芯片的 A0、A1、A2 引脚均接 GND(低电平,对应二进制 0);
  1. 地址拼接 :设备地址格式为 1010 A2 A1 A0,代入电平后为 1010 000(二进制),换算为十六进制即 0xA0

4.补充GPIO BSRRBRR****寄存器使用

5. I2C****头文件内容

myiic.h:

cpp 复制代码
#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:

cpp 复制代码
#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 ==>SCL
	if(state){
		//利用BSRR拉好输出,控制ODR为1
		GPIOB->BSRR |= I2C_SCL_PIN;
	}else{
		//利用BRR拉低输出,控制ODR为0
		GPIOB->BRR |= I2C_SCL_PIN;
	}
}

void SDA_Set(u8 state){
	//PB7 ==> SDA
	if(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 ==> SCL
	return (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,以便从设备读取SDA
		SCL_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.循环发送数据到24c02
	for(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:

cpp 复制代码
#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);
	}
	
}

0voice · GitHub