KiRorY
STM32G0系OTA开发相关问题的解决方案

Solutions to STM32G0 OTA Development Issues

During internship OTA development on an STM32G0 board, I ran into several issues and summarized the fixes, mainly around Bootloader jump logic and APP partition strategy.

  • The system hangs after Bootloader jumps to the APP region.
  • With dual APP partitions, Bootloader can jump to only one partition instead of both.

System Hangs After Bootloader Jump

First, let's briefly explain the mechanism of jumping to an application. It involves setting the PC pointer position and the stack pointer position. The address the PC pointer points to should be \([\text{program flash address}] + 4\). For development boards with the CBT6 suffix, you can refer to the following HAL library code:
1
2
3
4
5
typedef void (*load)(void);

__set_MSP(*(uint32_t*)addr); // addr is the jump destination address, i.e., the APP flashing address
load = (load)* (uint32_t*)(addr + 4);
load();

Of course, we can directly use the code above to perform the jump, provided that the address pointed to by addr exists in RAM. In actual execution, it was found that the program successfully jumped and ran, but it would hang somewhere and fail to proceed. The reason is simple: we redirected the PC pointer and the stack pointer, but did not relocate its interrupt vector pointer. As a result, when the APP encounters an interrupt function during execution, it jumps back to the Bootloader's interrupt vector table, naturally causing the program pointer to run astray. By default, the program's interrupt vector table pointer points to 0x0, but in reality, the starting position of the corresponding interrupt vector table for a program is the program's starting address. Refer to the diagram below: Diagram

Solution

In Cortex-M series chips, except for the M0, all chips retain the VTOR register for relocating the interrupt vector table. The G0 series development board uses an M0+ core, which supports this register. Therefore, before jumping to the program, simply assign the corresponding address to the VTOR register. Based on the diagram above, the vector table address is the start address of the program. The complete jump function code is as follows:
1
2
3
4
5
6
7
8
9
10
typedef void (*load)(void);
void jump(uint32_t addr){
if((*(uint32_t*)addr >= 0x20000000) && (*(uint32_t*)addr < 0x20000000 + ram_size)){ // 0x20000000 here is the base address of RAM; this checks if the jump target data is in RAM
__set_MSP(*(uint32_t*)addr);
load = (load)* (uint32_t*)(addr + 4);

SCB->VTOR = addr; // Newly added line
load();
}
}

In other projects, I have also encountered situations where OTA had to be implemented on M0 series chips. When registers cannot be configured directly, it becomes slightly more troublesome: we need to manually relocate the interrupt vector table. The general idea is to copy the entire interrupt vector table into RAM before jumping, and then remap the vector table pointer address to the base address of RAM (mapped to 0x20000000 according to the example above). A remap function in the corresponding STM32 library is typically used here. Unfortunately, because the board I developed did not use the STM standard library and lacked the corresponding function, I have not yet found another way to implement this feature. If needed, you can look up relevant documentation on implementing Bootloaders for the M0 series; since I have not verified it in practice, I will not discuss it further here.

Bootloader Can Only Jump to One Partition in a Dual-APP Setup

A dual-APP partition setup was the first method I conceived to achieve OTA without an external EEPROM, dividing the flash into Bootloader + Flag + Region A/B. The OTA update logic goes as follows: 1. The program enters Region A by default on startup; 2. An OTA request occurs, and the new program is written to Region B; 3. Once writing succeeds, the flag corresponding to the address in the Flag region is set to valid; 4. Force the program into an infinite loop and let the watchdog reset the system; 5. After the Bootloader verifies the program, it jumps to Region B to execute, while Region A becomes the new download region.

The advantage of this approach is that it always ensures one partition contains executable code. Even if OTA fails or the write process is forcibly interrupted, there remains a valid partition to run. In addition, it makes effective use of program flash space, reduces the erase/write cycles for a single region, and prolongs flash lifespan.

However, the above approach overlooks the issue of program address pointers. After compiling with various toolchains, the resulting binary executable actually retains information such as the program's starting address. This address information is generally located at the beginning of the bin file (which is also the interrupt vector table address mentioned in the previous issue). When hardware executes the bin program, it first reads this address information and jumps to the corresponding location to execute. In fact, if we configure different flash addresses for the same program, the generated bin files will be different. The comparison below illustrates this clearly: bin file comparison From the differences between the partitioned data segments, the difference in the header data segments is obvious. If we simply flash the exact same binary (compiled for a specific flash region) into different regions, even if the Bootloader jumps to the correct address, interrupts will jump back to the original region when the program reads its initial header fields. This means that when compiling a new program, we must know the target flash address beforehand; otherwise, partition jump errors will occur. This is very inconvenient for subsequent OTA updates, so this method cannot be used directly.

Solutions

1. Modifying Header Fields in the Program [Not Reusable]

This method involves modifying the header fields of the pending OTA program within the code based on the OTA target address stored in the flag region. As seen from the comparison data in the image above, the header fields repeat a certain field value within a specific range. This value is roughly \(\text{program flashing target address} + \text{a certain offset}\). However, I haven't found a fixed pattern for this offset yet, as it seems to vary across different programs. Given that projects should eliminate such unstable factors as much as possible, this solution was temporarily abandoned.

2. Revised Solution: Application Region + Download Region

In short, the goal is to avoid jumping to different regions and instead fix the startup jump to a single region whenever possible. Region A is configured as the APP region. When OTA is performed, the program is downloaded into Region B, and the OTA flag along with CRC checksum information are written to the flag region, followed by a watchdog reset. The Bootloader program verifies the validity of the program in Region B, copies it to Region A for execution, and then resets the flag region.

Of course, this solution comes with operational stability trade-offs: Bootloader startup latency increases when OTA is required, and ensuring the integrity of the copied program is challenging. Therefore, it is best to use an external EEPROM or flash to handle temporary storage of OTA binaries.

Author:KiRorY
Permalink:https://kirory.xyz/posts/11/
License:This work is licensed under CC BY-NC-SA 3.0 CN