AVR
Микроконтроллеры AVR фирмы Atmel хорошо знакомы разработчикам и не нуждаются в особом представлении. Эти устройства используют модифицированную гарвардскую архитектуру и демонстрируют приличную производительность при низком энергопотреблении. 8-битная архитектура контроллера AVR была представлена в 1997 году, а к 2003 году Atmel поставил 500 миллионов микроконтроллеров. Видимо не будет преувеличением сказать, что такой популярностью AVR во многом обязан проекту Arduino.
Arduino
Arduino — это открытая платформа для прототипирования. В настоящее время доступно богатое разнообразие различных плат Arduino и дополнительных устройств. Простое для освоения подмножество языка программирования C вместе с богатым набором библиотек, создаваемых энтузиастами со всего света, позволяют создавать любые приложения для решения практически неограниченного числа задач. Как профессионал, так и новичок в программировании имеет возможность быстро проверить любую идею или создать прототип будущего устройства в кратчайшие сроки. Однако вряд-ли кто-то будет использовать ПО Arduino для реальных проектов. Основная причина — неэффективность результирующего кода [8]. Стремление к универсальности и простоте инструментария Arduino не позволяет в полной мере использовать потенциал микроконтроллера AVR, его производительность и естественные возможности параллелелизма.
Подходы к разработке встраиваемого ПО
Старая школа
Представители старой школы являются экспертами в области как программного, так и аппаратного обеспечения. Их инструменты — язык C и ассемблер. Их главная цель — выжать все из каждого байта, достичь максимальной производительности кода при минимальном использовании памяти и энергопотреблении. В то же время, созданный ими код не всегда является легким для понимания, что может сильно усложнить дальнейшую поддержку и развитие кода.
Новая школа
Люди, воспитанные в эпоху объектов, склонны в каждой сущности видеть объект. Классы являются прекрасным примером повторно используемого кода. Использование классов поощряет разработчика к достижению лучшей структуры кода и продуманного распределения ответственности между компонентами. Правильно написанный объектно-ориентированный код является легким для понимания и поддержки. К недостаткам кода, написанного с использованием C++, часто относят его производительность. Объектно-ориентированные возможности языка являются его безусловным преимуществом, однако за это часто приходится платить. Автоматическая генерация методов, неявное создание временных объектов могут привести к ощутимому снижению
Шаблоны С++
Одной из сильнейших сторон C++ является механизм шаблонов. Основная идея — возможность обобщенного определения поведения кода без явного указания используемых сущностей. В качестве очевидного примера использования шаблонов можно привести стандартную библиотеку шаблонов. STL предоставляет три основных типа сущностей — контейнеры, алгоритмы и итераторы. Обобщенные контейнеры позволяют задать требуемые типы хранимых данных в точке использования. Алгоритмы ничего не знают о контейнерах; связь алгоритмов и контейнеров осуществляется через механизм итераторов. Таким образом, STL демонстрирует удивительную гибкость и позволяет решать бесконечное число практических задач.
Несомненным достоинством шаблонных классов является то, что компилятор инстанцирует только те методы класса, которые фактически используются в коде. Весь остальной код проходит лишь проверку на синтаксическую корректность. Это позволяет исключить неиспользуемый код и таким образом, снизить потребление памяти. Механизм специализации позволяет выполнить тонкую настройку поведения в зависимости от шаблонных параметров, что дает прекрасные возможности для оптимизации кода. К недостатком шаблонов можно отнести сложность разработки и недружелюбность компилятора к шаблонному коду.
Идею шаблонов проще всего показать на примере:
Предположим, нам нужна функция min для работы с целыми числами. Очевидным решением C-программиста будет нечто вроде:
int min(int a, int b)
{
return (a < b) ? a : b;
}
Если аналогичная функция понадобится для работы с плавающей точкой, придется написать еще одну функцию:
float min(float a, float b)
{
return (a < b) ? a : b;
}
Для каждого нового типа потребуется новая функция.
Для C++ программиста задача решается написанием примерно следующего шаблона:
template<typename T>
T min(T a, T b)
{
return (a < b) ? a : b;
}
В данном случае тип используемых значений не указывается явно, вместо этого мы используем обозначение T, которое присутствует в определении шаблона с ключевым словом typename. Для шаблонных функций (а также методов класса) компилятор способен самостоятельно вывести (deduce) требуемый тип параметра на основании типов передаваемых значений. В случае, если в данную функцию min будет передана пара параметров с различными типами, компилятор вполне обоснованно выскажет свое недовольство. При этом, если передача параметров разных типов выполнена намеренно, есть возможность помочь компилятору, явно указав тип шаблонного параметра при вызове функции:
float float_variable = 3.141;
int integer_variable = 3;
int result = min<int>(float_variable, integer_variable);
или в зависимости от того, что вам нужно:
float result = min<float>(float_variable, integer_variable);
Объявленная таким образом функция, может работать с любым типом данных, единственным условием является наличие операции "<" (меньше), определенной для данного типа. Это очень напоминает поведение языков с динамической типизацией, однако есть принципиальное отличие. В языках наподобие Питона функция может существовать в единственном экземпляре. Будучи языком со
Очень удобно, но именно это обстоятельство может являться причиной еще одной проблемы шаблонов — раздувания кода (code bloat). Данная конкретная функция не является проблемой, поскольку имеет малый размер и является очевидным кандидатом на встраивание (inlining). Однако наличие в коде множества по разному параметризированных экземпляров шаблонных классов, имеющих объемные методы, действительно может приводить к значительному разрастанию кода, создавая таким образом реальную проблему. Тодд Вельдхузен приводит рекомендации [5], позволяющие этого избежать.
Meta programming
В 1994 году на заседании комитета по стандартизации C++, Эрвин Унрух впервые продемонстрировал возможность выполнения вычислений на этапе компиляции. В процессе компиляции, представленный им код, выводил серию диагностических сообщений, содержавших значения ряда простых чисел. Дальнейшие исследования показали, что возможности выполнения математических действий обладают вычислительной полнотой [6]: действительно, имеется возможность использовать арифметические операции, организовывать циклы через использование рекурсии, а также ветвления через использование специализаций.
Было подмечено некоторое подобие между шаблонами и привычными функциями времени исполнения [3].
Параметры шаблонов играют роль параметров обычных функций, а вложенные типы и константные значения перечислимых типов являются аналогами возвращаемых значений. В качестве параметров метафункций и возвращаемых значений могут использоваться те же самые сущности, которые могут быть параметрами шаблонов, к ним относятся:
— константные значения перечислимых типов,
— типы, а также
— указатели
1. Наиболее простой и понятный случай — использование значений перечислимых типов
Приведенная ниже метафункция возводит значение BASE в степень PWR.
template // первичный шаблон
<
unsigned PWR,
unsigned BASE = 10 // параметры шаблонных классов
// могут иметь значения по умолчанию
>
struct power
{
enum{value = BASE * power<PWR-1,BASE>::value};
};
template<unsigned BASE> // специализация шаблона
struct power<0,BASE>
{
enum{value = 1};
};
Как видно, для вычисления результата шаблон power вызывает себя рекурсивно с модифицированным значением PWR. Для предотвращения бесконечной рекурсии необходима специализация, в данном случае для нулевого значения PWR.
Пример использования:
unsigned KILO = power<3,10>::value; // переменной KILO будет присвоено значение 1000
unsigned MEGA = power<6,10>::value; // переменной MEGA будет присвоено значение 1000000
unsigned kBytes = power<10,2>::value; // переменной kBytes будет присвоено значение 1024
2. Вычисления над типами
Представим, что нам необходимо передать в функцию входной параметр, имеющий тип ValueType. Мы бы хотели, чтобы оптимальный способ передачи параметра (по константной ссылке либо по значению) выбирался автоматически, в зависимости от целевой платформы и размера конкретного типа параметра.
template
<
typename ValueType
>
struct PARAM
{
typedef typename type_selector<
(sizeof(ValueType*) < sizeof(ValueType)),
const ValueType&,
ValueType
>::type type;
};
Шаблон type_selector, использованный внутри нашей метафункции, описан у многих авторов [например 3, 4] и может выглядеть следующим образом:
template // первичный шаблон
<
bool CONDITION, // логическое условие
typename TYPE0, // тип, который будет выбран в случае, если условие истинно
typename TYPE1 // тип, который будет выбран в случае, если условие ложно
>
struct type_selector
{
typedef TYPE0 type;
};
// специализация шаблона для значения CONDITION == false
template<typename TYPE0,typename TYPE1>
struct type_selector<0,TYPE0,TYPE1>
{
typedef TYPE1 type;
};
В зависимости от значения условия CONDITION, шаблон type_selector выбирает либо TYPE0 (CONDITION == true), либо TYPE1 (CONDITION == false). В качестве условия в данном случае мы используем логическое выражение: sizeof(ValueType) > sizeof(ValueType*). Для примера, если параметр имеет тип uint32_t, мы используем следующее определение для нашей функции:
void function(typename PARAM<uint32_t>::type value){...}
В данном случае компилятор требует указания ключевого слова typename перед обращением к шаблону, поскольку используемый для передачи параметра тип является вложенным (nested). Подобное объявление/определение функции выглядит несколько громоздко, тем не менее, поставленная задача решена: — на 32 и 64-битной платформах параметр будет передаваться по значению, а например, в случае компиляции под микроконтроллер AVR, где размер адреса равен двум байтам, параметр будет передаваться по константной ссылке.
3. Указатели в качестве шаблонных параметров
Предположим, внутри некоторого кода нам необходимо выполнить вызов callback — функции, тип которой определен как:
typedef void (*CALLBACK_FUNCTION_TYPE)(); // Тип callback функции
Теперь, определив наш код в виде шаблона:
// В качестве шаблонного параметра используем объект cb_func,
// имеющий тип CALLBACK_FUNCTION_TYPE
template<CALLBACK_FUNCTION_TYPE cb_func>
void some_code(...)
{
...
cb_func(); // Вызов функции внутри нашего кода
...
}
мы можем передать требуемую функцию при вызове нашего кода следующим образом:
some_code<&our_callback_function>(...);
Поскольку адрес функции our_callback_function известен во время компиляции, она может быть успешно встроена (inlined) компилятором [5]. О влиянии встраивания функций на размер и эффективность кода можно прочитать [7] — три главы этой книги целиком посвящены вопросам встраивания функций. В своей статье [5] Тодд Вельдхузен демонстрирует очень интересные примеры использования метафункций, в том числе разворачивание циклов на примере функции dotproduct для умножения матриц, вычисление тригонометрических констант для алгоритмов Быстрого Преобразования Фурье путем суммирования ряда. Здесь важно понимать, что во время исполнения все эти действия имеют нулевую стоимость, поскольку выполняются на этапе компиляции.
Дизайн
Когда речь заходит о коде, который предполагается использовать неоднократно, на первый план выходит вопрос об интерфейсе. Важность хорошо определенного интерфейса неоднократно обсуждалась в Сети. Набор требований, традиционно предъявляемых к хорошему интерфейсу включает в себя правильно определенные абстракции, сокрытие деталей реализации, минимальность и достаточность, удобство использования, сложность или невозможность неправильного использования и прочие [9]. При использовании метапрограммирования, некоторые из требований, могут быть реализованы лишь большими усилиями, а некоторые возможно не могут быть реализованы вовсе. Тот факт, что используемое в нашем случае свойство языка было обнаружено случайно, объясняет причину довольно неуклюжего синтаксиса. Это не добавляет удобства при разработке метапрограммного кода и использовании интерфейсов, основанных на шаблонах. Невозможность спецификации диагностических сообщений при компиляции, делают контроль за правильностью использования кода труднореализуемым, хотя некоторые попытки изменений в этом направлении уже предприняты, например Static assertions в библиотеке boost и новых стандартах языка.
При разработке кода для управления аппаратным устройством необходимо предоставить пользователю полный контроль над всеми его (устройства) компонентами. При этом сохраняется требование минимальности в интерфейсе. Разумным подходом здесь видимо будет использование правильного порядка следования параметров в интерфейсе устройства. Наиболее часто изменяемые параметры должны идти первыми, для всех остальных (параметров), следует определить значения по умолчанию, соответствующие наиболее типичным вариантам использования.
Удобным подходом для построения интерфейса является дизайн с использованием классов стратегий, описанный в [1, 2]. Идея очень проста. Часть реализуемого функционала делегируется внешним классам (стратегиям), которые используются в качестве шаблонных параметров. При необходимости изменить поведение, просто выбирается другая стратегия. Это очень напоминает использование параметров обычных (runtime) функций, где благодаря параметрам, мы имеем возможность получать различные результаты при передаче в функцию различных значений аргументов. Функция с жестко закодированными значениями аргументов, всегда будет возвращать один и тот же результат, что не имеет особого смысла. В качестве шаблонных параметров (стратегий) могут использоваться типы (классы) с полноценной функциональностью. Это обеспечивает возможность параметризации алгоритма в точке использования путем указания стратегий с требуемым поведением в качестве аргументов шаблона. Это дает новый уровень гибкости и обобщенности.
Рассмотрим пример реализации интерфейса устройства USART (универсальный синхронно-асинхронный приемопередатчик), входящего в состав типичного контроллера AVR
enum USART_ID // Идентификатор устройства
{
USART0,
USART1,
USART2,
USART3,
};
enum BAUD_RATE // Скорость обмена
{
BR_2400 = 2400,
...
BR_921600 = 921600,
BR_CUSTOM = CUSTOM_BAUD_RATE
};
// Класс стратегия - Параметры обмена (формат кадра)
template
<
BAUD_RATE baud = BR_9600, // Скорость обмена бод (enum)
DATA_BITS data_bits = DATA_BITS_8, // Количество бит данных в кадре (enum)
PARITY parity = NO_PARITY, // Паритет (enum)
STOP_BITS stop_bits = STOP_1, // Стоповые биты (enum)
...
>
struct FRAME_CONTROL;
Строгая типизация C++ потребует в качестве параметров указания значений, точно соответствующих объявленным типам данных. Например, для указания скорости обмена могут быть выбраны лишь те значения, которые объявлены в перечислении BAUD_RATE. Если потребуется какое-то особое (не стандартное) значение скорости, можно использовать значение BR_CUSTOM, предварительно объявив макроопределение CUSTOM_BAUD_RATE с требуемым значением скорости передачи данных.
Определение класса USART выглядит следующим образом:
template
<
USART_ID id, // Идентификатор устройства (enum)
class usart_ctrl = FRAME_CONTROL<>, // Параметры обмена - (стратегия) - структура FRAME_CONTROL
class receiver = USART_RECEIVER<>, // Параметры приемника - (стратегия) - структура USART_RECEIVER
class transmitter = USART_TRANSMITTER<> // Параметры передатчика - (стратегия) - структура USART_TRANSMITTER
>
struct USART
{
static void inline init(){...}
static size_type send(const uint8_t* data, size_type data_size){...}
static size_type print(const char* fmt, ...){...}
static size_type _vprintf(const char* fmt, va_list ap){...}
...
static void USART_UDRE_handler(){...}
};
Здесь для краткости, определения многих перечислений и структур опущено. Для использования в реальном коде мы включаем заголовочный файл с описанием всех этих структур при помощи директивы include и определяем требуемые параметры для нашего устройства:
#define SEND_BUFFER_SIZE 32
#define RECV_BUFFER_SIZE 16
typedef USART<
USART0,
FRAME_CONTROL<BR_921600>,
RECEIVER_DISABLED,
USART_TRANSMITTER<SEND_BUFFER_SIZE>
>
usart_0; // Устройство USART0, скорость 921600 бод, 8N1, приемник не используется, буфер передатчика 32 байта
typedef USART<
USART1,
FRAME_CONTROL<BR_9600, DATA_BITS_7, EVEN_PARITY, STOP_2>
USART_RECEIVER<RECV_BUFFER_SIZE>,
USART_TRANSMITTER<SEND_BUFFER_SIZE>
>
usart_1; // Устройство USART1, 9600-7E2, буфер приемника 16 байт, буфер передатчика 32 байта
typedef TWI<400000> I2C; // TWI - интерфейс 400 kHz
Итак, структура USART принимает четыре шаблонных параметра:
— идентификатор устройства — позволяет работать с любым из четырех имеющихся устройств (только Mega256, для младших чипов следует использовать USART0)
— стратегию usart_ctrl с единственной реализацией — FRAME_CONTROL — для спецификации параметров обмена (см. выше).
— стратегию receiver — приемник, для которого имеется всего две реализации — USART_RECEIVER, позволяющая задать требуемые параметры приемника (указать размер буфера и управлять прерыванием) и RECEIVER_DISABLED, позволяющая отключить приемник при необходимости
— стратегию transmitter — параметры передатчика с реализациями USART_TRANSMITTER (размер буфера, управление прерываниями) и TRANSMITTER_DISABLED, которая запрещает передатчик и соответствующие прерывания.
Данный набор стратегий обеспечивает полный контроль над устройством и благодаря использованию значений по умолчанию, упрощает параметризацию класса для наиболее типичных вариантов использования.
Далее инициализируем устройство:
usart_0::init();
I2C::init();
Здесь следует обратить внимание на необычный синтаксис вызова методов. Вместо привычного оператора “.” — ссылка на член структуры (structure reference), здесь использован оператор “::” — раскрытие области видимости (scope resolution). Дело в том, что все методы класса USART (а также TWI) определены как статические и здесь мы работаем не с объектами, а с типами. Это позволяет избежать накладных расходов на конструирование и разрушение объекта, а кроме того, явно отражает синглтон-подобную природу устройства. Это вовсе не означает, что мы полностью отказываемся от обычных объектов в пользу использования типов и их статических членов. Скорее всего в коде будет присутствовать множество привычных объектов, однако если говорить о структурах для управления аппаратными компонентами, данный подход имеет больше смысла.
Генерируемый при этом ассемблер (для Mega256) выглядит примерно следующим образом:
000000ba <_Z10usart_initv>:
ba: 10 92 c4 00 sts 0x00C4, r1
be: 10 92 c5 00 sts 0x00C5, r1
c2: 10 92 c0 00 sts 0x00C0, r1
c6: 88 e2 ldi r24, 0x28 ; 40
c8: 80 93 c1 00 sts 0x00C1, r24
cc: 86 e0 ldi r24, 0x06 ; 6
ce: 80 93 c2 00 sts 0x00C2, r24
d2: 10 92 26 01 sts 0x0126, r1
d6: 08 95 ret
000000d8 <_Z8twi_initv>:
d8: 8c e0 ldi r24, 0x0C ; 12
da: 80 93 b8 00 sts 0x00B8, r24
de: 10 92 b9 00 sts 0x00B9, r1
e2: 85 e4 ldi r24, 0x45 ; 69
e4: 80 93 bc 00 sts 0x00BC, r24
e8: 10 92 03 01 sts 0x0103, r1
ec: 08 95 ret
Из приведенного листинга видно, что вычисления всех необходимых констант для инициализации устройств, выполняются на этапе компиляции.
Еще один пример
Если мы разрабатываем свой протокол обмена, его объявление (с использованием стратегий) может выглядеть следующим образом:
template
<
class transport,
class params = PROTO_PARAMETERS<...> // Какие-то параметры нашего протокола
...
>
struct SUPER_DUPPER_EXCHANGE_PROTOCOL;
Здесь интересен следующий момент: транспорт для протокола задается в виде шаблонного параметра. Это позволяет настраивать наш протокол в точке использования, например:
typedef SUPER_DUPPER_EXCHANGE_PROTOCOL<usart_0, PROTO_PARAMETERS<>, ...> PROTO_SERIAL;
При желании мы сможем использовать тот же самый протокол с другим устройством, например SPI или TWI, то есть:
typedef SUPER_DUPPER_EXCHANGE_PROTOCOL<TWI<200000>, PROTO_PARAMETERS<>, ...> PROTO_TWI;
Для классов-стратегий отсутствуют какие-либо дополнительные ограничения, например требование наследования от общего предка. Единственным требованием к типу, используемому в качестве транспорта, является наличие методов (например send и receive) с требуемой сигнатурой.
Может быть определено любое необходимое количество стратегий, каждая из которых должна нести ответственность за определенный аспект функциональности, обеспечивая таким образом их ортогональность [2].
Для каждой стратегии в свою очередь может существовать множество различных реализаций. Как результат, количество различных вариантов поведения (множество возможных комбинаций стратегий) может быть достаточно большим. Это обеспечивает отличную гибкость кода, не внося при этом типичных проблем с производительностью, связанных с наследованием и является превосходным примером статического полиморфизма.
Определив таким образом требуемые нам типы, используем их в коде следующим образом:
PROTO_SERIAL::send(data, size); //отправка блока данных data на устройство usart_0
PROTO_TWI::send(data, size); //отправка блока данных data на TWI интерфейс
Отладка
Нет необходимости повторять, что отладка программного кода является непростой задачей. Отладка шаблонного кода представляет еще больше сложностей для разработчика в силу недружелюбности компилятора. Любая опечатка в тексте приводит к выводу длинных диагностических листингов, которые дополнительно удваиваются, вследствие двухпроходного режима компилятора. Читать эти листинги приходится начиная с самого начала, делая минимальное количество модификаций кода перед очередной попыткой компиляции. Часть сообщений может быть вызвана наведенными ошибками и исчезает при исправлении ошибки — первопричины.
Специализации шаблонов не связаны с первичным шаблоном никакими родственными отношениями. Фактически специализацию можно рассматривать как отдельный независимый класс, который подставляется вместо первичного шаблона в случае совпадения специализируемых параметров. Таким образом, чтобы быть хоть сколько-нибудь уверенным в работоспособности шаблонного кода, нужно как минимум однократно инстанцировать каждую специализацию шаблона. Все это делает процесс отладки шаблонного кода довольно длительным процессом.
Отладка встраиваемого (embedded) кода, в свою очередь, может стать кошмаром для разработчика, в особенности при отсутствии специального оборудования. В таком случае, единственным выходом является метод грубой силы — вставка отладочных сообщений.
Предположим, мы отлаживаем класс DEVICE, имеющий следующий интерфейс:
template
<
class params = DEVICE_SETTINGS<...>,
class dbg = NO_DEBUG
>
struct DEVICE
{
static uint8_t some_method(uint8_t parameter)
{
dbg::print("%s:%d\n", __FUNCTION__, parameter);
....
dbg:: print("retval:%d\n", retval);
return retval;
}
};
Здесь нам интересен шаблонный параметр dbg, который по умолчанию инициализируется значением NO_DEBUG. Внутри рассматриваемого метода мы выполняем вызов dbg::print.
В коде приложения устройство может быть объявлено следующим образом:
typedef DEVICE_SETTINGS<...> DEV_SETTINGS; // используем typedef для компактности
typedef DEVICE<DEV_SETTINGS, AVR_DEBUG<usart_0> > device; // объявление типа нашего устройства
Видно, что в качестве параметра dbg мы используем некий шаблон AVR_DEBUG, параметризированный типом usart_0. Если посмотреть на определение AVR_DEBUG, мы увидим примерно следующий код:
template
<
class SENDER
>
struct AVR_DEBUG
{
...
static void print(const char* fmt, ...)
{
va_list ap;
va_start(ap, fmt);
uint8_t retval = SENDER::_vprintf(fmt, ap);
va_end(ap);
}
};
Фактически, это означает, что в процессе выполнения приложения вызов dbg::print будет приводить к вызову метода print класса AVR_DEBUG<usart_0>, который в свою очередь вызывает метод _vprintf класса usart_0. Таким образом, в процессе отладки мы направляем требуемую отладочную информацию на последовательный интерфейс, заданный параметром usart_0.
По завершении отладки кода нашего устройства мы заменяем строку объявления следующим образом:
typedef DEVICE<DEV_SETTINGS, NO_DEBUG> device;
или проще, используем значение по умолчанию NO_DEBUG для параметра dbg:
typedef DEVICE<DEV_SETTINGS> device;
Реализация функции print класса NO_DEBUG имеет пустое тело и может выглядеть следующим образом:
struct NO_DEBUG
{
...
static void inline print(const char* , ...){}
};
Здесь мы опять полагаемся на компилятор, который успешно выполняет встраивание (inlining) и удаление тел пустых функций. При сборке релизной версии кода, весь неиспользуемый код будет удален.
Таким образом, мы имеем механизм, позволяющий управлять выводом отладочной информации.
Unit-tests
В своих документах Atmel рекомендует использовать типы данных, минимально допустимого для хранения данных размера. Рекомендация опирается на анализ размера результирующего кода, позволяет снизить затраты памяти, используемой приложением и подразумевает использование типов, размеры которых не зависят от платформы. А это в свою очередь способствует созданию кода, имеющего лучшую переносимость (portability). В идеале мы можем получить возможность запускать наш код на PC, что при разработке позволяет использовать юнит-тесты. Значительная часть кода, предназначенного для микроконтроллера, может быть проверена на тестах задолго до его загрузки на контроллер.
Если вернуться к приведенному выше примеру протокола, в качестве транспорта может быть использован специально написанный класс (mock/fake), который позволит имитировать получение пакетов с произвольным содержимым, что упростит отладку разрабатываемого протокола.
Развивая эту идею дальше, мы получаем возможность отладки практически любого кода на PC.
Множество периферийных устройств, с которыми приходится иметь дело, управляются контроллером через внешние интерфейсы (TWI, SPI...) и не имеют непосредственного отношения к той или иной платформе. Речь идет о различного рода сенсорах, часах реального времени, ЖК дисплеях и т.д. Код для управления подобной периферией в идеале должен быть способен исполняться на любой платформе. Это определяет важность переносимости (портируемости) кода.
Для отладки подобного кода, было бы очень удобно подключить устройство непосредственно к компьютеру разработчика. К сожалению, требуемый для устройства интерфейс может физически отсутствовать на компьютере. В таком случае мы можем использовать микроконтроллер в качестве адаптера между устройством и стандартным последовательным интерфейсом (COM или USB), который имеется на любом компьютере. Дизайн с использованием стратегий позволяет параметризировать отлаживаемый код таким образом, чтобы перенаправить управляющий поток на стандартный последовательный интерфейс. Микроконтроллер, подключенный одновременно к последовательному интерфейсу и к отлаживаемому устройству, программируется однократно на все время отладки и служит транслятором между двумя интерфейсами. Это дает возможность исполнять код непосредственно на компьютере разработчика, предоставляя полный контроль над этим кодом.
После завершения отладки готовый код может быть собран под требуемую платформу и загружен на соответствующий микроконтроллер. Данный прием успешно использовался при отладке кода для работы с SD card через SPI интерфейс (контроллер выполнял код адаптера serial to SPI), а также часов реального времени и датчиков инерционной навигации, использующих TWI интерфейс (контроллер выполнял функцию адаптера serial to TWI).
По приведенной ссылке [avr_meta] вы можете загрузить примеры реально используемого кода. Он разрабатывался с использованием avr8-gnu-toolchain-3.4.5.1522-linux, в качестве юнит-тест фреймворка использован TUT (C++ Template Unit Test Framework). Код разрабатывался для собственных нужд и еще требует большого количества работы. Однако мы нашли возможным опубликовать его в надежде, что для кого-то он может оказаться полезным:
bin Пара скриптов, используемых для генерации кода.
avr_adc Код для AVR ADC – Analog to Digital Converter
avr_debug Шаблон AVR_DEBUG
avr_interrupt/ext_int_control Код для управления внешними прерываниями AVR
avr_interrupt/pin_ch_int_control Код для управления Pin Change прерываний AVR
avr_misc вспомогательные функции
avr_pin Код для управления портами AVR
avr_power_mgmt Код для управления энергосбережением AVR
avr_spi Код для управления AVR SPI – Serial Peripheral Interface
avr_twi Код для управления AVR 2-wire Serial Interface (только серверная часть)
avr_usart Код для управления AVR USART (только асинхронные операции)
container/bit_field Шаблонная реализация bit field
container/circular_buffer Циклический буфер
event_driven Классы для событийно управляемого поведения
meta Шаблонные метафункции
misc вспомогательные функции
state/led_blinker Светодиодный индикатор режимов (состояний)
state/state_machine Шаблонная реализация конечного автомата
state/switch_case Шаблон switch case
Заключение
Использование объектно-ориентированных возможностей языка C++ позволяет улучшить структуру, читаемость и понятность кода. Классы являются прекрасным воплощением идеи повторного использования кода.
Гибкость, присущая шаблонам, позволяет разрабатывать обобщенный и в то же время очень эффективный код. Независимость кода от типов используемых данных позволяет принимать многие дизайнерские решения на заключительном этапе разработки или изменять эти решения без значительных переделок исходного кода.
Применение стратегий способно обеспечить множество вариантов поведения кода без применения наследования и типичных проблем с производительностью, присущих динамическому полиморфизму. Специализации шаблонов предоставляют разработчику прекрасные возможности для оптимизации и тонкой настройки поведения кода.
Случайно открытые возможности C++, которые продемонстрировал в 1994 году Эрвин Унрух, не входили в первоначальный замысел создателей языка, однако вызвали бурный интерес многих разработчиков. Возможность выполнения вычислений на этапе компиляции предоставляет разработчику новый уровень обобщенности кода и эффективности его исполнения. В настоящее время этот механизм хорошо известен C++ программистам и воплощен во множестве известных библиотек, таких, как Blitz++ или boost::MPL.
Фактически, в рамках одного языка программирования имеется возможность управлять как поведением кода во время его исполнения, так и процессом генерации того же кода еще на этапе компиляции. В одной языковой конструкции (в шаблонной функции) могут одновременно присутствовать сущности как с динамическим связыванием (параметры функции), так и со статическим связыванием (параметры шаблона). Тодд Вельдхузен называет C++ двухуровневым языком (two-level language).
Применение метапрограммирования позволяет значительно повысить эффективность исполнения и иногда уменьшить объем генерируемого кода, за счет принятия решений во время компиляции. Наличие в проекте параметров, которые не изменяются во время исполнения кода (являются константами времени компиляции) — хорошая возможность для оптимизации за счет использования метапрограммирования. Любые значения, которые могут быть вычислены на этапе компиляции, а также ветвления, управляемые константными параметрами, все это хорошие кандидаты для оптимизации. Другими словами, мы часто можем ускорить выполнение программы, за счет увеличения времени компиляции. В статье [10] приведены результаты измерения производительности метапрограммного кода, работающего на AVR и его сравнение с традиционно разработанным на базе библиотек Atmel кодом.
Разработка метапрограммного кода является довольно трудоемким и длительным процессом и вряд-ли целесообразна для разовых проектов. Однако, если речь идет о разработке библиотеки, то усилия оправдываются ожиданием, что такая долговременная инвестиция принесет свою пользу при каждом повторном использовании [3].
Хорошая переносимость (portability) программного кода означает меньший объем работы при большем результате и во всех случаях является преимуществом. Если вернуться к примеру с протоколом, то портируемость здесь является ключевым условием. Разработка отдельной реализации протокола для каждой стороны взаимодействия имеет мало смысла. Значительно лучше, если поставляя устройство, вы сможете предоставить клиенту и определение протокола в виде соответствующего кода, тем самым, значительно облегчая для него разработку управляющего программного обеспечения под нужную ему платформу.
Судя по небольшому количеству публикаций, шаблоны вообще и метапрограммирование в частности не очень востребованы в мире встраиваемого ПО, хотя именно здесь возможности метапрограммирования могут принести существенную выгоду. Они позволяет использовать традиционные для объектно-ориентированного программирования приемы и при этом обеспечить эффективность, присущую вручную написанному на C и ASM коду.
Литература
1. David Vandevoorde and Nicolai M. Josuttis. C++ Templates: The Complete Guide
2. Andrei Alexandrescu. C++ Design: Generic Programming and Design Patterns Applied
3. David Abrahams and Aleksey Gurtovoy, C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond
4. Davide Di Gennaro. Advanced C++ metaprogramming
5. Todd Veldhuizen. Techniques for Scientific C++. Indiana University Computer Science Technical Report #542
6. Todd L. Veldhuizen. C++ Templates are Turing Complete (2003).
7. Dov Bulka and David Mayhew. Efficient C++. Performance Programming Techniques. Addison-Wesley 2000.
8. Dale Wheat. Arduino Internals. Apress.
9. Martin Reddy. API Design for C++. 2011 Morgan Kaufmann Publishers
10. Christoph Steup, Michael Schulze, Jorg Kaiser. Exploiting Template-Metaprogramming for Highly Adaptable Device Drivers – a Case Study on CANARY an AVR CAN-Driver. Department for Distributed Systems Universitat Magdeburg
11. Материалы XXIV съезда КПСС. “Об усилении мер по повышению эффективности отраслей народного хозяйства. Надзор за соблюдением эффективности встраиваемого ПО”.
Комментарии (42)
ncix
22.09.2016 21:13>>Представители старой школы являются экспертами в области как программного, так и аппаратного обеспечения. Их инструменты — язык C и ассемблер. Их главная цель — выжать все из каждого байта, достичь максимальной производительности кода при минимальном использовании памяти
Вот чего я не понял, когда начал писать под AVR, это тотальное стремление разработчиков SDK заменить нормальные понятные имена на нечитаемые аббревиатуры: GIMSK, MCUCR, MCUSR и т.п. От длины имени переменной размер бинарника ну никак не зависитArcanum7
22.09.2016 21:19+1Наверное «тяжёлое» наследие мнемокода ассемблера. Там всё в 3 или 4 буквы расписано. Регистрам «повезло» чуть больше.
Ну ещё в каждом даташите есть раздел сокращений где каждое расписано что и как. Ну или по ходу текста тоже есть разъяснения.
А кто с «камнями» каждый день работает, тот все сокращение наизусть помнит (или помнит куда смотреть и освежить память).
father_mckenzie
22.09.2016 22:36+4Позволю себе предположить, что данные аббревиатуры не инициатива разработчиков программного инструментария, а следование именованию, предложенному разработчиками железа. Наверное было бы странно видеть что-то типа Sleep_Mode_Control_Register в названии регистра, который у схемотехников называется просто SMCR.
cyberly
23.09.2016 00:01Рискну предположить, что такие названия лучше смотрятся в здоровенной таблице в начале даташита с отображением регистров на адреса памяти, на диаграммах изменения состояния регистров (тех, где показано, какие биты в какой момент меняются при приходе какого-нибудь сигнала), логических схемах отдельных блоков, и еще, может быть, в трехэтажных выражениях с побитовыми операциями в коде.
С другой стороны, какой-нибудь «младший бит выбора режима таймера номер три» — вот как его красиво назвать?ncix
23.09.2016 09:40Битовые операции это отдельная история. В SDK вроде предусмотрели макрос для установки битов (чтобы не писать бесконечные сдвиги << 1), но кто-то очень «дружественный к разработчикам» назвал его _BV. К черту читаемость кода, главное покороче. Почему было не назвать BIT_VALUE? Или немного модифицировать и сделать SET_BIT/UNSET_BIT?
father_mckenzie
23.09.2016 11:12В прилагаемом коде (в хидере avr_misc) для этой цели объявлен шаблон вроде:
template < unsigned bitnum, typename ValueType = uint8_t > struct bit { static const ValueType value; // по факту value = (ValueType(1) << bitnum); };
При использовании мы и получаем требуемое
bit<0>::value ... bit<7>::value
Ну и конечно вместо целочисленного смещения бита можно использовать соответствующие мнемоники из Atmel-овских хидеров.
Как утверждают классики-программисты, такое определение имеет преимущество перед макросами. Действительно, компилятор сообщит вам если вы случайно сдвинули бит за пределы размера заданного типа.ncix
23.09.2016 12:00Спасибо, да, видел такую структурку, действительно удобно. Также видел удобную обертку, позволяющую писать еще лучше: РЕГИСТР.БИТ =
ittakir
23.09.2016 05:27+1Это очень удобно, потому что совпадает с даташитом — и названия регистров, и названия битов.
ncix
23.09.2016 09:33Вот и приходится прогать постоянно заглядывая в даташит, запомнить полсотни очень похожих аббревиатур непросто, ошибиться — запросто.
Alexey2005
23.09.2016 10:04Можно решить проблему самостоятельно, набрав полсотни #define с желаемыми именами.
4ebriking
23.09.2016 11:11Таки да, особо бесит когда и в даташите — мелким смазанным шрифтом, табличка в два стобца — большое поле имени регистра — и там в центре 4 буквы в огромной клеточке — они страницы pdf-ки экономили что ли? ил чернила в картридже?
Т.е эволюционно это понятно откуда взялось, но давно бы пора переключиться…
Особо это напрягает, когда это полу-хобби, полу-по-работе и возвращаешься к этому пару-тройку раз в год на недельку.
Alexeyslav
23.09.2016 10:53Эта проблема решается очень легко. #define новое_удобное_название_вещи = СТНЗВ в отдельном файле, и подключить его в начале проекта.
А вот что дальше потом с этим делать…ncix
23.09.2016 12:06Спасибо конечно, С/С++ тут думаю все неплохо знают. "=" там у вас лишнее.
Alexeyslav
23.09.2016 22:39Вот это конечно зря что оно там лишнее…
ncix
26.09.2016 10:37#define имя_макроса последовательность_символов
Разве нет?Alexeyslav
26.09.2016 16:13Я же говорю что зря в стандарт такое приняли, ненаглядно получается.
А так-то да, постоянно страдаю.ncix
26.09.2016 16:40Ну, может и не зря, можно задефайнить что-нибудь содержащее "=" а так бы путаница была.
Опять же бывают #define'ы без подстановки, что-то типа #define NO_DEBUGAlexeyslav
26.09.2016 19:50А так нельзя задефайнить нечто начинающееся с пробела…
Скорей всего это пустой дефайн, т.е. присваивает пустое множество.
wigneddoom
22.09.2016 21:50Спасибо за замечательную статью, в данной области их очень не хватает. Эх, когда же у меня руки дойдут плотно заняться С++ на MCU.
ncix
23.09.2016 09:47+1Да что может быть проще — берёте Arduino IDE и вперёд.
Бытует почему-то расхожее мнение что под Ардуино пишут на каком-то мистическом языке «wiring». На самом деле это настоящий честный C++ с шаблонами, классами и всем остальным, что поддерживает компилятор avr-gcc.wigneddoom
23.09.2016 13:12У меня руки не доходят до серьёзного и глубокого изучения С++, а это не «берёте Arduino IDE и вперёд».
Amomum
23.09.2016 00:02Осмелюсь подкинуть старую, но все еще любопытную статью на эту же тему Работа с портами ввода-вывода микроконтроллеров на Си++
father_mckenzie
23.09.2016 02:47Откровенно говоря начал было волноваться, когда увидел цикл в функции записи в порт. Когда дочитал до списков типов, успокоился, да это действительно соответствует теме моей статьи. Аналогичная проблема имеет место с ЖКИ тачскринами, при использовании покупного дисплея контакты шины данных устройства также оказываются раскидаными по разным битам разных портов. При использовании стратегий мы могли бы получить примерно следующий интерфейс:
template < class pin_ctrl, class device = LCD_C505, class orient = tftcd::ORIENTATION<PORTRAIT,240,320>, ... class dbg = NO_DEBUG > struct TFT_LCD;
Шаблонный параметр device скрывает все детали, специфические для конкретной модели дисплея, их существует множество и для каждой определен особый порядок инициализации как минимум. Параметр orient определяет ориентацию дисплея и позволяет существенно оптимизировать функции, связанные с расчетом координат.
А вот единственная вещь, которая является платформенно зависимой, это pin_ctrl — стратегия, которая скрывает всю механику работы с шиной данных устройства. На роль именно этой стратегии годится шаблон описанный в приведенной Вами статье.
Главный же класс TFT_LCD занимается собственно отрисовкой линий, окружностей и шрифтов.
mwaso
23.09.2016 05:37Спасибо за статью. Такое надо было на хабр в песочницу постить, а не сюда.
Инвайт на хабре дает полноценного юзера и на ГТ. Или я не прав?
eisaev
23.09.2016 10:45Валера, я рад, что ты добрался до написания данного материала. Прочитав четверть статьи, понял кто автор, а ник, которым она подписана, не оставил сомнений.
borisxm
23.09.2016 10:46Здесь мы опять полагаемся на компилятор, который успешно выполняет встраивание (inlining) и удаление тел пустых функций. При сборке релизной версии кода, весь неиспользуемый код будет удален.
Интересно. И как вы представляете себе удаление следующего кода:
extern volatile int somevar; dbg::debug("Somevar=%d\n", somevar);
?father_mckenzie
23.09.2016 11:00Я все-таки предположу, что пустая функция типа
void inline debug(const char*, type_of_somevar) { }
которая игнорирует аргументы, таки же будет удалена. Здесь print наверное имелся в виду, а не debug.
Однако ничто не мешает это проверить.
Или возможно я не понял Вашего вопроса?borisxm
23.09.2016 11:27Да, речь шла о функции print(), но суть в том, что переменная somevar объявлена как volatile и компилятор не может выкинуть обращение к ней не ломая семантику.
father_mckenzie
23.09.2016 11:35Стало быть правильно я Вас понял. Не возьму на себя смелость быть категоричным, однако мне все-таки кажется, что если внутри функции аргумент (каким бы он ни был) не используется, то это семантику не меняет.
По крайней мере примерно на таком утверждении основываются всяческие оптимизации компиляторов.borisxm
23.09.2016 13:08К сожалению, обращение к переменной будет оставлено, даже если все остальное тело функции будет исключено. Именно поэтому ничего лучше старых недобрых макросов для оборачивания дебажного вывода пока не предложено.
father_mckenzie
23.09.2016 13:53В принципе, если следовать этой логике, то как только мы обратились к volatile, мы уже нарушили семантику. Тогда уже при смене честной функции print на пустую и обратно семантика не меняется. А вот последующее комментирование (или исключение любым способом) функции исключает и обращение к переменной, что как раз семантику нарушает.
Идеальных вещей в мире не существует и приходится всегда помнить об ограничениях используемого инструмента.
В любом случае, спасибо Вам за важное замечание, к сожалению, без соответствующего тестирования и анализа я не готов обсуждать этот вопрос.
Strohmann
23.09.2016 10:46+1Спасибо за отличный список литературы. Который, видимо, никто не читает)
father_mckenzie
23.09.2016 10:49Вероятно Вы имеете в виду пункт 11? Вначале он появился как шутка, потом я решил оставить его в качестве маркера прочитали / не прочитали.
Вы первый, поздравляю!
ProstoTyoma
23.09.2016 19:59> Автоматическая генерация методов, неявное создание временных объектов могут привести к ощутимому снижению производительности результирующего кода
А чем автоматическая генерация методов уменьшает производительность?
Давно использую C++ на микроконтроллерах и не вижу никаких проблем. Правда, не на AVR и 8 бит, а Cortex-M3 и C2000. Места, в которых может снизиться производительность довольно очевидны. Если не уверен в чём-то, то смотрю что генерит компилятор и\или меряю производительность в конкретном месте. После нескольких таких подходов становится понятно, что можно, а что нельзя. Вобщем, это я к тому, что не надо бояться, надо анализировать и сопоставлять с задачей.
С шаблонами пробовал сам раньше. По моим ощущениям, читабельность падает, а требования к новым программистам повышаются.father_mckenzie
23.09.2016 21:16Очень правильное замечание. Должен признаться, мне пришлось одновременно работать над двумя версиями данной статьи. Русскоязычная версия находится перед вами, англоязычная, при благоприятном стечении обстоятельств, скоро тоже может стать доступной.
Говоря «производительность» (что у нас обычно воспринимается как скорость исполнения), я имею в виду значение слова «performance», которое имеет несколько более широкий смысл и в данном случае разумнее было бы применить слово «эффективность».
Спасибо Вам, видимо имеет смысл внести соответствующее исправление.
Что касается читабельности, зачастую код, в котором используются шаблоны, оказывается более читабельным. Разработка шаблонного кода штука действительно не простая, приходится иметь дело с очень недружелюбным синтаксисом, а вот для использования больших усилий не требуется, STL — хороший пример.
Еще раз спасибо за Вашу внимательность.
0xd34df00d
24.09.2016 03:01Статья хороша. Люблю такое, спасибо.
Однако, пришло время придираться.
Сначала общее: такое чувство, будто статья написана году в 2008-2009. Стандарту C++11 уже как бы так лет пять, поэтому вместоtype_selector
можно использовать std::conditional, вместо enum можно использовать enum class, и ещё можно спокойно говорить, что static_assert давно есть в языке C++, поэтому с диагностическими ошибками проблем чуть меньше (но они всё равно есть, и тут уместнее было бы сослаться на никак не принимаемые концепты).
В языках наподобие Питона функция может существовать в единственном экземпляре. Будучи языком со строгой типизацией, C++ потребует отдельного экземпляра функции для каждого использованного с ней типа.
Дело тут не в типизации (которая у C++ статическая, но не строгая, кстати). Дело в том, что C++ реализует полиморфизм в шаблонах через мономорфизацию — как вы и написали, для каждой комбинации шаблонных аргументов генерируется своя собственная версия шаблонной функции.
Однако, это не единственный способ реализации шаблонов. Можно таскать с собой этакий аналог vtable, словарь указателей на функции сравнения, проверки на равенство, и так далее, и тогда можно будет обойтись одной версией функции. Это уже подход Python, который динамически (относительно) строго типизирован, и подход, например, Haskell, который имеет строжайшую статическую типизацию. Haskell умеет мономорфизировать горячие функции, впрочем, через{-# SPECIALIZE #-}
, например.
Строгая типизация C++ потребует в качестве параметров указания значений, точно соответствующих объявленным типам данных.
Это требование легко ломается кастом, никто не помешает вам написать
YourClass<static_cast<YourEnum> (42)> instance;
ittakir
24.09.2016 07:37IAR вроде бы C++11 пока не поддерживает, к примеру.
0xd34df00d
24.09.2016 07:47Не скажу про IAR, но gcc поддерживает avr'ки в последних релизах в том числе.
father_mckenzie
24.09.2016 09:00Спасибо, порадовали. Честное слово, именно на этот эффект я очень надеялся. Первая пена прошла, рабочая неделя закончилась и настало время для вдумчивого прочтения-осознания.
По поводу типизации спорить не буду, она конечно же статическая, она же и строгая, в конце концов говорим мы здесь о C++ и упоминаем C. От кастинга enum-ов лично я бы воздержался, но удерживать кого-либо еще, не в праве. А вот по поводу причины отчего это все выглядит а-ля 2008. Причина видимо та же, что и у ребят, имена которых вы видите в списке литературы. Портируемость или как минимум стремление к ней. На моей рабочей машинке до сих пор живет win XP с 2008-й студией, там я бываю редко, а если и бываю, то лишь с одной целью — собрать какой-нибудь очередной кусок кода, который должен быть исполнен/продемонстрирован за пределами моего рабочего пространства.
Так что придирайтесь конечно, как бы мы ни старались, всегда еще остается над чем работать.
А за теплое слово спасибо, действительно приятно.
Arcanum7
Автору респект! Интересно и схемотехникам от программирования и программистам от схемотехники.