赞
踩
参考 中文参考手册 721
SPI 接口提供两个主要功能,支持 SPI 协议或 I 2 S 音频协议。 默认情况下,选择的是 SPI 功能。可通过软件将接口从 SPI 切换到 I 2 S。
SSM:软件从设备管理 (Software slave management)SSM置位时,NSS输入引脚的电平将被SSI的值代替。
SSI:内部从设备选择 (Internal slave select)
SSOE:SS输出使能 (SS output enable)
MSTR:主设备选择 (Master selection)
所谓输入,就是NSS的电平信号给自己,所谓输出,就是将NSS的电平信号发送出去,给从机。
设置SPI2的速度
当SPI工作在从模式(MSTR=0)
当SPI工作在主模式配置(MSTR=1)
SS 输出使能 (SS output enable)
0:在主模式下禁止 SS 输出,可在多主模式配置下工作
1:在主模式下使能 SS 输出,不能在多主模式环境下工作
注意: 不适用于 I 2 S 模式和 SPI TI 模式
发送缓冲区 DMA 使能 (Tx buffer DMA enable)
当此位置 1 时,每当 TXE 标志置 1 时,即产生 DMA 请求。
0:关闭发送缓冲区 DMA
1:使能发送缓冲区 DMA
接收缓冲区 DMA 使能 (Rx buffer DMA enable)
当此位置 1 时,每当 RXNE 标志置 1 时,即产生 DMA 请求。
0:关闭接收缓冲区 DMA
1:使能接收缓冲区 DMA
SPI总线有四种工作方式。主要输出串行同步时钟极性和相位可以进行配置。
重点:每个flash手册中会注明使用的工作模式,若没直接注明,需要根据flash命令的时序自行判断。
不同时钟相位下的总线数据传输时序如图
STM32F4 的 SPI 功能很强大,SPI 时钟最高可以到 37.5Mhz,支持 DMA,可以配置为 SPI协议或者 I2S 协议(支持全双工 I2S)。
我们将利用 STM32 的 SPI 来读取外部 SPI FLASH 芯片(W25Q128),实现类似IIC的功能。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );//PORTB 时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE );//SPI2 时钟使能
这里使用 PB13、14、15 这 3 个(SCK.、MISO、MOSI,CS 使用软件管理方式),所以设置这三个为复用 IO。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化 GPIOB
SPI2的引脚在PB上,可以参考W25Q128硬件连接图。
这在库函数中是通过 SPI_Init 函数来实现的。
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
SPI_InitTypeDef 的定义:
typedef struct
{
uint16_t SPI_Direction;
uint16_t SPI_Mode;
uint16_t SPI_DataSize;
uint16_t SPI_CPOL;
uint16_t SPI_CPHA;
uint16_t SPI_NSS;
uint16_t SPI_BaudRatePrescaler;
uint16_t SPI_FirstBit;
uint16_t SPI_CRCPolynomial;
}SPI_InitTypeDef;
这里挑取几个重要的成员变量讲解一下:
• SPI_Direction 用来设置 SPI 的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式
SPI_Direction_2Lines_FullDuplex //全双工
• SPI_Mode 用来设置 SPI 的主从模式
SPI_Mode_Master 主机模式
SPI_Mode_Slave //从机模式
• SPI_DataSize为 8 位还是 16 位帧格式选择项
SPI_DataSize_8b
• SPI_CPOL 用来设置时钟极性
SPI_CPOL_High 串行同步时钟的空闲状态为高电平
• SPI_CPHA 用来设置时钟相位,就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,可以为第一个或者第二个条边沿采集
SPI_CPHA_2Edge,或者1Edge
• SPI_NSS 设置NSS 信号由硬件(NSS 管脚)还是软件控制
SPI_NSS_Soft //软件
• SPI_BaudRatePrescaler设置 SPI 波特率预分频值决定 SPI 的时钟的参数,从不分频道 256 分频 8 个可选值
SPI_BaudRatePrescaler_256 //256 分频值
传输速度为 36M/256=140.625KHz。2-156,凡是2的几次方都可以。
• SPI_FirstBit ,设置数据传输顺序是 MSB 位在前还是 LSB 位在前
SPI_FirstBit_MSB 高位在前
• SPI_CRCPolynomial 来设置 CRC 校验多项式,提高通信可靠性,大于 1 即可。
初始化代码:
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主 SPI
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 信号由软件控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频 256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从 MSB 位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式
SPI_Init(SPI2, &SPI_InitStructure); //根据指定的参数初始化外设 SPIx 寄存器
使能 SPI2 的方法是:
SPI_Cmd(SPI2, ENABLE); //使能 SPI 外设
SPI2_ReadWriteByte(0xff); //④启动传输,主机发一个字节,进行一次传输,可以启动传输
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ;
判断数据是否传输完成,发送区是否为空
判断接收是否完成,接收区是否空
SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE);
SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE)
单独的设置分频系数的函数
//SPI 速度设置函数
//SpeedSet://SPI_BaudRatePrescaler_256 256 分频 (SPI 281.25K@sys 72M)
void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));
SPI2->CR1&=0XFFC7;
SPI2->CR1|=SPI_BaudRatePrescaler; //设置 SPI2 速度
SPI_Cmd(SPI2,ENABLE);
}
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); //通过外设 SPIx 发送一个数据
retry=0;
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //等待接收完一个 byte
{
retry++;
if(retry>200)return 0;
}
return SPI_I2S_ReceiveData(SPI2); //返回通过 SPIx 最近接收的数据
}
• W25Q128 是华邦公司推出的大容量 SPI FLASH 产品,W25Q128 的容量为 128Mb,该系列还有 W25Q80/16/32/64等。
24位地址
W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。
这样要求芯片必须有 4K 以上 SRAM 才能很好的操作。
写之前,必须擦除对应扇区内容,也就是确保其中的值是0xFFF。但是擦除的最小单位是扇区,也就是4K。所以在擦除之前我们先将这个扇区的数据读取出来,保存在缓存区。在缓存中将对应的地址更新之后,一次性将数据写到对应的sector之中。
w25qxx.c,里面编写的是与 W25Q128 操作相关的代码
W25QXX_CS片选,值0选定,1取消
先不选中
速度设置最高,因为操作SPI flash读写数据速度越快越好
读取ID的函数
片选和取消片选
第一句写得是个指令
第二句才是读取,写了一个空
写了两次,同样一个指令和一个字节
先写使能
等待不繁忙
片选
写指令
写地址开始
写24位,取消片选,flash擦除100多毫秒的一个扇区。
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大 65535)
void W25QXX_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[i]=SPI2_ReadWriteByte(0XFF); //循环读数
}
SPI_FLASH_CS=1;
}
由于 W25Q128 支持以任意地址开始读取数据,在发送 24 位地址之后,程序就可以开始循环读数据了,其地址会自动增加的,不过要注意,不能读的数据超过了 W25Q128 的地址范围哦!
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u16 pageremain;
pageremain=256-WriteAddr%256; //单页剩余的字节数
if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节,这也是结束标识
while(1)
{
W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
if(NumByteToWrite==pageremain)break;//写入结束了
else //NumByteToWrite>pageremain
{
pBuffer+=pageremain;
WriteAddr+=pageremain;
NumByteToWrite-=pageremain; //减去已经写入了的字节数
if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
else pageremain=NumByteToWrite; //不够256个字节了
}
//按照页剩余写一次,然后256个字节的写,然后写最后一页多出来的。
};
}
NoCheck是说可以跨扇区的写
下方表示写了一个扇区
作用与 W25QXX_Flash_Read 的作用类似,不过是用来写数据到 W25Q128 里面的,其代码如下:
u8 W25QXX_BUFFER[4096];
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u32 secpos;
u16 secoff;
u16 secremain;
u16 i;
u8 * W25QXX_BUF;
W25QXX_BUF=W25QXX_BUFFER;
secpos=WriteAddr/4096;//扇区地址,每个扇区是4096,所以除以4096得到的整数就是扇区的地址标号
secoff=WriteAddr%4096;//在扇区内的偏移
secremain=4096-secoff;//扇区剩余空间大小
//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于 4096 个字节
while(1)
{
W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
//secpos*4096是该扇区的起始地址
for(i=0;i<secremain;i++)//校验数据
{
if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除,偏移地址内有数据
//擦除后的默认值是0xFFF
}
if(i<secremain)//需要擦除
{
W25QXX_Erase_Sector(secpos); //擦除这个扇区
for(i=0;i<secremain;i++) //复制
{
W25QXX_BUF[i+secoff]=pBuffer[i];
}
W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区
}
else
W25QXX_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; //下一个扇区可以写完了
}
};
}
//跟无检查页写入的逻辑一致。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。