fwupd/plugins/emmc/fu-emmc-device.c
Richard Hughes 40cd18fa97 Allow using a per-device global percentage completion
It's actually quite hard to build a front-end for fwupd at the moment
as you're never sure when the progress bar is going to zip back to 0%
and start all over again. Some plugins go 0..100% for write, others
go 0..100% for erase, then again for write, then *again* for verify.

By creating a helper object we can easily split up the progress of the
specific task, e.g. write_firmware().

We can encode at the plugin level "the erase takes 50% of the time, the
write takes 40% and the read takes 10%". This means we can have a
progressbar which goes up just once at a consistent speed.
2021-09-13 14:28:15 +01:00

568 lines
17 KiB
C

/*
* Copyright (C) 2019 Mario Limonciello <mario.limonciello@dell.com>
*
* SPDX-License-Identifier: GPL-2+
*/
#include "config.h"
#include <fwupdplugin.h>
#include <linux/mmc/ioctl.h>
#include <sys/ioctl.h>
#include "fu-emmc-device.h"
/* From kernel linux/major.h */
#define MMC_BLOCK_MAJOR 179
/* From kernel linux/mmc/mmc.h */
#define MMC_SWITCH 6 /* ac [31:0] See below R1b */
#define MMC_SEND_EXT_CSD 8 /* adtc R1 */
#define MMC_SWITCH_MODE_WRITE_BYTE 0x03 /* Set target to value */
#define MMC_WRITE_BLOCK 24 /* adtc [31:0] data addr R1 */
/* From kernel linux/mmc/core.h */
#define MMC_RSP_PRESENT (1 << 0)
#define MMC_RSP_CRC (1 << 2) /* expect valid crc */
#define MMC_RSP_BUSY (1 << 3) /* card may send busy */
#define MMC_RSP_OPCODE (1 << 4) /* response contains opcode */
#define MMC_RSP_SPI_S1 (1 << 7) /* one status byte */
#define MMC_CMD_AC (0 << 5)
#define MMC_CMD_ADTC (1 << 5)
#define MMC_RSP_SPI_BUSY (1 << 10) /* card may send busy */
#define MMC_RSP_SPI_R1 (MMC_RSP_SPI_S1)
#define MMC_RSP_SPI_R1B (MMC_RSP_SPI_S1 | MMC_RSP_SPI_BUSY)
#define MMC_RSP_R1 (MMC_RSP_PRESENT | MMC_RSP_CRC | MMC_RSP_OPCODE)
#define MMC_RSP_R1B (MMC_RSP_PRESENT | MMC_RSP_CRC | MMC_RSP_OPCODE | MMC_RSP_BUSY)
/* EXT_CSD fields */
#define EXT_CSD_SUPPORTED_MODES 493 /* RO */
#define EXT_CSD_FFU_FEATURES 492 /* RO */
#define EXT_CSD_FFU_ARG_3 490 /* RO */
#define EXT_CSD_FFU_ARG_2 489 /* RO */
#define EXT_CSD_FFU_ARG_1 488 /* RO */
#define EXT_CSD_FFU_ARG_0 487 /* RO */
#define EXT_CSD_NUM_OF_FW_SEC_PROG_3 305 /* RO */
#define EXT_CSD_NUM_OF_FW_SEC_PROG_2 304 /* RO */
#define EXT_CSD_NUM_OF_FW_SEC_PROG_1 303 /* RO */
#define EXT_CSD_NUM_OF_FW_SEC_PROG_0 302 /* RO */
#define EXT_CSD_REV 192
#define EXT_CSD_FW_CONFIG 169 /* R/W */
#define EXT_CSD_DATA_SECTOR_SIZE 61 /* R */
#define EXT_CSD_MODE_CONFIG 30
#define EXT_CSD_MODE_OPERATION_CODES 29 /* W */
#define EXT_CSD_FFU_STATUS 26 /* R */
#define EXT_CSD_REV_V5_1 8
#define EXT_CSD_REV_V5_0 7
/* EXT_CSD field definitions */
#define EXT_CSD_NORMAL_MODE (0x00)
#define EXT_CSD_FFU_MODE (0x01)
#define EXT_CSD_FFU_INSTALL (0x01)
#define EXT_CSD_FFU (1 << 0)
#define EXT_CSD_UPDATE_DISABLE (1 << 0)
#define EXT_CSD_CMD_SET_NORMAL (1 << 0)
struct _FuEmmcDevice {
FuUdevDevice parent_instance;
guint32 sect_size;
};
G_DEFINE_TYPE(FuEmmcDevice, fu_emmc_device, FU_TYPE_UDEV_DEVICE)
static void
fu_emmc_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuEmmcDevice *self = FU_EMMC_DEVICE(device);
FU_DEVICE_CLASS(fu_emmc_device_parent_class)->to_string(device, idt, str);
fu_common_string_append_ku(str, idt, "SectorSize", self->sect_size);
}
static const gchar *
fu_emmc_device_get_manufacturer(guint64 mmc_id)
{
switch (mmc_id) {
case 0x00:
case 0x44:
return "SanDisk";
case 0x02:
return "Kingston/Sandisk";
case 0x03:
case 0x11:
return "Toshiba";
case 0x13:
return "Micron";
case 0x15:
return "Samsung/Sandisk/LG";
case 0x37:
return "Kingmax";
case 0x70:
case 0x2c:
return "Kingston";
default:
return NULL;
}
return NULL;
}
static gboolean
fu_emmc_device_get_sysattr_guint64(GUdevDevice *device,
const gchar *name,
guint64 *val_out,
GError **error)
{
const gchar *sysfs;
sysfs = g_udev_device_get_sysfs_attr(device, name);
if (sysfs == NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"failed get %s for %s",
name,
sysfs);
return FALSE;
}
*val_out = g_ascii_strtoull(sysfs, NULL, 16);
return TRUE;
}
static gboolean
fu_emmc_device_probe(FuDevice *device, GError **error)
{
GUdevDevice *udev_device = fu_udev_device_get_dev(FU_UDEV_DEVICE(device));
guint64 flag;
guint64 oemid = 0;
guint64 manfid = 0;
const gchar *tmp;
g_autoptr(GUdevDevice) udev_parent = NULL;
g_autofree gchar *name_only = NULL;
g_autofree gchar *man_oem = NULL;
g_autofree gchar *man_oem_name = NULL;
g_autofree gchar *vendor_id = NULL;
g_autoptr(GRegex) dev_regex = NULL;
/* FuUdevDevice->probe */
if (!FU_DEVICE_CLASS(fu_emmc_device_parent_class)->probe(device, error))
return FALSE;
udev_parent = g_udev_device_get_parent_with_subsystem(udev_device, "mmc", NULL);
if (udev_parent == NULL) {
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no MMC parent");
return FALSE;
}
/* look for only the parent node */
if (g_strcmp0(g_udev_device_get_devtype(udev_device), "disk") != 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"is not correct devtype=%s, expected disk",
g_udev_device_get_devtype(udev_device));
return FALSE;
}
/* ignore *rpmb and *boot* mmc block devices */
dev_regex = g_regex_new("mmcblk\\d$", 0, 0, NULL);
tmp = g_udev_device_get_name(udev_device);
if (tmp == NULL) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"device has no name");
return FALSE;
}
if (!g_regex_match(dev_regex, tmp, 0, NULL)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"is not raw mmc block device, devname=%s",
g_udev_device_get_name(udev_device));
return FALSE;
}
/* doesn't support FFU */
if (!fu_emmc_device_get_sysattr_guint64(udev_parent, "ffu_capable", &flag, error))
return FALSE;
if (flag == 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"%s does not support field firmware updates",
fu_device_get_name(device));
return FALSE;
}
/* name */
tmp = g_udev_device_get_sysfs_attr(udev_parent, "name");
if (tmp == NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"%s does not have 'name' sysattr",
fu_device_get_name(device));
return FALSE;
}
fu_device_set_name(device, tmp);
name_only = g_strdup_printf("EMMC\\%s", fu_device_get_name(device));
fu_device_add_instance_id(device, name_only);
/* manfid + oemid, manfid + oemid + name */
if (!fu_emmc_device_get_sysattr_guint64(udev_parent, "manfid", &manfid, error))
return FALSE;
if (!fu_emmc_device_get_sysattr_guint64(udev_parent, "oemid", &oemid, error))
return FALSE;
man_oem =
g_strdup_printf("EMMC\\%04" G_GUINT64_FORMAT "&%04" G_GUINT64_FORMAT, manfid, oemid);
fu_device_add_instance_id(device, man_oem);
man_oem_name = g_strdup_printf("EMMC\\%04" G_GUINT64_FORMAT "&%04" G_GUINT64_FORMAT "&%s",
manfid,
oemid,
fu_device_get_name(device));
fu_device_add_instance_id(device, man_oem_name);
/* set the vendor */
tmp = g_udev_device_get_sysfs_attr(udev_parent, "manfid");
vendor_id = g_strdup_printf("EMMC:%s", tmp);
fu_device_add_vendor_id(device, vendor_id);
fu_device_set_vendor(device, fu_emmc_device_get_manufacturer(manfid));
/* set the physical ID */
if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "mmc", error))
return FALSE;
/* internal */
if (!fu_emmc_device_get_sysattr_guint64(udev_device, "removable", &flag, error))
return FALSE;
if (flag == 0)
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL);
/* firmware version */
tmp = g_udev_device_get_sysfs_attr(udev_parent, "fwrev");
if (tmp != NULL) {
fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_NUMBER);
fu_device_set_version(device, tmp);
}
return TRUE;
}
static gboolean
fu_emmc_read_extcsd(FuEmmcDevice *self, guint8 *ext_csd, gsize ext_csd_sz, GError **error)
{
struct mmc_ioc_cmd idata = {0x0};
idata.write_flag = 0;
idata.opcode = MMC_SEND_EXT_CSD;
idata.arg = 0;
idata.flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC;
idata.blksz = 512;
idata.blocks = 1;
mmc_ioc_cmd_set_data(idata, ext_csd);
return fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
MMC_IOC_CMD,
(guint8 *)&idata,
NULL,
error);
}
static gboolean
fu_emmc_validate_extcsd(FuDevice *device, GError **error)
{
FuEmmcDevice *self = FU_EMMC_DEVICE(device);
guint8 ext_csd[512] = {0x0};
if (!fu_emmc_read_extcsd(FU_EMMC_DEVICE(device), ext_csd, sizeof(ext_csd), error))
return FALSE;
if (ext_csd[EXT_CSD_REV] < EXT_CSD_REV_V5_0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"FFU is only available on devices >= "
"MMC 5.0, not supported in %s",
fu_device_get_name(device));
return FALSE;
}
if ((ext_csd[EXT_CSD_SUPPORTED_MODES] & EXT_CSD_FFU) == 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"FFU is not supported in %s",
fu_device_get_name(device));
return FALSE;
}
if (ext_csd[EXT_CSD_FW_CONFIG] & EXT_CSD_UPDATE_DISABLE) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"firmware update was disabled in %s",
fu_device_get_name(device));
return FALSE;
}
self->sect_size = (ext_csd[EXT_CSD_DATA_SECTOR_SIZE] == 0) ? 512 : 4096;
return TRUE;
}
static gboolean
fu_emmc_device_setup(FuDevice *device, GError **error)
{
g_autoptr(GError) error_validate = NULL;
if (!fu_emmc_validate_extcsd(device, &error_validate))
g_debug("%s", error_validate->message);
else
fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_UPDATABLE);
return TRUE;
}
static FuFirmware *
fu_emmc_device_prepare_firmware(FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
FuEmmcDevice *self = FU_EMMC_DEVICE(device);
gsize fw_size = g_bytes_get_size(fw);
/* check alignment */
if ((fw_size % self->sect_size) > 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"firmware data size (%" G_GSIZE_FORMAT ") is not aligned",
fw_size);
return NULL;
}
return fu_firmware_new_from_bytes(fw);
}
static gboolean
fu_emmc_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FuProgress *progress,
FwupdInstallFlags flags,
GError **error)
{
FuEmmcDevice *self = FU_EMMC_DEVICE(device);
gsize fw_size = 0;
gsize total_done;
guint32 arg;
guint32 sect_done = 0;
guint8 ext_csd[512];
guint failure_cnt = 0;
g_autofree struct mmc_ioc_multi_cmd *multi_cmd = NULL;
g_autoptr(GBytes) fw = NULL;
g_autoptr(GPtrArray) chunks = NULL;
/* progress */
fu_progress_set_id(progress, G_STRLOC);
fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5); /* ffu */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 45);
if (!fu_emmc_read_extcsd(FU_EMMC_DEVICE(device), ext_csd, sizeof(ext_csd), error))
return FALSE;
fw = fu_firmware_get_bytes(firmware, error);
if (fw == NULL)
return FALSE;
fw_size = g_bytes_get_size(fw);
/* set CMD ARG */
arg = ext_csd[EXT_CSD_FFU_ARG_0] | ext_csd[EXT_CSD_FFU_ARG_1] << 8 |
ext_csd[EXT_CSD_FFU_ARG_2] << 16 | ext_csd[EXT_CSD_FFU_ARG_3] << 24;
/* prepare multi_cmd to be sent */
multi_cmd = g_malloc0(sizeof(struct mmc_ioc_multi_cmd) + 3 * sizeof(struct mmc_ioc_cmd));
multi_cmd->num_of_cmds = 3;
/* put device into ffu mode */
multi_cmd->cmds[0].opcode = MMC_SWITCH;
multi_cmd->cmds[0].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) | (EXT_CSD_MODE_CONFIG << 16) |
(EXT_CSD_FFU_MODE << 8) | EXT_CSD_CMD_SET_NORMAL;
multi_cmd->cmds[0].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC;
multi_cmd->cmds[0].write_flag = 1;
/* send image chunk */
multi_cmd->cmds[1].opcode = MMC_WRITE_BLOCK;
multi_cmd->cmds[1].blksz = self->sect_size;
multi_cmd->cmds[1].blocks = 1;
multi_cmd->cmds[1].arg = arg;
multi_cmd->cmds[1].flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC;
multi_cmd->cmds[1].write_flag = 1;
/* return device into normal mode */
multi_cmd->cmds[2].opcode = MMC_SWITCH;
multi_cmd->cmds[2].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) | (EXT_CSD_MODE_CONFIG << 16) |
(EXT_CSD_NORMAL_MODE << 8) | EXT_CSD_CMD_SET_NORMAL;
multi_cmd->cmds[2].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC;
multi_cmd->cmds[2].write_flag = 1;
fu_progress_step_done(progress);
/* build packets */
chunks = fu_chunk_array_new_from_bytes(fw,
0x00, /* start addr */
0x00, /* page_sz */
self->sect_size);
while (sect_done == 0) {
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index(chunks, i);
mmc_ioc_cmd_set_data(multi_cmd->cmds[1], fu_chunk_get_data(chk));
if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
MMC_IOC_MULTI_CMD,
(guint8 *)multi_cmd,
NULL,
error)) {
g_autoptr(GError) error_local = NULL;
g_prefix_error(error, "multi-cmd failed: ");
/* multi-cmd ioctl failed before exiting from ffu mode */
if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
MMC_IOC_CMD,
(guint8 *)&multi_cmd->cmds[2],
NULL,
&error_local)) {
g_prefix_error(error, "%s: ", error_local->message);
}
return FALSE;
}
if (!fu_emmc_read_extcsd(self, ext_csd, sizeof(ext_csd), error))
return FALSE;
/* if we need to restart the download */
sect_done = ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_0] |
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;
if (sect_done == 0) {
if (failure_cnt >= 3) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"programming failed");
return FALSE;
}
failure_cnt++;
g_debug("programming failed: retrying (%u)", failure_cnt);
break;
}
/* update progress */
fu_progress_set_percentage_full(fu_progress_get_child(progress),
(gsize)i + 1,
(gsize)chunks->len);
}
}
fu_progress_step_done(progress);
/* sanity check */
total_done = (gsize)sect_done * (gsize)self->sect_size;
if (total_done != fw_size) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"firmware size and number of sectors written "
"mismatch (%" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT "):",
total_done,
fw_size);
return FALSE;
}
/* check mode operation for ffu install*/
if (!ext_csd[EXT_CSD_FFU_FEATURES]) {
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
} else {
/* re-enter ffu mode and install the firmware */
multi_cmd->num_of_cmds = 2;
/* set ext_csd to install mode */
multi_cmd->cmds[1].opcode = MMC_SWITCH;
multi_cmd->cmds[1].blksz = 0;
multi_cmd->cmds[1].blocks = 0;
multi_cmd->cmds[1].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) |
(EXT_CSD_MODE_OPERATION_CODES << 16) |
(EXT_CSD_FFU_INSTALL << 8) | EXT_CSD_CMD_SET_NORMAL;
multi_cmd->cmds[1].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC;
multi_cmd->cmds[1].write_flag = 1;
/* send ioctl with multi-cmd */
if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
MMC_IOC_MULTI_CMD,
(guint8 *)multi_cmd,
NULL,
error)) {
g_autoptr(GError) error_local = NULL;
/* In case multi-cmd ioctl failed before exiting from ffu mode */
g_prefix_error(error, "multi-cmd failed setting install mode: ");
if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
MMC_IOC_CMD,
(guint8 *)&multi_cmd->cmds[2],
NULL,
&error_local)) {
g_prefix_error(error, "%s: ", error_local->message);
}
return FALSE;
}
/* return status */
if (!fu_emmc_read_extcsd(self, ext_csd, sizeof(ext_csd), error))
return FALSE;
if (ext_csd[EXT_CSD_FFU_STATUS] != 0) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"FFU install failed: %d",
ext_csd[EXT_CSD_FFU_STATUS]);
return FALSE;
}
}
fu_progress_step_done(progress);
return TRUE;
}
static void
fu_emmc_device_set_progress(FuDevice *self, FuProgress *progress)
{
fu_progress_set_id(progress, G_STRLOC);
fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* detach */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98); /* write */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* attach */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2); /* reload */
}
static void
fu_emmc_device_init(FuEmmcDevice *self)
{
fu_device_add_protocol(FU_DEVICE(self), "org.jedec.mmc");
fu_device_add_icon(FU_DEVICE(self), "media-memory");
}
static void
fu_emmc_device_finalize(GObject *object)
{
G_OBJECT_CLASS(fu_emmc_device_parent_class)->finalize(object);
}
static void
fu_emmc_device_class_init(FuEmmcDeviceClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
object_class->finalize = fu_emmc_device_finalize;
klass_device->setup = fu_emmc_device_setup;
klass_device->to_string = fu_emmc_device_to_string;
klass_device->prepare_firmware = fu_emmc_device_prepare_firmware;
klass_device->probe = fu_emmc_device_probe;
klass_device->write_firmware = fu_emmc_device_write_firmware;
klass_device->set_progress = fu_emmc_device_set_progress;
}