Встраиваемые системы широко применяются в бытовой электронике, промышленной автоматике, транспортной инфраструктуре, телекоммуникациях, медицинском оборудовании, а также в военной, аэрокосмической технике и т. д. Хотя последствия любой ошибки проектирования обходятся дорого, ошибку в ПО для ПК или в большом корпоративном приложении обычно относительно легко исправить. А если дефект будет во встраиваемом ПО (далее – ВПО) электронного блока управления тормозной системой автомобиля, то это может вызвать массовый и дорогостоящий отзыв продукции.
Сфера применения встраиваемых систем постоянно расширяется, сложность выполняемых ими задач растет. Это в свою очередь повышает риск внесения ошибок в процессе разработки, что увеличивает вероятность весьма дорогостоящих дефектов в ПО.
Одной из наиболее популярных методологий улучшения качества разрабатываемых приложений является Test-driven development (TDD). Но эффективна ли методология TDD для разработки встраиваемых систем? Ответ на этот вопрос будем искать под катом.
Все большее число разработчиков придерживаются мнения, что методология TDD имеет ряд преимуществ над Test-last development (TLD). При этом TDD понимается как процесс итеративного, непрерывного написания тестов и рабочего кода с обязательными фазами рефакторинга.
Схема итеративного процесса разработки TDD
В результате применения TDD можно выделить следующие улучшения при проектировании приложений:
В настоящее время довольно просто писать тесты. Многие среды разработки позволяют добавлять тест в проект за пару кликов мыши или с помощью сочетания клавиш, что экономит немало времени, разработчику остается только самостоятельно заполнить тело теста. При этом не нужно тратить время на настройку среды разработки или скачивание дополнительных фреймворков и их подключение. Например, в Android Studio это довольно простой и быстрый процесс.
Однако среды разработки для создания ВПО микроконтроллеров (далее – МК) не так стремительно развиваются, в некоторых из них отсутствует возможность создавать и запускать тесты, а также быстро получать результат их выполнения. К тому же один и тот же разработчик может иметь дело с разными аппаратными платформами, т. е. вести разработку с помощью разных интегрированных сред разработки (Integrated Development Environment – IDE). Поэтому возникают следующие вопросы:
Как запускать тесты для embedded, будь то TLD или TDD?
Будем ли мы наблюдать указанные выше улучшения в результате применения TDD при разработке встраиваемых систем?
На эти вопросы мы попытаемся ответить в данной статье.
При проектировании встраиваемых систем необходимо учитывать специфику разработки ВПО:
Как правило, для проектирования ВПО используется специализированная IDE. Обычно разработчик может загрузить с сайта производителя библиотеки для работы с аппаратными модулями конкретного МК – Hardware Abstraction Level (HAL). Но далеко не каждая IDE предоставляет инструменты для написания тестов ВПО. Кроме того, использование библиотек или написание собственных драйверов для взаимодействия с периферией МК вносит аппаратные зависимости в разрабатываемый код прошивки. Такой код будет работать только на конкретном МК (или на определенной серии МК).
Так, для применения TDD при проектировании встраиваемых систем нужно ответить на ряд вопросов:
Мы попробуем ответить на эти вопросы, реализовав конкретный пример ВПО для МК с помощью методологии TDD. В завершение мы приведем плюсы и минусы применения этой методологии для разработки встраиваемых систем. Конечно, ответы на все эти вопросы не будут рассматриваться в рамках одной статьи, поэтому мы запланировали к публикации небольшой цикл статей.
Планируемый цикл статей
Все исходники проекта выложены на GitLab.
Многие embedded-устройства могут подключаться к ПК для конфигурирования каких-либо параметров, т. е. в таком устройстве содержатся настройки, которые можно считывать или записывать. Мы приведем пример реализации именно такого функционала. Для подключения к ПК будем использовать UART-интерфейс, а в качестве энергонезависимой памяти – флеш-память МК. Таким образом, нам необходимо реализовать следующий функционал:
Для реализации нашего проекта мы выбрали отладочную плату с МК STM32F103C8, потому что МК STM32 одни из самых популярных в настоящее время, а отладочная плата стоит недорого и ее легко приобрести.
В качестве энергонезависимой памяти в выбранном МК может быть использована флеш-память. Однако следует помнить о том, что код ВПО также хранится во флеш-памяти, которая разделена на страницы. Количество и размер страниц варьируется в зависимости от линейки МК (подробно описано в Programming manual).
Перед записью во флеш-память необходимо убедиться, что страница была предварительно стерта.
Для создания тестов и основной логики проекта мы выбирали IDE на свой вкус и цвет, потому что в первую очередь разрабатывали платформонезависимый код, который можно скомпилировать и запустить на локальном ПК. Для разработки ВПО чаще всего используется либо «чистый» C, либо С++, поэтому для написания тестов ВПО нужно использовать соответствующий фреймворк для тестирования. В результате мы выбрали следующие инструменты для написания тестов и платформонезависимой бизнес-логики:
С целью написания тестов и кода нашей бизнес-логики в первую очередь мы создали новое решение в Visual Studio, добавили в него первый проект на Visual C++ с именем проекта Tests и типом «консольное приложение Windows». В этом проекте содержатся только код тестов и дополнительные программные модули для тестирования (например, spies, mocks, stubs и т. д.).
#include "CppUTest/CommandLineTestRunner.h"
int main(int argc, char** argv)
{
return RUN_ALL_TESTS(argc, argv);
}
Далее в этом же решении создали второй проект с именем ProductionCodeLib, тип – статическая библиотека Visual C++. В этот проект мы будем добавлять код бизнес-логики, который планируем запустить на «железе», т. е. код, компилируемый в файл прошивки для STM32F103C8.
Добавляем пути к заголовочным файлам, используемым для создания ВПО:
После настройки впервые запустили проект, нажав на кнопку «Run», и увидели отчет о том, что ни одного теста не было выполнено:
OK (0 tests, 0 ran, 0 checks, 0 ignored, 0 filtered out, 0 ms)
На этом настройка завершилась, можно приступить к итеративной разработке.
Мы решили использовать «чистый» C, при этом старались сохранить применение базовых принципов ООП. Такой подход обычно называют псевдо-ООП, потому что «чистый» С не поддерживает классы. В соответствии с целью нашего проекта мы создали класс Configurator
, в котором реализовали следующую логику:
Конечно, в первую очередь мы создавали список тестов для будущего класса. Для этого брали блокнот и ручку (клавиатуру и текстовый редактор) и описывали простыми словами, какая логика нам была нужна. Такой процесс для нашего модуля занял около 5 минут. Ниже приведен тест-лист для класса Configurator.
1. При получении команды read
возвращаются данные, размещенные по указанному адресу на флеш-памяти.
2. При получении команды write
производится запись данных по указанному адресу во флеш-память.
3. При получении команды erase
производится стирание страницы с указанным номером.
4. При получении команды help
выводится список поддерживаемых команд.
5. При получении неизвестной команды
возвращается сообщение об ошибке.
Для простоты и наглядности мы решили использовать строковый формат команд в кодировке ASCII.
Для реализации тестов создали файл ConfiguratorTests.cpp
в проекте Tests
, который затем постепенно наполняли новыми тестами в соответствии с методологией TDD.
Для написания тестов с помощью CppUTest
используется простая структура.
Структура написания тестов для CppUTest:
TEST_GROUP(TestGroupName)
{
void setup()
{
}
void 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;
Подведем промежуточные итоги опыта, описанного в этой статье:
В следующей статье мы напишем всю платформонезависимую логику нашего проекта по методологии TDD в соответствии с разработанным выше тест-листом. Если тебе интересны вопросы «железной» разработки и безопасного кода, присоединяйся к нашей команде. Так что продолжение следует…
Ссылки
Литература
Raccoon Security – специальная команда экспертов НТЦ «Вулкан» в области практической информационной безопасности, криптографии, схемотехники, обратной разработки и создания низкоуровневого программного обеспечения.
К сожалению, не доступен сервер mySQL