Adding a W25Q128 SPI flash memory to your STM32 project gives you an additional 16MB of non-volatile external storage — perfect for storing audio samples, firmware OTA updates, configuration data, file systems, and data logs. The W25Q128 uses SPI communication, making it easy to interface with any STM32 that has SPI peripheral. This comprehensive guide covers STM32 SPI Flash W25Q128 read-write operations using HAL and includes a custom driver implementation.
Table of Contents
- W25Q128 Overview
- Hardware Connection to STM32
- STM32CubeMX SPI Configuration
- W25Q128 HAL Driver Implementation
- Read and Write Operations
- Sector Erase Operations
- LittleFS/FatFS on W25Q128
- Frequently Asked Questions
W25Q128 Overview
The W25Q128 (Winbond) is a 128Mbit (16MB) SPI NOR Flash memory chip widely available in India for ₹40–₹80 per chip (SOIC-8 or DIP-8 adapter). Key characteristics:
- Capacity: 128Mb = 16MB = 16,777,216 bytes
- Interface: SPI (also supports Dual-SPI and Quad-SPI/QSPI)
- Erase units: 4KB sector, 32KB block, 64KB block, chip erase
- Page write: 256 bytes per page write operation
- Voltage: 2.7–3.6V (3.3V nominal — compatible with STM32)
- Speed: Up to 104 MHz SPI clock (standard SPI)
- Endurance: 100,000 write cycles per sector
- Data retention: 20 years
W25Q64 (8MB), W25Q32 (4MB), and W25Q256 (32MB) are pin-compatible alternatives — same driver works for all.
Hardware Connection to STM32
W25Q128 in SOIC-8 or via a DIP-8 adapter (for breadboard use):
W25Q128 Pin → STM32 Pin
CS (pin 1) → GPIO output (e.g., PA4) — active low chip select
DO (pin 2) → SPI MISO (Master In, Slave Out)
WP (pin 3) → 3.3V (Write Protect disabled)
GND (pin 4) → GND
DI (pin 5) → SPI MOSI (Master Out, Slave In)
CLK (pin 6) → SPI SCK (Clock)
HOLD(pin 7) → 3.3V (Hold function disabled)
VCC (pin 8) → 3.3V
// Example for STM32F4 (SPI1):
CS → PA4
SCK → PA5
MISO→ PA6
MOSI→ PA7
// Decoupling: 100nF capacitor close to VCC pin
// SPI mode: Mode 0 (CPOL=0, CPHA=0)
STM32CubeMX SPI Configuration
- Open .ioc file → Connectivity → SPI1
- Mode: Full-Duplex Master
- NSS Signal: Disable (we control CS manually via GPIO)
- Data Size: 8 Bits
- CPOL: Low (Mode 0)
- CPHA: 1 Edge (Mode 0)
- Prescaler: Set for ≤10 MHz to start (safe for most W25Q128 operations)
- Configure CS pin as GPIO Output, push-pull, initial HIGH
- Generate Code
W25Q128 HAL Driver Implementation
/* w25q128.h */
#ifndef W25Q128_H
#define W25Q128_H
#include "stm32f4xx_hal.h"
#define W25Q128_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define W25Q128_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
/* W25Q128 Commands */
#define W25Q_CMD_WRITE_ENABLE 0x06
#define W25Q_CMD_WRITE_DISABLE 0x04
#define W25Q_CMD_READ_STATUS1 0x05
#define W25Q_CMD_READ_DATA 0x03
#define W25Q_CMD_PAGE_PROGRAM 0x02
#define W25Q_CMD_SECTOR_ERASE_4K 0x20
#define W25Q_CMD_BLOCK_ERASE_64K 0xD8
#define W25Q_CMD_CHIP_ERASE 0xC7
#define W25Q_CMD_JEDEC_ID 0x9F
/* Status Register bits */
#define W25Q_STATUS_BUSY 0x01
#define W25Q_STATUS_WEL 0x02
extern SPI_HandleTypeDef hspi1;
/* Function prototypes */
uint32_t W25Q_ReadJEDECID(void);
void W25Q_Read(uint32_t address, uint8_t *data, uint32_t length);
void W25Q_Write(uint32_t address, uint8_t *data, uint32_t length);
void W25Q_EraseSector(uint32_t sectorAddr);
void W25Q_EraseChip(void);
void W25Q_WaitBusy(void);
#endif
/* w25q128.c */
#include "w25q128.h"
static void W25Q_SendCommand(uint8_t cmd) {
W25Q128_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
W25Q128_CS_HIGH();
}
void W25Q_WaitBusy(void) {
uint8_t status;
uint8_t cmd = W25Q_CMD_READ_STATUS1;
do {
W25Q128_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, &status, 1, HAL_MAX_DELAY);
W25Q128_CS_HIGH();
} while (status & W25Q_STATUS_BUSY);
}
uint32_t W25Q_ReadJEDECID(void) {
uint8_t cmd = W25Q_CMD_JEDEC_ID;
uint8_t id[3];
W25Q128_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, id, 3, HAL_MAX_DELAY);
W25Q128_CS_HIGH();
// W25Q128 returns: 0xEF 0x40 0x18
return (id[0] << 16) | (id[1] <> 16) & 0xFF; // Address byte 2 (MSB)
header[2] = (address >> 8) & 0xFF; // Address byte 1
header[3] = address & 0xFF; // Address byte 0 (LSB)
W25Q128_CS_LOW();
HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, data, length, HAL_MAX_DELAY);
W25Q128_CS_HIGH();
}
void W25Q_WritePage(uint32_t address, uint8_t *data, uint32_t length) {
uint8_t header[4];
// Must erase sector before writing!
W25Q_SendCommand(W25Q_CMD_WRITE_ENABLE);
W25Q_WaitBusy();
header[0] = W25Q_CMD_PAGE_PROGRAM;
header[1] = (address >> 16) & 0xFF;
header[2] = (address >> 8) & 0xFF;
header[3] = address & 0xFF;
W25Q128_CS_LOW();
HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY);
HAL_SPI_Transmit(&hspi1, data, length, HAL_MAX_DELAY);
W25Q128_CS_HIGH();
W25Q_WaitBusy();
}
void W25Q_EraseSector(uint32_t sectorAddr) {
uint8_t header[4];
W25Q_SendCommand(W25Q_CMD_WRITE_ENABLE);
W25Q_WaitBusy();
// Align to 4KB sector boundary
sectorAddr &= 0xFFFFF000;
header[0] = W25Q_CMD_SECTOR_ERASE_4K;
header[1] = (sectorAddr >> 16) & 0xFF;
header[2] = (sectorAddr >> 8) & 0xFF;
header[3] = sectorAddr & 0xFF;
W25Q128_CS_LOW();
HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY);
W25Q128_CS_HIGH();
W25Q_WaitBusy(); // Sector erase takes up to 400ms!
}
Read and Write Operations
/* Usage in main.c */
#include "w25q128.h"
#include <string.h>
int main(void) {
// Init peripherals...
// Verify chip is present
uint32_t jedec = W25Q_ReadJEDECID();
if (jedec != 0xEF4018) {
// W25Q128 not found! Check connections.
Error_Handler();
}
// Erase sector before writing (mandatory for NOR flash)
W25Q_EraseSector(0x000000); // Erase first 4KB sector
// Write data (must be within same page = 256 bytes for single write)
char message[] = "Hello from STM32! Stored in W25Q128 Flash.";
W25Q_WritePage(0x000000, (uint8_t*)message, strlen(message));
// Read back and verify
uint8_t readBuf[64];
W25Q_Read(0x000000, readBuf, 64);
readBuf[63] = 0; // Null-terminate for print
// Should print original message
printf("Read: %s
", readBuf);
while(1) {}
}
Sector Erase Operations
NOR flash can only erase bits (0→1), not set them. You must erase before writing:
- Sector erase (4KB): Fastest, ~100–400ms. Use for frequently updated data.
- Block erase (32KB or 64KB): Erases larger region, ~200–2000ms. For OTA firmware updates.
- Chip erase: Erases entire 16MB, ~25–60 seconds. Rare — only on full initialisation.
// Safe write function that handles multi-page and erase
void W25Q_Write(uint32_t address, uint8_t *data, uint32_t length) {
uint32_t currentAddr = address;
uint8_t *currentData = data;
uint32_t remaining = length;
while (remaining > 0) {
uint32_t pageOffset = currentAddr % 256;
uint32_t writeLen = 256 - pageOffset;
if (writeLen > remaining) writeLen = remaining;
W25Q_WritePage(currentAddr, currentData, writeLen);
currentAddr += writeLen;
currentData += writeLen;
remaining -= writeLen;
}
}
LittleFS/FatFS on W25Q128
Mount a filesystem on W25Q128 for organised file storage:
// LittleFS (recommended for MCU flash) - provides wear levelling
// Add littlefs library to your project
#include "lfs.h"
// LittleFS configuration pointing to W25Q128 functions
int lfs_read(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size) {
W25Q_Read(block * 4096 + off, buffer, size);
return 0;
}
int lfs_prog(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size) {
W25Q_WritePage(block * 4096 + off, (uint8_t*)buffer, size);
return 0;
}
int lfs_erase(const struct lfs_config *c, lfs_block_t block) {
W25Q_EraseSector(block * 4096);
return 0;
}
const struct lfs_config cfg = {
.read = lfs_read,
.prog = lfs_prog,
.erase = lfs_erase,
.sync = lfs_sync,
.read_size = 256,
.prog_size = 256,
.block_size = 4096, // 4KB sectors
.block_count = 4096, // 16MB / 4KB = 4096 sectors
.lookahead_size = 16,
};
Frequently Asked Questions
What is the W25Q128 read speed compared to STM32’s internal flash?
W25Q128 via SPI at 10 MHz reads at ~1.25 MB/s. Via QSPI (Quad-SPI) at 80 MHz, it reaches ~40 MB/s. STM32F4’s internal flash at 168 MHz reads at up to 21 MB/s without wait states (slower with wait states at high speeds). For large data storage, external flash is far more cost-effective despite the slower interface.
Is W25Q128 safe for storing firmware updates (OTA)?
Yes — W25Q128 is commonly used for OTA firmware storage. The typical approach: download new firmware via WiFi (on ESP32 or STM32 with WiFi module) and write it to W25Q128, then have a bootloader copy it to internal flash on next boot. The STM32 bootloader must be in internal flash and run before the application starts.
Can W25Q128 operate at 3.3V or 5V?
W25Q128 operates at 2.7–3.6V — it’s a 3.3V device. Do not connect 5V to VCC or signal pins. STM32 GPIO outputs 3.3V maximum, which is fully compatible. If using with 5V Arduino (Uno, Mega), use a voltage divider on MOSI and SCK lines (MISO is safe as-is since it’s W25Q → Arduino direction).
How do I know if my W25Q128 is genuine or a counterfeit?
Read the JEDEC ID: genuine W25Q128 returns 0xEF4018. Counterfeits often return incorrect IDs. Also check the Unique ID (64-bit): genuine chips have a proper unique ID. Counterfeit flash is less reliable and may have smaller actual capacity than labelled — use vendor-sourced components for production designs.
How long does chip erase take on W25Q128?
W25Q128 chip erase takes 25–60 seconds (typical 50 seconds). This is too long for most applications — use sector erase (4KB, ~400ms) or block erase (64KB, ~2000ms) instead. Plan your memory layout to use the smallest erase unit that contains your data.
Add comment