TDD для микроконтроллеров. Часть 1: Первый полет +16

Программирование микроконтроллеров, Информационная безопасность, Производство и разработка электроники, Реверс-инжиниринг, TDD, Блог компании НТЦ Вулкан

Рекомендация: подборка платных и бесплатных курсов таргетированной рекламе - https://katalog-kursov.ru/


Встраиваемые системы широко применяются в бытовой электронике, промышленной автоматике, транспортной инфраструктуре, телекоммуникациях, медицинском оборудовании, а также в военной, аэрокосмической технике и т. д. Хотя последствия любой ошибки проектирования обходятся дорого, ошибку в ПО для ПК или в большом корпоративном приложении обычно относительно легко исправить. А если дефект будет во встраиваемом ПО (далее – ВПО) электронного блока управления тормозной системой автомобиля, то это может вызвать массовый и дорогостоящий отзыв продукции.


Сфера применения встраиваемых систем постоянно расширяется, сложность выполняемых ими задач растет. Это в свою очередь повышает риск внесения ошибок в процессе разработки, что увеличивает вероятность весьма дорогостоящих дефектов в ПО.


Одной из наиболее популярных методологий улучшения качества разрабатываемых приложений является Test-driven development (TDD). Но эффективна ли методология TDD для разработки встраиваемых систем? Ответ на этот вопрос будем искать под катом.


Эффективность TDD


Все большее число разработчиков придерживаются мнения, что методология TDD имеет ряд преимуществ над Test-last development (TLD). При этом TDD понимается как процесс итеративного, непрерывного написания тестов и рабочего кода с обязательными фазами рефакторинга.



Схема итеративного процесса разработки TDD


В результате применения TDD можно выделить следующие улучшения при проектировании приложений:


  • TDD позволяет сконцентрироваться на дизайне и понять, как сделать его лучше;
  • TDD избавляет разработчиков от страха внесения изменений в код: при внесении некорректных изменений будет легко «отловить» ошибку с помощью запуска тестов;
  • хорошо спроектированные тесты выступают в роли показательного примера использования программного модуля, что заменяет документирование кода;
  • TDD улучшает покрытие кода тестами;
  • завершенные тесты демонстрируют прогресс разработки: каждый реализованный тест-кейс указывает на то, что очередной функционал завершен и работает;
  • уменьшается количество багов.

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


Однако среды разработки для создания ВПО микроконтроллеров (далее – МК) не так стремительно развиваются, в некоторых из них отсутствует возможность создавать и запускать тесты, а также быстро получать результат их выполнения. К тому же один и тот же разработчик может иметь дело с разными аппаратными платформами, т. е. вести разработку с помощью разных интегрированных сред разработки (Integrated Development Environment – IDE). Поэтому возникают следующие вопросы:


Как запускать тесты для embedded, будь то TLD или TDD?

Будем ли мы наблюдать указанные выше улучшения в результате применения TDD при разработке встраиваемых систем?

На эти вопросы мы попытаемся ответить в данной статье.


Особенности разработки в embedded


При проектировании встраиваемых систем необходимо учитывать специфику разработки ВПО:


  • ВПО запускается на МК, у которых могут быть ограниченный объем памяти, своя архитектура и т. д.;
  • ВПО выполняется в среде со специфичной аппаратной поддержкой, т. е. имеет множество библиотек для взаимодействия с различными аппаратными модулями.

Как правило, для проектирования ВПО используется специализированная IDE. Обычно разработчик может загрузить с сайта производителя библиотеки для работы с аппаратными модулями конкретного МК – Hardware Abstraction Level (HAL). Но далеко не каждая IDE предоставляет инструменты для написания тестов ВПО. Кроме того, использование библиотек или написание собственных драйверов для взаимодействия с периферией МК вносит аппаратные зависимости в разрабатываемый код прошивки. Такой код будет работать только на конкретном МК (или на определенной серии МК).


Так, для применения TDD при проектировании встраиваемых систем нужно ответить на ряд вопросов:


  1. Каким образом писать и запускать тесты?
  2. Что делать с аппаратными зависимостями?
  3. Как организовать непрерывную и итеративную разработку?

Мы попробуем ответить на эти вопросы, реализовав конкретный пример ВПО для МК с помощью методологии TDD. В завершение мы приведем плюсы и минусы применения этой методологии для разработки встраиваемых систем. Конечно, ответы на все эти вопросы не будут рассматриваться в рамках одной статьи, поэтому мы запланировали к публикации небольшой цикл статей.


Планируемый цикл статей


  1. В первой части (вы сейчас ее читаете) мы определим цель и инструменты разработки, затем напишем простейший тест, запустим его и представим результат.
  2. Во второй части рассмотрим процесс разработки ВПО по методологии TDD, реализуем основную платформонезависимую логику нашего проекта и применим методы для разрешения аппаратных зависимостей с целью тестирования нашего кода.
  3. В третьей части допишем платформозависимый код (драйвер) и запустим ВПО на МК STM32F103C8, подведем итоги, учитывая материалы всего цикла статей, и перечислим плюсы и минусы применения TDD при разработке ВПО для МК.

Все исходники проекта выложены на GitLab.


Разрабатываемый функционал


Многие embedded-устройства могут подключаться к ПК для конфигурирования каких-либо параметров, т. е. в таком устройстве содержатся настройки, которые можно считывать или записывать. Мы приведем пример реализации именно такого функционала. Для подключения к ПК будем использовать UART-интерфейс, а в качестве энергонезависимой памяти – флеш-память МК. Таким образом, нам необходимо реализовать следующий функционал:


  • подключение устройства к ПК по UART-интерфейсу;
  • сохранение параметров в энергонезависимой памяти с помощью ПК по UART-интерфейсу;
  • считывание параметров из энергонезависимой памяти с помощью ПК по UART-интерфейсу.

Выбор аппаратной платформы


Для реализации нашего проекта мы выбрали отладочную плату с МК STM32F103C8, потому что МК STM32 одни из самых популярных в настоящее время, а отладочная плата стоит недорого и ее легко приобрести.



В качестве энергонезависимой памяти в выбранном МК может быть использована флеш-память. Однако следует помнить о том, что код ВПО также хранится во флеш-памяти, которая разделена на страницы. Количество и размер страниц варьируется в зависимости от линейки МК (подробно описано в Programming manual).
Перед записью во флеш-память необходимо убедиться, что страница была предварительно стерта.


Инструменты разработки


Для создания тестов и основной логики проекта мы выбирали IDE на свой вкус и цвет, потому что в первую очередь разрабатывали платформонезависимый код, который можно скомпилировать и запустить на локальном ПК. Для разработки ВПО чаще всего используется либо «чистый» C, либо С++, поэтому для написания тестов ВПО нужно использовать соответствующий фреймворк для тестирования. В результате мы выбрали следующие инструменты для написания тестов и платформонезависимой бизнес-логики:


  1. В качестве IDE – Visual Studio, потому что нам нравится ее внешний вид, удобство отладки и рефакторинга кода. Данная IDE также подходит для написания кода на «чистом» C.
  2. CppUTest – простой в настройке и в освоении фреймворк для модульного тестирования, который может использоваться для написания любых unit-тестов на C/C++.

Создание и настройка проекта в Visual Studio


С целью написания тестов и кода нашей бизнес-логики в первую очередь мы создали новое решение в Visual Studio, добавили в него первый проект на Visual C++ с именем проекта Tests и типом «консольное приложение Windows». В этом проекте содержатся только код тестов и дополнительные программные модули для тестирования (например, spies, mocks, stubs и т. д.).


Настраиваем проект Tests
  1. Заходим в Properties -> C/C++ -> General -> Additional Include Directories и добавляем строки:
    — $(CPP_U_TEST)\include
    — $(SolutionDir)..\Firmware\Project\Include (путь к заголовочным файлам тестируемого кода)

  2. Заходим в Properties -> Linker -> Input и добавляем строки:
    — $(CPP_U_TEST)\lib\cpputestd.lib
    — $(SolutionDir)Debug\ProductionCodeLib.lib

    Где $(CPP_U_TEST) – переменная среды Windows, в которой содержится путь к папке cpputest (см. скриншот).

    Добавляем в проект файл Tests.cpp с содержанием:

#include "CppUTest/CommandLineTestRunner.h"
int main(int argc, char** argv)
{
    return RUN_ALL_TESTS(argc, argv);
}


Далее в этом же решении создали второй проект с именем ProductionCodeLib, тип – статическая библиотека Visual C++. В этот проект мы будем добавлять код бизнес-логики, который планируем запустить на «железе», т. е. код, компилируемый в файл прошивки для STM32F103C8.


Настраиваем проект ProductionCodeLib

Добавляем пути к заголовочным файлам, используемым для создания ВПО:


  • заходим в Properties -> C/C++ -> General -> Additional Include Directories и добавляем строку \$(SolutionDir)..\Firmware\Project\Include\

После настройки впервые запустили проект, нажав на кнопку «Run», и увидели отчет о том, что ни одного теста не было выполнено:


OK (0 tests, 0 ran, 0 checks, 0 ignored, 0 filtered out, 0 ms)


На этом настройка завершилась, можно приступить к итеративной разработке.


Разработка ВПО по методологии TDD


Мы решили использовать «чистый» C, при этом старались сохранить применение базовых принципов ООП. Такой подход обычно называют псевдо-ООП, потому что «чистый» С не поддерживает классы. В соответствии с целью нашего проекта мы создали класс Configurator, в котором реализовали следующую логику:


  • обработка команд ПК по UART-интерфейсу;
  • чтение/запись во флеш-память;
  • стирание страницы флеш-памяти.

Конечно, в первую очередь мы создавали список тестов для будущего класса. Для этого брали блокнот и ручку (клавиатуру и текстовый редактор) и описывали простыми словами, какая логика нам была нужна. Такой процесс для нашего модуля занял около 5 минут. Ниже приведен тест-лист для класса Configurator.


Тест-лист


1. При получении команды read возвращаются данные, размещенные по указанному адресу на флеш-памяти.
2. При получении команды write производится запись данных по указанному адресу во флеш-память.
3. При получении команды erase производится стирание страницы с указанным номером.
4. При получении команды help выводится список поддерживаемых команд.
5. При получении неизвестной команды возвращается сообщение об ошибке.


Для простоты и наглядности мы решили использовать строковый формат команд в кодировке ASCII.


Основы CppUTest и первый тест


Для реализации тестов создали файл ConfiguratorTests.cpp в проекте Tests, который затем постепенно наполняли новыми тестами в соответствии с методологией TDD.


Для написания тестов с помощью CppUTest используется простая структура.


Структура написания тестов для CppUTest:


TEST_GROUP(TestGroupName)
{
    void setup()
    {
    }

    void teardown()
    {
    }
};

TEST(TestGroupName, TestName)
{
}

Где:


  • TEST_GROUP – блок кода, в котором могут содержаться методы setup() и teardown(), а также другие вспомогательные методы или переменные;
  • setup() – функция, вызываемая перед запуском каждого теста;
  • teardown() – функция, вызываемая в завершении каждого теста;
  • TEST – блок кода, в котором реализуется один тест, таких тестов может быть много;
  • TestGroupName – название группы тестов обычно совпадает с именем класса, для которого предназначены тесты;
  • TestName – название теста.

Каждый тест должен работать независимо от любых других тестов. Поэтому перед каждым запуском теста следует создавать объект, а в завершение теста удалять его. Так, для нашего класса Configurator в простейшем случае в setup() создается экземпляр, а в teardown() удаляется. Чтобы убедиться в том, что объект успешно создается, мы добавили простейший тест для проверки значения указателя. Если объект по каким-то причинам не был создан, то указатель будет равен значению NULL. Назвали тест ShouldNotBeNull.


Реализация теста ShouldNotBeNull:


// ConfiguratorTests.cpp
TEST_GROUP(Configurator)
{
    Configurator * configurator = NULL;
    void setup()
    {
        configurator = Configurator_Create();
    }
    void teardown()
    {
        Configurator_Destroy(configurator);
    }
};

TEST(Configurator, ShouldNotBeNull)
{
    CHECK_TRUE(configurator);
}

Первый тест был готов, но еще возвращал ошибки компиляции, потому что на данном этапе не были реализованы методы Configurator_Create и Configurator_Destroy. Для успешного завершения теста оставалось написать лишь эти два метода. И только на этом шаге мы написали первые строчки с реализацией функционала ВПО в проекте ProductionCodeLib. Для этого создали заголовочный файл Configurator.h и файл Configurator.c, в котором содержится реализация бизнес-логики. В Configurator.h добавили прототипы двух перечисленных методов. А в файл Configurator.c сначала добавили заглушки, т. е. оставили тело каждого метода пустым. Это было нужно для того, чтобы скомпилировать проект и запустить тесты.


Реализация заглушек для теста ShouldNotBeNull:


// Configurator.h
typedef struct ConfiguratorStruct Configurator;
Configurator * Configurator_Create(void);

void Configurator_Destroy(Configurator * self);

// Configurator.c
#include "Configurator.h"

typedef struct ConfiguratorStruct
{
    char command[32];
} ConfiguratorStruct;

Configurator * Configurator_Create(void)
{
    return NULL;
}

void Configurator_Destroy(Configurator * self)
{
}

В соответствии с методологией TDD следует убедиться, что тест запускается, но завершается с ошибкой (т. к. тело метода Configurator_Create на данный момент было пустым). Пробуем запустить и получаем статус выполнения теста failed, что и следовало ожидать. Это означает, что мы успешно выполнили фазу test-fails.


Вывод ошибки на экран при запуске теста:


d:\\exampletdd\\tests\\tests\\configuratortests.cpp(27): error: Failure in TEST(Configurator, ShouldNotBeNull)
CHECK_TRUE(configurator) failed
.
Errors (1 failures, 1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 2 ms)

Для перехода на следующую фазу test-passes необходимо было заполнить тело конструктора. Мы добавили в метод Configurator_Create выделение памяти и возврат указателя на объект, этого достаточно для успешного выполнения теста ShouldNotBeNull. Также следовало освободить выделенную память в завершение теста, поэтому заполнили тело деструктора Configurator_Destroy.


В итоге Configurator_Create и Configurator_Destroy выглядят так:


Configurator * Configurator_Create(void)
{
    Configurator * self = (Configurator*)calloc(1, sizeof(ConfiguratorStruct));
    return self;
}

void Configurator_Destroy(Configurator * self)
{
    if (self == NULL)
    {
        return;
    }
    free(self);
    self = NULL;
}

В результате запустили тест и получили положительный результат:


.
OK (1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 0 ms)

Это означает, что фаза test-passes завершилась. Далее следует фаза рефакторинга, в которой, как правило, производится улучшение дизайна, читаемости кода и т. д. В нашем случае кода еще совсем мало, поэтому мы только заменили «магическое» число 32 на константу с помощью #define (можно использовать enum или const вместо define).


Убираем антипаттерн («магическое» число) с помощью #define:


// Configurator.h

#define SERIAL_RECEIVE_BUFFER_SIZE 32

// Configurator.c
typedef struct ConfiguratorStruct
{
    char command[SERIAL_RECEIVE_BUFFER_SIZE];
} ConfiguratorStruct;

Итог


Подведем промежуточные итоги опыта, описанного в этой статье:


  1. Мы определили цели проекта, выбрали аппаратную платформу и инструменты для проектирования.
  2. Подготовили ПК для локальной разработки тестов и кода ВПО без применения специализированных IDE для конкретного МК.
  3. Создали простейший тест, для запуска которого достаточно нажать на кнопку «Run» (или соответствующую горячую клавишу), чтобы мгновенно получить результат выполнения на экране.

В следующей статье мы напишем всю платформонезависимую логику нашего проекта по методологии TDD в соответствии с разработанным выше тест-листом. Если тебе интересны вопросы «железной» разработки и безопасного кода, присоединяйся к нашей команде. Так что продолжение следует…


Дополнительная информация


Raccoon Security – специальная команда экспертов НТЦ «Вулкан» в области практической информационной безопасности, криптографии, схемотехники, обратной разработки и создания низкоуровневого программного обеспечения.




К сожалению, не доступен сервер mySQL