文章目录
I2C 框图
通信引脚
输入的引脚有三个,SDA 数据传输引脚,SCL 时钟引脚, SMBA用于SMBUS警告,在 I2C 中并未用到。这些引脚实际对应的硬件引脚可以在 数据手册 的引脚定义中查到。这里小结如下:
引脚 | I2C1 | I2C2 |
---|---|---|
SCL | PB6 / PB8(重映射) | PB10 |
SDA | PB7 / PB9(重映射) | PB11 |
时钟控制
时钟控制决定了 SCL 的时钟频率,决定了数据的传输速率。其对应的的寄存器是 CCR 可以配置 I2C的模式。
这个寄存器的15位确定 I2C 处于什么模式,11:0位具体配置频率。
这里 T P C L K 1 T_{PCLK1} TPCLK1 是 APB1 的时钟,一般是 36 M H z 36M Hz 36MHz。 T h i g h T_{high} Thigh 就是 SCL 高电平时间。
数据控制
发送数据,数据先被写入数据寄存器,移到数据移位寄存器,再一位一位交由数据控制从 SDA 端口发送出去。
而框图下面的比较器,是在STM32作为从机,主机呼叫时用于比较。再下面的地址寄存器用来储存自己的地址。PEC用于数据校验。
整体控制
控制寄存器 CR1 和 CR2 可以配置 起始,复位,使能,应答,停止等功能。
状态寄存器的一些位在传输过程中会有置位和清零,便于判断传输过程中数据的状态。
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;
- 时钟频率直接写入就好。
- 模式有I2C模式和SMBus模式。
- 占空比的两种配置差别不大
- STM32自己的地址位数受限于
I2C_AcknowledgedAddress
。这里OwnAddress1
主要是为STM32为从机时准备的。 - 一般都要开启应答信号。
- 最后一个配置从机地址位数,需要和外设匹配。配置为多少为,发送地址信号时就会发送多少位。
固件库函数
- I2C_Init 用于初始化I2C
- I2C_Cmd用于使能I2C
- I2C_GenerateSTART,I2C_GenerateSTOP用于产生起始和结束信号。
- I2C_GetFlagStatus 获取标志位
- I2C_Send7bitAddress,I2C_SendData 发送地址和数据
- I2C_AcknowledgeConfig 应答信号控制,接受足够多的信号后,我们需要产生非应答信号,使用这个函数产生。
EEPROM
Electrically Erasable Programmable ROM,可电擦除ROM
电路图
其 SDA 和 SCL 引脚分别对应 PB7 和 PB6,正好对应了 I2C1 的两个端口。并且这两条线已经连接了上拉电阻了。下面的是引脚说明。
- A0~A2 是地址引脚,不同的接法对应不同的地址。这个EEPROM有7位地址,其中高4位是1010,低3位是 A2A1A0,这里全接了地,所以EEPROM的地址就是1010000。
- WP 是写保护,这里已经接地,即不使用写保护。
在说明书中也可看到,这个芯片是 2048 bit的,也就是有256个字节。(上面电路图中的 2kb 指 2k 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;
}
- 这个函数完成在指定位置发送指定的一个字节的任务。因此,函数需要传入要写入的数据 pBuffer, 和写入首地址 WriteAddr。
- 按照时序图,编写发送程序。
- 这里的代码是不完整的,需要加入检查。
编写检验错误的函数
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;
}
- 接收的时序更加复杂,一开始发送从机地址和储存地址和写入过程一致。但完成这些后,要重新以读取的模式是发送从机地址。
- 完成这些就进入接收数据 Receive Data 部分了。使用循环来接收数据。
- 时序图中表明,在接收完最后一个数据后,产生非应答信号(就是不用应答),再产生结束信号即可结束。
- 初始化时已经配置,每次接收数据后自动应答。因此,在接收循环中,接收最后一次数据前,使用
I2C_AcknowledgeConfig
关闭应答。 - 完成接收,产生结束信号。并且再次开启自动应答,以便再次使用 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);
}