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 通讯的主机端时的数据收发过程。
- 控制 NSS 信号线, 产生起始信号(图中没有画出);
- 把要发送的数据写入到“数据寄存器 DR”中, 该数据会被存储到发送缓冲区;
- 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去; MISO 则把数据一位一位地存储进接收缓冲区中;
- 当发送完一帧数据的时候,“状态寄存器 SR”中的“TXE 标志位”会被置 1,表示传输完一帧,发送缓冲区已空;类似地, 当接收完一帧数据的时候,“RXNE 标志位”会被置 1,表示传输完一帧,接收缓冲区非空;
- 等待到“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;
}
测试结果如下:
相关文章:

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

C#进阶-基于雪花算法的订单号设计与实现
在现代电商系统和分布式系统中,高效地生成全局唯一的订单号是一个关键需求。订单号不仅需要唯一性,还需要具备一定的趋势递增性,以满足数据库索引和排序的需求。本文将介绍如何在C#中使用雪花算法(Snowflake)设计和实现…...

低版本SqlSugar的where条件中使用可空类型报语法错误
SQLServer数据表中有两列可空列,均为数值类型,同时在数据库中录入测试数据,Age和Height列均部分有值。 使用SqlSugar的DbFirst功能生成数据库表类,其中Age、Height属性均为可空类型。 开始使用的SqlSugar版本较低&…...

跨游戏引擎的H5渲染解决方案(腾讯)
本文是腾讯的一篇H5 跨引擎解决方案的精炼。 介绍 本文通过实现基于精简版的HTML5(HyperText Mark Language 5)来屏蔽不同引擎,平台底层的差异。 好处: 采用H5的开发方式,可以将开发和运营分离,运营部门自…...
docker构建java镜像,运行镜像出现日志 no main manifest attribute, in /xxx.jar
背景 本文主要是一个随笔,记录一下出现"no main manifest attribute"的解决办法 问题原因 主要是近期在构建一个镜像,在镜像构建成功后,运行一直提示"no main manifest attribute",当时还在想,是不是Dockerfile写错了,后来仔细检查了一下,发现是…...

react + antDesignPro 企业微信扫码登录
效果 实现步骤 1、项目中document.ejs文件引入企微js链接 注意:技术栈是使用的react antDesignPro,不同的技术栈有不同的入口文件(如vue在html文件引入) <script src"https://wwcdn.weixin.qq.com/node/wework/wwopen/j…...

Go-知识-定时器
Go-知识-定时器 1. 介绍2. Timer使用场景2.1 设定超时时间2.2 延迟执行某个方法 3. Timer 对外接口3.1 创建定时器3.2 停止定时器3.3 重置定时器3.4 After3.5 AfterFunc 4. Timer 的实现原理4.1 Timer数据结构4.1.1 Timer4.1.2 runtimeTimer 4.2 Timer 实现原理4.2.1 创建Timer…...

【alluxio编译报错】Some files do not have the expected license header
Some files do not have the expected license header 快捷导航 在开始解决问题之前,大家可以通过下面的导航快速找到相关资源啦!💡👇 快捷导航链接地址备注相关文档-ambaribigtop自定义组件集成https://blog.csdn.net/TTBIGDA…...

基于SpringBoot+Vue的商城积分系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、SSM项目源码 精品专栏:Java精选实战项目源码、Python精…...

docker-compose up 报错:KeyError: ‘ContainerConfig‘
使用命令查看所有容器: docker ps -a 找到有异常的容器删除 docker rm {容器id} 后续发现还是会出现这种情况,尝试使用更高版本的docker-compose后解决...
股票行情接口,量化金融交易在未来会被广泛应用吗
炒股自动化:申请官方API接口,散户也可以 python炒股自动化(0),申请券商API接口 python炒股自动化(1),量化交易接口区别 Python炒股自动化(2):获取…...
[SDX35+WCN6856]SDX35 开启class/gpio子系统配置操作说明
SDX35 SDX35介绍 SDX35设备是一种多模调制解调器芯片,支持 4G/5G sub-6 技术。它是一个4nm芯片专为实现卓越的性能和能效而设计。它包括一个 1.9 GHz Cortex-A7 应用处理器。 SDX35主要特性 ■ 3GPP Rel. 17 with 5G Reduced Capability (RedCap) support. Backward compati…...

react:React Hook函数
使用规则 只能在组件中或者其他自定义的Hook函数中调用 只能在组件的顶层调用,不能嵌套在if、for、 其他函数中 基础Hook 函数 useState useState是一个hook函数,它允许我们向组件中添加一个状态变量,从而控制影响组件的渲染结果 示例1…...

算法学习2
学习目录 一.插入排序 一.插入排序 从数组的第一个元素开始,当前元素与其前一个元素进行比较; 大于(或小于时)将其进行交换,即当前元素替换到前一位; 再将该元素与替换后位置的前一个元素进行交换…...

vue循环渲染动态展示内容案例(“更多”按钮功能)
当我们在网页浏览时,常常会有以下情况:要展示的内容太多,但展示空间有限,比如我们要在页面的一部分空间中展示较多的内容放不下,通常会有两种解决方式:分页,“更多”按钮。 今天我们的案例用于…...
好用的工具网址
代码类: 1,json解析:JSON在线解析及格式化验证 - JSON.cn 2.传参转化编码 在线url网址编码、解码器-BeJSON.com 日常: 1.莆田医院查询:滚蛋吧!莆田系...
【Temporal】方法规范
在workflow或者childWorkflow的方法代码中,不能使用golang的一些库方法,比如sleep,go协程等,必须使用其对应的封装方法,比如对应关系如下: time.Sleep -> workflow.Sleepgo xx -> workflow.Go(xx) 这…...
Python实现图形学曲线和曲面的Bezier曲线算法
目录 使用Python实现图形学曲线和曲面的Bezier曲线算法引言Bezier曲线的数学原理1. Bezier曲线定义2. Bezier曲线的递归形式 Python实现Bezier曲线算法1. 代码实现 代码详解使用示例Bezier曲线的特点Bezier曲面的扩展Bezier曲面类实现 总结 使用Python实现图形学曲线和曲面的Be…...

Unity数据持久化4——2进制
概述 基础知识 各类型数据转字节数据 文件操作相关 文件相关 文件流相关 文件夹相关 练习题 using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine;public class Exercises1 : MonoBehaviour {/…...
经典sql题(八)SQL 查询详细指南总结一
SQL 查询详细指南 SQL(Structured Query Language)是一种用于管理和操作关系数据库的标准语言。本文将详细介绍 SQL 中的一些常见操作及其用法,包括 DISTINCT 去重、LIMIT 限制、排序、开窗函数、NULL 值替换、JOIN 与 UNION 等。 1. DISTI…...
用Python实现时间序列模型实战——Day 30: 学习总结与未来规划
在第30天,我们将对整个学习过程进行总结,复习关键知识点,并展望未来的学习与应用方向。我们将涵盖时间序列分析过程中涉及的主要模型、技术和工具,总结它们的优势和应用场景。此外,规划未来如何进一步深入学习…...

ChatGPT居然主动勾引用户,OpenAI又测试新功能? 一文教你学会订阅
有网友表示,他收到了ChatGPT主动给他发送的消息,询问“你高中的第一周过得怎么样?还适应吗?” 他很懵逼的回了一句“你刚才是给我发消息吗?”。也就是说,在没有任何先前文本提示下,ChatGPT主动…...

基于SpringBoot+Vue的考研百科网站系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、SSM项目源码 精品专栏:Java精选实战项目源码、Python精…...

深度之眼(三十)——pytorch(一)--深入浅出pytorch(附安装流程)
文章目录 一、前言一、pytoch二、六个部分三、如何学习四、学习路径(重要)五、安装pytorch5.1 坑15.2 坑2 一、前言 我看了下目录 第一章和第二章都是本科学的数字图像处理。 也就是这一专栏:数字图像实验。 所以就不准备学习前两章了,直接…...

麒麟银河桌面版,成功安装cuda12.6,mysql
一、 要卸载并禁用 nouveau 驱动程序,可以按照以下步骤进行: 1. 确认 nouveau 驱动的当前状态: 首先,你可以使用以下命令查看 nouveau 驱动是否正在运行: lsmod | grep nouveau如果有输出,说明 nouveau …...

CentOS 7 YUM源不可用
CentOS 7 操作系统在2024年6月30日后将停止官方维护,并且官方提供的YUM源将不再可用。 修改:nano /etc/yum.repos.d/CentOS-Base.repo # CentOS-Base.repo [base] nameCentOS-$releasever - Base baseurlhttp://mirrors.aliyun.com/centos/$rel…...

Java反序列化利用链篇 | URLDNS链
文章目录 URLDNS链调用链分析Payload编写 系列篇其他文章,推荐顺序观看~ Java反序列化利用链篇 | JdbcRowSetImpl利用链分析Java反序列化利用链篇 | CC1链_全网最菜的分析思路Java反序列化利用链篇 | CC1链的第二种方式-LazyMap版调用链Java反序列化利用链篇 | URLD…...
Android 短信验证码自动填充
本文主要介绍国外google上线的app 短信自动填充方案。 本方案主要是使用google提出的,防止开发者使用SMS相关权限造成的用户信息泄露 目录 注意点: 1、本方式不适合华为手机,华为有自己的获取方式 2、本方式不需要添加任何短信权限 3、…...

数据库 MySQL 是否需要容器化?
容器的定义:容器是为了解决“在切换运行环境时,如何保证软件能够正常运行”这一问题。 目前,容器和 Docker 依旧是技术领域最热门的词语,无状态的服务容器化已经是大势所趋,同时也带来了一个热点问题被大家所争论不以…...

Kettle的安装及简单使用
Kettle的安装及简单使用一、kettle概述二、kettle安装部署和使用Windows下安装案例1:MySQL to MySQL案例2:使用作业执行上述转换,并且额外在表stu2中添加一条数据案例3:将hive表的数据输出到hdfs案例4:读取hdfs文件并将…...