...

воскресенье, 29 сентября 2019 г.

[Из песочницы] Локальный запуск юнит-тестов в STM32CubeIDE под Windows

Введение

Всем известна польза юнит-тестирования. Прежде всего, написание тестов одновременно с кодом позволяет раньше выявлять ошибки и не тратить впоследствии время на трудоемкую комплексную отладку. В случае embedded-разработки у юнит-тестирования есть особенности, связанные, во-первых, с тем, что код выполняется где-то глубоко в недрах устройства и взаимодействовать с ним довольно сложно, и, во-вторых, код сильно завязан на целевое железо.

Если в проекте есть фрагменты, не зависящие от аппаратуры и при этом реализующие достаточно сложную логику, для них применение модульных тестов даст наибольшую выгоду. Например, это может быть реализация какого-то протокола передачи данных, различные расчеты или управляющий конечный автомат.

Существует три способа запуска юнит-тестов для встраиваемых платформ:


  1. Запуск непосредственно на целевой платформе. В этом случае можно работать с аппаратурой устройства, и код будет работать точно так же, как и в боевых условиях. Однако для тестирования будет нужен физический доступ к устройству. Кроме того, цикл тестирования получится достаточно долгим из-за необходимости постоянно загружать код в устройство.
  2. Запуск на эмуляторе. Данный способ хорош в основном тем, что позволяет работать, даже когда целевая платформа недоступна (например потому, что ее еще не сделали). Недостатки – ограниченная точность воспроизведения поведения железа (и окружающего мира), а также трудность создания такого эмулятора.
  3. Запуск на хост-машине (локально). Не получится работать с аппаратурой (можно вместо этого использовать тестовые заглушки), зато тесты будут быстро запускаться и отрабатывать, и не нужен доступ к целевому устройству. Хороший пример для использования этого способа – тестирование реализации на микроконтроллере какого-нибудь вычислительного алгоритма, который сам по себе не зависит от аппаратуры, но использует данные датчиков устройства. Тестировать алгоритм с реальным источником данных будет очень неудобно, гораздо лучше один раз записать эти измерения и гонять тесты уже на сохраненных данных. Этот сценарий с локальным запуском тестов и будет рассматриваться далее.

В этой публикации приведен способ настройки юнит-тестов в среде STM32CubeIDE, основанной на Eclipse и предназначенной для разработки для контроллеров семейства STM32. Язык разработки – С, но сами тесты пишутся на С++. Тесты будут запускаться на хост-машине c Windows с использованием Cygwin. В качестве тестового фреймворка используется Google Test. Результаты будут отображаться в специальном окне плагина для юнит-тестирования, и их можно будет запустить одной кнопкой из проекта для STM32:

Описанный способ подойдет и для других сред разработки на основе Eclipse, если конечно добрые производители не слишком сильно их урезали в угоду удобству разработчиков. Также этот метод будет работать и c CubeIDE под Linux, при этом не потребуется возиться с Cygwin.


Вам понадобятся


  1. Cygwin 3.0.7 x86 (поскольку тесты для 32-битного микроконтроллера, будем и на 64-битной платформе использовать 32-битное окружение)
  2. STM32CubeIDE 1.0.2 для Windows.
  3. Google Test Framework 1.8.1

Установка Cygwin и STM32CubeIDE


Cygwin

Устанавливаем Cygwin, версия x86. В инсталляторе выбираем дополнительные пакеты: gcc-core, g++, binutils, automake, autoconf, cmake, libtool, gdb, make. Можно ставить последние стабильные версии пакетов.

Также нужно прописать переменные среды:

PATH: …;C:\<path_to_Cygwin>\Cygwin\bin; C:\<path_to_Cygwin>\Cygwin\lib
classpath: C:\<path_to_Cygwin>\Cygwin\lib


STM32CubeIDE

Среда устанавливается как обычно. Желательно ставить CubeIDE после Cygwin, потому что в этом случае Cube сам подхватит существующий Cygwin тулчейн.

Сначала создадим проект С++ для x86 Cygwin платформы. Он нам понадобится, чтобы, во-первых, проверить работоспособность тулчейна, а во-вторых, мы будем использовать его как «донора» конфигурации сборки для основного проекта.

Выбираем File > New > C/C++ Project. Выбираем C++ Managed Build. Создаем проект типа hello world для тулчейна Cygwin GCC:

Далее нужно будет выбрать, какие конфигурации сборки создавать. Достаточно только Debug.
Теперь можно проверить, что проект собирается, выбрав Project > Build All. Также желательно проверить и отладку под Cygwin, запустив Run > Debug As > Local C/C++ Application. Приложение выведет «Hello world» в консоль внутри CubeIDE.

Для того, чтобы отладчик мог показывать исполняемые строки в файлах исходного кода, нужно настроить отображение путей. В окне Window > Preferences во вкладке С/С++ > Debug нужно выбрать Source Lookup Path и добавить новое отображение: Add > Path Mapping. В окне нужно назвать как-нибудь новое отображение и добавить строчки для дисков, которые есть в системе:


  • \cygdrive\c — C:\
  • \cygdrive\g — G:\

Для красивого запуска тестов нам также понадобится плагин для Eclipse с поддержкой юнит-тестов для С++. Он ставится прямо из STM32CubeIDE: меню Help > Install New Software, далее выбрать репозиторий Eclipse Repository и установить плагин С/С++ Unit Testing Support.


Сборка библиотеки Google Test

Исходный код библиотеки можно взять по ссылке: https://github.com/google/googletest/tree/release-1.8.1
Распаковываем исходники, заходим в директорию googletest-release-1.8.1 с помощью Cygwin terminal, и запускаем:

cmake .
make

После успешной сборки файл статической библиотеки будет лежать в ./googlemock/lib/libgtest.a, а заголовочные файлы будут находиться в каталоге ./googletest/include/gtest/. Их нужно будет скопировать в наш проект (или прописать путь к этим файлам в настройках проекта).


Создание проекта для STM32

Проект для отладочной платы STM32L476G-DISCO. Пример будет не слишком изощренным – на плате есть два светодиода, пусть показывают двоичный счетчик от 00 до 11. Реализуем для счетчика отдельный модуль, описанный в паре .h и .c файлов, и напишем для него тест.
Проект можно создавать как обычно, с помощью конфигуратора Cube, главное убедиться, что выводы PB2 и PE8 настроены как цифровые выходы. При создании проекта лучше будет указать тип – С++, это понадобится для компиляции тестов (основной код будет по-прежнему компилироваться С-компилятором). Сконвертировать проект из C можно будет и позже, нажав на название проекта ПКМ и выбрав «Convert to C++».

Для компиляции под МК и для тестов нам понадобятся две разные конфигурации сборки. В этих конфигурациях будут собираться разные наборы файлов – в основную попадут модули для работы с железом и тестируемые модули, а в тестовую – те же тестируемые модули и файлы тестов. Поэтому создадим в корне проекта разные каталоги – Application c кодом приложения для МК (можно просто переименовать директорию Src, которую создал Cube), Common для модулей, не зависящих от железа (которые мы будем тестировать) и Tests для тестов. Директории можно исключать из сборки, кликнув ПКМ по их названию, меню Resource Configuration > Exclude from build.

Добавим в каталог Common наш модуль счетчика:


Код led_counter

(led_counter.h):

#ifndef LED_COUNTER_H_
#define LED_COUNTER_H_

#include <stdint.h>

void Led_Counter_Init();
uint8_t Led_Counter_Get_Next();

#endif /* LED_COUNTER_H_ */

led_counter.cpp:

#include "led_counter.h"

static uint8_t led_cnt_state = 0;

void Led_Counter_Init()
{
    led_cnt_state = 0;
}

uint8_t Led_Counter_Get_Next()
{
    if(++led_cnt_state > 3)
        led_cnt_state = 0;
    return led_cnt_state;
}

Директории Common и Tests нужно добавить в путь поиска include-файлов: свойства проекта (Properties) > С/С++ General > Paths and Symbols > Includes.

Добавим в main работу со светодиодами


Фрагмент main.c

main.c:

…
/* USER CODE BEGIN Includes */
#include "led_counter.h"
/* USER CODE END Includes */
…
int main(void)
{
…
    /* USER CODE BEGIN WHILE */
    Led_Counter_Init();
    uint8_t led_state = 0;
    while (1)
    {
        /* USER CODE END WHILE */

        /* USER CODE BEGIN 3 */
        led_state = Led_Counter_Get_Next();
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, led_state & (1<<0));
        HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, led_state & (1<<1));
        HAL_Delay(500);
    }
    /* USER CODE END 3 */
…
}

Проект должен компилироваться и запускаться, а светодиоды – мигать.


Написание тестов

Теперь то, ради чего все затевалось.

Создадим новую конфигурацию сборки через свойства проекта – Properties > C/C++ Build > Settings > Manage Configurations. CubeIDE просто так не даст создать конфигурацию для сборки под Cygwin, поэтому скопируем ее из проекта, который мы создали ранее:

Теперь нужно переключиться на эту конфигурацию и настроить пути к файлам исходников и заголовочным файлам. В свойствах проекта во вкладке Paths and Symbols прописываем (при добавлении записи лучше ставить галку в поле «add to all languages»):


  • Includes – Tests/Inc, Common/Inc
  • Libraries – gtest
  • Library Paths – Tests/Lib
  • Source Location — /<prj_name>/Common и /<prj_name>/Tests (заменить <prj_name> на имя проекта)

Далее копируем в проект библиотеку gtest – файл .a в директорию Tests/Lib, а заголовочные файлы в папке gtest – в папку Tests/Inc. В папке Tests создаем новый файл main.cpp, в котором будут запускаться тесты. Его содержимое стандартное:

main.cpp:

/*
 * Unit tests main file
 */

#include "gtest/gtest.h"

int main(int argc, char *argv[])
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Также для проверки работы сетапа создадим один тест, который будет проверять, что в нашем окружении размер указателя 32 бита (мы хотим убедиться, что он такой же, как и на микроконтроллере, для этого мы ставили 32-битный Cygwin).

Создаем такой файл теста test_platform.cpp:

#include "gtest/gtest.h"

TEST(PlatformTest, TestPointerSize)
{
    //Check pointer size is 32 bit
    ASSERT_EQ(sizeof(void*)*8, 32U);
}

Теперь, если проект запустить как обычное С++ Application, в отладочном выводе будет сообщение от Google Test о том, что все тесты пройдены.

Структура проекта должна иметь примерно такой вид:

Теперь напишем тесты для нашего модуля светодиодного счетчика. Файлы тестов можно расположить в папке Tests:


test_led_counter.cpp
#include "gtest/gtest.h"

extern "C" {
#include "led_counter.h"
}

// Test fixture
class LedCounterTest: public ::testing::Test
{
protected:
    void SetUp()
    {
        Led_Counter_Init();
    }
};

// Check initial value
TEST_F(LedCounterTest, TestInitialValue)
{
    Led_Counter_Init();
    ASSERT_EQ(Led_Counter_Get_Next(), 1);
}

// Check how value is incremented
TEST_F(LedCounterTest, TestIncrementValue)
{
    Led_Counter_Init();
    unsigned int val = Led_Counter_Get_Next();
    for(int i=0;i<1;i++)
    {
        ASSERT_EQ(Led_Counter_Get_Next(), ++val);
    }
}

// Check how value return to 0 after 3
TEST_F(LedCounterTest, TestZeroCrossing)
{
    Led_Counter_Init();
    for(int i=0;i<3;i++)
    {
        Led_Counter_Get_Next();
    }
    ASSERT_EQ(Led_Counter_Get_Next(), 0);
}

Чтобы результаты тестов отображались в красивом окошке, нужно создать новую конфигурацию запуска в меню Run > Debug Configurations. Установленный плагин позволяет создавать конфигурации типа C/C++ Unit. Создадим ее, назовем Run Tests, выберем используемую конфигурацию сборки «Test» и снимем галку «stop on startup at» на вкладке Debugger. После этого конфигурацию можно запустить.

Для появления окна с результатами его нужно выбрать в Window > Show View > Other > C/C++ > C/C++ Unit.

Готово! Теперь проект можно компилировать и запускать под целевой МК как обычно. Когда нужно будет запустить локальные тесты, при запуске конфигурации Run Tests проект автоматически будет пересобран под x86, среда выполнит тесты и покажет результат.


Литература


  1. J. Grenning. Test-Driven Development for Embedded C. – фундаментальный труд про модульное тестирование embedded-систем и про применение методологии TDD.
  2. https://uncannier.com/unit-testing-of-embedded-firmware-part-1-software-confucius/ — Unit-тестирование микроконтроллерного кода на x86 в Code Composer Studio от Texas Instruments, фреймворк CppUTest
  3. http://blog.atollic.com/why-running-your-embedded-arm-cortex-code-on-a-host-pc-is-a-good-thing — статья о том, почему может быть полезно запускать код для микроконтроллера на десктопной платформе

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий