2
0
mirror of https://github.com/m-labs/artiq.git synced 2024-12-25 11:18:27 +08:00

firmware: port flash storage management to Rust.

This commit is contained in:
whitequark 2017-02-02 02:51:13 +00:00
parent 0df8a24135
commit 3b54736c02
9 changed files with 342 additions and 403 deletions

View File

@ -16,6 +16,9 @@ pub mod uart;
#[cfg(feature = "uart_console")]
pub mod uart_console;
#[cfg(has_spiflash)]
pub mod spiflash;
#[cfg(has_i2c)]
pub mod i2c;
#[cfg(has_i2c)]

View File

@ -0,0 +1,123 @@
#![allow(dead_code)]
use core::cmp;
use csr;
const CMD_PP: u8 = 0x02;
const CMD_WRDI: u8 = 0x04;
const CMD_RDSR: u8 = 0x05;
const CMD_WREN: u8 = 0x06;
const CMD_SE: u8 = 0xd8;
const PIN_CLK: u8 = 1 << 1;
const PIN_CS_N: u8 = 1 << 2;
const PIN_DQ_I: u8 = 1 << 3;
const SR_WIP: u8 = 1;
fn write_byte(mut byte: u8) {
unsafe {
csr::spiflash::bitbang_write(0);
for _ in 0..8 {
csr::spiflash::bitbang_write((byte & 0x80) >> 7);
csr::spiflash::bitbang_write((byte & 0x80) >> 7 | PIN_CLK);
byte <<= 1;
}
csr::spiflash::bitbang_write(0);
}
}
fn write_addr(mut addr: usize) {
unsafe {
csr::spiflash::bitbang_write(0);
for _ in 0..24 {
csr::spiflash::bitbang_write(((addr & 0x800000) >> 23) as u8);
csr::spiflash::bitbang_write(((addr & 0x800000) >> 23) as u8 | PIN_CLK);
addr <<= 1;
}
csr::spiflash::bitbang_write(0);
}
}
fn wait_until_ready() {
unsafe {
loop {
let mut sr = 0;
write_byte(CMD_RDSR);
for _ in 0..8 {
sr <<= 1;
csr::spiflash::bitbang_write(PIN_DQ_I | PIN_CLK);
sr |= csr::spiflash::miso_read();
csr::spiflash::bitbang_write(PIN_DQ_I);
}
csr::spiflash::bitbang_write(0);
csr::spiflash::bitbang_write(PIN_CS_N);
if sr & SR_WIP == 0 {
return
}
}
}
}
pub fn erase_sector(addr: usize) {
unsafe {
let sector_addr = addr & !(csr::CONFIG_SPIFLASH_SECTOR_SIZE as usize - 1);
csr::spiflash::bitbang_en_write(1);
wait_until_ready();
write_byte(CMD_WREN);
csr::spiflash::bitbang_write(PIN_CS_N);
write_byte(CMD_SE);
write_addr(sector_addr);
csr::spiflash::bitbang_write(PIN_CS_N);
wait_until_ready();
csr::spiflash::bitbang_en_write(0);
}
}
fn write_page(addr: usize, data: &[u8]) {
unsafe {
csr::spiflash::bitbang_en_write(1);
wait_until_ready();
write_byte(CMD_WREN);
csr::spiflash::bitbang_write(PIN_CS_N);
write_byte(CMD_PP);
write_addr(addr);
for &byte in data {
write_byte(byte)
}
csr::spiflash::bitbang_write(PIN_CS_N);
csr::spiflash::bitbang_write(0);
wait_until_ready();
csr::spiflash::bitbang_en_write(0);
}
}
const PAGE_SIZE: usize = csr::CONFIG_SPIFLASH_PAGE_SIZE as usize;
const PAGE_MASK: usize = PAGE_SIZE - 1;
pub fn write(mut addr: usize, mut data: &[u8]) {
if addr & PAGE_MASK != 0 {
let size = cmp::min((PAGE_SIZE - (addr & PAGE_MASK)) as usize, data.len());
write_page(addr, &data[..size]);
addr += size;
data = &data[size..];
}
while data.len() > 0 {
let size = cmp::min(PAGE_SIZE as usize, data.len());
write_page(addr, &data[..size]);
addr += size;
data = &data[size..];
}
}

View File

@ -1,68 +1,192 @@
use std::cmp;
use std::vec::Vec;
use std::string::String;
use libc::{c_void, c_char, c_int, c_uint};
use core::str;
use std::btree_map::BTreeMap;
use byteorder::{ByteOrder, BigEndian};
use board::{mem, csr, cache, spiflash};
extern {
fn fs_remove(key: *const c_char);
fn fs_erase();
fn fs_write(key: *const c_char, buffer: *const c_void, buflen: c_uint) -> c_int;
fn fs_read(key: *const c_char, buffer: *mut c_void, buflen: c_uint,
remain: *mut c_uint) -> c_uint;
}
const ADDR: usize = mem::FLASH_BOOT_ADDRESS + 0x80000 /* max runtime size */;
const SIZE: usize = csr::CONFIG_SPIFLASH_SECTOR_SIZE as usize;
macro_rules! c_str {
($s:ident) => {
{
let mut c = [0; 64 + 1];
let len = cmp::min($s.len(), c.len() - 1);
c[..len].copy_from_slice($s.as_bytes());
c
mod lock {
use core::slice;
use core::sync::atomic::{AtomicUsize, Ordering};
static LOCKED: AtomicUsize = AtomicUsize::new(0);
pub struct Lock;
impl Lock {
pub fn take() -> Result<Lock, ()> {
if LOCKED.swap(1, Ordering::SeqCst) != 0 {
Err(()) // already locked
} else {
Ok(Lock) // locked now
}
}
pub fn data(&self) -> &'static [u8] {
unsafe { slice::from_raw_parts(super::ADDR as *const u8, super::SIZE) }
}
}
impl Drop for Lock {
fn drop(&mut self) {
LOCKED.store(0, Ordering::SeqCst)
}
}
}
pub fn read(key: &str, buf: &mut [u8]) -> Result<usize, usize> {
let key_c = c_str!(key);
let mut remain: c_uint = 0;
let result = unsafe {
fs_read(key_c.as_ptr() as *const c_char,
buf.as_mut_ptr() as *mut c_void, buf.len() as c_uint, &mut remain)
};
if remain == 0 { Ok(result as usize) } else { Err(remain as usize) }
pub use self::lock::Lock;
struct Iter<'a> {
data: &'a [u8],
offset: usize
}
pub fn read_to_end(key: &str) -> Vec<u8> {
let mut value = Vec::new();
match read(key, &mut []) {
Ok(0) => (),
Ok(_) => unreachable!(),
Err(size) => {
value.resize(size, 0);
read(key, &mut value).unwrap();
impl<'a> Iter<'a> {
fn new(data: &'a [u8]) -> Iter<'a> {
Iter { data: data, offset: 0 }
}
}
impl<'a> Iterator for Iter<'a> {
type Item = Result<(&'a [u8], &'a [u8]), ()>;
fn next(&mut self) -> Option<Self::Item> {
let data = &self.data[self.offset..];
if data.len() < 4 {
error!("offset {}: truncated record", self.offset);
return Some(Err(()))
}
let record_size = BigEndian::read_u32(data) as usize;
if record_size < 4 {
error!("offset {}: invalid record size", self.offset);
return Some(Err(()))
}
if record_size == !0 /* all ones; erased flash */ {
return None
}
let record_body = &data[4..record_size];
match record_body.iter().position(|&x| x == 0) {
None => {
error!("offset {}: missing separator", self.offset);
Some(Err(()))
}
Some(pos) => {
self.offset += record_size;
let (key, zero_and_value) = record_body.split_at(pos);
Some(Ok((key, &zero_and_value[1..])))
}
}
}
value
}
pub fn read_string(key: &str) -> String {
String::from_utf8(read_to_end(key)).unwrap()
pub fn read<F: FnOnce(Result<&[u8], ()>) -> R, R>(key: &str, f: F) -> R {
f(Lock::take().and_then(|lock| {
let mut iter = Iter::new(lock.data());
let mut value = &[][..];
while let Some(result) = iter.next() {
let (record_key, record_value) = result?;
if key.as_bytes() == record_key {
// last write wins
value = record_value
}
}
Ok(value)
}))
}
pub fn write(key: &str, buf: &[u8]) -> Result<(), ()> {
let key_c = c_str!(key);
let result = unsafe {
fs_write(key_c.as_ptr() as *const c_char,
buf.as_ptr() as *mut c_void, buf.len() as c_uint)
pub fn read_str<F: FnOnce(Result<&str, ()>) -> R, R>(key: &str, f: F) -> R {
read(key, |result| {
f(result.and_then(|value| str::from_utf8(value).map_err(|_| ())))
})
}
fn append_at(mut offset: usize, key: &[u8], value: &[u8]) -> Result<usize, ()> {
let record_size = 4 + key.len() + 1 + value.len();
if offset + record_size > SIZE {
return Err(())
}
let mut record_size_bytes = [0u8; 4];
BigEndian::write_u32(&mut record_size_bytes[..], record_size as u32);
spiflash::write(ADDR + offset, &record_size_bytes[..]);
offset += record_size_bytes.len();
spiflash::write(ADDR + offset, key);
offset += key.len();
spiflash::write(ADDR + offset, &[0]);
offset += 1;
spiflash::write(ADDR + offset, value);
offset += value.len();
cache::flush_l2_cache();
Ok(offset)
}
fn compact() -> Result<(), ()> {
let lock = Lock::take()?;
let mut items = BTreeMap::new();
{
let mut iter = Iter::new(lock.data());
while let Some(result) = iter.next() {
let (key, value) = result?;
items.insert(key, value);
}
}
spiflash::erase_sector(ADDR);
cache::flush_l2_cache();
let mut offset = 0;
for (key, value) in items {
offset = append_at(offset, key, value)?;
}
Ok(())
}
fn append(key: &str, value: &[u8]) -> Result<(), ()> {
let lock = Lock::take()?;
let free_offset = {
let mut iter = Iter::new(lock.data());
while let Some(result) = iter.next() {
let _ = result?;
}
iter.offset
};
if result == 1 { Ok(()) } else { Err(()) }
append_at(free_offset, key.as_bytes(), value)?;
Ok(())
}
pub fn remove(key: &str) {
let key_c = c_str!(key);
unsafe { fs_remove(key_c.as_ptr() as *const c_char) }
pub fn write(key: &str, value: &[u8]) -> Result<(), ()> {
match append(key, value) {
Ok(()) => (),
Err(()) => {
compact()?;
append(key, value)?;
}
}
Ok(())
}
pub fn erase() {
unsafe { fs_erase() }
pub fn remove(key: &str) -> Result<(), ()> {
write(key, &[])
}
pub fn erase() -> Result<(), ()> {
let _lock = Lock::take()?;
spiflash::erase_sector(ADDR);
cache::flush_l2_cache();
Ok(())
}

View File

@ -1,5 +1,5 @@
#![no_std]
#![feature(libc, repr_simd)]
#![feature(libc, repr_simd, const_fn)]
extern crate alloc_artiq;
#[macro_use]
@ -67,8 +67,8 @@ fn startup() {
info!("press 'e' to erase startup and idle kernels...");
while board::clock::get_ms() < t + 1000 {
if unsafe { readchar_nonblock() != 0 && readchar() == b'e' as libc::c_char } {
config::remove("startup_kernel");
config::remove("idle_kernel");
config::remove("startup_kernel").unwrap();
config::remove("idle_kernel").unwrap();
info!("startup and idle kernels erased");
break
}
@ -83,7 +83,7 @@ fn startup() {
board::ad9154::init().expect("cannot initialize ad9154");
let hardware_addr;
match EthernetAddress::parse(&config::read_string("mac")) {
match config::read_str("mac", |r| r.and_then(|s| EthernetAddress::parse(s))) {
Err(()) => {
hardware_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
warn!("using default MAC address {}; consider changing it", hardware_addr);
@ -95,7 +95,7 @@ fn startup() {
}
let protocol_addr;
match IpAddress::parse(&config::read_string("ip")) {
match config::read_str("ip", |r| r.and_then(|s| IpAddress::parse(s))) {
Err(()) | Ok(IpAddress::Unspecified) => {
protocol_addr = IpAddress::v4(192, 168, 1, 50);
info!("using default IP address {}", protocol_addr);

View File

@ -154,24 +154,25 @@ mod drtio {
pub fn startup(io: &Io) {
crg::init();
let mut opt = [b'i'];
let clk;
match config::read("startup_clock", &mut opt) {
Ok(0) | Ok(1) if &opt == b"i" => {
info!("startup RTIO clock: internal");
clk = 0
}
Ok(1) if &opt == b"e" => {
info!("startup RTIO clock: external");
clk = 1
}
_ => {
error!("unrecognized startup_clock configuration entry");
clk = 0
}
#[derive(Debug)]
enum RtioClock {
Internal = 0,
External = 1
};
if !crg::switch_clock(clk) {
let clk = config::read("startup_clock", |result| {
match result {
Ok(b"i") => RtioClock::Internal,
Ok(b"e") => RtioClock::External,
_ => {
error!("unrecognized startup_clock configuration entry");
RtioClock::Internal
}
}
});
info!("startup RTIO clock: {:?}", clk);
if !crg::switch_clock(clk as u8) {
error!("startup RTIO clock failed");
warn!("this may cause the system initialization to fail");
warn!("fix clocking and reset the device");

View File

@ -233,8 +233,12 @@ fn process_host_message(io: &Io,
// artiq_coreconfig
host::Request::FlashRead { ref key } => {
let value = config::read_to_end(key);
host_write(stream, host::Reply::FlashRead(&value))
config::read(key, |result| {
match result {
Ok(value) => host_write(stream, host::Reply::FlashRead(&value)),
Err(()) => host_write(stream, host::Reply::FlashError)
}
})
}
host::Request::FlashWrite { ref key, ref value } => {
@ -245,13 +249,18 @@ fn process_host_message(io: &Io,
}
host::Request::FlashRemove { ref key } => {
config::remove(key);
host_write(stream, host::Reply::FlashOk)
match config::remove(key) {
Ok(()) => host_write(stream, host::Reply::FlashOk),
Err(_) => host_write(stream, host::Reply::FlashError),
}
}
host::Request::FlashErase => {
config::erase();
host_write(stream, host::Reply::FlashOk)
match config::erase() {
Ok(()) => host_write(stream, host::Reply::FlashOk),
Err(_) => host_write(stream, host::Reply::FlashError),
}
}
// artiq_run/artiq_master
@ -599,12 +608,12 @@ fn flash_kernel_worker(io: &Io,
config_key: &str) -> io::Result<()> {
let mut session = Session::new(congress);
let kernel = config::read_to_end(config_key);
if kernel.len() == 0 {
return Err(io::Error::new(io::ErrorKind::NotFound, "kernel not found"))
}
unsafe { kern_load(io, &mut session, &kernel)? };
config::read(config_key, |result| {
match result {
Ok(kernel) if kernel.len() > 0 => unsafe { kern_load(io, &mut session, &kernel) },
_ => Err(io::Error::new(io::ErrorKind::NotFound, "kernel not found")),
}
})?;
kern_run(&mut session)?;
loop {

View File

@ -39,7 +39,7 @@ $(RUSTOUT)/libruntime.a:
cargo build --target=or1k-unknown-none \
--manifest-path $(realpath $(RUNTIME_DIRECTORY)/../firmware/runtime/Cargo.toml)
runtime.elf: $(RUSTOUT)/libruntime.a flash_storage.o ksupport_data.o
runtime.elf: $(RUSTOUT)/libruntime.a ksupport_data.o
$(LD) $(LDFLAGS) \
-T $(RUNTIME_DIRECTORY)/runtime.ld \
-o $@ \

View File

@ -1,308 +0,0 @@
/*
* Yann Sionneau <ys@m-labs.hk>, 2015
*/
#include <string.h>
#include <stdio.h>
#include <system.h>
#include <spiflash.h>
#include <generated/mem.h>
#include <generated/csr.h>
#include "flash_storage.h"
#if (defined CSR_SPIFLASH_BASE && defined CONFIG_SPIFLASH_PAGE_SIZE)
#define STORAGE_ADDRESS ((char *)(FLASH_BOOT_ADDRESS + 0x80000 /* max runtime size */))
#define STORAGE_SIZE CONFIG_SPIFLASH_SECTOR_SIZE
#define END_MARKER (0xFFFFFFFF)
#define min(a, b) (a>b?b:a)
#define max(a, b) (a>b?a:b)
struct record {
char *key;
unsigned int key_len;
char *value;
unsigned int value_len;
char *raw_record;
unsigned int size;
};
struct iter_state {
char *buffer;
unsigned int seek;
unsigned int buf_len;
};
static unsigned int get_record_size(char *buff)
{
unsigned int record_size;
memcpy(&record_size, buff, 4);
return record_size;
}
static void record_iter_init(struct iter_state *is, char *buffer, unsigned int buf_len)
{
is->buffer = buffer;
is->seek = 0;
is->buf_len = buf_len;
}
static int record_iter_next(struct iter_state *is, struct record *record, int *fatal)
{
if(is->seek >= is->buf_len)
return 0;
record->raw_record = &is->buffer[is->seek];
record->size = get_record_size(record->raw_record);
if(record->size == END_MARKER)
return 0;
if(record->size < 6) {
// core_log("flash_storage might be corrupted: record size is %u (<6) at address %08x\n",
// record->size, record->raw_record);
if(fatal)
*fatal = 1;
return 0;
}
if(is->seek > is->buf_len - sizeof(record->size) - 2) { /* 2 is the minimum key length */
// core_log("flash_storage might be corrupted: END_MARKER missing at the end of "
// "the storage sector\n");
if(fatal)
*fatal = 1;
return 0;
}
if(record->size > is->buf_len - is->seek) {
// core_log("flash_storage might be corrupted: invalid record_size %d at address %08x\n",
// record->size, record->raw_record);
if(fatal)
*fatal = 1;
return 0;
}
record->key = record->raw_record + sizeof(record->size);
record->key_len = strnlen(record->key, record->size - sizeof(record->size)) + 1;
if(record->key_len == record->size - sizeof(record->size) + 1) {
// core_log("flash_storage might be corrupted: invalid key length at address %08x\n",
// record->raw_record);
if(fatal)
*fatal = 1;
return 0;
}
record->value = record->key + record->key_len;
record->value_len = record->size - record->key_len - sizeof(record->size);
is->seek += record->size;
return 1;
}
static unsigned int get_free_space(void)
{
struct iter_state is;
struct record record;
record_iter_init(&is, STORAGE_ADDRESS, STORAGE_SIZE);
while(record_iter_next(&is, &record, NULL));
return STORAGE_SIZE - is.seek;
}
static int is_empty(struct record *record)
{
return record->value_len == 0;
}
static int key_exists(char *buff, const char *key, char *end, char accept_empty,
struct record *found_record)
{
struct iter_state is;
struct record iter_record;
int found = 0;
record_iter_init(&is, buff, end - buff);
while(record_iter_next(&is, &iter_record, NULL)) {
if(strcmp(iter_record.key, key) == 0) {
found = 1;
if(found_record)
*found_record = iter_record;
}
}
if(found && is_empty(found_record) && !accept_empty)
return 0;
if(found)
return 1;
return 0;
}
static char check_for_duplicates(char *buff)
{
struct record record, following_record;
struct iter_state is;
int no_error;
record_iter_init(&is, buff, STORAGE_SIZE);
no_error = record_iter_next(&is, &record, NULL);
while(no_error) {
no_error = record_iter_next(&is, &following_record, NULL);
if(no_error && key_exists(following_record.raw_record, record.key, &buff[STORAGE_SIZE], 1, NULL))
return 1;
record = following_record;
}
return 0;
}
static char check_for_empty_records(char *buff)
{
struct iter_state is;
struct record record;
record_iter_init(&is, buff, STORAGE_SIZE);
while(record_iter_next(&is, &record, NULL))
if(is_empty(&record))
return 1;
return 0;
}
static unsigned int try_to_flush_duplicates(const char *new_key, unsigned int buf_len)
{
unsigned int key_size, new_record_size, ret = 0, can_rollback = 0;
struct record record, previous_record;
char sector_buff[STORAGE_SIZE];
struct iter_state is;
memcpy(sector_buff, STORAGE_ADDRESS, STORAGE_SIZE);
if(check_for_duplicates(sector_buff)
|| key_exists(sector_buff, new_key, &sector_buff[STORAGE_SIZE], 0, NULL)
|| check_for_empty_records(sector_buff)) {
fs_erase();
record_iter_init(&is, sector_buff, STORAGE_SIZE);
while(record_iter_next(&is, &record, NULL)) {
if(is_empty(&record))
continue;
if(!key_exists((char *)STORAGE_ADDRESS, record.key, STORAGE_ADDRESS + STORAGE_SIZE, 1, NULL)) {
struct record rec;
if(!key_exists(sector_buff, record.key, &sector_buff[STORAGE_SIZE], 0, &rec))
continue;
if(strcmp(new_key, record.key) == 0) { // If we are about to write this key we don't keep the old value.
previous_record = rec; // This holds the old record in case we need it back (for instance if new record is too long)
can_rollback = 1;
} else
fs_write(record.key, rec.value, rec.value_len);
}
}
ret = 1;
}
key_size = strlen(new_key) + 1;
new_record_size = key_size + buf_len + sizeof(new_record_size);
if(can_rollback && new_record_size > get_free_space()) {
fs_write(new_key, previous_record.value, previous_record.value_len);
}
return ret;
}
static void write_at_offset(const char *key, const void *buffer,
int buf_len, unsigned int sector_offset)
{
int key_len = strlen(key) + 1;
unsigned int record_size = key_len + buf_len + sizeof(record_size);
unsigned int flash_addr = (unsigned int)STORAGE_ADDRESS + sector_offset;
write_to_flash(flash_addr, (unsigned char *)&record_size, sizeof(record_size));
write_to_flash(flash_addr+sizeof(record_size), (unsigned char *)key, key_len);
write_to_flash(flash_addr+sizeof(record_size)+key_len, buffer, buf_len);
flush_cpu_dcache();
}
int fs_write(const char *key, const void *buffer, unsigned int buf_len)
{
struct record record;
unsigned int key_size = strlen(key) + 1;
unsigned int new_record_size = key_size + sizeof(int) + buf_len;
int no_error, fatal = 0;
struct iter_state is;
record_iter_init(&is, STORAGE_ADDRESS, STORAGE_SIZE);
while((no_error = record_iter_next(&is, &record, &fatal)));
if(fatal)
goto fatal_error;
if(STORAGE_SIZE - is.seek >= new_record_size) {
write_at_offset(key, buffer, buf_len, is.seek);
return 1;
}
if(!try_to_flush_duplicates(key, buf_len)) // storage is full, let's try to free some space up.
return 0; // No duplicates found, cannot write the new key-value record: sector is full.
// Now retrying to write, hoping enough flash was freed.
record_iter_init(&is, STORAGE_ADDRESS, STORAGE_SIZE);
while((no_error = record_iter_next(&is, &record, &fatal)));
if(fatal)
goto fatal_error;
if(STORAGE_SIZE - is.seek >= new_record_size) {
write_at_offset(key, buffer, buf_len, is.seek);
return 1; // We eventually succeeded in writing the record
} else
return 0; // Storage is definitely full.
fatal_error:
// core_log("fatal error: flash storage might be corrupted\n");
return 0;
}
void fs_erase(void)
{
erase_flash_sector((unsigned int)STORAGE_ADDRESS);
flush_cpu_dcache();
}
unsigned int fs_read(const char *key, void *buffer, unsigned int buf_len, unsigned int *remain)
{
unsigned int read_length = 0;
struct iter_state is;
struct record record;
int fatal = 0;
if(remain)
*remain = 0;
record_iter_init(&is, STORAGE_ADDRESS, STORAGE_SIZE);
while(record_iter_next(&is, &record, &fatal)) {
if(strcmp(record.key, key) == 0) {
memcpy(buffer, record.value, min(record.value_len, buf_len));
read_length = min(record.value_len, buf_len);
if(remain)
*remain = max(0, (int)record.value_len - (int)buf_len);
}
}
// if(fatal)
// core_log("fatal error: flash storage might be corrupted\n");
return read_length;
}
void fs_remove(const char *key)
{
fs_write(key, NULL, 0);
}
#endif /* CSR_SPIFLASH_BASE && CONFIG_SPIFLASH_PAGE_SIZE */

View File

@ -1,13 +0,0 @@
/*
* Yann Sionneau <ys@m-labs.hk>, 2015
*/
#ifndef __FLASH_STORAGE_H
#define __FLASH_STORAGE_H
void fs_remove(const char *key);
void fs_erase(void);
int fs_write(const char *key, const void *buffer, unsigned int buflen);
unsigned int fs_read(const char *key, void *buffer, unsigned int buflen, unsigned int *remain);
#endif /* __FLASH_STORAGE_H */