diff --git a/CLAUDE.md b/CLAUDE.md index a45ec70..eb97903 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,10 +118,12 @@ the loader blobs are vendor rkbin files the user supplies, not bundled. Bare-metal ARM32 C loaded onto the camera after SPL boot. COBS-framed binary protocol with CRC-32, 1024-byte max payload. It comes up at **115200** (the rate the boot ROM left the UART at) and is switched to 921600 by `CMD_SET_BAUD` after -the handshake, reverting to 115200 after ~30 s idle. 13 commands: `INFO 0x01`, +the handshake, reverting to 115200 after ~30 s idle. 14 commands: `INFO 0x01`, `READ 0x02`, `WRITE 0x03`, `ERASE 0x04`, `CRC32 0x05`, `REBOOT 0x06`, `SELFUPDATE 0x07`, `SET_BAUD 0x08`, `SCAN 0x09`, `FLASH_PROGRAM 0x0A`, -`FLASH_STREAM 0x0B`, `MARK_BAD 0x0C`, `MEMBW 0x0D`. `agent/protocol.h` and +`FLASH_STREAM 0x0B`, `MARK_BAD 0x0C`, `MEMBW 0x0D`, `NAND 0x0E` (SPI NAND study +ops on FMC100 builds: raw/ECC page I/O, OOB, feature registers; pages move +through agent RAM buffers, see `agent/nand_layout.h` and `defib.agent.nand`). `agent/protocol.h` and `src/defib/agent/protocol.py` must stay in lockstep; the client negotiates optional features through a capability bitmask (`client.py`). @@ -144,7 +146,7 @@ Backends: `spi_flash.c` (fmc100), `spi_flash_hisfc350.c` (V1-era parts), orchestrated by `src/defib/install/`. Commands: `burn`, `install`, `restore`, `dump-flash`, `detect`, `capture`, `replay`, `network`, `ports`, `list-chips`, `list-interfaces`, `tui`, plus the `agent` sub-app (`upload`, `flash`, `info`, - `read`, `write`, `scan`, `membw`). + `read`, `write`, `scan`, `membw`, and `nand` — SPI NAND study ops in `cli/nand.py`). - **Vendor U-Boot bootstrap** (`src/defib/vendors/`) — migration from an already-running stock/vendor U-Boot into an OpenIPC U-Boot shell. This is a later recovery stage than `BootProtocol`: targets use exact `soc:variant` @@ -197,8 +199,8 @@ speculatively, to see an error message, or to "check whether the board responds" | Safety | Commands | |---|---| | Host-only, never touches a device | `list-chips`, `ports`, `list-interfaces`, `replay` | -| Talks to the device; RAM-only or read-only | `detect`, `capture`, `dump-flash`, `burn`, `agent upload\|info\|read\|scan\|membw`, `network` (serves a file; the *device* decides to write) | -| **Irreversibly erases and writes flash** | `install`, `restore`, `agent flash`, `agent write`, TUI Flash Doctor write paths | +| Talks to the device; RAM-only or read-only | `detect`, `capture`, `dump-flash`, `burn`, `agent upload\|info\|read\|scan\|membw`, `agent nand info\|read-page\|ecc-scan\|dump\|feature\|fmc-reg` (the last two can change volatile chip/controller state, never the array), `network` (serves a file; the *device* decides to write) | +| **Irreversibly erases and writes flash** | `install`, `restore`, `agent flash`, `agent write`, `agent nand write-page\|erase-block`, TUI Flash Doctor write paths | `burn` only uploads into RAM, but it still power-cycles the board and can leave it parked in download mode — recoverable with another power cycle. diff --git a/agent/Makefile b/agent/Makefile index b6f9132..5e9a915 100644 --- a/agent/Makefile +++ b/agent/Makefile @@ -245,6 +245,8 @@ LIBGCC := $(shell $(CC) -mcpu=$(CPU_TYPE) -print-libgcc-file-name) SPI_DRIVER ?= fmc100 ifeq ($(SPI_DRIVER),fmc100) SPI_FLASH_SRC = spi_flash.c + # CMD_NAND (SPI NAND study ops) drives the FMC100 page engine. + CFLAGS += -DHAVE_NAND_STUDY else SPI_FLASH_SRC = spi_flash_$(SPI_DRIVER).c endif diff --git a/agent/main.c b/agent/main.c index 5db35fe..a683c90 100644 --- a/agent/main.c +++ b/agent/main.c @@ -8,6 +8,7 @@ #include "emmc_himci.h" #include "protocol.h" #include "spi_flash.h" +#include "nand_layout.h" static flash_info_t flash_info; @@ -145,8 +146,9 @@ static int addr_readable(uint32_t addr, uint32_t size) { * SoCs where it isn't 0x14000000 — e.g. hi3520dv200 has it at * 0x58000000). * v4 added: CMD_MEMBW for bare-metal DDR bandwidth measurement - * (ARMv7 only; ACK_FLASH_ERROR on ARMv5). */ -#define AGENT_VERSION 4 + * (ARMv7 only; ACK_FLASH_ERROR on ARMv5). + * v5 added: CMD_NAND, SPI NAND study ops on FMC100 (CAP_NAND). */ +#define AGENT_VERSION 5 /* Capability flags — advertise supported features */ #define CAP_FLASH_STREAM (1 << 0) /* CMD_FLASH_STREAM with double-buffer */ @@ -162,9 +164,15 @@ static int addr_readable(uint32_t addr, uint32_t size) { #define CAP_MEMBW 0 #endif +#ifdef HAVE_NAND_STUDY +#define CAP_NAND (1 << 8) /* CMD_NAND */ +#else +#define CAP_NAND 0 +#endif + #define AGENT_CAPS (CAP_FLASH_STREAM | CAP_SECTOR_BITMAP | CAP_PAGE_SKIP | \ CAP_SET_BAUD | CAP_REBOOT | CAP_SELFUPDATE | CAP_SCAN | \ - CAP_MEMBW) + CAP_MEMBW | CAP_NAND) static void handle_info(void) { uint8_t resp[28]; @@ -182,6 +190,23 @@ static void handle_info(void) { proto_send(RSP_INFO, resp, 28); } +/* I/O registers require 32-bit word-aligned access (ldr not ldrb). + * RAM and flash can use byte access. Covers the V3+/V4+/V5/V6 peripheral + * block (0x10000000..0x13000000) as well as the V1-era regions actually + * used by V1 SoCs (FMC, CRG, UART). */ +static int is_io_region(uint32_t addr) { + return (addr >= 0x10000000 && addr < 0x13000000) + || (addr >= FMC_BASE && addr < FMC_BASE + 0x1000) + || (addr >= CRG_BASE && addr < CRG_BASE + 0x1000) + || (addr >= UART_BASE && addr < UART_BASE + 0x1000); +} + +/* Byte `addr` of an I/O region, fetched with a word load as CMD_READ does. */ +static uint8_t io_byte(uint32_t addr) { + uint32_t val = *(volatile uint32_t *)(addr & ~3u); + return (val >> ((addr & 3) * 8)) & 0xFF; +} + static void handle_read(const uint8_t *data, uint32_t len) { if (len < 8) { proto_send_ack(ACK_CRC_ERROR); return; } @@ -198,10 +223,7 @@ static void handle_read(const uint8_t *data, uint32_t len) { * RAM and flash can use byte access. Cover the V3+/V4+/V5/V6 * peripheral block (0x10000000..0x13000000) as well as the V1-era * regions actually used by V1 SoCs (FMC, CRG, UART). */ - int io_region = (addr >= 0x10000000 && addr < 0x13000000) - || (addr >= FMC_BASE && addr < FMC_BASE + 0x1000) - || (addr >= CRG_BASE && addr < CRG_BASE + 0x1000) - || (addr >= UART_BASE && addr < UART_BASE + 0x1000); + int io_region = is_io_region(addr); uint16_t seq = 0; uint32_t offset = 0; @@ -292,6 +314,14 @@ static void handle_crc32_cmd(const uint8_t *data, uint32_t len) { if (flash_readable && addr >= FLASH_MEM && (addr + size) <= (FLASH_MEM + flash_info.size)) { c = flash_crc32(addr - FLASH_MEM, size); + } else if (is_io_region(addr)) { + /* Registers: the same word loads and byte order CMD_READ uses, or + * the CRC covers different bytes than a read returns. */ + c = 0; + for (uint32_t off = 0; off < size; off++) { + uint8_t b = io_byte(addr + off); + c = crc32(c, &b, 1); + } } else { const uint8_t *ptr = (const uint8_t *)addr; c = crc32(0, ptr, size); @@ -679,9 +709,10 @@ static void handle_flash_write(const uint8_t *data, uint32_t len) { offset += chunk; } - /* Verify written data by reading back from flash and comparing CRC */ - const uint8_t *flash_ptr = (const uint8_t *)(FLASH_MEM + flash_addr); - uint32_t verify_crc = crc32(0, flash_ptr, size); + /* Verify written data by reading it back through the controller. + * Not through the FLASH_MEM window: NAND has none, and on NOR it wraps + * at 1 MiB on some SoCs. */ + uint32_t verify_crc = flash_crc32(flash_addr, size); if (verify_crc != expected_crc) { uint8_t err[9]; err[0] = ACK_CRC_ERROR; @@ -1172,6 +1203,181 @@ static void handle_mark_bad(const uint8_t *data, uint32_t len) { proto_send_ack(rc == 0 ? ACK_OK : ACK_FLASH_ERROR); } +#ifdef HAVE_NAND_STUDY +/* + * CMD_NAND: SPI NAND study ops — page I/O the way the Linux FMC100 drivers + * do it, or deliberately not, for chasing ECC faults below the OS. + * Host sends: CMD_NAND [subop:1] [args...] + * Agent answers: RSP_NAND [subop:1] [status:1] [payload...] + * + * Bulk data does not travel in RSP_NAND. Pages live in NAND_DMA_BUF and + * per-page results in NAND_STAT_BUF; the host moves them with CMD_READ / + * CMD_WRITE at the addresses NAND_OP_INFO reports. + * + * Transfer spec (8 bytes, "xfer" below): + * [fmc_cfg:4LE] [mode:1] [opcode:1] [iftype:1] [dummy:1] see nand_xfer_t + * Per-page record (8 bytes, "rec"): + * [ecc_err:4LE] [ondie:1] [fmc_int:1] [flags:1] [0] see nand_rec_t + * + * INFO -> [page:2][oob:2][pages_per_block:2][blocks:2] + * [dma_buf:4][dma_size:4][stat_buf:4][stat_size:4] + * [fmc_cfg:4] + * FEATURE_GET [addr:1] -> [value:1] + * FEATURE_SET [addr:1][value:1] -> [read back:1] + * READ_PAGES [start:4][count:4][xfer:8][store:1] -> [done:4] + * rec i at NAND_STAT_BUF + 8*i; with store=1, page i at + * NAND_DMA_BUF + i*(page+oob), else data is discarded + * (an ECC scan). Stops at the first timeout. + * PROGRAM_PAGE [page:4][xfer:8][src_off:4] -> rec + * page + OOB from NAND_DMA_BUF + src_off, OOB as given. + * ERASE_BLOCK [page:4] -> rec + * FMC_REG [write:1][off:2][value:4] -> [value:4] + * any 32-bit FMC register, offset < 0x1000, read after + * the optional write. + */ +static void nand_reply(uint8_t subop, uint8_t status, + const uint8_t *payload, uint32_t n) { + uint8_t buf[40]; + buf[0] = subop; + buf[1] = status; + for (uint32_t i = 0; i < n && i < sizeof(buf) - 2; i++) + buf[2 + i] = payload[i]; + proto_send(RSP_NAND, buf, 2 + n); +} + +static void nand_parse_xfer(const uint8_t *p, nand_xfer_t *x) { + x->fmc_cfg = read_le32(&p[0]); + x->mode = p[4]; + x->opcode = p[5]; + x->iftype = p[6]; + x->dummy = p[7]; +} + +static void nand_put_rec(uint8_t *p, const nand_rec_t *r) { + write_le32(&p[0], r->ecc_err); + p[4] = r->ondie; + p[5] = r->fmc_int; + p[6] = r->flags; + p[7] = r->rsvd; +} + +static void nand_study_op(uint8_t op, const uint8_t *arg, uint32_t alen, + uint32_t stride, uint32_t pages); + +static void handle_nand(const uint8_t *data, uint32_t len) { + if (len < 1) { proto_send_ack(ACK_CRC_ERROR); return; } + uint8_t op = data[0]; + const uint8_t *arg = &data[1]; + uint32_t alen = len - 1; + + nand_geom_t g; + nand_get_geometry(&g); + uint32_t stride = (uint32_t)g.page_size + g.oob_size; + uint32_t pages = (uint32_t)g.pages_per_block * g.blocks; + uint8_t out[36]; + + if (op == NAND_OP_FMC_REG) { + if (alen < 7) { nand_reply(op, NAND_ST_BADARG, 0, 0); return; } + uint32_t off = arg[1] | ((uint32_t)arg[2] << 8); + if (off >= 0x1000 || (off & 3)) { nand_reply(op, NAND_ST_BADARG, 0, 0); return; } + if (arg[0]) fmc_reg(off) = read_le32(&arg[3]); + write_le32(out, fmc_reg(off)); + nand_reply(op, NAND_ST_OK, out, 4); + return; + } + if (op == NAND_OP_INFO) { + out[0] = g.page_size & 0xff; out[1] = g.page_size >> 8; + out[2] = g.oob_size & 0xff; out[3] = g.oob_size >> 8; + out[4] = g.pages_per_block & 0xff; out[5] = g.pages_per_block >> 8; + out[6] = g.blocks & 0xff; out[7] = g.blocks >> 8; + write_le32(&out[8], NAND_DMA_BUF); + write_le32(&out[12], NAND_DMA_BUF_SIZE); + write_le32(&out[16], NAND_STAT_BUF); + write_le32(&out[20], NAND_STAT_BUF_SIZE); + write_le32(&out[24], fmc_reg(0x00)); + nand_reply(op, g.page_size ? NAND_ST_OK : NAND_ST_NOT_NAND, out, 28); + return; + } + if (!g.page_size) { nand_reply(op, NAND_ST_NOT_NAND, 0, 0); return; } + + /* Page ops take FMC_CFG from the request; put it back afterwards so a + * DMA scan does not change how later CMD_READ, CRC32 or raw reads + * behave. */ + uint32_t saved_cfg = fmc_reg(0x00); + nand_study_op(op, arg, alen, stride, pages); + fmc_reg(0x00) = saved_cfg; +} + +static void nand_study_op(uint8_t op, const uint8_t *arg, uint32_t alen, + uint32_t stride, uint32_t pages) { + uint8_t out[36]; + nand_rec_t rec; + nand_xfer_t x; + + switch (op) { + case NAND_OP_FEATURE_GET: + if (alen < 1) break; + out[0] = nand_feature_get(arg[0]); + nand_reply(op, NAND_ST_OK, out, 1); + return; + case NAND_OP_FEATURE_SET: + if (alen < 2) break; + nand_feature_set(arg[0], arg[1]); + out[0] = nand_feature_get(arg[0]); + nand_reply(op, NAND_ST_OK, out, 1); + return; + case NAND_OP_READ_PAGES: { + if (alen < 17) break; + uint32_t start = read_le32(&arg[0]); + uint32_t count = read_le32(&arg[4]); + uint8_t store = arg[16]; + nand_parse_xfer(&arg[8], &x); + if (count == 0 || start >= pages || count > pages - start + || count > NAND_STAT_BUF_SIZE / 8 + || (store && count > NAND_DMA_BUF_SIZE / stride)) + break; + uint8_t *recs = (uint8_t *)NAND_STAT_BUF; + uint32_t done = 0; + while (done < count) { + uint8_t *dst = (uint8_t *)(NAND_DMA_BUF + (store ? done * stride : 0)); + int rc = nand_page_read(start + done, &x, dst, &rec); + nand_put_rec(&recs[done * 8], &rec); + done++; + proto_drain_fifo(); + if (rc) break; + } + write_le32(out, done); + nand_reply(op, done == count ? NAND_ST_OK : NAND_ST_IO, out, 4); + return; + } + case NAND_OP_PROGRAM_PAGE: { + if (alen < 16) break; + uint32_t page = read_le32(&arg[0]); + uint32_t src_off = read_le32(&arg[12]); + nand_parse_xfer(&arg[4], &x); + if (page >= pages || src_off > NAND_DMA_BUF_SIZE - stride) break; + int rc = nand_page_program(page, &x, + (const uint8_t *)(NAND_DMA_BUF + src_off), &rec); + nand_put_rec(out, &rec); + nand_reply(op, rc ? NAND_ST_IO : NAND_ST_OK, out, 8); + return; + } + case NAND_OP_ERASE_BLOCK: { + if (alen < 4) break; + uint32_t page = read_le32(&arg[0]); + if (page >= pages) break; + int rc = nand_block_erase(page, &rec); + nand_put_rec(out, &rec); + nand_reply(op, rc ? NAND_ST_IO : NAND_ST_OK, out, 8); + return; + } + default: + break; + } + nand_reply(op, NAND_ST_BADARG, 0, 0); +} +#endif /* HAVE_NAND_STUDY */ + static void handle_scan(const uint8_t *data __attribute__((unused)), uint32_t len __attribute__((unused))) { if (!flash_readable) { @@ -1423,6 +1629,11 @@ int main(void) { case CMD_MEMBW: handle_membw(cmd_buf, data_len); break; +#ifdef HAVE_NAND_STUDY + case CMD_NAND: + handle_nand(cmd_buf, data_len); + break; +#endif case CMD_SET_BAUD: handle_set_baud(cmd_buf, data_len); break; diff --git a/agent/nand_layout.h b/agent/nand_layout.h new file mode 100644 index 0000000..1d31904 --- /dev/null +++ b/agent/nand_layout.h @@ -0,0 +1,28 @@ +/* + * RAM layout of the SPI NAND study buffers (CMD_NAND). Plain #defines so + * startup.S can include it: the DMA buffer's 1 MiB section is mapped + * uncached there, which keeps the FMC's DMA and the CPU coherent without + * any cache maintenance. + * + * NAND_DMA_BUF page data + OOB, written/read by FMC DMA; consecutive + * pages are packed at (page_size + oob_size) stride + * NAND_STAT_BUF one nand_rec_t per page of the last CMD_NAND read + * + * The DMA buffer ends 1 MiB below RAM_BASE + 16 MiB, the lowest agent + * LOAD_ADDR, so the 16 KiB stack growing down from LOAD_ADDR never reaches + * it. The status buffer is past the agent and its bss; only a CMD_MEMBW + * run with a large scratch size can overlap it. + */ + +#ifndef NAND_LAYOUT_H +#define NAND_LAYOUT_H + +#define NAND_DMA_BUF_OFF 0x00E00000 /* must be 1 MiB aligned */ +#define NAND_DMA_BUF_SIZE 0x00100000 +#define NAND_STAT_BUF_OFF 0x02000000 +#define NAND_STAT_BUF_SIZE 0x00080000 /* 64 Ki pages x 8 B */ + +#define NAND_DMA_BUF (RAM_BASE + NAND_DMA_BUF_OFF) +#define NAND_STAT_BUF (RAM_BASE + NAND_STAT_BUF_OFF) + +#endif /* NAND_LAYOUT_H */ diff --git a/agent/protocol.h b/agent/protocol.h index c90fed6..696ab70 100644 --- a/agent/protocol.h +++ b/agent/protocol.h @@ -21,6 +21,7 @@ #define CMD_FLASH_STREAM 0x0B #define CMD_MARK_BAD 0x0C /* NAND only: write 0x00 to OOB[0] of page 0 of a block */ #define CMD_MEMBW 0x0D /* DDR bandwidth test (ARMv7 only): see handle_membw */ +#define CMD_NAND 0x0E /* SPI NAND study ops (FMC100 only): see handle_nand */ /* Responses (device → host) */ #define RSP_INFO 0x81 @@ -30,6 +31,22 @@ #define RSP_READY 0x85 #define RSP_SCAN 0x86 #define RSP_MEMBW 0x87 +#define RSP_NAND 0x88 + +/* CMD_NAND sub-operations (first payload byte) */ +#define NAND_OP_INFO 0x00 +#define NAND_OP_FEATURE_GET 0x01 +#define NAND_OP_FEATURE_SET 0x02 +#define NAND_OP_READ_PAGES 0x03 +#define NAND_OP_PROGRAM_PAGE 0x04 +#define NAND_OP_ERASE_BLOCK 0x05 +#define NAND_OP_FMC_REG 0x06 + +/* RSP_NAND status (second payload byte) */ +#define NAND_ST_OK 0x00 +#define NAND_ST_BADARG 0x01 /* malformed or out-of-range request */ +#define NAND_ST_NOT_NAND 0x02 /* no SPI NAND identified */ +#define NAND_ST_IO 0x03 /* the op ran but the hardware reported failure */ /* ACK status codes */ #define ACK_OK 0x00 diff --git a/agent/spi_flash.c b/agent/spi_flash.c index 16d8b23..773b027 100644 --- a/agent/spi_flash.c +++ b/agent/spi_flash.c @@ -29,6 +29,7 @@ #define FMC_OP_CTRL 0x68 #define FMC_STATUS 0xAC #define FMC_VERSION 0xBC +#define FMC_ECC_ERR_NUM0_BUF0 0xC0 /* FMC_CFG bits */ #define FMC_CFG_OP_MODE_NORMAL (1 << 0) @@ -148,7 +149,7 @@ static uint32_t detect_size(uint8_t id2) { } /* Forward declarations */ -static void fmc_wait_ready(void); +static int fmc_wait_ready(void); static void spi_wait_wip(void); /* Mode switching: normal mode for register commands, boot mode for reads */ @@ -214,10 +215,13 @@ static void fmc_enter_boot(void) { fmc_reg(FMC_INT_CLR) = 0xFF; } -static void fmc_wait_ready(void) { +/* Returns 0 when the register op finished, -1 if it was still running + * when the poll gave up. */ +static int fmc_wait_ready(void) { volatile uint32_t timeout = 400000; while ((fmc_reg(FMC_OP) & FMC_OP_REG_OP_START) && timeout > 0) timeout--; + return timeout ? 0 : -1; } static void spi_wait_wip(void) { @@ -431,21 +435,28 @@ static void nand_write_enable(void) { * An unrecognised NAND falls through to the NOR path, which is not just * wrong but can hang: flash_global_unlock() polls a NOR status register a * NAND does not implement (GD5F1GM7 never clears "WIP"). */ -static const uint8_t nand_ids[][2] = { - { 0xC2, 0x12 }, /* Macronix MX35LF1GE4AB */ - { 0xEF, 0xAA }, /* Winbond W25N01GV (EF AA 21) */ - { 0xC8, 0x91 }, /* GigaDevice GD5F1GM7UE, 3.3 V */ - { 0xC8, 0x81 }, /* GigaDevice GD5F1GM7RE, 1.8 V */ +static const struct { + uint8_t id[2]; + uint16_t oob_size; +} nand_ids[] = { + { { 0xC2, 0x12 }, 64 }, /* Macronix MX35LF1GE4AB */ + { { 0xEF, 0xAA }, 64 }, /* Winbond W25N01GV (EF AA 21) */ + { { 0xC8, 0x91 }, 128 }, /* GigaDevice GD5F1GM7UE, 3.3 V */ + { { 0xC8, 0x81 }, 128 }, /* GigaDevice GD5F1GM7RE, 1.8 V */ }; -/* Returns 1 if id[] is a known SPI NAND, read either directly or shifted - * by one dummy byte (id[0] = dummy). */ +/* Geometry of the identified SPI NAND; page_size 0 until one is found. */ +static nand_geom_t nand_geom; + +/* Returns the nand_ids[] index if id[] is a known SPI NAND, read either + * directly or shifted by one dummy byte (id[0] = dummy), else -1. */ static int nand_identify(const uint8_t id[3]) { for (unsigned i = 0; i < sizeof(nand_ids) / sizeof(nand_ids[0]); i++) { - if (id[0] == nand_ids[i][0] && id[1] == nand_ids[i][1]) return 1; - if (id[1] == nand_ids[i][0] && id[2] == nand_ids[i][1]) return 1; + const uint8_t *want = nand_ids[i].id; + if (id[0] == want[0] && id[1] == want[1]) return (int)i; + if (id[1] == want[0] && id[2] == want[1]) return (int)i; } - return 0; + return -1; } int flash_init(flash_info_t *info) { @@ -468,7 +479,8 @@ int flash_init(flash_info_t *info) { fmc_enter_normal(); flash_read_id(info->jedec_id); - if (nand_identify(info->jedec_id)) { + int nand_idx = nand_identify(info->jedec_id); + if (nand_idx >= 0) { /* SPI NAND path. No flash_unlock / fmc_enter_boot — NAND has no * memory-mapped boot mode and uses different protection (BP bits * via SET_FEATURE 0xA0 instead of write-status-register). */ @@ -477,6 +489,10 @@ int flash_init(flash_info_t *info) { info->sector_size = NAND_BLOCK_SIZE; /* 128 KiB erase block */ info->page_size = NAND_PAGE_SIZE; /* 2 KiB read/program page */ current_flash_type = FLASH_TYPE_NAND; + nand_geom.page_size = NAND_PAGE_SIZE; + nand_geom.oob_size = nand_ids[nand_idx].oob_size; + nand_geom.pages_per_block = NAND_BLOCK_SIZE / NAND_PAGE_SIZE; + nand_geom.blocks = info->size / NAND_BLOCK_SIZE; /* Clear block-protection bits (BP0..BP3 + BRWD) in feature 0xA0 so * subsequent erase/program commands aren't rejected. Most SPI @@ -578,6 +594,8 @@ static int nand_program_page(uint32_t row, uint32_t column, } /* Read up to NAND_PAGE_SIZE bytes from a NAND page (data area only). + * Returns the chip status after PAGE_READ, or 0xFF if the chip or an FMC + * register op never finished. * row = page index (0 .. flash_size/page_size - 1) * column = byte offset within the 2 KiB data area (0 .. NAND_PAGE_SIZE-1) * On-chip ECC is left at its power-on default (enabled on MX35LF*) so the @@ -588,18 +606,21 @@ static int nand_program_page(uint32_t row, uint32_t column, * byte 0 of its I/O buffer (always 0x00 since the chip drives the dummy * line low), so real chip data starts at iobuf[1]. We compensate by * requesting `chunk + 1` bytes per fetch and copying iobuf[1..chunk]. */ -static void nand_read(uint32_t row, uint32_t column, - uint8_t *buf, uint32_t len) { +static uint8_t nand_read(uint32_t row, uint32_t column, + uint8_t *buf, uint32_t len) { + int stalled = 0; + /* 1) PAGE_READ: load page from array into chip cache. */ fmc_reg(FMC_INT_CLR) = 0xFF; fmc_reg(FMC_CMD) = SPI_CMD_NAND_PAGE_READ; fmc_reg(FMC_ADDRL) = row; fmc_reg(FMC_OP_CFG) = OP_CFG_OEN_EN | OP_CFG_CS(0) | OP_CFG_ADDR_NUM(3); fmc_reg(FMC_OP) = FMC_OP_CMD1_EN | FMC_OP_ADDR_EN | FMC_OP_REG_OP_START; - fmc_wait_ready(); + stalled |= fmc_wait_ready(); - /* 2) Wait for OIP=0 — chip finishes ECC correction and signals ready. */ - nand_wait_oip(); + /* 2) Wait for OIP=0 — chip finishes ECC correction and signals ready. + * The status carries the on-die ECC verdict (ECC_S, bits 5:4). */ + uint8_t status = nand_wait_oip(); /* 3) READ_FROM_CACHE: pull data from cache via column addressing. * FMC stores the post-address dummy byte as iobuf[0] (always 0x00), @@ -618,11 +639,14 @@ static void nand_read(uint32_t row, uint32_t column, | OP_CFG_ADDR_NUM(2) | OP_CFG_DUMMY_NUM(0); /* dummy is implicit in iobuf[0] */ fmc_reg(FMC_OP) = FMC_OP_CMD1_EN | FMC_OP_ADDR_EN | FMC_OP_READ_DATA | FMC_OP_REG_OP_START; - fmc_wait_ready(); + stalled |= fmc_wait_ready(); for (uint32_t i = 0; i < chunk; i++) buf[off + i] = iobuf[i + 1]; /* skip iobuf[0] = dummy */ off += chunk; } + /* A stalled register op means buf holds stale I/O-buffer bytes; report + * it the way nand_wait_oip() reports a chip that never finished. */ + return stalled ? 0xFF : status; } void flash_read(uint32_t addr, uint8_t *buf, uint32_t len) { @@ -834,6 +858,24 @@ int flash_read_oob(uint32_t block, uint8_t *buf, uint32_t len) { } uint32_t flash_crc32(uint32_t addr, uint32_t len) { + if (current_flash_type == FLASH_TYPE_NAND) { + /* Page by page through the same path CMD_READ uses; the NOR + * register read below would send NOR opcodes to a NAND. */ + uint8_t page[NAND_PAGE_SIZE]; + uint32_t c = 0; + while (len > 0) { + uint32_t col = addr % NAND_PAGE_SIZE; + uint32_t chunk = NAND_PAGE_SIZE - col; + if (chunk > len) chunk = len; + flash_read(addr, page, chunk); + c = crc32(c, page, chunk); + addr += chunk; + len -= chunk; + proto_drain_fifo(); + } + return c; + } + /* Use register-based reads to compute CRC32 over flash region. * Boot mode memory window wraps at 1MB on some SoCs. */ fmc_enter_normal(); @@ -862,3 +904,154 @@ uint32_t flash_crc32(uint32_t addr, uint32_t len) { fmc_enter_boot(); return c; } + +/* ---- SPI NAND study interface ----------------------------------------- */ + +/* FMC_OP_CFG / FMC_ADDR encodings for the controller's page engine, from + * the Linux hifmc100 driver (hifmc100.c, hifmc100.h, hisi_fmc.h). */ +#define FMC_INT_OP_DONE_BIT (1u << 0) +#define DMA_ADDR_BLOCK_SHIFT 22 /* REG_CNT_BLOCK_NUM_SHIFT */ +#define DMA_ADDR_BLOCK_MASK 0x3ffu +#define DMA_ADDR_BLOCK_H_SHIFT 10 /* REG_CNT_HIGH_BLOCK_NUM_SHIFT */ +#define DMA_ADDR_PAGE_SHIFT 16 /* REG_CNT_PAGE_NUM_SHIFT */ +#define DMA_ADDR_PAGE_MASK 0x3fu + +void nand_get_geometry(nand_geom_t *geom) { + /* Field by field: a struct copy becomes a memcpy() call, and the + * agent links without libc. */ + geom->page_size = nand_geom.page_size; + geom->oob_size = nand_geom.oob_size; + geom->pages_per_block = nand_geom.pages_per_block; + geom->blocks = nand_geom.blocks; +} + +uint8_t nand_feature_get(uint8_t addr) { + return nand_get_feature(addr); +} + +void nand_feature_set(uint8_t addr, uint8_t val) { + nand_set_feature(addr, val); +} + +/* REG mode means raw. With the SPI NAND interface selected, a non-zero + * FMC_CFG ECC type also applies to register-path reads: the controller + * "corrects" READ_FROM_CACHE data, and with a mismatched ECC type it flips + * bits that were right (measured on GD5F1GM7: 1-6 bits on ~29% of pages). + * The caller's FMC_CFG is restored by handle_nand after the op. */ +static void nand_reg_raw(void) { + fmc_reg(FMC_CFG) = fmc_reg(FMC_CFG) & ~(7u << 5); +} + +static int fmc_wait_dma_done(void) { + for (uint32_t i = 0; i < 4000000; i++) { + if (fmc_reg(FMC_INT) & FMC_INT_OP_DONE_BIT) return 0; + } + return -1; +} + +/* Block/page address as the page engine wants it (64-page blocks). */ +static void nand_dma_set_addr(uint32_t page) { + uint32_t block = page / nand_geom.pages_per_block; + uint32_t in_block = page % nand_geom.pages_per_block; + fmc_reg(FMC_ADDRH) = (block >> DMA_ADDR_BLOCK_H_SHIFT) & 0xff; + fmc_reg(FMC_ADDRL) = ((block & DMA_ADDR_BLOCK_MASK) << DMA_ADDR_BLOCK_SHIFT) + | ((in_block & DMA_ADDR_PAGE_MASK) << DMA_ADDR_PAGE_SHIFT); +} + +/* `irq` is FMC_INT sampled right after the page op: the status read that + * follows clears it and runs a register op of its own. */ +static void nand_rec_finish(nand_rec_t *rec, uint8_t status, uint8_t irq, + int timed_out) { + rec->ondie = status; + rec->fmc_int = irq; + rec->flags = 0; + if (timed_out || status == 0xFF) rec->flags |= NAND_REC_TIMEOUT; + rec->rsvd = 0; +} + +/* Read page + full OOB into dst (page_size + oob_size bytes). */ +int nand_page_read(uint32_t page, const nand_xfer_t *x, uint8_t *dst, + nand_rec_t *rec) { + uint32_t total = (uint32_t)nand_geom.page_size + nand_geom.oob_size; + if (!nand_geom.page_size) return -1; + if (x->fmc_cfg) fmc_reg(FMC_CFG) = x->fmc_cfg; + + if (x->mode == NAND_XFER_REG) { + nand_reg_raw(); + uint8_t status = nand_read(page, 0, dst, total); + rec->ecc_err = 0; + nand_rec_finish(rec, status, (uint8_t)fmc_reg(FMC_INT), 0); + return (rec->flags & NAND_REC_TIMEOUT) ? -1 : 0; + } + + /* As hifmc100_send_cmd_read(): wait for the chip, then hand the whole + * PAGE_READ + READ_FROM_CACHE sequence to the controller. */ + nand_wait_oip(); + fmc_reg(FMC_INT_CLR) = 0xFF; + fmc_reg(FMC_OP_CFG) = OP_CFG_FM_CS(0) | OP_CFG_MEM_IF_TYPE(x->iftype) + | OP_CFG_DUMMY_NUM(x->dummy); + nand_dma_set_addr(page); + fmc_reg(FMC_DMA_SADDR_D0) = (uint32_t)(uintptr_t)dst; + fmc_reg(FMC_DMA_SADDR_OOB) = (uint32_t)(uintptr_t)dst + nand_geom.page_size; + fmc_reg(FMC_OP_CTRL) = OP_CTRL_RD_OPCODE(x->opcode) + | OP_CTRL_RD_OP_SEL(RD_OP_READ_ALL_PAGE) + | OP_CTRL_DMA_OP(OP_TYPE_DMA) + | OP_CTRL_RW_OP(RW_OP_READ) + | OP_CTRL_DMA_OP_READY; + int timed_out = fmc_wait_dma_done(); + uint8_t irq = (uint8_t)fmc_reg(FMC_INT); + rec->ecc_err = fmc_reg(FMC_ECC_ERR_NUM0_BUF0); + nand_rec_finish(rec, nand_get_feature(NAND_FEATURE_STATUS), irq, timed_out); + return (rec->flags & NAND_REC_TIMEOUT) ? -1 : 0; +} + +/* Program page + full OOB from src (page_size + oob_size bytes). No + * empty-page-mark or other OOB fix-ups: the caller lays out the OOB, + * exactly as it should land in the controller's buffer. */ +int nand_page_program(uint32_t page, const nand_xfer_t *x, + const uint8_t *src, nand_rec_t *rec) { + uint32_t total = (uint32_t)nand_geom.page_size + nand_geom.oob_size; + if (!nand_geom.page_size) return -1; + if (x->fmc_cfg) fmc_reg(FMC_CFG) = x->fmc_cfg; + rec->ecc_err = 0; + + uint8_t irq; + if (x->mode == NAND_XFER_REG) { + nand_reg_raw(); + nand_program_page(page, 0, src, total); + irq = (uint8_t)fmc_reg(FMC_INT); + } else { + /* As hifmc100_send_cmd_write(). */ + nand_wait_oip(); + nand_write_enable(); + fmc_reg(FMC_INT_CLR) = 0xFF; + fmc_reg(FMC_OP_CFG) = OP_CFG_FM_CS(0) | OP_CFG_MEM_IF_TYPE(x->iftype); + nand_dma_set_addr(page); + fmc_reg(FMC_DMA_SADDR_D0) = (uint32_t)(uintptr_t)src; + fmc_reg(FMC_DMA_SADDR_OOB) = (uint32_t)(uintptr_t)src + nand_geom.page_size; + fmc_reg(FMC_OP_CTRL) = OP_CTRL_WR_OPCODE(x->opcode) + | OP_CTRL_DMA_OP(OP_TYPE_DMA) + | OP_CTRL_RW_OP(RW_OP_WRITE) + | OP_CTRL_DMA_OP_READY; + int timed_out = fmc_wait_dma_done(); + irq = (uint8_t)fmc_reg(FMC_INT); + if (timed_out) { + nand_rec_finish(rec, nand_get_feature(NAND_FEATURE_STATUS), irq, 1); + return -1; + } + } + uint8_t status = nand_wait_oip(); + nand_rec_finish(rec, status, irq, 0); + if (status != 0xFF && (status & NAND_STATUS_P_FAIL)) rec->flags |= NAND_REC_FAIL; + return rec->flags ? -1 : 0; +} + +int nand_block_erase(uint32_t page, nand_rec_t *rec) { + if (!nand_geom.page_size) return -1; + rec->ecc_err = 0; + int rc = nand_erase_block(page); + uint8_t irq = (uint8_t)fmc_reg(FMC_INT); + nand_rec_finish(rec, nand_get_feature(NAND_FEATURE_STATUS), irq, 0); + if (rc) rec->flags |= NAND_REC_FAIL; + return rc; +} diff --git a/agent/spi_flash.h b/agent/spi_flash.h index 5f5b7c2..1c479be 100644 --- a/agent/spi_flash.h +++ b/agent/spi_flash.h @@ -3,8 +3,8 @@ * * NOR: register-based reads via FMC normal mode (faster than memory * window when window wraps at 1MB on some SoCs). - * NAND: PAGE_READ → READ_FROM_CACHE flow with on-chip ECC enabled. - * Currently read-only (erase/write are NOR-only). + * NAND: PAGE_READ → READ_FROM_CACHE flow, erase and program, plus the + * CMD_NAND study interface below (controller page engine, raw OOB). */ #ifndef SPI_FLASH_H @@ -83,4 +83,58 @@ int flash_read_oob(uint32_t block, uint8_t *buf, uint32_t len); * on success, -1 if NOR or program fails. */ int flash_program_oob(uint32_t block, const uint8_t *buf, uint32_t len); +/* ---- SPI NAND study interface (CMD_NAND) ------------------------------- + * + * Page I/O at the level the Linux hifmc100 / xmedia_fmc100 drivers work + * at, so their behaviour can be reproduced or varied one knob at a time: + * + * NAND_XFER_REG PAGE_READ/READ_FROM_CACHE or PROGRAM_LOAD/EXECUTE + * through FMC register ops, page + full OOB, no + * controller ECC. With the chip's on-die ECC off this + * is the raw array content. + * NAND_XFER_DMA the controller's own page engine (FMC_OP_CTRL + DMA), + * as the kernel drives it. ECC type, page and block + * size come from the FMC_CFG value passed in. + */ +#define NAND_XFER_REG 0 +#define NAND_XFER_DMA 1 + +typedef struct { + uint32_t fmc_cfg; /* written to FMC_CFG before the op; 0 = leave as is */ + uint8_t mode; /* NAND_XFER_REG or NAND_XFER_DMA */ + uint8_t opcode; /* DMA: SPI read/program opcode (0x03, 0x6B, 0x02, 0x32 ...) */ + uint8_t iftype; /* DMA: FMC MEM_IF_TYPE (0 std, 1 dual, 2 dio, 3 quad, 4 qio) */ + uint8_t dummy; /* DMA read: dummy bytes after the column address */ +} nand_xfer_t; + +/* What the hardware said about one page operation. */ +typedef struct { + uint32_t ecc_err; /* FMC ECC_ERR_NUM0_BUF0: one byte per ECC step, + * 0xff = uncorrectable (DMA reads only) */ + uint8_t ondie; /* chip status, feature 0xC0, after the op */ + uint8_t fmc_int; /* FMC_INT after the op */ + uint8_t flags; /* NAND_REC_* */ + uint8_t rsvd; +} nand_rec_t; + +#define NAND_REC_TIMEOUT (1 << 0) /* controller or chip never finished */ +#define NAND_REC_FAIL (1 << 1) /* chip reported P_FAIL / E_FAIL */ + +/* Geometry of the identified chip; page_size 0 if it is not a SPI NAND. */ +typedef struct { + uint16_t page_size; + uint16_t oob_size; /* physical spare area */ + uint16_t pages_per_block; + uint16_t blocks; +} nand_geom_t; + +void nand_get_geometry(nand_geom_t *geom); +uint8_t nand_feature_get(uint8_t addr); +void nand_feature_set(uint8_t addr, uint8_t val); +int nand_page_read(uint32_t page, const nand_xfer_t *x, uint8_t *dst, + nand_rec_t *rec); +int nand_page_program(uint32_t page, const nand_xfer_t *x, + const uint8_t *src, nand_rec_t *rec); +int nand_block_erase(uint32_t page, nand_rec_t *rec); + #endif /* SPI_FLASH_H */ diff --git a/agent/startup.S b/agent/startup.S index ac4d7b8..75360d2 100644 --- a/agent/startup.S +++ b/agent/startup.S @@ -10,6 +10,8 @@ * but the layout (4096 × 1 MB sections, 16 KB-aligned) is identical. */ +#include "nand_layout.h" + .section .text.start .global _start .arm @@ -145,6 +147,17 @@ pt_fill_ddr_v5: subs r7, r7, #1 bne pt_fill_ddr_v5 +#ifdef HAVE_NAND_STUDY + /* NAND study DMA buffer: uncached (B=0, C=0) so the FMC's DMA and the + * CPU see the same bytes without cache maintenance. */ + ldr r1, =NAND_DMA_BUF + lsr r2, r1, #20 + add r0, r4, r2, lsl #2 + ldr r6, =0x00000C02 + orr r1, r6, r2, lsl #20 + str r1, [r0] +#endif + /* Drain write buffer so the page table writes are visible to the MMU * before TTBR points at it. */ mov r0, #0 @@ -266,6 +279,18 @@ pt_fill_ddr: subs r7, r7, #1 bne pt_fill_ddr +#ifdef HAVE_NAND_STUDY + /* NAND study DMA buffer: Normal non-cacheable (TEX=001, C=0, B=0), so + * the FMC's DMA and the CPU see the same bytes without cache + * maintenance. AP=11 -> (3<<10)|(1<<12)|0b10 = 0x00001C02. */ + ldr r1, =NAND_DMA_BUF + lsr r2, r1, #20 + add r0, r4, r2, lsl #2 + ldr r6, =0x00001C02 + orr r1, r6, r2, lsl #20 + str r1, [r0] +#endif + /* ===== ENABLE MMU + CACHES ===== */ /* Invalidate TLB again (page table just written) */ diff --git a/src/defib/agent/client.py b/src/defib/agent/client.py index 6f9ba7f..c084be8 100644 --- a/src/defib/agent/client.py +++ b/src/defib/agent/client.py @@ -343,6 +343,7 @@ async def _restore_baud(self) -> None: CAP_SELFUPDATE = 1 << 5 CAP_SCAN = 1 << 6 CAP_MEMBW = 1 << 7 + CAP_NAND = 1 << 8 async def get_info(self) -> dict[str, int | str]: """Request device info from the agent.""" diff --git a/src/defib/agent/nand.py b/src/defib/agent/nand.py new file mode 100644 index 0000000..a761a63 --- /dev/null +++ b/src/defib/agent/nand.py @@ -0,0 +1,405 @@ +"""SPI NAND study operations over the flash agent (CMD_NAND). + +The agent drives the HiSilicon/Goke FMC100 controller's page engine the +way the Linux hifmc100 / xmedia_fmc100 drivers do — or deliberately not: +ECC type, transfer mode, SPI opcodes and the exact OOB bytes are all the +caller's. That is what it takes to chase ECC faults such as +OpenIPC/firmware#2285 and #2519 below the OS. + +Pages and per-page results stay in agent RAM (``NandInfo.dma_buf`` / +``stat_buf``) and move over the ordinary CMD_READ / CMD_WRITE stream; +CMD_NAND itself only carries parameters and one-record answers. +""" + +from __future__ import annotations + +import struct +import time +from dataclasses import dataclass, field +from enum import IntEnum +from typing import TYPE_CHECKING + +from defib.agent.protocol import ( + CMD_NAND, + RSP_ACK, + RSP_DATA, + RSP_NAND, + RSP_READY, + recv_packet, + send_packet, +) +from defib.transport.base import TransportError, TransportTimeout + +if TYPE_CHECKING: + from defib.agent.client import FlashAgentClient + +# Sub-operations and statuses (agent/protocol.h). +NAND_OP_INFO = 0x00 +NAND_OP_FEATURE_GET = 0x01 +NAND_OP_FEATURE_SET = 0x02 +NAND_OP_READ_PAGES = 0x03 +NAND_OP_PROGRAM_PAGE = 0x04 +NAND_OP_ERASE_BLOCK = 0x05 +NAND_OP_FMC_REG = 0x06 + +NAND_ST_OK = 0x00 +NAND_ST_BADARG = 0x01 +NAND_ST_NOT_NAND = 0x02 +NAND_ST_IO = 0x03 + +REC_SIZE = 8 +REC_TIMEOUT = 1 << 0 +REC_FAIL = 1 << 1 + +# SPI NAND feature registers. +FEATURE_PROTECT = 0xA0 +FEATURE_CONFIG = 0xB0 # bit 4 ECC-E (on-die ECC), bit 3 BUF on Winbond +FEATURE_STATUS = 0xC0 # bits 5:4 ECC_S, 3 P_FAIL, 2 E_FAIL, 0 OIP + +# FMC_CFG fields (include/linux/mfd/hisi_fmc.h). +FMC_CFG_OP_MODE_NORMAL = 1 << 0 +FMC_CFG_FLASH_SEL_MASK = 0x3 << 1 +FMC_CFG_FLASH_SEL_SPI_NAND = 0x1 << 1 +FMC_CFG_PAGE_SIZE_MASK = 0x3 << 3 +FMC_CFG_ECC_TYPE_SHIFT = 5 +FMC_CFG_ECC_TYPE_MASK = 0x7 << FMC_CFG_ECC_TYPE_SHIFT +FMC_CFG_BLOCK_SIZE_MASK = 0x3 << 8 + + +class EccType(IntEnum): + """FMC_CFG ECC_TYPE values.""" + + NONE = 0 + BIT8 = 1 + BIT16 = 2 + BIT24 = 3 + BIT28 = 4 + BIT40 = 5 + BIT64 = 6 + + +class XferMode(IntEnum): + REG = 0 # register ops: page + full OOB as the array holds it + DMA = 1 # the controller's page engine, controller ECC per FMC_CFG + + +class NandError(RuntimeError): + """The agent refused a CMD_NAND request or the hardware failed it.""" + + +def compose_fmc_cfg(live: int, ecc: EccType) -> int: + """FMC_CFG for SPI NAND, 2 KiB pages, 64-page blocks and ``ecc``. + + Bits the kernel does not touch (SPI_NAND_SEL, address mode, ...) are + taken from ``live``, the register as it stands. + """ + cfg = live & ~( + FMC_CFG_FLASH_SEL_MASK | FMC_CFG_PAGE_SIZE_MASK + | FMC_CFG_ECC_TYPE_MASK | FMC_CFG_BLOCK_SIZE_MASK + ) + return ( + cfg | FMC_CFG_OP_MODE_NORMAL | FMC_CFG_FLASH_SEL_SPI_NAND + | (int(ecc) << FMC_CFG_ECC_TYPE_SHIFT) + ) + + +def ecc_type_of(fmc_cfg: int) -> EccType: + return EccType((fmc_cfg & FMC_CFG_ECC_TYPE_MASK) >> FMC_CFG_ECC_TYPE_SHIFT) + + +@dataclass(frozen=True) +class NandXfer: + """How the agent moves one page (agent nand_xfer_t).""" + + mode: XferMode = XferMode.DMA + fmc_cfg: int = 0 # 0 = leave FMC_CFG as it is + opcode: int = 0x03 # read: 0x03 std / 0x0B fast / 0x6B quad; program: 0x02 / 0x32 + iftype: int = 0 # 0 std, 1 dual, 2 dio, 3 quad, 4 qio + dummy: int = 1 # dummy bytes after the column address (reads) + + def pack(self) -> bytes: + return struct.pack( + " NandXfer: + """The program-side twin of a read transfer: same mode and FMC_CFG.""" + quad = read.iftype in (3, 4) + return NandXfer( + mode=read.mode, fmc_cfg=read.fmc_cfg, + opcode=0x32 if quad else 0x02, iftype=3 if quad else 0, dummy=0, + ) + + +@dataclass(frozen=True) +class NandRecord: + """What the hardware said about one page op (agent nand_rec_t).""" + + ecc_err: int # FMC ECC_ERR_NUM0_BUF0: a byte per ECC step, 0xff = uncorrectable + ondie: int # chip status, feature 0xC0 + fmc_int: int + flags: int + + @classmethod + def unpack(cls, raw: bytes) -> NandRecord: + ecc_err, ondie, fmc_int, flags, _ = struct.unpack(" list[int]: + """Per-ECC-step error counts; 0xff means uncorrectable.""" + return [(self.ecc_err >> (8 * i)) & 0xFF for i in range(steps)] + + def uncorrectable_steps(self, steps: int) -> list[int]: + return [i for i, n in enumerate(self.step_errors(steps)) if n == 0xFF] + + def corrected_bits(self, steps: int) -> int: + return sum(n for n in self.step_errors(steps) if n != 0xFF) + + @property + def ondie_ecc(self) -> int: + """On-die ECC verdict, status bits 5:4 (0 clean, 1 corrected, 2 failed).""" + return (self.ondie >> 4) & 0x3 + + @property + def failed(self) -> bool: + return bool(self.flags & (REC_TIMEOUT | REC_FAIL)) + + +@dataclass(frozen=True) +class NandInfo: + page_size: int + oob_size: int + pages_per_block: int + blocks: int + dma_buf: int + dma_size: int + stat_buf: int + stat_size: int + fmc_cfg: int + + @property + def stride(self) -> int: + return self.page_size + self.oob_size + + @property + def pages(self) -> int: + return self.pages_per_block * self.blocks + + @property + def ecc_steps(self) -> int: + """ECC steps per page: the FMC100 BCH engine covers 1 KiB each.""" + return max(1, self.page_size // 1024) + + +@dataclass +class PageRead: + page: int + data: bytes + oob: bytes + record: NandRecord + + +@dataclass +class ScanResult: + start: int + records: list[NandRecord] = field(default_factory=list) + + +def kernel_oob(oob_size: int) -> bytes: + """The OOB a Linux hifmc100 write sends for a page with no OOB data. + + All 0xff except the empty-page mark: hifmc100_send_cmd_write() zeroes + the two bytes at oobfree[0].offset + 28 (= 30 with the default + layout) before every program, so the controller can later tell a + written page from an erased one. + """ + oob = bytearray(b"\xff" * oob_size) + oob[30:32] = b"\x00\x00" + return bytes(oob) + + +class NandStudy: + """CMD_NAND on top of a connected FlashAgentClient.""" + + def __init__(self, client: FlashAgentClient) -> None: + self._client = client + self._info: NandInfo | None = None + self.last_status = NAND_ST_OK + + async def _call(self, op: int, args: bytes = b"", timeout: float = 5.0) -> bytes: + transport = self._client._transport + self._client._clear_rx_buffers() + await send_packet(transport, CMD_NAND, bytes([op]) + args) + deadline = time.monotonic() + timeout + in_stale_stream = False + while True: + if time.monotonic() > deadline: + raise NandError(f"no answer to CMD_NAND op 0x{op:02x} within {timeout:.0f}s") + # recv_packet, not recv_response: the latter restarts its timeout + # on every READY, so an idle agent that never got this request + # (lost to a baud mismatch, say) would keep it waiting forever. + try: + cmd, data = await recv_packet( + transport, max(0.1, deadline - time.monotonic()), + ) + except TransportTimeout: + raise NandError(f"no answer to CMD_NAND op 0x{op:02x} within {timeout:.0f}s") + if cmd == RSP_READY: + continue + if cmd == RSP_NAND and len(data) >= 2 and data[0] == op: + break + # Anything else is left over from an exchange the host gave up + # on — the tail of an abandoned read (RSP_DATA ... ACK) or a + # late answer to an earlier request. Only an ACK with no data + # before it is a real answer: an agent without CMD_NAND. + if cmd == RSP_DATA: + in_stale_stream = True + continue + if cmd == RSP_ACK and not in_stale_stream: + break + in_stale_stream = False + if cmd != RSP_NAND or len(data) < 2 or data[0] != op: + raise NandError( + f"agent did not answer CMD_NAND op 0x{op:02x} " + f"(cmd=0x{cmd:02x}, {len(data)} bytes); does it advertise CAP_NAND?" + ) + status = data[1] + if status == NAND_ST_BADARG: + raise NandError(f"agent rejected CMD_NAND op 0x{op:02x}: bad argument") + if status == NAND_ST_NOT_NAND: + raise NandError("agent found no SPI NAND on this board") + if status not in (NAND_ST_OK, NAND_ST_IO): + raise NandError(f"CMD_NAND op 0x{op:02x}: unknown status 0x{status:02x}") + self.last_status = status + return data[2:] + + async def resync(self, quiet: float = 1.0, limit: float = 60.0) -> None: + """Drop whatever the agent is still sending until the line is quiet. + + After the host abandons a read (a sequence gap, say), the agent + still streams the rest of it; this waits that out. + """ + transport = self._client._transport + end = time.monotonic() + limit + quiet_since = time.monotonic() + while time.monotonic() < end and time.monotonic() - quiet_since < quiet: + try: + cmd, _ = await recv_packet(transport, quiet) + except (TransportTimeout, TransportError): + break + # An idle agent's periodic READY is not part of a stale stream. + if cmd != RSP_READY: + quiet_since = time.monotonic() + self._client._clear_rx_buffers() + # The link may have lost its fast baud along the way (on both DUTs a + # read that broke at 921600 left the agent answering only after it + # had idled back to 115200). If it does not answer where we are, + # go to the fallback rate and wait for its READY. + try: + await self._call(NAND_OP_INFO, timeout=3.0) + # It answered at whatever rate the port is at now; make the + # client agree, so the next fast read renegotiates if needed. + port = getattr(transport, "_port", None) + if port is not None and getattr(port, "baudrate", None): + self._client._current_baud = int(port.baudrate) + return + except (NandError, TransportError): + pass + from defib.agent.client import FALLBACK_BAUD + from defib.agent.protocol import wait_for_ready + await transport.set_baudrate(FALLBACK_BAUD) + self._client._current_baud = FALLBACK_BAUD + self._client._clear_rx_buffers() + if not await wait_for_ready(transport, 45.0): + raise NandError("agent lost after a failed transfer; no READY at the fallback baud") + + async def info(self) -> NandInfo: + raw = await self._call(NAND_OP_INFO) + page, oob, ppb, blocks, dma, dma_size, stat, stat_size, cfg = struct.unpack( + " NandInfo: + return self._info or await self.info() + + async def feature_get(self, addr: int) -> int: + return (await self._call(NAND_OP_FEATURE_GET, bytes([addr])))[0] + + async def feature_set(self, addr: int, value: int) -> int: + """Write a feature register; returns what it reads back.""" + return (await self._call(NAND_OP_FEATURE_SET, bytes([addr, value])))[0] + + async def fmc_reg(self, offset: int, value: int | None = None) -> int: + """Read (or write, then read) a 32-bit FMC register.""" + args = struct.pack(" tuple[list[NandRecord], bytes]: + """Read ``count`` pages from ``start``. + + Returns the records of the pages the agent got through (it stops + at the first timeout) and, with ``store``, their page + OOB bytes + packed at ``stride``. + """ + info = await self._geometry() + args = struct.pack(" PageRead: + info = await self._geometry() + records, data = await self.read_pages(page, 1, xfer) + if not records: + raise NandError(f"page {page}: no result") + return PageRead(page, data[:info.page_size], data[info.page_size:info.stride], records[0]) + + async def scan( + self, start: int, count: int, xfer: NandXfer, chunk: int = 4096, + ) -> ScanResult: + """ECC survey: read every page, keep only the records.""" + info = await self._geometry() + chunk = min(chunk, info.stat_size // REC_SIZE) + result = ScanResult(start=start) + page = start + while page < start + count: + n = min(chunk, start + count - page) + records, _ = await self.read_pages(page, n, xfer, store=False) + result.records.extend(records) + if len(records) < n: + break + page += n + return result + + async def program_page(self, page: int, payload: bytes, xfer: NandXfer) -> NandRecord: + """Program page + OOB exactly as given (no OOB fix-ups).""" + info = await self._geometry() + if len(payload) != info.stride: + raise ValueError(f"need {info.stride} bytes (page + OOB), got {len(payload)}") + if not await self._client.write_memory(info.dma_buf, payload): + raise NandError("could not stage the page in agent RAM") + args = struct.pack(" NandRecord: + raw = await self._call(NAND_OP_ERASE_BLOCK, struct.pack(" tuple[int, bytes]: waiting = port.in_waiting # type: ignore[attr-defined] if waiting > 0: data = port.read(waiting) # type: ignore[attr-defined] + elif getattr(port, "timeout", None) is None: + # A blocking port (SerialTransport opens with timeout=None): + # read(1) would wait for a byte forever, so a silent agent + # — wrong baud, crashed — would hang every caller past its + # timeout. Poll instead. + time.sleep(0.002) + continue else: # port.read() blocks up to the port's pre-set timeout # (Rfc2217Transport._PYSERIAL_READ_QUANTUM = 10 ms). diff --git a/src/defib/cli/app.py b/src/defib/cli/app.py index 2d3ca2d..ccd066d 100644 --- a/src/defib/cli/app.py +++ b/src/defib/cli/app.py @@ -1022,6 +1022,10 @@ def list_interfaces_cmd( agent_app = typer.Typer(help="Flash agent commands (fast binary protocol)") app.add_typer(agent_app, name="agent") +from defib.cli.nand import nand_app # noqa: E402 - sub-app registration + +agent_app.add_typer(nand_app, name="nand") + def _agent_not_responding_message(chip: str, uboot_address: int | None = None) -> str: """Build the diagnostic shown when boot protocol uploads complete but the diff --git a/src/defib/cli/nand.py b/src/defib/cli/nand.py new file mode 100644 index 0000000..b4dd701 --- /dev/null +++ b/src/defib/cli/nand.py @@ -0,0 +1,391 @@ +"""``defib agent nand`` — SPI NAND study commands over the flash agent. + +Low-level access to the FMC100 SPI NAND path for chasing ECC faults below +the OS: identify the chip and its feature registers, read pages raw or +through the controller's ECC engine, survey ECC status over a range, dump +page + OOB, and (destructively) program or erase single pages/blocks. + +``write-page`` and ``erase-block`` change the flash immediately, with no +prompt, like every other writing command in defib. Take a ``dump`` +first. +""" + +from __future__ import annotations + +import asyncio +import json +import sys +from collections.abc import Awaitable, Callable +from pathlib import Path +from typing import Any, TypeVar + +import typer +from rich.console import Console + +nand_app = typer.Typer(help="SPI NAND study ops: raw pages, OOB, ECC status (FMC100 SoCs)") + +T = TypeVar("T") + +PORT_HELP = "Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path" +ECC_CHOICES = {"none": 0, "8": 1, "16": 2, "24": 3, "28": 4, "40": 5, "64": 6} + + +def _run(port: str, body: Callable[[Any], Awaitable[T]]) -> T: + """Connect to the agent on ``port``, run ``body(NandStudy)``, close.""" + from defib.agent.client import FlashAgentClient + from defib.agent.nand import NandError, NandStudy + from defib.transport.serial_platform import create_transport, normalize_port_name + + console = Console(stderr=True) + + async def go() -> T: + transport = await create_transport(normalize_port_name(port)) + try: + client = FlashAgentClient(transport) + if not await client.connect(timeout=5.0): + console.print("[red]Agent not responding.[/red] Upload it first with " + "'defib agent upload'.") + raise typer.Exit(1) + info = await client.get_info() + if not int(info.get("capabilities", 0)) & FlashAgentClient.CAP_NAND: + console.print("[red]This agent has no CMD_NAND[/red] (agent version " + f"{info.get('agent_version', '?')}); rebuild and re-upload it.") + raise typer.Exit(1) + return await body(NandStudy(client)) + except NandError as e: + console.print(f"[red]{e}[/red]") + raise typer.Exit(1) + finally: + await transport.close() + + return asyncio.run(go()) + + +def _xfer( + study: Any, mode: str, ecc: str, fmc_cfg: str, opcode: str, iftype: int, dummy: int, +) -> Any: + """Build a NandXfer; the FMC_CFG base is the live register.""" + from defib.agent.nand import EccType, NandXfer, XferMode, compose_fmc_cfg + + if mode not in ("reg", "dma"): + raise typer.BadParameter("--mode must be reg or dma") + if fmc_cfg: + cfg = int(fmc_cfg, 0) + elif mode == "reg": + cfg = 0 + else: + if ecc not in ECC_CHOICES: + raise typer.BadParameter(f"--ecc must be one of {', '.join(ECC_CHOICES)}") + assert study._info is not None + cfg = compose_fmc_cfg(study._info.fmc_cfg, EccType(ECC_CHOICES[ecc])) + return NandXfer( + mode=XferMode.REG if mode == "reg" else XferMode.DMA, + fmc_cfg=cfg, opcode=int(opcode, 0), iftype=iftype, dummy=dummy, + ) + + +def _describe(rec: Any, steps: int) -> str: + parts = [] + errs = rec.step_errors(steps) + if any(errs): + parts.append("ecc " + "/".join("UNCORR" if n == 0xFF else str(n) for n in errs)) + else: + parts.append("ecc clean") + parts.append(f"on-die {['clean', 'corrected', 'FAILED', 'corrected(3)'][rec.ondie_ecc]}") + parts.append(f"status 0x{rec.ondie:02x}") + if rec.failed: + parts.append("[red]OP FAILED[/red]") + return ", ".join(parts) + + +def _features(study: Any) -> Awaitable[dict[str, int]]: + async def read() -> dict[str, int]: + from defib.agent.nand import FEATURE_CONFIG, FEATURE_PROTECT, FEATURE_STATUS + return { + "protect_a0": await study.feature_get(FEATURE_PROTECT), + "config_b0": await study.feature_get(FEATURE_CONFIG), + "status_c0": await study.feature_get(FEATURE_STATUS), + } + return read() + + +# Shared options -------------------------------------------------------------- + +PortOpt = typer.Option("/dev/ttyUSB0", "-p", "--port", help=PORT_HELP) +ModeOpt = typer.Option("dma", "--mode", help="reg: raw register reads; dma: controller page engine") +EccOpt = typer.Option("8", "--ecc", help="Controller ECC for --mode dma: none, 8, 16, 24, 28, 40, 64") +CfgOpt = typer.Option("", "--fmc-cfg", help="Exact FMC_CFG value (hex); overrides --ecc") +OpcodeOpt = typer.Option("0x03", "--opcode", help="DMA read opcode: 0x03 std, 0x0B fast, 0x6B quad") +IftypeOpt = typer.Option(0, "--iftype", help="FMC interface: 0 std, 1 dual, 2 dio, 3 quad, 4 qio") +DummyOpt = typer.Option(1, "--dummy", help="Dummy bytes after the column address") + + +@nand_app.command("info") +def nand_info(port: str = PortOpt) -> None: + """Chip geometry, feature registers and live FMC_CFG.""" + async def body(study: Any) -> None: + info = await study.info() + feats = await _features(study) + print(f"Page {info.page_size} + OOB {info.oob_size}, " + f"{info.pages_per_block} pages/block, {info.blocks} blocks " + f"({info.pages * info.page_size // (1024 * 1024)} MiB)") + print(f"FMC_CFG 0x{info.fmc_cfg:08x}") + b0 = feats["config_b0"] + print(f"Feature 0xA0 (protect) 0x{feats['protect_a0']:02x}") + print(f"Feature 0xB0 (config) 0x{b0:02x} on-die ECC {'ON' if b0 & 0x10 else 'off'}" + f", BUF {1 if b0 & 0x08 else 0}, QE {b0 & 0x01}") + print(f"Feature 0xC0 (status) 0x{feats['status_c0']:02x}") + print(f"Agent buffers: pages @ 0x{info.dma_buf:08x} ({info.dma_size} B), " + f"records @ 0x{info.stat_buf:08x} ({info.stat_size} B)") + _run(port, body) + + +@nand_app.command("feature") +def nand_feature( + addr: str = typer.Argument(..., help="Feature register, e.g. 0xB0"), + value: str = typer.Argument("", help="Value to write (hex); omit to read"), + port: str = PortOpt, +) -> None: + """Read or write a SPI NAND feature register (volatile).""" + async def body(study: Any) -> None: + reg = int(addr, 0) + if value: + got = await study.feature_set(reg, int(value, 0)) + print(f"0x{reg:02x} <- 0x{int(value, 0):02x}, reads back 0x{got:02x}") + else: + print(f"0x{reg:02x} = 0x{await study.feature_get(reg):02x}") + _run(port, body) + + +@nand_app.command("fmc-reg") +def nand_fmc_reg( + offset: str = typer.Argument(..., help="FMC register offset, e.g. 0x00 (FMC_CFG)"), + value: str = typer.Argument("", help="Value to write (hex); omit to read"), + port: str = PortOpt, +) -> None: + """Read or write a 32-bit FMC controller register.""" + async def body(study: Any) -> None: + got = await study.fmc_reg(int(offset, 0), int(value, 0) if value else None) + print(f"FMC+0x{int(offset, 0):03x} = 0x{got:08x}") + _run(port, body) + + +@nand_app.command("read-page") +def nand_read_page( + page: int = typer.Argument(..., help="Page number"), + port: str = PortOpt, + mode: str = ModeOpt, + ecc: str = EccOpt, + fmc_cfg: str = CfgOpt, + opcode: str = OpcodeOpt, + iftype: int = IftypeOpt, + dummy: int = DummyOpt, + output: str = typer.Option("", "-o", "--output", help="Write page + OOB to this file"), +) -> None: + """Read one page + OOB and report what the controller and chip said.""" + async def body(study: Any) -> None: + info = await study.info() + x = _xfer(study, mode, ecc, fmc_cfg, opcode, iftype, dummy) + r = await study.read_page(page, x) + print(f"page {page} ({mode}, FMC_CFG 0x{x.fmc_cfg or info.fmc_cfg:08x}): " + f"{_describe(r.record, info.ecc_steps)}") + print(f"data[0:32] {r.data[:32].hex()}") + for off in range(0, len(r.oob), 32): + print(f"oob[{off:3d}] {r.oob[off:off + 32].hex()}") + if output: + Path(output).write_bytes(r.data + r.oob) + print(f"wrote {output}") + _run(port, body) + + +@nand_app.command("ecc-scan") +def nand_ecc_scan( + port: str = PortOpt, + start: int = typer.Option(0, "--start", help="First page"), + count: int = typer.Option(0, "--count", help="Pages to scan (0 = to the end)"), + mode: str = ModeOpt, + ecc: str = EccOpt, + fmc_cfg: str = CfgOpt, + opcode: str = OpcodeOpt, + iftype: int = IftypeOpt, + dummy: int = DummyOpt, + json_out: str = typer.Option("", "--json", help="Write every page's record to this file"), +) -> None: + """Read a page range and report ECC status per page (data is discarded).""" + async def body(study: Any) -> None: + info = await study.info() + n = count or info.pages - start + x = _xfer(study, mode, ecc, fmc_cfg, opcode, iftype, dummy) + result = await study.scan(start, n, x) + steps = info.ecc_steps + uncorr = [(start + i, r) for i, r in enumerate(result.records) if r.uncorrectable_steps(steps)] + corrected = [(start + i, r) for i, r in enumerate(result.records) + if not r.uncorrectable_steps(steps) and r.corrected_bits(steps)] + print(f"scanned {len(result.records)}/{n} pages from {start} " + f"(FMC_CFG 0x{x.fmc_cfg or info.fmc_cfg:08x}, {mode})") + print(f"uncorrectable: {len(uncorr)} corrected: {len(corrected)}") + for page, rec in uncorr[:200]: + blk, pg = divmod(page, info.pages_per_block) + print(f" page {page:6d} (block {blk:4d} page {pg:2d}) " + f"steps {rec.uncorrectable_steps(steps)} ecc_err 0x{rec.ecc_err:08x}") + if len(uncorr) > 200: + print(f" ... {len(uncorr) - 200} more (see --json)") + if json_out: + Path(json_out).write_text(json.dumps({ + "start": start, "fmc_cfg": x.fmc_cfg or info.fmc_cfg, "mode": mode, + "ecc_steps": steps, "pages_per_block": info.pages_per_block, + "records": [[r.ecc_err, r.ondie, r.fmc_int, r.flags] for r in result.records], + })) + print(f"wrote {json_out}") + if len(result.records) < n: + print(f"[stopped early at page {start + len(result.records)}: controller timeout]", + file=sys.stderr) + _run(port, body) + + +async def _read_batch(study: Any, page: int, count: int, xfer: Any, tries: int = 4) -> Any: + """read_pages with retries: a long UART stream occasionally drops a + frame, and one bad batch must not cost a half-hour dump. Retries move + the data at the fallback baud: when 921600 drops frames once, it + tends to keep doing so while whatever loads the host lasts.""" + for attempt in range(1, tries + 1): + try: + return await study.read_pages(page, count, xfer, fast=attempt == 1) + except Exception as e: # noqa: BLE001 - transport and protocol errors alike + if attempt == tries: + raise + print(f"\n[batch at page {page} failed ({e}); retrying]", file=sys.stderr) + await study.resync() + raise AssertionError("unreachable") + + +@nand_app.command("dump") +def nand_dump( + output: str = typer.Option(..., "-o", "--output", help="Raw dump file (page + OOB per page)"), + port: str = PortOpt, + start: int = typer.Option(0, "--start", help="First page"), + count: int = typer.Option(0, "--count", help="Pages (0 = to the end)"), + mode: str = typer.Option("reg", "--mode", help="reg (default, raw array) or dma"), + ecc: str = typer.Option("none", "--ecc", help="Controller ECC for --mode dma"), + fmc_cfg: str = CfgOpt, + keep_ondie: bool = typer.Option( + False, "--keep-ondie-ecc", + help="Leave the chip's on-die ECC as it is; by default it is switched off for the " + "dump (raw bits) and restored afterwards", + ), +) -> None: + """Back up page + OOB, raw by default, with a JSON sidecar of per-page records.""" + async def body(study: Any) -> None: + from defib.agent.nand import FEATURE_CONFIG + + info = await study.info() + n = count or info.pages - start + x = _xfer(study, mode, ecc, fmc_cfg, "0x03", 0, 1) + feats = await _features(study) + b0 = feats["config_b0"] + toggled = not keep_ondie and bool(b0 & 0x10) + batch = max(1, info.dma_size // info.stride) + records: list[Any] = [] + failed_page = None + try: + if toggled: + got = await study.feature_set(FEATURE_CONFIG, b0 & ~0x10) + if got & 0x10: + Console(stderr=True).print( + f"[red]The chip kept its on-die ECC on (0xB0 reads 0x{got:02x}); " + "a dump now would not be raw.[/red] Use --keep-ondie-ecc to dump " + "corrected data anyway.") + raise typer.Exit(1) + with open(output, "wb") as f: + page = start + while page < start + n: + k = min(batch, start + n - page) + recs, data = await _read_batch(study, page, k, x) + # The agent stops after a failed page and still returns it; + # its bytes are whatever the buffer held, so keep them out. + good = len(recs) - 1 if recs and recs[-1].failed else len(recs) + f.write(data[:good * info.stride]) + records.extend(recs[:good]) + page += good + print(f"\r{page - start}/{n} pages", end="", file=sys.stderr, flush=True) + if good < k: + failed_page = page + break + finally: + if toggled: + try: + await study.feature_set(FEATURE_CONFIG, b0) + except Exception as e: # noqa: BLE001 - must not mask the dump's own error + Console(stderr=True).print( + f"[yellow]Could not restore feature 0xB0 to 0x{b0:02x}: {e}[/yellow]") + print(file=sys.stderr) + sidecar = Path(output + ".json") + sidecar.write_text(json.dumps({ + "start": start, "pages": len(records), "requested": n, + "complete": failed_page is None, "failed_page": failed_page, + "page_size": info.page_size, + "oob_size": info.oob_size, "pages_per_block": info.pages_per_block, + "mode": mode, "fmc_cfg": x.fmc_cfg or info.fmc_cfg, + "features": feats, "ondie_ecc_during_dump": bool(b0 & 0x10) and not toggled, + "records": [[r.ecc_err, r.ondie, r.fmc_int, r.flags] for r in records], + })) + print(f"{len(records)} pages x {info.stride} B -> {output} (+ {sidecar.name})") + if failed_page is not None: + Console(stderr=True).print( + f"[red]INCOMPLETE: page {failed_page} could not be read; " + f"{n - len(records)} of {n} pages are missing.[/red]") + raise typer.Exit(1) + _run(port, body) + + +@nand_app.command("write-page") +def nand_write_page( + page: int = typer.Argument(..., help="Page number"), + input_file: str = typer.Option(..., "-i", "--input", help="Page data, or page + OOB"), + port: str = PortOpt, + mode: str = ModeOpt, + ecc: str = EccOpt, + fmc_cfg: str = CfgOpt, + kernel_oob: bool = typer.Option( + False, "--kernel-oob", + help="Input is page data only; add the OOB a Linux hifmc100 write sends " + "(0xff with the empty-page mark zeroed)", + ), + quad: bool = typer.Option(False, "--quad", help="Program with 0x32 over 4 lines"), +) -> None: + """DESTRUCTIVE: program one page (+ OOB) exactly as given. No prompt.""" + async def body(study: Any) -> None: + from defib.agent.nand import kernel_oob as make_oob + from defib.agent.nand import program_xfer + + info = await study.info() + payload = Path(input_file).read_bytes() + if kernel_oob: + if len(payload) != info.page_size: + raise typer.BadParameter(f"--kernel-oob needs exactly {info.page_size} bytes") + payload += make_oob(info.oob_size) + if len(payload) != info.stride: + raise typer.BadParameter(f"need {info.stride} bytes (page + OOB), got {len(payload)}") + read_x = _xfer(study, mode, ecc, fmc_cfg, "0x6B" if quad else "0x03", 3 if quad else 0, 1) + rec = await study.program_page(page, payload, program_xfer(read_x)) + print(f"programmed page {page}: status 0x{rec.ondie:02x}" + f"{' FAILED' if rec.failed else ''}") + if rec.failed: + raise typer.Exit(1) + _run(port, body) + + +@nand_app.command("erase-block") +def nand_erase_block( + page: int = typer.Argument(..., help="Any page in the block"), + port: str = PortOpt, +) -> None: + """DESTRUCTIVE: erase the 128 KiB block containing PAGE. No prompt.""" + async def body(study: Any) -> None: + info = await study.info() + rec = await study.erase_block(page) + print(f"erased block {page // info.pages_per_block}: status 0x{rec.ondie:02x}" + f"{' FAILED' if rec.failed else ''}") + if rec.failed: + raise typer.Exit(1) + _run(port, body) diff --git a/tests/test_agent_nand.py b/tests/test_agent_nand.py new file mode 100644 index 0000000..9837d8a --- /dev/null +++ b/tests/test_agent_nand.py @@ -0,0 +1,395 @@ +"""Tests for the SPI NAND study ops (CMD_NAND) — host side. + +FakeNandAgent answers CMD_NAND the way agent/main.c handle_nand() does, +over a simulated chip, and keeps the agent's RAM buffers in a dict so the +CMD_READ / CMD_WRITE half of the protocol can be faked at the client. +""" + +from __future__ import annotations + +import json +import struct +from pathlib import Path +from typing import Any + +import pytest +from typer.testing import CliRunner + +from defib.agent import cobs +from defib.agent.nand import ( + FEATURE_CONFIG, + REC_FAIL, + EccType, + NandError, + NandRecord, + NandStudy, + NandXfer, + XferMode, + compose_fmc_cfg, + ecc_type_of, + kernel_oob, + program_xfer, +) +from defib.agent.protocol import CMD_NAND, RSP_NAND, build_packet, parse_packet +from defib.transport.mock import MockTransport + +PAGE, OOB, PPB, BLOCKS = 2048, 64, 64, 4 +STRIDE = PAGE + OOB +DMA_BUF, DMA_SIZE = 0x40E00000, 0x100000 +STAT_BUF, STAT_SIZE = 0x42000000, 0x80000 + + +class FakeNandAgent(MockTransport): + def __init__(self) -> None: + super().__init__(flush_clears_buffer=False) + self.pages: dict[int, bytes] = {} + self.records: dict[int, NandRecord] = {} # page -> what a read reports + self.features = {0xA0: 0x00, 0xB0: 0x18, 0xC0: 0x00} + self.fmc = {0x00: 0x1821} + self.ram: dict[int, bytes] = {} + self.calls: list[tuple[int, bytes]] = [] + self.status_override: int | None = None + self.timeout_at: int | None = None + self.not_nand = False + + async def write(self, data: bytes) -> None: + await super().write(data) + for frame in data.split(b"\x00"): + if frame: + cmd, payload = parse_packet(frame) + assert cmd == CMD_NAND + self._handle(payload) + + def _reply(self, op: int, status: int, payload: bytes = b"") -> None: + if self.status_override is not None: + status = self.status_override + self.enqueue_rx(build_packet(RSP_NAND, bytes([op, status]) + payload)) + + def _handle(self, payload: bytes) -> None: + op, arg = payload[0], payload[1:] + self.calls.append((op, arg)) + if op == 0x06: + write, off, value = struct.unpack(" None: + self._transport = agent + self.agent = agent + + def _clear_rx_buffers(self) -> None: + pass + + async def _restore_baud(self) -> None: + pass + + async def read_memory(self, addr: int, size: int, fast: bool = True) -> bytes: + return self.agent.ram[addr][:size] + + async def write_memory(self, addr: int, data: bytes) -> bool: + self.agent.ram[addr] = data + return True + + +@pytest.fixture +def agent() -> FakeNandAgent: + return FakeNandAgent() + + +@pytest.fixture +def study(agent: FakeNandAgent) -> NandStudy: + return NandStudy(FakeClient(agent)) # type: ignore[arg-type] + + +# --- pure helpers ------------------------------------------------------------- + +def test_compose_fmc_cfg_keeps_unrelated_bits() -> None: + live = 0x1821 # bootrom/agent default: NOR select, ECC 8 + cfg = compose_fmc_cfg(live, EccType.BIT24) + assert cfg & 0x1 == 1 # normal mode + assert (cfg >> 1) & 0x3 == 1 # SPI NAND + assert ecc_type_of(cfg) == EccType.BIT24 + assert (cfg >> 3) & 0x3 == 0 and (cfg >> 8) & 0x3 == 0 # 2K page, 64-page block + assert cfg & 0x1800 == 0x1800 # SPI_NAND_SEL untouched + assert ecc_type_of(compose_fmc_cfg(cfg, EccType.NONE)) == EccType.NONE + + +def test_xfer_packing() -> None: + x = NandXfer(mode=XferMode.DMA, fmc_cfg=0x11823, opcode=0x6B, iftype=3, dummy=1) + assert x.pack() == struct.pack(" None: + rec = NandRecord.unpack(struct.pack(" None: + oob = kernel_oob(64) + assert len(oob) == 64 + assert oob[30:32] == b"\x00\x00" + assert oob[:30] == b"\xff" * 30 and oob[32:] == b"\xff" * 32 + + +# --- NandStudy against the fake agent ----------------------------------------- + +async def test_info(study: NandStudy) -> None: + info = await study.info() + assert (info.page_size, info.oob_size, info.pages_per_block, info.blocks) == (PAGE, OOB, PPB, BLOCKS) + assert info.stride == STRIDE and info.pages == PPB * BLOCKS and info.ecc_steps == 2 + assert (info.dma_buf, info.stat_buf, info.fmc_cfg) == (DMA_BUF, STAT_BUF, 0x1821) + + +async def test_read_page_splits_data_and_oob(study: NandStudy, agent: FakeNandAgent) -> None: + agent.pages[7] = bytes(range(256)) * 8 + b"\xab" * OOB + agent.records[7] = NandRecord(0x0000FF00, 0x20, 1, 0) + r = await study.read_page(7, NandXfer()) + assert r.data == bytes(range(256)) * 8 + assert r.oob == b"\xab" * OOB + assert r.record.uncorrectable_steps(2) == [1] + + +async def test_scan_chunks_and_keeps_order(study: NandStudy, agent: FakeNandAgent) -> None: + agent.records[130] = NandRecord(0xFF, 0, 1, 0) + result = await study.scan(0, 200, NandXfer(), chunk=64) + assert len(result.records) == 200 + assert [i for i, r in enumerate(result.records) if r.uncorrectable_steps(2)] == [130] + reads = [struct.unpack(" None: + agent.timeout_at = 70 + result = await study.scan(0, 200, NandXfer(), chunk=64) + assert len(result.records) == 71 + assert result.records[-1].failed + + +async def test_program_page_stages_exact_bytes(study: NandStudy, agent: FakeNandAgent) -> None: + payload = b"\x5a" * PAGE + kernel_oob(OOB) + rec = await study.program_page(9, payload, program_xfer(NandXfer())) + assert not rec.failed + assert agent.pages[9] == payload + with pytest.raises(ValueError, match="2112"): + await study.program_page(9, b"\x00" * PAGE, NandXfer()) + + +async def test_erase_block(study: NandStudy, agent: FakeNandAgent) -> None: + agent.pages[65] = b"\x00" * STRIDE + await study.erase_block(64) + assert 65 not in agent.pages + + +async def test_feature_and_fmc_reg(study: NandStudy, agent: FakeNandAgent) -> None: + assert await study.feature_get(FEATURE_CONFIG) == 0x18 + assert await study.feature_set(FEATURE_CONFIG, 0x08) == 0x08 + assert agent.features[0xB0] == 0x08 + assert await study.fmc_reg(0x00) == 0x1821 + assert await study.fmc_reg(0x00, 0x11823) == 0x11823 + + +async def test_errors_raise(study: NandStudy, agent: FakeNandAgent) -> None: + agent.status_override = 1 + with pytest.raises(NandError, match="bad argument"): + await study.feature_get(0xB0) + agent.status_override = None + agent.not_nand = True + with pytest.raises(NandError, match="no SPI NAND"): + await study.feature_get(0xB0) + + +async def test_unexpected_answer_raises(agent: FakeNandAgent) -> None: + class Silent(FakeNandAgent): + def _handle(self, payload: bytes) -> None: + self.enqueue_rx(build_packet(0x83, b"\x01")) # plain ACK: old agent + + study = NandStudy(FakeClient(Silent())) # type: ignore[arg-type] + with pytest.raises(NandError, match="CAP_NAND"): + await study.info() + + +async def test_stale_read_tail_is_skipped(agent: FakeNandAgent) -> None: + """A read the host abandoned keeps streaming; the next call must not + take its frames for the answer.""" + class Stale(FakeNandAgent): + first = True + + def _handle(self, payload: bytes) -> None: + if self.first: + self.first = False + self.enqueue_rx(build_packet(0x82, b"\x05\x00" + b"\xaa" * 64)) + self.enqueue_rx(build_packet(0x83, b"\x00")) + self.enqueue_rx(build_packet(0x81, b"\x00" * 28)) # late INFO answer + FakeNandAgent._handle(self, payload) + + study = NandStudy(FakeClient(Stale())) # type: ignore[arg-type] + assert (await study.info()).page_size == PAGE + + +# --- CLI ------------------------------------------------------------------------ + +@pytest.fixture +def cli(monkeypatch: pytest.MonkeyPatch, agent: FakeNandAgent) -> CliRunner: + import asyncio + + from defib.cli import nand as nand_cli + + def fake_run(port: str, body: Any) -> Any: + return asyncio.run(body(NandStudy(FakeClient(agent)))) # type: ignore[arg-type] + + monkeypatch.setattr(nand_cli, "_run", fake_run) + return CliRunner() + + +def _app() -> Any: + from defib.cli.app import app + return app + + +def test_cli_ecc_scan_lists_uncorrectable( + cli: CliRunner, agent: FakeNandAgent, tmp_path: Path, +) -> None: + agent.records[65] = NandRecord(0x0000FF00, 0, 1, 0) + agent.records[66] = NandRecord(0x00000003, 0, 1, 0) + out = tmp_path / "scan.json" + res = cli.invoke(_app(), ["agent", "nand", "ecc-scan", "--count", "128", + "--json", str(out)]) + assert res.exit_code == 0, res.output + assert "uncorrectable: 1 corrected: 1" in res.output + assert "page 65 (block 1 page 1)" in res.output + assert json.loads(out.read_text())["records"][65][0] == 0xFF00 + # Default transfer: controller page engine, 8-bit ECC on a SPI NAND config. + xfer = NandXfer(fmc_cfg=compose_fmc_cfg(0x1821, EccType.BIT8)) + assert any(a[8:16] == xfer.pack() for op, a in agent.calls if op == 0x03) + + +def test_cli_dump_switches_ondie_ecc_off_and_back( + cli: CliRunner, agent: FakeNandAgent, tmp_path: Path, +) -> None: + agent.pages[1] = b"\x11" * STRIDE + out = tmp_path / "nand.bin" + res = cli.invoke(_app(), ["agent", "nand", "dump", "-o", str(out), "--count", "3"]) + assert res.exit_code == 0, res.output + assert out.read_bytes() == b"\xff" * STRIDE + b"\x11" * STRIDE + b"\xff" * STRIDE + sidecar = json.loads((tmp_path / "nand.bin.json").read_text()) + assert sidecar["pages"] == 3 and sidecar["mode"] == "reg" + assert sidecar["features"]["config_b0"] == 0x18 + sets = [a for op, a in agent.calls if op == 0x02] + assert sets == [bytes([0xB0, 0x08]), bytes([0xB0, 0x18])] # off for the dump, then back + # Raw dump: register reads, FMC_CFG untouched. + assert all(a[8:16] == NandXfer(mode=XferMode.REG).pack() + for op, a in agent.calls if op == 0x03) + + +def test_cli_dump_incomplete_is_an_error( + cli: CliRunner, agent: FakeNandAgent, tmp_path: Path, +) -> None: + agent.timeout_at = 2 + out = tmp_path / "nand.bin" + res = cli.invoke(_app(), ["agent", "nand", "dump", "-o", str(out), "--count", "5"]) + assert res.exit_code == 1 + assert "INCOMPLETE" in res.output + assert out.read_bytes() == b"\xff" * STRIDE * 2 # the failed page is not kept + sidecar = json.loads((tmp_path / "nand.bin.json").read_text()) + assert sidecar["complete"] is False and sidecar["failed_page"] == 2 + assert sidecar["pages"] == 2 + + +def test_cli_dump_refuses_when_ondie_ecc_stays_on( + cli: CliRunner, agent: FakeNandAgent, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + original = FakeNandAgent._handle + + def sticky(self: FakeNandAgent, payload: bytes) -> None: + if payload[0] == 0x02: # FEATURE_SET ignored by the chip + self.calls.append((0x02, payload[1:])) + self._reply(0x02, 0, bytes([self.features[payload[1]]])) + return + original(self, payload) + + monkeypatch.setattr(FakeNandAgent, "_handle", sticky) + out = tmp_path / "nand.bin" + res = cli.invoke(_app(), ["agent", "nand", "dump", "-o", str(out), "--count", "2"]) + assert res.exit_code == 1 + assert "kept its on-die ECC on" in res.output + assert not any(op == 0x03 for op, _ in agent.calls) # nothing was read + + +def test_cli_write_page_kernel_oob(cli: CliRunner, agent: FakeNandAgent, tmp_path: Path) -> None: + data = tmp_path / "page.bin" + data.write_bytes(b"\x42" * PAGE) + res = cli.invoke(_app(), ["agent", "nand", "write-page", "5", "-i", str(data), + "--kernel-oob"]) + assert res.exit_code == 0, res.output + assert agent.pages[5] == b"\x42" * PAGE + kernel_oob(OOB) + res = cli.invoke(_app(), ["agent", "nand", "write-page", "5", "-i", str(data)]) + assert res.exit_code != 0 + + +def test_cobs_roundtrip_sanity() -> None: + """The fake decodes real frames, not a shortcut.""" + pkt = build_packet(CMD_NAND, b"\x00") + assert cobs.decode(pkt[:-1])[0] == CMD_NAND diff --git a/tests/test_agent_protocol.py b/tests/test_agent_protocol.py index c9bdb1f..e1d8342 100644 --- a/tests/test_agent_protocol.py +++ b/tests/test_agent_protocol.py @@ -160,6 +160,21 @@ def test_timeout_raises(self): with pytest.raises(TransportTimeout): _recv_packet_sync(port, timeout=0.1) + def test_timeout_on_blocking_port_with_silent_agent(self): + """SerialTransport opens with timeout=None, so read() blocks until a + byte arrives; a silent agent must still time out, not hang.""" + class BlockingPort(FakePort): + def read(self, size: int = 1) -> bytes: + if not self._rx and self.timeout is None: + raise AssertionError("read() on an empty blocking port never returns") + return super().read(size) + + import time + t0 = time.monotonic() + with pytest.raises(TransportTimeout): + _recv_packet_sync(BlockingPort(b""), timeout=0.2) + assert time.monotonic() - t0 < 1.0 + def test_oversized_frame_discarded(self): # Frame larger than MAX_PACKET_SIZE should be discarded huge = bytes(range(1, 256)) * 5 # >1100 bytes, no 0x00