Unit testing with hardware with mock functions is really neat it allows you to test your upper layers without the need to have a hardware, but what if we are actually develop a low level driver that interact directly with the microcontroller registers, well in that case we need to mock the register right??... Sure.
Lets imagine for an instance wee need to write our own GPIO driver at register level, Example down below illustrate two function accessing the MODER and ODR register.
app/dummy.c
#include "stm32g0xx.h"
#include "dummy.h"
GPIO_TypeDef *Ports[] = { GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF };
void gpio_init_pin( uint8_t port, uint8_t pin, uint8_t mode )
{
Ports[port]->MODER &= ~( 0x3 << ( pin * 2 ) );
Ports[port]->MODER |= ( mode << ( pin * 2 ) );
}
void gpio_write_pin( uint8_t port, uint8_t pin, uint8_t value )
{
if( value == 0 ){
Ports[port]->ODR &= ~( 1 << pin );
}
else{
Ports[port]->ODR |= ( 1 << pin );
}
}
app/dummy.h
#ifndef __DUMMY_H__
#define __DUMMY_H__
#define GPIO_PORT_A 0
#define GPIO_PORT_B 1
#define GPIO_PORT_C 2
#define GPIO_PORT_D 3
#define GPIO_PORT_E 4
#define GPIO_PORT_F 5
#define GPIO_MODE_INPUT 0
#define GPIO_MODE_OUTPUT 1
void gpio_init_pin( uint8_t port, uint8_t pin, uint8_t mode );
void gpio_write_pin( uint8_t port, uint8_t pin, uint8_t value );
#endif // __DUMMY_H__
But what do we mock here?, well the register of course. Take a look at the microcontroller user manual and locate the GPIO register to write the initial value of each one, this is important to emulate as close as we can the hardware. Declare those register as global variables in your test file
test/test_dummy.c
#include "unity.h"
#include "dummy.h"
/*mock microcontroller registers with its initial values*/
/* MODER OTYPER OSPEEDR PUPDR IDR ODR BSRR LCKR AFRL AFRH BRR*/
GPIO_TypeDef GPIOA_BASE = { 0xEBFFFFFF, 0x00, 0x0C000000, 0x24000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOB_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOC_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOD_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOE_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOF_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
#include "stm32g0b1xx.h"
void setUp(void)
{
}
void tearDown(void)
{
}
void test__gpio_init_pin__portb_pin3_as_output(void)
{
/*run function under test*/
gpio_init_pin( GPIO_PORT_B, 3, GPIO_MODE_OUTPUT );
/*test the expected value against the mock register*/
TEST_ASSERT_EQUAL_HEX32_MESSAGE( 0xFFFFFF7F, GPIOB->MODER, "GPIOB->MODER does not have expected value" );
}
void test__gpio_write_pin__portb_pin3_high(void)
{
/*run function under test*/
gpio_write_pin( GPIO_PORT_B, 3, 1 );
/*test the expected value against the mock register*/
TEST_ASSERT_EQUAL_HEX32_MESSAGE( 0x00000008, GPIOB->ODR, "GPIOB->ODR does not have expected value" );
}
void test__gpio_write_pin__portb_pin3_low(void)
{
/*run function under test*/
gpio_write_pin( GPIO_PORT_B, 3, 0 );
/*test the expected value against the mock register*/
TEST_ASSERT_EQUAL_HEX32_MESSAGE( 0x00000000, GPIOB->ODR, "GPIOB->ODR does not have expected value" );
}
The real magic happens in here, an alternative version of the microcontroller registers created by us, This will imitate the register but using variables declared as part of our unit test. I this file we define the register structure just like in the original file (actually copy and paste them from there), but replace the register declaration with actual reference using extern (remember the fake register are in our test file)
test/stm32g0xx.h
#ifndef STM32G0B1xx_H
#define STM32G0B1xx_H
#include <stdint.h>
/**
* @brief General Purpose I/O
*/
typedef struct
{
volatile uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */
volatile uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */
volatile uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */
volatile uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */
volatile uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
volatile uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */
volatile uint32_t BSRR; /*!< GPIO port bit set/reset register, Address offset: 0x18 */
volatile uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */
volatile uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */
volatile uint32_t BRR; /*!< GPIO Bit Reset register, Address offset: 0x28 */
} GPIO_TypeDef;
/*these are only reference, the actaull mock register with their intial values
are declared in the test file test_dummy.c*/
extern GPIO_TypeDef GPIOA_BASE;
extern GPIO_TypeDef GPIOB_BASE;
extern GPIO_TypeDef GPIOC_BASE;
extern GPIO_TypeDef GPIOD_BASE;
extern GPIO_TypeDef GPIOE_BASE;
extern GPIO_TypeDef GPIOF_BASE;
/*pointer to the mock register as declared in the original file*/
#define GPIOA ((GPIO_TypeDef *) &GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) &GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) &GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) &GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) &GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) &GPIOF_BASE)
#endif
To avoid any conflict with files we need to tell Ceedling to ignore the one that is part of the library, or the directory where the file is in project.yml
: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
and voila!!
$ ceedling gcov:all utils:gcov
Test 'test_dummy.c'
-------------------
Generating runner for test_dummy.c...
Compiling test_dummy_runner.c...
Compiling test_dummy.c...
Linking test_dummy.out...
Running test_dummy.out...
Creating gcov results report(s) in 'Build/ceedling/artifacts/gcov'... Done in 0.903 seconds.
--------------------------
GCOV: OVERALL TEST SUMMARY
--------------------------
TESTED: 3
PASSED: 3
FAILED: 0
IGNORED: 0
---------------------------
GCOV: CODE COVERAGE SUMMARY
---------------------------
dummy.c Lines executed:100.00% of 9
dummy.c Branches executed:100.00% of 2
dummy.c Taken at least once:100.00% of 2
dummy.c No calls
dummy.c Lines executed:100.00% of 9
Ok wait, Our fake register declaration are global variables, they have to be declare like that, other wise we can not be reference by the mocking register file, and this can cause a problem if we are not paying attention. The registers values does not reset on every test, they kept the value updated by the test cases, and this could affect the expected results, if we want the values get reset after every test we need to use the function setUp
#include "unity.h"
#include "dummy.h"
/*mock microcontroller registers with its initial values*/
/* MODER OTYPER OSPEEDR PUPDR IDR ODR BSRR LCKR AFRL AFRH BRR*/
GPIO_TypeDef GPIOA_BASE = { 0xEBFFFFFF, 0x00, 0x0C000000, 0x24000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOB_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOC_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOD_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOE_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
GPIO_TypeDef GPIOF_BASE = { 0xFFFFFFFF, 0x00, 0x00000000, 0x00000000, 0x00, 0x00, 0x00, 0x00, { 0x00, 0x00 }, 0x00 };
#include "stm32g0b1xx.h"
void setUp(void)
{
GPIOA_BASE.MODER = 0xEBFFFFFF;
GPIOA_BASE.OTYPER = 0x00;
GPIOA_BASE.OSPEEDR = 0x0C000000;
GPIOA_BASE.PUPDR = 0x24000000;
...
...
}