打印

【连载】STM32开发指南--第二十八章 SPI实验

[复制链接]
6232|16
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
正点原子|  楼主 | 2013-3-16 21:22 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 正点原子 于 2013-3-16 21:24 编辑

第二十八章 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)
{     
  //省略部分代码        
}   
//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<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\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<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组下面,最后,我们在test.c里面,修改main函数如下:
//要写入到W25Q64的字符串数组
const u8 TEXT_Buffer[]={"WarShipSTM32 SPI TEST"};
#define SIZE sizeof(TEXT_Buffer)      
int main(void)
{
//省略部分代码
}
这部分代码和IIC实验那部分代码大同小异,我们就不多说了,实现的功能就和IIC差不多,不过此次写入和读出的是SPI FLASH,而不是EEPROM。
28.4 下载验证
在代码编译成功之后,我们通过下载代码到ALIENTEK战舰STM32开发板上,通过先按WK_UP按键写入数据,然后按KEY1读取数据,得到如图28.4.1所示:

28.4.1 SPI实验程序运行效果图
伴随DS0的不停闪烁,提示程序在运行。程序在开机的时候会检测W25Q64是否存在,如果不存在则会在TFTLCD模块上显示错误信息,同时DS0慢闪。大家可以通过跳线帽把PB12和PB13短接就可以看到报错了。
实验23 SPI实验.rar (124.62 KB)

《STM32开发指南》第二十八章 SPI实验.rar (601.08 KB)



沙发
moline| | 2013-3-16 21:34 | 只看该作者
哇靠,原子大哥来这里发布了……

使用特权

评论回复
板凳
hawksabre| | 2013-3-17 10:48 | 只看该作者
原子大哥  就是给力啊  呵呵  源码共享  不错哦   谢谢了  呵呵

使用特权

评论回复
地板
正点原子|  楼主 | 2013-3-17 23:04 | 只看该作者
谢谢大家支持,每天发布一章,希望大家不要厌烦就好.

使用特权

评论回复
5
qq20707| | 2013-3-18 23:33 | 只看该作者
原子大哥  就是给力

使用特权

评论回复
6
一般首席| | 2013-3-19 10:15 | 只看该作者
原子牛X

使用特权

评论回复
7
可惜不一样| | 2013-9-16 20:32 | 只看该作者
问原子哥一个很简单的问题,程序里面有 一句:
if(SPI_FLASH_BUF[secoff+i]!=0XFF)break;//需要擦除
为什么是不等于0xff就需要擦除呢??

使用特权

评论回复
8
newsoule| | 2013-9-17 10:44 | 只看该作者
顶起

使用特权

评论回复
9
kingxw| | 2013-9-17 20:12 | 只看该作者
有没有全部的呀?这样一个一个地找也找不全啊

使用特权

评论回复
10
薇儿安蓝| | 2013-9-18 11:31 | 只看该作者
好东西

使用特权

评论回复
11
978462113| | 2013-11-6 14:34 | 只看该作者
学习了,支持原子哥········

使用特权

评论回复
12
longfenghugui| | 2014-8-21 10:29 | 只看该作者
必须顶起

使用特权

评论回复
13
wx2188751| | 2014-12-26 22:21 | 只看该作者
学习了  谢谢

使用特权

评论回复
14
kfliuyan| | 2014-12-28 19:32 | 只看该作者
学习一下

使用特权

评论回复
15
小浣熊| | 2014-12-29 18:08 | 只看该作者
硬件设计这部分比较给力

使用特权

评论回复
16
kekeke| | 2014-12-29 21:03 | 只看该作者
谢楼主分享

使用特权

评论回复
17
白丁野老| | 2014-12-30 20:59 | 只看该作者
学习一下

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:我的STM32开发板店铺:http://openedv.taobao.com 我的技术论坛论坛:www.openedv.com

91

主题

264

帖子

71

粉丝