LED blinking - Example Application
Let's create our very simple embedded application.
Note
If you don't want to write the application yourself, you can check out the example repo on GitHub, which provides a working solution.
The Specification
We have 3 requirements:
- After power up, the LD2 user LED should blink with a 1Hz frequency and a 50% duty cycle.
- If the LED is blinking and the user presses the B1 user button, the LED should stop blinking.
- If the LED is not blinking and the user presses the B1 user button, the LED should start blinking again.
The Implementation
Create the directory structure for the application in the root of the project generated by the STM32CubeMX application.
app
├── src
└── test
├── mocks
└── tests
The application sources
Add the application source and header to the app/src directory:
#include <stdint.h>
#include "demo.h"
static void demo_error( void);
static void demo_error( void)
{
HAL_GPIO_WritePin( GPIOA, GPIO_PIN_5, GPIO_PIN_SET );
}
void demo( void )
{
HAL_StatusTypeDef hal_status;
/* Start the timer. */
hal_status = HAL_TIM_Base_Start_IT( &htim1 );
if ( hal_status != HAL_OK )
{
demo_error();
}
}
void HAL_TIM_PeriodElapsedCallback( TIM_HandleTypeDef * p_htim )
{
if ( p_htim == &htim1 )
{
HAL_GPIO_TogglePin( GPIOA, GPIO_PIN_5 );
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
HAL_StatusTypeDef hal_status;
hal_status = HAL_OK;
if ( GPIO_PIN_13 == GPIO_Pin )
{
if ( htim1.State == HAL_TIM_STATE_BUSY )
{
(void)HAL_TIM_Base_Stop_IT( &htim1 );
HAL_GPIO_WritePin( GPIOA, GPIO_PIN_5, GPIO_PIN_RESET );
}
else if ( htim1.State == HAL_TIM_STATE_READY )
{
__HAL_TIM_SET_COUNTER( &htim1, 0u );
hal_status = HAL_TIM_Base_Start_IT( &htim1 );
HAL_GPIO_WritePin( GPIOA, GPIO_PIN_5, GPIO_PIN_SET );
}
else
{
hal_status = HAL_ERROR;
}
}
if ( hal_status != HAL_OK )
{
demo_error();
}
}
Modify the generated main.c
You also need to include the demo.h at the private includes section of the generated Core/Src/main.c file.
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "../../app/src/demo.h"
/* USER CODE END Includes */
Don't forget to call the demo() function from the main() in main.c.
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_TIM1_Init();
/* USER CODE BEGIN 2 */
demo();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
Extend the makefile
Finally we need to tell the build system to include the application source. In the generated
makefile append the app/src/demo.c
to the C_SOURCES.
Warning
Don't forget to add a trailing backslash (\) to the above line!
The test cases
Let's create some tests to try out our containerized test environment.
Add a new makefile for the tests
First we need a Makefile that describes how to build the test cases. Save it in the app/test
directory.
#Set this to @ to keep the makefile quiet
SILENCE = @
#---- Outputs ----#
COMPONENT_NAME = demo
TEST_TARGET = \
$(COMPONENT_NAME)_tests
#--- Inputs ----#
CPP_PLATFORM = Gcc
CPPUTEST_WARNINGFLAGS += -Wall
CPPUTEST_WARNINGFLAGS += -Wswitch-default
CPPUTEST_WARNINGFLAGS += -Wswitch-enum
CPPUTEST_CFLAGS += -Wextra
CPPUTEST_ADDITIONAL_CFLAGS = \
-DSTM32F103xB
CPPUTEST_CXXFLAGS += \
-DSTM32F103xB
INCLUDE_DIRS = \
$(CPPUTEST_HOME)/include/ \
../../Drivers/STM32F1xx_HAL_Driver/Inc \
../../Core/Inc \
../../Drivers/CMSIS/Device/ST/STM32F1xx/Include \
../../Drivers/CMSIS/Include
SRC_DIRS = \
../../app/src
TEST_SRC_DIRS = \
tests
MOCKS_SRC_DIRS = \
mocks
CPPUTEST_USE_EXTENSIONS = Y
ifeq "$(CPPUTEST_HOME)" ""
$(error The CPPUTEST_HOME env variable is not set.)
endif
include $(CPPUTEST_HOME)/build/MakefileWorker.mk
The test cases
Now we add the test cases. Create a file called demoTest.cpp
in the app/test/tests
directory.
Info
These tests are specific to the CppUTest framework and are built as C++.
#include <stdio.h>
#include <string.h>
#include "CppUTest/TestHarness.h"
#include "CppUTestExt/MockSupport.h"
extern "C"
{
/* Header for unit under test. */
#include "../../src/demo.h"
/* Headers for mocked functions. */
#include "stm32f1xx_hal.h"
TIM_HandleTypeDef htim1;
}
TEST_GROUP( demo_tests )
{
void setup ()
{
memset( &htim1, 0u, sizeof( htim1 ) );
}
void teardown()
{
mock().clear();
}
};
TEST( demo_tests, expect_start_of_timer )
{
mock( "stm32f1xx_hal_tim" )
.expectOneCall( "HAL_TIM_Base_Start_IT" )
.withParameter( "htim", &htim1 )
.andReturnValue( HAL_OK );
demo();
mock( "stm32f1xx_hal_tim" ).checkExpectations();
}
TEST( demo_tests, if_error_expect_led_on )
{
mock( "stm32f1xx_hal_tim" )
.expectOneCall( "HAL_TIM_Base_Start_IT" )
.withParameter( "htim", &htim1 )
.andReturnValue( HAL_ERROR );
mock( "stm32f1xx_hal_gpio" )
.expectOneCall( "HAL_GPIO_WritePin" )
.withParameter( "GPIOx", GPIOA )
.withParameter( "GPIO_Pin", GPIO_PIN_5 )
.withParameter( "PinState", GPIO_PIN_SET );
demo();
mock( "stm32f1xx_hal_tim" ).checkExpectations();
mock( "stm32f1xx_hal_gpio" ).checkExpectations();
}
TEST_GROUP( interrupt_cb_tests )
{
void teardown()
{
mock().clear();
}
};
TEST( interrupt_cb_tests, if_tim_called_with_not_htim1_do_nothing )
{
HAL_TIM_PeriodElapsedCallback( NULL );
}
TEST( interrupt_cb_tests, if_tim_called_with_htim1_toggle_led )
{
mock( "stm32f1xx_hal_gpio" )
.expectOneCall( "HAL_GPIO_TogglePin" )
.withParameter( "GPIOx", GPIOA )
.withParameter( "GPIO_Pin", GPIO_PIN_5 );
HAL_TIM_PeriodElapsedCallback( &htim1 );
mock( "stm32f1xx_hal_gpio" ).checkExpectations();
}
TEST( interrupt_cb_tests, if_exti_called_with_not_pin13_do_nothing )
{
HAL_GPIO_EXTI_Callback( 0 );
}
TEST( interrupt_cb_tests, if_exti_called_with_pin13_and_led_blinking_then_stop )
{
mock( "stm32f1xx_hal_tim" )
.expectOneCall( "HAL_TIM_Base_Stop_IT" )
.withParameter( "htim", &htim1 )
.andReturnValue( HAL_OK );
mock( "stm32f1xx_hal_gpio" )
.expectOneCall( "HAL_GPIO_WritePin" )
.withParameter( "GPIOx", GPIOA )
.withParameter( "GPIO_Pin", GPIO_PIN_5 )
.withParameter( "PinState", GPIO_PIN_RESET );
htim1.State = HAL_TIM_STATE_BUSY;
HAL_GPIO_EXTI_Callback( GPIO_PIN_13 );
mock( "stm32f1xx_hal_tim" ).checkExpectations();
mock( "stm32f1xx_hal_gpio" ).checkExpectations();
}
TEST( interrupt_cb_tests, if_exti_called_with_pin13_and_led_not_blinking_then_start )
{
TIM_TypeDef g_register_base;
memset( &g_register_base, 0u, sizeof( g_register_base ) );
mock( "stm32f1xx_hal_tim" )
.expectOneCall( "HAL_TIM_Base_Start_IT" )
.withParameter( "htim", &htim1 )
.andReturnValue( HAL_OK );
mock( "stm32f1xx_hal_gpio" )
.expectOneCall( "HAL_GPIO_WritePin" )
.withParameter( "GPIOx", GPIOA )
.withParameter( "GPIO_Pin", GPIO_PIN_5 )
.withParameter( "PinState", GPIO_PIN_SET );
htim1.State = HAL_TIM_STATE_READY;
htim1.Instance = &g_register_base;
/* The timer already run previously, so its counter value can be not 0.*/
htim1.Instance->CNT = 0x5A5A;
HAL_GPIO_EXTI_Callback( GPIO_PIN_13 );
mock( "stm32f1xx_hal_tim" ).checkExpectations();
mock( "stm32f1xx_hal_gpio" ).checkExpectations();
/* The timer must start the counting from 0u. */
CHECK_EQUAL( 0u, htim1.Instance->CNT );
}
TEST( interrupt_cb_tests, if_exti_called_and_tim_start_fails_expect_led_on )
{
TIM_TypeDef g_register_base;
memset( &g_register_base, 0u, sizeof( g_register_base ) );
mock( "stm32f1xx_hal_tim" )
.expectOneCall( "HAL_TIM_Base_Start_IT" )
.withParameter( "htim", &htim1 )
.andReturnValue( HAL_ERROR );
mock( "stm32f1xx_hal_gpio" )
.expectNCalls( 2u, "HAL_GPIO_WritePin" )
.withParameter( "GPIOx", GPIOA )
.withParameter( "GPIO_Pin", GPIO_PIN_5 )
.withParameter( "PinState", GPIO_PIN_SET );
htim1.State = HAL_TIM_STATE_READY;
htim1.Instance = &g_register_base;
HAL_GPIO_EXTI_Callback( GPIO_PIN_13 );
mock( "stm32f1xx_hal_tim" ).checkExpectations();
mock( "stm32f1xx_hal_gpio" ).checkExpectations();
}
TEST( interrupt_cb_tests, if_exti_called_and_tim_state_is_invalid_expect_led_on )
{
mock( "stm32f1xx_hal_gpio" )
.expectOneCall( "HAL_GPIO_WritePin" )
.withParameter( "GPIOx", GPIOA )
.withParameter( "GPIO_Pin", GPIO_PIN_5 )
.withParameter( "PinState", GPIO_PIN_SET );
htim1.State = HAL_TIM_STATE_ERROR;
HAL_GPIO_EXTI_Callback( GPIO_PIN_13 );
mock( "stm32f1xx_hal_gpio" ).checkExpectations();
}
Create the mocks
We would like to mock the HAL calls in the demo.c. A solution for this is creating our own mock
files: stm32f1xx_hal_gpioMock.cpp and stm32f1xx_hal_timMock.cpp. Place these files under the
app/test/mocks
directory.
#include <stdio.h>
#include "CppUTestExt/MockSupport.h"
extern "C"
{
#include "stm32f1xx_hal.h"
}
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
mock( "stm32f1xx_hal_gpio" )
.actualCall( "HAL_GPIO_WritePin" )
.withParameter( "GPIOx", GPIOx )
.withParameter( "GPIO_Pin", GPIO_Pin )
.withParameter( "PinState", PinState );
}
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
mock( "stm32f1xx_hal_gpio" )
.actualCall( "HAL_GPIO_TogglePin" )
.withParameter( "GPIOx", GPIOx )
.withParameter( "GPIO_Pin", GPIO_Pin );
}
#include <stdio.h>
#include "CppUTestExt/MockSupport.h"
extern "C"
{
#include "stm32f1xx_hal.h"
}
HAL_StatusTypeDef HAL_TIM_Base_Start_IT( TIM_HandleTypeDef * htim )
{
return (HAL_StatusTypeDef)mock( "stm32f1xx_hal_tim" )
.actualCall( "HAL_TIM_Base_Start_IT" )
.withParameter( "htim", htim )
.returnValue().getIntValue();
}
HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim)
{
return (HAL_StatusTypeDef)mock( "stm32f1xx_hal_tim" )
.actualCall( "HAL_TIM_Base_Stop_IT" )
.withParameter( "htim", htim )
.returnValue().getIntValue();
}
Add the test runner
Finally we need to add an entry point for the test runner. Create a file called AllTests.cpp
in
the app/test/tests
directory.
#include "CppUTest/CommandLineTestRunner.h"
int main(int argc, char** argv)
{
return CommandLineTestRunner::RunAllTests(argc, argv);
}
Expected app directory structure
app
├── src
│ ├── demo.c
│ └── demo.h
└── test
├── Makefile
├── mocks
│ ├── stm32f1xx_hal_gpioMock.cpp
│ └── stm32f1xx_hal_timMock.cpp
└── tests
├── AllTests.cpp
└── demoTest.cpp