Книга: Чистая архитектура. Искусство разработки программного обеспечения
Назад: 28. Границы тестов
Дальше: VI.Детали

29. Чистая встраиваемая архитектура

Автор: Джеймс Греннинг (James Grenning)

Martin_Page_281_Image_0001.tif 

Некоторое время тому назад я прочитал статью The Growing Importance of Sustaining Software for the DoD («Растущее значение устойчивого программного обеспечения для министерства обороны») в блоге Дуга Шмидта. В ней Дуг сделал следующее заявление:

Несмотря на то что программное обеспечение не изнашивается, встроенные микропрограммы и оборудование устаревают, что требует модификации программного обеспечения.

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

Я хотел бы добавить к заявлению Дуга:

Несмотря на то что программное обеспечение не изнашивается, оно может быть разрушено неуправляемыми зависимостями от микропрограмм и оборудования.

Нередко встроенному программному обеспечению может быть отказано в долгой жизни из-за заражения зависимостями от аппаратного обеспечения.

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

• «Микропрограммы хранятся в энергонезависимых запоминающих устройствах, таких как ПЗУ, ППЗУ или флеш-память» ().

• «Микропрограмма — это программа, или набор инструкций, заключенная в аппаратном устройстве» ().

• «Микропрограмма — это программное обеспечение, встроенное в устройство» ().

• Микропрограмма — это «программное обеспечение (программа или данные), записанное в постоянное запоминающее устройство (ПЗУ)» ().

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

Я ничего не имею против микропрограмм или разработчиков микропрограмм (я сам занимался созданием микропрограмм). Но мы действительно должны меньше писать микропрограммы и больше — программное обеспечение. На самом деле я разочарован тем, что разработчики микропрограмм пишут их как микропрограммы!

Инженеры, не занимающиеся разработкой встраиваемого программного обеспечения, тоже пишут микропрограммы! Вы тоже фактически пишете микропрограммы, когда внедряете SQL в свой код или когда ставите его в зависимость от платформы. Разработчики приложений для Android пишут микропрограммы, когда не отделяют бизнес-логику от Android API.

Я участвовал в разработке многих проектов, где грань между прикладным кодом (программным обеспечением) и кодом, взаимодействующим с оборудованием (микропрограммой), была размыта до полного исчезновения. Например, в конце 1990-х годов мне посчастливилось участвовать в перепроектировании подсистемы коммуникации с целью перехода от технологии мультиплексирования с разделением по времени (Time-Division Multiplexing; TDM) к технологии передачи голоса по протоколу IP (Voice Over IP; VOIP). Технология VOIP широко используется в наши дни, а технология TDM считалась современной в 1950 — 1960-х годах и широко применялась в 1980 — 1990-х годах.

Всякий раз, когда у нас появлялся вопрос к инженеру-системотехнику о том, как вызов должен реагировать в той или иной ситуации, он исчезал и спустя какое-то время появлялся с очень подробным ответом. «Откуда ты это узнал?» — спрашивали мы. «Из кода продукта», — отвечал он. Запутанный и устаревший код служил справочником по новому продукту! Существующая реализация не имела разделения между TDM и бизнес-логикой, выполняющей вызовы. Весь продукт целиком зависел от оборудования/технологий, и этот клубок нельзя было распутать. Весь продукт фактически был микропрограммой.

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

Я давно знал и понимал важность отделения программного обеспечения от оборудования, но слова Дуга прояснили, как использовать термины программное обеспечение и микропрограмма в отношении друг к другу.

Это ясное сообщение для инженеров и программистов: прекратите писать так много микропрограмм и дайте своему коду шанс служить долго. Конечно, потребовать этого не получится. Но давайте посмотрим, как сохранить архитектуру встраиваемого программного кода в чистоте, чтобы дать программному обеспечению шанс служить долго.

Тест на профпригодность

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

1. «Сначала заставьте его работать». Вы останетесь не у дел, если он не работает.

2. «Затем перепишите его правильно». Реорганизуйте код, чтобы вы и другие смогли понимать и развивать его, когда потребуется что-то изменить или понять.

3. «Затем заставьте его работать быстро». Реорганизуйте код, чтобы добиться «необходимой» производительности.

Большая часть встраиваемых систем, которые мне приходилось видеть, похоже, писалась с единственной мыслью в голове: «Заставьте его работать», — и иногда с навязчивой идеей: «Заставьте его работать быстро», — воплощаемой введением микрооптимизаций при каждом удобном случае. В своей книге The Mythical Man-Month Фред Брукс предлагает «планировать отказ от первой версии». Кент и Фред советуют практически одно и то же: узнайте, как это работает, и найдите лучшее решение.

Встраиваемое программное обеспечение ничем не отличается в отношении этих проблем. Многие невстраиваемые приложения доводятся только до стадии «работает», и мало что делается, чтобы код получился правильным и служил долго.

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

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

ISR(TIMER1_vect) { ... }

ISR(INT2_vect) { ... }

void btn_Handler(void) { ... }

float calc_RPM(void) { ... }

static char Read_RawData(void) { ... }

void Do_Average(void) { ... }

void Get_Next_Measurement(void) { ... }

void Zero_Sensor_1(void) { ... }

void Zero_Sensor_2(void) { ... }

void Dev_Control(char Activation) { ... }

char Load_FLASH_Setup(void) { ... }

void Save_FLASH_Setup(void) { ... }

void Store_DataSet(void) { ... }

float bytes2float(char bytes[4]) { ... }

void Recall_DataSet(void) { ... }

void Sensor_init(void) { ... }

void uC_Sleep(void) { ... }

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

• функции, реализующие предметную логику:

• float calc_RPM(void) { ... }

• void Do_Average(void) { ... }

• void Get_Next_Measurement(void) { ... }

• void Zero_Sensor_1(void) { ... }

• void Zero_Sensor_2(void) { ... }

• функции, обслуживающие аппаратную платформу:

• ISR(TIMER1_vect) { ... }*

• ISR(INT2_vect) { ... }

• void uC_Sleep(void) { ... }

• функции, реагирующие на нажатия кнопок:

• void btn_Handler(void) { ... }

• void Dev_Control(char Activation) { ... }

• функция, читающая данные из аппаратного аналогово-цифрового преобразователя:

• static char Read_RawData(void) { ... }

• функции, записывающие значения в долговременное хранилище:

• char Load_FLASH_Setup(void) { ... }

• void Save_FLASH_Setup(void) { ... }

• void Store_DataSet(void) { ... }

• float bytes2float(char bytes[4]) { ... }

• void Recall_DataSet(void) { ... }

• функция, которая не делает того, что подразумевает ее имя:

• void Sensor_init(void) { ... }

Заглянув в другие файлы этого приложения, я нашел множество препятствий, мешающих пониманию кода. Также я обнаружил, что организация файлов подра­зумевает единственный способ тестирования этого кода — непосредственно внутри целевого устройства. Практически каждый бит этого кода знает, что относится к специализированной микропроцессорной архитектуре, используя «расширенные» конструкции языка C, привязывающие код к конкретному набору инструментов и микропроцессору. У этого кода нет ни малейшего шанса служить долго, если будет решено перенести продукт на другую аппаратную платформу.

Приложение работает: инженер прошел тест на профпригодность. Но нельзя сказать, что оно имеет чистую встраиваемую архитектуру.

Привязка к оборудованию — узкое место

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

Да, встраиваемое программное обеспечение имеет свои особенности, и инженеры встраиваемых систем — особые люди. Но разработка встраиваемых систем не настолько особенная, чтобы принципы, описываемые в этой книге, нельзя было применить к встраиваемым системам.

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

Чистая встраиваемая архитектура — архитектура, поддерживающая тестирование

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

Уровни

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

66329.png 

Рис. 29.1. Три уровня

Раздел между оборудованием и остальной частью системы — объективная реальность, по крайней мере после определения оборудования (рис. 29.2). Именно здесь часто возникают проблемы при попытке пройти тест на проф­пригодность. Ничто не мешает знаниям об оборудовании инфицировать весь код. Если не проявить осторожность при структурировании кода и не ограничить просачивание сведений об одном модуле в другой, код будет трудно изменить. Я говорю не только о случае, когда изменяется оборудование, но также о ситуации, когда понадобится исправить ошибку или внести изменение по требованию пользователя.

66338.png 

Рис. 29.2. Оборудование должно отделяться от остальной системы

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

Оборудование — это деталь

Линия между программным обеспечением и микропрограммами обычно видна не так четко, как линия, разделяющая код и оборудование (рис. 29.3).

Одна из задач разработчика встраиваемого программного обеспечения — укрепить эту линию. Границу между программным обеспечением и микро-

66356.png 

Рис. 29.3. Линия между программным обеспечением и микропрограммами обычно более размытая, чем линия между кодом и оборудованием

программой (рис. 29.4) называют слоем аппаратных абстракций (Hardware Abstraction Layer; HAL). Это не новая идея: она была реализована в персональных компьютерах еще в эпоху до Windows.

66365.png 

Рис. 29.4. Слой аппаратных абстракций

Слой HAL существует для программного обеспечения над ним, и его API должен приспосабливаться под потребности этого программного обеспечения. Например, микропрограмма может хранить байты и массивы байтов во флеш-памяти, а приложению требуется хранить и читать пары имя/значение с использованием некоторого механизма хранения. Программное обеспечение не должно заботиться о деталях хранения пар имя/значение во флеш-памяти, на вращающемся диске, в облаке или в основной памяти. Слой аппаратных абстракций (HAL) предоставляет услугу и не раскрывает программному обеспечению, как она работает. Реализация поддержки флеш-памяти — это деталь, которая должна быть скрыта от программного обеспечения.

Еще один пример: управление светодиодом привязано к биту GPIO. Микропрограмма может предоставлять доступ к битам GPIO, а слой HAL может иметь функцию Led_TurnOn(5). Это довольно низкоуровневый слой аппаратной абстракции. Давайте посмотрим, как повысить уровень абстракции с точки зрения программного обеспечения/продукта. Какую информацию сообщает светодиод? Допустим, что включение светодиода сообщает о низком заряде аккумулятора. На некотором уровне микропрограмма (или пакет поддержки платформы) может предоставлять функцию Led_TurnOn(5), а слой HAL — функцию Indicate_LowBattery(). Таким способом слой HAL выражает назначение услуги для приложения. Кроме того, уровни могут содержать подуровни. Это больше напоминает повторяющийся фрактальный узор, чем ограниченный набор предопределенных уровней. Назначение ввода/выводов GPIO — это детали, которые должны быть скрыты от программного обеспечения.

Не раскрывайте деталей об оборудовании пользователям HAL

Встраиваемое программное обеспечение с чистой архитектурой допускает возможность тестирования без целевого оборудования. Удачно спроектированный слой аппаратных абстракций (HAL) предоставляет тот шов, или набор, точек подстановки, которые облегчат тестирование без оборудования.

Процессор — это деталь

Когда сборка встраиваемого программного обеспечения производится с применением специализированного набора инструментов, в комплект часто входят заголовочные файлы с <i>поддержкой дополнительных конструкций </i>. Эти компиляторы часто допускают вольное обращение с языком C, добавляя новые ключевые слова для доступа к функциям процессора. Код выглядит как код на C, но он больше не является кодом на C.

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

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

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

#ifndef _ACME_STD_TYPES

#define _ACME_STD_TYPES

#if defined(_ACME_X42)

    typedef unsigned int     Uint_32;

    typedef unsigned short   Uint_16;

    typedef unsigned char    Uint_8;

 

    typedef int              Int_32;

    typedef short            Int_16;

    typedef char             Int_8;

 

#elif defined(_ACME_A42)

    typedef unsigned long    Uint_32;

    typedef unsigned int     Uint_16;

    typedef unsigned char    Uint_8;

 

    typedef long             Int_32;

    typedef int              Int_16;

    typedef char             Int_8;

#else

    #error <acmetypes.h> is not supported for this environment

#endif

 

#endif

Заголовочный файл acmetypes.h не следует использовать непосредственно, иначе ваш код окажется тесно связанным с процессорами ACME. Вы думаете, что если используете процессор ACME, то какой вред он может причинить? Все просто, вы не сможете скомпилировать свой код, не подключив этот заголовочный файл. А если подключить его и определить символ _ACME_X42 или _ACME_A42, целые числа будут иметь неправильный размер при тестировании за пределами целевой платформы. Какое-то время это может не вызывать никаких проблем, но однажды у вас может появиться желание перенести свое приложение на другой процессор, и тогда вы обнаружите, что задача сильно усложнилась из-за отказа от переносимости и ограничения на подключение файлов, знающих о процессорах ACME.

Вместо использования acmetypes.h следует попробовать пойти более стандартным путем и использовать stdint.h. Но как быть, если в состав целевого компилятора не входит файл stdint.h? Вы можете сами написать этот заголовочный файл. Файл stdint.h может использовать acmetypes.h, когда выполняется компиляция для целевой платформы:

#ifndef _STDINT_H_

#define _STDINT_H_

#include <acmetypes.h>

typedef Uint_32 uint32_t;

typedef Uint_16 uint16_t;

typedef Uint_8 uint8_t;

 

typedef Int_32 int32_t;

typedef Int_16 int16_t;

typedef Int_8 int8_t;

 

#endif

Использование stdint.h во встраиваемом программном обеспечении и микропрограммах поможет вам сохранить код чистым и переносимым. Всякое программное обеспечение должно быть независимым от типа процессора, но этот совет годится не для всякой микропрограммы. В следующем фрагменте кода используются особые расширения языка C, позволяющие коду обращаться к периферийным устройствам в микроконтроллере. Продукт может быть оснащен этим микроконтроллером, поэтому вы можете использовать интегрированные в него периферийные устройства. Следующая функция выводит в последовательный порт строку с текстом "hi". (Этот пример основан на реальном коде.)

void say_hi()

{

  IE = 0b11000000;

  SBUF0 = (0x68);

  while(TI_0 == 0);

  TI_0 = 0;

  SBUF0 = (0x69);

  while(TI_0 == 0);

  TI_0 = 0;

  SBUF0 = (0x0a);

  while(TI_0 == 0);

  TI_0 = 0;

  SBUF0 = (0x0d);

  while(TI_0 == 0);

  TI_0 = 0;

  IE = 0b11010000;

}

Эта маленькая функция страдает множеством проблем. Первое, что бросается в глаза, — присутствие последовательности символов 0b11000000. Такая форма записи двоичных чисел очень удобна, но поддерживается ли она стандартным языком C? К сожалению, нет. Еще несколько проблем проистекают непосредственно из использования нестандартных расширений:

• IE: устанавливает биты разрешения прерываний.

• SBUF0: буфер вывода последовательного порта.

• TI_0: прерывание опустошения буфера передачи последовательного порта. Если операция чтения возвращает 1, это указывает, что буфер пуст.

Переменные с именами, состоящими из букв верхнего регистра, в действительности представляют механизмы доступа к встроенной периферии микроконтроллера. Если программе понадобится управлять прерываниями и выводить символы, вам придется использовать эту периферию. Да, это удобно, но это уже не язык C.

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

Если вы используете микроконтроллер, подобный этому, ваша программа могла бы спрятать эти низкоуровневые функции за слоем абстракций процессора (Processor Abstraction Layer; PAL). Часть микропрограммы, находящаяся над слоем PAL, могла бы проверять платформу, на которой выполняется, и таким способом ослабить жесткость кода.

Операционная система — это деталь

Слой аппаратных абстракций (HAL) является насущной необходимостью, но достаточно ли его? Во встраиваемых системах, где отсутствует другое программное окружение, слоя HAL более чем достаточно, чтобы оградить код от избыточной зависимости от операционной среды. Но что, если встраиваемая система использует некоторую операционную систему реального времени (RealTime Operating System; RTOS) или встраиваемую версию Linux или Windows?

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

Программное обеспечение обращается к операционному окружению посредством операционной системы (ОС). ОС — это слой, отделяющий программное обеспечение от микропрограмм (рис. 29.5). Прямое использование механизмов ОС может стать источником проблем. Например, представьте,

66384.png 

Рис. 29.5. Дополнительный слой операционной системы

что производителя вашей RTOS купила другая компания и из-за этого выросла стоимость системы или упало ее качество. Или ваши потребности изменились, а используемая вами RTOS не обладает необходимыми возможностями. Вам придется изменить много кода. И это будут не просто синтаксические изменения, обусловленные сменой API, скорее всего, вам придется приспосабливать семантику кода к различным механизмам и примитивам новой ОС.

Чистая встраиваемая архитектура изолирует программное обеспечение от операционной системы, реализуя слой абстракции операционной системы (Operating System Abstraction Layer; OSAL), как показано на рис. 29.6. В некоторых случаях этот слой может иметь очень простую реализацию, выражающуюся в простой подмене имен функций. Но иногда может потребоваться полное обертывание некоторых функций.

Если вам доводилось переносить программное обеспечение с одной RTOS на другую, вы знаете, насколько трудно это дается. Если ваше программное обеспечение зависит только от слоя OSAL, но не зависит от ОС, вам потребуется только написать новый слой OSAL, совместимый с прежним. Что бы вы предпочли: изменить кучу сложного кода или написать новый код, определяющий интерфейс и поведение? Это даже не спорный вопрос. Я выбираю последнее.

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

66405.png 

Рис. 29.6. Слой абстракции операционной системы

Определяя слой OSAL, вы также получаете возможность привести свои приложения к некоторой общей структуре. Например, вы могли бы реализовать механизмы передачи сообщений и отказаться от ручного управления конкурентным выполнением в каждом потоке.

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

Программирование с применением интерфейсов и возможность подстановки

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

Идея многоуровневой архитектуры основывается на идее программирования с применением интерфейсов. Когда один модуль взаимодействует с другим посредством интерфейса, появляется возможность заменить одного поставщика услуг другим. Многим из вас наверняка приходилось писать свои небольшие версии printf для развертывания в целевой среде. Если интерфейс вашей функции printf совпадает с интерфейсом стандартной версии printf, вы можете использовать их взаимозаменяемо.

Главное правило — использовать заголовочные файлы как определения интерфейсов. Однако такой подход требует внимательно следить за тем, что помещается в заголовочный файл. Желательно не помещать в него ничего, кроме объявлений функций, а также констант и имен структур, необходимых функциям.

Не загромождайте интерфейсные заголовочные файлы структурами данных, константами и определениями типов, которые нужны только реализации. Это не просто вопрос беспорядка: подобный беспорядок приводит к нежелательным зависимостям. Ограничьте видимость деталей реализации. Исходите из предположения, что детали реализации будут изменяться. Чем меньше мест, где код знает детали, тем меньше кода вам придется просматривать и изменять.

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

Принцип DRY и директивы условной компиляции

Одно из использований возможности подстановки, которое часто упускают из виду, связано с особенностями обработки разных целевых платформ или операционных систем программами на C и C++. Для этого часто используются директивы условной компиляции, включающие и выключающие сегменты кода. Я вспоминаю один особенно тяжелый случай, когда в телекоммуникационном приложении директива #ifdef BOARD_V2 встречалась несколько тысяч раз.

Повторяющийся код нарушает принцип «не повторяйся» (Don’t Repeat Yourself; DRY). Если #ifdef BOARD_V2 встречается один раз, в этом нет никакой проблемы. Но шесть тысяч раз — это большая проблема. Условная компиляция, идентифицирующая тип целевого оборудования, часто используется во встраиваемых системах. Можно ли от нее избавиться?

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

Заключение

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

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

Микросхема, управляющая последовательным портом.

Фредерик Брукс. Мифический человеко-месяц, или Как создаются программные системы. СПб.: Символ-Плюс (2007). — Примеч. пер.

Некоторые производители микроконтроллеров добавляют свои ключевые слова в язык C, чтобы упростить доступ к регистрам и портам из кода на C. К сожалению, как только код начинает использовать такие ключевые слова, он перестает быть кодом на C.

General-Purpose Input/Output (интерфейс ввода/вывода общего назначения). — Примеч. пер.

В этом предложении преднамеренно использована разметка HTML.

Hunt and Thomas, The Pragmatic Programmer. (Э. Хант, Д. Томас. Программист-прагматик. Путь от подмастерья к мастеру. М.: Лори, 2016. — Примеч. пер.)

Назад: 28. Границы тестов
Дальше: VI.Детали