# spif: an SPI NOR flash driver for the STM32 HAL

> spif is a C driver for SPI NOR flash for the STM32 HAL, covering the Winbond W25Q family and the compatible parts from GigaDevice, Macronix, Micron, Spansion, Adesto, Microchip and others. The library identifies the chip and its size from the JEDEC id, handles the four byte address switch, the write protection, the per maker erase times, and it can run blocking or non-blocking, by polling or by DMA.

**nimaltd/spif** — W25qxx, N25Qxx and other SPI FLASH driver for stm32 HAL

- Repository: https://github.com/nimaltd/spif
- Stars: 724 · Forks: 174
- Language: C
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/nimaltd-spif

## One driver for the parts that claim to be compatible

spif is a driver for SPI NOR flash, the W25Qxx and the many compatible chips from every well known maker, written in C for the STM32 HAL.

The compatibility claim is the whole point of the library, and it is also where the complexity lives. A Winbond W25Q part and a Macronix MX25L part are both SPI NOR flash with a similar command set, and the differences are in the details: the address width, the sector size, the time the chip takes to erase, and whether the part locks itself at power up. A driver written for one of them and used with the other is the classic source of a fault that only appears on the production run.

So the library does the reading of the datasheets for you. It finds the chip and its size by itself, from the JEDEC id, and it deals with pages, with four byte addresses, with each maker's own ways, with the chip's write and erase times, and with the limits of the HAL underneath it.

The scope beyond the chip itself is the other thing to note. It works on any STM32 family, bare metal or alongside FreeRTOS or ThreadX, and the data transfer can be done by polling or by DMA.

The licence is Apache 2.0, the last push was on 2026-10-15, and the only tagged release is 1.20.0 from November 2023. The repository holds the sources in a src directory, a host test suite in a test directory, a CMakeLists file, a clang format configuration, an installer description file, and the usual project files including a NOTICE file.

The test suite is described as running on every commit, against a model of each kind of chip, both by polling and by DMA. That is the detail that makes the rest of the design credible.

## The chip table, and the differences that matter

The supported chips table is the most useful page in the repository, because it says not only which parts work but what the driver does for each of them, and the differences are the interesting part.

Winbond is the reference implementation. A 1 or 2 Gbit Winbond chip is two or four dies, and each die is asked in turn whether it is busy, which is a detail that only appears in the larger parts and that a naive driver gets wrong.

The GigaDevice group, which also covers XMC, Puya, Zbit, Boya, ISSI, EON, ESMT and Fudan, works like a Winbond, with the exception that the ESMT F25L locks itself at power up and is unlocked by the driver.

Macronix is treated the same way, with the 1.8 volt parts included in that.

Micron and ST are different, because the busy state and the refused write are read from the flag status register rather than assumed, and because the old M25P has 64 KB sectors on some parts, 32 KB on the M25P10, and 256 KB on the M25P128.

Spansion, Cypress and Infineon are handled from their own id, with 64 KB or 256 KB sectors, and a refused write is cleared so the chip does not stay busy.

Adesto and Renesas read their size from their own id, and the AT25DF locks itself at power up and is unlocked.

And the Microchip SST26 parts are locked at every power up and unlocked, with their 32 KB and 64 KB erases done a sector at a time, because the part's 64 KB erase clears less near the ends.

That last detail is a good example of what this library is for. The erase behaviour of a 64 KB command on a part that has 32 KB sectors is not something you want to discover on hardware.

## Addresses, sizes, and the chips it refuses

The address width is the first piece of housekeeping, and it is handled automatically.

Up to 16 MB a chip takes three address bytes. Above that, the init function switches it to four, and Spansion, which has no such switch, gets its own four byte commands instead.

The second piece is the refuse list, and the documentation is unusually explicit about it. The following are not supported, and they are refused at init with a specific error code: the SST25, because it programs a byte or a word at a time rather than a page; the Adesto AT45DB DataFlash, because it works in a different way altogether; the Infineon Semper S25HL and S25HS, whose sectors and errors need handling of their own; and NAND flash such as the W25N, because that is a different kind of chip.

Refusing at init is the right behaviour. A driver that quietly mis-erases a DataFlash part leaves a subtle data corruption behind, and a driver that mis-erases a NAND part can destroy a block. Failing to bring the chip up is a loud, immediate, and easily diagnosed outcome, and the error code makes the reason clear without a debugger session.

The other guarantees in the same spirit are worth listing, because they describe what the driver refuses to do rather than what it does. A read or a write past the end of the chip is refused and never wrapped round to overwrite the start. An erase of a sector, a 32 KB or 64 KB block, or the whole chip is available, and a chip without one of those sizes has the operation done in smaller pieces, or refused, and never erased wrong. Write protection is found at start up, and the parts that lock themselves at power up are unlocked.

Every timeout is set once in the configuration header, from the datasheet plus a margin, and the driver asks the chip how long it needs rather than guessing, polling it every millisecond when there is an RTOS so the other threads run meanwhile.

## Blocking, non-blocking, polling, and DMA

The execution model has two switches, and they are orthogonal.

The first is blocking or non-blocking. In blocking mode the read, write, and erase functions return once the operation is done, and the handle carries no job. In non-blocking mode their underscore variants return at once, so a state machine can do other work and move the job on with the process function.

That split matters more in embedded work than it first appears, because the operations that take real time on flash are the erases, and a blocking erase of a 64 KB block can be hundreds of milliseconds. A blocking driver in a loop is a driver that stops your main loop for a quarter of a second at a time.

The second switch is the data transfer, polling or DMA, and it is set once for the library rather than per call.

The concurrency story is the third piece. With FreeRTOS, through either version of CMSIS-RTOS, or with ThreadX by way of the operating system abstraction layer, there is a mutex per chip, so two tasks cannot drive the same flash at the same time. The library waits and locks through that abstraction, which is also why the installer brings it along.

The chip's unique 64 bit id is read as well, in each maker's own way, for use as a serial number or a key, which is the sort of thing that saves a firmware engineer from inventing one.

And there is a file system on top. A LittleFS port ships with the library and is installed when the installer is asked for it, and the answer to that question is remembered, so an update does not ask again.

## The installer, and the two ways it lands in a project

Installation is done by a separate tool, and the tool does more than copy files.

The stm32-installer copies the library into the project and adds it to the CMake, STM32CubeIDE, Keil, IAR, or Makefile project. The project file is backed up first, and the installer also checks that SPI is enabled in the CubeMX project and says so if it is not, which is a check that saves a lot of time when the answer is a long list of nothing happening on the bus.

The installer itself is installed once per machine with pip.

```bash
pip install stm32-installer
```

And then, from the root of the STM32 project, the library is added.

```bash
stm32-installer nimaltd/spif
```

The library waits and locks through the operating system abstraction layer, so the installer puts that in first and asks for its folder as it would for any library. If another library already brought the abstraction layer into the project, that copy is kept and nothing is asked, which is the kind of detail that avoids a duplicate symbol at link time.

The first install also asks whether the LittleFS file system is wanted, and the answer is kept, so an update does not ask again.

There is a path for a machine with no internet, and it is the one to know about before you are on a bench with no network. You give the installer the downloaded zip in place of the repository name, and it unpacks nothing and copies only the files the library needs.

```bash
stm32-installer D:/Downloads/spif-main.zip
```

If the abstraction layer is not on the machine either, you pass its zip as well, so the installer does not try to fetch it.

## Updating, pinning, and the limits of the support

Updates are the same command again, and the behaviour is specified precisely enough to be worth reading before you rely on it.

The code is replaced. The abstraction layer is updated only when the library needs a newer one. And the settings in both configuration files, the ones between the user code lines, are kept either way, which is the part that matters, because that is where the timeouts and the other per project settings live and where losing them would be painful.

That flow needs the installer at version 1.9.0 or newer, and the note is that an older one cannot find the library at all, with the fix being an upgrade of the installer.

Pinning is a flag rather than a separate workflow. By default you get the newest code on the main branch, and to hold a project on one release you add the ref option with a tag, a branch, or a commit.

```bash
stm32-installer nimaltd/spif --ref v3.0.0
```

There is also the option of copying the files in by hand, and the README shows the first step of that.

The last thing to check is the test story, because it tells you how much to trust the library on parts the table lists. The suite is a host unit test that runs on a PC, against a model of each kind of chip, and it runs on every commit in both the polling and the DMA paths. A model of each kind of chip is what makes that possible, since the point is to check the behaviour the datasheet describes rather than the behaviour of one particular part that happens to be on the desk.

And the parts outside the table are not silent omissions. The four families that are refused at init are named, with the reason for each, so you find out at bring up rather than at three in the morning in production.

## Conclusion

spif fits an embedded project that puts SPI NOR flash on an STM32 and wants one driver for the part that happens to be fitted, rather than code written against the Winbond command set on the assumption that every other maker is compatible. It does not fit a project using NAND flash or one of the parts the library explicitly refuses, and it does not fit a team that cannot accept an Apache 2.0 licensed dependency with its own installer. Verify first that the flash part in the design is on the supported list, that the erase geometry the driver will use matches the datasheet for that part, and that the timeout values in the configuration header are set from the datasheet of the fitted chip, because the driver asks the chip how long it needs rather than assuming.

## FAQ

### What is the spif library?

It is a C driver for SPI NOR flash written for the STM32 HAL, covering the Winbond W25Q and the compatible parts from GigaDevice, Macronix, Micron, Spansion, Adesto, Microchip and others. It reads or writes any number of bytes at any address, erases by sector, block or whole chip, and identifies the chip and its size from the JEDEC id.

### Which flash chips does spif refuse to work with?

The SST25, which programs a byte or a word at a time rather than a page, the Adesto AT45DB DataFlash, the Infineon Semper S25HL and S25HS, and NAND flash such as the W25N. They are refused at init with a specific error code rather than being attempted and misbehaving.

### Does spif support non-blocking operation and DMA?

Yes, and they are two separate settings. The blocking functions return once the operation is done and the handle carries no job, while the underscore variants return at once so a state machine can move the job on with the process function. The data transfer itself is polling or DMA, and with FreeRTOS or ThreadX there is a mutex per chip so two tasks cannot drive the same flash at once.

### How do I install spif into an STM32 project?

With the stm32-installer, which copies the library in, adds it to the CMake, CubeIDE, Keil, IAR or Makefile project, backs the project file up, and checks that SPI is enabled in the CubeMX project. You install the installer with pip, then run stm32-installer nimaltd/spif from the root of the project, and the installer brings the operating system abstraction layer along.

### How do I pin spif to a particular release?

Add the ref option with a tag, a branch, or a commit, for example stm32-installer nimaltd/spif --ref v3.0.0. By default the installer takes the newest code on the main branch, and a download can be installed instead by giving the installer the zip file in place of the repository name.

## Sources

- [Issues](https://github.com/nimaltd/spif/issues)
- [License: Apache-2.0](https://github.com/nimaltd/spif/blob/main/LICENSE)
- [nimaltd/spif on GitHub](https://github.com/nimaltd/spif)
- [README](https://github.com/nimaltd/spif/blob/main/README.md)
- [Releases](https://github.com/nimaltd/spif/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/nimaltd-spif
