本文的大部分内容来自B站up主 江协科技, 此文只供本人学习记录用途, 侵删
一、SPI
- SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
- 四根通信线:
- SCK(Serial Clock) 时钟线
- MOSI(Master Output Slave Input) 主机接收从机发送
- MISO(Master Input Slave Output) 主机发送从机接收
- SS(Slave Select) 从机选择
- 同步,全双工支持总线挂载多设备(一主多从)
- 大部分只当从机的设备, MOSI可能会叫DI, MISO叫DO, SS叫CS
I2C协议v2.1规定了100K,400K和3.4M三种速率(bps)。
SPI是一种事实标准,由Motorola开发,并没有一个官方标准。已知的有的器件SPI已达到50Mbps。
二、SPI硬件电路与通讯原理
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
- 在从机未被选中时, MISO必须配置为高阻态(防止电源短路)
交换一个字节的基本原理(模式一):
SPI的发送接收的 基本单元就是数据交换, 时钟由主机的波特率发生器提供, 并接到从机
- 当时钟上升沿来临时, 主机移出最高位放到MOSI, 从机移出最高位到MISO
- 当时钟下降沿来临时, 主机接收MISO的数据放到最低位 , 从机接收MOSI的数据放到最低位
经历了8个循环, 主机和从机移位寄存器中的数据就被交换了
- 当只想发送数据时, 只管把数据放到主机, 然后移位, 不用管接收的数据
- 当只想接收数据时, 随便写个00或者FF到主机, 移位后读取接收的数据就行
三、SPI基本时序
SPI有两个可配置的位,:
-
CPOL 决定了空闲时SS是高电平还是低电平 CPOL =1代表高电平 CPOL =0代表低电平
-
CPHA :
CPHA =0 SCK第一个边沿移入数据,第二个边沿移出数据
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
假设CPOL =1
起始条件和终止条件就是这样:
接下来看看两个可配置位的四种模式组合:
模式0下发送0x06 ( 0 0 0 0 0 1 1 0 ):
四、W25QXX
- W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
- 存储介质:Nor Flash(闪存)
- 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
- 存储容量(24位地址):
- W25Q40: 4Mbit / 512KByte
- W25Q80: 8Mbit / 1MByte
- W25Q16: 16Mbit / 2MByte
- W25Q32: 32Mbit / 4MByte
- W25Q64: 64Mbit / 8MByte
- W25Q128: 128Mbit / 16MByte
- W25Q256: 256Mbit / 32MByte
本博客使用的是W25Q64, 注意本博客的重点是SPI通信协议, 至于发送的命令是什么含义, 只在注释中简单解释, 不做详解
五、软件SPI代码
控制W25Q64实现读取ID号, 写入数据, 读出数据等操作
首先是软件SPI(模式1)的代码:
#include "stm32f10x.h" // Device header
void MYSPI_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0 | GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //PB0(SS) PB10(SCK) PB11(MOSI)推挽输出
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1;
GPIO_Init(GPIOB,&GPIO_InitStructure);//PB1(MISO) 浮空输入
SS(1); //闲置SS
SCK(0); //关闭时钟
}
//以下为四个写(SS SCK MOSI)或读(MISO) SPI的四条通讯线的函数
void SS(uint8_t flag){GPIO_WriteBit(GPIOB,GPIO_Pin_0,(BitAction)flag);}
void SCK(uint8_t flag){GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)flag);}
void MOSI(uint8_t flag){GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)flag);}
uint8_t MISO(void){return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1);}
//SPI通讯 开始和结束的函数 开始时拉低SS 结束时拉高SS
void MYSPI_Start(void){SS(0);}
void MYSPI_Over(void){SS(1);}
//SPI交换一个字节, 注意这里没有完全模拟出一个移位寄存器同时收发的场景, 而是用了两个变量存放
uint8_t MYSPI_Swap(uint8_t sendByte){
uint8_t receiveByte=0x00;
//循环8次
for(uint8_t i = 0;i < 8;i++){
//主机将sendByte的数据移动到MOSI上
MOSI(sendByte & (0x80>>i) );
SCK(1);
//读取MISO的数据放到receiveByte上
if( MISO() == SET)
receiveByte | = 0x80>>i;
SCK(0);
}
return receiveByte;
}
驱动W25Q64
#include "stm32f10x.h" // Device header
#include "MYSPI.h"
//初始化
void W25Q64_Init(void){
MYSPI_Init();
}
//读取厂商ID和设备ID
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID){
MYSPI_Start();
MYSPI_Swap(0x9f);
*MID=MYSPI_Swap(0);
*DID=MYSPI_Swap(0);
*DID<<=8;
*DID|=MYSPI_Swap(0);
MYSPI_Over();
}
//写使能
void W25Q64_WriteEnable(void){
MYSPI_Start();
MYSPI_Swap(0x06);
MYSPI_Over();
}
//等待忙状态结束
void W25Q64_WaitBusy(void){
MYSPI_Start();
MYSPI_Swap(0x05);
while((MYSPI_Swap(0)&0x01)==1);
MYSPI_Over();
}
//页编程(想指定地址写入数据 输入长度可以连续搬运)
void W25Q64_PageProgram(uint32_t Address,uint8_t * Data,uint8_t length){
W25Q64_WriteEnable();
MYSPI_Start();
MYSPI_Swap(0x02);
MYSPI_Swap(Address>>16);
MYSPI_Swap(Address>>8);
MYSPI_Swap(Address);
for(int i=0;i<length;i++)MYSPI_Swap(Data[i]);
MYSPI_Over();
W25Q64_WaitBusy();
}
//擦除扇区数据
void W25Q64_SectorErase(uint32_t Address){
MYSPI_Start();
MYSPI_Swap(0x20);
MYSPI_Swap(Address>>16);
MYSPI_Swap(Address>>8);
MYSPI_Swap(Address);
MYSPI_Over();
}
//读取数据
uint8_t W25Q64_ReadData(uint32_t Address){
uint8_t receiveData;
MYSPI_Start();
MYSPI_Swap(0x03);
MYSPI_Swap(Address>>16);
MYSPI_Swap(Address>>8);
MYSPI_Swap(Address);
receiveData= MYSPI_Swap(0);
MYSPI_Over();
return receiveData;
}
main函数中的使用
uint8_t Data[2] = {0x81,0x66}; //待写入地址
W25Q64_SectorErase(0x000001); //写入前先擦除
W25Q64_PageProgram(0x000001,Data,2); //向地址1 2 写入0x81 0x66
六、STM32中的SPI外设
- STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
- 可配置8位/16位数据帧、高位先行/低位先行时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
- 支持多主机模型、主或从操作可精简为半双工/单工通信
- 支持DMA
- 兼容I2S协议
- STM32F103C8T6 硬件SPI资源:SPI1、SPI2
看到简易框图, 还是TDR, RDR, TXE, RXNE 老熟人了
交换一个字节:
- 数据写入TDR, 如果TXE=1,直接进入移位寄存器开始移位, 移位过程中RXNE=0 TXE=0
- 交换完成后, 移位寄存器中的数据进入RDR, 并将RXNE=1
七、硬件SPI驱动W25Q64
#include "stm32f10x.h" // Device header
void W25Q64_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure); //PA5(SPI1_SCK) PA7(SPI1_MOSI) 复用推挽
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
GPIO_Init(GPIOA,&GPIO_InitStructure);//PA6(SPI1_MISO) 浮空输入
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;
GPIO_Init(GPIOA,&GPIO_InitStructure);//PA4(SPI1_NSS) SS还是GPIO方便点
SPI_InitTypeDef SPI_InitStructure;
SPI_StructInit(&SPI_InitStructure);//初始化用不到的参数
SPI_InitStructure.SPI_Mode=SPI_Mode_Master;
SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;
SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;
SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_128;
SPI_InitStructure.SPI_CPOL=SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA=SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS=SPI_NSS_Soft;
/*
SPI_Mode_Master; 设置单片机为主机
SPI_Direction_2Lines_FullDuplex; 裁剪引脚的参数 双线全双工(不裁剪)
SPI_DataSize_8b; 8位数据帧
SPI_FirstBit_MSB; 高位先行(LSB是低位先行)
SPI_BaudRatePrescaler_128; SCK的分频系数(72MHz/128)
SPI_CPOL_Low; CPOL=0
SPI_CPHA_1Edge; CPHA=1
(配置成了模式1)
SPI_NSS_Soft; SS用GPIO软件模拟
*/
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1,ENABLE);//使能SPI1
}
//开始和结束的函数 开始时拉低SS 结束时拉高SS
void W25Q64_Start(void){GPIO_ResetBits(GPIOA,GPIO_Pin_4);}
void W25Q64_Over(void){GPIO_SetBits(GPIOA,GPIO_Pin_4);}
//交换字节
uint8_t W25Q64_Swap(uint8_t sendByte){
uint8_t receiveByte;
//等待TXE=1并发送数据
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) !=SET);
SPI_I2S_SendData(SPI1, sendByte);
//等待RXNE=1并接收数据
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) !=SET);
receiveByte=SPI_I2S_ReceiveData(SPI1);
return receiveByte;
}
//以下代码功能和软件SPI驱动的版本完全相同 不做解释
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID){
W25Q64_Start();
W25Q64_Swap(0x9f);
*MID=W25Q64_Swap(0);
*DID=W25Q64_Swap(0);
*DID<<=8;
*DID|=W25Q64_Swap(0);
W25Q64_Over();
}
void W25Q64_WriteEnable(void){
W25Q64_Start();
W25Q64_Swap(0x06);
W25Q64_Over();
}
void W25Q64_WaitBusy(void){
W25Q64_Start();
W25Q64_Swap(0x05);
while((W25Q64_Swap(0)&0x01)==1);
W25Q64_Over();
}
void W25Q64_PageProgram(uint32_t Address,uint8_t * Data,uint8_t length){
W25Q64_WriteEnable();
W25Q64_Start();
W25Q64_Swap(0x02);
W25Q64_Swap(Address>>16);
W25Q64_Swap(Address>>8);
W25Q64_Swap(Address);
for(int i=0;i<length;i++){
W25Q64_Swap(Data[i]);
}
W25Q64_Over();
W25Q64_WaitBusy();
}
void W25Q64_SectorErase(uint32_t Address){
W25Q64_Start();
W25Q64_Swap(0x20);
W25Q64_Swap(Address>>16);
W25Q64_Swap(Address>>8);
W25Q64_Swap(Address);
W25Q64_Over();
}
uint8_t W25Q64_ReadData(uint32_t Address){
uint8_t receiveData;
W25Q64_Start();
W25Q64_Swap(0x03);
W25Q64_Swap(Address>>16);
W25Q64_Swap(Address>>8);
W25Q64_Swap(Address);
receiveData= W25Q64_Swap(0);
W25Q64_Over();
return receiveData;
}