前两篇文章介绍了SPI协议的 基础概念和 通讯过程的时序,这篇可以进入实践,完成一个小小的读写实验了。
从设备硬件特性
实验用的从设备是串行FLASH存储芯片W25Q64,这是一种使用SPI协议进行通讯的NOR FLASH存储器。这是一个64M位(即8M字节)的串行闪存,有0~127个块,每个块64KB,每个块包含16个扇区,其最小擦除单位是扇区(Sector)。如图18-1所示。
![](https://i-blog.csdnimg.cn/blog_migrate/3913b673a94ac8a8bc2b7f4136eb35d6.png)
查阅开发板的原理图可以知道,W25Q64的四个引脚和stm32f103引脚的连接关系为:
![](https://i-blog.csdnimg.cn/blog_migrate/94b5a759f2ea95cb51c00911503d25f6.png)
图18-2
需要注意的一点是,这里stm32的NSS引脚会被配置为软件模式,把它当成普通的GPIO引脚即可。
SPI_InitTypeDef
![](https://i-blog.csdnimg.cn/blog_migrate/97e79b43627b364c333c69e483468a31.png)
- SPI_Direction:SPI通讯方向,可配置双线全双工、双线只接收、单线只接收、单线只发送模式;
- SPI_Mode:SPI的工作模式,即工作在主机模式或从机模式。若工作在从机模式,则SCK信号由外部提供;
- SPI_DataSize:通讯的数据帧大小,可选8位或16位;
- SPI_CPOL:时钟极性,配置空闲状态时的SCK电平;
- SPI_CPHA:时钟相位,配置数据采样时刻,可配置在每个时钟周期的第1个或第2个边沿进行采样;
- SPI_NSS:配置NSS引脚的使用模式,可配置为硬件模式或软件模式。软件模式即是普通的GPIO口,人工拉高或置低其电平;
- SPI_BaudRatePrescaler:波特率分频因子,分频后的时钟即为SPI的SCK信号线的时钟频率;
- SPI_FirstBit:串行通讯中总会牵扯到MSB(高位)先行还是LSB(低位)先行的问题,可以用这个结构体成员进行配置;
- SPI_CRCPolynomial:CRC校验,若使用CRC,则可计算CRC的值。
SPI配置
先配置用到的GPIO,关于如何配置GPIO,之前专栏里的文章已经介绍很详细了,这里不再赘述。只是GPIO的引脚模式(GPIO_Mode)需要配置正确,关于这个可查阅stm32f103参考手册8.1.11章节的“外设的GPIO配置”,这里有关于各个外设的引脚模式配置分配。如图18-3。
![](https://i-blog.csdnimg.cn/blog_migrate/455ab8a4abc27d03384c1c2d96ae9d22.png)
配置SPI前,需要先了解从设备如何支持SPI,这些信息通过查询W25Q64的数据手册得到。
查阅W25Q64的数据手册可知,W25Q64最高可以支持80MHz的时钟频率,其支持SPI模式0(CPOL=0,CPHA=0)及模式3(CPOL=1,CPHA=1),支持双线全双工,MSB先行,8位或16位数据帧长度。
结合查到这些设备信息配置结构体即可。
/**
* @brief SPI 工作模式配置
* @param 无
* @retval 无
*/
static void SPI_Mode_Config(void)
{
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据帧
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件模式
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2 ; // 波特率分频因子,2分频
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行
SPI_InitStructure.SPI_CRCPolynomial = 0; // 不使用CRC校验,数值随便写
//SPI使用模式3,CPOL=1,CPHA=1
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // CPOL=1
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // CPHA=1
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
单字节数据发送与接收
刚才将SPI配置为双线全双工,意味着发送和接收是同时进行的。主机发送数据到从机,驱动SCK时钟运行,同时从机返回需要的数据到主机。
/**
* @brief SPI 工作模式配置
* @param data:发送并接收单字节数据
* @retval 返回从机发回的字节数据
*/
uint8_t SPI_Send_Byte(uint8_t data)
{
// 检查并等待TX缓冲区为空
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
// 缓冲区为空后向缓冲区写入要发送的字节数据
SPI_I2S_SendData(SPI1, data);
// 检查并等待RX缓冲区为非空
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
// 数据发送完毕,从RX缓冲区接收flash返回的数据
return SPI_I2S_ReceiveData(SPI1);
}
而字节接收的函数,正如上所述,需要向从机发送数据,才能驱动SCK运作,才能得到flash要返回给主机的数据,主机发送的这个数据可以是任意的,只是用于触发SCK。
指令控制
那么应该如何实现对flash设备的读写呢?这涉及到操作指令。
这些指令是flash芯片自定义的,主机通过SPI发送指令到从设备,从设备收到指令会执行对应的操作。如下图是W25Q64的指令。
![](https://i-blog.csdnimg.cn/blog_migrate/3b036d3e4b0d10d82c59c95446c2954b.png)
上图中,第一列是指令名称,第二列是指令编码,第三列及以后的指令功能与对应的指令有关。带括号的字节内容为flash向主机返回的字节数据,不带括号则是主机向flash发送字节数据。其中,
- A0~A23:flash内部存储器地址;
- M0~M7:制造商ID;
- ID0~ID15:flash芯片ID;
- D0~D7:flash内部存储的数据;
- dummy:指任意数据。
例如常常需要读取从设备ID用于测试识别设备。主机通过向flash发送“JEDEC ID”指令的编码“9Fh”,flash会通过MISO线向主机返回三个字节的数据:制造商ID(M7-M0)、芯片ID(ID15~ID8、ID7~ID0)。
在编程中,会用到刚才的SPI_Send_Byte()函数,这个函数是单字节的数据收发,所以flash返回的三个字节数据需要调用三次的SPI_Send_Byte()函数来进行接收。
#define FLASH_SPI_CS_HIGH GPIO_SetBits(GPIOA, GPIO_Pin_4); // flash芯片的CS引脚电平拉高
#define FLASH_SPI_CS_LOW GPIO_ResetBits(GPIOA, GPIO_Pin_4); // flash芯片的CS引脚电平置低
#define Dummy_Byte 0xFF // 任意值
/**
* @brief 读取设备ID
* @param 无
* @retval 返回从机发回的ID值
*/
uint32_t SPI_FLASH_ReadID(void)
{
uint32_t temp = 0, temp0 = 0, temp1 = 0, temp2 = 0;
FLASH_SPI_CS_LOW; // 低电平片选有效,SPI通讯开始
SPI_FLASH_SendByte(0x9f); // 发送读取ID指令
temp0 = SPI_FLASH_SendByte(Dummy_Byte); // 读取制造商ID(M7-M0)
temp1 = SPI_FLASH_SendByte(Dummy_Byte); // 读取芯片ID(ID15~ID8)
temp2 = SPI_FLASH_SendByte(Dummy_Byte); // 读取芯片ID(ID7~ID0)
FLASH_SPI_CS_HIGH; // 停止SPI通讯
temp = (temp0 << 16) | (temp1 << 8) | temp2; // 组合数据
return temp;
}
将得到的ID值与预定义的设备ID进行对比即可测试flash芯片是否连接正常等。
有一点需要注意,向flash芯片写入数据是需要时间的,所以进行下一次写数据前必须确认flash芯片处于空闲状态。而关于这个,flash芯片也定义了一个状态寄存器,这个寄存器的第0位“BUSY”反应当前flash是否处于忙碌状态。所以我们用“Read Status Register”指令来获取状态值。
#define FLASH_SPI_CS_HIGH GPIO_SetBits(GPIOA, GPIO_Pin_4); // flash芯片的CS引脚电平拉高
#define FLASH_SPI_CS_LOW GPIO_ResetBits(GPIOA, GPIO_Pin_4); // flash芯片的CS引脚电平置低
#define Dummy_Byte 0xFF // 任意值
/**
* @brief 等待flash内部时序操作完成
* @param 无
* @retval 无
*/
void SPI_WaitForWriteEnd(void)
{
uint8_t status_reg = 0;
FLASH_SPI_CS_LOW;
SPI_FLASH_Send_Byte(0x05); // 发送“Read Status Register”指令
do
{
status_reg = SPI_FLASH_Send_Byte(Dummy_Byte);
}while((status_reg & 0x01) == 1);
FLASH_SPI_CS_HIGH;
}
扇区擦除
flash有个特性,数据存储的每个位,在更新存储数据时,能把1改成0,而0却不能改为1。所以这就要求在写入数据前,必须对目标区域进行擦除操作,即把目标区域中的数据位擦除为1。
W25Q64支持扇区擦除、块擦除及整片擦除,最小的擦除单位是扇区,一个块包含16个扇区,有128个块。详见图18-1。
以扇区擦除为例,擦除大小为4KB,即4096字节的数据。由于擦除实际上是往扇区写入数据,把数据位都写为1,所以在擦除前还需要进行“写使能”,并且需要等待flash空闲。
/**
* @brief 写使能
* @param 无
* @retval 无
*/
void SPI_FLASH_WriteEnable(void)
{
FLASH_SPI_CS_LOW;
SPI_FLASH_SendByte(0x06); // 发送“Write Enable”指令,06h
FLASH_SPI_CS_HIGH;
}
/**
* @brief 扇区擦除
* @param SectorAddr:擦除地址
* @retval 无
*/
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
SPI_FLASH_WriteEnable();
SPI_FLASH_WaitForWriteEnd();
FLASH_SPI_CS_LOW;
SPI_FLASH_SendByte(0x20);
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16); // 高位擦除地址A23~A16
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8); // 中位擦除地址A15~A8
SPI_FLASH_SendByte(SectorAddr & 0xFF); // 低位擦除地址A7~A0
FLASH_SPI_CS_HIGH;
SPI_FLASH_WaitForWriteEnd();
}
这个函数有两个点需要注意,一是先发送高位擦除地址,二是擦除地址要对齐到4KB。
数据写入与读取
写入和读取函数也是发送指令到flash执行相应操作。
/**
* @brief 数据写入
* @param
* @arg addr:写入地址
* @arg writeBuff:包含数据的指针
* @arg numByteToWrite:写入字节数
* @retval 无
*/
void SPI_Write_Data(uint32_t addr,uint8_t *writeBuff,uint32_t numByteToWrite)
{
SPI_FLASH_WriteEnable();
FLASH_SPI_CS_LOW;
SPI_FLASH_Send_Byte(0x02); // 发送“Page Program”指令,02h
SPI_FLASH_Send_Byte((addr>>16) & 0xff);
SPI_FLASH_Send_Byte((addr>>8) & 0xff);
SPI_FLASH_Send_Byte(addr & 0xff);
while(numByteToWrite--)
{
SPI_FLASH_Send_Byte(*writeBuff);
writeBuff++;
}
FLASH_SPI_CS_HIGH;
SPI_WaitForWriteEnd();
}
这个写入函数发送的是“Page Program”指令,执行页写入操作。页写入一次最多发送256字节的数据。
#define Dummy_Byte 0xFF // 任意值
/**
* @brief 读取flash数据
* @param
* @arg addr:读取地址
* @arg readBuff:存放读出的数据的指针
* @arg numByteToWrite:读出的字节数
* @retval 无
*/
void SPI_Read_Data(uint32_t addr,uint8_t *readBuff,uint32_t numByteToRead)
{
FLASH_SPI_CS_LOW;
SPI_FLASH_Send_Byte(0x03); // 发送“Read Data”指令,03h
SPI_FLASH_Send_Byte((addr>>16) & 0xff);
SPI_FLASH_Send_Byte((addr>>8) & 0xff);
SPI_FLASH_Send_Byte(addr & 0xff);
while(numByteToRead--)
{
*readBuff = SPI_FLASH_Send_Byte(Dummy_Byte);
readBuff++;
}
FLASH_SPI_CS_HIGH;
}
至此,基本上的函数差不多都涉及到了,最后再main函数里调用并测试数据即可。
-----------------------------------------------------------------------
文章首发于知乎专栏 - stm32,转载请私信,并注明原文出处。