Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 68

Leveraging Hardware CRC for Firmware Integrity.

Recommended resources

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:

  1. 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.
  2. 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.

STM32 Custom Bootloader: Application Validation using CRC
This diagram illustrates the core concept. The bootloader inspects the application's metadata, including its size and a pre-calculated CRC, to validate its integrity before execution.

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.

  1. Open your My_Bootloader.ioc file.
  2. Under Categories, navigate to Computing and select CRC.
  3. In the CRC Mode and Configuration panel, check the Activated box.
  4. Save the .ioc file to generate the initialization code.
Enabling Hardware CRC in STM32CubeIDE for Firmware Integrity
Activating the CRC peripheral is a simple one-click step in the CubeMX configuration tool.

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.

  1. Define the Application Header: In your My_Application project, 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_ */
    
  2. Instantiate the Header: In main.c of My_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
    }
    
  3. Update the Linker Script: We need to tell the linker where to place our new .app_header section. Modify STM32F103C8TX_FLASH.ld to 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 */
    
  4. 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 ControllersTech video.

    • 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.py in your My_Application project 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.py from your terminal. It will output the exact size and CRC. Now, go back to My_Application/Core/Src/main.c and replace the placeholder values in your app_header instance with these real values.

    • Rebuild the project one last time. The final My_Application.bin now contains the correct header. The brilliance of this method is that since the header itself is not part of the _clean.bin file, its own contents don't affect the CRC calculation.

Task 2: Modify the Bootloader Project

Now, let's make the bootloader use this information.

  1. Include Header Definition: Copy the app_header.h file you created into your My_Bootloader project's Core/Inc folder.

  2. Update the Jump Logic: Modify the jump_to_application function in your bootloader's main.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_ADDRESS and +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.

  1. Flash and Run (Success Case):

    • Build and flash your modified My_Bootloader project.
    • Build your modified My_Application project (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.
  2. 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., change 0xAB to 0xAC). Do not change the header.
    • Click Write to 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.