Статья
Версия для печати
Обсудить на форуме
«Hello World!» в embedded-исполнении. Часть 6

© Dale, 11.11.2011 — 11.11.2011.

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

Оглавление


«Позвольте, товарищ, у меня все ходы записаны»

Взявшись за наведение порядка в коде, следует вспомнить о еще одном важном деле — документировании кода. Именно документирование ходов шахматной партии позволило вывести на чистую воду великого комбинатора, которому почти всегда все сходило с рук. В нашем случае тщательное документирование тоже поможет разрешить множество недоразумений, порой возникающих по ходу проекта. Мы уже начали этот процесс, когда рисовали диаграммы UML, разрабатывая архитектуру приложения. Но эти диаграммы поясняют лишь архитектуру в целом, так сказать, с высоты птичьего полета, не раскрывая крайне важных деталей.
Мы уже определили интерфейсы вспомогательных модулей Led и Timer, которыми будет пользоваться Application, и сгенерировали для них подставные объекты, которые позволили протестировать логику работы нашего центрального модуля Application до реализации вспомогательных модулей в соответствии с принципами TDD. Теперь следует задокументировать эти интерфейсы перед их реализацией.
Вообще говоря, сделать это следовало давно, одновременно с разработкой интерфейсов. Я сознательно отложил этот момент, поскольку втиснуть столько нового материала в статью не представилось возможным. Усвоить можно лишь ограниченный объем, а среди читателей наверняка найдутся и те, для кого многое из рассказанного не было знакомо ранее. Впредь мы исправимся и будем документировать сразу, по горячим следам. Сейчас же быстро наверстаем упущенное, ибо лучше поздно, чем никогда.
Для документирования я выбрал продукт под названием Doxygen. Он успешно развивается без малого полтора десятка лет и стал одним из стандартов де-факто для оформления встроенной в исходные тексты документации. Подход, когда документация вставляется в исходный код программы в виде специально оформленных комментариев, я считаю рациональным, поскольку это позволяет одновременно редактировать и то, и другое. Когда документация ведется в отдельном файле обычным текстовым процессором без специальных средств синхронизации кода и его описания, несоответствия практически гарантированы. В случае использования Doxygen, конечно, тоже многое зависит от добросовестности разработчика, поскольку в комментариях можно написать не относящуюся к делу ерунду, и Doxygen не сможет это обнаружить. Но тут выбирать не приходится — либо такая помощь, либо вовсе никакой.
Краткий обзор использования Doxygen вы можете найти на сайте нашего клуба, но я все же рекомендовал бы изучить прилагаемую документацию полностью, поскольку возможностей в продукт заложено очень много, и далеко не все из них удастся обнаружить «методом тыка» или беглым просмотром примеров.
Возвращаемся к нашим исходникам, открываем их в редакторе и комментируем. При комментировании крайне важно иметь чувство меры и не переусердствовать. Так, начинающие программисты (не все, конечно; есть и такие, которые способны наворотить горы запутанного кода без единого комментария; впрочем, отсутствие комментариев в данном случае — не признак лени, просто пишущий не понимает толком, что делает, и ему нечего написать в комментариях) часто пишут нечто такое:

Код: (C)
count++; // увеличить значение счетчика
total += current; // добавить текущий платеж к общей сумме

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

Код: (C) Application.h
/** @file
 * @brief Интерфейс модуля Application.
 */


#ifndef _APPLICATION_H
#define _APPLICATION_H

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

void Application_init(void);

/**
 * @brief Итерация главного цикла приложения.
 */

void Application_run(void);

#endif // _APPLICATION_H

Код: (C) Led.h
/**
 * @file
 * @brief Интерфейс модуля управления светодиодом.
 *
 * Сервис модуля позволяет включать и выключать светодиод, подключенный к микроконтроллеру.
 */

 
#ifndef _LED_H
#define _LED_H

/**
 * @brief Инициализация порта светодиода.
 *
 * Необходимо инициализировать порт перед работой с ним.
 */

void Led_init(void);

/**
 * @brief Включение светодиода.
 */

void Led_on(void);

/**
 * @brief Выключение светодиода.
 */

void Led_off(void);

#endif // _LED_H

Код: (C) Timer.h
/**
 * @file
 * @brief Интерфейс модуля таймера.
 */

 
#ifndef _TIMER_H
#define _TIMER_H

#include <stdint.h>

/**
 * @brief Инициализация таймера.
 *
 * Необходимо инициализировать таймер перед его использованием.
 */

void Timer_init(void);

/**
 * @brief Ожидание в течение заданного интервала.
 *
 * @param[in] ms Размер интервала в миллисекундах.
 */

void Timer_wait(uint16_t ms);

#endif // _TIMER_H

Код: (C) TestApplication.c
/**
 * @file
 * @brief Тест модуля Application.
 */

 
#include "unity.h"
#include "Application.h"
#include "MockLed.h"
#include "MockTimer.h"

void setUp(void)
{
}

void tearDown(void)
{
}

/**
 * @brief Проверка логики функции Application_init().
 */

void test_Application_init()
{
        Led_init_Expect();
        Timer_init_Expect();
   
        Application_init();
}

/**
 * @brief проверка логики функции Application_run().
 */

void test_Application_run()
{
    Led_on_Expect();
    Timer_wait_Expect(500);
    Led_off_Expect();
    Timer_wait_Expect(1500);
   
    Application_run();
}

Для генерации документации, помимо самих файлов исходных текстов со специальными комментариями, нужен еще специальный управляющий файл. Создание его вручную с нуля — само по себе нелегкий труд; к счастью, его можно избежать, воспользовавшись специальной настроечной утилитой с графическим интерфейсом, которая называется Doxywizard. В ней тоже придется заполнить немало опций, но хотя бы не придется учить синтаксис управляющего файла. Сам управляющий файл я приводить здесь не буду, он насчитывает почти 1800 строк и не представляет особого интереса, и к тому же интересующиеся могут посмотреть его в текущем архиве проекта, который, как всегда прикреплен в конце статьи.
Doxygen предлагает несколько вариантов оформления выходных данных. Я выбрал два из них: HTML в качестве онлайн-справки и RTF для твердой копии документации. Так выглядит документация HTML, открытая в браузере (рис. 1):

Рис. 1. Окно браузера с проектной документацией

Помимо описаний, Doxygen генерирует также навигационную панель, облегчающую поиск нужной информации.
Так выглядит версия описания интерфейса Led.h для печати:

(click to show)
Файл E:/Projects/Shelek/Blinker/Software/Led/include/Led.h

Интерфейс модуля управления светодиодом.
Функции

  • void Led_init (void)
    Инициализация порта светодиода.
  • void Led_on (void)
    Включение светодиода.
  • void Led_off (void)
    Выключение светодиода.

Подробное описание

Интерфейс модуля управления светодиодом.
Сервис модуля позволяет включать и выключать светодиод, подключенный к микроконтроллеру.
См. определение в файле Led.h

Функции

void Led_init (void)

Инициализация порта светодиода.
Необходимо инициализировать порт перед работой с ним.

void Led_off (void)
Выключение светодиода.


void Led_on (void)
Включение светодиода.


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

Модель приложения на инструментальной системе

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

- Мама, жарьте рыбу!
- Так рыбы ж нет!
- Вы жарьте, рыба будет!

Лучшее, что можно сделать в этом случае, — это написать уйму непроверенного, непротестированного кода в надежде, что как-нибудь обойдется (эта надежда никогда не оправдывается), а потом лихорадочно пытаться этот код отладить.
С использованием TDD все не столь безнадежно. Во-первых, мы можем использовать тестовые двойники, что позволяет не только реализовать, но и протестировать высокоуровневую часть кода. Это дает уверенность, что с высокой степенью вероятности этот код окажется работоспособным и на целевой системе. Во-вторых, мы можем локализовать аппаратно-зависимую часть кода (при грамотном проектировании архитектуры она будет небольшой) в тонкой прослойке HAL, которая столь проста, что в принципе не должна потребовать больших усилий для реализации и отладки.
Итак, забудем (на время) о недоступном сейчас прототипе и посмотрим, что можно сделать в нынешних условиях.

Модель светодиода

Начнем с более простой задачи — моделирования включения/выключения светодиода на инструментальной системе. Наша задача выбрать наиболее простую и при этом достаточно адекватную модель.
Первое, что приходит в голову, — это воспользоваться готовыми светодиодами на клавиатуре. Действительно, они есть практически на любой инструментальной системе, и к тому же среди них есть один загадочный, связанный со столь же загадочной клавишей Scroll Lock, которой за многие годы существования архитектуры IBM PC никто так и не смог придумать сколь-нибудь разумного применения. Включая/выключая его программно, мы не нарушим ничего в инструментальной системе.
Пожалуй, это наиболее естественный подход — моделировать свечение светодиода свечением другого светодиода. Впрочем, на практике оказывается, что зажигать и гасить светодиод Scroll Lock не так уж просто. Легче всего, пожалуй, имитировать нажатия клавиши Scroll Lock отправкой соответствующих сообщений MS Windows. Однако я сильно сомневаюсь в переносимости такого решения, скажем, в среду Linux, в которой многие читатели захотят производить разработку, тем более что все до сих пор использованные инструменты являлись кроссплатформенными, и объективных препятствий к этому нет. Поэтому попробуем поискать более практичный вариант.
Важно понять одну вещь: модель не обязана внешне быть похожей на прототип; например, математическая модель маятника представляет собой однородное дифференциальное уравнение второго порядка. Главное — чтобы модель описывала интересующее нас поведение прототипа (в данном случае — координаты маятника) с практически приемлемой точностью. В этих условиях мы можем перейти от физического маятника к его математической модели без ущерба для конечного результата.
При таком подходе можно было бы, например, моделировать включение/выключение светодиода, скажем, выводом на консоль надписей типа «LED On», «LED Off». Впрочем, при быстром чередовании таких надписей отличить их будет не так легко. Мы поступим еще проще: при включении нашего «светодиода» будем выводить звездочку «*», а при выключении — пробел. Если при этом не переводить строку, будет довольно похоже на мигание реального индикатора.
Реализация такой модели будет настолько тривиальна, что даже не потребует тестирования. Не будем же мы тестировать библиотечную функцию printf(), на которой основывается наша модель.
Что нам понадобится для такой модели? В первую очередь — правило make-файла для генерации объектного модуля модели:

Код: (Text) "Led\Makefile"
.PHONY : obj
obj : $(OBJ_PC)\Led.o

$(OBJ_PC)\Led.o : $(LED_SRC)\PC\Led.c $(LED_HDR)\Led.h
        $(COMPILE.c) -o $@ -I $(LED_HDR) $<

А теперь — нехитрый код:

Код: (C) Led\PC\Led.c
#include "Led.h"
#include <stdio.h>

#define SPACE " "
#define ASTERISK "*"
#define CR "\r"

void Led_init(void)
{
    // в модели инициализация не требуется
}


void Led_on(void)
{
    printf(ASTERISK CR);
}


void Led_off(void)
{
    printf(SPACE CR);
}

Модель таймера

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

Код: (Text) "Timer\Makefile"
.PHONY : obj
obj : $(OBJ_PC)\Timer.o

$(OBJ_PC)\Timer.o : $(TIMER_SRC)\PC\Timer.c $(TIMER_HDR)\Timer.h
        $(COMPILE.c) -o $@ -I $(TIMER_HDR) $<

Код: (C) Timer\PC\Timer.c
#include "Timer.h"
#include <time.h>
#include <stddef.h>

void Timer_init(void)
{
    // в модели инициализация не требуется
}

void Timer_wait(uint16_t ms)
{
    time_t current = time(NULL);
    while (difftime(time(NULL), current) * 1000 < ms);
}

Сборка модели

Осталось собрать нашу модель:

Код: (Text) "Blinker\Makefile"
include ..\Common.mk

.DEFAULT : Blinker.exe
Blinker.exe : src\Blinker.c $(APP_HDR)\Application.h $(APP_SRC)\Application.c $(LED_HDR)\Led.h \
$(LED_SRC)\PC\Led.c $(TIMER_HDR)\Timer.h $(TIMER_SRC)\PC\Timer.c
        $(MAKE) obj
        $(MAKE) -C ..\Application obj
        $(MAKE) -C ..\Led obj
        $(MAKE) -C ..\Timer obj
        $(LINK.c) -o $@ $(OBJ_PC)\Blinker.o ..\Application\$(OBJ_PC)\Application.o ..\Led\$(OBJ_PC)\Led.o \
..\Timer\$(OBJ_PC)\Timer.o

.PHONY : obj
obj : $(OBJ_PC)\Blinker.o
$(OBJ_PC)\Blinker.o : src\Blinker.c $(APP_HDR)\Application.h
        $(COMPILE.c) -o $@ -I $(APP_HDR) $^

.PHONY : clean
clean :
        del /q $(OBJ_PC)*.o
        del /q Blinker.exe

Запускаем Blinker.exe и видим в окне консоли мигающую звездочку. Мы определенно не теряем времени даром, пока коллеги из аппаратного отдела возятся со своей частью проекта. Аппаратура еще не готова, а у нас уже есть работоспособный каркас приложения с протестированным верхним уровнем и компактной аппаратно-зависимой прослойкой, которую легко будет адаптировать к архитектуре целевой системы.
Все ли возможное мы сделали? В части тестирования на инструментальной системе, пожалуй, все. Однако рано почивать на лаврах. В ожидании готовности аппаратного прототипа мы можем испытать нашу программу на модели микроконтроллера. Это сделает наше моделирование еще достовернее, ведь выполняться на модели будет уже настоящая, целевая программа, а не ее дальняя родственница, скомпилированная под инструментальную систему.

Текущее состояние проекта.



Окончание.
Версия для печати
Обсудить на форуме