Finally, time to test code that involves microcontroller functionality, don't worry is not so different from what you have been doing so far, as usual let start with the following example

app/dummy.c

#include "stm32g0xx.h"

ADC_HandleTypeDef       AdcHandler;      /*adc handler estructure*/
ADC_ChannelConfTypeDef  sChanConfig;     /*adc channel configuration structure*/

void temp_init( void )
{
    AdcHandler.Instance                   = ADC1;
    AdcHandler.Init.ClockPrescaler        = ADC_CLOCK_SYNC_PCLK_DIV2;   /*APB clock divided by two*/
    AdcHandler.Init.Resolution            = ADC_RESOLUTION8b;           /*8 bit resolution with a Tconv of 8.5*/
    AdcHandler.Init.ScanConvMode          = ADC_SCAN_SEQ_FIXED;         /*scan adc channels from 0 to 16 in that order*/
    AdcHandler.Init.DataAlign             = ADC_DATAALIGN_RIGHT;        /*data converter is right alightned*/
    AdcHandler.Init.SamplingTimeCommon1   = ADC_SAMPLETIME_1CYCLE_5;    /*sampling time of 1.5*/
    AdcHandler.Init.ExternalTrigConv      = ADC_SOFTWARE_START;         /*software trigger*/
    AdcHandler.Init.EOCSelection          = ADC_EOC_SINGLE_CONV;        /*only applicable on ISR*/
    AdcHandler.Init.Overrun               = ADC_OVR_DATA_OVERWRITTEN;   /*data will be overwriten in case is not read it*/
    /*apply ADC configuration*/
    HAL_ADC_Init( &AdcHandler );
    
    /*config adc channel number 0*/
    sChanConfig.Channel = ADC_CHANNEL_0;
    sChanConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
    sChanConfig.SamplingTime = ADC_SAMPLINGTIME_COMMON_1;
    /*apply channel configuration*/
    HAL_ADC_ConfigChannel( &AdcHandler, &sChanConfig );
}

uint8_t get_temperature( void )
{
    uint8_t val, temp;
    
    HAL_ADC_Start( &AdcHandler );                /*trigger conversion*/
    HAL_ADC_PollForConversion( &AdcHandler, 1u );/*wait untill conversion is performed, around 1.25us*/
    val = HAL_ADC_GetValue( &AdcHandler );     /*read the digital value*/
    
    /*claculate temperature*/
    temp = (val / 10) * 2;
    return temp;
}

According to the previous code on dummy.c our testing file should be similar as the previous one, we must mock the stm32g0xx_hal_adc.h, and call all the corresponding mock version use by the code under test. In this particular case we use ExpectAndReturn versions. Notice that even though some ADC function does not return nothing (at least nothing we are using) we must explicitly call their respective mock version because ceedling is testing the function under test actually calls these functions.

test/test_dummy.c

#include "unity.h"
#include "dummy.h"
/*tell ceedling to mock the adc from the stm32library driver*/
#include "mock_stm32g0xx_hal_adc.h"

extern ADC_HandleTypeDef       AdcHandler;  /*reference to adc handler structure*/
extern ADC_ChannelConfTypeDef  sChanConfig; /*reference to adc config channel structure*/

void setUp(void)
{
}

void tearDown(void)
{
}

/*unit test the init fucntion*/
void test_temp_init( void )
{
    /*the unit under test call tha low level functions from the adc
    therefore we must explicty call the mock version of both functions*/
    HAL_ADC_Init_ExpectAndReturn( &AdcHandler, HAL_OK );
    HAL_ADC_ConfigChannel_ExpectAndReturn( &AdcHandler, &sChanConfig, HAL_OK );
    /*function under test, the only thing we are testing here is
    the function is actually calling the corresponding ADC functions*/
    temp_init();
}

/*unit test the get temperature function*/
void test__get_temperature__20_degrees( void )
{
    /*the unit under test call the low level functions from the adc
    therefore we must explicty call the mock version of both functions*/
    HAL_ADC_Start_ExpectAndReturn( &AdcHandler, HAL_OK );
    HAL_ADC_PollForConversion_ExpectAndReturn( &AdcHandler, 1, HAL_OK );
    /*return 100 from the mock HAL_ADC_GetValue, the value will be use 
    internally by function under test to calculate the temperature*/
    HAL_ADC_GetValue_ExpectAndReturn( &AdcHandler, 100 );
    /*run the function under test*/
    uint8_t temp = get_temperature( );
    TEST_ASSERT_EQUAL_MESSAGE( 20, temp, "Temperature is not the value expected" );
}

But Ceedling needs a more elaborate set-up in order to test hardware, specially a library like the one we are using from ST, for instance, we need to specify the Library paths. I like this way

:paths:
  :test:
    - test/**     # directory where the unit testing are
  :source:
    - app/**      # directory where the functions to test are
    - halg0/Src
    - cmsisg0/startups
  :include:
    - halg0/Inc
    - cmsisg0/core
    - cmsisg0/registers

We need those global defines use by the library

defines:
  :test:
    - UTEST   #define the macro UTEST to remove the static qualifier
    - STM32G0B1xx 
    - USE_HAL_DRIVER

We need to tell GCOV do not cover the HAL library, so we ignore directories where is located

# enable and configure code coverage
:gcov:
  :abort_on_uncovered: true
  :utilities:
    - gcovr
  :reports:
    - HtmlDetailed
  :uncovered_ignore_list:
    - halg0/**        # ignore HAL library
    - cmsisg0/**      # ignore HAL library
    - app/main.c #
    - app/app_ints.c #
    - app/app_msps.c #
    - app/myadc.c #

An important thing is to specify in cmock a very important include should be add to each mock, this is mainly due to the way the library manages all the headers files

:cmock:
  :mock_prefix: mock_
  :treat_externs: :include
  :when_no_prototypes: :warn
  :enforce_strict_ordering: TRUE
  :includes:  
    - stm32g0xx.h   #thisis the very important include i was talking  
  :plugins:
    - :ignore
    - :callback
    - :ignore_arg
    - :return_thru_ptr
  :treat_as:
    uint8: HEX8
    uint16: HEX16
    uint32: UINT32
    int8: INT8
    bool: UINT8

An finally, it is important to add the same flags we are using to compile our project, plus some other extra to avoid some warnings the compiler ceedling is using (by default is GCC). If you want to know what i’m saying try to run the test without the following option and you will see

:flags:
  :test:
    :compile:
      :*:
        - -O0
        - -ffunction-sections
        - -fdata-sections
        - -fno-builtin
        - -std=c99
        - -pedantic
        - -Wall
        - -Werror
        - -Wstrict-prototypes
        - -fsigned-char
        - -fomit-frame-pointer
        - -fverbose-asm
        - -Wno-int-to-pointer-cast 
        - -Wno-pointer-to-int-cast
        - -Wno-error=address
  # Note the extra set of flags for gcov here
  :gcov:
    :compile:
      :*:
        - -O0
        - -ffunction-sections
        - -fdata-sections
        - -fno-builtin
        - -std=c99
        - -pedantic
        - -Wall
        - -Werror
        - -Wstrict-prototypes
        - -fsigned-char
        - -fomit-frame-pointer
        - -fverbose-asm
        - -Wno-int-to-pointer-cast 
        - -Wno-pointer-to-int-cast
        - -Wno-error=address

Run the test with code coverage included to see a very nice results!!

$ ceedling gcov:all utils:gcov  

Test 'test_dummy.c'
-------------------
Generating include list for stm32g0xx_hal_adc.h...
Creating mock for stm32g0xx_hal_adc...
Generating runner for test_dummy.c...
Compiling test_dummy_runner.c...
Compiling test_dummy.c...
Compiling mock_stm32g0xx_hal_adc.c...
Compiling unity.c...
Compiling dummy.c with coverage...
Compiling CException.c...
Compiling cmock.c...
Linking test_dummy.out...
Running test_dummy.out...
Creating gcov results report(s) in 'Build/ceedling/artifacts/gcov'... Done in 0.862 seconds.

--------------------------
GCOV: OVERALL TEST SUMMARY
--------------------------
TESTED:  2
PASSED:  2
FAILED:  0
IGNORED: 0


---------------------------
GCOV: CODE COVERAGE SUMMARY
---------------------------
dummy.c Lines executed:100.00% of 22
dummy.c No branches
dummy.c Calls executed:100.00% of 5
dummy.c Lines executed:100.00% of 22

Ignoring arguments

What if, we do not want to use the sChanConfig variable as global, because is actually been use by one single function

void temp_init( void )
{
    ADC_ChannelConfTypeDef  sChanConfig = {0};     /*adc channel configuration structure*/

    AdcHandler.Instance                   = ADC1;
    AdcHandler.Init.ClockPrescaler        = ADC_CLOCK_SYNC_PCLK_DIV2;   /*APB clock divided by two*/
    AdcHandler.Init.Resolution            = ADC_RESOLUTION8b;           /*8 bit resolution with a Tconv of 8.5*/
    AdcHandler.Init.ScanConvMode          = ADC_SCAN_SEQ_FIXED;         /*scan adc channels from 0 to 16 in that order*/
    AdcHandler.Init.DataAlign             = ADC_DATAALIGN_RIGHT;        /*data converter is right alightned*/
    AdcHandler.Init.SamplingTimeCommon1   = ADC_SAMPLETIME_1CYCLE_5;    /*sampling time of 1.5*/
    AdcHandler.Init.ExternalTrigConv      = ADC_SOFTWARE_START;         /*software trigger*/
    AdcHandler.Init.EOCSelection          = ADC_EOC_SINGLE_CONV;        /*only applicable on ISR*/
    AdcHandler.Init.Overrun               = ADC_OVR_DATA_OVERWRITTEN;   /*data will be overwriten in case is not read it*/
    /*apply ADC configuration*/
    HAL_ADC_Init( &AdcHandler );
    
    /*config adc channel number 0*/
    sChanConfig.Channel = ADC_CHANNEL_0;
    sChanConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
    sChanConfig.SamplingTime = ADC_SAMPLINGTIME_COMMON_1;
    /*apply channel configuration*/
    HAL_ADC_ConfigChannel( &AdcHandler, &sChanConfig );
}

There is no way we can tested using the same code (try it and you will see) we to make use of a different mock versions of HAL_ADC_ConfigChannel we need to ignore the second argument. It is not been use by the actual test anyway.

/*unit test the init fucntion*/
void test_temp_init( void )
{
    /*the unit under test call tha low level functions from the adc
    therefore we must explicty call the mock version of both functions*/
    HAL_ADC_Init_ExpectAndReturn( &AdcHandler, HAL_OK );
    /*it is not possible to use the secod paramter and we do not care
    that is way we ignore*/
    HAL_ADC_ConfigChannel_ExpectAndReturn( &AdcHandler, NULL, HAL_OK );
    HAL_ADC_ConfigChannel_IgnoreArg_pConfig( );
    /*function under test*/
    temp_init();
}

We can actually ignore the both fucntion calls (but specifying the return value)

/*unit test the init fucntion*/
void test_temp_init( void )
{
    /*ignore the fucntion under test is calling the functions*/
    HAL_ADC_Init_IgnoreAndReturn( HAL_OK );
    HAL_ADC_ConfigChannel_IgnoreAndReturn( HAL_OK );
    /*function under test*/
    temp_init();
}

Mocking Callbacks

The library make heavy use of callback functions to insert code right at the initialization or during an interrupt, the propotoypes fior this callbacks are written in the header file, therefore when cmock mock the file believes this functions has an implementation and make a mocks version, BUT, we don’t want that, because those functions are defined by us, in our application.

We need to tell cmock to ignore those functions, so we add the option strippables in projecy.yml we add only those functions been use in our application.

:cmock:
  :mock_prefix: mock_         # Generate mock version using mock prefix
  :treat_externs: :include
  :when_no_prototypes: :warn
  :enforce_strict_ordering: TRUE
  :includes:
    - stm32g0xx.h             # Include by default this header on each mock file
  :strippables:              # Add here all fucntions you do not want to be mocked
    - '(?:HAL_GPIO_EXTI_Rising_Callback\s*\(+.*?\)+)'     # For instance the callback functions
    - '(?:HAL_GPIO_EXTI_Falling_Callback\s*\(+.*?\)+)'    # For instance the callback functions
  :plugins:
    - :ignore                 # Generate <function>_Ignore and <function>_IgnoreAndReturn
    - :ignore_arg             # Generate <function>_IgnoreArg_<param_name>
    - :expect_any_args        # Generate <function>_func_ExpectAnyArgs and <function>_func_ExpectAnyArgsAndReturn
    - :array                  # Generate <function>_ExpectWithArray and <function>_ExpectWithArrayAndReturn
    - :callback               # Generate <function>_ 
    - :return_thru_ptr        # Generate <function>_ReturnArrayThruPtr_<param_name> and <function>_ReturnMemThruPtr_<param_na