图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<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组下面,再打开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;//设置字体为红色
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; //设置字体为蓝色
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慢闪。大家可以通过跳线帽把PB12和PB13短接就可以看到报错了。