当前位置: 首页 > news >正文

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

目录

  • 一、AT24CXXX 系列存储器介绍
    • 1、基本信息
    • 2、寻址方式
    • 3、页地址与页内单元地址
    • 4、I2C 地址
    • 5、AT24CXX 的数据读写
      • 5.1 写操作
        • 5.1.1 按字节写
        • 5.1.2 按页写
      • 5.2 读操作
        • 5.2.1 当前地址读取
        • 5.2.2 随机地址读取
        • 5.2.3 顺序读取
  • 二、代码实现
    • 1、ctl_i2c
    • 2、at24c
    • 3、测试程序


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

一、AT24CXXX 系列存储器介绍

1、基本信息

下表是 AT24CXXX 的容量

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

AT24CXXXbit容量Byte容量
AT24C011Kbit128Byte
AT24C022Kbit256Byte
AT24C044Kbit512Byte
AT24C088Kbit1024Byte
AT24C1616Kbit2048Byte
AT24C3232Kbit4096Byte
AT24C6464Kbit8192Byte
AT24C128128Kbit16384Byte
AT24C256256Kbit32768Byte
AT24C512512Kbit65536Byte

下表是 AT24CXXX 的页内单元数

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

AT24CXXXByte容量页数页内字节单元数
AT24C01128Byte16页8Byte
AT24C02256Byte32页8Byte
AT24C04512Byte32页16Byte
AT24C081024Byte64页16Byte
AT24C162048Byte128页16Byte
AT24C324096Byte128页32Byte
AT24C648192Byte256页32Byte
AT24C12816384Byte256页64Byte
AT24C25632768Byte512页64Byte
AT24C51265536Byte512页128Byte

2、寻址方式

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

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

先发送设备地址,收到应答后再发送需要写数据的地址(WORD ADDRESS)。AT24C04 容量为 512Byte 则 WORD ADDRESS 只需要 9bit 就可以覆盖 512Byte 的数据地址。通俗的讲就是 512Byte 就占用了 512 个地址,一个 9bit 的数据范围为( 0 − 511 0-511 0511)刚好 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 顺序读取

二、代码实现

说明:

// 实现i2c相关设置和初始化
ctl_i2c.h
ctl_i2c.c// 实现at24cx系列芯片的读写操作
at24c.h
at24c.c

1、ctl_i2c

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

// 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 端口:

/******************************************************************************* @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();
}

延时函数的实现:

/******************************************************************************* @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 线从高电平向低电平切换

/******************************************************************************* @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 线由低电平向高电平切换

/******************************************************************************* @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)
/******************************************************************************* @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 = 1i2c_delay();I2C_SCL_H();  // SCL 高电平期间,SDA 为高电平,表示非应答信号i2c_delay();I2C_SCL_L();i2c_delay();
}

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

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

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

/******************************************************************************* @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还是0if (I2C_SDA_RD()){value++;}I2C_SCL_L();  // 让EEPROM准备下一位数据i2c_delay();}return value;
}

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

/******************************************************************************* @brief  CPU产生一个时钟,并读取器件的ACK应答信号* * @return uint8_t * 
******************************************************************************/
uint8_t ctl_i2c_waitack(void)
{uint8_t re;I2C_SDA_H();// 自动释放SDA总线,将控制权交给EEPROMi2c_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

// 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不在首字节 */
#endifuint8_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 函数的实现:

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

/******************************************************************************* @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;}
}

然后是读写函数:

/******************************************************************************* @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 == 1ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR | ((address >> 7) & 0x0E));	// 写指令
#elsectl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR);	 //写指令
#endif// 第3步:发送ACKif (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 == 1ctl_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,发Nackif (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 == 1ctl_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步:发送ACKif (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 == 1ctl_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、测试程序

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---128else if (i >= 128)j = i - 128; // test_array1[128---255] 单元初始化数值 = 1---128elsej = i; // test_array1[0---127] 单元初始化数值 = 1---128test_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]);}
}

结果如下:

相关文章:

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

目录 一、AT24CXXX 系列存储器介绍1、基本信息2、寻址方式3、页地址与页内单元地址4、I2C 地址5、AT24CXX 的数据读写5.1 写操作5.1.1 按字节写5.1.2 按页写 5.2 读操作5.2.1 当前地址读取5.2.2 随机地址读取5.2.3 顺序读取 二、代码实现1、ctl_i2c2、at24c3、测试程序 I2C 相关…...

Go语言匿名字段使用与注意事项

1. 定义 Go语言支持一种特殊的字段只需要提供类型而不需要写字段名的字段&#xff0c;称之为匿名字段或者嵌套字段。 所谓匿名字段实际上是一种结构体嵌套的方式&#xff0c;所以也可以称作嵌套字段。 这种方式可以实现组合复用&#xff0c;即通过匿名字段&#xff0c;结构体…...

2024最新!!Java后端面试题(2)看这一篇就够了

hello uu们 感谢收看&#xff01;&#xff01;&#xff01;&#xff01;我最近听了一首歌《21》&#xff0c;真的很感慨&#xff0c;马上步入20的我也感觉时间真的飞快...望大家都能过上理想的生活&#xff0c;不负内心的所托...现在口语化更新答案&#xff0c;让大家更加模拟的…...

超好用的10款视频剪辑软件,从入门到精通

视频剪辑软件哪款比较好呢&#xff1f;无论是专业制作团队、自媒体创作者&#xff0c;还是家庭用户&#xff0c;一款好用的视频剪辑软件都能极大地提升创作效率和作品质量。以下是十款备受推崇的视频剪辑软件&#xff0c;分别从适用人群、易用程度和功能特点进行介绍。 1.影忆…...

python股票因子,交易所服务器宕机,量化交易程序怎么应对

炒股自动化&#xff1a;申请官方API接口&#xff0c;散户也可以 python炒股自动化&#xff08;0&#xff09;&#xff0c;申请券商API接口 python炒股自动化&#xff08;1&#xff09;&#xff0c;量化交易接口区别 Python炒股自动化&#xff08;2&#xff09;&#xff1a;获取…...

瑞芯微RK3566鸿蒙开发板Android11修改第三方输入法为默认输入法

本文适用于触觉智能所有支持Android11系统的开发板修改第三方输入法为默认输入法。本次使用的是触觉智能的Purple Pi OH鸿蒙开源主板&#xff0c;搭载了瑞芯微RK3566芯片&#xff0c;类树莓派设计&#xff0c;是Laval官方社区主荐的一款鸿蒙开发主板。 一、安装输入法并查看输入…...

使用nest+typeorm框架写数据库导致mysql的binlog暴增记录

这 两天用nesttypeorm写了一个商城&#xff0c;上线后mysql日志binlog两天就达到了10几个G&#xff0c;排查结果如下&#xff1a; 有个功能是定时遍历所有未签收的订单&#xff0c;看看是否到了自动签收时间&#xff0c;如果到了&#xff0c;就把订单状态设置成已签收。 代码…...

组合逻辑元件与时序逻辑元件

组合逻辑元件和时序逻辑元件都是数字电路中的基本构建块&#xff0c;但它们在功能和结构上存在显著差异。 1. 组合逻辑元件: 内容: 组合逻辑元件的输出仅取决于当前的输入&#xff0c;而与之前的输入无关。 它们没有记忆功能。 常见的组合逻辑元件包括&#xff1a; 与门 (AND…...

天龙八部怀旧单机微改人面桃花+安装教程+GM工具+虚拟机一键端

今天给大家带来一款单机游戏的架设&#xff1a;天龙八部怀旧单机微改人面桃花。 另外&#xff1a;本人承接各种游戏架设&#xff08;单机联网&#xff09; 本人为了学习和研究软件内含的设计思想和原理&#xff0c;带了架设教程仅供娱乐。 教程是本人亲自搭建成功的&#xf…...

docker管理

拉取容器镜像 docker pull 镜像名:镜像版本查看镜像 docker images查看容器列表 # 查看正在运行的容器 docker ps # 查看全部的容器(包括停止的容器) docker ps -a进入容器 docker exec -it 容器id /bin/bash停止容器 docker stop 容器id运行容器 docker start 容器id删除…...

electron教程(三)窗口设置

在main.js文件中&#xff0c;创建窗口时会设置窗口的大小&#xff0c;其实还有很多其他属性&#xff0c;可以根据实际需求选择设置&#xff0c;但部分属性存在局限性&#xff0c;官网也有明确告知&#xff1a;自定义窗口 | Electron (electronjs.org) 项目文件目录如下&#x…...

图像增强论文精读笔记-Deep Retinex Decomposition for Low-Light Enhancement(Retinex-Net)

1. 论文基本信息 论文标题&#xff1a;Deep Retinex Decomposition for Low-Light Enhancement 作者&#xff1a;Chen Wei等 发表时间和期刊&#xff1a;2018&#xff1b;BMVC 论文链接&#xff1a;https://arxiv.org/abs/1808.04560 2. 研究背景和动机 低光照条件下拍摄的…...

2024年配置YOLOX运行环境+windows+pycharm24.0.1+GPU

1.配置时间2024/9/25 2.Anaconda-python版本3.7&#xff0c;yolox版本0.2.0 YOLOX网址: https://github.com/Megvii-BaseDetection/YOLOX 本人下载的这个版本 1.创建虚拟环境 conda create -n yolox37 python37 激活 conda activate yolox37 2.安装Pytorch cuda等&…...

vue-i18n在使用$t时提示类型错误

1. 问题描述 Vue3项目中&#xff0c;使用vue-i18n&#xff0c;在模版中使用$t时&#xff0c;页面可以正常渲染&#xff0c;但是类型报错。 相关依赖版本如下&#xff1a; "dependencies": {"vue": "^3.4.29","vue-i18n": "^9.1…...

大厂面试真题-什么是CAS单点登录?什么原理

CAS&#xff08;Central Authentication Service&#xff0c;中央认证服务&#xff09;单点登录&#xff08;SSO&#xff0c;Single Sign-On&#xff09;的原理主要基于统一的认证机制和票据验证过程&#xff0c;使得用户只需在多个相互信任的应用系统中登录一次&#xff0c;即…...

用Java提取PDF表格到文本、CSV、Excel工作表

如何精准地提取PDF格式中嵌入的表格数据&#xff0c;并将其无缝转换为更加易于分析和操作的形式&#xff0c;如纯文本、CSV文件或Excel工作表&#xff0c;是一项重要的文档处理技巧。使用Java&#xff0c;我们可以简单地实现这一过程。本文将介绍如何利用Java从PDF文档提取表格…...

OpenCV视频I/O(10)视频采集类VideoCapture之从视频流中检索一帧图像函数 retrieve()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 解码并返回已抓取的视频帧。 cv::VideoCapture::retrieve() 是 VideoCapture 类的一个成员函数&#xff0c;用于从视频流中检索一帧图像。 retr…...

【RocketMQ】SpringBoot整合RocketMQ

&#x1f3af; 导读&#xff1a;本文档详细介绍了如何在Spring Boot应用中集成Apache RocketMQ&#xff0c;并实现消息生产和消费功能。首先通过创建消息生产者项目&#xff0c;配置POM文件引入RocketMQ依赖&#xff0c;实现同步消息发送&#xff0c;并展示了如何发送普通字符串…...

mysql replace无法替换空格?如何解决

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云/阿里云/华为云/51CTO&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互…...

Redis篇(环境搭建)

目录 一、安装包 1. Windows版下载地址 2. Linux版下载地址 二、安装Redis 1. 在Linux中安装Redis 2. 在Windows中安装Redis 3. 细节问题 三、Redis服务启动 1. 默认启动 2. 指定配置启动 3. 开机自启 四、Redis服务停止 1. Linux系统中启动和停止Redis 2. Window…...

【C++题目】7.双指针_和为 s 的两个数字

文章目录 题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;图解 题目链接&#xff1a; LCR 179.查找总价格为目标值的两个商品 题目描述&#xff1a; 解法 解法一&#xff08;暴力解法&#xff0c;会超时&#xff09; 两层 for 循环列出所有两个数字的组合…...

网络通信1-传输层

tcp的三次握手&#xff1a; TCP&#xff08;传输控制协议&#xff09;的三次握手是建立一个可靠的连接的过程。这个过程中涉及到的主要参数包括&#xff1a; 序列号&#xff08;Sequence Number, SEQ&#xff09;: 在第一次握手中&#xff0c;发起方&#xff08;客户端&#xf…...

【JAVA源码授权】

悯农二首 代码混淆加密 Class 文件许可证管理数字签名API 调用限制防止反编译使用私有库法律保护动态授权 其一 春种一粒粟&#xff0c;秋收万颗子。 四海无闲田&#xff0c;农夫犹饿死。 其二 锄禾日当午&#xff0c;汗滴禾下土。 谁知盘中餐&#xff0c;粒粒皆辛苦 代码混淆 …...

tauri开发软件中,使用tauri自带的api用浏览器打开指定的url链接

有能力的可以看官方文档&#xff1a;shell | Tauri Apps 就是使用这个api来打开指定的url链接&#xff0c;要在tauri.config.json中配置打开这个api&#xff1a; 然后在前端页面中导入使用&#xff1a; import { open } from tauri-apps/api/shell; // opens the given URL o…...

OpenCV-图像拼接

文章目录 一、基本原理二、步骤三、代码实现1.定义函数2.读取图像3.图像配准&#xff08;1&#xff09;.特征点检测&#xff08;2&#xff09;.特征匹配 4.透视变换5.图像拼接 四、图像拼接的注意事项 图像拼接是一种将多张有重叠部分的图像合并成一张无缝的全景图或高分辨率图…...

C++【类和对象】(取地址运算符重载与实现Date类)

文章目录 取地址运算符重载const成员函数取地址运算符重载 Date类的实现Date.hDate.cpp1.检查日期合法性2. 构造函数/赋值运算符重载3.得到某月的天数4. Date类 - 天数的操作4.1 日期 天数4.2 日期 天数4.3 日期 - 天数4.4 日期 - 天数 5. Date的前后置/--5.1 前置5.2 后置5.…...

oracle 数据库中的异常和游标管理

异常和游标管理 游标&#xff1a; 用来查询数据库&#xff0c;获取记录集合&#xff08;结果集&#xff09;的指针&#xff0c;可以让开发者一次访问一行结果集&#xff0c;在每条结果集上作操作。 分类&#xff1a; 静态游标&#xff1a; 分为显式游标和隐式游标。 REF游标&…...

关于python 日志设定为INFO 但是DEBUG仍旧写入的问题

问题&#xff1a;将logging设定为了INFO级别&#xff0c;但是在打印的时候发现jieba包中的DEBUG级别的信息还是出现了。 原因&#xff1a;在我引用的包中&#xff0c;一些python文件也使用了logging&#xff0c;我设定的INFO级别只能设定我当前使用的文件&#xff0c;而调用的包…...

TypeScript 语法基础 第一部分 类型

【视频链接】尚硅谷TypeScript教程&#xff08;李立超老师TS新课&#xff09; TypeScript TypeScript 语法基础 第二部分 类、接口、泛型1. 类型1.1 | 联合类型1.2 字面量类型1.3 any 任意类型1.4 unkown 类型1.5 as 类型断言1.6 object 对象类型1.7 { } 对象类型1.8 &#xff…...

GO Serial 学习与使用

文章目录 主要特性安装基本用法配置选项错误处理其他功能 github.com/goburrow/serial 是一个 Go 包&#xff0c;提供了一种简单的方式来与串口进行交互。以下是该包的主要特性和用法的简要概述&#xff1a; 主要特性 跨平台支持&#xff1a;支持 Windows、macOS 和 Linux。简…...