外设驱动库开发笔记40:AT25xxx外部存储器驱动
??我们在前面开发过AT24CXX系列EEPROM存储器,它使用的是I2C接口。不过有时候我们也会使用SPI接口的EEPROM存储器。在这一篇我们将来讨论AT25XXX系列EEPROM存储器的驱动设计、实现及使用。
1、功能概述
??AT25XXX系列EEPROM存储器采用SPI接口,因其操作简单且性价比高,常用于数据保存。出于开发面向AT25XXX系列EEPROM存储器操作的驱动目标,我们先来了解一下AT25XXX系列EEPROM存储器的基本情况。
1.1、硬件描述
??AT25XXX系列EEPROM存储器拥有从1K到2M的多种容量。AT25XXX系列EEPROM存储器采用SPI通讯接口。尽管容量跨度很大,但都采用相同的封装形式。具体的引脚排布及定义如下:
??为了更好地理解AT25XXX系列EEPROM存储器的操作过程,我们对CS、WP以及HOLD引脚做一个简单说明。
??首先来看一看CS引脚,CS为芯片选择引脚,低电平有效,AT25XXX系列EEPROM存储器被选中。当设备未被选中时,串行数据输入(SI)引脚将不接受数据,串行输出(SO)引脚将保持高阻抗状态。为了确保稳定的操作,CS引脚应在电源启动时跟随VCC。因此建议使用小于或等于10 kΩ的上拉电阻器连接到VCC。在电源启动后,要实现对AT25XXX系列EEPROM存储器的任何操作,都需要先将CS下拉到低电平。
??接下来我们看一看WP引脚,WP引脚为AT25XXX系列EEPROM存储器的写保护引脚。当写保护(WP)引脚保持高电位时,AT25XXX系列EEPROM存储器允许正常的读/写操作。当写保护(WP)引脚为低电平时,WPEN位设置为逻辑“1”时,所有对状态寄存器的写操作都被禁止。但如果内部写周期已经启动,那么写保护(WP)引脚变为低电平对状态寄存器的任何写操作都没有影响。当状态寄存器中的WPEN位设置为逻辑“0”时,写保护(WP)引脚的功能被屏蔽。
??最后我们来看一看HOLD引脚,暂停串行输入(HOLD)引脚低电平有效。暂停串行输入(HOLD)引脚与芯片选择(CS)引脚一起使用来暂停AT25XXX系列EEPROM存储器。当设备被选中,一个串行序列正在进行中,HOLD可以用来暂停与主设备的串行通信,而不需要重新设置串行序列。此时,串行数据输入(SI)引脚的输入将被忽略,而串行数据输出(SO)引脚将处于高阻抗状态。
1.2、通讯接口
??AT25XXX系列EEPROM存储器采用SPI通讯接口。AT25XXX系列EEPROM存储器由主机控制器(通常称为SPI主机)发送的一组指令控制。与AT25XXX系列EEPROM存储器的通信必须由SPI主设备(如微控制器)发起。SPI主设备必须在串行数据时钟(SCK)引脚上为AT25XXX系列EEPROM存储器生成串行时钟。AT25XXX系列EEPROM存储器总是作为一个从属操作,因为SCK总是一个输入。主机与AT25XXX系列EEPROM存储器通讯的拓扑图如下所示:
??SPI主机通过SPI总线与AT25XXX系列EEPROM存储器通信,SPI总线由四条信号线组成:芯片选择(CS)、串行数据时钟(SCK)、串行数据输入(SI)和串行数据输出(SO)。SPI协议定义了总共四种操作模式(模式0、模式1、模式2或模式3),每种模式在SCK极性和相位以及极性和相位如何控制SPI总线上的数据流方面有所不同。AT25XXX系列EEPROM存储器支持两种最常见的模式,SPI模式0和3。
1.3、命令操作
??AT25XXX系列EEPROM存储器被设计成直接与同步串行外围接口(SPI)接口。AT25XXX系列EEPROM存储器使用一个8位指令寄存器。所有的指令、地址和数据首先由高位开始传送,然后由高到低依次进行。指令列表及其操作代码如下:
??从上表我们知道,除了操作存储区域外,还可以操作状态寄存器。AT25XXX系列EEPROM存储器包括一个8位状态寄存器。状态寄存器位调节设备的各种特性。这些位可以通过指令进行更改。具体的结构如下:
??状态寄存器除了反应当前的状态外,实际上有些位还有配置功能。关于致谢为的属性及具体含义如下:
??通过写状态寄存器(WRSR)指令可以配置写保护使能和写保护的区域。块写保护位(BP1、BP0)决定了存储阵列的写保护区域。两位决定了阵列保护的四个级别,分别是:没有一个内存阵列被保护;上四分之一地址范围内存阵列被保护;上半部分地址范围内存阵列被保护;所有的内存阵列都是写保护的,这意味着所有的地址位都是只读的。块写保护级别和相应的状态寄存器控制位关系如下:
??而写保护使能 (WPEN)位用于启用或禁用写保护 (WP) 引脚。当WPEN位设置为逻辑“0”时,写入EEPROM数组的能力取决于块写保护(BP1、BP0)位的值。写入状态寄存器的权限是由WEL位控制的。当WPEN位设置为逻辑“1”时,状态寄存器是只读的。当WP引脚低且WPEN位设置为逻辑“1”时,硬件写保护就启用了。当设备被硬件写保护时,对状态寄存器的写操作,包括块写保护、WEL和WPEN位,以及对块写保护位所选择的内存阵列中的段的写操作被禁用。当启用硬件写保护时,只允许对未受块保护的内存段进行写。当WP引脚为高电平或WPEN位逻辑为“0”时,硬件写保护被禁用。当硬件写保护被禁用时,只允许对未被块保护的内存段进行写。当WPEN位被硬件写保护时,只要WP引脚保持低,它就不能被设置回逻辑“0”。写保护的关系如下所示:
??AT25XXX系列EEPROM存储器拥有从1K到2M的不同容量,寻址范围的不同所需的地址位数也不相同。地址位数根据容量从7位到18位不等,分别对应1到3个字节。具体的容量与地址位关系如下:
??需要注意的是,4K(512x8)容量的AT25XXX系列EEPROM存储器需要9为地址,但在实际操作时只用了1个字节来装载地址,最高位(第9位)地址借用了操作码的第4位来传送。
2、驱动设计与实现
??我们已经了解了AT25XXX存储器的基本功能及读写方式,接下来我们将开发操作AT25XXX系列EEPROM存储器的驱动程序。
2.1、对象定义
??在使用一个对象之前我们需要获得一个对象。同样的我们想要AT25XXX系列EEPROM存储器就需要先定义AT25XXX系列EEPROM存储器的对象。
2.1.1、对象的抽象
??我们要得到AT25XXX系列EEPROM存储器对象,需要先分析其基本特性。一般来说,一个对象至少包含两方面的特性:属性与操作。接下来我们就来从这两个方面思考一下AT25XXX系列EEPROM存储器的对象。
??先来考虑属性,作为属性肯定是用于标识或记录对象特征的东西。我们来考虑AT25XXX系列EEPROM存储器对象属性。首先AT25XXX系列EEPROM存储器有多种型号,不同型号在容量、地址位数等方面都有较大差异。为了区别不同类型的AT25XXX系列EEPROM存储器,我们将类型作为对象的属性。另外每一个AT25XXX对象都有一个状态寄存器,它标识了AT25XXX对象的当前状态,所以我们也将它作为对象的属性。
??接着我们还需要考虑AT25XXX系列EEPROM存储器对象的操作问题。我们总是要对AT25XXX对象进行数据读写,但读写操作使用SPI接口依赖于具体的硬件平台,所以我们将数据读写作为对象的操作。片选信号、写保护以及hold信号均依赖于具体的硬件定义来实现,所以我们将其作为对象的操作。还有用于时序控制的延时,其也要根据具体的软硬件平台来实现,所以我们也将其定义为对象的操作。
??根据上述我们对AT25XXX系列EEPROM存储器的分析,我们可以定义AT25XXX系列EEPROM存储器的对象类型如下:
/*定义AT25XXX对象类型*/
typedef struct At25Object {
uint8_t status; //状态寄存器
At25ModeType mode; //设备类型
void (*Read)(uint8_t *rData,uint16_t rSize); //读数据操作指针
void (*Write)(uint8_t *wData,uint16_t wSize); //写数据操作指针
void (*Delayms)(volatile uint32_t nTime); //毫秒延时操作指针
void (*ChipSelect)(AT25xxxCSType cs); //使用SPI接口时,片选操作
void (*WP)(AT25WPType wp); //写保护操作
void (*Hold)(AT25HoldType hold); //保持信号
}At25ObjectType;
2.1.2、对象初始化
??我们知道,一个对象仅作声明是不能使用的,我们需要先对其进行初始化,所以这里我们来考虑AT25XXX系列EEPROM存储器对象的初始化函数。一般来说,初始化函数需要处理几个方面的问题。一是检查输入参数是否合理;二是为对象的属性赋初值;三是对对象作必要的初始化配置。据此我们设计AT25XXX系列EEPROM存储器对象的初始化函数如下:
/* 初始化AT25XXX对象 */
void At25xxxInitialization(At25ObjectType *at, //AT25XXX对象实体
At25ModeType mode, //AT25XXX对象类型
AT25Read read, //读AT25XXX对象操作指针
AT25Write write, //写AT25XXX对象操作指针
AT25Delayms delayms, //延时操作指针
AT25ChipSelect cs //片选操作函数指针
)
{
if((at==NULL)||(read==NULL)||(write==NULL)||(delayms==NULL))
{
return;
}
at->Read=read;
at->Write=write;
at->Delayms=delayms;
if(cs!=NULL)
{
at->ChipSelect=cs;
}
else
{
at->ChipSelect=AT25ChipSelectDefault;
}
if(mode>=AT25Number)
{
return;
}
at->mode=mode;
if(modememAddLength=AT258BitMemAdd;
}
else if(modememAddLength=AT2516BitMemAdd;
}
else
{
at->memAddLength=AT2524BitMemAdd;
}
ReadStatusForAT25xxx(at);
//写允许
SetWriteEnableLatchForAT25xxx(at);
uint8_t cmd;
//使能写保护,保护全部区域
cmd=at->status|AT25_WPEN|AT25_BPALL;
WriteStatusForAT25xx(at,cmd);
ReadStatusForAT25xxx(at);
}
2.2、对象操作
??我们已经完成了AT25XXX系列EEPROM存储器对象类型的定义和对象初始化函数的设计。但我们的主要目标是获取对象的信息,接下来我们还要实现面向AT25XXX系列EEPROM存储器的各类操作。
2.2.1、读数据操作
??读取AT25XXX系列EEPROM存储器需要先将CS线拉低以选择设备,尔后发送READ(0x03)指令,在后发送要读的寄存器地址。一旦接收完寄存器地址,后续的信号将被忽略。然后返回指定地址的数据。读AT25XXX系列EEPROM存储器数据的操作时序如下:
??根据上述时序图,我们可以编写读AT25XXX系列EEPROM存储器数据的程序如下:
/*从AT25xxx读取数据*/
void ReadDatasFromAT25xxx(At25ObjectType *at,uint32_t regAddress,uint8_t *rData,uint16_t rSize)
{
uint8_t data[4];
uint16_t index=0;
uint8_t temp;
uint16_t size=0;
data[index++]=AT25_READ;
if(at->memAddLength==AT258BitMemAdd)
{
data[index++]=(uint8_t)regAddress;
if(at->mode==AT25040B)
{
temp=(uint8_t)(regAddress>>8);
data[0]|=((temp&0x01)<<3);
}
}
else if(at->memAddLength==AT2516BitMemAdd)
{
data[index++]=(uint8_t)(regAddress>>8);
data[index++]=(uint8_t)regAddress;
}
else
{
data[index++]=(uint8_t)(regAddress>>16);
data[index++]=(uint8_t)(regAddress>>8);
data[index++]=(uint8_t)regAddress;
}
temp=(uint8_t)(regAddress®AddMask[at->mode]);
if((rSize<=pageBytes[at->mode])&&(rSize<=(pageBytes[at->mode]-temp)))
{
size=rSize;
}
else
{
size=pageBytes[at->mode]-temp;
}
at->ChipSelect(AT25CS_Enable);
at->Write(data,index);
at->Delayms(1);
at->Read(rData,size);
at->ChipSelect(AT25CS_Disable);
}
??如果只需要读取一个字节,那么在读取一个字节后CS信号需在读取后恢复高电平。如果想读取多个字节,那么CS信号必须持续低电平,在存储器内部字节地址将自动递增,数据将继续移位。当到达最高地址时,地址计数器将滚动到最低阶地址,从而允许在一个连续的读取循环中读取整个内存,而不管起始地址是什么。
2.2.2、写数据操作
??写AT25XXX系列EEPROM存储器,必须执行两条单独的指令。首先,必须通过写使能(WREN)指令使设备能够写。然后,可以执行写序列。如果设备没有启用写(WREN),设备将忽略写指令。在内部写周期中,除了RDSR指令外,所有命令都将被忽略。写AT25XXX系列EEPROM存储器的时序如下:
??根据上述时序图,我们可以编写写AT25XXX系列EEPROM存储器数据的程序如下:
/*向AT25xxx写入数据*/
void WriteDatasToAT25xxx(At25ObjectType *at,uint16_t regAddress,uint8_t *wData,uint16_t wSize)
{
uint8_t data[wSize+4];
uint16_t index=0;
uint8_t temp;
uint16_t size=0;
data[index++]=AT25_WRITE;
if(at->memAddLength==AT258BitMemAdd)
{
data[index++]=(uint8_t)regAddress;
if(at->mode==AT25040B)
{
temp=(uint8_t)(regAddress>>8);
data[0]|=((temp&0x01)<<3);
}
}
else if(at->memAddLength==AT2516BitMemAdd)
{
data[index++]=(uint8_t)(regAddress>>8);
data[index++]=(uint8_t)regAddress;
}
else
{
data[index++]=(uint8_t)(regAddress>>16);
data[index++]=(uint8_t)(regAddress>>8);
data[index++]=(uint8_t)regAddress;
}
temp=(uint8_t)(regAddress®AddMask[at->mode]);
if((wSize<=pageBytes[at->mode])&&(wSize<=(pageBytes[at->mode]-temp)))
{
size=wSize;
}
else
{
size=pageBytes[at->mode]-temp;
}
for(int i;istatus)&0x02)!=0x02)
{
SetWriteEnableLatchForAT25xxx(at);
}
if(((at->status)&0x0C)!=0x00)
{
WriteStatusForAT25xx(at,at->status|AT25_BPNONE);
}
at->ChipSelect(AT25CS_Enable);
at->Write(data,index);
at->ChipSelect(AT25CS_Disable);
WriteStatusForAT25xx(at,at->status|AT25_BPALL);
}
2.2.3、状态寄存器操作
??前面我们已经详细描述过状态寄存器的格式以及每一位的定义。有一些位是只读的,有一些位是可读写的,记下来我们实现针对状态寄存器中的操作。
(1)、读状态寄存器
??读状态寄存器(RDSR)指令提供对状态寄存器的访问。RDSR指令可以确定设备的状态以及块写保护(BP1, BP0)位表示所使用的内存阵列保护的范围。读取状态寄存器的方法是拉低CS信号,然后发送0x05操作码。操作码完成后,设备将返回8位状态寄存器值。具体时序如下:
??根据上述时序图,我们可以编写读AT25XXX系列EEPROM存储器状态寄存器的程序如下:
/*读AT25xxx状态寄存器*/
void ReadStatusForAT25xxx(At25ObjectType *at)
{
uint8_t opCode=AT25_RDSR;
uint8_t status;
at->ChipSelect(AT25CS_Enable);
at->Write(&opCode,1);
at->Delayms(1);
at->Read(&status,1);
at->ChipSelect(AT25CS_Enable);
at->status=status;
}
(2)、写状态寄存器
??写状态寄存器(WRSR)指令使SPI主机能够更改状态寄存器的选定位。在WRSR指令开始之前,必须执行一条WREN指令,将WEL位设置为逻辑“1”。WREN指令完成后,可以执行WRSR指令。在WRSR指令之后,AT25XXX系列EEPROM存储器将不会响应除RDSR以外的命令,直到自动计时的内部写周期完成。写周期结束后,状态寄存器中的WEL位复位为逻辑“0”。具体的时序图如下:
??根据上述时序图,我们可以编写写AT25XXX系列EEPROM存储器状态寄存器的程序如下:
/*写AT25xxx状态寄存器*/
void WriteStatusForAT25xx(At25ObjectType *at,uint8_t cmd)
{
uint8_t data[2];
data[0]=AT25_WRSR;
data[1]=cmd;
if(((at->status)&0x02)!=0x02)
{
SetWriteEnableLatchForAT25xxx(at);
}
if((((at->status)&AT25_WPEN)!=AT25_WPEN)&&(at->WP!=NULL))
{
at->WP(AT25WP_Disable);
}
at->ChipSelect(AT25CS_Enable);
at->Write(data,2);
at->ChipSelect(AT25CS_Disable);
ReadStatusForAT25xxx(at);
if(at->WP!=NULL)
{
at->WP(AT25WP_Enable);
}
}
??WRSR指令对状态寄存器的第6位、第5位、第4位、第1位和第0位没有影响。只有第7位、第3位和第2位可以通过WRSR指令进行更改。这些可修改的位是写保护使能(WPEN)和块保护(BP1, BP0)位。这三个位元是非易失性位元,具有与常规EEPROM单元相同的特性和功能。当电源从设备中移除时,它们的值被保留。
2.2.4、写操作使能与失能操作
??通过写使能(WREN)指令和写失能(WRDI)指令实现对状态寄存器和EEPROM阵列的写操作的启用和禁用。这些功能改变了状态寄存器中WEL位的状态。
(1)写操作启用
??状态寄存器的写能门闩(WEL)位必须在每个写状态寄存器(WRSR)和写入内存阵列(Write)指令之前设置为逻辑“1”。这是通过向AT25XXX系列EEPROM存储器发送一条WREN(0x06)指令来完成的。首先,CS引脚被拉低以选择设备,然后发送WREN指令。然后CS信号被拉高,并将状态寄存器中的WEL位更新为逻辑“1”。具体的操作时序如下:
??根据上述时序图,我们可以编写AT25XXX系列EEPROM存储器操作启用的程序如下:
/* AT25XXX设置写使能所存器*/
void SetWriteEnableLatchForAT25xxx(At25ObjectType *at)
{
uint8_t opCode=AT25_WREN;
at->ChipSelect(AT25CS_Enable);
at->Write(&opCode,1);
at->ChipSelect(AT25CS_Enable);
ReadStatusForAT25xxx(at);
}
(2)、写操作禁用
??为了防止误写,写禁用(WRDI)指令(0x04)通过将WEL位设置为逻辑“0”来禁用所有编程模式。WRDI指令与WP引脚的状态无关。具体的操作时序如下图所示:
??根据上述时序图,我们可以编写AT25XXX系列EEPROM存储器操作禁用的程序如下:
/* AT25XXX复位写使能所存器*/
void ResetWriteEnableLatchForAT25xxx(At25ObjectType *at)
{
uint8_t opCode=AT25_WRDI;
at->ChipSelect(AT25CS_Enable);
at->Write(&opCode,1);
at->ChipSelect(AT25CS_Enable);
ReadStatusForAT25xxx(at);
}
3、驱动的使用
??在上一节我们设计并实现了AT25XXX系列EEPROM存储器的驱动程序,而这一节我们将设计一个简单的应用来验证这一驱动程序。
3.1、声明并初始化对象
??使用基于对象的操作我们需要先得到这个对象,所以我们先要使用前面定义的AT25XXX系列EEPROM存储器对象类型声明一个AT25XXX系列EEPROM存储器对象变量,具体操作格式如下:
??At25ObjectType at25;
??声明了这个对象变量并不能立即使用,我们还需要使用驱动中定义的初始化函数对这个变量进行初始化。这个初始化函数所需要的输入参数如下:
At25ObjectType *at,AT25XXX对象实体
At25ModeType mode,AT25XXX对象类型
AT25Read read,读AT25XXX对象操作指针
AT25Write write,写AT25XXX对象操作指针
AT25Delayms delayms,延时操作指针
AT25ChipSelect cs,片选操作函数指针
??对于这些参数,对象变量我们已经定义了。而对象类型为枚举,根据实际使用的AT25XXX系列EEPROM存储器来选择就好了。主要的是我们需要定义几个函数,并将函数指针作为参数。这几个函数的类型如下:
/* 定义读数据操作函数指针类型 */
typedef void (*AT25Read)(uint8_t *rData,uint16_t rSize);
/* 定义写数据操作函数指针类型 */
typedef void (*AT25Write)(uint8_t *wData,uint16_t wSize);
/* 定义延时操作函数指针类型 */
typedef void (*AT25Delayms)(volatile uint32_t nTime);
/* 定义使用SPI接口时,片选操作函数指针类型 */
typedef void (*AT25ChipSelect)(AT25xxxCSType cs);
??对于这几个函数我们根据样式定义就可以了,具体的操作可能与使用的硬件平台有关系。片选操作函数用于多设备需要软件操作时,如采用硬件片选可以传入NULL即可。具体函数定义如下:
/*读AT25寄存器值*/
static void ReadDataFromAT25(uint8_t *rData,uint16_t rSize)
{
HAL_SPI_Receive (&at25hspi,rData,rSize,1000);
}
/*写AT25寄存器值*/
static void WriteDataToAT25(uint8_t *wData,uint16_t wSize)
{
HAL_SPI_Transmit (&at25hspi,wData,wSize,1000);
}
/*片选操作*/
void ChipSelectForAT25(AT25xxxCSType cs)
{
if(cs==AT25CS_Enable)
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);
}
else
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);
}
}
??对于延时函数我们可以采用各种方法实现。我们采用的STM32平台和HAL库则可以直接使用HAL_Delay()函数。于是我们可以调用初始化函数如下:
At25xxxInitialization(&at25, //AT25XXX对象实体
AT25M01, //AT25XXX对象类型
ReadDataFromAT25, //读AT25XXX对象操作指针
WriteDataToAT25, //写AT25XXX对象操作指针
HAL_Delay, //延时操作指针
ChipSelectForAT25 //片选操作函数
);
3.2、基于对象进行操作
??我们定义了对象变量并使用初始化函数给其作了初始化。接着我们就来考虑操作这一对象获取我们想要的数据。我们在驱动中已经将获取数据并转换为转换值的比例值,接下来我们使用这一驱动开发我们的应用实例。
/*AT25XXX数据读写操作*/
void AT25ReadWriteData(void)
{
uint16_t regAddress=0x02;
uint8_t readByte;
uint8_t writeByte=0x0A;
uint8_t rData[2];
uint16_t rSize=2;
uint8_t wData[]={0x5A,0xA5};
uint16_t wSize=2;
/*从AT25XXX读取单个字节,从随机地址读取*/
readByte=ReadByteFromAT25xxx(&at25,regAddress);
/*向AT25XXX写入单个字节*/
WriteByteToAT25xxx(&at25,regAddress,writeByte);
/*从AT25XXX读取多个字节,从指定地址最多到所在页的结尾*/
ReadBytesFromAT25xxx(&at25,regAddress,rData,rSize);
/*向AT25XXX写入多个字节,从指定地址最多到所在页的结尾*/
WriteBytesToAT25xxx(&at25,regAddress,wData,wSize);
}
4、应用总结
??在本片中我们讨论并设计了AT25XXX系列EEPROM存储器的驱动程序,并据此设计了一个简单的验证应用。无论是写数据还是读数据均可顺利执行,说明我们的驱动设计是正确的。
??需要注意的是,4K(512x8)容量的AT25XXX系列EEPROM存储器需要9为地址,但在实际操作时只用了1个字节来装载地址,最高位(第9位)地址借用了操作码的第4位来传送。
??在使用驱动时需注意,采用SPI接口的器件需要考虑片选操作的问题。如果片选信号是通过硬件电路来实现的,我们在初始化时给其传递NULL值。如果是软件操作片选则传递我们编写的片选操作函数。
??在这一驱动设计的过程中我们并未验证读写数据的正确性,事实上如果是比较重要的数据我们可以为其添加验证,如CRC验证等。