文章目录
- SPI介绍
- SPI相关的寄存器
- SPI 控制寄存器 1(SPI_CR1)
- 9位SSM,8位SSI
- 456位
- 2位 MSTR
- SPI数据寄存器2(SPI_CR2)
- 2位SSOE
- 1 位TXDMAEN
- 0位 RXDMAEN
- SPI 数据寄存器(SPI_DR)
- SPI工作模式
- SPI中断
- SPI使用步骤
- 使能 SPI2 的时钟
- 配置相关引脚的复用功能
- 初始化 SPI2, 设置 SPI2 工作模式
- 使能SPI2
- SPI传输数据
- 发送数据函数
- 接收数据函数
- 查看 SPI 传输状态函数
- 接收
- 发送
- 设置SPI2速度函数
- 读写一个字节
- W25Q128
- 容量
- 擦除
- W25QXX驱动解读
- W25QXX.h
- 初始化SPI
- 读取状态寄存器
- 写状态寄存器
- 擦除一个扇区
- 读取 SPI FLASH
- 无检查写函数
- W25QXX_Write函数
SPI介绍
参考 中文参考手册 721
SPI 接口提供两个主要功能,支持 SPI 协议或 I 2 S 音频协议。 默认情况下,选择的是 SPI 功能。可通过软件将接口从 SPI 切换到 I 2 S。
SPI相关的寄存器
SPI 控制寄存器 1(SPI_CR1)
9位SSM,8位SSI
- SSM:软件从设备管理 (Software slave management)SSM置位时,NSS输入引脚的电平将被SSI的值代替。
- SSI:内部从设备选择 (Internal slave select)
- SSOE:SS输出使能 (SS output enable)
- MSTR:主设备选择 (Master selection)
- 主设备和从设备在进行SPI通信的时候,从设备都有个CS片选信号,低电平有效,我们通常都要用这个NSS连到从设备的CS上。
- 控制内部NSS引脚与SSI(一个寄存器)相连(软件模式),还是与外部NSS引脚(硬件引脚)相连(硬件模式)。
所谓输入,就是NSS的电平信号给自己,所谓输出,就是将NSS的电平信号发送出去,给从机。
456位
设置SPI2的速度
2位 MSTR
- 当SPI工作在从模式(MSTR=0)
- 当SPI工作在主模式配置(MSTR=1)
- 当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它的设备它是主设备;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个硬件失败错误(Hard Fault)。
SPI数据寄存器2(SPI_CR2)
2位SSOE
SS 输出使能 (SS output enable)
0:在主模式下禁止 SS 输出,可在多主模式配置下工作
1:在主模式下使能 SS 输出,不能在多主模式环境下工作
注意: 不适用于 I 2 S 模式和 SPI TI 模式
1 位TXDMAEN
发送缓冲区 DMA 使能 (Tx buffer DMA enable)
当此位置 1 时,每当 TXE 标志置 1 时,即产生 DMA 请求。
0:关闭发送缓冲区 DMA
1:使能发送缓冲区 DMA
0位 RXDMAEN
接收缓冲区 DMA 使能 (Rx buffer DMA enable)
当此位置 1 时,每当 RXNE 标志置 1 时,即产生 DMA 请求。
0:关闭接收缓冲区 DMA
1:使能接收缓冲区 DMA
SPI 数据寄存器(SPI_DR)
SPI工作模式
SPI总线有四种工作方式。主要输出串行同步时钟极性和相位可以进行配置。
重点:每个flash手册中会注明使用的工作模式,若没直接注明,需要根据flash命令的时序自行判断。
- 时钟极性
如果CPOL=0,串行同步时钟的空闲状态为低电平;
如果 CPOL=1,串行同步时钟的空闲状态为高电平。 - 时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。
如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;
如果 CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。 - SPI 主模块和与之通信的外设备时钟相位和极性应该一致。
不同时钟相位下的总线数据传输时序如图
STM32F4 的 SPI 功能很强大,SPI 时钟最高可以到 37.5Mhz,支持 DMA,可以配置为 SPI协议或者 I2S 协议(支持全双工 I2S)。
SPI中断
SPI使用步骤
我们将利用 STM32 的 SPI 来读取外部 SPI FLASH 芯片(W25Q128),实现类似IIC的功能。
使能 SPI2 的时钟
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硬件连接图。
初始化 SPI2, 设置 SPI2 工作模式
这在库函数中是通过 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
使能 SPI2 的方法是:
SPI_Cmd(SPI2, ENABLE); //使能 SPI 外设
SPI2_ReadWriteByte(0xff); //④启动传输,主机发一个字节,进行一次传输,可以启动传输
SPI传输数据
发送数据函数
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
接收数据函数
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ;
查看 SPI 传输状态函数
判断数据是否传输完成,发送区是否为空
判断接收是否完成,接收区是否空
接收
SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE);
发送
SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE)
设置SPI2速度函数
单独的设置分频系数的函数
//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);
}
- 参考SPI 控制寄存器 1(SPI_CR1)
读写一个字节
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
• W25Q128 是华邦公司推出的大容量 SPI FLASH 产品,W25Q128 的容量为 128Mb,该系列还有 W25Q80/16/32/64等。
容量
24位地址
- 16M ,2的24次方。
- 256 个块(Block)。2的8次方
- 每个块大小为 64K 字节。
- 每个块又分为16 个扇区(Sector)2的4次方
- 每个扇区 4K 个字节。2的12次方
- 每页256字节
共有2的24次方
块地址8位+扇区地址4位+偏移地址12位。
擦除
W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。
这样要求芯片必须有 4K 以上 SRAM 才能很好的操作。
- W25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V,
- W25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320M)。
写之前,必须擦除对应扇区内容,也就是确保其中的值是0xFFF。但是擦除的最小单位是扇区,也就是4K。所以在擦除之前我们先将这个扇区的数据读取出来,保存在缓存区。在缓存中将对应的地址更新之后,一次性将数据写到对应的sector之中。
W25QXX驱动解读
w25qxx.c,里面编写的是与 W25Q128 操作相关的代码
W25QXX.h
W25QXX_CS片选,值0选定,1取消
初始化SPI
先不选中
速度设置最高,因为操作SPI flash读写数据速度越快越好
读取ID的函数
读取状态寄存器
片选和取消片选
第一句写得是个指令
第二句才是读取,写了一个空
写状态寄存器
写了两次,同样一个指令和一个字节
擦除一个扇区
先写使能
等待不繁忙
片选
写指令
写地址开始
写24位,取消片选,flash擦除100多毫秒的一个扇区。
读取 SPI FLASH
//在指定地址开始读取指定长度的数据
//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_Write函数
作用与 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; //下一个扇区可以写完了
}
};
}
//跟无检查页写入的逻辑一致。
- 该函数可以在 W25Q128 的任意地址开始写入任意长度(必须不超过 W25Q128 的容量)的数据。
- 先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。
- 擦除的最小单位是扇区,也就是4K。所以在擦除之前我们先将这个扇区的数据读取出来,保存在缓存区。在缓存中将对应的地址更新之后,一次性将数据写到对应的sector之中。
- 当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。