SPI(下)——读写串行FLASH实验

origin: https://zhuanlan.zhihu.com/p/27489167
前两篇文章介绍了SPI协议的 基础概念通讯过程的时序,这篇可以进入实践,完成一个小小的读写实验了。

从设备硬件特性

实验用的从设备是串行FLASH存储芯片W25Q64,这是一种使用SPI协议进行通讯的NOR FLASH存储器。这是一个64M位(即8M字节)的串行闪存,有0~127个块,每个块64KB,每个块包含16个扇区,其最小擦除单位是扇区(Sector)。如图18-1所示。

图18-1

查阅开发板的原理图可以知道,W25Q64的四个引脚和stm32f103引脚的连接关系为:

图18-2

需要注意的一点是,这里stm32的NSS引脚会被配置为软件模式,把它当成普通的GPIO引脚即可。

SPI_InitTypeDef

图18-2
  • 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。

图18-3

配置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的指令。

图18-4

上图中,第一列是指令名称,第二列是指令编码,第三列及以后的指令功能与对应的指令有关。带括号的字节内容为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,转载请私信,并注明原文出处。


  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值