STM32F1实战:用CubeIDE HAL库搞定W25Q128跨页跨扇区写入(附完整代码)
当你在STM32项目中需要存储大量数据时,外部Flash芯片W25Q128是个不错的选择。但真正用起来,你会发现它有个让人头疼的特性:写入不能跨页,擦除最小单位是扇区。这就意味着,如果你的数据刚好卡在页或扇区边界,常规的写入方法就会失效。
我最近在一个物联网项目中就遇到了这个问题。设备需要定期记录传感器数据,每条记录大小不固定,经常出现跨页甚至跨扇区的情况。经过几次失败的尝试和调试,终于总结出一套可靠的解决方案。下面就把我的实战经验分享给大家,特别是那些已经掌握W25Q128基础操作,但在处理复杂写入场景时遇到瓶颈的中级开发者。
1. 理解W25Q128的存储架构
W25Q128的16MB存储空间被组织成一个层级结构:
- 块(Block):256个,每个64KB
- 扇区(Sector):每个块包含16个扇区,共4096个扇区,每个4KB
- 页(Page):每个扇区包含16页,每页256字节
关键限制:
- 写入操作:单次最多写入一页(256字节),且不能跨页
- 擦除操作:最小擦除单位是扇区(4KB),擦除后所有位变为1
- 编程特性:只能将1改为0,不能将0改为1,必须先擦除才能重新写入
c复制// W25Q128存储结构定义
#define PAGE_SIZE 256 // 页大小(字节)
#define SECTOR_SIZE 4096 // 扇区大小(字节)
#define BLOCK_SIZE 65536 // 块大小(字节)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 跨页写入的解决方案
当数据长度超过当前页剩余空间时,需要将数据拆分到多个页写入。下面是处理跨页写入的关键函数:
2.1 页写入基础函数
首先实现单页写入的基础函数,这是所有写入操作的基础:
c复制/**
* @brief 写入单页数据
* @param pData: 要写入的数据指针
* @param addr: 写入起始地址
* @param size: 要写入的数据大小(不超过256字节)
* @retval HAL状态
*/
HAL_StatusTypeDef W25Q128_WritePage(uint8_t *pData, uint32_t addr, uint16_t size) {
// 检查写入是否跨页
if((addr % PAGE_SIZE) + size > PAGE_SIZE) {
return HAL_ERROR;
}
W25Q128_WriteEnable();
W25Q128_Enable();
// 发送页编程指令
spi2_Transmit_one_byte(0x02);
// 发送24位地址
spi2_Transmit_one_byte((addr >> 16) & 0xFF);
spi2_Transmit_one_byte((addr >> 8) & 0xFF);
spi2_Transmit_one_byte(addr & 0xFF);
// 写入数据
for(uint16_t i = 0; i < size; i++) {
spi2_Transmit_one_byte(pData[i]);
}
W25Q128_Disable();
W25Q128_WaitBusy();
return HAL_OK;
}
2.2 跨页写入函数
基于单页写入,实现自动处理跨页情况的通用写入函数:
c复制/**
* @brief 跨页写入数据
* @param pData: 要写入的数据指针
* @param addr: 写入起始地址
* @param size: 要写入的数据总大小
* @retval HAL状态
*/
HAL_StatusTypeDef W25Q128_WriteMultiPage(uint8_t *pData, uint32_t addr, uint32_t size) {
uint32_t remaining = size;
uint32_t currentAddr = addr;
uint8_t *currentPtr = pData;
while(remaining > 0) {
// 计算当前页剩余空间
uint16_t pageOffset = currentAddr % PAGE_SIZE;
uint16_t chunkSize = PAGE_SIZE - pageOffset;
// 调整本次写入大小
if(chunkSize > remaining) {
chunkSize = remaining;
}
// 写入当前页
if(W25Q128_WritePage(currentPtr, currentAddr, chunkSize) != HAL_OK) {
return HAL_ERROR;
}
// 更新指针和剩余大小
currentPtr += chunkSize;
currentAddr += chunkSize;
remaining -= chunkSize;
}
return HAL_OK;
}
3. 跨扇区处理的擦除策略
由于擦除的最小单位是扇区(4KB),当写入跨越扇区边界时,需要特别处理:
3.1 扇区擦除函数
c复制/**
* @brief 擦除指定扇区
* @param sectorNum: 扇区编号(0-4095)
* @retval HAL状态
*/
HAL_StatusTypeDef W25Q128_EraseSector(uint32_t sectorNum) {
if(sectorNum >= 4096) return HAL_ERROR;
uint32_t sectorAddr = sectorNum * SECTOR_SIZE;
W25Q128_WriteEnable();
W25Q128_Enable();
// 发送扇区擦除指令
spi2_Transmit_one_byte(0x20);
// 发送24位地址
spi2_Transmit_one_byte((sectorAddr >> 16) & 0xFF);
spi2_Transmit_one_byte((sectorAddr >> 8) & 0xFF);
spi2_Transmit_one_byte(sectorAddr & 0xFF);
W25Q128_Disable();
W25Q128_WaitBusy();
return HAL_OK;
}
3.2 智能擦除函数
这个函数会自动计算需要擦除的扇区范围:
c复制/**
* @brief 智能擦除准备写入区域
* @param addr: 写入起始地址
* @param size: 要写入的数据大小
* @retval HAL状态
*/
HAL_StatusTypeDef W25Q128_PrepareWriteArea(uint32_t addr, uint32_t size) {
// 计算起始和结束扇区
uint32_t startSector = addr / SECTOR_SIZE;
uint32_t endSector = (addr + size - 1) / SECTOR_SIZE;
// 擦除所有涉及的扇区
for(uint32_t sector = startSector; sector <= endSector; sector++) {
if(W25Q128_EraseSector(sector) != HAL_OK) {
return HAL_ERROR;
}
}
return HAL_OK;
}
4. 完整解决方案与实战案例
将上述功能整合,实现一个完整的、健壮的写入流程:
4.1 完整写入流程
- 检查写入区域:确认地址和大小有效
- 准备擦除区域:擦除所有涉及的扇区
- 执行写入操作:处理可能的跨页情况
- 验证写入结果:可选的数据校验
c复制/**
* @brief 安全写入数据(自动处理跨页跨扇区)
* @param pData: 要写入的数据指针
* @param addr: 写入起始地址
* @param size: 要写入的数据大小
* @retval HAL状态
*/
HAL_StatusTypeDef W25Q128_SafeWrite(uint8_t *pData, uint32_t addr, uint32_t size) {
// 1. 参数检查
if(addr + size > 16*1024*1024) {
return HAL_ERROR; // 超出Flash范围
}
// 2. 擦除目标区域
if(W25Q128_PrepareWriteArea(addr, size) != HAL_OK) {
return HAL_ERROR;
}
// 3. 写入数据
if(W25Q128_WriteMultiPage(pData, addr, size) != HAL_OK) {
return HAL_ERROR;
}
// 4. 可选:验证写入数据
#ifdef W25Q128_WRITE_VERIFY
uint8_t *readBuf = malloc(size);
if(readBuf == NULL) return HAL_ERROR;
W25Q128_Read(readBuf, addr, size);
if(memcmp(pData, readBuf, size) != 0) {
free(readBuf);
return HAL_ERROR;
}
free(readBuf);
#endif
return HAL_OK;
}
4.2 实战案例:存储传感器数据
假设我们需要存储不定长的传感器数据包,数据包可能跨页或跨扇区:
c复制typedef struct {
uint32_t timestamp;
float temperature;
float humidity;
uint16_t pressure;
uint8_t batteryLevel;
uint8_t status;
} SensorData;
/**
* @brief 存储传感器数据
* @param data: 传感器数据结构体指针
* @param storageAddr: 存储地址指针(自动递增)
* @retval HAL状态
*/
HAL_StatusTypeDef StoreSensorData(SensorData *data, uint32_t *storageAddr) {
uint8_t buffer[sizeof(SensorData)];
memcpy(buffer, data, sizeof(SensorData));
// 计算下一个可用地址(考虑4字节对齐)
uint32_t nextAddr = (*storageAddr + sizeof(SensorData) + 3) & ~0x03;
// 写入数据
if(W25Q128_SafeWrite(buffer, *storageAddr, sizeof(SensorData)) != HAL_OK) {
return HAL_ERROR;
}
// 更新存储地址
*storageAddr = nextAddr;
return HAL_OK;
}
5. 性能优化与注意事项
在实际使用中,还需要考虑以下优化点和注意事项:
5.1 写入性能优化
- 批量写入:尽量合并多次小写入为单次大写入
- 地址对齐:4字节对齐可以提高访问效率
- 缓存管理:实现写入缓存减少实际写入次数
c复制// 写入缓存示例
#define WRITE_CACHE_SIZE 256
typedef struct {
uint8_t data[WRITE_CACHE_SIZE];
uint32_t addr;
uint16_t size;
} WriteCache;
WriteCache writeCache;
/**
* @brief 缓存写入数据
* @param pData: 数据指针
* @param addr: 写入地址
* @param size: 数据大小
* @retval HAL状态
*/
HAL_StatusTypeDef W25Q128_CachedWrite(uint8_t *pData, uint32_t addr, uint16_t size) {
// 检查是否可以合并到当前缓存
if(writeCache.size > 0 &&
addr == (writeCache.addr + writeCache.size) &&
(writeCache.size + size) <= WRITE_CACHE_SIZE) {
// 合并写入
memcpy(&writeCache.data[writeCache.size], pData, size);
writeCache.size += size;
return HAL_OK;
}
// 不能合并,先刷新缓存
if(writeCache.size > 0) {
if(W25Q128_SafeWrite(writeCache.data, writeCache.addr, writeCache.size) != HAL_OK) {
return HAL_ERROR;
}
}
// 如果数据大于缓存一半,直接写入不缓存
if(size > (WRITE_CACHE_SIZE / 2)) {
return W25Q128_SafeWrite(pData, addr, size);
}
// 否则存入缓存
memcpy(writeCache.data, pData, size);
writeCache.addr = addr;
writeCache.size = size;
return HAL_OK;
}
/**
* @brief 刷新写入缓存
* @retval HAL状态
*/
HAL_StatusTypeDef W25Q128_FlushCache(void) {
if(writeCache.size == 0) return HAL_OK;
HAL_StatusTypeDef status = W25Q128_SafeWrite(writeCache.data, writeCache.addr, writeCache.size);
writeCache.size = 0;
return status;
}
5.2 重要注意事项
- 电源稳定性:写入过程中断电可能导致数据损坏
- 擦除寿命:W25Q128每个扇区约10万次擦写寿命
- 写入速度:页写入约需0.5-3ms,扇区擦除约需50-200ms
- 并发访问:避免在写入/擦除时进行其他操作
提示:在实际项目中,建议实现一个简单的磨损均衡算法,避免频繁擦写同一扇区。
6. 高级应用:实现循环缓冲区
对于数据记录应用,循环缓冲区是个实用方案:
c复制#define CIRCULAR_BUFFER_SIZE (1024 * 1024) // 1MB循环缓冲区
uint32_t bufferStartAddr = 0; // 缓冲区起始地址
uint32_t writePointer = 0; // 当前写入位置
uint32_t readPointer = 0; // 当前读取位置
/**
* @brief 写入数据到循环缓冲区
* @param pData: 数据指针
* @param size: 数据大小
* @retval HAL状态
*/
HAL_StatusTypeDef CircularBuffer_Write(uint8_t *pData, uint32_t size) {
// 检查是否有足够空间
if(size > CIRCULAR_BUFFER_SIZE) return HAL_ERROR;
// 处理缓冲区回绕
if(writePointer + size > bufferStartAddr + CIRCULAR_BUFFER_SIZE) {
uint32_t firstChunk = (bufferStartAddr + CIRCULAR_BUFFER_SIZE) - writePointer;
if(W25Q128_SafeWrite(pData, writePointer, firstChunk) != HAL_OK) {
return HAL_ERROR;
}
if(W25Q128_SafeWrite(pData + firstChunk, bufferStartAddr, size - firstChunk) != HAL_OK) {
return HAL_ERROR;
}
writePointer = bufferStartAddr + (size - firstChunk);
} else {
if(W25Q128_SafeWrite(pData, writePointer, size) != HAL_OK) {
return HAL_ERROR;
}
writePointer += size;
}
// 处理读指针追赶
if((writePointer > readPointer && (writePointer - readPointer) >= CIRCULAR_BUFFER_SIZE) ||
(writePointer < readPointer && (readPointer - writePointer) <= (CIRCULAR_BUFFER_SIZE - size))) {
readPointer = writePointer;
}
return HAL_OK;
}
/**
* @brief 从循环缓冲区读取数据
* @param pData: 数据指针
* @param size: 要读取的数据大小
* @retval 实际读取的数据大小
*/
uint32_t CircularBuffer_Read(uint8_t *pData, uint32_t size) {
uint32_t available = 0;
// 计算可用数据量
if(writePointer >= readPointer) {
available = writePointer - readPointer;
} else {
available = (bufferStartAddr + CIRCULAR_BUFFER_SIZE) - readPointer + writePointer;
}
if(available == 0) return 0;
if(size > available) size = available;
// 处理缓冲区回绕
if(readPointer + size > bufferStartAddr + CIRCULAR_BUFFER_SIZE) {
uint32_t firstChunk = (bufferStartAddr + CIRCULAR_BUFFER_SIZE) - readPointer;
W25Q128_Read(pData, readPointer, firstChunk);
W25Q128_Read(pData + firstChunk, bufferStartAddr, size - firstChunk);
readPointer = bufferStartAddr + (size - firstChunk);
} else {
W25Q128_Read(pData, readPointer, size);
readPointer += size;
}
return size;
}
在实际项目中,这套方案成功解决了我的数据存储问题。特别是在处理跨边界写入时,再没有出现过数据损坏的情况。关键是要充分理解Flash的特性,并在设计时就考虑好边界条件。
