1 SPI简介
SPI:即Serial Peripheral interface 串行外设接口(Motorala公司)
SPI是一种高速的,全双工,串行的,同步的通信总线。
全双工意味着至少需要两根数据线,串行意味着按bit发送,同步意味着通信双方共用时钟线。
同样的SPI总线上也可以连接多个SPI设备,那么总线上的时钟由谁产生?
谁产生都可以,只要同一时刻没有多个设备同时产生时钟就可以。
通过谁去控制时钟线我们将设备分成:
Master 主设备:产生时钟信号的设备
Slave 从设备:接收时钟信号的设备
根据主从设备的分类,有时候两个数据接口的命名会有所不同:
MOSI: 主设备发送,从设备接收线
MISO: 主设备接收,从设备发送线
MOSI和MISO只是SDO和SDI的不同的叫法:
SDO :Serial Data Output 串行数据输出口
SDI :Serial Data Input 串行 数据输入
SCK :Serial Clock 时钟线
另外,SPI还有一根线SS(Slave Select)/CS(Chip Select)线。因为SPI总线上的设备不像IIC总线上的设备一样有地址这个概念,所以我们需要额外的一根线来确定到底跟谁通信。这根SS/CS就是片选信号线,此线可以利用一个任何的一个GPIO引脚,发送 0/1进行片选(比如:#SS表示低电平有效,则发送0表示通信)。
所以SPI通信一般需要四根线:SDI/SDO/SCK/SS
2 SPI协议
协议就是规定数据如何发。
- CPOL: Clock Polarity 时钟极性
时钟极性决定不传数据的时候(空闲的时候)时钟线的电平状态
CPOL = 1 保持高电平
CPOL = 0 保持低电平
- CPHA:Clock Phase 时钟相位
时钟相位决定SPI在何时锁定数据(决定数据采集时刻)
CPHA = 1 在时钟线的第二个边沿锁定数据(采集数据)
CPHA = 0 在时钟线的第一个边沿锁定数据(采集数据)
也就是说CPOL和CPHA的组合决定了数据采集和数据发送的时刻。(根据模块的手册决定)
1234
- DFF: Data Frame Format 数据帧格式
SPI数据的收发也是以帧Frame为单位,那么一帧数据的传送的bit数由什么决定?
8bits 一帧数据8bits
16bits 一帧数据16bits
那么一帧数据先发LSB还是MSB
LSBFIRST = 1 LSB先发
LSBFIRST = 0 MSB先发
-
传输速率由时钟频率决定,有一个原则:就低不就高
低频设备无法接收高频的信号,但是高频设备能够接收低频信号。
3 STM32F4xx SPI控制器
接收和发送缓冲区
在接收过程中,数据收到后,先存储到内部接收缓冲区中;而在发送过程中,先将数据存储
到内部发送缓冲区中,然后发送数据。
对 SPI_DR 寄存器的读访问将返回接收缓冲值,而对 SPI_DR 寄存器的写访问会将写入的数
据存储到发送缓冲区中。
4 STM32固件库SPI函数接口说明
SDO/SDI/SCK都是复用自GPIO(无论是主设备还是从设备)。
如果是作为从设备,则SS(片选)线也是复用的;如果是主设备,不需要GPIO复用,只需要配置通用输出功能。
4.1 配置SPI的GPIO引脚 (SPI - W25Q128)
通过原理图知接线情况:
SCK:PB3 MISO:PB4 MOSI:PB5 CS:PB14
a.使能时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
b.初始化 SDO/SDI/SCK都是复用自GPIO,CS是不需要复用
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF;
GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_14;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_OUT;
GPIO_Init(GPIOB,&GPIO_InitStruct);
c.复用成什么功能
GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);
4.2 配置SPI控制器 (SPI --APB2)
a.使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
b.初始化SPI控制器
SPI_InitTypeDef结构体介绍
typedef struct
{
uint16_t SPI_Direction;//设置SPI的通信方式
SPI_Direction_2Lines_FullDuplex 全双工 <— 选全双工
SPI_Direction_2Lines_RxOnly 半双工接收
SPI_Direction_1Line_Rx 串行接收
SPI_Direction_1Line_Tx 串行发送 uint16_t SPI_Mode;//设置SPI的主从模式
SPI_Mode_Master 主模式
SPI_Mode_Slave 从模式 uint16_t SPI_DataSize;//设置SPI传输的数据位长度
SPI_DataSize_16b 数据位16位
SPI_DataSize_8b 数据位8位 uint16_t SPI_CPOL;//时钟极性
SPI_CPOL_Low 空闲时时钟线为低
SPI_CPOL_High 空闲时时钟线为高 uint16_t SPI_CPHA;//时钟相位
SPI_CPHA_1Edge 在时钟线的第一个边沿锁定数据
SPI_CPHA_2Edge 在时钟线的第二个边沿锁定数据 uint16_t SPI_NSS;//设置SPI的NSS信号是由硬件控制还是软件控制
SPI_NSS_Soft 由用户软件代码控制
SPI_NSS_Hard 由硬件控制
uint16_t SPI_BaudRatePrescaler;//设置SPI波特率的预分频值
SPI(传输速率) = SPI的输入频率(APB1还是APB2)/SPI_BaudRatePrescaler
SPI_BaudRatePrescaler_2 2分频
SPI_BaudRatePrescaler_4
SPI_BaudRatePrescaler_8
…
SPI_BaudRatePrescaler_256
uint16_t SPI_FirstBit;//指定SPI数据顺序是先发低位还是高位
SPI_FirstBit_MSB 先发高位
SPI_FirstBit_LSB 先发低位 uint16_t SPI_CRCPolynomial;//指定CRC(循环冗余校验)校验多项式 <–课后去拓展
这个系统大于1即可,为了提供通信的可靠性
}SPI_InitTypeDef;
b.1 定义结构体变量
SPI_InitTypeDef SPI_InitStruct;
b.2 结构体成员赋值
SPI_InitStruct.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_4;
SPI_InitStruct.SPI_CPHA=SPI_CPHA_1Edge;
SPI_InitStruct.SPI_CPOL=SPI_CPOL_Low;
SPI_InitStruct.SPI_CRCPolynomial=7;
SPI_InitStruct.SPI_DataSize=SPI_DataSize_8b;
SPI_InitStruct.SPI_Direction=SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_FirstBit=SPI_FirstBit_MSB;
SPI_InitStruct.SPI_NSS=SPI_NSS_Soft;
SPI_InitStruct.SPI_Mode=SPI_Mode_Master;
b.3 写入控制器
SPI_Init(SPI1,&SPI_InitStruct);
4.3 使能SPI
SPI_Cmd(SPI1,ENABLE);
4.4 收发SPI的数据(可以参考USART)
怎么去获取标志位?(假如不在中断里面)
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG); 或者SPI_GetFlagStatus(这是一个宏)
标志位:
#define SPI_I2S_FLAG_RXNE ((uint16_t)0x0001)
#define SPI_I2S_FLAG_TXE ((uint16_t)0x0002)
#define SPI_FLAG_CRCERR ((uint16_t)0x0010)
#define SPI_FLAG_MODF ((uint16_t)0x0020)
#define SPI_I2S_FLAG_OVR ((uint16_t)0x0040)
#define SPI_I2S_FLAG_BSY ((uint16_t)0x0080)
a.读数据:只有RXNE==1
的时候才能读(可以轮询也可以使用中断)
怎么去读取数据?
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
例子: 轮询收
/*
通过SPI接收一个字节的数据
*/
uint8_t SPI1_Recv_a_byte(void)
{
//
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!=SET);
SPI_I2S_SendData(SPI1,0xFF);
/*
为什么要先发送一个字节,才可以开始接收?
因为当MCU没有向SPI上发送数据的时候,总线是空闲的(没有时钟信号的)。
而MCU被动接收数据是不会产生时钟的。
所以为了接收数据,必须先发送一个无效的数据,目的就是为了产生时钟,从而方便接收数据。
*/
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!=SET);
uint8_t ch=SPI_I2S_ReceiveData(SPI1);
return ch;
}
b.发数据:只有当TXE==1
才能发送数据
如何发:
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
例子: 轮询发
/*
SPI1_Send_a_byte:通过SPI1发送一个字节
*/
void SPI1_Send_a_byte(u8 data)
{
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!=SET);//如果TXE被设置,说明TDR中的数据已经被发送出去了
SPI_I2S_SendData(SPI1,data);//当数据没发完,卡在while循环,当数据发完跳出循环,发送data一个字节
/*
当MCU没有向SPI上发送数据的时候,总线是空闲的(没有时钟信号)
当MCU向SPI上发送数据的时候,SCK开始产生时钟信号,SPI开始工作。
因为SPI是全双工的通信,所以在MCU将data通过MOSI引脚发送出去的同时,MCU同时也会通过MISO引脚收到一个字节的数据(这个字节可能是无效的dummy),意思是即使W25Q128没有主动向MCU发送数据,但是MCU上SPI控制器仍然会主动去接收数据,需要处理这个数据。
*/
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!=SET);//如果RXNE被设置,说明RDR中已经存入了1个字节的数据(从对方接收过来的),CPU就可以去读取RDR
u8 ch=SPI_I2S_ReceiveData(SPI1);//当数据没接收完,卡在while循环,当数据接收完跳出循环,接收SPI1过来的数据
}
开中断去接收数据:
跟串口使用一样
5 W25Q128
5.1 接线说明
引脚: CS ----- PB14 CLK ----- PB3 SO ----- PB4 SI ----- PB5
CS:片选引脚(低电平有效)
当CS引脚处于高电平时,芯片处于不选择状态
当CS引脚处于低电平时,芯片处于选择状态(此时才能与MCU通信)
5.2 内部存储结构
内部存储地址是24bits,这24bits分成了以下的四部分:
8bits 4bits 4bits 8bits
block number sector number page number byte number
块地址 扇地址 页地址 字节地址
那么W25Q128共有:
=2^8 =256 block
每块 =2^4 =16 sector
每扇 =2^4 =16 page
每页 =2^8 =256 byte
所以W25Q128的存储的大小 = 256 *16 * 16 *256B = 224=24 * 2^10 * 2^10 B= 16M
反推:假设W25Q128的某个单元的地址为x,该字节位于哪里:
(x>>16)&0xFF 块号
(x>>12)&0xF 扇号
(x>>8)&0xF 页码
x&0xFF 字地址(页内偏移地址)
如果要往W25Q128芯片写入数据的话,需要分成两步:
a. erase 擦除
以块,半块,扇或者整个芯片为单位,清楚除数据
b. program 写入
而W25Q128芯片所有的操作都是通过指令完成。
我们只需要根据指令表发送相应的指令就可以了
5.3 W25Q128的指令
W25Q128BV 共有 34 个基本指令。指令都是以/CS下降沿开始的,表示在发送指令之前,必须对应片选引脚(PB14)拉低才能发送指令。
第一个传输的字节是指令码,指令码起始就是一个十六进制的数。
在DI上传输的数据是在时钟的上升沿被锁存的,也就是在上升沿采样数据–>CPOL和CPHA怎么配置
MSB首先被传输,SPI配置结构体里面的SPI_FirstBit选择SPI_FirstBit_MSB
指令是以/CS上升沿结束的,(MCU将PB14拉高代表指令发送完成)
所有的擦写指令必须在 /CS 拉高之后还有一个8位的时钟间隔,否则前面的擦写指令将被忽略。
(发送擦写指令之后,必须再经过8个SPI时钟周期才能发送下一个指令)。
5.3.1 读取状态寄存器指令(指令码: 0x05/0x35)
读取状态寄存器SR1 – 0x05
读取状态寄存器SR2 – 0x35
MCU向W25Q128发送一个读取状态寄存器的指令完成后,W25就会回复一个字节的数据给MCU
SR1:
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
SRP-0 SEC TB BP2 BP1 BP0 WEL BUSY
BUSY: 为1表示W25Q128繁忙,为0表示空闲
主机在发送指令之前,必须要判断从设备是否忙碌
BUSY == 0,才能发送指令
WEL : 为1表示写使能,可以写入数据
为0表示写禁止,不可以写入数据
/*
等待W25Q128不繁忙
*/
void wait_w25q128_not_busy(void)
{
while(Get_W25Q128_SR1()&0x01);//如果SR1第0bit(BUSY)是1,就在这里等待
}
/*
使能片选,拉低CS
*/
void W25Q128_ENABLE(void)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_14);
}
/*
禁止片选,拉高CS
*/
void W25Q128_DISABLE(void)
{
GPIO_SetBits(GPIOB,GPIO_Pin_14);
}
/*
获取状态寄存器SR1
*/
u8 Get_W25Q128_SR1(void)
{
W25Q128_ENABLE();
SPI1_Send_a_byte(0x05);//发送读取SR1的指令
u8 status = SPI1_Recv_a_byte();
W25Q128_DISABLE();
return status;
//if(status & 0x01 == 1)-->表示繁忙
}
5.3.2 读取W25Q128的ID(指令:0x90) 伪代码
/*
Manufacturer/ 1 2 3 4 5 6
Device ID 0x90 dummy dummy 0x00 (MF7-MF0) (ID7-ID0)
*/
uint16_t Read_W25Q128_ID(void)
{
W25Q128_ENABLE();
SPI1_Send_a_byte(0x90);
SPI1_Send_a_byte(0x00);
SPI1_Send_a_byte(0x00);
SPI1_Send_a_byte(0x00);
u8 idh = SPI1_Recv_a_byte();//制造商ID
u8 idl = SPI1_Recv_a_byte();//设备ID
uint16_t id = idh<<8|idl;
W25Q128_DISABLE();
return id;
//printf("制造商:%x 设备:%x\r\n",id>>8,id&0xFF);
}
5.3.3 读数据指令(指令码:0x03)
指令格式:
1 2 3 4 5 …
0x03 A23-A16 A15-A8 A7-A0 (D7-D0)
指令 |<-- 24bit地址 --> | 回复的第一个字节的数据 …
/*
Read_bytes_from_W25Q128:用来从W25Q128中读取数据
@addr:W25Q128存储器中的存储单元的地址
@data:用来保存你读取到的数据
@count:指定读取的数量,单位为字节
返回值:
失败返回0,成功返回非0
*/
int Read_bytes_from_W25Q128(u32 addr,u8 *data,int count)
{
//判断count是否合法
int max_bytes = (1<<24)-addr;//从addr处最多还能读取多少字节到末尾
count = (count<max_bytes)?count:max_bytes;
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&0xFF); //字节地址 //24bits地址
//循环接收数据
int i;
for(i=0;i<count;i++)
{
data[i]=SPI1_Recv_a_byte();
}
W25Q128_DISABLE();//禁止片选(不想继续接收数据)
}
5.3.4 写入数据(也叫page program 指令:0x02 以页为单位进行写入)
FLASH芯片在写入数据之前,一般要先擦除,所谓的擦除就是将页内所有字节的值全部变成0xFF。
当然在擦除之前,要进行一个写使能(指令: 0x06)。
W25Q128的擦除单位是:
擦除单位: sector(4k) half of block(32k) block(64k) chip(16M)
指令 0x20 0x52 0xD8 0xC7/0x60
所以写入数据的流程:
a.写使能
在erase/program之前,先写使能,才能修改flash中的内容。
b.擦除
在erase/program时,需要时间,所以你在运行的时候需要先判断它是否繁忙
c.program写入
一般的,如果写入的数据不大于(不够)擦除空间,为了使其他的空间的数据不变,一般是要将先要擦除的空间的数据先读取出来保存在数组中,然后将你要写入的数据替换到数组对应的位置,然后把替换掉的数组重新写入W25Q128。
比如:写入0x2C到W25Q128的0x123456地址上。
所以写入的操作为:
先将整个0x123000扇区全部读取出来
u8 data[4096]={0};//定义成全局变量
Read_bytes_from_W25Q128(0x123000,data,0x1000);//读取4K
data[0x456]=0x2C;
先去擦除整个0x123456地址处的扇区
W25Q128_Erase_Sector(0x123000);
//等待擦除完毕
//写入数据
//等待写入完毕
所以写入数据要考虑到两个问题:
1.写入的内容不足一个擦除空间
2.写入的超过一个擦除空间(此时需要多次写入)
/*
Write_bytes_to_W25Q128:用来向W25Q128中写入数据
@addr:W25Q128存储器中的存储单元的地址
@data:用来保存你即将写入的数据
@count:指定写入的数量,单位为字节
返回值:
失败返回0,成功返回非0
*/
int Write_bytes_to_W25Q128(u32 addr,u8 *data,int count)
{
//判断count是否合法 //假如0X123456 -- 0x1234
int max_bytes = (1<<24)-addr;//从addr处最多还能读取多少字节到末尾 //0x1000000-0x123456
count = (count<max_bytes)?count:max_bytes;
u32 first_sector_addr = addr & ~0xFFF;//访问的第一个扇区的地址//0x123000
u32 last_sector_addr = (addr+count)& ~0xFFF;//访问的最后一个扇区的地址//0x12468A-->0x124000
u32 bytes = 0;//记录已经写入的字节数
u32 sector_addr;
for(sector_addr = first_sector_addr;sector_addr<=last_sector_addr;sector_addr+=4096)
{
u8 buffer[4096]={0};//定义成全局变量,记得去改
//1.先将当前扇区的内容全部读取出来
Read_bytes_from_W25Q128(sector_addr,buffer,4096);//0x123456
//2.将要写入的数据替换到数组中对应的位置上去
//先将当前扇区要替换的字节数计算出来
u32 modify_bytes = 4096 - (addr&0xFFF);//4096-0x456
//然后计算出从数组的哪个位置开始替换
u32 addr_offset = addr - sector_addr; //也可以写成 addr &0xFFF //0x123456-0x123000 === -0x124000
u32 i;
//开始替换
for(i=0;i<modify_bytes&&bytes<count;i++,bytes++)
{
buffer[addr_offset+i]=data[bytes];
}
//当前扇区替换完毕后,addr要指向下一个扇区
addr+=modify_bytes;//下一个扇区的首地址
//3.擦除扇区
W25Q128_Erase_Sector(sector_addr);
//4.将此扇区数据重新写入(一次写入256个字节就是一页)
//所以需要一次需要写入16次页写
u32 page_addr = sector_addr;
for(i=0;i<16;i++)
{
W25Q128_page_program(page_addr,&buffer[i*256],256);
page_addr+=256;
}
}
return bytes;
}
/*
W25Q128写使能
*/
void W25Q128_write_enable(void)
{
W25Q128_ENABLE();
SPI1_Send_a_byte(0x06);
W25Q128_DISABLE();
}
/*
W25Q128_Erase_Sector:将sector_addr指向的扇区进行擦除(扇内所有的字节变成0xFF)
*/
void W25Q128_Erase_Sector(u32 sector_addr)
{
W25Q128_write_enable();//写使能
wait_w25q128_not_busy();//等待不繁忙
W25Q128_ENABLE();
SPI1_Send_a_byte(0x20); //擦除数据的指令码
SPI1_Send_a_byte((sector_addr>>16)&0xFF);
SPI1_Send_a_byte((sector_addr>>8)&0xFF);
SPI1_Send_a_byte(sector_addr&0xFF); //24bits地址
W25Q128_DISABLE();
wait_w25q128_not_busy();//等待擦除完成
}
/*
W25Q128_page_program:页写 -->0x02
@page_addr:进行页写的首地址
@data:保存需要写入的数据
@count:指定写入的数量,单位为字节
*/
void W25Q128_page_program(u32 page_addr,u8 *data,int count)
{
//计算count的合理性
int max_bytes = 256-(page_addr&0xFF);
count = (count<max_bytes)?count:max_bytes;
W25Q128_write_enable();//写使能
wait_w25q128_not_busy();//等待不繁忙
W25Q128_ENABLE();
SPI1_Send_a_byte(0x02); //读取数据的指令码
SPI1_Send_a_byte((page_addr>>16)&0xFF);
SPI1_Send_a_byte((page_addr>>8)&0xFF);
SPI1_Send_a_byte(page_addr&0xFF); //24bits地址
int i;
for(i=0;i<count;i++)
{
SPI1_Send_a_byte(data[i]);
}
W25Q128_DISABLE();
wait_w25q128_not_busy();//等待页写完成
}
/*ge_addr:进行页写的首地址
@data:保存需要写入的数据
@count:指定写入的数量,单位为字节
*/
void W25Q128_page_program(u32 page_addr,u8 *data,int count)
{
//计算count的合理性
int max_bytes = 256-(page_addr&0xFF);
count = (count<max_bytes)?count:max_bytes;
W25Q128_write_enable();//写使能
wait_w25q128_not_busy();//等待不繁忙
W25Q128_ENABLE();
SPI1_Send_a_byte(0x02); //读取数据的指令码
SPI1_Send_a_byte((page_addr>>16)&0xFF);
SPI1_Send_a_byte((page_addr>>8)&0xFF);
SPI1_Send_a_byte(page_addr&0xFF); //24bits地址
int i;
for(i=0;i<count;i++)
{
SPI1_Send_a_byte(data[i]);
}
W25Q128_DISABLE();
wait_w25q128_not_busy();//等待页写完成
}