I²C控制E²PROM(硬件控制)

根据开发板的原理图,我们得知,
SPI连接处理器芯片和SD卡插座(CN4)
I²C连接处理器芯片和E²PROM(256B)芯片M24C02MN6(U6)
UART2连接处理器芯片和USB转串口芯片IC_FT2232D(U3)
UART1连接处理器芯片和串口转RS232芯片ST3232ECTR(U5)
JTAG连接处理器芯片和USB转串口芯片IC_FT2232D(U3)(这是因为CN2既是USB调试接口,也是USB转串口)
Flash是处理器芯片内置的,没有专门的芯片。
一、I2C

物理层
1.I²C有俩根引脚,SCL(串行时钟线)和SDA(串行双向数据线),所以是半双工通信模式
2.每一个连接到I²C总线上的设备都有一个独立的地址(可以是7位或10位地址),主机(处理器)利用这个地址对不同设备进行访问。(rbt6开发板默认I²C总线只连接了E²PROM)
3.I²C支持一主多从式通信,所有设备共享总线。(从发送器模式、从接收器模式、主发送器模式、主接收器模式)
4.3种传输模式:标准模式(100kb/s)、快速模式(400kb/s)、高速模式(3.4mb/s)(CCR寄存器控制)

3种状态:高电平、低电平、高阻态
(1)当某个设备空闲时,处于高阻态,相当于把这个设备断开,此时其他设备进行通信时,不会受空闲设备干扰。
(2)当所有从设备处于空闲态,那么,所有从设备处于高阻态,SCL和SDA都会被上拉电阻拉到高电平。SCL不会再有脉冲变化。
(3)I²C总线规定,同一时间只能有一个设备占用总线。
(4)当某个设备要输出高电平,其他的设备一定是空闲,此时,相当于总线上只有主机和从设备,不论该设备是高阻态还是输出高电平,对于主机来说,都是高电平,因为如果这个设备也处于高阻态,上拉电阻会把整个总线电平拉高,使主机读取到高电平。
(5)I²C的从设备一般是使用高阻态来表示高电平
(6)那么,怎么区分总线是在输出高电平还是空闲?看SCL线,如果输出高电平,肯定是在SCL的一个边沿脉冲时读取到的,而如果是空闲,SCL也会被上拉电阻拉到高电平,自然不会有什么边沿脉冲,所以就不会读取到高电平了
(7)当某个设备输出低电平,其他的设备一定是空闲,此时,相当于总线上只有主机和从设备,总线的电平会被从设备接地的低电平拉低,变成低电平,主机读取到的就是低电平。
(8)如果没有高阻态,一旦一个从设备是低电平,另一个输出高电平,就可能导致短路。
(9)高阻态表示:总线处于空闲,可以和从设备进行通信。总线是低电平,表示总线正在被占用,不能和其他设备进行通信。

协议层:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由上面三个图,我们知道,不论是哪一种通信方式,都需要主机产生开始和终止信号。终止信号的产生,往往都是源于非应答信号。我们在计算机网络中的学习得知,非应答信号一般是由于数据传输完毕、数据丢失或数据错误产生的。前俩种都是在没有考虑数据丢失或数据错误的情况下的传输,第三个复合通信则考虑进来了;一旦接受到非应答信号,就重新发送一个起始信号,然后重新传递这些信息。
从发送和主接受需要在结束前发送非应答信号,主发送和从接受可以直接发送结束信号。

在这里插入图片描述
我们知道,当从设备都处于空闲,SCL和SDA都是处于高电平状态。那么,起始信号之前和终止信号之后,SCL和SDA都是处于高电平状态。
因此,起始信号是SCL高电平时,SDA的下降沿脉冲;终止信号是SCL高电平时,SDA的上升沿脉冲。
高低电平的获取则是在SCL的高电平时读取SDA的电平(不是边沿脉冲读取)。

主模式时, I2C接口启动数据传输并产生时钟信号。串行数据传输总是以起始条件开始并以停止条件结束。起始条件和停止条件都是在主模式下由软件控制产生。
从模式时, I2C接口能识别它自己的地址(7位或10位)和广播呼叫地址。软件能够控制开启或禁止广播呼叫地址的识别。
数据和地址按8位/字节进行传输,高位在前。跟在起始条件后的1或2个字节是地址(7位模式为1个字节, 10位模式为2个字节)。地址只在主模式发送。
在一个字节传输的8个时钟后的第9个时钟期间,接收器必须回送一个应答位(ACK)给发送器。

I2C有俩种实现发送:
硬件实现协议
软件模拟协议

I2C默认I2C1是PB6、PB7(开发板上就是这个);PB10、PB11是I2C2(开发板没有支持)

库函数:
typedef struct
{
uint32_t I2C_ClockSpeed; //SCL时钟频率,应小于400K
uint16_t I2C_Mode; //工作模式,IIC模式或SMBUS模式
uint16_t I2C_DutyCycle; //时钟占空比
uint16_t I2C_OwnAddress1; //自身IIC设备地址
uint16_t I2C_Ack; //使能ACK
uint16_t I2C_AcknowledgedAddress; // 地址的长度,7位
}I2C_InitTypeDef;

void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);//IIC初始化函数
void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);//为某个IIC事件配置中断响应
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);//获取中断标志
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);//获取状态标志
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);//检测事件的发生
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);//发送一个7位地址
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);//发送一个字节的数据
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);//接受一个字节的数据
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);//发出一个ACK或NACK;
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);//发一个起始信号
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);//发一个停止信号
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);//IIC使能
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);//IIC初始化

根据原理图和AT24C02参考手册,我们知道,E²PROM的地址前4位固定是1010,后3位由硬件的引脚决定,在原理图中,A0、A1、A2都是接地,所以地址后3位应该是000,即E²PROM地址为1010000,最后一位位读/写位,至于到底何处写7位地址,何处写8位地址,看源码注释即可。
我们在自己编写IIC的传输函数时,应该严格遵守IIC传输时的时序编写
在这里插入图片描述
如,要给E²PROM传数据时,如上图,先传一个S(起始信号),然后检测事件EV5,然后传递7位设备地址加读写位的8位地址,(ACK在初始化IIC时已经设置为自动发送),然后检测EV6事件,然后发送数据,检测EV8事件,直到最后一个数据发送完毕,检测EV8_2事件,然后发送P(结束信号)。
在这里插入图片描述
接受一个E²PROM的数据,时序同样应该遵守。

二、E²PROM
对E²PROM进行操作,在开发板中,其地址为1010000,最后一位位读/写位。
写E²PROM:按字节写入、按页写入
读E²PROM:顺序读取(从某一字节连续读取)、随机读取(指定某一字节读取)、读取当前地址(因为一般不知道当前地址,所以很少用)
E²PROM的数据传输需要在IIC传输的从设备地址后面再加一个E²PROM的内部地址,然后再传输数据
在这里插入图片描述
这个是EEPROM只写入一个字节。同样的,也应该严格的遵守时序。
我们可以发现,在EEPROM的时序基本和IIC时序相同。但如果想给EEPROM传递数据,第一个数据应该是EEPROM的内部地址(即0到255),第二个数据才是真正的数据。
在这里插入图片描述
这个是EEPROM写入多个字节
在这里插入图片描述
这个是从EEPROM读出一个字节。
读出字节时,又有所不同。分为俩个阶段。第一个阶段:发S信号,传EEPROM设备地址,检测EV6事件后,传第一个数据(EEPROM内部地址)。第二个阶段:发S信号,传EEPROM设备地址,然后检测EV6事件(EV6有俩个,这里应该是接收器模式的EV6),然后读出一个字节数据,检测EV7事件。然后手动发送一个NACK,最后发送P(结束信号)。下同。
在这里插入图片描述
这个是从EEPROM读出n多个字节

步骤:
一·、初始化I2C(只需要简单的调用库函数)
1.初始化I2C的GPIO接口
2.配置I2C的结构体
3.初始化I2C
4.使能I2C
二、读写EEPROM(需要严格按照时序编写)
5.编写I2C写入E2PROM的函数(按字节)
6.编写I2C读取E2PROM的函数(随机读取)
7.使用read和write进行校验
8.编写I2C写入和读取E2PROM的函数(按页写入、顺序读取)

初始化I2C

void I2C_EEPROM_Config()
{
	GPIO_InitTypeDef  GPIO_InitStruct;
  	I2C_InitTypeDef 	I2C_InitStruct;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);//开IIC的时钟
	//初始化GPIO引脚
	GPIO_InitStruct.GPIO_Pin=  GPIO_Pin_6;//SCL
	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AF_OD;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);
	GPIO_InitStruct.GPIO_Pin=  GPIO_Pin_7;//SDA
	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AF_OD;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; //这俩句不修改也行,因为和上面的一样
   	GPIO_Init(GPIOB,&GPIO_InitStruct);

  	I2C_InitStruct.I2C_ClockSpeed=400000;      
  	I2C_InitStruct.I2C_Mode=I2C_Mode_I2C;               
  	I2C_InitStruct.I2C_DutyCycle=I2C_DutyCycle_2;          
   	I2C_InitStruct.I2C_OwnAddress1=0x5f;  //自己定义一个 7位的地址,这里是定义主机,即STM32(处理器)的地址 
   	I2C_InitStruct.I2C_Ack=I2C_Ack_Enable;//自动应答    
   	I2C_InitStruct.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;
	I2C_Init(I2C1,&I2C_InitStruct);
	I2C_Cmd(I2C1,ENABLE);
}

向EEPROM写一个字节

void EEPROM_Byte_Write(uint8_t addr,uint8_t data)//向EEPROM写一个字节
{
	I2C_GenerateSTART(I2C1,ENABLE);//发送一个起始信号
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)==ERROR)//检测EV5事件
	;
	
	I2C_Send7bitAddress(I2C1,0xA0,I2C_Direction_Transmitter);//发送地址,0xA0是EEPROM的写地址
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR)//检测EV6事件
	{
	;
	}
	I2C_SendData(I2C1,addr); //发送的第一个数据,EEPROM认为是写入的地址
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR)//检测EV8事件
	{
	;
	}
	I2C_SendData(I2C1,data); //发送数据
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR)//检测EV8_2事件
	{
	;
	}
	I2C_GenerateSTOP(I2C1,ENABLE);//发送一个结束信号

}

向EEPROM写n个字节(页写入)
页写入必须页对齐
(我们查看AT24C02手册时,会知道,大小为2kb的EEPROM,每页是8B,所以,页写入每次最多8个字节)

void EEPROM_Page_Write(uint8_t addr,uint8_t *data,uint8_t n)//向EEPROM写n个字节,n<=8
{
	I2C_GenerateSTART(I2C1,ENABLE);//发送一个起始信号
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)==ERROR)//检测EV5事件
	;
	
	I2C_Send7bitAddress(I2C1,0xA0,I2C_Direction_Transmitter);//发送地址,0xA0是EEPROM的写地址
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR)//检测EV6事件
	{
	;
	}
	I2C_SendData(I2C1,addr); //发送的第一个数据,EEPROM认为是写入的地址
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR)//检测EV8事件
	{
	;
	}
	while(n--)
	{
		 I2C_SendData(I2C1,*data); //发送数据
		 while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR)//检测EV8事件
			;
		 data++;
	}

	
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR)//检测EV8_2事件
	{
	;
	}
	I2C_GenerateSTOP(I2C1,ENABLE);//发送一个结束信号

}

从EEPROM读n字节

void EEPROM_Byte_Read(uint8_t addr,uint8_t *data,uint8_t n)//向EEPROM读n字节
{
	I2C_GenerateSTART(I2C1,ENABLE);//发送一个起始信号,写入地址

	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)==ERROR)//检测EV5事件
	{
	;
	}
	I2C_Send7bitAddress(I2C1,0xA0,I2C_Direction_Transmitter);//发送地址,0xA0是EEPROM的写地址
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR)//检测EV6事件
	{
	;
	}
	I2C_SendData(I2C1,addr); //发送的第一个数据,EEPROM认为是写入的地址

	//第二次发起始信号

	I2C_GenerateSTART(I2C1,ENABLE);//发送一个起始信号,读出数据
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)==ERROR)//检测EV5事件
	{
	;
	}
	I2C_Send7bitAddress(I2C1,0xA1,I2C_Direction_Receiver);//读数据,0xA1是EEPROM的读地址
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)==ERROR)//检测EV6事件
	{
	;
	}
	
	while(n--)
	{
		if(n==0) //是最后一个字节
		{
			I2C_AcknowledgeConfig(I2C1,DISABLE);//那么产生一个NACK
		}
		while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)==ERROR)//检测EV7事件
		{
		;
		}
		*data=I2C_ReceiveData(I2C1);
		 data++;
	}		
	I2C_AcknowledgeConfig(I2C1,ENABLE);//重新配置ACK使能,以便下次使用
	I2C_GenerateSTOP(I2C1,ENABLE);//发送一个结束信号

}

等待EEPROM内部时序完成
这是因为EEPROM是非易失性存储器,速度比CPU要慢的多。在执行完写入EEPROM函数后,EEPROM内部时序仍然没有彻底完成。直接读数据,有可能卡在设备地址的发送上,导致程序卡死。(为什么是卡在这,我也不清楚)所以要等内部时序完成。(也可以直接进行简单的延时函数)
SB标志位:起始条件是否发送。
ADDR标志位:地址是否被发送

void EE_WaitWriteEnd() //等待EEPROM内部时序完成
{
	do
	{
		I2C_GenerateSTART(I2C1,ENABLE);//发送一个起始信号,写入地址
	
		while(I2C_GetFlagStatus(I2C1,I2C_FLAG_SB)==ERROR)//检测SB标志位
		;		
		I2C_Send7bitAddress(I2C1,0xA0,I2C_Direction_Transmitter);//发送地址,0xA0是EEPROM的写地址,发读地址还是写地址都可以,这里只是检测用的
	}while(I2C_GetFlagStatus(I2C1,I2C_FLAG_ADDR)==ERROR);//检测ADDR标志位
	//如果地址没有发送,那么一直重新发送S(起始信号),并发送地址,直到检测到地址成功发送为止。
	I2C_GenerateSTOP(I2C1,ENABLE);//EEPROM内部时序完成
}

向EEPROM写n个字节
(不需要页对齐,且能写入任意不大于256个字节的数据)

void EEPROM_STR_Write(uint8_t addr,uint8_t *data,uint8_t n)//向EEPROM写n个字节
{
	uint8_t addr_s=0,numPage=0,num=0,count=0;
	addr_s=addr%8;
	numPage=n/8;
	num=n%8;
	count=8-addr_s;
	if(addr_s==0)	//页对齐	 addr=32,n=18  18/8=2……2
	{
		while(numPage--)  //如果写入的字节数比8大,把整页的先写入
		{
			EEPROM_Page_Write(addr,data,8);
		  	EE_WaitWriteEnd();
			addr+=8;
			data+=8;
		}
		if(num!=0) //把最后不满一页的写入
		{
			EEPROM_Page_Write(addr,data,num);
		  	EE_WaitWriteEnd();
		}
	}
	else	//不是页对齐   如addr=22,n=22   22/8=2……6  8-6=2	  
	{
		// 16  17  18  19  20  21  22  23  
		// 24  25  26  27  28  29  30  31
		// ………………
		//则从addr=22开始,写入2个字节,
		EEPROM_Page_Write(addr,data,count);
	  	EE_WaitWriteEnd();
		addr+=count;
		data+=count;
		//然后按页对齐方式写入剩下的20个字节 ,n=22-2=20 ,20/8=2………4
		n-=count;
		numPage=n/8;
		num=n%8;
		while(numPage--)  //如果写入的字节数比8大,把整页的先写入
		{
			EEPROM_Page_Write(addr,data,8);
		  	EE_WaitWriteEnd();
			addr+=8;
			data+=8;
		}
		if(num!=0) //把最后不满一页的写入
		{
			EEPROM_Page_Write(addr,data,num);
		  	EE_WaitWriteEnd();
		}
	}
}

以上都是属于i2c.c的内容

uart.c
(为什么需要写串口函数?)
因为我们需要通过串口调试助手回显I2C传递数据的结果到电脑屏幕。

void usrt_init()
{
	GPIO_InitTypeDef  GPIO_InitStruct;
	USART_InitTypeDef UART_InitStruct;
//配置uart的GPIO口	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	//配置Rx
	GPIO_InitStruct.GPIO_Pin=  GPIO_Pin_3;
	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_IN_FLOATING;
		//这里应该是计算机传什么电平,就是什么电平,所以应该是浮空输入	
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	//配置Tx
	GPIO_InitStruct.GPIO_Pin=  GPIO_Pin_2;
	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
//配置usrt
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
	UART_InitStruct.USART_BaudRate=  115200;
	UART_InitStruct.USART_WordLength=  USART_WordLength_8b;
	UART_InitStruct.USART_StopBits=  USART_StopBits_1;
	UART_InitStruct.USART_Parity=  USART_Parity_No;
	UART_InitStruct.USART_Mode=  USART_Mode_Rx|USART_Mode_Tx;
	UART_InitStruct.USART_HardwareFlowControl=  USART_HardwareFlowControl_None;
	USART_Init(USART2,&UART_InitStruct);

//串口使能
	USART_Cmd(USART2,ENABLE);
}


 
 //在这里,我们要让开发板和计算机进行通信
int fputc(int ch,FILE* f) //printf的调用,用来接受开发板的数据
{
	USART_SendData(USART2,ch);
	while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET);
	return ch;
}
int fgetc(FILE* f) //scanf的调用,用来向开发板发送数据
{
	
	while(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==RESET);
	return (int)USART_ReceiveData(USART2);
}

main.c

#include "stm32f10x.h"
#include "led.h"
#include "i2c.h"
int main()
{
	uint8_t str[10]={0};
	uint8_t n;
	uint8_t str2[8]={1,2,3,4,5,6,7,8};
	uint8_t str3[45]={0},str4[45]={0};
	usrt_init();
	I2C_EEPROM_Config();
    //1.写一个字节
	EEPROM_Byte_Write(11,0x55);
	EE_WaitWriteEnd();
	EEPROM_Byte_Read(11,str,1);
	printf("接受到的数据是0x%x\n",str[0]);
	//2.页写入
	//关于页,只要学过操作系统应该都清楚,页是固定的,每8个字节一页的话,你肯定不能说3到11也是一页
	//页写入必须页对齐
	EEPROM_Page_Write(16,str2,8);
	EE_WaitWriteEnd();
	EEPROM_Byte_Read(16,str2,8);
	for(n=0;n<8;n++)
		printf("接受到的数据是0x%x\n",str2[n]);

	//3.写入n字节
	for(n=0;n<45;n++)
	{
		str3[n]=n; 	
	}
	EEPROM_STR_Write(11,str3,45);
	EE_WaitWriteEnd();
	EEPROM_Byte_Read(11,str4,45);
   	for(n=0;n<45;n++)
		printf("0x%x   0x%x\n",str3[n],str4[n]);

}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值