目录
- [一、AT24CXXX 系列存储器介绍](#一、AT24CXXX 系列存储器介绍)
-
- 1、基本信息
- 2、寻址方式
- 3、页地址与页内单元地址
- [4、I2C 地址](#4、I2C 地址)
- [5、AT24CXX 的数据读写](#5、AT24CXX 的数据读写)
-
- [5.1 写操作](#5.1 写操作)
-
- [5.1.1 按字节写](#5.1.1 按字节写)
- [5.1.2 按页写](#5.1.2 按页写)
- [5.2 读操作](#5.2 读操作)
-
- [5.2.1 当前地址读取](#5.2.1 当前地址读取)
- [5.2.2 随机地址读取](#5.2.2 随机地址读取)
- [5.2.3 顺序读取](#5.2.3 顺序读取)
- 二、代码实现
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]);
}
}
结果如下: