STM32学习(MCU控制)(I2C 模拟)

文章目录

    • IIC
        • [1. IIC](#1. IIC)
          • [1.1 IIC or I2C 概述](#1.1 IIC or I2C 概述)
          • [1.2 IIC 特征](#1.2 IIC 特征)
          • [1.3 IIC 数据帧分析](#1.3 IIC 数据帧分析)
          • [1.4 起始位和停止位](#1.4 起始位和停止位)
          • [1.5 IIC 数据传递 0 和 1](#1.5 IIC 数据传递 0 和 1)
          • [1.6 读写标志位](#1.6 读写标志位)
          • [1.7 应答信号](#1.7 应答信号)
          • [1.8 寄存器地址数据内容](#1.8 寄存器地址数据内容)
          • [1.9 数据内容【发送/写操作】到 IIC 中](#1.9 数据内容【发送/写操作】到 IIC 中)
          • [1.10 读取数据操作数据帧内容](#1.10 读取数据操作数据帧内容)
          • [1.11 IIC 协议核心内容总结](#1.11 IIC 协议核心内容总结)
        • [2. IIC 操作 EEPROM 存储设备](#2. IIC 操作 EEPROM 存储设备)
          • [2.1 EEPROM 设备概述](#2.1 EEPROM 设备概述)
          • [2.2 24C02 芯片](#2.2 24C02 芯片)
          • [2.3 24C02 存储芯片原理图分析](#2.3 24C02 存储芯片原理图分析)
          • [2.4 补充 GPIO BSRR 和 BRR 寄存器使用](#2.4 补充 GPIO BSRR 和 BRR 寄存器使用)
          • [2.5 I2C 头文件内容](#2.5 I2C 头文件内容)
          • [2.6 myiic.c 代码实现](#2.6 myiic.c 代码实现)

IIC

1. IIC
1.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
1.2 IIC 特征
特性 描述
优点 引脚极少 (2线),协议简单 ,支持多主多从 ,有硬件应答机制,业界支持广泛,成本低。
缺点 速度较慢 (相对于SPI),通信距离短 (通常限于同一板卡),软件实现较复杂 (需严格时序),地址冲突可能(7位地址仅128个)。
适用场景 板内IC间的低速、短距离通信,特别是需要连接大量传感器、驱动芯片的嵌入式系统。
1.3 IIC 数据帧分析
  • 起始位【1位】:当前 IIC 触发工作模式
  • 设备地址【7位】:MCU 利用设备地址,选择当前 IIC 总线中,根据当前指定的地址,选择 IIC 总线中的对应设备,当前地址 7位地址,地址范围是0 ~ 127,理论 IIC 支持 128 个设备。
  • 读/写位【1位】:MCU 设备告知当前 IIC 总线,操作的目标设备是执行写入数据操作还是 MCU 读取设备数据内容。
  • 应答信号 ACK :当前指定 IIC 设备接收到对应 MCU 请求链接之后,明确当前操作形式,给予的应答行为。明确收到应答信号,MCU 和对应 IIC 设备连接完成。
  • 寄存器地址【8位】 :当前图例对应写入数据操作,寄存器地址对应当前 IIC 设备中指定数据存储设备地址,8 位地址为可以支持的地址范围是 0 ~ 255 ,IIC 设备按照当前协议要求,可以支持的最大数据容量为 256 Byte
  • 应答信号 ACK :IIC 设备给予明确的应答信号,告示 MCU 收到指定寄存器地址数据
  • 数据 Data【8位】 :当前写入操作,MCU 将明确的一个字节数据内容发送给 IIC 设备,IIC 设备根据【寄存器地址】 + 【数据】对当前设备中的指定存储器位置进行数据写入操作。
  • 停止位【1位】:当前一次 IIC 操作结束
1.4 起始位和停止位
  • IIC 是基于 SDA 和 SCL 两根通信完成的数据传递过程
    • SDA ==> 数据线
    • SCL ==> 时钟线
  • IIC 空闲状态
    • SCL 时钟线处于高电平状态
    • SDA 数据线在时钟线高电平状态下,同时处于高电平状态。
  • 起始信号 Start
    • SCL 和 SDA 同时处于高电平状态。
    • SCL 保持高电平状态的情况下,SDA 进行了【高电平到低电平】跳变
    • IIC 触发起始信号
  • 停止信号 End
    • SCL 处于高电平状态,SDA 处于低电平状态
    • SCL 保持高电平状态的情况下,SDA 进行了【低电平到高电平】跳变
    • IIC 触发停止信号
1.5 IIC 数据传递 0 和 1
  • IIC 数据传递 1
    • SCL 和 SDA 处于同一个**【时钟周期中】**,不存在上升沿或者下降沿跳变,同时处于高电平模式,IIC 数据传递 1 或者 逻辑 1
    • 一般情况下 SDA 会在时钟前半个周期就进入到高电平状态,而且是在时钟后半个周期始终保存高电平状态。保证数据传递的完整性,避免 SDA 跳变操作触发起始或者终止信号。
  • IIC 数据传递 0
    • SCL 和 SDA 处于同一个**【时钟周期中】**,不存在上升沿或者下降沿跳变,SCL 时钟处于高电平状态,此时 SDA 在整个时钟线周期中,处于低电平状态,IIC 数据传递 0 或者 逻辑 0
    • 一般情况下 SDA 会在时钟前半个周期就进入到低电平状态,而且是在时钟后半个周期始终保存低电平状态。保证数据传递的完整性,避免 SDA 跳变操作触发起始或者终止信号
  • 根据数据传递规则,对应的 IIC 时序传递数据图例
1.6 读写标志位
  • IIC 在发送设备地址时,整个地址数据组成有【7位设备地址】 + 【1 位读取标志】。传递的数据是一个完整的 1 字节
  • 假设当前设备地址对应 101 0000 【对应发送数据的前 7 位】,可以当前地址进行二进制优化 1010 000X
  • 标志数据要求
    • 0 :表示主设备向从设备进行数据写入【写操作】
    • 1 :表示主设备从从设备中读取数据【读操作】
  • 假设当前设备地址为 101 0000
    • IIC 发送 1010 0000 表示写入数据到目标设备 ==> 0xA0
    • IIC 发送 1010 0001 表示读取目标设备中数据 ==> 0xA1
1.7 应答信号
  • 当主机发送一组数据之后,包括设备地址,寄存器地址... 当前主机设备进入等待状态,从机设备需要响应一个应答数据。
  • 数据发送完毕之后,对应 SDA 拉低,处于低电平状态,在一个时钟周期中,检测到当前 SDA 的电平情况。判断从机的应答状态。
应答信号 SDA 电平情况 对应情况
ACK 0, 低电平 从机明确收到了主机发送的相关数据,应答确认
NACK 1,高电平 从机未收到明确数据,或者设备损坏。
1.8 寄存器地址数据内容
  • 寄存器地址数据依然遵循 IIC 数据传递 0 和 1 的方式。数据支持的范围 0000 0000 ~ 1111 1111
  • 明确当前 IIC 主机操作目标设备中寄存器地址,根据读写标志位不同,可以对当前地址进行写入数据操作或者读取数据操作。
1.9 数据内容【发送/写操作】到 IIC 中
  • 寄存器地址之后,明确收到应答信号
  • 此时进行发送数据内容到目标 IIC 设备,发送数据对应 8 位/ 1 字节,数据内容中的 0 和 1 按照 IIC 的数据传递 1 或者 0 的方式完成
1.10 读取数据操作数据帧内容
  • 读取数据和写入数据数据帧对比
  • 读取数据帧分析
  • 主机到从机操作,利用设备地址找到目标 IIC 设备,同时选择【写入操作】,将目标读取的数据寄存器地址告知 IIC 设备。
  • 重新开始新的 IIC 通信
  • 主机到从机操作,利用设备地址再次找到目标 IIC 设备,此时地址信息中,明确告知操作为**【读取操作】**,指定地址 IIC 设备收到相关的信息内容
  • 从机到主机操作,IIC 设备会将**【之前主机指定寄存器地址】**对应的数据发送给主机,
1.11 IIC 协议核心内容总结
2. IIC 操作 EEPROM 存储设备
2.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。
特性 EEPROM FLASH
擦除单位 字节 扇区/块(通常为512字节或更大)
写入速度 慢(按字节写) (按页写,通常512字节一页)
结构复杂性 高(每个单元有独立选择管) 较低(共享选择管,单元更小)
容量/密度 小(通常KB级到MB级) (通常MB级到GB级甚至更大)
成本(每比特)
主要应用 存储小量、需频繁修改的数据 存储大量、需整体更新的数据(如程序代码、文件)
2.2 24C02 芯片

24C02 是一颗非常经典和常见的串行 EEPROM 芯片,由多家半导体公司(如 Microchip, ON Semiconductor, ST等)生产。其命名规则通常为 24C02,其中 "24" 代表I²C协议家族,"C" 代表系列,"02" 代表容量为 2 Kbit ==> 256 字节。


  • 24C02 核心特性
    • 容量2 Kbit 。 这相当于 256 字节 (因为 2 * 1024 / 8 = 256)。它的地址范围是 0x00 到 0xFF。
    • 接口I²C (Inter-Integrated Circuit)串行接口。这是一种两线制协议,极大地节省了微控制器的引脚资源。
      • SDA: 串行数据线,用于双向数据传输。
      • SCL: 串行时钟线,由主设备(通常是MCU)产生。
    • 工作电压: 通常兼容宽电压范围,如 1.7V 到 5.5V,使其既能用于3.3V系统也能用于5V系统。
    • 擦写寿命 : 通常为 100万次 擦写循环。
    • 数据保存期 : 通常为 100年
    • 写保护功能 : 具备一个 WP 引脚。当此引脚接高电平(VCC)时,整个存储器将被写保护,无法被修改,只能读取。当接低电平(GND)时,允许正常读写操作。这是防止数据被意外篡改的重要功能。
    • 页写模式 : 支持 16字节 的页写操作。这意味着可以连续写入最多16个字节,比单字节写入效率高得多。
2.3 24C02 存储芯片原理图分析
  • 需要对 PB6 和 PB7 两个引脚进行配置
    • PB6 ==> SCL IIC 时钟线
    • PB7 ==> SDA IIC 数据线
  • EEPROM 芯片24C02 WP 引脚连接 GND ,当前芯片可以满足【读写操作】
  • 当前芯片引脚 A0 A1 A2 都是对应 GND,都是低电平,按照当前 24C02 存储芯片要求,对应的设备地址为 1010 A2A1A0 ==> 1010 000 ==> 设备地址可以认为是 0xA0
2.4 补充 GPIO BSRR 和 BRR 寄存器使用
2.5 I2C 头文件内容
c 复制代码
#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 EERPOM_WriteByte(u8 addr, u8 data);

/**
 * @brief 读取 EERPOM 一个字节数据
 * 
 * @param addr 目标寄存器存储当前数据的地址
 * @return 返回是读取到的数据内容
 */
u8 EERPOM_ReadByte(u8 addr);

/**
 * @brief 写入一页数据到 EERPOM 芯片中。一页数据最大 8 个字节
 * 
 * @param addr 目标寄存器存储当前数据的起始地址
 * @param data 目标写入到 EERPOM 存储器中的数据
 * @param len  目标写入到当前 EEPROM 中的数据个数
 * @return 写入成功返回 0,失败返回 1
 */
u8 EERPOM_WritePage(u8 addr, u8 *data, u8 len);

/**
 * @brief 读取 EERPOM 芯片中一页数据存储到 buffer 中 。一页数据最大 8 个字节
 * 
 * @param addr   目标寄存器读取数据的起始地址
 * @param buffer 存储 EERPOM 临时空间
 * @param len    临时空间空间字节数
 * @return 写入成功返回 0,失败返回 1
 */
u8 EERPOM_ReadPage(u8 addr, u8 *buffer, u8 len);

#endif
2.6 myiic.c 代码实现
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 空闲状态。
	*/
	GPIOB->BSRR |= (0x03 << 6);
}

void SCL_Set(u8 state)
{
	// PB6 ==> SCL
	if (state)
	{
		GPIOB->BSRR |= I2C_SCL_PIN; // 利用 BSRR 拉高输出,控制 ODR 为 1
	}
	else
	{
		GPIOB->BRR |= I2C_SCL_PIN; // 利用 BRR 拉低输出,控制 ODR 为 0
	}
}

void SDA_Set(u8 state)
{
	// PB7 ==> SDA
	if (state)
	{
		GPIOB->BSRR |= I2C_SDA_PIN; // 利用 BSRR 拉高输出,控制 ODR 为 1
	}
	else
	{
		GPIOB->BRR |= I2C_SDA_PIN; // 利用 BRR 拉低输出,控制 ODR 为 0
	}
}

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(); // 5 us 延时
	
	// SDA 拉低,在 SCL 处于高电平状态下拉低,触发 I2C 起始信号
	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(); // 5 us 延时
		
		/*
		data & 0x80(b1000 0000) 
		假设 data 为 1010 0011 
			& 0x80 ==> 1000 0000 ==> 非0 
			当前数据最高位为 1,需要 SDA 拉高
		
		假设 data 为 0010 0011 
			& 0x80 ==> 0000 0000 ==> 0
			当前数据最高位为 0,需要 SDA 拉低
		*/
		if (data & 0x80)
		{
			SDA_Set(1); // I2C 数据 1
		}
		else 
		{
			SDA_Set(0); // I2C 数据 0
		}
		I2C_Delay(); // 5 us 延时
		
		SCL_Set(1);  // SCL 拉高,I2C 根据时钟高电平情况下读取 SDA 数据判断 0 和 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 起始位
		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_ERROR;
}


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;
}
相关推荐
国科安芯6 小时前
基于ASM1042通信接口芯片的两轮车充电机性能优化研究
服务器·网络·人工智能·单片机·嵌入式硬件·性能优化
自由日记6 小时前
前端学习:选择器的类别
前端·javascript·学习
WaibiJiangzhi6 小时前
《动手学深度学习》学习笔记——02深度学习介绍
笔记·学习
A9better7 小时前
嵌入式开发学习日志42——stm32之SPI工作方式
stm32·单片机·嵌入式硬件·学习
Main. 247 小时前
从0到1学习Qt -- 信号和槽(一)
学习
qqxhb7 小时前
系统架构设计师备考第60天——嵌入式硬件体系&软件架构
单片机·嵌入式硬件·系统架构·存储器·处理器·可裁剪·强实时
D.....l8 小时前
STM32学习(MCU控制)(SysTick and TIM)
stm32·单片机·学习
python百炼成钢12 小时前
10.串口
linux·stm32·单片机·嵌入式硬件
驭风少年君14 小时前
《搭建属于自己的网站之网页前端学习》基础入门
前端·学习