Bootstrap

STM32 硬件I2C外设

I2C 框图

通信引脚

  输入的引脚有三个,SDA 数据传输引脚,SCL 时钟引脚, SMBA用于SMBUS警告,在 I2C 中并未用到。这些引脚实际对应的硬件引脚可以在 数据手册 的引脚定义中查到。这里小结如下:

引脚I2C1I2C2
SCLPB6 / PB8(重映射)PB10
SDAPB7 / PB9(重映射)PB11

时钟控制

  时钟控制决定了 SCL 的时钟频率,决定了数据的传输速率。其对应的的寄存器是 CCR 可以配置 I2C的模式。

  这个寄存器的15位确定 I2C 处于什么模式,11:0位具体配置频率。

  这里 T P C L K 1 T_{PCLK1} TPCLK1APB1 的时钟,一般是 36 M H z 36M Hz 36MHz T h i g h T_{high} Thigh 就是 SCL 高电平时间。

数据控制

  发送数据,数据先被写入数据寄存器,移到数据移位寄存器,再一位一位交由数据控制从 SDA 端口发送出去。
  而框图下面的比较器,是在STM32作为从机,主机呼叫时用于比较。再下面的地址寄存器用来储存自己的地址。PEC用于数据校验。

整体控制

  控制寄存器 CR1CR2 可以配置 起始,复位,使能,应答,停止等功能。
状态寄存器的一些位在传输过程中会有置位和清零,便于判断传输过程中数据的状态。

STM32的通讯过程

  通信过程中对应寄存器的置位与清零,在 零死角教程 的223页写的非常清楚,这里不再重复。

固件库编程

结构体

初始化结构体

typedef struct
{
  uint32_t I2C_ClockSpeed;       //配置时钟频率   

  uint16_t I2C_Mode;             //配置I2C模式  

  uint16_t I2C_DutyCycle;        //占空比   

  uint16_t I2C_OwnAddress1;      //配置STM32自己的地址
  uint16_t I2C_Ack;              //配置是否开启应答信号  

  uint16_t I2C_AcknowledgedAddress;   //配置从机地址位数
}I2C_InitTypeDef;
  1. 时钟频率直接写入就好。
  2. 模式有I2C模式和SMBus模式。
  3. 占空比的两种配置差别不大
  4. STM32自己的地址位数受限于 I2C_AcknowledgedAddress。这里OwnAddress1主要是为STM32为从机时准备的。
  5. 一般都要开启应答信号。
  6. 最后一个配置从机地址位数,需要和外设匹配。配置为多少为,发送地址信号时就会发送多少位。

固件库函数

  • I2C_Init 用于初始化I2C
  • I2C_Cmd用于使能I2C
  • I2C_GenerateSTART,I2C_GenerateSTOP用于产生起始和结束信号。
  • I2C_GetFlagStatus 获取标志位
  • I2C_Send7bitAddress,I2C_SendData 发送地址和数据
  • I2C_AcknowledgeConfig 应答信号控制,接受足够多的信号后,我们需要产生非应答信号,使用这个函数产生。

EEPROM

Electrically Erasable Programmable ROM,可电擦除ROM

电路图

  其 SDASCL 引脚分别对应 PB7 和 PB6,正好对应了 I2C1 的两个端口。并且这两条线已经连接了上拉电阻了。下面的是引脚说明。

  • A0~A2 是地址引脚,不同的接法对应不同的地址。这个EEPROM有7位地址,其中高4位是1010,低3位是 A2A1A0,这里全接了地,所以EEPROM的地址就是1010000
  • WP 是写保护,这里已经接地,即不使用写保护。

在说明书中也可看到,这个芯片是 2048 bit的,也就是有256个字节。(上面电路图中的 2kb2k bit)。
在这里插入图片描述

读写说明

1.字写入

在这里插入图片描述

2.页写入

  字写入的缺点是,每写入一个字就得再次发送从机地址和写入首地址。这里使用的EEPROM有片写入功能。如果写入的区域是连续的,那只需要发送一次从机地址和写入首地址,就可以连续写入。
在这里插入图片描述

3.当前地址写入

  发送完器件地址后,直接写入,就会在上次操作地址的后面写入。

4.连续读取

  同理,想要读取指定位置的数据,需要把指针先指向要操作的地址,因此要先用写操作指定读取地址。
在这里插入图片描述

开始编程

1.初始化GPIO

static void I2C_GPIO_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_6;
	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;
	GPIO_Init(GPIOB,&GPIO_InitStruct);
}

把GPIO配置为开漏模式,这样才符合I2C空闲时的高阻态要求。

2.初始化I2C外设

static void I2C_Config(void)
{
	I2C_InitTypeDef I2C_InitStruct;
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
	
	I2C_InitStruct.I2C_ClockSpeed  = 400000;
	I2C_InitStruct.I2C_Mode        = I2C_Mode_I2C;
	I2C_InitStruct.I2C_DutyCycle   = I2C_DutyCycle_2;
	I2C_InitStruct.I2C_OwnAddress1 = 0X0A;
	I2C_InitStruct.I2C_Ack         = I2C_Ack_Enable;
	I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	
	I2C_Init(I2C1,&I2C_InitStruct);
	I2C_Cmd(I2C1,ENABLE);
}

配置速度为400k,stm32自己的地址可以随意配置,只要不和从机一样就可以。
把上面两个函数封装为一个便于管理。

void I2C_EEPROM_Config(void)
{
	I2C_GPIO_Config();
	I2C_Config();
}

3.编写发送函数

编写单字节写入函数

下面按照时序图编程。
在这里插入图片描述

#define EEPROM_ADDRESS 0xA0           //从机物理地址
uint32_t I2C_EE_ByteWrite(uint8_t pBuffer ,uint8_t WriteAddr)
{
	I2C_GenerateSTART(I2C1,ENABLE);//起始信号
	
	I2C_Send7bitAddress(I2C1,EEPROM_ADDRESS,I2C_Direction_Transmitter);
										//配置7位地址并设置为写模式
	I2C_SendData(I2C1,WriteAddr);       //写入数据地址
	
	I2C_SendData(I2C1,pBuffer);         //写入数据
	
	I2C_GenerateSTOP(I2C1,ENABLE);     //终止信号
	
	return 1;
	
}
  1. 这个函数完成在指定位置发送指定的一个字节的任务。因此,函数需要传入要写入的数据 pBuffer, 和写入首地址 WriteAddr
  2. 按照时序图,编写发送程序。
  3. 这里的代码是不完整的,需要加入检查。

编写检验错误的函数

  EEPROM的读写耗时较久,而stm32的速度是较快的,因此编写程序时需要严格遵循时序,并且检测对应的标志位。为了保证传输过程中数据正确,我们编写一些DEBUG函数,用来打印日志。
  为了打印日志,我们先编写一些宏,关于这些宏的具体用法,可以参考C语言宏定义拓展
  当等待超时可以调用这个函数提醒我们。注意要把 printf 重新定向到串口。

#define EEPROM_ERROR(fmt,arg...)          printf("<<-EEPROM-ERROR->> "fmt"\n",##arg)
static  uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
  /* Block communication and all processes */
  EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);
  
  return 0;
}

  我们检验错误的主要思想是检验对应标志位是否置位,如果超出时间,我们就用上面的代码报错。而时间的判断我们下面使用while循环来判断,每次循环检查一次标志位,并且把时间值减1,当时间为0时报错。
  因为这个时间值是一直被循环又不断被赋予新的值的,我们需要用 volatile 来避免其被优化。在固件库中 volatile 被定义为 __IO

//宏定义两个值,以便挑选
#define I2CT_FLAG_TIMEOUT         ((uint32_t)0x1000)
#define I2CT_LONG_TIMEOUT         ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
static __IO uint32_t  I2CTimeout   //把这个值作为循环

  相应的置位已经被总结为事件,固件库中也有检测事件的函数,对不同的事件进行不同的编写,如检测事件5(EV5)的程序如下:

//EV5
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT) == ERROR)
{
	I2CTimeout--;
	if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(0);
}

  I2C_EVENT_MASTER_MODE_SELECT 即为对应的事件5(EV5)。由此我们把上面的代码都加上检测的代码。

完整的字节写入

就是在每次操作后加入检测事件环节。

uint32_t I2C_EE_ByteWrite(uint8_t pBuffer ,uint8_t WriteAddr)
{
	I2C_GenerateSTART(I2C1,ENABLE);
	//EV5
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(0);
	}
	
	I2C_Send7bitAddress(I2C1,EEPROM_ADDRESS,I2C_Direction_Transmitter);
	//EV6
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(1);
	}
	
	I2C_SendData(I2C1,WriteAddr);
	//EV8_2
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(2);
	}
/**************************Sent Data*******************************/
	I2C_SendData(I2C1,pBuffer);
	//EV8_2
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(3);
	}
	
	I2C_GenerateSTOP(I2C1,ENABLE);
	return 1;
}

连续页写入

  连续写入就是在发送完第一个数据后,继续发送数据而不是发送结束信号。因此只需要把上述代码中最后 Sent Data 部分替换为循环即可。
  为了完成循环,需要增加一个传入参数 NumByteToWrite 表示要发送的数据个数,并且把 pBuffer 改为数组,以便发送。
  这里只给出Sent Data 部分代码。

for(i=0;i<NumByteToWrite;i++)
	{
		I2C_SendData(I2C1,pBuffer[i]);
		//EV8_2
		I2CTimeout = I2CT_FLAG_TIMEOUT;
		while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR)
		{
			I2CTimeout--;
			if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(3);
		}
	}

4.编写读取函数

整体过程

  依然是按照时序图编写程序。为了简便起见,这里没有写事件检测。
在这里插入图片描述

uint32_t I2C_EE_PageRead(uint8_t* pBuffer,uint8_t ReadAddr,uint16_t NumByteToRead)
{
	uint8_t i =0;
/******************************* START1 *********************************/	
	I2C_GenerateSTART(I2C1,ENABLE);
	I2C_Send7bitAddress(I2C1,EEPROM_ADDRESS,I2C_Direction_Transmitter);
	
	I2C_SendData(I2C1,ReadAddr);
/******************************* START2 ***********************************/	
	I2C_GenerateSTART(I2C1,ENABLE);
	I2C_Send7bitAddress(I2C1,EEPROM_ADDRESS,I2C_Direction_Receiver);
	
/**************************Receive Data*******************************/
	for(i=0;i<NumByteToRead;i++)
	{
		if(i == NumByteToRead-1) 	I2C_AcknowledgeConfig(I2C1,DISABLE);
		*pBuffer = I2C_ReceiveData(I2C1);
		pBuffer++;
	}
	
	I2C_GenerateSTOP(I2C1,ENABLE);
	
	I2C_AcknowledgeConfig(I2C1,ENABLE);
	return 1;
}
  1. 接收的时序更加复杂,一开始发送从机地址和储存地址和写入过程一致。但完成这些后,要重新以读取的模式是发送从机地址。
  2. 完成这些就进入接收数据 Receive Data 部分了。使用循环来接收数据。
  3. 时序图中表明,在接收完最后一个数据后,产生非应答信号(就是不用应答),再产生结束信号即可结束。
  4. 初始化时已经配置,每次接收数据后自动应答。因此,在接收循环中,接收最后一次数据前,使用 I2C_AcknowledgeConfig 关闭应答。
  5. 完成接收,产生结束信号。并且再次开启自动应答,以便再次使用 I2C

完整的读取

  为读取过程加入事件检测,并且在读取前判断I2C是否正忙。
在这里插入图片描述

uint32_t I2C_EE_PageRead(uint8_t* pBuffer
								,uint8_t ReadAddr,uint16_t NumByteToRead)
{
	uint8_t i =0;
/*****************************Judge wheather I2C is busy***********************/
	I2CTimeout = I2CT_LONG_TIMEOUT;
	while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY))
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(0);

	}
/******************************* START1 ***********************************/	
	I2C_GenerateSTART(I2C1,ENABLE);
	//EV5
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(0);

	}
	
	I2C_Send7bitAddress(I2C1,EEPROM_ADDRESS,I2C_Direction_Transmitter);
	//EV6
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(1);

	}
	
	I2C_SendData(I2C1,ReadAddr);
	//EV8_2
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(3);
	}
/******************************* START2 ***********************************/		
	I2C_GenerateSTART(I2C1,ENABLE);
	//EV5
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(0);

	}
	
	I2C_Send7bitAddress(I2C1,EEPROM_ADDRESS,I2C_Direction_Receiver);
	//EV6
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) == ERROR)
	{
		I2CTimeout--;
		if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(1);

	}
/**********************************Receive Data*********************************/
	for(i=0;i<NumByteToRead;i++)
	{
		if(i == NumByteToRead-1) 	I2C_AcknowledgeConfig(I2C1,DISABLE);
		*pBuffer = I2C_ReceiveData(I2C1);
		I2CTimeout = I2CT_FLAG_TIMEOUT;      //EV7
		while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED) == ERROR)
		{
			I2CTimeout--;
			if(I2CTimeout == 0) return I2C_TIMEOUT_UserCallback(0);

		}
		pBuffer++;
	}


	I2C_GenerateSTOP(I2C1,ENABLE);
	
	I2C_AcknowledgeConfig(I2C1,ENABLE);
	return 1;
}

这里需要注意的是,第一次起始的过程 START1 和写入过程一致,因此要检测 EV5 , EV6 , EV8_2 这些事件。完成第一起始后;从第二次起始 START2 开始,就和框图一样了。

5.完成代码

  完成上面的代码后,我们就已经完成了对 EEPROM 的读和写了。下面在main函数中调用这些函数,实现功能。

uint8_t I2C_Data_Write[200];
uint8_t I2C_Data_Read[200];
void Text()
{
	uint8_t i=0;
	for(i=0;i<200;i++)
	{
		I2C_Data_Write[i] = i;
	}
	I2C_EE_PageWrite(I2C_Data_Write,0X00,200);																					
	EEPROM_INFO("已完成写入数据");
	
	EEPROM_INFO("开始读取");	
	I2C_EE_PageRead(I2C_Data_Read,0X00,200);

	
	EEPROM_INFO("完成读操作,正在打印");
	for(i=0;i<200;i++)
	{
		printf("0x%02x ",I2C_Data_Read[i]);
	}
	
	EEPROM_INFO("读写已全部完成");
	
}

int main(void)
{
	LED_GPIO_Config();	
	USART_Config();
	I2C_EEPROM_Config();
	
	LED_B(ON);
	Text();
	LED_B(OFF);
	LED_G(ON);
}
;