STM32 通过 SPI 驱动 W25Q128

目录

  • 一、STM32 SPI 框图
    • 1、通讯引脚
    • 2、时钟控制
    • 3、数据控制逻辑
    • 4、整体控制逻辑
    • 5、主模式收发流程及事件说明如下:
  • 二、程序编写
    • 1、SPI 初始化
    • 2、W25Q128 驱动代码
      • 2.1 读写厂商 ID 和设备 ID
      • 2.2 读数据
      • 2.3 写使能/写禁止
      • 2.4 读/写状态寄存器
      • 2.5 擦除扇区
      • 2.6 擦除整个芯片
      • 2.7 页写
        • 2.7.1 写 SPI FLASH
    • 3、main 测试代码


有关 SPI 的内容在 SPI 通信协议详解
,不熟悉的可以参考一下

我使用设备的是 STM32F407 + W25Q128

一、STM32 SPI 框图

1、通讯引脚

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


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

如下是 W25Q128 的引脚图:

所以连接方式为:

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 初始化

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

// ctl_spi.h
#ifndef __CTL_SPI_H
#define __CTL_SPI_Hvoid spi_init(void);
uint8_t spi_read_write_byte(uint8_t tx_data);#endif /* __CTL_SPI_H */  

实现如下:

/******************************************************************************* @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));// CSGPIO_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 MOSIGPIO_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;       // 波特率预分频值为256SPI_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 的读写函数:

/******************************************************************************* @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 内部存储矩阵的内容。

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

// 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				0x00typedef 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 */

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

// 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 拉高结束。

格式如下:

实现如下:

/******************************************************************************* @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);  // Dummyw25qxx_r_w_byte(W25X_Dummy);  // Dummyw25qxx_r_w_byte(0x00);  // 决定芯片ID 和制造商ID 的传送顺序// 随便发两个字节数据,分别返回制造商ID 和设备IDid |= (w25qxx_r_w_byte(0xFF) << 8);  // 0xEFid |= w25qxx_r_w_byte(0xFF);		   // 0x17w25qxx_cs_high();return id;
}

2.2 读数据

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

/******************************************************************************* @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 位会自动复位完成写状态寄存器,擦除/程序安全寄存器,页程序,扇区擦除,块擦除,芯片擦除和复位指令。

/******************************************************************************* @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 位,检查相应的指令周期有没有结束,芯片是不是可以接受新的指令。状态寄存器可以连续的读出来:


/******************************************************************************* @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 位自动清零。如果该指令要操作的任何–页已经被保护起来,那么该指令也将不执行。

/******************************************************************************* @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 WELw25qxx_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)位或单个块/扇区保护锁。

/******************************************************************************* @brief  擦除整个芯片* * @return none* * @note   整片擦除时间非常长!!* 
******************************************************************************/
void w25qxx_erase_chip(void)
{w25qxx_write_enable(); // SET WELw25qxx_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 位自动清零。如果该指令要操作的页已经被保护起来,那么该指令也将不执行。

/******************************************************************************* @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
/******************************************************************************* @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 w25x16secoff = 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); // 写入整个扇区}elsew25qxx_write_nocheck(pbuffer, write_addr, secremain); // 写已经擦除了的,直接写入扇区剩余区间.if (num_byte_to_write == secremain)break; // 写入结束了else       // 写入未结束{secpos++;   // 扇区地址增1secoff = 0; // 偏移位置为0pbuffer += secremain;           // 指针偏移write_addr += secremain;        // 写地址偏移num_byte_to_write -= secremain; // 字节数递减if (num_byte_to_write > 4096)secremain = 4096; // 下一个扇区还是写不完elsesecremain = num_byte_to_write; // 下一个扇区可以写完了}};
}

3、main 测试代码

// 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;
}

测试结果如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/428691.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React组件如何暴露自身的方法

一、研究背景 最近遇到一个如何暴露React组件自身方法的问题。在某些时候&#xff0c;我们需要调用某个组件内部的方法以实现某个功能&#xff0c;因此我们需要了解如何暴露组件内部API的方法。 二、实践过程 本文主要介绍React组件暴露子组件API的方法&#xff0c;以下是实…

C++【类和对象】(一)

文章目录 前言1.类的定义1.1类定义格式1.2 访问限定符1.3 类域 2. 实例化2.1 实例化的概念2.2 对象大小 3.this指针结语 前言 在前文我们讲解了C基础语法知识。本文将会讲解C的类和对象。 1.类的定义 1.1类定义格式 class name {}&#xff1b;class为定义类的关键字&#x…

计算机网络传输层---课后综合题

线路&#xff1a;TCP报文下放到物理层传输。 TCP报文段中&#xff0c;“序号”长度为32bit&#xff0c;为了让序列号不会循环&#xff0c;则最多能传输2^32B的数据&#xff0c;则最多能传输&#xff1a;2^32/1500B个报文 结果&#xff1a; 吞吐率一个周期内传输的数据/周期时间…

网络协议全景:Linux环境下的TCP/IP、UDP

目录 1.UDP协议解析1.1.定义1.2.UDP报头1.3.特点1.4.缓冲区 2.TCP协议解析2.1.定义2.2.报头解析2.2.1.首部长度&#xff08;4位&#xff09;2.2.2.窗口大小2.2.3.确认应答机制2.2.4.6个标志位 2.3.超时重传机制2.4.三次握手四次挥手2.4.1.全/半连接队列2.4.2.listen2.4.3.TIME_…

加速开发体验:为 Android Studio 设置国内镜像源

Android Studio 是由 JetBrains 开发的一个官方 IDE&#xff0c;用于 Android 应用开发。由于网络原因&#xff0c;直接从 Google 的服务器下载可能会比较慢或者不稳定。幸运的是&#xff0c;我们可以通过配置国内镜像源来加速下载和更新。 文章目录 &#x1f4af; 修改 Gradle…

Python 从入门到实战23(属性property)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;通过熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 上篇文章我们讨论了类的定义、使用方法的相关知识。今天我们将学…

开源 AI 智能名片链动 2+1 模式 O2O 商城小程序在社群活动中的应用与时机选择

摘要&#xff1a;本文探讨了开源 AI 智能名片链动 21 模式 O2O 商城小程序在社群经济中的重要性&#xff0c;着重分析了如何借助该小程序适时举办大型活动以维持和引爆社群活跃度。通过对活动时机选择的研究&#xff0c;强调了针对社群用户量身定制活动时机的必要性&#xff0c…

GD32F103单片机-EXTI外部中断

GD32F103单片机-EXTI外部中断 一、EXTI及NVIC介绍二、编程实验2.1 相关库函数2.2 实验代码 一、EXTI及NVIC介绍 GD32和STM32的EXTI基本相似&#xff0c;具体见STM32F1单片机-外部中断GD32的EXTI包括20个相互独立的边沿检测电路请求产生中断或事件&#xff0c;4位优先级配置寄存…

C++中的new与delete

目录 1.简介 2.底层 1.简介 new是升级版的malloc&#xff0c;它会先开空间再去调用构造函数。 delete是升级版的free&#xff0c;它会先调用析构函数再free掉空间。 class A { public:A(int a10, int b10){a a1;b b1;}private:int a;int b; };int main() {//new会先开空间…

Java代码审计篇 | ofcms系统审计思路讲解 - 篇4 | XXE漏洞审计

文章目录 Java代码审计篇 | ofcms系统审计思路讲解 - 篇4 | XXE漏洞审计0. 前言1. XXE代码审计【有1处】1.1. 搜索JRXmlLoader1.1.1. JRAntApiWriteTask1.1.2. JRAntUpdateTask1.1.3. TableReportContextXmlRule1.1.4. JasperCompileManager【存在漏洞】 1.2. 搜索XMLReader1.2…

并查集LRU cache

并查集的定义 将n个不同的元素划分成一些不相交的集合。开始时&#xff0c;每个元素自成一个单元素集合&#xff0c;然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型称为并查集(unio…

Qt (19)【Qt 线程安全 | 互斥锁QMutex QMutexLocker | 条件变量 | 信号量】

阅读导航 引言一、互斥锁1. QMutex&#xff08;1&#xff09;基本概念&#xff08;2&#xff09;使用示例基本需求⭕thread.h⭕thread.cpp⭕widget.h⭕widget.cpp 2. QMutexLocker&#xff08;1&#xff09;基本概念&#xff08;2&#xff09;使用示例 3. QReadWriteLocker、QR…

string类(C++)

哈喽各位&#xff01;&#xff0c;久违了&#xff0c;最近怎么样捏&#xff0c;本次进入C的string类&#xff0c;加油加油呀&#xff01; 随记&#xff1a;鼓励创新&#xff0c;宽容失败&#xff01; 1.标准库的string类 1.1string类的了解 string的文献参考链接-->strin…

Buck变换器闭环控制,simulink仿真模型(适合初学者学习)

Buck变换器&#xff0c;又称为降压斩波器&#xff0c;是一种常见的DC-DC转换器&#xff0c;广泛应用于电源管理领域。它通过开关元件&#xff08;通常是MOSFET或BJT&#xff09;的导通与截止&#xff0c;改变输入电压到负载的平均电压&#xff0c;从而实现电压的降低。在实际应…

828华为云征文——使用Flexus云服务器X实例CentOS镜像下创建MySQL服务器教程

一、概述 1.1 前言 当前正值华为云盛大的828 B2B企业庆典&#xff0c;其中Flexus X实例的特惠活动尤为吸引人眼球。对于追求极致算力表现&#xff0c;并期望在自建MySQL数据库、Redis缓存系统及Nginx服务器部署上获得卓越性能的企业用户而言&#xff0c;这无疑是一个不可多得的…

SpringCloud (1) 服务拆解

1 服务拆解和治理 1.1 服务拆解 微服务的核心就是服务拆分,将传统的大项目拆分为多个微型服务(服务或微服务),实现服务之间"高内聚(微服务职责单一),低耦合(微服务功能相对独立)"的目的 (1) 水平(横向)拆分:先搭出拆分框架,比如【公共服务】(比如:common服务,client…

Python数据分析与可视化(Python绘图详解)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

轻量级流密码算法Trivium

轻量级流密码算法Trivium 0x0 Trivium算法简介 Trivium算法是由C&#xff0e;D Canniere和B&#xff0e;Preneel共同设计的一套对称加密算法&#xff0c;Trivium密码算法采用了分组密码和非线性反馈移位寄存器的设计思路。该密码算法总共288比特的内部状态&#xff0c;其中有…

数据篇| 关于Selenium反爬杂谈

友情提示:本章节只做相关技术讨论, 爬虫触犯法律责任与作者无关。 LLM虽然如火如荼进行着, 但是没有数据支撑, 都是纸上谈兵, 人工智能的三辆马车:算法-数据-算力,缺一不可。之前写过关于LLM微调文章《微调入门篇:大模型微调的理论学习》、《微调实操一: 增量预训练(Pretrai…

【手撕算法】快速排序(递归分治法)Python实现

一、算法 class Solution:def Partition(self, nums, low, high):pivotkey nums[low] # 元素copied, nums[low]空了出来while low < high:while low < high and nums[high] > pivotkey:high high - 1 # 直到找到一个nums[high]<pivotkey位置nums[low] nums[h…