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.
- 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).
- Clone the CppUTest repo.
- Incorporate the following files into your project:
- All of the
.h
files incpputest/include/CppUTest
- All of the
.h
files incpputest/include/CppUTestExt
- All of the
.cpp
files incpputest/src/CppUTest
- All of the
.cpp
files incpputest/src/CppUTestExt
- All of the
.h
and.cpp
files incpputest/test/CppUTest
EXCEPTcpputest/test/CppUTest/AllTests.cpp
- All of the
.h
and.cpp
files incpputest/test/CppUTest
EXCEPTcpputest/test/CppUTestExt/AllTests.cpp
- We exclude the two
AllTests.cpp
files because they definemain
, which your project should already have.
- All of the
- 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
orCPPUTEST_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
, andfree
. CPPUTEST_MEM_LEAK_DETECTION_DISABLED
can be used in place of-DCPPUTEST_USE_MEM_LEAK_DETECTION=0
.CPPUTEST_USE_LONG_LONG
orCPPUTEST_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
andPlatformSpecificFunctions_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
andPlatformSpecificFunctions_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
andPlatformSpecificFunctions_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
andPlatformSpecificFunctions_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
andPlatformSpecificFunctions_c.h
. CPPUTEST_USE_STD_C_LIB
orCPPUTEST_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
orCPPUTEST_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
.
THIS IS IMPORTANT! The following should always be defined:CPPUTEST_USE_MEM_LEAK_DETECTION
orCPPUTEST_MEM_LEAK_DETECTION_DISABLED
CPPUTEST_USE_LONG_LONG
orCPPUTEST_LONG_LONG_DISABLED
CPPUTEST_USE_STD_C_LIB
orCPPUTEST_STD_C_LIB_DISABLED
CPPUTEST_USE_STD_CPP_LIB
orCPPUTEST_STD_CPP_LIB_DISABLED
*_DISABLED
define. Or directlyCPPUTEST_*=0
orCPPUTEST_*=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
- 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
.
- For newlib-nano
- Create implementations for all of the functions defined in
include/cpputest/PlatformSpecificFunctions.h
andinclude/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
.
- 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 yourmain
function could be a good place for this code. - 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.