目录
一、SPI协议简介
SPI(Serial Peripheral Interface),串行外围设备接口,是一种高速全双工的通信总线。它被广泛使用在ADC、LCD等设备与MCU间、要求通讯速率较高的场合。
1.1、SPI物理层
![](https://i-blog.csdnimg.cn/blog_migrate/15eee1d7cbfd5b6bb3c258385a4adeb1.png)
- Slave Select:从设备选择信号线,常称为片选信号线,也称为NSS、CS。 当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、MOSI 及 MISO 同时并联到相 同的 SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;而每个从设备都有独立的 这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,接着主机开始与从设备进行SPI通讯,即SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号。
- SCK:时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,两个设备之间通讯时,通讯速率受限于低速设备。
- MOSI:主出从入,即数据从主机到从机。
- MISO:主入从出,即数据从从机到主机。
1.2、协议层
![](https://i-blog.csdnimg.cn/blog_migrate/d0d03a6d12510f1de578ead4ea46450c.png)
1. 通讯的起始和停止信号:
片选信号线置低电平为开始,拉高为结束信号;
2. 数据有效性:
SCK 的下降沿时刻,MOSI 及 MISO 的数据有效。SCK每个时钟周期MOSI、MISO传输一位数据,且数据输入输出是同时进行的。数据传输时SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。
3. CPOL/CPHA及通讯模式:
![](https://i-blog.csdnimg.cn/blog_migrate/302d37d923fb1121779413432c2a3dbb.png)
时钟极性CPOL是指SPI通讯设备处于空闲状态(通讯开始之前)时,SCK信号线的电平信号。CPOL=0 时,SCK 在空闲状态时为低电平,CPOL=1,则相反。
时钟相位CPHA是指数据的采样时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样,当CPHA=1时,数据线在SCK的“偶数边沿”采样。
二、STM32的SPI架构剖析
STM32 的 SPI 外设可用作通讯的主机及从机,支持最高的 SCK 时钟频率为 fpclk/2 (STM32F103 型 号的芯片默认 fpclk1 为 36MHz,fpclk2 为 72MHz),完全支持 SPI 协议的 4 种模式,数据帧长度可设置为 8 位或 16 位,可设置数据 MSB 先行或 LSB 先行。它还支持双线全双工、双线单向以及单线模式。
1. 时钟控制逻辑:
SCK线的时钟信号,由波特率发生器根据”控制寄存器CR1“中的BR[0:2]位控制,该位时对fplck时钟的分频因子 ,对 fpclk 的分频结果就是 SCK 引脚的输出时钟频率。
1.SPI初始化结构体
typedef struct
{
uint16_t SPI_Direction; /* 设置 SPI 的单双向模式 */
uint16_t SPI_Mode; /* 设置 SPI 的主/从机端模式 */
uint16_t SPI_DataSize; /* 设置 SPI 的数据帧长度,可选 8/16 位 */
uint16_t SPI_CPOL; /* 设置时钟极性 CPOL,可选高/低电平 */
uint16_t SPI_CPHA; /* 设置时钟相位,可选奇/偶数边沿采样 */
uint16_t SPI_NSS; /* 设置NSS引脚由SPI硬件控制还是软件控制 */
uint16_t SPI_BaudRatePrescaler; /* 设置时钟分频因子,fpclk/分频数 =fSCK */
uint16_t SPI_FirstBit; /* 设置 MSB/LSB 先行 */
uint16_t SPI_CRCPolynomial; /* 设置 CRC 校验的表达式 */
} SPI_InitTypeDef;
- SPI_Direction:设置SPI的通讯方向,可设置为双线全(SPI_Direction_2Lines_FullDuplex)、双线只接收(SPI_Direction_2Lines_RxOnly)、单线只接收(SPI_Direction_1Line_Rx)、单线只发送(SPI_Direction_1Line_Tx)模式;
- SPI_Mode:设置SPI工作在主机模式或从机模式,这两个模式 的最大区别为 SPI 的 SCK 信号线的时序来源。
三、SPI——读写串行FLASH实验
FLSAH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH 存储器容量普遍大于 EEPROM,现在基本取代了它的地位。 FLASH 芯片只能一大片一大片地擦写,EEPROM 可以单个字节擦写。
流程:
- 初始化通讯使用的目标引脚及端口时钟;
- 使能 SPI 外设的时钟;
- 配置 SPI 外设的模式、地址、速率等参数并使能 SPI 外设;
- 编写基本 SPI 按字节收发的函数;(通过发送读ID指令获取设备ID)
- 编写对 FLASH 擦除及读写操作的的函数;(FLASH在写入前需要进行擦除操作)
- 编写测试程序,对读写数据进行校验。
3.1、spi.h中进行FLASH操作的指令宏定义:
![](https://i-blog.csdnimg.cn/blog_migrate/f424df7c9944fcc63216eefd9d17e586.png)
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
#define sFLASH_ID 0XEF4017
#define Dummy_Byte 0XFF
#define SPI_FLASH_PageSize 256
#define SPI_FLASH_PerWritePageSize 256
#define SPI1_TIME_OUT 1000
3.2、声明应用的操作函数
void SPI_FLASH_SectorErase(uint32_t SectorAddr);
uint32_t SPI_Flash_ReadID(void);
void SPI_Flash_Erase_Chip(void);
void SPI_Flash_Read(uint32_t ReadAddr,uint16_t NumByteToRead,uint8_t* pBuffer);
void SPI_Flash_Write(uint32_t WriteAddr,uint16_t NumByteToWrite,uint8_t* pBuffer);
void SPI_Flash_Write_Page(uint32_t WriteAddr,uint16_t NumByteToWrite,uint8_t* pBuffer);
3.3、在spi.c中实现读写FLASH相关函数
3.3.1、对片选进行宏定义
// 使用了PC0引脚,作为SPI使能信号
#define SPI_FLASH_CS_H() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET)
#define SPI_FLASH_CS_L() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET)
3.3.2、读写字节函数
/******************************************************
* function:SPI读一个数据
* return:返回通过SPIx接收的数据
******************************************************/
uint8_t SPI1_ReadByte(void)
{
uint8_t RxData;
HAL_SPI_Receive(&hspi1, &RxData, 1, SPI1_TIME_OUT);
return RxData;
}
/******************************************************
* function:SPI写一个数据
******************************************************/
void SPI1_WriteByte(uint8_t TxData)
{
HAL_SPI_Transmit(&hspi1, &TxData, 1, SPI1_TIME_OUT);
}
3.3.3、FLASH的写使能和非使能
/**************************************************
* function:SPI_FLASH写使能,将WEL置位
***************************************************/
void SPI_FLASH_Write_Enable(void)
{
SPI_FLASH_CS_L(); //使能器件
SPI1_WriteByte(W25X_WriteEnable); //发送写使能
SPI_FLASH_CS_H(); //取消片选
}
/*****************************************************
* function:SPI_FLASH写禁止,将WEL清零
*****************************************************/
void SPI_FLASH_Write_Disable(void)
{
SPI_FLASH_CS_L(); //使能器件
SPI1_WriteByte(W25X_WriteDisable); //发送写禁止指令
SPI_FLASH_CS_H(); //取消片选
}
3.3.4、读取SPI_FLASH的状态寄存器,判断FLASH的状态
/*******************************************
* function:读取SPI_FLASH的状态寄存器
********************************************/
// BIT7 6 5 4 3 2 1 0
// SPR RV TB BP2 BP1 BP0 WEL BUSY
// SPR:默认0,状态寄存器保护位,配合WP使用
// TB,BP2,BP1,BP0:FLASH区域写保护设置
// WEL:写使能锁定
// BUSY:忙标记位(1,忙; 0,空闲)
// 默认:0x00
uint8_t SPI_Flash_ReadSR(void)
{
uint8_t byte=0;
SPI_FLASH_CS_L(); // 使能器件
SPI1_WriteByte(W25X_ReadStatusReg); // 发送读取状态寄存器命令
byte = SPI1_ReadByte(); // 读取一个字节
SPI_FLASH_CS_H(); // 取消片选
return byte;
}
/************************************************
* function:等待空闲
************************************************/
void SPI_Flash_Wait_Busy(void)
{
while ((SPI_Flash_ReadSR()&0x01)==0x01); // 等待BUSY位清空
}
3.3.5、读取芯片ID,判断FLASH是否正常
/*******************************************
*function:读取芯片ID W25Q64的ID
*******************************************/
uint32_t SPI_Flash_ReadID(void)
{
uint32_t Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
SPI_FLASH_Write_Enable();
SPI_FLASH_CS_L();
SPI1_WriteByte(W25X_JedecDeviceID); //发送读取ID命令
Temp0 = SPI1_ReadByte();
Temp1 = SPI1_ReadByte();
Temp2 = SPI1_ReadByte();
/* 把数据组合起来,作为函数的返回值 */
Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
SPI_FLASH_CS_H();
return Temp;
}
3.3.6、FLASH扇区擦除函数(FLASH在写之前一定要进行擦除,否则不能正常写入)
![](https://i-blog.csdnimg.cn/blog_migrate/20f174ca77753812bf8b8f989c000171.png)
/**********************************************************
* function:擦除整个芯片
**********************************************************/
//整片擦除时间:
//W25X16:25s
//W25X32:40s
//W25X64:40s
//等待时间超长...
void SPI_Flash_Erase_Chip(void)
{
SPI_FLASH_Write_Enable(); //SET WEL
SPI_Flash_Wait_Busy();
SPI_FLASH_CS_L(); //使能器件
SPI1_WriteByte(W25X_ChipErase); //发送片擦除命令
SPI_FLASH_CS_H(); //取消片选
SPI_Flash_Wait_Busy(); //等待芯片擦除结束
}
/**********************************************************
* function:SPI_FLASH_SectorErase
* annotation:Flash的一个扇区是4K,所以输入的地址要和4K对其。
**********************************************************/
void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
SPI_FLASH_Write_Enable();
SPI_Flash_Wait_Busy();
SPI_FLASH_CS_L();
SPI1_WriteByte(W25X_SectorErase);
SPI1_WriteByte((SectorAddr & 0xFF0000) >> 16);
SPI1_WriteByte((SectorAddr & 0xFF00) >> 8);
SPI1_WriteByte(SectorAddr & 0xFF);
SPI_FLASH_CS_H();
SPI_Flash_Wait_Busy();
}
3.3.7、读取FLASH函数
/**********************************************
* function:读取SPI FLASH
**********************************************/
// 在指定地址开始读取指定长度的数据
// pBuffer:数据存储区
// ReadAddr:开始读取的地址(24bit)
// NumByteToRead:要读取的字节数(最大65535)
void SPI_Flash_Read(uint32_t ReadAddr,uint16_t NumByteToRead,uint8_t* pBuffer)
{
uint16_t i;
SPI_FLASH_CS_L(); //使能器件
SPI1_WriteByte(W25X_ReadData); //发送读取命令
SPI1_WriteByte((uint8_t)((ReadAddr)>>16)); //发送24bit地址
SPI1_WriteByte((uint8_t)((ReadAddr)>>8));
SPI1_WriteByte((uint8_t)ReadAddr);
for(i=0;i<NumByteToRead; i++)
{
pBuffer[i] = SPI1_ReadByte(); //循环读数
}
SPI_FLASH_CS_H(); //取消片选
}
3.3.8、FLASH的按页写入,写入不是随机,可以任意写入、按页写入,一页最大数据是256字节
/************************************************************
* function:SPI在一页(0~65535)内写入少于256个字节的数据
* annotation:一页最大256个字节
************************************************************/
// 在指定地址开始写入最大256字节的数据
// pBuffer:数据存储区
// WriteAddr:开始写入的地址(24bit)
// NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void SPI_Flash_Write_Page(uint32_t WriteAddr,uint16_t NumByteToWrite,uint8_t* pBuffer)
{
uint16_t i;
SPI_FLASH_Write_Enable(); // SET WEL
SPI_FLASH_CS_L(); // 使能器件
SPI1_WriteByte(W25X_PageProgram); // 发送写页命令
SPI1_WriteByte((uint8_t)((WriteAddr)>>16)); // 发送24bit地址
SPI1_WriteByte((uint8_t)((WriteAddr)>>8));
SPI1_WriteByte((uint8_t)WriteAddr);
for(i=0;i<NumByteToWrite;i++) SPI1_WriteByte(pBuffer[i]); // 循环写数
SPI_FLASH_CS_H(); // 取消片选
SPI_Flash_Wait_Busy(); // 等待写入结束
}
/***************************************************************
*@function 不定量的写入数据,先确保写入前擦出扇区
*@annotation
*@param
*@retval
****************************************************************/
void SPI_Flash_Write(uint32_t WriteAddr, uint16_t NumByteToWrite, uint8_t* pBuffer)
{
uint8_t NumOfPage=0, NumOfSingle=0, Addr=0, count=0, temp=0;
/* 计算Addr,写入的地址是否和PageSize对齐 */
Addr = WriteAddr % SPI_FLASH_PageSize;
/* count 为剩余的地址 */
count = SPI_FLASH_PageSize - Addr;
/* 计算能写入多少整数页 */
NumOfPage = NumByteToWrite % SPI_FLASH_PageSize;
/* 计算不满一页的数据 */
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* Addr=0,则 WriteAddr 刚好按页对齐 aligned */
if(Addr == 0)
{
/* NumByteToWrite < SPI_FLASH_PageSize,一页能写完 */
if(NumOfPage == 0)
{
SPI_Flash_Write_Page(WriteAddr,NumByteToWrite,pBuffer);
}
else/* NumByteToWrite > SPI_FLASH_PageSize,一页写不完,先写整数页,在写剩下的 */
{
/*先把整数页都写了*/
while (NumOfPage--)
{
SPI_Flash_Write_Page(WriteAddr,SPI_FLASH_PageSize,pBuffer);
WriteAddr+=SPI_FLASH_PageSize; // flash 的地址加一页的大小
pBuffer+=SPI_FLASH_PageSize; // 写缓存数据的地址加一页的大小
}
/*若有多余的不满一页的数据,把它写完*/
SPI_Flash_Write_Page(WriteAddr,NumOfSingle,pBuffer);
}
}
else/* 若地址与 SPI_FLASH_PageSize 不对齐 */
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
/*当前页剩余的 count 个位置比 NumOfSingle 小,一页写不完*/
if(NumOfSingle >count)
{
temp = NumOfSingle -count;
/*先写满当前页*/
SPI_Flash_Write_Page(WriteAddr,count,pBuffer);
WriteAddr += count;
pBuffer += count;
/*再写剩余的数据*/
SPI_Flash_Write_Page(WriteAddr,temp,pBuffer);
}
else/*当前页剩余的 count 个位置能写完 NumOfSingle 个数据*/
{
SPI_Flash_Write_Page(WriteAddr,NumByteToWrite,pBuffer);
}
}
else/* NumByteToWrite > SPI_FLASH_PageSize */
{
/*地址不对齐多出的 count 分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* 先写完 count 个数据,为的是让下一次要写的地址对齐 */
SPI_Flash_Write_Page(WriteAddr,count,pBuffer);
/* 接下来就重复地址对齐的情况 */
WriteAddr += count;
pBuffer += count;
/*把整数页都写了*/
while (NumOfPage--)
{
SPI_Flash_Write_Page(WriteAddr,SPI_FLASH_PageSize,pBuffer);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
if (NumOfSingle != 0)
{
SPI_Flash_Write_Page(WriteAddr,NumOfSingle,pBuffer);
}
}
}
}
3.4、main.c中对FLASH的读写地址进行宏定义
#define FLASH_WriteAddress 0x00000
#define FLASH_ReadAddress FLASH_WriteAddress
#define FLASH_SectorToErase FLASH_WriteAddress
#define FLASH_SPI hspi1
3.5、main.c 中定义变量
uint32_t flash_ID = 0;
/* 获取缓冲区的长度 */
#define countof(a) (sizeof(a) / sizeof(*(a)))
uint8_t Tx_Buffer[] = "现在进行FLASH的读写测试\r\n";
#define BufferSize (countof(Tx_Buffer)-1)
uint8_t Rx_Buffer[BufferSize];
3.6、main函数中进行测试
printf("*************this is test for coding...**********\t\n");
printf("this is test code for spi1 read and write flash w25Q64 \r\n");
flash_ID = SPI_Flash_ReadID();
printf("\r\n flash ID is 0X%x\r\n",flash_ID);
SPI_FLASH_SectorErase(FLASH_SectorToErase);
SPI_Flash_Write(FLASH_WriteAddress,BufferSize,Tx_Buffer);
printf("\r\n 写入的数据为:%s \r\t", Tx_Buffer);
SPI_Flash_Read(FLASH_ReadAddress,BufferSize,Rx_Buffer);
printf("\r\n 读的数据为:%s \r\t", Rx_Buffer);
/* 检查写入的数据与读出的数据是否相等 */
if(memcmp(Tx_Buffer, Rx_Buffer, BufferSize)==0)
{
printf("写入的和读出的数据是正常的!\r\n");
}
printf("SPI 试验结束......!\r\n");
printf("*************this is test end....**********\t\n");
3.7、函数重定向,使用串口调试助手
/***********************************************
* @brief 重定向c库函数printf到USARTx
* @retval None
**********************************************/
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/***********************************************
* @brief 重定向c库函数getchar,scanf到USARTx
* @retval None
**********************************************/
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}
总结
SPI的使用流程:cubeMX配置->指令表定义->读写FLASH函数定义->获取设备ID->擦除目标存储地址(扇区擦除、块擦除和整片擦除)->写入FLASH(页面写入、不定量写入)->读取FLASH