/* * Copyright (C) 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include #include #include "fu-superio-common.h" #include "fu-superio-device.h" #define FU_PLUGIN_SUPERIO_TIMEOUT 0.25 /* s */ /* EC Status Register (see ec/google/chromeec/ec_commands.h) */ #define SIO_STATUS_EC_OBF (1 << 0) /* o/p buffer full */ #define SIO_STATUS_EC_IBF (1 << 1) /* i/p buffer full */ #define SIO_STATUS_EC_IS_BUSY (1 << 2) #define SIO_STATUS_EC_IS_CMD (1 << 3) #define SIO_STATUS_EC_BURST_ENABLE (1 << 4) #define SIO_STATUS_EC_SCI (1 << 5) /* 1 if more events in queue */ /* EC Command Register (see KB3700-ds-01.pdf) */ #define SIO_CMD_EC_READ 0x80 #define SIO_CMD_EC_WRITE 0x81 #define SIO_CMD_EC_BURST_ENABLE 0x82 #define SIO_CMD_EC_BURST_DISABLE 0x83 #define SIO_CMD_EC_QUERY_EVENT 0x84 /* unknown source, IT87 only */ #define SIO_CMD_EC_GET_NAME_STR 0x92 #define SIO_CMD_EC_GET_VERSION_STR 0x93 typedef struct { gint fd; gchar *chipset; guint16 port; guint16 pm1_iobad0; guint16 pm1_iobad1; guint16 id; guint32 size; } FuSuperioDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE (FuSuperioDevice, fu_superio_device, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_superio_device_get_instance_private (o)) static void fu_superio_device_to_string (FuDevice *device, GString *str) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); g_string_append (str, " FuSuperioDevice:\n"); g_string_append_printf (str, " fd:\t\t\t%i\n", priv->fd); g_string_append_printf (str, " chipset:\t\t%s\n", priv->chipset); g_string_append_printf (str, " id:\t\t\t0x%04x\n", (guint) priv->id); g_string_append_printf (str, " port:\t\t0x%04x\n", (guint) priv->port); g_string_append_printf (str, " pm1-iobad0:\t\t0x%04x\n", (guint) priv->pm1_iobad0); g_string_append_printf (str, " pm1-iobad1:\t\t0x%04x\n", (guint) priv->pm1_iobad1); g_string_append_printf (str, " size:\t\t0x%04x\n", (guint) priv->size); } static guint16 fu_superio_device_check_id (FuSuperioDevice *self, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); guint16 id_tmp; /* check ID, which can be done from any LDN */ if (!fu_superio_regval16 (priv->fd, priv->port, 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_superio_inb (priv->fd, priv->pm1_iobad1, &status, error)) return FALSE; if (g_timer_elapsed (timer, NULL) > FU_PLUGIN_SUPERIO_TIMEOUT) 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; } static gboolean fu_superio_device_ec_read (FuSuperioDevice *self, guint16 port, 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_superio_inb (priv->fd, port, data, error); } static gboolean fu_superio_device_ec_write (FuSuperioDevice *self, guint16 port, 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_superio_outb (priv->fd, port, data, 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_superio_inb (priv->fd, priv->pm1_iobad1, &status, error)) return FALSE; if ((status & SIO_STATUS_EC_OBF) == 0) break; if (!fu_superio_inb (priv->fd, priv->pm1_iobad0, &unused, error)) return FALSE; if (g_timer_elapsed (timer, NULL) > FU_PLUGIN_SUPERIO_TIMEOUT) { 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; } static gboolean fu_superio_device_ec_get_param (FuSuperioDevice *self, guint8 param, guint8 *data, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (!fu_superio_device_ec_write (self, priv->pm1_iobad1, SIO_CMD_EC_READ, error)) return FALSE; if (!fu_superio_device_ec_write (self, priv->pm1_iobad0, param, error)) return FALSE; return fu_superio_device_ec_read (self, priv->pm1_iobad0, data, error); } #if 0 static gboolean fu_superio_device_ec_set_param (FuSuperioDevice *self, guint8 param, guint8 data, GError **error) { if (!fu_superio_device_ec_write (self, priv->pm1_iobad1, SIO_CMD_EC_WRITE, error)) return FALSE; if (!fu_superio_device_ec_write (self, priv->pm1_iobad0, param, error)) return FALSE; return fu_superio_device_ec_write (self, priv->pm1_iobad0, data, error); } #endif static gchar * fu_superio_device_ec_get_str (FuSuperioDevice *self, guint8 idx, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); GString *str = g_string_new (NULL); if (!fu_superio_device_ec_write (self, priv->pm1_iobad1, idx, error)) return NULL; for (guint i = 0; i < 0xff; i++) { guint8 c = 0; if (!fu_superio_device_ec_read (self, priv->pm1_iobad0, &c, error)) return NULL; if (c == '$') break; g_string_append_c (str, c); } return g_string_free (str, FALSE); } static gboolean fu_superio_device_open (FuDevice *device, GError **error) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); /* open device */ priv->fd = g_open (fu_device_get_physical_id (device), O_RDWR); if (priv->fd < 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "failed to open %s: %s", fu_device_get_physical_id (device), strerror (errno)); return FALSE; } /* success */ return TRUE; } 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; /* 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); return TRUE; } static gboolean fu_superio_device_setup_it85xx (FuSuperioDevice *self, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); guint8 size_tmp = 0; g_autofree gchar *name = NULL; g_autofree gchar *version = NULL; /* get EC size */ if (!fu_superio_device_ec_flush (self, error)) { g_prefix_error (error, "failed to flush: "); return FALSE; } if (!fu_superio_device_ec_get_param (self, 0xe5, &size_tmp, error)) { g_prefix_error (error, "failed to get EC size: "); return FALSE; } priv->size = ((guint32) size_tmp) << 10; /* get EC strings */ name = fu_superio_device_ec_get_str (self, SIO_CMD_EC_GET_NAME_STR, error); if (name == NULL) { g_prefix_error (error, "failed to get EC name: "); return FALSE; } fu_device_set_name (FU_DEVICE (self), name); version = fu_superio_device_ec_get_str (self, SIO_CMD_EC_GET_VERSION_STR, error); if (version == NULL) { g_prefix_error (error, "failed to get EC version: "); return FALSE; } fu_device_set_version (FU_DEVICE (self), version); return TRUE; } static gboolean fu_superio_device_it89xx_read_ec_register (FuSuperioDevice *self, guint16 addr, guint8 *outval, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (!fu_superio_regwrite (priv->fd, priv->port, SIO_LDNxx_IDX_D2ADR, SIO_DEPTH2_I2EC_ADDRH, error)) return FALSE; if (!fu_superio_regwrite (priv->fd, priv->port, SIO_LDNxx_IDX_D2DAT, addr >> 8, error)) return FALSE; if (!fu_superio_regwrite (priv->fd, priv->port, SIO_LDNxx_IDX_D2ADR, SIO_DEPTH2_I2EC_ADDRL, error)) return FALSE; if (!fu_superio_regwrite (priv->fd, priv->port, SIO_LDNxx_IDX_D2DAT, addr & 0xff, error)) return FALSE; if (!fu_superio_regwrite (priv->fd, priv->port, SIO_LDNxx_IDX_D2ADR, SIO_DEPTH2_I2EC_DATA, error)) return FALSE; return fu_superio_regval (priv->fd, priv->port, SIO_LDNxx_IDX_D2DAT, outval, error); } static gboolean fu_superio_device_it89xx_ec_size (FuSuperioDevice *self, GError **error) { FuSuperioDevicePrivate *priv = GET_PRIVATE (self); guint8 tmp = 0; /* not sure why we can't just use SIO_LDNxx_IDX_CHIPID1, * but lets do the same as the vendor flash tool... */ if (!fu_superio_device_it89xx_read_ec_register (self, GCTRL_ECHIPID1, &tmp, error)) return FALSE; if (tmp == 0x85) { g_warning ("possibly IT85xx class device"); priv->size = 0x20000; return TRUE; } /* can't we just use SIO_LDNxx_IDX_CHIPVER... */ if (!fu_superio_device_it89xx_read_ec_register (self, GCTRL_ECHIPVER, &tmp, error)) return FALSE; if (tmp >> 4 == 0x00) { priv->size = 0x20000; return TRUE; } if (tmp >> 4 == 0x04) { priv->size = 0x30000; return TRUE; } if (tmp >> 4 == 0x08) { priv->size = 0x40000; return TRUE; } g_warning ("falling back to default size"); priv->size = 0x20000; return TRUE; } static gboolean fu_superio_device_setup_it89xx (FuSuperioDevice *self, GError **error) { guint8 version_tmp[2] = { 0x00 }; g_autofree gchar *version = NULL; /* get version */ if (!fu_superio_device_ec_flush (self, error)) { g_prefix_error (error, "failed to flush: "); return FALSE; } if (!fu_superio_device_ec_get_param (self, 0x00, &version_tmp[0], error)) { g_prefix_error (error, "failed to get version major: "); return FALSE; } if (!fu_superio_device_ec_get_param (self, 0x01, &version_tmp[1], error)) { g_prefix_error (error, "failed to get version minor: "); return FALSE; } version = g_strdup_printf ("%02u.%02u", version_tmp[0], version_tmp[1]); fu_device_set_version (FU_DEVICE (self), version); /* get size from the EC */ if (!fu_superio_device_it89xx_ec_size (self, error)) return FALSE; /* success */ 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; } /* dump LDNs */ if (g_getenv ("FWUPD_SUPERIO_VERBOSE") != NULL) { for (guint j = 0; j < SIO_LDN_LAST; j++) { if (!fu_superio_regdump (priv->fd, priv->port, j, error)) return FALSE; } } /* set Power Management I/F Channel 1 LDN */ if (!fu_superio_set_ldn (priv->fd, priv->port, SIO_LDN_PM1, error)) return FALSE; /* get the PM1 IOBAD0 address */ if (!fu_superio_regval16 (priv->fd, priv->port, SIO_LDNxx_IDX_IOBAD0, &priv->pm1_iobad0, error)) return FALSE; /* get the PM1 IOBAD1 address */ if (!fu_superio_regval16 (priv->fd, priv->port, SIO_LDNxx_IDX_IOBAD1, &priv->pm1_iobad1, error)) 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_ec_get_param (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, 0x100); } /* IT85xx */ if (priv->id >> 8 == 0x85) { if (!fu_superio_device_setup_it85xx (self, error)) return FALSE; } /* IT89xx */ if (priv->id >> 8 == 0x89) { if (!fu_superio_device_setup_it89xx (self, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_superio_device_attach (FuDevice *device, GError **error) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); /* re-enable HOSTWA -- use 0xfd for LCFC */ if (!fu_superio_device_ec_write (self, priv->pm1_iobad1, 0xfc, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_superio_device_detach (FuDevice *device, GError **error) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); guint8 tmp = 0x00; /* turn off HOSTWA bit, keeping HSEMIE and HSEMW high */ if (!fu_superio_device_ec_write (self, priv->pm1_iobad1, 0xdc, error)) return FALSE; if (!fu_superio_device_ec_read (self, priv->pm1_iobad0, &tmp, error)) return FALSE; if (tmp != 0x33) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "failed to clear HOSTWA, got 0x%02x, expected 0x33", tmp); return FALSE; } /* success */ return TRUE; } static gboolean fu_superio_device_close (FuDevice *device, GError **error) { FuSuperioDevice *self = FU_SUPERIO_DEVICE (device); FuSuperioDevicePrivate *priv = GET_PRIVATE (self); if (!g_close (priv->fd, error)) return FALSE; priv->fd = 0; return TRUE; } static void fu_superio_device_init (FuSuperioDevice *self) { fu_device_set_physical_id (FU_DEVICE (self), "/dev/port"); fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_summary (FU_DEVICE (self), "SuperIO device"); 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); FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass); object_class->finalize = fu_superio_device_finalize; klass_device->to_string = fu_superio_device_to_string; klass_device->open = fu_superio_device_open; klass_device->attach = fu_superio_device_attach; klass_device->detach = fu_superio_device_detach; klass_device->probe = fu_superio_device_probe; klass_device->setup = fu_superio_device_setup; klass_device->close = fu_superio_device_close; } FuSuperioDevice * fu_superio_device_new (const gchar *chipset, guint16 id, guint16 port) { FuSuperioDevice *self; FuSuperioDevicePrivate *priv; self = g_object_new (FU_TYPE_SUPERIO_DEVICE, NULL); priv = GET_PRIVATE (self); priv->chipset = g_strdup (chipset); priv->id = id; priv->port = port; return self; }