Embedded Mastery: C, Ada, Rust & Zig

Embedded Mastery: C, Ada, Rust & Zig

A Project-Based Tutorial for Embedded Development

Embedded Mastery Project

2026

A comprehensive, project-based tutorial for mastering embedded development in C, Ada, Rust, and Zig. Build 15 projects from LED blinker to safety-critical systems, all verified in QEMU and Renode emulators.

Project 5: SPI Flash Reader/Writer

Introduction

SPI (Serial Peripheral Interface) is the workhorse of embedded communication โ€” faster than I2C, simpler than USB, and found on everything from flash memory to displays. This project builds a complete SPI flash driver that reads, writes, and erases a W25Q series NOR flash chip, with CRC32 verification for data integrity.

What youโ€™ll learn:

SPI Protocol

SPI is a synchronous, full-duplex, single-master protocol using four signals:

Signal Name Direction Description
MOSI Master Out Slave In Master โ†’ Slave Data from master
MISO Master In Slave Out Slave โ†’ Master Data from slave
SCLK Serial Clock Master โ†’ Slave Clock from master
CS/SS Chip Select Master โ†’ Slave Active-low device select

Unlike I2C, SPI has no addressing โ€” the CS line selects the device. This means each device needs its own CS line, but the protocol is simpler and faster.

CPOL and CPHA: SPI Modes

SPI has four modes defined by clock polarity (CPOL) and clock phase (CPHA):

Mode CPOL CPHA Clock idle Data sampled Data shifted
0 0 0 Low Rising edge Falling edge
1 0 1 Low Falling edge Rising edge
2 1 0 High Falling edge Rising edge
3 1 1 High Rising edge Falling edge

Most flash memory (W25Q series) uses Mode 0 or Mode 3 โ€” both sample on the rising edge. Mode 0 is the most common default.

SPI Transaction

A single-byte SPI transaction is always full-duplex:

Master CS:  โ”€โ”€โ•ฒ___________________________________________โ•ฑโ”€โ”€
Master MOSI: โ”€โ”€โ•ณโ”€[D7]โ”€[D6]โ”€[D5]โ”€[D4]โ”€[D3]โ”€[D2]โ”€[D1]โ”€[D0]โ”€โ”€
Master SCLK: โ”€โ”€โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒ_โ•ฑโ•ฒโ”€
Master MISO: โ”€โ”€โ•ณโ”€[D7]โ”€[D6]โ”€[D5]โ”€[D4]โ”€[D3]โ”€[D2]โ”€[D1]โ”€[D0]โ”€โ”€

Every byte sent on MOSI simultaneously receives a byte on MISO. To read N bytes from flash, you must send N dummy bytes.

Flash Memory Commands

The W25Q64 (8 MB) and W25Q128 (16 MB) are industry-standard SPI NOR flash chips. Here are the essential commands:

Command Reference

Command Code Bytes Out Bytes In Description
Read Data 0x03 4 (cmd + 24-bit addr) N Read data at address
Fast Read 0x0B 5 (cmd + 24-bit addr + dummy) N Read with dummy byte
Page Program 0x02 4 + 1..256 0 Write up to 256 bytes
Sector Erase (4KB) 0x20 4 (cmd + 24-bit addr) 0 Erase 4KB sector
Block Erase (64KB) 0xD8 4 (cmd + 24-bit addr) 0 Erase 64KB block
Chip Erase 0xC7 1 0 Erase entire chip
Write Enable 0x06 1 0 Set WEL bit in status
Write Disable 0x04 1 0 Clear WEL bit
Read Status Reg 1 0x05 1 1 Busy + WEL + BP bits
Read JEDEC ID 0x9F 1 3 Manufacturer + device ID

JEDEC ID Structure

| Manufacturer (1 byte) | Memory Type (1 byte) | Capacity (1 byte) |
|        0xEF           |        0x40          |      0x17         |

Flash Memory Architecture

Page, Sector, Block Hierarchy

Chip (8 MB)
โ”œโ”€โ”€ Block 0 (64 KB)
โ”‚   โ”œโ”€โ”€ Sector 0 (4 KB)
โ”‚   โ”‚   โ”œโ”€โ”€ Page 0 (256 B)
โ”‚   โ”‚   โ”œโ”€โ”€ Page 1 (256 B)
โ”‚   โ”‚   โ”œโ”€โ”€ ...
โ”‚   โ”‚   โ””โ”€โ”€ Page 15 (256 B)
โ”‚   โ”œโ”€โ”€ Sector 1 (4 KB)
โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ Block 1 (64 KB)
โ””โ”€โ”€ ...

Critical constraints:

Read-Modify-Write for Partial Page Updates

To update a subset of a sector:

1. Read entire sector into RAM (4 KB)
2. Modify the target bytes in RAM
3. Erase the sector (all bytes โ†’ 0xFF)
4. Write the modified sector back (in 256-byte page chunks)

Warning: Never write across a page boundary without checking. If you send 300 bytes starting at page offset 200, bytes 200โ€“255 write correctly, then bytes 0โ€“43 of the same page are overwritten. This is the #1 flash programming bug.

Busy-Wait Polling vs Interrupt-Driven SPI

Busy-Wait Polling

The simplest approach โ€” poll the status register until the busy bit clears:

Write Enable โ†’ Send Command โ†’ Poll Status (BUSY=0?) โ†’ Done

Interrupt-Driven SPI

Use SPI TX/RX complete interrupts and DMA for zero-CPU transfers:

Configure DMA โ†’ Start SPI โ†’ ISR fires on completion โ†’ Clear busy flag

This project uses busy-wait polling for clarity. The interrupt-driven approach is covered in the Next Steps.

CRC32 for Data Integrity

CRC32 (Cyclic Redundancy Check) detects data corruption in stored data. The IEEE 802.3 polynomial is:

x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1

Polynomial: 0xEDB88320 (reflected form)

CRC32 Properties

Lookup Table Implementation

A byte-wise lookup table trades 1 KB of flash for ~8x speedup:

// Generate table at compile time
for each byte b (0..255):
    crc = b
    for 8 bits:
        crc = (crc >> 1) ^ (crc & 1 ? 0xEDB88320 : 0)
    table[b] = crc

Implementation

C: SPI Driver + Flash Read/Write/Erase with CRC32

SPI Driver (spi.h)

#ifndef SPI_DRIVER_H
#define SPI_DRIVER_H

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

typedef enum {
    SPI_OK = 0,
    SPI_ERR_TIMEOUT,
    SPI_ERR_MODE_FAULT,
    SPI_ERR_OVERRUN,
    SPI_ERR_CRC,
} spi_error_t;

typedef enum {
    SPI_MODE_0 = 0,  /* CPOL=0, CPHA=0 */
    SPI_MODE_1 = 1,  /* CPOL=0, CPHA=1 */
    SPI_MODE_2 = 2,  /* CPOL=1, CPHA=0 */
    SPI_MODE_3 = 3,  /* CPOL=1, CPHA=1 */
} spi_mode_t;

typedef struct {
    volatile uint32_t *cr1;
    volatile uint32_t *cr2;
    volatile uint32_t *sr;
    volatile uint32_t *dr;
    volatile uint32_t *crcpr;
    volatile uint32_t *rxcrcr;
    volatile uint32_t *txcrcr;
} spi_handle_t;

#define SPI_TIMEOUT_US  50000

spi_error_t spi_init(spi_handle_t *hspi, spi_mode_t mode, uint32_t baud_div);
spi_error_t spi_transfer(spi_handle_t *hspi, const uint8_t *tx,
                         uint8_t *rx, size_t len);
spi_error_t spi_write_then_read(spi_handle_t *hspi,
                                const uint8_t *tx_data, size_t tx_len,
                                uint8_t *rx_data, size_t rx_len);

#endif

SPI Driver Implementation (spi.c)

#include "spi.h"

/* STM32 SPI1 register base */
#define SPI1_BASE       0x40013000UL

/* CR1 bits */
#define CR1_SPE         (1U << 6)
#define CR1_MSTR        (1U << 2)
#define CR1_BR_SHIFT    3
#define CR1_CPOL        (1U << 1)
#define CR1_CPHA        (1U << 0)
#define CR1_SSM         (1U << 9)
#define CR1_SSI         (1U << 8)
#define CR1_BIDIMODE    (1U << 15)
#define CR1_BIDIOE      (1U << 14)

/* CR2 bits */
#define CR2_FRXTH       (1U << 12)
#define CR2_DS_SHIFT    8
#define CR2_DS_8BIT     0x7  /* 0111 = 8-bit data */

/* SR bits */
#define SR_RXNE         (1U << 0)
#define SR_TXE          (1U << 1)
#define SR_BSY          (1U << 7)
#define SR_OVR          (1U << 6)
#define SR_MODF         (1U << 5)

static void delay_us(uint32_t us) {
    volatile uint32_t count = us * 4;
    while (count--) {
        __asm volatile ("nop");
    }
}

spi_error_t spi_init(spi_handle_t *hspi, spi_mode_t mode, uint32_t baud_div) {
    hspi->cr1 = (volatile uint32_t *)(SPI1_BASE + 0x00);
    hspi->cr2 = (volatile uint32_t *)(SPI1_BASE + 0x04);
    hspi->sr = (volatile uint32_t *)(SPI1_BASE + 0x08);
    hspi->dr = (volatile uint32_t *)(SPI1_BASE + 0x0C);
    hspi->crcpr = (volatile uint32_t *)(SPI1_BASE + 0x10);
    hspi->rxcrcr = (volatile uint32_t *)(SPI1_BASE + 0x14);
    hspi->txcrcr = (volatile uint32_t *)(SPI1_BASE + 0x18);

    /* Disable SPI during config */
    *hspi->cr1 = 0;

    /* Configure CR2: 8-bit data, FIFO threshold */
    *hspi->cr2 = (CR2_DS_8BIT << CR2_DS_SHIFT) | CR2_FRXTH;

    /* Configure CR1: master, software NSS, mode, baud rate */
    uint32_t cr1 = CR1_MSTR | CR1_SSM | CR1_SSI;
    cr1 |= (mode & 1) ? CR1_CPHA : 0;
    cr1 |= (mode & 2) ? CR1_CPOL : 0;
    cr1 |= ((baud_div & 0x07) << CR1_BR_SHIFT);
    *hspi->cr1 = cr1;

    /* Enable SPI */
    *hspi->cr1 |= CR1_SPE;

    return SPI_OK;
}

spi_error_t spi_transfer(spi_handle_t *hspi, const uint8_t *tx,
                         uint8_t *rx, size_t len) {
    for (size_t i = 0; i < len; i++) {
        /* Wait for TXE */
        uint32_t timeout = SPI_TIMEOUT_US;
        while (!(*hspi->sr & SR_TXE)) {
            if (--timeout == 0) return SPI_ERR_TIMEOUT;
            delay_us(1);
        }

        /* Send byte */
        *(volatile uint8_t *)hspi->dr = tx ? tx[i] : 0xFF;

        /* Wait for RXNE */
        timeout = SPI_TIMEOUT_US;
        while (!(*hspi->sr & SR_RXNE)) {
            if (--timeout == 0) return SPI_ERR_TIMEOUT;
            delay_us(1);
        }

        /* Read received byte */
        if (rx) {
            rx[i] = *(volatile uint8_t *)hspi->dr;
        } else {
            (void)*(volatile uint8_t *)hspi->dr; /* Drain */
        }
    }

    /* Wait for not busy */
    uint32_t timeout = SPI_TIMEOUT_US;
    while (*hspi->sr & SR_BSY) {
        if (--timeout == 0) return SPI_ERR_TIMEOUT;
        delay_us(1);
    }

    return SPI_OK;
}

spi_error_t spi_write_then_read(spi_handle_t *hspi,
                                const uint8_t *tx_data, size_t tx_len,
                                uint8_t *rx_data, size_t rx_len) {
    /* Write phase */
    if (tx_len > 0) {
        spi_error_t err = spi_transfer(hspi, tx_data, NULL, tx_len);
        if (err != SPI_OK) return err;
    }

    /* Read phase (send dummy 0xFF bytes) */
    if (rx_len > 0) {
        spi_error_t err = spi_transfer(hspi, NULL, rx_data, rx_len);
        if (err != SPI_OK) return err;
    }

    return SPI_OK;
}

CRC32 Implementation (crc32.h)

#ifndef CRC32_H
#define CRC32_H

#include <stdint.h>
#include <stddef.h>

/* Initialize CRC32 lookup table (call once at startup) */
void crc32_init(void);

/* Calculate CRC32 over a buffer */
uint32_t crc32_calc(const uint8_t *data, size_t len);

/* Verify CRC32: returns true if CRC matches */
bool crc32_verify(const uint8_t *data, size_t len, uint32_t expected_crc);

#endif

CRC32 Implementation (crc32.c)

#include "crc32.h"

static uint32_t crc32_table[256];
static bool crc32_initialized = false;

void crc32_init(void) {
    if (crc32_initialized) return;

    const uint32_t poly = 0xEDB88320;

    for (uint32_t i = 0; i < 256; i++) {
        uint32_t crc = i;
        for (int j = 0; j < 8; j++) {
            crc = (crc >> 1) ^ ((crc & 1) ? poly : 0);
        }
        crc32_table[i] = crc;
    }

    crc32_initialized = true;
}

uint32_t crc32_calc(const uint8_t *data, size_t len) {
    uint32_t crc = 0xFFFFFFFF;

    for (size_t i = 0; i < len; i++) {
        uint8_t index = (uint8_t)(crc ^ data[i]);
        crc = (crc >> 8) ^ crc32_table[index];
    }

    return crc ^ 0xFFFFFFFF;
}

bool crc32_verify(const uint8_t *data, size_t len, uint32_t expected_crc) {
    return crc32_calc(data, len) == expected_crc;
}

Flash Driver (w25q.h)

#ifndef W25Q_H
#define W25Q_H

#include "spi.h"
#include "crc32.h"
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

/* W25Q command codes */
#define W25Q_CMD_READ_DATA      0x03
#define W25Q_CMD_FAST_READ      0x0B
#define W25Q_CMD_PAGE_PROGRAM   0x02
#define W25Q_CMD_SECTOR_ERASE   0x20
#define W25Q_CMD_BLOCK_ERASE    0xD8
#define W25Q_CMD_CHIP_ERASE     0xC7
#define W25Q_CMD_WRITE_ENABLE   0x06
#define W25Q_CMD_WRITE_DISABLE  0x04
#define W25Q_CMD_READ_STATUS    0x05
#define W25Q_CMD_JEDEC_ID       0x9F

/* Flash geometry */
#define W25Q_PAGE_SIZE      256
#define W25Q_SECTOR_SIZE    4096
#define W25Q_BLOCK_SIZE     65536

/* Status register bits */
#define W25Q_SR_BUSY        (1U << 0)
#define W25Q_SR_WEL         (1U << 1)

typedef enum {
    W25Q_OK = 0,
    W25Q_ERR_SPI,
    W25Q_ERR_TIMEOUT,
    W25Q_ERR_VERIFY,
    W25Q_ERR_JEDEC,
    W25Q_ERR_BOUNDARY,
} w25q_error_t;

typedef struct {
    spi_handle_t *spi;
    uint8_t cs_port;  /* GPIO port for CS control */
    uint16_t cs_pin;  /* GPIO pin for CS control */
} w25q_handle_t;

typedef struct {
    uint8_t manufacturer;
    uint8_t memory_type;
    uint8_t capacity;
} w25q_jedec_t;

/* CS control โ€” implement for your platform */
void w25q_cs_assert(w25q_handle_t *dev);
void w25q_cs_deassert(w25q_handle_t *dev);

/* Core API */
w25q_error_t w25q_init(w25q_handle_t *dev, spi_handle_t *spi);
w25q_error_t w25q_read_jedec(w25q_handle_t *dev, w25q_jedec_t *jedec);
w25q_error_t w25q_read(w25q_handle_t *dev, uint32_t addr,
                       uint8_t *data, size_t len);
w25q_error_t w25q_page_program(w25q_handle_t *dev, uint32_t addr,
                               const uint8_t *data, size_t len);
w25q_error_t w25q_sector_erase(w25q_handle_t *dev, uint32_t addr);
w25q_error_t w25q_write(w25q_handle_t *dev, uint32_t addr,
                        const uint8_t *data, size_t len);

/* Read with CRC32 verification */
typedef struct {
    uint8_t *data;
    size_t len;
    uint32_t crc;
} w25q_read_verified_t;

w25q_error_t w25q_read_verified(w25q_handle_t *dev, uint32_t addr,
                                w25q_read_verified_t *result);

#endif

Flash Driver Implementation (w25q.c)

#include "w25q.h"

static w25q_error_t wait_busy(w25q_handle_t *dev, uint32_t timeout_ms) {
    uint8_t status;
    uint32_t elapsed = 0;

    do {
        w25q_cs_assert(dev);
        uint8_t cmd = W25Q_CMD_READ_STATUS;
        spi_transfer(dev->spi, &cmd, NULL, 1);
        spi_transfer(dev->spi, NULL, &status, 1);
        w25q_cs_deassert(dev);

        if (!(status & W25Q_SR_BUSY)) return W25Q_OK;

        /* ~1ms delay */
        for (volatile int i = 0; i < 4000; i++);
        elapsed++;
    } while (elapsed < timeout_ms);

    return W25Q_ERR_TIMEOUT;
}

static w25q_error_t write_enable(w25q_handle_t *dev) {
    w25q_cs_assert(dev);
    uint8_t cmd = W25Q_CMD_WRITE_ENABLE;
    w25q_error_t err = spi_transfer(dev->spi, &cmd, NULL, 1);
    w25q_cs_deassert(dev);
    return err;
}

w25q_error_t w25q_init(w25q_handle_t *dev, spi_handle_t *spi) {
    dev->spi = spi;
    crc32_init();
    return W25Q_OK;
}

w25q_error_t w25q_read_jedec(w25q_handle_t *dev, w25q_jedec_t *jedec) {
    w25q_cs_assert(dev);

    uint8_t cmd = W25Q_CMD_JEDEC_ID;
    uint8_t rx[3];

    w25q_error_t err = spi_write_then_read(dev->spi, &cmd, 1, rx, 3);
    w25q_cs_deassert(dev);

    if (err != SPI_OK) return W25Q_ERR_SPI;

    jedec->manufacturer = rx[0];
    jedec->memory_type = rx[1];
    jedec->capacity = rx[2];

    return W25Q_OK;
}

w25q_error_t w25q_read(w25q_handle_t *dev, uint32_t addr,
                       uint8_t *data, size_t len) {
    uint8_t tx[4];
    tx[0] = W25Q_CMD_READ_DATA;
    tx[1] = (addr >> 16) & 0xFF;
    tx[2] = (addr >> 8) & 0xFF;
    tx[3] = addr & 0xFF;

    w25q_cs_assert(dev);
    w25q_error_t err = spi_write_then_read(dev->spi, tx, 4, data, len);
    w25q_cs_deassert(dev);

    return (err == SPI_OK) ? W25Q_OK : W25Q_ERR_SPI;
}

w25q_error_t w25q_page_program(w25q_handle_t *dev, uint32_t addr,
                               const uint8_t *data, size_t len) {
    if (len == 0 || len > W25Q_PAGE_SIZE) return W25Q_ERR_BOUNDARY;

    /* Check page boundary */
    uint32_t page_start = addr & ~(W25Q_PAGE_SIZE - 1);
    if (addr + len > page_start + W25Q_PAGE_SIZE) {
        return W25Q_ERR_BOUNDARY;
    }

    /* Enable writing */
    w25q_error_t err = write_enable(dev);
    if (err != W25Q_OK) return err;

    /* Send page program command + address + data */
    uint8_t header[4];
    header[0] = W25Q_CMD_PAGE_PROGRAM;
    header[1] = (addr >> 16) & 0xFF;
    header[2] = (addr >> 8) & 0xFF;
    header[3] = addr & 0xFF;

    w25q_cs_assert(dev);
    spi_transfer(dev->spi, header, NULL, 4);
    err = spi_transfer(dev->spi, data, NULL, len);
    w25q_cs_deassert(dev);

    if (err != SPI_OK) return W25Q_ERR_SPI;

    /* Wait for programming to complete (max 3ms for page program) */
    return wait_busy(dev, 10);
}

w25q_error_t w25q_sector_erase(w25q_handle_t *dev, uint32_t addr) {
    /* Address must be sector-aligned */
    if (addr & (W25Q_SECTOR_SIZE - 1)) {
        return W25Q_ERR_BOUNDARY;
    }

    /* Enable writing */
    w25q_error_t err = write_enable(dev);
    if (err != W25Q_OK) return err;

    /* Send sector erase command + address */
    uint8_t tx[4];
    tx[0] = W25Q_CMD_SECTOR_ERASE;
    tx[1] = (addr >> 16) & 0xFF;
    tx[2] = (addr >> 8) & 0xFF;
    tx[3] = addr & 0xFF;

    w25q_cs_assert(dev);
    err = spi_transfer(dev->spi, tx, NULL, 4);
    w25q_cs_deassert(dev);

    if (err != SPI_OK) return W25Q_ERR_SPI;

    /* Wait for erase to complete (max 400ms for sector erase) */
    return wait_busy(dev, 500);
}

w25q_error_t w25q_write(w25q_handle_t *dev, uint32_t addr,
                        const uint8_t *data, size_t len) {
    size_t written = 0;

    while (written < len) {
        /* Calculate bytes remaining in current page */
        uint32_t page_offset = addr & (W25Q_PAGE_SIZE - 1);
        size_t page_remaining = W25Q_PAGE_SIZE - page_offset;
        size_t chunk = (len - written < page_remaining) ?
                       (len - written) : page_remaining;

        w25q_error_t err = w25q_page_program(dev, addr,
                                             &data[written], chunk);
        if (err != W25Q_OK) return err;

        written += chunk;
        addr += chunk;
    }

    return W25Q_OK;
}

w25q_error_t w25q_read_verified(w25q_handle_t *dev, uint32_t addr,
                                w25q_read_verified_t *result) {
    w25q_error_t err = w25q_read(dev, addr, result->data, result->len);
    if (err != W25Q_OK) return err;

    result->crc = crc32_calc(result->data, result->len);
    return W25Q_OK;
}

Hardware Initialization (hw_init.c)

/* STM32F405 GPIO + RCC initialization for SPI1 */

/* RCC base address (STM32F4) */
#define RCC_BASE        0x40023800UL
#define RCC_AHB1ENR     (*(volatile uint32_t *)(RCC_BASE + 0x30))

/* GPIO base addresses (STM32F4: AHB1 bus) */
#define GPIOA_BASE      0x48000000UL
#define GPIOC_BASE      0x48000800UL

#define GPIO_MODER      (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIO_OTYPER     (*(volatile uint32_t *)(GPIOA_BASE + 0x04))
#define GPIO_OSPEEDR    (*(volatile uint32_t *)(GPIOA_BASE + 0x08))
#define GPIO_PUPDR      (*(volatile uint32_t *)(GPIOA_BASE + 0x0C))
#define GPIO_AFRL       (*(volatile uint32_t *)(GPIOA_BASE + 0x20))

/* SPI1 alternate function = AF5 on STM32F4 */
#define GPIO_AF_SPI1    5

void hw_init(void) {
    /* Enable GPIOA clock (bit 0 of AHB1ENR) */
    /* Enable GPIOC clock (bit 2 of AHB1ENR) */
    RCC_AHB1ENR |= (1U << 0) | (1U << 2);

    /* PA5 = SPI1_SCK: alternate function, push-pull, fast speed, pull-down */
    /* PA6 = SPI1_MISO: alternate function, push-pull, fast speed, pull-down */
    /* PA7 = SPI1_MOSI: alternate function, push-pull, fast speed, pull-down */

    /* MODER: set pin 5,6,7 to alternate function (10) */
    GPIO_MODER &= ~((0x3U << 10) | (0x3U << 12) | (0x3U << 14));
    GPIO_MODER |=  (0x2U << 10) | (0x2U << 12) | (0x2U << 14);

    /* OTYPER: push-pull (0) โ€” default */
    /* OSPEEDR: fast speed (10) */
    GPIO_OSPEEDR &= ~((0x3U << 10) | (0x3U << 12) | (0x3U << 14));
    GPIO_OSPEEDR |=  (0x2U << 10) | (0x2U << 12) | (0x2U << 14);

    /* PUPDR: pull-down (10) for SPI pins */
    GPIO_PUPDR &= ~((0x3U << 10) | (0x3U << 12) | (0x3U << 14));
    GPIO_PUPDR |=  (0x2U << 10) | (0x2U << 12) | (0x2U << 14);

    /* AFRL: set AF5 for pins 5, 6, 7 (4 bits each) */
    GPIO_AFRL &= ~((0xFU << 20) | (0xFU << 24) | (0xFU << 28));
    GPIO_AFRL |=  ((GPIO_AF_SPI1 << 20) | (GPIO_AF_SPI1 << 24) | (GPIO_AF_SPI1 << 28));

    /* PC4 = CS: output, push-pull, fast speed, pull-up */
    /* MODER: set pin 4 to output (01) */
    GPIO_MODER &= ~(0x3U << 8);
    GPIO_MODER |=  (0x1U << 8);

    /* Deassert CS (high) */
    (*(volatile uint32_t *)(GPIOC_BASE + 0x14)) |= (1U << 4);
}

Linker Script (stm32f405.ld)

/* STM32F405RG โ€” 1024K flash, 128K RAM */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .text :
    {
        KEEP(*(.isr_vector))
        *(.text*)
        *(.rodata*)
    } > FLASH

    .data :
    {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT > FLASH

    _sidata = LOADADDR(.data);

    .bss :
    {
        _sbss = .;
        *(.bss*)
        *(COMMON)
        _ebss = .;
    } > RAM
}

Startup Assembly (startup_stm32f405xx.s)

    .syntax unified
    .cpu cortex-m4
    .fpu softvfp
    .thumb

.global g_pfnVectors
.global Default_Handler

g_pfnVectors:
    .word _estack
    .word Reset_Handler
    .word NMI_Handler
    .word HardFault_Handler
    .word MemManage_Handler
    .word BusFault_Handler
    .word UsageFault_Handler
    .word 0
    .word 0
    .word 0
    .word 0
    .word SVC_Handler
    .word DebugMon_Handler
    .word 0
    .word PendSV_Handler
    .word SysTick_Handler

Reset_Handler:
    /* Copy .data from flash to RAM */
    ldr r0, =_sidata
    ldr r1, =_sdata
    ldr r2, =_edata
    b copy_data_loop_check
copy_data_loop:
    ldr r3, [r0], #4
    str r3, [r1], #4
copy_data_loop_check:
    cmp r1, r2
    blt copy_data_loop

    /* Zero .bss */
    ldr r0, =_sbss
    ldr r1, =_ebss
    movs r2, #0
    b zero_bss_loop_check
zero_bss_loop:
    str r2, [r0], #4
zero_bss_loop_check:
    cmp r0, r1
    blt zero_bss_loop

    /* Call hw_init then main */
    bl hw_init
    bl main
    b .

.size g_pfnVectors, .-g_pfnVectors

.weak NMI_Handler
.thumb_set NMI_Handler, Default_Handler

.weak HardFault_Handler
.thumb_set HardFault_Handler, Default_Handler

.weak MemManage_Handler
.thumb_set MemManage_Handler, Default_Handler

.weak BusFault_Handler
.thumb_set BusFault_Handler, Default_Handler

.weak UsageFault_Handler
.thumb_set UsageFault_Handler, Default_Handler

.weak SVC_Handler
.thumb_set SVC_Handler, Default_Handler

.weak DebugMon_Handler
.thumb_set DebugMon_Handler, Default_Handler

.weak PendSV_Handler
.thumb_set PendSV_Handler, Default_Handler

.weak SysTick_Handler
.thumb_set SysTick_Handler, Default_Handler

Default_Handler:
    b .

Main Application (main.c)

#include "w25q.h"
#include <stdio.h>
#include <string.h>

static spi_handle_t hspi;
static w25q_handle_t flash;

/* Platform-specific CS control */
void w25q_cs_assert(w25q_handle_t *dev) {
    (void)dev;
    /* GPIOA Pin 4 low (STM32F4: GPIOA ODR at 0x48000014) */
    /* *(volatile uint32_t *)0x48000014 &= ~(1U << 4); */
}

void w25q_cs_deassert(w25q_handle_t *dev) {
    (void)dev;
    /* GPIOA Pin 4 high */
    /* *(volatile uint32_t *)0x48000014 |= (1U << 4); */
}

int main(void) {
    /* Initialize SPI: Mode 0, baud = PCLK/16 */
    spi_init(&hspi, SPI_MODE_0, 3);

    /* Initialize flash driver */
    w25q_init(&flash, &hspi);

    /* Read and verify JEDEC ID */
    w25q_jedec_t jedec;
    w25q_error_t err = w25q_read_jedec(&flash, &jedec);
    if (err != W25Q_OK) {
        printf("JEDEC read failed: %d\n", err);
        return 1;
    }
    printf("JEDEC ID: %02X %02X %02X\n",
           jedec.manufacturer, jedec.memory_type, jedec.capacity);

    /* Test data */
    const char *test_msg = "Hello, SPI Flash! This is a test of the W25Q64 driver.";
    size_t msg_len = strlen(test_msg);
    uint32_t test_addr = 0x000000;  /* Start of flash */

    /* Erase sector containing test address */
    uint32_t sector_addr = test_addr & ~(W25Q_SECTOR_SIZE - 1);
    printf("Erasing sector at 0x%06lX...\n", sector_addr);
    err = w25q_sector_erase(&flash, sector_addr);
    if (err != W25Q_OK) {
        printf("Sector erase failed: %d\n", err);
        return 1;
    }
    printf("Sector erased.\n");

    /* Write data */
    printf("Writing %zu bytes at 0x%06lX...\n", msg_len, test_addr);
    err = w25q_write(&flash, test_addr,
                     (const uint8_t *)test_msg, msg_len);
    if (err != W25Q_OK) {
        printf("Write failed: %d\n", err);
        return 1;
    }
    printf("Write complete.\n");

    /* Read back with CRC verification */
    uint8_t read_buf[256];
    w25q_read_verified_t verified = {
        .data = read_buf,
        .len = msg_len,
        .crc = 0,
    };

    err = w25q_read_verified(&flash, test_addr, &verified);
    if (err != W25Q_OK) {
        printf("Read failed: %d\n", err);
        return 1;
    }

    /* Verify data matches */
    if (memcmp(read_buf, test_msg, msg_len) == 0) {
        printf("Data verified! CRC32: 0x%08lX\n", verified.crc);
        printf("Read: %.*s\n", (int)msg_len, read_buf);
    } else {
        printf("Data mismatch!\n");
        printf("Expected: %s\n", test_msg);
        printf("Got:      %.*s\n", (int)msg_len, read_buf);
    }

    /* Test page boundary handling */
    printf("\n--- Page boundary test ---\n");
    uint32_t boundary_addr = W25Q_PAGE_SIZE - 10;  /* 10 bytes before page end */
    uint8_t boundary_data[32];
    for (int i = 0; i < 32; i++) boundary_data[i] = (uint8_t)i;

    /* This should fail โ€” crosses page boundary */
    err = w25q_page_program(&flash, boundary_addr, boundary_data, 32);
    printf("Cross-page write (should fail): %d\n", err);

    /* Split it correctly */
    err = w25q_write(&flash, boundary_addr, boundary_data, 32);
    printf("Cross-page write via w25q_write: %d\n", err);

    return 0;
}

Rust: Generic SPI Flash Driver with embedded-hal Trait Bounds

// Cargo.toml
// [package]
// name = "w25q-driver"
// version = "0.1.0"
// edition = "2021"
//
// [dependencies]
// embedded-hal = "1.0"
// embedded-hal-bus = "0.1"

use core::marker::PhantomData;
use embedded_hal::spi::{SpiBus, SpiDevice, ErrorType};

/// W25Q command codes
mod cmd {
    pub const READ_DATA: u8 = 0x03;
    pub const FAST_READ: u8 = 0x0B;
    pub const PAGE_PROGRAM: u8 = 0x02;
    pub const SECTOR_ERASE: u8 = 0x20;
    pub const BLOCK_ERASE: u8 = 0xD8;
    pub const CHIP_ERASE: u8 = 0xC7;
    pub const WRITE_ENABLE: u8 = 0x06;
    pub const WRITE_DISABLE: u8 = 0x04;
    pub const READ_STATUS: u8 = 0x05;
    pub const JEDEC_ID: u8 = 0x9F;
}

/// Flash geometry constants
pub const PAGE_SIZE: usize = 256;
pub const SECTOR_SIZE: usize = 4096;
pub const BLOCK_SIZE: usize = 65536;

/// Status register bits
const SR_BUSY: u8 = 1 << 0;
const SR_WEL: u8 = 1 << 1;

/// W25Q driver errors
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum W25qError<SpiErr> {
    Spi(SpiErr),
    Timeout,
    VerifyFailed,
    InvalidJeId,
    PageBoundary,
    NotSectorAligned,
}

/// JEDEC identification
#[derive(Debug, Clone, Copy)]
pub struct JedecId {
    pub manufacturer: u8,
    pub memory_type: u8,
    pub capacity: u8,
}

impl JedecId {
    pub fn is_winbond(&self) -> bool {
        self.manufacturer == 0xEF
    }

    pub fn capacity_bytes(&self) -> Option<u32> {
        match self.capacity {
            0x14 => Some(1 << 17),  // W25Q80: 1 MB
            0x15 => Some(1 << 18),  // W25Q16: 2 MB
            0x16 => Some(1 << 19),  // W25Q32: 4 MB
            0x17 => Some(1 << 20),  // W25Q64: 8 MB
            0x18 => Some(1 << 21),  // W25Q128: 16 MB
            0x19 => Some(1 << 22),  // W25Q256: 32 MB
            _ => None,
        }
    }
}

/// Read result with CRC32
#[derive(Debug)]
pub struct VerifiedRead {
    pub data_len: usize,
    pub crc: u32,
}

/// CRC32 calculator (IEEE 802.3)
pub struct Crc32 {
    table: [u32; 256],
}

impl Crc32 {
    pub const fn new() -> Self {
        let mut table = [0u32; 256];
        let mut i = 0u32;
        while i < 256 {
            let mut crc = i;
            let mut j = 0;
            while j < 8 {
                if crc & 1 != 0 {
                    crc = (crc >> 1) ^ 0xEDB88320;
                } else {
                    crc >>= 1;
                }
                j += 1;
            }
            table[i as usize] = crc;
            i += 1;
        }
        Self { table }
    }

    pub fn calc(&self, data: &[u8]) -> u32 {
        let mut crc = 0xFFFFFFFF;
        for &byte in data {
            let index = (crc ^ byte as u32) & 0xFF;
            crc = (crc >> 8) ^ self.table[index as usize];
        }
        crc ^ 0xFFFFFFFF
    }
}

/// W25Q flash driver with generic SPI device
pub struct W25q<SPI> {
    spi: SPI,
    crc: Crc32,
}

impl<SPI, SpiErr> W25q<SPI>
where
    SPI: SpiDevice<Error = SpiErr>,
    SpiErr: core::fmt::Debug,
{
    /// Create new driver
    pub fn new(spi: SPI) -> Self {
        Self {
            spi,
            crc: Crc32::new(),
        }
    }

    /// Read JEDEC ID
    pub fn read_jedec(&mut self) -> Result<JedecId, W25qError<SpiErr>> {
        let mut tx = [cmd::JEDEC_ID, 0, 0, 0];
        self.spi.transfer_in_place(&mut tx)?;
        Ok(JedecId {
            manufacturer: tx[1],
            memory_type: tx[2],
            capacity: tx[3],
        })
    }

    /// Read data from flash
    pub fn read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), W25qError<SpiErr>> {
        let mut tx = [0u8; 4];
        tx[0] = cmd::READ_DATA;
        tx[1] = ((addr >> 16) & 0xFF) as u8;
        tx[2] = ((addr >> 8) & 0xFF) as u8;
        tx[3] = (addr & 0xFF) as u8;

        // Use write_read: send command+address, receive data
        self.spi.write(&tx)?;
        self.spi.read(buf)?;

        Ok(())
    }

    /// Read with CRC32 verification
    pub fn read_verified(
        &mut self,
        addr: u32,
        buf: &mut [u8],
    ) -> Result<VerifiedRead, W25qError<SpiErr>> {
        self.read(addr, buf)?;
        let crc = self.crc.calc(buf);
        Ok(VerifiedRead {
            data_len: buf.len(),
            crc,
        })
    }

    /// Wait until flash is not busy
    fn wait_busy(&mut self, timeout_ms: u32) -> Result<(), W25qError<SpiErr>> {
        for _ in 0..timeout_ms {
            let mut status = [cmd::READ_STATUS, 0];
            self.spi.transfer_in_place(&mut status)?;
            if status[1] & SR_BUSY == 0 {
                return Ok(());
            }
            // ~1ms delay
            cortex_m::asm::delay(16000);
        }
        Err(W25qError::Timeout)
    }

    /// Send write enable command
    fn write_enable(&mut self) -> Result<(), W25qError<SpiErr>> {
        self.spi.write(&[cmd::WRITE_ENABLE])?;
        Ok(())
    }

    /// Program a single page (must not cross page boundary)
    pub fn page_program(
        &mut self,
        addr: u32,
        data: &[u8],
    ) -> Result<(), W25qError<SpiErr>> {
        if data.is_empty() || data.len() > PAGE_SIZE {
            return Err(W25qError::PageBoundary);
        }

        let page_start = addr & !(PAGE_SIZE as u32 - 1);
        if addr + data.len() as u32 > page_start + PAGE_SIZE as u32 {
            return Err(W25qError::PageBoundary);
        }

        self.write_enable()?;

        let mut header = [0u8; 4];
        header[0] = cmd::PAGE_PROGRAM;
        header[1] = ((addr >> 16) & 0xFF) as u8;
        header[2] = ((addr >> 8) & 0xFF) as u8;
        header[3] = (addr & 0xFF) as u8;

        self.spi.write(&header)?;
        self.spi.write(data)?;

        self.wait_busy(10)
    }

    /// Erase a 4KB sector (address must be sector-aligned)
    pub fn sector_erase(&mut self, addr: u32) -> Result<(), W25qError<SpiErr>> {
        if addr & (SECTOR_SIZE as u32 - 1) != 0 {
            return Err(W25qError::NotSectorAligned);
        }

        self.write_enable()?;

        let mut tx = [0u8; 4];
        tx[0] = cmd::SECTOR_ERASE;
        tx[1] = ((addr >> 16) & 0xFF) as u8;
        tx[2] = ((addr >> 8) & 0xFF) as u8;
        tx[3] = (addr & 0xFF) as u8;

        self.spi.write(&tx)?;
        self.wait_busy(500)
    }

    /// Write data of any length, handling page boundaries automatically
    pub fn write(&mut self, addr: u32, data: &[u8]) -> Result<(), W25qError<SpiErr>> {
        let mut offset = 0;
        let mut current_addr = addr;

        while offset < data.len() {
            let page_offset = (current_addr as usize) & (PAGE_SIZE - 1);
            let page_remaining = PAGE_SIZE - page_offset;
            let chunk_len = core::cmp::min(data.len() - offset, page_remaining);

            self.page_program(current_addr, &data[offset..offset + chunk_len])?;

            offset += chunk_len;
            current_addr += chunk_len as u32;
        }

        Ok(())
    }
}

// --- Example usage ---
//
// use embedded_hal_bus::spi::ExclusiveDevice;
// use stm32f4xx_hal::{spi, gpio};
//
// #[entry]
// fn main() -> ! {
//     let dp = stm32::Peripherals::take().unwrap();
//     let rcc = dp.RCC.constrain();
//     let clocks = rcc.cfgr.sysclk(48.MHz()).freeze();
//
//     let gpioa = dp.GPIOA.split();
//     let sck = gpioa.pa5.into_alternate::<5>();
//     let miso = gpioa.pa6.into_alternate::<5>();
//     let mosi = gpioa.pa7.into_alternate::<5>();
//     let cs = gpioa.pa4.into_push_pull_output();
//
//     let spi_bus = spi::Spi::new(
//         dp.SPI1,
//         (sck, miso, mosi),
//         spi::Mode {
//             polarity: spi::Polarity::IdleLow,
//             phase: spi::Phase::CaptureOnFirstTransition,
//         },
//         1.MHz(),
//         clocks,
//     );
//
//     let spi_device = ExclusiveDevice::new(spi_bus, cs, Delay).unwrap();
//     let mut flash = W25q::new(spi_device);
//
//     let jedec = flash.read_jedec().expect("JEDEC read failed");
//     defmt::println!("JEDEC: {:02X} {:02X} {:02X}",
//         jedec.manufacturer, jedec.memory_type, jedec.capacity);
//
//     let test_data = b"Hello SPI Flash!";
//     flash.sector_erase(0).expect("Erase failed");
//     flash.write(0, test_data).expect("Write failed");
//
//     let mut buf = [0u8; 16];
//     let verified = flash.read_verified(0, &mut buf).expect("Read failed");
//     defmt::println!("CRC32: {:08X}, Data: {}", verified.crc, buf);
//
//     loop {}
// }

Ada: Flash Memory Management Package with Range-Checked Parameters

-- w25q.ads
with SPI_Driver; use SPI_Driver;

package W25Q is

   -- Flash geometry with strong typing
   Page_Size    : constant := 256;
   Sector_Size  : constant := 4096;
   Block_Size   : constant := 65536;

   -- Address type with range checking
   subtype Flash_Address is UInt32 range 0 .. 16_777_215;  -- 16 MB max
   subtype Page_Offset is UInt16 range 0 .. 255;
   subtype Sector_Index is UInt32 range 0 .. 4095;

   -- Command codes
   Cmd_Read_Data    : constant UInt8 := 16#03#;
   Cmd_Fast_Read    : constant UInt8 := 16#0B#;
   Cmd_Page_Program : constant UInt8 := 16#02#;
   Cmd_Sector_Erase : constant UInt8 := 16#20#;
   Cmd_Block_Erase  : constant UInt8 := 16#D8#;
   Cmd_Chip_Erase   : constant UInt8 := 16#C7#;
   Cmd_Write_Enable : constant UInt8 := 16#06#;
   Cmd_Write_Disable: constant UInt8 := 16#04#;
   Cmd_Read_Status  : constant UInt8 := 16#05#;
   Cmd_Jedec_Id     : constant UInt8 := 16#9F#;

   -- Status register bits
   SR_Busy : constant UInt8 := 16#01#;
   SR_WEL  : constant UInt8 := 16#02#;

   -- JEDEC ID record
   type JEDEC_ID is record
      Manufacturer : UInt8;
      Memory_Type  : UInt8;
      Capacity     : UInt8;
   end record;

   -- Verified read result
   type Verified_Read is record
      Data     : UInt8_Array (1 .. 256);
      Length   : UInt16;
      CRC      : UInt32;
   end record;

   -- Error types
   type W25Q_Error is (OK, SPI_Error, Timeout, Verify_Failed,
                       Invalid_JEDEC, Page_Boundary, Not_Aligned);

   -- Device handle
   type W25Q_Device is private;

   -- Initialize device
   procedure Initialize
     (Dev   : out W25Q_Device;
      Port  : access SPI_Port'Class);

   -- Check initialization status
   function Is_Valid (Dev : W25Q_Device) return Boolean;
   function Get_Error  (Dev : W25Q_Device) return W25Q_Error;

   -- Read JEDEC ID
   function Read_JEDEC (Dev : in out W25Q_Device) return JEDEC_ID;

   -- Read data from flash
   procedure Read
     (Dev    : in out W25Q_Device;
      Addr   : Flash_Address;
      Data   : out UInt8_Array;
      Length : UInt16);

   -- Program a single page (checked for boundary)
   procedure Page_Program
     (Dev    : in out W25Q_Device;
      Addr   : Flash_Address;
      Data   : UInt8_Array;
      Length : UInt16);

   -- Erase a 4KB sector (checked for alignment)
   procedure Sector_Erase
     (Dev  : in out W25Q_Device;
      Addr : Flash_Address);

   -- Write data of any length (handles page boundaries)
   procedure Write
     (Dev    : in out W25Q_Device;
      Addr   : Flash_Address;
      Data   : UInt8_Array;
      Length : UInt16);

   -- Read with CRC32 verification
   function Read_Verified
     (Dev    : in out W25Q_Device;
      Addr   : Flash_Address;
      Length : UInt16)
      return Verified_Read;

private

   type W25Q_Device is record
      Port     : access SPI_Port'Class := null;
      Last_Err : W25Q_Error := OK;
      Valid    : Boolean := False;
   end record;

end W25Q;
-- w25q.adb
with CRC32; use CRC32;

package body W25Q is

   -- CS control (platform-specific)
   procedure CS_Assert is null;
   procedure CS_Deassert is null;

   -- Wait for busy bit to clear
   procedure Wait_Busy
     (Dev      : in out W25Q_Device;
      Timeout  : UInt32)
   is
      Status : UInt8 := 0;
      Count  : UInt32 := 0;
      Cmd    : UInt8_Array (1 .. 2) := (Cmd_Read_Status, 0);
      Resp   : UInt8_Array (1 .. 2);
      Stat   : SPI_Status;
   begin
      loop
         exit when Count >= Timeout;
         CS_Assert;
         SPI_Transfer (Dev.Port.all, Cmd, Resp, 2, Stat);
         CS_Deassert;

         if Stat /= SPI_OK then
            Dev.Last_Err := SPI_Error;
            return;
         end if;

         Status := Resp (2);
         exit when (Status and SR_Busy) = 0;

         Count := Count + 1;
         delay 0.001;  -- 1ms
      end loop;

      if Count >= Timeout then
         Dev.Last_Err := Timeout;
      end if;
   end Wait_Busy;

   -- Send write enable
   procedure Write_Enable (Dev : in out W25Q_Device) is
      Cmd : UInt8_Array (1 .. 1) := (Cmd_Write_Enable);
      Resp : UInt8_Array (1 .. 1);
      Stat : SPI_Status;
   begin
      CS_Assert;
      SPI_Transfer (Dev.Port.all, Cmd, Resp, 1, Stat);
      CS_Deassert;
      if Stat /= SPI_OK then
         Dev.Last_Err := SPI_Error;
      end if;
   end Write_Enable;

   procedure Initialize
     (Dev   : out W25Q_Device;
      Port  : access SPI_Port'Class)
   is
   begin
      Dev.Port := Port;
      Dev.Last_Err := OK;
      Dev.Valid := True;
      CRC32_Init;
   end Initialize;

   function Is_Valid (Dev : W25Q_Device) return Boolean is
   begin
      return Dev.Valid;
   end Is_Valid;

   function Get_Error (Dev : W25Q_Device) return W25Q_Error is
   begin
      return Dev.Last_Err;
   end Get_Error;

   function Read_JEDEC (Dev : in out W25Q_Device) return JEDEC_ID is
      Cmd  : UInt8_Array (1 .. 4) := (Cmd_Jedec_Id, 0, 0, 0);
      Resp : UInt8_Array (1 .. 4);
      Stat : SPI_Status;
      Result : JEDEC_ID;
   begin
      Result.Manufacturer := 0;
      Result.Memory_Type := 0;
      Result.Capacity := 0;

      if not Dev.Valid then
         Dev.Last_Err := SPI_Error;
         return Result;
      end if;

      CS_Assert;
      SPI_Transfer (Dev.Port.all, Cmd, Resp, 4, Stat);
      CS_Deassert;

      if Stat /= SPI_OK then
         Dev.Last_Err := SPI_Error;
         return Result;
      end if;

      Result.Manufacturer := Resp (2);
      Result.Memory_Type := Resp (3);
      Result.Capacity := Resp (4);
      return Result;
   end Read_JEDEC;

   procedure Read
     (Dev    : in out W25Q_Device;
      Addr   : Flash_Address;
      Data   : out UInt8_Array;
      Length : UInt16)
   is
      Cmd : UInt8_Array (1 .. 4);
      Stat : SPI_Status;
   begin
      if not Dev.Valid then
         Dev.Last_Err := SPI_Error;
         return;
      end if;

      Cmd (1) := Cmd_Read_Data;
      Cmd (2) := UInt8 (Shift_Right (Addr, 16) and 16#FF#);
      Cmd (3) := UInt8 (Shift_Right (Addr, 8) and 16#FF#);
      Cmd (4) := UInt8 (Addr and 16#FF#);

      CS_Assert;
      SPI_Transfer (Dev.Port.all, Cmd, UInt8_Array (1 .. 4), 4, Stat);
      if Stat /= SPI_OK then
         CS_Deassert;
         Dev.Last_Err := SPI_Error;
         return;
      end if;
      SPI_Read (Dev.Port.all, Data, Length, Stat);
      CS_Deassert;

      if Stat /= SPI_OK then
         Dev.Last_Err := SPI_Error;
      end if;
   end Read;

   procedure Page_Program
     (Dev    : in out W25Q_Device;
      Addr   : Flash_Address;
      Data   : UInt8_Array;
      Length : UInt16)
   is
      Page_Start : UInt32;
      Cmd : UInt8_Array (1 .. 4);
      Stat : SPI_Status;
   begin
      if not Dev.Valid then
         Dev.Last_Err := SPI_Error;
         return;
      end if;

      -- Validate length
      if Length = 0 or Length > 256 then
         Dev.Last_Err := Page_Boundary;
         return;
      end if;

      -- Check page boundary
      Page_Start := Addr and not 255;
      if UInt32 (Addr) + UInt32 (Length) > Page_Start + 256 then
         Dev.Last_Err := Page_Boundary;
         return;
      end if;

      -- Enable writing
      Write_Enable (Dev);
      if Dev.Last_Err /= OK then
         return;
      end if;

      -- Send command + address
      Cmd (1) := Cmd_Page_Program;
      Cmd (2) := UInt8 (Shift_Right (Addr, 16) and 16#FF#);
      Cmd (3) := UInt8 (Shift_Right (Addr, 8) and 16#FF#);
      Cmd (4) := UInt8 (Addr and 16#FF#);

      CS_Assert;
      SPI_Transfer (Dev.Port.all, Cmd, UInt8_Array (1 .. 4), 4, Stat);
      if Stat /= SPI_OK then
         CS_Deassert;
         Dev.Last_Err := SPI_Error;
         return;
      end if;

      -- Send data
      SPI_Write (Dev.Port.all, Data, Length, Stat);
      CS_Deassert;

      if Stat /= SPI_OK then
         Dev.Last_Err := SPI_Error;
         return;
      end if;

      -- Wait for completion
      Wait_Busy (Dev, 10);
   end Page_Program;

   procedure Sector_Erase
     (Dev  : in out W25Q_Device;
      Addr : Flash_Address)
   is
      Cmd : UInt8_Array (1 .. 4);
      Stat : SPI_Status;
   begin
      if not Dev.Valid then
         Dev.Last_Err := SPI_Error;
         return;
      end if;

      -- Check sector alignment
      if (Addr and 4095) /= 0 then
         Dev.Last_Err := Not_Aligned;
         return;
      end if;

      Write_Enable (Dev);
      if Dev.Last_Err /= OK then
         return;
      end if;

      Cmd (1) := Cmd_Sector_Erase;
      Cmd (2) := UInt8 (Shift_Right (Addr, 16) and 16#FF#);
      Cmd (3) := UInt8 (Shift_Right (Addr, 8) and 16#FF#);
      Cmd (4) := UInt8 (Addr and 16#FF#);

      CS_Assert;
      SPI_Transfer (Dev.Port.all, Cmd, UInt8_Array (1 .. 4), 4, Stat);
      CS_Deassert;

      if Stat /= SPI_OK then
         Dev.Last_Err := SPI_Error;
         return;
      end if;

      Wait_Busy (Dev, 500);
   end Sector_Erase;

   procedure Write
     (Dev    : in out W25Q_Device;
      Addr   : Flash_Address;
      Data   : UInt8_Array;
      Length : UInt16)
   is
      Written      : UInt16 := 0;
      Current_Addr : UInt32 := UInt32 (Addr);
      Page_Offset  : UInt16;
      Page_Remain  : UInt16;
      Chunk        : UInt16;
   begin
      while Written < Length loop
         Page_Offset := UInt16 (Current_Addr and 255);
         Page_Remain := 256 - Page_Offset;
         Chunk := Length - Written;
         if Chunk > Page_Remain then
            Chunk := Page_Remain;
         end if;

         Page_Program (Dev,
                       Flash_Address (Current_Addr),
                       Data (Positive (Written) + 1 ..
                             Positive (Written + Chunk)),
                       Chunk);

         if Dev.Last_Err /= OK then
            return;
         end if;

         Written := Written + Chunk;
         Current_Addr := Current_Addr + UInt32 (Chunk);
      end loop;
   end Write;

   function Read_Verified
     (Dev    : in out W25Q_Device;
      Addr   : Flash_Address;
      Length : UInt16)
      return Verified_Read
   is
      Result : Verified_Read;
   begin
      Result.Length := 0;
      Result.CRC := 0;

      if Length > 256 then
         Dev.Last_Err := Page_Boundary;
         return Result;
      end if;

      Read (Dev, Addr, Result.Data, Length);
      if Dev.Last_Err /= OK then
         return Result;
      end if;

      Result.Length := Length;
      Result.CRC := CRC32_Calc (Result.Data, Length);
      return Result;
   end Read_Verified;

end W25Q;

Zig: Zero-Copy SPI Transactions with Packed Structs for Commands

// w25q.zig
const std = @import("std");

/// SPI error types
pub const SpiError = error{
    Timeout,
    ModeFault,
    Overrun,
};

/// SPI interface โ€” platform implementations provide this
pub const SpiInterface = struct {
    ctx: *anyopaque,
    transfer: *const fn (ctx: *anyopaque, tx: []const u8, rx: []u8) SpiError!void,
    write: *const fn (ctx: *anyopaque, data: []const u8) SpiError!void,
    read: *const fn (ctx: *anyopaque, data: []u8) SpiError!void,
    csAssert: *const fn (ctx: *anyopaque) void,
    csDeassert: *const fn (ctx: *anyopaque) void,
};

/// Flash geometry
pub const page_size: usize = 256;
pub const sector_size: usize = 4096;
pub const block_size: usize = 65536;

/// Packed command structures for zero-copy SPI transactions
pub const Command = packed struct {
    read_data: packed struct {
        code: u8 = 0x03,
        addr: u24,
    },
    page_program: packed struct {
        code: u8 = 0x02,
        addr: u24,
    },
    sector_erase: packed struct {
        code: u8 = 0x20,
        addr: u24,
    },
    jedec_id: packed struct {
        code: u8 = 0x9F,
    },
    write_enable: packed struct {
        code: u8 = 0x06,
    },
    read_status: packed struct {
        code: u8 = 0x05,
    },
};

/// Status register bits
const sr_busy: u8 = 1 << 0;
const sr_wel: u8 = 1 << 1;

/// JEDEC ID
pub const JedecId = packed struct {
    manufacturer: u8,
    memory_type: u8,
    capacity: u8,

    pub fn isWinbond(self: JedecId) bool {
        return self.manufacturer == 0xEF;
    }

    pub fn capacityBytes(self: JedecId) ?u32 {
        return switch (self.capacity) {
            0x14 => 1 << 17,
            0x15 => 1 << 18,
            0x16 => 1 << 19,
            0x17 => 1 << 20,
            0x18 => 1 << 21,
            0x19 => 1 << 22,
            else => null,
        };
    }
};

/// W25Q error union
pub const W25qError = SpiError || error{
    Timeout,
    VerifyFailed,
    InvalidJedec,
    PageBoundary,
    NotSectorAligned,
};

/// CRC32 calculator
pub const Crc32 = struct {
    table: [256]u32,

    pub fn init() Crc32 {
        var table: [256]u32 = undefined;
        var i: u32 = 0;
        while (i < 256) : (i += 1) {
            var crc = i;
            var j: u32 = 0;
            while (j < 8) : (j += 1) {
                crc = if (crc & 1 != 0) (crc >> 1) ^ 0xEDB88320 else crc >> 1;
            }
            table[i] = crc;
        }
        return Crc32{ .table = table };
    }

    pub fn calc(self: *const Crc32, data: []const u8) u32 {
        var crc: u32 = 0xFFFFFFFF;
        for (data) |byte| {
            const index = @as(u8, @intCast((crc ^ byte) & 0xFF));
            crc = (crc >> 8) ^ self.table[index];
        }
        return crc ^ 0xFFFFFFFF;
    }
};

/// Verified read result
pub const VerifiedRead = struct {
    data_len: usize,
    crc: u32,
};

/// W25Q flash driver
pub const W25q = struct {
    spi: SpiInterface,
    crc: Crc32,

    pub fn init(spi: SpiInterface) W25q {
        return W25q{
            .spi = spi,
            .crc = Crc32.init(),
        };
    }

    /// Read JEDEC ID
    pub fn readJedec(self: *W25q) W25qError!JedecId {
        var tx: [4]u8 = .{ 0x9F, 0, 0, 0 };
        var rx: [4]u8 = undefined;

        self.spi.csAssert(self.spi.ctx);
        try self.spi.transfer(self.spi.ctx, &tx, &rx);
        self.spi.csDeassert(self.spi.ctx);

        return JedecId{
            .manufacturer = rx[1],
            .memory_type = rx[2],
            .capacity = rx[3],
        };
    }

    /// Read data from flash
    pub fn read(self: *W25q, addr: u32, buf: []u8) W25qError!void {
        const cmd: [4]u8 = .{
            0x03,
            @as(u8, @intCast((addr >> 16) & 0xFF)),
            @as(u8, @intCast((addr >> 8) & 0xFF)),
            @as(u8, @intCast(addr & 0xFF)),
        };

        self.spi.csAssert(self.spi.ctx);
        try self.spi.write(self.spi.ctx, &cmd);
        try self.spi.read(self.spi.ctx, buf);
        self.spi.csDeassert(self.spi.ctx);
    }

    /// Read with CRC32 verification
    pub fn readVerified(self: *W25q, addr: u32, buf: []u8) W25qError!VerifiedRead {
        try self.read(addr, buf);
        const crc = self.crc.calc(buf);
        return VerifiedRead{
            .data_len = buf.len,
            .crc = crc,
        };
    }

    /// Wait until flash is not busy
    fn waitBusy(self: *W25q, timeout_ms: u32) W25qError!void {
        var i: u32 = 0;
        while (i < timeout_ms) : (i += 1) {
            var tx: [2]u8 = .{ 0x05, 0 };
            var rx: [2]u8 = undefined;

            self.spi.csAssert(self.spi.ctx);
            try self.spi.transfer(self.spi.ctx, &tx, &rx);
            self.spi.csDeassert(self.spi.ctx);

            if (rx[1] & sr_busy == 0) return;

            // ~1ms delay
            var j: usize = 0;
            while (j < 4000) : (j += 1) {}
        }
        return W25qError.Timeout;
    }

    /// Send write enable
    fn writeEnable(self: *W25q) W25qError!void {
        const cmd: [1]u8 = .{0x06};
        self.spi.csAssert(self.spi.ctx);
        try self.spi.write(self.spi.ctx, &cmd);
        self.spi.csDeassert(self.spi.ctx);
    }

    /// Program a single page
    pub fn pageProgram(self: *W25q, addr: u32, data: []const u8) W25qError!void {
        if (data.len == 0 or data.len > page_size) {
            return W25qError.PageBoundary;
        }

        const page_start = addr & ~@as(u32, page_size - 1);
        if (addr + @as(u32, @intCast(data.len)) > page_start + page_size) {
            return W25qError.PageBoundary;
        }

        try self.writeEnable();

        const header: [4]u8 = .{
            0x02,
            @as(u8, @intCast((addr >> 16) & 0xFF)),
            @as(u8, @intCast((addr >> 8) & 0xFF)),
            @as(u8, @intCast(addr & 0xFF)),
        };

        self.spi.csAssert(self.spi.ctx);
        try self.spi.write(self.spi.ctx, &header);
        try self.spi.write(self.spi.ctx, data);
        self.spi.csDeassert(self.spi.ctx);

        try self.waitBusy(10);
    }

    /// Erase a 4KB sector
    pub fn sectorErase(self: *W25q, addr: u32) W25qError!void {
        if (addr & (sector_size - 1) != 0) {
            return W25qError.NotSectorAligned;
        }

        try self.writeEnable();

        const cmd: [4]u8 = .{
            0x20,
            @as(u8, @intCast((addr >> 16) & 0xFF)),
            @as(u8, @intCast((addr >> 8) & 0xFF)),
            @as(u8, @intCast(addr & 0xFF)),
        };

        self.spi.csAssert(self.spi.ctx);
        try self.spi.write(self.spi.ctx, &cmd);
        self.spi.csDeassert(self.spi.ctx);

        try self.waitBusy(500);
    }

    /// Write data of any length
    pub fn write(self: *W25q, addr: u32, data: []const u8) W25qError!void {
        var offset: usize = 0;
        var current_addr = addr;

        while (offset < data.len) {
            const page_offset = @as(usize, @intCast(current_addr)) & (page_size - 1);
            const page_remaining = page_size - page_offset;
            const chunk_len = @min(data.len - offset, page_remaining);

            try self.pageProgram(current_addr, data[offset .. offset + chunk_len]);

            offset += chunk_len;
            current_addr += @as(u32, @intCast(chunk_len));
        }
    }
};
// main.zig
const std = @import("std");
const w25q = @import("w25q.zig");

// Mock SPI for demonstration
const MockSpi = struct {
    fn transfer(ctx: *anyopaque, tx: []const u8, rx: []u8) w25q.SpiError!void {
        _ = ctx;
        _ = tx;
        _ = rx;
    }

    fn write(ctx: *anyopaque, data: []const u8) w25q.SpiError!void {
        _ = ctx;
        _ = data;
    }

    fn read(ctx: *anyopaque, data: []u8) w25q.SpiError!void {
        _ = ctx;
        _ = data;
    }

    fn csAssert(ctx: *anyopaque) void {
        _ = ctx;
    }

    fn csDeassert(ctx: *anyopaque) void {
        _ = ctx;
    }

    fn interface() w25q.SpiInterface {
        return .{
            .ctx = undefined,
            .transfer = transfer,
            .write = write,
            .read = read,
            .csAssert = csAssert,
            .csDeassert = csDeassert,
        };
    }
};

pub fn main() !void {
    const spi = MockSpi.interface();
    var flash = w25q.W25q.init(spi);

    // Read JEDEC ID
    const jedec = try flash.readJedec();
    std.debug.print("JEDEC: {X:02} {X:02} {X:02}\n", .{
        jedec.manufacturer,
        jedec.memory_type,
        jedec.capacity,
    });

    // Test data
    const test_data = "Hello, SPI Flash! Testing W25Q driver with Zig.";
    const test_addr: u32 = 0;

    // Erase sector
    const sector_addr = test_addr & ~@as(u32, w25q.sector_size - 1);
    std.debug.print("Erasing sector at 0x{X:06}\n", .{sector_addr});
    try flash.sectorErase(sector_addr);

    // Write data
    std.debug.print("Writing {d} bytes\n", .{test_data.len});
    try flash.write(test_addr, test_data);

    // Read back with CRC
    var buf: [256]u8 = undefined;
    const verified = try flash.readVerified(test_addr, buf[0..test_data.len]);
    std.debug.print("CRC32: 0x{X:08}\n", .{verified.crc});
    std.debug.print("Data: {s}\n", .{buf[0..test_data.len]});

    // Verify data
    if (std.mem.eql(u8, buf[0..test_data.len], test_data)) {
        std.debug.print("Data verified successfully!\n", .{});
    } else {
        std.debug.print("Data mismatch!\n", .{});
    }
}

Build and Run Instructions

C (ARM GCC)

# Build
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O2 \
    -fno-common -ffunction-sections -fdata-sections \
    -Wall -Wextra -Werror \
    -T stm32f405.ld \
    -o w25q.elf \
    main.c spi.c w25q.c crc32.c startup_stm32f405xx.c

arm-none-eabi-objcopy -O binary w25q.elf w25q.bin
arm-none-eabi-size w25q.elf

Rust

rustup target add thumbv7em-none-eabihf
cargo build --release --target thumbv7em-none-eabihf

Ada

gprbuild -P w25q.gpr -XTARGET=arm-elf -O2

Zig

# Bare-metal ARM
zig build-exe main.zig -target thumbv7em-freestanding-eabihf -OReleaseSmall

# Host testing
zig build-exe main.zig -OReleaseFast

QEMU/Renode Verification

QEMU with SPI flash model

# QEMU supports m25p80 flash model (compatible with W25Q)
qemu-system-arm -M netduinoplus2 \
    -drive if=mtd,format=raw,file=flash_image.bin \
    -kernel w25q.elf \
    -semihosting \
    -d unimp,guest_errors \
    -serial stdio

Renode with SPI flash

# w25q.resc
mach create
machine LoadPlatformDescription @platforms/cpus/stm32f4.resc

# Add SPI flash at SPI1
spi.flash: Peripherals.W25Q64 @ spi1

mach start
sysbus LoadELF w25q.elf
start

# Monitor SPI transactions
showAnalyzer sysbus.spi1
logLevel 3 sysbus.spi1

Expected SPI transaction trace:

[0x00001000] SPI: CS asserted
[0x00001001] SPI: TX 0x9F (JEDEC ID)
[0x00001002] SPI: RX 0xEF (Winbond)
[0x00001003] SPI: RX 0x40 (Memory type)
[0x00001004] SPI: RX 0x17 (W25Q64)
[0x00001005] SPI: CS deasserted

[0x00002000] SPI: CS asserted
[0x00002001] SPI: TX 0x06 (Write Enable)
[0x00002002] SPI: CS deasserted

[0x00003000] SPI: CS asserted
[0x00003001] SPI: TX 0x20 0x00 0x00 0x00 (Sector Erase @ 0x000000)
[0x00003002] SPI: CS deasserted

What You Learned

Next Steps

Language Comparison

Feature C Rust Ada Zig
Command encoding #define constants, manual packing mod with typed constants Strongly typed constants packed struct for zero-copy
Page boundary check Manual bit math, easy to get wrong Checked with if + early return Range-checked subtypes Comptime-validatable
CRC32 table Global array, init at runtime const fn comptime generation Package-level initialization comptime block
SPI abstraction Raw register pointers embedded_hal::SpiDevice trait Abstract SPI_Port type Function pointer interface
Error handling Enum return codes Result<T, W25qError<SpiErr>> Typed error enum Error union with try
Address validation Runtime if checks Runtime checks, could be const Subtype range enforcement Runtime with @intCast
Zero-copy commands Manual byte arrays Slices with lifetime tracking Array types with bounds packed struct with u24
Binary size ~5KB (driver + CRC) ~7KB (with embedded-hal) ~9KB (runtime) ~6KB

Deliverables

References

STMicroelectronics Documentation

ARM Documentation

Flash Memory Documentation

Tools & Emulation