[usb-dfu] LoadFirmware impl and updated test app.

Currently doesn't enter application mode as we are missing USB reset.

Renamed and modified usb-test-fwloader to also work for DFU devices.

ZX-3155 #comment

TEST= usb-fwloader -d -f firmware_image
Firmware downloaded to DFU device with no errors.

Verified usb-fwloader still works for fx3 test device.

Change-Id: I05ee050198b90fad582a47ffa51e3d9395eb69f2
diff --git a/system/dev/usb/usb-dfu/usb-dfu.cpp b/system/dev/usb/usb-dfu/usb-dfu.cpp
index 137a879..f602ea6 100644
--- a/system/dev/usb/usb-dfu/usb-dfu.cpp
+++ b/system/dev/usb/usb-dfu/usb-dfu.cpp
@@ -17,6 +17,8 @@
 
 namespace {
 
+constexpr uint32_t kReqTimeoutSecs = 1;
+
 inline uint8_t MSB(int n) { return static_cast<uint8_t>(n >> 8); }
 inline uint8_t LSB(int n) { return static_cast<uint8_t>(n & 0xFF); }
 
@@ -27,8 +29,9 @@
 }
 
 zx_status_t fidl_LoadFirmware(void* ctx, const fuchsia_mem_Buffer* firmware, fidl_txn_t* txn) {
-    // TODO(jocelyndang): implement this.
-    return zircon_usb_test_fwloader_DeviceLoadFirmware_reply(txn, ZX_ERR_NOT_SUPPORTED);
+    auto fp = static_cast<usb::Dfu*>(ctx);
+    zx_status_t status = fp->LoadFirmware(zx::vmo(firmware->vmo), firmware->size);
+    return zircon_usb_test_fwloader_DeviceLoadFirmware_reply(txn, status);
 }
 
 zircon_usb_test_fwloader_Device_ops_t fidl_ops = {
@@ -40,6 +43,157 @@
 
 namespace usb {
 
+zx_status_t Dfu::ControlReq(uint8_t dir, uint8_t request, uint16_t value,
+                            void* data, size_t length, size_t* out_length) {
+    if (dir != USB_DIR_OUT && dir != USB_DIR_IN) {
+        return ZX_ERR_INVALID_ARGS;
+    }
+    zx_status_t status = usb_control(&usb_, dir | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
+                                     request, value, intf_num_, data, length,
+                                     ZX_SEC(kReqTimeoutSecs), out_length);
+    if (status == ZX_ERR_IO_REFUSED || status == ZX_ERR_IO_INVALID) {
+        usb_reset_endpoint(&usb_, 0);
+    }
+    return status;
+}
+
+zx_status_t Dfu::Download(uint16_t block_num, uint8_t* buf, size_t len_to_write) {
+    if (len_to_write > func_desc_.wTransferSize) {
+        return ZX_ERR_INVALID_ARGS;
+    }
+    size_t out_len;
+    zx_status_t status = ControlReq(USB_DIR_OUT, USB_DFU_DNLOAD,
+                                    block_num, buf, len_to_write, &out_len);
+
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "DNLOAD returned err %d\n", status);
+        return status;
+    } else if (out_len != len_to_write) {
+        zxlogf(ERROR, "DNLOAD returned bad len, want: %lu, got: %lu\n", len_to_write, out_len);
+        return ZX_ERR_IO;
+    }
+    return ZX_OK;
+}
+
+zx_status_t Dfu::GetStatus(usb_dfu_get_status_data_t* out_status) {
+    size_t want_len = sizeof(*out_status);
+    size_t out_len;
+    zx_status_t status = ControlReq(USB_DIR_IN, USB_DFU_GET_STATUS,
+                                    0, out_status, want_len, &out_len);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "GET_STATUS returned err %d\n", status);
+        return status;
+    } else if (out_len != want_len) {
+        zxlogf(ERROR, "GET_STATUS returned bad len, want: %lu, got: %lu\n", want_len, out_len);
+        return ZX_ERR_IO;
+    }
+    return ZX_OK;
+}
+
+zx_status_t Dfu::ClearStatus() {
+    size_t out_len;
+    zx_status_t status = ControlReq(USB_DIR_OUT, USB_DFU_CLR_STATUS, 0, nullptr, 0, &out_len);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "CLR_STATUS returned err %d\n", status);
+        return status;
+    }
+    return ZX_OK;
+}
+
+zx_status_t Dfu::GetState(uint8_t* out_state) {
+    size_t want_len = sizeof(*out_state);
+    size_t out_len;
+    zx_status_t status = ControlReq(USB_DIR_IN, USB_DFU_GET_STATE,
+                                    0, out_state, want_len, &out_len);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "GET_STATE returned err %d\n", status);
+        return status;
+    } else if (out_len != want_len) {
+        zxlogf(ERROR, "GET_STATE returned bad len, want: %lu, got: %lu\n", want_len, out_len);
+        return ZX_ERR_IO;
+    }
+    return ZX_OK;
+}
+
+zx_status_t Dfu::LoadFirmware(zx::vmo fw_vmo, size_t fw_size) {
+    if (fw_size == 0) {
+        return ZX_ERR_INVALID_ARGS;
+    }
+    size_t vmo_size;
+    zx_status_t status = fw_vmo.get_size(&vmo_size);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "failed to get firmware vmo size, err: %d\n", status);
+        return ZX_ERR_INVALID_ARGS;
+    }
+    if (vmo_size < fw_size) {
+        zxlogf(ERROR, "invalid vmo, vmo size was %lu, fw size was %lu\n", vmo_size, fw_size);
+        return ZX_ERR_INVALID_ARGS;
+    }
+
+    // We need to be in the DFU Idle state.
+    uint8_t state;
+    status = GetState(&state);
+    if (status != ZX_OK) {
+        return status;
+    }
+    switch (state) {
+    case USB_DFU_STATE_DFU_IDLE:
+        break;
+    case USB_DFU_STATE_DFU_ERROR:
+        // We can get back to the DFU Idle state by clearing the error status.
+        // USB DFU Spec Rev, 1.1, Table A.2.11.
+        zxlogf(ERROR, "device is in dfuERROR state, trying to clear error status...\n");
+        status = ClearStatus();
+        if (status != ZX_OK) {
+            zxlogf(ERROR, "could not clear error status, got err: %d\n", status);
+            return status;
+        }
+        break;
+    default:
+        // TODO(jocelyndang): handle more states.
+        zxlogf(ERROR, "device is in an unexpected state: %u\n", state);
+        return ZX_ERR_BAD_STATE;
+    }
+
+    // Write the firmware to the device.
+    // We just need to slice the firmware image into N pieces and call the USB_DFU_DNLOAD command.
+    size_t vmo_offset = 0;
+    // The block number is incremented per transfer.
+    uint16_t block_num = 0;
+    uint8_t write_buf[func_desc_.wTransferSize];
+
+    size_t len_to_write;
+    do {
+        len_to_write = fbl::min(fw_size - vmo_offset,
+                                static_cast<size_t>(func_desc_.wTransferSize));
+        zxlogf(TRACE, "fetching block %u, offset %lu len %lu\n",
+               block_num, vmo_offset, len_to_write);
+        zx_status_t status = fw_vmo.read(write_buf, vmo_offset, len_to_write);
+        if (status != ZX_OK) {
+            return status;
+        }
+        status = Download(block_num, write_buf, len_to_write);
+        if (status != ZX_OK) {
+            return status;
+        }
+        usb_dfu_get_status_data_t dfu_status;
+        status = GetStatus(&dfu_status);
+        if (status != ZX_OK) {
+            return status;
+        }
+        if (dfu_status.bStatus != USB_DFU_STATUS_OK) {
+            zxlogf(ERROR, "bad status %u\n", dfu_status.bStatus);
+            return ZX_ERR_IO;
+        }
+        // The device expects the block number to wrap around to zero, so no need to bounds check.
+        block_num++;
+        vmo_offset += len_to_write;
+    } while (len_to_write != 0);  // The device expects a zero length transfer to signify the end.
+    // TODO(jocelyndang): issue a USB Reset to enter Application Mode.
+
+    return ZX_OK;
+}
+
 zx_status_t Dfu::DdkMessage(fidl_msg_t* msg, fidl_txn_t* txn) {
     return zircon_usb_test_fwloader_Device_dispatch(this, txn, msg, &fidl_ops);
 }
@@ -98,7 +252,7 @@
     }
 
     fbl::AllocChecker ac;
-    auto dev = fbl::make_unique_checked<Dfu>(&ac, parent, intf_num, func_desc);
+    auto dev = fbl::make_unique_checked<Dfu>(&ac, parent, usb, intf_num, func_desc);
     if (!ac.check()) {
         return ZX_ERR_NO_MEMORY;
     }
diff --git a/system/dev/usb/usb-dfu/usb-dfu.h b/system/dev/usb/usb-dfu/usb-dfu.h
index f5de19c..b241084 100644
--- a/system/dev/usb/usb-dfu/usb-dfu.h
+++ b/system/dev/usb/usb-dfu/usb-dfu.h
@@ -19,8 +19,10 @@
             public ddk::EmptyProtocol<ZX_PROTOCOL_USB_TEST_FWLOADER> {
 
 public:
-    Dfu(zx_device_t* parent, uint8_t intf_num, const usb_dfu_func_desc_t& func_desc)
+    Dfu(zx_device_t* parent, const usb_protocol_t& usb,
+        uint8_t intf_num, const usb_dfu_func_desc_t& func_desc)
         : DfuBase(parent),
+          usb_(usb),
           intf_num_(intf_num),
           func_desc_(func_desc) {}
 
@@ -32,9 +34,31 @@
     void DdkUnbind() { DdkRemove(); }
     void DdkRelease() { delete this; }
 
+    // FIDL message implementation.
+    zx_status_t LoadFirmware(zx::vmo fw_vmo, size_t fw_size);
+
 private:
     zx_status_t Bind();
 
+    // Sends a USB control request to the device, and resets the control endpoint if stalled.
+    // Returns ZX_OK if the request succeeded.
+    zx_status_t ControlReq(uint8_t dir, uint8_t request, uint16_t value,
+                           void* data, size_t length, size_t* out_length);
+
+    // Downloads the data block to the device.
+    // |block_num| should be incremented each time a block is transferred and range
+    // from 0 to 65,535, wrapping around if necessary.
+    // |len_to_write| is limited by the device max transfer size stored in |func_desc_|.
+    zx_status_t Download(uint16_t block_num, uint8_t* buf, size_t len_to_write);
+    // Stores the status data of the last download transfer into |out_status|.
+    zx_status_t GetStatus(usb_dfu_get_status_data_t* out_status);
+    // Sets the device status to OK and transitions the device to the DFU Idle state.
+    zx_status_t ClearStatus();
+    // Stores the current state of the device to |out_dfu_state|.
+    zx_status_t GetState(uint8_t* out_dfu_state);
+
+    const usb_protocol_t usb_;
+
     const uint8_t intf_num_;
     const usb_dfu_func_desc_t func_desc_;
 };
diff --git a/system/uapp/usb-test-fwloader/rules.mk b/system/uapp/usb-fwloader/rules.mk
similarity index 93%
rename from system/uapp/usb-test-fwloader/rules.mk
rename to system/uapp/usb-fwloader/rules.mk
index 27d5247..b9818e0 100644
--- a/system/uapp/usb-test-fwloader/rules.mk
+++ b/system/uapp/usb-fwloader/rules.mk
@@ -8,7 +8,7 @@
 MODULE_GROUP := misc
 
 MODULE_SRCS += \
-    $(LOCAL_DIR)/usb-test-fwloader.cpp \
+    $(LOCAL_DIR)/usb-fwloader.cpp \
 
 MODULE_STATIC_LIBS := \
     system/ulib/zx \
diff --git a/system/uapp/usb-test-fwloader/usb-test-fwloader.cpp b/system/uapp/usb-fwloader/usb-fwloader.cpp
similarity index 78%
rename from system/uapp/usb-test-fwloader/usb-test-fwloader.cpp
rename to system/uapp/usb-fwloader/usb-fwloader.cpp
index 7812450..1c4d6ab 100644
--- a/system/uapp/usb-test-fwloader/usb-test-fwloader.cpp
+++ b/system/uapp/usb-fwloader/usb-fwloader.cpp
@@ -30,7 +30,13 @@
 
 struct WatchDirData {
     const char* dev_name;
-    int fd;
+    fbl::unique_fd fd;
+};
+
+enum class Mode {
+    kUpdateTest = 0,             // Update the test firmware.
+    kUpdateTestBoot = 1,         // Update the test device bootloader.
+    kDeviceFirmwareUpgrade = 2,  // Perform a DFU. The device must implement the USB DFU Spec.
 };
 
 constexpr char kFwLoaderDir[] = "/dev/class/usb-test-fwloader";
@@ -38,6 +44,8 @@
 
 constexpr char kFirmwareLoader[] = "fx3";
 constexpr char kFlashProgrammer[] = "flash-programmer";
+constexpr char kUSBDFU[] = "usb-dfu";
+constexpr char kUSBTester[] = "usb-tester";
 
 constexpr int kEnumerationWaitSecs = 5;
 
@@ -50,30 +58,37 @@
     printf("  -t                   : Load test firmware mode.\n"
            "                         This is the default if no mode is specified.\n"
            "  -b                   : Flash bootloader mode.\n"
+           "  -d                   : USB Device Firmware Upgrade.\n"
            "  -f <firmware_path>   : Firmware to load.\n"
            "  -p <flash_prog_path> : Firmware image for the flash programmer.\n"
-           "                         This is required when flashing a new bootloader.\n");
+           "                         This is only required when flashing a new bootloader.\n");
+}
+
+zx_status_t fd_matches_name(const fbl::unique_fd& fd, const char* dev_name, bool* out_match) {
+    char path[PATH_MAX];
+    const ssize_t r = ioctl_device_get_topo_path(fd.get(), path, sizeof(path));
+    if (r < 0) {
+        return ZX_ERR_IO;
+    }
+    *out_match = dev_name == nullptr || strstr(path, dev_name) != nullptr;
+    return ZX_OK;
 }
 
 zx_status_t watch_dir_cb(int dirfd, int event, const char* filename, void* cookie) {
     if (event != WATCH_EVENT_ADD_FILE) {
         return ZX_OK;
     }
-    int fd = openat(dirfd, filename, O_RDWR);
-    if (fd < 0) {
+    fbl::unique_fd fd(openat(dirfd, filename, O_RDWR));
+    if (!fd) {
         return ZX_OK;
     }
     auto data = reinterpret_cast<WatchDirData*>(cookie);
-    char path[PATH_MAX];
-    const ssize_t r = ioctl_device_get_topo_path(fd, path, sizeof(path));
-    if (r < 0) {
-        return ZX_ERR_IO;
+    bool match = false;
+    zx_status_t status = fd_matches_name(fd, data->dev_name, &match);
+    if (status != ZX_OK || !match) {
+        return status;
     }
-    if (data->dev_name && strstr(path, data->dev_name) == nullptr) {
-        close(fd);
-        return ZX_OK;
-    }
-    data->fd = fd;
+    data->fd = std::move(fd);
     return ZX_ERR_STOP;
 }
 
@@ -85,19 +100,19 @@
         return ZX_ERR_BAD_STATE;
     }
     auto close_dir = fbl::MakeAutoCall([&] { closedir(d); });
-    WatchDirData data = { .dev_name = dev_name, .fd = 0 };
+    WatchDirData data = { .dev_name = dev_name, .fd = fbl::unique_fd() };
     zx_status_t status = fdio_watch_directory(dirfd(d), watch_dir_cb,
                                               zx_deadline_after(ZX_SEC(kEnumerationWaitSecs)),
                                               reinterpret_cast<void*>(&data));
     if (status == ZX_ERR_STOP) {
-        out_fd->reset(data.fd);
+        *out_fd = std::move(data.fd);
         return ZX_OK;
     } else {
         return status;
     }
 }
 
-zx_status_t open_dev(const char* dir, fbl::unique_fd* out_fd) {
+zx_status_t open_dev(const char* dir, const char* dev_name, fbl::unique_fd* out_fd) {
     DIR* d = opendir(dir);
     if (d == nullptr) {
         fprintf(stderr, "Could not open dir: \"%s\"\n", dir);
@@ -106,11 +121,16 @@
 
     struct dirent* de;
     while ((de = readdir(d)) != nullptr) {
-        int fd = openat(dirfd(d), de->d_name, O_RDWR);
-        if (fd < 0) {
+        fbl::unique_fd fd(openat(dirfd(d), de->d_name, O_RDWR));
+        if (!fd) {
             continue;
         }
-        out_fd->reset(fd);
+        bool match = false;
+        zx_status_t status = fd_matches_name(fd, dev_name, &match);
+        if (status != ZX_OK || !match) {
+            continue;
+        }
+        *out_fd = std::move(fd);
         closedir(d);
         return ZX_OK;
     }
@@ -119,13 +139,18 @@
     return ZX_ERR_NOT_FOUND;
 }
 
-zx_status_t open_fwloader_dev(fbl::unique_fd* out_fd) {
-    return open_dev(kFwLoaderDir, out_fd);
+zx_status_t open_test_fwloader_dev(fbl::unique_fd* out_fd) {
+    return open_dev(kFwLoaderDir, kFirmwareLoader, out_fd);
 }
 
 zx_status_t open_usb_tester_dev(fbl::unique_fd* out_fd) {
-    return open_dev(kUsbTesterDevDir, out_fd);
+    return open_dev(kUsbTesterDevDir, kUSBTester, out_fd);
 }
+
+zx_status_t open_dfu_dev(fbl::unique_fd* out_fd) {
+    return open_dev(kFwLoaderDir, kUSBDFU, out_fd);
+}
+
 // Reads the firmware file and populates the provided vmo with the contents.
 zx_status_t read_firmware(fbl::unique_fd& file_fd, zx::vmo& vmo, size_t* out_fw_size) {
     struct stat s;
@@ -222,7 +247,7 @@
     std::variant<const char*, zircon_usb_test_fwloader_PrebuiltType> firmware) {
 
     fbl::unique_fd fd;
-    zx_status_t status = open_fwloader_dev(&fd);
+    zx_status_t status = open_test_fwloader_dev(&fd);
     if (status != ZX_OK) {
         fbl::unique_fd usb_tester_fd;
         // Check if there is a usb tester device we can switch to firmware loading mode.
@@ -324,23 +349,44 @@
     return ZX_OK;
 }
 
+zx_status_t device_firmware_upgrade(const char* firmware_path) {
+    fbl::unique_fd fd;
+    zx_status_t status = open_dfu_dev(&fd);
+    if (status != ZX_OK) {
+        fprintf(stderr, "Could not find any connected USB DFU device.\n");
+        return status;
+    }
+    // TODO(jocelyndang): support prebuilts.
+    std::variant<const char*, zircon_usb_test_fwloader_PrebuiltType> firmware = firmware_path;
+    status = device_load_firmware(std::move(fd), firmware);
+    if (status != ZX_OK) {
+        fprintf(stderr, "Device firmware upgrade failed, err: %d\n", status);
+        return status;
+    }
+    printf("Finished device firmware upgrade.\n");
+    return ZX_OK;
+}
+
 }  // namespace
 
 int main(int argc, char** argv) {
     auto print_usage = fbl::MakeAutoCall([prog_name = argv[0]]() { usage(prog_name); });
 
-    bool load_test_firmware_mode = true;
+    Mode mode = Mode::kUpdateTest;
     const char* firmware_path = nullptr;
     const char* flash_prog_path = nullptr;
 
     int opt;
-    while ((opt = getopt(argc, argv, "tbf:p:")) != -1) {
+    while ((opt = getopt(argc, argv, "tbdf:p:")) != -1) {
         switch (opt) {
         case 't':
-            load_test_firmware_mode = true;
+            mode = Mode::kUpdateTest;
             break;
         case 'b':
-            load_test_firmware_mode = false;
+            mode = Mode::kUpdateTestBoot;
+            break;
+        case 'd':
+            mode = Mode::kDeviceFirmwareUpgrade;
             break;
         case 'f':
             firmware_path = optarg;
@@ -356,10 +402,16 @@
     print_usage.cancel();
 
     zx_status_t status;
-    if (load_test_firmware_mode) {
+    switch (mode) {
+    case Mode::kUpdateTest:
         status = load_test_firmware(firmware_path);
-    } else {
+        break;
+    case Mode::kUpdateTestBoot:
         status = load_bootloader(flash_prog_path, firmware_path);
+        break;
+    case Mode::kDeviceFirmwareUpgrade:
+        status = device_firmware_upgrade(firmware_path);
+        break;
     }
     return status == ZX_OK ? 0 : -1;
 }