/* * Copyright (C) 2018 Richard Hughes * Copyright (C) 2021, TUXEDO Computers GmbH * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include "fu-superio-common.h" #include "fu-superio-device.h" #define FU_PLUGIN_SUPERIO_DEFAULT_TIMEOUT 250 /* ms */ typedef struct { gchar *chipset; guint timeout_ms; guint16 port; guint16 data_port; guint16 control_port; guint16 id; } FuSuperioDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE (FuSuperioDevice, fu_superio_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_superio_device_get_instance_private (o)) enum { PROP_0, PROP_CHIPSET, PROP_LAST }; gboolean fu_superio_device_io_read (FuSuperioDevice *self, guint8 addr, guint8 *data, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (priv->port == 0) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "port isn't set"); return FALSE; } if (!fu_udev_device_pwrite (FU_UDEV_DEVICE (self), priv->port, addr, error)) return FALSE; if (!fu_udev_device_pread (FU_UDEV_DEVICE (self), priv->port + 1, data, error)) return FALSE; return TRUE; } gboolean fu_superio_device_io_read16 (FuSuperioDevice *self, guint8 addr, guint16 *data, GError **error) { guint8 msb; guint8 lsb; if (!fu_superio_device_io_read (self, addr, &msb, error)) return FALSE; if (!fu_superio_device_io_read (self, addr + 1, &lsb, error)) return FALSE; *data = ((guint16) msb << 8) | (guint16) lsb; return TRUE; } gboolean fu_superio_device_io_write (FuSuperioDevice *self, guint8 addr, guint8 data, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (priv->port == 0) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "port isn't set"); return FALSE; } if (!fu_udev_device_pwrite (FU_UDEV_DEVICE (self), priv->port, addr, error)) return FALSE; if (!fu_udev_device_pwrite (FU_UDEV_DEVICE (self), priv->port + 1, data, error)) return FALSE; return TRUE; } static gboolean fu_superio_device_set_ldn (FuSuperioDevice *self, guint8 ldn, GError **error) { return fu_superio_device_io_write (self, SIO_LDNxx_IDX_LDNSEL, ldn, error); } static gboolean fu_superio_device_regdump (FuSuperioDevice *self, guint8 ldn, GError **error) { const gchar *ldnstr = fu_superio_ldn_to_text (ldn); guint8 buf[0xff] = { 0x00 }; guint16 iobad0 = 0x0; guint16 iobad1 = 0x0; g_autoptr(GString) str = g_string_new (NULL); /* set LDN */ if (!fu_superio_device_set_ldn (self, ldn, error)) return FALSE; for (guint i = 0x00; i < 0xff; i++) { if (!fu_superio_device_io_read (self, i, &buf[i], error)) return FALSE; } /* get the i/o base addresses */ if (!fu_superio_device_io_read16 (self, SIO_LDNxx_IDX_IOBAD0, &iobad0, error)) return FALSE; if (!fu_superio_device_io_read16 (self, SIO_LDNxx_IDX_IOBAD1, &iobad1, error)) return FALSE; g_string_append_printf (str, "LDN:0x%02x ", ldn); if (iobad0 != 0x0) g_string_append_printf (str, "IOBAD0:0x%04x ", iobad0); if (iobad1 != 0x0) g_string_append_printf (str, "IOBAD1:0x%04x ", iobad1); if (ldnstr != NULL) g_string_append_printf (str, "(%s)", ldnstr); if (g_getenv ("FWUPD_SUPERIO_VERBOSE") != NULL) fu_common_dump_raw (G_LOG_DOMAIN, str->str, buf, sizeof(buf)); return TRUE; } static void fu_superio_device_to_string (FuDevice *device, guint idt, GString *str) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); /* FuUdevDevice->to_string */ FU_DEVICE_CLASS (fu_superio_device_parent_class)->to_string (device, idt, str); fu_common_string_append_kv (str, idt, "Chipset", priv->chipset); fu_common_string_append_kx (str, idt, "Id", priv->id); fu_common_string_append_kx (str, idt, "Port", priv->port); fu_common_string_append_kx (str, idt, "DataPort", priv->data_port); fu_common_string_append_kx (str, idt, "ControlPort", priv->control_port); } static gboolean fu_superio_device_check_id (FuSuperioDevice *self, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); guint16 id_tmp; /* no quirk entry */ if (priv->id == 0x0) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "invalid SuperioId"); return FALSE; } /* can't check the ID, assume it's correct */ if (priv->port == 0) return TRUE; /* check ID, which can be done from any LDN */ if (!fu_superio_device_io_read16 (self, SIO_LDNxx_IDX_CHIPID1, &id_tmp, error)) return FALSE; if (priv->id != id_tmp) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "SuperIO chip not supported, got %04x, expected %04x", (guint) id_tmp, (guint) priv->id); return FALSE; } return TRUE; } static gboolean fu_superio_device_wait_for (FuSuperioDevice *self, guint8 mask, gboolean set, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); g_autoptr(GTimer) timer = g_timer_new (); do { guint8 status = 0x00; if (!fu_udev_device_pread (FU_UDEV_DEVICE (self), priv->control_port, &status, error)) return FALSE; if (g_timer_elapsed (timer, NULL) * 1000.0f > priv->timeout_ms) break; if (set && (status & mask) != 0) return TRUE; if (!set && (status & mask) == 0) return TRUE; } while (TRUE); g_set_error (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, "timed out whilst waiting for 0x%02x:%i", mask, set); return FALSE; } gboolean fu_superio_device_ec_read_data (FuSuperioDevice *self, guint8 *data, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (!fu_superio_device_wait_for (self, SIO_STATUS_EC_OBF, TRUE, error)) return FALSE; return fu_udev_device_pread (FU_UDEV_DEVICE (self), priv->data_port, data, error); } gboolean fu_superio_device_ec_write_data (FuSuperioDevice *self, guint8 data, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (!fu_superio_device_wait_for (self, SIO_STATUS_EC_IBF, FALSE, error)) return FALSE; return fu_udev_device_pwrite (FU_UDEV_DEVICE (self), priv->data_port, data, error); } gboolean fu_superio_device_ec_write_cmd (FuSuperioDevice *self, guint8 cmd, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (!fu_superio_device_wait_for (self, SIO_STATUS_EC_IBF, FALSE, error)) return FALSE; return fu_udev_device_pwrite (FU_UDEV_DEVICE (self), priv->control_port, cmd, error); } static gboolean fu_superio_device_ec_flush (FuSuperioDevice *self, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); guint8 status = 0x00; g_autoptr(GTimer) timer = g_timer_new (); do { guint8 unused = 0; if (!fu_udev_device_pread (FU_UDEV_DEVICE (self), priv->control_port, &status, error)) return FALSE; if ((status & SIO_STATUS_EC_OBF) == 0) break; if (!fu_udev_device_pread (FU_UDEV_DEVICE (self), priv->data_port, &unused, error)) return FALSE; if (g_timer_elapsed (timer, NULL) * 1000.f > priv->timeout_ms) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, "timed out whilst waiting for flush"); return FALSE; } } while (TRUE); return TRUE; } gboolean fu_superio_device_reg_read (FuSuperioDevice *self, guint8 address, guint8 *data, GError **error) { if (!fu_superio_device_ec_write_cmd (self, SIO_CMD_EC_READ, error)) return FALSE; if (!fu_superio_device_ec_write_data (self, address, error)) return FALSE; return fu_superio_device_ec_read_data (self, data, error); } gboolean fu_superio_device_reg_write (FuSuperioDevice *self, guint8 address, guint8 data, GError **error) { if (!fu_superio_device_ec_write_cmd (self, SIO_CMD_EC_WRITE, error)) return FALSE; if (!fu_superio_device_ec_write_data (self, address, error)) return FALSE; return fu_superio_device_ec_write_data (self, data, error); } static gboolean fu_superio_device_probe (FuDevice *device, GError **error) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); g_autofree gchar *devid = NULL; g_autofree gchar *name = NULL; /* use the chipset name as the logical ID and for the GUID */ fu_device_set_logical_id (device, priv->chipset); devid = g_strdup_printf ("SuperIO-%s", priv->chipset); fu_device_add_instance_id (device, devid); name = g_strdup_printf ("SuperIO %s", priv->chipset); fu_device_set_name (FU_DEVICE (self), name); return TRUE; } static gboolean fu_superio_device_setup (FuDevice *device, GError **error) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); /* check ID is correct */ if (!fu_superio_device_check_id (self, error)) { g_prefix_error (error, "failed to probe id: "); return FALSE; } /* discover the data port and control port from PM1 */ if (priv->data_port == 0 && priv->control_port == 0) { /* dump LDNs */ if (g_getenv ("FWUPD_SUPERIO_VERBOSE") != NULL) { for (guint j = 0; j < SIO_LDN_LAST; j++) { if (!fu_superio_device_regdump (self, j, error)) return FALSE; } } /* set Power Management I/F Channel 1 LDN */ if (!fu_superio_device_set_ldn (self, SIO_LDN_PM1, error)) return FALSE; /* get the PM1 IOBAD0 address */ if (!fu_superio_device_io_read16 (self, SIO_LDNxx_IDX_IOBAD0, &priv->data_port, error)) return FALSE; /* get the PM1 IOBAD1 address */ if (!fu_superio_device_io_read16 (self, SIO_LDNxx_IDX_IOBAD1, &priv->control_port, error)) return FALSE; } /* sanity check that EC is usable */ if (!fu_superio_device_wait_for (self, SIO_STATUS_EC_IBF, FALSE, error)) { g_prefix_error (error, "sanity check: "); return FALSE; } /* drain */ if (!fu_superio_device_ec_flush (self, error)) { g_prefix_error (error, "failed to flush: "); return FALSE; } /* dump PMC register map */ if (g_getenv ("FWUPD_SUPERIO_VERBOSE") != NULL) { guint8 buf[0xff] = { 0x00 }; for (guint i = 0x00; i < 0xff; i++) { g_autoptr(GError) error_local = NULL; if (!fu_superio_device_reg_read (self, i, &buf[i], &error_local)) { g_debug ("param: 0x%02x = %s", i, error_local->message); continue; } } fu_common_dump_raw (G_LOG_DOMAIN, "EC Registers", buf, sizeof(buf)); } /* success */ return TRUE; } static FuFirmware * fu_superio_device_prepare_firmware (FuDevice *device, GBytes *fw, FwupdInstallFlags flags, GError **error) { gsize sz = 0; const guint8 *buf = g_bytes_get_data (fw, &sz); const guint8 sig1[] = { 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5 }; const guint8 sig2[] = { 0x85, 0x12, 0x5a, 0x5a, 0xaa }; /* find signature -- maybe ignore byte 0x14 too? */ for (gsize off = 0; off < sz; off += 16) { if (memcmp (&buf[off], sig1, sizeof(sig1)) == 0 && memcmp (&buf[off + 8], sig2, sizeof(sig2)) == 0) { g_debug ("found signature at 0x%04x", (guint) off); return fu_firmware_new_from_bytes (fw); } } g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "did not detect signature in firmware image"); return NULL; } static void fu_superio_device_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (object); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); switch (prop_id) { case PROP_CHIPSET: g_value_set_string (value, priv->chipset); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void fu_superio_device_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (object); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); switch (prop_id) { case PROP_CHIPSET: g_free (priv->chipset); priv->chipset = g_value_dup_string (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static gboolean fu_superio_device_set_quirk_kv (FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (g_strcmp0 (key, "SuperioAutoloadAction") == 0) return TRUE; if (g_strcmp0 (key, "SuperioId") == 0) { guint64 tmp = fu_common_strtoull (value); if (tmp < G_MAXUINT16) { priv->id = tmp; return TRUE; } g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid value"); return FALSE; } if (g_strcmp0 (key, "SuperioPort") == 0) { guint64 tmp = fu_common_strtoull (value); if (tmp < G_MAXUINT16) { priv->port = tmp; return TRUE; } g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid value"); return FALSE; } if (g_strcmp0 (key, "SuperioControlPort") == 0) { guint64 tmp = fu_common_strtoull (value); if (tmp < G_MAXUINT16) { priv->control_port = tmp; return TRUE; } g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid value"); return FALSE; } if (g_strcmp0 (key, "SuperioDataPort") == 0) { guint64 tmp = fu_common_strtoull (value); if (tmp < G_MAXUINT16) { priv->data_port = tmp; return TRUE; } g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid value"); return FALSE; } if (g_strcmp0 (key, "SuperioTimeout") == 0) { guint64 tmp = fu_common_strtoull (value); if (tmp < G_MAXUINT) { priv->timeout_ms = tmp; return TRUE; } g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid value"); return FALSE; } /* failed */ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_superio_device_init (FuSuperioDevice *self) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); priv->timeout_ms = FU_PLUGIN_SUPERIO_DEFAULT_TIMEOUT; fu_device_set_physical_id (FU_DEVICE (self), "/dev/port"); fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_protocol (FU_DEVICE (self), "tw.com.ite.superio"); fu_device_set_summary (FU_DEVICE (self), "Embedded controller"); fu_device_add_icon (FU_DEVICE (self), "computer"); } static void fu_superio_device_finalize (GObject *object) { G_OBJECT_CLASS (fu_superio_device_parent_class)->finalize (object); } static void fu_superio_device_class_init (FuSuperioDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GParamSpec *pspec; FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass); /* properties */ object_class->get_property = fu_superio_device_get_property; object_class->set_property = fu_superio_device_set_property; pspec = g_param_spec_string ("chipset", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME); g_object_class_install_property (object_class, PROP_CHIPSET, pspec); object_class->finalize = fu_superio_device_finalize; klass_device->to_string = fu_superio_device_to_string; klass_device->set_quirk_kv = fu_superio_device_set_quirk_kv; klass_device->probe = fu_superio_device_probe; klass_device->setup = fu_superio_device_setup; klass_device->prepare_firmware = fu_superio_device_prepare_firmware; }