SPI读写FLASH原理和完整代码详解
引言
实现SPI通讯,对FLASH进行读写。读取FLASH的ID信息,写入数据,并读取出来进行校验,通过串口打印写入与读取出来的数据,输出测试结果。
一、SPI总线
SPI通信的基础知识
SPI是串行外设接口(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议,最大SPI速度可达到18MHz 。
通常SPI通过4个管脚与外部器件相连:
MISO:主设备输入/从设备输出管脚。
MOSI:主设备输出/从设备输入管脚。
SCK:串口时钟,作为主设备的输出,从设备的输入。
NSS:从设备选择。这是一个可选的管脚,用来选择主/从设备。它的功能是用来作为“片选管脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。
时钟信号的相位和极性
SPI_CR寄存器的CPOL和CPHA位,能够组合成四种可能的时序关系,其中使用的最为广泛的是SPI0和SPI3方式。
SPI模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI主模块和与之通信的外设音时钟相位和极性应该一致。
这些时序体现了SPI_CR1寄存器的LSBFIRST被重置(置0)时的情况,即MSB模式。
SPI模式 |
CPOL |
CPHA |
空闲时SCK时钟 |
采样时刻 |
0 |
0 |
0 |
低电平 |
奇数边沿 |
1 |
0 |
1 |
低电平 |
偶数边沿 |
2 |
1 |
0 |
高电平 |
奇数边沿 |
3 |
1 |
1 |
高电平 |
偶数边沿 |
SPI主模式配置
在主配置时,串行时钟在SCK脚产生,学会使用寄存器,了解底层逻辑(用库函数开发也会更加明了),配置步骤如下:
1、通过SPI_CR1寄存器的BR[2:0]位定义串行时钟波特率。
SPI控制寄存器 1(SPI_CR1)
2、选择CPOL和CPHA位,定义数据传输和串行时钟间的相位关系。
3、设置DFF位来定义8或16位数据帧格式。
4、配置SPI_CR1寄存器的LSBFIRST位定义帧格式。
5、如果NSS引脚需要工作在输入模式,硬件模式中在整个数据帧传输期间应把NSS脚连接到高 电平;在软件模式中,需设置SPI_CR1寄存器的SSM和SSI位。
6、必须设置MSTR和SPE位(只当NSS脚被连到高电平,这些位才能保持置位)。在这个配置中,MOSI脚是数据输出,而MISO脚是数据输入。
举例:
void SPI1_Init(void)
{
RCC->APB2ENR|=1<<2; //PORTA时钟使能
RCC->APB2ENR|=1<<12; //SPI1时钟使能
GPIOA->CRL&=0X000FFFFF; //PA5/6/7清零
GPIOA->CRL|=0XBBB00000; //PA5/6/7复用
GPIOA->ODR|=0X7<<5; //PA5.6.7上拉
GPIOA->CRL|=0X000000300; //PA2 SPI主设备时输出
SPI1->CR1|=0<<10; //全双工模式
SPI1->CR1|=1<<9; //软件NSS管理
SPI1->CR1|=1<<8;
SPI1->CR1|=1<<2; //SPI主机
SPI1->CR1|=0<<11; //8bit数据格式
SPI1->CR1|=1<<1; //空闲模式下SCK为1 CPOL=1
SPI1->CR1|=1<<0; //数据采样从第二个时间边沿开始,CPHA=1
SPI1->CR1|=2<<5; //Fpclk1/8
SPI1->CR1|=0<<7; //先发送MSB
SPI1->CR1|=1<<6; //SPI设备使能
}
数据发送过程
当一字节写进发送缓冲器时,发送过程开始。在发送第一个数据位时,数据字被并行地(通过内部总线)传入移位寄存器,而后串行地移出到MOSI脚上;MSB在先还是LSB在先,取决于SPI_CR1寄存器中的LSBFIRST位。如果设置SPI_CR2寄存器中的TXEIE位,将产生中断。
SPI控制寄存器 2(SPI_CR2)
数据接收过程
对于接收器来说,当发送完一帧数据的时候,“状态寄存器SR”中的“TXE标志位”会被置1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE标志位”会被置1,表示传输完一帧,接收缓冲区非空;读SPI_DR寄存器时,将清除RXNE位,SPI设备返回接收到的数据。
SPI状态寄存器(SPI_SR)
代码:
//SPI1 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
uint8_t SPI_FLASH_SendByte(uint8_ TxData)
{
uint16_t retry=0;
while((SPI2->SR&1<<1)==0) //等待发送区空
{
retry++;
if(retry>=0XFFFE)return 0; //超时退出
}
SPI1->DR=TxData; //发送一个byte
retry=0;
while((SPI2->SR&1<<0)==0) //等待接收完一个byte
{
retry++;
if(retry>=0XFFFE)return 0; //超时退出
}
return SPI2->DR; //返回收到的数据
}
二、FLASH控制
本文使用的FLASH与SPI总线的硬件连接如下图所示。
软件方面FLASH定义了很多的指令,确定通讯协议模块,通过协议收发命令、数据,进而驱动设备,因此首先需要了解芯片时序,通过熟悉底层逻辑从而编写驱动,也能为日后学习打下坚实的基础。下面仔细罗列为FLASH其中读取厂商ID的驱动实现步骤,其他驱动见文后代码清单。
FLASH指令集
读取Device ID时序图
通过结合指令中厂商ID所需指令和时序图,拉低片选,使能FLASH,利用用户函数SPI_FLASH_SendByte()来向FLASH发送第一个命令字节编码:“W25X_DeviceID”(自定义的宏:0xAB);根据指令表,发送完这个指令后,后面紧跟着三个字节的“Dummy byte”,可以自定义任意值,这里定义为“0xFF”,根据时序,在第5个字节,FLASH通过SO端口输出它的器件ID,我们调用函数SPI_FLASH_SendByte()接收返回的数据,并赋值给Temp变量。SPI_FLASH_ReadDeviceID()函数的返回值即为读取得的器件 ID。把片选信号拉高,结束通讯。这就完成了读FLASH 厂商ID。
代码:
uint32_t SPI_FLASH_ReadDeviceID(void)
{
uint32_t Temp=0;
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(W25X_DeviceID); //发送读取指令
SPI_FLASH_SendByte(Dummy_Byte); //三个Dummy_Byte
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
Temp = SPI_FLASH_SendByte(Dummy_Byte); //接收返回值
SPI_FLASH_CS_HIGH();
return Temp;
}
编译调试后获得厂商ID,与下表进行对比,确定芯片选择正确,W25Q16厂商ID为0x14。
制造厂商和设备ID
三、代码编写
main.c
#define FLASH_WriteAddress 0x00000
#define FLASH_ReadAddress FLASH_WriteAddress
#define FLASH_SectorToErase FLASH_WriteAddress
//#define countof(a) (sizeof(a) / sizeof(*(a)))
//#define BufferSize (countof(Tx_Buffer)-1)
#define BufferSize 255
uint8_t Tx_Buffer[255]={0};
//uint8_t Tx_Buffer[] = "士不可以不弘毅,任重而道远\r\n";
uint8_t Rx_Buffer[BufferSize];
int main()
{
uint16_t DeviceID;
uint16_t FlashID;
int cnt=0;
USART1_GPIO_Config();
USART1_Config();
SPI_FLASH_Init();
printf("\r\n 这是一个 2M 串行 flash(W25X16)实验 \r\n");
/* 获得 Flash Device ID */
DeviceID = SPI_FLASH_ReadDeviceID();
DelayMS(200);
/* 获得 Flash ID */
FlashID = SPI_FLASH_ReadID();
printf("\r\n FlashID is 0x%X, Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);
/* 擦除Flash数据 */
SPI_FLASH_BulkErase();
SPI_FLASH_SectorErase(0);
printf("\r\n擦除完毕\r\n");
/* 将发送缓冲区的数据写到 flash 中 */
SPI_FLASH_Buffer_Write(Tx_Buffer, FLASH_WriteAddress, BufferSize);
{
printf("\r\n 写入的数据为: \r\t");
for(cnt=0;cnt<BufferSize;cnt++)
{
Tx_Buffer [cnt]=cnt;
printf(" 0x%02X ", Tx_Buffer[cnt]);
}
}
//printf("\r\n 写入的数据为:%d \r\t", Tx_Buffer[cnt]);
/* 将写入的数据读出来放到接收缓冲区 */
SPI_FLASH_Buffer_Read(Rx_Buffer, FLASH_ReadAddress, BufferSize);
{
printf("\r\n 读出的数据为: \r\t");
for(cnt=0;cnt<BufferSize;cnt++)
{
Rx_Buffer [cnt]=cnt;
printf(" 0x%02X ", Rx_Buffer[cnt]);
}
}
//printf("\r\n 读出的数据为:%d \r\n", Rx_Buffer[cnt]);
printf("\r\n 2M串行flash(W25Q16)测试成功!\n\r");
SPI_Flash_PowerDown();
while(1)
{
}
}
spi.c
uint32_t time_out = SPI_FLASH_WAIT_TIMEOUT;
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode);
void SPI_FLASH_Init(void)
{
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能SPI1 和 GPI时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
/* SCK */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* MISO */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* MOSI */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* CS */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
SPI_FLASH_CS_HIGH();
/* SPI1配置 */
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
//发送一个字节(这里原理同前文寄存器编写代码)
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
uint8_t read_temp;
time_out = SPI_FLASH_WAIT_TIMEOUT;
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET)
{
if(time_out-- == 0) //超时
{
return SPI_TIMEOUT_UserCallback(1);
}
}
SPI_I2S_SendData(SPI1, byte);
time_out = SPI_FLASH_WAIT_TIMEOUT;
/* Wait to receive a byte */
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET)
{
if(time_out-- == 0)
{
return SPI_TIMEOUT_UserCallback(2);
}
}
read_temp = (uint8_t)SPI_I2S_ReceiveData (SPI1);
return read_temp;
}
//接收一个字节
uint8_t SPI_FLASH_Receive_Byte(void)
{
return SPI_FLASH_SendByte(0xFF);
}
//出错时调用这个函数
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
printf("\r\nSPI 检测超时,错误代码:%d",errorCode);
return 0;
}
flash.h
#ifndef __FLASH_H
#define __FLASH_H
#include "sys.h"
#define W25Q16 0XEF14
#define FLASH_ID 0XEF14
#define SPI_FLASH_CS PAout(2)
#define SPI_FLASH_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_2)
#define SPI_FLASH_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_2)
#define SPI_FLASH_PageSize 256
#define SPI_FLASH_PerWritePageSize 256
#define SPI_FLASH_WAIT_TIMEOUT 10000
//指令表
#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 Dummy_Byte 0xFF
uint32_t SPI_FLASH_ReadDeviceID(void);
uint8_t SPI_FLASH_SendByte(uint8_t byte);
uint8_t SPI_FLASH_Receive_Byte(void);
uint32_t SPI_FLASH_ReadID(void);
void SPI_FLASH_SectorErase(uint32_t SectorAddr);
void SPI_FLASH_BulkErase(void);
void SPI_FLASH_WriteEnable(void);
void SPI_FLASH_WaitForWriteEnd(void);
void SPI_FLASH_Page_Write(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
void SPI_FLASH_Buffer_Read(uint8_t* pBuffer, uint8_t ReadAddr, uint8_t NumByteToRead);
void SPI_FLASH_Buffer_Write(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
void SPI_Flash_PowerDown(void);
#endif /* __FLASH_H */
usart.c
void USART1_GPIO_Config() /* GPIO配置 */
{
GPIO_InitTypeDef GPIO_InitStruct; /* 结构体定义 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); /* USART1时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* GPIOA使能 */
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_9; /* PA2引脚 ,USART的TX*/
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP; /* 复用推挽输出 */
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; /* 50MHz*/
GPIO_Init(GPIOA,&GPIO_InitStruct); /* 初始化GPIOA */
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_10; /* PA3引脚 ,USART的RX*/
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IN_FLOATING; /* 浮空输入,用来接收数据 */
GPIO_Init(GPIOA,&GPIO_InitStruct); /* 初始化GPIOA */
}
void USART1_Config()
{
USART_InitTypeDef USART_InitStructure; /* 结构体定义 */
USART_InitStructure.USART_BaudRate = 115200; /* 配置波特率 */
USART_InitStructure.USART_WordLength = USART_WordLength_8b; /* 8位数据 */
USART_InitStructure.USART_StopBits = USART_StopBits_1; /* 1位停止位 */
USART_InitStructure.USART_Parity = USART_Parity_No ; /* 无奇偶校验 */
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;/* 无硬件流控制 */
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; /* 允许接收和发送 */
USART_Init(USART1, &USART_InitStructure); /* 完成串口初始化 */
USART_Cmd(USART1, ENABLE); /* 串口使能 */
}
//重定义printf函数
int fputc(int ch, FILE *f)
{
/* 将Printf内容发往串口 */
USART_SendData(USART1, (unsigned char)ch);
while (!USART_GetFlagStatus(USART1, USART_FLAG_TC));
USART_ClearFlag(USART1, USART_FLAG_TC);
return (ch);
}
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(USART1);
}
四、运行结果
通过编译调试后,使用串口监听运行结果如下:
五、结论
当SPI时钟频率较高时,建议采用DMA模式以避免SPI速度性能的降低,且SPI有一个缺点:有指定的流控制,没有应答机制确认是否接收到了数据,不像I2C没传输数据会有一个ACK应答机制,学习时序弄清底层逻辑尤为重要,不可得过且过。