014 - STM32学习笔记 - I2C访问存储器(一)

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总线中的设备,都有一个独立的地址,主机可以利用这个地址访问不同的设备,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能完成内部逻辑。
最终验证结果:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

放学校门口见

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值