mirror of
https://gitea.osmocom.org/sim-card/simtrace2.git
synced 2026-03-16 21:28:33 +03:00
WIP: Introduce USB DFU code from my at91lib DFU port
This commit is contained in:
81
firmware/atmel_softpack_libraries/usb/common/dfu/usb_dfu.h
Normal file
81
firmware/atmel_softpack_libraries/usb/common/dfu/usb_dfu.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#ifndef _USB_DFU_H
|
||||
#define _USB_DFU_H
|
||||
/* USB Device Firmware Update Implementation for OpenPCD
|
||||
* (C) 2006 by Harald Welte <hwelte@hmw-consulting.de>
|
||||
*
|
||||
* Protocol definitions for USB DFU
|
||||
*
|
||||
* This ought to be compliant to the USB DFU Spec 1.0 as available from
|
||||
* http://www.usb.org/developers/devclass_docs/usbdfu10.pdf
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <usb/include/USBRequests.h>
|
||||
|
||||
#define USB_DT_DFU 0x21
|
||||
|
||||
struct usb_dfu_func_descriptor {
|
||||
uint8_t bLength;
|
||||
uint8_t bDescriptorType;
|
||||
uint8_t bmAttributes;
|
||||
#define USB_DFU_CAN_DOWNLOAD (1 << 0)
|
||||
#define USB_DFU_CAN_UPLOAD (1 << 1)
|
||||
#define USB_DFU_MANIFEST_TOL (1 << 2)
|
||||
#define USB_DFU_WILL_DETACH (1 << 3)
|
||||
uint16_t wDetachTimeOut;
|
||||
uint16_t wTransferSize;
|
||||
uint16_t bcdDFUVersion;
|
||||
} __attribute__ ((packed));
|
||||
|
||||
#define USB_DT_DFU_SIZE 9
|
||||
|
||||
/* DFU class-specific requests (Section 3, DFU Rev 1.1) */
|
||||
#define USB_REQ_DFU_DETACH 0x00
|
||||
#define USB_REQ_DFU_DNLOAD 0x01
|
||||
#define USB_REQ_DFU_UPLOAD 0x02
|
||||
#define USB_REQ_DFU_GETSTATUS 0x03
|
||||
#define USB_REQ_DFU_CLRSTATUS 0x04
|
||||
#define USB_REQ_DFU_GETSTATE 0x05
|
||||
#define USB_REQ_DFU_ABORT 0x06
|
||||
|
||||
struct dfu_status {
|
||||
uint8_t bStatus;
|
||||
uint8_t bwPollTimeout[3];
|
||||
uint8_t bState;
|
||||
uint8_t iString;
|
||||
} __attribute__((packed));
|
||||
|
||||
#define DFU_STATUS_OK 0x00
|
||||
#define DFU_STATUS_errTARGET 0x01
|
||||
#define DFU_STATUS_errFILE 0x02
|
||||
#define DFU_STATUS_errWRITE 0x03
|
||||
#define DFU_STATUS_errERASE 0x04
|
||||
#define DFU_STATUS_errCHECK_ERASED 0x05
|
||||
#define DFU_STATUS_errPROG 0x06
|
||||
#define DFU_STATUS_errVERIFY 0x07
|
||||
#define DFU_STATUS_errADDRESS 0x08
|
||||
#define DFU_STATUS_errNOTDONE 0x09
|
||||
#define DFU_STATUS_errFIRMWARE 0x0a
|
||||
#define DFU_STATUS_errVENDOR 0x0b
|
||||
#define DFU_STATUS_errUSBR 0x0c
|
||||
#define DFU_STATUS_errPOR 0x0d
|
||||
#define DFU_STATUS_errUNKNOWN 0x0e
|
||||
#define DFU_STATUS_errSTALLEDPKT 0x0f
|
||||
|
||||
enum dfu_state {
|
||||
DFU_STATE_appIDLE = 0,
|
||||
DFU_STATE_appDETACH = 1,
|
||||
DFU_STATE_dfuIDLE = 2,
|
||||
DFU_STATE_dfuDNLOAD_SYNC = 3,
|
||||
DFU_STATE_dfuDNBUSY = 4,
|
||||
DFU_STATE_dfuDNLOAD_IDLE = 5,
|
||||
DFU_STATE_dfuMANIFEST_SYNC = 6,
|
||||
DFU_STATE_dfuMANIFEST = 7,
|
||||
DFU_STATE_dfuMANIFEST_WAIT_RST = 8,
|
||||
DFU_STATE_dfuUPLOAD_IDLE = 9,
|
||||
DFU_STATE_dfuERROR = 10,
|
||||
};
|
||||
|
||||
#endif /* _USB_DFU_H */
|
||||
49
firmware/atmel_softpack_libraries/usb/device/dfu/dfu.c
Normal file
49
firmware/atmel_softpack_libraries/usb/device/dfu/dfu.c
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
#include <usb/include/USBDescriptors.h>
|
||||
#include <usb/device/dfu/dfu.h>
|
||||
|
||||
|
||||
/* String 1 "SimTrace DFU Interface - Application Partition" */
|
||||
const struct USBStringDescriptor USBDFU_string1 = {
|
||||
.hdr = {
|
||||
.bLength = sizeof(USBGenericDescriptor) + 46 * sizeof(unsigned short),
|
||||
.bDescriptorType = USBGenericDescriptor_STRING,
|
||||
},
|
||||
.wData = { 0x0053, 0x0069, 0x006d, 0x0054, 0x0072, 0x0061,
|
||||
0x0063, 0x0065, 0x0020, 0x0044, 0x0046, 0x0055,
|
||||
0x0020, 0x0049, 0x006e, 0x0074, 0x0065, 0x0072,
|
||||
0x0066, 0x0061, 0x0063, 0x0065, 0x0020, 0x002d,
|
||||
0x0020, 0x0041, 0x0070, 0x0070, 0x006c, 0x0069,
|
||||
0x0063, 0x0061, 0x0074, 0x0069, 0x006f, 0x006e,
|
||||
0x0020, 0x0050, 0x0061, 0x0072, 0x0074, 0x0069,
|
||||
0x0074, 0x0069, 0x006f, 0x006e, },
|
||||
};
|
||||
|
||||
/* String 2 "SimTrace DFU Interface - Bootloader Partition" */
|
||||
const struct USBStringDescriptor USBDFU_string2 = {
|
||||
.hdr = {
|
||||
.bLength = sizeof(USBGenericDescriptor) + 45 * sizeof(unsigned short),
|
||||
.bDescriptorType = USBGenericDescriptor_STRING,
|
||||
},
|
||||
.wData = { 0x0053, 0x0069, 0x006d, 0x0054, 0x0072, 0x0061,
|
||||
0x0063, 0x0065, 0x0020, 0x0044, 0x0046, 0x0055,
|
||||
0x0020, 0x0049, 0x006e, 0x0074, 0x0065, 0x0072,
|
||||
0x0066, 0x0061, 0x0063, 0x0065, 0x0020, 0x002d,
|
||||
0x0020, 0x0042, 0x006f, 0x006f, 0x0074, 0x006c,
|
||||
0x006f, 0x0061, 0x0064, 0x0065, 0x0072, 0x0020,
|
||||
0x0050, 0x0061, 0x0072, 0x0074, 0x0069, 0x0074,
|
||||
0x0069, 0x006f, 0x006e, },
|
||||
};
|
||||
|
||||
/* String 3 "SimTrace DFU Interface - RAM" */
|
||||
const struct USBStringDescriptor USBDFU_string3 = {
|
||||
.hdr = {
|
||||
.bLength = sizeof(USBGenericDescriptor) + 28 * sizeof(unsigned short),
|
||||
.bDescriptorType = USBGenericDescriptor_STRING,
|
||||
},
|
||||
.wData = { 0x0053, 0x0069, 0x006d, 0x0054, 0x0072, 0x0061,
|
||||
0x0063, 0x0065, 0x0020, 0x0044, 0x0046, 0x0055,
|
||||
0x0020, 0x0049, 0x006e, 0x0074, 0x0065, 0x0072,
|
||||
0x0066, 0x0061, 0x0063, 0x0065, 0x0020, 0x002d,
|
||||
0x0020, 0x0052, 0x0041, 0x004d, },
|
||||
};
|
||||
130
firmware/atmel_softpack_libraries/usb/device/dfu/dfu.h
Normal file
130
firmware/atmel_softpack_libraries/usb/device/dfu/dfu.h
Normal file
@@ -0,0 +1,130 @@
|
||||
#ifndef _USB_DEV_DFU_H
|
||||
#define _USB_DEV_DFU_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <board.h>
|
||||
#include <usb/include/USBDescriptors.h>
|
||||
#include <usb/include/USBDDriver.h>
|
||||
|
||||
#if 0
|
||||
/* This is valid for CCID */
|
||||
#define CONFIG_DFU_NUM_APP_IF 1
|
||||
#define CONFIG_DFU_NUM_APP_STR 4
|
||||
#else
|
||||
/* This is valid for CDC-Serial */
|
||||
#define CONFIG_DFU_NUM_APP_IF 2
|
||||
#define CONFIG_DFU_NUM_APP_STR 2
|
||||
#endif
|
||||
|
||||
struct USBStringDescriptor {
|
||||
USBGenericDescriptor hdr;
|
||||
unsigned short wData[];
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
#ifdef BOARD_USB_DFU
|
||||
|
||||
#include <usb/common/dfu/usb_dfu.h>
|
||||
|
||||
/* for board-specific config */
|
||||
#include <board.h>
|
||||
|
||||
struct dfu_desc {
|
||||
USBConfigurationDescriptor ucfg;
|
||||
USBInterfaceDescriptor uif[BOARD_DFU_NUM_IF];
|
||||
struct usb_dfu_func_descriptor func_dfu;
|
||||
} __attribute__ ((packed));
|
||||
|
||||
/* USB DFU functional descriptor */
|
||||
#define DFU_FUNC_DESC { \
|
||||
.bLength = USB_DT_DFU_SIZE, \
|
||||
.bDescriptorType = USB_DT_DFU, \
|
||||
.bmAttributes = USB_DFU_CAN_UPLOAD | USB_DFU_CAN_DOWNLOAD, \
|
||||
.wDetachTimeOut = 0xff00, \
|
||||
.wTransferSize = BOARD_DFU_PAGE_SIZE, \
|
||||
.bcdDFUVersion = 0x0100, \
|
||||
}
|
||||
|
||||
/* Number of DFU interface during runtime mode */
|
||||
#define DFURT_NUM_IF 1
|
||||
|
||||
/* to be used by the runtime as part of its USB descriptor structure
|
||||
* declaration */
|
||||
#define DFURT_IF_DESCRIPTOR_STRUCT \
|
||||
USBInterfaceDescriptor dfu_rt; \
|
||||
struct usb_dfu_func_descriptor func_dfu;
|
||||
|
||||
/* to be used by the runtime as part of its USB Dsecriptor structure
|
||||
* definition */
|
||||
#define DFURT_IF_DESCRIPTOR(dfuIF, dfuSTR) \
|
||||
.dfu_rt = { \
|
||||
.bLength = sizeof(USBInterfaceDescriptor), \
|
||||
.bDescriptorType = USBGenericDescriptor_INTERFACE, \
|
||||
.bInterfaceNumber = dfuIF, \
|
||||
.bAlternateSetting = 0, \
|
||||
.bNumEndpoints = 0, \
|
||||
.bInterfaceClass = 0xFE, \
|
||||
.bInterfaceSubClass = 0x01, \
|
||||
.bInterfaceProtocol = 0x01, \
|
||||
.iInterface = dfuSTR, \
|
||||
}, \
|
||||
.func_dfu = DFU_FUNC_DESC \
|
||||
|
||||
/* provided by dfu_desc.c */
|
||||
extern const struct dfu_desc dfu_cfg_descriptor;
|
||||
extern const USBDDriverDescriptors dfu_descriptors;
|
||||
|
||||
#else /* BOARD_USB_DFU */
|
||||
|
||||
/* no DFU bootloader is being used */
|
||||
#define DFURT_NUM_IF 0
|
||||
#define DFURT_IF_DESCRIPTOR_STRUCT(a, b)
|
||||
#define DFURT_IF_DESCRIPTOR
|
||||
|
||||
#endif /* BOARD_USB_DFU */
|
||||
|
||||
/* magic value we use during boot to detect if we should start in DFU
|
||||
* mode or runtime mode */
|
||||
#define USB_DFU_MAGIC 0xDFDFDFDF
|
||||
/* RAM address for this magic value above */
|
||||
#define USB_DFU_MAGIC_ADDR IRAM_ADDR
|
||||
|
||||
/* The API between the core DFU handler and the board/soc specific code */
|
||||
|
||||
struct dfudata {
|
||||
uint8_t status;
|
||||
uint32_t state;
|
||||
int past_manifest;
|
||||
unsigned int total_bytes;
|
||||
};
|
||||
|
||||
extern struct dfudata g_dfu;
|
||||
|
||||
void set_usb_serial_str(const uint8_t *serial_usbstr);
|
||||
|
||||
void DFURT_SwitchToDFU(void);
|
||||
|
||||
/* call-backs from DFU USB function driver to the board/SOC */
|
||||
extern int USBDFU_handle_dnload(uint8_t altif, unsigned int offset,
|
||||
uint8_t *data, unsigned int len);
|
||||
extern int USBDFU_handle_upload(uint8_t altif, unsigned int offset,
|
||||
uint8_t *data, unsigned int req_len);
|
||||
|
||||
/* function to be called at end of EP0 handler during runtime */
|
||||
void USBDFU_Runtime_RequestHandler(const USBGenericRequest *request);
|
||||
|
||||
/* function to be called at end of EP0 handler during DFU mode */
|
||||
void USBDFU_DFU_RequestHandler(const USBGenericRequest *request);
|
||||
|
||||
/* initialization of USB DFU driver (in DFU mode */
|
||||
void USBDFU_Initialize(const USBDDriverDescriptors *pDescriptors);
|
||||
|
||||
/* USBD tells us to switch from DFU mode to application mode */
|
||||
void USBDFU_SwitchToApp(void);
|
||||
|
||||
/* Return values to be used by USBDFU_handle_{dn,up}load */
|
||||
#define DFU_RET_NOTHING 0
|
||||
#define DFU_RET_ZLP 1
|
||||
#define DFU_RET_STALL 2
|
||||
|
||||
#endif
|
||||
124
firmware/atmel_softpack_libraries/usb/device/dfu/dfu_desc.c
Normal file
124
firmware/atmel_softpack_libraries/usb/device/dfu/dfu_desc.c
Normal file
@@ -0,0 +1,124 @@
|
||||
/* DFU related USB Descriptors */
|
||||
/* (C) 2006-2017 Harald Welte <hwelte@hmw-consulting.de> */
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "board.h"
|
||||
|
||||
#include <usb/include/USBDescriptors.h>
|
||||
|
||||
#include <usb/include/USBDDriver.h>
|
||||
|
||||
#include <usb/common/dfu/usb_dfu.h>
|
||||
#include <usb/device/dfu/dfu.h>
|
||||
|
||||
enum {
|
||||
STR_MANUF = 1,
|
||||
STR_PROD,
|
||||
STR_CONFIG,
|
||||
_STR_FIRST_ALT,
|
||||
STR_SERIAL = (_STR_FIRST_ALT+BOARD_DFU_NUM_IF),
|
||||
};
|
||||
|
||||
static const USBDeviceDescriptor fsDevice = {
|
||||
.bLength = sizeof(USBDeviceDescriptor),
|
||||
.bDescriptorType = USBGenericDescriptor_DEVICE,
|
||||
.bcdUSB = USBDeviceDescriptor_USB2_00,
|
||||
.bDeviceClass = 0,
|
||||
.bDeviceSubClass = 0,
|
||||
.bDeviceProtocol = 0,
|
||||
.bMaxPacketSize0 = BOARD_USB_ENDPOINTS_MAXPACKETSIZE(0),
|
||||
.idVendor = BOARD_USB_VENDOR,
|
||||
.idProduct = BOARD_USB_PRODUCT,
|
||||
.bcdDevice = BOARD_USB_RELEASE,
|
||||
.iManufacturer = STR_MANUF,
|
||||
.iProduct = STR_PROD,
|
||||
#ifdef BOARD_USB_SERIAL
|
||||
.iSerialNumber = STR_SERIAL,
|
||||
#else
|
||||
.iSerialNumber = 0,
|
||||
#endif
|
||||
.bNumConfigurations = 1,
|
||||
};
|
||||
|
||||
/* Alternate Interface Descriptor, we use one per partition/memory type */
|
||||
#define DFU_IF(ALT) \
|
||||
{ \
|
||||
.bLength = sizeof(USBInterfaceDescriptor), \
|
||||
.bDescriptorType = USBGenericDescriptor_INTERFACE, \
|
||||
.bInterfaceNumber = 0, \
|
||||
.bAlternateSetting = ALT, \
|
||||
.bNumEndpoints = 0, \
|
||||
.bInterfaceClass = 0xfe, \
|
||||
.bInterfaceSubClass = 1, \
|
||||
.iInterface = (_STR_FIRST_ALT+ALT), \
|
||||
.bInterfaceProtocol = 2, \
|
||||
}
|
||||
|
||||
/* overall descriptor for the DFU configuration, including all
|
||||
* descriptors for alternate interfaces */
|
||||
const struct dfu_desc dfu_cfg_descriptor = {
|
||||
.ucfg = {
|
||||
.bLength = sizeof(USBConfigurationDescriptor),
|
||||
.bDescriptorType = USBGenericDescriptor_CONFIGURATION,
|
||||
.wTotalLength = sizeof(USBConfigurationDescriptor) +
|
||||
BOARD_DFU_NUM_IF * sizeof(USBInterfaceDescriptor) +
|
||||
sizeof(struct usb_dfu_func_descriptor),
|
||||
.bNumInterfaces = 1,
|
||||
.bConfigurationValue = 1,
|
||||
.iConfiguration = STR_CONFIG,
|
||||
.bmAttributes = BOARD_USB_BMATTRIBUTES,
|
||||
.bMaxPower = 100,
|
||||
},
|
||||
.uif[0] = DFU_IF(0),
|
||||
#if BOARD_DFU_NUM_IF > 1
|
||||
.uif[1] = DFU_IF(1),
|
||||
#endif
|
||||
#if BOARD_DFU_NUM_IF > 2
|
||||
.uif[2] = DFU_IF(2),
|
||||
#endif
|
||||
#if BOARD_DFU_NUM_IF > 3
|
||||
.uif[3] = DFU_IF(3),
|
||||
#endif
|
||||
#if BOARD_DFU_NUM_IF > 4
|
||||
.uif[4] = DFU_IF(4),
|
||||
#endif
|
||||
.func_dfu = DFU_FUNC_DESC
|
||||
};
|
||||
|
||||
#if 0
|
||||
#include "usb_strings.h"
|
||||
|
||||
|
||||
static const unsigned char *usb_strings[] = {
|
||||
USB_STRINGS_GENERATED
|
||||
#ifdef BOARD_USB_SERIAL
|
||||
NULL
|
||||
#endif
|
||||
};
|
||||
|
||||
void set_usb_serial_str(const uint8_t *serial_usbstr)
|
||||
{
|
||||
usb_strings[STR_SERIAL] = serial_usbstr;
|
||||
}
|
||||
#endif
|
||||
|
||||
const USBDDriverDescriptors dfu_descriptors = {
|
||||
.pFsDevice = &fsDevice,
|
||||
.pFsConfiguration = &dfu_cfg_descriptor.ucfg,
|
||||
//#if defined (CHIP_USB_UDPHS) || defined(CHIP_USB_OTGHS)
|
||||
#if 0 // DFU only supports FS for now
|
||||
.pFsQualifier = ,
|
||||
.pFsOtherSpeed = ,
|
||||
.pHsDevice = ,
|
||||
.pHsConfiguration = ,
|
||||
.pHsQualifier = ,
|
||||
.pHsOtherSpeed = ,
|
||||
#else
|
||||
0, 0, 0, 0, 0, 0,
|
||||
#endif
|
||||
#if 0
|
||||
.pStrings = usb_strings,
|
||||
.numStrings = ARRAY_SIZE(usb_strings),
|
||||
#endif
|
||||
};
|
||||
466
firmware/atmel_softpack_libraries/usb/device/dfu/dfu_driver.c
Normal file
466
firmware/atmel_softpack_libraries/usb/device/dfu/dfu_driver.c
Normal file
@@ -0,0 +1,466 @@
|
||||
/* USB Device Firmware Update Implementation for OpenPCD, OpenPICC SIMtrace
|
||||
* (C) 2006-2017 by Harald Welte <hwelte@hmw-consulting.de>
|
||||
*
|
||||
* This ought to be compliant to the USB DFU Spec 1.0 as available from
|
||||
* http://www.usb.org/developers/devclass_docs/usbdfu10.pdf
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <board.h>
|
||||
#include <core_cm3.h>
|
||||
|
||||
#include "trace.h"
|
||||
|
||||
#include <usb/include/USBDescriptors.h>
|
||||
#include <usb/include/USBRequests.h>
|
||||
#include <usb/include/USBD.h>
|
||||
#include <usb/common/dfu/usb_dfu.h>
|
||||
#include <usb/device/dfu/dfu.h>
|
||||
|
||||
/* FIXME: this was used for a special ELF section which then got called
|
||||
* by DFU code and Application code, across flash partitions */
|
||||
#define __dfudata
|
||||
#define __dfufunc
|
||||
|
||||
/// Standard device driver instance.
|
||||
static USBDDriver usbdDriver;
|
||||
static unsigned char if_altsettings[1];
|
||||
|
||||
__dfudata struct dfudata g_dfu = {
|
||||
.state = DFU_STATE_appIDLE,
|
||||
.past_manifest = 0,
|
||||
.total_bytes = 0,
|
||||
};
|
||||
|
||||
WEAK void dfu_drv_updatatus(void)
|
||||
{
|
||||
TRACE_INFO("DFU: updstatus()\n\r");
|
||||
|
||||
/* we transition immediately from MANIFEST_SYNC to MANIFEST,
|
||||
* as the flash-writing is not asynchronous in this
|
||||
* implementation */
|
||||
if (g_dfu.state == DFU_STATE_dfuMANIFEST_SYNC)
|
||||
g_dfu.state = DFU_STATE_dfuMANIFEST;
|
||||
}
|
||||
|
||||
static __dfufunc void handle_getstatus(void)
|
||||
{
|
||||
/* has to be static as USBD_Write is async ? */
|
||||
static struct dfu_status dstat;
|
||||
|
||||
dfu_drv_updstatus();
|
||||
|
||||
/* send status response */
|
||||
dstat.bStatus = g_dfu.status;
|
||||
dstat.bState = g_dfu.state;
|
||||
dstat.iString = 0;
|
||||
/* FIXME: set dstat.bwPollTimeout */
|
||||
|
||||
TRACE_DEBUG("handle_getstatus(%u, %u)\n\r", dstat.bStatus, dstat.bState);
|
||||
|
||||
USBD_Write(0, (char *)&dstat, sizeof(dstat), NULL, 0);
|
||||
}
|
||||
|
||||
static void __dfufunc handle_getstate(void)
|
||||
{
|
||||
uint8_t u8 = g_dfu.state;
|
||||
|
||||
TRACE_DEBUG("handle_getstate(%u)\n\r", g_dfu.state);
|
||||
|
||||
USBD_Write(0, (char *)&u8, sizeof(u8), NULL, 0);
|
||||
}
|
||||
|
||||
static void TerminateCtrlInWithNull(void *pArg,
|
||||
unsigned char status,
|
||||
unsigned long int transferred,
|
||||
unsigned long int remaining)
|
||||
{
|
||||
USBD_Write(0, // Endpoint #0
|
||||
0, // No data buffer
|
||||
0, // No data buffer
|
||||
(TransferCallback) 0,
|
||||
(void *) 0);
|
||||
}
|
||||
|
||||
static uint8_t dfu_buf[BOARD_DFU_PAGE_SIZE];
|
||||
|
||||
/* download of a single page has completed */
|
||||
static void dnload_cb(void *arg, unsigned char status, unsigned long int transferred,
|
||||
unsigned long int remaining)
|
||||
{
|
||||
int rc;
|
||||
|
||||
TRACE_DEBUG("COMPLETE\n\r");
|
||||
|
||||
if (status != USBD_STATUS_SUCCESS) {
|
||||
TRACE_ERROR("USBD download callback status %d\n\r", status);
|
||||
USBD_Stall(0);
|
||||
return;
|
||||
}
|
||||
|
||||
rc = USBDFU_handle_dnload(if_altsettings[0], g_dfu.total_bytes, dfu_buf, transferred);
|
||||
switch (rc) {
|
||||
case DFU_RET_ZLP:
|
||||
g_dfu.total_bytes += transferred;
|
||||
g_dfu.state = DFU_STATE_dfuDNLOAD_IDLE;
|
||||
TerminateCtrlInWithNull(0,0,0,0);
|
||||
break;
|
||||
case DFU_RET_STALL:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
USBD_Stall(0);
|
||||
break;
|
||||
case DFU_RET_NOTHING:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static int handle_dnload(uint16_t val, uint16_t len, int first)
|
||||
{
|
||||
int rc;
|
||||
|
||||
if (len > BOARD_DFU_PAGE_SIZE) {
|
||||
TRACE_ERROR("DFU length exceeds flash page size\n\r");
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
g_dfu.status = DFU_STATUS_errADDRESS;
|
||||
return DFU_RET_STALL;
|
||||
}
|
||||
|
||||
if (len & 0x03) {
|
||||
TRACE_ERROR("DFU length not four-byte-aligned\n\r");
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
g_dfu.status = DFU_STATUS_errADDRESS;
|
||||
return DFU_RET_STALL;
|
||||
}
|
||||
|
||||
if (first)
|
||||
g_dfu.total_bytes = 0;
|
||||
|
||||
if (len == 0) {
|
||||
TRACE_DEBUG("zero-size write -> MANIFEST_SYNC\n\r");
|
||||
g_dfu.state = DFU_STATE_dfuMANIFEST_SYNC;
|
||||
return DFU_RET_ZLP;
|
||||
}
|
||||
|
||||
/* else: actually read data */
|
||||
rc = USBD_Read(0, dfu_buf, len, &dnload_cb, 0);
|
||||
if (rc == USBD_STATUS_SUCCESS)
|
||||
return DFU_RET_NOTHING;
|
||||
else
|
||||
return DFU_RET_STALL;
|
||||
}
|
||||
|
||||
/* upload of a single page has completed */
|
||||
static void upload_cb(void *arg, unsigned char status, unsigned long int transferred,
|
||||
unsigned long int remaining)
|
||||
{
|
||||
int rc;
|
||||
|
||||
TRACE_DEBUG("COMPLETE\n\r");
|
||||
|
||||
if (status != USBD_STATUS_SUCCESS) {
|
||||
TRACE_ERROR("USBD upload callback status %d\n\r", status);
|
||||
USBD_Stall(0);
|
||||
return;
|
||||
}
|
||||
|
||||
g_dfu.total_bytes += transferred;
|
||||
}
|
||||
|
||||
static int handle_upload(uint16_t val, uint16_t len, int first)
|
||||
{
|
||||
int rc;
|
||||
|
||||
if (first)
|
||||
g_dfu.total_bytes = 0;
|
||||
|
||||
if (len > BOARD_DFU_PAGE_SIZE) {
|
||||
TRACE_ERROR("DFU length exceeds flash page size\n\r");
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
g_dfu.status = DFU_STATUS_errADDRESS;
|
||||
return DFU_RET_STALL;
|
||||
}
|
||||
|
||||
rc = USBDFU_handle_upload(if_altsettings[0], g_dfu.total_bytes, dfu_buf, len);
|
||||
if (rc < 0) {
|
||||
TRACE_ERROR("application handle_upload() returned %d\n\r", rc);
|
||||
return DFU_RET_STALL;
|
||||
}
|
||||
|
||||
if (USBD_Write(0, dfu_buf, rc, &upload_cb, 0) == USBD_STATUS_SUCCESS)
|
||||
return rc;
|
||||
|
||||
return DFU_RET_STALL;
|
||||
}
|
||||
|
||||
/* this function gets daisy-chained into processing EP0 requests */
|
||||
void USBDFU_DFU_RequestHandler(const USBGenericRequest *request)
|
||||
{
|
||||
uint8_t req = USBGenericRequest_GetRequest(request);
|
||||
uint16_t len = USBGenericRequest_GetLength(request);
|
||||
uint16_t val = USBGenericRequest_GetValue(request);
|
||||
int rc, ret;
|
||||
|
||||
TRACE_DEBUG("type=0x%x, recipient=0x%x val=0x%x len=%u\n\r",
|
||||
USBGenericRequest_GetType(request),
|
||||
USBGenericRequest_GetRecipient(request),
|
||||
val, len);
|
||||
|
||||
/* check for GET_DESCRIPTOR on DFU */
|
||||
if (USBGenericRequest_GetType(request) == USBGenericRequest_STANDARD &&
|
||||
USBGenericRequest_GetRecipient(request) == USBGenericRequest_DEVICE &&
|
||||
USBGenericRequest_GetRequest(request) == USBGenericRequest_GETDESCRIPTOR &&
|
||||
USBGetDescriptorRequest_GetDescriptorType(request) == USB_DT_DFU) {
|
||||
uint16_t length = sizeof(struct usb_dfu_func_descriptor);
|
||||
const USBDeviceDescriptor *pDevice;
|
||||
int terminateWithNull;
|
||||
|
||||
if (USBD_IsHighSpeed())
|
||||
pDevice = usbdDriver.pDescriptors->pHsDevice;
|
||||
else
|
||||
pDevice = usbdDriver.pDescriptors->pFsDevice;
|
||||
|
||||
terminateWithNull = ((length % pDevice->bMaxPacketSize0) == 0);
|
||||
USBD_Write(0, &dfu_cfg_descriptor.func_dfu, length,
|
||||
terminateWithNull ? TerminateCtrlInWithNull : 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* forward all non-DFU specific messages to core handler*/
|
||||
if (USBGenericRequest_GetType(request) != USBGenericRequest_CLASS ||
|
||||
USBGenericRequest_GetRecipient(request) != USBGenericRequest_INTERFACE) {
|
||||
TRACE_DEBUG("std_ho_usbd ");
|
||||
USBDDriver_RequestHandler(&usbdDriver, request);
|
||||
}
|
||||
|
||||
switch (g_dfu.state) {
|
||||
case DFU_STATE_appIDLE:
|
||||
case DFU_STATE_appDETACH:
|
||||
TRACE_ERROR("Invalid DFU State reached in DFU mode\r\n");
|
||||
ret = DFU_RET_STALL;
|
||||
break;
|
||||
case DFU_STATE_dfuIDLE:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_DNLOAD:
|
||||
if (len == 0) {
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
goto out;
|
||||
}
|
||||
g_dfu.state = DFU_STATE_dfuDNLOAD_SYNC;
|
||||
ret = handle_dnload(val, len, 1);
|
||||
break;
|
||||
case USB_REQ_DFU_UPLOAD:
|
||||
g_dfu.state = DFU_STATE_dfuUPLOAD_IDLE;
|
||||
handle_upload(val, len, 1);
|
||||
break;
|
||||
case USB_REQ_DFU_ABORT:
|
||||
/* no zlp? */
|
||||
ret = DFU_RET_ZLP;
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
handle_getstatus();
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
goto out;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case DFU_STATE_dfuDNLOAD_SYNC:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
handle_getstatus();
|
||||
/* FIXME: state transition depending on block completeness */
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
goto out;
|
||||
}
|
||||
break;
|
||||
case DFU_STATE_dfuDNBUSY:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
/* FIXME: only accept getstatus if bwPollTimeout
|
||||
* has elapsed */
|
||||
handle_getstatus();
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
goto out;
|
||||
}
|
||||
break;
|
||||
case DFU_STATE_dfuDNLOAD_IDLE:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_DNLOAD:
|
||||
g_dfu.state = DFU_STATE_dfuDNLOAD_SYNC;
|
||||
ret = handle_dnload(val, len, 0);
|
||||
break;
|
||||
case USB_REQ_DFU_ABORT:
|
||||
g_dfu.state = DFU_STATE_dfuIDLE;
|
||||
ret = DFU_RET_ZLP;
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
handle_getstatus();
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case DFU_STATE_dfuMANIFEST_SYNC:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
handle_getstatus();
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case DFU_STATE_dfuMANIFEST:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
/* we don't want to change to WAIT_RST, as it
|
||||
* would mean that we can not support another
|
||||
* DFU transaction before doing the actual
|
||||
* reset. Instead, we switch to idle and note
|
||||
* that we've already been through MANIFST in
|
||||
* the global variable 'past_manifest'.
|
||||
*/
|
||||
//g_dfu.state = DFU_STATE_dfuMANIFEST_WAIT_RST;
|
||||
g_dfu.state = DFU_STATE_dfuIDLE;
|
||||
g_dfu.past_manifest = 1;
|
||||
handle_getstatus();
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case DFU_STATE_dfuMANIFEST_WAIT_RST:
|
||||
/* we should never go here */
|
||||
break;
|
||||
case DFU_STATE_dfuUPLOAD_IDLE:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_UPLOAD:
|
||||
/* state transition if less data then requested */
|
||||
rc = handle_upload(val, len, 0);
|
||||
if (rc >= 0 && rc < len)
|
||||
g_dfu.state = DFU_STATE_dfuIDLE;
|
||||
break;
|
||||
case USB_REQ_DFU_ABORT:
|
||||
g_dfu.state = DFU_STATE_dfuIDLE;
|
||||
/* no zlp? */
|
||||
ret = DFU_RET_ZLP;
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
handle_getstatus();
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case DFU_STATE_dfuERROR:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
handle_getstatus();
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
case USB_REQ_DFU_CLRSTATUS:
|
||||
g_dfu.state = DFU_STATE_dfuIDLE;
|
||||
g_dfu.status = DFU_STATUS_OK;
|
||||
/* no zlp? */
|
||||
ret = DFU_RET_ZLP;
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_dfuERROR;
|
||||
ret = DFU_RET_STALL;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
out:
|
||||
switch (ret) {
|
||||
case DFU_RET_NOTHING:
|
||||
break;
|
||||
case DFU_RET_ZLP:
|
||||
USBD_Write(0, 0, 0, 0, 0);
|
||||
break;
|
||||
case DFU_RET_STALL:
|
||||
USBD_Stall(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void USBDFU_Initialize(const USBDDriverDescriptors *pDescriptors)
|
||||
{
|
||||
/* We already start in DFU idle mode */
|
||||
g_dfu.state = DFU_STATE_dfuIDLE;
|
||||
|
||||
USBDDriver_Initialize(&usbdDriver, pDescriptors, if_altsettings);
|
||||
|
||||
USBD_Init();
|
||||
USBD_ConfigureSpeed(1);
|
||||
}
|
||||
|
||||
void USBDFU_SwitchToApp(void)
|
||||
{
|
||||
/* make sure the MAGIC is not set to enter DFU again */
|
||||
*(unsigned int *)USB_DFU_MAGIC_ADDR = 0;
|
||||
|
||||
/* disconnect from USB to ensure re-enumeration */
|
||||
USBD_Disconnect();
|
||||
|
||||
/* disable any interrupts during transition */
|
||||
__disable_irq();
|
||||
|
||||
/* Tell the hybrid to execute FTL JUMP! */
|
||||
//BootIntoApp();
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
189
firmware/atmel_softpack_libraries/usb/device/dfu/dfu_runtime.c
Normal file
189
firmware/atmel_softpack_libraries/usb/device/dfu/dfu_runtime.c
Normal file
@@ -0,0 +1,189 @@
|
||||
/* DFU related functions that are active at runtime, i.e. during the
|
||||
* normal operation of the device firmware, *not* during DFU update mode
|
||||
* (C) 2006 Harald Welte <hwelte@hmw-consulting.de>
|
||||
*
|
||||
* This ought to be compliant to the USB DFU Spec 1.0 as available from
|
||||
* http://www.usb.org/developers/devclass_docs/usbdfu10.pdf
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
|
||||
#include <board.h>
|
||||
#include <core_cm3.h>
|
||||
|
||||
#include <usb/include/USBD.h>
|
||||
#include <usb/device/dfu/dfu.h>
|
||||
|
||||
#include "trace.h"
|
||||
|
||||
#include <usb/include/USBDescriptors.h>
|
||||
#include <usb/include/USBRequests.h>
|
||||
#include <usb/include/USBD.h>
|
||||
#include <usb/common/dfu/usb_dfu.h>
|
||||
#include <usb/device/dfu/dfu.h>
|
||||
|
||||
/* FIXME: this was used for a special ELF section which then got called
|
||||
* by DFU code and Application code, across flash partitions */
|
||||
#define __dfudata
|
||||
#define __dfufunc
|
||||
|
||||
static __dfufunc void handle_getstatus(void)
|
||||
{
|
||||
/* has to be static as USBD_Write is async ? */
|
||||
static struct dfu_status dstat;
|
||||
|
||||
/* send status response */
|
||||
dstat.bStatus = g_dfu.status;
|
||||
dstat.bState = g_dfu.state;
|
||||
dstat.iString = 0;
|
||||
/* FIXME: set dstat.bwPollTimeout */
|
||||
|
||||
TRACE_DEBUG("handle_getstatus(%u, %u)\n\r", dstat.bStatus, dstat.bState);
|
||||
|
||||
USBD_Write(0, (char *)&dstat, sizeof(dstat), NULL, 0);
|
||||
}
|
||||
|
||||
static void __dfufunc handle_getstate(void)
|
||||
{
|
||||
uint8_t u8 = g_dfu.state;
|
||||
|
||||
TRACE_DEBUG("handle_getstate(%u)\n\r", g_dfu.state);
|
||||
|
||||
USBD_Write(0, (char *)&u8, sizeof(u8), NULL, 0);
|
||||
}
|
||||
|
||||
static void TerminateCtrlInWithNull(void *pArg,
|
||||
unsigned char status,
|
||||
unsigned long int transferred,
|
||||
unsigned long int remaining)
|
||||
{
|
||||
USBD_Write(0, // Endpoint #0
|
||||
0, // No data buffer
|
||||
0, // No data buffer
|
||||
(TransferCallback) 0,
|
||||
(void *) 0);
|
||||
}
|
||||
|
||||
/* this function gets daisy-chained into processing EP0 requests */
|
||||
void USBDFU_Runtime_RequestHandler(const USBGenericRequest *request)
|
||||
{
|
||||
USBDDriver *usbdDriver = USBD_GetDriver();
|
||||
uint8_t req = USBGenericRequest_GetRequest(request);
|
||||
uint16_t len = USBGenericRequest_GetLength(request);
|
||||
uint16_t val = USBGenericRequest_GetValue(request);
|
||||
int rc, ret;
|
||||
|
||||
TRACE_DEBUG("type=0x%x, recipient=0x%x val=0x%x len=%u\n\r",
|
||||
USBGenericRequest_GetType(request),
|
||||
USBGenericRequest_GetRecipient(request),
|
||||
val, len);
|
||||
|
||||
/* check for GET_DESCRIPTOR on DFU */
|
||||
if (USBGenericRequest_GetType(request) == USBGenericRequest_STANDARD &&
|
||||
USBGenericRequest_GetRecipient(request) == USBGenericRequest_DEVICE &&
|
||||
USBGenericRequest_GetRequest(request) == USBGenericRequest_GETDESCRIPTOR &&
|
||||
USBGetDescriptorRequest_GetDescriptorType(request) == USB_DT_DFU) {
|
||||
uint16_t length = sizeof(struct usb_dfu_func_descriptor);
|
||||
const USBDeviceDescriptor *pDevice;
|
||||
int terminateWithNull;
|
||||
|
||||
if (USBD_IsHighSpeed())
|
||||
pDevice = usbdDriver->pDescriptors->pHsDevice;
|
||||
else
|
||||
pDevice = usbdDriver->pDescriptors->pFsDevice;
|
||||
|
||||
terminateWithNull = ((length % pDevice->bMaxPacketSize0) == 0);
|
||||
USBD_Write(0, &dfu_cfg_descriptor.func_dfu, length,
|
||||
terminateWithNull ? TerminateCtrlInWithNull : 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* forward all non-DFU specific messages to core handler*/
|
||||
if (USBGenericRequest_GetType(request) != USBGenericRequest_CLASS ||
|
||||
USBGenericRequest_GetRecipient(request) != USBGenericRequest_INTERFACE) {
|
||||
TRACE_DEBUG("std_ho_usbd ");
|
||||
USBDDriver_RequestHandler(usbdDriver, request);
|
||||
}
|
||||
|
||||
switch (g_dfu.state) {
|
||||
case DFU_STATE_appIDLE:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
handle_getstatus();
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
case USB_REQ_DFU_DETACH:
|
||||
/* we switch it DETACH state, send a ZLP and
|
||||
* return. The next USB reset in this state
|
||||
* will then trigger DFURT_SwitchToDFU() below */
|
||||
TRACE_DEBUG("\r\n====dfu_detach\n\r");
|
||||
g_dfu.state = DFU_STATE_appDETACH;
|
||||
ret = DFU_RET_ZLP;
|
||||
goto out;
|
||||
break;
|
||||
default:
|
||||
ret = DFU_RET_STALL;
|
||||
}
|
||||
break;
|
||||
case DFU_STATE_appDETACH:
|
||||
switch (req) {
|
||||
case USB_REQ_DFU_GETSTATUS:
|
||||
handle_getstatus();
|
||||
break;
|
||||
case USB_REQ_DFU_GETSTATE:
|
||||
handle_getstate();
|
||||
break;
|
||||
default:
|
||||
g_dfu.state = DFU_STATE_appIDLE;
|
||||
ret = DFU_RET_STALL;
|
||||
goto out;
|
||||
break;
|
||||
}
|
||||
/* FIXME: implement timer to return to appIDLE */
|
||||
break;
|
||||
default:
|
||||
TRACE_ERROR("Invalid DFU State reached in Runtime Mode\r\n");
|
||||
ret = DFU_RET_STALL;
|
||||
break;
|
||||
}
|
||||
|
||||
out:
|
||||
switch (ret) {
|
||||
case DFU_RET_NOTHING:
|
||||
break;
|
||||
case DFU_RET_ZLP:
|
||||
USBD_Write(0, 0, 0, 0, 0);
|
||||
break;
|
||||
case DFU_RET_STALL:
|
||||
USBD_Stall(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DFURT_SwitchToDFU(void)
|
||||
{
|
||||
unsigned int *dfu_except_tbl = (unsigned int *)IFLASH_ADDR;
|
||||
void (*toDFU)(void) = (void *)dfu_except_tbl[1];
|
||||
|
||||
*(unsigned int *)USB_DFU_MAGIC_ADDR = USB_DFU_MAGIC;
|
||||
|
||||
USBD_Disconnect();
|
||||
__disable_irq();
|
||||
|
||||
toDFU();
|
||||
}
|
||||
Reference in New Issue
Block a user