STM32F103之I2C协议的应用(读写EEPROM)

概要

本文主要记录个人在学习I2C协议的一些个人见解,且基于I2C协议实现STM32读写EEPROM的数据

声明:因个人能力有限,本文仅是个人的学习记录笔记,有错误之处还望指出

在这里插入图片描述

I2C特征

  1. 支持设备总线,即可以多个设备共用信号线。在一个I2C通讯总线中可以连接多个I2C通信设备,支持多个通讯主机及多个通过从机
  2. 使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。SCL用于数据收发的同步
  3. 每个连接到总线的设备都有一个独立的地址,通过这个地址来访问不同的设备
  4. 总线通过上拉电阻接到电源,当I2C设备空闲时候,会输出高阻态,当所有设备都空闲,都输出高阻态,由上拉电阻把总线拉成高电平
  5. 多个主机使用总线时,会利用仲裁的方式来决定那个设备占用总线

I2C的基本读写过程

主机写数据到从机
在这里插入图片描述
主机由从机中读数据

在这里插入图片描述

  • 在主机和从机通讯时,S表示从主机的I2C接口产生的传输起始信号,这个时候所有连接到I2C总线上的从机都会收到该信号。
  • 为了实现操作正确的从机,故通过主机广播的从机地址(SLAVE ADDRESS),在I2C总线上,每个设备的地址都是唯一的,故从机被选中。(从机地址可以是7位或10位)
  • 在成功找到从机后,要传输方向的选择位,该位为0时,表示主机向从机写数据,该位为1时,表示主机向从机读数据(0写1读)
  • 在从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号,只有接收到应答信号后才可以继续发送或接收数据。

写数据

配置方向传输位为“写数据”方向(0),广播完地址,接收到应答信号后,主机正式开始向从机写数据(DATA),数据包大小为8位,主机每次发送完一个字节的数据,都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输N个数据,最后当传输结束,主机向从机发送一个停止传输信号,表示不再传数据。

读数据
配置方向传输位为“读数据”方向(1),广播完地址,接收到应答信号后,主机正式开始向从机写数据(DATA),数据包大小为8位,主机每次发送完一个字节的数据,都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输N个数据。当主机希望停止接收数据时候,从机可以返回一个非应答信号(NACK),则从机自动停止数据传输

读和写数据
除了基本读写,I2C通讯更加常用的是复合模式,该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过SLAVE_ADDRESS寻找到从机设备后,发送一段数据,这段数据是表示从设备内部的存储器或存储器地址(注意区分它与 SLAVE_ADDRESS 的区别);在第二次传输中才是对改地址内容的读写。
简言之第一次传输读写地址,第二次是读写的具体数据

I2C的起始状态

在这里插入图片描述

  1. 开始
    SCL为高电平,SDA由高电平向低电平转换
/* SCL为高电平,出现一个下跳沿 */
void I2C_Start(void){
	SDA_HIGH();
	SCL_HIGH();
	delay();
	SDA_LOW();
	delay();
	SCL_LOW():
	delay();
}
  1. 结束
    SCL是高电平,SDA由低电平向高电平转换
/* SCL为高电平,出现一个上跳沿 */
void I2C_Stop(void){
	SDA_LOW();
	SCL_HIGH();
	delay();
	SDA_HIGH();
}

在这里插入图片描述
3. 数据的有效性
SDA数据线在SCL的每个时钟周期传输一位数据(8个字节),SCL为高电平的时候SDA表示的数据有效,此时SDA为高电平表示数据 “1”,低电平表示数据 “0”;当SCL为低电平时,SDA数据无效,一般在这个时候SDA进行电平切换,为下一次数据做准备。

传输数据

  1. 字节格式
    发送到 SDA 线上的每个字节必须为8位,每次传输可以发送的字节数量不受限制,但是每个字节后必须跟一个响应位

在这里插入图片描述

  1. 响应
    数据传输必须带响应,相应的响应时钟由主机产生。响应包括“应答”(ACK)和"非应答"信号(NACK)。在第九个时钟,发送器释放SDA线(高)SDA高电平表示非应答信号,SDA低电平表示应答信号,SCL线进行高低电平的转换,会产生一个时钟信号。

EEORPM

在这里插入图片描述

本次实验采用的EEPROM芯片(AT24C02),将A0/A1/A2均接地,故设备地址的7位地址为0x50(1010000),读地址:0xA1(10100001),写地址:0xA0(10100000);I2C采用软件模拟的方式实现。

编程流程

  1. 配置外设引脚
  2. 编写模拟I2C时序的控制函数
  3. 编写基本I2C读写函数
  4. 编写读写EEPROM此处内容的函数
  5. 编写测试函数

代码实现

软件模拟I2C

void I2C_Delay(void){
	uint8_t i;
	for(i=10;i>0;i--);
}
/* I2C开始信号 */
void EEPROM_I2C_Start(void){
	//SCL为高电平,SDA出现下降沿
	SDA_HIGH();
	SCL_HIGH();
	I2C_Delay();
	SDA_LOW();
	I2C_Delay();
	SCL_LOW();
	I2C_Delay();
}
/* I2C结束信号 */
void EEPROM_I2C_Stop(void){
	//SCL为高电平,SDA出现上升沿
	SDA_LOW();
	SCL_HIGH();
	I2C_Delay();
	SDA_HIGH();
	I2C_Delay();
}
/* I2C应答信号 */
void EEPROM_I2C_ACK(void){
	SDA_LOW();		//应答信号
	I2C_Delay();	
	SCL_HIGH();//CPU产生一个时钟
	I2C_Delay();
	SCL_LOW();//CPU产生一个时钟
	I2C_Delay();
	SDA_HIGH();			//CPU释放SDA线
	
}
/* I2C非应答信号 */
void EEPROM_I2C_NACK(void){
	SDA_HIGH();		//非应答信号
	I2C_Delay();	
	SCL_HIGH();//CPU产生一个时钟
	I2C_Delay();
	SCL_LOW();//CPU产生一个时钟
	I2C_Delay();
}

/* I2C等待应答 */
uint8_t EEPROM_I2C_WaitAck(void){
	//返回0表示正确应答,返回1表示无器件响应
	uint8_t re;
	
	SDA_HIGH();		//CPU释放SDA总线
	I2C_Delay();	
	SCL_HIGH();//CPU产生一个时钟
	I2C_Delay();
	
	//CPU此时读取SDA口状态(SDA低电平返回应答,高电平返回非应答)
	if(EEPROM_I2C_SDA_Read())
		re = 1;
	else 
		re = 0;
	SCL_LOW();
	I2C_Delay();
	return re;
}

/* CPU向I2C总线发送8bit数据 */
void I2C_SendByte(uint8_t Byte){
	
	uint8_t i;
	
	//先发送字节的高位bit7
	for(i = 0; i < 8 ;i++){
		if(Byte & 0x80)	//判断最高位逻辑值
			SDA_HIGH();
		else
			SDA_LOW();
		I2C_Delay();
		
		SCL_HIGH();//CPU产生一个时钟
		I2C_Delay();
		SCL_LOW();
		if( i == 7)
			SDA_HIGH();	//最后一个数据传输完毕,释放总线
		
		Byte <<= 1; 	//左移一个bit,便于下一次循环发送下一位数据
		I2C_Delay();
		
	}
}
/* CPU从I2C总线读取8bit数据 */
uint8_t I2C_ReadByte(void){
	uint8_t i,value;
	value = 0;
	//读到第一个bit为数据的 bit7
	for(i = 0; i < 8 ;i++ ){
		value <<= 1;		//串行读
		SCL_HIGH(); //产生一个时钟
		I2C_Delay();
		if(EEPROM_I2C_SDA_Read())
			value++;
		
		SCL_LOW(); 
		I2C_Delay();
	}
	return value;
}

I2C读写EEPROM

//检查I2C设备
uint8_t ee_CheckDevice(uint8_t _Address){
	uint8_t ucAck;
	
	//发送启动信号
	I2c_Start();
	
	//发送设备地址+读写控制位
	I2c_SendByte(_Address|EEPROM_I2C_WR);
	ucAck=i2c_WaitAck();
	
	
	I2c_Stop();	/* 发送停止信号 */
	i2c_NAck();	/*若输入的是读地址,需要产生非应答信号*/
	return ucAck;
}

/*
*	功能:等待EEPROM到准备状态,在写入数据后,必须调用本函数
*				写入操作时,使用 I2C把数据传输到EEPROM后,
*				EEPROM会向内部空间写入数据需要一定的时间,
*				当EEPEOM写入完成后,会对I2C设备寻址有响应
*				
*				调用本函数可等待至EEPROM内部时序写入完毕
*/
u8	ee_WaitStandby(void){
	u32	wait_count=0;
	
	while(ee_CheckDevice(EEPROM_DEV_ADDR))
	{
		//若检测超过次数,退出循环(避免死循环)	
		if(wait_count ++ >0xFFFF)
			//等待超时	
			return 1;
	}
	return 0;
}
/*
*	功能:向串行EEPROM指定地址写入若干数据,采用页操作可以提高写入效率
*				
*	形参:	_usAddress:起始地址		
*				_usSize:数据长度,单位为字节
*				_pWriteBuf:存放读到的数据的缓冲区指针
*/
u8	ee_WriteBytes(u8 *_pWriteBuf,u16 	_usAddress,u16 _usSize){
	u16 i,m,usAddr;
	/*
	写串行EEPROM不像读操作一样可以连续读取很多字节,每次操作都只能写在同一个page
	对应24xx02芯片,page size =8;
	简单的处理方式为:按照字节写操作模式,每写一个字节,都发送地址
	提高效率采用 page write
	
	*/
	usAddr = _usAddress;
	for(i=0;i<_usSize;i++){
		
		//当发送第一个字节或者是页面首地址时,需要重新发送启动信号和地址
		if((i==0)||(usAddr&(EEPROM_PAGE_SIZE-1))==0){
			//第0步:发送停止信号,结束上一页的通讯,准备下一次通讯
			I2c_Stop();
			/*	通过检查器件应答的方式,判断内部写操作是否完成,一般小于10ms
			CLK频率为KHz时,查询次数为30次左右
			原理与ee_WaitStandby()函数,但是该函数检查完成后会产生停止信号,
			不适用于此处
			*/
			for(m=0;m < 1000; m++){
			//第一步:发送I2C总线信号
				
				I2c_Start();
				
			//第二步:发起控制字节,高7bit是地址,bit0为读写控制位,0:写,1:读
				
				I2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_WR);
				
			//第三步:发送一个时钟,判断器件是否正确应答
					if(i2c_WaitAck() == 0)
							break;
				}
				if(m == 1000)
						goto cmd_fail;//EEPROM器件写超时
				
				//第四步:发送字节地址,24c02只有256个字节,因此1个字节就够了,如果是其他型号,\
				要连发多个地址
				I2c_SendByte((u8)usAddr);
				//第五步:等待ACK
				if(i2c_WaitAck() != 0)
				{
						goto cmd_fail;//EEPROM器件无应答
				}
			}
				//第六步:开始写入数据
				I2c_SendByte(_pWriteBuf[i]);
				
				//第七步:发送ACK
				if(i2c_WaitAck() != 0)
						goto cmd_fail;//EEPROM器件无应答
				
				usAddr++;		//地址增1
			}
			//命令执行成功,发送I2C总线停止信号
			I2c_Stop();
		
			//等待最后一次 EEPROM内部写入完成
				if(ee_WaitStandby() == 1)
						goto cmd_fail;			
			return 1;
	
cmd_fail:	/* 	命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备*/
		I2c_Stop();
		return 0;
}


/*
*	功能:从串行EEPROM指定地址读取若干数据
*				
*	形参:	_usAddress:起始地址		
*				_usSize:数据长度,单位为字节
*				_pReadBuf:存放读到的数据的缓冲区指针
*/
u8	ee_ReadBytes(u8 *_pReadBuf,u16	_usAddress,u16 _usSize){
	u16 i;
	
	//第一步:发起I2C总线启动信号
	I2c_Start();
	
	//第二步:发起控制字节,高七位是地址,最后一位是读写位 ,0:写,1:读
	I2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_WR);//写方向,写地址
	
	//第三步:等待ACK
	if(i2c_WaitAck() != 0)
			goto cmd_fail;		//EEPROM器件无应答
	
	//第四步:发送字节地址,24c02只有256个字节,因此一个字节就够了
	I2c_SendByte((u8)_usAddress);
	
	//第五步:等待ACK
	if(i2c_WaitAck() != 0)
			goto cmd_fail;		//EEPROM器件无应答
	
	//第六步:重新启动I2C总线
	//前面的目的是未来向EEPROM传送地址,下面开始读取数据
	I2c_Start();
	
	//第七步:发送控制字节
	I2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_RD);
	
	//第八步:发送ACK
	if(i2c_WaitAck() != 0)
			goto cmd_fail;		//EEPROM器件无应答
	
	//第九步:循环读取数据
	for(i = 0; i < _usSize; i++){
		
		_pReadBuf[i]=i2c_ReadByte();//读一个字节
		
		//每次读完一个字节后,需要发送ACK,最后一个字节不需要发送ACK,发送NACK
		if(i != _usSize -1)
				i2c_Ack();
		else
			i2c_NAck();
	}
	
	//发送停止信号
	I2c_Stop();	
	return 1;	//执行成功
cmd_fail:	//命令执行失败,切记发送停止信号,避免影响I2C总线上的其他设备
	//I2C总线停止信号
	I2c_Stop();
	return 0;
}



EEPROM读写测试

//EPROM读写测试	
u8 ee_test(void){
	u16 i;
	u8 writebuf[EEPROM_SIZE];
	u8 readbuf[EEPROM_SIZE];
	
	if(ee_CheckDevice(EEPROM_DEV_ADDR)==1){
		printf("没有检测到串行EEPROM\r\n");
		return 0;
	}
	//填充缓冲区
	for(i = 0;i < EEPROM_SIZE; i++)
		writebuf[i]=i;
	
	//写
	if(ee_WriteBytes(writebuf,0,EEPROM_SIZE) == 0){
			printf("写EEPROM出错\r\n");
			return 0;
	}else
	printf("写EEPROM成功\r\n");
	//读
		if(ee_ReadBytes(readbuf,0,EEPROM_SIZE) == 0){
			printf("读EEPROM出错\r\n");
			return 0;
	}else
	printf("读EEPROM成功\r\n");
	
	//输出读到的数据(不一致)
	printf("readbuf:\n");
	for(i=0;i<EEPROM_SIZE ; i++){
		if(readbuf[i] != writebuf[i]){
			printf("0x%02X ", readbuf[i]);
			printf("错误:EEPROM读出和写入的数据不一致\r\n");
			return 0;
		}
		if((i % 16)== 0&&(i != 0))
			printf("\n");
		printf(" %02X", readbuf[i]);
		
	}
	printf("\nEEPROM 读写测试成功\r\n");
	return 1;
}

硬件配置

SDA–>PB7
SCL–>PB6

结果图

成功将1~128的数据写入EEPROM,并且成功读取
在这里插入图片描述

而塞过 2021-2-14
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值