STM32 通过 SPI 驱动 W25Q128

目录


有关 SPI 的内容在 SPI 通信协议详解,不熟悉的可以参考一下
我使用设备的是 STM32F407 + W25Q128

一、STM32 SPI 框图

1、通讯引脚

这四个引脚想必大家也很熟悉了,就不过多介绍。我是用的是 SPI1,引脚如下:


SPI1 是 APB2 总线上的设备,最高通信速率达 42Mbtis/s

如下是 W25Q128 的引脚图:

所以连接方式为:

xml 复制代码
W25Q     STM32
VCC  --> VCC
GND  --> GND
DO   --> PA6 (MISO)
DI   --> PA7 (MOSI)
CLK  --> PA5 (SCK)
CS   --> PA4 (CS)

2、时钟控制

SCK 线的时钟信号,由波特率发生器根据"控制寄存器CR1"中的 BR[0:2] 位控制,该位是对 fpclk 时钟的分频因子, 对 fpclk 的分频结果就是 SCK 引脚的输出时钟频率,计算方法见下表:

其中的 fpclk 频率是指 SPI 所在的 APB 总线频率,APB1 为 fpclk1,APB2 为 fpckl2。

通过配置"控制寄存器 CR"的 CPOL 位及 CPHA 位可以把 SPI 设置成之前分析的 4 种 SPI 模式。

3、数据控制逻辑

SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及发送缓冲区以及 MISO、MOSI 线。

  • 当向外发送数据的时候, 数据移位寄存器以"发送缓冲区"为数据源,把数据一位一位地通过数据线发送出去;
  • 当从外部接收数据的时候, 数据移位寄存器把数据线采样到的数据一位一位地存储到"接收缓冲区"中。

通过写 SPI 的"数据寄存器 DR"把数据填充到发送缓冲区中, 通过 "数据寄存器 DR",可以获取接收缓冲区中的内容。其中数据帧长度可以通过"控制寄存器 CR1"的"DFF位"配置成 8 位及 16 位模式;配置"LSBFIRST位"可选择 MSB 先行还是 LSB 先行。

4、整体控制逻辑

整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的"控制寄存器(CR1/CR2)"的参数而改变,基本的控制参数包括 SPI 模式、 波特率、LSB 先行、主从模式、单双向模式等等。在外设工作时,控制逻辑会根据外设的工作状态修改"状态寄存器(SR)",我们只要读取状态寄存器相关的寄存器位, 就可以了解 SPI 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制 NSS 信号线。

实际应用中,我们一般不使用 STM32 SPI 外设的标准 NSS 信号线,而是更简单地使用普通的 GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

5、主模式收发流程及事件说明如下:

STM32 使用 SPI 外设通讯时,在通讯的不同阶段它会对"状态寄存器SR"的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。

下图演示的是"主模式"流程,即 STM32 作为 SPI 通讯的主机端时的数据收发过程。

  1. 控制 NSS 信号线, 产生起始信号(图中没有画出);
  2. 把要发送的数据写入到"数据寄存器 DR"中, 该数据会被存储到发送缓冲区;
  3. 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去; MISO 则把数据一位一位地存储进接收缓冲区中;
  4. 当发送完一帧数据的时候,"状态寄存器 SR"中的"TXE 标志位"会被置 1,表示传输完一帧,发送缓冲区已空;类似地, 当接收完一帧数据的时候,"RXNE 标志位"会被置 1,表示传输完一帧,接收缓冲区非空;
  5. 等待到"TXE 标志位"为 1 时,若还要继续发送数据,则再次往"数据寄存器 DR"写入数据即可;等待到"RXNE 标志位"为 1 时, 通过读取"数据寄存器 DR"可以获取接收缓冲区中的内容。

假如我们使能了 TXE 或 RXNE 中断,TXE 或 RXNE 置 1 时会产生 SPI 中断信号,进入同一个中断服务函数,到 SPI 中断服务程序后, 可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用 DMA 方式来收发"数据寄存器 DR"中的数据。

有了这些基础,下面写相应的代码就轻松多了。

二、程序编写

1、SPI 初始化

我们首先实现如下两个函数:

c 复制代码
// ctl_spi.h
#ifndef __CTL_SPI_H
#define __CTL_SPI_H


void spi_init(void);
uint8_t spi_read_write_byte(uint8_t tx_data);

#endif /* __CTL_SPI_H */  

实现如下:

c 复制代码
/******************************************************************************
 * @brief  SPI GPIO 初始化
 * 
 * @return none
 * 
******************************************************************************/
static void spi_pin_init(void)
{
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    memset(&GPIO_InitStructure, 0, sizeof(GPIO_InitStructure));

    // CS
    GPIO_InitStructure.GPIO_Mode   =  GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType  =  GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd   =  GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Speed  =  GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Pin    =  GPIO_Pin_4;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // SCK MISO MOSI
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 引脚复用
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_SPI1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1);
}

/******************************************************************************
 * @brief  SPI 初始化
 * 
 * @return none
 * 
******************************************************************************/
static void spi_lowlevel_init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);

    SPI_InitTypeDef SPI_InitStructure;
    memset(&SPI_InitStructure, 0, sizeof(SPI_InitStructure));

    SPI_InitStructure.SPI_Direction          =   SPI_Direction_2Lines_FullDuplex; // 双线全双工
    SPI_InitStructure.SPI_BaudRatePrescaler  =   SPI_BaudRatePrescaler_256;       // 波特率预分频值为256
    SPI_InitStructure.SPI_CPHA               =   SPI_CPHA_2Edge;                  // 同步时钟的第二个跳变沿(上升或下降)数据被采样
    SPI_InitStructure.SPI_CPOL               =   SPI_CPOL_High;                   // 同步时钟的空闲状态为高电平
    SPI_InitStructure.SPI_CRCPolynomial      =   7;                               // CRC计算的多项式
    SPI_InitStructure.SPI_DataSize           =   SPI_DataSize_8b;                 // 8位帧数据结构
    SPI_InitStructure.SPI_FirstBit           =   SPI_FirstBit_MSB;                // 数据传输从MSB位开始
    SPI_InitStructure.SPI_Mode               =   SPI_Mode_Master;                 // 主机模式
    SPI_InitStructure.SPI_NSS                =   SPI_NSS_Soft;                    // NSS 信号由软件(使用 SSI位)管理 
    SPI_Init(SPI1, &SPI_InitStructure);

    SPI_Cmd(SPI1, ENABLE);
}

void spi_init(void)
{
    spi_pin_init();
    spi_lowlevel_init();
}

还有 SPI 的读写函数:

c 复制代码
/******************************************************************************
 * @brief      SPI 数据读写函数
 * 
 * @param[in]  tx_data    :    要发送的数据
 * 
 * @return     uint8_t    :    接收到的数据
 * 
******************************************************************************/
uint8_t spi_read_write_byte(uint8_t tx_data)
{
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET)  // 等待发送区空
    { }

    SPI_I2S_SendData(SPI1, tx_data);  // SPIx发送一个 byte 数据

    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET)  // 等待接收完一个 byte
    { }

    return SPI_I2S_ReceiveData(SPI1);  // 返回接收的数据
}

2、W25Q128 驱动代码

接下来需要参考手册中的时序图和指令来编写代码:W25Q128JV

下面是 FLASH常用芯片指令表:

该表中的第一列为指令名,第二列为指令编码,第三至第N列的具体内容根据指令的不同而有不同的含义。

  • 其中带括号的是字节参数,方向为 FLASH 向主机传输,即命令响应;不带括号的则为主机向 FLASH 传输。
  • "A0~A23"指 FLASH 芯片内部存储器组织的地址;
  • "M0~M7"为厂商号(MANUFACTURERID);
  • "ID0-ID15"为 FLASH 芯片的 ID;
  • "dummy"指该处可为任意数据;
  • "D0~D7"为 FLASH 内部存储矩阵的内容。

如下代码,接下来,我们就将实现对应的函数:

c 复制代码
// w25q.h
#ifndef __W25Q_H
#define __W25Q_H

#include <stdint.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
#define W25X_Dummy				0x00

typedef struct w25qxx_device_s
{
    void (*init)(void);
    void (*wr)(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read);
    void (*rd)(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write);
    uint16_t type;
} w25qxx_device_t;

extern w25qxx_device_t w25q32_dev;

void w25qxx_init(void);
uint16_t w25qxx_readid(void);
uint8_t w25qxx_readsr(void);                                                          // 读取状态寄存器
void w25qxx_write_sr(uint8_t sr);                                                     // 写状态寄存器
void w25qxx_write_enable(void);                                                       // 写使能
void w25qxx_write_disable(void);                                                      // 写保护
void w25qxx_read(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read);    // 读取flash
void w25qxx_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write); // 写入flash
void w25qxx_erase_chip(void);                                                         // 整片擦除
void w25qxx_erase_sector(uint32_t dst_addr);                                          // 扇区擦除
void w25qxx_powerdown(void);                                                          // 进入掉电模式
void w25qxx_wakeup(void);                                                             // 唤醒

#endif /* __W25Q_H */

除此之外,为了程序的简洁以及方便实现,定义如下的功能函数:

c 复制代码
// w25q.c
#define w25qxx_cs_high()   GPIO_SetBits(GPIOA, GPIO_Pin_4)
#define w25qxx_cs_low()    GPIO_ResetBits(GPIOA, GPIO_Pin_4)
#define w25qxx_r_w_byte(n) spi_read_write_byte(n)
#define w25qxx_spi_init()  spi_init()
#define w25qxx_delay_us(n) bl_delay_us(n)

w25qxx_device_t w25q32_dev = {
    .init = w25qxx_init,
    .wr = w25qxx_write,
    .rd = w25qxx_read,
    .type = 0x00,
};

void w25qxx_init(void)
{
    w25qxx_spi_init();
    w25q32_dev.type = w25qxx_readid();
}

2.1 读写厂商 ID 和设备 ID

由上图可知厂商 ID 是 0xEF,设备 ID 是 0x17。

读取设备 ID 和时序图图下:

该指令以 /CS 拉低开始,然后通过 DI 传输指令代码 90H 和 24 位的地址(全为 00000H )。这之后 W25Q 的 ID(EFH)和芯片 ID 将在时钟的下降沿以高位在前的方式传出。关于 W25Q128 的芯片和制造商 ID,在上面的图中已经列出。如果 24 位地址传输的是 00001H,那么芯片 ID 将首先被传出,然后紧接着的是制造商 ID。这两个是连续读出来的。该指令以 /CS 拉高结束。

格式如下:

实现如下:

c 复制代码
/******************************************************************************
 * @brief      读取读写厂商 ID 和设备 ID
 * 
 * @return     uint16_t   :   读取到的 ID
 * 
******************************************************************************/
uint16_t w25qxx_readid(void)
{
    uint16_t id = 0;
    w25qxx_cs_low();

    w25qxx_r_w_byte(W25X_ManufactDeviceID); // 发送读取ID命令
    
    w25qxx_r_w_byte(W25X_Dummy);  // Dummy
    w25qxx_r_w_byte(W25X_Dummy);  // Dummy
    w25qxx_r_w_byte(0x00);  // 决定芯片ID 和制造商ID 的传送顺序

	// 随便发两个字节数据,分别返回制造商ID 和设备ID
    id |= (w25qxx_r_w_byte(0xFF) << 8);  // 0xEF
    id |= w25qxx_r_w_byte(0xFF);		   // 0x17

    w25qxx_cs_high();

    return id;
}

2.2 读数据

读数据指令允许从存储器读一个或连续多个字节。该指令是以 /CS 拉低开始,然后通过 DI 在时钟的上升沿来传输指令代码(03H)和 24 位地址。当芯片接受完地址位后,相应地址处的值将会在时钟的下降沿,以高位在前、低位在后的方式,在 DO 上传输。如果连续的读多个字节的话,地址是自动加 1 的。这意味着可以一次读出整个芯片。该指令也是以 /CS 拉高来结束的。

c 复制代码
/******************************************************************************
 * @brief      读取SPI FLASH
 * 
 * @param[in]  pbuffer            :    数据存储区
 * @param[in]  read_addr          :    开始读取的地址(24bit)
 * @param[in]  num_byte_to_read   :    要读取的字节数(最大65535)
 * 
 * @return     none
 * 
******************************************************************************/
void w25qxx_read(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read)
{
    uint16_t i;

    w25qxx_cs_low();
    w25qxx_r_w_byte(W25X_ReadData);                // 发送读取命令
    w25qxx_r_w_byte((uint8_t)((read_addr) >> 16)); // 发送24bit地址
    w25qxx_r_w_byte((uint8_t)((read_addr) >> 8));
    w25qxx_r_w_byte((uint8_t)read_addr);
    for (i = 0; i < num_byte_to_read; i++)
    {
        pbuffer[i] = w25qxx_r_w_byte(0XFF); // 循环读数
    }
    w25qxx_cs_high();
}

2.3 写使能/写禁止


分别发送对应的两条指令即可,非常简单。

写使能指可以设置状态寄存器中的 WEL 位置 1。在页写,QUAD 页写,扇区擦除,块擦除,片擦除,写状态寄存器,擦写安全寄存器指令之前,必须先将 WEL 位置 1。写使能指令是以 /CS 拉低开始的,将 06H 通过 DI 在时钟的上升沿锁存,然后 /CS 拉高来结束指令。
写禁用指令将状态寄存器中的写启用锁存器(WEL)位重置为 0。通过低电平驱动 /CS 进入写禁用指令,将指令代码"04h"移到 DI 引脚,然后驱动 /CS 为高电平。请注意,通电后和通电后,WEL 位会自动复位完成写状态寄存器,擦除/程序安全寄存器,页程序,扇区擦除,块擦除,芯片擦除和复位指令。

c 复制代码
/******************************************************************************
 * @brief  SPI_FLASH写使能(将WEL置位)
 * 
 * @return none
 * 
******************************************************************************/
void w25qxx_write_enable(void)
{
    w25qxx_cs_low();                   // 使能器件
    w25qxx_r_w_byte(W25X_WriteEnable); // 发送写使能
    w25qxx_cs_high();                  // 取消片选
}

/******************************************************************************
 * @brief  SPI_FLASH写禁止(将WEL清零)
 * 
 * @return none
 * 
******************************************************************************/
void w25qxx_write_disable(void)
{
    w25qxx_cs_low();                    // 使能器件
    w25qxx_r_w_byte(W25X_WriteDisable); // 发送写禁止指令
    w25qxx_cs_high();                   // 取消片选
}

2.4 读/写状态寄存器

读/写状态寄存器各有三条指令,相应内容查阅手册。

读状态寄存指令可以任何时间使用,在擦写,写状态寄存器指令周期中依然可以。这样就可以随时检查 BUSY 位,检查相应的指令周期有没有结束,芯片是不是可以接受新的指令。状态寄存器可以连续的读出来:


c 复制代码
/******************************************************************************
 * @brief      读取SPI_FLASH的状态寄存器
 * 
 * @return     uint8_t    :    状态寄存器的值
 * 
 * @note       BIT7  6   5   4   3   2   1   0
 *             SPR   RV  TB BP2 BP1 BP0 WEL BUSY
 *             SPR: 默认0,状态寄存器保护位,配合WP使用
 *             TB,BP2,BP1,BP0: FLASH区域写保护设置
 *             WEL:写使能锁定
 *             BUSY:忙标记位(1,忙;0,空闲)
 *             默认: 0x00
 * 
******************************************************************************/
uint8_t w25qxx_readsr(void)
{
    uint8_t byte = 0;
    w25qxx_cs_low();                     // 使能器件
    w25qxx_r_w_byte(W25X_ReadStatusReg); // 发送读取状态寄存器命令
    byte = w25qxx_r_w_byte(0xff);        // 读取一个字节
    w25qxx_cs_high();                    // 取消片选
    return byte;
}

/******************************************************************************
 * @brief      写SPI_FLASH状态寄存器
 * 
 * @param[in]  sr    :    要写入的状态寄存器的值
 * 
 * @return     none
 * 
 * @note       只有SPR,TB,BP2,BP1,BP0(bit 7,5,4,3,2)可以写
 * 
******************************************************************************/
void w25qxx_write_sr(uint8_t sr)
{
    w25qxx_cs_low();                      // 使能器件
    w25qxx_r_w_byte(W25X_WriteStatusReg); // 发送写取状态寄存器命令
    w25qxx_r_w_byte(sr);                  // 写入一个字节
    w25qxx_cs_high();                     // 取消片选
}

2.5 擦除扇区

扇区擦除可以擦除 4Kbit 存储空间(全为0XFF)。进行扇区擦写指令之前,必须进行写使能指令。该指令是以 /CS 拉低开始的,然后在 DI 上传输指令代码 20H 和 24 位地址。

时序图如下图。当最后字节的第 8 位进入芯片后,/CS 必须拉高。如果 /CS 没有拉高,那么扇区擦写指令将不被执行。/CS 拉高后,扇区擦写指令的内建时间为 tSE。在扇区擦写指令执行期间,读状态寄存器指令仍然可以识别,以此来进行检查 BUSY 位。当扇区擦写指令执行期间,BUSY 位为 1。当执行完后,BUSY 为 0,表明可以接受新的指令了。扇区擦写指令完成后 WEL 位自动清零。如果该指令要操作的任何--页已经被保护起来,那么该指令也将不执行。

c 复制代码
/******************************************************************************
 * @brief   等待W25QXX芯片Busy标志位清空
 * 
 * @return  none
 * 
******************************************************************************/
static void w25qxx_wait_busy(void)
{
    while ((w25qxx_readsr() & 0x01) == 0x01)
        ; // 等待BUSY位清空
}

/******************************************************************************
 * @brief      擦除一个扇区
 * 
 * @param[in]  dst_addr    :    扇区地址 0~511 for w25x16
 * 
 * @return     none
 * 
 * @note       擦除一个山区的最少时间:150ms
 * 
******************************************************************************/
void w25qxx_erase_sector(uint32_t dst_addr)
{
    dst_addr *= 4096;
    w25qxx_write_enable(); // SET WEL
    w25qxx_wait_busy();
    w25qxx_cs_low();                              // 使能器件
    w25qxx_r_w_byte(W25X_SectorErase);            // 发送扇区擦除指令
    w25qxx_r_w_byte((uint8_t)((dst_addr) >> 16)); // 发送24bit地址
    w25qxx_r_w_byte((uint8_t)((dst_addr) >> 8));
    w25qxx_r_w_byte((uint8_t)dst_addr);
    w25qxx_cs_high();   // 取消片选
    w25qxx_wait_busy(); // 等待擦除完成
}

2.6 擦除整个芯片

芯片擦除指令将设备内的所有内存设置为全1 (FFh)的擦除状态。一个写启用指令必须在设备接受芯片擦除指令(状态)之前执行寄存器位 WEL 必须等于 1)。指令通过驱动 /CS 引脚低电平和移位启动指令代码"C7h"或"60h"。芯片擦除指令序列如下图所示。

芯片擦除指令将不会被执行如果任何内存区域是受块保护(CMP、SEC、TB、BP2、BP1 和 BP0)位或单个块/扇区保护锁。

c 复制代码
/******************************************************************************
 * @brief  擦除整个芯片
 * 
 * @return none
 * 
 * @note   整片擦除时间非常长!!
 * 
******************************************************************************/
void w25qxx_erase_chip(void)
{
    w25qxx_write_enable(); // SET WEL
    w25qxx_wait_busy();
    w25qxx_cs_low();                 // 使能器件
    w25qxx_r_w_byte(W25X_ChipErase); // 发送片擦除命令
    w25qxx_cs_high();                // 取消片选
    w25qxx_wait_busy();              // 等待芯片擦除结束
}

2.7 页写

页编程指令允许 1 到 256 字节写入存储器的某一页,这一页必须是被擦除过的(也就是只能写 0,不能写 1,擦除时是全写为 1)。

在页编程指令之前,必须先写入写使能指令。页编程指令是以 /CS 拉低开始,然后在 DI 上传输指令代码 02H,再接着传输 24 位的地址,接着是至少-一个字节的数据。/CS 管脚必须一直保持低。页编程指令的时序图如下图。

  • 如果一次写一整页数据(256 字节),最后的地址字节应该全为 0。如果最后 8 字节地址不为 0,但是要写入的数据长度超过页剩下的长度,那么芯片会回到当前页的开始地址写。
  • 写入少于 256 字节的的数据,对页内的其他数据没有任何影响。对于这种情况的唯一要求是,时钟数不能超过剩下页的长度。
  • 如果一次写入多于 256 字节的数据,那么在页内会回头写,先前写的数据可能已经被覆盖。

作为擦写指令,当最后字节的第 8 位进入芯片后,/CS 必须拉高。如果 /CS 没有拉高, .那么页写指令将不被执行。/CS 拉高后,页编程指令的内建时间为 tpp。在页写指令执行期间,读状态寄存器指令仍然可以识别,以此来进行检查 BUSY 位。当页写指令执行期间,BUSY 位为了 1。当执行完后,BUSY 为 0,表明可以接受新的指令了。页写指令完成后 WEL 位自动清零。如果该指令要操作的页已经被保护起来,那么该指令也将不执行。

c 复制代码
/******************************************************************************
 * @brief      在指定地址开始写入最大256字节的数据
 * 
 * @param[in]  pbuffer             :    数据存储区
 * @param[in]  write_addr          :    开始写入的地址(24bit)
 * @param[in]  num_byte_to_write   :    要写入的字节数(最大256),该数不应该超过该页的剩余字节数
 * 
 * @return     none
 * 
******************************************************************************/
static void w25qxx_write_page(uint8_t* pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)
{
    uint16_t i;

    w25qxx_write_enable();                          // SET WEL 
    w25qxx_cs_low();  
    
    w25qxx_r_w_byte(W25X_PageProgram);              // 发送写页命令   
    w25qxx_r_w_byte((uint8_t)((write_addr) >> 16)); // 发送24bit地址    
    w25qxx_r_w_byte((uint8_t)((write_addr) >> 8));   
    w25qxx_r_w_byte((uint8_t)write_addr);
    
    for(i = 0; i < num_byte_to_write; i++)
        w25qxx_r_w_byte(pbuffer[i]);                // 循环写数  
    
    w25qxx_cs_high();

    w25qxx_wait_busy();                             // 等待写入结束
}

接下来在这个函数的基础上,实现写函数。

2.7.1 写 SPI FLASH
c 复制代码
/******************************************************************************
 * @brief      在指定地址开始写入指定长度的数据,不检查数据是否为0XFF(具有自动换页功能)
 * 
 * @param[in]  pbuffer              :    数据存储区
 * @param[in]  write_addr           :    开始写入的地址(24bit)
 * @param[in]  num_byte_to_write    :    要写入的字节数(最大65535)
 * 
 * @return     none
 * 
 * @note       必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败
 * 
******************************************************************************/
static void w25qxx_write_nocheck(uint8_t* pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)   
{                    
    uint16_t pageremain;

    pageremain = 256 - write_addr % 256;                            //单页剩余的字节数              
    if(num_byte_to_write <= pageremain)
        pageremain = num_byte_to_write;                             //不大于256个字节
    while(1)
    {      
        w25qxx_write_page(pbuffer, write_addr, pageremain);
        if(num_byte_to_write == pageremain)                         //写入结束了
            break;                      
        else                                            //num_byte_to_write>pageremain
        {
            pbuffer += pageremain;
            write_addr += pageremain;   

            num_byte_to_write -= pageremain;            //减去已经写入了的字节数
            if(num_byte_to_write > 256)
                pageremain = 256;                       //一次可以写入256个字节
            else 
                pageremain = num_byte_to_write;         //不够256个字节了
        }
    };      
}

uint8_t W25QXX_BUFFER[4096];

/******************************************************************************
 * @brief      在指定地址开始写入指定长度的数据
 * 
 * @param[in]  pbuffer              :    数据存储区
 * @param[in]  write_addr           :    开始写入的地址(24bit)
 * @param[in]  num_byte_to_write    :    要写入的字节数(最大65535)
 * 
 * @return     none
 * 
 * @note       该函数带擦除操作
 * 
******************************************************************************/
void w25qxx_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)
{
    uint32_t secpos;
    uint16_t secoff;
    uint16_t secremain;
    uint16_t i;

    secpos = write_addr / 4096; // 扇区地址 0~511 for w25x16
    secoff = write_addr % 4096; // 在扇区内的偏移
    secremain = 4096 - secoff;  // 扇区剩余空间大小

    if (num_byte_to_write <= secremain)
        secremain = num_byte_to_write; // 不大于4096个字节
    while (1)
    {
        w25qxx_read(W25QXX_BUFFER, secpos * 4096, 4096); // 读出整个扇区的内容
        for (i = 0; i < secremain; i++)                  // 校验数据
        {
            if (W25QXX_BUFFER[secoff + i] != 0XFF)
                break; // 需要擦除
        }
        
        if (i < secremain) // 需要擦除
        {
            w25qxx_erase_sector(secpos);    // 擦除这个扇区
            for (i = 0; i < secremain; i++) // 复制
            {
                W25QXX_BUFFER[i + secoff] = pbuffer[i];
            }
            w25qxx_write_nocheck(W25QXX_BUFFER, secpos * 4096, 4096); // 写入整个扇区
        }
        else
            w25qxx_write_nocheck(pbuffer, write_addr, secremain); // 写已经擦除了的,直接写入扇区剩余区间.
            
        if (num_byte_to_write == secremain)
            break; // 写入结束了
        else       // 写入未结束
        {
            secpos++;   // 扇区地址增1
            secoff = 0; // 偏移位置为0

            pbuffer += secremain;           // 指针偏移
            write_addr += secremain;        // 写地址偏移
            num_byte_to_write -= secremain; // 字节数递减
            if (num_byte_to_write > 4096)
                secremain = 4096; // 下一个扇区还是写不完
            else
                secremain = num_byte_to_write; // 下一个扇区可以写完了
        }
    };
}

3、main 测试代码

c 复制代码
// main.c
uint8_t wr_data[128] = {0};
uint8_t rd_data[128] = {0};

int main(void)
{
	uint8_t i = 0;

	/*
	 * 外设初始化
	 */
	// ...

	w25q32_dev.init();

	printf("\r\n\r\nw25q128 id is: 0x%x\r\n", w25q32_dev.type);

	printf("detact w25q128 ok!\r\n");
	printf("write data !\r\n"); // 向flash写入数据
	for (i = 0; i < 128; i++)
	{
		wr_data[i] = i;
	}
	w25q32_dev.wr(wr_data, 0, 128);

	w25q32_dev.rd(rd_data, 0, 128); // 从falsh读取数据并打印
	printf("\r\nread data is :\r\n");
	for (i = 0; i < 128; i++)
	{
		printf("%d, ", rd_data[i]);
	}

	return 0;
}

测试结果如下:

相关推荐
idcardwang20 分钟前
xl9555-IO拓展芯片
stm32·单片机·嵌入式硬件
Y1rong23 分钟前
STM32之EXTI
stm32·单片机·嵌入式硬件
兆龙电子单片机设计28 分钟前
【STM32项目开源】STM32单片机智能语音家居控制系统
stm32·单片机·嵌入式硬件·物联网·开源·自动化
TaidL1 小时前
茂捷M1020电感式编码器芯片赋能工业智能升级,适用于工业及机器人等领域的各种应用场景
单片机·嵌入式硬件
意法半导体STM321 小时前
【官方原创】SAU对NSC分区的影响 LAT1578
stm32·单片机·嵌入式硬件·mcu·信息安全·trustzone·stm32开发
SmartRadio1 小时前
MK8000(UWB射频芯片)与DW1000的协议适配
c语言·开发语言·stm32·单片机·嵌入式硬件·物联网·dw1000
LDR0061 小时前
芯片电路的引脚标识代表什么?
stm32·单片机·嵌入式硬件
F133168929572 小时前
WD5030K,耐高压输出电流10A,WD5030A输出电流12A
网络·单片机·嵌入式硬件·物联网·汽车
三佛科技-134163842122 小时前
HN32512非隔离12V300MA~600MA降压控制方案典型应用 电路
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
集芯微电科技有限公司2 小时前
替代HT6310/KP3310离线式AC-DC无感线性稳压器
数据结构·人工智能·单片机·嵌入式硬件·fpga开发