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;
}
相关推荐
一路往蓝-Anbo6 小时前
第三篇:ADC 与模拟前端
stm32·嵌入式硬件·嵌入式·硬件设计
努力小周9 小时前
STM32智能安防系统
c语言·stm32·单片机·嵌入式硬件·物联网·计算机网络·pcb工艺
袁小皮皮不皮10 小时前
1.HCIP BFD 学习笔记(优化版)
服务器·网络·笔记·网络协议·学习·智能路由器·ip
装不满的克莱因瓶10 小时前
【自动驾驶领域】学习 Cityscapes 数据集——城市街景语义理解的标准基准
人工智能·pytorch·python·深度学习·学习·机器学习·自动驾驶
清辞85311 小时前
产品经理需求推进流程
大数据·深度学习·学习·产品经理
华科大胡子11 小时前
在STM32上跑通TinyML
stm32·单片机·嵌入式硬件
YM52e11 小时前
鸿蒙PC ArkTS 声明合并问题深度解析与最佳实践
学习·华为·harmonyos·鸿蒙·鸿蒙系统
海兰12 小时前
【实用程序】电商销售分析仪表盘 — 从零搭建一个AI参与的全栈数据洞察系统
人工智能·学习·算法
iCxhust12 小时前
C#进程管理程序
开发语言·汇编·stm32·单片机·c#·微机原理
ken223213 小时前
在 Libreoffice Calc中输入自定义表情字符时,需要保存之后,才能正常显示
学习