Top.Mail.Ru

Stm32. Переходим на современный C++. Часть 3. Виртуальные порты ввода-вывода.

Вступление.

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

Применение шаблонов C++ для создания виртуальных портов.

Представим, что мы хотим объединить линии PA0, PB4, PA3, PB2 в один четырехбитный порт. Сразу же напрашивается вполне очевидное объявление шаблона класса, в котором конкретные линии являются шаблонными аргументами:

template<typename... _Pins>
class VirtualPort{};

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

using ExampleVirtualPort = VirtualPort<Pa0, Pb4, Pa3, Pb2>;

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

Рассмотрим метод включения тактирования порта: void Enable(). Несложно догадаться, что для корректной реализации достаточно выполнения двух действий:

  1. Определить, какие физические порты участвуют в качестве части виртуального.
  2. Включить тактирование всех физических портов.

Самая примитивная, но неоптимальная, реализация этого метода может выглядеть так:

static void Enable()
{
    (_Pins::Port::Enable(), ...);
}

На этапе компиляции для предложенного нами примера свертка (_Pins::Port::Enable(), ...) преобразуется в последовательный вызов четырех методов:

Pa0::Port::Enable(); // Включение тактирования порта A;
Pb4::Port::Enable(); // Включение тактирования порта B;
Pa3::Port::Enable(); // Включение тактирования порта A (повторно!);
Pb2::Port::Enable(); // Включение тактирования порта B (повторно!);

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

Теперь рассмотрим любой метод управления портом, потому что их реализация похожа: в целом нет разницы между настройкой конфигурации, режима виртуального порта или записи в него числового значения. Остановимся на методе установки битов. Прототип метода возьмем из класса обычного порта: void Set(DataT value);

К сожалению, очевидного решения, как в случае с методом включения тактирования, здесь не существует, поскольку параметр метода (значение для записи) шаблонным не является (это было бы странным), соответственно задачу невозможно разрешить шаблонными методами в compile-time.

Решим несколько вспомогательных задач.

Определение принадлежности линий виртуального порта физическому.

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

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

Само понятие списка типов очень простое (стоит отметить, что простым оно стало со стандарта C++17, а до этого использовалось рекурсивное определение, которое предложил Андрей Александреску в книге "Современное проектирование на С++" и реализации своих идей в виде библиотеки LOKI):

template<typename... Types>
class TypeList{};

// Использование
using MyTypeList = TypeList<int, int, double, std::vector>;
// Применительно к нашему случаю
using MyPins = TypeList<Pa0, Pb4, Pa3, Pb2>;

Внимательный читатель скажет, что это то же самое, что std::tuplestd::tuple и будет абсолютно прав!

Сам по себе список типов никакой пользы не несет, пока мы не определили возможные операции над ним. К базовым можно отнести следующие:

  • Взятие типа по его индексу;
  • Определение индекса типа в списке;
  • Определение принадлежности типа списку типов.

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

// Базовый класс для дальнейшей специализации
// Первый шаблонный параметр - номер типа
// Второй (variadic) - список типов
template<int, typename...>
class GetType {};

// Специализация дна рекурсии, когда из списка <Head, Tail>
// нужно извлечь начальный (нулевой) тип.
template<typename Head, typename... Tail>
class GetType<0, TypeList<Head, Tail...>>
{
public:
    using type = Head;
};

// Общий случай: если Index > 0, то берем тип с номером
// Index - 1 из хвоста списка
template<int Index, typename Head, typename... Tail>
class GetType<Index, TypeList<Head , Tail...>>
{
    static_assert(Index < Length<TypeList<Head, Tail...>>::value);
public:
    using type = typename GetType<Index - 1, TypeList<Tail...>>::type;
};

Но есть и более продвинутые задачи, например:

  • Удаление дубликатов из списка;
  • Сортировка по предикату;
  • Поиск (фильтрация) типов по предикату.

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

template<template <typename> class Predic