spi_flash: add generic SPI NOR flash layer (Phase 3)
Transport-agnostic (xfer callback) layer with a JEDEC-ID chip database and detect/read/erase_sector/program_page/program/verify. Standard opcode set, 3- and 4-byte addressing (4-byte command opcodes for parts over 16 MB). Seeded with the KCU105's MT25QU256 plus common Winbond/Macronix/ISSI/Micron parts. detect + read validated on the KCU105 over the proxy. erase/program are implemented but not yet hardware-tested (destructive on a config flash).
This commit is contained in:
7
modules/spi_flash/CMakeLists.txt
Normal file
7
modules/spi_flash/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
file(GLOB_RECURSE ALL_SOURCES "*.c")
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||
|
||||
add_library(spi_flash ${ALL_SOURCES})
|
||||
202
modules/spi_flash/spi_flash.c
Normal file
202
modules/spi_flash/spi_flash.c
Normal file
@@ -0,0 +1,202 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "spi_flash.h"
|
||||
|
||||
/* Standard JEDEC SPI NOR opcodes shared by all parts. */
|
||||
#define CMD_RDID 0x9F /* read JEDEC ID */
|
||||
#define CMD_WREN 0x06 /* write enable */
|
||||
#define CMD_RDSR 0x05 /* read status register */
|
||||
#define SR_WIP 0x01 /* status: write-in-progress */
|
||||
|
||||
/* Largest single transport transaction for read/verify, to bound the
|
||||
* buffers the transport has to allocate. */
|
||||
#define SPI_FLASH_CHUNK 4096u
|
||||
|
||||
/* Safety cap on status polling so a stuck WIP can't hang forever. */
|
||||
#define WIP_POLL_MAX 5000000
|
||||
|
||||
/* --- Chip database ------------------------------------------------- */
|
||||
|
||||
static const spi_flash_chip chip_db[] = {
|
||||
/* Micron MT25QU256 — KCU105 config flash, 1.8 V, 32 MB, 4-byte addr.
|
||||
* 4-byte command opcodes (READ4B/PP4B/SUBSEC-ERASE4B) so the full
|
||||
* range is reachable regardless of the address-mode latch. */
|
||||
{ "Micron MT25QU256", 0x20, 0xBB19, 32u * 1024 * 1024, 256, 4096, 4, 0x13, 0x12, 0x21 },
|
||||
{ "Micron MT25QL256", 0x20, 0xBA19, 32u * 1024 * 1024, 256, 4096, 4, 0x13, 0x12, 0x21 },
|
||||
/* 16 MB, 3-byte parts. */
|
||||
{ "Winbond W25Q128", 0xEF, 0x4018, 16u * 1024 * 1024, 256, 4096, 3, 0x03, 0x02, 0x20 },
|
||||
{ "Macronix MX25L128",0xC2, 0x2018, 16u * 1024 * 1024, 256, 4096, 3, 0x03, 0x02, 0x20 },
|
||||
{ "ISSI IS25LP128", 0x9D, 0x6018, 16u * 1024 * 1024, 256, 4096, 3, 0x03, 0x02, 0x20 },
|
||||
};
|
||||
|
||||
#define CHIP_DB_LEN ((int)(sizeof(chip_db) / sizeof(chip_db[0])))
|
||||
|
||||
int spi_flash_chip_count(void)
|
||||
{
|
||||
return CHIP_DB_LEN;
|
||||
}
|
||||
|
||||
const spi_flash_chip *spi_flash_chip_by_index(int i)
|
||||
{
|
||||
if (i < 0 || i >= CHIP_DB_LEN) return NULL;
|
||||
return &chip_db[i];
|
||||
}
|
||||
|
||||
/* --- Helpers ------------------------------------------------------- */
|
||||
|
||||
static int put_addr(uint8_t *b, uint32_t addr, int addr_bytes)
|
||||
{
|
||||
if (addr_bytes == 4) {
|
||||
b[0] = (uint8_t)(addr >> 24);
|
||||
b[1] = (uint8_t)(addr >> 16);
|
||||
b[2] = (uint8_t)(addr >> 8);
|
||||
b[3] = (uint8_t)(addr);
|
||||
return 4;
|
||||
}
|
||||
b[0] = (uint8_t)(addr >> 16);
|
||||
b[1] = (uint8_t)(addr >> 8);
|
||||
b[2] = (uint8_t)(addr);
|
||||
return 3;
|
||||
}
|
||||
|
||||
static int write_enable(spi_flash *sf)
|
||||
{
|
||||
uint8_t cmd = CMD_WREN;
|
||||
return sf->xfer(sf->ctx, &cmd, 1, NULL, 0);
|
||||
}
|
||||
|
||||
/* Poll the status register until write-in-progress clears. */
|
||||
static int wait_wip(spi_flash *sf)
|
||||
{
|
||||
long tries;
|
||||
for (tries = 0; tries < WIP_POLL_MAX; tries++) {
|
||||
uint8_t cmd = CMD_RDSR, st = 0;
|
||||
if (sf->xfer(sf->ctx, &cmd, 1, &st, 1) < 0) return -1;
|
||||
if (!(st & SR_WIP)) return 0;
|
||||
}
|
||||
return -1; /* timeout */
|
||||
}
|
||||
|
||||
/* --- Operations ---------------------------------------------------- */
|
||||
|
||||
int spi_flash_detect(spi_flash *sf)
|
||||
{
|
||||
uint8_t cmd = CMD_RDID;
|
||||
uint8_t id[3] = {0, 0, 0};
|
||||
uint16_t dev;
|
||||
int i;
|
||||
|
||||
if (!sf || !sf->xfer) return -1;
|
||||
if (sf->xfer(sf->ctx, &cmd, 1, id, 3) < 0) return -1;
|
||||
|
||||
memcpy(sf->jedec, id, 3);
|
||||
sf->chip = NULL;
|
||||
|
||||
dev = (uint16_t)((id[1] << 8) | id[2]);
|
||||
for (i = 0; i < CHIP_DB_LEN; i++) {
|
||||
if (chip_db[i].mfg_id == id[0] && chip_db[i].dev_id == dev) {
|
||||
sf->chip = &chip_db[i];
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1; /* read OK but unknown part */
|
||||
}
|
||||
|
||||
int spi_flash_read(spi_flash *sf, uint32_t addr, uint8_t *buf, size_t len)
|
||||
{
|
||||
uint8_t cmd[1 + 4];
|
||||
int n;
|
||||
|
||||
if (!sf || !sf->chip || !buf) return -1;
|
||||
|
||||
while (len) {
|
||||
size_t chunk = (len > SPI_FLASH_CHUNK) ? SPI_FLASH_CHUNK : len;
|
||||
cmd[0] = sf->chip->read_cmd;
|
||||
n = 1 + put_addr(&cmd[1], addr, sf->chip->addr_bytes);
|
||||
if (sf->xfer(sf->ctx, cmd, (size_t)n, buf, chunk) < 0) return -1;
|
||||
addr += (uint32_t)chunk;
|
||||
buf += chunk;
|
||||
len -= chunk;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int spi_flash_erase_sector(spi_flash *sf, uint32_t addr)
|
||||
{
|
||||
uint8_t cmd[1 + 4];
|
||||
int n;
|
||||
|
||||
if (!sf || !sf->chip) return -1;
|
||||
|
||||
addr -= (addr % sf->chip->sector_size); /* align to sector base */
|
||||
|
||||
if (write_enable(sf) < 0) return -1;
|
||||
cmd[0] = sf->chip->erase_cmd;
|
||||
n = 1 + put_addr(&cmd[1], addr, sf->chip->addr_bytes);
|
||||
if (sf->xfer(sf->ctx, cmd, (size_t)n, NULL, 0) < 0) return -1;
|
||||
return wait_wip(sf);
|
||||
}
|
||||
|
||||
int spi_flash_program_page(spi_flash *sf, uint32_t addr,
|
||||
const uint8_t *data, size_t len)
|
||||
{
|
||||
uint8_t cmd[1 + 4 + 256];
|
||||
int n;
|
||||
|
||||
if (!sf || !sf->chip || !data) return -1;
|
||||
if (len == 0) return 0;
|
||||
if (len > sf->chip->page_size || len > 256) return -1;
|
||||
/* must not cross a page boundary */
|
||||
if ((addr % sf->chip->page_size) + len > sf->chip->page_size) return -1;
|
||||
|
||||
if (write_enable(sf) < 0) return -1;
|
||||
cmd[0] = sf->chip->pp_cmd;
|
||||
n = 1 + put_addr(&cmd[1], addr, sf->chip->addr_bytes);
|
||||
memcpy(&cmd[n], data, len);
|
||||
n += (int)len;
|
||||
if (sf->xfer(sf->ctx, cmd, (size_t)n, NULL, 0) < 0) return -1;
|
||||
return wait_wip(sf);
|
||||
}
|
||||
|
||||
int spi_flash_program(spi_flash *sf, uint32_t addr,
|
||||
const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!sf || !sf->chip || !data) return -1;
|
||||
|
||||
while (len) {
|
||||
uint32_t page = sf->chip->page_size;
|
||||
uint32_t off = addr % page;
|
||||
size_t chunk = page - off;
|
||||
if (chunk > len) chunk = len;
|
||||
if (spi_flash_program_page(sf, addr, data, chunk) < 0) return -1;
|
||||
addr += (uint32_t)chunk;
|
||||
data += chunk;
|
||||
len -= chunk;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int spi_flash_verify(spi_flash *sf, uint32_t addr,
|
||||
const uint8_t *data, size_t len, uint32_t *mismatch_off)
|
||||
{
|
||||
uint8_t tmp[SPI_FLASH_CHUNK];
|
||||
uint32_t done = 0;
|
||||
|
||||
if (!sf || !sf->chip || !data) return -1;
|
||||
|
||||
while (len) {
|
||||
size_t chunk = (len > sizeof(tmp)) ? sizeof(tmp) : len;
|
||||
size_t i;
|
||||
if (spi_flash_read(sf, addr, tmp, chunk) < 0) return -1;
|
||||
for (i = 0; i < chunk; i++) {
|
||||
if (tmp[i] != data[done + i]) {
|
||||
if (mismatch_off) *mismatch_off = done + (uint32_t)i;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
addr += (uint32_t)chunk;
|
||||
done += (uint32_t)chunk;
|
||||
len -= chunk;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
79
modules/spi_flash/spi_flash.h
Normal file
79
modules/spi_flash/spi_flash.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#ifndef _SPI_FLASH_H
|
||||
#define _SPI_FLASH_H
|
||||
|
||||
/*
|
||||
* Generic SPI NOR flash layer (Phase 3).
|
||||
*
|
||||
* Transport-agnostic: it issues SPI commands through an `xfer` callback
|
||||
* (one CS-framed transaction), so it works over the BSCAN proxy, the
|
||||
* EXTEST bit-bang, or anything else that can clock bytes in and out.
|
||||
*
|
||||
* Standard JEDEC command set (RDID/READ/WREN/RDSR/PP/SE). Chips are
|
||||
* matched by JEDEC ID against a small built-in database; 3- and 4-byte
|
||||
* addressing are both supported (parts over 16 MB use the 4-byte
|
||||
* command opcodes, which are stateless regardless of the part's
|
||||
* address-mode latch).
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* One CS-framed SPI transaction: clock out `txlen` MOSI bytes, then
|
||||
* read `rxlen` MISO bytes into `rx`. Either length may be 0. Returns 0
|
||||
* on success, <0 on error. */
|
||||
typedef int (*spi_flash_xfer_fn)(void *ctx,
|
||||
const uint8_t *tx, size_t txlen,
|
||||
uint8_t *rx, size_t rxlen);
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint8_t mfg_id; /* JEDEC byte 0 (manufacturer) */
|
||||
uint16_t dev_id; /* JEDEC bytes 1..2 ((b1 << 8) | b2) */
|
||||
uint32_t size_bytes; /* total capacity */
|
||||
uint32_t page_size; /* program granularity (typically 256) */
|
||||
uint32_t sector_size; /* erase granularity (typically 4096) */
|
||||
uint8_t addr_bytes; /* address width: 3 or 4 */
|
||||
uint8_t read_cmd; /* READ opcode (no dummy) */
|
||||
uint8_t pp_cmd; /* PAGE PROGRAM opcode */
|
||||
uint8_t erase_cmd; /* sector/subsector ERASE opcode */
|
||||
} spi_flash_chip;
|
||||
|
||||
typedef struct {
|
||||
spi_flash_xfer_fn xfer; /* transport */
|
||||
void *ctx; /* opaque, passed to xfer */
|
||||
const spi_flash_chip *chip; /* matched part, set by detect */
|
||||
uint8_t jedec[3]; /* raw JEDEC ID from last detect */
|
||||
} spi_flash;
|
||||
|
||||
/* Read the JEDEC ID (0x9F) and match the database.
|
||||
* Returns 0 if a known chip matched (sf->chip set), 1 if the ID was
|
||||
* read but is not in the database (sf->chip left NULL, sf->jedec set),
|
||||
* <0 on transport error. */
|
||||
int spi_flash_detect(spi_flash *sf);
|
||||
|
||||
/* Read `len` bytes starting at `addr` into `buf`. Returns 0 / <0. */
|
||||
int spi_flash_read(spi_flash *sf, uint32_t addr, uint8_t *buf, size_t len);
|
||||
|
||||
/* Erase the sector containing `addr` (WREN + ERASE + poll WIP). */
|
||||
int spi_flash_erase_sector(spi_flash *sf, uint32_t addr);
|
||||
|
||||
/* Program at most one page at `addr` (`len` <= page_size and must not
|
||||
* cross a page boundary): WREN + PP + poll WIP. */
|
||||
int spi_flash_program_page(spi_flash *sf, uint32_t addr,
|
||||
const uint8_t *data, size_t len);
|
||||
|
||||
/* Program `len` bytes from `addr`, splitting on page boundaries. The
|
||||
* target range must already be erased. */
|
||||
int spi_flash_program(spi_flash *sf, uint32_t addr,
|
||||
const uint8_t *data, size_t len);
|
||||
|
||||
/* Read back and compare. Returns 0 if identical, 1 on first mismatch
|
||||
* (and reports its offset via *mismatch_off if non-NULL), <0 on error. */
|
||||
int spi_flash_verify(spi_flash *sf, uint32_t addr,
|
||||
const uint8_t *data, size_t len, uint32_t *mismatch_off);
|
||||
|
||||
/* Built-in database access. */
|
||||
int spi_flash_chip_count(void);
|
||||
const spi_flash_chip *spi_flash_chip_by_index(int i);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user