打印
[STM32F2]

基于STM32与NOR FLASH的SPI通信

[复制链接]
646|29
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
基于STM32与NOR FLASH的SPI通信http://blog.csdn.net/qq_29344757/article/details/77046724
 SPI的通信很容易实现,相比之下,驱动FLASH反而耗费了我学习SPI整个过程的大部分时间。下面是我学习过程的一些记录。

硬件平台:秉火ISO_V2开发板
实现功能:STM32使用SPI协议读写板载NOR FLASH
1. 通讯引脚
  SPI通讯需要4个引脚,nSS、SCK、MISO和MOSI,

  以STM32的SPI1为例,其关联GPIO如上图标(摘自《STM32中文参考手册_V10.pdf》-P120)。AFIO_MAPR寄存器的BIT0(SPI1_REMAP)为0时则不重映射SPI1的4个GPIO,nSS、SCK、MISO和MOSI依次为PA4、PA5、PA6、PA7。我们使用寄存器的复位值为0x00,即我们不重映射SPI1关联引脚。开发板原理图的设计也确实如此:

  另外,一般在实际工程中,nSS引脚不采用硬件SPI专用的nSS引脚,而是用STM32的一个普通GPIO功能来控制。

使用特权

评论回复
沙发
yutingwei|  楼主 | 2024-3-31 21:16 | 只看该作者
软件设计
2.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;              //空闲时钟极性,高低电平
    uint16_t SPI_CPHA;              //时钟相位,即奇偶边沿采样
    uint16_t SPI_NSS;               //片选引脚nSS是交由硬件控制还是软件控制
    uint16_t SPI_BaudRatePrescaler; //时钟分频系数,FSCK = FCLK / 分频系数
    uint16_t SPI_FirstBit;          //MSB/LSB先行
    uint16_t SPI_CRCPolynomial;     //CRC校验表达式
}SPI_InitTypeDef;

操作函数
(1) 使能SPI的时钟

GPIO、SCK、MISO、MOSI都是PA组,且SPI外设跟GPIO外设一样,隶属于APB2总线:所以:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_SPI1, ENABLE);

使用特权

评论回复
板凳
yutingwei|  楼主 | 2024-3-31 21:19 | 只看该作者
初始化GPIO引脚
设置SPI1的相关引脚为复用输出,这样才会连接到SPI1上否则这些IO还是默认作为标准输入/输出。

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;     //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7);

使用特权

评论回复
地板
yutingwei|  楼主 | 2024-3-31 21:19 | 只看该作者
初始化SPI1,设置其工作模式

SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;                       //设置为主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;                   //SPI发送接收8位数据帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;                         //时钟悬空高
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;                        //数据捕获于第二个时钟沿
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                           //nSS信号软件控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;        //波特率预分频值为256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;                  //数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7;                            //CRC值计算的表达式
SPI_Init(SPI1, &SPI_InitStructure);                                 //将上述设置信息初始化外设SPIx寄存器

使用特权

评论回复
5
yutingwei|  楼主 | 2024-3-31 21:19 | 只看该作者
使能SPI1

SPI_Cmd(SPI1, ENABLE);

SPI接收数据

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

SPI发送数据

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

查看SPI传输过程状态

FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

  在SPI的传输过程中,若要判断数据是否传输完成,发送缓冲区是否为空等状态,可通过此函数实现。以判断是否发送完成为例:

while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);     //发送缓冲区不为空则一直阻塞

使用特权

评论回复
6
yutingwei|  楼主 | 2024-3-31 21:20 | 只看该作者
FLASH相关特性
  ISO_V2开发板的板载FLASH型号为W25Q64,是一种使用SPI通讯协议的NOR FLASH存储器。

  W25Q64除了上述的SPI关联的4个引脚之外,还有用于控制写保护功能的WP引脚、暂停通讯控制的HOLD引脚。当WP为低电平时禁止写入数据,HOLD为低电平时暂停通讯,硬件上将这两个引脚都接到3.3V,即不使用这两个功能。其他细节,到代码实现的时候补充。

使用特权

评论回复
7
yutingwei|  楼主 | 2024-3-31 21:20 | 只看该作者
软件实现
  实现功能:往板载W25Q64 FLASH写入数据,再读取出来,调试信息和运行结果通过串口打印到PC机串口工具。

  基于具有串口BSP的工程,新建bsp_spi_flash.h和bsp_spi_flash.c。

  函数声明:

//SPI通信相关函数
void SPI_GPIO_Init(void);
void SPI_FLASH_Init(void);
uint8_t SPI_FLASH_RecvSendByte(uint8_t byte);

//驱动FLASH相关函数
uint32_t SPI_FLASH_ReadID(void);
uint8_t SPI_FLASH_Read_SR(void);
void SPI_FLASH_Wait_Busy(void);
void SPI_FLASH_Read(uint8_t *Buf, uint32_t ReadAddr, uint16_t ReadCnt);
void SPI_FLASH_Write(uint8_t* Buf, uint32_t WriteAddr, uint16_t WriteCnt);
void SPI_FLASH_WriteEnable(void);
void SPI_FLASH_Write_Page(uint8_t* Buf, uint32_t WriteAddr, uint16_t WriteCnt);
void SPI_FLASH_Write_Sector(uint8_t* Buf, uint32_t WriteAddr, uint16_t WriteCnt);
void SPI_FLASH_WaitForWriteEnd(void);
void SPI_FLASH_Erase_Sector(uint32_t Addr);

使用特权

评论回复
8
yutingwei|  楼主 | 2024-3-31 21:20 | 只看该作者
设置SPI通讯的关联引脚和SPI的工作模式
//设置SPI通信的相关引脚
void SPI_GPIO_Init()
{
    GPIO_InitTypeDef GPIO_InitStruct;

    //开启GPIOA以及SPI1外设的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);

    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;    //CLK、MISO、MOSI、
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;                        //复用推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;  
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;          //nSS
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;   //推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    FLASH_SPI_CS_HIGH();
}

//设置SPI外设的工作模式
void SPI_FLASH_Init()
{
    SPI_InitTypeDef SPI_InitStruct;

    SPI_GPIO_Init();   
    SPI_InitStruct.SPI_Mode = SPI_Mode_Master;                      //主机端工作模式
    SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线全双工模式
    SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;                  //8位数据帧长度
    SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;                       //偶数边沿采SPI信号
    SPI_InitStruct.SPI_CPOL = SPI_CPOL_High;                        //空闲时钟为空
    SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;                 //高位先行
    SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;                          //使用软件控制片选信号
    SPI_InitStruct.SPI_CRCPolynomial = 7;                           //CRC校验
    SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;   //256分频
    SPI_Init(SPI1, &SPI_InitStruct);

    //打开SPI外设
    SPI_Cmd(SPI1, ENABLE);
}

使用特权

评论回复
9
yutingwei|  楼主 | 2024-3-31 21:21 | 只看该作者
通过SPI的硬件接口发送/接收数据
uint8_t SPI_FLASH_RecvSendByte(uint8_t byte)
{
    uint8_t TimeCnt = 0;

    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET)      //发送缓冲区不为空说明还要数据待发送
    {
        if (TimeCnt++ > 200)
            return 0;
    }

    SPI_I2S_SendData(SPI1, byte);   //通过外设SPIx发送一个数据

    TimeCnt = 0;
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET)
    {
        if (TimeCnt++ > 200)
            return 0;
    }
    return SPI_I2S_ReceiveData(SPI1);
}

使用特权

评论回复
10
yutingwei|  楼主 | 2024-3-31 21:28 | 只看该作者
该函数含超时机制,可用于发送/接收一字节数据。至于用于发送还是接收,看用户关注哪一个。

  下来是驱动FLASH的相关函数。
读取FLASH的存储器类型(ID)
uint32_t SPI_FLASH_ReadID(void)
{
    uint32_t RET_ID;

    //选中FLASH的片选
    FLASH_SPI_CS_LOW();

    //发送读取FLASH存储器类型的指令
    SPI_FLASH_RecvSendByte(W25X_JedecDeviceID);

    //接收FLASH发来的存储器类型,它是先发送高位的
    RET_ID = SPI_FLASH_RecvSendByte(NOTUSEDAT) << 16;           
    RET_ID |= SPI_FLASH_RecvSendByte(NOTUSEDAT) << 8;      
    RET_ID |= SPI_FLASH_RecvSendByte(NOTUSEDAT);

    //放开FLASH的片选
    FLASH_SPI_CS_HIGH();

    return RET_ID;
}

使用特权

评论回复
11
yutingwei|  楼主 | 2024-3-31 21:28 | 只看该作者
FLASH_SPI_CS_LOW()和FLASH_SPI_CS_HIGH()是分别实现选中和不选中FLASH片选的宏,其实质就是控制nSS引脚为高/低电平:

#define FLASH_SPI_CS_LOW()      GPIO_ResetBits(GPIOA, GPIO_Pin_4)   //选中FLASH
#define FLASH_SPI_CS_HIGH()     GPIO_SetBits(GPIOA, GPIO_Pin_4)     //不选中FLASH

使用特权

评论回复
12
yutingwei|  楼主 | 2024-3-31 21:28 | 只看该作者
 W25X_JedecDeviceID也是一个宏定义,这是由FLASH定义的用来控制FLASH的指令,从W25Q64手册知,支持的指令有:

使用特权

评论回复
13
yutingwei|  楼主 | 2024-3-31 21:28 | 只看该作者
这些命令被宏定义在bsp_spi_flash.h文件中:
#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

使用特权

评论回复
14
yutingwei|  楼主 | 2024-3-31 21:29 | 只看该作者
 发送完W25X_JedecDeviceID指令后,调用SPI_FLASH_RecvSendByte()函数用于接收FLASH发来的3字节分3次发送的DeviceID,注意,该FLASH的发送顺序是MSB先行,所以接收到的第一、二、三字节需要依次左移16、8、0位。
  当SPI_FLASH_RecvSendByte()用于发送数据(指令)时候我们并不关注返回的内容,所以不需要接收其返回值;
  当SPI_FLASH_RecvSendByte()用于接收数据时候我们并不关注发送的内容,所以NOTUSEDAT宏是我们任意定义的:

#define NOTUSEDAT   0xFF

使用特权

评论回复
15
yutingwei|  楼主 | 2024-3-31 21:29 | 只看该作者
读取FLASH的当前运行状态
uint8_t SPI_FLASH_Read_SR(void)
{
    uint8_t ret = 0;

    FLASH_SPI_CS_LOW();

    SPI_FLASH_RecvSendByte(W25X_ReadStatusReg);

    ret = SPI_FLASH_RecvSendByte(NOTUSEDAT);

    FLASH_SPI_CS_HIGH();

    return ret;
}

使用特权

评论回复
16
yutingwei|  楼主 | 2024-3-31 21:29 | 只看该作者
 获取FLASH的运行状态则是向FLASH发送获取状态的指令W25X_ReadStatusReg。跟上一个函数类似,不赘述。利用此函数的返回值,可以判断FLASH是否处于忙状态:

void SPI_FLASH_Wait_Busy(void)
{
    while ((SPI_FLASH_Read_SR() & 0x01) == 0x01);
}

使用特权

评论回复
17
yutingwei|  楼主 | 2024-3-31 21:29 | 只看该作者
读取FLASH的数据
void SPI_FLASH_Read(uint8_t *Buf, uint32_t ReadAddr, uint16_t ReadCnt)
{
    uint16_t i;

    FLASH_SPI_CS_LOW();

    SPI_FLASH_RecvSendByte(W25X_ReadData);

    SPI_FLASH_RecvSendByte(ReadAddr >> 16);
    SPI_FLASH_RecvSendByte(ReadAddr >> 8);
    SPI_FLASH_RecvSendByte(ReadAddr);

    for (i = 0; i < ReadCnt; i++)
    {
        Buf[i] = SPI_FLASH_RecvSendByte(NOTUSEDAT);     
    }

    FLASH_SPI_CS_HIGH();
}

使用特权

评论回复
18
yutingwei|  楼主 | 2024-3-31 21:30 | 只看该作者
 选中FLASH芯片后向其发送W25X_ReadData表示主机要读取FLASH的数据,接着发送要读取的目标地址,还是遵循MSB先行的发送规则,发送完毕就可以接收数据了。
  为什么主机发送完指令后,发送的数据会被FLASH解析成目标地址?这是由FLASH定义的。主机要读取ReadCnt个数据,为什么主机不用事先告诉FLASH?这也是FLASH定义的,FLASH就是这么工作的,一旦接收到W25X_ReadData,它就会知道它接下来要收取到一个目标地址,接着只管把从目标地址后的数据发回主机,直至FLASH不被选中。

使用特权

评论回复
19
yutingwei|  楼主 | 2024-3-31 21:30 | 只看该作者
往FLASH写数据
  往FLASH写数据,有3种写范围,写一整个扇区、写一整页、写一个字节,当我们要从某个扇区的开始写入一整个扇区的数据(4096字节),程序需要将对这个扇区分为一页一页来写(256字节),对这一页的写又会转换成一字节一字节的写。
  SPI_FLASH_Write(uint8_t* Buf, uint32_t WriteAddr, uint16_t WriteCnt)是用户调来写数据的函数

使用特权

评论回复
20
yutingwei|  楼主 | 2024-3-31 21:30 | 只看该作者
//写扇区 -> 写页 -> 按字节写
uint32_t SectorNum;
uint16_t SectorOffset;
uint16_t SectorRemainder;
uint16_t i = 0;
uint8_t FLASH_Buf[4096] = {0};          //整个扇区的副本

void SPI_FLASH_Write(uint8_t* Buf, uint32_t WriteAddr, uint16_t WriteCnt)
{   
    SectorNum = WriteAddr / 4096;           //得到目标地址位于第几个扇区
    SectorOffset = WriteAddr % 4096;        //得到目标地址在扇区内的偏移量
    SectorRemainder = 4096 - SectorOffset;  //得到所在扇区还剩下多少空间

    if (WriteCnt <= SectorRemainder)        //剩下的空间足够写用户要写的数据
    {
        SectorRemainder = WriteCnt;
    }

    while (1)
    {
        SPI_FLASH_Read(FLASH_Buf, SectorNum * 4096, 4096);  //读出整个扇区的内容

        //FLASH被擦除后的状态是都为1
        for (i = 0; i < SectorRemainder; i++)
        {
            if (FLASH_Buf[i + SectorOffset] != 0xff)
                break;
        }

        //i < SectorRemainder说明存在没擦除的区域,那就擦除一整块
        if (i < SectorRemainder)
        {
            SPI_FLASH_Erase_Sector(SectorNum);

            for (i = 0; i < SectorRemainder; i++)
            {
                FLASH_Buf[i + SectorOffset] = Buf[i];
            }

            //写一整个扇区(其实现会自动转换成按照多页来写)
            SPI_FLASH_Write_Sector(FLASH_Buf, SectorNum * 4096, 4096);
        }
        else
            //写一整个扇区(其实现会自动转换成按照多页来写)
            SPI_FLASH_Write_Sector(FLASH_Buf, WriteAddr, SectorRemainder);

        if (WriteCnt == SectorRemainder)
            break;
        else
        {
            SectorNum++;
            SectorOffset = 0;
            Buf += SectorRemainder;
            WriteAddr += SectorRemainder;
            WriteCnt -= SectorRemainder;

            if (WriteCnt > 4096)
            {
                SectorRemainder = 4096;
            }
            else
                SectorRemainder = WriteCnt;         
        }
    }
}\

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

24

主题

235

帖子

0

粉丝