storage/stream: Add persistent write progress to stream_flash

Add additional API to stream_flash that can be used to make
stream write progress persistent using the settings subsystem.
This functionality makes it possible to resume a write operation
after it was interrupted, e.g. by power loss.

Signed-off-by: Jonathan Nilsen <Jonathan.Nilsen@nordicsemi.no>
This commit is contained in:
Jonathan Nilsen 2021-03-15 15:38:48 +01:00 committed by Maureen Helm
commit 0e6ac008a0
6 changed files with 376 additions and 1 deletions

View file

@ -17,6 +17,17 @@ limitations which does not allow flash writes to be performed in parallell with
other operations, such as radio RX and TX. Also, fewer write operations result other operations, such as radio RX and TX. Also, fewer write operations result
in faster response times seen from the application. in faster response times seen from the application.
Persistent stream write progress
********************************
Some stream write operations, such as DFU operations, may run for a long time.
When performing such long running operations it can be useful to be able to save
the stream write progress to persistent storage so that the operation can resume
at the same point after an unexpected interruption.
The Stream Flash module offers an API for loading, saving and clearing stream
write progress to persistent storage using the :ref:`Settings <settings_api>`
module. The API can be enabled using :option:`CONFIG_STREAM_FLASH_PROGRESS`.
API Reference API Reference
************* *************

View file

@ -128,6 +128,47 @@ int stream_flash_buffered_write(struct stream_flash_ctx *ctx, const uint8_t *dat
*/ */
int stream_flash_erase_page(struct stream_flash_ctx *ctx, off_t off); int stream_flash_erase_page(struct stream_flash_ctx *ctx, off_t off);
/**
* @brief Load persistent stream write progress stored with key
* @p settings_key .
*
* This function should be called directly after @ref stream_flash_init to
* load previous stream write progress before writing any data. If the loaded
* progress has fewer bytes written than @p ctx then it will be ignored.
*
* @param ctx context
* @param settings_key key to use with the settings module for loading
* the stream write progress
*
* @return non-negative on success, negative errno code on fail
*/
int stream_flash_progress_load(struct stream_flash_ctx *ctx,
const char *settings_key);
/**
* @brief Save persistent stream write progress using key @p settings_key .
*
* @param ctx context
* @param settings_key key to use with the settings module for storing
* the stream write progress
*
* @return non-negative on success, negative errno code on fail
*/
int stream_flash_progress_save(struct stream_flash_ctx *ctx,
const char *settings_key);
/**
* @brief Clear persistent stream write progress stored with key
* @p settings_key .
*
* @param ctx context
* @param settings_key key previously used for storing the stream write progress
*
* @return non-negative on success, negative errno code on fail
*/
int stream_flash_progress_clear(struct stream_flash_ctx *ctx,
const char *settings_key);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View file

@ -17,6 +17,15 @@ config STREAM_FLASH_ERASE
If disabled an external actor must erase the flash area being written If disabled an external actor must erase the flash area being written
to. to.
config STREAM_FLASH_PROGRESS
bool "Persistent stream write progress"
depends on SETTINGS
depends on !SETTINGS_NONE
help
Enable API for loading and storing the current write progress to flash
using the settings subsystem. In case of power failure or device
reset, the API can be used to resume writing from the latest state.
module = STREAM_FLASH module = STREAM_FLASH
module-str = stream flash module-str = stream flash
source "subsys/logging/Kconfig.template.log_config" source "subsys/logging/Kconfig.template.log_config"

View file

@ -16,6 +16,62 @@ LOG_MODULE_REGISTER(LOG_MODULE_NAME, CONFIG_STREAM_FLASH_LOG_LEVEL);
#include <storage/stream_flash.h> #include <storage/stream_flash.h>
#ifdef CONFIG_STREAM_FLASH_PROGRESS
#include <settings/settings.h>
static int settings_direct_loader(const char *key, size_t len,
settings_read_cb read_cb, void *cb_arg,
void *param)
{
struct stream_flash_ctx *ctx = (struct stream_flash_ctx *) param;
/* Handle the subtree if it is an exact key match. */
if (settings_name_next(key, NULL) == 0) {
size_t bytes_written = 0;
ssize_t len = read_cb(cb_arg, &bytes_written,
sizeof(bytes_written));
if (len != sizeof(ctx->bytes_written)) {
LOG_ERR("Unable to read bytes_written from storage");
return len;
}
/* Check that loaded progress is not outdated. */
if (bytes_written >= ctx->bytes_written) {
ctx->bytes_written = bytes_written;
} else {
LOG_WRN("Loaded outdated bytes_written %zu < %zu",
bytes_written, ctx->bytes_written);
return 0;
}
#ifdef CONFIG_STREAM_FLASH_ERASE
int rc;
struct flash_pages_info page;
off_t offset = (off_t) (ctx->offset + ctx->bytes_written) - 1;
/* Update the last erased page to avoid deleting already
* written data.
*/
if (ctx->bytes_written > 0) {
rc = flash_get_page_info_by_offs(ctx->fdev, offset,
&page);
if (rc != 0) {
LOG_ERR("Error %d while getting page info", rc);
return rc;
}
ctx->last_erased_page_start_offset = page.start_offset;
} else {
ctx->last_erased_page_start_offset = -1;
}
#endif /* CONFIG_STREAM_FLASH_ERASE */
}
return 0;
}
#endif /* CONFIG_STREAM_FLASH_PROGRESS */
#ifdef CONFIG_STREAM_FLASH_ERASE #ifdef CONFIG_STREAM_FLASH_ERASE
int stream_flash_erase_page(struct stream_flash_ctx *ctx, off_t off) int stream_flash_erase_page(struct stream_flash_ctx *ctx, off_t off)
@ -201,6 +257,15 @@ int stream_flash_init(struct stream_flash_ctx *ctx, const struct device *fdev,
return -EFAULT; return -EFAULT;
} }
#ifdef CONFIG_STREAM_FLASH_PROGRESS
int rc = settings_subsys_init();
if (rc != 0) {
LOG_ERR("Error %d initializing settings subsystem", rc);
return rc;
}
#endif
struct _inspect_flash inspect_flash_ctx = { struct _inspect_flash inspect_flash_ctx = {
.buf_len = buf_len, .buf_len = buf_len,
.total_size = 0 .total_size = 0
@ -241,3 +306,62 @@ int stream_flash_init(struct stream_flash_ctx *ctx, const struct device *fdev,
return 0; return 0;
} }
#ifdef CONFIG_STREAM_FLASH_PROGRESS
int stream_flash_progress_load(struct stream_flash_ctx *ctx,
const char *settings_key)
{
if (!ctx || !settings_key) {
return -EFAULT;
}
int rc = settings_load_subtree_direct(settings_key,
settings_direct_loader,
(void *) ctx);
if (rc != 0) {
LOG_ERR("Error %d while loading progress for \"%s\"",
rc, settings_key);
}
return rc;
}
int stream_flash_progress_save(struct stream_flash_ctx *ctx,
const char *settings_key)
{
if (!ctx || !settings_key) {
return -EFAULT;
}
int rc = settings_save_one(settings_key,
&ctx->bytes_written,
sizeof(ctx->bytes_written));
if (rc != 0) {
LOG_ERR("Error %d while storing progress for \"%s\"",
rc, settings_key);
}
return rc;
}
int stream_flash_progress_clear(struct stream_flash_ctx *ctx,
const char *settings_key)
{
if (!ctx || !settings_key) {
return -EFAULT;
}
int rc = settings_delete(settings_key);
if (rc != 0) {
LOG_ERR("Error %d while deleting progress for \"%s\"",
rc, settings_key);
}
return rc;
}
#endif /* CONFIG_STREAM_FLASH_PROGRESS */

View file

@ -7,7 +7,11 @@
CONFIG_ZTEST=y CONFIG_ZTEST=y
CONFIG_FLASH=y CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_DEBUG_OPTIMIZATIONS=y CONFIG_DEBUG_OPTIMIZATIONS=y
CONFIG_STREAM_FLASH=y CONFIG_STREAM_FLASH=y
CONFIG_STREAM_FLASH_ERASE=y CONFIG_STREAM_FLASH_ERASE=y
CONFIG_STREAM_FLASH_PROGRESS=y

View file

@ -9,6 +9,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <ztest.h> #include <ztest.h>
#include <drivers/flash.h> #include <drivers/flash.h>
#include <settings/settings.h>
#include <storage/stream_flash.h> #include <storage/stream_flash.h>
@ -35,6 +36,8 @@ static size_t cb_len;
static size_t cb_offset; static size_t cb_offset;
static int cb_ret; static int cb_ret;
static const char progress_key[] = "sf-test/progress";
static uint8_t buf[BUF_LEN]; static uint8_t buf[BUF_LEN];
static uint8_t read_buf[TESTBUF_SIZE]; static uint8_t read_buf[TESTBUF_SIZE];
const static uint8_t write_buf[TESTBUF_SIZE] = {[0 ... TESTBUF_SIZE - 1] = 0xaa}; const static uint8_t write_buf[TESTBUF_SIZE] = {[0 ... TESTBUF_SIZE - 1] = 0xaa};
@ -456,6 +459,186 @@ static void test_stream_flash_buffered_write_whole_page(void)
} }
#endif #endif
static size_t write_and_save_progress(size_t bytes, const char *save_key)
{
int rc;
size_t bytes_written;
rc = stream_flash_buffered_write(&ctx, write_buf, bytes, true);
zassert_equal(rc, 0, "expected success");
bytes_written = stream_flash_bytes_written(&ctx);
zassert_true(bytes_written > 0, "expected bytes to be written");
if (save_key) {
rc = stream_flash_progress_save(&ctx, save_key);
zassert_equal(rc, 0, "expected success");
}
return bytes_written;
}
static void clear_all_progress(void)
{
(void) settings_delete(progress_key);
}
static size_t load_progress(const char *load_key)
{
int rc;
rc = stream_flash_progress_load(&ctx, progress_key);
zassert_equal(rc, 0, "expected success");
return stream_flash_bytes_written(&ctx);
}
static void test_stream_flash_progress_api(void)
{
int rc;
clear_all_progress();
init_target();
/* Test save parameter validation */
rc = stream_flash_progress_save(NULL, progress_key);
zassert_true(rc < 0, "expected error since ctx is NULL");
rc = stream_flash_progress_save(&ctx, NULL);
zassert_true(rc < 0, "expected error since key is NULL");
rc = stream_flash_progress_save(&ctx, progress_key);
zassert_equal(rc, 0, "expected success");
(void) write_and_save_progress(BUF_LEN, progress_key);
/* Test load parameter validation */
rc = stream_flash_progress_load(NULL, progress_key);
zassert_true(rc < 0, "expected error since ctx is NULL");
rc = stream_flash_progress_load(&ctx, NULL);
zassert_true(rc < 0, "expected error since key is NULL");
rc = stream_flash_progress_load(&ctx, progress_key);
zassert_equal(rc, 0, "expected success");
/* Test clear parameter validation */
rc = stream_flash_progress_clear(NULL, progress_key);
zassert_true(rc < 0, "expected error since ctx is NULL");
rc = stream_flash_progress_clear(&ctx, NULL);
zassert_true(rc < 0, "expected error since key is NULL");
rc = stream_flash_progress_clear(&ctx, progress_key);
zassert_equal(rc, 0, "expected success");
}
static void test_stream_flash_progress_resume(void)
{
int rc;
size_t bytes_written_old;
size_t bytes_written;
#ifdef CONFIG_STREAM_FLASH_ERASE
off_t erase_offset_old;
off_t erase_offset;
#endif
clear_all_progress();
init_target();
bytes_written_old = stream_flash_bytes_written(&ctx);
#ifdef CONFIG_STREAM_FLASH_ERASE
erase_offset_old = ctx.last_erased_page_start_offset;
#endif
/* Test load with zero bytes_written */
rc = stream_flash_progress_save(&ctx, progress_key);
zassert_equal(rc, 0, "expected success");
rc = stream_flash_progress_load(&ctx, progress_key);
zassert_equal(rc, 0, "expected success");
bytes_written = stream_flash_bytes_written(&ctx);
zassert_equal(bytes_written, bytes_written_old,
"expected bytes_written to be unchanged");
#ifdef CONFIG_STREAM_FLASH_ERASE
erase_offset = ctx.last_erased_page_start_offset;
zassert_equal(erase_offset, erase_offset_old,
"expected erase offset to be unchanged");
#endif
clear_all_progress();
init_target();
/* Write some data and save the progress */
bytes_written_old = write_and_save_progress(page_size * 2,
progress_key);
#ifdef CONFIG_STREAM_FLASH_ERASE
erase_offset_old = ctx.last_erased_page_start_offset;
zassert_true(erase_offset_old != 0, "expected pages to be erased");
#endif
init_target();
/* Load the previous progress */
bytes_written = load_progress(progress_key);
zassert_equal(bytes_written, bytes_written_old,
"expected bytes_written to be loaded");
#ifdef CONFIG_STREAM_FLASH_ERASE
zassert_equal(erase_offset_old, ctx.last_erased_page_start_offset,
"expected last erased page offset to be loaded");
#endif
/* Check that outdated progress does not overwrite current progress */
init_target();
(void) write_and_save_progress(BUF_LEN, progress_key);
bytes_written_old = write_and_save_progress(BUF_LEN, NULL);
bytes_written = load_progress(progress_key);
zassert_equal(bytes_written, bytes_written_old,
"expected bytes_written to not be overwritten");
}
static void test_stream_flash_progress_clear(void)
{
int rc;
size_t bytes_written_old;
size_t bytes_written;
#ifdef CONFIG_STREAM_FLASH_ERASE
off_t erase_offset_old;
off_t erase_offset;
#endif
clear_all_progress();
init_target();
/* Test that progress is cleared. */
(void) write_and_save_progress(BUF_LEN, progress_key);
rc = stream_flash_progress_clear(&ctx, progress_key);
zassert_equal(rc, 0, "expected success");
init_target();
bytes_written_old = stream_flash_bytes_written(&ctx);
#ifdef CONFIG_STREAM_FLASH_ERASE
erase_offset_old = ctx.last_erased_page_start_offset;
#endif
rc = stream_flash_progress_load(&ctx, progress_key);
zassert_equal(rc, 0, "expected success");
bytes_written = stream_flash_bytes_written(&ctx);
zassert_equal(bytes_written, bytes_written_old,
"expected bytes_written to be unchanged");
#ifdef CONFIG_STREAM_FLASH_ERASE
erase_offset = ctx.last_erased_page_start_offset;
zassert_equal(erase_offset, erase_offset_old,
"expected erase offset to be unchanged");
#endif
}
void test_main(void) void test_main(void)
{ {
fdev = device_get_binding(FLASH_NAME); fdev = device_get_binding(FLASH_NAME);
@ -476,7 +659,10 @@ void test_main(void)
ztest_unit_test(test_stream_flash_flush), ztest_unit_test(test_stream_flash_flush),
ztest_unit_test(test_stream_flash_buffered_write_whole_page), ztest_unit_test(test_stream_flash_buffered_write_whole_page),
ztest_unit_test(test_stream_flash_erase_page), ztest_unit_test(test_stream_flash_erase_page),
ztest_unit_test(test_stream_flash_bytes_written) ztest_unit_test(test_stream_flash_bytes_written),
ztest_unit_test(test_stream_flash_progress_api),
ztest_unit_test(test_stream_flash_progress_resume),
ztest_unit_test(test_stream_flash_progress_clear)
); );
ztest_run_test_suite(lib_stream_flash_test); ztest_run_test_suite(lib_stream_flash_test);