Portable W25Q64 SPI NOR flash driver for STM32 using STM32Cube HAL, with pluggable SPI transfer and delay callbacks.
- Transport-agnostic architecture — no hard dependency on a specific STM32 peripheral instance
- Config-driven setup through
W25Q_Config - User-supplied SPI transfer callback with full CS control
- User-supplied millisecond delay callback for HAL-agnostic busy polling
- Compatible with Winbond W25Q64 and clones (XMC XM25QH64C, BOYA BY25Q64)
- Read API with no alignment restriction — spans page and sector boundaries freely
- High-level write API (
W25Q_Write) with automatic page splitting - Low-level page program (
W25Q_PageProgram) for precise control - Full erase suite:
W25Q_EraseSector(4 KB)W25Q_EraseBlock32(32 KB)W25Q_EraseBlock64(64 KB)W25Q_EraseChip(8 MB)
- Power management (
W25Q_PowerDown,W25Q_ReleasePowerDown) - JEDEC ID read for startup chip verification
include/w25q64.h— public APIsrc/w25q64.c— driver implementationexamples/jedec_id_rw_verify.c— chip identification and read/write/erase verificationexamples/boot_counter.c— persistent counter using read-modify-erase-write patternexamples/power_mgmt_sector_layout.c— power-down usage and multi-sector flash layoutdocumentation.md— detailed API and usage documentation
- Configure your STM32 clock, GPIO, and SPI peripheral.
- Implement a
W25Q_SPITransferFncallback that asserts CS, callsHAL_SPI_TransmitReceive, and deasserts CS. - Implement a
W25Q_DelayMsFncallback that wrapsHAL_Delay. - Fill
W25Q_Configwith both callbacks and your SPI handle asuser_context. - Call
W25Q_ReadJEDECIDto verify the chip is responding. - Erase a sector with
W25Q_EraseSector, then write withW25Q_Writeand read withW25Q_Read.
static int32_t flash_spi_transfer(void *user_context,
const uint8_t *tx, uint8_t *rx, size_t length)
{
SPI_HandleTypeDef *spi = (SPI_HandleTypeDef *)user_context;
HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_RESET);
HAL_StatusTypeDef r = HAL_SPI_TransmitReceive(spi, (uint8_t *)tx, rx,
(uint16_t)length, HAL_MAX_DELAY);
HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_SET);
return (r == HAL_OK) ? W25Q_OK : W25Q_ERR_IO;
}
static void flash_delay_ms(void *user_context, uint32_t delay_ms)
{
(void)user_context;
HAL_Delay(delay_ms);
}
W25Q_Config flash = {
.transfer_fn = flash_spi_transfer,
.delay_ms_fn = flash_delay_ms,
.user_context = &hspi1
};
uint8_t buf[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
W25Q_EraseSector(&flash, 0x000000);
W25Q_Write(&flash, 0x000000, buf, sizeof(buf));
W25Q_Read(&flash, 0x000000, buf, sizeof(buf));| Code | Value | Meaning |
|---|---|---|
W25Q_OK |
0 |
Success |
W25Q_ERR_INVALID_ARG |
-1 |
NULL pointer or out-of-range argument |
W25Q_ERR_IO |
-2 |
SPI transfer callback returned failure |
W25Q_ERR_TIMEOUT |
-3 |
W25Q_WaitBusy timed out |
W25Q_ERR_NOT_ALIGNED |
-4 |
Address not aligned to required boundary |
- Erase before write. NOR flash bits only go
1→0. Writing into un-erased flash silently corrupts data. Always callW25Q_EraseSector(or a block erase) beforeW25Q_Write. - CS control is your responsibility. The
transfer_fncallback must assert CS low before the transfer and deassert high after — including on error. The driver sends each command as one complete callback call; splitting a call corrupts the transaction. W25Q_Writedoes not erase. It handles page boundary splitting automatically but assumes the target area is already erased.- After
W25Q_ReleasePowerDown, wait at least 3 µs (t_RES1) before issuing any command. The driver does not insert this delay. - For wear-levelled storage with power-loss resilience, use STM32-pio-libs/littlefs on top of this driver.
See documentation.md for the complete API reference.
