014 - STM32学习笔记 - I2C访问存储器
1、存储器分类
存储器主要分为两类:易失性存储器和非易失性存储器,从字面上理解,判断易失/非易失主要取决于设备掉电后,存储的数据是否会丢失。常规的来说,易失性存储器存取速度快,掉电后数据丢失,非易失性存储器掉电数据不丢失,但读取速度相对于易失性存储器来说较慢。
易失性存储器中常见的为RAM存储器,即随机存储器(Random Access Memory),随机存储是指当存储器的消息倍读取或者写入时,需要的时间和这段信息所在位置无关,,根据RAM的读取机制,又分为动态随机存储器DRAM(Dynamic RAM)和静态随机存储器SRAM(Static RAM)两种。
2、I2C读写EEPROM
I2C协议简介
I2C协议(Inter-Integrated Circuit)时飞利浦公司开发的,由于它引脚少,硬件实现简答,可扩展性强,不需要USART、CAN等通讯协议的外部收发设备,所以被广泛的应用在系统内多个集成电路(IC)之间的通讯。
I2C是支持支持多设备通信的总线,所谓“总线”,是指多个设备共用信号线,在一个I2C通讯总线中,可以连接多个I2C通讯设备,支持多个主机于从机通讯,所有连接在I2C总线中的设备,都有一个独立的地址,主机可以利用这个地址访问不同的设备,I2C有两条总线,SCL(双向串行数据)总线和SDA(川航时钟)总线,SDA用来表示数据,SCL为数据收发提供同步时钟信号。
总线通过上拉电阻,当I2C设备空闲时,会输出高阻态,当所有设备都空闲时,由上拉电阻把总线拉成高电平,当多个主机同时使用总线时,为了防止数据通途,会利用仲裁的方式决定由哪个设备占用总线。
这里解释一下,高阻态再电路中的电平表现形式为非高非低,假设上图的EEPROM和ADC都不输出数据,那么EEPROM和ADC都处于高阻态,那么当前SDA总线就处于高电平状态,如果此时EEPROM输出数据为0,那么再EEPROM的SDA端就相当于输出低电平,此时SDA总线就处于低电平状态。
I2C具有是那种传输模式,标准模式(100kbit/s)、快速模式(400kbit/s)和高速模式(3.4Mbit/s),目前多数I2C设备不支持高速模式。
关于I2C的架构和通讯过程,内容太多,篇幅很长,这里不做赘述了,有兴趣的可以看看相关资料。
下面开始实战编程:
结合之前USART的内容,首先对I2C的GPIO引脚进行配置,配置之前,对相关的参数进行宏定义:
#define EEPROM_ADDR 0xA0 //EEPROM地址
#define EEPROM_I2C I2C1 //选择I2C1
#define EEPROM_I2C_CLK RCC_APB1Periph_I2C1 //I2C1总线时钟
#define EEPROM_I2C_SPEED 400000 //I2C速率
#define EEPROM_I2C_OAdr 0x77 //本机I2C地址
#define EEPROM_SCL_GPIO_PORT GPIOB //SCL端口
#define EEPROM_SCL_GPIO_CLK RCC_AHB1Periph_GPIOB //SCL时钟
#define EEPROM_SCL_PIN GPIO_Pin_6 //SCL引脚
#define EEPROM_SCL_AF GPIO_AF_I2C1 //I2C复用功能
#define EEPROM_SCL_PinSoruce GPIO_PinSource6 //PB6源
#define EEPROM_SDA_GPIO_PORT GPIOB //SDA端口
#define EEPROM_SDA_GPIO_CLK RCC_AHB1Periph_GPIOB //SDA时钟
#define EEPROM_SDA_PIN GPIO_Pin_7 //SDA引脚
#define EEPROM_SDA_AF GPIO_AF_I2C1 //I2C复用功能
#define EEPROM_SDA_PinSoruce GPIO_PinSource7 //PB7源
接下来对GPIO进行配置
void EEPROM_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//使能GPIO时钟,只要是外设,第一步一定是开时钟
RCC_AHB1PeriphClockCmd(EEPROM_SCL_GPIO_CLK|EEPROM_SDA_GPIO_CLK,ENABLE);
//选择开漏输出
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
//选择悬空
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
//速度设置为50MHz,这里I2C的速度最大为400KHz,只要速度不小于这个就行
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
//将PB6复用功能连接到I2C1,用法与USART的相同
GPIO_PinAFConfig(EEPROM_SCL_GPIO_PORT,EEPROM_SCL_PinSoruce,EEPROM_SCL_AF);
//将PB7复用功能连接到I2C1
GPIO_PinAFConfig(EEPROM_SDA_GPIO_PORT,EEPROM_SDA_PinSoruce,EEPROM_SDA_AF);
//选择模式为复用模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
//设置SCL的引脚号
GPIO_InitStructure.GPIO_Pin = EEPROM_SCL_PIN;
//初始化SCL引脚
GPIO_Init(EEPROM_SCL_GPIO_PORT,&GPIO_InitStructure);
//设置SDA的引脚号
GPIO_InitStructure.GPIO_Pin = EEPROM_SDA_PIN;
//初始化SDA引脚
GPIO_Init(EEPROM_SDA_GPIO_PORT,&GPIO_InitStructure);
}
GPIO配置完后,就该对I2C进行配置了
//初始化I2C模式
void EEPROM_I2C_ModeConfig(void)
{
I2C_InitTypeDef I2C_InitStructure;
//使能I2C时钟,只要是外设,第一步一定是开时钟
RCC_APB1PeriphClockCmd(EEPROM_I2C_CLK,ENABLE);
//设置I2C响应使能
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
//设置地址为7位
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
//设置I2C时钟速率为400KHz
I2C_InitStructure.I2C_ClockSpeed = EEPROM_I2C_SPEED;
//设置I2C占空比为Low:High = 2
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
//设置I2C模式为I2C_Mode_I2C模式
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
//设置I2C本机地址为0x77(只要和总线上其他设备地址不冲突即可)
I2C_InitStructure.I2C_OwnAddress1 = EEPROM_I2C_OAdr;
//初始化I2C
I2C_Init(EEPROM_I2C,&I2C_InitStructure);
//使能I2C
I2C_Cmd(EEPROM_I2C,ENABLE);
}
以上就是对GPIO及I2C的配置,这里仅仅是进行配置,对于I2C总线的读写功能需要单独实现,结合I2C的时序图,具体实现如下:
在时序图中,关于EVx的说明,在stm32f4xx_i2c.c文件中有说明,这里贴出来看一下
@param I2C_EVENT: specifies the event to be checked.
* This parameter can be one of the following values:
* @arg I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED: EV1
* @arg I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED: EV1
* @arg I2C_EVENT_SLAVE_TRANSMITTER_SECONDADDRESS_MATCHED: EV1
* @arg I2C_EVENT_SLAVE_RECEIVER_SECONDADDRESS_MATCHED: EV1
* @arg I2C_EVENT_SLAVE_GENERALCALLADDRESS_MATCHED: EV1
* @arg I2C_EVENT_SLAVE_BYTE_RECEIVED: EV2
* @arg (I2C_EVENT_SLAVE_BYTE_RECEIVED | I2C_FLAG_DUALF): EV2
* @arg (I2C_EVENT_SLAVE_BYTE_RECEIVED | I2C_FLAG_GENCALL): EV2
* @arg I2C_EVENT_SLAVE_BYTE_TRANSMITTED: EV3
* @arg (I2C_EVENT_SLAVE_BYTE_TRANSMITTED | I2C_FLAG_DUALF): EV3
* @arg (I2C_EVENT_SLAVE_BYTE_TRANSMITTED | I2C_FLAG_GENCALL): EV3
* @arg I2C_EVENT_SLAVE_ACK_FAILURE: EV3_2
* @arg I2C_EVENT_SLAVE_STOP_DETECTED: EV4
* @arg I2C_EVENT_MASTER_MODE_SELECT: EV5
* @arg I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED: EV6
* @arg I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED: EV6
* @arg I2C_EVENT_MASTER_BYTE_RECEIVED: EV7
* @arg I2C_EVENT_MASTER_BYTE_TRANSMITTING: EV8
* @arg I2C_EVENT_MASTER_BYTE_TRANSMITTED: EV8_2
* @arg I2C_EVENT_MASTER_MODE_ADDRESS10: EV9
根据时序图,先实现主发送器的写函数,程序如下(关于超时检测的程序会放在最后,前面看不懂的,可以先忽略超时检测的内容):
//byte write函数
uint32_t EEPROM_Byte_Write(uint8_t *pData,uint8_t addr)
{
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
while(I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY)) /* 检测I2C总线状态 */
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(0);
}
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
/*-----------------------1 产生开始信号 ----------------------------*/
I2C_GenerateSTART(EEPROM_I2C,ENABLE);
/*-----------------------2 等待EV5事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(1);
}
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
/*-----------------------3 发送要访问的设备地址 ----------------------------*/
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDR,I2C_Direction_Transmitter);
/*-----------------------4 等待EV6事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(2);
}
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
/*------5 发送要写入的EEPROM内部地址,即EEPROM内部存储器地址 -----*/
I2C_SendData(EEPROM_I2C,addr);
/*-----------------------6 等待EV8事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(3);
}
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
/*-----------------------7 发送数据 ----------------------------*/
I2C_SendData(EEPROM_I2C,*pData);
/*-----------------------8 等待EV8事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(4);
}
/*-----------------------9 发送停止信号 ----------------------------*/
I2C_GenerateSTOP(EEPROM_I2C,ENABLE);
printf("\r\nEEPROM数据写入完成!写入数据为:0x%x\r\n",*pData);
return 1;
}
在实现了写数据到EEPROM中后,还需要再实验一个从EEPROM中读取数据的函数,用来验证我们上面的写程序,同样,读取的函数也要根据时序图来编写
读数据实现:
uint8_t readTemp = 0;
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
while(I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY)) /* 检测I2C总线状态 */
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(5);
}
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
/*-----------------------1 产生开始信号 ----------------------------*/
I2C_GenerateSTART(EEPROM_I2C,ENABLE);
/*-----------------------2 等待EV5事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(6);
}
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
/*-----------------------3 发送要访问的设备地址 ----------------------------*/
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDR,I2C_Direction_Transmitter);
/*-----------------------4 等待EV6事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(7);
}
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
/*------5 发送要写入的EEPROM内部地址,即EEPROM内部存储器地址 -----*/
I2C_SendData(EEPROM_I2C,addr);
/*-----------------------6 等待EV8事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(8);
}
/****************************************************************************************/
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
/*-----------------------7 产生第二次开始信号 ----------------------------*/
I2C_GenerateSTART(EEPROM_I2C,ENABLE);
/*-----------------------8 等待EV5事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(9);
}
/*----------------------- 发送要访问的设备地址:发送read方向 ----------------------------*/
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDR,I2C_Direction_Receiver);
/*-----------------------9 等待EV6事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(10);
}
I2CTimeout = I2CT_FLAG_TIMEOUT; /* 设置超时等待时间 */
I2C_AcknowledgeConfig(EEPROM_I2C,DISABLE); /* 关闭响应 */
/*-----------------------11 等待EV7事件 ----------------------------*/
while(I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS)
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(11);
}
/*-----------------------10 读取数据 ----------------------------*/
readTemp = I2C_ReceiveData(EEPROM_I2C);
/*-----------------------12 发送停止信号 ----------------------------*/
I2C_GenerateSTOP(EEPROM_I2C,ENABLE);
printf("\r\nEEPROM数据读取完成!\r\n");
I2C_AcknowledgeConfig(EEPROM_I2C,ENABLE); /* 使能响应,方便下一次操作 */
return readTemp;
下来说明一下超时检测,当主控发送信号后,需要等待从机反馈响应事件,可以使用I2C_CheckEvent
来检测对应的事件信号,但是这里需要主义的是,当发主机发送数据给从机后,从机并不能在第一时间就反馈响应,可能需要等一会,那我们不可能就判断一次,而且若从机一直未响应,也不可能一直在这里等待响应信号,因此我们设置一个超时检测变量I2CTimeout
,当响应信号一直未收到时,就在while
循环里面一直给I2CTimeout
进行自减操作,直到I2CTimeout
自减到0为止还没有收到响应信号的话,就认为等待响应超时,那么就可以直接退出了。
当产生响应超时时,可以通过回调函数I2C_TIMEOUT_UserCallback
来提示用户I2C响应超时,具体实现如下:
static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* EEPROM_ERROR为宏实现,实际为printf */
/* #define EEPROM_ERROR(fmt,arg...) printf("<<-EEPROM-ERROR->> "fmt"\n",##arg) */
EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);
return 0;
}
完成以上内容后,就可以在主函数中调用来测试IIC读写EEPROM的功能了,main.c的实现如下:
#include "stm32f4xx.h"
#include "bsp_led.h"
#include "bsp_systick.h"
#include "bsp_usart_dma.h" /* 这里用的都是之前学习过程中自己写的板级支持包 */
#include "stm32f4xx_dma.h"
#include "bsp_i2c_ee.h"
#include <stdio.h>
int main(void)
{
int8_t Test_Addr = 0x00;
int8_t Test_WriteByte = 0x47;
int8_t Test_ReadByte = 0;
LED_Config();
DEBUG_USART1_Config();
SysTick_Init();
printf("这是一个EEPROM测试实验\n");
printf("\r\n初始化EEPROM\r\n");
EEPROM_GPIO_Config();
printf("\r\n初始化I2C模式配置\r\n");
EEPROM_I2C_ModeConfig();
EEPROM_Byte_Write(&Test_WriteByte,Test_Addr);
Delay_ms(10);
Test_ReadByte = EEPROM_Byte_Read(Test_Addr);
printf("I2C中读取到的数据为:0x%x\n",Test_ReadByte);
while(1)
{
if(Test_ReadByte == Test_WriteByte)
{
LED_G_TOGGLE;
Delay_ms(1000);
}
else
{
LED_R_TOGGLE;
Delay_ms(1000);
}
}
}
这里需要注意的是,我在刚写完程序的时候,在Test_ReadByte = EEPROM_Byte_Read(Test_Addr);
之前没有加延时函数,在测试过程中,一直卡在读取的第7步过不去:
<<-EEPROM-ERROR->> I2C 等待超时!errorCode = 7
查了一下资料,才知道STM32在向EEPROM写入数据后,如果立马就去读数据,但是这时EEPROM内部正在处理自己的事情(可能正在写数据),因此立马去读的话,就没办法往下继续运行了。因此在这里增加一个延时函数,确保EEPROM能完成内部逻辑。
最终验证结果: