Русский
Русский
English
Статистика
Реклама

Метапрограммирование

Из песочницы Валидация данных в C с использованием библиотеки cpp-validator

27.10.2020 12:09:43 | Автор: admin


Казалось бы, валидация данных это одна из базовых задач в программировании, которая встретится и в начале изучения языка вместе с "Hello world!", и в том или ином виде будет присутствовать в множестве зрелых проектов. Тем не менее, Google до сих пор выдает ноль релевантных результатов при попытке найти универсальную библиотеку валидации данных с открытым исходным кодом на C++.


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


Если в комментариях кто-то сможет привести примеры открытых библиотек валидации данных на C++ помимо отдельных GUI-форм, то буду очень признателен и добавлю соответствующий список в статью.


Содержание



Мотивация


Стоить отметить, что мотивом к разработке валидатора данных для C++ послужило не столько отсутствие подобной библиотеки, сколько желание получить инструмент, при помощи которого можно было бы единообразно описывать:


  • условия выборки записей в базе данных;
  • условия проверки на сервере содержимого команд, поступающих от клиентов;
  • условия проверки данных, вводимых через пользовательский интерфейс на клиенте, перед их записью в объект и отправкой на сервер.

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


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


  • описанием правил валидации;
  • реализацией обработчиков правил валидации;
  • обработкой конкретных правил валидации конкретным обработчиком.

То есть, правила валидации должны предварительно описываться в отдельном месте, желательно с применением синтаксиса, похожего на декларативный. В другом месте должны быть реализованы обработчики правил валидации. Причем, разные обработчики могут транслировать те же самые правила в разные операции например, один обработчик использует правила для фактической проверки данных, а другой обработчик транслирует правила в запросы SQL. И, собственно, третий участок это непосредственно применение правил валидации конкретным обработчиком в момент вызова процедуры валидации.


Возможности библиотеки


cpp-validator является header-only библиотекой для современного C++ с поддержкой стандартов C++14/C++17. В коде cpp-validator активно используется метапрограммирование на шаблонах и библиотека Boost.Hana.


Основные возможности библиотеки cpp-validator перечислены ниже.


  • Валидация данных для различных конструкций языка:
    • простых переменных;
    • свойств объектов, включая:
      • переменные классов;
      • методы классов вида getter;
    • содержимого и свойств контейнеров;
    • иерархических типов данных, таких как вложенные объекты и контейнеры.
  • Пост-валидация объектов, когда проверяется содержимое уже заполненного объекта на соответствие сразу всем правилам.
  • Пре-валидация данных, когда перед записью в объект проверяются только те свойства, которые планируется изменить.
  • Комбинация правил с использованием логических связок AND, OR и NOT.
  • Массовая проверка элементов контейнеров с условиями ALL или ANY.
  • Частично подготовленные правила валидации с отложенной подстановкой аргументов (lazy operands).
  • Сравнение друг с другом разных свойств одного и того же объекта.
  • Автоматическая генерация описания ошибок валидации:
    • широкие возможности по настройке генерации текста ошибок;
    • перевод текста ошибок на различные языки с учетом грамматических атрибутов слов, например, числа, рода и т.д.
  • Расширяемость:
    • регистрация новых свойств объектов, доступных для валидации;
    • добавление новых операторов правил валидации;
    • добавление новых обработчиков правил валидации (адаптеров).
  • Операторы, уже встроенные в библиотеку:
    • сравнения;
    • лексикографические, с учетом и без учета регистра;
    • существования элементов;
    • проверки вхождения в интервал или набор;
    • регулярные выражения.
  • Широкая поддержка платформ и компиляторов, включая компиляторы Clang, GCC, MSVC и операционные системы Windows, Linux, macOS, iOS, Android.

Использование библиотеки


Базовая валидация данных с использованием cpp-validator выполняется в три шага:


  1. сперва создается валидатор, содержащий правила валидации, описанные с использованием почти-декларативного языка;
  2. затем валидатор применяется к объекту валидации;
  3. в конце проверяется результат валидации, для работы с которым может использоваться либо специальный объект ошибки, либо исключение.

// определение валидатораauto container_validator=validator(   _[size](eq,1), // размер контейнера должен быть равен 1   _["field1"](exists,true), // поле "field1" должно существовать в контейнере   _["field1"](ne,"undefined") // поле "field1" должно быть не равно "undefined");// успешная валидацияstd::map<std::string,std::string> map1={{"field1","value1"}};validate(map1,container_validator);// неуспешная валидация, с объектом ошибкиerror_report err;std::map<std::string,std::string> map2={{"field2","value2"}};validate(map2,container_validator,err);if (err){    std::cerr<<err.message()<<std::endl;    /* напечатает:    field1 must exist    */}// неуспешная валидация, с исключениемtry{    std::map<std::string,std::string> map3={{"field1","undefined"}};    validate(map3,container_validator);}catch(const validation_error& ex){    std::cerr<<ex.what()<<std::endl;    /* напечатает:    field1 must be not equal to undefined    */}

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


Текущий статус библиотеки


Библиотека cpp-validator доступна на GitHub по адресу https://github.com/evgeniums/cpp-validator и готова к использованию на момент написания статьи номер стабильной версии 1.0.2. Библиотека распространяется под лицензией Boost 1.0.


Приветствуются замечания, пожелания и дополнения.


Примеры


Тривиальная валидация числа


// определение валидатораauto v=validator(gt,100); // больше чем 100// объект ошибкиerror err;// условия не выполненыvalidate(90,v,err);if (err){  // валидация неуспешна}// условия выполненыvalidate(200,v,err);if (!err){  // валидация успешна}

Валидация с исключением


// определение валидатораauto v=validator(gt,100); // больше чем 100try{    validate(200,v); // успешно    validate(90,v); // генерирует исключение}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /* напечатает:    must be greater than 100    */}

Явное применение валидатора к переменной


// определение валидатораauto v=validator(gt,100); // больше чем 100// применить валидатор к переменнымint value1=90;if (!v.apply(value1)){  // валидация неуспешна}int value2=200;if (v.apply(value2)){  // валидация успешна}

Составной валидатор


// валидатор: размер меньше 15 и значение бинарно больше или равно "sample string"auto v=validator(  length(lt,15),  value(gte,"sample string"));// явное применение валидатора к переменнымstd::string str1="sample";if (!v.apply(str1)){  // валидация неупешна потому что sample бинарно меньше, чем sample string}std::string str2="sample string+";if (v.apply(str2)){  // валидация успешна}std::string str3="too long sample string";if (!v.apply(str3)){  // валидация неуспешна, потому что длина строки больше 15 символов}

Проверить, что число входит в интервал, и напечатать описание ошибки


// валидатор: входит в интервал [95,100]auto v=validator(in,interval(95,100));// объект ошибкиerror_report err;// проверить значениеsize_t val=90;validate(val,v,err);if (err){    std::cerr << err.message() << std::endl;     /* напечатает:    must be in interval [95,100]    */}

Составной валидатор для проверки элемента контейнера


// составной валидаторauto v=validator(                _["field1"](gte,"xxxxxx")                 ^OR^                _["field1"](size(gte,100) ^OR^ value(gte,"zzzzzzzzzzzz"))            );// валидация контейнера и печать ошибкиerror_report err;std::map<std::string,std::string> test_map={{"field1","value1"}};validate(test_map,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    field1 must be greater than or equal to xxxxxx OR size of field1 must be greater than or equal to 100 OR field1 must be greater than or equal to zzzzzzzzzzzz    */}

Проверить элементы вложенных контейнеров


// составной валидатор элементов вложенных контейнеровauto v=validator(                _["field1"][1](in,range({10,20,30,40,50})),                _["field1"][2](lt,100),                _["field2"](exists,false),                _["field3"](empty(flag,true))            );// валидация вложенного контейнера и печать ошибкиerror_report err;std::map<std::string,std::map<size_t,size_t>> nested_map={            {"field1",{{1,5},{2,50}}},            {"field3",{}}        };validate(nested_map,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    element #1 of field1 must be in range [10, 20, 30, 40, 50]    */}

Провести валидацию кастомного свойства объекта


// структура с getter методомstruct Foo{    bool red_color() const    {        return true;    }};// зарегистрировать новое свойство red_colorDRACOSHA_VALIDATOR_PROPERTY_FLAG(red_color,"Must be red","Must be not red");// валидатор зарегистрированного свойства red_colorauto v=validator(    _[red_color](flag,false));// провести валидацию кастомного свойства и напечатать ошибкуerror_report err;Foo foo_instance;validate(foo_instance,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    "Must be not red"    */}

Пре-валидация данных перед записью


// структура с переменными и методом вида setterstruct Foo{    std::string bar_value;    uint32_t other_value;    size_t some_size;    void set_bar_value(std::string val)    {        bar_value=std::move(val);    }};using namespace DRACOSHA_VALIDATOR_NAMESPACE;// зарегистрировать кастомные свойстваDRACOSHA_VALIDATOR_PROPERTY(bar_value);DRACOSHA_VALIDATOR_PROPERTY(other_value);// специализация шаблона класса set_member_t для записи свойства bar_value структуры FooDRACOSHA_VALIDATOR_NAMESPACE_BEGINtemplate <>struct set_member_t<Foo,DRACOSHA_VALIDATOR_PROPERTY_TYPE(bar_value)>{    template <typename ObjectT, typename MemberT, typename ValueT>    void operator() (            ObjectT& obj,            MemberT&&,            ValueT&& val        ) const    {        obj.set_bar_value(std::forward<ValueT>(val));    }};DRACOSHA_VALIDATOR_NAMESPACE_END// валидатор с кастомными свойствамиauto v=validator(    _[bar_value](ilex_ne,"UNKNOWN"), // лексикографическое "не равно" без учета регистра    _[other_value](gte,1000) // больше или равно 1000);Foo foo_instance;error_report err;// запись валидного значение в свойство bar_value объекта foo_instanceset_validated(foo_instance,bar_value,"Hello world",v,err);if (!err){    // свойство bar_value объекта foo_instance успешно записано}// попытка записи невалидного значение в свойство bar_value объекта foo_instanceset_validated(foo_instance,bar_value,"unknown",v,err);if (err){    // запись не удалась    std::cerr << err.message() << std::endl;    /* напечатает:     bar_value must be not equal to UNKNOWN     */}

Один и тот же валидатор для пост-валидации и пре-валидации


#include <iostream>#include <dracosha/validator/validator.hpp>#include <dracosha/validator/validate.hpp>using namespace DRACOSHA_VALIDATOR_NAMESPACE;namespace validator_ns {// зарегистрировать getter свойства "x"DRACOSHA_VALIDATOR_PROPERTY(GetX);// валидатор GetXauto MyClassValidator=validator(   /*    "x" в кавычках - это имя поля, которое писать в отчете вместо GetX;   interval.open() - модификатор открытого интервала без учета граничных точек   */   _[GetX]("x")(in,interval(0,500,interval.open())) );}using namespace validator_ns;// определение тестового класса  class MyClass {  double x;public:  // Конструктор с пост-валидацией  MyClass(double _x) : x(_x) {      validate(*this,MyClassValidator);  }  // Getter  double GetX() const noexcept  {     return _x;  }  // Setter с пре-валидацией  void SetX(double _x) {    validate(_[validator_ns::GetX],_x,MyClassValidator);    x = _x;  }};int main(){// конструктор с валидным аргументомtry {    MyClass obj1{100.0}; // ok}catch (const validation_error& err){}// конструктор с невалидным аргументомtry {    MyClass obj2{1000.0}; // значение вне интервала}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /*     напечатает:     x must be in interval(0,500)    */}MyClass obj3{100.0};// запись с валидным аргументомtry {    obj3.SetX(200.0); // ok}catch (const validation_error& err){}// попытка записи с невалидным аргументомtry {    obj3.SetX(1000.0); // значение вне интервала}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /*     напечатает:     x must be in interval (0,500)    */}return 0;}

Перевод ошибок валидации на русский язык


// переводчик ключей контейнера на русский язык с учетом рода, падежа и числаphrase_translator tr;tr["password"]={                    {"пароль"},                    {"пароля",grammar_ru::roditelny_padezh}               };tr["hyperlink"]={                    {{"гиперссылка",grammar_ru::zhensky_rod}},                    {{"гиперссылки",grammar_ru::zhensky_rod},grammar_ru::roditelny_padezh}                };tr["words"]={                {{"слова",grammar_ru::mn_chislo}}            };/* финальный переводчик включает в себя встроенный переводчик на русскийvalidator_translator_ru() и переводчик tr для имен элементов*/auto tr1=extend_translator(validator_translator_ru(),tr);// контейнер для валидацииstd::map<std::string,std::string> m1={    {"password","123456"},    {"hyperlink","zzzzzzzzz"}};// адаптер с генерацией отчета об ошибке на русском языкеstd::string rep;auto ra1=make_reporting_adapter(m1,make_reporter(rep,make_formatter(tr1)));// различные валидаторы и печать ошибок на русском языкеauto v1=validator(    _["words"](exists,true) );if (!v1.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    слова должны существовать    */}rep.clear();auto v2=validator(    _["hyperlink"](eq,"https://www.boost.org") );if (!v2.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    гиперссылка должна быть равна https://www.boost.org    */}rep.clear();auto v3=validator(    _["password"](length(gt,7)) );if (!v3.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    длина пароля должна быть больше 7    */}rep.clear();auto v4=validator(    _["hyperlink"](length(lte,7)) );if (!v4.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    длина гиперссылки должна быть меньше или равна 7    */}rep.clear();
Подробнее..

Из песочницы Включаем периферию контроллера за 1 такт или магия 500 строк кода

16.11.2020 12:07:26 | Автор: admin


Как часто, при разработке прошивки для микроконтроллера, во время отладки, когда байтики не бегают по UART, вы восклицаете: Ааа, точно! Не включил тактирование!. Или, при смене ножки светодиода, забывали подать питание на новый порт? Думаю, что довольно часто. Я, по крайней мере, уж точно.

На первый взгляд может показаться, что управление тактированием периферии тривиально: записал 1 включил, 0 выключил.

Но просто, не всегда оказывается эффективно

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


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

  • Во встраиваемых системах, один из самых главных критериев это минимально-возможный результирующий код, исполняемый за минимальное время
  • Легкая масштабируемость. Добавление или изменение в проекте какой-либо периферии не должно сопровождаться code review всех исходников, чтобы удалить строчки включения/отключения тактирования
  • Пользователь должен быть лишен возможности совершить ошибку, либо, по крайней мере эта возможность должна быть сведена к минимуму
  • Нет необходимости работы с отдельными битами и регистрами
  • Удобство и однообразность использования независимо от микроконтроллера
  • Помимо основных возможностей включения и выключения тактирования периферии необходим расширенный функционал (о нем речь пойдет далее)

После выяснения критериев оценки, поставим конкретную задачу, попутно определив условия и окружение для реализации:

Компилятор: GCC 10.1.1 + Make
Язык: C++17
Среда: Visual Studio Code
Контроллер: stm32f103c8t6 (cortex-m3)
Задача: включение тактирования SPI2, USART1 (оба интерфейса с использованием DMA)

Выбор данного контроллера обусловлен, естественно, его распространённостью, особенно, благодаря одному из китайских народных промыслов производству плат Blue Pill.



С точки зрения идеологии, совершенно неважно, какой именно контроллер выбран: stmf1, stmf4 или lpc, т.к. работа с системой тактирования периферии сводится лишь к записи в определенный бит либо 0 для выключения, либо 1 для включения.

В stm32f103c8t6 имеется 3 регистра, которые ответственны за включение тактирования периферии: AHBENR, APB1ENR, APB2ENR.

Аппаратные интерфейсы передачи данных SPI2 и USART1 выбраны неслучайно, потому что для их полноценного функционирования необходимо включить биты тактирования, расположенные во всех перечисленных регистрах биты самих интерфейсов, DMA1, а также биты портов ввода-вывода (GPIOB для SPI2 и GPIOA для USART1).




Следует отметить, что для оптимальной работы с тактированием, необходимо учитывать AHBENR содержит разделяемый ресурс, используемые для функционирования как SPI2, так и USART1. То есть, отключение DMA сразу приведет к неработоспособности обоих интерфейсов, вместе с тем, КПД повторного включения будет даже не нулевым, а отрицательным, ведь эта операция займет память программ и приведет к дополнительному расходу тактов на чтение-модификацию-запись volatile регистра.

Разобравшись с целями, условиями и особенностями задачи, перейдем к поиску решений.

Основные подходы


В этом разделе собраны типовые способы включения тактирования периферии, которые мне встречались и, наверняка, Вы их также видели и/или используете. От более простых, реализуемых на C, до fold expression из C++17. Рассмотрены присущие им достоинства и недостатки.

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

Прямая запись в регистры


Классический способ, доступный из коробки и для С и для C++. Вендор, чаще всего, представляет заголовочные файлы для контроллера, в которых задефайнены все регистры и их биты, что дает возможность сразу начать работу с периферией:

int main(){  RCC->AHBENR  |= RCC_AHBENR_DMA1EN;  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN               |  RCC_APB2ENR_IOPBEN               |  RCC_APB2ENR_USART1EN;  RCC->APB2ENR |= RCC_APB1ENR_SPI2EN;}

Листинг
    // AHBENR(Включение DMA1)  ldr     r3, .L3  ldr     r2, [r3, #20]  orr     r2, r2, #1  str     r2, [r3, #20]    // APB2ENR(Включение GPIOA, GPIOB, USART1)  ldr     r2, [r3, #24]  orr     r2, r2, #16384  orr     r2, r2, #12  str     r2, [r3, #24]    // APB1ENR(Включение SPI2)  ldr     r2, [r3, #28]  orr     r2, r2, #16384  str     r2, [r3, #28]


Размер кода: 36 байт. Посмотреть

Плюсы:

  • Минимальный размер кода и скорость выполнения
  • Самый простой и очевидный способ

Минусы:

  • Необходимо помнить и названия регистров и названия битов, либо постоянно обращаться к мануалу
  • Легко допустить ошибку в коде. Читатель, наверняка, заметил, что вместо SPI2 был повторно включен USART1
  • Для работы некоторых периферийных блоков требуется также включать другую периферию, например, GPIO и DMA для интерфейсов
  • Полное отсутствие переносимости. При выборе другого контроллера этот код теряет смысл

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

Функции инициализации


Давайте попробуем абстрагироваться и спрятать работу с регистрами от пользователя. И в этом нам поможет обыкновенная C-функция:

void UART1_Init(){ RCC->AHBENR  |= RCC_AHBENR_DMA1EN; RCC->APB2ENR |= RCC_APB2ENR_IOPAEN              |  RCC_APB2ENR_USART1EN;  // Остальная инициализация}void SPI2_Init(){ RCC->AHBENR  |= RCC_AHBENR_DMA1EN; RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;  // Остальная инициализация}int main(){  UART1_Init();  SPI2_Init();}

Размер кода: 72 байта. Посмотреть

Листинг
UART1_Init():    // AHBENR(Включение DMA1)  ldr     r2, .L2  ldr     r3, [r2, #20]  orr     r3, r3, #1  str     r3, [r2, #20]    // APB2ENR(Включение GPIOA, USART1)  ldr     r3, [r2, #24]  orr     r3, r3, #16384  orr     r3, r3, #4  str     r3, [r2, #24]  bx      lrSPI2_Init():    //Повторно (!) AHBENR(Включение DMA1)  ldr     r3, .L5  ldr     r2, [r3, #20]  orr     r2, r2, #1  str     r2, [r3, #20]    //Повторно (!) APB2ENR(Включение GPIOB)  ldr     r2, [r3, #24]  orr     r2, r2, #8  str     r2, [r3, #24]    //Запись в APB1ENR(Включение SPI2)  ldr     r2, [r3, #28]  orr     r2, r2, #16384  str     r2, [r3, #28]  bx      lrmain:   push    {r3, lr}   bl      UART1_Init()   bl      SPI2_Init()


Плюсы:

  • Можно не заглядывать в мануал по каждому поводу
  • Ошибки локализованы на этапе написания драйвера периферии
  • Пользовательский код легко воспринимать

Минусы:

  • Количество необходимых инструкций возросло кратно количеству задействованной периферии
  • Очень много дублирования кода для каждого номера UART и SPI он будет фактически идентичен

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

Функция включения тактирования


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

void PowerEnable(uint32_t ahb, uint32_t apb2, uint32_t apb1){    RCC->AHBENR  |= ahb;    RCC->APB2ENR |= apb2;    RCC->APB1ENR |= apb1;}void UART_Init(int identifier){    uint32_t ahb = RCC_AHBENR_DMA1EN, apb1 = 0U, apb2 = 0U;    if (identifier == 1){      apb2 = RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;    }     else if (identifier == 2){}    PowerEnable(ahb, apb2, apb1);  // Остальная инициализация}void SPI_Init(int identifier){    uint32_t ahb = RCC_AHBENR_DMA1EN, apb1 = 0U, apb2 = 0U;    if (identifier == 1){}     else if (identifier == 2){      apb2 = RCC_APB2ENR_IOPBEN;      apb1 = RCC_APB1ENR_SPI2EN;    }    PowerEnable(ahb, apb2, apb1);  // Остальная инициализация}int main(){  UART_Init(1);  SPI_Init(2);}

Размер кода: 92 байта. Посмотреть

Листинг
PowerEnable(unsigned long, unsigned long, unsigned long):  push    {r4}  ldr     r3, .L3  ldr     r4, [r3, #20]  orrs    r4, r4, r0  str     r4, [r3, #20]  ldr     r0, [r3, #24]  orrs    r0, r0, r1  str     r0, [r3, #24]  ldr     r1, [r3, #28]  orrs    r1, r1, r2  str     r1, [r3, #28]  pop     {r4}  bx      lrUART_Init(int):  push    {r3, lr}  cmp     r0, #1  mov     r2, #0  movw    r1, #16388  it      ne  movne   r1, r2  movs    r0, #1  bl      PowerEnable(unsigned long, unsigned long, unsigned long)  pop     {r3, pc}SPI_Init(int):  push    {r3, lr}  cmp     r0, #2  ittee   eq  moveq   r1, #8  moveq   r1, #16384  movne   r1, #0  movne   r2, r1  movs    r0, #1  bl      PowerEnable(unsigned long, unsigned long, unsigned long)  pop     {r3, pc}main:   push    {r3, lr}   movs    r0, #1   bl      UART_Init(int)   movs    r0, #2   bl      SPI_Init(int)


Плюсы:
  • Удалось сократить код описания драйверов микроконтроллера
  • Результирующее количество инструкций сократилось*

Минусы:

  • Увеличилось время выполнения

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

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

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

Свойства-значения и шаблоны


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

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

struct Power{template< uint32_t valueAHBENR, uint32_t valueAPB2ENR, uint32_t valueAPB1ENR>    static void Enable(){// Если значение = 0, то в результирующем коде операций с регистром не будет        if constexpr (valueAHBENR)            RCC->AHBENR |= valueAHBENR;        if constexpr (valueAPB2ENR)            RCC->APB2ENR |= valueAPB2ENR;        if constexpr (valueAPB1ENR)            RCC->APB1ENR |= valueAPB1ENR;    };};template<auto identifier>struct UART{// С помощью identifier на этапе компиляции можно выбрать значения для периферии  static constexpr auto valueAHBENR = RCC_AHBENR_DMA1EN;  static constexpr auto valueAPB1ENR = identifier == 1 ? 0U : RCC_APB1ENR_USART2EN;  static constexpr auto valueAPB2ENR = RCC_APB2ENR_IOPAEN                                    |  (identifier == 1 ? RCC_APB2ENR_USART1EN : 0U);    // Остальная реализация};template<auto identifier>struct SPI{  static constexpr auto valueAHBENR = RCC_AHBENR_DMA1EN;  static constexpr auto valueAPB1ENR = identifier == 1 ? 0U : RCC_APB1ENR_SPI2EN;  static constexpr auto valueAPB2ENR = RCC_APB2ENR_IOPBEN                                    |  (identifier == 1 ? RCC_APB2ENR_SPI1EN : 0U);    // Остальная реализация};int main(){    // Необязательные псевдонимы для используемой периферии  using uart = UART<1>;  using spi = SPI<2>;  Power::Enable<                uart::valueAHBENR  | spi::valueAHBENR,                uart::valueAPB2ENR | spi::valueAPB2ENR,                uart::valueAPB1ENR | spi::valueAPB1ENR                >();}

Размер кода: 36 байт. Посмотреть

Листинг
main:    // AHBENR(Включение DMA1)  ldr     r3, .L3  ldr     r2, [r3, #20]  orr     r2, r2, #1  str     r2, [r3, #20]    // APB2ENR(Включение GPIOA, GPIOB, USART1)  ldr     r2, [r3, #24]  orr     r2, r2, #16384  orr     r2, r2, #12  str     r2, [r3, #24]    // APB1ENR(Включение SPI2)  ldr     r2, [r3, #28]  orr     r2, r2, #16384  str     r2, [r3, #28]


Плюсы:

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

Минусы:

  • Можно совершить ошибку, поставив свойство-значение не в тот параметр
  • Как и в случае с прямой записью в регистры страдает переносимость
  • Перегруженность конструкции

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

Идеальный вариант почти


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

struct Power{template<typename... Peripherals>  static void Enable(){      // Для всех параметров пакета будет применена операция |       // В нашем случае value = uart::valueAHBENR | spi::valueAHBENR и т.д.    if constexpr (constexpr auto value = (Peripherals::valueAHBENR | ... ); value)      RCC->AHBENR |= value;    if constexpr (constexpr auto value = (Peripherals::valueAPB2ENR | ... ); value)      RCC->APB2ENR |= value;    if constexpr (constexpr auto value = (Peripherals::valueAPB1ENR | ... ); value)      RCC->APB1ENR |= value;  };};    int main(){    // Необязательные псевдонимы для используемой периферии  using uart = UART<1>;  using spi = SPI<2>;  Power::Enable<uart, spi>();}

Размер кода: 36 байт. Посмотреть

Листинг
main:    // AHBENR(Включение DMA1)  ldr     r3, .L3  ldr     r2, [r3, #20]  orr     r2, r2, #1  str     r2, [r3, #20]    // APB2ENR(Включение GPIOA, GPIOB, USART1)  ldr     r2, [r3, #24]  orr     r2, r2, #16384  orr     r2, r2, #12  str     r2, [r3, #24]    // APB1ENR(Включение SPI2)  ldr     r2, [r3, #28]  orr     r2, r2, #16384  str     r2, [r3, #28]


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

И, вроде бы, можно на этом остановиться, но


Расширяем функционал


Обратимся к одной из поставленных целей:
Помимо основных возможностей включения и выключения тактирования периферии необходим расширенный функционал

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

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

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

int main(){  using uart = UART<1>;  using spi = SPI<2>;    // Включаем USART, SPI, DMA, GPIOA, GPIOB  Power::Enable<uart, spi>();    // Some code    // Выключаем SPI и GPIOB вместе (!) с DMA  Power::Disable<spi>();        // Включаем обратно DMA вместе(!) с USART и GPIOA  Power::Enable<uart>();        // Sleep();    // Включаем SPI и GPIOB вместе(!) с DMA  Power::Enable<spi>();}

Размер кода: 100 байт. Посмотреть

Листинг
main:        // AHBENR(Включение DMA1)        ldr     r3, .L3        ldr     r2, [r3, #20]        orr     r2, r2, #1        str     r2, [r3, #20]       // APB2ENR(Включение GPIOA, GPIOB, USART1)        ldr     r2, [r3, #24]        orr     r2, r2, #16384        orr     r2, r2, #12        str     r2, [r3, #24]       // APB1ENR(Включение SPI2)        ldr     r2, [r3, #28]        orr     r2, r2, #16384        str     r2, [r3, #28]        // Выключение SPI2       // AHBENR(Выключение DMA1)        ldr     r2, [r3, #20]        bic     r2, r2, #1        str     r2, [r3, #20]       // APB2ENR(Выключение GPIOB)        ldr     r2, [r3, #24]        bic     r2, r2, #8        str     r2, [r3, #24]       // APB1ENR(Выключение SPI2)        ldr     r2, [r3, #28]        bic     r2, r2, #16384        str     r2, [r3, #28]        // Повторное (!) включение USART1        // AHBENR(Включение DMA1)        ldr     r2, [r3, #20]        orr     r2, r2, #1        str     r2, [r3, #20]       // APB2ENR(Включение GPIOA, USART1)        ldr     r2, [r3, #24]        orr     r2, r2, #16384        orr     r2, r2, #4        str     r2, [r3, #24]        // Sleep();        // AHBENR(Включение DMA1)        ldr     r2, [r3, #20]        orr     r2, r2, #1        str     r2, [r3, #20]       // APB2ENR(Включение GPIOB)        ldr     r2, [r3, #24]        orr     r2, r2, #8        str     r2, [r3, #24]       // APB1ENR(Включение SPI2)        ldr     r2, [r3, #28]        orr     r2, r2, #16384        str     r2, [r3, #28]


В это же время эталонный код на регистрах занял 68 байт. Посмотреть

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

Давайте попробуем найти решение

Структура


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



Она состоит всего из четырех блоков:

Независимые:

  • IPower интерфейс взаимодействия с пользователем, подготавливающий данные для записи в регистры
  • Hardware запись значений в регистры контроллера

Аппаратно-зависимые:
  • Peripherals периферия, которая используется в проекте и сообщает интерфейсу, какие устройства надо включить или выключить
  • Adapter передает значения для записи в Hardware, указывая в какие именно регистры их следует записать

Интерфейс IPower


С учетом всех требований, определим методы, необходимые в интерфейсе:

template<typename Peripherals>Enable();template<typename EnableList, typename ExceptList>EnableExcept();template<typename EnableList, typename DisableList>Keep();

Enable включение периферии, указанной в параметре шаблона.

EnableExcept включение периферии, указанной в параметре EnableList, за исключением той, что указана в ExceptList.

Пояснение
Таблица истинности
Бит включения Бит исключения Результат включение Результат выключение
0 0 0 0
0 1 0 0
1 0 1 0
1 1 0 0

Например, вызов:
EnableExcept<spi, uart>();

должен установить бит SPI2EN и бит IOPBEN. В то время, как общий DMA1EN, а также USART1EN и IOPAEN останутся в исходном состоянии.

Чтобы получить соответствующую таблицу истинности, необходимо произвести следующие операции:

resultEnable = (enable ^ except) & enable


К ним в дополнение также идут комплементарные методы Disable, выполняющие противоположные действия.

Keep включение периферии из EnableList, выключение периферии из DisableList, при этом, если периферия присутствует в обоих списках, то она сохраняет свое состояние.

Пояснение
Таблица истинности
Бит включения Бит выключения Результат включение Результат выключение
0 0 0 0
0 1 0 1
1 0 1 0
1 1 0 0

Например, при вызове:
Keep<spi, uart>();

установятся SPI2EN и IOPBEN, при этом USART1EN и IOPAEN сбросятся, а DMA1EN останется неизменным.

Чтобы получить соответствующую таблицу истинности, необходимо произвести следующие операции:

resultEnable = (enable ^ disable) & enableresultDisable = (enable ^ disable) & disable


Методы включения/выключения уже реализованы довольно неплохо с помощью fold expression, но как быть с остальными?

Если ограничиться использованием 2 видов периферии, как это сделано в пояснении, то никаких сложностей не возникнет. Однако, когда в проекте используется много различных периферийных устройств, появляется проблема в шаблоне нельзя явно использовать более одного parameter pack, т.к. компилятор не сможет определить где заканчивается один и начинается второй:

template<typename EnableList, typename ExceptList>EnableExcept(){};  // Невозможно определить где заканчивается EnableList и начинается ExceptListEnableExcept<spi2, pin3, uart1, pin1, i2c3>();

Можно было бы создать отдельный класс-обертку для периферии и передавать его в метод:

template<typename Peripherals>PowerWrap{  static constexpr auto valueAHBENR = (Peripherals::valueAHBENR | );  static constexpr auto valueAPB1ENR = (Peripherals:: valueAPB1ENR | );  static constexpr auto valueAPB2ENR = (Peripherals:: valueAPB2ENR | );};using EnableList = PowerWrap<spi2, uart1>;using ExceptList = PowerWrap<pin1, i2c1>;EnableExcept<EnableList, ExceptList>();

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

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

Метапрограммирование


Из-за того, что в основе метапрограммирования лежит работа не с обычными типами, а с их списками, то определим две сущности, которые будут оперировать типовыми и нетиповыми параметрами:

template<typename... Types>struct Typelist{};template<auto... Values>struct Valuelist{};using listT = Typelist<char, int> ;// Список из последовательности типов char и intusing listV = Valuelist<8,9,5,11> ;// Список из 4 нетиповых параметров

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

1. Извлечение первого элемента из списка

front
  // Прототип функцииtemplate<typename List>struct front;  // Специализация для списка типов  // Разделение списка в пакете параметров на заглавный и оставшиесяtemplate<typename Head, typename... Tail>struct front<Typelist<Head, Tail...>>{     // Возвращение заглавного типа  using type = Head; }; // Специализация для списка нетиповых параметровtemplate<auto Head, auto... Tail>struct front<Valuelist<Head, Tail...>> {  // Возвращение заглавного значения  static constexpr auto value = Head;};  // Псевдонимы для простоты использованияtemplate<typename List>using front_t = typename front<List>::type;template<typename List>static constexpr auto front_v = front<List>::value;  // Примерыusing listT = Typelist<char, bool, int>;using type = front_t<listT>; // type = charusing listV = Valuelist<9,8,7>;constexpr auto value = front_v<listV>; //value = 9


2. Удаление первого элемента из списка

pop_front
template<typename List>struct pop_front;  // Специализация для списка типов  // Разделение списка в пакете параметров на заглавный и оставшиесяtemplate<typename Head, typename... Tail>struct pop_front<Typelist<Head, Tail...>> {  // Возвращение списка, содержащего оставшиеся типы  using type = Typelist<Tail...>;};template<auto Head, auto... Tail>struct pop_front<Valuelist<Head, Tail...>> {  using type = Valuelist<Tail...>;};template<typename List>using pop_front_t = typename pop_front<List>::type; // Примерыusing listT = Typelist<char, bool, int>;using typeT = pop_front_t<listT>; // type = Typelist<bool, int>using listV = Valuelist<9,8,7>;using typeV = pop_front_t<listV>; // type = Valuelist<8,7>


3. Добавление элемента в начало списка
push_front
template<typename List, typename NewElement>struct push_front;template<typename... List, typename NewElement>struct push_front<Typelist<List...>, NewElement> {  using type = Typelist<NewElement, List...>;};template<typename List, typename NewElement>using push_front_t = typename push_front<List, NewElement>::type;  // Примерusing listT = Typelist<char, bool, int>;using typeT = push_front_t<listT, long >; // type = Typelist<long, char, bool, int>


4. Добавление нетипового параметра в конец списка

push_back_value
template<typename List, auto NewElement>struct push_back;template<auto... List, auto NewElement>struct push_back<Valuelist<List...>, NewElement>{  using type = Valuelist<List..., NewElement>;};template<typename List, auto NewElement>using push_back_t = typename push_back<List, NewElement>::type;  // Примерusing listV = Valuelist<9,8,7>;using typeV = push_back_t<listV, 6>; // typeV = Valuelist<9,8,7,6>


5. Проверка списка на пустоту

is_empty
template<typename List>struct is_empty{    static constexpr auto value = false;}; // Специализация для базового случая, когда список пустtemplate<>struct is_empty<Typelist<>>{    static constexpr auto value = true;};template<typename List>static constexpr auto is_empty_v = is_empty<List>::value; // Примерusing listT = Typelist<char, bool, int>;constexpr auto value = is_empty_v<listT>; // value = false


6. Нахождение количества элементов в списке

size_of_list
  // Функция рекурсивно извлекает по одному элементу из списка,  // инкрементируя счетчик count, пока не дойдет до одного из 2 базовых случаевtemplate<typename List, std::size_t count = 0>struct size_of_list : public size_of_list<pop_front_t<List>, count + 1>{};  // Базовый случай для пустого списка типовtemplate<std::size_t count>struct size_of_list<Typelist<>, count>{  static constexpr std::size_t value = count;};  // Базовый случай для пустого списка нетиповых параметров template<std::size_t count>struct size_of_list<Valuelist<>, count>{  static constexpr std::size_t value = count;};template<typename List>static constexpr std::size_t size_of_list_v = size_of_list<List>::value;  // Примерusing listT = Typelist<char, bool, int>;constexpr auto value = size_of_list_v <listT>; // value = 3


Теперь, когда определены все базовые действия, можно переходить к написанию метафункций для битовых операций: or, and, xor, необходимых для методов интерфейса.

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

Функция, выполняющая абстрактную операцию над списком

lists_operation
template<template<typename first, typename second> class operation,         typename Lists, bool isEnd = size_of_list_v<Lists> == 1>class lists_operation{  using first = front_t<Lists>; // (3)  using second = front_t<pop_front_t<Lists>>; // (4)  using next = pop_front_t<pop_front_t<Lists>>; // (5)  using result = operation<first, second>; // (6)public:  using type = typename       lists_operation<operation, push_front_t<next, result>>::type; // (7)};template<template<typename first, typename second> class operation, typename List>class lists_operation<operation, List, true>{ // (1)public:  using type = front_t<List>; // (2)};

Lists список, состоящий из типов или списков, над которым необходимо провести некоторое действие.
operation функциональный адаптер, который принимает 2 первых элемента Lists и возвращает результирующий тип после операции.
isEnd граничное условие метафункции, которое проверяет количество типов в Lists.

В базовом случае (1) Lists состоит из 1 элемента, поэтому результатом работы функции станет его извлечение(2).

Для остальных случаев определяют первый (3) и второй (4) элементы из Lists, к которым применяется операция (6). Для получения результирующего типа (7) происходит рекурсивный вызов метафункции с новым списком типов, на первом месте которого стоит (6), за которым следуют оставшиеся типы (5) исходного Lists. Окончанием рекурсии становиться вызов специализации (1).

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

valuelists_operation
template<template <auto value1, auto value2> typename operation,          typename List1, typename List2, typename Result = Valuelist<>>struct operation_2_termwise_valuelists{  constexpr static auto newValue =       operation<front_v<List1>, front_v<List2>>::value; // (2)    using nextList1 = pop_front_t<List1>;  using nextList2 = pop_front_t<List2>;      using result = push_back_value_t<Result, newValue>; // (3)  using type = typename       operation_2_termwise_valuelists <operation, nextList1, nextList2, result>::type; // (4)};template<template <auto value1, auto value2> typename operation, typename Result>struct operation_2_termwise_valuelists <operation, Valuelist<>, Valuelist<>, Result>{ // (1)  using type = Result;};

List1 и List2 списки нетиповых параметров, над которыми необходимо произвести действие.
operation операция, производимая над нетиповыми параметрами.
Result тип, используемый для накопления промежуточных результатов.

Базовый случай (1), когда оба списка пусты, возвращает Result.

Для остальных случаев происходит вычисление значения операции (2) и занесение его в результирующий список Result (3). Далее рекурсивно вызывается метафункция (4) до того момента, пока оба списка не станут пустыми.

Функции битовых операций:

bitwise_operation
template<auto value1, auto value2>struct and_operation{ static constexpr auto value = value1 & value2;};template<auto value1, auto value2>struct or_operation{ static constexpr auto value = value1 | value2;};template<auto value1, auto value2>struct xor_operation{ static constexpr auto value = value1 ^ value2;};


Осталось создать псевдонимы для более простого использования:
псевдонимы
  // Псевдонимы для битовых почленных операций над 2 спискамиtemplate<typename List1, typename List2>using operation_and_termwise_t = typename           operation_2_termwise_valuelists<and_operation, List1, List2>::type;template<typename List1, typename List2>using operation_or_termwise_t = typename           operation_2_termwise_valuelists<or_operation, List1, List2>::type;template<typename List1, typename List2>using operation_xor_termwise_t = typename           operation_2_termwise_valuelists<xor_operation, List1, List2>::type;  // Псевдонимы почленных битовых операций для произвольного количества списковtemplate<typename... Lists>using lists_termwise_and_t = typename           lists_operation<operation_and_termwise_t, Typelist<Lists...>>::type;template<typename... Lists>using lists_termwise_or_t= typename           lists_operation<operation_or_termwise_t, Typelist<Lists...>>::type;template<typename... Lists>using lists_termwise_xor_t = typename           lists_operation<operation_xor_termwise_t, Typelist<Lists...>>::type;

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

Возвращаясь к имплементации интерфейса


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

template<typename adapter>  struct IPower{  template<typename... Peripherals>  static void Enable(){           // Раскрытие пакета параметров периферии, содержащей свойство power      // и применение побитового или к значениям    using tEnableList = lists_termwise_or_t<typename Peripherals::power...>;      // Псевдоним Valuelist<>, содержащий только 0,       // количество которых равно количеству регистров    using tDisableList = typename adapter::template fromValues<>::power;         // Передача списков включения/отключения адаптеру   adapter:: template _Set<tEnableList , tDisableList>();  }  template<typename EnableList, typename ExceptList>  static void EnableExcept(){    using tXORedList = lists_termwise_xor_t <        typename EnableList::power, typename ExceptList::power>;    using tEnableList = lists_termwise_and_t <        typename EnableList::power, tXORedList>;    using tDisableList = typename adapter::template fromValues<>::power;    adapter:: template _Set<tEnableList , tDisableList>();  }  template<typename EnableList, typename DisableList>    static void Keep(){    using tXORedList = lists_termwise_xor_t <        typename EnableList::power, typename DisableList::power>;    using tEnableList = lists_termwise_and_t <        typename EnableList::power, tXORedList>;    using tDisableList = lists_termwise_and_t <        typename DisableList::power, tXORedList>;    adapter:: template _Set<tEnableList , tDisableList>();  }  template<typename... PeripheralsList>  struct fromPeripherals{    using power = lists_termwise_or_t<typename PeripheralsList::power...>;  };};

Также, интерфейс содержит встроенный класс fromPeripherals, позволяющий объединять периферию в один список, который, затем, можно использовать в методах:

  using listPower = Power::fromPeripherals<spi, uart>;  Power::Enable<listPower>();

Методы Disable реализуются аналогично.

Адаптер контроллера


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

struct Power: public IPower<Power>{  static constexpr uint32_t     _addressAHBENR  = 0x40021014,    _addressAPB2ENR = 0x40021018,    _addressAPB1ENR = 0x4002101C;    using AddressesList = Valuelist<      _addressAHBENR, _addressAPB1ENR, _addressAPB2ENR>;  template<typename EnableList, typename DisableList>  static void _Set(){    // Вызов метода класса, осуществляющий запись в регистры    HPower:: template ModifyRegisters<EnableList, DisableList, AddressesList>();  }      template<uint32_t valueAHBENR = 0, uint32_t valueAPB1ENR = 0, uint32_t valueAPB2ENR = 0>  struct fromValues{    using power = Valuelist<valueAHBENR, valueAPB1ENR, valueAPB2ENR>;  };};

Периферия


Наделяем периферию свойством power, используя структуру fromValues адаптера:

template<int identifier>struct SPI{  // С помощью identifier можно выбирать необходимые биты на этапе компиляции  using power = Power::fromValues<      RCC_AHBENR_DMA1EN, // Значения для соответствующих регистров,      RCC_APB1ENR_SPI2EN, // последовательность которых определена в адаптере      RCC_APB2ENR_IOPBEN>::power;};template<int identifier>struct UART{  using power = Power::fromValues<      RCC_AHBENR_DMA1EN,      0U,       RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN>::power;};

Запись в регистры


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

В качестве параметров, метод принимает 3 списка нетиповых параметров Valuelist<>:

  • SetList и ResetList списки из последовательностей значений битов, которые необходимо установить/сбросить в регистре
  • AddressesList список адресов регистров, в которые будет производится запись значений из предыдущих параметров

struct HPower{  template<typename SetList, typename ResetList, typename AddressesList>    static void ModifyRegisters(){    if constexpr (!is_empty_v<SetList> && !is_empty_v<ResetList> &&   !is_empty_v<AddressesList>){        // Получаем первые значения списков      constexpr auto valueSet = front_v<SetList>;      constexpr auto valueReset = front_v<ResetList>;      if constexpr(valueSet || valueReset){        constexpr auto address = front_v<AddressesList>;        using pRegister_t = volatile std::remove_const_t<decltype(address)>* const;        auto& reg = *reinterpret_cast<pRegister_t>(address);        // (!)Единственная строчка кода, которая может попасть в ассемблерный листинг        reg = (reg &(~valueReset)) | valueSet;      }        // Убираем первые значения из всех списков                  using tRestSet = pop_front_t<SetList>;      using tRestReset = pop_front_t<ResetList>;      using tRestAddress = pop_front_t<AddressesList>;              // Вызывается до тех пор, пока списки не станут пустыми      ModifyRegisters<tRestSet, tRestReset, tRestAddress>();    }  };};

В классе присутствует единственная строчка кода, которая попадет в ассемблерный листинг.

Теперь, когда все блоки структуры готовы, перейдём к тестированию.

Тестируем код


Вспомним условия последней задачи:

  • Включение SPI2 и USART1
  • Выключение SPI2 перед входом в режим энергосбережения
  • Включение SPI2 после выхода из режима энергосбережения

// Необязательные псевдонимы для периферииusing spi = SPI<2>;using uart = UART<1>;// Задаем списки управления тактированием (для удобства)using listPowerInit = Power::fromPeripherals<spi, uart>;using listPowerDown = Power::fromPeripherals<spi>;using listPowerWake = Power::fromPeripherals<uart>;int main() {   // Включение SPI2, UASRT1, DMA1, GPIOA, GPIOB    Power::Enable<listPowerInit>();    // Some code        // Выключение только SPI2 и GPIOB    Power::DisableExcept<listPowerDown, listPowerWake>();    //Sleep();    // Включение только SPI2 и GPIOB    Power::EnableExcept<listPowerDown, listPowerWake>();}

Размер кода: 68 байт*, как и в случае с прямой записью в регистры.

Листинг
main:  // AHBENR(Включение DMA1)  ldr     r3, .L3  ldr     r2, [r3, #20]  orr     r2, r2, #1  str     r2, [r3, #20]  // APB1ENR(Включение SPI2  ldr     r2, [r3, #28]  orr     r2, r2, #16384  str     r2, [r3, #28]  // APB2ENR(Включение GPIOA, GPIOB, USART1)  ldr     r2, [r3, #24]  orr     r2, r2, #16384  orr     r2, r2, #12  str     r2, [r3, #24]  // APB1ENR(Выключение SPI2)  ldr     r2, [r3, #28]  bic     r2, r2, #16384  str     r2, [r3, #28]  // APB2ENR(Выключение GPIOB)  ldr     r2, [r3, #24]  bic     r2, r2, #8  str     r2, [r3, #24]  // APB1ENR(Включение SPI2  ldr     r2, [r3, #28]  orr     r2, r2, #16384  str     r2, [r3, #28]  // APB2ENR(Выключение GPIOB)  ldr     r2, [r3, #24]  orr     r2, r2, #8  str     r2, [r3, #24]


*При использовании GCC 9.2.1 получается на 8 байт больше, чем в версии GCC 10.1.1. Как видно из листинга добавляются несколько ненужных инструкций, например, перед чтением по адресу (ldr) есть инструкция добавления (adds), хотя эти инструкции можно заменить на чтение со смещением. Новая версия оптимизирует эти операции. При этом clang генерирует одинаковые листинги.

Итоги


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

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

Полный код
type_traits_custom.hpp
#ifndef _TYPE_TRAITS_CUSTOM_HPP#define _TYPE_TRAITS_CUSTOM_HPP#include <type_traits>/*!  @file  @brief Traits for metaprogramming*//*!  @brief Namespace for utils.*/namespace utils{/*-----------------------------------Basic----------------------------------------*//*!  @brief Basic list of types  @tparam Types parameter pack*/template<typename... Types>struct Typelist{};/*!  @brief Basic list of values  @tparam Values parameter pack*/template<auto... Values>struct Valuelist{};/*------------------------------End of Basic--------------------------------------*//*----------------------------------Front-------------------------------------------  Description:  Pop front type or value from list  using listOfTypes = Typelist<int, short, bool, unsigned>;  using listOfValues = Valuelist<1,2,3,4,5,6,1>;  |-----------------|--------------------|----------|  |      Trait      |    Parameters      |  Result  |  |-----------------|--------------------|----------|  |     front_t     |   <listOfTypes>    |    int   |  |-----------------|--------------------|----------|  |     front_v     |   <listOfValues>   |     1    |  |-----------------|--------------------|----------| */namespace{template<typename List>struct front;template<typename Head, typename... Tail>struct front<Typelist<Head, Tail...>>{   using type = Head; };template<auto Head, auto... Tail>struct front<Valuelist<Head, Tail...>> {  static constexpr auto value = Head;};}template<typename List>using front_t = typename front<List>::type;template<typename List>static constexpr auto front_v = front<List>::value;/*----------------------------------End of Front----------------------------------*//*----------------------------------Pop_Front---------------------------------------  Description:  Pop front type or value from list and return rest of the list  using listOfTypes = Typelist<int, short, bool>;  using listOfValues = Valuelist<1,2,3,4,5,6,1>;  |-----------------|--------------------|------------------------|  |      Trait      |    Parameters      |         Result         |  |-----------------|--------------------|------------------------|  |   pop_front_t   |    <listOfTypes>   | Typelist<short, bool>  |  |-----------------|--------------------|------------------------|  |   pop_front_t   |   <listOfValues>   | Valuelist<2,3,4,5,6,1> |  |-----------------|--------------------|------------------------| */namespace{template<typename List>struct pop_front;template<typename Head, typename... Tail>struct pop_front<Typelist<Head, Tail...>> {  using type = Typelist<Tail...>;};template<auto Head, auto... Tail>struct pop_front<Valuelist<Head, Tail...>> {  using type = Valuelist<Tail...>;};}template<typename List>using pop_front_t = typename pop_front<List>::type;/*------------------------------End of Pop_Front----------------------------------*//*----------------------------------Push_Front--------------------------------------  Description:  Push new element to front of the list  using listOfTypes = Typelist<short, bool>;  |-----------------------|--------------------------|-------------------------------|  |      Trait            |        Parameters        |             Result            |  |-----------------------|--------------------------|-------------------------------|  |      push_front_t     |   <listOfTypes, float>   | Typelist<float, short, bool>  |  |-----------------------|--------------------------|-------------------------------| */namespace{template<typename List, typename NewElement>struct push_front;template<typename... List, typename NewElement>struct push_front<Typelist<List...>, NewElement> {  using type = Typelist<NewElement, List...>;};}template<typename List, typename NewElement>using push_front_t = typename push_front<List, NewElement>::type;/*------------------------------End of Push_Front---------------------------------*//*----------------------------------Push_Back---------------------------------------  Description:  Push new value to back of the list  using listOfValues = Valuelist<1,2,3,4,5,6>;  |-----------------------|--------------------------|-------------------------------|  |      Trait            |        Parameters        |             Result            |  |-----------------------|--------------------------|-------------------------------|  |   push_back_value_t   |     <listOfValues, 0>    |    Valuelist<1,2,3,4,5,6,0>   |  |-----------------------|--------------------------|-------------------------------| */namespace{template<typename List, auto NewElement>struct push_back_value;template<auto... List, auto NewElement>struct push_back_value<Valuelist<List...>, NewElement>{  using type = Valuelist<List..., NewElement>;};}template<typename List, auto NewElement>using push_back_value_t = typename push_back_value<List, NewElement>::type;/*----------------------------------End of Push_Back------------------------------*//*-----------------------------------Is_Empty---------------------------------------  Description:  Check parameters list for empty and return bool value  using listOfTypes = Typelist<int, short, bool, unsigned>;  using listOfValues = Valuelist<>;  |-------------------------|--------------------|----------|  |          Trait          |     Parameters     |  Result  |  |-------------------------|--------------------|----------|  |        is_empty_v       |    <listOfTypes>   |  false   |  |-------------------------|--------------------|----------|  |        is_empty_v       |   <listOfValues>   |   true   |  |-------------------------|--------------------|----------| */namespace{/*!  @brief Check the emptiness of the types in parameters.   \n     E.g.: is_empty<int, short, bool>::value;*/ template<typename List>struct is_empty{    static constexpr auto value = false;};/*!  @brief Check the emptiness of the types in parameter. Specializatio for empty parameters   \n     E.g.: is_empty<>::value;*/ template<>struct is_empty<Typelist<>>{    static constexpr auto value = true;};template<>struct is_empty<Valuelist<>>{    static constexpr auto value = true;};}/*!  @brief Check the emptiness of the types-list in parameter.   \n     E.g.: using list = Typelist<int, short, bool>; is_empty_v<list>;*/ template<typename List>static constexpr auto is_empty_v = is_empty<List>::value;/*--------------------------------End of Is_Empty---------------------------------*//*---------------------------------Size_Of_List-------------------------------------  Description:  Return number of elements in list  using listOfTypes = Typelist<int, float, double, bool>;  |------------------|--------------------|----------|  |       Trait      |     Parameters     |  Result  |  |------------------|--------------------|----------|  |  size_of_list_v  |     listOfTypes    |    4     |  |------------------|--------------------|----------| */namespace{template<typename List, std::size_t count = 0U>struct size_of_list : public size_of_list<pop_front_t<List>, count + 1>{};template<std::size_t count>struct size_of_list<Typelist<>, count>{  static constexpr std::size_t value = count;};template<std::size_t count>struct size_of_list<Valuelist<>, count>{  static constexpr std::size_t value = count;};}template<typename List>static constexpr std::size_t size_of_list_v = size_of_list<List>::value;/*-------------------------------End Size_Of_List---------------------------------*//*---------------------------------Lists Operation--------------------------------*/  /*Description: Operations with lists of values  using list1 = Valuelist<1, 4, 8, 16>;  using list2 = Valuelist<1, 5, 96, 17>;  |------------------------------|-------------------|---------------------------|  |               Trait          |    Parameters     |           Result          |  |------------------------------|-------------------|---------------------------|  |     lists_termwise_and_t     |  <list1, list2>   |  Valuelist<1, 4, 0, 16>   |  |------------------------------|-------------------|---------------------------|  |     lists_termwise_or_t      |  <list1, list2>   |  Valuelist<1, 5, 104, 17> |  |---------------------------- -|-------------------|---------------------------|  |     lists_termwise_xor_t     |  <list1, list2>   |  Valuelist<0, 1, 104, 1>  |  |------------------------------|-------------------|---------------------------| */namespace{template<template <auto value1, auto value2> typename operation,          typename List1, typename List2, typename Result = Valuelist<>>struct operation_2_termwise_valuelists{  constexpr static auto newValue = operation<front_v<List1>, front_v<List2>>::value;  using nextList1 = pop_front_t<List1>;  using nextList2 = pop_front_t<List2>;      using result = push_back_value_t<Result, newValue>;  using type = typename       operation_2_termwise_valuelists<operation, nextList1, nextList2, result>::type;};template<template <auto value1, auto value2> typename operation, typename Result>struct operation_2_termwise_valuelists<operation, Valuelist<>, Valuelist<>, Result>{  using type = Result;};template<template <auto value1, auto value2> typename operation,          typename List2, typename Result>struct operation_2_termwise_valuelists<operation, Valuelist<>, List2, Result>{  using type = typename       operation_2_termwise_valuelists<operation, Valuelist<0>, List2, Result>::type;};template<template <auto value1, auto value2> typename operation,          typename List1, typename Result>struct operation_2_termwise_valuelists<operation, List1, Valuelist<>, Result>{  using type = typename       operation_2_termwise_valuelists<operation, List1, Valuelist<0>, Result>::type;};template<template<typename first, typename second> class operation,         typename Lists, bool isEnd = size_of_list_v<Lists> == 1>class lists_operation{  using first = front_t<Lists>;  using second = front_t<pop_front_t<Lists>>;  using next = pop_front_t<pop_front_t<Lists>>;  using result = operation<first, second>;public:  using type = typename lists_operation<operation, push_front_t<next, result>>::type;};template<template<typename first, typename second> class operation,         typename Lists>class lists_operation<operation, Lists, true>{public:  using type = front_t<Lists>;};template<auto value1, auto value2>struct and_operation{ static constexpr auto value = value1 & value2;};template<auto value1, auto value2>struct or_operation{ static constexpr auto value = value1 | value2;};template<auto value1, auto value2>struct xor_operation{ static constexpr auto value = value1 ^ value2;};template<typename List1, typename List2>using operation_and_termwise_t = typename     operation_2_termwise_valuelists<and_operation, List1, List2>::type;template<typename List1, typename List2>using operation_or_termwise_t = typename     operation_2_termwise_valuelists<or_operation, List1, List2>::type;template<typename List1, typename List2>using operation_xor_termwise_t = typename     operation_2_termwise_valuelists<xor_operation, List1, List2>::type;}template<typename... Lists>using lists_termwise_and_t =     typename lists_operation<operation_and_termwise_t, Typelist<Lists...>>::type;template<typename... Lists>using lists_termwise_or_t = typename     lists_operation<operation_or_termwise_t, Typelist<Lists...>>::type;template<typename... Lists>using lists_termwise_xor_t = typename     lists_operation<operation_xor_termwise_t, Typelist<Lists...>>::type;/*--------------------------------End of Lists Operation----------------------------*/} // !namespace utils#endif //!_TYPE_TRAITS_CUSTOM_HPP



IPower.hpp
#ifndef _IPOWER_HPP#define _IPOWER_HPP#include "type_traits_custom.hpp"#define __FORCE_INLINE __attribute__((always_inline)) inline/*!  @brief Controller's peripherals interfaces*/namespace controller::interfaces{/*!  @brief Interface for Power(Clock control). Static class. CRT pattern  @tparam <adapter> class of specific controller*/template<typename adapter>  class IPower{  IPower() = delete;public:  /*!    @brief Enables peripherals Power(Clock)    @tparam <Peripherals> list of peripherals with trait 'power'  */  template<typename... Peripherals>  __FORCE_INLINE static void Enable(){    using tEnableList = utils::lists_termwise_or_t<typename Peripherals::power...>;    using tDisableList = typename adapter::template fromValues<>::power;   adapter:: template _Set<tEnableList, tDisableList>();  }  /*!    @brief Enables Power(Clock) except listed peripherals in 'ExceptList'.       If Enable = Exception = 1, then Enable = 0, otherwise depends on Enable.    @tparam <EnableList> list to enable, with trait 'power'    @tparam <ExceptList> list of exception, with trait 'power'  */  template<typename EnableList, typename ExceptList>  __FORCE_INLINE static void EnableExcept(){    using tXORedList = utils::lists_termwise_xor_t<typename EnableList::power, typename ExceptList::power>;    using tEnableList = utils::lists_termwise_and_t<typename EnableList::power, tXORedList>;    using tDisableList = typename adapter::template fromValues<>::power;    adapter:: template _Set<tEnableList, tDisableList>();  }  /*!    @brief Disables peripherals Power(Clock)    @tparam <Peripherals> list of peripherals with trait 'power'  */  template<typename... Peripherals>  __FORCE_INLINE static void Disable(){    using tDisableList = utils::lists_termwise_or_t<typename Peripherals::power...>;    using tEnableList = typename adapter::template fromValues<>::power;    adapter:: template _Set<tEnableList, tDisableList>();  }  /*!    @brief Disables Power(Clock) except listed peripherals in 'ExceptList'.       If Disable = Exception = 1, then Disable = 0, otherwise depends on Disable.    @tparam <DisableList> list to disable, with trait 'power'    @tparam <ExceptList> list of exception, with trait 'power'  */  template<typename DisableList, typename ExceptList>  __FORCE_INLINE static void DisableExcept(){    using tXORedList = utils::lists_termwise_xor_t<typename DisableList::power, typename ExceptList::power>;    using tDisableList = utils::lists_termwise_and_t<typename DisableList::power, tXORedList>;    using tEnableList = typename adapter::template fromValues<>::power;    adapter:: template _Set<tEnableList, tDisableList>();  }  /*!    @brief Disable and Enables Power(Clock) depends on values.       If Enable = Disable = 1, then Enable = Disable = 0, otherwise depends on values    @tparam <EnableList> list to enable, with trait 'power'    @tparam <DisableList> list to disable, with trait 'power'  */  template<typename EnableList, typename DisableList>  __FORCE_INLINE static void Keep(){    using tXORedList = utils::lists_termwise_xor_t<typename EnableList::power, typename DisableList::power>;    using tEnableList = utils::lists_termwise_and_t<typename EnableList::power, tXORedList>;    using tDisableList = utils::lists_termwise_and_t<typename DisableList::power, tXORedList>;    adapter:: template _Set<tEnableList, tDisableList>();  }  /*!    @brief Creates custom 'power' list from peripherals. Peripheral driver should implement 'power' trait.      E.g.: using power = Power::makeFromValues<1, 512, 8>::power;     @tparam <PeripheralsList> list of peripherals with trait 'power'  */ template<typename... PeripheralsList>  class fromPeripherals{    fromPeripherals() = delete;    using power = utils::lists_termwise_or_t<typename PeripheralsList::power...>;    friend class IPower<adapter>;  };};} // !namespace controller::interfaces#undef   __FORCE_INLINE#endif // !_IPOWER_HPP



HPower.hpp
#ifndef _HPOWER_HPP#define _HPOWER_HPP#include "type_traits_custom.hpp"#define __FORCE_INLINE __attribute__((always_inline)) inline/*!  @brief Hardware operations*/namespace controller::hardware{/*!  @brief Implements hardware operations with Power(Clock) registers*/class HPower{  HPower() = delete;protected:/*!  @brief Set or Reset bits in the registers  @tparam <SetList> list of values to set   @tparam <ResetList> list of values to reset  @tparam <AddressesList> list of registers addresses to operate*/  template<typename SetList, typename ResetList, typename AddressesList>  __FORCE_INLINE static void ModifyRegisters(){    using namespace utils;    if constexpr (!is_empty_v<SetList> && !is_empty_v<ResetList> &&   !is_empty_v<AddressesList>){      constexpr auto valueSet = front_v<SetList>;      constexpr auto valueReset = front_v<ResetList>;      if constexpr(valueSet || valueReset){        constexpr auto address = front_v<AddressesList>;                  using pRegister_t = volatile std::remove_const_t<decltype(address)>* const;        auto& reg = *reinterpret_cast<pRegister_t>(address);        reg = (reg &(~valueReset)) | valueSet;      }              using tRestSet = pop_front_t<SetList>;      using tRestReset = pop_front_t<ResetList>;      using tRestAddress = pop_front_t<AddressesList>;            ModifyRegisters<tRestSet, tRestReset, tRestAddress>();    }  };};} // !namespace controller::hardware#undef __FORCE_INLINE#endif // !_HPOWER_HPP



stm32f1_Power.hpp
#ifndef _STM32F1_POWER_HPP#define _STM32F1_POWER_HPP#include <cstdint>#include "IPower.hpp"#include "HPower.hpp"#include "type_traits_custom.hpp"#define __FORCE_INLINE __attribute__((always_inline)) inline/*!  @brief Controller's peripherals*/namespace controller{/*!  @brief Power managment for controller*/class Power: public interfaces::IPower<Power>, public hardware::HPower{  Power() = delete;public:  /*!    @brief Creates custom 'power' list from values. Peripheral driver should implement 'power' trait.      E.g.: using power = Power::fromValues<1, 512, 8>::power;     @tparam <valueAHB=0> value for AHBENR register    @tparam <valueAPB1=0> value for APB1ENR register    @tparam <valueAPB2=0> value for APB1ENR register  */  template<uint32_t valueAHBENR = 0, uint32_t valueAPB1ENR = 0, uint32_t valueAPB2ENR = 0>  struct fromValues{    fromValues() = delete;    using power = utils::Valuelist<valueAHBENR, valueAPB1ENR, valueAPB2ENR>;  };private:   static constexpr uint32_t     _addressAHBENR  = 0x40021014,    _addressAPB2ENR = 0x40021018,    _addressAPB1ENR = 0x4002101C;    using AddressesList = utils::Valuelist<_addressAHBENR, _addressAPB1ENR, _addressAPB2ENR>;  template<typename EnableList, typename DisableList>  __FORCE_INLINE static void _Set(){    HPower:: template ModifyRegisters<EnableList, DisableList, AddressesList>();  }  friend class IPower<Power>;};} // !namespace controller#undef __FORCE_INLINE#endif // !_STM32F1_POWER_HPP



stm32f1_SPI.hpp
#ifndef _STM32F1_SPI_HPP#define _STM32F1_SPI_HPP#include "stm32f1_Power.hpp"namespace controller{template<auto baseAddress>class SPI{  static const uint32_t RCC_AHBENR_DMA1EN = 1;  static const uint32_t RCC_APB2ENR_IOPBEN = 8;  static const uint32_t RCC_APB1ENR_SPI2EN = 0x4000;  /*!    @brief Trait for using in Power class. Consists of Valueslist with      values for AHBENR, APB1ENR, APB2ENR registers   */  using power = Power::fromValues<           RCC_AHBENR_DMA1EN,           RCC_APB1ENR_SPI2EN,            RCC_APB2ENR_IOPBEN>::power;  template<typename>  friend class interfaces::IPower;};}#endif // !_STM32F1_SPI_HPP



stm32f1_UART.hpp
#ifndef _STM32F1_UART_HPP#define _STM32F1_UART_HPP#include "stm32f1_Power.hpp"namespace controller{template<auto baseAddress>class UART{  static const uint32_t RCC_AHBENR_DMA1EN = 1;  static const uint32_t RCC_APB2ENR_IOPAEN = 4;  static const uint32_t RCC_APB2ENR_USART1EN = 0x4000;  /*!    @brief Trait for using in Power class. Consists of Valueslist with      values for AHBENR, APB1ENR, APB2ENR registers   */  using power = Power::fromValues<           RCC_AHBENR_DMA1EN,           0U,            RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN>::power;  template<typename>  friend class interfaces::IPower;};}#endif // !_STM32F1_UART_HPP



main.cpp
#include "stm32f1_Power.hpp"#include "stm32f1_UART.hpp"#include "stm32f1_SPI.hpp"using namespace controller;using spi = SPI<2>;using uart = UART<1>;using listPowerInit = Power::fromPeripherals<spi, uart>;using listPowerDown = Power::fromPeripherals<spi>;using listPowerWake = Power::fromPeripherals<uart>;int main(){  Power::Enable<listPowerInit>();  //Some code  Power::DisableExcept<listPowerDown, listPowerWake>();  //Sleep();  Power::EnableExcept<listPowerDown, listPowerWake>();  while(1);  return 1;};



Github
Подробнее..

Роль логического программирования, и стоит ли планировать его изучение на 2021-й

22.12.2020 00:22:49 | Автор: admin

Начну, пожалуй, с представления читателя этой статьи, так как ничто не приковывает внимание к тексту более, чем сопереживание главному герою, тем более, в его роли сейчас выступаете Вы. Вероятно, услышав или прочитав однажды словосочетание "логическое программирование" и преисполнившись интересом, Вы как настоящий или будущий программист направились в Google. Первая ссылка, разумеется, ведёт на Википедию - читаем определение:

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

"Мда" - думаете Вы, и этим все сказано. Сложно! И тут наш отважный герой должен бы был перейти по второй ссылке, но я позволю себе сделать небольшую вставку, описав главное действующее лицо: Вы, по моей задумке, новичок в программировании, а даже если и нет, то точно не знакомы с логическим его обличием. Если же читатель уже несколько (или даже много) искушен знаниями в этой области, то рекомендую прочитать статью Что такое логическое программирование и зачем оно нам нужно, раз уж в вас горит интерес и любопытство к теме, а изучение материала ниже оставьте менее опытным коллегам.

Итак, пришло время второй ссылки. Что это будет? Статья на Хабре? Может быть статья на ином ресурсе? Прочитав пару первых абзацев на разных сайтах, вы, скорее всего, мало что поймете, так как, во-первых, материал обычно ориентирован на знающего читателя, во-вторых, хорошей и понятной информации по теме не так много в русскоязычном интернете, в-третьих, там почему-то постоянно речь идёт о некоем "прологе" (речь о языке программирования Prolog, разумеется), но сам язык, кажется, использует мало кто (почётное 35 место в рейтинге TIOBE). Однако наш герой не теряет мотивации и, спустя некоторое время, натыкается на эту самую статью, желая, все-таки понять:

  • Что такое логическое программирование

  • Какова история его создания и фундаментальные основы (серьезно, какому новичку это может быть интересно?)

  • Зачем и где его применяют

  • Стоит ли лично вам его изучать

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

Что такое логическое программирование

В школе на уроках информатики многие, если не все, слышали про Pascal (а кто-то даже писал на нем). Многие также могли слышать про Python, C/C++/C#, Java. Обычно программирование начинают изучать именно с языков из этого набора, поэтому все привыкли, что программа выглядит как-то так:

НачатьКоманда1Команда2Если УСЛОВИЕ  Команда3Иначе  Команда4Закончить

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

Давайте устроимся поудобнее рядом со своим компьютером и порассуждаем о жизни и смерти вместе с Аристотелем:

Всякий человек смертен.

Сократ - человек.

Следовательно, Сократ смертен.

Звучит логично. Но есть ли способ научить компьютер делать выводы как Аристотель? Конечно! И вот тут мы вспомним о Prolog-e, который так часто мелькает при поиске информации о логическом программировании. Как несложно догадаться, Prolog (Пролог) является самым популярным чисто логическим языком программирования. Давайте рассуждения об этом языке оставим на следующие разделы статьи, а пока что продемонстрируем "фишки" логических языков, используя Пролог.

Напишем небольшую программу, где перечислим, кто является людьми (ограничимся тремя) и добавим правило "всякий человек смертен":

% Всё, что после знака процента в строке - комментарииhuman('Plato'). % Платон - человекhuman('Socrates'). % Сократ - тоже человекhuman('Aristotle'). % Конечно, человеком был и Аристотель% ...и др. философыmortal(X) :- human(X). % Читаем так: "X смертен, если X - человек"

Что ж, давайте спросим у компьютера, смертен ли Сократ:

?- mortal('Socrates').true.

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

Так, теперь стоит успокоиться и разобраться, что же произошло. Вначале мы записали т. н. факты, то есть знания нашей программы о мире. В нашем случае ей известно лишь то, что Платон, Сократ и Аристотель - люди. Но что за странная запись "human('Socrates')." и почему это выглядит как функция? На самом деле "human" и "mortal" - предикаты от одной переменной. Да, тут уже пошли термины, но постараюсь объяснять их просто и понятно для тех, кто привык к императивному нормальному программированию.

Логическое программирование основано на логике предикатов. Предикатом называется (и здесь я позволю себе вольность) функция от нуля или более переменных, возвращающая значение логического типа (истина (true) или ложь (false)). Факт - это предикат с определенным набором параметров и заведомо известным значением.

% слова с большой буквы Prolog считает переменными, поэтому их следует заключать в кавычкиlike('Petya', 'Milk'). % программа знает, что Петя любит молокоgood('Kesha'). % Кеша хорошийnumber_of_sides('Triangle', 3). % у треугольника три вершиныlike('Misha', X). % не является фактом, так как значение переменной X не определено

Помимо фактов в логической программе присутствуют правила вывода. В данном случае это "mortal(X) :- human(X).". Набор правил вывода - это знания нашей программы о том, как выводить (искать/подбирать) решение. Правила записываются следующим образом:

a(X,Y,Z) :- b(X), c(Y,Z), d().

Предикат a от трех аргументов вернет истину, если удастся доказать истинность предикатов b, c и d. Читаются правила справа налево следующим образом: "Если b от X истинно И c от X, Y истинно И d истинно, то a от X, Y, Z истинно".

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

% Опишем набор фактов о том, кто что обычно ест на завтрак в семье Петиeat(father, cheese).eat(father, apple).eat(father, melon).eat(mother, meat).eat(sister, meat).eat('Petya', cheese).eat(brother, orange).

Теперь начнём делать запросы к программе (всё те же предикаты):

?- eat(father, apple). % ест ли отец яблокиtrue.?- eat(father, meat).  % ест ли отец мясоfalse.?- eat(sister, X). % что ест сестраX = meat.?- eat(X, cheese). % кто ест сырX = father ;X = 'Petya'.?- eat(X, Y). % кто что естX = father,Y = cheese ;X = father,Y = apple ;X = father,Y = melon ;X = mother,Y = meat ;X = sister,Y = meat ;X = 'Petya',Y = cheese ;X = brother,Y = orange.

Как видите, очень удобно. Стало быть, первым и очевидным применением логического программирования (об эффективности поговорим ниже) является работа с базами данных. Мы можем достаточно естественным образом описывать запросы, комбинируя предикаты, причем научить писать такие запросы можно даже человека, совершенно не знакомого с логическим программированием.

Какие задачи и как можно решать с помощью логического программирования

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

d(X,X,1) :- !. % производная X по X = 1d(T,X,0) :- atomic(T). % производная константы = 0d(U+V,X,DU+DV) :- d(U,X,DU), d(V,X,DV). % производная суммы = сумме производныхd(U-V,X,DU-DV) :- d(U,X,DU), d(V,X,DV). d(-T,X,-R) :- d(T,X,R).d(C*U,X,C*W) :- atomic(C), C\=X, !, d(U,X,W). % производная константы, умноженной на выражение = константе на производную от выраженияd(U*V,X,Vd*U+Ud*V) :- d(U,X,Ud), d(V,X,Vd). % производная произведенияd(U/V,X,(Ud*V-Vd*U)/(V*V)) :- d(U,X,Ud), d(V,X,Vd). 

Запустим:

?- d((x-1)/(x+1),x,R).   R =  ((1-0)*(x+1)-(1+0)*(x-1))/((x+1)*(x+1)).

Пусть производная получилась довольно громоздкой, но мы и не ставили цель её упростить. Главное, из примера видно, что правила вывода производной на Prolog-е описываются очень близким образом к их математическому представлению. Чтобы сделать подобное на привычных языках программирования, пришлось бы вводить понятие дерева выражений, описывать каждое правило в виде функции и т. д. Тут же мы обошлись 8-ю строками. Но здесь важно остановиться и задуматься: компьютер не начал работать как-то иначе, он все ещё обрабатывает последовательности команд. Стало быть, те самые деревья, которые где-то все-таки должны быть зашиты, чтобы программа работала, действительно присутствуют, но в неявном виде. Деревья эти именуют "деревьями вывода", именно они позволяют подбирать нужные значения переменных, перебирая все возможные варианты их значений (существует механизм отсечения, который является надстройкой над логической основой языка, но не будем об этом).

Давайте на простом примере рассмотрим, что из себя представляет перебор, а затем то, чем он может быть опасен.

speciality(X,tech_translator) :- studied_languages(X), studied_technical(X). % X - технический переводчик, если изучал языки и технические предметыspeciality(X,programmer) :- studied(X,mathematics), studied(X, compscience). % X - программист, если изучал математику и компьютерные наукиspeciality(X,lit_translator) :- studied_languages(X), studied(X,literature). % X - литературный переводчик, если изучал языкиstudied_technical(X) :- studied(X,mathematics). % X изучал технические предметы, если изучал математикуstudied_technical(X) :- studied(X,compscience). % ...или компьютерные наукиstudied_languages(X) :- studied(X,english). % X изучал языки, если изучал английскийstudied_languages(X) :- studied(X,german). % ...или немецкийstudied(petya,mathematics). % Петя изучал математикуstudied(petya,compscience). % ...компьютерные наукиstudied(petya,english). % ...и английскиstudied(vasya,german). % Вася изучал немецкийstudied(vasya,literature). %...и литературу

Спросим, кто из ребят, известных компьютеру - технический переводчик:

?- speciality(X,tech_translator).X = petya ;X = petya ;false.

Агато есть Петя, Петя и ложь Что-то не так, подумает программист и попробует разобраться. На самом деле, перебирая все варианты значений X, Пролог пройдёт по такому дереву:

Дерево будет обходиться в глубину, то есть сначала рассматривается всё левое поддерево для каждой вершины, затем правое. Таким образом, Пролог дважды докажет, что Петя - технический переводчик, но больше решений не найдёт и вернёт false. Стало быть, половина дерева нам, в общем-то, была не нужна. В данном случае, перебор не выглядит особенно страшным, всего-то обработали лишнюю запись в базе. Чтобы показать "опасность" перебора, рассмотрим другой пример:

Представим, что перед нами в ячейках расположены три чёрных и три белых шара (как на картинке выше), которые требуется поменять местами. За один ход шар может или передвинуться в соседнюю пустую клетку, или в пустую клетку за соседним шаром ("перепрыгнуть" его). Решать будем поиском в ширину в пространстве состояний (состоянием будем считать расположение шаров в ячейках). Суть этого метода заключается в том, что мы ищем все пути длины 1, затем все их продления, затем продления продлений и т. д., пока не найдем целевую вершину (состояние). Почему поиск в ширину? Он первым делом выведет самый оптимальный путь, то есть самый короткий. Как может выглядеть код решения:

% Обозначения: w - белый шар, b - чёрный, e - пустая ячейкаis_ball(w). % w - шарis_ball(b). % b - шарnear([X,e|T],[e,X|T]) :- is_ball(X). % если фишка рядом с пустой ячейкой, то можно переместитьсяnear([e,X|T],[X,e|T]) :- is_ball(X).jump([X,Y,e|T],[e,Y,X|T]) :- is_ball(X), is_ball(Y). % если за соседним шаром есть пустая ячейка, то можно переместитьсяjump([e,Y,X|T],[X,Y,e|T]) :- is_ball(X), is_ball(Y).% предикат перемещения. Мы или рассматриваем первые элементы списка, или убираем первый элемент и повторяем операциюmove(L1,L2) :- near(L1,L2). move(L1,L2) :- jump(L1,L2).move([X|T1],[X|T2]) :- move(T1,T2).% предикат продления текущего пути. Если из состояния X можно перейти в состояние Y и% Y не содержится в текущем пути, то Y - удачное продлениеprolong([X|T],[Y,X|T]) :- move(X,Y), not(member(Y,[X|T])).% Первый аргумент - очередь путей, второй - целевое состояние, третий - результат, то есть найденный путьbdth([[X|T]|_],X,R) :- reverse([X|T], R). % Поиск в ширину нашел решение, если первый элемент пути совпадает с целью (путь наращивается с начала, так что перевернем результат)bdth([P|QI],Y,R) :- bagof(Z,prolong(P,Z),T), append(QI,T,QO), !, bdth(QO,Y,R). % Ищем все возможные продления первого пути и кладём в очередь, рекурсивно запускаем поискbdth([_|T],Y,R) :- bdth(T,Y,R). % Если продлений на предыдущем шаге не нашлось, то есть bagof вернул false, убираем первый путь из очередиbsearch(X,Y,R) :- bdth([[X]],Y,R). % Удобная обёртка над предикатом bdth% Предикат, который решает нашу задачу и выводит результат и длину найденного пути на экранsolve :- bsearch([w,w,w,e,b,b,b],[b,b,b,e,w,w,w],P), write(P), nl, length(P, Len), write(Len), nl.

Если вы попытаетесь вызвать предикат solve, то, в лучшем случае увидите ошибку, в худшем - среда зависнет. Дерево здесь (с учётом лежащих в памяти путей) настолько велико, что переполнит стек, так и не подарив нам ответа. Ну и что - скажете вы, это же может происходить и в императивных (процедурных (обычных)) языках программирования. Конечно. Но, повторюсь, на решение той же задачи на Питоне или Си (без использования библиотек) ушло бы на порядки больше времени. Давайте для полноты картины я приведу решение данной проблемы, а после перейдем к тому, какие же задачи решаются подобным образом.

Со стороны улучшения алгоритма можно предложить использовать поиск в глубину. Но как же, он ведь не даст оптимального результата? Сделаем просто: ограничим глубину поиска. Так мы точно не забьём стек и, возможно, получим ответ. Поступим так: проверим, есть ли пути длины 1, затем длины 2, затем длины 4 и т. д. Получим так называемый поиск с итерационным заглублением:

% Первый аргумент - текущий путь, второй - целевое состояние, третий - результат, то есть найденный путьdpth_id([X|T],X,R,0) :- reverse([X|T], R). % Успешное окончание поискаdpth_id(P,Y,R,N) :- N > 0, prolong(P,P1), N1 is N - 1, dpth_id(P1,Y,R,N1). % Если счётчик >0, то уменьшаем его и продолжаем поиск рекурсивноgenerator(1). % Изначально предикат вернет 1generator(N) :- generator(M), N is M + 1. % Рекурсивно получаем 2, 3, 4 и т. д.isearch(X,Y,R) :- generator(D), dpth_id([X],Y,R,D). % Удобная обертка, которая будет вызывать поиск от каждого натурального значения глубины.

Во-первых, здесь стоит обратить внимание, что мы не используем очереди, а также внешних предикатов (кроме reverse, но он для красоты). Это потому, что поиск в глубину естественен для Пролога (ищите картинку с деревом выше). Во-вторых, пусть мы и делаем вроде как "лишние" действия, то есть для каждого нового значения глубины проходим по всем путям заново, мы практически не теряем в скорости относительно поиска в ширину (может в несколько раз, но не на порядок), при этом значительно экономим память. В-третьих, мы наконец-то получаем ответ, и это самое главное. Приводить его не буду, так как он займет много места, но для интриги оставлю вам его длину: 16.

С другой стороны, задачу можно было бы решить, не меняя код поиска, а лишь изменив правила перемещения шаров. Обратим внимание, что нам заранее известны входные и выходные данные. Приглядевшись становится понятно, что нет никакого смысла в движении фишек "назад". Действительно, если чёрным нужно встать в правые позиции, то какой смысл делать ходы влево? Перепишем предикаты движения:

near([w,e|T],[e,w|T]).near([e,b|T],[b,e|T]).jump([w,X,e|T],[e,X,w|T]) :- is_ball(X).jump([e,X,b|T],[b,X,e|T]) :- is_ball(X).

Хм, код стал даже проще. Запустив мы убедимся, что поиск (оба варианта), во-первых, работает, во-вторых, работает быстро, в-третьих, работает быстро и выводит результат. Это успех. Мало того, что мы решили задачку, только что был создан самый настоящий искусственный интеллект. Программа получает входные данные и желаемый результат, а затем сама ищет, как его достигнуть. Да, это однозначно успех.

Зачем и где применяют логическое программирование

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

  • Анализ естественного языка: Пример с производной - классический пример разбора выражений. Но если мы заменим арифметические операторы, переменные и числа на слова, добавим правила, скажем, английского языка, то сможем получить программу, разбирающую текст на структурные элементы. Занимательно, что одновременно мы получим и программу, способную генерировать текст. Но если логическое программирование можно удобно и эффективно использовать для анализа и разбора текста, то в задачах генерации качественного текста скорее придется обращаться к нейросетям. Тут важно отметить, что рассуждая об анализе и генерации предложений нельзя не упомянуть сложность решения подобных задач. Человек при составлении и восприятии текста ориентируется не только на набор слов и их значений, но и на свою картину мира. К примеру, если в базе лежит факт "Миша починил Маше компьютер", то на вопрос "Разбирается ли Миша в компьютерах?" программа не сможет ответить, не смотря даже на то, что решение вроде как "на поверхности". Именно из-за низкой скорости и особенностей использования на чисто логических языках не занимаются тем, что мы ждем увидеть, загуглив "нейросети" (поиск котиков на картинке, например, не для Пролога). Но вот задачи синтаксического разбора, текстовой аналитики и т. п. на логических языках решать очень даже комфортно.

  • Поиск решений: Задача с Петей и Васей, а также задача с шарами - примеры поиска решений. Представим, что у нас есть набор знаний о некоторой системе и нам нужно понять, можно ли тем или иным путем её взломать (обойти защиту). Это можно достаточно лаконичным образом описать на логических языках и запустить процесс поиска решений. Помимо переборных задач, хорошо будут решаться те, где требуются логические рассуждения (анализ кода или, опять же, естественного текста).

  • Мета-программирование: С помощью того же Пролога можно описать свой собственный язык, соответственно, со своими правилами. Это достаточно важный момент для многих проектов, где сталкиваются специалисты разных сфер. Например, стоит задача анализа неких химических данных. В команде работают химик и программист. Получается, программист должен постоянно советоваться с химиком о том, что и как ему делать, ведь химическим формулам то в IT не учат. А разговаривать - это, в общем-то, не особенно приятно, особенно если кто-то умнее тебя. И тут программисту было бы очень удобно написать простой язык, на котором химик сможет сам записать свои формулы, не трогая разработчика. Вот тут и пригодится логическое программирование.

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

Стоит ли планировать его изучение на 2021-й

Тут оставлю своё субъективное мнение, разделённое на две части:

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

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

И здесь остаётся лишь пожелать продуктивного 2021-го года!

Подробнее..

Метапрограммирование в реальной задаче

28.01.2021 00:21:03 | Автор: admin

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

Когда кто то говорит про метапрограммирование у олдскульного кодировщика случается приступ ярости.

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

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

Справка из википеди

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

На этом заканчивается вступление. Теперь хочу перейти к практической части и рассказать про суть проблемы

Дальше речь пойдёт о языке ruby и фраймворке Rails в частности. В руби есть рефлексия и большие возможности для метапрограммирования. Большинство гемов, модулей и фреймворков созданы при помощи мощных инструментов рефлексии.

Photo by Joshua Fuller on Unsplash

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

Так вот там есть неприятная особенность проблема с has_one association.

Они не обрабатываются автоматически и у вас не будет возможности редактировать вашу has_one связь. Выглядит это примерно как на рисунке 1. Вместо селекта, просто ссылка на чертеж.

рисунок 1рисунок 1

Посмотрим на внутренности вот так выглядит модель нашего техпроцесса. У неё есть has_one связь с чертежом (draft) и хотелось бы иметь возможность редактировать её через CMS.

class TechProcess < ApplicationRecord  include MdcSchema  has_many :executor_programs, inverse_of: :tech_process, foreign_key: :barcode_tech_process, primary_key: :barcode  validates :barcode, presence: true  validates :barcode, uniqueness: true  has_one :draft2tech_process, dependent: :destroy  has_one :draft, through: :draft2tech_process  has_many :tech_process2tech_operations  has_many :tech_operations, through: :tech_process2tech_operationsend

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

Добавляем их в модель

def draft_id  self.draft.try :idenddef draft_id=(id)  self.draft = Draft.find_by_id(id)end

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

Ничего сложного, но что если у нас 15 моделей с одной или даже несколькими связями has_one это что придется повторить эти гетеры сеттеры в каждом месте? Такое конечно можно сделать, но тогда нарушается очень важный принцип DRY. Ну и плюс природная лень не позволяет так расточительно писать много строк кода. Так что надо найти способ автоматизировать этот процесс и тут на помощь приходит метапрограммирование.

Meta решение

Что же приступим

self.reflect_on_all_associations(:has_one).each do |has_one_association|  define_method("#{has_one_association.name}_id") do    self.send(has_one_association.name).try :id  end  define_method("#{has_one_association.name}_id=") do |id|    self.send("#{has_one_association.name}=",has_one_association.klass.find_by_id(id))  endend

Вот так выглядит код, который подружит has_one и Rails Admin

А теперь более подробно что тут происходит. Далее детально буду останавливаться только на аспектах которые касаются рефлексии и мета программирования.

В руби всё является объектом, связь также является объектом и несет полную информацию о самой себе и всех своих отношениях. Первый интересный метод reflect_on_all_associations Который возвращает массив всех связей, но может принимать параметр "macro" в примере выше я передал туда :hasone и он вернул мне только has_one связи, прекрасно, даже не пришлось дальше селектить только нужные связи.

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

В итоге этот код создает методы необходимые для корректной инициализации has_one в rails admin.

Сушим до конца

Всё это задумывалось для создания "сухого" кода, так что сейчас опишу последнюю деталь. Нужно всю эту мета-магию вынести в concern

require 'active_support/concern'module HasOneHandler  extend ActiveSupport::Concern  included do    self.reflect_on_all_associations(:has_one).each do |has_one_association|      define_method("#{has_one_association.name}_id") do        self.send(has_one_association.name).try :id      end      define_method("#{has_one_association.name}_id=") do |id|        self.send("#{has_one_association.name}=",has_one_association.klass.find_by_id(id))      end    end  endend

И в итоге класса который в который нужно добавить гетеры и сетеры, добавляется всего одна строчка со включением консерна. Таким малословным способом удалось подружить CMS и has_one связи. Если применять стандартный подход то пришлось бы писать определять гетеры и сетеры для каждой связи в каждой модели, а их может быть немало.

Итоговая версия модели
class TechProcess < ApplicationRecord  include MdcSchema  include HasOneHandler  has_many :executor_programs, inverse_of: :tech_process, foreign_key: :barcode_tech_process, primary_key: :barcode  validates :barcode, presence: true  validates :barcode, uniqueness: true  has_one :draft2tech_process, dependent: :destroy  has_one :draft, through: :draft2tech_process  has_many :tech_process2tech_operations  has_many :tech_operations, through: :tech_process2tech_operationsend

Заключение

Надеюсь мне удалось показать практическую ценность использования приемов метапрограммирования. Использовать его или нет решать конечно вам. Если применять его слишком часто и не к месту, то проект превратиться в абсолютно не читаемый и трудно отлаживаемый, но в случае правильного использования, напротив, уменьшает количество кода, улучшает читаемость и избавляет от рутиной работы. Спасибо всем кто дочитал!

Подробнее..

Stm32 USB на шаблонах C. Продолжение. Делаем HID

27.03.2021 20:12:03 | Автор: admin

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

Разделение прерывания

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

using EpRequestHandler = std::add_pointer_t<void()>;template<typename...>class EndpointHandlersBase;template<typename... Endpoints, int8_t... Indexes>class EndpointHandlersBase<TypeList<Endpoints...>, Int8_tArray<Indexes...>>{public:  // Массив указателей на обработчики  static constexpr EpRequestHandler _handlers[] = {Endpoints::Handler...};  // Индексы обработчиков  static constexpr int8_t _handlersIndexes[] = {Indexes...};public:  inline static void Handle(uint8_t number, EndpointDirection direction)  {    _handlers[_handlersIndexes[2 * number + (direction == EndpointDirection::Out ? 1 : 0)]]();  }};

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

template<int8_t Index, typename Endpoints>class EndpointHandlersIndexes{  // Предикат для поиска очередной конечной точки.  using Predicate = Select<Index % 2 == 0, IsTxOrBidirectionalEndpointWithNumber<Index / 2>, IsRxOrBidirectionalEndpointWithNumber<Index / 2>>::value;  static const int8_t EndpointIndex = Search<Predicate::template type, Endpoints>::value;public:  // В конец массива индекса вставляется номер соответствующей конечной точки или -1 в случае пропуска.  using type = typename Int8_tArray_InsertBack<typename EndpointHandlersIndexes<Index - 1, Endpoints>::type, EndpointIndex>::type;};template<typename Endpoints>class EndpointHandlersIndexes<-1, Endpoints>{public:  using type = Int8_tArray<>;};

Из такой реализации, кстати, вытекает рекомендация объявлять конечные точки с номерами по порядку, потому что размер массива индексов обработчиков равен удвоенному максимальному номеру конечной точки.

Класс конечной точки

Из-за необходимости распределить ресурсы между конечными точками пришлось разделить код на два класса: базовый, который и нужно инстанцировать:

template <uint8_t _Number, EndpointDirection _Direction, EndpointType _Type, uint16_t _MaxPacketSize, uint8_t _Interval>class EndpointBase...

А также расширенный, с адресами буферов и регистра (которые назначаются менеджером, описанным в прошлой статье). Из-за различий между конечными точками разных типов/направлений (например, у однонаправленной конечной точки типа Interrupt будет один буфер, а у двунаправленной или Bulk с двойной буферизацией - два) для каждого типа объявлен свой класс:

template <typename _Base, typename _Reg>class Endpoint : public _Base...template<typename _Base, typename _Reg, uint32_t _TxBufferAddress, uint32_t _TxCountRegAddress, uint32_t _RxBufferAddress, uint32_t _RxCountRegAddress>class BidirectionalEndpoint : public Endpoint<_Base, _Reg>...template<typename _Base, typename _Reg, uint32_t _Buffer0Address, uint32_t _Count0RegAddress, uint32_t _Buffer1Address, uint32_t _Count1RegAddress>class BulkDoubleBufferedEndpoint : public Endpoint<_Base, _Reg>

Конечная точка на текущий момент реализована простой: экспортирует метод инициализации (в котором заполняется регистр EPnR), метод заполнения дескриптора, методы управления битами регистра (Очистка битов CTR_TX/RX, установка битов TX/RX_STATUS), а также отправку данных.

Класс интерфейса

Следующей сущностью в иерархии является интерфейс, который, по сути (как я понимаю) есть просто контейнер для конечных точек, поэтому реализующий его класс очень простой (прикладываю код полностью, потому что здесь применяется мощь variadic-шаблонов, который позволил исключить лишние зависимости):

template <uint8_t _Number, uint8_t _AlternateSetting = 0, uint8_t _Class = 0, uint8_t _SubClass = 0, uint8_t _Protocol = 0, typename... _Endpoints>class Interface{public:  using Endpoints = Zhele::TemplateUtils::TypeList<_Endpoints...>;  static const uint8_t EndpointsCount = ((_Endpoints::Direction == EndpointDirection::Bidirectional ? 2 : 1) + ...);  static void Reset()  {    (_Endpoints::Reset(), ...);  }  static uint16_t FillDescriptor(InterfaceDescriptor* descriptor)  {    uint16_t totalLength = sizeof(InterfaceDescriptor);    *descriptor = InterfaceDescriptor {      .Number = _Number,      .AlternateSetting = _AlternateSetting,      .EndpointsCount = EndpointsCount,      .Class = _Class,      .SubClass = _SubClass,      .Protocol = _Protocol    };        EndpointDescriptor* endpointsDescriptors = reinterpret_cast<EndpointDescriptor*>(++descriptor);    totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + ...);    return totalLength;  }};

Класс конфигурации

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

template <uint8_t _Number, uint8_t _MaxPower, bool _RemoteWakeup = false, bool _SelfPowered = false, typename... _Interfaces>class Configuration{public:  using Endpoints = Zhele::TemplateUtils::Append_t<typename _Interfaces::Endpoints...>;  static void Reset()  {    (_Interfaces::Reset(), ...);  }...

Класс устройства

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

template<  typename _Regs,  IRQn_Type _IRQNumber,  typename _ClockCtrl,   uint16_t _UsbVersion,  DeviceClass _Class,  uint8_t _SubClass,  uint8_t _Protocol,  uint16_t _VendorId,  uint16_t _ProductId,  uint16_t _DeviceReleaseNumber,  typename _Ep0,  typename... _Configurations>class DeviceBase : public _Ep0{  using This = DeviceBase<_Regs, _IRQNumber, _ClockCtrl, _UsbVersion, _Class, _SubClass, _Protocol, _VendorId, _ProductId, _DeviceReleaseNumber, _Ep0, _Configurations...>;  using Endpoints = Append_t<typename _Configurations::Endpoints...>;  using Configurations = TypeList<_Configurations...>;  // Replace Ep0 with this for correct handler register.  using EpBufferManager = EndpointsManager<Append_t<_Ep0, Endpoints>>;  using EpHandlers = EndpointHandlers<Append_t<This, Endpoints>>;...

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

static void CommonHandler(){  if(_Regs()->ISTR & USB_ISTR_RESET)  {    Reset();  }  if (_Regs()->ISTR & USB_ISTR_CTR)  {    uint8_t endpoint = _Regs()->ISTR & USB_ISTR_EP_ID;    EpHandlers::Handle(endpoint, ((_Regs()->ISTR & USB_ISTR_DIR) != 0 ? EndpointDirection::Out : EndpointDirection::In));  }  NVIC_ClearPendingIRQ(_IRQNumber);}

Являясь одновременно и нулевой конечной точкой, класс Device содержит обработчик обращений к нулевой точке, реализуя, на данный момент, процесс нумерации:

Обработчик прерывания нулевой конечной точки
static void Handler(){  if(_Ep0::Reg::Get() & USB_EP_CTR_RX)  {    _Ep0::ClearCtrRx();    if(_Ep0::Reg::Get() & USB_EP_SETUP)    {      SetupPacket* setup = reinterpret_cast<SetupPacket*>(_Ep0::RxBuffer);      switch (setup->Request) {      case StandartRequestCode::GetStatus: {        uint16_t status = 0;        _Ep0::Writer::SendData(&status, sizeof(status));        break;      }      case StandartRequestCode::SetAddress: {        TempAddressStorage = setup->Value;        _Ep0::Writer::SendData(0);        break;      }      case StandartRequestCode::GetDescriptor: {        switch (static_cast<GetDescriptorParameter>(setup->Value)) {        case GetDescriptorParameter::DeviceDescriptor: {          DeviceDescriptor tempDeviceDescriptor;          FillDescriptor(reinterpret_cast<DeviceDescriptor*>(&tempDeviceDescriptor));          _Ep0::Writer::SendData(&tempDeviceDescriptor, setup->Length < sizeof(DeviceDescriptor) ? setup->Length : sizeof(DeviceDescriptor));          break;        }        case GetDescriptorParameter::ConfigurationDescriptor: {          uint8_t temp[64];          uint16_t size = GetType<0, Configurations>::type::FillDescriptor(reinterpret_cast<ConfigurationDescriptor*>(&temp[0]));          _Ep0::Writer::SendData(reinterpret_cast<ConfigurationDescriptor*>(&temp[0]), setup->Length < size ? setup->Length : size);          break;        }        case GetDescriptorParameter::HidReportDescriptor: {          uint16_t size = sizeof(GetType_t<0, Configurations>::HidReport::Data);          _Ep0::Writer::SendData(GetType_t<0, Configurations>::HidReport::Data, setup->Length < size ? setup->Length : size);          break;        }        default:          _Ep0::SetTxStatus(EndpointStatus::Stall);          break;        }        break;      }      case StandartRequestCode::GetConfiguration: {        uint16_t configuration = 0;        _Ep0::Writer::SendData(&configuration, 1);        break;      }      case StandartRequestCode::SetConfiguration: {        _Ep0::Writer::SendData(0);        break;      }      default:        _Ep0::SetTxStatus(EndpointStatus::Stall);        break;      }    }    _Ep0::SetRxStatus(EndpointStatus::Valid);  }  if(_Ep0::Reg::Get() & USB_EP_CTR_TX)  {    _Ep0::ClearCtrTx();    if(TempAddressStorage != 0)    {      _Regs()->DADDR = USB_DADDR_EF | (TempAddressStorage & USB_DADDR_ADD);      TempAddressStorage = 0;    }    _Ep0::SetRxStatus(EndpointStatus::Valid);  }}

Интерфейс HID

HID-устройство - это устройство как минимум с одним интерфейсом типа HID, поэтому в библиотеке класс HID - это производный от интерфейса:

Класс интерфейса hid
template <uint8_t _Number, uint8_t _AlternateSetting, uint8_t _SubClass, uint8_t _Protocol, typename _Hid, typename... _Endpoints>class HidInterface : public Interface<_Number, _AlternateSetting, 0x03, _SubClass, _Protocol, _Endpoints...>{  using Base = Interface<_Number, _AlternateSetting, 0x03, _SubClass, _Protocol, _Endpoints...>;public:  using Endpoints = Base::Endpoints;  static uint16_t FillDescriptor(InterfaceDescriptor* descriptor)  {    uint16_t totalLength = sizeof(InterfaceDescriptor);    *descriptor = InterfaceDescriptor {      .Number = _Number,      .AlternateSetting = _AlternateSetting,      .EndpointsCount = Base::EndpointsCount,      .Class = 0x03,      .SubClass = _SubClass,      .Protocol = _Protocol    };    _Hid* hidDescriptor = reinterpret_cast<_Hid*>(++descriptor);    *hidDescriptor = _Hid {    };    uint8_t* reportsPart = reinterpret_cast<uint8_t*>(++hidDescriptor);    uint16_t bytesWritten = _Hid::FillReports(reportsPart);    totalLength += sizeof(_Hid) + bytesWritten;    EndpointDescriptor* endpointsDescriptors = reinterpret_cast<EndpointDescriptor*>(&reportsPart[bytesWritten]);    totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + ...);    return totalLength;  }private:};

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

HID-устройство

Теперь давайте из всего этого сделаем устройство, которое будет содержать один светодиод (потому что так удобно, он есть на плате BluePill) и поддерживать возможность управления этим светодиодом с компьютера (через USB HID Demonstrator).

Основной любого HID-устройства является Report, определяющий порядок взаимодействия. В нашем случае он будет достаточно простым:

using Report = HidReport<  0x06, 0x00, 0xff,    // USAGE_PAGE (Generic Desktop)  0x09, 0x01,          // USAGE (Vendor Usage 1)  0xa1, 0x01,          // COLLECTION (Application)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x15, 0x00,          //   LOGICAL_MINIMUM (0)  0x25, 0x01,          //   LOGICAL_MAXIMUM (1)  0x75, 0x08,          //   REPORT_SIZE (8)  0x95, 0x01,          //   REPORT_COUNT (1)  0xb1, 0x82,          //   FEATURE (Data,Var,Abs,Vol)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x91, 0x82,          //   OUTPUT (Data,Var,Abs,Vol)  0xc0                 // END_COLLECTION>;

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

using HidDesc = HidDescriptor<0x1001, Report>;using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 32>;using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;using Hid = HidInterface<0, 0, 0, 0, HidDesc, LedsControlEp>;using Config = HidConfiguration<0, 250, false, false, Report, Hid>;using MyDevice = Device<0x0200, DeviceClass::InterfaceSpecified, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;

В общем-то всё, осталось написать обработчик для конечной точки управления светодиодом:

using Led = IO::Pc13Inv; // Inv - инвертированный.template<>void LedsControlEp::Handler(){  LedsControlEp::ClearCtrRx();  uint8_t* buffer = reinterpret_cast<uint8_t*>(LedsControlEp::Buffer);  bool needSet = buffer[1] != 0;  // Код почти целиком позаимствован из поста "STM32 и USB-HID  это просто".  // Не стал изменять его для удобной навигации.  switch(buffer[0])  {  case 1:    needSet ? Led::Set() : Led::Clear();    break;  }  LedsControlEp::SetRxStatus(EndpointStatus::Valid);}

Целиком файл main.c для Stm32f103 выглядит так (по-моему, достаточно компактно):

Полный код программы
#include <clock.h>#include <iopins.h>#include <usb.h>using namespace Zhele;using namespace Zhele::Clock;using namespace Zhele::IO;using namespace Zhele::Usb;using Report = HidReport<  0x06, 0x00, 0xff,        // USAGE_PAGE (Generic Desktop)  0x09, 0x01,          // USAGE (Vendor Usage 1)  0xa1, 0x01,          // COLLECTION (Application)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x15, 0x00,          //   LOGICAL_MINIMUM (0)  0x25, 0x01,          //   LOGICAL_MAXIMUM (1)  0x75, 0x08,          //   REPORT_SIZE (8)  0x95, 0x01,          //   REPORT_COUNT (1)  0xb1, 0x82,          //   FEATURE (Data,Var,Abs,Vol)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x91, 0x82,          //   OUTPUT (Data,Var,Abs,Vol)  0xc0               // END_COLLECTION>;using HidDesc = HidDescriptor<0x1001, Report>;using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 32>;using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;using Hid = HidInterface<0, 0, 0, 0, HidDesc, LedsControlEp>;using Config = HidConfiguration<0, 250, false, false, Report, Hid>;using MyDevice = Device<0x0200, DeviceClass::InterfaceSpecified, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;using Led = IO::Pc13Inv;void ConfigureClock();void ConfigureLeds();int main(){  ConfigureClock();  ConfigureLeds();  Zhele::IO::Porta::Enable();  MyDevice::Enable();  for(;;)  {  }}void ConfigureClock(){  PllClock::SelectClockSource(PllClock::ClockSource::External);  PllClock::SetMultiplier(9);  Apb1Clock::SetPrescaler(Apb1Clock::Div2);  SysClock::SelectClockSource(SysClock::Pll);  MyDevice::SelectClockSource(Zhele::Usb::ClockSource::PllDividedOneAndHalf);}void ConfigureLeds(){  Led::Port::Enable();  Led::SetConfiguration<Led::Configuration::Out>();  Led::SetDriverType<Led::DriverType::PushPull>();  Led::Set();}template<>void LedsControlEp::Handler(){  LedsControlEp::ClearCtrRx();  uint8_t* buffer = reinterpret_cast<uint8_t*>(LedsControlEp::Buffer);  bool needSet = buffer[1] != 0;  switch(buffer[0])  {  case 1:    needSet ? Led::Set() : Led::Clear();    break;  }  LedsControlEp::SetRxStatus(EndpointStatus::Valid);}extern "C" void USB_LP_IRQHandler(){  MyDevice::CommonHandler();}

Заключение

Не совсем очевидная реализация библиотечного кода (в прошлой статье получил заслуженные комментарии в стиле "Не хотел бы увидеть такой код в продакшне", "Как это поддерживать" и т.п.) позволила максимально упростить непосредственно реализацию устройства, не нужно даже вручную объявлять дескрипторы: все генерируется из подставленных в шаблоны аргументов. Использование variadic-шаблонов помогло избавиться от лишних зависимостей. Прошивка тоже получается компактной, код из примера выше с оптимизацией Og вышел в 2360 байтов Flash и 36 байтов RAM (с оптимизацией Os прошивка весит 1712 байтов, но не работает. Пока не разобрался, почему именно), что я считаю неплохим результатом.

Благодарности

За замечательный пост про HID благодарен @RaJa. Также менее, чем за неделю до написания этого поста вышел еще крутой материал по HID от @COKPOWEHEU. Без этих постов я бы ничего не осилил. Еще большую помощь оказали пользователи с форума radiokot (COKPOWEHEU и VladislavS), был приятно удивлен оперативностью ответов и желанием помочь.

Подробнее..

Stm32 USB на шаблонах C. Продолжение. Делаем CDC

30.05.2021 20:12:29 | Автор: admin

Продолжаю разработку полностью шаблонной библиотеки под микроконтроллеры Stm32, в прошлой статье рассказал об успешной (почти) реализации HID устройства. Еще одним популярным классом USB является виртуальный COM-порт (VCP) из класса CDC. Популярность объясняется тем, что обмен данными осуществляется аналогично привычному и простому последовательному протоколу UART, однако снимает необходимость установки в устройство отдельного преобразователя.

Интерфейсы

Устройство класса CDC должно поддерживать два интерфейса: интерфейс для управления параметрами соединения и интерфейс обмена данными.

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

template <uint8_t _Number, uint8_t _AlternateSetting, uint8_t _SubClass, uint8_t _Protocol, typename _Ep0, typename _Endpoint, typename... _Functionals>class CdcCommInterface : public Interface<_Number, _AlternateSetting, DeviceAndInterfaceClass::Comm, _SubClass, _Protocol, _Ep0, _Endpoint>{  using Base = Interface<_Number, _AlternateSetting, DeviceAndInterfaceClass::Comm, _SubClass, _Protocol, _Ep0, _Endpoint>;  static LineCoding _lineCoding;  ...

В базовом случае интерфейс должен поддерживать три управляющих (setup) пакета:

  • SET_LINE_CODING: установка параметров линии: Baudrate, Stop Bits, Parity, Data bits. Некоторые проекты, на которые я ориентировался (основным источников вдохновения стал этот проект), игнорируют данный пакет, однако в этом случае некоторые терминалы (например, Putty), отказываются работать.

  • GET_LINE_CODING: обратная операция, в ответ на эту команду устройство должно вернуть текущие параметры.

  • SET_CONTROL_LINE_STATE: установка состояния линии (RTS, DTR и т.д.).

Код обработчика setup-пакетов:

switch (static_cast<CdcRequest>(setup->Request)){case CdcRequest::SetLineCoding:  if(setup->Length == 7)  {    // Wait line coding    _Ep0::SetOutDataTransferCallback([]{      memcpy(&_lineCoding, reinterpret_cast<const void*>(_Ep0::RxBuffer), 7);      _Ep0::ResetOutDataTransferCallback();      _Ep0::SendZLP();    });    _Ep0::SetRxStatus(EndpointStatus::Valid);  }  break;case CdcRequest::GetLineCoding:  _Ep0::SendData(&_lineCoding, sizeof(LineCoding));  break;case CdcRequest::SetControlLineState:  _Ep0::SendZLP();  break;default:  break;}

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

static uint16_t FillDescriptor(InterfaceDescriptor* descriptor){  uint16_t totalLength = sizeof(InterfaceDescriptor);    *descriptor = InterfaceDescriptor {    .Number = _Number,    .AlternateSetting = _AlternateSetting,    .EndpointsCount = Base::EndpointsCount,    .Class = DeviceAndInterfaceClass::Comm,    .SubClass = _SubClass,    .Protocol = _Protocol  };  uint8_t* functionalDescriptors = reinterpret_cast<uint8_t*>(descriptor);  ((totalLength += _Functionals::FillDescriptor(&functionalDescriptors[totalLength])), ...);  EndpointDescriptor* endpointDescriptors = reinterpret_cast<EndpointDescriptor*>(&functionalDescriptors[totalLength]);  totalLength += _Endpoint::FillDescriptor(endpointDescriptors);  return totalLength;}

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

template <uint8_t _Number, uint8_t _AlternateSetting, uint8_t _SubClass, uint8_t _Protocol, typename _Ep0, typename _Endpoint>class CdcDataInterface : public Interface<_Number, _AlternateSetting, DeviceAndInterfaceClass::CdcData, _SubClass, _Protocol, _Ep0, _Endpoint>{  using Base = Interface<_Number, _AlternateSetting, DeviceAndInterfaceClass::CdcData, _SubClass, _Protocol, _Ep0, _Endpoint>;  ...

Поскольку мои познания в CDC-устройствах весьма небольшие, из просмотренных примеров я сделал вывод, что управляющий интерфейс почти всегда одинаковый и содержит 4 функциональности: Header, CallManagement, ACM, Union, поэтому добавил упрощенный шаблон интерфейса:

template<uint8_t _Number, typename _Ep0, typename _Endpoint>using DefaultCdcCommInterface = CdcCommInterface<_Number, 0, 0x02, 0x01, _Ep0, _Endpoint, HeaderFunctional, CallManagementFunctional, AcmFunctional, UnionFunctional>;

Применение разработанных классов

Для использования разработанных классов достаточно объявить две конечные точки (Interrupt для первого интерфейса и двунаправленную Bulk для второго), объявить оба интерфейса, конфигурацию с ними и, наконец, инстанцировать класс устройства:

using CdcCommEndpointBase = InEndpointBase<1, EndpointType::Interrupt, 8, 0xff>;using CdcDataEndpointBase = BidirectionalEndpointBase<2, EndpointType::Bulk, 32, 0>;using EpInitializer = EndpointsInitializer<DefaultEp0, CdcCommEndpointBase, CdcDataEndpointBase>;using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;using CdcCommEndpoint = EpInitializer::ExtendEndpoint<CdcCommEndpointBase>;using CdcDataEndpoint = EpInitializer::ExtendEndpoint<CdcDataEndpointBase>;using CdcComm = DefaultCdcCommInterface<0, Ep0, CdcCommEndpoint>;using CdcData = CdcDataInterface<1, 0, 0, 0, Ep0, CdcDataEndpoint>;using Config = Configuration<0, 250, false, false, CdcComm, CdcData>;using MyDevice = Device<0x0200, DeviceAndInterfaceClass::Comm, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;

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

template<>void CdcDataEndpoint::HandleRx(){  uint8_t* data = reinterpret_cast<uint8_t*>(CdcDataEndpoint::RxBuffer);  uint8_t size = CdcDataEndpoint::RxBufferCount::Get();  if(size > 0)  {    if(data[0] == '0')    {      Led::Clear();      CdcDataEndpoint::SendData("LED is turn off\r\n", 17);    }    if(data[0] == '1')    {      Led::Set();      CdcDataEndpoint::SendData("LED is turn on\r\n", 16);    }  }  CdcDataEndpoint::SetRxStatus(EndpointStatus::Valid);}

Отладка и тестирование

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

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

WireShark с установленным UsbPcap оказался весьма удобным, он нормально парсит все данные, так что поиск ошибок значительно упрощается. Главное, что нужно сделать - правильно установить фильтры. Не нашел ничего лучше, кроме выполнить следующие две операции:

Сначала отфильтровать по заведомо известному значению. Например, по значению PID, которое присутствует в ответе устройства на запрос GET_DEVICE_DESCRIPTOR. Фильтр: "usb.idProduct == 0x5711". Это позволит быстро определить адрес устройства.

Далее отфильтровать по адресу устройства с помощью оператора contains. Дело в том, что отображаемый адрес состоит из трех частей, последняя из которых является номером конечной точки (можно, конечно, перечислить все адреса). Фильтр: "usb.addr contains "1.19"".

Однако стоит заметить, что UsbPcap может доставить некоторые трудности, под катом опишу ситуацию, в которую недавно попал и потратил кучу времени и нервов.

Проблема с usbpcap

Для большей мобильности завел себе внешний SSD, на котором установлена Windows 10 To Go (Windows, предназначенная для установки на внешние носители). Хотя Microsoft вроде отказалась от поддержки этой технологии, в целом все работает. Прихожу с диском в новое место, гружусь с него, система подтягивает драйвера и все нормально (и быстро) работает.

Однажды Windows просто не загрузилась с синим экраном "inaccessible boot device". Потратил целые выходные, восстановить так и не смог, пришлось все переустановить. Через некоторое время та же проблема и снова потраченные на переустановку выходные. Спустя пару дней система опять не грузится, начал вспоминать и анализировать, что я такого делал. Выяснил, что проблема возникала после установки как раз WireShark с usbpcap. На одном из форумов наткнулся на сообщение от пользователя, который жаловался на проблему с мышкой/клавиатурой после установки usbpcap. Снес через LiveCD драйвер и Windows запустилась. Не уверен на 100%, но предположение такое: при запуске компьютера Windows начинается загружаться, подгружает драйвера usbpcap, тот блокирует USB, система дальше грузиться не может и падает в BSOD. Очень неочевидное поведение, жаль потраченного времени.

Тестировал написанный код в программе Terminal v1.9b, на скриншоте приведен результат отправки на устройство сообщений "0" и "1".

Полный код примера можно посмотреть в репозитории. Пример протестирован на STM32F072B-DISCO. Как и в случае с HID, громоздкая библиотека (особенно менеджер конечных точек) сильно облегчили реализацию поддержки CDC, на все ушел примерно полный день. Далее планирую добавить еще класс Mass Storage Device, и на этом, наверно, можно остановиться. Приветствую вопросы и замечания.

Подробнее..

Семинары лаборатории языковых инструментов JetBrains Research

29.10.2020 20:09:21 | Автор: admin
Лаборатория языковых инструментов совместная инициатива JetBrains и математико-механического факультета СПбГУ.

Сотрудники лаборатории исследуют:
  • формализацию и верификацию семантики языков программирования в контексте слабых моделей памяти;
  • логическое и реляционное программирование;
  • теорию формальных языков и ее применения;
  • метапрограммирование, специализацию и частичные вычисления;
  • формальную верификацию и применение SMT-решателей.

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



Прошедшие доклады:

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

Первым шагом к решению этой проблемы является формализация файловых систем. В докладе мы рассмотрим Linux ext4 и её формальную модель, интегрированную с моделью памяти C/С++11. Кроме того, обсудим адаптацию алгоритма проверки моделей GenMC для верификации программ, работающих с файлами. В конце будут приведены примеры ошибок, которые удалось найти с помощью адаптированного GenMC в текстовых редакторах, таких как vim и nano.

Докладчик: Илья Кайсин

Ссылка

Реализация сжатия ссылок в кучу в GraalVM Native Image
В некоторых приложениях размер адресного пространства существенно превышает размер области памяти, на которую может ссылаться указатель. Сжатие ссылок позволяет уменьшить размер приложения и ускорить его выполнение и старт. Рассматриваются варианты реализации сжатых ссылок. Демонстрируется способ легкого введения сжатия ссылок в хорошо организованную реализацию языков программирования.

Докладчик: Олег Плисс

Ссылка

Слегка субкубический алгоритм для задачи поиска путей с контекстно-свободными ограничениями
Все мы так или иначе сталкиваемся с задачами, решаемыми за полиномиальное время. Но есть такие задачи, для которых кубическое или, например, квадратичное время работы является слишком неэффективным для использования на практике. Возникает вопрос, может ли конкретная задача быть решена немного быстрее, например, хотя бы за субкубическое (n^{3-e}) (или субквадратичное) время? Если нет, то как показать, что такое время работы оптимально?

В докладе будет рассмотрена одна из таких задач задача поиска путей с контекстно-свободными ограничениями (CFL-reachability), к которой сводится большое количество задач анализа программ. Вопрос о существовании субкубического алгоритма для решения этой задачи является открытым уже более 30 лет. Оптимально ли классическое кубическое решение? Мы попробуем ответить на этот вопрос, используя инструменты современного направления теории сложности fine-grained complexity. Также будет показано, как ускорить время решения на логарифмический фактор, получив тем самым "слегка субкубический" алгоритм для задачи CFL-reachability.

Докладчик: Екатерина Шеметова

Ссылка

Построение сертифицированных частичных вычислителей
Частичные вычисления позволяют улучшать производительность кода на основании частично известных данных или свойств использованных в программе операций, заданных, например, правилами переписывания термов. Реализация сертифицированных частичных вычислителей, работающих за адекватное время нетривиальная задача сама по себе. Создавать их, используя как можно меньше несертифицированных компонент еще сложнее. В докладе я расскажу про систему для создания таких частичных вычислителей, предложенную Джейсоном Гроссом. Система реализована на Coq, позволяет задавать свои правила перепивывания термов и способна специализировать библиотеки сертифицированного кода на Coq длиной в тысячи строк.

Докладчик: Екатерина Вербицкая

Ссылка

Проверка моделей в слабых моделях памяти
Проверка моделей это метод автоматической формальной верификации программ. На сегодняшний день большинство инструментов для проверки моделей либо не учитывают эффекты, возникающие в слабых моделях памяти, либо работают только с некоторой фиксированной моделью. Среди этих инструментов выделяется GenMC, который не привязан к конкретной модели памяти. GenMC может работать с широким классом моделей (в который входят, например, модели RC11 и IMM). Тем не менее наиболее сложные и интересные модели (Promising, Weakestmo) не поддерживаются, так как эти в этих моделях не выполняются некоторые свойства, подразумеваемые GenMC. Кроме того, оказывается что и сами эти модели (Promising, Weakestmo), настолько слабые, что какой-либо алгоритм их проверки может оказаться неприменимым на практике.

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

Докладчик: Евгений Моисеенко

Ссылка

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

Докладчик: Екатерина Вербицкая

Ссылка

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

Докладчик: Даниил Березун

Ссылка

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

Докладчик: Дмитрий Косарев

Ссылка

О представимости инвариантов программ с алгебраическими типами данных
Со времён появления логики Хоара принято выражать свойства и сертификаты корректности программ на языках первого порядка. Современные методы автоматического вывода индуктивных инвариантов программ ориентированы на представление инвариантов также в логике первого порядка. Хотя такие представления очень выразительны для некоторых теорий (LIA, LRA BV, теория массивов), они не позволяют выразить многие интересные свойства программ над алгебраическими типами данных (АТД).

В докладе будут рассмотрены три различных подхода к представлению индуктивных инвариантов программ над АТД: формулами первого порядка, древовидными автоматами и формулами первого порядка с ограничениями на размер. Мы сравним их выразительную мощность и увидим на простых примерах, что очень часто современные инструменты вывода инвариантов не будут завершаться по причине непредставимости инварианта в языке. Также мы рассмотрим, как автоматически выводить регулярные инварианты программ над АТД с помощью инструментов поиска конечных моделей. Будет представлено сравнение эффективности вывода регулярных инвариантов через построение конечных моделей с современными Хорн-решателями.

Докладчик: Юрий Костюков

Ссылка

Разработка компиляторов предметно-ориентированных языков для спецпроцессоров
В составе современных вычислительных систем все чаще используются аппаратные спецпроцессоры, программируемые на предметно-ориентированных языках. Популярность набирает подход compiler-in-the-loop, предполагающий совместную разработку спецпроцессора и компилятора. При этом традиционный инструментарий, GCC и LLVM, оказывается недостаточным для быстрой разработки оптимизирующих компиляторов, порождающих целевой код нетрадиционной, нерегулярной архитектуры со статическим параллелизмом операций.

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

Докладчик: Пётр Советов

Ссылка

Логика некорректности
Всем хорошо известна так называемая логика корректности Хоара, позволяющая формально доказать, что программа работает правильно. А что если мы захотим формально доказать, что в программе есть баг? Например, что программа упадет, если дать ей на вход очень большую строку, при этом не уточняя, какая именно это строка. Оказывается, процесс доказательства наличия бага в программе можно формализовать так же, как мы формализуем доказательство корректности с помощью логики Хоара, используя Логику Некорректности В докладе мы поговорим про Логику Некорректности: обсудим ее связь с логикой корректности Хоара, посмотрим на ее натуральный вывод и семантику, отловим пару багов и, наконец, рассмотрим, как она связана с Динамической Логикой и Relation Algebra.

Докладчик: Владимир Гладштейн

Ссылка

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

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

Докладчик: Дима Розплохас

Ссылка

Ближайший доклад 2 ноября сделает Антон Трунов по теме Неразличимые доказательства: по определению, но без аксиомы К. Присоединяйтесь в 17:30 в Google Meet по ссылке.

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

Первая часть доклада подготовит почву для освещения основной темы: в частности будет рассмотрена структура вселенных в системе интерактивных доказательств Coq. Во второй части пойдет речь об относительно недавно добавленной экспериментальной вселенной строгих высказываний SProp с неразличимыми по определению доказательствами. При этом сохраняются перечисленные выше желательные свойства и показывается неадекватность известного подхода одиночной элиминации во вселенной Prop для настоящей проблемы. Также будет показана связь SProp с другими вселенными на простых примерах.

Чтобы получать анонсы наших семинаров:
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru