blob: f602ea6d8e41ecbda02a6feb0c53e0689a3fa9f4 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "usb-dfu.h"
#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/protocol/usb.h>
#include <fbl/algorithm.h>
#include <fbl/unique_ptr.h>
#include <fuchsia/mem/c/fidl.h>
#include <lib/zx/vmo.h>
#include <zircon/hw/usb-dfu.h>
#include <zircon/usb/test/fwloader/c/fidl.h>
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); }
zx_status_t fidl_LoadPrebuiltFirmware(void* ctx, zircon_usb_test_fwloader_PrebuiltType type,
fidl_txn_t* txn) {
// TODO(jocelyndang): implement this.
return zircon_usb_test_fwloader_DeviceLoadPrebuiltFirmware_reply(txn, ZX_ERR_NOT_SUPPORTED);
}
zx_status_t fidl_LoadFirmware(void* ctx, const fuchsia_mem_Buffer* firmware, fidl_txn_t* txn) {
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 = {
.LoadPrebuiltFirmware = fidl_LoadPrebuiltFirmware,
.LoadFirmware = fidl_LoadFirmware,
};
} // namespace
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);
}
zx_status_t Dfu::Bind() {
zxlogf(TRACE, "adding DFU, interface %x, v%x.%x\n",
intf_num_, MSB(func_desc_.bcdDFUVersion), LSB(func_desc_.bcdDFUVersion));
zx_status_t status = DdkAdd("usb-dfu", DEVICE_ADD_NON_BINDABLE);
if (status != ZX_OK) {
return status;
}
return ZX_OK;
}
// static
zx_status_t Dfu::Create(zx_device_t* parent) {
usb_protocol_t usb;
zx_status_t status = device_get_protocol(parent, ZX_PROTOCOL_USB, &usb);
if (status != ZX_OK) {
return status;
}
usb_desc_iter_t iter;
status = usb_desc_iter_init(&usb, &iter);
if (status != ZX_OK) {
return status;
}
usb_interface_descriptor_t* intf = usb_desc_iter_next_interface(&iter, true);
if (!intf) {
usb_desc_iter_release(&iter);
return ZX_ERR_NOT_SUPPORTED;
}
uint8_t intf_num = intf->bInterfaceNumber;
// Look for the DFU Functional Descriptor.
usb_dfu_func_desc_t func_desc = {};
usb_descriptor_header_t* header = usb_desc_iter_next(&iter);
while (header) {
if (header->bDescriptorType == USB_DFU_CS_FUNCTIONAL) {
if (header->bLength < sizeof(func_desc)) {
zxlogf(ERROR, "DFU func desc should be at least %lu long, got %u\n",
sizeof(func_desc), header->bLength);
} else {
memcpy(&func_desc, header, sizeof(func_desc));
zxlogf(TRACE, "DFU func desc bmAttributes %u wDetachTimeOut %u wTransferSize %u\n",
func_desc.bmAttributes, func_desc.wDetachTimeOut, func_desc.wTransferSize);
break;
}
}
header = usb_desc_iter_next(&iter);
}
usb_desc_iter_release(&iter);
if (func_desc.bLength == 0) {
zxlogf(ERROR, "could not find any valid DFU functional descriptor\n");
return ZX_ERR_NOT_SUPPORTED;
}
fbl::AllocChecker ac;
auto dev = fbl::make_unique_checked<Dfu>(&ac, parent, usb, intf_num, func_desc);
if (!ac.check()) {
return ZX_ERR_NO_MEMORY;
}
status = dev->Bind();
if (status == ZX_OK) {
// Intentionally leak as it is now held by DevMgr.
__UNUSED auto ptr = dev.release();
}
return status;
}
zx_status_t dfu_bind(void* ctx, zx_device_t* parent) {
zxlogf(TRACE, "dfu_bind\n");
return usb::Dfu::Create(parent);
}
static constexpr zx_driver_ops_t dfu_driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = dfu_bind;
return ops;
}();
} // namespace usb
// clang-format off
ZIRCON_DRIVER_BEGIN(usb_dfu, usb::dfu_driver_ops, "zircon", "0.1", 4)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB),
BI_ABORT_IF(NE, BIND_USB_CLASS, USB_CLASS_APPLICATION_SPECIFIC),
BI_ABORT_IF(NE, BIND_USB_SUBCLASS, USB_SUBCLASS_DFU),
BI_MATCH_IF(EQ, BIND_USB_PROTOCOL, USB_PROTOCOL_DFU),
ZIRCON_DRIVER_END(usb_dfu)
// clang-format on