嵌入式开发学习笔记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有个特点就是每个设备有一个特定的器件地址。不同的设备 它的器件地址是不一样的,就可以区分总线上的多个设备了。
主设备访问从设备的一般过程为:
假设主设备要发数据:
- 主器件发送START条件并寻址从器件。
- 主器件发数据。
- 主器件以STOP条件终止传输。
假设主设备要接收数据:
- 主器件发送START条件并寻址从器件。
- 主器件发送请求读取的寄存器的地址。
- 主器件从发送器接收数据。
- 主器件以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的读写时序(数据手册上有)来编写,一定不能错。
作者:我不是板神