Особенно лишнее здесь GPIO. Просто потому, что I2C, как и многие другие модули, не фиксированы жестко на конкретных пинах, они могут быть перенесены. Следовательно, привязываться к пинам в функции настройки модуля - совершенно излишне.
А это для меня вообще больное место. Не смог придумать ничего лучшего.
Если бы Вы мне с этим помогли, был бы очень благодарен.
Есть ещё один человек, у которого другой подход. winnie_the_poo.
Посмотрите его реализацию. По моему у него как раз так сделано как Вы сказали.
И после того, как будут приведены в действительно С++-совый вид хотябы основные периферийные модули (GPIO, SPI, I2C, DMA) можно будет приступить к разбирательству с дисплеем. У автора - снова типичная ошибка ардуинщика - в файл работы с дисплеем вписал и работу с GPIO и SPI, и даже отрисовку графики. Из-за этого автору приходится для каждого дисплея писать отдельную библиотеку со всей требухой, и бОльшая часть её будет копироваться без изменений из файла в файл. Весьма неудобная вещь, которая пошла из демо-примеров к дисплеям. В тех примерах кидали всё в одну кучу просто для демонстрации работы, но никак не как руководство к действию.
Таким образом, файл для дисплея должен содержать исключительно инструментарий для управления дисплеем через какой-либо интерфейс, но ни работы интерфейса, ни построителя графики (прямоугольников, линий, текста) в этом файле быть не должно.
Для нашей задачи хорошо подойдет шаблонный класс:
template<typename SPI, typename CS, typename DC> class Ssd1306;
в параметры шаблона которого мы передадим написанные ранее классы для SPI и GPIO:
using display = Ssd1306<Spi1, GpioA::pin4, GpioA::pin6>;
Причем, для дисплея типа SSD1306, который может работать как по SPI, так и по I2C, можем предусмотреть два варианта интерфейса, переписав иначе:
template<typename Ifc> class Ssd1306: public Ifc; template<typename SPI, typename CS, typename DC> class SpiIfc; template<typename I2C, uint8_t SlaveAddress> class I2cIfc;
и тогда класс Ssd1306 не придется переписывать под каждый тип интерфейса, дублируя текст.
Работа с дисплеем будет выглядеть в этом случае так:
using spi = SpiIfc<Spi1, GpioA::pin4, GpioA::pin6>; using display = Ssd1306<spi>; display::Open(); display::Init<VertDir::UP, HorizDir::LEFT>(); display::SetContrast(20); display::PowerOn(); display::Close();
Методы Open() и Close() прописаны в классах SpiIfc и I2cIfc и означают начало (открытие) и конец (закрытие) сеанса связи. Для интерфейса SPI это дергание ножкой CS, а для I2C - генерацию Start и Stop, а так же передачу адреса слейва.
Если у дисплея есть только один тип интерфейса, можно писать как в первом варианте.
В классе дисплея Ssd1306 пропишем СТАТИЧЕСКИЕ методы, реализующие взаимодействие непосредственно с дисплеем:
template<VertDir VD, HorizDir HD> static void Init(); static void PowerOn(); static void PowerOff(); static void SetContrast(uint8_t value); static void SetColumn(uint8_t column); static void SetPage(uint8_t page);
и так далее.
Вот это уже будет человеческий, куда более правильный код на C++!
А всё, что касается отрисовки линий, прямоугольников, текста и прочего - это должно описываться совсем в других файлах, никак не связанных с дисплеем, и это тема совсем другого разговора.
Вывод. Причина, по которой проделано описанное выше разделение - избавление от дублирования (копирования) в разных файлах общих для всех них участков кода. Файл дисплея описывает ТОЛЬКО функционал дисплея. Если сомневаетесь, что нужно писать в этом файле, откройте даташит дисплея - вы увидите список команд, которые как раз и нужно описать в файле. И ничего другого более!
@eduard Хм, а я думал, что за прошедшие несколько лет вы уже разобрались с этим 🙂 Если честно, я писал просто "вникуда", как бы отнефик делать 😀 Просто лазил по инету, наткнулся, поглазел, ну и написал то, что думаю.
Посмотрите его реализацию. По моему у него как раз так сделано как Вы сказали.
Вкратце глянул. К сожалению, несмотря на применение шаблонов для передачи параметров, есть нелогичности в структуре кода. Как будто автор что-то не до конца понял - шаблоны то применил, а всё остальное - написал как попало.
Сделаем на примере SPI:
Создаем файл Spi.hpp (лично я указываю расширение файла .hpp вместо .h просто для ясности, поскольку в языке С++ изменено назначение заголовочного файла .h)
В нем будет класс описания модулей SPIx.
Вычисление базового адреса регистров SPI выполняется во время компиляции по параметру шаблона класса, и этим заведует статическая constexpr-функция в private-области класса.
Остальные методы, тоже статические, размещены в public-области класса и реализуют элементарный функционал модуля.
Для создания "красивости и порядка" в именах используются namespace. В рассматриваемом примере их два: namespace HAL - общее имя Hardware Abstraction Layer (именно так расшифровывается HAL), и вложенное в него namespace SPI.
Для лучшего структурирования в этот класс вложены внутренние классы struct Flags, struct Irq, struct Dma.
Конфигурация SPI передается в параметрах шаблонного метода Init(). Параметры представлены в виде enum class.
Здесь я немного сократил код, иначе простыня длинная.
В конце using Spi1 = Spi<1> создает псевдоним Spi1, эквивалентный записи Spi<1> для более компактной записи.
#include "stm32f1xx.h"
namespace HAL {
namespace SPI {
enum class Role {MASTER, SLAVE};
enum class ClockDiv {_2, _4, _8 /* и так далее */};
template<int N>
class Spi {
public:
template<Role Role, ClockDiv BR /* и так далее */ >
static void Init()
{
GetBase()->CR1 = static_cast<uint16_t>(Role) |
static_cast<uint16_t>(BR);
}
static void Enable()
{
GetBase()->CR1 |= SPI_CR1_SPE;
}
static void Disable()
{
GetBase()->CR1 &= ~SPI_CR1_SPE;
}
static bool IsEnable()
{
return GetBase()->CR1 & SPI_CR1_SPE;
}
static void WriteDR(uint16_t data)
{
GetBase()->DR = data;
}
static uint16_t ReadDR()
{
return GetBase()->DR;
}
static void Send(uint16_t data)
{
while(Flags::IsTXE() != true);
WriteDR(data);
}
static uint16_t Receive()
{
while(Flags::IsRXNE() != true);
return ReadDR();
}
static void WaitTxComplete()
{
while(Flags::IsTXE() != true);
while(Flags::IsBusy() == true);
}
struct Flags {
static bool IsTXE()
{
return GetBase()->SR & SPI_SR_TXE;
}
static bool IsRXNE();
static bool IsBusy();
static bool IsOverrun();
static bool IsModeFault();
static bool IsCrcError();
};
struct Irq {
struct Tx {
static void Enable();
static void Disable();
static bool IsEnable();
};
struct Rx {
static void Enable();
static void Disable();
static bool IsEnable();
};
struct Error {
static void Enable();
static void Disable();
static bool IsEnable();
};
};
struct Dma {
struct Tx {
static void Enable();
static void Disable();
static bool IsEnable();
};
struct Rx {
static void Enable();
static void Disable();
static bool IsEnable();
};
};
private:
static constexpr SPI_TypeDef* GetBase()
{
#ifdef SPI1
if constexpr (N == 1) return SPI1;
#endif
#ifdef SPI2
if constexpr (N == 2) return SPI2;
#endif
/* и так далее */
}
};
#ifdef SPI1
using Spi1 = Spi<1>;
#endif
#ifdef SPI2
using Spi2 = Spi<2>;
#endif
/* и так далее */
Описания параметров конфигурации enum class можно поместить и внутрь класса Spi. Лично мне больше нравится снаружи, чтобы меньше букв писать потом. Но это вопрос чисто эстетический, у обоих вариантов есть свои плюсы и минусы.
static_cast<uint16_t>(Role) - это плюшка языка С++, указывающая на явное преобразование типа enum class Role к типу uint16_t.
Можно записать и покороче в стиле Си: (uint16_t)Role
Применение класса:
using namespace HAL::SPI подключает пространство имен HAL::SPI, чтобы его не нужно было каждый раз писать перед Spi1.
using namespace HAL::SPI; Spi1::Init<Role::MASTER, ClockDiv::_4>(); Spi1::Enable(); Spi1::Send(0xAF); Spi1::WaitTxComplete(); Spi1::Dma::Tx::Enable();
В таком подходе главная затея состоит в вычислении базового адреса регистров SPI1, SPI2 и тд во время компиляции, без влияния на скорость работы кода. При этом не требуется писать отдельного класса для каждого SPI или передавать базовый адрес в параметрах. Так же созданы функциональные обертки для работы с регистрами периферии, что положительно влияет на структурированность и независимость кода. А включение оптимизации -O2 или -O3 уберет вызовы методов и сделает максимально быстрый код, как если бы писали напрямую обращения к регистрам.
Аналогично можно описать и остальную периферию - GPIO, I2C, UART и прочее.
в файл работы с дисплеем вписал и работу с GPIO и SPI, и даже отрисовку графики.
Здесь Вы немного неправы. У меня отдельная библиотека для работы с графикой и текстом. Основана на Adafruit_GFX. Я её "передрал" и добавил русификацию utf-8 символов.
В драйвере дисплея только его инициализация и функции, которые могут отличаться от контроллера к контроллеру.
Единственный драйвер, который не пользуется графической библиотекой, на RA8875. Так как его контроллер сам умеет всё рисовать.
Достаточно ему только давать координаты.
Кроме этого я очень плохой теоретик. Я больше практик. А код написанный на шаблонах отладке практически не поддаётся.
Хм, а я думал, что за прошедшие несколько лет вы уже разобрались с этим
Всё, что лежит на сайте, давно устарело. Я уже начал разделять функционал как Вы говорите. Но мне некогда стало писать новые статьи и выкладывать новые библиотеки. Я больше схемотехник и теперь занимаюсь своей основной специальностью. Надеюсь после окончания шухера и выхода на пенсию смогу заняться этим более плотно.
Я попытаюсь осмыслить Ваши советы. Вы не против, если буду обращаться к Вам за консультациями?
А код написанный на шаблонах отладке практически не поддаётся.
Вполне поддается. Единственная проблема - это проблема самого языка С++ в отношении шаблонов и метапрограммирования - немалый список ограничений и невнятный инструментарий отладки. Например, переданные в шаблоне функции параметры можно проверить только косвенно, либо вводя промежуточные переменные для отладки.
Вы не против, если буду обращаться к Вам за консультациями?
Нет, не против 🙂 Главное - не забыть на каком сайте я был.
А это для меня вообще больное место. Не смог придумать ничего лучшего.
Что касается настройки режима пинов, то просто выносите эти настройки за пределы функции настройки SPI или прочего. Вариант с вложенной настройкой пинов происходил из ложного убеждения, что дескать можно забыть настроить пины или забыть включить тактирование. Но ведь дело то в том, что SPI и ему подобные могут переназначаться на разные пины. И уж лучше оставить настройку пинов вне функции настройки SPI, чем городить излишний огород.
Касательно класса GPIO. Здесь есть некоторые неудобства, что GPIO - это как целый порт, так и одиночные пины в разных их комбинациях. Поэтому тут могут быть разные взгляды на описание GPIO. Я видел и перепробовал немало вариантов, и на сегодняшний момент пришел к некоему среднему, компромиссному варианту. Он неидеален (как впрочем и предыдущие), но вполне себе неплохо работает. Главное - работает быстро.
Основа та же самая - шаблонный класс Gpio, параметр которого - буква имени порта. Эту букву можно задать непосредственно ANSI-символом: template<char Letter> class Gpio; и прописать псевдоним using GpioA = Gpio<'A'>;
Но можно завести enum class Letter {A, B, C /*и так далее */} и написать template<Letter Name> class Gpio; using GpioA = Gpio<Letter::A>;
В свете того, что будут использоваться псевдонимы GpioA, GpioB и тд большой разницы в этих вариантах нет. Во втором варианте более строгая типизация шаблона, и при написании не ошибешься буквой. Впрочем, как уже сказал, в основном то используются заданные псевдонимы имени.
namespace HAL {
namespace GPIO {
enum class Letter {A, B, C /* и так далее */ };
template<Letter Name>
class Gpio {
public:
template<uint16_t Mask>
static void SetPins()
{
GetBase()->BSRR = Mask;
}
template<uint16_t Mask>
static void ResetPins();
template<uint16_t Mask>
static void TogglePins();
template<uint16_t Mask>
static void WritePins(uint16_t value);
static void WritePort(uint16_t value);
static uint16_t ReadPort();
template<uint8_t N>
struct Pin {
static constexpr uint16_t mask = 1 << N;
static void Set()
{
SetPins<mask>();
}
static void Reset();
static void Toggle();
enum class State {
RESET = 0,
SET = mask
};
static State Read();
};
using Pin0 = Gpio<Name>::Pin<0>;
using Pin1 = Gpio<Name>::Pin<1>;
private:
static constexpr GPIO_TypeDef* GetBase()
{
#ifdef GPIOA
if constexpr (Name == Letter::A) return GPIOA;
#endif
/* и так далее */
}
};
Класс Gpio содержит вложенный класс template<uint8_t N> Pin, который описывает отдельный пин порта. Обращение к этому пину можно выполнять так:
GpioA::Pin0::Set(); GpioA::Pin0::Reset(); GpioA::Pin0::Set(); GpioA::Pin0::Reset();
что при включенной оптимизации -O3 будет преобразовано в весьма короткий ассемблерный код:
movs r1, #1 mov.w r2, #65536 ldr r3, [pc, #12] movs r0, #0 str r1, [r3, #16] str r2, [r3, #16] str r1, [r3, #16] str r2, [r3, #16]
А вот что касается конфигурации пинов, тут есть некоторая замута. Впрочем, не столь уж и сложная.
Лично я пошел по пути того, что описал свойства пинов раздельно для входа и для выхода, ибо они действительно разные.
enum class InType { HiZ, PU, PD };
enum class OutType { PP, OD };
enum class Speed { LOW, MED, HIGH, VHIGH };
