Как стать автором
Обновить

Комментарии 39

Спасибо за статью.
Сам задумывался над чем-то подобным. Только знаний С++ для реализации не хватило.
Частично решал проблему с помощью макросов вроде:
#define WRITE_BITFIELD(MOD, N,  REG, FIELD, VAL)   { (MOD ## N)->REG = ((MOD ## N)->REG &= ~(MOD ## _ ## REG ## _ ## FIELD)) | \ (uint32_t)(VAL) << (MOD ## _ ## REG ## _ ## FIELD ## _ ## Pos); }

Что выливалось в код:
WRITE_BITFIELD(I2C, 1, TIMINGR, PRESC,  0x0B)

Не оптимальное решение, но от ошибок вроде записи не в тот регистр спасало.
Теперь можно и ряд других ошибок отлавливать.
Можно еще доработать подход и использовать его для работы с регистрами различных ИС подключаемых посредством I2C или SPI, или пр. Писать придется в переменную, а не по адресу. А уже потом эту переменную передавать.
18 линий ассемблера
/facepalm, с каким пор строки стали называться линиями?
Подправил.

Эта статья великолепна! У меня наконец появилась ссылка, которой можно кидать в тех, кто говорит, что плюсы это оверхед.

Данная статья не годится для кидания, т.к. почти все можно и макросами сделать (см первый коммент).
Мне больше понравилась вот эта статья трехлетней давности:
habr.com/ru/post/357910
Кидаться точно не стоит. Статья написана для того, чтобы показать преимущество строгой типизации С++ на Си. И я приверженец того, что макросы это зло: habr.com/ru/post/246971, так как по опыту знаю, что они источник потенциальных ошибок.
Я с вами согласен про макросы, я имел в виду, что те кто говорит что «плюсы оверхед, сишка рулит» обычно крайне, крайне неохотно будут читать любые опровержения. Более того, может сложиться ситуация что ты иллюстрируешь одно, а человек увидит в точности обратное.
Ваша статья прекрасна, я отметил что для общения с фанатиками она точно не подойдет.
ну, на самом деле и обычный c достаточен в основном, битовые поля есть… другое дело что с плюсами можно сделать чуть удобнее, но насчет безопаснее — не уверен…
Давно мечтал о такой штуке с zero-cost abstraction. Такая статическая проверка корректности намного лучше, чем те костыльные HAL библиотеки от ST, с которыми ошибки нужно ловить в рантайме. Они еще, к тому же, сильно раздувают бинарник. Помнится HAL для таймера вообще не влезал в чип со средним размером флеша, если не включить оптимизацию.
Да с HAL точно перебор по коду. Кроме того, там много ошибок, особенно с USB. Правда надо отдать должное, все исправляется довольно оперативно.
Насколько хорошо работает автокомплишин при использовании вашей библиотеки? Если он показывает поля, содержащиеся в регистре, а также возможные значения этих полей, то это очень круто.

Не думали-ли вы над реализаций различных представлений для одних и тех же полей? Например, в одних случаях порт ввода/вывода полезно представить как набор отдельных пинов. В других, на него может понадобиться выводить 8-битное целое число. Насколько я понимаю, сейчас доступен только второй вариант, в то время как со стандартным подходом можно реализовать оба варианта.
Ну это не совсем библиотека, это только подход, я его использую для обучения студентов. По поводу показывает ли среда поля. Это зависит от среды. Clion показывает все, т. е., там вообще думать не надо., но сам по себе он сыроват для того, чтобы полноценно использовать для разработки встроенного ПО. IAR, тоже показывает, но со странностями :)., иногда выдаёт какую-то чушь, но чаще работает нормально.
Про остальное пока не думал, задача была, по большому счету сделать так, чтобы студенты ошибок меньше делали. А то на это уходило много времени у меня, ещё ведь нормальная работа есть, помимо обучения. Хотелось минимизировать трату времени на студентов, которые документацию читают сквозь пальцы.
Работал с подобной библиотечкой для STM32, написанной шаблонным мастером. Перескажу впечатления: куча шаблонов быстро приводят код в нечитаемое состояние, низкая скорость компиляции, невозмножно дебажить — при попытке поставить бряк в шаблонную функцию ругается программатор, большая вложенность неймспейсов отбивает все желание набивать раз за разом одно и то же. Главное, что оно нисколько не помогает: код как был кучей записей в регистры, так им и остался. По мне лучше делать абстракции на уровне периферии целиком с дружелюбными названиями методов. А с регистрами советую не заморачиваться: чем их меньше торчит в коде, тем лучше.
Ну идея тут как раз и заключалась в том, чтобы шаблонами не злоупотреблять, для использования регистров — шаблонов нет уже:
#include "gpioaregisters.hpp" //for GPIOA
#include "rccregisters.hpp"   //for RCC

int main()
{
  RCC::AHB1ENR::GPIOAEN::Enable::Set() ;
  GPIOA::MODER::MODER15::Output::Set() ;  
}

Шаблон используется только при установке сразу нескольких битов, но опять же можно это перенести в функцию, чтобы было понятнее, тому кто с шаблонами не знаком, на Medium оптимизации уже точно передачи параметров через стек не будет, так как они могут быть вычислены компилятором.
моё почтенье после прочтения, было интересно и познавательно. но позволю себе пару вставить пару комментариев:
Без комментариев тут не обойтись. Хотя код всего-то устанавливает скорость работы порта GPIOA.0 в значение 40 Мгц

тут не совсем так, скорее даже совсем не так. Порт как работал со «скоростью» шины AHB так и будет работать. Регистр OSPEEDR определяет скорость нарастания фронта на выводе настроенном как выход. Кстати, в более новых поделиях этой фирмы, они заменили конкретные цифры на абстракции вида VeryLow\Low\Medium\High или их вариации.

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

я наверно Вас удивлю сейчас, но для некоторых МК st не то что не описали перечисления, они некоторые периферийные модули забыли описать. Так что полностью автоматизировать вряд ли получится. Для примера реальная история с stm32l4r4zi и попыткой найти DMAMUX в svd файле:

с момента публикации прошло полгода, исправления так и не было… за
это время были найдены ещё несколько ошибок в этом файле.
Спасибо за комментарий, про порты поправил, про SVD описание, согласен, ST как то очень легкомысленно к этому относится. Я предполагал, что они генерируют описания из каких-то инженерных документов, которые используются для производства микроконтроллера автоматически.
Кстати, я не смог и просто Си заголовочник найти для STM32L4.
и снова ошибка. не скорость переключения, а именно время нарастания фронта (то есть его крутизну). За примером в даташит, а именно поискать таблицу "I/O AC characteristics" и посмотреть там строки «Output rise and fall time».
Из-за своего названия этот регистр много путаницы вносит в понимание работы такого просто модуля, как GPIO.

как правило заголовочники можно вытащить из IDE, которая пользуется отдельными паками для вендоров (Keil/Segger Embedded Studio например), ну или выкачивая монструозные библиотеки HAL\LL на все семейство.

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

в принципе наверно можно и так сказать.
но опять же, смотрите. Вот выставили мы например значение 00 в регистр. Это же не означает, что МК не сможет и не позволит вывести через данный пин сигнал в 40МГц. Меандр будет на выходе, но очень уж близкий к синусу. Если Вам нужно работать с цифровым интерфейсом, то это проблема. Вот и получается, что это не сколько частота работы (максимальная\разрешенная), сколько признак того насколько меандр выведенный с данного пина будет меандром.

я надеюсь Вы понимаете что я не пытаюсь придираться)
Исправил по вашему комментарию. Конечно, понимаю, я за любое конструктивное обсуждение, спасибо, за пояснение.
Красивый метод, но не универсальный. Дело в том что кроме установки отдельных битов регистра — есть ещё одна зависимость, которую даже не сразу видно. Это внутренние триггеры запуска событий.
Например для таймера это поле называется CEN, и просто запускает таймер в работу. А вот для qspi есть сразу четыре внутренних триггера. Которые явно зависят от состояния полей. Установка QUADSPI_CR_EN — лишь разрешает работу этих триггеров, всё остальное требует строгой последовательности работы с регистрами и данными.
Причём для qspi, также как и для sd интерфейса — ошибки в алгоритме в работе с регистрами приводят к потери информации.

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

Пытался сделать что-то подобное, правда, дальше экспериментов дело не дошло.
Вместо параметра шаблона, определяющего режим доступа и enable_if банально наследовал регистры от от чего-то вроде RegR с определенным методом get и/или RegW с методом set.


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

template<uint32_t address, size_t size, typename AccessMode>
struct RegisterBase
наверно лучше
uintptr_t address
Вы имеете ввиду передавать в шаблон uintptr_t — это же указатель, т.е. к нему нужно будет привести, а чтобы привести опять таки будет использоваться reinterpret_cast, а его использовать в шаблонах нельзя? или приводить уже внутри к типу uintptr_t.
Я что-то не уловил. Можете уточнить. Кроме того, насколько я помню, этот тип поддерживается опционально, т.е. компилятор может его и не поддерживать.
Ну во первых этот тип специально сделан для представления указателя в виде числа. Имхо, хороший тон его использовать, даже если дальше reinterpret_cast будет такой-же.
Во вторых, будут проблемы, если ваш код когда-нибудь попадёт, например, на машину с 64-битной адресацией.
Насчёт поддержки в реальности на всех платформах не знаю, но если sizeof(uint32_t) == sizeof(void*), то не добавить sizeof(uintptr_t) == sizeof(uint32_t) как-то тупо.
:) Спасибо, за наводку, занятно. Если будет время, попробую поразбираться повнимательнее, взглянул мельком, выглядит многообещающе.
Затея неплохая, но централизации по шаблонизации не предвидится, а значит будет кто во что горазд.
Ну и выглядит это как «защита детей», когда на все что только можно клеятся защитные шалабушки, чтобы не ударился. В результате дети становятся непуганные и несамостоятельные. А если бы пару раз вдарившись, стали бы думать прежде чем делать.

Кроме того, производители софта не торопятся делиться им бесплатно. Откуда у вас KEIL и IAR на cm4? В суровой реальности, если софт не краденный, то это gcc + эклипс или иной редактор.
Откуда у вас KEIL и IAR на cm4? В суровой реальности, если софт не краденный, то это gcc + эклипс или иной редактор.
Всё не так однозначно, у IAR есть триальная лицензия с ограничением по размеру прошивки, пригодная для учебных целей.
IAR, конкретно у меня, лицензионный, а для студентов специальная версия с ограничением по размеру коду (30 кБ) — для учебных целей подходит.
Лицензионный софт — это хорошо, но 0 кило — мало же. А студенты будут уже подсажены на эту обёртку, получается.

Да, именно так, хочу посадить их на обертку над регистрами.

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

Защита — это хорошо. Я люблю защиту, потому что я знаю, что я невнимательный, attention span у меня так себе, и что я там делал и какие неявные предположения о состоянии я принимал полтора месяца назад, я могу и не помнить.


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

У меня тоже память как у золотой рыбки. Поэтому не жалею комментариев.

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

assert(value < ((1 << size) - 1)) ;

Не совсем понял, эта проверка для поля шириной, например, 3 бита не разрешит ввести значение 7. Так и задумано? Или я что-то недогнал по позднему времени?
НЛО прилетело и опубликовало эту надпись здесь
Сидел, несколько дней недель крутил эту идею и для себя сделал вот так:
template<std::uint32_t base_addr>
class PerBase
{
public:
	static inline void* get_ptr() { return (reinterpret_cast<void*>(base_addr)); }
};

template<typename BaseType, volatile std::uint32_t BaseType::periph_t::* field, typename RegType>
class RegBase
{
public:
	RegType	value;
	void Read()
	{
		value.dw = get_value();
	}
	void Write()
	{
		set_value(value.dw);
	}
	static std::uint32_t get_value()
	{
		return (reinterpret_cast<typename BaseType::periph_t*>(BaseType::get_ptr()))->*field;
	}
	static void set_value(std::uint32_t value)
	{
		(reinterpret_cast<typename BaseType::periph_t*>(BaseType::get_ptr()))->*field = value;
	}
};

template <typename Register, typename Type, std::uint32_t width, std::uint32_t offset>
class ValBase
{
private:
	constexpr static std::uint32_t Mask = ((1U << width) - 1U);
	constexpr static std::uint32_t Offset = offset;
public:
	static Type Get()
	{
		std::uint32_t value = Register::get_value();
		return static_cast<Type>((value >> Offset) & Mask);
	}
	static void Set(Type value)
	{
		std::uint32_t old_value = Register::get_value() & ~(Mask << Offset);
		std::uint32_t new_value = (static_cast<std::uint32_t>(value) << Offset);
		Register::set_value(old_value | new_value);
	}
};

/* Reset and clock control */
class RCC_REG : public PerBase<RCC_BASE>
{
public:
	struct periph_t
	{
		__O  std::uint32_t	CR;
		__O  std::uint32_t	CFGR;
		__O  std::uint32_t	CIR;
		__IO std::uint32_t	APB2RSTR;
		__IO std::uint32_t	APB1RSTR;
		__IO std::uint32_t	AHBENR;
		__IO std::uint32_t	APB2ENR;
		__IO std::uint32_t	APB1ENR;
		__O  std::uint32_t	BDCR;
		__O  std::uint32_t	CSR;
	};
	union CR_t
	{
		uint32_t dw;
		struct
		{
			bool          HSION          :  1;	/* Internal High Speed clock enable */
			bool          HSIRDY         :  1;	/* Internal High Speed clock ready flag */
			std::uint32_t                :  1;
			std::uint32_t HSITRIM        :  5;	/* Internal High Speed clock trimming */
			std::uint32_t HSICAL         :  8;	/* Internal High Speed clock Calibration */
			bool          HSEON          :  1;	/* External High Speed clock enable */
			bool          HSERDY         :  1;	/* External High Speed clock ready flag */
			bool          HSEBYP         :  1;	/* External High Speed clock Bypass */
			bool          CSSON          :  1;	/* Clock Security System enable */
			std::uint32_t                :  4;
			bool          PLLON          :  1;	/* PLL enable */
			bool          PLLRDY         :  1;	/* PLL clock ready flag */
			std::uint32_t                :  6;
		} bt;
	};
	union CFGR_t
	{
		struct
		{
			std::uint32_t SW             :  2;	/* System clock Switch */
			std::uint32_t SWS            :  2;	/* System Clock Switch Status */
			std::uint32_t HPRE           :  4;	/* AHB prescaler */
			std::uint32_t PPRE1          :  3;	/* APB Low speed prescaler (APB1) */
			std::uint32_t PPRE2          :  3;	/* APB High speed prescaler (APB2) */
			std::uint32_t ADCPRE         :  2;	/* ADC prescaler */
			std::uint32_t PLLSRC         :  1;	/* PLL entry clock source */
			std::uint32_t PLLXTPRE       :  1;	/* HSE divider for PLL entry */
			std::uint32_t PLLMUL         :  4;	/* PLL Multiplication Factor */
			std::uint32_t OTGFSPRE       :  1;	/* USB OTG FS prescaler */
			std::uint32_t                :  1;
			std::uint32_t MCO            :  3;	/* Microcontroller clock output */
			std::uint32_t                :  5;
		} bt;
		uint32_t dw;
	};
public:

	class CR : public RegBase<RCC_REG, &periph_t::CR, CR_t>
	{
	public:
		using HSION = ValBase<CR, bool, 1, 0>;
		using HSIRDY = ValBase<CR, bool, 1, 1>;
		using HSITRIM = ValBase<CR, std::uint32_t, 5, 3>;
	};

	/*Clock configuration register (RCC_CFGR)*/
	class CFGR : public RegBase<RCC_REG, &periph_t::CFGR, CFGR_t>
	{
	public:
		using SWS = ValBase<CFGR, std::uint32_t, 2, 2>;
	};
};

Как пример использования (абстрактный код, использовался для проверок компиляции):
	STMX::RCC_REG::CR cr;
	cr.Read();
	cr.value.bt.HSITRIM = 5;
	cr.value.bt.HSION = true;
	cr.Write();
	
	while (STMX::RCC_REG::CFGR::SWS::Get() == 1);

После компиляции получил следующее:
080000ec <App::entry_point()>:
_ZN3App11entry_pointEv():
 80000ec:	4907      	ldr	r1, [pc, #28]	; (800010c <App::entry_point()+0x20>)
 80000ee:	680b      	ldr	r3, [r1, #0]
 80000f0:	f003 0206 	and.w	r2, r3, #6
 80000f4:	f042 0229 	orr.w	r2, r2, #41	; 0x29
 80000f8:	f362 0307 	bfi	r3, r2, #0, #8
 80000fc:	4a04      	ldr	r2, [pc, #16]	; (8000110 <App::entry_point()+0x24>)
 80000fe:	600b      	str	r3, [r1, #0]
 8000100:	6813      	ldr	r3, [r2, #0]
 8000102:	f3c3 0381 	ubfx	r3, r3, #2, #2
 8000106:	2b01      	cmp	r3, #1
 8000108:	d0fa      	beq.n	8000100 <App::entry_point()+0x14>
 800010a:	e7fe      	b.n	800010a <App::entry_point()+0x1e>
 800010c:	40021000 	.word	0x40021000
 8000110:	40021004 	.word	0x40021004

Как ни крутил, так и не смог избавиться от хранения 2-х адресов в памяти.
Да, пока что без уникальных перечислений, но я только базис сделал.
Основная цель — уменьшение кода в более обычных сценариях вида «прочитать-модифицировать_поля-записать-модифицировать-записать».
Позже модифицирую свой генератор под данные шаблоны — удобнее, чем имеющиеся в данный момент.
И да — код не претендует на образцовость, поскольку с шаблонами знаком поверхностно…
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.