unit-testingembeddedstm32stm32cubeideceedling

How do I create an optimal, customizable Ceedling configuration for STM32 unit testing?


I’m working on an STM32 project using the stm32f407g-disc1 and want to set up Ceedling for unit testing. It is important for me to maintain the standard folder structure typically used in STM32 projects (such as in STM32CubeMX or STM32CubeIDE), while also having a robust and customizable Ceedling configuration.

I’ve already tried creating the project.yml file myself, but I’m not sure if I’ve implemented all the necessary steps correctly. My goal is to find a good Ceedling configuration that can be easily adapted to different STM32 projects. Are there any best practices or examples for setting up Ceedling optimally for STM32 unit testing, so that the configuration is flexible and sustainable?"

The STM32 Project


:project:
  :use_exceptions: FALSE
  :use_test_preprocessor: :all
  :use_auxiliary_dependencies: TRUE
  :build_root: build
  :release_build: TRUE
  :test_file_prefix: test_
  :which_ceedling: gem
  :ceedling_version: 0.31.1
  :default_tasks:
    - test:all

#:test_build:
# :use_assembly: TRUE

:release_build:
  :output: stmBase
  :use_assembly: TRUE
  :artifacts:
    - stmBase.map

# :output: MyApp.out
# :use_assembly: FALSE

:environment:

:extension:
  :executable: .elf

:paths:
  :test:
    - +:test/**
    - -:test/support
  :source:
    - Core/Inc/**
    - Core/Src/**
  :include:
    - Core/Inc/
    - Drivers/CMSIS/Device/ST/STM32F4xx/Include/
    - Drivers/CMSIS/Include/
    - Drivers/STM32F4xx_HAL_Driver/Inc/
  :toolchain_include:
    - USB_HOST/App
    - USB_HOST/Target
    - Core/Inc
    - build/vendor/unity/src
    - Drivers/STM32F4xx_HAL_Driver/Inc
    - Drivers/STM32F4xx_HAL_Driver/Inc/Legacy
    - Middlewares/ST/STM32_USB_Host_Library/Core/Inc
    - Middlewares/ST/STM32_USB_Host_Library/Class/CDC/Inc
    - Drivers/CMSIS/Device/ST/STM32F4xx/Include
    - Drivers/CMSIS/Include
  :support:
    - test/support

  :libraries: []

# :exclude_files:
#   - Core\Src\main.c

:defines:
  :common: &common_defines
    - STM32F407xx 
    - USE_HAL_DRIVER 
  :test:
    - *common_defines
    - TEST
  :test_preprocess:
    - *common_defines
    - TEST

:cmock:
  :mock_prefix: mock_
  :when_no_prototypes: :warn
  :enforce_strict_ordering: TRUE
  :plugins:
    - :ignore
    - :callback
  :treat_as:
    uint8: HEX8
    uint16: HEX16
    uint32: UINT32
    int8: INT8
    bool: UINT8
  # TODO: herausfinden, ob das richtig ist.
  :includes:
    - <stdbool.h>
    - <stdint.h>
  :treat_externs: :include


:gcov:
  :reports:
    - HtmlDetailed
  :gcovr:
    :html_medium_threshold: 75
    :html_high_threshold: 90

:tools:
  :test_compiler:
    :executable: arm-none-eabi-gcc
    :arguments:
      - ${1}
      - -mcpu=cortex-m4
      - -std=gnu11
      - -g3
      - -DDEBUG
      - -DUSE_HAL_DRIVER
      - -DSTM32F407xx
      - -c
      - -I"$": COLLECTION_PATHS_TOOLCHAIN_INCLUDE
      - -O0
      - -ffunction-sections
      - -fdata-sections
      - -Wall
      - -fstack-usage
      - -fcyclomatic-complexity
      - -MMD
      - -MP
      - -MF"${2}.d"
      - -MT"${2}.o"
      - --specs=nano.specs
      - -mfpu=fpv4-sp-d16
      - -mfloat-abi=hard
      - -mthumb
      - -o ${2}

  :test_linker:
    :executable: arm-none-eabi-gcc
    :arguments:

      - ${1}
      - -DTARGET
      - -Isrc/ 
      - -I"$": COLLECTION_PATHS_RELEASE_TOOLCHAIN_INCLUDE 
      - -mcpu=cortex-m4 
      - -mthumb 
      - -mfpu=fpv4-sp-d16 
      - -mfloat-abi=hard 
      - -Wl,-Map="${2}.map" 
      - -g 
      - -T"STM32F407VGTX_FLASH.ld" 
      - --specs=nosys.specs 
      - -Wl,--gc-sections 
      - -static 
      - --specs=nano.specs 
      - -Wl,--start-group -lc -lm -Wl,--end-group 
      - -o ${2}.elf 


:libraries:
  :placement: :end

  :flag: "-l${1}"

  :path_flag: "-L ${1}"

  :system: [] 

  :test: []

  :release: []

:plugins:
  :load_paths: []
  :enabled: []


Solution

  • Here's an example of a working setup for a project for an STM32L4P5. Since you are using an STM32F407 you'll obviously have to change some include paths and defines as appropriate.

    First a note on directory structure. Here I have a top-level git repository. Within are two directories containing my application/driver specific code (in MyApp and MyDrivers). You probably want to choose better names, I've renamed these from project-specific names just as an example. These directories contain the code that will be unit tested. You might only have one such directory, or more than two, depending on your project.

    Then there is the STM32 project (created with STM32CubeIDE or STM32CubeMX, for example). I did not add any of my code into this directory. I changed main.c to call out my project-specific app_main() function.

    Lastly there is the unit_test directory, where you will construct the unit-test framework.

    It looks like this:

    repo_root/
    ├─ MpApp/
    │  ├─ Src/
    │  │  ├─ app_file.c
    │  ├─ Inc/
    │  │  ├─ app_file.h
    ├─ MyDrivers/
    │  ├─ Src/
    │  │  ├─ foo_driver.c
    │  ├─ Inc/
    │  │  ├─ foo_driver.h
    ├─ STMProject/
    │  ├─ Core/
    │  ├─ Drivers/
    │  ├─ ...etc.../
    ├─ unit_test/
    │  ├─ build/
    │  ├─ test/
    │  │  ├─ test_app_file.c
    │  │  ├─ test_foo_driver.c
    │  │  ├─ support/
    │  ├─ project.yml
    

    You'll see that the project contains two files that will be unit tested: app_file.c and foo_driver.c. Hence there are only two files in the unit_test/test directory, one for each file to test.

    The unit_test directory contains a build directory, which is where build artifacts will be placed during the test process.

    Here is the project.yml file that lives in unit_test:

    ---
    
    # Notes:
    # Sample project C code is not presently written to produce a release artifact.
    # As such, release build options are disabled.
    # This sample, therefore, only demonstrates running a collection of unit tests.
    
    :project:
      :use_exceptions: TRUE
      :use_test_preprocessor: TRUE
      :use_auxiliary_dependencies: TRUE
      :build_root: build
    #  :release_build: TRUE
      :test_file_prefix: test_
      :which_ceedling: gem
      :ceedling_version: 0.31.1
      :default_tasks:
        - test:all
    
    :test_build:
      :use_assembly: FALSE
    
    #:release_build:
    #  :output: MyApp.out
    #  :use_assembly: FALSE
    
    :environment:
    
    :extension:
      :executable: .out
    
    :paths:
      :test:
        - +:test/**
        - -:test/support
      :source:
        - ../STMProject/Core/Inc/
        - ../MyApp/Src/
        - ../MyApp/Inc/
        - ../MyDrivers/Src/
        - ../MyDrivers/Inc
        - ../STMProject/Drivers/STM32L4xx_HAL_Driver/Inc
        - ../STMProject/Drivers/STM32L4xx_HAL_Driver/Inc/Legacy/
        - ../STMProject/Drivers/CMSIS/Device/ST/STM32L4xx/Include/
        - ../STMProject/Drivers/CMSIS/Include
      :support:
        - test/support
      :libraries: []
    
    :defines:
      # in order to add common defines:
      #  1) remove the trailing [] from the :common: section
      #  2) add entries to the :common: section (e.g. :test: has TEST defined)
      :common: &common_defines
        - STM32L4P5xx
        - USE_HAL_DRIVER
        - TEST
        - CMOCK_MEM_DYNAMIC
      :test:
        - *common_defines
      :test_preprocess:
        - *common_defines
    
    :flags:
      :test:
        :compile:
          :*:
            - -Wno-int-to-pointer-cast
            - -Wno-pointer-to-int-cast
    #        - -O0
    #        - -g
    
    :cmock:
      :mock_prefix: mock_
      :when_no_prototypes: :warn
      :enforce_strict_ordering: TRUE
      :plugins:
        - :ignore
        - :callback
        - :expect_any_args
        - :ignore_arg
      :includes:
        - stm32l4xx_hal.h
      :includes_h_pre_orig_header:
        - stm32l4xx_hal.h
      :includes_c_pre_header:
        - stm32l4xx_hal.h
      :treat_as:
        uint8:    HEX8
        uint16:   HEX16
        uint32:   UINT32
        int8:     INT8
        bool:     UINT8
      :strippables:
        - '__NO_RETURN'
    
    # Add -gcov to the plugins list to make sure of the gcov plugin
    # You will need to have gcov and gcovr both installed to make it work.
    # For more information on these options, see docs in plugins/gcov
    :gcov:
      :reports:
        - HtmlDetailed
        - Text
      :gcovr:
        :html_medium_threshold: 75
        :html_high_threshold: 90
        :print_summary: true
    
    #:tools:
    # Ceedling defaults to using gcc for compiling, linking, etc.
    # As [:tools] is blank, gcc will be used (so long as it's in your system path)
    # See documentation to configure a given toolchain for use
    
    
    # LIBRARIES
    # These libraries are automatically injected into the build process. Those specified as
    # common will be used in all types of builds. Otherwise, libraries can be injected in just
    # tests or releases. These options are MERGED with the options in supplemental yaml files.
    :libraries:
      :placement: :end
      :flag: "-l${1}"
      :path_flag: "-L ${1}"
      :system: []    # for example, you might list 'm' to grab the math library
      :test: []
      :release: []
    
    :plugins:
      :load_paths:
        - "#{Ceedling.load_path}"
      :enabled:
        - stdout_pretty_tests_report
        - module_generator
        - gcov
    ...
    

    I'm not sure I can give a line-by-line breakdown of what all that means, this was made over time as a collective effort until it worked.

    The interesting part, probably, is the :paths:source: directive. This has to include all the directories that contain source code to be tested, and directories containing header files that can be mocked. Don't include paths to source code that you don't want to test.