| /* |
| * Copyright (c) 2016 Intel Corporation. |
| * Copyright (c) 2022-2023 Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <string.h> |
| #include <zephyr/types.h> |
| #include <zephyr/sys/__assert.h> |
| #include <zephyr/sys/util.h> |
| #include <zephyr/drivers/disk.h> |
| #include <errno.h> |
| #include <zephyr/init.h> |
| #include <zephyr/device.h> |
| #include <zephyr/drivers/flash.h> |
| #include <zephyr/storage/flash_map.h> |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(flashdisk, CONFIG_FLASHDISK_LOG_LEVEL); |
| |
| struct flashdisk_data { |
| struct disk_info info; |
| struct k_mutex lock; |
| const unsigned int area_id; |
| const off_t offset; |
| uint8_t *const cache; |
| const size_t cache_size; |
| const size_t size; |
| const size_t sector_size; |
| size_t page_size; |
| off_t cached_addr; |
| bool cache_valid; |
| bool cache_dirty; |
| }; |
| |
| #define GET_SIZE_TO_BOUNDARY(start, block_size) \ |
| (block_size - (start & (block_size - 1))) |
| |
| static int disk_flash_access_status(struct disk_info *disk) |
| { |
| LOG_DBG("status : %s", disk->dev ? "okay" : "no media"); |
| if (!disk->dev) { |
| return DISK_STATUS_NOMEDIA; |
| } |
| |
| return DISK_STATUS_OK; |
| } |
| |
| static int flashdisk_init_runtime(struct flashdisk_data *ctx, |
| const struct flash_area *fap) |
| { |
| int rc; |
| struct flash_pages_info page; |
| off_t offset; |
| |
| rc = flash_get_page_info_by_offs(ctx->info.dev, ctx->offset, &page); |
| if (rc < 0) { |
| LOG_ERR("Error %d while getting page info", rc); |
| return rc; |
| } |
| |
| ctx->page_size = page.size; |
| LOG_INF("Initialize device %s", ctx->info.name); |
| LOG_INF("offset %lx, sector size %zu, page size %zu, volume size %zu", |
| (long)ctx->offset, ctx->sector_size, ctx->page_size, ctx->size); |
| |
| if (ctx->cache_size == 0) { |
| /* Read-only flashdisk, no flash partition constraints */ |
| LOG_INF("%s is read-only", ctx->info.name); |
| return 0; |
| } |
| |
| if (IS_ENABLED(CONFIG_FLASHDISK_VERIFY_PAGE_LAYOUT)) { |
| if (ctx->offset != page.start_offset) { |
| LOG_ERR("Disk %s does not start at page boundary", |
| ctx->info.name); |
| return -EINVAL; |
| } |
| |
| offset = ctx->offset + page.size; |
| while (offset < ctx->offset + ctx->size) { |
| rc = flash_get_page_info_by_offs(ctx->info.dev, offset, &page); |
| if (rc < 0) { |
| LOG_ERR("Error %d getting page info at offset %lx", rc, offset); |
| return rc; |
| } |
| if (page.size != ctx->page_size) { |
| LOG_ERR("Non-uniform page size is not supported"); |
| return rc; |
| } |
| offset += page.size; |
| } |
| |
| if (offset != ctx->offset + ctx->size) { |
| LOG_ERR("Last page crossess disk %s boundary", |
| ctx->info.name); |
| return -EINVAL; |
| } |
| } |
| |
| if (ctx->page_size > ctx->cache_size) { |
| LOG_ERR("Cache too small (%zu needs %zu)", |
| ctx->cache_size, ctx->page_size); |
| return -ENOMEM; |
| } |
| |
| return 0; |
| } |
| |
| static int disk_flash_access_init(struct disk_info *disk) |
| { |
| struct flashdisk_data *ctx; |
| const struct flash_area *fap; |
| int rc; |
| |
| ctx = CONTAINER_OF(disk, struct flashdisk_data, info); |
| |
| rc = flash_area_open(ctx->area_id, &fap); |
| if (rc < 0) { |
| LOG_ERR("Flash area %u open error %d", ctx->area_id, rc); |
| return rc; |
| } |
| |
| k_mutex_lock(&ctx->lock, K_FOREVER); |
| |
| disk->dev = flash_area_get_device(fap); |
| |
| rc = flashdisk_init_runtime(ctx, fap); |
| if (rc < 0) { |
| flash_area_close(fap); |
| } |
| k_mutex_unlock(&ctx->lock); |
| |
| return rc; |
| } |
| |
| static bool sectors_in_range(struct flashdisk_data *ctx, |
| uint32_t start_sector, uint32_t sector_count) |
| { |
| uint32_t start, end; |
| |
| start = ctx->offset + (start_sector * ctx->sector_size); |
| end = start + (sector_count * ctx->sector_size); |
| |
| if ((end >= start) && (start >= ctx->offset) && (end <= ctx->offset + ctx->size)) { |
| return true; |
| } |
| |
| LOG_ERR("sector start %" PRIu32 " count %" PRIu32 |
| " outside partition boundary", start_sector, sector_count); |
| return false; |
| } |
| |
| static int disk_flash_access_read(struct disk_info *disk, uint8_t *buff, |
| uint32_t start_sector, uint32_t sector_count) |
| { |
| struct flashdisk_data *ctx; |
| off_t fl_addr; |
| uint32_t remaining; |
| uint32_t offset; |
| uint32_t len; |
| int rc = 0; |
| |
| ctx = CONTAINER_OF(disk, struct flashdisk_data, info); |
| |
| if (!sectors_in_range(ctx, start_sector, sector_count)) { |
| return -EINVAL; |
| } |
| |
| fl_addr = ctx->offset + start_sector * ctx->sector_size; |
| remaining = (sector_count * ctx->sector_size); |
| |
| k_mutex_lock(&ctx->lock, K_FOREVER); |
| |
| /* Operate on page addresses to easily check for cached data */ |
| offset = fl_addr & (ctx->page_size - 1); |
| fl_addr = ROUND_DOWN(fl_addr, ctx->page_size); |
| |
| /* Read up to page boundary on first iteration */ |
| len = ctx->page_size - offset; |
| while (remaining) { |
| if (remaining < len) { |
| len = remaining; |
| } |
| |
| if (ctx->cache_valid && ctx->cached_addr == fl_addr) { |
| memcpy(buff, &ctx->cache[offset], len); |
| } else if (flash_read(disk->dev, fl_addr + offset, buff, len) < 0) { |
| rc = -EIO; |
| goto end; |
| } |
| |
| fl_addr += ctx->page_size; |
| remaining -= len; |
| buff += len; |
| |
| /* Try to read whole page on next iteration */ |
| len = ctx->page_size; |
| offset = 0; |
| } |
| |
| end: |
| k_mutex_unlock(&ctx->lock); |
| |
| return rc; |
| } |
| |
| static int flashdisk_cache_commit(struct flashdisk_data *ctx) |
| { |
| if (!ctx->cache_valid || !ctx->cache_dirty) { |
| /* Either no cached data or cache matches flash data */ |
| return 0; |
| } |
| |
| if (flash_erase(ctx->info.dev, ctx->cached_addr, ctx->page_size) < 0) { |
| return -EIO; |
| } |
| |
| /* write data to flash */ |
| if (flash_write(ctx->info.dev, ctx->cached_addr, ctx->cache, ctx->page_size) < 0) { |
| return -EIO; |
| } |
| |
| ctx->cache_dirty = false; |
| return 0; |
| } |
| |
| static int flashdisk_cache_load(struct flashdisk_data *ctx, off_t fl_addr) |
| { |
| int rc; |
| |
| __ASSERT_NO_MSG((fl_addr & (ctx->page_size - 1)) == 0); |
| |
| if (ctx->cache_valid) { |
| if (ctx->cached_addr == fl_addr) { |
| /* Page is already cached */ |
| return 0; |
| } |
| /* Different page is in cache, commit it first */ |
| rc = flashdisk_cache_commit(ctx); |
| if (rc < 0) { |
| /* Failed to commit dirty page, abort */ |
| return rc; |
| } |
| } |
| |
| /* Load page into cache */ |
| ctx->cache_valid = false; |
| ctx->cache_dirty = false; |
| ctx->cached_addr = fl_addr; |
| rc = flash_read(ctx->info.dev, fl_addr, ctx->cache, ctx->page_size); |
| if (rc == 0) { |
| /* Successfully loaded into cache, mark as valid */ |
| ctx->cache_valid = true; |
| return 0; |
| } |
| |
| return -EIO; |
| } |
| |
| /* input size is either less or equal to a block size (ctx->page_size) |
| * and write data never spans across adjacent blocks. |
| */ |
| static int flashdisk_cache_write(struct flashdisk_data *ctx, off_t start_addr, |
| uint32_t size, const void *buff) |
| { |
| int rc; |
| off_t fl_addr; |
| uint32_t offset; |
| |
| /* adjust offset if starting address is not erase-aligned address */ |
| offset = start_addr & (ctx->page_size - 1); |
| |
| /* always align starting address for flash cache operations */ |
| fl_addr = ROUND_DOWN(start_addr, ctx->page_size); |
| |
| /* when writing full page the address must be page aligned |
| * when writing partial page user data must be within a single page |
| */ |
| __ASSERT_NO_MSG(fl_addr + ctx->page_size >= start_addr + size); |
| |
| rc = flashdisk_cache_load(ctx, fl_addr); |
| if (rc < 0) { |
| return rc; |
| } |
| |
| /* Do not mark cache as dirty if data to be written matches cache. |
| * If cache is already dirty, copy data to cache without compare. |
| */ |
| if (ctx->cache_dirty || memcmp(&ctx->cache[offset], buff, size)) { |
| /* Update cache and mark it as dirty */ |
| memcpy(&ctx->cache[offset], buff, size); |
| ctx->cache_dirty = true; |
| } |
| |
| return 0; |
| } |
| |
| static int disk_flash_access_write(struct disk_info *disk, const uint8_t *buff, |
| uint32_t start_sector, uint32_t sector_count) |
| { |
| struct flashdisk_data *ctx; |
| off_t fl_addr; |
| uint32_t remaining; |
| uint32_t size; |
| int rc = 0; |
| |
| ctx = CONTAINER_OF(disk, struct flashdisk_data, info); |
| |
| if (ctx->cache_size == 0) { |
| return -ENOTSUP; |
| } |
| |
| if (!sectors_in_range(ctx, start_sector, sector_count)) { |
| return -EINVAL; |
| } |
| |
| fl_addr = ctx->offset + start_sector * ctx->sector_size; |
| remaining = (sector_count * ctx->sector_size); |
| |
| k_mutex_lock(&ctx->lock, K_FOREVER); |
| |
| /* check if start address is erased-aligned address */ |
| if (fl_addr & (ctx->page_size - 1)) { |
| off_t block_bnd; |
| |
| /* not aligned */ |
| /* check if the size goes over flash block boundary */ |
| block_bnd = fl_addr + ctx->page_size; |
| block_bnd = block_bnd & ~(ctx->page_size - 1); |
| if ((fl_addr + remaining) <= block_bnd) { |
| /* not over block boundary (a partial block also) */ |
| if (flashdisk_cache_write(ctx, fl_addr, remaining, buff) < 0) { |
| rc = -EIO; |
| } |
| goto end; |
| } |
| |
| /* write goes over block boundary */ |
| size = GET_SIZE_TO_BOUNDARY(fl_addr, ctx->page_size); |
| |
| /* write first partial block */ |
| if (flashdisk_cache_write(ctx, fl_addr, size, buff) < 0) { |
| rc = -EIO; |
| goto end; |
| } |
| |
| fl_addr += size; |
| remaining -= size; |
| buff += size; |
| } |
| |
| /* start is an erase-aligned address */ |
| while (remaining) { |
| if (remaining < ctx->page_size) { |
| break; |
| } |
| |
| if (flashdisk_cache_write(ctx, fl_addr, ctx->page_size, buff) < 0) { |
| rc = -EIO; |
| goto end; |
| } |
| |
| fl_addr += ctx->page_size; |
| remaining -= ctx->page_size; |
| buff += ctx->page_size; |
| } |
| |
| /* remaining partial block */ |
| if (remaining) { |
| if (flashdisk_cache_write(ctx, fl_addr, remaining, buff) < 0) { |
| rc = -EIO; |
| goto end; |
| } |
| } |
| |
| end: |
| k_mutex_unlock(&ctx->lock); |
| |
| return 0; |
| } |
| |
| static int disk_flash_access_ioctl(struct disk_info *disk, uint8_t cmd, void *buff) |
| { |
| int rc; |
| struct flashdisk_data *ctx; |
| |
| ctx = CONTAINER_OF(disk, struct flashdisk_data, info); |
| |
| switch (cmd) { |
| case DISK_IOCTL_CTRL_SYNC: |
| k_mutex_lock(&ctx->lock, K_FOREVER); |
| rc = flashdisk_cache_commit(ctx); |
| k_mutex_unlock(&ctx->lock); |
| return rc; |
| case DISK_IOCTL_GET_SECTOR_COUNT: |
| *(uint32_t *)buff = ctx->size / ctx->sector_size; |
| return 0; |
| case DISK_IOCTL_GET_SECTOR_SIZE: |
| *(uint32_t *)buff = ctx->sector_size; |
| return 0; |
| case DISK_IOCTL_GET_ERASE_BLOCK_SZ: /* in sectors */ |
| k_mutex_lock(&ctx->lock, K_FOREVER); |
| *(uint32_t *)buff = ctx->page_size / ctx->sector_size; |
| k_mutex_unlock(&ctx->lock); |
| return 0; |
| default: |
| break; |
| } |
| |
| return -EINVAL; |
| } |
| |
| static const struct disk_operations flash_disk_ops = { |
| .init = disk_flash_access_init, |
| .status = disk_flash_access_status, |
| .read = disk_flash_access_read, |
| .write = disk_flash_access_write, |
| .ioctl = disk_flash_access_ioctl, |
| }; |
| |
| #define DT_DRV_COMPAT zephyr_flash_disk |
| |
| #define PARTITION_PHANDLE(n) DT_PHANDLE_BY_IDX(DT_DRV_INST(n), partition, 0) |
| /* Force cache size to 0 if partition is read-only */ |
| #define CACHE_SIZE(n) (DT_INST_PROP(n, cache_size) * !DT_PROP(PARTITION_PHANDLE(n), read_only)) |
| |
| #define DEFINE_FLASHDISKS_CACHE(n) \ |
| static uint8_t __aligned(4) flashdisk##n##_cache[CACHE_SIZE(n)]; |
| DT_INST_FOREACH_STATUS_OKAY(DEFINE_FLASHDISKS_CACHE) |
| |
| #define DEFINE_FLASHDISKS_DEVICE(n) \ |
| { \ |
| .info = { \ |
| .ops = &flash_disk_ops, \ |
| .name = DT_INST_PROP(n, disk_name), \ |
| }, \ |
| .area_id = DT_FIXED_PARTITION_ID(PARTITION_PHANDLE(n)), \ |
| .offset = DT_REG_ADDR(PARTITION_PHANDLE(n)), \ |
| .cache = flashdisk##n##_cache, \ |
| .cache_size = sizeof(flashdisk##n##_cache), \ |
| .size = DT_REG_SIZE(PARTITION_PHANDLE(n)), \ |
| .sector_size = DT_INST_PROP(n, sector_size), \ |
| }, |
| |
| static struct flashdisk_data flash_disks[] = { |
| DT_INST_FOREACH_STATUS_OKAY(DEFINE_FLASHDISKS_DEVICE) |
| }; |
| |
| #define VERIFY_CACHE_SIZE_IS_NOT_ZERO_IF_NOT_READ_ONLY(n) \ |
| COND_CODE_1(DT_PROP(PARTITION_PHANDLE(n), read_only), \ |
| (/* cache-size is not used for read-only disks */), \ |
| (BUILD_ASSERT(DT_INST_PROP(n, cache_size) != 0, \ |
| "Devicetree node " DT_NODE_PATH(DT_DRV_INST(n)) \ |
| " must have non-zero cache-size");)) |
| DT_INST_FOREACH_STATUS_OKAY(VERIFY_CACHE_SIZE_IS_NOT_ZERO_IF_NOT_READ_ONLY) |
| |
| #define VERIFY_CACHE_SIZE_IS_MULTIPLY_OF_SECTOR_SIZE(n) \ |
| BUILD_ASSERT(DT_INST_PROP(n, cache_size) % DT_INST_PROP(n, sector_size) == 0, \ |
| "Devicetree node " DT_NODE_PATH(DT_DRV_INST(n)) \ |
| " has cache size which is not a multiple of its sector size"); |
| DT_INST_FOREACH_STATUS_OKAY(VERIFY_CACHE_SIZE_IS_MULTIPLY_OF_SECTOR_SIZE) |
| |
| static int disk_flash_init(void) |
| { |
| int err = 0; |
| |
| for (int i = 0; i < ARRAY_SIZE(flash_disks); i++) { |
| int rc; |
| |
| k_mutex_init(&flash_disks[i].lock); |
| |
| rc = disk_access_register(&flash_disks[i].info); |
| if (rc < 0) { |
| LOG_ERR("Failed to register disk %s error %d", |
| flash_disks[i].info.name, rc); |
| err = rc; |
| } |
| } |
| |
| return err; |
| } |
| |
| SYS_INIT(disk_flash_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); |