fwupd/plugins/dell-dock/fu-dell-dock-hid.c
2021-08-24 11:18:40 -05:00

490 lines
13 KiB
C

/*
* Copyright (C) 2018 Realtek Semiconductor Corporation
* Copyright (C) 2018 Dell Inc.
* All rights reserved.
*
* This software and associated documentation (if any) is furnished
* under a license and may only be used or copied in accordance
* with the terms of the license.
*
* This file is provided under a dual MIT/LGPLv2 license. When using or
* redistributing this file, you may do so under either license.
* Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement.
*
* SPDX-License-Identifier: LGPL-2.1+ OR MIT
*/
#include "config.h"
#include <fwupdplugin.h>
#include <errno.h>
#include <string.h>
#include "fu-dell-dock-hid.h"
#define HIDI2C_MAX_REGISTER 4
#define HID_MAX_RETRIES 5
#define TBT_MAX_RETRIES 2
#define HIDI2C_TRANSACTION_TIMEOUT 2000
#define HUB_CMD_READ_DATA 0xC0
#define HUB_CMD_WRITE_DATA 0x40
#define HUB_EXT_READ_STATUS 0x09
#define HUB_EXT_MCUMODIFYCLOCK 0x06
#define HUB_EXT_I2C_WRITE 0xC6
#define HUB_EXT_WRITEFLASH 0xC8
#define HUB_EXT_I2C_READ 0xD6
#define HUB_EXT_VERIFYUPDATE 0xD9
#define HUB_EXT_ERASEBANK 0xE8
#define HUB_EXT_WRITE_TBT_FLASH 0xFF
#define TBT_COMMAND_WAKEUP 0x00000000
#define TBT_COMMAND_AUTHENTICATE 0xFFFFFFFF
#define TBT_COMMAND_AUTHENTICATE_STATUS 0xFFFFFFFE
typedef struct __attribute__((packed)) {
guint8 cmd;
guint8 ext;
union {
guint32 dwregaddr;
struct {
guint8 cmd_data0;
guint8 cmd_data1;
guint8 cmd_data2;
guint8 cmd_data3;
};
};
guint16 bufferlen;
FuHIDI2CParameters parameters;
guint8 extended_cmdarea[53];
guint8 data[192];
} FuHIDCmdBuffer;
typedef struct __attribute__((packed)) {
guint8 cmd;
guint8 ext;
guint8 i2ctargetaddr;
guint8 i2cspeed;
union {
guint32 startaddress;
guint32 tbt_command;
};
guint8 bufferlen;
guint8 extended_cmdarea[55];
guint8 data[192];
} FuTbtCmdBuffer;
static gboolean
fu_dell_dock_hid_set_report_cb(FuDevice *self, gpointer user_data, GError **error)
{
guint8 *outbuffer = (guint8 *)user_data;
return fu_hid_device_set_report(FU_HID_DEVICE(self),
0x0,
outbuffer,
192,
HIDI2C_TRANSACTION_TIMEOUT,
FU_HID_DEVICE_FLAG_NONE,
error);
}
static gboolean
fu_dell_dock_hid_set_report(FuDevice *self, guint8 *outbuffer, GError **error)
{
return fu_device_retry(self,
fu_dell_dock_hid_set_report_cb,
HID_MAX_RETRIES,
outbuffer,
error);
}
static gboolean
fu_dell_dock_hid_get_report_cb(FuDevice *self, gpointer user_data, GError **error)
{
guint8 *inbuffer = (guint8 *)user_data;
return fu_hid_device_get_report(FU_HID_DEVICE(self),
0x0,
inbuffer,
192,
HIDI2C_TRANSACTION_TIMEOUT,
FU_HID_DEVICE_FLAG_NONE,
error);
}
static gboolean
fu_dell_dock_hid_get_report(FuDevice *self, guint8 *inbuffer, GError **error)
{
return fu_device_retry(self,
fu_dell_dock_hid_get_report_cb,
HID_MAX_RETRIES,
inbuffer,
error);
}
gboolean
fu_dell_dock_hid_get_hub_version(FuDevice *self, GError **error)
{
g_autofree gchar *version = NULL;
FuHIDCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_READ_DATA,
.ext = HUB_EXT_READ_STATUS,
.cmd_data0 = 0,
.cmd_data1 = 0,
.cmd_data2 = 0,
.cmd_data3 = 0,
.bufferlen = GUINT16_TO_LE(12),
.parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
.extended_cmdarea[0 ... 52] = 0,
};
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to query hub version: ");
return FALSE;
}
if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) {
g_prefix_error(error, "failed to query hub version: ");
return FALSE;
}
version = g_strdup_printf("%02x.%02x", cmd_buffer.data[10], cmd_buffer.data[11]);
fu_device_set_version_format(self, FWUPD_VERSION_FORMAT_PAIR);
fu_device_set_version(self, version);
return TRUE;
}
gboolean
fu_dell_dock_hid_raise_mcu_clock(FuDevice *self, gboolean enable, GError **error)
{
FuHIDCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_WRITE_DATA,
.ext = HUB_EXT_MCUMODIFYCLOCK,
.cmd_data0 = (guint8)enable,
.cmd_data1 = 0,
.cmd_data2 = 0,
.cmd_data3 = 0,
.bufferlen = 0,
.parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
.extended_cmdarea[0 ... 52] = 0,
};
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to set mcu clock to %d: ", enable);
return FALSE;
}
return TRUE;
}
gboolean
fu_dell_dock_hid_get_ec_status(FuDevice *self, guint8 *status1, guint8 *status2, GError **error)
{
FuHIDCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_WRITE_DATA,
.ext = HUB_EXT_READ_STATUS,
.cmd_data0 = 0,
.cmd_data1 = 0,
.cmd_data2 = 0,
.cmd_data3 = 0,
.bufferlen = GUINT16_TO_LE(27),
.parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
.extended_cmdarea[0 ... 52] = 0,
};
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to get EC status: ");
return FALSE;
}
if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) {
g_prefix_error(error, "failed to get EC status: ");
return FALSE;
}
*status1 = cmd_buffer.data[25];
*status2 = cmd_buffer.data[26];
return TRUE;
}
gboolean
fu_dell_dock_hid_erase_bank(FuDevice *self, guint8 idx, GError **error)
{
FuHIDCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_WRITE_DATA,
.ext = HUB_EXT_ERASEBANK,
.cmd_data0 = 0,
.cmd_data1 = idx,
.cmd_data2 = 0,
.cmd_data3 = 0,
.bufferlen = 0,
.parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
.extended_cmdarea[0 ... 52] = 0,
};
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to erase bank: ");
return FALSE;
}
return TRUE;
}
gboolean
fu_dell_dock_hid_write_flash(FuDevice *self,
guint32 dwAddr,
const guint8 *input,
gsize write_size,
GError **error)
{
FuHIDCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_WRITE_DATA,
.ext = HUB_EXT_WRITEFLASH,
.dwregaddr = GUINT32_TO_LE(dwAddr),
.bufferlen = GUINT16_TO_LE(write_size),
.parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
.extended_cmdarea[0 ... 52] = 0,
};
g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE);
memcpy(cmd_buffer.data, input, write_size);
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error,
"failed to write %" G_GSIZE_FORMAT " flash to %x: ",
write_size,
dwAddr);
return FALSE;
}
return TRUE;
}
gboolean
fu_dell_dock_hid_verify_update(FuDevice *self, gboolean *result, GError **error)
{
FuHIDCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_WRITE_DATA,
.ext = HUB_EXT_VERIFYUPDATE,
.cmd_data0 = 1,
.cmd_data1 = 0,
.cmd_data2 = 0,
.cmd_data3 = 0,
.bufferlen = GUINT16_TO_LE(1),
.parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
.extended_cmdarea[0 ... 52] = 0,
};
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to verify update: ");
return FALSE;
}
if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) {
g_prefix_error(error, "failed to verify update: ");
return FALSE;
}
*result = cmd_buffer.data[0];
return TRUE;
}
gboolean
fu_dell_dock_hid_i2c_write(FuDevice *self,
const guint8 *input,
gsize write_size,
const FuHIDI2CParameters *parameters,
GError **error)
{
FuHIDCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_WRITE_DATA,
.ext = HUB_EXT_I2C_WRITE,
.dwregaddr = 0,
.bufferlen = GUINT16_TO_LE(write_size),
.parameters = {.i2ctargetaddr = parameters->i2ctargetaddr,
.regaddrlen = 0,
.i2cspeed = parameters->i2cspeed | 0x80},
.extended_cmdarea[0 ... 52] = 0,
};
g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE);
memcpy(cmd_buffer.data, input, write_size);
return fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error);
}
gboolean
fu_dell_dock_hid_i2c_read(FuDevice *self,
guint32 cmd,
gsize read_size,
GBytes **bytes,
const FuHIDI2CParameters *parameters,
GError **error)
{
FuHIDCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_WRITE_DATA,
.ext = HUB_EXT_I2C_READ,
.dwregaddr = GUINT32_TO_LE(cmd),
.bufferlen = GUINT16_TO_LE(read_size),
.parameters = {.i2ctargetaddr = parameters->i2ctargetaddr,
.regaddrlen = parameters->regaddrlen,
.i2cspeed = parameters->i2cspeed | 0x80},
.extended_cmdarea[0 ... 52] = 0,
.data[0 ... 191] = 0,
};
g_return_val_if_fail(read_size <= HIDI2C_MAX_READ, FALSE);
g_return_val_if_fail(bytes != NULL, FALSE);
g_return_val_if_fail(parameters->regaddrlen < HIDI2C_MAX_REGISTER, FALSE);
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error))
return FALSE;
if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error))
return FALSE;
*bytes = g_bytes_new(cmd_buffer.data, read_size);
return TRUE;
}
gboolean
fu_dell_dock_hid_tbt_wake(FuDevice *self, const FuHIDI2CParameters *parameters, GError **error)
{
FuTbtCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_READ_DATA, /* special write command that reads status result */
.ext = HUB_EXT_WRITE_TBT_FLASH,
.i2ctargetaddr = parameters->i2ctargetaddr,
.i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */
.tbt_command = TBT_COMMAND_WAKEUP,
.bufferlen = 0,
.extended_cmdarea[0 ... 53] = 0,
.data[0 ... 191] = 0,
};
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to set wake thunderbolt: ");
return FALSE;
}
if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) {
g_prefix_error(error, "failed to get wake thunderbolt status: ");
return FALSE;
}
g_debug("thunderbolt wake result: 0x%x", cmd_buffer.data[1]);
return TRUE;
}
static const gchar *
fu_dell_dock_hid_tbt_map_error(guint32 code)
{
if (code == 1)
return g_strerror(EINVAL);
else if (code == 2)
return g_strerror(EPERM);
return g_strerror(EIO);
}
gboolean
fu_dell_dock_hid_tbt_write(FuDevice *self,
guint32 start_addr,
const guint8 *input,
gsize write_size,
const FuHIDI2CParameters *parameters,
GError **error)
{
FuTbtCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_READ_DATA, /* It's a special write command that reads status result */
.ext = HUB_EXT_WRITE_TBT_FLASH,
.i2ctargetaddr = parameters->i2ctargetaddr,
.i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */
.startaddress = GUINT32_TO_LE(start_addr),
.bufferlen = write_size,
.extended_cmdarea[0 ... 53] = 0,
};
guint8 result;
g_return_val_if_fail(input != NULL, FALSE);
g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE);
memcpy(cmd_buffer.data, input, write_size);
for (gint i = 1; i <= TBT_MAX_RETRIES; i++) {
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to run TBT update: ");
return FALSE;
}
if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) {
g_prefix_error(error, "failed to get TBT flash status: ");
return FALSE;
}
result = cmd_buffer.data[1] & 0xf;
if (result == 0)
break;
g_debug("attempt %d/%d: Thunderbolt write failed: %x", i, TBT_MAX_RETRIES, result);
}
if (result != 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"Writing address 0x%04x failed: %s",
start_addr,
fu_dell_dock_hid_tbt_map_error(result));
return FALSE;
}
return TRUE;
}
gboolean
fu_dell_dock_hid_tbt_authenticate(FuDevice *self,
const FuHIDI2CParameters *parameters,
GError **error)
{
FuTbtCmdBuffer cmd_buffer = {
.cmd = HUB_CMD_READ_DATA, /* It's a special write command that reads status result */
.ext = HUB_EXT_WRITE_TBT_FLASH,
.i2ctargetaddr = parameters->i2ctargetaddr,
.i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */
.tbt_command = GUINT32_TO_LE(TBT_COMMAND_AUTHENTICATE),
.bufferlen = 0,
.extended_cmdarea[0 ... 53] = 0,
};
guint8 result;
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to send authentication: ");
return FALSE;
}
cmd_buffer.tbt_command = GUINT32_TO_LE(TBT_COMMAND_AUTHENTICATE_STATUS);
/* needs at least 2 seconds */
g_usleep(2000000);
for (gint i = 1; i <= TBT_MAX_RETRIES; i++) {
if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) {
g_prefix_error(error, "failed to set check authentication: ");
return FALSE;
}
if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) {
g_prefix_error(error, "failed to get check authentication: ");
return FALSE;
}
result = cmd_buffer.data[1] & 0xf;
if (result == 0)
break;
g_debug("attempt %d/%d: Thunderbolt authenticate failed: %x",
i,
TBT_MAX_RETRIES,
result);
g_usleep(500000);
}
if (result != 0) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Thunderbolt authentication failed: %s",
fu_dell_dock_hid_tbt_map_error(result));
return FALSE;
}
return TRUE;
}