For the Arduino Portenta H7.
The Story So Far…
As described in Part 1 of our series, we are designing a drone using the new Arduino Portenta H7 as a flight controller. Our intention is to port BetaFlight across to the STM32H747 microprocessor as the flight controller firmware.
We have modified the BetaFlight firmware so that it compiles for the H7. The next step is to load the firmware into memory and tell the processor where the first instruction is located.
Normally you can just use BetaFlight Configurator for this purpose but the Portenta has a custom Arduino bootloader. The Portenta bootloader is accessed by tapping the reset button twice. The issue we have is that the custom Arduino bootloader is located at the same memory address as the BetaFlight ISR vector table. If you overwrite the custom bootloader you will brick your Portenta. Theoretically, you should be able to revive the board using the embedded bootloader (which is selected by connecting the BOOT pin to VCC during reset), but we were unable to do this.
Memory Considerations in the Portenta
Our Portenta is configured with:
- 8MB SDRAM
- 16MB NOR Flash
At startup, the boot memory space is selected by the BOOT pin and the BOOT_ADDx option bytes (Figure 2), allowing us to program in any boot memory address from 0x0000 0000 to 0x3FFF FFFF which includes flash memory, SRAM and the system memory (bootloader). The default boot addresses are:
- Boot address 0: Flash memory at 0x0800 0000 (custom bootloader)
- Boot address 1: ITCM-RAM at 0x1FF0 0000 (embedded bootloader)
If the programmed boot memory address is out of the memory mapped area, or in a reserved area (Figure 3), then the default boot fetch address become (ref: RM0399 STM32H747 Reference Manual):
Cortex®-M7 Boot address 0: FLASH at 0x0800 0000Cortex®-M7 Boot address 1: ITCM-RAM at 0x0000 0000Cortex®-M4 Boot address 0: FLASH at 0x0810 0000Cortex®-M4 Boot address 1: SRAM1 at 0x1000 0000
The overall Flash memory architecture is summarized in Figure 4.
User and system memories are used differently according to whether the microcontroller is configured by the application software in Standard mode or in Secure access mode (Figure 5). This selection is done through the SECURITY option bit.
In Standard mode, the user memory contains the application code and data, while the system memory is loaded with the STM32 bootloader. When a reset occurs, the executing core jumps to the boot address configured through the BOOT pin and the BOOT_CMx_ADD0/1 option bytes.
You need to get your board/MCU into DFU mode in order to flash the firmware. DFU stands for Device Firmware Upgrade and is a vendor and device independent mechanism for upgrading the firmware of devices. For the H747 this can be done via the USB Port but this isn’t the only way.
You get into DFU mode via the bootloader. Once boot mode is entered and the STM32 microcontroller has been configured, the bootloader configures the USB and its interrupts, and waits for the “enumeration done” interrupt. The USB enumeration is performed as soon as the USB cable is plugged (or immediately if the cable is already plugged). If you don’t want the STM32 to enter DFU mode, the USB cable has to be unplugged before reset.
Once in DFU mode, the bootloader handles the DFU requests (Figure 6). This is an important point which we didn’t fully appreciate until we managed to brick our Portenta.
Thus, we need to load BetaFlight into Flash memory somewhere after where the custom bootloader code ends. An easy way to determine a safe location is to have a look where the Arduino IDE loads sketches. If you compile and upload from the IDE in verbose mode you will see the command:
dfu-util — device 0x2341:0x035b -D /PortentaM7RedBlink.ino.bin -a0 — dfuse-address=0x08040000:leave
Based on this, the address we need to upload BetaFlight to is 0x0804 0000.
At this stage, rather than modify BetaFlight configurator we are going to use dfu-util (like the Arduino IDE does), to upload BetaFlight.
dfu-util is a host application which you run on your PC. It comes in all the major OS flavours (Windows, OS X and Linux).
Let’s have a look at the option flags that the Arduino IDE is using in order to replicate this for BetaFlight.
-d, — device [DFU Mode VENDOR]:[DFU Mode PRODUCT] -> Specifies the USB VENDOR and PRODUCT ID’s. Arduino devices have a proprietary USB VID (Vendor ID) and PID (Product ID) — see Figure 7. Envie was the name used during development of the Portenta H7. If you only have one standards-compliant DFU device attached to your computer, this parameter is optional. However, as soon as you have multiple DFU devices connected, dfu-util will detect this and abort, asking you to specify which device to use.
-D, — download FILE -> Write firmware from FILE into device. In the example above the FILE is a .bin (binary file), while our compiled version of BetaFlight is currently .hex (hexadecimal). Intel Hex format is a standard layout for files produced by assemblers or C compilers and is the format used for BetaFlight Configurator. For dfu-util we need a binary file. We can build a binary version of the firmware by moving to the GitHub BetaFlight repository for the Portenta and using the following commands.
make TARGET=PORTENTA_H7 clean
make TARGET=PORTENTA_H7 binary
The result will look something like Figure 8.
-a, — alt ALT -> Specify the alt setting of the DFU interface by name or by number. You can use dfu-util -l to find out what the alt setting should be. For our Portenta we see (Figure 9), alt=0 is Internal Flash and alt=1 is External Flash. We will upload to internal flash.
-s, — dfuse-address ADDRESS -> This specifies the target address for binary download/upload on DfuSe devices. Do not use this for downloading DfuSe (.dfu) files. Modifiers can be added to the address, separated by a colon, to perform special DfuSE commands such as “leave” DFU mode, “unprotect” and “mass-erase” flash memory.
DfuSe (DFU with ST Microsystems extensions) is a protocol based on DFU 1.1. However, in expanding the functionality of the DFU protocol, ST Microsystems broke all compatibility with the DFU 1.1 standard. DfuSe devices report the DFU version as “1.1a”.
The main difference from standard DFU is that the target address in the device (flash) memory is specified by the host, so that a download can be performed to parts of the device memory. The host program is also responsible for erasing flash pages before they are written to.
Some DfuSe devices (like the Portenta H7) have their DfuSe bootloader running from flash memory. Erasing the whole flash memory would therefore destroy the DfuSe bootloader itself and practically brick the device for most users (yep).
Well-written bootloaders running from flash will report their own memory region as read-only and not eraseable, but this does not prevent dfu-util from sending a “unprotect” or “mass-erase” request which overrides this.
Changes Required in BetaFlight
Initially we thought that we would need to move the vector table, but it turns out that this isn’t necessary. The Arduino R&D team tells us that you can safely move your application to 0x8040000 as the Arduino bootloader takes care of moving vtor before jumping to the application. This is common practice and there’s really nothing strange about having multiple vector tables for different pieces of code. The bootloader will use its own until it jumps at which point it’ll switch to application’s one.
You have to be careful where you place the firmware, because the bootloader will only consider an application valid if the stack is located at one of the following addresses:
int app_valid = (((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD) & 0xFF000000) == 0x20000000)|| (((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD) & 0xFF000000) == 0x24000000)|| (((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD) & 0xFF000000) == 0x30000000)|| (((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD) & 0xFF000000) == 0x38000000);
To move BetaFlight (BF) and keep the Arduino boot loader, the firmware address must be changed to what the Arduino boot loader expects (i.e. 0x08040000). In BF, the program link address is done in the loader definition file, which is located in src/link. We think we can use stm32_flash_h743_2m.ld as a starting point and shift each segment to the new location. There is a MEMORY section at the top, and we have shifted the FLASH related segments backward by 0x40000.
The updated FLASH memory map will then be as shown in Figure 11.
You can determine the used values in Figure 11 when you compile and link the source code (Figure 12). We are using 80% of FLASH1 so adding a lot more functionality may be tricky without overwriting the Arduino M4 bootloader.
Flashing BetaFlight using dfu-util
In a section above we analysed the dfu-util command which the Arduino IDE uses to flash the Portenta. We are going to use something very similar. You need to be in the obj folder or add the path details.
dfu-util — device 0x2341:0x035b -D betaflight_4.3.0_PORTENTA_H7_8bffde48e.bin -a0 — dfuse-address=0x08040000:leave
Appendix 1. Moving the Vector Table
Should you ever need to move the vector table, this is fairly easy to achieve.
The STM32 startup code calls SystemInit() before main(). SystemInit() sets the SCB->VTOR value (among other things). SystemInit() is implemented in the vendor provided file: system_stm32h7xx.c.
In src/main/startup/system_stm32h7xx.c, there is a vector table offset (VECT_TAB_OFFSET) which we can use to move the vector table. Assuming the Arduino bootloader needs the vector table at its original location, we can use a preprocessor directive like:
#define VECT_TAB_OFFSET 0x00 /*!< Vector Table base offset field.
This value must be a multiple of 0x200. */
#define VECT_TAB_OFFSET 0x40000