嵌入式开发学习笔记09:STM32G431的EEPROM编程技巧

   系列文章目录

嵌入式|蓝桥杯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个字节。

Page0 00 01 02 03 04 05 06 07
Page1 08 09 10 11 12 13 14 15
…… …… …… …… …… …… …… …… ……
Page31 248 249 250 251 252 253 254 255

 四、程序设计

(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的读写时序(数据手册上有)来编写,一定不能错。

作者:我不是板神

物联沃分享整理
物联沃-IOTWORD物联网 » 嵌入式开发学习笔记09:STM32G431的EEPROM编程技巧

发表评论