注意说明

网上的示例代码都基于arm等内核MCU,有些地方不适合用在低端MCU上,因为内存和堆栈的显示不允许这样使用,需要重新调整IIC通讯!这边只是参考,通讯部分还需要自己重头写!特别是 delayms(10),这个在产品中是不允许出现!整整10ms内MCU啥都不做,这容易触发卡顿或者看门狗复位!

一、应用场景?

有些单片机没有内部的EEPROM,或者说内部的EEPROM空间不够大,这时候,我们就想到了外挂一个EEPROM芯片,而AT24C32就是一种常用的串行EEPROM存储器,它适用于许多需要在系统掉电后保留数据的嵌入式应用场景。

二、AT24C32是什么?

AT24C32是一种常见的串行 EEPROM 存储器芯片,通常用于存储系统配置、用户数据、校准参数等。它具有以下特点,一般会在以下情况下被使用:

  1. 小容量数据存储:AT24C32的存储容量一般为32Kb,适合存储少量的配置信息、校准参数、设备标识等数据。
  2. 串行接口:AT24C32采用串行接口进行数据通信,这使得它在占用较少引脚的同时能够进行高效的数据读写操作,因此适合于资源受限的嵌入式系统或小型设备中使用。
  3. 数据保存与恢复:AT24C32通常被用于存储需要持久保存的数据,例如设备配置、校准参数等。它的非易失性存储特性意味着即使断电也能保持数据完整,可在下次上电时恢复。
  4. 嵌入式系统:由于其小巧的封装、低功耗和易于集成的特点,AT24C32常被嵌入式系统设计中用于管理系统配置、日志记录、用户个性化设置等数据。

芯片对比

型号 容量bit 容量byte 页数 字节/页 器件寻址位 可寻址器件数 WordAddress位数/字节数 备注
AT24C32 32k 4k 128 32 A2A1A0 8 12/2
AT24C64 64k 8k 256 32 A2A1A0 8 13/2
AT24C128 128k 16k 256 64 A1A0 4 14/2
AT24C256 256k 32k 512 64 A1A0 4 15/2
T24C512 512k 64k 512 128 A2A1A0 8 16/2

这5款芯片与x24C01~x24C16主要的不同是,WordAddress是两个字节,器件寻址位A2/A1/A0三位或者两位,不再有页选择位。例如,下图是AT24C128、AT24C256的引脚图,只有两个硬件地址引脚A0/A1,第3脚不连接。

在这里插入图片描述

三、使用步骤

1.引脚说明

在这里插入图片描述

1-3.A0~A2:地址输入引脚,共3个,用于设定芯片的I2C地址。
4.GND:地引脚,与VCC联合供电。
5.SDA:串行数据输入/输出引脚,用于与I2C总线进行通信。
6.SCL:串行时钟输入引脚,用于与I2C总线进行同步。
7.WP:写保护引脚,当WP引脚为高电平时,AT24C32将无法写入数据,起到了对AT24C32数据的保护作用。
8.VCC:供电引脚,连接正极电源。

其中,WP引脚是AT24C32的写保护引脚,用于控制AT24C32是否可以写入数据。当WP引脚处于高电平时,AT24C32将进入写保护模式,此时将无法写入数据。当WP引脚处于低电平时,AT24C32将退出写保护模式,此时可以向其写入数据。通常情况下,WP引脚需要连接到系统中的GPIO引脚上,以便在需要时能够动态控制AT24C32的写保护模式

1.2读写操作

1.2.1 写操作

Byte Write写一个字节

在这里插入图片描述

在这里插入图片描述

AT24C32和AT24C64手册中写单个字节的操作及时序,红框中可以看出,在器件地址、应答信号后,跟着的是两个字节的WordAddress,这就是与AT24C01~AT24C16芯片的主要区别。

Page Write写一页

在这里插入图片描述

在这里插入图片描述

页写操作也是没有多少新东西,除了WordAddress有两个字节,与其他容量芯片的操作类似。

1.2.2 写操作

读任意地址

在这里插入图片描述

在这里插入图片描述

与较小容量的芯片相比,读任意地址的时序,主要的区别是需要发送两个字节的WordAddress。

顺序读(页读)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

以上为页读操作,也是由“读当前地址”或“读任意地址”操作开始,mcu收到一个数据后,应答,不发送停止信号,即可接收下一个字节数据。

1.2.3 示例代码(可参考)

宏定义
先对器件地址等信息进行宏定义,根据不同的器件进行条件编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#define READ_CMD				1
#define WRITE_CMD 0

#define x24C32//器件名称,AT24C32、AT24C64、AT24C128、AT24C256、AT24C512
#define DEV_ADDR 0xA0 //设备硬件地址

#ifdef x24C32
#define PAGE_NUM 128 //页数
#define PAGE_SIZE 32 //页面大小(字节)
#define CAPACITY_SIZE (PAGE_NUM * PAGE_SIZE) //总容量(字节)
#define ADDR_BYTE_NUM 2 //地址字节个数
#endif

#ifdef x24C64
#define PAGE_NUM 256 //页数
#define PAGE_SIZE 32 //页面大小(字节)
#define CAPACITY_SIZE (PAGE_NUM * PAGE_SIZE) //总容量(字节)
#define ADDR_BYTE_NUM 2 //地址字节个数
#endif

#ifdef x24C128
#define PAGE_NUM 256 //页数
#define PAGE_SIZE 64 //页面大小(字节)
#define CAPACITY_SIZE (PAGE_NUM * PAGE_SIZE) //总容量(字节)
#define ADDR_BYTE_NUM 2 //地址字节个数
#endif

#ifdef x24C256
#define PAGE_NUM 512 //页数
#define PAGE_SIZE 64 //页面大小(字节)
#define CAPACITY_SIZE (PAGE_NUM * PAGE_SIZE) //总容量(字节)
#define ADDR_BYTE_NUM 2 //地址字节个数
#endif

#ifdef x24C512
#define PAGE_NUM 512 //页数
#define PAGE_SIZE 128 //页面大小(字节)
#define CAPACITY_SIZE (PAGE_NUM * PAGE_SIZE) //总容量(字节)
#define ADDR_BYTE_NUM 2 //地址字节个数
#endif

写单个字节(写任意地址)
发送起始信号–>发送器件地址(包含写入命令)–>收到应答–>发送需要写入数据的地址的高字节(8bit)–>收到应答–>发送需要写入数据的地址的低字节(8bit)–>收到应答–>发送需要写入的数据–>收到应答–>发送停止信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*******************************************************************************
* 函数名:x24Cxx_WriteByte
* 功 能:写一个字节
* 参 数:u16Addr要写入的地址
u8Data要写入的数据
* 返回值:无
* 说 明:无
*******************************************************************************/
void x24Cxx_WriteByte(uint16_t u16Addr, uint8_t u8Data)
{
x24Cxx_WriteEnable();//使能写入
IIC_Start();//起始信号
IIC_WriteByte(DEV_ADDR | WRITE_CMD);//器件寻址+读/写选择
IIC_WaitAck();//等待应答
IIC_WriteByte((uint8_t)((u16Addr >> 8) & 0xFF));
IIC_WaitAck();//等待应答
IIC_WriteByte((uint8_t)(u16Addr & 0xFF));
IIC_WaitAck();//等待应答
IIC_WriteByte(u8Data);
IIC_WaitAck();//等待应答
IIC_Stop();//停止信号
x24Cxx_WriteDisble();//禁止写入
}

写一页
发送起始信号–>发送器件地址(包含写入命令)–>收到应答–>发送需要写入数据的首地址的高字节(8bit)–>收到应答–>发送需要写入数据的首地址的低字节(8bit)–>收到应答–>发送需要写入的第1个数据–>收到应答–>发送需要写入的第2个数据–>收到应答…–>发送需要写入的第n个数据–>收到应答–>发送停止信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*******************************************************************************
* 函数名:x24Cxx_WritePage
* 功 能:页写
* 参 数:u16Addr要写入的首地址;
u8Len写入数据字节数,最大为PAGE_SIZE
pData要写入的数据首地址
* 返回值:无
* 说 明:最多写入1页,防止翻卷,如果地址跨页则去掉跨页的部分
*******************************************************************************/
void x24Cxx_WritePage(uint16_t u16Addr, uint8_t u8Len, uint8_t *pData)
{
uint8_t i;
x24Cxx_WriteEnable();//使能写入
IIC_Start();//起始信号
IIC_WriteByte(DEV_ADDR | WRITE_CMD);//器件寻址+读/写选择
IIC_WaitAck();//等待应答
IIC_WriteByte((uint8_t)((u16Addr >> 8) & 0xFF));
IIC_WaitAck();//等待应答
IIC_WriteByte((uint8_t)(u16Addr & 0xFF));
IIC_WaitAck();//等待应答
if (u8Len > PAGE_SIZE)//长度大于页的长度
{
u8Len = PAGE_SIZE;
}
if ((u16Addr + (uint16_t)u8Len) > CAPACITY_SIZE)//超过容量
{
u8Len = (uint8_t)(CAPACITY_SIZE - u16Addr);
}
if (((u16Addr % PAGE_SIZE) + (uint16_t)u8Len) > PAGE_SIZE)//判断是否跨页
{
u8Len -= (uint8_t)((u16Addr + (uint16_t)u8Len) % PAGE_SIZE);//跨页,截掉跨页的部分
}

for (i = 0; i < u8Len; i++)
{
IIC_WriteByte(*(pData + i));
IIC_WaitAck();//等待应答
}
IIC_Stop();//停止信号
x24Cxx_WriteDisble();//禁止写入
}

读单个字节(读任意地址)
发送起始信号–>发送器件地址(包含写入命令)–>收到应答–>发送需要读取数据的地址的高字节(8bit)–>收到应答–>发送需要读取数据的地址的低字节(8bit)–>收到应答–>发送起始信号–>发送器件地址(包含读取命令)–>收到应答–>读取数据–>不应答–>发送停止信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*******************************************************************************
* 函数名:x24Cxx_ReadByte
* 功 能:读一个字节
* 参 数:u16Addr要读取的地址
* 返回值:u8Data读出的数据
* 说 明:无
*******************************************************************************/
uint8_t x24Cxx_ReadByte(uint16_t u16Addr)
{
uint8_t u8Data = 0;
IIC_Start();//起始信号
IIC_WriteByte(DEV_ADDR | WRITE_CMD);//器件寻址+读/写选择
IIC_WaitAck();//等待应答
IIC_WriteByte((uint8_t)((u16Addr >> 8) & 0xFF));
IIC_WaitAck();//等待应答
IIC_WriteByte((uint8_t)(u16Addr & 0xFF));
IIC_WaitAck();//等待应答
IIC_Start();//起始信号
IIC_WriteByte(DEV_ADDR | READ_CMD);//器件寻址+读
IIC_WaitAck();//等待应答
u8Data = IIC_ReadByte();
IIC_NoAck();
IIC_Stop();//停止信号
return u8Data;
}

读一页(顺序读)
发送起始信号–>发送器件地址(包含写入命令)–>收到应答–>发送需要读取数据的首地址高字节–>收到应答–>发送需要读取数据的首地址低字节–>收到应答–>发送起始信号–>发送器件地址(包含读取命令)–>收到应答–>读取第1个数据–>发送应答–>读取第2个数据–>发送应答…–>读取第n个数据–>不应答–>发送停止信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*******************************************************************************
* 函数名:x24Cxx_ReadPage
* 功 能:页读
* 参 数:u16Addr要读取的首地址;
u8Len读取数据字节数,最大为PAGE_SIZE
pBuff读取数据存入的缓存
* 返回值:无
* 说 明:最多读1页,防止翻卷,如果地址跨页则去掉跨页的部分
*******************************************************************************/
void x24Cxx_ReadPage(uint16_t u16Addr, uint8_t u8Len, uint8_t *pBuff)
{
uint8_t i;
IIC_Start();//起始信号
IIC_WriteByte(DEV_ADDR | WRITE_CMD);//器件寻址+读/写选择
IIC_WaitAck();//等待应答
IIC_WriteByte((uint8_t)((u16Addr >> 8) & 0xFF));
IIC_WaitAck();//等待应答
IIC_WriteByte((uint8_t)(u16Addr & 0xFF));
IIC_WaitAck();//等待应答
IIC_Start();//起始信号
IIC_WriteByte(DEV_ADDR | READ_CMD);//器件寻址+读
IIC_WaitAck();//等待应答
if (u8Len > PAGE_SIZE)//长度大于页的长度
{
u8Len = PAGE_SIZE;
}
if ((u16Addr + (uint16_t)u8Len) > CAPACITY_SIZE)//超过容量
{
u8Len = (uint8_t)(CAPACITY_SIZE - u16Addr);
}
if (((u16Addr % PAGE_SIZE) + (uint16_t)u8Len) > PAGE_SIZE)//判断是否跨页
{
u8Len -= (uint8_t)((u16Addr + (uint16_t)u8Len) % PAGE_SIZE);//跨页,截掉跨页的部分
}
for (i = 0; i < (u8Len - 1); i++)
{
*(pBuff + i) = IIC_ReadByte();
IIC_Ack();//主机应答
}
*(pBuff + u8Len - 1) = IIC_ReadByte();
IIC_NoAck();//最后一个不应答
IIC_Stop();//停止信号
}

注意事项

  1. 仅适用于x24C32、x24C64、x24C128、x24C256、x24C512系列EEPROM芯片,其他容量芯片请参考第一节中链接的相关文章;
  2. 器件地址必须与A2/A1/A0引脚的硬件连接对应;
  3. 调用写入程序(无论是单字节写入还是页写),需要延时10ms(即twr,有的芯片手册说是5ms)后再对器件进行操作,否则这段时间内器件不响应命令;

2.示例代码

EEPROM设备都需要一个8位设备地址字,包含一个启动条件,以使芯片能够进行读或写操作。设备地址字前4位最高有效位为1010。这对所有串行EEPROM设备都是通用的。接下来的3位是EEPROM的A2、A1和A0设备地址位。设备地址的第8位是读写操作选择位。如果该位高,则进行读操作;如果该位低,则进行写操作。

综上,如果对AT24C32进行读操作,则设备地址为10100001B=A1H;如果对AT24C32进行写操作,则设备地址为10100000B=A0H.

2.1 读取一个字节数据代码如下(示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*******************************************************************************
* 函数名:AT24CXX_ReadOneByte
* 描述 :从串行EEPROM指定地址处开始读取一个字节数据
* 输入 :_usAddress地址
* 输出 :读取的数据dat
* 调用 :
* 备注 :
*******************************************************************************/
uint8_t AT24CXX_ReadOneByte(uint16_t _usAddress)
{
uint8_t dat = 0;
EE_IIC_Start();
if(EE_SIZE > ((128*16)))//>AT24C16
{
EE_IIC_Send_Byte(EE_DEV_ADDR);//发送写命令
EE_IIC_Wait_Ack();
EE_IIC_Send_Byte(_usAddress>>8);//发送高地址
EE_IIC_Wait_Ack();
}
else
{
EE_IIC_Send_Byte(EE_DEV_ADDR + ((_usAddress/256)<<1));//发送器件地址0xA0,写数据
}
EE_IIC_Send_Byte(_usAddress%256);//发送低地址
EE_IIC_Wait_Ack();
EE_IIC_Start();
EE_IIC_Send_Byte(EE_DEV_ADDR | I2C_RD);//进入接收模式0xA1
EE_IIC_Wait_Ack();
dat = EE_IIC_Read_Byte(0);
EE_IIC_Stop();//产生一个停止条件
return dat;
}

2.2 写入一个字节数据代码如下(示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*******************************************************************************
* 函数名:AT24CXX_WriteOneByte
* 描述 :从串行EEPROM指定地址处开始写入一个字节数据
* 输入 :_usAddress地址,_usWrite数据
* 输出 :void
* 调用 :
* 备注 :
*******************************************************************************/
void AT24CXX_WriteOneByte(uint16_t _usAddress,uint8_t _usWrite)
{
EE_IIC_Start();
if(EE_SIZE > ((128*16)))//>AT24C16
{
EE_IIC_Send_Byte(EE_DEV_ADDR);//发送写命令
EE_IIC_Wait_Ack();
EE_IIC_Send_Byte(_usAddress>>8);//发送高地址
}
else
{
EE_IIC_Send_Byte(EE_DEV_ADDR + ((_usAddress/256)<<1));//发送器件地址0xA0,写数据
}
EE_IIC_Wait_Ack();
EE_IIC_Send_Byte(_usAddress%256);//发送低地址
EE_IIC_Wait_Ack();
EE_IIC_Send_Byte(_usWrite);//发送字节
EE_IIC_Wait_Ack();
EE_IIC_Stop();//产生一个停止条件
delay_syms(10);
}


2.3 读取若干数据代码如下(示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*******************************************************************************
* 函数名:AT24CXX_ReadBytes
* 描述 :从串行EEPROM指定地址处开始读取若干数据
* 输入 : usAddress : 起始地址
* _pReadBuf : 存放读到的数据的缓冲区指针
* _usSize : 数据长度,单位为字节
* 输出 :0 表示失败,1表示成功
* 调用 :
* 备注 :
*******************************************************************************/
void AT24CXX_ReadBytes(uint16_t _usAddress, uint8_t *_pReadBuf, uint16_t _usSize)
{
while(_usSize)
{
*_pReadBuf++ = AT24CXX_ReadOneByte(_usAddress++);
_usSize--;
}
}

2.4 写入若干数据代码如下(示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*******************************************************************************
* 函数名:AT24CXX_WriteBytes
* 描述 :向串行EEPROM指定地址写入若干数据
* 输入 :_usAddress : 起始地址
* _pWriteBuf : 存放读到的数据的缓冲区指针
* _usSize : 数据长度,单位为字节
* 调用 :0 表示失败,1表示成功
* 备注 :
*******************************************************************************/
void AT24CXX_WriteBytes(uint16_t _usAddress, uint8_t *_pWriteBuf, uint16_t _usSize)
{
while(_usSize--)
{
AT24CXX_WriteOneByte(_usAddress,*_pWriteBuf);
_usAddress++;
_pWriteBuf++;
}
}


2.5 测试代码如下(示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*******************************************************************************
* 函数名:DEBUG_test_AT24C32
* 描述 :测试AT24C32
* 输入 :void
* 输出 :void
* 调用 :
* 备注 :
*******************************************************************************/
void DEBUG_test_AT24C32(void)
{
uint8_t i,j;
if(EE_CheckOk() == 1)//检测设备是否存在
{
printf("Check OK \r\n");

for(i = 0; i < 16; i++)
{
test_buf1[i] = i;
}
AT24CXX_WriteBytes(0x20,test_buf1,16);

AT24CXX_ReadBytes(0x20,test_buf2,16);
for(j=0;j<16;j++)
{
printf("test_buf2 = %d\r\n",test_buf2[j]);
}
}
else
{
printf("Check Fail \r\n");
}
}

3.测试结果

和测试代码写的结果是一样,测试代码首先把0-15写入到test_buf1数组里面,再通过写函数把它写入到EEPROM芯片,最后通过读函数将它读取出来给到test_buf2数组,再打印出来。

在这里插入图片描述

其他示例代码一

IIC.H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#ifndef __IIC_H
#define __IIC_H
#include "sys.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//Mini STM32开发板
//IIC 驱动函数
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2010/6/10
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 正点原子 2009-2019
//All rights reserved
//

/*
//IO方向设置
#define SDA_IN() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=8<<12;}
#define SDA_OUT() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=3<<12;}
//IO操作函数
#define IIC_SCL PCout(12) //SCL
#define IIC_SDA PCout(11) //SDA
#define READ_SDA PCin(11) //输入SDA
*/

//IO方向设置
#define SDA_IN() {GPIOA->CRL&=0X0FFFFFFF;GPIOA->CRL|=8<<28;}
#define SDA_OUT() {GPIOA->CRL&=0X0FFFFFFF;GPIOA->CRL|=3<<28;}

//IO操作函数
#define IIC_SCL PAout(6) //SCL
#define IIC_SDA PAout(7) //SDA
#define READ_SDA PAin(7) //输入SDA



//IIC所有操作函数
void IIC_Init(void); //初始化IIC的IO口
void IIC_Start(void); //发送IIC开始信号
void IIC_Stop(void); //发送IIC停止信号
void IIC_Send_Byte(u8 txd); //IIC发送一个字节
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
u8 IIC_Wait_Ack(void); //IIC等待ACK信号
void IIC_Ack(void); //IIC发送ACK信号
void IIC_NAck(void); //IIC不发送ACK信号

void IIC_Write_One_Byte(u8 daddr,u8 addr,u8 data);
u8 IIC_Read_One_Byte(u8 daddr,u8 addr);
#endif

IIC.C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#include "iic.h"
#include "delay.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//Mini STM32开发板
//IIC 驱动函数
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2010/6/10
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 正点原子 2009-2019
//All rights reserved
//
/*
//初始化IIC
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//RCC->APB2ENR|=1<<4;//先使能外设IO PORTC时钟
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE );

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);

IIC_SCL=1;
IIC_SDA=1;
}*/

void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //PA6 SCL
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //PA7 SDA
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

IIC_SCL=1;
IIC_SDA=1;
}


//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;//START:when CLK is high,DATA change form high to low
delay_us(4);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
}


//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL=0;
IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
delay_us(4);
IIC_SCL=1;
delay_us(4);
IIC_SDA=1;//发送I2C总线结束信号
}


//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
IIC_SDA=1;delay_us(1);
IIC_SCL=1;delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;//时钟输出0
return 0;
}


//产生ACK应答
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}


//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}


//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7;
txd<<=1;
delay_us(2); //对TEA5767这三个延时都是必须的
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}


//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)
receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}

24CXX.H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#ifndef __24CXX_H
#define __24CXX_H
#include "iic.h"
//Mini STM32开发板
//24CXX驱动函数(适合24C01~24C16,24C32~256未经过测试!有待验证!)
//正点原子@ALIENTEK
//2010/6/10
//V1.2
#define AT24C01 127
#define AT24C02 255
#define AT24C04 511
#define AT24C08 1023
#define AT24C16 2047
#define AT24C32 4095
#define AT24C64 8191
#define AT24C128 16383
#define AT24C256 32767
#define AT24C512 65535
//Mini STM32开发板使用的是24c02,所以定义EE_TYPE为AT24C02
//#define EE_TYPE AT24C02

#define EE_TYPE AT24C512

u8 AT24CXX_ReadOneByte(u16 ReadAddr); //指定地址读取一个字节
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite); //指定地址写入一个字节
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len);//指定地址开始写入指定长度的数据
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len); //指定地址开始读取指定长度数据
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite); //从指定地址开始写入指定长度的数据
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead); //从指定地址开始读出指定长度的数据

u8 AT24CXX_Check(void); //检查器件
void AT24CXX_Init(void); //初始化IIC
#endif

24CXX.C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include "24cxx.h" 
#include "delay.h"
//Mini STM32开发板
//24CXX驱动函数(适合24C01~24C16,24C32~256未经过测试!有待验证!)
//正点原子@ALIENTEK
//2010/6/10
//V1.2

//初始化IIC接口
void AT24CXX_Init(void)
{
IIC_Init();
}


//在AT24CXX指定地址读出一个数据
//ReadAddr:开始读数的地址
//返回值 :读到的数据
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 temp=0;
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr>>8);//发送高地址
IIC_Wait_Ack(); //这句也得考虑注释掉
}
else
IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送器件地址0XA0,写数据

//IIC_Wait_Ack(); //这句去掉就OK了,就可以通过AT24CXX_Check()了
IIC_Send_Byte(ReadAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0XA1); //进入接收模式
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop();//产生一个停止条件
return temp;
}


//在AT24CXX指定地址写入一个数据
//WriteAddr :写入数据的目的地址
//DataToWrite:要写入的数据
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr>>8);//发送高地址
}
else
{
IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送字节
IIC_Wait_Ack();
IIC_Stop();//产生一个停止条件
delay_ms(10);
}


//在AT24CXX里面的指定地址开始写入长度为Len的数据
//该函数用于写入16bit或者32bit的数据.
//WriteAddr :开始写入的地址
//DataToWrite:数据数组首地址
//Len :要写入数据的长度2,4
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{
u8 t;
for(t=0;t<Len;t++)
{
AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
}
}


//在AT24CXX里面的指定地址开始读出长度为Len的数据
//该函数用于读出16bit或者32bit的数据.
//ReadAddr :开始读出的地址
//返回值 :数据
//Len :要读出数据的长度2,4
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{
u8 t;
u32 temp=0;
for(t=0;t<Len;t++)
{
temp<<=8;
temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1);
}
return temp;
}


//检查AT24CXX是否正常
//这里用了24XX的最后一个地址来存储标志字.24C02是255,24C512是65535
//如果用其他24C系列,这个地址要修改
//返回1:检测失败
//返回0:检测成功
u8 AT24CXX_Check(void)
{
u8 temp;
temp=AT24CXX_ReadOneByte(65535);//避免每次开机都写AT24CXX
if(temp==0X55)
return 0;
else//排除第一次初始化的情况
{
AT24CXX_WriteOneByte(65535,0X55);
temp=AT24CXX_ReadOneByte(65535);
if(temp==0X55)
return 0;
}
return 1;
}


//在AT24CXX里面的指定地址开始读出指定个数的数据
//ReadAddr :开始读出的地址 对24c02为0~255
//pBuffer :数据数组首地址
//NumToRead:要读出数据的个数
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
while(NumToRead)
{
*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}


//在AT24CXX里面的指定地址开始写入指定个数的数据
//WriteAddr :开始写入的地址 对24c02为0~255
//pBuffer :数据数组首地址
//NumToWrite:要写入数据的个数
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
WriteAddr++;
pBuffer++;
}
}



/*
AT24C512的读写问题?- 百度文库
https://wenku.baidu.com/view/cfc89adaa48da0116c175f0e7cd184254b351bb6.html
阿波罗F4/F7开发板例程不支持24C512的解决办法-OpenEdv-开源电子网
http://www.openedv.com/forum.php?mod=viewthread&tid=93162&highlight=24C512
*/

主程序main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "usart.h"
//#include "lcd.h"
#include "key.h"
#include "24cxx.h"
#include "iic.h"
//ALIENTEK Mini STM32开发板范例代码19
//IIC实验
//技术支持:www.openedv.com
//广州市星翼电子科技有限公司

//要写入到24Cxx的字符串数组
const u8 TEXT_Buffer[]={"MiniSTM32 IIC TEST"};
#define SIZE sizeof(TEXT_Buffer)
int main(void)
{ //QQ:1447233384
u8 key;
u16 i=0;
u8 datatemp[SIZE];
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
delay_init(); //延时函数初始化
uart_init(9600); //串口初始化为9600
LED_Init(); //初始化与LED连接的硬件接口
//LCD_Init();
KEY_Init(); //按键初始化
AT24CXX_Init(); //IIC初始化
printf("开机成功\r\n");


while(AT24CXX_Check())//检测不到24c02
{
//LCD_ShowString(60,150,200,16,16,"24C02 Check Failed!");
printf("检测不到\r\n");
delay_ms(500);
//LCD_ShowString(60,150,200,16,16,"Please Check! ");
delay_ms(500);
printf("请检查\r\n");
LED0=!LED0;//DS0闪烁
}
printf("检测存在\r\n");


printf("read \t %d \t.\r\n", AT24CXX_ReadOneByte(65535) );
//读出AT24CXX_Check()中65535位置中的数值


while(1)
{
key=KEY_Scan(0);
if(key==WKUP_PRES)//WK_UP 按下,写入24C02
{
//LCD_Fill(0,170,239,319,WHITE);//清除半屏
//LCD_ShowString(60,170,200,16,16,"Start Write 24C02....");
printf("Start Write 24C512....\r\n");
AT24CXX_Write(0,(u8*)TEXT_Buffer,SIZE);
//LCD_ShowString(60,170,200,16,16,"24C02 Write Finished!");//提示传送完成
printf("24C512 Write Finished!\r\n");
}
if(key==KEY0_PRES)//KEY0 按下,读取字符串并显示
{
//LCD_ShowString(60,170,200,16,16,"Start Read 24C02.... ");
printf("Start Read 24C512.... \r\n");
AT24CXX_Read(0,datatemp,SIZE);
//LCD_ShowString(60,170,200,16,16,"The Data Readed Is: ");//提示传送完成
printf("The Data Readed Is: %s\r\n",datatemp);
//LCD_ShowString(60,190,200,16,16,datatemp);//显示读到的字符串
//printf("Start Read 24C02.... \r\n");
}
i++;
delay_ms(10);
if(i==20)
{
LED0=!LED0;//提示系统正在运行
i=0;
}
}
}

AT24Cxx应用介绍

结论:
1、读写AT24CXX芯片,根据容量有多种方式:
一、容量为AT24C01AT24C16,首先发送设备地址(8位地址),再发送数据地址(8位地址),再发送或者接受数据。
二、AT24C32/AT24C64
AT24C512,首先发送设备地址(8位地址),再发送高位数据地址,再发送地位数据地址,再发送或者接受数据。
三、容量AT24C1024的芯片,是把容量一和容量二的方法结合,设备地址中要用一位作为数据地址位,存储地址长度是17位。
2、它的设备地址根据容量不同有区别:
1)、AT24C01AT24C16:这一类又分为两类,分别为AT24C01/AT24C02和AT24C04AT24C16;他们的设备地址为高7位,低1位用来作为读写标示位,1为读,0为写。
1、AT24C01/AT24C02。AT24C01/AT24C02的A0、A1、A2引脚作为7位设备地址的低三位,高4为固定为1010B,低三位A0、A1、A2确定了AT24CXX的设备地址,所以一根I2C线上最大可以接8个AT24CXX,地址为1010000B1010111B。
2、AT24C04
AT24C16的 A0、A1、A2只使用一部分,不用的悬空或者接地(数据手册中写的是悬空不接)。举例:AT24C04只用A2、A1引脚作为设备地址,另外一位A0不用悬空,发送地址中对应的这位(A0)用来写入页寻址的页面号,一根I2C线上最大可以接4个,地址为101000xB~101011xB
2)、AT24C32/AT24C64:和AT24C01/AT24C02一样,区别是,发送数据地址变成16位。

注意事项:对AT24C32来说,WP置高,则只有四分之一受保护,即0x0C00-0x0FFF。也就是说保护区为1KBytes。对于低地址的四分之三,则不保护。所以,如果数据较多时,可以有选择地存储。不重要的数据则放在低四分之三区域,重要的数据则放在高四分之一区域。

详细如下:
图AT24C01/02/04/08/16的外形级封装和引脚说明

AT24C系列为美国ATMEL公司推出的串行COMS型E2PROM,是典型的串行通信E2PROM 。

AT24CXX是IIC总线串行器件,具有工作电源宽(1.8~6.0 V),抗干扰能力强(输入引脚内置施密特触发器滤波抑制噪声),功耗低(写状态时最大工作电流3 mA),高可靠性(写次数100万次,数据保存100年),支持在线编程等特点.

从上面两张图片可以得知:

AT :ATMEL公司出品

24: 系列号

C :商业

XX : 存储容量 ,举例 01 –> 1K à 128 字节

02 à 2K à 256 字节

…………….

16à 16K à 2K 字节

I2C总线协议规定,任何将数据传送到总线的器件作为发送器。任何从总线接收数据的器件为接收器。

主器件控制串行时钟和起始、停止信号的发生。主器件任何期间都可以发送或接收数据,但是主器件控制数据传送模式(发送或者接收)。

WP写保护引脚:当该引脚连接到VCC,I2C器件内的内容被写保护(只能读)。如果允许对器件进行正常的读写,那么WP引脚需连接到地或者悬空。

通过器件地址输入端A0、A1、A2可以实现讲最多8个at24c01器件和at24c02器件、4个at24c04器件、2个at24c08器件、1个at24c16器件连接到总线上。当总线上只有一个器件时,A0、A1、A2可以连接到地或者悬空。

下面重点分析一下这句话:为什么 at24c01/at24c02 可以挂接8个器件呢?为什么到了 at24C16 却只能挂接1个器件呢???

WHY?

器件识别控制字节的作用

以at24c01/at24c02 和at24C16 举例:

I2C总线上所有外围器件都有唯一的地址,这个地址由器件地址和引脚地址两部分组成。共7位。

器件地址是I2C器件固有的地址编码,器件出厂时已经给定,不可更改。

引脚地址由I2C总线外围器件的地址引脚A0、A1、A2决定,根据其在电路中接电源正极、接地或者悬空的不同,形成不同的地址代码。引脚地址数也决定了同一器件可接入总线的最大数目。

此时于引脚地址无关,与 P2、P1、P0有关,即页地址有关,页地址高三位是器件识别控制字节的1-3位,器件上的A0,A1,A2,就无效了,所以只能接1个AT24C16器件。

页地址是什么乖乖,从何而来呢????

上图可知 AT24C16 存储容量 16K = 2K字节 = 128(页面数)* 16 (每页的字节数) = 2^11 (寻址地址位数 11位)。

AT24C16内部有2048*8位的存储容量,即可以存储2K字节的数据。这2K字节被放在128个页内,每页存放16个字节。所以对AT24C16内部的访问需要11位地址(0-7ff)。

举个实际的例子:

对AT24C16访问时,按照页地址和页偏移量的方式进行访问。

比如要访问第100页的第3个字节,则在发送寻址的时候,就要发送0X0643,其中页地址的高三位放在器件地址中。

第100页的第3个字节 == 0X0643

0643 = 6 * 256 + 4 * 16 + 3 = (616+4)16 + 3 = 1603

就是 100页的第3个字节。

所以在编写程序对AT24C16第100页的第3个字节进行写数据的时候,步骤如下:

1)发送起始信号;

2)发送器件地址0XAC(1010 1100,1010是固定地址,110是页地址的高三位,0表示写操作);

3)发送操作地址0X43(0100 0010,0100是页地址的低四位,0010是页地址偏移量,即第100页内的第三个字节;

4)发送要写的数据;

5)发送终止信号。

STM32 AT24CXX器件地址的理解(IIC通讯协议)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (EE_TYPE > AT24C16)      /* 24C16以上的型号, 分2个字节发送地址 */
{
iic_send_byte(0XA0); /* 发送写命令, IIC规定最低位是0, 表示写入 */
iic_wait_ack(); /* 每次发送完一个字节,都要等待ACK */
iic_send_byte(addr >> 8);/* 发送高字节地址 */
}
else
{
iic_send_byte(0XA0 + ((addr >> 8) << 1)); /* 发送器件 0XA0 + 高位a8/a9/a10地址,写数据 */
}

iic_wait_ack(); /* 每次发送完一个字节,都要等待ACK */
iic_send_byte(addr % 256); /* 发送低位地址 */
iic_wait_ack(); /* 等待ACK, 此时地址发送完成了 */

EETYPE为宏定义,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#define AT24C01     127
#define AT24C02 255
#define AT24C04 511
#define AT24C08 1023
#define AT24C16 2047
#define AT24C32 4095
#define AT24C64 8191
#define AT24C128 16383
#define AT24C256 32767

/* 开发板使用的是24c02,所以定义EE_TYPE为AT24C02 */

#define EE_TYPE AT24C02

根据尼使用得AT24C型号得不同,给EE_TYPE初始化不同的值。先讨论C16以下的

 *    对于24C01/02, 其器件地址格式(8bit)为: 1  0  1  0  A2  A1  A0  R/W
 *    对于24C04,    其器件地址格式(8bit)为: 1  0  1  0  A2  A1  a8  R/W
 *    对于24C08,    其器件地址格式(8bit)为: 1  0  1  0  A2  a9  a8  R/W
 *    对于24C16,    其器件地址格式(8bit)为: 1  0  1  0  a10 a9  a8  R/W

R/W: 读/写控制位 0,表示写; 1,表示读;
A0/A1/A2: 对应器件的1,2,3引脚(只有24C01/02/04/8有这些脚,16以上的芯片没设计这些脚)
a8/a9/a10: 对应存储地址的高位。

比如24C04 有512byte(32page16byte),地址本质都是0 1这种二进制(经常我们看到的地址表示形式是0x00000008这种,是32位机下,四个二进制转换成一个十六进制形成的),一位二进制(2的一次方)能代表两个地址块,2位二进制(2的平方)能代表四个地址块,512个地址块就需要2的九次方,所以每个地址需要9位来表示来能把这521byte覆盖完。在发送时,只能一个字节一个字节的发送,你要一口气发九个字节,从机会领悟不到的,所以可以先把高位的1位弄成00000001发送,再发最后的八位,加上1 0 1 0 A2 A1 A0 R/W,一共要发三次才能确定要读或者写的地址,麻烦!!

所以我们把多出来的1位(第九位,这个最高位)放到1 0 1 0 A2 A1 A0 R/W的A0位去传输,这样只用传输两次即可。如果多了两位就把多的放在A1,和A0位去传输。


以上方法最多11bit地址,最多可以表示2048个位置,可以寻址24C16及以内的型号

针对C32以后的,不够用了,所以干脆就大大方方的先发1 0 1 0 A2 A1 A0 R/W,再分两次发送地址


iic_send_byte(0XA0 + ((addr >> 8) << 1));

这句代码的理解:addr >> 8左移八位,把八位之后的东西留下来再左移一位后与0XA0相加,左移这一位相当于是给R/W腾了个位置,通过这句代码,把地址的高x位取出来了,然后随着1 0 1 0 A2 A1 A0 R/W发送出去。


相关链接(侵删)

  1. AT24C32、AT24C64、AT24C128、AT24C256、AT24C512系列EEPROM芯片单片机读写驱动程序
  2. 串行 EEPROM 存储器芯片AT24C32(兼容同系列AT24CXX)
  3. AT24CXX系列芯片在STM32F103单片机下的读写程序
  4. EEPROM—AT24Cxx应用介绍
  5. STM32 AT24CXX器件地址的理解(IIC通讯协议)

=================我是分割线=================

欢迎到公众号来唠嗑: