STM32 通过软件模拟 I2C 驱动 24Cxx 系列存储器

目录


I2C 相关知识可以参考 IIC 通信协议详解

一、AT24CXXX 系列存储器介绍

1、基本信息

下表是 AT24CXXX 的容量

AT24C01,AT24C02,AT24C04,AT24C08,AT24C16,AT24C32,AT24C64,AT24C128,AT24C256... 不同的 xxx 代表不同的容量。

AT24CXXX bit容量 Byte容量
AT24C01 1Kbit 128Byte
AT24C02 2Kbit 256Byte
AT24C04 4Kbit 512Byte
AT24C08 8Kbit 1024Byte
AT24C16 16Kbit 2048Byte
AT24C32 32Kbit 4096Byte
AT24C64 64Kbit 8192Byte
AT24C128 128Kbit 16384Byte
AT24C256 256Kbit 32768Byte
AT24C512 512Kbit 65536Byte

下表是 AT24CXXX 的页内单元数

总容量(Byte容量) = 页数 × 页内字节单元数

AT24CXXX Byte容量 页数 页内字节单元数
AT24C01 128Byte 16页 8Byte
AT24C02 256Byte 32页 8Byte
AT24C04 512Byte 32页 16Byte
AT24C08 1024Byte 64页 16Byte
AT24C16 2048Byte 128页 16Byte
AT24C32 4096Byte 128页 32Byte
AT24C64 8192Byte 256页 32Byte
AT24C128 16384Byte 256页 64Byte
AT24C256 32768Byte 512页 64Byte
AT24C512 65536Byte 512页 128Byte

2、寻址方式

不是 I2C 地址,是存储器内部寻址

AT24CXXX 进行读写操作时,都得先访问存储地址、比如 AT24C04 写一个字节的 I2C 时序:

先发送设备地址,收到应答后再发送需要写数据的地址(WORD ADDRESS)。AT24C04 容量为 512Byte 则 WORD ADDRESS 只需要 9bit 就可以覆盖 512Byte 的数据地址。通俗的讲就是 512Byte 就占用了 512 个地址,一个 9bit 的数据范围为( 0 − 511 0-511 0−511)刚好 512,所以 512Byte 的字节地址需要一个 9bit 的数据来表示。

3、页地址与页内单元地址

比如 AT24C04 有 32 页每页 16 个字节,9bit 的地址数据对其寻址,低 4bit(D3-D0)为页内字节单元地址,高 5bit(D8-D4)为页地址。

如从第 16 页开始写,则 WORD ADDRESS = 0x0100(0001 0000 0000),则:

  • 000:地址无效位
  • 1 0000:5 位页地址
  • 0000:4 位页内单元地址

4、I2C 地址

I2C 通信需要先向从设备发送设备地址,AT24CXXX 芯片上有 A2、A1、A0 引脚,通过这三个引脚我们就可以自定义 AT24CXXX 芯片的通信地址。

下面以 24C04 和 24C08 的官方手册为例,说明其 I2C 地址,其它型号的芯片自行查阅手册。

可以看到,前 4 位是固定的为 1010,而后的 A2、A1、P0 三个引脚以及读写标志位有我们自己设置。如果将 A2、A1、P0 接地,则 I2C 写地址为 1010 0000(0xA0),读地址为 1010 0001(0xA1)。

5、AT24CXX 的数据读写

5.1 写操作

5.1.1 按字节写
5.1.2 按页写

和按字节写类似,不过在往 AT24CXXX 中写数据时,每写一个 Byte 的数据页内地址 +1,当前页写满后会重新覆盖掉这一页前面的数据,而不会自动跳转到下一页,但是读会自动翻页。

那要如何实现翻页写呢?

按页写其实就是执行一次上面的时序,也就是发送一次从机设备和字节地址最大就可以写入 16 字节(AT24C04)的数据,如果要连写多页,就重新按照上面的时序发送从机地址和字节地址等。

5.2 读操作

写操作和读操作类似,不过 R/W 标志位要设置为 1。

5.2.1 当前地址读取
5.2.2 随机地址读取
5.2.3 顺序读取

二、代码实现

说明:

c 复制代码
// 实现i2c相关设置和初始化
ctl_i2c.h
ctl_i2c.c

// 实现at24cx系列芯片的读写操作
at24c.h
at24c.c

1、ctl_i2c

下面是 ctl_i2c.h 文件,没什么可说的,实现了一些宏,以及相关函数的声明。

c 复制代码
// ctl_i2c.h
#ifndef _BSP_I2C_GPIO_H
#define _BSP_I2C_GPIO_H
 
#include "stm32f4xx.h"
 
 
#define I2C_WR	0		// 写控制bit
#define I2C_RD	1		// 读控制bit
 
#define RCC_AT24CXX_I2C_PORT 			RCC_AHB1Periph_GPIOB		// GPIO端口时钟
#define GPIO_AT24CXX_I2C_PORT			GPIOB						// GPIO端口
#define GPIO_AT24CXX_I2C_SCL_Pin		GPIO_Pin_8					// 连接到SCL时钟线的GPIO
#define GPIO_AT24CXX_I2C_SDA_Pin		GPIO_Pin_9					// 连接到SDA数据线的GPIO
 
#define I2C_SCL_H()  	 GPIO_SetBits(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SCL_Pin) 		      // SCL = 1
#define I2C_SCL_L()  	 GPIO_ResetBits(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SCL_Pin) 		  // SCL = 0
#define I2C_SDA_H()  	 GPIO_SetBits(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SDA_Pin) 		      // SDA = 1
#define I2C_SDA_L()  	 GPIO_ResetBits(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SDA_Pin) 		  // SDA = 0
#define I2C_SDA_RD()     GPIO_ReadInputDataBit(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SDA_Pin)   // 读SDA口线状态
#define I2C_SCL_RD()     GPIO_ReadInputDataBit(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SCL_Pin)   // 读SCL口线状态


void ctl_at24cxx_i2c_init(void);
void ctl_i2c_start(void);
void ctl_i2c_stop(void);
void ctl_i2c_sendbyte(uint8_t byte);
void ctl_i2c_ack(void);
void ctl_i2c_nack(void);
uint8_t ctl_i2c_waitack(void);
uint8_t ctl_i2c_readbyte(void);
uint8_t ctl_i2c_checkdevice(uint8_t address);
 
#endif

接下来看 ctl_i2c.c 文件:

初始化 I2C 的 GPIO 端口:

c 复制代码
/******************************************************************************
 * @brief  初始化I2C总线的GPIO
 * 
 * @return none
 * 
 * @note   采用模拟IO的方式实现
 * 
******************************************************************************/
void ctl_at24cxx_i2c_init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
 
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;		// 设为输出口 
	GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;		// 设为开漏模式 
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;	// 上下拉电阻不使能 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz;	// IO口最大速度
	GPIO_InitStructure.GPIO_Pin = GPIO_AT24CXX_I2C_SCL_Pin | GPIO_AT24CXX_I2C_SDA_Pin;
	GPIO_Init(GPIO_AT24CXX_I2C_PORT, &GPIO_InitStructure);
 
	// 给一个停止信号, 复位I2C总线上的所有设备到待机模式
	ctl_i2c_stop();
}

延时函数的实现:

c 复制代码
/******************************************************************************
 * @brief  I2C总线位延迟,最快400KHz
 * 
 * @return none
 * 
******************************************************************************/
static void i2c_delay(void)
{
	uint8_t i;
 
	/** 
	 *	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++)
	{
		__NOP();
		__NOP();
	}
}

I2C 开始信号:当 SCL 线在高电平期间 SDA 线从高电平向低电平切换

c 复制代码
/******************************************************************************
 * @brief  CPU发起I2C总线启动信号
 * 
 * @return none
 * 
 * @note   当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号
 * 
******************************************************************************/
void ctl_i2c_start(void)
{
	// 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号
	I2C_SDA_H();
	I2C_SCL_H();
	i2c_delay();
	I2C_SDA_L();
	i2c_delay();
	I2C_SCL_L();
	i2c_delay();
}

I2C 停止信号:当 SCL 线在高电平期间 SDA 线由低电平向高电平切换

c 复制代码
/******************************************************************************
 * @brief  CPU发起I2C总线停止信号
 * 
 * @return none
 * 
 * @note   当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号
 * 
******************************************************************************/
void ctl_i2c_stop(void)
{
	/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
	I2C_SDA_L();
	I2C_SCL_H();
	i2c_delay();
	I2C_SDA_H();
	i2c_delay();
}

下面是应答信号和非应答信号的函数实现:

在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,给发送端传输应答或非应答信号

  • SDA 为高电平:表示非应答信号(NACK)
  • SDA为低电平:表示应答信号(ACK)
c 复制代码
/******************************************************************************
 * @brief  CPU产生一个ACK信号
 * 
 * @return none
 * 
******************************************************************************/
void ctl_i2c_ack(void)
{
	I2C_SDA_L();	// SCL低电平期间,SDA 为低电平,表示应答信号
	i2c_delay();
	I2C_SCL_H();	// CPU产生1个时钟
	i2c_delay();
	I2C_SCL_L();
	i2c_delay();
	I2C_SDA_H();	// 应答完成释放SDA总线,否则接收到的数据全是0
}
 
/******************************************************************************
 * @brief  CPU产生1个NACK信号
 * 
 * @return none
 * 
******************************************************************************/
void ctl_i2c_nack(void)
{
	I2C_SDA_H();  // CPU驱动SDA = 1
	i2c_delay();
	I2C_SCL_H();  // SCL 高电平期间,SDA 为高电平,表示非应答信号
	i2c_delay();
	I2C_SCL_L();
	i2c_delay();
}

为什么数据发送端要释放 SDA 的控制权(将SDA总线置为高电平)

数据有效性:IIC 总线进行数据传送时,SCL 信号为高电平期间,SDA 上的数据必须保持稳定,只有在 SCL 上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化(准备下一位数据)。数据在 SCL 的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。

数据传输:在 SCL 串行时钟的配合下,在 SDA 上逐位地串行传送每一位数据。数据位的传输是边沿触发

c 复制代码
/******************************************************************************
 * @brief      CPU向I2C总线设备发送8bit数据
 * 
 * @param[in]  byte    :     等待发送的1个字节数据
 * 
 * @return     none
 * 
 * @note       SDA 上的数据变化只能在 SCL 低电平期间发生
 * 
******************************************************************************/
void ctl_i2c_sendbyte(uint8_t byte)
{
	uint8_t i;
 
	/* 先发送字节的高位bit7 */
	for (i = 0; i < 8; i++)
	{
		if (byte & 0x80)
		{
			I2C_SDA_H();
		}
		else
		{
			I2C_SDA_L();
		}
		i2c_delay();
		I2C_SCL_H();  // SCL高电平有效,发送一位数据
		i2c_delay();
		I2C_SCL_L();  // SCL低电平,准备下一位数据

		// 若是最后一位数据,释放SDA总线,表示数据传输结束
		if (i == 7)
		{
			 I2C_SDA_H(); // 释放总线
		}

		// 数据左移,准备下一位数据(高位先到
		byte <<= 1;
		i2c_delay();
	}
} 
 
/******************************************************************************
 * @brief  CPU从I2C总线设备读取8bit数据
 * 
 * @return uint8_t 
 * 
******************************************************************************/
uint8_t ctl_i2c_readbyte(void)
{
	uint8_t i;
	uint8_t value = 0;
 
	/* 读到第1个bit为数据的bit7 */
	for (i = 0; i < 8; i++)
	{
		value <<= 1;

		I2C_SCL_H();  // 将SCL拉高,准备接收数据
		i2c_delay();

		// 判断EEPROM发送过来的是1还是0
		if (I2C_SDA_RD())
		{
			value++;
		}

		I2C_SCL_L();  // 让EEPROM准备下一位数据
		i2c_delay();
	}

	return value;
}

最后是等待从机 EEPROM 应答和检查设备是否已连接:

c 复制代码
/******************************************************************************
 * @brief  CPU产生一个时钟,并读取器件的ACK应答信号
 * 
 * @return uint8_t 
 * 
******************************************************************************/
uint8_t ctl_i2c_waitack(void)
{
	uint8_t re;
 
	I2C_SDA_H();// 自动释放SDA总线,将控制权交给EEPROM
	i2c_delay();
	I2C_SCL_H();	/* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
	i2c_delay();
	if (I2C_SDA_RD())	/* CPU读取SDA口线状态 */
	{
		re = 1;
	}
	else
	{
		re = 0;
	}
	I2C_SCL_L();
	i2c_delay();
	return re;
}

/******************************************************************************
 * @brief      检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在
 * 
 * @param[in]  address    :     设备地址
 * 
 * @return     uint8_t    :     0 表示成功检测到设备; 返回1表示未探测到
 * 
******************************************************************************/
uint8_t ctl_i2c_checkdevice(uint8_t _Address)
{
	uint8_t ucAck;
 
	if (I2C_SDA_RD() && I2C_SCL_RD())
	{
		ctl_i2c_start();  // 发送启动信号
 
		// 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传
		ctl_i2c_sendbyte(_Address | I2C_WR);
		ucAck = ctl_i2c_waitack();	// 检测设备的ACK应答
 
		ctl_i2c_stop();  // 发送停止信号
 
		return ucAck;
	}
	return 1;  // I2C总线异常
}

2、at24c

at24.h 文件中针对 AT24CX 系列的容量和页内单元数设置了不同的宏,可以针对自己使用的型号设置选择不同的宏使用,这里以 AT24C04 为例:#define AT24C04

c 复制代码
// at24.h
#ifndef __AT24C_H
#define	__AT24C_H
 
#include "stm32f4xx.h"
 
/* 
 * AT24C02 2kb = 2048bit = 2048/8 B = 256 B
 * 32 pages of 8 bytes each
 *
 * Device Address
 * 1 0 1 0 A2 A1 A0 R/W
 * 1 0 1 0 0  0  0  0 = 0xA0
 * 1 0 1 0 0  0  0  1 = 0xA1 
 */
 
/* AT24C01/02每页有8个字节 
 * AT24C04/08A/16A每页有16个字节 、
 */
	
#define AT24C04
 
 
 
#ifdef AT24C01
	#define AT24CX_MODEL_NAME		"AT24C01"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		8			/* 页面大小(字节) */
	#define AT24CX_SIZE				128			/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		1			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			0			/* 地址字节的高8bit不在首字节 */
#endif
 
#ifdef AT24C02
	#define AT24CX_MODEL_NAME		"AT24C02"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		8			/* 页面大小(字节) */
	#define AT24CX_SIZE				256			/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		1			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			0			/* 地址字节的高8bit不在首字节 */
#endif
 
#ifdef AT24C04
	#define AT24CX_MODEL_NAME		"AT24C04"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		8			/* 页面大小(字节) */
	#define AT24CX_SIZE				512			/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		1			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			1			/* 地址字节的高8bit在首字节 */
#endif 
 
#ifdef AT24C08
	#define AT24CX_MODEL_NAME		"AT24C08"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		16			/* 页面大小(字节) */
	#define AT24CX_SIZE				(16*64)		/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		2			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			1			/* 地址字节的高8bit在首字节 */
#endif
 
#ifdef AT24C16
	#define AT24CX_MODEL_NAME		"AT24C16"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		16			/* 页面大小(字节) */
	#define AT24CX_SIZE				(128*16)	/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		2			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			1			/* 地址字节的高8bit在首字节 */
#endif
 
#ifdef AT24C32
	#define AT24CX_MODEL_NAME		"AT24C32"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		32			/* 页面大小(字节) */
	#define AT24CX_SIZE				(128*32)	/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		2			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			1			/* 地址字节的高8bit在首字节 */
#endif

#ifdef AT24C64
	#define AT24CX_MODEL_NAME		"AT24C64"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		32			/* 页面大小(字节) */
	#define AT24CX_SIZE				(256*32)	/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		2			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			1			/* 地址字节的高8bit在首字节 */
#endif
 
#ifdef AT24C128
	#define AT24CX_MODEL_NAME		"AT24C128"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		64			/* 页面大小(字节) */
	#define AT24CX_SIZE				(256*64)	/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		2			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			0			/* 地址字节的高8bit不在首字节 */
#endif
 
#ifdef AT24C256
	#define AT24CX_MODEL_NAME		"AT24C256"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		64			/* 页面大小(字节) */
	#define AT24CX_SIZE				(512*64)	/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		2			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			0			/* 地址字节的高8bit不在首字节 */
#endif 
 
#ifdef AT24C512
	#define AT24CX_MODEL_NAME		"AT24C512"
	#define AT24CX_DEV_ADDR			0xA0		/* 设备地址 */
	#define AT24CX_PAGE_SIZE		128			/* 页面大小(字节) */
	#define AT24CX_SIZE				(512*128)	/* 总容量(字节) */
	#define AT24CX_ADDR_BYTES		2			/* 地址字节个数 */
	#define AT24CX_ADDR_A8			0			/* 地址字节的高8bit不在首字节 */
#endif
 

uint8_t at24cx_checkok(void);
uint8_t at24cx_readbytes(uint8_t *readbuf, uint16_t address, uint16_t size);
uint8_t at24cx_writebytes(uint8_t *writebuf, uint16_t address, uint16_t size);
 
#endif /* __AT24CH */

下面是 at24c.c 函数的实现:

首先检查设备是否连接成功:

c 复制代码
/******************************************************************************
 * @brief  判断串行EERPOM是否正常
 * 
 * @return uint8_t : 1 表示正常, 0 表示不正常
 * 
******************************************************************************/
uint8_t at24cx_checkok(void)
{
	if (ctl_i2c_checkdevice(AT24CX_DEV_ADDR) == 0)
	{
		return 1;
	}
	else
	{
		// 失败后,切记发送I2C总线停止信号
		ctl_i2c_stop();
		return 0;
	}
}

然后是读写函数:

c 复制代码
/******************************************************************************
 * @brief      从串行EEPROM指定地址处开始读取若干数据
 * 
 * @param[in]  readuf    :    起始地址  
 * @param[in]  address   :    数据长度,单位为字节
 * @param[in]  size      :    存放读到的数据的缓冲区指针
 * 
 * @return     uint8_t   :    0 表示失败,1表示成功
 * 
******************************************************************************/
uint8_t at24cx_readbytes(uint8_t *readbuf, uint16_t address, uint16_t size)
{
	uint16_t i;
 
	/**
	 * 采用串行AT24CXPROM随即读取指令序列,连续读取若干字节
	 */ 
 
	// 第1步:发起I2C总线启动信号
	ctl_i2c_start();
 
	// 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
#if AT24CX_ADDR_A8 == 1
	ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR | ((address >> 7) & 0x0E));	// 写指令
#else
	ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR);	 //写指令
#endif
 
	// 第3步:发送ACK
	if (ctl_i2c_waitack() != 0)
	{
		goto cmd_fail;	// AT24CXPROM器件无应答
	}
 
	// 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址
	if (AT24CX_ADDR_BYTES == 1)
	{
		ctl_i2c_sendbyte((uint8_t)address);
		if (ctl_i2c_waitack() != 0)
		{
			goto cmd_fail;	// AT24CXPROM器件无应答
		}
	}
	else
	{
		ctl_i2c_sendbyte(address >> 8);
		if (ctl_i2c_waitack() != 0)
		{
			goto cmd_fail;	// AT24CXPROM器件无应答
		}
 
		ctl_i2c_sendbyte(address);
		if (ctl_i2c_waitack() != 0)
		{
			goto cmd_fail;	// AT24CXPROM器件无应答
		}
	}
 
	// 第5步:重新启动I2C总线。下面开始读取数据
	ctl_i2c_start();
 
	// 第6步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
#if AT24CX_ADDR_A8 == 1
	ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_RD | ((address >> 7) & 0x0E));	// 写指令
#else		
	ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_RD);  // 此处是写指令
#endif	
 
	// 第7步:发送ACK 
	if (ctl_i2c_waitack() != 0)
	{
		goto cmd_fail;	// AT24CXPROM器件无应答 
	}
 
	// 第8步:循环读取数据 
	for (i = 0; i < size; i++)
	{
		readbuf[i] = ctl_i2c_readbyte();	// 读1个字节
 
		// 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack
		if (i != size - 1)
		{
			ctl_i2c_ack();	// 中间字节读完后,CPU产生ACK信号(驱动SDA = 0)
		}
		else
		{
			ctl_i2c_nack();	// 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) 
		}
	}

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

/******************************************************************************
 * @brief      向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率
 * 
 * @param[in]  writeBuf   :   起始地址  
 * @param[in]  address    :   数据长度,单位为字节
 * @param[in]  size       :   存放读到的数据的缓冲区指针
 * 
 * @return     uint8_t    :   0 表示失败,1表示成功
 * 
******************************************************************************/
uint8_t at24cx_writebytes(uint8_t *writebuf, uint16_t address, uint16_t size)
{
	uint16_t i, m;
	uint16_t addr;
 
	/**
	 *	写串行AT24CXPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。
	 *	对于24xx02,page size = 8
	 *	简单的处理方法为:按字节写操作模式,每写1个字节,都发送地址
	 *	为了提高连续写的效率: 本函数采用page wirte操作。
	 */
 
	addr = address;
	for (i = 0; i < size; i++)
	{
		// 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址
		if ((i == 0) || (addr & (AT24CX_PAGE_SIZE - 1)) == 0)
		{
			// 第0步:发停止信号,启动内部写操作
			ctl_i2c_stop();
 
			/**
			 *  通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms
			 *	CLK频率为200KHz时,查询次数为30次左右
			 */
			for (m = 0; m < 1000; m++)
			{
				// 第1步:发起I2C总线启动信号
				ctl_i2c_start();
 
				// 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
			#if AT24CX_ADDR_A8 == 1
				ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR | ((address >> 7) & 0x0E));  // 此处是写指令
			#else				
				ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR);
			#endif
 
				// 第3步:发送一个时钟,判断器件是否正确应答
				if (ctl_i2c_waitack() == 0)
				{
					break;
				}
			}
			if (m  == 1000)
			{
				goto cmd_fail;	// AT24CXPROM器件写超时
			}
 
			// 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址
			if (AT24CX_ADDR_BYTES == 1)
			{
				ctl_i2c_sendbyte((uint8_t)addr);
				if (ctl_i2c_waitack() != 0)
				{
					goto cmd_fail;	// AT24CXPROM器件无应答
				}
			}
			else
			{
				ctl_i2c_sendbyte(addr >> 8);
				if (ctl_i2c_waitack() != 0)
				{
					goto cmd_fail;	// AT24CXPROM器件无应答
				}
 
				ctl_i2c_sendbyte(addr);
				if (ctl_i2c_waitack() != 0)
				{
					goto cmd_fail;	// AT24CXPROM器件无应答
				}
			}
		}
 
		// 第5步:开始写入数据 
		ctl_i2c_sendbyte(writebuf[i]);
 
		// 第6步:发送ACK
		if (ctl_i2c_waitack() != 0)
		{
			goto cmd_fail;	// AT24CXPROM器件无应答
		}
 
		addr++;  // 地址增1
	}
 
	// 命令执行成功,发送I2C总线停止信号
	ctl_i2c_stop();
 
	/**
	 *  通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms
	 *	CLK频率为200KHz时,查询次数为30次左右
	 */
	for (m = 0; m < 1000; m++)
	{
		// 第1步:发起I2C总线启动信号
		ctl_i2c_start();
 
		// 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
	#if AT24CX_ADDR_A8 == 1
		ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR | ((address >> 7) & 0x0E));  // 此处是写指令
	#else		
		ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR);	 // 此处是写指令
	#endif
 
		// 第3步:发送一个时钟,判断器件是否正确应答 
		if (ctl_i2c_waitack() == 0)
		{
			break;
		}
	}
	if (m  == 1000)
	{
		goto cmd_fail;	// AT24CXPROM器件写超时
	}
 
	// 命令执行成功,发送I2C总线停止信号
	ctl_i2c_stop();	
 
	return 1;
 
	// 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备
cmd_fail: 
	// 发送I2C总线停止信号
	ctl_i2c_stop();
	return 0;
}

3、测试程序

c 复制代码
uint8_t test_array1[3 * AT24CX_PAGE_SIZE]; // 注:AT24C04时,AT24CX_PAGE_SIZE=8
uint8_t test_array2[3 * AT24CX_PAGE_SIZE]; //     AT24C04时,一个页面有24个字节


void at24c04_test_num(void)
{
	uint16_t i;
	uint16_t j;

	for (i = 0; i < 3 * AT24CX_PAGE_SIZE; i++)
	{
		if (i >= 256)
			j = i - 256; // test_array1[256---383] 单元初始化数值 = 1---128
		else if (i >= 128)
			j = i - 128; // test_array1[128---255] 单元初始化数值 = 1---128
		else
			j = i; // test_array1[0---127] 单元初始化数值 = 1---128
		test_array1[i] = j + 1;
	}

	memset(test_array2, 0x00, 3 * AT24CX_PAGE_SIZE);

	if (at24cx_checkok() == 1) // 如果检测到I2C器件存在
	{
		at24cx_writebytes(test_array1, 80, 3 * AT24CX_PAGE_SIZE); // 从I2C的地址80处开始写3页字节(测试跨页连续写)
		at24cx_readbytes(test_array2, 80, 3 * AT24CX_PAGE_SIZE);  // 从I2C的地址80处开始读3页字节(测试跨页连续读)
	}

	printf("test begin\r\n");
	
	for (i = 0; i < sizeof(test_array2); ++i)
	{
		printf("%d, ", test_array2[i]);
	}
}

结果如下:

相关推荐
MARIN_shen34 分钟前
Marin说PCB之POC电路layout设计仿真案例---06
网络·单片机·嵌入式硬件·硬件工程·pcb工艺
Asa3191 小时前
STM32-按键扫描配置
stm32·单片机·嵌入式硬件
南城花随雪。1 小时前
单片机:实现驱动超声波(附带源码)
单片机·嵌入式硬件
嵌入式科普1 小时前
十三、从0开始卷出一个新项目之瑞萨RZN2L串口DMA接收不定长
c语言·stm32·瑞萨·e2studio·rzn2l
yutian060610 小时前
Keil MDK下载程序后MCU自动重启设置
单片机·嵌入式硬件·keil
析木不会编程13 小时前
【小白51单片机专用教程】protues仿真独立按键控制LED
单片机·嵌入式硬件·51单片机
枯无穷肉16 小时前
stm32制作CAN适配器4--WinUsb的使用
stm32·单片机·嵌入式硬件
不过四级不改名67717 小时前
基于HAL库的stm32的can收发实验
stm32·单片机·嵌入式硬件
嵌入式科普17 小时前
十一、从0开始卷出一个新项目之瑞萨RA6M5串口DTC接收不定长
c语言·stm32·cubeide·e2studio·ra6m5·dma接收不定长
嵌入式大圣17 小时前
单片机UDP数据透传
单片机·嵌入式硬件·udp