Introduction
Welcome back! In our last lesson, you successfully built a complete UART bootloader, a significant step toward creating field-updatable embedded systems. However, that bootloader had a critical weakness: it blindly trusted the binary file it received. A single bit flipped during transmission or a corrupted flash sector could cause the bootloader to jump to invalid code, potentially "bricking" the device.
This lesson addresses that vulnerability directly. We will implement a Cyclic Redundancy Check (CRC) to ensure firmware integrity. A CRC is a powerful error-detecting code, often described as a digital fingerprint for a block of data. By using it, our bootloader can mathematically verify that the application firmware in flash is exactly what it's supposed to be before ever attempting to execute it.
Specifically, we will use the STM32's dedicated hardware CRC peripheral to perform this check efficiently. By the end of this lesson, you will be able to use this peripheral to make your bootloader robust and reliable—a requirement for any professional product.
1. The Concept of Firmware Verification with CRC
The workflow for a CRC-enabled bootloader involves both the application and the bootloader working in tandem:
- At Compile Time (Application): After the application is compiled into a binary file, a post-build script calculates a 32-bit CRC value from the binary's contents. This CRC value, along with the application's size, is then embedded into a special "header" at the very beginning of the final application binary.
- At Run Time (Bootloader): On startup, the bootloader first reads this header from the application's memory region. It then uses the hardware CRC peripheral to calculate a new CRC based on the application code currently in flash. Finally, it compares its calculated CRC with the one stored in the header.
If the values match, the firmware is considered valid, and the bootloader proceeds with the jump. If they don't, the jump is aborted, preventing a crash.

Let's watch a short segment that explains this process.
STM32 Custom Bootloader (Part 3): Application Size & CRC Validation
The video 'STM32 Custom Bootloader (Part 3)' by ControllersTech, though it uses a software CRC library for its demonstration, provides an excellent conceptual overview of the CRC validation process. The principles are identical to what we will implement with the hardware peripheral.
Watch the segment from 00:31 to 01:47. Pay close attention to the explanation of how the application gets its CRC at build time and how the bootloader uses it for verification at run time.
2. Using the STM32 Hardware CRC Peripheral
While the video above used a software library, our STM32F103 includes a hardware peripheral dedicated to calculating CRC-32, which is faster and doesn't burden the CPU.
Enabling the Peripheral
First, we need to enable the CRC peripheral in the bootloader project using STM32CubeIDE.
- Open your
My_Bootloader.iocfile. - Under
Categories, navigate toComputingand selectCRC. - In the
CRC Mode and Configurationpanel, check theActivatedbox. - Save the
.iocfile to generate the initialization code.

Calculating CRC in Code
With the peripheral enabled, we can use the HAL_CRC_Calculate() function. This function takes a pointer to the data buffer and the number of 32-bit words to process.
// hcrc is the handle for the CRC peripheral, automatically generated by CubeMX.
// pBuffer is a pointer to the start of the data (e.g., your application code).
// BufferLength is the number of 32-bit words in the data.
uint32_t calculated_crc = HAL_CRC_Calculate(&hcrc, (uint32_t*)pBuffer, BufferLength);
For a quick, practical demonstration of how this function is used, the following video is very helpful.
STM32 Nucleo - Keil 5 IDE with CubeMX: Tutorial 8 - CRC32 Calculation
The video 'STM32 Nucleo - Keil 5 IDE with CubeMX: Tutorial 8 - CRC32 Calculation' by Mutex Embedded shows a minimal example of using the hardware CRC peripheral. It clearly demonstrates the HAL function call and how to verify the result.
Watch from 01:32 to 05:08. Notice how a simple array of data is created and how HAL_CRC_Calculate is called. The verification against an online calculator confirms the hardware is working as expected. You can ignore the Keil-specific parts; the C code is what matters.
For more in-depth details on the CRC peripheral and the HAL functions, you can refer to the ST Community article "How is CRC value calculated". It provides code snippets for both hardware and software implementations, which can be useful for comparison.
3. Practical Implementation: Upgrading the Bootloader System
Now, let's apply this to the bootloader and application projects you created in the previous lesson.
Task 1: Modify the Application Project
We need to add a header to our application and set up a post-build step to automatically calculate and insert the CRC.
-
Define the Application Header: In your
My_Applicationproject, create a new header file, e.g.,app_header.h, and define a structure for the header.// In app_header.h #ifndef APP_HEADER_H_ #define APP_HEADER_H_ #include <stdint.h> typedef struct { uint32_t app_size; uint32_t app_crc; } app_header_t; #endif /* APP_HEADER_H_ */ -
Instantiate the Header: In
main.cofMy_Application, create an instance of this header. We will use a GCC attribute to place it in a specific memory section named.app_header.// In My_Application/Core/Src/main.c #include "app_header.h" // IMPORTANT: The values here are placeholders. // They will be updated by the post-build script. // We will find the real values in step 4. const app_header_t app_header __attribute__((section(".app_header"))) = { .app_size = 0, // Placeholder .app_crc = 0 // Placeholder }; int main(void) { // ... your existing code } -
Update the Linker Script: We need to tell the linker where to place our new
.app_headersection. ModifySTM32F103C8TX_FLASH.ldto place it at the very beginning of the application's memory region (0x08004000), and have the rest of the application code (.text, etc.) follow it./* In My_Application/STM32F103C8TX_FLASH.ld */ /* ... inside the SECTIONS block ... */ .app_header : { KEEP(*(.app_header)) /* Keep the header section */ } >FLASH /* Change the .isr_vector to come AFTER the header */ .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* Vector table */ . = ALIGN(4); } >FLASH /* The rest of the sections (.text, .rodata, etc.) follow as before */ -
Create the Post-Build Script: The most crucial step is calculating the CRC after compilation. We will use a Python script for this, as demonstrated in the
ControllersTechvideo.-
First, generate a "clean" binary. Go to
Project Properties > C/C++ Build > Settings > MCU Post build outputs. Add a second command to create a binary that excludes our header section. This clean binary is what we'll use to calculate the CRC.# The existing command creates the full binary (e.g., My_Application.bin) # Add a second command for the clean binary: arm-none-eabi-objcopy -O binary -j .isr_vector -j .text -j .rodata -j .data "${BuildArtifactFileBaseName}.elf" "${BuildArtifactFileBaseName}_clean.bin"Build the project once. This will create
My_Application_clean.bin. -
Next, create the Python script. Create a file named
crc_generator.pyin yourMy_Applicationproject folder. This script will take the clean binary, calculate its size and CRC, and print them.# In crc_generator.py import os import zlib # --- CONFIGURATION --- APP_BINARY_PATH = "Debug/My_Application_clean.bin" # --------------------- # Get the application size app_size = os.path.getsize(APP_BINARY_PATH) # Read the application binary with open(APP_BINARY_PATH, 'rb') as f: app_binary = f.read() # Calculate CRC32 app_crc = zlib.crc32(app_binary) print(f"Application Size: {app_size} bytes") print(f"Application CRC32: 0x{app_crc:08X}") -
Run the script and update your header. Run
python crc_generator.pyfrom your terminal. It will output the exact size and CRC. Now, go back toMy_Application/Core/Src/main.cand replace the placeholder values in yourapp_headerinstance with these real values. -
Rebuild the project one last time. The final
My_Application.binnow contains the correct header. The brilliance of this method is that since the header itself is not part of the_clean.binfile, its own contents don't affect the CRC calculation.
-
Task 2: Modify the Bootloader Project
Now, let's make the bootloader use this information.
-
Include Header Definition: Copy the
app_header.hfile you created into yourMy_Bootloaderproject'sCore/Incfolder. -
Update the Jump Logic: Modify the
jump_to_applicationfunction in your bootloader'smain.c. The new logic will perform the CRC check before jumping.// In My_Bootloader/Core/Src/main.c #include "app_header.h" // Include our new header definition // Make sure CRC is enabled in CubeMX and you have #include "main.h" for hcrc // extern CRC_HandleTypeDef hcrc; #define APPLICATION_ADDRESS 0x08004000 #define APP_MAX_SIZE (48 * 1024) // 48KB, matching the app linker script void jump_to_application(void) { // 1. Read the application header app_header_t* app_header = (app_header_t*)APPLICATION_ADDRESS; // 2. Perform Sanity Checks // Check 1: Is there anything that looks like an application? (Stack pointer check) uint32_t initial_sp = *(volatile uint32_t*)APPLICATION_ADDRESS; if (initial_sp < 0x20000000 || initial_sp > 0x20005000) { printf("ERROR: No application found (invalid stack pointer).\r\n"); return; } // Check 2: Is the application size reasonable? if (app_header->app_size == 0 || app_header->app_size > APP_MAX_SIZE) { printf("ERROR: Application size is invalid.\r\n"); return; } // 3. Verify the CRC // The data to be checked starts AFTER the header uint32_t* app_code_start = (uint32_t*)(APPLICATION_ADDRESS + sizeof(app_header_t)); // The length is in bytes, HAL function needs it in 32-bit words uint32_t app_code_len_words = (app_header->app_size + 3) / 4; // Round up to nearest word // Calculate the CRC using the hardware peripheral uint32_t calculated_crc = HAL_CRC_Calculate(&hcrc, app_code_start, app_code_len_words); if (calculated_crc != app_header->app_crc) { printf("ERROR: CRC Mismatch! Calculated: 0x%08lX, Expected: 0x%08lX\r\n", calculated_crc, app_header->app_crc); return; } printf("SUCCESS: CRC check passed. Jumping to application.\r\n"); HAL_Delay(100); // 4. Perform the jump (same as before) void (*app_reset_handler)(void); uint32_t app_entry_point = *(volatile uint32_t*)(APPLICATION_ADDRESS + sizeof(app_header_t) + 4); // Reset handler is after SP app_reset_handler = (void (*)(void))app_entry_point; HAL_UART_DeInit(&huart1); HAL_DeInit(); __disable_irq(); // The stack pointer is the first word in the app's *code* section, which is after our header. uint32_t app_stack_pointer = *(volatile uint32_t*)(APPLICATION_ADDRESS + sizeof(app_header_t)); __set_MSP(app_stack_pointer); app_reset_handler(); }Note: We have a small architectural subtlety now. The stack pointer and reset handler are no longer at
APPLICATION_ADDRESSand+4. They are at the start of the application code, which is after our custom header. The code above reflects this change.
4. Testing the Integrity Check
Now for the crucial test.
-
Flash and Run (Success Case):
- Build and flash your modified
My_Bootloaderproject. - Build your modified
My_Applicationproject (with the correct CRC in its header). - Use the bootloader's UART update mechanism from the previous lesson to flash
My_Application.bin. - Reset the device. In your serial terminal, you should see the bootloader print the "SUCCESS: CRC check passed" message before the application's LED starts blinking.
- Build and flash your modified
-
Corrupt the Firmware (Failure Case):
- Connect to the board with STM32CubeProgrammer.
- Read the device memory starting from
0x08004000. - Find a byte somewhere in the application code (e.g., at
0x08004100) and manually change its value to something else (e.g., change0xABto0xAC). Do not change the header. - Click
Writeto save this change to the flash. - Now, reset your board.
- This time, the bootloader should detect the corruption. Your terminal should display the "ERROR: CRC Mismatch!" message, and the application's LED will not start blinking.
Congratulations! You have successfully implemented a robust firmware integrity check.
Conclusion
In this lesson, you have significantly improved the reliability of your bootloader by adding a CRC verification step. This is not just a theoretical exercise; it is a fundamental requirement for building safe and dependable products that can be updated in the field.
Key Takeaways:
- CRC is a powerful tool for detecting accidental data corruption in firmware.
- The process involves a post-build step to calculate and embed the application's size and CRC into its binary.
- The bootloader uses the hardware CRC peripheral (
HAL_CRC_Calculate) to efficiently verify the firmware in flash against the embedded CRC before executing it. - This verification prevents the system from running corrupted code, drastically improving its robustness.
Preview of the Next Lesson:
We will conclude this module by tackling another critical aspect of system stability: memory management. In the next lesson, you will learn how to configure and debug stack overflow issues using memory analysis tools, ensuring your applications are not just robust against corruption but also against runtime memory errors.