378 lines
18 KiB
C
378 lines
18 KiB
C
/** library for USB DFU to write on internal flash
|
|
* @file
|
|
* @author King Kévin <kingkevin@cuvoodoo.info>
|
|
* @copyright SPDX-License-Identifier: GPL-3.0-or-later
|
|
* @date 2017-2020
|
|
* @note peripherals used: USB
|
|
*/
|
|
|
|
/* standard libraries */
|
|
#include <stdint.h> // standard integer types
|
|
#include <stdlib.h> // general utilities
|
|
#include <string.h> // memory utilities
|
|
|
|
/* STM32 (including CM3) libraries */
|
|
#include <libopencmsis/core_cm3.h> // Cortex M3 utilities
|
|
#include <libopencm3/cm3/scb.h> // reset utilities
|
|
#include <libopencm3/stm32/rcc.h> // real-time control clock library
|
|
#include <libopencm3/stm32/gpio.h> // general purpose input output library
|
|
#include <libopencm3/stm32/desig.h> // flash size definition
|
|
#include <libopencm3/stm32/flash.h> // flash utilities
|
|
#include <libopencm3/usb/usbd.h> // USB library
|
|
#include <libopencm3/usb/dfu.h> // USB DFU library
|
|
|
|
#include "global.h" // global utilities
|
|
#include "usb_dfu.h" // USB DFU header and definitions
|
|
#include "flash_internal.h" // internal flash utilities
|
|
|
|
static uint8_t usbd_control_buffer[1024] = {0}; /**< buffer to be used for control requests (must be a flash section divider) */
|
|
static usbd_device *usb_device = NULL; /**< structure holding all the info related to the USB device */
|
|
static enum dfu_state usb_dfu_state = STATE_DFU_IDLE; /**< current DFU state */
|
|
static enum dfu_status usb_dfu_status = DFU_STATUS_OK; /**< current DFU status */
|
|
|
|
static uint8_t download_data[sizeof(usbd_control_buffer)] = {0}; /**< downloaded data to be programmed in flash */
|
|
static uint16_t download_length = 0; /**< length of downloaded data */
|
|
static uint32_t download_offset = 0; /**< destination offset of where the downloaded data should be flashed */
|
|
static uint8_t firmware_head[8] = {0xff}; /**< to store the first bytes of the firmware, to be flashed at the end so we will stay in bootloader mode when the download is interrupted */
|
|
|
|
/** symbol for beginning of the application
|
|
* @note this symbol will be provided by the bootloader linker script
|
|
*/
|
|
extern char __application_beginning;
|
|
|
|
/** USB DFU device descriptor
|
|
* @note as defined in USB Device Firmware Upgrade specification section 4.2.1
|
|
*/
|
|
static const struct usb_device_descriptor usb_dfu_device = {
|
|
.bLength = USB_DT_DEVICE_SIZE, /**< the size of this header in bytes, 18 */
|
|
.bDescriptorType = USB_DT_DEVICE, /**< a value of 1 indicates that this is a device descriptor */
|
|
.bcdUSB = 0x0200, /**< this device supports USB 2.0 */
|
|
.bDeviceClass = 0, /**< unused */
|
|
.bDeviceSubClass = 0, /**< unused */
|
|
.bDeviceProtocol = 0, /**< unused */
|
|
.bMaxPacketSize0 = 64, /**< packet size for endpoint zero in bytes */
|
|
.idVendor = 0x1209, /**< pid.codes vendor ID */
|
|
.idProduct = 0x4356, /**< CuVoodo product ID within the Vendor ID space */
|
|
.bcdDevice = 0x0000, /**< Device Release Number: board version number */
|
|
.iManufacturer = 1, /**< the index of the string in the string table that represents the name of the manufacturer of this device */
|
|
.iProduct = 2, /**< the index of the string in the string table that represents the name of the product */
|
|
.iSerialNumber = 3, /**< the index of the string in the string table that represents the serial number of this item in string form */
|
|
.bNumConfigurations = 1, /**< the number of possible configurations this device has */
|
|
};
|
|
|
|
/** USB DFU functional descriptor
|
|
* @note as defined in USB Device Firmware Upgrade specification section 4.2.4
|
|
*/
|
|
static const struct usb_dfu_descriptor usb_dfu_functional = {
|
|
.bLength = sizeof(struct usb_dfu_descriptor), /**< provide own size */
|
|
.bDescriptorType = DFU_FUNCTIONAL, /**< functional descriptor type */
|
|
.bmAttributes = USB_DFU_CAN_DOWNLOAD | USB_DFU_WILL_DETACH, /**< this DFU can download and will detach after download (we don't support manifest for simplicity, technically we could) */
|
|
.wDetachTimeout = 0, /**< maximum time in milliseconds to detach (and reboot) */
|
|
.wTransferSize = sizeof(usbd_control_buffer), /**< set max transfer size */
|
|
.bcdDFUVersion = 0x0110, /**< DFU specification version 1.1 used */
|
|
};
|
|
|
|
/** USB DFU interface descriptor
|
|
* @note as defined in USB Device Firmware Upgrade specification section 4.2.3
|
|
*/
|
|
static const struct usb_interface_descriptor usb_dfu_interface = {
|
|
.bLength = USB_DT_INTERFACE_SIZE, /**< size of descriptor in byte */
|
|
.bDescriptorType = USB_DT_INTERFACE, /**< interface descriptor type */
|
|
.bInterfaceNumber = 0, /**< this interface is the first (and only) */
|
|
.bAlternateSetting = 0, /**< no alternative settings */
|
|
.bNumEndpoints = 0, /**< only the control pipe at endpoint 0 is used */
|
|
.bInterfaceClass = 0xFE, /**< DFU interface class (not defined in libopencm3 dfu lib) */
|
|
.bInterfaceSubClass = 1, /**< DFU interface subclass (not defined in libopencm3 dfu lib) */
|
|
.bInterfaceProtocol = 2, /**< DFU interface mode protocol (not defined in libopencm3 dfu lib) */
|
|
.iInterface = 4, /**< the index of the string in the string table that represents interface description */
|
|
.extra = &usb_dfu_functional, /**< point to functional descriptor */
|
|
.extralen = sizeof(usb_dfu_functional), /**< size of functional descriptor */
|
|
};
|
|
|
|
/** USB DFU interface descriptor list */
|
|
static const struct usb_interface usb_dfu_interfaces[] = {{
|
|
.num_altsetting = 1, /**< this is the only alternative */
|
|
.altsetting = &usb_dfu_interface, /**< point to only interface descriptor */
|
|
}};
|
|
|
|
/** USB DFU configuration descriptor
|
|
* @note as defined in USB Device Firmware Upgrade specification section 4.2.2
|
|
*/
|
|
static const struct usb_config_descriptor usb_dfu_configuration = {
|
|
.bLength = USB_DT_CONFIGURATION_SIZE, /**< the length of this header in bytes */
|
|
.bDescriptorType = USB_DT_CONFIGURATION, /**< a value of 2 indicates that this is a configuration descriptor */
|
|
.wTotalLength = 0, /**< total size of the configuration descriptor including all sub interfaces (automatically filled in by the USB stack in libopencm3) */
|
|
.bNumInterfaces = LENGTH(usb_dfu_interfaces), /**< the number of interfaces in this configuration */
|
|
.bConfigurationValue = 1, /**< the index of this configuration */
|
|
.iConfiguration = 0, /**< a string index describing this configuration (zero means not provided) */
|
|
.bmAttributes = 0x80, /**< bus powered (1<<7) */
|
|
.bMaxPower = 0x32, /**< the maximum amount of current that this device will draw in 2mA units */
|
|
// end of header
|
|
.interface = usb_dfu_interfaces, /**< pointer to an array of interfaces */
|
|
};
|
|
|
|
/** device ID used as serial number */
|
|
static char usb_serial[] = "0123456789ab";
|
|
|
|
/** USB string table
|
|
* @note starts with index 1
|
|
*/
|
|
static const char *usb_dfu_strings[] = {
|
|
"CuVoodoo", /**< manufacturer string */
|
|
"CuVoodoo STM32F4xx DFU bootloader", /**< product string */
|
|
(const char*)usb_serial, /**< device ID used as serial number */
|
|
"DFU bootloader (DFU mode)", /**< DFU interface string */
|
|
};
|
|
|
|
/** disconnect USB to force re-enumerate */
|
|
static void usb_disconnect(void)
|
|
{
|
|
if (usb_device) {
|
|
usbd_disconnect(usb_device, true);
|
|
}
|
|
// pull USB D+ low for a short while
|
|
rcc_periph_clock_enable(RCC_GPIOA);
|
|
gpio_mode_setup(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO12);
|
|
gpio_set_output_options(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO12);
|
|
gpio_clear(GPIOA, GPIO12);
|
|
// USB disconnected must be at least 10 ms long, at most 100 ms
|
|
for (volatile uint32_t i = 0; i < 0x2000; i++);
|
|
}
|
|
|
|
/** disconnect USB and perform system reset
|
|
* @param[in] usbd_dev USB device (unused)
|
|
* @param[in] req USB request (unused)
|
|
* @note this function is called after the corresponding GETSTATUS request
|
|
*/
|
|
static void usb_dfu_reset(usbd_device *usbd_dev, struct usb_setup_data *req)
|
|
{
|
|
(void)usbd_dev; // variable not used
|
|
(void)req; // variable not used
|
|
usb_disconnect(); // USB detach (disconnect to force re-enumeration)
|
|
scb_reset_system(); // reset device
|
|
while (true); // wait for the reset to happen
|
|
}
|
|
|
|
/** flash downloaded data block
|
|
* @param[in] usbd_dev USB device (unused)
|
|
* @param[in] req USB request (unused)
|
|
* @note this function is called after the corresponding GETSTATUS request
|
|
*/
|
|
static void usb_dfu_flash(usbd_device *usbd_dev, struct usb_setup_data *req)
|
|
{
|
|
(void)usbd_dev; // variable not used
|
|
(void)req; // variable not used
|
|
led_off(); // indicate we are processing
|
|
static struct flash_internal_section_info_s* erased_section = NULL; // last erased flash section
|
|
const struct flash_internal_section_info_s* target_section = flash_internal_section((uint32_t)&__application_beginning + download_offset); // get the section where to write the downloaded data
|
|
if (NULL == target_section) { // the corresponding section does not exist or is unknown
|
|
usb_dfu_status = DFU_STATUS_ERR_ADDRESS; // tell there is something wrong with the address
|
|
usb_dfu_state = STATE_DFU_ERROR; // tell the is something wrong
|
|
led_on(); // indicate we finished processing
|
|
return;
|
|
}
|
|
if (STATE_DFU_MANIFEST != usb_dfu_state && target_section != erased_section) { // we started a new section
|
|
// WARNING we except the offset to increment: if the offset lands in a previous section, it will be erased, possibly deleting previously written data
|
|
flash_clear_status_flags(); // clear all errors before performing operation
|
|
flash_unlock(); // unlock flash so we can erase the section
|
|
flash_erase_sector(target_section->number, 2); // erase section
|
|
flash_lock(); // lock back after operation completed
|
|
if (FLASH_SR) { // error during erasure happened (EOP is not set since we did not enable interrupts)
|
|
usb_dfu_status = DFU_STATUS_ERR_ERASE; // tell there is something wrong while erasing
|
|
usb_dfu_state = STATE_DFU_ERROR; // tell the is something wrong
|
|
led_on(); // indicate we finished processing
|
|
return;
|
|
}
|
|
// verify the section has been erased (recommended by TRM)
|
|
bool erased = true;
|
|
for (uint32_t* address = (uint32_t*)target_section->start; address <= (uint32_t*)target_section->end; address++) {
|
|
if (0xffffffff != *address) {
|
|
erased = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!erased) { // error during erasure happened
|
|
usb_dfu_status = DFU_STATUS_ERR_ERASE; // tell there is something wrong while erasing
|
|
usb_dfu_state = STATE_DFU_ERROR; // tell the is something wrong
|
|
led_on(); // indicate we finished processing
|
|
return;
|
|
}
|
|
erased_section = (struct flash_internal_section_info_s*)target_section; // remember which section has been erased
|
|
}
|
|
flash_clear_status_flags(); // clear all errors before performing operation
|
|
flash_unlock(); // unlock flash so we can write to it
|
|
flash_program((uint32_t)&__application_beginning + download_offset, download_data, download_length); // program data
|
|
flash_lock(); // lock back after operation completed
|
|
if (0 == FLASH_SR) { // check if flashing worked (EOP is not set since we did not enable interrupts)
|
|
switch (usb_dfu_state) {
|
|
case STATE_DFU_DNLOAD_SYNC: // download ongoing
|
|
case STATE_DFU_DNBUSY: // flashing ongoing
|
|
usb_dfu_state = STATE_DFU_DNLOAD_IDLE; // go back to idle stat to wait for next segment
|
|
break;
|
|
case STATE_DFU_MANIFEST: // this was the last part
|
|
led_off(); // indicate the end
|
|
usb_dfu_reset(NULL, NULL); // start reset without waiting for request since we advertised we would detach
|
|
break;
|
|
default: // unknown state
|
|
usb_dfu_status = DFU_STATUS_ERR_PROG;
|
|
usb_dfu_state = STATE_DFU_ERROR;
|
|
break;
|
|
}
|
|
} else { // warn about writing error
|
|
usb_dfu_status = DFU_STATUS_ERR_WRITE;
|
|
usb_dfu_state = STATE_DFU_ERROR;
|
|
}
|
|
led_on(); // indicate we finished processing
|
|
}
|
|
|
|
/** handle incoming USB DFU control request
|
|
* @param[in] usbd_dev USB device descriptor
|
|
* @param[in] req control request information
|
|
* @param[in] buf control request data
|
|
* @param[in] len control request data length
|
|
* @param[in] complete not used
|
|
* @return USBD_REQ_HANDLED if handled correctly, USBD_REQ_NOTSUPP else
|
|
* @note resets device when configured with 5 bits
|
|
*/
|
|
static enum usbd_request_return_codes usb_dfu_control_request(usbd_device *usbd_dev, struct usb_setup_data *req, uint8_t **buf, uint16_t *len, void (**complete)(usbd_device *usbd_dev, struct usb_setup_data *req))
|
|
{
|
|
(void)complete;
|
|
(void)usbd_dev; // device is not used
|
|
|
|
// DFU only requires handling class requests
|
|
if ((req->bmRequestType & USB_REQ_TYPE_TYPE) != USB_REQ_TYPE_CLASS) {
|
|
return USBD_REQ_NOTSUPP;
|
|
}
|
|
|
|
led_off(); // indicate we are processing request
|
|
int to_return = USBD_REQ_HANDLED; // value to return
|
|
switch (req->bRequest) {
|
|
case DFU_DNLOAD: // download firmware on flash
|
|
if (STATE_DFU_IDLE != usb_dfu_state && STATE_DFU_DNLOAD_IDLE != usb_dfu_state) { // wrong start to request download
|
|
// warn about programming error
|
|
usb_dfu_status = DFU_STATUS_ERR_PROG;
|
|
usb_dfu_state = STATE_DFU_ERROR;
|
|
} else if (STATE_DFU_IDLE == usb_dfu_state && ((NULL == len) || (0 == *len))) { // download request should not start empty
|
|
// warn about programming error
|
|
usb_dfu_status = DFU_STATUS_ERR_PROG;
|
|
usb_dfu_state = STATE_DFU_ERROR;
|
|
} else if (STATE_DFU_DNLOAD_IDLE == usb_dfu_state && ((NULL == len) || (0 == *len))) { // download completed
|
|
// go to manifestation phase
|
|
usb_dfu_state = STATE_DFU_MANIFEST_SYNC;
|
|
} else { // there is data to be flashed
|
|
download_length = *len; // remember download length
|
|
const uint32_t offset = req->wValue * sizeof(usbd_control_buffer); // remember offset
|
|
if (offset != 0 && offset <= download_offset) { // ensure the offset is incrementing (else this break our section erase procedure)
|
|
usb_dfu_status = DFU_STATUS_ERR_PROG;
|
|
usb_dfu_state = STATE_DFU_ERROR;
|
|
} else if (*len > sizeof(usbd_control_buffer) || *len > sizeof(download_data)) { // got too much data
|
|
usb_dfu_status = DFU_STATUS_ERR_PROG;
|
|
usb_dfu_state = STATE_DFU_ERROR;
|
|
} else if ((uint32_t)&__application_beginning + download_offset + download_length >= (uint32_t)(FLASH_BASE + desig_get_flash_size() * 1024)) {
|
|
// application data is exceeding advertised flash size
|
|
usb_dfu_status = DFU_STATUS_ERR_ADDRESS;
|
|
usb_dfu_state = STATE_DFU_ERROR;
|
|
} else {
|
|
download_offset = offset; // remember offset
|
|
memcpy(download_data, *buf, *len); // copy data (length has already been checked))
|
|
if (0 == download_offset) { // this is the beginning of the firmware
|
|
// we will keep the beginning of the firmware and flash in the head
|
|
// this is because the head is used to check if the bootloader should boot into the application
|
|
// but if the download gets interrupted, the head is valid and the application will be started
|
|
// but the application is corrupted, and will not allow to go back to the bootloader
|
|
// by flashing the firmware head at the end, we ensure the application to boot is valid
|
|
// note: the force DFU input/button could be used to start the bootloader when the application is corrupted
|
|
for (uint16_t i = 0 ; i < LENGTH(firmware_head) && i < sizeof(download_data); i++) {
|
|
firmware_head[i] = download_data[i]; // backup head
|
|
download_data[i] = 0xff; // invalided head (with value that still allows to program it later)
|
|
}
|
|
}
|
|
usb_dfu_state = STATE_DFU_DNLOAD_SYNC; // go to sync state
|
|
*complete = usb_dfu_flash; // start flashing the downloaded data
|
|
}
|
|
}
|
|
break;
|
|
case DFU_UPLOAD: // upload firmware from flash
|
|
to_return = USBD_REQ_NOTSUPP; // upload no supported
|
|
break;
|
|
case DFU_GETSTATUS: // get status
|
|
(*buf)[0] = usb_dfu_status; // set status
|
|
(*buf)[1] = 10; // set poll timeout (24 bits, in milliseconds) to small value for periodical poll
|
|
(*buf)[2] = 0; // set poll timeout (24 bits, in milliseconds) to small value for periodical poll
|
|
(*buf)[3] = 0; // set poll timeout (24 bits, in milliseconds) to small value for periodical poll
|
|
(*buf)[4] = usb_dfu_state; // set state
|
|
(*buf)[5] = 0; // string not used
|
|
*len = 6; // set length of buffer to return
|
|
if (STATE_DFU_DNLOAD_SYNC == usb_dfu_state) {
|
|
usb_dfu_state = STATE_DFU_DNBUSY; // switch to busy state
|
|
} else if (STATE_DFU_MANIFEST_SYNC == usb_dfu_state) {
|
|
// we still need to flash the saved head of the firmware
|
|
for (uint16_t i = 0 ; i < LENGTH(firmware_head) && i < sizeof(download_data); i++) {
|
|
download_data[i] = firmware_head[i]; // prepare data to be flashed
|
|
}
|
|
download_offset = 0; // flash the head of the firmware
|
|
download_length = sizeof(firmware_head); // only flash the head
|
|
usb_dfu_state = STATE_DFU_MANIFEST; // go to manifest mode
|
|
*complete = usb_dfu_flash; // flash the head
|
|
}
|
|
break;
|
|
case DFU_CLRSTATUS: // clear status
|
|
if (STATE_DFU_ERROR == usb_dfu_state || DFU_STATUS_OK != usb_dfu_status) { // only clear in case there is an error
|
|
usb_dfu_status = DFU_STATUS_OK; // clear error status
|
|
usb_dfu_state = STATE_DFU_IDLE; // put back in idle state
|
|
}
|
|
break;
|
|
case DFU_GETSTATE: // get state
|
|
(*buf)[0] = usb_dfu_state; // return state
|
|
*len = 1; // only state needs to be provided
|
|
break;
|
|
case DFU_ABORT: // abort current operation
|
|
usb_dfu_state = STATE_DFU_IDLE; // put back in idle state (nothing else to do)
|
|
break;
|
|
case DFU_DETACH: // this request is only defined in runtime mode
|
|
default:
|
|
to_return = USBD_REQ_NOTSUPP;
|
|
}
|
|
led_on(); // indicate we finished processing
|
|
|
|
return to_return;
|
|
}
|
|
|
|
void usb_dfu_setup(void)
|
|
{
|
|
// set serial according to STM32 USB-FS_Device developer kit
|
|
// see https://community.st.com/s/question/0D50X0000ADCaTJSQ1/dfu-serial-number
|
|
for (uint8_t i = 0; i < LENGTH(usb_serial) - 1; i++) { // write the serial
|
|
uint32_t id; // current ID part
|
|
if (i < 8) {
|
|
id = DESIG_UNIQUE_ID0 + DESIG_UNIQUE_ID2;
|
|
} else {
|
|
id = DESIG_UNIQUE_ID1;
|
|
}
|
|
// transform into character
|
|
char c = (id >> ((7 - (i % 8)) * 4)) & 0x0f; // get nibble
|
|
if (c < 10) {
|
|
c = '0' + c;
|
|
} else {
|
|
c = 'a' + (c - 10);
|
|
}
|
|
// set character
|
|
usb_serial[i] = c;
|
|
}
|
|
|
|
rcc_periph_reset_pulse(RST_OTGFS); // reset USB peripheral
|
|
rcc_periph_clock_enable(RCC_GPIOA); // enable clock for GPIO used for USB
|
|
gpio_mode_setup(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO11 | GPIO12); // set pin to alternate function
|
|
gpio_set_af(GPIOA, GPIO_AF10, GPIO11 | GPIO12); // set alternate function to USB
|
|
usb_device = usbd_init(&otgfs_usb_driver, &usb_dfu_device, &usb_dfu_configuration, usb_dfu_strings, LENGTH(usb_dfu_strings), usbd_control_buffer, sizeof(usbd_control_buffer)); // configure USB device
|
|
usbd_register_control_callback(usb_device, USB_REQ_TYPE_CLASS | USB_REQ_TYPE_INTERFACE, USB_REQ_TYPE_TYPE | USB_REQ_TYPE_RECIPIENT, usb_dfu_control_request); // set control request handling DFU operations
|
|
}
|
|
|
|
void usb_dfu_start(void)
|
|
{
|
|
// infinitely poll device to handle requests
|
|
while (true) {
|
|
usbd_poll(usb_device);
|
|
}
|
|
}
|