STM32软件模拟IIC---读写驱动AT24Cxx

以下内容皆是个人学习过程中的总结,记录一下整个过程,用于后期复习,如有不对之处,麻烦各位大佬指出~

(喜欢的朋友麻烦点个关注~~~ 后期还会进行持续更新)

概述

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

一、模拟IIC

1.1、IIC简介

IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行线,用于连接微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。在 CPU 与被控 IC 之间、 IC 与 IC 之间进行双向传送, 高速 IIC 总线一般可达 400kbps 以上。

1.2、IIC详解

IIC:两线式串行总线,它是由数据线SDA时钟线SCL构成的串行总线,可发送和接收数据。

在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbs以上。

时钟线SCL:在通信过程起到控制作用。 
数据线SDA:用来一位一位的传送数据。

并且IIC又分为软件模拟IIC以及硬件IIC

软件IIC:软件IIC通信指的是用单片机的两个I/O端口模拟出来的IIC,用软件控制管脚状态以模拟I2C通信波形,软件模拟寄存器的工作方式。

硬件IIC:一块硬件电路,硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用的,硬件(固件)I2C是直接调用内部寄存器进行配置。

补充:

1.硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活。
2.IIC是半双工通信方式

IIC总线上可以挂很多设备:多个主设备,多个从设备(外围 设备)。上图中主设备是两个单片机,剩下的都是从设备 

多主机会产生总线裁决问题。当多个主机同时想占用总线时,企图启动总线传输数据,就叫做总线竞争。I2C通过总线仲裁,以决定哪台主机控制总线

上拉电阻一般在4.7k~10k之间

每个接到I2C总线上的器件都有唯一的地址。主机与其它器件间的数据传输可以是由主机发送数据到其它器件,这时主机 即为发送器,总线上收数据的器件则为接收器。

1.3、IIC通信详解

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

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

IIC 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答信号。
开始信号: SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据。
结束信号: SCL 为高电平时, SDA 由低电平向高电平跳变,结束传送数据。
应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号, CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障
这些信号中,起始信号是必需的,IIC 总线时序图如图所示

I2C总线数据传送,数据位的有效性规定: 

接着IIC通信过程由开始、结束、发送、响应、接收五个部分构成

 1、(在发送、接收数据的时候)当SCL为高电平时,SDA线不允许变化;当SCL线为低电平时,SDA线可以任意0、1变化,因此要传输数据的时候,SDA线要提前拉高,以保证SCL拉高之后的稳定性
 2、(在任意时候)只有当SCL为高电平时,IIC电路才对SDA线上的电平(0或者1)进行记录,当SCL线为低电平时,无论SDA是高还是低,IIC电路都不对SDA进行采样。

1.3.1、空闲状态

在介绍上面五个部分前,我们首先说说空闲状态,什么是空闲状态,就是没有通信时的状态初始状态

I2C总线的SDA和SCL两条信号同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。

1.3.2、开始信号与停止信号

开始信号:当SCL为高期间,SDA由高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平。
停止信号:当SCL为高期间,SDA由低到高的跳变;停止信号也是一种电平跳变时序信号,而不是一个电平信号。

起始信号和终止信号都是由主机发送的。在起始信号产生之后,总线就处于被占用的状态,在终止信号产生之后,总线就处于空闲状态。

连接到I2C总线上的器件,若具有I2C总线的硬件接口,则很容易检测到起始和终止信号。

开始信号代码:

//产生IIC起始信号
//1.设置SDA输出
//2.先拉高SDA,再拉高SCL,空闲状态
//3.拉低SDA
//4.准备接收数据
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停止信号
//1.设置SDA输出
//2.先拉低SDA,再拉低SCL
//3.拉高SCL
//4.拉高SDA
//5.停止接收数据
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; 
	IIC_SDA=1;//发送I2C总线结束信号
	delay_us(4);                            
}

1.3.3、应答信号

发送器每发送一个字节,就在时钟脉冲9期间释放数据先,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间位稳定的低电平。如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号P

每当发送器传输完一个字节的数据之后,发送端会等待一定的时间,等接收方的应答信号。接收端通过拉低SDA数据线,给发送端发送一个应答信号,以提醒发送端我这边已经接受完成,数据可以继续传输,接下来,发送端就可以继续发送数据了

每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

应答信号代码:

//产生ACK应答
//这里就很清楚了,产生应答:SCL在SDA一直为低电平期间完成低高电平转换
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应答    
//这里就很清楚了,不产生应答:SCL在SDA一直为高电平期间完成低高电平转换
void IIC_NAck(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=1;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
}

 1.3.4、发送数据

在I2C总线上传送的每位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,SDA逐位地串行传送每一位数据。数据位的传输是边沿触发。
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答            

//IIC_SCL=0;
//在SCL上升沿时准备好数据,进行传送数据时,拉高拉低SDA,因为传输一个字节,一个SCL脉冲里传输一个位。
//数据传输过程中,数据传输保持稳定(在SCL高电平期间,SDA一直保持稳定,没有跳变)
//只有当SCL被拉低后,SDA才能被改变
//总结:在SCL为高电平期间,发送数据,发送8次数据,数据为1,SDA被拉高,数据为0,SDA被拉低。
//传输期间保持传输稳定,所以数据线仅可以在时钟SCL为低电平时改变。
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
	SDA_OUT(); 	    
    IIC_SCL=0;//拉低时钟开始数据传输
    for(t=0;t<8;t++)
    {  
        //IIC_SDA=(txd&0x80)>>7;
        //获取数据的最高位,然后左移7位
        //如果某位为1,则SDA为1,否则相反            
        IIC_SDA=(txd&0x80)>>7;
        txd<<=1; 	  
		delay_us(2);   //对TEA5767这三个延时都是必须的
		IIC_SCL=1;
		delay_us(2); 
		IIC_SCL=0;	
		delay_us(2);
    }
}

单片机发送完一个字节后面必须跟一个等外应答函数:

思路:先让SDA=1,再判断在一定时间内SDA是否变为0,从而识别出外设有没有发送应答信号。

//等待应答信号到来
//返回值:1,接收应答失败
//        0,接收应答成功
//1.设置SDA为输入
//2.拉高SDA
//3.拉高SCL
//4.等待接收器返回应答信号,如果数据线SDA一直为高,就一直等待,并返回1(无效应答),如果数据线SDA为低,返回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; 
}

 1.3.5、接收数据

发送数据是一位一位发送,接收数据也是一位一位接收进来,最后返回应答信号

//读1个字节,ack=1时,发送ACK,ack=0,发送nACK   

//先拉低SCL,延时后拉高
//读取数据
//是否发送应答
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;
}

 1.3.6、IIC主要代码

iic.c

#include "iic.h"
#include "delay.h"

//初始化IIC
void IIC_Init(void)
{					     
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOB, ENABLE );	//使能GPIOB时钟
	   
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;   //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7); 	//PB6,PB7 输出高,空闲状态
}

//产生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; 
	IIC_SDA=1;    //发送I2C总线结束信号
	delay_us(4);							   	
}

//发送数据后,等待应答信号到来
//返回值:1,接收应答失败,IIC直接退出
//        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;
		if((txd&0x80)>>7)
			IIC_SDA=1;
		else
			IIC_SDA=0;
		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;
}

iic.h

#ifndef __IIC_H
#define __IIC_H	 
#include "sys.h"

位段定义 方便后面直接操作IO口   Reg:寄存器地址  Bit:该寄存器的第多少位
//#define BITBAND_REG(Reg,Bit) (*((uint32_t volatile*)(0x42000000u + (((uint32_t)&(Reg) - (uint32_t)0x40000000u)<<5) + (((uint32_t)(Bit))<<2))))

//IO方向设置
#define SDA_IN()  {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}
#define SDA_OUT() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}
 
//IO操作函数	 
#define IIC_SCL    PBout(6) //SCL
#define IIC_SDA    PBout(7) //SDA	 
#define READ_SDA   PBin(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信号

#endif

 二、AT24Cxx

AT :ATMEL公司出品

24:  系列号

C :商业

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

    02 à 2K à 256 字节

   …………….

    16à 16K à 2K 字节

2.1、AT24CXX注意事项

  1. 在产品中尽量使用WP写保护引脚;
  2. 程序中尽量减少不必要的AT24CXX的读写;
  3. 如果是大数据,尽量使用多字节读写,不要使用单字节读写

2.2、AT24CXX设备地址

型号容量bit容量byte页数字节/页器件寻址位可寻址器件数WordAddress位数/字节数备注
AT24C011k128168A2A1A087/1
AT24C022k256328A2A1A088/1
AT24C044k5123216A2A149/1WordAddress使用P0位
AT24C088k10246416A2210/1WordAddress使用P0、P1位
AT24C1616k204812816-111/1WordAddress使用P0、P1、P2位
AT24C3232k4k12832A2A1A0812/2
AT24C6464k8k25632A2A1A0813/2
AT24C128128k16k25664A1A0414/2
AT24C256256k32k51264A1A0415/2
AT24C512512k64k512128A2A1A0816/2

如上图所示,根据AT24CXX容量不同,设备地址也不同;如,
AT24C01/AT24C02:A0、A1、A2引脚作为7位设备地址的低三位,高4位固定为1010B,低三位A0、A1、A2(接GND为0,接VCC为1)确定了AT24CXX的设备地址,所以一根I2C线上最大可以接8个AT24CXX,地址为1010000B~1010111B。
AT24C04~AT24C16: A0、A1、A2只使用一部分,不用的悬空或者接地(数据手册中写的是悬空不接)。举例:AT24C04只用A2、A1引脚作为设备地址。另外一引脚P0(即原来A0的位置)没有使用,PCB中可悬空,发送地址中对应的这位(P0)用来写入页寻址的页面号,因此一根I2C线上最大可以接4个AT24C04,地址为101000xB~ 101011xB;同理,一根I2C线上最大可以接2个AT24C08,地址为10100xxB ~ 10101xxB;一根I2C线上最大可以接1个AT24C16,地址为1010xxxB ~ 1010xxxB;
AT24C32/AT24C64: 和AT24C01/AT24C02一样,区别是,发送数据地址变成16位。先发送设备地址高8位,再发送设备地址8位。

并且容量为AT24C01~AT24C16,首先发送设备地址(8位地址),再发送数据地址(8位地址),再发送或者接受数据。

AT24C32/AT24C64~AT24C512,首先发送设备地址(8位地址),再发送高位数据地址,再发送地位数据地址,再发送或者接受数据。

AT24C32/AT24C64~AT24C512,首先发送设备地址(8位地址),再发送高位数据地址,再发送地位数据地址,再发送或者接受数据。

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

2.3、AT24Cxx读写操作

在本篇文章,我们使用的是AT24C16

I2C总线协议规定,任何将数据传送到总线的器件作为发送器。任何从总线接收数据的器件为接收器。
主器件控制串行时钟和起始、停止信号的发生。主器件任何期间都可以发送或接收数据,但是主器件控制数据传送模式(发送或者接收)。WP写保护引脚:当该引脚连接到VCC,I2C器件内的内容被写保护(只能读)。如果允许对器件进行正常的读写,那么WP引脚需连接到地或者悬空。通过器件地址输入端A0、A1、A2可以实现讲最多8个at24c01器件和at24c02器件、4个at24c04器件、2个at24c08器件、1个at24c16器件连接到总线上。当总线上只有一个器件时,A0、A1、A2可以连接到地或者悬空

以at24c01/at24c02 和at24C16 举例:
I2C总线上所有外围器件都有唯一的地址,这个地址由器件地址和引脚地址两部分组成。共7位。
器件地址是I2C器件固有的地址编码,器件出厂时已经给定,不可更改。
引脚地址由I2C总线外围器件的地址引脚A0、A1、A2决定,根据其在电路中接电源正极、接地或者悬空的不同,形成不同的地址代码。引脚地址数也决定了同一器件可接入总线的最大数目

AT24C16与AT24C01/02/04/08 不同,它引脚的A2,A1,A0是无效的,也就是它没有自己独立的地址,总线上只能挂一个AT24C16设备。

AT24C16总共2048字节,分为128页,每页16字节,地址范围是0~2047。

128页只需要7位地址,分为高3位和低4位,高3位在设备地址中,低4位在字节地址中。

设备地址:1010+页地址高3位+读写方向(1:读  0:写)

字节地址:页地址高4位+4位页内偏移地址

例如读写地址:1864 ,首先计算该地址是多少页的多少个字节,1864/16=116(0x74)页,1864%16=8(0x08),即116页的第8个字节

其中页地址0x74=0 1 1 1 0 1 0 0,最高位忽略,分为D6、D5、D4(高3位)和D3~D0(低4位)两个部分 。

可以计算出 设备地址和字节地址:

设备地址:1010+111+0/1  (AT24C16设备地址高4位固定为1010)

字节地址:0100+1000(高4位是页地址低4位,低4位是页内偏移地址,即0x08)

举个实际的例子:
对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)发送终止信号。

总线寻址:

主机向从机发送8位数据,这8位数据是在起始信号之后发送的第一个字节,后面的字节都是数据,不再是寻址,除非又重新来一个起始信号。

 主机给从机发送第一个字节(总线寻址那个字节),若是读命令,则从机接收到该 命令之后,主动往主机发送数据。

  主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/T位将自己确定为发送器和接收器

从机地址的确定:第0位是读写位。(如对于24C02这块存储器,它若作为从机,那么它的地址中7~4位是固定的,更改不了,第3~1位是可以更改的,每一位根据硬件的管教连接来确定,连接高电平那就是1,低电平就是0)

在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/T),用“0”表示主机发送数据(T),“1”表示主机接收数据(R)。

每次数据传送总是由主机产生的终止信号来结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。

数据传输组合方式:

主机向从机发送数据,数据传送方向在整个传递过程中不变:

主机在第一个字节后,立即从从机读数据(传输方向不变):

在传送过程中,当需要改变传递方向时,起始信号和从机地址都被重复一次产生一次,但两次读/写方向位正好相反

注:主机做的都是编程控制,从机做的都是自主控制,也可以说是硬件控制,如主机给应答信号是编程控制,但是从机给应答信号是硬件控制,我们只需要检查在SDA为高期间,SCL保持低电平一些时间,即可判定从机给了主机应答信号 

2.3.1、写操作

Byte Write写一个字节

上图是x24C04(实际为BR24G04)的写单个字节的时序,可看出与x24c01/x24c02的写单个字节基本相同,不同的是SlaveAddress中只有A2、A1两位表示硬件地址,另外一位为P0,用来扩展内存字节的地址。x24C08则只有一位A2表示器件的硬件地址,页选择位有P1、P0两位,x24C16没有硬件地址位,也就是说使用x24C16只能在同一条IIC总线上连接1个器件,本来表示地址的3个bit全部用作“页选择位”P2、P1、P0。我们可以通过一些设置,将这3款芯片的读单字节的驱动程序统一起来

Page Write写一页

上图是x24C04的页写时序,与x24C01、x24C02的也基本相同,仅红框中的部分有区别,和3.1.1中的写单个字节一样,器件地址只有2位,另一位为页选择位;x24C08、x24C16与此类似

 2.3.2、读操作

读任意地址

上图是x24C04的读任意地址时序,同样,读任意地址的时序与x24C01、x24C02的也基本相同,只是第一次发送的SlaveAddress包含页选择位P0-P2。

顺序读(页读)

上图是x24C04的顺序读即页读的时序,与前述类似,顺序读在发送SlaveAddress的时候,也会包含页选择位。

2.4、主要代码

AT24Cxx.c

#include "AT24Cxx.h"
#include "delay.h"
#include "stdio.h"
 
//初始化IIC接口
void AT24CXX_Init(void)
{
    IIC_Init();
}
 
#if EE_TYPE<=AT24C08  //24C01/02/08
//在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();//等待应答
    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);
}
#else  //24C16
/*****************************************************************
*函数名: AT24CXX_ReadOneByte(u16 ReadAddr)
*功能:AT24CXX 读指定地址的一个字节   AT24C16使用
*调用:底层I2C读写函数
*被调用:外部调用
*形参:
			ReadAddr:要读取的地址
*返回值:返回读取的数据
*其他:每次读就启动一次I2C时序
*****************************************************************/
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
    unsigned char Page=0,WordAddress=0,DeviceAddress=0xA0;
    u8 temp=0;
    Page=ReadAddr/AT24CXX_Page_Size;
    WordAddress=(ReadAddr%AT24CXX_Page_Size) & 0x0F;
    DeviceAddress |= (((Page<<1) & 0xE0)>>4);//High 3 bits
    WordAddress |= (Page & 0x0F)<<4;//Low 4 bits
    IIC_Start(); //起始信号
    IIC_Send_Byte(DeviceAddress&0xFE);//发送设备地址+写方向
    IIC_Wait_Ack();//等待应答
    IIC_Send_Byte(WordAddress);//发送字节地址
    IIC_Wait_Ack();//等待应答
    IIC_Start();                //起始信号
    IIC_Send_Byte(DeviceAddress|0x01);//发送设备地址+读方向
    IIC_Wait_Ack();//等待应答
    temp=IIC_Read_Byte(0);
    IIC_Stop();//产生一个停止条件
    return temp;
}
/*****************************************************************
*函数名: AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
*功能:AT24CXX 向指定地址写入一个字节   AT24C16使用
*调用:
*被调用:外部调用
*形参:
			WriteAddr:要写入的地址
			DataToWrite:写入的数据
*返回值:无
*其他:每次写就启动一次I2C时序
*****************************************************************/
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
    unsigned char Page=0,WordAddress=0,DeviceAddress=0xA0;
    Page=WriteAddr/AT24CXX_Page_Size;
    WordAddress=(WriteAddr%AT24CXX_Page_Size) & 0x0F;
    DeviceAddress |= (((Page<<1) & 0xE0)>>4);//High 3 bits
    WordAddress |= (Page & 0x0F)<<4;//Low 4 bits
#if DEBUG	> 0
    printf("Page:%x\r\n",Page);
    printf("WordAddress:%x\r\n",WordAddress);
    printf("DeviveAddress:%x\r\n",DeviceAddress);
#endif
    IIC_Start(); //启始信号
    IIC_Send_Byte(DeviceAddress);//发送设备地址
    IIC_Wait_Ack();//等待应答
    IIC_Send_Byte(WordAddress);//发送字节地址
    IIC_Wait_Ack();//等待应答
    IIC_Send_Byte(DataToWrite);     //发送要写入的数据
    IIC_Wait_Ack();//等待应答
    IIC_Stop();//产生一个停止条件
    delay_ms(10);
}
#endif
 
 
 
/*---------------读写方式选择-----------------*/
#if  QuickWR == 0
//在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++;
    }
}
#else  //快速读写方式
/*****************************************************************
*函数名: AT24CXX_Write_Bytes(u8 *pBuffer,u16 WriteAddress,u8 Len)
*功能: 页写函数 最多写入一页(16字节)
*调用: 底层I2C写函数
*被调用:外部调用
*形参:
      *pBuffer:指向写入缓存区
			WriteAddr:要写入的地址
			Len:写入数据长度
*返回值:无
*其他:启动一次I2C时序最多写入一页(16Bytes)数据,明显快于按字节写入
*****************************************************************/
void AT24CXX_Write_Bytes(u8 *pBuffer,u16 WriteAddress,u8 Len)
{
    unsigned char Page=0,WordAddress=0,DeviceAddress=0xA0;
    u8 i=0;
    Page=WriteAddress/AT24CXX_Page_Size;
    WordAddress=(WriteAddress%AT24CXX_Page_Size) & 0x0F;
    DeviceAddress |= (((Page<<1) & 0xE0)>>4);//High 3 bits
    WordAddress |= (Page & 0x0F)<<4;//Low 4 bits
    IIC_Start();//启始信号
    IIC_Send_Byte(DeviceAddress);//发送设备地址
    IIC_Wait_Ack();//等待应答
    IIC_Send_Byte(WordAddress);//发送字节地址
    IIC_Wait_Ack();//等待应答
    for(i=0; i<Len; i++)
    {
        IIC_Send_Byte(*pBuffer++);//发送字节地址
        IIC_Wait_Ack();//等待应答
    }
    IIC_Stop();//产生一个停止条件
    delay_ms(10);
}
/*****************************************************************
*函数名: AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
*功能:AT24CXX 快速写入不定量字节
*调用:
*被调用:外部调用
*形参:
			WriteAddr:要写入的首地址
			*pBuffer:指向写入缓存区
			NumToWrite:写入的字节数
*返回值:无
*其他:快速模式 不用每次写一个字节就启动一次I2C时序
*****************************************************************/
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
    unsigned char NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
    Addr=WriteAddr%AT24CXX_Page_Size;//地址正好是16字节对齐
    count=AT24CXX_Page_Size-Addr;//不对齐字节数
    NumOfPage=NumToWrite/AT24CXX_Page_Size;//需要写入多少页
    NumOfSingle=NumToWrite%AT24CXX_Page_Size;//剩余需写入的字节数
    if(0==Addr)//如果地址对齐
    {
        if(NumToWrite<=AT24CXX_Page_Size)//写入字节<=1页
        {
            AT24CXX_Write_Bytes(pBuffer,WriteAddr,NumToWrite);
        }
        else
        {
            while(NumOfPage--)//按页写入
            {
                AT24CXX_Write_Bytes(pBuffer,WriteAddr,AT24CXX_Page_Size);
                pBuffer+=AT24CXX_Page_Size;
                WriteAddr+=AT24CXX_Page_Size;
            }
            if(NumOfSingle != 0)//如果还剩下字节
            {
                AT24CXX_Write_Bytes(pBuffer,WriteAddr,NumOfSingle);//把剩下的字节写入
            }
        }
    }
    else//地址不对齐
    {
        if(NumToWrite<=count)// 要写入的字节数<=count
        {
            AT24CXX_Write_Bytes(pBuffer,WriteAddr,NumToWrite);//写入实际字节数
        }
        else//要写入字节数大于count
        {
            AT24CXX_Write_Bytes(pBuffer,WriteAddr,count);//现将count个字节写入 写入后 地址刚好对齐
            NumToWrite-=count;//计算剩余字节数
            pBuffer+=count;//写入内容偏移count
            WriteAddr+=count;//写入地址偏移count
 
            NumOfPage=NumToWrite/AT24CXX_Page_Size;//需要写入多少页
            NumOfSingle=NumToWrite%AT24CXX_Page_Size;//剩余需写入的字节数
 
            while(NumOfPage--)//先按页写入
            {
                AT24CXX_Write_Bytes(pBuffer,WriteAddr,AT24CXX_Page_Size);
                pBuffer+=AT24CXX_Page_Size;
                WriteAddr+=AT24CXX_Page_Size;
            }
            if(NumOfSingle != 0)//还剩余字节
            {
                AT24CXX_Write_Bytes(pBuffer,WriteAddr,NumOfSingle);//把剩下的字节写入
            }
        }
    }
}
 
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
    unsigned char Page=0,WordAddress=0,DeviceAddress=0x50;
    Page=ReadAddr/AT24CXX_Page_Size;
    WordAddress=(ReadAddr%AT24CXX_Page_Size) & 0x0F;
    DeviceAddress |= (((Page<<1) & 0xE0)>>4);//High 3 bits
    WordAddress |= (Page & 0x0F)<<4;//Low 4 bits
    while(NumToRead)
    {
        *pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
        NumToRead--;
    }
}
 
#endif  //快速读写方式
 
//检查AT24CXX是否正常
//这里用了24XX的最后一个地址(255)来存储标志字.
//如果用其他24C系列,这个地址要修改成该系列的最后一个字节地址
//返回1:检测失败
//返回0:检测成功
u8 AT24CXX_Check(void)
{
    u8 temp;
    temp=AT24CXX_ReadOneByte(255);//避免每次开机都写AT24CXX
    if(temp==0X55)return 0;
    else//排除第一次初始化的情况
    {
        AT24CXX_WriteOneByte(255,0X55);
        temp=AT24CXX_ReadOneByte(255);
        if(temp==0X55)return 0;
    }
    return 1;
}
 
 

AT24Cxx.h

#ifndef __AT24CXX_H
#define __AT24CXX_H
#include "IIC.h"
//24CXX驱动函数(适合24C01~24C16,24C32~256未经过测试!有待验证!)
//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
 
 
/*----------------EEPROM相关配置--------------------*/
#define EE_TYPE AT24C16  //EEPROM类型
#define AT24CXX_Page_Size 16 //AT24C16每页有16个字节
#define DEBUG   0  //串口调试开关
#define QuickWR 0 //快速读写开关
 
 
u8 AT24CXX_ReadOneByte(u16 ReadAddr);							//指定地址读取一个字节
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite);		//指定地址写入一个字节
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

    main.c

#include "led.h"
#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "AT24cxx.h" 
#include "IIC.h"
#include "stdio.h"
//要写入到24c16的字符串数组
const u8 TEXT_Buffer[]={"C++ is the best language!"};//要写入的内容
#define SIZE sizeof(TEXT_Buffer)	//写入内容的大小
#define ADDRESS 2020	//读写地址
 int main(void)
 { 
	u8 datatemp[SIZE];
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
	delay_init();	    	 //延时函数初始化	  
	uart_init(9600);	 	//串口初始化为9600
	LED_Init();		  		//初始化与LED连接的硬件接口		 	
	AT24CXX_Init();			//IIC初始化 	
 	while(AT24CXX_Check())//检测不到24c16
	{		
		delay_ms(500);
		LED0=!LED0;//DS0闪烁
	}	  
	while(1)
	{			
			   		
			AT24CXX_Write(ADDRESS,(u8*)TEXT_Buffer,SIZE);
			printf("Write:%s\r\n",TEXT_Buffer); //显示写入内容		
		    delay_ms(1000);		
			AT24CXX_Read(ADDRESS,datatemp,SIZE);
			printf("Read:%s\r\n",datatemp);//显示读取内容 						
	}
}

注:此代码可用于AT24C01~~AT24C512系列,但博主只测试了AT24C08/16,其余系列待验证

参考资料:

(5条消息) STM32快速读写AT24C16 代码 模拟I2C_Ruler.的博客-CSDN博客_at24c16读写程序

(5条消息) AT24C32、AT24C64、AT24C128、AT24C256、AT24C512系列EEPROM芯片单片机读写驱动程序_wanglong3713的博客-CSDN博客_at24c512驱动

IIC通信协议详解 - -零 - 博客园 (cnblogs.com)

代码下载:

https://download.csdn.net/download/m0_59601101/85659458

        

  • 10
    点赞
  • 80
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值