rk27xx: add the Scheme A FTL

ftl-rk27xx.c has been four empty stubs since 2010. Fill them in with
the flash translation layer the rk2705/rk2706 original firmware uses,
called Scheme A here to tell it from the log-structured layout of later
firmware. It reads and writes that format exactly as the original
firmware does, so a device keeps working with its original firmware
after Rockbox has written to it.

- ftl-scheme-a.{c,h}: the FTL. The file opens with a description of
  the on-flash format and how the FTL works: the SYS and USER volumes,
  super-blocks and zones, the zone table, the remap log and its mirror,
  the exchange record and the write protocol, power-loss recovery, bad
  blocks. Oddities of the original firmware kept for compatibility are
  marked where they are.
- Parameters that differ between firmware builds - the zone reserve
  base, the system zone offset, the format flag - are recovered from
  the media at mount and checked against its structure; a mount that
  cannot confirm them is read-only. A mount that would have to repair
  the remap log while not allowed to write fails rather than serve
  wrong data.
- ftl-rk27xx.c: the storage glue. It finds the boot area's ID block,
  which records where SYS ends, and mounts the FTL.
- ata-nand-rk27xx.c: a drive per volume. SYS holds the original
  firmware - on a Rockbox device including the BASE.RKW that chainloads
  the bootloader - and nothing of the user's, so it is a drive only when
  the target defines HAVE_RK27XX_NAND_SYS. Capacity comes from the FTL's
  tables, not from raw block geometry.
- config.h: HAVE_STORAGE_FLUSH for the rk27xx NAND. The FTL holds up to
  three part-written pages in RAM; storage_flush() commits them at
  shutdown and ROLO.

Writing is opt-in: without FTL_ALLOW_WRITE the FTL mounts read-only and
never writes the flash, not even a repair the mount could make.

Two bugs of the original firmware are not reproduced. A write starting
before a page held part-written in RAM and running through it left two
buffers holding that page, and the older one was later programmed over
the newer data; such a write now flushes the held page first. And its
bad-block marker took two of its three metadata bytes from the stack,
which can make a retired block look like a remap-log block; the marker
is now written in full.

Tested in a host simulator on NAND images of a Samsung YP-CP3 and a
generic rk2705, against the original firmware's FTL object run under
qemu-arm: identical traces of every read, program and erase, with a
hash of the data each program writes, over mounting and reading, random
writes with every sector verified, a power cut at every flash operation
of a write, and a program or erase failure at every one.

On a generic rk2705: the read-only mount reports the layout and
capacities the original firmware does and every file's MD5 matches; a
write test passes 8192/8192 across a remount and a power cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Change-Id: I573d944389cefb456ecf38c6c7b91bebfe0fe377
This commit is contained in:
Marcin Bukat 2026-09-23 19:03:43 +02:00
parent f428c491d1
commit a0c1085f3f
7 changed files with 2460 additions and 45 deletions

View file

@ -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/ftl-scheme-a.c
target/arm/rk27xx/flash-rk27xx.c
target/arm/rk27xx/nand-rk27xx.c
target/arm/rk27xx/usb-rk27xx.c

View file

@ -893,6 +893,14 @@ Lyre prototype 1 */
/* Storage related config handling */
/* The rk27xx NAND's flash translation layer holds part-written pages in RAM
* (ftl-scheme-a.c) until a later write completes them; storage_flush()
* commits them at shutdown, ROLO and wherever else it is called. */
#if (CONFIG_STORAGE & STORAGE_NAND) && (CONFIG_NAND == NAND_RK27XX) \
&& !defined(HAVE_STORAGE_FLUSH)
#define HAVE_STORAGE_FLUSH
#endif
#if (CONFIG_STORAGE & (CONFIG_STORAGE - 1)) != 0
/* Multiple storage drivers */
#define CONFIG_STORAGE_MULTI

View file

@ -28,26 +28,29 @@
#include "ftl-target.h"
#include "nand-target.h"
uint32_t ftl_banks;
const struct nand_device_info_type* ftl_nand_type;
/* This file provides only STUBS for now */
/** static, private data **/
static bool initialized = false;
/* The NAND is FTL_NUM_DRIVES drives - USER, and SYS when the target exposes
* it. storage.c hands us a drive index relative to our first, which is the
* FTL_DRIVE_* number. */
#ifdef HAVE_MULTIDRIVE
#define NAND_DRIVE(d) (d)
#else
#define NAND_DRIVE(d) FTL_DRIVE_USER
#endif
/* API Functions */
int nand_read_sectors(IF_MD(int drive,) sector_t start, int incount,
void* inbuf)
{
(void)drive;
return ftl_read(start, incount, inbuf);
return ftl_read(NAND_DRIVE(IF_MD_DRV(drive)), start, incount, inbuf);
}
int nand_write_sectors(IF_MD(int drive,) sector_t start, int count,
const void* outbuf)
{
(void)drive;
return ftl_write(start, count, outbuf);
return ftl_write(NAND_DRIVE(IF_MD_DRV(drive)), start, count, outbuf);
}
void nand_spindown(int seconds)
@ -72,13 +75,20 @@ void nand_enable(bool on)
void nand_get_info(IF_MD(int drive,) struct storage_info *info)
{
(void)drive;
uint32_t ppb = ftl_banks * (*ftl_nand_type).pagesperblock;
int d = NAND_DRIVE(IF_MD_DRV(drive));
/* Capacity comes from the FTL's own tables, not from raw block
* geometry: Scheme A's usable size depends on the per-zone valid-block
* counts and on the system/user split recorded in ID block 1. */
(*info).sector_size = SECTOR_SIZE;
(*info).num_sectors = (*ftl_nand_type).userblocks * ppb;
(*info).vendor = "";
(*info).product = "";
(*info).revision = "";
(*info).num_sectors = ftl_get_sectors(d);
(*info).vendor = "Rockchip";
#ifdef HAVE_RK27XX_NAND_SYS
(*info).product = (d == FTL_DRIVE_SYS) ? "NAND SYS" : "NAND USER";
#else
(*info).product = "NAND USER";
#endif
(*info).revision = "1.0";
}
long nand_last_disk_activity(void)
@ -109,7 +119,11 @@ int nand_num_drives(int first_drive)
/* We don't care which logical drive number(s) we have been assigned */
(void)first_drive;
#ifdef HAVE_MULTIDRIVE
return FTL_NUM_DRIVES;
#else
return 1;
#endif
}
#endif

View file

@ -7,6 +7,7 @@
* \/ \/ \/ \/ \/
*
* Copyright (C) 2010 by Bertrik Sikken
* 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
@ -17,39 +18,190 @@
* KIND, either express or implied.
*
****************************************************************************/
#include "config.h"
#include "ftl-target.h"
/* this file provides empty STUBS for now */
/* Rockbox storage on the rk27xx Scheme A FTL (ftl-scheme-a.c).
*
* Writing is opt-in: a build without FTL_ALLOW_WRITE mounts read-only and
* never writes the flash, not even the repairs a mount can make. */
#include "config.h"
#include "system.h"
#include "ftl-target.h"
#include "nand-target.h"
#include "flash-rk27xx.h"
#include "ftl-scheme-a.h"
/* The boot ROM looks for ID blocks at every 512th raw sector of the boot
* area, up to 50 positions, by metadata type 0x69. */
#define IDB_STRIDE 512
#define IDB_POSITIONS 50
#define IDB_META_TYPE 2
#define IDB_TYPE 0x69
#define IDB_MAX_BOOT_BLOCKS 64
static bool ftl_mounted = false;
/* The size of the SYS volume, from ID block 1.
*
* ID block 0 is scrambled; ID block 1, the sector after it, is plain:
*
* +0 uint16_t LE blocks of bootloader
* +2 uint16_t LE SYS volume size, MB
*
* A sector marked 0x69 whose ID block 1 gives a sane block count and a SYS
* volume smaller than the chip is taken; descrambling ID block 0 to check its
* signature would buy little over that. Returns 0 if none is found. */
static uint32_t idb_sys_sectors(void)
{
const struct flash_geometry *geo = flash_get_geometry();
uint8_t data[FLASH_SECTOR_SIZE], meta[FLASH_META_SIZE];
uint32_t sectors = 0;
uint32_t pos;
for (pos = 0; pos < IDB_POSITIONS && sectors == 0; pos++)
{
uint32_t raw = pos * IDB_STRIDE;
uint32_t blocks, mb;
if (raw + 1 >= geo->total_sectors)
{
break;
}
if (flash_read_raw(raw, data, meta) != 0
|| meta[IDB_META_TYPE] != IDB_TYPE)
{
continue;
}
if (flash_read_raw(raw + 1, data, meta) != 0)
{
continue;
}
blocks = data[0] | (data[1] << 8);
mb = data[2] | (data[3] << 8);
if (blocks > 0 && blocks <= IDB_MAX_BOOT_BLOCKS && mb > 0 &&
mb * 2048 < geo->total_sectors)
{
sectors = mb * 2048;
}
}
return sectors;
}
uint32_t ftl_init(void)
{
/* TODO implement */
return 0;
struct ftl_a_config config;
uint32_t ret = 0;
flash_init();
if (flash_layer_init() != 0)
{
ret = 1;
}
else
{
config.sys_sectors = idb_sys_sectors();
#ifdef FTL_ALLOW_WRITE
config.read_only = false;
/* the rk2705 NAND bootloader's generation formats with flag 1; its
* write logic is the same as the standard one's */
config.alt_format_flag = 1;
config.alt_format_writable = true;
#else
config.read_only = true;
config.alt_format_flag = 1;
config.alt_format_writable = false;
#endif
if (config.sys_sectors == 0)
{
ret = 2; /* without it USER cannot be told from SYS */
}
else if (ftl_a_mount(&config) != FTL_A_OK)
{
ret = 3;
}
else if (ftl_a_capacity(FTL_A_VOL_USER) == 0)
{
ret = 4;
}
else
{
ftl_mounted = true;
}
}
return ret;
}
uint32_t ftl_read(uint32_t sector, uint32_t count, void* buffer)
/* The FTL volume behind a drive. SYS is reachable only when exposed. */
static int ftl_volume(int drive)
{
/* TODO implement */
(void)sector;
(void)count;
(void)buffer;
return 0;
int volume = FTL_A_VOL_USER;
#ifdef HAVE_RK27XX_NAND_SYS
if (drive == FTL_DRIVE_SYS)
{
volume = FTL_A_VOL_SYS;
}
#else
(void)drive;
#endif
return volume;
}
uint32_t ftl_write(uint32_t sector, uint32_t count, const void* buffer)
static bool drive_valid(int drive)
{
/* TODO implement */
(void)sector;
(void)count;
(void)buffer;
return 0;
return ftl_mounted && drive >= 0 && drive < FTL_NUM_DRIVES;
}
uint32_t ftl_get_sectors(int drive)
{
uint32_t sectors = 0;
if (drive_valid(drive))
{
sectors = ftl_a_capacity(ftl_volume(drive));
}
return sectors;
}
uint32_t ftl_read(int drive, uint32_t sector, uint32_t count, void *buffer)
{
uint32_t ret = 1;
if (drive_valid(drive))
{
ret = ftl_a_read(ftl_volume(drive), sector, buffer, count) ? 2 : 0;
}
return ret;
}
uint32_t ftl_write(int drive, uint32_t sector, uint32_t count,
const void *buffer)
{
uint32_t ret = 1;
#ifdef FTL_ALLOW_WRITE
if (drive_valid(drive))
{
ret = ftl_a_write(ftl_volume(drive), sector, buffer, count) ? 2 : 0;
}
#else
/* refuse rather than pretend: a silent success would let the filesystem
* believe data was committed */
(void)drive; (void)sector; (void)count; (void)buffer;
#endif
return ret;
}
uint32_t ftl_sync(void)
{
/* TODO implement */
if (ftl_mounted)
{
ftl_a_sync();
}
return 0;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,99 @@
/***************************************************************************
* __________ __ ___.
* 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.
*
****************************************************************************/
/* The "Scheme A" flash translation layer of rk27xx devices - the on-flash
* format the original firmware of many rk2705/rk2706 players uses, read and
* written compatibly so that the original firmware keeps working on the same
* media. See ftl-scheme-a.c for how it works. */
#ifndef __FTL_SCHEME_A_H__
#define __FTL_SCHEME_A_H__
#include <stdbool.h>
#include <stdint.h>
/* The two volumes. SYS holds the original firmware and its resources; USER
* is the music storage. */
#define FTL_A_VOL_SYS 0
#define FTL_A_VOL_USER 1
struct ftl_a_config
{
/* Size of the SYS volume. The one layout value the FTL cannot read from
* its own tables: it is recorded in the boot area's ID block. */
uint32_t sys_sectors;
/* Never write, not even the repairs a mount normally makes. */
bool read_only;
/* A format flag accepted besides the standard 2 (0 = none), and whether
* media carrying it may be written. See ftl_a_mount(). */
uint8_t alt_format_flag;
bool alt_format_writable;
};
/* Why a mount failed */
enum ftl_a_error
{
FTL_A_OK = 0,
FTL_A_ERR_LAYOUT, /* the zone layout could not be recognised */
FTL_A_ERR_NO_LOG_BLOCK, /* a zone has no remap log: damaged */
FTL_A_ERR_ZERO_CAPACITY, /* the block counts never loaded */
FTL_A_ERR_COUNTS_ERASED, /* ... or were read from an erased page */
FTL_A_ERR_SYS_TOO_BIG, /* SYS larger than the chip */
FTL_A_ERR_FORMAT_FLAG, /* not formatted, or an unknown generation */
FTL_A_ERR_NEEDS_REPAIR, /* a log needs rewriting; not while read-only */
};
/* How sure the mount is of the layout it recognised */
enum ftl_a_confidence
{
FTL_A_CONF_NONE = 0,
FTL_A_CONF_CONFIRMED, /* derived and cross-checked: writable */
FTL_A_CONF_ASSUMED, /* partly assumed: read-only */
};
struct ftl_a_status
{
enum ftl_a_error error;
enum ftl_a_confidence confidence;
bool writable;
uint8_t reserve_base; /* first reserve-pool entry of a zone table */
uint8_t sys_zone; /* zone holding the boot area */
uint8_t sys_offset; /* boot-area blocks at the start of it */
uint8_t base_fits; /* reserve bases that fit the media; 1 = sure */
uint8_t format_flag; /* as read from the media */
};
/* Mount the media. Returns FTL_A_OK or the reason it failed. Even a
* successful mount may be read-only - see ftl_a_get_status(). */
enum ftl_a_error ftl_a_mount(const struct ftl_a_config *config);
void ftl_a_get_status(struct ftl_a_status *status);
/* Sectors on a volume; 0 when not mounted */
uint32_t ftl_a_capacity(int volume);
/* Return 0, or non-zero if the request was refused or a sector could not be
* read reliably. Sectors are 512 bytes. */
int ftl_a_read(int volume, uint32_t sector, void *buf, uint32_t count);
int ftl_a_write(int volume, uint32_t sector, const void *buf, uint32_t count);
/* Write out everything still held in RAM */
void ftl_a_sync(void);
#endif /* __FTL_SCHEME_A_H__ */

View file

@ -24,21 +24,26 @@
#include "config.h"
#include "inttypes.h"
#ifdef BOOTLOADER
/* Bootloaders don't need write access */
#define FTL_READONLY
/* The drives the NAND presents: USER, and SYS only when the target exposes
* it - see HAVE_RK27XX_NAND_SYS in the target config. */
#ifdef HAVE_RK27XX_NAND_SYS
#define FTL_DRIVE_SYS 0
#define FTL_DRIVE_USER 1
#define FTL_NUM_DRIVES 2
#else
#define FTL_DRIVE_USER 0
#define FTL_NUM_DRIVES 1
#endif
/* Pointer to an info structure regarding the flash type used */
extern const struct nand_device_info_type* ftl_nand_type;
/* Number of banks we detected a chip on */
extern uint32_t ftl_banks;
uint32_t ftl_init(void);
uint32_t ftl_read(uint32_t sector, uint32_t count, void* buffer);
uint32_t ftl_write(uint32_t sector, uint32_t count, const void* buffer);
uint32_t ftl_read(int drive, uint32_t sector, uint32_t count, void* buffer);
uint32_t ftl_write(int drive, uint32_t sector, uint32_t count,
const void* buffer);
uint32_t ftl_sync(void);
/* Usable sectors on a logical disk, 0 if not mounted. Comes from the FTL's
* own tables, not from raw block geometry - see ftl-rk27xx.c. */
uint32_t ftl_get_sectors(int drive);
#endif