文章目录
引言
本文基于 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 读数据
读取数据的步骤如下:
- 开始通信:发送I2C总线开始信号,初始化与EEPROM的通信。
- 发送写入控制字节:发送包含EEPROM设备地址和写入标志的控制字节,同时等待并确认收到EEPROM的应答(ACK)。
- 发送地址:根据EEPROM的地址长度,顺序发送地址的高位字节和低位字节,每次发送后都等待并确认收到EEPROM的应答。
- 重新启动通信:发送I2C总线开始信号,准备从EEPROM读取数据。
- 发送读取控制字节:发送包含EEPROM设备地址和读取标志的控制字节,同时等待并确认收到EEPROM的应答。
- 读取数据:循环读取指定长度的数据字节。对于循环中的每个字节(除了最后一个),使用ACK确认接收;对于最后一个字节,使用NACK表示结束接收。
- 存储数据:将读取的每个字节存储到提供的缓冲区_pReadBuf中。
- 结束通信:发送I2C总线停止信号,结束与EEPROM的通信。
- 返回结果:如果数据读取成功,返回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.
以下是该函数的逻辑步骤:
- 初始化变量:定义循环计数器 i 和 m,以及用于写入操作的地址变量 usAddr。
- 写入前准备:通过发送停止信号 i2c_Stop() 来结束上一次写入操作(如果有的话)。
- 检查EEPROM准备状态:循环检查EEPROM是否已完成上一次写入操作,通常等待时间小于10毫秒。
- 发送地址:如果是写入序列的第一个字节或到达新的页开始位置,重新发送EEPROM设备的地址和写入控制位。
- 地址传输:根据EEPROM的地址长度,发送 usAddr 的高位字节(如果需要)和低位字节,每次发送后都等待EEPROM的应答。
- 写入数据:循环遍历要写入的数据缓冲区 _pWriteBuf,对每个字节执行以下操作:
- 发送当前字节数据 _pWriteBuf[i]。
- 等待并接收EEPROM的应答。
- 更新地址:每次写入一个字节后,更新 usAddr 为下一个要写入的地址。
- 写入完成后检查:写入所有数据后,再次发送停止信号,并检查EEPROM是否准备好接受新的写入操作。
- 超时处理:如果在指定的循环次数内未收到EEPROM的应答,跳转到 cmd_fail 标签,表示写入超时。
- 成功结束:如果所有数据都成功写入,发送停止信号并返回1表示成功。
- 错误处理:如果在任何点上通信失败,发送停止信号,返回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;
}