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__ */