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

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

初始化 链接到标题

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