Skip to main content

cocotbext-qspi

QSPI flash verification for cocotb: a bus driver, a device-level API over the JEDEC command set, and a NOR flash model to test against.

Requires cocotb 2.0+. Tests run on Icarus Verilog.

pip install cocotbext-qspi

Why another SPI extension

cocotbext-spi covers single-lane SPI. This one covers flash specifically: dual and quad I/O reads, the write-enable latch, status polling, page program and sector erase — and it targets cocotb 2.x.

Pointing a testbench at the models

The Verilog ships inside the package, so there is nothing to vendor. Ask the package where it is:

VERILOG_DIR := $(shell python3 -c \
    "import cocotbext.qspi as q; print(q.verilog_dir())")

VERILOG_SOURCES  = $(VERILOG_DIR)/qspi_flash.v
VERILOG_SOURCES += $(VERILOG_DIR)/qspi_flash_test.v

Or from Python, cocotbext.qspi.verilog_dir() returns a pathlib.Path.

Usage

import cocotb
from cocotb.clock import Clock
from cocotbext.qspi import QspiFlash, CMD_QIOR4

@cocotb.test()
async def test_flash(dut):
    cocotb.start_soon(Clock(dut.clk, 20, unit="ns").start())

    flash = QspiFlash(dut)
    await flash.initialize()

    assert await flash.read_id() == [0xEF, 0x40, 0x18]

    # program() sets WEL, then polls the status register until WIP clears.
    await flash.program(0x1000, [0xDE, 0xAD, 0xBE, 0xEF])

    assert await flash.read(0x1000, 4) == [0xDE, 0xAD, 0xBE, 0xEF]
    assert await flash.read(0x1000, 4, opcode=CMD_QIOR4) == [0xDE, 0xAD, 0xBE, 0xEF]

    await flash.erase_sector(0x1000)
    assert await flash.read(0x1000, 4) == [0xFF] * 4

The protocol, and two things that catch people out

SPI mode 0: the master launches data while the clock is low, the device samples it on the rising edge, and vice versa.

The opcode is always single-lane. Only the address, mode byte and data widen. A quad I/O read is not "everything on four lanes" — it is one single-lane command byte, then four-lane address and data. Getting this wrong is the most common reason a driver talks to nothing.

Programming only clears bits. NOR flash needs an erase to set a bit back to 1. Programming 0x0F over 0xF0 gives 0x00, not 0x0F.

What it looks like on the wire

All three diagrams below are generated from a real simulation — capture.py runs the transactions, Icarus dumps a VCD, and render.py draws it. Nothing is drawn by hand, so they cannot drift away from what the model does.

A quad I/O read. The opcode goes out one bit per clock on a single lane; only then does the bus widen to four lanes for the address and data. Note the eight dummy cycles, where neither side drives while the bus turns around:

Fast read quad I/O

The same byte read three ways. This is the whole point of the wide modes — 40 clocks single-lane, 36 dual, 26 quad, for one byte at the same address:

One byte, three widths

A status read while a program is in flight. The device answers 0x01 — WIP set — which is what wait_ready() polls for:

Read status

To regenerate them:

make -C docs/waveforms        # run the sim, dump capture.vcd
make -C docs/waveforms svg    # capture.vcd -> *.svg

Commands

Opcode Name Address Data
0x06 Write enable — —
0x04 Write disable — —
0x05 Read status — 1 lane, repeats
0x9F JEDEC id — 1 lane, 3 bytes
0x03 Read 1 lane 1 lane
0xBB Fast read dual I/O 2 lanes 2 lanes, after mode byte + dummy
0xEB Fast read quad I/O 4 lanes 4 lanes, after mode byte + dummy
0x02 Page program 1 lane 1 lane; needs WEL, sets WIP
0x20 Sector erase (4 KB) 1 lane — ; needs WEL, sets WIP
0x66 / 0x99 Reset enable / reset — —

RSTEN arms a reset for the next command only; anything in between cancels it, so a stray 0x99 cannot reset a device mid-operation. initialize() issues the pair, so a test cannot inherit the write enable latch from whatever ran before it.

Status register: bit 0 WIP (write in progress), bit 1 WEL (write enable latch). wait_ready() polls it rather than assuming a fixed delay, which is what a real controller must do.

Bus signals

QspiBus.from_entity(dut) picks up clk, csb and io, plus io_out and io_oe.

A simulator will not let a testbench drive an inout net, so the top level splits the master's half into a value and a per-lane output enable:

wire [3:0] io;
assign io[0] = io_oe[0] ? io_out[0] : 1'bz;
assign io[1] = io_oe[1] ? io_out[1] : 1'bz;
assign io[2] = io_oe[2] ? io_out[2] : 1'bz;
assign io[3] = io_oe[3] ? io_out[3] : 1'bz;

Per-lane, not bus-wide: in single-lane mode the master drives io0 while the device answers on io1.

Note also that csb is left uninitialised in qspi_flash_test.v. The model frames transactions on chip-select edges, and an initialiser there races cocotb's first write at time 0 — the edge is lost and the device never starts. initialize() drives the sequence explicitly.

Testing

Two suites, and the split matters:

make -C tests                      # against our own JEDEC model: 11 tests
make -C tests -f Makefile.interop  # against PicoSoC's spiflash.v: 5 tests

The interop suite drives spiflash.v — a model this project did not write — and checks the bytes against known $readmemh content.

That distinction earned its keep. Testing only against our own model proves the driver and the model agree; it does not prove either is right. Driving somebody else's model immediately found that the master was dropping the first bit of every byte — our model had the same off-by-one assumption, so the closed loop had been happily agreeing with itself.

Layout

Path Contents
cocotbext/qspi/qspi_flash.py QspiFlash — JEDEC command set, status polling
cocotbext/qspi/qspi_master.py QspiMaster — byte transfers at 1/2/4 lanes
cocotbext/qspi/qspi_bus.py QspiBus — signal bundle
cocotbext/qspi/verilog/qspi_flash.v NOR flash model: WEL, WIP, page program, sector erase
cocotbext/qspi/verilog/qspi_flash_test.v cocotb top level
tests/reference/ third-party model for interop (ISC, see its README)

Licence

MIT. tests/reference/spiflash.v is ISC, © Claire Xenia Wolf.

Metadata

Release files for cocotbext-qspi 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cocotbext-qspi 0.2.0
File Size Uploaded
cocotbext_qspi-0.2.0.tar.gz 18.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cocotbext-qspi 0.2.0
File Interpreter ABI Platform
cocotbext_qspi-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 33.1 kB

Release files / cocotbext_qspi-0.2.0.tar.gz

Download URL cocotbext_qspi-0.2.0.tar.gz
Size 18.1 kB
Tags Source
SHA-256 checksum
How to use checksums
e9f31c671e616e267b1ae5679d04c781f6aa1cd4266a3982847ec33a8305c087
BLAKE2b-256 checksum
How to use checksums
56e5e2ed982d72c7721453815b99cfbc9ddb7704bdf809f7d7ac16d22377b6d2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / cocotbext_qspi-0.2.0-py3-none-any.whl

Download URL cocotbext_qspi-0.2.0-py3-none-any.whl
Size 14.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
84dfd7cff5dcb0ba05afca3fc4e96f0a289d7ded54bfb3a0428f4b8e078f1052
BLAKE2b-256 checksum
How to use checksums
1726585f5b378f705ec32871bd093ec2da3d2d22fa3aaad9c73e7d6d2cf393a3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page