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

Оптимизированный доступ к GPIO и не только, часть вторая

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров6.7K

Уважаемые жители Habr‑а, В данной статье, речь пойдет о доступе к GPIO, и другим регистрам, используя C++.

Несколько лет назад, я подготовил статью, о том, как можно используя constexpr‑клаcсы, серьезно оптимизировать доступ к GPIO‑порту, таким образом, чтобы команда «PA0.set()» — превращалась в одну‑три ассемблерных инструкции, в зависимости от обстоятельств. Этот класс лишь выглядел обычным, который оптимизировался компилятором. Фактически, это была высокоуровневая оптимизация низкоуровневого кода. Теоретически возможно было перегрузить оператор равенства и писать просто PA0=1 или PA0=0, фактически это вызывало inline инструкцию, позволяющую добавить еще и барьерную инструкцию.

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

Итак. Задача состояла в том, чтобы максимально описать любой регистр, и получить доступ к нему, удобным способом. Я несколько раз пытался различными способами формализовать описание, и мои попытки привели меня к следующему коду.

// PERI_CRG_PLL0/PERI_CRG_PLL6 is APLL/VPLL configuration register 0/6.
struct PllConfig0 : public Description<0x12010000> {
        // [30:28] The second stage of the APLL outputs the frequency division factor.
        typedef RW< getAddress(), Field< 30, 28, uint8_t>> Postdiv2;
        // [26:24] APLL first-stage output frequency division coefficient.
        typedef RW< getAddress(), Field< 26, 24, uint8_t>> Postdiv1;
        // [23:0]  The fractional part of the APLL multiplier coefficient.
        typedef RW< getAddress(), Field< 23,  0, uint32_t>> Frac;
        // [27], [31] - Reservied fields
        typedef RS< getAddress(), Field< 27, 27 >, 
                RS< getAddress(), Field< 31, 31> > > Reserved;
};

Описание регистров привожу на примере регистров микросхемы hi3516ev200. И описания, найденного на китайском языке, где то на просторах сети Internet. Итак, для примера, выше, представлено опсиание битовых полей регистра PllConfig0, отвечающего за конфигурацию PLLA и расположенного по адресу 0×12 010 000. Существует аналогичный регистр PllConfig6, отвечающий за конфигурацию PLLV и расположенный по адресу 0×12 010 018. Его описание ничем не отличается, за исключением адреса, и выглядит следующим образом.

// PERI_CRG_PLL0/PERI_CRG_PLL6 is APLL/VPLL configuration register 0/6.
struct PllConfig6 : public Description<0x12010018> {
        // [30:28] The second stage of the APLL outputs the frequency division factor.
        typedef RW< getAddress(), Field< 30, 28, uint8_t>> Postdiv2;
        // [26:24] VPLL first-stage output frequency division coefficient.
        typedef RW< getAddress(), Field< 26, 24, uint8_t>> Postdiv1;
        // [23:0]  The fractional part of the VPLL multiplier coefficient.
        typedef RW< getAddress(), Field< 23,  0, uint32_t>> Frac;
        // [27], [31] - Reservied fields
        typedef RS< getAddress(), Field< 27, 27 >, 
                RS< getAddress(), Field< 31, 31> > > Reserved;
};

Итак, что представляют из себя указанные выше регистры? Это всего лишь три битовых поля: Postdiv2, Postdiv1, Frac. Поле Frac, занимает младшие 24 бита, поле Postdiv1 биты 24 25 и 26, а поле Postdiv2, биты с 28 по 30. Каждое поле, описывается при помощи typedef, или using. Я выбрал первый вариант, поскольку это заставляет вызываться проверки на этапе компиляции для всех полей, а не только для тех, которые используются. Был еще вариант, наследоваться от Field, но он тоже показался менее удобным.

т. е. люое поле регистра может быть описано следующим образом:

// Поле для чтения и записи
typedef RW <getAddress(), Field< HighBitOfField, LowBitOfField, FieldType>> NameReadWrite;
// Поле только для чтения
typedef RO <getAddress(), Field< HighBitOfField, LowBitOfField, FieldType>> NameReadOnly;
// Поле только для записи
typedef WO <getAddress(), Field< HighBitOfField, LowBitOfField, FieldType>> NameWriteOnly;
// Зарезервированное поле
typedef RS< getAddress(), Field< HighBitOfField, LowBitOfField >> Reserved;
// HighBitOfField - старший бит поля
// LowBitOfField - младший бит поля
// FieldType -можно указать тип поля

Возможность указать тип доступа, позволяет не инстанцировать некоторые функции, и таким образом, для поля только для чтения, будет отсутствовать функция записи. Описание зарезервированных полей, не сложнее, просто оно состоит из вложенных типов структур, и записывается во одну шаблонную строчку. Забегая вперед, замечу, что данное, поле, позволило, определять зарезервированные биты, и в случае доступа к отдельным полям регистра, определять все ли поля регистра используются. Так, если, Вы хотите записать Postdiv2, сформируется код по принципу: чтение => модификация => запись. Если Вы пожелаете записать все три поля: Postdiv2, Postdiv1, Frac. То чтение регистра, не требуется, т.к. все три поля будут перезаписаны. Однако, если не описать зарезервированные биты, то класс, не узнает о их существовании, и попытвается прочитать, изменить три поля, и записать регистр. Добавление зарезервированных полей, укажет о том, что Вы изменяете только три поля, а оставшиеся биты — зарезервированы, а значит можно не читать регистр, а сразу сформировать значение и записать это значение в регистр. Помимо экономии транзакций доступа к памяти, это позволяет избегать добавления барьерных инструкций чтения, если это необходимо.

Итак, как можно обращаться к указанным полям регистра? Если поставить Visual Studio Code, с IntelliSense, она подскажет формат достуа к каждому полю. Вероятно STM32CubeIde, сделает то же самое. Перейдем к использованию.

  1. Использвание непосредстваенно описания, и адреса по умолчанию:

auto pllConfig0Value = PllConfig0::Value::get(); // Прочитать регистр целиком
auto fracValue = PllConfig0::Frac::get(); // Прочитать только поле регистра Frac
PllConfig0::Frac::set(fracValue); // Записать значение в поле Frac
  1. Использование шаблонных функций для доступа не скольким полям одновременно:

  // Запись регистра целиком, или нескольких полей одновременно
  // Следует заметить, что в регистре не используются биты 27 и 31
  // Но благодаря, тому что мы описали их как зарезервированные, регистр
  // не будет прочитан из памяти, новое значение будет сформировано и
  // записано как одно 32-битное число.
  // Если мы уберем одно из полей, например Frac
  // Это приведет к процедуре чтение - модификация - запись,
  // поскольку, нам требуется сохранить значение Frac, таким,
  // каким оно было.
  Register::Write< PllConfig0,
					 PllConfig0::Frac,
					 PllConfig0::Postdiv1,
					 PllConfig0::Postdiv2 > (
						PllConfig0::Frac::Type(0),		// Frac = 0
						PllConfig0::Postdiv1::Type(2),	// 1800MHz / 2 = 900MHz
						PllConfig0::Postdiv2::Type(1)	// 900MHz / 1 = 900MHz
					 );

    // Чтение нескольких полей одновременно
    // В данном случае читаются одновременно поля postDiv1 и postDiv2
	PllConfig0::Postdiv1::Type postDiv1;
	PllConfig0::Postdiv2::Type postDiv2;
	Register::Read<PllConfig0,
				PllConfig0::Postdiv1,
				PllConfig0::Postdiv2>(
					postDiv1,
					postDiv2
				);
  1. Использование модифицированного адреса:

// В данном случае коду должен определять адрес регистра,
// но уже в Runtime-е. В некоторых случаях,
// это может происходить в Compile-Time, 
// например, если вы используете константный адрес, как здесь

// Класс с констурктором по умолчанию, используется адрес по умолчанию
// или 0x12010000, тот который был задан при описании регистра.
Register::Class<PllConfig0> pllConfig0;
// Класс интсанцируется с адресом 0x12010018, соответственно все обращения,
// будут происходить по этому адресу.
Register::Class<PllConfig0> pllConfig6(0x12010018);
// Следует заметить, что класс содержит лишь одну костатнту,
// Адрес регистра, остальные функции, компилируются как inline
// функции, что приводит как правило к формированию, 2-3  ассемблерных инструкций

// Прочитать регистр pllConfig0 целиком
auto pllConfig0Value = pllConfig0.Get<PllConfig0::Value>();
// Прочитать регистр pllConfig6 целиком. 
// Да!!!! Используется описание pllConfig0, для доступа по адресу
// регистра pllConfig6, поскольку они отличаются только адресами
auto pllConfig6Value = pllConfig6.Get<PllConfig0::Value>();

// Читаем регистра PllConfig6, с описанием PllConfig0
// Будет прочитан регистр целиком (32 бита),
// При необходимости, добавлены барьерные инструкции
// А затем будут выделены отдельные битовые поля
// frac и postdiv1
PllConfig0::Frac::Type      frac;
PllConfig0::Postdiv1::Type  postdiv1;
pllConfig6.Read<PllConfig0::Frac, PllConfig0::Postdiv1>( frac, postdiv1 );

// Все с точностью до наоборот, пишем два поля.
// Это приведет к четнию регистра по адрему 0x12010018
// Изменению двух полей и записи
pllConfig0.Write<PllConfig0::Frac, PllConfig0::Postdiv1>(frac, postdiv1);

// Запись уже трех полей в регистр
// Чтения уже не будет, будет только запись 
pllConfig0.Write<PllConfig0::Frac, PllConfig0::Postdiv1, PllConfig0::Postdiv2>(frac, postdiv1, 1);
  1. Строгая типизация. Преимущества строгой типизации соложно недооценить. Она, с одной стороны, уменьшает количество ошибок, а с другой, повышает читабельность кода. Я сейчас говорю, не только о типизации в целом, но и о применении enum class. Для того чтобы понять, это. Давайте посмотрим на более строгое описание регистра с типами, и доступ к нему.

// Это описание регистра из блока CRG,
// который отвечает за состояние PLL.
// т.е. определяет готова ли PLL
// PERI_CRG_PLL122 It is the PLL LOCK status register.
struct PllLockStatus : public Register::Description< 0x120101E8 > {
        // [2] VPLL LOCK state.
        // 0: Unlock; 1: Locked.
        enum class TVPll {
                Unlock,
                Locked
        };
        typedef RW< getAddress(), Bit<2, TVPll>> VPll;
        
        // [0] APLL LOCK state.
        // 0: Unlock; 1: Locked.
        enum class TAPll {
                Unlock,
                Locked
        };
        typedef RW< getAddress(), Bit<0, TAPll>> APll;

        // [31:3], [1] - Reserved
        typedef RS< getAddress(), Bit<1>,
                RS< getAddress(), Field<31,3>>> Reserved;

};

// Предположим, нам требуется подождать, пока PLLA будет готова.
// Это можно сделать так:
// Wait for PLLA is locked
while( PllLockStatus::APll::Type::Locked != PllLockStatus::APll::get() ) {};

// Однако, иногда, мы должны ждать готовности нескольких полей одновременно
// Например, пока обе PLL-ки не перейдут в состояние Locked
while( !Register::IsEqual<PllLockStatus, 
						   PllLockStatus::APll,
						   PllLockStatus::VPll>( 
							PllLockStatus::APll::Type::Locked,
							PllLockStatus::VPll::Type::Locked ) ){};

// В то же время, данный процессор, содержить регистры кофигруации Pll.
// Давайте, попроубем сконифгурировать частоты. 
// Или по другом, какой выход делителя PLL будет использоваться
// Тем или иным модулем системы
// Насколько читабельный код получается?????
// Кроме того, данный код, компилируется в минимальное количество инструкций
	// Switch all clocks to there frequences
	Register::Write< SocClkSel,
			SocClkSel::DdrClkSel,
			SocClkSel::CoreA7ClkSel,
			SocClkSel::SysApbClock,
			SocClkSel::SysAxiClk,
			SocClkSel::SysCfgClk
			 > (
				SocClkSel::DdrClkSel::Type::Freq300MHz,
				SocClkSel::CoreA7ClkSel::Type::Freq900MHz,
				SocClkSel::SysApbClock::Type::Freq50MHZ,
				SocClkSel::SysAxiClk::Type::Freq200MHz,
				SocClkSel::SysCfgClk::Type::Freq100MHz
			);
// А что еслим мы хотим переключить процессор обратно, на 24MHz
// Нам потребуется одна простая команда
SocClkSel::CoreA7ClkSel::set(SocClkSel::CoreA7ClkSel::Type::Freq24MHz);

Если попытаться скомпилировать код, используя несоответствующие типы, или другие обратиться к полям соседнего регистра, подрузумевая текущий, — код так же не скопмпилируется, поскольку осществляется проверка адресов. Позже подумаю как добавить проверку типов полей.

Если вы неверно укажате размерности полей, выйдите за границу регистра или перепутаете младший и старший бит, код так же не скомпилируется.

Есть еще одна фишка. Вы можете создать пересекающиеся поля. Например, описать два бита на порту GPIO, отвечающие за цвет двухцветного светодиода, назвать их, например Led1Red, Led1Green. А еще одним полем указать enum class, который будет использовать те‑же два поля, и описать три цвета и состояние выключено. Выглядеть это булет как то так:

struct LedGpio : public Register::Description< 0x000000114> {
        typedef RW< getAddress(), Bit<2>> Led1Red;
        typedef RW< getAddress(), Bit<3>> Led1Green;
        enum class TLed1Color {
            Off, // Альтернатива Dark
            Red,
            Green,
            Yellow // А может оранжевый а не желтый, вам виднее
        };
        typedef RW< getAddress(), Field<3,2, TLed1Color> > Led1;
};

// Можно включить зеленый
LedGpio::Led1Green::set(true);
// Или выключить 
LedGpio::Led1Green::set(false);

// Можно сразу включить зеленый и выключить красный
LedGpio::Led1::set(LedGpio::Led1::Type::Green);

// А как насчет желтого???? 
LedGpio::Led1::set(LedGpio::Led1::Type::Yellow);

// P.S.:
// Если твоя работа,- это твое хобби, то,
// ты не работаешь а получаешь удовольствие.....:-)
// С радиотехникой, я дружу приблизительно так же...

Осталось дописать, что я еще не до конца решил вопросы, касающиеся множественного доступа к полям, которые не являются RW (ReadWrite), однако, это дело техники, и вопрос времени.

Репозиторий доступен по ссылке:

https://github.com/hwswdevelop/MemoryMappedRegAccess

Репозиторий доступен по сслыке

Cсылка на предыдущую публикацию: Хабр (habr.com)

Теги:
Хабы:
Всего голосов 13: ↑11 и ↓2+13
Комментарии47

Публикации

Истории

Работа

QT разработчик
5 вакансий
Программист C++
118 вакансий

Ближайшие события

28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань