SPI 是英语 Serial Peripheral interface 的缩写,顾名思义就是串行外围设备接口。
SPI,是一种高速的,全双工,同步的通信总线。
SPI 主要特点有:可以同时发出和接收串行数据;可以当作主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。


详细的内部图:


仔细看右下角有两个关键寄存器,CR1和CR2。
SPI 接口一般使用 4 条线通信:
• MISO 主设备数据输入,从设备数据输出。Master input slaver output,主如从出。
• MOSI 主设备数据输出,从设备数据输入。
• SCLK 时钟信号,由主设备产生。
• CS(NSS) 从设备片选信号,由主设备控制,从设备也需要进行相应的设置。可让 SPI主器件与从器件进行单独通信,从而并避免数据线上的竞争。

参考 中文参考手册 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; //下一个扇区可以写完了
}
};
}
//跟无检查页写入的逻辑一致。

NAND FLASH W25N01GVZEIG支持Standard/DUAL/QUAD SPI


区别就是在于注释的内容:
引脚分配的说明:
(Clip Select) :片选线
当CS被拉低,装置将会被选择,能量消耗将会增加到激活水平,命令可以被写或者数据可以被读。上电后,CS必须从高到低的转变,在一个命令被传来之前。CS输入必须在上下电的时候跟随VCC供能水平。如果需要,一个上拉电阻可以帮助完成这个
(Serial Data Output(Data Input Output 1)) :串行数据输出线
(Write Protect Input (Data Input Output 2)) :写数据保护线
写保护引脚可以被用来保护状态寄存器。和状态寄存器的BP位、SRP位结合使用。一部分内存如256K到一整块的内存可以被硬件保护。WP-E位控制WP引脚的功能。

当WP-E为0时,装置软件保护,只有SR1被保护。WP引脚变作一个为四元SPI操作的数据引脚。
当WP-E为1时,装置硬件保护,WP变成一个激活的低输入引脚,作为整个装置的写保护。如果WP接地,装置变成一个只读引脚。
4、GND :地线
5、DI(Serial Data Input(Data Input Output 0)) :串行数据输入线
6、CLK(Serial Clock) :时钟线
(Hold Input (Data Input Output 3)) :数据输入保持线

在标准和双SPI操作器件,hold引脚被选择时允许装置暂停。当hold被拉低,CS同时也拉低,DO引脚将会高阻态同时DI和CLK引脚上的信号将会被忽略。当hold是高,装置恢复。hold函数可以在多个装置被共享相同的SPI信号时使用。hold引脚低时激活。
8、VCC :电源输入

W25N01可以通过SPI兼容总线访问,由clk,cs,DI和DO组成。标准SPI命令使用:

当使用3B命令和BBH命令时候,w25n01支持双spi操作。这些命令允许数据可以从装置在两到三个时间周期内传递。双SPI命令对于快速下载到RAM上等非常理想。

当使用命令如6BH、EBH、32H等命令时,w25n01支持四元SPI操作。这些命令允许数据从装置以4-6个周期传输。四元读命令。。。

需要注意的是:片选引脚必须跟随VCC供电水平,从上电直到最小电压的VCC供电水平和Tvsl延时时间到达。并且该引脚必须跟随VCC供电水平在下电时阻止指令序列颠倒。如果需要上拉电阻可以帮助完成这玩意。

上电之后装置自动处于写失能状态,WEL位0。一个写使能命令必须在一个页程序、扇区擦除、块擦除命令执行被接收之前发出。在执行完一个程序、擦朱或者写命令之后,WEL位会被自动清除到0。

软件控制写保护是容易的,通过使用写状态寄存器命令并且设置状态寄存器的SRP0、SRP1和块波阿虎TB、BP3-0位。这样可以设置一个部分或者完全的内存序列被配置为只读。与WP引脚结合,在软件控制下改变状态寄存器就可以使能或者失能。看写保护寄存器了解更多信息。

写访问状态寄存器由非只读的状态寄存器保护位SRP0,SRP1、写使能命令、和当WP-E为1的时候的WP引脚控制。
W25N01G的寻址方式是页地址+列地址(Page Add + Column Add)。


1024*(4096)*N每个扇区大小4K,但实际可用的只有2K。
1024代表快,N代表第几个页。
OTP锁定的值。
One Time Program (OTP) write protection

S7-S2为块保护位,主要提供写保护控制和状态。块保护位上电后的默认值是1,以保护整个flash。如果配置寄存器(SR-2)中的SR1-L位被设置为1,那么其默认值将是OTP锁定的值。
Protection Register / Status Register-1 (Volatile Writable, OTP lockable) 此寄存器上电后的默认值是 0b01111100,也就是说 Flash 处于写保护状态。我们需要用 Write Status Register (1Fh/01h) 这条指令清除写保护。 否则擦写指令不起作用。
{
}
当WP-E = 0(默认值)时,设备处于软件保护模式,当WP-E = 1时,设备处于硬件保护模式。

写保护位用来使能硬件保护。当该位1时,WP引脚被拉低,堵塞任何写、执行和擦除命令。装置变成只读的。四元SPI操作也不能在WP-E为1的时候操作。
状态寄存器2可以改变BUF和ECC-E的状态。

地址:0xC0即可。
擦写、执行程序忙。
程序执行失败
擦写失败
累计的ECC状态

ECC函数用在NAND闪存中纠正在读操作器件有限内存的错误。
ECC状态位应该在完成读操作后检查一下证明数据的完整性。
ECC状态位不关心0.
这些位将会在重新上电或者执行复位命令后清零。

解读上图。ECC1和0都是1的情况只在BUF=0的时候应用。
0 0 整个数据输出成功,没有任何ECC纠正
0 1 整个数据输出成功,有1-4位的ECC纠正
1 0 仅一个单页里不能被ECC修复,整个数据输出包含超过4位的错误。
1 1 在多个页中整个数据输出包含超过4位错误。在连续读模式,额外的命令只能提供最后失败的PA地址,用户不能获得其它失败页的PA地址。数据不适合使用。


写状态寄存器指令让我们可以写状态寄存器。可以写的状态位包括…,所有其它位是只读的,不受该命令影响。

未来写状态寄存器位,命令通过拉低CS进入。发送命令1F,呆着寄存器地址,然后写状态寄存器即可。

参照7.1.3的描述,上拉之后,默认BP,TB,ECC-E位都是1,其它位是0.

写状态寄存器命令。
impedance/阻抗/
1、SPI 的信号线最好加 1k 上拉电阻, 实测发现 W25N01GV 这颗芯片的驱动能力比较弱。在调试的时候这可能发生误导,因为 BUSY = 0 时表明命令执行完毕,实际上是读回来的虚假低电平。(待验证)。
3、要注意两个型号的区别
W25N01GVxxIG, 默认 BUF = 1, 处于 Buffer Read Mode。
W25N01GVxxIT, 默认 BUF = 0,处于 Continous Read Mode。
两种模式可以用过命令切换。
4、写入数据分两步:1. 先把数据传入芯片内部的 2048 字节 RAM 缓冲区。比如通过 Quad Program Data Load (32h) 指令装载数据。 2 执行 Program Execute (10h)。如果只执行第一步,不执行第二步,数据还可以从缓冲区读出来,但是掉电后数据就没有了。
5.读数据也分为两步,第一步先将指定页数据读取到缓冲区(0x13H ),第二步将数据从缓冲区读出来(0x03H)。
6、在执行 Block Erase,Program Execute等命令前要先执行Write Enable(06h)指令,否则指令不起作用。
7、 非常重要的一点是:SPI的 频率1M合适 ,SPI的的极性一定要为高。也就是空闲时要为高电平。(吃亏了,血泪教训)