Zephyr flash流式操作

本文分析Zephyr的flash流式操作如何实现。

概述

Flash在写入前必须擦除,擦除有page的限制,写入根据设备的不同可能会有对齐的限制。由于其操作的特殊性Flash无法直接接受流式数据的写入(连续的大小不定的数据),为应对该情况Zephyr对flash驱动进行了封装提供stream_flash,用于支持流式数据的写入。 stream_flash模块的特性如下:

  • 提供缓存,将流式数据收集到指定量后再写入到Flash

  • 支持缓存回读

  • 支持断电恢复机制

stream_flash最典型的应用就是DFU升级,在Zephyr中DFU升级和coredump写入flash都使用了stream_flash。

API

API说明

flash_stream的API声明在include/storage/stream_flash.h中,这里做注释说明

/**
* @brief 初始化flash_stream.
*
* @param ctx flash_stream的上下文,相关的管理数据都保存在其中
* @param fdev flash_stream使用的flash设备,流式数据将写入该设备
* @param buf flash_stream使用的缓存,写入的流式数据将先保存到该缓存
* @param buf_len 缓存大小,不能大于flash page尺寸,并按照flash的最小可写单位write-block-size对齐。
* @param offset flash设备中的偏移地址
* @param size 最大可写入量。当设置为0时表示可以从offset开始一直写到flash末尾
* @param cb 每次flash写操作完成后就会调用这个回调
*
* @return 0为成功,负数为失败
*/
int stream_flash_init(struct stream_flash_ctx *ctx, const struct device *fdev,
            uint8_t *buf, size_t buf_len, size_t offset, size_t size,
            stream_flash_callback_t cb);

/**
* @brief  写入数据到flash stream
*
* @param ctx flash_stream的上下文
* @param data 写入数据指针
* @param len 写入数据长度
* @param flush 最后一笔写入时使用true强制写入到flash内,当为false时只有在stream_flash满时才会写flash
*
* @return 0为成功,负数为失败
*/
int stream_flash_buffered_write(struct stream_flash_ctx *ctx, const uint8_t *data,
                size_t len, bool flush);

/**
* @brief 获取目前有多少数据被写入到flash.
*
* @param ctx flash_stream的上下文
*
* @return 已写入到flash的数据长度.
*/
size_t stream_flash_bytes_written(struct stream_flash_ctx *ctx);

在配置`CONFIG_STREAM_FLASH_ERASE=y`的情况下提供擦除API

/**
* @brief 擦除flash页
*
* @param ctx flash_stream的上下文
* @param off 擦除off所在页面
*
* @return 0为成功,负数为失败
*/
int stream_flash_erase_page(struct stream_flash_ctx *ctx, off_t off);

在配置`CONFIG_STREAM_FLASH_PROGRESS=y`的情况下提供恢复机制API

/**
* @brief 加载上一次保存的写入进度到flash_stream上下文中
*
* @param ctx flash_stream的上下文
* @param settings_key setting模块中标识进度的key
*
* @return 0为成功,负数为失败
*/
int stream_flash_progress_load(struct stream_flash_ctx *ctx,
                const char *settings_key);

/**
* @brief 保存当前flash_stream的写进度 .
*
* @param ctx flash_stream的上下文
* @param settings_key setting模块中标识进度的key
*
* @return 0为成功,负数为失败
*/
int stream_flash_progress_save(struct stream_flash_ctx *ctx,
                const char *settings_key);

/**
* @brief 清除flash_stream保存的进度.
*
* @param ctx flash_stream的上下文
* @param settings_key setting模块中标识进度的key
*
* @return 0为成功,负数为失败
*/
int stream_flash_progress_clear(struct stream_flash_ctx *ctx,
                const char *settings_key);

使用示例

这里做简单的使用示例如下注释,当不需要做断电恢复时,移除stream_flash_progress_xxx相关流程即可

#define STREAM_FLASH_KEY    "prop/stream_flash_key"

struct stream_flash_ctx stream_flash;

int data_verify(uint8_t *buf, size_t len, size_t offset)
{
    //校验回读数据
    ...

    //通知显示当前下载进度last_writed
    size_t last_writed = stream_flash_bytes_written()
    ....

    //保存当前下载进度
    stream_flash_progress_save(&stream_flash, STREAM_FLASH_KEY);

    //返回0表示校验通过
    return 0;
}

void download_task(void)
{
    const struct device *fdev = device_get_binding(FLASH_NAME);
    uint_t buf[64*1024];

    //设用FLASH_NAME设备
    //缓存为buf,大小为64K
    //stream_flash,位于flash的0x100000处,大小为3M
    int stream_flash_init(&stream_flash, fdev,
                            buf, sizeof(buf), 0x100000, 3*1024*1024,
                            data_verify);

    //加载上次位置
    stream_flash_progress_load(&stream_flash, STREAM_FLASH_KEY);

    //读取上次写入的位置,通知从该处下载数据
    size_t last_writed = stream_flash_bytes_written()
    .... nodify(last_writed)

    while(1){
        uint8 rev_buf[32];
        size_t rev_len;
        //接收数据
        int finish = revice(rev_buf, &rev_len);

        if(finish){
            //下载完所有数据成做flush写
            stream_flash_buffered_write(&stream_flash, rev_buf, rev_len, true);
            //清除下载进度,退出下载
            stream_flash_progress_clear(&stream_flash, STREAM_FLASH_KEY);
            break;
        }else{
            //写入当前下载数据,继续下载
            stream_flash_buffered_write(&stream_flash, rev_buf, rev_len, false);
        }
    }
}

实现分析

stream flash实现的核心就是将数据缓存到buffer中,当buffer满后一次性写入到flash。通过struct stream_flash_ctx进行管理

struct stream_flash_ctx {
    uint8_t *buf; //写缓存,写入数据时先保存在该buffer中
    size_t buf_len; //写缓存的长度
    size_t buf_bytes; //写缓存中有效数据的长度
    const struct device *fdev; //stream_flash操控的flash device,数据实际要被写入到该flash设备上
    size_t bytes_written; //总计写了多少数据到flash
    size_t offset; //stream_flash从flash内该偏移地址开始使用
    size_t available; //flash内还剩多少区域可以被stream_flash使用
    stream_flash_callback_t callback; //每写一次flash,调用一次该callback
#ifdef CONFIG_STREAM_FLASH_ERASE
    off_t last_erased_page_start_offset; //上一次擦除flash page所在的地址
#endif
};

各字段图示如下,后面的分析可以参考该图进行理解

../../../_images/stream_flash.png

初始化

stream flash初始化主要完成:对setting系统的初始化用于存储下载进度,完成对struct stream_flash_ctx的初始化赋值。

int stream_flash_init(struct stream_flash_ctx *ctx, const struct device *fdev,
            uint8_t *buf, size_t buf_len, size_t offset, size_t size,
            stream_flash_callback_t cb)
{
    if (!ctx || !fdev || !buf) {
        return -EFAULT;
    }

//初始化setting子系统,为存储下载进度做准备
#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 = {
        .buf_len = buf_len,
        .total_size = 0
    };

    //缓存的长度必须与write-block-size对齐
    if (buf_len % flash_get_write_block_size(fdev)) {
        LOG_ERR("Buffer size is not aligned to minimal write-block-size");
        return -EFAULT;
    }

    //遍历flash的页,计算flash的大小
    flash_page_foreach(fdev, find_flash_total_size, &inspect_flash_ctx);

    /* The flash size counted should never be equal zero */
    if (inspect_flash_ctx.total_size == 0) {
        return -EFAULT;
    }

    //检查stream_flash使用的范围是否在flash内
    if ((offset + size) > inspect_flash_ctx.total_size ||
        offset % flash_get_write_block_size(fdev)) {
        LOG_ERR("Incorrect parameter");
        return -EFAULT;
    }

    //初始化flash_stream管理上下文
    ctx->fdev = fdev;
    ctx->buf = buf;
    ctx->buf_len = buf_len;
    ctx->bytes_written = 0;
    ctx->buf_bytes = 0U;
    ctx->offset = offset;
    //计算flash_stream实际可使用的flash空间,size为0时,就是从offset开始到flash结束的长度
    ctx->available = (size == 0 ? inspect_flash_ctx.total_size - offset :
                    size);
    ctx->callback = cb;

#ifdef CONFIG_STREAM_FLASH_ERASE
    ctx->last_erased_page_start_offset = -1;
#endif

    return 0;
}

写入

int stream_flash_buffered_write(struct stream_flash_ctx *ctx, const uint8_t *data,
                size_t len, bool flush)
{
    int processed = 0;
    int rc = 0;
    int buf_empty_bytes;

    if (!ctx) {
        return -EFAULT;
    }

    //计算flash内剩余的空间是否能容纳还没写入的数据总长度
    if (ctx->bytes_written + ctx->buf_bytes + len > ctx->available) {
        return -ENOMEM;
    }

    //写入flash_stream数据比缓存空间大时,先将缓存空间填满,再写入到flash
    while ((len - processed) >=
        (buf_empty_bytes = ctx->buf_len - ctx->buf_bytes)) {
        //填满缓存空间
        memcpy(ctx->buf + ctx->buf_bytes, data + processed,
            buf_empty_bytes);

        //将缓存空间数据写到flash
        ctx->buf_bytes = ctx->buf_len;
        rc = flash_sync(ctx);

        if (rc != 0) {
            return rc;
        }

        processed += buf_empty_bytes;
    }

    //剩余的数据不足以填满缓存空间的,就保留在缓存空间
    if (processed < len) {
        memcpy(ctx->buf + ctx->buf_bytes,
            data + processed, len - processed);
        ctx->buf_bytes += len - processed;
    }

    //如果指定flush,无论缓存空间剩多少都一次性写入到flash中
    if (flush && ctx->buf_bytes > 0) {
        rc = flash_sync(ctx);
    }

    return rc;
}

flash写入流程,由于缓存的大小小于页,因此写入可以直接以页来操作

static int flash_sync(struct stream_flash_ctx *ctx)
{
    int rc = 0;
    //计算flash内写入地址
    size_t write_addr = ctx->offset + ctx->bytes_written;
    size_t buf_bytes_aligned;
    size_t fill_length;
    uint8_t filler;


    if (ctx->buf_bytes == 0) {
        return 0;
    }

    //擦除页,页所在位置由写入数据最后一byte位置计算而得
    if (IS_ENABLED(CONFIG_STREAM_FLASH_ERASE)) {

        rc = stream_flash_erase_page(ctx,
                        write_addr + ctx->buf_bytes - 1);
        if (rc < 0) {
            LOG_ERR("stream_flash_erase_page err %d offset=0x%08zx",
                rc, write_addr);
            return rc;
        }
    }

    //写入的数据不能和write-block-size对齐时,未对齐部分填入擦除flash后的值,通常时0xff
    //该操作只会在最后一笔数据flush发生
    fill_length = flash_get_write_block_size(ctx->fdev);
    if (ctx->buf_bytes % fill_length) {
        fill_length -= ctx->buf_bytes % fill_length;
        filler = flash_get_parameters(ctx->fdev)->erase_value;      //获取擦除flash后的值,通常是0xff

        memset(ctx->buf + ctx->buf_bytes, filler, fill_length);
    } else {
        fill_length = 0;
    }

    //写入flash
    buf_bytes_aligned = ctx->buf_bytes + fill_length;
    rc = flash_write(ctx->fdev, write_addr, ctx->buf, buf_bytes_aligned);

    if (rc != 0) {
        LOG_ERR("flash_write error %d offset=0x%08zx", rc,
            write_addr);
        return rc;
    }

    if (ctx->callback) {
        /* Invert to ensure that caller is able to discover a faulty
        * flash_read() even if no error code is returned.
        */
        for (int i = 0; i < ctx->buf_bytes; i++) {
            ctx->buf[i] = ~ctx->buf[i];
        }

        rc = flash_read(ctx->fdev, write_addr, ctx->buf,
                ctx->buf_bytes);
        if (rc != 0) {
            LOG_ERR("flash read failed: %d", rc);
            return rc;
        }

        rc = ctx->callback(ctx->buf, ctx->buf_bytes, write_addr);
        if (rc != 0) {
            LOG_ERR("callback failed: %d", rc);
            return rc;
        }
    }

    ctx->bytes_written += ctx->buf_bytes;
    ctx->buf_bytes = 0U;

    return rc;
}

上面你可能会发现,无论缓存是多大,即使小于一个page都会被执行stream_flash_erase_page,这会不会导致前面写入在同一页得数据被擦除呢? 我们来看一下其实现

int stream_flash_erase_page(struct stream_flash_ctx *ctx, off_t off)
{
    int rc;
    struct flash_pages_info page;

    //找到要擦除的页
    rc = flash_get_page_info_by_offs(ctx->fdev, off, &page);
    if (rc != 0) {
        LOG_ERR("Error %d while getting page info", rc);
        return rc;
    }

    //如果该页的地址和上一次擦除的一致,就不再执行擦除,从而避免丢失
    if (ctx->last_erased_page_start_offset == page.start_offset) {
        return 0;
    }

    LOG_DBG("Erasing page at offset 0x%08lx", (long)page.start_offset);
    //执行擦除
    rc = flash_erase(ctx->fdev, page.start_offset, page.size);

    if (rc != 0) {
        LOG_ERR("Error %d while erasing page", rc);
    } else {
        //更新上一次擦除地址
        ctx->last_erased_page_start_offset = page.start_offset;
    }

    return rc;
}

恢复机制实现

恢复机制使用setting模块存储和改写struct stream_flash_ctx中的bytes_written字段完成,setting是一个可读写模块后端可以对接nvs或者文件等,这里不做展开说明,只用指定setting以key+value的map形式保存即可。

int stream_flash_progress_save(struct stream_flash_ctx *ctx,
                const char *settings_key)
{
    if (!ctx || !settings_key) {
        return -EFAULT;
    }
    //将bytes_written写入到setting中
    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_load(struct stream_flash_ctx *ctx,
                const char *settings_key)
{
    if (!ctx || !settings_key) {
        return -EFAULT;
    }

    //在回调settings_direct_loader中对cts中的bytes_written进行更新
    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;
}

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;

    //查找满足条件的key
    if (settings_name_next(key, NULL) == 0) {
        size_t bytes_written = 0;

        //通过setting的callback和key读出bytes_written
        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;
        }

        //更新ctx中的bytes_written
        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;
        }

        //更新last_erased_page_start_offset
#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;
}


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;
}

参考

https://docs.zephyrproject.org/latest/reference/storage/stream/stream_flash.html