Impementation of USB DFU secondary bootloader for NXP LPC11U68 MCU

Contents

What is DFU?

USB Device Firmware Upgrade (DFU) is an official device class specification from the USB Implementers Forum. It’s purpose to provide standard and hardware-independent way to update firmware and code of a compliant USB Device. DFU-compatible device can be used with any standard DFU host firmware update tool, ideally vendor-independent. Also DFU protocol support not only firmware download (write into device by the host), it also specifies upload function, so loading the currently installed device firmware to the USB Host is possible. This can be useful for backup or verification purposes.

The latest USB DFU standard version is 1.1, and it can be freely downloaded from USB.org site

However, even with simple architecture and DFU being an official USB standard for many years, actually only very few companies have implemented it in products. To help correct this situation a bit, decision to write detailed and practical tutorial of implementing DFU functionality was made.

System target is USB-equipped Cortex-M0+ microcontroller from NXP, model LPC11U68. This is small, yet powerful chip, featuring 256KB of internal Flash-memory for firmware code, 32KB SRAM and fully featured USB 2.0 Device controller. Host device used in this tutorial is regular Windows x64 based PC. Development IDE for compiling and debug of our test C-application is IAR ARM ver.7.50

What is a bootloader?

The bootloader is code that is executed every time the system/processor is powered on or reset. The bootloader have functions to read/write firmware and memory or jump and execute the user application code. Bootloader usually also have function to verify correct firmware image (by Checksum, CRC or similar verification methods). Sometimes bootloader support operation with encrypted/secured firmware to avoid unwanted tampering and device cloning.

Usually bootloader using one of available on-chip interfaces as main transport for firmware download/upload and control. In case of NXP LPC-series Cortex-M microcontrollers it’s either UART or USB.

Integrated bootloader

This primary bootloader is implemented by NXP and stored in ROM. During the boot process, the primary bootloader checks whether there is valid user code detected in Flash memory. The criterion for valid user code is as follows: The reserved ARM Cortex-M0 exception vector location 7 (offset 0×0000001C in the vector table) should contain the 2’s complement of the checksum of table entries 0 through 6. This causes the checksum of the first 8 table entries to be 0. The bootloader code checksums the first 8 locations in sector 0 of the flash. If the result is 0, then execution control is transferred to the user code. If the signature is not valid, the boot code checks pin PIO0.3 and enumerates as USB MSC device (if pin PIO0.3 is HIGH) or enters ISP UART mode (PIO0.3 is read LOW).

One important thing, is correct setting of NVIC VTOR, to map for correct vector map. Without this our application mapping will not work correctly. This register has native implementation in ARM Cortex-M0+, and documented in ARM Reference Guide.

Here’s important table with register of System Controller registers:

Address Name Type Reset value Description
0xE000ED00 CPUID RO 0×410CC601 CPUID Register
0xE000ED04 ICSR RW 0×00000000 Interrupt Control and State Register
0xE000ED08 VTOR RW 0×00000000 Vector Table Offset Register
0xE000ED0C AIRCR RW 0xFA050000 Application Interrupt and Reset Control Register
0xE000ED10 SCR RW 0×00000000 System Control Register
0xE000ED14 CCR RO 0×00000204 Configuration and Control Register
0xE000ED1C SHPR2 RW 0×00000000 System Handler Priority Register 2
0xE000ED20 SHPR3 RW 0×00000000 System Handler Priority Register 3

Table from ARM M0+ documentation: summary of the SCB registers

Secondary bootloader with DFU support

#define VTOR_ADDR                                   0xE000ED08
#define CPUID_ADDR                                  0xE000ED00
#define user_start_sector_address                   0x3000
#define delay_wait                                  1200        // Small delay to let UART finish txmit

defines

/*****************************************************************************
**   User app loader function
*****************************************************************************/
void run_user_code(void){
    vector_table_layout const*const user_vector_table=(vector_table_layout*)user_start_sector_address;
    unsigned int volatile * const vtor_reg = (unsigned int *) VTOR_ADDR;
    NVIC_DisableIRQ(TIMER_16_0_IRQn);                       // Disable Timer IRQ

    __disable_irq();                                        //no interrupt should be enabled by bootloader, but disable interrupts to be on safe side
    //The variable port is a constant pointer to a volatile unsigned integer, so we can access the memory-mapped register
    *vtor_reg = (user_start_sector_address);                //set vector table offset to user code
    __set_MSP(user_vector_table->stack_address);            //load stackpointer with initial value

    for (uint32_t del = 0; del < delay_wait; del++);        // Small delay to let UART finish
    __set_CONTROL(0);                                       // Change from PSP to MSP

    __enable_irq();
    Chip_Clock_SetMainClockSource(SYSCTL_MAINCLKSRC_IRC);   // Switch to IRC
    (user_vector_table->reset_address)();                   // Call user code
}

Due to System Control Block being part of ARM core, extra code construction was used to access and write VTOR register. Details on access SCB register covered well in ARM’s Article: Placing C variables at specific addresses to access memory-mapped peripherals.

Jumper

uint32_t FlashBlankCheck(void)
{
    int out_res;
    int start_sector;
    int end_sector;
    uint32_t command[5], result[4];

    start_sector = 3;                               // Sectors 0-2 used by SBL
    end_sector = 25;                                // Final sector is 28

    /* Disable interrupt mode so it doesn't fire during FLASH updates */
    __disable_irq();

    command[0] = IAP_BLANK_CHECK_SECTOR_CMD;
    command[1] = start_sector;
    command[2] = end_sector;
    iap_entry(command, result);

    /* Error checking */
    if(result[0] == IAP_SECTOR_NOT_BLANK) {
        out_res = result[1] + 1;
    } else if (result[0] == IAP_CMD_SUCCESS ) {
        out_res = 0;                                // App section is blank
    }

    /* Re-enable interrupt mode */
    __enable_irq();

    return out_res;
}

test

/*****************************************************************************
**   Application verification function
*****************************************************************************/
uint8_t check_user_app(void) {
    uint8_t* Memory = (uint8_t*)user_start_sector_address;
    uint32_t i;
    uint32_t blank_sts;
    // Check for blank
    blank_sts = FlashBlankCheck();

    if( blank_sts == 0 ) {
        // Blank!
        DEBUGOUT("\033[0;31m-i- Firmware not loaded ...\r\n");
        return 1;                                       // APP Code not good
    } else {
        // Not blank
        DEBUGOUT("\033[0;36m-i- Code found at 0x%08X ...", (blank_sts - 1) + user_start_sector_address ); // Status - 1 to allow non-blank byte at offset 0x0
        DEBUGOUT("\r\n\033[0;33m-d-     00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\033[0;36m");
        for (i = 0; i <= 0xFF; i++) {
            if (!(i % 16)) {
                // Output row index
                DEBUGOUT("\r\n\033[0;33m-d- %02X \033[0;36m",i / 16);
            }
            DEBUGOUT("%02X ",Memory[i]);
        };

    }
    DEBUGOUT("\r\n");
    return 0;                                       // APP Code seems good

};

test

/*****************************************************************************
**   Main Function  main()
*****************************************************************************/
int main (void) {
    USBD_API_INIT_PARAM_T usb_param;
    USB_CORE_DESCS_T desc;
    ErrorCode_t ret;
    char dmesg_buf[64];
    USB_INTERFACE_DESCRIPTOR* pIntfDesc;
    USB_COMMON_DESCRIPTOR *pD;
    uint32_t next_desc_adr, total_len = 0;
    uint8_t button_enter_dfu = 0;                   // Enter DFU if FALSE

    SystemCoreClockUpdate();
    Chip_Clock_SetMainClockSource(SYSCTL_MAINCLKSRC_PLLOUT);// Switch to main PLL Clock

    Chip_USB_Init();                                        // enable clocks and pinmux

    UARTInit(115200);                                       // Setup UART for 115.2K, 8N1

    strncpy(dmesg_buf, "\r\n\r\n\033[1;36;49m EVGA SBL * $FWVer: 29 2016/10/13 09:37:07 $ \r\n", 64);
    UARTSend((uint8_t*)dmesg_buf, strlen(dmesg_buf));
    strncpy(dmesg_buf, "\033[1;31m-n- Hold RETURN button on POWER-ON to update FW\033[1;36m\r\n", 64);
    UARTSend((uint8_t*)dmesg_buf, strlen(dmesg_buf));

    button_enter_dfu = key_read(KEY_RETURN_ID);

    pUsbApi = (USBD_API_T*)((*(ROM **)(0x1FFF1FF8))->pUSBD); // get USB API table pointer

    /* Initialize Descriptor pointers */
    memset((void*)&desc, 0, sizeof(USB_CORE_DESCS_T));
    desc.device_desc = (uint8_t *)&USB_DeviceDescriptor[0];
    desc.string_desc = (uint8_t *)&USB_StringDescriptor[0];
    desc.full_speed_desc = (uint8_t *)&USB_FsConfigDescriptor[0];
    desc.high_speed_desc = (uint8_t *)&USB_FsConfigDescriptor[0];
    /* Valid application located in the next sector(s) of flash so execute */

    if (button_enter_dfu) {
        sprintf(dmesg_buf, "-i- Booting ... from 0x%08X \033[0;39;49m\r\n", user_start_sector_address);
        UARTSend((uint8_t*)dmesg_buf, strlen(dmesg_buf));
        if (!check_user_app()) {                                // Test if app correct
            // Valid data found, starting...
            run_user_code();                                        // Jump to user application
        } else {
            sprintf(dmesg_buf, "-E- Booting failed, revert to DFU \033[0;39;49m\r\n");
            UARTSend((uint8_t*)dmesg_buf, strlen(dmesg_buf));
        }
    } else {
        sprintf(dmesg_buf, "-i- User override for manual update \033[0;39;49m\r\n");
        UARTSend((uint8_t*)dmesg_buf, strlen(dmesg_buf));
    }

    /*************************************************
     * User app not started, Firmware update functions
     *************************************************/
    strncpy(dmesg_buf, "\033[1;36;49m-i- Initiate USB DFU Firmware update \r\n", 64);
    UARTSend((uint8_t*)dmesg_buf, strlen(dmesg_buf));

    timer_init();
    strncpy(dmesg_buf, "\033[1;36;49m-i- Timer module initialized \r\n", 64);
    UARTSend((uint8_t*)dmesg_buf, strlen(dmesg_buf));

    ....
}

test app

DFU-util toolkit

Freeware dfu-util is a host side implementation of the USB DFU 1.0 and DFU 1.1. Using dfu-util you can download firmware to your DFU-enabled device or upload firmware from it. Dfu-util 0.9 has been tested with the example in this guide.
It’s usage options are in Table 2, available by running program without any command line parameters.

Option key Verbose Description
-h —help Print this help message
-V —version Print the version number
-v —verbose Print verbose debug statements
-l —list List currently attached DFU capable devices
-e —detach Detach currently attached DFU capable devices
-E —detach-delay seconds Time to wait before reopening a device after detach
-d —device <vendor>:<product>[,<vendor_dfu>:<product_dfu>] Specify Vendor/Product ID(s) of DFU device
-p —path <bus-port. … .port> Specify path to DFU device
-c —cfg <config_nr> Specify the Configuration of DFU device
-i —intf <intf_nr> Specify the DFU Interface number
-S —serial <serial_string>[,<serial_string_dfu>] Specify Serial String of DFU device
-a —alt <alt> Specify the Altsetting of the DFU Interface by name or by number
-t —transfer-size <size> Specify the number of bytes per USB Transfer
-U —upload <file> Read firmware from device into <file>
-Z —upload-size <bytes> Specify the expected upload size in bytes
-D —download <file> Write firmware from <file> into device
-R —reset Issue USB Reset signalling once we’re finished
-s —dfuse-address <address> ST DfuSe mode, specify target address for raw file download or upload. Not applicable for DfuSe file (.dfu) downloads

Table 2: dfu-util 0.9 accepted parameter list

dfu-util package also comes with dfu-prefix.exe and dfu-suffix.exe programs to generate and check correct DFU data for firmwares.

First enter our MCU into DFU-bootloader mode and check if it’s detected properly.

\dfu-util.exe -l
...
Found Runtime: [dead:beef] ver=0100, devnum=4, cfg=1, intf=0, path="3-13", alt=0, name="UNKNOWN", serial="Controller 00"

Our device with VID:PID 0xDEAD/0xBEEF is detected correctly.

Test application

c:\EVGA\EPOWER5\fwtest\config\LPC11U68JBD64.icf

/*###ICF### Section handled by ICF editor, don't touch! ****/
/*-Editor annotation file-*/
/* IcfEditorFile="$TOOLKIT_DIR$\config\ide\IcfEditor\cortex_v1_0.xml" */
/*-Specials-*/
define symbol __ICFEDIT_intvec_start__ = 0x00003000;
/*-Memory Regions-*/
define symbol __ICFEDIT_region_ROM_start__ = 0x00003000;
define symbol __ICFEDIT_region_ROM_end__   = 0x0003FFFF - 0x3000;
define symbol __ICFEDIT_region_RAM_start__ = 0x10001000;
define symbol __ICFEDIT_region_RAM_end__   = 0x10007FDF;
/*-Sizes-*/
define symbol __ICFEDIT_size_cstack__ = 0x7F0;
define symbol __ICFEDIT_size_heap__   = 0xFF0;
/**** End of ICF editor section. ###ICF###*/

define symbol __CRP_start__    = 0x000032FC;
define symbol __CRP_end__      = 0x000032FF;

define symbol __RAM1_start__   = 0x20000000;
define symbol __RAM1_end__     = 0x200007FF;

define symbol __RAM_USB_start__= 0x20004000;
define symbol __RAM_USB_end__  = 0x200047FF;

define memory mem with size     = 4G;
define region ROM_region           = mem:[from __ICFEDIT_region_ROM_start__   to __ICFEDIT_region_ROM_end__];// -  mem:[from  __CRP_start__ to __CRP_end__];
define region RAM_region           = mem:[from __ICFEDIT_region_RAM_start__   to __ICFEDIT_region_RAM_end__];
define region RAM1_region          = mem:[from __RAM1_start__  to __RAM1_end__];
define region RAM_USB_region     = mem:[from __RAM_USB_start__  to __RAM_USB_end__];
define region CRP_region           = mem:[from  __CRP_start__ to __CRP_end__];

define block CSTACK    with alignment = 8, size = __ICFEDIT_size_cstack__   { };
define block HEAP   with alignment = 8, size = __ICFEDIT_size_heap__     { };

initialize by copy { readwrite };
do not initialize  { section .noinit };

place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };
place in ROM_region       { readonly };
place in RAM_region       { block HEAP, readwrite };
place in CRP_region       { section .crp };
place in RAM1_region      { section .sram1 };
/* section .sram_usb*/
place in RAM_USB_region    { section .sram_usb, block CSTACK };

icf file

Project settings

Final test

https://tessel.io/blog/66686276686/reverse-engineering-lpcs-device-firmware-upgrade

Write to VTOR from IAR ARM in LPC11U68:

#define VTOR_ADDR 0xE000ED08 unsigned int volatile * const vtor_reg = (unsigned int *) VTOR_ADDR; //The variable port is a constant pointer to a volatile unsigned integer, so we can access the memory-mapped register using: vtor_reg = 0×00003000; / write to port */ //value = port; / read from port */

Author: Ilya Tsemenko
Created: Oct. 7, 2016, 6:48 a.m.
Modified: Oct. 14, 2016, 3:11 a.m.

References