/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2017-2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include "fu-csr-device.h" #include "dfu-common.h" #include "dfu-chunked.h" #include "dfu-firmware.h" struct _FuCsrDevice { FuUsbDevice parent_instance; FuCsrDeviceQuirks quirks; DfuState dfu_state; guint32 dnload_timeout; }; G_DEFINE_TYPE (FuCsrDevice, fu_csr_device, FU_TYPE_USB_DEVICE) #define FU_CSR_REPORT_ID_COMMAND 0x01 #define FU_CSR_REPORT_ID_STATUS 0x02 #define FU_CSR_REPORT_ID_CONTROL 0x03 #define FU_CSR_COMMAND_HEADER_SIZE 6 /* bytes */ #define FU_CSR_COMMAND_UPGRADE 0x01 #define FU_CSR_STATUS_HEADER_SIZE 7 #define FU_CSR_CONTROL_HEADER_SIZE 2 /* bytes */ #define FU_CSR_CONTROL_CLEAR_STATUS 0x04 #define FU_CSR_CONTROL_RESET 0xff /* maximum firmware packet, including the command header */ #define FU_CSR_PACKET_DATA_SIZE 1023 /* bytes */ #define FU_CSR_DEVICE_TIMEOUT 5000 /* ms */ static void fu_csr_device_to_string (FuDevice *device, GString *str) { FuCsrDevice *self = FU_CSR_DEVICE (device); g_string_append (str, " DfuCsrDevice:\n"); g_string_append_printf (str, " state:\t\t%s\n", dfu_state_to_string (self->dfu_state)); g_string_append_printf (str, " timeout:\t\t%" G_GUINT32_FORMAT "\n", self->dnload_timeout); } static void fu_csr_device_dump (const gchar *title, const guint8 *buf, gsize sz) { if (g_getenv ("FWUPD_CSR_VERBOSE") == NULL) return; g_print ("%s (%" G_GSIZE_FORMAT "):\n", title, sz); for (gsize i = 0; i < sz; i++) g_print ("%02x ", buf[i]); g_print ("\n"); } void fu_csr_device_set_quirks (FuCsrDevice *self, FuCsrDeviceQuirks quirks) { self->quirks = quirks; } static gboolean fu_csr_device_attach (FuDevice *device, GError **error) { FuCsrDevice *self = FU_CSR_DEVICE (device); GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self)); gsize sz = 0; guint8 buf[] = { FU_CSR_REPORT_ID_CONTROL, FU_CSR_CONTROL_RESET }; fu_csr_device_dump ("Reset", buf, sz); if (!g_usb_device_control_transfer (usb_device, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_CLASS, G_USB_DEVICE_RECIPIENT_INTERFACE, HID_REPORT_SET, /* bRequest */ HID_FEATURE | FU_CSR_REPORT_ID_CONTROL, /* wValue */ 0x0000, /* wIndex */ buf, sizeof(buf), &sz, FU_CSR_DEVICE_TIMEOUT, /* timeout */ NULL, error)) { g_prefix_error (error, "Failed to ClearStatus: "); return FALSE; } /* check packet */ if (sz != FU_CSR_CONTROL_HEADER_SIZE) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Reset packet was %" G_GSIZE_FORMAT " expected %i", sz, FU_CSR_CONTROL_HEADER_SIZE); return FALSE; } return TRUE; } static gboolean fu_csr_device_get_status (FuCsrDevice *self, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self)); gsize sz = 0; guint8 buf[64] = {0}; /* hit hardware */ if (!g_usb_device_control_transfer (usb_device, G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST, G_USB_DEVICE_REQUEST_TYPE_CLASS, G_USB_DEVICE_RECIPIENT_INTERFACE, HID_REPORT_GET, /* bRequest */ HID_FEATURE | FU_CSR_REPORT_ID_STATUS, /* wValue */ 0x0000, /* wIndex */ buf, sizeof(buf), &sz, FU_CSR_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error (error, "Failed to GetStatus: "); return FALSE; } fu_csr_device_dump ("GetStatus", buf, sz); /* check packet */ if (sz != FU_CSR_STATUS_HEADER_SIZE) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "GetStatus packet was %" G_GSIZE_FORMAT " expected %i", sz, FU_CSR_STATUS_HEADER_SIZE); return FALSE; } if (buf[0] != FU_CSR_REPORT_ID_STATUS) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "GetStatus packet-id was %i expected %i", buf[0], FU_CSR_REPORT_ID_STATUS); return FALSE; } self->dfu_state = buf[5]; self->dnload_timeout = buf[2] + (((guint32) buf[3]) << 8) + (((guint32) buf[4]) << 16); g_debug ("timeout=%" G_GUINT32_FORMAT, self->dnload_timeout); g_debug ("state=%s", dfu_state_to_string (self->dfu_state)); g_debug ("status=%s", dfu_status_to_string (buf[6])); return TRUE; } static gboolean fu_csr_device_clear_status (FuCsrDevice *self, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self)); gsize sz = 0; guint8 buf[] = { FU_CSR_REPORT_ID_CONTROL, FU_CSR_CONTROL_CLEAR_STATUS }; /* only clear the status if the state is error */ if (!fu_csr_device_get_status (self, error)) return FALSE; if (self->dfu_state != DFU_STATE_DFU_ERROR) return TRUE; /* hit hardware */ fu_csr_device_dump ("ClearStatus", buf, sz); if (!g_usb_device_control_transfer (usb_device, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_CLASS, G_USB_DEVICE_RECIPIENT_INTERFACE, HID_REPORT_SET, /* bRequest */ HID_FEATURE | FU_CSR_REPORT_ID_CONTROL, /* wValue */ 0x0000, /* wIndex */ buf, sizeof(buf), &sz, FU_CSR_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error (error, "Failed to ClearStatus: "); return FALSE; } /* check packet */ if (sz != FU_CSR_CONTROL_HEADER_SIZE) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "ClearStatus packet was %" G_GSIZE_FORMAT " expected %i", sz, FU_CSR_CONTROL_HEADER_SIZE); return FALSE; } /* check the hardware again */ return fu_csr_device_get_status (self, error); } static GBytes * fu_csr_device_upload_chunk (FuCsrDevice *self, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self)); gsize sz = 0; guint16 data_sz; guint8 buf[64] = {0}; /* hit hardware */ if (!g_usb_device_control_transfer (usb_device, G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST, G_USB_DEVICE_REQUEST_TYPE_CLASS, G_USB_DEVICE_RECIPIENT_INTERFACE, HID_REPORT_GET, /* bRequest */ HID_FEATURE | FU_CSR_REPORT_ID_COMMAND, /* wValue */ 0x0000, /* wIndex */ buf, sizeof(buf), &sz, FU_CSR_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error (error, "Failed to ReadFirmware: "); return NULL; } fu_csr_device_dump ("ReadFirmware", buf, sz); /* too small to parse */ if (sz < FU_CSR_COMMAND_HEADER_SIZE) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "ReadFirmware packet too small, got %" G_GSIZE_FORMAT, sz); return NULL; } /* check command byte */ if (buf[0] != FU_CSR_REPORT_ID_COMMAND) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "wrong report ID %u", buf[0]); return NULL; } /* check the length */ data_sz = fu_common_read_uint16 (&buf[1], G_LITTLE_ENDIAN); if (data_sz + FU_CSR_COMMAND_HEADER_SIZE != (guint16) sz) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "wrong data length %" G_GUINT16_FORMAT, data_sz); return NULL; } /* return as bytes */ return g_bytes_new (buf + FU_CSR_COMMAND_HEADER_SIZE, sz - FU_CSR_COMMAND_HEADER_SIZE); } static GBytes * fu_csr_device_upload (FuDevice *device, GError **error) { FuCsrDevice *self = FU_CSR_DEVICE (device); g_autoptr(GPtrArray) chunks = NULL; guint32 total_sz = 0; gsize done_sz = 0; /* notify UI */ fu_device_set_status (device, FWUPD_STATUS_DEVICE_READ); chunks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref); for (guint32 i = 0; i < 0x3ffffff; i++) { g_autoptr(GBytes) chunk = NULL; gsize chunk_sz; /* hit hardware */ chunk = fu_csr_device_upload_chunk (self, error); if (chunk == NULL) return NULL; chunk_sz = g_bytes_get_size (chunk); /* get the total size using the CSR header */ if (i == 0 && chunk_sz >= 10) { const guint8 *buf = g_bytes_get_data (chunk, NULL); if (memcmp (buf, "CSR-dfu", 7) == 0) { guint16 hdr_ver; guint16 hdr_len; hdr_ver = fu_common_read_uint16 (buf + 8, G_LITTLE_ENDIAN); if (hdr_ver != 0x03) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "CSR header version is " "invalid %" G_GUINT16_FORMAT, hdr_ver); return NULL; } total_sz = fu_common_read_uint32 (buf + 10, G_LITTLE_ENDIAN); if (total_sz == 0) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "CSR header data length " "invalid %" G_GUINT32_FORMAT, total_sz); return NULL; } hdr_len = fu_common_read_uint16 (buf + 14, G_LITTLE_ENDIAN); g_debug ("CSR header length: %" G_GUINT16_FORMAT, hdr_len); } } /* add to chunk array */ done_sz += chunk_sz; g_ptr_array_add (chunks, g_steal_pointer (&chunk)); fu_device_set_progress_full (device, done_sz, (gsize) total_sz); /* we're done */ if (chunk_sz < 64 - FU_CSR_COMMAND_HEADER_SIZE) break; } /* notify UI */ fu_device_set_status (device, FWUPD_STATUS_IDLE); return dfu_utils_bytes_join_array (chunks); } static gboolean fu_csr_device_download_chunk (FuCsrDevice *self, guint16 idx, GBytes *chunk, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self)); const guint8 *chunk_data; gsize chunk_sz = 0; gsize write_sz = 0; guint8 buf[FU_CSR_PACKET_DATA_SIZE] = {0}; /* too large? */ chunk_data = g_bytes_get_data (chunk, &chunk_sz); if (chunk_sz + FU_CSR_COMMAND_HEADER_SIZE > FU_CSR_PACKET_DATA_SIZE) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "packet was too large: %" G_GSIZE_FORMAT, chunk_sz); return FALSE; } g_debug ("writing %" G_GSIZE_FORMAT " bytes of data", chunk_sz); /* create packet */ buf[0] = FU_CSR_REPORT_ID_COMMAND; buf[1] = FU_CSR_COMMAND_UPGRADE; fu_common_write_uint16 (&buf[2], idx, G_LITTLE_ENDIAN); fu_common_write_uint16 (&buf[4], chunk_sz, G_LITTLE_ENDIAN); memcpy (buf + FU_CSR_COMMAND_HEADER_SIZE, chunk_data, chunk_sz); /* hit hardware */ fu_csr_device_dump ("Upgrade", buf, sizeof(buf)); if (!g_usb_device_control_transfer (usb_device, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_CLASS, G_USB_DEVICE_RECIPIENT_INTERFACE, HID_REPORT_SET, /* bRequest */ HID_FEATURE | FU_CSR_REPORT_ID_COMMAND, /* wValue */ 0x0000, /* wIndex */ buf, sizeof(buf), &write_sz, FU_CSR_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error (error, "Failed to Upgrade: "); return FALSE; } /* check packet */ if (write_sz != sizeof(buf)) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Not all packet written for upgrade got " "%" G_GSIZE_FORMAT " expected %" G_GSIZE_FORMAT, write_sz, sizeof(buf)); return FALSE; } /* wait for hardware */ if (self->quirks & FU_CSR_DEVICE_QUIRK_REQUIRE_DELAY) { g_debug ("sleeping for %ums", self->dnload_timeout); g_usleep (self->dnload_timeout * 1000); } /* get status */ if (!fu_csr_device_get_status (self, error)) return FALSE; /* is still busy */ if (self->dfu_state == DFU_STATE_DFU_DNBUSY) { g_debug ("busy, so sleeping a bit longer"); g_usleep (G_USEC_PER_SEC); if (!fu_csr_device_get_status (self, error)) return FALSE; } /* not correct */ if (self->dfu_state != DFU_STATE_DFU_DNLOAD_IDLE && self->dfu_state != DFU_STATE_DFU_IDLE) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "device did not return to IDLE"); return FALSE; } /* success */ return TRUE; } static GBytes * _dfu_firmware_get_default_element_data (DfuFirmware *firmware) { DfuElement *element; DfuImage *image; image = dfu_firmware_get_image_default (firmware); if (image == NULL) return NULL; element = dfu_image_get_element_default (image); if (element == NULL) return NULL; return dfu_element_get_contents (element); } static gboolean fu_csr_device_download (FuDevice *device, GBytes *blob, GError **error) { FuCsrDevice *self = FU_CSR_DEVICE (device); GBytes *blob_noftr; const guint8 *data; gsize sz = 0; guint16 idx; g_autoptr(DfuFirmware) dfu_firmware = dfu_firmware_new (); g_autoptr(GBytes) blob_empty = NULL; g_autoptr(GPtrArray) packets = NULL; /* notify UI */ fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE); /* parse the file */ if (!dfu_firmware_parse_data (dfu_firmware, blob, DFU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; if (g_getenv ("FWUPD_CSR_VERBOSE") != NULL) { g_autofree gchar *fw_str = NULL; fw_str = dfu_firmware_to_string (dfu_firmware); g_debug ("%s", fw_str); } if (dfu_firmware_get_format (dfu_firmware) != DFU_FIRMWARE_FORMAT_DFU) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "expected DFU firmware"); return FALSE; } /* get the blob from the firmware file */ blob_noftr = _dfu_firmware_get_default_element_data (dfu_firmware); if (blob_noftr == NULL) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware contained no data"); return FALSE; } /* create packets */ data = g_bytes_get_data (blob_noftr, &sz); packets = dfu_chunked_new (data, (guint32) sz, 0x0, 0x0, FU_CSR_PACKET_DATA_SIZE - FU_CSR_COMMAND_HEADER_SIZE); /* send to hardware */ for (idx = 0; idx < packets->len; idx++) { DfuChunkedPacket *pkt = g_ptr_array_index (packets, idx); g_autoptr(GBytes) blob_tmp = g_bytes_new_static (pkt->data, pkt->data_sz); /* send packet */ if (!fu_csr_device_download_chunk (self, idx, blob_tmp, error)) return FALSE; /* update progress */ fu_device_set_progress_full (device, (gsize) idx, (gsize) packets->len); } /* all done */ blob_empty = g_bytes_new (NULL, 0); if (!fu_csr_device_download_chunk (self, idx, blob_empty, error)) return FALSE; /* notify UI */ fu_device_set_status (device, FWUPD_STATUS_IDLE); return TRUE; } static gboolean fu_csr_device_probe (FuUsbDevice *device, GError **error) { const gchar *quirk_str; /* devices have to be whitelisted */ quirk_str = fu_device_get_plugin_hints (FU_DEVICE (device)); if (quirk_str == NULL) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported with this device"); return FALSE; } if (g_strcmp0 (quirk_str, "require-delay") == 0) { fu_csr_device_set_quirks (FU_CSR_DEVICE (device), FU_CSR_DEVICE_QUIRK_REQUIRE_DELAY); } /* hardcoded */ fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_UPDATABLE); /* success */ return TRUE; } static gboolean fu_csr_device_open (FuUsbDevice *device, GError **error) { FuCsrDevice *self = FU_CSR_DEVICE (device); GUsbDevice *usb_device = fu_usb_device_get_dev (device); /* open device and clear status */ if (!g_usb_device_claim_interface (usb_device, 0x00, /* HID */ G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error (error, "failed to claim HID interface: "); return FALSE; } if (!fu_csr_device_clear_status (self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_csr_device_close (FuUsbDevice *device, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev (device); /* we're done here */ if (!g_usb_device_release_interface (usb_device, 0x00, /* HID */ G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error (error, "failed to release interface: "); return FALSE; } /* success */ return TRUE; } static void fu_csr_device_init (FuCsrDevice *device) { } static void fu_csr_device_class_init (FuCsrDeviceClass *klass) { FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass); FuUsbDeviceClass *klass_usb_device = FU_USB_DEVICE_CLASS (klass); klass_device->to_string = fu_csr_device_to_string; klass_device->write_firmware = fu_csr_device_download; klass_device->read_firmware = fu_csr_device_upload; klass_device->attach = fu_csr_device_attach; klass_usb_device->open = fu_csr_device_open; klass_usb_device->close = fu_csr_device_close; klass_usb_device->probe = fu_csr_device_probe; } FuCsrDevice * fu_csr_device_new (GUsbDevice *usb_device) { FuCsrDevice *device = NULL; device = g_object_new (FU_TYPE_CSR_DEVICE, "usb-device", usb_device, NULL); return device; }