原创 【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第二十八章 SPI实验

2013-3-16 21:26 3901 15 18 分类: MCU/ 嵌入式 文集: STM32学习

第二十八章 SPI 实验

本章我们将向大家介绍STM32的SPI功能。在本章中,我们将使用STM32自带的SPI来实现对外部FLASH(W25Q64)的读写,并将结果显示在TFTLCD模块上。本章分为如下几个部分:

28.1 SPI 简介

28.2 硬件设计

28.3 软件设计

28.4 下载验证

28.1 SPI 简介

SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,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)

STM32的SPI功能很强大,SPI时钟最多可以到18Mhz,支持DMA,可以配置为SPI协议或者I2S协议(仅大容量型号支持,战舰STM32开发板是支持的)。

本章,我们将使用STM32的SPI来读取外部SPI FLASH芯片(W25Q64),实现类似上节的功能。这里对SPI我们只简单介绍一下SPI的使用,STM32的SPI详细介绍请参考《STM32参考手册》第457页,23节。然后我们再介绍下SPI FLASH芯片。

这节,我们使用STM32的SPI2的主模式,下面就来看看SPI2部分的设置步骤吧,STM32的主模式配置步骤如下:

1)配置相关引脚的复用功能,使能SPI2时钟。

我们要用SPI2,第一步就要使能SPI2的时钟,SPI2的时钟通过APB1ENR的第14位来设置。其次要设置SPI2的相关引脚为复用输出,这样才会连接到SPI2上否则这些IO口还是默认的状态,也就是标准输入输出口。这里我们使用的是PB13、14、15这3个(SCK.、MISO、MOSI,CS使用软件管理方式),所以设置这三个为复用IO。

2)设置SPI2工作模式。

这一步全部是通过SPI2_CR1来设置,我们设置SPI2为主机模式,设置数据格式为8位,然后通过CPOL和CPHA位来设置SCK时钟极性及采样方式。并设置SPI2的时钟频率(最大18Mhz),以及数据的格式(MSB在前还是LSB在前)。

3)使能SPI2

这一步通过SPI2_CR1的bit6来设置,以启动SPI2,在启动之后,我们就可以开始SPI通讯了。

SPI2的使用就介绍到这里,接下来介绍一下W25Q64。W25Q64是华邦公司推出的大容量SPI FLASH产品,W25Q64的容量为64Mb,该系列还有W25Q80/16/32等。ALIENTEK所选择的W25Q64容量为64Mb,也就是8M字节。

W25Q64将8M的容量分为128个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每个扇区4K个字节。W25Q64的最少擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Q64开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。

W25Q64的擦写周期多达10W次,具有20年的数据保存期限,支持电压为2.7~3.6V,W25Q64支持标准的SPI,还支持双输出/四输出的SPI,最大SPI时钟可以到80Mhz(双输出时相当于160Mhz,四输出时相当于320M),更多的W25Q64的介绍,请参考W25Q64的DATASHEET。

28.2 硬件设计

本章实验功能简介:开机的时候先检测W25Q64是否存在,然后在主循环里面检测两个按键,其中1个按键(WK_UP)用来执行写入W25Q64的操作,另外一个按键(KEY1)用来执行读出操作,在TFTLCD模块上显示相关信息。同时用DS0提示程序正在运行。

所要用到的硬件资源如下:

1)  指示灯DS0

2)  WK_UP和KEY1按键

3) TFTLCD模块

4) SPI

5) W25Q64

这里只介绍W25Q64与STM32的连接,板上的W25Q64是直接连在STM32的SPI2上的,连接关系如图28.2.1所示:

 



图28.2.1 STM32与W25Q64连接电路图

28.3 软件设计

打开上一章的工程,首先在HARDWARE文件夹下新建一个FLASH的文件夹和SPI的文件夹。然后新建一个flash.c和flash.h的文件保存在FLASH文件夹下,新建spi.c和spi.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;                     //空闲模式下SCK为1 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的频率设置成了最低(36Mhz,256分频)。在外部函数里面,我们通过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

       {

        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\r\n",WriteAddr,NumByteToWrite);//测试用

      if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096个字节

       while(1)

       {    

              SPI_Flash_Read(SPI_FLASH_BUF,secpos*4096,4096);//读出整个扇区的内容

              for(i=0;i

              {

                     if(SPI_FLASH_BUF[secoff+i]!=0XFF)break;//需要擦除          

              }

              if(i

              {

                     SPI_Flash_Erase_Sector(secpos);//擦除这个扇区

                     for(i=0;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"

......//省略部分代码

#endif

这里面就定义了一些与W25Q64操作相关的命令(部分省略了),这些命令在W25Q64的数据手册上都有详细的介绍,感兴趣的读者可以参考该数据手册,其他的就没啥好说的了。保存此部分代码。最后,我们在test.c里面,修改main函数如下:

由于编辑器的问题,下面的内容省略,请大家下载附件阅读。

;i++)      > )> ;i++)> ;i++)<>
PARTNER CONTENT

文章评论3条评论)

登录后参与讨论

用户1834176 2015-4-7 13:56

对初学者是很好的教材,谢谢了!

用户1827483 2015-1-30 22:51

很有用的资料,比较实用

用户1827233 2015-1-29 11:03

不错,很好的资料!
相关推荐阅读
正点原子 2013-05-17 23:47
【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第六十一章 战舰STM32开发板综合实验(标准例程终结篇)
   第六十一章 战舰STM32开发板综合实验        前面已经给大家讲了55个实例了,本章将设计一个综合实例,作为本指南的最后一个实验 ,该实验向大家展示了STM...
正点原子 2013-05-03 23:02
【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第六十章 UCOSII实验3-消息队列、信号量集和软件定时器
   第六十章 UCOSII实验3-消息队列、信号量集和软件定时器   上一章,我们学习了UCOSII的信号量和邮箱的使用,本章,我们将学习消息队列、信号量集和软件定时器...
正点原子 2013-05-03 20:42
【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第五十七章 ENC28J60网络实验
第五十七章 ENC28J60网络实验   本章,我们将向大家介绍ALIENTEK ENC28J60网络模块及其使用。本章,我们将使用ALIENTEK ENC28J60网络模块...
正点原子 2013-05-01 23:00
【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第五十九章 UCOSII实验2-信号量和邮箱
第五十九章 UCOSII实验2-信号量和邮箱      上一章,我们学习了如何使用UCOSII,学习了UCOSII的任务调度,但是并没有用到任务间的同步与通信,本章我们将学习两个最基本的...
正点原子 2013-04-30 10:55
【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第五十八章 UCOSII实验1-任务调度
  第五十八章 UCOSII实验1-任务调度      前面我们所有的例程都是跑的裸机程序(裸奔),从本章开始,我们将分3个章节向大家介绍UCOSII(实时多任务操作系...
正点原子 2013-04-26 23:16
【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第五十七章 ENC28J60网络实验
 第五十七章 ENC28J60网络实验  本章,我们将向大家介绍ALIENTEK ENC28J60网络模块及其使用。本章,我们将使用ALIENTEK ENC28J60网络模块和uIP 1...
EE直播间
更多
我要评论
3
15
关闭 站长推荐上一条 /3 下一条