STM32模拟I2C时序读写EEPROM精简版

平台:STM32ZET6(核心板)+ST-LINK/V2+SD卡+USB串口线+外部EEPROM(不需要上拉电阻)

工程介绍:主要文件在USER组中,bsp_i2c_ee.c,bsp_i2c_ee.h,bsp_eeprom.c,bsp_eeprom.h和main.c,其中bsp_i2c_ee.c中主要时基本的模拟I2C时序,而bsp_eeprom.c中主要利用前一个文件中定义的基本操作,进行EEPROM的读写操作。其他类似I2C时序的协议,均可以保留bsp_i2c_ee.c的基础上添加新的内容。本文有些内容借鉴了其他网友的总结,在此表示感谢。


1.硬件部分:电路连接较为简单,笔者在淘宝上买的24C02N主要有四根线,两根电源线,一根SCL和一根SDA。这里我们把SCL和SDA连接到B端口的6和7引脚。如上图所示,如果需要改变引脚设置,只需要更改宏即可。


 MCU和EEPROM连接好之后就像下图所示,MCU作为主机,EEPROM作为从机,从机地址不可以重复,由于STM32ZET6(核心板)上提供了上拉输入功能,我们可以很方便的将EEPROM模块的SCL和SDA直接连接到PB6和PB7上。


2.软件部分:

关键在于 I2C时序的模拟,主要模拟的是起始信号,终止信号,应答信号,非应答信号,等待接收应答信号,发送一个字节,读取一个字节。

这些工作分别由以下函数模拟产生。

int I2C_Start(void);
void I2C_Stop(void);
void I2C_Ack();
void I2C_NoAck();
uint8_t I2C_GetAck(void);
void I2C_SendByte(uint8_t Data);
uint8_t I2C_ReadByte(uint8_t ack);


2.1 起始信号和终止信号


 如图所示,当SCL(SCLK)为高电平,SDA(SDI)从高电平到低电平跳变,作为起始信号。反映在程序上,如下:

int I2C_Start(void)
{
	I2C_SDA_OUT();	//配置SDA为推挽输出
	
	SDA_H;
	SCL_H;//高电平有效
	I2C_delay();//延时
	//查看此时SDA是否就绪(高电平)
	if(!SDA_read)
	{
		printf("\r\nSDA线为低电平,总线忙,退出\r\n");
		return DISABLE;//SDA总线忙,退出
	}
	//制造一个下降沿,下降沿是开始的标志
	SDA_L;
	I2C_delay();
	//查看此时SDA已经变为低电平
	if(SDA_read)
	{
		printf("\r\nSDA线为高电平,总线出错,退出\r\n");
		return DISABLE;//SDA总线忙,退出
	}
	SCL_L;
	return ENABLE;
}

 当SCL(SCLK)为高电平,SDA(SDI)从低电平到高电平跳变,作为终止信号。反映在程序上,如下:

void I2C_Stop(void)
{
	I2C_SDA_OUT();	//配置SDA为推挽输出
	
	SCL_L;
	//制造一个上升沿,上升沿是结束的标志
	SDA_L;	
	SCL_H;//高电平有效
	I2C_delay();//延时
	SDA_H;
	I2C_delay();
}

2.2 应答和非应答信号



//主机的应答信号,主机把第九位置高,从机将其拉低表示应答
static void I2C_Ack()
{
	SCL_L;
	I2C_SDA_OUT();	//配置SDA为推挽输出
	
	SDA_L;//置低
	I2C_delay();   //注意延时时间应该大于4微秒,其他位置也是如此
	SCL_H;
	I2C_delay();
	SCL_L;
}

//主机的非应答信号,从机把第九位置高,主机将其拉低表示非应答
static void I2C_NoAck()
{
	SCL_L;
	I2C_SDA_OUT();	//配置SDA为推挽输出
	
	I2C_delay();
	SDA_H;//置高
	I2C_delay();
	SCL_H;
	I2C_delay();
	SCL_L;
}

2.3 应答位的接收

uint8_t I2C_GetAck(void)
{
  uint8_t time = 0;
	I2C_SDA_IN();	//配置SDA为上拉输入
	
	SDA_H;
	I2C_delay();
	SCL_H;
	I2C_delay();
	while(SDA_read)//从机未应答,若应答,会拉低第九位
	{
		time++;
		if(time > 250)
		{
			//不应答时不可以发出终止信号,否则,复合读写模式下不可以进行第二阶段
			//SCCB_Stop();
			
			SCL_L;
			return DISABLE;
		}
	}
	SCL_L;
	return ENABLE;
}

2.4 读一个字节和写一个字节

//I2C写一个字节
void I2C_SendByte(uint8_t Data)
{
  uint8_t cnt;
  I2C_SDA_OUT();	//配置SDA为推挽输出

  for(cnt=0; cnt<8; cnt++)
  {
    SCL_L;                                 //SCL低(SCL低时,变化SDA)
    I2C_delay();

    if(Data & 0x80)
    {
      SDA_H;                              //SDA高,从最低位开始写起
    }
    else
    {
      SDA_L;                               //SDA低
    }
    Data <<= 1;
    SCL_H;                                //SCL高(发送数据)
    I2C_delay();
  }
  SCL_L;                                   //SCL低(等待应答信号)
  I2C_delay();
}

//I2C读取一个字节
uint8_t I2C_ReadByte(uint8_t ack)
{
  uint8_t cnt;
  uint8_t data;
  I2C_SDA_IN();	//配置SDA为上拉输入
	
  for(cnt=0; cnt<8; cnt++)
  {
    SCL_L;                                 //SCL低
    I2C_delay();
		
    SCL_H;                                //SCL高(读取数据)
    data <<= 1;
    if(SDA_read)
    {
      data |= 0x01;                              //SDA高(数据有效)
    }
    I2C_delay();
  }
  //发送应答信号,为低代表应答,高代表非应答
  if(ack == 1)
  {
     I2C_NoAck();
  }
  else
  {
     I2C_Ack();
  }
  return data;                                   //返回数据
}
2.5 GPIO的初始化,以及SDA的输入输出模式的重新配置,前面我们注意到,当在应答位的接收,以及读取一个字节的信息时,

SDA需要被设置为输入模式,读取从机(EEPROM)发送来的数据。

//GPIO配置函数
void I2C_GPIO_Configuration(void)
{
  GPIO_InitTypeDef  GPIO_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

  GPIO_InitStructure.GPIO_Pin = PIN_I2C_SCL;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  //推挽输出
  GPIO_Init(PORT_I2C_SCL, &GPIO_InitStructure);
	
  GPIO_InitStructure.GPIO_Pin = PIN_I2C_SDA;
  GPIO_Init(PORT_I2C_SDA, &GPIO_InitStructure);
}


//重新设置SDA为上拉输入模式
void I2C_SDA_IN()
{
   GPIO_InitTypeDef  GPIO_InitStructure;
   GPIO_InitStructure.GPIO_Pin = PIN_I2C_SDA;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  //上拉输入,使得板外部不需要接上拉电阻
   GPIO_Init(PORT_I2C_SDA, &GPIO_InitStructure);	
}

//重新设置SDA为推挽输出模式
void I2C_SDA_OUT()
{
   GPIO_InitTypeDef  GPIO_InitStructure;
   GPIO_InitStructure.GPIO_Pin = PIN_I2C_SDA;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  //推挽输出
   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度为50MHz
   GPIO_Init(PORT_I2C_SDA, &GPIO_InitStructure);	
}
//I2C初始化
void I2C_Initializes(void)
{
  I2C_GPIO_Configuration();
  SCL_H;                                  //置位状态
  SDA_H;
}
2.6 与EEPROM相关的宏定义
#define EEPROM_DEV_ADDR           0xA0           //地址(设备地址)
#define EEPROM_WR                 0x00                     //写
#define EEPROM_RD                 0x01                     //读
2.7 调用之前定义好的I2C时序模拟函数,完成EEPROM的读写一个字节操作。(借鉴于他人)

//写入一个字节
int EEPROM_WriteByte(uint16_t Addr, uint8_t Data)
{
  /* 1.开始 */
  I2C_Start();

  /* 2.设备地址/写 */
  I2C_SendByte(EEPROM_DEV_ADDR | EEPROM_WR);
	
  //读取应答位
  if(!I2C_GetAck())
  {
	printf("\r\n发送设备地址时非应答!\r\n");
	I2C_Stop();
	return DISABLE;
  }
  /* 3.数据地址 */
  #if (8 == EEPROM_WORD_ADDR_SIZE)
  I2C_SendByte((uint8_t)(Addr&0x00FF));         //数据地址(8位)
  #else
  I2C_SendByte((uint8_t)(Addr>>8));             //数据地址(16位)
  I2C_SendByte((uint8_t)(Addr&0x00FF));
  #endif
  I2C_GetAck();//不需要判断应答位的状况

  /* 4.写一字节数据 */
  I2C_SendByte(Data);

  /* 5.停止 */
  I2C_Stop();
}

//读取一个字节
int EEPROM_ReadByte(uint16_t Addr, uint8_t *Data)
{
  /* 1.开始 */
  I2C_Start();

 /* 2.设备地址/写 */
  I2C_SendByte(EEPROM_DEV_ADDR | EEPROM_WR);
  //读取应答位
  if(!I2C_GetAck())
  {
	printf("\r\n读一串数据的两相写阶段非应答!\r\n");
	I2C_Stop();
	return DISABLE;
  }

 /* 3.数据地址 */
  #if (8 == EEPROM_WORD_ADDR_SIZE)
  I2C_SendByte((uint8_t)(Addr&0x00FF));         //数据地址(8位)
  #else
  I2C_SendByte((uint8_t)(Addr>>8));             //数据地址(16位)
  I2C_SendByte((uint8_t)(Addr&0x00FF));
  #endif

 /* 4.重新开始 */
  I2C_Start();

 /* 5.设备地址/读 */
  I2C_SendByte(EEPROM_DEV_ADDR | EEPROM_RD);
  //读取应答位
  if(!I2C_GetAck())
  {
	printf("\r\n读一串数据的两相读阶段非应答!\r\n");
	I2C_Stop();
	return DISABLE;
  }

 /* 6.读一字节数据 */
  *Data = I2C_ReadByte(I2C_NOACK);               //只读取1字节(产生非应答)

 /* 7.停止 */
  I2C_Stop();
}
2.8 重复调用读写一个字节函数,实现同时读写多个字节的数据。

//写入多个字节
void EEPROM_WriteNByte(uint16_t Addr, uint8_t *pData, uint16_t Length)
{
	uint16_t i;

	//每写一个字节,调用一次EEPROM_WriteByte
	for(i=0;i<Length;i++)
	{
		//写入数据
		EEPROM_WriteByte(Addr, *pData);
		Addr++;
		pData++;
		//延时
		Delay_us(10000);
	}
}

//读取多个字节
void EEPROM_ReadNByte(uint16_t Addr, uint8_t *pData, uint16_t Length)
{
	uint16_t i;

	//每写一个字节,调用一次EEPROM_ReadByte
	for(i=0;i<Length;i++)
	{
		//写入数据
		EEPROM_ReadByte(Addr, pData);
		Addr++;
		pData++;
		//延时
		Delay_us(10000);
	}
}
2.9 测试读写功能
#define EEPROM_BUF_LEN            64            //测试BUF长度
void System_Initializes(void)
{
  //定时器配置
  SysTick_Init();
	
  //串口配置
  USART_Config();

  //I2C配置
  I2C_Initializes();
}
int main(void)
{
  uint8_t  cnt;
  uint8_t  line = 0;
  uint8_t  w_buf[EEPROM_BUF_LEN];
  uint8_t  r_buf[EEPROM_BUF_LEN];

  System_Initializes();
  /* 填充缓冲区 */
  for(cnt=0; cnt<EEPROM_BUF_LEN; cnt++)
  {
    w_buf[cnt] = cnt;
  }
  printf("************************I2C协议读写EEPROM实验*********************\r\n");
  //打印读取的内容
  for(cnt=0; cnt<EEPROM_BUF_LEN; cnt++)
  {
    printf("w_buf[%d] = %d\t", cnt, w_buf[cnt]);
    line++;
    if(line >= 4)
    {
	printf("\r\n");
	line = 0;
    }
  }
  //0地址连续写EEPROM_BUF_LEN节数据
  EEPROM_WriteNByte(0, w_buf, EEPROM_BUF_LEN);
  Delay_us(100000);

 //0地址连续读EEPROM_BUF_LEN节数据,并打印
  EEPROM_ReadNByte(0, r_buf, EEPROM_BUF_LEN);
  //打印读取的内容
  for(cnt=0; cnt<EEPROM_BUF_LEN; cnt++)
  {
    printf("r_buf[%d] = %d\t", cnt, r_buf[cnt]);
    line++;
    if(line >= 4)
    {
	printf("\r\n");
	line = 0;
    }
  }
}




猜你喜欢

转载自blog.csdn.net/zyxhangiian123456789/article/details/79114166