From 455b1bcbf8349425d7971b5bf87e791135da06af Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 10:16:34 +0200 Subject: [PATCH] rk27xx: add the NAND flash layer flash-rk27xx.c drives the NAND controller and its BCH engine for the flash translation layer that follows. It knows the chip's geometry and how to read, program, erase and copy sectors, but not what the sectors mean. - Addresses are 512-byte sectors in a linear view of the chip where a block is a super-block: on a two-plane part, one physical block from each plane, consecutive pages alternating between them. The layer maps that view to the chip; the FTL never sees planes. - Every sector carries 16 spare bytes: 13 of BCH code, generated and checked by the hardware, and 3 for the FTL. Byte 1 is written 0x00 on every program - the "page programmed" marker the original firmware's FTL keys its mount and recovery on. - The program sequence was read out of the original firmware's own machine code. The write kick is the read kick plus FL_WR, and the BCH engine needs BCH_WR to encode rather than decode. - A copy is read through the ECC engine and programmed, never the chip's internal data move, which on this MLC part would carry bit errors forward. - Writes are refused until the FTL enables them, and writes into the boot area are dropped and reported successful, as the original firmware does. The write-protect line is lifted only for the duration of each program or erase. - Failures and timeouts are counted: the FTL can act on few of them. Only the first chip is handled; every device the FTL has been checked on has one. Tested on a generic rk2705 directly, on a free block: a program across both planes, a single-sector program with metadata, a whole page, a copy and an erase each read back byte-exact through the controller's ECC decode, which also shows the code it generated is valid. Co-Authored-By: Claude Opus 5.5 Change-Id: If6ffa9f9811172f4f8829c1e68d6de9466a022c8 --- firmware/SOURCES | 1 + firmware/target/arm/rk27xx/flash-rk27xx.c | 617 ++++++++++++++++++++++ firmware/target/arm/rk27xx/flash-rk27xx.h | 118 +++++ 3 files changed, 736 insertions(+) create mode 100644 firmware/target/arm/rk27xx/flash-rk27xx.c create mode 100644 firmware/target/arm/rk27xx/flash-rk27xx.h diff --git a/firmware/SOURCES b/firmware/SOURCES index 0664e5788a..81e4459bea 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1711,6 +1711,7 @@ target/arm/rk27xx/backlight-rk27xx.c target/arm/rk27xx/adc-rk27xx.c target/arm/rk27xx/sd-rk27xx.c target/arm/rk27xx/ftl-rk27xx.c +target/arm/rk27xx/flash-rk27xx.c target/arm/rk27xx/nand-rk27xx.c target/arm/rk27xx/usb-rk27xx.c target/arm/rk27xx/lcdif-rk27xx.c diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c new file mode 100644 index 0000000000..c419efcc9f --- /dev/null +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -0,0 +1,617 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* NAND access for the rk27xx flash translation layer - see flash-rk27xx.h. + * + * THE CONTROLLER + * + * Commands and addresses are written to the chip through FLASH_CMD/ADDR, one + * cycle per write. Data moves a sector at a time between the chip and one of + * four controller slots - 512 bytes of PAGE_BUF and 16 of SPARE_BUF each - + * with the BCH engine in the path: on a read it corrects the sector and + * reports the result in BCHST, on a program it computes the code into the + * last 13 spare bytes. A transfer is started with FLCTL; the slot is chosen + * by bits 3-4. + * + * A read latches a whole page in the chip, then streams its sectors out in + * order from the first: reaching sector k of a page means transferring the + * k before it too. + * + * A program sends the column, the sectors, then the confirm command, and + * reads the chip's status: one program operation per page between erases, + * as this MLC part allows no more (NOP = 1). + * + * The chip's write-protect line is lifted only for the duration of each + * program or erase. */ + +#include "config.h" +#include "system.h" +#include "string.h" +#include "rk27xx.h" +#include "nand-target.h" +#include "flash-rk27xx.h" + +#define CMD_READ_1ST 0x00 +#define CMD_READ_2ND 0x30 +#define CMD_PROG_1ST 0x80 +#define CMD_PROG_2ND 0x10 +#define CMD_ERASE_1ST 0x60 +#define CMD_ERASE_2ND 0xD0 +#define CMD_STATUS 0x70 +#define NAND_STATUS_FAIL 0x01 + +#define SPARE_SIZE 16 /* spare bytes per sector on the chip */ +#define SECTOR_STRIDE (FLASH_SECTOR_SIZE + SPARE_SIZE) + +/* Metadata byte 1 of every programmed sector: "programmed" */ +#define META_PROGRAMMED 1 + +/* Transfer one sector between the chip and a controller slot: ECC on, + * transfer on, bit 5 (region select), start. A write adds FL_WR, and the + * BCH engine then encodes (BCH_WR) rather than decodes. */ +#define FL_KICK_READ (FL_COR_EN | FL_XFER_EN | (1<<5) | FL_START) +#define FL_KICK_WRITE (FL_KICK_READ | FL_WR) + +/* Busy limits: the datasheet maxima (tPROG 2.2 ms, tBERS 10 ms) with margin */ +#define PROG_TIMEOUT_US 5000 +#define ERASE_TIMEOUT_US 20000 + +static struct flash_geometry geo; +static bool ready; +static bool writable; +static uint32_t boot_area = UINT32_MAX; +static struct flash_stats stats; + +int flash_layer_init(void) +{ + const struct flashspec_t *f = &flash_spec[0]; + int ret = 0; + + ready = false; + + if (f->total_phy_sec == 0 || f->sec_per_page_raw == 0 || + f->sec_per_block_raw == 0) + { + ret = 1; + } + /* the plane interleave below knows one plane or two */ + else if (f->mul_plane != 1 && f->mul_plane != 2) + { + ret = 2; + } + else if (f->sec_per_page_raw * f->mul_plane > FLASH_MAX_SEC_PER_PAGE) + { + ret = 3; + } + else + { + geo.planes = f->mul_plane; + geo.sec_per_page_raw = f->sec_per_page_raw; + geo.sec_per_page = f->sec_per_page; + geo.sec_per_block_raw = f->sec_per_block_raw; + geo.sec_per_block = f->sec_per_block; + geo.total_blocks = f->total_bloks; + geo.total_sectors = f->total_phy_sec; + ready = true; + } + return ret; +} + +const struct flash_geometry *flash_get_geometry(void) +{ + return &geo; +} + +void flash_set_writable(bool on) +{ + writable = on; +} + +void flash_set_boot_area(uint32_t sectors) +{ + boot_area = sectors; +} + +void flash_get_stats(struct flash_stats *out) +{ + *out = stats; +} + +/* Map an FTL sector to a raw sector on the chip. + * + * On a two-plane part a super-block is a pair of physical blocks, and + * consecutive pages alternate between them: + * + * super-block sec / (spb_raw * 2) + * within it page = L / (spp_raw * 2) + * plane = (L % (spp_raw * 2)) / spp_raw + * slot = L % spp_raw + * raw = (super_block * 2 + plane) * spb_raw + page * spp_raw + slot + * + * On a single-plane part it is the identity. */ +static uint32_t sec_to_raw(uint32_t sec) +{ + uint32_t raw = sec; + + if (geo.planes == 2) + { + uint32_t spb = (uint32_t)geo.sec_per_block_raw * 2; + uint32_t sb = sec / spb; + uint32_t l = sec % spb; + uint32_t page = l / ((uint32_t)geo.sec_per_page_raw * 2); + uint32_t plane = (l % ((uint32_t)geo.sec_per_page_raw * 2)) + / geo.sec_per_page_raw; + uint32_t slot = l % geo.sec_per_page_raw; + + raw = (sb * 2 + plane) * geo.sec_per_block_raw + + page * geo.sec_per_page_raw + slot; + } + return raw; +} + +static void wait_flash_ready(void) +{ + while (!(FMCTL & FM_RDY)) + { + } +} + +/* R/B# drops within tWB (100 ns) of a confirm command, so wait that long + * before polling, or a still-high line reads as done. */ +static bool wait_ready_us(uint32_t us) +{ + bool timeout = false; + + udelay(1); + while (!(FMCTL & FM_RDY) && !timeout) + { + if (us-- == 0) + { + stats.timeouts++; + timeout = true; + } + else + { + udelay(1); + } + } + return timeout; +} + +static void send_row(uint32_t row) +{ + FLASH_ADDR(0) = row & 0xff; + FLASH_ADDR(0) = (row >> 8) & 0xff; + FLASH_ADDR(0) = (row >> 16) & 0xff; +} + +/* Latch raw page `row` in the chip and prepare the ECC engine to decode. */ +static void latch_page(uint32_t row) +{ + flash_chip_select(0); + wait_flash_ready(); + + FLASH_CMD(0) = CMD_READ_1ST; + FLASH_ADDR(0) = 0x00; + FLASH_ADDR(0) = 0x00; + send_row(row); + FLASH_CMD(0) = CMD_READ_2ND; + + wait_flash_ready(); + + /* ECC on, t=8 - right for the whole chip on the devices seen */ + BCHCTL = BCH_RST; +} + +/* Transfer the next sector of the latched page through slot `slot` & 3. + * Returns the BCH status. */ +static uint32_t read_next_sector(uint32_t slot, uint8_t *data, uint8_t *meta) +{ + uint32_t st; + uint32_t buf = slot & 3; + + FLCTL = FL_KICK_READ | (buf << 3); + while (!(FLCTL & FL_RDY)) + { + } + + st = BCHST; + + if (data) + { + memcpy(data, (const void *)((uintptr_t)&PAGE_BUF + (buf << 9)), + FLASH_SECTOR_SIZE); + } + if (meta) + { + memcpy(meta, (const void *)((uintptr_t)&SPARE_BUF + (buf << 4)), + FLASH_META_SIZE); + } + return st; +} + +/* Read sectors [first, first + n) of raw page `row`. Returns 1 if any was + * uncorrectable. */ +static int read_raw_run(uint32_t row, uint32_t first, uint32_t n, + uint8_t *data, uint8_t *meta) +{ + uint32_t j; + int uncorrectable = 0; + + latch_page(row); + + for (j = 0; j < first + n; j++) + { + bool wanted = j >= first; + uint32_t k = j - first; + uint8_t *d = (wanted && data) ? data + (size_t)k * FLASH_SECTOR_SIZE + : NULL; + uint8_t *m = (wanted && meta) ? meta + (size_t)k * FLASH_META_SIZE + : NULL; + uint32_t st = read_next_sector(j, d, m); + + if (wanted) + { + if (BCH_UNCORRECTABLE(st)) + { + uncorrectable = 1; + } + else if (BCH_CORRECTED(st) >= BCH_REFRESH_THRESHOLD) + { + stats.refresh_pending++; + } + } + } + + flash_chip_deselect(); + return uncorrectable; +} + +int flash_read_raw(uint32_t raw_sec, void *data, void *meta) +{ + int ret = 1; + + if (ready && raw_sec < geo.total_sectors) + { + ret = read_raw_run(raw_sec / geo.sec_per_page_raw, + raw_sec % geo.sec_per_page_raw, 1, data, meta); + } + return ret; +} + +int flash_read(uint32_t sec, void *data, void *meta, unsigned n) +{ + uint8_t *d = data; + uint8_t *m = meta; + unsigned i; + int ret = ready ? 0 : 1; + + /* One page latch per sector. Reading whole runs in one pass is an + * obvious speed-up, left until the access pattern has been measured. */ + for (i = 0; i < n && ready; i++) + { + uint32_t raw = sec_to_raw(sec + i); + + if (raw >= geo.total_sectors) + { + ret = 1; + break; + } + + if (read_raw_run(raw / geo.sec_per_page_raw, + raw % geo.sec_per_page_raw, 1, + d ? d + (size_t)i * FLASH_SECTOR_SIZE : NULL, + m ? m + (size_t)i * FLASH_META_SIZE : NULL)) + { + ret = 1; + } + } + return ret; +} + +/* ---- writing ---- */ + +static bool write_refused(void) +{ + bool refused = !ready || !writable; + + if (refused) + { + stats.write_refusals++; + } + return refused; +} + +static uint8_t read_status(void) +{ + FLASH_CMD(0) = CMD_STATUS; + return FLASH_DATA(0); +} + +/* The page and spare buffers are written a word at a time; byte stores are + * not known to work. src may be unaligned, or NULL for erased content. */ +static void put_words(uintptr_t dst, const uint8_t *src, uint32_t len) +{ + volatile uint32_t *d = (volatile uint32_t *)dst; + uint32_t i; + + for (i = 0; i < len / 4; i++) + { + if (src == NULL) + { + d[i] = 0xffffffff; + } + else + { + d[i] = src[4*i] | (src[4*i + 1] << 8) | + (src[4*i + 2] << 16) | ((uint32_t)src[4*i + 3] << 24); + } + } +} + +/* Program `n` sectors of raw page `row`, from sector `first` in it. */ +static int prog_raw_run(uint32_t row, uint32_t first, uint32_t n, + const uint8_t *data, const uint8_t *meta) +{ + uint32_t col = first * SECTOR_STRIDE; + uint32_t i; + int fail; + + flash_chip_select(0); + FMCTL |= FM_PROTECT; /* lift WP# for this operation */ + wait_flash_ready(); + + FLASH_CMD(0) = CMD_PROG_1ST; + FLASH_ADDR(0) = col & 0xff; + FLASH_ADDR(0) = (col >> 8) & 0xff; + send_row(row); + + for (i = 0; i < n; i++) + { + uint32_t buf = i & 3; + uint8_t spare[SPARE_SIZE]; + + memset(spare, 0xff, sizeof(spare)); + if (meta) + { + memcpy(spare, meta + i * FLASH_META_SIZE, FLASH_META_SIZE); + } + spare[META_PROGRAMMED] = 0x00; + + /* a slot is reused every four sectors: its last transfer must be + * done */ + while (!(FLCTL & FL_RDY)) + { + } + + put_words((uintptr_t)&PAGE_BUF + (buf << 9), + data ? data + (size_t)i * FLASH_SECTOR_SIZE : NULL, + FLASH_SECTOR_SIZE); + put_words((uintptr_t)&SPARE_BUF + (buf << 4), spare, SPARE_SIZE); + + BCHCTL = BCH_WR | BCH_RST; + FLCTL = FL_KICK_WRITE | (buf << 3); + } + + while (!(FLCTL & FL_RDY)) + { + } + + FLASH_CMD(0) = CMD_PROG_2ND; + fail = wait_ready_us(PROG_TIMEOUT_US) || (read_status() & NAND_STATUS_FAIL); + + flash_chip_deselect(); + FMCTL &= ~FM_PROTECT; + + if (fail) + { + stats.prog_failures++; + } + return fail ? 1 : 0; +} + +static int erase_raw_block(uint32_t row) +{ + int fail; + + flash_chip_select(0); + FMCTL |= FM_PROTECT; + wait_flash_ready(); + + FLASH_CMD(0) = CMD_ERASE_1ST; + send_row(row); + FLASH_CMD(0) = CMD_ERASE_2ND; + + fail = wait_ready_us(ERASE_TIMEOUT_US) + || (read_status() & NAND_STATUS_FAIL); + + flash_chip_deselect(); + FMCTL &= ~FM_PROTECT; + + if (fail) + { + stats.erase_failures++; + } + return fail ? 1 : 0; +} + +/* Sectors of the FTL's view are split into one program per raw page - and + * so per plane. */ +int flash_program(uint32_t sec, const void *data, const void *meta, unsigned n) +{ + const uint8_t *d = data; + const uint8_t *m = meta; + uint32_t i = 0; + int ret = 0; + + if (n == 0) + { + ret = 0; + } + else if (write_refused()) + { + ret = 1; + } + else if (sec < boot_area) + { + stats.boot_area_skips++; + } + else + { + while (i < n && ret == 0) + { + uint32_t raw = sec_to_raw(sec + i); + uint32_t row = raw / geo.sec_per_page_raw; + uint32_t first = raw % geo.sec_per_page_raw; + uint32_t run = 1; + + if (raw >= geo.total_sectors) + { + ret = 1; + break; + } + + /* extend the run while the next sector is the next slot of the + * same raw page */ + while (i + run < n && first + run < geo.sec_per_page_raw && + sec_to_raw(sec + i + run) == raw + run) + { + run++; + } + + if (prog_raw_run(row, first, run, + d ? d + (size_t)i * FLASH_SECTOR_SIZE : NULL, + m ? m + (size_t)i * FLASH_META_SIZE : NULL)) + { + ret = 1; + } + i += run; + } + } + return ret; +} + +int flash_program_page(uint32_t sec, const void *data, const void *meta) +{ + return flash_program(sec & ~(uint32_t)(geo.sec_per_page - 1), data, meta, + geo.sec_per_page); +} + +int flash_erase(uint32_t sec) +{ + uint32_t base = sec - sec % geo.sec_per_block; + uint32_t p; + int ret = 0; + + if (write_refused()) + { + ret = 1; + } + else if (sec < boot_area) + { + stats.boot_area_skips++; + } + else if (sec_to_raw(base) >= geo.total_sectors) + { + ret = 1; + } + else + { + for (p = 0; p < geo.planes; p++) + { + uint32_t raw = sec_to_raw(base + p * geo.sec_per_page_raw); + + if (erase_raw_block(raw / geo.sec_per_page_raw)) + { + ret = 1; + } + } + } + return ret; +} + +/* Copy through the ECC engine, never with the chip's internal data move: on + * this MLC part moved pages accumulate bit errors, and the data area's + * t=8 is already below the chip's 12-bit minimum. A source sector that + * fails ECC is copied as read, and counted. */ +int flash_copy(uint32_t src, uint32_t dst, unsigned n) +{ + static uint8_t buf[FLASH_MAX_SEC_PER_PAGE * FLASH_SECTOR_SIZE] + __attribute__((aligned(4))); + uint32_t k = 0; + int ret = 0; + + if (n == 0 || n > geo.sec_per_block) + { + ret = 0; + } + else if (write_refused()) + { + ret = 1; + } + else + { + while (k < n && ret == 0) + { + /* one destination raw page at a time */ + uint32_t len = geo.sec_per_page_raw + - (dst + k) % geo.sec_per_page_raw; + uint32_t i = 0; + + if (len > n - k) + { + len = n - k; + } + + /* the source may straddle raw pages if its offset differs */ + while (i < len && ret == 0) + { + uint32_t raw = sec_to_raw(src + k + i); + uint32_t first = raw % geo.sec_per_page_raw; + uint32_t run = 1; + + if (raw >= geo.total_sectors) + { + ret = 1; + break; + } + while (i + run < len && first + run < geo.sec_per_page_raw && + sec_to_raw(src + k + i + run) == raw + run) + { + run++; + } + + if (read_raw_run(raw / geo.sec_per_page_raw, first, run, + buf + (size_t)i * FLASH_SECTOR_SIZE, NULL)) + { + stats.copy_uncorrectable++; + } + i += run; + } + + if (ret == 0 && flash_program(dst + k, buf, NULL, len)) + { + ret = 1; + } + k += len; + } + } + return ret; +} diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.h b/firmware/target/arm/rk27xx/flash-rk27xx.h new file mode 100644 index 0000000000..952d537ffc --- /dev/null +++ b/firmware/target/arm/rk27xx/flash-rk27xx.h @@ -0,0 +1,118 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* NAND access for the rk27xx flash translation layer. + * + * This layer drives the NAND controller and its BCH engine and nothing else: + * it knows the chip's geometry and how to read, program, erase and copy + * sectors, but not what the sectors mean. The FTL (ftl-scheme-a.c) is built + * on it. + * + * ADDRESSES are sector numbers (512 bytes) in the FTL's linear view of the + * chip, where a block is a SUPER-BLOCK: on a two-plane part, one physical + * block from each plane, with consecutive pages alternating between the + * planes. This layer maps that view to the chip; the FTL never sees planes. + * + * METADATA: every sector carries 16 spare bytes. The last 13 are the BCH + * code and belong to the hardware; the first three are the FTL's, and are + * what the meta arguments below carry - FLASH_META_SIZE bytes per sector. + * Byte 1 is special: this layer writes 0x00 there on every program, so that + * the FTL can tell a programmed page from an erased one whatever it wrote. + * + * Only the first chip is supported; every device the FTL has been validated + * on has one. */ + +#ifndef __FLASH_RK27XX_H__ +#define __FLASH_RK27XX_H__ + +#include +#include + +#define FLASH_SECTOR_SIZE 512 +#define FLASH_META_SIZE 3 /* FTL metadata bytes per sector */ + +/* The largest page the layer handles: 8 sectors per plane, two planes. */ +#define FLASH_MAX_SEC_PER_PAGE 16 + +struct flash_geometry +{ + uint8_t planes; /* 1 or 2 */ + uint8_t sec_per_page_raw; /* sectors in one physical page */ + uint8_t sec_per_page; /* sectors in one page of every plane */ + uint16_t sec_per_block_raw; /* sectors in one physical block */ + uint16_t sec_per_block; /* sectors in one super-block */ + uint32_t total_blocks; /* super-blocks on the chip */ + uint32_t total_sectors; +}; + +/* Write-path error counters. Most write results do not change what the FTL + * does next - it has no way to recover from most of them - so these are the + * record that something went wrong. */ +struct flash_stats +{ + uint32_t prog_failures; /* programs the chip reported failed */ + uint32_t erase_failures; + uint32_t timeouts; /* the chip never became ready */ + uint32_t boot_area_skips; /* writes into the boot area, dropped */ + uint32_t write_refusals; /* writes attempted while not writable */ + uint32_t copy_uncorrectable;/* copy sources failing ECC, copied anyway */ + uint32_t refresh_pending; /* reads near the ECC limit: blocks decaying */ +}; + +/* Take the geometry flash_init() detected. Returns 0 if the chip is usable. */ +int flash_layer_init(void); +const struct flash_geometry *flash_get_geometry(void); + +/* Read n sectors from sec. data (n * 512 bytes) and meta (n * 3 bytes) may + * each be NULL. Returns 0, or 1 if any sector was uncorrectable - its data + * is then delivered as read. */ +int flash_read(uint32_t sec, void *data, void *meta, unsigned n); + +/* Program n sectors from sec, which must be erased. data NULL programs + * 0xff; meta NULL programs {0xff, 0x00, 0xff}. Returns 0 or 1 on failure. */ +int flash_program(uint32_t sec, const void *data, const void *meta, unsigned n); + +/* Program the whole page (every plane) that contains sec. */ +int flash_program_page(uint32_t sec, const void *data, const void *meta); + +/* Erase the super-block containing sec. */ +int flash_erase(uint32_t sec); + +/* Copy n sectors (at most one super-block) from src to dst through the ECC + * engine, so that correctable errors are not propagated. The destination + * gets fresh metadata {0xff, 0x00, 0xff}. */ +int flash_copy(uint32_t src, uint32_t dst, unsigned n); + +/* Read one PHYSICAL sector, bypassing the super-block view: for the boot + * area, which the boot ROM addresses physically. */ +int flash_read_raw(uint32_t raw_sec, void *data, void *meta); + +/* Writes - program, erase and copy - are refused (return 1, counted) until + * the FTL has confirmed it understands the media and enables them. */ +void flash_set_writable(bool writable); + +/* The first `sectors` sectors hold the boot area. A write there is dropped + * and reported successful, as the original firmware does: no FTL error may + * reach the bootloader. UINT32_MAX protects the whole chip. */ +void flash_set_boot_area(uint32_t sectors); + +void flash_get_stats(struct flash_stats *stats); + +#endif /* __FLASH_RK27XX_H__ */