CppUTest on STM32L475VGTx

Post Written: 08/22/2025

Project Start: Jul 2025

Project End: August 2025

Getting CppUTest and CppUTestExt Running on an STM32L475VGTx

TL;DR

I got CppUTest running on a STM32L475VGTx. I have yet to verify but this methodology should work for any embedded target using arm-none-eabi-gcc with newlib nano. Reasoning would tell me that using newlib and arm-none-eabi-gcc should work.


Longer Version

I really struggled to write this post because there are so many ways to incorporate CppUTest into your project. However, there are a few things that must be done. So, I'm not going to make any recommendations around organizational types of things. We'll keep it simple and focus only on the necessities. The rest, like static library, submodule, etc. is up to you. I will share how I chose to do it. Maybe I'll do it differently later.

Two years after getting my hands on Test-Driven Development for Embedded C, I've finally made time to start working through it. TDD is a wonderful tool and my preferred way to do development. Unfortunately, getting it started is not always straightforward - especially on an embedded target. I was comfortably making my way through the book and its exercises, until I got to the end of chapter 5... Chapter 5, exercise 2 asks the reader to compile CppUTest (or Unity) for their target. Well, after some time, I've got it working!

My hope is to spare you the trouble and headache and share my learnings. Between https://cpputest.github.io/manual.html and https://github.com/cpputest/cpputest I was left confused. It wasn't immediately apparent to me that most of the getting started information was not intended for embedded use. Platform Stories had some hints but I think a big overview would've been helpful. I'm grateful for CppUTest and those who maintain it. I'd like to give back, so here we go!

All of this is my working understanding. I didn't develop CppUTest so please don't defer to me as an expert. If you have a better way of doing this, I'd love to hear about it!

I'll share:

  • TL;DR: How to get it done
  • How I did it
  • Thoughts for Extensibility

TL;DR: How to get it done

I'm not going to cover organization or build targets, just the bare minimum to get things done in this section.

  1. Have a project (that builds and runs) where you want to use CppUTest. (I recommend trying this with an empty project that runs - Hello World style).
  2. Clone the CppUTest repo.
  3. Incorporate the following files into your project:
    • All of the .h files in cpputest/include/CppUTest
    • All of the .h files in cpputest/include/CppUTestExt
    • All of the .cpp files in cpputest/src/CppUTest
    • All of the .cpp files in cpputest/src/CppUTestExt
    • All of the .h and .cpp files in cpputest/test/CppUTest EXCEPT cpputest/test/CppUTest/AllTests.cpp
    • All of the .h and .cpp files in cpputest/test/CppUTest EXCEPT cpputest/test/CppUTestExt/AllTests.cpp
    • We exclude the two AllTests.cpp files because they define main, which your project should already have.
  4. Define -D the following for your preprocessor. You need to choose these based on what you have available to use on your platform. The notes below each define are not exhaustive and may change.
    • CPPUTEST_USE_MEM_LEAK_DETECTION or CPPUTEST_MEM_LEAK_DETECTION_DISABLED
      • Tells CppUTest to enable or disable memory leak detection.
      • This is done by overriding the global operator for new, delete, malloc, and free.
      • CPPUTEST_MEM_LEAK_DETECTION_DISABLED can be used in place of -DCPPUTEST_USE_MEM_LEAK_DETECTION=0.
    • CPPUTEST_USE_LONG_LONG or CPPUTEST_LONG_LONG_DISABLED
      • Tells CppUTest if long long is available.
      • CPPUTEST_LONG_LONG_DISABLED can be used in place of -DCPPUTEST_USE_LONG_LONG=0.
    • CPPUTEST_HAVE_STRDUP
      • Tells CppUTest if string duplicate is available.
      • Used in "StandardCLibrary.h" to determine if <string.h> will be included.
    • CPPUTEST_HAVE_FORK
      • Tells CppUTest if we have fork().
      • We can implement our own version when implementing what is defined in PlatformSpecificFunctions.h and PlatformSpecificFunctions_c.h.
    • CPPUTEST_HAVE_WAITPID
      • Tells CppUTest if we have waitpid().
      • We can implement our own version when implementing what is defined in PlatformSpecificFunctions.h and PlatformSpecificFunctions_c.h.
    • CPPUTEST_HAVE_KILL
      • Tells CppUTest if we have kill().
      • We can implement our own version when implementing what is defined in PlatformSpecificFunctions.h and PlatformSpecificFunctions_c.h.
    • CPPUTEST_HAVE_PTHREAD_MUTEX_LOCK
      • Tells CppUTest if we have <pthread.h>.
      • We can implement our own version when implementing what is defined in PlatformSpecificFunctions.h and PlatformSpecificFunctions_c.h.
    • CPPUTEST_HAVE_GETTIMEOFDAY
      • Tells CppUTest if we have <sys/time.h>.
      • We can implement our own version when implementing what is defined in PlatformSpecificFunctions.h and PlatformSpecificFunctions_c.h.
    • CPPUTEST_USE_STD_C_LIB or CPPUTEST_STD_C_LIB_DISABLED
      • Tells CppUTest if we have the C standard library.
      • CPPUTEST_STD_C_LIB_DISABLED can be used in place of -DCPPUTEST_USE_STD_C_LIB=0.
    • CPPUTEST_USE_STD_CPP_LIB or CPPUTEST_STD_CPP_LIB_DISABLED
      • Tells CppUTest if we have the C++ standard library.
      • CPPUTEST_STD_CPP_LIB_DISABLED can be used in place of -DCPPUTEST_USE_STD_CPP_LIB=0.
    I encourage you to explore the downstream effects of each of these macros.

    THIS IS IMPORTANT! The following should always be defined:
    • CPPUTEST_USE_MEM_LEAK_DETECTION or CPPUTEST_MEM_LEAK_DETECTION_DISABLED
    • CPPUTEST_USE_LONG_LONG or CPPUTEST_LONG_LONG_DISABLED
    • CPPUTEST_USE_STD_C_LIB or CPPUTEST_STD_C_LIB_DISABLED
    • CPPUTEST_USE_STD_CPP_LIB or CPPUTEST_STD_CPP_LIB_DISABLED
    Choose to disable them indirectly by using the corresponding *_DISABLED define. Or directly CPPUTEST_*=0 or CPPUTEST_*=1.

    DO NOT define the following unless you're using them. In fact, I would explicitly set them as undefined (-UCPPUTEST_HAVE_*) UNLESS you're using them. This is because in CppUTest it just checks to see if the macro has been defined.
    • CPPUTEST_HAVE_STRDUP
    • CPPUTEST_HAVE_FORK
    • CPPUTEST_HAVE_WAITPID
    • CPPUTEST_HAVE_KILL
    • CPPUTEST_HAVE_PTHREAD_MUTEX_LOCK
    • CPPUTEST_HAVE_GETTIMEOFDAY

  5. Enable float support for printf if it's not already enabled. If you're not sure, you will find out later when CppUTest's tests fail. I'm not sure if you can skip this step if you're not using a print-style communication interface (e.g. LED alternative to indicate test success or failure).
    • For newlib-nano -u _printf_float.
  6. Create implementations for all of the functions defined in include/cpputest/PlatformSpecificFunctions.h and include/cpputest/PlatformSpecificFunctions_c.h.
    • Figure out how your platform will communicate test outcomes with you. This could be flashing an LED or printing over UART.
    • This will be implemented in PlatformSpecificFPuts.
  7. Add the following header and code to wherever you plan to call your test code: #include "CppUTest/CommandLineTestRunner.h" and // Can add command line arguments here if you so choose: char* argv[] = { (char*) 0, // (char*) "-v", // verbose mode // (char*) "-gSimpleStringBuffer", // (char*) "-ojunit", }; int argc = sizeof(argv) / sizeof(char*); CommandLineTestRunner::RunAllTests(argc, argv); Wherever you have your main function could be a good place for this code.
  8. Build and run your project on your platform. The CppUTest and CppUTestExt combined executable/image may be too large to run on your system. Consider breaking them into smaller executables/images.

How I Did It

Originally, I built CppUTest as a static library and linked against it. This has its advantages and disadvantages. Keeping track of #defines and settings for both the CppUTest static library build and project build is a downside. I think not including CppUTest files in your project is an advantage. Additionally, you can create function pointers for your PlatformSpecific when building CppUTest and then implement these in your project. When linking against the CppUTest static library the linker resolves the symbols with whatever you've defined in your project. This keeps CppUTest and your project code separate. For this blog post, I'm not going to share more details on the static library approach.

Defines and Undefines

Here are the settings I chose:

These were defined in the STM32CubeIDE preprocessor for Assembler, C, and C++.

  • CPPUTEST_USE_MEM_LEAK_DETECTION=1
  • CPPUTEST_USE_LONG_LONG=0
  • CPPUTEST_USE_STD_C_LIB=1
  • CPPUTEST_USE_STD_CPP_LIB=0

These were undefined in the STM32CubeIDE preprocessor for Assembler, C, and C++.

  • CPPUTEST_HAVE_STRDUP
  • CPPUTEST_HAVE_FORK
  • CPPUTEST_HAVE_WAITPID
  • CPPUTEST_HAVE_KILL
  • CPPUTEST_HAVE_PTHREAD_MUTEX_LOCK
  • CPPUTEST_HAVE_GETTIMEOFDAY

Communication Interface

I chose to use UART so I could get a similar output to what I would experience developing on the host platform. This was done by enabling UART through the STM32CubeIDE.

PlatformSpecific Code

Here's the platform-specific code I used:

// Written by Gabriel Rubin on 07/01/2025 // A good bit of this is copied from other sources. CppUTest platforms being one of them. // for use with the STM32L475VGTx MCU. #include "stm32l4xx_hal.h" #include "cmsis_gcc.h" #include "stm32l4xx_hal_uart.h" #include "CppUTest/TestOutput.h" #include "CppUTest/PlatformSpecificFunctions_c.h" #include "CppUTest/TestHarness.h" #define far // eliminate "meaningless type qualifier" warning extern "C" { #include #include #include #include } #undef far static jmp_buf test_exit_jmp_buf[10]; static int jmp_buf_index = 0; extern UART_HandleTypeDef huart1; extern "C" TestOutput::WorkingEnvironment STM32L475VGTxGetWorkingEnvironment() { // Not sure why, but we'll copy what is done for GccNoStdC platform. // It's an enum value anyway. Plus CubeIDE is based on Eclipse. return TestOutput::WorkingEnvironment::eclipse; } extern "C" void STM32L475VGTxRunTestInASeperateProcess(UtestShell* shell, TestPlugin* plugin, TestResult* result) { (void)plugin; result->addFailure(TestFailure(shell, "-p doesn't work on this platform, as it is lacking fork.\b")); } extern "C" int STM32L475VGTxFork(void) { return 0; } extern "C" int STM32L475VGTxWaitPid(int pid, int* status, int options) { return 0; } /* Jumping operations. They manage their own jump buffers */ extern "C" int STM32L475VGTxSetJmp(void (*function) (void* data), void* data) { if (0 == setjmp(test_exit_jmp_buf[jmp_buf_index])) { jmp_buf_index++; function(data); jmp_buf_index--; return 1; } return 0; } extern "C" void STM32L475VGTxLongJmp(void) { jmp_buf_index--; longjmp(test_exit_jmp_buf[jmp_buf_index], 1); } extern "C" void STM32L475VGTxRestoreJumpBuffer(void) { jmp_buf_index--; } /* Time operations */ extern "C" unsigned long STM32L475VGTxTimeInMillis() { return HAL_GetTick(); } extern "C" const char* STM32L475VGTxTimeString() { unsigned long curTime = STM32L475VGTxTimeInMillis(); constexpr size_t MAX_LONG_UNSIGNED_STRING_SIZE = 11; static char timeString[MAX_LONG_UNSIGNED_STRING_SIZE] = {}; snprintf(timeString, sizeof(timeString), "%lu", curTime); return timeString; } /* String operations */ extern "C" int STM32L475VGTxVSNprintf(char *str, size_t size, const char* format, va_list va_args_list) { return vsnprintf(str, size, format, va_args_list); } /* Misc */ extern "C" double STM32L475VGTxFabs(double d) { return fabs(d); } extern "C" int STM32L475VGTxIsNan(double d) { return isnan(d); } extern "C" int STM32L475VGTxIsInf(double d) { return isinf(d); } extern "C" int STM32L475VGTxAtExit(void(*func)(void)) { return atexit(func); } /* IO operations */ extern "C" PlatformSpecificFile STM32L475VGTxFOpen(const char* filename, const char* flag) { // return fopen(filename, flag); return NULL; } extern "C" void STM32L475VGTxFPuts(const char* str, PlatformSpecificFile file) { // fputs(str, stdout); (void)file; char outputStr[1000] = {}; size_t curPos = 0; char curChar = 0; size_t strLength = strlen(str); for(size_t i = 0; i < strLength; ++i) { curChar = str[i]; if(curChar == '\n') { outputStr[curPos] = '\r'; ++curPos; } outputStr[curPos] = curChar; ++curPos; } HAL_UART_Transmit(&huart1, (const uint8_t*)outputStr, curPos, HAL_MAX_DELAY); } extern "C" void STM32L475VGTxFClose(PlatformSpecificFile file) { // fclose((FILE *) file); // do nothing // Suppress unused warnings (void)file; } extern "C" void STM32L475VGTxFlush(void) { // fflush(stdout); // const uint8_t flushStr[] = "\r"; // HAL_UART_Transmit(&huart1, (uint8_t*)NULL, 0, HAL_MAX_DELAY); } PlatformSpecificFile STM32L475VGTxStdOut = stdout; /* Random operations */ extern "C" void STM32L475VGTxSrand(unsigned int seed) { srand(seed); } extern "C" int STM32L475VGTxRand(void) { return rand(); } /* Dynamic Memory operations */ extern "C" void* STM32L475VGTxMalloc(size_t size) { return malloc(size); } extern "C" void* STM32L475VGTxRealloc(void* memory, size_t size) { return realloc(memory, size); } extern "C" void STM32L475VGTxFree(void* memory) { free(memory); } extern "C" void* STM32L475VGTxMemCpy(void* s1, const void* s2, size_t size) { return memcpy(s1, s2, size); } extern "C" void* STM32L475VGTxMemset(void* mem, int c, size_t size) { return memset(mem, c, size); } typedef void* PlatformSpecificMutex; extern "C" PlatformSpecificMutex STM32L475VGTxMutexCreate(void) { return NULL; } extern "C" void STM32L475VGTxMutexLock(PlatformSpecificMutex mtx) { // Suppress unused warnings (void)mtx; } extern "C" void STM32L475VGTxMutexUnlock(PlatformSpecificMutex mtx) { // Suppress unused warnings (void)mtx; } extern "C" void STM32L475VGTxMutexDestroy(PlatformSpecificMutex mtx) { // Suppress unused warnings (void)mtx; } extern "C" void STM32L475VGTxAbort(void) { // Should do something better than calling stdlib abort here. abort(); }

Binary Size

When trying to build the tests for CppUTest and CppUTestExt, the binary/image size was too large. Instead, I broke them out into two targets which made the size compatible with my platform.

Thoughts for Extensibility

Here are a few thoughts on how to make getting started with CppUTest on an embedded platform more friendly. It'd be great to have a guide that explains all of the "switches" (macros) that can be flipped for different system behavior. It seems like there was a push to automate a lot of this, which is appreciated, but it seems like the embedded side of things may have suffered in the process. Parity across the macros would be nice. It's confusing that defining some macros to 0 means they work, while that's not the case for others. Creating a way to decouple CppUTest from the project would be great. I think a static library approach as mentioned earlier could work well so long as there's some sort of unifying config when they're being built. This is a bigger ask when working with STM32CubeIDE.