Top.Mail.Ru
Уведомления
Очистить все

[Закреплено] Сообщество | Библиотеки для STM32 на C++

Страница 3 / 3
(@eduard)
Level 5 Moderator

Запись от: @yumayuma

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

А это для меня вообще больное место. Не смог придумать ничего лучшего.

Если бы Вы мне с этим помогли, был бы очень благодарен.

Есть ещё один человек, у которого другой подход. winnie_the_poo.

Посмотрите его реализацию. По моему у него как раз так сделано как Вы сказали.

https://microtechnics.ru/profilegrid_blogs/stm32-perehodim-na-sovremennyj-c-chast-1-nastrojka-rabochego-prostranstva/#more-19465


ОтветитьЦитата
Создатель темы Размещено : 16.11.2025 11:00
(@yumayuma)
Level 1

И после того, как будут приведены в действительно С++-совый вид хотябы основные периферийные модули (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++! 
А всё, что касается отрисовки линий, прямоугольников, текста и прочего - это должно описываться совсем в других файлах, никак не связанных с дисплеем, и это тема совсем другого разговора. 

Вывод. Причина, по которой проделано описанное выше разделение - избавление от дублирования (копирования) в разных файлах общих для всех них участков кода. Файл дисплея описывает ТОЛЬКО функционал дисплея. Если сомневаетесь, что нужно писать в этом файле, откройте даташит дисплея - вы увидите список команд, которые как раз и нужно описать в файле. И ничего другого более!


ОтветитьЦитата
Размещено : 16.11.2025 11:20
(@yumayuma)
Level 1

@eduard Хм, а я думал, что за прошедшие несколько лет вы уже разобрались с этим 🙂 Если честно, я писал просто "вникуда", как бы отнефик делать 😀 Просто лазил по инету, наткнулся, поглазел, ну и написал то, что думаю.


ОтветитьЦитата
Размещено : 16.11.2025 11:22
(@yumayuma)
Level 1

@eduard

Запись от: @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 и прочее.


ОтветитьЦитата
Размещено : 16.11.2025 13:40
(@eduard)
Level 5 Moderator

Запись от: @yumayuma

в файл работы с дисплеем вписал и работу с GPIO и SPI, и даже отрисовку графики.

Здесь Вы немного неправы. У меня отдельная библиотека для работы с графикой и текстом. Основана на Adafruit_GFX. Я её "передрал" и добавил русификацию utf-8 символов.

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

Единственный драйвер, который не пользуется графической библиотекой, на RA8875. Так как его контроллер сам умеет всё рисовать.
Достаточно ему только давать координаты.

Кроме этого я очень плохой теоретик. Я больше практик. А код написанный на шаблонах отладке практически не поддаётся.

Запись от: @yumayuma

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

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

Я попытаюсь осмыслить Ваши советы. Вы не против, если буду обращаться к Вам за консультациями?

 

 


ОтветитьЦитата
Создатель темы Размещено : 16.11.2025 14:20
(@yumayuma)
Level 1

Запись от: @eduard

А код написанный на шаблонах отладке практически не поддаётся.

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

Запись от: @eduard

Вы не против, если буду обращаться к Вам за консультациями?

Нет, не против 🙂 Главное - не забыть на каком сайте я был.


ОтветитьЦитата
Размещено : 16.11.2025 15:50
(@yumayuma)
Level 1

Запись от: @eduard

А это для меня вообще больное место. Не смог придумать ничего лучшего.

Что касается настройки режима пинов, то просто выносите эти настройки за пределы функции настройки 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 };

ОтветитьЦитата
Размещено : 16.11.2025 15:51
(@eduard)
Level 5 Moderator

Запись от: @yumayuma

Нет, не против 🙂 Главное - не забыть на каком сайте я был

Может тогда Телега?


ОтветитьЦитата
Создатель темы Размещено : 16.11.2025 17:53
Страница 3 / 3
Поделиться:
Обзор конфиденциальности

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