Wrangling Two Monsters Part I: CMake and Pico C/C++ SDK Setup for TDD

Intro

Embedded world is a tedious place. The best way to navigate it while keeping your sanity is through the practice of Test Driven Development (TDD).

Being a bit blunt, Pico C/C++ SDK is a monstrosity (huge codebase with nested projects) managed by another monstrosity (CMake).

Nevertheless, both can be dealt with by using the popular strategy of “How to eat an elephant” - one small piece at a time.

TDD Setup Overview

TDD Considerations and Trade-Offs

The Pico C/C++ SDK is a mess of nested CMake-managed projects/directories. The scary nature of this monster does not start to manifest until you have to compile your first unit test. It tends to sneak up during inclusion of Pico-specific headers.

Most of the mess is caused by endless transient dependencies, and the differences in available headers between host and target builds (the host builds will be missing a bunch of board-specific headers).

The root cause for this mess lies in CMake’s invocation of pico_skd_init(), and it’s seemingly endless recursive invocation of other CMake files within the Pico C/C++ SDK that check for all kinds of variables and then deciding of what will be included based on the specified target (host or a specific board). Ultimately Pico C/C++ SDK developers did not seem to expect their users to practice TDD and therefore did not deem it necessary to make hardware specific headers available for host builds.

Refactoring the entire Pico C/C++ SDK in an attempt to fix the CMake nightmare produced by it’s developers is not worth wasting your brain cycles.

A reasonable way to address this is to strike a balance between using the actual headers (when possible), and mocking just enough dependencies for the tests to pass in order to lock in the expected “production” code behavior. More on that in another article.

Wrangling Your Very Own CMake Monster

Whatever your love/hate/both relationship with CMake is, at a minimum, CMake can be useful (maybe even liked a little :P) if one keeps things organized.

The key factors to success:

Here is an example of the project, which can be cloned as follows

git clone https://github.com/vladstechblog/pico_blink_tdd.git -b pico_tcc_cmake_setup

The project CMake directory structure is listed below, with source and test files omitted for clarity:

|-- CMakeLists.txt <= project root listfile
|-- include_pico_init.cmake <= factored out import of pico SDK
|-- src
|   |-- CMakeLists.txt <= responsible of building the app

`-- test
    |-- CMakeLists.txt <= responsible for building unit tests
    `-- include_local_cpputest_git_repo.cmake <= imports CppUTest

Key CMake Project Structure Takeaways

There is single root project listfile. Its purpose is to intialize the project, and delegate the build actions the appropriate listfiles contained in the src and test subdirectories. Note the that Pico C/C++ SDK initialization is a part of the project initialization process, as it configures the compiler toolchain specific to the target (host/board) build.

Actions not directly related to the build process are factored out into their own listfiles. Good candidates for those are mundane things related to dependency imports, such as validating that dependency directory, or a listfile exists, etc… Factoring out trivial work will help to keep the project listfiles focused and maintainable (the largest listfile in the example repo is 19 lines long)

Note the separation of concerns:

Quick Test Build Walkthrough

  1. Clone the the project as starting point and cd into project directory

    git clone https://github.com/vladstechblog/pico_blink_tdd.git -b pico_tcc_cmake_setup
    cd pico_blink_tdd
    
  2. Initialize project for host builds

    cmake -B build_host -DPICO_PLATFORM=host
    
  3. Built and run the test target

    cmake --build build_host/ -t appTestRunner
    
  4. Run the test runner binary

    ./build_host/test/appTestRunner
    
  5. Add code, rebuild and re-run the tests

The instructions above are a bit contrived. The actual blink TDD example will be covered in the next article.

Tags: