diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index e998314b8..345745f08 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -422,6 +422,7 @@ done %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_bcm57xx.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_cfu.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_ccgx.so +%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_ch341a.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_colorhug.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_cros_ec.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_cpu.so diff --git a/libfwupdplugin/fu-cfi-device.c b/libfwupdplugin/fu-cfi-device.c index 1e9c568c0..0847a6e60 100644 --- a/libfwupdplugin/fu-cfi-device.c +++ b/libfwupdplugin/fu-cfi-device.c @@ -467,6 +467,64 @@ fu_cfi_device_to_string(FuDevice *device, guint idt, GString *str) fu_common_string_append_kx(str, idt, "BlockSize", priv->block_size); } +/** + * fu_cfi_device_chip_select: + * @self: a #FuCfiDevice + * @value: boolean + * @error: (nullable): optional return location for an error + * + * Sets the chip select value. + * + * Returns: %TRUE on success + * + * Since: 1.8.0 + **/ +gboolean +fu_cfi_device_chip_select(FuCfiDevice *self, gboolean value, GError **error) +{ + FuCfiDeviceClass *klass = FU_CFI_DEVICE_GET_CLASS(self); + g_return_val_if_fail(FU_IS_CFI_DEVICE(self), FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + if (klass->chip_select == NULL) { + g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); + return FALSE; + } + return klass->chip_select(self, value, error); +} + +static gboolean +fu_cfi_device_chip_select_assert(GObject *device, GError **error) +{ + return fu_cfi_device_chip_select(FU_CFI_DEVICE(device), TRUE, error); +} + +static gboolean +fu_cfi_device_chip_select_deassert(GObject *device, GError **error) +{ + return fu_cfi_device_chip_select(FU_CFI_DEVICE(device), FALSE, error); +} + +/** + * fu_cfi_device_chip_select_locker_new: + * @self: a #FuCfiDevice + * + * Creates a custom device locker that asserts and deasserts the chip select signal. + * + * Returns: (transfer full): (nullable): a #FuDeviceLocker + * + * Since: 1.8.0 + **/ +FuDeviceLocker * +fu_cfi_device_chip_select_locker_new(FuCfiDevice *self, GError **error) +{ + g_return_val_if_fail(FU_IS_CFI_DEVICE(self), NULL); + g_return_val_if_fail(error == NULL || *error == NULL, NULL); + return fu_device_locker_new_full(self, + fu_cfi_device_chip_select_assert, + fu_cfi_device_chip_select_deassert, + error); +} + static void fu_cfi_device_init(FuCfiDevice *self) { diff --git a/libfwupdplugin/fu-cfi-device.h b/libfwupdplugin/fu-cfi-device.h index e574570e5..1650b8afc 100644 --- a/libfwupdplugin/fu-cfi-device.h +++ b/libfwupdplugin/fu-cfi-device.h @@ -15,7 +15,8 @@ G_DECLARE_DERIVABLE_TYPE(FuCfiDevice, fu_cfi_device, FU, CFI_DEVICE, FuDevice) struct _FuCfiDeviceClass { FuDeviceClass parent_class; - gpointer __reserved[31]; + gboolean (*chip_select)(FuCfiDevice *self, gboolean value, GError **error); + gpointer __reserved[30]; }; /** @@ -70,3 +71,8 @@ void fu_cfi_device_set_block_size(FuCfiDevice *self, guint32 block_size); gboolean fu_cfi_device_get_cmd(FuCfiDevice *self, FuCfiDeviceCmd cmd, guint8 *value, GError **error); + +gboolean +fu_cfi_device_chip_select(FuCfiDevice *self, gboolean value, GError **error); +FuDeviceLocker * +fu_cfi_device_chip_select_locker_new(FuCfiDevice *self, GError **error); diff --git a/libfwupdplugin/fwupdplugin.map b/libfwupdplugin/fwupdplugin.map index 4ee22e155..e534955b9 100644 --- a/libfwupdplugin/fwupdplugin.map +++ b/libfwupdplugin/fwupdplugin.map @@ -1020,6 +1020,8 @@ LIBFWUPDPLUGIN_1.7.7 { LIBFWUPDPLUGIN_1.8.0 { global: + fu_cfi_device_chip_select; + fu_cfi_device_chip_select_locker_new; fu_common_reverse_uint8; fu_coswid_firmware_get_type; fu_coswid_firmware_new; diff --git a/plugins/ch341a/README.md b/plugins/ch341a/README.md new file mode 100644 index 000000000..9b873feee --- /dev/null +++ b/plugins/ch341a/README.md @@ -0,0 +1,47 @@ +# CH341A + +## Introduction + +The CH341A is an affordable SPI programmer. + +The assumed map between UIO command bits, pins on CH341A chip and pins on SPI chip: + + UIO CH341A SPI CH341A + 0 D0/15 CS/1 CS0 + 1 D1/16 unused CS1 + 2 D2/17 unused CS2 + 3 D3/18 SCK/6 DCK + 4 D4/19 unused DOUT2 + 5 D5/20 SI/5 DOUBT + 6 D6/21 unused DIN2 + 7 D7/22 SO/2 DIN + +## Firmware Format + +The daemon will decompress the cabinet archive and extract a firmware blob of unspecified format. + +This plugin supports the following protocol ID: + +- com.winchiphead.ch341a + +## GUID Generation + +These devices use the standard USB DeviceInstanceId values, e.g. + +- `USB\VID_1A86&PID_5512&REV_0304` +- `USB\VID_1A86&PID_5512` + +## Update Behavior + +The device programs devices in raw mode, and can best be used with `fwupdtool`. + +To write an image, use `sudo fwupdtool --plugins ch341a install-blob firmware.bin` and to backup +the contents of a SPI device use `sudo fwupdtool --plugins ch341a firmware-dump backup.bin` + +## Vendor ID Security + +The vendor ID is set from the USB vendor, in this instance set to `USB:0x1A86` + +## External Interface Access + +This plugin requires read/write access to `/dev/bus/usb`. diff --git a/plugins/ch341a/ch341a.quirk b/plugins/ch341a/ch341a.quirk new file mode 100644 index 000000000..f1744ae19 --- /dev/null +++ b/plugins/ch341a/ch341a.quirk @@ -0,0 +1,2 @@ +[USB\VID_1A86&PID_5512] +Plugin = ch341a diff --git a/plugins/ch341a/fu-ch341a-cfi-device.c b/plugins/ch341a/fu-ch341a-cfi-device.c new file mode 100644 index 000000000..55821807f --- /dev/null +++ b/plugins/ch341a/fu-ch341a-cfi-device.c @@ -0,0 +1,444 @@ +/* + * Copyright (C) 2022 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-ch341a-cfi-device.h" +#include "fu-ch341a-device.h" + +struct _FuCh341aCfiDevice { + FuCfiDevice parent_instance; +}; + +G_DEFINE_TYPE(FuCh341aCfiDevice, fu_ch341a_cfi_device, FU_TYPE_CFI_DEVICE) + +#define CH341A_PAYLOAD_SIZE 0x1A + +static gboolean +fu_ch341a_cfi_device_chip_select(FuCfiDevice *self, gboolean value, GError **error) +{ + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); + return fu_ch341a_device_chip_select(proxy, value, error); +} + +typedef struct { + guint8 mask; + guint8 value; +} FuCh341aCfiDeviceHelper; + +static gboolean +fu_ch341a_cfi_device_wait_for_status_cb(FuDevice *device, gpointer user_data, GError **error) +{ + FuCh341aCfiDeviceHelper *helper = (FuCh341aCfiDeviceHelper *)user_data; + FuCh341aCfiDevice *self = FU_CH341A_CFI_DEVICE(device); + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(device)); + guint8 buf[2] = {0x0}; + g_autoptr(FuDeviceLocker) cslocker = NULL; + + /* enable chip */ + cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); + if (cslocker == NULL) + return FALSE; + if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), + FU_CFI_DEVICE_CMD_READ_STATUS, + &buf[0], + error)) + return FALSE; + if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) { + g_prefix_error(error, "failed to want to status: "); + return FALSE; + } + if ((buf[0x1] & helper->mask) != helper->value) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_INTERNAL, + "wanted 0x%x, got 0x%x", + helper->value, + buf[0x1] & helper->mask); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_ch341a_cfi_device_wait_for_status(FuCh341aCfiDevice *self, + guint8 mask, + guint8 value, + guint count, + guint delay, + GError **error) +{ + FuCh341aCfiDeviceHelper helper = {.mask = mask, .value = value}; + return fu_device_retry_full(FU_DEVICE(self), + fu_ch341a_cfi_device_wait_for_status_cb, + count, + delay, + &helper, + error); +} + +static gboolean +fu_ch341a_cfi_device_read_jedec(FuCh341aCfiDevice *self, GError **error) +{ + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); + guint8 buf[CH341A_PAYLOAD_SIZE] = {0x9F}; + g_autoptr(FuDeviceLocker) cslocker = NULL; + g_autoptr(GString) flash_id = g_string_new(NULL); + + /* enable chip */ + cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); + if (cslocker == NULL) + return FALSE; + + /* read JEDEC ID */ + if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) { + g_prefix_error(error, "failed to request JEDEC ID: "); + return FALSE; + } + if (buf[1] == 0x0 && buf[2] == 0x0 && buf[3] == 0x0) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "flash ID non-valid"); + return FALSE; + } + if (buf[1] == 0xFF && buf[2] == 0xFF && buf[3] == 0xFF) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "device not detected"); + return FALSE; + } + g_string_append_printf(flash_id, "%02X", buf[1]); + g_string_append_printf(flash_id, "%02X", buf[2]); + g_string_append_printf(flash_id, "%02X", buf[3]); + fu_cfi_device_set_flash_id(FU_CFI_DEVICE(self), flash_id->str); + + /* success */ + return TRUE; +} + +static gboolean +fu_ch341a_cfi_device_setup(FuDevice *device, GError **error) +{ + FuCh341aCfiDevice *self = FU_CH341A_CFI_DEVICE(device); + + /* setup SPI chip */ + if (!fu_ch341a_cfi_device_read_jedec(self, error)) + return FALSE; + + /* this is a generic SPI chip */ + fu_device_add_instance_id(device, "SPI"); + fu_device_add_vendor_id(device, "SPI:*"); + + /* FuCfiDevice->setup */ + return FU_DEVICE_CLASS(fu_ch341a_cfi_device_parent_class)->setup(device, error); +} + +static gboolean +fu_ch341a_cfi_device_write_enable(FuCh341aCfiDevice *self, GError **error) +{ + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); + guint8 buf[1] = {0x0}; + g_autoptr(FuDeviceLocker) cslocker = NULL; + + /* write enable */ + if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), FU_CFI_DEVICE_CMD_WRITE_EN, &buf[0], error)) + return FALSE; + cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); + if (cslocker == NULL) + return FALSE; + if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) + return FALSE; + if (!fu_device_locker_close(cslocker, error)) + return FALSE; + + /* check that WEL is now set */ + return fu_ch341a_cfi_device_wait_for_status(self, 0b10, 0b10, 10, 5, error); +} + +static gboolean +fu_ch341a_cfi_device_chip_erase(FuCh341aCfiDevice *self, GError **error) +{ + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); + guint8 buf[] = {0x0}; + g_autoptr(FuDeviceLocker) cslocker = NULL; + + /* enable chip */ + cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); + if (cslocker == NULL) + return FALSE; + + /* erase */ + if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), + FU_CFI_DEVICE_CMD_CHIP_ERASE, + &buf[0], + error)) + return FALSE; + if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) + return FALSE; + if (!fu_device_locker_close(cslocker, error)) + return FALSE; + + /* poll Read Status register BUSY */ + return fu_ch341a_cfi_device_wait_for_status(self, 0b1, 0b0, 100, 500, error); +} + +static gboolean +fu_ch341a_cfi_device_write_page(FuCh341aCfiDevice *self, FuChunk *page, GError **error) +{ + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); + guint8 buf[4] = {0x0}; + g_autoptr(GPtrArray) chunks = NULL; + g_autoptr(FuDeviceLocker) cslocker = NULL; + + if (!fu_ch341a_cfi_device_write_enable(self, error)) + return FALSE; + + cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); + if (cslocker == NULL) + return FALSE; + + /* cmd, then 24 bit starting address */ + fu_common_write_uint32(buf, fu_chunk_get_address(page), G_BIG_ENDIAN); + if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), + FU_CFI_DEVICE_CMD_PAGE_PROG, + &buf[0], + error)) + return FALSE; + if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) + return FALSE; + + /* send data */ + chunks = fu_chunk_array_new(fu_chunk_get_data(page), + fu_chunk_get_data_sz(page), + 0x0, + 0x0, + CH341A_PAYLOAD_SIZE); + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + guint8 buf2[CH341A_PAYLOAD_SIZE] = {0x0}; + if (!fu_memcpy_safe(buf2, + sizeof(buf2), + 0x0, /* dst */ + fu_chunk_get_data(chk), + fu_chunk_get_data_sz(chk), + 0x0, /* src */ + fu_chunk_get_data_sz(chk), + error)) + return FALSE; + if (!fu_ch341a_device_spi_transfer(proxy, buf2, fu_chunk_get_data_sz(chk), error)) + return FALSE; + } + if (!fu_device_locker_close(cslocker, error)) + return FALSE; + + /* poll Read Status register BUSY */ + return fu_ch341a_cfi_device_wait_for_status(self, 0b1, 0b0, 100, 50, error); +} + +static gboolean +fu_ch341a_cfi_device_write_pages(FuCh341aCfiDevice *self, + GPtrArray *pages, + FuProgress *progress, + GError **error) +{ + /* progress */ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_steps(progress, pages->len); + for (guint i = 0; i < pages->len; i++) { + FuChunk *page = g_ptr_array_index(pages, i); + if (!fu_ch341a_cfi_device_write_page(self, page, error)) + return FALSE; + fu_progress_step_done(progress); + } + /* success */ + return TRUE; +} + +static GBytes * +fu_ch341a_cfi_device_read_firmware(FuCh341aCfiDevice *self, + gsize bufsz, + FuProgress *progress, + GError **error) +{ + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); + guint8 buf[CH341A_PAYLOAD_SIZE] = {0x0}; + g_autoptr(FuDeviceLocker) cslocker = NULL; + g_autoptr(GByteArray) blob = g_byte_array_new(); + g_autoptr(GPtrArray) chunks = NULL; + + /* enable chip */ + cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); + if (cslocker == NULL) + return NULL; + + /* read each block */ + chunks = fu_chunk_array_new(NULL, bufsz + 0x4, 0x0, 0x0, CH341A_PAYLOAD_SIZE); + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_steps(progress, chunks->len); + fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); + + /* cmd, then 24 bit starting address */ + fu_common_write_uint32(buf, 0x0, G_BIG_ENDIAN); + if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), + FU_CFI_DEVICE_CMD_READ_DATA, + &buf[0], + error)) + return NULL; + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + + /* the first package has cmd and address info */ + if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) + return NULL; + if (i == 0) { + g_byte_array_append(blob, buf + 0x4, fu_chunk_get_data_sz(chk) - 0x4); + } else { + g_byte_array_append(blob, buf + 0x0, fu_chunk_get_data_sz(chk)); + } + + /* done */ + fu_progress_step_done(progress); + } + + /* success */ + return g_byte_array_free_to_bytes(g_steal_pointer(&blob)); +} + +static gboolean +fu_ch341a_cfi_device_write_firmware(FuDevice *device, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + FuCh341aCfiDevice *self = FU_CH341A_CFI_DEVICE(device); + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); + g_autoptr(GBytes) fw = NULL; + g_autoptr(GBytes) fw_verify = NULL; + g_autoptr(GPtrArray) pages = NULL; + g_autoptr(FuDeviceLocker) cslocker = NULL; + g_autoptr(FuDeviceLocker) locker = NULL; + + /* open programmer */ + locker = fu_device_locker_new(proxy, error); + if (locker == NULL) + return FALSE; + + /* 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, 33); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 44); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 35); + + /* get default image */ + fw = fu_firmware_get_bytes(firmware, error); + if (fw == NULL) + return FALSE; + + /* erase */ + if (!fu_ch341a_cfi_device_write_enable(self, error)) { + g_prefix_error(error, "failed to enable writes: "); + return FALSE; + } + if (!fu_ch341a_cfi_device_chip_erase(self, error)) { + g_prefix_error(error, "failed to erase: "); + return FALSE; + } + fu_progress_step_done(progress); + + /* write each block */ + pages = fu_chunk_array_new_from_bytes(fw, + 0x0, + 0x0, + fu_cfi_device_get_page_size(FU_CFI_DEVICE(self))); + if (!fu_ch341a_cfi_device_write_pages(self, + pages, + fu_progress_get_child(progress), + error)) { + g_prefix_error(error, "failed to write pages: "); + return FALSE; + } + fu_progress_step_done(progress); + + /* verify each block */ + fw_verify = fu_ch341a_cfi_device_read_firmware(self, + g_bytes_get_size(fw), + fu_progress_get_child(progress), + error); + if (fw_verify == NULL) { + g_prefix_error(error, "failed to verify blocks: "); + return FALSE; + } + if (!fu_common_bytes_compare(fw, fw_verify, error)) + return FALSE; + fu_progress_step_done(progress); + + /* success! */ + return TRUE; +} + +static GBytes * +fu_ch341a_cfi_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) +{ + FuCh341aCfiDevice *self = FU_CH341A_CFI_DEVICE(device); + FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); + gsize bufsz = fu_device_get_firmware_size_max(device); + g_autoptr(FuDeviceLocker) locker = NULL; + + /* open programmer */ + locker = fu_device_locker_new(proxy, error); + if (locker == NULL) + return NULL; + + /* sanity check */ + if (bufsz == 0x0) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_READ, + "device firmware size not set"); + return NULL; + } + return fu_ch341a_cfi_device_read_firmware(self, bufsz, progress, error); +} + +static void +fu_ch341a_cfi_device_set_progress(FuDevice *self, FuProgress *progress) +{ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* detach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100); /* write */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* attach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0); /* reload */ +} + +static void +fu_ch341a_cfi_device_init(FuCh341aCfiDevice *self) +{ + fu_device_add_protocol(FU_DEVICE(self), "org.jedec.cfi"); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); +} + +static void +fu_ch341a_cfi_device_class_init(FuCh341aCfiDeviceClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + FuCfiDeviceClass *klass_cfi = FU_CFI_DEVICE_CLASS(klass); + + klass_cfi->chip_select = fu_ch341a_cfi_device_chip_select; + + klass_device->setup = fu_ch341a_cfi_device_setup; + klass_device->write_firmware = fu_ch341a_cfi_device_write_firmware; + klass_device->dump_firmware = fu_ch341a_cfi_device_dump_firmware; + klass_device->set_progress = fu_ch341a_cfi_device_set_progress; +} diff --git a/plugins/ch341a/fu-ch341a-cfi-device.h b/plugins/ch341a/fu-ch341a-cfi-device.h new file mode 100644 index 000000000..91814b2ba --- /dev/null +++ b/plugins/ch341a/fu-ch341a-cfi-device.h @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2022 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_CH341A_CFI_DEVICE (fu_ch341a_cfi_device_get_type()) +G_DECLARE_FINAL_TYPE(FuCh341aCfiDevice, fu_ch341a_cfi_device, FU, CH341A_CFI_DEVICE, FuCfiDevice) diff --git a/plugins/ch341a/fu-ch341a-device.c b/plugins/ch341a/fu-ch341a-device.c new file mode 100644 index 000000000..fcead588f --- /dev/null +++ b/plugins/ch341a/fu-ch341a-device.c @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2022 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-ch341a-cfi-device.h" +#include "fu-ch341a-device.h" + +struct _FuCh341aDevice { + FuUsbDevice parent_instance; + guint8 speed; +}; + +G_DEFINE_TYPE(FuCh341aDevice, fu_ch341a_device, FU_TYPE_USB_DEVICE) + +#define CH341A_USB_TIMEOUT 1000 +#define CH341A_EP_OUT 0x02 /* host to device (write) */ +#define CH341A_EP_IN 0x82 /* device to host (read) */ +#define CH341A_EP_SIZE 0x20 + +#define CH341A_CMD_SET_OUTPUT 0xA1 +#define CH341A_CMD_IO_ADDR 0xA2 +#define CH341A_CMD_PRINT_OUT 0xA3 +#define CH341A_CMD_SPI_STREAM 0xA8 +#define CH341A_CMD_SIO_STREAM 0xA9 +#define CH341A_CMD_I2C_STREAM 0xAA +#define CH341A_CMD_UIO_STREAM 0xAB + +#define CH341A_CMD_I2C_STM_START 0x74 +#define CH341A_CMD_I2C_STM_STOP 0x75 +#define CH341A_CMD_I2C_STM_OUT 0x80 +#define CH341A_CMD_I2C_STM_IN 0xC0 +#define CH341A_CMD_I2C_STM_SET 0x60 +#define CH341A_CMD_I2C_STM_US 0x40 +#define CH341A_CMD_I2C_STM_MS 0x50 +#define CH341A_CMD_I2C_STM_DLY 0x0F +#define CH341A_CMD_I2C_STM_END 0x00 + +#define CH341A_CMD_UIO_STM_IN 0x00 +#define CH341A_CMD_UIO_STM_DIR 0x40 +#define CH341A_CMD_UIO_STM_OUT 0x80 +#define CH341A_CMD_UIO_STM_US 0xC0 +#define CH341A_CMD_UIO_STM_END 0x20 + +#define CH341A_STM_I2C_SPEED_LOW 0x00 +#define CH341A_STM_I2C_SPEED_STANDARD 0x01 +#define CH341A_STM_I2C_SPEED_FAST 0x02 +#define CH341A_STM_I2C_SPEED_HIGH 0x03 + +#define CH341A_STM_SPI_MODUS_STANDARD 0x00 +#define CH341A_STM_SPI_MODUS_DOUBLE 0x04 + +#define CH341A_STM_SPI_ENDIAN_BIG 0x0 +#define CH341A_STM_SPI_ENDIAN_LITTLE 0x80 + +static const gchar * +fu_ch341a_device_speed_to_string(guint8 speed) +{ + if (speed == CH341A_STM_I2C_SPEED_LOW) + return "20kHz"; + if (speed == CH341A_STM_I2C_SPEED_STANDARD) + return "100kHz"; + if (speed == CH341A_STM_I2C_SPEED_FAST) + return "400kHz"; + if (speed == CH341A_STM_I2C_SPEED_HIGH) + return "750kHz"; + if (speed == (CH341A_STM_I2C_SPEED_LOW | CH341A_STM_SPI_MODUS_DOUBLE)) + return "2*20kHz"; + if (speed == (CH341A_STM_I2C_SPEED_STANDARD | CH341A_STM_SPI_MODUS_DOUBLE)) + return "2*100kHz"; + if (speed == (CH341A_STM_I2C_SPEED_FAST | CH341A_STM_SPI_MODUS_DOUBLE)) + return "2*400kHz"; + if (speed == (CH341A_STM_I2C_SPEED_HIGH | CH341A_STM_SPI_MODUS_DOUBLE)) + return "2*750kHz"; + return NULL; +} + +static void +fu_ch341a_device_to_string(FuDevice *device, guint idt, GString *str) +{ + FuCh341aDevice *self = FU_CH341A_DEVICE(device); + + /* FuUsbDevice->to_string */ + FU_DEVICE_CLASS(fu_ch341a_device_parent_class)->to_string(device, idt, str); + + fu_common_string_append_kv(str, + idt, + "Speed", + fu_ch341a_device_speed_to_string(self->speed)); +} + +static gboolean +fu_ch341a_device_write(FuCh341aDevice *self, guint8 *buf, gsize bufsz, GError **error) +{ + GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self)); + gsize actual_length = 0; + + /* debug */ + if (g_getenv("FWUPD_CH341A_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "write", buf, bufsz); + + if (!g_usb_device_bulk_transfer(usb_device, + CH341A_EP_OUT, + buf, + bufsz, + &actual_length, + CH341A_USB_TIMEOUT, + NULL, + error)) { + g_prefix_error(error, "failed to write 0x%x bytes:", (guint)bufsz); + return FALSE; + } + if (bufsz != actual_length) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_INTERNAL, + "only wrote 0x%x of 0x%x", + (guint)actual_length, + (guint)bufsz); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_ch341a_device_read(FuCh341aDevice *self, guint8 *buf, gsize bufsz, GError **error) +{ + GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self)); + gsize actual_length = 0; + + if (!g_usb_device_bulk_transfer(usb_device, + CH341A_EP_IN, + buf, + bufsz, + &actual_length, + CH341A_USB_TIMEOUT, + NULL, + error)) { + g_prefix_error(error, "failed to read 0x%x bytes: ", (guint)bufsz); + return FALSE; + } + if (bufsz != actual_length) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_INTERNAL, + "only read 0x%x of 0x%x", + (guint)actual_length, + (guint)bufsz); + return FALSE; + } + + /* debug */ + if (g_getenv("FWUPD_CH341A_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "read", buf, bufsz); + + /* success */ + return TRUE; +} + +gboolean +fu_ch341a_device_spi_transfer(FuCh341aDevice *self, guint8 *buf, gsize bufsz, GError **error) +{ + gsize buf2sz = bufsz + 1; + g_autofree guint8 *buf2 = g_malloc0(buf2sz); + + /* requires LSB first */ + buf2[0] = CH341A_CMD_SPI_STREAM; + for (gsize i = 0; i < bufsz; i++) + buf2[i + 1] = fu_common_reverse_uint8(buf[i]); + + /* debug */ + if (g_getenv("FWUPD_CH341A_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "SPIwrite", buf, bufsz); + if (!fu_ch341a_device_write(self, buf2, buf2sz, error)) + return FALSE; + + if (!fu_ch341a_device_read(self, buf, bufsz, error)) + return FALSE; + + /* requires LSB first */ + for (gsize i = 0; i < bufsz; i++) + buf[i] = fu_common_reverse_uint8(buf[i]); + + /* debug */ + if (g_getenv("FWUPD_CH341A_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "SPIread", buf, bufsz); + + /* success */ + return TRUE; +} + +static gboolean +fu_ch341a_device_configure_stream(FuCh341aDevice *self, GError **error) +{ + guint8 buf[] = {CH341A_CMD_I2C_STREAM, + CH341A_CMD_I2C_STM_SET | self->speed, + CH341A_CMD_I2C_STM_END}; + if (!fu_ch341a_device_write(self, buf, sizeof(buf), error)) { + g_prefix_error(error, "failed to configure stream: "); + return FALSE; + } + + /* success */ + return TRUE; +} + +gboolean +fu_ch341a_device_chip_select(FuCh341aDevice *self, gboolean val, GError **error) +{ + guint8 buf[] = { + CH341A_CMD_UIO_STREAM, + CH341A_CMD_UIO_STM_OUT | (val ? 0x36 : 0x37), /* CS* high, SCK=0, DOUBT*=1 */ + CH341A_CMD_UIO_STM_DIR | (val ? 0x3F : 0x00), /* pin direction */ + CH341A_CMD_UIO_STM_END, + }; + return fu_ch341a_device_write(self, buf, sizeof(buf), error); +} + +static gboolean +fu_ch341a_device_setup(FuDevice *device, GError **error) +{ + FuCh341aDevice *self = FU_CH341A_DEVICE(device); + g_autoptr(FuCh341aCfiDevice) cfi_device = NULL; + + /* FuUsbDevice->setup */ + if (!FU_DEVICE_CLASS(fu_ch341a_device_parent_class)->setup(device, error)) + return FALSE; + + /* set speed */ + if (!fu_ch341a_device_configure_stream(self, error)) + return FALSE; + + /* setup SPI chip */ + cfi_device = g_object_new(FU_TYPE_CH341A_CFI_DEVICE, + "context", + fu_device_get_context(FU_DEVICE(self)), + "proxy", + FU_DEVICE(self), + "logical-id", + "SPI", + NULL); + if (!fu_device_setup(FU_DEVICE(cfi_device), error)) + return FALSE; + fu_device_add_child(device, FU_DEVICE(cfi_device)); + + /* success */ + return TRUE; +} + +static void +fu_ch341a_device_init(FuCh341aDevice *self) +{ + self->speed = CH341A_STM_I2C_SPEED_STANDARD; + fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x0); + fu_device_set_name(FU_DEVICE(self), "CH341A"); + fu_device_set_vendor(FU_DEVICE(self), "WinChipHead"); +} + +static void +fu_ch341a_device_class_init(FuCh341aDeviceClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + klass_device->setup = fu_ch341a_device_setup; + klass_device->to_string = fu_ch341a_device_to_string; +} diff --git a/plugins/ch341a/fu-ch341a-device.h b/plugins/ch341a/fu-ch341a-device.h new file mode 100644 index 000000000..ef3d1cb7d --- /dev/null +++ b/plugins/ch341a/fu-ch341a-device.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2022 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_CH341A_DEVICE (fu_ch341a_device_get_type()) +G_DECLARE_FINAL_TYPE(FuCh341aDevice, fu_ch341a_device, FU, CH341A_DEVICE, FuUsbDevice) + +gboolean +fu_ch341a_device_chip_select(FuCh341aDevice *self, gboolean val, GError **error); +gboolean +fu_ch341a_device_spi_transfer(FuCh341aDevice *self, guint8 *buf, gsize bufsz, GError **error); diff --git a/plugins/ch341a/fu-plugin-ch341a.c b/plugins/ch341a/fu-plugin-ch341a.c new file mode 100644 index 000000000..558d061db --- /dev/null +++ b/plugins/ch341a/fu-plugin-ch341a.c @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-ch341a-device.h" + +static void +fu_plugin_ch341a_init(FuPlugin *plugin) +{ + fu_plugin_add_device_gtype(plugin, FU_TYPE_CH341A_DEVICE); +} + +void +fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs) +{ + vfuncs->build_hash = FU_BUILD_HASH; + vfuncs->init = fu_plugin_ch341a_init; +} diff --git a/plugins/ch341a/lsusb.txt b/plugins/ch341a/lsusb.txt new file mode 100644 index 000000000..93cd47de2 --- /dev/null +++ b/plugins/ch341a/lsusb.txt @@ -0,0 +1,68 @@ +Bus 001 Device 124: ID 1a86:5512 QinHeng Electronics CH341 in EPP/MEM/I2C mode, EPP/I2C adapter +Device Descriptor: + bLength 18 + bDescriptorType 1 + bcdUSB 1.10 + bDeviceClass 255 Vendor Specific Class + bDeviceSubClass 0 + bDeviceProtocol 2 + bMaxPacketSize0 8 + idVendor 0x1a86 QinHeng Electronics + idProduct 0x5512 CH341 in EPP/MEM/I2C mode, EPP/I2C adapter + bcdDevice 3.04 + iManufacturer 0 + iProduct 0 + iSerial 0 + bNumConfigurations 1 + Configuration Descriptor: + bLength 9 + bDescriptorType 2 + wTotalLength 0x0027 + bNumInterfaces 1 + bConfigurationValue 1 + iConfiguration 0 + bmAttributes 0x80 + (Bus Powered) + MaxPower 96mA + Interface Descriptor: + bLength 9 + bDescriptorType 4 + bInterfaceNumber 0 + bAlternateSetting 0 + bNumEndpoints 3 + bInterfaceClass 255 Vendor Specific Class + bInterfaceSubClass 1 + bInterfaceProtocol 2 + iInterface 0 + Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x82 EP 2 IN + bmAttributes 2 + Transfer Type Bulk + Synch Type None + Usage Type Data + wMaxPacketSize 0x0020 1x 32 bytes + bInterval 0 + Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x02 EP 2 OUT + bmAttributes 2 + Transfer Type Bulk + Synch Type None + Usage Type Data + wMaxPacketSize 0x0020 1x 32 bytes + bInterval 0 + Endpoint Descriptor: + bLength 7 + bDescriptorType 5 + bEndpointAddress 0x81 EP 1 IN + bmAttributes 3 + Transfer Type Interrupt + Synch Type None + Usage Type Data + wMaxPacketSize 0x0008 1x 8 bytes + bInterval 1 +Device Status: 0x0000 + (Bus Powered) diff --git a/plugins/ch341a/meson.build b/plugins/ch341a/meson.build new file mode 100644 index 000000000..980ba35ec --- /dev/null +++ b/plugins/ch341a/meson.build @@ -0,0 +1,33 @@ +if gusb.found() +cargs = ['-DG_LOG_DOMAIN="FuPluginCh341a"'] + +install_data([ + 'ch341a.quirk', + ], + install_dir: join_paths(datadir, 'fwupd', 'quirks.d') +) + +shared_module('fu_plugin_ch341a', + fu_hash, + sources : [ + 'fu-ch341a-cfi-device.c', + 'fu-ch341a-device.c', + 'fu-plugin-ch341a.c', + ], + include_directories : [ + root_incdir, + fwupd_incdir, + fwupdplugin_incdir, + ], + install : true, + install_dir: plugin_dir, + link_with : [ + fwupd, + fwupdplugin, + ], + c_args : cargs, + dependencies : [ + plugin_deps, + ], +) +endif diff --git a/plugins/meson.build b/plugins/meson.build index 2e3ddceee..b23a42dd3 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -23,6 +23,7 @@ subdir('bcm57xx') subdir('bios') subdir('ccgx') subdir('cfu') +subdir('ch341a') subdir('colorhug') subdir('cpu') subdir('cros-ec')