EEPROM 读写操作完全指南(I2C 接口)

引言

本文基于 AT24C128AN 这颗 128K 的 EEPROM,来试验下 EEPROM 的基本读写操作。同款其他容量的 EEPROM 只需变更一些地址参数便可拿来就用,如果是其他品牌的,可能就得仔细对对。 本文使用的开发板是 anfulai v6,这套驱动和 bsp 也是非常方便移植,我们也会讲到先关部分的硬件电路,以供设计参考。

如果想了解 EEPROM 的一些基本特性、应用场景与历史发展,可以移步这篇文章:容量那么小的 EEPROM 有什么用?

本文涉及到的代码段和原理图一部分来自 anfulai 的资料,大家可以去anfulai 的论坛上下载。

硬件设计与硬件配置

下面我们来结合 .h 文件来看看 EEPROM 这一块的硬件设计。这里的代码全部都是硬件配置强相关的代码,包括地址配置、引脚配置和时钟配置,软件逻辑或者时序逻辑的代码会在下一个板块来讲。本设计中,MCU 为 STMF429BIT6,MCU 作为 Master 主设备,EEPROM 作为 Slave 从设备。

在这里插入图片描述

这里的 Pin Table 参考的是 Microchip 的 Datasheet

在这里插入图片描述
在这里插入图片描述

I2C 地址配置

开发板上使用的这颗 AT24C128AN 不知道是什么牌子的,其中 A[0:2] 为 I2C 设备的地址,开发板中全部进行了下拉接地。

在这里插入图片描述

但开发板中的 PIN3 的引脚名是NC,也不知道为什么。原理图里标注的地址 0xC0 有误,去看了下 bsp_i2c_eeprom_24xx.h 这个文件,设备地址为 0xA0,也就是 1010 000x,我们看到 Microchip 的 Datasheet,其中 Bit [7:4] 固定为 1010,Bit [3:1] 对应 A [2:0],本设计中全部接地,故为 000。

#ifdef AT24C128
	#define EE_MODEL_NAME		"AT24C128"
	#define EE_DEV_ADDR			0xA0		/* 设备地址 */
	#define EE_PAGE_SIZE		64			/* 页面大小(字节) */
	#define EE_SIZE				(16*1024)	/* 总容量(字节) */
	#define EE_ADDR_BYTES		2			/* 地址字节个数 */
#endif

在这里插入图片描述

I2C GPIO配置

EEPROM 在开发板上被接在了 I2C1 总线,MCU到各个 I2C 设备均有串联电阻以方便调试,这个 I2C 总线上还接了很多其他外设,硬件设计上需要注意避免地址冲突。EEPROM 连接在 PB6 和PB9 引脚,可以在 bsp_i2c_gpio.c 中确认宏定义。

在这里插入图片描述

/* 定义I2C总线连接的GPIO端口, 用户只需要修改下面4行代码即可任意改变SCL和SDA的引脚 */
#define I2C_SCL_GPIO	GPIOB			/* 连接到SCL时钟线的GPIO */
#define I2C_SDA_GPIO	GPIOB			/* 连接到SDA数据线的GPIO */

#define I2C_SCL_PIN		GPIO_PIN_6			/* 连接到SCL时钟线的GPIO */
#define I2C_SDA_PIN		GPIO_PIN_9			/* 连接到SDA数据线的GPIO */

在初始化阶段,我们还需要对引脚的状态进行定义,我们看到 bsp_i2c_gpio.c 中的 I2C 初始化函数,我们进一步查看 GPIO_InitTypeDef,就可以看到一个 GPIO 引脚通常包含的可配置项,这个结构体定义在 stm32f4xx_hal_gpio.h 中。

void bsp_InitI2C(void)
{
	GPIO_InitTypeDef gpio_init;

	/* 第1步:打开GPIO时钟 */
	ALL_I2C_GPIO_CLK_ENABLE();
	
	gpio_init.Mode = GPIO_MODE_OUTPUT_OD;	/* 设置开漏输出 */
	gpio_init.Pull = GPIO_NOPULL;			/* 上下拉电阻不使能 */
	gpio_init.Speed = GPIO_SPEED_FREQ_LOW;	// GPIO_SPEED_FREQ_HIGH;  /* GPIO速度等级 */
	
	gpio_init.Pin = I2C_SCL_PIN;	
	HAL_GPIO_Init(I2C_SCL_GPIO, &gpio_init);	
	
	gpio_init.Pin = I2C_SDA_PIN;	
	HAL_GPIO_Init(I2C_SDA_GPIO, &gpio_init);	

	/* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
	i2c_Stop();
}

可以看到,GPIO 的初始化类型定义包括:引脚、模式、上下拉和速率。对于 I2C 而言,我们配置开漏输出,即依赖于外部上拉(如原理图所示,SCL SDA 外部均有电阻上拉至 3.3V),同时不使能上拉电阻,并将 GPIO 速度等级设置为 LOW。这里的速度等级,从硬件上来看,应该是指驱动能力。具体有哪些配置可以选择,每个配置分别是什么含义,可以到 stm32f4xx_hal_gpio.h 里面去找。

/** 
  * @brief GPIO Init structure definition  
  */ 
typedef struct
{
  uint32_t Pin;       /*!< Specifies the GPIO pins to be configured.
                           This parameter can be any value of @ref GPIO_pins_define */
  uint32_t Mode;      /*!< Specifies the operating mode for the selected pins.
                           This parameter can be a value of @ref GPIO_mode_define */
  uint32_t Pull;      /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
                           This parameter can be a value of @ref GPIO_pull_define */
  uint32_t Speed;     /*!< Specifies the speed for the selected pins.
                           This parameter can be a value of @ref GPIO_speed_define */
  uint32_t Alternate;  /*!< Peripheral to be connected to the selected pins. 
                            This parameter can be a value of @ref GPIO_Alternate_function_selection */
}GPIO_InitTypeDef;

I2C 时钟配置

这个板块是讲硬件配置,这当中必不可少的自然是时钟,I2C 的时钟由主设备发给从设备。我们首先需要考虑的是,这个时钟从哪里来,这个时钟的路径是怎样的?v6这个开发板的 bsp 使用了 GPIO 模拟12C,首先我们来看时钟树:

在这里插入图片描述

在 bsp_i2c_gpio.c 中可以看到时钟使能函数的定义,进一步进入到 stm32_hal_rcc.h 中,可以看到该函数使用 SET_BIT 宏将 RCC 的 AHB1ENR(AHB1外设使能寄存器)中的 GPIOBEN 位设置为1,从而使能 GPIOB 端口的时钟。

/* Exported macro ------------------------------------------------------------*/
/** @defgroup RCC_Exported_Macros RCC Exported Macros
  * @{
  */

/** @defgroup RCC_AHB1_Clock_Enable_Disable AHB1 Peripheral Clock Enable Disable
  * @brief  Enable or disable the AHB1 peripheral clock.
  * @note   After reset, the peripheral clock (used for registers read/write access)
  *         is disabled and the application software has to enable this clock before
  *         using it.
  * @{
  */

#define __HAL_RCC_GPIOB_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)

现在我们来这个 AHB1 究竟是多少频率的时钟,在 bsp.c 中可以找到对于系统时钟的配置,其中 SYSCLK 也就是系统时钟,在上面的时钟框图中由 SW 输出的部分。其经由外部晶振输入,经过一轮轮的倍频,最终在进行多轮分频后排得到。具体公式为:
P L L 输出频率 = H S E 频率 × P L L N P L L M × P L L P PLL输出频率 = \frac{HSE频率×PLLN}{PLLM×PLLP} PLL输出频率=PLLM×PLLPHSE频率×PLLN

/*
*********************************************************************************************************
*	函 数 名: SystemClock_Config
*	功能说明: 初始化系统时钟
*            	System Clock source            = PLL (HSE)
*            	SYSCLK(Hz)                     = 168000000 (CPU Clock)
*            	HCLK = SYSCLK / 1              = 168000000 (AHB1Periph)
*            	PCLK2 = HCLK / 2               = 84000000  (APB2Periph)
*            	PCLK1 = HCLK / 4               = 42000000  (APB1Periph)
*            	HSE Frequency(Hz)              = 25000000
*           	PLL_M                          = 25
*            	PLL_N                          = 336
*            	PLL_P                          = 2
*            	PLL_Q                          = 4
*            	VDD(V)                         = 3.3
*            	Flash Latency(WS)              = 5
*	形    参: 无
*	返 回 值: 无
*********************************************************************************************************
*/
static void SystemClock_Config(void)
{
	RCC_ClkInitTypeDef RCC_ClkInitStruct;
	RCC_OscInitTypeDef RCC_OscInitStruct;

	
	/* 芯片内部的LDO稳压器输出的电压范围,选用的PWR_REGULATOR_VOLTAGE_SCALE1 */
	__HAL_RCC_PWR_CLK_ENABLE();
	__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

	/* 使能HSE,并选择HSE作为PLL时钟源 */
	RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
	RCC_OscInitStruct.HSEState = RCC_HSE_ON;
	RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
	RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
	RCC_OscInitStruct.PLL.PLLM = 8;
	RCC_OscInitStruct.PLL.PLLN = 336;
	RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
	RCC_OscInitStruct.PLL.PLLQ = 4;
	if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
	{
        Error_Handler(__FILE__, __LINE__);
	}

	/* 
       选择PLL的输出作为系统时钟
		HCLK = SYSCLK / 1  (AHB1Periph)
		PCLK2 = HCLK / 2   (APB2Periph)
		PCLK1 = HCLK / 4   (APB1Periph)
    */
	RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
								  |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
	RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
	RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
	RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
	RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

	/* 此函数会更新SystemCoreClock,并重新配置HAL_InitTick */
	if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
	{
        Error_Handler(__FILE__, __LINE__);
	}

    /* 使能SYS时钟和IO补偿 */
	__HAL_RCC_SYSCFG_CLK_ENABLE() ;

	HAL_EnableCompensationCell();
}

对于 I2C 而言, bsp_i2c_gpio.c 中定义了 delay 函数,在进行 I2C 读写时,通过 delay 函数对信号状态保持时间进行定义,进而控制通信频率。

static void i2c_Delay(void)
{
	/* 
		CPU主频168MHz时,在内部Flash运行, MDK工程不优化。用台式示波器观测波形。
		循环次数为5时,SCL频率 = 1.78MHz (读耗时: 92ms, 读写正常,但是用示波器探头碰上就读写失败。时序接近临界)
		循环次数为10时,SCL频率 = 1.1MHz (读耗时: 138ms, 读速度: 118724B/s)
		循环次数为30时,SCL频率 = 440KHz, SCL高电平时间1.0us,SCL低电平时间1.2us

		上拉电阻选择2.2K欧时,SCL上升沿时间约0.5us,如果选4.7K欧,则上升沿约1us

		实际应用选择400KHz左右的速率即可
	*/
	//for (i = 0; i < 30; i++);
	//for (i = 0; i < 60; i++);
	//bsp_DelayUS(2); 229.57KHz时钟
	bsp_DelayUS(2);
}

/*
*********************************************************************************************************
*	函 数 名: i2c_SendByte
*	功能说明: CPU向I2C总线设备发送8bit数据
*	形    参:  _ucByte : 等待发送的字节
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_SendByte(uint8_t _ucByte)
{
	uint8_t i;

	/* 先发送字节的高位bit7 */
	for (i = 0; i < 8; i++)
	{
		if (_ucByte & 0x80)
		{
			I2C_SDA_1();
		}
		else
		{
			I2C_SDA_0();
		}
		i2c_Delay();
		I2C_SCL_1();
		i2c_Delay();
		I2C_SCL_0();
		I2C_SCL_0();	/* 2019-03-14 针对GT811电容触摸,添加一行,相当于延迟几十ns */
		if (i == 7)
		{
			 I2C_SDA_1(); // 释放总线
		}
		_ucByte <<= 1;	/* 左移一个bit */	
	}
}

供电与写保护

硬件上还有要关心的就是供电和写保护引脚了,开发板的设计中直接将写保护接地,也就是不使用写保护功能。这颗 EEPROM 应该是宽压供电,只要在 Datasheet 标称的供电范围内进行供电即可。这里要注意的是,I2C 的上拉供电应尽量不要早于 EEPROM芯片本身的供电,否则可能会出现倒灌。

在这里插入图片描述

I2C协议基本概念

TI 的这篇文章写的比较全面了,我将其中协议相关的内容摘出来看看。

I2C 的开始与结束

I2C通信是由控制器设备用一个I2C开始条件启动的。如果总线是空闲的,一个I2C控制器通过发送一个I2C开始信号来占用总线进行通信。为此,控制器设备首先将SDA拉低,然后将SCL拉低。这个顺序表明控制器设备正在占用I2C总线进行通信,迫使总线上的其他控制器设备暂停它们的通信。
当控制器设备完成通信时,SCL释放为高,然后SDA释放为高。这表示一个I2C停止条件。这释放了总线,允许其他控制器进行通信,或者允许同一个控制器与另一个设备通信。下图显示了I2C开始和停止的协议。

在这里插入图片描述

逻辑1和逻辑0

I2C使用一系列一和零来进行串行通信。SDA用于数据位,而SCL是串行时钟,用于定时比特序列。当SDA释放线路时,允许上拉电阻将线路拉到高电平,发送一个逻辑一。当SDA拉低线路时,将线路设置为接近地线的低电平,发送一个逻辑零。下图显示了I2C通信中数字一和零的表示。当SCL被脉冲化时,接收一和零。对于有效的位,SDA在该位的上升沿和下降沿之间不会改变。SDA在SCL的上升沿和下降沿之间变化可以被解释为I2C总线上的开始或停止条件。

在这里插入图片描述

I2C通信帧

I2C协议将通信分解为帧。通信始于控制器设备在开始条件后发送地址帧。地址帧后面是一到多个数据帧,每个数据帧由一个字节组成。每个帧还有一个应答位,用于提醒控制器目标设备或控制器设备已接收到通信。图3-3展示了两个I2C通信帧的示意图。在地址帧开始时,控制器设备发起一个开始条件。控制器设备首先将SDA拉低,然后拉低SCL以开始。这允许控制器设备在总线上声明控制权,而不受其他控制器设备的干扰。每个I2C目标设备都有一个相关的I2C地址。当开始与特定目标设备通信时,控制器使用目标设备地址来发送或接收以下I2C帧中的数据。I2C地址由7位组成,I2C总线上的设备,每个设备都有一个唯一的地址。

在这里插入图片描述

7位地址意味着 2 7 2^7 27(或128)个唯一地址。然而,有几个保留的I2C地址限制了可能的设备数量。保留地址在第5节中讨论。地址以SDA作为数据和SCL作为串行时钟发送。有了这些信息,你可以阅读设备的I2C通信,并理解控制器设备和目标设备之间来回发送的内容。
这个帧的第8位是读写(R/W)位。如果该位是1,控制器请求从目标设备读取数据。如果该位是0,控制器请求向目标设备写入数据。
在任何通信字节之后,使用额外的第9位来验证通信是否成功。在地址字节通信结束时,目标设备在SCL脉冲期间将SDA拉低,以向控制器指示地址已被接收。这被称为应答(ACK)位。如果该位是高电平,则没有目标设备接收到地址,通信失败。如果该位是高电平,这被称为非应答(NACK),没有ACK。
地址帧后面是一到多个数据帧。这些帧一次发送一个字节。在每个数据字节传输之后,还有另一个ACK。如果数据字节是向设备的写入,那么目标设备将SDA拉低以确认传输。如果数据字节是从设备的读取,控制器将SDA拉低以确认已接收数据。ACK是一个有用的调试工具。缺少这个位可能表明目标外设没有接收到正确的I2C地址进行通信,或者控制器外设没有接收到预期的数据。
通信完成后,控制器发出I2C停止条件。SCL首先释放,然后SDA释放。控制器使用STOP来表示通信已完成,并释放I2C总线。
这是控制器设备和目标设备之间任何I2C通信的基本协议。通信可以包含多于一个字节的数据。在某些情况下,如果目标设备有多个数据和配置寄存器,从设备读取可以开始于向设备写入,以指示要读取哪个寄存器。以下各节展示了如何从不同的数据转换器设备读取和写入的示例。

EEPROM

接下来,我们对 bsp_i2c_eeprom_24xx.c 中的函数进行分析:

检测 EEPROM 是否在线

ee_Check 用于检测 EEPROM 是否正常,其判断方法是通过 i2c_CheckDevice 函数发送发送设备地址+读写控制bit(0 = w, 1 = r),并检测ACK信号,若 Device 有 ACK 应答,即说明 EEPROM 设备在线。

/*
*********************************************************************************************************
*	函 数 名: ee_CheckOk
*	功能说明: 判断串行EERPOM是否正常
*	形    参:  无
*	返 回 值: 1 表示正常, 0 表示不正常
*********************************************************************************************************
*/
uint8_t ee_CheckOk(void)
{
	if (i2c_CheckDevice(EE_DEV_ADDR) == 0)
	{
		return 1;
	}
	else
	{
		/* 失败后,切记发送I2C总线停止信号 */
		i2c_Stop();
		return 0;
	}
}
uint8_t i2c_CheckDevice(uint8_t _Address)
{
	uint8_t ucAck;

	if (I2C_SDA_READ() && I2C_SCL_READ())
	{
		i2c_Start();		/* 发送启动信号 */

		/* 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传 */
		i2c_SendByte(_Address | I2C_WR);
		ucAck = i2c_WaitAck();	/* 检测设备的ACK应答 */

		i2c_Stop();			/* 发送停止信号 */

		return ucAck;
	}
	return 1;	/* I2C总线异常 */
}

EEPROM 读数据

读取数据的步骤如下:

  1. 开始通信:发送I2C总线开始信号,初始化与EEPROM的通信。
  2. 发送写入控制字节:发送包含EEPROM设备地址和写入标志的控制字节,同时等待并确认收到EEPROM的应答(ACK)。
  3. 发送地址:根据EEPROM的地址长度,顺序发送地址的高位字节和低位字节,每次发送后都等待并确认收到EEPROM的应答。
  4. 重新启动通信:发送I2C总线开始信号,准备从EEPROM读取数据。
  5. 发送读取控制字节:发送包含EEPROM设备地址和读取标志的控制字节,同时等待并确认收到EEPROM的应答。
  6. 读取数据:循环读取指定长度的数据字节。对于循环中的每个字节(除了最后一个),使用ACK确认接收;对于最后一个字节,使用NACK表示结束接收。
  7. 存储数据:将读取的每个字节存储到提供的缓冲区_pReadBuf中。
  8. 结束通信:发送I2C总线停止信号,结束与EEPROM的通信。
  9. 返回结果:如果数据读取成功,返回1;如果在任何步骤中未收到EEPROM的应答,返回0表示失败。
/*
*********************************************************************************************************
*	函 数 名: ee_ReadBytes
*	功能说明: 从串行EEPROM指定地址处开始读取若干数据
*	形    参:  _usAddress : 起始地址
*			 _usSize : 数据长度,单位为字节
*			 _pReadBuf : 存放读到的数据的缓冲区指针
*	返 回 值: 0 表示失败,1表示成功
*********************************************************************************************************
*/
uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i;

	/* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */

	/* 第1步:发起I2C总线启动信号 */
	i2c_Start();

	/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
	i2c_SendByte(EE_DEV_ADDR | I2C_WR);	/* 此处是写指令 */

	/* 第3步:等待ACK */
	if (i2c_WaitAck() != 0)
	{
		goto cmd_fail;	/* EEPROM器件无应答 */
	}

	/* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
	if (EE_ADDR_BYTES == 1)
	{
		i2c_SendByte((uint8_t)_usAddress);
		if (i2c_WaitAck() != 0)
		{
			goto cmd_fail;	/* EEPROM器件无应答 */
		}
	}
	else
	{
		i2c_SendByte(_usAddress >> 8);
		if (i2c_WaitAck() != 0)
		{
			goto cmd_fail;	/* EEPROM器件无应答 */
		}

		i2c_SendByte(_usAddress);
		if (i2c_WaitAck() != 0)
		{
			goto cmd_fail;	/* EEPROM器件无应答 */
		}
	}

	/* 第6步:重新启动I2C总线。下面开始读取数据 */
	i2c_Start();

	/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
	i2c_SendByte(EE_DEV_ADDR | I2C_RD);	/* 此处是读指令 */

	/* 第8步:等待ACK */
	if (i2c_WaitAck() != 0)
	{
		goto cmd_fail;	/* EEPROM器件无应答 */
	}

	/* 第9步:循环读取数据 */
	for (i = 0; i < _usSize; i++)
	{
		_pReadBuf[i] = i2c_ReadByte();	/* 读1个字节 */

		/* 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack */
		if (i != _usSize - 1)
		{
			i2c_Ack();	/* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
		}
		else
		{
			i2c_NAck();	/* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
		}
	}
	/* 发送I2C总线停止信号 */
	i2c_Stop();
	return 1;	/* 执行成功 */

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
	/* 发送I2C总线停止信号 */
	i2c_Stop();
	return 0;
}

这里有一个语法上的点值得注意,函数传入的形参 uint16_t _usAddress 为 uint16 的,但是 i2c_SendByte() 的输入为 uint8,当一个16位的 uint16_t 赋值给8位的 uint8_t 时,会发生隐式类型转换,只保留 uint16_t 值的低8位。在第二次地址字节发送时,执行了 _usAddress >> 8 操作,这将执行算术右移操作,将 _usAddress 中的所有位向右移动8位。这种操作通常用于获取一个16位值的高8位。

EEPROM 写数据

ee_WriteBytes 函数的目的是向串行 EEPROM 的指定地址写入一定数量的数据。写串行 EEPROM 不像读操作可以连续读取很多字节,每次写操作只能在同一个 page。从DS上可以得知:The AT24C128C is internally organized as 256 pages of 64 bytes each. The AT24C256C is internally organized as 512 pages of 64 bytes each.

以下是该函数的逻辑步骤:

  1. 初始化变量:定义循环计数器 i 和 m,以及用于写入操作的地址变量 usAddr。
  2. 写入前准备:通过发送停止信号 i2c_Stop() 来结束上一次写入操作(如果有的话)。
  3. 检查EEPROM准备状态:循环检查EEPROM是否已完成上一次写入操作,通常等待时间小于10毫秒。
  4. 发送地址:如果是写入序列的第一个字节或到达新的页开始位置,重新发送EEPROM设备的地址和写入控制位。
  5. 地址传输:根据EEPROM的地址长度,发送 usAddr 的高位字节(如果需要)和低位字节,每次发送后都等待EEPROM的应答。
  6. 写入数据:循环遍历要写入的数据缓冲区 _pWriteBuf,对每个字节执行以下操作:
    • 发送当前字节数据 _pWriteBuf[i]。
    • 等待并接收EEPROM的应答。
  7. 更新地址:每次写入一个字节后,更新 usAddr 为下一个要写入的地址。
  8. 写入完成后检查:写入所有数据后,再次发送停止信号,并检查EEPROM是否准备好接受新的写入操作。
  9. 超时处理:如果在指定的循环次数内未收到EEPROM的应答,跳转到 cmd_fail 标签,表示写入超时。
  10. 成功结束:如果所有数据都成功写入,发送停止信号并返回1表示成功。
  11. 错误处理:如果在任何点上通信失败,发送停止信号,返回0表示失败。
/*
*********************************************************************************************************
*	函 数 名: ee_WriteBytes
*	功能说明: 向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率
*	形    参:  _usAddress : 起始地址
*			 _usSize : 数据长度,单位为字节
*			 _pWriteBuf : 存放数据的缓冲区指针
*	返 回 值: 0 表示失败,1表示成功
*********************************************************************************************************
*/
uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i,m;
	uint16_t usAddr;

	/*
		写串行EEPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。
		对于24xx02,page size = 8
		简单的处理方法为:按字节写操作模式,每写1个字节,都发送地址。
	*/

	usAddr = _usAddress;
	for (i = 0; i < _usSize; i++)
	{
		/* 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址 */
		if ((i == 0) || (usAddr & (EE_PAGE_SIZE - 1)) == 0)
		{
			/* 第0步:发停止信号,启动内部写操作 */
			i2c_Stop();

			/* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms
				CLK频率为200KHz时,查询次数为30次左右
			*/
			for (m = 0; m < 1000; m++)
			{
				/* 第1步:发起I2C总线启动信号 */
				i2c_Start();

				/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
				i2c_SendByte(EE_DEV_ADDR | I2C_WR);	/* 此处是写指令 */

				/* 第3步:发送一个时钟,判断器件是否正确应答 */
				if (i2c_WaitAck() == 0)
				{
					break;
				}
			}
			if (m  == 1000)
			{
				goto cmd_fail;	/* EEPROM器件写超时 */
			}

			/* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
			if (EE_ADDR_BYTES == 1)
			{
				i2c_SendByte((uint8_t)usAddr);
				if (i2c_WaitAck() != 0)
				{
					goto cmd_fail;	/* EEPROM器件无应答 */
				}
			}
			else
			{
				i2c_SendByte(usAddr >> 8);
				if (i2c_WaitAck() != 0)
				{
					goto cmd_fail;	/* EEPROM器件无应答 */
				}

				i2c_SendByte(usAddr);
				if (i2c_WaitAck() != 0)
				{
					goto cmd_fail;	/* EEPROM器件无应答 */
				}
			}
		}

		/* 第6步:开始写入数据 */
		i2c_SendByte(_pWriteBuf[i]);

		/* 第7步:发送ACK */
		if (i2c_WaitAck() != 0)
		{
			goto cmd_fail;	/* EEPROM器件无应答 */
		}

		usAddr++;	/* 地址增1 */
	}

	/* 命令执行成功,发送I2C总线停止信号 */
	i2c_Stop();

	/* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms
		CLK频率为200KHz时,查询次数为30次左右
	*/
	for (m = 0; m < 1000; m++)
	{
		/* 第1步:发起I2C总线启动信号 */
		i2c_Start();

		/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */	
		#if EE_ADDR_A8 == 1
			i2c_SendByte(EE_DEV_ADDR | I2C_WR | ((_usAddress >> 7) & 0x0E));	/* 此处是写指令 */
		#else		
			i2c_SendByte(EE_DEV_ADDR | I2C_WR);	/* 此处是写指令 */
		#endif

		/* 第3步:发送一个时钟,判断器件是否正确应答 */
		if (i2c_WaitAck() == 0)
		{
			break;
		}
	}
	if (m  == 1000)
	{
		goto cmd_fail;	/* EEPROM器件写超时 */
	}
	
	/* 命令执行成功,发送I2C总线停止信号 */
	i2c_Stop();
	return 1;

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
	/* 发送I2C总线停止信号 */
	i2c_Stop();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值