Я отказался от идеи тащить пины в классы UART, SPI и т.д. GPIO это
отдельный блок, настраиваю его сразу после тактирования один раз
проходясь по схеме. Бывает, что по ходу работы надо менять режимы
пинов, тогда это в общих алгоритмах через вызов методов класса. В
классах других периферийных (и внешних исполнительных) модулей
реализую только их логику. Например, в класс какого-нибудь
аттенюатора, висящего на SPI или I2C шаблонным параметром передаю
соответствующий порт и
пользуюсь его методами. Код такого аттенюатора легко инстанцируется на любом контроллере, где вы реализовали интерфейс к порту, да хоть ногодрыгом - логика исполнительного устройства от этого не меняется.
Чтобы это всё работало нужны некие "начальные вложения" в классы типовых портов. Чтобы этот подход пронизывал весь код. Зато потом это окупается скоростью разработки и лучшей оптимизацией кода.
consteval в C++20 не несёт ничего принципиально нового, это лишь дополнительное удобство. В С++17 чтобы гарантировать вычисление функции на этапе компиляции нужно было присвоить её результат в constexpr константу. А раз все делали это лишнее действие - получили consteval. Куда более мощные средства С++20 это концепты, диапазоны, больше constexpr классов в стандартной библиотеке, модули... Когда он придёт во все компиляторы будет почти "щастье".
constexpr уже в C++17 очень мощный инструмент. Можно навычислять чёрта лысого, инициализировать кучу структур и массивов. И если вы их используете полностью, компилятор положит их во флэш. А если только несколько элементов, то выкинет и оптимизирует до констант.