// SPDX-License-Identifier: Apache-2.0 /* * Copyright (c) 2021 Gwenhael Goavec-Merou */ #include #include #include #include #include #include #include #include #include #include #include #include "display.hpp" #include "progressBar.hpp" #include "dfu.hpp" using namespace std; /* USB request write */ static const uint8_t DFU_REQUEST_OUT = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE; /* USB request read */ static const uint8_t DFU_REQUEST_IN = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE; /* DFU command */ enum dfu_cmd { DFU_DETACH = 0, DFU_DNLOAD = 1, DFU_UPLOAD = 2, DFU_GETSTATUS = 3, DFU_CLRSTATUS = 4, DFU_GETSTATE = 5, DFU_ABORT = 6 }; /* Question: - * - add vid/pid and override DFU file ? * - index as jtag chain (fix issue when more than one device connected) */ DFU::DFU(const string &filename, bool bypass_bitstream, uint16_t vid, uint16_t pid, int16_t altsetting, int verbose_lvl):_verbose(verbose_lvl > 0), _debug(verbose_lvl > 1), _quiet(verbose_lvl < 0), dev_idx(0), _vid(0), _pid(0), _altsetting(altsetting), usb_ctx(NULL), dev_handle(NULL), curr_intf(0), transaction(0), _bit(NULL) { struct dfu_status status; int dfu_vid = 0, dfu_pid = 0, ret; printInfo("Open file : ", false); if (bypass_bitstream) { _bit = nullptr; printInfo("bypassed"); } else { try { _bit = new DFUFileParser(filename, _verbose > 0); printSuccess("DONE"); } catch (std::exception &e) { printError("FAIL"); throw runtime_error("Error: Fail to open file"); } printInfo("Parse file ", false); try { _bit->parse(); printSuccess("DONE"); } catch (std::exception &e) { printError("FAIL"); delete _bit; throw runtime_error("Error: Fail to parse file"); } if (_verbose > 0) _bit->displayHeader(); /* get VID and PID from dfu file */ try { dfu_vid = std::stoi(_bit->getHeaderVal("idVendor"), 0, 16); dfu_pid = std::stoi(_bit->getHeaderVal("idProduct"), 0, 16); } catch (std::exception &e) { if (_verbose) printWarn(e.what()); } } if (libusb_init(&usb_ctx) < 0) { delete _bit; throw std::runtime_error("libusb init failed"); } /* no vid or pid provided by DFU file or by params */ if ((dfu_vid == 0 || dfu_pid == 0) && (vid == 0 || pid == 0)) { // search all DFU compatible devices if (searchDFUDevices() != EXIT_SUCCESS) { libusb_exit(usb_ctx); delete _bit; throw std::runtime_error("Devices enumeration failed"); } } else { bool found = false; if (dfu_vid != 0 && dfu_pid != 0) found = searchWithVIDPID(dfu_vid, dfu_pid); if (vid != 0 && pid != 0 && !found) found = searchWithVIDPID(vid, pid); } /* check if DFU compatible devices are present */ if (dfu_dev.size() != 0) { /* more than one: only possible if file is not DFU */ if (dfu_dev.size() > 1 && !filename.empty()) { libusb_exit(usb_ctx); delete _bit; throw std::runtime_error("Only one device supported"); } } else { libusb_exit(usb_ctx); delete _bit; throw std::runtime_error("No DFU compatible device found"); } if (_verbose) displayDFU(); /* don't try device without vid/pid */ if ((_vid == 0 || _pid == 0) && _bit) { libusb_exit(usb_ctx); delete _bit; throw std::runtime_error("Can't open device vid/pid == 0"); } /* the If bitstream has been bypassed -> it's the end */ if (!_bit) return; /* open the first */ if (open_DFU(0) == EXIT_FAILURE) { libusb_exit(usb_ctx); delete _bit; throw std::runtime_error("Fail to claim device"); } printf("%02x %02x\n", _vid, _pid); if (_verbose > 0) { if ((ret = get_status(&status)) < 0) { delete _bit; throw std::runtime_error("get device status failed with error code " + std::to_string(ret)); } printInfo("Default DFU status " + dfu_dev_state_val[status.bState]); } } DFU::~DFU() { close_DFU(); libusb_exit(usb_ctx); if (_bit) delete _bit; } /* open the device using VID and PID */ int DFU::open_DFU(int index) { struct dfu_dev curr_dfu; int ret; if (_vid == 0 || _pid == 0) { printError("Error: Can't open device without VID/PID"); return EXIT_FAILURE; } dev_idx = index; curr_dfu = dfu_dev[dev_idx]; curr_intf = curr_dfu.interface; dev_handle = libusb_open_device_with_vid_pid(usb_ctx, curr_dfu.vid, curr_dfu.pid); if (!dev_handle) { printError("Error: fail to open device"); return EXIT_FAILURE; } if ((ret = libusb_claim_interface(dev_handle, curr_intf)) != 0) { libusb_close(dev_handle); printError("Error: fail to claim interface with error code " + std::to_string(ret)); return EXIT_FAILURE; } if ((ret = libusb_set_interface_alt_setting(dev_handle, curr_intf, _altsetting)) != 0) { libusb_release_interface(dev_handle, curr_intf); libusb_close(dev_handle); printError("Error: fail to set interface " + std::to_string(curr_intf) + " with error code " + std::to_string(ret)); return EXIT_FAILURE; } return EXIT_SUCCESS; } /* if the is claimed close it */ int DFU::close_DFU() { if (dev_handle) { int ret; ret = libusb_release_interface(dev_handle, dfu_dev[dev_idx].interface); if (ret != 0) { /* device is already disconnected ... */ if (ret == LIBUSB_ERROR_NO_DEVICE) { return EXIT_SUCCESS; } else { printError("Error: Fail to release interface"); return EXIT_FAILURE; } } libusb_close(dev_handle); dev_handle = NULL; } return EXIT_SUCCESS; } /* search one device using VID/PID */ bool DFU::searchWithVIDPID(uint16_t vid, uint16_t pid) { char mess[40]; /* search device using vid/pid */ libusb_device_handle *handle = NULL; /* try first by using VID:PID from DFU file */ snprintf(mess, 40, "Open device %04x:%04x ", vid, pid); printInfo(mess, false); handle = libusb_open_device_with_vid_pid(usb_ctx, vid, pid); /* No device found */ if (!handle) { printWarn("Not found"); if (_verbose) printError("Error: unable to connect to device"); return false; } else { printSuccess("DONE"); } /* retrieve usb device structure */ libusb_device *dev = libusb_get_device(handle); if (!dev) { libusb_close(handle); if (_verbose) printError("Error: unable to retrieve usb device"); return false; } /* and device descriptor */ struct libusb_device_descriptor desc; int r = libusb_get_device_descriptor(dev, &desc); if (r != 0) { libusb_close(handle); printError("Error: fail to retrieve usb descriptor"); return false; } /* search if one descriptor is DFU compatible */ int ret = searchIfDFU(handle, dev, &desc); if (ret == 1) { if (_verbose) printError("Error: No DFU interface"); } _vid = vid; _pid = pid; libusb_close(handle); // no more needed -> reopen after return (ret == 1) ? false : true; } /* Tree steps are required to discover all * DFU capable devices * 1. loop over devices */ int DFU::searchDFUDevices() { int i = 0; libusb_device **dev_list; libusb_device *usb_dev; libusb_device_handle *handle; /* clear dfu list */ dfu_dev.clear(); /* iteration */ ssize_t list_size = libusb_get_device_list(usb_ctx, &dev_list); if (_verbose) printInfo("found " + to_string(list_size) + " USB device"); while ((usb_dev = dev_list[i++]) != NULL) { struct libusb_device_descriptor desc; if (libusb_get_device_descriptor(usb_dev, &desc) != 0) { printError("Unable to get device descriptor"); return EXIT_FAILURE; } if (_verbose > 0) { printf("%04x:%04x (bus %d, device %2d)\n", desc.idVendor, desc.idProduct, libusb_get_bus_number(usb_dev), libusb_get_device_address(usb_dev)); } int ret = libusb_open(usb_dev, &handle); if (ret == 0) { ret = searchIfDFU(handle, usb_dev, &desc); libusb_close(handle); if (ret != 0) { return EXIT_FAILURE; } } else if (_debug) { char mess[256]; snprintf(mess, 256, "Unable to open device: " "%04x:%04x (bus %d, device %2d) Error: %s -> skip\n", desc.idVendor, desc.idProduct, libusb_get_bus_number(usb_dev), libusb_get_device_address(usb_dev), libusb_error_name(ret)); printWarn(mess); } } libusb_free_device_list(dev_list, 1); return EXIT_SUCCESS; } /* 2. loop over configuration */ int DFU::searchIfDFU(struct libusb_device_handle *handle, struct libusb_device *dev, struct libusb_device_descriptor *desc) { /* configuration descriptor iteration */ for (int i = 0; i < desc->bNumConfigurations; i++) { struct libusb_config_descriptor *cfg; int ret = libusb_get_config_descriptor(dev, i, &cfg); if (ret != 0) { printError("Fail to retrieve config_descriptor " + to_string(i)); return 1; } /* configuration interface iteration */ for (int if_idx=0; if_idx < cfg->bNumInterfaces; if_idx++) { const struct libusb_interface *uif = &cfg->interface[if_idx]; std::vector dfu_dev_tmp; struct dfu_desc *dfu_desc; bool dfu_found = false; /* altsettings interation */ for (int intf_idx = 0; intf_idx < uif->num_altsetting; intf_idx++) { const struct libusb_interface_descriptor *intf = &uif->altsetting[intf_idx]; uint8_t intfClass = intf->bInterfaceClass; uint8_t intfSubClass = intf->bInterfaceSubClass; if (intfClass == 0xfe && intfSubClass == 0x01) { struct dfu_dev my_dev; if (_verbose) printInfo("DFU found"); /* dfu functional descriptor */ /* not all DFU implementations provides a descriptor per altsettings * so don't stop directly. */ if (parseDFUDescriptor(intf, reinterpret_cast(&my_dev.dfu_desc), sizeof(my_dev.dfu_desc))) { dfu_desc = &my_dev.dfu_desc; dfu_found = true; } /* this altsetting is a DFU interface: * - if user has selected one altsetting current is only stored if it match * - otherwise all are stored */ if (_altsetting != -1 && _altsetting != intf_idx) continue; my_dev.vid = desc->idVendor; my_dev.pid = desc->idProduct; my_dev.altsettings = intf_idx; my_dev.interface = if_idx; my_dev.bus = libusb_get_bus_number(dev); my_dev.device = libusb_get_device_address(dev); my_dev.bMaxPacketSize0 = desc->bMaxPacketSize0; memset(my_dev.iProduct, 0, 128); libusb_get_string_descriptor_ascii(handle, desc->iProduct, my_dev.iProduct, 128); if (strlen((char *)my_dev.iProduct) == 0) snprintf((char *)my_dev.iProduct, 128, "empty"); memset(my_dev.iInterface, 0, 128); libusb_get_string_descriptor_ascii(handle, intf->iInterface, my_dev.iInterface, 128); if (strlen((char *)my_dev.iInterface) == 0) snprintf((char *)my_dev.iInterface, 128, "empty"); int r = libusb_get_port_numbers(dev, my_dev.path, sizeof(my_dev.path)); my_dev.path[r] = '\0'; dfu_dev_tmp.push_back(my_dev); } } /* At least one altsetting has a DFU descriptor */ if (dfu_found) { /* with tinyDFU only one (the first) altsetting contains a descriptor * fill others with this information */ for (struct dfu_dev &d: dfu_dev_tmp) { if (d.dfu_desc.bDescriptorType == 0) memcpy(&d.dfu_desc, dfu_desc, sizeof(struct dfu_desc)); } std::move(dfu_dev_tmp.begin(), dfu_dev_tmp.end(), std::back_inserter(dfu_dev)); } } libusb_free_config_descriptor(cfg); } return 0; } /* 3. check if altsettings contains a valid * dfu descriptor. * Since libusb has no support for those structure * fill a custom structure */ bool DFU::parseDFUDescriptor(const struct libusb_interface_descriptor *intf, uint8_t *dfu_desc, int dfu_desc_size) { const uint8_t *extra = intf->extra; int extra_len = intf->extra_length; /* not all altsettings for a DFU device have a DFU descriptor * set to 0 to be able to detect a missing descriptor */ memset(dfu_desc, 0, 9); if (extra_len < 9) return false; /* map memory to structure */ for (int j = 0; j + 1 < extra_len; j++) { if (extra[j+1] == 0x21) { memcpy(dfu_desc, (const void *)&extra[j], dfu_desc_size); return true; } } return false; } /* abstraction to send/receive to control */ int DFU::send(bool out, uint8_t brequest, uint16_t wvalue, const uint8_t *data, uint16_t length) { uint8_t type = out ? DFU_REQUEST_OUT : DFU_REQUEST_IN; int ret = libusb_control_transfer(dev_handle, type, brequest, wvalue, curr_intf, const_cast(data), length, 5000); if (ret < 0) { if (checkDevicePresent()) { /* close device ? */ return 0; } } return ret; } /* subset of state transitions */ int DFU::set_state(char newState) { int ret = 0; struct dfu_status status; if (get_status(&status) < 0) return -1; while (status.bState != newState) { switch (status.bState) { case STATE_appIDLE: if (dfu_detach() == EXIT_FAILURE) return -1; if (get_status(&status) <= 0) return -1; if (status.bState != STATE_appDETACH || status.bStatus != STATUS_OK) { cerr << dfu_dev_status_val[status.bStatus] << endl; return -1; } break; case STATE_appDETACH: if (newState == STATE_appIDLE) { // ? reset + timeout ? } else { // reset // reenum } break; case STATE_dfuIDLE: /* better to test upload/download * an handle others states */ if (newState == STATE_appIDLE) { // reset ret = libusb_reset_device(dev_handle); // reenum } else { // download or upload // are handled by download() and upload() return -2; } return ret; break; case STATE_dfuDNLOAD_IDLE: if (newState == STATE_dfuMANIFEST_SYNC) { /* send the zero sized download request * dfuDNLOAD_IDLE -> dfuMANITEST-SYNC */ if (_verbose) printInfo("send zero sized download request"); ret = send(true, DFU_DNLOAD, transaction, NULL, 0); } else { // dfuIDLE ret = send(true, DFU_ABORT, 0, NULL, 0); } if (ret < 0) { printError("Fails to send packet\n"); return ret; } if (get_status(&status) < 0) return ret; /* not always true: * the newState may be the next one or another */ if (status.bState != newState) { printError(dfu_dev_state_val[status.bState]); return -1; } break; case STATE_dfuERROR: if (newState == STATE_appIDLE) { ret = libusb_reset_device(dev_handle); } else { // dfuIDLE ret = send(true, DFU_CLRSTATUS, 0, NULL, 0); } /* if command fails * try to determine if device is lost * or if it's another issue */ if (ret < 0) { if (checkDevicePresent() == 1) { printError("Fails to send packet\n"); } else { printInfo("device disconnected\n"); } return ret; } /* now state must be appIDLE or dfuIDLE * use GETSTATUS to check */ if ((ret = get_status(&status)) <= 0) return ret; if (status.bState != newState || status.bStatus != STATUS_OK) { cerr << dfu_dev_status_val[status.bStatus] << endl; return -1; } break; } } return ret; } /* detach device -> move from appIDLE to dfuIDLE */ int DFU::dfu_detach() { int res = send(true, DFU_DETACH, 0, NULL, 0); return (res < 0) ? EXIT_FAILURE : EXIT_SUCCESS; } int DFU::get_status(struct dfu_status *status) { uint8_t buffer[6] = {0}; int res; res = send(false, DFU_GETSTATUS, 0, buffer, 6); if (res == 6) { status->bStatus = buffer[0]; status->bwPollTimeout = (((uint32_t)buffer[3] & 0xff) << 16) || (((uint32_t)buffer[2] & 0xff) << 8) || (((uint32_t)buffer[1] & 0xff) << 0); status->bState = buffer[4]; status->iString = buffer[5]; } else if (res < 0) { printError("Get DFU status failed with error " + std::to_string(res) + "(" + std::string(libusb_error_name(res)) + ")"); } return res; } /* read current device state -> unlike status * this request didn't change state */ char DFU::get_state() { char c = 0; int res = send(false, DFU_GETSTATE, 0, (unsigned char *)&c, 1); if (res != 1) return res; return c; } /* read status until device state match * wait for bwPollTimeout ms between each requests */ int DFU::poll_state(uint8_t state) { int ret = 0; struct dfu_status status; do { ret = get_status(&status); if (ret <= 0) { printError("Error: poll state " + string(libusb_error_name(ret))); break; } /* millisecond */ usleep(1000 * status.bwPollTimeout); } while (status.bState != state && status.bState != STATE_dfuERROR); return (ret > 0) ? status.bState : ret; } /* display details about device informations and capabilities */ void DFU::displayDFU() { /* display dfu device */ printf("Found DFU:\n"); for (unsigned int i = 0; i < dfu_dev.size(); i++) { printf("%04x:%04x (bus %d, device %2d),", dfu_dev[i].vid, dfu_dev[i].pid, dfu_dev[i].bus, dfu_dev[i].device); printf(" path: %d",dfu_dev[i].path[0]); for (size_t j = 1; j < strlen(((const char *)dfu_dev[i].path)); j++) printf(".%d", dfu_dev[i].path[j]); printf(", alt: %d, iProduct \"%s\", iInterface \"%s\"", dfu_dev[i].altsettings, dfu_dev[i].iProduct, dfu_dev[i].iInterface); printf("\n"); printf("\tDFU details\n"); printf("\t\tbLength %02x\n", dfu_dev[i].dfu_desc.bLength); printf("\t\tbDescriptorType %02x\n", dfu_dev[i].dfu_desc.bDescriptorType); printf("\t\tbmAttributes %02x\n", dfu_dev[i].dfu_desc.bmAttributes); printf("\t\twDetachTimeOut %04x\n", dfu_dev[i].dfu_desc.wDetachTimeOut); printf("\t\twTransferSize %04d\n", libusb_le16_to_cpu(dfu_dev[i].dfu_desc.wTransferSize)); printf("\t\tbcdDFUVersion %04x\n", libusb_le16_to_cpu(dfu_dev[i].dfu_desc.bcdDFUVersion)); uint8_t bmAttributes = dfu_dev[i].dfu_desc.bmAttributes; printf("\tDFU attributes %02x\n", bmAttributes); printf("\t\tDFU_DETACH : "); if (bmAttributes & (1 << 3)) printf("ON\n"); else printf("OFF\n"); printf("\t\tBitManifestionTolerant: "); if (bmAttributes & (1 << 2)) printf("ON\n"); else printf("OFF\n"); printf("\t\tUPLOAD : "); if (bmAttributes & (1 << 1)) printf("ON\n"); else printf("OFF\n"); printf("\t\tDOWNLOAD : "); if (bmAttributes & (1 << 0)) printf("ON\n"); else printf("OFF\n"); } } /*! * \brief download filename content * \param[in] filename: name of the file to download * \return -1 when issue with file * -2 when file parse fail */ int DFU::download() { /* a device must be open. Can't try * to download image on an enumerate device */ if (!dev_handle) { printError("Error: No device. Can't download file"); return -1; } if (!_bit) { printError("Error: No bitstream. Stop"); return -1; } int ret, ret_val = EXIT_SUCCESS; const uint8_t *buffer, *ptr; int size, xfer_len; int bMaxPacketSize0 = dfu_dev[dev_idx].bMaxPacketSize0; struct dfu_status status; struct dfu_dev curr_dev = dfu_dev[dev_idx]; /* check if device allows download */ if (!(curr_dev.dfu_desc.bmAttributes & (1 << 0))) { printError("Error: download is not supported by the device\n"); return -1; } /* download must start in dfu IDLE state */ if (get_status(&status) < 0) return -1; if (status.bState != STATE_dfuIDLE) set_state(STATE_dfuIDLE); xfer_len = libusb_le16_to_cpu(curr_dev.dfu_desc.wTransferSize); if (xfer_len < bMaxPacketSize0) xfer_len = bMaxPacketSize0; size = _bit->getLength() / 8; buffer = _bit->getData(); if (size == 0) { printError("Error: empty configuration file\n"); return -3; } ptr = buffer; int max_iter = size / xfer_len; if (max_iter * xfer_len != size) max_iter++; ProgressBar progress("Loading", max_iter, 50, _quiet); /* send data configuration by up to xfer_len bytes */ for (transaction = 0; transaction < max_iter; transaction++, ptr+=xfer_len) { int residual = size - (xfer_len * transaction); if (residual < xfer_len) xfer_len = residual; ret = send(true, DFU_DNLOAD, transaction, (xfer_len) ? ptr : NULL, xfer_len); if (ret != xfer_len) { // can't be wrong here printf("Fails to send packet %s\n", libusb_error_name(ret)); ret_val = -4; break; } /* wait until dfu state is again STATE_dfuDNLOAD_IDLE * using status pollTimeout value */ ret_val = poll_state(STATE_dfuDNLOAD_IDLE); if (ret_val != STATE_dfuDNLOAD_IDLE) { printf("download: failed %d %d\n", ret_val, STATE_dfuDNLOAD_IDLE); break; } progress.display(transaction); } if (ret_val != STATE_dfuDNLOAD_IDLE) { progress.fail(); ret_val = -5; return ret_val; } progress.done(); /* send the zero sized download request * dfuDNLOAD_IDLE -> dfuMANITEST-SYNC */ printInfo("send zero sized download request: ", false); //ret = set_state(STATE_dfuMANIFEST_SYNC); ret = send(true, DFU_DNLOAD, transaction, NULL, 0); if (ret < 0) { printError("Error: fail to change state " + to_string(ret)); return -6; } printSuccess("DONE"); /* Now FSM must be in dfuMANITEST-SYNC */ bool must_continue = true; do { ret = get_status(&status); if (ret != 6) { /* we consider device disconnected * is ret == 0 */ if (ret < 0 && ret != LIBUSB_ERROR_NO_DEVICE) { printf("Error: fail to get status %d\n", ret); printf("%s\n", libusb_error_name(ret)); ret_val = ret; } break; } if (_verbose) { printInfo("status " + dfu_dev_state_val[status.bState] + " " + dfu_dev_status_val[status.bStatus]); } usleep(1000 * status.bwPollTimeout); switch (status.bState) { case STATE_dfuMANIFEST_SYNC: case STATE_dfuMANIFEST: break; /* need send reset */ case STATE_dfuMANIFEST_WAIT_RESET: ret = libusb_reset_device(dev_handle); if (ret < 0) { /* dfu device may be disconnected */ if (ret != LIBUSB_ERROR_NOT_FOUND) { printf("%s\n", libusb_error_name(ret)); printf("ret %d\n", ret); ret_val = -7; } } must_continue = false; break; case STATE_dfuERROR: printError("Error: dfuERROR state\n"); /* before quit try to cleanup the state */ set_state(STATE_dfuIDLE); ret_val = -7; must_continue = false; break; case STATE_dfuIDLE: case STATE_appIDLE: must_continue = false; break; } } while (must_continue); if (status.bState == STATE_dfuIDLE) set_state(STATE_appIDLE); return ret_val; } /* After download and manifest * device may reset itself with * a lost of connection * return 1 if the device is present * 0 if the device is lost */ bool DFU::checkDevicePresent() { struct dfu_dev curr_dfu = dfu_dev[dev_idx]; libusb_device_handle *handle; handle = libusb_open_device_with_vid_pid(usb_ctx, curr_dfu.vid, curr_dfu.pid); if (_verbose) { printInfo("device present ", false); if (handle) printInfo("True"); else printInfo("False"); } return (handle != NULL); }