fwupd/plugins/realtek-mst/fu-realtek-mst-device.c
2021-07-10 13:51:28 +01:00

868 lines
28 KiB
C

/*
* Copyright (C) 2021 Peter Marheine <pmarheine@chromium.org>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#include <fcntl.h>
#include <glib/gstdio.h>
#include <linux/i2c-dev.h>
#include "fu-realtek-mst-device.h"
/* firmware debug address */
#define I2C_ADDR_DEBUG 0x35
/* programming address */
#define I2C_ADDR_ISP 0x4a
/* some kind of operation attribute bits */
#define REG_CMD_ATTR 0x60
/* write set to begin executing, cleared when done */
#define CMD_ERASE_BUSY 0x01
/* 24-bit address for commands */
#define REG_CMD_ADDR_HI 0x64
#define REG_CMD_ADDR_MID 0x65
#define REG_CMD_ADDR_LO 0x66
/* register for erase commands */
#define REG_ERASE_OPCODE 0x61
#define CMD_OPCODE_ERASE_SECTOR 0x20
#define CMD_OPCODE_ERASE_BLOCK 0xD8
/* register for read commands */
#define REG_READ_OPCODE 0x6A
#define CMD_OPCODE_READ 0x03
/* register for write commands */
#define REG_WRITE_OPCODE 0x6D
#define CMD_OPCODE_WRITE 0x02
/* mode register address */
#define REG_MCU_MODE 0x6F
/* when bit is set in mode register, ISP mode is active */
#define MCU_MODE_ISP (1 << 7)
/* write set to begin write, reset by device when complete */
#define MCU_MODE_WRITE_BUSY (1 << 5)
/* when bit is clear, write buffer contains data */
#define MCU_MODE_WRITE_BUF (1 << 4)
/* write data into write buffer */
#define REG_WRITE_FIFO 0x70
/* number of bytes to write minus 1 (0xff means 256 bytes) */
#define REG_WRITE_LEN 0x71
/* Indirect registers allow access to registers with 16-bit addresses. Write
* 0x9F to the LO register, then the top byte of the address to HI, the
* bottom byte of the address to LO, then read or write HI to read or write
* the value of the target register. */
#define REG_INDIRECT_LO 0xF4
#define REG_INDIRECT_HI 0xF5
/* GPIO configuration/access registers */
#define REG_GPIO88_CONFIG 0x104F
#define REG_GPIO88_VALUE 0xFE3F
/* flash chip properties */
#define FLASH_SIZE 0x100000
#define FLASH_SECTOR_SIZE 4096
#define FLASH_BLOCK_SIZE 65536
/* MST flash layout */
#define FLASH_USER1_ADDR 0x10000
#define FLASH_FLAG1_ADDR 0xfe304
#define FLASH_USER2_ADDR 0x80000
#define FLASH_FLAG2_ADDR 0xff304
#define FLASH_USER_SIZE 0x70000
enum dual_bank_mode {
DUAL_BANK_USER_ONLY = 0,
DUAL_BANK_DIFF = 1,
DUAL_BANK_COPY = 2,
DUAL_BANK_USER_ONLY_FLAG = 3,
DUAL_BANK_MAX_VALUE = 3,
};
enum flash_bank {
FLASH_BANK_BOOT = 0,
FLASH_BANK_USER1 = 1,
FLASH_BANK_USER2 = 2,
FLASH_BANK_MAX_VALUE = 2,
FLASH_BANK_INVALID = 255,
};
struct dual_bank_info {
gboolean is_enabled;
enum dual_bank_mode mode;
enum flash_bank active_bank;
guint8 user1_version[2];
guint8 user2_version[2];
};
struct _FuRealtekMstDevice {
FuI2cDevice parent_instance;
gchar *dp_aux_dev_name;
enum flash_bank active_bank;
};
G_DEFINE_TYPE (FuRealtekMstDevice, fu_realtek_mst_device, FU_TYPE_I2C_DEVICE)
static gboolean
fu_realtek_mst_device_set_quirk_kv (FuDevice *device,
const gchar *key,
const gchar *value,
GError **error)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (device);
if (g_strcmp0 (key, "RealtekMstDpAuxName") == 0) {
self->dp_aux_dev_name = g_strdup (value);
} else {
g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
"unsupported quirk key: %s", key);
return FALSE;
}
return TRUE;
}
static gboolean
fu_realtek_mst_device_override_dev (FuRealtekMstDevice *self, GError **error)
{
gboolean found = FALSE;
g_autoptr(GUdevClient) udev_client = g_udev_client_new (NULL);
g_autoptr(GUdevEnumerator) udev_enumerator = g_udev_enumerator_new (udev_client);
g_autoptr(GList) matches = NULL;
g_udev_enumerator_add_match_subsystem (udev_enumerator, "drm_dp_aux_dev");
g_udev_enumerator_add_match_sysfs_attr (udev_enumerator, "name",
self->dp_aux_dev_name);
matches = g_udev_enumerator_execute (udev_enumerator);
/* from a drm_dp_aux_dev with the given name, locate its sibling i2c
* device and in turn the i2c-dev under that representing the actual
* I2C bus that runs over DPDDC on the port represented by the
* drm_dp_aux_dev */
for (GList *element = matches; element != NULL; element = element->next) {
g_autoptr(FuUdevDevice) device = fu_udev_device_new (element->data);
g_autoptr(GPtrArray) i2c_devices = NULL;
if (found) {
g_debug ("Ignoring additional aux device %s",
fu_udev_device_get_sysfs_path (device));
continue;
}
i2c_devices = fu_udev_device_get_siblings_with_subsystem (device, "i2c");
for (guint i = 0; i < i2c_devices->len; i++) {
FuUdevDevice *i2c_device = g_ptr_array_index (i2c_devices, i);
FuUdevDevice *bus_device;
g_autoptr(GPtrArray) i2c_buses =
fu_udev_device_get_children_with_subsystem (i2c_device, "i2c-dev");
if (i2c_buses->len == 0) {
g_debug ("no i2c-dev found under %s",
fu_udev_device_get_sysfs_path (i2c_device));
continue;
}
if (i2c_buses->len > 1) {
g_debug ("ignoring %u additional i2c-dev under %s",
i2c_buses->len - 1,
fu_udev_device_get_sysfs_path (i2c_device));
}
bus_device = g_ptr_array_index (i2c_buses, 0);
g_debug ("Found I2C bus at %s, using this device",
fu_udev_device_get_sysfs_path (bus_device));
fu_udev_device_set_dev (FU_UDEV_DEVICE (self),
fu_udev_device_get_dev (bus_device));
found = TRUE;
}
}
if (!found) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"did not find an i2c-dev associated with DP aux \"%s\"",
self->dp_aux_dev_name);
return FALSE;
}
return TRUE;
}
static gboolean
mst_ensure_device_address (FuRealtekMstDevice *self, guint8 address, GError **error)
{
return fu_udev_device_ioctl (FU_UDEV_DEVICE (self), I2C_SLAVE,
(guint8 *) (guintptr) address, NULL, error);
}
/** Write a value to a device register */
static gboolean
mst_write_register (FuRealtekMstDevice *self, guint8 address, guint8 value, GError **error)
{
const guint8 command[] = { address, value };
return fu_i2c_device_write_full (FU_I2C_DEVICE (self), command,
sizeof(command), error);
}
static gboolean
mst_write_register_multi (FuRealtekMstDevice *self, guint8 address,
const guint8 *data, gsize count, GError **error)
{
g_autofree guint8 *command = g_malloc0 (count + 1);
memcpy (command + 1, data, count);
command[0] = address;
return fu_i2c_device_write_full (FU_I2C_DEVICE (self),
command, count + 1, error);
}
/** Read a register from the device */
static gboolean
mst_read_register (FuRealtekMstDevice *self,
guint8 address,
guint8 *value,
GError **error)
{
if (!fu_i2c_device_write (FU_I2C_DEVICE (self), address, error))
return FALSE;
return fu_i2c_device_read (FU_I2C_DEVICE (self), value, error);
}
static gboolean
mst_set_indirect_address (FuRealtekMstDevice *self, guint16 address, GError **error)
{
if (!mst_write_register (self, REG_INDIRECT_LO, 0x9F, error))
return FALSE;
if (!mst_write_register (self, REG_INDIRECT_HI, address >> 8, error))
return FALSE;
return mst_write_register (self, REG_INDIRECT_LO, address, error);
}
static gboolean
mst_read_register_indirect (FuRealtekMstDevice *self, guint16 address, guint8 *value, GError **error)
{
if (!mst_set_indirect_address (self, address, error))
return FALSE;
return mst_read_register (self, REG_INDIRECT_HI, value, error);
}
static gboolean
mst_write_register_indirect (FuRealtekMstDevice *self, guint16 address, guint8 value, GError **error)
{
if (!mst_set_indirect_address (self, address, error))
return FALSE;
return mst_write_register (self, REG_INDIRECT_HI, value, error);
}
/**
* Wait until a device register reads an expected value.
*
* Waiting up to @timeout_seconds, poll the given @address for the read value
* bitwise-ANDed with @mask to be equal to @expected.
*
* Returns an error if the timeout expires or in case of an I/O error.
*/
static gboolean
mst_poll_register (FuRealtekMstDevice *self,
guint8 address,
guint8 mask,
guint8 expected,
guint timeout_seconds,
GError **error)
{
guint8 value;
g_autoptr(GTimer) timer = g_timer_new ();
if (!mst_read_register (self, address, &value, error))
return FALSE;
while ((value & mask) != expected
&& g_timer_elapsed (timer, NULL) <= timeout_seconds) {
g_usleep(G_TIME_SPAN_MILLISECOND);
if (!mst_read_register (self, address, &value, error))
return FALSE;
}
if ((value & mask) == expected)
return TRUE;
g_set_error (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT,
"register %x still reads %x after %us, wanted %x (mask %x)",
address, value, timeout_seconds, expected, mask);
return FALSE;
}
static gboolean
mst_set_gpio88 (FuRealtekMstDevice *self, gboolean level, GError **error)
{
guint8 value;
/* ensure pin is configured as push-pull GPIO */
if (!mst_read_register_indirect (self, REG_GPIO88_CONFIG, &value, error))
return FALSE;
if (!mst_write_register_indirect (self,
REG_GPIO88_CONFIG,
(value & 0xF0) | 1,
error))
return FALSE;
/* set output level */
g_debug ("set pin 88 = %d", level);
if (!mst_read_register_indirect (self, REG_GPIO88_VALUE, &value, error))
return FALSE;
return mst_write_register_indirect (self, REG_GPIO88_VALUE,
(value & 0xFE) | (level != FALSE),
error);
}
static gboolean
fu_realtek_mst_device_probe (FuDevice *device, GError **error)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (device);
FuContext *context = fu_device_get_context (device);
const gchar *hardware_family = NULL;
const gchar *quirk_name = NULL;
g_autofree gchar *family_instance_id = NULL;
g_autofree gchar *instance_id = NULL;
/* set custom instance ID and load matching quirks */
instance_id = g_strdup_printf ("REALTEK-MST\\NAME_%s",
fu_udev_device_get_sysfs_attr (
FU_UDEV_DEVICE (device),
"name",
NULL));
fu_device_add_instance_id (device, instance_id);
hardware_family = fu_context_get_hwid_value (context, FU_HWIDS_KEY_FAMILY);
family_instance_id = g_strdup_printf ("%s&FAMILY_%s", instance_id, hardware_family);
fu_device_add_instance_id_full (device, family_instance_id,
FU_DEVICE_INSTANCE_FLAG_ONLY_QUIRKS);
/* having loaded quirks, check this device is supported */
quirk_name = fu_device_get_name (device);
if (g_strcmp0 (quirk_name, "RTD2142") != 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"only RTD2142 is supported");
return FALSE;
}
if (self->dp_aux_dev_name == NULL) {
g_set_error_literal (error, FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"RealtekMstDpAuxName must be specified");
return FALSE;
}
/* locate its sibling i2c device and use that instead */
if (!fu_realtek_mst_device_override_dev (self, error))
return FALSE;
/* FuI2cDevice */
if (!FU_DEVICE_CLASS (fu_realtek_mst_device_parent_class)->probe (device, error))
return FALSE;
/* success */
return TRUE;
}
static gboolean
fu_realtek_mst_device_get_dual_bank_info (FuRealtekMstDevice *self,
struct dual_bank_info *info,
GError **error)
{
guint8 response[11] = { 0x0 };
if (!mst_ensure_device_address (self, I2C_ADDR_DEBUG, error))
return FALSE;
/* switch to DDCCI mode */
if (!mst_write_register (self, 0xca, 0x09, error))
return FALSE;
/* wait for mode switch to complete */
g_usleep (200 * G_TIME_SPAN_MILLISECOND);
/* request dual bank state and read back */
if (!fu_i2c_device_write (FU_I2C_DEVICE (self), 0x01, error))
return FALSE;
if (!fu_i2c_device_read_full (FU_I2C_DEVICE (self), response, sizeof(response), error))
return FALSE;
if (response[0] != 0xca || response[1] != 9) {
/* unexpected response code or length usually means the current
* firmware doesn't support dual-bank mode at all */
g_debug ("unexpected response code %#x, length %d",
response[0], response[1]);
info->is_enabled = FALSE;
return TRUE;
}
/* enable flag, assume anything other than 1 is unsupported */
if (response[2] != 1) {
info->is_enabled = FALSE;
return TRUE;
}
info->is_enabled = TRUE;
info->mode = response[3];
if (info->mode > DUAL_BANK_MAX_VALUE) {
g_debug ("unexpected dual bank mode value %#x", info->mode);
info->is_enabled = FALSE;
return TRUE;
}
info->active_bank = response[4];
if (info->active_bank > FLASH_BANK_MAX_VALUE) {
g_debug ("unexpected active flash bank value %#x",
info->active_bank);
info->is_enabled = FALSE;
return TRUE;
}
info->user1_version[0] = response[5];
info->user1_version[1] = response[6];
info->user2_version[0] = response[7];
info->user2_version[1] = response[8];
/* last two bytes of response are reserved */
return TRUE;
}
static gboolean
fu_realtek_mst_device_probe_version (FuDevice *device, GError **error)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (device);
struct dual_bank_info info = { 0x0 };
guint8 *active_version;
g_autofree gchar *version_str = NULL;
/* ensure probed state is cleared in case of error */
fu_device_remove_flag (device, FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_remove_flag (device, FWUPD_DEVICE_FLAG_DUAL_IMAGE);
self->active_bank = FLASH_BANK_INVALID;
fu_device_set_version (device, NULL);
if (!fu_realtek_mst_device_get_dual_bank_info (FU_REALTEK_MST_DEVICE (self),
&info, error))
return FALSE;
if (!info.is_enabled) {
g_debug ("dual-bank mode is not enabled");
return TRUE;
}
if (info.mode != DUAL_BANK_DIFF) {
g_debug ("can only update from dual-bank-diff mode");
return TRUE;
}
/* dual-bank mode seems to be fully supported, so we can update
* regardless of the active bank- if it's FLASH_BANK_BOOT, updating is
* possible even if the current version is unknown */
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_DUAL_IMAGE);
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_UPDATABLE);
g_debug ("device is currently running from bank %u", info.active_bank);
g_return_val_if_fail (info.active_bank <= FLASH_BANK_MAX_VALUE, FALSE);
self->active_bank = info.active_bank;
g_debug ("firmware version reports user1 %d.%d, user2 %d.%d",
info.user1_version[0], info.user1_version[1],
info.user2_version[0], info.user2_version[1]);
if (info.active_bank == FLASH_BANK_USER1)
active_version = info.user1_version;
else if (info.active_bank == FLASH_BANK_USER2)
active_version = info.user2_version;
else
/* only user bank versions are reported, can't tell otherwise */
return TRUE;
version_str = g_strdup_printf ("%u.%u", active_version[0], active_version[1]);
fu_device_set_version (FU_DEVICE (self), version_str);
return TRUE;
}
static gboolean
flash_iface_read (FuRealtekMstDevice *self,
guint32 address, guint8 *buf, const gsize buf_size,
GError **error)
{
gsize bytes_read = 0;
guint8 byte;
g_return_val_if_fail (address < FLASH_SIZE, FALSE);
g_return_val_if_fail (buf_size <= FLASH_SIZE, FALSE);
g_debug ("read %#" G_GSIZE_MODIFIER "x bytes from %#08x", buf_size, address);
/* read must start one byte prior to the desired address and ignore the
* first byte of data, since the first read value is unpredictable */
address = (address - 1) & 0xFFFFFF;
if (!mst_write_register (self, REG_CMD_ADDR_HI, address >> 16, error))
return FALSE;
if (!mst_write_register (self, REG_CMD_ADDR_MID, address >> 8, error))
return FALSE;
if (!mst_write_register (self, REG_CMD_ADDR_LO, address, error))
return FALSE;
if (!mst_write_register (self, REG_READ_OPCODE, CMD_OPCODE_READ, error))
return FALSE;
/* ignore first byte of data */
if (!fu_i2c_device_write (FU_I2C_DEVICE (self), 0x70, error))
return FALSE;
if (!fu_i2c_device_read (FU_I2C_DEVICE (self), &byte, error))
return FALSE;
while (bytes_read < buf_size) {
/* read up to 256 bytes in one transaction */
gsize read_len = buf_size - bytes_read;
if (read_len > 256)
read_len = 256;
if (!fu_i2c_device_read_full (FU_I2C_DEVICE (self),
buf + bytes_read, read_len,
error))
return FALSE;
bytes_read += read_len;
fu_device_set_progress_full (FU_DEVICE (self), bytes_read, buf_size);
}
return TRUE;
}
static gboolean
flash_iface_erase_sector (FuRealtekMstDevice *self, guint32 address,
GError **error)
{
/* address must be 4k-aligned */
g_return_val_if_fail ((address & 0xFFF) == 0, FALSE);
g_debug ("sector erase %#08x-%#08x", address, address + FLASH_SECTOR_SIZE);
/* sector address */
if (!mst_write_register (self, REG_CMD_ADDR_HI, address >> 16, error))
return FALSE;
if (!mst_write_register (self, REG_CMD_ADDR_MID, address >> 8, error))
return FALSE;
if (!mst_write_register (self, REG_CMD_ADDR_LO, address, error))
return FALSE;
/* command type + WREN */
if (!mst_write_register (self, REG_CMD_ATTR, 0xB8, error))
return FALSE;
/* sector erase opcode */
if (!mst_write_register (self, REG_ERASE_OPCODE, CMD_OPCODE_ERASE_SECTOR, error))
return FALSE;
/* begin operation and wait for completion */
if (!mst_write_register (self, REG_CMD_ATTR, 0xB8 | CMD_ERASE_BUSY, error))
return FALSE;
return mst_poll_register (self, REG_CMD_ATTR, CMD_ERASE_BUSY, 0, 10, error);
}
static gboolean
flash_iface_erase_block (FuRealtekMstDevice *self, guint32 address, GError **error)
{
/* address must be 64k-aligned */
g_return_val_if_fail ((address & 0xFFFF) == 0, FALSE);
g_debug ("block erase %#08x-%#08x", address, address + FLASH_BLOCK_SIZE);
/* block address */
if (!mst_write_register (self, REG_CMD_ADDR_HI, address >> 16, error))
return FALSE;
if (!mst_write_register (self, REG_CMD_ADDR_MID, 0, error))
return FALSE;
if (!mst_write_register (self, REG_CMD_ADDR_LO, 0, error))
return FALSE;
/* command type + WREN */
if (!mst_write_register (self, REG_CMD_ATTR, 0xB8, error))
return FALSE;
/* block erase opcode */
if (!mst_write_register (self, REG_ERASE_OPCODE, CMD_OPCODE_ERASE_BLOCK, error))
return FALSE;
/* begin operation and wait for completion */
if (!mst_write_register (self, REG_CMD_ATTR, 0xB8 | CMD_ERASE_BUSY, error))
return FALSE;
return mst_poll_register (self, REG_CMD_ATTR, CMD_ERASE_BUSY, 0, 10, error);
}
static gboolean
flash_iface_write (FuRealtekMstDevice *self, guint32 address,
GBytes *data, GError **error)
{
gsize bytes_written = 0;
gsize total_size = g_bytes_get_size (data);
g_autoptr(GPtrArray) chunks = fu_chunk_array_new_from_bytes (data, address, 0, 256);
g_debug ("write %#" G_GSIZE_MODIFIER "x bytes at %#08x", total_size, address);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chunk = g_ptr_array_index (chunks, i);
guint32 chunk_address = fu_chunk_get_address (chunk);
guint32 chunk_size = fu_chunk_get_data_sz (chunk);
/* write opcode */
if (!mst_write_register (self, REG_WRITE_OPCODE, CMD_OPCODE_WRITE, error))
return FALSE;
/* write length */
if (!mst_write_register (self, REG_WRITE_LEN, chunk_size - 1, error))
return FALSE;
/* target address */
if (!mst_write_register (self, REG_CMD_ADDR_HI, chunk_address >> 16, error))
return FALSE;
if (!mst_write_register (self, REG_CMD_ADDR_MID, chunk_address >> 8, error))
return FALSE;
if (!mst_write_register (self, REG_CMD_ADDR_LO, chunk_address, error))
return FALSE;
/* ensure write buffer is empty */
if (!mst_poll_register (self, REG_MCU_MODE, MCU_MODE_WRITE_BUF, MCU_MODE_WRITE_BUF, 10, error)) {
g_prefix_error (error, "failed waiting for write buffer to clear: ");
return FALSE;
}
/* write data into FIFO */
if (!mst_write_register_multi (self, REG_WRITE_FIFO,
fu_chunk_get_data (chunk),
chunk_size, error))
return FALSE;
/* begin operation and wait for completion */
if (!mst_write_register (self, REG_MCU_MODE, MCU_MODE_ISP | MCU_MODE_WRITE_BUSY, error))
return FALSE;
if (!mst_poll_register (self, REG_MCU_MODE, MCU_MODE_WRITE_BUSY, 0, 10, error)) {
g_prefix_error (error,
"timed out waiting for write at %#x to complete: ",
address);
return FALSE;
}
bytes_written += chunk_size;
fu_device_set_progress_full (FU_DEVICE (self), bytes_written, total_size);
}
return TRUE;
}
static gboolean
fu_realtek_mst_device_detach (FuDevice *device, GError **error)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (device);
if (!mst_ensure_device_address (self, I2C_ADDR_ISP, error))
return FALSE;
/* Switch to programming mode (stops regular operation) */
fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
if (!mst_write_register (self, REG_MCU_MODE, MCU_MODE_ISP, error))
return FALSE;
g_debug ("wait for ISP mode ready");
if (!mst_poll_register (self, REG_MCU_MODE, MCU_MODE_ISP, MCU_MODE_ISP, 60, error))
return FALSE;
/* magic value makes the MCU clock run faster than normal; this both
* helps programming performance and fixes flakiness where register
* writes sometimes get nacked for no apparent reason */
if (!mst_write_register_indirect (self, 0x06A0, 0x74, error))
return FALSE;
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
fu_device_set_status (device, FWUPD_STATUS_IDLE);
/* Disable hardware write protect, assuming Flash ~WP is connected to
* device pin 88, a GPIO. */
return mst_set_gpio88 (self, 1, error);
}
static gboolean
fu_realtek_mst_device_write_firmware (FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (device);
/* write an inactive bank: USER2 if USER1 is active, otherwise USER1
* (including if the boot bank is active) */
guint32 base_addr = self->active_bank == FLASH_BANK_USER1 ? FLASH_USER2_ADDR : FLASH_USER1_ADDR;
guint32 flag_addr = self->active_bank == FLASH_BANK_USER1 ? FLASH_FLAG2_ADDR : FLASH_FLAG1_ADDR;
GBytes *firmware_bytes = fu_firmware_get_bytes (firmware, error);
const guint8 flag_data[] = {0xaa, 0xaa, 0xaa, 0xff, 0xff};
g_autofree guint8 *readback_buf = g_malloc0 (FLASH_USER_SIZE);
g_return_val_if_fail (g_bytes_get_size (firmware_bytes) == FLASH_USER_SIZE, FALSE);
if (!mst_ensure_device_address (self, I2C_ADDR_ISP, error))
return FALSE;
/* erase old image */
g_debug ("erase old image from %#x", base_addr);
fu_device_set_status (device, FWUPD_STATUS_DEVICE_ERASE);
for (guint32 offset = 0; offset < FLASH_USER_SIZE; offset += FLASH_BLOCK_SIZE) {
fu_device_set_progress_full (device, offset, FLASH_USER_SIZE);
if (!flash_iface_erase_block (self, base_addr + offset, error))
return FALSE;
}
/* write new image */
g_debug ("write new image to %#x", base_addr);
fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
if (!flash_iface_write (self, base_addr, firmware_bytes, error))
return FALSE;
fu_device_set_status (device, FWUPD_STATUS_DEVICE_VERIFY);
if (!flash_iface_read (self, base_addr, readback_buf, FLASH_USER_SIZE, error))
return FALSE;
if (memcmp (g_bytes_get_data (firmware_bytes, NULL), readback_buf, FLASH_USER_SIZE) != 0) {
g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_WRITE,
"flash contents after write do not match firmware image");
return FALSE;
}
/* Erase old flag and write new one. The MST appears to modify the
* flag value once booted, so we always write the same value here and
* it picks up what we've updated. */
fu_device_set_status (device, FWUPD_STATUS_DEVICE_ERASE);
if (!flash_iface_erase_sector (self, flag_addr & ~(FLASH_SECTOR_SIZE - 1), error))
return FALSE;
fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
return flash_iface_write (self, flag_addr,
g_bytes_new_static (flag_data, sizeof(flag_data)),
error);
}
static FuFirmware*
fu_realtek_mst_device_read_firmware (FuDevice *device, GError **error)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (device);
guint32 bank_address;
g_autofree guint8 *image_bytes = NULL;
if (self->active_bank == FLASH_BANK_USER1)
bank_address = FLASH_USER1_ADDR;
else if (self->active_bank == FLASH_BANK_USER2)
bank_address = FLASH_USER2_ADDR;
else {
g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
"cannot read firmware from bank %u",
self->active_bank);
return NULL;
}
image_bytes = g_malloc0 (FLASH_USER_SIZE);
if (!mst_ensure_device_address (self, I2C_ADDR_ISP, error))
return NULL;
if (!flash_iface_read (self, bank_address, image_bytes, FLASH_USER_SIZE, error))
return NULL;
return fu_firmware_new_from_bytes(g_bytes_new_take (g_steal_pointer (&image_bytes), FLASH_USER_SIZE));
}
static GBytes*
fu_realtek_mst_device_dump_firmware (FuDevice *device, GError **error)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (device);
g_autofree guint8 *flash_contents = g_malloc0 (FLASH_SIZE);
if (!mst_ensure_device_address (self, I2C_ADDR_ISP, error))
return NULL;
fu_device_set_status (device, FWUPD_STATUS_DEVICE_READ);
if (!flash_iface_read (self, 0, flash_contents, FLASH_SIZE, error))
return NULL;
fu_device_set_status (device, FWUPD_STATUS_IDLE);
return g_bytes_new_take (g_steal_pointer (&flash_contents), FLASH_SIZE);
}
static gboolean
fu_realtek_mst_device_attach (FuDevice *device, GError **error)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (device);
guint8 value;
if (!mst_ensure_device_address (self, I2C_ADDR_ISP, error))
return FALSE;
/* re-enable hardware write protect via GPIO */
if (!mst_set_gpio88 (self, 0, error))
return FALSE;
if (!mst_read_register (self, REG_MCU_MODE, &value, error))
return FALSE;
if ((value & MCU_MODE_ISP) != 0) {
g_autoptr(GError) error_local = NULL;
g_debug ("resetting device to exit ISP mode");
fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
/* Set register EE bit 2 to request reset. This write can fail
* spuriously, so we ignore the write result and verify the device is
* no longer in programming mode after giving it time to reset. */
if (!mst_read_register (self, 0xEE, &value, error))
return FALSE;
if (!mst_write_register (self, 0xEE, value | 2, &error_local)) {
g_debug ("write spuriously failed, ignoring: %s",
error_local->message);
}
/* allow device some time to reset */
g_usleep (G_USEC_PER_SEC);
/* verify device has exited programming mode and actually reset */
if (!mst_read_register (self, REG_MCU_MODE, &value, error))
return FALSE;
if ((value & MCU_MODE_ISP) == MCU_MODE_ISP) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NEEDS_USER_ACTION,
"device failed to reset when requested");
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN);
return FALSE;
}
} else {
g_debug ("device is already in normal mode");
}
fu_device_remove_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
fu_device_set_status (device, FWUPD_STATUS_IDLE);
return TRUE;
}
static void
fu_realtek_mst_device_init (FuRealtekMstDevice *self)
{
self->active_bank = FLASH_BANK_INVALID;
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_INTERNAL);
fu_device_set_version_format (FU_DEVICE (self), FWUPD_VERSION_FORMAT_PAIR);
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE);
fu_device_add_protocol (FU_DEVICE (self), "com.realtek.rtd2142");
fu_device_set_vendor (FU_DEVICE (self), "Realtek");
fu_device_add_vendor_id (FU_DEVICE (self), "PCI:0x10EC");
fu_device_set_summary (FU_DEVICE (self), "DisplayPort MST hub");
fu_device_add_icon (FU_DEVICE (self), "video-display");
fu_device_set_firmware_size (FU_DEVICE (self), FLASH_USER_SIZE);
}
static void
fu_realtek_mst_device_finalize (GObject *object)
{
FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE (object);
g_free (self->dp_aux_dev_name);
G_OBJECT_CLASS (fu_realtek_mst_device_parent_class)->finalize (object);
}
static void
fu_realtek_mst_device_class_init (FuRealtekMstDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
GObjectClass *klass_object = G_OBJECT_CLASS(klass);
klass_object->finalize = fu_realtek_mst_device_finalize;
klass_device->probe = fu_realtek_mst_device_probe;
klass_device->set_quirk_kv = fu_realtek_mst_device_set_quirk_kv;
klass_device->setup = fu_realtek_mst_device_probe_version;
klass_device->detach = fu_realtek_mst_device_detach;
klass_device->attach = fu_realtek_mst_device_attach;
klass_device->write_firmware = fu_realtek_mst_device_write_firmware;
klass_device->reload = fu_realtek_mst_device_probe_version;
/* read active image */
klass_device->read_firmware = fu_realtek_mst_device_read_firmware;
/* dump whole flash */
klass_device->dump_firmware = fu_realtek_mst_device_dump_firmware;
}