SAM D21 - Firmware upgrade over i2c

Arduino has a way to do this. the SDU and the SFU library show it

I had to write a batch file to build my sketch with '.text' starting at 0x3a000:

@echo off
set uuid=D31097C5B3C3230B1B8AC2B5E2D38822
set path_arduino=C:\\Users\\admin\\AppData\\Local\\Arduino15\\packages\\arduino
set path_tmp=C:\\Users\\admin\\AppData\\Local\\Temp\\arduino\\sketches\\%uuid%

echo path_arduino = %path_arduino%
echo path_tmp     = %path_tmp%
pause

%path_arduino%\\tools\\arm-none-eabi-gcc\\7-2017q4\\bin\\arm-none-eabi-g++ ^
 -o ArduinoBC43_UpdaterSketch_moved.ino.elf ^
 "%path_tmp%\\sketch\\ArduinoBC43_UpdaterSketch.ino.cpp.o" ^
 "%path_tmp%\\libraries\\Wire\\Wire.cpp.o" ^
 "%path_tmp%\\core\\variant.cpp.o" ^
 "-L%path_tmp%" ^
 "-T%path_arduino%\\hardware\\samd\\1.8.13\\variants\arduino_zero\linker_scripts\gcc\flash_with_bootloader.ld" ^
 -Os ^
 -Wl,--gc-sections -save-temps "-Wl,-Map,%path_tmp%\\ArduinoBC43_UpdaterSketch.ino.map" ^
 --specs=nano.specs --specs=nosys.specs -mcpu=cortex-m0plus -mthumb -Wl,--cref -Wl,--check-sections -Wl,--gc-sections ^
 -Wl,--unresolved-symbols=report-all -Wl,--warn-common -Wl,--warn-section-align ^
 -Wl,--start-group ^
 "-L%path_arduino%\\tools\\CMSIS\\4.5.0\CMSIS\Lib\GCC" ^
 -larm_cortexM0l_math -lm ^
 "%path_tmp%\\..\\..\\cores\\arduino_samd_arduino_zero_edbg_27e9adf56847ad6d10a7b91f750de76b\\core.a" ^
 -Wl,--end-group -Wl,-section-start=.text=0x3a000

"%path_arduino%\\tools\\arm-none-eabi-gcc\\7-2017q4\bin\arm-none-eabi-objcopy" -O binary ArduinoBC43_UpdaterSketch_moved.ino.elf ArduinoBC43_UpdaterSketch_moved.ino.bin

"%path_arduino%\\tools\\arm-none-eabi-gcc\\7-2017q4\bin\arm-none-eabi-objdump" -D ArduinoBC43_UpdaterSketch_moved.ino.elf | head -512 >> assembler_moved.txt

I need to execute the function 'WriteFlashPage' from SRAM, and so I've renamed it to 'WriteFlashPage_detail', and then I wrote a wrapper function around it that copies the machine code from Flash into SRAM. The machine code is only about 150 bytes but I copy an entire kilobyte:

void WriteFlashPage(long unsigned const addr, char unsigned const (&bytes)[64u])
{
    char unsigned const volatile *const pFlash = (char unsigned const volatile*)&WriteFlashPage_detail;
    cout << "WriteFlashPage_detail resides in Flash at address 0x" << (long unsigned)pFlash << endl;
    static alignas(long double) char unsigned volatile func_in_ram[1024u];
    cout << "Now copying 1 kilobyte of machine code from 0x" << hex << (long unsigned)pFlash << " to 0x" << hex << (long unsigned)&func_in_ram << endl;
    for ( unsigned i = 0u; i < 1024u; ++i ) func_in_ram[i] = pFlash[i];
    void (*const f)(long unsigned, char unsigned const(&)[64u]) = (void (*)(long unsigned, char unsigned const(&)[64u]))&func_in_ram;
    cout << "About to invoke 'WriteFlashPage_detail' which resides in SRAM at address 0x" << hex << (long unsigned)f << endl;
    delay(1000u);  // Don't disable interrupts before calling 'delay' as the millisecond counter is incremented inside an interrupt routine!
    __disable_irq();
    f(addr, bytes);
    __enable_irq();
}

The assembler for 'WriteFlashPage_detail' seems to be position-independent:

0000212c <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])>:
    212c:	4a1f      	ldr	r2, [pc, #124]	; (21ac <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x80>)
    212e:	b5f0      	push	{r4, r5, r6, r7, lr}
    2130:	0844      	lsrs	r4, r0, #1
    2132:	61d4      	str	r4, [r2, #28]
    2134:	4c1e      	ldr	r4, [pc, #120]	; (21b0 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x84>)
    2136:	8014      	strh	r4, [r2, #0]
    2138:	7d14      	ldrb	r4, [r2, #20]
    213a:	07e4      	lsls	r4, r4, #31
    213c:	d5fc      	bpl.n	2138 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0xc>
    213e:	2480      	movs	r4, #128	; 0x80
    2140:	6855      	ldr	r5, [r2, #4]
    2142:	4f1c      	ldr	r7, [pc, #112]	; (21b4 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x88>)
    2144:	432c      	orrs	r4, r5
    2146:	2510      	movs	r5, #16
    2148:	6054      	str	r4, [r2, #4]
    214a:	4c1b      	ldr	r4, [pc, #108]	; (21b8 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x8c>)
    214c:	8014      	strh	r4, [r2, #0]
    214e:	7d14      	ldrb	r4, [r2, #20]
    2150:	07e4      	lsls	r4, r4, #31
    2152:	d5fc      	bpl.n	214e <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x22>
    2154:	2440      	movs	r4, #64	; 0x40
    2156:	46a4      	mov	ip, r4
    2158:	4484      	add	ip, r0
    215a:	26ff      	movs	r6, #255	; 0xff
    215c:	780c      	ldrb	r4, [r1, #0]
    215e:	43b3      	bics	r3, r6
    2160:	4323      	orrs	r3, r4
    2162:	4e16      	ldr	r6, [pc, #88]	; (21bc <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x90>)
    2164:	784c      	ldrb	r4, [r1, #1]
    2166:	4033      	ands	r3, r6
    2168:	0224      	lsls	r4, r4, #8
    216a:	4323      	orrs	r3, r4
    216c:	403b      	ands	r3, r7
    216e:	001c      	movs	r4, r3
    2170:	788e      	ldrb	r6, [r1, #2]
    2172:	78cb      	ldrb	r3, [r1, #3]
    2174:	0436      	lsls	r6, r6, #16
    2176:	b2a4      	uxth	r4, r4
    2178:	061b      	lsls	r3, r3, #24
    217a:	4334      	orrs	r4, r6
    217c:	4323      	orrs	r3, r4
    217e:	c008      	stmia	r0!, {r3}
    2180:	3104      	adds	r1, #4
    2182:	3d01      	subs	r5, #1
    2184:	4584      	cmp	ip, r0
    2186:	d001      	beq.n	218c <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x60>
    2188:	2d00      	cmp	r5, #0
    218a:	d1e6      	bne.n	215a <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x2e>
    218c:	4c0c      	ldr	r4, [pc, #48]	; (21c0 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x94>)
    218e:	8014      	strh	r4, [r2, #0]
    2190:	7d14      	ldrb	r4, [r2, #20]
    2192:	07e4      	lsls	r4, r4, #31
    2194:	d5fc      	bpl.n	2190 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x64>
    2196:	2d00      	cmp	r5, #0
    2198:	d1d7      	bne.n	214a <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x1e>
    219a:	4b0a      	ldr	r3, [pc, #40]	; (21c4 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x98>)
    219c:	3b01      	subs	r3, #1
    219e:	2b00      	cmp	r3, #0
    21a0:	d1fc      	bne.n	219c <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x70>
    21a2:	4b08      	ldr	r3, [pc, #32]	; (21c4 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x98>)
    21a4:	3b01      	subs	r3, #1
    21a6:	2b00      	cmp	r3, #0
    21a8:	d1fc      	bne.n	21a4 <WriteFlashPage_detail(unsigned long, unsigned char const (&) [64])+0x78>
    21aa:	bdf0      	pop	{r4, r5, r6, r7, pc}
    21ac:	41004000 	mrsmi	r4, (UNDEF: 0)
    21b0:	ffffa502 			; <UNDEFINED> instruction: 0xffffa502
    21b4:	ff00ffff 			; <UNDEFINED> instruction: 0xff00ffff
    21b8:	ffffa544 			; <UNDEFINED> instruction: 0xffffa544
    21bc:	ffff00ff 			; <UNDEFINED> instruction: 0xffff00ff
    21c0:	ffffa504 			; <UNDEFINED> instruction: 0xffffa504
    21c4:	0001e848 	andeq	lr, r1, r8, asr #16

This all looks fine to me, but my program freezes when I invoke the function pointer named 'f' inside the function 'WriteFlashPage'. Can anyone see what I'm doing wrong?

Oh crap I just realised now that the assembler for WriteFlashPage_detail contains hard-coded addresses such as 0x2138 -- so I need to somehow compile that function as position-independent code.

By the way I've tried using the 'ramfunc' attribute as follows:

void WriteFlashPage_detail(long unsigned const addr, char unsigned const (&bytes)[64u])
__attribute__((flatten)) __attribute__((section(".ramfunc")));

but it doesn't work. If it worked properly then the address of the function would be greater than 0x200000 because that's where SRAM starts -- but its address is 0x1066d, and that's in the Flash.

So 'ramfunc' doesn't work for me with the SAM D21 in Arduino IDE.

When I get home later I'll try compile with -fPIC and then memcpy the function from Flash into SRAM.

I've already 'flattened' the function -- it doesn't contain any function calls.

If I can't get it to compile with -fPIC then I'll ask ChatGPT to convert all the absolute jumps to relative jumps, then use inline assembler to put the assembler in my sketch.

To be honest though, even with the absolute jumps, the function in SRAM should simply jump to its copy residing in Flash, so I don't know why my program is freezing.

Those are relative jumps; the disassembler is just doing the calculations for you.
In fact, I think all of the "label-based" CM0+ branch instructions are PC-relative. To do an absolute jump, you have to load your 32bit address as a constant, into a register, and then do a register-based branch (BX or BLX)
The range of conditional branches (bpl.n, etc) is pretty small on a CM0+ (+/-2k)
The subroutine call (BL) has a much more substantial range (+/-16M), but I don't think it will get you from flash to RAM.

Of course, if you copy code to a new memory location, any PC-relative branch that jumps outside of the code that you've copied will fail.

You don't think it's possible to execute a function in RAM as follows?

void (*f)(void) = (void (*)(void))0x260000;
f();

You reckon that the caller function (located in Flash), might fail to do a 'BL' instruction to an SRAM address?

That should work fine. The C compiler is smart enough to use the register form of branch when needed.

Okay let me tell you where I'm at now.

I downloaded from Github the C++ source code for the bootloader for the SAM D21 for the Arduino Zero board. I built that code to a binary. I then used Microchip Studio to upload this bootloader to my chip.

Next I made an edit to the bootloader C++ code, changing the jump from 0x2004 to 0x3a004, and then I built a new bootloader binary.

I compared these two binaries in a hex editor, and was able to clearly see which bytes were changed from:

00 20 00 00

to:

00 a0 03 00

So then I was able to write a function in C++ in my Arduino sketch which would changes these specific bytes in the bootloader.

I used the command line program 'openocd' to upload the two sketches to my chip (one sketch at 0x2000, and another sketch at 0x3a004).

So now I have a SAM D21 with the following on it:

  • A bootloader at 0x00000 configured to jump to 0x2000
  • My main sketch at 0x2000
  • My 'Uploader Sketch' at 0x3a000

So if I then power my chip on, I can see in the Serial Monitor:

Hello from the main sketch every 2 seconds

I then send a message over i2c to the chip telling it to change the bytes in the bootloader so that it will jump to 0x3a004 instead of 0x02000. I then cut the power to my chip and then re-apply the power, and next I see:

Hello from the Updater Sketch every 2 seconds

so I'm making good progress, I nearly have this working the way I need it.

I want to be able to send a message over i2c to my chip telling it to reboot (so that I don't have to cut the power and re-apply the power). I have tried three strategies to get this working. First thing I do is disable all interrupts and then clear all pending interrupts:

cout << "\n==== Rebooting ====\n";
delay(1000u);
__disable_irq();
// Next line should clear pending interrupts
for ( uint32_t IRQn = 0u; IRQn < 28u; ++IRQn ) NVIC->ICPR[0] = (1 << (IRQn & 0x1F));

Here's the 1st strategy using the watchdog timer. I've tried this with interrupts disabled and also with interrupts enabled, and it doesn't work:

WDT->CTRL.reg = 0;                        // disable watchdog
while ( 1 == WDT->STATUS.bit.SYNCBUSY );  // Just wait till WDT is free
WDT->CONFIG.reg = 2;                      // Timeout Period (valid values 0-11)
WDT->CTRL.reg = WDT_CTRL_ENABLE;          // enable watchdog
while ( 1 == WDT->STATUS.bit.SYNCBUSY );  // Just wait till WDT is free

Strategy No. 2 was simply just to jump to 0x00000 as follows:

#define APP_START 0x00000000 
uint32_t app_start_address = *(uint32_t *)(APP_START + 4);
 /* Rebase the Stack Pointer */
__set_MSP(*(uint32_t*)APP_START); 
/* Rebase the vector table base address */ 
SCB->VTOR = ((uint32_t)APP_START & SCB_VTOR_TBLOFF_Msk); 
/* Jump to application Reset Handler in the application */ 
asm("bx %0"::"r"(app_start_address));

And then the third strategy I tried was:

#define SCB_AIRCR_PRIGROUP_Pos              8                                             /*!< SCB AIRCR: PRIGROUP Position */
#define SCB_AIRCR_PRIGROUP_Msk             (7UL << SCB_AIRCR_PRIGROUP_Pos)                /*!< SCB AIRCR: PRIGROUP Mask */
__DSB();             /* Ensure all outstanding memory accesses included
                        buffered write are completed before reset */
SCB->AIRCR  = ((0x5FA << SCB_AIRCR_VECTKEY_Pos)      |
               (SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) |
                SCB_AIRCR_SYSRESETREQ_Msk);   /* Keep priority group unchanged */
 __DSB();                                     /* Ensure completion of memory access */
 //while(1);                                  /* wait until reset */

All three of these strategies just make the microcontroller freeze -- it doesn't reboot. I have to cut the power and re-apply the power to get it to reboot. Does anyone know if it's possible to eradicate the need for this power cut?

I don't know if this gives any clue, but when I use 'openocd' to upload the 'Updater Sketch' to address 0x3a004, I get:

** Programming Finished **
** Verify Started **
verified 18932 bytes in 1.568529s (11.787 KiB/s)
** Verified OK **
** Resetting Target **
target halted due to breakpoint, current mode: Handler HardFault
xPSR: 0x81000003 pc: 0x00000480 msp: 0x20007ba8
shutdown command invoked

And then I need to power-cycle the microcontroller. So what's the problem here? Do I need to reset a breakpoint or reset a hardware fault or something?

reset of any ARM is NVIC_SystemReset();

so I try it with an "I am an expert listen to me". I am the author of ArduinoOTA library for classic AVR Arduino with more than 64 kB flash, Arduino Uno R4, nRF5, RP2040 and the STM32F family. The ArduinoOTA library inherited SAMD support from the WiFi101OTA library. Additionally I adopted the SAMD SDU libray for Arduino M0, created an SDU library for nRF51 and the SDU library for Uno R4.

And I say that the best way for you is to create a second stage bootloader like the SDU library.

I have the firmware upgrade over i2c working now, with the new firmware being sent from a desktop PC program:

The desktop PC has an RS232 connection to a Texas Instruments microcontroller, which has an i2c connection to the Arduino, and so the desktop PC sends one page (i.e. 64 bytes) per packet out over the RS232 line, and it makes its way to the Arduino.

I just need to figure out now how to reset the Arduino without power-cycling it. It's actually not that big a deal if it needs to be power-cycled, but it would be convenient if I could find a way of resetting the SAM D21 from inside my Sketch. By the way @Juraj , I found the source code for 'NVIC_SystemReset':

/** \brief  System Reset

    This function initiate a system reset request to reset the MCU.
 */
static __INLINE void NVIC_SystemReset(void)
{
  __DSB();                                                     /* Ensure all outstanding memory accesses included
                                                                  buffered write are completed before reset */
  SCB->AIRCR  = ((0x5FA << SCB_AIRCR_VECTKEY_Pos)      |
                 SCB_AIRCR_SYSRESETREQ_Msk);
  __DSB();                                                     /* Ensure completion of memory access */
  while(1);                                                    /* wait until reset */
}

That's the same code I showed as my 'third strategy' a few posts above this one.

I have a quick question about writing to the Flash in the SAM D21. My understanding is as follows:

  • Each page is 64 bytes
  • You must erase a page before writing to it
  • You can't erase one page on its own -- you need to erase 4 pages

Does that sound right? So if you need to write to one page, then you need to save the three pages beside in SRAM before erasing the 4 pages at once, and then you have to write all four pages, right? Have I got that right?

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.