STM32SPI flash读写测试实验报告
一、实验目的
1.学习SPI的基本工作原理
2.通过实验加深对STM32SPI的理解
3.利用STM32的SPI11和SPI接口的flash芯片进行通信,读写测试,并将测试结果用串口打印出来
二、实验原理
1.SPI基础知识
SPI特征
●3线全双工同步传输
●带或不带第三根双向数据线的双线单工同步传输
●8或16位传输帧格式选择
●主或从操作
●支持多主模式
●8个主模式波特率预分频系数(最大为fPCLK/2)
●从模式频率(最大为fPCLK/2)
●主模式和从模式的快速通信
●主模式和从模式下均可以由软件或硬件进行NSS管理:主/从操作模式的动态改变
●可编程的时钟极性和相位
●可编程的数据顺序,MSB在前或LSB在前
●可触发中断的专用发送和接收标志
●SPI总线忙状态标志
●支持可靠通信的硬件CRC
─在发送模式下,CRC值可以被作为最后一个字节发送
─在全双工模式中对接收到的最后一个字节自动进行CRC校验
●可触发中断的主模式故障、过载以及CRC错误标志
●支持DMA功能的1字节发送和接收缓冲器:产生发送和接受请求
从选择(NSS)脚管理
有2种NSS模式:
图211●软件NSS模式:可以通过设置SPI_CR1寄存器的SSM位来使能这种模式(见)。在这种
模式下NSS引脚可以用作它用,而内部NSS信号电平可以通过写SPI_CR1的SSI位来驱动
●硬件NSS模式,分两种情况:
─NSS输出被使能:当STM32F10xxx工作为主SPI,并且NSS输出已经通过SPI_CR2寄存
器的SSOE位使能,这时NSS引脚被拉低,所有NSS引脚与这个主SPI的NSS引脚相连并
配置为硬件NSS的SPI设备,将自动变成从SPI设备。
当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它的设备它是主
设备;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个
硬件失败错误(Hard Fault)。
─NSS输出被关闭:允许操作于多主环境。
时钟信号的相位和极性可以组合成四种不同的模式,下面为其中来两种模式
数据帧格式
根据SPI_CR1寄存器中的LSBFIRST位,输出数据位时可以MSB在先也可以LSB在先。
根据SPI_CR1寄存器的DFF位,每个数据帧可以是8位或是16位。所选择的数据帧格式对发送和或接收都有效。
配置SPI为主模式
在主配置时,在SCK脚产生串行时钟。
配置步骤
1.通过SPI_CR1寄存器的BR[2:0]位定义串行时钟波特率。
2.选择CPOL和CPHA位,定义数据传输和串行时钟间的相位关系(见图212)。
3.设置DFF位来定义8位或16位数据帧格式。
4.配置SPI_CR1寄存器的LSBFIRST位定义帧格式。
5.如果需要NSS引脚工作在输入模式,硬件模式下,在整个数据帧传输期间应把NSS脚连接
到高电平;在软件模式下,需设置SPI_CR1寄存器的SSM位和SSI位。如果NSS引脚工作
在输出模式,则只需设置SSOE位。
6.必须设置MSTR位和SPE位(只当NSS脚被连到高电平,这些位才能保持置位)。
在这个配置中,MOSI引脚是数据输出,而MISO引脚是数据输入。
SPI模块能够以两种配置工作于单工方式:
●1条时钟线和1条双向数据线;
●1条时钟线和1条数据线(只接收或只发送);
SPI通信可以通过以下步骤使用CRC:
●设置CPOL、CPHA、LSBFirst、BR、SSM、SSI和MSTR的值;
●在SPI_CRCPR寄存器输入多项式;
●通过设置SPI_CR1寄存器CRCEN位使能CRC计算,该操作也会清除寄存器SPI_RXCRCR
和SPI_TXCRC;
●设置SPI_CR1寄存器的SPE位启动SPI功能;
●启动通信并且维持通信,直到只剩最后一个字节或者半字;
●在把最后一个字节或半字写进发送缓冲器时,设置SPI_CR1的CRCNext位,指示硬件在发
送完成最后一个数据之后,发送CRC的数值。在发送CRC数值期间,停止CRC计算;
●当最后一个字节或半字被发送后,SPI发送CRC数值,CRCNext位被清除。同样,接收到
的CRC与SPI_RXCRCR值进行比较,如果比较不相配,则设置SPI_SR上的CRCERR标志
位,当设置了SPI_CR2寄存器的ERRIE时,则产生中断。
个人理解:CRC校验,计算发送端是CRC值,接收端计算CRC值,然后发送端将计算的CRC值发送出去,接收端接收到之后与接收端计算的值相比较,若不同则返回错误标志
利用DMA的SPI通信
为了达到最大通信速度,需要及时往SPI发送缓冲器填数据,同样接收缓冲器中的数据也必须及
时读走以防止溢出。为了方便高速率的数据传输,SPI实现了一种采用简单的请求/应答的DMA
机制。
当SPI_CR2寄存器上的对应使能位被设置时,SPI模块可以发出DMA传输请求。发送缓冲器和
接收缓冲器亦有各自的DMA请求(见)。
●发送时,在每次TXE被设置为’1’时发出DMA请求,DMA控制器则写数据至SPI_DR寄存
器,TXE标志因此而被清除。
●接收时,在每次RXNE被设置为’1’时发出DMA请求,DMA控制器则从SPI_DR寄存器读出数
据,RXNE标志因此而被清除。
当只使用SPI发送数据时,只需使能SPI的发送DMA通道。此时,因为没有读取收到的数据,
OVR被置为’1’(译注:软件不必理会这个标志)。
当只使用SPI接收数据时,只需使能SPI的接收DMA通道
SPI寄存器
SPI控制寄存器1 SPI_CR1
(其中每个位的作用参阅数据手册486页)
SPI控制寄存器2(SPI_CR2)
SPI状态寄存器(SPI_SR)
SPI数据寄存器(SPI_DR)
SPI CRC多项式寄存器
SPI Rx CRC寄存器(SPI_RXCRCR)
在启用CRC计算时,RXCRC[15:0]中包含了依据收到的字节计算的CRC数值。当在SPI_CR1
的CRCEN位写入’1’时,该寄存器被复位。CRC计算使用SPI_CRCPR中的多项式。
当数据帧格式被设置为8位时,仅低8位参与计算,并且按照CRC8的方法进行;当数据帧格式
为16位时,寄存器中的所有16位都参与计算,并且按照CRC16的标准。
SPI Tx CRC寄存器(SPI_TXCRCR)
在启用CRC计算时,TXCRC[15:0]中包含了依据将要发送的字节计算的CRC数值。当在
SPI_CR1中的CRCEN位写入’1’时,该寄存器被复位。CRC计算使用SPI_CRCPR中的多项
式。
当数据帧格式被设置为8位时,仅低8位参与计算,并且按照CRC8的方法进行;当数据帧格式
为16位时,寄存器中的所有16个位都参与计算,并且按照CRC16的标准
2.实验电路图
W25X16有8192个可编程页,每页256字节。用“页编程指令”每次可以编程256个字节。用“扇区(sector)指令”每次可以擦除16页,用“块(block)擦除指令”每次可以擦除256页,用“整片擦除指令”即可擦除整个芯片。W25X16有512个可擦除扇区或32个可擦除块。
W25X16支持标准的SPI接口,传输速率最大75MHz。
双输出SPI方式
W25X16支持SPI双输出方式,需要使用“快读双输出指令0x3B”,这时,传输速率相当于两倍的标准SPI速率。这个命令非常适合于在需要一上电就快速下载代码到内存中的情况或者需要缓存代码段到内存中运行的情况下。在使用“快速双输出指令”后,DIO引脚变为输出引脚。
指令:写使能(Write Enable),页编程(Page Program),扇区擦除(Sector Erase),块区擦除(Block Erase),芯片擦除(Chip Erase),写状态寄存器指令(Write Status Register)
三、实验内容
软件设计
配置STM32的SPI为主模式来读取SPIflash实现读写功能。
下面对SPI1部分进行配置。
在主模式时,SCK脚输出串行时钟。
配置步骤:
1)配置SPI串行时钟比特率
2)定义数据传输和串行间的相位关系
3)设置8位或16位数据帧格式
4)如果需要NSS引脚工作在输入模式,硬件模式下,在整个数据帧传输期间应该把NSS脚连接到高电平,在软件模式下,需要设置SPI1_CR1的SSM位和SSI位。如果NSS引脚工作在输出模式,则需要设置SSOE位。
5)必须设置MSTR和SPE位(只有当NSS脚被连接到高电平,这些为才能保持置位)
//串行外设接口SPI的初始化,SPI配置成主模式
//本例程选用SPI1对W25X16进行读写操作,对SPI1进行初始化
void SPIx_Init(void)
{
SPI_InitTypeDefSPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
/* Enable SPI1 and GPIOA clocks */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOC|RCC_APB2Periph_SPI1|RCC_APB2Periph_AFIO, ENABLE);
/* Configure SPI1 pins: NSS, SCK, MISO and MOSI */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//SPI1 NSS
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_SetBits(GPIOC, GPIO_Pin_4);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_4);
/* SPI1 configuration */
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //SPI1设置为两线全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI1为主模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //SPI发送接收8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行时钟在不操作时,时钟为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //第二个时钟沿开始采样数据
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由软件(使用SSI位)管理
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; //定义波特率预分频的值:波特率预分频值为8
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式
SPI_Init(SPI1, &SPI_InitStructure);
/* Enable SPI1*/
SPI_Cmd(SPI1, ENABLE); //使能SPI1外设
}
SPI接口读写Flash一个字节
//SPIx读写一个字节
//返回值:读取到的字节
u8 SPIx_ReadWriteByte(u8 TxData)
{
u8 retry=0;
while((SPI1->SR&1<<1)==0)//等待发送区空
{
retry++;
if(retry>200)return 0;
}
SPI1->DR=TxData; //发送一个byte
retry=0;
while((SPI1->SR&1<<0)==0) //等待接收完一个byte
{
retry++;
if(retry>200)return 0;
}
return SPI1->DR; //返回收到的数据
}
获取SPI flash ID
//读取芯片ID W25X16的ID:0XEF14
u16 SPI_Flash_ReadID(void)
{
u16 Temp = 0;
SPI_FLASH_CS=0;
SPIx_ReadWriteByte(0x90);//发送读取ID命令
SPIx_ReadWriteByte(0x00);
SPIx_ReadWriteByte(0x00);
SPIx_ReadWriteByte(0x00);
Temp|=SPIx_ReadWriteByte(0xFF)<<8;
Temp|=SPIx_ReadWriteByte(0xFF);
SPI_FLASH_CS=1;
return Temp;
}
在指定位置开始读取指定长度的数据
//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大65535)
void SPI_Flash_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)
{
u16 i;
SPI_FLASH_CS=0; //使能器件
SPIx_ReadWriteByte(W25X_ReadData); //发送读取命令
SPIx_ReadWriteByte((u8)((ReadAddr)>>16));//发送24bit地址
SPIx_ReadWriteByte((u8)((ReadAddr)>>8));
SPIx_ReadWriteByte((u8)ReadAddr);
for(i=0;i<NumByteToRead;i++)
{
pBuffer[i]=SPIx_ReadWriteByte(0XFF); //循环读数
}
SPI_FLASH_CS=1; //取消片选
}
在指定位置写入指定长度的数据
//写SPI FLASH
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
u8 SPI_FLASH_BUF[4096];
void SPI_Flash_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u32 secpos;
u16 secoff;
u16 secremain;
u16 i;
secpos=WriteAddr/4096;//扇区地址0~511 for w25x16
secoff=WriteAddr%4096;//在扇区内的偏移
secremain=4096-secoff;//扇区剩余空间大小
if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096个字节
while(1)
{
SPI_Flash_Read(SPI_FLASH_BUF,secpos*4096,4096);//读出整个扇区的内容
for(i=0;i<secremain;i++)//校验数据
{
if(SPI_FLASH_BUF[secoff+i]!=0XFF)break;//需要擦除
}
if(i<secremain)//需要擦除
{
SPI_Flash_Erase_Sector(secpos);//擦除这个扇区
for(i=0;i<secremain;i++) //复制
{
SPI_FLASH_BUF[i+secoff]=pBuffer[i];
}
SPI_Flash_Write_NoCheck(SPI_FLASH_BUF,secpos*4096,4096);//写入整个扇区
}else SPI_Flash_Write_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间.
if(NumByteToWrite==secremain)break;//写入结束了
else//写入未结束
{
secpos++;//扇区地址增1
secoff=0;//偏移位置为0
pBuffer+=secremain;//指针偏移
WriteAddr+=secremain;//写地址偏移
NumByteToWrite-=secremain; //字节数递减
if(NumByteToWrite>4096)secremain=4096; //下一个扇区还是写不完
else secremain=NumByteToWrite; //下一个扇区可以写完了
}
};
}
在main函数中,先向Flash中写入指定长度的数据,然后在读出并打印到串口,比较读出和写入的数据,并将结果显示到串口
int main(void)
{
u8 i=0;
u8 datatemp[SIZE];
GPIO_Configuration();
USART_Configuration();
GPIO_SetBits(GPIOF, GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9);
SPI_Flash_Init();
while(SPI_Flash_ReadID()!=FLASH_ID)//检测不到W25X16
{
i=SPI_Flash_ReadID();
printf("\n\r ID:%d",i);
printf("\n\r没有读到正确的W25X16芯片ID,请检查硬件连接");
Delay(0xaFFFF);
Delay(0xaFFFF);
GPIO_ResetBits(GPIOF,GPIO_Pin_7);
GPIO_SetBits(GPIOF,GPIO_Pin_7);
}
printf("\n\r开始写入W25X16 SPI FLASH芯片....");
SPI_Flash_Write((u8*)TEXT_Buffer,1000,SIZE);//从1000字节处开始,写入SIZE长度的数据
printf("\n\r写入完成!");//提示传送完成
printf("\n\r");