505 lines
11 KiB
C

/*
* * ffu.c
*
* Copyright 2007-2008 Pierre Ossman
*
* Modified by SanDisk Corp.
* Modified by Google Inc.
*
* 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.
*
*/
#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/mmc/core.h>
#include <linux/scatterlist.h>
#include <linux/slab.h>
#include <linux/swap.h>
#include <linux/mmc/ffu.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;
};
/*
* Get hack value
*/
static const struct mmc_ffu_hack *mmc_get_hack(
const struct mmc_ffu_args *args,
enum mmc_ffu_hack_type type)
{
int i;
for (i = 0; i < args->ack_nb; i++) {
if (args->hack[i].type == type)
return &args->hack[i];
}
return NULL;
}
/*
* 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;
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(*area->mem.arr) * area->max_segs,
GFP_KERNEL);
if (!area->mem.arr)
return -ENOMEM;
area->mem.cnt = 0;
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 size)
{
int ret;
int i;
int length = 0, page_length;
int min_size = 0;
area->max_tfr = size;
ret = mmc_ffu_alloc_mem(area, 1);
for (i = 0; i < area->mem.cnt; i++) {
if (length > size) {
ret = -EINVAL;
goto out_free;
}
page_length = PAGE_SIZE << area->mem.arr[i].order;
min_size = min(size - length, page_length);
memcpy(page_address(area->mem.arr[i].page), data + length,
min(size - 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, size, 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 max_tfr;
area.max_segs = card->host->max_segs;
area.max_seg_sz = card->host->max_seg_size;
do {
max_tfr = size;
if ((max_tfr >> 9) > card->host->max_blk_count)
max_tfr = card->host->max_blk_count << 9;
if (max_tfr > card->host->max_req_size)
max_tfr = card->host->max_req_size;
if (DIV_ROUND_UP(max_tfr, area.max_seg_sz) > area.max_segs)
max_tfr = area.max_segs * area.max_seg_sz;
rc = mmc_ffu_area_init(&area, card, src, max_tfr);
if (rc != 0)
goto exit;
rc = mmc_simple_transfer(card, area.sgtable.sgl, area.sg_len,
arg, max_tfr >> 9, 512, 1);
mmc_ffu_area_cleanup(&area);
if (rc != 0) {
pr_err("%s mmc_ffu_simple_transfer %d\n", __func__, rc);
goto exit;
}
src += max_tfr;
size -= 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) {
err = mmc_switch(card, 0,
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: Using maximum timeout: %s\n",
mmc_hostname(card->host),
"operation code timeout out of range");
}
/* 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_send_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: FFU status 0x%02x, expected 0\n",
mmc_hostname(card->host), err);
return -EINVAL;
}
return 0;
}
int mmc_ffu_invoke(struct mmc_card *card, const struct mmc_ffu_args *args)
{
u8 ext_csd[512];
int err;
u32 arg;
u32 fw_prog_bytes;
const struct firmware *fw;
const struct mmc_ffu_hack *hack;
/* 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(args->name) > 512) {
pr_err("FFU: %s: name %.20s is too long.\n",
mmc_hostname(card->host), args->name);
return -EINVAL;
}
/* setup FW data buffer */
err = request_firmware(&fw, args->name, &card->dev);
if (err) {
pr_err("FFU: %s: Firmware request failed %d\n",
mmc_hostname(card->host), err);
return err;
}
if ((fw->size % 512)) {
pr_warn("FFU: %s: Warning %zd firmware data size unaligned!\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_send_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 */
hack = mmc_get_hack(args, MMC_OVERRIDE_FFU_ARG);
if (hack == NULL) {
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;
} else {
arg = cpu_to_le32(hack->value);
}
/* 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_send_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;
fw_prog_bytes *= card->ext_csd.data_sector_size;
if (fw_prog_bytes != fw->size) {
err = -EINVAL;
pr_err("FFU: %s: error %d: incorrect programmation\n",
__func__, err);
pr_err("FFU: sectors written: %d, expected %zd\n",
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 */
mmc_ffu_switch_mode(card, MMC_FFU_MODE_NORMAL);
}
release_firmware(fw);
mmc_put_card(card);
return err;
}
EXPORT_SYMBOL(mmc_ffu_invoke);