Статья
Версия для печати
Обсудить на форуме
Многозадачность во встроенном приложении

© Dale, 04.01.2012 — 06.01.2012.

Начало: часть 1, часть 2, часть 3.

Часть 4

Оглавление


Совершенствуем объектную модель на языке C

Разработка встроенного кода в наших проектах до сих пор проходила по такой схеме: сначала строилась объектная модель, документировалась в виде диаграмм UML, а затем воплощалась в коде на языке C. Объектно-ориентированный подход к проектированию при правильном использовании позволяет получить высокое качество проекта. Однако язык программирования C, который мы используем для создания кода для микроконтроллеров, не является объектно-ориентированным, поэтому нам приходилось использовать некоторые ухищрения для имитации объектных средств. Так, например, вместо классов мы использовали модули, вместо методов — функции с именами вида «ИмяМодуля_имяФункции», роль интерфейсов у нас выполняли заголовочные файлы и т.д. Механизм инкапсуляции имитировался посредством видимости «закрытых» членов в пределах файла. Вплоть до настоящего момента такой подход достаточно хорошо работал: мы успешно выполнили на его основе простейший завершенный проект и начали другой, который до сих пор также продвигался успешно.
Однако я неспроста сделал акцент на словах «простейший» и «до сих пор». Вследствие этой простоты нам не требовалось несколько экземпляров «объектов», вполне хватало одного (один канал таймера, один ключ управления источником света и т.д.). Если продолжить параллель с объектной парадигмой, мы научились работать со статическими членами класса (фактически реализовали паттерн проектирования Monostate). Но если мы ограничимся лишь таким примитивным уровнем, все наши усилия, направленные на получение качественного кода посредством TDD, пропадут впустую: несколько строк незатейливого кода можно было бы отладить и обычным методом «коекакинга», а путь к более сложным устройствам для нас будет закрыт.
В спецификациях нашего текущего проекта указано, что устройство должно иметь два независимых канала, которые ведут себя сходным образом, но имеют различные количественные характеристики. Конечно, мы могли бы продублировать код методом Copy-Paste, а затем внести необходимые поправки; полученный в результате этих действий код, возможно, даже работал бы некоторое время (до первой исправленной ошибки или модификации кода, которая будет внесена в одну из копий кода, в то время как другая окажется благополучно забытой)... Ну, или по крайней мере создавал бы видимость работы. Впрочем, раз уж мы взяли курс на разработку качественного кода, этот вариант нам никак не годится. Вот если бы мы могли создавать несколько экземпляров наших «объектов», и каждый из них мог иметь собственное состояние, сохраняя при этом общее для класса поведение...
В общем-то в этом пожелании нет ничего нереального; вспомним, что в том же C++ class — это фактически синоним struct, а структуры у нас в C имеются. Структура вполне пригодна для хранения состояния объекта, то есть набора значений его членов-переменных. Правда, есть одна проблема: в C отсутствуют средства управления видимостью полей структуры. Область видимости поля структуры совпадает с областью видимости самой структуры; иными словами, если в структуре есть некоторое поле и эта структура доступна нам в некоторой точке программы, то и само поле также является полностью доступным. Получается, об инкапсуляции членов-переменных можно забыть, поскольку все они фактически имеют видимость public? Это очень плохая новость, поскольку без инкапсуляции сам объектный подход полностью утрачивает для нас привлекательность: объект, который не в состоянии гарантировать собственную целостность, не может быть основой для качественного кода.
Не знаю, как вам, уважаемые читатели, но мне очень не хотелось бы бросать объектно-ориентированное проектирование встроенных систем, не успев завершить даже столь скромный проект, и возвращаться к старой структурной парадигме. Это было бы большим шагом назад. Поэтому перед тем, как двинуться дальше в разработке проекта, я сделаю небольшую паузу для усовершенствования методики объектного программирования на языке C.

Постановка задачи

Прежде всего наметим цель. Понятно, что реализовать полную объектную парадигму средствами процедурного языка вообще и C в частности — задача совершенно нереальная, поскольку в противном случае попросту не было бы необходимости реализовывать объектные расширения процедурных языков, которые в конечном итоге развивались в самостоятельные языки (например, Object Pascal из Pascal или C++ из C).
Разумеется, мы на такие масштабы замахиваться не будем. Поэтому поставим более скромную задачу, решение которой не будет непосильным и не отнимет много времени, но в то же время даст пригодный ля практических нужд результат.
Как уже было упомянуто выше, класс C++ выглядит до определенной степени сходно со структурой C. И класс, и структура имеют набор переменных-полей (членов), совокупность значений которых определяет состояние класса/структуры. Однако в C++ есть возможность задать область видимости поля при помощи ключевых слов public, private и protected. В C такой возможности нет: если уж мы получили доступ к структуре, нам автоматически становятся доступны все ее поля. Поскольку у нас нет возможности ограничить видимость отдельных полей структуры C, остается единственный выход: спрятать структуру, хранящую состояние объекта, от внешнего доступа полностью.
С точки зрения хорошего тона программирования, открытый доступ к членам класса весьма нежелателен, поскольку позволяет произвольно менять значения полей извне класса. Это не позволяет поддерживать целостность состояния класса; фактически такой класс ничем принципиально не отличается от структур C или аналогичных конструкций других процедурных языков программирования. Поэтому вариант с сокрытием структуры состояния объекта полностью будет вполне приемлемым для практических нужд и ничуть не ухудшит чистоту нашего кода.
Второй важный аспект, отличающий классы C++ от структур C, состоит в том, что классы, помимо данных, включают также набор собственных функций (методов), которые имеют полный доступ к переменным состояния объекта (структура C может включать лишь поля-данные). Открытое подмножество методов определяет интерфейс класса, то есть набор операций, которые объект данного класса способен выполнять. Грамотно спроектированный класс позволяет изменять свое состояние лишь посредством обращения к интерфейсу.
Итак, на данном этапе проекта нас полностью устроило бы решение, которое позволило бы полностью скрыть состояние объекта от клиентов. Все взаимодействие клиента с объектом должно производиться посредством обращения к методам объекта.
Вопросы наследования и полиморфизма пока оставляем открытыми, поскольку практическая потребность в этих механизмах в рамках наших проектов пока не возникала (да и, учитывая довольно скромные задачи, решаемые встроенными микроконтроллерами, будет возникать не так уж часто). Вернемся к ним при необходимости.
Примечание. Некоторое подобие «статического полиморфизма времени компоновки» мы уже применяли ранее, когда писали несколько реализаций кода для одного интерфейсного заголовочного файла. Конечно, это недотягивает до полноценного динамического полиморфизма времени выполнения, но для наших насущных задач этого хватало полностью.
Попробуем найти решение поставленной задачи.

Сокрытие состояния объекта

На первый взгляд, мы поставили перед собой довольно противоречивую задачу. С одной стороны, клиент должен иметь доступ к объекту, иначе он не сможет обращаться к его методам. С другой стороны, получив доступ к структуре, клиент может сотворить с ней что угодно, причем его нежелательные действия могут аукнуться в другой части программы, что сильно затрудняет диагностику, поскольку воспроизвести ошибку не всегда просто.
Попробуем воспользоваться одним средством языка C, которое до сих пор оставалось невостребованным нами. В стандарте ISO/IEC 9899:1999(E) оно называется «incomplete structure or union type» (6.7.2.3/7, стр. 107). Мы можем включить в программу объявление вида:

Код: (C)
struct S1;

Эта конструкция объявляет, что неполный (незавершенный) тип S1 является тэгом некоторой структуры. В данном случае мы не объявили ни одного поля этой структуры. Не следует путать такой тэг с объявлением пустой структуры:

Код: (C)
struct S2
{
};

S2 — это вполне определенная структура, не имеющая ни одного поля. Относительно состава полей S1 мы не можем сказать ничего определенного. Структура S1 должна быть определена в другом месте программы, без этого мы не сможем обрабатывать ее содержимое.
В чем же смысл введения тэгов в программу, если с ними ничего нельзя делать, не зная устройства структуры? Это не совсем так: на тэги можно ссылаться посредством указателей C (механизма полноценных ссылок C не предоставляет, но и с указателями мы сумеем кое-чего добиться):

Код: (Text) с
struct S1;

typedef S1* Sp1;

Мы получили полностью абстрактный тип Sp1, который указывает на некую структуру, детали строения которой нам в данный момент неизвестны. Что же мы можем делать с этим типом?
Прежде всего, как и в случае любого другого типа, мы можем объявить переменную этого типа:

Код: (C)
Sp1 myVar1;

Далее, мы можем присваивать значения таких переменных друг другу, поскольку они имеют одинаковый тип:


Код: (C)
Sp1 myVar2;
...
myVar2 = myVar1;

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

Методы

Каким же образом методы получат доступ к скрытому состоянию объекта? Очень просто — определение этих функций мы разместим в том же файле, что и определение структуры состояния класса. В объектно-ориентированных языках методы объекта имеют неявный параметр, который ссылается на экземпляр; в нашем случае методу придется придется явно задавать указатель на структуру.
Объявление методов будет располагаться в интерфейсном заголовочном файле. Клиент, желающий работать с экземплярами класса, должен включить заголовочный файл в свой код.

Шаблон построения класса

Теперь у нас есть необходимые кирпичики для построения объектного механизма средствами языка C. Во-первых, есть тип указателя на структуру, в которой хранится состояние объекта, причем даже модуль, располагающий таким указателем, не имеет никаких сведений о внутреннем устройстве этой структуры, поскольку она объявлена лишь тэгом. Эта структура будет соответствовать экземпляру объекта. В некотором роде инкапсуляция получилась даже «крепче», чем в объектах C++, поскольку в заголовочных файлах C++ обычно внутреннее строение класса выставляется на всеобщее обозрение, включая закрытые члены класса.
Итак, в целом вырисовывается следующая картина. В заголовочном файле, который играет роль интерфейса класса, объявляется тэг структуры, которая будет хранить состояние объекта, а также определяются тип указателя на эту структуру и сигнатуры открытых методов класса:

Код: (C) MyClass.h
...

// тэг структуры для хранения состояния объекта
struct MyClassStruct;

// абстрактный тип объекта
typedef MyClassStruct* MyClass;

...

// метод объекта без параметров
RT1 MyClass_Method1(MyClass self);

// метод объекта с двумя параметрами
RT2 MyClass_Method2(MyClass self, PT1 param1, PT2 param2);

// метод класса (статический) без параметров
void MyClass_Method3(void);

...

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

Код: (C) MyClass.c
#include "MyClass.h"

...

// определение структуры для хранения состояния объекта
struct MyClassStruct
{
  FT1 field1;
  FT2 field2;
  ...
};

...

// метод объекта без параметров
RT1 MyClass_Method1(MyClass self)
{
  //тело метода
}

// метод объекта с двумя параметрами
RT2 MyClass_Method2(MyClass self, PT1 param1, PT2 param2)
{
  //тело метода
}

// метод класса (статический) без параметров
void MyClass_Method3(void)
{
  //тело метода
}

...


Создание объекта

До сих пор осталась нерешенной одна очень важная проблема: как (а также где) создавать объекты? Здесь возможны варианты.
Один из вариантов — создание локальной переменной в стеке. Он привлекателен в первую очередь тем, что в этом случае принципиально невозможна утечка памяти — при выходе из функции, в которой был создан объект, область ее стека полностью освобождается. К сожалению, нам это не подходит, ведь в нашей модели клиент не знает ничего о внутреннем строении объекта, поэтому не может выделить достаточную область стека для его хранения. Конечно, можно было бы запрашивать у класса его размер через специальный метод, затем выделять область заданного размера в стеке и создавать там объект, но такое решение слишком усложнено.
Другой вариант, который часто применяется в «больших программах», — создание объекта в «куче» посредством оператора new. Это мощное и гибкое решение, поскольку позволяет создавать динамические структуры, размер которых неизвестен заранее при компиляции программы. Впрочем, мощные инструменты, как правило, опасны в обращении. Данный случай — не исключение. Во-первых, динамическое создание объектов в среде, не оснащенной «сборщиком мусора», требует весьма кропотливой работы по своевременному уничтожению ненужных объектов, в противном случае неизбежна утечка памяти: «осиротевшие» объекты, забытые создателями, продолжают занимать место в драгоценной оперативной памяти. При интенсивном создании объектов запросто можно превысить лимит доступной памяти, после чего практически неминуем крах приложения. Об актуальности этой проблемы говорит тот пример, что изрядная часть учебников программирования на C++ начинается изобретением велосипеда в виде «умного указателя», который ведет подсчет ссылок на объекты и при обнулении их числа удаляет объект (на самом деле эта полумера, как и все прочие полумеры, не в состоянии решить проблему полностью; впрочем, эта тема лежит за рамками статьи).
Во-вторых, управление «кучей» — нетривиальная задача, требующая немалых накладных расходов. После нескольких циклов выделения/освобождения памяти «куча» фрагментируется, а с ростом фрагментации снижается эффективность использования «кучи». Даже в культовой для многих системе Unix эта задача решалась с переменным успехом, как упоминается в статье «Оптимизация: ваш злейший враг». Реализовать полноценную систему управления памятью на микроконтроллере с объемом оперативной памяти в несколько байт — совершенно нереальная задача, поскольку управлять будет попросту нечем: свободной памяти не останется. Получается замкнутый круг: чем меньше доступной памяти, тем выше потребность в качественной системе управления ей, и одновременно тем меньше шансов такую систему реализовать. Очевидно, создание объектов в «куче» также плохо подходит для встроенных систем.
Примечание. Есть одна специфическая стратегия работы с «кучей», пригодная для реализации на системах со скромными ресурсами. Я сталкивался с этой стратегией в «легких» реализациях языка Pascal для PDP-11 и Sinclair. Суть ее заключается в следующем: «куча» растет подобно стеку, но в противоположном направлении, навстречу ему. Граница «кучи» периодически фиксируется операцией mark. Если выполнить затем операцию restore, граница кучи будет восстановлена из зафиксированного ранее значения. Конечно, это не позволяет уничтожать объекты индивидуально; вычищаются разом все динамические объекты, созданные после последнего вызова mark. Впрочем, при продуманной стратегии создания объектов это не столь сильное ограничение, и уж во всяком случае такое управление «кучей» оказывается гораздо лучше, чем вовсе никакого. Поэтому будем держать эту стратегию про запас на всякий случай.
Чтобы избежать сложностей работы с «кучей», во встроенных приложениях обычно применяется статическое управление памятью: все необходимые объекты создаются заранее, на этапе компиляции. При этом вероятность неприятных сюрпризов во время выполнения минимальна: если мы выйдем за допустимые пределы, то получим предупреждение еще во время компиляции. Такой подход представляется мне наиболее целесообразным с учетом нашей специфики, и я намерен его придерживаться. Модуль, отвечающий за работу с объектами, будет содержать пул готовых экземпляров. Например, в нашем проекте достаточно иметь два экземпляра интервальных таймеров, по одному для красного и зеленого каналов. Поэтому модуль интервального таймера будет содержать два таймера. Клиент, которому необходим таймер, сможет запросить его у модуля через вызов специального метода (этот метод является своеобразной разновидностью паттерна проектирования «фабричный метод»). Метод вернет клиенту указатель на выделенный ему экземпляр объекта, которым клиент может пользоваться для своих нужд.
Такая простая стратегия не слишком гибка: мы не можем уничтожить ненужный нам более объект и использовать выделенную ему память для других целей. С другой стороны, встроенные приложения, как правило, используются циклически, производя одни и те же действия, и созданные объекты используются постоянно. Поэтому это ограничение не будет для нас критическим.
Теперь у нас есть все необходимое для создания семейств сходных объектов.

Реализация интервального таймера с произвольным числом каналов

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

Интерфейс

Основное изменение в интерфейсе — добавление в методы, относящиеся к объекту, указателя на экземпляр self. Также появился «фабричный метод» IntervalTimer_getIntervalTimer(), посредством которого можно получить указатель на один из экземпляров объекта.

Код: (C) IntervalTimer.h
#ifndef _INTERVALTIMER_H
#define _INTERVALTIMER_H

/** @file
 * @brief Интерфейс модуля IntervalTimer.
 *
 * Модуль предназначен для реализации многоканального интервального таймера.
 *
 * Перед использованием модуль следует инициализировать (см. @ref IntervalTimer_init()).
 * Количество каналов отсчета интервалов задается константой @ref INTERVALTIMERS_NUMBER.
 * Получить указатель на канал с заданным номером @c n можно вызовом @ref IntervalTimer_getIntervalTimer(uint8_t n).
 */


#include <stdint.h>

/**
 * @def INTERVALTIMERS_NUMBER
 * Задает максимальное количество независимых каналов интервального таймера в программе.
 */

#define INTERVALTIMERS_NUMBER 2

#if INTERVALTIMERS_NUMBER == 0
    #error "INTERVALTIMERS_NUMBER must be greater than 0"
#endif

struct IntervalTimerStruct;

/**
 * @brief Абстрактный тип - указатель на экземпляр объекта.
 */

typedef struct IntervalTimerStruct* IntervalTimer;

/**
 * @brief Получение канала интервального таймера с заданным номером.
 *
 * Номер  канала не должен превышать максимальное значение, заданное макроопределением @c INTERVALTIMERS_NUMBER.
 *
 * @param[in] n Номер канала интервального таймера.
 *
 * @return Указатель на  канал интервального таймера либо 0, если задан недопустимый номер  канала.
 */

IntervalTimer IntervalTimer_getIntervalTimer(uint8_t n);

/**
 * @brief Инициализация модуля IntervalTimer.
 *
 * Должна выполняться перед использованием модуля.
 */

void IntervalTimer_init(void);

/**
 * @brief Запуск отсчета заданного интервала.
 *
 * @param[in] self Указатель на канал таймера.
 * @param[in] interval Величина интервала в миллисекундах.
 */

void IntervalTimer_start(IntervalTimer self, uint16_t interval);

/**
 * @brief Проверка истечения интервала, запущенного вызовом IntervalTimer_start().
 *
 * @param[in] self Указатель на канал таймера.
 *
 * @return Признак истечения интервала.
 */

_Bool IntervalTimer_isElapsed(IntervalTimer self);

#endif // _INTERVALTIMER_H

Модуль имеет также интерфейс, предназначенный для взаимодействия с модулем аппаратного таймера (а также упрощения тестирования). Он содержит единственную callback-функцию, вызываемую каждую миллисекунду:

Код: (C) IntervalTimerCallBack.h
#ifndef INTERVALTIMERCALLBACK_H
#define INTERVALTIMERCALLBACK_H

/** @file
 * @brief Интерфейс callback-функций модуля IntervalTimer.
 *
 * Модуль предназначен для связи с драйвером аппаратного таймера.
 */


/**
 * @brief Callback-функция, вызываемая аппаратным таймером каждую миллисекунду.
 * Используется для отсчета оставшегося времени каждым из независимых каналов.
 */

void IntervalTimer_onTick(void);

#endif // INTERVALTIMERCALLBACK_H

Тесты

Внутренние спецификации нового модуля сформулируем в виде тестов по правилам TDD.
В случае, если номер канала превышает максимально допустимый, фабричный метод возвращает пустой указатель:

Код: (C) TestIntervalTimer.c
void test_IntervalTimer_getIntervalTimer_IllegalTimerNumberMustReturnNull_1(void)
{
    IntervalTimer it = IntervalTimer_getIntervalTimer(INTERVALTIMERS_NUMBER);

    TEST_ASSERT_NULL(it);
}

void test_IntervalTimer_getIntervalTimer_IllegalTimerNumberMustReturnNull_2(void)
{
    IntervalTimer it = IntervalTimer_getIntervalTimer(INTERVALTIMERS_NUMBER + 2);

    TEST_ASSERT_NULL(it);
}

В случае, если запрошен канал с минимально допустимым номером (0), указатель на него не должен быть пустым:

Код: (C) TestIntervalTimer.c
void test_IntervalTimer_getIntervalTimer_FirstLegalTimerNumberMustNotReturnNull(void)
{
    IntervalTimer it = IntervalTimer_getIntervalTimer(0);

    TEST_ASSERT_NOT_NULL(it);
}

Аналогично в случае канала с максимально допустимым номером:

Код: (C) TestIntervalTimer.c
void test_IntervalTimer_getIntervalTimer_LastLegalTimerNumberMustNotReturnNull(void)
{
    IntervalTimer it = IntervalTimer_getIntervalTimer(INTERVALTIMERS_NUMBER - 1);

    TEST_ASSERT_NOT_NULL(it);
}

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

Код: (C) TestIntervalTimer.c
void test_IntervalTimer_getIntervalTimer_FirstAndLastTimersMustBeDifferent(void)
{
    if (INTERVALTIMERS_NUMBER == 1)
        return;

    IntervalTimer itFirst = IntervalTimer_getIntervalTimer(0);
    IntervalTimer itLast = IntervalTimer_getIntervalTimer(INTERVALTIMERS_NUMBER - 1);

    TEST_ASSERT_FALSE(itFirst == itLast);
}

Только что проинициализированный и еще не запущенный канал должен находиться в состоянии «истек»:

Код: (C) TestIntervalTimer.c
void test_IntervalTimer_MustBeElapsedAfterInit(void)
{
    IntervalTimer allTimers[INTERVALTIMERS_NUMBER];
    for (uint8_t i = 0; i < INTERVALTIMERS_NUMBER; ++i)
    {
        allTimers[i] = IntervalTimer_getIntervalTimer(i);
        TEST_ASSERT_TRUE(IntervalTimer_isElapsed(allTimers[i]));
    }
}

Немедленно после запуска первого канала он не должен быть в истекшем состоянии. При этом последний канал (если каналов больше одного) не должен запуститься (проверка на независимость каналов друг от друга).

Код: (C) TestIntervalTimer.c
void test_IntervalTimer_MustNotBeElapsedImmediatelyAfterStart(void)
{
    IntervalTimer itFirst = IntervalTimer_getIntervalTimer(0);
    IntervalTimer itLast = IntervalTimer_getIntervalTimer(INTERVALTIMERS_NUMBER - 1);

    IntervalTimer_start(itFirst, 10);

    TEST_ASSERT_FALSE_MESSAGE(IntervalTimer_isElapsed(itFirst), "Just started first timer must not be elapsed");
    if (INTERVALTIMERS_NUMBER > 1)
    {
        TEST_ASSERT_TRUE_MESSAGE(IntervalTimer_isElapsed(itLast), "Last timer must be elapsed");
    }
}

Канал, запущенный на интервал 10 миллисекунд, не должен истечь сразу после запуска и в течение первых девяти тиков таймера. После последнего, десятого тика он должен перейти в истекшее состояние:

Код: (C) TestIntervalTimer.c
void test_IntervalTimer_MustBeElapsedAfter10Ticks(void)
{
    IntervalTimer it = IntervalTimer_getIntervalTimer(0);
    IntervalTimer_start(it, 10);

    for (int i = 0; i < 10; ++i)
    {
        TEST_ASSERT_FALSE_MESSAGE(IntervalTimer_isElapsed(it), "IntervalTimer elapsed unexpectedly");
        IntervalTimer_onTick();
    }
    TEST_ASSERT_TRUE_MESSAGE(IntervalTimer_isElapsed(it), "IntervalTimer must be elapsed after 10 ticks");
}

Самый сложный итоговый тест (проводится при условии, что каналов более одного). Первый канал запускается на 10 тиков, второй — на 20. После запуска и в течение первых девяти тиков оба канала не должны истечь. После 10-го тика первый канал должен истечь, второй — остаться в прежнем состоянии. Это состояние должно сохраняться 10 следующих тиков (с 11-го по 19-й). После 20-го тика второй канал также должен истечь.
Тест проверяет независимую параллельную работу двух каналов, а также корректную реакцию истекшего канала на последующие тики (игнорирование).

Код: (C) TestIntervalTimer.c
void test_IntervalTimer_TwoTimersSimultaneously(void)
{
    if (INTERVALTIMERS_NUMBER < 2)
        return;

    IntervalTimer it1 = IntervalTimer_getIntervalTimer(0);
    IntervalTimer_start(it1, 10);
    IntervalTimer it2 = IntervalTimer_getIntervalTimer(1);
    IntervalTimer_start(it2, 20);

    for (int i = 0; i < 10; ++i)
    {
        TEST_ASSERT_FALSE_MESSAGE(IntervalTimer_isElapsed(it1), "IntervalTimer1 elapsed unexpectedly");
        TEST_ASSERT_FALSE_MESSAGE(IntervalTimer_isElapsed(it2), "IntervalTimer2 elapsed unexpectedly");
        IntervalTimer_onTick();
    }
    TEST_ASSERT_TRUE_MESSAGE(IntervalTimer_isElapsed(it1), "IntervalTimer1 must be elapsed after 10 ticks");
    TEST_ASSERT_FALSE_MESSAGE(IntervalTimer_isElapsed(it2), "IntervalTimer2 must not be elapsed after 10 ticks");
    for (int i = 0; i < 10; ++i)
    {
        TEST_ASSERT_TRUE_MESSAGE(IntervalTimer_isElapsed(it1), "IntervalTimer1 must be elapsed after 10 ticks");
        TEST_ASSERT_FALSE_MESSAGE(IntervalTimer_isElapsed(it2), "IntervalTimer2 elapsed unexpectedly");
        IntervalTimer_onTick();
    }
    TEST_ASSERT_TRUE_MESSAGE(IntervalTimer_isElapsed(it1), "IntervalTimer1 must be elapsed after 20 ticks");
    TEST_ASSERT_TRUE_MESSAGE(IntervalTimer_isElapsed(it2), "IntervalTimer2 must be elapsed after 20 ticks");
}

Перед запуском каждого теста модуль таймера инициализируется:

Код: (C) TestIntervalTimer.c
#include "unity.h"
#include "IntervalTimer.h"
#include "IntervalTimerCallback.h"

void setUp(void)
{
    IntervalTimer_init();
}

void tearDown(void)
{
}

Код

И в заключение — код, проходящий этот набор тестов:

Код: (C) IntervalTimer.c
#include <stddef.h>
#include "IntervalTimer.h"
#include "IntervalTimerCallback.h"

struct IntervalTimerStruct
{
    uint16_t tickCounter;
};

static struct IntervalTimerStruct intervalTimer[INTERVALTIMERS_NUMBER];

void IntervalTimer_init(void)
{
    for (uint8_t i = 0; i < INTERVALTIMERS_NUMBER; ++i)
        intervalTimer[i].tickCounter = 0;
}

IntervalTimer IntervalTimer_getIntervalTimer(uint8_t n)
{
    if (n >= INTERVALTIMERS_NUMBER)
        return NULL;

    return &intervalTimer[n];
}

_Bool IntervalTimer_isElapsed(IntervalTimer self)
{
    return (self->tickCounter == 0);
}

void IntervalTimer_start(IntervalTimer self, uint16_t interval)
{
    self->tickCounter = interval;
}

void IntervalTimer_onTick(void)
{
    for (uint8_t i = 0; i < INTERVALTIMERS_NUMBER; ++i)
    {
        if (intervalTimer[i].tickCounter > 0)
            --(intervalTimer[i].tickCounter);
    }
}

Результат:

test/TestIntervalTimer.c:14:test_IntervalTimer_getIntervalTimer_IllegalTimerNumberMustReturnNull_1:PASS
test/TestIntervalTimer.c:21:test_IntervalTimer_getIntervalTimer_IllegalTimerNumberMustReturnNull_2:PASS
test/TestIntervalTimer.c:28:test_IntervalTimer_getIntervalTimer_FirstLegalTimerNumberMustNotReturnNull:PASS
test/TestIntervalTimer.c:35:test_IntervalTimer_getIntervalTimer_LastLegalTimerNumberMustNotReturnNull:PASS
test/TestIntervalTimer.c:42:test_IntervalTimer_getIntervalTimer_FirstAndLastTimersMustBeDifferent:PASS
test/TestIntervalTimer.c:53:test_IntervalTimer_MustBeElapsedAfterInit:PASS
test/TestIntervalTimer.c:63:test_IntervalTimer_MustNotBeElapsedImmediatelyAfterStart:PASS
test/TestIntervalTimer.c:77:test_IntervalTimer_MustBeElapsedAfter10Ticks:PASS
test/TestIntervalTimer.c:90:test_IntervalTimer_TwoTimersSimultaneously:PASS
-----------------------
9 Tests 0 Failures 0 Ignored
OK



(Продолжение следует)

Поскольку проект в данный момент находится в неустойчивом состоянии (интерфейс модуля IntervalTimer изменен, а код использующих его клиентов пока еще не приведен в надлежащее состояние), вопреки сложившейся традиции я не прикладываю результирующий код к статье. В подобных случаях в репозитории проекта создается параллельная ветвь (branch), в которой производится вся работа, а по ее завершению происходит слияние с главной ветвью кода. Поскольку я выкладываю «плоскую» файловую структуру в виде архива, я лишен возможности создавать параллельные ветви проекта. «Чистый» код будет выложен в момент его получения.
Версия для печати
Обсудить на форуме