fwupd/plugins/ch341a/fu-ch341a-cfi-device.c
2022-06-14 14:36:52 -05:00

444 lines
13 KiB
C

/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#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_memwrite_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_memwrite_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) 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, NULL);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 44, NULL);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 35, NULL);
/* 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_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;
}