Skip to content

DFU update

fanoush edited this page Oct 15, 2019 · 25 revisions

WARNING!!!

Performing DFU update is risky and by flashing custom firmware you may make this $9.99 $7.99 $6.99 $5.99 $4.99 watch semi-bricked. With good luck it may be enough to drain the battery to perform reset but most probably you may need to open the watch and attach SWD debugger to restore it to working order. Do not attempt to flash anything if you are not willing to open the device in case something goes wrong. My suggestion is to get at least two devices and use one with front glass removed for testing and flash only verified code to unopened watch. It may also make sense to enable watchdog timer in your code so it reboots when your code hangs (see e.g. here).

About DS-D6 firmware

DS-D6 firmware is developed with Nordic SDK 11 and uses SoftDevice S132 v2.0.0. Memory layout is described here and Nordic bootloader details are here. Since Nordic bootloader sources are freely available as part of SDK the Desay bootloader is derived from this code (including bugs, see below).

0x00000-0x1BFFF - MBR and SoftDevice
0x1c000 main application (1.0.0 version ends at 0x47bd4)
0x78000 Desay DFU bootloader (ends at 0x7d464), UICR.NRFFW[0] points here
0x7e000 bootloader settings, UICR.NRFFW[1] points here

DFU package

If your app is developed with SDK11 and SoftDevice S132 v2.0.0 (=>starts at 0x1c000) you can package just your app without SoftDevice or bootloader and flash it via DFU update over serial or bluetooth and it should run. With newer Nordic SDKs (12 and up) this became more complicated since the bootloader can be secure and package must be signed via private key but we are lucky that with SDK 11 this is still easy and you can upload anything. The tool Nordic provides for creating packages and performing serial DFU update is called nrfutil. As described for legacy package format one needs 0.5.x version. Latest is 0.5.3 Either install from source or install via pip (I tried python2 version) sudo pip2 install nrfutil==0.5.3 Also if you have newer pip >=10 you may need to downgrade it first as described here if you see same error message sudo python2 -m pip install pip==9.0.3

To create zip package from D6-DS.hex file (memory locations 0x1c000-0x47bd4) use following command:

nrfutil dfu genpkg --application-version 65535 --application D6-DS.hex D6-DS.zip

Result also uploaded here.

Serial DFU procedure

To perform serial update reboot to DFU bootloader and run following command

nrfutil dfu serial -pkg D6-DS.zip -p /dev/ttyUSB0

where ttyUSB0 is TTL 3.3V serial port connected to GND and middle usb pins. DS-D6 connector pinout is:

color pin
black GND
green RX (connect TX here)
white TX (connect RX here)
red 5V

See also this photo. Default parameters are speed 38400 with no flow control. The output looks like this:

$ nrfutil dfu serial -pkg D6-DS.zip  -p /dev/ttyUSB0 
Upgrading target on /dev/ttyUSB0 with DFU package /home/pi/DS-D6/D6-DS.zip. Flow control is disabled.
  [####################################]  100%          
Device programmed.

First it goes quickly to 100% (package is checked?), then it takes some time. If you have usb-serial adapter with RX/TX LEDs, you should see some traffic all the time. If your adapter has no traffic LEDs please be patient and wait for 'device programmed' message.

If you don't see 'device programmed' but some timeout error instead then just re-run it again. Sometimes this happens but it eventually succeeds in the end. Also the quality of your usb to serial adapter may be the reason. I first tried with blackmagic probe which also includes serial port but this was constantly failing at various stages of upload so I had to use real adapter. Also if it fails with error "Serial port could not be opened" then check permissions, on some systems normal users do not belong to group (e.g. dialout) with rights to access serial port. In such case fix permissions/add user to group or try to run nrfutil with sudo.

To put device to DFU mode just try to run the serial procedure and it will probably switch to bootloader automatically but will timeout so you need to retry. Other way is to use following AT commands over bluetooth:

BT+UPGB:1
BT+RESET

The UPGB command writes value 1 to GPREGRET register and this is a flag for bootloader to enter DFU mode after reset. This is similar to how Nordic bootloader works too. If you implement your own firmware you should provide way to set this too so your app could be updated or original firmware could be restored. E.g. in Espruino it is possible to trigger this with poke32(0x4000051c,1).

Adafruit nrfutil fork

If you need both the latest nrfutil for newer Nordic SDKs and this old 0.5.3 you can also use adafruit fork of old 0.5.3 code ported to python3. You can use it together with latest nrfutil as long as you install original nrfutil in python2 and adafruit one in python3. It has different command name adafruit-nrfutil and different output when sending the update but it should work too with baud rate forced to 38400 (default is 115200 for this fork). https://github.com/adafruit/Adafruit_nRF52_nrfutil

Bluetooth DFU procedure

D6Flasher

There is now user friendly Android app for flashing custom firmware over bluetooth done by atc1441, see Google Play page here

desay_dfu.py - python flasher script for Linux/Bluez/gatttool

https://github.com/fanoush/ds-d6/blob/master/fwdump/README.md#desay_dfupy-flasher

Updating SoftDevice and Bootloader

nrfutil can also make package with just soft device or combined soft device and bootloader. bootloader is dependent on soft device version so you can update only minor versions of soft device without updating also bootloader.

It is possible to upgrade 2.0.0 SoftDevice to 2.0.1 to get latest bugfixes. Upgrade package is here

Package data is loaded into application space first so after update one needs to send application package again.

I also tried softdevice 3.0 with newer bootloader however I have verified that Desay bootloader cannot successfully flash such combined bootloader+softdevice package. Looks like some part of soft device is not updated correctly and result does not boot. While the bootloader itself is updated correctly the SoftDevice is not. Later I found that this is in fact known Nordic bug described here https://devzone.nordicsemi.com/f/nordic-q-a/16774/updating-from-s132-v2-0-x-to-s132-v3-0-0-with-dual-bank-bootloader-from-sdk-v11-0-0-does-not-work

I have binary patched the bug in existing bootloader (for details see below) and working bootloader is here. You first need to update existing bootloader with the -fixed- DFU zip and only then you can safely use combined packages with newer soft device and bootloader.

Fixing Desay bootloader

Below is relevant code in desay bootloader binary (decompiled by arm-none-eabi-objdump -D -bihex -marm ds-d6-bootloader.hex -Mforce-thumb > ds-d6-bootloader.S). Looks like wrong, unaligned block size is in register r4 so what we need is to replace this:

   7a7e8:       f5a2 5080       sub.w   r0, r2, #4096   ; 0x1000 ; r0 =boot_settings.sd_image_start - sd_start
   7a7ec:       0844            lsrs    r4, r0, #1 ;r4 = block_size = (boot_settings.sd_image_start - sd_start) / 2;

with this

   7a7e8:       f44f 4450       mov.w   r4, #53248      ; 0xd000
   7a7ec:       bf00            nop

I have uploaded DFU package for this fixed bootloader alone and also together with SoftDevice 2.0.1, check DS-D6-desay-bootloader-fix* zips in fwdump folder. I have also verified that this indeed successfully flashes combined package with SoftDevice 132v3.0.0 and SDK12 based Espruino bootloader.

Full listing of buggy dfu_sd_image_swap method:

<dfu_sd_image_swap>:
   7a7c8:       e92d 41f0       stmdb   sp!, {r4, r5, r6, r7, r8, lr}
   7a7cc:       b088            sub     sp, #32
   7a7ce:       4668            mov     r0, sp
bootloader_settings_get(&boot_settings);
   7a7d0:       f7ff fad8       bl      0x79d84
   7a7d4:       9803            ldr     r0, [sp, #12] ; r0=boot_settings.sd_image_size
if (boot_settings.sd_image_size == 0)
    {
        return NRF_SUCCESS;
    }
   7a7d6:       2800            cmp     r0, #0
   7a7d8:       d02b            beq.n   0x7a832
   if ((SOFTDEVICE_REGION_START + boot_settings.sd_image_size) > boot_settings.sd_image_start)
   7a7da:       9a06            ldr     r2, [sp, #24] ;r2=boot_settings.sd_image_start
   7a7dc:       f500 5080       add.w   r0, r0, #4096   ; 0x1000 ; +SOFTDEVICE_REGION_START
   7a7e0:       f44f 5180       mov.w   r1, #4096       ; 0x1000 ; r1=SOFTDEVICE_REGION_START
   7a7e4:       4290            cmp     r0, r2
   7a7e6:       d927            bls.n   0x7a838
        uint32_t sd_start        = SOFTDEVICE_REGION_START;
        uint32_t block_size      = (boot_settings.sd_image_start - sd_start) / 2;
        uint32_t image_end       = boot_settings.sd_image_start + boot_settings.sd_image_size;
        uint32_t img_block_start = boot_settings.sd_image_start + 2 * block_size;
        uint32_t sd_block_start  = sd_start + 2 * block_size;

   7a7e8:       f5a2 5080       sub.w   r0, r2, #4096   ; 0x1000 ; r0 =boot_settings.sd_image_start - sd_start
   7a7ec:       0844            lsrs    r4, r0, #1 ;r4 = block_size = (boot_settings.sd_image_start - sd_start) / 2;
   7a7ee:       4610            mov     r0, r2 ; boot_settings.sd_image_start
   7a7f0:       9a03            ldr     r2, [sp, #12]; boot_settings.sd_image_size
   7a7f2:       eb00 0644       add.w   r6, r0, r4, lsl #1	;r6 = img_block_start = boot_settings.sd_image_start + 2 * block_size;
   7a7f6:       1887            adds    r7, r0, r2 		;r7= image_end       = boot_settings.sd_image_start + boot_settings.sd_image_size;
       if (SD_SIZE_GET(MBR_SIZE) < boot_settings.sd_image_size)
        {
    7a7f8:       f44f 5040       mov.w   r0, #12288      ; 0x3000
   7a7fc:       460d            mov     r5, r1 ; r5=sd_start = SOFTDEVICE_REGION_START;
   7a7fe:       6880            ldr     r0, [r0, #8]	;r0=SD_SIZE_GET(MBR_SIZE)
   7a800:       eb01 0844       add.w   r8, r1, r4, lsl #1	;r8 = sd_block_start  = sd_start + 2 * block_size;
   7a804:       4290            cmp     r0, r2
   7a806:       d20e            bcs.n   0x7a826
           err_code = dfu_copy_sd((uint32_t *)(sd_start + block_size),
                                   (uint32_t *)(sd_start + block_size),
                                   sizeof(uint32_t));
   7a808:       f504 5080       add.w   r0, r4, #4096   ; 0x1000
   7a80c:       2204            movs    r2, #4
   7a80e:       4601            mov     r1, r0
   7a810:       f7ff fd73       bl      0x7a2fa
            VERIFY_SUCCESS(err_code);
   7a814:       2800            cmp     r0, #0
   7a816:       d10c            bne.n   0x7a832
           err_code = dfu_copy_sd((uint32_t *)sd_start, (uint32_t *)sd_start, sizeof(uint32_t));
    7a818:       4629            mov     r1, r5
   7a81a:       2204            movs    r2, #4
   7a81c:       4608            mov     r0, r1
   7a81e:       f7ff fd6c       bl      0x7a2fa
           VERIFY_SUCCESS(err_code);
   7a822:       2800            cmp     r0, #0
   7a824:       d105            bne.n   0x7a832
        return dfu_sd_img_block_swap((uint32_t *)img_block_start,
                                     (uint32_t *)sd_block_start,
                                     image_end - img_block_start,
                                     block_size);
   7a826:       1bba            subs    r2, r7, r6
   7a828:       4623            mov     r3, r4
   7a82a:       4641            mov     r1, r8
   7a82c:       4630            mov     r0, r6
   7a82e:       f000 f837       bl      0x7a8a0
   7a832:       b008            add     sp, #32
   7a834:       e8bd 81f0       ldmia.w sp!, {r4, r5, r6, r7, r8, pc}
else
           return dfu_copy_sd((uint32_t *)boot_settings.sd_image_start,
                               (uint32_t *)SOFTDEVICE_REGION_START,
                               boot_settings.sd_image_size);
   7a838:       9a03            ldr     r2, [sp, #12]
   7a83a:       9806            ldr     r0, [sp, #24]
   7a83c:       f7ff fd5d       bl      0x7a2fa
   7a840:       e7f7            b.n     0x7a832


dfu_copy_sd:
   7a2fa:       b51f            push    {r0, r1, r2, r3, r4, lr}
   7a2fc:       2301            movs    r3, #1
   7a2fe:       9300            str     r3, [sp, #0]
   7a300:       e9cd 0101       strd    r0, r1, [sp, #4]
   7a304:       0890            lsrs    r0, r2, #2
   7a306:       9003            str     r0, [sp, #12]
   7a308:       4668            mov     r0, sp
   7a30a:       df18            svc     24
   7a30c:       b004            add     sp, #16
   7a30e:       bd10            pop     {r4, pc}

Additionally, there is almost the same code in the method dfu_sd_image_validate() which is run when something fails and softdevice update is interrupted, this will continue and try again to finish the copy. Here needs to be replaced this:

        0007a860 a2 f5 80 50     sub.w      r0,r2,#0x1000
        0007a864 43 08           lsr        r3,r0,#0x1

with this

        0007a860 4f f4 50 43     mov.w      r3,#0xd000
        0007a864 00 bf           nop