1、SPI简介
SPI(Serial Periphreral Interface,串行外设接口)是一种高速的、全双工的、串行的、同步的通信总线。
全双工的意味着SPI至少有两根数据线
串行的意味着按bit一个bit接着一个bit的传输
同步的意味着通信双方有共同的时钟线
SDO : Serial Data Output 串行数据输出
SDI : Serial Data Input 串行数据输入
SCK : Serial Clock 时钟线
同样的SPI总线上可以同时接多个SPI设备。
通过谁控制时钟线人为的将SPI总线上的设备分为:
Master 主设备:产生时钟信号的设备
Slave 从设备:接收时钟信号的设备
根据主从设备的分别,有时候两个数据接口命名也会有所不同:
MOSI:Master Output Slave Input 主设备发送,从设备接收线
MISO:Master Input Slave Output 主设备接收,从设备发送线
SPI还有另外一根SS(Slave Select)线
因为SPI总线上的设备不像IIC总线上一样有地
址,所以我们需要一根额外的线来确定到底与谁进
行通信。
这个SS就是片选信号线,此线可以利用任何一个GPIO引脚,发送0/1进行片选。
所以SPI通信一般需要4根线:
SDO/SDI/SCK/SS
2、SPI协议
CPOL:CLock Polarity 时钟极性
时钟极性决定不传任何数据(SPI空闲)时时钟线的电平状态。
CPOL == 1 不传数据时,时钟线保持高电平
CPOL == 0 不传数据时,时钟线保持低电平
CPHA:Clock PHase 时钟相位
时钟相位决定SPI在何时锁定数据(决定数据采集的时刻)
CPHA == 1 在时钟线SCK的第二个边沿采集数据
CPHA == 0 在时钟线SCK的第一个边沿采集数据
CPOL和CPHA的组合决定了数据采集和数据发送的时间。
SPI数据的收发是按帧(Frame)为单位
DFF:Data Frame Format 数据帧格式
规定的是一帧数据占多少个bit位
8bit 一帧数据8bit
16bit 一帧数据16bit
LSBFIRST:决定到底是LSB先发还是MSB先发
1 LSB(低位)先发
0 MSB(高位)先发
SPI的传输速率也是由时钟频率决定的。有一个原则:就低不就高。
3、STM32F4xx SPI控制器
4、STM32F4xx 固件库中SPI相关的功能函数
SDO/SDI/SCK都是复用自GPIO口(无论是主从都没有区别)。
如果是作为从设备,则NSS(片选)也是复用自GPIO。
如果是主设备,则不需要利用GPIO去复用为NSS。
1)初始化SPI对应的GPIO引脚
a. 使能GPIO分组时钟
RCC_AHB1...
b. 初始化GPIO为复用功能(NSS为推挽输出 SDO/SDI/SCK复用功能)
GPIO_Init()
c. 指定GPIO复用成何种功能
GPIO_PinAFConfig
2)配置SPI控制器
a. 使能SPI控制器时钟
RCC_APBx...
b. 初始化SPI控制器
SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)
@SPIx:指定要配置的SPI控制器的编号
SPI1~SPI6
@SPI_InitStruct:指向SPI控制器初始化信息结构体
3)使能SPI
SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState)
4)收发数据
a. 读取数据:只有RXNE == 1时才能读取
可以利用中断也可以利用轮询读取数据
读取标志位:
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG)
利用轮询读取数据:
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
return SPI_ReceiveData(SPI1);
b. 发送数据,只能在TXE == 1时才能发送数据
void SPI1_Send_Byte(u8 ch)
{
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
SPI_SendData(SPI1,ch);
}
5、W25Q128
是一款SPI接口的Flash存储器芯片。
1)接口说明
W25Q128 CPU
CS PB14
SO PB4
SI PB5
CLK PB3
当CS管脚为高电平时,芯片处在不选择的状态
当CS管脚为低电平时,芯片处于被选择的状
2)W25Q128内部存储结构
内部的存储地址是24bit,这个24bit的地址分为4部分:
8bit 4bit 4bit 8bit
block number sector number page number byte number
W25Q128 共有 256 block(块) 一共是65536个字节
每块 = 16 sector(扇区) 一扇4096个字节(4K)
每扇 = 16 page(页) 一页256个字节
每页 = 256 byte(字节)
所以W25Q128存储器容量大小为 = 256 * 16 * 16 * 256 = 2 ^ 24 = 16M
反推得知:
假设某个字节的地址为x,则该字节位于:
(x >> 16) & 0xFF 块号
(x >> 12) & 0xF 扇号
(x >> 8) & 0xF 页码
x & 0xFF 字地址
如果要往W25Q128中写入数据的话,需要分为两个步骤:
a. Erase 擦除
以块 或者 半块 或者 扇区 或者 整个芯片为单位,清除数据
b. Program写入
W25Q128芯片所有的操作,全部都是通过指令来进行的。
通过SPI总线CPU向W25Q128发送指令即可控制。
3)W25Q128的指令
W25Q128BV 共有 34 个基本指令。指令都是以 /CS 下降沿开始的。
-->表示发送指令前,必须先将对应的片选引脚输出
低电平,也就是将MCU的PB14拉低之后才能发送指令。
第一个传输的字节是指令码。
-->指令其实就是一个字节的十六进制数。
在DI上传输的数据是在时钟的上升沿被锁存的。
-->MCU发给W25Q128的数据(指令),必须在时钟的上升沿被锁存,也就是说
W25Q128是在时钟的上升沿去采样。CPU发送数据给W25Q128是下降沿的时候发送。
这句话决定了CPHA和CPOL该如何配置:
SPI选择模式0(0,0) or 模式3(1,1)
MSB首先被传输。
-->高位先发 LSBFIRST-->0
指令是以 /CS 上升沿结束的。
-->MCU将PB14拉高则指令发送完成
所有的读指令可以在任何时钟周期结束。但是,所
有的擦写指令必须在 /CS 拉高之后还有一个 8 位
的时钟间隔,否则前面的擦写指令将被忽略。
-->发送擦写指令后,必须再过8个SPI时钟周期才能发送下一条指令。
3.1)读状态寄存器的指令(指令:0x05/0x35)
读取状态寄存器1 SR1 -->0x05
读取状态寄存器2 SR2 -->0x35
MCU向W25Q128发送读取状态寄存器的指令完成后,W25Q128回复一个字节的数据这一个字节的数据就是状态寄存器的值。
SR1:
SRP0 SEC TB BP2 BP1 BP0 WEL BUSY
BUSY:为1表示W25Q128忙碌,为0表示空闲
主机在发送指令之前,必须先判断从设备的状态是否忙碌
BUSY == 0时,才能发送指令
WEL:为1表示写使能,可以写入
为0表示写禁止,不能写入
BP2 BP1 BP0:块保护(指定某些块只能读不能写)
#define W25Q128_ENABLE GPIO_ResetBits(GPIOB,GPIO_Pin_14)
#define W25Q128_DISABLE GPIO_SetBits(GPIOB,GPIO_Pin_14)
u8 Get_W25Q128_SR1(void)
{
//使能片选 PB14-->0
W25Q128_ENABLE;
Write_Bytes_To_W25Q128(0x05); //发送读取SR1的指令
u8 sta = SPI_ReceiveData(SPI1);
//禁止片选 PB14-->1
W25Q128_DISABLE;
return sta;
if(sta & 0x01 != 0)-->意味着W25Q128是忙碌的
}
3.2)读取W25Q128的ID(指令:0x90)
u16 Read_W25Q128_ID(void)
{
//使能片选 PB14-->0
W25Q128_ENABLE;
Write_Bytes_To_W25Q128(0x90); //发送读取ID的指令
Write_Bytes_To_W25Q128(0x00);
Write_Bytes_To_W25Q128(0x00);
Write_Bytes_To_W25Q128(0x00);
u8 idh8 = SPI_ReceiveData(SPI1); //制造商ID
u8 idl8 = SPI_ReceiveData(SPI1); //设备ID
//禁止片选 PB14-->1
W25Q128_DISABLE;
return (idh8 << 8) | idl8;
}
//SPI初始化
//SPI1_Send_A_Byte
//SPI1_Recv_A_Byte
3.3)读取数据
3.4)写入数据
以页为单位进行写入。
Flash芯片在写入数据之前,一般要先擦除,所谓擦除就是将页内字节的值都变成0xFF
当然在擦除之前,要先进行写使能(指令:0x06)
W25Q128的擦除单位是:
擦除单元 Sector(4K) Half of Block(32K) Block(64K) Chip(整个芯片)
指令 0x20 0x52 0xD8 0xC7/0x60
写入的流程:
a. 写使能
在擦除/写入之前,先要写使能,才能修改Flash中的内容。
b. 擦除
在擦除/写入时,都需要时间,所以你在擦除/写入的时候要先判断W25Q128是否BUSY。
c. 写入
一般的,如果写入数据不够擦除空间,为了使空间其它存储单元的数据不变,一般
是要将要擦除的空间的数据先读取出来保存到数组中,然后再将要写入的数据
替换到数组对应的位置,最后将把替换后的数组写入到W25Q128中去。
比如:
写入0x2B到W25Q128的0x123456地址上去。
地址为0x123456的存储单元处于地址为0x123000的扇区上。
写入操作的流程为:
//先将整个扇区的数据读取出来
Read_Bytes_From_W25Q128(0x123000,data,4096);
//将要写入的数据替换到数组对应的位置上去
data[0x456] = 0x2B;
//在去擦除整个扇区
W25Q128_Erase_Sector(0x123000);
//等待擦除完毕
//最后将data中的4096个字节重新写进去
//等待写入完成
需要考虑两个问题:
1. 写入的内容不足1个擦除空间
2. 写入的内容超过1个擦除空间(此时就需要分多次写入)
/*
SPI1_Init:对SPI1的初始化
W25Q128 CPU
CS PB14
SO PB4
SI PB5
CLK PB3
CS-->输出推挽
SO/SI/CLK-->复用
*/
void SPI1_Init(void)
{
//1.配置与SPI相关的GPIO
//1.1 使能GPIO分组时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
//1.2 初始化GPIO
GPIO_InitTypeDef g;
g.GPIO_Mode = GPIO_Mode_OUT; //输出推挽
g.GPIO_OType = GPIO_OType_PP;
g.GPIO_Pin = GPIO_Pin_14;
g.GPIO_PuPd = GPIO_PuPd_NOPULL;
g.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&g);
g.GPIO_Mode = GPIO_Mode_AF; //复用模式
g.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;
GPIO_Init(GPIOB,&g);
//1.3 指定复用成什么功能
GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);
//2.配置SPI控制器
//2.1 使能SPI1控制器时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
//2.2 初始化SPI控制器
SPI_InitTypeDef s;
s.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
s.SPI_CPHA = SPI_CPHA_1Edge; //W25Q128只支持MODE0(0,0)orMODE3(1,1)
s.SPI_CPOL = SPI_CPOL_Low;
s.SPI_CRCPolynomial = 5;
s.SPI_DataSize = SPI_DataSize_8b;
s.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //全双工
s.SPI_FirstBit = SPI_FirstBit_MSB; //W25Q128只支持高位先发
s.SPI_Mode = SPI_Mode_Master; //主模式
s.SPI_NSS = SPI_NSS_Soft; //NSS由软件控制
SPI_Init(SPI1,&s);
//3.使能SPI
SPI_Cmd(SPI1,ENABLE);
}
/*
SPI1_Send_A_Byte:往SPI1总线上发送一个字节的数据
@data : 为要发送的数据(1个字节)
*/
void SPI1_Send_A_Byte(u8 data)
{
//等待发送数据寄存器为空
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
SPI_SendData(SPI1,data);
/*
当MCU没有向SPI总线上发送数据的时候,总线是空闲的(没有时钟信号)
当MCU向SPI上发送数据的时候,MCU开始向SCK线上产生时钟
SPI开始工作,因为SPI是全双工模式,所以MCU在将data通过MOSI引脚发送
出去的同时,也通过MISO引脚接收到了一个字节的数据(这个数据是无效的dummy)
意思就是即使W25Q128没有主动通过MISO向MCU发送数据,但是MCU上的SPI
控制器任然会自动的从MISO上接收数据,所以应该要对无效数据进行处理
*/
while(SPI_GetFlagStatus(SPI1,SPI_FLAG_RXNE) != SET);
u8 c = SPI_ReceiveData(SPI1);
}
/*
SPI1_Recv_A_Byte:通过SPI1接收一个字节的数据
@返回值 : 将接收到的数据进行返回
*/
u8 SPI1_Recv_A_Byte(void)
{
/*
为啥先发送一个字节之后才能去接收数据呢?
因为当MCU没有向SPI总线上发送数据的时候,总线是空闲的(没有时钟信号)
当MCU作为主设备,SPI控制器只有在MCU主动发送数据的时候才会往SCK线上
产生时钟,而MCU被动的从W25Q128上接收数据的时候,并不会产生时钟信号
所以为了能够收到从设备的数据,必须先发送一个无效数据
主要就是为了在SCK上产生时钟,便于接收数据
*/
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
SPI_SendData(SPI1,0xFF);
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
return SPI_ReceiveData(SPI1);
}
/*
Get_W25Q128_SR1:用来获取W25Q128的状态寄存器1的值
*/
u8 Get_W25Q128_SR1(void)
{
//使能片选 PB14-->0
W25Q128_ENABLE;
SPI1_Send_A_Byte(0x05); //发送读取SR1的指令
u8 sta = SPI1_Recv_A_Byte();
//printf("%d\r\n",sta);
//禁止片选 PB14-->1
W25Q128_DISABLE;
return sta;
//if(sta & 0x01 != 0)-->意味着W25Q128是忙碌的
}
/*
Read_W25Q128_ID:获取W25Q128的16bit的厂商ID和设备ID
*/
u16 Read_W25Q128_ID(void)
{
//使能片选 PB14-->0
W25Q128_ENABLE;
SPI1_Send_A_Byte(0x90); //发送读取ID的指令
SPI1_Send_A_Byte(0x00);
SPI1_Send_A_Byte(0x00);
SPI1_Send_A_Byte(0x00);
u8 idh8 = SPI1_Recv_A_Byte(); //制造商ID
u8 idl8 = SPI1_Recv_A_Byte(); //设备ID
//禁止片选 PB14-->1
W25Q128_DISABLE;
printf("0x%x 0x%x\r\n",idh8,idl8);
return (idh8 << 8) | idl8;
}
/*
Read_Bytes_From_W25Q128:通过SPI1总线从W25Q128中连续的读取数据
@addr : 表示从W25Q128的哪个存储单元开始读取数据
@data : 指向的空间用来保存读取出来的数据
@count : 表示要读取的数据的个数
@返回值 : 返回真正读取的字节数
*/
u32 Read_Bytes_From_W25Q128(u32 addr,u8 *data,u32 count)
{
/*
读数据指令允许从存储器读一个字节和连续多个字节。该指令是以/CS 拉低开始,
然后通 DI 在时钟的上升沿来传输指令代码(03H)和 24 位地址。当芯片接受完地址位后,
相应地址处的值将会,在时钟的下降沿,以高位在前低位在后的方式,在 D0 上传输。
如果连续的读多个字节的话,地址是自动加 1 的。这意味着可以一次读出整个芯片。该
指令也是以/CS 拉高来结束的。
*/
//使能片选 PB14-->0
W25Q128_ENABLE;
SPI1_Send_A_Byte(0x03); //发送读指令
SPI1_Send_A_Byte((addr >> 16) & 0xFF);
SPI1_Send_A_Byte((addr >> 8) & 0xFF);
SPI1_Send_A_Byte((addr >> 0) & 0xFF);
int i;
for(i = 0;i < count;i++)
{
data[i] = SPI1_Recv_A_Byte();
}
//禁止片选 PB14-->1
W25Q128_DISABLE;
return i;
}
/*
W25Q128_Write_Enable:W25Q128的写使能
*/
static void W25Q128_Write_Enable(void)
{
//使能片选 PB14-->0
W25Q128_ENABLE;
SPI1_Send_A_Byte(0x06); //发送写使能指令
//禁止片选 PB14-->1
W25Q128_DISABLE;
}
/*
Wait_W25Q128_Is_Not_Busy:等待W25Q128不繁忙
*/
static void Wait_W25Q128_Is_Not_Busy(void)
{
while(Get_W25Q128_SR1() & 0x01 != 0);
}
/*
W25Q128_Erase_Sector:擦除指定的扇区(将扇区中的所有的存储单元的值变成0xFF)
@addr : 要擦除的扇区的首地址
*/
static void W25Q128_Erase_Sector(u32 addr)
{
W25Q128_Write_Enable(); //写使能
Wait_W25Q128_Is_Not_Busy(); //等待W25Q128不繁忙
W25Q128_ENABLE; //使能片选 PB14-->0
SPI1_Send_A_Byte(0x20); //发送扇区擦写指令
SPI1_Send_A_Byte((addr >> 16) & 0xFF); //A23-A16
SPI1_Send_A_Byte((addr >> 8) & 0xFF); //A15-A8
SPI1_Send_A_Byte((addr >> 0) & 0xFF); //A7-A0
W25Q128_DISABLE; //禁止片选 PB14-->1
Wait_W25Q128_Is_Not_Busy(); //等待W25Q128擦写完成
}
/*
W25Q128_Page_Program:对W25Q128进行页写
@addr : 进行页写的页的首地址
@data : 要写进去的数据
@count : 需要写进去的字节的个数
*/
void W25Q128_Page_Program(u32 addr,u8 *data,int count)
{
//计算count的合法性
//当前页最多还能写入max_bytes个字节
int max_bytes = 256 - (addr & 0xFF);
count = (count > max_bytes) ? max_bytes : count;
W25Q128_Write_Enable(); //写使能
Wait_W25Q128_Is_Not_Busy(); //等待W25Q128不繁忙
W25Q128_ENABLE; //使能片选 PB14-->0
SPI1_Send_A_Byte(0x02); //发送页写指令
SPI1_Send_A_Byte((addr >> 16) & 0xFF); //A23-A16
SPI1_Send_A_Byte((addr >> 8) & 0xFF); //A15-A8
SPI1_Send_A_Byte((addr >> 0) & 0xFF); //A7-A0
int i;
for(i = 0;i < count;i++)
{
SPI1_Send_A_Byte(data[i]);
}
W25Q128_DISABLE; //禁止片选 PB14-->1
Wait_W25Q128_Is_Not_Busy(); //等待W25Q128页写完成
}
//用来临时保存扇区数据的缓冲区
u8 buffer[SECTOR_SIZE] = {0};
/*
Write_Bytes_To_W25Q128:通过SPI1总线将数据连续写入到W25Q128中去
@addr : 表示从W25Q128的哪个存储单元中开始写入数据
@data : 指向的空间中保存的就是要写入的数据
@count : 表示要写入的字节数
@返回值 : 成功写进去的字节数
*/
u32 Write_Bytes_To_W25Q128(u32 addr,u8 *data,u32 count)
{
//判断count的合法性
u32 max_bytes = (1 << 24) - addr; //从addr开始能够写入的最大的字节数
count = (count > max_bytes) ? max_bytes : count;//计算出实际能够读取到的字节数
//计算出第一个扇区的地址
u32 first_sector = addr & 0xFFF000;
//计算出最后一个扇区的地址
u32 last_sector = (addr + count) & 0xFFF000;
//已经写入的字节数
u32 bytes = 0;
u32 sector_addr;
//从第一个扇区遍历到最后一个扇区
for(sector_addr = first_sector;sector_addr <= last_sector;sector_addr += SECTOR_SIZE)
{
//1.先将整个扇区的数据读取出来
Read_Bytes_From_W25Q128(sector_addr,buffer,SECTOR_SIZE);
//2.将要写入到W25Q128中的数据替换到数组的对应的位置上去
//先将当前扇区要替换的字节数计算出来
u32 modify_bytes = 4096 - (addr & 0xFFF);
//然后计算出从data的哪个位置开始替换
u32 offset = addr - sector_addr; //addr & 0xFFF
//替换工作
int i;
for(i = 0;i < modify_bytes && bytes < count;i++,bytes++)
{
//data ---> buffer
buffer[offset + i] = data[bytes];
}
//当前扇区替换完后addr指向下一个扇区
addr += modify_bytes;
//3.擦除扇区
W25Q128_Erase_Sector(sector_addr);
u32 page_addr = sector_addr;
//4.写入扇区
for(i = 0;i < 16;i++)
{
//一次写入最多写入256个字节(一页),所以需要分十六次
W25Q128_Page_Program(page_addr,&buffer[256*i],256);
page_addr += 256;
}
}
return bytes;
}