【ALIENTEK 战舰STM32开发板例程系列连载+教学】第二十八章 SPI实验

2019-08-19 20:42发布

第二十八章 SPI 实验

本章我们将向大家介绍STM32SPI功能。在本章中,我们将使用STM32自带的SPI来实现对外部FLASHW25Q64)的读写,并将结果显示在TFTLCD模块上。本章分为如下几个部分: 28.1 SPI 简介 28.2 硬件设计 28.3 软件设计 28.4 下载验证 

28.1 SPI 简介

SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROMFLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,STM32也有SPI接口。 SPI接口一般使用4条线通信: MISO 主设备数据输入,从设备数据输出。 MOSI 主设备数据输出,从设备数据输入。 SCLK时钟信号,由主设备产生。 CS从设备片选信号,由主设备控制。 SPI主要特点有:可以同时发出和接收串行数据;可以当作主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。 SPI总线四种工作方式 SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。如果 CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI主模块和与之通信的外设备时钟相位和极性应该一致。 不同时钟相位下的总线数据传输时序如图28.1.1所示:

28.1.1 不同时钟相位下的总线传输时序(CPHA=0/1 STM32SPI功能很强大,SPI时钟最多可以到18Mhz,支持DMA,可以配置为SPI协议或者I2S协议(仅大容量型号支持,战舰STM32开发板是支持的)。 本章,我们将使用STM32SPI来读取外部SPI FLASH芯片(W25Q64),实现类似上节的功能。这里对SPI我们只简单介绍一下SPI的使用,STM32SPI详细介绍请参考《STM32参考手册》第457页,23节。然后我们再介绍下SPI FLASH芯片。 这节,我们使用STM32SPI2的主模式,下面就来看看SPI2部分的设置步骤吧,STM32的主模式配置步骤如下: 1)配置相关引脚的复用功能,使能SPI2时钟。 我们要用SPI2,第一步就要使能SPI2的时钟,SPI2的时钟通过APB1ENR的第14位来设置。其次要设置SPI2的相关引脚为复用输出,这样才会连接到SPI2上否则这些IO口还是默认的状态,也就是标准输入输出口。这里我们使用的是PB1314153个(SCK.MISOMOSICS使用软件管理方式),所以设置这三个为复用IO 2)设置SPI2工作模式。 这一步全部是通过SPI2_CR1来设置,我们设置SPI2为主机模式,设置数据格式为8位,然后通过CPOLCPHA位来设置SCK时钟极性及采样方式。并设置SPI2的时钟频率(最大18Mhz),以及数据的格式(MSB在前还是LSB在前)。 3)使能SPI2 这一步通过SPI2_CR1bit6来设置,以启动SPI2,在启动之后,我们就可以开始SPI通讯了。 SPI2的使用就介绍到这里,接下来介绍一下W25Q64W25Q64是华邦公司推出的大容量SPI FLASH产品,W25Q64的容量为64Mb,该系列还有W25Q80/16/32等。ALIENTEK所选择的W25Q64容量为64Mb,也就是8M字节。 W25Q648M的容量分为128个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每个扇区4K个字节。W25Q64的最少擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Q64开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。 W25Q64的擦写周期多达10W次,具有20年的数据保存期限,支持电压为2.7~3.6VW25Q64支持标准的SPI,还支持双输出/四输出的SPI,最大SPI时钟可以到80Mhz(双输出时相当于160Mhz,四输出时相当于320M),更多的W25Q64的介绍,请参考W25Q64DATASHEET

28.2 硬件设计

本章实验功能简介:开机的时候先检测W25Q64是否存在,然后在主循环里面检测两个按键,其中1个按键(WK_UP)用来执行写入W25Q64的操作,另外一个按键(KEY1)用来执行读出操作,在TFTLCD模块上显示相关信息。同时用DS0提示程序正在运行。 所要用到的硬件资源如下: 1)  指示灯DS0 2)  WK_UPKEY1按键 3) TFTLCD模块 4) SPI 5) W25Q64 这里只介绍W25Q64STM32的连接,板上的W25Q64是直接连在STM32SPI2上的,连接关系如图28.2.1所示:

28.2.1 STM32W25Q64连接电路图

28.3 软件设计

打开上一章的工程,首先在HARDWARE文件夹下新建一个FLASH的文件夹和SPI的文件夹。然后新建一个flash.cflash.h的文件保存在FLASH文件夹下,新建spi.cspi.h的文件,保存在SPI文件夹下,并将这两个文件夹加入头文件包含路径。 打开spi.c文件,输入如下代码: #include "spi.h" //SPI口初始化 //这里针是对SPI2的初始化 void SPI2_Init(void) {            RCC->APB2ENR|=1<<3;           //PORTB时钟使能                RCC->APB1ENR|=1<<14;        //SPI2时钟使能        //这里只针对SPI口初始化        GPIOB->CRH&=0X000FFFFF;        GPIOB->CRH|=0XBBB00000;     //PB13/14/15复用                    GPIOB->ODR|=0X7<<13;         //PB13/14/15上拉        SPI2->CR1|=0<<10;                    //全双工模式         SPI2->CR1|=1<<9;                     //软件nss管理        SPI2->CR1|=1<<8;           SPI2->CR1|=1<<2;                     //SPI主机        SPI2->CR1|=0<<11;                    //8bit数据格式             SPI2->CR1|=1<<1;                     //空闲模式下SCK1 CPOL=1        SPI2->CR1|=1<<0;                     //数据采样从第二个时间边沿开始,CPHA=1         //SPI2属于APB1的外设.时钟频率最大为36M.        SPI2->CR1|=3<<3;                     //Fsck=Fpclk1/256        SPI2->CR1|=0<<7;                     //MSBfirst          SPI2->CR1|=1<<6;                     //SPI设备使能        SPI2_ReadWriteByte(0xff);//启动传输        }   //SPI2速度设置函数 //SpeedSet:0~7 //SPI速度=fAPB1/2^(SpeedSet+1) //APB1时钟一般为36Mhz void SPI2_SetSpeed(u8 SpeedSet) {        SpeedSet&=0X07;                //限制范围        SPI2->CR1&=0XFFC7;        SPI2->CR1|=SpeedSet<<3;   //设置SPI2速度         SPI2->CR1|=1<<6;              //SPI设备使能        } //SPI2 读写一个字节 //TxData:要写入的字节 //返回值:读取到的字节 u8 SPI2_ReadWriteByte(u8 TxData) {                   u16 retry=0;                               while((SPI2->SR&1<<1)==0)             //等待发送区空            {               retry++;               if(retry>=0XFFFE)return 0;        //超时退出        }                            SPI2->DR=TxData;                         //发送一个byte        retry=0;        while((SPI2->SR&1<<0)==0)            //等待接收完一个byte         {               retry++;               if(retry>=0XFFFE)return 0;  //超时退出        }                                                          return SPI2->DR;                       //返回收到的数据                           } 此部分代码主要初始化SPI,这里我们选择的是SPI2,所以在SPI2_Init函数里面,其相关的操作都是针对SPI2的,其初始化步骤和我们上面介绍的一样。在初始化之后,我们就可以开始使用SPI2了,在SPI2_Init函数里面,把SPI2的频率设置成了最低(36Mhz256分频)。在外部函数里面,我们通过SPI2_SetSpeed来设置SPI2的速度,而我们的数据发送和接收则是通过SPI2_ReadWriteByte函数来实现的。 保存spi.c,并把该文件加入HARDWARE组下面,然后我们打开spi.h在里面输入如下代码: #ifndef __SPI_H #define __SPI_H #include "sys.h" // SPI总线速度设置 #define SPI_SPEED_2              0 #define SPI_SPEED_4              1 #define SPI_SPEED_8              2 #define SPI_SPEED_16             3 #define SPI_SPEED_32              4 #define SPI_SPEED_64              5 #define SPI_SPEED_128            6 #define SPI_SPEED_256            7 void SPI2_Init(void);                       //初始化SPI2 void SPI2_SetSpeed(u8 SpeedSet);     //设置SPI2速度   u8 SPI2_ReadWriteByte(u8 TxData);   //SPI2总线读写一个字节 #endif 此部分代码我们就不多介绍了,保存spi.h,然后我们打开flash.c,在里面编写与W25Q64操作相关的代码,由于篇幅所限,详细代码,这里就不贴出了。我们仅介绍几个重要的函数,首先是SPI_Flash_Read函数,该函数用于从W25Q64的指定地址读出指定长度的数据。其代码如下:            //读取SPI FLASH  //在指定地址开始读取指定长度的数据 //pBuffer:数据存储区 //ReadAddr:开始读取的地址(24bit) //NumByteToRead:要读取的字节数(最大65535) void SPI_Flash_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)   {       u16 i;                                                                               SPI_FLASH_CS=0;                       //使能器件        SPI2_ReadWriteByte(W25X_ReadData);        //发送读取命令       SPI2_ReadWriteByte((u8)((ReadAddr)>>16));         //发送24bit地址        SPI2_ReadWriteByte((u8)((ReadAddr)>>8));       SPI2_ReadWriteByte((u8)ReadAddr);       for(i=0;i<NumByteToRead;i++)        {         pBuffer=SPI2_ReadWriteByte(0XFF);   //循环读数      }        SPI_FLASH_CS=1;                                            } 由于W25Q64支持以任意地址(但是不能超过W25Q64的地址范围)开始读取数据,所以,这个代码相对来说就比较简单了,在发送24位地址之后,程序就可以开始循环读数据了,其地址会自动增加的,不过要注意,不能读的数据超过了W25Q64的地址范围哦!否则读出来的数据,就不是你想要的数据了。 有读的函数,当然就有写的函数了,接下来,我们介绍SPI_Flash_Write这个函数,该函数的作用与SPI_Flash_Read的作用类似,不过是用来写数据到W25Q64里面的,其代码如下: //SPI FLASH  //在指定地址开始写入指定长度的数据 //该函数带擦除操作! //pBuffer:数据存储区 //WriteAddr:开始写入的地址(24bit)                                       //NumByteToWrite:要写入的字节数(最大65535)   u8 SPI_FLASH_BUFFER[4096];        void SPI_Flash_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   {        u32 secpos;        u16 secoff;        u16 secremain;                u16 i;           u8 * SPI_FLASH_BUF;              SPI_FLASH_BUF=SPI_FLASH_BUFFER;            secpos=WriteAddr/4096;//扇区地址 0~511 for w25x16        secoff=WriteAddr%4096;//在扇区内的偏移        secremain=4096-secoff;//扇区剩余空间大小         //printf("ad:%X,nb:%X ",WriteAddr,NumByteToWrite);//测试用       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;                            }                      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;                      //下一个扇区可以写完了               }            };    } 该函数可以在W25Q64的任意地址开始写入任意长度(必须不超过W25Q64的容量)的数据。我们这里简单介绍一下思路:先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。 其他的代码就比较简单了,我们这里不介绍了。保存falsh.c,然后加入到HARDWARE组下面,再打开flahs.h,在该文件里面输入如下代码: #ifndef __FLASH_H #define __FLASH_H                       #include "sys.h" //W25X系列/Q系列芯片列表        //W25Q80 ID  0XEF13 //W25Q16 ID  0XEF14 //W25Q32 ID  0XEF15 //W25Q32 ID  0XEF16       #define W25Q80   0XEF13        #define W25Q16   0XEF14 #define W25Q32   0XEF15 #define W25Q64   0XEF16 extern u16 SPI_FLASH_TYPE;           //定义我们使用的flash芯片型号                 #define    SPI_FLASH_CS PBout(12)  //选中FLASH      //指令表 #define W25X_WriteEnable                0x06 #define W25X_WriteDisable               0x04 #define W25X_ReadStatusReg             0x05 #define W25X_WriteStatusReg            0x01 #define W25X_ReadData                    0x03 #define W25X_FastReadData              0x0B #define W25X_FastReadDual              0x3B #define W25X_PageProgram               0x02 #define W25X_BlockErase                  0xD8 #define W25X_SectorErase                 0x20 #define W25X_ChipErase                   0xC7 #define W25X_PowerDown                0xB9 #define W25X_ReleasePowerDown      0xAB #define W25X_DeviceID                    0xAB #define W25X_ManufactDeviceID       0x90 #define W25X_JedecDeviceID            0x9F void SPI_Flash_Init(void); u16  SPI_Flash_ReadID(void);                           //读取FLASH ID u8    SPI_Flash_ReadSR(void);                  //读取状态寄存器 void SPI_FLASH_Write_SR(u8 sr);                 //写状态寄存器 void SPI_FLASH_Write_Enable(void);             //写使能 void SPI_FLASH_Write_Disable(void);             //写保护 void SPI_Flash_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite); void SPI_Flash_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead);      //读取flash void SPI_Flash_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);      //写入flash void SPI_Flash_Erase_Chip(void);                 //整片擦除 void SPI_Flash_Erase_Sector(u32 Dst_Addr);     //扇区擦除 void SPI_Flash_Wait_Busy(void);              //等待空闲 void SPI_Flash_PowerDown(void);              //进入掉电模式 void SPI_Flash_WAKEUP(void);                     //唤醒 #endif 这里面就定义了一些与W25Q64操作相关的命令(部分省略了),这些命令在W25Q64的数据手册上都有详细的介绍,感兴趣的读者可以参考该数据手册,其他的就没啥好说的了。保存此部分代码。最后,我们在test.c里面,修改main函数如下: //要写入到W25Q64的字符串数组 const u8 TEXT_Buffer[]={"WarShipSTM32 SPI TEST"}; #define SIZE sizeof(TEXT_Buffer)      int main(void) {                   u8 key;        u16 i=0;        u8 datatemp[SIZE];        u32 FLASH_SIZE;       Stm32_Clock_Init(9);    //系统时钟设置        uart_init(72,9600);      //串口初始化为9600        delay_init(72);                  //延时初始化        LED_Init();                 //初始化与LED连接的硬件接口        LCD_Init();                  //初始化LCD        usmart_dev.init(72);      //初始化USMART               KEY_Init();                  //按键初始化                     SPI_Flash_Init();                //SPI FLASH 初始化             POINT_COLOR=RED;//设置字体为红 {MOD}        LCD_ShowString(60,50,200,16,16,"WarShip STM32");           LCD_ShowString(60,70,200,16,16,"SPI TEST");              LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK");        LCD_ShowString(60,110,200,16,16,"2012/9/9");              LCD_ShowString(60,130,200,16,16,"WKUP:Write  KEY1:Read"); //显示提示信息                   while(SPI_Flash_ReadID()!=W25Q64)                                                 //检测不到W25Q64        {               LCD_ShowString(60,150,200,16,16,"25Q64 Check Failed!");               delay_ms(500);               LCD_ShowString(60,150,200,16,16,"Please Check!      ");               delay_ms(500);               LED0=!LED0;//DS0闪烁        }        LCD_ShowString(60,150,200,16,16,"25Q64 Ready!");        FLASH_SIZE=8*1024*1024;       //FLASH 大小为8M字节       POINT_COLOR=BLUE;              //设置字体为蓝 {MOD}           while(1)        {               key=KEY_Scan(0);               if(key==KEY_UP)//KEY_UP按下,写入W25Q64               {                      LCD_Fill(0,170,239,319,WHITE);//清除半屏                        LCD_ShowString(60,170,200,16,16,"Start Write W25Q64....");                      SPI_Flash_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE);              //从倒数第100个地址处开始,写入SIZE长度的数据                      LCD_ShowString(60,170,200,16,16,"W25Q64 Write Finished!");//提示传送完成               }               if(key==KEY_DOWN)//KEY_DOWN按下,读取字符串并显示               {                     LCD_ShowString(60,170,200,16,16,"Start Read W25Q64.... ");                      SPI_Flash_Read(datatemp,FLASH_SIZE-100,SIZE);                      //从倒数第100个地址处开始,读出SIZE个字节                      LCD_ShowString(60,170,200,16,16,"The Data Readed Is:  ");//提示传送完成                      LCD_ShowString(60,190,200,16,16,datatemp);//显示读到的字符串               }               i++;               delay_ms(10);               if(i==20)               {                      LED0=!LED0;//提示系统正在运行                          i=0;               }                      } } 这部分代码和IIC实验那部分代码大同小异,我们就不多说了,实现的功能就和IIC差不多,不过此次写入和读出的是SPI FLASH,而不是EEPROM

28.4 下载验证

在代码编译成功之后,我们通过下载代码到ALIENTEK战舰STM32开发板上,通过先按WK_UP按键写入数据,然后按KEY1读取数据,得到如图28.4.1所示:

28.4.1 SPI实验程序运行效果图 伴随DS0的不停闪烁,提示程序在运行。程序在开机的时候会检测W25Q64是否存在,如果不存在则会在TFTLCD模块上显示错误信息,同时DS0慢闪。大家可以通过跳线帽把PB12PB13短接就可以看到报错了。  
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
26条回答
southernsky
2019-08-21 15:22
原子站长,在函数u8 SPI2_ReadWriteByte(u8 TxData)中,while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) 这句话是用来接收从flash发送的数据的吗?如果是的话,似乎是先发送一个数据,接着又接收数据,因为全双工的原因?
u8 SPI2_ReadWriteByte(u8 TxData)
{
u8 retry=0;  
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //
{
retry++;
if(retry>200)return 0;
}   
SPI_I2S_SendData(SPI2, TxData); //
retry=0;

while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //
{
retry++;
if(retry>200)return 0;
}        
return SPI_I2S_ReceiveData(SPI2); //     
}

一周热门 更多>