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
-
terms:
host build: code compiler for your development architecture (Your dev laptop)target build: code compiler for the Pico boardtarget: generic reference for which platform the project is builtCMake target(for the purpose of this article): an item which will be produced by CMake build systemlistfile: file containing CMake language
-
All development will be done in a Debian container
- with a local development directory mounted into
/home/developerinside the container (aka~/)
- with a local development directory mounted into
-
all pico-related GitHub repos are located in
~/pico, inside the development container and their corresponding environment variables are set as per this script -
TDD will be conducted on the host build.
cmake -B build_host -DPICO_PLATFORM=host -DPICO_NO_PICOTOOL=True -DCMAKE_BUILD_TYPE=Debug -
CppUTest repo will be located in the
/home/developer/cpputest -
board specifics will be skipped in order to reduce an already dense material.
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:
- keep project stucture organized, and logically separate
- keep CMake listfiles reasonably small (one per subdirectory)
- factor out trivial CMake items into their own list files and include them where needed
- keep your code modular (compiling it into static libraries is a reasonable choice)
- be mindful of what you are linking against (headers only vs actual pico
libraries). For example:
pico_stdlib_headersvspico_stdlib. - always try to minimize board-specific dependencies
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:
- PICO SDK import is factored out into
include_pico_init.cmake - core application source code build is delegated to the listfile in
src - unit test build is dedicate to the listfile in
test - CppUTest import is factored out into
include_local_cpputest_git_repo.cmake - in more complex projects, each software component in the
srcdirectory should have it’s own dedicated CMake listfile - source code builds are separated from test builds
- test builds are excluded from automatic core source codes builds
Quick Test Build Walkthrough
-
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 -
Initialize project for host builds
cmake -B build_host -DPICO_PLATFORM=host -
Built and run the test target
cmake --build build_host/ -t appTestRunner -
Run the test runner binary
./build_host/test/appTestRunner -
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.