当你在STM32项目中需要存储大量数据时,外部Flash芯片W25Q128是个不错的选择。但真正用起来,你会发现它有个让人头疼的特性:写入不能跨页,擦除最小单位是扇区。这就意味着,如果你的数据刚好卡在页或扇区边界,常规的写入方法就会失效。
我最近在一个物联网项目中就遇到了这个问题。设备需要定期记录传感器数据,每条记录大小不固定,经常出现跨页甚至跨扇区的情况。经过几次失败的尝试和调试,终于总结出一套可靠的解决方案。下面就把我的实战经验分享给大家,特别是那些已经掌握W25Q128基础操作,但在处理复杂写入场景时遇到瓶颈的中级开发者。
W25Q128的16MB存储空间被组织成一个层级结构:
关键限制:
c复制// W25Q128存储结构定义
#define PAGE_SIZE 256 // 页大小(字节)
#define SECTOR_SIZE 4096 // 扇区大小(字节)
#define BLOCK_SIZE 65536 // 块大小(字节)
当数据长度超过当前页剩余空间时,需要将数据拆分到多个页写入。下面是处理跨页写入的关键函数:
首先实现单页写入的基础函数,这是所有写入操作的基础:
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;
}
基于单页写入,实现自动处理跨页情况的通用写入函数:
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;
}
由于擦除的最小单位是扇区(4KB),当写入跨越扇区边界时,需要特别处理:
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;
}
这个函数会自动计算需要擦除的扇区范围:
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;
}
将上述功能整合,实现一个完整的、健壮的写入流程:
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;
}
假设我们需要存储不定长的传感器数据包,数据包可能跨页或跨扇区:
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;
}
在实际使用中,还需要考虑以下优化点和注意事项:
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;
}
提示:在实际项目中,建议实现一个简单的磨损均衡算法,避免频繁擦写同一扇区。
对于数据记录应用,循环缓冲区是个实用方案:
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的特性,并在设计时就考虑好边界条件。