/* * Copyright (C) 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #define G_LOG_DOMAIN "FuCfiDevice" #include "config.h" #include "fu-cfi-device.h" #include "fu-quirks.h" #include "fu-string.h" /** * FuCfiDevice: * * A chip conforming to the Common Flash Memory Interface, typically a SPI flash chip. * * Where required, the quirks instance IDs will be added in ->setup(). * * The defaults are set as follows, and can be overridden in quirk files: * * * `PageSize`: 0x100 * * `SectorSize`: 0x1000 * * `BlockSize`: 0x10000 * * See also: [class@FuDevice] */ typedef struct { gchar *flash_id; guint8 cmd_read_id_sz; guint32 page_size; guint32 sector_size; guint32 block_size; FuCfiDeviceCmd cmds[FU_CFI_DEVICE_CMD_LAST]; } FuCfiDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuCfiDevice, fu_cfi_device, FU_TYPE_DEVICE) enum { PROP_0, PROP_FLASH_ID, PROP_LAST }; #define GET_PRIVATE(o) (fu_cfi_device_get_instance_private(o)) #define FU_CFI_DEVICE_PAGE_SIZE_DEFAULT 0x100 #define FU_CFI_DEVICE_SECTOR_SIZE_DEFAULT 0x1000 #define FU_CFI_DEVICE_BLOCK_SIZE_DEFAULT 0x10000 static const gchar * fu_cfi_device_cmd_to_string(FuCfiDeviceCmd cmd) { if (cmd == FU_CFI_DEVICE_CMD_READ_ID) return "ReadId"; if (cmd == FU_CFI_DEVICE_CMD_PAGE_PROG) return "PageProg"; if (cmd == FU_CFI_DEVICE_CMD_CHIP_ERASE) return "ChipErase"; if (cmd == FU_CFI_DEVICE_CMD_READ_DATA) return "ReadData"; if (cmd == FU_CFI_DEVICE_CMD_READ_STATUS) return "ReadStatus"; if (cmd == FU_CFI_DEVICE_CMD_SECTOR_ERASE) return "SectorErase"; if (cmd == FU_CFI_DEVICE_CMD_WRITE_EN) return "WriteEn"; if (cmd == FU_CFI_DEVICE_CMD_WRITE_STATUS) return "WriteStatus"; if (cmd == FU_CFI_DEVICE_CMD_BLOCK_ERASE) return "BlockErase"; return NULL; } /** * fu_cfi_device_get_size: * @self: a #FuCfiDevice * * Gets the chip maximum size. * * This is typically set with the `FirmwareSizeMax` quirk key. * * Returns: size in bytes, or 0 if unknown * * Since: 1.7.1 **/ guint64 fu_cfi_device_get_size(FuCfiDevice *self) { g_return_val_if_fail(FU_IS_CFI_DEVICE(self), G_MAXUINT64); return fu_device_get_firmware_size_max(FU_DEVICE(self)); } /** * fu_cfi_device_set_size: * @self: a #FuCfiDevice * @size: maximum size in bytes, or 0 if unknown * * Sets the chip maximum size. * * Since: 1.7.1 **/ void fu_cfi_device_set_size(FuCfiDevice *self, guint64 size) { g_return_if_fail(FU_IS_CFI_DEVICE(self)); fu_device_set_firmware_size_max(FU_DEVICE(self), size); } /** * fu_cfi_device_get_flash_id: * @self: a #FuCfiDevice * * Gets the chip ID used to identify the device. * * Returns: the ID, or %NULL * * Since: 1.7.1 **/ const gchar * fu_cfi_device_get_flash_id(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), NULL); return priv->flash_id; } /** * fu_cfi_device_set_flash_id: * @self: a #FuCfiDevice * @flash_id: (nullable): The chip ID * * Sets the chip ID used to identify the device. * * Since: 1.7.1 **/ void fu_cfi_device_set_flash_id(FuCfiDevice *self, const gchar *flash_id) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFI_DEVICE(self)); if (g_strcmp0(flash_id, priv->flash_id) == 0) return; g_free(priv->flash_id); priv->flash_id = g_strdup(flash_id); } static void fu_cfi_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuCfiDevice *self = FU_CFI_DEVICE(object); FuCfiDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_FLASH_ID: g_value_set_object(value, priv->flash_id); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_cfi_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuCfiDevice *self = FU_CFI_DEVICE(object); switch (prop_id) { case PROP_FLASH_ID: fu_cfi_device_set_flash_id(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_cfi_device_finalize(GObject *object) { FuCfiDevice *self = FU_CFI_DEVICE(object); FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->flash_id); G_OBJECT_CLASS(fu_cfi_device_parent_class)->finalize(object); } static gboolean fu_cfi_device_setup(FuDevice *device, GError **error) { gsize flash_idsz = 0; FuCfiDevice *self = FU_CFI_DEVICE(device); FuCfiDevicePrivate *priv = GET_PRIVATE(self); /* sanity check */ if (priv->flash_id != NULL) flash_idsz = strlen(priv->flash_id); if (flash_idsz == 0 || flash_idsz % 2 != 0) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "not a valid flash-id"); return FALSE; } /* typically this will add quirk strings of 2, 4, then 6 bytes */ for (guint i = 0; i < flash_idsz; i += 2) { g_autofree gchar *flash_id = g_strndup(priv->flash_id, i + 2); fu_device_add_instance_str(device, "FLASHID", flash_id); if (!fu_device_build_instance_id_quirk(device, error, "CFI", "FLASHID", NULL)) return FALSE; } /* success */ return TRUE; } /** * fu_cfi_device_get_cmd: * @self: a #FuCfiDevice * @cmd: a #FuCfiDeviceCmd, e.g. %FU_CFI_DEVICE_CMD_CHIP_ERASE * @value: the API command value to use * @error: (nullable): optional return location for an error * * Gets the self vendor code. * * Returns: %TRUE on success * * Since: 1.7.1 **/ gboolean fu_cfi_device_get_cmd(FuCfiDevice *self, FuCfiDeviceCmd cmd, guint8 *value, GError **error) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (cmd >= FU_CFI_DEVICE_CMD_LAST) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "CFI cmd invalid"); return FALSE; } if (priv->cmds[cmd] == 0x0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "No defined CFI cmd for %s", fu_cfi_device_cmd_to_string(cmd)); return FALSE; } if (value != NULL) *value = priv->cmds[cmd]; return TRUE; } /** * fu_cfi_device_get_page_size: * @self: a #FuCfiDevice * * Gets the chip page size. This is typically the largest writable block size. * * This is typically set with the `CfiDevicePageSize` quirk key. * * Returns: page size in bytes * * Since: 1.7.3 **/ guint32 fu_cfi_device_get_page_size(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), G_MAXUINT32); return priv->page_size; } /** * fu_cfi_device_set_page_size: * @self: a #FuCfiDevice * @page_size: page size in bytes, or 0 if unknown * * Sets the chip page size. This is typically the largest writable block size. * * Since: 1.7.3 **/ void fu_cfi_device_set_page_size(FuCfiDevice *self, guint32 page_size) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFI_DEVICE(self)); priv->page_size = page_size; } /** * fu_cfi_device_get_sector_size: * @self: a #FuCfiDevice * * Gets the chip sector size. This is typically the smallest erasable page size. * * This is typically set with the `CfiDeviceSectorSize` quirk key. * * Returns: sector size in bytes * * Since: 1.7.3 **/ guint32 fu_cfi_device_get_sector_size(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), G_MAXUINT32); return priv->sector_size; } /** * fu_cfi_device_set_block_size: * @self: a #FuCfiDevice * @block_size: block size in bytes, or 0 if unknown * * Sets the chip block size. This is typically the largest erasable chunk size. * * Since: 1.7.4 **/ void fu_cfi_device_set_block_size(FuCfiDevice *self, guint32 block_size) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFI_DEVICE(self)); priv->block_size = block_size; } /** * fu_cfi_device_get_block_size: * @self: a #FuCfiDevice * * Gets the chip block size. This is typically the largest erasable block size. * * This is typically set with the `CfiDeviceBlockSize` quirk key. * * Returns: block size in bytes * * Since: 1.7.4 **/ guint32 fu_cfi_device_get_block_size(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), G_MAXUINT32); return priv->block_size; } /** * fu_cfi_device_set_sector_size: * @self: a #FuCfiDevice * @sector_size: sector size in bytes, or 0 if unknown * * Sets the chip sector size. This is typically the smallest erasable page size. * * Since: 1.7.3 **/ void fu_cfi_device_set_sector_size(FuCfiDevice *self, guint32 sector_size) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFI_DEVICE(self)); priv->sector_size = sector_size; } static gboolean fu_cfi_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuCfiDevice *self = FU_CFI_DEVICE(device); FuCfiDevicePrivate *priv = GET_PRIVATE(self); guint64 tmp; if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_READ_ID) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_READ_ID] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_READ_ID_SZ) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmd_read_id_sz = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_CHIP_ERASE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_CHIP_ERASE] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_BLOCK_ERASE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_BLOCK_ERASE] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_SECTOR_ERASE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_SECTOR_ERASE] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_WRITE_STATUS) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_WRITE_STATUS] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_PAGE_PROG) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_PAGE_PROG] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_READ_DATA) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_READ_DATA] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_READ_STATUS) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_READ_STATUS] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_WRITE_EN) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_WRITE_EN] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_PAGE_SIZE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, error)) return FALSE; priv->page_size = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_SECTOR_SIZE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, error)) return FALSE; priv->sector_size = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_BLOCK_SIZE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, error)) return FALSE; priv->block_size = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_cfi_device_to_string(FuDevice *device, guint idt, GString *str) { FuCfiDevice *self = FU_CFI_DEVICE(device); FuCfiDevicePrivate *priv = GET_PRIVATE(self); fu_string_append(str, idt, "FlashId", priv->flash_id); for (guint i = 0; i < FU_CFI_DEVICE_CMD_LAST; i++) { fu_string_append_kx(str, idt, fu_cfi_device_cmd_to_string(i), priv->cmds[i]); } if (priv->page_size > 0) fu_string_append_kx(str, idt, "PageSize", priv->page_size); if (priv->sector_size > 0) fu_string_append_kx(str, idt, "SectorSize", priv->sector_size); if (priv->block_size > 0) fu_string_append_kx(str, idt, "BlockSize", priv->block_size); } /** * fu_cfi_device_chip_select: * @self: a #FuCfiDevice * @value: boolean * @error: (nullable): optional return location for an error * * Sets the chip select value. * * Returns: %TRUE on success * * Since: 1.8.0 **/ gboolean fu_cfi_device_chip_select(FuCfiDevice *self, gboolean value, GError **error) { FuCfiDeviceClass *klass = FU_CFI_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (klass->chip_select == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "chip select is not implemented on this device"); return FALSE; } return klass->chip_select(self, value, error); } static gboolean fu_cfi_device_chip_select_assert(GObject *device, GError **error) { return fu_cfi_device_chip_select(FU_CFI_DEVICE(device), TRUE, error); } static gboolean fu_cfi_device_chip_select_deassert(GObject *device, GError **error) { return fu_cfi_device_chip_select(FU_CFI_DEVICE(device), FALSE, error); } /** * fu_cfi_device_chip_select_locker_new: * @self: a #FuCfiDevice * * Creates a custom device locker that asserts and deasserts the chip select signal. * * Returns: (transfer full): (nullable): a #FuDeviceLocker * * Since: 1.8.0 **/ FuDeviceLocker * fu_cfi_device_chip_select_locker_new(FuCfiDevice *self, GError **error) { g_return_val_if_fail(FU_IS_CFI_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_device_locker_new_full(self, fu_cfi_device_chip_select_assert, fu_cfi_device_chip_select_deassert, error); } static void fu_cfi_device_init(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); priv->page_size = FU_CFI_DEVICE_PAGE_SIZE_DEFAULT; priv->sector_size = FU_CFI_DEVICE_SECTOR_SIZE_DEFAULT; priv->block_size = FU_CFI_DEVICE_BLOCK_SIZE_DEFAULT; priv->cmds[FU_CFI_DEVICE_CMD_WRITE_STATUS] = 0x01; priv->cmds[FU_CFI_DEVICE_CMD_PAGE_PROG] = 0x02; priv->cmds[FU_CFI_DEVICE_CMD_READ_DATA] = 0x03; priv->cmds[FU_CFI_DEVICE_CMD_READ_STATUS] = 0x05; priv->cmds[FU_CFI_DEVICE_CMD_WRITE_EN] = 0x06; priv->cmds[FU_CFI_DEVICE_CMD_SECTOR_ERASE] = 0x20; priv->cmds[FU_CFI_DEVICE_CMD_CHIP_ERASE] = 0x60; priv->cmds[FU_CFI_DEVICE_CMD_READ_ID] = 0x9f; fu_device_set_summary(FU_DEVICE(self), "CFI flash chip"); } static void fu_cfi_device_class_init(FuCfiDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_cfi_device_finalize; object_class->get_property = fu_cfi_device_get_property; object_class->set_property = fu_cfi_device_set_property; klass_device->setup = fu_cfi_device_setup; klass_device->to_string = fu_cfi_device_to_string; klass_device->set_quirk_kv = fu_cfi_device_set_quirk_kv; /** * FuCfiDevice:flash-id: * * The CCI JEDEC flash ID. * * Since: 1.7.1 */ pspec = g_param_spec_string("flash-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLASH_ID, pspec); } /** * fu_cfi_device_new: * @ctx: a #FuContext * * Creates a new #FuCfiDevice. * * Returns: (transfer full): a #FuCfiDevice * * Since: 1.7.1 **/ FuCfiDevice * fu_cfi_device_new(FuContext *ctx, const gchar *flash_id) { return g_object_new(FU_TYPE_CFI_DEVICE, "context", ctx, "flash-id", flash_id, NULL); }