Последнее время я сильно увлекся вопросом надежности софта для микроконтроллеров, 0xd34df00d посоветовал мне сильнодействующие препараты, но к сожалению руки пока не дошли до изучения Haskell и Ivory для микроконтроллеров, да и вообще до совершенно новых подходов к разработке ПО отличных от ООП. Я лишь начал очень медленно вкуривать функциональное программирование и формальные методы.  Все мои потуги в этих направлениях это, как было сказано в комментарии ради любви к технологиям, но есть подозрение, что сейчас никто не даст мне применять такие подходы (хотя, как говориться, поживем увидим). Уж больно специфические навыки должны быть у программиста, который все это дело будет поддерживать. Полагаю, что написав однажды программу на таком языке, моя контора будет долго искать человека, который сможет принять такой код, поэтому на практике для студентов и для работы я все еще по старинке использую С++.  Продолжу развивать тему о встроенном софте для небольших микроконтроллеров в устройствах для safety critical систем. На этот раз попробую предложить способ работы с конкретными ножками микроконтроллера, используя обертку над регистрами, которую я описал в прошлой статье Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM) Чтобы было общее представление того о чем я хочу рассказать, приведу небольшой кусок кода: Я решил разбить повествование на две статьи. На часть с мыслями по-поводу организации настройки портов и работы с Pinaми, и часть экспериментальную, описывающую объединение Pinов, которая, полагаю не будет иметь особо практического применения из-за сложности, но возможно будет интересна для того, чтобы показать насколько С++ может быть эффективным. Как я уже говорил, я обучаю студентов разработке ПО для измерительных устройств. Работа в университете — это мое хобби, основное место работы с университетом не связано, но тоже коррелирует с разработкой встроенного софта, в том числе и для высоко-надежных систем.  На ранних стадиях преподавания я рассказывал студентов про CMSIS и автоматические системы первоначальной настройки микроконтроллера (типа Cube), но через некоторое время понял:  Я не могу показывать студентам код, который мы используем на основной работе из-за NDA, поэтому решил использовать что-то самописное, что с одной стороны, позволит студентам показать все от самого низкого уровня (как обращаться к регистрам), а с другой стороны позволит писать бизнес логику, не задумываясь о внутренностях с наименьшим количеством ошибок.  Первым таким шагом, была обертка над регистрами. Логично, что вторым должны быть порты и пины. Поэтому с них и начнем. Порт — это средство общения микроконтроллера с внешним миром.  Порт может работать как цифровой вход и цифровой выход, некоторые порты могут работать в аналоговом режиме, т.е. на них можно подавать аналоговый сигнал, который затем будет поступать на входы АЦП, а таже функционировать в альтернативном режиме (это когда порт работает в режиме какой-нибудь периферии, скажем UART, SPI или USB). Для упрощения аналоговый и альтернативные режимы рассматривать не будем. Рассмотрим только два режима цифровой вход и выход: Есть еще несколько настроек портов, такие как подтяжка к 0 или 1, для того чтобы порт не "висел" в воздухе и еще настройка типа выхода (c открытым колектором или двухтактный), но с точки зрения программирования нам эти вещи сейчас не интересны. Давайте ограничимся простой абстракцией порта у которого есть методы Set() — установка 1, Reset() — установка 0, Get() — чтение состояния порта, SetInput() — установка в режим входа,SetOutput() — установка в режим выхода. Ради экономии текста, опустим метод Reset(). Можно описать Port следующим классом: Или, используя обертку над регистрами из прошлой статьи, кодом: Кому интересно, как сделан атомарный доступ см ниже: Первоисточник: Атомарные операции в Cortex-M3 Все сделали класс и забыли о нем. Его вообще не надо использовать на уровне бизнес логики, так как он совсем не безопасный, он служит чисто как обертка над регистрами, которые отвечают за работу с портом микроконтроллера. Вообще все его методы должны быть приватными (как показано в дизайне), чтобы у программиста уровня приложения не было соблазна использовать эти методы и не накликать беду. Я сейчас не будут делать эти методы приватными и специально добавлять друзей, дабы не загромождать итак уже довольно большой код, но посыл понятен. Переходим к Pinам Конкретный Pin порта мы хотим сделать безопасным и процесоро-независимым. Нужно запретить делать пользователю, то чего ему делать не положено и отвязать его от микропроцессора. Итак, конкретный Pin должен иметь связь с портом, номер пина и Get() и Set() методы. Эти два метода актуальны для любого микроконтроллера. Так же как, скорее всего, для любого микроконтроллера актуальны методы настройки Pin на вход или выход. А вот перевод в альтернативный режим или в аналоговый не всегда поддерживается микроконтроллерами, поэтому, чтобы не зависеть от микроконтроллеров не будем добавлять эту возможность, ниже я поясню, этот момент. Наша абстракция Pin для любого микроконтроллера будет выглядеть следующим образом: Можно сделать обычный класс, можно сделать полностью статический. Чтобы не создавать отдельно объекты класс Pin здесь я сделаю статический класс, но ничего не запрещает сделать это обычным классом. В общем это уже дело реализации, остановлюсь на статическом классе, мне кажется он проще и кода меньше. Сразу же добавили частичку статической проверки: пинов на порту у нас 16, поэтому пользователю уже не позволено передавать значение больше 15. Внимательный читатель заметит, что не у всех микроконтроллеров 15 пинов на одном порту. Да, можно этот параметр задавать в парметре шаблона, а можно просто константой. Здесь, я хотел показать, что у нас уже есть возможность запретить пользователю сделать неправильные вещи на уровне типа. По сути мы объявили тип Pin, который не может принимать значение номера Pina больше 15. Использовать класс можно так: Теперь немного отойду от темы и затрону вопрос как можно настраивать аппаратную часть для устройства.  Пользователь Vadimatorikda в статье Пять лет использования C++ под проекты для микроконтроллеров в продакшене поделился минусами использования С++ для своих проектов. В том числе описал и проблемы с которыми встречался я. С моей точки зрения он сделал очень правильные вывод: Проекты в которых я работал всегда имеют специальные спецификации, которые описывают все настройки микроконтроллера и всей его периферии. Эти настройки в большинстве случаев не меняются в течении работы и поэтому нет смысла засовывать во все классы возможность настройки пинов, портов, системы тактирования и так далее… Логичнее сделать классы простыми и только с той функциональностью, которая действительно нужна для работы. Это избавит от необходимости лазить по разным местам и искать где, что настраивается и перенастраиваться. Обычно для настройки периферии я использую функцию __low_level_int() это IAR встроенная функция, которая вызывается еще до инициализации всех переменных и объектов. Т.е. можно быть уверенным, что до того как объекты будут инициализированы, вся необходимая периферия микроконтроллера уже будет настроена и можно смело вызывать методы объектов или статических классов.  Замечу
Так как с помощью регистров можно сделать очень много плохих вещей, то функция __low_level_init() должна быть единственным местом, где идет обращение к регистрам и настраивается процессоро-зависимая часть периферии, в любом другом коде, обращение к регистрам должно быть строго запрещено.  Если, же все таки нужно, что-то перенастроить, например режим работы Pina с выхода на вход и обратно (если вы, скажем реализуете программно какой-нибудь однопроводной интерфейс), то необходимо будет вызывать метод соответствующей периферии(если ей позволено настраиваться спецификацией).  С Pinaми по идее ничего больше делать не надо. Как я уже сказал, в редких случая (см пример выше) нам нужна настройка Pina на вход и выход, поэтому просто так удалять методы SetInput() и SetOutput() нельзя. В связи с этим снова вернемся к классу Pin Как я уже говорил, обычно в проектах, в которых я участвую, все прописано в спецификациях, в том числе и режимы настройки каждого пина каждого порта. Некоторые пины настроены только как вход, а некоторые как выход на всю свою жизнь.  И у нас должна быть возможность сделать так, чтобы у Pinа, настроенного на вход не было возможности вызвать метод Set(), и наоборот для Pinа, настроенного на выход, не было даже намека на метод Get().  Пины же, которые могут работать в обоих режимах, должны быть конфигурируемы. Для этого можно ввести интерфейсы: Всемогущий Pin может делать что угодно, но он самый небезопасный и здесь он чисто для примера. C помощью SFINAE можно определить набор методов для Pin, имеющих разные интерфейсы, чтобы не загружать сильно код покажу только 3 метода: Сделав такой класс, можем посмотреть в спецификацию настройки периферии, в ней может быть прописано, что то типа такого:  Сконфигурировав так пины, мы позволим пользователю (программисту) делать только то, что утверждено спецификацией: Т.е. вся идея конфигурирования заключается в том, что программист смотрит спецификацию, ищет настройку пина и переписывает её в конфигурацию Pin, задавая соответствующий тип, а затем на уровне бизнес логики использует только те функции типаPin, которые позволены в соответствии со спецификацией. Если Pin, настроен как PinReadable, то уж извините ни перенастроить его, ни установить в него из уровня приложении будет невозможно. Быстродействие здесь точно такое же как и у Си и у ассемблерного кода, все методы сделаны принудительно inline, поэтому вызов функции, например Set() даже в режиме без оптимизации преобразуется в простой вызов установки бита, например: полностью идентично строке: ну или на более привычном CMSIS варианте В принципе это вся идея, но тут меня посетила мысль, а что, если мне одновременно нужно установить (или режим поменять или сбросить) сразу несколько Pinов, находящихся на разных портах?  В качестве эксперимента, я взял свою плату, на ней 4 светодиода, и они как раз находятся на разных портах: GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9;
Первое что приходит в голову, это вот такой код: Вроде бы нормально, но, во-первых много кода, если Pinов будет 10, то придется 10 раз писать одно и то же — нехорошо. Поэтому я сделал класс PinsPack: После этого можно будет написать проще, оно тоже развернется в те же 4 строчки: Поэтому во вторых, такой код не оптимальный, ведь по сути мы можем сделать все установки в 2 строчки: А как это сделать на С++, я попробую описать в следующей статье. Хоккеист — на микроконтроллерах программист