STM32(十二)通过I2C总线向EEPROM(AT24C02 )读写数据的过程

一、概述

(1)背景

  • I2C(IIC,Inter-Integrated Circuit)总线是由Philips公司开发的一种简单、双向二线制同步串行总线。
  • 它只需要两根线即可在连接于总线上的器件之间传送信息。
  • 主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件。
  • I2C总线简化了硬件电路PCB布线,降低了系统成本,提高了系统可靠性。因为I2C芯片(如mpu6050、ft5x06等)除了这两根线和少量中断线,与系统再没有连接的线,用户常用IC可以很容易形成标准化和模块化,便于重复利用。

 

                     

(2)传输方向

  •   在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。
  • 如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送。
  • 如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下,主机负责产生定时时钟和终止数据传送。

(3)速度

  • 连接到相同总线上的IC数量只受总线最大电容的限制,串行的8位双向数据传输位速率在标准模式下可达100Kbit/s快速模式下可达400Kbit/s高速模式下可达3.4Mbit/s。
  • 总线具有极低的电流消耗抗高噪声干扰,增加总线驱动器可以使总线电容扩大10倍,传输距离达到15m兼容不同电压等级的器件工作温度范围宽。

(4)地址

  I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址(地址通过物理接地或者拉高,可以从I2C器件的数据手册得知,如AT24C02芯片,7位地址依次1010xxx, 最低三位可配,如果全部物理接地,则该设备地址为0x50),主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把STM32作为主设备,把挂接在总线上的其他设备都作为从设备。

 二、AT240C02 EEPROM介绍

(1)特点

  • 宽范围的工作电压1.8V~5.5V低电压技术
  • 1mA典型工作电流- 1uA典型待机电流·存储器组织结构
  • 24C02,256 X8(2K bits)- 24C04,512×8(4K bits)- 24C08,1024 × 8 (8K bits)-24C16,2048 ×8(16K bits)-24C32,4096 X8(32K bits)- 24C64,8192 × 8(64K bits)
  • 2线串行接口,完全兼容l2C总线
  • I2C时钟频率为1 MHz (5V),400 kHz (1.8V,2.5V,2.7V)施密特触发输入噪声抑制
  • 硬件数据写保护
  • 内部写周期(最大5 ms)可按字节写
  • 页写:8字节页(24C02),16字节页(24C04/08/16),32字节页(24C32/64)可按字节,随机和序列读
  • 自动递增地址
  • 高可靠性擦写寿命:100万次-数据保持时间:100年

     

 有规格书可知,EEPROM的读写速率是100KHZ.

(2)引脚说明

 

 

AT24C02芯片,7位地址依次1010xxx, 最低三位(A0~A2)可配,由原理图可知三个脚物理接地,地址为1010000,即该设备地址为0x50)。

(3)起始和停止时序

  数据和时钟线都为高则称总线处在空闲状态。当SCL为高电平时SDA的下降沿(高到低叫做起始条件(START,简写为S),SDA的上升沿(低到高)则叫做停止条件(STOP,简写为P)。参见图5。

 

(4)数据有效性

     SCL高电平时SDA数据有效,低电平时SDA数据交换,数据变换为高电平或者低电平。

                     

 (5)应答信号:

                        

三、读写操作

1、写操作

  (1).字节写

  写操作要求在接收器件地址和ACK应答后,接收8位的字地址。接收到这个地址后EEPROM应答"0",然后是一个8位数据。在接收8位数据后EEPROM应答"0",接着必须由主器件发送停止条件来终止写序列。
  此时EEPROM进入内部写周期twR,数据写入非易失性存储器中,在此期间所有输入都无效。直到写周期完成,EEPROM才会有应答(见图9)。

               

      (2)页写
  • 24C02器件按8字节/页执行页写,24C04/08/16器件按16字节/页执行页写,24C32/64器件按32字节/页执行页写。
  • 页写初始化与字节写相同,只是主器件不会在第一个数据后发送停止条件,而是在EEPROM的ACK以后,接着发送7个(24C02数据。EEPROM收到每个数据后都应答“0”。最后仍需由主器件发送停止条件,终止写序列(见图10)。接收到每个数据后,字地址的低3位(24C02)内部自动加1,高位地址位不变,维持在当前页内。当内部产生的字地址达到该页边界地址时,随后的数据将写入该页的页首。如果超过8个(24C02)数据传送给了EEPROM,字地址将回转到该页的首字节,先前的字节将会被覆盖。  

            

   MCU向AT24CT02写数据时,MCU为主机,EEPROM为从机。由上图可知:

  • 主机通过SDA先发一个起始信号。
  • 发设备地址寻找设备,并设置是写入还是读取。
  • 从机回一个Ack应答信号。
  • 主机发送要写入的EEPROM片内地址。
  • 从机回Ack应答信号。
  • 主机写数据到从机,每写一byte,从机回发一个ack应答。
  • 主机发送停止信号,结束数据写入。
  (3)代码分析:
#include "iic.h"
/*软件模拟IIC写数据到EEPROM*/
GPIO_InitTypeDef GPIO_InitStructure;

#define SDA_OUT   PBout(9)
#define SDA_IN    PBin(9)
#define SCL_OUT   PBout(8)

//初始化SDL、SCL GPIO
void Iic_AT24C02_Init(void)
{
	//1.初始化时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
	
	//2.初始化硬件
	GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9 | GPIO_Pin_8;//PB8  PB9 
	GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_OUT;//输出模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz
	GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;//上拉
	GPIO_Init(GPIOB,&GPIO_InitStructure);	

	//空闲状态高电平
	SDA_OUT = 1;
	SCL_OUT = 1;
}

//设置引脚模式
void sda_pin_mode(GPIOMode_TypeDef mode)
{
	GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;//PB9 
	GPIO_InitStructure.GPIO_Mode  = mode;//模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz
	GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;//上拉
	GPIO_Init(GPIOB,&GPIO_InitStructure);	
}


//IIC起始信号
void iic_start(void)
{
	//保证SDA引脚为输出模式
	sda_pin_mode(GPIO_Mode_OUT);
	
	//保证开始是一个高电平
	SDA_OUT = 1;
	SCL_OUT = 1;
	delay_us(5);     //AT24C02的读写速度为100kHZ,一个周期为10us.
	
	SDA_OUT = 0;//时钟线为高电平期间,数据线由高到低
	delay_us(5);
	
	SCL_OUT = 0;//高速总线上的从机,有通信的设备
	delay_us(5);
}
//发送数据
void iic_send_byte(uint8_t d)// 10101110
{
	int32_t i;
	//保证SDA引脚为输出模式
	sda_pin_mode(GPIO_Mode_OUT);
	SDA_OUT = 0;
	SCL_OUT = 0;
	delay_us(5);
	
	for(i=7;i>=0;i--)
	{
		if(d & (0x1<<i))
			SDA_OUT = 1;
		else
			SDA_OUT = 0;
	
		delay_us(5);
		
		SCL_OUT = 1;
		delay_us(5);//当前数据是可靠的,告诉从机可以读取数据
		
		SCL_OUT = 0;
		delay_us(5);//当前数据是不可靠的,正在准备
	}	
}
//应答信号
uint8_t iic_wait_ack(void)
{
	uint8_t ack = 0;
	//保证SDA引脚为输入模式
	sda_pin_mode(GPIO_Mode_IN);
	
	SCL_OUT = 1;
	delay_us(5);//当前数据是可靠的,主机可以访问
	
	if(SDA_IN == 0)
		ack = 0;//有应答   要
	else
		ack = 1;//无应答   不要了
	
	SCL_OUT = 0;
	delay_us(5);
	
	return ack;
}
void iic_stop(void) { //保证SDA引脚为输出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 1; delay_us(5); SDA_OUT = 1;//时钟线为高电平期间,数据线由低到高 delay_us(5); } int32_t at24c02_write(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //发送起始信号 iic_start(); //设备寻址,设备写访问地址为 10100000 = 0xA0 iic_send_byte(0xA0); //等待应答 ack = iic_wait_ack(); if(ack) { printf("devices address failed!\r\n"); return -1; } //告诉从机我要访问的数据存储地址 iic_send_byte(word_addr); //等待应答 ack = iic_wait_ack(); if(ack) { printf("word address failed!\r\n"); return -2; } //连续写数据 while(len--) { //写入数据 iic_send_byte(*p); //等待应答 ack = iic_wait_ack(); if(ack) { printf("data failed!\r\n"); return -3; } p++; } //发送停止信号 iic_stop(); printf("write success!\r\n"); return 0; }

2、读操作  

 

读操作与写操作初始化相同,只是器件地址中的读/写选择位应为"1"。有三种不同的读操作方式:当前地址读随机读顺序读

(1)当前地址读
  • 内部地址计数器保存着上次访问时最后一个地址加1的值。只要芯片有电,该地址就一直保存。当读到最后页的最后字节,地址会回转到0;当写到某页尾的最后一个字节,地址会回转到该页的首字节。
  • 接收器件地址(读/写选择位为"1")、EEPROM应答ACK后,当前地址的数据就随时钟送出。主器件无需应答"O",但需发送停止条件(见图12)。

 

                

2.随机读

       随机读需先写一个目标字地址,一旦EEPROM接收器件地址和字地址并应答了ACK,主器件就产生一个重复的起始条件。
然后,主器件发送器件地址(读/写选择位为"1"),EEPROM应答ACK,并随时钟送出数据。主器件无需应答"O",但需发送停止条件(见图13)。

 

 

 

  • 主机通过SDA先发一个起始信号。
  • 发设备地址寻找设备,并设置是写入(因为要写入读取数据的地址)。
  • 写入要读取的地址
  • 从机回一个Ack应答信号。
  • 主机再发一个起始信号开始读
  • 主机发送要写入的EEPROM片内地址(读写位设置为读)。
  • 读数据。
  • 主机发送停止信号,结束数据读取。
3.顺序读

  顺序读可以通过“当前地址读"或“随机读"启动。主器件接收到一个数据后,应答ACK。只要EEPROM接收到ACK,将自动增加字地址并继续随时钟发送后面的数据。若达到存储器地址末尾,地址自动回转到0,仍可继续顺序读取数据。
主器件不应答"0",而发送停止条件,即可结束顺序读操作(见图14)。

 

 

 

 

 

 

 

#include "iic.h"

GPIO_InitTypeDef GPIO_InitStructure;
void Iic_AT24C02_Init(void)
{
	//1.初始化时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
	
	//2.初始化硬件
	GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9 | GPIO_Pin_8;//PB8  PB9 
	GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_OUT;//输出模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz
	GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;//上拉
	GPIO_Init(GPIOB,&GPIO_InitStructure);	

	//空闲状态高电平
	SDA_OUT = 1;
	SCL_OUT = 1;
}

void sda_pin_mode(GPIOMode_TypeDef mode)
{
	GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;//PB9 
	GPIO_InitStructure.GPIO_Mode  = mode;//模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz
	GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;//上拉
	GPIO_Init(GPIOB,&GPIO_InitStructure);	
}

void iic_start(void)
{
	//保证SDA引脚为输出模式
	sda_pin_mode(GPIO_Mode_OUT);
	
	//保证开始是一个高电平
	SDA_OUT = 1;
	SCL_OUT = 1;
	delay_us(5);
	
	SDA_OUT = 0;//时钟线为高电平期间,数据线由高到低
	delay_us(5);
	
	SCL_OUT = 0;//高速总线上的从机,有通信的设备
	delay_us(5);
}

void iic_send_byte(uint8_t d)// 10101110
{
	int32_t i;
	//保证SDA引脚为输出模式
	sda_pin_mode(GPIO_Mode_OUT);
	SDA_OUT = 0;
	SCL_OUT = 0;
	delay_us(5);
	
	for(i=7;i>=0;i--)
	{
		if(d & (0x1<<i))
			SDA_OUT = 1;
		else
			SDA_OUT = 0;
	
		delay_us(5);
		
		SCL_OUT = 1;
		delay_us(5);//当前数据是可靠的,告诉从机可以读取数据
		
		SCL_OUT = 0;
		delay_us(5);//当前数据是不可靠的,正在准备
	}	
}
uint8_t iic_recv_byte(void)
{
	int8_t i;
	uint8_t data;
	//保证SDA引脚为输入模式
	sda_pin_mode(GPIO_Mode_IN);
	
	//当前数据不可靠,切换数据
	SCL_OUT = 0;
	delay_us(5);
	
	for(i=7;i>=0;i--)//MSB  10101111
	{
		SCL_OUT = 1;
		delay_us(5);//当前数据可靠,读把
		
		if(SDA_IN == 1)
		{
			data |= (0x1<<i);
		}
		else
		{
			data &= ~(0x1<<i);
		}
		
		SCL_OUT = 0;
		delay_us(5);//当前数据不可靠,切换数据
	}
	
	return data;
}

void iic_ack(uint8_t ack)
{
	//保证SDA引脚为输出模式
	sda_pin_mode(GPIO_Mode_OUT);
	
	SDA_OUT = 0;
	SCL_OUT = 0;
	delay_us(5);//当前数据不可靠
	
	//发送高/低电平
	SDA_OUT = ack;
	delay_us(5);//准备数据
	
	SCL_OUT = 1;
	delay_us(5);//当前数据可靠,然后从机可以访问
	
	SCL_OUT = 0;
	delay_us(5);//当前数据bu可靠,切换数据
}

uint8_t iic_wait_ack(void)
{
	uint8_t ack = 0;
	//保证SDA引脚为输入模式
	sda_pin_mode(GPIO_Mode_IN);
	
	SCL_OUT = 1;
	delay_us(5);//当前数据是可靠的,主机可以访问
	
	if(SDA_IN == 0)
		ack = 0;//有应答   要
	else
		ack = 1;//无应答   不要了
	
	SCL_OUT = 0;
	delay_us(5);
	
	return ack;
}
void iic_stop(void)
{
	//保证SDA引脚为输出模式
	sda_pin_mode(GPIO_Mode_OUT);
	
	SDA_OUT = 0;
	SCL_OUT = 1;
	delay_us(5);
	
	SDA_OUT = 1;//时钟线为高电平期间,数据线由低到高
	delay_us(5);

}
int32_t at24c02_write(uint8_t word_addr,uint8_t *buf,uint8_t len)
{
	uint8_t ack = 0;
	uint8_t *p = buf;
	//发送起始信号
	iic_start();
	
	//设备寻址,设备写访问地址为 10100000 = 0xA0
	iic_send_byte(0xA0);
	
	//等待应答
	ack = iic_wait_ack();
	if(ack)
	{
		printf("devices address failed!\r\n");
		return -1;
	}
	
	//告诉从机我要访问的数据存储地址
	iic_send_byte(word_addr);
	//等待应答
	ack = iic_wait_ack();
	if(ack)
	{
		printf("word address failed!\r\n");
		return -2;
	}
	
	//连续写数据
	while(len--)
	{
		//写入数据
		iic_send_byte(*p);
		
		//等待应答
		ack = iic_wait_ack();
		if(ack)
		{
			printf("data failed!\r\n");
			return -3;
		}
		p++;
	}
	
	//发送停止信号
	iic_stop();
	
	printf("write success!\r\n");
	
	return 0;
}

int32_t at24c02_read(uint8_t word_addr,uint8_t *buf,uint8_t len)
{
	uint8_t ack = 0;
	uint8_t *p = buf;
	//发送开始信号
	iic_start();
	
	//设备寻址,设备写访问地址  0xA0
	iic_send_byte(0xA0);
	
	//等待应答
	ack = iic_wait_ack();
	if(ack) //1 无应答   0 应答
	{
		printf("devices address failed!\r\n");
		return -1;
	}
	
	//告诉从机我要访问的数据存储地址
	iic_send_byte(word_addr);
	//等待应答
	ack = iic_wait_ack();
	if(ack) //1 无应答   0 应答
	{
		printf("word address failed!\r\n");
		return -2;
	}

	//再次发送开始信号
	iic_start();
	
	//设备寻址,设备读访问地址  0xA1
	iic_send_byte(0xA1);
	
	//等待应答
	ack = iic_wait_ack();
	if(ack) //1 无应答   0 应答
	{
		printf("devices address failed!\r\n");
		return -3;
	}
	//连续接收数据
	len = len - 1;
	while(len--)
	{
		*p++ = iic_recv_byte();
		//发送应答
		iic_ack(0);
	}
	
	//接收最后一个字节
	*p = iic_recv_byte();
	
	//发送不应答
	iic_ack(1);
	
	//发送停止信号
	iic_stop();
	
	printf("read success!\r\n");
	
	return 0;	
}

  

 

 

 

 

 

posted @ 2020-05-23 10:17  轻轻的吻  阅读(2650)  评论(0编辑  收藏  举报