On Thu, Nov 13, 2014 at 8:32 AM, Avi Shchislowski
<[email protected]> wrote:
> Add support of FFU for eMMC v5.0
>
> Signed-off-by: Avi Shchislowski <[email protected]>
> ---
> drivers/mmc/card/Kconfig | 8 +
> drivers/mmc/card/block.c | 5 +
> drivers/mmc/core/Makefile | 1 +
> drivers/mmc/core/mmc_ffu.c | 487
> ++++++++++++++++++++++++++++++++++++++++++++
> include/linux/mmc/core.h | 22 ++
> 5 files changed, 523 insertions(+)
> create mode 100644 drivers/mmc/core/mmc_ffu.c
>
> diff --git a/drivers/mmc/card/Kconfig b/drivers/mmc/card/Kconfig
> index 5562308..19ba729 100644
> --- a/drivers/mmc/card/Kconfig
> +++ b/drivers/mmc/card/Kconfig
> @@ -68,3 +68,11 @@ config MMC_TEST
>
> This driver is only of interest to those developing or
> testing a host driver. Most people should say N here.
> +
> +config MMC_FFU
> + bool "FFU SUPPORT"
> + depends on MMC != n
> + help
> + This is an option to run firmware update on eMMC 5.0.
> + Field firmware updates (FFU) enables features enhancment
> + in the field.
Given that most of the code is in mmc/core, MMC_FFU should be defined
in in mmc/core/Kconfig.
> diff --git a/drivers/mmc/card/block.c b/drivers/mmc/card/block.c
> index 0c41ee0..79a3065 100644
> --- a/drivers/mmc/card/block.c
> +++ b/drivers/mmc/card/block.c
> @@ -480,6 +480,11 @@ static int mmc_blk_ioctl_cmd(struct block_device *bdev,
> goto cmd_done;
> }
>
> + if (idata->ic.opcode == MMC_FFU_INVOKE_OP) {
> + err = mmc_ffu_invoke(card, idata->buf);
> + goto cmd_done;
> + }
> +
> cmd.opcode = idata->ic.opcode;
> cmd.arg = idata->ic.arg;
> cmd.flags = idata->ic.flags;
> diff --git a/drivers/mmc/core/Makefile b/drivers/mmc/core/Makefile
> index 38ed210..f2fdfd4 100644
> --- a/drivers/mmc/core/Makefile
> +++ b/drivers/mmc/core/Makefile
> @@ -10,3 +10,4 @@ mmc_core-y := core.o bus.o host.o \
> quirks.o slot-gpio.o
>
> mmc_core-$(CONFIG_DEBUG_FS) += debugfs.o
> +obj-$(CONFIG_MMC_FFU) += mmc_ffu.o
> diff --git a/drivers/mmc/core/mmc_ffu.c b/drivers/mmc/core/mmc_ffu.c
Given we are already in mmc directory, the file can be named ffu.c
> new file mode 100644
> index 0000000..cde58eb
> --- /dev/null
> +++ b/drivers/mmc/core/mmc_ffu.c
> @@ -0,0 +1,487 @@
> +/*
> + * * ffu.c
> + *
> + * Copyright 2007-2008 Pierre Ossman
> + *
> + * Modified by SanDisk Corp., Copyright © 2013 SanDisk Corp.
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; either version 2 of the License, or (at
> + * your option) any later version.
> + *
> + * This program includes bug.h, card.h, host.h, mmc.h, scatterlist.h,
> + * slab.h, ffu.h & swap.h header files
> + * The original, unmodified version of this program – the mmc_test.c
> + * file – is obtained under the GPL v2.0 license that is available via
> + * http://www.gnu.org/licenses/,
> + * or http://www.opensource.org/licenses/gpl-2.0.php
> +*/
> +
> +#include <linux/bug.h>
> +#include <linux/errno.h>
> +#include <linux/mmc/card.h>
> +#include <linux/mmc/host.h>
> +#include <linux/mmc/mmc.h>
> +#include <linux/scatterlist.h>
> +#include <linux/slab.h>
> +#include <linux/swap.h>
> +#include <linux/firmware.h>
> +
> +/**
> + * struct mmc_ffu_pages - pages allocated by 'alloc_pages()'.
> + * @page: first page in the allocation
> + * @order: order of the number of pages allocated
> + */
> +struct mmc_ffu_pages {
> + struct page *page;
> + unsigned int order;
> +};
> +
> +/**
> + * struct mmc_ffu_mem - allocated memory.
> + * @arr: array of allocations
> + * @cnt: number of allocations
> + */
> +struct mmc_ffu_mem {
> + struct mmc_ffu_pages *arr;
> + unsigned int cnt;
> +};
> +
> +struct mmc_ffu_area {
> + unsigned long max_sz;
> + unsigned int max_tfr;
> + unsigned int max_segs;
> + unsigned int max_seg_sz;
> + unsigned int blocks;
> + unsigned int sg_len;
> + struct mmc_ffu_mem mem;
> + struct sg_table sgtable;
> +};
> +
> +/*
> + * Map memory into a scatterlist.
> + */
> +static unsigned int mmc_ffu_map_sg(struct mmc_ffu_mem *mem, int size,
> + struct scatterlist *sglist)
> +{
> + struct scatterlist *sg = sglist;
> + unsigned int i;
> + unsigned long sz = size;
> + unsigned int sctr_len = 0;
> + unsigned long len;
> +
> + for (i = 0; i < mem->cnt && sz; i++, sz -= len) {
> + len = PAGE_SIZE << mem->arr[i].order;
> +
> + if (len > sz) {
> + len = sz;
> + sz = 0;
> + }
> +
> + sg_set_page(sg, mem->arr[i].page, len, 0);
> + sg = sg_next(sg);
> + sctr_len++;
> + }
> +
> + return sctr_len;
> +}
> +
> +static void mmc_ffu_free_mem(struct mmc_ffu_mem *mem)
> +{
> + if (!mem)
> + return;
> +
> + while (mem->cnt--)
> + __free_pages(mem->arr[mem->cnt].page,
> mem->arr[mem->cnt].order);
> +
> + kfree(mem->arr);
> +}
> +
> +/*
> + * Cleanup struct mmc_ffu_area.
> + */
> +static int mmc_ffu_area_cleanup(struct mmc_ffu_area *area)
> +{
> + sg_free_table(&area->sgtable);
> + mmc_ffu_free_mem(&area->mem);
> + return 0;
> +}
> +
> +/*
> + * Allocate a lot of memory, preferably max_sz but at least min_sz. In case
> + * there isn't much memory do not exceed 1/16th total low mem pages. Also do
> + * not exceed a maximum number of segments and try not to make segments much
> + * bigger than maximum segment size.
> + */
> +static int mmc_ffu_alloc_mem(struct mmc_ffu_area *area, unsigned long min_sz)
> +{
> + unsigned long max_page_cnt = DIV_ROUND_UP(area->max_tfr, PAGE_SIZE);
> + unsigned long min_page_cnt = DIV_ROUND_UP(min_sz, PAGE_SIZE);
> + unsigned long max_seg_page_cnt =
> + DIV_ROUND_UP(area->max_seg_sz, PAGE_SIZE);
> + unsigned long page_cnt = 0;
> + /* we divide by 16 to ensure we will not allocate a big amount
> + * of unnecessary pages */
> + unsigned long limit = nr_free_buffer_pages() >> 4;
> +
> + gfp_t flags = GFP_KERNEL | GFP_DMA | __GFP_NOWARN | __GFP_NORETRY;
> +
> + if (max_page_cnt > limit) {
> + max_page_cnt = limit;
> + area->max_tfr = max_page_cnt * PAGE_SIZE;
> + }
> +
> + if (min_page_cnt > max_page_cnt)
> + min_page_cnt = max_page_cnt;
> +
> + if (area->max_segs * max_seg_page_cnt > max_page_cnt)
> + area->max_segs = DIV_ROUND_UP(max_page_cnt, max_seg_page_cnt);
> +
> + area->mem.arr = kzalloc(sizeof(struct mmc_ffu_pages) * area->max_segs,
> + GFP_KERNEL);
> + area->mem.cnt = 0;
> + if (!area->mem.arr)
> + goto out_free;
> +
> + while (max_page_cnt) {
> + struct page *page;
> + unsigned int order;
> +
> + order = get_order(max_seg_page_cnt << PAGE_SHIFT);
> +
> + do {
> + page = alloc_pages(flags, order);
> + } while (!page && order--);
> +
> + if (!page)
> + goto out_free;
> +
> + area->mem.arr[area->mem.cnt].page = page;
> + area->mem.arr[area->mem.cnt].order = order;
> + area->mem.cnt++;
> + page_cnt += 1UL << order;
> + if (max_page_cnt <= (1UL << order))
> + break;
> + max_page_cnt -= 1UL << order;
> + }
> +
> + if (page_cnt < min_page_cnt)
> + goto out_free;
> +
> + return 0;
> +
> +out_free:
> + mmc_ffu_free_mem(&area->mem);
> + return -ENOMEM;
> +}
> +
> +/*
> + * Initialize an area for data transfers.
> + * Copy the data to the allocated pages.
> + */
> +static int mmc_ffu_area_init(struct mmc_ffu_area *area, struct mmc_card
> *card,
> + const u8 *data)
> +{
> + int ret;
> + int i;
> + unsigned int length = 0, page_length;
> +
> + ret = mmc_ffu_alloc_mem(area, 1);
> + for (i = 0; i < area->mem.cnt; i++) {
> + if (length > area->max_tfr) {
> + ret = -EINVAL;
> + goto out_free;
> + }
> + page_length = PAGE_SIZE << area->mem.arr[i].order;
> + memcpy(page_address(area->mem.arr[i].page), data + length,
> + min(area->max_tfr - length, page_length));
> + length += page_length;
> + }
> +
> + ret = sg_alloc_table(&area->sgtable, area->mem.cnt, GFP_KERNEL);
> + if (ret)
> + goto out_free;
> +
> + area->sg_len = mmc_ffu_map_sg(&area->mem, area->max_tfr,
> + area->sgtable.sgl);
> +
> +
> + return 0;
> +
> +out_free:
> + mmc_ffu_free_mem(&area->mem);
> + return ret;
> +}
> +
> +static int mmc_ffu_write(struct mmc_card *card, const u8 *src, u32 arg,
> + int size)
> +{
> + int rc;
> + struct mmc_ffu_area area = {0};
> + int block_size = card->ext_csd.data_sector_size;
> +
> + area.max_segs = card->host->max_segs;
> + area.max_seg_sz = card->host->max_seg_size & ~(block_size - 1);
> +
> + do {
> + area.max_tfr = size;
> + if (area.max_tfr >> 9 > card->host->max_blk_count)
> + area.max_tfr = card->host->max_blk_count << 9;
> + if (area.max_tfr > card->host->max_req_size)
> + area.max_tfr = card->host->max_req_size;
> + if (DIV_ROUND_UP(area.max_tfr, area.max_seg_sz) >
> area.max_segs)
> + area.max_tfr = area.max_segs * area.max_seg_sz;
> +
> + rc = mmc_ffu_area_init(&area, card, src);
> + if (rc != 0)
> + goto exit;
> +
> + rc = mmc_simple_transfer(card, area.sgtable.sgl, area.sg_len,
> + arg, area.max_tfr / block_size, block_size, 1);
> + mmc_ffu_area_cleanup(&area);
> + if (rc != 0) {
> + pr_err("%s mmc_ffu_simple_transfer %d\n", __func__,
> rc);
> + goto exit;
> + }
> + src += area.max_tfr;
> + size -= area.max_tfr;
> +
> + } while (size > 0);
> +
> +exit:
> + return rc;
> +}
> +
> +/* Flush all scheduled work from the MMC work queue.
> + * and initialize the MMC device */
> +static int mmc_ffu_restart(struct mmc_card *card)
> +{
> + struct mmc_host *host = card->host;
> + int err = 0;
> +
> + err = mmc_power_save_host(host);
> + if (err) {
> + pr_warn("%s: going to sleep failed (%d)!!!\n",
> + __func__, err);
> + goto exit;
> + }
> +
> + err = mmc_power_restore_host(host);
> +
> +exit:
> +
> + return err;
> +}
> +
> +static int mmc_ffu_switch_mode(struct mmc_card *card, int mode)
> +{
> + int err = 0;
> + int offset;
> +
> + switch (mode) {
> + case MMC_FFU_MODE_SET:
> + case MMC_FFU_MODE_NORMAL:
> + offset = EXT_CSD_MODE_CONFIG;
> + break;
> + case MMC_FFU_INSTALL_SET:
> + offset = EXT_CSD_MODE_OPERATION_CODES;
> + mode = 0x1;
> + break;
> + default:
> + err = -EINVAL;
> + break;
> + }
> +
> + if (err == 0) {
> + err = mmc_switch(card, EXT_CSD_CMD_SET_NORMAL,
> + offset, mode,
> + card->ext_csd.generic_cmd6_time);
> + }
> +
> + return err;
> +}
> +
> +static int mmc_ffu_install(struct mmc_card *card, u8 *ext_csd)
> +{
> + int err;
> + u32 timeout;
> +
> + /* check mode operation */
> + if (!card->ext_csd.ffu_mode_op) {
> + /* host switch back to work in normal MMC Read/Write commands
> */
> + err = mmc_ffu_switch_mode(card, MMC_FFU_MODE_NORMAL);
> + if (err) {
> + pr_err("FFU: %s: switch to normal mode error %d:\n",
> + mmc_hostname(card->host), err);
> + return err;
> + }
> +
> + /* restart the eMMC */
> + err = mmc_ffu_restart(card);
> + if (err) {
> + pr_err("FFU: %s: install error %d:\n",
> + mmc_hostname(card->host), err);
> + return err;
> + }
> + } else {
> + timeout = ext_csd[EXT_CSD_OPERATION_CODE_TIMEOUT];
> + if (timeout == 0 || timeout > 0x17) {
> + timeout = 0x17;
> + pr_warn("FFU: %s: operation code timeout is out "\
> + "of range. Using maximum timeout.\n",
> + mmc_hostname(card->host));
> + }
> +
> + /* timeout is at millisecond resolution */
> + timeout = DIV_ROUND_UP((100 * (1 << timeout)), 1000);
> +
> + /* set ext_csd to install mode */
> + err = mmc_ffu_switch_mode(card, MMC_FFU_INSTALL_SET);
> + if (err) {
> + pr_err("FFU: %s: error %d setting install mode\n",
> + mmc_hostname(card->host), err);
> + return err;
> + }
> + }
> +
> + /* read ext_csd */
> + err = mmc_get_ext_csd(card, &ext_csd);
> + if (err) {
> + pr_err("FFU: %s: error %d sending ext_csd\n",
> + mmc_hostname(card->host), err);
> + return err;
> + }
> +
> + /* return status */
> + err = ext_csd[EXT_CSD_FFU_STATUS];
> + if (err) {
> + pr_err("FFU: %s: error %d FFU install:\n",
> + mmc_hostname(card->host), err);
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +int mmc_ffu_invoke(struct mmc_card *card, const char *name)
> +{
> + u8 *ext_csd;
> + int err;
> + u32 arg;
> + u32 fw_prog_bytes;
> + const struct firmware *fw;
> + int block_size = card->ext_csd.data_sector_size;
> +
> + /* Check if FFU is supported */
> + if (!card->ext_csd.ffu_capable) {
> + pr_err("FFU: %s: error FFU is not supported %d rev %d\n",
> + mmc_hostname(card->host), card->ext_csd.ffu_capable,
> + card->ext_csd.rev);
> + return -EOPNOTSUPP;
> + }
> +
> + if (strlen(name) > 512) {
> + pr_err("FFU: %s: %.20s is not a valid argument\n",
> + mmc_hostname(card->host), name);
> + return -EINVAL;
> + }
> +
> + /* setup FW data buffer */
> + err = request_firmware(&fw, name, &card->dev);
> + if (err) {
> + pr_err("FFU: %s: Firmware request failed %d\n",
> + mmc_hostname(card->host), err);
> + return err;
> + }
> + if ((fw->size % block_size)) {
> + pr_warn("FFU: %s: Warning %zd firmware data size "\
> + "is not aligned!!!\n", mmc_hostname(card->host),
> + fw->size);
> + }
> +
> + mmc_get_card(card);
> +
> + /* trigger flushing*/
> + err = mmc_flush_cache(card);
> + if (err) {
> + pr_err("FFU: %s: error %d flushing data\n",
> + mmc_hostname(card->host), err);
> + goto exit;
> + }
> +
> + /* Read the EXT_CSD */
> + err = mmc_get_ext_csd(card, &ext_csd);
> + if (err) {
> + pr_err("FFU: %s: error %d sending ext_csd\n",
> + mmc_hostname(card->host), err);
> + goto exit;
> + }
> +
> + /* set CMD ARG */
> + arg = ext_csd[EXT_CSD_FFU_ARG] |
> + ext_csd[EXT_CSD_FFU_ARG + 1] << 8 |
> + ext_csd[EXT_CSD_FFU_ARG + 2] << 16 |
> + ext_csd[EXT_CSD_FFU_ARG + 3] << 24;
> +
> + /* set device to FFU mode */
> + err = mmc_ffu_switch_mode(card, MMC_FFU_MODE_SET);
> + if (err) {
> + pr_err("FFU: %s: error %d FFU is not supported\n",
> + mmc_hostname(card->host), err);
> + goto exit;
> + }
> +
> + err = mmc_ffu_write(card, fw->data, arg, fw->size);
> + if (err) {
> + pr_err("FFU: %s: write error %d\n",
> + mmc_hostname(card->host), err);
> + goto exit;
> + }
> + /* payload will be checked only in op_mode supported */
> + if (card->ext_csd.ffu_mode_op) {
> + /* Read the EXT_CSD */
> + err = mmc_get_ext_csd(card, &ext_csd);
> + if (err) {
> + pr_err("FFU: %s: error %d sending ext_csd\n",
> + mmc_hostname(card->host), err);
> + goto exit;
> + }
> +
> + /* check that the eMMC has received the payload */
> + fw_prog_bytes = ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG] |
> + ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG + 1] << 8 |
> + ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG + 2] << 16 |
> + ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG + 3] << 24;
> +
> + /* convert sectors to bytes: multiply by -512B or 4KB as
Remove -
> + required by the card */
> + fw_prog_bytes *=
> + block_size << (ext_csd[EXT_CSD_DATA_SECTOR_SIZE] * 3);
Do we need the <<
The spec says:
"""The value is in terms of 512 Bytes or in multiple of eight 512Bytes
sectors (4KBytes) depending on the value of the DATA_SECTOR_SIZE
field."""
Therefore fw_prog_bytes *= block_size; should be enough (and what
happens for 512B block with this code).
> + if (fw_prog_bytes != fw->size) {
> + err = -EINVAL;
> + pr_err("FFU: %s: error %d number of programmed fw "\
> + "sector incorrect %d %zd\n", __func__, err,
> + fw_prog_bytes, fw->size);
> + goto exit;
> + }
> + }
> +
> + err = mmc_ffu_install(card, ext_csd);
> + if (err) {
> + pr_err("FFU: %s: error firmware install %d\n",
> + mmc_hostname(card->host), err);
> + goto exit;
> + }
> +
> +exit:
> + if (err != 0) {
> + /* host switch back to work in normal MMC
> + * Read/Write commands */
indentation
> + mmc_ffu_switch_mode(card, MMC_FFU_MODE_NORMAL);
> + }
> + release_firmware(fw);
> + mmc_put_card(card);
Add a pr_info that the device has been upgraded
> + return err;
> +}
> +EXPORT_SYMBOL(mmc_ffu_invoke);
> diff --git a/include/linux/mmc/core.h b/include/linux/mmc/core.h
> index 1740e6f..9b433ac 100644
> --- a/include/linux/mmc/core.h
> +++ b/include/linux/mmc/core.h
> @@ -220,4 +220,26 @@ struct device_node;
> extern u32 mmc_vddrange_to_ocrmask(int vdd_min, int vdd_max);
> extern int mmc_of_parse_voltage(struct device_node *np, u32 *mask);
>
> +/*
> + * eMMC5.0 Field Firmware Update (FFU) opcodes
> +*/
> +#define MMC_FFU_INVOKE_OP 302
> +
> +#define MMC_FFU_MODE_SET 0x1
> +#define MMC_FFU_MODE_NORMAL 0x0
> +#define MMC_FFU_INSTALL_SET 0x2
> +
> +#ifdef CONFIG_MMC_FFU
> +#define MMC_FFU_FEATURES 0x1
> +#define FFU_FEATURES(ffu_features) (ffu_features & MMC_FFU_FEATURES)
> +
> +int mmc_ffu_invoke(struct mmc_card *card, const char *name);
> +
> +#else
> +static inline int mmc_ffu_invoke(struct mmc_card *card, const char *name)
> +{
> + return -ENOSYS;
> +}
> +#endif
> +
> #endif /* LINUX_MMC_CORE_H */
> --
> 1.7.9.5
>
> --
> To unsubscribe from this list: send the line "unsubscribe linux-mmc" in
> the body of a message to [email protected]
> More majordomo info at http://vger.kernel.org/majordomo-info.html
When ffu_mode_op == 0, we need to alter mmc_init_card, to query
ext_csd even if oldcard != NULL.
When ffu_mode_op == 1, we also need to add some code to reprocess ext_csd.
Otherwise /sys/block/mmcblkX/device/fwrev will still report the older
firmware image, and firmware changes that affects ext_csd will not be
taken into account until the next reboot.
When modifying mmc_init_card, we have to be careful of the side
effects. For instance, in mmc_read_ext_csd(), we need to reset
card->nr_parts to 0 each time we reread ext_csd.
--
To unsubscribe from this list: send the line "unsubscribe linux-mmc" in
the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html