From a12d7150385ec87d55663d768f5aed329dd63a0a Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Tue, 29 Jun 2021 11:55:55 +1000 Subject: [PATCH] New plugin: parade-lspcon --- contrib/ci/build_windows.sh | 1 + contrib/freebsd/Makefile | 1 + contrib/fwupd.spec.in | 1 + meson_options.txt | 1 + plugins/meson.build | 1 + plugins/parade-lspcon/README.md | 58 ++ .../parade-lspcon/fu-parade-lspcon-device.c | 838 ++++++++++++++++++ .../parade-lspcon/fu-parade-lspcon-device.h | 14 + .../parade-lspcon/fu-plugin-parade-lspcon.c | 17 + plugins/parade-lspcon/meson.build | 38 + plugins/parade-lspcon/parade-lspcon.quirk | 11 + 11 files changed, 981 insertions(+) create mode 100644 plugins/parade-lspcon/README.md create mode 100644 plugins/parade-lspcon/fu-parade-lspcon-device.c create mode 100644 plugins/parade-lspcon/fu-parade-lspcon-device.h create mode 100644 plugins/parade-lspcon/fu-plugin-parade-lspcon.c create mode 100644 plugins/parade-lspcon/meson.build create mode 100644 plugins/parade-lspcon/parade-lspcon.quirk diff --git a/contrib/ci/build_windows.sh b/contrib/ci/build_windows.sh index e3599531d..a679a6ca4 100755 --- a/contrib/ci/build_windows.sh +++ b/contrib/ci/build_windows.sh @@ -23,6 +23,7 @@ meson .. \ -Dplugin_altos=false \ -Dplugin_dell=false \ -Dplugin_nvme=false \ + -Dplugin_parade_lspcon=false \ -Dplugin_realtek_mst=false \ -Dplugin_platform_integrity=false \ -Dplugin_tpm=false \ diff --git a/contrib/freebsd/Makefile b/contrib/freebsd/Makefile index a485e79ab..85980f8c2 100644 --- a/contrib/freebsd/Makefile +++ b/contrib/freebsd/Makefile @@ -46,6 +46,7 @@ MESON_ARGS= -Dgudev=false \ -Dplugin_dell=false \ -Dplugin_emmc=false \ -Dplugin_nvme=false \ + -Dplugin_parade_lspcon=false \ -Dplugin_redfish=false \ -Dplugin_synaptics_mst=false \ -Dplugin_synaptics_rmi=false \ diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index 046dd5353..61bbd201b 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -440,6 +440,7 @@ done %{_libdir}/fwupd-plugins-3/libfu_plugin_nitrokey.so %{_libdir}/fwupd-plugins-3/libfu_plugin_nvme.so %{_libdir}/fwupd-plugins-3/libfu_plugin_optionrom.so +%{_libdir}/fwupd-plugins-3/libfu_plugin_parade_lspcon.so %{_libdir}/fwupd-plugins-3/libfu_plugin_pci_bcr.so %{_libdir}/fwupd-plugins-3/libfu_plugin_pci_mei.so %{_libdir}/fwupd-plugins-3/libfu_plugin_pixart_rf.so diff --git a/meson_options.txt b/meson_options.txt index fbd453413..4578e09e1 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -18,6 +18,7 @@ option('plugin_amt', type : 'boolean', value : true, description : 'enable Intel option('plugin_dell', type : 'boolean', value : true, description : 'enable Dell-specific support') option('plugin_dummy', type : 'boolean', value : false, description : 'enable the dummy device') option('plugin_emmc', type : 'boolean', value : true, description : 'enable eMMC support') +option('plugin_parade_lspcon', type : 'boolean', value : true, description : 'enable Parade LSPCON support') option('plugin_realtek_mst', type : 'boolean', value : true, description : 'enable Realtek MST hub support') option('plugin_synaptics_mst', type: 'boolean', value: true, description : 'enable Synaptics MST hub support') option('plugin_synaptics_rmi', type: 'boolean', value: true, description : 'enable Synaptics RMI support') diff --git a/plugins/meson.build b/plugins/meson.build index 3781c64d4..30ad0eb5a 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -40,6 +40,7 @@ subdir('msr') subdir('nitrokey') subdir('nvme') subdir('optionrom') +subdir('parade-lspcon') subdir('pci-bcr') subdir('pci-mei') subdir('pixart-rf') diff --git a/plugins/parade-lspcon/README.md b/plugins/parade-lspcon/README.md new file mode 100644 index 000000000..f6f3a0b4e --- /dev/null +++ b/plugins/parade-lspcon/README.md @@ -0,0 +1,58 @@ +Parade LSPCON +============= + +Introduction +------------ + +This plugin updates the firmware of HDMI level shifter and protocol converter +(LSPCON) devices made by Parade Technologies, such as the PS175. + +These devices communicate over I²C, via either the DisplayPort aux channel or a +dedicated bus- this plugin uses a dedicated bus declared by system firmware for, +flashing, and reads the device firmware version from DPCD. Quirks specify the +DisplayPort bus over which DPCD is read for a given system. + +Firmware is stored on an external flash attached to an SPI bus on the device. +The attached flash is assumed to be compatible with the W25Q20 series of +devices, in particular supporting a 64k Block Erase command (0xD8) with 24-bit +address and Write Enable for Volatile Status Register (0x05). + +Firmware Format +--------------- + +The device firmware is in an unspecified binary format that is written directly +to an inactive partition of the Flash attached to the device. + +This plugin supports the following protocol ID: + + * com.paradetech.ps176 + +GUID Generation +--------------- + +The plugin uses a custom DeviceInstanceId value derived from the device name +provided by system firmware and read from sysfs, such as: + + * `PARADE-LSPCON\NAME_1AF80175:00` + * `PARADE-LSPCON\NAME_1AF80175:00&FAMILY_Google_Hatch` + +Quirk use +--------- + +This plugin uses the following plugin-specific quirks: + +| Quirk | Description | Minimum fwupd version | +| `ParadeLspconAuxDeviceName` | sysfs name of the `drm_dp_aux_dev` over which device version should be read | 1.6.2 | + +Vendor ID security +------------------ + +The vendor ID is specified by system firmware (such as ACPI tables) and is +part of the device's name as read from sysfs. + +External interface access +------------------------- + +This plugin requires access to the DisplayPort aux channel to read DPCD, such +as `/dev/drm_dp_aux0` as well as the i2c bus attached to the device, such as +`/dev/i2c-7`. diff --git a/plugins/parade-lspcon/fu-parade-lspcon-device.c b/plugins/parade-lspcon/fu-parade-lspcon-device.c new file mode 100644 index 000000000..81660b614 --- /dev/null +++ b/plugins/parade-lspcon/fu-parade-lspcon-device.c @@ -0,0 +1,838 @@ +/* + * Copyright (C) 2021 Peter Marheine + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#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 + +/* + * 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); + 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); +} + +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; + g_autofree gchar *instance_id_hwid = NULL; + g_autofree gchar *instance_id = NULL; + + /* custom instance IDs to get device quirks */ + instance_id = g_strdup_printf ("PARADE-LSPCON\\NAME_%s", + fu_udev_device_get_sysfs_attr (udev_device, "name", NULL)); + fu_device_add_instance_id (device, instance_id); + instance_id_hwid = g_strdup_printf ("%s&FAMILY_%s", instance_id, + fu_context_get_hwid_value (context, + FU_HWIDS_KEY_FAMILY)); + fu_device_add_instance_id_full (device, instance_id_hwid, + FU_DEVICE_INSTANCE_FLAG_ONLY_QUIRKS); + + 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, + error)) { + g_prefix_error (error, "failed to set I2C slave 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_full (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, register_addr, error)) + return FALSE; + return fu_i2c_device_read (i2c_device, value, 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, + GError **error) +{ + FuDevice *device = FU_DEVICE (self); + 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_full (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_device_set_progress_full (device, 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, + GError **error) +{ + FuDevice *device = FU_DEVICE (self); + 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); + /* 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_full (i2c_device, + write_data, + chunk_size + 1, + error)) + return FALSE; + + fu_device_set_progress_full (device, 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 (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; + fu_device_set_context (FU_DEVICE (aux_device), fu_device_get_context (device)); + + /* 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_full (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_full (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, + 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; + + 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) */ + fu_device_set_status (device, FWUPD_STATUS_DEVICE_ERASE); + 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; + } + + /* write image */ + fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE); + if (!fu_parade_lspcon_flash_write (self, target_address, blob_fw, error)) { + g_prefix_error (error, "failed to write firmware to partition %d: ", + target_partition); + return FALSE; + } + + /* read back written image to verify */ + fu_device_set_status (device, FWUPD_STATUS_DEVICE_VERIFY); + readback_buf = g_malloc0 (bufsz); + if (!fu_parade_lspcon_flash_read (self, target_address, + readback_buf, bufsz, + error)) + return FALSE; + if (!fu_common_bytes_compare_raw (buf, bufsz, + readback_buf, bufsz, + error)) { + g_prefix_error (error, "flash contents do not match: "); + return FALSE; + } + + /* erase flag partition */ + fu_device_set_status (device, FWUPD_STATUS_DEVICE_ERASE); + 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_device_set_status (device, 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, error)) + return FALSE; + + /* verify flag partition */ + fu_device_set_status (device, FWUPD_STATUS_DEVICE_VERIFY); + if (!fu_parade_lspcon_flash_read (self, 0, + readback_buf, sizeof(flag_data), + error)) + return FALSE; + if (!fu_common_bytes_compare_raw (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; + + /* 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, 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, 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, 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, error)) + return NULL; + return g_bytes_new_take (g_steal_pointer (&data), FLASH_BLOCK_SIZE); +} + +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; +} diff --git a/plugins/parade-lspcon/fu-parade-lspcon-device.h b/plugins/parade-lspcon/fu-parade-lspcon-device.h new file mode 100644 index 000000000..683f45e6d --- /dev/null +++ b/plugins/parade-lspcon/fu-parade-lspcon-device.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2021 Peter Marheine + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_PARADE_LSPCON_DEVICE (fu_parade_lspcon_device_get_type ()) +G_DECLARE_FINAL_TYPE (FuParadeLspconDevice, + fu_parade_lspcon_device, FU, + PARADE_LSPCON_DEVICE, FuI2cDevice) diff --git a/plugins/parade-lspcon/fu-plugin-parade-lspcon.c b/plugins/parade-lspcon/fu-plugin-parade-lspcon.c new file mode 100644 index 000000000..60f0d5cfd --- /dev/null +++ b/plugins/parade-lspcon/fu-plugin-parade-lspcon.c @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2021 Peter Marheine + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "fu-parade-lspcon-device.h" + +void +fu_plugin_init (FuPlugin *plugin) +{ + fu_plugin_set_build_hash (plugin, FU_BUILD_HASH); + fu_plugin_add_udev_subsystem (plugin, "i2c"); + fu_plugin_add_device_gtype (plugin, FU_TYPE_PARADE_LSPCON_DEVICE); +} diff --git a/plugins/parade-lspcon/meson.build b/plugins/parade-lspcon/meson.build new file mode 100644 index 000000000..fa4eaca79 --- /dev/null +++ b/plugins/parade-lspcon/meson.build @@ -0,0 +1,38 @@ +if get_option('plugin_parade_lspcon') + +if not get_option('gudev') + error('gudev is required for plugin_parade_lspcon') +endif + +cargs = ['-DG_LOG_DOMAIN="FuPluginParadeLspcon"'] + +install_data(['parade-lspcon.quirk'], + install_dir: join_paths(get_option('datadir'), 'fwupd', 'quirks.d') +) + +shared_module('fu_plugin_parade_lspcon', + fu_hash, + sources : [ + 'fu-parade-lspcon-device.c', + 'fu-plugin-parade-lspcon.c', + ], + include_directories : [ + root_incdir, + fwupd_incdir, + fwupdplugin_incdir, + ], + install : true, + install_dir: plugin_dir, + link_with : [ + fwupd, + fwupdplugin, + ], + c_args : [ + cargs, + '-DLOCALSTATEDIR="' + localstatedir + '"', + ], + dependencies : [ + plugin_deps, + ], +) +endif diff --git a/plugins/parade-lspcon/parade-lspcon.quirk b/plugins/parade-lspcon/parade-lspcon.quirk new file mode 100644 index 000000000..b34672200 --- /dev/null +++ b/plugins/parade-lspcon/parade-lspcon.quirk @@ -0,0 +1,11 @@ +# match all devices with this udev subsystem +[I2C] +Plugin = parade_lspcon + +# Parade PS175 +[PARADE-LSPCON\NAME_1AF80175:00] +Name = PS175 + +# "Puff" Chromeboxes +[PARADE-LSPCON\NAME_1AF80175:00&FAMILY_Google_Hatch] +ParadeLspconAuxDeviceName=DPDDC-B