fwupd/plugins/superio/fu-superio-it55-device.c
Richard Hughes 99df74f0c2 Add API to wait for a device
This allows us to ignore all the delays when the device is emulated, with the
idea being to do dozens of device emulations in the CI tests.

Also, do not call fu_progress_sleep() when the device is emulated.
2023-02-01 09:42:08 +00:00

649 lines
18 KiB
C

/*
* Copyright (C) 2021, TUXEDO Computers GmbH
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include <string.h>
#include "fu-superio-common.h"
#include "fu-superio-it55-device.h"
/* ROM of IT5570 consists of 64KB blocks. Blocks can be further subdivided in
* 256-byte chunks, which is especially visible when erasing the ROM. This is
* because in case of erasure offset within a block is specified in chunks (even
* though erasure is done one kilobyte at a time).
*
* Accessing ROM requires entering a special mode, which should be always left
* to restore normal operation of EC (handling of buttons, keyboard, etc.). */
#define SIO_CMD_EC_WRITE_BLOCK 0x02
#define SIO_CMD_EC_READ_BLOCK 0x03
#define SIO_CMD_EC_ERASE_KBYTE 0x05
#define SIO_CMD_EC_WRITE_1ST_KBYTE 0x06
#define EC_ROM_ACCESS_ON_1 0xDE
#define EC_ROM_ACCESS_ON_2 0xDC
#define EC_ROM_ACCESS_OFF 0xFE
#define BLOCK_SIZE 0x10000
#define CHUNK_SIZE 0x100
#define CHUNKS_IN_KBYTE 0x4
#define CHUNKS_IN_BLOCK 0x100
#define MAX_FLASHING_ATTEMPTS 5
typedef enum {
AUTOLOAD_NO_ACTION,
AUTOLOAD_DISABLE,
AUTOLOAD_SET_ON,
AUTOLOAD_SET_OFF,
} AutoloadAction;
struct _FuEcIt55Device {
FuSuperioDevice parent_instance;
gchar *prj_name;
AutoloadAction autoload_action;
};
G_DEFINE_TYPE(FuEcIt55Device, fu_superio_it55_device, FU_TYPE_SUPERIO_DEVICE)
static void
fu_superio_it55_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
/* FuSuperioDevice->to_string */
FU_DEVICE_CLASS(fu_superio_it55_device_parent_class)->to_string(device, idt, str);
fu_string_append(str, idt, "PrjName", self->prj_name);
fu_string_append_kx(str, idt, "AutoloadAction", self->autoload_action);
}
static gboolean
fu_superio_it55_device_ec_project(FuSuperioDevice *device, GError **error)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
gchar project[16] = {0x0};
if (!fu_superio_device_ec_write_cmd(device, SIO_CMD_EC_GET_NAME_STR, error))
return FALSE;
for (guint i = 0; i < sizeof(project) - 1; ++i) {
guint8 tmp = 0;
if (!fu_superio_device_ec_read_data(device, &tmp, error)) {
g_prefix_error(error, "failed to read firmware project: ");
return FALSE;
}
if (tmp == '$')
break;
project[i] = tmp;
}
self->prj_name = g_strdup(project);
/* success */
return TRUE;
}
static gboolean
fu_superio_it55_device_ec_version(FuSuperioDevice *self, GError **error)
{
gchar version[16] = {'1', '.', '\0'};
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_GET_VERSION_STR, error))
return FALSE;
for (guint i = 2; i < sizeof(version) - 1; i++) {
guint8 tmp = 0;
if (!fu_superio_device_ec_read_data(self, &tmp, error)) {
g_prefix_error(error, "failed to read firmware version: ");
return FALSE;
}
if (tmp == '$')
break;
version[i] = tmp;
}
fu_device_set_version(FU_DEVICE(self), version);
/* success */
return TRUE;
}
static gboolean
fu_superio_it55_device_ec_size(FuSuperioDevice *self, GError **error)
{
guint8 tmp = 0;
if (!fu_superio_device_reg_read(self, 0xf9, &tmp, error))
return FALSE;
switch (tmp & 0xf0) {
case 0xf0:
fu_device_set_firmware_size(FU_DEVICE(self), BLOCK_SIZE * 4);
break;
case 0x40:
fu_device_set_firmware_size(FU_DEVICE(self), BLOCK_SIZE * 3);
break;
default:
fu_device_set_firmware_size(FU_DEVICE(self), BLOCK_SIZE * 2);
break;
}
return TRUE;
}
static gboolean
fu_superio_it55_device_setup(FuDevice *device, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
/* FuSuperioDevice->setup */
if (!FU_DEVICE_CLASS(fu_superio_it55_device_parent_class)->setup(device, error))
return FALSE;
/* basic initialization */
if (!fu_superio_device_reg_write(self, 0xf9, 0x20, error) ||
!fu_superio_device_reg_write(self, 0xfa, 0x02, error) ||
!fu_superio_device_reg_write(self, 0xfb, 0x00, error) ||
!fu_superio_device_reg_write(self, 0xf8, 0xb1, error)) {
g_prefix_error(error, "initialization: ");
return FALSE;
}
/* Order of interactions with EC below matters. Additionally, reading EC
* project seems to be mandatory for successful firmware operations.
* Test after making changes here! */
/* get size from the EC */
if (!fu_superio_it55_device_ec_size(self, error))
return FALSE;
/* get installed firmware project from the EC */
if (!fu_superio_it55_device_ec_project(self, error))
return FALSE;
/* get installed firmware version from the EC */
if (!fu_superio_it55_device_ec_version(self, error))
return FALSE;
/* success */
return TRUE;
}
static GBytes *
fu_plugin_superio_patch_autoload(FuDevice *device, GBytes *fw, GError **error)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
guint offset;
gsize sz = 0;
const guint8 *unpatched = g_bytes_get_data(fw, &sz);
gboolean small_flash = (sz <= BLOCK_SIZE * 2);
g_autofree guint8 *patched = NULL;
if (self->autoload_action == AUTOLOAD_NO_ACTION)
return g_bytes_ref(fw);
for (offset = 0; offset < sz - 6; ++offset) {
if (unpatched[offset] == 0xa5 &&
(unpatched[offset + 1] == 0xa5 || unpatched[offset + 1] == 0xa4) &&
unpatched[offset + 5] == 0x5a)
break;
}
if (offset >= sz - 6)
return g_bytes_ref(fw);
/* not big enough */
if (offset + 8 >= sz) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"image is too small to patch");
return NULL;
}
patched = fu_memdup_safe(unpatched, sz, error);
if (patched == NULL)
return NULL;
if (self->autoload_action == AUTOLOAD_DISABLE) {
patched[offset + 2] = (small_flash ? 0x94 : 0x85);
patched[offset + 8] = 0x00;
} else if (self->autoload_action == AUTOLOAD_SET_ON) {
patched[offset + 2] = (small_flash ? 0x94 : 0x85);
patched[offset + 8] = (small_flash ? 0x7f : 0xbe);
} else if (self->autoload_action == AUTOLOAD_SET_OFF) {
patched[offset + 2] = (small_flash ? 0xa5 : 0xb5);
patched[offset + 8] = 0xaa;
}
return g_bytes_new_take(g_steal_pointer(&patched), sz);
}
/* progress callback is optional to not affect device progress during writing
* firmware */
static GBytes *
fu_superio_it55_device_get_firmware(FuDevice *device, FuProgress *progress, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
guint64 fwsize = fu_device_get_firmware_size_min(device);
guint64 block_count = (fwsize + BLOCK_SIZE - 1) / BLOCK_SIZE;
goffset offset = 0;
g_autofree guint8 *buf = NULL;
buf = g_malloc0(fwsize);
fu_progress_set_id(progress, G_STRLOC);
fu_progress_set_steps(progress, block_count);
for (guint i = 0; i < block_count; ++i) {
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_READ_BLOCK, error) ||
!fu_superio_device_ec_write_cmd(self, i, error))
return NULL;
for (guint j = 0; j < BLOCK_SIZE; ++j, ++offset) {
if (!fu_superio_device_ec_read_data(self, &buf[offset], error))
return NULL;
}
fu_progress_step_done(progress);
}
return g_bytes_new_take(g_steal_pointer(&buf), fwsize);
}
static GBytes *
fu_superio_it55_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error)
{
g_autoptr(FuDeviceLocker) locker = NULL;
/* require detach -> attach */
locker = fu_device_locker_new_full(device,
(FuDeviceLockerFunc)fu_device_detach,
(FuDeviceLockerFunc)fu_device_attach,
error);
if (locker == NULL)
return NULL;
fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ);
return fu_superio_it55_device_get_firmware(device, progress, error);
}
static gboolean
fu_superio_it55_device_attach(FuDevice *device, FuProgress *progress, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER))
return TRUE;
/* leave ROM access mode */
if (!fu_superio_device_ec_write_cmd(self, EC_ROM_ACCESS_OFF, error))
return FALSE;
/* success */
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
return TRUE;
}
static gboolean
fu_superio_it55_device_detach(FuDevice *device, FuProgress *progress, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER))
return TRUE;
/* enter ROM access mode */
if (!fu_superio_device_ec_write_cmd(self, EC_ROM_ACCESS_ON_1, error) ||
!fu_superio_device_ec_write_cmd(self, EC_ROM_ACCESS_ON_2, error))
return FALSE;
/* success */
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
return TRUE;
}
static gboolean
fu_superio_it55_device_erase(FuDevice *device, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
guint64 fwsize = fu_device_get_firmware_size_min(device);
guint64 chunk_count = (fwsize + CHUNK_SIZE - 1) / CHUNK_SIZE;
for (guint i = 0; i < chunk_count; i += CHUNKS_IN_KBYTE) {
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_ERASE_KBYTE, error) ||
!fu_superio_device_ec_write_cmd(self, i / CHUNKS_IN_BLOCK, error) ||
!fu_superio_device_ec_write_cmd(self, i % CHUNKS_IN_BLOCK, error) ||
!fu_superio_device_ec_write_cmd(self, 0x00, error))
return FALSE;
fu_device_sleep(device, 1); /* ms */
}
fu_device_sleep(device, 100); /* ms */
return TRUE;
}
static gboolean
fu_superio_it55_device_write_attempt(FuDevice *device,
GBytes *firmware,
FuProgress *progress,
GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
const guint8 *fw_data = NULL;
g_autoptr(GBytes) erased_fw = NULL;
g_autoptr(GBytes) written_fw = NULL;
g_autoptr(GPtrArray) blocks = 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_ERASE, 10, NULL);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "device-write-blk0");
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 9, NULL);
if (!fu_superio_it55_device_erase(device, error))
return FALSE;
erased_fw =
fu_superio_it55_device_get_firmware(device, fu_progress_get_child(progress), error);
if (erased_fw == NULL) {
g_prefix_error(error, "failed to read erased firmware: ");
return FALSE;
}
if (!fu_bytes_is_empty(erased_fw)) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_READ,
"firmware was not erased");
return FALSE;
}
fu_progress_step_done(progress);
/* write everything but the first kilobyte */
blocks = fu_chunk_array_new_from_bytes(firmware, 0x00, 0x00, BLOCK_SIZE);
for (guint i = 0; i < blocks->len; ++i) {
FuChunk *block = g_ptr_array_index(blocks, i);
gboolean first = (i == 0);
guint32 offset = 0;
guint32 bytes_left = fu_chunk_get_data_sz(block);
const guint8 *data = fu_chunk_get_data(block);
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_WRITE_BLOCK, error) ||
!fu_superio_device_ec_write_cmd(self, 0x00, error) ||
!fu_superio_device_ec_write_cmd(self, i, error) ||
!fu_superio_device_ec_write_cmd(self, first ? 0x04 : 0x00, error) ||
!fu_superio_device_ec_write_cmd(self, 0x00, error))
return FALSE;
for (guint j = 0; j < CHUNKS_IN_BLOCK; ++j) {
if (first && j < CHUNKS_IN_KBYTE) {
offset += CHUNK_SIZE;
bytes_left -= CHUNK_SIZE;
continue;
}
for (guint k = 0; k < CHUNK_SIZE; ++k) {
if (bytes_left == 0) {
if (!fu_superio_device_ec_write_data(self, 0xff, error))
return FALSE;
continue;
}
if (!fu_superio_device_ec_write_data(self, data[offset], error))
return FALSE;
++offset;
--bytes_left;
}
}
fu_progress_set_percentage_full(fu_progress_get_child(progress),
(gsize)i + 1,
(gsize)blocks->len);
}
fu_progress_step_done(progress);
/* now write the first kilobyte */
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_WRITE_1ST_KBYTE, error))
return FALSE;
fw_data = g_bytes_get_data(firmware, NULL);
for (guint i = 0; i < CHUNK_SIZE * CHUNKS_IN_KBYTE; ++i)
if (!fu_superio_device_ec_write_data(self, fw_data[i], error))
return FALSE;
fu_progress_step_done(progress);
fu_device_sleep(device, 1); /* ms */
written_fw =
fu_superio_it55_device_get_firmware(device, fu_progress_get_child(progress), error);
if (written_fw == NULL) {
g_prefix_error(error, "failed to read firmware: ");
return FALSE;
}
if (!fu_bytes_compare(written_fw, firmware, error)) {
g_prefix_error(error, "firmware verification: ");
return FALSE;
}
fu_progress_step_done(progress);
/* success */
return TRUE;
}
static gboolean
fu_superio_it55_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FuProgress *progress,
FwupdInstallFlags flags,
GError **error)
{
gsize fwsize;
g_autoptr(GBytes) fw = NULL;
g_autoptr(GBytes) fw_patched = NULL;
g_autoptr(FuDeviceLocker) locker = NULL;
/* progress */
fu_progress_set_id(progress, G_STRLOC);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, NULL);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL);
/* require detach -> attach */
locker = fu_device_locker_new_full(device,
(FuDeviceLockerFunc)fu_device_detach,
(FuDeviceLockerFunc)fu_device_attach,
error);
if (locker == NULL)
return FALSE;
/* get default image */
fw = fu_firmware_get_bytes(firmware, error);
if (fw == NULL)
return FALSE;
fwsize = g_bytes_get_size(fw);
if (fwsize < 1024) {
g_prefix_error(error, "firmware is too small %u: ", (guint)fwsize);
return FALSE;
}
fw_patched = fu_plugin_superio_patch_autoload(device, fw, error);
if (fw_patched == NULL)
return FALSE;
fu_progress_step_done(progress);
/* try this many times; the failure-to-flash case leaves you without a
* keyboard and future boot may completely fail */
for (guint i = 1;; ++i) {
g_autoptr(GError) error_chk = NULL;
if (fu_superio_it55_device_write_attempt(device,
fw_patched,
fu_progress_get_child(progress),
&error_chk))
break;
if (i == MAX_FLASHING_ATTEMPTS) {
g_propagate_error(error, g_steal_pointer(&error_chk));
return FALSE;
}
g_warning("failure %u: %s", i, error_chk->message);
}
fu_progress_step_done(progress);
/* success */
return TRUE;
}
static gchar *
fu_ec_extract_field(GBytes *fw, const gchar *name, GError **error)
{
guint offset;
gsize prefix_len;
gsize fwsz = 0;
const gchar *value;
const guint8 *buf = g_bytes_get_data(fw, &fwsz);
g_autofree gchar *field = g_strdup_printf("%s:", name);
prefix_len = strlen(field);
for (offset = 0; offset < fwsz - prefix_len; ++offset) {
if (memcmp(&buf[offset], field, prefix_len) == 0)
break;
}
if (offset >= fwsz - prefix_len) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"did not find %s field in the firmware image",
name);
return NULL;
}
offset += prefix_len;
value = (const gchar *)&buf[offset];
for (; offset < fwsz; ++offset) {
if (buf[offset] == '$')
return g_strndup(value, (const gchar *)&buf[offset] - value);
}
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"couldn't extract %s field value from the firmware image",
name);
return NULL;
}
static FuFirmware *
fu_superio_it55_device_prepare_firmware(FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
g_autofree gchar *date = NULL;
g_autofree gchar *prj_name = NULL;
g_autofree gchar *version = NULL;
prj_name = fu_ec_extract_field(fw, "PRJ", error);
if (prj_name == NULL)
return NULL;
version = fu_ec_extract_field(fw, "VER", NULL);
if (version == NULL)
version = g_strdup("(unknown version)");
date = fu_ec_extract_field(fw, "DATE", NULL);
if (date == NULL)
date = g_strdup("(unknown build date)");
g_debug("New firmware: %s %s built on %s", prj_name, version, date);
if (g_strcmp0(prj_name, self->prj_name) != 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"firmware targets %s instead of %s",
prj_name,
self->prj_name);
return NULL;
}
return fu_firmware_new_from_bytes(fw);
}
static gboolean
fu_superio_it55_device_set_quirk_kv(FuDevice *device,
const gchar *key,
const gchar *value,
GError **error)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
/* FuSuperioDevice->set_quirk_kv */
if (!FU_DEVICE_CLASS(fu_superio_it55_device_parent_class)
->set_quirk_kv(device, key, value, error))
return FALSE;
if (g_strcmp0(key, "SuperioAutoloadAction") == 0) {
if (g_strcmp0(value, "none") == 0) {
self->autoload_action = AUTOLOAD_NO_ACTION;
} else if (g_strcmp0(value, "disable") == 0) {
self->autoload_action = AUTOLOAD_DISABLE;
} else if (g_strcmp0(value, "on") == 0) {
self->autoload_action = AUTOLOAD_SET_ON;
} else if (g_strcmp0(value, "off") == 0) {
self->autoload_action = AUTOLOAD_SET_OFF;
} else {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"invalid value");
return FALSE;
}
}
/* success */
return TRUE;
}
static void
fu_superio_it55_device_init(FuEcIt55Device *self)
{
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_ONLY_OFFLINE);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
/* version string example: 1.07.02TR1 */
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN);
}
static void
fu_superio_it55_device_finalize(GObject *obj)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(obj);
g_free(self->prj_name);
G_OBJECT_CLASS(fu_superio_it55_device_parent_class)->finalize(obj);
}
static void
fu_superio_it55_device_class_init(FuEcIt55DeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
G_OBJECT_CLASS(klass)->finalize = fu_superio_it55_device_finalize;
klass_device->to_string = fu_superio_it55_device_to_string;
klass_device->attach = fu_superio_it55_device_attach;
klass_device->detach = fu_superio_it55_device_detach;
klass_device->dump_firmware = fu_superio_it55_device_dump_firmware;
klass_device->write_firmware = fu_superio_it55_device_write_firmware;
klass_device->setup = fu_superio_it55_device_setup;
klass_device->prepare_firmware = fu_superio_it55_device_prepare_firmware;
klass_device->set_quirk_kv = fu_superio_it55_device_set_quirk_kv;
}