mirror of
https://git.proxmox.com/git/fwupd
synced 2025-07-26 22:23:20 +00:00
858 lines
26 KiB
C
858 lines
26 KiB
C
/*
|
|
* Copyright (C) 2021 Peter Marheine <pmarheine@chromium.org>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <linux/i2c-dev.h>
|
|
|
|
#include "fu-parade-lspcon-device.h"
|
|
|
|
/* device registers are split into pages, where
|
|
* each page has its own I2C address */
|
|
#define I2C_ADDR_PAGE2 0x4A
|
|
#define REG_ADDR_CLT2SPI 0x82
|
|
/* FLASH_ADDR_* are the upper 16 bits of the 24-bit flash address that gets
|
|
* mapped into page 7. Writing 0x01, 0x42 will map the 256 bytes from 0x420100
|
|
* into page 7. */
|
|
#define REG_ADDR_FLASH_ADDR_LO 0x8E
|
|
#define REG_ADDR_FLASH_ADDR_HI 0x8F
|
|
/* 16-deep SPI write and read buffer FIFOs */
|
|
#define REG_ADDR_WR_FIFO 0x90
|
|
#define REG_ADDR_RD_FIFO 0x91
|
|
/* Low nibble is write operation length, high nibble for read commands.
|
|
* Reset to 0 after command completion. */
|
|
#define REG_ADDR_SPI_LEN 0x92
|
|
|
|
#define REG_ADDR_SPI_CTL 0x93
|
|
/* set to do a write-only transaction */
|
|
#define SPI_CTL_NOREAD 0x04
|
|
/* set to begin executing command */
|
|
#define SPI_CTL_TRIGGER 0x01
|
|
|
|
/* operation status fields: set to 1 when operation begins, 2 when command has been
|
|
* sent, reset to 0 when command completed */
|
|
#define REG_ADDR_SPI_STATUS 0x9e
|
|
/* byte programming */
|
|
#define SPI_STATUS_BP_MASK 0x03
|
|
/* sector erase */
|
|
#define SPI_STATUS_SE_MASK 0x0C
|
|
/* chip erase */
|
|
#define SPI_STATUS_CE_MASK 0x30
|
|
|
|
/* write WR_PROTECT_DISABLE to permit flash write operations */
|
|
#define REG_ADDR_WR_PROTECT 0xB3
|
|
#define WR_PROTECT_DISABLE 0x10
|
|
|
|
/* MPU control register */
|
|
#define REG_ADDR_MPU 0xBC
|
|
|
|
/* write a magic sequence to this register to enable writes to
|
|
* mapped memory via page 7, or anything else to disable */
|
|
#define REG_ADDR_MAP_WRITE 0xDA
|
|
|
|
#define I2C_ADDR_PAGE5 0x4D
|
|
#define REG_ADDR_ACTIVE_PARTITION 0x0E
|
|
|
|
#define I2C_ADDR_PAGE7 0x4F
|
|
|
|
#define FLASH_BLOCK_SIZE 0x10000
|
|
|
|
#define FU_PARADE_LSPCON_DEVICE_IOCTL_TIMEOUT 5000 /* ms */
|
|
|
|
/*
|
|
* user1: 0x10000 - 0x20000
|
|
* user2: 0x20000 - 0x30000
|
|
* flag: 0x00002 - 0x00004
|
|
*/
|
|
struct _FuParadeLspconDevice {
|
|
FuI2cDevice parent_instance;
|
|
guint8 active_partition;
|
|
gchar *aux_device_name;
|
|
};
|
|
|
|
G_DEFINE_TYPE(FuParadeLspconDevice, fu_parade_lspcon_device, FU_TYPE_I2C_DEVICE)
|
|
|
|
static void
|
|
fu_parade_lspcon_device_init(FuParadeLspconDevice *self)
|
|
{
|
|
FuDevice *device = FU_DEVICE(self);
|
|
fu_device_set_vendor(device, "Parade Technologies");
|
|
fu_device_add_vendor_id(device, "PCI:0x1AF8");
|
|
fu_device_add_protocol(device, "com.paradetech.ps176");
|
|
fu_device_add_icon(device, "video-display");
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
|
|
fu_device_set_firmware_size(device, 0x10000);
|
|
fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PAIR);
|
|
}
|
|
|
|
static void
|
|
fu_parade_lspcon_device_finalize(GObject *object)
|
|
{
|
|
FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(object);
|
|
g_free(self->aux_device_name);
|
|
G_OBJECT_CLASS(fu_parade_lspcon_device_parent_class)->finalize(object);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_device_set_quirk_kv(FuDevice *device,
|
|
const gchar *key,
|
|
const gchar *value,
|
|
GError **error)
|
|
{
|
|
FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
|
|
|
|
if (g_strcmp0(key, "ParadeLspconAuxDeviceName") == 0) {
|
|
self->aux_device_name = g_strdup(value);
|
|
return TRUE;
|
|
}
|
|
return FU_DEVICE_CLASS(fu_parade_lspcon_device_parent_class)
|
|
->set_quirk_kv(device, key, value, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_device_probe(FuDevice *device, GError **error)
|
|
{
|
|
FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
|
|
FuContext *context = fu_device_get_context(device);
|
|
FuUdevDevice *udev_device = FU_UDEV_DEVICE(device);
|
|
const gchar *device_name;
|
|
|
|
/* custom instance IDs to get device quirks */
|
|
fu_device_add_instance_str(device,
|
|
"NAME",
|
|
fu_udev_device_get_sysfs_attr(udev_device, "name", NULL));
|
|
fu_device_add_instance_str(device,
|
|
"FAMILY",
|
|
fu_context_get_hwid_value(context, FU_HWIDS_KEY_FAMILY));
|
|
if (!fu_device_build_instance_id(device, error, "PARADE-LSPCON", "NAME", NULL))
|
|
return FALSE;
|
|
fu_device_build_instance_id_quirk(device, NULL, "PARADE-LSPCON", "NAME", "FAMILY", NULL);
|
|
|
|
/* probably set from quirk */
|
|
device_name = fu_device_get_name(device);
|
|
if (g_strcmp0(device_name, "PS175") != 0) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"device name %s is not supported by this plugin",
|
|
device_name);
|
|
return FALSE;
|
|
}
|
|
|
|
/* should know which aux device over which we read DPCD version */
|
|
if (self->aux_device_name == NULL) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"ParadeLspconAuxDeviceName must be specified");
|
|
return FALSE;
|
|
}
|
|
|
|
/* FuI2cDevice->probe */
|
|
return FU_DEVICE_CLASS(fu_parade_lspcon_device_parent_class)->probe(device, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_ensure_i2c_address(FuParadeLspconDevice *self, guint8 address, GError **error)
|
|
{
|
|
if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
|
|
I2C_SLAVE,
|
|
(guint8 *)(guintptr)address,
|
|
NULL,
|
|
FU_PARADE_LSPCON_DEVICE_IOCTL_TIMEOUT,
|
|
error)) {
|
|
g_prefix_error(error, "failed to set I2C address: ");
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_device_open(FuDevice *device, GError **error)
|
|
{
|
|
if (!FU_DEVICE_CLASS(fu_parade_lspcon_device_parent_class)->open(device, error))
|
|
return FALSE;
|
|
|
|
/* general assumption is that page 2 is selected: code that uses another address
|
|
* should use an address guard to ensure it gets reset */
|
|
return fu_parade_lspcon_ensure_i2c_address(FU_PARADE_LSPCON_DEVICE(device),
|
|
I2C_ADDR_PAGE2,
|
|
error);
|
|
}
|
|
|
|
/**
|
|
* creates a scope in which the device's target I2C address is something
|
|
* other than page 2, and resets it to page 2 when the scope is left.
|
|
*/
|
|
typedef struct {
|
|
FuParadeLspconDevice *device;
|
|
} FuParadeLspconI2cAddressGuard;
|
|
|
|
static FuParadeLspconI2cAddressGuard *
|
|
fu_parade_lspcon_i2c_address_guard_new(FuParadeLspconDevice *self,
|
|
guint8 new_address,
|
|
GError **error)
|
|
{
|
|
FuParadeLspconI2cAddressGuard *out;
|
|
|
|
if (!fu_parade_lspcon_ensure_i2c_address(self, new_address, error))
|
|
return NULL;
|
|
out = g_new0(FuParadeLspconI2cAddressGuard, 1);
|
|
out->device = self;
|
|
return out;
|
|
}
|
|
|
|
static void
|
|
fu_parade_lspcon_i2c_address_guard_free(FuParadeLspconI2cAddressGuard *guard)
|
|
{
|
|
g_autoptr(GError) error_local = NULL;
|
|
if (!fu_parade_lspcon_ensure_i2c_address(guard->device, I2C_ADDR_PAGE2, &error_local)) {
|
|
g_warning("failed to set page2 back: %s", error_local->message);
|
|
}
|
|
g_free(guard);
|
|
}
|
|
|
|
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuParadeLspconI2cAddressGuard,
|
|
fu_parade_lspcon_i2c_address_guard_free);
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_write_register(FuParadeLspconDevice *self,
|
|
guint8 register_addr,
|
|
guint8 value,
|
|
GError **error)
|
|
{
|
|
guint8 transaction[] = {register_addr, value};
|
|
return fu_i2c_device_write(FU_I2C_DEVICE(self), transaction, sizeof(transaction), error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_read_register(FuParadeLspconDevice *self,
|
|
guint8 register_addr,
|
|
guint8 *value,
|
|
GError **error)
|
|
{
|
|
FuI2cDevice *i2c_device = FU_I2C_DEVICE(self);
|
|
if (!fu_i2c_device_write(i2c_device, ®ister_addr, 0x1, error))
|
|
return FALSE;
|
|
return fu_i2c_device_read(i2c_device, value, 0x1, error);
|
|
}
|
|
|
|
/* map the page containing the given address into page 7 */
|
|
static gboolean
|
|
fu_parade_lspcon_map_page(FuParadeLspconDevice *self, guint32 address, GError **error)
|
|
{
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_FLASH_ADDR_HI, address >> 16, error))
|
|
return FALSE;
|
|
return fu_parade_lspcon_write_register(self, REG_ADDR_FLASH_ADDR_LO, address >> 8, error);
|
|
}
|
|
|
|
/* wait until the specified register masked with mask reads the expected
|
|
* value, up to 10 seconds */
|
|
static gboolean
|
|
fu_parade_lspcon_poll_register(FuParadeLspconDevice *self,
|
|
guint8 register_address,
|
|
guint8 mask,
|
|
guint8 expected,
|
|
GError **error)
|
|
{
|
|
guint8 value;
|
|
g_autoptr(GTimer) timer = g_timer_new();
|
|
|
|
do {
|
|
if (!fu_parade_lspcon_read_register(self, register_address, &value, error))
|
|
return FALSE;
|
|
if ((value & mask) == expected)
|
|
return TRUE;
|
|
} while (g_timer_elapsed(timer, NULL) <= 10.0);
|
|
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_TIMED_OUT,
|
|
"register %x did not read %x (mask %x) within 10 seconds: read %x",
|
|
register_address,
|
|
expected,
|
|
mask,
|
|
value);
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_flash_read(FuParadeLspconDevice *self,
|
|
guint32 base_address,
|
|
guint8 *data,
|
|
const gsize len,
|
|
FuProgress *progress,
|
|
GError **error)
|
|
{
|
|
FuI2cDevice *i2c_device = FU_I2C_DEVICE(self);
|
|
gsize offset = 0;
|
|
|
|
while (offset < len) {
|
|
/* page 7 reads always start from the base of the mapped window- we'll
|
|
* read the whole page then pull out the parts we care about, using the
|
|
* full page everywhere except possibly in the first and last reads */
|
|
guint8 page_data[256] = {0x0};
|
|
guint8 page_data_start = base_address & 0xFF;
|
|
gsize page_data_take = MIN((gssize)len, 256 - page_data_start);
|
|
g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL;
|
|
|
|
if (!fu_parade_lspcon_map_page(self, base_address, error))
|
|
return FALSE;
|
|
guard = fu_parade_lspcon_i2c_address_guard_new(self, I2C_ADDR_PAGE7, error);
|
|
if (guard == NULL)
|
|
return FALSE;
|
|
if (!fu_i2c_device_read(i2c_device, page_data, 256, error))
|
|
return FALSE;
|
|
|
|
if (!fu_memcpy_safe(data,
|
|
len,
|
|
offset,
|
|
page_data,
|
|
sizeof(page_data),
|
|
page_data_start,
|
|
page_data_take,
|
|
error))
|
|
return FALSE;
|
|
base_address += page_data_take;
|
|
offset += page_data_take;
|
|
|
|
fu_progress_set_percentage_full(progress, offset, len);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_flash_transmit_command(FuParadeLspconDevice *self,
|
|
const guint8 *command,
|
|
gsize command_len,
|
|
GError **error)
|
|
{
|
|
/* write length field is 4 bits wide */
|
|
g_return_val_if_fail(command_len > 0 && command_len <= 16, FALSE);
|
|
|
|
/* fill transmit buffer */
|
|
for (gsize i = 0; i < command_len; i++) {
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_WR_FIFO, command[i], error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* set command length */
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_SPI_LEN, command_len - 1, error))
|
|
return FALSE;
|
|
|
|
/* execute operation */
|
|
return fu_parade_lspcon_write_register(self,
|
|
REG_ADDR_SPI_CTL,
|
|
SPI_CTL_NOREAD | SPI_CTL_TRIGGER,
|
|
error);
|
|
}
|
|
|
|
/*
|
|
* set the flash Write Enable Latch, permitting the next program, erase or
|
|
* status register write operation.
|
|
*/
|
|
static gboolean
|
|
fu_parade_lspcon_flash_enable_write(FuParadeLspconDevice *self, GError **error)
|
|
{
|
|
const guint8 write_enable[] = {0x06};
|
|
return fu_parade_lspcon_flash_transmit_command(self,
|
|
write_enable,
|
|
sizeof(write_enable),
|
|
error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_flash_read_status(FuParadeLspconDevice *self, guint8 *value, GError **error)
|
|
{
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_WR_FIFO, 0x05, error))
|
|
return FALSE;
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_SPI_LEN, 0, error))
|
|
return FALSE;
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_SPI_CTL, SPI_CTL_TRIGGER, error))
|
|
return FALSE;
|
|
|
|
/* wait for command completion */
|
|
if (!fu_parade_lspcon_poll_register(self, REG_ADDR_SPI_CTL, SPI_CTL_TRIGGER, 0, error))
|
|
return FALSE;
|
|
|
|
/* read SR value */
|
|
return fu_parade_lspcon_read_register(self, REG_ADDR_RD_FIFO, value, error);
|
|
}
|
|
|
|
/* poll the flash status register for operation completion */
|
|
static gboolean
|
|
fu_parade_lspcon_flash_wait_ready(FuParadeLspconDevice *self, GError **error)
|
|
{
|
|
g_autoptr(GTimer) timer = g_timer_new();
|
|
|
|
do {
|
|
guint8 status_register;
|
|
if (!fu_parade_lspcon_flash_read_status(self, &status_register, error))
|
|
return FALSE;
|
|
|
|
/* BUSY bit clears on completion */
|
|
if ((status_register & 1) == 0)
|
|
return TRUE;
|
|
|
|
/* flash operations generally take between 1ms and 4s; polling
|
|
* at 1000 Hz is still quite responsive and not overly slow */
|
|
g_usleep(G_TIME_SPAN_MILLISECOND);
|
|
} while (g_timer_elapsed(timer, NULL) <= 10.0);
|
|
|
|
g_set_error_literal(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_TIMED_OUT,
|
|
"flash did not become ready within 10 seconds");
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_flash_write(FuParadeLspconDevice *self,
|
|
guint32 base_address,
|
|
GBytes *data,
|
|
FuProgress *progress,
|
|
GError **error)
|
|
{
|
|
FuI2cDevice *i2c_device = FU_I2C_DEVICE(self);
|
|
const guint8 unlock_writes[] = {0xaa, 0x55, 0x50, 0x41, 0x52, 0x44};
|
|
gsize data_len = g_bytes_get_size(data);
|
|
g_autoptr(GPtrArray) chunks = NULL;
|
|
|
|
/* address must be 256-byte aligned */
|
|
g_return_val_if_fail((base_address & 0xFF) == 0, FALSE);
|
|
g_debug("flash write %" G_GSIZE_FORMAT " bytes at %#x",
|
|
g_bytes_get_size(data),
|
|
base_address);
|
|
|
|
/* unlock map writes by writing the magic sequence */
|
|
for (gsize i = 0; i < sizeof(unlock_writes); i++) {
|
|
if (!fu_parade_lspcon_write_register(self,
|
|
REG_ADDR_MAP_WRITE,
|
|
unlock_writes[i],
|
|
error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* reset clt2SPI, required before write */
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_CLT2SPI, 0x20, error))
|
|
return FALSE;
|
|
g_usleep(100 * G_TIME_SPAN_MILLISECOND);
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_CLT2SPI, 0, error))
|
|
return FALSE;
|
|
|
|
chunks = fu_chunk_array_new_from_bytes(data, base_address, 0, 256);
|
|
for (gsize i = 0; i < chunks->len; i++) {
|
|
FuChunk *chunk = g_ptr_array_index(chunks, i);
|
|
guint32 address = fu_chunk_get_address(chunk);
|
|
guint32 chunk_size = fu_chunk_get_data_sz(chunk);
|
|
guint8 write_data[257] = {0x0};
|
|
g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL;
|
|
|
|
/* map target address range in page 7 */
|
|
if (!fu_parade_lspcon_map_page(self, address, error))
|
|
return FALSE;
|
|
|
|
/* write data to page 7 memory window */
|
|
guard = fu_parade_lspcon_i2c_address_guard_new(self, I2C_ADDR_PAGE7, error);
|
|
if (guard == NULL)
|
|
return FALSE;
|
|
|
|
/* page write is prefixed with an offset:
|
|
* we always start from offset 0 */
|
|
write_data[0] = 0;
|
|
if (!fu_memcpy_safe(write_data,
|
|
sizeof(write_data),
|
|
1,
|
|
fu_chunk_get_data(chunk),
|
|
chunk_size,
|
|
0,
|
|
chunk_size,
|
|
error))
|
|
return FALSE;
|
|
|
|
if (!fu_i2c_device_write(i2c_device, write_data, chunk_size + 1, error))
|
|
return FALSE;
|
|
|
|
fu_progress_set_percentage_full(progress, address - base_address, data_len);
|
|
}
|
|
|
|
/* re-lock map writes */
|
|
return fu_parade_lspcon_write_register(self, REG_ADDR_MAP_WRITE, 0, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_flash_erase_block(FuParadeLspconDevice *self,
|
|
guint32 base_address,
|
|
guint32 size,
|
|
GError **error)
|
|
{
|
|
const guint8 block_erase[] = {0xd8, base_address >> 16, base_address >> 8, base_address};
|
|
|
|
/* address must be block-aligned */
|
|
g_return_val_if_fail((base_address & (FLASH_BLOCK_SIZE - 1)) == 0, FALSE);
|
|
g_return_val_if_fail(size == FLASH_BLOCK_SIZE, FALSE);
|
|
|
|
g_debug("flash erase block at %#x", base_address);
|
|
|
|
if (!fu_parade_lspcon_flash_enable_write(self, error))
|
|
return FALSE;
|
|
|
|
if (!fu_parade_lspcon_flash_transmit_command(self, block_erase, sizeof(block_erase), error))
|
|
return FALSE;
|
|
|
|
/* wait for command completion */
|
|
if (!fu_parade_lspcon_poll_register(self,
|
|
REG_ADDR_SPI_STATUS,
|
|
SPI_STATUS_SE_MASK,
|
|
0,
|
|
error))
|
|
return FALSE;
|
|
|
|
/* wait for flash to complete erase */
|
|
return fu_parade_lspcon_flash_wait_ready(self, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_probe_active_flash_partition(FuParadeLspconDevice *self,
|
|
guint8 *partition,
|
|
GError **error)
|
|
{
|
|
guint8 data = 0x0;
|
|
g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL;
|
|
|
|
/* read currently-running flash partition number */
|
|
guard = fu_parade_lspcon_i2c_address_guard_new(self, I2C_ADDR_PAGE5, error);
|
|
if (guard == NULL)
|
|
return FALSE;
|
|
if (!fu_parade_lspcon_read_register(self, REG_ADDR_ACTIVE_PARTITION, &data, error))
|
|
return FALSE;
|
|
|
|
*partition = data;
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_device_reload(FuDevice *device, GError **error)
|
|
{
|
|
FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
|
|
guint32 oui;
|
|
guint8 version_buf[2] = {0x0};
|
|
g_autofree gchar *version = NULL;
|
|
g_autofree gchar *oui_string = NULL;
|
|
g_autoptr(FuDeviceLocker) aux_device_locker = NULL;
|
|
g_autoptr(FuUdevDevice) aux_device = NULL;
|
|
g_autoptr(GList) aux_devices = NULL;
|
|
g_autoptr(GUdevClient) udev_client = g_udev_client_new(NULL);
|
|
g_autoptr(GUdevEnumerator) enumerator = g_udev_enumerator_new(udev_client);
|
|
|
|
/* determine active partition for flashing later */
|
|
if (!fu_parade_lspcon_probe_active_flash_partition(self, &self->active_partition, error))
|
|
return FALSE;
|
|
g_debug("device reports running from partition %d", self->active_partition);
|
|
if (self->active_partition < 1 || self->active_partition > 3) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"unexpected active flash partition: %d",
|
|
self->active_partition);
|
|
return FALSE;
|
|
}
|
|
|
|
/* find the drm_dp_aux_dev specified by quirks that is connected to the
|
|
* LSPCON, in order to read DPCD from it */
|
|
if (self->aux_device_name == NULL) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"no DP aux device specified, unable to query LSPCON");
|
|
return FALSE;
|
|
}
|
|
g_udev_enumerator_add_match_subsystem(enumerator, "drm_dp_aux_dev");
|
|
g_udev_enumerator_add_match_sysfs_attr(enumerator, "name", self->aux_device_name);
|
|
aux_devices = g_udev_enumerator_execute(enumerator);
|
|
if (aux_devices == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to locate a DP aux device named \"%s\"",
|
|
self->aux_device_name);
|
|
return FALSE;
|
|
}
|
|
if (g_list_length(aux_devices) > 1) {
|
|
g_list_free_full(aux_devices, g_object_unref);
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"found multiple DP aux devices with name \"%s\"",
|
|
self->aux_device_name);
|
|
return FALSE;
|
|
}
|
|
aux_device =
|
|
fu_udev_device_new(fu_device_get_context(device), g_steal_pointer(&aux_devices->data));
|
|
g_debug("using aux dev %s", fu_udev_device_get_sysfs_path(aux_device));
|
|
|
|
/* the following open() requires the device have IDs set */
|
|
if (!fu_udev_device_set_physical_id(aux_device, "drm_dp_aux_dev", error))
|
|
return FALSE;
|
|
|
|
/* open device to read version from DPCD */
|
|
if ((aux_device_locker = fu_device_locker_new(aux_device, error)) == NULL)
|
|
return FALSE;
|
|
|
|
/* DPCD address 00500-00502: device OUI */
|
|
if (!fu_udev_device_pread(aux_device, 0x500, (guint8 *)&oui, 3, error))
|
|
return FALSE;
|
|
oui = GUINT32_FROM_BE(oui) >> 8;
|
|
oui_string = g_strdup_printf("OUI:%06X", oui);
|
|
fu_device_add_vendor_id(device, oui_string);
|
|
|
|
if (oui != 0x001CF8) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"device OUI %06X does not match expected value for Paradetech",
|
|
oui);
|
|
return FALSE;
|
|
}
|
|
|
|
/* DPCD address 0x50A, 0x50B: branch device firmware
|
|
* major and minor revision */
|
|
if (!fu_udev_device_pread(aux_device, 0x50a, version_buf, sizeof(version_buf), error))
|
|
return FALSE;
|
|
version = g_strdup_printf("%d.%d", version_buf[0], version_buf[1]);
|
|
fu_device_set_version(device, version);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_device_write_firmware(FuDevice *device,
|
|
FuFirmware *firmware,
|
|
FuProgress *progress,
|
|
FwupdInstallFlags flags,
|
|
GError **error)
|
|
{
|
|
FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
|
|
const guint8 write_sr_volatile[] = {0x50};
|
|
const guint8 write_sr_disable_bp[] = {
|
|
0x01, /* write SR */
|
|
0x80, /* write protect follows /WP signal, no block protection */
|
|
0x00};
|
|
const guint8 write_sr_enable_bp[] = {0x01, 0x8c, 0x00};
|
|
/* if the boot partition is active we could flash either, but prefer
|
|
* the first */
|
|
const guint8 target_partition = self->active_partition == 1 ? 2 : 1;
|
|
const guint32 target_address = target_partition << 16;
|
|
const guint8 flag_data[] = {0x55, 0xaa, target_partition, 1 - target_partition};
|
|
const guint8 *buf;
|
|
gsize bufsz;
|
|
g_autofree guint8 *readback_buf = NULL;
|
|
g_autoptr(GBytes) blob_fw = NULL;
|
|
g_autoptr(GBytes) flag_data_bytes = 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, 5, NULL);
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, NULL);
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 25, NULL);
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 3, "device-write-boot");
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 2, "device-verify-boot");
|
|
|
|
blob_fw = fu_firmware_get_bytes(firmware, error);
|
|
if (blob_fw == NULL)
|
|
return FALSE;
|
|
|
|
buf = g_bytes_get_data(blob_fw, &bufsz);
|
|
if (bufsz != FLASH_BLOCK_SIZE) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"invalid image size %#" G_GSIZE_MODIFIER "x, expected %#x",
|
|
bufsz,
|
|
(unsigned)FLASH_BLOCK_SIZE);
|
|
return FALSE;
|
|
}
|
|
|
|
/* deassert flash /WP */
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_WR_PROTECT, WR_PROTECT_DISABLE, error))
|
|
return FALSE;
|
|
|
|
/* disable flash protection until next power-off */
|
|
if (!fu_parade_lspcon_flash_transmit_command(self,
|
|
write_sr_volatile,
|
|
sizeof(write_sr_volatile),
|
|
error))
|
|
return FALSE;
|
|
if (!fu_parade_lspcon_flash_transmit_command(self,
|
|
write_sr_disable_bp,
|
|
sizeof(write_sr_disable_bp),
|
|
error))
|
|
return FALSE;
|
|
|
|
/* wait for SR write to complete */
|
|
if (!fu_parade_lspcon_flash_wait_ready(self, error))
|
|
return FALSE;
|
|
|
|
/* erase entire target partition (one flash block) */
|
|
if (!fu_parade_lspcon_flash_erase_block(self, target_address, bufsz, error)) {
|
|
g_prefix_error(error, "failed to erase flash partition %d: ", target_partition);
|
|
return FALSE;
|
|
}
|
|
fu_progress_step_done(progress);
|
|
|
|
/* write image */
|
|
if (!fu_parade_lspcon_flash_write(self,
|
|
target_address,
|
|
blob_fw,
|
|
fu_progress_get_child(progress),
|
|
error)) {
|
|
g_prefix_error(error,
|
|
"failed to write firmware to partition %d: ",
|
|
target_partition);
|
|
return FALSE;
|
|
}
|
|
fu_progress_step_done(progress);
|
|
|
|
/* read back written image to verify */
|
|
readback_buf = g_malloc0(bufsz);
|
|
if (!fu_parade_lspcon_flash_read(self,
|
|
target_address,
|
|
readback_buf,
|
|
bufsz,
|
|
fu_progress_get_child(progress),
|
|
error))
|
|
return FALSE;
|
|
if (!fu_memcmp_safe(buf, bufsz, readback_buf, bufsz, error)) {
|
|
g_prefix_error(error, "flash contents do not match: ");
|
|
return FALSE;
|
|
}
|
|
fu_progress_step_done(progress);
|
|
|
|
/* erase flag partition */
|
|
if (!fu_parade_lspcon_flash_erase_block(self, 0, FLASH_BLOCK_SIZE, error))
|
|
return FALSE;
|
|
|
|
/* write flag indicating device should boot the target partition */
|
|
fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
|
|
flag_data_bytes = g_bytes_new_static(flag_data, sizeof(flag_data));
|
|
if (!fu_parade_lspcon_flash_write(self,
|
|
0,
|
|
flag_data_bytes,
|
|
fu_progress_get_child(progress),
|
|
error))
|
|
return FALSE;
|
|
fu_progress_step_done(progress);
|
|
|
|
/* verify flag partition */
|
|
if (!fu_parade_lspcon_flash_read(self,
|
|
0,
|
|
readback_buf,
|
|
sizeof(flag_data),
|
|
fu_progress_get_child(progress),
|
|
error))
|
|
return FALSE;
|
|
if (!fu_memcmp_safe(flag_data,
|
|
sizeof(flag_data),
|
|
readback_buf,
|
|
MIN(sizeof(flag_data), bufsz),
|
|
error)) {
|
|
g_prefix_error(error, "flag partition contents do not match: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* re-enable flash protection */
|
|
if (!fu_parade_lspcon_flash_transmit_command(self,
|
|
write_sr_volatile,
|
|
sizeof(write_sr_volatile),
|
|
error))
|
|
return FALSE;
|
|
if (!fu_parade_lspcon_flash_transmit_command(self,
|
|
write_sr_enable_bp,
|
|
sizeof(write_sr_enable_bp),
|
|
error))
|
|
return FALSE;
|
|
fu_progress_step_done(progress);
|
|
|
|
/* reassert /WP to flash */
|
|
return fu_parade_lspcon_write_register(self, REG_ADDR_WR_PROTECT, 0, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_set_mpu_running(FuParadeLspconDevice *self, gboolean running, GError **error)
|
|
{
|
|
/* reset */
|
|
if (!fu_parade_lspcon_write_register(self, REG_ADDR_MPU, 0xc0, error))
|
|
return FALSE;
|
|
|
|
/* release reset, set MPU active or not */
|
|
return fu_parade_lspcon_write_register(self, REG_ADDR_MPU, running ? 0 : 0x40, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_device_detach(FuDevice *device, FuProgress *progress, GError **error)
|
|
{
|
|
FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
|
|
return fu_parade_lspcon_set_mpu_running(self, FALSE, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_parade_lspcon_device_attach(FuDevice *device, FuProgress *progress, GError **error)
|
|
{
|
|
FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
|
|
return fu_parade_lspcon_set_mpu_running(self, TRUE, error);
|
|
}
|
|
|
|
static GBytes *
|
|
fu_parade_lspcon_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error)
|
|
{
|
|
FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
|
|
g_autofree guint8 *data = g_malloc0(FLASH_BLOCK_SIZE);
|
|
|
|
if (!fu_parade_lspcon_flash_read(self,
|
|
self->active_partition * FLASH_BLOCK_SIZE,
|
|
data,
|
|
FLASH_BLOCK_SIZE,
|
|
progress,
|
|
error))
|
|
return NULL;
|
|
return g_bytes_new_take(g_steal_pointer(&data), FLASH_BLOCK_SIZE);
|
|
}
|
|
|
|
static void
|
|
fu_parade_lspcon_device_set_progress(FuDevice *self, FuProgress *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_RESTART, 2, "detach");
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write");
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach");
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload");
|
|
}
|
|
|
|
static void
|
|
fu_parade_lspcon_device_class_init(FuParadeLspconDeviceClass *klass)
|
|
{
|
|
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
|
|
GObjectClass *klass_object = G_OBJECT_CLASS(klass);
|
|
|
|
klass_object->finalize = fu_parade_lspcon_device_finalize;
|
|
klass_device->set_quirk_kv = fu_parade_lspcon_device_set_quirk_kv;
|
|
klass_device->probe = fu_parade_lspcon_device_probe;
|
|
klass_device->setup = fu_parade_lspcon_device_reload;
|
|
klass_device->open = fu_parade_lspcon_device_open;
|
|
klass_device->reload = fu_parade_lspcon_device_reload;
|
|
klass_device->detach = fu_parade_lspcon_device_detach;
|
|
klass_device->write_firmware = fu_parade_lspcon_device_write_firmware;
|
|
klass_device->attach = fu_parade_lspcon_device_attach;
|
|
klass_device->dump_firmware = fu_parade_lspcon_device_dump_firmware;
|
|
klass_device->set_progress = fu_parade_lspcon_device_set_progress;
|
|
}
|