嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记09:EEPROM

本文介绍了蓝桥杯STM32G431项目中关于HAL库开发的学习笔记,详细讲解了IIC协议和EEPROM的使用,包括IIC通信原理、驱动代码实现以及如何通过I2C与EEPROM进行数据读写,用于记录设备状态和持久化数据。
摘要由CSDN通过智能技术生成

   系列文章目录

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记01:赛事介绍与硬件平台

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记02:开发环境安装

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记03:G4时钟结构

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记04:从零开始创建工程模板并开始点灯

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记05:Systick滴答定时器

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记06:按键输入

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记07:ADC模数转换

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记08:LCD液晶屏

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记09:EEPROM

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记10:USART串口通讯


目录

   系列文章目录

前言

一、IIC协议

二、比赛驱动解析

三、EEPROM基础知识

 四、程序设计

总结


前言

上一节我们学习了LCD模块,这节课学习一下EEPROM。学习EEPROM芯片之前要先学习一下它所使用的IIC协议,它通过这个协议和单片机进行通讯。


一、IIC协议

IIC是一种非常流行并且功能强大的总线,它可以用于主器件与从器件之间交换数据使用。主器件就是我们所用的单片机了,不同的外设可以共享一条总线与主器件进行通信。IIC协议需要用到两根线,一根数据线,一根时钟线,相比于只有一个I/O口的单总线协议,IIC可以提高通讯速率,也不同于像SPI这样,有四根线的总线,IIC只需要用到主器件的两个引脚,这也是它的优点之一。IIC协议是一个通用的协议,有很多芯片都是用这个协议进行通讯的,EEPROM就是其中之一。

这里我们准备了一个理解IIC协议的文档,是由德州仪器公司编写,我们将其打开。可以看到IIC总线使用的是开漏输出模式,相比推挽输出来说,它的稳定性更好,因为高电平是由上拉电阻提供的,没有设备可以强制线路出现高电平,这意味着总线永远不会出现通信问题,而如果使用推挽输出的话就会很容易短路。

IIC总线是双向接口,主器件可以给从器件,从器件也可以给主器件发数据。但是除非主设备寻址,否则从器件不能主动传数据。IIC有个特点就是每个设备有一个特定的器件地址。不同的设备 它的器件地址是不一样的,就可以区分总线上的多个设备了。

主设备访问从设备的一般过程为:

假设主设备要发数据:

  1. 主器件发送START条件并寻址从器件。
  2. 主器件发数据。
  3. 主器件以STOP条件终止传输。

假设主设备要接收数据:

  1. 主器件发送START条件并寻址从器件。
  2. 主器件发送请求读取的寄存器的地址。
  3. 主器件从发送器接收数据。
  4. 主器件以STOP条件终止传输。

因为IIC协议是通用协议,所以不管什么器件,高低电平的变化都是一样的。所谓START条件就是SCL为高电平时,SDA线上由高到低。当SCL为高电平是,SDL由低到高就是STOP条件

对于数据来说,一个字节由SDA上的8位组成。SCL会每隔一个周期产生一个高电平,SDA线上的数据必须在时钟周期的高电平期间保持稳定,这是因为SCL为高电平时数据线的变化会被解释成控制指令(START和STOP),所以传数据时,SCL为高的时候SDA是不能变的,变了就有可能STOP了。

而且数据传输的时候,先传输最高位(MSB)。此外我们还注意到,8位数据过后还有一个ACK的位,它代表确认(ACK)和不确认(NACK),每个字节后面都要有一个来自接收方的ACK位,代表我收到了这个字节,可以发送下个字节。ACK是由SDA的低电平定义的,如果SDA是高位说明是非应答NACK。

除此之外我们还要了解一下通信的流程。当单片机向从器件写入数据时,首先发送一个开始位,然后发送一个从器件的七位地址,第八位是用来告诉从器件我是要读数据还是写数据。1即为Read,0则为Write。然后是一个应答位。然后送寄存器地址,接一个应答,然后开始发送一个字节的数据,再接一个应答位,发送了n个字节后,主器件发送停止位停止传输。

当单片机从从器件接收数据时,也是类似的。单片机先发一个主从器件地址(这里是0,write),再发一个寄存器地址,然后要重新START一下,再接收一个从设备地址,然后开始读数据。最后停止的时候主机要发个非应答NACK,然后STOP。

二、比赛驱动解析

比赛会给一个i2c的底层驱动代码,我们可以直接调用,但是最好能看懂。其实驱动代码就是上面i2c协议原理的代码实现。

#include "i2c.h"

#define DELAY_TIME	20


/**
  * @brief SDAÏßÊäÈëģʽÅäÖÃ
  * @param None
  * @retval None
  */
void SDA_Input_Mode()
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    GPIO_InitStructure.Pin = GPIO_PIN_7;
    GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
    GPIO_InitStructure.Pull = GPIO_PULLUP;
    GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}

/**
  * @brief SDAÏßÊä³öģʽÅäÖÃ
  * @param None
  * @retval None
  */
void SDA_Output_Mode()
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    GPIO_InitStructure.Pin = GPIO_PIN_7;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStructure.Pull = GPIO_NOPULL;
    GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}

/**
  * @brief SDAÏßÊä³öÒ»¸öλ
  * @param val Êä³öµÄÊý¾Ý
  * @retval None
  */
void SDA_Output( uint16_t val )
{
    if ( val )
    {
        GPIOB->BSRR |= GPIO_PIN_7;
    }
    else
    {
        GPIOB->BRR |= GPIO_PIN_7;
    }
}

/**
  * @brief SCLÏßÊä³öÒ»¸öλ
  * @param val Êä³öµÄÊý¾Ý
  * @retval None
  */
void SCL_Output( uint16_t val )
{
    if ( val )
    {
        GPIOB->BSRR |= GPIO_PIN_6;
    }
    else
    {
        GPIOB->BRR |= GPIO_PIN_6;
    }
}

/**
  * @brief SDAÊäÈëһλ
  * @param None
  * @retval GPIO¶ÁÈëһλ
  */
uint8_t SDA_Input(void)
{
	if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_SET){
		return 1;
	}else{
		return 0;
	}
}


/**
  * @brief I2CµÄ¶ÌÔÝÑÓʱ
  * @param None
  * @retval None
  */
static void delay1(unsigned int n)
{
    uint32_t i;
    for ( i = 0; i < n; ++i);
}

/**
  * @brief I2CÆðʼÐźÅ
  * @param None
  * @retval None
  */
void I2CStart(void)
{
    SDA_Output(1);
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    SDA_Output(0);
    delay1(DELAY_TIME);
    SCL_Output(0);
    delay1(DELAY_TIME);
}

/**
  * @brief I2C½áÊøÐźÅ
  * @param None
  * @retval None
  */
void I2CStop(void)
{
    SCL_Output(0);
    delay1(DELAY_TIME);
    SDA_Output(0);
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    SDA_Output(1);
    delay1(DELAY_TIME);

}

/**
  * @brief I2CµÈ´ýÈ·ÈÏÐźÅ
  * @param None
  * @retval None
  */
unsigned char I2CWaitAck(void)
{
    unsigned short cErrTime = 5;
    SDA_Input_Mode();
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    while(SDA_Input())
    {
        cErrTime--;
        delay1(DELAY_TIME);
        if (0 == cErrTime)
        {
            SDA_Output_Mode();
            I2CStop();
            return ERROR;
        }
    }
    SDA_Output_Mode();
    SCL_Output(0);
    delay1(DELAY_TIME);
    return SUCCESS;
}

/**
  * @brief I2C·¢ËÍÈ·ÈÏÐźÅ
  * @param None
  * @retval None
  */
void I2CSendAck(void)
{
    SDA_Output(0);
    delay1(DELAY_TIME);
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    SCL_Output(0);
    delay1(DELAY_TIME);

}

/**
  * @brief I2C·¢ËÍ·ÇÈ·ÈÏÐźÅ
  * @param None
  * @retval None
  */
void I2CSendNotAck(void)
{
    SDA_Output(1);
    delay1(DELAY_TIME);
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    SCL_Output(0);
    delay1(DELAY_TIME);

}

/**
  * @brief I2C·¢ËÍÒ»¸ö×Ö½Ú
  * @param cSendByte ÐèÒª·¢Ë͵Ä×Ö½Ú
  * @retval None
  */
void I2CSendByte(unsigned char cSendByte)
{
    unsigned char  i = 8;
    while (i--)
    {
        SCL_Output(0);
        delay1(DELAY_TIME);
        SDA_Output(cSendByte & 0x80);
        delay1(DELAY_TIME);
        cSendByte += cSendByte;
        delay1(DELAY_TIME);
        SCL_Output(1);
        delay1(DELAY_TIME);
    }
    SCL_Output(0);
    delay1(DELAY_TIME);
}

/**
  * @brief I2C½ÓÊÕÒ»¸ö×Ö½Ú
  * @param None
  * @retval ½ÓÊÕµ½µÄ×Ö½Ú
  */
unsigned char I2CReceiveByte(void)
{
    unsigned char i = 8;
    unsigned char cR_Byte = 0;
    SDA_Input_Mode();
    while (i--)
    {
        cR_Byte += cR_Byte;
        SCL_Output(0);
        delay1(DELAY_TIME);
        delay1(DELAY_TIME);
        SCL_Output(1);
        delay1(DELAY_TIME);
        cR_Byte |=  SDA_Input();
    }
    SCL_Output(0);
    delay1(DELAY_TIME);
    SDA_Output_Mode();
    return cR_Byte;
}

//
void I2CInit(void)
{
		GPIO_InitTypeDef GPIO_InitStructure = {0};

    GPIO_InitStructure.Pin = GPIO_PIN_7 | GPIO_PIN_6;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStructure.Pull = GPIO_PULLUP;
    GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}

三、EEPROM基础知识

我们用的是AT24C02的EEPROM芯片,它是一个可擦除的只读存储器,他的作用是STM32可以通过IIC总线向里面写入一些数据,这些数据在单片机断电后是不会丢失的,类似于电脑的硬盘,比如机械硬盘内部就是一个一个碟片叠起来的。EEPROM内部则是MOS管的一些存储机构,具体就不展开说了。

如图是IIC总线的电路,上面接了一个AT24C02的芯片。看到单片机的PB6为SCL线,PB7为SDA线,接到EEPROM的通讯管脚5、6号上,而且要通过两个上拉电阻上拉到VDD,也就是通过开漏模式输出。(如果是推挽的话,那么只要IIC总线上挂了两个设备并且一个输出一个输入,就会造成短路)。1、2、3、7管脚都接地是为什么呢?之前讲过,IIC从器件有个7位的器件地址,而1、2、3就是用来确定那个器件地址的,这里E1、E2、E3分别是器件地址的最后三位,如:1、0、1、0、E3、E2、E1、R/W位(R/W=0则写),我们把他们接地或者接VDD就可以确定期间地址了,这里最后三位都是0。地址为1010000,即0xa0。WC#管脚用于写保护,接到地上就可以任意读写,如果接到VDD上就不能写了,这里不怎么用得到,电脑上的BIOS会用到。

芯片有2K(2048位)的存储,即256个字节,可以通讯降噪,有400kHz的通讯速率,可以一次写入8个字节,写入周期为5ms,即两次写入的间隔为5ms。256个字节的分布情况是这样的:它内部有32个pages一个page有8个字节,通过8位的数据就可以进行寻址。(2^8=256=32*8)它每次是按一个page来写的,这就是为什么能一次性写入8个字节。

Page00001020304050607
Page10809101112131415
………………………………………………
Page31248249250251252253254255

 四、程序设计

(1)复制“资源数据包”里的i2c-hal.c和.h文件到“编程工程”。(名字最好改一下,改成i2c.c和i2c.h)。

(2)在main.c调用I2C部分IO初始化代码。(先#include "i2c.h",然后在while(1)前将I2CInit()初始化)​​​​​​。                                         

(3)在i2c.c文件里编程:读写EEPROM函数。(比赛时是要自己编写的)(写完之后记得在.h里声明)

void EEPROM_Write(u8 add,u8 dat)
{
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	
	I2CSendByte(add);
	I2CWaitAck();
	I2CSendByte(dat);
	I2CWaitAck();
	I2CStop();
	HAL_Delay(5);//两次数据之间要有5ms的延时才能正确写入
}

u8 EEPROM_Read(u8 add)
{
	u8 dat;
	
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(add);
	I2CWaitAck();
	
	I2CStart();
	I2CSendByte(0xa1);
	I2CWaitAck();
	dat = I2CReceiveByte();
	I2CSendNotAck();
	I2CStop();
	
	return(dat);
}

根据前面讲的I2C读写的时序很容易就可以写这个代码。需要注意的是,在写函数的最后要加一个5ms的延时,因为在两次数据之间要有一个5ms的延迟才能正确写入,这个在前面也明确说明了。比赛的时候可以对着数据手册写,就会比较容易,当然最好是背下来,这样节约时间。

(4)在main.c调用EEPROM_Write和EEPROM_Read函数,完成EEPROM读写程序。可以用EEPROM来统计设备的开机次数。

先测试一下函数有没有错误:

#include "i2c.h"

u8 val_24c02;

I2C_Init();
EEPROM_Write(0x10,0x55);
val_24c02 = EEPROM_Read(0x10);

它的含义就是在EEPROM的0x10的地址(这个地址随便取,只要在0-255之间就行)写入0x55这个数据(数据也必须是一个Byte的数据),即01010101,然后再读取到val_24c02这个变量中。我们可以利用debug来查看val_24c02是否真的为0x55。

EEPROM很多时候会用于统计一个设备的开机次数

#include "i2c.h"

u8 startup_times;

int main(void)
{

    //......
    I2C_Init();//初始化
    startup_times = EEPROM_Read(0x10);//先读取0X10获得之前的开机次数
    startup_times++;//开机次数加一
    EEPROM_Write(0x10,startup_times);//再写入
    //......
}

总结

这一节主要讲了I2C通讯协议和EEPROM的使用,我们知道了I2C协议的内容,我们可以用I2C总线与EEPROM进行通讯,也了解了底层的驱动代码,可以利用他们来编写EEPROM的读写程序,用于记录单片机的开机次数或者保存一些电压采集的结果等等。

其中EEPROM的读写程序是重点,比赛的时候需要自己根据I2C的读写时序(数据手册上有)来编写,一定不能错。

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值