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=1SPI1->CR1|=1<<0; //数据采样从第二个时间边沿开始,CPHA=1 SPI1->CR1|=2<<5; //Fpclk1/8SPI1->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_ByteSPI_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 255uint8_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.cuint32_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 0xFFuint32_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应答机制,学习时序弄清底层逻辑尤为重要,不可得过且过。
SPI读写FLASH 原理+完整代码相关推荐
- STM32笔记(十二)---SPI读写FLASH
SPI读写FLASH 文章目录 SPI读写FLASH 一.SPI协议简介 1.1 SPI 物理层 1.2 协议层 1.2.1 SPI 基本通讯过程 1.2.2 通讯的起始和停止信号 1.2.3 数据有 ...
- 《STM32从零开始学习历程》——SPI读写FLASH
<STM32从零开始学习历程>@EnzoReventon SPI读写FLASH 相关链接: SPI物理层及FLASH芯片介绍 SPI协议层 SPI特性及架构 参考资料: [野火EmbedF ...
- STM32CUBEIDE之SPI读写FLASH进阶串行FLASH文件系统FatFs
预备知识 >>W25Q128是16M spi flash,一共有256个block ,每个Block 64KB. >>一个Block可以分割为16个扇区(small secto ...
- STM32F103学习笔记——SPI读写Flash(二)
此系列文章是小白学习STM32的一些学习笔记.小白第一次写笔记文章,有不足或是错误之处,请多体谅和交流! 目录 1.软件设计流程 2.SPI初始化 3.SPI发送接收一字节函数编写 4.FLASH ...
- STM32F103配合STM32CubeMX实现SPI读写flash
本人采用的是正点原子的精英STM32F103开发板,其包含一块W25Q128型号的flash芯片.该flash与STM32F103的SPI2相连. 下面根据正点原子提供的开发指南文档,实现FreeRT ...
- STM32F429入门(二十一):SPI协议及SPI读写FLASH
IIC主要用于通讯速率一般的场合,而SPI一般用于较高速的场合. 一.SPI协议简介 SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设 ...
- STM32 SPI读写FLASH
文章目录 一.SPI协议 1.物理层 2.协议层 总体讲解 具体讲解 二.STM32 SPI外设 1.通讯引脚 2.时钟控制逻辑 3.数据控制 4.整体控制逻辑 三.通信过程 四.固件库编程 1.结构 ...
- AXI Quad SPI读写Flash做远程升级
未经允许,本文禁止转载 目录 简介 AXI Quad SPI IP设置 寄存器说明 AXI Quad SPI支持的通用命令 读flash id 读flash 数据 擦除扇区 写flash 数据 注意事 ...
- stm32中spi可以随便接吗_STM32的SPI模式读写FLASH芯片全面讲解
例程完整代码: SPI协议简介 SPI协议,即串行外围设备接口,是一种告诉全双工的通信总线,它被广泛地使用在ADC,LCD等设备与MCU间通信的场合. SPI信号线 SPI包含4条总线,分别为SS,S ...
最新文章
- unittest 框架学习
- react 从零开始搭建开发环境
- 前端框架MVC/MVVM分析系列
- BZOJ1305: [CQOI2009]dance跳舞
- api 另一窗体 之上_12 个设计 API 的安全建议,不要等出事儿了“捶胸顿足”
- spring cloud中gateway存在的意义是什么?
- Python格式化字符串字面值 | 被官方文档称之为『漂亮』的输出格式
- 如何设置程序默认“以管理员身份运行”
- java web 邮件_Java Web(十二) JavaMail发送邮件
- Idea中使用maven命令
- 电路串联和并联图解_判断串联并联电路图口诀
- Linux程序动态库的加载
- python 批量读取csv_Python Pandas批量读取csv文件到dataframe的方法
- c语言实参和形参占用存储单元_在C语言中,以下说法正确的是()。 A.实参和与其对应的形参分别占用独立的存储单元。 B.实参和与...
- 阅读类APP开发的好处有哪些
- 获取店铺商品详情和订单详情
- Bert几个数据集的概念Cola、MRPC、XNLI、MNLI等
- php中左移和右移,c语言左移和右移的示例详解
- 多项式分解 java
- 【mysql新加不了中文】Error Code: 1366. Incorrect string value: ‘\xE7\xBA\xB8\xE7\xB1\xBB‘ for colum